This commit is contained in:
33333-33333 2026-06-23 18:11:42 +09:00
commit 54aff02658
27 changed files with 1700 additions and 1034 deletions

View file

Binary file not shown.

View file

@ -0,0 +1 @@
level=none expiry=0

View file

@ -0,0 +1 @@
{"user_experience_metrics.stability.exited_cleanly":false,"variations_crash_streak":0}

View file

@ -1,123 +1,23 @@
# Patch notes
# v15.24.2 needs compliance fix
## 15.20.16
This patch focuses on making the implementation closer to the need-system prompt.
- Added canonical item visual, food, and disease registries; wired food decay/visual helpers and mystery-drug disease sampling to registry data.
- Generated the tool palette from `TOOL_CATEGORIES` at runtime and removed duplicated category markup from `index.html`.
- Added compact family-tree personality tags, daily personality-change details, and selected-Tarinai rolling recent-change causes for personality, damage, item-effect, disease, and ant-carry changes.
- Added `InputModeManager` for touch-first mobile classification and `WeatherSystem` for weather transitions/rain-water spawning.
- Added central domain events for placement, eating, decay, effects, diseases, damage, death, birth, fights, ants, weather, and rain item drops.
- Moved item-effect modifiers, active summaries, apply/tick behavior callbacks, and food interest class queries into registries.
- Moved explosion-disease base infection chance reads into `DiseaseRegistry` while preserving protein/niterop disease multipliers.
- Moved serving-food nutrition and hunger-relief reads into `FoodRegistry`, including laxative's zero hunger relief.
- Routed direct love/fight/sleep item eating through `EffectRegistry` while preserving the previous bite-specific stress, fear, and sleep-disease chance behavior.
- Routed sleep drug, rain-water item, and water-bowl field visuals through `item_visual_registry.js`; added water-bowl icon-generation support.
- Moved canonical sound gains, sample paths, categories, cooldowns, and ID category mapping out of `audio.js` into `sound_pack.js`.
- Added named ant ecology extension hooks/comments without changing ant balance.
- Bumped cache/version references to `15.20.16`; documented the new source-of-truth rules.
## Fixed
## 15.20.12
- Kept the six-need system as the behavior driver.
- Kept needs clamped/quantized and displayed as 0-10 values.
- Kept tied top-need random selection and tie reason text.
- Kept StructureRegistry and both buildable structures: grass_bed and plushie.
- Removed remaining mojibake strings in the main need/action implementation file.
- Changed safety fleeing so normal safety behavior no longer directly enters panic.
- Reworked stress so direct stress mutation is removed from the main behavior files.
- Stress is now recalculated from needs or projected needs via calculateStressFromNeeds.
- Added applyNeedShock/applyNeedRelief helpers and converted shock/relief events into need changes.
- Reworked enterPanic as a legacy adapter that converts old calls into need shock first.
- Panic/intimidate are now short reaction states triggered through need shock logic.
- Updated cache/version strings to 15.24.2.
- Updated documentation after inspecting the current game surface: field choices, startup flow, tool groups, chart/family/debug surfaces, localStorage settings, and validation fallback when Node is unavailable.
- Removed unused `css/styles.css` shim and no-op `js/ui_family.js` loader entry; refactored duplicate world initialization and allocation-heavy live-count paths.
## Notes
- Removed the unwanted item-effect speech bubbles listed in `docs/prompt.txt`.
- Reorganized the tool palette into color-coded collapsible groups: operation, food/water/grass, medicine/effects, habitat/living things, hazards/obstacles.
- Changed the tool UI ball icon to a soccer-ball emoji preview.
- Changed time phases: morning 05:00, day 10:00, evening 17:00, night 20:00.
- Increased genkotsu knockback for Tarinai and ants; genkotsu now also blasts zunchi away.
- Genkotsu damage now adds fear/stress and decreases Tarinai openness.
- Rain weather overlay is now diagonal streaks. Fallen rain-water items, zunda juice, and mercury remain horizontal oval item sprites.
- Tightened mobile operation UI visibility to smartphone-class compact touch devices.
- Sleep-disease Tarinai can be moved by ant hauling without snapping back to their sleep anchor.
## 15.20.12 and earlier summary
- Consolidated documentation into dense English files; left `docs/prompt.txt` untouched.
- Restored rain, revised water/zunda/mercury item sprites, added zunchi-slave head zunchi, fight win rate, fight defeat chance, and post-wake lock.
- Earlier patches added parameter items, persistent item effects, WebP assets, sound categories, push notifications, ant systems, family tree UI, performance manager, event bus, manifest generation, CSS split, and large-file JS split.
## Debug patch: panic chain and action reasons
- Removed the friend-only panic chain that made only a best friend follow a panic target.
- Added general nearby panic contagion: any nearby live tarinai in panic can cause another tarinai to panic. Neuroticism, distance, and current stress affect the probability.
- Added / completed visible action reasons for:
- eating all food and effect items
- eating grass, ant corpses, and zunchi
- drinking / touching water
- resting near stone, bed, and nest box
- sunbathing
- zunchi-slave avoidance
- being pinched / dragged
- firecracker blast panic
- explosion-disease blast panic
- falling object / stone / genkotsu impact
- high-speed ball collision
- high-speed tarinai collision
- intimidation, intimidation escape, fights, birth ritual, and thaw surprise
- fence-blocked path give-up
- Added state labels for `sunbath`, `ant_attack`, `ant_intimidate`, and `frozen`.
- Expanded target labels for all current tool/item target types, including medicine/effect items, ant corpses, ant nests, firecrackers, pushpins, signboards, and fences.
- Verification performed:
- `node --check` over every `js/*.js`
- state-label coverage scan: no uncovered tarinai states
- old friend-panic chain scan: zero matches
## おしり鋲・冷凍操作変更パッチ
- ずんちどれい化条件の喧嘩勝率を30%以下に変更。
- 衰弱死因判定を厳格化。
- 冷凍手段をクリック式から「つまむで冷凍UIへ運ぶ」方式へ変更。
- 新アイテム「おしり鋲」を追加。刺さるとずんちを溜め、日光浴不可。抜くと溜まったずんちが高速で飛散。
- 高速ずんち衝突時の吹き飛び・パニック理由文を追加。
## 15.22.0 統合リファクタ
- `snapshot_system.js` を追加し、セーブ/ロードと冷凍で重複していた `clonePlain` / `copyOwnData` / `applyOwnData` / 参照復元 / スナップショット作成・復元を共通化。
- `tarinai_action_state.js` を追加し、`setActionState()` / `enterPanic()` / `beginEating()` により、状態変更・パニック化・食事開始の理由文設定を統合。
- 直接 `state = "panic"` していた処理を `enterPanic()` 経由へ変更。爆竹、爆発病、高速ボール、高速衝突、アリ、画鋲、近傍パニック連鎖、関係者死亡、解凍時の訃報などが対象。
- 直接 `state = "eat"` していた食事処理を `beginEating()` 経由へ変更。
- `physics_helpers.js` を追加し、壁反射・摩擦・回転を `TarinaiPhysics.applyKinematicItemMotion()` に共通化。おしり鋲/画鋲/高速ずんちの移動処理に適用。
- `ITEM_TRAITS``PIN_BEHAVIORS` を追加し、アイテム分類と画鋲/おしり鋲の差分挙動をテーブル化。
- `text_catalog.js` に状態ラベル、対象ラベル、理由文補完を集約。`ui_selected.js` はカタログ参照へ変更。
- `family_graph.js` を追加し、家系グラフの連結成分探索を共通化。同期家系データと描画側に適用。
- `relation_event_system.js` を追加し、関係者死亡の即時反応と冷凍個体への遅延通知を共通化。
- `ui_helpers.js` を追加し、HTMLエスケープ等のUI共通処理を切り出し。
- 構文チェック: 全 `js/*.js` OK。
- 重複確認: `clonePlain` / `copyOwnData` / `applyOwnData``snapshot_system.js` に集約、直接 `state = "panic"` と直接 `state = "eat"` は除去済み。
## 15.23.0 safe duplicate integration pass
- `registry_base.js` を追加し、`food_registry.js` / `effect_registry.js` / `disease_registry.js``get` / `has` / `label` / `color` / `debugSnapshot` 系の共通基盤を統合。
- `patch_helpers.js` を追加し、後付けの `World.prototype` パッチを `TarinaiPatcher.patchWorld()` 経由に統合。冷凍・関係者死亡通知で利用。
- `data.js``sleepFurniture` / `draggable` trait と、`isPinType()` / `isSleepFurnitureType()` / `lodgedPinFor()` / `hasLodgedPinEffect()` / `normalizedToolSizeFor()` / `toolSizeScaleFor()` を追加。
- `ui_tools.js``world_view.js` のツールサイズ算出を `normalizedToolSizeFor()` / `toolSizeScaleFor()` に統合。
- `tarinai_action_state.js``currentLodgedPin()` / `currentLodgedPinBehavior()` / `hasLodgedPinEffect()` / `goIdle()` / `startSleeping()` / `seekTarget()` を追加。
- `blockedByOshibyo` 系の重複判定を `hasLodgedPinEffect("blocksZunchi")` に統合。
- 画鋲/おしり鋲の型判定を `isPinType()` に寄せ、描画・配置・つまみ・削除判定の分岐を整理。
- 睡眠家具判定を `isSleepFurnitureType()` に寄せ、ベッド/巣箱の直書き判定を削減。
- 手動アリ近傍探索の一部を `world.nearbyAnts()` に寄せた。
- `pruneExtinctFamilies()``TarinaiFamilyGraph.connectedComponents()` に寄せた。`family_graph.js` 側のデフォルト edge も親子参照を使うようにした。
- `save_system.js` / `freeze_system.js` / `ui_selected.js` のHTMLエスケープを `TarinaiUIHelpers.htmlEscape()` に統合。
- `assets.js` のスプライト/汚れオーバーレイ用キャンバス生成を `TarinaiPhysics.createCachedCanvas()` に寄せた。
- 危険度の高い `render.js` 全体再設計、`Tarinai.update()` の状態機械化、`ants.js` の状態機械化、`items.js` 描画全体のレジストリ化は未実施。
- 確認: 全 `js/*.js``node --check` 通過。
### 15.23.0 追確認
- `service-worker.js` / `app_manifest.json` のスクリプト一覧を `index.html` の実ロード順に同期。新規共通化ファイルもキャッシュ対象に追加。
- アリ運搬中に寝ているたりないの状態変更も `startSleeping()` 経由へ寄せた。
- 最終確認: 全 `js/*.js``service-worker.js``node --check` 通過。
- 最終確認: `blockedByOshibyo``type === "oshibyo"``type === "pushpin"` の直接判定は 0件。`clonePlain` / `copyOwnData` / `applyOwnData` / `isPlainObject``snapshot_system.js` のみ。
## 15.24.0 safe integration / UI escape recursion fix
- Fixed `Maximum call stack size exceeded` caused by `ui_helpers.htmlEscape()` calling the global `escapeHtml()` while `ui_selected.escapeHtml()` called back into `TarinaiUIHelpers.htmlEscape()`.
- Made `TarinaiUIHelpers.htmlEscape()` a pure escaping function and loaded `ui_helpers.js` before `ui_selected.js`.
- Added a guard in `ui_selected.escapeHtml()` so it never delegates to itself.
- Added a data URL favicon link to stop the browser's default `/favicon.ico` 404 request.
- Bumped static asset version to `15.24.0` in `app_manifest.json`, `index.html`, `service-worker.js`, and `js/version.js` to avoid stale cached scripts.
- Reduced target-label duplication: item labels now come from `TOOL_DEFINITIONS` / `toolLabel()`, with `text_catalog.js` holding only non-tool overrides.
- Removed the remaining synchronous family-tree DFS fallback blocks in `ui_family_data.js` and `ui_family_render.js`; both now use `TarinaiFamilyGraph.connectedComponents()`.
- Simplified the remaining manual ant proximity fallback in `tarinai_needs_items.js` to `world.nearbyAnts()`.
- Verification: all `js/*.js` and `service-worker.js` pass `node --check`.
- Old save compatibility is intentionally not provided.
- Some legacy fields such as loneliness, affection, mood, and fearTimer still exist as compatibility/input/display fields, but the main action selection now uses the six needs.

