This commit is contained in:
33333-33333 2026-06-24 18:49:17 +09:00
commit d875df8600
22 changed files with 625 additions and 496 deletions

View file

@ -1,123 +1,25 @@
# 15.24.35
# 15.24.53
- 持ち主不明のかんたんベッドは、最初に利用した個体の所有物になるよう変更。
- かんたんベッド同士が近すぎる位置に作られにくいよう、配置ブロックと建築位置選定を調整。
- 複製機にセットされた食べ物の種類を、食べ物探索の優先順位と接触距離ソートに反映。
- 排泄直後のずんちを通常個体が即座に食べないよう、ずんち専用の摂食保護時間を追加。
- 通常個体のずんち摂食条件をさらに厳格化し、他の食料が近い場合はずんちを食べないよう再調整。
- ずんちの風化・乾燥・分解・肥土化の進行速度と量減少速度を低下。
- 日光浴スプライトの切替を `updateSunbath` に一本化し、`spriteId()` が毎フレーム再抽選しないよう修正。
- 日光浴中はキャッシュ済み透明キャンバスではなく、読み込み済みの `sunbath` 画像を直接描画するよう修正。
# 15.24.34
# 15.24.52
- ぬいぐるみを添付スプライトに差し替え、白背景を透過化
- たりないデータの「関係」と「家族」を1セクションに統合
- 観察中の友達/敵対ハイライト判定を相互関係ベースに拡張
- かんたんベッドで睡眠中の個体に、親子でも友達でもない個体が寝に来た場合は喧嘩開始
- 日光浴中の専用直接描画を停止し、通常のたりない描画経路で `sunbath` スプライトを常時表示するよう修正
- 日光浴中は通常描画直前の `spriteId()` で0.5秒ごとに `sunbath` 系スプライトをランダム切替するよう統一
- 切替フレーム以外で日光浴スプライトが消える問題を抑止
- 持ち主不明のかんたんベッドが放置で徐々に劣化・消滅する処理を追加
# Patch Notes 15.24.33
# 15.24.52
## 15.24.33
- 日光浴中の描画を通常表情描画から分離し、毎フレーム必ず sunbath 系スプライトを直接描画するよう修正。
- 0.5秒ごとのランダム切替は維持しつつ、切替フレーム以外でスプライトが消える問題を抑止。
- sunbath画像が未ロードの瞬間も、読み込み済み候補またはフォールバックで描画を継続。
- Version: `15.24.33`.
- Build: `compact-save-v2`.
- Added item-bucket dirty tracking, `ensureItemBuckets()`, `itemById()`, `liveItemsOfType()`, and `addItem()` to keep type buckets and id lookup coherent after runtime item additions.
- Routed common item creation paths through `World.addItem()` for placements, rain water, grass growth/spawn, zunchi burst/spawn, ant corpses/nests, Tarinai death traces, and constructed owned structures.
- Expanded runtime diagnostics with bucket dirty state, item id-map size, and bucket rebuild counters.
- Reworked several hot item lookups to use buckets or id-map helpers where safe.
- Updated regression checks for item-bucket helper availability and direct item push documentation.
- Added final pre-save runtime-polish report and remaining-work split note.
- Did not touch save migration, population/family limits, or family-tree UI.
# 15.24.50
# Patch Notes 15.24.31
## 15.24.31
- Version: `15.24.31`.
- Build: `social-balance-runtime-polish`.
- Fixed social-action balance regression where conflict could dominate too often after ActionSpec migration.
- Reduced normal conflict pressure, lowered fight/intimidation priority, and raised mate/bond selection weight.
- Added pair-level fight cooldown guards so one pair/individual cannot trigger repeated fight starts in the same burst.
- Shortened intimidation/defeated display timers.
- Preserved fight mochi as a strong forced exception while reducing ordinary proximity-fight probability.
- Added regression checks for social balance constants and fight re-entry guards.
- Did not touch save migration, population/family limits, or family-tree UI.
# Patch Notes 15.24.30
## 15.24.30
- Version: `15.24.30`.
- Build: `action-spec-sunbath-runtime-check`.
- Moved `sunbath` into `createSunbathActionSpec()` and registered it directly in `TARINAI_ACTION_DEFINITIONS`.
- Removed the remaining `configureTarinaiNeedActions()` late-binding bridge.
- Kept the existing sunbath body/timer subsystem intact; player-visible sunbath behavior should remain unchanged.
- Updated regression checks so late action patching is treated as a failure.
- Did not touch save migration, population/family limits, or family-tree UI.
# Patch Notes 15.24.29
## 15.24.29
- Migrated social/fear/combat/mate/birth runtime actions into ActionSpec factories.
- Kept family data, family tree UI, population limits, and save migration untouched.
- Added regression checks for pass 3 ActionSpec migration.
- Build: `action-spec-social-runtime-check`.
# Patch Notes 15.24.28
## 15.24.28
- Added centralized behavior-text composition through `composeTarinaiBehaviorText()` and `patchBehaviorTextFromComposer()`.
- Added `textTarinaiAction()` to the ActionSpec lifecycle helpers.
- Kept existing explicit live behavior text as the default display fallback so player-facing wording does not churn during normal observation.
- Added a player-visible diff check comparing the current build against the initial 15.24.20 ZIP.
- Kept save migration, population/family caps, and family-tree UI out of scope.
# Patch Notes 15.24.25
## 15.24.25
- Added `scripts/regression_check.py` and `docs/REGRESSION_CHECKLIST.md` to freeze the current generated-file, syntax, and ActionSpec lifecycle baseline.
- Expanded `js/tarinai_action_spec.js` with explicit lifecycle helpers: `selectTarinaiActionTarget`, `canStartTarinaiAction`, `startTarinaiAction`, `tickTarinaiAction`, `finishTarinaiAction`, and `failTarinaiAction`.
- Migrated need-action definitions from legacy `condition/run/update` shape toward explicit `selectTarget/canStart/start/tick` ActionSpec fields.
- Routed priority/action eligibility through `canStartTarinaiAction()` while preserving legacy fallback behavior.
- Added finish/fail lifecycle calls for completed or failed action ticks.
- Kept save migration, population caps, family caps, and family-tree UI out of scope.
# Patch Notes 15.24.23
## 15.24.23
- Added coarse-grained spatial invalidation via `markSpatialDirty()` + `ensureSpatial()` for the main update loop.
- Reduced redundant spatial rebuilds around item, ball, ant, and Tarinai update phases while keeping direct `rebuildSpatial()` calls compatible.
- Added per-type item buckets in `updateItemCounts()` and switched ball-ball collision setup to `itemsOfType("ball")`.
- Reworked render layering into a cached dynamic render stack so expensive y-sort work runs on a short interval or count/signature change, not every paint.
- Kept population and family-tree UI behavior unchanged.
# Patch Notes 15.24.22
- Added `activeBehavior` as the authoritative runtime behavior layer above legacy state timers.
- Added `forcedBehaviorQueue` and `forceBehavior()` hook for external forced actions such as love mochi, fight mochi, and drugs.
- Re-routed love mochi, fight mochi, and sleep drug effects through forced need actions.
- Converted eating, panic, fighting, intimidation, and birth ritual display/scheduling to behavior-backed actions.
- Split social need pressure internally into bond/family/mate/conflict/fear parts and used those parts in social action selection.
- Weighted fulfill action selection by its internal reason parts.
- Reduced direct old-style birth starts: proximity now raises mate pressure; ritual starts when mate behavior is active or forced.
- UI action text now prefers `activeBehavior` over stale thoughts or intent strings.
## 15.24.22
- `js/tarinai_action_spec.js` を追加し、行動を `ActionSpec` として登録できる共通層を導入。
- 既存の `condition` / `run` / `update` を維持しつつ、`canStart` / `start` / `tick` / `finish` / `fail` / `text` に段階移行できる互換ラッパーを追加。
- `TARINAI_ACTIONS``TARINAI_ACTION_DEFINITIONS` から登録生成する形に変更。既存参照は維持。
- 行動開始・継続処理を `startTarinaiAction()` / `tickTarinaiAction()` 経由へ移行。
- `behavior``specId` / `specKind` を保持し、表示・旧API互換と行動仕様の対応を追いやすくした。
## 15.24.21
- 外部強制行動を queued / active / retry / expire の状態機械に拡張。
- 強制行動の source / forcedReasonText を activeBehavior に保持し、旧タイマー同期で失われにくくした。
- 関係欲求の開始判定を social 大分類ではなく bond / family / mate / conflict のサブ欲求に寄せた。
- 食事行動を activeBehavior 内で対象接近から摂食完了まで完結するよう整理。
- 行動選択時に高い未選択欲求がある場合、「眠たいけど、食べている」のように対立理由を表示。
## 15.24.21
- 外部強制行動を強化。薬・餅から `forceBehavior()` 経由で優先割込み、対象再探索、失敗時再試行を行うようにした。
- へこ餅、けんか餅、ねむり薬の強制行動優先度を更新。
- 喧嘩対象が Item 等になった場合の `relationTo is not a function` エラーを防止。
- 睡眠欲求を概日圧 `circadianSleepPressure` 中心に変更。夜は大きく上昇、昼下がりは微増、それ以外は減衰。
- 初期生成時、夜なら睡眠欲求が高めになるよう初期エネルギーと概日圧を調整。
- 日光浴中のスプライトを、通常表情ロックとは別枠で固定。
- sunbath 系スプライトを初期ロード対象へ追加。

View file

@ -1,6 +1,6 @@
# Tarinai Colony Observation Game
Buildless single-page browser simulation. Open `index.html`; no package manager, transpiler, server API, or module loader is required. Runtime order is explicit `<script defer>` order generated from `app_manifest.json`. Current inspected version: `15.24.33`.
Buildless single-page browser simulation. Open `index.html`; no package manager, transpiler, server API, or module loader is required. Runtime order is explicit `<script defer>` order generated from `app_manifest.json`. Current inspected version: `15.24.53`.
## Edit contract

View file

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

View file

