diff --git a/README.md b/README.md index c719566..8b20754 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# たりない観察 v39.18.3 +# たりない観察 v39.18.6 ## 実績システム @@ -12,7 +12,7 @@ 実績状態とサーバー資格情報は、通常保存、予備保存、同一タブ保存、IndexedDBへ世代付きで保存します。起動時は同じ世代の状態を追加方向に統合し、明示的な実績リセット時だけ世代を進めます。 -v39.18.3は、v39.18.0以前の実績保存、旧ワールド実績メタデータ、実績スペルv9・v10、旧バイナリセーブを読み込めます。旧データは現行形式へ正規化され、新規保存には廃止済みフィールドを書きません。 +v39.18.6は、v39.18.0以前の実績保存、旧ワールド実績メタデータ、実績スペルv9・v10、旧バイナリセーブを読み込めます。旧データは現行形式へ正規化され、新規保存には廃止済みフィールドを書きません。 ## サーバー同期 @@ -25,7 +25,7 @@ v39.18.3は、v39.18.0以前の実績保存、旧ワールド実績メタデー - 古い端末からリセット前の状態を復活させない - プレイヤーIDと所有秘密鍵を耐久保存から復旧 -共有達成率は、サーバーがゲームシミュレーションを再実行した検証値ではなく、クライアント端末の報告値です。UIとAPIでは「端末報告ベース」と明示します。 +共有達成率は、サーバーがゲームシミュレーションを再実行した検証値ではなく、クライアント端末の報告値です。UIでは共有値を「達成率」「達成人数」と表示します。 ## 旧サーバーデータの自動移行 @@ -42,7 +42,7 @@ v39.18.3は、v39.18.0以前の実績保存、旧ワールド実績メタデー 旧ブラウザーに22文字のプレイヤーIDが残っている場合、クライアントはUUIDへ上書きする前に専用領域へ退避し、初回セッションで一度だけ新しい所有秘密鍵へ引き継ぎます。引継ぎ済みの旧IDを別の秘密鍵で再取得することはできません。 -旧版から直接v39.18.3へ更新したブラウザーは自動引継ぎできます。v39.18.1を既に起動して旧22文字IDがブラウザーから失われている場合、サーバーデータだけから端末との本人対応を再構成することはできません。解除履歴と共有集計自体は正常に移行されます。 +旧版から直接v39.18.6へ更新したブラウザーは自動引継ぎできます。v39.18.1を既に起動して旧22文字IDがブラウザーから失われている場合、サーバーデータだけから端末との本人対応を再構成することはできません。解除履歴と共有集計自体は正常に移行されます。 ## 実績APIの保存先 diff --git a/app_manifest.json b/app_manifest.json index 30886fe..1f598cf 100644 --- a/app_manifest.json +++ b/app_manifest.json @@ -1,5 +1,5 @@ { - "version": "39.18.3", + "version": "39.18.18", "css": [ "css/base.css", "css/layout.css", diff --git a/assets/objects/mamekusaredake.png b/assets/objects/mamekusaredake.png new file mode 100644 index 0000000..254f5b2 Binary files /dev/null and b/assets/objects/mamekusaredake.png differ diff --git a/assets/sounds/acquired_stress_001.wav b/assets/sounds/acquired_stress_001.wav new file mode 100644 index 0000000..81ca9c2 Binary files /dev/null and b/assets/sounds/acquired_stress_001.wav differ diff --git a/assets/sounds/acquired_stress_002.wav b/assets/sounds/acquired_stress_002.wav new file mode 100644 index 0000000..fea55d0 Binary files /dev/null and b/assets/sounds/acquired_stress_002.wav differ diff --git a/assets/sounds/acquired_stress_003.wav b/assets/sounds/acquired_stress_003.wav new file mode 100644 index 0000000..8f57321 Binary files /dev/null and b/assets/sounds/acquired_stress_003.wav differ diff --git a/assets/sounds/acquired_stress_004.wav b/assets/sounds/acquired_stress_004.wav new file mode 100644 index 0000000..110d484 Binary files /dev/null and b/assets/sounds/acquired_stress_004.wav differ diff --git a/assets/sounds/acquired_stress_005.wav b/assets/sounds/acquired_stress_005.wav new file mode 100644 index 0000000..eee7794 Binary files /dev/null and b/assets/sounds/acquired_stress_005.wav differ diff --git a/assets/sounds/acquired_stress_006.wav b/assets/sounds/acquired_stress_006.wav new file mode 100644 index 0000000..9311ed7 Binary files /dev/null and b/assets/sounds/acquired_stress_006.wav differ diff --git a/assets/sounds/acquired_stress_007.wav b/assets/sounds/acquired_stress_007.wav new file mode 100644 index 0000000..dd56594 Binary files /dev/null and b/assets/sounds/acquired_stress_007.wav differ diff --git a/assets/sounds/acquired_stress_008.wav b/assets/sounds/acquired_stress_008.wav new file mode 100644 index 0000000..9585451 Binary files /dev/null and b/assets/sounds/acquired_stress_008.wav differ diff --git a/assets/sounds/acquired_stress_009.wav b/assets/sounds/acquired_stress_009.wav new file mode 100644 index 0000000..f6c07dc Binary files /dev/null and b/assets/sounds/acquired_stress_009.wav differ diff --git a/assets/sounds/acquired_tarinai/001_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲繝代ヱ縺壹s縺ッ縺ゥ縺薙↑縺ョ窶ヲ(1).wav b/assets/sounds/acquired_tarinai/001_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲繝代ヱ縺壹s縺ッ縺ゥ縺薙↑縺ョ窶ヲ(1).wav new file mode 100644 index 0000000..81ca9c2 Binary files /dev/null and b/assets/sounds/acquired_tarinai/001_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲繝代ヱ縺壹s縺ッ縺ゥ縺薙↑縺ョ窶ヲ(1).wav differ diff --git a/assets/sounds/acquired_tarinai/002_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲繝槭・縺壹s縺ッ縺ゥ縺薙↑縺ョ窶ヲ(2).wav b/assets/sounds/acquired_tarinai/002_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲繝槭・縺壹s縺ッ縺ゥ縺薙↑縺ョ窶ヲ(2).wav new file mode 100644 index 0000000..fea55d0 Binary files /dev/null and b/assets/sounds/acquired_tarinai/002_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲繝槭・縺壹s縺ッ縺ゥ縺薙↑縺ョ窶ヲ(2).wav differ diff --git a/assets/sounds/acquired_tarinai/003_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺薙s縺ェ縺ッ縺壹§繧・↑縺銀€ヲ(2).wav b/assets/sounds/acquired_tarinai/003_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺薙s縺ェ縺ッ縺壹§繧・↑縺銀€ヲ(2).wav new file mode 100644 index 0000000..8f57321 Binary files /dev/null and b/assets/sounds/acquired_tarinai/003_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺薙s縺ェ縺ッ縺壹§繧・↑縺銀€ヲ(2).wav differ diff --git a/assets/sounds/acquired_tarinai/004_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺ゥ縺・@縺ヲ(2).wav b/assets/sounds/acquired_tarinai/004_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺ゥ縺・@縺ヲ(2).wav new file mode 100644 index 0000000..110d484 Binary files /dev/null and b/assets/sounds/acquired_tarinai/004_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺ゥ縺・@縺ヲ(2).wav differ diff --git a/assets/sounds/acquired_tarinai/005_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺翫∪縺医r繧・k縺輔↑縺・€ヲ(2).wav b/assets/sounds/acquired_tarinai/005_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺翫∪縺医r繧・k縺輔↑縺・€ヲ(2).wav new file mode 100644 index 0000000..eee7794 Binary files /dev/null and b/assets/sounds/acquired_tarinai/005_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺翫∪縺医r繧・k縺輔↑縺・€ヲ(2).wav differ diff --git a/assets/sounds/acquired_tarinai/006_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺ョ繧阪▲縺ヲ繧・k縺ョ縺(2).wav b/assets/sounds/acquired_tarinai/006_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺ョ繧阪▲縺ヲ繧・k縺ョ縺(2).wav new file mode 100644 index 0000000..9311ed7 Binary files /dev/null and b/assets/sounds/acquired_tarinai/006_縺壹s縺繧ゅs・医ヮ繝シ繝槭Ν・雲縺ョ繧阪▲縺ヲ繧・k縺ョ縺(2).wav differ diff --git a/assets/sounds/intimidate_alt_001.wav b/assets/sounds/intimidate_alt_001.wav new file mode 100644 index 0000000..146764b Binary files /dev/null and b/assets/sounds/intimidate_alt_001.wav differ diff --git a/assets/sounds/ochibi_calm_001.wav b/assets/sounds/ochibi_calm_001.wav new file mode 100644 index 0000000..6ec4cb2 Binary files /dev/null and b/assets/sounds/ochibi_calm_001.wav differ diff --git a/assets/sounds/ochibi_calm_002.wav b/assets/sounds/ochibi_calm_002.wav new file mode 100644 index 0000000..f0bb63f Binary files /dev/null and b/assets/sounds/ochibi_calm_002.wav differ diff --git a/assets/sounds/ochibi_calm_003.wav b/assets/sounds/ochibi_calm_003.wav new file mode 100644 index 0000000..ec88928 Binary files /dev/null and b/assets/sounds/ochibi_calm_003.wav differ diff --git a/assets/sounds/ochibi_calm_004.wav b/assets/sounds/ochibi_calm_004.wav new file mode 100644 index 0000000..ee10326 Binary files /dev/null and b/assets/sounds/ochibi_calm_004.wav differ diff --git a/assets/sounds/ochibi_calm_005.wav b/assets/sounds/ochibi_calm_005.wav new file mode 100644 index 0000000..bd71d76 Binary files /dev/null and b/assets/sounds/ochibi_calm_005.wav differ diff --git a/assets/sounds/ochibi_calm_006.wav b/assets/sounds/ochibi_calm_006.wav new file mode 100644 index 0000000..1d5c55a Binary files /dev/null and b/assets/sounds/ochibi_calm_006.wav differ diff --git a/assets/sounds/ochibi_calm_007.wav b/assets/sounds/ochibi_calm_007.wav new file mode 100644 index 0000000..8606b70 Binary files /dev/null and b/assets/sounds/ochibi_calm_007.wav differ diff --git a/assets/sounds/ochibi_calm_008.wav b/assets/sounds/ochibi_calm_008.wav new file mode 100644 index 0000000..624d717 Binary files /dev/null and b/assets/sounds/ochibi_calm_008.wav differ diff --git a/assets/sounds/ochibi_calm_009.wav b/assets/sounds/ochibi_calm_009.wav new file mode 100644 index 0000000..8fbb49c Binary files /dev/null and b/assets/sounds/ochibi_calm_009.wav differ diff --git a/assets/sounds/ochibi_damage_001.wav b/assets/sounds/ochibi_damage_001.wav new file mode 100644 index 0000000..5a774bd Binary files /dev/null and b/assets/sounds/ochibi_damage_001.wav differ diff --git a/assets/sounds/ochibi_damage_002.wav b/assets/sounds/ochibi_damage_002.wav new file mode 100644 index 0000000..5f8fb15 Binary files /dev/null and b/assets/sounds/ochibi_damage_002.wav differ diff --git a/assets/sounds/ochibi_damage_003.wav b/assets/sounds/ochibi_damage_003.wav new file mode 100644 index 0000000..8ff254b Binary files /dev/null and b/assets/sounds/ochibi_damage_003.wav differ diff --git a/assets/sounds/ochibi_dirty_001.wav b/assets/sounds/ochibi_dirty_001.wav new file mode 100644 index 0000000..cdaf92b Binary files /dev/null and b/assets/sounds/ochibi_dirty_001.wav differ diff --git a/assets/sounds/ochibi_dirty_002.wav b/assets/sounds/ochibi_dirty_002.wav new file mode 100644 index 0000000..dfa73a9 Binary files /dev/null and b/assets/sounds/ochibi_dirty_002.wav differ diff --git a/assets/sounds/ochibi_dirty_003.wav b/assets/sounds/ochibi_dirty_003.wav new file mode 100644 index 0000000..8470f4a Binary files /dev/null and b/assets/sounds/ochibi_dirty_003.wav differ diff --git a/assets/sounds/ochibi_dirty_004.wav b/assets/sounds/ochibi_dirty_004.wav new file mode 100644 index 0000000..8d48ae6 Binary files /dev/null and b/assets/sounds/ochibi_dirty_004.wav differ diff --git a/assets/sounds/ochibi_disease_001.wav b/assets/sounds/ochibi_disease_001.wav new file mode 100644 index 0000000..f486513 Binary files /dev/null and b/assets/sounds/ochibi_disease_001.wav differ diff --git a/assets/sounds/ochibi_electric_006.wav b/assets/sounds/ochibi_electric_006.wav new file mode 100644 index 0000000..6706edc Binary files /dev/null and b/assets/sounds/ochibi_electric_006.wav differ diff --git a/assets/sounds/ochibi_fight_hit_004.wav b/assets/sounds/ochibi_fight_hit_004.wav new file mode 100644 index 0000000..b4c99fa Binary files /dev/null and b/assets/sounds/ochibi_fight_hit_004.wav differ diff --git a/assets/sounds/ochibi_hot_007.wav b/assets/sounds/ochibi_hot_007.wav new file mode 100644 index 0000000..9a334a0 Binary files /dev/null and b/assets/sounds/ochibi_hot_007.wav differ diff --git a/assets/sounds/ochibi_panic_005.wav b/assets/sounds/ochibi_panic_005.wav new file mode 100644 index 0000000..e8dbbf2 Binary files /dev/null and b/assets/sounds/ochibi_panic_005.wav differ diff --git a/assets/sounds/ochibi_sleep_002.wav b/assets/sounds/ochibi_sleep_002.wav new file mode 100644 index 0000000..24eba42 Binary files /dev/null and b/assets/sounds/ochibi_sleep_002.wav differ diff --git a/assets/sounds/ochibi_sleep_003.wav b/assets/sounds/ochibi_sleep_003.wav new file mode 100644 index 0000000..6ab4744 Binary files /dev/null and b/assets/sounds/ochibi_sleep_003.wav differ diff --git a/assets/sounds/ochibi_stress_008.wav b/assets/sounds/ochibi_stress_008.wav new file mode 100644 index 0000000..d0650a9 Binary files /dev/null and b/assets/sounds/ochibi_stress_008.wav differ diff --git a/assets/sprites/tarinai_40_intimidate_alt.webp b/assets/sprites/tarinai_40_intimidate_alt.webp new file mode 100644 index 0000000..07e2454 Binary files /dev/null and b/assets/sprites/tarinai_40_intimidate_alt.webp differ diff --git a/assets/sprites/tarinai_acquired_overlay.png b/assets/sprites/tarinai_acquired_overlay.png new file mode 100644 index 0000000..3afc640 Binary files /dev/null and b/assets/sprites/tarinai_acquired_overlay.png differ diff --git a/assets/ui/tool_acquired_tarinai.webp b/assets/ui/tool_acquired_tarinai.webp new file mode 100644 index 0000000..1b23823 Binary files /dev/null and b/assets/ui/tool_acquired_tarinai.webp differ diff --git a/assets/ui/tool_mamekusaredake.webp b/assets/ui/tool_mamekusaredake.webp new file mode 100644 index 0000000..6b63693 Binary files /dev/null and b/assets/ui/tool_mamekusaredake.webp differ diff --git a/css/base.css b/css/base.css index 1fbdde1..03a8520 100644 --- a/css/base.css +++ b/css/base.css @@ -78,10 +78,19 @@ button { min-width: 0; } +.room-links { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 6px; + flex: 0 0 auto; +} + .room-link { text-decoration: none; white-space: nowrap; background: rgba(255,255,255,0.86); + text-align: center; } h1 { @@ -386,6 +395,7 @@ h1 { word-break: normal; overflow-wrap: anywhere; text-shadow: 0 1px 0 rgba(255,255,255,0.62); + white-space: pre-line; } .tool.tool-has-canvas-icon { --tool-icon: none; grid-template-columns: 31px minmax(0,1fr); min-height: 47px; padding-top: 4px; } diff --git a/index.html b/index.html index 55f92be..09f28dc 100644 --- a/index.html +++ b/index.html @@ -4,16 +4,16 @@ たりない観察 - - - + + + - - - - - - + + + + + +
@@ -29,7 +29,10 @@
- 333の部屋に移動 +