View file

@ -1,5 +1,5 @@
{
"version": "15.24.0",
"version": "15.24.2",
"css": [
"css/base.css",
"css/layout.css",

View file

@ -451,6 +451,13 @@
.selected-info .info-row[data-metric="loneliness"] { background: rgba(116, 150, 214, 0.10); }
.selected-info .info-row[data-metric="energy"] { background: rgba(104, 184, 96, 0.10); }
.selected-info .info-row[data-metric="stress"] { background: rgba(214, 104, 88, 0.10); }
.selected-info .need-row[data-chosen-need="true"] {
border-color: rgba(65, 112, 74, 0.52);
background: rgba(106, 154, 84, 0.16);
}
.selected-info .need-row[data-chosen-need="true"] strong {
color: #315f32;
}
.selected-info .info-row[data-metric="zunchi"] { background: rgba(112, 142, 70, 0.10); }
.selected-info .info-row[data-metric="mood"] { background: rgba(222, 196, 86, 0.10); }
.selected-info .info-row[data-metric="lack"] { background: rgba(142, 112, 184, 0.10); }

File diff suppressed because it is too large Load diff

View file

@ -5,11 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>&#x305F;&#x308A;&#x306A;&#x3044;&#x89B3;&#x5BDF;</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 64 64%27%3E%3Ctext y=%2752%27 font-size=%2752%27%3E%E3%81%9F%3C/text%3E%3C/svg%3E" />
<link rel="stylesheet" href="css/base.css?v=15.24.0" />
<link rel="stylesheet" href="css/layout.css?v=15.24.0" />
<link rel="stylesheet" href="css/panel.css?v=15.24.0" />
<link rel="stylesheet" href="css/components.css?v=15.24.0" />
<link rel="stylesheet" href="css/mobile.css?v=15.24.0" />
<link rel="stylesheet" href="css/base.css?v=15.24.2" />
<link rel="stylesheet" href="css/layout.css?v=15.24.2" />
<link rel="stylesheet" href="css/panel.css?v=15.24.2" />
<link rel="stylesheet" href="css/components.css?v=15.24.2" />
<link rel="stylesheet" href="css/mobile.css?v=15.24.2" />
</head>
<body>
<div id="loadingScreen" class="loading-screen" aria-live="polite">
@ -183,66 +183,67 @@
</div>
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
<script src="js/version.js?v=15.24.0" defer></script>
<script src="js/event_bus.js?v=15.24.0" defer></script>
<script src="js/performance_manager.js?v=15.24.0" defer></script>
<script src="js/registry_base.js?v=15.24.0" defer></script>
<script src="js/disease_registry.js?v=15.24.0" defer></script>
<script src="js/effect_registry.js?v=15.24.0" defer></script>
<script src="js/sound_pack.js?v=15.24.0" defer></script>
<script src="js/data.js?v=15.24.0" defer></script>
<script src="js/item_visual_registry.js?v=15.24.0" defer></script>
<script src="js/food_registry.js?v=15.24.0" defer></script>
<script src="js/input_mode_manager.js?v=15.24.0" defer></script>
<script src="js/math.js?v=15.24.0" defer></script>
<script src="js/physics_helpers.js?v=15.24.0" defer></script>
<script src="js/assets.js?v=15.24.0" defer></script>
<script src="js/audio.js?v=15.24.0" defer></script>
<script src="js/render.js?v=15.24.0" defer></script>
<script src="js/sim_core.js?v=15.24.0" defer></script>
<script src="js/items.js?v=15.24.0" defer></script>
<script src="js/ants.js?v=15.24.0" defer></script>
<script src="js/health.js?v=15.24.0" defer></script>
<script src="js/tarinai.js?v=15.24.0" defer></script>
<script src="js/tarinai_identity_social.js?v=15.24.0" defer></script>
<script src="js/tarinai_action_state.js?v=15.24.0" defer></script>
<script src="js/tarinai_disease_nest.js?v=15.24.0" defer></script>
<script src="js/tarinai_item_effects.js?v=15.24.0" defer></script>
<script src="js/tarinai_needs_items.js?v=15.24.0" defer></script>
<script src="js/tarinai_social_move_life.js?v=15.24.0" defer></script>
<script src="js/tarinai_render.js?v=15.24.0" defer></script>
<script src="js/world.js?v=15.24.0" defer></script>
<script src="js/world_view.js?v=15.24.0" defer></script>
<script src="js/family_graph.js?v=15.24.0" defer></script>
<script src="js/world_family_social.js?v=15.24.0" defer></script>
<script src="js/world_combat_effects.js?v=15.24.0" defer></script>
<script src="js/world_environment.js?v=15.24.0" defer></script>
<script src="js/world_spatial_budget.js?v=15.24.0" defer></script>
<script src="js/world_ants_system.js?v=15.24.0" defer></script>
<script src="js/weather_system.js?v=15.24.0" defer></script>
<script src="js/world_update.js?v=15.24.0" defer></script>
<script src="js/world_placement_log.js?v=15.24.0" defer></script>
<script src="js/world_bootstrap.js?v=15.24.0" defer></script>
<script src="js/text_catalog.js?v=15.24.0" defer></script>
<script src="js/ui.js?v=15.24.0" defer></script>
<script src="js/ui_log.js?v=15.24.0" defer></script>
<script src="js/ui_helpers.js?v=15.24.0" defer></script>
<script src="js/ui_selected.js?v=15.24.0" defer></script>
<script src="js/snapshot_system.js?v=15.24.0" defer></script>
<script src="js/save_system.js?v=15.24.0" defer></script>
<script src="js/patch_helpers.js?v=15.24.0" defer></script>
<script src="js/freeze_system.js?v=15.24.0" defer></script>
<script src="js/relation_event_system.js?v=15.24.0" defer></script>
<script src="js/ui_layout_dialogs.js?v=15.24.0" defer></script>
<script src="js/ui_tools.js?v=15.24.0" defer></script>
<script src="js/ui_bind.js?v=15.24.0" defer></script>
<script src="js/ui_charts.js?v=15.24.0" defer></script>
<script src="js/ui_family_data.js?v=15.24.0" defer></script>
<script src="js/ui_family_async.js?v=15.24.0" defer></script>
<script src="js/ui_family_layout.js?v=15.24.0" defer></script>
<script src="js/ui_family_paths.js?v=15.24.0" defer></script>
<script src="js/ui_family_render.js?v=15.24.0" defer></script>
<script src="js/main.js?v=15.24.0" defer></script>
<script src="js/debug_tools.js?v=15.24.0" defer></script>
<script src="js/version.js?v=15.24.2" defer></script>
<script src="js/event_bus.js?v=15.24.2" defer></script>
<script src="js/performance_manager.js?v=15.24.2" defer></script>
<script src="js/registry_base.js?v=15.24.2" defer></script>
<script src="js/disease_registry.js?v=15.24.2" defer></script>
<script src="js/effect_registry.js?v=15.24.2" defer></script>
<script src="js/sound_pack.js?v=15.24.2" defer></script>
<script src="js/data.js?v=15.24.2" defer></script>
<script src="js/item_visual_registry.js?v=15.24.2" defer></script>
<script src="js/food_registry.js?v=15.24.2" defer></script>
<script src="js/input_mode_manager.js?v=15.24.2" defer></script>
<script src="js/math.js?v=15.24.2" defer></script>
<script src="js/physics_helpers.js?v=15.24.2" defer></script>
<script src="js/assets.js?v=15.24.2" defer></script>
<script src="js/audio.js?v=15.24.2" defer></script>
<script src="js/render.js?v=15.24.2" defer></script>
<script src="js/sim_core.js?v=15.24.2" defer></script>
<script src="js/structures.js?v=15.24.2" defer></script>
<script src="js/items.js?v=15.24.2" defer></script>
<script src="js/ants.js?v=15.24.2" defer></script>
<script src="js/health.js?v=15.24.2" defer></script>
<script src="js/tarinai.js?v=15.24.2" defer></script>
<script src="js/tarinai_identity_social.js?v=15.24.2" defer></script>
<script src="js/tarinai_action_state.js?v=15.24.2" defer></script>
<script src="js/tarinai_disease_nest.js?v=15.24.2" defer></script>
<script src="js/tarinai_item_effects.js?v=15.24.2" defer></script>
<script src="js/tarinai_needs_items.js?v=15.24.2" defer></script>
<script src="js/tarinai_social_move_life.js?v=15.24.2" defer></script>
<script src="js/tarinai_render.js?v=15.24.2" defer></script>
<script src="js/world.js?v=15.24.2" defer></script>
<script src="js/world_view.js?v=15.24.2" defer></script>
<script src="js/family_graph.js?v=15.24.2" defer></script>
<script src="js/world_family_social.js?v=15.24.2" defer></script>
<script src="js/world_combat_effects.js?v=15.24.2" defer></script>
<script src="js/world_environment.js?v=15.24.2" defer></script>
<script src="js/world_spatial_budget.js?v=15.24.2" defer></script>
<script src="js/world_ants_system.js?v=15.24.2" defer></script>
<script src="js/weather_system.js?v=15.24.2" defer></script>
<script src="js/world_update.js?v=15.24.2" defer></script>
<script src="js/world_placement_log.js?v=15.24.2" defer></script>
<script src="js/world_bootstrap.js?v=15.24.2" defer></script>
<script src="js/text_catalog.js?v=15.24.2" defer></script>
<script src="js/ui.js?v=15.24.2" defer></script>
<script src="js/ui_log.js?v=15.24.2" defer></script>
<script src="js/ui_helpers.js?v=15.24.2" defer></script>
<script src="js/ui_selected.js?v=15.24.2" defer></script>
<script src="js/snapshot_system.js?v=15.24.2" defer></script>
<script src="js/save_system.js?v=15.24.2" defer></script>
<script src="js/patch_helpers.js?v=15.24.2" defer></script>
<script src="js/freeze_system.js?v=15.24.2" defer></script>
<script src="js/relation_event_system.js?v=15.24.2" defer></script>
<script src="js/ui_layout_dialogs.js?v=15.24.2" defer></script>
<script src="js/ui_tools.js?v=15.24.2" defer></script>
<script src="js/ui_bind.js?v=15.24.2" defer></script>
<script src="js/ui_charts.js?v=15.24.2" defer></script>
<script src="js/ui_family_data.js?v=15.24.2" defer></script>
<script src="js/ui_family_async.js?v=15.24.2" defer></script>
<script src="js/ui_family_layout.js?v=15.24.2" defer></script>
<script src="js/ui_family_paths.js?v=15.24.2" defer></script>
<script src="js/ui_family_render.js?v=15.24.2" defer></script>
<script src="js/main.js?v=15.24.2" defer></script>
<script src="js/debug_tools.js?v=15.24.2" defer></script>
</body>
</html>

View file

@ -109,7 +109,7 @@
const scale = context.scale ?? 1;
tarinai.loveMochiTimer = Math.max(tarinai.loveMochiTimer || 0, 42 * scale + rand(4, 12));
const nutrition = Number(context.nutrition) || 0;
tarinai.stress = clamp(tarinai.stress - (context.source === "eating" ? nutrition * 0.25 : 8), 0, 130);
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { fulfill: -(context.source === "eating" ? nutrition * 0.5 : 12), social: -4 });
return true;
},
});
@ -118,7 +118,7 @@
const scale = context.scale ?? 1;
tarinai.fightMochiTimer = Math.max(tarinai.fightMochiTimer || 0, 52 * scale + rand(10, 18));
const nutrition = Number(context.nutrition) || 0;
tarinai.stress = clamp(tarinai.stress + (context.source === "eating" ? nutrition * 0.22 : 10), 0, 130);
if (typeof applyNeedShock === "function") applyNeedShock(tarinai, { safety: context.source === "eating" ? nutrition * 0.5 : 16 });
if (context.source === "eating") tarinai.fearTimer = Math.max(tarinai.fearTimer || 0, 0.32);
return true;
},
@ -127,7 +127,7 @@
onApply(tarinai, context = {}) {
if (context.source === "eating") {
const nutrition = Number(context.nutrition) || 0;
tarinai.stress = clamp(tarinai.stress - nutrition * 0.10, 0, 130);
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { safety: -nutrition * 0.25, sleep: -nutrition * 0.15 });
tarinai.energy = clamp(tarinai.energy - nutrition * 0.20, 0, 100);
tarinai.fearTimer = Math.max(0, (tarinai.fearTimer || 0) - 0.18);
tarinai.fightMochiTimer = Math.max(0, (tarinai.fightMochiTimer || 0) - 2.5);

View file

@ -183,7 +183,7 @@
const stress = Math.min(36, 10 + unique.length * 6 + relPriority * 8);
if (t.enterPanic) t.enterPanic({ reason: `${unique[0]?.name || "誰か"}がいないことに気づいた`, fear: 2.4 + Math.min(2.0, unique.length * 0.35), stress, cause: "pending_relation_death" });
else {
t.stress = clamp((t.stress || 0) + stress, 0, 130);
if (typeof applyNeedShock === "function") applyNeedShock(t, { safety: stress * 1.8, social: stress * 1.2 });
t.fearTimer = Math.max(t.fearTimer || 0, 2.4 + Math.min(2.0, unique.length * 0.35));
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination() : null, reason: `${unique[0]?.name || "誰か"}がいないことに気づいた`, wake: true });
}

