hmm
This commit is contained in:
parent
10fbeb2327
commit
c6ba3e38a2
21 changed files with 1565 additions and 898 deletions
12
.htaccess
12
.htaccess
|
|
@ -1,21 +1,21 @@
|
|||
<IfModule mod_expires.c>
|
||||
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"
|
||||
</IfModule>
|
||||
<IfModule mod_headers.c>
|
||||
<FilesMatch "\.(html?|css|js|json)$">
|
||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||
<FilesMatch "\.html?$">
|
||||
Header set Cache-Control "no-cache, must-revalidate"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</FilesMatch>
|
||||
<FilesMatch "\.(png|webp|mp3|wav)$">
|
||||
<FilesMatch "\.(css|js|json|png|webp|mp3|wav)$">
|
||||
Header set Cache-Control "public, max-age=2592000"
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
BIN
assets/objects/genkotsu.png
Normal file
BIN
assets/objects/genkotsu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
assets/ui/tool_genkotsu.png
Normal file
BIN
assets/ui/tool_genkotsu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
388
css/styles.css
388
css/styles.css
|
|
@ -1,4 +1,3 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=Yomogi&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Zen+Maru+Gothic:wght@400;500;700&family=M+PLUS+Rounded+1c:wght@400;500;700&family=Yomogi&display=swap");
|
||||
|
||||
:root {
|
||||
|
|
@ -36,6 +35,64 @@ button {
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
.loading-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at 50% 30%, rgba(164, 225, 104, 0.22), transparent 32%),
|
||||
linear-gradient(180deg, rgba(239, 248, 255, 0.98), rgba(247, 245, 236, 0.98));
|
||||
transition: opacity 180ms ease, visibility 180ms ease;
|
||||
}
|
||||
.loading-screen.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.loading-panel {
|
||||
width: min(420px, 92vw);
|
||||
padding: 24px 22px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(84, 68, 48, 0.16);
|
||||
background: rgba(255, 252, 246, 0.92);
|
||||
box-shadow: 0 18px 48px rgba(64, 48, 32, 0.16);
|
||||
text-align: center;
|
||||
}
|
||||
.loading-title {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.loading-text {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
min-height: 1.55em;
|
||||
}
|
||||
.loading-bar {
|
||||
height: 12px;
|
||||
margin: 18px 0 8px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
background: rgba(90, 70, 48, 0.12);
|
||||
}
|
||||
.loading-bar-fill {
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #9ce84b, #f0d35b);
|
||||
transition: width 160ms ease;
|
||||
}
|
||||
.loading-percent {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: min(1480px, 100vw);
|
||||
margin: 0 auto;
|
||||
|
|
@ -191,23 +248,6 @@ h1 {
|
|||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.save-text-dialog-panel {
|
||||
width: min(760px, calc(100vw - 30px));
|
||||
}
|
||||
.save-text-area {
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.82);
|
||||
color: var(--ink);
|
||||
font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
resize: vertical;
|
||||
word-break: break-all;
|
||||
}
|
||||
.save-text-actions { flex-wrap: wrap; }
|
||||
.archive-reset-dialog-panel {
|
||||
width: min(420px, 100%);
|
||||
}
|
||||
|
|
@ -349,32 +389,18 @@ h1 {
|
|||
.tool[data-tool="sweet"] { --tool-icon: url("../assets/ui/tool_sweet.png"); }
|
||||
.tool[data-tool="grass"] { --tool-icon: url("../assets/ui/tool_grass.png"); }
|
||||
.tool[data-tool="stone"] { --tool-icon: url("../assets/ui/tool_stone.png"); }
|
||||
.tool[data-tool="genkotsu"] { --tool-icon: url("../assets/ui/tool_genkotsu.png"); }
|
||||
.tool[data-tool="firecracker"] { --tool-icon: url("../assets/ui/tool_firecracker.png"); }
|
||||
.tool[data-tool="fence_v"] { --tool-icon: url("../assets/ui/tool_fence_v.png"); }
|
||||
.tool[data-tool="fence_h"] { --tool-icon: url("../assets/ui/tool_fence_h.png"); }
|
||||
.tool[data-tool="bed"] { --tool-icon: url("../assets/ui/tool_bed.png"); }
|
||||
.tool[data-tool="ball"] { --tool-icon: none; }
|
||||
.tool[data-tool="ball"]::before {
|
||||
content: "\26bd ";
|
||||
background: none;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||
}
|
||||
.tool[data-tool="ball"]::after {
|
||||
content: "\26bd ";
|
||||
background: none;
|
||||
font-size: 48px;
|
||||
line-height: 1;
|
||||
opacity: 0.10;
|
||||
right: -6px;
|
||||
left: auto;
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
.tool[data-tool="love_mochi"] { --tool-icon: url("../assets/ui/tool_love_mochi.png"); }
|
||||
.tool[data-tool="fight_mochi"] { --tool-icon: url("../assets/ui/tool_fight_mochi.png"); }
|
||||
.tool[data-tool="water_bowl"] { --tool-icon: url("../assets/ui/tool_water_bowl.png"); }
|
||||
.tool[data-tool="sleep_drug"] { --tool-icon: url("../assets/ui/tool_sleep_drug.png"); }
|
||||
.tool[data-tool="nest_box"] { --tool-icon: url("../assets/ui/tool_nest_box.png"); }
|
||||
.tool[data-tool="ant_nest"] { --tool-icon: url("../assets/ui/tool_ant_nest.png"); }
|
||||
.tool[data-tool="ball"] { --tool-icon: url("../assets/ui/tool_ball.png"); }
|
||||
.tool[data-tool="poke"] { --tool-icon: none; }
|
||||
.tool[data-tool="poke"]::before {
|
||||
content: "\1F448";
|
||||
|
|
@ -420,39 +446,19 @@ h1 {
|
|||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
.tool[data-tool="love_mochi"] { --tool-icon: none; }
|
||||
.tool[data-tool="love_mochi"]::before {
|
||||
content: "\1F495";
|
||||
.tool[data-tool="water_hose"] { --tool-icon: none; }
|
||||
.tool[data-tool="water_hose"]::before {
|
||||
content: "\1F6BF";
|
||||
background: none;
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||
}
|
||||
.tool[data-tool="love_mochi"]::after {
|
||||
content: "\1F495";
|
||||
background: none;
|
||||
font-size: 46px;
|
||||
line-height: 1;
|
||||
opacity: 0.10;
|
||||
right: -6px;
|
||||
left: auto;
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
.tool[data-tool="fight_mochi"] { --tool-icon: none; }
|
||||
.tool[data-tool="fight_mochi"]::before {
|
||||
content: "\1F4A2";
|
||||
background: none;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||
}
|
||||
.tool[data-tool="fight_mochi"]::after {
|
||||
content: "\1F4A2";
|
||||
.tool[data-tool="water_hose"]::after {
|
||||
content: "\1F6BF";
|
||||
background: none;
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 46px;
|
||||
line-height: 1;
|
||||
opacity: 0.10;
|
||||
|
|
@ -506,6 +512,36 @@ h1 {
|
|||
.tool-category.collapsed .tool-category-toggle::before { content: "\25b8 "; }
|
||||
.tool-category-body { padding: 8px; }
|
||||
.tool-category.collapsed .tool-category-body { display: none; }
|
||||
|
||||
.selected-data-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.selected-category {
|
||||
border: 1px solid rgba(84,68,48,0.10);
|
||||
border-radius: 14px;
|
||||
background: rgba(255,255,255,0.30);
|
||||
overflow: hidden;
|
||||
}
|
||||
.selected-category-toggle {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(84,68,48,0.08);
|
||||
background: rgba(255,252,244,0.66);
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
color: var(--ink);
|
||||
}
|
||||
.selected-category-toggle::before { content: "\25be "; display: inline-block; width: 16px; color: var(--muted); }
|
||||
.selected-category.collapsed .selected-category-toggle::before { content: "\25b8 "; }
|
||||
.selected-category-body { padding: 7px 9px; }
|
||||
.selected-category.collapsed .selected-category-body { display: none; }
|
||||
.selected-category-body .info-row:last-child { border-bottom: 0; }
|
||||
.tool-size-badge {
|
||||
position: absolute;
|
||||
right: 7px;
|
||||
|
|
@ -993,6 +1029,20 @@ body::before {
|
|||
z-index: 1;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.lineage-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.btn.active {
|
||||
background: #efffde;
|
||||
border-color: rgba(103,178,44,0.46);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lineage-render-status {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
|
@ -1106,38 +1156,6 @@ body::before {
|
|||
}
|
||||
|
||||
|
||||
.tool[data-tool="water_bowl"] { --tool-icon: none; }
|
||||
.tool[data-tool="water_bowl"]::before {
|
||||
content: "\1F963";
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 23px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.tool[data-tool="sleep_drug"] { --tool-icon: none; }
|
||||
.tool[data-tool="sleep_drug"]::before {
|
||||
content: "\1F48A";
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 23px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.tool[data-tool="nest_box"] { --tool-icon: none; }
|
||||
.tool[data-tool="nest_box"]::before {
|
||||
content: "\1FABA";
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 23px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.tool[data-tool="water_hose"] { --tool-icon: none; }
|
||||
.tool[data-tool="water_hose"]::before {
|
||||
content: "\1F6BF";
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 23px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.tool[data-tip] { position: relative; }
|
||||
.tool-tip-popover {
|
||||
position: fixed;
|
||||
|
|
@ -1210,149 +1228,6 @@ body::before {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tool[data-tool="water_bowl"]::before,
|
||||
.tool[data-tool="sleep_drug"]::before,
|
||||
.tool[data-tool="nest_box"]::before,
|
||||
.tool[data-tool="water_hose"]::before {
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tool[data-tool="water_bowl"]::after,
|
||||
.tool[data-tool="sleep_drug"]::after,
|
||||
.tool[data-tool="nest_box"]::after,
|
||||
.tool[data-tool="water_hose"]::after {
|
||||
content: "";
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* Tool UI icons: use in-game styled image assets instead of emoji glyphs. */
|
||||
.tool[data-tool="love_mochi"] { --tool-icon: url("../assets/ui/tool_love_mochi.png"); }
|
||||
.tool[data-tool="fight_mochi"] { --tool-icon: url("../assets/ui/tool_fight_mochi.png"); }
|
||||
.tool[data-tool="water_bowl"] { --tool-icon: url("../assets/ui/tool_water_bowl.png"); }
|
||||
.tool[data-tool="sleep_drug"] { --tool-icon: url("../assets/ui/tool_sleep_drug.png"); }
|
||||
.tool[data-tool="nest_box"] { --tool-icon: url("../assets/ui/tool_nest_box.png"); }
|
||||
.tool[data-tool="water_hose"] { --tool-icon: url("../assets/ui/tool_water_hose.png"); }
|
||||
.tool[data-tool="ball"] { --tool-icon: url("../assets/ui/tool_ball.png"); }
|
||||
|
||||
.tool[data-tool="love_mochi"]::before,
|
||||
.tool[data-tool="fight_mochi"]::before,
|
||||
.tool[data-tool="water_bowl"]::before,
|
||||
.tool[data-tool="sleep_drug"]::before,
|
||||
.tool[data-tool="nest_box"]::before,
|
||||
.tool[data-tool="water_hose"]::before,
|
||||
.tool[data-tool="ball"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
transform: translateY(-50%);
|
||||
background: var(--tool-icon) center / contain no-repeat;
|
||||
opacity: 0.92;
|
||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||
line-height: normal;
|
||||
text-align: initial;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.tool[data-tool="love_mochi"]::after,
|
||||
.tool[data-tool="fight_mochi"]::after,
|
||||
.tool[data-tool="water_bowl"]::after,
|
||||
.tool[data-tool="sleep_drug"]::after,
|
||||
.tool[data-tool="nest_box"]::after,
|
||||
.tool[data-tool="water_hose"]::after,
|
||||
.tool[data-tool="ball"]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
top: auto;
|
||||
bottom: auto;
|
||||
transform: none;
|
||||
background: var(--tool-icon) right -8px center / 54px 54px no-repeat;
|
||||
opacity: 0.10;
|
||||
pointer-events: none;
|
||||
font-size: initial;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
|
||||
/* 15.2.0: replace legacy poke/pinch PNG icons with emoji glyphs. */
|
||||
.tool[data-tool="poke"],
|
||||
.tool[data-tool="pinch"] { --tool-icon: none; }
|
||||
.tool[data-tool="poke"]::before,
|
||||
.tool[data-tool="pinch"]::before {
|
||||
background: none;
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
opacity: 0.96;
|
||||
}
|
||||
.tool[data-tool="poke"]::before { content: "\1F448"; }
|
||||
.tool[data-tool="pinch"]::before { content: "\1F90F"; }
|
||||
.tool[data-tool="poke"]::after,
|
||||
.tool[data-tool="pinch"]::after {
|
||||
background: none;
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 46px;
|
||||
line-height: 54px;
|
||||
text-align: center;
|
||||
opacity: 0.10;
|
||||
right: -6px;
|
||||
left: auto;
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
.tool[data-tool="poke"]::after { content: "\1F448"; }
|
||||
.tool[data-tool="pinch"]::after { content: "\1F90F"; }
|
||||
.tool[data-tool="poke"] span,
|
||||
.tool[data-tool="pinch"] span { display: none; }
|
||||
|
||||
|
||||
/* 15.3.0: shower uses emoji; nest box and fences use appearance-matched assets/hitboxes. */
|
||||
.tool[data-tool="water_hose"] { --tool-icon: none; }
|
||||
.tool[data-tool="water_hose"]::before {
|
||||
content: "\1F6BF";
|
||||
background: none;
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
opacity: 0.96;
|
||||
}
|
||||
.tool[data-tool="water_hose"]::after {
|
||||
content: "\1F6BF";
|
||||
background: none;
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 46px;
|
||||
line-height: 54px;
|
||||
text-align: center;
|
||||
opacity: 0.10;
|
||||
right: -6px;
|
||||
left: auto;
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.relation-list-row {
|
||||
align-items: start;
|
||||
}
|
||||
|
|
@ -1416,32 +1291,3 @@ body::before {
|
|||
opacity: 0.72;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.tool[data-tool="ant_nest"] { --tool-icon: url("../assets/ui/tool_ant_nest.png"); }
|
||||
.tool[data-tool="ant_nest"]::before {
|
||||
content: "";
|
||||
background: var(--tool-icon) center / contain no-repeat;
|
||||
}
|
||||
.tool[data-tool="ant_nest"]::after {
|
||||
content: "";
|
||||
background: var(--tool-icon) right -8px center / 54px 54px no-repeat;
|
||||
}
|
||||
|
||||
/* v15.7.2: object-faithful tool icons for ball and nest box. */
|
||||
.tool[data-tool="ball"] { --tool-icon: url("../assets/ui/tool_ball.png"); }
|
||||
.tool[data-tool="ball"]::before,
|
||||
.tool[data-tool="ball"]::after {
|
||||
content: "";
|
||||
background: var(--tool-icon) center / contain no-repeat;
|
||||
font-size: 0;
|
||||
}
|
||||
.tool[data-tool="ball"]::after {
|
||||
background-position: right -8px center;
|
||||
background-size: 54px 54px;
|
||||
opacity: 0.10;
|
||||
}
|
||||
.tool[data-tool="nest_box"] { --tool-icon: url("../assets/ui/tool_nest_box.png"); }
|
||||
.tool[data-tool="nest_box"]::after {
|
||||
content: "";
|
||||
background: var(--tool-icon) right -8px center / 54px 54px no-repeat;
|
||||
opacity: 0.10;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ Purpose: dense orientation for code-editing agents. Browser-only simulation; no
|
|||
Load order from `index.html`: `data -> 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
991
docs/prompt.txt
991
docs/prompt.txt
File diff suppressed because it is too large
Load diff
53
index.html
53
index.html
|
|
@ -3,13 +3,20 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<title>たりない観察</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=15.7.2" />
|
||||
<link rel="stylesheet" href="css/styles.css?v=15.9.2" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingScreen" class="loading-screen" role="status" aria-live="polite">
|
||||
<div class="loading-panel">
|
||||
<div class="loading-title">たりない観察を読み込み中</div>
|
||||
<div id="loadingText" class="loading-text">通信量を抑えて初期化しています…</div>
|
||||
<div class="loading-bar" aria-hidden="true"><div id="loadingBar" class="loading-bar-fill"></div></div>
|
||||
<div id="loadingPercent" class="loading-percent">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app">
|
||||
<header class="topbar">
|
||||
<div class="top-left">
|
||||
|
|
@ -74,6 +81,7 @@
|
|||
<button class="tool-category-toggle" type="button" aria-expanded="true">オブジェクト</button>
|
||||
<div class="tool-grid tool-category-body">
|
||||
<button class="tool" data-tool="stone">石</button>
|
||||
<button class="tool" data-tool="genkotsu">げんこつ</button>
|
||||
<button class="tool" data-tool="bed">干草寝床</button>
|
||||
<button class="tool" data-tool="nest_box">巣箱</button>
|
||||
<button class="tool" data-tool="ant_nest">アリの巣</button>
|
||||
|
|
@ -109,7 +117,10 @@
|
|||
<section class="card lineage-main-card">
|
||||
<div class="lineage-head">
|
||||
<h2>家系図</h2>
|
||||
<button id="archiveResetBtn" class="btn danger" type="button">家系図をリセット</button>
|
||||
<div class="lineage-actions">
|
||||
<button id="archiveUpdateToggleBtn" class="btn" type="button" aria-pressed="false">更新 OFF</button>
|
||||
<button id="archiveResetBtn" class="btn danger" type="button">家系図をリセット</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="archiveContent" class="archive-content lineage-main-content"></div>
|
||||
</section>
|
||||
|
|
@ -167,21 +178,21 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/data.js?v=15.8.0"></script>
|
||||
<script src="js/math.js?v=15.6.2"></script>
|
||||
<script src="js/assets.js?v=15.6.2"></script>
|
||||
<script src="js/audio.js?v=15.6.2"></script>
|
||||
<script src="js/render.js?v=15.6.2"></script>
|
||||
<script src="js/sim_core.js?v=15.6.2"></script>
|
||||
<script src="js/items.js?v=15.8.0"></script>
|
||||
<script src="js/ants.js?v=15.8.0"></script>
|
||||
<script src="js/health.js?v=15.8.0"></script>
|
||||
<script src="js/tarinai.js?v=15.8.0"></script>
|
||||
<script src="js/world.js?v=15.8.0"></script>
|
||||
<script src="js/text_catalog.js?v=15.6.2"></script>
|
||||
<script src="js/ui.js?v=15.6.2"></script>
|
||||
<script src="js/ui_charts.js?v=15.6.2"></script>
|
||||
<script src="js/ui_family.js?v=15.6.2"></script>
|
||||
<script src="js/main.js?v=15.6.2"></script>
|
||||
<script src="js/data.js?v=15.9.2" defer></script>
|
||||
<script src="js/math.js?v=15.9.2" defer></script>
|
||||
<script src="js/assets.js?v=15.9.2" defer></script>
|
||||
<script src="js/audio.js?v=15.9.2" defer></script>
|
||||
<script src="js/render.js?v=15.9.2" defer></script>
|
||||
<script src="js/sim_core.js?v=15.9.2" defer></script>
|
||||
<script src="js/items.js?v=15.9.2" defer></script>
|
||||
<script src="js/ants.js?v=15.9.2" defer></script>
|
||||
<script src="js/health.js?v=15.9.2" defer></script>
|
||||
<script src="js/tarinai.js?v=15.9.2" defer></script>
|
||||
<script src="js/world.js?v=15.9.2" defer></script>
|
||||
<script src="js/text_catalog.js?v=15.9.2" defer></script>
|
||||
<script src="js/ui.js?v=15.9.2" defer></script>
|
||||
<script src="js/ui_charts.js?v=15.9.2" defer></script>
|
||||
<script src="js/ui_family.js?v=15.9.2" defer></script>
|
||||
<script src="js/main.js?v=15.9.2" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
43
js/ants.js
43
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();
|
||||
|
|
|
|||
20
js/assets.js
20
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
234
js/items.js
234
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");
|
||||
|
|
|
|||
30
js/main.js
30
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
116
js/tarinai.js
116
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
101
js/ui.js
101
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 => `<span>${escapeHtml(PERSONALITY_LABELS[key]?.label || key)}: ${personalityValueText(p[key])}</span>`).join("");
|
||||
}
|
||||
|
||||
function selectedDataCategoryHtml(id, label, bodyHtml) {
|
||||
if (!uiCache.selectedCollapsedCategories) uiCache.selectedCollapsedCategories = new Set();
|
||||
const collapsed = uiCache.selectedCollapsedCategories.has(id);
|
||||
return `<section class="selected-category${collapsed ? " collapsed" : ""}" data-selected-category="${escapeHtml(id)}">
|
||||
<button class="selected-category-toggle" type="button" aria-expanded="${collapsed ? "false" : "true"}">${escapeHtml(label)}</button>
|
||||
<div class="selected-category-body">${bodyHtml}</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
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 `<li><span>${escapeHtml(timeText)}</span><strong>${escapeHtml(recordText)}</strong></li>`;
|
||||
}).join("") || `<li><span>-</span><strong>\u8a18\u9332\u306a\u3057</strong></li>`;
|
||||
ui.selectedInfo.innerHTML = `
|
||||
<div class="selected-name-editor">
|
||||
<input class="selected-name-input" data-selected-name data-selected-id="${escapeHtml(t.id)}" value="${escapeHtml(t.name)}" maxlength="18" aria-label="\u305f\u308a\u306a\u3044\u306e\u540d\u524d" title="\u540d\u524d\u3092\u7de8\u96c6">
|
||||
</div>
|
||||
${effectBadges.length ? `<div class="selected-effects">${effectBadges.join("")}</div>` : ""}
|
||||
const basicHtml = `
|
||||
<div class="info-row"><span>\u4e16\u4ee3</span><strong>${escapeHtml(generationDisplay || "\u4e0d\u660e")}</strong></div>
|
||||
<div class="info-row"><span>\u72b6\u614b</span><strong>${escapeHtml(stateLabel(t.state))}</strong></div>
|
||||
<div class="info-row"><span>\u7406\u7531</span><strong>${escapeHtml(reasonLabel(t))}</strong></div>
|
||||
${personalityHtml}
|
||||
<div class="info-row"><span>\u5bfe\u8c61</span><strong>${escapeHtml(targetLabel(t.target))}</strong></div>
|
||||
<div class="info-row"><span>\u5e74\u9f62</span><strong>${fmt(t.age)} / ${fmt(t.lifeSpan)}</strong></div>
|
||||
`;
|
||||
const healthHtml = `
|
||||
<div class="info-row"><span>\u7a7a\u8179</span><strong>${fmt(t.hunger)}</strong></div>
|
||||
<div class="info-row"><span>\u5bc2\u3057\u3055</span><strong>${fmt(t.loneliness)}</strong></div>
|
||||
<div class="info-row"><span>\u4f53\u529b</span><strong>${fmt(t.energy)}</strong></div>
|
||||
<div class="info-row"><span>\u30b9\u30c8\u30ec\u30b9</span><strong>${fmt(t.stress)}</strong></div>
|
||||
<div class="info-row"><span>\u305a\u3093\u3061\u6c5a\u308c</span><strong>${fmt(t.zunchiStain || 0)}</strong></div>
|
||||
<div class="info-row"><span>\u6c17\u5206</span><strong>${fmt(t.mood)}</strong></div>
|
||||
<div class="info-row"><span>\u305f\u308a\u306a\u3044</span><strong>${fmt(t.lack)}</strong></div>
|
||||
`;
|
||||
const relationHtml = `
|
||||
<div class="info-row relation-row relation-list-row"><span>\u53cb\u9054</span><strong>${relationListHtml(t, "friend")}</strong></div>
|
||||
<div class="info-row relation-row relation-list-row"><span>\u6575\u5bfe</span><strong>${relationListHtml(t, "enemy")}</strong></div>
|
||||
<div class="info-row"><span>\u55a7\u5629\u8a18\u61b6</span><strong>${escapeHtml(fightRecord)}</strong></div>
|
||||
<div class="info-row"><span>\u7d2f\u8a08\u6557\u5317</span><strong>${t.totalFightLosses || 0}</strong></div>
|
||||
`;
|
||||
const familyHtml = `
|
||||
<div class="info-row"><span>\u89aa</span><strong>${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "\u4e0d\u660e")}</strong></div>
|
||||
<div class="info-row"><span>\u5b50</span><strong>${t.children?.length || 0}</strong></div>
|
||||
<div class="info-row"><span>\u305f\u308a\u306a\u3044</span><strong>${fmt(t.lack)}</strong></div>
|
||||
<div class="selected-records"><h3>\u500b\u4f53\u5225\u306e\u8a18\u9332</h3><ol>${recordHtml}</ol></div>
|
||||
`;
|
||||
ui.selectedInfo.innerHTML = `
|
||||
<div class="selected-name-editor">
|
||||
<input class="selected-name-input" data-selected-name data-selected-id="${escapeHtml(t.id)}" value="${escapeHtml(t.name)}" maxlength="18" aria-label="\u305f\u308a\u306a\u3044\u306e\u540d\u524d" title="\u540d\u524d\u3092\u7de8\u96c6">
|
||||
</div>
|
||||
${effectBadges.length ? `<div class="selected-effects">${effectBadges.join("")}</div>` : ""}
|
||||
<div class="selected-data-groups">
|
||||
${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", `<div class="selected-records"><h3>\u500b\u4f53\u5225\u306e\u8a18\u9332</h3><ol>${recordHtml}</ol></div>`)}
|
||||
</div>
|
||||
<div class="selected-actions"><button type="button" class="favorite-toggle ${t.favorite ? "active" : ""}" data-selected-action="favorite">${t.favorite ? "\u2605 \u304a\u6c17\u306b\u5165\u308a\u89e3\u9664" : "\u2606 \u304a\u6c17\u306b\u5165\u308a"}</button></div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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", `<div class="lineage-stale-status">\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</div>`);
|
||||
ui.archiveContent.insertAdjacentHTML("afterbegin", `<div class="lineage-stale-status">\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</div>`);
|
||||
}
|
||||
|
||||
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 = `<div class="selected-info empty">\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</div>`;
|
||||
} 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;
|
||||
|
|
|
|||
388
js/world.js
388
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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue