fulfillment
125
PATCH_NOTES.md
|
|
@ -121,3 +121,128 @@
|
|||
- Removed the remaining synchronous family-tree DFS fallback blocks in `ui_family_data.js` and `ui_family_render.js`; both now use `TarinaiFamilyGraph.connectedComponents()`.
|
||||
- Simplified the remaining manual ant proximity fallback in `tarinai_needs_items.js` to `world.nearbyAnts()`.
|
||||
- Verification: all `js/*.js` and `service-worker.js` pass `node --check`.
|
||||
|
||||
## 15.25.0 save/load UI and pin UI fix
|
||||
- Fixed `Item.draw()` crash caused by an undefined `isOshibyo` local in the pin drawing branch.
|
||||
- Reworked the save dialog into a 3x3 slot grid and added per-slot canvas thumbnails.
|
||||
- Added compressed `TARINAI_SAVE_V2:` hash export/import using browser gzip streams when available; legacy `TARINAI_SAVE_V1:` imports remain supported.
|
||||
- Replaced browser confirm prompts in save/load/import/delete flows with in-game save dialog confirmation UI.
|
||||
- Added missing tool UI icons for `pushpin` and `oshibyo` and updated their tooltips.
|
||||
- Renamed tool categories: 操作, 飲食, 薬, 生活, 危険物. Moved freeze to 操作 and fences to 生活.
|
||||
- Made the frozen individual panel initially collapsed.
|
||||
- Made colony mood span the full colony UI width with a compact height.
|
||||
- Moved push notification cards inside the top of the Event UI.
|
||||
- Bumped static cache version to `15.25.0`.
|
||||
|
||||
## v15.26.0 save/sunbath/signboard/freeze UI patch
|
||||
- ずんちどれいの食対象を、ずんだ餅・草・水系を除く食べ物全般へ拡張。
|
||||
- 看板を大型化し、観察/看板ツールでクリック編集、配置直後編集に対応。
|
||||
- 日光浴の開始処理が呼ばれていなかった問題を修正し、発生確率と継続時間を大幅に増加。
|
||||
- フィールド内の非地形オブジェクト・たりない・アリをY座標で統合ソートし、下側が前に出る描画順へ調整。
|
||||
- 冷凍ボタンを操作カテゴリから廃止し、冷凍個体一覧を操作カテゴリ内部へ移動。
|
||||
- セーブスロットを小型化し、サムネイルをスロット全面背景として表示。
|
||||
- ハッシュ書き出しをV3コンパクト形式へ変更。イベントログ、エフェクト、吹き出し等の非本体情報は保存対象外。
|
||||
- α版のため、セーブ/ハッシュ互換性は当面保証しない旨をコードコメントに明記。
|
||||
|
||||
## v15.27.0 hash V4 / layer / warning patch
|
||||
- ハッシュ書き出しを `TARINAI_SAVE_V4:` に更新。V3の密な配列形式から、短い疎配列形式へ変更。
|
||||
- V4ではゲームプレイ本体に必要な world / tarinai / item / ant / frozen 状態のみを保存し、ログ、イベント表示、エフェクト、吹き出し、サムネイル等は保存対象外。
|
||||
- アイテム種別・スプライト種別は固定辞書の数値コードで保存し、初期状態で全アイテムを1つずつ置いたケースでも短くなるようにした。
|
||||
- アイテムは `type,x,y` をヘッダ化し、その他はデフォルト値から変化した値だけを保存する方式へ変更。
|
||||
- 干草寝床の描画レイヤーを同じY座標の他オブジェクトより少し後ろへ調整。
|
||||
- ロード/インポート/削除の上書き警告を中央の大きなゲーム内UIとして表示。
|
||||
- キャッシュ対策としてバージョンを `15.27.0` へ更新。
|
||||
|
||||
## v15.28.0 machine / pin placement patch
|
||||
- おしり鋲を約半分の表示サイズに縮小し、刺さり位置をたりないスプライト右側へ移動。
|
||||
- 画鋲/おしり鋲の刺さり表示は、たりないのスプライト反転に合わせて左右反転するよう調整。
|
||||
- 新アイテム「複製機」を追加。消費アイテムツールで複製機をクリックするとスロットにセットし、下側の皿へ無限補充する。
|
||||
- 新アイテム「大砲」を追加。消費アイテムツールで大砲をクリックするとスロットにセットし、クリック位置で360度向きを変更し、セット品を撃ち出す。
|
||||
- 大砲弾として飛んだ消費アイテムは、高速時にたりないへ当たるとボール同様に吹き飛ばす。
|
||||
- 複製機/大砲の道具UIアイコンを追加。
|
||||
- セーブV4のアイテム種別表・追加キーへ複製機/大砲および機械スロット情報を追加。
|
||||
- キャッシュ対策としてバージョンを `15.28.0` へ更新。
|
||||
|
||||
## v15.29.0 asset / save compression patch
|
||||
- 全たりないスプライトを 512x468 以下に収まるよう縮小し、WebP を再圧縮。
|
||||
- `ALPHA_BOUNDS` を現行画像寸法と透過領域から再生成。
|
||||
- 音声サンプルを軽量 MP3 に置き換え、WAV を配布対象から除外。
|
||||
- セーブハッシュ内の座標系数値は小数点第一位までに丸めるよう変更。
|
||||
- コメントは削除せず、JS minify は行っていない。
|
||||
- キャッシュ対策としてバージョンを `15.29.0` へ更新。
|
||||
|
||||
## v15.30.0 save hash v5 compact patch
|
||||
- ハッシュ書き出しを `TARINAI_SAVE_V5:` に更新。
|
||||
- V5はトップレベルを短い配列形式にし、world / たりない / item / ant / 冷凍個体をゲームプレイに必要な値だけで再パック。
|
||||
- たりないの性格・遺伝・関係性・効果タイマー・状態/欲求/アイテム種別を短い配列または数値コード化。
|
||||
- 初期値・決定論的に再生成できる値・UI/演出専用値はハッシュ保存対象から除外。
|
||||
- 座標は引き続き小数点第一位まで保存。
|
||||
- V4/V3/V2/V1の読み込み互換は一応残すが、α版のため今後の互換性保証はしない。
|
||||
- キャッシュ対策としてバージョンを `15.30.0` へ更新。
|
||||
|
||||
## 15.31.0 - Hash V6 compaction
|
||||
|
||||
- Added `TARINAI_SAVE_V6:` export format.
|
||||
- Further shortened hash text while preserving gameplay state.
|
||||
- Removed individual observation history/name fields from hash exports:
|
||||
- names
|
||||
- former names
|
||||
- parent display names
|
||||
- personality daily history
|
||||
- mystery drug history
|
||||
- recent change records
|
||||
- per-individual observation records
|
||||
- Replaced long family keys in hash payload with compact numeric family references.
|
||||
- Encoded live IDs like `t12` as numeric IDs in hash payload and restored them on load.
|
||||
- Packed tarinai core gameplay stats into fixed compact rows.
|
||||
- Kept gameplay-critical data:
|
||||
- needs/status
|
||||
- position and movement-relevant values
|
||||
- personality and genetics
|
||||
- parent/child links
|
||||
- relationships
|
||||
- diseases
|
||||
- zunchi slave state
|
||||
- item effects
|
||||
- oshibyo/zunchi stock
|
||||
- sunbath/fear/fall gameplay timers
|
||||
- items, machines, ants, frozen tarinai
|
||||
- Flattened item base records remains active from V6 export.
|
||||
- Hash remains ASCII base64url + gzip for copy/paste and editor durability.
|
||||
|
||||
## 15.32.0 - Hash V7 base91/name preservation
|
||||
|
||||
- Added `TARINAI_SAVE_V7:` hash export.
|
||||
- V7 uses gzip plus a 91-character printable ASCII transport alphabet instead of base64url.
|
||||
- V7 keeps individual names in compact hash exports.
|
||||
- V7 excludes dead tarinai rows from compact hash exports so family graph history does not revive/save dead individuals.
|
||||
- V7 filters parent/child and relationship links to living or frozen individuals; pending frozen death notices remain gameplay data.
|
||||
- V7 stores selected numeric tarinai stats as relative deltas from common defaults while preserving the existing compact numeric precision.
|
||||
- V1-V6 import support remains.
|
||||
|
||||
## 15.33.0 - Hash V8 current-only compact save
|
||||
- Hash exports now use `TARINAI_SAVE_V8:` only. Old hash import compatibility was intentionally removed for the alpha save format.
|
||||
- Added an untouched-initial-colony sentinel. Initial colonies now export as a compact reset marker plus names instead of full entity/item rows, targeting roughly 100 characters for the initial state depending on generated names.
|
||||
- Frozen individual save data no longer preserves pending death notices or relationship/memory records.
|
||||
- Dead family nodes remain omitted from compact hash saves.
|
||||
- Individual names are preserved in V8 compact saves.
|
||||
- V8 keeps printable ASCII transport using the 91-character alphabet, with an explicit compressed/uncompressed mode marker.
|
||||
|
||||
|
||||
## 15.34.0 - Hash V9 binary compact format
|
||||
|
||||
- Removed the V8 initial-state-only save sentinel. All states now use the same compact format.
|
||||
- Export hash format is now `TARINAI_SAVE_V9:` and legacy hash imports are rejected during alpha.
|
||||
- Replaced JSON-based compact hashes with a dedicated binary packet plus printable ASCII Base94 transport.
|
||||
- Stopped saving elapsed day count in compact hash; loaded compact hashes restore day as 1.
|
||||
- Kept custom individual names, while default four-character names made only from `は / う / ぅ / っ` are packed as 2-bit character codes.
|
||||
- Replaced saved tarinai/item IDs and family keys with array-index references; IDs are regenerated on load.
|
||||
- Uses default-relative numeric deltas, optional timer tags, boolean bit fields, and 0.1-unit integer coordinates.
|
||||
- Keeps gameplay-critical tarinai needs, personality, genetics, family links, relationships, item states, machines, ants, and frozen individuals; frozen pending-loss memory remains omitted.
|
||||
|
||||
## 15.35.0 - Hash V10 compact packet
|
||||
- Changed exported hash prefix to `TARINAI-SAVE-V10:` so the full hash text no longer uses `_` in its version marker.
|
||||
- Excluded `\\`, `*`, `'`, and `_` from the printable ASCII transport alphabet.
|
||||
- Replaced the V9 generic array/value packet with a V10 field-specific binary packet for common gameplay data.
|
||||
- Kept gameplay-critical state while reducing overhead for names, world data, tarinai core rows, item rows, ants, and frozen individuals.
|
||||
- Kept elapsed day count out of the hash; loads restore to day 1.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "15.24.0",
|
||||
"version": "15.35.0",
|
||||
"css": [
|
||||
"css/base.css",
|
||||
"css/layout.css",
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
"js/food_registry.js",
|
||||
"js/input_mode_manager.js",
|
||||
"js/math.js",
|
||||
"js/need_behavior_system.js",
|
||||
"js/physics_helpers.js",
|
||||
"js/assets.js",
|
||||
"js/audio.js",
|
||||
|
|
@ -74,7 +75,6 @@
|
|||
"assets/ui/*.webp",
|
||||
"assets/objects/*.webp",
|
||||
"assets/sprites/*.webp",
|
||||
"assets/sounds/*.wav",
|
||||
"assets/sounds/*.mp3"
|
||||
],
|
||||
"notes": "Source of truth for static loading order; generated files are index.html script/link tags and service-worker.js cache list."
|
||||
|
|
|
|||
BIN
assets/sounds/voice_001_hau.mp3
Normal file
BIN
assets/sounds/voice_002_flee.mp3
Normal file
BIN
assets/sounds/voice_003_po.mp3
Normal file
BIN
assets/sounds/voice_004_eat.mp3
Normal file
BIN
assets/sounds/voice_005_stress.mp3
Normal file
BIN
assets/sounds/voice_006_sleep.mp3
Normal file
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 15 KiB |
BIN
assets/ui/tool_cannon.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/ui/tool_duplicator.webp
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/ui/tool_oshibyo.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
26
css/base.css
|
|
@ -400,15 +400,15 @@ h1 {
|
|||
.tool[data-tool="water_bowl"] { --tool-icon: url("../assets/ui/tool_water_bowl.webp"); }
|
||||
|
||||
/* Generated parameter item tool icons. */
|
||||
.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"); }
|
||||
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.33.0"); }
|
||||
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.33.0"); }
|
||||
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.33.0"); }
|
||||
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.33.0"); }
|
||||
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.33.0"); }
|
||||
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.33.0"); }
|
||||
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.33.0"); }
|
||||
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.33.0"); }
|
||||
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.33.0"); }
|
||||
.tool[data-tool="freeze"] { --tool-icon: none; }
|
||||
.tool[data-tool="freeze"]::before {
|
||||
content: "🧊";
|
||||
|
|
@ -418,3 +418,11 @@ h1 {
|
|||
text-align: center;
|
||||
}
|
||||
.tool[data-tool="freeze"]::after { content: "🧊"; background: none; font-size: 52px; right: -10px; left: auto; opacity: 0.08; display:flex; align-items:center; justify-content:flex-end; }
|
||||
|
||||
/* v15.28.0: missing pin tool icons. */
|
||||
.tool[data-tool="pushpin"] { --tool-icon: url("../assets/ui/tool_pushpin.webp?v=15.33.0"); }
|
||||
.tool[data-tool="oshibyo"] { --tool-icon: url("../assets/ui/tool_oshibyo.webp?v=15.33.0"); }
|
||||
|
||||
/* v15.28.0: machine tool icons. */
|
||||
.tool[data-tool="duplicator"] { --tool-icon: url("../assets/ui/tool_duplicator.webp?v=15.33.0"); }
|
||||
.tool[data-tool="cannon"] { --tool-icon: url("../assets/ui/tool_cannon.webp?v=15.33.0"); }
|
||||
|
|
|
|||
|
|
@ -454,6 +454,14 @@
|
|||
.selected-info .info-row[data-metric="zunchi"] { background: rgba(112, 142, 70, 0.10); }
|
||||
.selected-info .info-row[data-metric="mood"] { background: rgba(222, 196, 86, 0.10); }
|
||||
.selected-info .info-row[data-metric="lack"] { background: rgba(142, 112, 184, 0.10); }
|
||||
.selected-info .selected-need-row.active {
|
||||
background: rgba(124, 198, 70, 0.18);
|
||||
box-shadow: inset 3px 0 0 rgba(99, 166, 52, 0.78);
|
||||
}
|
||||
.selected-info .selected-need-mark {
|
||||
color: rgba(72, 126, 42, 0.95);
|
||||
font-size: 0.82em;
|
||||
}
|
||||
.selected-personality-changes {
|
||||
align-items: start;
|
||||
}
|
||||
|
|
@ -621,3 +629,179 @@
|
|||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* v15.25.0: save menu grid, thumbnails, compact import/export confirmation. */
|
||||
.save-dialog-panel {
|
||||
width: min(860px, calc(100vw - 32px));
|
||||
max-height: min(88vh, 790px);
|
||||
}
|
||||
.save-slot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin: 14px 0;
|
||||
}
|
||||
.save-slot-card {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr auto;
|
||||
gap: 6px;
|
||||
min-height: 190px;
|
||||
padding: 9px;
|
||||
border: 1px solid rgba(115, 91, 62, 0.20);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
box-shadow: 0 2px 8px rgba(68, 52, 33, 0.06);
|
||||
}
|
||||
.save-slot-card.empty {
|
||||
background: rgba(250, 247, 240, 0.54);
|
||||
}
|
||||
.save-slot-thumb {
|
||||
height: 82px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, rgba(231, 221, 204, 0.80), rgba(245, 240, 230, 0.90));
|
||||
border: 1px solid rgba(115, 91, 62, 0.14);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.save-slot-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.save-slot-thumb-empty {
|
||||
font-size: 11px;
|
||||
letter-spacing: .04em;
|
||||
color: rgba(88, 72, 54, 0.48);
|
||||
font-weight: 800;
|
||||
}
|
||||
.save-slot-title {
|
||||
font-weight: 800;
|
||||
color: #4f3928;
|
||||
line-height: 1.15;
|
||||
}
|
||||
.save-slot-meta {
|
||||
color: #6d5a46;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
min-height: 48px;
|
||||
}
|
||||
.save-slot-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
justify-content: stretch;
|
||||
}
|
||||
.save-slot-actions .btn.mini {
|
||||
min-width: 0;
|
||||
padding-inline: 5px;
|
||||
}
|
||||
.save-confirm-box {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(169, 90, 73, 0.26);
|
||||
background: rgba(255, 245, 236, 0.96);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,.62);
|
||||
}
|
||||
.save-confirm-box.hidden { display: none; }
|
||||
.save-confirm-message { font-size: 13px; line-height: 1.45; color: #4f3928; }
|
||||
.save-confirm-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:8px; }
|
||||
.save-hash-text { min-height: 82px; }
|
||||
@media (max-width: 740px) {
|
||||
.save-slot-grid { grid-template-columns: 1fr; }
|
||||
.save-slot-card { min-height: 0; }
|
||||
.save-slot-thumb { height: 96px; }
|
||||
}
|
||||
|
||||
/* v15.25.0: compact full-width colony mood. */
|
||||
.colony-stats .stat:nth-child(3) {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 5px 9px;
|
||||
min-height: 28px;
|
||||
text-align: left;
|
||||
}
|
||||
.colony-stats .stat:nth-child(3) span { font-size: 10px; }
|
||||
.colony-stats .stat:nth-child(3) strong { font-size: 14px; line-height: 1.15; text-align: right; }
|
||||
|
||||
/* v15.25.0: event push cards are contained at the top of the event UI. */
|
||||
.log-push-overlay {
|
||||
position: static;
|
||||
z-index: auto;
|
||||
width: 100%;
|
||||
margin: 0 0 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.log-push-toast {
|
||||
box-shadow: 0 6px 18px rgba(46, 34, 24, 0.13);
|
||||
padding: 8px 10px 8px 13px;
|
||||
}
|
||||
|
||||
/* v15.28.0: smaller save slots with full-card thumbnails. */
|
||||
.save-dialog-panel { width: min(760px, calc(100vw - 32px)); max-height: min(88vh, 760px); }
|
||||
.save-slot-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px; }
|
||||
.save-slot-card { position: relative; overflow: hidden; min-height: 126px; padding: 7px; grid-template-rows: auto 1fr auto; gap: 4px; background: rgba(255,255,255,.64); }
|
||||
.save-slot-thumb { position: absolute; inset: 0; height: auto; border: 0; border-radius: inherit; z-index: 0; opacity: .88; }
|
||||
.save-slot-thumb::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(255,250,241,.42), rgba(255,250,241,.78)); }
|
||||
.save-slot-thumb img { object-fit: cover; }
|
||||
.save-slot-thumb-empty { width:100%; height:100%; display:grid; place-items:center; background:rgba(240,234,222,.80); }
|
||||
.save-slot-title, .save-slot-meta, .save-slot-actions { position: relative; z-index: 1; }
|
||||
.save-slot-title { padding: 3px 6px; border-radius: 999px; background: rgba(255,251,244,.76); width: fit-content; }
|
||||
.save-slot-meta { min-height: 34px; font-size: 11px; line-height: 1.25; padding: 5px 6px; border-radius: 10px; background: rgba(255,251,244,.70); }
|
||||
.save-slot-actions { gap: 4px; }
|
||||
.save-slot-actions .btn.mini { padding: 5px 4px; font-size: 11px; }
|
||||
@media (max-width: 740px) { .save-slot-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .save-slot-card { min-height: 118px; } }
|
||||
@media (max-width: 520px) { .save-slot-grid { grid-template-columns: 1fr; } .save-slot-card { min-height: 128px; } }
|
||||
|
||||
/* v15.28.0: frozen list lives inside the Operation tool category. */
|
||||
.tool-category[data-tool-category="operate"] .frozen-panel { grid-column: 1 / -1; margin: 4px 0 0; }
|
||||
|
||||
|
||||
/* v15.28.0: large centered load/overwrite warning. */
|
||||
.save-confirm-box {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 12000;
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
margin: 0;
|
||||
padding: 22px 24px;
|
||||
border-radius: 22px;
|
||||
border: 2px solid rgba(169, 90, 73, 0.34);
|
||||
background: rgba(255, 248, 239, 0.98);
|
||||
box-shadow: 0 24px 70px rgba(42, 30, 18, 0.30), inset 0 0 0 1px rgba(255,255,255,.74);
|
||||
}
|
||||
.save-confirm-box::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
left: -100vw;
|
||||
right: -100vw;
|
||||
top: -100vh;
|
||||
bottom: -100vh;
|
||||
background: rgba(30, 22, 14, 0.24);
|
||||
z-index: -1;
|
||||
}
|
||||
.save-confirm-message {
|
||||
font-size: 18px;
|
||||
line-height: 1.55;
|
||||
font-weight: 800;
|
||||
color: #4a2f23;
|
||||
}
|
||||
.save-confirm-actions {
|
||||
margin-top: 18px;
|
||||
gap: 12px;
|
||||
}
|
||||
.save-confirm-actions .btn {
|
||||
min-width: 118px;
|
||||
padding: 10px 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,15 +180,15 @@
|
|||
}
|
||||
|
||||
/* 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"); }
|
||||
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.30.0"); }
|
||||
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.30.0"); }
|
||||
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.30.0"); }
|
||||
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.30.0"); }
|
||||
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.30.0"); }
|
||||
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.30.0"); }
|
||||
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.30.0"); }
|
||||
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.30.0"); }
|
||||
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.30.0"); }
|
||||
.mobile-mode-controls {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
|
@ -239,3 +239,21 @@ body.touch-mobile-ui.mobile-mode-family .lineage-main-card { z-index: 9; }
|
|||
text-align: center;
|
||||
}
|
||||
.tool[data-tool="freeze"]::after { content: "🧊"; background: none; font-size: 52px; right: -10px; left: auto; opacity: 0.08; display:flex; align-items:center; justify-content:flex-end; }
|
||||
|
||||
/* v15.28.0: keep push notifications inside the event card on mobile too. */
|
||||
.log-push-overlay {
|
||||
position: static;
|
||||
z-index: auto;
|
||||
width: 100%;
|
||||
margin: 0 0 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tool[data-tool="pushpin"] { --tool-icon: url("../assets/ui/tool_pushpin.webp?v=15.30.0"); }
|
||||
.tool[data-tool="oshibyo"] { --tool-icon: url("../assets/ui/tool_oshibyo.webp?v=15.30.0"); }
|
||||
|
||||
/* v15.28.0: machine tool icons. */
|
||||
.tool[data-tool="duplicator"] { --tool-icon: url("../assets/ui/tool_duplicator.webp?v=15.30.0"); }
|
||||
.tool[data-tool="cannon"] { --tool-icon: url("../assets/ui/tool_cannon.webp?v=15.30.0"); }
|
||||
|
|
|
|||
1326
docs/prompt.txt
135
index.html
|
|
@ -5,11 +5,11 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>たりない観察</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 64 64%27%3E%3Ctext y=%2752%27 font-size=%2752%27%3E%E3%81%9F%3C/text%3E%3C/svg%3E" />
|
||||
<link rel="stylesheet" href="css/base.css?v=15.24.0" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=15.24.0" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=15.24.0" />
|
||||
<link rel="stylesheet" href="css/components.css?v=15.24.0" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=15.24.0" />
|
||||
<link rel="stylesheet" href="css/base.css?v=15.35.0" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=15.35.0" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=15.35.0" />
|
||||
<link rel="stylesheet" href="css/components.css?v=15.35.0" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=15.35.0" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||
|
|
@ -99,6 +99,7 @@
|
|||
<section class="card log-card collapsible-card" data-card-key="events">
|
||||
<button class="panel-card-toggle" type="button" aria-expanded="true"><span class="panel-card-title">イベント</span><span class="panel-card-state">折り畳み</span></button>
|
||||
<div class="panel-card-body">
|
||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||
<div id="log" class="log"></div>
|
||||
<div class="log-push-controls" id="logPushControls" aria-label="プッシュ通知設定">
|
||||
<label class="push-master"><input id="logPushEnabled" type="checkbox"> プッシュ通知</label>
|
||||
|
|
@ -182,67 +183,67 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||
<script src="js/version.js?v=15.24.0" defer></script>
|
||||
<script src="js/event_bus.js?v=15.24.0" defer></script>
|
||||
<script src="js/performance_manager.js?v=15.24.0" defer></script>
|
||||
<script src="js/registry_base.js?v=15.24.0" defer></script>
|
||||
<script src="js/disease_registry.js?v=15.24.0" defer></script>
|
||||
<script src="js/effect_registry.js?v=15.24.0" defer></script>
|
||||
<script src="js/sound_pack.js?v=15.24.0" defer></script>
|
||||
<script src="js/data.js?v=15.24.0" defer></script>
|
||||
<script src="js/item_visual_registry.js?v=15.24.0" defer></script>
|
||||
<script src="js/food_registry.js?v=15.24.0" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=15.24.0" defer></script>
|
||||
<script src="js/math.js?v=15.24.0" defer></script>
|
||||
<script src="js/physics_helpers.js?v=15.24.0" defer></script>
|
||||
<script src="js/assets.js?v=15.24.0" defer></script>
|
||||
<script src="js/audio.js?v=15.24.0" defer></script>
|
||||
<script src="js/render.js?v=15.24.0" defer></script>
|
||||
<script src="js/sim_core.js?v=15.24.0" defer></script>
|
||||
<script src="js/items.js?v=15.24.0" defer></script>
|
||||
<script src="js/ants.js?v=15.24.0" defer></script>
|
||||
<script src="js/health.js?v=15.24.0" defer></script>
|
||||
<script src="js/tarinai.js?v=15.24.0" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=15.24.0" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=15.24.0" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=15.24.0" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=15.24.0" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=15.24.0" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=15.24.0" defer></script>
|
||||
<script src="js/tarinai_render.js?v=15.24.0" defer></script>
|
||||
<script src="js/world.js?v=15.24.0" defer></script>
|
||||
<script src="js/world_view.js?v=15.24.0" defer></script>
|
||||
<script src="js/family_graph.js?v=15.24.0" defer></script>
|
||||
<script src="js/world_family_social.js?v=15.24.0" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=15.24.0" defer></script>
|
||||
<script src="js/world_environment.js?v=15.24.0" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=15.24.0" defer></script>
|
||||
<script src="js/world_ants_system.js?v=15.24.0" defer></script>
|
||||
<script src="js/weather_system.js?v=15.24.0" defer></script>
|
||||
<script src="js/world_update.js?v=15.24.0" defer></script>
|
||||
<script src="js/world_placement_log.js?v=15.24.0" defer></script>
|
||||
<script src="js/world_bootstrap.js?v=15.24.0" defer></script>
|
||||
<script src="js/text_catalog.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_log.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_helpers.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_selected.js?v=15.24.0" defer></script>
|
||||
<script src="js/snapshot_system.js?v=15.24.0" defer></script>
|
||||
<script src="js/save_system.js?v=15.24.0" defer></script>
|
||||
<script src="js/patch_helpers.js?v=15.24.0" defer></script>
|
||||
<script src="js/freeze_system.js?v=15.24.0" defer></script>
|
||||
<script src="js/relation_event_system.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_tools.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_bind.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_charts.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_family_data.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_family_async.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=15.24.0" defer></script>
|
||||
<script src="js/ui_family_render.js?v=15.24.0" defer></script>
|
||||
<script src="js/main.js?v=15.24.0" defer></script>
|
||||
<script src="js/debug_tools.js?v=15.24.0" defer></script>
|
||||
<script src="js/version.js?v=15.35.0" defer></script>
|
||||
<script src="js/event_bus.js?v=15.35.0" defer></script>
|
||||
<script src="js/performance_manager.js?v=15.35.0" defer></script>
|
||||
<script src="js/registry_base.js?v=15.35.0" defer></script>
|
||||
<script src="js/disease_registry.js?v=15.35.0" defer></script>
|
||||
<script src="js/effect_registry.js?v=15.35.0" defer></script>
|
||||
<script src="js/sound_pack.js?v=15.35.0" defer></script>
|
||||
<script src="js/data.js?v=15.35.0" defer></script>
|
||||
<script src="js/item_visual_registry.js?v=15.35.0" defer></script>
|
||||
<script src="js/food_registry.js?v=15.35.0" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=15.35.0" defer></script>
|
||||
<script src="js/math.js?v=15.35.0" defer></script>
|
||||
<script src="js/need_behavior_system.js?v=15.35.0" defer></script>
|
||||
<script src="js/physics_helpers.js?v=15.35.0" defer></script>
|
||||
<script src="js/assets.js?v=15.35.0" defer></script>
|
||||
<script src="js/audio.js?v=15.35.0" defer></script>
|
||||
<script src="js/render.js?v=15.35.0" defer></script>
|
||||
<script src="js/sim_core.js?v=15.35.0" defer></script>
|
||||
<script src="js/items.js?v=15.35.0" defer></script>
|
||||
<script src="js/ants.js?v=15.35.0" defer></script>
|
||||
<script src="js/health.js?v=15.35.0" defer></script>
|
||||
<script src="js/tarinai.js?v=15.35.0" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=15.35.0" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=15.35.0" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=15.35.0" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=15.35.0" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=15.35.0" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=15.35.0" defer></script>
|
||||
<script src="js/tarinai_render.js?v=15.35.0" defer></script>
|
||||
<script src="js/world.js?v=15.35.0" defer></script>
|
||||
<script src="js/world_view.js?v=15.35.0" defer></script>
|
||||
<script src="js/family_graph.js?v=15.35.0" defer></script>
|
||||
<script src="js/world_family_social.js?v=15.35.0" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=15.35.0" defer></script>
|
||||
<script src="js/world_environment.js?v=15.35.0" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=15.35.0" defer></script>
|
||||
<script src="js/world_ants_system.js?v=15.35.0" defer></script>
|
||||
<script src="js/weather_system.js?v=15.35.0" defer></script>
|
||||
<script src="js/world_update.js?v=15.35.0" defer></script>
|
||||
<script src="js/world_placement_log.js?v=15.35.0" defer></script>
|
||||
<script src="js/world_bootstrap.js?v=15.35.0" defer></script>
|
||||
<script src="js/text_catalog.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_log.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_helpers.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_selected.js?v=15.35.0" defer></script>
|
||||
<script src="js/snapshot_system.js?v=15.35.0" defer></script>
|
||||
<script src="js/save_system.js?v=15.35.0" defer></script>
|
||||
<script src="js/patch_helpers.js?v=15.35.0" defer></script>
|
||||
<script src="js/freeze_system.js?v=15.35.0" defer></script>
|
||||
<script src="js/relation_event_system.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_tools.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_bind.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_charts.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_family_data.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_family_async.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=15.35.0" defer></script>
|
||||
<script src="js/ui_family_render.js?v=15.35.0" defer></script>
|
||||
<script src="js/main.js?v=15.35.0" defer></script>
|
||||
<script src="js/debug_tools.js?v=15.35.0" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
60
js/data.js
|
|
@ -50,7 +50,7 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
observe: { id: "observe", label: "\u89b3\u5bdf", placeable: false, scalable: false, icon: "assets/ui/tool_observe.webp", tooltip: "\u500b\u4f53\u3092\u9078\u629e\u3057\u3001\u72b6\u614b\u30fb\u6027\u683c\u30fb\u75c5\u6c17\u30fb\u304a\u6c17\u306b\u5165\u308a\u3092\u78ba\u8a8d\u3059\u308b\u3002" },
|
||||
delete: { id: "delete", label: "\u524a\u9664", placeable: false, scalable: false, icon: "assets/ui/tool_delete.webp", tooltip: "\u7f6e\u3044\u305f\u9053\u5177\u3084\u6c5a\u308c\u3092\u6d88\u3059\u3002" },
|
||||
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" },
|
||||
pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "たりないや物を移動させる。冷凍庫にも持っていける。" },
|
||||
new: { id: "new", label: "\u8ffd\u52a0", placeable: false, scalable: false, icon: "assets/ui/tool_new.webp", 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}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" },
|
||||
freeze: { id: "freeze", label: "冷凍", placeable: false, scalable: false, iconText: "\u{1F9CA}", tooltip: "つまむでたりないを冷凍個体UIまで運ぶと冷凍保存する。" },
|
||||
|
|
@ -77,10 +77,12 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
nest_box: { id: "nest_box", itemType: "nest_box", label: "\u5de3\u7bb1", placeable: true, scalable: false, radius: 42, amount: 999, icon: "assets/ui/tool_nest_box.webp", collisionShape: "nest_box_3x3", tooltip: "\u3044\u308b\u3060\u3051\u3067\u30b9\u30c8\u30ec\u30b9\u4f4e\u6e1b\u3002\u7720\u308b\u3068\u7761\u7720\u306e\u8cea\u304c\u4e0a\u304c\u308b\u3002" },
|
||||
ant_nest: { id: "ant_nest", itemType: "ant_nest", label: "\u30a2\u30ea\u306e\u5de3", placeable: true, scalable: false, radius: 17, amount: 999, icon: "assets/ui/tool_ant_nest.webp", collisionShape: "circle", tooltip: "\u50cd\u304d\u30a2\u30ea\u304c\u305f\u308a\u306a\u3044\u3092\u63a2\u3057\u3001\u5de3\u3078\u904b\u3076\u3002" },
|
||||
ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, icon: "assets/ui/tool_ball.webp", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
|
||||
signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 24, amount: 999, iconText: "\u{1FAA7}", collisionShape: "circle", tooltip: "\u77ed\u3044\u30c6\u30ad\u30b9\u30c8\u3092\u66f8\u304d\u8fbc\u3081\u308b\u770b\u677f\u3002\u305f\u308a\u306a\u3044\u306b\u5bfe\u3059\u308b\u52b9\u679c\u306f\u7121\u3057" },
|
||||
signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 42, amount: 999, iconText: "\u{1FAA7}", collisionShape: "circle", tooltip: "短いテキストを書き込める大きめの看板。クリックで編集できる。たりないに対する効果は無し" },
|
||||
firecracker: { id: "firecracker", itemType: "firecracker", label: "\u7206\u7af9", placeable: true, scalable: true, radius: 15, amount: 999, icon: "assets/ui/tool_firecracker.webp", tooltip: "\u7206\u767a\u3092\u8d77\u3053\u3059\u3002" },
|
||||
pushpin: { id: "pushpin", itemType: "pushpin", label: "\u753b\u92f2", placeable: true, scalable: false, radius: 8, amount: 999, icon: "assets/ui/tool_pushpin.webp", tooltip: "\u843d\u3068\u3059\u3068\u3053\u308d\u304c\u308a\u3001\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\u3068\u30d1\u30cb\u30c3\u30af\u3068\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002" },
|
||||
oshibyo: { id: "oshibyo", itemType: "oshibyo", label: "おしり鋲", placeable: true, scalable: false, radius: 9, amount: 999, icon: "assets/objects/oshibyo.webp", tooltip: "刺さるとずんちが出なくなる。つまんで抜くと溜まったずんちが弾け飛ぶ。" },
|
||||
pushpin: { id: "pushpin", itemType: "pushpin", label: "\u753b\u92f2", placeable: true, scalable: false, radius: 8, amount: 999, icon: "assets/ui/tool_pushpin.webp", tooltip: "転がってたりないに刺さる。" },
|
||||
oshibyo: { id: "oshibyo", itemType: "oshibyo", label: "おしり鋲", placeable: true, scalable: false, radius: 5, amount: 999, icon: "assets/ui/tool_oshibyo.webp", tooltip: "転がってずんちのおでかけを防止。" },
|
||||
duplicator: { id: "duplicator", itemType: "duplicator", label: "複製機", placeable: true, scalable: false, radius: 44, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "セットした消費アイテムを減らさず皿に出し続ける。" },
|
||||
cannon: { id: "cannon", itemType: "cannon", label: "大砲", placeable: true, scalable: false, radius: 42, amount: 999, icon: "assets/ui/tool_cannon.webp", collisionShape: "circle", tooltip: "セットした消費アイテムを向いている方向へ撃ち出す。クリック位置で向きを変える。" },
|
||||
fence_v: { id: "fence_v", itemType: "fence_v", label: "\u7e26\u306e\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.webp", collisionShape: "rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u3002" },
|
||||
fence_h: { id: "fence_h", itemType: "fence_h", label: "\u6a2a\u306e\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_h.webp", collisionShape: "rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u3002" },
|
||||
water: { id: "water", itemType: "water", label: "\u6c34", placeable: false, scalable: false, radius: 12, amount: 64, simulationOnly: true },
|
||||
|
|
@ -92,13 +94,14 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
|
||||
|
||||
const ITEM_TRAITS = Object.freeze({
|
||||
obstacle: new Set(["stone", "ball", "nest_box", "bed", "fence_v", "fence_h"]),
|
||||
obstacle: new Set(["stone", "ball", "nest_box", "bed", "fence_v", "fence_h", "duplicator", "cannon"]),
|
||||
food_interest: new Set(["sweet", "love_mochi", "fight_mochi", "grass", "water", "water_bowl", "ant_corpse", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "sleep_drug"]),
|
||||
hazard: new Set(["firecracker", "genkotsu", "pushpin", "oshibyo", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
|
||||
pin: new Set(["pushpin", "oshibyo"]),
|
||||
kinematic: new Set(["ball", "pushpin", "oshibyo", "zunchi"]),
|
||||
machine: new Set(["duplicator", "cannon"]),
|
||||
sleepFurniture: new Set(["bed", "nest_box"]),
|
||||
draggable: new Set(["ball", "stone", "bed", "nest_box", "signboard", "pushpin", "oshibyo"]),
|
||||
draggable: new Set(["ball", "stone", "bed", "nest_box", "signboard", "pushpin", "oshibyo", "duplicator", "cannon"]),
|
||||
});
|
||||
|
||||
function itemHasTrait(type = "", trait = "") {
|
||||
|
|
@ -113,6 +116,13 @@ function itemTraitsFor(type = "") {
|
|||
|
||||
function isPinType(type = "") { return itemHasTrait(type, "pin"); }
|
||||
function isSleepFurnitureType(type = "") { return itemHasTrait(type, "sleepFurniture"); }
|
||||
function isMachineType(type = "") { return itemHasTrait(type, "machine"); }
|
||||
function isMachineConsumableType(type = "") {
|
||||
const t = String(type || "");
|
||||
if (!t) return false;
|
||||
if (typeof isServingFoodType === "function" && isServingFoodType(t)) return true;
|
||||
return ["grass", "water", "water_bowl", "zunchi", "ant_corpse"].includes(t);
|
||||
}
|
||||
function isLodgedPin(item = null) { return Boolean(item && !item.dead && isPinType(item.type) && item.pinState === "lodged"); }
|
||||
function lodgedPinFor(tarinai = null, worldRef = null) {
|
||||
if (!tarinai?.stuckPushpinId) return null;
|
||||
|
|
@ -124,7 +134,7 @@ function hasLodgedPinEffect(tarinai = null, effectKey = "") { return Boolean(lod
|
|||
|
||||
const PIN_BEHAVIORS = Object.freeze({
|
||||
pushpin: { type: "pushpin", looseAsset: "pushpin", lodgedAsset: "pushpin_stuck", attachMode: "impact", panicOnAttach: true, damageOnAttach: 6, damagePerTick: 1.4, stressOnAttach: 18, stressPerTick: 2.6, blocksZunchi: false, burstZunchiOnDetach: false },
|
||||
oshibyo: { type: "oshibyo", looseAsset: "oshibyo", lodgedAsset: "oshibyo_stuck", attachMode: "butt", panicOnAttach: false, damageOnAttach: 0, damagePerTick: 0, stressOnAttach: 0, stressPerTick: 0, blocksZunchi: true, burstZunchiOnDetach: true },
|
||||
oshibyo: { type: "oshibyo", looseAsset: "oshibyo", lodgedAsset: "oshibyo_stuck", attachMode: "butt", panicOnAttach: false, damageOnAttach: 0, damagePerTick: 0, stressOnAttach: 0, stressPerTick: 0, blocksZunchi: true, burstZunchiOnDetach: true, scale: 0.50 },
|
||||
});
|
||||
|
||||
function pinBehaviorFor(type = "") {
|
||||
|
|
@ -133,12 +143,11 @@ function pinBehaviorFor(type = "") {
|
|||
|
||||
|
||||
const TOOL_CATEGORIES = Object.freeze([
|
||||
{ id: "operate", label: "\u64cd\u4f5c\u30fb\u89b3\u5bdf", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "delete", "poke", "pinch", "new", "water_hose"] },
|
||||
{ id: "food", label: "\u98df\u3079\u7269\u30fb\u6c34\u30fb\u8349", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water_bowl", "zunda_juice"] },
|
||||
{ id: "medicine", label: "\u85ac\u30fb\u500b\u4f53\u52b9\u679c", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"] },
|
||||
{ id: "habitat", label: "\u751f\u6d3b\u30fb\u751f\u304d\u7269", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "bed", "nest_box", "ant_nest", "ball", "signboard"] },
|
||||
{ id: "hazard", label: "\u5371\u967a\u30fb\u969c\u5bb3\u7269", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "oshibyo", "fence_v", "fence_h"] },
|
||||
{ id: "freeze", label: "冷凍", themeClass: "tool-category-freeze", order: 99, toolIds: ["freeze"] },
|
||||
{ id: "operate", label: "操作", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "delete", "poke", "pinch", "new", "water_hose"] },
|
||||
{ id: "food", label: "飲食", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water_bowl", "zunda_juice"] },
|
||||
{ id: "medicine", label: "薬", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"] },
|
||||
{ id: "habitat", label: "生活", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "bed", "nest_box", "ant_nest", "ball", "signboard", "duplicator", "cannon", "fence_v", "fence_h"] },
|
||||
{ id: "hazard", label: "危険物", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "oshibyo"] },
|
||||
]);
|
||||
|
||||
function toolCategories() {
|
||||
|
|
@ -194,12 +203,12 @@ function toolTipsFromDefinitions() {
|
|||
const ALPHA_BOUNDS = {
|
||||
smile: { x: 18, y: 18, w: 476, h: 432, imageW: 512, imageH: 468 },
|
||||
angry: { x: 16, y: 16, w: 480, h: 432, imageW: 512, imageH: 464 },
|
||||
teary: { x: 17, y: 17, w: 478, h: 438, imageW: 512, imageH: 472 },
|
||||
jito: { x: 18, y: 18, w: 476, h: 451, imageW: 512, imageH: 487 },
|
||||
teary: { x: 17, y: 17, w: 474, h: 434, imageW: 508, imageH: 468 },
|
||||
jito: { x: 18, y: 18, w: 456, h: 432, imageW: 492, imageH: 468 },
|
||||
drool: { x: 18, y: 18, w: 476, h: 425, imageW: 512, imageH: 461 },
|
||||
cry: { x: 15, y: 15, w: 482, h: 448, imageW: 512, imageH: 478 },
|
||||
sleep: { x: 18, y: 18, w: 475, h: 439, imageW: 511, imageH: 475 },
|
||||
pokan: { x: 18, y: 18, w: 476, h: 433, imageW: 512, imageH: 469 },
|
||||
cry: { x: 15, y: 15, w: 471, h: 438, imageW: 501, imageH: 468 },
|
||||
sleep: { x: 18, y: 18, w: 467, h: 432, imageW: 503, imageH: 468 },
|
||||
pokan: { x: 18, y: 17, w: 475, h: 434, imageW: 511, imageH: 468 },
|
||||
stretch: { x: 15, y: 15, w: 482, h: 354, imageW: 512, imageH: 384 },
|
||||
hurt: { x: 17, y: 21, w: 478, h: 432, imageW: 512, imageH: 468 },
|
||||
weak: { x: 53, y: 18, w: 405, h: 435, imageW: 512, imageH: 468 },
|
||||
|
|
@ -218,17 +227,22 @@ const ALPHA_BOUNDS = {
|
|||
stress_dizzy: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
|
||||
stress_sweat: { x: 16, y: 26, w: 480, h: 428, imageW: 512, imageH: 468 },
|
||||
intimidate: { x: 98, y: 16, w: 316, h: 438, imageW: 512, imageH: 468 },
|
||||
zunchi_slave: { x: 17, y: 22, w: 478, h: 431, imageW: 512, imageH: 468 },
|
||||
birth_ritual: { x: 0, y: 0, w: 512, h: 456, imageW: 512, imageH: 456 },
|
||||
zunchi_slave: { x: 17, y: 22, w: 477, h: 431, imageW: 512, imageH: 468 },
|
||||
hungry_70: { x: 0, y: 0, w: 430, h: 366, imageW: 430, imageH: 366 },
|
||||
zunchi_slave_alt: { x: 42, y: 22, w: 425, h: 426, imageW: 512, imageH: 468 },
|
||||
zunchi_slave_alt: { x: 33, y: 56, w: 446, h: 402, imageW: 512, imageH: 468 },
|
||||
sunbath_1: { x: 71, y: 18, w: 369, h: 432, imageW: 512, imageH: 468 },
|
||||
sunbath_2: { x: 69, y: 18, w: 374, h: 432, imageW: 512, imageH: 468 },
|
||||
sunbath_3: { x: 71, y: 18, w: 369, h: 432, imageW: 512, imageH: 468 },
|
||||
zunchi: { x: 0, y: 0, w: 236, h: 178, imageW: 236, imageH: 178 },
|
||||
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 },
|
||||
sunbath_1: { x: 71, y: 18, w: 369, h: 432, imageW: 512, imageH: 468 },
|
||||
sunbath_2: { x: 69, y: 18, w: 374, h: 432, imageW: 512, imageH: 468 },
|
||||
sunbath_3: { x: 71, y: 18, w: 369, h: 432, imageW: 512, imageH: 468 },
|
||||
pushpin: { x: 6, y: 6, w: 82, h: 186, imageW: 95, imageH: 200 },
|
||||
pushpin_stuck: { x: 7, y: 7, w: 135, h: 135, imageW: 150, imageH: 150 },
|
||||
oshibyo: { x: 0, y: 0, w: 198, h: 449, imageW: 198, imageH: 449 },
|
||||
oshibyo_stuck: { x: 0, y: 0, w: 247, h: 247, imageW: 247, imageH: 247 },
|
||||
};
|
||||
|
||||
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"];
|
||||
|
|
|
|||
|
|
@ -44,8 +44,12 @@
|
|||
const style = document.createElement("style");
|
||||
style.id = "freezeSystemStyles";
|
||||
style.textContent = `
|
||||
.frozen-panel { margin-top: 10px; padding: 10px; border: 1px solid rgba(82, 63, 43, 0.18); border-radius: 14px; background: rgba(244, 249, 255, 0.72); }
|
||||
.frozen-panel-title { display:flex; align-items:center; justify-content:space-between; gap:8px; font-weight:700; font-size:13px; color:#4f5f70; margin-bottom:8px; }
|
||||
.frozen-panel { margin-top: 10px; padding: 8px; border: 1px solid rgba(82, 63, 43, 0.18); border-radius: 14px; background: rgba(244, 249, 255, 0.72); }
|
||||
.frozen-panel-title { display:flex; align-items:center; justify-content:space-between; gap:8px; width:100%; border:0; background:transparent; padding:0; font-weight:700; font-size:13px; color:#4f5f70; margin:0; cursor:pointer; text-align:left; }
|
||||
.frozen-panel-title::before { content:"▸"; width:14px; color:rgba(79,95,112,.74); }
|
||||
.frozen-panel:not(.collapsed) .frozen-panel-title::before { content:"▾"; }
|
||||
.frozen-panel-body { margin-top:8px; }
|
||||
.frozen-panel.collapsed .frozen-panel-body { display:none; }
|
||||
.frozen-panel-count { font-size:12px; opacity:.72; }
|
||||
.frozen-list { display:grid; grid-template-columns: 1fr; gap:7px; }
|
||||
.frozen-card { display:flex; align-items:center; gap:8px; width:100%; border:1px solid rgba(72, 98, 124, .20); border-radius:12px; padding:7px; background:rgba(255,255,255,.78); cursor:grab; color:#354657; text-align:left; box-shadow: 0 2px 7px rgba(41,53,65,.06); }
|
||||
|
|
@ -64,16 +68,23 @@
|
|||
function ensurePanel() {
|
||||
ensureStyles();
|
||||
let panel = document.getElementById("frozenPanel");
|
||||
if (panel) return panel;
|
||||
if (panel) {
|
||||
const operateBody = document.querySelector('[data-tool-category="operate"] .tool-category-body');
|
||||
if (operateBody && panel.parentNode !== operateBody) operateBody.appendChild(panel);
|
||||
return panel;
|
||||
}
|
||||
panel = document.createElement("section");
|
||||
panel.id = "frozenPanel";
|
||||
panel.className = "frozen-panel";
|
||||
panel.classList.add("collapsed");
|
||||
panel.innerHTML = `
|
||||
<div class="frozen-panel-title"><span>冷凍個体</span><span id="frozenPanelCount" class="frozen-panel-count">0</span></div>
|
||||
<div id="frozenList" class="frozen-list"></div>
|
||||
<button class="frozen-panel-title" type="button" aria-expanded="false"><span>冷凍個体</span><span id="frozenPanelCount" class="frozen-panel-count">0</span></button>
|
||||
<div class="frozen-panel-body"><div id="frozenList" class="frozen-list"></div></div>
|
||||
`;
|
||||
const operateBody = document.querySelector('[data-tool-category="operate"] .tool-category-body');
|
||||
const anchor = global.ui?.toolPalette || document.getElementById("toolPalette");
|
||||
if (anchor?.parentNode) anchor.parentNode.insertBefore(panel, anchor.nextSibling);
|
||||
if (operateBody) operateBody.appendChild(panel);
|
||||
else if (anchor?.parentNode) anchor.parentNode.insertBefore(panel, anchor.nextSibling);
|
||||
else document.body.appendChild(panel);
|
||||
return panel;
|
||||
}
|
||||
|
|
@ -246,6 +257,12 @@
|
|||
const panel = ensurePanel();
|
||||
if (panel.dataset.freezeBound !== "1") {
|
||||
panel.dataset.freezeBound = "1";
|
||||
panel.addEventListener("click", (e) => {
|
||||
const title = e.target.closest(".frozen-panel-title");
|
||||
if (!title) return;
|
||||
const collapsed = !panel.classList.toggle("collapsed");
|
||||
title.setAttribute("aria-expanded", collapsed ? "true" : "false");
|
||||
});
|
||||
panel.addEventListener("dragstart", (e) => {
|
||||
const card = e.target.closest(".frozen-card[data-frozen-id]");
|
||||
if (!card) return;
|
||||
|
|
|
|||
287
js/items.js
|
|
@ -174,6 +174,43 @@ function drawBulletSprite(ctx, r) {
|
|||
ctx.restore();
|
||||
}
|
||||
|
||||
function machineDishPosition(machine) {
|
||||
const r = machine?.r || 42;
|
||||
return { x: machine.x, y: machine.y + r * 1.18 };
|
||||
}
|
||||
|
||||
function machineMuzzlePosition(machine) {
|
||||
const r = machine?.r || 42;
|
||||
const a = Number.isFinite(machine?.cannonAngle) ? machine.cannonAngle : 0;
|
||||
return { x: machine.x + Math.cos(a) * r * 0.98, y: machine.y + Math.sin(a) * r * 0.98 };
|
||||
}
|
||||
|
||||
function makeMachineConsumableCopy(type, x, y, source = null, opts = {}) {
|
||||
const it = new Item(type, x, y);
|
||||
it.machineProduced = true;
|
||||
it.machineProducerId = source?.id || "";
|
||||
it.sourceMachineType = source?.type || "";
|
||||
it.noDecay = true;
|
||||
if (opts.infiniteServing) it.infiniteServing = true;
|
||||
if (opts.projectile) {
|
||||
it.cannonProjectile = true;
|
||||
it.projectileSourceId = source?.id || "";
|
||||
it.projectileTimer = 0;
|
||||
it.projectileHitMemo = {};
|
||||
}
|
||||
if (typeof isServingFoodType === "function" && isServingFoodType(type)) {
|
||||
it.toolSize = source?.machineSlotSize || "medium";
|
||||
it.foodServingScale = 1;
|
||||
it.foodServingsMax = typeof foodServingsForSize === "function" ? foodServingsForSize(it.toolSize) : 5;
|
||||
it.foodServingsRemaining = it.foodServingsMax;
|
||||
it.amount = it.foodServingsMax;
|
||||
}
|
||||
if (type === "water") it.amount = 120;
|
||||
if (type === "grass") { it.amount = Math.max(it.amount || 0, 120); it.growth = Math.max(it.growth || 0, 0.82); }
|
||||
if (type === "zunchi") { it.amount = 240; it.stage = "fresh"; it.stageTimer = 0; it.freshness = 1; }
|
||||
return it;
|
||||
}
|
||||
|
||||
class Item {
|
||||
constructor(type, x, y) {
|
||||
this.id = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
|
||||
|
|
@ -206,6 +243,14 @@ class Item {
|
|||
this.text = "";
|
||||
this.textEditedAt = -999;
|
||||
}
|
||||
if (type === "duplicator" || type === "cannon") {
|
||||
this.machineSlotType = "";
|
||||
this.machineSlotSize = "medium";
|
||||
this.machineSpawnTimer = rand(0.4, 1.1);
|
||||
this.machineLastSlotType = "";
|
||||
this.cannonAngle = type === "cannon" ? -Math.PI * 0.25 : 0;
|
||||
this.cannonFireTimer = rand(0.8, 1.6);
|
||||
}
|
||||
if (type === "ball") {
|
||||
this.vx = rand(-5, 5);
|
||||
this.vy = rand(-5, 5);
|
||||
|
|
@ -278,6 +323,8 @@ class Item {
|
|||
this.passiveFoodDecayTimer = rand(0, passiveFoodDecayInterval());
|
||||
this.amount = this.foodServingsRemaining;
|
||||
}
|
||||
if (window.TarinaiNeedSystem?.itemRolesFor) window.TarinaiNeedSystem.itemRolesFor(this);
|
||||
if (window.TarinaiNeedSystem?.ensureItemNeedEffects) window.TarinaiNeedSystem.ensureItemNeedEffects(this);
|
||||
}
|
||||
update(dt, worldRef = world) {
|
||||
this.age += dt;
|
||||
|
|
@ -289,8 +336,8 @@ class Item {
|
|||
if (worldRef.itemDropImpact) worldRef.itemDropImpact(this);
|
||||
}
|
||||
}
|
||||
if (this.type === "water") this.amount -= dt * 0.9;
|
||||
if (isServingFoodType(this.type)) {
|
||||
if (this.type === "water" && !this.noDecay && !this.infiniteServing) this.amount -= dt * 0.9;
|
||||
if (isServingFoodType(this.type) && !this.noDecay && !this.infiniteServing) {
|
||||
const interval = passiveFoodDecayInterval(worldRef);
|
||||
this.passiveFoodDecayTimer = (this.passiveFoodDecayTimer || 0) + dt;
|
||||
let ticks = 0;
|
||||
|
|
@ -344,6 +391,9 @@ class Item {
|
|||
if (this.type === "ball") this.updateBall(dt, worldRef);
|
||||
if (isPinType(this.type)) this.updatePushpin(dt, worldRef);
|
||||
if (this.type === "zunchi") this.updateZunchiMotion(dt, worldRef);
|
||||
if (this.cannonProjectile) this.updateConsumableProjectile(dt, worldRef);
|
||||
if (this.type === "duplicator") this.updateDuplicator(dt, worldRef);
|
||||
if (this.type === "cannon") this.updateCannon(dt, worldRef);
|
||||
if (this.type === "grass") {
|
||||
this.lifecycleTimer += dt;
|
||||
const factor = worldRef.grassUpdateFactor ? worldRef.grassUpdateFactor() : 1;
|
||||
|
|
@ -364,6 +414,152 @@ class Item {
|
|||
}
|
||||
}
|
||||
|
||||
machineDishPosition() {
|
||||
return machineDishPosition(this);
|
||||
}
|
||||
|
||||
clearMachineProducedItems(worldRef, keepType = "") {
|
||||
for (const it of worldRef?.items || []) {
|
||||
if (!it || it.dead || it.machineProducerId !== this.id) continue;
|
||||
if (keepType && it.type === keepType && this.type === "duplicator") continue;
|
||||
it.amount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateDuplicator(dt, worldRef) {
|
||||
if (!worldRef || !this.machineSlotType) return;
|
||||
if (this.machineLastSlotType && this.machineLastSlotType !== this.machineSlotType) this.clearMachineProducedItems(worldRef);
|
||||
this.machineLastSlotType = this.machineSlotType;
|
||||
this.machineSpawnTimer = Math.max(0, (this.machineSpawnTimer || 0) - dt);
|
||||
const dish = this.machineDishPosition();
|
||||
let serving = null;
|
||||
for (const it of worldRef.items || []) {
|
||||
if (!it || it.dead || it.machineProducerId !== this.id || it.type !== this.machineSlotType) continue;
|
||||
serving = it;
|
||||
break;
|
||||
}
|
||||
if (!serving && this.machineSpawnTimer <= 0) {
|
||||
serving = makeMachineConsumableCopy(this.machineSlotType, dish.x, dish.y, this, { infiniteServing: true });
|
||||
serving.r = Math.max(6, (serving.r || 12) * 0.92);
|
||||
worldRef.items.push(serving);
|
||||
worldRef.itemCounts[serving.type] = (worldRef.itemCounts[serving.type] || 0) + 1;
|
||||
this.machineSpawnTimer = 1.2;
|
||||
worldRef.rebuildSpatial?.(true);
|
||||
}
|
||||
if (serving) {
|
||||
serving.x = dish.x;
|
||||
serving.y = dish.y;
|
||||
serving.vx = 0;
|
||||
serving.vy = 0;
|
||||
serving.prevX = serving.x;
|
||||
serving.prevY = serving.y;
|
||||
serving.noDecay = true;
|
||||
serving.infiniteServing = true;
|
||||
serving.machineProduced = true;
|
||||
serving.machineProducerId = this.id;
|
||||
if (typeof isServingFoodType === "function" && isServingFoodType(serving.type)) {
|
||||
if (!Number.isFinite(serving.foodServingsMax) || serving.foodServingsMax <= 0) serving.foodServingsMax = typeof foodServingsForSize === "function" ? foodServingsForSize(serving.toolSize || "medium") : 5;
|
||||
serving.foodServingsRemaining = serving.foodServingsMax;
|
||||
serving.amount = serving.foodServingsMax;
|
||||
} else if (serving.type === "water") serving.amount = Math.max(serving.amount || 0, 120);
|
||||
else if (serving.type === "grass") serving.amount = Math.max(serving.amount || 0, 120);
|
||||
else if (serving.type === "zunchi") serving.amount = Math.max(serving.amount || 0, 240);
|
||||
}
|
||||
}
|
||||
|
||||
updateCannon(dt, worldRef) {
|
||||
if (!worldRef || !this.machineSlotType) return;
|
||||
this.cannonFireTimer = Math.max(0, (this.cannonFireTimer || 0) - dt);
|
||||
if (this.cannonFireTimer > 0) return;
|
||||
this.cannonFireTimer = rand(2.2, 3.4);
|
||||
const a = Number.isFinite(this.cannonAngle) ? this.cannonAngle : 0;
|
||||
const muzzle = machineMuzzlePosition(this);
|
||||
const shot = makeMachineConsumableCopy(this.machineSlotType, muzzle.x, muzzle.y, this, { projectile: true });
|
||||
shot.r = Math.max(5, (shot.r || 12) * 0.78);
|
||||
const speed = rand(470, 620);
|
||||
shot.vx = Math.cos(a) * speed + rand(-18, 18);
|
||||
shot.vy = Math.sin(a) * speed + rand(-18, 18);
|
||||
shot.prevX = shot.x;
|
||||
shot.prevY = shot.y;
|
||||
shot.spinVelocity = rand(-6, 6) + speed / Math.max(8, shot.r || 10) * 0.12;
|
||||
shot.amount = Math.max(shot.amount || 0, typeof isServingFoodType === "function" && isServingFoodType(shot.type) ? (shot.foodServingsMax || 5) : itemAmountFor(shot.type, 60));
|
||||
worldRef.items.push(shot);
|
||||
worldRef.itemCounts[shot.type] = (worldRef.itemCounts[shot.type] || 0) + 1;
|
||||
worldRef.effects?.push(new Effect("ring", muzzle.x, muzzle.y, { size: Math.max(16, (this.r || 42) * 0.42), life: 0.22, color: "rgba(255,238,160,0.52)" }));
|
||||
worldRef.log?.(`大砲が${toolLabel(shot.type)}を撃ち出した。`, "accident");
|
||||
audio.place?.(shot.type);
|
||||
worldRef.rebuildSpatial?.(true);
|
||||
}
|
||||
|
||||
updateConsumableProjectile(dt, worldRef) {
|
||||
if (!worldRef) return;
|
||||
this.projectileTimer = (this.projectileTimer || 0) + dt;
|
||||
this.prevX = this.x;
|
||||
this.prevY = this.y;
|
||||
const speedBefore = Math.hypot(this.vx || 0, this.vy || 0);
|
||||
if (speedBefore > 0.05) {
|
||||
if (window.TarinaiPhysics?.applyKinematicItemMotion) {
|
||||
window.TarinaiPhysics.applyKinematicItemMotion(this, worldRef, dt, { padding: 28, bounce: 0.46, frictionBase: 0.62, frictionRate: 1.25, spin: false, stopSpeed: 0.12, zeroBelow: 14 });
|
||||
} else {
|
||||
this.x += (this.vx || 0) * dt;
|
||||
this.y += (this.vy || 0) * dt;
|
||||
const p = Math.max(28, CONFIG.worldPadding || 30);
|
||||
if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx || 0) * 0.46; }
|
||||
if (this.x > worldRef.w - p) { this.x = worldRef.w - p; this.vx = -Math.abs(this.vx || 0) * 0.46; }
|
||||
if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy || 0) * 0.46; }
|
||||
if (this.y > worldRef.h - p) { this.y = worldRef.h - p; this.vy = -Math.abs(this.vy || 0) * 0.46; }
|
||||
const slow = Math.pow(0.62, dt * 1.25);
|
||||
this.vx *= slow;
|
||||
this.vy *= slow;
|
||||
}
|
||||
this.spin = (this.spin || 0) + speedBefore * dt / Math.max(6, this.r || 10);
|
||||
this.resolveProjectileTarinaiCollision(dt, worldRef, speedBefore);
|
||||
}
|
||||
const speedAfter = Math.hypot(this.vx || 0, this.vy || 0);
|
||||
if (speedAfter < 18 && (this.projectileTimer || 0) > 0.30) {
|
||||
this.cannonProjectile = false;
|
||||
this.noDecay = false;
|
||||
this.projectileHitMemo = {};
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
}
|
||||
if ((this.projectileTimer || 0) > 28) this.cannonProjectile = false;
|
||||
}
|
||||
|
||||
resolveProjectileTarinaiCollision(dt, worldRef, speed = 0) {
|
||||
if (!worldRef || speed < 210 || (this.amount || 0) <= 0) return;
|
||||
const range = Math.max(42, (this.r || 12) + speed * Math.max(dt || 0.016, 0.016) + 22);
|
||||
for (const t of worldRef.nearbyTarinai?.(this.x, this.y, range, true) || []) {
|
||||
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
const d = distXY(this.x, this.y, t.x, t.y);
|
||||
if (d > (t.radius || 20) * 0.72 + (this.r || 12)) continue;
|
||||
const now = worldRef.time || 0;
|
||||
this.projectileHitMemo = this.projectileHitMemo || {};
|
||||
if (this.projectileHitMemo[t.id] && this.projectileHitMemo[t.id] + 0.58 > now) continue;
|
||||
this.projectileHitMemo[t.id] = now;
|
||||
const nx = speed > 0 ? (this.vx || 1) / speed : rand(-1, 1);
|
||||
const ny = speed > 0 ? (this.vy || 0) / speed : rand(-1, 1);
|
||||
const impulse = clamp(speed * 0.95, 130, 560);
|
||||
t.vx = (t.vx || 0) + nx * impulse + rand(-18, 18);
|
||||
t.vy = (t.vy || 0) + ny * impulse * 0.70 + rand(-14, 12);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, 0.72);
|
||||
t.fallMax = Math.max(t.fallMax || 0.72, t.fallTimer);
|
||||
t.fallDir = nx < 0 ? -1 : 1;
|
||||
const label = toolLabel(this.type) || "食べ物";
|
||||
if (t.enterPanic) t.enterPanic({ threat: this, reason: `大砲から飛んできた${label}にぶつかって吹き飛んでいる`, fear: 0.72, stress: 6, stressDuration: 3.0, surpriseTimer: 0.48, cause: "cannon_projectile_hit" });
|
||||
else t.setActionState?.("panic", { target: { x: this.x, y: this.y }, reason: `大砲から飛んできた${label}にぶつかって吹き飛んでいる`, wake: true });
|
||||
if (worldRef.applyImpactDamage) worldRef.applyImpactDamage(t, clamp((speed - 190) / 30, 1.4, 16), "大砲");
|
||||
worldRef.spawnFallEffect?.(t.x, t.y + (t.radius || 20) * 0.50, 0.78);
|
||||
worldRef.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(17, (this.r || 12) * 1.15), life: 0.22, color: "rgba(220,160,70,0.52)" }));
|
||||
if (worldRef.relationNotice?.(t.id, this.id || "projectile", "cannon-projectile-hit", 2.8)) worldRef.log?.(`${t.name}は大砲から飛んできた${label}にぶつかって吹き飛ばされた。`, "accident", { participants: [t] });
|
||||
this.vx *= -0.12;
|
||||
this.vy *= -0.12;
|
||||
this.cannonProjectile = false;
|
||||
this.noDecay = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
updateZunchiMotion(dt, worldRef) {
|
||||
const speed = Math.hypot(this.vx || 0, this.vy || 0);
|
||||
if (speed < 0.08) { this.vx = 0; this.vy = 0; return; }
|
||||
|
|
@ -668,9 +864,11 @@ class Item {
|
|||
this.pinState = "lodged";
|
||||
this.deletable = false;
|
||||
this.pinTargetId = t.id;
|
||||
this.pinAttachAngle = isOshibyo ? 0.62 : Math.atan2(dy || -1, dx || (t.facingDir ? t.facingDir() : 1));
|
||||
this.pinAttachDistance = isOshibyo ? (t.radius || 16) * 0.58 : clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74);
|
||||
this.pinOffsetY = isOshibyo ? (t.radius || 16) * 0.30 : clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38);
|
||||
const visualSide = (t.facingDir ? t.facingDir() : (t.facing || 1)) > 0 ? -1 : 1;
|
||||
this.pinAttachAngle = isOshibyo ? (visualSide < 0 ? -0.48 : 0.48) : Math.atan2(dy || -1, dx || visualSide);
|
||||
this.pinAttachDistance = isOshibyo ? (t.radius || 16) * 0.76 : clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74);
|
||||
this.pinOffsetY = isOshibyo ? (t.radius || 16) * 0.22 : clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38);
|
||||
this.pinFlipX = visualSide;
|
||||
this.pinDamageTick = 0;
|
||||
this.pinFallCheckTimer = 0;
|
||||
this.vx = 0;
|
||||
|
|
@ -714,11 +912,13 @@ class Item {
|
|||
const isOshibyo = Boolean(behavior?.blocksZunchi);
|
||||
t.stuckPushpinId = this.id;
|
||||
this.deletable = false;
|
||||
this.x = isOshibyo ? t.x + (t.radius || 16) * 0.42 : t.x + (t.radius || 16) * 0.36;
|
||||
this.y = isOshibyo ? t.y + (t.radius || 16) * 0.34 : t.y - (t.radius || 16) * 0.04;
|
||||
const visualSide = (t.facingDir ? t.facingDir() : (t.facing || 1)) > 0 ? -1 : 1;
|
||||
this.pinFlipX = visualSide;
|
||||
this.x = t.x + visualSide * (isOshibyo ? (t.radius || 16) * 0.72 : (t.radius || 16) * 0.36);
|
||||
this.y = isOshibyo ? t.y + (t.radius || 16) * 0.22 : t.y - (t.radius || 16) * 0.04;
|
||||
this.prevX = this.x;
|
||||
this.prevY = this.y;
|
||||
this.spin = isOshibyo ? 0.52 : 0.28;
|
||||
this.spin = (isOshibyo ? 0.42 : 0.28) * (this.pinFlipX < 0 ? -1 : 1);
|
||||
this.vx = t.vx || 0;
|
||||
this.vy = t.vy || 0;
|
||||
if (isOshibyo) {
|
||||
|
|
@ -1114,20 +1314,89 @@ class Item {
|
|||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "duplicator" || this.type === "cannon") {
|
||||
const r = this.r || 42;
|
||||
const isCannon = this.type === "cannon";
|
||||
ctx.save();
|
||||
ctx.shadowColor = "rgba(40,35,30,0.24)";
|
||||
ctx.shadowBlur = 6;
|
||||
ctx.shadowOffsetY = 3;
|
||||
if (!isCannon) {
|
||||
// Upper machine body.
|
||||
const g = ctx.createLinearGradient(0, -r * 1.0, 0, r * 0.32);
|
||||
g.addColorStop(0, "#e7f1ff");
|
||||
g.addColorStop(1, "#8aa2bd");
|
||||
ctx.fillStyle = g;
|
||||
ctx.strokeStyle = "rgba(48,68,92,0.70)";
|
||||
ctx.lineWidth = Math.max(2, r * 0.06);
|
||||
roundedRect(ctx, -r * 0.82, -r * 1.05, r * 1.64, r * 0.92, r * 0.18);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = this.machineSlotType ? "rgba(100,214,102,0.92)" : "rgba(255,180,72,0.92)";
|
||||
ctx.beginPath(); ctx.arc(r * 0.48, -r * 0.76, r * 0.12, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.strokeStyle = "rgba(42,55,68,0.54)";
|
||||
ctx.lineWidth = Math.max(1.6, r * 0.04);
|
||||
ctx.beginPath(); ctx.moveTo(-r * 0.48, -r * 0.78); ctx.lineTo(r * 0.20, -r * 0.78); ctx.stroke();
|
||||
// Plate for tarinai below.
|
||||
ctx.fillStyle = "rgba(245,244,233,0.96)";
|
||||
ctx.strokeStyle = "rgba(116,96,75,0.58)";
|
||||
ctx.lineWidth = Math.max(2, r * 0.05);
|
||||
ctx.beginPath(); ctx.ellipse(0, r * 1.18, r * 0.95, r * 0.34, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = "rgba(196,180,154,0.22)";
|
||||
ctx.beginPath(); ctx.ellipse(0, r * 1.18, r * 0.58, r * 0.16, 0, 0, Math.PI * 2); ctx.fill();
|
||||
if (this.machineSlotType) {
|
||||
ctx.fillStyle = "rgba(42,36,28,0.72)";
|
||||
ctx.font = `${Math.max(10, Math.round(r * 0.25))}px ui-rounded, sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(toolLabel(this.machineSlotType), 0, r * 0.52);
|
||||
}
|
||||
} else {
|
||||
const a = Number.isFinite(this.cannonAngle) ? this.cannonAngle : 0;
|
||||
// Base plate.
|
||||
ctx.fillStyle = "#6c7685";
|
||||
ctx.strokeStyle = "rgba(42,48,58,0.72)";
|
||||
ctx.lineWidth = Math.max(2, r * 0.06);
|
||||
ctx.beginPath(); ctx.ellipse(0, r * 0.72, r * 0.78, r * 0.30, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
// Barrel.
|
||||
ctx.rotate(a);
|
||||
const bg = ctx.createLinearGradient(-r * 0.34, -r * 0.26, r * 1.08, r * 0.26);
|
||||
bg.addColorStop(0, "#596575");
|
||||
bg.addColorStop(0.55, "#2f3946");
|
||||
bg.addColorStop(1, "#1f2630");
|
||||
ctx.fillStyle = bg;
|
||||
ctx.strokeStyle = "rgba(16,20,26,0.82)";
|
||||
roundedRect(ctx, -r * 0.28, -r * 0.26, r * 1.32, r * 0.52, r * 0.18);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = "rgba(10,12,16,0.88)";
|
||||
ctx.beginPath(); ctx.ellipse(r * 1.02, 0, r * 0.16, r * 0.23, 0, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.rotate(-a);
|
||||
ctx.fillStyle = "#8793a5";
|
||||
ctx.beginPath(); ctx.arc(0, 0, r * 0.36, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
if (this.machineSlotType) {
|
||||
ctx.fillStyle = "rgba(42,36,28,0.72)";
|
||||
ctx.font = `${Math.max(10, Math.round(r * 0.23))}px ui-rounded, sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(toolLabel(this.machineSlotType), 0, -r * 0.62);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (isPinType(this.type)) {
|
||||
const stuck = this.pinState === "lodged";
|
||||
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(this.type) : null;
|
||||
const assetId = behavior ? (stuck ? behavior.lodgedAsset : behavior.looseAsset) : (stuck ? "pushpin_stuck" : "pushpin");
|
||||
const isOshibyo = Boolean(behavior?.blocksZunchi);
|
||||
const img = typeof getRenderableImage === "function" ? getRenderableImage(assetId, assetId) : images.get(assetId);
|
||||
ctx.save();
|
||||
if (!stuck) ctx.rotate((this.spin || 0) + Math.PI * 0.5);
|
||||
else if ((this.pinFlipX || 1) < 0) ctx.scale(-1, 1);
|
||||
ctx.shadowColor = stuck ? "rgba(116,24,32,0.18)" : "rgba(54,42,31,0.16)";
|
||||
ctx.shadowBlur = 3;
|
||||
ctx.shadowOffsetY = 2;
|
||||
if (img) {
|
||||
const metrics = typeof getImageMetrics === "function" ? getImageMetrics(assetId) : null;
|
||||
const ratio = metrics?.ratio || (stuck ? 1 : 2.0);
|
||||
const w = isOshibyo ? (stuck ? this.r * 3.1 : this.r * 1.55) : (stuck ? this.r * 2.15 : this.r * 1.78);
|
||||
const w = isOshibyo ? (stuck ? this.r * 2.45 : this.r * 1.25) : (stuck ? this.r * 2.15 : this.r * 1.78);
|
||||
const h = w * ratio;
|
||||
ctx.drawImage(img, -w * 0.5, -h * 0.5, w, h);
|
||||
} else if (stuck) {
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ loadImages(null, (done, total) => {
|
|||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
const swUrl = window.TARINAI_APP?.withVersion?.("./service-worker.js") || "./service-worker.js?v=15.20.16";
|
||||
const swUrl = window.TARINAI_APP?.withVersion?.("./service-worker.js") || "./service-worker.js?v=15.30.0";
|
||||
navigator.serviceWorker.register(swUrl, { updateViaCache: "none" }).catch(() => undefined);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
547
js/need_behavior_system.js
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const NEED_KEYS = Object.freeze(["food", "sleep", "health", "safety", "social", "fulfill"]);
|
||||
const NEED_LABELS = Object.freeze({
|
||||
food: "\u6442\u990c",
|
||||
sleep: "\u7761\u7720",
|
||||
health: "\u5065\u5eb7",
|
||||
safety: "\u5b89\u5168",
|
||||
social: "\u95a2\u4fc2",
|
||||
fulfill: "\u5145\u8db3",
|
||||
});
|
||||
const NEED_PHRASES = Object.freeze({
|
||||
food: "\u304a\u306a\u304b\u304c\u3059\u3044\u3066\u3044\u308b",
|
||||
sleep: "\u306d\u3080\u3044",
|
||||
health: "\u8abf\u5b50\u304c\u60aa\u3044",
|
||||
safety: "\u3042\u3076\u306a\u3044",
|
||||
social: "\u3060\u308c\u304b\u304c\u6c17\u306b\u306a\u308b",
|
||||
fulfill: "\u81ea\u5206\u306e\u5834\u6240\u304c\u6c17\u306b\u306a\u308b",
|
||||
});
|
||||
const NEED_PRIORITY = Object.freeze(["safety", "health", "food", "sleep", "social", "fulfill"]);
|
||||
|
||||
function createId(prefix = "id") {
|
||||
if (global.crypto?.randomUUID) return `${prefix}_${global.crypto.randomUUID()}`;
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function createDefaultNeeds() {
|
||||
return { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 };
|
||||
}
|
||||
|
||||
function quantizeNeed(value) {
|
||||
return Math.max(0, Math.min(100, Math.round((Number(value) || 0) / 10) * 10));
|
||||
}
|
||||
|
||||
function getNeedDisplayValue(value) {
|
||||
return Math.round(quantizeNeed(value) / 10);
|
||||
}
|
||||
|
||||
function personalityValue(t, key) {
|
||||
if (typeof global.effectivePersonalityValue === "function") return Number(global.effectivePersonalityValue(t, key)) || 0;
|
||||
return Number(t?.currentPersonality?.[key] ?? t?.birthPersonality?.[key] ?? 0) || 0;
|
||||
}
|
||||
|
||||
function needPressure(value) {
|
||||
const x = clamp((Number(value) || 0) / 100, 0, 1);
|
||||
return x * x * 100;
|
||||
}
|
||||
|
||||
function deriveStressFromNeeds(needs = {}) {
|
||||
return clamp(
|
||||
needPressure(needs.food) * 0.10 +
|
||||
needPressure(needs.sleep) * 0.12 +
|
||||
needPressure(needs.health) * 0.25 +
|
||||
needPressure(needs.safety) * 0.30 +
|
||||
needPressure(needs.social) * 0.08 +
|
||||
needPressure(needs.fulfill) * 0.15,
|
||||
0,
|
||||
130
|
||||
);
|
||||
}
|
||||
|
||||
function itemRolesFor(item) {
|
||||
if (!item) return {};
|
||||
if (!item.roles) item.roles = {};
|
||||
const type = String(item.type || "");
|
||||
const roles = item.roles;
|
||||
if (["sweet", "food", "grass", "ant_corpse", "zunchi", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"].includes(type)) roles.food = true;
|
||||
if (type === "water" || type === "water_bowl") roles.drink = true;
|
||||
if (["sweet", "protein", "water", "water_bowl", "zunda_juice"].includes(type)) roles.medicine = true;
|
||||
if (type === "bed" || type === "nest_box") roles.sleepPlace = true;
|
||||
if (["firecracker", "genkotsu", "pushpin", "oshibyo", "splat", "zunchi", "ant_nest", "cannon"].includes(type)) roles.danger = true;
|
||||
if (type === "grass") roles.grassMaterial = true;
|
||||
if (type === "ball") roles.playObject = true;
|
||||
if (item.isStructure) roles.ownedStructure = true;
|
||||
return roles;
|
||||
}
|
||||
|
||||
function ensureItemNeedEffects(item) {
|
||||
if (!item) return createDefaultNeeds();
|
||||
if (!item.needEffects) item.needEffects = createDefaultNeeds();
|
||||
return item.needEffects;
|
||||
}
|
||||
|
||||
function findNearestItemWithRole(world, t, role, maxDist = 640, predicate = null) {
|
||||
let best = null;
|
||||
let bestD = maxDist;
|
||||
const items = Number.isFinite(maxDist) && world?.nearbyItems ? world.nearbyItems(t.x, t.y, maxDist) : (world?.items || []);
|
||||
for (const it of items) {
|
||||
if (!it || it.dead || (it.amount != null && it.amount <= 0)) continue;
|
||||
const roles = itemRolesFor(it);
|
||||
if (!roles[role]) continue;
|
||||
if (predicate && !predicate(it)) continue;
|
||||
if (t?.shouldAvoidTarget?.(it)) continue;
|
||||
const d = distXY(t.x, t.y, it.x, it.y);
|
||||
if (d < bestD) { best = it; bestD = d; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function findNearestSleepPlace(world, t, maxDist = 560) {
|
||||
const own = findOwnedStructure(world, t, "grass_bed", maxDist);
|
||||
return own || findNearestItemWithRole(world, t, "sleepPlace", maxDist);
|
||||
}
|
||||
|
||||
function findNearbyDanger(world, t, maxDist = 180) {
|
||||
const ant = (world?.nearbyAnts?.(t.x, t.y, maxDist, true) || []).find(a => a && !a.dead);
|
||||
if (ant) return ant;
|
||||
return findNearestItemWithRole(world, t, "danger", maxDist);
|
||||
}
|
||||
|
||||
function findNearbyMaterial(world, t, materialRole, maxDist = 520) {
|
||||
return findNearestItemWithRole(world, t, materialRole, maxDist);
|
||||
}
|
||||
|
||||
function ownedStructures(world, t, type = "") {
|
||||
return (world?.items || []).filter(it => it && !it.dead && it.isStructure && it.ownerId === t?.id && (!type || it.type === type));
|
||||
}
|
||||
|
||||
function findOwnedStructure(world, t, type = "", maxDist = Infinity) {
|
||||
let best = null;
|
||||
let bestD = maxDist;
|
||||
for (const it of ownedStructures(world, t, type)) {
|
||||
const d = distXY(t.x, t.y, it.x, it.y);
|
||||
if (d < bestD) { best = it; bestD = d; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function hasOwnedStructure(world, t, type) {
|
||||
return Boolean(findOwnedStructure(world, t, type));
|
||||
}
|
||||
|
||||
function canActNormally(t) {
|
||||
return !t.dead && t.state !== "sleep" && t.state !== "fight" && t.state !== "panic" && t.state !== "intimidate" && t.birthRitualTimer <= 0.04 && t.fightTimer <= 0.04 && t.defeatedTimer <= 0.04;
|
||||
}
|
||||
|
||||
function updateAttachedStructures(world, t) {
|
||||
for (const s of ownedStructures(world, t)) {
|
||||
if (s.type !== "plushie" || s.carriedById !== t.id || !s.onHead) continue;
|
||||
s.x = t.x + (t.facingDir ? t.facingDir() : 1) * t.radius * 0.12;
|
||||
s.y = t.y - t.radius * 1.10;
|
||||
s.amount = Math.max(1, s.hp || 1);
|
||||
if (t.needs) t.needShock = { ...(t.needShock || {}), fulfill: Math.max(0, Number(t.needShock?.fulfill || 0) - 0.08) };
|
||||
}
|
||||
}
|
||||
|
||||
function updateNeeds(t, world) {
|
||||
const prev = t.needsInitialized && t.needs ? { ...t.needs } : null;
|
||||
const pAgg = personalityValue(t, "aggression");
|
||||
const pOpen = personalityValue(t, "openness");
|
||||
const pSoc = personalityValue(t, "sociability");
|
||||
const pNeu = personalityValue(t, "neuroticism");
|
||||
const raw = createDefaultNeeds();
|
||||
raw.food = clamp((t.hunger || 0) * 0.88 + (findNearestItemWithRole(world, t, "drink", 120) ? 6 : 0), 0, 100);
|
||||
const dayP = world?.dayProgress?.() ?? 0.5;
|
||||
const night = dayP >= 0.62 || dayP < 0.18 ? 12 : 0;
|
||||
raw.sleep = clamp((100 - (t.energy || 0)) * 0.98 + night, 0, 100);
|
||||
raw.health = clamp(
|
||||
(t.zunchiDisease ? 38 : 0) + (t.sleepDisease ? 22 : 0) + (t.explosionDisease ? 30 : 0) + (t.fightDisease ? 34 : 0) +
|
||||
clamp((100 - (t.energy || 100)) * 0.22, 0, 26) + clamp((t.zunchiStain || 0) * 0.34, 0, 34) + Number(t.needShock?.health || 0),
|
||||
0,
|
||||
100
|
||||
);
|
||||
const danger = findNearbyDanger(world, t, 210);
|
||||
const recentDamage = (world?.time || 0) - (t.lastDamageAt || -999) < 8 ? clamp((t.lastDamageAmount || 0) * 2.4, 0, 52) : 0;
|
||||
const fear = clamp((t.fearTimer || 0) * 22, 0, 45);
|
||||
raw.safety = clamp((danger ? 42 : 0) + recentDamage + fear + Number(t.needShock?.safety || 0), 0, 100);
|
||||
raw.social = clamp((t.loneliness || 0) * (0.78 + Math.max(0, pSoc) * 0.16) + (t.reproductionTimer <= 4 ? 18 : 0), 0, 100);
|
||||
const hasBed = hasOwnedStructure(world, t, "grass_bed");
|
||||
const hasPlushie = hasOwnedStructure(world, t, "plushie");
|
||||
const play = findNearestItemWithRole(world, t, "playObject", 280) ? 10 : 0;
|
||||
raw.fulfill = clamp((hasBed ? 0 : 26) + (hasPlushie ? 0 : 20) + play + Math.max(0, pOpen) * 16 + Number(t.needShock?.fulfill || 0), 0, 100);
|
||||
raw.safety *= clamp(1 - Math.max(0, pAgg) * 0.12 + Math.max(0, pNeu) * 0.16, 0.78, 1.22);
|
||||
raw.health *= clamp(1 + Math.max(0, pNeu) * 0.12, 0.9, 1.16);
|
||||
raw.social *= clamp(1 + Math.max(0, pSoc) * 0.12, 0.9, 1.16);
|
||||
raw.fulfill *= clamp(1 + Math.max(0, pOpen) * 0.12, 0.9, 1.18);
|
||||
const needs = createDefaultNeeds();
|
||||
for (const key of NEED_KEYS) needs[key] = quantizeNeed(raw[key]);
|
||||
t.previousNeeds = prev;
|
||||
t.needs = needs;
|
||||
t.needsInitialized = true;
|
||||
t.stress = deriveStressFromNeeds(needs);
|
||||
handleNeedShockReaction(t, prev, needs);
|
||||
return needs;
|
||||
}
|
||||
|
||||
function handleNeedShockReaction(t, prev, needs, breaker = null) {
|
||||
if (!prev || !needs || t.birthRitualTimer > 0.04 || t.fightTimer > 0.04) return false;
|
||||
const safetyDelta = (needs.safety || 0) - (prev.safety || 0);
|
||||
const healthDelta = (needs.health || 0) - (prev.health || 0);
|
||||
const fulfillDelta = (needs.fulfill || 0) - (prev.fulfill || 0);
|
||||
if (safetyDelta < 40 && healthDelta < 45 && fulfillDelta < 50) return false;
|
||||
const attacker = breaker || t.needShockBreaker || null;
|
||||
if (attacker && personalityValue(t, "aggression") > 0.6) {
|
||||
t.intimidateTimer = Math.max(t.intimidateTimer || 0, 2.6);
|
||||
t.intimidateTargetId = attacker.id || null;
|
||||
t.setActionState?.("intimidate", { target: attacker, reason: "\u5927\u4e8b\u306a\u3082\u306e\u3092\u58ca\u3055\u308c\u3066\u5a01\u5687\u3057\u3066\u3044\u308b", wake: true });
|
||||
} else {
|
||||
t.panicTimer = Math.max(t.panicTimer || 0, 3.0);
|
||||
t.enterPanic?.({ target: t.panicDestination?.(attacker, true) || null, reason: "\u304a\u3069\u308d\u3044\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.8, wake: true, cause: "need_shock" });
|
||||
}
|
||||
t.needShockBreaker = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
function addNeedShock(t, values = {}, breaker = null) {
|
||||
if (!t) return;
|
||||
const shock = { ...(t.needShock || {}) };
|
||||
for (const key of NEED_KEYS) shock[key] = clamp(Number(shock[key] || 0) + Number(values[key] || 0), 0, 100);
|
||||
t.needShock = shock;
|
||||
t.needShockBreaker = breaker || t.needShockBreaker || null;
|
||||
const prev = t.needsInitialized && t.needs ? { ...t.needs } : null;
|
||||
updateNeeds(t, t.world);
|
||||
handleNeedShockReaction(t, prev, t.needs, breaker);
|
||||
}
|
||||
|
||||
function decayNeedShock(t, dt) {
|
||||
if (!t?.needShock) return;
|
||||
for (const key of NEED_KEYS) t.needShock[key] = Math.max(0, Number(t.needShock[key] || 0) - dt * (key === "fulfill" ? 2.2 : 2.8));
|
||||
}
|
||||
|
||||
function chooseTopNeedRandom(needs) {
|
||||
const maxNeed = Math.max(...NEED_KEYS.map(key => Number(needs?.[key] || 0)));
|
||||
const tiedNeeds = NEED_KEYS.filter(key => Number(needs?.[key] || 0) === maxNeed);
|
||||
return tiedNeeds[Math.floor(Math.random() * tiedNeeds.length)] || "fulfill";
|
||||
}
|
||||
|
||||
function weightedRandom(items) {
|
||||
const total = items.reduce((sum, a) => sum + Math.max(0.01, Number(a.weight || 1)), 0);
|
||||
let pickPoint = Math.random() * total;
|
||||
for (const item of items) {
|
||||
pickPoint -= Math.max(0.01, Number(item.weight || 1));
|
||||
if (pickPoint <= 0) return item;
|
||||
}
|
||||
return items[items.length - 1] || null;
|
||||
}
|
||||
|
||||
function buildReasonText(chosenNeed, tiedNeeds, action) {
|
||||
const tied = Array.isArray(tiedNeeds) ? tiedNeeds : [chosenNeed];
|
||||
const ignored = NEED_PRIORITY.find(key => key !== chosenNeed && tied.includes(key));
|
||||
const phrase = action?.phrase || "\u5f85\u3063\u305f";
|
||||
if (ignored) return `${NEED_PHRASES[ignored]}\u3051\u3069\u3001${phrase}\u3002`;
|
||||
return `${NEED_PHRASES[chosenNeed]}\u306e\u3067\u3001${phrase}\u3002`;
|
||||
}
|
||||
|
||||
function moveTowardOrUse(t, target, state, label) {
|
||||
if (!target) return false;
|
||||
if (distXY(t.x, t.y, target.x, target.y) > Math.max(34, (target.r || 12) + t.radius * 0.75)) {
|
||||
t.setActionState?.(state, { target, reason: label });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function createStructure(world, owner, type, x, y) {
|
||||
const def = StructureRegistry[type];
|
||||
if (!def) return null;
|
||||
const structure = def.create(owner, x, y, world);
|
||||
structure.update = updateStructure;
|
||||
structure.draw = drawStructure;
|
||||
structure.damage = damageStructure;
|
||||
world.items.push(structure);
|
||||
world.itemCounts[structure.type] = (world.itemCounts[structure.type] || 0) + 1;
|
||||
world.markTerrainDirty?.(`structure:${structure.type}`);
|
||||
world.rebuildSpatial?.(true);
|
||||
return structure;
|
||||
}
|
||||
|
||||
function consumeGrassMaterial(grass) {
|
||||
if (!grass || grass.dead) return false;
|
||||
grass.amount = Math.max(0, Number(grass.amount || 0) - 34);
|
||||
grass.growth = clamp((grass.growth || 0.5) - 0.24, 0.04, 1.2);
|
||||
if (grass.amount <= 1) grass.dead = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function finishBuild(t, type, grass) {
|
||||
const world = t.world;
|
||||
const x = clamp(t.x + rand(-18, 18), CONFIG.worldPadding, world.w - CONFIG.worldPadding);
|
||||
const y = clamp(t.y + rand(10, 30), CONFIG.worldPadding, world.h - CONFIG.worldPadding);
|
||||
consumeGrassMaterial(grass);
|
||||
const s = createStructure(world, t, type, x, y);
|
||||
if (s) {
|
||||
t.buildTimer = 0;
|
||||
t.buildType = "";
|
||||
t.buildMaterialId = "";
|
||||
t.needShock = { ...(t.needShock || {}), fulfill: Math.max(0, Number(t.needShock?.fulfill || 0) - 26) };
|
||||
StructureRegistry[type].onUse?.(t, s, world);
|
||||
}
|
||||
return Boolean(s);
|
||||
}
|
||||
|
||||
function runBuildAction(t, type, label) {
|
||||
const grass = findNearbyMaterial(t.world, t, "grassMaterial", 520);
|
||||
if (!grass) return false;
|
||||
if (moveTowardOrUse(t, grass, "seek_food", label)) {
|
||||
t.buildType = type;
|
||||
t.buildMaterialId = grass.id;
|
||||
return true;
|
||||
}
|
||||
t.buildType = type;
|
||||
t.buildMaterialId = grass.id;
|
||||
t.buildTimer = Math.max(0, Number(t.buildTimer || 0)) + 1;
|
||||
t.setActionState?.("idle", { target: grass, reason: label });
|
||||
if (t.buildTimer >= (StructureRegistry[type]?.buildTime || 4)) return finishBuild(t, type, grass);
|
||||
return true;
|
||||
}
|
||||
|
||||
const TARINAI_ACTIONS = [
|
||||
{ id: "eat_food", need: "food", label: "\u98df\u3079\u7269\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u98df\u3079\u305f", weight: 50,
|
||||
condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "food", 620, it => !t.foodItemHasServingLeft || t.foodItemHasServingLeft(it))),
|
||||
run(t, world) { const item = findNearestItemWithRole(world, t, "food", 620, it => !t.foodItemHasServingLeft || t.foodItemHasServingLeft(it)); return moveTowardOrUse(t, item, "seek_food", this.label) || t.beginEating?.(item, this.label); } },
|
||||
{ id: "drink_water", need: "food", label: "\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u6c34\u3092\u98f2\u3093\u3060", weight: 22,
|
||||
condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "drink", 620)),
|
||||
run(t, world) { const item = findNearestItemWithRole(world, t, "drink", 620); return moveTowardOrUse(t, item, "seek_water", this.label) || (t.setActionState?.("seek_water", { target: item, reason: this.label }), true); } },
|
||||
{ id: "sleep_in_bed", need: "sleep", label: "\u5bdd\u5e8a\u3078\u5411\u304b\u3063\u3066\u3044\u308b", phrase: "\u5bdd\u5e8a\u3078\u623b\u3063\u305f", weight: 42,
|
||||
condition: (t, world) => Boolean(findNearestSleepPlace(world, t, 620)),
|
||||
run(t, world) { const bed = findNearestSleepPlace(world, t, 620); if (moveTowardOrUse(t, bed, "seek_bed", this.label)) return true; return t.startSleeping?.(bed, this.label); } },
|
||||
{ id: "sleep_anywhere", need: "sleep", label: "\u305d\u306e\u5834\u3067\u4f11\u3093\u3067\u3044\u308b", phrase: "\u4f11\u3093\u3060", weight: 20, condition: () => true,
|
||||
run(t) { return t.startSleeping?.(null, this.label); } },
|
||||
{ id: "use_medicine", need: "health", label: "\u4f53\u306b\u3088\u3055\u305d\u3046\u306a\u3082\u306e\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u624b\u5f53\u3066\u3057\u305f", weight: 35,
|
||||
condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "medicine", 620)),
|
||||
run(t, world) { const item = findNearestItemWithRole(world, t, "medicine", 620); return moveTowardOrUse(t, item, itemRolesFor(item).drink ? "seek_water" : "seek_food", this.label) || t.beginEating?.(item, this.label); } },
|
||||
{ id: "rest_to_recover", need: "health", label: "\u4f53\u3092\u4f11\u3081\u3066\u3044\u308b", phrase: "\u4f11\u3093\u3060", weight: 20, condition: () => true,
|
||||
run(t) { t.setActionState?.("idle", { target: null, reason: this.label }); t.energy = clamp((t.energy || 0) + 1.0, 0, 100); return true; } },
|
||||
{ id: "flee", need: "safety", label: "\u96e2\u308c\u308b\u5834\u6240\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u9003\u3052\u305f", weight: 32,
|
||||
condition: (t, world) => Boolean(findNearbyDanger(world, t, 260)),
|
||||
run(t, world) { const danger = findNearbyDanger(world, t, 260); t.setActionState?.("panic", { target: t.panicDestination?.(danger, true) || null, reason: this.label, wake: true }); t.panicTimer = Math.max(t.panicTimer || 0, 2.0); return true; } },
|
||||
{ id: "hide_at_owned_structure", need: "safety", label: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u308d\u3046\u3068\u3057\u3066\u3044\u308b", phrase: "\u81ea\u5206\u306e\u5bdd\u5e8a\u3078\u623b\u3063\u305f", weight: 28,
|
||||
condition: (t, world) => Boolean(findOwnedStructure(world, t, "grass_bed", 760)),
|
||||
run(t, world) { const bed = findOwnedStructure(world, t, "grass_bed", 760); if (moveTowardOrUse(t, bed, "seek_bed", this.label)) return true; StructureRegistry.grass_bed.onUse(t, bed, world); return t.startSleeping?.(bed, this.label); } },
|
||||
{ id: "approach_friend", need: "social", label: "\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b", phrase: "\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u305f", weight: 38,
|
||||
condition: (t, world) => Boolean(world.nearestOther?.(t, 560)),
|
||||
run(t, world) { const other = world.nearestOther?.(t, 560); t.setActionState?.(t.parentToFollow?.() === other ? "follow_parent" : "seek_friend", { target: other, reason: this.label }); return true; } },
|
||||
{ id: "intimidate_enemy", need: "social", label: "\u6c17\u306b\u306a\u308b\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b", phrase: "\u5a01\u5687\u3057\u305f", weight: 8,
|
||||
condition: (t, world) => personalityValue(t, "aggression") > 0.45 && Boolean(world.nearestOther?.(t, 220)),
|
||||
run(t, world) { const other = world.nearestOther?.(t, 220); t.intimidateTimer = Math.max(t.intimidateTimer || 0, 2.0); t.intimidateTargetId = other?.id || null; t.setActionState?.("intimidate", { target: other, reason: this.label }); return true; } },
|
||||
{ id: "play", need: "fulfill", label: "\u904a\u3093\u3067\u3044\u308b", phrase: "\u904a\u3093\u3060", weight: 26,
|
||||
condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "playObject", 460)),
|
||||
run(t, world) { const ball = findNearestItemWithRole(world, t, "playObject", 460); t.setActionState?.("play_ball", { target: ball, reason: this.label }); t.goodMode = "smile"; return true; } },
|
||||
{ id: "build_grass_bed", need: "fulfill", label: "\u8349\u306e\u7c21\u6613\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u3066\u3044\u308b", phrase: "\u81ea\u5206\u306e\u5bdd\u5e8a\u3092\u4f5c\u3063\u305f", weight: 24,
|
||||
condition: (t, world) => canActNormally(t) && !hasOwnedStructure(world, t, "grass_bed") && Boolean(findNearbyMaterial(world, t, "grassMaterial", 520)),
|
||||
run(t) { return runBuildAction(t, "grass_bed", this.label); } },
|
||||
{ id: "build_plushie", need: "fulfill", label: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u3066\u3044\u308b", phrase: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u305f", weight: 18,
|
||||
condition: (t, world) => canActNormally(t) && !hasOwnedStructure(world, t, "plushie") && Boolean(findNearbyMaterial(world, t, "grassMaterial", 520)),
|
||||
run(t) { return runBuildAction(t, "plushie", this.label); } },
|
||||
{ id: "return_owned_structure", need: "fulfill", label: "\u81ea\u5206\u306e\u5834\u6240\u306b\u623b\u3063\u3066\u3044\u308b", phrase: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u3063\u305f", weight: 18,
|
||||
condition: (t, world) => Boolean(findOwnedStructure(world, t, "grass_bed", 760)),
|
||||
run(t, world) { const bed = findOwnedStructure(world, t, "grass_bed", 760); if (moveTowardOrUse(t, bed, "seek_bed", this.label)) return true; StructureRegistry.grass_bed.onUse(t, bed, world); return true; } },
|
||||
{ id: "use_plushie", need: "fulfill", label: "\u306c\u3044\u3050\u308b\u307f\u3092\u5927\u4e8b\u306b\u3057\u3066\u3044\u308b", phrase: "\u306c\u3044\u3050\u308b\u307f\u3067\u843d\u3061\u7740\u3044\u305f", weight: 22,
|
||||
condition: (t, world) => Boolean(findOwnedStructure(world, t, "plushie", Infinity)),
|
||||
run(t, world) { const plushie = findOwnedStructure(world, t, "plushie", Infinity); StructureRegistry.plushie.onUse(t, plushie, world); t.setActionState?.("idle", { target: plushie, reason: this.label }); return true; } },
|
||||
{ id: "wander_lightly", need: "fulfill", label: "\u8efd\u304f\u6563\u6b69\u3057\u3066\u3044\u308b", phrase: "\u6563\u6b69\u3057\u305f", weight: 12, condition: () => true,
|
||||
run(t) { t.wanderAngle += rand(-1.7, 1.7); t.setActionState?.("idle", { target: null, reason: this.label }); return true; } },
|
||||
];
|
||||
|
||||
function chooseActionForNeed(need, t, world) {
|
||||
const candidates = TARINAI_ACTIONS.filter(a => a.need === need && (!a.condition || a.condition(t, world)));
|
||||
return weightedRandom(candidates) || TARINAI_ACTIONS.find(a => a.id === "wander_lightly");
|
||||
}
|
||||
|
||||
function runNeedDecision(t, world, dt = 0.1) {
|
||||
updateAttachedStructures(world, t);
|
||||
decayNeedShock(t, dt);
|
||||
const needs = updateNeeds(t, world);
|
||||
const maxNeed = Math.max(...NEED_KEYS.map(key => needs[key]));
|
||||
const tiedNeeds = NEED_KEYS.filter(key => needs[key] === maxNeed);
|
||||
const chosenNeed = chooseTopNeedRandom(needs);
|
||||
const action = chooseActionForNeed(chosenNeed, t, world);
|
||||
if (!action || !action.run(t, world, dt)) {
|
||||
t.setActionState?.("idle", { target: null, reason: "\u5f85\u3063\u3066\u3044\u308b" });
|
||||
}
|
||||
const reasonText = buildReasonText(chosenNeed, tiedNeeds, action);
|
||||
t.intent = {
|
||||
need: chosenNeed,
|
||||
tiedNeeds: tiedNeeds.slice(),
|
||||
actionId: action?.id || "idle",
|
||||
actionLabel: action?.label || "\u5f85\u3063\u3066\u3044\u308b",
|
||||
reasonText,
|
||||
};
|
||||
t.need = chosenNeed;
|
||||
t.thought = reasonText;
|
||||
return t.intent;
|
||||
}
|
||||
|
||||
function updateStructure(dt, world) {
|
||||
if (this.dead) return;
|
||||
this.age = (this.age || 0) + dt;
|
||||
if (this.type === "plushie" && this.carriedById) {
|
||||
const owner = (world?.tarinai || []).find(t => t && !t.dead && t.id === this.carriedById);
|
||||
if (owner) {
|
||||
this.x = owner.x + (owner.facingDir ? owner.facingDir() : 1) * owner.radius * 0.12;
|
||||
this.y = owner.y - owner.radius * 1.10;
|
||||
} else {
|
||||
this.carriedById = "";
|
||||
this.onHead = false;
|
||||
}
|
||||
}
|
||||
this.amount = Math.max(0, this.hp || 0);
|
||||
}
|
||||
|
||||
function damageStructure(amount = 1, breaker = null, world = null) {
|
||||
this.hp = Math.max(0, Number(this.hp || 0) - Math.max(0, Number(amount) || 0));
|
||||
if (this.hp > 0) return false;
|
||||
const w = world || global.world;
|
||||
const def = StructureRegistry[this.type];
|
||||
def?.onDestroyed?.(this, w, breaker);
|
||||
this.dead = true;
|
||||
this.amount = 0;
|
||||
w?.markTerrainDirty?.(`structure-destroyed:${this.type}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function drawStructure(ctx, time, lighting) {
|
||||
const r = this.r || (this.type === "plushie" ? 14 : 28);
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.y);
|
||||
if (this.type === "grass_bed") {
|
||||
ctx.fillStyle = "rgba(175, 196, 102, 0.88)";
|
||||
ctx.strokeStyle = "rgba(90, 118, 55, 0.72)";
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 3, r * 1.55, r * 0.74, -0.08, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = "rgba(71, 112, 48, 0.55)";
|
||||
ctx.lineWidth = 1.6;
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const x = randSeed((this.seed || 1) + i, -r * 1.1, r * 1.1);
|
||||
const y = randSeed((this.seed || 1) + i + 20, -r * 0.32, r * 0.46);
|
||||
const len = randSeed((this.seed || 1) + i + 40, r * 0.30, r * 0.68);
|
||||
const a = randSeed((this.seed || 1) + i + 60, -0.7, 0.7);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - Math.cos(a) * len * 0.5, y - Math.sin(a) * len * 0.5);
|
||||
ctx.lineTo(x + Math.cos(a) * len * 0.5, y + Math.sin(a) * len * 0.5);
|
||||
ctx.stroke();
|
||||
}
|
||||
} else if (this.type === "plushie") {
|
||||
const img = typeof getRenderableImage === "function" ? getRenderableImage("smile", "smile") : null;
|
||||
ctx.rotate(Math.sin(time * 2.2 + (this.seed || 0)) * 0.08);
|
||||
if (img) ctx.drawImage(img, -r * 0.8, -r * 0.8, r * 1.6, r * 1.45);
|
||||
else {
|
||||
ctx.fillStyle = "#f4ead6";
|
||||
ctx.strokeStyle = "rgba(92,71,49,0.48)";
|
||||
ctx.lineWidth = 1.4;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, r * 0.68, r * 0.58, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
const StructureRegistry = {
|
||||
grass_bed: {
|
||||
type: "grass_bed",
|
||||
label: "\u8349\u306e\u7c21\u6613\u30d9\u30c3\u30c9",
|
||||
buildNeed: "fulfill",
|
||||
buildTime: 4,
|
||||
maxHp: 100,
|
||||
materials: { grassMaterial: 1 },
|
||||
roles: { sleepPlace: true, ownedStructure: true },
|
||||
needEffects: { food: 0, sleep: 60, health: 5, safety: 25, social: 0, fulfill: 40 },
|
||||
create(owner, x, y) {
|
||||
return { id: createId("structure"), type: "grass_bed", isStructure: true, ownerId: owner.id, hp: 100, attachment: 20, usedCount: 0, x, y, r: 28, amount: 100, seed: Math.random() * 1000, roles: { ...this.roles }, needEffects: { ...this.needEffects } };
|
||||
},
|
||||
onUse(user, structure) {
|
||||
const owner = user?.id === structure?.ownerId;
|
||||
structure.usedCount = (structure.usedCount || 0) + 1;
|
||||
structure.attachment = clamp((structure.attachment || 0) + (owner ? 2 : 0.4), 0, 100);
|
||||
user.needShock = { ...(user.needShock || {}) };
|
||||
user.needShock.sleep = Math.max(0, Number(user.needShock.sleep || 0) - (owner ? 18 : 8));
|
||||
user.needShock.safety = Math.max(0, Number(user.needShock.safety || 0) - (owner ? 12 : 4));
|
||||
user.needShock.fulfill = Math.max(0, Number(user.needShock.fulfill || 0) - (owner ? 18 : 6));
|
||||
},
|
||||
onDestroyed(structure, world, breaker) {
|
||||
const owner = (world?.tarinai || []).find(t => t && !t.dead && t.id === structure.ownerId);
|
||||
if (owner) addNeedShock(owner, { safety: 45, fulfill: 52 }, breaker);
|
||||
},
|
||||
},
|
||||
plushie: {
|
||||
type: "plushie",
|
||||
label: "\u306c\u3044\u3050\u308b\u307f",
|
||||
buildNeed: "fulfill",
|
||||
buildTime: 4,
|
||||
maxHp: 1,
|
||||
materials: { grassMaterial: 1 },
|
||||
roles: { carriedObject: true, ownedStructure: true, fulfillObject: true },
|
||||
needEffects: { food: 0, sleep: 0, health: 0, safety: 5, social: 0, fulfill: 50 },
|
||||
create(owner, x, y) {
|
||||
return { id: createId("structure"), type: "plushie", isStructure: true, ownerId: owner.id, hp: 1, attachment: 30, usedCount: 0, x, y, r: 14, amount: 1, carriedById: owner.id, onHead: true, seed: Math.random() * 1000, roles: { ...this.roles }, needEffects: { ...this.needEffects } };
|
||||
},
|
||||
onUse(user, structure) {
|
||||
if (!user || !structure) return;
|
||||
structure.attachment = clamp((structure.attachment || 0) + 1.5, 0, 100);
|
||||
user.needShock = { ...(user.needShock || {}), fulfill: Math.max(0, Number(user.needShock?.fulfill || 0) - 20), safety: Math.max(0, Number(user.needShock?.safety || 0) - 3) };
|
||||
},
|
||||
onDestroyed(structure, world, breaker) {
|
||||
const owner = (world?.tarinai || []).find(t => t && !t.dead && t.id === structure.ownerId);
|
||||
if (owner) addNeedShock(owner, { safety: 58, fulfill: 70 }, breaker);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
global.TarinaiNeedSystem = {
|
||||
NEED_KEYS,
|
||||
NEED_LABELS,
|
||||
NEED_PHRASES,
|
||||
TARINAI_ACTIONS,
|
||||
StructureRegistry,
|
||||
createDefaultNeeds,
|
||||
quantizeNeed,
|
||||
updateNeeds,
|
||||
chooseTopNeedRandom,
|
||||
chooseActionForNeed,
|
||||
buildReasonText,
|
||||
getNeedDisplayValue,
|
||||
findNearestItemWithRole,
|
||||
findNearestSleepPlace,
|
||||
findNearbyDanger,
|
||||
findNearbyMaterial,
|
||||
findOwnedStructure,
|
||||
ownedStructures,
|
||||
itemRolesFor,
|
||||
ensureItemNeedEffects,
|
||||
deriveStressFromNeeds,
|
||||
addNeedShock,
|
||||
runNeedDecision,
|
||||
createStructure,
|
||||
damageStructure,
|
||||
};
|
||||
|
||||
global.createDefaultNeeds = createDefaultNeeds;
|
||||
global.quantizeNeed = quantizeNeed;
|
||||
global.updateNeeds = updateNeeds;
|
||||
global.chooseTopNeedRandom = chooseTopNeedRandom;
|
||||
global.chooseActionForNeed = chooseActionForNeed;
|
||||
global.buildReasonText = buildReasonText;
|
||||
global.getNeedDisplayValue = getNeedDisplayValue;
|
||||
global.findNearestItemWithRole = findNearestItemWithRole;
|
||||
global.findNearestSleepPlace = findNearestSleepPlace;
|
||||
global.findNearbyDanger = findNearbyDanger;
|
||||
global.findNearbyMaterial = findNearbyMaterial;
|
||||
global.StructureRegistry = StructureRegistry;
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
30
js/render.js
|
|
@ -249,8 +249,8 @@ const terrainCache = {
|
|||
|
||||
function isCachedTerrainItem(it) {
|
||||
if (!it || it.dead) return false;
|
||||
if (it.machineProduced) return false;
|
||||
if (it.type === "grass" || it.type === "trace" || it.type === "splat") return true;
|
||||
if (it.type === "zunchi") return Math.hypot(it.vx || 0, it.vy || 0) < 0.08;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -740,15 +740,6 @@ function render() {
|
|||
for (const ef of world.effects) {
|
||||
if (ef.type === "bleed" && isEntityVisibleInRect(ef, visibleRect)) ef.draw(ctx);
|
||||
}
|
||||
for (const it of world.items) {
|
||||
if (isCachedTerrainItem(it)) continue;
|
||||
if (isPinType(it.type) && it.pinState === "lodged") continue;
|
||||
if (it.type !== "zunchi" && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const it of world.items) {
|
||||
if (it.type === "zunchi" && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.lineWidth = 1.15;
|
||||
ctx.setLineDash([4, 4]);
|
||||
|
|
@ -765,13 +756,20 @@ function render() {
|
|||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
|
||||
const sorted = world.sortedDrawList ? world.sortedDrawList() : world.tarinai;
|
||||
for (const t of sorted) {
|
||||
if (isEntityVisibleInRect(t, visibleRect)) t.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const it of world.items) {
|
||||
if (isPinType(it.type) && it.pinState === "lodged" && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
const fieldEntries = [];
|
||||
const pushFieldEntry = (entity, kind, order = 0) => {
|
||||
if (!entity || entity.dead || !isEntityVisibleInRect(entity, visibleRect)) return;
|
||||
fieldEntries.push({ entity, kind, order, y: Number.isFinite(entity.y) ? entity.y : 0 });
|
||||
};
|
||||
for (const it of world.items || []) {
|
||||
if (isCachedTerrainItem(it)) continue;
|
||||
const layerOffset = it.type === "plushie" && it.onHead ? 999 : (it.type === "bed" ? -1.0 : ((typeof isMachineType === "function" && isMachineType(it.type)) ? (it.r || 42) * 0.58 : (isPinType(it.type) && it.pinState === "lodged" ? 0.15 : 0)));
|
||||
pushFieldEntry(it, "item", layerOffset);
|
||||
}
|
||||
for (const t of world.tarinai || []) pushFieldEntry(t, "actor", 0.1);
|
||||
for (const a of world.ants || []) pushFieldEntry(a, "actor", 0.05);
|
||||
fieldEntries.sort((a, b) => ((a.y + a.order) - (b.y + b.order)));
|
||||
for (const entry of fieldEntries) entry.entity.draw(ctx, world.time, lighting);
|
||||
for (const ef of world.effects) {
|
||||
if (ef.type !== "bleed" && isEntityVisibleInRect(ef, visibleRect)) ef.draw(ctx);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,81 @@
|
|||
(function (global) {
|
||||
const STORAGE_PREFIX = "tarinai_save_slot_v1_";
|
||||
const EXPORT_PREFIX = "TARINAI_SAVE_V1:";
|
||||
const EXPORT_PREFIX_COMPRESSED = "TARINAI-SAVE-V11:";
|
||||
// Alpha note: save/import compatibility is not guaranteed yet.
|
||||
// Hash imports intentionally accept only the current V11 format during alpha.
|
||||
// V11 stores compact semantic early-game saves when possible, falls back to the V10 binary packet for complex states, and encodes bytes with printable ASCII excluding \, *, ' and _.
|
||||
const SLOT_COUNT = 9;
|
||||
const Snapshot = global.TarinaiSnapshot;
|
||||
if (!Snapshot) throw new Error("TarinaiSnapshot is not available for save_system.js");
|
||||
const createSnapshot = (...args) => Snapshot.createSnapshot(...args);
|
||||
const restoreSnapshot = (...args) => Snapshot.restoreSnapshot(...args);
|
||||
const htmlEscape = global.TarinaiUIHelpers?.htmlEscape || ((v) => String(v ?? "").replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])));
|
||||
|
||||
|
||||
const BASE94_ALPHABET = (() => {
|
||||
const excluded = new Set(["\\", "*", "'", "_"]);
|
||||
let out = "";
|
||||
for (let i = 33; i <= 126; i++) {
|
||||
const ch = String.fromCharCode(i);
|
||||
if (!excluded.has(ch)) out += ch;
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
const BASE94_RADIX = BASE94_ALPHABET.length;
|
||||
const BASE94_DECODE = (() => {
|
||||
const map = Object.create(null);
|
||||
for (let i = 0; i < BASE94_ALPHABET.length; i++) map[BASE94_ALPHABET[i]] = i;
|
||||
return map;
|
||||
})();
|
||||
|
||||
function bytesToBase94(bytes) {
|
||||
if (!bytes || !bytes.length) return "!";
|
||||
let zeros = 0;
|
||||
while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
|
||||
const digits = [0];
|
||||
for (let i = zeros; i < bytes.length; i++) {
|
||||
let carry = bytes[i];
|
||||
for (let j = 0; j < digits.length; j++) {
|
||||
const x = digits[j] * 256 + carry;
|
||||
digits[j] = x % BASE94_RADIX;
|
||||
carry = Math.floor(x / BASE94_RADIX);
|
||||
}
|
||||
while (carry > 0) {
|
||||
digits.push(carry % BASE94_RADIX);
|
||||
carry = Math.floor(carry / BASE94_RADIX);
|
||||
}
|
||||
}
|
||||
let out = BASE94_ALPHABET[0].repeat(zeros);
|
||||
for (let i = digits.length - 1; i >= 0; i--) out += BASE94_ALPHABET[digits[i]];
|
||||
return out || BASE94_ALPHABET[0];
|
||||
}
|
||||
|
||||
function base94ToBytes(text) {
|
||||
const str = String(text || "").replace(/\s/g, "");
|
||||
if (!str) return new Uint8Array();
|
||||
let zeros = 0;
|
||||
while (zeros < str.length && str[zeros] === BASE94_ALPHABET[0]) zeros++;
|
||||
const bytes = [0];
|
||||
for (let i = zeros; i < str.length; i++) {
|
||||
const digit = BASE94_DECODE[str[i]];
|
||||
if (digit === undefined) throw new Error("invalid base94 character");
|
||||
let carry = digit;
|
||||
for (let j = 0; j < bytes.length; j++) {
|
||||
const x = bytes[j] * BASE94_RADIX + carry;
|
||||
bytes[j] = x & 255;
|
||||
carry = x >> 8;
|
||||
}
|
||||
while (carry > 0) {
|
||||
bytes.push(carry & 255);
|
||||
carry >>= 8;
|
||||
}
|
||||
}
|
||||
const out = new Uint8Array(zeros + bytes.length);
|
||||
for (let i = 0; i < zeros; i++) out[i] = 0;
|
||||
for (let i = 0; i < bytes.length; i++) out[out.length - 1 - i] = bytes[i];
|
||||
return out;
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
let binary = "";
|
||||
|
|
@ -16,8 +86,18 @@
|
|||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
||||
}
|
||||
|
||||
function stripExportPrefix(text, prefixes) {
|
||||
let value = String(text || "").trim();
|
||||
const hashless = value.replace(/^#+/, "");
|
||||
if (prefixes.some(prefix => hashless.startsWith(prefix))) value = hashless;
|
||||
for (const prefix of prefixes) {
|
||||
if (value.startsWith(prefix)) return value.slice(prefix.length);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function base64ToBytes(text) {
|
||||
let b64 = String(text || "").trim().replace(/^#+/, "").replace(EXPORT_PREFIX, "").replace(/-/g, "+").replace(/_/g, "/");
|
||||
let b64 = stripExportPrefix(text, [EXPORT_PREFIX]).replace(/-/g, "+").replace(/_/g, "/");
|
||||
while (b64.length % 4) b64 += "=";
|
||||
const binary = atob(b64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
|
|
@ -25,25 +105,88 @@
|
|||
return bytes;
|
||||
}
|
||||
|
||||
function encodeSnapshot(snapshot) {
|
||||
async function gzipBytes(bytes) {
|
||||
if (typeof CompressionStream !== "function") return null;
|
||||
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
|
||||
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||
}
|
||||
|
||||
async function gunzipBytes(bytes) {
|
||||
if (typeof DecompressionStream !== "function") throw new Error("compressed import is not supported in this browser");
|
||||
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
|
||||
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||
}
|
||||
|
||||
function encodeSnapshotLegacy(snapshot) {
|
||||
const json = JSON.stringify(snapshot);
|
||||
const bytes = new TextEncoder().encode(json);
|
||||
return EXPORT_PREFIX + bytesToBase64(bytes);
|
||||
}
|
||||
|
||||
function decodeSnapshot(text) {
|
||||
async function encodeSnapshot(snapshot) {
|
||||
if (snapshot?.__tarinaiCompactBytes && snapshot.bytes) {
|
||||
const raw = snapshot.bytes instanceof Uint8Array ? snapshot.bytes : new Uint8Array(snapshot.bytes);
|
||||
const compressed = await gzipBytes(raw);
|
||||
if (compressed && compressed.length < raw.length) return EXPORT_PREFIX_COMPRESSED + "z" + bytesToBase94(compressed);
|
||||
return EXPORT_PREFIX_COMPRESSED + "r" + bytesToBase94(raw);
|
||||
}
|
||||
const json = JSON.stringify(snapshot);
|
||||
const bytes = new TextEncoder().encode(json);
|
||||
const compressed = await gzipBytes(bytes);
|
||||
if (compressed && compressed.length < bytes.length) return EXPORT_PREFIX_COMPRESSED + "z" + bytesToBase94(compressed);
|
||||
return EXPORT_PREFIX_COMPRESSED + "r" + bytesToBase94(bytes);
|
||||
}
|
||||
|
||||
async function decodeSnapshot(text) {
|
||||
const raw = String(text || "").trim();
|
||||
if (!raw) throw new Error("empty import text");
|
||||
if (raw.startsWith("{") || raw.startsWith("[")) return JSON.parse(raw);
|
||||
const bytes = base64ToBytes(raw);
|
||||
const json = new TextDecoder().decode(bytes);
|
||||
return JSON.parse(json);
|
||||
const normalized = raw.replace(/^#+/, "");
|
||||
if (!normalized) throw new Error("empty import text");
|
||||
if (!normalized.startsWith(EXPORT_PREFIX_COMPRESSED)) throw new Error("unsupported save hash version");
|
||||
const body = stripExportPrefix(normalized, [EXPORT_PREFIX_COMPRESSED]);
|
||||
const mode = body[0] || "r";
|
||||
const payload = body.slice(1);
|
||||
const bytes = base94ToBytes(payload);
|
||||
const packet = mode === "z" ? await gunzipBytes(bytes) : bytes;
|
||||
if (!Snapshot.decodeV11SnapshotBytes) throw new Error("V11 decoder is not available");
|
||||
return Snapshot.decodeV11SnapshotBytes(packet);
|
||||
}
|
||||
|
||||
function slotKey(slot) { return `${STORAGE_PREFIX}${slot}`; }
|
||||
|
||||
function saveSlot(slot) {
|
||||
function makeThumbnail() {
|
||||
const source = global.canvas || document.getElementById("gameCanvas");
|
||||
if (!source || !source.width || !source.height) return "";
|
||||
try {
|
||||
const tw = 168;
|
||||
const th = 96;
|
||||
const c = document.createElement("canvas");
|
||||
c.width = tw;
|
||||
c.height = th;
|
||||
const ctx = c.getContext("2d", { alpha: false });
|
||||
if (!ctx) return "";
|
||||
ctx.fillStyle = "#f6efe3";
|
||||
ctx.fillRect(0, 0, tw, th);
|
||||
const scale = Math.min(tw / source.width, th / source.height);
|
||||
const w = Math.max(1, Math.round(source.width * scale));
|
||||
const h = Math.max(1, Math.round(source.height * scale));
|
||||
const x = Math.floor((tw - w) / 2);
|
||||
const y = Math.floor((th - h) / 2);
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(source, x, y, w, h);
|
||||
return c.toDataURL("image/jpeg", 0.58);
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function makeSaveSnapshot() {
|
||||
const snapshot = createSnapshot(global.world);
|
||||
snapshot.thumbnail = makeThumbnail();
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function saveSlot(slot) {
|
||||
const snapshot = makeSaveSnapshot();
|
||||
localStorage.setItem(slotKey(slot), JSON.stringify(snapshot));
|
||||
return snapshot;
|
||||
}
|
||||
|
|
@ -62,14 +205,11 @@
|
|||
try { return JSON.parse(raw); } catch (_) { return null; }
|
||||
}
|
||||
|
||||
const htmlEscape = global.TarinaiUIHelpers.htmlEscape;
|
||||
|
||||
|
||||
function formatSlotSummary(snapshot) {
|
||||
if (!snapshot) return "空き";
|
||||
const d = snapshot.summary || {};
|
||||
const date = snapshot.createdAt ? new Date(snapshot.createdAt).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "日時不明";
|
||||
return `${date} / ${d.fieldType || "庭"} / ${d.day || 1}日目 ${d.time || ""} / ${d.population || 0}匹`;
|
||||
return `${date}\n${d.fieldType || "庭"} / ${d.day || 1}日目 ${d.time || ""}\n${d.population || 0}匹 / 道具${d.items || 0}`;
|
||||
}
|
||||
|
||||
function ensureDialog() {
|
||||
|
|
@ -83,14 +223,21 @@
|
|||
dialog.innerHTML = `
|
||||
<div class="field-dialog-panel save-dialog-panel">
|
||||
<h2 id="saveDialogTitle">セーブ / ロード</h2>
|
||||
<p class="hint">9つのスロットに現在のコロニーを保存できます。ハッシュテキストは別端末や別ブラウザへの移動に使えます。</p>
|
||||
<div id="saveSlotList" class="save-slot-list"></div>
|
||||
<p class="hint">9つのスロットに現在のコロニーを保存できます。ハッシュテキストは圧縮して短く書き出します。</p>
|
||||
<div id="saveSlotList" class="save-slot-list save-slot-grid"></div>
|
||||
<div class="save-export-panel">
|
||||
<div class="save-export-actions">
|
||||
<button id="saveExportBtn" class="btn" type="button">現在の状態を書き出し</button>
|
||||
<button id="saveImportBtn" class="btn primary" type="button">ハッシュテキストを読み込み</button>
|
||||
</div>
|
||||
<textarea id="saveHashText" class="save-hash-text" spellcheck="false" placeholder="ここにハッシュテキストが表示されます。読み込む場合はここに貼り付けてください。"></textarea>
|
||||
<textarea id="saveHashText" class="save-hash-text" spellcheck="false" placeholder="ここに短縮ハッシュテキストが表示されます。読み込む場合はここに貼り付けてください。"></textarea>
|
||||
</div>
|
||||
<div id="saveConfirmBox" class="save-confirm-box hidden" role="alertdialog" aria-modal="false">
|
||||
<div class="save-confirm-message"></div>
|
||||
<div class="save-confirm-actions">
|
||||
<button class="btn" data-save-confirm="cancel" type="button">やめる</button>
|
||||
<button class="btn danger" data-save-confirm="ok" type="button">実行</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-dialog-actions">
|
||||
<button id="saveCloseBtn" class="btn" type="button">閉じる</button>
|
||||
|
|
@ -100,6 +247,35 @@
|
|||
return dialog;
|
||||
}
|
||||
|
||||
function confirmInGame(message, okLabel = "実行") {
|
||||
const dialog = ensureDialog();
|
||||
const box = dialog.querySelector("#saveConfirmBox");
|
||||
if (!box) return Promise.resolve(false);
|
||||
box.querySelector(".save-confirm-message").textContent = message;
|
||||
const ok = box.querySelector('[data-save-confirm="ok"]');
|
||||
if (ok) ok.textContent = okLabel;
|
||||
box.classList.remove("hidden");
|
||||
return new Promise(resolve => {
|
||||
const finish = (value) => {
|
||||
box.classList.add("hidden");
|
||||
box.removeEventListener("click", onClick);
|
||||
resolve(value);
|
||||
};
|
||||
const onClick = (e) => {
|
||||
const btn = e.target.closest("[data-save-confirm]");
|
||||
if (!btn) return;
|
||||
finish(btn.dataset.saveConfirm === "ok");
|
||||
};
|
||||
box.addEventListener("click", onClick);
|
||||
});
|
||||
}
|
||||
|
||||
function slotThumbnailHtml(snapshot) {
|
||||
const src = snapshot?.thumbnail || "";
|
||||
if (src) return `<img src="${htmlEscape(src)}" alt="" loading="lazy">`;
|
||||
return `<div class="save-slot-thumb-empty">No Image</div>`;
|
||||
}
|
||||
|
||||
function renderSlotList() {
|
||||
const dialog = ensureDialog();
|
||||
const list = dialog.querySelector("#saveSlotList");
|
||||
|
|
@ -108,10 +284,11 @@
|
|||
for (let i = 1; i <= SLOT_COUNT; i++) {
|
||||
const snapshot = readSlot(i);
|
||||
const row = document.createElement("div");
|
||||
row.className = "save-slot-row";
|
||||
row.className = `save-slot-card ${snapshot ? "filled" : "empty"}`;
|
||||
row.innerHTML = `
|
||||
<div class="save-slot-thumb">${slotThumbnailHtml(snapshot)}</div>
|
||||
<div class="save-slot-title">スロット ${i}</div>
|
||||
<div class="save-slot-meta">${htmlEscape(formatSlotSummary(snapshot))}</div>
|
||||
<div class="save-slot-meta">${htmlEscape(formatSlotSummary(snapshot)).replace(/\n/g, "<br>")}</div>
|
||||
<div class="save-slot-actions">
|
||||
<button class="btn mini" data-save-slot="${i}" type="button">保存</button>
|
||||
<button class="btn mini" data-load-slot="${i}" type="button" ${snapshot ? "" : "disabled"}>読込</button>
|
||||
|
|
@ -140,8 +317,9 @@
|
|||
openDialog();
|
||||
});
|
||||
const dialog = ensureDialog();
|
||||
dialog.addEventListener("click", (e) => {
|
||||
dialog.addEventListener("click", async (e) => {
|
||||
if (e.target === dialog) { closeDialog(); return; }
|
||||
if (e.target.closest("#saveConfirmBox")) return;
|
||||
const saveBtn = e.target.closest("[data-save-slot]");
|
||||
const loadBtn = e.target.closest("[data-load-slot]");
|
||||
const deleteBtn = e.target.closest("[data-delete-slot]");
|
||||
|
|
@ -160,7 +338,7 @@
|
|||
}
|
||||
if (loadBtn) {
|
||||
const slot = Number(loadBtn.dataset.loadSlot);
|
||||
if (!global.confirm?.(`スロット${slot}を読み込みます。現在の状態は上書きされます。`)) return;
|
||||
if (!await confirmInGame(`スロット${slot}を読み込みます。現在の状態は上書きされます。`, "読み込む")) return;
|
||||
try {
|
||||
loadSlot(slot);
|
||||
closeDialog();
|
||||
|
|
@ -174,7 +352,7 @@
|
|||
}
|
||||
if (deleteBtn) {
|
||||
const slot = Number(deleteBtn.dataset.deleteSlot);
|
||||
if (!global.confirm?.(`スロット${slot}を削除しますか?`)) return;
|
||||
if (!await confirmInGame(`スロット${slot}を削除しますか?`, "削除")) return;
|
||||
deleteSlot(slot);
|
||||
renderSlotList();
|
||||
global.audio?.delete?.();
|
||||
|
|
@ -184,21 +362,37 @@
|
|||
if (e.target.closest("#saveCloseBtn")) closeDialog();
|
||||
if (e.target.closest("#saveExportBtn")) {
|
||||
const area = dialog.querySelector("#saveHashText");
|
||||
if (area) {
|
||||
area.value = encodeSnapshot(createSnapshot(global.world));
|
||||
area.focus();
|
||||
area.select();
|
||||
const exportBtn = dialog.querySelector("#saveExportBtn");
|
||||
try {
|
||||
if (exportBtn) exportBtn.disabled = true;
|
||||
if (area) {
|
||||
area.value = "書き出し中…";
|
||||
const fullSnapshot = createSnapshot(global.world);
|
||||
const exportSnapshot = Snapshot.createCompactSnapshot ? Snapshot.createCompactSnapshot(global.world) : fullSnapshot;
|
||||
const text = await encodeSnapshot(exportSnapshot);
|
||||
area.value = text;
|
||||
area.focus();
|
||||
area.select();
|
||||
const legacy = encodeSnapshotLegacy(fullSnapshot);
|
||||
const rate = legacy.length ? Math.round((1 - text.length / legacy.length) * 100) : 0;
|
||||
global.showToast?.(rate > 0 ? `軽量ハッシュを書き出しました(約${rate}%短縮)。` : "現在の状態を書き出しました。");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
global.showToast?.("書き出しに失敗しました。");
|
||||
} finally {
|
||||
if (exportBtn) exportBtn.disabled = false;
|
||||
}
|
||||
global.showToast?.("現在の状態をハッシュテキストに書き出しました。");
|
||||
}
|
||||
if (e.target.closest("#saveImportBtn")) {
|
||||
const area = dialog.querySelector("#saveHashText");
|
||||
const text = area?.value || "";
|
||||
if (!text.trim()) { global.showToast?.("読み込むハッシュテキストを貼り付けてください。"); return; }
|
||||
if (!global.confirm?.("ハッシュテキストを読み込みます。現在の状態は上書きされます。")) return;
|
||||
if (!await confirmInGame("ハッシュテキストを読み込みます。現在の状態は上書きされます。", "読み込む")) return;
|
||||
try {
|
||||
restoreSnapshot(decodeSnapshot(text), global.world);
|
||||
restoreSnapshot(await decodeSnapshot(text), global.world);
|
||||
closeDialog();
|
||||
global.audio?.notify?.();
|
||||
global.showToast?.("ハッシュテキストから読み込みました。");
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@
|
|||
sfx_log_push: 0.10,
|
||||
},
|
||||
samplePaths: {
|
||||
voice_hau: "assets/sounds/voice_001_hau.wav",
|
||||
voice_flee: "assets/sounds/voice_002_flee.wav",
|
||||
voice_poop: "assets/sounds/voice_003_po.wav",
|
||||
sfx_eat: "assets/sounds/voice_004_eat.wav",
|
||||
voice_stress: "assets/sounds/voice_005_stress.wav",
|
||||
sfx_sleep: "assets/sounds/voice_006_sleep.wav",
|
||||
voice_hau: "assets/sounds/voice_001_hau.mp3",
|
||||
voice_flee: "assets/sounds/voice_002_flee.mp3",
|
||||
voice_poop: "assets/sounds/voice_003_po.mp3",
|
||||
sfx_eat: "assets/sounds/voice_004_eat.mp3",
|
||||
voice_stress: "assets/sounds/voice_005_stress.mp3",
|
||||
sfx_sleep: "assets/sounds/voice_006_sleep.mp3",
|
||||
sfx_firecracker_explode: "assets/sounds/firecracker_explosion.mp3"
|
||||
},
|
||||
categories: {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,23 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
this.hpBarTimer = opts.hpBarTimer ?? 0;
|
||||
this.stressBarTimer = opts.stressBarTimer ?? 0;
|
||||
this.affection = opts.affection ?? rand(0, 30);
|
||||
this.need = opts.need || pick(NEEDS);
|
||||
const defaultNeeds = typeof createDefaultNeeds === "function" ? createDefaultNeeds() : { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 };
|
||||
this.needs = opts.needs && typeof opts.needs === "object" ? { ...defaultNeeds, ...opts.needs } : { ...defaultNeeds };
|
||||
this.previousNeeds = opts.previousNeeds && typeof opts.previousNeeds === "object" ? { ...this.needs, ...opts.previousNeeds } : { ...this.needs };
|
||||
this.needsInitialized = Boolean(opts.needsInitialized);
|
||||
this.needShock = opts.needShock && typeof opts.needShock === "object" ? { ...defaultNeeds, ...opts.needShock } : { ...defaultNeeds };
|
||||
this.need = opts.need || opts.intent?.need || "fulfill";
|
||||
this.intent = opts.intent && typeof opts.intent === "object" ? { ...opts.intent } : {
|
||||
need: this.need,
|
||||
tiedNeeds: [this.need],
|
||||
actionId: "idle",
|
||||
actionLabel: "\u5f85\u3063\u3066\u3044\u308b",
|
||||
reasonText: "\u5f85\u3063\u3066\u3044\u308b",
|
||||
};
|
||||
this.panicTimer = opts.panicTimer ?? 0;
|
||||
this.buildTimer = opts.buildTimer ?? 0;
|
||||
this.buildType = opts.buildType || "";
|
||||
this.buildMaterialId = opts.buildMaterialId || "";
|
||||
this.state = opts.state || "idle";
|
||||
this.dead = !!opts.dead;
|
||||
this.deathReason = opts.deathReason || "";
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@
|
|||
if (Number.isFinite(opts.surpriseTimer)) this.surpriseTimer = Math.max(this.surpriseTimer || 0, opts.surpriseTimer);
|
||||
if (Number.isFinite(opts.hurtTimer)) this.hurtTimer = Math.max(this.hurtTimer || 0, opts.hurtTimer);
|
||||
if (Number.isFinite(opts.awakeLockTimer)) this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, opts.awakeLockTimer);
|
||||
this.panicTimer = Math.max(this.panicTimer || 0, Number.isFinite(opts.duration) ? opts.duration : 3.0);
|
||||
if (Number.isFinite(opts.fallTimer)) {
|
||||
this.fallTimer = Math.max(this.fallTimer || 0, opts.fallTimer);
|
||||
this.fallMax = Math.max(this.fallMax || 0, this.fallTimer);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,19 @@
|
|||
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_needs_items.js");
|
||||
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
|
||||
canEatItemType(type) {
|
||||
return this.isZunchiSlave ? type === "zunchi" : true;
|
||||
const t = String(type || "");
|
||||
if (!this.isZunchiSlave) return true;
|
||||
if (!t || t === "sweet" || t === "grass" || t === "water" || t === "water_bowl") return false;
|
||||
if (t === "zunchi" || t === "food" || t === "ant_corpse") return true;
|
||||
const foodDef = global.TarinaiFoodRegistry?.get?.(t);
|
||||
return Boolean(foodDef && foodDef.interest !== "water");
|
||||
},
|
||||
|
||||
zunchiSlaveFoodTypes() {
|
||||
const defs = global.TARINAI_FOOD_DEFINITIONS || {};
|
||||
const types = Object.keys(defs).filter(type => this.canEatItemType(type));
|
||||
if (!types.includes("zunchi")) types.push("zunchi");
|
||||
return types;
|
||||
},
|
||||
|
||||
hasLoveMochiEffect() {
|
||||
|
|
@ -146,8 +158,8 @@
|
|||
startSunbath() {
|
||||
if (!this.canStartSunbath()) return false;
|
||||
this.setActionState?.("sunbath", { target: null, reason: "日なたぼっこ", sleeping: false });
|
||||
this.sunbathTimer = rand(6, 14);
|
||||
this.sunbathCooldown = rand(16, 34);
|
||||
this.sunbathTimer = rand(12, 24);
|
||||
this.sunbathCooldown = rand(8, 18);
|
||||
this.sunbathFrameTimer = 0;
|
||||
this.sunbathSpriteVariant = 1 + Math.floor(Math.random() * 3);
|
||||
this.sunbathRecoveryChecked = false;
|
||||
|
|
@ -166,7 +178,7 @@
|
|||
}
|
||||
this.stress = clamp((this.stress || 0) - dt * 1.15, 0, 130);
|
||||
this.energy = clamp((this.energy || 0) + dt * 0.08, 0, 100);
|
||||
if (!this.isSunbathTime() || this.hunger > 74 || this.energy < 10 || this.state === "panic") this.sunbathTimer = 0;
|
||||
if (!this.isSunbathTime() || this.hunger > 86 || this.energy < 8 || this.state === "panic") this.sunbathTimer = 0;
|
||||
if ((this.sunbathTimer || 0) <= 0 && this.state === "sunbath") {
|
||||
this.goIdle("");
|
||||
}
|
||||
|
|
@ -211,6 +223,7 @@
|
|||
this.fearTimer = Math.max(0, this.fearTimer - dt);
|
||||
this.intimidateTimer = Math.max(0, this.intimidateTimer - dt);
|
||||
this.intimidatedTimer = Math.max(0, this.intimidatedTimer - dt);
|
||||
this.panicTimer = Math.max(0, (this.panicTimer || 0) - dt);
|
||||
this.blastSpinTimer = Math.max(0, (this.blastSpinTimer || 0) - dt);
|
||||
this.postBirthPeaceTimer = Math.max(0, (this.postBirthPeaceTimer || 0) - dt);
|
||||
this.hpBarTimer = Math.max(0, (this.hpBarTimer || 0) - dt);
|
||||
|
|
@ -453,8 +466,13 @@
|
|||
this.ensureFoodServings(it);
|
||||
if (!this.isServingFoodItem(it) || (it.foodServingsRemaining || 0) <= 0) return 0;
|
||||
const value = Number.isFinite(nutrition) ? nutrition : (window.TarinaiFoodRegistry?.nutrition?.(it.type, 0) ?? 0);
|
||||
it.foodServingsRemaining = Math.max(0, (it.foodServingsRemaining || 0) - 1);
|
||||
it.amount = it.foodServingsRemaining;
|
||||
if (!it.infiniteServing) {
|
||||
it.foodServingsRemaining = Math.max(0, (it.foodServingsRemaining || 0) - 1);
|
||||
it.amount = it.foodServingsRemaining;
|
||||
} else {
|
||||
it.foodServingsRemaining = Math.max(it.foodServingsRemaining || 0, it.foodServingsMax || 1);
|
||||
it.amount = it.foodServingsRemaining;
|
||||
}
|
||||
this.world?.emit?.("item:eaten", { tarinai: this, item: it, type: it.type, nutrition: value });
|
||||
return value;
|
||||
},
|
||||
|
|
@ -578,6 +596,20 @@
|
|||
}
|
||||
this.stuckPushpinId = null;
|
||||
}
|
||||
|
||||
if ((this.panicTimer || 0) > 0 && this.state === "panic") {
|
||||
this.setActionState?.("panic", {
|
||||
target: previousTarget || this.panicDestination?.(null, true) || null,
|
||||
reason: this.intent?.reasonText || "\u304a\u3069\u308d\u3044\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b",
|
||||
wake: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.TarinaiNeedSystem?.runNeedDecision) {
|
||||
window.TarinaiNeedSystem.runNeedDecision(this, this.world, dt);
|
||||
return;
|
||||
}
|
||||
|
||||
const friendNearby = this.bestFriendLive ? this.bestFriendLive(FRIEND_AFFINITY_THRESHOLD, 170) : null;
|
||||
const panicSource = (this.world?.nearbyTarinai?.(this.x, this.y, 128, true) || [])
|
||||
|
|
@ -623,6 +655,16 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.canStartSunbath?.()) {
|
||||
const moodBonus = (this.world?.colonyMood?.id === "sun_party") ? 0.72 : 0;
|
||||
const sickBonus = (this.zunchiDisease || this.explosionDisease || this.fightDisease) ? 0.48 : 0;
|
||||
const stressBonus = this.stress > 28 ? 0.22 : 0;
|
||||
const idleBonus = this.state === "idle" ? 0.18 : 0;
|
||||
if (Math.random() < dt * (0.34 + moodBonus + sickBonus + stressBonus + idleBonus)) {
|
||||
if (this.startSunbath()) return;
|
||||
}
|
||||
}
|
||||
|
||||
const dayP = this.world.dayProgress();
|
||||
const personalSleep = this.sleepStart < this.sleepEnd
|
||||
? dayP >= this.sleepStart && dayP < this.sleepEnd
|
||||
|
|
@ -793,14 +835,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (friendNearby && this.hunger > 58 && friendNearby.target && (this.isZunchiSlave ? ["zunchi"] : ["sweet", "grass", "ant_corpse"]).includes(friendNearby.target.type) && !friendNearby.target.dead && !this.shouldAvoidTarget(friendNearby.target)) {
|
||||
if (friendNearby && this.hunger > 58 && friendNearby.target && (this.isZunchiSlave ? this.canEatItemType(friendNearby.target.type) : ["sweet", "grass", "ant_corpse"].includes(friendNearby.target.type)) && !friendNearby.target.dead && !this.shouldAvoidTarget(friendNearby.target)) {
|
||||
this.setActionState?.("seek_food", { target: friendNearby.target, reason: "友達と一緒に食べに行く" });
|
||||
this.foodReactTimer = Math.max(this.foodReactTimer, 0.24);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hunger > 62) {
|
||||
const itemTypes = this.isZunchiSlave ? ["zunchi"] : ["sweet", ...((window.TarinaiFoodRegistry?.typesByInterest?.("grass_like", "corpse")) || ["grass", "ant_corpse"])];
|
||||
const itemTypes = this.isZunchiSlave ? this.zunchiSlaveFoodTypes() : ["sweet", ...((window.TarinaiFoodRegistry?.typesByInterest?.("grass_like", "corpse")) || ["grass", "ant_corpse"])];
|
||||
const item = this.world.nearest(this, itemTypes, 420);
|
||||
if (item) {
|
||||
this.setActionState?.("seek_food", { target: item, reason: "食べ物を探している" });
|
||||
|
|
@ -856,12 +898,13 @@
|
|||
interactWithItems(dt) {
|
||||
const canBite = !this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 18;
|
||||
const canSweetBite = !this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 7;
|
||||
const slaveCanFoodBite = this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 16;
|
||||
const canZunchiBite = this.eatCooldown <= 0 && this.hunger > (this.isZunchiSlave ? 16 : 104);
|
||||
for (const it of this.world.nearbyItems(this.x, this.y, this.radius + 42)) {
|
||||
const d = dist(this, it);
|
||||
if (d > this.radius + it.r + 12) continue;
|
||||
|
||||
if (((it.type === "sweet" && canSweetBite) || (it.type === "food" && canBite)) && this.foodItemHasServingLeft(it)) {
|
||||
if (((it.type === "sweet" && canSweetBite) || (it.type === "food" && (canBite || (slaveCanFoodBite && this.canEatItemType(it.type))))) && this.foodItemHasServingLeft(it)) {
|
||||
const eating = this.consumeFoodServing(it);
|
||||
if (eating > 0) {
|
||||
this.beginEating(it, `${toolLabel(it.type)}を食べている`);
|
||||
|
|
@ -896,7 +939,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (canSweetBite && (it.type === "love_mochi" || it.type === "fight_mochi" || it.type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(it.type))) && this.foodItemHasServingLeft(it)) {
|
||||
if ((canSweetBite || (slaveCanFoodBite && this.canEatItemType(it.type))) && (it.type === "love_mochi" || it.type === "fight_mochi" || it.type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(it.type))) && this.foodItemHasServingLeft(it)) {
|
||||
const eating = this.consumeFoodServing(it);
|
||||
if (eating <= 0) continue;
|
||||
this.beginEating(it, `${toolLabel(it.type)}を食べている`);
|
||||
|
|
@ -946,9 +989,14 @@
|
|||
this.grassEatTimer = Math.max(this.grassEatTimer, 0.22);
|
||||
this.vx *= Math.pow(0.20, dt * 60);
|
||||
this.vy *= Math.pow(0.20, dt * 60);
|
||||
it.amount -= bite;
|
||||
it.eatenAmount = (it.eatenAmount || 0) + bite;
|
||||
it.growth = clamp((it.growth ?? it.amount / 120) - bite / 150, 0.04, 1.2);
|
||||
if (!it.infiniteServing) {
|
||||
it.amount -= bite;
|
||||
it.eatenAmount = (it.eatenAmount || 0) + bite;
|
||||
it.growth = clamp((it.growth ?? it.amount / 120) - bite / 150, 0.04, 1.2);
|
||||
} else {
|
||||
it.amount = Math.max(it.amount || 0, 120);
|
||||
it.growth = Math.max(it.growth || 0, 0.82);
|
||||
}
|
||||
this.applyFoodHungerRelief ? this.applyFoodHungerRelief(bite * 0.46) : (this.hunger -= bite * 0.46);
|
||||
this.recoverHealth(bite * 0.18);
|
||||
this.stress -= dt * 0.5;
|
||||
|
|
@ -965,13 +1013,14 @@
|
|||
break;
|
||||
}
|
||||
|
||||
if (canBite && it.type === "ant_corpse") {
|
||||
if ((canBite || (slaveCanFoodBite && this.canEatItemType(it.type))) && it.type === "ant_corpse") {
|
||||
const bite = Math.min(it.amount, rand(3.2, 5.4));
|
||||
if (bite > 0) {
|
||||
this.beginEating(it, "アリの死骸を食べている");
|
||||
this.vx *= Math.pow(0.18, dt * 60);
|
||||
this.vy *= Math.pow(0.18, dt * 60);
|
||||
it.amount -= bite;
|
||||
if (!it.infiniteServing) it.amount -= bite;
|
||||
else it.amount = Math.max(it.amount || 0, 24);
|
||||
this.applyFoodHungerRelief ? this.applyFoodHungerRelief(bite * 0.62) : (this.hunger -= bite * 0.62);
|
||||
this.recoverHealth(bite * 0.12);
|
||||
this.stress = clamp(this.stress + bite * 0.05 * this.personalityProfile().fear, 0, 130);
|
||||
|
|
@ -1000,7 +1049,8 @@
|
|||
this.beginEating(it, this.isZunchiSlave ? "ずんちどれいなのでずんちを食べている" : "ずんちを食べている");
|
||||
this.vx *= Math.pow(0.18, dt * 60);
|
||||
this.vy *= Math.pow(0.18, dt * 60);
|
||||
it.amount -= bite;
|
||||
if (!it.infiniteServing) it.amount -= bite;
|
||||
else it.amount = Math.max(it.amount || 0, 240);
|
||||
this.applyFoodHungerRelief ? this.applyFoodHungerRelief(bite * (this.isZunchiSlave ? 0.66 : 0.18)) : (this.hunger -= bite * (this.isZunchiSlave ? 0.66 : 0.18));
|
||||
this.recoverHealth(bite * (this.isZunchiSlave ? 0.10 : 0.02));
|
||||
this.stress = clamp(this.stress + bite * (this.isZunchiSlave ? 0.06 : 0.52) * this.personalityProfile().fear, 0, 130);
|
||||
|
|
@ -1025,7 +1075,8 @@
|
|||
audio.play?.("sfx_drink", { category: "ops", minGap: 0.20 });
|
||||
this.nextDrinkSound = (this.world?.time || 0) + 0.72;
|
||||
}
|
||||
if (it.type === "water") it.amount -= dt * 2.4;
|
||||
if (it.type === "water" && !it.infiniteServing) it.amount -= dt * 2.4;
|
||||
if (it.type === "water" && it.infiniteServing) it.amount = Math.max(it.amount || 0, 120);
|
||||
if (this.type === "teary" || this.type === "cry") this.affection += dt * 0.18;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -166,6 +166,25 @@ function selectedInfoRowHtml(label, valueHtml, metric = "") {
|
|||
return `<div class="info-row"${metricAttr}><span>${escapeHtml(label)}</span><strong>${valueHtml}</strong></div>`;
|
||||
}
|
||||
|
||||
function selectedNeedRowsHtml(t) {
|
||||
const system = window.TarinaiNeedSystem;
|
||||
const keys = system?.NEED_KEYS || ["food", "sleep", "health", "safety", "social", "fulfill"];
|
||||
const labels = system?.NEED_LABELS || {
|
||||
food: "\u6442\u990c",
|
||||
sleep: "\u7761\u7720",
|
||||
health: "\u5065\u5eb7",
|
||||
safety: "\u5b89\u5168",
|
||||
social: "\u95a2\u4fc2",
|
||||
fulfill: "\u5145\u8db3",
|
||||
};
|
||||
const active = t?.intent?.need || t?.need || "";
|
||||
return keys.map(key => {
|
||||
const value = system?.getNeedDisplayValue ? system.getNeedDisplayValue(t?.needs?.[key] || 0) : Math.round((t?.needs?.[key] || 0) / 10);
|
||||
const mark = key === active ? ` <span class="selected-need-mark">\u25b2</span>` : "";
|
||||
return `<div class="info-row selected-need-row${key === active ? " active" : ""}" data-need="${escapeHtml(key)}"><span>${escapeHtml(labels[key] || key)}</span><strong>${escapeHtml(value)}${mark}</strong></div>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function renderSelected() {
|
||||
const t = world.selected;
|
||||
if (!t || t.dead || !world.tarinai.includes(t)) {
|
||||
|
|
@ -201,6 +220,7 @@ function renderSelected() {
|
|||
t.isZunchiSlave ? 1 : 0, fmt(t.totalFightLosses || 0), fmt(t.loveMochiTimer || 0), fmt(t.fightMochiTimer || 0),
|
||||
t.zunchiDisease ? 1 : 0, fmt(t.zunchiDiseaseSeverity || 0), fmt(t.zunchiStain || 0),
|
||||
t.sleepDisease ? 1 : 0, t.explosionDisease ? 1 : 0, fmt(t.explosionDiseaseTimer || 0), t.fightDisease ? 1 : 0, t.favorite ? 1 : 0,
|
||||
JSON.stringify(t.needs || {}), t.intent?.need || "", t.intent?.actionId || "", t.intent?.actionLabel || "", t.intent?.reasonText || "",
|
||||
personalitySnapshot, personalityDailySnapshot(t),
|
||||
["life", "attack", "speed", "size"].map(key => geneticPercentText(t, key)).join(","), fmt((t.growth || 0) * 100),
|
||||
itemEffectSummary.map(e => `${e.id}:${e.detail}`).join(","),
|
||||
|
|
@ -239,7 +259,8 @@ function renderSelected() {
|
|||
const basicHtml = `
|
||||
<div class="info-row"><span>\u4e16\u4ee3</span><strong>${escapeHtml(generationDisplay || "\u4e0d\u660e")}</strong></div>
|
||||
<div class="info-row"><span>\u72b6\u614b</span><strong>${escapeHtml(stateLabel(t.state))}</strong></div>
|
||||
<div class="info-row"><span>\u7406\u7531</span><strong>${escapeHtml(reasonLabel(t))}</strong></div>
|
||||
<div class="info-row"><span>\u3044\u307e</span><strong>${escapeHtml(t.intent?.actionLabel || stateLabel(t.state))}</strong></div>
|
||||
<div class="info-row"><span>\u7406\u7531</span><strong>${escapeHtml(t.intent?.reasonText || reasonLabel(t))}</strong></div>
|
||||
<div class="info-row"><span>\u5bfe\u8c61</span><strong>${escapeHtml(targetLabel(t.target))}</strong></div>
|
||||
<div class="info-row"><span>\u5e74\u9f62</span><strong>${fmt(t.age)} / ${fmt(t.lifeSpan)}</strong></div>
|
||||
<div class="info-row"><span>\u6700\u5927\u5bff\u547d</span><strong>${geneticPercentText(t, "life")}</strong></div>
|
||||
|
|
@ -247,14 +268,7 @@ function renderSelected() {
|
|||
<div class="info-row"><span>\u79fb\u52d5\u901f\u5ea6</span><strong>${geneticPercentText(t, "speed")}</strong></div>
|
||||
<div class="info-row"><span>\u30b5\u30a4\u30ba</span><strong>${geneticSizeText(t)}</strong></div>
|
||||
`;
|
||||
const healthHtml = [
|
||||
selectedInfoRowHtml("\u7dcf\u5408\u7684\u6c17\u5206", fmt(t.mood), "mood"),
|
||||
selectedInfoRowHtml("\u7a7a\u8179", fmt(t.hunger), "hunger"),
|
||||
selectedInfoRowHtml("\u5bc2\u3057\u3055", fmt(t.loneliness), "loneliness"),
|
||||
selectedInfoRowHtml("\u4f53\u529b", fmt(t.energy), "energy"),
|
||||
selectedInfoRowHtml("\u30b9\u30c8\u30ec\u30b9", fmt(t.stress), "stress"),
|
||||
selectedInfoRowHtml("\u305a\u3093\u3061\u6c5a\u308c", fmt(t.zunchiStain || 0), "zunchi"),
|
||||
].join("");
|
||||
const healthHtml = selectedNeedRowsHtml(t) + selectedInfoRowHtml("\u30b9\u30c8\u30ec\u30b9", fmt(t.stress), "stress");
|
||||
const itemEffectHtml = itemEffectSummary.length
|
||||
? itemEffectSummary.map(e => `<div class="info-row"><span>${escapeHtml(e.label)}</span><strong>${escapeHtml(e.detail)}</strong></div>`).join("")
|
||||
: `<div class="info-row"><span>\u30a2\u30a4\u30c6\u30e0\u52b9\u679c</span><strong>\u306a\u3057</strong></div>`;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
(function () {
|
||||
const APP_VERSION = "15.24.0";
|
||||
const APP_BUILD = "safe-refactor-ui-fix";
|
||||
const APP_VERSION = "15.35.0";
|
||||
const APP_BUILD = "hash-v10-shorter-current-only";
|
||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,31 @@
|
|||
return hit;
|
||||
},
|
||||
|
||||
damageStructuresInRadius(x, y, radius, damageFn, breaker = null) {
|
||||
let hit = 0;
|
||||
for (const structure of this.items || []) {
|
||||
if (!structure || structure.dead || !structure.isStructure) continue;
|
||||
const d = Math.max(1, distXY(structure.x, structure.y, x, y));
|
||||
if (d > radius + (structure.r || 18)) continue;
|
||||
const p = clamp(1 - d / Math.max(1, radius), 0, 1);
|
||||
if (p <= 0 && d > radius) continue;
|
||||
const damage = typeof damageFn === "function" ? damageFn(p, structure, d) : Number(damageFn || 0);
|
||||
if (damage <= 0) continue;
|
||||
if (typeof structure.damage === "function") structure.damage(damage, breaker, this);
|
||||
else if (window.TarinaiNeedSystem?.damageStructure) window.TarinaiNeedSystem.damageStructure.call(structure, damage, breaker, this);
|
||||
else {
|
||||
structure.hp = Math.max(0, Number(structure.hp || 0) - damage);
|
||||
if (structure.hp <= 0) structure.dead = true;
|
||||
}
|
||||
hit += 1;
|
||||
}
|
||||
if (hit > 0) {
|
||||
this.drawListDirty = true;
|
||||
this.markTerrainDirty?.("structure-damage");
|
||||
}
|
||||
return hit;
|
||||
},
|
||||
|
||||
explodeFirecracker(it) {
|
||||
if (!it || it.dead || it.type !== "firecracker") return;
|
||||
it.amount = 0;
|
||||
|
|
@ -109,6 +134,7 @@
|
|||
this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u7af9", {
|
||||
push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * (it.blastScale || 1),
|
||||
});
|
||||
this.damageStructuresInRadius(x, y, blastRadius, p => 35 + p * 90, it);
|
||||
let blastedBalls = 0;
|
||||
for (const ball of this.items) {
|
||||
if (!ball || ball.dead || ball.type !== "ball") continue;
|
||||
|
|
@ -199,6 +225,7 @@
|
|||
this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u767a\u75c5", {
|
||||
push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * blastScale,
|
||||
});
|
||||
this.damageStructuresInRadius(x, y, blastRadius, q => 28 + q * 76, t);
|
||||
for (const ball of this.items) {
|
||||
if (!ball || ball.dead || ball.type !== "ball") continue;
|
||||
let dx = ball.x - x;
|
||||
|
|
@ -589,6 +616,7 @@
|
|||
const antHit = this.damageAntsInRadius(it.x, it.y, radius + 54, p => damage * (0.35 + p * 0.65), reason, {
|
||||
push: p => push * (0.62 + p * 1.25),
|
||||
});
|
||||
const structureHit = this.damageStructuresInRadius(it.x, it.y, radius + 54, p => damage * (0.45 + p * 0.85), it);
|
||||
if (isGenkotsu) {
|
||||
this.blastZunchiFrom?.(it.x, it.y, radius + 92, 2.75);
|
||||
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)" }));
|
||||
|
|
@ -596,7 +624,7 @@
|
|||
} 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 || isGenkotsu) this.spawnFallEffect(it.x, it.y + (it.r || 12) * 0.6, isGenkotsu ? 1.32 : (isStone ? 0.9 : 0.45));
|
||||
if (hit > 0 || antHit > 0 || structureHit > 0 || isGenkotsu) this.spawnFallEffect(it.x, it.y + (it.r || 12) * 0.6, isGenkotsu ? 1.32 : (isStone ? 0.9 : 0.45));
|
||||
if (isGenkotsu) audio.genkotsuImpact?.();
|
||||
else if (isStone) audio.stoneImpact?.();
|
||||
else if (hit > 0 || antHit > 0) audio.poke?.();
|
||||
|
|
|
|||
|
|
@ -211,6 +211,73 @@
|
|||
return item;
|
||||
},
|
||||
|
||||
signboardAt(x, y) {
|
||||
let sign = null;
|
||||
let bestD = Infinity;
|
||||
for (let i = (this.items || []).length - 1; i >= 0; i--) {
|
||||
const it = this.items[i];
|
||||
if (!it || it.dead || it.type !== "signboard") continue;
|
||||
const d = distXY(x, y, it.x, it.y);
|
||||
if (d <= Math.max(72, (it.r || 42) * 2.2) && d < bestD) { sign = it; bestD = d; }
|
||||
}
|
||||
return sign;
|
||||
},
|
||||
|
||||
machineAt(x, y) {
|
||||
let best = null;
|
||||
let bestD = Infinity;
|
||||
for (let i = (this.items || []).length - 1; i >= 0; i--) {
|
||||
const it = this.items[i];
|
||||
if (!it || it.dead || !(typeof isMachineType === "function" ? isMachineType(it.type) : (it.type === "duplicator" || it.type === "cannon"))) continue;
|
||||
const hit = Math.max(46, (it.r || 42) * (it.type === "cannon" ? 1.35 : 1.55));
|
||||
const d = distXY(x, y, it.x, it.y);
|
||||
if (d <= hit && d < bestD) { best = it; bestD = d; }
|
||||
}
|
||||
return best;
|
||||
},
|
||||
|
||||
setMachineSlot(machine, itemType) {
|
||||
if (!machine || machine.dead || !itemType || !(typeof isMachineConsumableType === "function" ? isMachineConsumableType(itemType) : false)) return false;
|
||||
if (machine.machineSlotType && machine.machineSlotType !== itemType) {
|
||||
for (const it of this.items || []) {
|
||||
if (it && !it.dead && it.machineProducerId === machine.id) it.amount = 0;
|
||||
}
|
||||
}
|
||||
machine.machineSlotType = itemType;
|
||||
machine.machineSlotSize = this.toolSizeFor ? this.toolSizeFor(itemType) : "medium";
|
||||
machine.machineSpawnTimer = 0;
|
||||
machine.cannonFireTimer = machine.type === "cannon" ? 0.35 : machine.cannonFireTimer;
|
||||
const label = toolLabel(itemType);
|
||||
this.log?.(`${toolLabel(machine.type)}に${label}をセットした。`, "observe");
|
||||
showToast(`${toolLabel(machine.type)}: ${label}をセット`);
|
||||
this.rebuildSpatial?.(true);
|
||||
return true;
|
||||
},
|
||||
|
||||
rotateCannon(cannon, x, y) {
|
||||
if (!cannon || cannon.dead || cannon.type !== "cannon") return false;
|
||||
const dx = x - cannon.x;
|
||||
const dy = y - cannon.y;
|
||||
if (Math.hypot(dx, dy) > 3) cannon.cannonAngle = Math.atan2(dy, dx);
|
||||
else cannon.cannonAngle = ((Number.isFinite(cannon.cannonAngle) ? cannon.cannonAngle : 0) + Math.PI / 4) % (Math.PI * 2);
|
||||
const deg = Math.round((((cannon.cannonAngle * 180 / Math.PI) % 360) + 360) % 360);
|
||||
showToast(`大砲の向き: ${deg}°`);
|
||||
this.log?.(`大砲の向きを${deg}度に変えた。`, "observe");
|
||||
return true;
|
||||
},
|
||||
|
||||
editSignboard(sign) {
|
||||
if (!sign || sign.dead || sign.type !== "signboard") return false;
|
||||
const current = String(sign.text || "");
|
||||
const next = typeof prompt === "function" ? prompt("看板のテキスト", current) : current;
|
||||
if (next === null) return true;
|
||||
sign.text = String(next || "").replace(/[\r\n]+/g, " ").slice(0, 36).trim();
|
||||
sign.textEditedAt = this.time || 0;
|
||||
this.markTerrainDirty?.("signboard:text");
|
||||
this.log(sign.text ? `看板を書き換えた。` : `看板の文字を消した。`, "observe");
|
||||
return true;
|
||||
},
|
||||
|
||||
handleClick(x, y) {
|
||||
const tool = this.tool;
|
||||
if (tool === "observe") {
|
||||
|
|
@ -219,22 +286,16 @@
|
|||
if (!this.isTarinaiHiddenInNestBox(this.tarinai[i]) && this.tarinai[i].contains(x, y)) { found = this.tarinai[i]; break; }
|
||||
}
|
||||
if (!found) {
|
||||
let sign = null;
|
||||
let bestD = Infinity;
|
||||
for (let i = this.items.length - 1; i >= 0; i--) {
|
||||
const it = this.items[i];
|
||||
if (!it || it.dead || it.type !== "signboard") continue;
|
||||
const d = distXY(x, y, it.x, it.y);
|
||||
if (d <= Math.max(54, (it.r || 24) * 2.4) && d < bestD) { sign = it; bestD = d; }
|
||||
const machine = this.machineAt?.(x, y) || null;
|
||||
if (machine) {
|
||||
if (machine.type === "cannon") this.rotateCannon?.(machine, x, y);
|
||||
else showToast(machine.machineSlotType ? `複製機: ${toolLabel(machine.machineSlotType)}を複製中` : "複製機: 消費アイテムを選んでクリックするとセット");
|
||||
this.selected = null;
|
||||
return;
|
||||
}
|
||||
const sign = this.signboardAt?.(x, y) || null;
|
||||
if (sign) {
|
||||
const current = String(sign.text || "");
|
||||
const next = typeof prompt === "function" ? prompt("看板のテキスト", current) : current;
|
||||
if (next !== null) {
|
||||
sign.text = String(next || "").replace(/[\r\n]+/g, " ").slice(0, 24).trim();
|
||||
sign.textEditedAt = this.time || 0;
|
||||
this.log(sign.text ? `看板を書き換えた。` : `看板の文字を消した。`, "observe");
|
||||
}
|
||||
this.editSignboard?.(sign);
|
||||
this.selected = null;
|
||||
return;
|
||||
}
|
||||
|
|
@ -284,7 +345,8 @@
|
|||
}
|
||||
if (!best) { showToast("\u524a\u9664\u3067\u304d\u308b\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); return; }
|
||||
if (isPinType(best.type) && best.pinState === "lodged") { showToast("\u523a\u3055\u3063\u305f\u92f2\u306f\u3064\u307e\u3093\u3067\u53d6\u308a\u9664\u3044\u3066\u304f\u3060\u3055\u3044\u3002"); return; }
|
||||
best.amount = 0;
|
||||
if (best.isStructure && typeof best.damage === "function") best.damage(9999, null, this);
|
||||
else best.amount = 0;
|
||||
audio.delete?.();
|
||||
this.compactItems();
|
||||
this.updateItemCounts();
|
||||
|
|
@ -295,7 +357,20 @@
|
|||
}
|
||||
|
||||
const itemType = toolItemType(tool);
|
||||
if (itemType === "signboard") {
|
||||
const sign = this.signboardAt?.(x, y) || null;
|
||||
if (sign) { this.editSignboard?.(sign); return; }
|
||||
}
|
||||
if (itemType) {
|
||||
const machine = this.machineAt?.(x, y) || null;
|
||||
if (machine && (typeof isMachineConsumableType === "function" ? isMachineConsumableType(itemType) : false)) {
|
||||
this.setMachineSlot?.(machine, itemType);
|
||||
return;
|
||||
}
|
||||
if (machine && machine.type === "cannon" && itemType !== "cannon" && itemType !== "duplicator") {
|
||||
this.rotateCannon?.(machine, x, y);
|
||||
return;
|
||||
}
|
||||
let px = x, py = y;
|
||||
const rawItem = this.applyToolSize(new Item(itemType, px, py));
|
||||
// Placement uses the same footprint that the preview validates.
|
||||
|
|
@ -303,6 +378,7 @@
|
|||
if (!placed) return;
|
||||
const label = toolLabel(itemType);
|
||||
this.log(itemType === "genkotsu" || isPinType(itemType) ? `${label}\u3092\u843d\u3068\u3057\u305f\u3002` : `${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`);
|
||||
if (itemType === "signboard") this.editSignboard?.(placed);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
"use strict";
|
||||
|
||||
const APP_VERSION = "15.24.0";
|
||||
const APP_VERSION = "15.35.0";
|
||||
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const v = `v=${APP_VERSION}`;
|
||||
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.
|
||||
const coreScriptNames = [
|
||||
"version", "event_bus", "performance_manager", "registry_base", "disease_registry", "effect_registry", "sound_pack", "data", "item_visual_registry", "food_registry", "input_mode_manager", "math", "physics_helpers", "assets", "audio", "render", "sim_core", "items", "ants", "health", "tarinai", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_items", "tarinai_social_move_life", "tarinai_render", "world", "world_view", "family_graph", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "world_update", "world_placement_log", "world_bootstrap", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "snapshot_system", "save_system", "patch_helpers", "freeze_system", "relation_event_system", "ui_layout_dialogs", "ui_tools", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
|
||||
"version", "event_bus", "performance_manager", "registry_base", "disease_registry", "effect_registry", "sound_pack", "data", "item_visual_registry", "food_registry", "input_mode_manager", "math", "need_behavior_system", "physics_helpers", "assets", "audio", "render", "sim_core", "items", "ants", "health", "tarinai", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_items", "tarinai_social_move_life", "tarinai_render", "world", "world_view", "family_graph", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "world_update", "world_placement_log", "world_bootstrap", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "snapshot_system", "save_system", "patch_helpers", "freeze_system", "relation_event_system", "ui_layout_dialogs", "ui_tools", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
|
||||
];
|
||||
const CORE_ASSETS = [
|
||||
"./",
|
||||
|
|
|
|||