stable
This commit is contained in:
parent
8bd4405fe8
commit
d5c661bc22
53 changed files with 4264 additions and 950 deletions
110
PATCH_NOTES.md
110
PATCH_NOTES.md
|
|
@ -1,4 +1,96 @@
|
|||
# Patch Notes 15.24.12
|
||||
# 15.24.35
|
||||
|
||||
- 持ち主不明のかんたんベッドは、最初に利用した個体の所有物になるよう変更。
|
||||
- かんたんベッド同士が近すぎる位置に作られにくいよう、配置ブロックと建築位置選定を調整。
|
||||
- 複製機にセットされた食べ物の種類を、食べ物探索の優先順位と接触距離ソートに反映。
|
||||
|
||||
# 15.24.34
|
||||
|
||||
- ぬいぐるみを添付スプライトに差し替え、白背景を透過化。
|
||||
- たりないデータの「関係」と「家族」を1セクションに統合。
|
||||
- 観察中の友達/敵対ハイライト判定を相互関係ベースに拡張。
|
||||
- かんたんベッドで睡眠中の個体に、親子でも友達でもない個体が寝に来た場合は喧嘩開始。
|
||||
|
||||
# Patch Notes 15.24.33
|
||||
|
||||
## 15.24.33
|
||||
|
||||
- Version: `15.24.33`.
|
||||
- Build: `compact-save-v2`.
|
||||
- Added item-bucket dirty tracking, `ensureItemBuckets()`, `itemById()`, `liveItemsOfType()`, and `addItem()` to keep type buckets and id lookup coherent after runtime item additions.
|
||||
- Routed common item creation paths through `World.addItem()` for placements, rain water, grass growth/spawn, zunchi burst/spawn, ant corpses/nests, Tarinai death traces, and constructed owned structures.
|
||||
- Expanded runtime diagnostics with bucket dirty state, item id-map size, and bucket rebuild counters.
|
||||
- Reworked several hot item lookups to use buckets or id-map helpers where safe.
|
||||
- Updated regression checks for item-bucket helper availability and direct item push documentation.
|
||||
- Added final pre-save runtime-polish report and remaining-work split note.
|
||||
- Did not touch save migration, population/family limits, or family-tree UI.
|
||||
|
||||
# Patch Notes 15.24.31
|
||||
|
||||
## 15.24.31
|
||||
|
||||
- Version: `15.24.31`.
|
||||
- Build: `social-balance-runtime-polish`.
|
||||
- Fixed social-action balance regression where conflict could dominate too often after ActionSpec migration.
|
||||
- Reduced normal conflict pressure, lowered fight/intimidation priority, and raised mate/bond selection weight.
|
||||
- Added pair-level fight cooldown guards so one pair/individual cannot trigger repeated fight starts in the same burst.
|
||||
- Shortened intimidation/defeated display timers.
|
||||
- Preserved fight mochi as a strong forced exception while reducing ordinary proximity-fight probability.
|
||||
- Added regression checks for social balance constants and fight re-entry guards.
|
||||
- Did not touch save migration, population/family limits, or family-tree UI.
|
||||
|
||||
# Patch Notes 15.24.30
|
||||
|
||||
## 15.24.30
|
||||
|
||||
- Version: `15.24.30`.
|
||||
- Build: `action-spec-sunbath-runtime-check`.
|
||||
- Moved `sunbath` into `createSunbathActionSpec()` and registered it directly in `TARINAI_ACTION_DEFINITIONS`.
|
||||
- Removed the remaining `configureTarinaiNeedActions()` late-binding bridge.
|
||||
- Kept the existing sunbath body/timer subsystem intact; player-visible sunbath behavior should remain unchanged.
|
||||
- Updated regression checks so late action patching is treated as a failure.
|
||||
- Did not touch save migration, population/family limits, or family-tree UI.
|
||||
|
||||
# Patch Notes 15.24.29
|
||||
|
||||
## 15.24.29
|
||||
- Migrated social/fear/combat/mate/birth runtime actions into ActionSpec factories.
|
||||
- Kept family data, family tree UI, population limits, and save migration untouched.
|
||||
- Added regression checks for pass 3 ActionSpec migration.
|
||||
- Build: `action-spec-social-runtime-check`.
|
||||
|
||||
# Patch Notes 15.24.28
|
||||
|
||||
## 15.24.28
|
||||
|
||||
- Added centralized behavior-text composition through `composeTarinaiBehaviorText()` and `patchBehaviorTextFromComposer()`.
|
||||
- Added `textTarinaiAction()` to the ActionSpec lifecycle helpers.
|
||||
- Kept existing explicit live behavior text as the default display fallback so player-facing wording does not churn during normal observation.
|
||||
- Added a player-visible diff check comparing the current build against the initial 15.24.20 ZIP.
|
||||
- Kept save migration, population/family caps, and family-tree UI out of scope.
|
||||
|
||||
# Patch Notes 15.24.25
|
||||
|
||||
## 15.24.25
|
||||
|
||||
- Added `scripts/regression_check.py` and `docs/REGRESSION_CHECKLIST.md` to freeze the current generated-file, syntax, and ActionSpec lifecycle baseline.
|
||||
- Expanded `js/tarinai_action_spec.js` with explicit lifecycle helpers: `selectTarinaiActionTarget`, `canStartTarinaiAction`, `startTarinaiAction`, `tickTarinaiAction`, `finishTarinaiAction`, and `failTarinaiAction`.
|
||||
- Migrated need-action definitions from legacy `condition/run/update` shape toward explicit `selectTarget/canStart/start/tick` ActionSpec fields.
|
||||
- Routed priority/action eligibility through `canStartTarinaiAction()` while preserving legacy fallback behavior.
|
||||
- Added finish/fail lifecycle calls for completed or failed action ticks.
|
||||
- Kept save migration, population caps, family caps, and family-tree UI out of scope.
|
||||
|
||||
# Patch Notes 15.24.23
|
||||
|
||||
## 15.24.23
|
||||
|
||||
- Added coarse-grained spatial invalidation via `markSpatialDirty()` + `ensureSpatial()` for the main update loop.
|
||||
- Reduced redundant spatial rebuilds around item, ball, ant, and Tarinai update phases while keeping direct `rebuildSpatial()` calls compatible.
|
||||
- Added per-type item buckets in `updateItemCounts()` and switched ball-ball collision setup to `itemsOfType("ball")`.
|
||||
- Reworked render layering into a cached dynamic render stack so expensive y-sort work runs on a short interval or count/signature change, not every paint.
|
||||
- Kept population and family-tree UI behavior unchanged.
|
||||
|
||||
# Patch Notes 15.24.22
|
||||
|
||||
- Added `activeBehavior` as the authoritative runtime behavior layer above legacy state timers.
|
||||
- Added `forcedBehaviorQueue` and `forceBehavior()` hook for external forced actions such as love mochi, fight mochi, and drugs.
|
||||
|
|
@ -9,7 +101,21 @@
|
|||
- Reduced direct old-style birth starts: proximity now raises mate pressure; ritual starts when mate behavior is active or forced.
|
||||
- UI action text now prefers `activeBehavior` over stale thoughts or intent strings.
|
||||
|
||||
## 15.24.14
|
||||
## 15.24.22
|
||||
- `js/tarinai_action_spec.js` を追加し、行動を `ActionSpec` として登録できる共通層を導入。
|
||||
- 既存の `condition` / `run` / `update` を維持しつつ、`canStart` / `start` / `tick` / `finish` / `fail` / `text` に段階移行できる互換ラッパーを追加。
|
||||
- `TARINAI_ACTIONS` を `TARINAI_ACTION_DEFINITIONS` から登録生成する形に変更。既存参照は維持。
|
||||
- 行動開始・継続処理を `startTarinaiAction()` / `tickTarinaiAction()` 経由へ移行。
|
||||
- `behavior` に `specId` / `specKind` を保持し、表示・旧API互換と行動仕様の対応を追いやすくした。
|
||||
|
||||
## 15.24.21
|
||||
- 外部強制行動を queued / active / retry / expire の状態機械に拡張。
|
||||
- 強制行動の source / forcedReasonText を activeBehavior に保持し、旧タイマー同期で失われにくくした。
|
||||
- 関係欲求の開始判定を social 大分類ではなく bond / family / mate / conflict のサブ欲求に寄せた。
|
||||
- 食事行動を activeBehavior 内で対象接近から摂食完了まで完結するよう整理。
|
||||
- 行動選択時に高い未選択欲求がある場合、「眠たいけど、食べている」のように対立理由を表示。
|
||||
|
||||
## 15.24.21
|
||||
- 外部強制行動を強化。薬・餅から `forceBehavior()` 経由で優先割込み、対象再探索、失敗時再試行を行うようにした。
|
||||
- へこ餅、けんか餅、ねむり薬の強制行動優先度を更新。
|
||||
- 喧嘩対象が Item 等になった場合の `relationTo is not a function` エラーを防止。
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Tarinai Colony Observation Game
|
||||
|
||||
Buildless single-page browser simulation. Open `index.html`; no package manager, transpiler, server API, or module loader is required. Runtime order is explicit `<script defer>` order generated from `app_manifest.json`. Current inspected version: `15.20.16`.
|
||||
Buildless single-page browser simulation. Open `index.html`; no package manager, transpiler, server API, or module loader is required. Runtime order is explicit `<script defer>` order generated from `app_manifest.json`. Current inspected version: `15.24.33`.
|
||||
|
||||
## Edit contract
|
||||
|
||||
|
|
@ -16,6 +16,9 @@ Buildless single-page browser simulation. Open `index.html`; no package manager,
|
|||
- `docs/ARCHITECTURE.md`: runtime architecture, file ownership, system boundaries.
|
||||
- `docs/CONTENT_GUIDE.md`: item visuals, UI/field visual parity, audio, bubble text inventory.
|
||||
- `docs/MAINTENANCE.md`: release process, validation, debugging, cache/version rules.
|
||||
- `docs/RUNTIME_POLISH_PRE_SAVE_REPORT.md`: 15.24.32 runtime polish summary before save migration.
|
||||
- `docs/COMPACT_SAVE_V2_REPORT.md`: 15.24.33 compact save v2 summary.
|
||||
- `docs/REMAINING_WORK_SPLIT.md`: work intentionally split after 15.24.33.
|
||||
- `docs/prompt.txt`: untouched user prompt/context archive.
|
||||
|
||||
## Player-facing surface
|
||||
|
|
@ -41,6 +44,7 @@ Buildless single-page browser simulation. Open `index.html`; no package manager,
|
|||
- `WeatherSystem`: `js/weather_system.js`; weather state transitions and rain-spawned water items.
|
||||
- `Audio`: `js/sound_pack.js` owns sound metadata; `js/audio.js` loads samples, applies settings, enforces cooldowns, and runs synthetic fallbacks.
|
||||
- `Performance`: `js/performance_manager.js`; FPS quality level, ant stride, effect budget, terrain redraw cadence.
|
||||
- `Runtime item lookup`: `World.addItem()`, `itemsOfType()`, `itemById()`, and `ensureItemBuckets()` keep item caches coherent for population-scale runs.
|
||||
|
||||
## Current high-risk areas
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "15.24.14",
|
||||
"version": "15.24.35",
|
||||
"css": [
|
||||
"css/base.css",
|
||||
"css/layout.css",
|
||||
|
|
@ -25,10 +25,13 @@
|
|||
"js/audio.js",
|
||||
"js/render.js",
|
||||
"js/sim_core.js",
|
||||
"js/structures.js",
|
||||
"js/items.js",
|
||||
"js/ants.js",
|
||||
"js/health.js",
|
||||
"js/tarinai.js",
|
||||
"js/tarinai_action_spec.js",
|
||||
"js/tarinai_behavior_state.js",
|
||||
"js/tarinai_identity_social.js",
|
||||
"js/tarinai_action_state.js",
|
||||
"js/tarinai_disease_nest.js",
|
||||
|
|
|
|||
BIN
assets/objects/plushie.webp
Normal file
BIN
assets/objects/plushie.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 810 B |
|
|
@ -478,14 +478,18 @@ h1 {
|
|||
#signboardEditorText {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
resize: none;
|
||||
min-height: 88px;
|
||||
max-height: 88px;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(84,68,48,0.18);
|
||||
background: rgba(255,255,255,0.84);
|
||||
padding: 10px 11px;
|
||||
color: var(--ink);
|
||||
font: 14px/1.55 ui-rounded, "Hiragino Maru Gothic ProN", "Yu Gothic", sans-serif;
|
||||
font: 15px/1.55 ui-rounded, "Hiragino Maru Gothic ProN", "Yu Gothic", sans-serif;
|
||||
letter-spacing: 0.03em;
|
||||
outline: none;
|
||||
}
|
||||
#signboardEditorText:focus {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Architecture
|
||||
|
||||
Build target: static browser page. No bundler. Globals and ordered deferred scripts are intentional. Current inspected version: `15.20.16`.
|
||||
Build target: static browser page. No bundler. Globals and ordered deferred scripts are intentional. Current inspected version: `15.24.33`.
|
||||
|
||||
## Load order
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ The old `css/styles.css` compatibility shim and `js/ui_family.js` split marker h
|
|||
|
||||
- `World`: constructor in `js/world.js`; methods split into view, family/social, combat/effects, environment, spatial/budget, ants, update, placement/log, bootstrap.
|
||||
- `Tarinai`: constructor in `js/tarinai.js`; methods split into identity/social, disease/nest, item effects, needs/items, social/move/life, render.
|
||||
- Behavior text is composed from `behavior`, needs, tied needs, ActionSpec metadata, and forced-request cause metadata; stored explicit live text remains the display fallback to avoid player-visible wording churn.
|
||||
- `Item`: item state, lifecycle, scaling, and field rendering in `js/items.js`.
|
||||
- `Effect`: transient visuals in `js/sim_core.js`; combat/weather/food effects are spawned through `World` helpers.
|
||||
- `TarinaiEvents`: global event bus for logs, terrain dirtiness, audio failures, phase/performance changes.
|
||||
|
|
@ -46,7 +47,7 @@ The old `css/styles.css` compatibility shim and `js/ui_family.js` split marker h
|
|||
|
||||
`main.js` starts asset preload, world bootstrap, animation loop, deferred image loading, service-worker registration. `World.update(dt)` advances time, weather, items, ants, Tarinai, effects, budgets, terrain dirtiness. `render()` draws cached background/terrain, visible entities, weather overlay, cards, HUD, and debug overlays.
|
||||
|
||||
World time is `CONFIG.dayLength` seconds per day. Version `15.20.16` uses 120 seconds/day and weather changes every 46-96 seconds. The phase boundary rules live in `World.phaseName()`.
|
||||
World time is `CONFIG.dayLength` seconds per day. Version `15.24.33` uses 120 seconds/day and weather changes every 46-96 seconds. The phase boundary rules live in `World.phaseName()`.
|
||||
|
||||
Domain events are emitted at central mutation points: placement, eating, decay, effect apply/remove, disease infect/cure, damage/death/birth, fight hit/loss/end, ant spawn/grab/carry/death, weather changes, and rain item drops. Logs/audio still use direct calls where they already existed; do not add duplicate log or sound side effects when subscribing to these events.
|
||||
|
||||
|
|
@ -57,3 +58,20 @@ Ant behavior remains intentionally conservative. `ANT_EXTENSION_HOOKS` names fut
|
|||
Use `World.nearby*` helpers rather than full-array scans in hot paths. Terrain cache uses dirty/version flags; do not reintroduce every-frame full terrain hashing. Performance degradation should be centralized in `performance_manager.js`.
|
||||
|
||||
Family tree rendering is intentionally asynchronous and cached. `ui_family_async.js` chunks component discovery with `requestIdleCallback` when available; `ui_family_render.js` hydrates cached HTML, keeps archive dirty/version state, and exposes `validateFamilyTree()` plus `familyTreeDiagnostics()` for browser-console checks.
|
||||
|
||||
|
||||
## Basic ActionSpec migration
|
||||
|
||||
Version `15.24.33` keeps low-risk need actions, social/fear/combat/mate/birth actions, and sunbath registered through ActionSpec factories. The former configureTarinaiNeedActions late-binding bridge has been removed; the existing sunbath subsystem remains as the body-level timer implementation called by the ActionSpec.
|
||||
|
||||
## Runtime diagnostics
|
||||
|
||||
`World.runtimeDiagnostics()` exposes spatial rebuild counters, draw-list status, item bucket counts, and behavior counts. `?debug=1` renders these values in the existing debug overlay for manual browser regression checks.
|
||||
|
||||
## Social balance guards
|
||||
|
||||
Version `15.24.33` adds explicit guardrails around combat-heavy social behavior. Normal conflict must be dominant over bond/mate/family pressure before `fight_rival` can start, and `World.startFight()` / `startConflict()` / `startForcedFight()` use pair-level cooldowns plus already-fighting checks to prevent burst re-entry. Fight mochi remains a strong forced exception, but ordinary proximity conflict is intentionally lower than mate/friend pressure.
|
||||
|
||||
## Pre-save runtime polish
|
||||
|
||||
Version `15.24.33` keeps item type buckets and item id lookup as runtime caches owned by `World.updateItemCounts()`. Runtime item additions should go through `World.addItem(item, reason)` when practical so spatial, terrain, and item-bucket invalidation stay synchronized. Save migration is still intentionally deferred until this runtime shape has been browser-observed.
|
||||
|
|
|
|||
37
docs/COMPACT_SAVE_V2_REPORT.md
Normal file
37
docs/COMPACT_SAVE_V2_REPORT.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Compact Save V2 Report — 15.24.33
|
||||
|
||||
## 方針
|
||||
|
||||
15.24.33 では旧セーブ互換を捨て、保存形式を `v: 2` に作り直した。
|
||||
目的はハッシュテキストを短くすることであり、旧 `TARINAI_SAVE_V1:` / `tarinai_save_slot_v1_` は読み込み対象外。
|
||||
|
||||
## 削除した保存対象
|
||||
|
||||
- ログ履歴。
|
||||
- 一時エフェクト。
|
||||
- UI選択状態。
|
||||
- 描画用キャッシュ、spatial/grid、bucket、terrain cache。
|
||||
- `needDisplay` / `needRawBase` / `previousNeeds` など、読み込み後に再計算または近似できる値。
|
||||
- 短命な表示タイマー、吹き出し文、内部デバッグ計測値。
|
||||
- 死亡者を含む完全な家系アーカイブ履歴。生存個体の親子関係は index 参照として保存し、読み込み後に `recordFamily()` で再構築する。
|
||||
|
||||
## 残した保存対象
|
||||
|
||||
- world の field / day / time / weather / camera / generation などの進行状態。
|
||||
- 生存中たりないの名前、見た目、性格、遺伝、位置、主要欲求、病気、親子参照、友好・恐怖関係。
|
||||
- 生存中アイテムの type / 位置 / 量 / type別に必要な追加状態。
|
||||
- アリの巣・アリの主要状態。
|
||||
- 構造物の owner / hp / attachment などのプレイに関わる状態。
|
||||
|
||||
## ハッシュテキスト
|
||||
|
||||
- Prefix: `TN2!`
|
||||
- Codec: `D` = `CompressionStream("deflate-raw")`、`R` = fallback raw bytes。
|
||||
- Text encoding: printable ASCII 33〜126 のうち `*`, `'`, `_` を除いた91文字集合。
|
||||
- Binary-to-text は base91 相当の13/14bit詰め。
|
||||
|
||||
## 注意
|
||||
|
||||
- 旧セーブは読めない。これは意図した仕様。
|
||||
- 家系図の死亡者履歴は保存しない。読み込み後の家系図は生存個体ベースになる。
|
||||
- `CompressionStream` 非対応環境では raw bytes を同じ91文字集合で符号化する。短縮率は下がるが読み書きは維持する。
|
||||
52
docs/PLAYER_VISIBLE_DIFF_REPORT.md
Normal file
52
docs/PLAYER_VISIBLE_DIFF_REPORT.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Player-visible diff report: 15.24.20 initial ZIP vs 15.24.33
|
||||
|
||||
Comparison target: `tarinai_ui_revision_v15_24_20_pin_social_sign_polish(1).zip` → current build `15.24.33`.
|
||||
|
||||
## Verdict
|
||||
|
||||
No large UI/tool/item/save/family-tree specification change was detected. The intentional player-visible behavior change in this build is limited to social-action balance: ordinary fights and intimidation should occur less often, while friendship and reproduction should no longer be crowded out by conflict.
|
||||
|
||||
## Evidence
|
||||
|
||||
| Surface | Result | Notes |
|
||||
|---|---|---|
|
||||
| Top-level visible text in `index.html` | unchanged | Script tags/version params changed; visible UI copy is still the same. |
|
||||
| Assets | unchanged | No sprite, object, UI icon, or sound asset change was introduced in this pass. |
|
||||
| CSS | unchanged | No style-file change in this pass. |
|
||||
| Tool/item definitions | unchanged | `js/data.js` tool/item definitions were not changed. |
|
||||
| Tool UI binding | unchanged | No tool-list or operation binding change in this pass. |
|
||||
| Item behavior/render definitions | unchanged | `items.js` was not changed. |
|
||||
| Save/snapshot files | unchanged | Save migration remains intentionally deferred. |
|
||||
| Family-tree UI modules | unchanged | Family-tree UI remains out of scope. |
|
||||
| Tarinai social behavior | intentionally tuned | Conflict pressure and repeated-fight gates were adjusted to restore friend/mate/birth viability. |
|
||||
|
||||
## Files added since the original internal-structure pass
|
||||
|
||||
- `docs/REGRESSION_CHECKLIST.md`
|
||||
- `docs/SOCIAL_BALANCE_FIX_REPORT.md`
|
||||
- `js/tarinai_action_spec.js`
|
||||
- `js/tarinai_behavior_state.js`
|
||||
- `scripts/regression_check.py`
|
||||
|
||||
## Changed files with possible player-visible risk
|
||||
|
||||
| File | Risk | Assessment |
|
||||
|---|---|---|
|
||||
| `js/tarinai_needs_items.js` | Behavior selection and observation text | Social conflict is now less dominant; mate/friend actions are easier to select. |
|
||||
| `js/world_family_social.js` | Fight/intimidation timing | Pair-level fight re-entry guard added; intimidation and fight durations shortened. |
|
||||
| `js/tarinai_social_move_life.js` | Proximity conflict pressure | Ordinary close-contact fight probability reduced; fight mochi remains strong. |
|
||||
| `js/render.js` | Draw ordering | Dynamic render stack remains cached for performance; possible minor y-sort delay under dense scenes. |
|
||||
| `js/world_update.js` / `js/world_spatial_budget.js` | Simulation lookup performance | Spatial rebuild timing remains dirty/ensure style. |
|
||||
| `service-worker.js` / `index.html` / `app_manifest.json` / `js/version.js` | Loading/cache | Version and script query params changed only. |
|
||||
|
||||
## Explicitly not changed
|
||||
|
||||
- No population cap was added.
|
||||
- No family or generation cap was added.
|
||||
- Family-tree UI was not modified.
|
||||
- No tool category, tool name, item sprite, CSS layout, or field choice change was introduced.
|
||||
- Save migration was not implemented in this pass.
|
||||
|
||||
## Residual risk
|
||||
|
||||
The remaining visible risk is balance rather than UI specification: social behavior should now produce fewer conflict loops, but exact rates still require browser playtesting with dense populations and multiple personality mixes.
|
||||
45
docs/REGRESSION_CHECKLIST.md
Normal file
45
docs/REGRESSION_CHECKLIST.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Regression Checklist
|
||||
|
||||
Version: 15.24.33
|
||||
|
||||
This pass freezes the current internal baseline before continuing deeper behavior refactors.
|
||||
|
||||
## Scope
|
||||
|
||||
- Static loading order must remain generated from `app_manifest.json`.
|
||||
- `index.html` and `service-worker.js` must match the manifest after `scripts/generate_app_files.py`.
|
||||
- JavaScript syntax must pass `node --check`.
|
||||
- `ActionSpec` lifecycle helpers must support `selectTarget / canStart / start / tick / finish / fail`.
|
||||
- Save-data migration is intentionally out of scope for this pass.
|
||||
- Population caps and family-tree UI are intentionally out of scope.
|
||||
|
||||
## Command
|
||||
|
||||
```bash
|
||||
python3 scripts/regression_check.py
|
||||
```
|
||||
|
||||
Expected result: all checks print `[OK]` and exit with status 0.
|
||||
|
||||
|
||||
## 15.24.28 runtime diagnostics
|
||||
|
||||
When opening with `?debug=1`, verify the overlay reports spatial rebuilds, draw-list state, item bucket/id-map state, forced/locked behavior counts, and top behavior ids. These are observation aids only and do not change player controls or simulation rules.
|
||||
|
||||
## Social balance regression checks
|
||||
|
||||
For `15.24.33+`, also verify:
|
||||
|
||||
- Ordinary conflict does not crowd out `approach_friend` / `approach_mate` in a neutral population.
|
||||
- Fight mochi still forces conflict, but one individual should not start several fights at once.
|
||||
- Intimidation resolves quickly enough that it does not visually lock the colony.
|
||||
- With `?debug=1`, behavior counts should show non-conflict social actions appearing over time in a dense but non-drugged population.
|
||||
|
||||
|
||||
## 15.24.33 runtime item-bucket checks
|
||||
|
||||
- Runtime-created items should go through `World.addItem()` where safe.
|
||||
- `World.itemsOfType()` should call `ensureItemBuckets()` before returning a bucket.
|
||||
- `World.itemById()` should not return dead items.
|
||||
- The debug overlay should expose item bucket dirty state, id-map size, and bucket rebuild counters.
|
||||
- Save migration remains out of scope until this runtime polish is observed in-browser.
|
||||
46
docs/REMAINING_WORK_SPLIT.md
Normal file
46
docs/REMAINING_WORK_SPLIT.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Remaining Work Split — after 15.24.32
|
||||
|
||||
## Bundled in 15.24.32
|
||||
|
||||
- Performance/runtime polish that does not alter save semantics.
|
||||
- Item bucket/id-map coherence.
|
||||
- Direct item creation helper consolidation.
|
||||
- Regression guard expansion.
|
||||
- Documentation update for the pre-save state.
|
||||
|
||||
## Still intentionally split
|
||||
|
||||
### 1. Browser long-run verification
|
||||
|
||||
Static checks pass, but long-run behavior still needs a real browser observation pass:
|
||||
|
||||
- 10+ minutes normal observation.
|
||||
- Food/water/medicine/mochi placement.
|
||||
- Friend formation, mate approach, birth ritual.
|
||||
- Conflict/fight/intimidation cooldown behavior.
|
||||
- Large population stress check with `?debug=1`.
|
||||
|
||||
### 2. Save migration
|
||||
|
||||
Do after the runtime shape is stable.
|
||||
|
||||
Target migration scope:
|
||||
|
||||
- Convert legacy `intent/currentAction/activeBehavior/activeForcedBehavior` save fields into `behavior`.
|
||||
- Avoid saving transient display text and short-lived timers where possible.
|
||||
- Keep old saves readable.
|
||||
- Keep new saves simpler.
|
||||
|
||||
### 3. Compatibility-layer shrink
|
||||
|
||||
Only after save migration:
|
||||
|
||||
- Mark legacy behavior fields as deprecated runtime views.
|
||||
- Remove direct write paths where no longer needed.
|
||||
- Keep read-only compatibility only if old save support requires it.
|
||||
|
||||
## Not in scope by user preference
|
||||
|
||||
- Population cap.
|
||||
- Family/generation cap.
|
||||
- Family-tree UI rewrite.
|
||||
63
docs/RUNTIME_POLISH_PRE_SAVE_REPORT.md
Normal file
63
docs/RUNTIME_POLISH_PRE_SAVE_REPORT.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Runtime Polish Pre-Save Report — 15.24.32
|
||||
|
||||
This pass bundles the remaining low-risk internal cleanup that can be done before save-data migration.
|
||||
|
||||
## Scope
|
||||
|
||||
Included:
|
||||
|
||||
- Runtime item lookup coherence.
|
||||
- Item bucket/id-map helper expansion.
|
||||
- Common runtime item creation paths routed through `World.addItem()`.
|
||||
- Regression guard updates.
|
||||
- Final pre-save documentation.
|
||||
|
||||
Explicitly excluded:
|
||||
|
||||
- Save-data migration.
|
||||
- Population or family caps.
|
||||
- Family-tree UI performance work.
|
||||
- Player-facing tool/UI redesign.
|
||||
|
||||
## What changed internally
|
||||
|
||||
`World.updateItemCounts()` now rebuilds both:
|
||||
|
||||
- `itemTypeBuckets: Map<string, Item[]>`
|
||||
- `itemIdMap: Map<string, Item>`
|
||||
|
||||
The following helpers are available:
|
||||
|
||||
```txt
|
||||
markItemBucketsDirty(reason)
|
||||
ensureItemBuckets(reason)
|
||||
addItem(item, reason, opts)
|
||||
itemById(id)
|
||||
itemsOfType(type)
|
||||
liveItemsOfType(type, predicate)
|
||||
```
|
||||
|
||||
This keeps high-frequency code from repeatedly scanning `world.items` for common lookups.
|
||||
|
||||
## Runtime creation paths routed through addItem
|
||||
|
||||
Common additions now dirty item buckets/spatial/terrain consistently:
|
||||
|
||||
- Player placement.
|
||||
- Rain water drop.
|
||||
- Grass spawn and grass seed growth.
|
||||
- Zunchi burst/spawn.
|
||||
- Ant corpse and ant nest creation.
|
||||
- Tarinai death trace/splat/grass.
|
||||
- Built owned structures.
|
||||
- Field reset grass/stone.
|
||||
|
||||
## Player-visible impact
|
||||
|
||||
No intentional change to normal UI, tools, item definitions, population/family rules, family-tree UI, or save/load controls.
|
||||
|
||||
The only visible change is in `?debug=1`: item bucket dirty state, id-map size, and bucket rebuild counters are now exposed through runtime diagnostics.
|
||||
|
||||
## Remaining split
|
||||
|
||||
Save migration is still separated because it changes persistence semantics. It should run after this runtime structure has been observed in browser for basic stability.
|
||||
28
docs/SOCIAL_BALANCE_FIX_REPORT.md
Normal file
28
docs/SOCIAL_BALANCE_FIX_REPORT.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Social Balance Fix Report — 15.24.31
|
||||
|
||||
## Reported symptoms
|
||||
|
||||
- Tarinai rarely reproduced or formed friendships.
|
||||
- Fights could start in rapid consecutive bursts.
|
||||
- Intimidation lasted too long.
|
||||
|
||||
## Main cause
|
||||
|
||||
The ActionSpec social migration preserved the old combat hooks, but conflict pressure and same-priority social selection made `fight_rival` too competitive against `approach_friend` and `approach_mate`. Friend contact also routed into fight behavior whenever nearby conflict was detected, even when bond or mate pressure was stronger.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Reduced ordinary conflict pressure and carried conflict weight.
|
||||
- Lowered `fight_rival` and `intimidate_enemy` priority/weight.
|
||||
- Raised `approach_mate` and `approach_friend` priority/weight.
|
||||
- Required non-forced conflict to be genuinely dominant over bond/mate/family pressure.
|
||||
- Added pair-level fight cooldown and already-fighting guards in `World.startFight()`, `startConflict()`, and `startForcedFight()`.
|
||||
- Shortened intimidation and intimidation-defeat timers.
|
||||
- Kept fight mochi as a high-priority forced behavior.
|
||||
|
||||
## Not changed
|
||||
|
||||
- No population cap.
|
||||
- No family-generation cap.
|
||||
- No family-tree UI work.
|
||||
- No save migration.
|
||||
136
index.html
136
index.html
|
|
@ -5,11 +5,11 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>たりない観察</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.14" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=15.24.14" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=15.24.14" />
|
||||
<link rel="stylesheet" href="css/components.css?v=15.24.14" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=15.24.14" />
|
||||
<link rel="stylesheet" href="css/base.css?v=15.24.35" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=15.24.35" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=15.24.35" />
|
||||
<link rel="stylesheet" href="css/components.css?v=15.24.35" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=15.24.35" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||
|
|
@ -183,67 +183,69 @@
|
|||
</div>
|
||||
|
||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||
<script src="js/version.js?v=15.24.14" defer></script>
|
||||
<script src="js/event_bus.js?v=15.24.14" defer></script>
|
||||
<script src="js/performance_manager.js?v=15.24.14" defer></script>
|
||||
<script src="js/registry_base.js?v=15.24.14" defer></script>
|
||||
<script src="js/disease_registry.js?v=15.24.14" defer></script>
|
||||
<script src="js/effect_registry.js?v=15.24.14" defer></script>
|
||||
<script src="js/sound_pack.js?v=15.24.14" defer></script>
|
||||
<script src="js/data.js?v=15.24.14" defer></script>
|
||||
<script src="js/item_visual_registry.js?v=15.24.14" defer></script>
|
||||
<script src="js/food_registry.js?v=15.24.14" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=15.24.14" defer></script>
|
||||
<script src="js/math.js?v=15.24.14" defer></script>
|
||||
<script src="js/physics_helpers.js?v=15.24.14" defer></script>
|
||||
<script src="js/assets.js?v=15.24.14" defer></script>
|
||||
<script src="js/audio.js?v=15.24.14" defer></script>
|
||||
<script src="js/render.js?v=15.24.14" defer></script>
|
||||
<script src="js/sim_core.js?v=15.24.14" defer></script>
|
||||
<script src="js/structures.js?v=15.24.14" defer></script>
|
||||
<script src="js/items.js?v=15.24.14" defer></script>
|
||||
<script src="js/ants.js?v=15.24.14" defer></script>
|
||||
<script src="js/health.js?v=15.24.14" defer></script>
|
||||
<script src="js/tarinai.js?v=15.24.14" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=15.24.14" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=15.24.14" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=15.24.14" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=15.24.14" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=15.24.14" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=15.24.14" defer></script>
|
||||
<script src="js/tarinai_render.js?v=15.24.14" defer></script>
|
||||
<script src="js/world.js?v=15.24.14" defer></script>
|
||||
<script src="js/world_view.js?v=15.24.14" defer></script>
|
||||
<script src="js/family_graph.js?v=15.24.14" defer></script>
|
||||
<script src="js/world_family_social.js?v=15.24.14" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=15.24.14" defer></script>
|
||||
<script src="js/world_environment.js?v=15.24.14" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=15.24.14" defer></script>
|
||||
<script src="js/world_ants_system.js?v=15.24.14" defer></script>
|
||||
<script src="js/weather_system.js?v=15.24.14" defer></script>
|
||||
<script src="js/world_update.js?v=15.24.14" defer></script>
|
||||
<script src="js/world_placement_log.js?v=15.24.14" defer></script>
|
||||
<script src="js/world_bootstrap.js?v=15.24.14" defer></script>
|
||||
<script src="js/text_catalog.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_log.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_helpers.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_selected.js?v=15.24.14" defer></script>
|
||||
<script src="js/snapshot_system.js?v=15.24.14" defer></script>
|
||||
<script src="js/save_system.js?v=15.24.14" defer></script>
|
||||
<script src="js/patch_helpers.js?v=15.24.14" defer></script>
|
||||
<script src="js/freeze_system.js?v=15.24.14" defer></script>
|
||||
<script src="js/relation_event_system.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_tools.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_bind.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_charts.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_family_data.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_family_async.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=15.24.14" defer></script>
|
||||
<script src="js/ui_family_render.js?v=15.24.14" defer></script>
|
||||
<script src="js/main.js?v=15.24.14" defer></script>
|
||||
<script src="js/debug_tools.js?v=15.24.14" defer></script>
|
||||
<script src="js/version.js?v=15.24.35" defer></script>
|
||||
<script src="js/event_bus.js?v=15.24.35" defer></script>
|
||||
<script src="js/performance_manager.js?v=15.24.35" defer></script>
|
||||
<script src="js/registry_base.js?v=15.24.35" defer></script>
|
||||
<script src="js/disease_registry.js?v=15.24.35" defer></script>
|
||||
<script src="js/effect_registry.js?v=15.24.35" defer></script>
|
||||
<script src="js/sound_pack.js?v=15.24.35" defer></script>
|
||||
<script src="js/data.js?v=15.24.35" defer></script>
|
||||
<script src="js/item_visual_registry.js?v=15.24.35" defer></script>
|
||||
<script src="js/food_registry.js?v=15.24.35" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=15.24.35" defer></script>
|
||||
<script src="js/math.js?v=15.24.35" defer></script>
|
||||
<script src="js/physics_helpers.js?v=15.24.35" defer></script>
|
||||
<script src="js/assets.js?v=15.24.35" defer></script>
|
||||
<script src="js/audio.js?v=15.24.35" defer></script>
|
||||
<script src="js/render.js?v=15.24.35" defer></script>
|
||||
<script src="js/sim_core.js?v=15.24.35" defer></script>
|
||||
<script src="js/structures.js?v=15.24.35" defer></script>
|
||||
<script src="js/items.js?v=15.24.35" defer></script>
|
||||
<script src="js/ants.js?v=15.24.35" defer></script>
|
||||
<script src="js/health.js?v=15.24.35" defer></script>
|
||||
<script src="js/tarinai.js?v=15.24.35" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=15.24.35" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=15.24.35" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=15.24.35" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=15.24.35" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=15.24.35" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=15.24.35" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=15.24.35" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=15.24.35" defer></script>
|
||||
<script src="js/tarinai_render.js?v=15.24.35" defer></script>
|
||||
<script src="js/world.js?v=15.24.35" defer></script>
|
||||
<script src="js/world_view.js?v=15.24.35" defer></script>
|
||||
<script src="js/family_graph.js?v=15.24.35" defer></script>
|
||||
<script src="js/world_family_social.js?v=15.24.35" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=15.24.35" defer></script>
|
||||
<script src="js/world_environment.js?v=15.24.35" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=15.24.35" defer></script>
|
||||
<script src="js/world_ants_system.js?v=15.24.35" defer></script>
|
||||
<script src="js/weather_system.js?v=15.24.35" defer></script>
|
||||
<script src="js/world_update.js?v=15.24.35" defer></script>
|
||||
<script src="js/world_placement_log.js?v=15.24.35" defer></script>
|
||||
<script src="js/world_bootstrap.js?v=15.24.35" defer></script>
|
||||
<script src="js/text_catalog.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_log.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_helpers.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_selected.js?v=15.24.35" defer></script>
|
||||
<script src="js/snapshot_system.js?v=15.24.35" defer></script>
|
||||
<script src="js/save_system.js?v=15.24.35" defer></script>
|
||||
<script src="js/patch_helpers.js?v=15.24.35" defer></script>
|
||||
<script src="js/freeze_system.js?v=15.24.35" defer></script>
|
||||
<script src="js/relation_event_system.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_tools.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_bind.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_charts.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_family_data.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_family_async.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=15.24.35" defer></script>
|
||||
<script src="js/ui_family_render.js?v=15.24.35" defer></script>
|
||||
<script src="js/main.js?v=15.24.35" defer></script>
|
||||
<script src="js/debug_tools.js?v=15.24.35" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
10
js/ants.js
10
js/ants.js
|
|
@ -344,10 +344,10 @@ class AntActor {
|
|||
const corpse = new Item("ant_corpse", this.x, this.y);
|
||||
corpse.amount = 24;
|
||||
corpse.workerSprite = this.workerSpriteId();
|
||||
this.world.items.push(corpse);
|
||||
this.world.addItem?.(corpse, "ant-corpse") || this.world.items.push(corpse);
|
||||
this.world.effects?.push(new Effect("zunchi_miasma", this.x, this.y - 4, { size: 10, life: 0.44, color: "rgba(82,61,42,0.28)" }));
|
||||
if (home) syncAntNestCount(this.world, home);
|
||||
this.world.updateItemCounts?.();
|
||||
this.world.ensureItemBuckets?.("ant-corpse");
|
||||
this.world.drawListDirty = true;
|
||||
}
|
||||
|
||||
|
|
@ -370,11 +370,9 @@ class AntActor {
|
|||
base.antWorkers = buildAntWorkerPool(ANT_NEST_START_COUNT);
|
||||
if (!force && this.world.placementBlocked?.(base)) return null;
|
||||
audio.antNest?.();
|
||||
this.world.items.push(base);
|
||||
this.world.itemCounts[base.type] = (this.world.itemCounts[base.type] || 0) + 1;
|
||||
this.world.markTerrainDirty?.("ant-nest-built");
|
||||
this.world.addItem?.(base, "ant-nest-built") || this.world.items.push(base);
|
||||
this.world.emit?.("ant:nest-built", { nest: base, queen: this });
|
||||
this.world.rebuildSpatial?.(true);
|
||||
this.world.ensureSpatial?.("ant-nest-built");
|
||||
this.world.drawListDirty = true;
|
||||
this.world.log("\u5973\u738b\u30a2\u30ea\u304c\u65b0\u3057\u3044\u30a2\u30ea\u306e\u5de3\u3092\u4f5c\u3063\u305f\u3002", "event");
|
||||
return base;
|
||||
|
|
|
|||
21
js/data.js
21
js/data.js
|
|
@ -44,6 +44,7 @@ const DECOR_ASSETS = [
|
|||
{ id: "pushpin_stuck", path: "assets/objects/pushpin_stuck.webp" },
|
||||
{ id: "oshibyo", path: "assets/objects/oshibyo.webp" },
|
||||
{ id: "oshibyo_stuck", path: "assets/objects/oshibyo_stuck.webp" },
|
||||
{ id: "plushie", path: "assets/objects/plushie.webp" },
|
||||
];
|
||||
|
||||
const TOOL_DEFINITIONS = Object.freeze({
|
||||
|
|
@ -75,9 +76,9 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
bed: { id: "bed", itemType: "bed", label: "\u5e72\u8349\u5bdd\u5e8a", placeable: true, scalable: false, radius: 37, amount: 999, icon: "assets/ui/tool_bed.webp", tooltip: "\u8fd1\u304f\u3067\u7720\u308b\u3002\u4f53\u529b\u304c\u56de\u5fa9\u3059\u308b\u3002" },
|
||||
nest_box: { id: "nest_box", itemType: "nest_box", label: "\u5de3\u7bb1", placeable: true, scalable: false, radius: 42, amount: 999, icon: "assets/ui/tool_nest_box.webp", collisionShape: "nest_box_3x3", tooltip: "\u3044\u308b\u3060\u3051\u3067\u30b9\u30c8\u30ec\u30b9\u4f4e\u6e1b\u3002\u7720\u308b\u3068\u7761\u7720\u306e\u8cea\u304c\u4e0a\u304c\u308b\u3002" },
|
||||
ant_nest: { id: "ant_nest", itemType: "ant_nest", label: "\u30a2\u30ea\u306e\u5de3", placeable: true, scalable: false, radius: 17, amount: 999, icon: "assets/ui/tool_ant_nest.webp", collisionShape: "circle", tooltip: "\u50cd\u304d\u30a2\u30ea\u304c\u305f\u308a\u306a\u3044\u3092\u63a2\u3057\u3001\u5de3\u3078\u904b\u3076\u3002" },
|
||||
ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, icon: "assets/ui/tool_ball.webp", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
|
||||
signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 30, amount: 999, icon: "assets/ui/tool_signboard.webp", collisionShape: "circle", tooltip: "\u8a2d\u7f6e\u5f8c\u306b\u89b3\u5bdf\u30c4\u30fc\u30eb\u3067\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u6587\u5b57\u3092\u66f8\u304d\u8fbc\u3081\u308b\u770b\u677f\u30026文字×3行まで\u3002" },
|
||||
duplicator: { id: "duplicator", itemType: "duplicator", label: "\u8907\u88fd\u6a5f", placeable: true, scalable: false, radius: 34, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "食べ物や薬をセットすると、自動供給してくれる。" },
|
||||
ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
|
||||
signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 30, amount: 999, icon: "assets/ui/tool_signboard.webp", collisionShape: "circle", tooltip: "\u8a2d\u7f6e\u5f8c\u306b\u89b3\u5bdf\u30c4\u30fc\u30eb\u3067\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u6587\u5b57\u3092\u66f8\u304d\u8fbc\u3081\u308b\u770b\u677f\u30026\u6587\u5b57\u00d73\u884c\u307e\u3067\u3002" },
|
||||
duplicator: { id: "duplicator", itemType: "duplicator", label: "\u8907\u88fd\u6a5f", placeable: true, scalable: false, radius: 34, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "\u98df\u3079\u7269\u3084\u85ac\u3092\u30bb\u30c3\u30c8\u3059\u308b\u3068\u3001\u81ea\u52d5\u4f9b\u7d66\u3057\u3066\u304f\u308c\u308b\u3002" },
|
||||
firecracker: { id: "firecracker", itemType: "firecracker", label: "\u7206\u7af9", placeable: true, scalable: true, radius: 15, amount: 999, icon: "assets/ui/tool_firecracker.webp", tooltip: "\u7206\u767a\u3092\u8d77\u3053\u3059\u3002" },
|
||||
pushpin: { id: "pushpin", itemType: "pushpin", label: "\u753b\u92f2", placeable: true, scalable: false, radius: 8, amount: 999, icon: "assets/ui/tool_pushpin.webp", tooltip: "\u843d\u3068\u3059\u3068\u3053\u308d\u304c\u308a\u3001\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\u3068\u30d1\u30cb\u30c3\u30af\u3068\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002" },
|
||||
oshibyo: { id: "oshibyo", itemType: "oshibyo", label: "\u304a\u3057\u308a\u92f2", placeable: true, scalable: false, radius: 9, amount: 999, icon: "assets/ui/tool_oshibyo.webp", tooltip: "\u523a\u3055\u308b\u3068\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u308b\u3002\u3064\u307e\u3093\u3067\u629c\u304f\u3068\u6e9c\u307e\u3063\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3076\u3002" },
|
||||
|
|
@ -133,11 +134,11 @@ function pinBehaviorFor(type = "") {
|
|||
|
||||
|
||||
const TOOL_CATEGORIES = Object.freeze([
|
||||
{ id: "operate", label: "\u64cd\u4f5c\u30fb\u89b3\u5bdf", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "delete", "poke", "pinch", "new", "water_hose"] },
|
||||
{ id: "food", label: "\u98df\u3079\u7269\u30fb\u6c34\u30fb\u8349", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water_bowl", "zunda_juice"] },
|
||||
{ id: "medicine", label: "\u85ac\u30fb\u500b\u4f53\u52b9\u679c", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"] },
|
||||
{ id: "habitat", label: "\u751f\u6d3b\u30fb\u751f\u304d\u7269", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "bed", "nest_box", "ant_nest", "ball", "signboard", "duplicator"] },
|
||||
{ id: "hazard", label: "\u5371\u967a\u30fb\u969c\u5bb3\u7269", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "oshibyo", "fence_v", "fence_h"] },
|
||||
{ id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "delete", "poke", "pinch", "new", "water_hose"] },
|
||||
{ id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water_bowl", "zunda_juice", "duplicator"] },
|
||||
{ id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"] },
|
||||
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "bed", "nest_box", "ball", "signboard", "fence_v", "fence_h"] },
|
||||
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "oshibyo", "ant_nest"] },
|
||||
]);
|
||||
|
||||
function toolCategories() {
|
||||
|
|
@ -171,7 +172,7 @@ function itemAmountFor(type = "", fallback = 80) {
|
|||
}
|
||||
|
||||
const TOOL_SIZE_ORDER = Object.freeze(["small", "medium", "large"]);
|
||||
const TOOL_SIZE_LABELS = Object.freeze({ small: "小", medium: "中", large: "大" });
|
||||
const TOOL_SIZE_LABELS = Object.freeze({ small: "\u5c0f", medium: "\u4e2d", large: "\u5927" });
|
||||
const TOOL_SIZE_SCALES = Object.freeze({ small: 0.68, medium: 1.0, large: 1.48 });
|
||||
function normalizedToolSizeFor(worldRef = null, type = "") {
|
||||
const def = itemDefinition(type);
|
||||
|
|
@ -255,7 +256,7 @@ const CONFIG = {
|
|||
weatherChangeMin: 46,
|
||||
weatherChangeMax: 96,
|
||||
bubbleLimit: 32,
|
||||
grassLimit: 180,
|
||||
grassLimit: 360,
|
||||
highPopulationMode: 70,
|
||||
ultraPopulationMode: 120,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,13 +21,19 @@
|
|||
const w = window.world;
|
||||
const perf = window.TarinaiPerformance?.debugSnapshot(w) || {};
|
||||
const input = window.TarinaiInputMode?.snapshot?.() || {};
|
||||
const diag = w?.lastRuntimeDiagnostics || w?.runtimeDiagnostics?.() || {};
|
||||
const behaviorCounts = diag.behavior?.counts || {};
|
||||
const topBehaviors = Object.entries(behaviorCounts).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([id, n]) => `${id}:${n}`).join(", ");
|
||||
panel.textContent = [
|
||||
`tarinai v${window.TARINAI_VERSION || "?"}`,
|
||||
`fps ${perf.fps ?? window.__tarinaiFps ?? "?"} perf ${perf.level ?? "?"}`,
|
||||
`items ${(w?.items || []).length} tarinai ${(w?.tarinai || []).length} ants ${(w?.ants || []).length}`,
|
||||
`effects ${(w?.effects || []).length}`,
|
||||
`terrain v${w?.terrainVersion ?? 0} spatial v${w?.spatialVersion ?? 0}`,
|
||||
`drawList ${(w?.drawList || []).length} logs ${(w?.logs || []).length}`,
|
||||
`spatial rebuild ${diag.spatial?.rebuildsThisFrame ?? 0}/frame total:${diag.spatial?.rebuildsTotal ?? 0} dirty:${diag.spatial?.dirtyMarksThisFrame ?? 0} reason:${diag.spatial?.dirtyReason || diag.spatial?.lastRebuildReason || "-"}`,
|
||||
`drawList ${(w?.drawList || []).length} dirty:${Boolean(diag.render?.drawListDirty)} logs ${(w?.logs || []).length}`,
|
||||
`item buckets:${diag.items?.bucketTypes ?? 0} dirty:${Boolean(diag.items?.bucketsDirty)} id-map:${diag.items?.idMapSize ?? 0} rebuilds:${diag.items?.bucketRebuildsTotal ?? 0}`,
|
||||
`behavior forced:${diag.behavior?.forced ?? 0} locked:${diag.behavior?.locked ?? 0} top ${topBehaviors || "-"}`,
|
||||
`dpr ${uiCache?.canvasDpr || "?"} cache ${window.TARINAI_APP?.cacheName || "?"}`,
|
||||
`sw ${navigator.serviceWorker?.controller ? "controlled" : "uncontrolled"} soundPack ${window.TarinaiAudio?.soundPackVersion || "?"}`,
|
||||
`input ${input.currentMode || "?"} touchFirst:${Boolean(input.touchFirst)} coarse:${Boolean(input.pointerCoarse)} hover:${Boolean(input.hasHover)} compact:${Boolean(input.isCompactViewport)} uaMobile:${Boolean(input.userAgentMobile)}`,
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@
|
|||
const nutrition = Number(context.nutrition) || 0;
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { fulfill: -(context.source === "eating" ? nutrition * 0.5 : 12), social: -4 });
|
||||
if (context.source === "eating") {
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("approach_mate", { source: "love_mochi", priority: 150, ttl: 24, searchRange: 820, reasonText: "へこ餅で繁殖できる相手を探している" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "approach_mate", { source: "love_mochi", priority: 150, ttl: 24, searchRange: 820, reasonText: "へこ餅で繁殖できる相手を探している" });
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("approach_mate", { source: "love_mochi", priority: 150, ttl: 24, searchRange: 820, causeText: "\u3078\u3053\u9905\u306e\u52b9\u679c" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "approach_mate", { source: "love_mochi", priority: 150, ttl: 24, searchRange: 820, causeText: "\u3078\u3053\u9905\u306e\u52b9\u679c" });
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
|
@ -125,8 +125,8 @@
|
|||
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);
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("fight_rival", { source: "fight_mochi", priority: 180, ttl: 24, searchRange: 820, reasonText: "けんか餅で気に入らない相手を探している" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "fight_rival", { source: "fight_mochi", priority: 180, ttl: 24, searchRange: 820, reasonText: "けんか餅で気に入らない相手を探している" });
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("fight_rival", { source: "fight_mochi", priority: 180, ttl: 24, searchRange: 820, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "fight_rival", { source: "fight_mochi", priority: 180, ttl: 24, searchRange: 820, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
|
@ -141,8 +141,8 @@
|
|||
tarinai.fightMochiTimer = Math.max(0, (tarinai.fightMochiTimer || 0) - 2.5);
|
||||
const baseChance = Math.min(0.98, 0.35 + nutrition * 0.075);
|
||||
const sleepChance = tarinai.diseaseChance ? tarinai.diseaseChance(baseChance) : baseChance;
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("sleep_anywhere", { source: "sleep_drug", priority: 145, ttl: 14, duration: 8, minDuration: 8, reasonText: "ねむり薬で眠ろうとしている" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "sleep_anywhere", { source: "sleep_drug", priority: 145, ttl: 14, duration: 8, minDuration: 8, reasonText: "ねむり薬で眠ろうとしている" });
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("sleep_anywhere", { source: "sleep_drug", priority: 145, ttl: 14, duration: 8, minDuration: 8, causeText: "\u306d\u3080\u308a\u85ac\u306e\u52b9\u679c" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "sleep_anywhere", { source: "sleep_drug", priority: 145, ttl: 14, duration: 8, minDuration: 8, causeText: "\u306d\u3080\u308a\u85ac\u306e\u52b9\u679c" });
|
||||
if (!tarinai.sleepDisease && Math.random() < Math.min(0.98, sleepChance)) return tarinai.infectSleepDisease?.(context.item || null) || true;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@
|
|||
.frozen-panel { grid-column: 1 / -1; margin-top: 8px; padding: 7px 8px; border: 1px solid rgba(82, 63, 43, 0.18); border-radius: 14px; background: rgba(244, 249, 255, 0.72); min-width: 0; }
|
||||
.frozen-panel-title { display:flex; align-items:center; justify-content:space-between; gap:8px; font-weight:800; font-size:12px; color:#4f5f70; margin-bottom:5px; }
|
||||
.frozen-panel-toggle { width:100%; border:0; background:transparent; color:inherit; font:inherit; display:flex; align-items:center; justify-content:space-between; gap:8px; padding:0; cursor:pointer; text-align:left; }
|
||||
.frozen-panel-toggle::after { content:"折り畳み"; font-size:11px; opacity:.62; font-weight:700; }
|
||||
.frozen-panel.collapsed .frozen-panel-toggle::after { content:"展開"; }
|
||||
.frozen-panel-toggle::after { content:"\u6298\u308a\u7573\u307f"; font-size:11px; opacity:.62; font-weight:700; }
|
||||
.frozen-panel.collapsed .frozen-panel-toggle::after { content:"\u5c55\u958b"; }
|
||||
.frozen-panel-count { font-size:12px; opacity:.72; }
|
||||
.frozen-list { display:flex; gap:6px; overflow-x:auto; padding-bottom:2px; }
|
||||
.frozen-panel.collapsed .frozen-list { display:none; }
|
||||
|
|
@ -162,8 +162,8 @@
|
|||
worldRef.rebuildSpatial?.(true);
|
||||
saveFrozenStore(worldRef);
|
||||
renderFrozenPanel(worldRef);
|
||||
global.showToast?.(`${t.name}を冷凍保存しました。`);
|
||||
worldRef.log?.(`${t.name}をフィールド外で冷凍保存した。`, "observe", { participants: [t] });
|
||||
global.showToast?.(`${t.name}\u3092\u51b7\u51cd\u4fdd\u5b58\u3057\u307e\u3057\u305f\u3002`);
|
||||
worldRef.log?.(`${t.name}\u3092\u30d5\u30a3\u30fc\u30eb\u30c9\u5916\u3067\u51b7\u51cd\u4fdd\u5b58\u3057\u305f\u3002`, "observe", { participants: [t] });
|
||||
global.renderStats?.();
|
||||
global.render?.();
|
||||
return true;
|
||||
|
|
@ -194,18 +194,18 @@
|
|||
seen.add(key);
|
||||
unique.push(loss);
|
||||
}
|
||||
const relPriority = unique.some(l => l.relation === "親" || l.relation === "子") ? 1 : 0;
|
||||
const relPriority = unique.some(l => l.relation === "\u89aa" || l.relation === "\u5b50") ? 1 : 0;
|
||||
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" });
|
||||
if (t.enterPanic) t.enterPanic({ reason: `${unique[0]?.name || "\u8ab0\u304b"}\u304c\u3044\u306a\u3044\u3053\u3068\u306b\u6c17\u3065\u3044\u305f`, fear: 2.4 + Math.min(2.0, unique.length * 0.35), stress, cause: "pending_relation_death" });
|
||||
else {
|
||||
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 });
|
||||
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination() : null, reason: `${unique[0]?.name || "\u8ab0\u304b"}\u304c\u3044\u306a\u3044\u3053\u3068\u306b\u6c17\u3065\u3044\u305f`, wake: true });
|
||||
}
|
||||
t.bubble?.("いない", 1.1, "rgba(60,50,68,0.82)");
|
||||
t.recordChangeCause?.("冷凍中の\u8a03\u5831", "ストレス", { value: stress });
|
||||
t.addRecord?.(`冷凍中に${unique.map(l => l.name || "誰か").slice(0, 3).join("、")}が死亡していたことに気づいた。`, "death");
|
||||
worldRef.log?.(`${t.name}は解凍時に身近な個体の死に気づき、パニックになった。`, "death", { participants: [t] });
|
||||
t.bubble?.("\u3044\u306a\u3044", 1.1, "rgba(60,50,68,0.82)");
|
||||
t.recordChangeCause?.("\u51b7\u51cd\u4e2d\u306e\u8a03\u5831", "\u30b9\u30c8\u30ec\u30b9", { value: stress });
|
||||
t.addRecord?.(`\u51b7\u51cd\u4e2d\u306b${unique.map(l => l.name || "\u8ab0\u304b").slice(0, 3).join("\u3001")}\u304c\u6b7b\u4ea1\u3057\u3066\u3044\u305f\u3053\u3068\u306b\u6c17\u3065\u3044\u305f\u3002`, "death");
|
||||
worldRef.log?.(`${t.name}\u306f\u89e3\u51cd\u6642\u306b\u8eab\u8fd1\u306a\u500b\u4f53\u306e\u6b7b\u306b\u6c17\u3065\u304d\u3001\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u305f\u3002`, "death", { participants: [t] });
|
||||
}
|
||||
|
||||
function thawFrozen(freezeId, x, y, worldRef = global.world) {
|
||||
|
|
@ -230,9 +230,9 @@
|
|||
t.panicTarget = null;
|
||||
t.insideNestBoxId = "";
|
||||
t.sleeping = false;
|
||||
if (t.state === "sleep" || t.state === "seek_bed") t.goIdle?.("解凍されて少し驚いている");
|
||||
if (t.state === "sleep" || t.state === "seek_bed") t.goIdle?.("\u89e3\u51cd\u3055\u308c\u3066\u5c11\u3057\u9a5a\u3044\u3066\u3044\u308b");
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.35);
|
||||
t.thought = "解凍されて少し驚いている";
|
||||
t.thought = "\u89e3\u51cd\u3055\u308c\u3066\u5c11\u3057\u9a5a\u3044\u3066\u3044\u308b";
|
||||
t.frozenAt = 0;
|
||||
updateLiveIdAfterThaw(worldRef, t);
|
||||
if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize();
|
||||
|
|
@ -247,8 +247,8 @@
|
|||
worldRef.rebuildSpatial?.(true);
|
||||
saveFrozenStore(worldRef);
|
||||
renderFrozenPanel(worldRef);
|
||||
global.showToast?.(`${t.name}を解凍しました。`);
|
||||
worldRef.log?.(`${t.name}を解凍してフィールドに戻した。`, "birth", { participants: [t] });
|
||||
global.showToast?.(`${t.name}\u3092\u89e3\u51cd\u3057\u307e\u3057\u305f\u3002`);
|
||||
worldRef.log?.(`${t.name}\u3092\u89e3\u51cd\u3057\u3066\u30d5\u30a3\u30fc\u30eb\u30c9\u306b\u623b\u3057\u305f\u3002`, "birth", { participants: [t] });
|
||||
global.renderStats?.();
|
||||
global.render?.();
|
||||
return true;
|
||||
|
|
@ -284,7 +284,7 @@
|
|||
if (!card) return;
|
||||
pendingThawId = card.dataset.frozenId || "";
|
||||
for (const c of panel.querySelectorAll(".frozen-card")) c.classList.toggle("pending-thaw", c === card);
|
||||
global.showToast?.("フィールドをクリックすると解凍します。ドラッグ&ドロップでも配置できます。");
|
||||
global.showToast?.("\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u89e3\u51cd\u3057\u307e\u3059\u3002\u30c9\u30e9\u30c3\u30b0&\u30c9\u30ed\u30c3\u30d7\u3067\u3082\u914d\u7f6e\u3067\u304d\u307e\u3059\u3002");
|
||||
});
|
||||
}
|
||||
const canvas = global.canvas || document.getElementById("gameCanvas");
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ 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);
|
||||
const plushie = (t.world?.itemsOfType?.("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);
|
||||
|
|
|
|||
85
js/items.js
85
js/items.js
|
|
@ -210,7 +210,7 @@ function drawConsumableFieldSprite(ctx, type = "", r = 10, seed = 1, opts = {})
|
|||
ctx.font = `${Math.round(localR * 1.15)}px sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("❤", 0, 1);
|
||||
ctx.fillText("\u2764", 0, 1);
|
||||
ctx.fillStyle = "rgba(255,255,255,0.55)";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-localR * 0.28, -localR * 0.18, localR * 0.23, localR * 0.10, -0.35, 0, Math.PI * 2);
|
||||
|
|
@ -356,7 +356,7 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
|
|||
wear: 0,
|
||||
text: type === "signboard" ? "" : undefined,
|
||||
storedFoodType: type === "duplicator" ? "sweet" : "",
|
||||
storedFoodLabel: type === "duplicator" ? "ずんだ餅" : "",
|
||||
storedFoodLabel: type === "duplicator" ? "\u305a\u3093\u3060\u9905" : "",
|
||||
pinState: "loose",
|
||||
spin: type === "pushpin" || type === "oshibyo" ? -0.55 : 0,
|
||||
zunchiVariant: "zunchi",
|
||||
|
|
@ -467,10 +467,12 @@ class Item {
|
|||
this.lastPokeAngle = rand(0, Math.PI * 2);
|
||||
}
|
||||
if (type === "zunchi") {
|
||||
this.amount = Math.max(Number(this.amount || 0) || 0, 240);
|
||||
this.stage = "fresh";
|
||||
this.stageTimer = 0;
|
||||
this.fertility = 0;
|
||||
this.freshness = 1;
|
||||
this.spawnGrace = 1.2;
|
||||
this.zunchiVariant = stableUnit(this.id, "zunchi-variant") < 0.5 ? "zunchi" : "zunchi_02";
|
||||
}
|
||||
if (type === "ant_nest") {
|
||||
|
|
@ -528,6 +530,7 @@ class Item {
|
|||
}
|
||||
update(dt, worldRef = world) {
|
||||
this.age += dt;
|
||||
if (this.type === "zunchi") this.spawnGrace = Math.max(0, (this.spawnGrace || 0) - dt);
|
||||
if (this.dropTimer > 0) {
|
||||
const beforeDrop = this.dropTimer;
|
||||
this.dropTimer = Math.max(0, this.dropTimer - dt);
|
||||
|
|
@ -687,11 +690,11 @@ class Item {
|
|||
t.fallTimer = Math.max(t.fallTimer || 0, 0.85);
|
||||
t.fallMax = Math.max(t.fallMax || 0.85, t.fallTimer);
|
||||
t.fallDir = nx < 0 ? -1 : 1;
|
||||
if (t.enterPanic) t.enterPanic({ threat: this, reason: "高速ずんちにぶつかって吹き飛んでいる", fear: 0.55, stress: 7, stressDuration: 3.2, surpriseTimer: 0.5, cause: "fast_zunchi_hit" });
|
||||
if (t.enterPanic) t.enterPanic({ threat: this, reason: "\u9ad8\u901f\u305a\u3093\u3061\u306b\u3076\u3064\u304b\u3063\u3066\u5439\u304d\u98db\u3093\u3067\u3044\u308b", fear: 0.55, stress: 7, stressDuration: 3.2, surpriseTimer: 0.5, cause: "fast_zunchi_hit" });
|
||||
else {
|
||||
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.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(this) : null, reason: "\u9ad8\u901f\u305a\u3093\u3061\u306b\u3076\u3064\u304b\u3063\u3066\u5439\u304d\u98db\u3093\u3067\u3044\u308b", wake: true });
|
||||
if (t.addStress) t.addStress(7, { threshold: 8, duration: 3.2 });
|
||||
}
|
||||
worldRef.spawnFallEffect?.(t.x, t.y + (t.radius || 20) * 0.45, 0.65);
|
||||
|
|
@ -924,11 +927,14 @@ class Item {
|
|||
tryStickPushpin(worldRef) {
|
||||
if (!worldRef?.tarinai?.length) return false;
|
||||
if (this.pinState === "lodged") return false;
|
||||
const now = worldRef.time || 0;
|
||||
if ((this.noStickUntil || 0) > now) return false;
|
||||
let best = null;
|
||||
let bestD = Infinity;
|
||||
const hitRange = Math.max(10, (this.r || 8) * 1.25);
|
||||
for (const t of worldRef.tarinai) {
|
||||
if (!t || t.dead) continue;
|
||||
if (this.noStickTargetId && this.noStickTargetId === t.id && (this.noStickTargetUntil || 0) > now) continue;
|
||||
const d = distXY(this.x, this.y, t.x, t.y);
|
||||
const hit = (t.radius || 16) * 0.86 + hitRange;
|
||||
if (d <= hit && d < bestD) { best = t; bestD = d; }
|
||||
|
|
@ -948,7 +954,10 @@ class Item {
|
|||
this.pinState = "lodged";
|
||||
this.deletable = false;
|
||||
this.pinTargetId = t.id;
|
||||
this.pinAttachAngle = isOshibyo ? 0.36 : Math.atan2(dy || -1, dx || (t.facingDir ? t.facingDir() : 1));
|
||||
const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1);
|
||||
const visualSide = faceDir;
|
||||
this.pinVisualSide = visualSide;
|
||||
this.pinAttachAngle = isOshibyo ? visualSide * 0.36 : Math.atan2(dy || -1, dx || visualSide);
|
||||
this.pinAttachDistance = isOshibyo ? (t.radius || 16) * 0.72 : clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74);
|
||||
this.pinOffsetY = isOshibyo ? (t.radius || 16) * 0.40 : clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38);
|
||||
this.pinDamageTick = 0;
|
||||
|
|
@ -962,23 +971,23 @@ class Item {
|
|||
t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 7);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, isOshibyo ? 0.35 : 0.8);
|
||||
if (isOshibyo) {
|
||||
t.thought = "おしり鋲が刺さってずんちが出なくなった";
|
||||
worldRef.log?.(`${t.name}におしり鋲が刺さった。`, "accident", { participants: [t] });
|
||||
t.thought = "\u304a\u3057\u308a\u92f2\u304c\u523a\u3055\u3063\u3066\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u3063\u305f";
|
||||
worldRef.log?.(`${t.name}\u306b\u304a\u3057\u308a\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] });
|
||||
worldRef.effects?.push(new Effect("ring", t.x, t.y + (t.radius || 16) * 0.34, { size: Math.max(14, (t.radius || 16) * 0.58), life: 0.18, color: "rgba(73,119,205,0.36)" }));
|
||||
return true;
|
||||
}
|
||||
if (t.enterPanic) {
|
||||
t.enterPanic({ threat: this, reason: "画鋲が刺さってパニックになっている", fear: 1.3, stress: behavior?.stressOnAttach ?? 18, hurtTimer: 1.2, awakeLockTimer: 7, surpriseTimer: 0.8, cause: "pushpin_attach" });
|
||||
t.enterPanic({ threat: this, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 1.3, stress: behavior?.stressOnAttach ?? 18, hurtTimer: 1.2, awakeLockTimer: 7, surpriseTimer: 0.8, cause: "pushpin_attach" });
|
||||
} else {
|
||||
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.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(this) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) }, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true });
|
||||
if (t.addStress) t.addStress(behavior?.stressOnAttach ?? 18, { threshold: 8 });
|
||||
}
|
||||
if (t.damage && (behavior?.damageOnAttach ?? 6) > 0) t.damage(behavior?.damageOnAttach ?? 6, "画鋲");
|
||||
if (t.damage && (behavior?.damageOnAttach ?? 6) > 0) t.damage(behavior?.damageOnAttach ?? 6, "\u753b\u92f2");
|
||||
if ((worldRef.time || 0) >= (this.pinLogAt || -999) + 1.2) {
|
||||
this.pinLogAt = worldRef.time || 0;
|
||||
worldRef.log?.(`${t.name}に画鋲が刺さった。`, "accident", { participants: [t] });
|
||||
worldRef.log?.(`${t.name}\u306b\u753b\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] });
|
||||
}
|
||||
worldRef.effects?.push(new Effect("ring", t.x, t.y - (t.radius || 16) * 0.12, { size: Math.max(18, (t.radius || 16) * 0.90), life: 0.22, color: "rgba(214,72,72,0.50)" }));
|
||||
return true;
|
||||
|
|
@ -994,29 +1003,32 @@ class Item {
|
|||
const isOshibyo = Boolean(behavior?.blocksZunchi);
|
||||
t.stuckPushpinId = this.id;
|
||||
this.deletable = false;
|
||||
this.x = isOshibyo ? t.x + (t.radius || 16) * 0.66 : t.x + (t.radius || 16) * 0.36;
|
||||
const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1);
|
||||
const visualSide = faceDir;
|
||||
this.pinVisualSide = visualSide;
|
||||
this.x = isOshibyo ? t.x + visualSide * (t.radius || 16) * 0.72 : t.x + visualSide * (t.radius || 16) * 0.36;
|
||||
this.y = isOshibyo ? t.y + (t.radius || 16) * 0.42 : t.y - (t.radius || 16) * 0.04;
|
||||
this.prevX = this.x;
|
||||
this.prevY = this.y;
|
||||
this.spin = isOshibyo ? 0.52 : 0.28;
|
||||
this.spin = visualSide * (isOshibyo ? 0.52 : 0.28);
|
||||
this.vx = t.vx || 0;
|
||||
this.vy = t.vy || 0;
|
||||
if (isOshibyo) {
|
||||
if ((t.oshiriByoZunchiStock || 0) >= 6 && t.state !== "eat" && t.state !== "sleep") t.thought = "おしり鋲でずんちが溜まってつらい";
|
||||
if ((t.oshiriByoZunchiStock || 0) >= 6 && t.state !== "eat" && t.state !== "sleep") t.thought = "\u304a\u3057\u308a\u92f2\u3067\u305a\u3093\u3061\u304c\u6e9c\u307e\u3063\u3066\u3064\u3089\u3044";
|
||||
return;
|
||||
}
|
||||
this.pinDamageTick = (this.pinDamageTick || 0) + dt;
|
||||
while (this.pinDamageTick >= 0.55) {
|
||||
this.pinDamageTick -= 0.55;
|
||||
if (t.damage && (behavior?.damagePerTick ?? 1.4) > 0) t.damage(behavior?.damagePerTick ?? 1.4, "画鋲");
|
||||
if (t.damage && (behavior?.damagePerTick ?? 1.4) > 0) t.damage(behavior?.damagePerTick ?? 1.4, "\u753b\u92f2");
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.50);
|
||||
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;
|
||||
if (t.enterPanic) t.enterPanic({ threat: this, reason: "画鋲が刺さってパニックになっている", fear: 0.65, surpriseTimer: 0.22, awakeLockTimer: 2.4, cause: "pushpin_lodged" });
|
||||
if (t.enterPanic) t.enterPanic({ threat: this, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.65, surpriseTimer: 0.22, awakeLockTimer: 2.4, cause: "pushpin_lodged" });
|
||||
else {
|
||||
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.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(this) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) }, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true });
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 0.65);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.22);
|
||||
t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 2.4);
|
||||
|
|
@ -1038,6 +1050,16 @@ class Item {
|
|||
const storedZunchi = wasOshibyo && t ? Math.max(0, Math.floor(t.oshiriByoZunchiStock || 0)) : 0;
|
||||
if (t && t.stuckPushpinId === this.id) t.stuckPushpinId = null;
|
||||
if (t && wasOshibyo) t.oshiriByoZunchiStock = 0;
|
||||
if (reason === "pinch") {
|
||||
const now = worldRef?.time || 0;
|
||||
this.noStickUntil = now + 1.0;
|
||||
this.noStickTargetId = t?.id || "";
|
||||
this.noStickTargetUntil = now + 1.0;
|
||||
} else if (t) {
|
||||
const now = worldRef?.time || 0;
|
||||
this.noStickTargetId = t.id || "";
|
||||
this.noStickTargetUntil = now + 0.55;
|
||||
}
|
||||
this.pinState = "loose";
|
||||
this.pinTargetId = "";
|
||||
this.pinDamageTick = 0;
|
||||
|
|
@ -1104,14 +1126,14 @@ class Item {
|
|||
this.fertilityBoost = clamp(fertility, 0, 1.4);
|
||||
const weatherBoost = weather === "sunny" ? 0.09 : weather === "light_rain" ? 0.42 : 0.08;
|
||||
const dryStress = weather === "sunny" && light > 0.78 && waterNear < 0.04 && this.fertilityBoost < 0.12 ? 0.012 : 0;
|
||||
const growthRate = (0.006 + light * 0.004 + weatherBoost * 0.01 + waterNear * 0.014 + this.fertilityBoost * 0.014) * dt * 0.5;
|
||||
const growthRate = (0.006 + light * 0.004 + weatherBoost * 0.01 + waterNear * 0.014 + this.fertilityBoost * 0.014) * dt * (2 / 3);
|
||||
this.growth = clamp((this.growth ?? 0.25) + growthRate - dryStress * dt, 0, 0.88);
|
||||
this.health = clamp((this.health ?? 1) + dt * (waterNear > 0.05 || weather === "light_rain" ? 0.017 : -0.0015 - dryStress * 0.28), 0.18, 1);
|
||||
const grassLimit = worldRef.grassLimit ? worldRef.grassLimit() : (CONFIG.grassLimit ?? 96);
|
||||
if (this.growth >= 0.86 && (worldRef.itemCounts?.grass || 0) < grassLimit) {
|
||||
const spawned = this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 2);
|
||||
if (spawned > 0) this.growth = rand(0.52, 0.68);
|
||||
this.seedTimer = rand(18, 34);
|
||||
this.seedTimer = rand(9, 17);
|
||||
}
|
||||
if (this.age > (this.lifeSpan || 640) * 0.82) {
|
||||
const fade = clamp((this.age - (this.lifeSpan || 640) * 0.82) / Math.max((this.lifeSpan || 640) * 0.18, 1), 0, 1);
|
||||
|
|
@ -1123,10 +1145,10 @@ class Item {
|
|||
}
|
||||
this.amount = clamp(this.growth * 120 * this.health, 0, 130);
|
||||
if (this.age > (this.lifeSpan || 640) * 1.05) this.amount -= dt * (1.2 + (this.wither || 0) * 24);
|
||||
this.seedTimer -= dt * (this.growth > 0.72 ? 1 : 0.25) * 0.5;
|
||||
this.seedTimer -= dt * (this.growth > 0.72 ? 1 : 0.25) * (2 / 3);
|
||||
if (this.seedTimer <= 0 && this.growth > 0.64 && (worldRef.itemCounts?.grass || 0) < grassLimit) {
|
||||
this.seedTimer = rand(24, 54);
|
||||
if (Math.random() < 0.42 + this.fertilityBoost * 0.18) this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 1);
|
||||
this.seedTimer = rand(12, 27);
|
||||
if (Math.random() < 0.62 + this.fertilityBoost * 0.22) this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 1);
|
||||
}
|
||||
}
|
||||
seedAround(worldRef, fertileSource = null, fertile = false, maxNew = 1) {
|
||||
|
|
@ -1141,8 +1163,7 @@ class Item {
|
|||
const sprout = new Item("grass", spot.x, spot.y);
|
||||
sprout.growth = rand(0.08, 0.18);
|
||||
sprout.amount = sprout.growth * 120;
|
||||
worldRef.items.push(sprout);
|
||||
if (worldRef.itemCounts) worldRef.itemCounts.grass = (worldRef.itemCounts.grass || 0) + 1;
|
||||
worldRef.addItem?.(sprout, "grass-seed") || worldRef.items.push(sprout);
|
||||
made += 1;
|
||||
}
|
||||
return made;
|
||||
|
|
@ -1159,21 +1180,22 @@ class Item {
|
|||
waterBoost += clamp(1 - distXY(this.x, this.y, it.x, it.y) / 80, 0, 1);
|
||||
}
|
||||
this.stageTimer += dt * 2.0 * (rainBoost + waterBoost * 1.3);
|
||||
const decayDt = (this.spawnGrace || 0) > 0 ? 0 : dt;
|
||||
if (this.stage === "fresh") {
|
||||
this.freshness = clamp(1 - this.stageTimer / 110, 0, 1);
|
||||
this.amount -= dt * 0.05 * decayBoost;
|
||||
this.amount -= decayDt * 0.05 * decayBoost;
|
||||
if (this.stageTimer > 110) { this.stage = "dry"; this.stageTimer = 0; }
|
||||
} else if (this.stage === "dry") {
|
||||
this.freshness = clamp(0.55 - this.stageTimer / 220, 0.20, 0.55);
|
||||
this.amount -= dt * 0.09 * decayBoost;
|
||||
this.amount -= decayDt * 0.09 * decayBoost;
|
||||
if (this.stageTimer > 150) { this.stage = "decomposing"; this.stageTimer = 0; this.fertility = 0.35; }
|
||||
} else if (this.stage === "decomposing") {
|
||||
this.fertility = clamp(this.fertility + dt * 0.015, 0, 1);
|
||||
this.amount -= dt * 0.12 * decayBoost;
|
||||
this.amount -= decayDt * 0.12 * decayBoost;
|
||||
if (this.stageTimer > 190) { this.stage = "fertile_soil"; this.stageTimer = 0; this.amount = Math.min(this.amount, 140); }
|
||||
} else if (this.stage === "fertile_soil") {
|
||||
this.fertility = clamp(1 - this.stageTimer / 360, 0, 1);
|
||||
this.amount -= dt * 0.26 * decayBoost;
|
||||
this.amount -= decayDt * 0.26 * decayBoost;
|
||||
}
|
||||
}
|
||||
get dead() {
|
||||
|
|
@ -1185,6 +1207,10 @@ class Item {
|
|||
const warm = lightState.warmth;
|
||||
const styleNight = lightState.light < 0.42;
|
||||
const styleWarm = !styleNight && (lightState.goldenStrength > 0.22 || warm > 0.55);
|
||||
if (isPinType(this.type) && this.pinState === "lodged") {
|
||||
const owner = world?.tarinai?.find?.(o => o && !o.dead && o.id === this.pinTargetId) || null;
|
||||
if (owner?.insideNestBoxId) return;
|
||||
}
|
||||
const servingRatio = isServingFoodType(this.type) ? ((this.foodServingsRemaining ?? this.amount) / Math.max(1, this.foodServingsMax || foodServingsForSize(this.toolSize || "medium"))) : null;
|
||||
const visibleAlpha = isServingFoodType(this.type) ? clamp(servingRatio ?? 1, 0.24, 1) : clamp(this.amount / 40, 0.24, 1);
|
||||
const dropT = this.dropMax > 0 ? clamp(this.dropTimer / this.dropMax, 0, 1) : 0;
|
||||
|
|
@ -1274,6 +1300,7 @@ class Item {
|
|||
const assetId = behavior ? (stuck ? behavior.lodgedAsset : behavior.looseAsset) : (stuck ? "pushpin_stuck" : "pushpin");
|
||||
const img = typeof getRenderableImage === "function" ? getRenderableImage(assetId, assetId) : images.get(assetId);
|
||||
ctx.save();
|
||||
if (stuck) ctx.scale((this.pinVisualSide || 1) < 0 ? -1 : 1, 1);
|
||||
if (!stuck) ctx.rotate((this.spin || 0) + Math.PI * 0.5);
|
||||
ctx.shadowColor = stuck ? "rgba(116,24,32,0.18)" : "rgba(54,42,31,0.16)";
|
||||
ctx.shadowBlur = 3;
|
||||
|
|
@ -1546,7 +1573,7 @@ class Item {
|
|||
ctx.font = `bold ${Math.max(14, Math.round(this.r * 0.44))}px ui-rounded, sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("…", 0, -this.r * 0.58);
|
||||
ctx.fillText("\u2026", 0, -this.r * 0.58);
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "ant_nest") {
|
||||
|
|
|
|||
|
|
@ -10,31 +10,35 @@
|
|||
const observerFamily = observer.familyKey || observer.archiveKey || observer.id || "";
|
||||
const deadId = dead.id || dead.releasedLiveId || "";
|
||||
const observerId = observer.id || observer.liveId || "";
|
||||
if (deadFamily && (observer.parents || []).includes(deadFamily)) return "親";
|
||||
if (deadFamily && (observer.children || []).includes(deadFamily)) return "子";
|
||||
if (observerFamily && (dead.parents || []).includes(observerFamily)) return "子";
|
||||
if (observerFamily && (dead.children || []).includes(observerFamily)) return "親";
|
||||
if (deadFamily && (observer.parents || []).includes(deadFamily)) return "\u89aa";
|
||||
if (deadFamily && (observer.children || []).includes(deadFamily)) return "\u5b50";
|
||||
if (observerFamily && (dead.parents || []).includes(observerFamily)) return "\u5b50";
|
||||
if (observerFamily && (dead.children || []).includes(observerFamily)) return "\u89aa";
|
||||
const rel = deadId ? observer.relationships?.[deadId] : null;
|
||||
const reverse = observerId ? dead.relationships?.[observerId] : null;
|
||||
if ((rel?.affinity || 0) >= (global.FRIEND_AFFINITY_THRESHOLD || 14) || (reverse?.affinity || 0) >= (global.FRIEND_AFFINITY_THRESHOLD || 14)) return "友達";
|
||||
if ((rel?.affinity || 0) >= (global.FRIEND_AFFINITY_THRESHOLD || 14) || (reverse?.affinity || 0) >= (global.FRIEND_AFFINITY_THRESHOLD || 14)) return "\u53cb\u9054";
|
||||
return "";
|
||||
}
|
||||
|
||||
function applyLiveDeathShock(observer, dead, relation, worldRef) {
|
||||
const strong = relation === "親" || relation === "子";
|
||||
if (observer.state === "sleep" || observer.state === "seek_bed") observer.goIdle?.("身近な個体の死に気づいた");
|
||||
const strong = relation === "\u89aa" || relation === "\u5b50";
|
||||
if (observer.state === "sleep" || observer.state === "seek_bed") observer.goIdle?.("\u8eab\u8fd1\u306a\u500b\u4f53\u306e\u6b7b\u306b\u6c17\u3065\u3044\u305f");
|
||||
const stress = strong ? 18 : 11;
|
||||
const fear = strong ? 2.1 : 1.45;
|
||||
if (observer.enterPanic) observer.enterPanic({ threat: dead, reason: `${relation}が死んだ`, fear, stress, cause: "relation_death", bubble: "!!", bubbleColor: "rgba(60,50,68,0.82)" });
|
||||
if (observer.enterPanic) {
|
||||
const relLabel = relation || "\u8eab\u8fd1\u306a\u500b\u4f53";
|
||||
observer.lastPanicDetail = `${dead.name || "\u8eab\u8fd1\u306a\u500b\u4f53"}\uff08${relLabel}\uff09\u306e\u6b7b\u306b\u6c17\u4ed8\u3044\u305f`;
|
||||
observer.enterPanic({ threat: dead, reason: `${dead.name || "\u8eab\u8fd1\u306a\u500b\u4f53"}\uff08${relLabel}\uff09\u306e\u6b7b\u306b\u6c17\u4ed8\u3044\u3066\u3001\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b\u3002`, fear, stress, cause: "relation_death", bubble: "!!", bubbleColor: "rgba(60,50,68,0.82)" });
|
||||
}
|
||||
else {
|
||||
observer.sleeping = false;
|
||||
observer.setActionState?.("panic", { target: observer.panicDestination ? observer.panicDestination(dead) : null, reason: `${relation}が死んだ`, wake: true });
|
||||
observer.setActionState?.("panic", { target: observer.panicDestination ? observer.panicDestination(dead) : null, reason: `${dead.name || "\u8eab\u8fd1\u306a\u500b\u4f53"}\uff08${relation || "\u8eab\u8fd1\u306a\u500b\u4f53"}\uff09\u306e\u6b7b\u306b\u6c17\u4ed8\u3044\u3066\u3001\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b\u3002`, wake: true });
|
||||
observer.fearTimer = Math.max(observer.fearTimer || 0, fear);
|
||||
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 });
|
||||
observer.addRecord?.(`${dead.name || "身近な個体"}の死に気づいてパニックになった。`, "death");
|
||||
observer.recordChangeCause?.(`${relation}\u306e\u6b7b\u4ea1`, "\u30b9\u30c8\u30ec\u30b9", { value: stress });
|
||||
observer.addRecord?.(`${dead.name || "\u8eab\u8fd1\u306a\u500b\u4f53"}\u306e\u6b7b\u306b\u6c17\u3065\u3044\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u305f\u3002`, "death");
|
||||
}
|
||||
|
||||
function notifyDeathToRelations(dead, reason = "") {
|
||||
|
|
@ -59,16 +63,16 @@
|
|||
if (!duplicate) {
|
||||
entry.pendingLosses.unshift({
|
||||
relation,
|
||||
name: dead.name || "たりない",
|
||||
name: dead.name || "\u305f\u308a\u306a\u3044",
|
||||
familyKey: deadFamily,
|
||||
liveId: deadId,
|
||||
reason: reason || dead.deathReason || "死亡",
|
||||
reason: reason || dead.deathReason || "\u6b7b\u4ea1",
|
||||
time: worldRef.time || 0,
|
||||
});
|
||||
if (entry.pendingLosses.length > MAX_PENDING_LOSSES) entry.pendingLosses.length = MAX_PENDING_LOSSES;
|
||||
}
|
||||
}
|
||||
if (liveHits) worldRef.log?.(`${dead.name || "たりない"}の死に、身近な個体が動揺した。`, "death", { participants: [dead] });
|
||||
if (liveHits) worldRef.log?.(`${dead.name || "\u305f\u308a\u306a\u3044"}\u306e\u6b7b\u306b\u3001\u8eab\u8fd1\u306a\u500b\u4f53\u304c\u52d5\u63fa\u3057\u305f\u3002`, "death", { participants: [dead] });
|
||||
global.TarinaiFreezeSystem?.saveFrozenStore?.(worldRef);
|
||||
global.TarinaiFreezeSystem?.renderFrozenPanel?.(worldRef);
|
||||
}
|
||||
|
|
|
|||
83
js/render.js
83
js/render.js
|
|
@ -348,6 +348,55 @@ function isEntityVisibleInRect(entity, rect, extra = 0) {
|
|||
return x + r >= rect.left && x - r <= rect.right && y + r >= rect.top && y - r <= rect.bottom;
|
||||
}
|
||||
|
||||
function renderStackSignature(worldRef) {
|
||||
return [
|
||||
(worldRef.items || []).length,
|
||||
(worldRef.tarinai || []).length,
|
||||
(worldRef.ants || []).length,
|
||||
worldRef.terrainVersion || 0,
|
||||
].join(":");
|
||||
}
|
||||
|
||||
function ensureWorldRenderStack(worldRef) {
|
||||
if (!worldRef._renderStack) {
|
||||
worldRef._renderStack = { signature: "", backItems: [], layered: [], carriedPlushies: [], lodgedPins: [] };
|
||||
}
|
||||
const stack = worldRef._renderStack;
|
||||
const signature = renderStackSignature(worldRef);
|
||||
if (!worldRef.drawListDirty && stack.signature === signature) return stack;
|
||||
|
||||
stack.backItems.length = 0;
|
||||
stack.layered.length = 0;
|
||||
stack.carriedPlushies.length = 0;
|
||||
stack.lodgedPins.length = 0;
|
||||
|
||||
for (const it of worldRef.items || []) {
|
||||
if (!it || it.dead || isCachedTerrainItem(it)) continue;
|
||||
if (isPinType(it.type) && it.pinState === "lodged") {
|
||||
stack.lodgedPins.push(it);
|
||||
continue;
|
||||
}
|
||||
if (it.isStructure && it.type === "plushie" && it.carriedById) {
|
||||
stack.carriedPlushies.push(it);
|
||||
continue;
|
||||
}
|
||||
if (isBehindSpriteLayerItem(it)) stack.backItems.push(it);
|
||||
else stack.layered.push({ entity: it, y: renderLayerSortY(it), rank: renderLayerKindRank(it) });
|
||||
}
|
||||
for (const t of worldRef.tarinai || []) {
|
||||
if (t && !t.dead) stack.layered.push({ entity: t, y: renderLayerSortY(t), rank: renderLayerKindRank(t) });
|
||||
}
|
||||
for (const a of worldRef.ants || []) {
|
||||
if (a && !a.dead) stack.layered.push({ entity: a, y: renderLayerSortY(a), rank: renderLayerKindRank(a) });
|
||||
}
|
||||
|
||||
stack.backItems.sort((a, b) => renderLayerSortY(a) - renderLayerSortY(b) || (a.x || 0) - (b.x || 0));
|
||||
stack.layered.sort((a, b) => a.y - b.y || a.rank - b.rank || ((a.entity.x || 0) - (b.entity.x || 0)));
|
||||
stack.signature = signature;
|
||||
worldRef.drawListDirty = false;
|
||||
return stack;
|
||||
}
|
||||
|
||||
function ensureBackgroundCache(w, h, lighting) {
|
||||
const key = backgroundCacheKey(world, lighting);
|
||||
if (!backgroundCache.canvas) {
|
||||
|
|
@ -756,19 +805,12 @@ function render() {
|
|||
drawPlacementPreview(ctx, world);
|
||||
if (terrainLayer) ctx.drawImage(terrainLayer, 0, 0, sceneW, sceneH);
|
||||
|
||||
// Low bedding is drawn one step behind the live sprite stack.
|
||||
const backItems = [];
|
||||
const layered = [];
|
||||
for (const it of world.items) {
|
||||
if (!it || it.dead || isCachedTerrainItem(it)) continue;
|
||||
if (isPinType(it.type) && it.pinState === "lodged") continue;
|
||||
if (it.isStructure && it.type === "plushie" && it.carriedById) continue;
|
||||
if (!isEntityVisibleInRect(it, visibleRect)) continue;
|
||||
if (isBehindSpriteLayerItem(it)) backItems.push(it);
|
||||
else layered.push({ entity: it, y: renderLayerSortY(it), rank: renderLayerKindRank(it) });
|
||||
// Dynamic render stacks are sorted on a short interval rather than every paint.
|
||||
// Visibility is still checked per frame, so camera movement remains responsive.
|
||||
const renderStack = ensureWorldRenderStack(world);
|
||||
for (const it of renderStack.backItems) {
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
backItems.sort((a, b) => renderLayerSortY(a) - renderLayerSortY(b) || (a.x || 0) - (b.x || 0));
|
||||
for (const it of backItems) it.draw(ctx, world.time, lighting);
|
||||
|
||||
ctx.save();
|
||||
ctx.lineWidth = 1.15;
|
||||
|
|
@ -786,20 +828,15 @@ function render() {
|
|||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
|
||||
for (const t of world.tarinai || []) {
|
||||
if (t && !t.dead && isEntityVisibleInRect(t, visibleRect)) layered.push({ entity: t, y: renderLayerSortY(t), rank: renderLayerKindRank(t) });
|
||||
for (const entry of renderStack.layered) {
|
||||
if (isEntityVisibleInRect(entry.entity, visibleRect)) entry.entity.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const a of world.ants || []) {
|
||||
if (a && !a.dead && isEntityVisibleInRect(a, visibleRect)) layered.push({ entity: a, y: renderLayerSortY(a), rank: renderLayerKindRank(a) });
|
||||
}
|
||||
layered.sort((a, b) => a.y - b.y || a.rank - b.rank || ((a.entity.x || 0) - (b.entity.x || 0)));
|
||||
for (const entry of layered) entry.entity.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 renderStack.carriedPlushies) {
|
||||
if (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);
|
||||
for (const it of renderStack.lodgedPins) {
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const ef of world.effects) {
|
||||
if (isEntityVisibleInRect(ef, visibleRect)) ef.draw(ctx);
|
||||
|
|
|
|||
|
|
@ -1,57 +1,152 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const STORAGE_PREFIX = "tarinai_save_slot_v1_";
|
||||
const EXPORT_PREFIX = "TARINAI_SAVE_V1:";
|
||||
const STORAGE_PREFIX = "tarinai_save_slot_v2_";
|
||||
const EXPORT_PREFIX = "TN2!";
|
||||
const SLOT_COUNT = 9;
|
||||
const Snapshot = global.TarinaiSnapshot;
|
||||
if (!Snapshot) throw new Error("TarinaiSnapshot is not available for save_system.js");
|
||||
const createSnapshot = (...args) => Snapshot.createSnapshot(...args);
|
||||
const restoreSnapshot = (...args) => Snapshot.restoreSnapshot(...args);
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
let binary = "";
|
||||
const chunk = 0x8000;
|
||||
for (let i = 0; i < bytes.length; i += chunk) binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
||||
const SAVE_TEXT_ALPHABET = (() => {
|
||||
let out = "";
|
||||
for (let i = 33; i <= 126; i++) {
|
||||
const ch = String.fromCharCode(i);
|
||||
if (ch === "*" || ch === "'" || ch === "_") continue;
|
||||
out += ch;
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
const SAVE_TEXT_REVERSE = (() => {
|
||||
const map = new Map();
|
||||
for (let i = 0; i < SAVE_TEXT_ALPHABET.length; i++) map.set(SAVE_TEXT_ALPHABET[i], i);
|
||||
return map;
|
||||
})();
|
||||
if (SAVE_TEXT_ALPHABET.length !== 91) throw new Error("save alphabet must contain 91 printable ASCII chars");
|
||||
|
||||
function base91Encode(bytes) {
|
||||
let b = 0;
|
||||
let n = 0;
|
||||
let out = "";
|
||||
for (const byte of bytes || []) {
|
||||
b |= (byte & 255) << n;
|
||||
n += 8;
|
||||
if (n > 13) {
|
||||
let v = b & 8191;
|
||||
if (v > 88) {
|
||||
b >>= 13;
|
||||
n -= 13;
|
||||
} else {
|
||||
v = b & 16383;
|
||||
b >>= 14;
|
||||
n -= 14;
|
||||
}
|
||||
out += SAVE_TEXT_ALPHABET[v % 91] + SAVE_TEXT_ALPHABET[Math.floor(v / 91)];
|
||||
}
|
||||
}
|
||||
if (n) {
|
||||
out += SAVE_TEXT_ALPHABET[b % 91];
|
||||
if (n > 7 || b > 90) out += SAVE_TEXT_ALPHABET[Math.floor(b / 91)];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function base64ToBytes(text) {
|
||||
let b64 = String(text || "").trim().replace(/^#+/, "").replace(EXPORT_PREFIX, "").replace(/-/g, "+").replace(/_/g, "/");
|
||||
while (b64.length % 4) b64 += "=";
|
||||
const binary = atob(b64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
function base91Decode(text) {
|
||||
const clean = String(text || "").trim();
|
||||
let v = -1;
|
||||
let b = 0;
|
||||
let n = 0;
|
||||
const out = [];
|
||||
for (const ch of clean) {
|
||||
const c = SAVE_TEXT_REVERSE.get(ch);
|
||||
if (c == null) continue;
|
||||
if (v < 0) {
|
||||
v = c;
|
||||
} else {
|
||||
v += c * 91;
|
||||
b |= v << n;
|
||||
n += (v & 8191) > 88 ? 13 : 14;
|
||||
do {
|
||||
out.push(b & 255);
|
||||
b >>= 8;
|
||||
n -= 8;
|
||||
} while (n > 7);
|
||||
v = -1;
|
||||
}
|
||||
}
|
||||
if (v >= 0) out.push((b | (v << n)) & 255);
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
async function compressBytes(bytes) {
|
||||
if (typeof CompressionStream === "function") {
|
||||
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw"));
|
||||
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function encodeSnapshot(snapshot) {
|
||||
const json = JSON.stringify(snapshot);
|
||||
const bytes = new TextEncoder().encode(json);
|
||||
return EXPORT_PREFIX + bytesToBase64(bytes);
|
||||
async function decompressBytes(bytes, codec = "R") {
|
||||
if (codec === "D") {
|
||||
if (typeof DecompressionStream !== "function") throw new Error("deflate decoder is not available in this browser");
|
||||
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw"));
|
||||
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function decodeSnapshot(text) {
|
||||
async function encodeSnapshot(snapshot) {
|
||||
const compact = snapshot && snapshot.v === 2 ? snapshot : createSnapshot(global.world);
|
||||
const json = JSON.stringify(compact);
|
||||
const bytes = new TextEncoder().encode(json);
|
||||
let codec = "R";
|
||||
let packed = bytes;
|
||||
try {
|
||||
const compressed = await compressBytes(bytes);
|
||||
if (compressed.length < bytes.length) {
|
||||
codec = "D";
|
||||
packed = compressed;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("save compression fallback", error);
|
||||
}
|
||||
return `${EXPORT_PREFIX}${codec}${base91Encode(packed)}`;
|
||||
}
|
||||
|
||||
async function decodeSnapshot(text) {
|
||||
const raw = String(text || "").trim();
|
||||
if (!raw) throw new Error("empty import text");
|
||||
if (raw.startsWith("{") || raw.startsWith("[")) return JSON.parse(raw);
|
||||
const bytes = base64ToBytes(raw);
|
||||
if (raw.startsWith("{") || raw.startsWith("[")) {
|
||||
const data = JSON.parse(raw);
|
||||
if (data?.v !== 2) throw new Error("unsupported save version");
|
||||
return data;
|
||||
}
|
||||
if (!raw.startsWith(EXPORT_PREFIX)) throw new Error("unsupported save text");
|
||||
const codec = raw.charAt(EXPORT_PREFIX.length) || "R";
|
||||
const payload = raw.slice(EXPORT_PREFIX.length + 1);
|
||||
const packed = base91Decode(payload);
|
||||
const bytes = await decompressBytes(packed, codec);
|
||||
const json = new TextDecoder().decode(bytes);
|
||||
return JSON.parse(json);
|
||||
const data = JSON.parse(json);
|
||||
if (data?.v !== 2) throw new Error("unsupported save version");
|
||||
return data;
|
||||
}
|
||||
|
||||
function slotKey(slot) { return `${STORAGE_PREFIX}${slot}`; }
|
||||
|
||||
function saveSlot(slot) {
|
||||
async function saveSlot(slot) {
|
||||
const snapshot = createSnapshot(global.world);
|
||||
localStorage.setItem(slotKey(slot), JSON.stringify(snapshot));
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function loadSlot(slot) {
|
||||
async function loadSlot(slot) {
|
||||
const raw = localStorage.getItem(slotKey(slot));
|
||||
if (!raw) throw new Error("empty slot");
|
||||
return restoreSnapshot(JSON.parse(raw), global.world);
|
||||
const snapshot = JSON.parse(raw);
|
||||
if (snapshot?.v !== 2) throw new Error("unsupported slot version");
|
||||
return restoreSnapshot(snapshot, global.world);
|
||||
}
|
||||
|
||||
function deleteSlot(slot) { localStorage.removeItem(slotKey(slot)); }
|
||||
|
|
@ -66,10 +161,14 @@
|
|||
|
||||
|
||||
function formatSlotSummary(snapshot) {
|
||||
if (!snapshot) return "空き";
|
||||
const d = snapshot.summary || {};
|
||||
const date = snapshot.createdAt ? new Date(snapshot.createdAt).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "日時不明";
|
||||
return `${date} / ${d.fieldType || "庭"} / ${d.day || 1}日目 ${d.time || ""} / ${d.population || 0}匹`;
|
||||
if (!snapshot) return "\u7a7a\u304d";
|
||||
const m = snapshot.m || [];
|
||||
const date = snapshot.c ? new Date(snapshot.c).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "\u65e5\u6642\u4e0d\u660e";
|
||||
const day = m[0] || 1;
|
||||
const population = m[1] || 0;
|
||||
const fieldType = m[2] || "\u5ead";
|
||||
const timeText = Number.isFinite(m[3]) ? `${Math.floor((m[3] / 10) % 24).toString().padStart(2, "0")}:00` : "";
|
||||
return `${date} / ${fieldType} / ${day}\u65e5\u76ee ${timeText} / ${population}\u5339`;
|
||||
}
|
||||
|
||||
function ensureDialog() {
|
||||
|
|
@ -141,7 +240,7 @@
|
|||
openDialog();
|
||||
});
|
||||
const dialog = ensureDialog();
|
||||
dialog.addEventListener("click", (e) => {
|
||||
dialog.addEventListener("click", async (e) => {
|
||||
if (e.target === dialog) { closeDialog(); return; }
|
||||
const saveBtn = e.target.closest("[data-save-slot]");
|
||||
const loadBtn = e.target.closest("[data-load-slot]");
|
||||
|
|
@ -149,61 +248,61 @@
|
|||
if (saveBtn) {
|
||||
const slot = Number(saveBtn.dataset.saveSlot);
|
||||
try {
|
||||
saveSlot(slot);
|
||||
await saveSlot(slot);
|
||||
renderSlotList();
|
||||
global.audio?.notify?.();
|
||||
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}に\u4fdd\u5b58しました。`);
|
||||
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u306b\u4fdd\u5b58\u3057\u307e\u3057\u305f\u3002`);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
global.showToast?.("\u4fdd\u5b58に失敗しました。空き容量を確認してください。");
|
||||
global.showToast?.("\u4fdd\u5b58\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u7a7a\u304d\u5bb9\u91cf\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (loadBtn) {
|
||||
const slot = Number(loadBtn.dataset.loadSlot);
|
||||
if (!global.confirm?.(`\u30b9\u30ed\u30c3\u30c8${slot}を読み込みます。現在の状態は上書きされます。`)) return;
|
||||
if (!global.confirm?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u8aad\u307f\u8fbc\u307f\u307e\u3059\u3002\u73fe\u5728\u306e\u72b6\u614b\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002`)) return;
|
||||
try {
|
||||
loadSlot(slot);
|
||||
await loadSlot(slot);
|
||||
closeDialog();
|
||||
global.audio?.notify?.();
|
||||
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}を読み込みました。`);
|
||||
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u8aad\u307f\u8fbc\u307f\u307e\u3057\u305f\u3002`);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
global.showToast?.("読み込みに失敗しました。");
|
||||
global.showToast?.("\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (deleteBtn) {
|
||||
const slot = Number(deleteBtn.dataset.deleteSlot);
|
||||
if (!global.confirm?.(`\u30b9\u30ed\u30c3\u30c8${slot}を\u524a\u9664しますか?`)) return;
|
||||
if (!global.confirm?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u524a\u9664\u3057\u307e\u3059\u304b\uff1f`)) return;
|
||||
deleteSlot(slot);
|
||||
renderSlotList();
|
||||
global.audio?.delete?.();
|
||||
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}を\u524a\u9664しました。`);
|
||||
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u524a\u9664\u3057\u307e\u3057\u305f\u3002`);
|
||||
return;
|
||||
}
|
||||
if (e.target.closest("#saveCloseBtn")) closeDialog();
|
||||
if (e.target.closest("#saveExportBtn")) {
|
||||
const area = dialog.querySelector("#saveHashText");
|
||||
if (area) {
|
||||
area.value = encodeSnapshot(createSnapshot(global.world));
|
||||
area.value = await encodeSnapshot(createSnapshot(global.world));
|
||||
area.focus();
|
||||
area.select();
|
||||
}
|
||||
global.showToast?.("現在の状態をハッシュテキストに書き出しました。");
|
||||
global.showToast?.("\u73fe\u5728\u306e\u72b6\u614b\u3092\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u306b\u66f8\u304d\u51fa\u3057\u307e\u3057\u305f\u3002");
|
||||
}
|
||||
if (e.target.closest("#saveImportBtn")) {
|
||||
const area = dialog.querySelector("#saveHashText");
|
||||
const text = area?.value || "";
|
||||
if (!text.trim()) { global.showToast?.("読み込むハッシュテキストを貼り付けてください。"); return; }
|
||||
if (!global.confirm?.("\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u3092\u8aad\u307f\u8fbc\u307fます。現在の状態は上書きされます。")) return;
|
||||
if (!text.trim()) { global.showToast?.("\u8aad\u307f\u8fbc\u3080\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u304f\u3060\u3055\u3044\u3002"); return; }
|
||||
if (!global.confirm?.("\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u3092\u8aad\u307f\u8fbc\u307f\u307e\u3059\u3002\u73fe\u5728\u306e\u72b6\u614b\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002")) return;
|
||||
try {
|
||||
restoreSnapshot(decodeSnapshot(text), global.world);
|
||||
restoreSnapshot(await decodeSnapshot(text), global.world);
|
||||
closeDialog();
|
||||
global.showToast?.("ハッシュテキストから読み込みました。");
|
||||
global.showToast?.("\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u304b\u3089\u8aad\u307f\u8fbc\u307f\u307e\u3057\u305f\u3002");
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
global.showToast?.("読み込みに失敗しました。テキストを確認してください。");
|
||||
global.showToast?.("\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30c6\u30ad\u30b9\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const SNAPSHOT_VERSION = 1;
|
||||
const SNAPSHOT_VERSION = 2;
|
||||
const NEED_KEYS = ["food", "sleep", "health", "safety", "social", "fulfill"];
|
||||
const DEFAULT_SKIP = new Set(["world", "target", "panicTarget", "targetRef", "targetEntity", "source"]);
|
||||
|
||||
function isPlainObject(value) {
|
||||
|
|
@ -56,6 +57,63 @@
|
|||
return target;
|
||||
}
|
||||
|
||||
function q(value, scale = 1, fallback = 0) {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? Math.round(n * scale) : fallback;
|
||||
}
|
||||
|
||||
function u(value, scale = 1, fallback = 0) {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n / scale : fallback;
|
||||
}
|
||||
|
||||
function compactNumberArray(obj, keys, scale = 1) {
|
||||
return keys.map(k => q(obj?.[k], scale));
|
||||
}
|
||||
|
||||
function expandNumberArray(arr, keys, scale = 1) {
|
||||
const out = {};
|
||||
if (!Array.isArray(arr)) return out;
|
||||
keys.forEach((key, i) => { out[key] = u(arr[i], scale); });
|
||||
return out;
|
||||
}
|
||||
|
||||
function needsToArray(needs = {}) {
|
||||
return NEED_KEYS.map(k => q(needs[k], 1));
|
||||
}
|
||||
|
||||
function arrayToNeeds(arr = []) {
|
||||
const out = typeof createDefaultNeeds === "function" ? createDefaultNeeds() : { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 };
|
||||
NEED_KEYS.forEach((k, i) => { out[k] = u(arr[i], 1); });
|
||||
return out;
|
||||
}
|
||||
|
||||
function flagPack(t = {}) {
|
||||
let flags = 0;
|
||||
if (t.dead) flags |= 1 << 0;
|
||||
if (t.favorite) flags |= 1 << 1;
|
||||
if (t.zunchiDisease) flags |= 1 << 2;
|
||||
if (t.sleepDisease) flags |= 1 << 3;
|
||||
if (t.explosionDisease) flags |= 1 << 4;
|
||||
if (t.fightDisease) flags |= 1 << 5;
|
||||
if (t.isZunchiSlave) flags |= 1 << 6;
|
||||
if (t.zunchiSlaveLocked) flags |= 1 << 7;
|
||||
if (t.birthRitualLeader) flags |= 1 << 8;
|
||||
return flags;
|
||||
}
|
||||
|
||||
function flagApply(t, flags = 0) {
|
||||
t.dead = !!(flags & (1 << 0));
|
||||
t.favorite = !!(flags & (1 << 1));
|
||||
t.zunchiDisease = !!(flags & (1 << 2));
|
||||
t.sleepDisease = !!(flags & (1 << 3));
|
||||
t.explosionDisease = !!(flags & (1 << 4));
|
||||
t.fightDisease = !!(flags & (1 << 5));
|
||||
t.isZunchiSlave = !!(flags & (1 << 6));
|
||||
t.zunchiSlaveLocked = !!(flags & (1 << 7));
|
||||
t.birthRitualLeader = !!(flags & (1 << 8));
|
||||
}
|
||||
|
||||
function refFor(worldRef, value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
if ((worldRef?.tarinai || []).includes(value)) return { kind: "tarinai", id: value.id || "", token: value.liveToken || 0, familyKey: value.familyKey || "" };
|
||||
|
|
@ -75,22 +133,209 @@
|
|||
}
|
||||
|
||||
function worldData(worldRef) {
|
||||
const keys = [
|
||||
"w", "h", "viewportW", "viewportH", "time", "day", "paused", "speed", "tool", "toolSize", "toolSizes", "frozenTarinai",
|
||||
"fieldType", "fieldZoom", "cameraX", "cameraY", "weather", "nextWeatherChange", "deadCount",
|
||||
"liveIdNext", "liveIdSerial", "lastBirthAt", "maxGeneration", "family", "familyVersion", "familyTreeDirtyReason",
|
||||
"relationNotices", "resolvedFightIds", "eventCounters", "foodSpoilagePenalty", "foodSpoilageEvents",
|
||||
"lastPhase", "colonyMood", "lastColonyMoodDay", "antUpdatePhase", "shakeTimer", "shakeDuration", "shakeStrength"
|
||||
return {
|
||||
fieldType: worldRef?.fieldType || "garden",
|
||||
time: Number(worldRef?.time) || 0,
|
||||
day: Number(worldRef?.day) || 1,
|
||||
cameraX: Number(worldRef?.cameraX) || 0,
|
||||
cameraY: Number(worldRef?.cameraY) || 0,
|
||||
weather: worldRef?.weather || "sunny",
|
||||
nextWeatherChange: Number(worldRef?.nextWeatherChange) || 0,
|
||||
deadCount: Number(worldRef?.deadCount) || 0,
|
||||
liveIdNext: Number(worldRef?.liveIdNext) || 1,
|
||||
liveIdSerial: Number(worldRef?.liveIdSerial) || 1,
|
||||
lastBirthAt: Number(worldRef?.lastBirthAt) || -999,
|
||||
maxGeneration: Number(worldRef?.maxGeneration) || 1,
|
||||
colonyMood: worldRef?.colonyMood?.id || worldRef?.colonyMood || "relaxed",
|
||||
};
|
||||
}
|
||||
|
||||
function compactWorld(worldRef) {
|
||||
const mood = worldRef?.colonyMood?.id || worldRef?.colonyMood || "relaxed";
|
||||
return [
|
||||
worldRef?.fieldType || "garden",
|
||||
q(worldRef?.time, 10),
|
||||
q(worldRef?.day, 1, 1),
|
||||
q(worldRef?.cameraX, 1),
|
||||
q(worldRef?.cameraY, 1),
|
||||
worldRef?.weather || "sunny",
|
||||
q(worldRef?.nextWeatherChange, 10),
|
||||
q(worldRef?.deadCount, 1),
|
||||
q(worldRef?.liveIdNext, 1, 1),
|
||||
q(worldRef?.liveIdSerial, 1, 1),
|
||||
q(worldRef?.lastBirthAt, 10, -9990),
|
||||
q(worldRef?.maxGeneration, 1, 1),
|
||||
mood,
|
||||
];
|
||||
const out = {};
|
||||
for (const key of keys) {
|
||||
const cloned = clonePlain(worldRef?.[key]);
|
||||
if (cloned !== undefined) out[key] = cloned;
|
||||
}
|
||||
|
||||
function applyCompactWorld(worldRef, arr = []) {
|
||||
worldRef.fieldType = arr[0] || "garden";
|
||||
worldRef.time = u(arr[1], 10);
|
||||
worldRef.day = u(arr[2], 1, 1);
|
||||
worldRef.cameraX = u(arr[3], 1);
|
||||
worldRef.cameraY = u(arr[4], 1);
|
||||
worldRef.weather = arr[5] || "sunny";
|
||||
worldRef.nextWeatherChange = u(arr[6], 10);
|
||||
worldRef.deadCount = u(arr[7], 1);
|
||||
worldRef.liveIdNext = Math.max(1, u(arr[8], 1, 1));
|
||||
worldRef.liveIdSerial = Math.max(1, u(arr[9], 1, 1));
|
||||
worldRef.lastBirthAt = u(arr[10], 10, -999);
|
||||
worldRef.maxGeneration = Math.max(1, u(arr[11], 1, 1));
|
||||
const moodId = arr[12] || "relaxed";
|
||||
worldRef.colonyMood = worldRef.colonyMoodDefinition?.(moodId) || worldRef.colonyMoodDefinition?.("relaxed") || { id: moodId, label: moodId, effects: { personality: {} } };
|
||||
}
|
||||
|
||||
function compactBehavior(b = null, tarinaiIndex = new Map(), itemIndex = new Map()) {
|
||||
if (!b || typeof b !== "object") return null;
|
||||
const ref = b.targetRef || b.target || null;
|
||||
let target = null;
|
||||
if (ref?.kind === "tarinai") target = ["t", tarinaiIndex.get(ref.id) ?? -1];
|
||||
else if (ref?.kind === "item") target = ["i", itemIndex.get(ref.id) ?? -1];
|
||||
else if (Number.isFinite(ref?.x) && Number.isFinite(ref?.y)) target = ["p", q(ref.x, 1), q(ref.y, 1)];
|
||||
return [b.id || "", b.need || "", b.subNeed || "", b.phase || "", b.forced ? 1 : 0, target].filter((v, i) => i < 5 || v);
|
||||
}
|
||||
|
||||
function expandBehavior(arr = null) {
|
||||
if (!Array.isArray(arr) || !arr[0]) return null;
|
||||
return {
|
||||
id: arr[0] || "",
|
||||
need: arr[1] || "",
|
||||
subNeed: arr[2] || "",
|
||||
phase: arr[3] || "",
|
||||
forced: !!arr[4],
|
||||
};
|
||||
}
|
||||
|
||||
function compactRelationships(t, tarinaiIndex) {
|
||||
const out = [];
|
||||
for (const [id, rel] of Object.entries(t.relationships || {})) {
|
||||
const idx = tarinaiIndex.get(id);
|
||||
if (!Number.isInteger(idx) || idx < 0) continue;
|
||||
const affinity = Number(rel?.affinity) || 0;
|
||||
const fear = Number(rel?.fear) || 0;
|
||||
const wins = Number(rel?.fightsWon) || 0;
|
||||
const losses = Number(rel?.fightsLost) || 0;
|
||||
if (Math.abs(affinity) < 1 && fear < 1 && wins < 1 && losses < 1) continue;
|
||||
out.push([idx, q(affinity, 10), q(fear, 10), q(wins, 1), q(losses, 1)]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function expandRelationships(rows = [], tarinaiList = [], worldRef = null) {
|
||||
const out = {};
|
||||
for (const row of rows || []) {
|
||||
if (!Array.isArray(row)) continue;
|
||||
const other = tarinaiList[row[0]];
|
||||
if (!other?.id) continue;
|
||||
out[other.id] = {
|
||||
affinity: u(row[1], 10),
|
||||
fear: u(row[2], 10),
|
||||
fightsWon: u(row[3], 1),
|
||||
fightsLost: u(row[4], 1),
|
||||
lastEvent: (u(row[3], 1) || u(row[4], 1)) ? "fight" : "",
|
||||
lastTime: worldRef?.time || 0,
|
||||
};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function compactTarinai(t, idx, familyIndex, tarinaiIndex, itemIndex) {
|
||||
const parentIdx = (t.parents || []).map(id => familyIndex.get(id)).filter(Number.isInteger);
|
||||
const childIdx = (t.children || []).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.postBirthPeaceTimer, 10), q(t.awakeLockTimer, 10),
|
||||
q(t.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100),
|
||||
];
|
||||
const modes = [t.powerItemMode || "", t.sizeItemMode || "", t.lifeItemMode || ""];
|
||||
const pinIdx = itemIndex.get(t.stuckPushpinId) ?? -1;
|
||||
const nestIdx = itemIndex.get(t.insideNestBoxId) ?? -1;
|
||||
return [
|
||||
t.name || "", t.type || "smile", t.birthPersonality || null, t.currentPersonality || null, t.genetics || null,
|
||||
q(t.x, 1), q(t.y, 1), q(t.vx, 10), q(t.vy, 10), q(t.scale, 1000), q(t.adultScale, 1000),
|
||||
q(t.age, 10), q(t.lifeSpan, 10), q(t.birthTime, 10), q(t.generation, 1, 1), t.hasPaired ? 1 : 0,
|
||||
q(t.hunger, 1), q(t.loneliness, 1), q(t.energy, 1), q(t.circadianSleepPressure, 1), needsToArray(t.needs || t.needRaw || {}),
|
||||
t.state || "idle", flagPack(t), parentIdx, childIdx, compactRelationships(t, tarinaiIndex), timers, modes,
|
||||
pinIdx, nestIdx, q(t.totalFightWins, 1), q(t.totalFightLosses, 1), t.deathReason || "", compactBehavior(t.behavior, tarinaiIndex, itemIndex),
|
||||
];
|
||||
}
|
||||
|
||||
function itemExtra(item, tarinaiIndex = new Map()) {
|
||||
const type = item?.type || "";
|
||||
if (item?.isStructure) return ["S", q(item.hp, 10), q(item.maxHp, 10), q(item.attachment, 10), q(item.usedCount, 1), tarinaiIndex.get(item.ownerId) ?? -1, item.carriedById ? (tarinaiIndex.get(item.carriedById) ?? -1) : -1, item.onHead ? 1 : 0];
|
||||
if (type === "grass") return [q(item.growth, 1000), q(item.health, 1000), q(item.seedTimer, 10), q(item.eatenAmount, 10), q(item.fertilityBoost, 1000), q(item.lifeSpan, 10), q(item.wither, 1000)];
|
||||
if (type === "bed") return [q(item.comfort, 1000), q(item.wear, 1000)];
|
||||
if (type === "signboard") return [item.text || ""];
|
||||
if (type === "duplicator") return [item.storedFoodType || "", item.storedFoodLabel || ""];
|
||||
if (type === "ball") return [q(item.vx, 10), q(item.vy, 10), q(item.spin, 1000), q(item.spinVelocity, 1000)];
|
||||
if (type === "zunchi") return [item.stage || "fresh", q(item.stageTimer, 10), q(item.fertility, 1000), q(item.freshness, 1000), item.zunchiVariant || ""];
|
||||
if (type === "ant_nest") return [q(item.antCount, 1), Array.isArray(item.antWorkers) ? item.antWorkers.map(v => q(v, 10)) : [], q(item.queenSpawnAt, 10)];
|
||||
if (type === "ant_corpse") return [item.workerSprite || "ant_worker", q(item.decayTimer, 10)];
|
||||
if (type === "firecracker") return [q(item.fuseTimer, 10), q(item.fuseMax, 10)];
|
||||
if (typeof isPinType === "function" && isPinType(type)) return [q(item.vx, 10), q(item.vy, 10), q(item.spin, 1000), q(item.spinVelocity, 1000), item.pinState || "loose", tarinaiIndex.get(item.pinTargetId) ?? -1, q(item.pinAttachAngle, 1000), q(item.pinAttachDistance, 10), q(item.pinOffsetY, 10)];
|
||||
if (typeof isServingFoodType === "function" && isServingFoodType(type)) return [item.toolSize || "medium", q(item.foodServingsRemaining ?? item.amount, 10), q(item.foodServingsMax, 10)];
|
||||
return null;
|
||||
}
|
||||
|
||||
function compactItem(item, index, tarinaiIndex = new Map()) {
|
||||
return [
|
||||
item?.type || "", q(item?.x, 1), q(item?.y, 1), q(item?.r, 10), q(item?.amount, 10), q(item?.age, 10), q(item?.seed, 10), item?.dead ? 1 : 0, itemExtra(item, tarinaiIndex),
|
||||
];
|
||||
}
|
||||
|
||||
function applyItemExtra(item, extra, tarinaiList = []) {
|
||||
if (!Array.isArray(extra)) return;
|
||||
const type = item.type || "";
|
||||
if (extra[0] === "S") {
|
||||
item.isStructure = true;
|
||||
item.hp = u(extra[1], 10);
|
||||
item.maxHp = u(extra[2], 10);
|
||||
item.attachment = u(extra[3], 10);
|
||||
item.usedCount = u(extra[4], 1);
|
||||
item.ownerId = tarinaiList[extra[5]]?.id || "";
|
||||
item.carriedById = tarinaiList[extra[6]]?.id || "";
|
||||
item.onHead = !!extra[7];
|
||||
const def = global.StructureRegistry?.get?.(type);
|
||||
if (def) {
|
||||
item.label = def.label;
|
||||
item.roles = clonePlain(def.roles) || item.roles || {};
|
||||
item.needEffects = clonePlain(def.needEffects) || item.needEffects || {};
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type === "grass") {
|
||||
item.growth = u(extra[0], 1000); item.health = u(extra[1], 1000); item.seedTimer = u(extra[2], 10); item.eatenAmount = u(extra[3], 10); item.fertilityBoost = u(extra[4], 1000); item.lifeSpan = u(extra[5], 10); item.wither = u(extra[6], 1000);
|
||||
} else if (type === "bed") {
|
||||
item.comfort = u(extra[0], 1000); item.wear = u(extra[1], 1000);
|
||||
} else if (type === "signboard") {
|
||||
item.text = String(extra[0] || "");
|
||||
} else if (type === "duplicator") {
|
||||
item.storedFoodType = extra[0] || ""; item.storedFoodLabel = extra[1] || "";
|
||||
item.roles.food = Boolean(item.storedFoodType);
|
||||
} else if (type === "ball") {
|
||||
item.vx = u(extra[0], 10); item.vy = u(extra[1], 10); item.spin = u(extra[2], 1000); item.spinVelocity = u(extra[3], 1000);
|
||||
} else if (type === "zunchi") {
|
||||
item.stage = extra[0] || "fresh"; item.stageTimer = u(extra[1], 10); item.fertility = u(extra[2], 1000); item.freshness = u(extra[3], 1000); item.zunchiVariant = extra[4] || item.zunchiVariant || "zunchi";
|
||||
} else if (type === "ant_nest") {
|
||||
item.antCount = u(extra[0], 1); item.antWorkers = Array.isArray(extra[1]) ? extra[1].map(v => u(v, 10)) : []; item.queenSpawnAt = u(extra[2], 10);
|
||||
} else if (type === "ant_corpse") {
|
||||
item.workerSprite = extra[0] || "ant_worker"; item.decayTimer = u(extra[1], 10);
|
||||
} else if (type === "firecracker") {
|
||||
item.fuseTimer = u(extra[0], 10); item.fuseMax = u(extra[1], 10);
|
||||
} else if (typeof isPinType === "function" && isPinType(type)) {
|
||||
item.vx = u(extra[0], 10); item.vy = u(extra[1], 10); item.spin = u(extra[2], 1000); item.spinVelocity = u(extra[3], 1000); item.pinState = extra[4] || "loose"; item.pinTargetId = tarinaiList[extra[5]]?.id || ""; item.pinAttachAngle = u(extra[6], 1000); item.pinAttachDistance = u(extra[7], 10); item.pinOffsetY = u(extra[8], 10);
|
||||
} else if (typeof isServingFoodType === "function" && isServingFoodType(type)) {
|
||||
item.toolSize = extra[0] || "medium"; item.foodServingsRemaining = u(extra[1], 10); item.foodServingsMax = u(extra[2], 10); item.amount = item.foodServingsRemaining;
|
||||
}
|
||||
}
|
||||
|
||||
function compactAnt(ant, itemIndex = new Map(), tarinaiIndex = new Map()) {
|
||||
return [ant.kind || "worker", itemIndex.get(ant.homeId) ?? -1, tarinaiIndex.get(ant.targetId) ?? -1, q(ant.x, 1), q(ant.y, 1), q(ant.vx, 10), q(ant.vy, 10), q(ant.hp, 10), q(ant.maxHp, 10), ant.state || "search", q(ant.age, 10), q(ant.seed, 10), q(ant.returnTimer, 10), q(ant.foundingX, 1), q(ant.foundingY, 1), q(ant.foundingDelayUntil, 10), q(ant.foundingAttempts, 1), ant.dead ? 1 : 0];
|
||||
}
|
||||
|
||||
function snapshotEntity(worldRef, entity, skip = new Set(["world", "target", "panicTarget", "targetRef"])) {
|
||||
// Compatibility helper for non-save subsystems such as freeze. The exported save format no longer uses this broad snapshot.
|
||||
const data = copyOwnData(entity, skip);
|
||||
const targetRef = refFor(worldRef, entity?.target);
|
||||
const panicTargetRef = refFor(worldRef, entity?.panicTarget);
|
||||
|
|
@ -103,25 +348,27 @@
|
|||
|
||||
function createSnapshot(worldRef = global.world) {
|
||||
if (!worldRef) throw new Error("world is not ready");
|
||||
const selectedRef = refFor(worldRef, worldRef.selected);
|
||||
const tarinaiLive = (worldRef.tarinai || []).filter(Boolean);
|
||||
const itemsLive = (worldRef.items || []).filter(Boolean).filter(it => it && !it.dead);
|
||||
const tarinaiIndex = new Map();
|
||||
const familyIndex = new Map();
|
||||
tarinaiLive.forEach((t, i) => {
|
||||
if (t.id) tarinaiIndex.set(t.id, i);
|
||||
if (t.familyKey) familyIndex.set(t.familyKey, i);
|
||||
});
|
||||
const itemIndex = new Map();
|
||||
itemsLive.forEach((it, i) => { if (it.id) itemIndex.set(it.id, i); });
|
||||
const now = Date.now();
|
||||
return {
|
||||
version: SNAPSHOT_VERSION,
|
||||
app: "tarinai_colony_game",
|
||||
createdAt: Date.now(),
|
||||
summary: {
|
||||
day: worldRef.day || 1,
|
||||
time: worldRef.clockString ? worldRef.clockString() : "",
|
||||
fieldType: worldRef.fieldType || "garden",
|
||||
population: (worldRef.tarinai || []).filter(t => t && !t.dead).length,
|
||||
items: (worldRef.items || []).filter(it => it && !it.dead).length,
|
||||
},
|
||||
world: worldData(worldRef),
|
||||
tarinai: (worldRef.tarinai || []).filter(Boolean).map(t => snapshotEntity(worldRef, t)),
|
||||
items: (worldRef.items || []).filter(Boolean).map(it => snapshotEntity(worldRef, it)),
|
||||
ants: (worldRef.ants || []).filter(Boolean).map(a => snapshotEntity(worldRef, a)),
|
||||
effects: (worldRef.effects || []).filter(Boolean).map(ef => snapshotEntity(worldRef, ef)),
|
||||
logs: clonePlain((worldRef.logs || []).slice(0, 100)) || [],
|
||||
selectedRef,
|
||||
v: SNAPSHOT_VERSION,
|
||||
a: "tcg",
|
||||
c: now,
|
||||
m: [worldRef.day || 1, (worldRef.tarinai || []).filter(t => t && !t.dead).length, worldRef.fieldType || "garden", q(worldRef.time || 0, 10), itemsLive.length],
|
||||
w: compactWorld(worldRef),
|
||||
t: tarinaiLive.map((t, i) => compactTarinai(t, i, familyIndex, tarinaiIndex, itemIndex)),
|
||||
i: itemsLive.map((it, i) => compactItem(it, i, tarinaiIndex)),
|
||||
n: (worldRef.ants || []).filter(a => a && !a.dead).map(a => compactAnt(a, itemIndex, tarinaiIndex)),
|
||||
s: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -141,11 +388,52 @@
|
|||
return t;
|
||||
}
|
||||
|
||||
function expandTarinai(row, index, worldRef) {
|
||||
const TarinaiClass = global.Tarinai || (typeof Tarinai !== "undefined" ? Tarinai : null);
|
||||
if (!TarinaiClass) throw new Error("Tarinai is not available");
|
||||
const opts = {
|
||||
familyKey: `sf${index}`,
|
||||
id: `st${index}`,
|
||||
liveToken: 1,
|
||||
name: row[0] || undefined,
|
||||
type: row[1] || "smile",
|
||||
birthPersonality: row[2] || undefined,
|
||||
currentPersonality: row[3] || undefined,
|
||||
genetics: row[4] || undefined,
|
||||
x: u(row[5], 1), y: u(row[6], 1), vx: u(row[7], 10), vy: u(row[8], 10),
|
||||
scale: u(row[9], 1000, 0.28), adultScale: u(row[10], 1000, 0.28),
|
||||
age: u(row[11], 10), lifeSpan: u(row[12], 10, 1600), birthTime: u(row[13], 10), generation: Math.max(1, u(row[14], 1, 1)),
|
||||
hasPaired: !!row[15], hunger: u(row[16], 1), loneliness: u(row[17], 1), energy: u(row[18], 1), circadianSleepPressure: u(row[19], 1),
|
||||
needs: arrayToNeeds(row[20]), state: row[21] || "idle", behavior: expandBehavior(row[33]),
|
||||
};
|
||||
const t = new TarinaiClass(worldRef, opts);
|
||||
t.familyKey = `sf${index}`;
|
||||
t.id = `st${index}`;
|
||||
t.liveToken = 1;
|
||||
t.needRaw = { ...t.needs };
|
||||
t.needDisplay = { ...t.needs };
|
||||
t.previousNeeds = { ...t.needs };
|
||||
flagApply(t, Number(row[22]) || 0);
|
||||
t.__savedParentIdx = Array.isArray(row[23]) ? row[23] : [];
|
||||
t.__savedChildIdx = Array.isArray(row[24]) ? row[24] : [];
|
||||
t.__savedRelationships = Array.isArray(row[25]) ? row[25] : [];
|
||||
const timers = Array.isArray(row[26]) ? row[26] : [];
|
||||
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.postBirthPeaceTimer = u(timers[6], 10); t.awakeLockTimer = u(timers[7], 10);
|
||||
t.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[8], 10), ammo: u(timers[9], 10) };
|
||||
t.mercuryLifeMultiplier = u(timers[10], 100, t.mercuryLifeMultiplier || 1);
|
||||
const modes = Array.isArray(row[27]) ? row[27] : [];
|
||||
t.powerItemMode = modes[0] || ""; t.sizeItemMode = modes[1] || ""; t.lifeItemMode = modes[2] || "";
|
||||
t.__savedPinIdx = row[28]; t.__savedNestIdx = row[29];
|
||||
t.totalFightWins = u(row[30], 1); t.totalFightLosses = u(row[31], 1); t.deathReason = row[32] || "";
|
||||
if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize();
|
||||
return t;
|
||||
}
|
||||
|
||||
function restoreSnapshot(snapshot, worldRef = global.world) {
|
||||
if (!snapshot || typeof snapshot !== "object") throw new Error("invalid save data");
|
||||
if (!snapshot || typeof snapshot !== "object" || snapshot.v !== SNAPSHOT_VERSION) throw new Error("invalid v2 save data");
|
||||
if (!worldRef) throw new Error("world is not ready");
|
||||
const data = snapshot.version ? snapshot : JSON.parse(JSON.stringify(snapshot));
|
||||
const fieldType = data.world?.fieldType || "garden";
|
||||
const fieldType = snapshot.w?.[0] || "garden";
|
||||
if (typeof applyFieldLayout === "function") applyFieldLayout(fieldType);
|
||||
if (typeof worldRef.reset === "function") worldRef.reset(0, fieldType);
|
||||
|
||||
|
|
@ -153,82 +441,83 @@
|
|||
worldRef.items = [];
|
||||
worldRef.ants = [];
|
||||
worldRef.effects = [];
|
||||
worldRef.logs = Array.isArray(data.logs) ? clonePlain(data.logs) || [] : [];
|
||||
worldRef.logs = [];
|
||||
worldRef.selected = null;
|
||||
|
||||
applyOwnData(worldRef, data.world || {}, new Set(["tarinai", "items", "ants", "effects", "logs", "selected", "liveTarinai", "events", "spatial"]));
|
||||
worldRef.events = global.TarinaiEvents || null;
|
||||
worldRef.liveTarinai = new Map();
|
||||
worldRef.liveIdFree = [];
|
||||
applyCompactWorld(worldRef, snapshot.w || []);
|
||||
|
||||
const items = Array.isArray(data.items) ? data.items : [];
|
||||
for (const saved of items) {
|
||||
if (!saved || !saved.type) continue;
|
||||
const item = new Item(saved.type, Number(saved.x) || 0, Number(saved.y) || 0);
|
||||
applyOwnData(item, saved, new Set(["target", "targetRef", "world"]));
|
||||
const tarRows = Array.isArray(snapshot.t) ? snapshot.t : [];
|
||||
for (let idx = 0; idx < tarRows.length; idx++) {
|
||||
const t = expandTarinai(tarRows[idx] || [], idx, worldRef);
|
||||
worldRef.tarinai.push(t);
|
||||
if (t.id) worldRef.liveTarinai.set(t.id, { token: t.liveToken || 1, target: t });
|
||||
}
|
||||
for (const [idx, t] of worldRef.tarinai.entries()) {
|
||||
t.parents = (t.__savedParentIdx || []).map(i => worldRef.tarinai[i]?.familyKey).filter(Boolean);
|
||||
t.children = (t.__savedChildIdx || []).map(i => worldRef.tarinai[i]?.familyKey).filter(Boolean);
|
||||
delete t.__savedParentIdx; delete t.__savedChildIdx;
|
||||
}
|
||||
|
||||
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
|
||||
for (let idx = 0; idx < itemRows.length; idx++) {
|
||||
const row = itemRows[idx] || [];
|
||||
const type = row[0] || "";
|
||||
if (!type) continue;
|
||||
let item = null;
|
||||
if (Array.isArray(row[8]) && row[8][0] === "S" && global.StructureRegistry?.get?.(type)) {
|
||||
const owner = worldRef.tarinai[row[8][5]] || null;
|
||||
item = global.StructureRegistry.create(type, owner, u(row[1], 1), u(row[2], 1), worldRef);
|
||||
} else {
|
||||
item = new Item(type, u(row[1], 1), u(row[2], 1));
|
||||
}
|
||||
item.id = `si${idx}`;
|
||||
item.r = u(row[3], 10, item.r || 12);
|
||||
item.amount = u(row[4], 10, item.amount || 0);
|
||||
item.age = u(row[5], 10, 0);
|
||||
item.seed = u(row[6], 10, item.seed || 0);
|
||||
item.dead = !!row[7];
|
||||
applyItemExtra(item, row[8], worldRef.tarinai);
|
||||
worldRef.items.push(item);
|
||||
}
|
||||
|
||||
const tarinaiList = Array.isArray(data.tarinai) ? data.tarinai : [];
|
||||
for (const saved of tarinaiList) {
|
||||
if (!saved) continue;
|
||||
const t = restoreTarinaiData(worldRef, saved, { clearTransient: false });
|
||||
worldRef.tarinai.push(t);
|
||||
if (t.id) worldRef.liveTarinai.set(t.id, { token: t.liveToken || 0, target: t });
|
||||
for (const t of worldRef.tarinai) {
|
||||
if (Number.isInteger(t.__savedPinIdx) && t.__savedPinIdx >= 0) t.stuckPushpinId = worldRef.items[t.__savedPinIdx]?.id || null;
|
||||
if (Number.isInteger(t.__savedNestIdx) && t.__savedNestIdx >= 0) t.insideNestBoxId = worldRef.items[t.__savedNestIdx]?.id || null;
|
||||
t.relationships = expandRelationships(t.__savedRelationships || [], worldRef.tarinai, worldRef);
|
||||
delete t.__savedPinIdx; delete t.__savedNestIdx; delete t.__savedRelationships;
|
||||
}
|
||||
|
||||
const ants = Array.isArray(data.ants) ? data.ants : [];
|
||||
for (const saved of ants) {
|
||||
if (!saved) continue;
|
||||
const ant = new AntActor(worldRef, saved);
|
||||
applyOwnData(ant, saved, new Set(["world", "targetRef"]));
|
||||
ant.world = worldRef;
|
||||
const antRows = Array.isArray(snapshot.n) ? snapshot.n : [];
|
||||
for (let idx = 0; idx < antRows.length; idx++) {
|
||||
const row = antRows[idx] || [];
|
||||
const ant = new AntActor(worldRef, {
|
||||
id: `sa${idx}`,
|
||||
kind: row[0] || "worker",
|
||||
homeId: worldRef.items[row[1]]?.id || "",
|
||||
targetId: worldRef.tarinai[row[2]]?.id || "",
|
||||
targetToken: worldRef.tarinai[row[2]]?.liveToken || 0,
|
||||
x: u(row[3], 1), y: u(row[4], 1), vx: u(row[5], 10), vy: u(row[6], 10), hp: u(row[7], 10), maxHp: u(row[8], 10), state: row[9] || "search", age: u(row[10], 10), seed: u(row[11], 10), returnTimer: u(row[12], 10), foundingX: u(row[13], 1), foundingY: u(row[14], 1), foundingDelayUntil: u(row[15], 10), foundingAttempts: u(row[16], 1), dead: !!row[17],
|
||||
});
|
||||
ant.targetRef = ant.targetId ? worldRef.liveTarinaiById?.(ant.targetId, ant.targetToken) || null : null;
|
||||
worldRef.ants.push(ant);
|
||||
}
|
||||
|
||||
const effects = Array.isArray(data.effects) ? data.effects : [];
|
||||
for (const saved of effects) {
|
||||
if (!saved || !saved.type) continue;
|
||||
const ef = new Effect(saved.type, Number(saved.x) || 0, Number(saved.y) || 0, saved);
|
||||
applyOwnData(ef, saved, new Set(["world", "target", "targetRef"]));
|
||||
worldRef.effects.push(ef);
|
||||
}
|
||||
|
||||
const itemById = new Map(worldRef.items.map(it => [it.id, it]));
|
||||
const antById = new Map(worldRef.ants.map(a => [a.id, a]));
|
||||
const tarinaiById = new Map(worldRef.tarinai.map(t => [t.id, t]));
|
||||
const resolve = (ref) => {
|
||||
if (!ref) return null;
|
||||
if (ref.kind === "item") return itemById.get(ref.id) || null;
|
||||
if (ref.kind === "ant") return antById.get(ref.id) || null;
|
||||
if (ref.kind === "tarinai") return tarinaiById.get(ref.id) || worldRef.tarinai.find(t => t.familyKey === ref.familyKey) || null;
|
||||
return resolveRef(worldRef, ref);
|
||||
};
|
||||
for (const [i, saved] of tarinaiList.entries()) {
|
||||
const t = worldRef.tarinai[i];
|
||||
if (!t) continue;
|
||||
t.target = resolve(saved.__targetRef);
|
||||
t.panicTarget = resolve(saved.__panicTargetRef);
|
||||
}
|
||||
for (const [i, saved] of ants.entries()) {
|
||||
const ant = worldRef.ants[i];
|
||||
if (!ant) continue;
|
||||
ant.targetRef = resolve(saved.__targetRefObject);
|
||||
}
|
||||
|
||||
worldRef.selected = resolve(data.selectedRef);
|
||||
worldRef.family = clonePlain(data.world?.family) || worldRef.family || {};
|
||||
worldRef.relationNotices = clonePlain(data.world?.relationNotices) || {};
|
||||
worldRef.resolvedFightIds = clonePlain(data.world?.resolvedFightIds) || {};
|
||||
worldRef.eventCounters = clonePlain(data.world?.eventCounters) || {};
|
||||
worldRef.colonyMood = clonePlain(data.world?.colonyMood) || worldRef.colonyMoodDefinition?.("relaxed") || { id: "relaxed", label: "のんびり", effects: { personality: {} } };
|
||||
worldRef.family = {};
|
||||
worldRef.familyVersion = (worldRef.familyVersion || 0) + 1;
|
||||
for (const t of worldRef.tarinai) worldRef.recordFamily?.(t);
|
||||
worldRef.familyCleanVersion = null;
|
||||
worldRef.familyTreeDirty = true;
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef.terrainDirty = true;
|
||||
worldRef.relationNotices = {};
|
||||
worldRef.resolvedFightIds = {};
|
||||
worldRef.eventCounters = {};
|
||||
if (typeof worldRef.updateItemCounts === "function") worldRef.updateItemCounts();
|
||||
if (typeof worldRef.updateEffectCounts === "function") worldRef.updateEffectCounts();
|
||||
if (typeof worldRef.rebuildSpatial === "function") worldRef.rebuildSpatial(true);
|
||||
if (typeof worldRef.markTerrainDirty === "function") worldRef.markTerrainDirty("load");
|
||||
if (typeof worldRef.markTerrainDirty === "function") worldRef.markTerrainDirty("load-v2");
|
||||
if (typeof worldRef.clampCamera === "function") worldRef.clampCamera();
|
||||
if (typeof renderLog === "function") renderLog(worldRef.logs || []);
|
||||
if (typeof resetArchiveRenderState === "function") resetArchiveRenderState();
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
ctx.translate(this.x, this.y);
|
||||
const night = (lighting?.light || 1) < 0.42;
|
||||
if (this.type === "grass_bed") {
|
||||
// かんたんベッド: 緑色の小さい干草寝床。通常の干草寝床より一回り小さく、草束感を強める。
|
||||
// \u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9: \u7dd1\u8272\u306e\u5c0f\u3055\u3044\u5e72\u8349\u5bdd\u5e8a\u3002\u901a\u5e38\u306e\u5e72\u8349\u5bdd\u5e8a\u3088\u308a\u4e00\u56de\u308a\u5c0f\u3055\u304f\u3001\u8349\u675f\u611f\u3092\u5f37\u3081\u308b\u3002
|
||||
const grassFill = night ? "rgba(78,132,67,0.90)" : "rgba(84,177,72,0.93)";
|
||||
const grassStroke = night ? "rgba(39,78,43,0.76)" : "rgba(45,112,48,0.78)";
|
||||
const strawStroke = night ? "rgba(168,207,119,0.42)" : "rgba(205,234,128,0.66)";
|
||||
|
|
@ -106,13 +106,13 @@
|
|||
ctx.ellipse(0, this.r * 0.22, this.r * 1.28, this.r * 0.28, -0.06, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (this.type === "plushie") {
|
||||
const img = typeof getRenderableImage === "function" ? getRenderableImage("smile", "smile") : global.images?.get?.("smile");
|
||||
const img = typeof getRenderableImage === "function" ? getRenderableImage("plushie", "plushie") : global.images?.get?.("plushie");
|
||||
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);
|
||||
const metrics = typeof getImageMetrics === "function" ? getImageMetrics("plushie") : null;
|
||||
const w = this.r * 3.0;
|
||||
const h = w * (metrics?.ratio || 0.74);
|
||||
ctx.drawImage(img, -w * 0.58, -h * 0.60, w, h);
|
||||
} else {
|
||||
ctx.fillStyle = "#f7dfc0";
|
||||
ctx.strokeStyle = "rgba(86,62,48,0.70)";
|
||||
|
|
@ -178,16 +178,18 @@
|
|||
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) {
|
||||
onUse(user, structure, worldRef) {
|
||||
if (user?.id && !structure.ownerId) {
|
||||
structure.ownerId = user.id;
|
||||
structure.ownerName = user.name || "";
|
||||
structure.attachment = Math.max(structure.attachment || 0, 24);
|
||||
worldRef?.markItemBucketsDirty?.("grass-bed-owner-adopted");
|
||||
worldRef?.markTerrainDirty?.("grass-bed-owner-adopted");
|
||||
worldRef?.emit?.("structure:ownerAdopted", { structure, owner: user });
|
||||
}
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -67,11 +67,13 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
this.lastSatisfactionSource = opts.lastSatisfactionSource || "";
|
||||
this.stress = typeof calculateStressFromNeeds === "function" ? calculateStressFromNeeds(this.needRaw || this.needs) : 0;
|
||||
this.previousNeeds = opts.previousNeeds && typeof opts.previousNeeds === "object" ? { ...defaultNeeds, ...opts.previousNeeds } : { ...this.needs };
|
||||
this.intent = opts.intent && typeof opts.intent === "object" ? { ...opts.intent } : null;
|
||||
this.currentAction = opts.currentAction && typeof opts.currentAction === "object" ? { ...opts.currentAction } : null;
|
||||
this.activeBehavior = opts.activeBehavior && typeof opts.activeBehavior === "object" ? { ...opts.activeBehavior } : null;
|
||||
this.forcedBehaviorQueue = Array.isArray(opts.forcedBehaviorQueue) ? opts.forcedBehaviorQueue.map(v => ({ ...(v || {}) })).slice(-8) : [];
|
||||
this.need = "";
|
||||
this.state = opts.state || "idle";
|
||||
this.behaviorSerial = opts.behaviorSerial ?? 0;
|
||||
this.behavior = typeof globalThis.normalizeTarinaiBehaviorState === "function"
|
||||
? globalThis.normalizeTarinaiBehaviorState(opts, this)
|
||||
: (opts.behavior && typeof opts.behavior === "object" ? { ...opts.behavior } : (opts.activeBehavior && typeof opts.activeBehavior === "object" ? { ...opts.activeBehavior } : null));
|
||||
this.forcedBehaviorQueue = Array.isArray(opts.forcedBehaviorQueue) ? opts.forcedBehaviorQueue.map(v => ({ ...(v || {}) })).slice(-8) : [];
|
||||
this.actionCooldowns = opts.actionCooldowns && typeof opts.actionCooldowns === "object" ? { ...opts.actionCooldowns } : {};
|
||||
this.needShock = opts.needShock && typeof opts.needShock === "object" ? { ...opts.needShock } : {};
|
||||
this.buildPlan = opts.buildPlan && typeof opts.buildPlan === "object" ? { ...opts.buildPlan } : null;
|
||||
|
|
@ -79,8 +81,6 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
this.hpBarTimer = opts.hpBarTimer ?? 0;
|
||||
this.stressBarTimer = opts.stressBarTimer ?? 0;
|
||||
this.affection = opts.affection ?? rand(0, 30);
|
||||
this.need = "";
|
||||
this.state = opts.state || "idle";
|
||||
this.dead = !!opts.dead;
|
||||
this.deathReason = opts.deathReason || "";
|
||||
this.lastDamageCause = opts.lastDamageCause || "";
|
||||
|
|
|
|||
201
js/tarinai_action_spec.js
Normal file
201
js/tarinai_action_spec.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const registry = new Map();
|
||||
|
||||
function isObject(value) {
|
||||
return Boolean(value && typeof value === "object");
|
||||
}
|
||||
|
||||
function finiteOr(value, fallback) {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function toList(value) {
|
||||
if (Array.isArray(value)) return value.filter(Boolean).map(String);
|
||||
if (value == null || value === "") return [];
|
||||
return [String(value)];
|
||||
}
|
||||
|
||||
function hasOwnFn(obj, key) {
|
||||
return typeof obj?.[key] === "function";
|
||||
}
|
||||
|
||||
function resolveActionSpec(action) {
|
||||
if (!action) return null;
|
||||
return typeof action === "string" ? getTarinaiActionSpec(action) : action;
|
||||
}
|
||||
|
||||
function buildActionContext(tarinai, world, ctx = {}, spec = null) {
|
||||
const base = isObject(ctx) ? { ...ctx } : {};
|
||||
if (base.target === undefined && spec && hasOwnFn(spec, "selectTarget")) {
|
||||
base.target = spec.selectTarget(tarinai, world, base);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function normalizeTarinaiActionSpec(input = {}) {
|
||||
if (!isObject(input)) throw new TypeError("ActionSpec must be an object");
|
||||
const id = String(input.id || input.actionId || "").trim();
|
||||
if (!id) throw new TypeError("ActionSpec.id is required");
|
||||
|
||||
// Public lifecycle: canStart -> selectTarget -> start -> tick -> finish/fail -> text.
|
||||
// Legacy hooks (`condition`, `run`, `update`) remain accepted so old code can be
|
||||
// migrated incrementally, but newly migrated actions should use the lifecycle names.
|
||||
const spec = {
|
||||
...input,
|
||||
id,
|
||||
kind: String(input.kind || "need_action"),
|
||||
need: String(input.need || "fulfill"),
|
||||
subNeed: String(input.subNeed || ""),
|
||||
label: String(input.label || input.phrase || id),
|
||||
phrase: String(input.phrase || input.label || id),
|
||||
weight: finiteOr(input.weight, 1),
|
||||
priority: finiteOr(input.priority, 0),
|
||||
tags: toList(input.tags),
|
||||
timerBacked: Boolean(input.timerBacked),
|
||||
interruptible: input.interruptible !== false,
|
||||
};
|
||||
|
||||
const originalCanStart = hasOwnFn(input, "canStart") ? input.canStart : null;
|
||||
const originalCondition = hasOwnFn(input, "condition") ? input.condition : null;
|
||||
const originalSelectTarget = hasOwnFn(input, "selectTarget") ? input.selectTarget : null;
|
||||
const originalStart = hasOwnFn(input, "start") ? input.start : null;
|
||||
const originalRun = hasOwnFn(input, "run") ? input.run : null;
|
||||
const originalTick = hasOwnFn(input, "tick") ? input.tick : null;
|
||||
const originalUpdate = hasOwnFn(input, "update") ? input.update : null;
|
||||
const originalFinish = hasOwnFn(input, "finish") ? input.finish : null;
|
||||
const originalFail = hasOwnFn(input, "fail") ? input.fail : null;
|
||||
const originalText = hasOwnFn(input, "text") ? input.text : null;
|
||||
|
||||
spec.selectTarget = function selectTarget(tarinai, world, ctx = {}) {
|
||||
if (originalSelectTarget) return originalSelectTarget.call(spec, tarinai, world, ctx);
|
||||
return ctx.target ?? tarinai?.target ?? null;
|
||||
};
|
||||
|
||||
spec.canStart = function canStart(tarinai, world, ctx = {}) {
|
||||
if (originalCanStart) return Boolean(originalCanStart.call(spec, tarinai, world, ctx));
|
||||
if (originalCondition) return Boolean(originalCondition.call(spec, tarinai, world, ctx));
|
||||
return true;
|
||||
};
|
||||
|
||||
spec.start = function start(tarinai, world, ctx = {}) {
|
||||
if (originalStart) return originalStart.call(spec, tarinai, world, ctx);
|
||||
if (originalRun) return originalRun.call(spec, tarinai, world, ctx);
|
||||
return true;
|
||||
};
|
||||
|
||||
spec.tick = function tick(tarinai, world, dt = 0, needs = null, ctx = {}) {
|
||||
if (originalTick) return originalTick.call(spec, tarinai, world, dt, needs, ctx);
|
||||
if (originalUpdate) return originalUpdate.call(spec, tarinai, world, dt, needs, ctx);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
spec.finish = function finish(tarinai, world, ctx = {}) {
|
||||
return originalFinish ? originalFinish.call(spec, tarinai, world, ctx) : true;
|
||||
};
|
||||
|
||||
spec.fail = function fail(tarinai, world, ctx = {}) {
|
||||
return originalFail ? originalFail.call(spec, tarinai, world, ctx) : false;
|
||||
};
|
||||
|
||||
spec.text = function text(tarinai, world, ctx = {}) {
|
||||
if (originalText) return String(originalText.call(spec, tarinai, world, ctx) || "");
|
||||
if (ctx.phase === "finished") return spec.phrase;
|
||||
return spec.label;
|
||||
};
|
||||
|
||||
Object.defineProperty(spec, "__actionSpec", {
|
||||
value: true,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
});
|
||||
return spec;
|
||||
}
|
||||
|
||||
function registerTarinaiActionSpecs(definitions = []) {
|
||||
const specs = [];
|
||||
for (const definition of definitions || []) {
|
||||
const spec = normalizeTarinaiActionSpec(definition);
|
||||
registry.set(spec.id, spec);
|
||||
specs.push(spec);
|
||||
}
|
||||
return specs;
|
||||
}
|
||||
|
||||
function registerTarinaiActionSpec(definition = {}) {
|
||||
const spec = normalizeTarinaiActionSpec(definition);
|
||||
registry.set(spec.id, spec);
|
||||
return spec;
|
||||
}
|
||||
|
||||
function getTarinaiActionSpec(id = "") {
|
||||
return registry.get(String(id || "")) || null;
|
||||
}
|
||||
|
||||
function listTarinaiActionSpecs() {
|
||||
return Array.from(registry.values());
|
||||
}
|
||||
|
||||
function selectTarinaiActionTarget(action, tarinai, world, ctx = {}) {
|
||||
const spec = resolveActionSpec(action);
|
||||
if (!spec) return null;
|
||||
return hasOwnFn(spec, "selectTarget") ? spec.selectTarget(tarinai, world, ctx) : (ctx.target ?? tarinai?.target ?? null);
|
||||
}
|
||||
|
||||
function canStartTarinaiAction(action, tarinai, world, ctx = {}) {
|
||||
const spec = resolveActionSpec(action);
|
||||
if (!spec) return false;
|
||||
return hasOwnFn(spec, "canStart") ? Boolean(spec.canStart(tarinai, world, isObject(ctx) ? ctx : {})) : true;
|
||||
}
|
||||
|
||||
function startTarinaiAction(action, tarinai, world, ctx = {}) {
|
||||
const spec = resolveActionSpec(action);
|
||||
if (!spec) return false;
|
||||
const canCtx = isObject(ctx) ? ctx : {};
|
||||
if (hasOwnFn(spec, "canStart") && !spec.canStart(tarinai, world, canCtx)) return false;
|
||||
const nextCtx = buildActionContext(tarinai, world, canCtx, spec);
|
||||
return hasOwnFn(spec, "start") ? spec.start(tarinai, world, nextCtx) : spec.run?.(tarinai, world, nextCtx);
|
||||
}
|
||||
|
||||
function tickTarinaiAction(action, tarinai, world, dt = 0, needs = null, ctx = {}) {
|
||||
const spec = resolveActionSpec(action);
|
||||
if (!spec) return undefined;
|
||||
const nextCtx = buildActionContext(tarinai, world, ctx, spec);
|
||||
return hasOwnFn(spec, "tick") ? spec.tick(tarinai, world, dt, needs, nextCtx) : spec.update?.(tarinai, world, dt, needs, nextCtx);
|
||||
}
|
||||
|
||||
function finishTarinaiAction(action, tarinai, world, ctx = {}) {
|
||||
const spec = resolveActionSpec(action);
|
||||
return hasOwnFn(spec, "finish") ? spec.finish(tarinai, world, ctx) : true;
|
||||
}
|
||||
|
||||
function failTarinaiAction(action, tarinai, world, ctx = {}) {
|
||||
const spec = resolveActionSpec(action);
|
||||
return hasOwnFn(spec, "fail") ? spec.fail(tarinai, world, ctx) : false;
|
||||
}
|
||||
|
||||
function textTarinaiAction(action, tarinai, world, ctx = {}) {
|
||||
const spec = resolveActionSpec(action);
|
||||
if (!spec) return "";
|
||||
return hasOwnFn(spec, "text") ? String(spec.text(tarinai, world, isObject(ctx) ? ctx : {}) || "") : String(spec.label || spec.phrase || spec.id || "");
|
||||
}
|
||||
|
||||
Object.assign(global, {
|
||||
normalizeTarinaiActionSpec,
|
||||
createTarinaiActionSpec: normalizeTarinaiActionSpec,
|
||||
registerTarinaiActionSpec,
|
||||
registerTarinaiActionSpecs,
|
||||
getTarinaiActionSpec,
|
||||
listTarinaiActionSpecs,
|
||||
selectTarinaiActionTarget,
|
||||
canStartTarinaiAction,
|
||||
startTarinaiAction,
|
||||
tickTarinaiAction,
|
||||
finishTarinaiAction,
|
||||
failTarinaiAction,
|
||||
textTarinaiAction,
|
||||
TarinaiActionSpecRegistry: registry,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -24,13 +24,13 @@
|
|||
},
|
||||
|
||||
beginEating(item = null, reason = "") {
|
||||
const text = reason || (item?.type && typeof toolLabel === "function" ? `${toolLabel(item.type)}を食べている` : "食べている");
|
||||
const text = reason || (item?.type && typeof toolLabel === "function" ? `${toolLabel(item.type)}\u3092\u98df\u3079\u3066\u3044\u308b` : "\u98df\u3079\u3066\u3044\u308b");
|
||||
const ok = this.setActionState("eat", {
|
||||
target: item || null,
|
||||
reason: text,
|
||||
sleeping: false,
|
||||
});
|
||||
if (typeof setLiveActionText === "function") setLiveActionText(this, { need: "food", actionId: "eat_food", actionLabel: "食べている", reasonText: text, target: item || null, phase: "perform", source: "behavior" });
|
||||
if (typeof setLiveActionText === "function") setLiveActionText(this, { need: "food", actionId: "eat_food", actionLabel: "\u98df\u3079\u3066\u3044\u308b", reasonText: text, target: item || null, phase: "perform", source: "behavior" });
|
||||
return ok;
|
||||
},
|
||||
|
||||
|
|
@ -48,8 +48,8 @@
|
|||
},
|
||||
|
||||
goIdle(reason = "") {
|
||||
if (typeof clearActiveBehavior === "function") clearActiveBehavior(this, reason || this.thought || "待っている");
|
||||
return this.setActionState("idle", { target: null, reason: reason || this.thought || "待っている", sleeping: false });
|
||||
if (typeof clearActiveBehavior === "function") clearActiveBehavior(this, reason || this.thought || "\u5f85\u3063\u3066\u3044\u308b");
|
||||
return this.setActionState("idle", { target: null, reason: reason || this.thought || "\u5f85\u3063\u3066\u3044\u308b", sleeping: false });
|
||||
},
|
||||
|
||||
startSleeping(target = null, reason = "") {
|
||||
|
|
@ -64,10 +64,8 @@
|
|||
targetId: target?.id || null,
|
||||
};
|
||||
}
|
||||
this.intent = this.intent || { need: "sleep", tiedNeeds: ["sleep"], actionId: target ? "sleep_in_bed" : "sleep_anywhere", actionLabel: reason || "眠っている", reasonText: "ねむいので、眠っている。", startedAt: now };
|
||||
this.currentAction = this.currentAction || { id: this.intent.actionId || "sleep", need: "sleep", startedAt: this.sleepSession.startedAt, minDuration: this.sleepSession.minDuration, lockSeconds: this.sleepSession.minDuration };
|
||||
const text = reason || (target?.type === "nest_box" ? "巣箱の中で眠っている" : "眠っている");
|
||||
if (typeof setLiveActionText === "function") setLiveActionText(this, { need: "sleep", actionId: target ? "sleep_in_bed" : "sleep_anywhere", actionLabel: "眠っている", reasonText: text, target, phase: "perform", source: "behavior" });
|
||||
const text = reason || (target?.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u7720\u3063\u3066\u3044\u308b" : "\u7720\u3063\u3066\u3044\u308b");
|
||||
if (typeof setLiveActionText === "function") setLiveActionText(this, { need: "sleep", actionId: target ? "sleep_in_bed" : "sleep_anywhere", actionLabel: "\u7720\u3063\u3066\u3044\u308b", reasonText: text, target, phase: "perform", source: "behavior" });
|
||||
return this.setActionState("sleep", { target, reason: text, sleeping: true });
|
||||
},
|
||||
|
||||
|
|
@ -82,7 +80,7 @@
|
|||
|
||||
currentBehaviorText() {
|
||||
if (typeof activeBehaviorText === "function") return activeBehaviorText(this);
|
||||
return String(this.activeBehavior?.presentText || this.activeBehavior?.reasonText || this.thought || "").trim();
|
||||
return String((typeof getTarinaiBehaviorText === "function" ? getTarinaiBehaviorText(this) : "") || this.thought || "").trim();
|
||||
},
|
||||
|
||||
enterPanic(opts = {}) {
|
||||
|
|
@ -100,18 +98,23 @@
|
|||
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 (typeof triggerNeedShockReaction === "function") handled = triggerNeedShockReaction(this, threat, opts.reason || "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b");
|
||||
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);
|
||||
const panicReason = opts.reason || this.thought || "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b";
|
||||
this.setActionState("panic", {
|
||||
target,
|
||||
reason: opts.reason || this.thought || "パニックになっている",
|
||||
reason: panicReason,
|
||||
wake: opts.wake !== false,
|
||||
sleeping: false,
|
||||
});
|
||||
if (typeof setLiveActionText === "function") {
|
||||
const causeText = this.lastPanicDetail || (String(panicReason).includes("\u3001") ? String(panicReason).split("\u3001")[0].replace(/\u3066$/, "\u305f") : "\u6016\u3044");
|
||||
setLiveActionText(this, { need: "safety", actionId: "panic_escape", actionLabel: "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", reasonText: panicReason, causeText, target, phase: "perform", source: "behavior" });
|
||||
}
|
||||
this.fearTimer = Math.max(this.fearTimer || 0, 2.4);
|
||||
}
|
||||
if (Number.isFinite(opts.surpriseTimer)) this.surpriseTimer = Math.max(this.surpriseTimer || 0, opts.surpriseTimer);
|
||||
|
|
|
|||
417
js/tarinai_behavior_state.js
Normal file
417
js/tarinai_behavior_state.js
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const Tarinai = global.Tarinai;
|
||||
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_behavior_state.js");
|
||||
|
||||
function isObject(value) {
|
||||
return Boolean(value && typeof value === "object");
|
||||
}
|
||||
|
||||
function finiteOr(value, fallback) {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function localBehaviorTargetId(target = null) {
|
||||
if (!target) return null;
|
||||
if (target.id != null) return target.id;
|
||||
if (Number.isFinite(target.x) && Number.isFinite(target.y)) return `pos:${Math.round(target.x)},${Math.round(target.y)}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
function nowFor(tarinai = null) {
|
||||
return finiteOr(tarinai?.world?.time, 0);
|
||||
}
|
||||
|
||||
function hidden(obj, key, value) {
|
||||
if (!obj) return value;
|
||||
Object.defineProperty(obj, key, {
|
||||
value,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function actionIdFrom(input = {}, legacyIntent = null, legacyAction = null, previous = null) {
|
||||
return String(input.id || input.actionId || legacyIntent?.actionId || legacyAction?.id || previous?.id || "");
|
||||
}
|
||||
|
||||
function normalizeForcedRequestValue(value = null, fallback = null) {
|
||||
const src = isObject(value) ? value : {};
|
||||
const prev = isObject(fallback) ? fallback : {};
|
||||
const uid = String(src.uid || src.forcedId || prev.uid || prev.forcedId || "");
|
||||
const id = String(src.id || src.actionId || prev.id || prev.actionId || "");
|
||||
if (!uid && !id) return null;
|
||||
const request = {
|
||||
uid,
|
||||
id,
|
||||
source: String(src.source || prev.source || "external"),
|
||||
priority: finiteOr(src.priority, finiteOr(prev.priority, 0)),
|
||||
status: String(src.status || prev.status || "active"),
|
||||
reasonText: String(src.reasonText || src.forcedReasonText || prev.reasonText || prev.forcedReasonText || ""),
|
||||
causeText: String(src.causeText || prev.causeText || ""),
|
||||
targetId: src.targetId !== undefined ? src.targetId : (prev.targetId !== undefined ? prev.targetId : null),
|
||||
createdAt: finiteOr(src.createdAt, finiteOr(prev.createdAt, 0)),
|
||||
startedAt: finiteOr(src.startedAt, finiteOr(src.activeSince, finiteOr(prev.startedAt, finiteOr(prev.activeSince, 0)))),
|
||||
activeSince: finiteOr(src.activeSince, finiteOr(src.startedAt, finiteOr(prev.activeSince, finiteOr(prev.startedAt, 0)))),
|
||||
expiresAt: Number.isFinite(Number(src.expiresAt)) ? Number(src.expiresAt) : (Number.isFinite(Number(prev.expiresAt)) ? Number(prev.expiresAt) : undefined),
|
||||
minDuration: finiteOr(src.minDuration, finiteOr(prev.minDuration, 0)),
|
||||
duration: finiteOr(src.duration, finiteOr(prev.duration, 0)),
|
||||
attempt: finiteOr(src.attempt ?? src.attempts, finiteOr(prev.attempt ?? prev.attempts, 0)),
|
||||
failReason: String(src.failReason || prev.failReason || ""),
|
||||
};
|
||||
return request;
|
||||
}
|
||||
|
||||
function normalizeBehaviorValue(input, tarinai = null, previous = null) {
|
||||
if (!isObject(input)) return null;
|
||||
const legacyIntent = isObject(input.intent) ? input.intent : null;
|
||||
const legacyAction = isObject(input.currentAction) ? input.currentAction : null;
|
||||
const now = nowFor(tarinai);
|
||||
const id = actionIdFrom(input, legacyIntent, legacyAction, previous) || String(tarinai?.state || "idle");
|
||||
const need = String(input.need || legacyIntent?.need || legacyAction?.need || previous?.need || "fulfill");
|
||||
const label = String(input.label || input.actionLabel || legacyIntent?.actionLabel || previous?.label || id || "行動している");
|
||||
const reasonText = String(input.reasonText || input.presentText || legacyIntent?.reasonText || previous?.reasonText || label || "");
|
||||
const target = input.target !== undefined ? input.target : tarinai?.target;
|
||||
const startedAt = finiteOr(input.startedAt ?? legacyIntent?.startedAt ?? legacyAction?.startedAt, finiteOr(previous?.startedAt, now));
|
||||
const minDuration = finiteOr(input.minDuration ?? legacyAction?.minDuration, finiteOr(previous?.minDuration, 0.45));
|
||||
const lockSeconds = finiteOr(input.lockSeconds ?? legacyAction?.lockSeconds, finiteOr(previous?.lockSeconds, 1.0));
|
||||
const tiedNeeds = Array.isArray(input.tiedNeeds) && input.tiedNeeds.length
|
||||
? [...input.tiedNeeds]
|
||||
: (Array.isArray(legacyIntent?.tiedNeeds) && legacyIntent.tiedNeeds.length
|
||||
? [...legacyIntent.tiedNeeds]
|
||||
: (Array.isArray(previous?.tiedNeeds) && previous.tiedNeeds.length ? [...previous.tiedNeeds] : [need]));
|
||||
const behavior = {
|
||||
id,
|
||||
need,
|
||||
subNeed: String(input.subNeed || legacyAction?.subNeed || previous?.subNeed || ""),
|
||||
phase: String(input.phase || legacyAction?.phase || previous?.phase || (String(tarinai?.state || "") === "idle" ? "idle" : "active")),
|
||||
state: String(input.state || tarinai?.state || previous?.state || "idle"),
|
||||
label,
|
||||
presentText: String(input.presentText || reasonText || ""),
|
||||
reasonText,
|
||||
targetId: input.targetId !== undefined ? input.targetId : localBehaviorTargetId(target),
|
||||
tiedNeeds,
|
||||
startedAt,
|
||||
updatedAt: finiteOr(input.updatedAt, now),
|
||||
minDuration,
|
||||
lockSeconds,
|
||||
deadlineAt: Number.isFinite(Number(input.deadlineAt)) ? Number(input.deadlineAt) : previous?.deadlineAt,
|
||||
source: String(input.source || previous?.source || "need"),
|
||||
sourceReasonText: String(input.sourceReasonText || input.forcedReasonText || previous?.sourceReasonText || previous?.forcedReasonText || ""),
|
||||
forcedReasonText: String(input.forcedReasonText || input.sourceReasonText || previous?.forcedReasonText || previous?.sourceReasonText || ""),
|
||||
causeText: String(input.causeText || legacyIntent?.causeText || previous?.causeText || ""),
|
||||
forced: Boolean(input.forced ?? previous?.forced ?? false),
|
||||
forcedId: String(input.forcedId || previous?.forcedId || ""),
|
||||
priority: finiteOr(input.priority, finiteOr(previous?.priority, 0)),
|
||||
specId: String(input.specId || input.actionSpecId || previous?.specId || id),
|
||||
specKind: String(input.specKind || previous?.specKind || "need_action"),
|
||||
};
|
||||
const forcedRequest = normalizeForcedRequestValue(
|
||||
input.forcedRequest || input.request || null,
|
||||
behavior.forced || behavior.forcedId || previous?.forcedRequest
|
||||
? { ...(previous?.forcedRequest || {}), uid: behavior.forcedId || previous?.forcedRequest?.uid || "", id: behavior.id, source: behavior.source, priority: behavior.priority, reasonText: behavior.forcedReasonText || behavior.sourceReasonText, causeText: behavior.causeText, targetId: behavior.targetId }
|
||||
: null
|
||||
);
|
||||
behavior.forcedRequest = forcedRequest;
|
||||
if (forcedRequest) {
|
||||
behavior.forced = true;
|
||||
behavior.forcedId = forcedRequest.uid || behavior.forcedId;
|
||||
behavior.source = forcedRequest.source || behavior.source || "external";
|
||||
behavior.priority = finiteOr(forcedRequest.priority, behavior.priority);
|
||||
behavior.forcedReasonText = behavior.forcedReasonText || forcedRequest.reasonText || "";
|
||||
behavior.sourceReasonText = behavior.sourceReasonText || forcedRequest.reasonText || behavior.forcedReasonText || "";
|
||||
behavior.causeText = behavior.causeText || forcedRequest.causeText || "";
|
||||
}
|
||||
if (Number.isFinite(Number(input.serial))) behavior.serial = Number(input.serial);
|
||||
return syncLegacyViews(behavior);
|
||||
}
|
||||
|
||||
function behaviorFromLegacyIntent(intent, currentAction = null, tarinai = null, previous = null) {
|
||||
if (!isObject(intent) && !isObject(currentAction)) return null;
|
||||
const now = nowFor(tarinai);
|
||||
return normalizeBehaviorValue({
|
||||
id: currentAction?.id || intent?.actionId || tarinai?.state || "idle",
|
||||
need: intent?.need || currentAction?.need || previous?.need || "fulfill",
|
||||
subNeed: currentAction?.subNeed || previous?.subNeed || "",
|
||||
phase: currentAction?.phase || previous?.phase || "active",
|
||||
state: tarinai?.state || previous?.state || "idle",
|
||||
label: intent?.actionLabel || previous?.label || "行動している",
|
||||
presentText: intent?.reasonText || previous?.presentText || "",
|
||||
reasonText: intent?.reasonText || previous?.reasonText || intent?.actionLabel || "",
|
||||
causeText: intent?.causeText || previous?.causeText || "",
|
||||
tiedNeeds: Array.isArray(intent?.tiedNeeds) && intent.tiedNeeds.length ? [...intent.tiedNeeds] : undefined,
|
||||
startedAt: intent?.startedAt ?? currentAction?.startedAt ?? now,
|
||||
minDuration: currentAction?.minDuration,
|
||||
lockSeconds: currentAction?.lockSeconds,
|
||||
priority: previous?.priority,
|
||||
source: previous?.source || "legacy",
|
||||
forced: previous?.forced,
|
||||
forcedId: previous?.forcedId,
|
||||
sourceReasonText: previous?.sourceReasonText,
|
||||
forcedReasonText: previous?.forcedReasonText,
|
||||
}, tarinai, previous);
|
||||
}
|
||||
|
||||
function syncLegacyViews(behavior) {
|
||||
if (!isObject(behavior)) return null;
|
||||
const intent = {
|
||||
need: behavior.need || "fulfill",
|
||||
tiedNeeds: Array.isArray(behavior.tiedNeeds) && behavior.tiedNeeds.length ? [...behavior.tiedNeeds] : [behavior.need || "fulfill"],
|
||||
actionId: behavior.id || "idle",
|
||||
actionLabel: behavior.label || behavior.presentText || behavior.reasonText || "行動している",
|
||||
reasonText: behavior.reasonText || behavior.presentText || behavior.label || "",
|
||||
causeText: behavior.causeText || "",
|
||||
startedAt: finiteOr(behavior.startedAt, 0),
|
||||
};
|
||||
const currentAction = {
|
||||
id: behavior.id || "idle",
|
||||
need: behavior.need || intent.need,
|
||||
subNeed: behavior.subNeed || "",
|
||||
startedAt: finiteOr(behavior.startedAt, 0),
|
||||
minDuration: finiteOr(behavior.minDuration, 0.45),
|
||||
lockSeconds: finiteOr(behavior.lockSeconds, 1.0),
|
||||
phase: behavior.phase || "active",
|
||||
specId: behavior.specId || behavior.id || "idle",
|
||||
specKind: behavior.specKind || "need_action",
|
||||
};
|
||||
const forcedRequest = normalizeForcedRequestValue(behavior.forcedRequest || null,
|
||||
behavior.forced || behavior.forcedId ? { uid: behavior.forcedId || "", id: behavior.id || "", source: behavior.source || "external", priority: finiteOr(behavior.priority, 0), reasonText: behavior.forcedReasonText || behavior.sourceReasonText || "", causeText: behavior.causeText || "", targetId: behavior.targetId } : null);
|
||||
behavior.forcedRequest = forcedRequest;
|
||||
const activeForcedBehavior = forcedRequest
|
||||
? { uid: forcedRequest.uid || behavior.forcedId || "", id: forcedRequest.id || behavior.id || "", source: forcedRequest.source || behavior.source || "external", priority: finiteOr(forcedRequest.priority, finiteOr(behavior.priority, 0)) }
|
||||
: null;
|
||||
hidden(behavior, "_intent", intent);
|
||||
hidden(behavior, "_currentAction", currentAction);
|
||||
hidden(behavior, "_activeForcedBehavior", activeForcedBehavior);
|
||||
return behavior;
|
||||
}
|
||||
|
||||
function normalizeBehaviorState(opts = {}, tarinai = null) {
|
||||
const previous = isObject(tarinai?.behavior) ? tarinai.behavior : null;
|
||||
let behavior = normalizeBehaviorValue(opts.behavior || opts.activeBehavior || null, tarinai, previous);
|
||||
if (!behavior) behavior = behaviorFromLegacyIntent(opts.intent || null, opts.currentAction || null, tarinai, previous);
|
||||
if (!behavior) return null;
|
||||
if (isObject(opts.intent)) mergeIntent(tarinai, opts.intent, behavior);
|
||||
if (isObject(opts.currentAction)) mergeCurrentAction(tarinai, opts.currentAction, behavior);
|
||||
if (isObject(opts.activeForcedBehavior)) mergeActiveForcedBehavior(tarinai, opts.activeForcedBehavior, behavior);
|
||||
return syncLegacyViews(behavior);
|
||||
}
|
||||
|
||||
function ensureBehavior(tarinai) {
|
||||
if (!tarinai) return null;
|
||||
if (isObject(tarinai.behavior)) return syncLegacyViews(tarinai.behavior);
|
||||
return null;
|
||||
}
|
||||
|
||||
function assignBehavior(tarinai, value) {
|
||||
if (!tarinai) return null;
|
||||
const next = normalizeBehaviorValue(value, tarinai, tarinai.behavior || null);
|
||||
tarinai.behavior = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
function getTarinaiBehavior(tarinai) {
|
||||
return ensureBehavior(tarinai);
|
||||
}
|
||||
|
||||
function getTarinaiBehaviorId(tarinai, fallback = "") {
|
||||
const b = ensureBehavior(tarinai);
|
||||
return String(b?.id || fallback || "");
|
||||
}
|
||||
|
||||
function getTarinaiBehaviorNeed(tarinai, fallback = "") {
|
||||
const b = ensureBehavior(tarinai);
|
||||
return String(b?.need || fallback || "");
|
||||
}
|
||||
|
||||
function getTarinaiBehaviorTiedNeeds(tarinai, fallbackNeed = "fulfill") {
|
||||
const b = ensureBehavior(tarinai);
|
||||
if (Array.isArray(b?.tiedNeeds) && b.tiedNeeds.length) return [...b.tiedNeeds];
|
||||
return [String(b?.need || fallbackNeed || "fulfill")];
|
||||
}
|
||||
|
||||
function getTarinaiBehaviorText(tarinai, fallback = "") {
|
||||
const b = ensureBehavior(tarinai);
|
||||
return String(b?.presentText || b?.reasonText || b?.label || fallback || "").trim();
|
||||
}
|
||||
|
||||
function patchTarinaiBehavior(tarinai, fields = {}) {
|
||||
if (!tarinai || !isObject(fields)) return ensureBehavior(tarinai);
|
||||
const previous = ensureBehavior(tarinai) || {};
|
||||
const next = normalizeBehaviorValue({ ...previous, ...fields }, tarinai, previous);
|
||||
tarinai.behavior = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
function clearTarinaiBehavior(tarinai, opts = {}) {
|
||||
if (!tarinai) return null;
|
||||
tarinai.behavior = null;
|
||||
if (opts.clearTarget) tarinai.target = null;
|
||||
if (opts.reason) tarinai.thought = String(opts.reason || "");
|
||||
return null;
|
||||
}
|
||||
|
||||
function setTarinaiForcedBehavior(tarinai, value = null) {
|
||||
return mergeActiveForcedBehavior(tarinai, value || null);
|
||||
}
|
||||
|
||||
function mergeIntent(tarinai, value, behavior = null) {
|
||||
if (!tarinai || !isObject(value)) return null;
|
||||
const b = behavior || ensureBehavior(tarinai) || normalizeBehaviorValue({
|
||||
id: value.actionId || tarinai.state || "idle",
|
||||
need: value.need || "fulfill",
|
||||
label: value.actionLabel || value.reasonText || "行動している",
|
||||
reasonText: value.reasonText || value.actionLabel || "",
|
||||
tiedNeeds: value.tiedNeeds,
|
||||
causeText: value.causeText || "",
|
||||
startedAt: value.startedAt,
|
||||
source: "intent",
|
||||
}, tarinai, null);
|
||||
if (!b) return null;
|
||||
if (value.actionId != null) b.id = String(value.actionId || b.id || "idle");
|
||||
if (value.need != null) b.need = String(value.need || b.need || "fulfill");
|
||||
if (value.actionLabel != null) b.label = String(value.actionLabel || b.label || "");
|
||||
if (value.reasonText != null) {
|
||||
b.reasonText = String(value.reasonText || "");
|
||||
b.presentText = b.presentText || b.reasonText;
|
||||
}
|
||||
if (value.causeText != null) b.causeText = String(value.causeText || "");
|
||||
if (Array.isArray(value.tiedNeeds) && value.tiedNeeds.length) b.tiedNeeds = [...value.tiedNeeds];
|
||||
if (Number.isFinite(Number(value.startedAt))) b.startedAt = Number(value.startedAt);
|
||||
if (value.specId != null) b.specId = String(value.specId || b.id || "idle");
|
||||
if (value.specKind != null) b.specKind = String(value.specKind || b.specKind || "need_action");
|
||||
tarinai.behavior = syncLegacyViews(b);
|
||||
return tarinai.behavior._intent;
|
||||
}
|
||||
|
||||
function mergeCurrentAction(tarinai, value, behavior = null) {
|
||||
if (!tarinai || !isObject(value)) return null;
|
||||
const b = behavior || ensureBehavior(tarinai) || normalizeBehaviorValue({
|
||||
id: value.id || tarinai.state || "idle",
|
||||
need: value.need || "fulfill",
|
||||
subNeed: value.subNeed || "",
|
||||
startedAt: value.startedAt,
|
||||
minDuration: value.minDuration,
|
||||
lockSeconds: value.lockSeconds,
|
||||
phase: value.phase,
|
||||
source: "currentAction",
|
||||
}, tarinai, null);
|
||||
if (!b) return null;
|
||||
if (value.id != null) b.id = String(value.id || b.id || "idle");
|
||||
if (value.need != null) b.need = String(value.need || b.need || "fulfill");
|
||||
if (value.subNeed != null) b.subNeed = String(value.subNeed || "");
|
||||
if (value.phase != null) b.phase = String(value.phase || b.phase || "active");
|
||||
if (Number.isFinite(Number(value.startedAt))) b.startedAt = Number(value.startedAt);
|
||||
if (Number.isFinite(Number(value.minDuration))) b.minDuration = Number(value.minDuration);
|
||||
if (Number.isFinite(Number(value.lockSeconds))) b.lockSeconds = Number(value.lockSeconds);
|
||||
if (value.specId != null) b.specId = String(value.specId || b.id || "idle");
|
||||
if (value.specKind != null) b.specKind = String(value.specKind || b.specKind || "need_action");
|
||||
tarinai.behavior = syncLegacyViews(b);
|
||||
return tarinai.behavior._currentAction;
|
||||
}
|
||||
|
||||
function mergeActiveForcedBehavior(tarinai, value, behavior = null) {
|
||||
if (!tarinai) return null;
|
||||
const b = behavior || ensureBehavior(tarinai);
|
||||
if (!b) return null;
|
||||
if (!value) {
|
||||
b.forced = false;
|
||||
b.forcedId = "";
|
||||
b.forcedRequest = null;
|
||||
b.sourceReasonText = b.sourceReasonText || "";
|
||||
b.forcedReasonText = b.forcedReasonText || "";
|
||||
tarinai.behavior = syncLegacyViews(b);
|
||||
return null;
|
||||
}
|
||||
const request = normalizeForcedRequestValue(value, { uid: b.forcedId || "", id: b.id || "idle", source: b.source || "external", priority: b.priority || 0, reasonText: b.forcedReasonText || b.sourceReasonText || "", causeText: b.causeText || "", targetId: b.targetId });
|
||||
b.forced = true;
|
||||
b.forcedRequest = request;
|
||||
b.forcedId = String(request?.uid || value.uid || value.forcedId || b.forcedId || "");
|
||||
if (request?.id || value.id != null) b.id = String(request?.id || value.id || b.id || "idle");
|
||||
b.source = String(request?.source || value.source || b.source || "external");
|
||||
b.priority = finiteOr(request?.priority, finiteOr(value.priority, finiteOr(b.priority, 0)));
|
||||
b.forcedReasonText = b.forcedReasonText || request?.reasonText || "";
|
||||
b.sourceReasonText = b.sourceReasonText || request?.reasonText || b.forcedReasonText || "";
|
||||
b.causeText = b.causeText || request?.causeText || "";
|
||||
tarinai.behavior = syncLegacyViews(b);
|
||||
return tarinai.behavior._activeForcedBehavior;
|
||||
}
|
||||
|
||||
function getTarinaiForcedBehavior(tarinai) {
|
||||
const b = ensureBehavior(tarinai);
|
||||
if (!b || !b.forced) return null;
|
||||
return b.forcedRequest || b._activeForcedBehavior || null;
|
||||
}
|
||||
|
||||
function isTarinaiBehaviorForced(tarinai) {
|
||||
const b = ensureBehavior(tarinai);
|
||||
return Boolean(b?.forced || b?.forcedId || b?.forcedRequest);
|
||||
}
|
||||
|
||||
Object.defineProperties(Tarinai.prototype, {
|
||||
activeBehavior: {
|
||||
configurable: true,
|
||||
get() {
|
||||
return ensureBehavior(this);
|
||||
},
|
||||
set(value) {
|
||||
if (!value) { clearTarinaiBehavior(this); return; }
|
||||
assignBehavior(this, value);
|
||||
},
|
||||
},
|
||||
intent: {
|
||||
configurable: true,
|
||||
get() {
|
||||
const b = ensureBehavior(this);
|
||||
return b ? b._intent : null;
|
||||
},
|
||||
set(value) {
|
||||
if (!value) { clearTarinaiBehavior(this); return; }
|
||||
mergeIntent(this, value);
|
||||
},
|
||||
},
|
||||
currentAction: {
|
||||
configurable: true,
|
||||
get() {
|
||||
const b = ensureBehavior(this);
|
||||
return b ? b._currentAction : null;
|
||||
},
|
||||
set(value) {
|
||||
if (!value) { clearTarinaiBehavior(this); return; }
|
||||
mergeCurrentAction(this, value);
|
||||
},
|
||||
},
|
||||
activeForcedBehavior: {
|
||||
configurable: true,
|
||||
get() {
|
||||
const b = ensureBehavior(this);
|
||||
return b ? b._activeForcedBehavior : null;
|
||||
},
|
||||
set(value) {
|
||||
mergeActiveForcedBehavior(this, value || null);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
global.normalizeTarinaiBehaviorState = normalizeBehaviorState;
|
||||
global.normalizeTarinaiBehaviorValue = normalizeBehaviorValue;
|
||||
global.syncTarinaiBehaviorLegacyViews = syncLegacyViews;
|
||||
global.getTarinaiBehavior = getTarinaiBehavior;
|
||||
global.currentTarinaiBehavior = getTarinaiBehavior;
|
||||
global.getTarinaiBehaviorId = getTarinaiBehaviorId;
|
||||
global.getTarinaiBehaviorNeed = getTarinaiBehaviorNeed;
|
||||
global.getTarinaiBehaviorTiedNeeds = getTarinaiBehaviorTiedNeeds;
|
||||
global.getTarinaiBehaviorText = getTarinaiBehaviorText;
|
||||
global.patchTarinaiBehavior = patchTarinaiBehavior;
|
||||
global.setTarinaiBehavior = assignBehavior;
|
||||
global.clearTarinaiBehavior = clearTarinaiBehavior;
|
||||
global.normalizeTarinaiForcedRequest = normalizeForcedRequestValue;
|
||||
global.getTarinaiForcedBehavior = getTarinaiForcedBehavior;
|
||||
global.isTarinaiBehaviorForced = isTarinaiBehaviorForced;
|
||||
global.setTarinaiForcedBehavior = setTarinaiForcedBehavior;
|
||||
global.clearTarinaiForcedBehavior = function clearTarinaiForcedBehavior(tarinai) { return setTarinaiForcedBehavior(tarinai, null); };
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
this.sleepDiseaseCooldown = 8;
|
||||
this.sleepDiseaseAnchorX = this.x;
|
||||
this.sleepDiseaseAnchorY = this.y;
|
||||
this.startSleeping(null, "ねむり病で眠り続けている");
|
||||
this.startSleeping(null, "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b");
|
||||
this.fightTimer = 0;
|
||||
this.intimidateTimer = 0;
|
||||
this.fightTargetIds = [];
|
||||
|
|
@ -104,7 +104,7 @@
|
|||
this.sleepDiseaseCooldown = 14;
|
||||
this.sleepDiseaseAnchorX = null;
|
||||
this.sleepDiseaseAnchorY = null;
|
||||
this.goIdle(reason || "ねむり病が治った");
|
||||
this.goIdle(reason || "\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f");
|
||||
this.sleeping = false;
|
||||
this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 10);
|
||||
audio.heal?.();
|
||||
|
|
@ -181,7 +181,7 @@
|
|||
const beingDraggedByAnt = Boolean((this.antDraggedTimer || 0) > 0 && this.antDraggedBy);
|
||||
if (!Number.isFinite(this.sleepDiseaseAnchorX)) this.sleepDiseaseAnchorX = this.x;
|
||||
if (!Number.isFinite(this.sleepDiseaseAnchorY)) this.sleepDiseaseAnchorY = this.y;
|
||||
this.startSleeping(null, beingDraggedByAnt ? "眠ったままアリに運ばれている" : "ねむり病で眠り続けている");
|
||||
this.startSleeping(null, beingDraggedByAnt ? "\u7720\u3063\u305f\u307e\u307e\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b" : "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b");
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
if (beingDraggedByAnt) {
|
||||
|
|
@ -211,8 +211,8 @@
|
|||
|
||||
wantsNestBox(box = null) {
|
||||
if (!box || box.dead || box.type !== "nest_box") return false;
|
||||
if (this.insideNestBoxId === box.id && (this.world?.time || 0) < (this.nestBoxStayUntil || -Infinity)) return true;
|
||||
if (!(this.state === "sleep" || this.state === "seek_bed")) return false;
|
||||
if (this.insideNestBoxId === box.id && (this.world?.time || 0) < (this.nestBoxStayUntil || -Infinity) && (this.state === "sleep" || this.state === "seek_bed" || this.sleeping)) return true;
|
||||
if (!(this.state === "sleep" || this.state === "seek_bed" || this.sleeping)) return false;
|
||||
return this.target === box || this.insideNestBoxId === box.id;
|
||||
},
|
||||
|
||||
|
|
@ -228,7 +228,7 @@
|
|||
}
|
||||
this.insideNestBoxId = box.id;
|
||||
this.target = box;
|
||||
if (this.state === "seek_bed") this.startSleeping(box, "巣箱の中で眠っている");
|
||||
if (this.state === "seek_bed") this.startSleeping(box, "\u5de3\u7bb1\u306e\u4e2d\u3067\u7720\u3063\u3066\u3044\u308b");
|
||||
if (this.state === "sleep") this.sleeping = true;
|
||||
const inner = this.world?.nestBoxInnerPoint ? this.world.nestBoxInnerPoint(box, this) : { x: box.x, y: box.y };
|
||||
const pull = clamp(dt * (alreadyInside ? 10.0 : 13.5), 0, 1);
|
||||
|
|
@ -278,8 +278,8 @@
|
|||
}
|
||||
|
||||
if (currentBox) {
|
||||
if ((this.world?.time || 0) < (this.nestBoxStayUntil || -Infinity)) {
|
||||
this.startSleeping(box, "巣箱の中で眠っている");
|
||||
if ((this.world?.time || 0) < (this.nestBoxStayUntil || -Infinity) && (this.state === "sleep" || this.state === "seek_bed" || this.sleeping)) {
|
||||
this.startSleeping(box, "\u5de3\u7bb1\u306e\u4e2d\u3067\u7720\u3063\u3066\u3044\u308b");
|
||||
this.sleeping = true;
|
||||
this.target = currentBox;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@
|
|||
this.targetGiveupKey = key;
|
||||
this.targetGiveupUntil = this.world.time + 8;
|
||||
this.target = null;
|
||||
this.goIdle?.("柵に阻まれて行き先を変えている");
|
||||
this.goIdle?.("\u67f5\u306b\u963b\u307e\u308c\u3066\u884c\u304d\u5148\u3092\u5909\u3048\u3066\u3044\u308b");
|
||||
this.wanderAngle += rand(-1.4, 1.4);
|
||||
return;
|
||||
}
|
||||
|
|
@ -379,7 +379,7 @@
|
|||
this.targetGiveupKey = key;
|
||||
this.targetGiveupUntil = this.world.time + 12;
|
||||
this.target = null;
|
||||
this.goIdle?.("辿り着けず、あきらめた");
|
||||
this.goIdle?.("\u8fbf\u308a\u7740\u3051\u305a\u3001\u3042\u304d\u3089\u3081\u305f");
|
||||
this.wanderAngle += rand(-2.2, 2.2);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -218,14 +218,22 @@
|
|||
applyFoodHungerRelief(amount = 0) {
|
||||
const raw = Math.max(0, Number(amount) || 0);
|
||||
const actual = this.hasItemEffect("laxative") ? raw * (EFFECT_REGISTRY?.modifier?.("laxative", "hungerReliefMultiplier", 0) ?? 0) : raw;
|
||||
if (actual > 0.2) this.lastAteAt = this.world?.time || 0;
|
||||
if (actual >= 6) {
|
||||
const now = this.world?.time || 0;
|
||||
if (actual > 0.2) this.lastAteAt = now;
|
||||
if (actual >= 3.5) {
|
||||
const day = Number(this.world?.config?.dayLength || CONFIG?.dayLength || 120) || 120;
|
||||
const base = day / 4;
|
||||
const base = day / 3.65;
|
||||
const jitter = typeof stableUnit === "function" ? (0.82 + stableUnit(this.familyKey || this.id, "meal-cooldown") * 0.36) : 1;
|
||||
this.mealCooldownUntil = Math.max(this.mealCooldownUntil || 0, (this.world?.time || 0) + base * jitter);
|
||||
this.mealCooldownUntil = Math.max(this.mealCooldownUntil || 0, now + base * jitter);
|
||||
this.needSatisfaction = this.needSatisfaction || (typeof createDefaultNeeds === "function" ? createDefaultNeeds() : {});
|
||||
this.needSatisfaction.food = Math.max(this.needSatisfaction.food || 0, Math.min(88, actual * 1.85));
|
||||
this.actionCooldowns = this.actionCooldowns || {};
|
||||
this.actionCooldowns.food = Math.max(this.actionCooldowns.food || 0, 5.2);
|
||||
}
|
||||
this.hunger -= actual;
|
||||
if (actual >= 3.5 && this.hunger < 68 && (typeof getTarinaiBehaviorNeed === "function" ? getTarinaiBehaviorNeed(this) : this.behavior?.need) === "food" && !((typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(this) : this.behavior)?.forced)) {
|
||||
this.intentLockTimer = Math.min(this.intentLockTimer || 0, 0.25);
|
||||
}
|
||||
return actual;
|
||||
},
|
||||
|
||||
|
|
@ -267,7 +275,7 @@
|
|||
if (!this.world || this.dead) return;
|
||||
if (this.hasLodgedPinEffect?.("blocksZunchi")) {
|
||||
this.oshiriByoZunchiStock = Math.min(24, Math.max(0, this.oshiriByoZunchiStock || 0) + 1);
|
||||
this.thought = (this.oshiriByoZunchiStock || 0) >= 6 ? "おしり鋲でずんちが溜まってつらい" : "おしり鋲でずんちを我慢している";
|
||||
this.thought = (this.oshiriByoZunchiStock || 0) >= 6 ? "\u304a\u3057\u308a\u92f2\u3067\u305a\u3093\u3061\u304c\u6e9c\u307e\u3063\u3066\u3064\u3089\u3044" : "\u304a\u3057\u308a\u92f2\u3067\u305a\u3093\u3061\u3092\u6211\u6162\u3057\u3066\u3044\u308b";
|
||||
return;
|
||||
}
|
||||
const side = this.facingDir ? this.facingDir() : (this.facing || 1);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -300,16 +300,6 @@
|
|||
drawDawnRimLight(ctx, dw, dh, lightState);
|
||||
if (faceRight) ctx.scale(-1, 1);
|
||||
|
||||
if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 3 && (this.hunger > 90 || this.loneliness > 88 || this.energy < 16)) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.80;
|
||||
ctx.font = `${Math.max(10, drawW * 0.14)}px ui-rounded, sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillStyle = "rgba(42,36,29,0.72)";
|
||||
const mark = this.hunger > 90 ? "..." : (this.energy < 16 ? "Z" : "?");
|
||||
ctx.fillText(mark, 0, -drawH * 0.72);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@
|
|||
if (typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * 36, safety: dt * 5 }, o);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(o, { social: dt * 30, safety: dt * 5 }, this);
|
||||
if (typeof queueForcedTarinaiBehavior === "function") {
|
||||
queueForcedTarinaiBehavior(this, "fight_rival", { target: o, source: "fight_mochi", priority: 98, ttl: 8, reasonText: `${o.name || "相手"}と喧嘩しようとしている` });
|
||||
queueForcedTarinaiBehavior(o, "fight_rival", { target: this, source: "fight_mochi", priority: 98, ttl: 8, reasonText: `${this.name || "相手"}と喧嘩しようとしている` });
|
||||
queueForcedTarinaiBehavior(this, "fight_rival", { target: o, source: "fight_mochi", priority: 180, ttl: 8, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
|
||||
queueForcedTarinaiBehavior(o, "fight_rival", { target: this, source: "fight_mochi", priority: 180, ttl: 8, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,14 +136,14 @@
|
|||
const birthPeace = (this.postBirthPeaceTimer || 0) > 0 || (o.postBirthPeaceTimer || 0) > 0;
|
||||
const familyFightBlocked = this.world.areParentChild ? this.world.areParentChild(this, o) : false;
|
||||
const pairFightBlocked = this.world.areCoParents ? this.world.areCoParents(this, o) : false;
|
||||
if (this.world.canFightPair?.(this, o) && d < 56 && !familyFightBlocked && !pairFightBlocked && !birthPeace && fightRisk && this.fightCooldown <= 0 && o.fightCooldown <= 0 && Math.random() < dt * 0.23 * personalityRisk * fearBrake * friendBrake * conflictBias) {
|
||||
if (this.world.canFightPair?.(this, o) && d < 56 && !familyFightBlocked && !pairFightBlocked && !birthPeace && fightRisk && this.fightCooldown <= 0 && o.fightCooldown <= 0 && Math.random() < dt * (battleDrug ? 0.23 : 0.085) * personalityRisk * fearBrake * friendBrake * conflictBias) {
|
||||
this.conflictTargetId = o.id;
|
||||
o.conflictTargetId = this.id;
|
||||
const urge = 48 + (battleDrug ? 34 : 0) + (mixedZunchiSlave ? 12 : 0);
|
||||
const urge = 34 + (battleDrug ? 34 : 0) + (mixedZunchiSlave ? 10 : 0);
|
||||
this.conflictUrge = Math.max(this.conflictUrge || 0, urge);
|
||||
o.conflictUrge = Math.max(o.conflictUrge || 0, urge * 0.82);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * urge * 0.32, safety: dt * (battleDrug ? 4 : 1.6) }, o);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(o, { social: dt * urge * 0.24, safety: dt * (battleDrug ? 4 : 1.6) }, this);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * urge * (battleDrug ? 0.32 : 0.18), safety: dt * (battleDrug ? 4 : 0.9) }, o);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(o, { social: dt * urge * (battleDrug ? 0.24 : 0.14), safety: dt * (battleDrug ? 4 : 0.9) }, this);
|
||||
}
|
||||
|
||||
const compatibleBirthStatus = this.isZunchiSlave === o.isZunchiSlave;
|
||||
|
|
@ -156,10 +156,10 @@
|
|||
applyNeedShock(o, { social: dt * (loveDrug ? 34 : 14) }, this);
|
||||
}
|
||||
if (loveDrug && typeof queueForcedTarinaiBehavior === "function") {
|
||||
queueForcedTarinaiBehavior(this, "approach_mate", { target: o, source: "love_mochi", priority: 92, ttl: 10, reasonText: `${o.name || "相手"}と繁殖したくて近づいている` });
|
||||
queueForcedTarinaiBehavior(o, "approach_mate", { target: this, source: "love_mochi", priority: 92, ttl: 10, reasonText: `${this.name || "相手"}と繁殖したくて近づいている` });
|
||||
queueForcedTarinaiBehavior(this, "approach_mate", { target: o, source: "love_mochi", priority: 150, ttl: 10, causeText: "\u3078\u3053\u9905\u306e\u52b9\u679c" });
|
||||
queueForcedTarinaiBehavior(o, "approach_mate", { target: this, source: "love_mochi", priority: 150, ttl: 10, causeText: "\u3078\u3053\u9905\u306e\u52b9\u679c" });
|
||||
}
|
||||
const mateIntent = this.activeBehavior?.id === "approach_mate" || o.activeBehavior?.id === "approach_mate" || this.intent?.actionId === "approach_mate" || o.intent?.actionId === "approach_mate";
|
||||
const mateIntent = (typeof getTarinaiBehaviorId === "function" ? getTarinaiBehaviorId(this) : this.behavior?.id) === "approach_mate" || (typeof getTarinaiBehaviorId === "function" ? getTarinaiBehaviorId(o) : o.behavior?.id) === "approach_mate";
|
||||
if (mateIntent && Math.random() < dt * (loveDrug ? 0.78 : 0.30)) {
|
||||
this.world.startBirthRitual(this, o);
|
||||
}
|
||||
|
|
@ -197,7 +197,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);
|
||||
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 });
|
||||
if (crowdedSleep && typeof applyNeedShock === "function") applyNeedShock(this, { safety: dt * Math.max(1, bedCrowd - 5) });
|
||||
this.hunger = clamp(this.hunger + dt * 0.018, 0, 115);
|
||||
if (this.target && !this.target.dead && this.target.type === "nest_box") {
|
||||
this.updateNestBoxPresence(dt);
|
||||
|
|
@ -276,9 +276,9 @@
|
|||
const d = Math.hypot(dx, dy) || 1;
|
||||
if (this.state === "wander" && this.target?.detour && d < Math.max(22, (this.radius || 20) * 0.9)) {
|
||||
if (typeof applyNeedSatisfaction === "function") applyNeedSatisfaction(this, { fulfill: 6 }, "wander");
|
||||
this.currentAction = null;
|
||||
if (typeof clearTarinaiBehavior === "function") clearTarinaiBehavior(this, { reason: this.thought }); else this.behavior = null;
|
||||
this.intentLockTimer = 0;
|
||||
this.goIdle?.("少し歩いた");
|
||||
this.goIdle?.("\u5c11\u3057\u6b69\u3044\u305f");
|
||||
return;
|
||||
}
|
||||
if (this.state === "seek_bed" && this.isSleepFurniture(this.target)) {
|
||||
|
|
@ -406,11 +406,11 @@
|
|||
this.dead = true;
|
||||
this.deathReason = finalReason;
|
||||
this.world.deadCount += 1;
|
||||
this.world.items.push(new Item("trace", this.x, this.y));
|
||||
this.world.items.push(new Item("splat", this.x, this.y));
|
||||
this.world.addItem?.(new Item("trace", this.x, this.y), "tarinai-death-trace") || this.world.items.push(new Item("trace", this.x, this.y));
|
||||
this.world.addItem?.(new Item("splat", this.x, this.y), "tarinai-death-splat") || this.world.items.push(new Item("splat", this.x, this.y));
|
||||
if (Math.random() < 0.55) {
|
||||
const spot = this.world.findGrassPlantingSpot(this.x, this.y, { allowOriginal: false, minRadius: 26, maxRadius: 76 });
|
||||
if (spot) this.world.items.push(new Item("grass", spot.x, spot.y));
|
||||
if (spot) this.world.addItem?.(new Item("grass", spot.x, spot.y), "tarinai-death-grass") || this.world.items.push(new Item("grass", spot.x, spot.y));
|
||||
}
|
||||
this.world.markDead(this, finalReason);
|
||||
this.releasedLiveToken = this.liveToken || 0;
|
||||
|
|
|
|||
|
|
@ -53,34 +53,34 @@ const TEXT_CATALOG = {
|
|||
|
||||
|
||||
TEXT_CATALOG.stateLabels = Object.freeze({
|
||||
idle: "待機",
|
||||
seek_food: "食べ物へ向かう",
|
||||
seek_bed: "寝床へ向かう",
|
||||
play_ball: "ボール遊び",
|
||||
seek_water: "水を探す",
|
||||
zunchi_sick: "ずんち病",
|
||||
fight_sick: "きずつき病",
|
||||
system: "ずんち病",
|
||||
seek_friend: "仲間を探す",
|
||||
follow_parent: "親についていく",
|
||||
cursor_friend: "カーソルについていく",
|
||||
cursor_enemy: "カーソルを避ける",
|
||||
fight: "喧嘩中",
|
||||
sleep: "睡眠中",
|
||||
eat: "食事中",
|
||||
panic: "パニック",
|
||||
intimidate: "威嚇中",
|
||||
birth_ritual: "誕生前",
|
||||
sunbath: "日光浴中",
|
||||
ant_attack: "アリ攻撃中",
|
||||
ant_intimidate: "アリ威嚇中",
|
||||
frozen: "冷凍中",
|
||||
idle: "\u5f85\u6a5f",
|
||||
seek_food: "\u98df\u3079\u7269\u3078\u5411\u304b\u3046",
|
||||
seek_bed: "\u5bdd\u5e8a\u3078\u5411\u304b\u3046",
|
||||
play_ball: "\u30dc\u30fc\u30eb\u904a\u3073",
|
||||
seek_water: "\u6c34\u3092\u63a2\u3059",
|
||||
zunchi_sick: "\u305a\u3093\u3061\u75c5",
|
||||
fight_sick: "\u304d\u305a\u3064\u304d\u75c5",
|
||||
system: "\u305a\u3093\u3061\u75c5",
|
||||
seek_friend: "\u4ef2\u9593\u3092\u63a2\u3059",
|
||||
follow_parent: "\u89aa\u306b\u3064\u3044\u3066\u3044\u304f",
|
||||
cursor_friend: "\u30ab\u30fc\u30bd\u30eb\u306b\u3064\u3044\u3066\u3044\u304f",
|
||||
cursor_enemy: "\u30ab\u30fc\u30bd\u30eb\u3092\u907f\u3051\u308b",
|
||||
fight: "\u55a7\u5629\u4e2d",
|
||||
sleep: "\u7761\u7720\u4e2d",
|
||||
eat: "\u98df\u4e8b\u4e2d",
|
||||
panic: "\u30d1\u30cb\u30c3\u30af",
|
||||
intimidate: "\u5a01\u5687\u4e2d",
|
||||
birth_ritual: "\u8a95\u751f\u524d",
|
||||
sunbath: "\u65e5\u5149\u6d74\u4e2d",
|
||||
ant_attack: "\u30a2\u30ea\u653b\u6483\u4e2d",
|
||||
ant_intimidate: "\u30a2\u30ea\u5a01\u5687\u4e2d",
|
||||
frozen: "\u51b7\u51cd\u4e2d",
|
||||
});
|
||||
|
||||
TEXT_CATALOG.targetLabelOverrides = Object.freeze({
|
||||
system: "ずんち病",
|
||||
zunchi_sick: "ずんち病",
|
||||
fight_sick: "きずつき病",
|
||||
system: "\u305a\u3093\u3061\u75c5",
|
||||
zunchi_sick: "\u305a\u3093\u3061\u75c5",
|
||||
fight_sick: "\u304d\u305a\u3064\u304d\u75c5",
|
||||
});
|
||||
// Backward-compatible alias. Tool/item labels come from TOOL_DEFINITIONS via toolLabel().
|
||||
TEXT_CATALOG.targetLabels = TEXT_CATALOG.targetLabelOverrides;
|
||||
|
|
@ -90,53 +90,53 @@ TEXT_CATALOG.stateLabel = function stateLabelFromCatalog(s) {
|
|||
};
|
||||
|
||||
TEXT_CATALOG.targetLabel = function targetLabelFromCatalog(target) {
|
||||
if (!target) return "なし";
|
||||
if (!target) return "\u306a\u3057";
|
||||
if (target.name) return target.name;
|
||||
if (target.kind === "queen") return "女王アリ";
|
||||
if (target.kind === "worker") return "働きアリ";
|
||||
if (target.kind === "queen") return "\u5973\u738b\u30a2\u30ea";
|
||||
if (target.kind === "worker") return "\u50cd\u304d\u30a2\u30ea";
|
||||
if (target.type) {
|
||||
const type = String(target.type || "");
|
||||
if (TEXT_CATALOG.targetLabelOverrides?.[type]) return TEXT_CATALOG.targetLabelOverrides[type];
|
||||
const label = typeof toolLabel === "function" ? toolLabel(type) : "";
|
||||
return label || type;
|
||||
}
|
||||
if (Number.isFinite(target.x) && Number.isFinite(target.y)) return "位置";
|
||||
return "不明";
|
||||
if (Number.isFinite(target.x) && Number.isFinite(target.y)) return "\u4f4d\u7f6e";
|
||||
return "\u4e0d\u660e";
|
||||
};
|
||||
|
||||
TEXT_CATALOG.reasonLabel = function reasonLabelFromCatalog(t) {
|
||||
if (!t) return "なし";
|
||||
if ((t.pinchThoughtTimer || 0) > 0) return "つままれている";
|
||||
const behaviorText = typeof activeBehaviorText === "function" ? activeBehaviorText(t) : String(t.activeBehavior?.presentText || t.activeBehavior?.reasonText || "").trim();
|
||||
if (!t) return "\u306a\u3057";
|
||||
if ((t.pinchThoughtTimer || 0) > 0) return "\u3064\u307e\u307e\u308c\u3066\u3044\u308b";
|
||||
const behaviorText = typeof activeBehaviorText === "function" ? activeBehaviorText(t) : String((typeof getTarinaiBehaviorText === "function" ? getTarinaiBehaviorText(t) : (t.behavior?.presentText || t.behavior?.reasonText || ""))).trim();
|
||||
if (behaviorText) return behaviorText;
|
||||
const target = TEXT_CATALOG.targetLabel(t.target);
|
||||
if (t.state === "eat") return target !== "なし" ? `${target}を食べている` : "食べている";
|
||||
if (t.state === "sleep") return t.target?.type === "nest_box" ? "巣箱の中で体力を回復している" : "体力を回復している";
|
||||
if (t.state === "panic") return t.target ? `${target}から逃げている` : "怖がっている";
|
||||
if (t.state === "intimidate") return target !== "なし" ? `${target}を威嚇している` : "相手を威嚇している";
|
||||
if (t.state === "ant_intimidate") return "アリを威嚇している";
|
||||
if (t.state === "ant_attack") return "アリを攻撃している";
|
||||
if (t.state === "birth_ritual") return t.target?.name ? `${t.target.name}と繁殖の前ぶれをしている` : "繁殖の前ぶれをしている";
|
||||
if (t.state === "sunbath") return "晴れた日なたでストレスを下げている";
|
||||
if (t.state === "frozen") return "フィールド外で冷凍保存されている";
|
||||
if (t.state === "seek_food") return target !== "なし" ? `${target}を探している` : "お腹がすいている";
|
||||
if (t.state === "seek_water") return t.explosionDisease ? "爆発病で水を探している" : "ずんち病で水を探している";
|
||||
if (t.state === "seek_bed") return target !== "なし" ? `${target}へ向かっている` : "眠る場所を探している";
|
||||
if (t.state === "play_ball") return "ボールが気になっている";
|
||||
if (t.state === "seek_friend" && t.intent?.actionId === "approach_mate") return t.target?.name ? `${t.target.name}に繁殖のため近づいている` : "繁殖できる相手を探している";
|
||||
if (t.state === "seek_friend") return "仲間を探している";
|
||||
if (t.state === "follow_parent") return "親についていく";
|
||||
if (t.state === "cursor_friend") return "カーソルについていく";
|
||||
if (t.state === "cursor_enemy") return "カーソルを避けている";
|
||||
if (t.state === "fight") return target !== "なし" ? `${target}と喧嘩している` : "喧嘩している";
|
||||
if (t.state === "fight_sick") return "きずつき病で当てもなく彷徨っている";
|
||||
if (t.state === "zunchi_sick" || t.state === "system") return "ずんち病で調子が悪い";
|
||||
if ((t.hurtTimer || 0) > 0.08) return "痛みで混乱している";
|
||||
if ((t.fearTimer || 0) > 0.08) return "怖がっている";
|
||||
if ((t.grassEatTimer || 0) > 0.04) return "草を食べている";
|
||||
if (t.state === "eat") return target !== "\u306a\u3057" ? `${target}\u3092\u98df\u3079\u3066\u3044\u308b` : "\u98df\u3079\u3066\u3044\u308b";
|
||||
if (t.state === "sleep") return t.target?.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u4f53\u529b\u3092\u56de\u5fa9\u3057\u3066\u3044\u308b" : "\u4f53\u529b\u3092\u56de\u5fa9\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "panic") return t.target ? `${target}\u304b\u3089\u9003\u3052\u3066\u3044\u308b` : "\u6016\u304c\u3063\u3066\u3044\u308b";
|
||||
if (t.state === "intimidate") return target !== "\u306a\u3057" ? `${target}\u3092\u5a01\u5687\u3057\u3066\u3044\u308b` : "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "ant_intimidate") return "\u30a2\u30ea\u3092\u5a01\u5687\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "ant_attack") return "\u30a2\u30ea\u3092\u653b\u6483\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "birth_ritual") return t.target?.name ? `${t.target.name}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b` : "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "sunbath") return "\u6674\u308c\u305f\u65e5\u306a\u305f\u3067\u30b9\u30c8\u30ec\u30b9\u3092\u4e0b\u3052\u3066\u3044\u308b";
|
||||
if (t.state === "frozen") return "\u30d5\u30a3\u30fc\u30eb\u30c9\u5916\u3067\u51b7\u51cd\u4fdd\u5b58\u3055\u308c\u3066\u3044\u308b";
|
||||
if (t.state === "seek_food") return target !== "\u306a\u3057" ? `${target}\u3092\u63a2\u3057\u3066\u3044\u308b` : "\u304a\u8179\u304c\u3059\u3044\u3066\u3044\u308b";
|
||||
if (t.state === "seek_water") return t.explosionDisease ? "\u7206\u767a\u75c5\u3067\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b" : "\u305a\u3093\u3061\u75c5\u3067\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "seek_bed") return target !== "\u306a\u3057" ? `${target}\u3078\u5411\u304b\u3063\u3066\u3044\u308b` : "\u7720\u308b\u5834\u6240\u3092\u63a2\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "play_ball") return "\u30dc\u30fc\u30eb\u304c\u6c17\u306b\u306a\u3063\u3066\u3044\u308b";
|
||||
if (t.state === "seek_friend" && (typeof getTarinaiBehaviorId === "function" ? getTarinaiBehaviorId(t) : t.behavior?.id) === "approach_mate") return t.target?.name ? `${t.target.name}\u306b\u7e41\u6b96\u306e\u305f\u3081\u8fd1\u3065\u3044\u3066\u3044\u308b` : "\u7e41\u6b96\u3067\u304d\u308b\u76f8\u624b\u3092\u63a2\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "seek_friend") return "\u4ef2\u9593\u3092\u63a2\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "follow_parent") return "\u89aa\u306b\u3064\u3044\u3066\u3044\u304f";
|
||||
if (t.state === "cursor_friend") return "\u30ab\u30fc\u30bd\u30eb\u306b\u3064\u3044\u3066\u3044\u304f";
|
||||
if (t.state === "cursor_enemy") return "\u30ab\u30fc\u30bd\u30eb\u3092\u907f\u3051\u3066\u3044\u308b";
|
||||
if (t.state === "fight") return target !== "\u306a\u3057" ? `${target}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b` : "\u55a7\u5629\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "fight_sick") return "\u304d\u305a\u3064\u304d\u75c5\u3067\u5f53\u3066\u3082\u306a\u304f\u5f77\u5fa8\u3063\u3066\u3044\u308b";
|
||||
if (t.state === "zunchi_sick" || t.state === "system") return "\u305a\u3093\u3061\u75c5\u3067\u8abf\u5b50\u304c\u60aa\u3044";
|
||||
if ((t.hurtTimer || 0) > 0.08) return "\u75db\u307f\u3067\u6df7\u4e71\u3057\u3066\u3044\u308b";
|
||||
if ((t.fearTimer || 0) > 0.08) return "\u6016\u304c\u3063\u3066\u3044\u308b";
|
||||
if ((t.grassEatTimer || 0) > 0.04) return "\u8349\u3092\u98df\u3079\u3066\u3044\u308b";
|
||||
const thought = String(t.thought || "").trim();
|
||||
if (thought) return thought;
|
||||
return "周囲を見ている";
|
||||
return "\u5468\u56f2\u3092\u898b\u3066\u3044\u308b";
|
||||
};
|
||||
|
||||
function stateLabel(s) { return TEXT_CATALOG.stateLabel(s); }
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ function bindUI() {
|
|||
document.addEventListener("click", (e) => {
|
||||
if (!isActivePlacementTool()) return;
|
||||
if (clickedInsideGameOrToolUi(e.target)) return;
|
||||
selectTool("observe"); // 道具設置モードを解除
|
||||
selectTool("observe"); // \u9053\u5177\u8a2d\u7f6e\u30e2\u30fc\u30c9\u3092\u89e3\u9664
|
||||
});
|
||||
ui.creditsBtn?.addEventListener("click", () => { audio.uiClick?.(); openCreditsDialog(); });
|
||||
ui.creditsCloseBtn?.addEventListener("click", closeCreditsDialog);
|
||||
|
|
@ -380,9 +380,9 @@ function bindUI() {
|
|||
target.y = uiCache.grabOrigin.y;
|
||||
target.vx = uiCache.grabOrigin.vx || 0;
|
||||
target.vy = uiCache.grabOrigin.vy || 0;
|
||||
target.thought = "フィールドに戻された";
|
||||
target.thought = "\u30d5\u30a3\u30fc\u30eb\u30c9\u306b\u623b\u3055\u308c\u305f";
|
||||
target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8);
|
||||
showToast("フィールド外なので元の位置に戻しました。");
|
||||
showToast("\u30d5\u30a3\u30fc\u30eb\u30c9\u5916\u306a\u306e\u3067\u5143\u306e\u4f4d\u7f6e\u306b\u623b\u3057\u307e\u3057\u305f\u3002");
|
||||
uiCache.grabOrigin = null;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -403,10 +403,12 @@ function bindUI() {
|
|||
uiCache.grabStartX = e.touches?.[0]?.clientX || 0;
|
||||
uiCache.grabStartY = e.touches?.[0]?.clientY || 0;
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
const lodged = target.currentLodgedPin?.() || (world.items || []).find(it => it && isPinType(it.type) && it.pinState === "lodged" && it.pinTargetId === target.id);
|
||||
if (lodged?.detachPushpin) lodged.detachPushpin(world, "pinch");
|
||||
target.sleeping = false;
|
||||
if (target.state === "sleep" || target.state === "seek_bed") target.goIdle?.("つままれている");
|
||||
if (target.state === "sleep" || target.state === "seek_bed") target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
||||
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18);
|
||||
target.thought = "つままれている";
|
||||
target.thought = "\u3064\u307e\u307e\u308c\u3066\u3044\u308b";
|
||||
target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 1.2);
|
||||
} else {
|
||||
if (isPinType(target.type) && target.pinState === "lodged" && target.detachPushpin) target.detachPushpin(world, "pinch");
|
||||
|
|
@ -540,7 +542,7 @@ function bindUI() {
|
|||
target.vx = clamp(dx * 16, -160, 160);
|
||||
target.vy = clamp(dy * 16, -160, 160);
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
target.goIdle?.("つままれている");
|
||||
target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
||||
target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8);
|
||||
} else if (target.type === "ball") {
|
||||
target.prevX = target.x - dx;
|
||||
|
|
@ -658,10 +660,12 @@ function bindUI() {
|
|||
uiCache.grabStartX = e.clientX;
|
||||
uiCache.grabStartY = e.clientY;
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
const lodged = target.currentLodgedPin?.() || (world.items || []).find(it => it && isPinType(it.type) && it.pinState === "lodged" && it.pinTargetId === target.id);
|
||||
if (lodged?.detachPushpin) lodged.detachPushpin(world, "pinch");
|
||||
target.sleeping = false;
|
||||
if (target.state === "sleep" || target.state === "seek_bed") target.goIdle?.("つままれている");
|
||||
if (target.state === "sleep" || target.state === "seek_bed") target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
||||
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18);
|
||||
target.thought = "つままれている";
|
||||
target.thought = "\u3064\u307e\u307e\u308c\u3066\u3044\u308b";
|
||||
target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 1.2);
|
||||
} else {
|
||||
if (isPinType(target.type) && target.pinState === "lodged" && target.detachPushpin) target.detachPushpin(world, "pinch");
|
||||
|
|
@ -761,7 +765,7 @@ function bindUI() {
|
|||
target.vx = clamp(dx * 16, -160, 160);
|
||||
target.vy = clamp(dy * 16, -160, 160);
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
target.goIdle?.("つままれている");
|
||||
target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
||||
} else if (target.type === "ball") {
|
||||
target.prevX = target.x - dx;
|
||||
target.prevY = target.y - dy;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function renderStats() {
|
|||
};
|
||||
setTextIfChanged(ui.statPop, "pop", values.pop);
|
||||
setTextIfChanged(ui.statDead, "dead", values.dead);
|
||||
setTextIfChanged(ui.statColonyMood, "colonyMood", world.colonyMood?.label || "のんびり");
|
||||
setTextIfChanged(ui.statColonyMood, "colonyMood", world.colonyMood?.label || "\u306e\u3093\u3073\u308a");
|
||||
updateColonyHistory(values);
|
||||
drawColonyChart(values);
|
||||
renderSelected();
|
||||
|
|
|
|||
|
|
@ -169,20 +169,20 @@ function selectedInfoRowHtml(label, valueHtml, metric = "") {
|
|||
|
||||
function selectedInventoryItems(t) {
|
||||
const worldRef = t?.world || world;
|
||||
const labels = { grass_bed: "かんたんベッド", plushie: "ぬいぐるみ" };
|
||||
const labels = { grass_bed: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", plushie: "\u306c\u3044\u3050\u308b\u307f" };
|
||||
const items = (worldRef?.items || []).filter(it => it && !it.dead && it.isStructure && it.ownerId === t?.id);
|
||||
const counts = new Map();
|
||||
for (const it of items) {
|
||||
const label = labels[it.type] || toolLabel(it.type) || it.type;
|
||||
counts.set(label, (counts.get(label) || 0) + 1);
|
||||
}
|
||||
return Array.from(counts.entries()).map(([label, count]) => count > 1 ? `${label} ×${count}` : label);
|
||||
return Array.from(counts.entries()).map(([label, count]) => count > 1 ? `${label} \u00d7${count}` : label);
|
||||
}
|
||||
|
||||
function selectedInventoryHtml(t) {
|
||||
const entries = selectedInventoryItems(t);
|
||||
if (!entries.length) return `<div class="info-row"><span>所持品</span><strong>なし</strong></div>`;
|
||||
return `<div class="info-row"><span>所持品</span><strong>${entries.map(v => `<span class="personality-tag">${escapeHtml(v)}</span>`).join("")}</strong></div>`;
|
||||
if (!entries.length) return `<div class="info-row"><span>\u6240\u6301\u54c1</span><strong>\u306a\u3057</strong></div>`;
|
||||
return `<div class="info-row"><span>\u6240\u6301\u54c1</span><strong>${entries.map(v => `<span class="personality-tag">${escapeHtml(v)}</span>`).join("")}</strong></div>`;
|
||||
}
|
||||
|
||||
function selectedStateText(t) {
|
||||
|
|
@ -216,9 +216,10 @@ function renderSelected() {
|
|||
].join(",");
|
||||
const itemEffectSummary = t.activeItemEffectSummary ? t.activeItemEffectSummary() : [];
|
||||
const needKeys = typeof TARINAI_NEED_KEYS !== "undefined" ? TARINAI_NEED_KEYS : ["food", "sleep", "health", "safety", "social", "fulfill"];
|
||||
const behaviorState = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(t) : t.behavior;
|
||||
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 || "", t.activeBehavior?.id || "", t.activeBehavior?.presentText || "", needKeys.map(key => `${key}:${t.needs?.[key] ?? 0}`).join(","),
|
||||
behaviorState?.need || "", behaviorState?.id || "", behaviorState?.label || "", behaviorState?.reasonText || "", behaviorState?.id || "", behaviorState?.presentText || "", 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),
|
||||
|
|
@ -273,7 +274,7 @@ function renderSelected() {
|
|||
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"` : "";
|
||||
const chosen = behaviorState?.need === key ? ` data-chosen-need="true"` : "";
|
||||
return `<div class="need-cell"${chosen}><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
|
||||
}).join("");
|
||||
const healthHtml = [
|
||||
|
|
@ -286,13 +287,11 @@ function renderSelected() {
|
|||
? itemEffectSummary.map(e => `<div class="info-row"><span>${escapeHtml(e.label)}</span><strong>${escapeHtml(e.detail)}</strong></div>`).join("")
|
||||
: `<div class="info-row"><span>\u30a2\u30a4\u30c6\u30e0\u52b9\u679c</span><strong>\u306a\u3057</strong></div>`;
|
||||
const relationHtml = `
|
||||
<div class="info-row relation-row relation-list-row"><span>\u53cb\u9054</span><strong>${relationListHtml(t, "friend")}</strong></div>
|
||||
<div class="info-row relation-row relation-list-row"><span>\u6575\u5bfe</span><strong>${relationListHtml(t, "enemy")}</strong></div>
|
||||
<div class="info-row"><span>\u3051\u3093\u304b\u52dd\u7387</span><strong>${escapeHtml(fightWinRateLabel(t))}</strong></div>
|
||||
`;
|
||||
const familyHtml = `
|
||||
<div class="info-row"><span>\u89aa</span><strong>${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "\u4e0d\u660e")}</strong></div>
|
||||
<div class="info-row"><span>\u5b50</span><strong>${t.children?.length || 0}</strong></div>
|
||||
<div class="info-row relation-row relation-list-row"><span>友達</span><strong>${relationListHtml(t, "friend")}</strong></div>
|
||||
<div class="info-row relation-row relation-list-row"><span>敵対</span><strong>${relationListHtml(t, "enemy")}</strong></div>
|
||||
<div class="info-row"><span>親</span><strong>${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "不明")}</strong></div>
|
||||
<div class="info-row"><span>子</span><strong>${t.children?.length || 0}</strong></div>
|
||||
<div class="info-row"><span>けんか勝率</span><strong>${escapeHtml(fightWinRateLabel(t))}</strong></div>
|
||||
`;
|
||||
ui.selectedInfo.innerHTML = `
|
||||
<div class="selected-name-editor">
|
||||
|
|
@ -304,8 +303,7 @@ function renderSelected() {
|
|||
${selectedDataCategoryHtml("health", "\u6b32\u6c42", healthHtml)}
|
||||
${selectedDataCategoryHtml("effects", "\u30a2\u30a4\u30c6\u30e0\u52b9\u679c", itemEffectHtml)}
|
||||
${selectedDataCategoryHtml("personality", "\u6027\u683c", personalityHtml)}
|
||||
${selectedDataCategoryHtml("relation", "\u95a2\u4fc2", relationHtml)}
|
||||
${selectedDataCategoryHtml("family", "\u5bb6\u65cf", familyHtml)}
|
||||
${selectedDataCategoryHtml("relation", "\u95a2\u4fc2\u3068\u5bb6\u65cf", relationHtml)}
|
||||
${selectedDataCategoryHtml("inventory", "\u6240\u6301\u54c1", selectedInventoryHtml(t))}
|
||||
${selectedDataCategoryHtml("records", "\u8a18\u9332", `<div class="selected-records"><ol>${recordHtml}</ol></div>`)}
|
||||
</div>
|
||||
|
|
@ -325,19 +323,20 @@ function stateLabel(s) {
|
|||
}
|
||||
|
||||
function reasonLabel(t) {
|
||||
if (!t) return "なし";
|
||||
const behavior = typeof activeBehaviorText === "function" ? activeBehaviorText(t) : String(t.activeBehavior?.presentText || t.activeBehavior?.reasonText || "").trim();
|
||||
if (!t) return "\u306a\u3057";
|
||||
const behavior = typeof activeBehaviorText === "function" ? activeBehaviorText(t) : String((typeof getTarinaiBehaviorText === "function" ? getTarinaiBehaviorText(t) : (t.behavior?.presentText || t.behavior?.reasonText || ""))).trim();
|
||||
if (behavior) return behavior;
|
||||
const state = String(t.state || "");
|
||||
const live = window.TEXT_CATALOG?.reasonLabel?.(t) || "";
|
||||
const liveStates = new Set(["birth_ritual", "eat", "sleep", "panic", "fight", "intimidate", "ant_attack", "ant_intimidate", "frozen"]);
|
||||
if (live && (liveStates.has(state) || (t.birthRitualTimer || 0) > 0.04 || (t.eatTimer || 0) > 0.04 || (t.fightTimer || 0) > 0.04 || (t.intimidateTimer || 0) > 0.04)) return live;
|
||||
if (t?.intent?.reasonText) return t.intent.reasonText;
|
||||
return live || "なし";
|
||||
const behaviorState = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(t) : t?.behavior;
|
||||
if (behaviorState?.reasonText) return behaviorState.reasonText;
|
||||
return live || "\u306a\u3057";
|
||||
}
|
||||
|
||||
function targetLabel(target) {
|
||||
return window.TEXT_CATALOG?.targetLabel?.(target) || "なし";
|
||||
return window.TEXT_CATALOG?.targetLabel?.(target) || "\u306a\u3057";
|
||||
}
|
||||
|
||||
function fightWinRateLabel(t) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const SCALABLE_TOOLS = new Set(scalableToolIds());
|
|||
|
||||
const SIZE_ORDER = typeof TOOL_SIZE_ORDER !== "undefined" ? TOOL_SIZE_ORDER : ["small", "medium", "large"];
|
||||
|
||||
const SIZE_LABELS = typeof TOOL_SIZE_LABELS !== "undefined" ? TOOL_SIZE_LABELS : { small: "小", medium: "中", large: "大" };
|
||||
const SIZE_LABELS = typeof TOOL_SIZE_LABELS !== "undefined" ? TOOL_SIZE_LABELS : { small: "\u5c0f", medium: "\u4e2d", large: "\u5927" };
|
||||
|
||||
function renderToolPalette() {
|
||||
if (!ui?.toolPalette || typeof toolCategories !== "function") return;
|
||||
|
|
@ -41,6 +41,7 @@ function renderToolPalette() {
|
|||
function attachToolSpritePreview(btn, def, toolId) {
|
||||
const itemType = def?.itemType || toolId;
|
||||
if (!btn || !itemType || !def?.placeable) return;
|
||||
if (toolId === "ball" || itemType === "ball") return;
|
||||
const drawPreview = window.drawToolItemPreview || window.drawConsumableFieldSprite;
|
||||
if (typeof drawPreview !== "function") return;
|
||||
const canvas = document.createElement("canvas");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
(function () {
|
||||
const APP_VERSION = "15.24.14";
|
||||
const APP_BUILD = "forced-behavior-sleep-rhythm";
|
||||
const APP_VERSION = "15.24.35";
|
||||
const APP_BUILD = "owned-bed-duplicator-priority";
|
||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class World { constructor() {
|
|||
this.drawList = [];
|
||||
this.itemCounts = {};
|
||||
this.effectCounts = {};
|
||||
this.colonyMood = { id: "relaxed", label: "のんびり", description: "何も無し", effects: { personality: {} } };
|
||||
this.colonyMood = { id: "relaxed", label: "\u306e\u3093\u3073\u308a", description: "\u4f55\u3082\u7121\u3057", effects: { personality: {} } };
|
||||
this.lastColonyMoodDay = 0;
|
||||
this.foodSpoilagePenalty = 0;
|
||||
this.foodSpoilageEvents = 0;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
updateAnts(dt) {
|
||||
if (!this.ants) this.ants = [];
|
||||
const nests = (this.items || []).filter(it => it && !it.dead && it.type === "ant_nest");
|
||||
const nests = this.itemsOfType?.("ant_nest") || (this.items || []).filter(it => it && !it.dead && it.type === "ant_nest");
|
||||
for (const nest of nests) {
|
||||
if (typeof ensureAntNestWorkerPool === "function") ensureAntNestWorkerPool(nest);
|
||||
if (!Number.isFinite(nest.antCount)) nest.antCount = ANT_NEST_START_COUNT || 6;
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@
|
|||
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" });
|
||||
else { t.fearTimer = Math.max(t.fearTimer, 1.7 + p); t.setActionState?.("panic", { target: { x, y }, reason: "爆竹の爆風でパニックになっている", wake: true }); }
|
||||
if (t.enterPanic) t.enterPanic({ target: { x, y }, reason: "\u7206\u7af9\u306e\u7206\u98a8\u3067\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 1.7 + p, wake: true, cause: "firecracker_blast" });
|
||||
else { t.fearTimer = Math.max(t.fearTimer, 1.7 + p); t.setActionState?.("panic", { target: { x, y }, reason: "\u7206\u7af9\u306e\u7206\u98a8\u3067\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); }
|
||||
let nx = dx / d;
|
||||
let ny = dy / d;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
panic: true,
|
||||
target: { x, y },
|
||||
fearTimer: 1.7 + p,
|
||||
thought: "爆竹の爆風でパニックになっている",
|
||||
thought: "\u7206\u7af9\u306e\u7206\u98a8\u3067\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b",
|
||||
});
|
||||
t.fallTimer = Math.max(t.fallTimer, 1.45 + p * 1.85);
|
||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer);
|
||||
|
|
@ -181,12 +181,12 @@
|
|||
panic: true,
|
||||
target: { x, y },
|
||||
fearTimer: 1.7 + q,
|
||||
thought: "爆発病の爆発に巻き込まれている",
|
||||
thought: "\u7206\u767a\u75c5\u306e\u7206\u767a\u306b\u5dfb\u304d\u8fbc\u307e\u308c\u3066\u3044\u308b",
|
||||
});
|
||||
o.hurtTimer = Math.max(o.hurtTimer || 0, 1.5 + q * 1.8);
|
||||
o.surpriseTimer = Math.max(o.surpriseTimer || 0, 0.85);
|
||||
if (o.enterPanic) o.enterPanic({ target: { x, y }, reason: "爆発病の爆発に巻き込まれている", fear: 1.7 + q, wake: true, cause: "explosion_disease_blast" });
|
||||
else { o.fearTimer = Math.max(o.fearTimer || 0, 1.7 + q); o.setActionState?.("panic", { target: { x, y }, reason: "爆発病の爆発に巻き込まれている", wake: true }); }
|
||||
if (o.enterPanic) o.enterPanic({ target: { x, y }, reason: "\u7206\u767a\u75c5\u306e\u7206\u767a\u306b\u5dfb\u304d\u8fbc\u307e\u308c\u3066\u3044\u308b", fear: 1.7 + q, wake: true, cause: "explosion_disease_blast" });
|
||||
else { o.fearTimer = Math.max(o.fearTimer || 0, 1.7 + q); o.setActionState?.("panic", { target: { x, y }, reason: "\u7206\u767a\u75c5\u306e\u7206\u767a\u306b\u5dfb\u304d\u8fbc\u307e\u308c\u3066\u3044\u308b", wake: true }); }
|
||||
o.fallTimer = Math.max(o.fallTimer || 0, 1.45 + q * 1.85);
|
||||
o.fallMax = Math.max(o.fallMax || 0, o.fallTimer);
|
||||
o.fallDir = (dx >= 0 ? 1 : -1) * (Math.random() < 0.5 ? 1 : -1);
|
||||
|
|
@ -279,13 +279,12 @@
|
|||
it.vy = Math.sin(a) * rand(200, 480) - rand(30, 160);
|
||||
it.stage = "fresh";
|
||||
it.burstFromOshibyo = true;
|
||||
this.items.push(it);
|
||||
this.itemCounts.zunchi = (this.itemCounts.zunchi || 0) + 1;
|
||||
this.addItem?.(it, "zunchi-burst") || this.items.push(it);
|
||||
}
|
||||
this.effects?.push(new Effect("zunchi_miasma", sx, sy, { size: 28, life: 0.65, color: "rgba(74,91,50,0.48)" }));
|
||||
this.log?.(`${source?.name || "たりない"}から溜まっていたずんちが弾け飛んだ。`, "accident", { participants: source?.name ? [source] : [] });
|
||||
this.log?.(`${source?.name || "\u305f\u308a\u306a\u3044"}\u304b\u3089\u6e9c\u307e\u3063\u3066\u3044\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3093\u3060\u3002`, "accident", { participants: source?.name ? [source] : [] });
|
||||
audio.place?.("zunchi");
|
||||
this.rebuildSpatial?.(true);
|
||||
this.ensureSpatial?.("zunchi-burst");
|
||||
},
|
||||
|
||||
spawnZunchi(x, y) {
|
||||
|
|
@ -300,8 +299,7 @@
|
|||
}
|
||||
const it = new Item("zunchi", clamp(x, 40, this.w - 40), clamp(y, 40, this.h - 40));
|
||||
it.amount = 260;
|
||||
this.items.push(it);
|
||||
this.itemCounts.zunchi = (this.itemCounts.zunchi || 0) + 1;
|
||||
this.addItem?.(it, "zunchi-spawn") || this.items.push(it);
|
||||
audio.place?.("zunchi");
|
||||
},
|
||||
|
||||
|
|
@ -439,8 +437,8 @@
|
|||
entity.vy = (Number.isFinite(entity.vy) ? entity.vy : 0) + vy;
|
||||
}
|
||||
if (options.panic && entity instanceof Tarinai) {
|
||||
if (entity.enterPanic) entity.enterPanic({ target: options.target || null, reason: options.thought || "パニックになっている", fear: options.fearTimer ?? 0.55, wake: true, cause: options.cause || "impulse" });
|
||||
else { entity.setActionState?.("panic", { target: options.target || null, reason: options.thought || "パニックになっている", wake: true }); entity.fearTimer = Math.max(entity.fearTimer || 0, options.fearTimer ?? 0.55); }
|
||||
if (entity.enterPanic) entity.enterPanic({ target: options.target || null, reason: options.thought || "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: options.fearTimer ?? 0.55, wake: true, cause: options.cause || "impulse" });
|
||||
else { entity.setActionState?.("panic", { target: options.target || null, reason: options.thought || "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); entity.fearTimer = Math.max(entity.fearTimer || 0, options.fearTimer ?? 0.55); }
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
|
@ -492,8 +490,8 @@
|
|||
victim.fightTimer = Math.min(victim.fightTimer || 0, rand(0.20, 0.42));
|
||||
attacker.fightTimer = Math.min(attacker.fightTimer || 0, rand(0.20, 0.42));
|
||||
this.emit?.("fight:lost", { winner: attacker, loser: victim, damage });
|
||||
if (victim.enterPanic) victim.enterPanic({ target: attacker, threat: attacker, reason: "喧嘩に負けてパニックになっている", fear: 1.1, wake: true, cause: "fight_lost" });
|
||||
else { victim.setActionState?.("panic", { target: attacker, reason: "喧嘩に負けてパニックになっている", wake: true }); victim.fearTimer = Math.max(victim.fearTimer || 0, 1.1 * (profile.fear || 1)); }
|
||||
if (victim.enterPanic) victim.enterPanic({ target: attacker, threat: attacker, reason: "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 1.1, wake: true, cause: "fight_lost" });
|
||||
else { victim.setActionState?.("panic", { target: attacker, reason: "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); victim.fearTimer = Math.max(victim.fearTimer || 0, 1.1 * (profile.fear || 1)); }
|
||||
const dx = victim.x - attacker.x;
|
||||
const dy = victim.y - attacker.y;
|
||||
const d = Math.hypot(dx, dy) || 1;
|
||||
|
|
@ -579,9 +577,9 @@
|
|||
t.fearTimer = Math.max(t.fearTimer || 0, 4.0 + p * 2.2);
|
||||
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 = "げんこつがこわい";
|
||||
t.thought = "\u3052\u3093\u3053\u3064\u304c\u3053\u308f\u3044";
|
||||
} else {
|
||||
t.thought = isStone ? "石が落ちてきてパニックになっている" : "落ちてきた道具に当たって混乱している";
|
||||
t.thought = isStone ? "\u77f3\u304c\u843d\u3061\u3066\u304d\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b";
|
||||
}
|
||||
t.hurtTimer = Math.max(t.hurtTimer, isGenkotsu ? 3.4 : (isStone ? 2.7 : 1.25));
|
||||
t.fallTimer = Math.max(t.fallTimer, isGenkotsu ? 0.86 : (isStone ? 0.62 : 0.26));
|
||||
|
|
@ -641,7 +639,7 @@
|
|||
return dist(a, ball) + ap - (dist(b, ball) + bp);
|
||||
});
|
||||
for (const t of arr.slice(5)) {
|
||||
t.goIdle("ボールが混み合っているので眺めている");
|
||||
t.goIdle("\u30dc\u30fc\u30eb\u304c\u6df7\u307f\u5408\u3063\u3066\u3044\u308b\u306e\u3067\u773a\u3081\u3066\u3044\u308b");
|
||||
t.wanderAngle = Math.atan2(t.y - ball.y, t.x - ball.x) + rand(-0.45, 0.45);
|
||||
}
|
||||
}
|
||||
|
|
@ -682,8 +680,9 @@
|
|||
if ((this.itemCounts?.ball || 0) < 2) return;
|
||||
const now = this.time || 0;
|
||||
if (!this.ballCollisionMemo) this.ballCollisionMemo = new Map();
|
||||
const balls = (this.items || []).filter(it => it && !it.dead && it.type === "ball");
|
||||
const balls = this.itemsOfType?.("ball") || (this.items || []).filter(it => it && !it.dead && it.type === "ball");
|
||||
for (const a of balls) {
|
||||
if (!a || a.dead || a.type !== "ball") continue;
|
||||
const ar = a.r || 18;
|
||||
const avx = a.vx || 0;
|
||||
const avy = a.vy || 0;
|
||||
|
|
@ -816,14 +815,14 @@
|
|||
panic: true,
|
||||
target: { x: ball.x, y: ball.y },
|
||||
fearTimer: 0.95,
|
||||
thought: "高速のボールにぶつかってパニックになっている",
|
||||
thought: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b",
|
||||
});
|
||||
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);
|
||||
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 });
|
||||
if (t.enterPanic) t.enterPanic({ target: { x: ball.x, y: ball.y }, reason: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.95, stress: 0, wake: true, cause: "fast_ball_hit" });
|
||||
else t.setActionState?.("panic", { target: { x: ball.x, y: ball.y }, reason: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true });
|
||||
t.hurtTimer = Math.max(t.hurtTimer, damage > 16 ? 2.4 : 1.35);
|
||||
t.fallTimer = Math.max(t.fallTimer, damage > 14 ? 0.98 : 0.46);
|
||||
t.fallMax = Math.max(t.fallMax || 0.98, t.fallTimer);
|
||||
|
|
@ -929,7 +928,7 @@
|
|||
panic: true,
|
||||
target: { x: impactor.x, y: impactor.y },
|
||||
fearTimer: 0.55,
|
||||
thought: "高速でぶつかって混乱している",
|
||||
thought: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b",
|
||||
});
|
||||
impactor.vx = ivx * 0.72 - nx * Math.min(56, relSpeed * 0.10);
|
||||
impactor.vy = ivy * 0.72 - ny * Math.min(56, relSpeed * 0.10);
|
||||
|
|
@ -937,8 +936,8 @@
|
|||
if (!impactor.dead) this.applyImpactDamage(impactor, damage * 0.35, "\u885d\u7a81");
|
||||
for (const t of [target, impactor]) {
|
||||
if (!t || t.dead) continue;
|
||||
if (t.enterPanic) t.enterPanic({ target: { x: impactor.x, y: impactor.y }, reason: "高速でぶつかって混乱している", fear: 0.75, wake: true, cause: "tarinai_highspeed_collision" });
|
||||
else t.setActionState?.("panic", { target: { x: impactor.x, y: impactor.y }, reason: "高速でぶつかって混乱している", wake: true });
|
||||
if (t.enterPanic) t.enterPanic({ target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", fear: 0.75, wake: true, cause: "tarinai_highspeed_collision" });
|
||||
else t.setActionState?.("panic", { target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", wake: true });
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.9 + damage * 0.035);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, 0.36 + damage * 0.025);
|
||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
this.familyVersion = 0;
|
||||
this.relationNotices = {};
|
||||
this.resolvedFightIds = {};
|
||||
this.fightPairCooldowns = {};
|
||||
this.foodSpoilagePenalty = 0;
|
||||
this.foodSpoilageEvents = 0;
|
||||
this.pointer = { x: this.w / 2, y: this.h / 2, inside: false, motion: 0, movedAt: 0 };
|
||||
|
|
@ -62,8 +63,8 @@
|
|||
generation: 1,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < grassCount; i++) this.items.push(new Item("grass", rand(70, this.w - 70), rand(70, this.h - 70)));
|
||||
this.items.push(new Item("stone", this.w * 0.68, this.h * 0.32));
|
||||
for (let i = 0; i < grassCount; i++) this.addItem?.(new Item("grass", rand(70, this.w - 70), rand(70, this.h - 70)), "reset-grass") || this.items.push(new Item("grass", rand(70, this.w - 70), rand(70, this.h - 70)));
|
||||
this.addItem?.(new Item("stone", this.w * 0.68, this.h * 0.32), "reset-stone") || this.items.push(new Item("stone", this.w * 0.68, this.h * 0.32));
|
||||
this.updateItemCounts();
|
||||
this.updateEffectCounts();
|
||||
this.markTerrainDirty?.("reset");
|
||||
|
|
@ -342,7 +343,7 @@
|
|||
if (!other || other === dead || other.dead) continue;
|
||||
if (other.target === dead || other.target?.id === deadId) {
|
||||
other.target = null;
|
||||
if (["seek_friend", "panic", "fight", "intimidate"].includes(other.state)) other.goIdle?.("参照していた相手がいなくなった");
|
||||
if (["seek_friend", "panic", "fight", "intimidate"].includes(other.state)) other.goIdle?.("\u53c2\u7167\u3057\u3066\u3044\u305f\u76f8\u624b\u304c\u3044\u306a\u304f\u306a\u3063\u305f");
|
||||
changed = true;
|
||||
}
|
||||
if (other.panicTarget === dead || other.panicTarget?.id === deadId) { other.panicTarget = null; changed = true; }
|
||||
|
|
@ -428,11 +429,17 @@
|
|||
b.birthRitualRole = 1;
|
||||
a.birthRitualLeader = true;
|
||||
b.birthRitualLeader = false;
|
||||
a.setActionState("birth_ritual", { target: b, reason: "繁殖の前ぶれをしている" });
|
||||
b.setActionState("birth_ritual", { target: a, reason: "繁殖の前ぶれをしている" });
|
||||
a.setActionState("birth_ritual", { target: b, reason: "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b" });
|
||||
b.setActionState("birth_ritual", { target: a, reason: "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b" });
|
||||
if (typeof setLiveActionText === "function") {
|
||||
setLiveActionText(a, { need: "social", actionId: "birth_ritual", actionLabel: "繁殖の前ぶれをしている", reasonText: `${b.name || "相手"}と繁殖の前ぶれをしている`, target: b });
|
||||
setLiveActionText(b, { need: "social", actionId: "birth_ritual", actionLabel: "繁殖の前ぶれをしている", reasonText: `${a.name || "相手"}と繁殖の前ぶれをしている`, target: a });
|
||||
const aBehavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(a) : a.behavior;
|
||||
const bBehavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(b) : b.behavior;
|
||||
const aLoveCause = (aBehavior?.source === "love_mochi" || (a.loveMochiTimer || 0) > 0.04) ? "\u3078\u3053\u9905\u306e\u52b9\u679c" : `${b.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u306a\u308b`;
|
||||
const bLoveCause = (bBehavior?.source === "love_mochi" || (b.loveMochiTimer || 0) > 0.04) ? "\u3078\u3053\u9905\u306e\u52b9\u679c" : `${a.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u306a\u308b`;
|
||||
const aText = typeof buildReasonText === "function" ? buildReasonText("social", aBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${b.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b` }, a, this, { causeText: aLoveCause }) : `${b.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`;
|
||||
const bText = typeof buildReasonText === "function" ? buildReasonText("social", bBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${a.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b` }, b, this, { causeText: bLoveCause }) : `${a.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`;
|
||||
setLiveActionText(a, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${b.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`, reasonText: aText, causeText: aLoveCause, target: b });
|
||||
setLiveActionText(b, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${a.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`, reasonText: bText, causeText: bLoveCause, target: a });
|
||||
}
|
||||
a.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10);
|
||||
b.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10);
|
||||
|
|
@ -447,8 +454,8 @@
|
|||
a.birthRitualTimer = b.birthRitualTimer = 0;
|
||||
a.birthPartnerId = b.birthPartnerId = null;
|
||||
a.birthRitualLeader = b.birthRitualLeader = false;
|
||||
a.goIdle("誕生の前ぶれが終わった");
|
||||
b.goIdle("誕生の前ぶれが終わった");
|
||||
a.goIdle("\u8a95\u751f\u306e\u524d\u3076\u308c\u304c\u7d42\u308f\u3063\u305f");
|
||||
b.goIdle("\u8a95\u751f\u306e\u524d\u3076\u308c\u304c\u7d42\u308f\u3063\u305f");
|
||||
if (reproductionBlockedByDisease || (!!a.isZunchiSlave !== !!b.isZunchiSlave)) {
|
||||
a.birthRitualRole = b.birthRitualRole = 0;
|
||||
return null;
|
||||
|
|
@ -579,11 +586,39 @@
|
|||
return child;
|
||||
},
|
||||
|
||||
fightPairKey(a, b) {
|
||||
const aid = a?.id || a?.familyKey || "";
|
||||
const bid = b?.id || b?.familyKey || "";
|
||||
if (!aid || !bid) return "";
|
||||
return String(aid) < String(bid) ? `${aid}:${bid}` : `${bid}:${aid}`;
|
||||
},
|
||||
|
||||
fightPairCooldownRemaining(a, b) {
|
||||
const key = this.fightPairKey?.(a, b);
|
||||
if (!key) return 0;
|
||||
const until = Number(this.fightPairCooldowns?.[key] || 0) || 0;
|
||||
return Math.max(0, until - (this.time || 0));
|
||||
},
|
||||
|
||||
markFightPairCooldown(a, b, seconds = 3.2) {
|
||||
const key = this.fightPairKey?.(a, b);
|
||||
if (!key) return;
|
||||
this.fightPairCooldowns = this.fightPairCooldowns || {};
|
||||
this.fightPairCooldowns[key] = Math.max(Number(this.fightPairCooldowns[key] || 0) || 0, (this.time || 0) + Math.max(0.2, Number(seconds) || 3.2));
|
||||
},
|
||||
|
||||
isAlreadyFightingPair(a, b) {
|
||||
if (!a || !b) return false;
|
||||
return a.fightTimer > 0.04 && b.fightTimer > 0.04 && ((a.fightTargetIds || []).includes(b.id) || a.fightTargetId === b.id) && ((b.fightTargetIds || []).includes(a.id) || b.fightTargetId === a.id);
|
||||
},
|
||||
|
||||
startForcedFight(a, b) {
|
||||
if (!a || !b || a === b || a.dead || b.dead) return false;
|
||||
if (!this.canFightPair(a, b)) return false;
|
||||
const alreadyFighting = a.fightTimer > 0.04 && b.fightTimer > 0.04 && ((a.fightTargetIds || []).includes(b.id) || a.fightTargetId === b.id);
|
||||
const alreadyFighting = this.isAlreadyFightingPair?.(a, b);
|
||||
if (alreadyFighting) return true;
|
||||
if ((a.fightTimer || 0) > 0.04 || (b.fightTimer || 0) > 0.04) return false;
|
||||
if (this.fightPairCooldownRemaining?.(a, b) > 0.04) return false;
|
||||
for (const t of [a, b]) {
|
||||
t.birthRitualTimer = 0;
|
||||
t.birthRitualMax = 0;
|
||||
|
|
@ -601,7 +636,7 @@
|
|||
t.goIdle("");
|
||||
}
|
||||
this.startFightMochiIntimidation(a, b);
|
||||
this.startFight(a, b);
|
||||
this.startFight(a, b, { forced: true, initiator: a });
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -629,33 +664,36 @@
|
|||
},
|
||||
|
||||
startConflict(a, b) {
|
||||
if (!a || !b || a.dead || b.dead) return;
|
||||
if (typeof a.relationTo !== "function" || typeof b.relationTo !== "function") return;
|
||||
if ((a.fightMochiTimer || 0) > 0.04 || (b.fightMochiTimer || 0) > 0.04) { this.startForcedFight(a, b); return; }
|
||||
if (this.areParentChild(a, b) || this.areCoParents?.(a, b)) return;
|
||||
if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return;
|
||||
if (a.fightTimer > 0.04 || b.fightTimer > 0.04 || a.intimidateTimer > 0.04 || b.intimidateTimer > 0.04) return;
|
||||
if (!a || !b || a.dead || b.dead) return false;
|
||||
if (typeof a.relationTo !== "function" || typeof b.relationTo !== "function") return false;
|
||||
if ((a.fightMochiTimer || 0) > 0.04 || (b.fightMochiTimer || 0) > 0.04) return this.startForcedFight(a, b);
|
||||
if (this.areParentChild(a, b) || this.areCoParents?.(a, b)) return false;
|
||||
if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return false;
|
||||
if (this.fightPairCooldownRemaining?.(a, b) > 0.04) return false;
|
||||
if (a.fightTimer > 0.04 || b.fightTimer > 0.04 || a.intimidateTimer > 0.04 || b.intimidateTimer > 0.04) return false;
|
||||
const aAggressiveIntent = a.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
|
||||
const bAggressiveIntent = b.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
|
||||
const scoreA = a.energy * 0.38 + a.mood * 0.18 + a.stress * 0.12 + (a.type === "angry" ? 12 : 0) + aAggressiveIntent * 8 + ((typeof b.relationTo === "function" ? (b.relationTo(a.id).fear || 0) : 0) * 0.65) + (a.isZunchiSlave ? -10 : 0) + ((a.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5);
|
||||
const scoreB = b.energy * 0.38 + b.mood * 0.18 + b.stress * 0.12 + (b.type === "angry" ? 12 : 0) + bAggressiveIntent * 8 + ((typeof a.relationTo === "function" ? (a.relationTo(b.id).fear || 0) : 0) * 0.65) + (b.isZunchiSlave ? -10 : 0) + ((b.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5);
|
||||
const actor = scoreA >= scoreB ? a : b;
|
||||
const target = actor === a ? b : a;
|
||||
if (!this.canFightPair(actor, target)) return;
|
||||
if (!this.canFightPair(actor, target)) return false;
|
||||
const actorScore = actor === a ? scoreA : scoreB;
|
||||
const targetScore = actor === a ? scoreB : scoreA;
|
||||
const canIntimidate = !actor.lowHealthSprite || !actor.lowHealthSprite();
|
||||
const battleDrug = (actor.fightMochiTimer || 0) > 0.04 || (target.fightMochiTimer || 0) > 0.04;
|
||||
const intimidationChance = clamp(0.40 + (battleDrug ? 0.24 : 0) + (target.isZunchiSlave ? 0.18 : 0), 0.24, 0.90);
|
||||
if (canIntimidate && Math.random() < intimidationChance) {
|
||||
this.startIntimidation(actor, target, actorScore, targetScore);
|
||||
return;
|
||||
return this.startIntimidation(actor, target, actorScore, targetScore);
|
||||
}
|
||||
this.startFight(a, b);
|
||||
return this.startFight(actor, target, { initiator: actor });
|
||||
},
|
||||
|
||||
startIntimidation(actor, target, actorScore = 0, targetScore = 0) {
|
||||
if (!actor || !target || actor.dead || target.dead) return false;
|
||||
if ((actor.fightTimer || 0) > 0.04 || (target.fightTimer || 0) > 0.04) return false;
|
||||
if ((actor.intimidateTimer || 0) > 0.04 || (target.intimidateTimer || 0) > 0.04) return false;
|
||||
if (this.fightPairCooldownRemaining?.(actor, target) > 0.04) return false;
|
||||
if (actor.lowHealthSprite && actor.lowHealthSprite()) return false;
|
||||
const fearLoad = ((typeof target.relationTo === "function" ? (target.relationTo(actor.id).fear || 0) : 0) * 0.72) + target.personalityProfile().fear * 8;
|
||||
const actorAggression = Math.max(0, actor.currentPersonality?.aggression || 0);
|
||||
|
|
@ -670,17 +708,17 @@
|
|||
target.surpriseTimer = Math.max(target.surpriseTimer, 0.32);
|
||||
actor.fightCooldown = CONFIG.fightCooldown + rand(1, 4);
|
||||
target.fightCooldown = CONFIG.fightCooldown + rand(1, 4);
|
||||
actor.intimidateTimer = Math.max(actor.intimidateTimer, rand(2.45, 3.35));
|
||||
actor.intimidateTimer = Math.max(actor.intimidateTimer, rand(1.05, 1.65));
|
||||
actor.intimidateTargetId = target.id;
|
||||
actor.setActionState("intimidate", { target, reason: "相手を威嚇している" });
|
||||
if (typeof setLiveActionText === "function") setLiveActionText(actor, { need: "social", subNeed: "conflict", actionId: "intimidate_enemy", actionLabel: "威嚇している", reasonText: `${target.name || "相手"}を威嚇している`, target, phase: "perform", source: "behavior" });
|
||||
actor.setActionState("intimidate", { target, reason: "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b" });
|
||||
if (typeof setLiveActionText === "function") setLiveActionText(actor, { need: "social", subNeed: "conflict", actionId: "intimidate_enemy", actionLabel: "\u5a01\u5687\u3057\u3066\u3044\u308b", reasonText: `${target.name || "\u76f8\u624b"}\u3092\u5a01\u5687\u3057\u3066\u3044\u308b`, target, phase: "perform", source: "behavior" });
|
||||
this.effects.push(new Effect("ring", actor.x, actor.y - actor.radius * 0.65, {
|
||||
life: 0.42,
|
||||
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)");
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(1.65, 2.45));
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(0.95, 1.55));
|
||||
target.fearTimer = Math.max(target.fearTimer, 0.65 * target.personalityProfile().fear);
|
||||
if (Math.random() >= successChance) {
|
||||
actor.adjustPersonality?.("aggression", -0.018, "after failed intimidation");
|
||||
|
|
@ -688,20 +726,20 @@
|
|||
actor.intimidateTimer = 0;
|
||||
actor.intimidateTargetId = null;
|
||||
target.intimidatedTimer = 0;
|
||||
if (actor.state === "intimidate") actor.goIdle("威嚇に失敗した");
|
||||
if (target.state === "panic") target.goIdle("威嚇から戻った");
|
||||
if (actor.state === "intimidate") actor.goIdle("\u5a01\u5687\u306b\u5931\u6557\u3057\u305f");
|
||||
if (target.state === "panic") target.goIdle("\u5a01\u5687\u304b\u3089\u623b\u3063\u305f");
|
||||
actor.spriteLockUntil = 0;
|
||||
target.spriteLockUntil = 0;
|
||||
this.startFight(actor, target);
|
||||
this.startFight(actor, target, { initiator: actor });
|
||||
return true;
|
||||
}
|
||||
actor.adjustPersonality?.("aggression", 0.018, "after successful intimidation");
|
||||
target.defeatedById = actor.id;
|
||||
target.fightWinnerId = actor.id;
|
||||
target.defeatedTimer = Math.max(target.defeatedTimer, rand(2.8, 4.2));
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(2.4, 3.4));
|
||||
if (target.enterPanic) target.enterPanic({ threat: actor, target: actor, reason: "威嚇されて逃げている", fear: 1.35, wake: true, cause: "intimidated" });
|
||||
else { target.fearTimer = Math.max(target.fearTimer, 1.35 * target.personalityProfile().fear); target.setActionState?.("panic", { target: actor, reason: "威嚇されて逃げている", wake: true }); }
|
||||
target.defeatedTimer = Math.max(target.defeatedTimer, rand(1.8, 2.8));
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(1.2, 2.0));
|
||||
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 * target.personalityProfile().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)");
|
||||
target.vx += (target.x < actor.x ? -1 : 1) * rand(26, 54);
|
||||
target.vy += rand(-18, 18);
|
||||
|
|
@ -711,21 +749,41 @@
|
|||
this.log(`${actor.name}\u306f${target.name}\u3092\u5a01\u5687\u3057\u3066\u8ffd\u3044\u6255\u3063\u305f\u3002`, "fight", { participants: [actor, target] });
|
||||
actor.lastLog = target.lastLog = this.time;
|
||||
}
|
||||
this.markFightPairCooldown?.(actor, target, 2.8);
|
||||
return true;
|
||||
},
|
||||
|
||||
startFight(a, b) {
|
||||
if (!a || !b || a.dead || b.dead) return;
|
||||
if (!this.canFightPair(a, b) && !this.canFightPair(b, a)) return;
|
||||
if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return;
|
||||
a.setActionState("fight", { target: b, reason: `${b.name || "相手"}と喧嘩している` });
|
||||
b.setActionState("fight", { target: a, reason: `${a.name || "相手"}と喧嘩している` });
|
||||
startFight(a, b, opts = {}) {
|
||||
if (!a || !b || a.dead || b.dead) return false;
|
||||
if (!this.canFightPair(a, b) && !this.canFightPair(b, a)) return false;
|
||||
if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return false;
|
||||
if (this.isAlreadyFightingPair?.(a, b)) return true;
|
||||
const allowForcedStart = Boolean(opts?.forced);
|
||||
if (!allowForcedStart && ((a.fightTimer || 0) > 0.04 || (b.fightTimer || 0) > 0.04 || (a.intimidateTimer || 0) > 0.04 || (b.intimidateTimer || 0) > 0.04)) return false;
|
||||
if (!allowForcedStart && this.fightPairCooldownRemaining?.(a, b) > 0.04) return false;
|
||||
this.markFightPairCooldown?.(a, b, 3.4);
|
||||
const initiator = opts?.initiator === b ? b : a;
|
||||
const receiver = initiator === a ? b : a;
|
||||
const causeFor = (self, other) => {
|
||||
if (((typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(self) : self.behavior)?.source === "fight_mochi") || (self.fightMochiTimer || 0) > 0.04) return "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c";
|
||||
if (self === receiver && other === initiator) return `${other.name || "\u76f8\u624b"}\u306b\u3051\u3093\u304b\u3092\u58f2\u3089\u308c\u305f`;
|
||||
return `${other.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u5165\u3089\u306a\u3044`;
|
||||
};
|
||||
const textFor = (self, other) => typeof buildReasonText === "function"
|
||||
? buildReasonText("social", (typeof getTarinaiBehaviorTiedNeeds === "function" ? getTarinaiBehaviorTiedNeeds(self, "social") : self.behavior?.tiedNeeds) || ["social"], { id: "fight_rival", need: "social", subNeed: "conflict", label: self === receiver ? "\u53cd\u6483\u3057\u3066\u3044\u308b" : `${other.name || "\u76f8\u624b"}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b` }, self, this, { causeText: causeFor(self, other) })
|
||||
: `${other.name || "\u76f8\u624b"}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b`;
|
||||
const reasonA = textFor(a, b);
|
||||
const reasonB = textFor(b, a);
|
||||
a.counterAttackFromId = a === receiver ? initiator.id : null;
|
||||
b.counterAttackFromId = b === receiver ? initiator.id : null;
|
||||
a.setActionState("fight", { target: b, reason: reasonA });
|
||||
b.setActionState("fight", { target: a, reason: reasonB });
|
||||
if (typeof setLiveActionText === "function") {
|
||||
setLiveActionText(a, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: "喧嘩している", reasonText: `${b.name || "相手"}と喧嘩している`, target: b, phase: "perform", source: "behavior" });
|
||||
setLiveActionText(b, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: "喧嘩している", reasonText: `${a.name || "相手"}と喧嘩している`, target: a, phase: "perform", source: "behavior" });
|
||||
setLiveActionText(a, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: a === receiver ? "\u53cd\u6483\u3057\u3066\u3044\u308b" : "\u55a7\u5629\u3057\u3066\u3044\u308b", reasonText: reasonA, causeText: causeFor(a, b), target: b, phase: "perform", source: "behavior" });
|
||||
setLiveActionText(b, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: b === receiver ? "\u53cd\u6483\u3057\u3066\u3044\u308b" : "\u55a7\u5629\u3057\u3066\u3044\u308b", reasonText: reasonB, causeText: causeFor(b, a), target: a, phase: "perform", source: "behavior" });
|
||||
}
|
||||
a.fightTimer = Math.max(a.fightTimer, rand(4.5, 7.0));
|
||||
b.fightTimer = Math.max(b.fightTimer, rand(4.5, 7.0));
|
||||
a.fightTimer = Math.max(a.fightTimer, rand(3.2, 4.8));
|
||||
b.fightTimer = Math.max(b.fightTimer, rand(3.2, 4.8));
|
||||
a.nextHeadbutt = this.time + rand(0.08, 0.18);
|
||||
b.nextHeadbutt = a.nextHeadbutt;
|
||||
a.fightCooldown = CONFIG.fightCooldown + rand(1, 5);
|
||||
|
|
@ -761,6 +819,7 @@
|
|||
this.log(`${a.name}\u3068${b.name}\u304c\u5c0f\u3055\u306a\u55a7\u5629\u3092\u3057\u305f\u3002`, "fight", { participants: [a, b] });
|
||||
a.lastLog = b.lastLog = this.time;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@
|
|||
<div class="signboard-editor-panel" role="dialog" aria-modal="true" aria-labelledby="signboardEditorTitle">
|
||||
<div class="signboard-editor-titlebar">
|
||||
<h2 id="signboardEditorTitle">\u770b\u677f</h2>
|
||||
<button id="signboardEditorClose" class="signboard-editor-close" type="button" aria-label="\u9589\u3058\u308b">×</button>
|
||||
<button id="signboardEditorClose" class="signboard-editor-close" type="button" aria-label="\u9589\u3058\u308b">\u00d7</button>
|
||||
</div>
|
||||
<textarea id="signboardEditorText" maxlength="${SIGNBOARD_TEXT_LIMIT}" rows="3" spellcheck="false"></textarea>
|
||||
<div class="signboard-editor-meta"><span>6文字×3行まで</span><span id="signboardEditorCount">0/${SIGNBOARD_TEXT_LIMIT}</span></div>
|
||||
<textarea id="signboardEditorText" maxlength="${SIGNBOARD_TEXT_LIMIT + SIGNBOARD_LINES - 1}" rows="3" cols="6" wrap="off" spellcheck="false" inputmode="text"></textarea>
|
||||
<div class="signboard-editor-meta"><span>6\u6587\u5b57\u00d73\u884c\u307e\u3067</span><span id="signboardEditorCount">0/${SIGNBOARD_TEXT_LIMIT}</span></div>
|
||||
<div class="signboard-editor-actions">
|
||||
<button id="signboardEditorClear" class="btn" type="button">\u6d88\u3059</button>
|
||||
<button id="signboardEditorCancel" class="btn" type="button">\u30ad\u30e3\u30f3\u30bb\u30eb</button>
|
||||
|
|
@ -40,18 +40,14 @@
|
|||
|
||||
function sanitizeSignboardText(value = "") {
|
||||
const normalized = String(value || "").replace(/\r/g, "").replace(/[\t ]+/g, " ");
|
||||
const rawLines = normalized.split("\n");
|
||||
const rawLines = normalized.split("\n").slice(0, SIGNBOARD_LINES);
|
||||
const out = [];
|
||||
for (const rawLine of rawLines) {
|
||||
let rest = String(rawLine || "").trim();
|
||||
if (!rest && out.length < SIGNBOARD_LINES) { out.push(""); continue; }
|
||||
while (rest && out.length < SIGNBOARD_LINES) {
|
||||
out.push(sliceGraphemes(rest, SIGNBOARD_CHARS_PER_LINE));
|
||||
rest = Array.from(rest).slice(SIGNBOARD_CHARS_PER_LINE).join("");
|
||||
}
|
||||
if (out.length >= SIGNBOARD_LINES) break;
|
||||
for (let i = 0; i < SIGNBOARD_LINES; i += 1) {
|
||||
const rawLine = rawLines[i];
|
||||
if (rawLine == null) break;
|
||||
out.push(sliceGraphemes(String(rawLine), SIGNBOARD_CHARS_PER_LINE));
|
||||
}
|
||||
return out.slice(0, SIGNBOARD_LINES).join("\n").replace(/\n+$/g, "").trim();
|
||||
return out.join("\n").replace(/\n+$/g, "");
|
||||
}
|
||||
|
||||
function signboardVisibleLength(text = "") {
|
||||
|
|
@ -88,9 +84,39 @@
|
|||
dialog.__worldRef = worldRef;
|
||||
if (area) {
|
||||
area.value = sanitizeSignboardText(sign.text || "");
|
||||
area.onkeydown = (e) => {
|
||||
const value = String(area.value || "");
|
||||
const start = Number(area.selectionStart || 0);
|
||||
const end = Number(area.selectionEnd || start);
|
||||
const before = value.slice(0, start);
|
||||
const selected = value.slice(start, end);
|
||||
const lineIndex = before.split("\n").length - 1;
|
||||
const lines = value.split("\n");
|
||||
const currentLine = lines[lineIndex] || "";
|
||||
const replacing = selected.length > 0;
|
||||
if (e.key === "Enter") {
|
||||
if (lines.length >= SIGNBOARD_LINES) e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.key && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
const selectedSameLine = !selected.includes("\n");
|
||||
const selectedCount = selectedSameLine ? Array.from(selected).length : 0;
|
||||
if (!replacing && Array.from(currentLine).length >= SIGNBOARD_CHARS_PER_LINE) e.preventDefault();
|
||||
else if (replacing && selectedSameLine && Array.from(currentLine).length - selectedCount >= SIGNBOARD_CHARS_PER_LINE) e.preventDefault();
|
||||
}
|
||||
};
|
||||
area.onpaste = (e) => {
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData?.getData?.("text") || "";
|
||||
const start = Number(area.selectionStart || 0);
|
||||
const end = Number(area.selectionEnd || start);
|
||||
const next = sanitizeSignboardText((area.value || "").slice(0, start) + text + (area.value || "").slice(end));
|
||||
area.value = next;
|
||||
syncCount();
|
||||
};
|
||||
area.oninput = () => {
|
||||
const clean = sanitizeSignboardText(area.value || "");
|
||||
if (area.value !== clean && signboardVisibleLength(area.value || "") > SIGNBOARD_TEXT_LIMIT) area.value = clean;
|
||||
if (area.value !== clean) area.value = clean;
|
||||
syncCount();
|
||||
};
|
||||
setTimeout(() => { area.focus(); area.select(); syncCount(); }, 0);
|
||||
|
|
@ -219,6 +245,14 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
if (item.type === "grass_bed") {
|
||||
const spacing = Math.max(46, (item.r || 18) * 3.0);
|
||||
for (const it of this.nearbyItems(item.x, item.y, spacing + 70) || []) {
|
||||
if (!it || it === item || it.dead || it.type !== "grass_bed") continue;
|
||||
const required = spacing + Math.max(0, ((it.r || 18) - (item.r || 18)) * 0.5);
|
||||
if (distXY(item.x, item.y, it.x, it.y) < required) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
|
|
@ -302,6 +336,10 @@
|
|||
if (d <= hit && d < foundItemD) { foundItem = it; foundItemD = d; }
|
||||
}
|
||||
if (foundItem && isPinType(foundItem.type) && foundItem.pinState === "lodged") return foundItem;
|
||||
if (foundT) {
|
||||
const lodged = foundT.currentLodgedPin?.() || (this.items || []).find(it => it && isPinType(it.type) && it.pinState === "lodged" && it.pinTargetId === foundT.id);
|
||||
if (lodged && lodged.detachPushpin) return lodged;
|
||||
}
|
||||
if (foundT && foundItem) {
|
||||
const td = distXY(x, y, foundT.x, foundT.y);
|
||||
return td <= foundItemD + 8 ? foundT : foundItem;
|
||||
|
|
@ -332,13 +370,11 @@
|
|||
item.dropTimer = item.dropMax;
|
||||
item.dropImpactDone = false;
|
||||
}
|
||||
this.items.push(item);
|
||||
this.itemCounts[item.type] = (this.itemCounts[item.type] || 0) + 1;
|
||||
this.markTerrainDirty?.(`place:${item.type}`);
|
||||
this.addItem?.(item, `place:${item.type}`) || this.items.push(item);
|
||||
this.emit("tool:place", { item, type: item.type, dropped });
|
||||
this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type });
|
||||
if (dropped) audio.place?.(item.type);
|
||||
this.rebuildSpatial(true);
|
||||
this.ensureSpatial?.(`place:${item.type}`);
|
||||
return item;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,49 @@
|
|||
return window.TarinaiPerformance?.quality?.(this) || { level: this.performanceLevel(), antStride: 1, effectLimit: CONFIG.effectLimit ?? 96 };
|
||||
},
|
||||
|
||||
runtimeDiagnostics() {
|
||||
const behaviorCounts = {};
|
||||
let forcedBehaviors = 0;
|
||||
let lockedBehaviors = 0;
|
||||
for (const t of this.tarinai || []) {
|
||||
if (!t || t.dead) continue;
|
||||
const behavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(t) : t.behavior;
|
||||
const id = behavior?.id || t.state || "none";
|
||||
behaviorCounts[id] = (behaviorCounts[id] || 0) + 1;
|
||||
if (behavior?.forced) forcedBehaviors += 1;
|
||||
if ((t.intentLockTimer || 0) > 0.01) lockedBehaviors += 1;
|
||||
}
|
||||
return {
|
||||
spatial: {
|
||||
version: this.spatialVersion || 0,
|
||||
dirty: Boolean(this.spatialDirty),
|
||||
dirtyReason: this.spatialDirtyReason || this.lastSpatialDirtyReason || "",
|
||||
dirtyMarksThisFrame: this.spatialDirtyMarksThisFrame || 0,
|
||||
dirtyMarksTotal: this.spatialDirtyMarksTotal || 0,
|
||||
rebuildsThisFrame: this.spatialRebuildsThisFrame || 0,
|
||||
rebuildsTotal: this.spatialRebuildsTotal || 0,
|
||||
lastRebuildReason: this.lastSpatialRebuildReason || "",
|
||||
},
|
||||
render: {
|
||||
drawList: (this.drawList || []).length,
|
||||
drawListDirty: Boolean(this.drawListDirty),
|
||||
drawSortTimer: Number(this.drawSortTimer || 0),
|
||||
},
|
||||
items: {
|
||||
total: (this.items || []).length,
|
||||
bucketTypes: this.itemTypeBuckets?.size || 0,
|
||||
bucketsDirty: Boolean(this.itemBucketsDirty),
|
||||
idMapSize: this.itemIdMap?.size || 0,
|
||||
bucketRebuildsTotal: this.itemBucketRebuildsTotal || 0,
|
||||
},
|
||||
behavior: {
|
||||
forced: forcedBehaviors,
|
||||
locked: lockedBehaviors,
|
||||
counts: behaviorCounts,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
grassLoadLevel() {
|
||||
const grass = this.itemCounts?.grass || 0;
|
||||
const perf = this.performanceLevel();
|
||||
|
|
@ -52,40 +95,124 @@
|
|||
return this.drawList;
|
||||
},
|
||||
|
||||
ensureSpatial(reason = "read") {
|
||||
if (!this.spatial) return false;
|
||||
if (!this.spatialDirty && (this.spatialVersion || 0) > 0) return false;
|
||||
return this.rebuildSpatial(true, reason);
|
||||
},
|
||||
|
||||
nearbyItems(x, y, radius, precise = false) {
|
||||
this.ensureSpatial?.("nearby-items");
|
||||
return this.spatial.nearby(this.spatial.itemCells, x, y, radius, this.spatialItemScratch, precise);
|
||||
},
|
||||
|
||||
nearbyTarinai(x, y, radius, precise = false) {
|
||||
this.ensureSpatial?.("nearby-tarinai");
|
||||
return this.spatial.nearby(this.spatial.tarinaiCells, x, y, radius, this.spatialTarinaiScratch, precise);
|
||||
},
|
||||
|
||||
nearbyAnts(x, y, radius, precise = false) {
|
||||
this.ensureSpatial?.("nearby-ants");
|
||||
return this.spatial.nearby(this.spatial.antCells, x, y, radius, this.spatialAntScratch, precise);
|
||||
},
|
||||
|
||||
nearbyObstacles(x, y, radius, precise = false) {
|
||||
this.ensureSpatial?.("nearby-obstacles");
|
||||
return this.spatial.nearby(this.spatial.obstacleCells, x, y, radius, this.spatialObstacleScratch, precise);
|
||||
},
|
||||
|
||||
nearbyFood(x, y, radius, precise = false) {
|
||||
this.ensureSpatial?.("nearby-food");
|
||||
return this.spatial.nearby(this.spatial.foodCells, x, y, radius, this.spatialFoodScratch, precise);
|
||||
},
|
||||
|
||||
nearbyHazards(x, y, radius, precise = false) {
|
||||
this.ensureSpatial?.("nearby-hazards");
|
||||
return this.spatial.nearby(this.spatial.hazardCells, x, y, radius, this.spatialHazardScratch, precise);
|
||||
},
|
||||
|
||||
rebuildSpatial(force = false) {
|
||||
rebuildSpatial(force = false, reason = "manual") {
|
||||
// Direct calls keep their old eager semantics. update() uses markSpatialDirty + ensureSpatial
|
||||
// so repeated rebuild requests in the same phase are collapsed to one rebuild.
|
||||
if (!force && this.spatialDirty === false && reason !== "manual") return false;
|
||||
this.spatial.rebuild(this.items, this.tarinai, this.ants || []);
|
||||
this.spatialDirty = false;
|
||||
this.spatialDirtyReason = "";
|
||||
this.spatialVersion = (this.spatialVersion || 0) + 1;
|
||||
this.spatialRebuildsThisFrame = (this.spatialRebuildsThisFrame || 0) + 1;
|
||||
this.spatialRebuildsTotal = (this.spatialRebuildsTotal || 0) + 1;
|
||||
this.lastSpatialRebuildReason = reason;
|
||||
return true;
|
||||
},
|
||||
|
||||
updateItemCounts() {
|
||||
markItemBucketsDirty(reason = "manual") {
|
||||
this.itemBucketsDirty = true;
|
||||
this.itemBucketsDirtyReason = reason;
|
||||
this.lastItemBucketsDirtyReason = reason;
|
||||
this.itemBucketDirtyMarksTotal = (this.itemBucketDirtyMarksTotal || 0) + 1;
|
||||
},
|
||||
|
||||
ensureItemBuckets(reason = "read") {
|
||||
if (!this.itemTypeBuckets || !this.itemIdMap || this.itemBucketsDirty) {
|
||||
this.updateItemCounts(reason);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
addItem(item, reason = "add-item", opts = {}) {
|
||||
if (!item) return null;
|
||||
this.items.push(item);
|
||||
this.markItemBucketsDirty?.(reason);
|
||||
this.markSpatialDirty?.(reason);
|
||||
if (opts.terrain !== false) this.markTerrainDirty?.(reason);
|
||||
if (opts.countNow !== false && item.type) this.itemCounts[item.type] = (this.itemCounts[item.type] || 0) + 1;
|
||||
return item;
|
||||
},
|
||||
|
||||
itemById(id) {
|
||||
if (id == null || id === "") return null;
|
||||
this.ensureItemBuckets?.("item-by-id");
|
||||
const item = this.itemIdMap?.get?.(id) || null;
|
||||
return item && !item.dead ? item : null;
|
||||
},
|
||||
|
||||
itemsOfType(type) {
|
||||
this.ensureItemBuckets?.(`items-of-type:${type}`);
|
||||
const bucket = this.itemTypeBuckets?.get?.(type);
|
||||
return bucket || [];
|
||||
},
|
||||
|
||||
liveItemsOfType(type, predicate = null) {
|
||||
const bucket = this.itemsOfType(type);
|
||||
if (typeof predicate !== "function") return bucket;
|
||||
const out = [];
|
||||
for (const item of bucket) if (item && !item.dead && predicate(item)) out.push(item);
|
||||
return out;
|
||||
},
|
||||
|
||||
updateItemCounts(reason = "manual") {
|
||||
const counts = this.itemCounts;
|
||||
if (!this.itemTypeBuckets) this.itemTypeBuckets = new Map();
|
||||
if (!this.itemIdMap) this.itemIdMap = new Map();
|
||||
for (const key of Object.keys(counts)) counts[key] = 0;
|
||||
for (const it of this.items) counts[it.type] = (counts[it.type] || 0) + 1;
|
||||
this.itemTypeBuckets.clear();
|
||||
this.itemIdMap.clear();
|
||||
for (const it of this.items) {
|
||||
if (!it || it.dead) continue;
|
||||
counts[it.type] = (counts[it.type] || 0) + 1;
|
||||
if (it.id != null) this.itemIdMap.set(it.id, it);
|
||||
let bucket = this.itemTypeBuckets.get(it.type);
|
||||
if (!bucket) {
|
||||
bucket = [];
|
||||
this.itemTypeBuckets.set(it.type, bucket);
|
||||
}
|
||||
bucket.push(it);
|
||||
}
|
||||
this.itemBucketsDirty = false;
|
||||
this.itemBucketsDirtyReason = "";
|
||||
this.itemBucketRebuildsTotal = (this.itemBucketRebuildsTotal || 0) + 1;
|
||||
this.lastItemBucketRebuildReason = reason;
|
||||
},
|
||||
|
||||
updateEffectCounts() {
|
||||
|
|
@ -108,6 +235,7 @@
|
|||
if (changed) {
|
||||
this.markSpatialDirty?.("compact-items");
|
||||
this.markTerrainDirty?.("compact-items");
|
||||
this.markItemBucketsDirty?.("compact-items");
|
||||
}
|
||||
return changed;
|
||||
},
|
||||
|
|
@ -115,11 +243,11 @@
|
|||
enforceItemBudget() {
|
||||
const pruneOldest = (type, limit) => {
|
||||
if (!Number.isFinite(limit) || limit < 0) return;
|
||||
let count = 0;
|
||||
for (const it of this.items) if (it.type === type && !it.dead) count += 1;
|
||||
const live = this.liveItemsOfType?.(type) || (this.items || []).filter(it => it && it.type === type && !it.dead);
|
||||
const count = live.length;
|
||||
if (count <= limit) return;
|
||||
const victims = this.items
|
||||
.filter(it => it.type === type && !it.dead)
|
||||
const victims = live
|
||||
.slice()
|
||||
.sort((a, b) => (b.age || 0) - (a.age || 0))
|
||||
.slice(0, count - limit);
|
||||
for (const it of victims) it.amount = 0;
|
||||
|
|
@ -131,8 +259,8 @@
|
|||
const fenceLimit = this.fenceLimit ? this.fenceLimit() : (CONFIG.fenceLimit ?? 72);
|
||||
const liveFences = this.liveFenceCount ? this.liveFenceCount() : 0;
|
||||
if (liveFences > fenceLimit) {
|
||||
const victims = this.items
|
||||
.filter(it => this.isFenceType?.(it.type) && !it.dead)
|
||||
const victims = (this.items || [])
|
||||
.filter(it => it && this.isFenceType?.(it.type) && !it.dead)
|
||||
.sort((a, b) => (b.age || 0) - (a.age || 0))
|
||||
.slice(0, liveFences - fenceLimit);
|
||||
for (const it of victims) it.amount = 0;
|
||||
|
|
@ -141,8 +269,8 @@
|
|||
const limit = CONFIG.itemLimit ?? Infinity;
|
||||
if (!Number.isFinite(limit) || this.items.length <= limit) return;
|
||||
const priority = { trace: 1, splat: 2, zunchi: 3, water: 4, grass: 5, sweet: 6, firecracker: 7, fence_v: 8, fence_h: 8 };
|
||||
const removable = this.items
|
||||
.filter(it => !it.dead && (priority[it.type] || 99) < 99)
|
||||
const removable = (this.items || [])
|
||||
.filter(it => it && !it.dead && (priority[it.type] || 99) < 99)
|
||||
.sort((a, b) => (priority[a.type] || 99) - (priority[b.type] || 99) || (b.age || 0) - (a.age || 0));
|
||||
let extra = this.items.length - limit;
|
||||
for (const it of removable) {
|
||||
|
|
|
|||
|
|
@ -43,15 +43,15 @@
|
|||
|
||||
colonyMoodDefinition(id = "relaxed") {
|
||||
const defs = {
|
||||
calm: { id: "calm", label: "おだやか", description: "社交性プラス", effects: { personality: { sociability: 0.42 } } },
|
||||
prickly: { id: "prickly", label: "ピリピリ", description: "攻撃性プラス", effects: { personality: { aggression: 0.46 } } },
|
||||
stinky: { id: "stinky", label: "くさい", description: "開放性マイナス", effects: { personality: { openness: -0.44 } } },
|
||||
weaklings: { id: "weaklings", label: "ざこだらけ", description: "ずんちどれい以外のストレス減少", effects: { personality: {} } },
|
||||
zunda_party: { id: "zunda_party", label: "ずんだぱーてぃー", description: "社交性と開放性プラス。ストレス減少", effects: { personality: { sociability: 0.38, openness: 0.38 } } },
|
||||
ball_party: { id: "ball_party", label: "ぼーるぱーてぃー", description: "社交性と開放性プラス。ストレス減少", effects: { personality: { sociability: 0.38, openness: 0.38 } } },
|
||||
sun_party: { id: "sun_party", label: "おひさまぱーてぃー", description: "日光浴頻度増加", effects: { personality: {} } },
|
||||
ant_overrun: { id: "ant_overrun", label: "ありだらけ", description: "アリを見るとパニック", effects: { personality: {} } },
|
||||
relaxed: { id: "relaxed", label: "のんびり", description: "何も無し", effects: { personality: {} } },
|
||||
calm: { id: "calm", label: "\u304a\u3060\u3084\u304b", description: "\u793e\u4ea4\u6027\u30d7\u30e9\u30b9", effects: { personality: { sociability: 0.42 } } },
|
||||
prickly: { id: "prickly", label: "\u30d4\u30ea\u30d4\u30ea", description: "\u653b\u6483\u6027\u30d7\u30e9\u30b9", effects: { personality: { aggression: 0.46 } } },
|
||||
stinky: { id: "stinky", label: "\u304f\u3055\u3044", description: "\u958b\u653e\u6027\u30de\u30a4\u30ca\u30b9", effects: { personality: { openness: -0.44 } } },
|
||||
weaklings: { id: "weaklings", label: "\u3056\u3053\u3060\u3089\u3051", description: "\u305a\u3093\u3061\u3069\u308c\u3044\u4ee5\u5916\u306e\u30b9\u30c8\u30ec\u30b9\u6e1b\u5c11", effects: { personality: {} } },
|
||||
zunda_party: { id: "zunda_party", label: "\u305a\u3093\u3060\u3071\u30fc\u3066\u3043\u30fc", description: "\u793e\u4ea4\u6027\u3068\u958b\u653e\u6027\u30d7\u30e9\u30b9\u3002\u30b9\u30c8\u30ec\u30b9\u6e1b\u5c11", effects: { personality: { sociability: 0.38, openness: 0.38 } } },
|
||||
ball_party: { id: "ball_party", label: "\u307c\u30fc\u308b\u3071\u30fc\u3066\u3043\u30fc", description: "\u793e\u4ea4\u6027\u3068\u958b\u653e\u6027\u30d7\u30e9\u30b9\u3002\u30b9\u30c8\u30ec\u30b9\u6e1b\u5c11", effects: { personality: { sociability: 0.38, openness: 0.38 } } },
|
||||
sun_party: { id: "sun_party", label: "\u304a\u3072\u3055\u307e\u3071\u30fc\u3066\u3043\u30fc", description: "\u65e5\u5149\u6d74\u983b\u5ea6\u5897\u52a0", effects: { personality: {} } },
|
||||
ant_overrun: { id: "ant_overrun", label: "\u3042\u308a\u3060\u3089\u3051", description: "\u30a2\u30ea\u3092\u898b\u308b\u3068\u30d1\u30cb\u30c3\u30af", effects: { personality: {} } },
|
||||
relaxed: { id: "relaxed", label: "\u306e\u3093\u3073\u308a", description: "\u4f55\u3082\u7121\u3057", effects: { personality: {} } },
|
||||
};
|
||||
return defs[id] || defs.relaxed;
|
||||
},
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
const nextId = candidates.length ? candidates[Math.floor(Math.random() * candidates.length)] : "relaxed";
|
||||
this.colonyMood = this.colonyMoodDefinition(nextId);
|
||||
this.lastColonyMoodDay = this.day || 1;
|
||||
this.log?.(`今日のコロニーの雰囲気は「${this.colonyMood.label}」。`, "event");
|
||||
this.log?.(`\u4eca\u65e5\u306e\u30b3\u30ed\u30cb\u30fc\u306e\u96f0\u56f2\u6c17\u306f\u300c${this.colonyMood.label}\u300d\u3002`, "event");
|
||||
return this.colonyMood;
|
||||
},
|
||||
|
||||
|
|
@ -86,12 +86,47 @@
|
|||
const tFamilyKey = this.tarinaiFamilyKey(t);
|
||||
if ((s.parents || []).includes(tFamilyKey)) return "parent";
|
||||
if ((s.children || []).includes(tFamilyKey)) return "child";
|
||||
const friendThreshold = typeof FRIEND_AFFINITY_THRESHOLD === "number" ? FRIEND_AFFINITY_THRESHOLD : 14;
|
||||
const rel = s.relationTo ? s.relationTo(t.id) : null;
|
||||
if (rel && (rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD) return "friend";
|
||||
if (rel && (rel.fear || 0) >= 8) return "enemy";
|
||||
const reverse = t.relationTo ? t.relationTo(s.id) : null;
|
||||
if ((rel && (rel.affinity || 0) >= friendThreshold) || (reverse && (reverse.affinity || 0) >= friendThreshold)) return "friend";
|
||||
if ((rel && (rel.fear || 0) >= 8) || (reverse && (reverse.fear || 0) >= 8)) return "enemy";
|
||||
return "";
|
||||
},
|
||||
|
||||
resolveOwnedBedSleepConflicts() {
|
||||
const alive = (this.tarinai || []).filter(t => t && !t.dead);
|
||||
if (alive.length < 2) return 0;
|
||||
const friendThreshold = typeof FRIEND_AFFINITY_THRESHOLD === "number" ? FRIEND_AFFINITY_THRESHOLD : 14;
|
||||
const isFriendPair = (a, b) => {
|
||||
const relAB = a?.relationTo ? a.relationTo(b?.id) : null;
|
||||
const relBA = b?.relationTo ? b.relationTo(a?.id) : null;
|
||||
return Boolean((relAB && (relAB.affinity || 0) >= friendThreshold) || (relBA && (relBA.affinity || 0) >= friendThreshold));
|
||||
};
|
||||
const bedSleepers = new Map();
|
||||
for (const sleeper of alive) {
|
||||
if (!(sleeper.sleeping || sleeper.state === "sleep")) continue;
|
||||
const bed = sleeper.target;
|
||||
if (!bed || bed.dead || bed.type !== "grass_bed") continue;
|
||||
if (distXY(sleeper.x, sleeper.y, bed.x, bed.y) > Math.max((bed.r || 18) * 1.55, 32)) continue;
|
||||
if (!bedSleepers.has(bed.id)) bedSleepers.set(bed.id, []);
|
||||
bedSleepers.get(bed.id).push(sleeper);
|
||||
}
|
||||
let conflicts = 0;
|
||||
for (const intruder of alive) {
|
||||
if (!(intruder.state === "seek_bed" || intruder.state === "sleep")) continue;
|
||||
const bed = intruder.target;
|
||||
if (!bed || bed.dead || bed.type !== "grass_bed") continue;
|
||||
const sleepers = bedSleepers.get(bed.id) || [];
|
||||
if (!sleepers.length) continue;
|
||||
if (distXY(intruder.x, intruder.y, bed.x, bed.y) > Math.max((bed.r || 18) * 2.2, 54)) continue;
|
||||
const defender = sleepers.find(other => other && other !== intruder && !this.areParentChild?.(intruder, other) && !isFriendPair(intruder, other));
|
||||
if (!defender) continue;
|
||||
if (this.startForcedFight?.(defender, intruder)) conflicts += 1;
|
||||
}
|
||||
return conflicts;
|
||||
},
|
||||
|
||||
nestBoxOccupants(box, limit = 5) {
|
||||
if (!box || box.dead || box.type !== "nest_box") return [];
|
||||
const occupants = this.tarinai.filter(t => t && !t.dead && t.insideNestBoxId === box.id);
|
||||
|
|
@ -188,24 +223,30 @@
|
|||
this.updateWeather(dt);
|
||||
this.foodSpoilagePenalty = Math.max(0, (this.foodSpoilagePenalty || 0) - dt * 0.010);
|
||||
this.updateTerrainDirtyState?.(dt);
|
||||
this.rebuildSpatial();
|
||||
this.spatialRebuildsThisFrame = 0;
|
||||
this.spatialDirtyMarksThisFrame = 0;
|
||||
this.ensureSpatial?.("update-start");
|
||||
|
||||
const perf = this.performanceLevel();
|
||||
const itemCountBefore = this.items.length;
|
||||
for (const it of this.items) it.update(dt, this);
|
||||
if (this.items.length) this.markSpatialDirty?.("items-updated");
|
||||
if (this.itemCounts?.ball) {
|
||||
this.rebuildSpatial();
|
||||
this.ensureSpatial?.("ball-ball-collisions");
|
||||
this.resolveBallBallCollisions(dt);
|
||||
this.rebuildSpatial();
|
||||
this.markSpatialDirty?.("ball-ball-collisions");
|
||||
}
|
||||
this.updateAnts?.(dt);
|
||||
if ((this.ants || []).length) this.markSpatialDirty?.("ants-updated");
|
||||
|
||||
for (const ef of this.effects) ef.update(dt);
|
||||
|
||||
for (const t of this.tarinai) {
|
||||
t.update(dt);
|
||||
}
|
||||
this.rebuildSpatial();
|
||||
this.resolveOwnedBedSleepConflicts?.();
|
||||
if (this.tarinai.length) this.markSpatialDirty?.("tarinai-updated");
|
||||
this.ensureSpatial?.("post-tarinai-update");
|
||||
this.limitBallChasers();
|
||||
this.resolveBallInteractions(dt);
|
||||
this.resolveTarinaiHighSpeedCollisions(dt);
|
||||
|
|
@ -259,7 +300,7 @@
|
|||
|
||||
|
||||
const grassCount = this.itemCounts.grass || 0;
|
||||
if (grassCount < Math.min(54, this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 180)) && Math.random() < dt * 0.082) {
|
||||
if (grassCount < Math.min(108, this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 360)) && Math.random() < dt * 0.109) {
|
||||
const spot = this.findGrassPlantingSpot(rand(60, this.w - 60), rand(60, this.h - 60), {
|
||||
allowOriginal: true,
|
||||
minRadius: 28,
|
||||
|
|
@ -270,9 +311,7 @@
|
|||
const sprout = new Item("grass", spot.x, spot.y);
|
||||
sprout.amount = rand(30, 62);
|
||||
sprout.growth = rand(0.18, 0.38);
|
||||
this.items.push(sprout);
|
||||
this.itemCounts.grass = grassCount + 1;
|
||||
this.markTerrainDirty?.("grass-spawn");
|
||||
this.addItem?.(sprout, "grass-spawn") || this.items.push(sprout);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -281,12 +320,12 @@
|
|||
// share the rain overlay renderer.
|
||||
if (window.TarinaiWeatherSystem?.shouldDropRainWater?.(this, dt)) {
|
||||
const drop = window.TarinaiWeatherSystem.createRainWaterItem(this);
|
||||
this.items.push(drop);
|
||||
this.itemCounts.water = (this.itemCounts.water || 0) + 1;
|
||||
this.markTerrainDirty?.("rain-water");
|
||||
this.addItem?.(drop, "rain-water") || this.items.push(drop);
|
||||
this.emit?.("rain:itemDropped", { item: drop, type: "water" });
|
||||
}
|
||||
|
||||
this.lastRuntimeDiagnostics = this.runtimeDiagnostics?.() || null;
|
||||
|
||||
},
|
||||
|
||||
sanitizeOutOfBounds() {
|
||||
|
|
@ -360,7 +399,8 @@
|
|||
if (changedItems) {
|
||||
this.compactItems();
|
||||
this.updateItemCounts();
|
||||
this.rebuildSpatial();
|
||||
this.markSpatialDirty?.("sanitize-out-of-bounds");
|
||||
this.ensureSpatial?.("sanitize-out-of-bounds");
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@
|
|||
markSpatialDirty(reason = "manual") {
|
||||
this.spatialDirty = true;
|
||||
this.spatialDirtyReason = reason;
|
||||
this.spatialDirtyMarksThisFrame = (this.spatialDirtyMarksThisFrame || 0) + 1;
|
||||
this.spatialDirtyMarksTotal = (this.spatialDirtyMarksTotal || 0) + 1;
|
||||
this.lastSpatialDirtyReason = reason;
|
||||
},
|
||||
|
||||
markTerrainDirty(reason = "manual") {
|
||||
|
|
|
|||
BIN
scripts/__pycache__/regression_check.cpython-313.pyc
Normal file
BIN
scripts/__pycache__/regression_check.cpython-313.pyc
Normal file
Binary file not shown.
|
|
@ -4,21 +4,34 @@ This project intentionally stays buildless; this script is a lightweight manifes
|
|||
"""
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import json, re
|
||||
import json
|
||||
import re
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
manifest = json.loads((ROOT / "app_manifest.json").read_text(encoding="utf-8"))
|
||||
version = manifest["version"]
|
||||
css_files = manifest["css"]
|
||||
js_files = manifest["js"]
|
||||
|
||||
index = ROOT / "index.html"
|
||||
s = index.read_text(encoding="utf-8")
|
||||
css_block = "".join([f'\n <link rel="stylesheet" href="{f}?v={version}" />' for f in css_files])
|
||||
s = re.sub(r'\s*<link rel="stylesheet" href="css/[^\"]+" />(?:\s*<link rel="stylesheet" href="css/[^\"]+" />)*', css_block, s, count=1)
|
||||
s = re.sub(
|
||||
r'\s*<link rel="stylesheet" href="css/[^\"]+" />(?:\s*<link rel="stylesheet" href="css/[^\"]+" />)*',
|
||||
css_block,
|
||||
s,
|
||||
count=1,
|
||||
)
|
||||
script_block = "\n".join([f' <script src="{f}?v={version}" defer></script>' for f in js_files])
|
||||
s = re.sub(r'\n\s*<script src="js/version\.js\?v=[^\"]+" defer></script>[\s\S]*?\n\s*<script src="js/debug_tools\.js\?v=[^\"]+" defer></script>', "\n" + script_block, s, count=1)
|
||||
s = re.sub(
|
||||
r'\n\s*<script src="js/version\.js\?v=[^\"]+" defer></script>[\s\S]*?\n\s*<script src="js/debug_tools\.js\?v=[^\"]+" defer></script>',
|
||||
"\n" + script_block,
|
||||
s,
|
||||
count=1,
|
||||
)
|
||||
index.write_text(s, encoding="utf-8")
|
||||
|
||||
core_names = [Path(f).stem for f in js_files]
|
||||
css_entries = ",\n ".join([f'`./{f}?${{v}}`' for f in css_files])
|
||||
js_names = ", ".join([json.dumps(n, ensure_ascii=False) for n in core_names])
|
||||
(ROOT / "service-worker.js").write_text(f'''"use strict";\n\nconst APP_VERSION = "{version}";\nconst CACHE_NAME = `tarinai-colony-${{APP_VERSION}}`;\nconst v = `v=${{APP_VERSION}}`;\n// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.\nconst coreScriptNames = [\n {js_names}\n];\nconst CORE_ASSETS = [\n "./",\n "./index.html",\n {css_entries},\n ...coreScriptNames.map(name => `./js/${{name}}.js?${{v}}`),\n];\n\nself.addEventListener("install", (event) => {{\n event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(CORE_ASSETS)).catch(() => undefined));\n self.skipWaiting();\n}});\n\nself.addEventListener("activate", (event) => {{\n event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)))));\n self.clients.claim();\n}});\n\nfunction isStaticAsset(request) {{\n const url = new URL(request.url);\n return /\\.(?:js|css|webp|png|jpg|jpeg|gif|svg|mp3|wav|woff2?)$/i.test(url.pathname);\n}}\n\nself.addEventListener("fetch", (event) => {{\n const request = event.request;\n if (request.method !== "GET") return;\n const url = new URL(request.url);\n if (url.origin !== self.location.origin) return;\n if (/service-worker\\.js$/i.test(url.pathname)) return;\n if (request.mode === "navigate" || /index\\.html$/i.test(url.pathname)) {{\n event.respondWith(fetch(request).then(response => {{\n const copy = response.clone();\n caches.open(CACHE_NAME).then(cache => cache.put(request, copy));\n return response;\n }}).catch(() => caches.match(request).then(cached => cached || caches.match("./index.html"))));\n return;\n }}\n if (isStaticAsset(request)) {{\n event.respondWith(caches.match(request).then(cached => cached || fetch(request).then(response => {{\n const copy = response.clone();\n caches.open(CACHE_NAME).then(cache => cache.put(request, copy));\n return response;\n }})));\n }}\n}});\n''', encoding="utf-8")
|
||||
(ROOT / "service-worker.js").write_text(f'''"use strict";\n\nconst APP_VERSION = "{version}";\nconst CACHE_NAME = `tarinai-colony-${{APP_VERSION}}`;\nconst v = `v=${{APP_VERSION}}`;\n// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.\nconst coreScriptNames = [\n {js_names}\n];\nconst CORE_ASSETS = [\n "./",\n "./index.html",\n {css_entries},\n ...coreScriptNames.map(name => `./js/${{name}}.js?${{v}}`),\n];\n\nself.addEventListener("install", (event) => {{\n event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(CORE_ASSETS)).catch(() => undefined));\n self.skipWaiting();\n}});\n\nself.addEventListener("activate", (event) => {{\n event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)))));\n self.clients.claim();\n}});\n\nfunction isStaticAsset(request) {{\n const url = new URL(request.url);\n return /\\.(?:js|css|webp|png|jpg|jpeg|gif|svg|mp3|wav|woff2?)$/i.test(url.pathname);\n}}\n\nfunction cacheableResponse(response) {{\n return response && response.status === 200 && response.type !== "opaque";\n}}\n\nfunction putCacheSafely(request, response) {{\n if (!cacheableResponse(response)) return Promise.resolve(false);\n return caches.open(CACHE_NAME).then(cache => cache.put(request, response.clone())).then(() => true).catch(() => false);\n}}\n\nself.addEventListener("fetch", (event) => {{\n const request = event.request;\n if (request.method !== "GET") return;\n const url = new URL(request.url);\n if (url.origin !== self.location.origin) return;\n if (/service-worker\\.js$/i.test(url.pathname)) return;\n if (request.mode === "navigate" || /index\\.html$/i.test(url.pathname)) {{\n event.respondWith(fetch(request).then(response => {{\n putCacheSafely(request, response);\n return response;\n }}).catch(() => caches.match(request).then(cached => cached || caches.match("./index.html"))));\n return;\n }}\n if (isStaticAsset(request)) {{\n event.respondWith(caches.match(request).then(cached => cached || fetch(request).then(response => {{\n putCacheSafely(request, response);\n return response;\n }})));\n }}\n}});\n''', encoding="utf-8")
|
||||
|
|
|
|||
408
scripts/regression_check.py
Normal file
408
scripts/regression_check.py
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Lightweight regression guard for the buildless Tarinai app.
|
||||
|
||||
Checks static-file consistency, generated-load order, syntax, and the ActionSpec
|
||||
registry contract. This intentionally avoids save-data migration checks; save
|
||||
compatibility was intentionally dropped in compact save v2.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MANIFEST = ROOT / "app_manifest.json"
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
print(f"[FAIL] {message}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def ok(message: str) -> None:
|
||||
print(f"[OK] {message}")
|
||||
|
||||
|
||||
def read_text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def manifest() -> dict:
|
||||
try:
|
||||
data = json.loads(read_text(MANIFEST))
|
||||
except Exception as exc: # pragma: no cover - diagnostic path
|
||||
fail(f"cannot read app_manifest.json: {exc}")
|
||||
for key in ("version", "css", "js"):
|
||||
if key not in data:
|
||||
fail(f"manifest missing {key}")
|
||||
return data
|
||||
|
||||
|
||||
def check_files_exist(data: dict) -> None:
|
||||
missing = [p for p in [*data["css"], *data["js"]] if not (ROOT / p).exists()]
|
||||
if missing:
|
||||
fail("manifest references missing files: " + ", ".join(missing))
|
||||
ok("manifest file references exist")
|
||||
|
||||
|
||||
def check_versions(data: dict) -> None:
|
||||
version = str(data["version"])
|
||||
version_js = read_text(ROOT / "js/version.js")
|
||||
service_worker = read_text(ROOT / "service-worker.js")
|
||||
if f'APP_VERSION = "{version}"' not in version_js:
|
||||
fail("js/version.js APP_VERSION does not match manifest")
|
||||
if f'APP_VERSION = "{version}"' not in service_worker:
|
||||
fail("service-worker.js APP_VERSION does not match manifest")
|
||||
ok(f"version synchronized: {version}")
|
||||
|
||||
|
||||
def check_index_order(data: dict) -> None:
|
||||
index = read_text(ROOT / "index.html")
|
||||
css = re.findall(r'<link rel="stylesheet" href="([^"]+)"\s*/>', index)
|
||||
scripts = re.findall(r'<script src="([^"]+)" defer></script>', index)
|
||||
css_clean = [entry.split("?", 1)[0] for entry in css if entry.startswith("css/")]
|
||||
script_clean = [entry.split("?", 1)[0] for entry in scripts if entry.startswith("js/")]
|
||||
if css_clean[: len(data["css"])] != data["css"]:
|
||||
fail("index.html CSS order differs from manifest")
|
||||
if script_clean[: len(data["js"])] != data["js"]:
|
||||
fail("index.html JS order differs from manifest")
|
||||
ok("index.html load order matches manifest")
|
||||
|
||||
|
||||
def check_service_worker_order(data: dict) -> None:
|
||||
sw = read_text(ROOT / "service-worker.js")
|
||||
match = re.search(r"const coreScriptNames = \[\s*(.*?)\s*\];", sw, re.S)
|
||||
if not match:
|
||||
fail("service-worker.js coreScriptNames not found")
|
||||
names = re.findall(r'"([^"]+)"', match.group(1))
|
||||
expected = [Path(p).stem for p in data["js"]]
|
||||
if names != expected:
|
||||
fail("service-worker.js coreScriptNames differs from manifest JS order")
|
||||
ok("service-worker.js core script order matches manifest")
|
||||
|
||||
|
||||
def node_check(paths: list[Path]) -> None:
|
||||
node = "node"
|
||||
for path in paths:
|
||||
result = subprocess.run([node, "--check", str(path)], cwd=ROOT, text=True, capture_output=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stdout)
|
||||
sys.stderr.write(result.stderr)
|
||||
fail(f"node --check failed: {path.relative_to(ROOT)}")
|
||||
ok(f"node --check passed for {len(paths)} files")
|
||||
|
||||
|
||||
def action_spec_smoke() -> None:
|
||||
source = read_text(ROOT / "js/tarinai_action_spec.js")
|
||||
js = f"""
|
||||
const vm = require('vm');
|
||||
const context = {{ console, globalThis: null }};
|
||||
context.globalThis = context;
|
||||
context.window = context;
|
||||
vm.createContext(context);
|
||||
vm.runInContext({source!r}, context, {{ filename: 'tarinai_action_spec.js' }});
|
||||
const action = context.registerTarinaiActionSpec({{
|
||||
id: 'smoke_action',
|
||||
need: 'fulfill',
|
||||
label: 'smoke',
|
||||
selectTarget() {{ return {{ id: 'target-1' }}; }},
|
||||
canStart(t, world, ctx) {{ const target = ctx.target || this.selectTarget(t, world, ctx); return target && target.id === 'target-1'; }},
|
||||
start(t, world, ctx) {{ t.startedWith = ctx.target.id; return true; }},
|
||||
tick() {{ return 'finished'; }},
|
||||
finish(t) {{ t.finished = true; return true; }},
|
||||
}});
|
||||
const subject = {{}};
|
||||
if (!context.canStartTarinaiAction('smoke_action', subject, {{}})) throw new Error('canStart failed');
|
||||
if (!context.startTarinaiAction(action, subject, {{}})) throw new Error('start failed');
|
||||
if (subject.startedWith !== 'target-1') throw new Error('selectTarget/start context failed');
|
||||
if (context.tickTarinaiAction('smoke_action', subject, {{}}, 0.1, {{}}) !== 'finished') throw new Error('tick failed');
|
||||
if (!context.finishTarinaiAction('smoke_action', subject, {{}}, {{ phase: 'finished' }})) throw new Error('finish failed');
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||
f.write(js)
|
||||
tmp = Path(f.name)
|
||||
try:
|
||||
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stdout)
|
||||
sys.stderr.write(result.stderr)
|
||||
fail("ActionSpec smoke test failed")
|
||||
ok("ActionSpec lifecycle smoke test passed")
|
||||
|
||||
|
||||
def behavior_state_smoke() -> None:
|
||||
source = read_text(ROOT / "js/tarinai_behavior_state.js")
|
||||
js = f"""
|
||||
const vm = require('vm');
|
||||
const context = {{ console, globalThis: null }};
|
||||
context.globalThis = context;
|
||||
context.window = context;
|
||||
context.Tarinai = class Tarinai {{}};
|
||||
vm.createContext(context);
|
||||
vm.runInContext({source!r}, context, {{ filename: 'tarinai_behavior_state.js' }});
|
||||
const t = new context.Tarinai();
|
||||
t.world = {{ time: 12 }};
|
||||
t.state = 'seek_food';
|
||||
context.setTarinaiBehavior(t, {{ id: 'eat_food', need: 'food', label: 'eat', reasonText: 'hungry', tiedNeeds: ['food', 'sleep'], forced: true, forcedId: 'f1', source: 'test', forcedRequest: {{ uid: 'f1', id: 'eat_food', source: 'test', priority: 99, reasonText: 'forced hungry', status: 'active' }} }});
|
||||
if (context.getTarinaiBehaviorId(t) !== 'eat_food') throw new Error('behavior id mismatch');
|
||||
if (context.getTarinaiBehaviorNeed(t) !== 'food') throw new Error('behavior need mismatch');
|
||||
if (!context.isTarinaiBehaviorForced(t)) throw new Error('forced flag not visible');
|
||||
const forced = context.getTarinaiForcedBehavior(t);
|
||||
if (!forced || forced.uid !== 'f1' || forced.priority !== 99) throw new Error('forced request not normalized');
|
||||
if (t.intent.actionId !== 'eat_food' || t.currentAction.need !== 'food' || t.activeForcedBehavior.uid !== 'f1') throw new Error('legacy views not synced');
|
||||
context.setTarinaiForcedBehavior(t, null);
|
||||
if (context.currentTarinaiBehavior(t).forced || context.getTarinaiForcedBehavior(t)) throw new Error('forced flag not cleared');
|
||||
context.clearTarinaiBehavior(t);
|
||||
if (context.currentTarinaiBehavior(t) !== null) throw new Error('behavior not cleared');
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||
f.write(js)
|
||||
tmp = Path(f.name)
|
||||
try:
|
||||
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stdout)
|
||||
sys.stderr.write(result.stderr)
|
||||
fail("Behavior state smoke test failed")
|
||||
ok("behavior state smoke test passed")
|
||||
|
||||
|
||||
def check_legacy_behavior_refs() -> None:
|
||||
allowed = {"js/tarinai.js", "js/tarinai_behavior_state.js"}
|
||||
tokens = re.compile(r"\b(activeBehavior|currentAction|activeForcedBehavior|intent)\b")
|
||||
offenders = []
|
||||
for path in (ROOT / "js").glob("*.js"):
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
if rel in allowed:
|
||||
continue
|
||||
for i, line in enumerate(read_text(path).splitlines(), start=1):
|
||||
if tokens.search(line) and "intentLockTimer" not in line and "//" not in line:
|
||||
offenders.append(f"{rel}:{i}")
|
||||
if offenders:
|
||||
fail("legacy behavior fields outside compatibility layer: " + ", ".join(offenders[:20]))
|
||||
ok("legacy behavior fields isolated to compatibility layer")
|
||||
|
||||
|
||||
|
||||
|
||||
def check_basic_action_spec_migration() -> None:
|
||||
source = read_text(ROOT / "js/tarinai_needs_items.js")
|
||||
required = {
|
||||
"eat_food": "createConsumableActionSpec",
|
||||
"drink_water": "createConsumableActionSpec",
|
||||
"use_medicine": "createConsumableActionSpec",
|
||||
"sleep_in_bed": "createSleepInBedActionSpec",
|
||||
"sleep_build_grass_bed": "createSleepBuildGrassBedActionSpec",
|
||||
"sleep_anywhere": "createSleepAnywhereActionSpec",
|
||||
"rest_to_recover": "createRestActionSpec",
|
||||
"sunbath": "createSunbathActionSpec",
|
||||
"wander_lightly": "createWanderActionSpec",
|
||||
}
|
||||
for action_id, factory in required.items():
|
||||
if factory not in source:
|
||||
fail(f"basic ActionSpec factory missing for {action_id}: {factory}")
|
||||
forbidden_late_ticks = [
|
||||
'set("eat_food",',
|
||||
'set("drink_water",',
|
||||
'set("use_medicine",',
|
||||
'set("sunbath",',
|
||||
]
|
||||
offenders = [token for token in forbidden_late_ticks if token in source]
|
||||
if offenders:
|
||||
fail("basic actions still patched in configureTarinaiNeedActions: " + ", ".join(offenders))
|
||||
ok("basic need actions migrated into ActionSpec factories")
|
||||
|
||||
|
||||
|
||||
def check_social_action_spec_migration() -> None:
|
||||
source = read_text(ROOT / "js/tarinai_needs_items.js")
|
||||
required_factories = [
|
||||
"createPanicEscapeActionSpec",
|
||||
"createFleeActionSpec",
|
||||
"createApproachFriendActionSpec",
|
||||
"createApproachFamilyActionSpec",
|
||||
"createApproachMateActionSpec",
|
||||
"createFightRivalActionSpec",
|
||||
"createIntimidateEnemyActionSpec",
|
||||
"createBirthRitualActionSpec",
|
||||
]
|
||||
for factory in required_factories:
|
||||
if factory not in source:
|
||||
fail(f"social/combat ActionSpec factory missing: {factory}")
|
||||
definitions_match = re.search(r"const TARINAI_ACTION_DEFINITIONS = \[(.*?)\];", source, re.S)
|
||||
if not definitions_match:
|
||||
fail("TARINAI_ACTION_DEFINITIONS block missing")
|
||||
definitions = definitions_match.group(1)
|
||||
for factory in required_factories:
|
||||
if f"{factory}()" not in definitions:
|
||||
fail(f"{factory} is not registered in TARINAI_ACTION_DEFINITIONS")
|
||||
if "function configureTarinaiNeedActions" in source:
|
||||
fail("configureTarinaiNeedActions should not be needed after ActionSpec runtime migration")
|
||||
forbidden_late_ticks = [
|
||||
'set("panic_escape",',
|
||||
'set("flee",',
|
||||
'set("approach_friend",',
|
||||
'set("approach_parent_or_child",',
|
||||
'set("approach_mate",',
|
||||
'set("fight_rival",',
|
||||
'set("intimidate_enemy",',
|
||||
'set("sunbath",',
|
||||
"birthRitualAction",
|
||||
"TARINAI_ACTIONS.push",
|
||||
]
|
||||
offenders = [token for token in forbidden_late_ticks if token in source]
|
||||
if offenders:
|
||||
fail("actions still patched late: " + ", ".join(offenders))
|
||||
ok("social/fear/combat/mate/birth/sunbath actions migrated into ActionSpec factories")
|
||||
|
||||
def check_runtime_diagnostics_hooks() -> None:
|
||||
spatial_budget = read_text(ROOT / "js/world_spatial_budget.js")
|
||||
debug_tools = read_text(ROOT / "js/debug_tools.js")
|
||||
world_update = read_text(ROOT / "js/world_update.js")
|
||||
if "runtimeDiagnostics()" not in spatial_budget:
|
||||
fail("World.runtimeDiagnostics hook missing")
|
||||
if "lastRuntimeDiagnostics" not in world_update:
|
||||
fail("world update does not publish lastRuntimeDiagnostics")
|
||||
if "spatial rebuild" not in debug_tools or "behavior forced" not in debug_tools:
|
||||
fail("debug overlay is missing runtime diagnostic lines")
|
||||
ok("runtime diagnostics hooks present")
|
||||
|
||||
def check_social_balance_guards() -> None:
|
||||
needs = read_text(ROOT / "js/tarinai_needs_items.js")
|
||||
world_social = read_text(ROOT / "js/world_family_social.js")
|
||||
social_move = read_text(ROOT / "js/tarinai_social_move_life.js")
|
||||
required_need_tokens = [
|
||||
"fight_rival: 56",
|
||||
"intimidate_enemy: 42",
|
||||
"approach_mate: 68",
|
||||
"approach_friend: 66",
|
||||
"return ({ conflict: 34, mate: 24, family: 12, bond: 14",
|
||||
"if (subNeed !== \"conflict\" && subValue >= threshold) return true",
|
||||
"subValue >= threshold && subValue >= socialPull * 0.82",
|
||||
]
|
||||
for token in required_need_tokens:
|
||||
if token not in needs:
|
||||
fail(f"social balance guard token missing: {token}")
|
||||
if "canStart: basicCanStartWithTarget,\n start(t, world, ctx = {}) {\n const rival" in needs:
|
||||
fail("intimidate_enemy reverted to ungated basicCanStart")
|
||||
required_world_tokens = [
|
||||
"fightPairCooldowns",
|
||||
"fightPairKey(a, b)",
|
||||
"fightPairCooldownRemaining(a, b)",
|
||||
"markFightPairCooldown(a, b",
|
||||
"isAlreadyFightingPair(a, b)",
|
||||
"if (this.isAlreadyFightingPair?.(a, b)) return true",
|
||||
]
|
||||
for token in required_world_tokens:
|
||||
if token not in world_social:
|
||||
fail(f"fight re-entry guard missing: {token}")
|
||||
if "dt * (battleDrug ? 0.23 : 0.085)" not in social_move:
|
||||
fail("ordinary proximity fight probability guard missing")
|
||||
ok("social balance and fight re-entry guards present")
|
||||
|
||||
|
||||
def check_item_bucket_polish() -> None:
|
||||
spatial_budget = read_text(ROOT / "js/world_spatial_budget.js")
|
||||
required_tokens = [
|
||||
"markItemBucketsDirty(reason",
|
||||
"ensureItemBuckets(reason",
|
||||
"addItem(item",
|
||||
"itemById(id)",
|
||||
"itemsOfType(type)",
|
||||
"liveItemsOfType(type",
|
||||
"this.itemIdMap = new Map()",
|
||||
"bucketRebuildsTotal",
|
||||
]
|
||||
for token in required_tokens:
|
||||
if token not in spatial_budget:
|
||||
fail(f"item bucket polish helper missing: {token}")
|
||||
if "return item && !item.dead ? item : null" not in spatial_budget:
|
||||
fail("itemById must not return dead items")
|
||||
debug_tools = read_text(ROOT / "js/debug_tools.js")
|
||||
if "bucket" not in debug_tools.lower() and "id-map" not in debug_tools.lower():
|
||||
fail("debug overlay is missing item bucket/id-map diagnostics")
|
||||
routed_files = {
|
||||
"js/world_placement_log.js": "addItem?.(item",
|
||||
"js/world_update.js": "addItem?.(drop",
|
||||
"js/world_combat_effects.js": "addItem?.(it",
|
||||
"js/ants.js": "addItem?.(corpse",
|
||||
"js/tarinai_social_move_life.js": "addItem?.(new Item(\"trace\"",
|
||||
"js/tarinai_needs_items.js": "addItem?.(structure",
|
||||
}
|
||||
for rel, token in routed_files.items():
|
||||
if token not in read_text(ROOT / rel):
|
||||
fail(f"runtime item creation path is not routed through addItem: {rel}")
|
||||
ok("runtime item bucket/id-map polish present")
|
||||
|
||||
|
||||
def check_compact_save_v2() -> None:
|
||||
snapshot = read_text(ROOT / "js/snapshot_system.js")
|
||||
save = read_text(ROOT / "js/save_system.js")
|
||||
required_snapshot = [
|
||||
"const SNAPSHOT_VERSION = 2",
|
||||
"function compactWorld",
|
||||
"function compactTarinai",
|
||||
"function compactItem",
|
||||
"function compactAnt",
|
||||
"logs = []",
|
||||
"worldRef.family = {}",
|
||||
"recordFamily",
|
||||
]
|
||||
for token in required_snapshot:
|
||||
if token not in snapshot:
|
||||
fail(f"compact save v2 snapshot token missing: {token}")
|
||||
forbidden_snapshot = [
|
||||
"logs: clonePlain",
|
||||
"effects: (worldRef.effects",
|
||||
"summary:",
|
||||
"app: \"tarinai_colony_game\"",
|
||||
]
|
||||
offenders = [token for token in forbidden_snapshot if token in snapshot]
|
||||
if offenders:
|
||||
fail("old broad snapshot fields still present: " + ", ".join(offenders))
|
||||
required_save = [
|
||||
'STORAGE_PREFIX = "tarinai_save_slot_v2_"',
|
||||
'EXPORT_PREFIX = "TN2!"',
|
||||
"SAVE_TEXT_ALPHABET",
|
||||
"base91Encode",
|
||||
"base91Decode",
|
||||
'CompressionStream("deflate-raw")',
|
||||
"DecompressionStream",
|
||||
"async function encodeSnapshot",
|
||||
"async function decodeSnapshot",
|
||||
]
|
||||
for token in required_save:
|
||||
if token not in save:
|
||||
fail(f"compact save v2 encoder token missing: {token}")
|
||||
if "TARINAI_SAVE_V1" in save or "tarinai_save_slot_v1" in save:
|
||||
fail("old save v1 prefix still present")
|
||||
ok("compact save v2 snapshot and encoder present")
|
||||
|
||||
def main() -> None:
|
||||
data = manifest()
|
||||
check_files_exist(data)
|
||||
check_versions(data)
|
||||
check_index_order(data)
|
||||
check_service_worker_order(data)
|
||||
node_check([*(ROOT / "js").glob("*.js"), ROOT / "service-worker.js"])
|
||||
action_spec_smoke()
|
||||
behavior_state_smoke()
|
||||
check_basic_action_spec_migration()
|
||||
check_social_action_spec_migration()
|
||||
check_runtime_diagnostics_hooks()
|
||||
check_social_balance_guards()
|
||||
check_item_bucket_polish()
|
||||
check_compact_save_v2()
|
||||
check_legacy_behavior_refs()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
"use strict";
|
||||
|
||||
const APP_VERSION = "15.24.14";
|
||||
const APP_VERSION = "15.24.35";
|
||||
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.
|
||||
const coreScriptNames = [
|
||||
"version", "event_bus", "performance_manager", "registry_base", "disease_registry", "effect_registry", "sound_pack", "data", "item_visual_registry", "food_registry", "input_mode_manager", "math", "physics_helpers", "assets", "audio", "render", "sim_core", "items", "ants", "health", "tarinai", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_items", "tarinai_social_move_life", "tarinai_render", "world", "world_view", "family_graph", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "world_update", "world_placement_log", "world_bootstrap", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "snapshot_system", "save_system", "patch_helpers", "freeze_system", "relation_event_system", "ui_layout_dialogs", "ui_tools", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
|
||||
"version", "event_bus", "performance_manager", "registry_base", "disease_registry", "effect_registry", "sound_pack", "data", "item_visual_registry", "food_registry", "input_mode_manager", "math", "physics_helpers", "assets", "audio", "render", "sim_core", "structures", "items", "ants", "health", "tarinai", "tarinai_action_spec", "tarinai_behavior_state", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_items", "tarinai_social_move_life", "tarinai_render", "world", "world_view", "family_graph", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "world_update", "world_placement_log", "world_bootstrap", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "snapshot_system", "save_system", "patch_helpers", "freeze_system", "relation_event_system", "ui_layout_dialogs", "ui_tools", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
|
||||
];
|
||||
const CORE_ASSETS = [
|
||||
"./",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue