From c6ba3e38a2f24fd7f09436f9c910dc6fb24c1132 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Sat, 20 Jun 2026 23:27:39 +0900 Subject: [PATCH] hmm --- .htaccess | 12 +- PATCH_NOTES.md | 15 + README.md | 6 +- assets/objects/genkotsu.png | Bin 0 -> 42110 bytes assets/ui/tool_genkotsu.png | Bin 0 -> 42110 bytes assets/ui/tool_nest_box.png | Bin 1857 -> 1697 bytes css/styles.css | 388 +++++--------- docs/AI_CONTEXT.md | 4 +- docs/code_map.md | 2 +- docs/prompt.txt | 991 +++++++++++++++++++++++++++++------- index.html | 53 +- js/ants.js | 43 +- js/assets.js | 20 +- js/data.js | 6 +- js/items.js | 234 +++------ js/main.js | 30 +- js/sim_core.js | 8 +- js/tarinai.js | 116 ++--- js/ui.js | 101 +++- js/ui_family.js | 48 +- js/world.js | 388 +++++++++----- 21 files changed, 1566 insertions(+), 899 deletions(-) create mode 100644 assets/objects/genkotsu.png create mode 100644 assets/ui/tool_genkotsu.png diff --git a/.htaccess b/.htaccess index d579227..36b3f0a 100644 --- a/.htaccess +++ b/.htaccess @@ -1,21 +1,21 @@ ExpiresActive On ExpiresByType text/html "access plus 0 seconds" - ExpiresByType text/css "access plus 0 seconds" - ExpiresByType application/javascript "access plus 0 seconds" - ExpiresByType application/json "access plus 0 seconds" + ExpiresByType text/css "access plus 30 days" + ExpiresByType application/javascript "access plus 30 days" + ExpiresByType application/json "access plus 30 days" ExpiresByType image/png "access plus 30 days" ExpiresByType image/webp "access plus 30 days" ExpiresByType audio/mpeg "access plus 30 days" ExpiresByType audio/wav "access plus 30 days" - - Header set Cache-Control "no-cache, no-store, must-revalidate" + + Header set Cache-Control "no-cache, must-revalidate" Header set Pragma "no-cache" Header set Expires "0" - + Header set Cache-Control "public, max-age=2592000" diff --git a/PATCH_NOTES.md b/PATCH_NOTES.md index 8c5ba5c..938feeb 100644 --- a/PATCH_NOTES.md +++ b/PATCH_NOTES.md @@ -1,3 +1,18 @@ +## 15.9.2 修正 +- 巣箱アイコンとゲーム内描画から、枠内の明色・透過っぽく見える装飾を除去。 +- アリのHPバーを、たりないと同様にダメージ直後だけ表示する方式へ変更。 +- 道具UIに「げんこつ」を追加。添付画像から正義げんこつ部分を切り抜き、上から落下してたりない・アリにダメージと吹っ飛びを与える効果を追加。 +- 初期ロード画面と読み込み進捗バーを追加。JS/CSSをdefer化し、CSS/JSのサーバーキャッシュを有効化して再訪問時の通信量を削減。 + + +## 15.9.1 修正 +- 草を地面レイヤーとして扱い、既存の草の上にもオブジェクトを配置できるようにしました。草同士・草と障害物の植え付け制限は維持しています。 +- けんか餅のゲーム内描画から星印を削除し、×印のみ表示するようにしました。 +- ダメージ後に出る「こわい…」吹き出しを削除しました。 +- 家系図に「更新」トグルを追加しました。デフォルトはOFFで、ONの間だけ家系図を描画・更新します。 +- 死亡した個体のライブIDを再利用しないようにし、友達・敵対関係が別個体へ引き継がれて見える問題を防止しました。死亡時の一時ターゲット参照も掃除します。 +- たりないデータUIをカテゴリ別の折り畳み表示に変更しました。 + # Patch Notes ## Encoding cleanup diff --git a/README.md b/README.md index a55624e..69725d1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Read only these first: `README.md`, `docs/AI_CONTEXT.md`, `docs/code_map.md`. Th ## Core simulation - Entities: `Tarinai` agents with hunger, sleep/energy, loneliness, stress, health, age, personality, family links, relationships, diseases, favorite flag, target/need AI, transient animation timers. -- World: bounded field with weather, time, lighting, temperature, spatial-grid queries, items, effects, logs, archive/family data, persistence, placement and collision rules. +- World: bounded field with weather, time, lighting, temperature, spatial-grid queries, items, effects, logs, archive/family data, placement and collision rules. - Observation loop: agents autonomously seek food/water/sleep/social targets, react to zunchi, tools, rain, cold, crowding, relationships, disease, damage, conflict, reproduction, and player input. - Records: observation log and family tree store births, deaths, parentage, cause-of-death labels, favorite state, and key state changes. @@ -50,7 +50,7 @@ js/sim_core.js Names, personalities, relationships, effects, spatial grid. js/health.js Damage memory, death-cause normalization, damage application. js/items.js Item state, lifecycle, drawing, grass/zunchi/ball/firecracker logic. js/tarinai.js Agent state, AI, disease state, eating, conflict, reproduction, draw. -js/world.js World update, placement, weather, logs, save/load, global interactions. +js/world.js World update, placement, weather, logs, global interactions. js/ui.js Input, tool selection, dialogs, observation panel. js/ui_charts.js Population/statistics charts. js/ui_family.js Family/archive UI and cause-of-death display. @@ -69,7 +69,7 @@ docs/code_map.md Symbol-to-file lookup table. ## v15 notes - Colony chart tab `events` was removed. `environment` now reports security, hygiene, and happiness. -- Selected Tarinai UI includes per-individual records derived from matching observation log entries. +- Selected Tarinai UI includes per-individual records attached by structured observation events. - Observation highlights selected relatives and major social relations: parent, child, friend, enemy. - Explosions impart velocity to nearby zunchi items; cursor contact reduces stress and emits dense heart effects. diff --git a/assets/objects/genkotsu.png b/assets/objects/genkotsu.png new file mode 100644 index 0000000000000000000000000000000000000000..81603688134ad47b03c5d46b4245d18dca25ce78 GIT binary patch literal 42110 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%0D&2@EuibYnMfHTvlkg5R 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 literal 0 HcmV?d00001 diff --git a/assets/ui/tool_genkotsu.png b/assets/ui/tool_genkotsu.png new file mode 100644 index 0000000000000000000000000000000000000000..81603688134ad47b03c5d46b4245d18dca25ce78 GIT binary patch literal 42110 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%0D&2@EuibYnMfHTvlkg5R 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 literal 0 HcmV?d00001 diff --git a/assets/ui/tool_nest_box.png b/assets/ui/tool_nest_box.png index c681d1942b342453e8e288c5457a70e18014ef11..4f6015624ddabb82dd927a2f2f8d89446a744771 100644 GIT binary patch delta 1682 zcma)6X*k;n9!`XimgzXsXh*SWHzuvK&T1X$Vuos_hz@lmt{60GW6{>}cWf(!F-jee zAxlexG$k3;5Tr&_Nvd0nA|x^?Ma5YnOEaCX^J(8N@A3Se-}8QYpMO!W^V3NH`h&Lz z26uCM^=DzisYy70?M3>8g>c}l1ZJ!UErJam)?YX0?h-#OTxT5_gxXRGcL%Ja+X}S3 zjS_HSYdz^b^cH!1v3WXUiP^&3`iCjZ-eSUKarTLxr#k5W0PvR^LzB8`lkF**c(+kq z^#q@3dwX9a7IciDm!X_DsinN1bGAe&DpQcihQ7=LtMFgIw>K&UK`2Rcf$EW%X63aV ztI9gp1yeTzoQrfzyzO&nT-8p4I<5cXCQj6Aw~?Q*Cj`DZqY+UEzO{Ze>fvc#m_F$MrWiN{HCq$r57;s>p@6eyud0Hu@sZJsr(tyr}HC5lSzssq? z>IcU+);5(KJ=1-~6v4OcP`Fd>Sl6gG1${pqo<1OLIN8O?BP>?k!8_bKimF4GF zD;1}_so|9v_iSyijjDhxn}Ki&Sjhvn-`D@A-X-^hK{K zwj@d;lNmYc3gKcUvgGP?0+G{&(XAf zeoXcxJQ*gnLPEHS4nc+MR9oH_RT(G#*7CB<^B4h}5M&5QK*;Y%SQDbLAwkvyI{T`? zP*NpT9nV?ho(LT`u-?6%=K>nY2lH}$cy7Jvx5!|Xq$fEV1cHFdv-cV={i(RfFkVJlp6RmznF+IGj@*_|P6^n_@D+&8&&rwtyq1> zUdEJ>n`>~(IlA{ch)KxJ09z2X4WK7cT3QWb!aT#i$BeenSLiJN33~tOi^CzMX6T)j zn*M^HMFP6PmcNg8^V=rx#zZnF-oH73<$RxoUYHWBzD8-iK;R?cF+GLzb6tlP1IO8? zhpy!fni@@yz3TV95b*rQax#U|;mEdbF04Uo+L!pnz}d`(iq>mnna3O8RBM6xiD~tD zik~EWKLswQJ(eDPCcbIwfAqe)qL|TsE)aY$?`NWna2D$KzkXA3zQOl0lH57iZFDvS zVy#bYgWiva|GGDIdd@B6PI}<$WD*~ryWV*`r@CV$rK<}=ta!X~LUdpqY-+S}?M`r> zPm$kb=C24G^6(j}OA6qtVpD72PdaB~LB}`R$b@aUS zNcTTcDBUJYS4KZc^hNctk`Aq9ulhtI^5_Ci^m1S8!7FpGI}Tn%Y*+@jDuAXua`dE& z{}K1F4*iIurK!D(Pd8@1l2_6*JI&hs%^O3R4wpI8c6%)EM&K2(xO;}7lXP%9Eg9oz zxM8VlIixj5DiCn-+QDItdz^0ciW_^x43n)k5 zQH!h3FnQE+EZFg=o)Pg&!hZ^yfrYyUQVNW4cFHAruL6Y{yBQq{tn%#u$I z$nT1lv|Jq_`z$n6i0{KZ6Pl*k@mkgJ3D8I8JI#g?ER=HzR`r1QmwErw1{u47AL{G6(3Src2=V1lL@k40_M`jFV^hs42l4Z(Un_BLNjqkI) z{ybT2=>z3t)~?BpM^=KAs$@v&IVaNSO9?>;iVFiH<(?z@N8;MWobJ)~bUO(60alU{ zGGXZnKkL>NE3ZlNBub7)+fvERU`t@KrKf6)%$-CbFKtlGD@p+}dLOVak}(|H2dHa> z0SHhvr}C)CXhq%dn~?%hWTytCepL*nk>^!cv9Ic)V!45F9^9sK$#O1z@-F<8o0qwC zR{!G+)GR(7zaVWG=_CySqi*dc8Iorbtdx5$0qa zM=u7*j#$)8CoXfZh9TtmdE>9o;~zvsgc6=ZnS|m~@)y_E4_8;i`y$u>A;#=o3v;Ir zDv>ZjM_dkKX!|On`y{B`eKhy6(*@aGi%GRPbJ{NJO2{NI+CZ9Zc3=bCfh&#$1B8gm#p zow}9Q+aQ*X;IC=_r1+gS_~RSUa+>g>xtj>~^!n`x5`$|wDybnSrGdRkU@9hq8=BjP zz3+C?q2^1;@R`40mL!al6^!c<=i>LB7|7N|Br&K@210_f@uGA!8<9gZS52y%joX^8 zhwbDQlecSX;|gd%QCr0o!8WyS%m_|MI!O~H!@d9~|lX7!m3BuJX{~9~UbyI-jxEn=tuY2({wXYx= zk*1L;^?M`umUs4UV9(B^we?S00`0TNEX!W2xz^)V7c71#n|B%Iy7rf!tne)a<5{N? z_vabYTuQgfK6n?r0Rr&jy*ipRvG4r+0)lQ4^>E7dR1SB|a{l^5pN0#P9*6J?$ORI4 z$a#DS@0(MZB(nOLqT-GW$-Av1Px;v{)dfi6MwS;ub~-V zazY=|R7jYNC9FCVYVMT#<_1{DmCkppVRu~BWWL&yV!mNwi*&5|GSy3YhTfP~k|2L< z7Lb8Cvo5SF;!zm^)0HT#`5R$lQwFCTRu@@<)%9565HNKDR=f^($S(~;N(nlTK z)}h19_t@U@rjUVQ&^u=?o|P$7K9D}3OW%4u|8NZ}i8r(wj7S4R9zy z%d7tnh_yTUEZeOReN#hg;&Mk(i{0$DaVODrPq=UsaToMX{uV`l>zNnZKrEY#h^S3z znxUc;FT$o^(B*RWOc}Jrnqr0mU68Yaz`U}drfwCSApM>bX*pZfSRwN3;Ox=Ch7>EaIS&1V&fkbM+x#wy?-^vZu=7#-^8t&gZFdHz zLpj;@au3wKW6Fn?SaX5d*V;R05>q?U-J!3}674(feE+5Y1|{xeaq7TYLf}o{2m4=BOJSz-$N$?I9pnA2_%*k$p@7CmwqNMeP~?K3y++F2kTY zYPESQ+VtP4Js@?brC!!B>rypL*B7M3$lQa-;EKd>4Vz=vOihy!xrU$lZtk*y4jsSW zx^>Gu1ggjxp4uiX#*{)cLeB&jR{~iU-E3;UEyY4Zs-?m(#bQ2f_y6Ar{) math -> assets -> audio -> render -> sim_core -> items -> health -> tarinai -> world -> text_catalog -> ui -> ui_charts -> ui_family -> main`. Primary objects: -- `World` (`js/world.js`): owns field size, time/weather, tarinai array, items, effects, logs, archive, save/load, placement, collision, interaction dispatch. +- `World` (`js/world.js`): owns field size, time/weather, tarinai array, items, effects, logs, archive, placement, collision, interaction dispatch. - `Tarinai` (`js/tarinai.js`): agent state and AI. Use helper files for shared rules; avoid making this file larger. -- `Item` (`js/items.js`): item state, draw, lifecycle, serialization. +- `Item` (`js/items.js`): item state, draw, lifecycle. - `HEALTH` (`js/health.js`): damage memory, major-damage window, death-cause classification, damage application. - `TEXT_CATALOG` (`js/text_catalog.js`): Japanese tooltips and ecology cards. Keep Japanese escaped. diff --git a/docs/code_map.md b/docs/code_map.md index 6a4bd87..5ae1a80 100644 --- a/docs/code_map.md +++ b/docs/code_map.md @@ -15,7 +15,7 @@ Use this as the first lookup table before opening large files. | Item creation/lifecycle/draw | `js/items.js` / `class Item` | | Cleaning, rain, placement | `js/world.js` / placement/update interaction helpers | | Fence collision | `js/world.js` / `resolveFenceCollision` | -| Logs/archive/save | `js/world.js` | +| Logs/archive | `js/world.js` | | Main renderer | `js/render.js` / `render` helpers | | Agent drawing | `js/tarinai.js` / `draw` | | Input/tools/dialogs | `js/ui.js` | diff --git a/docs/prompt.txt b/docs/prompt.txt index dc90944..c54aa78 100644 --- a/docs/prompt.txt +++ b/docs/prompt.txt @@ -1,265 +1,910 @@ -You are modifying an existing JavaScript browser game project. +You are working on the latest Tarinai observation game build. -Implement a personality system for the \u201ctarinai\u201d creatures. Keep the implementation conservative and debuggable. Do not add extra systems beyond the specification below. +Base input: -## Goal +* Start from `tarinai_colony_game_ant_nest_v5_fixed(1).zip`. +* The extracted project folder may be named `tarinai_colony_game_personality_fixed`. +* Modify the project files directly. +* Return a new zip named something like `tarinai_colony_game_ant_nest_v6_fixed.zip`. +* Preserve the current Japanese UI tone and naming style. +* After changes, run syntax checks on all JavaScript files and verify zip integrity. +* Bump all cache-busting versions in `index.html` consistently so browsers do not keep loading old JS/CSS. -Add four continuous personality parameters to each tarinai: +Important context: + +* This latest build already includes the four-axis personality system. +* This latest build already includes the ant nest feature. +* Do not revert either of those systems. +* Do not reintroduce the old legacy named personality system. +* Do not preserve old save compatibility. +* Old local save data is irrelevant. + +Important intentional behaviors to preserve: + +* The family tree reset feature is intentional. It exists so the player can delete an extremely large/heavy family tree. Do not remove it. +* The family tree reset may clear family tree data/cache and related heavy relationship graph data according to the current intended behavior. Do not “fix” it as a bug. +* Item placement drop-impact damage is intentional. Do not remove it. +* Keep item placement drop damage behavior as-is unless it directly causes a runtime error. +* Grass planting checks may remain mostly as-is. +* Tool-size inheritance may remain as-is. +* Screen resizing may reset the field. Do not add complicated resize preservation logic. + +--- + +# 1. Completely remove save/load functionality + +Remove the save/load feature entirely. + +Requirements: + +* Remove manual save buttons, load buttons, autosave controls, import/export controls, or any equivalent UI if present. +* Remove localStorage save/load usage. +* Remove autosave timers. +* Remove save key constants such as `tarinai-colony-save-*`. +* Remove save migration logic. +* Remove backward compatibility code for old saves. +* Remove serialization/deserialization code that exists only for save/load. +* Remove comments and fallback code related to old save formats. +* Do not preserve compatibility with previous versions. +* Do not add new save/load functionality. +* Do not remove normal image/audio asset loading functions. Only remove game-state save/load. + +Current-code hints: + +* `CONFIG.saveKey` still exists and should be removed. +* `serialize()` / `load(data)` style world-state code still exists and should be removed if it is only used for save/load. +* CSS classes for old save text dialogs should be removed if unused. +* Legacy personality stubs that exist only for old-save compatibility should be removed if they are not used by the current four-axis personality system. + +Expected result: + +* The game starts fresh. +* No save/load UI is visible. +* No old localStorage data is read. +* No migration or compatibility logic remains. + +--- + +# 2. Living Tarinai internal IDs + +Introduce or refactor to a living-only internal ID system for Tarinai. + +Important: + +* The existing code uses `id` widely for Tarinai identity, family tree, ants, relationships, and logs. +* Do not blindly make historical family-tree identity reusable if that would corrupt the family tree. +* The requested reusable ID is the active living individual ID. +* If persistent historical/family-tree identity is still needed, introduce a separate internal snapshot/archive key such as `familyKey` or `archiveKey`. +* Do not display either ID in the UI. + +Requirements: + +* Every living Tarinai must have an internal living ID. +* The living ID must not be displayed. +* Use the living ID internally for: + + * observation event ownership + * relationship highlights + * fight participants + * damage attribution where needed + * current ant targeting/dragging + * current selection state + * avoiding name-based ambiguity +* Do not use display name text as an internal identifier. +* Death must completely remove/release the living ID from the active living registry. +* Released living IDs may be reused by later Tarinai. +* Reusing a living ID must not attach new Tarinai events to an old dead Tarinai’s logs, observation history, family tree entry, or ant target. +* Any historical display that survives death must store a display snapshot separately: + + * name snapshot + * sprite/type snapshot + * death cause + * family links if needed + * any required personality display snapshot +* Ants that target or drag a Tarinai must not accidentally switch to a new Tarinai after a released living ID is reused. Use direct object references, validation tokens, or live ID plus generation token if necessary. + +Expected result: + +* Living Tarinai are internally identifiable without relying on names. +* Dead Tarinai living IDs are released and reusable. +* Historical displays remain stable. +* ID reuse does not corrupt logs, family tree, relationships, selection, or ant behavior. + +--- + +# 3. Four-axis personality compatibility + +The latest build uses four continuous personality axes: * aggression * openness * sociability * neuroticism -Each parameter ranges from `-1.0` to `+1.0`. +Do not reintroduce legacy named personality as a main data model. -Personality should affect behavior only when the value crosses clear thresholds. Small changes should be recorded internally, but observation logs should only be written when a threshold is crossed. +Coward/timid behavior: -## Personality axes +* Do not set a legacy `currentPersonality = "臆病"` style value. +* Do not add a separate cowardice parameter. +* `臆病` should remain a derived tag/state based on the four-axis personality model. +* The existing intended derivation is high neuroticism plus low aggression. +* When a Tarinai takes damage and its total fight losses are greater than its total fight wins: -### aggression + * with 5% chance, shift its current personality slightly toward cowardly behavior, + * for example increase neuroticism and decrease aggression, + * use only total fight wins/losses, + * do not include per-relationship win/loss counts, + * do not double count. +* If this behavior is already implemented correctly, keep it and only clean up naming/comments so it does not imply a legacy named-personality route. -Range: +Expected result: -* low side: calm -* high side: irritable +* The four-axis personality system remains intact. +* `臆病` remains a derived display/behavior tag, not a separate old personality. +* The damage/loss condition uses only aggregate lifetime fight result counters. -Behavior: +--- -* High aggression makes the tarinai more likely to threaten others, start fights, or retaliate. -* Low aggression makes the tarinai less likely to start fights. +# 4. Observation logs, event ownership, filtering, and statistics -Changes: +Current issues: -* Winning fights should slightly increase aggression. -* Losing fights should slightly decrease aggression. +* Observation logs may still be assigned by checking whether event text includes a Tarinai’s name. +* Hidden observation events may still count in event statistics. +* `つつく` / poke may still be classified as fight by text inference. -### openness +Implement: -Range: +* Observation events must store structured participant references. +* Use living Tarinai IDs while participants are alive. +* Also store display-name snapshots so old logs remain correct after death or ID reuse. +* Do not assign observation events by `text.includes(name)` or similar name substring matching. +* Event objects should have structured fields such as: -* low side: conservative -* high side: playful + * `type` + * `actorLiveId` + * `targetLiveId` + * `participantLiveIds` + * `participantSnapshots` + * `hiddenFromObservation` + * `countInStats` +* Exact field names are flexible, but the data must be structured. +* Hidden observation events must not be counted in statistics. +* Do not merely hide them from the UI while still incrementing counters. +* Events hidden from observation UI should either not be logged as visible observation events or should be marked with `countInStats: false`. -Behavior: +Remove from the observation UI and statistics: -* High openness makes the tarinai more likely to wander farther, investigate new objects, and play with toys such as balls. -* Low openness makes the tarinai prefer familiar or nearby areas. +* events about poking a ball +* events of the form “X avoided Y” / `○○が××を避けた` +* events that merely indicate selecting a Tarinai for observation, such as `○○を選択した。` -Changes: +Important classification rule: -* Being petted by the cursor should slightly increase openness. +* `つつく` / poke is not a fight. +* Do not classify poke events as `fight`. +* If there is an `inferLogKind()` or equivalent text-based classifier, fix or replace it so poke does not become fight. +* Prefer structured event types over Japanese text matching. -### sociability +Log display formatting: -Range: +* Remove name-substring based log decoration where practical. +* Render participant names from structured event snapshots. +* If a participant has died and its living ID was released, use the display-name snapshot stored in the event. +* Do not allow reused living IDs to rewrite old log display names. -* low side: loner -* high side: social +Expected result: -Behavior: +* Observation logs attach to the correct individuals. +* Hidden events are not shown and not counted. +* Poke is not treated as fight. +* Similar Tarinai names no longer corrupt logs. -* High sociability makes the tarinai more likely to approach other tarinai, especially parents, children, friends, or the cursor. -* High sociability also makes the tarinai more likely to react when a parent, child, or friend is fighting nearby. -* Low sociability makes the tarinai prefer being alone. +--- -Changes: +# 5. Family tree update performance and scheduling -* Spending time near parents, children, or friends should slightly increase sociability. +The latest build already has some idle/chunked family tree rendering. Preserve it and finish the scheduling/consistency work. -### neuroticism +Requirements: -Range: +* Keep heavy preprocessing split into small chunks. +* Use `requestIdleCallback` when available, with a safe `setTimeout` fallback. +* Avoid doing large graph construction, signature generation, relationship counting, or DOM creation in one blocking pass. +* The game loop must remain responsive while family tree data is prepared. -* low side: insensitive / unfazed -* high side: delicate / sensitive +Update timing rules: -Behavior: +* When the family tree panel is hidden or offscreen: -* High neuroticism makes the tarinai gain more stress from unpleasant stimuli such as corpses, poop, nearby fights, or overcrowding. -* Low neuroticism makes the tarinai less affected by such stimuli. + * Do not update every 5 seconds. + * Check/update at most once every 30 seconds. +* When the family tree panel is visible: -Changes: + * Do not update on a fixed 5-second timer. + * Update only when the family tree is dirty, meaning a relevant change occurred. +* Relevant dirty events include at least: -* Touching poop or corpses should still increase stress immediately. -* However, repeated exposure should slightly reduce neuroticism over time, representing habituation. -* This means tarinai can become less sensitive to unpleasant stimuli after repeated exposure. + * birth + * death + * parent/child relationship creation + * mate/partner relationship creation or change + * name/identity data change that affects the family tree display + * family tree reset +* Mark dirty from the source of the change, not by repeatedly recomputing expensive signatures. +* If a background update is already running, do not start another one. +* If a newer dirty event occurs while an update is running, schedule one additional update after the current update completes. +* If the family tree reset button is used, cancel stale pending render tokens/timers so old async work cannot reinsert deleted tree HTML afterward. -Important: +Important preservation rule: -* Do not treat \u201ccowardice\u201d as a direct low-aggression state. -* If a cowardly tag is needed, derive it as a compound state from high neuroticism and low aggression. -* Do not implement a separate cowardice parameter. +* Do not remove the family tree reset feature. +* It is intentional and may clear family tree data/cache and current heavy relationship graph data. +* Only fix reset-related runtime errors or stale async render problems. -## Birth personality and current personality +Expected result: -Each tarinai should have two personality objects: +* Opening or updating the family tree should not freeze the main simulation. +* Hidden family tree work should be rare. +* Visible family tree should refresh promptly after actual changes. +* Reset remains available and does not allow stale async renders to restore old content. -```js -birthPersonality -currentPersonality -``` +--- -Rules: +# 6. Disease fight initiation and targeting rules -* `birthPersonality` is created at birth and normally does not change. -* `currentPersonality` starts equal to `birthPersonality`. -* Experience modifies only `currentPersonality`. -* The tarinai data UI should show both birth personality and current personality. +Implement the following disease fight rules exactly. -## Inheritance +Fight initiation: -When a child is born: +* Tarinai with `爆発病` can initiate fights. +* Tarinai with `ずんち病` can initiate fights. +* Tarinai with any other disease must not initiate fights. +* Healthy Tarinai may initiate fights according to the existing rules. -```text -child birth personality = - average of the parents' current personality - + random variation between -0.2 and +0.2 for each parameter -``` +Being targeted / challenged: -Clamp each resulting value to `-1.0` through `+1.0`. +* Tarinai with `ねむり病` must not be targeted for fights. +* Tarinai with any other disease may still be targeted/challenged by another Tarinai. +* This means: -Then: + * `爆発病` and `ずんち病`: can initiate and can be targeted. + * `きずつき病` and other non-sleep diseases: cannot initiate, but can be targeted. + * `ねむり病`: cannot initiate and cannot be targeted. -```js -child.currentPersonality = child.birthPersonality -``` +Apply consistently to: -The child should pass its changed current personality to future descendants, not only its original birth personality. +* Normal fight selection. +* Forced fight logic. +* Intimidation logic if it can lead to fights. +* Fight-food effects such as `けんか餅`. +* Any helper such as `canStartFight`, `canBeFightTarget`, or equivalent. -## Thresholds and personality tags +Expected result: -Use two threshold levels: +* Disease rules are handled by clear helper functions. +* No scattered ad-hoc disease checks disagree. -```text --1.0 to -0.75: strong low-side trait --0.75 to -0.5: slight low-side trait --0.5 to +0.5: neutral, no visible personality tag -+0.5 to +0.75: slight high-side trait -+0.75 to +1.0: strong high-side trait -``` +--- -Display tags: +# 7. Collision and external-force unification -* Slight traits should be displayed as \u201cslightly X\u201d. -* Strong traits should be displayed as \u201cX\u201d. -* Neutral values should not create a visible tag. +The latest build already has `applyImpulse()` and some collision refactoring. Preserve and finish the design. -Example: +Requirements: -* aggression `+0.62` -> \u201cslightly irritable\u201d -* aggression `+0.83` -> \u201cirritable\u201d -* sociability `-0.58` -> \u201cslightly loner\u201d -* sociability `-0.82` -> \u201cloner\u201d -* openness `+0.20` -> no tag +* Normal self-propelled movement and external impulse/knockback must remain separate. +* External impulses must not be clamped by the normal Tarinai walking speed cap. +* If helper functions already exist, reuse and tighten them instead of adding duplicates. +* Useful helpers may include: -Behavior activation: + * `applyImpulse(entity, vx, vy, options)` + * `applyKnockback(entity, sourceX, sourceY, strength, options)` + * `applyImpactDamage(target, amount, cause)` + * `resolveCircleCircleCollision(...)` + * `resolveCircleRectCollision(...)` -* Slight traits affect relevant behavior with a 50% chance when choosing a behavior. -* Strong traits affect relevant behavior with a 100% chance when choosing a behavior. -* Do not check this every frame. Apply it only during behavior selection or when evaluating a relevant event. +Unlimited ball speed: -## Daily personality change limit +* Do not reintroduce a ball speed cap. +* Collision detection must handle high-speed balls using swept movement based on actual frame movement distance. +* Remove fixed low `Math.min(...)` collision-search caps that contradict unlimited ball speed. +* Ball vs wall, ball vs fence, ball vs nest box, ball vs ball, and ball vs Tarinai should remain reliable at high speed. -Add a daily change cap so personality values cannot change too quickly. +High-speed ball vs Tarinai: -Recommended limits: +* When a sufficiently fast ball hits a Tarinai: -```text -Per parameter: - maximum increase per day: +0.08 - maximum decrease per day: -0.08 + * Knock the Tarinai away using velocity equal to 1/5 of the ball velocity. + * This knockback must be an external impulse, not normal walking movement. + * Do not let normal “play with ball” behavior also run for that same dangerous impact if it would conflict. + * If the damage from this impact kills the Tarinai, death cause must be `衝突`. -Across all parameters: - maximum total absolute personality change per day: 0.20 -``` +High-speed Tarinai vs Tarinai: -If a change would exceed the daily limit, reduce or ignore the excess change. +* When a sufficiently fast Tarinai hits another Tarinai: -Reset these daily counters at the start of each new in-game day. + * The hit Tarinai should be knocked away. + * Use the attacker’s actual movement/impulse direction and speed. + * If collision damage kills a Tarinai, death cause must be `衝突`. -## Observation log rules +Explosion knockback: -Write personality change entries to the observation log only when a threshold is crossed. +* Keep explosion knockback strong. +* Use external impulse, not normal movement velocity. +* Knockback should be partly random. +* Tarinai closer to the blast center should be thrown faster and farther. +* Tarinai farther from the blast center should be thrown less strongly. +* Preserve or improve existing explosion damage behavior. +* If an explosion kills a Tarinai, keep the appropriate explosion-related death cause, not `衝突`. -Log when: +Ant interaction preservation: -* a value crosses `+0.5` -* a value crosses `+0.75` -* a value crosses `-0.5` -* a value crosses `-0.75` -* a value returns from a visible trait range back into neutral +* Preserve existing ant damage handling from firecracker explosions and dropped-object impacts. +* Do not break worker ants, queen ants, ant corpses, or ant nest reproduction behavior while refactoring forces. -Do not log every small personality change. +Expected result: -Example log messages: +* Normal walking speed remains bounded. +* Knockback from explosions and collisions can exceed walking speed. +* High-speed objects do not frequently tunnel through collisions. +* Collision behavior is handled by shared primitives rather than scattered custom math. -```text -\u201cMochi\u201d has become slightly irritable after winning fights. -\u201cMimi\u201d has become playful after spending time with toys. -\u201cNoko\u201d has become less sensitive after repeated contact with corpses. -\u201cTari\u201d no longer seems especially social. -``` +--- -Use the existing observation log system if one exists. +# 8. Ball-to-ball collision -## Tarinai data UI +Ensure balls collide with each other robustly. -Update the tarinai data UI to show personality information clearly. +Requirements: -It should include: +* Ball vs ball collision must use circle-circle collision. +* Resolve overlap. +* Exchange or reflect velocity in a physically plausible way. +* Work with high-speed balls, not only overlapping balls. +* Avoid infinite acceleration or NaN velocity. +* Do not apply arbitrary maximum speed caps. -1. Birth personality -2. Current personality -3. Visible personality tags derived from current personality +If this is already implemented, verify it and remove any remaining contradictory caps or stale-grid issues. -Example structure: +--- -```text -Personality Tags: -slightly irritable / social +# 9. Item falling speed -Birth Personality: -Aggression: -0.12 -Openness: +0.31 -Sociability: +0.48 -Neuroticism: +0.66 +Current issue: +Item falling motion can look like it changes speed unnaturally. -Current Personality: -Aggression: +0.57 -Openness: +0.34 -Sociability: +0.76 -Neuroticism: +0.22 -``` +Implement: -Keep the UI compact. Do not create a large new panel unless necessary. +* Make the visible item falling motion constant speed. +* Replace nonlinear visual curves such as `Math.pow(dropT, ...)` with linear progress unless a specific item intentionally has a special animation. +* The item should fall at a stable fixed speed from its start height to the ground. +* Keep the existing item placement drop-impact damage behavior. +* Do not remove drop-impact damage. +* Do not make harmless-looking items non-damaging merely because they look harmless. Current placement drop-impact behavior is intentional. -## Implementation constraints +Expected result: -* Keep the code simple and localized. -* Reuse existing behavior, logging, and UI systems where possible. -* Avoid adding unrelated features. -* Avoid adding complex new simulation systems. -* Preserve existing save data as much as possible. -* If old save data lacks personality fields, initialize them safely. -* Clamp all personality values to `-1.0` through `+1.0`. -* Add helper functions where useful, such as: +* Falling items move at a visually consistent constant speed. +* Existing drop impact behavior remains. - * `ensurePersonality(tarinai)` - * `adjustPersonality(tarinai, key, delta, reason)` - * `getPersonalityTraitTags(tarinai)` - * `getTraitStrength(value)` - * `shouldApplyPersonalityBehavior(tarinai, key, direction)` +--- -## Validation +# 10. Solid obstacle and placement logic unification -After implementation: +Current issues: +Fence, nest box, water dish, grass, and placement preview logic are partly fragmented. -* Run syntax checks for all JavaScript files. -* Verify that old tarinai without personality data do not crash the game. -* Verify that personality values are clamped. -* Verify that daily limits work. -* Verify that threshold crossing creates observation logs. -* Verify that minor personality changes do not spam logs. -* Verify that tarinai data UI shows birth personality, current personality, and visible tags. -* Verify that children inherit from parents' current personality with \u00b10.2 random variation. +Implement: + +* Unify placement validation for the object types where overlap matters. +* Overlap is important for: + + * grass + * fence + * nest box + * water dish +* For other items, overlap does not need to be strict unless an existing rule already requires it. +* Do not over-engineer overlap rules for stones, balls, food items, firecrackers, poop, ant nests, ant corpses, or similar objects. + +Solid obstacles: + +* Unify all solid rectangular obstacle collision logic. +* Fence and nest box should use the same obstacle system where possible. +* Rename misleading helpers if needed: + + * For example, functions named only for “fence” but now handling nest boxes too should be renamed or wrapped with neutral names such as `solidObstacle`. +* Ball movement, Tarinai movement, path blocking, and placement checks should reference the same obstacle data. + +Fence collision: + +* Fence collision bounds must match the visible fence appearance. +* If the visual fence is not the same size as the previous hitbox, update the hitbox to match the visual footprint. +* Avoid separate visual size and collision size unless explicitly documented in one definition table. + +Nest box collision: + +* Preserve the existing 3×3 nest box hitbox rule: + + * If the nest box is divided into a 3×3 grid, the collidable cells are: + + * top-left + * top-center + * top-right + * middle-left + * middle-right + * The middle-center, bottom-left, bottom-center, and bottom-right cells are passable/enterable according to the current nest behavior. +* Ball must bounce off these nest box solid cells. +* Tarinai must not path through these solid cells. + +Placement preview: + +* The preview highlight must use the exact same placement validation as actual placement. +* If the preview is red, clicking must not place the object. +* If the preview is green, clicking should place the object at the previewed position. +* Do not silently clamp or auto-correct an out-of-bounds placement to an in-bounds location. +* If the planned placement would extend outside the field, the highlight must turn red. +* Fix any case where a fence preview extending outside the field does not turn red. +* The highlighted preview position should show the true placement footprint, not a different corrected footprint. + +Grass placement: + +* Keep the existing grass planting spot search/check behavior unless it directly conflicts with the unified placement preview/actual placement contract. +* Do not spend time expanding grass checks to every object type. + +Screen resize: + +* Do not implement special preservation behavior for screen resizing. +* Screen size change is allowed to reset the field. +* Do not add complicated coordinate scaling or migration logic for resize. + +Expected result: + +* Placement UI, actual placement, and collision behavior agree. +* No “red but placeable” or “green but placed elsewhere” behavior. +* Grass, fence, nest box, and water dish avoid important overlap. +* Other item overlap can remain permissive. + +--- + +# 11. Spatial query scratch arrays + +Current issue: +`nearbyItems()` and `nearbyTarinai()` may share the same mutable scratch array. Nested spatial queries can overwrite an outer loop’s result. + +Implement: + +* `nearbyItems()` and `nearbyTarinai()` must not share the same mutable scratch array. +* Nested spatial queries must not overwrite an outer loop’s result. +* Use separate scratch arrays, return fresh arrays, or explicitly copy results where nested queries are possible. +* Verify: + + * item interaction loops + * bed/nest occupancy checks + * best bed search + * ball interactions + * Tarinai interaction loops + * ant prey search if it uses nearby queries + +Expected result: + +* Spatial queries are safe even when nested. +* Loops over nearby entities do not mutate unexpectedly. + +--- + +# 12. Spatial rebuild/update phase + +Current issue: +The spatial grid may be rebuilt before entities move, so later collision and interaction passes can use stale positions. + +Implement: + +* Ensure spatial grid data is not stale during collision and interaction passes. +* Prefer phase separation: + + * update items + * update ants if their movement affects targeting/collision + * update Tarinai movement + * rebuild spatial grid + * resolve cross-entity interactions/collisions +* If full phase separation is too invasive, at minimum rebuild the spatial grid after Tarinai movement and before any interaction/collision logic that relies on it. +* Avoid relying on grid data from before entities moved. + +Expected result: + +* Nearby queries use current positions. +* Collision and interaction behavior is more stable. + +--- + +# 13. Duplicate weather log + +Current issue: +Weather changes may create duplicate identical log entries in some builds. + +Implement: + +* Verify `updateWeather()`. +* A weather change must create only one weather log entry. +* Do not log the same weather text twice. +* Preserve the actual weather behavior. + +Expected result: + +* One weather change produces one visible log entry. + +--- + +# 14. Tool/item definition and CSS icon cleanup + +The latest build already has a `TOOL_DEFINITIONS`-style structure. Finish consolidating and remove conflicts. + +Requirements: + +* Use a single current source of truth for tool/item definitions where practical. +* It should include at least: + + * internal id + * Japanese label + * tooltip text + * UI icon or icon renderer + * game appearance reference where applicable + * placement size / footprint + * whether it is placeable + * whether it is scalable + * collision shape if applicable + * simulation-only status if applicable +* Use this definition in the tool UI, placement preview, placement logic, and logs where appropriate. +* Do not leave conflicting duplicate label/icon/size definitions in separate files unless unavoidable. + +Specific icon requirements: + +* `つつく` must use `👈`. +* `つまむ` must use `🤏`. +* `洗浄` must use the shower emoji `🚿`. +* Remove or stop referencing old PNG icons for `つつく` and `つまむ`. +* If `洗浄` has both an emoji and a PNG icon, ensure the displayed tool UI follows the current requested shower emoji specification. +* Fix the nest box tool UI icon so it matches the actual in-game nest box appearance. +* Preserve the ant nest tool icon and ensure it remains visible. +* Ensure every tool that appears in the tool UI has an icon. +* Avoid mojibake/garbled icon rendering. +* CSS icon definitions must be unified to the current specification. +* Remove old CSS generations and conflicting icon overrides. +* Do not rely on CSS order to accidentally choose the correct icon. + +Water tool: + +* `水` must remain removed from the tool UI. +* If water still exists as a simulation object produced by rain or other systems, keep that object behavior. +* `水の皿` remains a valid tool/item. +* Keep `water` as simulation-only if needed. +* Remove leftover UI-only water tool entries, labels, tooltips, and placement handlers unless required by non-UI simulation. + +Static tool buttons: + +* If `index.html` still hardcodes tool buttons, ensure it does not conflict with `TOOL_DEFINITIONS`. +* Prefer generating or validating tool metadata from the definition table if practical. +* At minimum, make sure static buttons and definition data agree. + +Expected result: + +* Tool UI no longer has missing icons, old icons, or mismatched nest box visuals. +* Current icon definitions are clean and non-duplicated. +* Ant nest remains integrated. + +--- + +# 15. Death cause normalization + +Ensure these death causes are applied consistently: + +* Death by old age / lifespan: + + * `天寿を全うした` +* Tarinai deleted because it went too far outside the field: + + * `仕様により画面外で削除` +* Death caused by ball collision or Tarinai collision damage: + + * `衝突` +* Explosion death: + + * Keep explosion-related cause. +* Existing disease-related deaths: + + * Preserve current intended disease death causes unless they conflict with the above. + +Implementation: + +* Centralize death cause normalization if possible. +* Avoid scattered caller-side strings that can drift. +* Ensure observation UI, family tree, memorial/death display, and any other death display show the normalized cause. +* Since save/load is removed, do not implement migration for old death-cause strings. + +Expected result: + +* Death cause strings are consistent. +* Collision deaths do not appear as generic accidents. +* Lifespan deaths show `天寿を全うした`. + +--- + +# 16. Out-of-bounds object policy + +Implement or verify a unified out-of-bounds policy: + +* For objects or entities slightly outside the field: + + * Return them to the nearest valid in-field position. +* For objects or entities far outside the field, or with invalid coordinates such as NaN/Infinity: + + * Delete them. +* For Tarinai deleted by this system: + + * Death cause must be `仕様により画面外で削除`. + * Release its living ID so it may be reused. +* For non-living objects: + + * Delete without a death cause. +* Keep wall bounce behavior for balls at normal field boundaries, but still sanitize balls that somehow get far outside or invalid coordinates. +* Ant workers/queens should not remain permanently invisible offscreen. If they go far out or become invalid, delete or safely return them according to the least disruptive current ant behavior. +* Do not add resize-specific preservation logic; field reset on screen size change is acceptable. + +Expected result: + +* No permanent invisible offscreen objects or actors. +* No NaN entities poison the simulation. +* Far-outside Tarinai deletion has the correct death cause. + +--- + +# 17. Observation relationship highlight + +Current issue: +Relationship highlight during observation can be affected by sprite rotation in some builds. + +Implement or verify: + +* Relationship highlights must be drawn in world/screen space independent of Tarinai sprite rotation. +* Do not draw the highlight inside a rotated sprite transform. +* Ensure highlight position follows the Tarinai but does not rotate with the Tarinai body. +* Use living IDs or stable current object references for relationship lookup where appropriate, not name matching. +* Include the latest intended relationship types: + + * parents + * children + * enemies + * friends, using the current friendship threshold + +Expected result: + +* Highlight remains visually stable regardless of sprite rotation. +* Highlight targets the correct individuals. + +--- + +# 18. Nest box behavior preservation + +Preserve and verify previous nest box fixes: + +* Tarinai inside the nest box should be fully transparent in the main field. +* Nest box can contain up to 5 Tarinai. +* Nest box occupant popup should show each individual on a separate line. +* The nest box solid hitbox must follow the 3×3 rule described above. +* Ball should bounce off nest box solid hitbox cells. +* Living Tarinai IDs must not break nest box occupancy. +* Since save/load is removed, do not add save compatibility for nest box IDs. + +Expected result: + +* Nest box behavior remains stable after obstacle and ID refactoring. + +--- + +# 19. Ant nest behavior preservation + +Preserve the current ant nest feature. + +Do not regress: + +* `ant_nest` remains placeable. +* Each ant nest starts with the intended worker count. +* Worker ants spawn from their home nest. +* Worker ants search for Tarinai and drag them back to the nest. +* Multiple workers dragging the same Tarinai increase carrying speed. +* Dragged Tarinai can struggle and kill workers. +* Dead workers become edible `ant_corpse` items. +* Damaged workers keep their HP when returning to and leaving the nest. +* A full nest can spawn a queen. +* A queen can create a new ant nest in an open spot. +* Queen spawning remains limited according to the current intended daily rule. +* Ant health does not recover. +* Ant sprites and Irasutoya credit remain. + +ID-related requirements: + +* Ants must not use display names to target Tarinai. +* Ant target references must not break after living ID reuse. +* If a target Tarinai dies, is deleted, or its living ID is released, worker ants should safely abandon or retarget according to current behavior. +* Removing save/load must not remove active runtime ant behavior. + +Expected result: + +* Ant nest behavior remains intact after ID, save/load, and spatial refactoring. + +--- + +# 20. Encyclopedia / ecology page + +Preserve and verify the `たりないの生態` addition: + +* It must include an explanation of health. +* It must include an explanation of stress. +* It must use a Tarinai sprite/image with both the health bar and stress bar visible. +* Keep the latest shortened ecology cards and HP/stress meter placement note if present. +* Do not break existing encyclopedia entries. +* Preserve the Irasutoya credit entry added for ant assets. + +--- + +# 21. Naming and Japanese text preservation + +Preserve previous Japanese terminology changes: + +* `喧嘩傷病` must remain renamed to `きずつき病`. +* Lifespan death text must display as `天寿を全うした`. +* Friend relation label should remain `友達` if that is the current intended label. +* Use the existing tone/style for Japanese labels and descriptions. +* Do not reintroduce removed or old names. + +--- + +# 22. Remove obsolete or dead code where safe + +Clean up old or dead specifications, but do not remove active gameplay. + +Remove or simplify where safe: + +* old save/load compatibility code +* old save migration code +* old CSS icon generations +* obsolete tool icon definitions +* UI-only `water` tool leftovers +* unused save-only item serialization fields +* comments describing removed old-save behavior +* old code paths for removed tools such as UI `水` +* old legacy named-personality compatibility stubs if they exist only for old saves +* old `food`, `mirror`, `cardboard`, or similar legacy remnants only if they are truly unreachable and not part of current gameplay + +Keep: + +* Current rain/simulation water behavior if it exists. +* Current grass planting behavior. +* Current tool-size inheritance behavior. +* Current field reset behavior on screen resize. +* Family tree reset feature. +* Item placement drop-impact damage. +* Four-axis personality system. +* Ant nest feature. + +--- + +# 23. Input cleanup + +If practical without destabilizing gameplay, clean up mouse/touch input handling. + +Requirements: + +* Consider converting canvas input to `PointerEvent` handling. +* Keep behavior equivalent on desktop. +* Ensure dragging, pinching, hosing, placing, panning, and clicking remain stable. +* Do not make this a large risky rewrite if the existing input system is stable. +* This is lower priority than save/load removal, IDs, logging, collision, family tree performance, spatial safety, and item falling speed. + +Expected result: + +* Input behavior remains stable. +* Touch and pointer handling do not regress. + +--- + +# 24. Code quality expectations + +Refactor carefully: + +* Prefer small, well-named helper functions. +* Avoid adding more scattered special cases. +* Avoid duplicating collision math. +* Avoid duplicating item/tool metadata. +* Avoid string-matching for gameplay logic where structured data is possible. +* Do not add save compatibility or migration code. +* Do not over-engineer screen-resize preservation. +* Keep intentional odd behaviors when explicitly listed above. + +Add comments only where they clarify non-obvious game rules, especially: + +* disease fight rules +* nest box 3×3 collision cells +* external impulse vs normal movement +* family tree dirty/update scheduling +* reusable living Tarinai IDs +* hidden observation events not counting in statistics +* four-axis personality compatibility +* constant item falling speed +* intentional family tree reset behavior +* intentional item placement drop-impact damage +* ant target safety with reusable living IDs + +--- + +# 25. Validation checklist + +Before returning the zip, verify at minimum: + +Static checks: + +* Run `node --check` on every JavaScript file. +* Ensure there are no `ReferenceError` risks from renamed helpers. +* Ensure all cache version references are updated consistently. +* Ensure save/load functions and UI are removed. +* Ensure no localStorage game-state save/load code remains. +* Ensure old save migration and old-save compatibility comments are removed. + +Manual or scripted behavior checks where practical: + +* The game starts fresh without reading old saves. +* No save/load UI is visible. +* Living Tarinai have internal living IDs. +* Living IDs are not displayed. +* Dead Tarinai living IDs are released and can be reused without corrupting active references. +* Historical family tree/log display snapshots remain stable after death and ID reuse. +* Ant targeting does not break when Tarinai die or living IDs are released. +* Observation events attach by structured IDs/snapshots, not by name substring. +* Hidden observation events do not appear and do not count in statistics. +* `つつく` is not classified as fight. +* Selecting a Tarinai for observation is not counted as a gameplay event/stat. +* Ball vs ball collision works. +* Fast ball hitting Tarinai knocks it by ball velocity / 5. +* Fast collision death cause is `衝突`. +* Explosion throws nearby Tarinai farther than distant Tarinai and is not clamped by walking speed. +* Falling items use visually constant falling speed. +* Existing item placement drop-impact damage still exists. +* Fence preview turns red when the footprint leaves the field. +* Red placement preview cannot place an object. +* Green placement preview places at the same footprint shown. +* Grass, fence, nest box, and water dish respect important overlap checks. +* Other item overlap remains permissive unless already constrained. +* Nest box collision uses the required 3×3 cells. +* Ball bounces off nest box solid cells. +* Spatial query results are safe from nested-query overwrites. +* Spatial grid is not stale during interaction/collision passes. +* A weather change creates only one log entry. +* Family tree hidden/offscreen update is no more frequent than every 30 seconds. +* Family tree visible update happens when dirty, not every 5 seconds. +* Family tree processing does not freeze the game. +* Family tree reset feature still exists and can clear heavy family tree data/cache. +* Family tree reset does not allow stale async renders to restore old tree content. +* Four-axis personality UI still works. +* Legacy named personality is not reintroduced. +* Damage plus total losses greater than total wins can shift personality toward derived `臆病` behavior without creating a legacy personality field. +* `爆発病` and `ずんち病` can initiate fights. +* Other diseases cannot initiate fights. +* `ねむり病` cannot initiate and cannot be targeted. +* Other non-sleep diseases can be targeted. +* Observation UI does not show ball-poke events or avoid events. +* Tool icons are correct: `👈`, `🤏`, `🚿`, nest box matching in-game appearance, and ant nest visible. +* CSS icon definitions are unified to current specs. +* `水` is absent from the tool UI. +* `水の皿` remains available. +* Simulation-only rain water still works if present. +* Lifespan death cause is `天寿を全うした`. +* Far-outside Tarinai deletion cause is `仕様により画面外で削除`. +* Ant nest behavior still works. +* Screen resize may reset the field; no special preservation behavior is required. + +Deliverable: + +* Return a zip named something like `tarinai_colony_game_ant_nest_v6_fixed.zip`. +* Include a concise summary of changed areas and validation results. diff --git a/index.html b/index.html index 9e55382..c95f7b3 100644 --- a/index.html +++ b/index.html @@ -3,13 +3,20 @@ - - - + + たりない観察 - + +
+
+
たりない観察を読み込み中
+
通信量を抑えて初期化しています…
+ +
0%
+
+
@@ -74,6 +81,7 @@
+ @@ -109,7 +117,10 @@