たりない観察

@@ -307,176 +310,221 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/achievement_catalog.js b/js/achievement_catalog.js index 56dd5ac..875a9de 100644 --- a/js/achievement_catalog.js +++ b/js/achievement_catalog.js @@ -82,6 +82,13 @@ window.TARINAI_ACHIEVEMENT_CATALOG = { "category": "ecology", "showLockedDescription": true }, + { + "id": "ochibi_natural_slave", + "title": "たりないより「たりない」", + "description": "おちびが鎖無しでずんちどれいになる。", + "category": "ecology", + "showLockedDescription": true + }, { "id": "natural_tarinai_king", "title": "たりない王", @@ -329,7 +336,7 @@ window.TARINAI_ACHIEVEMENT_CATALOG = { "title": "過保護", "description": "同じたりないに手から10回餌を与え、3回以上薬で治療する。", "category": "ecology", - "showLockedDescription": false + "showLockedDescription": true }, { "id": "self_sufficient", diff --git a/js/achievements.js b/js/achievements.js index 2bdfa13..31bff77 100644 --- a/js/achievements.js +++ b/js/achievements.js @@ -34,7 +34,7 @@ const PLAYER_SECRET_KEY = "tarinai_achievement_player_secret_v1"; const LEGACY_PLAYER_KEY = "tarinai_achievement_legacy_player_v4"; const API_URL = String(global.TARINAI_ACHIEVEMENT_API || "achievement_api.php"); - const GAME_VERSION = String(global.TARINAI_VERSION || "39.18.3"); + const GAME_VERSION = String(global.TARINAI_VERSION || "39.18.18"); const DEBUG_MODE = ["1", "true"].includes(new URLSearchParams(global.location?.search || "").get("debug")); const PLAYER_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const LEGACY_PLAYER_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/; @@ -1021,7 +1021,7 @@ } function sharedAchievementText(id) { - return `端末報告ベース ${percentageText(id)}(報告プレイヤー ${achieverCountText(id)})`; + return `達成率 ${percentageText(id)}(達成人数 ${achieverCountText(id)})`; } function continuousPlayElapsedMs(now = Date.now()) { @@ -2671,6 +2671,7 @@ const world = detail.world || tarinai?.world || null; if (kind === "slave") { unlock("natural_zunchi_slave", detail); + if (tarinai?.isAcquiredTarinai) unlock("ochibi_natural_slave", detail); if (recordNaturalGeneration(world, tarinai, "_achievementNaturalSlaveLineageDepth", 5)) { unlock("natural_zunchi_slave_5_generations", detail); } diff --git a/js/assets.js b/js/assets.js index 54b6a0e..a7799bb 100644 --- a/js/assets.js +++ b/js/assets.js @@ -16,6 +16,8 @@ const DECOR_ASSETS = [ { id: "oshibyo", path: "assets/objects/oshibyo.webp" }, { id: "oshibyo_stuck", path: "assets/objects/oshibyo_stuck.webp" }, { id: "plushie_bear", path: "assets/objects/plushie_bear.png" }, + { id: "acquired_tarinai_overlay", path: "assets/sprites/tarinai_acquired_overlay.png" }, + { id: "mamekusaredake", path: "assets/objects/mamekusaredake.png" }, ]; const ALPHA_BOUNDS = { @@ -45,6 +47,7 @@ const ALPHA_BOUNDS = { stress_dizzy: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 }, stress_sweat: { x: 16, y: 26, w: 480, h: 428, imageW: 512, imageH: 468 }, intimidate: { x: 98, y: 16, w: 316, h: 438, imageW: 512, imageH: 468 }, + intimidate_alt: { x: 45, y: 48, w: 422, h: 379, imageW: 512, imageH: 468 }, zunchi_slave: { x: 17, y: 22, w: 478, h: 431, imageW: 512, imageH: 468 }, hungry_70: { x: 0, y: 0, w: 430, h: 366, imageW: 430, imageH: 366 }, zunchi_slave_alt: { x: 42, y: 22, w: 425, h: 426, imageW: 512, imageH: 468 }, @@ -78,7 +81,7 @@ const SUNBATH_IMAGE_IDS = new Set(SPRITES .map(asset => asset?.id || "") .filter(id => String(id).startsWith("sunbath"))); const INITIAL_IMAGE_IDS = new Set(["smile", "sleep", "hungry_70", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]); -const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "weak", "hurt", "hurt2", "fear", "flee", "flee_fear2", "fear_blue", "fear_cry", "sleep2", "stress_dizzy", "stress_sweat", "intimidate", "birth_ritual", "zunchi_slave", "zunchi_slave_alt", "hungry_70", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]); +const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "weak", "hurt", "hurt2", "fear", "flee", "flee_fear2", "fear_blue", "fear_cry", "sleep2", "stress_dizzy", "stress_sweat", "intimidate", "intimidate_alt", "birth_ritual", "zunchi_slave", "zunchi_slave_alt", "hungry_70", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]); function trimCanvasCache(cache, limit = 128) { while (cache.size > limit) { diff --git a/js/data.js b/js/data.js index 2dd404c..66110f5 100644 --- a/js/data.js +++ b/js/data.js @@ -26,6 +26,7 @@ const SPRITES = [ { id: "stress_dizzy", label: "\u9ad8\u30b9\u30c8\u30ec\u30b9\u305f\u308a\u306a\u3044\u30fb\u3050\u308b\u3050\u308b", path: "assets/sprites/tarinai_24_stress_dizzy.webp", actionOnly: true }, { id: "stress_sweat", label: "\u9ad8\u30b9\u30c8\u30ec\u30b9\u305f\u308a\u306a\u3044\u30fb\u6c57", path: "assets/sprites/tarinai_25_stress_sweat.webp", actionOnly: true }, { id: "intimidate", label: "\u5a01\u5687\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_26_intimidate.webp", actionOnly: true }, + { id: "intimidate_alt", label: "威嚇たりない 2", path: "assets/sprites/tarinai_40_intimidate_alt.webp", actionOnly: true }, { id: "birth_ritual", label: "\u3078\u3053\u3078\u3053\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_27_birth_ritual.webp", actionOnly: true }, { id: "zunchi_slave", label: "\u305a\u3093\u3061\u3069\u308c\u3044", path: "assets/sprites/tarinai_28_zunchi_slave.webp", displayOnly: true }, { id: "hungry_70", label: "\u7a7a\u817970\u4ee5\u4e0a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_29_hungry_70.webp", displayOnly: true }, diff --git a/js/disease_registry.js b/js/disease_registry.js index 5bd4e69..8b08892 100644 --- a/js/disease_registry.js +++ b/js/disease_registry.js @@ -22,6 +22,12 @@ id: "disease_fight", flag: "fightDisease", mysteryEligible: true, + }, + disease_mamekusaredake: { + id: "disease_mamekusaredake", + flag: "mamekusaredakeDisease", + baseInfectionChance: 0.025, + mysteryEligible: false, } }; diff --git a/js/health.js b/js/health.js index 48456c6..e6d31d8 100644 --- a/js/health.js +++ b/js/health.js @@ -180,10 +180,16 @@ const HEALTH = (() => { const MAJOR_DAMAGE_REACTIONS = Object.freeze([ - { id: "voice_major_damage_01", text: "\u3042\u3042\u3042\u3042\uff01" }, - { id: "voice_major_damage_02", text: "\u3048\u3048\u3048\u3048\uff01" }, - { id: "voice_major_damage_03", text: "\u306f\u3046\u3046\u3046\uff01" }, - { id: "voice_major_damage_04", text: "\u306f\u3046\u306f\u3046\uff01" }, + { id: "voice_major_damage_01", text: "ああああ!" }, + { id: "voice_major_damage_02", text: "ええええ!" }, + { id: "voice_major_damage_03", text: "はううう!" }, + { id: "voice_major_damage_04", text: "はうはう!" }, + ]); + + const OCHIBI_MAJOR_DAMAGE_REACTIONS = Object.freeze([ + { id: "voice_ochibi_damage_001", text: "いちゃい!" }, + { id: "voice_ochibi_damage_002", text: "ごふっ" }, + { id: "voice_ochibi_damage_003", text: "ああああああああ" }, ]); function spawnSilentDamageBubble(t, text = "!", color = "rgba(142,58,58,0.86)") { @@ -242,7 +248,7 @@ const HEALTH = (() => { t.lastMajorDamageAt = now; if (now < Number(t.nextMajorDamageVoiceAt || -999)) return false; t.nextMajorDamageVoiceAt = now + 1.0; - const list = MAJOR_DAMAGE_REACTIONS; + const list = t.isAcquiredTarinai ? OCHIBI_MAJOR_DAMAGE_REACTIONS : MAJOR_DAMAGE_REACTIONS; const reaction = list[Math.floor(Math.random() * list.length)] || list[0]; audio.play?.(reaction.id, { category: "voice", minGap: 0.85 }); spawnSilentDamageBubble(t, reaction.text, "rgba(146,58,58,0.88)"); diff --git a/js/item_dynamic_system.js b/js/item_dynamic_system.js index 59bdcb4..df42975 100644 --- a/js/item_dynamic_system.js +++ b/js/item_dynamic_system.js @@ -12,6 +12,8 @@ case "leakage_device": case "poison_gas_can": case "poison_gas": + case "mamekusaredake": + case "mame_spore": return global.TarinaiItemEnvironmentHazardSystem.update(item, dt, worldRef) !== false; case "firecracker": case "flame_firecracker": diff --git a/js/item_environment_hazard_system.js b/js/item_environment_hazard_system.js index 16014f2..7a49723 100644 --- a/js/item_environment_hazard_system.js +++ b/js/item_environment_hazard_system.js @@ -259,6 +259,7 @@ t.hurtTimer=Math.max(t.hurtTimer||0,.22); t.electricShockUntil=Math.max(Number(t.electricShockUntil||0),now+.48); t.setActionState?.("panic",{target:null,reason:"\u611f\u96fb\u3057\u3066\u3044\u308b\u3002",wake:true,sleeping:false}); + if(t.isAcquiredTarinai&&now>=Number(t.ochibiElectricVoiceAt||0)){t.ochibiElectricVoiceAt=now+1.8;if(t.bubble?.("びりびりちゃ!",1.4,"rgba(70,104,168,0.82)",{force:true})){const a=globalThis.TarinaiAudio;if(a?.playSample)a.playSample("voice_ochibi_electric_006",.9,"voice")||a.play?.("voice_ochibi_electric_006",{category:"voice",minGap:.9});}} world.queueEffect?.("electric_shock",t.x,t.y-Math.max(2,(t.radius||18)*.15),{size:Math.max(18,(t.radius||18)*1.35),life:.32,color:"rgba(122,224,255,0.98)",importance:3,forceVisual:true}); hit++; } @@ -365,6 +366,7 @@ if(item.type==="leakage_device")return updateLeakage(item,dt,world); if(item.type==="poison_gas_can")return updatePoisonGasCan(item,dt,world); if(item.type==="poison_gas")return updateGas(item,dt,world); + if(item.type==="mamekusaredake"||item.type==="mame_spore") return global.TarinaiMamekusaredakeSystem?.updateItem?.(item,dt,world) || false; return false; } diff --git a/js/item_render_runtime.js b/js/item_render_runtime.js index 425304f..1bc87cb 100644 --- a/js/item_render_runtime.js +++ b/js/item_render_runtime.js @@ -1294,6 +1294,8 @@ draw(ctx, t, lighting = null) { ctx.fill(); } ctx.restore(); + } else if (this.type === "mamekusaredake" || this.type === "mame_spore") { + globalThis.TarinaiMamekusaredakeSystem?.drawItem?.(ctx, this, t); } else if (this.type === "fan") { const r = this.r || 26; const bodyAngle = Number.isFinite(Number(this.fanBodyAngle)) ? normalizedItemAngle(Number(this.fanBodyAngle), itemAngleFor(this)) : itemAngleFor(this); diff --git a/js/item_tool_definitions.js b/js/item_tool_definitions.js index 618a371..d71e146 100644 --- a/js/item_tool_definitions.js +++ b/js/item_tool_definitions.js @@ -13,6 +13,7 @@ const TOOL_DEFINITIONS = Object.freeze({ poke: { id: "poke", label: "\u3064\u3064\u304f", placeable: false, scalable: false, iconText: "\u{1F448}", tooltip: "\u305f\u308a\u306a\u3044\u3092\u3064\u3064\u304f\u3002\u30dc\u30fc\u30eb\u3082\u3064\u3064\u3044\u3066\u8ee2\u304c\u305b\u308b\u3002" }, pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "\u305f\u308a\u306a\u3044\u3084\u4e00\u90e8\u306e\u7f6e\u304d\u3082\u306e\u3092\u79fb\u52d5\u3055\u305b\u308b\u3002" }, new: { id: "new", label: "\u8ffd\u52a0", placeable: false, scalable: false, icon: "assets/ui/tool_new.webp", tooltip: "\u753b\u9762\u5916\u304b\u3089\u65b0\u3057\u3044\u305f\u308a\u306a\u3044\u3092\u547c\u3076\u3002" }, + acquired_tarinai: { id: "acquired_tarinai", label: "おちび", placeable: false, scalable: false, icon: "assets/ui/tool_acquired_tarinai.webp", tooltip: "近所の鬼威惨から貰ったおちび。後天的なたりないらしい。" }, water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3084\u706b\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" }, rope: { id: "rope", label: "\u30d2\u30e2", placeable: false, scalable: false, iconText: "\u2570", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3075\u306b\u3083\u3075\u306b\u3083\u3057\u305f\u30d2\u30e2\u3067\u7e4b\u3050\u3002" }, rod: { id: "rod", label: "\u68d2", placeable: false, scalable: false, iconText: "\u2501", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u66f2\u304c\u3089\u306a\u3044\u68d2\u3067\u63a5\u7d9a\u3059\u308b\u3002" }, @@ -24,6 +25,8 @@ const TOOL_DEFINITIONS = Object.freeze({ bear_trap: { id: "bear_trap", itemType: "bear_trap", label: "\u30c8\u30e9\u30d0\u30b5\u30df", placeable: true, scalable: false, radius: 34, amount: 999, iconText: "\u7f60", tooltip: "\u52d5\u304f\u3082\u306e\u3092\u6355\u3089\u3048\u3066\u3057\u3070\u3089\u304f\u96e2\u3055\u306a\u3044\u3002" }, leakage_device: { id: "leakage_device", itemType: "leakage_device", label: "\u6f0f\u96fb\u5668", placeable: true, scalable: false, radius: 34, amount: 999, iconText: "\u6f0f", tooltip: "\u901a\u96fb\u4e2d\u3001\u5e83\u7bc4\u56f2\u306b\u96fb\u6483\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002" }, poison_gas_can: { id: "poison_gas_can", itemType: "poison_gas_can", label: "\u6bd2\u30ac\u30b9\u7f36", placeable: true, scalable: true, radius: 28, amount: 999, iconText: "\u6bd2", tooltip: "\u6bd2\u30ac\u30b9\u3092\u653e\u51fa\u3059\u308b\u3002" }, + mamekusaredake: { id: "mamekusaredake", itemType: "mamekusaredake", label: "マメクサレダケ", placeable: true, scalable: false, radius: 34, amount: 999, icon: "assets/ui/tool_mamekusaredake.webp", tooltip: "胞子で感染する寄生キノコ。つまむと除去できる。" }, + mame_spore: { id: "mame_spore", itemType: "mame_spore", label: "マメクサレダケ胞子", placeable: false, scalable: false, radius: 28, amount: 100, iconText: "胞", tooltip: "マメクサレダケの胞子。" }, robot_cleaner: { id: "robot_cleaner", itemType: "robot_cleaner", label: "\u30ed\u30dc\u6383\u9664\u6a5f", placeable: true, scalable: false, radius: 22, amount: 999, iconText: "\u25c9", tooltip: "\u30b4\u30df\u3084\u6c5a\u308c\u3092\u7247\u4ed8\u3051\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u8a73\u7d30\u8a2d\u5b9a\u3002" }, zunchi: { id: "zunchi", itemType: "zunchi", label: "\u305a\u3093\u3061", placeable: true, scalable: true, radius: 14, amount: 240, icon: "assets/ui/tool_zunchi.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u305a\u3093\u3061\u3002\u75c5\u6c17\u3084\u8349\u306e\u80a5\u6599\u306b\u95a2\u308f\u308b\u3002" }, sweet: { id: "sweet", itemType: "sweet", label: "\u305a\u3093\u3060\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_sweet.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u5927\u597d\u7269\u3002\u4e00\u90e8\u306e\u75c5\u6c17\u3092\u6cbb\u305b\u308b\u3002" }, diff --git a/js/item_tool_metadata.js b/js/item_tool_metadata.js index 454ca42..3b6c5e3 100644 --- a/js/item_tool_metadata.js +++ b/js/item_tool_metadata.js @@ -5,12 +5,12 @@ const ITEM_TRAITS = Object.freeze({ obstacle: new Set(["stone", "ball", "balloon", "fan", "nest_box", "bed", "duplicator", "dry_ice", "stove", "rain_shelter", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch", "circuit_board"]), food_interest: new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "slave_chain", "golden_crown", "sleep_drug"]), - hazard: new Set(["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]), + hazard: new Set(["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug", "mamekusaredake"]), pin: new Set(["pushpin", "oshibyo"]), kinematic: new Set(["ball", "balloon", "pushpin", "oshibyo", "zunchi"]), freeStackingMovable: new Set(["ball", "balloon", "pushpin", "oshibyo", "zunchi", "water", "zunda_juice", "mercury", "genkotsu", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "ant_corpse"]), sleepFurniture: new Set(["bed", "nest_box", "pipe", "grass_bed"]), - draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "rain_shelter", "signboard", "duplicator", "robot_cleaner", "oil", "bear_trap", "leakage_device", "poison_gas_can", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch", "seesaw", "circuit_board"]), + draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "rain_shelter", "signboard", "duplicator", "robot_cleaner", "oil", "bear_trap", "leakage_device", "poison_gas_can", "mamekusaredake", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch", "seesaw", "circuit_board"]), }); @@ -30,10 +30,10 @@ { id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "new", "poke", "pinch", "copy", "undo", "redo", "delete", "area_delete", "clear_tool", "water_hose", "bell"] }, { id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["food", "sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] }, { id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["first_aid", "sedative", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "slave_chain", "golden_crown"] }, - { id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "toilet", "rain_shelter", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "seesaw", "signboard"] }, + { id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["acquired_tarinai", "zunchi", "toilet", "rain_shelter", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "seesaw", "signboard"] }, { id: "physics", label: "\u7269\u7406", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_CATEGORY_TOOL_IDS }, { id: "electric", label: "\u96fb\u6c17", themeClass: "tool-category-electric", order: 46, toolIds: ELECTRIC_TOOL_IDS }, - { id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "oil", "bear_trap", "poison_gas_can", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "pushpin", "oshibyo", "shoot", "ant_nest"] }, + { id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "oil", "bear_trap", "poison_gas_can", "mamekusaredake", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "pushpin", "oshibyo", "shoot", "ant_nest"] }, ]); const TOOL_SIZE_ORDER = Object.freeze(["small", "medium", "large"]); diff --git a/js/item_type_initializers.js b/js/item_type_initializers.js index 909c7b1..d714a4e 100644 --- a/js/item_type_initializers.js +++ b/js/item_type_initializers.js @@ -41,7 +41,7 @@ function initializeItemTypeState(item, type, x, y) { if (type === "water" || type === "zunda_juice") item.roles.drink = true; if (["sweet", "water", "zunda_juice", "slave_chain", "golden_crown"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = true; if (type === "bed" || type === "nest_box" || type === "pipe") item.roles.sleepPlace = true; - if (["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "splat", "oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas"].includes(type)) item.roles.danger = true; + if (["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "splat", "oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "mamekusaredake", "mame_spore"].includes(type)) item.roles.danger = true; if (type === "grass") item.roles.grassMaterial = true; if (type === "grass") { item.grassStage = Math.floor(rand(0, 2.999)); @@ -310,6 +310,8 @@ function initializeItemTypeState(item, type, x, y) { if (type === "leakage_device") { item.r = 34; item.amount = 999; item.nextLeakagePulseAt = 0; item.deletable = true; } if (type === "poison_gas_can") { item.r = 28; item.amount = 999; item.fuseMax = 5; item.fuseTimer = 5; item._gasReleased = false; item.deletable = true; } if (type === "poison_gas") { item.r = 96; item.amount = 100; item.gasConcentration = 1; item.gasAge = 0; item.gasLife = 22; item.gasExpansion = 72; item.vx = 0; item.vy = 0; item.deletable = false; } + if (type === "mamekusaredake") { item.r = 34; item.amount = 999; item.mamePlanted = true; item.nextSporeAt = 0; item.deletable = true; item.roles.danger = true; } + if (type === "mame_spore") { item.r = 28; item.amount = 100; item.sporeAge = 0; item.sporeLife = 9; item.sporeConcentration = 1; item.vx = 0; item.vy = 0; item.deletable = false; item.roles.danger = true; } if (globalThis.TarinaiPhysicsBodySystem.isPhysicsType(type)) { globalThis.TarinaiPhysicsBodySystem.invalidateItem(item, "item-initialized"); diff --git a/js/item_update_policy.js b/js/item_update_policy.js index 6c1f484..bc9ec45 100644 --- a/js/item_update_policy.js +++ b/js/item_update_policy.js @@ -5,10 +5,10 @@ // helpers and the item scheduler both read this single policy instead of // duplicating item-type timing rules. (function (global) { - const REALTIME_ITEM_TYPES = new Set(["oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "robot_cleaner"]); + const REALTIME_ITEM_TYPES = new Set(["oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "mamekusaredake", "mame_spore", "ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "robot_cleaner"]); const PIN_ITEM_TYPES = new Set(["pushpin", "oshibyo"]); const SCHEDULED_ITEM_TYPES = Object.freeze([ - "oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "pushpin", "oshibyo", + "oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "mamekusaredake", "mame_spore", "ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "pushpin", "oshibyo", "plushie", "grass_bed", "zunchi", "robot_cleaner", "duplicator", "water", "trace", "splat", "ant_corpse", "food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "stone", "bed", "nest_box", "pipe", "toilet", "rain_shelter", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch", @@ -52,7 +52,7 @@ function idleRealtimeInterval(item) { if (!item) return Infinity; - if (item.type === "oil" || item.type === "bear_trap" || item.type === "leakage_device" || item.type === "poison_gas_can" || item.type === "poison_gas") return 0.18; + if (item.type === "oil" || item.type === "bear_trap" || item.type === "leakage_device" || item.type === "poison_gas_can" || item.type === "poison_gas" || item.type === "mamekusaredake" || item.type === "mame_spore") return 0.18; if (item.type === "ball" || item.type === "balloon") return 0.24; if (item.type === "fan") return 0.60; if (item.type === "genkotsu") return 0.45; diff --git a/js/mamekusaredake_system.js b/js/mamekusaredake_system.js new file mode 100644 index 0000000..877b73a --- /dev/null +++ b/js/mamekusaredake_system.js @@ -0,0 +1,187 @@ +"use strict"; +(function(global){ + function mushroomImage(){ return global.getRenderableImage?.("mamekusaredake", "") || global.images?.get?.("mamekusaredake") || null; } + function growthOf(t){ return Math.max(0,Math.min(1,Number(t?.mamekusaredakeGrowth||0)||0)); } + const hostAlphaCache = typeof WeakMap !== "undefined" ? new WeakMap() : null; + function unit(seed="",salt=""){ + if(typeof global.stableUnit==="function") return global.stableUnit(String(seed||"mame"),String(salt||"")); + const s=`${seed}|${salt}`; let h=2166136261; + for(let i=0;i>>0)%1000003)/1000003; + } + function alphaPointsForImage(img){ + if(!img)return null; + if(hostAlphaCache?.has(img)) return hostAlphaCache.get(img); + let out=null; + try{ + const c=document.createElement("canvas"); c.width=64; c.height=64; + const cx=c.getContext("2d",{willReadFrequently:true}); cx.clearRect(0,0,64,64); cx.drawImage(img,0,0,64,64); + const data=cx.getImageData(0,0,64,64).data, pts=[]; + for(let y=3;y<61;y++) for(let x=3;x<61;x++) if(data[(y*64+x)*4+3]>=96) pts.push([x,y]); + out=pts.length?pts:null; + }catch(_){out=null;} + hostAlphaCache?.set(img,out); return out; + } + function hostPoint(t,img,dw,dh,index=0){ + const seed=t?.birthSeed||t?.familyKey||t?.id||"mame-host"; + const pts=alphaPointsForImage(img); + if(pts?.length){ + const k=Math.min(pts.length-1,Math.floor(unit(seed,`mame-pos-${index}`)*pts.length)); + const p=pts[k]||pts[0]; + return {x:(p[0]/63-.5)*dw,y:(p[1]/63-.5)*dh}; + } + // Fallback stays inside the usual non-transparent body ellipse. + const a=unit(seed,`mame-a-${index}`)*Math.PI*2, rr=Math.sqrt(unit(seed,`mame-r-${index}`))*.70; + return {x:Math.cos(a)*dw*.34*rr,y:Math.sin(a)*dh*.28*rr+dh*.03}; + } + function hostMushroomCount(g){ return g<=.001?0:Math.max(1,Math.min(5,Math.ceil(g*5))); } + function infect(t, source=null){ + if(!t||t.dead||t.mamekusaredakeDisease) return false; + t.mamekusaredakeDisease=true; t.mamekusaredakeGrowth=0; t.mamekusaredakeDeathPlanted=false; + t.mamekusaredakeSpeechAt=(t.world?.time||0)+3+Math.random()*5; + t.world?.log?.(`${t.name}がマメクサレダケ病に罹患した。`,"accident",{participants:[t]}); + return true; + } + function visualSizeForHost(t,g=growthOf(t)){ + const rendered=Number(t?._mameRenderedSize||0)||0; + if(rendered>0) return Math.max(3,rendered); + const radius=Math.max(16,Number(t?.radius||20)||20); + return Math.max(3,radius*1.16*Math.max(.05,g)); + } + function spawnDetached(world,t,growth=growthOf(t)){ + if(!world||!t||typeof Item==="undefined") return null; + const side=t.facingDir?.()||1; + const it=new Item("mamekusaredake", t.x+side*(t.radius||20)*0.46, t.y+(t.radius||20)*0.30); + it.mameDetached=true; it.mamePlanted=false; it.mameLife=1.25; it.vx=side*(35+Math.random()*25); it.vy=-35-Math.random()*20; + it.mameDetachedSize=visualSizeForHost(t,growth); it.r=Math.max(2,it.mameDetachedSize/2.15); + world.addItem?.(it,"mame-detached",{terrain:false}); return it; + } + function recover(t, reason="キノコが取れた"){ + if(!t?.mamekusaredakeDisease) return false; + const growth=growthOf(t); + spawnDetached(t.world,t,growth); + t.mamekusaredakeDisease=false; t.mamekusaredakeGrowth=0; t.mamekusaredakeSpeechAt=0; t.mamekusaredakeDeathPlanted=false; + t.world?.spawnEffect?.("ring",t.x,t.y,{size:(t.radius||20)*(.55+.75*growth),life:.55,color:"rgba(190,153,80,.66)"}); + t.world?.log?.(`${t.name}のマメクサレダケが外れて治った。`,"event",{participants:[t]}); return true; + } + function plantAt(world,x,y,r=34){ + if(!world||typeof Item==="undefined")return null; + const it=new Item("mamekusaredake",x,y); it.r=r; it.mamePlanted=true; it.mameDetached=false; it.nextSporeAt=(world.time||0)+1.2+Math.random(); + world.addItem?.(it,"mame-plant",{terrain:false}); return it; + } + function plantFromHost(t){ + if(!t?.world||t.mamekusaredakeDeathPlanted) return null; + t.mamekusaredakeDeathPlanted=true; + const g=growthOf(t); + // 死亡サイズの50%以下なら地面には残らず消滅する。 + if(g<=.5) return null; + // 表示上の本数とは無関係に、死亡時に植わるのは必ず1本だけ。 + return plantAt(t.world,t.x,t.y,Math.max(22,(t.radius||20)*(.62+.32*g))); + } + function spawnSpore(world,m){ + if(!world||typeof Item==="undefined")return null; + const a=Math.random()*Math.PI*2,d=Math.random()*(m.r||34)*.35; + const s=new Item("mame_spore",m.x+Math.cos(a)*d,m.y+Math.sin(a)*d*.55); s.r=22+Math.random()*18; s.sporeAge=0; s.sporeLife=7+Math.random()*5; s.sporeConcentration=.45+Math.random()*.25; s.vx=Math.cos(a)*(6+Math.random()*10); s.vy=-4+Math.sin(a)*5; + world.addItem?.(s,"mame-spore",{terrain:false}); return s; + } + function updateTarinai(t,dt){ + if(!t||t.dead)return false; + const now=t.world?.time||0; + const sick=Boolean(t.zunchiDisease||t.sleepDisease||t.explosionDisease||t.fightDisease||t.mamekusaredakeDisease); + if(t.mamekusaredakeDisease){ + if(t.state==="sunbath"||(t.sunbathTimer||0)>.04){ + t.sunbathTimer=0; t.sunbathLeisure=false; t.sunbathCooldown=Math.max(t.sunbathCooldown||0,20); + t.visibleSpriteId=""; t.setActionState?.("idle",{target:null,reason:"マメクサレダケ病のため日光浴できない",sleeping:false,clearTarget:true}); + } + t.mamekusaredakeGrowth=Math.min(1.08,(Number(t.mamekusaredakeGrowth||0)||0)+dt/90); + const g=growthOf(t); + if(g>=.5){ + const maxEnergy=Math.max(1,typeof global.tarinaiMaxEnergy==="function"?global.tarinaiMaxEnergy(t):(Number(t.maxEnergy)||100)); + const severity=Math.max(0,Math.min(1,(g-.5)/.5)); + const loss=dt*maxEnergy*(.0018+.0037*severity); + t.energy=Math.max(0,(Number(t.energy)||0)-loss); + if(loss>.01) t.showHpBar?.(); + if(t.energy<=.5){ t.die?.("マメクサレダケ病",{fromDamage:true}); return true; } + } + } + if(t.isAcquiredTarinai && sick && now>=Number(t.mamekusaredakeSpeechAt||0) && (t.nextBubbleAt||0)<=now){ + const choices=[{text:"くるち、くるち",id:"voice_ochibi_disease_001"},{text:"おみじゅ...",id:"voice_ochibi_hot_007"},{text:"ぶるぶる",id:"voice_temp_cold"}]; + const c=choices[Math.floor(Math.random()*choices.length)]; if(c&&t.bubble?.(c.text,4.5,"rgba(96,72,48,.82)")){ const a=global.TarinaiAudio; a?.playSample?.(c.id,1.1,"voice")||a?.play?.(c.id,{category:"voice",minGap:1.1}); } + t.mamekusaredakeSpeechAt=now+7+Math.random()*10; + } + if(t.mamekusaredakeDisease && t.mamekusaredakeGrowth>=1){ t.die?.("マメクサレダケ病"); } + return true; + } + function updateItem(item,dt,world){ + if(item.type==="mamekusaredake"){ + if(item.mameDetached){ item.mameLife=(item.mameLife||1)-dt; item.x+=(item.vx||0)*dt; item.y+=(item.vy||0)*dt; item.vy=(item.vy||0)+55*dt; item.vx*=Math.pow(.6,dt); if(item.mameLife<=0)item.amount=0; return true; } + item.mamePlanted=true; const now=world?.time||0; if(now>=Number(item.nextSporeAt||0)){ item.nextSporeAt=now+1.4+Math.random()*1.8; if(Math.random()<.75)spawnSpore(world,item); } return true; + } + if(item.type==="mame_spore"){ + item.sporeAge=(item.sporeAge||0)+dt; const life=Math.max(1,item.sporeLife||9); item.sporeConcentration=Math.max(0,1-item.sporeAge/life); item.x+=(item.vx||0)*dt; item.y+=(item.vy||0)*dt; item.vx*=Math.pow(.72,dt); item.vy*=Math.pow(.72,dt); + for(const t of world?.tarinai||[]){ if(!t||t.dead||t.mamekusaredakeDisease)continue; if(distXY(item.x,item.y,t.x,t.y)<=item.r+(t.radius||18)*.45 && Math.random()0?Number(item.mameDetachedSize):(item.r||34)*2.15; + if(!item.mameDetached){ + ctx.save(); ctx.globalAlpha*=.34; ctx.fillStyle="rgba(38,28,18,.72)"; ctx.beginPath(); ctx.ellipse(size*.055,size*.41,size*.30,size*.085,0,0,Math.PI*2); ctx.fill(); ctx.restore(); + } + return drawMushroom(ctx,0,0,size,alpha,item.mameDetached?(time*2.2):0); + } + if(item.type==="mame_spore"){ const r=item.r||28,c=Math.max(0,Math.min(1,item.sporeConcentration??1));ctx.save();ctx.globalAlpha*=.10+.22*c;ctx.fillStyle="rgba(181,139,65,.58)";for(let i=0;i<3;i++){const a=(item.seed||0)+i*2.2,rr=r*(.30+i*.06);ctx.beginPath();ctx.arc(Math.cos(a)*r*.18,Math.sin(a)*r*.12,rr,0,Math.PI*2);ctx.fill();}ctx.restore();return true;} + return false; + } + function mushroomHit(t,x,y){ + if(!t?.mamekusaredakeDisease)return false; + const worldTargets=Array.isArray(t._mameHostWorldTargets)?t._mameHostWorldTargets:[]; + for(const p of worldTargets){ + if(distXY(x,y,p.x,p.y)<=Math.max(8,(p.size||10)*.52)) return true; + } + // Fallback for the short interval before the first render after infection. + // It intentionally stays conservative so pinching the body itself does not + // accidentally cure the disease. + const g=Math.max(.05,growthOf(t)); + const face=typeof t.facingDir==="function"?(t.facingDir()||1):(t.facing||1); + return distXY(x,y,t.x+face*(t.radius||20)*.30,t.y+(t.radius||20)*.18)<=Math.max(7,(t.radius||20)*.34*g); + } + global.TarinaiMamekusaredakeSystem=Object.freeze({infect,recover,plantAt,plantFromHost,spawnSpore,updateTarinai,updateItem,drawOnTarinai,drawItem,mushroomHit}); +})(typeof window!=="undefined"?window:globalThis); diff --git a/js/signal_system.js b/js/signal_system.js index b775c2f..2975969 100644 --- a/js/signal_system.js +++ b/js/signal_system.js @@ -449,6 +449,13 @@ target.fearTimer = Math.max(num(target.fearTimer), 0.8); target.electricShockUntil = Math.max(num(target.electricShockUntil), now + 0.62); target.setActionState?.("panic", { target: null, reason: "\u611f\u96fb\u3057\u3066\u3044\u308b", wake: true, sleeping: false }); + if (target.isAcquiredTarinai && now >= Number(target.ochibiElectricVoiceAt || 0)) { + target.ochibiElectricVoiceAt = now + 1.8; + if (target.bubble?.("びりびりちゃ!", 1.4, "rgba(70,104,168,0.82)", { force: true })) { + const a = global.TarinaiAudio; + if (a?.playSample) a.playSample("voice_ochibi_electric_006", 0.9, "voice") || a.play?.("voice_ochibi_electric_006", { category: "voice", minGap: 0.9 }); + } + } } emitEffect(worldRef, "electric_shock", target.x, target.y - Math.max(3, num(target.radius ?? target.r, 12) * 0.12), { size: Math.max(12, num(target.radius ?? target.r, 12) * 1.15), diff --git a/js/sim_core.js b/js/sim_core.js index 7e58880..8c91773 100644 --- a/js/sim_core.js +++ b/js/sim_core.js @@ -660,32 +660,10 @@ class Effect { } } else if (this.type === "bubble") { ctx.font = "bold 12px Yomogi, ui-rounded, sans-serif"; - const maxTextW = 118; - const lineH = 14; - const ellipsize = (text, maxW) => globalThis.canvasEllipsizeText(ctx, text, maxW); - const wrap = (text, maxW) => { - const src = String(text || "").replace(/\s+/g, " ").trim() || " "; - const units = src.includes(" ") ? src.split(/(\s+)/).filter(Boolean) : Array.from(src); - const out = []; - let line = ""; - for (const unit of units) { - const candidate = line ? `${line}${unit}` : unit; - if (ctx.measureText(candidate).width <= maxW) line = candidate; - else { - if (line) out.push(line.trim()); - line = unit.trim(); - if (ctx.measureText(line).width > maxW) { out.push(ellipsize(line, maxW)); line = ""; } - } - if (out.length >= 3) break; - } - if (line && out.length < 3) out.push(line.trim()); - if (out.length > 2) out[1] = ellipsize([out[1], ...out.slice(2)].join(""), maxW); - return out.slice(0, 2).map(line => ellipsize(line, maxW)); - }; - const lines = wrap(this.text, maxTextW); - const textW = lines.reduce((m, line) => Math.max(m, ctx.measureText(line).width), 0); - const w = Math.max(30, Math.min(142, textW + 18)); - const h = Math.max(20, 10 + lines.length * lineH); + const line = String(this.text || "").replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim() || " "; + const textW = Math.ceil(ctx.measureText(line).width); + const w = Math.max(30, textW + 20); + const h = 28; ctx.globalAlpha = Math.min(1, alpha * 1.25); ctx.fillStyle = "rgba(255, 252, 240, 0.92)"; ctx.strokeStyle = "rgba(78, 64, 48, 0.22)"; @@ -704,9 +682,7 @@ class Effect { ctx.fillStyle = this.color || "rgba(42,36,29,0.78)"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; - for (let i = 0; i < lines.length; i++) { - ctx.fillText(lines[i], 0, -h + 8 + lineH * i + lineH / 2); - } + ctx.fillText(line, 0, -h / 2); } ctx.restore(); } diff --git a/js/snapshot_system.js b/js/snapshot_system.js index 3d370dd..3bb490b 100644 --- a/js/snapshot_system.js +++ b/js/snapshot_system.js @@ -301,6 +301,8 @@ if (t.birthRitualLeader) flags |= 1 << 7; if (t.hasPaired) flags |= 1 << 8; if (t.isTarinaiChampion) flags |= 1 << 9; + if (t.isAcquiredTarinai) flags |= 1 << 10; + if (t.mamekusaredakeDisease) flags |= 1 << 11; return flags; } function flagApply(t, flags = 0) { @@ -314,6 +316,8 @@ t.birthRitualLeader = !!(flags & (1 << 7)); t.hasPaired = !!(flags & (1 << 8)); t.isTarinaiChampion = !!(flags & (1 << 9)); + t.isAcquiredTarinai = !!(flags & (1 << 10)); + t.mamekusaredakeDisease = !!(flags & (1 << 11)); if (t.isZunchiSlave) t.zunchiSlaveLocked = true; } function compactWorld(worldRef) { @@ -507,8 +511,7 @@ const parentIdx = (t.parents || []).map(id => familyIndex.get(id)).filter(Number.isInteger); const timers = [ q(t.reproductionTimer, 10), q(t.fightCooldown, 10), q(t.loveMochiTimer, 10), q(t.fightMochiTimer, 10), - q(t.zunchiDiseaseSeverity, 10), q(t.zunchiStain, 10), q(t.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100, 100), q(t.itemEffectTimers?.sedative, 10) - ]; + q(t.zunchiDiseaseSeverity, 10), q(t.zunchiStain, 10), q(t.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100, 100), q(t.itemEffectTimers?.sedative, 10), q(t.mamekusaredakeGrowth, 100)]; const modes = [enumIndex(POWER_MODE_IDS, t.powerItemMode || ""), enumIndex(SIZE_MODE_IDS, t.sizeItemMode || ""), enumIndex(LIFE_MODE_IDS, t.lifeItemMode || "")]; const pinIdx = itemIndex.get(t.stuckPushpinId) ?? -1; const nestIdx = itemIndex.get(t.insideNestBoxId) ?? -1; @@ -548,6 +551,8 @@ needs, state: "idle", isZunchiSlave: Boolean((Number(row[0]) || 0) & (1 << 5)), isTarinaiChampion: Boolean((Number(row[0]) || 0) & (1 << 9)), + isAcquiredTarinai: Boolean((Number(row[0]) || 0) & (1 << 10)), + mamekusaredakeDisease: Boolean((Number(row[0]) || 0) & (1 << 11)), totalFightWins: Array.isArray(row[19]) ? u(row[19][0], 1) : 0, totalFightLosses: Array.isArray(row[19]) ? u(row[19][1], 1) : 0, tarinaiChampionSince: Array.isArray(row[19]) ? u(row[19][2], 10) : 0, @@ -567,6 +572,7 @@ const timers = Array.isArray(row[13]) ? row[13] : []; t.reproductionTimer = u(timers[0], 10); t.fightCooldown = u(timers[1], 10); t.loveMochiTimer = u(timers[2], 10); t.fightMochiTimer = u(timers[3], 10); t.zunchiDiseaseSeverity = u(timers[4], 10); t.zunchiStain = u(timers[5], 10); + t.mamekusaredakeGrowth = u(timers[10], 100, 0); t.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[6], 10), ammo: u(timers[7], 10), sedative: u(timers[9], 10) }; t.mercuryLifeMultiplier = u(timers[8], 100, t.mercuryLifeMultiplier || 1); const modes = Array.isArray(row[14]) ? row[14] : []; diff --git a/js/sound_pack.js b/js/sound_pack.js index d749cea..9663b67 100644 --- a/js/sound_pack.js +++ b/js/sound_pack.js @@ -30,6 +30,40 @@ voice_major_damage_04: 1.0, voice_temp_cold: 1.0, voice_temp_hot: 1.0, + voice_acquired_stress_001: 1.0, + voice_acquired_stress_002: 1.0, + voice_acquired_stress_003: 1.0, + voice_acquired_stress_004: 1.0, + voice_acquired_stress_005: 1.0, + voice_acquired_stress_006: 1.0, + voice_acquired_stress_009: 1.0, + voice_ochibi_calm_001: 1.0, + voice_ochibi_calm_002: 1.0, + voice_ochibi_calm_003: 1.0, + voice_ochibi_calm_004: 1.0, + voice_ochibi_calm_005: 1.0, + voice_ochibi_calm_006: 1.0, + voice_ochibi_calm_007: 1.0, + voice_ochibi_calm_008: 1.0, + voice_ochibi_calm_009: 1.0, + voice_ochibi_dirty_004: 1.0, + voice_ochibi_damage_001: 1.0, + voice_ochibi_damage_002: 1.0, + voice_ochibi_damage_003: 1.0, + voice_ochibi_disease_001: 1.0, + voice_intimidate_alt: 1.0, + voice_ochibi_sleep_002: 1.0, + voice_ochibi_sleep_003: 1.0, + voice_ochibi_fight_hit_004: 1.0, + voice_ochibi_panic_005: 1.0, + voice_ochibi_electric_006: 1.0, + voice_ochibi_hot_007: 1.0, + voice_ochibi_stress_008: 1.0, + voice_ochibi_dirty_003: 1.0, + voice_ochibi_dirty_002: 1.0, + voice_ochibi_dirty_001: 1.0, + voice_acquired_stress_008: 1.0, + voice_acquired_stress_007: 1.0, }, idMinGap: { sfx_ui_click: 0.030, @@ -48,6 +82,40 @@ voice_major_damage_04: 0.45, voice_temp_cold: 1.20, voice_temp_hot: 1.20, + voice_acquired_stress_001: 1.50, + voice_acquired_stress_002: 1.50, + voice_acquired_stress_003: 1.50, + voice_acquired_stress_004: 1.50, + voice_acquired_stress_005: 1.50, + voice_acquired_stress_006: 1.50, + voice_acquired_stress_009: 1.50, + voice_ochibi_calm_001: 1.50, + voice_ochibi_calm_002: 1.50, + voice_ochibi_calm_003: 1.50, + voice_ochibi_calm_004: 1.50, + voice_ochibi_calm_005: 1.50, + voice_ochibi_calm_006: 1.50, + voice_ochibi_calm_007: 1.50, + voice_ochibi_calm_008: 1.50, + voice_ochibi_calm_009: 1.50, + voice_ochibi_dirty_004: 1.50, + voice_ochibi_damage_001: 1.25, + voice_ochibi_damage_002: 1.25, + voice_ochibi_damage_003: 1.25, + voice_ochibi_disease_001: 1.25, + voice_intimidate_alt: 1.35, + voice_ochibi_sleep_002: 1.35, + voice_ochibi_sleep_003: 1.35, + voice_ochibi_fight_hit_004: 1.35, + voice_ochibi_panic_005: 1.35, + voice_ochibi_electric_006: 1.35, + voice_ochibi_hot_007: 1.35, + voice_ochibi_stress_008: 1.35, + voice_ochibi_dirty_003: 1.50, + voice_ochibi_dirty_002: 1.50, + voice_ochibi_dirty_001: 1.50, + voice_acquired_stress_008: 1.50, + voice_acquired_stress_007: 1.50, }, samplePaths: { voice_hau: "assets/sounds/voice_001_hau.wav", @@ -67,7 +135,41 @@ voice_major_damage_04: "assets/sounds/major_damage_04.wav", voice_temp_cold: "assets/sounds/voice_008_temperature_buruburu.wav", voice_temp_hot: "assets/sounds/voice_009_temperature_achui.wav", - sfx_achievement_unlock: "assets/sounds/achievement_unlock.mp3" + sfx_achievement_unlock: "assets/sounds/achievement_unlock.mp3", + voice_acquired_stress_001: "assets/sounds/acquired_stress_001.wav", + voice_acquired_stress_002: "assets/sounds/acquired_stress_002.wav", + voice_acquired_stress_003: "assets/sounds/acquired_stress_003.wav", + voice_acquired_stress_004: "assets/sounds/acquired_stress_004.wav", + voice_acquired_stress_005: "assets/sounds/acquired_stress_005.wav", + voice_acquired_stress_006: "assets/sounds/acquired_stress_006.wav", + voice_acquired_stress_009: "assets/sounds/acquired_stress_009.wav", + voice_ochibi_calm_001: "assets/sounds/ochibi_calm_001.wav", + voice_ochibi_calm_002: "assets/sounds/ochibi_calm_002.wav", + voice_ochibi_calm_003: "assets/sounds/ochibi_calm_003.wav", + voice_ochibi_calm_004: "assets/sounds/ochibi_calm_004.wav", + voice_ochibi_calm_005: "assets/sounds/ochibi_calm_005.wav", + voice_ochibi_calm_006: "assets/sounds/ochibi_calm_006.wav", + voice_ochibi_calm_007: "assets/sounds/ochibi_calm_007.wav", + voice_ochibi_calm_008: "assets/sounds/ochibi_calm_008.wav", + voice_ochibi_calm_009: "assets/sounds/ochibi_calm_009.wav", + voice_ochibi_dirty_001: "assets/sounds/ochibi_dirty_001.wav", + voice_ochibi_dirty_002: "assets/sounds/ochibi_dirty_002.wav", + voice_ochibi_dirty_003: "assets/sounds/ochibi_dirty_003.wav", + voice_ochibi_dirty_004: "assets/sounds/ochibi_dirty_004.wav", + voice_ochibi_damage_001: "assets/sounds/ochibi_damage_001.wav", + voice_ochibi_damage_002: "assets/sounds/ochibi_damage_002.wav", + voice_ochibi_damage_003: "assets/sounds/ochibi_damage_003.wav", + voice_ochibi_disease_001: "assets/sounds/ochibi_disease_001.wav", + voice_intimidate_alt: "assets/sounds/intimidate_alt_001.wav", + voice_ochibi_sleep_002: "assets/sounds/ochibi_sleep_002.wav", + voice_ochibi_sleep_003: "assets/sounds/ochibi_sleep_003.wav", + voice_ochibi_fight_hit_004: "assets/sounds/ochibi_fight_hit_004.wav", + voice_ochibi_panic_005: "assets/sounds/ochibi_panic_005.wav", + voice_ochibi_electric_006: "assets/sounds/ochibi_electric_006.wav", + voice_ochibi_hot_007: "assets/sounds/ochibi_hot_007.wav", + voice_ochibi_stress_008: "assets/sounds/ochibi_stress_008.wav", + voice_acquired_stress_008: "assets/sounds/acquired_stress_008.wav", + voice_acquired_stress_007: "assets/sounds/acquired_stress_007.wav", }, categories: { voice: "\u305f\u308a\u306a\u3044\u306e\u58f0", @@ -78,6 +180,11 @@ voice_hau: "voice", voice_flee: "voice", voice_poop: "voice", voice_stress: "voice", voice_sunbath: "voice", voice_major_damage_01: "voice", voice_major_damage_02: "voice", voice_major_damage_03: "voice", voice_major_damage_04: "voice", voice_temp_cold: "voice", voice_temp_hot: "voice", + voice_acquired_stress_001: "voice", voice_acquired_stress_002: "voice", voice_acquired_stress_003: "voice", voice_acquired_stress_004: "voice", voice_acquired_stress_005: "voice", voice_acquired_stress_006: "voice", voice_acquired_stress_007: "voice", voice_acquired_stress_008: "voice", voice_acquired_stress_009: "voice", + voice_ochibi_calm_001: "voice", voice_ochibi_calm_002: "voice", voice_ochibi_calm_003: "voice", voice_ochibi_calm_004: "voice", voice_ochibi_calm_005: "voice", voice_ochibi_calm_006: "voice", voice_ochibi_calm_007: "voice", voice_ochibi_calm_008: "voice", voice_ochibi_calm_009: "voice", + voice_ochibi_dirty_001: "voice", voice_ochibi_dirty_002: "voice", voice_ochibi_dirty_003: "voice", voice_ochibi_dirty_004: "voice", + voice_ochibi_damage_001: "voice", voice_ochibi_damage_002: "voice", voice_ochibi_damage_003: "voice", voice_ochibi_disease_001: "voice", + voice_intimidate_alt: "voice", voice_ochibi_sleep_002: "voice", voice_ochibi_sleep_003: "voice", voice_ochibi_fight_hit_004: "voice", voice_ochibi_panic_005: "voice", voice_ochibi_electric_006: "voice", voice_ochibi_hot_007: "voice", voice_ochibi_stress_008: "voice", sfx_eat: "voice", sfx_sleep: "voice", sfx_wake: "voice", sfx_birth: "voice", sfx_death: "voice", sfx_damage_soft: "voice", sfx_damage_heavy: "voice", sfx_fight_hit: "voice", sfx_fight_finish: "voice", sfx_disease: "voice", sfx_heal: "voice", sfx_ant_drag: "voice", sfx_ant_die: "voice", sfx_queen_ant: "voice", diff --git a/js/tarinai.js b/js/tarinai.js index 648cbca..49ad6f1 100644 --- a/js/tarinai.js +++ b/js/tarinai.js @@ -16,17 +16,28 @@ function tarinaiRoundRectPath(ctx, x, y, w, h, r) { } function tarinaiChampionName(name = "") { - const map = { "\u306F": "\u8987", "\u3046": "\u5B87", "\u3045": "\u30A5", "\u3063": "\uFF01" }; - return String(name || "").replace(/[\u306F\u3046\u3045\u3063]/g, ch => map[ch] || ch); + const raw = String(name || ""); + if (/おちび(?:の王)?$/.test(raw)) return raw.endsWith("の王") ? raw : `${raw}の王`; + const map = { "は": "覇", "う": "宇", "ぅ": "ゥ", "っ": "!" }; + return raw.replace(/[はうぅっ]/g, ch => map[ch] || ch); } if (typeof window !== "undefined") window.tarinaiChampionName = tarinaiChampionName; if (typeof globalThis !== "undefined") globalThis.tarinaiChampionName = tarinaiChampionName; +const ACQUIRED_TARINAI_NAME_PREFIXES = ["かしこい", "すごい", "たくましい", "げんきな", "わんぱくな", "ぴかぴかあにゃるの", "しゅーるすとれみんぐ・", "つよい", "もちもちほっぺの", "でかずんぽの", "ちびずんぽの", "すばらしい", "かわいい", "ずんちがくさい", "やさしい", "ゆうかんな", "いいにおいの"]; +function acquiredTarinaiBaseName(seed = "", name = "") { + const raw = String(name || "").trim(); + if (raw && raw !== "ずんちどれい") return raw.replace(/の王$/, ""); + const pick = typeof stableChoice === "function" ? stableChoice(seed || raw || "acquired", "acquired-name-prefix", ACQUIRED_TARINAI_NAME_PREFIXES) : (ACQUIRED_TARINAI_NAME_PREFIXES[0] || ""); + return `${pick || ACQUIRED_TARINAI_NAME_PREFIXES[0] || ""}おちび`; +} + class Tarinai { constructor(world, opts = {}) { if (world && !opts.birthSeed && !opts.familyKey && !opts.id && globalThis.TarinaiSeedFactory.prepareAddOptions) { opts = globalThis.TarinaiSeedFactory.prepareAddOptions(world, opts) || opts; } this.world = world; + this.isAcquiredTarinai = Boolean(opts.isAcquiredTarinai); this.birthSeed = opts.birthSeed || opts.familyKey || opts.id || (globalThis.TarinaiSeedFactory.childSeed(world?.worldSeed || "", Number(world?.birthSerial || 0) + 1, []) || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`)); this.birthSerial = Number.isFinite(opts.birthSerial) ? Number(opts.birthSerial) : (globalThis.TarinaiSeedFactory.serialFromBirthSeed(this.birthSeed) || 0); const birthProfile = globalThis.TarinaiSeedFactory.birthProfile(this.birthSeed, opts) || {}; @@ -34,6 +45,7 @@ class Tarinai { constructor(world, opts = {}) { this.id = opts.id || ""; this.liveToken = opts.liveToken || 0; this.name = opts.name || birthProfile.name || makeName(world); + this.acquiredBaseName = opts.acquiredBaseName || null; this.type = opts.type || birthProfile.type || baseSpriteId(); const typeMeta = SPRITES.find(s => s.id === this.type); if (typeMeta?.actionOnly || typeMeta?.displayOnly) this.type = birthProfile.type || baseSpriteId(); @@ -120,6 +132,29 @@ class Tarinai { constructor(world, opts = {}) { this.sleeping = false; this.awakeLockTimer = opts.awakeLockTimer ?? 0; this.goodMode = opts.goodMode ?? birthProfile.goodMode ?? stableChoice(this.birthSeed || this.familyKey, "good-mode", displayNormalSprites()) ?? "smile"; + if (this.isAcquiredTarinai) { + const explicitAcquiredName = this.acquiredBaseName || (Object.prototype.hasOwnProperty.call(opts, "name") ? this.name : ""); + this.acquiredBaseName = acquiredTarinaiBaseName(this.birthSeed || this.familyKey, explicitAcquiredName); + this.name = this.acquiredBaseName; + } + if (this.isAcquiredTarinai) { + const acquiredNeeds = { food: 22, sleep: 34, health: 78, safety: 100, social: 90, fulfill: 82 }; + const hasExplicitNeedState = Boolean((opts.needs && typeof opts.needs === "object") || (opts.needRaw && typeof opts.needRaw === "object") || (opts.needDisplay && typeof opts.needDisplay === "object") || (opts.needRawBase && typeof opts.needRawBase === "object") || (opts.previousNeeds && typeof opts.previousNeeds === "object")); + if (!hasExplicitNeedState) { + this.needs = { ...acquiredNeeds }; + this.needDisplay = { ...acquiredNeeds }; + this.needRaw = { ...acquiredNeeds }; + this.needRawBase = { ...acquiredNeeds }; + this.previousNeeds = { ...acquiredNeeds }; + } + if (!Object.prototype.hasOwnProperty.call(opts, "loneliness")) this.loneliness = Math.max(Number(this.loneliness) || 0, 90); + if (!Object.prototype.hasOwnProperty.call(opts, "hunger")) this.hunger = Math.max(Number(this.hunger) || 0, 22); + if (!Object.prototype.hasOwnProperty.call(opts, "energy")) this.energy = Math.min(Number(this.energy) || 0, 45); + if (!Object.prototype.hasOwnProperty.call(opts, "goodMode") && Array.isArray(SPRITES) && SPRITES.some(s => s.id === "jito")) this.goodMode = "jito"; + this.stress = typeof calculateStressFromNeeds === "function" ? calculateStressFromNeeds(this.needRaw || this.needs) : this.stress; + this.acquiredStressSpeechAt = Number.isFinite(opts.acquiredStressSpeechAt) ? opts.acquiredStressSpeechAt : ((this.world?.time || 0) + rand(2.8, 6.5)); + this.acquiredStressSpeechCooldownUntil = Number.isFinite(opts.acquiredStressSpeechCooldownUntil) ? opts.acquiredStressSpeechCooldownUntil : 0; + } this.facing = opts.facing ?? (stableUnit(this.familyKey, "facing") < 0.5 ? -1 : 1); this.visualFacing = opts.visualFacing ?? this.facing; this.facingLockUntil = opts.facingLockUntil ?? 0; @@ -164,6 +199,9 @@ class Tarinai { constructor(world, opts = {}) { this.explosionDisease = Boolean(opts.explosionDisease); this.explosionDiseaseTimer = opts.explosionDiseaseTimer ?? (this.explosionDisease ? 60 : 0); this.fightDisease = Boolean(opts.fightDisease); + this.mamekusaredakeDisease = Boolean(opts.mamekusaredakeDisease); + this.mamekusaredakeGrowth = Number.isFinite(opts.mamekusaredakeGrowth) ? opts.mamekusaredakeGrowth : 0; + this.mamekusaredakeSpeechAt = Number.isFinite(opts.mamekusaredakeSpeechAt) ? opts.mamekusaredakeSpeechAt : 0; this.fightDiseaseCooldown = opts.fightDiseaseCooldown ?? 0; this.lastBleedAt = opts.lastBleedAt ?? -999; this.favorite = Boolean(opts.favorite); diff --git a/js/tarinai_action_state.js b/js/tarinai_action_state.js index 83ed75a..79992da 100644 --- a/js/tarinai_action_state.js +++ b/js/tarinai_action_state.js @@ -4,12 +4,34 @@ const Tarinai = global.Tarinai; if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_action_state.js"); + function prepareTarinaiIntimidatePresentation(tarinai) { + if (!tarinai || tarinai.dead) return "intimidate"; + if (!tarinai.intimidateSpriteVariant) { + tarinai.intimidateSpriteVariant = tarinai.isAcquiredTarinai ? "intimidate_alt" : (Math.random() < 0.5 ? "intimidate_alt" : "intimidate"); + } + if (tarinai.intimidateSpriteVariant === "intimidate_alt" && !tarinai._intimidateAltAnnounced) { + tarinai._intimidateAltAnnounced = true; + tarinai.bubble?.("ぷきゅー!", 1.0, "rgba(92,62,34,0.82)", { force: true }); + const audio = global.TarinaiAudio; + if (audio?.playSample) audio.playSample("voice_intimidate_alt", 0.9, "voice") || audio.play?.("voice_intimidate_alt", { category: "voice", minGap: 0.9 }); + else audio?.play?.("voice_intimidate_alt", { category: "voice", minGap: 0.9 }); + } + return tarinai.intimidateSpriteVariant; + } + global.prepareTarinaiIntimidatePresentation = prepareTarinaiIntimidatePresentation; + Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({ setActionState(state, opts = {}) { if (this.dead) return false; const wasSleeping = this.state === "sleep" || this.sleeping; + const previousState = this.state; const nextState = state || "idle"; this.state = nextState; + if (nextState === "intimidate" || nextState === "ant_intimidate") prepareTarinaiIntimidatePresentation(this); + else if ((previousState === "intimidate" || previousState === "ant_intimidate") && nextState !== previousState) { + this.intimidateSpriteVariant = ""; + this._intimidateAltAnnounced = false; + } if ("target" in opts) this.target = opts.target; if (opts.reason != null) this.thought = String(opts.reason || ""); if (opts.clearTarget) this.target = null; @@ -204,7 +226,17 @@ this.fallTimer = Math.max(this.fallTimer || 0, opts.fallTimer); this.fallMax = Math.max(this.fallMax || 0, this.fallTimer); } - if (opts.bubble) this.bubble?.(opts.bubble, opts.bubbleCooldown ?? 0.9, opts.bubbleColor || "rgba(84,66,86,0.80)"); + if (opts.bubble && !this.isAcquiredTarinai) this.bubble?.(opts.bubble, opts.bubbleCooldown ?? 0.9, opts.bubbleColor || "rgba(84,66,86,0.80)"); + if (this.isAcquiredTarinai) { + const now = Number(this.world?.time || 0) || 0; + if (now >= Number(this.ochibiPanicVoiceAt || 0)) { + this.ochibiPanicVoiceAt = now + 2.4; + if (this.bubble?.("きょわいのだ...", 1.8, "rgba(84,66,86,0.80)", { force: true })) { + const a = global.TarinaiAudio; + if (a?.playSample) a.playSample("voice_ochibi_panic_005", 1.0, "voice") || a.play?.("voice_ochibi_panic_005", { category: "voice", minGap: 1.0 }); + } + } + } this.lastPanicCause = opts.cause || opts.reason || this.thought || "panic"; this.lastPanicAt = this.world?.time || 0; if (this.state === "panic" && !Number.isFinite(this.panicStartedAt)) this.panicStartedAt = this.lastPanicAt; diff --git a/js/tarinai_contact_item_system.js b/js/tarinai_contact_item_system.js index 0558fc9..4118c62 100644 --- a/js/tarinai_contact_item_system.js +++ b/js/tarinai_contact_item_system.js @@ -22,11 +22,9 @@ const d = contact.distance; tarinai.tarinaiChampionSince = 0; tarinai.becomeZunchiSlave?.({ locked: true, forcedItem: true }); tarinai.world?.syncTarinaiCountEntry?.(tarinai); - tarinai.bubble("\u9396", 2.4, "rgba(74,64,56,0.82)"); tarinai.world?.log?.(`${tarinai.name}\u306F${itemLabel}\u306B\u89E6\u308C\u3066\u305A\u3093\u3061\u3069\u308C\u3044\u306B\u306A\u3063\u305F\u3002`, "event", { participants: [tarinai], sound: false }); } else { tarinai.becomeTarinaiChampion?.({ force: true, forcePersonality: true, forcedItem: true }); - tarinai.bubble("\u738B", 2.4, "rgba(166,112,22,0.84)"); tarinai.world?.log?.(`${tarinai.name}\u306F${itemLabel}\u306B\u89E6\u308C\u3066\u305F\u308A\u306A\u3044\u738B\u306B\u306A\u3063\u305F\u3002`, "event", { participants: [tarinai], sound: false }); } global.TarinaiAchievements?.recordConsumableUse?.({ world: tarinai.world, tarinai, item: it, type: it.type, source: "contact" }); diff --git a/js/tarinai_food_interaction_system.js b/js/tarinai_food_interaction_system.js index e1c0eaa..046142d 100644 --- a/js/tarinai_food_interaction_system.js +++ b/js/tarinai_food_interaction_system.js @@ -3,6 +3,34 @@ // Food, medicine, grass, ant-corpse, and zunchi eating branch for Tarinai item interaction. (function (global) { + const OCHIBI_DIRTY_SPEECHES = [ + { id: "voice_ochibi_dirty_001", text: "くちゃいのだ" }, + { id: "voice_ochibi_dirty_002", text: "ずんちがめだまについたのだ!" }, + { id: "voice_ochibi_dirty_003", text: "おえええ!" }, + { id: "voice_ochibi_dirty_004", text: "ずんちがおくちについたのだ!" } + ]; + + function maybePlayOchibiDirtySpeech(tarinai, delta = 0) { + if (!tarinai || !tarinai.isAcquiredTarinai || tarinai.dead) return false; + if (!(delta > 0.01)) return false; + const now = Number(tarinai.world?.time || 0) || 0; + const nextAllowed = Number(tarinai.ochibiDirtySpeechCooldownUntil || 0) || 0; + const lastLevel = Number(tarinai.ochibiDirtySpeechLastLevel || 0) || 0; + const currentLevel = Number(tarinai.zunchiStain || 0) || 0; + if (now < nextAllowed && currentLevel < lastLevel + 5) return false; + if ((tarinai.nextBubbleAt || 0) > now) return false; + const idx = Math.floor(Math.random() * OCHIBI_DIRTY_SPEECHES.length) % OCHIBI_DIRTY_SPEECHES.length; + const line = OCHIBI_DIRTY_SPEECHES[idx]; + if (!line) return false; + const bubbled = tarinai.bubble?.(line.text, 4.8, "rgba(82,66,42,0.84)"); + if (!bubbled) return false; + const audio = global.TarinaiAudio; + if (audio?.playSample) audio.playSample(line.id, 1.5, "voice") || audio.play?.(line.id, { category: "voice", minGap: 1.5 }); + else if (audio?.play) audio.play(line.id, { category: "voice", minGap: 1.5 }); + tarinai.ochibiDirtySpeechCooldownUntil = now + 3.2; + tarinai.ochibiDirtySpeechLastLevel = currentLevel; + return true; + } function updateCandidate(tarinai, ctx, it, dt = 0) { if (!tarinai || tarinai.dead || !ctx || !it || it.dead) return false; if (it.type === "duplicator" && global.TarinaiSignalSystem?.powerOverride?.(it) === false) return false; @@ -157,7 +185,7 @@ if (it.type === "zunchi") { applyNeedShock(tarinai, { health: touch * dt * 7, safety: touch * dt * 4 }); if (touch > 0.08) tarinai.adjustPersonality("neuroticism", -dt * touch * 0.0045, "after repeated contact with poop."); } - tarinai.zunchiStain = clamp((tarinai.zunchiStain || 0) + touch * dt * (tarinai.isZunchiSlave ? 5.2 : 9.5), 0, 100); + { const __prev = Number(tarinai.zunchiStain || 0) || 0; tarinai.zunchiStain = clamp((tarinai.zunchiStain || 0) + touch * dt * (tarinai.isZunchiSlave ? 5.2 : 9.5), 0, 100); maybePlayOchibiDirtySpeech(tarinai, (Number(tarinai.zunchiStain || 0) || 0) - __prev); } const emergencyHunger = (tarinai.hunger || 0) >= 104 || foodNeed >= 94; const zunchiTooFresh = zunchiEatingBlocked(tarinai, it, tarinai.world) || (!emergencyHunger && !tarinai.isZunchiSlave && it.stage === "fresh" && (it.age || 0) < 150 && foodNeed < 94 && (tarinai.hunger || 0) < 104); if (canZunchiBite && !zunchiTooFresh) { @@ -171,7 +199,7 @@ if (it.type === "zunchi") { tarinai.recoverHealth(bite * (tarinai.isZunchiSlave ? 0.10 : 0.02)); applyNeedShock(tarinai, { health: bite * (tarinai.isZunchiSlave ? 0.2 : 1.2), safety: bite * (tarinai.isZunchiSlave ? 0.1 : 0.6) }); if (!tarinai.isZunchiSlave) tarinai.adjustPersonality("neuroticism", -bite * 0.0015, "after repeated contact with poop."); - tarinai.zunchiStain = clamp((tarinai.zunchiStain || 0) + bite * (tarinai.isZunchiSlave ? 0.75 : 1.75), 0, 100); + { const __prev = Number(tarinai.zunchiStain || 0) || 0; tarinai.zunchiStain = clamp((tarinai.zunchiStain || 0) + bite * (tarinai.isZunchiSlave ? 0.75 : 1.75), 0, 100); maybePlayOchibiDirtySpeech(tarinai, (Number(tarinai.zunchiStain || 0) || 0) - __prev); } tarinai.lastMealColor = "#6b8d3f"; tarinai.eatTimer = Math.max(tarinai.eatTimer, 1.0); tarinai.eatCooldown = rand(1.0, 1.22); diff --git a/js/tarinai_item_effects.js b/js/tarinai_item_effects.js index 8e3fd77..570057f 100644 --- a/js/tarinai_item_effects.js +++ b/js/tarinai_item_effects.js @@ -295,7 +295,7 @@ const x = this.x - side * this.radius * rand(0.72, 0.96); const y = this.y + this.radius * rand(0.25, 0.48); this.poopCount = (this.poopCount || 0) + 1; - this.world.spawnZunchi?.(x, y); + this.world.spawnZunchi?.(x, y, this); this.digest = Math.max(0, (this.digest || 0) - 3.0); const now = this.world?.time || 0; this.bubble("\u3076\u308a\u3085\u3063", 1.2, effectColor("laxative"), { force: true }); diff --git a/js/tarinai_needs_items.js b/js/tarinai_needs_items.js index c6040f3..6acd114 100644 --- a/js/tarinai_needs_items.js +++ b/js/tarinai_needs_items.js @@ -182,6 +182,15 @@ function updateNeeds(tarinai, world, dt = 0) { raw.health *= clamp(1 + Math.max(0, neuroticism) * 0.14, 0.9, 1.22); raw.fulfill *= clamp(1 + Math.max(0, openness) * 0.14, 0.9, 1.24); + // おちびは、通常個体よりも精神的な負荷が高い状態を基準にする。 + // ここで加算することで、初回の needs 再計算で初期ストレスが消えない。 + if (tarinai.isAcquiredTarinai) { + raw.health += 44; + raw.safety += 84; + raw.social += 20; + raw.fulfill += 34; + } + decayNeedSatisfaction(tarinai, dt); const effective = createDefaultNeeds(); const display = createDefaultNeeds(); @@ -651,7 +660,17 @@ function protectSleepSession(tarinai, world, needs) { const now = world?.time || 0; if (now >= (tarinai.nextSleepBubbleAt || 0)) { tarinai.nextSleepBubbleAt = now + rand(3.6, 5.6); - world?.spawnBubble?.(tarinai.x, tarinai.y - (tarinai.radius || 20) * 1.18, "Zzz...", "rgba(65,70,92,0.72)"); + if (tarinai.isAcquiredTarinai && Math.random() < 0.42) { + const sleepLine = Math.random() < 0.5 + ? { id: "voice_ochibi_sleep_002", text: "おててしゃん..." } + : { id: "voice_ochibi_sleep_003", text: "あんよしゃん..." }; + if (tarinai.bubble?.(sleepLine.text, 2.8, "rgba(65,70,92,0.72)", { force: true })) { + const a = globalThis.TarinaiAudio; + if (a?.playSample) a.playSample(sleepLine.id, 1.0, "voice") || a.play?.(sleepLine.id, { category: "voice", minGap: 1.0 }); + } + } else { + world?.spawnBubble?.(tarinai.x, tarinai.y - (tarinai.radius || 20) * 1.18, "Zzz...", "rgba(65,70,92,0.72)"); + } } return true; } @@ -898,7 +917,7 @@ function resolveNeedsCore(dt) { if (this.isZunchiSlave) return this.zunchiSlaveSpriteId(); const low = this.lowHealthSprite(); if (low) return low; - if ((this.state === "seek_food" || this.state === "idle" || this.state === "follow_parent") && (this.vy < -18 || (this.target && this.target.y < this.y - 36 && Math.abs(this.target.y - this.y) > Math.abs(this.target.x - this.x) * 0.55))) return "back"; + if (!this.isAcquiredTarinai && (this.state === "seek_food" || this.state === "idle" || this.state === "follow_parent") && (this.vy < -18 || (this.target && this.target.y < this.y - 36 && Math.abs(this.target.y - this.y) > Math.abs(this.target.x - this.x) * 0.55))) return "back"; if (this.explosionDisease && Math.sin((this.world?.time || 0) * 8) > 0.15) return "pokan"; if (this.fightDisease) return "stress_dizzy"; if ((this.oshiriByoZunchiStock || 0) >= 6) return this.variantSprite ? this.variantSprite("hurt") : "weak"; @@ -912,7 +931,9 @@ function resolveNeedsCore(dt) { if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") return "birth_ritual"; if (foodNeedForSprite >= 72) return "hungry_70"; if (this.defeatedTimer > 0.04) return this.variantSprite("flee"); - if (this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04) return this.variantSprite("intimidate"); + if (this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04) { + return globalThis.prepareTarinaiIntimidatePresentation?.(this) || this.intimidateSpriteVariant || "intimidate"; + } if (this.state === "panic" || this.state === "cursor_enemy" || this.fearTimer > 0.08 || this.intimidatedTimer > 0.08) return this.variantSprite("fear"); if (this.state === "fight" || this.state === "seek_enemy" || this.fightTimer > 0.04) return "angry"; if (this.stress > 62) return this.variantSprite("stress"); @@ -947,7 +968,7 @@ function resolveNeedsCore(dt) { const isCurrentIntimidator = this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04; const isCurrentIntimidated = !isCurrentIntimidator && this.intimidatedTimer > 0.04; const trappedNow = Number(this._bearTrapHoldUntil || 0) > now; - const forceImmediate = trappedNow || String(next || "").startsWith("zunchi_slave") || next === "birth_ritual" || next === "intimidate" || isCurrentIntimidator || isCurrentIntimidated || (this.visibleSpriteId === "intimidate" && next !== "intimidate"); + const forceImmediate = trappedNow || String(next || "").startsWith("zunchi_slave") || next === "birth_ritual" || String(next || "").startsWith("intimidate") || isCurrentIntimidator || isCurrentIntimidated || (String(this.visibleSpriteId || "").startsWith("intimidate") && !String(next || "").startsWith("intimidate")); if (!this.visibleSpriteId || forceImmediate) { this.visibleSpriteId = next; this.spriteLockUntil = forceImmediate ? now : now + 1.0; @@ -1043,6 +1064,7 @@ function resolveNeedsCore(dt) { }, canStartSunbath(options = {}) { + if (this.isAcquiredTarinai || this.mamekusaredakeDisease) return false; if (this.dead || this.sleeping) return false; if (!Number.isFinite(this.x) || !Number.isFinite(this.y)) return false; if (!Number.isFinite(this.world?.w) || !Number.isFinite(this.world?.h)) return false; @@ -1125,8 +1147,24 @@ function resolveNeedsCore(dt) { }, bubble(text, cooldown = 2.4, color = "rgba(42,36,29,0.78)", options = {}) { + const bubbleText = String(text || ""); + if (this.isAcquiredTarinai) { + const allowSubstring = ["Zzz", "あちゅい", "もぐ", "ぶるぶる"]; + const allowExact = new Set([ + "パパずんはどこなのだ?", "ママずんはどこなのだ?", "こんなはずじゃなかったのだ", "どうして", "おまえをゆるさないのだ", "のろってやるのだ", + "てあしをかえせなのだ", "すべすべしっぽしゃんが", "くちょにんげん", + "ぼくはゆうしゅうなのだ", "しあわせなのだ", "てあしがなくてもげんきなのだ", "へこへこしたいのだ", "のだのだのだ", + "くちょおやがいなくてもぼくはたたかえるのだ", "ぼくはげんきいっぱいなのだ", "かっきにみちみちしているのだ", "ずんちでそう", + "くちゃいのだ", "ずんちがめだまについたのだ!", "おえええ!", "ずんちがおくちについたのだ!", + "いちゃい!", "ごふっ", "ああああああああ", + "ぷきゅー!", "くるち、くるち", "おててしゃん...", "あんよしゃん...", "ちね!", "きょわいのだ...", "びりびりちゃ!", "おみじゅ...", "なんでぼくがこんなめに..." + ]); + const plainPunct = /^[!?!?…\.。ー〜~]+$/.test(bubbleText); + const allowed = plainPunct || allowExact.has(bubbleText) || allowSubstring.some(part => bubbleText.includes(part)); + if (!allowed) return false; + } if (!options.force && this.world.time < this.nextBubbleAt) return false; - const spawned = this.world.spawnBubble(this.x, this.y - this.radius * 1.35, text, color); + const spawned = this.world.spawnBubble(this.x, this.y - this.radius * 1.35, bubbleText, color); if (!spawned) return false; this.nextBubbleAt = this.world.time + cooldown; return true; diff --git a/js/tarinai_render.js b/js/tarinai_render.js index 6b26cf0..dc3e1e5 100644 --- a/js/tarinai_render.js +++ b/js/tarinai_render.js @@ -1,5 +1,13 @@ "use strict"; +function drawAcquiredTarinaiOverlay(ctx, tarinai, drawW, drawH) { + if (!tarinai?.isAcquiredTarinai) return false; + const overlay = getRenderableImage("acquired_tarinai_overlay", ""); + if (!overlay) return false; + ctx.drawImage(overlay, -drawW / 2, -drawH / 2, drawW, drawH); + return true; +} + function tarinaiSpriteReversesFacing(sprite) { const id = String(sprite || ""); return id === "back" || id === "tarinai12_back" || /(^|_)12_back$/i.test(id); @@ -170,6 +178,7 @@ function tarinaiSpriteReversesFacing(sprite) { ctx.fill(); ctx.stroke(); } + drawAcquiredTarinaiOverlay(ctx, this, drawW, drawH); ctx.restore(); if (showDecor) { @@ -200,6 +209,11 @@ function tarinaiSpriteReversesFacing(sprite) { const showDecor = shouldShowTarinaiDecor(this); if (sunbathActive) this.preloadSunbathSprites?.(); let sprite = this.spriteId(); + if (this.isAcquiredTarinai && tarinaiSpriteReversesFacing(sprite)) { + const fallbackOchibiSprite = this.applyTemperatureSprite?.(this.goodMode || this.type || "smile") || this.goodMode || this.type || "smile"; + this.visibleSpriteId = ""; + sprite = fallbackOchibiSprite; + } if (!sunbathActive && String(sprite || "").startsWith("sunbath")) { this.visibleSpriteId = ""; this.spriteLockUntil = 0; @@ -273,7 +287,7 @@ function tarinaiSpriteReversesFacing(sprite) { drawW *= 1.16; drawH *= 1.24; } - if (sprite === "intimidate") { + if (String(sprite || "").startsWith("intimidate")) { drawW *= 1.32; drawH *= 1.32; } @@ -526,6 +540,12 @@ function tarinaiSpriteReversesFacing(sprite) { ctx.stroke(); ctx.restore(); } + drawAcquiredTarinaiOverlay(ctx, this, dw, dh); + globalThis.TarinaiMamekusaredakeSystem?.drawOnTarinai?.(ctx, this, dw, dh, faceRight, img, { + originX: this.x + intimidateShake + coldShakeX, + originY: this.y + bob + fearShakeY + coldShakeY, + angle + }); if (img && (this.zunchiStain || 0) > 3) { const stainCanvas = getCachedStainOverlayCanvas(renderSprite || sprite, img, cacheW, cacheH, clamp((this.zunchiStain || 0) / 100, 0, 1), this.zunchiStainSeed || 0.5); if (stainCanvas) { diff --git a/js/tarinai_social_move_life.js b/js/tarinai_social_move_life.js index 373dbf1..6c32761 100644 --- a/js/tarinai_social_move_life.js +++ b/js/tarinai_social_move_life.js @@ -376,6 +376,17 @@ o._achievementLastFightDamageAt = this.world.time || 0; this.damage(damageToThis, "\u55a7\u5629"); o.damage(damageToOther, "\u55a7\u5629"); + const nowFightVoice = Number(this.world?.time || 0) || 0; + const ochibiAttackVoice = attacker => { + if (!attacker?.isAcquiredTarinai || nowFightVoice < Number(attacker.ochibiFightHitVoiceAt || 0)) return; + attacker.ochibiFightHitVoiceAt = nowFightVoice + 1.25; + if (attacker.bubble?.("ちね!", 1.0, "rgba(120,54,44,0.84)", { force: true })) { + const a = globalThis.TarinaiAudio; + if (a?.playSample) a.playSample("voice_ochibi_fight_hit_004", 0.8, "voice") || a.play?.("voice_ochibi_fight_hit_004", { category: "voice", minGap: 0.8 }); + } + }; + if (damageToOther > 0.01) ochibiAttackVoice(this); + if (damageToThis > 0.01) ochibiAttackVoice(o); this.world.maybeDefeatFromFightDamage?.(this, o, damageToThis); this.world.maybeDefeatFromFightDamage?.(o, this, damageToOther); this.hurtTimer = Math.max(this.hurtTimer, 1.4); @@ -815,6 +826,7 @@ if (this.sleepDisease) return "\u306d\u3080\u308a\u75c5"; if (this.explosionDisease) return "\u7206\u767a\u75c5"; if (this.fightDisease) return "\u304d\u305a\u3064\u304d\u75c5"; + if (this.mamekusaredakeDisease) return "マメクサレダケ病"; return ""; }; if (this.energy <= 0) { @@ -834,6 +846,7 @@ this.dead = true; this.deathReason = finalReason; this.deathTime = Number(this.world?.time || 0) || 0; + if (this.mamekusaredakeDisease) globalThis.TarinaiMamekusaredakeSystem?.plantFromHost?.(this); this.world.syncTarinaiCountEntry?.(this); this.world.deadCount += 1; globalThis.TarinaiAchievements?.evaluateEvent?.(this.world, "population", { tarinai: this, delta: -1, reason: finalReason }); diff --git a/js/tarinai_update_step_frame.js b/js/tarinai_update_step_frame.js index 637fe39..006d171 100644 --- a/js/tarinai_update_step_frame.js +++ b/js/tarinai_update_step_frame.js @@ -18,8 +18,62 @@ return global.deterministicChance(worldRef, salt, chance, ...parts); } + const OCHIBI_HIGH_STRESS_SPEECHES = [ + { id: "voice_acquired_stress_001", text: "パパずんはどこなのだ?" }, + { id: "voice_acquired_stress_002", text: "ママずんはどこなのだ?" }, + { id: "voice_acquired_stress_003", text: "こんなはずじゃなかったのだ" }, + { id: "voice_acquired_stress_004", text: "どうして" }, + { id: "voice_acquired_stress_005", text: "おまえをゆるさないのだ" }, + { id: "voice_acquired_stress_006", text: "のろってやるのだ" }, + { id: "voice_acquired_stress_007", text: "てあしをかえせなのだ" }, + { id: "voice_acquired_stress_008", text: "すべすべしっぽしゃんが" }, + { id: "voice_acquired_stress_009", text: "くちょにんげん" }, + { id: "voice_ochibi_stress_008", text: "なんでぼくがこんなめに..." } + ]; + const OCHIBI_LOW_STRESS_SPEECHES = [ + { id: "voice_ochibi_calm_001", text: "ぼくはゆうしゅうなのだ" }, + { id: "voice_ochibi_calm_002", text: "しあわせなのだ" }, + { id: "voice_ochibi_calm_003", text: "てあしがなくてもげんきなのだ" }, + { id: "voice_ochibi_calm_004", text: "へこへこしたいのだ" }, + { id: "voice_ochibi_calm_005", text: "のだのだのだ" }, + { id: "voice_ochibi_calm_006", text: "くちょおやがいなくてもぼくはたたかえるのだ" }, + { id: "voice_ochibi_calm_007", text: "ぼくはげんきいっぱいなのだ" }, + { id: "voice_ochibi_calm_008", text: "かっきにみちみちしているのだ" }, + { id: "voice_ochibi_calm_009", text: "ずんちでそう" } + ]; + + function maybePlayOchibiIdleSpeech(t) { + if (!t || t.dead || !t.isAcquiredTarinai || String(t.state || "") !== "idle") return false; + const now = Number(t.world?.time || 0) || 0; + const stress = Number(t.stress || 0) || 0; + const highStress = stress >= 68; + const pool = highStress ? OCHIBI_HIGH_STRESS_SPEECHES : OCHIBI_LOW_STRESS_SPEECHES; + if (!pool.length) return false; + if (!Number.isFinite(t.acquiredStressSpeechAt)) t.acquiredStressSpeechAt = now + detRange(t.world, "ochibi-idle-speech-init", 2.8, 6.5, t); + if ((t.acquiredStressSpeechCooldownUntil || 0) > now || (t.nextBubbleAt || 0) > now || now < t.acquiredStressSpeechAt) return false; + const idx = Math.max(0, Math.min(pool.length - 1, Math.floor(detRange(t.world, highStress ? "ochibi-high-line" : "ochibi-low-line", 0, pool.length, t, Math.floor(now * 10))))); + const line = pool[idx] || pool[0]; + if (!line) return false; + const color = highStress ? "rgba(58,48,63,0.82)" : "rgba(62,84,45,0.78)"; + const bubbled = typeof t.bubble === "function" ? t.bubble(line.text, 5.8, color) : false; + if (!bubbled) return false; + const audio = global.TarinaiAudio; + if (audio?.playSample) audio.playSample(line.id, 1.5, "voice") || audio.play(line.id, { category: "voice", minGap: 1.5 }); + else if (audio?.play) audio.play(line.id, { category: "voice", minGap: 1.5 }); + t.acquiredStressSpeechCooldownUntil = now + 1.5; + t.acquiredStressSpeechAt = now + detRange(t.world, highStress ? "ochibi-high-next" : "ochibi-low-next", highStress ? 6.5 : 7.0, highStress ? 12.0 : 13.0, t, line.id, Math.floor(now)); + return true; + } + function update(t, dt, context = {}) { if (!t || t.dead) return { done: true }; + if (t.isAcquiredTarinai && (t.state === "sunbath" || (t.sunbathTimer || 0) > 0.04)) { + t.sunbathTimer = 0; + t.sunbathLeisure = false; + t.sunbathCooldown = Math.max(t.sunbathCooldown || 0, 18); + t.visibleSpriteId = ""; + t.setActionState?.("idle", { target: null, reason: "おちびは日光浴をしない", sleeping: false, clearTarget: true }); // acquired-sunbath-cancel + } t.prevX = t.x; t.prevY = t.y; @@ -47,6 +101,7 @@ t._achievementLaxativeActiveThisFrame = Boolean(t.hasItemEffect?.("laxative")); if (t.updateBurning) t.updateBurning(dt); if (t.updateItemEffects) t.updateItemEffects(dt); + global.TarinaiMamekusaredakeSystem?.updateTarinai?.(t, dt); if (t.hasItemEffect?.("sedative")) { t.fearTimer = Math.max(0, (t.fearTimer || 0) - dt * 1.8); t.intimidatedTimer = Math.max(0, (t.intimidatedTimer || 0) - dt * 1.4); @@ -139,10 +194,11 @@ t.setActionState?.("idle", { target: null, reason: "\u8db3\u3064\u307c\u306e\u7a81\u8d77\u306b\u3064\u307e\u3065\u3044\u305f\u3002", sleeping: false, clearTarget: false }); if ((t.world?.time || 0) >= (t.nextBubbleAt || 0)) t.bubble?.("\u3046\u3063", 1.6, "rgba(86,70,96,0.78)"); } - if (t.stress > 92 && t.state !== "sleep" && t.state !== "fight" && t.state !== "eat" && detChance(t.world, "stress-breath-bubble", dt * 0.050, t)) { + maybePlayOchibiIdleSpeech(t); + if (!t.isAcquiredTarinai && t.stress > 92 && t.state !== "sleep" && t.state !== "fight" && t.state !== "eat" && detChance(t.world, "stress-breath-bubble", dt * 0.050, t)) { t.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)"); } - if (t.state === "idle" && t.stress < 78 && detChance(t.world, "idle-bubble", dt * 0.018, t)) { + if (!t.isAcquiredTarinai && t.state === "idle" && t.stress < 78 && detChance(t.world, "idle-bubble", dt * 0.018, t)) { t.bubble(pick(["\u306f\u3046", "\u306f\u3046\u3045", "\u306f\u3046\u3063", "\u306f\u3045"]), 4.2, "rgba(62,84,45,0.76)"); } if (t.hurtTimer > 0 && detChance(t.world, "hurt-bleed-effect", dt * 5.2, t)) { @@ -176,7 +232,14 @@ const stressRate = excess * (0.66 + Math.min(20, excess) * 0.035); applyNeedShock(t, { safety: stressRate * dt, health: stressRate * dt * 0.42 }); if ((t.world?.time || 0) >= (t.nextBubbleAt || 0) && detChance(t.world, "temperature-stress-bubble", dt * 0.18, t)) { - t.bubble(tempStatus.direction === "cold" ? "\u3076\u308B\u3076\u308B" : "\u3042\u3061\u3085\u3044", 2.4, "rgba(76,62,72,0.78)"); + if (t.isAcquiredTarinai && tempStatus.direction === "hot" && Math.random() < 0.68) { + if (t.bubble("おみじゅ...", 2.4, "rgba(76,62,72,0.78)")) { + const a = global.TarinaiAudio; + if (a?.playSample) a.playSample("voice_ochibi_hot_007", 1.2, "voice") || a.play?.("voice_ochibi_hot_007", { category: "voice", minGap: 1.2 }); + } + } else { + t.bubble(tempStatus.direction === "cold" ? "ぶるぶる" : "あちゅい", 2.4, "rgba(76,62,72,0.78)"); + } } } if ((tempStatus.harmExcess || 0) > 0) { diff --git a/js/ui_input_shared.js b/js/ui_input_shared.js index 171a08f..76f3480 100644 --- a/js/ui_input_shared.js +++ b/js/ui_input_shared.js @@ -195,6 +195,25 @@ } function beginGrab(p, clientX, clientY, options = {}) { + // マメクサレダケ病の除去判定は、キノコ自体の小さな見た目ではなく + // 通常のたりないをつまむ時と同じ個体当たり判定を使う。 + // 一度命中すれば recover() が病気フラグを解除するため、表示中の全キノコが一括で抜ける。 + let infectedTarinai = null; + for (let i = (world.tarinai?.length || 0) - 1; i >= 0; i--) { + const candidate = world.tarinai[i]; + if (!candidate || candidate.dead || !candidate.mamekusaredakeDisease || world.isTarinaiHiddenInNestBox?.(candidate)) continue; + const hit = candidate.contains + ? candidate.contains(p.x, p.y) + : distXY(p.x, p.y, candidate.x, candidate.y) <= (candidate.radius || 22) * 1.4; + if (hit) { infectedTarinai = candidate; break; } + } + if (infectedTarinai) { + global.TarinaiHistory.capture(world, "pinch-mamekusaredake"); + global.TarinaiMamekusaredakeSystem?.recover?.(infectedTarinai, "つまんで取れた"); + audio.grab?.(); + world.drawListDirty = true; + return true; + } const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null; if (!target) { if (options.notifyNoTarget) showToast("\u3064\u307e\u3081\u308b\u5bfe\u8c61\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); diff --git a/js/ui_layout_dialogs.js b/js/ui_layout_dialogs.js index a027eef..f9e71e1 100644 --- a/js/ui_layout_dialogs.js +++ b/js/ui_layout_dialogs.js @@ -228,6 +228,11 @@ function selectTool(tool) { const wasCopyArmed = world.tool === "copy" && tool === "copy" && Boolean(world.copyBuffer); const result = window.TarinaiCommands.dispatch(world, { type: "tool.select", toolId: tool }); const activeTool = result?.toolId || world.tool || tool; + // A freshly selected tool must not inherit drag/click-suppression state from + // the previous tool. Reset these transients for every tool, not for one item. + if (typeof uiCache === "object" && uiCache) { + for (const key of ["areaDeleteMoved", "hoseMoved", "grabMoved", "shootSuppressClick", "panMoved"]) uiCache[key] = false; + } if (!["rope", "rod", "spring", "wire", "insulated_wire"].includes(activeTool)) world.pendingLinkEndpoint = null; if (activeTool !== "copy") world.copyBuffer = null; else if (wasCopyArmed) { diff --git a/js/ui_selected.js b/js/ui_selected.js index 0f39cd0..627f160 100644 --- a/js/ui_selected.js +++ b/js/ui_selected.js @@ -222,7 +222,7 @@ function renderSelected() { relationEntries(t, "friend").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), relationEntries(t, "enemy").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), fightWinRateLabel(t), t.isZunchiSlave ? 1 : 0, t.isTarinaiChampion ? 1 : 0, fmt(t.totalFightWins || 0), fmt(t.totalFightLosses || 0), fmt(t.loveMochiTimer || 0), fmt(t.fightMochiTimer || 0), t.zunchiDisease ? 1 : 0, fmt(t.zunchiDiseaseSeverity || 0), fmt(t.zunchiStain || 0), - t.sleepDisease ? 1 : 0, t.explosionDisease ? 1 : 0, fmt(t.explosionDiseaseTimer || 0), t.fightDisease ? 1 : 0, t.favorite ? 1 : 0, + t.sleepDisease ? 1 : 0, t.explosionDisease ? 1 : 0, fmt(t.explosionDiseaseTimer || 0), t.fightDisease ? 1 : 0, t.mamekusaredakeDisease ? 1 : 0, fmt(t.mamekusaredakeGrowth || 0), t.favorite ? 1 : 0, personalitySnapshot, personalityDailySnapshot(t), ["life", "attack", "speed", "size"].map(key => geneticPercentText(t, key)).join(","), geneticComfortTemperatureText(t), fmt((t.growth || 0) * 100), itemEffectSummary.map(e => `${e.id}:${e.detail}`).join(","), @@ -241,6 +241,7 @@ function renderSelected() { if (t.sleepDisease) effectBadges.push(`\u306d\u3080\u308a\u75c5`); if (t.explosionDisease) effectBadges.push(`\u7206\u767a\u75c5 ${fmt(t.explosionDiseaseTimer || 0)}`); if (t.fightDisease) effectBadges.push(`\u304d\u305a\u3064\u304d\u75c5`); + if (t.mamekusaredakeDisease) effectBadges.push(`マメクサレダケ病 ${fmt((t.mamekusaredakeGrowth || 0) * 100)}%`); if ((t.loveMochiTimer || 0) > 0.1) effectBadges.push(`\u3078\u3053\u9905 ${fmt(t.loveMochiTimer)}`); if ((t.fightMochiTimer || 0) > 0.1) effectBadges.push(`\u3051\u3093\u304b\u9905 ${fmt(t.fightMochiTimer)}`); const personalityTagHtml = personalityTags.length diff --git a/js/ui_tools.js b/js/ui_tools.js index 217b43a..0d45972 100644 --- a/js/ui_tools.js +++ b/js/ui_tools.js @@ -371,6 +371,7 @@ function attachToolSpritePreview(btn, def, toolId) { const itemType = def?.itemType || toolId; if (!btn || !itemType || !def?.placeable || TEXT_ICON_TOOL_IDS.has(toolId)) return; if (toolId === "reciprocator") return; + if (toolId === "mamekusaredake") return; if (toolId === "ball" || itemType === "ball") return; if (typeof window.drawToolItemPreview !== "function") return; diff --git a/js/version.js b/js/version.js index c01e149..12c278e 100644 --- a/js/version.js +++ b/js/version.js @@ -1,8 +1,8 @@ "use strict"; (function () { - const APP_VERSION = "39.18.3"; - const APP_BUILD = "tarinai-ui-v39-18-3"; + const APP_VERSION = "39.18.18"; + const APP_BUILD = "tarinai-ui-v39-18-18"; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; diff --git a/js/world_combat_effects.js b/js/world_combat_effects.js index 2ddfebd..bc67a41 100644 --- a/js/world_combat_effects.js +++ b/js/world_combat_effects.js @@ -268,7 +268,7 @@ this.spawnFallEffect(t.x, t.y + t.radius * 0.45, 1.1 + p); const explosionChance = window.TarinaiDiseaseRegistry.infectionChance("disease_explosion", 0.15) ?? 0.15; if (!t.dead && p > 0.22 && deterministicChance(this, "firecracker-explosion-disease", t.diseaseChance ? t.diseaseChance(explosionChance) : explosionChance, it, t)) t.infectExplosionDisease?.(it); - if (!t.dead && deterministicChance(this, "firecracker-panic-bubble", 0.45, it, t)) this.spawnBubble(t.x, t.y - t.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); + if (!t.dead && !t.isAcquiredTarinai && deterministicChance(this, "firecracker-panic-bubble", 0.45, it, t)) this.spawnBubble(t.x, t.y - t.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); } if (typeof damageNearbyStructures === "function") damageNearbyStructures(this, x, y, blastRadius, 34 * (it.blastScale || 1), it); this.applyExplosionExternalForces?.(x, y, blastRadius, it, it.blastScale || 1, "firecracker"); @@ -368,7 +368,7 @@ this.spawnFallEffect(o.x, o.y + o.radius * 0.45, 1.1 + q); const explosionChance = window.TarinaiDiseaseRegistry.infectionChance("disease_explosion", 0.15) ?? 0.15; if (!o.dead && q > 0.22 && deterministicChance(this, "disease-explosion-spread", o.diseaseChance ? o.diseaseChance(explosionChance) : explosionChance, t, o)) o.infectExplosionDisease?.(t); - if (!o.dead && deterministicChance(this, "disease-explosion-panic-bubble", 0.45, t, o)) this.spawnBubble(o.x, o.y - o.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); + if (!o.dead && !o.isAcquiredTarinai && deterministicChance(this, "disease-explosion-panic-bubble", 0.45, t, o)) this.spawnBubble(o.x, o.y - o.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); } this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u767a\u75c5", { push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * blastScale, @@ -568,6 +568,9 @@ }, spawnZunchi(x, y, source = null) { + if (source?.mamekusaredakeDisease && Math.random() < 0.05) { + globalThis.TarinaiMamekusaredakeSystem?.recover?.(source, "ずんちで取れた"); + } const spawnX = clamp(x, 40, this.w - 40); const spawnY = clamp(y, 40, this.h - 40); const sand = this.toiletSandAt?.(spawnX, spawnY) || null; diff --git a/js/world_family_social.js b/js/world_family_social.js index 4b3e6ba..d31af1a 100644 --- a/js/world_family_social.js +++ b/js/world_family_social.js @@ -998,9 +998,11 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) { spawnChild(a, b, index = 0, total = 1) { const zunchiSlaveChild = !!(a.isZunchiSlave && b.isZunchiSlave); + const ochibiChild = !!(a.isAcquiredTarinai && b.isAcquiredTarinai); const child = this.addTarinai({ seedParents: [a, b], isZunchiSlave: zunchiSlaveChild, + isAcquiredTarinai: ochibiChild, age: 0, x: (a.x + b.x) / 2 + rand(-14, 14) + (index - (total - 1) / 2) * 18, y: (a.y + b.y) / 2 + rand(-14, 14) + (total > 1 ? rand(-10, 10) : 0), @@ -1115,6 +1117,7 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) { target.intimidationCooldown = Math.max(target.intimidationCooldown || 0, deterministicRange(this, "fight-mochi-intimidate-repeat-cooldown-target", 4.5, 7.5, actor, target)); actor.intimidateTimer = Math.max(actor.intimidateTimer || 0, deterministicRange(this, "fight-mochi-intimidate-actor-timer", 0.32, 0.58, actor, target)); actor.intimidateTargetId = target.id; + globalThis.prepareTarinaiIntimidatePresentation?.(actor); actor.surpriseTimer = Math.max(actor.surpriseTimer || 0, 0.28); target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "fight-mochi-intimidate-target-timer", 0.30, 0.56, actor, target)); const targetProfile = socialPersonalityProfile(target); @@ -1126,7 +1129,7 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) { size: actor.radius * 0.62, color: "rgba(154, 88, 42, 0.74)", }); - this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "!", "rgba(120,62,38,0.82)"); + if (actor.intimidateSpriteVariant !== "intimidate_alt" && !actor.isAcquiredTarinai) this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "!", "rgba(120,62,38,0.82)"); return true; }, @@ -1189,7 +1192,7 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) { size: actor.radius * 0.55, color: "rgba(145, 106, 55, 0.70)", }); - this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "\u306f\u3046\u30fc\uff01", "rgba(92,62,34,0.82)"); + if (actor.intimidateSpriteVariant !== "intimidate_alt" && !actor.isAcquiredTarinai) this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "\u306f\u3046\u30fc\uff01", "rgba(92,62,34,0.82)"); target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "intimidation-target-prep-timer", 0.28, 0.52, actor, target)); target.fearTimer = Math.max(target.fearTimer, 0.65 * targetProfile.fear); if (!deterministicChance(this, "intimidation-success", successChance, actor, target)) { @@ -1212,7 +1215,7 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) { target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "intimidation-success-timer", 0.34, 0.70, actor, target)); if (target.enterPanic) target.enterPanic({ threat: actor, target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", fear: 1.35, wake: true, cause: "intimidated" }); else { target.fearTimer = Math.max(target.fearTimer, 1.35 * targetProfile.fear); target.setActionState?.("panic", { target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", wake: true }); } - this.spawnBubble(target.x, target.y - target.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); + if (!target.isAcquiredTarinai) this.spawnBubble(target.x, target.y - target.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); target.vx += (target.x < actor.x ? -1 : 1) * deterministicRange(this, "intimidation-knockback-x", 26, 54, actor, target); target.vy += deterministicRange(this, "intimidation-knockback-y", -18, 18, actor, target); actor.adjustRelation(target, -0.08, 0.06, "intimidate"); diff --git a/js/world_placement_log.js b/js/world_placement_log.js index 9b0802d..e0ed9ef 100644 --- a/js/world_placement_log.js +++ b/js/world_placement_log.js @@ -1539,7 +1539,7 @@ handleClick(x, y) { const tool = this.tool; - if (!isPlacementTool(tool) && !["delete", "area_delete", "copy", "poke", "pinch", "water_hose", "rope", "rod", "spring", "wire", "insulated_wire", "new", "bell"].includes(tool)) { + if (!isPlacementTool(tool) && !["delete", "area_delete", "copy", "poke", "pinch", "water_hose", "rope", "rod", "spring", "wire", "insulated_wire", "new", "acquired_tarinai", "bell"].includes(tool)) { const editable = findEditableItemAt(this, x, y); if (editable && openEditableItemEditor(editable, this)) { global.TarinaiCommands.setSelection(this, null) || (this.selected = null); @@ -1629,7 +1629,28 @@ global.TarinaiAchievements?.recordManualTarinaiAdded?.({ world: this, tarinai: t, tool: "new" }); return; } - + + if (tool === "acquired_tarinai") { + const spot = this.edgeSpawnPoint(x, y, 42, true); + const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, generation: 1, entryTimer: 2.0, isAcquiredTarinai: true }); + if (!t) { showToast(`\u305f\u308a\u306a\u3044\u500b\u4f53\u6570\u306f${this.tarinaiPopulationLimit}\u5339\u307e\u3067\u3067\u3059\u3002`); return; } + t.wanderAngle = spot.angle; + if (Math.random() < 0.5 && typeof t.enterPanic === "function") { + t.enterPanic({ + target: null, + reason: "おちびになって混乱している", + fear: 1.25, + wake: true, + cause: "acquired_tarinai_spawn", + bubble: "!!", + bubbleCooldown: 0.75 + }); + } + global.TarinaiAchievements?.recordManualTarinaiAdded?.({ world: this, tarinai: t, tool: "acquired_tarinai" }); + return; + } + + if (tool === "copy") { this.useCopyToolAt?.(x, y); return; } if (this.useToolAt?.(x, y)) return; diff --git a/service-worker.js b/service-worker.js index 8b93dcc..29450c2 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,6 +1,6 @@ "use strict"; -const APP_VERSION = "39.18.3"; +const APP_VERSION = "39.18.18"; const CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const v = `v=${APP_VERSION}`; // Static runtime list synchronized with app_manifest.json for this release. @@ -19,6 +19,12 @@ const mediaAssetNames = [ "assets/objects/zunchi.webp", "assets/objects/zunchi_02.webp", "assets/sounds/achievement_unlock.mp3", + "assets/sounds/acquired_stress_001.wav", + "assets/sounds/acquired_stress_002.wav", + "assets/sounds/acquired_stress_003.wav", + "assets/sounds/acquired_stress_004.wav", + "assets/sounds/acquired_stress_005.wav", + "assets/sounds/acquired_stress_006.wav", "assets/sounds/bicycle_bell.mp3", "assets/sounds/firecracker_explosion.mp3", "assets/sounds/major_damage_01.wav", @@ -36,6 +42,14 @@ const mediaAssetNames = [ "assets/sounds/voice_007_sunbath_pokapoka.wav", "assets/sounds/voice_008_temperature_buruburu.wav", "assets/sounds/voice_009_temperature_achui.wav", + "assets/sounds/intimidate_alt_001.wav", + "assets/sounds/ochibi_sleep_002.wav", + "assets/sounds/ochibi_sleep_003.wav", + "assets/sounds/ochibi_fight_hit_004.wav", + "assets/sounds/ochibi_panic_005.wav", + "assets/sounds/ochibi_electric_006.wav", + "assets/sounds/ochibi_hot_007.wav", + "assets/sounds/ochibi_stress_008.wav", "assets/sprites/tarinai_01_smile.webp", "assets/sprites/tarinai_02_angry.webp", "assets/sprites/tarinai_03_teary.webp", @@ -75,9 +89,12 @@ const mediaAssetNames = [ "assets/sprites/tarinai_37_cold_01.webp", "assets/sprites/tarinai_38_cold_02.webp", "assets/sprites/tarinai_39_cold_03.webp", + "assets/sprites/tarinai_40_intimidate_alt.webp", + "assets/sprites/tarinai_acquired_overlay.png", "assets/ui/apple-touch-icon.png", "assets/ui/ecology_hp_stress.webp", "assets/ui/favicon.png", + "assets/ui/tool_acquired_tarinai.webp", "assets/ui/tool_ammo.webp", "assets/ui/tool_ant_nest.webp", "assets/ui/tool_ball.webp", @@ -173,10 +190,13 @@ self.addEventListener("fetch", event => { if (isStaticAsset(request)) { const responsePromise = caches.match(request).then(async cached => { if (cached) return cached; - // Media are precached at their canonical URL. Match versioned and unversioned - // requests to the same entry so dynamically requested assets remain offline-safe. - const canonical = await caches.match(request, { ignoreSearch: true }); - if (canonical) return canonical; + // Media are precached at their canonical URL. JS/CSS query versions must not + // resolve to an older cached file, otherwise UI fixes can remain stale. + const isCodeAsset = /\.(?:js|css)$/i.test(url.pathname); + if (!isCodeAsset) { + const canonical = await caches.match(request, { ignoreSearch: true }); + if (canonical) return canonical; + } const response = await fetch(request); await putCacheSafely(request, response); return response;