View file

@ -134,6 +134,8 @@ const HEALTH = (() => {
if (actualLoss > 0.01) {
cause = rememberDamage(t, actualLoss, reason);
t.world?.emit?.("tarinai:damaged", { tarinai: t, amount: actualLoss, cause, reason });
const plushie = (t.world?.items || []).find(item => item && !item.dead && item.isStructure && item.type === "plushie" && item.ownerId === t.id && item.carriedById === t.id);
if (plushie && actualLoss >= 1) plushie.damage(actualLoss, t.world, null);
if (cause === CAUSES.fight || /\u55a7\u5629|fight/.test(String(reason || ""))) t.recordBleedExposure();
audio.damage?.(actualLoss);
if (t.sleepDisease) t.recoverSleepDisease("\u30c0\u30e1\u30fc\u30b8\u3067\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f");

View file

@ -184,6 +184,14 @@ class Item {
this.amount = itemAmountFor(type, 80);
this.age = 0;
this.seed = Math.random() * 1000;
this.roles = {};
this.needEffects = { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 };
if (isServingFoodType(type) || ["grass", "ant_corpse", "zunchi"].includes(type)) this.roles.food = true;
if (type === "water" || type === "water_bowl" || type === "zunda_juice") this.roles.drink = true;
if (["sweet", "water", "water_bowl", "zunda_juice"].includes(type) || isParamEffectItemType(type)) this.roles.medicine = true;
if (type === "bed" || type === "nest_box") this.roles.sleepPlace = true;
if (["firecracker", "genkotsu", "pushpin", "oshibyo", "zunchi", "splat"].includes(type)) this.roles.danger = true;
if (type === "grass") this.roles.grassMaterial = true;
this.dropTimer = 0;
this.dropMax = 0;
this.dropImpactDone = false;
@ -412,7 +420,7 @@ class Item {
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.5);
t.fearTimer = Math.max(t.fearTimer || 0, 0.55);
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(this) : null, reason: "高速ずんちにぶつかって吹き飛んでいる", wake: true });
t.addStress ? t.addStress(7, { threshold: 8, duration: 3.2 }) : (t.stress = clamp((t.stress || 0) + 7, 0, 130));
if (t.addStress) t.addStress(7, { threshold: 8, duration: 3.2 });
}
worldRef.spawnFallEffect?.(t.x, t.y + (t.radius || 20) * 0.45, 0.65);
worldRef.effects?.push(new Effect("zunchi_miasma", this.x, this.y - 4, { size: 14, life: 0.38, color: "rgba(77,92,42,0.36)" }));
@ -693,7 +701,7 @@ class Item {
t.hurtTimer = Math.max(t.hurtTimer || 0, 1.2);
t.fearTimer = Math.max(t.fearTimer || 0, 1.3);
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(this) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) }, reason: "画鋲が刺さってパニックになっている", wake: true });
t.addStress ? t.addStress(behavior?.stressOnAttach ?? 18, { threshold: 8 }) : (t.stress = clamp((t.stress || 0) + (behavior?.stressOnAttach ?? 18), 0, 130));
if (t.addStress) t.addStress(behavior?.stressOnAttach ?? 18, { threshold: 8 });
}
if (t.damage && (behavior?.damageOnAttach ?? 6) > 0) t.damage(behavior?.damageOnAttach ?? 6, "画鋲");
if ((worldRef.time || 0) >= (this.pinLogAt || -999) + 1.2) {
@ -730,7 +738,7 @@ class Item {
this.pinDamageTick -= 0.55;
if (t.damage && (behavior?.damagePerTick ?? 1.4) > 0) t.damage(behavior?.damagePerTick ?? 1.4, "画鋲");
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.50);
t.addStress ? t.addStress(behavior?.stressPerTick ?? 2.6, { threshold: 8, duration: 3.4 }) : (t.stress = clamp((t.stress || 0) + (behavior?.stressPerTick ?? 2.6), 0, 130));
if (t.addStress) t.addStress(behavior?.stressPerTick ?? 2.6, { threshold: 8, duration: 3.4 });
if (Math.random() < 0.22) t.bubble?.("!!", 0.6, "rgba(168,72,72,0.82)");
}
t.sleeping = false;