家系図

- +
+ + +
@@ -167,21 +178,21 @@
- - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/js/ants.js b/js/ants.js index 49a17b9..afbc83f 100644 --- a/js/ants.js +++ b/js/ants.js @@ -53,6 +53,8 @@ class AntActor { this.kind = opts.kind || "worker"; this.homeId = opts.homeId || ""; this.targetId = opts.targetId || ""; + this.targetToken = opts.targetToken || 0; + this.targetRef = opts.targetRef || null; this.x = Number.isFinite(opts.x) ? opts.x : 0; this.y = Number.isFinite(opts.y) ? opts.y : 0; this.vx = opts.vx || 0; @@ -60,6 +62,7 @@ class AntActor { this.r = opts.r || (this.kind === "queen" ? ANT_QUEEN_RADIUS : ANT_WORKER_RADIUS); this.maxHp = Number.isFinite(opts.maxHp) ? opts.maxHp : (this.kind === "queen" ? ANT_QUEEN_HP : ANT_WORKER_HP); this.hp = Number.isFinite(opts.hp) ? opts.hp : this.maxHp; + this.hpBarTimer = Number.isFinite(opts.hpBarTimer) ? opts.hpBarTimer : 0; this.state = opts.state || (this.kind === "queen" ? "founding" : "search"); this.age = opts.age || 0; this.seed = opts.seed || Math.random() * 1000; @@ -74,30 +77,19 @@ class AntActor { this.dead = Boolean(opts.dead); } - serialize() { - return { - id: this.id, kind: this.kind, homeId: this.homeId, targetId: this.targetId, - x: this.x, y: this.y, vx: this.vx, vy: this.vy, r: this.r, hp: this.hp, maxHp: this.maxHp, - state: this.state, age: this.age, seed: this.seed, wanderAngle: this.wanderAngle, - nextTurn: this.nextTurn, returnTimer: this.returnTimer, foundingX: this.foundingX, - foundingY: this.foundingY, foundingDelayUntil: this.foundingDelayUntil, carryLogAt: this.carryLogAt, foundingAttempts: this.foundingAttempts, dead: this.dead, - }; - } - - static from(worldRef, data) { - return new AntActor(worldRef, data || {}); - } - homeNest() { return (this.world?.items || []).find(it => it && !it.dead && it.id === this.homeId && it.type === "ant_nest") || null; } targetTarinai() { - return (this.world?.tarinai || []).find(t => t && !t.dead && t.id === this.targetId) || null; + if (this.targetRef && !this.targetRef.dead && this.targetRef.id === this.targetId && this.targetRef.liveToken === this.targetToken) return this.targetRef; + const live = this.world?.liveTarinaiById?.(this.targetId, this.targetToken) || null; + this.targetRef = live; + return live; } activeHaulersForTarget() { - return (this.world?.ants || []).filter(a => a && !a.dead && a.kind === "worker" && a.state === "drag" && a.targetId && a.targetId === this.targetId); + return (this.world?.ants || []).filter(a => a && !a.dead && a.kind === "worker" && a.state === "drag" && a.targetId && a.targetId === this.targetId && a.targetToken === this.targetToken); } isLeadHauler() { @@ -116,7 +108,7 @@ class AntActor { const d = distXY(this.x, this.y, t.x, t.y); if (d > maxDist) continue; if ((t.antDraggedTimer || 0) > 0 && (t.antDraggedBy || "") && t.antDraggedBy !== this.id) { - const haulers = (this.world?.ants || []).filter(a => a && !a.dead && a.kind === "worker" && a.state === "drag" && a.targetId === t.id).length; + const haulers = (this.world?.ants || []).filter(a => a && !a.dead && a.kind === "worker" && a.state === "drag" && a.targetId === t.id && a.targetToken === t.liveToken).length; if (haulers >= ANT_NEST_MAX_OUTSIDE) continue; } const weak = clamp((100 - (t.energy || 0)) / 100, 0, 1); @@ -131,9 +123,12 @@ class AntActor { update(dt) { if (this.dead) return; + const hpBefore = this.hp; this.age += dt; - if (this.kind === "queen") return this.updateQueen(dt); - return this.updateWorker(dt); + if (this.kind === "queen") this.updateQueen(dt); + else this.updateWorker(dt); + if (Number.isFinite(hpBefore) && this.hp < hpBefore - 0.01) this.hpBarTimer = Math.max(this.hpBarTimer || 0, 1.25); + this.hpBarTimer = Math.max(0, (this.hpBarTimer || 0) - dt); } updateWorker(dt) { @@ -156,6 +151,8 @@ class AntActor { if (target) { this.state = "drag"; this.targetId = target.id; + this.targetToken = target.liveToken || 0; + this.targetRef = target; this.carryLogAt = this.world?.time || 0; target.antDraggedTimer = Math.max(target.antDraggedTimer || 0, 0.45); target.antDraggedBy = this.id; @@ -209,6 +206,8 @@ class AntActor { const target = this.targetTarinai(); if (!target || target.dead || target.insideNestBoxId) { this.targetId = ""; + this.targetToken = 0; + this.targetRef = null; this.state = "return"; return; } @@ -225,6 +224,8 @@ class AntActor { if (dHome < Math.max(18, home.r * 0.66)) { this.world.completeAntHaul?.(home, target); this.targetId = ""; + this.targetToken = 0; + this.targetRef = null; this.state = "return"; return; } @@ -253,7 +254,7 @@ class AntActor { if ((this.world.time || 0) - (this.carryLogAt || -999) > 8 && Math.random() < dt * 0.04) { this.carryLogAt = this.world.time || 0; - this.world.log(`${target.name}\u304c\u30a2\u30ea\u306b\u5f15\u3063\u5f35\u3089\u308c\u3066\u3044\u308b\u3002`, "event"); + this.world.log(`${target.name}\u304c\u30a2\u30ea\u306b\u5f15\u3063\u5f35\u3089\u308c\u3066\u3044\u308b\u3002`, "event", { participants: [target] }); } if (this.hp <= 0) this.dieAsCorpse(home); } @@ -373,12 +374,14 @@ class AntActor { } drawHealthBar(ctx) { + if ((this.hpBarTimer || 0) <= 0) return; const maxHp = Math.max(1, this.maxHp || (this.kind === "queen" ? ANT_QUEEN_HP : ANT_WORKER_HP)); const pct = clamp((this.hp || 0) / maxHp, 0, 1); const bw = this.kind === "queen" ? 28 : 18; const bh = this.kind === "queen" ? 4 : 3; const y = this.y - this.r * (this.kind === "queen" ? 2.0 : 2.6); ctx.save(); + ctx.globalAlpha = clamp((this.hpBarTimer || 0) / 0.65, 0.35, 1); ctx.fillStyle = "rgba(255,252,242,0.86)"; roundedRect(ctx, this.x - bw / 2 - 1.5, y - 1.5, bw + 3, bh + 3, 4); ctx.fill(); diff --git a/js/assets.js b/js/assets.js index e5a8cb2..efd8b2b 100644 --- a/js/assets.js +++ b/js/assets.js @@ -8,8 +8,8 @@ const imageAssetIndex = new Map(); const scaledSpriteCache = new Map(); const stainOverlayCache = new Map(); -const INITIAL_IMAGE_IDS = new Set(["smile", "zunchi", "zunchi_02"]); -const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "weak", "hurt", "hurt2", "zunchi", "zunchi_02"]); +const INITIAL_IMAGE_IDS = new Set(["smile", "zunchi", "zunchi_02", "genkotsu"]); +const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "weak", "hurt", "hurt2", "zunchi", "zunchi_02", "genkotsu"]); function trimCanvasCache(cache, limit = 420) { while (cache.size > limit) { @@ -160,14 +160,18 @@ function getRenderableImage(id, fallbackId = "smile") { return null; } -function loadImages(ids = null) { +function loadImages(ids = null, onProgress = null) { setupAssetIndex(); const requested = ids ? new Set(ids) : INITIAL_IMAGE_IDS; - const tasks = []; - for (const id of requested) { - const asset = imageAssetIndex.get(id); - if (asset) tasks.push(loadImageAsset(asset, "high")); - } + const assets = [...requested].map(id => imageAssetIndex.get(id)).filter(Boolean); + const total = Math.max(assets.length, 1); + let done = 0; + if (typeof onProgress === "function") onProgress(done, total, null); + const tasks = assets.map(asset => loadImageAsset(asset, "high").then((img) => { + done += 1; + if (typeof onProgress === "function") onProgress(done, total, asset); + return img; + })); return Promise.all(tasks).then(() => undefined); } diff --git a/js/data.js b/js/data.js index fe68986..dea50df 100644 --- a/js/data.js +++ b/js/data.js @@ -34,6 +34,7 @@ const DECOR_ASSETS = [ { id: "zunchi_02", path: "assets/objects/zunchi_02.png" }, { id: "ant_queen", path: "assets/objects/ant_queen.png" }, { id: "ant_worker", path: "assets/objects/ant_worker.png" }, + { id: "genkotsu", path: "assets/objects/genkotsu.png" }, ]; const TOOL_DEFINITIONS = Object.freeze({ @@ -42,7 +43,7 @@ const TOOL_DEFINITIONS = Object.freeze({ poke: { id: "poke", label: "\u3064\u3064\u304f", placeable: false, scalable: false, iconText: "\u{1F448}", tooltip: "\u305f\u308a\u306a\u3044\u3092\u3064\u3064\u304f\u3002\u30dc\u30fc\u30eb\u3082\u3064\u3064\u3044\u3066\u8ee2\u304c\u305b\u308b\u3002" }, pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "\u305f\u308a\u306a\u3044\u3084\u7269\u3092\u3064\u307e\u3093\u3067\u79fb\u52d5\u3055\u305b\u308b\u3002" }, new: { id: "new", label: "\u8ffd\u52a0", placeable: false, scalable: false, icon: "assets/ui/tool_new.png", tooltip: "\u753b\u9762\u5916\u304b\u3089\u65b0\u3057\u3044\u305f\u308a\u306a\u3044\u3092\u547c\u3076\u3002" }, - water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", icon: "assets/ui/tool_water_hose.png", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" }, + water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" }, zunchi: { id: "zunchi", itemType: "zunchi", label: "\u305a\u3093\u3061", placeable: true, scalable: true, radius: 14, amount: 240, icon: "assets/ui/tool_zunchi.png", tooltip: "\u305f\u308a\u306a\u3044\u306e\u305a\u3093\u3061\u3002\u75c5\u6c17\u3084\u8349\u306e\u80a5\u6599\u306b\u95a2\u308f\u308b\u3002" }, sweet: { id: "sweet", itemType: "sweet", label: "\u305a\u3093\u3060\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_sweet.png", tooltip: "\u305f\u308a\u306a\u3044\u306e\u5927\u597d\u7269\u3002\u4e00\u90e8\u306e\u75c5\u6c17\u3092\u6cbb\u305b\u308b\u3002" }, love_mochi: { id: "love_mochi", itemType: "love_mochi", label: "\u3078\u3053\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_love_mochi.png", tooltip: "\u98df\u5f8c\u3057\u3070\u3089\u304f\u7e41\u6b96\u884c\u52d5\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\u308b\u3002" }, @@ -51,6 +52,7 @@ const TOOL_DEFINITIONS = Object.freeze({ sleep_drug: { id: "sleep_drug", itemType: "sleep_drug", label: "\u306d\u3080\u308a\u85ac", placeable: true, scalable: true, radius: 13, amount: 58, icon: "assets/ui/tool_sleep_drug.png", tooltip: "\u98df\u3079\u308b\u3068\u306d\u3080\u308a\u75c5\u306b\u306a\u308b\u3002\u30c0\u30e1\u30fc\u30b8\u3067\u5b8c\u6cbb\u3059\u308b\u3002" }, grass: { id: "grass", itemType: "grass", label: "\u8349", placeable: true, scalable: true, radius: 17, amount: 120, icon: "assets/ui/tool_grass.png", tooltip: "\u98df\u3079\u7269\u3002\u305f\u308a\u306a\u3044\u304c\u5b89\u5fc3\u3059\u308b\u3002" }, stone: { id: "stone", itemType: "stone", label: "\u77f3", placeable: true, scalable: true, radius: 20, amount: 999, icon: "assets/ui/tool_stone.png", tooltip: "\u91cd\u3044\u77f3\u3002\u843d\u3068\u3059\u3068\u5371\u306a\u3044\u3002" }, + genkotsu: { id: "genkotsu", itemType: "genkotsu", label: "\u3052\u3093\u3053\u3064", placeable: true, scalable: false, radius: 72, amount: 100, icon: "assets/ui/tool_genkotsu.png", tooltip: "\u4e0a\u304b\u3089\u6b63\u7fa9\u3052\u3093\u3053\u3064\u3092\u843d\u3068\u3057\u3001\u30a2\u30ea\u3068\u305f\u308a\u306a\u3044\u306b\u30c0\u30e1\u30fc\u30b8\u3068\u5439\u3063\u98db\u3073\u3092\u4e0e\u3048\u308b\u3002" }, bed: { id: "bed", itemType: "bed", label: "\u5e72\u8349\u5bdd\u5e8a", placeable: true, scalable: false, radius: 37, amount: 999, icon: "assets/ui/tool_bed.png", 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.png", 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.png", collisionShape: "circle", tooltip: "\u50cd\u304d\u30a2\u30ea\u304c\u305f\u308a\u306a\u3044\u3092\u63a2\u3057\u3001\u5de3\u3078\u904b\u3076\u3002" }, @@ -131,6 +133,7 @@ const ALPHA_BOUNDS = { zunchi_02: { x: 8, y: 63, w: 220, h: 105, imageW: 236, imageH: 178 }, ant_queen: { x: 0, y: 0, w: 300, h: 155, imageW: 300, imageH: 155 }, ant_worker: { x: 0, y: 0, w: 140, h: 72, imageW: 140, imageH: 72 }, + genkotsu: { x: 8, y: 8, w: 220, h: 283, imageW: 236, imageH: 299 }, }; const NEEDS = ["\u98df\u3079\u7269", "\u4ef2\u9593", "\u7720\u308a", "\u52c7\u6c17", "\u6c34", "\u9759\u3051\u3055", "\u3042\u305f\u305f\u304b\u3055", "\u610f\u5473", "\u7a7a\u9593", "\u81ea\u4fe1"]; @@ -145,7 +148,6 @@ const CONFIG = { traceLimit: 64, splatLimit: 48, effectLimit: 96, - saveKey: "tarinai-colony-save-v38", worldPadding: 30, hungerPerMinute: 11.2, lonelinessPerMinute: 7.6, diff --git a/js/items.js b/js/items.js index 6a666c5..30c8d86 100644 --- a/js/items.js +++ b/js/items.js @@ -76,6 +76,12 @@ class Item { this.fuseTimer = 5; this.fuseMax = 5; } + if (type === "genkotsu") { + this.amount = 100; + this.dropMax = 0; + this.dropImpactDone = false; + this.impactFlash = 0; + } if (isServingFoodType(type)) { this.toolSize = "medium"; this.foodServingScale = 1; @@ -106,6 +112,11 @@ class Item { else this.amount = 0; } } + if (this.type === "genkotsu") { + this.impactFlash = Math.max(0, (this.impactFlash || 0) - dt); + if (this.dropImpactDone) this.amount -= dt * 115; + else this.amount = Math.max(this.amount || 0, 100); + } if (this.type === "ball") this.updateBall(dt, worldRef); if (this.type === "zunchi") this.updateZunchiMotion(dt, worldRef); if (this.type === "grass") { @@ -176,8 +187,6 @@ class Item { const speed = Math.hypot(this.vx || 0, this.vy || 0); const searchRadius = Math.max(150, (this.r || 18) + speed * Math.max(dt || 0.016, 0.016) + 120); const items = worldRef.nearbyItems(this.x, this.y, searchRadius) || []; - let solidChecks = 0; - const maxSolidChecks = CONFIG.maxFenceCollisionChecks ?? 24; for (const it of items) { if (!it || it === this || it.dead) continue; if (it.type === "bed") { @@ -187,8 +196,6 @@ class Item { } const rects = worldRef.solidObstacleRects ? worldRef.solidObstacleRects(it) : []; if (!rects.length) continue; - if (solidChecks >= maxSolidChecks) continue; - solidChecks += 1; for (const rect of rects) this.resolveBallRectBounce(rect, worldRef, it); } } @@ -467,148 +474,6 @@ class Item { get dead() { return this.amount <= 0; } - serialize() { - return { - type: this.type, x: this.x, y: this.y, amount: this.amount, age: this.age, seed: this.seed, - stage: this.stage, stageTimer: this.stageTimer, fertility: this.fertility, freshness: this.freshness, - growth: this.growth, health: this.health, seedTimer: this.seedTimer, eatenAmount: this.eatenAmount, fertilityBoost: this.fertilityBoost, lifeSpan: this.lifeSpan, wither: this.wither, - comfort: this.comfort, wear: this.wear, crawlOpen: this.crawlOpen, zunchiVariant: this.zunchiVariant, toolSize: this.toolSize, foodServingScale: this.foodServingScale, foodServingsMax: this.foodServingsMax, foodServingsRemaining: this.foodServingsRemaining, blastScale: this.blastScale, fuseTimer: this.fuseTimer, fuseMax: this.fuseMax, dropTimer: this.dropTimer, dropMax: this.dropMax, - vx: this.vx, vy: this.vy, prevX: this.prevX, prevY: this.prevY, - spin: this.spin, spinVelocity: this.spinVelocity, lastKickedAt: this.lastKickedAt, lastKickerId: this.lastKickerId, - lastPokedAt: this.lastPokedAt, pokeCombo: this.pokeCombo, lastPokeAngle: this.lastPokeAngle, - antCount: this.antCount, antSpawnTimer: this.antSpawnTimer, antSpawnCooldown: this.antSpawnCooldown, antWorkers: Array.isArray(this.antWorkers) ? this.antWorkers.slice() : undefined, queenSpawnAt: this.queenSpawnAt, workerSprite: this.workerSprite, decayTimer: this.decayTimer - }; - } - static from(o) { - const type = o?.type || "grass"; - const it = Object.create(Item.prototype); - Item._loadId = (Item._loadId || 0) + 1; - it.id = o.id || `loaded-${Item._loadId}`; - it.type = type; - it.x = o.x; - it.y = o.y; - it.r = itemRadiusFor(type, 12); - it.amount = o.amount ?? itemAmountFor(type, 80); - it.age = o.age || 0; - it.seed = o.seed || Math.random() * 1000; - it.dropTimer = 0; - it.dropMax = 0; - it.dropImpactDone = false; - it.lifecycleTimer = o.lifecycleTimer ?? rand(0, type === "grass" ? 1.15 : 0.55); - it.lifecycleInterval = o.lifecycleInterval ?? (type === "grass" ? (1.75 + stableUnit(it.id, "grass-life") * 0.90) : (type === "zunchi" ? (0.95 + stableUnit(it.id, "zunchi-life") * 0.45) : 0)); - if (type === "grass") { - it.growth = 0.25; - it.health = 1; - it.seedTimer = rand(18, 38); - it.eatenAmount = 0; - it.fertilityBoost = 0; - } - if (type === "bed") { - it.comfort = 1; - it.wear = 0; - } - if (type === "ball") { - it.vx = o.vx || 0; - it.vy = o.vy || 0; - it.prevX = o.prevX ?? o.x; - it.prevY = o.prevY ?? o.y; - it.spin = o.spin || 0; - it.spinVelocity = o.spinVelocity || 0; - it.lastKickedAt = o.lastKickedAt ?? -999; - it.lastKickerId = o.lastKickerId || ""; - it.lastPokedAt = o.lastPokedAt ?? -999; - it.pokeCombo = o.pokeCombo || 0; - it.lastPokeAngle = o.lastPokeAngle || rand(0, Math.PI * 2); - } - if (type === "zunchi") { - it.stage = "fresh"; - it.stageTimer = 0; - it.fertility = 0; - it.freshness = 1; - it.zunchiVariant = "zunchi"; - } - if (type === "ant_nest") { - it.antCount = Number.isFinite(o.antCount) ? clamp(Math.round(o.antCount), 0, ANT_NEST_MAX_COUNT || 10) : (ANT_NEST_START_COUNT || 6); - it.antSpawnTimer = o.antSpawnTimer ?? rand(0.6, 2.2); - it.antSpawnCooldown = o.antSpawnCooldown ?? rand(0.8, 2.4); - const legacyPool = Array.isArray(o.antWorkers) ? o.antWorkers : (Array.isArray(o.workerHpPool) ? o.workerHpPool : null); - it.antWorkers = Array.isArray(legacyPool) - ? legacyPool.map(v => clamp(Number(v) || ANT_WORKER_HP || 32, 0.1, ANT_WORKER_HP || 32)).slice(0, ANT_NEST_MAX_COUNT || 10) - : (typeof buildAntWorkerPool === "function" - ? buildAntWorkerPool(it.antCount) - : Array.from({ length: it.antCount }, () => ANT_WORKER_HP || 32)); - it.queenSpawnAt = Number.isFinite(o.queenSpawnAt) ? o.queenSpawnAt : -999; - } - if (type === "ant_corpse") { - it.workerSprite = o.workerSprite || "ant_worker"; - it.decayTimer = o.decayTimer || 0; - } - if (type === "firecracker") { - it.fuseTimer = 5; - it.fuseMax = 5; - } - it.stage = o.stage || it.stage; - it.stageTimer = o.stageTimer || 0; - it.fertility = o.fertility || 0; - it.freshness = o.freshness ?? it.freshness; - it.growth = o.growth ?? it.growth; - it.health = o.health ?? it.health; - it.seedTimer = o.seedTimer ?? it.seedTimer; - it.eatenAmount = o.eatenAmount || 0; - it.fertilityBoost = o.fertilityBoost || 0; - it.lifeSpan = o.lifeSpan ?? it.lifeSpan; - it.wither = o.wither ?? it.wither ?? 0; - it.comfort = o.comfort ?? it.comfort; - it.wear = o.wear ?? it.wear; - it.crawlOpen = o.crawlOpen ?? it.crawlOpen; - it.vx = o.vx ?? it.vx ?? 0; - it.vy = o.vy ?? it.vy ?? 0; - it.prevX = o.prevX ?? it.prevX ?? it.x; - it.prevY = o.prevY ?? it.prevY ?? it.y; - it.spin = o.spin ?? it.spin ?? 0; - it.spinVelocity = o.spinVelocity ?? it.spinVelocity ?? 0; - it.lastKickedAt = o.lastKickedAt ?? it.lastKickedAt ?? -999; - it.lastKickerId = o.lastKickerId || it.lastKickerId || ""; - it.lastPokedAt = o.lastPokedAt ?? it.lastPokedAt ?? -999; - it.pokeCombo = o.pokeCombo ?? it.pokeCombo ?? 0; - it.lastPokeAngle = o.lastPokeAngle ?? it.lastPokeAngle ?? rand(0, Math.PI * 2); - it.zunchiVariant = o.zunchiVariant || it.zunchiVariant || "zunchi"; - it.antCount = Number.isFinite(o.antCount) ? clamp(Math.round(o.antCount), 0, ANT_NEST_MAX_COUNT || 10) : it.antCount; - it.antSpawnTimer = o.antSpawnTimer ?? it.antSpawnTimer; - it.antSpawnCooldown = o.antSpawnCooldown ?? it.antSpawnCooldown; - if (type === "ant_nest") { - const legacyPool = Array.isArray(o.antWorkers) ? o.antWorkers : (Array.isArray(o.workerHpPool) ? o.workerHpPool : null); - if (Array.isArray(legacyPool)) { - it.antWorkers = legacyPool.map(v => clamp(Number(v) || 0, 0, ANT_WORKER_HP || 32)).filter(v => v > 0).slice(0, ANT_NEST_MAX_COUNT || 10); - } else if (!Array.isArray(it.antWorkers)) { - it.antWorkers = typeof buildAntWorkerPool === "function" - ? buildAntWorkerPool(it.antCount || (ANT_NEST_START_COUNT || 6)) - : Array.from({ length: it.antCount || (ANT_NEST_START_COUNT || 6) }, () => ANT_WORKER_HP || 32); - } - it.queenSpawnAt = Number.isFinite(o.queenSpawnAt) ? o.queenSpawnAt : (it.queenSpawnAt ?? -999); - } - it.workerSprite = o.workerSprite || it.workerSprite; - it.decayTimer = o.decayTimer ?? it.decayTimer; - it.toolSize = o.toolSize || it.toolSize || "medium"; - it.foodServingScale = o.foodServingScale || it.foodServingScale || ({ small: 0.68, medium: 1.0, large: 1.48 }[it.toolSize] || 1); - if (isServingFoodType(it.type)) { - const servingMax = foodServingsForSize(it.toolSize); - it.foodServingsMax = Number.isFinite(o.foodServingsMax) ? o.foodServingsMax : servingMax; - if (Number.isFinite(o.foodServingsRemaining)) { - it.foodServingsRemaining = clamp(o.foodServingsRemaining, 0, it.foodServingsMax); - } else { - it.foodServingsRemaining = Math.max(1, Math.min(it.foodServingsMax, Math.ceil(it.foodServingsMax * clamp((o.amount ?? it.amount) / 70, 0.05, 1)))); - } - it.amount = it.foodServingsRemaining; - } - it.blastScale = o.blastScale || it.blastScale || 1; - it.fuseTimer = o.fuseTimer ?? it.fuseTimer; - it.fuseMax = o.fuseMax ?? it.fuseMax; - it.dropTimer = 0; - it.dropMax = 0; - it.dropImpactDone = true; - return it; - } draw(ctx, t, lighting = null) { const lightState = lighting || getLightingState(world); const night = clamp(lightState.nightStrength * 1.35, 0, 1); @@ -618,7 +483,8 @@ class Item { 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; - const dropY = dropT > 0 ? -170 * Math.pow(dropT, 0.42) : 0; + const dropFallHeight = this.type === "genkotsu" ? Math.max(360, this.r * 5.8) : 170; + const dropY = dropT > 0 ? -dropFallHeight * dropT : 0; const shadow = projectedShadowParams(lightState, Math.max(0.4, this.r / 18)); if (this.type !== "trace" && this.type !== "splat") { const zunchiId = this.type === "zunchi" ? (this.zunchiVariant || "zunchi") : null; @@ -689,13 +555,8 @@ class Item { ctx.ellipse(-this.r * 0.28, -this.r * 0.18, this.r * 0.23, this.r * 0.10, -0.35, 0, Math.PI * 2); ctx.fill(); } else if (fightMochi) { - ctx.fillStyle = "#d55632"; - ctx.font = `${Math.round((this.r || 13) * 1.02)}px sans-serif`; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText("\u2726", 0, 1); - ctx.strokeStyle = "rgba(168,76,42,0.72)"; - ctx.lineWidth = 1.4; + ctx.strokeStyle = "rgba(168,76,42,0.82)"; + ctx.lineWidth = 1.8; ctx.beginPath(); ctx.moveTo(-this.r * 0.44, -this.r * 0.42); ctx.lineTo(this.r * 0.44, this.r * 0.42); @@ -725,6 +586,42 @@ class Item { ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("Z", 0, this.r * 1.18); + } else if (this.type === "genkotsu") { + const img = typeof getRenderableImage === "function" ? getRenderableImage("genkotsu", "genkotsu") : images.get("genkotsu"); + const impact = clamp(this.impactFlash || 0, 0, 1); + ctx.save(); + ctx.shadowColor = "rgba(54,42,31,0.26)"; + ctx.shadowBlur = 5 + impact * 8; + ctx.shadowOffsetY = 4; + const squashY = 1 - impact * 0.08; + const stretchX = 1 + impact * 0.05; + ctx.scale(stretchX, squashY); + if (img) { + const metrics = typeof getImageMetrics === "function" ? getImageMetrics("genkotsu") : null; + const w = this.r * 2.55; + const h = w * (metrics?.ratio || 1.27); + ctx.drawImage(img, -w * 0.5, -h * 0.76, w, h); + } else { + ctx.fillStyle = "rgba(255,255,255,0.94)"; + ctx.strokeStyle = "rgba(64,64,64,0.78)"; + ctx.lineWidth = 5; + roundedRect(ctx, -this.r * 0.72, -this.r * 1.45, this.r * 1.44, this.r * 1.84, 18); + ctx.fill(); ctx.stroke(); + ctx.fillStyle = "rgba(42,46,52,0.92)"; + ctx.font = `bold ${Math.round(this.r * 0.45)}px sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText("\u6b63", 0, -this.r * 0.74); + ctx.fillText("\u7fa9", 0, -this.r * 0.27); + } + if (impact > 0.01) { + ctx.globalAlpha = impact * 0.20; + ctx.fillStyle = "#ffffff"; + ctx.beginPath(); + ctx.ellipse(0, -this.r * 0.65, this.r * 0.92, this.r * 0.22, -0.10, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); } else if (this.type === "water_bowl") { ctx.fillStyle = "rgba(171, 120, 76, 0.72)"; ctx.strokeStyle = "rgba(94, 69, 48, 0.55)"; @@ -812,21 +709,26 @@ class Item { ctx.stroke(); } } else if (this.type === "nest_box") { - ctx.fillStyle = styleNight ? "#7d6040" : (styleWarm ? "#b98246" : "#9a6b3d"); - ctx.strokeStyle = styleNight ? "#4a3a2d" : "#5d4028"; - ctx.lineWidth = 2.4; + ctx.fillStyle = styleNight ? "#5f4933" : (styleWarm ? "#89613e" : "#775237"); + ctx.strokeStyle = styleNight ? "#31261f" : "#3f2e24"; + ctx.lineWidth = 2.6; roundedRect(ctx, -this.r * 1.28, -this.r * 0.80, this.r * 2.56, this.r * 1.52, 9); ctx.fill(); ctx.stroke(); - ctx.fillStyle = styleNight ? "#3b2f27" : "#65432a"; - ctx.beginPath(); - ctx.arc(0, -this.r * 0.02, this.r * 0.42, 0, Math.PI * 2); + ctx.fillStyle = styleNight ? "#6e5439" : "#8a5f3b"; + roundedRect(ctx, -this.r * 1.12, -this.r * 0.62, this.r * 2.24, this.r * 1.18, 6); + ctx.fill(); + ctx.strokeStyle = styleNight ? "rgba(42,32,25,0.76)" : "rgba(65,45,32,0.72)"; + ctx.lineWidth = 2.0; + ctx.beginPath(); + ctx.moveTo(-this.r * 0.95, -this.r * 0.50); + ctx.lineTo(this.r * 0.95, -this.r * 0.50); + ctx.moveTo(-this.r * 0.95, this.r * 0.48); + ctx.lineTo(this.r * 0.95, this.r * 0.48); + ctx.stroke(); + ctx.fillStyle = styleNight ? "#2b211c" : "#3c281f"; + ctx.beginPath(); + ctx.arc(0, -this.r * 0.08, this.r * 0.40, 0, Math.PI * 2); ctx.fill(); - ctx.fillStyle = "rgba(255,230,152,0.38)"; - for (let i = 0; i < 6; i++) { - ctx.beginPath(); - ctx.ellipse(randSeed(this.seed + i, -this.r * 0.84, this.r * 0.84), randSeed(this.seed + 20 + i, this.r * 0.32, this.r * 0.68), this.r * 0.26, this.r * 0.045, randSeed(this.seed + 40 + i, -0.7, 0.7), 0, Math.PI * 2); - ctx.fill(); - } } else if (this.type === "ant_nest") { const count = clamp(Math.round(this.antCount ?? ANT_NEST_START_COUNT ?? 6), 0, ANT_NEST_MAX_COUNT ?? 10); ctx.fillStyle = styleNight ? "#5e4634" : (styleWarm ? "#956b42" : "#77583a"); diff --git a/js/main.js b/js/main.js index d2e6542..3b9286f 100644 --- a/js/main.js +++ b/js/main.js @@ -6,6 +6,25 @@ let fpsFrames = 0; let perfResizeTimer = 0; let lastPerfLevel = 0; window.__tarinaiFps = 0; + +function setLoadingProgress(text = "", ratio = 0) { + const screen = document.getElementById("loadingScreen"); + if (!screen) return; + const pct = clamp(Number(ratio) || 0, 0, 1); + const label = document.getElementById("loadingText"); + const bar = document.getElementById("loadingBar"); + const percent = document.getElementById("loadingPercent"); + if (label && text) label.textContent = text; + if (bar) bar.style.width = `${Math.round(pct * 100)}%`; + if (percent) percent.textContent = `${Math.round(pct * 100)}%`; +} + +function hideLoadingScreen() { + setLoadingProgress("\u8d77\u52d5\u5b8c\u4e86", 1); + const screen = document.getElementById("loadingScreen"); + if (!screen) return; + window.setTimeout(() => screen.classList.add("hidden"), 120); +} function loop() { const t = nowSec(); const rawDt = t - last; @@ -38,16 +57,25 @@ function loop() { requestAnimationFrame(loop); } -loadImages().then(() => { +setLoadingProgress("\u521d\u671f\u753b\u50cf\u3092\u8aad\u307f\u8fbc\u307f\u4e2d", 0.08); +loadImages(null, (done, total) => { + const p = total > 0 ? done / total : 1; + setLoadingProgress(`\u521d\u671f\u753b\u50cf\u3092\u8aad\u307f\u8fbc\u307f\u4e2d ${done}/${total}`, 0.08 + p * 0.46); +}).then(() => { + setLoadingProgress("UI\u3092\u6e96\u5099\u4e2d", 0.62); bindUI(); + setLoadingProgress("\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u69cb\u7bc9\u4e2d", 0.72); applyFieldLayout("garden"); world.reset(null, "garden"); selectTool("observe"); + setLoadingProgress("\u8868\u793a\u3092\u521d\u671f\u5316\u4e2d", 0.84); renderLog(world.logs); renderArchive(); render(); renderStats(); syncTopButtons(); if (typeof startBackgroundImageLoading === "function") startBackgroundImageLoading(); + setLoadingProgress("\u30b7\u30df\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u59cb\u4e2d", 0.94); loop(); + requestAnimationFrame(hideLoadingScreen); }); diff --git a/js/sim_core.js b/js/sim_core.js index 43fb3bb..a8330e9 100644 --- a/js/sim_core.js +++ b/js/sim_core.js @@ -64,8 +64,6 @@ function stableUnit(seed, salt = "") { return ((h >>> 0) % 100000) / 100000; } -// Legacy named personalities were removed. Keep these stubs only so old save data -// and old UI fallback references cannot revive the pre-four-axis personality model. const PERSONALITIES = Object.freeze({}); const PERSONALITY_IDS = []; @@ -255,12 +253,14 @@ function relationDefaults() { function isParentChild(a, b) { if (!a || !b) return false; - return Boolean(a.parents?.includes(b.id) || b.parents?.includes(a.id)); + const aKey = a.familyKey || a.id; + const bKey = b.familyKey || b.id; + return Boolean(a.parents?.includes(bKey) || b.parents?.includes(aKey)); } function relationDisplayName(worldRef, id) { if (!id) return "\u306a\u3057"; - const live = worldRef?.tarinai?.find(t => t.id === id); + const live = worldRef?.tarinai?.find(t => t.id === id || t.familyKey === id); if (live) return live.name; const fam = worldRef?.family?.[id]; return fam?.name || "\u4e0d\u660e"; diff --git a/js/tarinai.js b/js/tarinai.js index 5cfaf88..65532f1 100644 --- a/js/tarinai.js +++ b/js/tarinai.js @@ -17,7 +17,9 @@ function tarinaiRoundRectPath(ctx, x, y, w, h, r) { class Tarinai { constructor(world, opts = {}) { this.world = world; - this.id = opts.id || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`); + this.familyKey = opts.familyKey || opts.archiveKey || opts.id || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`); + this.id = opts.id || ""; + this.liveToken = opts.liveToken || 0; this.name = opts.name || makeName(world); // Legacy string personality is intentionally ignored. The game now uses only the four-axis personality model. this.personality = ""; @@ -25,7 +27,7 @@ class Tarinai { const typeMeta = SPRITES.find(s => s.id === this.type); if (typeMeta?.actionOnly || typeMeta?.displayOnly) this.type = baseSpriteId(); this.birthPersonality = normalizePersonality(opts.birthPersonality || opts.personalityBirth); - if (!opts.birthPersonality && !opts.personalityBirth) this.birthPersonality = randomBirthPersonality(this.id); + if (!opts.birthPersonality && !opts.personalityBirth) this.birthPersonality = randomBirthPersonality(this.familyKey); this.currentPersonality = normalizePersonality(opts.currentPersonality || opts.personalityCurrent || this.birthPersonality); this.personalityDaily = opts.personalityDaily && typeof opts.personalityDaily === "object" ? JSON.parse(JSON.stringify(opts.personalityDaily)) : null; ensurePersonality(this); @@ -76,8 +78,8 @@ class Tarinai { this.sleeping = false; this.blink = rand(0, 10); this.lastSocial = 0; - this.goodMode = opts.goodMode ?? stableChoice(this.id, "good-mode", displayNormalSprites()) ?? "smile"; - this.facing = opts.facing ?? (stableUnit(this.id, "facing") < 0.5 ? -1 : 1); + this.goodMode = opts.goodMode ?? stableChoice(this.familyKey, "good-mode", displayNormalSprites()) ?? "smile"; + this.facing = opts.facing ?? (stableUnit(this.familyKey, "facing") < 0.5 ? -1 : 1); this.visualFacing = opts.visualFacing ?? this.facing; this.facingLockUntil = opts.facingLockUntil ?? 0; this.eatTimer = opts.eatTimer ?? 0; @@ -95,7 +97,7 @@ class Tarinai { this.birthRitualLeader = !!opts.birthRitualLeader; this.pokeFlashTimer = opts.pokeFlashTimer ?? 0; this.zunchiStain = opts.zunchiStain ?? 0; - this.zunchiStainSeed = opts.zunchiStainSeed ?? stableUnit(this.id, "zunchi-stain"); + this.zunchiStainSeed = opts.zunchiStainSeed ?? stableUnit(this.familyKey, "zunchi-stain"); this.zunchiDisease = Boolean(opts.zunchiDisease); this.zunchiDiseaseSeverity = opts.zunchiDiseaseSeverity ?? (this.zunchiDisease ? 1 : 0); this.zunchiDiseaseCooldown = opts.zunchiDiseaseCooldown ?? 0; @@ -120,7 +122,7 @@ class Tarinai { this.focusPulseTimer = opts.focusPulseTimer ?? 0; if (this.isZunchiSlave) this.name = "\u305a\u3093\u3061\u3069\u308c\u3044"; this.tempComfort = opts.tempComfort ?? 0.62; - this.tempTimer = opts.tempTimer ?? stableUnit(this.id, "temp-start") * 0.55; + this.tempTimer = opts.tempTimer ?? stableUnit(this.familyKey, "temp-start") * 0.55; this.parents = opts.parents || []; this.parentNames = opts.parentNames || []; this.children = opts.children || []; @@ -143,60 +145,25 @@ class Tarinai { this.relationships = opts.relationships && typeof opts.relationships === "object" ? JSON.parse(JSON.stringify(opts.relationships)) : {}; this.fallTimer = opts.fallTimer ?? 0; this.fallMax = opts.fallMax ?? 1.15; - this.fallDir = opts.fallDir ?? (stableUnit(this.id, "fall-dir") < 0.5 ? -1 : 1); + this.fallDir = opts.fallDir ?? (stableUnit(this.familyKey, "fall-dir") < 0.5 ? -1 : 1); this.blastSpinTimer = opts.blastSpinTimer ?? 0; this.blastSpinMax = opts.blastSpinMax ?? 0; this.postBirthPeaceTimer = opts.postBirthPeaceTimer ?? 0; - this.sleepStart = 0.60 + stableUnit(this.id, "sleep-start") * 0.22; - this.sleepEnd = 0.15 + stableUnit(this.id, "sleep-end") * 0.14; - this.sleepFacing = stableUnit(this.id, "sleep-facing") < 0.5 ? -1 : 1; + this.sleepStart = 0.60 + stableUnit(this.familyKey, "sleep-start") * 0.22; + this.sleepEnd = 0.15 + stableUnit(this.familyKey, "sleep-end") * 0.14; + this.sleepFacing = stableUnit(this.familyKey, "sleep-facing") < 0.5 ? -1 : 1; this.nextBubbleAt = 0; this.nextSleepBubbleAt = 0; this.nextFearBubbleAt = 0; this.nextCursorHeartAt = opts.nextCursorHeartAt ?? 0; this.cursorPetting = opts.cursorPetting ?? 0; - this.aiTimer = opts.aiTimer ?? stableUnit(this.id, "ai-start") * 0.22; - this.envTimer = opts.envTimer ?? stableUnit(this.id, "env-start") * 0.55; + this.aiTimer = opts.aiTimer ?? stableUnit(this.familyKey, "ai-start") * 0.22; + this.envTimer = opts.envTimer ?? stableUnit(this.familyKey, "env-start") * 0.55; this.visibleSpriteId = opts.visibleSpriteId || null; this.spriteLockUntil = opts.spriteLockUntil ?? 0; this.entryTimer = opts.entryTimer ?? 0; } - serialize() { - return { - id: this.id, name: this.name, type: this.type, x: this.x, y: this.y, vx: this.vx, vy: this.vy, - scale: this.scale, age: this.age, lifeSpan: this.lifeSpan, generation: this.generation, hasPaired: this.hasPaired, - dead: this.dead, deathReason: this.deathReason, lastDamageCause: this.lastDamageCause, lastDamageAmount: this.lastDamageAmount, lastDamageAt: this.lastDamageAt, lastMajorDamageCause: this.lastMajorDamageCause, lastMajorDamageAmount: this.lastMajorDamageAmount, lastMajorDamageAt: this.lastMajorDamageAt, - hunger: this.hunger, loneliness: this.loneliness, energy: this.energy, stress: this.stress, hpBarTimer: this.hpBarTimer, stressBarTimer: this.stressBarTimer, - affection: this.affection, need: this.need, state: this.state, - goodMode: this.goodMode, facing: this.facing, visualFacing: this.visualFacing, facingLockUntil: this.facingLockUntil, eatTimer: this.eatTimer, - foodReactTimer: this.foodReactTimer, surpriseTimer: this.surpriseTimer, fearTimer: this.fearTimer, - intimidateTimer: this.intimidateTimer, intimidateTargetId: this.intimidateTargetId, intimidatedTimer: this.intimidatedTimer, - birthRitualTimer: this.birthRitualTimer, birthRitualMax: this.birthRitualMax, birthPartnerId: this.birthPartnerId, - birthRitualRole: this.birthRitualRole, birthRitualLeader: this.birthRitualLeader, - pokeFlashTimer: this.pokeFlashTimer, zunchiStain: this.zunchiStain, zunchiStainSeed: this.zunchiStainSeed, - totalFightLosses: this.totalFightLosses, totalFightWins: this.totalFightWins, isZunchiSlave: this.isZunchiSlave, formerName: this.formerName, loveMochiTimer: this.loveMochiTimer, fightMochiTimer: this.fightMochiTimer, - sleepDisease: this.sleepDisease, sleepDiseaseCooldown: this.sleepDiseaseCooldown, explosionDisease: this.explosionDisease, explosionDiseaseTimer: this.explosionDiseaseTimer, fightDisease: this.fightDisease, fightDiseaseCooldown: this.fightDiseaseCooldown, lastBleedAt: this.lastBleedAt, favorite: this.favorite, insideNestBoxId: this.insideNestBoxId || null, nestFade: this.nestFade || 0, nestBoxEnteredAt: this.nestBoxEnteredAt, nestBoxStayUntil: this.nestBoxStayUntil, focusPulseTimer: this.focusPulseTimer || 0, sleepDiseaseAnchorX: this.sleepDiseaseAnchorX, sleepDiseaseAnchorY: this.sleepDiseaseAnchorY, records: this.records ? this.records.slice(0, 42) : [], - zunchiDisease: this.zunchiDisease, zunchiDiseaseSeverity: this.zunchiDiseaseSeverity, zunchiDiseaseCooldown: this.zunchiDiseaseCooldown, - tempComfort: this.tempComfort, tempTimer: this.tempTimer, - parents: this.parents, parentNames: this.parentNames, children: this.children, birthTime: this.birthTime, - fightTimer: this.fightTimer, fightCooldown: this.fightCooldown, fightTargetId: this.fightTargetId, fightTargetIds: this.fightTargetIds, targetGiveupKey: this.targetGiveupKey, targetGiveupUntil: this.targetGiveupUntil, - panicTarget: this.panicTarget, panicTargetUntil: this.panicTargetUntil, panicStuckTimer: this.panicStuckTimer, - nextHeadbutt: this.nextHeadbutt, digest: this.digest, poopCount: this.poopCount, stretchTimer: this.stretchTimer, - grassEatTimer: this.grassEatTimer, hurtTimer: this.hurtTimer, - defeatedTimer: this.defeatedTimer, defeatedById: this.defeatedById, fightWinnerId: this.fightWinnerId, - relationships: this.relationships, fallTimer: this.fallTimer, fallMax: this.fallMax, fallDir: this.fallDir, - birthPersonality: normalizePersonality(this.birthPersonality), currentPersonality: normalizePersonality(this.currentPersonality), personalityDaily: this.personalityDaily || null, - blastSpinTimer: this.blastSpinTimer, blastSpinMax: this.blastSpinMax, postBirthPeaceTimer: this.postBirthPeaceTimer, - lastMealColor: this.lastMealColor, aiTimer: this.aiTimer, envTimer: this.envTimer, nextCursorHeartAt: this.nextCursorHeartAt, cursorPetting: this.cursorPetting, - entryTimer: this.entryTimer || 0, - }; - } - - static from(world, o) { - return new Tarinai(world, o); - } - get lack() { return clamp((this.hunger + this.loneliness + this.stress + (100 - this.energy)) / 4, 0, 100); } @@ -221,7 +188,7 @@ class Tarinai { if (!this.juvenile || !this.parents?.length) return null; let best = null, bestD = Infinity; for (const id of this.parents) { - const p = this.world.tarinai.find(t => t.id === id && !t.dead); + const p = this.world.tarinai.find(t => (t.familyKey || t.id) === id && !t.dead); if (!p) continue; const d = dist(this, p); if (d < bestD) { best = p; bestD = d; } @@ -292,7 +259,6 @@ class Tarinai { const changedB = this.adjustPersonality?.("aggression", -0.015, "after being hurt") || 0; this.fearTimer = Math.max(this.fearTimer || 0, 1.2); this.stress = clamp((this.stress || 0) + 5, 0, 130); - this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.35, "\u3053\u308f\u3044\u2026", "rgba(90,80,120,0.78)"); return Boolean(changedA || changedB); } @@ -313,10 +279,12 @@ class Tarinai { if (event) { rel.lastEvent = event; rel.lastTime = this.world.time; } } - strongestRelation(kind = "friend") { + strongestRelation(kind = "friend", liveOnly = true) { let bestId = null; let bestScore = kind === "fear" ? 5 : FRIEND_AFFINITY_THRESHOLD; + const liveIds = liveOnly ? new Set((this.world?.tarinai || []).filter(o => o && !o.dead && o !== this).map(o => o.id).filter(Boolean)) : null; for (const [id, rel] of Object.entries(this.relationships || {})) { + if (liveIds && !liveIds.has(id)) continue; const score = kind === "fear" ? (rel.fear || 0) : (rel.affinity || 0); if (score > bestScore) { bestId = id; bestScore = score; } } @@ -575,7 +543,7 @@ class Tarinai { normal: displayNormalSprites(), }; const options = (pools[category] || []).filter(id => SPRITES.some(s => s.id === id)); - return stableChoice(this.id, `variant-${category}`, options) || options[0] || this.goodMode || "smile"; + return stableChoice(this.familyKey || this.id, `variant-${category}`, options) || options[0] || this.goodMode || "smile"; } infectZunchiDisease(source = null, force = false) { @@ -625,7 +593,7 @@ class Tarinai { } if (!this.zunchiDisease && this.zunchiStain > 82 && Math.random() < dt * clamp((this.zunchiStain - 82) / 18, 0, 1) * 0.045) { this.infectZunchiDisease(null, true); - this.world?.log?.(`${this.name}\u306f\u305a\u3093\u3061\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "accident"); + this.world?.log?.(`${this.name}\u306f\u305a\u3093\u3061\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "accident", { participants: [this] }); } if (this.zunchiDisease) { this.zunchiDiseaseSeverity = clamp((this.zunchiDiseaseSeverity || 0.7) + dt * 0.004, 0, 1.25); @@ -634,7 +602,7 @@ class Tarinai { if (this.energy > 82) this.zunchiDiseaseSeverity = Math.max(0, this.zunchiDiseaseSeverity - dt * (this.energy > 94 ? 0.020 : 0.010)); this.emitZunchiDiseaseEffect(dt); if (this.zunchiDiseaseSeverity <= 0.04) { - if (this.recoverZunchiDisease("\u4f53\u529b\u304c\u623b\u308a\u3001\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f")) this.world?.log?.(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f\u3002`, "accident"); + if (this.recoverZunchiDisease("\u4f53\u529b\u304c\u623b\u308a\u3001\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f")) this.world?.log?.(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f\u3002`, "accident", { participants: [this] }); } } } @@ -661,7 +629,7 @@ class Tarinai { this.fightTargetIds = []; this.thought = "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b"; this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(72,76,120,0.76)"); - this.world?.log?.(`${this.name}\u306f\u306d\u3080\u308a\u75c5\u306b\u304b\u304b\u3063\u305f\u3002`, "accident"); + this.world?.log?.(`${this.name}\u306f\u306d\u3080\u308a\u75c5\u306b\u304b\u304b\u3063\u305f\u3002`, "accident", { participants: [this] }); return true; } @@ -676,7 +644,7 @@ class Tarinai { this.thought = reason || "\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f"; this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.65); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "!", "rgba(72,76,120,0.76)"); - this.world?.log?.(`${this.name}\u306e\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident"); + this.world?.log?.(`${this.name}\u306e\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] }); return true; } @@ -687,7 +655,7 @@ class Tarinai { this.fearTimer = Math.max(this.fearTimer || 0, 0.55); this.thought = "\u7206\u767a\u75c5\u3067\u843d\u3061\u7740\u304b\u306a\u3044"; this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.2, "!", "rgba(178,78,42,0.82)"); - this.world?.log?.(`${this.name}\u306f\u7206\u767a\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "accident"); + this.world?.log?.(`${this.name}\u306f\u7206\u767a\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "accident", { participants: [this] }); return true; } @@ -698,7 +666,7 @@ class Tarinai { this.thought = reason || "\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f"; this.stress = clamp(this.stress - 10, 0, 130); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)"); - this.world?.log?.(`${this.name}\u306e\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident"); + this.world?.log?.(`${this.name}\u306e\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] }); return true; } @@ -713,7 +681,7 @@ class Tarinai { this.fearTimer = Math.max(this.fearTimer || 0, 0.35); this.thought = "\u304d\u305a\u3064\u304d\u75c5\u3067\u5f53\u3066\u3082\u306a\u304f\u5f77\u5fa8\u3063\u3066\u3044\u308b"; this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "?", "rgba(150,78,44,0.80)"); - this.world?.log?.(`${this.name}\u306f\u304d\u305a\u3064\u304d\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "fight"); + this.world?.log?.(`${this.name}\u306f\u304d\u305a\u3064\u304d\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "fight", { participants: [this] }); return true; } @@ -724,7 +692,7 @@ class Tarinai { this.thought = reason || "\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f"; this.stress = clamp(this.stress - 12, 0, 130); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)"); - this.world?.log?.(`${this.name}\u306e\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "fight"); + this.world?.log?.(`${this.name}\u306e\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "fight", { participants: [this] }); return true; } @@ -866,7 +834,7 @@ class Tarinai { if (this.zunchiDisease) { this.zunchiDiseaseSeverity = Math.max(0, (this.zunchiDiseaseSeverity || 1) - power * (this.energy > 70 ? 0.050 : 0.028)); if (this.zunchiDiseaseSeverity <= 0.04 && this.recoverZunchiDisease("\u6c34\u3067\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f")) { - this.world?.log?.(`${this.name}\u306f\u6c34\u3067\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f\u3002`, "accident"); + this.world?.log?.(`${this.name}\u306f\u6c34\u3067\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f\u3002`, "accident", { participants: [this] }); } } if (this.explosionDisease) this.recoverExplosionDisease("\u6c34\u3067\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f"); @@ -895,7 +863,7 @@ class Tarinai { if (Math.random() < dt * 0.000375) this.infectFightDisease(); } if (this.zunchiDisease && Math.random() < dt * 0.00055 * (this.energy > 66 ? 1.8 : 1.0)) { - if (this.recoverZunchiDisease("\u81ea\u7136\u306b\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f")) this.world?.log?.(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c\u81ea\u7136\u306b\u6cbb\u3063\u305f\u3002`, "accident"); + if (this.recoverZunchiDisease("\u81ea\u7136\u306b\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f")) this.world?.log?.(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c\u81ea\u7136\u306b\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] }); } if (this.fightDisease && Math.random() < dt * 0.00070 * (this.energy > 58 ? 1.4 : 1.0)) { this.recoverFightDisease("\u81ea\u7136\u306b\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f"); @@ -1143,7 +1111,7 @@ class Tarinai { this.aiTimer += dt; const urgentAi = this.fightTimer > 0.04 || this.intimidateTimer > 0.04 || this.intimidatedTimer > 0.04 || this.birthRitualTimer > 0.04 || this.defeatedTimer > 0.04 || this.eatTimer > 0.02 || this.hurtTimer > 0.02; const popPerf = this.world.performanceLevel ? this.world.performanceLevel() : 0; - const aiInterval = urgentAi ? (popPerf >= 2 ? 0.105 : 0.075) : (0.18 + stableUnit(this.id, "ai-interval") * 0.09 + popPerf * 0.055); + const aiInterval = urgentAi ? (popPerf >= 2 ? 0.105 : 0.075) : (0.18 + stableUnit(this.familyKey || this.id, "ai-interval") * 0.09 + popPerf * 0.055); if (this.aiTimer >= aiInterval) { const aiDt = Math.min(this.aiTimer, 0.48); this.aiTimer = 0; @@ -1270,9 +1238,9 @@ class Tarinai { if (this.world?.nestBoxEntryPoint) return this.world.nestBoxEntryPoint(bed); return { x: clamp(bed.x, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding), y: clamp(bed.y + (bed.r || 42) * 0.24, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding) }; } - const angle = stableUnit(this.id, `bed-angle-${bed.id || bed.seed || "bed"}`) * Math.PI * 2; + const angle = stableUnit(this.familyKey || this.id, `bed-angle-${bed.id || bed.seed || "bed"}`) * Math.PI * 2; const baseRing = bed.r * 0.52; - const ring = baseRing + stableUnit(this.id, `bed-ring-${bed.id || bed.seed || "bed"}`) * bed.r * 0.95; + const ring = baseRing + stableUnit(this.familyKey || this.id, `bed-ring-${bed.id || bed.seed || "bed"}`) * bed.r * 0.95; return { x: clamp(bed.x + Math.cos(angle) * ring, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding), y: clamp(bed.y + Math.sin(angle) * ring * 0.56 - this.radius * 0.10, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding), @@ -1659,7 +1627,7 @@ class Tarinai { const backY = this.y + this.radius * rand(0.25, 0.48); this.world.spawnZunchi(backX, backY); this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)"); - if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`); + if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] }); } interactWithItems(dt) { @@ -1698,7 +1666,7 @@ class Tarinai { this.makePoop(eating * (it.type === "sweet" ? 0.18 : 0.22)); if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; } if (this.world.time - this.lastLog > 9 && Math.random() < 0.014) { - this.world.log(`${this.name}\u306f${it.type === "sweet" ? "\u305a\u3093\u3060\u9905" : "\u98df\u3079\u7269"}\u3092\u3057\u3070\u3089\u304f\u5473\u308f\u3063\u305f\u3002`); + this.world.log(`${this.name}\u306f${it.type === "sweet" ? "\u305a\u3093\u3060\u9905" : "\u98df\u3079\u7269"}\u3092\u3057\u3070\u3089\u304f\u5473\u308f\u3063\u305f\u3002`, null, { participants: [this] }); this.lastLog = this.world.time; } break; @@ -1745,7 +1713,7 @@ class Tarinai { this.makePoop(eating * 0.20); if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; } if (this.world.time - this.lastLog > 9 && Math.random() < 0.02) { - this.world.log(`${this.name}\u306f${it.type === "love_mochi" ? "\u3078\u3053\u9905" : (it.type === "fight_mochi" ? "\u3051\u3093\u304b\u9905" : "\u306d\u3080\u308a\u85ac")}\u3092\u3057\u3070\u3089\u304f\u5473\u308f\u3063\u305f\u3002`, "food"); + this.world.log(`${this.name}\u306f${it.type === "love_mochi" ? "\u3078\u3053\u9905" : (it.type === "fight_mochi" ? "\u3051\u3093\u304b\u9905" : "\u306d\u3080\u308a\u85ac")}\u3092\u3057\u3070\u3089\u304f\u5473\u308f\u3063\u305f\u3002`, "food", { participants: [this] }); this.lastLog = this.world.time; } break; @@ -1906,10 +1874,10 @@ class Tarinai { } if (this.zunchiDisease && !o.zunchiDisease && (o.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && Math.random() < dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) { - if (o.infectZunchiDisease(this)) this.world.log(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c${o.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident"); + if (o.infectZunchiDisease(this)) this.world.log(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c${o.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [o, this] }); } if (o.zunchiDisease && !this.zunchiDisease && (this.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && Math.random() < dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) { - if (this.infectZunchiDisease(o)) this.world.log(`${o.name}\u306e\u305a\u3093\u3061\u75c5\u304c${this.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident"); + if (this.infectZunchiDisease(o)) this.world.log(`${o.name}\u306e\u305a\u3093\u3061\u75c5\u304c${this.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [o, this] }); } const pairBond = this.world.areCoParents?.(this, o); @@ -1934,10 +1902,10 @@ class Tarinai { if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD) { this.stress = clamp(this.stress - dt * 0.62 * this.trait.social, 0, 130); this.affection = clamp(this.affection + dt * 0.18, 0, 80); - if (this.world.relationNotice(this.id, o.id, "friend-calm", 55)) this.world.log(`${this.name}\u306f${o.name}\u3068\u3044\u308b\u3068\u5c11\u3057\u843d\u3061\u7740\u304f\u3002`, "relation"); + if (this.world.relationNotice(this.id, o.id, "friend-calm", 55)) this.world.log(`${this.name}\u306f${o.name}\u3068\u3044\u308b\u3068\u5c11\u3057\u843d\u3061\u7740\u304f\u3002`, "relation", { participants: [o, this] }); } if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD && this.world.relationNotice(this.id, o.id, "friend", 32)) { - this.world.log(`${this.name}\u306f\u3088\u304f${o.name}\u306e\u8fd1\u304f\u306b\u3044\u308b\u3002`, "relation"); + this.world.log(`${this.name}\u306f\u3088\u304f${o.name}\u306e\u8fd1\u304f\u306b\u3044\u308b\u3002`, "relation", { participants: [o, this] }); } if (this.type === "angry") this.stress += dt * 0.08; if (this.type === "teary" && o.type === "teary") this.stress -= dt * 0.12; @@ -2011,7 +1979,7 @@ class Tarinai { } if (this.loneliness < 12 && Math.random() < dt * 0.002) { - this.world.log(`${this.name}\u306f\u8ab0\u304b\u306e\u8fd1\u304f\u3067\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u5c11\u3057\u5fd8\u308c\u305f\u3002`); + this.world.log(`${this.name}\u306f\u8ab0\u304b\u306e\u8fd1\u304f\u3067\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u5c11\u3057\u5fd8\u308c\u305f\u3002`, null, { participants: [this] }); } } @@ -2242,8 +2210,10 @@ class Tarinai { if (spot) this.world.items.push(new Item("grass", spot.x, spot.y)); } this.world.markDead(this, finalReason); + this.releasedLiveToken = this.liveToken || 0; + this.world.releaseTarinaiLiveId?.(this); audio.death(); - this.world.log(`${this.name}\u306f\u9759\u304b\u306a\u75d5\u8de1\u3092\u6b8b\u3057\u305f\u3002\u6b7b\u56e0: ${finalReason}`, "death"); + this.world.log(`${this.name}\u306f\u9759\u304b\u306a\u75d5\u8de1\u3092\u6b8b\u3057\u305f\u3002\u6b7b\u56e0: ${finalReason}`, "death", { participants: [this] }); if (this.world.selected === this) this.world.selected = null; } diff --git a/js/ui.js b/js/ui.js index c172696..993a464 100644 --- a/js/ui.js +++ b/js/ui.js @@ -24,6 +24,7 @@ const ui = { colonyChart: document.getElementById("colonyChart"), colonyChartLegend: document.getElementById("colonyChartLegend"), archiveContent: document.getElementById("archiveContent"), + archiveUpdateToggleBtn: document.getElementById("archiveUpdateToggleBtn"), archiveResetBtn: document.getElementById("archiveResetBtn"), archiveResetDialog: document.getElementById("archiveResetDialog"), archiveResetCancelBtn: document.getElementById("archiveResetCancelBtn"), @@ -39,6 +40,8 @@ const uiCache = { stats: {}, selectedSnapshot: "", selectedEmpty: false, + selectedCollapsedCategories: new Set(), + archiveUpdateEnabled: false, toolButtons: [], archiveVersion: "", archiveRows: [], @@ -124,7 +127,7 @@ function logKindLabel(kind) { } function tarinaiSpritePathForId(id) { - const live = world?.tarinai?.find(t => t.id === id && !t.dead); + const live = world?.tarinai?.find(t => (t.id === id || t.familyKey === id) && !t.dead); const sid = live?.spriteId ? live.spriteId() : live?.type; const fam = world?.family?.[id]; const fallback = fam?.type; @@ -163,7 +166,7 @@ function relationListHtml(t, kind = "friend") { } function focusTarinaiById(id) { - const t = world?.tarinai?.find(o => o && !o.dead && o.id === id); + const t = world?.tarinai?.find(o => o && !o.dead && (o.id === id || o.familyKey === id)); if (!t) { showToast("\u305d\u306e\u500b\u4f53\u306f\u3082\u3046\u9078\u629e\u3067\u304d\u307e\u305b\u3093\u3002"); return false; } world.selected = t; t.focusPulseTimer = Math.max(t.focusPulseTimer || 0, 1.25); @@ -230,6 +233,15 @@ function personalityRowsHtml(personality = {}) { return PERSONALITY_KEYS.map(key => `${escapeHtml(PERSONALITY_LABELS[key]?.label || key)}: ${personalityValueText(p[key])}`).join(""); } +function selectedDataCategoryHtml(id, label, bodyHtml) { + if (!uiCache.selectedCollapsedCategories) uiCache.selectedCollapsedCategories = new Set(); + const collapsed = uiCache.selectedCollapsedCategories.has(id); + return `
+ +
${bodyHtml}
+
`; +} + function renderSelected() { const t = world.selected; if (!t || t.dead || !world.tarinai.includes(t)) { @@ -294,31 +306,45 @@ function renderSelected() { const recordText = quoteNameInRecordText(r.text || "", t.name); return `
  • ${escapeHtml(timeText)}${escapeHtml(recordText)}
  • `; }).join("") || `
  • -\u8a18\u9332\u306a\u3057
  • `; - ui.selectedInfo.innerHTML = ` -
    - -
    - ${effectBadges.length ? `
    ${effectBadges.join("")}
    ` : ""} + const basicHtml = `
    \u4e16\u4ee3${escapeHtml(generationDisplay || "\u4e0d\u660e")}
    \u72b6\u614b${escapeHtml(stateLabel(t.state))}
    \u7406\u7531${escapeHtml(reasonLabel(t))}
    - ${personalityHtml}
    \u5bfe\u8c61${escapeHtml(targetLabel(t.target))}
    \u5e74\u9f62${fmt(t.age)} / ${fmt(t.lifeSpan)}
    + `; + const healthHtml = `
    \u7a7a\u8179${fmt(t.hunger)}
    \u5bc2\u3057\u3055${fmt(t.loneliness)}
    \u4f53\u529b${fmt(t.energy)}
    \u30b9\u30c8\u30ec\u30b9${fmt(t.stress)}
    \u305a\u3093\u3061\u6c5a\u308c${fmt(t.zunchiStain || 0)}
    \u6c17\u5206${fmt(t.mood)}
    +
    \u305f\u308a\u306a\u3044${fmt(t.lack)}
    + `; + const relationHtml = `
    \u53cb\u9054${relationListHtml(t, "friend")}
    \u6575\u5bfe${relationListHtml(t, "enemy")}
    \u55a7\u5629\u8a18\u61b6${escapeHtml(fightRecord)}
    \u7d2f\u8a08\u6557\u5317${t.totalFightLosses || 0}
    + `; + const familyHtml = `
    \u89aa${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "\u4e0d\u660e")}
    \u5b50${t.children?.length || 0}
    -
    \u305f\u308a\u306a\u3044${fmt(t.lack)}
    -

    \u500b\u4f53\u5225\u306e\u8a18\u9332

      ${recordHtml}
    + `; + ui.selectedInfo.innerHTML = ` +
    + +
    + ${effectBadges.length ? `
    ${effectBadges.join("")}
    ` : ""} +
    + ${selectedDataCategoryHtml("basic", "\u57fa\u672c", basicHtml)} + ${selectedDataCategoryHtml("health", "\u4f53\u8abf", healthHtml)} + ${selectedDataCategoryHtml("personality", "\u6027\u683c", personalityHtml)} + ${selectedDataCategoryHtml("relation", "\u95a2\u4fc2", relationHtml)} + ${selectedDataCategoryHtml("family", "\u5bb6\u65cf", familyHtml)} + ${selectedDataCategoryHtml("records", "\u8a18\u9332", `

    \u500b\u4f53\u5225\u306e\u8a18\u9332

      ${recordHtml}
    `)} +
    `; } @@ -371,7 +397,7 @@ function reasonLabel(t) { function targetLabel(target) { if (!target) return "\u306a\u3057"; if (target.name) return target.name; - if (target.type) return { sweet: "\u305a\u3093\u3060\u9905", love_mochi: "\u3078\u3053\u9905", fight_mochi: "\u3051\u3093\u304b\u9905", sleep_drug: "\u306d\u3080\u308a\u85ac", food: "\u98df\u4e8b", grass: "\u8349", water: "\u6c34", water_bowl: "\u6c34\u306e\u76bf", bed: "\u5e72\u8349\u5bdd\u5e8a", nest_box: "\u5de3\u7bb1", ball: "\u30dc\u30fc\u30eb", stone: "\u77f3", zunchi: "\u305a\u3093\u3061", system: "\u305a\u3093\u3061\u75c5", zunchi_sick: "\u305a\u3093\u3061\u75c5", fight_sick: "\u304d\u305a\u3064\u304d\u75c5", trace: "\u8db3\u8de1", splat: "\u3057\u3076\u304d" }[target.type] || target.type; + if (target.type) return { sweet: "\u305a\u3093\u3060\u9905", love_mochi: "\u3078\u3053\u9905", fight_mochi: "\u3051\u3093\u304b\u9905", sleep_drug: "\u306d\u3080\u308a\u85ac", food: "\u98df\u4e8b", grass: "\u8349", water: "\u6c34", water_bowl: "\u6c34\u306e\u76bf", bed: "\u5e72\u8349\u5bdd\u5e8a", nest_box: "\u5de3\u7bb1", ball: "\u30dc\u30fc\u30eb", stone: "\u77f3", genkotsu: "\u3052\u3093\u3053\u3064", zunchi: "\u305a\u3093\u3061", system: "\u305a\u3093\u3061\u75c5", zunchi_sick: "\u305a\u3093\u3061\u75c5", fight_sick: "\u304d\u305a\u3064\u304d\u75c5", trace: "\u8db3\u8de1", splat: "\u3057\u3076\u304d" }[target.type] || target.type; if (Number.isFinite(target.x) && Number.isFinite(target.y)) return "\u30ab\u30fc\u30bd\u30eb"; return "\u4e0d\u660e"; } @@ -611,6 +637,36 @@ function syncTopButtons() { ui.pauseBtn.textContent = world.paused ? "\u518d\u958b" : "\u4e00\u6642\u505c\u6b62"; ui.speedBtn.textContent = `\u901f\u5ea6 x${world.speed}`; if (ui.soundBtn) ui.soundBtn.textContent = audio.enabled ? "\u97f3 ON" : "\u97f3 OFF"; + syncArchiveUpdateButton(); +} + +function archiveAutoUpdateEnabled() { + return Boolean(uiCache.archiveUpdateEnabled); +} + +function syncArchiveUpdateButton() { + const btn = ui.archiveUpdateToggleBtn; + if (!btn) return; + const on = archiveAutoUpdateEnabled(); + btn.textContent = on ? "\u66f4\u65b0 ON" : "\u66f4\u65b0 OFF"; + btn.classList.toggle("active", on); + btn.setAttribute("aria-pressed", on ? "true" : "false"); +} + +function setArchiveUpdateEnabled(on) { + uiCache.archiveUpdateEnabled = Boolean(on); + syncArchiveUpdateButton(); + if (uiCache.archiveUpdateEnabled) { + uiCache.archiveVersion = ""; + uiCache.archiveFamilyVersion = null; + world.familyTreeDirty = true; + scheduleArchiveWindowRender?.(); + showToast("\u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u3092ON\u306b\u3057\u307e\u3057\u305f\u3002"); + } else { + if (uiCache.archiveRenderTimer) { clearTimeout(uiCache.archiveRenderTimer); uiCache.archiveRenderTimer = 0; } + uiCache.archiveScheduled = false; + showToast("\u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u3092OFF\u306b\u3057\u307e\u3057\u305f\u3002"); + } } const SCALABLE_TOOLS = new Set(scalableToolIds()); @@ -692,7 +748,7 @@ function bindUI() { const node = e.target.closest("[data-family-tarinai-id]"); if (!node) return; const id = node.getAttribute("data-family-tarinai-id"); - const t = world.tarinai.find(x => x.id === id && !x.dead); + const t = world.tarinai.find(x => (x.id === id || x.familyKey === id) && !x.dead); if (!t) { showToast("\u305d\u306e\u500b\u4f53\u306f\u3082\u3046\u9078\u629e\u3067\u304d\u307e\u305b\u3093\u3002"); return; } world.selected = t; t.focusPulseTimer = Math.max(t.focusPulseTimer || 0, 1.15); @@ -708,12 +764,25 @@ function bindUI() { if (!uiCache.archiveAutoRefreshTimer && typeof window !== "undefined") { uiCache.archiveAutoRefreshTimer = window.setInterval(() => { - if (!ui.archiveContent) return; + if (!ui.archiveContent || !archiveAutoUpdateEnabled()) return; scheduleArchiveWindowRender(); }, 5000); } ui.selectedInfo?.addEventListener("click", (e) => { + const categoryToggle = e.target.closest(".selected-category-toggle"); + if (categoryToggle) { + const category = categoryToggle.closest(".selected-category"); + const id = category?.dataset?.selectedCategory || ""; + if (!category || !id) return; + if (!uiCache.selectedCollapsedCategories) uiCache.selectedCollapsedCategories = new Set(); + category.classList.toggle("collapsed"); + const collapsed = category.classList.contains("collapsed"); + if (collapsed) uiCache.selectedCollapsedCategories.add(id); + else uiCache.selectedCollapsedCategories.delete(id); + categoryToggle.setAttribute("aria-expanded", collapsed ? "false" : "true"); + return; + } const focusBtn = e.target.closest("[data-focus-tarinai-id]"); if (focusBtn) { e.preventDefault(); @@ -750,7 +819,7 @@ function bindUI() { ui.pauseBtn?.addEventListener("click", () => { world.paused = !world.paused; const archiveStale = uiCache.archiveFamilyVersion !== (world.familyVersion || 0); - if (world.paused && (uiCache.archivePendingWhileRunning || archiveStale)) { + if (archiveAutoUpdateEnabled() && world.paused && (uiCache.archivePendingWhileRunning || archiveStale)) { uiCache.archivePendingWhileRunning = false; scheduleArchiveWindowRender(); } @@ -787,6 +856,8 @@ function bindUI() { ui.ecologyDialog?.addEventListener("click", (e) => { if (e.target === ui.ecologyDialog) closeEcologyDialog(); }); + ui.archiveUpdateToggleBtn?.addEventListener("click", () => setArchiveUpdateEnabled(!archiveAutoUpdateEnabled())); + syncArchiveUpdateButton(); ui.archiveResetBtn?.addEventListener("click", openArchiveResetDialog); ui.archiveResetCancelBtn?.addEventListener("click", closeArchiveResetDialog); ui.archiveResetDialog?.addEventListener("click", (e) => { @@ -794,7 +865,7 @@ function bindUI() { }); ui.archiveResetConfirmBtn?.addEventListener("click", () => { world.resetFamilyTree?.(); - uiCache.archiveVersion = ""; + window.resetArchiveRenderState?.(); closeArchiveResetDialog(); renderArchive(); renderSelected(); diff --git a/js/ui_family.js b/js/ui_family.js index a6bff5d..b4a6d6a 100644 --- a/js/ui_family.js +++ b/js/ui_family.js @@ -45,8 +45,8 @@ function lineageMutualParentIds(n, family = world.family || {}, idSet = null) { if (!n?.id) return parents; const relationIndex = lineageRelationIndex(family); const candidates = new Set((n.parents || []).filter(Boolean)); - // Older saves may contain only one side of the parent/child edge. For - // rendering, recover the missing side from a cached relation index. + // Runtime updates can briefly touch one side first; recover the reciprocal + // edge from the relation index so the archive does not flicker. for (const parentId of relationIndex.parentsByChild.get(n.id) || []) candidates.add(parentId); for (const id of candidates) { if (!id || id === n.id || seen.has(id)) continue; @@ -69,8 +69,7 @@ function lineageMutualChildIds(n, family = world.family || {}, idSet = null) { if (!n?.id) return children; const relationIndex = lineageRelationIndex(family); const candidates = new Set((n.children || []).filter(Boolean)); - // Same recovery as above, in the opposite direction: child.parents is enough - // to draw a real parent/child edge even if parent.children was not persisted. + // Same recovery as above, in the opposite direction. for (const childId of relationIndex.childrenByParent.get(n.id) || []) candidates.add(childId); for (const id of candidates) { if (!id || id === n.id || seen.has(id)) continue; @@ -348,7 +347,7 @@ function lineageNodeSort(a, b) { } function lineageTreeNodeHtml(n, x, y) { - const entity = n?.id ? world.tarinai.find(t => t.id === n.id) : null; + const entity = n?.id ? world.tarinai.find(t => (t.familyKey || t.id) === n.id) : null; const live = entity && !entity.dead ? entity : null; const img = n ? tarinaiSpritePathForId(n.id) : SPRITES[0].path; const name = escapeHtml(n?.name || "\u4e0d\u660e"); @@ -1121,7 +1120,7 @@ function lineageSetArchiveStaleStatus(show) { return; } if (existing) return; - ui.archiveContent.insertAdjacentHTML("afterbegin", `
    \u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u306f\u4e00\u6642\u505c\u6b62\u4e2d\u3067\u3059\u3002\u4e00\u6642\u505c\u6b62\u3059\u308b\u3068\u53cd\u6620\u3055\u308c\u307e\u3059\u3002
    `); + ui.archiveContent.insertAdjacentHTML("afterbegin", `
    \u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u306fOFF\u3067\u3059\u3002\u300c\u66f4\u65b0\u300d\u3092ON\u306b\u3059\u308b\u3068\u53cd\u6620\u3055\u308c\u307e\u3059\u3002
    `); } function lineageAssignPartnerAttachOffsets(linkGroups) { @@ -1418,6 +1417,15 @@ function lineageFamilyHtml(component, index, family, signature = "") { function renderArchive() { if (!ui.archiveContent) return; + if (typeof archiveAutoUpdateEnabled === "function" && !archiveAutoUpdateEnabled()) { + uiCache.archiveScheduled = false; + if (!uiCache.archiveVersion) { + ui.archiveContent.innerHTML = `
    \u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u306fOFF\u3067\u3059\u3002\u300c\u66f4\u65b0\u300d\u3092ON\u306b\u3059\u308b\u3068\u63cf\u753b\u3057\u307e\u3059\u3002
    `; + } else if (world.familyTreeDirty) { + lineageSetArchiveStaleStatus(true); + } + return; + } if (uiCache.deferArchiveUntil && performance.now() < uiCache.deferArchiveUntil) return; uiCache.deferArchiveUntil = 0; uiCache.archiveScheduled = false; @@ -1553,6 +1561,27 @@ function renderArchive() { ); } +function resetArchiveRenderState() { + if (uiCache.archiveRenderTimer) { + clearTimeout(uiCache.archiveRenderTimer); + uiCache.archiveRenderTimer = 0; + } + uiCache.archiveRenderToken = ""; + uiCache.archiveRenderRunning = false; + uiCache.archiveScheduled = false; + uiCache.archivePendingOffscreen = false; + uiCache.archivePendingWhileRunning = false; + uiCache.archivePendingDirtyAfterRun = false; + uiCache.archiveLastHiddenCheckAt = -Infinity; + uiCache.archiveLastScheduleFamilyVersion = null; + uiCache.archiveVersion = ""; + uiCache.archiveFamilyVersion = null; + uiCache.archiveRelationSignature = ""; + uiCache.archiveRows = []; + uiCache.lastArchiveWindow = ""; + uiCache.archiveHtmlCache?.clear?.(); +} + function validateFamilyTree() { const issues = world.validateFamily ? [...world.validateFamily()] : []; const rendered = new Map(); @@ -1595,6 +1624,11 @@ function renderArchiveWindow() { } function scheduleArchiveWindowRender() { + if (typeof archiveAutoUpdateEnabled === "function" && !archiveAutoUpdateEnabled()) { + uiCache.archiveScheduled = false; + if (world.familyTreeDirty) lineageSetArchiveStaleStatus(true); + return; + } const familyDirty = Boolean(world.familyTreeDirty); if (uiCache.archiveVersion && uiCache.archiveFamilyVersion === (world.familyVersion || 0) && !familyDirty) return; if (uiCache.archiveRenderRunning) { @@ -1617,6 +1651,7 @@ function scheduleArchiveWindowRender() { } function schedulePendingArchiveRender() { + if (typeof archiveAutoUpdateEnabled === "function" && !archiveAutoUpdateEnabled()) return; if (!uiCache.archivePendingOffscreen) return; if (!lineageArchiveNearViewport()) return; uiCache.archivePendingOffscreen = false; @@ -1625,3 +1660,4 @@ function schedulePendingArchiveRender() { window.validateFamilyTree = validateFamilyTree; window.familyTreeDiagnostics = () => ({ ...(uiCache.archiveDiagnostics || {}) }); +window.resetArchiveRenderState = resetArchiveRenderState; diff --git a/js/world.js b/js/world.js index 44c933a..ec04b10 100644 --- a/js/world.js +++ b/js/world.js @@ -25,6 +25,10 @@ class World { this.drawListDirty = true; this.selected = null; this.deadCount = 0; + this.liveIdNext = 1; + this.liveIdFree = []; + this.liveIdSerial = 0; + this.liveTarinai = new Map(); this.lastBirthAt = -999; this.maxGeneration = 1; this.family = {}; @@ -42,7 +46,8 @@ class World { this.cameraY = 0; this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax); this.spatial = new SpatialGrid(96); - this.spatialScratch = []; + this.spatialItemScratch = []; + this.spatialTarinaiScratch = []; this.drawList = []; this.itemCounts = {}; this.effectCounts = {}; @@ -330,6 +335,10 @@ class World { this.drawListDirty = true; this.selected = null; this.deadCount = 0; + this.liveIdNext = 1; + this.liveIdFree = []; + this.liveIdSerial = 0; + this.liveTarinai = new Map(); this.lastBirthAt = -999; this.maxGeneration = 1; this.family = {}; @@ -367,9 +376,51 @@ class World { } + tarinaiFamilyKey(t) { + return t?.familyKey || t?.archiveKey || t?.id || ""; + } + + assignTarinaiLiveId(t) { + if (!t) return ""; + if (!this.liveTarinai) this.liveTarinai = new Map(); + // Live IDs are never recycled. Relationships and transient targets are keyed by + // live ID, so reuse can make dead friends/enemies leak onto new individuals. + this.liveIdFree = []; + if (!Number.isFinite(this.liveIdNext)) this.liveIdNext = 1; + if (!Number.isFinite(this.liveIdSerial)) this.liveIdSerial = 0; + const id = `t${this.liveIdNext++}`; + t.id = id; + t.liveToken = ++this.liveIdSerial; + this.liveTarinai.set(id, { token: t.liveToken, target: t }); + return id; + } + + releaseTarinaiLiveId(t) { + if (!t?.id || !this.liveTarinai) return false; + const current = this.liveTarinai.get(t.id); + if (!current || current.target !== t || current.token !== t.liveToken) return false; + const id = t.id; + this.liveTarinai.delete(id); + // Do not recycle live IDs. Relationships are keyed by live ID; recycling can + // make a dead friend's/enemy's relationship appear on a newly spawned individual. + t.releasedLiveId = id; + t.id = ""; + t.liveToken = 0; + return true; + } + + liveTarinaiById(id, token = null) { + if (!id || !this.liveTarinai) return null; + const entry = this.liveTarinai.get(id); + if (!entry || (token !== null && entry.token !== token)) return null; + const t = entry.target; + return t && !t.dead ? t : null; + } + recordFamily(t) { if (!this.family) this.family = {}; - if (!t?.id) return; + const familyKey = this.tarinaiFamilyKey(t); + if (!familyKey) return; const uniqueIds = (list) => Array.from(new Set((list || []).filter(Boolean))); const sameList = (a, b) => { const aa = uniqueIds(a); @@ -380,9 +431,9 @@ class World { const nextParents = uniqueIds(t.parents || []); const nextChildren = uniqueIds(t.children || []); const nextPersonalityTags = getPersonalityTraitTags(t); - if (!this.family[t.id]) { - this.family[t.id] = { - id: t.id, + if (!this.family[familyKey]) { + this.family[familyKey] = { + id: familyKey, name: t.name, type: t.type, personalityTags: nextPersonalityTags, @@ -397,7 +448,7 @@ class World { }; changed = true; } else { - const entry = this.family[t.id]; + const entry = this.family[familyKey]; const nextAlive = !t.dead; if (entry.name !== t.name) changed = true; if (entry.type !== t.type) changed = true; @@ -436,12 +487,13 @@ class World { if (!this.family) this.family = {}; if (this.familyCleanVersion === (this.familyVersion || 0)) return false; const family = this.family; - const liveById = new Map((this.tarinai || []).map(t => [t.id, t])); + const liveById = new Map((this.tarinai || []).map(t => [this.tarinaiFamilyKey(t), t]).filter(([id]) => id)); let changed = false; const uniqueIds = (list, allow) => Array.from(new Set((list || []).filter(id => id && (!allow || allow.has(id))))); const sameList = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]); for (const t of this.tarinai || []) { - if (!this.family[t.id] && !(t.hasPaired || (t.parents || []).length || (t.children || []).length)) continue; + const familyKey = this.tarinaiFamilyKey(t); + if (!familyKey || (!this.family[familyKey] && !(t.hasPaired || (t.parents || []).length || (t.children || []).length))) continue; const before = this.familyVersion || 0; this.recordFamily(t); changed = changed || before !== (this.familyVersion || 0); @@ -571,14 +623,40 @@ class World { return issues; } + clearLiveReferencesTo(dead) { + const deadId = dead?.id || dead?.releasedLiveId || ""; + if (!deadId) return false; + let changed = false; + for (const other of this.tarinai || []) { + 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.state = "idle"; + changed = true; + } + if (other.panicTarget === dead || other.panicTarget?.id === deadId) { other.panicTarget = null; changed = true; } + if (other.fightTargetId === deadId) { other.fightTargetId = null; changed = true; } + if (Array.isArray(other.fightTargetIds) && other.fightTargetIds.includes(deadId)) { + other.fightTargetIds = other.fightTargetIds.filter(id => id !== deadId); + changed = true; + } + for (const key of ["defeatedById", "fightWinnerId", "intimidateTargetId", "birthPartnerId"]) { + if (other[key] === deadId) { other[key] = null; changed = true; } + } + } + return changed; + } + markDead(t, reason) { + this.clearLiveReferencesTo(t); if (!this.family) this.family = {}; const finalReason = t?.normalizeDeathReason ? t.normalizeDeathReason(reason) : reason; - if (this.family[t.id]) { - if (this.family[t.id].alive !== false || this.family[t.id].deathReason !== finalReason) { - this.family[t.id].alive = false; - this.family[t.id].deathReason = finalReason; - delete this.family[t.id].personality; + const familyKey = this.tarinaiFamilyKey(t); + if (familyKey && this.family[familyKey]) { + if (this.family[familyKey].alive !== false || this.family[familyKey].deathReason !== finalReason) { + this.family[familyKey].alive = false; + this.family[familyKey].deathReason = finalReason; + delete this.family[familyKey].personality; this.familyVersion = (this.familyVersion || 0) + 1; this.markFamilyTreeDirty("death"); } @@ -632,6 +710,7 @@ class World { addTarinai(opts = {}) { const t = new Tarinai(this, opts); + this.assignTarinaiLiveId(t); this.tarinai.push(t); this.drawListDirty = true; if (t.hasPaired || (t.parents || []).length || (t.children || []).length) this.recordFamily(t); @@ -642,6 +721,7 @@ class World { startBirthRitual(a, b) { if (!a || !b || a.dead || b.dead) return false; if (a.isZunchiSlave || b.isZunchiSlave) return false; + if (this.areParentChild(a, b)) return false; if (a.sleepDisease || b.sleepDisease || a.fightDisease || b.fightDisease) return false; if (a.birthRitualTimer > 0.04 || b.birthRitualTimer > 0.04) return false; const aJoined = a.familyJoined ? a.familyJoined() : Boolean((a.parents || []).length || (a.children || []).length); @@ -696,25 +776,28 @@ class World { if (child) { child.postBirthPeaceTimer = 18; children.push(child); } } if (children.length > 0) this.lastBirthAt = this.time; - if (children.length > 1) this.log(`${a.name}\u3068${b.name}\u306e\u5b50\u304c${children.length}\u5339\u751f\u307e\u308c\u305f\u3002`, "birth"); + if (children.length > 1) this.log(`${a.name}\u3068${b.name}\u306e\u5b50\u304c${children.length}\u5339\u751f\u307e\u308c\u305f\u3002`, "birth", { participants: [a, b, ...children] }); return children[0] || null; } areParentChild(a, b) { if (!a || !b) return false; - const aId = a.id || a; - const bId = b.id || b; + const aId = this.tarinaiFamilyKey(a) || a; + const bId = this.tarinaiFamilyKey(b) || b; if (!aId || !bId) return false; return Boolean((a.parents || []).includes(bId) || (a.children || []).includes(bId) || (b.parents || []).includes(aId) || (b.children || []).includes(aId)); } areCoParents(a, b) { if (!a || !b) return false; - const aId = a.id || a; - const bId = b.id || b; + const aLiveId = a.id || a; + const bLiveId = b.id || b; + if (!aLiveId || !bLiveId || aLiveId === bLiveId) return false; + if (a.birthPartnerId && a.birthPartnerId === bLiveId) return true; + if (b.birthPartnerId && b.birthPartnerId === aLiveId) return true; + const aId = this.tarinaiFamilyKey(a) || aLiveId; + const bId = this.tarinaiFamilyKey(b) || bLiveId; if (!aId || !bId || aId === bId) return false; - if (a.birthPartnerId && a.birthPartnerId === bId) return true; - if (b.birthPartnerId && b.birthPartnerId === aId) return true; const aChildren = new Set((a.children || []).filter(Boolean)); if (!aChildren.size) return false; for (const id of (b.children || [])) { @@ -726,10 +809,12 @@ class World { canStartFight(t) { if (!t || t.dead) return false; if (t.sleepDisease) return false; - // Disease fight rule: explosion disease and zunchi disease can lash out; + // Disease fight rule: explosion, zunchi, and fight disease can lash out; // sleep disease cannot fight or be challenged, and other diseases cannot start fights. - if (t.explosionDisease || t.zunchiDisease) return true; - if (t.fightDisease) return false; + if (t.explosionDisease || t.zunchiDisease || t.fightDisease) return true; + const flag = (value) => typeof value === "function" ? Boolean(value.call(t)) : Boolean(value); + const hasOtherDisease = flag(t.disease) || flag(t.sick) || flag(t.infection) || flag(t.hasDisease); + if (hasOtherDisease) return false; return true; } @@ -761,7 +846,7 @@ class World { need: Math.random() < 0.6 ? pick([a.need, b.need]) : pick(NEEDS), generation: Math.max(a.generation, b.generation) + 1, lifeSpan: rand(1500, 2400), - parents: [a.id, b.id], + parents: [this.tarinaiFamilyKey(a), this.tarinaiFamilyKey(b)], parentNames: [a.name, b.name], birthTime: this.time, }); @@ -774,17 +859,20 @@ class World { b.adjustRelation?.(a, 0.65, -0.20, "co_parent"); a.postBirthPeaceTimer = Math.max(a.postBirthPeaceTimer || 0, 28); b.postBirthPeaceTimer = Math.max(b.postBirthPeaceTimer || 0, 28); - a.children.push(child.id); - b.children.push(child.id); + const childKey = this.tarinaiFamilyKey(child); + const aKey = this.tarinaiFamilyKey(a); + const bKey = this.tarinaiFamilyKey(b); + a.children.push(childKey); + b.children.push(childKey); this.recordFamily(a); this.recordFamily(b); this.recordFamily(child); - if (this.family[a.id] && !this.family[a.id].children.includes(child.id)) this.family[a.id].children.push(child.id); - if (this.family[b.id] && !this.family[b.id].children.includes(child.id)) this.family[b.id].children.push(child.id); + if (this.family[aKey] && !this.family[aKey].children.includes(childKey)) this.family[aKey].children.push(childKey); + if (this.family[bKey] && !this.family[bKey].children.includes(childKey)) this.family[bKey].children.push(childKey); this.familyCleanVersion = null; audio.birth(); this.lastBirthAt = this.time; - if (total <= 1) this.log(`${child.name}\u304c${a.name}\u3068${b.name}\u306e\u5b50\u3068\u3057\u3066\u751f\u307e\u308c\u305f\u3002`, "birth"); + if (total <= 1) this.log(`${child.name}\u304c${a.name}\u3068${b.name}\u306e\u5b50\u3068\u3057\u3066\u751f\u307e\u308c\u305f\u3002`, "birth", { participants: [child, a, b] }); return child; } @@ -901,7 +989,7 @@ class World { actor.adjustRelation(target, -0.08, 0.06, "intimidate"); target.adjustRelation(actor, -0.22, 0.85 * target.personalityProfile().fear, "intimidate"); if (this.time - Math.max(actor.lastLog, target.lastLog) > 5.5) { - this.log(`${actor.name}\u306f${target.name}\u3092\u5a01\u5687\u3057\u3066\u8ffd\u3044\u6255\u3063\u305f\u3002`, "fight"); + 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; } return true; @@ -953,7 +1041,7 @@ class World { })); audio.fight(); if (this.time - Math.max(a.lastLog, b.lastLog) > 6) { - this.log(`${a.name}\u3068${b.name}\u304c\u5c0f\u3055\u306a\u55a7\u5629\u3092\u3057\u305f\u3002`, "fight"); + 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; } } @@ -967,7 +1055,10 @@ class World { const p = clamp(1 - d / Math.max(1, radius), 0, 1); if (p <= 0 && d > radius) continue; const damage = typeof damageFn === "function" ? damageFn(p, ant, d) : Number(damageFn || 0); - if (damage > 0) ant.hp = Math.max(0, (ant.hp ?? ant.maxHp ?? ANT_WORKER_HP ?? 32) - damage); + if (damage > 0) { + ant.hp = Math.max(0, (ant.hp ?? ant.maxHp ?? ANT_WORKER_HP ?? 32) - damage); + ant.hpBarTimer = Math.max(ant.hpBarTimer || 0, 1.25); + } if (opts.push) { let nx = (ant.x - x) / d; let ny = (ant.y - y) / d; @@ -1124,7 +1215,7 @@ class World { } t.die("\u7206\u767a\u75c5"); audio.explode(); - this.log(`${t.name}\u306f\u7206\u767a\u75c5\u3067\u7206\u767a\u3057\u305f\u3002`, "accident"); + this.log(`${t.name}\u306f\u7206\u767a\u75c5\u3067\u7206\u767a\u3057\u305f\u3002`, "accident", { participants: [t] }); } applyWaterHose(x, y, dx = 0, dy = 0, dt = 0.08) { @@ -1377,47 +1468,63 @@ class World { if ((loser.totalFightLosses || 0) >= 5 && loser.becomeZunchiSlave?.(winner)) { const oldName = loser.formerName || loser.name; this.spawnBubble(loser.x, loser.y - loser.radius * 1.20, "\u3076\u308a\u3085\u2026", "rgba(74,91,50,0.78)"); - this.log(`${oldName}\u306f5\u56de\u55a7\u5629\u306b\u8ca0\u3051\u3001\u305a\u3093\u3061\u3069\u308c\u3044\u306b\u306a\u3063\u305f\u3002`, "fight"); + this.log(`${oldName}\u306f5\u56de\u55a7\u5629\u306b\u8ca0\u3051\u3001\u305a\u3093\u3061\u3069\u308c\u3044\u306b\u306a\u3063\u305f\u3002`, "fight", { participants: [loser] }); } loser.fearTimer = Math.max(loser.fearTimer, 1.2 * loser.personalityProfile().fear); this.spawnBubble(loser.x, loser.y - loser.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); winner.affection = clamp(winner.affection + 0.8, 0, 80); winner.stress = clamp(winner.stress - rand(9, 18), 0, 130); if (this.relationNotice(winner.id, loser.id, "fight-result", 8)) { - this.log(`${loser.name}\u306f${winner.name}\u306b\u8ca0\u3051\u3001\u305d\u306e\u3053\u3068\u3092\u899a\u3048\u305f\u3002`, "fight"); + this.log(`${loser.name}\u306f${winner.name}\u306b\u8ca0\u3051\u3001\u305d\u306e\u3053\u3068\u3092\u899a\u3048\u305f\u3002`, "fight", { participants: [loser, winner] }); } } itemDropImpact(it) { if (!it || it.dead || it.type === "water") return; const isStone = it.type === "stone"; - const radius = isStone ? 64 : Math.max(34, (it.r || 14) * 1.75); - const damage = isStone ? rand(20, 34) : rand(3, 7); - const push = isStone ? rand(145, 210) : rand(54, 92); + const isGenkotsu = it.type === "genkotsu"; + const radius = isGenkotsu ? Math.max(118, (it.r || 72) * 1.72) : (isStone ? 64 : Math.max(34, (it.r || 14) * 1.75)); + const damage = isGenkotsu ? rand(34, 54) : (isStone ? rand(20, 34) : rand(3, 7)); + const push = isGenkotsu ? rand(330, 470) : (isStone ? rand(145, 210) : rand(54, 92)); + const reason = isGenkotsu ? "\u3052\u3093\u3053\u3064\u304c\u843d\u3061\u305f" : (isStone ? "\u77f3\u304c\u843d\u3061\u305f" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f"); + if (isGenkotsu) it.impactFlash = 1; let hit = 0; - for (const t of this.nearbyTarinai(it.x, it.y, radius + 48)) { + for (const t of this.nearbyTarinai(it.x, it.y, radius + 54)) { if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) continue; const d = Math.max(1, distXY(t.x, t.y, it.x, it.y)); const p = clamp(1 - d / (radius + t.radius), 0, 1); if (p <= 0) continue; - const nx = (t.x - it.x) / d; - const ny = (t.y - it.y) / d; + let nx = (t.x - it.x) / d; + let ny = (t.y - it.y) / d; + if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) { + const a = rand(0, Math.PI * 2); + nx = Math.cos(a); + ny = Math.sin(a); + } t.vx += nx * push * (0.35 + p); - t.vy += ny * push * (0.35 + p) - (isStone ? 42 : 18) * p; - t.damage(damage * (0.35 + p * 0.65), isStone ? "\u77f3\u304c\u843d\u3061\u305f" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f"); - t.hurtTimer = Math.max(t.hurtTimer, isStone ? 2.7 : 1.25); - t.fallTimer = Math.max(t.fallTimer, isStone ? 0.62 : 0.26); + t.vy += ny * push * (0.35 + p) - (isGenkotsu ? 92 : (isStone ? 42 : 18)) * p; + t.damage(damage * (0.35 + p * 0.65), reason); + 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)); t.fallMax = Math.max(t.fallMax || t.fallTimer, t.fallTimer); t.fallDir = nx >= 0 ? 1 : -1; hit += 1; } - const antHit = this.damageAntsInRadius(it.x, it.y, radius + 48, p => damage * (0.35 + p * 0.65), isStone ? "\u77f3\u304c\u843d\u3061\u305f" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f", { + const antHit = this.damageAntsInRadius(it.x, it.y, radius + 54, p => damage * (0.35 + p * 0.65), reason, { push: p => push * (0.35 + p), }); - this.effects.push(new Effect("ring", it.x, it.y, { size: isStone ? 32 : 18, life: 0.34, color: isStone ? "rgba(128,96,58,0.76)" : "rgba(174,128,70,0.52)" })); - if (hit > 0 || antHit > 0) this.spawnFallEffect(it.x, it.y + (it.r || 12) * 0.6, isStone ? 0.9 : 0.45); - if (isStone && (hit > 0 || antHit > 0)) audio.poke(); - if (hit > 0) this.log(`${isStone ? "\u77f3" : "\u9053\u5177"}\u304c\u843d\u3061\u3001${hit}\u5339\u306e\u305f\u308a\u306a\u3044\u304c\u5dfb\u304d\u8fbc\u307e\u308c\u305f\u3002`, "accident"); + if (isGenkotsu) { + for (let r = 0; r < 4; r++) this.effects.push(new Effect("ring", it.x, it.y, { size: 32 + r * 34, life: 0.34 + r * 0.07, color: r % 2 ? "rgba(255,203,54,0.72)" : "rgba(74,57,42,0.42)" })); + for (let i = 0; i < 12; i++) this.effects.push(new Effect("fight", it.x + rand(-38, 38), it.y + rand(-22, 28), { size: rand(12, 22), life: rand(0.25, 0.48), color: "rgba(255,205,46,0.82)" })); + } else { + this.effects.push(new Effect("ring", it.x, it.y, { size: isStone ? 32 : 18, life: 0.34, color: isStone ? "rgba(128,96,58,0.76)" : "rgba(174,128,70,0.52)" })); + } + if (hit > 0 || antHit > 0) this.spawnFallEffect(it.x, it.y + (it.r || 12) * 0.6, isGenkotsu ? 1.22 : (isStone ? 0.9 : 0.45)); + if ((isStone || isGenkotsu) && (hit > 0 || antHit > 0)) audio.poke(); + if (hit > 0 || antHit > 0) { + const label = isGenkotsu ? "\u3052\u3093\u3053\u3064" : (isStone ? "\u77f3" : "\u9053\u5177"); + this.log(`${label}\u304c\u843d\u3061\u3001${hit}\u5339\u306e\u305f\u308a\u306a\u3044\u3068${antHit}\u5339\u306e\u30a2\u30ea\u304c\u5dfb\u304d\u8fbc\u307e\u308c\u305f\u3002`, "accident"); + } } countBallChasers(ball, except = null) { @@ -1640,7 +1747,7 @@ class World { t.blastSpinMax = Math.max(t.blastSpinMax || 0, t.blastSpinTimer); this.spawnFallEffect(t.x, t.y + t.radius * 0.55, clamp(damage / 18, 0.55, 1.8)); this.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(20, ball.r * 1.25), life: 0.22, color: "rgba(210,75,65,0.50)" })); - if (this.relationNotice(t.id, ball.id || "ball", "fast-ball-hit", 2.8)) this.log(`${t.name}\u306f\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident"); + if (this.relationNotice(t.id, ball.id || "ball", "fast-ball-hit", 2.8)) this.log(`${t.name}\u306f\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [t] }); ball.vx = (ball.vx || 0) * 0.58 - nx * Math.min(110, ballSpeed * 0.10); ball.vy = (ball.vy || 0) * 0.58 - ny * Math.min(110, ballSpeed * 0.10); ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 8.5, -38, 38); @@ -1676,7 +1783,7 @@ class World { t.nextPlayBubbleAt = now + rand(2.0, 3.8); this.spawnBubble(t.x, t.y - t.radius * 1.15, pick(["!", "\u306f\u3046", "?" ]), "rgba(70,96,50,0.76)"); } - if (playful && this.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) this.log(`${t.name}\u306f\u30dc\u30fc\u30eb\u3092\u8ffd\u3044\u304b\u3051\u3066\u5f3e\u3044\u305f\u3002`, "observe"); + if (playful && this.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) this.log(`${t.name}\u306f\u30dc\u30fc\u30eb\u3092\u8ffd\u3044\u304b\u3051\u3066\u5f3e\u3044\u305f\u3002`, "observe", { participants: [t] }); this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(12, ball.r * 0.80), life: 0.20, color: "rgba(170,210,95,0.50)" })); } } @@ -1752,7 +1859,7 @@ class World { t.fearTimer = Math.max(t.fearTimer || 0, 0.55); } this.effects.push(new Effect("ring", (a.x + b.x) * 0.5, (a.y + b.y) * 0.5, { size: Math.max(16, hitDistance * 0.45), life: 0.20, color: "rgba(210,75,65,0.40)" })); - if (this.relationNotice(target.id, impactor.id, "tarinai-highspeed-collision", 2.6)) this.log(`${target.name}\u306f\u9ad8\u901f\u306e${impactor.name}\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident"); + if (this.relationNotice(target.id, impactor.id, "tarinai-highspeed-collision", 2.6)) this.log(`${target.name}\u306f\u9ad8\u901f\u306e${impactor.name}\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [target, impactor] }); } } } @@ -2107,11 +2214,11 @@ class World { } nearbyItems(x, y, radius) { - return this.spatial.nearby(this.spatial.itemCells, x, y, radius, this.spatialScratch); + return this.spatial.nearby(this.spatial.itemCells, x, y, radius, this.spatialItemScratch); } nearbyTarinai(x, y, radius) { - return this.spatial.nearby(this.spatial.tarinaiCells, x, y, radius, this.spatialScratch); + return this.spatial.nearby(this.spatial.tarinaiCells, x, y, radius, this.spatialTarinaiScratch); } rebuildSpatial() { @@ -2344,8 +2451,9 @@ class World { observeHighlightKind(t) { const s = this.selected; if (!s || !t || t === s || t.dead || this.tool !== "observe") return ""; - if ((s.parents || []).includes(t.id)) return "parent"; - if ((s.children || []).includes(t.id)) return "child"; + const tFamilyKey = this.tarinaiFamilyKey(t); + if ((s.parents || []).includes(tFamilyKey)) return "parent"; + if ((s.children || []).includes(tFamilyKey)) return "child"; 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"; @@ -2440,6 +2548,7 @@ class World { for (const t of this.tarinai) { t.update(dt); } + this.rebuildSpatial(); this.limitBallChasers(); this.resolveBallInteractions(dt); this.resolveTarinaiHighSpeedCollisions(dt); @@ -2573,6 +2682,28 @@ class World { t.entryTimer = 0; } } + let changedAnts = false; + for (const ant of this.ants || []) { + if (!ant || ant.dead) continue; + const result = repairPoint(ant, ant.kind === "queen" ? 18 : 10); + if (result === "delete") { + ant.dead = true; + changedAnts = true; + continue; + } + if (result === "moved") { + ant.vx = 0; + ant.vy = 0; + if (ant.state === "drag") { + ant.state = "return"; + ant.targetId = ""; + ant.targetToken = 0; + ant.targetRef = null; + } + changedAnts = true; + } + } + if (changedAnts) this.compactAnts?.(); if (changedItems) { this.compactItems(); this.updateItemCounts(); @@ -2589,9 +2720,10 @@ class World { : current === "cloudy" ? ["sunny", "cloudy", "light_rain"] : ["sunny", "cloudy"]; - this.weather = pick(choices); + const next = pick(choices); + this.weather = next; this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax); - this.log(`\u5929\u6c17: ${weatherLabel(this.weather)}`); + if (next !== current) this.log(`\u5929\u6c17: ${weatherLabel(this.weather)}`); } fencePlacementBlocked(item) { @@ -2611,8 +2743,49 @@ class World { return false; } + importantPlacementOverlapBlocked(item) { + if (!item || item.dead) return true; + const important = new Set(["grass", "fence_v", "fence_h", "nest_box", "water_bowl"]); + if (!important.has(item.type)) return false; + const placingGrass = item.type === "grass"; + const itemRects = this.solidObstacleRects(item); + const radiusFor = typeof itemRadiusFor === "function" ? itemRadiusFor : ((type, fallback = 12) => fallback); + const itemRadius = Math.max(14, item.r || radiusFor(item.type, 12) || 12); + const rectCircleOverlap = (rect, cx, cy, radius, margin = 0) => { + if (!rect) return false; + const px = clamp(cx, rect.left - margin, rect.right + margin); + const py = clamp(cy, rect.top - margin, rect.bottom + margin); + return distXY(cx, cy, px, py) < radius + margin; + }; + const rectsOverlap = (a, b, margin = 0) => Boolean(a && b && !(a.right + margin < b.left || a.left - margin > b.right || a.bottom + margin < b.top || a.top - margin > b.bottom)); + for (const other of this.nearbyItems(item.x, item.y, Math.max(132, itemRadius * 4))) { + if (!other || other === item || other.dead || !important.has(other.type)) continue; + // Grass is a ground layer: objects may be placed on top of existing grass. + // Grass placement itself still avoids objects and other grass via grassSpotOpen(). + if (!placingGrass && other.type === "grass") continue; + const otherRects = this.solidObstacleRects(other); + const otherRadius = Math.max(14, other.r || radiusFor(other.type, 12) || 12); + const margin = Math.max(4, Math.min(itemRadius, otherRadius) * 0.18); + if (itemRects.length && otherRects.length) { + if (itemRects.some(a => otherRects.some(b => rectsOverlap(a, b, margin)))) return true; + continue; + } + if (itemRects.length) { + if (itemRects.some(r => rectCircleOverlap(r, other.x, other.y, otherRadius, margin))) return true; + continue; + } + if (otherRects.length) { + if (otherRects.some(r => rectCircleOverlap(r, item.x, item.y, itemRadius, margin))) return true; + continue; + } + if (distXY(item.x, item.y, other.x, other.y) < itemRadius + otherRadius + margin) return true; + } + return false; + } + placementBlocked(item) { if (!item || item.dead) return true; + if (item.type === "genkotsu") return false; const pad = CONFIG.worldPadding || 30; const rects = this.solidObstacleRects(item); if (rects.length) { @@ -2625,6 +2798,7 @@ class World { } if (item.type === "grass" && !this.grassSpotOpen(item.x, item.y, { avoidTarinai: true })) return true; if (this.isFenceType(item.type) && this.fencePlacementBlocked(item)) return true; + if (this.importantPlacementOverlapBlocked(item)) return true; if (item.type === "nest_box") { const margin = Math.max(8, (item.r || 42) * 0.18); for (const it of this.nearbyItems(item.x, item.y, Math.max(120, (item.r || 42) * 3))) { @@ -2745,7 +2919,7 @@ class World { return null; } if (dropped) { - item.dropMax = item.type === "stone" ? 0.72 : 0.58; + item.dropMax = item.type === "genkotsu" ? 0.78 : (item.type === "stone" ? 0.72 : 0.58); item.dropTimer = item.dropMax; item.dropImpactDone = false; } @@ -2763,7 +2937,6 @@ class World { if (!this.isTarinaiHiddenInNestBox(this.tarinai[i]) && this.tarinai[i].contains(x, y)) { found = this.tarinai[i]; break; } } this.selected = found; - if (found) this.log(`${found.name}\u3092\u9078\u629e\u3057\u305f\u3002`); return; } @@ -2782,9 +2955,9 @@ class World { const target = [...this.tarinai].reverse().find(t => t.contains(x, y)); if (target) { target.poke(); - if (!target.dead) this.log(`${target.name}\u306f\u3064\u3064\u304b\u308c\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u899a\u3048\u305f\u3002`); + if (!target.dead) this.log(`${target.name}\u306f\u3064\u3064\u304b\u308c\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u899a\u3048\u305f\u3002`, "observe", { participants: [target] }); } else { - this.log("\u7a7a\u6c17\u3092\u3064\u3064\u3044\u305f\u3002\u305d\u3053\u306b\u306f\u4f55\u3082\u305f\u308a\u306a\u304b\u3063\u305f\u3002"); + this.log("\u7a7a\u6c17\u3092\u3064\u3064\u3044\u305f\u3002\u305d\u3053\u306b\u306f\u4f55\u3082\u305f\u308a\u306a\u304b\u3063\u305f\u3002", "observe", { hiddenFromObservation: true, countInStats: false }); } return; } @@ -2826,14 +2999,14 @@ class World { const placed = this.placeItem(rawItem, true); if (!placed) return; const label = toolLabel(itemType); - this.log(`${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`); + this.log(itemType === "genkotsu" ? `${label}\u3092\u843d\u3068\u3057\u305f\u3002` : `${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`); } } inferLogKind(text) { const s = String(text || "").toLowerCase(); if (s.includes("firecracker") || s.includes("accident") || s.includes("drop") || s.includes("\u7206\u7af9") || s.includes("\u843d\u3061") || s.includes("\u4e8b\u6545") || s.includes("\u5439\u304d\u98db")) return "accident"; - if (s.includes("fight") || s.includes("lost to") || s.includes("poked") || s.includes("\u55a7\u5629") || s.includes("\u8ca0\u3051") || s.includes("\u3064\u3064\u304b")) return "fight"; + if (s.includes("fight") || s.includes("lost to") || s.includes("\u55a7\u5629") || s.includes("\u8ca0\u3051")) return "fight"; if (s.includes("appeared") || s.includes("wandered in") || s.includes("birth") || s.includes("\u73fe\u308c") || s.includes("\u8ff7\u3044\u8fbc")) return "birth"; if (s.includes("dead") || s.includes("trace") || s.includes("reason:") || s.includes("\u6b7b\u4ea1") || s.includes("\u75d5\u8de1") || s.includes("\u7406\u7531:") || s.includes("\u6b7b\u56e0:")) return "death"; if (s.includes("weather") || s.includes("day") || s.includes("night") || s.includes("morning") || s.includes("evening") || s.includes("\u5929\u6c17") || s.includes("\u65e5\u76ee") || s.includes("\u591c") || s.includes("\u671d") || s.includes("\u5915")) return "weather"; @@ -2856,12 +3029,32 @@ class World { kind: kind || this.inferLogKind(text), eventType: options.eventType || "", hiddenFromObservation: Boolean(options.hiddenFromObservation), + participants: (options.participants || []).filter(Boolean).map(t => ({ + id: t.id || t.releasedLiveId || "", + liveId: t.id || t.releasedLiveId || "", + liveToken: t.liveToken || t.releasedLiveToken || 0, + familyKey: this.tarinaiFamilyKey(t), + name: t.name || "", + type: t.type || "", + generation: t.generation || 1, + })), + participantLiveIds: (options.participants || []).filter(t => t?.id).map(t => t.id), + participantSnapshots: (options.participants || []).filter(Boolean).map(t => ({ + liveId: t.id || t.releasedLiveId || "", + liveToken: t.liveToken || t.releasedLiveToken || 0, + familyKey: this.tarinaiFamilyKey(t), + name: t.name || "", + type: t.type || "", + generation: t.generation || 1, + })), }; if (this.shouldHideFromObservationRecords(entry)) entry.hiddenFromObservation = true; + const countInStats = options.countInStats ?? !entry.hiddenFromObservation; + entry.countInStats = Boolean(countInStats); if (!this.eventCounters) this.eventCounters = {}; - this.eventCounters[entry.kind] = (this.eventCounters[entry.kind] || 0) + 1; - for (const t of this.tarinai || []) { - if (!t || !t.name || !text || !String(text).includes(t.name)) continue; + if (countInStats) this.eventCounters[entry.kind] = (this.eventCounters[entry.kind] || 0) + 1; + for (const t of options.participants || []) { + if (!t) continue; if (!entry.hiddenFromObservation && t.addRecord) t.addRecord(text, entry.kind); } this.logs.unshift(entry); @@ -2869,65 +3062,6 @@ class World { renderLog(this.logs); } - serialize() { - return { - w: this.w, h: this.h, time: this.time, day: this.day, deadCount: this.deadCount, - maxGeneration: this.maxGeneration, lastBirthAt: this.lastBirthAt || -999, - tarinai: this.tarinai.map(t => t.serialize()), - items: this.items.map(i => i.serialize()), - ants: (this.ants || []).map(a => a.serialize ? a.serialize() : a), - logs: this.logs.slice(0, 30), - family: this.family || {}, - weather: this.weather || "sunny", - fieldType: this.fieldType || "garden", - fieldZoom: this.fieldZoom || 1, - cameraX: this.cameraX || 0, - cameraY: this.cameraY || 0, - eventCounters: this.eventCounters || {}, - toolSize: this.toolSize || "medium", - toolSizes: this.toolSizes || {}, - nextWeatherChange: this.nextWeatherChange || CONFIG.weatherChangeMin, - }; - } - - load(data) { - this.time = data.time || 0; - this.day = data.day || 1; - this.deadCount = data.deadCount || 0; - this.eventCounters = data.eventCounters || {}; - this.toolSize = data.toolSize || this.toolSize || "medium"; - this.toolSizes = data.toolSizes || this.toolSizes || {}; - this.maxGeneration = data.maxGeneration || 1; - this.lastBirthAt = Number.isFinite(data.lastBirthAt) ? data.lastBirthAt : -999; - this.fieldType = FIELD_TYPES?.[data.fieldType]?.id || "garden"; - this.fieldZoom = Number(data.fieldZoom) || 1; - this.cameraX = Number(data.cameraX) || 0; - this.cameraY = Number(data.cameraY) || 0; - this.tarinai = (data.tarinai || []).map(o => Tarinai.from(this, o)); - this.items = (data.items || []).filter(o => o?.type !== "mirror" && o?.type !== "food" && o?.type !== "cardboard").map(o => Item.from(o)); - this.ants = (data.ants || []).map(o => AntActor.from(this, o)); - this.effects = []; - this.compactItems(); - this.compactEffects(); - this.logs = (data.logs || []).map(e => typeof e === "string" ? { time: this.time, text: e, kind: this.inferLogKind(e) } : { time: e.time || this.time, text: e.text || "", kind: e.kind || this.inferLogKind(e.text || "") }); - this.family = data.family || {}; - this.familyCleanVersion = null; - this.familyVersion = (this.familyVersion || 0) + 1; - this.markFamilyTreeDirty("load"); - this.weather = data.weather || "sunny"; - this.nextWeatherChange = data.nextWeatherChange || rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax); - this.relationNotices = {}; - this.resolvedFightIds = {}; - for (const t of this.tarinai) this.recordFamily(t); - this.normalizeFamily(); - this.pruneExtinctFamilies({ normalize: false }); - this.lastPhase = this.phaseName(); - this.selected = null; - this.updateItemCounts(); - this.updateEffectCounts(); - this.rebuildSpatial(); - this.log("\u30bb\u30fc\u30d6\u30c7\u30fc\u30bf\u3092\u8aad\u307f\u8fbc\u307f\u307e\u3057\u305f\u3002"); - } } let world;