@ -216,7 +216,7 @@ h1 {
width: min(440px, 100%);
max-height: calc(100vh - 36px);
overflow: auto;
border-radius: 16px;
border-radius: 12px;
padding: 16px;
background: rgba(255, 252, 245, 0.96);
border: 1px solid rgba(84,68,48,0.16);
@ -309,7 +309,7 @@ h1 {
color: white;
padding: 10px 16px;
border-radius: 999px;
font-size: 14px;
font-size: 12px;
pointer-events: none;
opacity: 1;
transition: opacity 200ms ease, transform 200ms ease;
@ -346,59 +346,83 @@ h1 {
.tool-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0,1fr));
grid-template-columns: repeat(3, minmax(0,1fr));
gap: 8px;
}
.tool {
border-radius: 14px;
display: flex;
align-items: center;
gap: 7px;
justify-content: flex-start;
padding: 8px 8px 8px 42px;
--tool-main-icon-size: 34px;
--tool-watermark-size: 0px;
border-radius: 12px;
display: grid;
grid-template-rows: 31px minmax(1.05em, auto);
align-items: start;
justify-items: center;
gap: 1px;
padding: 4px 5px 5px;
min-height: 46px;
font-size: 12px;
line-height: 1.08;
letter-spacing: -0.015em;
text-align: center;
position: relative;
overflow: hidden;
background-repeat: no-repeat;
background-position: 7px center, center;
background-size: 30px 30px, cover;
background-position: center 4px, center;
background-size: var(--tool-main-icon-size) var(--tool-main-icon-size), cover;
}
.tool::before {
content: "";
position: absolute;
left: 7px;
top: 50%;
width: 30px;
height: 30px;
transform: translateY(-50%);
left: 50%;
top: 4px;
width: var(--tool-main-icon-size);
height: var(--tool-main-icon-size);
transform: translateX(-50%);
background: var(--tool-icon) center / contain no-repeat;
opacity: 0.92;
opacity: 0.98;
z-index: 1;
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
}
.tool::after {
content: "";
position: absolute;
inset: 0;
background: var(--tool-icon) right -8px center / 54px 54px no-repeat;
opacity: 0.10;
background: none;
display: none;
pointer-events: none;
z-index: 0;
}
.tool-label {
position: relative;
z-index: 2;
display: block;
grid-row: 2;
align-self: start;
max-width: 100%;
min-width: 0;
overflow-wrap: anywhere;
font-size: 12px;
line-height: 1.12;
font-weight: 700;
text-shadow: 0 1px 0 rgba(255,255,255,0.62);
}
.tool.tool-has-canvas-icon { padding-left: 42px; --tool-icon: none; }
.tool.tool-has-canvas-icon { --tool-icon: none; grid-template-rows: 42px minmax(1.05em, auto); min-height: 58px; padding-top: 2px; }
.tool.tool-has-canvas-icon::before,
.tool.tool-has-canvas-icon::after { background: none !important; }
.tool-sprite-preview {
position: absolute;
left: 3px;
top: 50%;
width: 36px;
height: 36px;
transform: translateY(-50%);
left: 50%;
top: -2px;
width: 56px;
height: 56px;
transform: translateX(-50%);
pointer-events: none;
z-index: 1;
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
}
.tool > span:not(.tool-size-badge) { display: none; }
.tool-sprite-watermark { display: none !important; }
.tool[data-tool="observe"] { --tool-icon: url("../assets/ui/tool_observe.webp"); }
.tool[data-tool="zunchi"] { --tool-icon: url("../assets/ui/tool_zunchi.webp"); }
.tool[data-tool="sweet"] { --tool-icon: url("../assets/ui/tool_sweet.webp"); }
@ -443,7 +467,7 @@ h1 {
.signboard-editor.hidden { display: none; }
.signboard-editor-panel {
width: min(220px, calc(100vw - 28px));
border-radius: 18px;
border-radius: 12px;
padding: 14px;
background: linear-gradient(180deg, rgba(255,253,247,0.98), rgba(245,237,221,0.96));
border: 1px solid rgba(94, 70, 42, 0.20);

View file

@ -3,78 +3,24 @@
.tool[data-tool="nest_box"] { --tool-icon: url("../assets/ui/tool_nest_box.webp"); }
.tool[data-tool="ant_nest"] { --tool-icon: url("../assets/ui/tool_ant_nest.webp"); }
.tool[data-tool="ball"] { --tool-icon: url("../assets/ui/tool_ball.webp"); }
.tool[data-tool="poke"] { --tool-icon: none; }
.tool[data-tool="poke"]::before {
content: "\1F448";
background: none;
font-size: 25px;
line-height: 1;
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
}
.tool[data-tool="poke"]::after {
content: "\1F448";
background: none;
font-size: 48px;
line-height: 1;
opacity: 0.11;
right: -6px;
left: auto;
top: 50%;
bottom: auto;
transform: translateY(-50%);
width: 54px;
height: 54px;
}
.tool[data-tool="pinch"] { --tool-icon: none; }
.tool[data-tool="pinch"]::before {
content: "\1F90F";
background: none;
font-size: 25px;
line-height: 1;
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
}
.tool[data-tool="pinch"]::after {
content: "\1F90F";
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="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="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;
right: -6px;
left: auto;
top: 50%;
bottom: auto;
transform: translateY(-50%);
width: 54px;
height: 54px;
}
.tool[data-tool="delete"] { --tool-icon: url("../assets/ui/tool_delete.webp"); }
.tool[data-tool="new"] { --tool-icon: url("../assets/ui/tool_new.webp"); }
.tool[data-tool="poke"] { --tool-emoji: "\1F448"; }
.tool[data-tool="pinch"] { --tool-emoji: "\1F90F"; }
.tool[data-tool="water_hose"] { --tool-emoji: "\1F6BF"; }
.tool.tool-emoji-icon { --tool-icon: none; }
.tool.tool-emoji-icon::before {
content: var(--tool-emoji);
background: none;
display: grid;
place-items: center;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
font-size: 29px;
line-height: 1;
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
}
.tool.tool-emoji-icon::after { content: none; display: none; }
.tool.selected {
background: linear-gradient(180deg, #ffffff, #efffde);
border-color: rgba(103, 178, 44, 0.52);
@ -91,8 +37,8 @@
.tool-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.tool-head h2 { margin-bottom: 0; }
.btn.mini { padding: 6px 10px; font-size: 12px; }
.tool-body { margin-top: 12px; }
.tool-palette { display: flex; flex-direction: column; gap: 9px; }
.tool-body { margin-top: 10px; }
.tool-palette { display: flex; flex-direction: column; gap: 8px; }
.tool-category {
border: 1px solid rgba(84,68,48,0.10);
border-radius: 14px;
@ -104,7 +50,7 @@
border: 0;
border-bottom: 1px solid rgba(84,68,48,0.08);
background: rgba(255,252,244,0.66);
padding: 8px 10px;
padding: 7px 10px;
text-align: left;
cursor: pointer;
font-weight: 700;
@ -112,7 +58,7 @@
}
.tool-category-toggle::before { content: "\25be "; display: inline-block; width: 16px; color: var(--muted); }
.tool-category.collapsed .tool-category-toggle::before { content: "\25b8 "; }
.tool-category-body { padding: 8px; }
.tool-category-body { padding: 7px; }
.tool-category.collapsed .tool-category-body { display: none; }
.selected-data-groups {
@ -146,14 +92,14 @@
.selected-category-body .info-row:last-child { border-bottom: 0; }
.tool-size-badge {
position: absolute;
right: 7px;
bottom: 6px;
min-width: 20px;
padding: 2px 5px;
right: 4px;
bottom: 3px;
min-width: 16px;
padding: 1px 4px;
border-radius: 999px;
background: rgba(42,36,29,0.68);
background: rgba(42,36,29,0.72);
color: #fff;
font-size: 10px;
font-size: 8.5px;
line-height: 1;
text-align: center;
box-shadow: 0 1px 4px rgba(42,36,29,0.16);
@ -321,7 +267,7 @@ meter {
margin-right: 6px;
padding: 2px 5px;
border-radius: 999px;
font-size: 9px;
font-size: 9.5px;
line-height: 1.1;
text-align: center;
background: rgba(84,68,48,0.08);
@ -373,18 +319,6 @@ meter {
.tool-category[data-tool-category="hazard"] { --tool-cat-a: rgba(255,230,224,0.84); --tool-cat-b: rgba(250,207,197,0.78); --tool-cat-line: rgba(189,88,66,0.30); }
.tool-category { border-color: var(--tool-cat-line, rgba(84,68,48,0.10)); background: linear-gradient(180deg, var(--tool-cat-a, rgba(255,255,255,0.36)), rgba(255,255,255,0.28)); }
.tool-category-toggle { background: linear-gradient(180deg, var(--tool-cat-a, rgba(255,252,244,0.66)), var(--tool-cat-b, rgba(255,252,244,0.54))); border-bottom-color: var(--tool-cat-line, rgba(84,68,48,0.08)); }
.tool[data-tool="ball"] { --tool-icon: none; }
.tool[data-tool="ball"]::before {
content: "⚽";
background: none;
display: grid;
place-items: center;
font-size: 28px;
line-height: 1;
opacity: 1;
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
}
.tool[data-tool="ball"]::after { background: none; }
@media (min-width: 921px) {
.mobile-mode-controls { display: none !important; }
}

View file

@ -11,20 +11,6 @@
.loading-progress-meta { margin-top: 8px; display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: rgba(88,73,60,.82); }
.loading-hint { margin: 10px 0 0; font-size: 12px; line-height: 1.55; color: rgba(111,92,73,.78); }
.collapsible-card { padding-top: 10px; }
.panel-card-toggle { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 0; border-radius: 14px; padding: 10px 12px; cursor: pointer; font: inherit; color: var(--ink); background: rgba(255,255,255,.48); box-shadow: inset 0 1px 0 rgba(255,255,255,.54); }
.panel-card-title { font-size: 17px; font-weight: 700; letter-spacing: .03em; }
.panel-card-state { font-size: 12px; color: rgba(98,82,64,.78); padding-left: 16px; position: relative; }
.panel-card-state::before { content: "▾"; position: absolute; left: 0; top: 50%; transform: translateY(-52%); color: rgba(98,82,64,.72); }
.collapsible-card.collapsed .panel-card-state::before { content: "▸"; }
.collapsible-card.collapsed .panel-card-body { display: none; }
.panel-card-body { padding-top: 12px; }
.tool-card { background: linear-gradient(180deg, rgba(255,247,252,.92), rgba(249,240,249,.90)); border-color: rgba(200,153,186,.34); }
.colony-card { background: linear-gradient(180deg, rgba(244,250,238,.92), rgba(235,246,230,.90)); border-color: rgba(132,178,128,.34); }
.log-card { background: linear-gradient(180deg, rgba(255,246,238,.92), rgba(252,239,226,.90)); border-color: rgba(207,157,121,.34); }
.tool-card .panel-card-toggle { background: linear-gradient(180deg,rgba(255,239,248,.82),rgba(253,234,246,.84)); }
.colony-card .panel-card-toggle { background: linear-gradient(180deg,rgba(240,252,237,.82),rgba(230,247,228,.86)); }
.log-card .panel-card-toggle { background: linear-gradient(180deg,rgba(255,242,231,.84),rgba(253,235,220,.86)); }
.sound-category-controls,
.log-push-controls {
@ -91,25 +77,7 @@
@media (max-width: 760px) { .sound-panel { right: auto; left: 0; } }
/* v15.12.3: collapsed event panel should be genuinely thin */
.log-card.collapsed {
min-height: 0 !important;
padding: 5px 7px !important;
flex: 0 0 auto !important;
}
.log-card.collapsed .panel-card-toggle {
min-height: 30px;
padding: 5px 9px;
border-radius: 11px;
}
.log-card.collapsed .panel-card-title {
font-size: 13px;
line-height: 1.1;
}
.log-card.collapsed .panel-card-state {
font-size: 10px;
}
/* Collapsed card sizing is shared by the base panel rules; no event-only shrink override. */
/* v15.12.4: event log scroll chains to the right menu at top/bottom */
.log {
@ -179,16 +147,7 @@
}
}
/* Item icons and mobile input mode */
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.20.16"); }
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.20.16"); }
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.20.16"); }
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.20.16"); }
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.20.16"); }
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.20.16"); }
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.20.16"); }
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.20.16"); }
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.20.16"); }
/* Mobile input mode */
.mobile-mode-controls {
display: none;
position: absolute;

View file

@ -35,3 +35,8 @@
- 旧セーブは読めない。これは意図した仕様。
- 家系図の死亡者履歴は保存しない。読み込み後の家系図は生存個体ベースになる。
- `CompressionStream` 非対応環境では raw bytes を同じ91文字集合で符号化する。短縮率は下がるが読み書きは維持する。
## 15.24.37 修正
- エクスポート対象のたりないを生存個体のみに限定した。15.24.33 の方針では死亡者履歴を保存しない設計だったが、実装では `world.tarinai` 全体を書き出していたため、長期プレイほどハッシュテキストが不必要に肥大化していた。
- 読み込み後の死亡累計は world メタ情報から維持する。死亡者を含む完全な家系履歴は引き続き保存しない。

View file

@ -5,22 +5,22 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>&#x305F;&#x308A;&#x306A;&#x3044;&#x89B3;&#x5BDF;</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 64 64%27%3E%3Ctext y=%2752%27 font-size=%2752%27%3E%E3%81%9F%3C/text%3E%3C/svg%3E" />
<link rel="stylesheet" href="css/base.css?v=15.24.35" />
<link rel="stylesheet" href="css/layout.css?v=15.24.35" />
<link rel="stylesheet" href="css/panel.css?v=15.24.35" />
<link rel="stylesheet" href="css/components.css?v=15.24.35" />
<link rel="stylesheet" href="css/mobile.css?v=15.24.35" />
<link rel="stylesheet" href="css/base.css?v=15.24.53" />
<link rel="stylesheet" href="css/layout.css?v=15.24.53" />
<link rel="stylesheet" href="css/panel.css?v=15.24.53" />
<link rel="stylesheet" href="css/components.css?v=15.24.53" />
<link rel="stylesheet" href="css/mobile.css?v=15.24.53" />
</head>
<body>
<div id="loadingScreen" class="loading-screen" aria-live="polite">
<div class="loading-panel">
<div class="loading-kicker">たりないコロニー</div>
<h1 class="loading-title">読み込み中…</h1>
<p id="loadingText" class="loading-text">準備しています</p>
<div class="loading-kicker">&#x305F;&#x308A;&#x306A;&#x3044;&#x30B3;&#x30ED;&#x30CB;&#x30FC;</div>
<h1 class="loading-title">&#x8AAD;&#x307F;&#x8FBC;&#x307F;&#x4E2D;&#x2026;</h1>
<p id="loadingText" class="loading-text">&#x6E96;&#x5099;&#x3057;&#x3066;&#x3044;&#x307E;&#x3059;&#x3002;</p>
<div id="loadingSpriteStrip" class="loading-sprite-strip" aria-hidden="true"></div>
<div class="loading-progress-track"><div id="loadingBar" class="loading-progress-bar" style="width:0%"></div></div>
<div class="loading-progress-meta"><span>進行状況</span><strong id="loadingPercent">0%</strong></div>
<p class="loading-hint">初期表示に必要な画像だけ先に読み込み、残りは後から順次ロードします。</p>
<div class="loading-progress-meta"><span>&#x9032;&#x884C;&#x72B6;&#x6CC1;</span><strong id="loadingPercent">0%</strong></div>
<p class="loading-hint">&#x521D;&#x671F;&#x8868;&#x793A;&#x306B;&#x5FC5;&#x8981;&#x306A;&#x753B;&#x50CF;&#x3060;&#x3051;&#x5148;&#x306B;&#x8AAD;&#x307F;&#x8FBC;&#x307F;&#x3001;&#x6B8B;&#x308A;&#x306F;&#x5F8C;&#x304B;&#x3089;&#x9806;&#x6B21;&#x30ED;&#x30FC;&#x30C9;&#x3057;&#x307E;&#x3059;&#x3002;</p>
</div>
</div>
<div id="app">
@ -36,17 +36,17 @@
<button id="speedBtn" class="btn">&#x901F;&#x5EA6; x1</button>
<button id="ecologyBtn" class="btn">&#x305F;&#x308A;&#x306A;&#x3044;&#x306E;&#x751F;&#x614B;</button>
<div id="soundMenu" class="sound-menu">
<button id="soundBtn" class="btn sound-menu-btn" type="button" aria-haspopup="true" aria-expanded="false">音設定 ON</button>
<div id="soundPanel" class="sound-panel hidden" role="menu" aria-label="効果音設定">
<label class="sound-master"><input id="soundMasterToggle" type="checkbox" checked><span>効果音</span></label>
<button id="soundBtn" class="btn sound-menu-btn" type="button" aria-haspopup="true" aria-expanded="false">&#x97F3;&#x8A2D;&#x5B9A; ON</button>
<div id="soundPanel" class="sound-panel hidden" role="menu" aria-label="&#x52B9;&#x679C;&#x97F3;&#x8A2D;&#x5B9A;">
<label class="sound-master"><input id="soundMasterToggle" type="checkbox" checked><span>&#x52B9;&#x679C;&#x97F3;</span></label>
<div class="sound-panel-divider"></div>
<label><input type="checkbox" data-sound-category="voice"><span>たりないの声</span></label>
<label><input type="checkbox" data-sound-category="notify" checked><span>通知</span></label>
<label><input type="checkbox" data-sound-category="ops" checked><span>操作</span></label>
<label><input type="checkbox" data-sound-category="voice"><span>&#x305F;&#x308A;&#x306A;&#x3044;&#x306E;&#x58F0;</span></label>
<label><input type="checkbox" data-sound-category="notify" checked><span>&#x901A;&#x77E5;</span></label>
<label><input type="checkbox" data-sound-category="ops" checked><span>&#x64CD;&#x4F5C;</span></label>
</div>
</div>
<button id="creditsBtn" class="btn">&#x30AF;&#x30EC;&#x30B8;&#x30C3;&#x30C8;</button>
<button id="saveBtn" class="btn">セーブ</button>
<button id="saveBtn" class="btn">&#x30BB;&#x30FC;&#x30D6;</button>
<button id="resetBtn" class="btn danger">&#x30EA;&#x30BB;&#x30C3;&#x30C8;</button>
</div>
</header>
@ -54,11 +54,11 @@
<main class="layout">
<section class="stage-wrap">
<canvas id="gameCanvas" aria-label="&#x305F;&#x308A;&#x306A;&#x3044;&#x89B3;&#x5BDF;&#x30AD;&#x30E3;&#x30F3;&#x30D0;&#x30B9;"></canvas>
<div id="mobileModeControls" class="mobile-mode-controls" aria-label="スマホ操作モード">
<button type="button" data-mobile-mode="auto" class="active">自動</button>
<button type="button" data-mobile-mode="camera">カメラ</button>
<button type="button" data-mobile-mode="tool">道具</button>
<button type="button" data-mobile-mode="family">家系図</button>
<div id="mobileModeControls" class="mobile-mode-controls" aria-label="&#x30B9;&#x30DE;&#x30DB;&#x64CD;&#x4F5C;&#x30E2;&#x30FC;&#x30C9;">
<button type="button" data-mobile-mode="auto" class="active">&#x81EA;&#x52D5;</button>
<button type="button" data-mobile-mode="camera">&#x30AB;&#x30E1;&#x30E9;</button>
<button type="button" data-mobile-mode="tool">&#x9053;&#x5177;</button>
<button type="button" data-mobile-mode="family">&#x5BB6;&#x7CFB;&#x56F3;</button>
</div>
<div id="toast" class="toast hidden"></div>
</section>
@ -88,9 +88,9 @@
<div class="stats colony-stats">
<div class="stat"><span>&#x500B;&#x4F53;&#x6570;</span><strong id="statPop">0</strong></div>
<div class="stat"><span>&#x7D2F;&#x8A08;&#x6B7B;&#x4EA1;&#x6570;</span><strong id="statDead">0</strong></div>
<div class="stat"><span>雰囲気</span><strong id="statColonyMood">のんびり</strong></div>
<div class="stat"><span>&#x96F0;&#x56F2;&#x6C17;</span><strong id="statColonyMood">&#x306E;&#x3093;&#x3073;&#x308A;</strong></div>
</div>
<div id="colonyChartTabs" class="chart-tabs"><button data-chart="population" class="active">&#x4EBA;&#x53E3;</button><button data-chart="life">精神</button><button data-chart="environment">&#x74B0;&#x5883;</button><button data-chart="objects">&#x30AA;&#x30D6;&#x30B8;&#x30A7;&#x30AF;&#x30C8;</button></div>
<div id="colonyChartTabs" class="chart-tabs"><button data-chart="population" class="active">&#x4EBA;&#x53E3;</button><button data-chart="life">&#x7CBE;&#x795E;</button><button data-chart="environment">&#x74B0;&#x5883;</button><button data-chart="objects">&#x30AA;&#x30D6;&#x30B8;&#x30A7;&#x30AF;&#x30C8;</button></div>
<div id="colonyChartLegend" class="colony-chart-legend"></div>
<div id="colonyChart" class="colony-chart" aria-label="&#x30B3;&#x30ED;&#x30CB;&#x30FC;&#x306E;&#x63A8;&#x79FB;&#x30B0;&#x30E9;&#x30D5;" role="img"></div>
</div>
@ -183,69 +183,69 @@
</div>
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
<script src="js/version.js?v=15.24.35" defer></script>
<script src="js/event_bus.js?v=15.24.35" defer></script>
<script src="js/performance_manager.js?v=15.24.35" defer></script>
<script src="js/registry_base.js?v=15.24.35" defer></script>
<script src="js/disease_registry.js?v=15.24.35" defer></script>
<script src="js/effect_registry.js?v=15.24.35" defer></script>
<script src="js/sound_pack.js?v=15.24.35" defer></script>
<script src="js/data.js?v=15.24.35" defer></script>
<script src="js/item_visual_registry.js?v=15.24.35" defer></script>
<script src="js/food_registry.js?v=15.24.35" defer></script>
<script src="js/input_mode_manager.js?v=15.24.35" defer></script>
<script src="js/math.js?v=15.24.35" defer></script>
<script src="js/physics_helpers.js?v=15.24.35" defer></script>
<script src="js/assets.js?v=15.24.35" defer></script>
<script src="js/audio.js?v=15.24.35" defer></script>
<script src="js/render.js?v=15.24.35" defer></script>
<script src="js/sim_core.js?v=15.24.35" defer></script>
<script src="js/structures.js?v=15.24.35" defer></script>
<script src="js/items.js?v=15.24.35" defer></script>
<script src="js/ants.js?v=15.24.35" defer></script>
<script src="js/health.js?v=15.24.35" defer></script>
<script src="js/tarinai.js?v=15.24.35" defer></script>
<script src="js/tarinai_action_spec.js?v=15.24.35" defer></script>
<script src="js/tarinai_behavior_state.js?v=15.24.35" defer></script>
<script src="js/tarinai_identity_social.js?v=15.24.35" defer></script>
<script src="js/tarinai_action_state.js?v=15.24.35" defer></script>
<script src="js/tarinai_disease_nest.js?v=15.24.35" defer></script>
<script src="js/tarinai_item_effects.js?v=15.24.35" defer></script>
<script src="js/tarinai_needs_items.js?v=15.24.35" defer></script>
<script src="js/tarinai_social_move_life.js?v=15.24.35" defer></script>
<script src="js/tarinai_render.js?v=15.24.35" defer></script>
<script src="js/world.js?v=15.24.35" defer></script>
<script src="js/world_view.js?v=15.24.35" defer></script>
<script src="js/family_graph.js?v=15.24.35" defer></script>
<script src="js/world_family_social.js?v=15.24.35" defer></script>
<script src="js/world_combat_effects.js?v=15.24.35" defer></script>
<script src="js/world_environment.js?v=15.24.35" defer></script>
<script src="js/world_spatial_budget.js?v=15.24.35" defer></script>
<script src="js/world_ants_system.js?v=15.24.35" defer></script>
<script src="js/weather_system.js?v=15.24.35" defer></script>
<script src="js/world_update.js?v=15.24.35" defer></script>
<script src="js/world_placement_log.js?v=15.24.35" defer></script>
<script src="js/world_bootstrap.js?v=15.24.35" defer></script>
<script src="js/text_catalog.js?v=15.24.35" defer></script>
<script src="js/ui.js?v=15.24.35" defer></script>
<script src="js/ui_log.js?v=15.24.35" defer></script>
<script src="js/ui_helpers.js?v=15.24.35" defer></script>
<script src="js/ui_selected.js?v=15.24.35" defer></script>
<script src="js/snapshot_system.js?v=15.24.35" defer></script>
<script src="js/save_system.js?v=15.24.35" defer></script>
<script src="js/patch_helpers.js?v=15.24.35" defer></script>
<script src="js/freeze_system.js?v=15.24.35" defer></script>
<script src="js/relation_event_system.js?v=15.24.35" defer></script>
<script src="js/ui_layout_dialogs.js?v=15.24.35" defer></script>
<script src="js/ui_tools.js?v=15.24.35" defer></script>
<script src="js/ui_bind.js?v=15.24.35" defer></script>
<script src="js/ui_charts.js?v=15.24.35" defer></script>
<script src="js/ui_family_data.js?v=15.24.35" defer></script>
<script src="js/ui_family_async.js?v=15.24.35" defer></script>
<script src="js/ui_family_layout.js?v=15.24.35" defer></script>
<script src="js/ui_family_paths.js?v=15.24.35" defer></script>
<script src="js/ui_family_render.js?v=15.24.35" defer></script>
<script src="js/main.js?v=15.24.35" defer></script>
<script src="js/debug_tools.js?v=15.24.35" defer></script>
<script src="js/version.js?v=15.24.53" defer></script>
<script src="js/event_bus.js?v=15.24.53" defer></script>
<script src="js/performance_manager.js?v=15.24.53" defer></script>
<script src="js/registry_base.js?v=15.24.53" defer></script>
<script src="js/disease_registry.js?v=15.24.53" defer></script>
<script src="js/effect_registry.js?v=15.24.53" defer></script>
<script src="js/sound_pack.js?v=15.24.53" defer></script>
<script src="js/data.js?v=15.24.53" defer></script>
<script src="js/item_visual_registry.js?v=15.24.53" defer></script>
<script src="js/food_registry.js?v=15.24.53" defer></script>
<script src="js/input_mode_manager.js?v=15.24.53" defer></script>
<script src="js/math.js?v=15.24.53" defer></script>
<script src="js/physics_helpers.js?v=15.24.53" defer></script>
<script src="js/assets.js?v=15.24.53" defer></script>
<script src="js/audio.js?v=15.24.53" defer></script>
<script src="js/render.js?v=15.24.53" defer></script>
<script src="js/sim_core.js?v=15.24.53" defer></script>
<script src="js/structures.js?v=15.24.53" defer></script>
<script src="js/items.js?v=15.24.53" defer></script>
<script src="js/ants.js?v=15.24.53" defer></script>
<script src="js/health.js?v=15.24.53" defer></script>
<script src="js/tarinai.js?v=15.24.53" defer></script>
<script src="js/tarinai_action_spec.js?v=15.24.53" defer></script>
<script src="js/tarinai_behavior_state.js?v=15.24.53" defer></script>
<script src="js/tarinai_identity_social.js?v=15.24.53" defer></script>
<script src="js/tarinai_action_state.js?v=15.24.53" defer></script>
<script src="js/tarinai_disease_nest.js?v=15.24.53" defer></script>
<script src="js/tarinai_item_effects.js?v=15.24.53" defer></script>
<script src="js/tarinai_needs_items.js?v=15.24.53" defer></script>
<script src="js/tarinai_social_move_life.js?v=15.24.53" defer></script>
<script src="js/tarinai_render.js?v=15.24.53" defer></script>
<script src="js/world.js?v=15.24.53" defer></script>
<script src="js/world_view.js?v=15.24.53" defer></script>
<script src="js/family_graph.js?v=15.24.53" defer></script>
<script src="js/world_family_social.js?v=15.24.53" defer></script>
<script src="js/world_combat_effects.js?v=15.24.53" defer></script>
<script src="js/world_environment.js?v=15.24.53" defer></script>
<script src="js/world_spatial_budget.js?v=15.24.53" defer></script>
<script src="js/world_ants_system.js?v=15.24.53" defer></script>
<script src="js/weather_system.js?v=15.24.53" defer></script>
<script src="js/world_update.js?v=15.24.53" defer></script>
<script src="js/world_placement_log.js?v=15.24.53" defer></script>
<script src="js/world_bootstrap.js?v=15.24.53" defer></script>
<script src="js/text_catalog.js?v=15.24.53" defer></script>
<script src="js/ui.js?v=15.24.53" defer></script>
<script src="js/ui_log.js?v=15.24.53" defer></script>
<script src="js/ui_helpers.js?v=15.24.53" defer></script>
<script src="js/ui_selected.js?v=15.24.53" defer></script>
<script src="js/snapshot_system.js?v=15.24.53" defer></script>
<script src="js/save_system.js?v=15.24.53" defer></script>
<script src="js/patch_helpers.js?v=15.24.53" defer></script>
<script src="js/freeze_system.js?v=15.24.53" defer></script>
<script src="js/relation_event_system.js?v=15.24.53" defer></script>
<script src="js/ui_layout_dialogs.js?v=15.24.53" defer></script>
<script src="js/ui_tools.js?v=15.24.53" defer></script>
<script src="js/ui_bind.js?v=15.24.53" defer></script>
<script src="js/ui_charts.js?v=15.24.53" defer></script>
<script src="js/ui_family_data.js?v=15.24.53" defer></script>
<script src="js/ui_family_async.js?v=15.24.53" defer></script>
<script src="js/ui_family_layout.js?v=15.24.53" defer></script>
<script src="js/ui_family_paths.js?v=15.24.53" defer></script>
<script src="js/ui_family_render.js?v=15.24.53" defer></script>
<script src="js/main.js?v=15.24.53" defer></script>
<script src="js/debug_tools.js?v=15.24.53" defer></script>
</body>
</html>

View file

@ -10,8 +10,11 @@ const stainOverlayCache = new Map();
const SPRITE_CACHE_LIMIT = 128;
const STAIN_CACHE_LIMIT = 64;
const INITIAL_IMAGE_IDS = new Set(["smile", "hungry_70", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck"]);
const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "weak", "hurt", "hurt2", "fear", "flee", "flee_fear2", "fear_blue", "fear_cry", "sleep2", "stress_dizzy", "stress_sweat", "intimidate", "birth_ritual", "zunchi_slave", "zunchi_slave_alt", "hungry_70", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck"]);
const SUNBATH_IMAGE_IDS = new Set((typeof SPRITES !== "undefined" ? SPRITES : [])
.map(asset => asset?.id || "")
.filter(id => String(id).startsWith("sunbath")));
const INITIAL_IMAGE_IDS = new Set(["smile", "hungry_70", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "weak", "hurt", "hurt2", "fear", "flee", "flee_fear2", "fear_blue", "fear_cry", "sleep2", "stress_dizzy", "stress_sweat", "intimidate", "birth_ritual", "zunchi_slave", "zunchi_slave_alt", "hungry_70", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
function trimCanvasCache(cache, limit = 128) {
while (cache.size > limit) {

View file

@ -336,6 +336,7 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
if (!ctx || !type) return false;
const w = Math.max(1, Number(opts.width || ctx.canvas?.width || 72));
const h = Math.max(1, Number(opts.height || ctx.canvas?.height || 72));
const watermark = !!opts.watermark;
const localR = previewItemRadiusFor(type);
const item = Object.create(Item.prototype);
Object.assign(item, {
@ -371,7 +372,10 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
if (type === "water") item.amount = 50;
ctx.save();
ctx.clearRect(0, 0, w, h);
ctx.translate(w / 2, h / 2 + (type === "signboard" ? 11 : type === "genkotsu" ? 12 : type === "fence_v" || type === "fence_h" ? 1 : 4));
const baseOffsetY = type === "signboard" ? 11 : type === "genkotsu" ? 12 : type === "fence_v" || type === "fence_h" ? 1 : 4;
const centerX = watermark ? w * 0.78 : w / 2;
const centerY = watermark ? (h * 0.80 + baseOffsetY * 0.15) : (h / 2 + baseOffsetY);
ctx.translate(centerX, centerY);
const scaleMap = {
genkotsu: 0.52,
bed: 0.80,
@ -386,7 +390,14 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
oshibyo: 0.92,
firecracker: 0.90,
};
ctx.scale(scaleMap[type] || 0.92, scaleMap[type] || 0.92);
let scale = scaleMap[type] || 0.92;
if (isConsumableSpriteType(type)) scale *= 1.95;
else if (["grass", "water", "water_bowl", "zunda_juice"].includes(type)) scale *= 1.85;
else if (type === "duplicator") scale *= 1.55;
else if (["signboard", "bed", "nest_box", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "firecracker", "genkotsu"].includes(type)) scale *= 1.08;
else if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) scale *= 1.35;
if (watermark) scale *= 1.42;
ctx.scale(scale, scale);
try {
item.draw(ctx, 0, typeof getLightingState === "function" ? getLightingState(globalThis.world) : null);
} catch (err) {
@ -472,7 +483,7 @@ class Item {
this.stageTimer = 0;
this.fertility = 0;
this.freshness = 1;
this.spawnGrace = 1.2;
this.spawnGrace = 10.0;
this.zunchiVariant = stableUnit(this.id, "zunchi-variant") < 0.5 ? "zunchi" : "zunchi_02";
}
if (type === "ant_nest") {
@ -955,7 +966,7 @@ class Item {
this.deletable = false;
this.pinTargetId = t.id;
const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1);
const visualSide = faceDir;
const visualSide = isOshibyo ? -faceDir : faceDir;
this.pinVisualSide = visualSide;
this.pinAttachAngle = isOshibyo ? visualSide * 0.36 : Math.atan2(dy || -1, dx || visualSide);
this.pinAttachDistance = isOshibyo ? (t.radius || 16) * 0.72 : clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74);
@ -1004,7 +1015,7 @@ class Item {
t.stuckPushpinId = this.id;
this.deletable = false;
const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1);
const visualSide = faceDir;
const visualSide = isOshibyo ? -faceDir : faceDir;
this.pinVisualSide = visualSide;
this.x = isOshibyo ? t.x + visualSide * (t.radius || 16) * 0.72 : t.x + visualSide * (t.radius || 16) * 0.36;
this.y = isOshibyo ? t.y + (t.radius || 16) * 0.42 : t.y - (t.radius || 16) * 0.04;
@ -1171,31 +1182,31 @@ class Item {
updateZunchi(dt, worldRef) {
const rainy = worldRef.weather === "light_rain";
const rainBoost = rainy ? 1.95 : 1;
const decayBoost = rainy ? 2.15 : 1;
const rainBoost = rainy ? 1.28 : 1;
const decayBoost = rainy ? 1.35 : 1;
let waterBoost = 0;
const nearby = worldRef.nearbyItems ? worldRef.nearbyItems(this.x, this.y, 84) : (worldRef.items || []);
for (const it of nearby) {
if (it.type !== "water") continue;
waterBoost += clamp(1 - distXY(this.x, this.y, it.x, it.y) / 80, 0, 1);
}
this.stageTimer += dt * 2.0 * (rainBoost + waterBoost * 1.3);
this.stageTimer += dt * 0.70 * (rainBoost + waterBoost * 0.65);
const decayDt = (this.spawnGrace || 0) > 0 ? 0 : dt;
if (this.stage === "fresh") {
this.freshness = clamp(1 - this.stageTimer / 110, 0, 1);
this.amount -= decayDt * 0.05 * decayBoost;
this.amount -= decayDt * 0.018 * decayBoost;
if (this.stageTimer > 110) { this.stage = "dry"; this.stageTimer = 0; }
} else if (this.stage === "dry") {
this.freshness = clamp(0.55 - this.stageTimer / 220, 0.20, 0.55);
this.amount -= decayDt * 0.09 * decayBoost;
this.amount -= decayDt * 0.032 * decayBoost;
if (this.stageTimer > 150) { this.stage = "decomposing"; this.stageTimer = 0; this.fertility = 0.35; }
} else if (this.stage === "decomposing") {
this.fertility = clamp(this.fertility + dt * 0.015, 0, 1);
this.amount -= decayDt * 0.12 * decayBoost;
this.amount -= decayDt * 0.046 * decayBoost;
if (this.stageTimer > 190) { this.stage = "fertile_soil"; this.stageTimer = 0; this.amount = Math.min(this.amount, 140); }
} else if (this.stage === "fertile_soil") {
this.fertility = clamp(1 - this.stageTimer / 360, 0, 1);
this.amount -= decayDt * 0.26 * decayBoost;
this.amount -= decayDt * 0.090 * decayBoost;
}
}
get dead() {
@ -1522,6 +1533,7 @@ class Item {
} else if (this.type === "signboard") {
ctx.save();
const boardGrad = ctx.createLinearGradient(0, -this.r * 1.28, 0, this.r * 0.12);
const boardHalfW = this.r * 1.20;
boardGrad.addColorStop(0, styleNight ? "#d5cab8" : "#fff8e8");
boardGrad.addColorStop(0.55, styleNight ? "#c0b29a" : "#f3e3c6");
boardGrad.addColorStop(1, styleNight ? "#aa9a82" : "#dfc397");
@ -1533,7 +1545,7 @@ class Item {
ctx.stroke();
ctx.fillStyle = boardGrad;
ctx.strokeStyle = styleNight ? "#63513c" : "#a77e48";
roundedRect(ctx, -this.r * 1.36, -this.r * 1.28, this.r * 2.72, this.r * 1.38, this.r * 0.18);
roundedRect(ctx, -boardHalfW, -this.r * 1.28, boardHalfW * 2, this.r * 1.38, this.r * 0.18);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = styleNight ? "rgba(103,83,58,0.36)" : "rgba(167,126,72,0.38)";
@ -1541,12 +1553,12 @@ class Item {
for (let i = 0; i < 3; i++) {
const y = -this.r * 1.02 + i * this.r * 0.36;
ctx.beginPath();
ctx.moveTo(-this.r * 1.12, y);
ctx.quadraticCurveTo(-this.r * 0.12, y + Math.sin(this.seed + i) * 2, this.r * 1.12, y + Math.cos(this.seed + i) * 2);
ctx.moveTo(-boardHalfW * 0.82, y);
ctx.quadraticCurveTo(-this.r * 0.12, y + Math.sin(this.seed + i) * 2, boardHalfW * 0.82, y + Math.cos(this.seed + i) * 2);
ctx.stroke();
}
ctx.fillStyle = "rgba(255,255,255,0.28)";
roundedRect(ctx, -this.r * 1.12, -this.r * 1.10, this.r * 2.24, this.r * 0.20, this.r * 0.08);
roundedRect(ctx, -boardHalfW * 0.82, -this.r * 1.10, boardHalfW * 1.64, this.r * 0.20, this.r * 0.08);
ctx.fill();
const raw = String(this.text || "").replace(/\r/g, "").trim();
if (raw) {

View file

@ -814,23 +814,7 @@ function render() {
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
}
if (density > 0.45) {
ctx.save();
ctx.lineWidth = 1.15;
ctx.setLineDash([4, 4]);
ctx.strokeStyle = light > 0.42 ? "rgba(115, 151, 86, 0.32)" : "rgba(202, 220, 255, 0.22)";
for (const t of world.tarinai) {
if (!isEntityVisibleInRect(t, visibleRect, 80)) continue;
const p = t.parentToFollow?.();
if (!p || !t.juvenile || t.dead || p.dead || dist(t, p) > 96 || !isEntityVisibleInRect(p, visibleRect, 80)) continue;
ctx.beginPath();
ctx.moveTo(t.x, t.y);
ctx.quadraticCurveTo((t.x + p.x) * 0.5, Math.min(t.y, p.y) - 10, p.x, p.y);
ctx.stroke();
}
ctx.setLineDash([]);
ctx.restore();
}
// Parent-follow guide lines intentionally disabled; they looked like stray lines between Tarinai.
for (const entry of renderStack.layered) {
if (isEntityVisibleInRect(entry.entity, visibleRect)) entry.entity.draw(ctx, world.time, lighting);

View file

@ -349,7 +349,7 @@
function createSnapshot(worldRef = global.world) {
if (!worldRef) throw new Error("world is not ready");
const tarinaiLive = (worldRef.tarinai || []).filter(Boolean);
const tarinaiLive = (worldRef.tarinai || []).filter(t => t && !t.dead);
const itemsLive = (worldRef.items || []).filter(Boolean).filter(it => it && !it.dead);
const tarinaiIndex = new Map();
const familyIndex = new Map();

View file

@ -22,7 +22,7 @@
this.usedCount = 0;
this.x = x;
this.y = y;
this.r = definition.type === "plushie" ? 13 : (definition.type === "grass_bed" ? 18 : 24);
this.r = definition.type === "plushie" ? 6.5 : (definition.type === "grass_bed" ? 18 : 24);
this.amount = this.hp;
this.seed = Math.random() * 1000;
this.roles = clonePlain(definition.roles);
@ -54,9 +54,21 @@
return;
}
this.x = owner.x;
this.y = owner.y - Math.max(18, (owner.radius || 24) * 0.78);
this.y = owner.y - Math.max(15, (owner.radius || 24) * 0.70);
if (owner.needs) owner.needs.fulfill = Math.max(0, (owner.needs.fulfill || 0) - dt * 2.8);
}
if (this.type === "grass_bed") {
const owner = this.owner(worldRef);
if (!owner) {
this.unownedDecayTimer = (this.unownedDecayTimer || 0) + dt;
const rainBoost = worldRef?.weather === "light_rain" ? 1.65 : 1;
const ageBoost = 1 + Math.min(1.8, this.unownedDecayTimer / 360);
this.damage(dt * 0.070 * rainBoost * ageBoost, worldRef, null);
if (this.dead) return;
} else {
this.unownedDecayTimer = 0;
}
}
this.amount = Math.max(0.001, this.hp);
}

View file

@ -46,8 +46,8 @@ const TARINAI_ACTION_LOCK_SECONDS = {
use_medicine: 2.4,
rest_to_recover: 6.0,
sunbath: 10.5,
panic_escape: 4.2,
flee: 3.4,
panic_escape: 2.1,
flee: 1.7,
hide_at_owned_structure: 6.4,
approach_friend: 6.4,
approach_parent_or_child: 7.0,
@ -70,10 +70,10 @@ const TARINAI_ACTION_PRIORITY = {
hide_at_owned_structure: 90,
use_medicine: 86,
rest_to_recover: 84,
sunbath: 83,
sleep_in_bed: 80,
sleep_build_grass_bed: 79,
sleep_anywhere: 78,
sunbath: 91,
sleep_in_bed: 86,
sleep_build_grass_bed: 85,
sleep_anywhere: 84,
birth_ritual: 72,
approach_mate: 68,
approach_parent_or_child: 66,
@ -895,12 +895,23 @@ function effectiveFoodTypeForItem(item) {
return item.type === "duplicator" ? String(item.storedFoodType || "") : String(item.type || "");
}
function isDuplicatorFoodSource(item) {
return !!item && item.type === "duplicator";
}
function duplicatorPoopMultiplier(foodType = "") {
if (foodType === "sweet") return 0.035;
if (foodType === "food") return 0.040;
if (foodType === "grass") return 0.028;
return 0.042;
}
function effectiveFoodRoleMatches(item, role) {
const type = effectiveFoodTypeForItem(item);
if (!type) return false;
if (role === "food") return ["sweet", "food", "grass", "ant_corpse", "zunchi"].includes(type) || (typeof isServingFoodType === "function" && isServingFoodType(type));
if (role === "drink") return type === "water" || type === "water_bowl" || type === "zunda_juice";
if (role === "medicine") return type === "sweet" || type === "water" || type === "water_bowl" || type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type));
if (role === "medicine") return type === "sweet" || type === "water" || type === "water_bowl" || type === "zunda_juice" || type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type));
return false;
}
@ -941,6 +952,8 @@ function foodPriorityRank(tarinai, item) {
const socialNeed = Number(tarinai?.needRaw?.social ?? tarinai?.needs?.social ?? 0) || 0;
const healthNeed = Number(tarinai?.needRaw?.health ?? tarinai?.needs?.health ?? 0) || 0;
if (type === "sweet") return 4100 + Math.max(0, healthNeed - 40) * 6;
if (type === "water" || type === "water_bowl") return 3650 + Math.max(0, healthNeed - 42) * 7;
if (type === "zunda_juice") return 3720 + Math.max(0, healthNeed - 42) * 6;
if (type === "love_mochi") return 3300 + Math.max(0, socialNeed - 45) * 7;
if (type === "fight_mochi") return 2600 + Math.max(0, Number(tarinai?.fightMochiTimer || 0)) * 4;
if (type === "sleep_drug") return need === "sleep" ? 3600 : 2450;
@ -956,11 +969,48 @@ function foodPriorityScore(tarinai, item, distance = 0) {
let rank = foodPriorityRank(tarinai, item);
if (rank < 0) return -Infinity;
const hunger = Number(tarinai?.hunger || 0) || 0;
if (effectiveFoodTypeForItem(item) === "zunchi" && !tarinai?.isZunchiSlave && hunger < 88) rank -= 900;
if (item?.type === "zunchi") {
if (zunchiEatingBlocked(tarinai, item, tarinai?.world)) return -Infinity;
if (!tarinai?.isZunchiSlave) rank -= 1400;
if (effectiveFoodTypeForItem(item) === "zunchi" && !tarinai?.isZunchiSlave && hunger < 104) rank -= 1200;
}
if (item?.type === "duplicator") rank += 85;
return rank - Math.max(0, Number(distance) || 0) * 0.65;
}
function hasBetterFoodThanZunchiNearby(tarinai, world, maxDist = 180) {
if (!tarinai || !world) return false;
for (const it of world.nearbyItems?.(tarinai.x, tarinai.y, maxDist) || world.items || []) {
if (!it || it.dead || it.type === "zunchi") continue;
if (tarinai.shouldAvoidTarget?.(it)) continue;
const type = effectiveFoodTypeForItem(it);
if (!type) continue;
if (["sweet", "food", "grass", "water_bowl", "zunda_juice", "ant_corpse"].includes(type) || (typeof isServingFoodType === "function" && isServingFoodType(type))) {
if (it.type === "grass" && (it.amount || 0) <= 0) continue;
if (typeof tarinai.foodItemHasServingLeft === "function" && ["sweet", "food", "zunda_juice"].includes(type) && !tarinai.foodItemHasServingLeft(it)) continue;
return true;
}
}
return false;
}
function zunchiEatingBlocked(tarinai, item, world) {
if (!tarinai || !item || item.type !== "zunchi") return false;
const itemAge = Number(item.age || 0) || 0;
const worldAge = Number.isFinite(item.producedAt) ? Math.max(0, (world?.time || 0) - item.producedAt) : itemAge;
const protectedAge = Math.max(itemAge, worldAge);
const protectedUntil = Number(item.eatProtectedUntil || 0) || 0;
if (protectedUntil > 0 && (world?.time || 0) < protectedUntil) return true;
const foodNeed = Number(tarinai.needRaw?.food ?? tarinai.needs?.food ?? 0) || 0;
const hunger = Number(tarinai.hunger || 0) || 0;
// 排泄直後のずんちは観察・接触対象であり、通常食として即消費させない。
// ずんちどれいも数秒は待つ。通常個体は極限飢餓でも最低90秒は食べない。
const grace = tarinai.isZunchiSlave ? 18 : 90;
if (protectedAge < grace) return true;
if (!tarinai.isZunchiSlave && item.stage === "fresh" && protectedAge < 150 && foodNeed < 98 && hunger < 108) return true;
return false;
}
function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
let best = null, bestScore = Infinity;
const hunger = Number(tarinai?.hunger || 0) || 0;
@ -1154,16 +1204,16 @@ function circadianSleepPhase(world) {
function updateCircadianSleepPressure(tarinai, world, dt = 0) {
const phase = circadianSleepPhase(world);
if (!Number.isFinite(tarinai.circadianSleepPressure)) {
tarinai.circadianSleepPressure = phase.night ? rand(58, 78) : (phase.afternoon ? rand(8, 18) : rand(0, 5));
tarinai.circadianSleepPressure = phase.night ? rand(68, 88) : (phase.afternoon ? rand(8, 18) : rand(0, 5));
}
const step = Math.max(0, Number(dt) || 0);
if (phase.night) {
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure + step * 1.18, 0, 78);
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure + step * 1.85, 0, 94);
} else if (phase.afternoon) {
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure + step * 0.16, 0, 26);
} else {
const decay = (tarinai.state === "sleep" || tarinai.sleeping) ? 1.85 : 0.42;
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure - step * decay, 0, 78);
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure - step * decay, 0, 94);
}
return { ...phase, pressure: tarinai.circadianSleepPressure };
}
@ -1465,12 +1515,13 @@ function createSunbathActionSpec() {
phrase: "日光浴している",
weight: 32,
timerBacked: true,
canStart(t) {
return (Number(t?.needRaw?.health ?? t?.needs?.health ?? 0) || 0) >= needThreshold("health", "start")
&& !!t?.canStartSunbath?.();
canStart(t, world, ctx = {}) {
const healthNeed = Number(t?.needRaw?.health ?? t?.needs?.health ?? 0) || 0;
return (ctx.leisure || healthNeed >= needThreshold("health", "start"))
&& !!t?.canStartSunbath?.({ leisure: !!ctx.leisure });
},
start(t) {
return !!t?.startSunbath?.();
start(t, world, ctx = {}) {
return !!t?.startSunbath?.({ leisure: !!ctx.leisure });
},
tick(t) {
return (t?.sunbathTimer || 0) > 0.04 ? true : "finished";
@ -1871,8 +1922,12 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) {
const foodType = effectiveFoodTypeForItem(target);
const labelFor = type => typeof toolLabel === "function" ? toolLabel(type || target.type) : "\u98df\u3079\u7269";
const effectiveDt = Math.max(0.10, Number(dt) || 0.12);
if (isDuplicatorFoodSource(target) && (t.eatCooldown || 0) > 0.04) {
t.setActionState?.(role === "medicine" ? "seek_food" : "eat", { target, reason: `${labelFor(foodType)}\u3092\u98df\u3079\u3066\u3044\u308b`, sleeping: false });
return true;
}
if (role === "drink" && (target.type === "water" || target.type === "water_bowl")) {
if ((role === "drink" || role === "medicine") && (target.type === "water" || target.type === "water_bowl")) {
const text = target.type === "water_bowl" ? "\u6c34\u306e\u76bf\u3067\u6c34\u3092\u98f2\u3093\u3067\u3044\u308b" : "\u6c34\u3092\u98f2\u3093\u3067\u3044\u308b";
t.setActionState?.("seek_water", { target, reason: text, sleeping: false });
t.thought = text;
@ -1882,7 +1937,7 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) {
audio.play?.("sfx_drink", { category: "ops", minGap: 0.20 });
t.nextDrinkSound = (world.time || 0) + 0.72;
}
applyNeedRelief(t, { food: -4, health: -1 });
applyNeedRelief(t, role === "medicine" ? { food: -2, health: -5, safety: -1 } : { food: -4, health: -1 });
return true;
}
@ -1916,7 +1971,12 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) {
if (t.explosionDisease) t.recoverExplosionDisease?.("\u305a\u3093\u3060\u9905\u3067\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f");
if (t.fightDisease) t.recoverFightDisease?.("\u305a\u3093\u3060\u9905\u3067\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f");
}
t.makePoop?.(eating * (foodType === "sweet" ? 0.18 : 0.20));
const fromDuplicator = isDuplicatorFoodSource(target);
t.makePoop?.(eating * (fromDuplicator ? duplicatorPoopMultiplier(foodType) : (foodType === "sweet" ? 0.18 : 0.20)));
if (fromDuplicator) {
t.mealCooldownUntil = Math.max(t.mealCooldownUntil || 0, (world.time || 0) + 1.2);
t.intentLockTimer = Math.max(0, Math.min(t.intentLockTimer || 0, 0.18));
}
return true;
}
@ -1937,7 +1997,10 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) {
if (target.type === "zunchi" && (target.amount || 0) > 0) {
const foodNeed = Number(t.needRaw?.food ?? t.needs?.food ?? 0) || 0;
if (!t.isZunchiSlave && foodNeed < 82 && (t.hunger || 0) < 88) return false;
if (zunchiEatingBlocked(t, target, world)) return false;
if (!t.isZunchiSlave && hasBetterFoodThanZunchiNearby(t, world, 240)) return false;
if (!t.isZunchiSlave && target.stage === "fresh" && (target.age || 0) < 180 && foodNeed < 98 && (t.hunger || 0) < 108) return false;
if (!t.isZunchiSlave && foodNeed < 94 && (t.hunger || 0) < 104) return false;
const servingScale = Number.isFinite(target.foodServingScale) ? target.foodServingScale : (target.toolSize === "large" ? 1.18 : (target.toolSize === "small" ? 0.82 : 1));
const bite = Math.min(target.amount, (t.isZunchiSlave ? rand(4.8, 7.4) : rand(3.4, 5.8)) * servingScale);
if (!(bite > 0)) return false;
@ -2075,7 +2138,7 @@ if (typeof window !== "undefined") Object.assign(window, { TARINAI_ACTION_DEFINI
function structureBuildProbe(type, x, y) {
const def = globalThis.StructureRegistry?.get?.(type) || {};
const r = type === "plushie" ? 13 : (type === "grass_bed" ? 18 : 24);
const r = type === "plushie" ? 6.5 : (type === "grass_bed" ? 18 : 24);
return { type, x, y, r, dead: false, isStructure: true, roles: def.roles || {}, ownerId: "" };
}
@ -2348,6 +2411,7 @@ function shouldStartNeedAction(tarinai, need, needs) {
const hunger = Number(tarinai?.hunger || 0) || 0;
if (inMealCooldown && hunger < 82 && value < 78 && !((typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(tarinai) : tarinai?.behavior)?.forced)) return false;
}
if (need === "sleep" && circadianSleepPhase(tarinai?.world).night && value >= Math.max(44, needThreshold("sleep", "start") - 16)) return true;
if (value >= needThreshold(need, "start")) return true;
if ((typeof getTarinaiBehaviorNeed === "function" ? getTarinaiBehaviorNeed(tarinai) : tarinai?.behavior?.need) === need && value >= needThreshold(need, "continue")) return true;
return false;
@ -2628,8 +2692,15 @@ function protectSleepSession(tarinai, world, needs) {
},
spriteId() {
const next = this.rawSpriteId();
const now = this.world?.time || 0;
if (this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04) {
this.preloadSunbathSprites?.();
const sunbathId = this.sunbathSpriteId ? this.sunbathSpriteId() : (String(this.sunbathSpriteVariant || "").startsWith("sunbath") ? this.sunbathSpriteVariant : "sunbath_1");
this.visibleSpriteId = sunbathId;
this.spriteLockUntil = now + 0.55;
return sunbathId;
}
const next = this.rawSpriteId();
const isCurrentIntimidator = this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04;
const isCurrentIntimidated = !isCurrentIntimidator && this.intimidatedTimer > 0.04;
const forceImmediate = String(next || "").startsWith("zunchi_slave") || next === "birth_ritual" || next === "intimidate" || isCurrentIntimidator || isCurrentIntimidated || (this.visibleSpriteId === "intimidate" && next !== "intimidate");
@ -2653,26 +2724,69 @@ function protectSleepSession(tarinai, world, needs) {
return (this.needs?.social || 0) < 70 || this.goodMode === "smile" || this.shouldApplyPersonalityBehavior("sociability", 1);
},
sunbathSpriteCandidates() {
const sprites = typeof SPRITES !== "undefined" ? SPRITES : (typeof globalThis !== "undefined" ? globalThis.SPRITES : []);
const ids = Array.isArray(sprites)
? sprites.map(s => s?.id).filter(id => typeof id === "string" && id.startsWith("sunbath"))
: [];
const ordered = ids.length ? ids : ["sunbath_1", "sunbath_2", "sunbath_3"];
return ordered.slice().sort((a, b) => {
const an = Number(String(a).match(/(\d+)$/)?.[1] || 0);
const bn = Number(String(b).match(/(\d+)$/)?.[1] || 0);
return an - bn || String(a).localeCompare(String(b));
});
},
preloadSunbathSprites() {
const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"];
for (const id of ids) {
if (typeof ensureImage === "function") ensureImage(id);
else if (globalThis.TarinaiAssets?.ensureImage) globalThis.TarinaiAssets.ensureImage(id);
}
},
normalizeSunbathSpriteId(value = "") {
if (typeof value === "number" && Number.isFinite(value)) return `sunbath_${Math.max(1, Math.floor(value))}`;
const str = String(value || "");
if (/^sunbath_\d+$/.test(str) || str.startsWith("sunbath")) return str;
return "";
},
randomSunbathSpriteId(exclude = "") {
const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"];
if (!ids.length) return "sunbath_1";
const normalizedExclude = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(exclude) : String(exclude || "");
const choices = ids.length > 1 ? ids.filter(id => id !== normalizedExclude) : ids;
return choices[Math.floor(Math.random() * choices.length)] || ids[0] || "sunbath_1";
},
sunbathSpriteId() {
if (!this.sunbathSpriteVariant) this.sunbathSpriteVariant = 1 + Math.floor(Math.random() * 3);
return `sunbath_${this.sunbathSpriteVariant}`;
const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"];
const current = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(this.sunbathSpriteVariant) : String(this.sunbathSpriteVariant || "");
if (!current || !ids.includes(current)) this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId() : "sunbath_1";
return this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(this.sunbathSpriteVariant) : String(this.sunbathSpriteVariant || "sunbath_1");
},
isSunbathTime() {
const weatherOk = (this.world?.weather || "") === "sunny";
const weather = this.world?.weather || "";
const weatherOk = weather === "sunny" || weather === "cloudy";
const hour = this.world?.hourOfDay ? this.world.hourOfDay() : ((((this.world?.time || 0) / Math.max(1, CONFIG.dayLength || 120)) * 24) % 24);
return weatherOk && hour >= 6 && hour <= 13.5;
},
canStartSunbath() {
canStartSunbath(options = {}) {
if (this.dead || this.sleeping) return false;
if (!Number.isFinite(this.x) || !Number.isFinite(this.y)) return false;
if (!Number.isFinite(this.world?.w) || !Number.isFinite(this.world?.h)) return false;
if ((this.entryTimer || 0) > 0.04) return false;
if (!this.isSunbathTime()) return false;
if ((this.sunbathCooldown || 0) > 0) return false;
if (this.stuckPushpinId || this.currentLodgedPin?.()) return false;
if (["fight", "panic", "cursor_enemy", "eat", "seek_food", "seek_bed", "sleep", "birth_ritual", "ant_attack", "intimidate", "ant_intimidate"].includes(this.state)) return false;
if (this.hunger > 68 || this.energy < 18) return false;
if (["fight", "panic", "cursor_enemy", "eat", "seek_food", "seek_water", "seek_bed", "sleep", "birth_ritual", "ant_attack", "intimidate", "ant_intimidate"].includes(this.state)) return false;
if (this.hunger > (options.leisure ? 58 : 68) || this.energy < (options.leisure ? 28 : 18)) return false;
const healthNeed = Number(this.needRaw?.health ?? this.needs?.health ?? 0) || 0;
return healthNeed >= needThreshold("health", "start");
return options.leisure ? healthNeed < needThreshold("health", "start") : healthNeed >= needThreshold("health", "start");
},
tryRecoverBySunbath() {
@ -2700,17 +2814,28 @@ function protectSleepSession(tarinai, world, needs) {
return true;
},
startSunbath() {
if (!this.canStartSunbath()) return false;
this.setActionState?.("sunbath", { target: null, reason: "\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b", sleeping: false });
startSunbath(options = {}) {
if (!this.canStartSunbath(options)) return false;
const pad = Math.max(28, CONFIG.worldPadding || 30);
const safeW = Math.max(pad * 2 + 1, Number(this.world?.w || 1000) || 1000);
const safeH = Math.max(pad * 2 + 1, Number(this.world?.h || 720) || 720);
this.x = clamp(Number.isFinite(this.x) ? this.x : safeW * 0.5, pad, safeW - pad);
this.y = clamp(Number.isFinite(this.y) ? this.y : safeH * 0.5, pad, safeH - pad);
this.entryTimer = 0;
const reason = options.leisure ? "\u5c11\u3057\u843d\u3061\u7740\u3044\u3066\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b" : "\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b";
this.setActionState?.("sunbath", { target: null, reason, sleeping: false });
this.preloadSunbathSprites?.();
this.sunbathTimer = Math.max(6, (CONFIG.dayLength || 120) / 12);
this.sunbathCooldown = 0;
this.sunbathFrameTimer = 0;
this.sunbathSpriteVariant = 1 + Math.floor(Math.random() * 3);
this.sunbathFrameTimer = 0.5;
this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId() : `sunbath_${1 + Math.floor(Math.random() * 3)}`;
this.sunbathSpriteFrameKey = Math.floor((this.world?.time || 0) / 0.5);
this.visibleSpriteId = this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant;
this.spriteLockUntil = 0;
this.sunbathRecoveryChecked = false;
this.vx = 0;
this.vy = 0;
if (typeof setLiveActionText === "function") setLiveActionText(this, { need: "health", subNeed: "sunbath", actionId: "sunbath", actionLabel: "\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b", reasonText: "\u8abf\u5b50\u304c\u60aa\u3044\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002", target: null, phase: "perform", source: "behavior" });
if (typeof setLiveActionText === "function") setLiveActionText(this, { need: options.leisure ? "fulfill" : "health", subNeed: "sunbath", actionId: "sunbath", actionLabel: "\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b", reasonText: options.leisure ? "\u4f59\u88d5\u304c\u3042\u308b\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002" : "\u8abf\u5b50\u304c\u60aa\u3044\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002", target: null, phase: "perform", source: "behavior" });
if ((this.world?.time || 0) >= (this.nextBubbleAt || 0) && Math.random() < 0.45) this.bubble("\u307d\u304b\u3063", 4.0, "rgba(112,86,36,0.78)");
return true;
},
@ -2722,8 +2847,11 @@ function protectSleepSession(tarinai, world, needs) {
this.vx = 0;
this.vy = 0;
if ((this.sunbathFrameTimer || 0) <= 0) {
this.sunbathSpriteVariant = 1 + Math.floor(Math.random() * 3);
this.sunbathFrameTimer = 0.4;
const prev = this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant;
this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId(prev) : `sunbath_${1 + Math.floor(Math.random() * 3)}`;
this.visibleSpriteId = this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant;
this.spriteLockUntil = (this.world?.time || 0) + 0.55;
this.sunbathFrameTimer = 0.5;
}
this.energy = clamp((this.energy || 0) + dt * 0.03, 0, 100);
if (!this.isSunbathTime() || this.hunger > 82 || this.energy < 8 || this.state === "panic" || this.stuckPushpinId || this.currentLodgedPin?.()) this.sunbathTimer = 0;
@ -3201,6 +3329,12 @@ function protectSleepSession(tarinai, world, needs) {
const picked = choosePriorityNeedAction(needs, this, this.world);
if (!picked?.action) {
const sunbathAction = actionById("sunbath");
if (sunbathAction && this.canStartSunbath?.({ leisure: true }) && Math.random() < Math.max(0.006, dt * 0.08)) {
const choice = { need: "fulfill", tiedNeeds: ["fulfill"], max: Number(needs?.fulfill || 0) || 0, priority: actionPriority(sunbathAction) };
startNeedAction(this, this.world, choice, sunbathAction, "\u4f59\u88d5\u304c\u3042\u308b\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002", { leisure: true, priority: actionPriority(sunbathAction), lockSeconds: 6.0 });
return;
}
const choice = chooseTopNeedRandom(needs, this, this.world);
setActiveBehavior(this, { id: "idle", need: choice.need, tiedNeeds: [...(choice.tiedNeeds || [])], label: "\u5f85\u3063\u3066\u3044\u308b", reasonText: "\u5c11\u3057\u843d\u3061\u7740\u3044\u3066\u3044\u308b\u3002", state: "idle", phase: "idle", startedAt: this.world?.time || 0 });
clearActiveBehavior(this);
@ -3230,7 +3364,7 @@ function protectSleepSession(tarinai, world, needs) {
const side = this.facingDir();
const backX = this.x - side * this.radius * rand(0.72, 0.96);
const backY = this.y + this.radius * rand(0.25, 0.48);
this.world.spawnZunchi(backX, backY);
this.world.spawnZunchi(backX, backY, this);
const now = this.world?.time || 0;
if (now >= (this.nextPoopBubbleAt || -999)) {
this.nextPoopBubbleAt = now + 2.8;
@ -3250,7 +3384,7 @@ function protectSleepSession(tarinai, world, needs) {
const wantsMedicine = healthActionActive || healthNeed >= needThreshold("health", "start");
const canBite = !this.isZunchiSlave && this.eatCooldown <= 0 && mealReady && wantsFood && this.hunger > 18;
const canSweetBite = !this.isZunchiSlave && this.eatCooldown <= 0 && mealReady && (wantsFood || wantsMedicine) && this.hunger > 7;
const canZunchiBite = this.eatCooldown <= 0 && (this.isZunchiSlave ? this.hunger > 16 : (this.hunger > 88 || foodNeed > 82));
const canZunchiBite = this.eatCooldown <= 0 && (this.isZunchiSlave ? this.hunger > 16 : (this.hunger > 108 || foodNeed > 98)) && (this.isZunchiSlave || !hasBetterFoodThanZunchiNearby(this, this.world, 240));
const quality = this.world?.performanceQuality?.() || {};
const scanLimit = Math.max(3, Number(quality.itemScanLimit || 28) || 28);
const nearbyItems = this.world.nearbyItems(this.x, this.y, this.radius + 46);
@ -3297,7 +3431,9 @@ function protectSleepSession(tarinai, world, needs) {
this.world.spawnEatEffect(mouth.x, mouth.y, foodType === "sweet" ? "#78b957" : "#f1dfb7");
}
this.lastMealColor = foodType === "sweet" ? "#78b957" : "#f1dfb7";
this.makePoop(eating * (foodType === "sweet" ? 0.18 : 0.22));
const fromDuplicator = isDuplicatorFoodSource(it);
this.makePoop(eating * (fromDuplicator ? duplicatorPoopMultiplier(foodType) : (foodType === "sweet" ? 0.18 : 0.22)));
if (fromDuplicator) this.mealCooldownUntil = Math.max(this.mealCooldownUntil || 0, this.world.time + 1.2);
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`, null, { participants: [this] });
@ -3342,7 +3478,9 @@ function protectSleepSession(tarinai, world, needs) {
const mouth = this.mouthPosition(it);
this.world.spawnEatEffect(mouth.x, mouth.y, foodType === "love_mochi" ? "#ff7bab" : (foodType === "fight_mochi" ? "#e07c43" : (foodType === "sleep_drug" ? "#b89cff" : (this.lastMealColor || "#41aa66"))));
}
this.makePoop(eating * 0.20);
const fromDuplicator = isDuplicatorFoodSource(it);
this.makePoop(eating * (fromDuplicator ? duplicatorPoopMultiplier(foodType) : 0.20));
if (fromDuplicator) this.mealCooldownUntil = Math.max(this.mealCooldownUntil || 0, this.world.time + 1.2);
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${toolLabel(foodType || it.type)}\u3092\u3057\u3070\u3089\u304f\u5473\u308f\u3063\u305f\u3002`, "food", { participants: [this] });
@ -3406,7 +3544,8 @@ function protectSleepSession(tarinai, world, needs) {
if (touch > 0.08) this.adjustPersonality("neuroticism", -dt * touch * 0.0045, "after repeated contact with poop.");
}
this.zunchiStain = clamp((this.zunchiStain || 0) + touch * dt * (this.isZunchiSlave ? 5.2 : 9.5), 0, 100);
if (canZunchiBite) {
const zunchiTooFresh = zunchiEatingBlocked(this, it, this.world) || (!this.isZunchiSlave && it.stage === "fresh" && (it.age || 0) < 180 && foodNeed < 98 && (this.hunger || 0) < 108);
if (canZunchiBite && !zunchiTooFresh) {
const servingScale = Number.isFinite(it.foodServingScale) ? it.foodServingScale : (it.toolSize === "large" ? 1.18 : (it.toolSize === "small" ? 0.82 : 1));
const bite = Math.min(it.amount, (this.isZunchiSlave ? rand(4.8, 7.4) : rand(3.4, 5.8)) * servingScale);
this.beginEating(it, this.isZunchiSlave ? "\u305a\u3093\u3061\u3069\u308c\u3044\u306a\u306e\u3067\u305a\u3093\u3061\u3092\u98df\u3079\u3066\u3044\u308b" : "\u305a\u3093\u3061\u3092\u98df\u3079\u3066\u3044\u308b");

View file

@ -10,9 +10,31 @@
const quality = this.world.performanceQuality?.() || {};
const effectDensity = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1);
const showDecor = effectDensity > 0.45 || this.world.selected === this;
if (this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04) this.preloadSunbathSprites?.();
const sprite = this.spriteId();
const img = typeof getRenderableImage === "function" ? getRenderableImage(sprite, "smile") : (images.get(sprite) || images.get(this.type));
const metrics = getImageMetrics(sprite) || getImageMetrics(this.type) || getImageMetrics("smile");
const sunbathSprite = String(sprite || "").startsWith("sunbath");
const sunbathCandidates = sunbathSprite && this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : [];
if (sunbathSprite) {
for (const id of sunbathCandidates) {
if (typeof ensureImage === "function") ensureImage(id);
else if (globalThis.TarinaiAssets?.ensureImage) globalThis.TarinaiAssets.ensureImage(id);
}
}
const spriteFallback = sunbathSprite ? "" : "smile";
let renderSprite = sprite;
let img = typeof getRenderableImage === "function" ? getRenderableImage(sprite, spriteFallback) : (images.get(sprite) || images.get(spriteFallback) || images.get(this.type));
if (sunbathSprite) {
const ready = (id) => typeof isImageReady === "function" ? isImageReady(id) : !!(images?.get?.(id)?.complete || images?.get?.(id)?.naturalWidth || images?.get?.(id)?.width);
if (!ready(sprite)) {
const fallbackSunbath = sunbathCandidates.find(id => ready(id));
if (fallbackSunbath) {
renderSprite = fallbackSunbath;
img = images.get(fallbackSunbath);
}
}
if (!img) img = typeof getRenderableImage === "function" ? getRenderableImage("smile", "smile") : (images.get("smile") || images.get(this.type));
}
const metrics = getImageMetrics(renderSprite) || getImageMetrics(sprite) || getImageMetrics(this.type) || getImageMetrics("smile");
const w = metrics?.w || 512;
const h = metrics?.h || 512;
const moveSpeed = Math.hypot(this.vx, this.vy);
@ -265,7 +287,19 @@
const cacheW = Math.max(1, Math.round((dw * renderScale) / 8) * 8);
const cacheH = Math.max(1, Math.round((dh * renderScale) / 8) * 8);
if (img) {
const spriteCanvas = typeof getCachedSpriteCanvas === "function" ? getCachedSpriteCanvas(sprite, img, cacheW, cacheH) : img;
let spriteCacheKey = sprite;
try {
if (typeof images !== "undefined" && images.get?.(sprite) !== img) {
let fallbackId = "fallback";
for (const [imageId, imageObj] of images) {
if (imageObj === img) { fallbackId = imageId; break; }
}
spriteCacheKey = `${sprite}:fallback:${fallbackId}`;
}
} catch (err) {
spriteCacheKey = sprite;
}
const spriteCanvas = sunbathSprite ? img : (typeof getCachedSpriteCanvas === "function" ? getCachedSpriteCanvas(spriteCacheKey, img, cacheW, cacheH) : img);
ctx.drawImage(spriteCanvas, -dw / 2, -dh / 2, dw, dh);
} else {
ctx.save();
@ -279,7 +313,7 @@
ctx.restore();
}
if (img && (this.zunchiStain || 0) > 3 && typeof getCachedStainOverlayCanvas === "function") {
const stainCanvas = getCachedStainOverlayCanvas(sprite, img, cacheW, cacheH, clamp((this.zunchiStain || 0) / 100, 0, 1), this.zunchiStainSeed || 0.5);
const stainCanvas = getCachedStainOverlayCanvas(renderSprite || sprite, img, cacheW, cacheH, clamp((this.zunchiStain || 0) / 100, 0, 1), this.zunchiStainSeed || 0.5);
if (stainCanvas) {
ctx.save();
ctx.globalAlpha = clamp((this.zunchiStain || 0) / 100, 0.10, 0.88);
@ -364,6 +398,87 @@
}
},
drawSunbathSpriteDirect(ctx, t, lightState, showDecor = true) {
return false;
const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"];
const candidates = (Array.isArray(ids) && ids.length ? ids : ["sunbath_1", "sunbath_2", "sunbath_3"]).filter(id => String(id || "").startsWith("sunbath"));
const now = Number(this.world?.time ?? t ?? 0) || 0;
const frameKey = Math.floor(now / 0.5);
if (this._sunbathRenderFrameKey !== frameKey || !candidates.includes(this._sunbathRenderSpriteId)) {
const prev = this._sunbathRenderSpriteId || this.sunbathSpriteVariant || "";
const choices = candidates.length > 1 ? candidates.filter(id => id !== prev) : candidates;
this._sunbathRenderSpriteId = choices[Math.floor(Math.random() * choices.length)] || candidates[0] || "sunbath_1";
this._sunbathRenderFrameKey = frameKey;
}
const sprite = this._sunbathRenderSpriteId || this.sunbathSpriteVariant || candidates[0] || "sunbath_1";
this.sunbathSpriteVariant = sprite;
this.visibleSpriteId = sprite;
this.spriteLockUntil = now;
for (const id of candidates) {
if (typeof ensureImage === "function") ensureImage(id);
else if (globalThis.TarinaiAssets?.ensureImage) globalThis.TarinaiAssets.ensureImage(id);
}
let img = null;
const ordered = [sprite, this.sunbathSpriteVariant, ...candidates, "smile"];
const seen = new Set();
for (const id of ordered) {
if (!id || seen.has(id)) continue;
seen.add(id);
const next = typeof getRenderableImage === "function"
? getRenderableImage(id, id === "smile" ? "smile" : "smile")
: (typeof images !== "undefined" ? (images.get(id) || images.get("smile")) : null);
const ready = next && (next.complete || next.naturalWidth || next.width);
if (ready) { img = next; break; }
}
const metrics = (typeof getImageMetrics === "function" ? (getImageMetrics(sprite) || getImageMetrics(img) || getImageMetrics("smile")) : null) || {};
const w = metrics.w || img?.naturalWidth || img?.width || 512;
const h = metrics.h || img?.naturalHeight || img?.height || 468;
const visualScale = this.effectiveScale ? this.effectiveScale() : this.scale;
const breathing = Math.sin(t * 1.4 + this.age * 0.18);
const bob = breathing * 0.55;
const drawW = w * visualScale * 0.30;
const drawH = h * visualScale * 0.30;
const squish = 1 + breathing * 0.006;
const dw = drawW * squish;
const dh = drawH / squish;
const faceRight = this.facingDir ? this.facingDir() > 0 : (this.facing || 1) > 0;
const nestHideAlpha = this.nestFade ? clamp(1 - this.nestFade, 0, 1) : 1;
if (nestHideAlpha <= 0.01) return true;
const shadow = projectedShadowParams(lightState, 0.95 + visualScale * 0.8);
const shadowContactY = this.y + bob + dh * 0.30;
if (showDecor && img) {
drawImageProjectedShadow(ctx, img, this.x, shadowContactY, dw, dh, shadow, {
faceRight,
alpha: shadow.alpha * 0.92 * nestHideAlpha,
});
} else if (showDecor) {
drawProjectedShadow(ctx, this.x, shadowContactY, drawW * 0.52, drawH * 0.13, { ...shadow, alpha: shadow.alpha * 0.82 * nestHideAlpha });
}
if (showDecor) drawCreatureGlow(ctx, this, drawW, drawH, lightState);
ctx.save();
ctx.translate(this.x, this.y + bob);
ctx.rotate((faceRight ? 1 : -1) * (Math.PI / 18));
if (faceRight) ctx.scale(-1, 1);
ctx.globalAlpha = nestHideAlpha;
if (img) {
ctx.drawImage(img, -dw / 2, -dh / 2, dw, dh);
} else {
ctx.fillStyle = "#f4ead6";
ctx.strokeStyle = "rgba(92,71,49,0.48)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(0, 0, dw * 0.42, dh * 0.36, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
ctx.restore();
return true;
},
contains(x, y) {
if (this.insideNestBoxId || (this.nestFade || 0) >= 0.92) return false;
const hit = this.world.screenSizeToWorld ? this.world.screenSizeToWorld(this.radius * 1.9) : this.radius * 1.9;

View file

@ -193,8 +193,11 @@
this.target = null;
this.vx *= Math.pow(0.90, dt * 60);
this.vy *= Math.pow(0.90, dt * 60);
this.x = clamp(this.x + Math.sin((this.world?.time || 0) * 1.3 + this.seed) * 0.03, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding);
this.y = clamp(this.y + Math.cos((this.world?.time || 0) * 1.1 + this.seed) * 0.02, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding);
const pad = Math.max(28, CONFIG.worldPadding || 30);
const safeW = Math.max(pad * 2 + 1, Number(this.world?.w || 1000) || 1000);
const safeH = Math.max(pad * 2 + 1, Number(this.world?.h || 720) || 720);
this.x = clamp((Number.isFinite(this.x) ? this.x : safeW * 0.5) + Math.sin((this.world?.time || 0) * 1.3 + this.seed) * 0.03, pad, safeW - pad);
this.y = clamp((Number.isFinite(this.y) ? this.y : safeH * 0.5) + Math.cos((this.world?.time || 0) * 1.1 + this.seed) * 0.02, pad, safeH - pad);
return;
}
if (this.state === "sleep") {

View file

@ -1,6 +1,7 @@
"use strict";
const SCALABLE_TOOLS = new Set(scalableToolIds());
const EMOJI_ICON_TOOLS = new Set(["poke", "pinch", "water_hose"]);
const SIZE_ORDER = typeof TOOL_SIZE_ORDER !== "undefined" ? TOOL_SIZE_ORDER : ["small", "medium", "large"];
@ -26,8 +27,13 @@ function renderToolPalette() {
if (!def || def.simulationOnly) continue;
const btn = document.createElement("button");
btn.className = "tool";
if (EMOJI_ICON_TOOLS.has(toolId)) btn.classList.add("tool-emoji-icon");
btn.dataset.tool = toolId;
btn.textContent = def.label || toolId;
const label = document.createElement("span");
label.className = "tool-label";
label.textContent = def.label || toolId;
btn.title = label.textContent;
btn.appendChild(label);
attachToolSpritePreview(btn, def, toolId);
if (toolId === world?.tool) btn.classList.add("selected");
body.appendChild(btn);
@ -38,32 +44,47 @@ function renderToolPalette() {
ui.toolPalette.appendChild(frag);
}
function drawToolPreviewCanvas(canvas, itemType, { watermark = false } = {}) {
if (!canvas || !itemType) return false;
const ctx = canvas.getContext("2d");
if (!ctx) return false;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (typeof window.drawToolItemPreview === "function") {
return window.drawToolItemPreview(ctx, itemType, { width: canvas.width, height: canvas.height, compact: true, watermark }) !== false;
}
if (typeof window.isConsumableSpriteType === "function" && window.isConsumableSpriteType(itemType) && typeof window.drawConsumableFieldSprite === "function") {
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
const baseScale = itemType === "protein" || itemType === "niteropu" ? 0.9 : 1.0;
ctx.scale(baseScale, baseScale);
window.drawConsumableFieldSprite(ctx, itemType, 17.5, 112.4, { compact: true });
ctx.restore();
return true;
}
return false;
}
function createToolPreviewCanvas(className, size = 96) {
const canvas = document.createElement("canvas");
canvas.className = className;
canvas.width = size;
canvas.height = size;
canvas.setAttribute("aria-hidden", "true");
return canvas;
}
function attachToolSpritePreview(btn, def, toolId) {
const itemType = def?.itemType || toolId;
if (!btn || !itemType || !def?.placeable) return;
if (toolId === "ball" || itemType === "ball") return;
const drawPreview = window.drawToolItemPreview || window.drawConsumableFieldSprite;
if (typeof drawPreview !== "function") return;
const canvas = document.createElement("canvas");
canvas.className = "tool-sprite-preview";
canvas.width = 72;
canvas.height = 72;
canvas.setAttribute("aria-hidden", "true");
const ctx = canvas.getContext("2d");
if (!ctx) return;
let drawn = false;
if (typeof window.drawToolItemPreview === "function") {
drawn = window.drawToolItemPreview(ctx, itemType, { width: canvas.width, height: canvas.height, compact: true }) !== false;
} else if (typeof window.isConsumableSpriteType === "function" && window.isConsumableSpriteType(itemType)) {
ctx.translate(canvas.width / 2, canvas.height / 2);
const scale = itemType === "protein" || itemType === "niteropu" ? 0.82 : 0.92;
ctx.scale(scale, scale);
window.drawConsumableFieldSprite(ctx, itemType, 13.5, 112.4, { compact: true });
drawn = true;
}
if (!drawn) return;
if (typeof window.drawToolItemPreview !== "function" && typeof window.drawConsumableFieldSprite !== "function") return;
const iconCanvas = createToolPreviewCanvas("tool-sprite-preview", 160);
const iconDrawn = drawToolPreviewCanvas(iconCanvas, itemType);
if (!iconDrawn) return;
btn.classList.add("tool-has-canvas-icon");
btn.prepend(canvas);
btn.prepend(iconCanvas);
}
function toolSizeFor(tool) {

View file

@ -1,8 +1,8 @@
"use strict";
(function () {
const APP_VERSION = "15.24.35";
const APP_BUILD = "owned-bed-duplicator-priority";
const APP_VERSION = "15.24.53";
const APP_BUILD = "tool-ui-icon-scale-collapse-fix";
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;

View file

@ -287,7 +287,7 @@
this.ensureSpatial?.("zunchi-burst");
},
spawnZunchi(x, y) {
spawnZunchi(x, y, source = null) {
const zunchiLimit = CONFIG.zunchiLimit ?? 56;
if ((this.itemCounts.zunchi || 0) >= zunchiLimit) {
let oldest = null;
@ -299,6 +299,11 @@
}
const it = new Item("zunchi", clamp(x, 40, this.w - 40), clamp(y, 40, this.h - 40));
it.amount = 260;
it.stage = "fresh";
it.producedById = source?.id || "";
it.producedAt = this.time || 0;
it.spawnGrace = Math.max(it.spawnGrace || 0, 10.0);
it.eatProtectedUntil = (this.time || 0) + 90;
this.addItem?.(it, "zunchi-spawn") || this.items.push(it);
audio.place?.("zunchi");
},

View file

@ -54,6 +54,19 @@
function signboardVisibleLength(text = "") {
return Array.from(String(text || "").replace(/\n/g, "")).length;
}
function replaceSignboardEditorSelection(area, text = "") {
if (!area) return;
const value = String(area.value || "");
const start = Number(area.selectionStart || 0);
const end = Number(area.selectionEnd || start);
const next = sanitizeSignboardText(value.slice(0, start) + text + value.slice(end), { trimTrailing: false });
area.value = next;
const caret = Math.min(next.length, start + String(text || "").length);
area.selectionStart = caret;
area.selectionEnd = caret;
}
function openSignboardEditor(sign, worldRef) {
if (!sign) return false;
const dialog = ensureSignboardEditor();
@ -97,7 +110,11 @@
const replacing = selected.length > 0;
if (e.key === "Enter") {
const selectedLineBreaks = (selected.match(/\n/g) || []).length;
if (lines.length - selectedLineBreaks >= SIGNBOARD_LINES) e.preventDefault();
e.preventDefault();
if (lines.length - selectedLineBreaks < SIGNBOARD_LINES) {
replaceSignboardEditorSelection(area, "\n");
syncCount();
}
return;
}
if (e.key && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
@ -110,10 +127,7 @@
area.onpaste = (e) => {
e.preventDefault();
const text = e.clipboardData?.getData?.("text") || "";
const start = Number(area.selectionStart || 0);
const end = Number(area.selectionEnd || start);
const next = sanitizeSignboardText((area.value || "").slice(0, start) + text + (area.value || "").slice(end), { trimTrailing: false });
area.value = next;
replaceSignboardEditorSelection(area, text);
syncCount();
};
area.oninput = () => {

View file

@ -400,23 +400,20 @@
const result = repairPoint(ef, ef.size || 12);
if (result === "delete") ef.life = 0;
}
const safeW = Math.max(pad * 2 + 1, Number(this.w || 1000) || 1000);
const safeH = Math.max(pad * 2 + 1, Number(this.h || 720) || 720);
const repairTarinai = (t) => {
t.x = clamp(Number.isFinite(t.x) ? t.x : safeW * 0.5, pad, safeW - pad);
t.y = clamp(Number.isFinite(t.y) ? t.y : safeH * 0.5, pad, safeH - pad);
t.vx = 0;
t.vy = 0;
t.entryTimer = 0;
t.lastOutOfBoundsRepairAt = this.time || 0;
};
for (const t of this.tarinai || []) {
if (!t || t.dead) continue;
const result = repairPoint(t, t.radius || 22);
if (result === "delete") {
t.x = clamp(Number.isFinite(t.x) ? t.x : this.w * 0.5, pad, this.w - pad);
t.y = clamp(Number.isFinite(t.y) ? t.y : this.h * 0.5, pad, this.h - pad);
t.vx = 0;
t.vy = 0;
t.entryTimer = 0;
t.die(HEALTH?.CAUSES?.outOfBounds || "\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664");
} else if (result === "moved") {
t.x = clamp(Number.isFinite(t.x) ? t.x : this.w * 0.5, pad, this.w - pad);
t.y = clamp(Number.isFinite(t.y) ? t.y : this.h * 0.5, pad, this.h - pad);
t.vx = 0;
t.vy = 0;
t.entryTimer = 0;
}
if (result === "delete" || result === "moved") repairTarinai(t);
}
let changedAnts = false;
for (const ant of this.ants || []) {

View file

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