View file

@ -30,7 +30,7 @@
observer.sleeping = false;
observer.setActionState?.("panic", { target: observer.panicDestination ? observer.panicDestination(dead) : null, reason: `${relation}が死んだ`, wake: true });
observer.fearTimer = Math.max(observer.fearTimer || 0, fear);
observer.stress = clamp((observer.stress || 0) + stress, 0, 130);
if (typeof applyNeedShock === "function") applyNeedShock(observer, { safety: stress * 1.6, social: stress * 1.4 });
observer.bubble?.("!!", 1.0, "rgba(60,50,68,0.82)");
}
observer.recordChangeCause?.(`${relation}の死亡`, "ストレス", { value: stress });

View file

@ -743,6 +743,7 @@ function render() {
for (const it of world.items) {
if (isCachedTerrainItem(it)) continue;
if (isPinType(it.type) && it.pinState === "lodged") continue;
if (it.isStructure && it.type === "plushie" && it.carriedById) continue;
if (it.type !== "zunchi" && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
}
for (const it of world.items) {
@ -769,6 +770,9 @@ function render() {
for (const t of sorted) {
if (isEntityVisibleInRect(t, visibleRect)) t.draw(ctx, world.time, lighting);
}
for (const it of world.items) {
if (it.isStructure && it.type === "plushie" && it.carriedById && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
}
for (const it of world.items) {
if (isPinType(it.type) && it.pinState === "lodged" && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
}

214
js/structures.js Normal file
View file

@ -0,0 +1,214 @@
"use strict";
(function (global) {
function createStructureId(prefix = "structure") {
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;
}
function clonePlain(value) {
return JSON.parse(JSON.stringify(value || {}));
}
class TarinaiStructure {
constructor(definition, owner, x, y, world) {
this.id = createStructureId("structure");
this.type = definition.type;
this.label = definition.label;
this.isStructure = true;
this.ownerId = owner?.id || "";
this.hp = definition.maxHp || 1;
this.maxHp = definition.maxHp || this.hp;
this.attachment = definition.type === "plushie" ? 30 : 20;
this.usedCount = 0;
this.x = x;
this.y = y;
this.r = definition.type === "plushie" ? 13 : 24;
this.amount = this.hp;
this.seed = Math.random() * 1000;
this.roles = clonePlain(definition.roles);
this.needEffects = clonePlain(definition.needEffects);
this.carriedById = definition.type === "plushie" ? this.ownerId : "";
this.onHead = definition.type === "plushie";
this.dead = false;
this.world = world || null;
}
owner(worldRef = this.world) {
return (worldRef?.tarinai || []).find(t => t && !t.dead && t.id === this.ownerId) || null;
}
update(dt, worldRef = this.world) {
this.world = worldRef || this.world;
if (this.dead || this.hp <= 0) {
this.amount = 0;
return;
}
if (this.type === "plushie") {
const owner = this.owner(worldRef);
if (!owner) {
this.damage(1, worldRef, null);
return;
}
this.x = owner.x;
this.y = owner.y - Math.max(18, (owner.radius || 24) * 0.78);
if (owner.needs) owner.needs.fulfill = Math.max(0, (owner.needs.fulfill || 0) - dt * 2.8);
}
this.amount = Math.max(0.001, this.hp);
}
use(user, worldRef = this.world) {
const definition = global.StructureRegistry?.get?.(this.type);
return definition?.onUse?.(user, this, worldRef) || false;
}
damage(amount = 1, worldRef = this.world, breaker = null) {
if (this.dead) return false;
this.hp = Math.max(0, (this.hp || 0) - Math.max(0, amount || 0));
this.amount = this.hp;
if (this.hp > 0) return false;
this.dead = true;
this.amount = 0;
global.StructureRegistry?.get?.(this.type)?.onDestroyed?.(this, worldRef, breaker);
return true;
}
draw(ctx, time = 0, lighting = {}) {
if (this.dead) return;
ctx.save();
ctx.translate(this.x, this.y);
const night = (lighting?.light || 1) < 0.42;
if (this.type === "grass_bed") {
ctx.fillStyle = night ? "rgba(108,129,77,0.88)" : "rgba(134,174,83,0.92)";
ctx.strokeStyle = night ? "rgba(55,73,49,0.72)" : "rgba(72,105,50,0.72)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(0, 4, this.r * 1.55, this.r * 0.62, -0.08, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = night ? "rgba(185,203,142,0.35)" : "rgba(231,229,139,0.55)";
for (let i = 0; i < 12; i++) {
const a = -0.8 + i * 0.145;
ctx.beginPath();
ctx.moveTo(-this.r * 1.05 + i * this.r * 0.19, this.r * 0.38);
ctx.quadraticCurveTo(Math.sin(time + i) * 4, -this.r * 0.12, Math.cos(a) * this.r * 0.92, -this.r * 0.42 + Math.sin(i) * 4);
ctx.stroke();
}
} else if (this.type === "plushie") {
const img = typeof getRenderableImage === "function" ? getRenderableImage("smile", "smile") : global.images?.get?.("smile");
ctx.rotate(Math.sin(time * 2 + this.seed) * 0.05);
if (img) {
const w = this.r * 2.2;
const metrics = typeof getImageMetrics === "function" ? getImageMetrics("smile") : null;
const h = w * (metrics?.ratio || 0.92);
ctx.drawImage(img, -w * 0.5, -h * 0.58, w, h);
} else {
ctx.fillStyle = "#f7dfc0";
ctx.strokeStyle = "rgba(86,62,48,0.70)";
ctx.lineWidth = 1.7;
ctx.beginPath();
ctx.arc(0, 0, this.r, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "rgba(52,43,38,0.88)";
ctx.beginPath(); ctx.arc(-this.r * 0.28, -this.r * 0.08, 1.5, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(this.r * 0.28, -this.r * 0.08, 1.5, 0, Math.PI * 2); ctx.fill();
}
}
ctx.restore();
}
}
const StructureRegistry = {
defs: {},
register(definition) { this.defs[definition.type] = definition; return definition; },
get(type) { return this.defs[type] || null; },
create(type, owner, x, y, world) {
const definition = this.get(type);
if (!definition) return null;
return definition.create ? definition.create(owner, x, y, world) : new TarinaiStructure(definition, owner, x, y, world);
},
};
function applyStructureNeedShock(owner, safetyGain, fulfillGain, breaker = null) {
if (!owner || owner.dead) return;
if (typeof global.applyNeedShock === "function") global.applyNeedShock(owner, { safety: safetyGain || 0, fulfill: fulfillGain || 0 }, breaker);
else {
owner.needShock = owner.needShock || {};
owner.needShock.safety = Math.max(owner.needShock.safety || 0, safetyGain || 0);
owner.needShock.fulfill = Math.max(owner.needShock.fulfill || 0, fulfillGain || 0);
owner.lastNeedShockBreaker = breaker || null;
}
if (typeof global.triggerNeedShockReaction === "function") {
const reason = breaker ? "\u81ea\u5206\u306e\u3082\u306e\u304c\u58ca\u308c\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b" : "\u81ea\u5206\u306e\u3082\u306e\u304c\u58ca\u308c\u3066\u3044\u308b";
global.triggerNeedShockReaction(owner, breaker, reason);
}
}
function damageNearbyStructures(worldRef, x, y, radius, amount, breaker = null) {
let hit = 0;
for (const structure of worldRef?.nearbyItems?.(x, y, radius + 80) || worldRef?.items || []) {
if (!structure?.isStructure || structure.dead) continue;
const d = distXY(x, y, structure.x, structure.y);
const p = clamp(1 - d / Math.max(1, radius + (structure.r || 12)), 0, 1);
if (p <= 0) continue;
if (structure.damage?.(Math.max(1, amount * (0.35 + p * 0.65)), worldRef, breaker)) hit++;
}
return hit;
}
StructureRegistry.register({
type: "grass_bed",
label: "\u8349\u306e\u7c21\u6613\u30d9\u30c3\u30c9",
buildNeed: "fulfill",
buildTime: 4,
maxHp: 100,
materials: { grassMaterial: 1 },
roles: { sleepPlace: true, ownedStructure: true },
needEffects: { food: 0, sleep: 60, health: 5, safety: 25, social: 0, fulfill: 40 },
create(owner, x, y, world) { return new TarinaiStructure(this, owner, x, y, world); },
onUse(user, structure) {
const ownerUse = user?.id && user.id === structure.ownerId;
if (ownerUse) structure.attachment = Math.min(100, (structure.attachment || 0) + 2);
structure.usedCount = (structure.usedCount || 0) + 1;
if (user) {
user.needShock = user.needShock || {};
user.needShock.sleep = Math.min(0, (user.needShock.sleep || 0) - (ownerUse ? 18 : 8));
user.needShock.safety = Math.min(0, (user.needShock.safety || 0) - (ownerUse ? 12 : 4));
user.needShock.fulfill = Math.min(0, (user.needShock.fulfill || 0) - (ownerUse ? 18 : 6));
}
return true;
},
onDestroyed(structure, world, breaker) {
applyStructureNeedShock(structure.owner(world), 42, 46, breaker);
},
});
StructureRegistry.register({
type: "plushie",
label: "\u306c\u3044\u3050\u308b\u307f",
buildNeed: "fulfill",
buildTime: 4,
maxHp: 1,
materials: { grassMaterial: 1 },
roles: { carriedObject: true, ownedStructure: true, fulfillObject: true },
needEffects: { food: 0, sleep: 0, health: 0, safety: 5, social: 0, fulfill: 50 },
create(owner, x, y, world) { return new TarinaiStructure(this, owner, x, y, world); },
onUse(user, structure) {
if (user?.id === structure.ownerId) structure.attachment = Math.min(100, (structure.attachment || 0) + 1);
structure.usedCount = (structure.usedCount || 0) + 1;
if (user?.id === structure.ownerId) {
user.needShock = user.needShock || {};
user.needShock.fulfill = Math.min(0, (user.needShock.fulfill || 0) - 24);
user.needShock.safety = Math.min(0, (user.needShock.safety || 0) - 4);
}
return true;
},
onDestroyed(structure, world, breaker) {
applyStructureNeedShock(structure.owner(world), 55, 64, breaker);
},
});
global.TarinaiStructure = TarinaiStructure;
global.StructureRegistry = StructureRegistry;
global.damageNearbyStructures = damageNearbyStructures;
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -53,7 +53,12 @@ class Tarinai { constructor(world, opts = {}) {
this.hunger = opts.hunger ?? rand(16, 45);
this.loneliness = opts.loneliness ?? rand(18, 62);
this.energy = opts.energy ?? rand(54, 98);
this.stress = opts.stress ?? rand(0, 20);
this.needs = opts.needs && typeof opts.needs === "object" && typeof createDefaultNeeds === "function" ? { ...createDefaultNeeds(), ...opts.needs } : (typeof createDefaultNeeds === "function" ? createDefaultNeeds() : { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 });
this.stress = typeof calculateStressFromNeeds === "function" ? calculateStressFromNeeds(this.needs) : 0;
this.previousNeeds = opts.previousNeeds && typeof opts.previousNeeds === "object" && typeof createDefaultNeeds === "function" ? { ...createDefaultNeeds(), ...opts.previousNeeds } : { ...this.needs };
this.intent = opts.intent && typeof opts.intent === "object" ? { ...opts.intent } : null;
this.needShock = opts.needShock && typeof opts.needShock === "object" ? { ...opts.needShock } : {};
this.buildPlan = opts.buildPlan && typeof opts.buildPlan === "object" ? { ...opts.buildPlan } : null;
this.hpBarTimer = opts.hpBarTimer ?? 0;
this.stressBarTimer = opts.stressBarTimer ?? 0;
this.affection = opts.affection ?? rand(0, 30);

View file

@ -58,18 +58,30 @@
const profile = this.personalityProfile ? this.personalityProfile() : { fear: 1 };
const fearMul = opts.fearMultiplier ?? profile.fear ?? 1;
const threat = opts.threat ?? opts.target ?? null;
let target = null;
if ("target" in opts) target = opts.target;
else if (opts.destination) target = opts.destination;
else if (this.panicDestination) target = this.panicDestination(threat, !!opts.forceDestination);
this.setActionState("panic", {
target,
reason: opts.reason || this.thought || "パニックになっている",
wake: opts.wake !== false,
sleeping: false,
});
if (Number.isFinite(opts.fearTimer)) this.fearTimer = Math.max(this.fearTimer || 0, opts.fearTimer * fearMul);
else this.fearTimer = Math.max(this.fearTimer || 0, (opts.fear ?? 0.55) * fearMul);
const safetyShock = Math.max(40, (opts.fearTimer ?? opts.fear ?? 0.55) * 32 * fearMul, Number(opts.stress || 0) * 1.8);
const healthShock = Math.max(0, Number(opts.hurtTimer || 0) > 0 ? 45 : 0);
if (typeof applyNeedShock === "function") {
applyNeedShock(this, { safety: safetyShock, health: healthShock }, threat);
} else {
this.needShock = this.needShock || {};
this.needShock.safety = Math.max(this.needShock.safety || 0, safetyShock);
if (healthShock > 0) this.needShock.health = Math.max(this.needShock.health || 0, healthShock);
}
let handled = false;
if (typeof triggerNeedShockReaction === "function") handled = triggerNeedShockReaction(this, threat, opts.reason || "パニックになっている");
if (!handled) {
let target = null;
if ("target" in opts) target = opts.target;
else if (opts.destination) target = opts.destination;
else if (this.panicDestination) target = this.panicDestination(threat, !!opts.forceDestination);
this.setActionState("panic", {
target,
reason: opts.reason || this.thought || "パニックになっている",
wake: opts.wake !== false,
sleeping: false,
});
this.fearTimer = Math.max(this.fearTimer || 0, 2.4);
}
if (Number.isFinite(opts.surpriseTimer)) this.surpriseTimer = Math.max(this.surpriseTimer || 0, opts.surpriseTimer);
if (Number.isFinite(opts.hurtTimer)) this.hurtTimer = Math.max(this.hurtTimer || 0, opts.hurtTimer);
if (Number.isFinite(opts.awakeLockTimer)) this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, opts.awakeLockTimer);
@ -77,10 +89,6 @@
this.fallTimer = Math.max(this.fallTimer || 0, opts.fallTimer);
this.fallMax = Math.max(this.fallMax || 0, this.fallTimer);
}
if (Number.isFinite(opts.stress) && opts.stress > 0) {
if (this.addStress) this.addStress(opts.stress, { threshold: opts.stressThreshold ?? 8, duration: opts.stressDuration ?? 4.2 });
else this.stress = clamp((this.stress || 0) + opts.stress, 0, 130);
}
if (opts.bubble) this.bubble?.(opts.bubble, opts.bubbleCooldown ?? 0.9, opts.bubbleColor || "rgba(84,66,86,0.80)");
this.lastPanicCause = opts.cause || opts.reason || this.thought || "panic";
this.lastPanicAt = this.world?.time || 0;

View file

@ -26,7 +26,7 @@
this.zunchiDiseaseSeverity = 0;
this.zunchiDiseaseCooldown = 18;
this.zunchiStain = clamp((this.zunchiStain || 0) - 35, 0, 100);
this.stress = clamp(this.stress - 8, 0, 130);
if (typeof applyNeedRelief === "function") applyNeedRelief(this, { health: -18, safety: -6 });
this.thought = reason || "\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f";
audio.heal?.();
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(62,104,62,0.78)");
@ -61,7 +61,7 @@
}
if (this.zunchiDisease) {
this.zunchiDiseaseSeverity = clamp((this.zunchiDiseaseSeverity || 0.7) + dt * 0.004, 0, 1.25);
this.stress = clamp(this.stress + dt * (0.28 + 0.18 * this.zunchiDiseaseSeverity), 0, 130);
if (typeof applyNeedShock === "function") applyNeedShock(this, { health: dt * (5 + 4 * this.zunchiDiseaseSeverity) });
this.energy = clamp(this.energy - dt * 0.035 * this.zunchiDiseaseSeverity, 0, 100);
if (this.energy > 82) this.zunchiDiseaseSeverity = Math.max(0, this.zunchiDiseaseSeverity - dt * (this.energy > 94 ? 0.020 : 0.010));
this.emitZunchiDiseaseEffect(dt);
@ -136,7 +136,7 @@
this.explosionDiseaseTimer = 0;
this.thought = reason || "\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f";
audio.heal?.();
this.stress = clamp(this.stress - 10, 0, 130);
if (typeof applyNeedRelief === "function") applyNeedRelief(this, { health: -22, safety: -8 });
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)");
this.world?.log?.(`${this.name}\u306e\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] });
this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_explosion", reason });
@ -168,7 +168,7 @@
this.fightDiseaseCooldown = 20;
this.thought = reason || "\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f";
audio.heal?.();
this.stress = clamp(this.stress - 12, 0, 130);
if (typeof applyNeedRelief === "function") applyNeedRelief(this, { health: -24, safety: -10 });
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)");
this.world?.log?.(`${this.name}\u306e\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "fight", { participants: [this] });
this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_fight", reason });
@ -311,7 +311,7 @@
applyWaterEffect(dt, source = "water") {
const power = Math.max(0, dt || 0);
this.stress = clamp(this.stress - power * 2.4, 0, 130);
if (typeof applyNeedRelief === "function") applyNeedRelief(this, { health: -power * 12, safety: -power * 6 });
this.energy = clamp(this.energy + power * 0.5, 0, 100);
this.hunger = clamp(this.hunger - power * 0.2, 0, 115);
this.zunchiStain = clamp((this.zunchiStain || 0) - power * 10.5, 0, 100);
@ -329,7 +329,7 @@
this.fightDiseaseCooldown = Math.max(0, (this.fightDiseaseCooldown || 0) - dt);
if (this.explosionDisease) {
this.explosionDiseaseTimer = Math.max(0, (this.explosionDiseaseTimer || 0) - dt);
this.addStress ? this.addStress(dt * 0.22, { threshold: 9, duration: 3.6 }) : (this.stress = clamp(this.stress + dt * 0.22, 0, 130));
if (this.addStress) this.addStress(dt * 0.22, { threshold: 9, duration: 3.6 });
if (Math.random() < dt * 2.1) {
const a = rand(0, Math.PI * 2);
this.world?.effects?.push(new Effect("explosion", this.x + Math.cos(a) * this.radius * rand(0.2, 0.9), this.y + Math.sin(a) * this.radius * rand(0.1, 0.8), {
@ -353,7 +353,7 @@
this.recoverFightDisease("\u81ea\u7136\u306b\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f");
}
if (this.fightDisease) {
this.stress = clamp(this.stress + dt * 0.16, 0, 130);
if (typeof applyNeedShock === "function") applyNeedShock(this, { health: dt * 3, safety: dt * 2 });
if (Math.random() < dt * 0.30) this.wanderAngle += rand(-2.8, 2.8);
if (Math.random() < dt * 0.08) this.bubble("?", 2.6, "rgba(150,78,44,0.80)");
}

View file

@ -147,7 +147,7 @@
const changedA = this.adjustPersonality?.("neuroticism", 0.025, "after being hurt") || 0;
const changedB = this.adjustPersonality?.("aggression", -0.015, "after being hurt") || 0;
this.fearTimer = Math.max(this.fearTimer || 0, 1.2);
this.stress = clamp((this.stress || 0) + 5, 0, 130);
if (typeof applyNeedShock === "function") applyNeedShock(this, { safety: 9 });
return Boolean(changedA || changedB);
},
@ -256,9 +256,15 @@
const tolerance = Number.isFinite(opts.tolerance) ? opts.tolerance : 0.88;
const delta = Math.max(0, (amount || 0) * tolerance);
if (delta <= 0) return 0;
if (!this.needShock) this.needShock = {};
const shock = Math.min(100, delta * 1.8);
this.needShock.safety = Math.max(this.needShock.safety || 0, shock);
const before = this.stress || 0;
this.stress = clamp(before + delta, 0, 130);
const gained = Math.max(0, this.stress - before);
if (this.needs && typeof calculateStressFromNeeds === "function") {
const nextSafety = typeof quantizeNeed === "function" ? quantizeNeed((this.needs.safety || 0) + shock) : Math.min(100, (this.needs.safety || 0) + shock);
this.stress = calculateStressFromNeeds({ ...this.needs, safety: nextSafety });
}
const gained = Math.max(delta, Math.max(0, (this.stress || 0) - before));
const threshold = opts.threshold ?? 8;
if (gained >= threshold) this.showStressBar(opts.duration ?? 4.2);
return gained;

File diff suppressed because it is too large Load diff

View file

@ -35,12 +35,12 @@
nonSlave.vx += nx * dt * 34;
nonSlave.vy += ny * dt * 34;
if (nonSlave.state === "idle" || nonSlave.state === "seek_friend") {
if (nonSlave.enterPanic) nonSlave.enterPanic({ threat: slave, reason: "ずんちどれいを怖がっている", fear: 0.38, wake: false, cause: "zunchi_slave_nearby" });
else { nonSlave.fearTimer = Math.max(nonSlave.fearTimer || 0, 0.38 * nonSlave.personalityProfile().fear); nonSlave.target = slave; nonSlave.thought = "ずんちどれいを怖がっている"; }
if (nonSlave.enterPanic) nonSlave.enterPanic({ threat: slave, reason: "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u8fd1\u3044", fear: 0.38, wake: false, cause: "zunchi_slave_nearby" });
else { nonSlave.fearTimer = Math.max(nonSlave.fearTimer || 0, 0.38 * nonSlave.personalityProfile().fear); nonSlave.target = slave; nonSlave.thought = "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u8fd1\u3044"; }
} else {
nonSlave.fearTimer = Math.max(nonSlave.fearTimer || 0, 0.38 * nonSlave.personalityProfile().fear);
nonSlave.target = slave;
nonSlave.thought = "ずんちどれいを怖がっている";
nonSlave.thought = "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u8fd1\u3044";
}
nonSlave.adjustRelation?.(slave, -dt * 0.08, dt * 0.24, "zunchi_slave_avoid");
slave.adjustRelation?.(nonSlave, -dt * 0.02, 0, "zunchi_slave_avoid");
@ -66,15 +66,15 @@
this.affection += dt * 0.28 * this.trait.social;
this.adjustRelation(o, dt * 0.18 * pdef.relation * (rel.fear > 10 ? 0.35 : 1), -dt * 0.018, "nearby");
if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD) {
this.stress = clamp(this.stress - dt * 0.62 * this.trait.social, 0, 130);
if (typeof applyNeedRelief === "function") applyNeedRelief(this, { social: -dt * 5 * this.trait.social, fulfill: -dt * 2 });
this.affection = clamp(this.affection + dt * 0.18, 0, 80);
if (this.world.relationNotice(this.id, o.id, "friend-calm", 55)) this.world.log(`${this.name}\u306f${o.name}\u3068\u3044\u308b\u3068\u5c11\u3057\u843d\u3061\u7740\u304f\u3002`, "relation", { participants: [o, this] });
}
if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD && this.world.relationNotice(this.id, o.id, "friend", 32)) {
this.world.log(`${this.name}\u306f\u3088\u304f${o.name}\u306e\u8fd1\u304f\u306b\u3044\u308b\u3002`, "relation", { participants: [o, this] });
}
if (this.type === "angry") this.stress += dt * 0.08;
if (this.type === "teary" && o.type === "teary") this.stress -= dt * 0.12;
if (this.type === "angry" && typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * 1.5, safety: dt * 1 });
if (this.type === "teary" && o.type === "teary" && typeof applyNeedRelief === "function") applyNeedRelief(this, { social: -dt * 2 });
}
const fightingThis = this.fightTimer > 0 && (this.fightTargetId === o.id || (this.fightTargetIds || []).includes(o.id));
@ -86,8 +86,8 @@
this.vy -= ny * rand(24, 44);
o.vx += nx * rand(34, 64);
o.vy += ny * rand(34, 64);
this.stress = clamp(this.stress + 1.6, 0, 130);
o.stress = clamp(o.stress + 2.8, 0, 130);
if (typeof applyNeedShock === "function") applyNeedShock(this, { safety: 8, health: 4 });
if (typeof applyNeedShock === "function") applyNeedShock(o, { safety: 12, health: 6 });
const damageToThis = o.outgoingDamage ? o.outgoingDamage(1.2) : 1.2;
const damageToOther = this.outgoingDamage ? this.outgoingDamage(1.6) : 1.6;
this.world.emit?.("fight:hit", { attacker: o, target: this, amount: damageToThis, cause: "\u55a7\u5629" });
@ -106,7 +106,7 @@
o.fallDir = nx > 0 ? 1 : -1;
this.world.spawnFallEffect(o.x, o.y + o.radius * 0.45, 0.55);
}
if (o.energy + o.mood * 0.25 < this.energy + this.mood * 0.25 - 8) {
if (o.energy < this.energy - 8) {
this.world.maybeDefeatFromFightDamage?.(o, this, Math.max(damageToOther, 2.4));
}
this.world.spawnHeadbuttEffect((this.x + o.x) / 2, (this.y + o.y) / 2 - 5);
@ -122,7 +122,7 @@
const aggressiveOther = o.shouldApplyPersonalityBehavior?.("aggression", 1);
const calmThis = this.shouldApplyPersonalityBehavior("aggression", -1);
const calmOther = o.shouldApplyPersonalityBehavior?.("aggression", -1);
const fightRisk = (this.mood < 55 || o.mood < 55 || this.stress > 46 || o.stress > 46 || this.type === "angry" || o.type === "angry" || closeCount > 3 || aggressiveThis || aggressiveOther || battleDrug || o.isZunchiSlave || this.isZunchiSlave);
const fightRisk = (this.stress > 46 || o.stress > 46 || (this.needs?.safety || 0) > 60 || (o.needs?.safety || 0) > 60 || this.type === "angry" || o.type === "angry" || closeCount > 3 || aggressiveThis || aggressiveOther || battleDrug || o.isZunchiSlave || this.isZunchiSlave);
const fearBrake = battleDrug ? clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 220, 0.74, 1.15) : clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 120, 0.34, 1);
const friendBrake = battleDrug ? clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 180, 0.72, 1.12) : clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 90, 0.48, 1);
const personalityRisk = this.personalityProfile().fight * o.personalityProfile().fight * (calmThis ? 0.62 : 1) * (calmOther ? 0.62 : 1);
@ -145,7 +145,7 @@
}
if (closeCount > 8) {
this.stress += dt * 0.9 * this.personalityProfile().fear;
if (typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * 5, safety: dt * 3 * this.personalityProfile().fear });
this.energy -= dt * 0.25;
this.fearTimer = Math.max(this.fearTimer, 0.18);
}
@ -174,7 +174,7 @@
const bedCrowd = this.isSleepFurniture(this.target) ? this.world.bedOccupancy(this.target) : 0;
const crowdedSleep = bedCrowd > 5;
this.energy = clamp(this.energy + dt * (this.target ? 1.8 + bedComfort * 1.35 : 2.1), 0, 100);
this.stress = clamp(this.stress + dt * (crowdedSleep ? 0.18 * Math.max(1, bedCrowd - 5) : -0.12 * bedComfort), 0, 130);
if (crowdedSleep && typeof applyNeedShock === "function") applyNeedShock(this, { safety: dt * Math.max(1, bedCrowd - 5) }); else if (typeof applyNeedRelief === "function") applyNeedRelief(this, { sleep: -dt * 3 * bedComfort, safety: -dt * 1.5 * bedComfort });
this.hunger = clamp(this.hunger + dt * 0.018, 0, 115);
if (this.target && !this.target.dead && this.target.type === "nest_box") {
this.updateNestBoxPresence(dt);
@ -259,7 +259,8 @@
? (this.insideNestBoxId === this.target.id || d <= Math.max(this.radius + 34, this.target.r * 1.08) || bedDist <= Math.max(this.target.r * 1.05, 62))
: (d <= Math.max(this.radius + 8, 26) || bedDist <= Math.max(this.target.r * 0.58, 20));
if (reachedSleepFurniture) {
this.startSleeping?.(this.target, this.target.type === "nest_box" ? "巣箱の中で休んでいる" : "眠る場所に着いたので休んでいる");
this.target.use?.(this, this.world);
this.startSleeping?.(this.target, this.target.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u4f11\u3093\u3067\u3044\u308b" : "\u5bdd\u308b\u5834\u6240\u306b\u7740\u3044\u305f\u306e\u3067\u4f11\u3093\u3067\u3044\u308b");
if (this.target.type === "nest_box") this.enterNestBox(this.target, dt);
return;
}
@ -366,14 +367,6 @@
else if (this.stress >= 124) {
reason = this.dominantDeathCause("\u30b9\u30c8\u30ec\u30b9\u904e\u591a", { stressDeath: true }) || "\u30b9\u30c8\u30ec\u30b9\u904e\u591a";
}
else if (this.mood <= 20) {
const lowMood = clamp((20 - this.mood) / 20, 0, 1);
const bodyLoad = clamp((this.hunger - 72) / 42, 0, 1) * 0.018 + clamp((28 - this.energy) / 28, 0, 1) * 0.020 + clamp((this.stress - 86) / 44, 0, 1) * 0.022;
const deathRisk = 0.010 + lowMood * 0.060 + bodyLoad;
if (Math.random() < Math.max(0, dt) * deathRisk) {
reason = this.dominantDeathCause("", { lowMood: true }) || "\u30b9\u30c8\u30ec\u30b9\u904e\u591a";
}
}
if (reason) this.die(reason);
},

View file

@ -192,8 +192,10 @@ function renderSelected() {
personalityTags.join(",")
].join(",");
const itemEffectSummary = t.activeItemEffectSummary ? t.activeItemEffectSummary() : [];
const needKeys = typeof TARINAI_NEED_KEYS !== "undefined" ? TARINAI_NEED_KEYS : ["food", "sleep", "health", "safety", "social", "fulfill"];
const snapshot = [
t.id, t.type, t.name, generationDisplay, t.state, t.thought, targetLabel(t.target),
t.intent?.need || "", t.intent?.actionId || "", t.intent?.actionLabel || "", t.intent?.reasonText || "", needKeys.map(key => `${key}:${t.needs?.[key] ?? 0}`).join(","),
fmt(t.age), fmt(t.lifeSpan), fmt(t.hunger), fmt(t.loneliness),
fmt(t.energy), fmt(t.stress), fmt(t.mood),
t.parentNames?.join("+") || "", t.children?.length || 0, fmt(t.lack),
@ -239,6 +241,7 @@ function renderSelected() {
const basicHtml = `
<div class="info-row"><span>\u4e16\u4ee3</span><strong>${escapeHtml(generationDisplay || "\u4e0d\u660e")}</strong></div>
<div class="info-row"><span>\u72b6\u614b</span><strong>${escapeHtml(stateLabel(t.state))}</strong></div>
<div class="info-row"><span>\u3044\u307e</span><strong>${escapeHtml(t.intent?.actionLabel || stateLabel(t.state))}</strong></div>
<div class="info-row"><span>\u7406\u7531</span><strong>${escapeHtml(reasonLabel(t))}</strong></div>
<div class="info-row"><span>\u5bfe\u8c61</span><strong>${escapeHtml(targetLabel(t.target))}</strong></div>
<div class="info-row"><span>\u5e74\u9f62</span><strong>${fmt(t.age)} / ${fmt(t.lifeSpan)}</strong></div>
@ -247,13 +250,16 @@ function renderSelected() {
<div class="info-row"><span>\u79fb\u52d5\u901f\u5ea6</span><strong>${geneticPercentText(t, "speed")}</strong></div>
<div class="info-row"><span>\u30b5\u30a4\u30ba</span><strong>${geneticSizeText(t)}</strong></div>
`;
const needRowsHtml = needKeys.map(key => {
const label = (typeof TARINAI_NEED_LABELS !== "undefined" && TARINAI_NEED_LABELS[key]) || key;
const value = typeof getNeedDisplayValue === "function" ? getNeedDisplayValue(t.needs?.[key] || 0) : Math.round((t.needs?.[key] || 0) / 10);
const chosen = t.intent?.need === key ? ` data-chosen-need="true"` : "";
return `<div class="info-row need-row"${chosen}><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
}).join("");
const healthHtml = [
selectedInfoRowHtml("\u7dcf\u5408\u7684\u6c17\u5206", fmt(t.mood), "mood"),
selectedInfoRowHtml("\u7a7a\u8179", fmt(t.hunger), "hunger"),
selectedInfoRowHtml("\u5bc2\u3057\u3055", fmt(t.loneliness), "loneliness"),
needRowsHtml,
selectedInfoRowHtml("\u4f53\u529b", fmt(t.energy), "energy"),
selectedInfoRowHtml("\u30b9\u30c8\u30ec\u30b9", fmt(t.stress), "stress"),
selectedInfoRowHtml("\u305a\u3093\u3061\u6c5a\u308c", fmt(t.zunchiStain || 0), "zunchi"),
].join("");
const itemEffectHtml = itemEffectSummary.length
? itemEffectSummary.map(e => `<div class="info-row"><span>${escapeHtml(e.label)}</span><strong>${escapeHtml(e.detail)}</strong></div>`).join("")
@ -297,6 +303,7 @@ function stateLabel(s) {
}
function reasonLabel(t) {
if (t?.intent?.reasonText) return t.intent.reasonText;
return window.TEXT_CATALOG?.reasonLabel?.(t) || "なし";
}

View file

@ -1,8 +1,8 @@
"use strict";
(function () {
const APP_VERSION = "15.24.0";
const APP_BUILD = "safe-refactor-ui-fix";
const APP_VERSION = "15.24.2";
const APP_BUILD = "needs-compliance-fix";
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;

View file

@ -68,7 +68,7 @@
if (t.enterPanic) t.enterPanic({ target: { x, y, dead: false }, reason: "\u7206\u7af9\u3067\u305f\u305f\u304d\u8d77\u3053\u3055\u308c\u305f", fear: 1.2, surpriseTimer: 0.95, wake: true, cause: "firecracker_wakeup" });
else { t.setActionState?.("panic", { target: { x, y, dead: false }, reason: "\u7206\u7af9\u3067\u305f\u305f\u304d\u8d77\u3053\u3055\u308c\u305f", wake: true }); t.surpriseTimer = Math.max(t.surpriseTimer, 0.95); t.fearTimer = Math.max(t.fearTimer, 1.2); }
}
t.addStress ? t.addStress(rand(8, 15), { threshold: 8 }) : (t.stress = clamp(t.stress + rand(8, 15), 0, 130));
if (t.addStress) t.addStress(rand(8, 15), { threshold: 8 });
t.fearTimer = Math.max(t.fearTimer, 0.58);
const dx = t.x - x;
const dy = t.y - y;
@ -76,7 +76,7 @@
if (d > blastRadius) continue;
const p = clamp(1 - d / blastRadius, 0, 1);
t.damage(5 + p * 18, "\u7206\u7af9");
t.addStress ? t.addStress(22 * p, { threshold: 8 }) : (t.stress = clamp(t.stress + 22 * p, 0, 130));
if (t.addStress) t.addStress(22 * p, { threshold: 8 });
t.hurtTimer = Math.max(t.hurtTimer, 1.5 + p * 1.8);
t.surpriseTimer = Math.max(t.surpriseTimer, 0.85);
if (t.enterPanic) t.enterPanic({ target: { x, y }, reason: "爆竹の爆風でパニックになっている", fear: 1.7 + p, wake: true, cause: "firecracker_blast" });
@ -106,6 +106,7 @@
if (!t.dead && p > 0.22 && Math.random() < (t.diseaseChance ? t.diseaseChance(explosionChance) : explosionChance)) t.infectExplosionDisease?.(it);
if (!t.dead && Math.random() < 0.45) 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.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u7af9", {
push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * (it.blastScale || 1),
});
@ -166,7 +167,7 @@
if (d > blastRadius) continue;
const q = clamp(1 - d / blastRadius, 0, 1);
o.damage(5 + q * 18, "\u7206\u767a\u75c5");
o.addStress ? o.addStress(22 * q, { threshold: 8 }) : (o.stress = clamp(o.stress + 22 * q, 0, 130));
if (o.addStress) o.addStress(22 * q, { threshold: 8 });
let nx = dx / d;
let ny = dy / d;
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
@ -365,7 +366,7 @@
},
isSleepFurniture(it) {
return Boolean(it && (typeof isSleepFurnitureType === "function" ? isSleepFurnitureType(it.type) : (it.type === "bed" || it.type === "nest_box")));
return Boolean(it && (it.roles?.sleepPlace || (typeof isSleepFurnitureType === "function" ? isSleepFurnitureType(it.type) : (it.type === "bed" || it.type === "nest_box"))));
},
isTarinaiHiddenInNestBox(t) {
@ -412,8 +413,10 @@
}
return best;
};
// Prefer an available nest box, then fall back to a hay bed.
return choose("nest_box") || choose("bed");
const owned = (this.nearbyItems(t.x, t.y, maxDist) || [])
.filter(bed => bed && !bed.dead && bed.ownerId === t.id && bed.roles?.sleepPlace)
.sort((a, b) => dist(t, a) - dist(t, b))[0] || null;
return owned || choose("nest_box") || choose("bed") || choose("grass_bed");
},
relationNotice(aId, bId, kind, cooldown = 30) {
@ -525,7 +528,7 @@
loser.fearTimer = Math.max(loser.fearTimer, 1.2 * loser.personalityProfile().fear);
this.spawnBubble(loser.x, loser.y - loser.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
winner.affection = clamp(winner.affection + 0.8, 0, 80);
winner.stress = clamp(winner.stress - rand(9, 18), 0, 130);
if (typeof applyNeedRelief === "function") applyNeedRelief(winner, { safety: -rand(12, 22), fulfill: -rand(8, 16) });
if (this.relationNotice(winner.id, loser.id, "fight-result", 8)) {
this.log(`${loser.name}\u306f${winner.name}\u306b\u8ca0\u3051\u3001\u305d\u306e\u3053\u3068\u3092\u899a\u3048\u305f\u3002`, "fight", { participants: [loser, winner] });
}
@ -574,7 +577,7 @@
t.damage(dealtDamage, reason);
if (isGenkotsu) {
t.fearTimer = Math.max(t.fearTimer || 0, 4.0 + p * 2.2);
t.stress = clamp((t.stress || 0) + 10 + p * 18, 0, 130);
if (typeof applyNeedShock === "function") applyNeedShock(t, { safety: 18 + p * 30, health: 8 + p * 15 });
t.adjustPersonality?.("openness", -(0.020 + p * 0.030), "after being struck by genkotsu.");
t.thought = "げんこつがこわい";
} else {
@ -586,6 +589,7 @@
t.fallDir = nx >= 0 ? 1 : -1;
hit += 1;
}
if (typeof damageNearbyStructures === "function") damageNearbyStructures(this, it.x, it.y, radius, damage, it);
const antHit = this.damageAntsInRadius(it.x, it.y, radius + 54, p => damage * (0.35 + p * 0.65), reason, {
push: p => push * (0.62 + p * 1.25),
});
@ -817,7 +821,7 @@
this.applyImpactDamage(t, damage, "\u885d\u7a81");
const sensitiveHit = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) ? 1.35 : (t.shouldApplyPersonalityBehavior?.("neuroticism", -1) ? 0.82 : 1.0);
const stressGain = clamp(damage * 0.55 * sensitiveHit, 3, sensitiveHit > 1 ? 24 : 18);
t.addStress ? t.addStress(stressGain, { threshold: 8 }) : (t.stress = clamp(t.stress + stressGain, 0, 130));
if (t.addStress) t.addStress(stressGain, { threshold: 8 });
if (t.enterPanic) t.enterPanic({ target: { x: ball.x, y: ball.y }, reason: "高速のボールにぶつかってパニックになっている", fear: 0.95, stress: 0, wake: true, cause: "fast_ball_hit" });
else t.setActionState?.("panic", { target: { x: ball.x, y: ball.y }, reason: "高速のボールにぶつかってパニックになっている", wake: true });
t.hurtTimer = Math.max(t.hurtTimer, damage > 16 ? 2.4 : 1.35);
@ -855,7 +859,7 @@
t.energy = clamp(t.energy - (playIntent ? 0.20 : 0.08), 0, 100);
const playRelief = playful && playIntent ? 2.8 : playful ? 1.5 : playIntent ? 0.75 : 0.20;
const sensitiveBump = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) && !playIntent ? clamp(ballSpeed / 230, 0.6, 2.6) : 0;
t.stress = clamp(t.stress - playRelief + sensitiveBump, 0, 130);
if (typeof applyNeedShock === "function" && sensitiveBump > playRelief) applyNeedShock(t, { safety: (sensitiveBump - playRelief) * 4 }); else if (typeof applyNeedRelief === "function") applyNeedRelief(t, { fulfill: -playRelief * 6, social: -playRelief * 2 });
if (playful && playIntent) {
t.loneliness = clamp(t.loneliness - 0.9, 0, 100);
t.affection = clamp(t.affection + 0.18, 0, 100);

View file

@ -730,8 +730,8 @@
b.fightTargetIds = b.fightTargetIds.slice(-4);
a.fightTargetId = a.fightTargetIds[0];
b.fightTargetId = b.fightTargetIds[0];
a.addStress ? a.addStress(rand(8, 18), { threshold: 8 }) : (a.stress = clamp(a.stress + rand(8, 18), 0, 130));
b.addStress ? b.addStress(rand(8, 18), { threshold: 8 }) : (b.stress = clamp(b.stress + rand(8, 18), 0, 130));
if (a.addStress) a.addStress(rand(8, 18), { threshold: 8 });
if (b.addStress) b.addStress(rand(8, 18), { threshold: 8 });
const damageToA = b.outgoingDamage ? b.outgoingDamage(rand(2, 7)) : rand(2, 7);
const damageToB = a.outgoingDamage ? a.outgoingDamage(rand(2, 7)) : rand(2, 7);
a.damage(damageToA, "\u55a7\u5629");

View file

@ -1,6 +1,6 @@
"use strict";
const APP_VERSION = "15.24.0";
const APP_VERSION = "15.24.2";
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const v = `v=${APP_VERSION}`;
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.