Compare commits
4 commits
f657b6a4a4
...
28591233f3
| Author | SHA1 | Date | |
|---|---|---|---|
| 28591233f3 | |||
| 091afc2b5c | |||
| f6d7412744 | |||
| d163ceb291 |
1
.achievement_data/state.json
Normal file
9
.gitignore
vendored
|
|
@ -1,9 +0,0 @@
|
|||
.tmp-edge-profile/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Local browser smoke/probe artifacts.
|
||||
*probe*.html
|
||||
*probe*.png
|
||||
*smoke*.html
|
||||
*smoke*.png
|
||||
15
.htaccess
|
|
@ -1,3 +1,5 @@
|
|||
AddType application/manifest+json .webmanifest
|
||||
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType text/html "access plus 0 seconds"
|
||||
|
|
@ -21,15 +23,22 @@
|
|||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</FilesMatch>
|
||||
<FilesMatch "\.(css|js|json|webmanifest|ico|png|webp|mp3|wav)$">
|
||||
Header set Cache-Control "public, max-age=2592000, immutable"
|
||||
</FilesMatch>
|
||||
<FilesMatch "service-worker\.js$">
|
||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</FilesMatch>
|
||||
<FilesMatch "\.(css|js|json|png|webp|mp3|wav)$">
|
||||
Header set Cache-Control "public, max-age=2592000, immutable"
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json text/plain
|
||||
</IfModule>
|
||||
|
||||
|
||||
# Never serve hidden server-data paths.
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteRule (^|/)\. - [R=404,L]
|
||||
</IfModule>
|
||||
|
|
|
|||
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"liveServer.settings.port": 5501
|
||||
}
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
# 実績システム再監査レポート v39.16.78
|
||||
|
||||
対象: 全72実績、解除イベント、判定順序、進捗リセット、保存・復元、Undo/Redo、サーバー同期
|
||||
方針: 前回重点対象だった「自分のずんち」「敵の敵は味方」だけでなく、これまで大きく取り上げていなかった実績を再度コード経路から監査した。明確な不具合は修正し、ゲームデザイン判断が必要な条件は変更していない。
|
||||
|
||||
## 1. 今回新たに確定・修正した不具合
|
||||
|
||||
### 1.1 「ミニマリスト」: プレイヤー設置物情報が保存されなくなる
|
||||
|
||||
`ミニマリスト` は「現在存在するプレイヤー設置物が10個以下」を判定するため、各アイテムの `_achievementPlayerPlaced` を必要とする。
|
||||
|
||||
旧実装では、この情報を保存する条件が実質的に `無計画都市` の未解除状態へ依存していた。そのため、`無計画都市` を先に解除した後にセーブ・ロードすると、既存アイテムのプレイヤー設置情報が失われ、`ミニマリスト` の物数判定が過少になる可能性があった。
|
||||
|
||||
**修正:** `無計画都市` と `ミニマリスト` のどちらかが未解除なら、プレイヤー設置情報を保存する。30秒削除用の設置時刻は従来どおり `無計画都市` に必要な場合だけ保存する。
|
||||
|
||||
---
|
||||
|
||||
### 1.2 「サウナ水風呂」: 高温開始時刻が更新され続ける
|
||||
|
||||
条件は「暑すぎる状態になった個体を15秒以内に寒すぎる状態へ移す」だが、旧実装は高温状態を評価するたびに開始時刻を現在時刻へ更新していた。
|
||||
|
||||
このため、長時間高温に置いた後でも、寒冷へ移した直前の評価時刻との差だけで15秒以内と判定され得た。
|
||||
|
||||
**修正:** 快適域外の高温状態へ**入った瞬間**だけ開始時刻を記録し、連続して高温にいる間は更新しない。高温状態を離れてから再び入った場合は新しい試行として時刻を記録する。
|
||||
|
||||
---
|
||||
|
||||
### 1.3 「抵抗器じゃない」: 致死感電した7体目がカウントされない
|
||||
|
||||
旧実装では感電ダメージを与えた後に実績カウントを行っていた。感電が致死になると対象は死亡済みになり、`recordWireShock` の生存チェックで除外されるため、7体目がその感電で死亡すると解除できない場合があった。
|
||||
|
||||
**修正:** 有効な感電を確認した時点で、ダメージ適用より先に実績カウントを行う。致死・非致死のどちらでも、その感電自体は1体分として扱う。
|
||||
|
||||
---
|
||||
|
||||
### 1.4 実績リセット後に隠れた途中進捗が残る
|
||||
|
||||
全実績リセット後も、World・たりない・アイテム本体に直接保持された一部の途中状態が残っていた。そのため、リセット直後に少ない追加操作だけで再解除できる実績があった。
|
||||
|
||||
**修正したリセット対象:**
|
||||
|
||||
- 同一個体への直接給餌回数・直接治療回数
|
||||
- サウナ開始時刻・直前高温状態
|
||||
- たりない保持開始時刻
|
||||
- 喧嘩餅から喧嘩開始までの一時情報
|
||||
- 粘着爆弾の受け渡し回数
|
||||
- `無計画都市` 用の配置時刻
|
||||
- 自然奴隷・自然王の世代進捗
|
||||
|
||||
`ミニマリスト` が現在の配置物を正しく識別するため、既存アイテムの「プレイヤーが配置した物か」という事実自体はリセットで消していない。
|
||||
|
||||
---
|
||||
|
||||
### 1.5 「監視カメラ」: Undo/Redo・地面変更が介入になっていない
|
||||
|
||||
配置・削除・給餌・つつく・射撃などは観察タイマーをリセットしていた一方、Undo、Redo、有効な地面変更は世界状態を変更する操作なのに観察タイマーをリセットしていなかった。
|
||||
|
||||
**修正:** 成功したUndo/Redo、および実際に地面種別が変わった操作を介入として記録する。
|
||||
|
||||
---
|
||||
|
||||
## 2. 前回重点対象の修正状態
|
||||
|
||||
### 「あぁ^~ たまらねぇぜ」
|
||||
|
||||
現在の条件は次の経路で成立する。
|
||||
|
||||
`おしり鋲でずんち蓄積 → おしり鋲を抜いて排出 → 排出ずんちが本人から一度離れる → バウンス柵等で反射・加速 → 本人へ帰還して致死衝突`
|
||||
|
||||
- 生成直後の本人との重なりは衝突扱いしない。
|
||||
- 本人から一度離れた後に帰還した衝突だけを自己帰還として扱う。
|
||||
- 非致死では解除しない。
|
||||
- バウンス柵による反射・加速は資格を失わせない。
|
||||
- 停止済み・プレイヤー保持済みの排出ずんちは対象外。
|
||||
- 致死処理で本人の生存IDが解放されても、ダメージ前に所有者一致を保持するため解除可能。
|
||||
|
||||
### 「敵の敵は味方」
|
||||
|
||||
30体以上の状態で稼いだ連続数は、生存たりないが30体未満になった時点で0へ戻る。危険カテゴリのアイテムによるたりないへのダメージでも従来どおりリセットする。
|
||||
|
||||
前回ユーザー確認済みの「無傷」の範囲、戦闘ペア危険死の期限、機械化産業の履歴AND条件は現行仕様のまま変更していない。
|
||||
|
||||
---
|
||||
|
||||
## 3. これまで大きく取り上げていなかった実績の再監査結果
|
||||
|
||||
### 生態・繁殖
|
||||
|
||||
- `はじめての繁殖`、`ベビーブーム`、`大いなる母` は実際の繁殖成功イベントから接続されている。
|
||||
- `天寿`、`水銀で天寿` は最終確定した死亡理由を受けた後に判定される。
|
||||
- `25体全員が病気` は睡眠病だけを除外し、ずんち病・爆発病・喧嘩病を対象としている。
|
||||
- `管理された楽園` は6日目以降から連続1ゲーム日を計測する。
|
||||
- `安全第一` は25体以上を300ゲーム秒維持し、天寿以外の死亡で再計測する。天寿後に25体を下回れば人数条件によって停止する。
|
||||
- `自給自足` は20体以上・複製機なし・直接給餌なしを600ゲーム秒維持するフィールド内条件。
|
||||
- `雨宿り完了` は現行の雨天である小雨時に、パラソル・巣箱・土管の保護判定を使う。
|
||||
- `町のお医者さん` は応急手当を使った回数ではなく、実際にHPが1以上回復した回数だけを累計する。
|
||||
- `豊穣ヲ希求スル者` はラブ餅の影響下で成立した繁殖だけを累計する。
|
||||
|
||||
**追加の到達不能・イベント未接続は確認されなかった。**
|
||||
|
||||
### 実験・危険行動
|
||||
|
||||
- `6番目の大量絶滅` は同一Worldの10ゲーム秒ローリング区間で死亡50件。
|
||||
- `豆野、殺ッカーやろうぜ!` は通常ボールの速度条件を満たした致死衝突から直接接続される。
|
||||
- `ヘコヘコ中に着火` は着火処理が繁殖儀式状態を消す前に判定される。
|
||||
- `下剤中の餓死` は死亡フレームの下剤状態を保持して判定する。
|
||||
- `物理法則を下側に超越` はアイテム位置の実温度計算を走査しており、温度効果の重畳で到達可能。
|
||||
- `おくすり手帳全埋め` は現行仕様どおり11種類を対象とし、鎖・王冠は除外する。
|
||||
- `革命` は王の喧嘩死と、直前0.1ゲーム秒以内の奴隷による喧嘩ダメージを照合する。
|
||||
- `火に油` は喧嘩餅を受けた個体が30ゲーム秒以内に参加する喧嘩開始を判定する。
|
||||
- `五体満足` / `零体満足` は状態効果付与時と自然ステータス変化時の双方から再評価される。
|
||||
- `混沌ヲ希求スル者` は喧嘩餅影響下の喧嘩開始だけを永続累計する。
|
||||
- `命のバトン` は同じ生存中の粘着爆弾の受け渡し回数を保存・復元する。
|
||||
- `情報化産業` は別々の2基盤を電線類で直接つなぎ、信号が実際に伝達された経路から解除される。
|
||||
|
||||
**追加の確定不具合は「抵抗器じゃない」の致死感電順序だけで、修正済み。**
|
||||
|
||||
### 建築・環境
|
||||
|
||||
- `物でいっぱい` はプレイヤー配置イベントを永続累計し、リンク類の配置も配置として数える。
|
||||
- `清掃業者` はロボット掃除機による有効な清掃200回のフィールド内進捗で、現行ドキュメントと一致する。
|
||||
- `潔癖症` は生存たりない0、生存アリ0、生存アイテムがロボット掃除機のみ、かつ条件を満たす稼働掃除機が存在する場合に成立する。死亡たりないは死亡直後に非表示となり定期圧縮されるため、画面上の死体残留との矛盾は確認されなかった。
|
||||
- `メガロポリス` は100体・複製機10・草ベッド/巣箱/土管合計15の境界値で評価される。
|
||||
- `砂場` は生存中のトイレ5個で成立する。
|
||||
- `そこ公共空間だよ?` は公園フィールドで実際に地面変更が成立した時だけ解除する。
|
||||
- `破格の工事費用` は壁時計1秒以内の有効な地面変更4回を数える。
|
||||
|
||||
**追加の確定不具合は「ミニマリスト」の保存メタデータだけで、修正済み。**
|
||||
|
||||
### 操作・継続
|
||||
|
||||
- `秘密のコレクション` は9スロットすべてに実セーブハッシュがあることを確認する。
|
||||
- `何やってるの?` は空フィールドへのクリックだけを壁時計1秒内に4回数える。
|
||||
- `見なかったことにしよう` は1回のUndo前後で死亡個体が10体以上減った場合に成立する。
|
||||
- `やっぱなし` / `やっぱあり` は成功したUndo/Redoだけを永続累計する。
|
||||
- `割とふわふわ` は共通の「つつく」対象判定からぬいぐるみを飛ばした場合に成立する。
|
||||
- `狙撃手` は命中ではなく発砲333回を永続累計する。
|
||||
- `スペックがたりない` は有効な計測FPSが10未満になった場合で、非表示タブ由来の異常なフレーム間隔を除外する。
|
||||
- `毎日たりない観察` はローカル暦の日付単位の7日連続起動。
|
||||
- `寝ろ` / `めっちゃたりない観察` は壁時計の連続経過で、現行仕様どおりタブ非表示時間も継続として扱う。
|
||||
- `真・たりない観察` は自身以外の71実績が解除済みかを判定する。
|
||||
|
||||
**追加の確定不具合は「監視カメラ」の介入漏れと、全実績リセット後の隠れ進捗残留で、修正済み。**
|
||||
|
||||
---
|
||||
|
||||
## 4. 仕様確認が必要な曖昧点 — 現在は未変更
|
||||
|
||||
以下は実装が壊れているとは断定できず、表示文から複数の解釈が成立するため、回答を受けるまで条件変更を行っていない。
|
||||
|
||||
### Q1. 「ずんちどれい」「たりない王」の「出現」範囲
|
||||
|
||||
現行は**喧嘩戦績による自然変身だけ**で解除する。
|
||||
|
||||
- 鎖を付けて奴隷化 → 対象外
|
||||
- 王冠で王化 → 対象外
|
||||
- 奴隷同士から生まれた生来の奴隷 → 対象外
|
||||
|
||||
候補:
|
||||
|
||||
- **A: 現行どおり** — 喧嘩戦績による変身だけ
|
||||
- **B: 人工付与以外** — 喧嘩戦績変身 + 生来の奴隷を対象。鎖・王冠は除外
|
||||
- **C: 文字どおり全出現** — 鎖・王冠を含め、状態になった時点で解除
|
||||
|
||||
### Q2. 「末代までの恥」「華麗なる王族」の「○代連続」
|
||||
|
||||
現行は**同じ家系でなくても、自然変身した個体の世代番号が連番なら成立**する。例えば無関係な家系の2代・3代・4代・5代・6代に自然奴隷が1体ずついれば5代連続になる。
|
||||
|
||||
候補:
|
||||
|
||||
- **A: 現行どおり** — 世界内の世代番号の連続
|
||||
- **B: 直系家系限定** — 親子関係を辿って同じ系譜で連続して状態が続く必要がある
|
||||
|
||||
Q1で生来の奴隷を対象にする場合、Q2の奴隷5代判定にもその出生を含めるかを合わせて決める必要がある。
|
||||
|
||||
### Q3. 「過保護」の「薬で治療」
|
||||
|
||||
現行は、薬カテゴリの直接使用なら効果の善悪を問わず治療回数に加算する。対象には応急手当だけでなく、睡眠薬、下剤、プロテイン、ニテロプ、弾薬、水銀、巨大化薬、小型化薬なども含まれる。
|
||||
|
||||
候補:
|
||||
|
||||
- **A: 現行どおり** — 薬カテゴリの直接使用3回
|
||||
- **B: 治療系アイテムだけ** — 対象薬を明示的に限定
|
||||
- **C: 実際に有益な回復が発生した使用だけ**
|
||||
|
||||
### Q4. 「無計画都市」の「削除する操作を累計30回」
|
||||
|
||||
現行は**操作回数ではなく、30秒以内に削除された対象物1個につき1回**進む。範囲削除1回で対象物を10個消した場合、最大10進む。
|
||||
|
||||
候補:
|
||||
|
||||
- **A: 現行どおり** — 削除した対象物数を数える
|
||||
- **B: 削除コマンド単位** — クリック/範囲削除の1実行を1回として数える
|
||||
- **C: 連続削除ジェスチャ単位** — 押している間の連続削除を1回として数える
|
||||
|
||||
### Q5. 「監視カメラ」の5分と「見るだけ」の意味
|
||||
|
||||
現行は**300ゲーム秒**。高速化すれば実時間5分より早く解除できる。今回、世界を変更するUndo/Redo・地面変更も介入として追加したが、UI操作、セーブ、速度変更、ポーズ、空クリックなど世界を直接変更しない操作はタイマーを止めない。
|
||||
|
||||
時間基準:
|
||||
|
||||
- **A: 現行どおり300ゲーム秒**
|
||||
- **B: 実時間5分**
|
||||
|
||||
「見るだけ」の入力基準:
|
||||
|
||||
- **1: 現行どおり世界への介入だけでリセット**
|
||||
- **2: ほぼすべてのユーザー操作でリセット**
|
||||
|
||||
例: `Q5 = A1` のように指定可能。
|
||||
|
||||
### Q6. 「抵抗器じゃない」のセーブ/Undo跨ぎ
|
||||
|
||||
現行の「同じ1本」はランタイム中の同一電線オブジェクト。セーブロードやUndo/RedoでWorldが再構築されると、1~6体までの途中人数は失われる。
|
||||
|
||||
候補:
|
||||
|
||||
- **A: 現行どおり** — 7体すべてを同じランタイム中に感電させる
|
||||
- **B: 同じ保存上の電線なら継続** — セーブロード後も電線ごとの途中人数を保存する
|
||||
|
||||
### Q7. 「害虫駆除」の「アリを倒す」主体
|
||||
|
||||
現行は**アリが死亡した事実をすべて累計**する。プレイヤーの危険アイテムだけでなく、たりないとの戦闘、設備・環境など別要因で死亡しても1体として数える。
|
||||
|
||||
候補:
|
||||
|
||||
- **A: 現行どおり** — 死亡したアリをすべて数える
|
||||
- **B: プレイヤー由来の撃破だけ**
|
||||
- **C: プレイヤーまたはたりない陣営が倒した場合だけ**
|
||||
|
||||
---
|
||||
|
||||
## 5. 前回ユーザー確認済みの仕様 — 変更なし
|
||||
|
||||
以下は前回の確認により現行仕様を維持している。
|
||||
|
||||
- 戦闘ペア危険死: 第1死亡から第2死亡まで同一World内で明示的な時間制限なし。
|
||||
- 機械化産業: ロープ・棒・ばねの配置履歴と信号作動履歴のAND。現在同時に存在する必要なし。
|
||||
- 敵の敵は味方: 「無傷」は危険カテゴリ由来のたりないへのダメージをリセット対象とし、あらゆるダメージを禁止する意味ではない。
|
||||
|
||||
---
|
||||
|
||||
## 6. 検証結果
|
||||
|
||||
v39.16.78 に対して以下を実行し、すべて成功した。
|
||||
|
||||
- JavaScript全ファイル・監査スクリプトの `node --check`
|
||||
- 全72実績の定義・カテゴリUI・同期監査
|
||||
- プレイスタイル実績の境界値・リセット範囲・永続化・フック監査
|
||||
- 「あぁ^~ たまらねぇぜ」専用の生成直後除外 / 離脱 / 帰還 / 致死・非致死回帰テスト
|
||||
- サウナ開始時刻、実績リセット、監視介入、ミニマリスト保存情報、致死感電の追加回帰テスト
|
||||
- 実績イベント統合監査
|
||||
- サーバー状態・旧形式移行・並行同期監査
|
||||
- 全体回帰監査 `scripts/regression_check.py`
|
||||
- Python監査スクリプトのコンパイル確認
|
||||
- `achievement_api.php` のPHP構文確認
|
||||
|
||||
結果はすべて `[OK]`。今回の再監査で新たに確認した**確定不具合は5系統で、すべて修正済み**。上記Q1~Q7は仕様回答待ちのため、v39.16.78では現行挙動を維持している。
|
||||
|
||||
|
||||
## Confirmed specification decisions (39.16.78)
|
||||
- ずんちどれい / たりない王: birth-status appearances also qualify.
|
||||
- 末代までの恥 / 華麗なる王族: only a direct qualifying parent-child lineage counts.
|
||||
- 過保護: a treatment counts only when the direct use actually improves the health need.
|
||||
- 無計画都市: one deletion operation counts once, even if area deletion removes multiple objects.
|
||||
- 監視カメラ: requires 3 uninterrupted game days; gameplay operations are interventions.
|
||||
- 抵抗器じゃない: partial wire progress remains nonpersistent across save/load and Undo.
|
||||
- 害虫駆除: only player-caused ant deaths count.
|
||||
105
AI_MAP.md
|
|
@ -1,105 +0,0 @@
|
|||
# tarinai_ AI map
|
||||
|
||||
Purpose: static Japanese browser simulation/game. Optimize context by reading this file only after `README.md`, then open the relevant subsystem files. For exact file expansion, use `scripts/ai_inventory.ps1` or `scripts/ai_inventory.py`.
|
||||
|
||||
## Architecture
|
||||
|
||||
- Entrypoint: `index.html` defines the app shell, canvas, dialogs, right panels, and exact script order.
|
||||
- Runtime style: plain browser JS, global IIFEs, exports on `window`, no bundler/module loader.
|
||||
- Version/cache: `app_manifest.json`, `js/version.js`, generated `service-worker.js`, and `index.html` query params share `39.16.78`.
|
||||
- Main loop: `js/main.js` initializes assets/audio/world/UI/save, then drives update/render.
|
||||
- Performance/display settings: `js/perf_profiler.js` owns manual visual settings and profiler buckets; display tiers affect rendering/visuals, not simulation-quality branches.
|
||||
- Update order: `js/system_order.js` calls phase facades from `js/simulation_systems.js`.
|
||||
- Command path: UI/input emits command objects; `js/command_dispatcher.js` routes tool, pointer, selection, save, reset, and UI intents.
|
||||
- Persistence: save files are schema/codec/storage/coordinator modules; snapshot/history modules keep world restoration and stats consistent.
|
||||
|
||||
## Subsystem Map
|
||||
|
||||
- Core/bootstrap: `js/version.js`, `js/event_bus.js`, registries, `js/data.js`, `js/ground_types.js`, `js/health.js`, `js/main.js`, `js/debug_tools.js`, `js/perf_profiler.js`.
|
||||
- Rendering/assets/audio: `js/assets.js`, `js/audio.js`, `js/render.js`, `js/tarinai_render.js`, `js/item_render_runtime.js`, `js/sound_pack.js`.
|
||||
- Simulation phases: `js/sim_core.js`, `js/simulation*.js`, `js/world_update*.js`, `js/system_order.js`.
|
||||
- World domain: `js/world*.js`, `js/weather_system.js`; covers state, camera/view, environment, combat/effects, placement/history, tools, family/social, ants, spatial budgets.
|
||||
- Creature domain: `js/tarinai*.js`; covers state, identity, needs, actions, forced behavior, social life, item effects, disease/nests, movement/update pipeline, rendering.
|
||||
- Item/structure domain: `js/item*.js`, `js/items.js`, `js/structures.js`, `js/structure_lifecycle.js`; covers definitions, spawning defaults, lifecycle, dynamic behavior, rendering.
|
||||
- Physics/mechanics: `js/physics*.js`, `js/mechanical_system.js`, `js/constraint_system.js`, `js/collision_footprint_system.js`.
|
||||
- UI/input: `js/ui*.js`, `js/text_catalog.js`, `js/command_dispatcher.js`; covers panels, tools, selected creature, logs, charts, family tree, mouse/touch, dialogs, labels.
|
||||
- Family tree: `js/family_graph.js`, `js/ui_family_*.js`; data transform, layout, paths, async rendering, validation.
|
||||
- Save/history: `js/save*.js`, `js/snapshot_system.js`, `js/restore_coordinator.js`, `js/history_system.js`.
|
||||
- Scripts: `scripts/generate_app_files.py`, `scripts/generate_item_icons.py`, `scripts/regression_check.py`, `scripts/ai_inventory.py`, `scripts/ai_inventory.ps1`.
|
||||
|
||||
## File Pattern Semantics
|
||||
|
||||
- `*_pipeline.js`: ordered runner/facade for update or lifecycle work.
|
||||
- `*_step_*.js`: narrow pipeline step wrapper.
|
||||
- `*_system.js`: domain service, phase service, or compatibility facade.
|
||||
- `*_runtime.js`: stable runtime-facing facade around split modules.
|
||||
- `*_registry.js`: definition registry and lookup/normalization helpers.
|
||||
- `world_*`: world-owned behavior; `tarinai_*`: creature-owned behavior; `item_*`: item-owned behavior; `ui_*`: DOM/input/panel behavior.
|
||||
- `assets/sprites/tarinai_*.webp`: creature visual state sprites; numeric prefix is stable asset ID/order.
|
||||
- `assets/ui/tool_*`: tool palette icons; `assets/ui/favicon*`, `apple-touch-icon`, `ecology_*` are app/help icons.
|
||||
- `assets/objects/*`: field object sprites for ants, zunchi, pushpin, oshibyo, genkotsu, plushie.
|
||||
- `assets/sounds/voice_*`: creature voice samples; `shoot_*`, `major_damage_*`, `firecracker_*` are SFX.
|
||||
|
||||
## Task-Oriented Read Sets
|
||||
|
||||
- UI/layout bug: `index.html`, relevant `css/*.css`, `js/ui_bind.js`, `js/ui_layout_dialogs.js`, matching `js/ui_*.js`.
|
||||
- Tool behavior: `js/command_dispatcher.js`, `js/world_tool_actions.js`, `js/item_registry.js`, relevant `js/item_*` or `js/tarinai_item_*`.
|
||||
- Creature behavior: `js/tarinai.js`, `js/tarinai_update_pipeline.js`, relevant `js/tarinai_*`, `js/system_order.js`.
|
||||
- World/environment/combat: matching `js/world_*`, `js/simulation_*`, `js/system_order.js`.
|
||||
- Fire/burning behavior: `js/fire_runtime_system.js`, `js/item_dynamic_tool_system.js`, relevant item definitions, `js/world_combat_effects.js`, and nearby environment helpers.
|
||||
- Performance/crowding: `js/perf_profiler.js`, `js/item_update_scheduler.js`, `js/tarinai_update_policy.js`, `js/constraint_system.js`, `js/world_spatial_budget.js`, and render/detail code.
|
||||
- Save/load: `js/save_schema.js`, `js/save_codec.js`, `js/save_system.js`, `js/snapshot_system.js`, `js/restore_coordinator.js`.
|
||||
- Family tree: `js/world_family_social.js`, `js/family_graph.js`, `js/ui_family_*.js`.
|
||||
- Static app/cache/version: `app_manifest.json`, `scripts/generate_app_files.py`, `service-worker.js`, `js/version.js`, `index.html`.
|
||||
|
||||
## Invariants
|
||||
|
||||
- Preserve `index.html` script order unless all dependent manifests/generated files are updated together.
|
||||
- Keep `app_manifest.json`, `js/version.js`, `service-worker.js`, and cache query versions synchronized.
|
||||
- Keep intentional global exports for compatibility with regression checks and browser runtime; remove public facades only after repo-wide caller checks.
|
||||
- Prefer focused edits in the owning subsystem; split modules are intentional compatibility boundaries.
|
||||
- Avoid reintroducing `simulationQualityTier` branches unless a real simulation-quality setting is restored; current quality controls are visual/detail controls.
|
||||
- `High` display quality may include quality-preserving optimizations, but should not reduce simulation cadence, object limits, or visible fidelity in a noticeable way.
|
||||
- Save format compatibility with old text saves is not required when intentionally shortening/replacing save hash text; keep current save/load internally consistent instead.
|
||||
- Run `node --check js/*.js` and `python scripts/regression_check.py` for behavior changes.
|
||||
|
||||
## Current Maintenance Notes
|
||||
|
||||
This section records current boundaries only. Version-by-version cleanup history and completed work logs are intentionally omitted.
|
||||
|
||||
### Live Compatibility Surfaces
|
||||
|
||||
- Ordered global-IIFE loading is still part of the app contract; check repo-wide callers before removing bare globals or facade objects.
|
||||
- Keep public event names with active listeners: `audio:play`, `selection:changed`, `tool:selected`, `world:phase`, `log:entry`, and `tool:placed`. Telemetry-only emit calls should stay removed.
|
||||
- Save compatibility with old exported text is not required, but the current save/load round-trip must remain internally consistent.
|
||||
- Tool icon ownership is `TOOL_DEFINITIONS.icon`; UI code converts asset paths for CSS variables. Avoid restoring CSS-local `url(...)` definitions.
|
||||
- Shared geometry/display/domain helpers should stay in their owning helper modules unless a dedicated namespace migration is being done.
|
||||
|
||||
### Open Architecture Candidates
|
||||
|
||||
- Full namespace migration for behavior/action/needs/tool helpers. This requires rewriting many bare global call sites and updating regression contracts.
|
||||
- Effect-specific spatial indexing, fuller delta-history storage, and high-speed collision partitioning. These need new indexes or semantics beyond cleanup.
|
||||
- UI/text-side meaning consolidation, including labels, wrapping/measurement helpers, and duplicated scalar helpers such as `num`, `finiteOr`, `isObject`, and `clonePlain`.
|
||||
- Further `World.prototype` reduction is possible, but only for helpers proven not to be runtime-facing canvas/UI/tool APIs.
|
||||
|
||||
## Current Feature Routing
|
||||
|
||||
- Achievement system: `js/achievements.js`, `achievement_api.php`, `js/save_schema.js`, `js/save_codec.js`, `js/snapshot_system.js`, and achievement audit scripts. Current save schema is `49`; current app/cache version is `39.16.78`.
|
||||
- Achievement UI: `index.html` achievement dialog, `css/components.css` achievement rows/toast styles, and `js/achievements.js` DOM rendering.
|
||||
- Achievement spell payload: `TarinaiAchievements.exportSpellState()`, `snapshot.g.s`, `save_system.js` spell export inclusion, and `save_codec.js` achievement metadata readers/writers.
|
||||
- Logic board: `js/circuit_board_system.js`, `js/signal_system.js`, signal-related item definitions/metadata, and the generated circuit editor markup/styles.
|
||||
- Shared play: `js/seesaw_system.js`, tarinai action/need modules, placement preview, and item/structure definitions.
|
||||
- Reset presets: `js/world_reset_presets.js`; UI reset selection is in `js/ui_layout_dialogs.js`; social archive reset/fallback routing touches `js/world_family_social.js`.
|
||||
- Physics tool icons and previews: `js/ui_tools.js`, `js/render.js`, `js/placement_preview_system.js`, `js/physics_shape_editor_system.js`, and `js/item_tool_metadata.js`.
|
||||
|
||||
## Cleanup Candidate Audit - resolved
|
||||
|
||||
The previous cleanup candidate queue has been applied or adjusted. Current state:
|
||||
|
||||
- Version/docs routing was aligned to `39.16.78`; `js/version.js` build identity now matches the app version, and `README.md` no longer points at an older cache-query script URL.
|
||||
- One-version and overlapping audit scripts from the candidate list were removed; the broad audit suite remains `scripts/achievement_audit.js`, `scripts/playstyle_achievement_audit.js`, `scripts/achievement_integration_audit.py`, `scripts/achievement_server_state_audit.py`, and `scripts/regression_check.py`.
|
||||
- Shared scalar/object/clone/hash/text helpers now live on `TarinaiCoreHelpers` in `js/deterministic_helpers.js`; action, behavior, structure, snapshot, save, geometry, physics, seesaw, placement-preview, circuit, and signal code route repeated helper logic through that shared surface or through `TarinaiGeometry`.
|
||||
- UI HTML escaping is owned by `js/ui_helpers.js`; consumers bind explicitly to `TarinaiUIHelpers.htmlEscape`, and `index.html` loads `ui_helpers.js` before the consumers that need it.
|
||||
- `js/achievements.js` uses `renderIfDialogOpen()` for progress refreshes instead of repeating the hidden-dialog check at each event recorder.
|
||||
- `js/world_placement_log.js` uses shared close/backdrop binding for editor dialogs; deeper editor extraction can still be done as feature work, but the repeated close process from the cleanup list is removed.
|
||||
- `FILES.json` now includes JavaScript audit scripts and newer core bootstrap files (`domain_ids`, deterministic helpers, item registries/definitions) in machine routing.
|
||||
262
BUILD_NOTE.txt
|
|
@ -1,262 +0,0 @@
|
|||
tarinai_compact_save_stage4
|
||||
- Save schema 33 omits standard item amounts and stores only an optional non-standard block base plus per-item differences.
|
||||
- Relationship targets are sorted and delta-encoded within each Tarinai row.
|
||||
- World seeds use a canonical 64-bit hexadecimal identity and occupy exactly 8 bytes in the binary save.
|
||||
- The binary header contains one schema byte; its high bit signals whether a string table follows. Empty string tables and the duplicate snapshot version are omitted.
|
||||
- Deflate state is folded into the low bit of the frame-length varuint, eliminating the separate compression byte.
|
||||
- 15-bit text conversion uses arithmetic Unicode range mapping instead of a 32768-character string and reverse Map.
|
||||
- Binary schema 51 is the current save format; schemas 49 and 50 remain readable. Achievement payload v6 stores 72 unlock bits and persistent statistician progress while v5 remains importable.
|
||||
- Version: 39.16.94
|
||||
- CompressionStream and DecompressionStream are drained concurrently to avoid browser backpressure stalls.
|
||||
|
||||
- Load confirmation uses the in-game dialog; fan wind is realtime; zunchi tool preview opacity, carried plushie depth, and blanket grass rules were corrected.
|
||||
|
||||
- Passive spring: removed time-driven oscillation; force now depends on extension/compression and relative endpoint velocity.
|
||||
- Signal effects use the exported TarinaiEffect constructor.
|
||||
- Pressure-switch placement/editor overlays display the configured detection radius.
|
||||
|
||||
- v39.16.18: removed redundant physics normalization, dirty-marking, compaction, pickup, and collision bookkeeping; unified one-way-fence and bounce corrections while retaining debug diagnostics and defensive recovery paths.
|
||||
|
||||
- Earlier mechanism pass: stronger passive spring, persistent detector ranges, robot-cleaner signal power, and electric shock effect.
|
||||
|
||||
- v39.16.17: fixed the undefined bodyFor reference in mechanical one-way-fence contact handling by using the canonical body velocity helper.
|
||||
|
||||
- v39.16.16: link rendering no longer depends on midpoint spatial visibility; bounce fences launch Tarinai without shock damage; passive springs are more under-damped and elastic.
|
||||
|
||||
- v39.16.16: population chart includes per-bucket births/deaths; rotators accept signed motor speed; champion personality bonuses inherit through descendants; champion-pair mate weighting is moderately reduced.
|
||||
- v39.16.16: colony population chart records births/deaths, rotators accept negative motor speed, champion personality boosts inherit through descendants, champion-pair mating is slightly reduced, and one-way fence previews/pathfinding honor the passable direction.
|
||||
|
||||
- v39.16.16: one-way fences allow Tarinai forward crossing in swept collision; chart legend dots toggle series; wire tools attach to Tarinai/ants; energized living endpoints panic and take damage; selected data reports electrocution.
|
||||
|
||||
- v39.16.19: added direct food/medicine giving by placement or pinched drop, with a give overlay, stronger food-gift personality change, and heart bursts; removed unused collision reasons and duplicate constraint projection dirty checks.
|
||||
|
||||
- v39.16.20: localized the direct-feeding personality reason, normalized electrocution deaths to the Japanese electrocution label, added detector-controlled gate fences, made chart legend labels fully clickable, and escaped non-ASCII runtime source.
|
||||
|
||||
- v39.16.21: hid pinch overlays for rope/rod/spring/wire connection items and added detector time schedules with a dual-thumb range control, overnight ranges, and persistent copy/save state.
|
||||
|
||||
- v39.16.22: shortened detector copy, removed the time-range explanatory hint, cascaded connection-item removal when any endpoint disappears, and repaired detached plushies so they return to a living owner or disappear with a missing owner.
|
||||
|
||||
- v39.16.23: excluded transient splat effects from all connection endpoint selection and renamed the user-facing feeding need from the formal feeding term to the appetite label.
|
||||
- v39.16.25: added toilet-seeking before normal defecation; renamed rain shelter to parasol; added sunny +3.5C and unsheltered rain -3.5C felt-temperature corrections; parasols neutralize both and block sunbathing. Pet food, first aid, and sedative remain included.
|
||||
|
||||
- v39.16.29: replaced toilet-seeking with walkable toilet sand that immediately removes poop spawned on its surface; added a parasol ground shadow and explicit hot-sun/cold-rain temperature-seeking targets; fixed null candidate temperatures being interpreted as 0C.
|
||||
|
||||
- v39.16.29: aligned parasol placement overlay with its shade, made toilet sand absorb rain water, expanded Happy/Athletic/War presets, simplified Athletic rotators, and reserved their full rotation envelope from fences.
|
||||
|
||||
- v39.16.30: added a few bounce fences to War, reduced ordinary-fence restitution, restored ball collisions for visible sleeping Tarinai, moved nest-box tooltips closer, allowed movable-item placement overlap, and fixed delete-tool target lookup.
|
||||
|
||||
- v39.16.31: unified in-world tooltip spacing, added the Hair Trigger preset, and replaced physics-category line-like tool icons with distinct semantic silhouettes.
|
||||
|
||||
- v39.16.35: moved snap-mode switching to a dedicated operation control with Free/20px/40px/60px cycling, removed F/G from size badges, allowed valid placement against the field top edge, and standardized preset descriptions with Japanese full stops.
|
||||
|
||||
- v39.16.37: unified biological/environmental conditions into the detector, removed the standalone sensor and relay types with no save migration, folded temperature/limits, moved secondary controls into the header menu, preserved tool mode while browsing tarinai data, and added five-second mobile selection tips.
|
||||
|
||||
- v39.16.38: detector numeric sliders match the time-control presentation; colony settings are unified; display settings are inline; meter colors, snap control, range-delete icon, and ant chart defaults are revised.
|
||||
|
||||
- v39.16.39: restored the compact sticky operation palette, added exact-match panel search, reorganized colony settings/stats, and refreshed range-delete and mechanism icons.
|
||||
- v39.16.40: moved partial panel search to the header with previous/next navigation and tooltip text indexing; localized snap mode labels; simplified bounce-fence and reciprocator icons; matched expanded colony settings to the chart background.
|
||||
|
||||
39.16.41: Added local achievements and optional self-hosted shared unlock percentage API.
|
||||
|
||||
39.16.42: Expanded achievements to ten conditions, added local/shared reset, rapid-event windows, generation streaks, and special collision tracking.
|
||||
|
||||
39.16.44: Expanded achievements to fifteen conditions, changed rapid deaths to 50 in 10 seconds, moved conditions into game tooltips, added toast-to-button absorption, and added colony disease/happiness, cumulative hand feeding, ritual ignition, and laxative-starvation achievements.
|
||||
|
||||
39.16.44: Audited all achievements; fixed impossible slave generation tracking, persisted generation runs, queued simultaneous unlock toasts, hardened launched-zunchi stop detection, and preserved laxative-at-death timing.
|
||||
|
||||
39.16.45: Added seven play-style achievements for passive observation, fifty world placements, detector automation, rope/rod/spring coverage, a five-minute death-free 25-Tarinai colony, 25 live ants, and 30 cumulative ant kills; persisted world-scoped timers and placement progress.
|
||||
|
||||
39.16.46: Revised achievement naming and disclosure UI, replaced achievement tooltips with inline post-unlock descriptions and horizontal shared-rate bars, merged detector automation plus link coverage into 機械化産業, changed placement and ant-kill goals to persistent totals of 100, and made direct-feeding progress reset with the field.
|
||||
|
||||
39.16.47: Added 大いなる母 and 喧嘩両成敗, renamed five achievements, revised five revealed descriptions, and added a debug-only all-achievements button.
|
||||
|
||||
39.16.48: Corrected the zunchi-slave fight-record threshold text to 30%; fixed wire placement totals, danger-kill attribution for 喧嘩両成敗, partial legacy link migration, strict player-ID validation, post-unlock storage writes, and debug-only unlock isolation from shared statistics.
|
||||
|
||||
39.16.49: Added additive client-to-server full-state synchronization for automatic aggregate recovery after server-side achievement data loss.
|
||||
|
||||
39.16.51: Expanded achievements from 27 to 43 with history, cleanup, care, ecology, temperature, weather, medicine, combat, and ant-experiment conditions; persisted the required reset-scoped metadata in a new incompatible save schema; made nest-box and pipe occupants weather-protected; and added false-positive guards for time-zero markers and stale ant damage attribution.
|
||||
|
||||
39.16.52: Reorganized achievements into four collapsible categories, removed achievement tooltips, surfaced pre-unlock disclosures in the achievement UI, and replaced native browser confirmations with a shared in-game dialog.
|
||||
|
||||
|
||||
39.16.53: Renamed the four achievement categories to 生態/実験/建築/その他, moved unlocked achievements to the top of their own categories, disclosed four additional conditions before unlock, raised 大いなる母 to 3333 births, and moved achievement unlock toasts to the upper-left.
|
||||
|
||||
39.16.54: Added 真・たりない観察, 悠久の歴史, 五体満足, and 零体満足; disclosed 悠久の歴史 before unlock; reduced achievement overhead by skipping closed-dialog list rebuilds, caching category mappings, debouncing pending sync, and retaining periodic full recovery sync.
|
||||
|
||||
39.16.55: Added 町のお医者さん and 割とふわふわ; changed ベビーブーム to 30 births per minute and 管理された楽園 to one uninterrupted happy in-game day from play day 6; repaired shared つつく targeting, damage, plushie fling, and hover overlay; corrected manual rotator/reciprocator push direction; tightened tool/limit typography; reduced periodic achievement scans; and changed server aggregate writes to compact v2 JSON with legacy-state migration.
|
||||
39.16.56: Added 混沌ヲ希求スル者 and 潔癖症; made fight progress persistent; revised ミニマリスト, おくすり手帳全埋め, and 清掃業者; and made achievement-rate bars visible on unlocked cards.
|
||||
|
||||
39.16.57: Compactly embeds unlocked achievements and persistent achievement progress in exported spell saves; spell loads merge achievements without erasing local progress and resync restored unlocks to the server.
|
||||
|
||||
39.16.59: Replaced JSON-string achievement metadata in spell/save binaries with a sparse binary schema; achievement unlock masks are raw bits, progress is bit-packed, timestamps use minute deltas, and old v39.16.57 spells remain readable.
|
||||
39.16.60: Added persistent sniper, megalopolis, and love-mochi fertility achievements; changed chaos progress to fight-mochi-influenced fights only; extended compact spell progress without migrating obsolete generic fight counts; omitted completed achievement counters from spell metadata; and tightened population-limit labels/layout.
|
||||
|
||||
39.16.62: Expanded achievements from 54 to 59 with single-digit FPS, 24-hour continuous play, five toilet-sand placements, park-ground editing, and four ground changes per second; revised safety, observation, and empty-click conditions; moved the two observation achievements to the bottom of その他; and expanded binary achievement masks to eight bytes while retaining schemas 41–43.
|
||||
|
||||
39.16.63: Fixed achievement progress typography and tab-switch observation continuity; added アリ観察 and 命のバトン; persisted sticky-bomb pass counts; aligned sauna/enemy-ant conditions; removed obsolete spell compatibility; current exports use schema 45 and achievement payload v4.
|
||||
|
||||
## v39.16.64
|
||||
- Added 毎日たりない観察 and 抵抗器じゃない.
|
||||
- Daily streak state is carried by achievement payload v5 and binary schema 46.
|
||||
- Achievement aggregate JSON migrates compact v2/verbose input to compact v3 automatically under the API file lock.
|
||||
|
||||
## v39.16.65
|
||||
- Re-audited the not-yet-deployed shared achievement server with generated randomized unlock data instead of a fixed production sample.
|
||||
- Shared summaries are read-only; only POST session/unlock/sync/reset calls register or update players, and GET mutations are rejected.
|
||||
- State writes now use a separate lock file plus atomic same-directory replacement, preserving the previous state across interrupted writes.
|
||||
- Invalid or structurally unknown JSON returns `storage_corrupt` without overwriting the source file.
|
||||
- `completionistEligible: false` now prevents `true_tarinai_observer` from being re-added by the same sync payload.
|
||||
- The randomized server audit covers 32 v2/verbose migrations and 72 concurrent player syncs with exact semantic comparison.
|
||||
|
||||
## v39.16.66
|
||||
- Added seesaw cooperative play and friendship growth.
|
||||
- Added circuit-board editor with positioned input/output terminals and AND/OR/NOT gates.
|
||||
- Added electrical control for fan, stove, dry ice, and duplicator.
|
||||
- Added connectable-item and circuit-terminal highlights while wiring.
|
||||
- Save snapshot/schema: 38/47.
|
||||
|
||||
## v39.16.67
|
||||
- Circuit board editor now uses a grid canvas with direct gate placement and cursor-drawn wiring.
|
||||
- Legacy circuit layouts are intentionally not converted.
|
||||
- Colony state evaluates every two in-game hours; growing populations cannot be classified as Weakened.
|
||||
- Seesaw action selection, fixed orientation, and placement preview were corrected.
|
||||
|
||||
## v39.16.68
|
||||
- Seesaw leisure can start at a lower boredom threshold, and a waiting rider raises the second rider's action priority.
|
||||
- Active paired play is shown as 「シーソーをしている。」 in selected Tarinai data.
|
||||
- Seesaws render in the low/back item layer instead of covering Tarinai sprites.
|
||||
- AND/OR gate input pins and gate outputs are snapped to the same 40px circuit-editor grid used by cursor wiring.
|
||||
|
||||
## v39.16.69
|
||||
- Circuit-board internal wiring now uses straight grid segments with explicit branch junctions and unlimited fan-out per terminal.
|
||||
- Removed OR and added a one-input/one-output rectifier; internal logic parts rotate in 90-degree steps.
|
||||
- Circuit-board external terminal counts and positions are editable; the board footprint and default terminal spacing were expanded.
|
||||
- Detector numeric conditions now use a dual-thumb minimum/maximum range plus direct numeric inputs.
|
||||
- Seesaw riders render slightly above the seat surface.
|
||||
- Save snapshot/schema: 39/48. Older saves are intentionally unsupported.
|
||||
|
||||
|
||||
## v39.16.70
|
||||
- The default circuit-board grid is now 10x6, one quarter of the previous editor area, with a correspondingly smaller world footprint.
|
||||
- Circuit-board width and height are editable; the board world size follows the grid dimensions.
|
||||
- Input/output terminal count is limited by board height, and every external terminal can be dragged independently onto the left, right, top, or bottom edge.
|
||||
- Branch junctions can be moved directly; all connected straight segments follow the junction.
|
||||
- NOT and rectifier bodies are visually smaller while their pins remain on grid intersections.
|
||||
- Existing external wires follow terminal positions after a board resize or terminal move.
|
||||
- Seesaw action reasons now use play-specific wording instead of inheriting unrelated fulfillment reasons such as bed demand.
|
||||
- Save snapshot/schema: 40/49. Older saves are intentionally unsupported.
|
||||
|
||||
|
||||
## v39.16.71
|
||||
- Circuit-board external terminals are now placed directly with IN/OUT terminal tools; the +/- terminal-count controls were removed.
|
||||
- Disconnected external terminals are dimmed in the circuit-board editor; connected terminals retain their signal colors and cannot be deleted until their world wire is removed.
|
||||
- Placement previews no longer use the generic ellipse fallback: circle items use circles, obstacle/mechanical items use their collision footprints, and special items use dedicated shapes.
|
||||
- Detector, wire, insulated wire, and circuit board moved from the Physics palette section to a new Electrical section.
|
||||
- Clicking a detector dual-range track outside either thumb teleports the nearer thumb to the clicked value.
|
||||
- Save snapshot/schema remains 40/49; the persisted circuit-board v4 structure is unchanged.
|
||||
|
||||
|
||||
## v39.16.72
|
||||
- Circuit-board default grid is 10x10 and both dimensions can grow to 20x20; the world footprint scales proportionally in both axes.
|
||||
- AND gate body height is two grid cells. Compact one-input parts can be placed one grid row from the top/bottom edges when their rotated pins remain in bounds.
|
||||
- Placement previews prefer shared occupancy/collision footprints; the dedicated seesaw preview was removed and circuit boards use their rectangular board footprint.
|
||||
- Internal combinational feedback loops are explicitly detected and forced OFF; a NOT self-loop is unstable and does not provide power.
|
||||
- Save snapshot/schema remains 40/49; the persisted circuit-board v4 structure is unchanged.
|
||||
|
||||
|
||||
## v39.16.73
|
||||
- Circuit-board editing shows placement ghosts for pending internal parts and terminals.
|
||||
- Q/E rotates pending internal parts by 90 degrees before placement.
|
||||
- The ON constant source is documented in the editor.
|
||||
- Wire and insulated-wire palette silhouettes render correctly in the Electrical category.
|
||||
- Save snapshot/schema remains 40/49.
|
||||
|
||||
|
||||
## v39.16.76
|
||||
- Removed the circuit-board OFF constant source.
|
||||
- Added the achievement 「情報化産業」 for transmitting an active signal directly from one circuit board output to another circuit board input through wire or insulated wire.
|
||||
- Extended the 8-byte achievement mask to use its final bit, supporting all 64 current achievements.
|
||||
- Save snapshot/schema remains 40/49.
|
||||
|
||||
|
||||
## v39.16.78
|
||||
- Fixed 「あぁ^~ たまらねぇぜ」: burst zunchi no longer treat their spawn overlap with the owner as a collision.
|
||||
- A burst zunchi now becomes eligible only after it has cleared the owner once; a later fatal return collision unlocks the achievement, including routes that reflect and accelerate on a bounce fence.
|
||||
- Preserved the existing nonfatal, stopped, and player-held disqualification behavior.
|
||||
- Fixed ミニマリスト placement-origin metadata so it remains saveable after 無計画都市 has already unlocked.
|
||||
- Fixed サウナ水風呂 to measure 15 seconds from genuine entry into a hot state instead of refreshing the hot timestamp on every hot evaluation.
|
||||
- Fixed 抵抗器じゃない so a Tarinai killed by the qualifying shock still counts before death clears its live state.
|
||||
- Achievement reset now clears reset-scoped hidden per-Tarinai and per-item progress, including care counts, sauna state, held duration, fight-mochi metadata, sticky-bomb pass count, and quick-delete timestamps.
|
||||
- Undo/Redo and valid ground changes now reset 監視カメラ observation progress as world interventions.
|
||||
- Added route-level and secondary regression coverage for these achievement paths. Ambiguous achievement semantics identified by the renewed audit remain unchanged pending explicit specification.
|
||||
|
||||
39.16.78: Applied confirmed achievement semantics: birth-status slave/king appearances count; dynasty achievements require direct parent-child qualifying lineage; Overprotective treatment counts only actual beneficial health recovery; Unplanned City counts deletion operations rather than removed objects; Surveillance Camera requires 3 uninterrupted game days and resets on gameplay interventions; wire-shock partial progress remains nonpersistent; Pest Control counts only player-caused ant deaths.
|
||||
|
||||
|
||||
## v39.16.80
|
||||
- Optimized Tarinai social-neighbor scans with squared-distance filtering, nearest-N partial selection, pair-deduplicated symmetric work, cached personality profiles, and shared disease spread logic.
|
||||
- Added personality-derived crowd comfort bounds. Overcrowded Tarinai flee toward less crowded space with the specified behavior text; undercrowded Tarinai become stressed and approach the nearest living Tarinai with the specified loneliness text.
|
||||
|
||||
|
||||
## v39.16.85
|
||||
- Replaced Undo/Redo object snapshots and recursive patch history with the existing synchronous binary save codec payload.
|
||||
- Exposed binary snapshot encode/decode helpers from TarinaiSaveCodec for internal history reuse without Base32768 text conversion or asynchronous compression.
|
||||
- Replaced JSON.stringify-based duplicate detection with a compact dual 32-bit fingerprint over the encoded history bytes.
|
||||
- Unified Undo and Redo stack movement through a shared history transition path while preserving synchronous behavior and the 48-entry limit.
|
||||
- Added binary-history regression coverage for storage shape, duplicate suppression, Undo restoration, and Redo restoration.
|
||||
|
||||
|
||||
## v39.16.85
|
||||
- Render stack optimization: static visible stacks are cached by static spatial generation and camera region, while dynamic stacks reuse previous-frame order before insertion sorting and are merged linearly.
|
||||
- Link rendering now uses dedicated AABB spatial cells instead of per-frame full type-bucket fallback scans.
|
||||
- Carried plushies render at owner-relative positions without mutating simulation coordinates during rendering.
|
||||
- Fixed render culling radii are cached on eligible static entities.
|
||||
- Render-layer sort keys are computed once per stack insertion.
|
||||
|
||||
|
||||
## v39.16.85
|
||||
- Reduced fixed render cost by skipping the fallback full-screen gradient whenever the transformed field fully covers the viewport.
|
||||
- Cached full-screen fallback gradients and rebuilt atmospheric full-screen lighting at 8 Hz instead of constructing multiple full-screen gradients every frame.
|
||||
- Reused dynamic render-layer entries, sort maps, and previous-order ID arrays to reduce per-frame garbage collection as population grows.
|
||||
|
||||
|
||||
## v39.16.86
|
||||
- Removed social crowd-retreat selection and the overcrowded colony-state distance scan.
|
||||
- Removed the unconditional three-second full terrain-cache invalidation; cached terrain now refreshes only from real terrain changes, with splat fading invalidating local chunks.
|
||||
- Throttled structure dependency safety sweeps, staggered item/ant compaction, and stopped timer-only item bucket rebuilds.
|
||||
- Deferred periodic statistics UI refreshes and collapsed population statistics into a single pass.
|
||||
- Ordinary item additions/removals no longer invalidate the full terrain cache unless cached terrain items actually changed.
|
||||
|
||||
## v39.16.88
|
||||
- Large-save restore uses an adaptive non-damaging settling phase for dense loaded populations, preventing synthetic body-overlap and obstacle-correction impulses from cascading into impact deaths.
|
||||
- Duplicator loading takes priority over direct feeding when a Tarinai overlaps the machine.
|
||||
- Save schema 50 explicitly stores elapsed completed days; total simulation time is restored monotonically and seasons/climate derive from elapsed days. Schema 49 spells remain readable.
|
||||
|
||||
|
||||
## v39.16.89
|
||||
- Moved the ずんちどれい and たりない王 population-chart toggles to immediately after 死亡数.
|
||||
- Removed the 12-second periodic Tarinai runtime-cache full sweep that caused large-population performance regressions.
|
||||
- Runtime relationship/cache cleanup is now scheduled only after accumulated deaths reach max(20, floor(living population * 15%)), then runs once during the next Tarinai compaction and resets its counter.
|
||||
|
||||
|
||||
## v39.16.90
|
||||
- Added eight playstyle-survey achievements: 数こそ力, 少数精鋭, 踩屎感, 快適な寝床, 石に枕す, 季節を越えて, 推し, and 統計学者.
|
||||
- 数こそ力 unlocks at 100 living Tarinai. 少数精鋭 requires 1-25 living Tarinai continuously for five game days and discloses its condition/progress before unlock.
|
||||
- 統計学者 counts 30 statistics-tab or chart-series visibility button presses in persistent achievement state, so field resets do not clear its progress.
|
||||
- Achievement spell payload v6 and binary save schema 51 expand the unlock mask from 64 to 72 achievements while retaining v5/schema 49-50 read compatibility.
|
||||
- Updated the Item lifecycle pipeline regression to validate local bucket/spatial invalidation when passive serving-food decay actually changes its visual footprint.
|
||||
|
||||
|
||||
## v39.16.94
|
||||
- Removed previous-order render Maps and custom insertion sorting; dynamic visible lists now use native Array.sort after culling.
|
||||
- Removed the shared route cache while retaining actor-local route throttling.
|
||||
- Disabled detailed performance counters in normal play; diagnostics remain available only in explicit debug/perf mode.
|
||||
- Isolated obstacle-query cache invalidation to routing-obstacle generations instead of generic spatial movement.
|
||||
- Replaced per-frame full Tarinai spatial-grid rebuilds with cell-crossing incremental updates.
|
||||
- Added sustained hysteresis to automatic performance-profile transitions to prevent rapid quality oscillation.
|
||||
- Death-reference cleanup now executes in bounded per-frame chunks after the existing adaptive death threshold.
|
||||
|
||||
## v39.16.94
|
||||
- Fixed dense-population birth regression caused by incremental Tarinai spatial-grid bucket ordering interacting with an early 20-candidate contact cutoff. Contact interactions now consider the complete already-fetched local candidate set while retaining only the nearest bounded interaction list.
|
||||
- Moved the "季節を越えて" achievement from the ecology category to "その他".
|
||||
39.16.96: UI input/limits/zoom refinements, infection chance reduced to one-third, 75 achievements, grass spatial/hot-path optimization, summer flat temperature bonus removed.
|
||||
191
FILES.json
|
|
@ -1,191 +0,0 @@
|
|||
{
|
||||
"purpose": "Token-efficient machine-readable routing for tarinai_. Use scripts/ai_inventory.py for exact current file expansion.",
|
||||
"version": "39.16.98",
|
||||
"entrypoints": {
|
||||
"app": "index.html",
|
||||
"runtime": "js/main.js",
|
||||
"update_order": "js/system_order.js",
|
||||
"manifest": "app_manifest.json",
|
||||
"service_worker": "service-worker.js"
|
||||
},
|
||||
"read_strategy": {
|
||||
"default": [
|
||||
"README.md"
|
||||
],
|
||||
"architecture": [
|
||||
"README.md",
|
||||
"AI_MAP.md"
|
||||
],
|
||||
"exact_inventory": [
|
||||
"powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All",
|
||||
"python scripts/ai_inventory.py --all"
|
||||
],
|
||||
"machine_routing": [
|
||||
"FILES.json"
|
||||
]
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"id": "root",
|
||||
"patterns": [
|
||||
".gitignore",
|
||||
".htaccess",
|
||||
"app_manifest.json",
|
||||
"favicon.ico",
|
||||
"index.html",
|
||||
"README.md",
|
||||
"AI_MAP.md",
|
||||
"GAME_FEATURES.md",
|
||||
"FILES.json",
|
||||
"service-worker.js",
|
||||
"achievement_api.php",
|
||||
".achievement_data/.htaccess"
|
||||
],
|
||||
"meaning": "Repo/app shell metadata, server/static cache config, docs, manifest, entrypoint."
|
||||
},
|
||||
{
|
||||
"id": "css",
|
||||
"patterns": [
|
||||
"css/*.css"
|
||||
],
|
||||
"meaning": "Base, layout, panel, component, and mobile responsive styling."
|
||||
},
|
||||
{
|
||||
"id": "scripts",
|
||||
"patterns": [
|
||||
"scripts/*.js",
|
||||
"scripts/*.py",
|
||||
"scripts/*.ps1"
|
||||
],
|
||||
"meaning": "Generators, regression checks, version-specific audits, and token-aware AI inventory tooling."
|
||||
},
|
||||
{
|
||||
"id": "core",
|
||||
"patterns": [
|
||||
"js/version.js",
|
||||
"js/event_bus.js",
|
||||
"js/domain_ids.js",
|
||||
"js/registry_base.js",
|
||||
"js/disease_registry.js",
|
||||
"js/sound_pack.js",
|
||||
"js/deterministic_helpers.js",
|
||||
"js/item_tool_metadata.js",
|
||||
"js/item_tool_definitions.js",
|
||||
"js/item_visual_definitions.js",
|
||||
"js/item_food_definitions.js",
|
||||
"js/item_effect_definitions.js",
|
||||
"js/item_registry.js",
|
||||
"js/data.js",
|
||||
"js/ground_types.js",
|
||||
"js/health.js",
|
||||
"js/input_mode_manager.js",
|
||||
"js/main.js",
|
||||
"js/math.js",
|
||||
"js/perf_profiler.js",
|
||||
"js/debug_tools.js",
|
||||
"js/update_step_pipeline_runner.js",
|
||||
"js/burn_motion_util.js"
|
||||
],
|
||||
"meaning": "Bootstrap, shared constants/helpers, registries, config, profiling/debug."
|
||||
},
|
||||
{
|
||||
"id": "assets_runtime",
|
||||
"patterns": [
|
||||
"js/assets.js",
|
||||
"js/audio.js",
|
||||
"js/render.js",
|
||||
"js/tarinai_render.js",
|
||||
"js/item_render_runtime.js"
|
||||
],
|
||||
"meaning": "Image/audio loading and canvas rendering."
|
||||
},
|
||||
{
|
||||
"id": "simulation",
|
||||
"patterns": [
|
||||
"js/sim_core.js",
|
||||
"js/simulation*.js",
|
||||
"js/system_order.js"
|
||||
],
|
||||
"meaning": "Simulation primitives, concrete systems, phase ordering."
|
||||
},
|
||||
{
|
||||
"id": "world",
|
||||
"patterns": [
|
||||
"js/world*.js",
|
||||
"js/weather_system.js"
|
||||
],
|
||||
"meaning": "World state/view/update/tools/environment/combat/family/social/ants."
|
||||
},
|
||||
{
|
||||
"id": "physics",
|
||||
"patterns": [
|
||||
"js/physics*.js",
|
||||
"js/mechanical_system.js",
|
||||
"js/constraint_system.js",
|
||||
"js/collision_footprint_system.js"
|
||||
],
|
||||
"meaning": "Geometry, collisions, constraints, mechanical item behavior."
|
||||
},
|
||||
{
|
||||
"id": "items",
|
||||
"patterns": [
|
||||
"js/item*.js",
|
||||
"js/items.js",
|
||||
"js/structures.js",
|
||||
"js/structure_lifecycle.js",
|
||||
"js/circuit_board_system.js"
|
||||
],
|
||||
"meaning": "Item/structure registry, lifecycle, dynamic behavior, rendering."
|
||||
},
|
||||
{
|
||||
"id": "tarinai",
|
||||
"patterns": [
|
||||
"js/tarinai*.js",
|
||||
"js/ants.js",
|
||||
"js/seesaw_system.js"
|
||||
],
|
||||
"meaning": "Creature state/actions/needs/social life/item effects/disease/rendering plus ant actor model."
|
||||
},
|
||||
{
|
||||
"id": "save",
|
||||
"patterns": [
|
||||
"js/save*.js",
|
||||
"js/snapshot_system.js",
|
||||
"js/restore_coordinator.js",
|
||||
"js/history_system.js"
|
||||
],
|
||||
"meaning": "Snapshot schema/codec/storage/restore/history."
|
||||
},
|
||||
{
|
||||
"id": "ui",
|
||||
"patterns": [
|
||||
"js/ui*.js",
|
||||
"js/text_catalog.js",
|
||||
"js/command_dispatcher.js",
|
||||
"js/family_graph.js",
|
||||
"js/achievements.js"
|
||||
],
|
||||
"meaning": "DOM UI, input, tools, selected panel, logs, charts, family tree, text labels, command routing."
|
||||
},
|
||||
{
|
||||
"id": "assets",
|
||||
"patterns": [
|
||||
"assets/sprites/tarinai_*.webp",
|
||||
"assets/ui/tool_*",
|
||||
"assets/ui/favicon*",
|
||||
"assets/ui/apple-touch-icon.png",
|
||||
"assets/ui/ecology_*.webp",
|
||||
"assets/objects/*",
|
||||
"assets/sounds/*"
|
||||
],
|
||||
"meaning": "Creature sprites, UI/tool icons, field object images, voice/SFX samples."
|
||||
}
|
||||
],
|
||||
"conventions": {
|
||||
"*_pipeline.js": "ordered runner/facade",
|
||||
"*_step_*.js": "pipeline stage wrapper",
|
||||
"*_system.js": "domain service or compatibility facade",
|
||||
"*_runtime.js": "stable runtime-facing facade",
|
||||
"*_registry.js": "definition lookup/normalization"
|
||||
}
|
||||
}
|
||||
100
GAME_FEATURES.md
|
|
@ -1,100 +0,0 @@
|
|||
# tarinai_ Game Features
|
||||
|
||||
`tarinai_` is an observation sandbox about small creatures called tarinai. The player shapes a field with food, medicine, furniture, hazards, machines, weather, and ground choices, then watches how the colony reacts. The game is about cause and effect: small changes in the environment can alter movement, stress, disease, relationships, reproduction, accidents, cleanup, and survival.
|
||||
|
||||
## Core Play
|
||||
|
||||
The player starts by choosing a field and ground, then places objects or uses direct tools on the colony. Tarinai continue acting on their own while the player observes, edits the environment, removes problems, or creates new ones. The colony panel, event log, selected-tarinai data, and family tree help explain what is happening without taking control away from the simulation.
|
||||
|
||||
The game supports quiet observation and chaotic experiments. A player can build a comfortable habitat with beds, food, grass, and cleaning tools, or create a dangerous machine field with rotators, fences, stones, pins, explosions, ants, and poison.
|
||||
|
||||
## Tarinai
|
||||
|
||||
Tarinai are autonomous creatures with HP, needs, stress, personality, age, family links, diseases, item effects, and behavior states. They choose actions from their current condition and surroundings instead of following a fixed script. Hunger can make them seek grass or food, sleepiness can make them find a bed, fear can make them flee, and social state can make them follow, fight, avoid, or approach another tarinai.
|
||||
|
||||
Each tarinai has individual tendencies. Aggressive tarinai fight more easily, social tarinai seek others more often, nervous tarinai react strongly to danger, and inherited traits can make children resemble their parents. Selecting a tarinai shows health, needs, stress, effects, relations, inventory, family information, and the reason for its current behavior.
|
||||
|
||||
Tarinai can be damaged by hazards, pushed by physics objects, carried by ants, attached to links, blocked by fences, affected by medicine, frightened by accidents, comforted by habitat items, or preserved when outside the active field. Some objects can become emotionally important, so losing them may cause panic.
|
||||
|
||||
## Food And Consumables
|
||||
|
||||
Grass is both food and habitat material. Tarinai can eat it, feel comfort near it, gather it for simple beds, and interact with it through ground and weather systems. Water can be drunk, can appear from rain, and can be generated or copied by tools.
|
||||
|
||||
Sweet food helps satisfy hunger and can support recovery in some cases. Love mochi makes reproduction behavior more likely for a while, while fight mochi pushes the colony toward conflict. Zunda juice clears item effects, making it useful after medicine or special consumables have changed a tarinai too much.
|
||||
|
||||
Medicine changes the simulation more aggressively than normal food. Sleep drugs push tarinai toward sleep or sleep disease, laxatives create repeated zunchi and reduce food benefit, protein makes a stronger permanent mode with lower disease risk, and niteropu makes a weaker permanent mode with higher disease risk. Ammo creates temporary speed and impact danger. Mystery drugs choose from eligible effects unpredictably. Mercury changes lifespan, while giant and dwarf drugs reshape size, movement, power, and ongoing damage risk.
|
||||
|
||||
## Habitat And Cleanup
|
||||
|
||||
Zunchi is waste, but it is also part of the ecology. It affects hygiene and disease risk and can fertilize grass. Beds and nest boxes give tarinai safer or better sleep options, reduce stress around resting areas, and help recovery. Balls give tarinai something to play with and can also become part of physics experiments.
|
||||
|
||||
Robot cleaners are mobile cleanup items. They scan for selected target types, pursue matching waste or corpses, and remove them on contact. The water hose gives the player direct cleanup control by washing dirt, zunchi, corpses, stains, and similar field problems. Signboards are editable objects that can be inspected after placement, useful for labeling areas or experiments.
|
||||
|
||||
## Physics And Machines
|
||||
|
||||
Physics items turn the field into a mechanical sandbox. Ropes connect two targets with a flexible link that can go slack but should not stretch freely. Rods create a rigid connection. Springs connect targets with elastic motion, allowing linked objects to pull back and oscillate.
|
||||
|
||||
Fences block movement and can be rotated for layout control. Gates can be opened or closed after placement. One-way fences allow passage from one direction while blocking the opposite direction. Bounce fences launch colliding targets more strongly than normal fences, which makes them useful for traps or kinetic experiments.
|
||||
|
||||
Rotators are editable mechanical shapes that spin and push nearby targets. Their shape and speed can create conveyors, crushers, traps, or chaotic collision fields. Reciprocators move back and forth along a line, so they can shove objects repeatedly. Poison blocks are custom hazard shapes that damage on contact.
|
||||
|
||||
## Electrical And Automation
|
||||
|
||||
Detectors, wires, insulated wires, and circuit boards are grouped in the Electrical tool category. Normal energized wire can hurt living targets on contact, while insulated wire is safer for incidental contact. Detectors can sense target ranges or time windows and send signals; clicking either dual-range track moves the nearer thumb directly to the clicked value. Circuit-board external input and output terminals are placed directly on the board edge, can be moved around all four sides, and appear dim in the editor until a world wire is actually connected.
|
||||
|
||||
## Danger Tools
|
||||
|
||||
Danger tools create immediate or ongoing pressure. Stones are heavy objects that become dangerous when dropped, thrown, or moved by machines. Genkotsu creates a large impact on the ground. Firecrackers cause explosions, with flame variants adding fire while respecting active-fire limits.
|
||||
|
||||
Pushpins can roll, lodge in tarinai, cause panic, and deal continuous damage. Oshibyo blocks zunchi while lodged and releases stored zunchi when removed. Shooting is a direct attack tool that can hit tarinai, ants, items, pins, balloons, or placed objects depending on the target under the aim point.
|
||||
|
||||
Ant nests introduce an enemy system. Ants search for tarinai, carry captured targets back to the nest, and can be shot or killed. Worker ants and queen ants have separate HP values, and ant counts are shown through colony statistics.
|
||||
|
||||
## Operation Tools
|
||||
|
||||
The observe tool selects tarinai or compatible objects and opens their data. The add tool introduces new tarinai. Poke applies a small interaction or push, while pinch lets the player drag tarinai and some objects. Copy duplicates placed items with their settings so setups can be repeated quickly. The operation palette also provides a dedicated snap control shared by placement and pinch movement; it cycles through Free, 20px, 40px, and 60px.
|
||||
|
||||
Undo and redo reverse or reapply placement and edit actions. Delete removes a specific object or stain, while area delete removes many nearby targets at once. These tools let the player repair a field after experiments without resetting the whole colony.
|
||||
|
||||
## Grounds And Environment
|
||||
|
||||
Fields define the playable space and boundary style. Grounds change the character of the colony: sand, dirt, concrete, blanket, foot massage, ice, and laboratory-style surfaces can affect movement, stress, comfort, and grass behavior. Ground choice matters because tarinai react not only to objects, but also to the surface they live on.
|
||||
|
||||
Weather changes the field over time. Rain can create water and affect comfort, shade can protect from weather or sun, and temperature changes how tarinai experience the environment. Grass, zunchi, splats, corpses, ant corpses, rain drops, and stains all feed into hygiene, disease, cleanup, rendering, and observation.
|
||||
|
||||
## Colony Statistics
|
||||
|
||||
The colony panel summarizes population, sick tarinai, juveniles, elders, deaths, mood, temperature, population limits, and object limits. Graph tabs show population, needs, and environment. Total ants are included in the environment statistics so active ant pressure can be read alongside other field signals.
|
||||
|
||||
The event panel records important simulation events such as birth, death, fights, accidents, weather, grass changes, and relation changes. The family tree shows parent-child structure, generations, inherited stats, personality tags, and family links.
|
||||
|
||||
## Save, Display, And Performance
|
||||
|
||||
Manual save and restore use browser storage. Reset returns to field selection and starts a new state. Service worker caching can keep the static app shell available after the first successful load when the browser supports it.
|
||||
|
||||
Display settings control effects, shadows, drawing detail, and quality choices for crowded scenes. Crowded fields are an intended part of the sandbox, so the game uses spatial lookup, scheduling, cached drawing, and incremental maintenance systems to keep large colonies and many objects playable.
|
||||
|
||||
|
||||
## Achievement system (v39.16.78)
|
||||
- Sixty-four achievements track reproduction, natural status changes, generation runs, rapid population events, special deaths, colony conditions, individual care, autonomous ecology, history usage, cleanup, rapid deletion, temperature transitions, weather shelter, medicine-category completion, combat reversals, circuit-board signaling, and ant-related play styles.
|
||||
- Locked rows reveal only the configured condition allowlist; all others show `???`. There are no achievement tooltips. 大いなる母, めっちゃたりない観察, 敵の敵は味方, 混沌ヲ希求スル者, and 豊穣ヲ希求スル者 include local progress percentages, and unlocked achievements are sorted first inside each category.
|
||||
- Each achievement row behaves as a horizontal bar chart. Its background fill visualizes the optional global completion percentage from 0–100%, and the global achiever count is displayed beside it.
|
||||
- `物でいっぱい` requires 100 cumulative player placements across field resets. `害虫駆除` requires 100 cumulative ant kills across field resets.
|
||||
- `餌やりじいさん` requires 33 direct feedings in one field; resetting the field resets this progress.
|
||||
- `機械化産業` replaces the former detector-automation and link-craftsman achievements and unlocks only when both requirements are met.
|
||||
- v39.16.56 adds 混沌ヲ希求スル者 and 潔癖症, raises 清掃業者 to 200, starts ミニマリスト from day 6, excludes chain/crown from おくすり手帳全埋め, and keeps shared-rate bars visible after unlock.
|
||||
- v39.16.54 added 真・たりない観察, 悠久の歴史, 五体満足, and 零体満足. v39.16.55 added 町のお医者さん for 50 effective first-aid recoveries and 割とふわふわ for flinging a plushie with つつく. ベビーブーム now requires 30 births in one minute; 管理された楽園 starts tracking from play day 6 and requires one uninterrupted happy in-game day.
|
||||
- Local unlocks and persistent totals live in browser storage. Field-scoped direct-feeding, cleaner, self-sufficiency, enemy-ant, generation, observation, safety, and per-entity/item progress follow current-format saves where applicable. Nest-box and pipe occupants block rain/weather effects and count as sheltered for 雨宿り完了.
|
||||
- Achievement evaluation is primarily event-driven; periodic world scans skip completed or irrelevant conditions. The server writes compact v3 achievement aggregate JSON while migrating older aggregate formats on read.
|
||||
- v39.16.78 fixes achievement edge paths found by a renewed 64-condition audit: Minimalist placement metadata survives unrelated unlocks, Sauna/Cold-Plunge uses true hot-entry timing, lethal qualifying wire shocks count before death cleanup, reset clears hidden reset-scoped progress, and Undo/Redo/ground changes interrupt passive observation. Ambiguous semantics are intentionally unchanged pending specification.
|
||||
|
||||
### Achievement spell persistence (v39.16.78)
|
||||
- Exported spells compactly include non-debug achievement unlocks, unlock times, and persistent progress.
|
||||
- Loading a spell merges achievements and keeps the greater progress instead of erasing local achievement data.
|
||||
- Restored unlocks are queued for the existing server recovery synchronization. Save slots remain world-only.
|
||||
|
||||
- Spell achievement metadata uses a sparse binary encoding: raw unlock bits, bit-packed persistent progress, minute-delta unlock timestamps, and sparse per-entity state. Binary save schema 51 is current; schemas 49 and 50 remain readable. Achievement payload v6 stores 72 unlock bits and statistician progress, while legacy payload v5 remains importable.
|
||||
- v39.16.60: Added 狙撃手, メガロポリス, and 豊穣ヲ希求スル者; restricted 混沌ヲ希求スル者 to fight-mochi-influenced fights; extended compact achievement spell progress and omitted counters for already-unlocked achievements.
|
||||
- v39.16.62: Added five achievements for low FPS, uninterrupted 24-hour play, toilet-sand placement, park-ground editing, and rapid ground changes. 寿命 deaths are excluded from 安全第一 failures, empty-field clicks replace pause-button clicks for 何やってるの?, and the two long observation achievements are last in その他.
|
||||
|
||||
- v39.16.63: Added アリ観察 and 命のバトン; stabilized progress-row typography; continuous-play observation no longer resets on tab changes; corrected sauna wording and enemy-ant streak reset behavior; obsolete spell formats are rejected.
|
||||
140
README.md
|
|
@ -1,112 +1,60 @@
|
|||
# tarinai_
|
||||
# たりない観察 v39.18.6
|
||||
|
||||
AI-first entrypoint for a static browser simulation/game. Keep this file small; read deeper docs only when needed.
|
||||
## 実績システム
|
||||
|
||||
## Read Strategy
|
||||
実績定義は `js/achievement_catalog.js` を唯一のカタログとして管理します。表示名、説明、カテゴリ、未解除時の公開設定、同期可能な進捗条件、累積上限は、クライアントと `achievement_api.php` が同じカタログから読み取ります。
|
||||
|
||||
- Start here for architecture, script order constraints, and where to look next.
|
||||
- For game mechanics, UI, items, and tarinai behavior, read `GAME_FEATURES.md`.
|
||||
- For subsystem work, read `AI_MAP.md` plus only the matching files.
|
||||
- For exact file inventory, run `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All` or narrow it: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 ui`. Python equivalent: `python scripts/ai_inventory.py --all`.
|
||||
- For machine-readable routing, read `FILES.json`.
|
||||
- Avoid loading all assets into context; asset filenames are mostly self-describing and grouped by glob.
|
||||
累積進捗の正規データは端末・タブ別の増加台帳です。表示用 `progress` に同じ累積値を重複保存しません。旧形式の重複カウンターと `synced` 一覧は読込時だけ移行し、現行形式では保存しません。
|
||||
|
||||
## Runtime Shape
|
||||
「敵の敵は味方」は実績画面の「生態」カテゴリに掲載されます。
|
||||
|
||||
- v39.16.37 changes: biological and environmental conditions are unified into the detector; the standalone sensor and relay are removed; temperature and limits are folded; ecology, sound, display settings, and credits are in the header menu; reset remains outside in red; tarinai-data navigation preserves the active tool; mobile tool descriptions appear at the top for five seconds. Snap mode remains Free/20px/40px/60px, top-edge placement is fixed, and preset descriptions end with punctuation.
|
||||
- v39.16.38 changes: detector numeric controls use value-labelled sliders like the time control; colony temperature and limits share one Colony settings fold; display settings use an inline menu matching sound settings; HP/stress meter colors are fixed; the snap control follows Clear with a faint grid icon; area-delete has a new icon; the Ant chart series is renamed and hidden initially.
|
||||
- v39.16.55 changes: expanded the achievement set to 49 with 町のお医者さん and 割とふわふわ; changed ベビーブーム to 30 births per minute and 管理された楽園 to one happy in-game day from play day 6; restored shared poke targeting/highlights and plushie fling; corrected manual rotator/reciprocator push direction; tightened tool/limit UI; reduced periodic achievement scans; and introduced compact achievement aggregate storage.
|
||||
- v39.16.52 changes: reorganized achievements into collapsible categories, removed achievement tooltips, and moved confirmation flows into the game UI. Previous v39.16.51 changes: expanded the achievement set to 43 with sixteen play-style and special-action conditions; added persistent Undo/Redo and quick-delete counters, field-local cleaner/automation-free ecology progress, individual care/temperature/fight tracking, complete medicine-category collection, protected rain shelter in nest boxes and pipes, and stricter ant-kill attribution.
|
||||
- v39.16.49 changes: added additive full-state synchronization so shared achievement aggregates can be rebuilt as players return after server data loss.
|
||||
- v39.16.48 changes: corrected the zunchi-slave threshold text to 30%; repaired wire placement counting, danger-kill attribution, partial legacy link migration, UUID validation, and debug-only achievement isolation.
|
||||
- v39.16.47 changes: Two achievements are added (大いなる母 and 喧嘩両成敗); five names and five revealed descriptions are revised; `?debug=1` exposes an all-achievements button.
|
||||
- v39.16.46 changes: achievement names and presentation are revised; conditions are revealed only after unlock; shared completion rates render as horizontal background bars; automation and link coverage merge into 機械化産業; placement and ant-kill targets are persistent totals of 100; direct feeding resets with the field.
|
||||
- v39.16.45 changes: seven play-style achievements add passive observation, cumulative placement, detector automation, link-tool coverage, safe large-colony operation, ant population, and ant elimination measurements. World-scoped timers and placement counts persist in current-format saves.
|
||||
- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.16.78`.
|
||||
- v39.16.31 changes: all in-world tooltips use the closer offset; Hair Trigger adds a central bounce-fence ball enclosure with Tarinai and pet-food duplicators outside; physics tools use distinct icons.
|
||||
- v39.16.29 changes: parasol placement preview now uses the exact shade center and footprint; toilet sand absorbs rain-created water drops. Happy adds parasols, water balloons, and toilet sand beneath each duplicator; Athletic uses fence-safe single-bar rotators and water balloons; War adds rotating fans.
|
||||
- v39.16.19 changes: food and medicine can be given directly by placement or pinched drop, with a dedicated overlay, food heart bursts, and stronger personality effects; unused collision reasons and duplicate constraint projection dirty checks were removed.
|
||||
- Mutable runtime lives on `window`; primary entities are `World`, `Tarinai`, items, structures, ants, family graph, weather, and save snapshots.
|
||||
- Update loop: `main.js` creates the world/UI loop; `World.update` delegates to `TarinaiSystemOrder.update`; `system_order.js` runs named phases.
|
||||
- UI loop: pointer/tool/UI intents go through `command_dispatcher.js`; canvas drawing is in render modules; side panels are `ui_*.js`.
|
||||
- Generated/static app shell: `app_manifest.json`, `service-worker.js`, `FILES.json`, and `js/version.js` must stay version-aligned.
|
||||
## ローカル保存
|
||||
|
||||
## File Routing
|
||||
実績状態とサーバー資格情報は、通常保存、予備保存、同一タブ保存、IndexedDBへ世代付きで保存します。起動時は同じ世代の状態を追加方向に統合し、明示的な実績リセット時だけ世代を進めます。
|
||||
|
||||
- `index.html`: DOM shell, CSS/JS load order, panels/dialogs/canvas.
|
||||
- `css/*.css`: base/layout/panel/components/mobile styling.
|
||||
- `js/domain_ids.js`, `js/item_type_catalog.js`, `js/data.js`, `js/ground_types.js`: stable domain ids, current item-type save catalog, core config, field/ground definitions.
|
||||
- `js/world*.js`: world state, view, update phases, tools, placement, combat, environment, family/social, ants.
|
||||
- `js/tarinai*.js`: creature state, needs, actions, behavior, social life, item effects, update pipeline, rendering.
|
||||
- `js/item*.js`, `js/structures*.js`: item registry/runtime/lifecycle/dynamic behavior/rendering plus structures.
|
||||
- `js/simulation*.js`, `js/system_order.js`, `js/sim_core.js`: concrete update systems and phase ordering.
|
||||
- `js/ui*.js`, `js/text_catalog.js`, `js/command_dispatcher.js`: DOM UI, input, selected panel, logs, charts, family tree, tools, labels.
|
||||
- `js/save*.js`, `js/snapshot_system.js`, `js/restore_coordinator.js`, `js/history_system.js`: persistence and history.
|
||||
- `js/physics*.js`, `js/mechanical_system.js`, `js/constraint_system.js`, `js/collision_footprint_system.js`: geometry, mechanics, constraints, collisions.
|
||||
- `assets/sprites/tarinai_*.webp`: creature state sprites.
|
||||
- `assets/ui/tool_*`: tool palette icons; other `assets/ui/*` are app/help icons.
|
||||
- `assets/objects/*`: field object images.
|
||||
- `assets/sounds/*`: voice and SFX samples.
|
||||
- `scripts/*`: generators, regression checks, and AI inventory.
|
||||
v39.18.6は、v39.18.0以前の実績保存、旧ワールド実績メタデータ、実績スペルv9・v10、旧バイナリセーブを読み込めます。旧データは現行形式へ正規化され、新規保存には廃止済みフィールドを書きません。
|
||||
|
||||
## Conventions
|
||||
## サーバー同期
|
||||
|
||||
- `*_pipeline` = ordered runner/facade.
|
||||
- `*_step_*` = pipeline stage wrapper.
|
||||
- `*_system` = domain service or compatibility facade.
|
||||
- `*_runtime` = stable runtime-facing facade.
|
||||
- `*_registry` = definition lookup/normalization.
|
||||
- Many split files preserve regression contracts; do not collapse them casually.
|
||||
同期プロトコルはv5です。
|
||||
|
||||
## Checks
|
||||
- 解除状態と途中進捗を追加型で統合
|
||||
- 同じ端末台帳の再送を重複加算しない
|
||||
- リセット要求の再送を無害化
|
||||
- 古いサーバーバックアップから新しいローカル世代を復旧
|
||||
- 古い端末からリセット前の状態を復活させない
|
||||
- プレイヤーIDと所有秘密鍵を耐久保存から復旧
|
||||
|
||||
- Syntax: `node --check js/*.js`
|
||||
- Regression: `python scripts/regression_check.py`
|
||||
- Inventory sanity: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All`
|
||||
共有達成率は、サーバーがゲームシミュレーションを再実行した検証値ではなく、クライアント端末の報告値です。UIでは共有値を「達成率」「達成人数」と表示します。
|
||||
|
||||
## 旧サーバーデータの自動移行
|
||||
|
||||
データディレクトリ、またはアプリ直下の `.achievement_data` に旧コンパクト形式の `state.json`(スキーマ4)がある場合、最初のAPI要求時に現行形式へ自動移行します。環境変数で新保存先を指定した場合や、v39.18.1が空の現行保存先を先に作成済みの場合も検出します。
|
||||
|
||||
- 原本は `legacy-backup/` へSHA-256付きで保存
|
||||
- 旧プレイヤーIDから決定的なUUIDを生成
|
||||
- プレイヤー数、解除実績、解除時刻、実績別集計を移行
|
||||
- 旧データに存在しない途中進捗は0から開始
|
||||
- `legacy-v4-migration.json` に完了件数を記録
|
||||
- 中断時は次のAPI要求で冪等に再開
|
||||
- 切替中にコピー元の旧 `state.json` が更新された場合も、内容ハッシュを再確認して追加方向で再取込
|
||||
- ファイルサイズとmtimeが同じ更新もSHA-256比較で検出
|
||||
|
||||
旧ブラウザーに22文字のプレイヤーIDが残っている場合、クライアントはUUIDへ上書きする前に専用領域へ退避し、初回セッションで一度だけ新しい所有秘密鍵へ引き継ぎます。引継ぎ済みの旧IDを別の秘密鍵で再取得することはできません。
|
||||
|
||||
旧版から直接v39.18.6へ更新したブラウザーは自動引継ぎできます。v39.18.1を既に起動して旧22文字IDがブラウザーから失われている場合、サーバーデータだけから端末との本人対応を再構成することはできません。解除履歴と共有集計自体は正常に移行されます。
|
||||
|
||||
## 実績APIの保存先
|
||||
|
||||
本番環境では、PHP実行ユーザーが書き込める非公開ディレクトリを用意し、環境変数 `TARINAI_ACHIEVEMENT_DATA_DIR` に絶対パスを設定してください。未設定時は、HTTP公開ディレクトリ外の安定したユーザーデータ領域を使用します。
|
||||
|
||||
|
||||
### v39.16.16
|
||||
Colony birth/death chart lines, reverse rotators, champion-trait inheritance, reduced champion-pair mating, and directional one-way-fence routing.
|
||||
## PHP組み込みサーバーでの起動
|
||||
|
||||
PHP組み込みサーバーは `.htaccess` を解釈しません。旧 `.achievement_data/state.json` には引継ぎ用IDが含まれるため、必ず同梱のルーターを使用してください。
|
||||
|
||||
- v39.16.39 changes: restored the compact sticky operation palette, added exact-match right-panel search, reorganized colony settings, and refreshed tool icons.
|
||||
```sh
|
||||
./start_server.sh 8000
|
||||
```
|
||||
|
||||
|
||||
## Achievement system (v39.16.78)
|
||||
- Sixty-four local achievements cover biological events, special deaths, colony conditions, individual care, passive ecology, Undo/Redo behavior, rapid deletion, cleaner use, temperature transitions, medicine-category completion, weather shelter, combat reversals, circuit-board signaling, and ant-related play styles.
|
||||
- Locked entries show the configured pre-unlock disclosure directly in the row; all other locked entries show `???`. 大いなる母, めっちゃたりない観察, and 敵の敵は味方 show local progress percentages. Unlocked entries appear first within each category.
|
||||
- Each entry is a horizontal completion-rate bar whose background fill reflects the optional shared global unlock percentage; the shared achiever count appears beside the percentage.
|
||||
- v39.16.64 adds 毎日たりない観察(7 calendar days) and 抵抗器じゃない(7 distinct tarinai shocked by one energized bare wire). v39.16.54 added 真・たりない観察, 悠久の歴史, 五体満足, and 零体満足. v39.16.55 added 町のお医者さん for 50 effective first-aid recoveries and 割とふわふわ for flinging a plushie with つつく; both are event-driven and 町のお医者さん is disclosed before unlock. v39.16.56 added two achievements, persistent fight progress, stricter robot-only evaluation, revised thresholds, and unlocked-card rate bars. v39.16.60 adds three achievements and restricts the chaos counter to fight-mochi-influenced fights.
|
||||
- Persistent totals include 物でいっぱい, 害虫駆除, 大いなる母(3333体), 無計画都市, やっぱなし, and やっぱあり. Field-local progress includes 餌やりじいさん, 清掃業者, 自給自足, and 敵の敵は味方; applicable field-local metadata follows current-format saves.
|
||||
- `自動化の第一歩` and `リンク職人` are retired and replaced by `機械化産業`, which requires both detector-driven activation and rope/rod/spring coverage. Existing players with both retired achievements migrate automatically.
|
||||
- Optional same-origin PHP API aggregates anonymous player counts and global unlock percentages. Raw aggregate data is stored under `.achievement_data/state.json` in compact v3 positional arrays. Compact v2 and the prior verbose object are accepted only as migration inputs and are automatically rewritten under a separate server lock with atomic file replacement on the next API read. UUIDs are binary/base64url encoded, achievement IDs are indexed, timestamps are delta-coded, and repeated game versions use a dictionary.
|
||||
- On startup, shared-stat refresh, achievement-screen opening, and every five minutes, the client sends its complete non-debug unlock set with original local timestamps. Ordinary unlock bursts send only unsynchronized records after a short debounce. The server adds only missing records, so a lost `state.json` is rebuilt incrementally as players return without overwriting surviving records.
|
||||
|
||||
### Shared achievement percentage deployment
|
||||
- Upload `achievement_api.php` and the hidden `.achievement_data` directory with the game.
|
||||
- PHP must be enabled, and `.achievement_data` must be writable by the PHP process.
|
||||
- On a static-only host, local unlocks still work; the global percentage is shown as unavailable.
|
||||
- Set `window.TARINAI_ACHIEVEMENT_API` before `achievements.js` to use a different same-origin endpoint.
|
||||
- The current release tracks sixty-four achievement IDs, supports per-player reset, and accepts additive bulk synchronization for automatic recovery after aggregate-data loss. A separate backup remains necessary to recover players who never return.
|
||||
- `python3 scripts/achievement_server_state_audit.py` creates deterministic randomized achievement histories at runtime; it does not require or modify a production `state.json`.
|
||||
- v39.16.60 changes: achievement metadata in spells is now encoded as sparse binary data instead of embedded JSON; unlock masks, progress, timestamps, per-entity counters, and placement metadata are compactly packed and spell data is accepted only from the current format.
|
||||
- v39.16.60: Added 狙撃手, メガロポリス, and 豊穣ヲ希求スル者; restricted 混沌ヲ希求スル者 to fight-mochi-influenced fights; extended compact achievement spell progress and omitted counters for already-unlocked achievements.
|
||||
- v39.16.62: Added スペックがたりない, 寝ろ, 砂場, そこ公共空間だよ?, and 破格の工事費用; 寿命 deaths no longer reset 安全第一; 何やってるの? now uses empty-field clicks; and save schema 44 expands the achievement mask while decoding schemas 41–43.
|
||||
|
||||
- v39.16.63: Fixed achievement progress text weight and continuous-play tab switching; added アリ観察 and 命のバトン; corrected サウナ水風呂 and 敵の敵は味方 condition mismatches; save/spell data accepts only schema 45 and achievement payload v4.
|
||||
- v39.16.64: Added 毎日たりない観察 and 抵抗器じゃない; introduced calendar-day streak persistence; and migrated server aggregate JSON to compact v3 positional data automatically on read.
|
||||
- v39.16.65: Hardened pre-deployment server migration with read-only summaries, POST-only mutations, corruption-safe decoding, atomic writes under a separate lock, completionist eligibility filtering, and seeded randomized/concurrent migration audits.
|
||||
|
||||
- v39.16.66: Added two-user seesaw play, an editable AND/OR/NOT circuit board with positioned terminals, signal control for fan/stove/dry ice/duplicator, and wire-target highlighting.
|
||||
|
||||
- v39.16.67: Replaced the circuit-board dropdown editor with direct grid placement and cursor-drawn wiring; changed colony-state evaluation to every two in-game hours and excluded growing populations from Weakened; fixed seesaw action selection, fixed its orientation, and matched the placement overlay to its shape.
|
||||
- v39.16.69: Reworked circuit-board wiring into straight grid segments with branches and multi-connect terminals; removed OR and added a rectifier; made internal parts rotatable and external terminal count/positions editable; expanded board terminal spacing; changed detector numeric conditions to min/max dual sliders with number inputs; and raised seesaw riders slightly above the seats.
|
||||
- v39.16.68: Fixed seesaw participation so idle/bored Tarinai can initiate play and a waiting rider strongly recruits a partner; selected data reports active play as 「シーソーをしている。」; moved seesaws behind sprite layers; and aligned all two-input circuit gate pins to the 40px wiring grid.
|
||||
- v39.16.70: Reduced the default circuit-board footprint to one quarter of the previous area; made grid width/height editable; made external terminals movable on all four edges with a height-dependent count limit; made branch junctions movable; reduced NOT/rectifier body sizes; and corrected seesaw action-reason text.
|
||||
|
||||
- v39.16.71: Replaced circuit-board terminal +/- controls with direct IN/OUT terminal placement and erasing, dimmed terminals without external world wires, removed generic elliptical placement previews, added an Electrical tool category for detector/wires/circuit board, and made detector range-track clicks move the nearest thumb.
|
||||
|
||||
- v39.16.72: Circuit boards now default to 10x10 with a 20x20 maximum, AND uses a two-cell height, compact gates can sit closer to top/bottom edges, placement previews follow occupancy footprints more directly, and unstable internal logic loops are explicitly forced OFF.
|
||||
- v39.16.73: Circuit-board editing now shows placement ghosts for internal parts and terminals, Q/E rotates the pending internal part by 90 degrees, ON constant sources are explained in the editor, and wire/insulated-wire palette silhouettes render correctly after the Electrical category move.
|
||||
- v39.16.78: Fixed 「あぁ^~ たまらねぇぜ」 so burst zunchi ignore their spawn overlap with the owner, arm only after clearing the owner once, and unlock on a later fatal return collision including bounce-fence reflections. The renewed achievement audit also fixes ミニマリスト save metadata, サウナ水風呂 hot-entry timing, lethal wire-shock counting, reset-scoped hidden progress cleanup, and 監視カメラ intervention gaps for Undo/Redo and ground changes.
|
||||
- v39.16.76: Removed the circuit-board OFF constant part and added the 「情報化産業」 achievement for carrying an active signal directly from one circuit board to another over wire or insulated wire.
|
||||
Windowsでは `start_server.bat 8000` を使用します。本番環境では引き続き `TARINAI_ACHIEVEMENT_DATA_DIR` をHTTP公開ディレクトリ外へ設定してください。
|
||||
|
|
|
|||
1576
achievement_api.php
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "39.16.98",
|
||||
"version": "39.18.18",
|
||||
"css": [
|
||||
"css/base.css",
|
||||
"css/layout.css",
|
||||
|
|
@ -58,6 +58,7 @@
|
|||
"js/item_dynamic_duplicator_system.js",
|
||||
"js/item_dynamic_pin_system.js",
|
||||
"js/item_dynamic_zunchi_system.js",
|
||||
"js/item_environment_hazard_system.js",
|
||||
"js/item_dynamic_system.js",
|
||||
"js/item_lifecycle_decay_system.js",
|
||||
"js/item_lifecycle_growth_system.js",
|
||||
|
|
@ -69,6 +70,7 @@
|
|||
"js/item_lifecycle_pipeline.js",
|
||||
"js/item_runtime.js",
|
||||
"js/structure_lifecycle.js",
|
||||
"js/memorial_bell_system.js",
|
||||
"js/item_render_helpers.js",
|
||||
"js/item_render_runtime.js",
|
||||
"js/burn_motion_util.js",
|
||||
|
|
@ -148,6 +150,7 @@
|
|||
"js/text_catalog.js",
|
||||
"js/ui.js",
|
||||
"js/game_dialogs.js",
|
||||
"js/achievement_catalog.js",
|
||||
"js/achievements.js",
|
||||
"js/ui_helpers.js",
|
||||
"js/ui_log.js",
|
||||
|
|
|
|||
BIN
assets/objects/mamekusaredake.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
assets/sounds/achievement_unlock.mp3
Normal file
BIN
assets/sounds/acquired_stress_001.wav
Normal file
BIN
assets/sounds/acquired_stress_002.wav
Normal file
BIN
assets/sounds/acquired_stress_003.wav
Normal file
BIN
assets/sounds/acquired_stress_004.wav
Normal file
BIN
assets/sounds/acquired_stress_005.wav
Normal file
BIN
assets/sounds/acquired_stress_006.wav
Normal file
BIN
assets/sounds/acquired_stress_007.wav
Normal file
BIN
assets/sounds/acquired_stress_008.wav
Normal file
BIN
assets/sounds/acquired_stress_009.wav
Normal file
BIN
assets/sounds/bicycle_bell.mp3
Normal file
BIN
assets/sounds/intimidate_alt_001.wav
Normal file
BIN
assets/sounds/ochibi_calm_001.wav
Normal file
BIN
assets/sounds/ochibi_calm_002.wav
Normal file
BIN
assets/sounds/ochibi_calm_003.wav
Normal file
BIN
assets/sounds/ochibi_calm_004.wav
Normal file
BIN
assets/sounds/ochibi_calm_005.wav
Normal file
BIN
assets/sounds/ochibi_calm_006.wav
Normal file
BIN
assets/sounds/ochibi_calm_007.wav
Normal file
BIN
assets/sounds/ochibi_calm_008.wav
Normal file
BIN
assets/sounds/ochibi_calm_009.wav
Normal file
BIN
assets/sounds/ochibi_damage_001.wav
Normal file
BIN
assets/sounds/ochibi_damage_002.wav
Normal file
BIN
assets/sounds/ochibi_damage_003.wav
Normal file
BIN
assets/sounds/ochibi_dirty_001.wav
Normal file
BIN
assets/sounds/ochibi_dirty_002.wav
Normal file
BIN
assets/sounds/ochibi_dirty_003.wav
Normal file
BIN
assets/sounds/ochibi_dirty_004.wav
Normal file
BIN
assets/sounds/ochibi_disease_001.wav
Normal file
BIN
assets/sounds/ochibi_electric_006.wav
Normal file
BIN
assets/sounds/ochibi_fight_hit_004.wav
Normal file
BIN
assets/sounds/ochibi_hot_007.wav
Normal file
BIN
assets/sounds/ochibi_panic_005.wav
Normal file
BIN
assets/sounds/ochibi_sleep_002.wav
Normal file
BIN
assets/sounds/ochibi_sleep_003.wav
Normal file
BIN
assets/sounds/ochibi_stress_008.wav
Normal file
BIN
assets/sprites/tarinai_40_intimidate_alt.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/sprites/tarinai_acquired_overlay.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/ui/pwa-icon-192.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/ui/pwa-icon-512.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
assets/ui/tool_acquired_tarinai.webp
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
assets/ui/tool_mamekusaredake.webp
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
15
css/base.css
|
|
@ -78,10 +78,19 @@ button {
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.room-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.room-link {
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background: rgba(255,255,255,0.86);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
|
@ -91,6 +100,11 @@ h1 {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.speed-select-native { display: none; }
|
||||
.speed-menu { position: relative; display: inline-block; }
|
||||
.speed-menu-btn { min-width: 92px; text-align: center; white-space: nowrap; }
|
||||
|
||||
.top-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
|
|
@ -381,6 +395,7 @@ h1 {
|
|||
word-break: normal;
|
||||
overflow-wrap: anywhere;
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,0.62);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.tool.tool-has-canvas-icon { --tool-icon: none; grid-template-columns: 31px minmax(0,1fr); min-height: 47px; padding-top: 4px; }
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@
|
|||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin: 8px 0 4px;
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
isolation: isolate;
|
||||
}
|
||||
.favorite-toggle {
|
||||
border: 1px solid rgba(128, 102, 56, 0.32);
|
||||
|
|
@ -34,8 +37,15 @@
|
|||
background: rgba(255, 248, 221, 0.92);
|
||||
color: #5a4630;
|
||||
font-weight: 700;
|
||||
padding: 5px 10px;
|
||||
padding: 7px 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
pointer-events: auto;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.favorite-toggle.active {
|
||||
background: rgba(255, 229, 122, 0.95);
|
||||
|
|
@ -335,13 +345,20 @@
|
|||
border: 1px solid rgba(192, 138, 96, 0.24);
|
||||
background: rgba(255, 247, 239, 0.74);
|
||||
}
|
||||
.event-category-heading {
|
||||
margin-bottom: 7px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: rgba(82, 66, 52, 0.88);
|
||||
}
|
||||
.push-master {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 9px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.push-kind-grid {
|
||||
display: grid;
|
||||
|
|
@ -419,6 +436,13 @@
|
|||
.log-push-toast.log-push-weather::before { background: #7bbbdc; }
|
||||
.log-push-toast.log-push-grass::before { background: #8cc77a; }
|
||||
.log-push-toast.log-push-event::before { background: #8b82d6; }
|
||||
|
||||
.log-push-toast.log-push-life::before { background: #82bd72; }
|
||||
.log-push-toast.log-push-danger::before { background: #df845d; }
|
||||
.log-push-toast.log-push-social::before { background: #b28bd0; }
|
||||
.log-push-toast.log-push-living::before { background: #d7ad58; }
|
||||
.log-push-toast.log-push-environment::before { background: #70aac8; }
|
||||
.log-push-toast.log-push-observe::before { background: #9b9184; }
|
||||
.log-push-toast.leaving {
|
||||
animation: logPushOut .18s ease both;
|
||||
}
|
||||
|
|
@ -667,6 +691,10 @@
|
|||
gap: 5px;
|
||||
}
|
||||
.top-menu-panel.hidden { display: none; }
|
||||
.speed-menu-panel { width: 116px; min-width: 116px; }
|
||||
.speed-menu-panel .top-menu-item { text-align: center; }
|
||||
.speed-menu-panel .top-menu-item.active,
|
||||
.speed-menu-panel .top-menu-item[aria-checked="true"] { background: rgba(224,242,201,.92); }
|
||||
.top-menu-item {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
.tool[data-tool="delete"] { --tool-icon-zoom: 1.42; }
|
||||
.tool[data-tool="area_delete"] { --tool-icon-zoom: 1; }
|
||||
.tool[data-tool="pinch"].tool-emoji-icon::before { font-size: 22px; }
|
||||
.tool[data-tool="bell"].tool-emoji-icon::before { font-size: 21px; z-index: 1; }
|
||||
.tool[data-tool="area_delete"].tool-emoji-icon::before {
|
||||
font-size: 31px;
|
||||
line-height: 1;
|
||||
|
|
@ -125,6 +126,7 @@
|
|||
box-shadow: 0 1px 4px rgba(42,36,29,0.16);
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
z-index: 4;
|
||||
}
|
||||
.chart-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; margin: 3px 0 8px; }
|
||||
.chart-tabs button { border: 1px solid var(--line); background: rgba(255,255,255,0.62); border-radius: 999px; padding: 5px 4px; font-size: 11px; cursor: pointer; }
|
||||
|
|
@ -295,9 +297,15 @@ meter {
|
|||
.log-grass { border-left-color: rgba(72, 148, 67, 0.68); }
|
||||
.log-weather { border-left-color: rgba(92, 142, 210, 0.62); }
|
||||
.log-relation { border-left-color: rgba(204, 126, 176, 0.62); }
|
||||
.log-observe { border-left-color: rgba(175, 146, 86, 0.62); }
|
||||
.log-event { border-left-color: rgba(132, 120, 210, 0.62); }
|
||||
|
||||
.log-life { border-left-color: rgba(105, 166, 96, 0.68); }
|
||||
.log-danger { border-left-color: rgba(214, 104, 70, 0.72); }
|
||||
.log-social { border-left-color: rgba(184, 116, 184, 0.66); }
|
||||
.log-living { border-left-color: rgba(183, 151, 74, 0.68); }
|
||||
.log-environment { border-left-color: rgba(82, 142, 170, 0.66); }
|
||||
.log-observe { border-left-color: rgba(142, 132, 116, 0.62); }
|
||||
|
||||
/* Clearer tool category grouping. */
|
||||
.tool-category[data-tool-category="operate"] { --tool-cat-a: rgba(224,239,255,0.82); --tool-cat-b: rgba(203,226,255,0.74); --tool-cat-line: rgba(82,128,190,0.26); }
|
||||
.tool-category[data-tool-category="food"] { --tool-cat-a: rgba(236,252,219,0.82); --tool-cat-b: rgba(214,244,194,0.74); --tool-cat-line: rgba(96,156,72,0.26); }
|
||||
|
|
@ -347,3 +355,7 @@ meter {
|
|||
border-color: rgba(38,132,190,0.56);
|
||||
box-shadow: inset 0 0 0 1px rgba(84,176,226,0.20), 0 8px 20px rgba(59,133,177,0.10);
|
||||
}
|
||||
|
||||
.chart-range-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; margin: -2px 0 7px; }
|
||||
.chart-range-tabs button { border: 1px solid var(--line); background: rgba(255,255,255,0.48); border-radius: 8px; padding: 4px 4px; font-size: 10px; cursor: pointer; color: var(--muted); }
|
||||
.chart-range-tabs button.active { background: rgba(239,255,222,0.92); border-color: rgba(103,178,44,0.42); color: var(--text); font-weight: 700; }
|
||||
|
|
|
|||
|
|
@ -429,3 +429,23 @@ body.touch-mobile-ui .tool-tip-popover.mobile-selection-tip {
|
|||
@media (max-width: 980px) {
|
||||
.top-actions { overflow-x: auto; overflow-y: hidden; }
|
||||
}
|
||||
|
||||
/* Mobile toolbar: keep every primary action reachable without hidden horizontal overflow. */
|
||||
@media (max-width: 520px) {
|
||||
.top-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow: visible;
|
||||
align-items: center;
|
||||
}
|
||||
.top-panel-search {
|
||||
flex: 1 1 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.top-actions > .btn,
|
||||
.top-actions > .top-menu,
|
||||
.top-actions > .speed-menu {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,14 @@ body::before {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.family-death-time {
|
||||
display: block;
|
||||
margin-top: 1px;
|
||||
font-size: 10px;
|
||||
line-height: 1.15;
|
||||
color: rgba(93, 72, 61, 0.76);
|
||||
}
|
||||
.family-genetic-stats {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
|
@ -260,8 +268,8 @@ body::before {
|
|||
.lineage-family-tree .tree-node {
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 104px;
|
||||
min-height: 104px;
|
||||
height: 112px;
|
||||
min-height: 112px;
|
||||
box-sizing: border-box;
|
||||
align-items: flex-start;
|
||||
background: linear-gradient(180deg, #fffdf8, #f8f1e5);
|
||||
|
|
@ -280,6 +288,29 @@ body::before {
|
|||
.lineage-family-tree .tree-node[data-family-tarinai-id]:hover {
|
||||
transform: none;
|
||||
}
|
||||
.lineage-family-tree.family-hover-active .tree-node.family-hover-dim {
|
||||
opacity: 0.28;
|
||||
filter: saturate(0.58);
|
||||
transition: opacity .12s ease, filter .12s ease, box-shadow .12s ease, border-color .12s ease;
|
||||
}
|
||||
.lineage-family-tree.family-hover-active .tree-node.family-hover-focus,
|
||||
.lineage-family-tree.family-hover-active .tree-node.family-hover-parent,
|
||||
.lineage-family-tree.family-hover-active .tree-node.family-hover-child {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
.lineage-family-tree.family-hover-active .tree-node.family-hover-focus {
|
||||
border-color: rgba(124, 165, 66, 0.78);
|
||||
box-shadow: 0 0 0 3px rgba(124, 165, 66, 0.16), 0 5px 13px rgba(64,48,32,0.10);
|
||||
}
|
||||
.lineage-family-tree.family-hover-active .tree-node.family-hover-parent {
|
||||
border-color: rgba(80, 132, 198, 0.72);
|
||||
box-shadow: 0 0 0 2px rgba(80, 132, 198, 0.14);
|
||||
}
|
||||
.lineage-family-tree.family-hover-active .tree-node.family-hover-child {
|
||||
border-color: rgba(87, 170, 91, 0.72);
|
||||
box-shadow: 0 0 0 2px rgba(87, 170, 91, 0.14);
|
||||
}
|
||||
.lineage-family-tree .tree-node .family-portrait {
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
|
|
@ -319,7 +350,7 @@ body::before {
|
|||
/* Restored from the original ZIP: fixed colony graph/legend sizing. */
|
||||
.colony-card { overflow: visible; min-height: 0; flex: 0 0 auto; }
|
||||
.colony-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.colony-chart {
|
||||
|
|
@ -442,3 +473,12 @@ body::before {
|
|||
line-height: 9px !important;
|
||||
font-size: 6.8px !important;
|
||||
}
|
||||
|
||||
.colony-chart { position: relative; }
|
||||
.colony-chart-tooltip { position: fixed; z-index: 10020; pointer-events: none; min-width: 112px; max-width: 210px; padding: 7px 8px; border-radius: 9px; border: 1px solid rgba(80,65,48,0.18); background: rgba(255,252,245,0.98); color: #4d443a; box-shadow: 0 4px 14px rgba(64,48,30,0.16); font-size: 10px; line-height: 1.45; transform: none; }
|
||||
.colony-chart-tooltip.hidden { display: none; }
|
||||
.colony-chart-tooltip strong { display: block; margin-bottom: 3px; font-size: 10.5px; }
|
||||
.colony-chart-tooltip-row { display: flex; justify-content: space-between; gap: 10px; white-space: nowrap; }
|
||||
.colony-chart-hover-point { position: absolute; z-index: 5; width: 8px; height: 8px; border: 2px solid #5d8ee6; border-radius: 50%; background: rgba(255,255,255,0.96); box-shadow: 0 0 0 2px rgba(255,255,255,0.7); pointer-events: none; transform: translate(-50%, -50%); }
|
||||
.colony-chart-hover-point.hidden { display: none; }
|
||||
|
||||
|
|
|
|||
459
index.html
|
|
@ -4,14 +4,16 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>たりない観察</title>
|
||||
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=39.16.98" />
|
||||
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=39.16.98" />
|
||||
<link rel="shortcut icon" href="favicon.ico?v=39.16.98" />
|
||||
<link rel="stylesheet" href="css/base.css?v=39.16.98" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.16.98" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.16.98" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.16.98" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.16.98" />
|
||||
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=39.18.18" />
|
||||
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=39.18.18" />
|
||||
<link rel="manifest" href="manifest.webmanifest?v=39.18.18" />
|
||||
<meta name="theme-color" content="#244d3a" />
|
||||
<link rel="shortcut icon" href="favicon.ico?v=39.18.18" />
|
||||
<link rel="stylesheet" href="css/base.css?v=39.18.18" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.18.18" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.18.18" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.18.18" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.18.18" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||
|
|
@ -27,7 +29,10 @@
|
|||
<div id="app">
|
||||
<header class="topbar">
|
||||
<div class="top-left">
|
||||
<a class="btn room-link" href="https://2012r2.nishi.boats/~333/">333の部屋に移動</a>
|
||||
<div class="room-links" style="display:flex;flex-direction:column;align-items:stretch;gap:6px;">
|
||||
<a class="btn room-link" style="display:block;text-align:center;" href="https://2012r2.nishi.boats/~333/">333の部屋に移動</a>
|
||||
<a class="btn room-link" style="display:block;text-align:center;" href="https://seesaawiki.jp/informalzunda/d/%a5%b2%a1%bc%a5%e0%3a%20%a4%bf%a4%ea%a4%ca%a4%a4%b4%d1%bb%a1">地下牢に戻る</a>
|
||||
</div>
|
||||
<div class="brand">
|
||||
<h1>たりない観察</h1>
|
||||
</div>
|
||||
|
|
@ -40,17 +45,30 @@
|
|||
<button class="panel-search-next" type="button" disabled aria-label="次の該当位置へ移動">↓</button>
|
||||
</div>
|
||||
<button id="pauseBtn" class="btn primary">一時停止</button>
|
||||
<button id="speedBtn" class="btn">速度 x1</button>
|
||||
<div id="speedMenu" class="top-menu speed-menu">
|
||||
<button id="speedMenuBtn" class="btn speed-menu-btn" type="button" aria-haspopup="true" aria-expanded="false">速度 ×1</button>
|
||||
<div id="speedMenuPanel" class="top-menu-panel speed-menu-panel hidden" role="menu" aria-label="速度">
|
||||
<button class="top-menu-item" type="button" role="menuitemradio" data-speed-value="0.5">×0.5</button>
|
||||
<button class="top-menu-item" type="button" role="menuitemradio" data-speed-value="1">×1</button>
|
||||
<button class="top-menu-item" type="button" role="menuitemradio" data-speed-value="2">×2</button>
|
||||
<button class="top-menu-item" type="button" role="menuitemradio" data-speed-value="4">×4</button>
|
||||
<button class="top-menu-item" type="button" role="menuitemradio" data-speed-value="8">×8</button>
|
||||
<button class="top-menu-item" type="button" role="menuitemradio" data-speed-value="16">×16</button>
|
||||
</div>
|
||||
<select id="speedSelect" class="speed-select-native" aria-hidden="true" tabindex="-1">
|
||||
<option value="0.5">0.5</option><option value="1" selected>1</option><option value="2">2</option><option value="4">4</option><option value="8">8</option><option value="16">16</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="saveBtn" class="btn">セーブ</button>
|
||||
<button id="achievementsBtn" class="btn achievement-top-btn" type="button" aria-haspopup="dialog" aria-controls="achievementsDialog">実績 <span id="achievementButtonCount">0/75</span></button>
|
||||
<button id="ecologyBtn" class="btn ecology-top-btn" type="button">たりないの生態</button>
|
||||
<button id="achievementsBtn" class="btn achievement-top-btn" type="button" aria-haspopup="dialog" aria-controls="achievementsDialog">実績 <span id="achievementButtonCount">0/77</span></button>
|
||||
<button id="resetBtn" class="btn danger reset-top-btn">リセット</button>
|
||||
<div id="topMenu" class="top-menu">
|
||||
<button id="topMenuBtn" class="btn top-menu-btn" type="button" aria-haspopup="true" aria-expanded="false" aria-label="メニュー">☰</button>
|
||||
<div id="topMenuPanel" class="top-menu-panel hidden" role="menu" aria-label="メニュー">
|
||||
<button id="ecologyBtn" class="top-menu-item" type="button" role="menuitem">生態</button>
|
||||
<div id="topMenuPanel" class="top-menu-panel hidden" role="group" aria-label="メニュー">
|
||||
<div id="soundMenu" class="sound-menu top-menu-sound">
|
||||
<button id="soundBtn" class="top-menu-item sound-menu-btn" type="button" aria-haspopup="true" aria-expanded="false">音 ON</button>
|
||||
<div id="soundPanel" class="sound-panel hidden" role="menu" aria-label="音">
|
||||
<div id="soundPanel" class="sound-panel hidden" role="group" aria-label="音">
|
||||
<label class="sound-master"><input id="soundMasterToggle" type="checkbox" checked><span>効果音</span></label>
|
||||
<div class="sound-panel-divider"></div>
|
||||
<label><input type="checkbox" data-sound-category="voice"><span>たりないの声</span></label>
|
||||
|
|
@ -60,7 +78,7 @@
|
|||
</div>
|
||||
<div id="visualSettingsMenu" class="sound-menu top-menu-sound top-menu-visual">
|
||||
<button id="visualSettingsBtn" class="top-menu-item sound-menu-btn" type="button" aria-haspopup="true" aria-expanded="false">表示設定</button>
|
||||
<div id="visualSettingsPanel" class="sound-panel hidden" role="menu" aria-label="表示設定">
|
||||
<div id="visualSettingsPanel" class="sound-panel hidden" role="group" aria-label="表示設定">
|
||||
<label><input id="visualEffectsToggle" type="checkbox" data-visual-setting="effects" data-visual-on="high" data-visual-off="off"><span>エフェクト</span></label>
|
||||
<label><input id="visualShadowsToggle" type="checkbox" data-visual-setting="shadows" data-visual-on="high" data-visual-off="off"><span>影</span></label>
|
||||
<div class="sound-panel-divider"></div>
|
||||
|
|
@ -70,7 +88,7 @@
|
|||
<label><input type="radio" name="visualDetails" data-visual-setting="details" data-visual-value="high"><span>高</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<button id="creditsBtn" class="top-menu-item" type="button" role="menuitem">クレジット</button>
|
||||
<button id="creditsBtn" class="top-menu-item" type="button">クレジット</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -134,7 +152,7 @@
|
|||
<div class="colony-setting-section-head"><span>気温</span><strong id="temperatureValue">10℃</strong></div>
|
||||
<div class="temperature-control" aria-label="気温調整">
|
||||
<label class="temperature-auto"><input id="temperatureAutoToggle" type="checkbox" checked> 自動</label>
|
||||
<input id="temperatureSlider" class="temperature-slider" type="range" min="-100" max="120" step="1" value="10">
|
||||
<input id="temperatureSlider" class="temperature-slider" type="range" aria-label="気温" min="-100" max="120" step="1" value="10">
|
||||
</div>
|
||||
</section>
|
||||
<section class="colony-setting-section colony-limit-section">
|
||||
|
|
@ -150,6 +168,7 @@
|
|||
</div>
|
||||
</details>
|
||||
<div id="colonyChartTabs" class="chart-tabs"><button data-chart="population" class="active">個体数</button><button data-chart="life">欲求</button><button data-chart="objects">環境</button></div>
|
||||
<div id="colonyChartRangeTabs" class="chart-range-tabs" aria-label="統計の表示期間"><button data-chart-range="day">1日</button><button data-chart-range="season" class="active">季節</button><button data-chart-range="year">年</button></div>
|
||||
<div id="colonyChartLegend" class="colony-chart-legend"></div>
|
||||
<div id="colonyChart" class="colony-chart" aria-label="コロニーの推移グラフ" role="img"></div>
|
||||
</div>
|
||||
|
|
@ -159,19 +178,17 @@
|
|||
<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="log" class="log"></div>
|
||||
<div class="log-push-controls" id="logPushControls" aria-label="プッシュ通知設定">
|
||||
<label class="push-master"><input id="logPushEnabled" type="checkbox"> プッシュ通知</label>
|
||||
<div class="log-push-controls" id="logPushControls" aria-label="イベント表示・プッシュ通知設定">
|
||||
<div class="event-category-heading">表示カテゴリ</div>
|
||||
<div class="push-kind-grid">
|
||||
<label><input type="checkbox" data-push-kind="death" checked> 死亡</label>
|
||||
<label><input type="checkbox" data-push-kind="accident" checked> 事故</label>
|
||||
<label><input type="checkbox" data-push-kind="birth" checked> 誕生</label>
|
||||
<label><input type="checkbox" data-push-kind="fight"> 喧嘩</label>
|
||||
<label><input type="checkbox" data-push-kind="food"> 食事</label>
|
||||
<label><input type="checkbox" data-push-kind="weather"> 天気</label>
|
||||
<label><input type="checkbox" data-push-kind="grass"> 草</label>
|
||||
<label><input type="checkbox" data-push-kind="event"> 出来事</label>
|
||||
<label><input type="checkbox" data-push-kind="relation"> 関係</label>
|
||||
<label><input type="checkbox" data-event-category="life" checked> 生命</label>
|
||||
<label><input type="checkbox" data-event-category="danger" checked> 危険</label>
|
||||
<label><input type="checkbox" data-event-category="social" checked> 交流</label>
|
||||
<label><input type="checkbox" data-event-category="living" checked> 生活</label>
|
||||
<label><input type="checkbox" data-event-category="environment" checked> 環境</label>
|
||||
<label><input type="checkbox" data-event-category="observe" checked> 観察</label>
|
||||
</div>
|
||||
<label class="push-master"><input id="logPushEnabled" type="checkbox"> プッシュ通知</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -202,7 +219,7 @@
|
|||
<div class="achievement-dialog-head">
|
||||
<div>
|
||||
<div class="achievement-dialog-kicker">たりない観察</div>
|
||||
<h2 id="achievementsDialogTitle">実績 <span id="achievementDialogCount">0 / 75</span></h2>
|
||||
<h2 id="achievementsDialogTitle">実績 <span id="achievementDialogCount">0 / 77</span></h2>
|
||||
</div>
|
||||
<button id="achievementsCloseIconBtn" class="achievement-close-icon" type="button" aria-label="実績を閉じる">×</button>
|
||||
</div>
|
||||
|
|
@ -293,173 +310,221 @@
|
|||
</div>
|
||||
|
||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||
<script src="js/version.js?v=39.16.98" defer></script>
|
||||
<script src="js/event_bus.js?v=39.16.98" defer></script>
|
||||
<script src="js/domain_ids.js?v=39.16.98" defer></script>
|
||||
<script src="js/registry_base.js?v=39.16.98" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.16.98" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.16.98" defer></script>
|
||||
<script src="js/deterministic_helpers.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_tool_metadata.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_tool_definitions.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_visual_definitions.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_food_definitions.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_effect_definitions.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_registry.js?v=39.16.98" defer></script>
|
||||
<script src="js/data.js?v=39.16.98" defer></script>
|
||||
<script src="js/ground_types.js?v=39.16.98" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.16.98" defer></script>
|
||||
<script src="js/math.js?v=39.16.98" defer></script>
|
||||
<script src="js/geometry_helpers.js?v=39.16.98" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.16.98" defer></script>
|
||||
<script src="js/placement_preview_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/pin_attachment_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/physics_shape_editor_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/mechanical_shape_bridge.js?v=39.16.98" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/physics_projection_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/circuit_board_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/signal_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/assets.js?v=39.16.98" defer></script>
|
||||
<script src="js/audio.js?v=39.16.98" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.16.98" defer></script>
|
||||
<script src="js/display_helpers.js?v=39.16.98" defer></script>
|
||||
<script src="js/render.js?v=39.16.98" defer></script>
|
||||
<script src="js/sim_core.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.16.98" defer></script>
|
||||
<script src="js/structures.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_type_catalog.js?v=39.16.98" defer></script>
|
||||
<script src="js/items.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.16.98" defer></script>
|
||||
<script src="js/fire_runtime_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/robot_cleaner_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.16.98" defer></script>
|
||||
<script src="js/update_step_pipeline_runner.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.16.98" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_render_helpers.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.16.98" defer></script>
|
||||
<script src="js/burn_motion_util.js?v=39.16.98" defer></script>
|
||||
<script src="js/ants.js?v=39.16.98" defer></script>
|
||||
<script src="js/health.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_nest_sleep_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.16.98" defer></script>
|
||||
<script src="js/seesaw_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_food_prototype_mixin.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_direct_feeding_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.16.98" defer></script>
|
||||
<script src="js/world.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_view.js?v=39.16.98" defer></script>
|
||||
<script src="js/family_graph.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_reset_presets.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.16.98" defer></script>
|
||||
<script src="js/impact_core_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/impact_response_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/collision_response_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_environment.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_temperature_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_pathfinding_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_grass_placement_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/weather_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.16.98" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.16.98" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.16.98" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.16.98" defer></script>
|
||||
<script src="js/system_order.js?v=39.16.98" defer></script>
|
||||
<script src="js/simulation.js?v=39.16.98" defer></script>
|
||||
<script src="js/colony_situation_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_update.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.16.98" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.16.98" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.16.98" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui.js?v=39.16.98" defer></script>
|
||||
<script src="js/game_dialogs.js?v=39.16.98" defer></script>
|
||||
<script src="js/achievements.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_log.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.16.98" defer></script>
|
||||
<script src="js/save_schema.js?v=39.16.98" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.16.98" defer></script>
|
||||
<script src="js/history_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/save_codec.js?v=39.16.98" defer></script>
|
||||
<script src="js/save_storage.js?v=39.16.98" defer></script>
|
||||
<script src="js/save_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_pointer_action_system.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.16.98" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.16.98" defer></script>
|
||||
<script src="js/main.js?v=39.16.98" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.16.98" defer></script>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const replacements = [
|
||||
["端末報告ベース", "達成率"],
|
||||
["報告プレイヤー", "達成人数"],
|
||||
];
|
||||
const replaceText = value => replacements.reduce((text, [from, to]) => text.replaceAll(from, to), String(value || ""));
|
||||
const normalizeNode = node => {
|
||||
if (!node) return;
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const next = replaceText(node.nodeValue);
|
||||
if (next !== node.nodeValue) node.nodeValue = next;
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
if (node.hasAttribute("aria-label")) {
|
||||
const current = node.getAttribute("aria-label");
|
||||
const next = replaceText(current);
|
||||
if (next !== current) node.setAttribute("aria-label", next);
|
||||
}
|
||||
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
||||
while (walker.nextNode()) {
|
||||
const textNode = walker.currentNode;
|
||||
const next = replaceText(textNode.nodeValue);
|
||||
if (next !== textNode.nodeValue) textNode.nodeValue = next;
|
||||
}
|
||||
node.querySelectorAll?.("[aria-label]").forEach(element => {
|
||||
const current = element.getAttribute("aria-label");
|
||||
const next = replaceText(current);
|
||||
if (next !== current) element.setAttribute("aria-label", next);
|
||||
});
|
||||
};
|
||||
const list = document.getElementById("achievementList");
|
||||
if (!list) return;
|
||||
normalizeNode(list);
|
||||
new MutationObserver(records => {
|
||||
for (const record of records) {
|
||||
if (record.type === "characterData") normalizeNode(record.target);
|
||||
for (const node of record.addedNodes || []) normalizeNode(node);
|
||||
}
|
||||
}).observe(list, { childList: true, subtree: true, characterData: true });
|
||||
})();
|
||||
</script>
|
||||
<script src="js/version.js?v=39.18.18" defer></script>
|
||||
<script src="js/event_bus.js?v=39.18.18" defer></script>
|
||||
<script src="js/domain_ids.js?v=39.18.18" defer></script>
|
||||
<script src="js/registry_base.js?v=39.18.18" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.18.18" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.18.18" defer></script>
|
||||
<script src="js/deterministic_helpers.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_tool_metadata.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_tool_definitions.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_visual_definitions.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_food_definitions.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_effect_definitions.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_registry.js?v=39.18.18" defer></script>
|
||||
<script src="js/data.js?v=39.18.18" defer></script>
|
||||
<script src="js/ground_types.js?v=39.18.18" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.18.18" defer></script>
|
||||
<script src="js/math.js?v=39.18.18" defer></script>
|
||||
<script src="js/geometry_helpers.js?v=39.18.18" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.18.18" defer></script>
|
||||
<script src="js/placement_preview_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/pin_attachment_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/physics_shape_editor_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/mechanical_shape_bridge.js?v=39.18.18" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/physics_projection_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/circuit_board_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/signal_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/assets.js?v=39.18.18" defer></script>
|
||||
<script src="js/audio.js?v=39.18.18" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.18.18" defer></script>
|
||||
<script src="js/display_helpers.js?v=39.18.18" defer></script>
|
||||
<script src="js/render.js?v=39.18.18" defer></script>
|
||||
<script src="js/sim_core.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.18.18" defer></script>
|
||||
<script src="js/structures.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_type_catalog.js?v=39.18.18" defer></script>
|
||||
<script src="js/items.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.18.18" defer></script>
|
||||
<script src="js/fire_runtime_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/robot_cleaner_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_environment_hazard_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.18.18" defer></script>
|
||||
<script src="js/update_step_pipeline_runner.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.18.18" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.18.18" defer></script>
|
||||
<script src="js/memorial_bell_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_render_helpers.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.18.18" defer></script>
|
||||
<script src="js/burn_motion_util.js?v=39.18.18" defer></script>
|
||||
<script src="js/ants.js?v=39.18.18" defer></script>
|
||||
<script src="js/health.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai.js?v=39.18.18" defer></script>
|
||||
<script src="js/mamekusaredake_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_nest_sleep_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.18.18" defer></script>
|
||||
<script src="js/seesaw_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_food_prototype_mixin.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_direct_feeding_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.18.18" defer></script>
|
||||
<script src="js/world.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_view.js?v=39.18.18" defer></script>
|
||||
<script src="js/family_graph.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_reset_presets.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.18.18" defer></script>
|
||||
<script src="js/impact_core_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/impact_response_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/collision_response_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_environment.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_temperature_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_pathfinding_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_grass_placement_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/weather_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.18.18" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.18.18" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.18.18" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.18.18" defer></script>
|
||||
<script src="js/system_order.js?v=39.18.18" defer></script>
|
||||
<script src="js/simulation.js?v=39.18.18" defer></script>
|
||||
<script src="js/colony_situation_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_update.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.18.18" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.18.18" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.18.18" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui.js?v=39.18.18" defer></script>
|
||||
<script src="js/game_dialogs.js?v=39.18.18" defer></script>
|
||||
<script src="js/achievement_catalog.js?v=39.18.18" defer></script>
|
||||
<script src="js/achievements.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_log.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.18.18" defer></script>
|
||||
<script src="js/save_schema.js?v=39.18.18" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.18.18" defer></script>
|
||||
<script src="js/history_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/save_codec.js?v=39.18.18" defer></script>
|
||||
<script src="js/save_storage.js?v=39.18.18" defer></script>
|
||||
<script src="js/save_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_pointer_action_system.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.18.18" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.18.18" defer></script>
|
||||
<script src="js/main.js?v=39.18.18" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.18.18" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
731
js/achievement_catalog.js
Normal file
|
|
@ -0,0 +1,731 @@
|
|||
window.TARINAI_ACHIEVEMENT_CATALOG = {
|
||||
"revision": "2026-07-25-achievement-cleanup-v1",
|
||||
"completionistId": "true_tarinai_observer",
|
||||
"categories": [
|
||||
{
|
||||
"id": "ecology",
|
||||
"title": "生態"
|
||||
},
|
||||
{
|
||||
"id": "population",
|
||||
"title": "人口"
|
||||
},
|
||||
{
|
||||
"id": "experiment",
|
||||
"title": "実験"
|
||||
},
|
||||
{
|
||||
"id": "construction",
|
||||
"title": "建築"
|
||||
},
|
||||
{
|
||||
"id": "operation",
|
||||
"title": "操作"
|
||||
},
|
||||
{
|
||||
"id": "other",
|
||||
"title": "その他"
|
||||
}
|
||||
],
|
||||
"medicineLedgerTypes": [
|
||||
"first_aid",
|
||||
"sedative",
|
||||
"sleep_drug",
|
||||
"laxative",
|
||||
"protein",
|
||||
"niteropu",
|
||||
"ammo",
|
||||
"mystery_drug",
|
||||
"mercury",
|
||||
"giant_drug",
|
||||
"dwarf_drug"
|
||||
],
|
||||
"linkProgressTypes": [
|
||||
"rope",
|
||||
"rod",
|
||||
"spring"
|
||||
],
|
||||
"additiveProgressCaps": {
|
||||
"placementCount": 100,
|
||||
"antKills": 100,
|
||||
"birthCount": 3333,
|
||||
"quickDeleteCount": 30,
|
||||
"undoCount": 20,
|
||||
"redoCount": 20,
|
||||
"directFeedCount": 33,
|
||||
"firstAidHeals": 50,
|
||||
"fightMochiFightCount": 666,
|
||||
"shotCount": 333,
|
||||
"loveMochiBirthCount": 721,
|
||||
"statsButtonPressCount": 30,
|
||||
"totalDeathCount": 10000,
|
||||
"manualTarinaiAddedCount": 100,
|
||||
"robotCleanCount": 200
|
||||
},
|
||||
"definitions": [
|
||||
{
|
||||
"id": "first_birth",
|
||||
"title": "はじめての繁殖",
|
||||
"description": "たりないの繁殖を1回成功させる。",
|
||||
"category": "population",
|
||||
"showLockedDescription": true,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "birthCount",
|
||||
"target": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "natural_zunchi_slave",
|
||||
"title": "ずんちどれい",
|
||||
"description": "ずんちどれいが出現する。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "ochibi_natural_slave",
|
||||
"title": "たりないより「たりない」",
|
||||
"description": "おちびが鎖無しでずんちどれいになる。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "natural_tarinai_king",
|
||||
"title": "たりない王",
|
||||
"description": "たりない王が出現する。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "self_zunchi_death",
|
||||
"title": "あぁ^~ たまらねぇぜ",
|
||||
"description": "おしり鋲を抜いた時、自分のずんちで自爆する。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "zunchi_collision_death",
|
||||
"title": "くちゃいのだ!",
|
||||
"description": "ずんちとの衝突で死亡する個体が出現する。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "natural_zunchi_slave_5_generations",
|
||||
"title": "末代までの恥",
|
||||
"description": "5代連続でずんちどれい(鎖なし)",
|
||||
"category": "population",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "natural_tarinai_king_3_generations",
|
||||
"title": "華麗なる王族",
|
||||
"description": "3代連続でたりない王(王冠なし)",
|
||||
"category": "population",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "death_50_in_10_seconds",
|
||||
"title": "6番目の大量絶滅",
|
||||
"description": "10秒以内に50体のたりないが死亡する。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "birth_50_in_60_seconds",
|
||||
"title": "ベビーブーム",
|
||||
"description": "1分以内に30体のたりないが誕生する。",
|
||||
"category": "population",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "great_mother_1000_births",
|
||||
"title": "母なる大地",
|
||||
"description": "たりないを繁殖で累計3333体誕生させる。",
|
||||
"category": "population",
|
||||
"showLockedDescription": true,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "birthCount",
|
||||
"target": 3333,
|
||||
"display": "percent",
|
||||
"unit": "体"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lifespan_completed",
|
||||
"title": "天寿",
|
||||
"description": "たりない1体が天寿を全うする。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "soccer_ball_death",
|
||||
"title": "豆野、殺ッカーやろうぜ!",
|
||||
"description": "サッカーボールとの衝突で、たりない1体が死亡する。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "fight_pair_danger_kill",
|
||||
"title": "喧嘩両成敗",
|
||||
"description": "喧嘩中のたりないを、両方とも危険カテゴリのアイテムで倒す。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "all_non_sleep_diseased_25",
|
||||
"title": "緊急事態宣言",
|
||||
"description": "25体以上が存命し、フィールド上の全員がねむり病以外の病気にかかる。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "colony_happy",
|
||||
"title": "管理された楽園",
|
||||
"description": "コロニー状態をしばらく「幸福」にする。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "direct_feed_33",
|
||||
"title": "餌やりじいさん",
|
||||
"description": "手から直接33回餌を与える。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": false,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "directFeedCount",
|
||||
"target": 33
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ignite_during_birth_ritual",
|
||||
"title": "燃え上がる恋",
|
||||
"description": "たりないがヘコヘコ中に着火する。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "laxative_starvation",
|
||||
"title": "内臓全部出た",
|
||||
"description": "下剤の効果中に餓死する。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "idle_observer_5_minutes",
|
||||
"title": "監視カメラ",
|
||||
"description": "3ゲーム日、操作せず画面を見るだけ",
|
||||
"category": "operation",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "placed_objects_100",
|
||||
"title": "物でいっぱい",
|
||||
"description": "配置物を累計100個置く。",
|
||||
"category": "construction",
|
||||
"showLockedDescription": false,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "placementCount",
|
||||
"target": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mechanized_industry",
|
||||
"title": "機械化産業",
|
||||
"description": "検知器の信号で接続先を作動させ、ロープ・棒・ばねをそれぞれ1本以上設置する。",
|
||||
"category": "construction",
|
||||
"showLockedDescription": false,
|
||||
"progress": {
|
||||
"type": "mechanized"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "safe_colony_25_5_minutes",
|
||||
"title": "安全第一",
|
||||
"description": "25体以上いる状態をゲーム内5分間維持し、その間寿命以外で死亡者を出さない。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "ants_alive_25",
|
||||
"title": "アリの惑星",
|
||||
"description": "生存しているアリが25体に到達する。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "ants_killed_100",
|
||||
"title": "害虫駆除",
|
||||
"description": "アリを累計100体倒す。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": false,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "antKills",
|
||||
"target": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "secret_collection_9_slots",
|
||||
"title": "秘密のコレクション",
|
||||
"description": "セーブスロット9つをすべて埋める。",
|
||||
"category": "operation",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "pause_spam_4_in_1_second",
|
||||
"title": "何やってるの?",
|
||||
"description": "何もない部分を1秒間に4回連打する。",
|
||||
"category": "operation",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "continuous_play_1_hour",
|
||||
"title": "めっちゃたりない観察",
|
||||
"description": "1時間連続プレイする。",
|
||||
"category": "other",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "below_absolute_zero_item",
|
||||
"title": "物理法則を下側に超越",
|
||||
"description": "温度計算されるアイテムを1つ絶対零度を下回る温度にする。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "undo_mass_revival",
|
||||
"title": "見なかったことにしよう",
|
||||
"description": "一度のUndoで10体以上の死亡をなかったことにする。",
|
||||
"category": "operation",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "robot_cleaner_100",
|
||||
"title": "清掃業者",
|
||||
"description": "ロボ掃除機にずんち・死骸・汚れを累計200個処理させる。",
|
||||
"category": "construction",
|
||||
"showLockedDescription": true,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "robotCleanCount",
|
||||
"target": 200,
|
||||
"display": "count",
|
||||
"unit": "個"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "held_30_seconds",
|
||||
"title": "空中生活",
|
||||
"description": "同じたりないを30秒間つまみ続ける。",
|
||||
"category": "operation",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "minimalist_happy",
|
||||
"title": "ミニマリスト",
|
||||
"description": "プレイ開始6日目以降、配置物を10個以下に抑えてコロニー状態を「幸福」にする。",
|
||||
"category": "construction",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "overprotective",
|
||||
"title": "過保護",
|
||||
"description": "同じたりないに手から10回餌を与え、3回以上薬で治療する。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "self_sufficient",
|
||||
"title": "自給自足",
|
||||
"description": "複製機がない状態で、20体以上を直接給餌せず10分間維持する。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "unplanned_city_30",
|
||||
"title": "無計画都市",
|
||||
"description": "配置後30秒以内に削除する操作を累計30回行う。",
|
||||
"category": "construction",
|
||||
"showLockedDescription": false,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "quickDeleteCount",
|
||||
"target": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sauna_cold_plunge",
|
||||
"title": "サウナ水風呂",
|
||||
"description": "暑すぎる状態になったたりないを、15秒以内に寒すぎる状態にする。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "rain_shelter_all",
|
||||
"title": "雨宿り完了",
|
||||
"description": "雨天時、20体以上の全員をパラソル・巣箱・土管で雨宿りさせる。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "medicine_ledger_all",
|
||||
"title": "おくすり手帳全埋め",
|
||||
"description": "全種類の薬を少なくとも1回使用する。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": false,
|
||||
"progress": {
|
||||
"type": "set",
|
||||
"key": "medicineTypes",
|
||||
"required": "medicineLedgerTypes"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mercury_lifespan",
|
||||
"title": "不老不死まであと一歩",
|
||||
"description": "水銀を使用したたりないが天寿を全うする。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "enemy_enemy_friend",
|
||||
"title": "敵の敵は味方",
|
||||
"description": "たりない30体以上を無傷で保ちながら、危険アイテムでアリを10体倒す。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "revolution",
|
||||
"title": "革命",
|
||||
"description": "ずんちどれいが、たりない王を喧嘩で死亡させる。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "fuel_to_fire",
|
||||
"title": "火に油",
|
||||
"description": "けんか餅を与えたたりないが30秒以内に喧嘩を始める。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "undo_20",
|
||||
"title": "やっぱなし",
|
||||
"description": "Undoを累計20回成功させる。",
|
||||
"category": "operation",
|
||||
"showLockedDescription": false,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "undoCount",
|
||||
"target": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "redo_20",
|
||||
"title": "やっぱあり",
|
||||
"description": "Redoを累計20回成功させる。",
|
||||
"category": "operation",
|
||||
"showLockedDescription": false,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "redoCount",
|
||||
"target": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "eternal_history_generation_10",
|
||||
"title": "悠久の歴史",
|
||||
"description": "世代が10に到達する。",
|
||||
"category": "population",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "king_full_satisfaction",
|
||||
"title": "五体満足",
|
||||
"description": "たりない王1個体にプロテインと巨大薬の効果を同時に付与する。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "slave_zero_satisfaction",
|
||||
"title": "零体満足",
|
||||
"description": "ずんちどれい1個体にンイテロプと矮小薬の効果を同時に付与する。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "town_doctor_50",
|
||||
"title": "町のお医者さん",
|
||||
"description": "救急薬を累計50回使用し、体力を回復させる。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": true,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "firstAidHeals",
|
||||
"target": 50,
|
||||
"display": "count",
|
||||
"unit": "回"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "poke_plushie_fling",
|
||||
"title": "割とふわふわ",
|
||||
"description": "ぬいぐるみを「つつく」で弾き飛ばす。",
|
||||
"category": "operation",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "chaos_seeker_666_fights",
|
||||
"title": "混沌ヲ希求スル者",
|
||||
"description": "けんか餅の影響下で喧嘩を累計666回発生させる。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": true,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "fightMochiFightCount",
|
||||
"target": 666,
|
||||
"display": "percent",
|
||||
"unit": "回"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "clean_freak_robot_only",
|
||||
"title": "潔癖症",
|
||||
"description": "ロボット掃除機の設定全項目にチェックを入れて稼働させ、フィールド上にロボット掃除機以外を存在させない。",
|
||||
"category": "construction",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "true_tarinai_observer",
|
||||
"title": "真・たりない観察",
|
||||
"description": "これ以外の全ての実績を達成する。",
|
||||
"category": "other",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "sniper_333_shots",
|
||||
"title": "狙撃手",
|
||||
"description": "累計333発撃つ。",
|
||||
"category": "operation",
|
||||
"showLockedDescription": false,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "shotCount",
|
||||
"target": 333
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "megalopolis",
|
||||
"title": "メガロポリス",
|
||||
"description": "複製機を10個以上、干草寝床・巣箱・土管を合計15個以上設置し、たりないを100体以上生存させる。",
|
||||
"category": "construction",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "fertility_seeker_721_love_births",
|
||||
"title": "豊穣ヲ希求スル者",
|
||||
"description": "へこ餅の影響下で繁殖を721回成功させる。",
|
||||
"category": "population",
|
||||
"showLockedDescription": true,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "loveMochiBirthCount",
|
||||
"target": 721,
|
||||
"display": "percent",
|
||||
"unit": "回"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "low_fps_single_digit",
|
||||
"title": "スペックがたりない",
|
||||
"description": "fpsを1桁にする。",
|
||||
"category": "other",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "continuous_play_24_hours",
|
||||
"title": "寝ろ",
|
||||
"description": "24時間連続でプレイする。",
|
||||
"category": "other",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "sandbox_five_toilets",
|
||||
"title": "砂場",
|
||||
"description": "トイレの砂をフィールドに5個設置する。",
|
||||
"category": "construction",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "park_ground_changed",
|
||||
"title": "そこ公共空間だよ?",
|
||||
"description": "公園の地面を勝手に変更する。",
|
||||
"category": "construction",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "ground_change_4_in_1_second",
|
||||
"title": "破格の工事費用",
|
||||
"description": "地面を変えすぎて破産する。",
|
||||
"category": "construction",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "ant_nest_without_tarinai",
|
||||
"title": "アリ観察",
|
||||
"description": "アリの巣があるが、たりないがいない。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "sticky_bomb_15_passes",
|
||||
"title": "命のバトン",
|
||||
"description": "粘着ボムを爆発までに15回以上パスする。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "daily_play_7_days",
|
||||
"title": "毎日たりない観察",
|
||||
"description": "7日連続でプレイする。",
|
||||
"category": "other",
|
||||
"showLockedDescription": true,
|
||||
"progress": {
|
||||
"type": "value",
|
||||
"key": "dailyPlayStreak",
|
||||
"target": 7,
|
||||
"display": "percent",
|
||||
"unit": "日"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wire_shock_7_tarinai",
|
||||
"title": "抵抗器じゃない",
|
||||
"description": "電流が流れている1本の電線で7体のたりないを感電させる。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "information_industry",
|
||||
"title": "情報化産業",
|
||||
"description": "基盤同士を電線類でつなぎ、信号を流す。",
|
||||
"category": "construction",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "strength_in_numbers",
|
||||
"title": "数こそ力",
|
||||
"description": "生存しているたりないが100体に到達する。",
|
||||
"category": "population",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "elite_few",
|
||||
"title": "少数精鋭",
|
||||
"description": "たりないが25体以下の状態を5日間維持する。",
|
||||
"category": "population",
|
||||
"showLockedDescription": true
|
||||
},
|
||||
{
|
||||
"id": "zunchi_overflow",
|
||||
"title": "踩屎感",
|
||||
"description": "たりないが20体以上いる状態で、ずんちの数がたりないの数を上回る。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "comfortable_beds",
|
||||
"title": "快適な寝床",
|
||||
"description": "生存している全てのたりないに行き渡る数の寝床を用意する。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "stone_pillow",
|
||||
"title": "石に枕す",
|
||||
"description": "たりないが30体以上いる状態で、寝床充足率を0%にする。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "across_seasons",
|
||||
"title": "季節を越えて",
|
||||
"description": "春夏秋冬の四季を一巡する。",
|
||||
"category": "other",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "favorite_one",
|
||||
"title": "推し",
|
||||
"description": "ある個体をお気に入りに登録する。",
|
||||
"category": "ecology",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "statistician",
|
||||
"title": "統計学者",
|
||||
"description": "統計の個体数・欲求・環境・グラフ表示切替ボタンを累計30回押す。",
|
||||
"category": "operation",
|
||||
"showLockedDescription": false,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "statsButtonPressCount",
|
||||
"target": 30,
|
||||
"display": "count",
|
||||
"unit": "回"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "well_informed",
|
||||
"title": "情報通",
|
||||
"description": "プッシュ通知をONにする。",
|
||||
"category": "operation",
|
||||
"showLockedDescription": false
|
||||
},
|
||||
{
|
||||
"id": "lively_making",
|
||||
"title": "賑わいづくり",
|
||||
"description": "「たりないを追加」で100体追加する。",
|
||||
"category": "population",
|
||||
"showLockedDescription": true,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "manualTarinaiAddedCount",
|
||||
"target": 100,
|
||||
"display": "count",
|
||||
"unit": "体"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "memento_mori",
|
||||
"title": "多数の死は統計上の数字でしかない",
|
||||
"description": "たりないが合計10000体死亡する。",
|
||||
"category": "population",
|
||||
"showLockedDescription": true,
|
||||
"progress": {
|
||||
"type": "counter",
|
||||
"key": "totalDeathCount",
|
||||
"target": 10000,
|
||||
"display": "percent",
|
||||
"unit": "体"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mad_scientist",
|
||||
"title": "マッドサイエンティスト",
|
||||
"description": "異なる10匹のたりないに怪しい薬を投与する。",
|
||||
"category": "experiment",
|
||||
"showLockedDescription": false,
|
||||
"progress": {
|
||||
"type": "unique",
|
||||
"key": "mysteryDrugTarinaiIds",
|
||||
"target": 10,
|
||||
"display": "count",
|
||||
"unit": "匹"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
1993
js/achievements.js
14
js/ants.js
|
|
@ -100,7 +100,7 @@ class AntActor {
|
|||
this.targetRef = null;
|
||||
this.burnWanderAngle = rand(0, Math.PI * 2);
|
||||
this.hpBarTimer = Math.max(this.hpBarTimer || 0, 1.0);
|
||||
this.world?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(10, this.r * 2.8), life: 0.18, color: "rgba(255,110,36,0.56)" }));
|
||||
this.world?.queueEffect?.("ring", this.x, this.y, { size: Math.max(10, this.r * 2.8), life: 0.18, color: "rgba(255,110,36,0.56)" });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ class AntActor {
|
|||
if (!this.burning && !(this.burnTimer > 0)) return false;
|
||||
this.burning = false;
|
||||
this.burnTimer = 0;
|
||||
this.world?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(9, this.r * 2.2), life: 0.16, color: "rgba(108,192,236,0.58)" }));
|
||||
this.world?.queueEffect?.("ring", this.x, this.y, { size: Math.max(9, this.r * 2.2), life: 0.16, color: "rgba(108,192,236,0.58)" });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ class AntActor {
|
|||
if (distXY(this.x, this.y, it.x, it.y) <= contactRadius + Math.max(it.r || 16, 12)) globalThis.TarinaiItemDynamicToolSystem?.igniteGrassByFire?.(it, this, this.world, dt);
|
||||
}
|
||||
if (deterministicChance(this.world, "ant-fire-effect", dt * 2.0, this)) {
|
||||
this.world?.effects?.push(new Effect("flame", this.x, this.y - this.r, { vx: rand(-2, 2), vy: rand(-14, -6), size: Math.max(4, this.r * 1.0), life: 0.22, color: "rgba(236,46,24,0.72)" }));
|
||||
this.world?.queueEffect?.("flame", this.x, this.y - this.r, { vx: rand(-2, 2), vy: rand(-14, -6), size: Math.max(4, this.r * 1.0), life: 0.22, color: "rgba(236,46,24,0.72)" });
|
||||
}
|
||||
if (this.burnTimer <= 0.01) this.extinguishFire("timeout");
|
||||
return true;
|
||||
|
|
@ -300,6 +300,7 @@ class AntActor {
|
|||
updateSearch(dt, home) {
|
||||
const target = this.chooseTarget(52);
|
||||
if (target) {
|
||||
const alreadyCaptured = Boolean((target.antDraggedTimer || 0) > 0 && target.antDraggedBy);
|
||||
this.state = "drag";
|
||||
this.targetId = target.id;
|
||||
this.targetToken = target.liveToken || 0;
|
||||
|
|
@ -310,6 +311,7 @@ class AntActor {
|
|||
target.fearTimer = Math.max(target.fearTimer || 0, 0.55);
|
||||
target.thought = "\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b";
|
||||
target.recordChangeCause?.("\u30a2\u30ea\u306b\u904b\u3070\u308c\u305f", "\u72b6\u614b");
|
||||
if (!alreadyCaptured) this.world?.log?.(`${target.name}\u304c\u30a2\u30ea\u306b\u6355\u307e\u3063\u305f\u3002`, "event", { participants: [target] });
|
||||
audio.antDrag?.();
|
||||
this.world.drawListDirty = true;
|
||||
return;
|
||||
|
|
@ -416,10 +418,6 @@ class AntActor {
|
|||
globalThis.TarinaiAchievements?.recordAntDamageSource?.(this, target, { world: this.world, reason: "tarinai_struggle" });
|
||||
this.hp -= dt * struggle;
|
||||
|
||||
if ((this.world.time || 0) - (this.carryLogAt || -999) > 8 && Math.random() < dt * 0.04) {
|
||||
this.carryLogAt = this.world.time || 0;
|
||||
this.world.log(`${target.name}\u304c\u30a2\u30ea\u306b\u5f15\u3063\u5f35\u3089\u308c\u3066\u3044\u308b\u3002`, "event", { participants: [target] });
|
||||
}
|
||||
if (this.hp <= 0) this.dieAsCorpse(home);
|
||||
}
|
||||
|
||||
|
|
@ -431,7 +429,7 @@ class AntActor {
|
|||
const corpse = new Item("ant_corpse", this.x, this.y);
|
||||
corpse.amount = 24;
|
||||
this.world.addItem?.(corpse, "ant-corpse");
|
||||
this.world.effects?.push(new Effect("zunchi_miasma", this.x, this.y - 4, { size: 10, life: 0.44, color: "rgba(82,61,42,0.28)" }));
|
||||
this.world.queueEffect?.("zunchi_miasma", this.x, this.y - 4, { size: 10, life: 0.44, color: "rgba(82,61,42,0.28)" });
|
||||
if (home) syncAntNestCount(this.world, home);
|
||||
this.world.ensureItemBuckets?.("ant-corpse");
|
||||
this.world.drawListDirty = true;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ const DECOR_ASSETS = [
|
|||
{ id: "oshibyo", path: "assets/objects/oshibyo.webp" },
|
||||
{ id: "oshibyo_stuck", path: "assets/objects/oshibyo_stuck.webp" },
|
||||
{ id: "plushie_bear", path: "assets/objects/plushie_bear.png" },
|
||||
{ id: "acquired_tarinai_overlay", path: "assets/sprites/tarinai_acquired_overlay.png" },
|
||||
{ id: "mamekusaredake", path: "assets/objects/mamekusaredake.png" },
|
||||
];
|
||||
|
||||
const ALPHA_BOUNDS = {
|
||||
|
|
@ -45,6 +47,7 @@ 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 },
|
||||
intimidate_alt: { x: 45, y: 48, w: 422, h: 379, imageW: 512, imageH: 468 },
|
||||
zunchi_slave: { x: 17, y: 22, w: 478, 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 },
|
||||
|
|
@ -77,8 +80,8 @@ let imageLoadTimeoutsTotal = 0;
|
|||
const SUNBATH_IMAGE_IDS = new Set(SPRITES
|
||||
.map(asset => asset?.id || "")
|
||||
.filter(id => String(id).startsWith("sunbath")));
|
||||
const INITIAL_IMAGE_IDS = new Set(["smile", "hungry_70", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
|
||||
const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "weak", "hurt", "hurt2", "fear", "flee", "flee_fear2", "fear_blue", "fear_cry", "sleep2", "stress_dizzy", "stress_sweat", "intimidate", "birth_ritual", "zunchi_slave", "zunchi_slave_alt", "hungry_70", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
|
||||
const INITIAL_IMAGE_IDS = new Set(["smile", "sleep", "hungry_70", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
|
||||
const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "weak", "hurt", "hurt2", "fear", "flee", "flee_fear2", "fear_blue", "fear_cry", "sleep2", "stress_dizzy", "stress_sweat", "intimidate", "intimidate_alt", "birth_ritual", "zunchi_slave", "zunchi_slave_alt", "hungry_70", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
|
||||
|
||||
function trimCanvasCache(cache, limit = 128) {
|
||||
while (cache.size > limit) {
|
||||
|
|
|
|||
|
|
@ -297,6 +297,7 @@ const audio = {
|
|||
explode() { if (!this.playSample("sfx_firecracker_explode", 0.30, "ops")) this.play("sfx_firecracker_explode", { category: "ops", minGap: 0.30 }); },
|
||||
phase() { this.play("sfx_phase", { category: "notify", minGap: 1.0 }); },
|
||||
notify() { this.play("sfx_notify", { category: "notify", minGap: 0.12 }); },
|
||||
achievementUnlock() { this.ensure(); this.play("sfx_achievement_unlock", { category: "notify", minGap: 0.35 }); },
|
||||
uiClick() { this.play("sfx_ui_click", { category: "ops", minGap: 0.035 }); },
|
||||
uiFold() { this.play("sfx_ui_fold", { category: "ops", minGap: 0.08 }); },
|
||||
place(type = "") {
|
||||
|
|
@ -313,6 +314,7 @@ const audio = {
|
|||
waterHose() { this.play("sfx_water_hose", { category: "ops", minGap: 0.20 }); },
|
||||
shoot() { this.play("sfx_shoot", { category: "ops", minGap: 0.045 }); },
|
||||
boltAction() { this.play("sfx_bolt_action", { category: "ops", minGap: 0.10 }); },
|
||||
bell() { if (!this.playSample("sfx_bell", 0.16, "ops")) this.play("sfx_bell", { category: "ops", minGap: 0.16 }); },
|
||||
ballHit() { this.play("sfx_ball_hit", { category: "ops", minGap: 0.08 }); },
|
||||
stoneImpact() { this.play("sfx_stone_impact", { category: "ops", minGap: 0.18 }); },
|
||||
genkotsuImpact() { this.play("sfx_genkotsu_impact", { category: "ops", minGap: 0.20 }); setTimeout(() => this.play("sfx_ground_rumble", { category: "ops", minGap: 0.20 }), 80); },
|
||||
|
|
|
|||
|
|
@ -395,13 +395,23 @@
|
|||
result.boardId = board.id || "";
|
||||
result.direction = port.direction;
|
||||
result.signalActive = port.direction === "output" ? Boolean(board._circuitOutputState?.[port.id]) : Boolean(board._circuitInputState?.[port.id]);
|
||||
result._signalDetectorSources = port.direction === "output"
|
||||
? [...(board._circuitOutputDetectorSources?.[port.id] || [])]
|
||||
: [...(board._circuitInputDetectorSources?.[port.id] || [])];
|
||||
return result;
|
||||
}
|
||||
|
||||
function evaluate(board, inputValues = null) {
|
||||
function evaluate(board, inputValues = null, inputProvenance = null) {
|
||||
const config = ensureConfig(board);
|
||||
const inputs = Object.create(null);
|
||||
for (const port of config.inputs) inputs[port.id] = Boolean(inputValues ? inputValues[port.id] : board?._circuitInputState?.[port.id]);
|
||||
const inputDetectorSources = Object.create(null);
|
||||
for (const port of config.inputs) {
|
||||
inputs[port.id] = Boolean(inputValues ? inputValues[port.id] : board?._circuitInputState?.[port.id]);
|
||||
const rawSources = inputProvenance ? inputProvenance[port.id] : board?._circuitInputDetectorSources?.[port.id];
|
||||
inputDetectorSources[port.id] = inputs[port.id]
|
||||
? [...new Set(Array.isArray(rawSources) ? rawSources.filter(Boolean) : [])]
|
||||
: [];
|
||||
}
|
||||
|
||||
const parent = new Map();
|
||||
const rank = new Map();
|
||||
|
|
@ -511,18 +521,74 @@
|
|||
return Boolean(value);
|
||||
}
|
||||
|
||||
const netDetectorSources = new Map();
|
||||
const nodeDetectorSources = Object.create(null);
|
||||
const visitingSourceNodes = new Set();
|
||||
const visitingSourceNets = new Set();
|
||||
const unionSources = (...lists) => [...new Set(lists.flatMap(list => Array.isArray(list) ? list : []))];
|
||||
|
||||
function driverDetectorSources(token) {
|
||||
if (token.startsWith("i:")) {
|
||||
const id = token.slice(2);
|
||||
return inputs[id] ? inputDetectorSources[id] || [] : [];
|
||||
}
|
||||
const match = /^n:([^:]+):o$/.exec(token);
|
||||
return match ? nodeSourceValue(match[1]) : [];
|
||||
}
|
||||
|
||||
function netSourceValue(token) {
|
||||
const root = find(token);
|
||||
if (netDetectorSources.has(root)) return netDetectorSources.get(root);
|
||||
if (visitingSourceNets.has(root)) return [];
|
||||
visitingSourceNets.add(root);
|
||||
let sources = [];
|
||||
for (const member of members.get(root) || [token]) {
|
||||
if ((member.startsWith("i:") || /^n:[^:]+:o$/.test(member)) && driverValue(member)) {
|
||||
sources = unionSources(sources, driverDetectorSources(member));
|
||||
}
|
||||
}
|
||||
visitingSourceNets.delete(root);
|
||||
netDetectorSources.set(root, sources);
|
||||
return sources;
|
||||
}
|
||||
|
||||
function nodeSourceValue(id) {
|
||||
if (Object.prototype.hasOwnProperty.call(nodeDetectorSources, id)) return nodeDetectorSources[id];
|
||||
if (visitingSourceNodes.has(id) || !nodeValue(id)) return [];
|
||||
const node = nodeMap.get(id);
|
||||
if (!node || unstableNodes.has(id) || node.type === "on" || node.type === "not") {
|
||||
nodeDetectorSources[id] = [];
|
||||
return [];
|
||||
}
|
||||
visitingSourceNodes.add(id);
|
||||
let sources = netSourceValue(`n:${id}:a`);
|
||||
if (node.type === "and") sources = unionSources(sources, netSourceValue(`n:${id}:b`));
|
||||
visitingSourceNodes.delete(id);
|
||||
nodeDetectorSources[id] = sources;
|
||||
return sources;
|
||||
}
|
||||
|
||||
const outputs = Object.create(null);
|
||||
for (const port of config.outputs) outputs[port.id] = netValue(`o:${port.id}`);
|
||||
const outputDetectorSources = Object.create(null);
|
||||
for (const port of config.outputs) {
|
||||
outputs[port.id] = netValue(`o:${port.id}`);
|
||||
outputDetectorSources[port.id] = outputs[port.id] ? netSourceValue(`o:${port.id}`) : [];
|
||||
}
|
||||
if (board) {
|
||||
board._circuitInputState = inputs;
|
||||
board._circuitInputDetectorSources = inputDetectorSources;
|
||||
board._circuitNodeState = nodeValues;
|
||||
board._circuitOutputState = outputs;
|
||||
board._circuitOutputDetectorSources = outputDetectorSources;
|
||||
for (const port of config.outputs) {
|
||||
const term = terminal(board, `o:${port.id}`);
|
||||
if (term) term.signalActive = Boolean(outputs[port.id]);
|
||||
if (term) {
|
||||
term.signalActive = Boolean(outputs[port.id]);
|
||||
term._signalDetectorSources = [...(outputDetectorSources[port.id] || [])];
|
||||
}
|
||||
}
|
||||
}
|
||||
return { inputs, nodes: nodeValues, outputs };
|
||||
return { inputs, nodes: nodeValues, outputs, inputDetectorSources, outputDetectorSources };
|
||||
}
|
||||
|
||||
function nearestPin(config, x, y, maxDistance = 17) {
|
||||
|
|
@ -565,8 +631,6 @@
|
|||
return null;
|
||||
}
|
||||
|
||||
const distanceToSegment = (px, py, ax, ay, bx, by) => global.TarinaiGeometry.pointSegmentDistance(px, py, ax, ay, bx, by);
|
||||
|
||||
function wirePath(config, wire) {
|
||||
const a = endpointPosition(config, wire.a);
|
||||
const b = endpointPosition(config, wire.b);
|
||||
|
|
@ -579,7 +643,7 @@
|
|||
for (let i = config.wires.length - 1; i >= 0; i -= 1) {
|
||||
const path = wirePath(config, config.wires[i]);
|
||||
if (path.length < 2) continue;
|
||||
const d = distanceToSegment(x, y, path[0].x, path[0].y, path[1].x, path[1].y);
|
||||
const d = global.TarinaiGeometry.pointSegmentDistance(x, y, path[0].x, path[0].y, path[1].x, path[1].y);
|
||||
if (d <= bestD) { bestD = d; best = { wire: config.wires[i], index: i, distance: d }; }
|
||||
}
|
||||
return best;
|
||||
|
|
@ -608,7 +672,7 @@
|
|||
}
|
||||
|
||||
function pointOnSegment(point, a, b) {
|
||||
return distanceToSegment(point.x, point.y, a.x, a.y, b.x, b.y) < 0.6
|
||||
return global.TarinaiGeometry.pointSegmentDistance(point.x, point.y, a.x, a.y, b.x, b.y) < 0.6
|
||||
&& point.x >= Math.min(a.x, b.x) - 0.6 && point.x <= Math.max(a.x, b.x) + 0.6
|
||||
&& point.y >= Math.min(a.y, b.y) - 0.6 && point.y <= Math.max(a.y, b.y) + 0.6;
|
||||
}
|
||||
|
|
@ -700,7 +764,11 @@
|
|||
dialog = document.createElement("div");
|
||||
dialog.id = "circuitBoardEditor";
|
||||
dialog.className = "rotator-editor circuit-grid-editor hidden";
|
||||
dialog.innerHTML = `<div class="rotator-editor-panel circuit-grid-panel" role="dialog" aria-modal="true" aria-labelledby="circuitBoardTitle">
|
||||
dialog.setAttribute("role", "dialog");
|
||||
dialog.setAttribute("aria-modal", "true");
|
||||
dialog.setAttribute("aria-labelledby", "circuitBoardTitle");
|
||||
dialog.setAttribute("aria-hidden", "true");
|
||||
dialog.innerHTML = `<div class="rotator-editor-panel circuit-grid-panel">
|
||||
<div class="rotator-editor-titlebar"><h2 id="circuitBoardTitle">\u57fa\u76e4</h2><button class="rotator-editor-close" type="button" data-action="close" aria-label="\u9589\u3058\u308b">\u00d7</button></div>
|
||||
<div class="circuit-port-count-controls">
|
||||
<label>\u6a2a\u5e45 <button type="button" data-grid-delta="cols:-1">\u2212</button><input id="circuitGridCols" type="number" min="${MIN_COLS}" max="${MAX_COLS}" step="1"><button type="button" data-grid-delta="cols:1">\uff0b</button></label>
|
||||
|
|
@ -1091,20 +1159,27 @@
|
|||
dialog.querySelector("[data-action=clear]").onclick = () => { snapshot(); draft.nodes = []; draft.wires = []; draw(); };
|
||||
dialog.querySelector("[data-action=reset]").onclick = () => { snapshot(); draft = defaultConfig(); syncControls(); resizeCanvas(); draw(); };
|
||||
const close = () => {
|
||||
dialog.classList.add("hidden");
|
||||
if (global.TarinaiManagedDialogs?.close) global.TarinaiManagedDialogs.close(dialog);
|
||||
else { dialog.classList.add("hidden"); dialog.setAttribute("aria-hidden", "true"); }
|
||||
document.removeEventListener("keydown", handleEditorKeydown, true);
|
||||
canvas.onpointerdown = null; canvas.onpointermove = null; canvas.onpointerup = null; canvas.onpointercancel = null; canvas.onpointerleave = null; canvas.oncontextmenu = null;
|
||||
};
|
||||
dialog.querySelector("[data-action=close]").onclick = close;
|
||||
dialog.querySelector("[data-action=cancel]").onclick = close;
|
||||
if (dialog._tarinaiCloseRequestHandler) dialog.removeEventListener("tarinai:dialog-close-request", dialog._tarinaiCloseRequestHandler);
|
||||
dialog._tarinaiCloseRequestHandler = close;
|
||||
dialog.addEventListener("tarinai:dialog-close-request", close);
|
||||
dialog.querySelector("[data-action=save]").onclick = () => {
|
||||
applySerializedConfig(board, draft); evaluate(board); remapAttachedWireEndpoints(board, worldRef);
|
||||
if (worldRef) { worldRef.drawListDirty = true; worldRef.markSpatialDirty?.("circuit-board-edited"); worldRef.log?.("\u57fa\u76e4\u306e\u56de\u8def\u3092\u7d44\u307f\u66ff\u3048\u305f\u3002", "observe"); }
|
||||
if (worldRef) { worldRef.drawListDirty = true; worldRef.markSpatialDirty?.("circuit-board-edited"); }
|
||||
global.showToast?.("\u57fa\u76e4\u306e\u56de\u8def\u3092\u53cd\u6620\u3057\u307e\u3057\u305f\u3002");
|
||||
global.render?.(); close();
|
||||
};
|
||||
dialog.onclick = event => { if (event.target === dialog) close(); };
|
||||
syncControls(); resizeCanvas(); setTool("wire"); dialog.classList.remove("hidden"); draw();
|
||||
syncControls(); resizeCanvas(); setTool("wire");
|
||||
if (global.TarinaiManagedDialogs?.open) global.TarinaiManagedDialogs.open(dialog, document.activeElement);
|
||||
else { dialog.classList.remove("hidden"); dialog.setAttribute("aria-hidden", "false"); }
|
||||
draw();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
|
||||
function isSoftPlacementIgnoredType(type = "") {
|
||||
const key = String(type || "");
|
||||
return key === "zunchi" || key === "grass_bed" || key === "grass" || key === "water";
|
||||
return key === "zunchi" || key === "grass_bed" || key === "grass" || key === "water" || key === "grave";
|
||||
}
|
||||
|
||||
function placementRectsFor(worldRef, item) {
|
||||
|
|
|
|||
|
|
@ -55,11 +55,18 @@
|
|||
const crisisCollapseSignal = m.weakRatio >= 0.70 && m.criticalRatio >= 0.60;
|
||||
add("devastation", 120, m.n <= 10 && devastationSignal);
|
||||
add("crisis", 110, !m.populationIncreasing && ((crisisMortalitySignal && crisisSystemicStress) || crisisCollapseSignal));
|
||||
add("confusion", 100, (m.recentShock && (m.panicRatio >= 0.10 || m.safetyRatio >= 0.18)) || m.panicRatio >= 0.25);
|
||||
const confusionPanicThreshold = Math.max(4, Math.ceil(m.n * 0.32));
|
||||
const confusionShockPanicThreshold = Math.max(3, Math.ceil(m.n * 0.18));
|
||||
const confusionShockSafetyThreshold = Math.max(5, Math.ceil(m.n * 0.30));
|
||||
const confusionSignal = m.panicCount >= confusionPanicThreshold
|
||||
|| (m.recentShock && m.panicCount >= confusionShockPanicThreshold && m.safetyHighCount >= confusionShockSafetyThreshold);
|
||||
const breedingThreshold = Math.max(4, Math.ceil(m.n * 0.18));
|
||||
const breedingSignal = m.breedingCount >= breedingThreshold && m.breedingRatio >= 0.18;
|
||||
add("confusion", 100, confusionSignal);
|
||||
add("fearful", 90, (m.avgSafety >= 50 && m.safetyRatio >= 0.28) || (m.recentShock && m.safetyRatio >= 0.18));
|
||||
add("disease_spread", 82, m.diseaseRatio >= 0.25 || (m.diseasedCount >= 3 && m.diseaseRatio >= 0.16));
|
||||
add("weakened", 74, !m.populationIncreasing && ((m.avgEnergy < 54 && m.weakRatio >= 0.18) || m.avgHunger > 82 || m.weakRatio >= 0.34));
|
||||
add("breeding", 66, m.breedingRatio >= 0.10 || m.breedingCount >= 2 || m.now - (worldRef?.lastBirthAt || -999) <= 45);
|
||||
add("breeding", 66, breedingSignal);
|
||||
add("happy", 46, m.avgStress < 28 && m.panicRatio < 0.08 && m.diseaseRatio < 0.12 && m.weakRatio < 0.18);
|
||||
add("isolated", 28, m.n <= 1);
|
||||
if (!candidates.length) return "relaxed";
|
||||
|
|
|
|||
|
|
@ -102,7 +102,6 @@
|
|||
|
||||
function normalizeToolPickMode(mode = "free") {
|
||||
const value = String(mode || "free").toLowerCase();
|
||||
if (value === "grid") return "grid40"; // compatibility with v39.16.34
|
||||
return DEFAULT_PICK_MODE_ORDER.includes(value) ? value : "free";
|
||||
}
|
||||
|
||||
|
|
@ -139,6 +138,15 @@
|
|||
return ok({ speed: worldRef.speed });
|
||||
}
|
||||
|
||||
function setSpeed(worldRef, speed, allowed = [0.5, 1, 2, 4, 8, 16]) {
|
||||
if (!worldRef) return fail("missing_world");
|
||||
const value = Number(speed);
|
||||
const list = Array.isArray(allowed) ? allowed.map(Number).filter(Number.isFinite) : [0.5, 1, 2, 4, 8, 16];
|
||||
if (!Number.isFinite(value) || !list.includes(value)) return fail("invalid_speed", { speed });
|
||||
worldRef.speed = value;
|
||||
return ok({ speed: worldRef.speed });
|
||||
}
|
||||
|
||||
function primaryPointer(worldRef, x, y) {
|
||||
if (!worldRef) return fail("missing_world");
|
||||
const px = Number(x);
|
||||
|
|
@ -171,6 +179,8 @@
|
|||
return togglePaused(worldRef);
|
||||
case "simulation.speed.cycle":
|
||||
return cycleSpeed(worldRef, command.speeds);
|
||||
case "simulation.speed.set":
|
||||
return setSpeed(worldRef, command.speed, command.allowed);
|
||||
case "pointer.primary":
|
||||
return primaryPointer(worldRef, command.x, command.y);
|
||||
case "history.undo": {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const SPRITES = [
|
|||
{ id: "stress_dizzy", label: "\u9ad8\u30b9\u30c8\u30ec\u30b9\u305f\u308a\u306a\u3044\u30fb\u3050\u308b\u3050\u308b", path: "assets/sprites/tarinai_24_stress_dizzy.webp", actionOnly: true },
|
||||
{ id: "stress_sweat", label: "\u9ad8\u30b9\u30c8\u30ec\u30b9\u305f\u308a\u306a\u3044\u30fb\u6c57", path: "assets/sprites/tarinai_25_stress_sweat.webp", actionOnly: true },
|
||||
{ id: "intimidate", label: "\u5a01\u5687\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_26_intimidate.webp", actionOnly: true },
|
||||
{ id: "intimidate_alt", label: "威嚇たりない 2", path: "assets/sprites/tarinai_40_intimidate_alt.webp", actionOnly: true },
|
||||
{ id: "birth_ritual", label: "\u3078\u3053\u3078\u3053\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_27_birth_ritual.webp", actionOnly: true },
|
||||
{ id: "zunchi_slave", label: "\u305a\u3093\u3061\u3069\u308c\u3044", path: "assets/sprites/tarinai_28_zunchi_slave.webp", displayOnly: true },
|
||||
{ id: "hungry_70", label: "\u7a7a\u817970\u4ee5\u4e0a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_29_hungry_70.webp", displayOnly: true },
|
||||
|
|
@ -51,7 +52,7 @@ const CONFIG = {
|
|||
hungerPerMinute: 36.0,
|
||||
lonelinessPerMinute: 7.6,
|
||||
energyPerMinute: 8.2,
|
||||
reproductionCooldown: 18,
|
||||
reproductionCooldown: 39,
|
||||
dayLength: 120,
|
||||
standardTemperature: 15,
|
||||
summerTemperatureBoost: 0,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@
|
|||
birthHash32,
|
||||
isEffectCauseText,
|
||||
});
|
||||
global.isTarinaiEffectCauseText = global.isTarinaiEffectCauseText || isEffectCauseText;
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
// Keep the historical bare high-level helper names available to legacy scripts while the
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@
|
|||
id: "disease_fight",
|
||||
flag: "fightDisease",
|
||||
mysteryEligible: true,
|
||||
},
|
||||
disease_mamekusaredake: {
|
||||
id: "disease_mamekusaredake",
|
||||
flag: "mamekusaredakeDisease",
|
||||
baseInfectionChance: 0.025,
|
||||
mysteryEligible: false,
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
const WEATHER_IDS = Object.freeze(["sunny", "cloudy", "light_rain"]);
|
||||
const MOOD_IDS = Object.freeze(["relaxed", "devastation", "crisis", "confusion", "fearful", "disease_spread", "weakened", "breeding", "happy", "overcrowded", "isolated"]);
|
||||
|
||||
const STATE_IDS = Object.freeze(["idle", "sleep"]);
|
||||
const STATE_IDS = Object.freeze(["idle", "sleep", "eat", "fight", "panic", "sunbath", "birth_ritual", "build", "social", "flee", "wander", "defeated"]);
|
||||
const POWER_MODE_IDS = Object.freeze(["", "protein", "niteropu"]);
|
||||
const SIZE_MODE_IDS = Object.freeze(["", "giant_drug", "dwarf_drug"]);
|
||||
const LIFE_MODE_IDS = Object.freeze(["", "mercury"]);
|
||||
const PIN_STATE_IDS = Object.freeze(["loose", "lodged", "falling", "stuck"]);
|
||||
const STAGE_IDS = Object.freeze(["fresh", "aging", "old", "dry"]);
|
||||
const STAGE_IDS = Object.freeze(["fresh", "aging", "old", "dry", "decomposing", "fertile_soil"]);
|
||||
|
||||
function indexMap(list) {
|
||||
return Object.freeze(Object.fromEntries(list.map((id, index) => [id, index])));
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
function consumeWaterForFire(worldRef, water, amount = 8) {
|
||||
if (!water) return false;
|
||||
water.amount = Math.max(0, Number(water.amount || 0) - amount);
|
||||
worldRef?.effects?.push(new Effect("ring", water.x, water.y, { size: Math.max(10, (water.r || 10) * 1.4), life: 0.18, color: "rgba(108,192,236,0.58)" }));
|
||||
worldRef?.queueEffect?.("ring", water.x, water.y, { size: Math.max(10, (water.r || 10) * 1.4), life: 0.18, color: "rgba(108,192,236,0.58)" });
|
||||
worldRef?.markItemBucketsDirty?.("fire-water");
|
||||
worldRef?.markSpatialDirty?.("fire-water");
|
||||
return true;
|
||||
|
|
@ -160,7 +160,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
box.flameWaterCooldown = 0;
|
||||
box.nestFireEffectCooldown = 0;
|
||||
worldRef?.log?.("\u5de3\u7bb1\u306b\u706b\u304c\u71c3\u3048\u79fb\u3063\u305f\u3002", "danger");
|
||||
worldRef?.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.35, life: 0.30, color: "rgba(255,122,48,0.56)" }));
|
||||
worldRef?.queueEffect?.("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.35, life: 0.30, color: "rgba(255,122,48,0.56)" });
|
||||
}
|
||||
worldRef?.markTerrainDirtyAt?.(box.x, box.y, nestBoxFireRadius(box) + 18, "nest-box-fire");
|
||||
worldRef?.markItemBucketsDirty?.("nest-box-fire");
|
||||
|
|
@ -179,7 +179,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
box.nestFireWetUntil = reason === "water" ? ((Number(worldRef?.time || box.world?.time || 0) || 0) + NEST_BOX_FIRE_WET_REIGNITE_DELAY) : 0;
|
||||
box.flameWaterCooldown = 0;
|
||||
box.nestFireEffectCooldown = 0;
|
||||
worldRef?.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.22, life: 0.24, color: "rgba(108,192,236,0.62)" }));
|
||||
worldRef?.queueEffect?.("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.22, life: 0.24, color: "rgba(108,192,236,0.62)" });
|
||||
worldRef?.markTerrainDirtyAt?.(box.x, box.y, nestBoxFireRadius(box) + 18, "nest-box-fire-extinguished");
|
||||
worldRef?.markItemBucketsDirty?.("nest-box-fire-extinguished");
|
||||
worldRef?.markSpatialDirty?.("nest-box-fire-extinguished");
|
||||
|
|
@ -207,7 +207,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
}
|
||||
if (count) {
|
||||
worldRef.log?.(`\u708e\u4e0a\u3057\u305f\u5de3\u7bb1\u304b\u3089${count}\u5339\u306e\u305f\u308a\u306a\u3044\u304c\u8ffd\u3044\u51fa\u3055\u308c\u305f\u3002`, "danger", { participants: occupants });
|
||||
worldRef.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.55, life: 0.32, color: "rgba(255,92,38,0.58)" }));
|
||||
worldRef.queueEffect?.("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.55, life: 0.32, color: "rgba(255,92,38,0.58)" });
|
||||
worldRef.markSpatialDirty?.(reason);
|
||||
worldRef.drawListDirty = true;
|
||||
}
|
||||
|
|
@ -231,7 +231,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
box.dead = true;
|
||||
}
|
||||
worldRef?.log?.("\u5de3\u7bb1\u304c\u713c\u5931\u3057\u305f\u3002", "danger");
|
||||
worldRef?.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.45, life: 0.34, color: "rgba(90,54,38,0.58)" }));
|
||||
worldRef?.queueEffect?.("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.45, life: 0.34, color: "rgba(90,54,38,0.58)" });
|
||||
worldRef?.markTerrainDirtyAt?.(box.x, box.y, nestBoxFireRadius(box) + 18, "nest-box-burnout");
|
||||
worldRef?.markItemBucketsDirty?.("nest-box-burnout");
|
||||
worldRef?.markSpatialDirty?.("nest-box-burnout");
|
||||
|
|
@ -290,7 +290,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
fire.flameLife = Math.max(0, Number(fire.flameLife || 0) - 0.85);
|
||||
grass.health = clamp((grass.health ?? 1) - 0.035, 0, 1);
|
||||
if ((fire.amount || 0) <= 24 || (fire.flameLife || 0) <= 0.35) fire.amount = 0;
|
||||
worldRef?.effects?.push(new Effect("ring", fire.x, fire.y, { size: Math.max(18, (fire.r || 14) * 1.8), life: 0.18, color: "rgba(118,188,94,0.46)" }));
|
||||
worldRef?.queueEffect?.("ring", fire.x, fire.y, { size: Math.max(18, (fire.r || 14) * 1.8), life: 0.18, color: "rgba(118,188,94,0.46)" });
|
||||
worldRef?.markTerrainDirtyAt?.(grass.x, grass.y, Math.max(grass.r || 17, 28), "grass-extinguish-fire");
|
||||
worldRef?.markItemBucketsDirty?.("grass-extinguish-fire");
|
||||
worldRef?.markSpatialDirty?.("grass-extinguish-fire");
|
||||
|
|
@ -590,7 +590,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
}
|
||||
pushFireEffect(worldRef, item.x + Math.cos(a) * Math.min(d, 28), item.y + Math.sin(a) * Math.min(d, 28), 14, item);
|
||||
}
|
||||
worldRef.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(42, (item.r || 15) * (2.4 + sizeScale * 0.65)), life: 0.30 + sizeScale * 0.045, color: "rgba(255,126,54,0.58)" }));
|
||||
worldRef.queueEffect?.("ring", item.x, item.y, { size: Math.max(42, (item.r || 15) * (2.4 + sizeScale * 0.65)), life: 0.30 + sizeScale * 0.045, color: "rgba(255,126,54,0.58)" });
|
||||
item.amount = 0;
|
||||
worldRef.markItemBucketsDirty?.("flame-firecracker");
|
||||
worldRef.markSpatialDirty?.("flame-firecracker");
|
||||
|
|
|
|||
|
|
@ -29,10 +29,7 @@
|
|||
if (event.target === dialog || event.target.closest("[data-game-dialog-cancel]")) settle(false);
|
||||
else if (event.target.closest("[data-game-dialog-confirm]")) settle(true);
|
||||
});
|
||||
document.addEventListener("keydown", event => {
|
||||
if (!active || dialog.classList.contains("hidden")) return;
|
||||
if (event.key === "Escape") { event.preventDefault(); event.stopImmediatePropagation?.(); settle(false); }
|
||||
}, true);
|
||||
dialog.addEventListener("tarinai:dialog-close-request", () => { if (active) settle(false); });
|
||||
return dialog;
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +49,8 @@
|
|||
confirmButton.classList.toggle("primary", !active.options.danger);
|
||||
}
|
||||
if (cancelButton) cancelButton.textContent = active.options.cancelText || "\u30ad\u30e3\u30f3\u30bb\u30eb";
|
||||
dialog.classList.remove("hidden");
|
||||
if (global.TarinaiManagedDialogs?.open) global.TarinaiManagedDialogs.open(dialog, active.restoreFocus);
|
||||
else { dialog.classList.remove("hidden"); dialog.setAttribute("aria-hidden", "false"); }
|
||||
global.setTimeout(() => confirmButton?.focus?.(), 0);
|
||||
}
|
||||
|
||||
|
|
@ -60,8 +58,9 @@
|
|||
if (!active) return;
|
||||
const current = active;
|
||||
active = null;
|
||||
document.getElementById("gameConfirmDialog")?.classList.add("hidden");
|
||||
current.restoreFocus?.focus?.();
|
||||
const dialog = document.getElementById("gameConfirmDialog");
|
||||
if (global.TarinaiManagedDialogs?.close) global.TarinaiManagedDialogs.close(dialog);
|
||||
else { dialog?.classList.add("hidden"); dialog?.setAttribute("aria-hidden", "true"); current.restoreFocus?.focus?.(); }
|
||||
current.resolve(Boolean(accepted));
|
||||
global.setTimeout(pump, 0);
|
||||
}
|
||||
|
|
|
|||
24
js/health.js
|
|
@ -34,6 +34,8 @@ const HEALTH = (() => {
|
|||
heatstroke: "\u71B1\u4E2D\u75C7",
|
||||
cleaning: "\u6383\u9664",
|
||||
electrocution: "\u611f\u96fb",
|
||||
trap: "\u7f60\u306b\u6355\u307e\u3063\u305f",
|
||||
poisonGas: "\u6bd2\u30ac\u30b9",
|
||||
});
|
||||
const DISEASE_RE = /([^\u3001\u3002\s]+\u75c5)/;
|
||||
|
||||
|
|
@ -41,6 +43,8 @@ const HEALTH = (() => {
|
|||
const text = String(reason || "");
|
||||
if (!text) return "";
|
||||
if (/\u6383\u9664|\u30ed\u30dc\u6383\u9664\u6a5f|robot.?cleaner|cleaned/.test(text)) return CAUSES.cleaning;
|
||||
if (/\u7f60\u306b\u6355\u307e\u3063\u305f|bear.?trap|trapped/i.test(text)) return CAUSES.trap;
|
||||
if (/\u6bd2\u30ac\u30b9|poison.?gas/i.test(text)) return CAUSES.poisonGas;
|
||||
if (/\u611f\u96fb|\u901a\u96fb\u4e2d\u306e\u96fb\u7dda|electrocution|electric.?shock|shocked/i.test(text)) return CAUSES.electrocution;
|
||||
if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, "");
|
||||
if (/\u51CD\u7D50|\u4F4E\u6E29|\u5BD2\u3055|cold|freeze|freezing/i.test(text)) return CAUSES.freeze;
|
||||
|
|
@ -176,10 +180,16 @@ const HEALTH = (() => {
|
|||
|
||||
|
||||
const MAJOR_DAMAGE_REACTIONS = Object.freeze([
|
||||
{ id: "voice_major_damage_01", text: "\u3042\u3042\u3042\u3042\uff01" },
|
||||
{ id: "voice_major_damage_02", text: "\u3048\u3048\u3048\u3048\uff01" },
|
||||
{ id: "voice_major_damage_03", text: "\u306f\u3046\u3046\u3046\uff01" },
|
||||
{ id: "voice_major_damage_04", text: "\u306f\u3046\u306f\u3046\uff01" },
|
||||
{ id: "voice_major_damage_01", text: "ああああ!" },
|
||||
{ id: "voice_major_damage_02", text: "ええええ!" },
|
||||
{ id: "voice_major_damage_03", text: "はううう!" },
|
||||
{ id: "voice_major_damage_04", text: "はうはう!" },
|
||||
]);
|
||||
|
||||
const OCHIBI_MAJOR_DAMAGE_REACTIONS = Object.freeze([
|
||||
{ id: "voice_ochibi_damage_001", text: "いちゃい!" },
|
||||
{ id: "voice_ochibi_damage_002", text: "ごふっ" },
|
||||
{ id: "voice_ochibi_damage_003", text: "ああああああああ" },
|
||||
]);
|
||||
|
||||
function spawnSilentDamageBubble(t, text = "!", color = "rgba(142,58,58,0.86)") {
|
||||
|
|
@ -196,14 +206,14 @@ const HEALTH = (() => {
|
|||
if (typeof Effect === "undefined") return false;
|
||||
if ((world.effectCounts?.bubble || 0) >= (CONFIG?.bubbleLimit || 80)) return false;
|
||||
const r = Math.max(16, t.radius || 22);
|
||||
world.effects?.push(new Effect("bubble", t.x, t.y - r * 1.38, {
|
||||
world.queueEffect?.("bubble", t.x, t.y - r * 1.38, {
|
||||
vx: (Math.random() - 0.5) * 2.4,
|
||||
vy: -4.8 - Math.random() * 2.8,
|
||||
life: 2.4 + Math.random() * 0.6,
|
||||
size: 9,
|
||||
text,
|
||||
color,
|
||||
}));
|
||||
});
|
||||
world.effectCounts.bubble = (world.effectCounts.bubble || 0) + 1;
|
||||
t.nextBubbleAt = Math.max(t.nextBubbleAt || 0, (world.time || 0) + 1.55);
|
||||
t._damageBubblePriorityUntil = Math.max(Number(t._damageBubblePriorityUntil || 0), (world.time || 0) + 1.55);
|
||||
|
|
@ -238,7 +248,7 @@ const HEALTH = (() => {
|
|||
t.lastMajorDamageAt = now;
|
||||
if (now < Number(t.nextMajorDamageVoiceAt || -999)) return false;
|
||||
t.nextMajorDamageVoiceAt = now + 1.0;
|
||||
const list = MAJOR_DAMAGE_REACTIONS;
|
||||
const list = t.isAcquiredTarinai ? OCHIBI_MAJOR_DAMAGE_REACTIONS : MAJOR_DAMAGE_REACTIONS;
|
||||
const reaction = list[Math.floor(Math.random() * list.length)] || list[0];
|
||||
audio.play?.(reaction.id, { category: "voice", minGap: 0.85 });
|
||||
spawnSilentDamageBubble(t, reaction.text, "rgba(146,58,58,0.88)");
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
// User-facing undo/redo for direct editing actions.
|
||||
(function (global) {
|
||||
const MAX_HISTORY = 48;
|
||||
const MAX_HISTORY_BYTES = 16 * 1024 * 1024;
|
||||
const MAX_HISTORY = 32;
|
||||
const MAX_HISTORY_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
function ensure(worldRef) {
|
||||
if (!worldRef) return null;
|
||||
|
|
|
|||
|
|
@ -78,10 +78,18 @@
|
|||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? 1 : -1) * (10 + p * 12), -48, 48);
|
||||
ball.lastPokedAt = worldRef.time || 0;
|
||||
ballHit += 1;
|
||||
worldRef.effects?.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, (ball.r || 18) * 1.1), life: 0.20, color: "rgba(255,255,255,0.52)" }));
|
||||
worldRef.queueEffect?.("ring", ball.x, ball.y, { size: Math.max(18, (ball.r || 18) * 1.1), life: 0.20, color: "rgba(255,255,255,0.52)" });
|
||||
}
|
||||
if (ballHit > 0) worldRef.markSpatialDirty?.("genkotsu-ball-impact");
|
||||
}
|
||||
for (const grave of worldRef.itemsOfType?.("grave") || (worldRef.items || []).filter(item => item && item.type === "grave")) {
|
||||
if (!grave || grave.dead) continue;
|
||||
const d = Math.max(1, distXY(grave.x, grave.y, it.x, it.y));
|
||||
const p = clamp(1 - d / (radius + (grave.r || 18)), 0, 1);
|
||||
if (p <= 0) continue;
|
||||
const force = push * (0.35 + p * 1.15);
|
||||
global.TarinaiMemorialBellSystem?.applyExternalForce?.(worldRef, grave, force, it, "drop-impact");
|
||||
}
|
||||
const antHit = damage > 0 ? worldRef.damageAntsInRadius(it.x, it.y, radius + 54, p => damage * (0.35 + p * 0.65), reason, {
|
||||
push: p => push * (0.62 + p * 1.25),
|
||||
source: it,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@
|
|||
resolveObstacleCollisions(item, dt, worldRef);
|
||||
item.x = clamp(item.x, p, worldRef.w - p);
|
||||
item.y = clamp(item.y, p, worldRef.h - p);
|
||||
const groundFriction = Math.pow(0.72, dt);
|
||||
const oilMul = global.TarinaiItemEnvironmentHazardSystem?.groundFrictionMultiplier?.(worldRef, item.x, item.y) ?? 1;
|
||||
const groundFriction = Math.pow(1 - (1 - 0.72) * oilMul, dt);
|
||||
item.vx *= groundFriction;
|
||||
item.vy *= groundFriction;
|
||||
}
|
||||
|
|
@ -138,7 +139,7 @@
|
|||
item.spinVelocity *= Math.pow(0.24, Math.max(0.016, dt || 0.016));
|
||||
if ((item.lastHaySlowLogAt || -999) + 3.5 < (worldRef.time || 0) && Math.hypot(item.vx || 0, item.vy || 0) > 120) {
|
||||
item.lastHaySlowLogAt = worldRef.time || 0;
|
||||
worldRef.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(14, item.r * 0.95), life: 0.20, color: "rgba(214,184,96,0.42)" }));
|
||||
worldRef.queueEffect?.("ring", item.x, item.y, { size: Math.max(14, item.r * 0.95), life: 0.20, color: "rgba(214,184,96,0.42)" });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -216,8 +217,8 @@
|
|||
const now = worldRef?.time || 0;
|
||||
if ((item.lastObstacleBounceAt || -999) + 0.08 > now) return;
|
||||
item.lastObstacleBounceAt = now;
|
||||
const color = item.type === "balloon" ? "rgba(248,122,166,0.36)" : ((obstacle?.type === "bounce_fence" || obstacle?.type === "bounce_fence_v") ? "rgba(82,153,230,0.46)" : (obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)"));
|
||||
worldRef?.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(13, (item.r || 18) * 0.82), life: 0.18, color }));
|
||||
const color = item.type === "balloon" ? "rgba(248,122,166,0.36)" : ((obstacle?.type === "bounce_fence" || obstacle?.type === "bounce_fence_v") ? "rgba(235,104,165,0.50)" : (obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)"));
|
||||
worldRef?.queueEffect?.("ring", item.x, item.y, { size: Math.max(13, (item.r || 18) * 0.82), life: 0.18, color });
|
||||
}
|
||||
|
||||
function normalizeAngleLocal(angle = 0, fallback = 0) {
|
||||
|
|
@ -342,7 +343,7 @@
|
|||
const phase = Math.sin(item.fanPulse || 0);
|
||||
const px = item.x + ax * (42 + 24 * phase);
|
||||
const py = item.y + ay * (42 + 24 * phase);
|
||||
worldRef?.effects?.push(new Effect("ring", px, py, { size: 12 + 7 * Math.abs(phase), life: 0.18, color: "rgba(174,214,236,0.26)" }));
|
||||
worldRef?.queueEffect?.("ring", px, py, { size: 12 + 7 * Math.abs(phase), life: 0.18, color: "rgba(174,214,236,0.26)" });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -372,13 +373,13 @@
|
|||
// A fan should carry embers forward instead of merely dimming the source flame.
|
||||
// The source loses a little fuel, while the downwind flame handles normal fire contacts.
|
||||
fire.amount = Math.max(4, (fire.amount || 0) - (2.2 + falloff * 3.8));
|
||||
worldRef.effects?.push(new Effect("flame", fire.x + ax * Math.max(12, fire.r || 14), fire.y + ay * Math.max(12, fire.r || 14), {
|
||||
worldRef.queueEffect?.("flame", fire.x + ax * Math.max(12, fire.r || 14), fire.y + ay * Math.max(12, fire.r || 14), {
|
||||
size: Math.max(7, (fire.r || 14) * 0.72),
|
||||
life: 0.22,
|
||||
color: "rgba(255,126,54,0.58)",
|
||||
vx: ax * (80 + falloff * 120),
|
||||
vy: ay * (80 + falloff * 120),
|
||||
}));
|
||||
});
|
||||
worldRef.markItemBucketsDirty?.("fan-fire-spread");
|
||||
worldRef.markSpatialDirty?.("fan-fire-spread");
|
||||
worldRef.drawListDirty = true;
|
||||
|
|
@ -429,7 +430,7 @@
|
|||
t.lastFanSurpriseAt = worldRef?.time || 0;
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.12);
|
||||
if (typeof t.spawnBubble === "function") t.spawnBubble("~", { life: 0.55 });
|
||||
worldRef?.effects?.push(new Effect("ring", t.x + ax * radius * 0.4, t.y + ay * radius * 0.2, { size: Math.max(12, radius * 0.72), life: 0.16, color: "rgba(174,214,236,0.28)" }));
|
||||
worldRef?.queueEffect?.("ring", t.x + ax * radius * 0.4, t.y + ay * radius * 0.2, { size: Math.max(12, radius * 0.72), life: 0.16, color: "rgba(174,214,236,0.28)" });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -522,13 +523,13 @@
|
|||
if (!addedDrop) continue;
|
||||
spawned += 1;
|
||||
if (i < 8) {
|
||||
worldRef.effects?.push(new Effect("fight", drop.x, drop.y, {
|
||||
worldRef.queueEffect?.("fight", drop.x, drop.y, {
|
||||
size: deterministicRange(worldRef || balloon, "water-balloon-drop-spark-size", 3.5, 7.0, balloon, i),
|
||||
life: deterministicRange(worldRef || balloon, "water-balloon-drop-spark-life", 0.24, 0.42, balloon, i),
|
||||
color: "rgba(96,196,244,0.66)",
|
||||
vx: drop.vx * 0.42,
|
||||
vy: drop.vy * 0.42,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
if (spawned) {
|
||||
|
|
@ -566,7 +567,7 @@
|
|||
}
|
||||
if (worldRef && ((item.lastSplashTrailAt || -999) + 0.12 < (worldRef.time || 0)) && Math.hypot(item.vx || 0, item.vy || 0) > 72) {
|
||||
item.lastSplashTrailAt = worldRef.time || 0;
|
||||
worldRef.effects?.push(new Effect("fight", item.x, item.y, { size: Math.max(2.8, r * 0.55), life: 0.18, color: "rgba(112,206,248,0.36)", vx: -(item.vx || 0) * 0.05, vy: -(item.vy || 0) * 0.05 }));
|
||||
worldRef.queueEffect?.("fight", item.x, item.y, { size: Math.max(2.8, r * 0.55), life: 0.18, color: "rgba(112,206,248,0.36)", vx: -(item.vx || 0) * 0.05, vy: -(item.vy || 0) * 0.05 });
|
||||
}
|
||||
worldRef?.markSpatialDirty?.("water-drop-motion");
|
||||
worldRef?.markTerrainDirtyAt?.(item.x, item.y, Math.max(28, r * 3.2), "water-drop-motion");
|
||||
|
|
@ -624,11 +625,11 @@
|
|||
// through amount/hp instead of assigning to .dead directly.
|
||||
balloon.amount = 0;
|
||||
balloon.hp = 0;
|
||||
worldRef?.effects?.push(new Effect("ring", balloon.x, balloon.y, { size: Math.max(32, (balloon.r || 20) * 1.9), life: 0.28, color: "rgba(76,178,236,0.62)" }));
|
||||
worldRef?.queueEffect?.("ring", balloon.x, balloon.y, { size: Math.max(32, (balloon.r || 20) * 1.9), life: 0.28, color: "rgba(76,178,236,0.62)" });
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const a = deterministicAngle(worldRef || balloon, "water-balloon-pop-piece", balloon, i);
|
||||
const s = deterministicRange(worldRef || balloon, "water-balloon-pop-speed", 24, 96, balloon, i);
|
||||
worldRef?.effects?.push(new Effect("fight", balloon.x, balloon.y, { size: deterministicRange(worldRef || balloon, "water-balloon-pop-size", 4, 10, balloon, i), life: 0.28, color: "rgba(95,204,246,0.72)", vx: Math.cos(a) * s, vy: Math.sin(a) * s }));
|
||||
worldRef?.queueEffect?.("fight", balloon.x, balloon.y, { size: deterministicRange(worldRef || balloon, "water-balloon-pop-size", 4, 10, balloon, i), life: 0.28, color: "rgba(95,204,246,0.72)", vx: Math.cos(a) * s, vy: Math.sin(a) * s });
|
||||
}
|
||||
spawnWaterFromBalloon(balloon, worldRef);
|
||||
const sourceLabel = cause?.type === "fire" || cause?.type === "flame_firecracker" ? "\u706b"
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
if (!ball.stuckPinIds.includes(item.id)) ball.stuckPinIds.push(item.id);
|
||||
ball.stuckPinId = ball.stuckPinIds[0] || item.id;
|
||||
updateBallLodged(item, 0, worldRef);
|
||||
worldRef?.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(12, (item.r || 8) * 1.4), life: 0.16, color: item.type === "oshibyo" ? "rgba(73,119,205,0.42)" : "rgba(214,72,72,0.42)" }));
|
||||
worldRef?.queueEffect?.("ring", item.x, item.y, { size: Math.max(12, (item.r || 8) * 1.4), life: 0.16, color: item.type === "oshibyo" ? "rgba(73,119,205,0.42)" : "rgba(214,72,72,0.42)" });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +212,7 @@
|
|||
if (isOshibyo) {
|
||||
t.thought = "\u304a\u3057\u308a\u92f2\u304c\u523a\u3055\u3063\u3066\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u3063\u305f";
|
||||
worldRef.log?.(`${t.name}\u306b\u304a\u3057\u308a\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] });
|
||||
worldRef.effects?.push(new Effect("ring", t.x, t.y + (t.radius || 16) * 0.34, { size: Math.max(14, (t.radius || 16) * 0.58), life: 0.18, color: "rgba(73,119,205,0.36)" }));
|
||||
worldRef.queueEffect?.("ring", t.x, t.y + (t.radius || 16) * 0.34, { size: Math.max(14, (t.radius || 16) * 0.58), life: 0.18, color: "rgba(73,119,205,0.36)" });
|
||||
return true;
|
||||
}
|
||||
if (t.enterPanic) {
|
||||
|
|
@ -229,7 +229,7 @@
|
|||
item.pinLogAt = worldRef.time || 0;
|
||||
worldRef.log?.(`${t.name}\u306b\u753b\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] });
|
||||
}
|
||||
worldRef.effects?.push(new Effect("ring", t.x, t.y - (t.radius || 16) * 0.12, { size: Math.max(18, (t.radius || 16) * 0.90), life: 0.22, color: "rgba(214,72,72,0.50)" }));
|
||||
worldRef.queueEffect?.("ring", t.x, t.y - (t.radius || 16) * 0.12, { size: Math.max(18, (t.radius || 16) * 0.90), life: 0.22, color: "rgba(214,72,72,0.50)" });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +342,7 @@
|
|||
item.vx = clamp(baseVx * 0.45 + nx * 18, -90, 90);
|
||||
item.vy = clamp(baseVy * 0.45 + Math.max(12, ny * 18), -60, 110);
|
||||
item.spinVelocity = clamp((item.spinVelocity || 0) * 0.35 + (Math.sign(nx) || 1) * 0.9, -2.2, 2.2);
|
||||
worldRef?.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(14, (item.r || 16) * 0.92), life: 0.16, color: "rgba(214,112,112,0.40)" }));
|
||||
worldRef?.queueEffect?.("ring", item.x, item.y, { size: Math.max(14, (item.r || 16) * 0.92), life: 0.16, color: "rgba(214,112,112,0.40)" });
|
||||
if (reason === "fall" && t) worldRef?.log?.(`${t.name}\u306E\u753B\u92F2\u304C\u629C\u3051\u843D\u3061\u305F\u3002`, "observe", { participants: [t] });
|
||||
}
|
||||
if (wasOshibyo && storedZunchi > 0 && worldRef?.spawnBurstZunchi) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,14 @@
|
|||
function update(item, dt, worldRef) {
|
||||
if (!item || item.dead) return false;
|
||||
switch (item.type) {
|
||||
case "oil":
|
||||
case "bear_trap":
|
||||
case "leakage_device":
|
||||
case "poison_gas_can":
|
||||
case "poison_gas":
|
||||
case "mamekusaredake":
|
||||
case "mame_spore":
|
||||
return global.TarinaiItemEnvironmentHazardSystem.update(item, dt, worldRef) !== false;
|
||||
case "firecracker":
|
||||
case "flame_firecracker":
|
||||
case "sticky_bomb":
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@
|
|||
&& item.achievementBurstNeverStopped !== false
|
||||
);
|
||||
worldRef.applyImpactDamage?.(t, damage, "\u885d\u7a81", { source: item }) || t.damage?.(damage, "\u885d\u7a81");
|
||||
if (t.dead) {
|
||||
globalThis.TarinaiAchievements?.recordZunchiCollisionDeath?.({ world: worldRef, tarinai: t, zunchi: item, speed });
|
||||
}
|
||||
if (t.dead && eligibleSelfZunchiDeath) {
|
||||
globalThis.TarinaiAchievements?.recordSelfZunchiDeath?.({ world: worldRef, tarinai: t, zunchi: item, speed });
|
||||
}
|
||||
|
|
@ -108,7 +111,7 @@
|
|||
if (t.addStress) t.addStress(7, { threshold: 8, duration: 3.2 });
|
||||
}
|
||||
worldRef.spawnFallEffect?.(t.x, t.y + (t.radius || 20) * 0.45, 0.65);
|
||||
worldRef.effects?.push(new Effect("zunchi_miasma", item.x, item.y - 4, { size: 14, life: 0.38, color: "rgba(77,92,42,0.36)" }));
|
||||
worldRef.queueEffect?.("zunchi_miasma", item.x, item.y - 4, { size: 14, life: 0.38, color: "rgba(77,92,42,0.36)" });
|
||||
item.vx *= -0.18;
|
||||
item.vy *= -0.18;
|
||||
break;
|
||||
|
|
@ -139,7 +142,7 @@
|
|||
item.burning = false;
|
||||
item.burnTimer = 0;
|
||||
item.amount = 0;
|
||||
worldRef?.effects?.push(new Effect("zunchi_miasma", item.x, item.y - 4, { size: Math.max(13, (item.r || 15) * 1.25), life: 0.42, color: "rgba(64,44,34,0.44)" }));
|
||||
worldRef?.queueEffect?.("zunchi_miasma", item.x, item.y - 4, { size: Math.max(13, (item.r || 15) * 1.25), life: 0.42, color: "rgba(64,44,34,0.44)" });
|
||||
}
|
||||
worldRef?.markTerrainDirtyAt?.(item.x, item.y, Math.max(item.r || 15, 28), "zunchi-burning");
|
||||
worldRef?.markItemBucketsDirty?.("zunchi-burning");
|
||||
|
|
@ -183,7 +186,7 @@
|
|||
}
|
||||
if ((item.amount || 0) <= 0.5) {
|
||||
item.amount = 0;
|
||||
worldRef?.effects?.push(new Effect("zunchi_miasma", item.x, item.y - 4, { size: Math.max(12, (item.r || 15) * 1.15), life: 0.34, color: "rgba(64,44,34,0.34)" }));
|
||||
worldRef?.queueEffect?.("zunchi_miasma", item.x, item.y - 4, { size: Math.max(12, (item.r || 15) * 1.15), life: 0.34, color: "rgba(64,44,34,0.34)" });
|
||||
worldRef?.markTerrainDirtyAt?.(item.x, item.y, Math.max(item.r || 15, 28), "zunchi-decay-out");
|
||||
worldRef?.markItemBucketsDirty?.("zunchi-decay-out");
|
||||
worldRef?.markSpatialDirty?.("zunchi-decay-out");
|
||||
|
|
|
|||
384
js/item_environment_hazard_system.js
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
"use strict";
|
||||
(function(global){
|
||||
const LOOSE_DYNAMIC_TYPES = new Set([
|
||||
"ball","balloon","zunchi","pushpin","oshibyo","genkotsu",
|
||||
"firecracker","flame_firecracker","sticky_bomb","water"
|
||||
]);
|
||||
|
||||
function nearby(world,x,y,r){ return world?.nearbyItems?.(x,y,r,true) || world?.items || []; }
|
||||
function oilAt(world,x,y){
|
||||
for(const o of (world?.itemsOfType?.("oil")||[])) {
|
||||
if(o&&!o.dead&&distXY(x,y,o.x,o.y)<=Math.max(12,o.r||58)) return o;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function groundFrictionMultiplier(world,x,y){ return oilAt(world,x,y)?0:1; }
|
||||
|
||||
function spawnOilFire(item,world,initial=false){
|
||||
if(!item||!world)return null;
|
||||
const r=Math.max(24,item.r||58);
|
||||
const angle=initial ? global.deterministicAngle?.(world,"oil-ignite-fire",item,item.oilBurnTimer||0) ?? 0 : Math.random()*Math.PI*2;
|
||||
const distance=initial ? 0 : Math.random()*r*.58;
|
||||
return global.TarinaiFireRuntimeSystem?.spawnFireAt?.(
|
||||
world,
|
||||
item.x+Math.cos(angle)*distance,
|
||||
item.y+Math.sin(angle)*distance*.58,
|
||||
{radius:initial?20:12+Math.random()*5,amount:initial?72:30+Math.random()*24,life:initial?4.4:2.8+Math.random()*2.0,source:item,mergeRadius:initial?48:42}
|
||||
) || null;
|
||||
}
|
||||
|
||||
function igniteOil(item,world){
|
||||
if(!item||item.dead||item.type!=="oil"||item.oilBurning)return false;
|
||||
item.oilBurning=true;
|
||||
item.oilBurnTimer=0;
|
||||
item.nextOilFireAt=Number(world?.time||0);
|
||||
spawnOilFire(item,world,true);
|
||||
if(world){ world.drawListDirty=true; world.markSpatialDirty?.("oil-ignited"); }
|
||||
return true;
|
||||
}
|
||||
function igniteOilNear(world,x,y,r=120){
|
||||
let n=0;
|
||||
for(const o of (world?.itemsOfType?.("oil")||[])){
|
||||
if(o&&!o.dead&&distXY(x,y,o.x,o.y)<=r+Math.max(12,o.r||58)) n+=igniteOil(o,world)?1:0;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function igniteFromBurningOilContact(item,world){
|
||||
if(!item||!world||!item.oilBurning)return 0;
|
||||
const radius=Math.max(24,Number(item.r||58));
|
||||
let ignited=0;
|
||||
const tarinai=world.nearbyTarinai?.(item.x,item.y,radius+48,true)||world.tarinai||[];
|
||||
for(const t of tarinai){
|
||||
if(!t||t.dead||t.insideNestBoxId||world.isTarinaiHiddenInNestBox?.(t))continue;
|
||||
if(distXY(item.x,item.y,t.x,t.y)>radius+Math.max(12,Number(t.radius||18))*.72)continue;
|
||||
const wasBurning=Boolean(t.burning||(t.burnTimer||0)>.02);
|
||||
if(typeof t.igniteFire==="function")t.igniteFire(item,false);
|
||||
if(!wasBurning&&Boolean(t.burning||(t.burnTimer||0)>.02))ignited++;
|
||||
}
|
||||
const ants=world.nearbyAnts?.(item.x,item.y,radius+24,true)||world.ants||[];
|
||||
for(const ant of ants){
|
||||
if(!ant||ant.dead||distXY(item.x,item.y,ant.x,ant.y)>radius+Math.max(4,Number(ant.r||5)))continue;
|
||||
const wasBurning=Boolean(ant.burning||(ant.burnTimer||0)>.02);
|
||||
global.TarinaiFireRuntimeSystem?.igniteAntByFire?.(ant,item,world);
|
||||
if(!wasBurning&&Boolean(ant.burning||(ant.burnTimer||0)>.02))ignited++;
|
||||
}
|
||||
return ignited;
|
||||
}
|
||||
function updateOil(item,dt,world){
|
||||
if(!item.oilBurning){
|
||||
for(const f of (world?.itemsOfType?.("fire")||[])){
|
||||
if(f&&!f.dead&&distXY(item.x,item.y,f.x,f.y)<=Math.max(item.r||58,24)+Math.max(f.r||14,10)){
|
||||
igniteOil(item,world); break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const now=Number(world?.time||0);
|
||||
igniteFromBurningOilContact(item,world);
|
||||
item.oilBurnTimer=(item.oilBurnTimer||0)+dt;
|
||||
item.amount=Math.max(0,(item.amount||100)-dt*5.2);
|
||||
if(now>=Number(item.nextOilFireAt||0)){
|
||||
item.nextOilFireAt=now+0.36+Math.random()*0.34;
|
||||
spawnOilFire(item,world,false);
|
||||
}
|
||||
if(item.amount<=0){
|
||||
item.amount=0;
|
||||
world?.markItemBucketsDirty?.("oil-burnout");
|
||||
world?.markSpatialDirty?.("oil-burnout");
|
||||
world && (world.drawListDirty=true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isTrappableDynamic(it,world){
|
||||
if(!it||it.dead||it.type==="bear_trap"||it.type==="poison_gas"||it.type==="fire") return false;
|
||||
if(it.isStructure) return false;
|
||||
if(["rope","rod","spring","wire","insulated_wire","rotator","reciprocator","poison_block"].includes(it.type)) return false;
|
||||
if(LOOSE_DYNAMIC_TYPES.has(it.type)) return true;
|
||||
if((it.dropTimer||0)>0 || Math.hypot(it.vx||0,it.vy||0)>0.2) return true;
|
||||
return Boolean(world?.spatial?.isDynamicItem?.(it) && !it.isStructure);
|
||||
}
|
||||
|
||||
function trapTarget(item,world){
|
||||
const ref=item?.trapTargetRef;
|
||||
if(ref&&!ref.dead&&(!item.trapTargetId||ref.id===item.trapTargetId)) return ref;
|
||||
if(!item?.trapTargetId) return null;
|
||||
const found=world?.itemById?.(item.trapTargetId)
|
||||
|| (world?.items||[]).find(it=>it&&!it.dead&&it.id===item.trapTargetId)
|
||||
|| (world?.tarinai||[]).find(t=>t&&!t.dead&&t.id===item.trapTargetId)
|
||||
|| null;
|
||||
item.trapTargetRef=found;
|
||||
return found;
|
||||
}
|
||||
|
||||
function hasTrapDamageStat(target){
|
||||
if(!target||target.dead)return false;
|
||||
if(Number.isFinite(Number(target.hp))||Number.isFinite(Number(target.health)))return true;
|
||||
if(Number.isFinite(Number(target.maxHp))||Number.isFinite(Number(target.maxHealth)))return true;
|
||||
if(typeof target.damage==="function"&&Number.isFinite(Number(target.energy)))return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function damageHeld(target,world,trap){
|
||||
if(!target||target.dead||!hasTrapDamageStat(target))return false;
|
||||
if(typeof target.damage==="function"){
|
||||
try {
|
||||
target.damage(2,"\u7f60\u306b\u6355\u307e\u3063\u305f",{source:trap});
|
||||
} catch (_) {
|
||||
target.damage(2,"\u7f60\u306b\u6355\u307e\u3063\u305f",{source:trap});
|
||||
}
|
||||
if (target.world && Number.isFinite(Number(target.radius))) target.hurtTimer=Math.max(Number(target.hurtTimer||0),.62);
|
||||
return true;
|
||||
}
|
||||
if(Number.isFinite(Number(target.hp))){
|
||||
target.hp=Math.max(0,Number(target.hp)-2);
|
||||
target.hpBarTimer=Math.max(Number(target.hpBarTimer||0),.8);
|
||||
if(target.hp<=0) global.TarinaiStructureLifecycle?.deleteItem?.(world,target,{reason:"bear-trap",breaker:trap});
|
||||
return true;
|
||||
}
|
||||
if(Number.isFinite(Number(target.health))){
|
||||
target.health=Math.max(0,Number(target.health)-2);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function holdTrapTarget(trap,target,world){
|
||||
if(!trap||!target||target.dead)return false;
|
||||
const now=Number(world?.time||0);
|
||||
target._bearTrapHoldUntil=Math.max(Number(target._bearTrapHoldUntil||0),Number(trap.trapUntil||now));
|
||||
target._bearTrapOwnerId=trap.id;
|
||||
target._bearTrapHoldX=trap.x;
|
||||
target._bearTrapHoldY=trap.y;
|
||||
target.x=trap.x;
|
||||
target.y=trap.y;
|
||||
target.prevX=trap.x;
|
||||
target.prevY=trap.y;
|
||||
target.vx=0; target.vy=0; target.impulseVx=0; target.impulseVy=0; target.spinVelocity=0;
|
||||
const body=global.TarinaiPhysicsBodySystem?.isBodyType?.(target) ? global.TarinaiPhysicsBodySystem.ensureBody?.(target,world,{syncFromLegacy:true}) : null;
|
||||
if(body?.pose){ body.pose.x=trap.x; body.pose.y=trap.y; }
|
||||
if(body?.velocity){ body.velocity.x=0; body.velocity.y=0; body.velocity.linear=0; body.velocity.angular=0; }
|
||||
target._physicsExternalPoseDirty=true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function triggerBearTrap(item,target,world){
|
||||
if(!item||!target||target.dead)return false;
|
||||
const now=Number(world?.time||0);
|
||||
item.trapTargetId=target.id||"";
|
||||
item.trapTargetRef=target;
|
||||
item.trapTriggeredAt=now;
|
||||
item.trapDamageableTarget=hasTrapDamageStat(target);
|
||||
item.trapHoldDuration=item.trapDamageableTarget?10:5;
|
||||
item.trapUntil=now+item.trapHoldDuration;
|
||||
item.nextTrapDamageAt=now+.5;
|
||||
item.trapClosed=true;
|
||||
holdTrapTarget(item,target,world);
|
||||
world?.queueEffect?.("ring",item.x,item.y,{size:Math.max(34,(item.r||34)*1.2),life:.22,color:"rgba(190,204,214,0.58)"});
|
||||
if(world){ world.drawListDirty=true; world.markSpatialDirty?.("bear-trap-trigger"); }
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateBearTrap(item,dt,world){
|
||||
const now=Number(world?.time||0);
|
||||
if(item.trapClosed || item.trapUntil>0){
|
||||
const target=trapTarget(item,world);
|
||||
if(now>=Number(item.trapUntil||0)){
|
||||
if(target){
|
||||
target._bearTrapHoldUntil=0;
|
||||
target._bearTrapOwnerId="";
|
||||
target._bearTrapHoldX=undefined;
|
||||
target._bearTrapHoldY=undefined;
|
||||
}
|
||||
item.trapTargetRef=null;
|
||||
item.trapTargetId="";
|
||||
const removed=global.TarinaiStructureLifecycle?.deleteItem?.(world,item,{reason:"bear-trap-expired"});
|
||||
if(!removed)item.amount=0;
|
||||
world?.markItemBucketsDirty?.("bear-trap-expired");
|
||||
world?.markSpatialDirty?.("bear-trap-expired");
|
||||
return true;
|
||||
}
|
||||
if(target){
|
||||
holdTrapTarget(item,target,world);
|
||||
if(item.trapDamageableTarget&&now>=Number(item.nextTrapDamageAt||0)){
|
||||
item.nextTrapDamageAt=now+.5;
|
||||
damageHeld(target,world,item);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const captureRadius=Math.max(46,(item.r||34)*1.45);
|
||||
let best=null,bestD=Infinity;
|
||||
for(const it of nearby(world,item.x,item.y,captureRadius)){
|
||||
if(it===item||!isTrappableDynamic(it,world))continue;
|
||||
if(Number(it._bearTrapHoldUntil||0)>now)continue;
|
||||
const d=distXY(item.x,item.y,it.x,it.y);
|
||||
const rr=Math.max(12,(item.r||34)*.72)+Math.max(7,(it.r||12)*.62);
|
||||
if(d<=rr&&d<bestD){ best=it;bestD=d; }
|
||||
}
|
||||
for(const t of (world?.nearbyTarinai?.(item.x,item.y,captureRadius+36,true)||world?.tarinai||[])){
|
||||
if(!t||t.dead||Number(t._bearTrapHoldUntil||0)>now)continue;
|
||||
const d=distXY(item.x,item.y,t.x,t.y);
|
||||
const rr=Math.max(12,(item.r||34)*.72)+Math.max(10,(t.radius||18)*.62);
|
||||
if(d<=rr&&d<bestD){ best=t;bestD=d; }
|
||||
}
|
||||
if(best) triggerBearTrap(item,best,world);
|
||||
return true;
|
||||
}
|
||||
|
||||
function enforceTrapHolds(world){
|
||||
const now=Number(world?.time||0);
|
||||
let held=0;
|
||||
for(const trap of (world?.itemsOfType?.("bear_trap")||[])){
|
||||
if(!trap||trap.dead||!trap.trapClosed||now>=Number(trap.trapUntil||0))continue;
|
||||
const target=trapTarget(trap,world);
|
||||
if(target&&holdTrapTarget(trap,target,world)) held++;
|
||||
}
|
||||
if(held) world?.markSpatialDirty?.("bear-trap-hold");
|
||||
return held;
|
||||
}
|
||||
|
||||
function leakageRadius(item){ return Math.max(260,(Number(item?.r)||34)*8.2); }
|
||||
|
||||
function pulseLeakage(item,world){
|
||||
if(!item||item.dead||!world)return 0;
|
||||
const powered=Boolean(item._signalDriven&&item._signalOn);
|
||||
if(!powered)return 0;
|
||||
const now=Number(world.time||0);
|
||||
if(now<Number(item.nextLeakagePulseAt||0))return 0;
|
||||
item.nextLeakagePulseAt=now+.24;
|
||||
const radius=leakageRadius(item);
|
||||
let hit=0;
|
||||
for(const t of world.tarinai||[]){
|
||||
if(!t||t.dead||world.isTarinaiHiddenInNestBox?.(t))continue;
|
||||
const d=distXY(item.x,item.y,t.x,t.y);
|
||||
if(d>radius+(t.radius||18))continue;
|
||||
const q=Math.max(.2,1-d/Math.max(1,radius));
|
||||
t.damage?.(1.5+.9*q,"\u6f0f\u96fb",{source:item});
|
||||
t.hurtTimer=Math.max(t.hurtTimer||0,.22);
|
||||
t.electricShockUntil=Math.max(Number(t.electricShockUntil||0),now+.48);
|
||||
t.setActionState?.("panic",{target:null,reason:"\u611f\u96fb\u3057\u3066\u3044\u308b\u3002",wake:true,sleeping:false});
|
||||
if(t.isAcquiredTarinai&&now>=Number(t.ochibiElectricVoiceAt||0)){t.ochibiElectricVoiceAt=now+1.8;if(t.bubble?.("びりびりちゃ!",1.4,"rgba(70,104,168,0.82)",{force:true})){const a=globalThis.TarinaiAudio;if(a?.playSample)a.playSample("voice_ochibi_electric_006",.9,"voice")||a.play?.("voice_ochibi_electric_006",{category:"voice",minGap:.9});}}
|
||||
world.queueEffect?.("electric_shock",t.x,t.y-Math.max(2,(t.radius||18)*.15),{size:Math.max(18,(t.radius||18)*1.35),life:.32,color:"rgba(122,224,255,0.98)",importance:3,forceVisual:true});
|
||||
hit++;
|
||||
}
|
||||
for(const ant of world.ants||[]){
|
||||
if(!ant||ant.dead)continue;
|
||||
const d=distXY(item.x,item.y,ant.x,ant.y);
|
||||
if(d>radius+(ant.r||6))continue;
|
||||
ant.hp=Math.max(0,Number(ant.hp??ant.maxHp??32)-1.4);
|
||||
ant.hpBarTimer=Math.max(Number(ant.hpBarTimer||0),.7);
|
||||
hit++;
|
||||
}
|
||||
world.queueEffect?.("ring",item.x,item.y,{size:radius*.62,life:.22,color:"rgba(112,206,255,0.38)"});
|
||||
world.drawListDirty=true;
|
||||
return hit;
|
||||
}
|
||||
|
||||
function updateLeakage(item,dt,world){
|
||||
// Electrical damage is applied after the signal graph has been resolved for
|
||||
// this simulation step. Keep the per-item update as a lightweight no-op.
|
||||
return true;
|
||||
}
|
||||
|
||||
function updatePoweredHazards(world,dt){
|
||||
let hit=0;
|
||||
for(const item of (world?.itemsOfType?.("leakage_device")||[])) hit+=pulseLeakage(item,world);
|
||||
return hit;
|
||||
}
|
||||
|
||||
function spawnPoisonGas(world,can){
|
||||
if(!world||!can||can._gasReleased)return 0;
|
||||
can._gasReleased=true;
|
||||
let n=0;
|
||||
const sizeScale=Math.max(.55,Math.min(1.8,Number(can.r||28)/28));
|
||||
const count=Math.max(7,Math.round(10*sizeScale));
|
||||
for(let i=0;i<count;i++){
|
||||
const a=Math.PI*2*i/count+Math.random()*.28;
|
||||
const d=(10+Math.random()*56)*sizeScale;
|
||||
const g=new Item("poison_gas",can.x+Math.cos(a)*d,can.y+Math.sin(a)*d*.72);
|
||||
g.r=(82+Math.random()*48)*sizeScale;
|
||||
g.gasConcentration=.82+Math.random()*.28;
|
||||
g.gasLife=(18+Math.random()*8)*Math.sqrt(sizeScale);
|
||||
g.gasExpansion=(58+Math.random()*42)*sizeScale;
|
||||
g.vx=Math.cos(a)*(10+Math.random()*20)*sizeScale;
|
||||
g.vy=Math.sin(a)*(10+Math.random()*20)*sizeScale;
|
||||
if(world.addItem?.(g,"poison-gas",{terrain:false}))n++;
|
||||
}
|
||||
world.drawListDirty=true;
|
||||
return n;
|
||||
}
|
||||
|
||||
function burstPoisonGasCan(item,world,reason="fuse"){
|
||||
if(!item||item.dead||item._gasReleased)return false;
|
||||
const x=item.x,y=item.y;
|
||||
const sizeScale=Math.max(.55,Math.min(1.8,Number(item.r||28)/28));
|
||||
spawnPoisonGas(world,item);
|
||||
world?.spawnEffect?.("explosion",x,y,{size:72*sizeScale,life:.58,color:"rgba(136,74,170,0.78)"});
|
||||
world?.spawnEffect?.("ring",x,y,{size:112*sizeScale,life:.54,color:"rgba(184,108,216,0.52)"});
|
||||
world?.applyExplosionExternalForces?.(x,y,105*sizeScale,item,.42*sizeScale,"poison-gas-can");
|
||||
item.amount=0;
|
||||
item.hp=0;
|
||||
world?.markItemBucketsDirty?.("poison-gas-can-burst");
|
||||
world?.markSpatialDirty?.("poison-gas-can-burst");
|
||||
world && (world.drawListDirty=true);
|
||||
global.TarinaiAudio?.explode?.();
|
||||
world?.log?.("\u6bd2\u30ac\u30b9\u7f36\u304c\u7834\u88c2\u3057\u3001\u6bd2\u30ac\u30b9\u304c\u5e83\u304c\u3063\u305f\u3002","accident");
|
||||
return true;
|
||||
}
|
||||
|
||||
function updatePoisonGasCan(item,dt,world){
|
||||
item.fuseTimer=Math.max(0,Number(item.fuseTimer??5)-Math.max(0,Number(dt||0)));
|
||||
if(item.fuseTimer<=0) burstPoisonGasCan(item,world,"fuse");
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateGas(item,dt,world){
|
||||
item.gasAge=(item.gasAge||0)+dt;
|
||||
const life=Math.max(1,item.gasLife||22);
|
||||
item.gasConcentration=Math.max(0,1-item.gasAge/life);
|
||||
item.r+=(Math.max(36,Number(item.gasExpansion||72))/life)*dt;
|
||||
for(const fan of (world?.itemsOfType?.("fan")||[])){
|
||||
const wind=global.TarinaiItemDynamicBallSystem?.fanWindAt?.(fan,item.x,item.y,{range:430});
|
||||
if(wind){
|
||||
const a=itemAngleFor(fan),f=82*wind.falloff*dt;
|
||||
item.vx=(item.vx||0)+Math.cos(a)*f;
|
||||
item.vy=(item.vy||0)+Math.sin(a)*f;
|
||||
}
|
||||
}
|
||||
item.x+=(item.vx||0)*dt;
|
||||
item.y+=(item.vy||0)*dt;
|
||||
item.vx*=Math.pow(.62,dt);
|
||||
item.vy*=Math.pow(.62,dt);
|
||||
for(const t of world?.tarinai||[]){
|
||||
if(!t||t.dead)continue;
|
||||
if(distXY(item.x,item.y,t.x,t.y)<=item.r+(t.radius||18)) t.damage?.(.16*item.gasConcentration*dt,"\u6bd2\u30ac\u30b9",{source:item});
|
||||
}
|
||||
if(item.gasConcentration<=.01)item.amount=0;
|
||||
return true;
|
||||
}
|
||||
|
||||
function update(item,dt,world){
|
||||
if(!item||item.dead)return false;
|
||||
if(item.type==="oil")return updateOil(item,dt,world);
|
||||
if(item.type==="bear_trap")return updateBearTrap(item,dt,world);
|
||||
if(item.type==="leakage_device")return updateLeakage(item,dt,world);
|
||||
if(item.type==="poison_gas_can")return updatePoisonGasCan(item,dt,world);
|
||||
if(item.type==="poison_gas")return updateGas(item,dt,world);
|
||||
if(item.type==="mamekusaredake"||item.type==="mame_spore") return global.TarinaiMamekusaredakeSystem?.updateItem?.(item,dt,world) || false;
|
||||
return false;
|
||||
}
|
||||
|
||||
global.TarinaiItemEnvironmentHazardSystem=Object.freeze({
|
||||
update,
|
||||
updatePoweredHazards,
|
||||
enforceTrapHolds,
|
||||
groundFrictionMultiplier,
|
||||
igniteOil,
|
||||
igniteOilNear,
|
||||
spawnPoisonGas,
|
||||
burstPoisonGasCan,
|
||||
leakageRadius,
|
||||
});
|
||||
})(typeof window!=="undefined"?window:globalThis);
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
(function (global) {
|
||||
function update(item, dt, worldRef) {
|
||||
if (!item || item.dead) return { done: true };
|
||||
item.age += dt;
|
||||
item.age = Math.max(0, Number.isFinite(Number(item.age)) ? Number(item.age) : 0) + Math.max(0, Number(dt) || 0);
|
||||
if (item.type === "zunchi") item.spawnGrace = Math.max(0, (item.spawnGrace || 0) - dt);
|
||||
if (item.type === "toilet") item.toiletFlash = Math.max(0, (item.toiletFlash || 0) - dt * 1.8);
|
||||
if (item.dropTimer > 0) {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ function duplicatorSetStoredType(duplicator, storedType = "", worldRef = null, o
|
|||
worldRef?.markItemBucketsDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
|
||||
worldRef?.markSpatialDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
|
||||
worldRef?.markTerrainDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
|
||||
if (options.log !== false) worldRef?.log?.(`\u8907\u88fd\u6a5f\u306b${duplicator.storedFoodLabel}\u3092\u30bb\u30c3\u30c8\u3057\u305f\u3002`, isPinType(loadType) ? "observe" : "food");
|
||||
// Direct duplicator loading is a player operation and is not an event-log entry.
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ function duplicatorApplyStoredPin(duplicator, worldRef) {
|
|||
duplicator.usedCount = (duplicator.usedCount || 0) + 1;
|
||||
duplicator.nextDuplicatedPinAt = now + 0.82;
|
||||
duplicator.duplicatorPinFlash = 0.24;
|
||||
worldRef.effects?.push(new Effect("ring", dish.x, dish.y, { size: Math.max(15, (duplicator.r || 34) * 0.62), life: 0.18, color: storedType === "oshibyo" ? "rgba(73,119,205,0.40)" : "rgba(214,72,72,0.42)" }));
|
||||
worldRef.queueEffect?.("ring", dish.x, dish.y, { size: Math.max(15, (duplicator.r || 34) * 0.62), life: 0.18, color: storedType === "oshibyo" ? "rgba(73,119,205,0.40)" : "rgba(214,72,72,0.42)" });
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef.markSpatialDirty?.("duplicator-pin-lodged");
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -629,37 +629,50 @@ draw(ctx, t, lighting = null) {
|
|||
const r = this.r || 60;
|
||||
const tilt = Number(this.seesawTilt || 0) || 0;
|
||||
ctx.save();
|
||||
ctx.fillStyle = styleNight ? "rgba(81,68,57,0.96)" : "rgba(130,99,69,0.98)";
|
||||
ctx.strokeStyle = styleNight ? "rgba(41,34,29,0.86)" : "rgba(78,55,37,0.82)";
|
||||
ctx.lineWidth = 2.4;
|
||||
const standGrad = ctx.createLinearGradient(-r * 0.30, -r * 0.05, r * 0.30, r * 0.55);
|
||||
standGrad.addColorStop(0, styleNight ? "#667078" : "#a9b6bd");
|
||||
standGrad.addColorStop(0.55, styleNight ? "#3f474d" : "#718089");
|
||||
standGrad.addColorStop(1, styleNight ? "#2b3135" : "#4a555c");
|
||||
ctx.fillStyle = standGrad;
|
||||
ctx.strokeStyle = styleNight ? "rgba(22,25,28,0.94)" : "rgba(48,57,63,0.92)";
|
||||
ctx.lineWidth = 2.6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-r * 0.22, r * 0.42);
|
||||
ctx.lineTo(0, -r * 0.05);
|
||||
ctx.lineTo(r * 0.22, r * 0.42);
|
||||
ctx.closePath(); ctx.fill(); ctx.stroke();
|
||||
ctx.moveTo(-r * 0.34, r * 0.52);
|
||||
ctx.lineTo(-r * 0.12, -r * 0.04);
|
||||
ctx.quadraticCurveTo(0, -r * 0.20, r * 0.12, -r * 0.04);
|
||||
ctx.lineTo(r * 0.34, r * 0.52);
|
||||
ctx.closePath();
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = styleNight ? "#31383c" : "#526069";
|
||||
roundedRect(ctx, -r * 0.52, r * 0.45, r * 1.04, r * 0.16, r * 0.07);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.save();
|
||||
ctx.rotate(tilt);
|
||||
const grad = ctx.createLinearGradient(0, -r * 0.14, 0, r * 0.14);
|
||||
grad.addColorStop(0, styleNight ? "#c5895c" : "#f0ad6f");
|
||||
grad.addColorStop(1, styleNight ? "#8d5d3f" : "#ba7447");
|
||||
ctx.fillStyle = grad;
|
||||
ctx.strokeStyle = styleNight ? "rgba(57,37,29,0.9)" : "rgba(104,59,35,0.85)";
|
||||
roundedRect(ctx, -r * 1.08, -r * 0.13, r * 2.16, r * 0.26, r * 0.08);
|
||||
const boardGrad = ctx.createLinearGradient(0, -r * 0.17, 0, r * 0.17);
|
||||
boardGrad.addColorStop(0, styleNight ? "#c58a27" : "#ffc94f");
|
||||
boardGrad.addColorStop(0.48, styleNight ? "#9e681c" : "#eaa52d");
|
||||
boardGrad.addColorStop(1, styleNight ? "#774b17" : "#b96c1c");
|
||||
ctx.fillStyle = boardGrad;
|
||||
ctx.strokeStyle = styleNight ? "#4b2f12" : "#734315";
|
||||
ctx.lineWidth = 3;
|
||||
roundedRect(ctx, -r * 1.16, -r * 0.14, r * 2.32, r * 0.28, r * 0.10);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = "rgba(255,255,255,0.28)";
|
||||
roundedRect(ctx, -r * 1.05, -r * 0.095, r * 2.10, r * 0.055, r * 0.025);
|
||||
ctx.fill();
|
||||
for (const side of [-1, 1]) {
|
||||
ctx.fillStyle = side < 0 ? "rgba(88,177,231,0.98)" : "rgba(239,111,116,0.98)";
|
||||
roundedRect(ctx, side * r * 0.79 - r * 0.18, -r * 0.26, r * 0.36, r * 0.22, r * 0.07);
|
||||
ctx.fillStyle = side < 0 ? (styleNight ? "#6a314a" : "#df5f8d") : (styleNight ? "#31577a" : "#5d9ee0");
|
||||
ctx.strokeStyle = styleNight ? "#1f2428" : "#3c4650";
|
||||
ctx.lineWidth = 2.2;
|
||||
roundedRect(ctx, side * r * 0.78 - r * 0.22, -r * 0.22, r * 0.44, r * 0.24, r * 0.09);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.strokeStyle = "rgba(70,55,45,0.75)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(side * r * 0.90, -r * 0.22);
|
||||
ctx.lineTo(side * r * 0.90, -r * 0.52);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = styleNight ? "#b8c0c4" : "#dce3e5";
|
||||
ctx.strokeStyle = styleNight ? "#3a4145" : "#59656b";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath(); ctx.arc(0, 0, r * 0.13, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
ctx.beginPath(); ctx.arc(0, 0, r * 0.045, 0, Math.PI * 2); ctx.fillStyle = "#596168"; ctx.fill();
|
||||
ctx.restore();
|
||||
ctx.fillStyle = "rgba(70,58,48,0.9)";
|
||||
ctx.beginPath(); ctx.arc(0, 0, r * 0.13, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.restore();
|
||||
} else if (this.type === "circuit_board") {
|
||||
const circuit = globalThis.TarinaiCircuitBoardSystem;
|
||||
|
|
@ -823,33 +836,57 @@ draw(ctx, t, lighting = null) {
|
|||
} else if (this.type === "stove") {
|
||||
const r = this.r || 24;
|
||||
const powered = globalThis.TarinaiSignalSystem?.powerOverride?.(this) !== false;
|
||||
const timeNow = this.world?.time || (typeof performance !== "undefined" ? performance.now() * 0.001 : 0);
|
||||
ctx.save();
|
||||
if (powered && itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(255, 120, 43, 0.34)";
|
||||
ctx.shadowBlur = 9;
|
||||
ctx.shadowColor = "rgba(255, 116, 36, 0.42)";
|
||||
ctx.shadowBlur = 13;
|
||||
}
|
||||
ctx.fillStyle = styleNight ? "rgba(96, 72, 58, 0.96)" : "rgba(150, 104, 76, 0.96)";
|
||||
ctx.strokeStyle = styleNight ? "rgba(49, 35, 29, 0.78)" : "rgba(92, 62, 45, 0.72)";
|
||||
ctx.lineWidth = 2.2;
|
||||
roundedRect(ctx, -r * 0.78, -r * 0.62, r * 1.56, r * 1.24, r * 0.20);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "rgba(42, 32, 28, 0.92)";
|
||||
roundedRect(ctx, -r * 0.48, -r * 0.34, r * 0.96, r * 0.62, r * 0.14);
|
||||
ctx.fill();
|
||||
const iron = ctx.createLinearGradient(-r, -r, r, r);
|
||||
iron.addColorStop(0, styleNight ? "#4a4d4e" : "#737778");
|
||||
iron.addColorStop(0.45, styleNight ? "#292c2d" : "#484d4e");
|
||||
iron.addColorStop(1, styleNight ? "#17191a" : "#292d2e");
|
||||
ctx.fillStyle = iron;
|
||||
ctx.strokeStyle = "rgba(18,20,20,0.94)";
|
||||
ctx.lineWidth = 2.6;
|
||||
roundedRect(ctx, -r * 0.22, -r * 1.34, r * 0.44, r * 0.72, r * 0.08);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = styleNight ? "#26292a" : "#3f4445";
|
||||
roundedRect(ctx, -r * 0.84, -r * 0.70, r * 1.68, r * 1.38, r * 0.18);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = styleNight ? "#101213" : "#242829";
|
||||
ctx.beginPath(); ctx.ellipse(0, -r * 0.68, r * 0.62, r * 0.18, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
ctx.strokeStyle = powered ? "rgba(255,136,54,0.76)" : "rgba(112,116,115,0.62)";
|
||||
ctx.lineWidth = 1.8;
|
||||
for (const rr of [0.44, 0.28]) { ctx.beginPath(); ctx.ellipse(0, -r * 0.68, r * rr, r * rr * 0.29, 0, 0, Math.PI * 2); ctx.stroke(); }
|
||||
ctx.fillStyle = "#0d1010";
|
||||
ctx.strokeStyle = styleNight ? "#6a6f70" : "#8b9293";
|
||||
ctx.lineWidth = 2.4;
|
||||
roundedRect(ctx, -r * 0.54, -r * 0.30, r * 1.08, r * 0.68, r * 0.10);
|
||||
ctx.fill(); ctx.stroke();
|
||||
if (powered) {
|
||||
const flame = 0.85 + 0.15 * Math.sin((this.world?.time || (typeof performance !== "undefined" ? performance.now() * 0.001 : 0)) * 4.2 + (this.seed || 0));
|
||||
ctx.fillStyle = "rgba(255, 168, 57, 0.92)";
|
||||
const flicker = 0.88 + Math.sin(timeNow * 5.1 + (this.seed || 0)) * 0.08;
|
||||
const fireGrad = ctx.createRadialGradient(0, r * 0.16, 1, 0, r * 0.10, r * 0.54);
|
||||
fireGrad.addColorStop(0, "rgba(255,246,150,0.98)");
|
||||
fireGrad.addColorStop(0.35, "rgba(255,169,53,0.96)");
|
||||
fireGrad.addColorStop(1, "rgba(194,49,20,0.28)");
|
||||
ctx.fillStyle = fireGrad;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, r * 0.16);
|
||||
ctx.bezierCurveTo(-r * 0.38, -r * 0.10, -r * 0.10, -r * 0.52 * flame, 0, -r * 0.66 * flame);
|
||||
ctx.bezierCurveTo(r * 0.34, -r * 0.20, r * 0.40, -r * 0.02, 0, r * 0.16);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "rgba(255, 235, 120, 0.82)";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, -r * 0.10, r * 0.16, r * 0.28 * flame, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.moveTo(-r * 0.42, r * 0.30);
|
||||
ctx.bezierCurveTo(-r * 0.48, r * 0.02, -r * 0.28, -r * 0.20 * flicker, -r * 0.10, r * 0.03);
|
||||
ctx.bezierCurveTo(0, -r * 0.34 * flicker, r * 0.25, -r * 0.12, r * 0.16, r * 0.05);
|
||||
ctx.bezierCurveTo(r * 0.42, -r * 0.04, r * 0.46, r * 0.17, r * 0.40, r * 0.30);
|
||||
ctx.closePath(); ctx.fill();
|
||||
ctx.strokeStyle = "rgba(255,106,36,0.90)"; ctx.lineWidth = r * 0.11; ctx.lineCap = "round";
|
||||
ctx.beginPath(); ctx.moveTo(-r * 0.31, r * 0.25); ctx.lineTo(r * 0.26, r * 0.13); ctx.moveTo(-r * 0.25, r * 0.10); ctx.lineTo(r * 0.31, r * 0.26); ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = styleNight ? "#858b8d" : "#aeb5b6";
|
||||
ctx.beginPath(); ctx.arc(r * 0.42, r * 0.02, r * 0.065, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.strokeStyle = styleNight ? "#686e70" : "#9da4a5"; ctx.lineWidth = 2;
|
||||
for (const sx of [-1, 1]) { ctx.beginPath(); ctx.moveTo(sx * r * 0.70, -r * 0.15); ctx.lineTo(sx * r * 0.70, r * 0.30); ctx.stroke(); }
|
||||
ctx.fillStyle = styleNight ? "#1a1c1d" : "#303435";
|
||||
roundedRect(ctx, -r * 0.68, r * 0.58, r * 0.28, r * 0.30, r * 0.05); ctx.fill();
|
||||
roundedRect(ctx, r * 0.40, r * 0.58, r * 0.28, r * 0.30, r * 0.05); ctx.fill();
|
||||
ctx.restore();
|
||||
} else if (this.type === "ant_nest") {
|
||||
const count = clamp(Math.round(this.antCount ?? ANT_NEST_START_COUNT ?? 6), 0, ANT_NEST_MAX_COUNT ?? 10);
|
||||
|
|
@ -944,6 +981,321 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "oil") {
|
||||
const r = this.r || 58;
|
||||
const seed = Number(this.seed || 0);
|
||||
ctx.save();
|
||||
ctx.scale(1.42, 0.70);
|
||||
const puddle = ctx.createRadialGradient(-r * 0.28, -r * 0.30, r * 0.08, 0, 0, r * 1.05);
|
||||
if (this.oilBurning) {
|
||||
puddle.addColorStop(0, "rgba(92,65,25,0.96)");
|
||||
puddle.addColorStop(0.48, "rgba(54,42,23,0.96)");
|
||||
puddle.addColorStop(1, "rgba(22,20,16,0.94)");
|
||||
} else {
|
||||
puddle.addColorStop(0, "rgba(118,94,42,0.94)");
|
||||
puddle.addColorStop(0.42, "rgba(58,51,34,0.96)");
|
||||
puddle.addColorStop(1, "rgba(20,22,19,0.94)");
|
||||
}
|
||||
ctx.fillStyle = puddle;
|
||||
ctx.strokeStyle = "rgba(16,17,14,0.78)";
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.beginPath();
|
||||
const points = 18;
|
||||
for (let i = 0; i <= points; i++) {
|
||||
const a = Math.PI * 2 * i / points;
|
||||
const wobble = 0.86 + 0.10 * Math.sin(seed * 0.37 + i * 1.73) + 0.06 * Math.cos(seed * 0.19 + i * 2.41);
|
||||
const px = Math.cos(a) * r * wobble;
|
||||
const py = Math.sin(a) * r * wobble;
|
||||
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha *= 0.62;
|
||||
ctx.strokeStyle = this.oilBurning ? "rgba(255,151,45,0.46)" : "rgba(142,184,149,0.44)";
|
||||
ctx.lineWidth = 3.2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-r * 0.20, -r * 0.18, r * 0.50, r * 0.17, -0.18, 3.42, 5.82);
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = this.oilBurning ? "rgba(255,207,92,0.32)" : "rgba(180,135,214,0.32)";
|
||||
ctx.lineWidth = 2.1;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(r * 0.22, r * 0.16, r * 0.34, r * 0.10, 0.24, 0.15, 2.82);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
} else if (this.type === "bear_trap") {
|
||||
const r = this.r || 34;
|
||||
const closed = Boolean(this.trapClosed || this.trapTargetId);
|
||||
ctx.save();
|
||||
ctx.rotate((this.angle || 0) + 0.02 * Math.sin(this.seed || 0));
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(0,0,0,0.22)";
|
||||
ctx.shadowBlur = 4;
|
||||
ctx.shadowOffsetY = 3;
|
||||
}
|
||||
// Chain and anchor ring.
|
||||
ctx.strokeStyle = "rgba(72,66,61,0.95)";
|
||||
ctx.lineWidth = 3.3;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const x = r * 0.78 + i * r * 0.28;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, r * 0.52 + Math.sin(i * 1.8) * 2, r * 0.16, r * 0.10, i % 2 ? 0.9 : -0.9, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.arc(r * 2.05, r * 0.52, r * 0.23, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
// Heavy base ring.
|
||||
const metal = ctx.createLinearGradient(0, -r, 0, r);
|
||||
metal.addColorStop(0, "#c6c8c6");
|
||||
metal.addColorStop(0.48, "#757a78");
|
||||
metal.addColorStop(1, "#3f4543");
|
||||
ctx.fillStyle = "rgba(63,68,66,0.94)";
|
||||
ctx.strokeStyle = "rgba(38,42,41,0.98)";
|
||||
ctx.lineWidth = 5;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, r * 0.88, r * 0.68, 0, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
// Side springs.
|
||||
for (const sx of [-1, 1]) {
|
||||
ctx.save();
|
||||
ctx.translate(sx * r * 0.76, 0);
|
||||
ctx.strokeStyle = "rgba(84,88,86,0.98)";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i <= 7; i++) {
|
||||
const yy = -r * 0.30 + i * r * 0.086;
|
||||
const xx = (i % 2 ? 1 : -1) * r * 0.09;
|
||||
if (i === 0) ctx.moveTo(xx, yy); else ctx.lineTo(xx, yy);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
const gap = closed ? r * 0.14 : r * 0.46;
|
||||
// Two opposing toothed jaws.
|
||||
for (const side of [-1, 1]) {
|
||||
ctx.save();
|
||||
ctx.translate(0, side * gap);
|
||||
ctx.scale(1, side);
|
||||
ctx.fillStyle = metal;
|
||||
ctx.strokeStyle = "rgba(52,57,55,0.98)";
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-r * 0.80, 0);
|
||||
ctx.quadraticCurveTo(0, -r * 0.36, r * 0.80, 0);
|
||||
ctx.lineTo(r * 0.70, r * 0.14);
|
||||
ctx.quadraticCurveTo(0, -r * 0.16, -r * 0.70, r * 0.14);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
for (let i = -4; i <= 4; i++) {
|
||||
const x = i * r * 0.16;
|
||||
const toothH = r * (closed ? 0.18 : 0.25);
|
||||
ctx.fillStyle = "#b8bbb8";
|
||||
ctx.strokeStyle = "rgba(54,58,56,0.96)";
|
||||
ctx.lineWidth = 1.4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - r * 0.055, -r * 0.03);
|
||||
ctx.lineTo(x + r * 0.055, -r * 0.03);
|
||||
ctx.lineTo(x, -toothH);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
if (!closed) {
|
||||
ctx.fillStyle = "#737873";
|
||||
ctx.strokeStyle = "#454a47";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, r * 0.34, r * 0.24, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "rgba(220,224,218,0.26)";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-r * 0.08, -r * 0.06, r * 0.18, r * 0.08, -0.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "leakage_device") {
|
||||
const r = this.r || 34;
|
||||
const powered = Boolean(this._signalDriven && this._signalOn);
|
||||
ctx.save();
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(0,0,0,0.24)";
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.shadowOffsetY = 4;
|
||||
}
|
||||
const baseGrad = ctx.createLinearGradient(-r, -r, r, r);
|
||||
baseGrad.addColorStop(0, "#58636a");
|
||||
baseGrad.addColorStop(0.5, "#30383e");
|
||||
baseGrad.addColorStop(1, "#1c2429");
|
||||
ctx.fillStyle = baseGrad;
|
||||
ctx.strokeStyle = "#151b1f";
|
||||
ctx.lineWidth = 2.6;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(-r * 0.92, r * 0.18, r * 1.84, r * 0.72, r * 0.12);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
// Porcelain insulators.
|
||||
for (const sx of [-1, 1]) {
|
||||
ctx.save();
|
||||
ctx.translate(sx * r * 0.52, -r * 0.18);
|
||||
const porcelain = ctx.createLinearGradient(-r * 0.22, 0, r * 0.22, 0);
|
||||
porcelain.addColorStop(0, "#8e9697");
|
||||
porcelain.addColorStop(0.45, "#eef0e8");
|
||||
porcelain.addColorStop(1, "#747d7e");
|
||||
ctx.fillStyle = porcelain;
|
||||
ctx.strokeStyle = "rgba(65,72,73,0.94)";
|
||||
ctx.lineWidth = 1.6;
|
||||
ctx.fillRect(-r * 0.13, -r * 0.58, r * 0.26, r * 0.70);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, -r * 0.50 + i * r * 0.17, r * 0.24, r * 0.075, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = "#9f713d";
|
||||
ctx.fillRect(-r * 0.07, -r * 0.76, r * 0.14, r * 0.18);
|
||||
ctx.restore();
|
||||
}
|
||||
// Copper transformer coil.
|
||||
ctx.strokeStyle = powered ? "#ffc763" : "#b4773b";
|
||||
ctx.lineWidth = 3.2;
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const yy = -r * 0.35 + i * r * 0.10;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, yy, r * 0.27, r * 0.08, 0, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = "rgba(241,194,62,0.95)";
|
||||
ctx.strokeStyle = "rgba(80,61,18,0.95)";
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-r * 0.18, r * 0.46);
|
||||
ctx.lineTo(0, r * 0.22);
|
||||
ctx.lineTo(r * 0.18, r * 0.46);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
if (powered) {
|
||||
ctx.shadowColor = "rgba(112,220,255,0.78)";
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.strokeStyle = "rgba(142,232,255,0.96)";
|
||||
ctx.lineWidth = 2.2;
|
||||
for (let arc = 0; arc < 3; arc++) {
|
||||
const phase = t * 13 + (this.seed || 0) * 0.17 + arc * 1.9;
|
||||
ctx.beginPath();
|
||||
let x = -r * 0.48;
|
||||
let y = -r * 0.73;
|
||||
ctx.moveTo(x, y);
|
||||
for (let j = 1; j <= 6; j++) {
|
||||
x = -r * 0.48 + j * r * 0.16;
|
||||
y = -r * 0.73 + Math.sin(phase + j * 2.1) * r * 0.16;
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "poison_gas_can") {
|
||||
const r = this.r || 28;
|
||||
const fuseRatio = clamp((this.fuseTimer ?? 5) / Math.max(0.1, this.fuseMax || 5), 0, 1);
|
||||
ctx.save();
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(0,0,0,0.26)";
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.shadowOffsetY = 4;
|
||||
}
|
||||
const body = ctx.createLinearGradient(-r * 0.72, 0, r * 0.72, 0);
|
||||
body.addColorStop(0, "#493552");
|
||||
body.addColorStop(0.24, "#80588f");
|
||||
body.addColorStop(0.54, "#9c72a7");
|
||||
body.addColorStop(0.78, "#64446f");
|
||||
body.addColorStop(1, "#382d41");
|
||||
ctx.fillStyle = body;
|
||||
ctx.strokeStyle = "rgba(49,36,56,0.98)";
|
||||
ctx.lineWidth = 2.4;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(-r * 0.62, -r * 0.82, r * 1.24, r * 1.64, r * 0.28);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
// Reinforcing bands.
|
||||
for (const yy of [-r * 0.44, r * 0.38]) {
|
||||
ctx.fillStyle = "rgba(49,43,52,0.88)";
|
||||
ctx.fillRect(-r * 0.67, yy, r * 1.34, r * 0.12);
|
||||
ctx.fillStyle = "rgba(190,151,72,0.72)";
|
||||
for (let i = -2; i <= 2; i++) ctx.fillRect(i * r * 0.25 - r * 0.05, yy, r * 0.10, r * 0.12);
|
||||
}
|
||||
// Valve, cap and pressure gauge.
|
||||
ctx.fillStyle = "#6d7471";
|
||||
ctx.strokeStyle = "#303633";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.fillRect(-r * 0.18, -r * 1.02, r * 0.36, r * 0.23);
|
||||
ctx.strokeRect(-r * 0.18, -r * 1.02, r * 0.36, r * 0.23);
|
||||
ctx.beginPath();
|
||||
ctx.arc(r * 0.35, -r * 0.91, r * 0.18, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "#d9d6c5";
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = fuseRatio < 0.28 && Math.sin(t * 20) > 0 ? "#ff4f5f" : "#6e2d37";
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(r * 0.35, -r * 0.91);
|
||||
ctx.lineTo(r * 0.35 + Math.cos(-2.5 + fuseRatio * 2.0) * r * 0.12, -r * 0.91 + Math.sin(-2.5 + fuseRatio * 2.0) * r * 0.12);
|
||||
ctx.stroke();
|
||||
// Corrosion and vent dots.
|
||||
ctx.fillStyle = "rgba(66,42,31,0.54)";
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const a = (this.seed || 0) * 0.21 + i * 1.7;
|
||||
ctx.beginPath();
|
||||
ctx.arc(Math.sin(a) * r * 0.43, Math.cos(a * 1.3) * r * 0.58, r * (0.035 + (i % 3) * 0.018), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.fillStyle = "rgba(225,205,118,0.88)";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -r * 0.18);
|
||||
ctx.lineTo(-r * 0.24, r * 0.18);
|
||||
ctx.lineTo(r * 0.24, r * 0.18);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "rgba(73,45,82,0.95)";
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, r * 0.04, r * 0.06, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
const timerText = String(Math.max(0, Math.ceil(this.fuseTimer ?? 5)));
|
||||
ctx.fillStyle = "rgba(255,249,230,0.98)";
|
||||
ctx.strokeStyle = "rgba(53,31,61,0.98)";
|
||||
ctx.lineWidth = Math.max(2.4, r * 0.09);
|
||||
ctx.font = `900 ${Math.max(12, r * 0.68)}px ui-rounded, sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.strokeText(timerText, 0, r * 0.08);
|
||||
ctx.fillText(timerText, 0, r * 0.08);
|
||||
ctx.restore();
|
||||
} else if (this.type === "poison_gas") {
|
||||
const r = this.r || 96;
|
||||
const c = Math.max(0, Math.min(1, this.gasConcentration ?? 1));
|
||||
ctx.save();
|
||||
ctx.globalAlpha *= 0.16 + 0.46 * c;
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const a = (this.seed || 0) * 0.13 + i * 2.17;
|
||||
const ox = Math.cos(a) * r * (0.16 + (i % 3) * 0.07);
|
||||
const oy = Math.sin(a * 1.21) * r * (0.12 + (i % 2) * 0.07);
|
||||
const rr = r * (0.48 + (i % 4) * 0.08);
|
||||
const g = ctx.createRadialGradient(ox - rr * 0.18, oy - rr * 0.18, rr * 0.10, ox, oy, rr);
|
||||
g.addColorStop(0, `rgba(198,112,226,${0.60 + c * 0.22})`);
|
||||
g.addColorStop(0.54, `rgba(132,62,166,${0.34 + c * 0.28})`);
|
||||
g.addColorStop(1, "rgba(83,39,112,0.02)");
|
||||
ctx.fillStyle = g;
|
||||
ctx.beginPath();
|
||||
ctx.arc(ox, oy, rr, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "mamekusaredake" || this.type === "mame_spore") {
|
||||
globalThis.TarinaiMamekusaredakeSystem?.drawItem?.(ctx, this, t);
|
||||
} else if (this.type === "fan") {
|
||||
const r = this.r || 26;
|
||||
const bodyAngle = Number.isFinite(Number(this.fanBodyAngle)) ? normalizedItemAngle(Number(this.fanBodyAngle), itemAngleFor(this)) : itemAngleFor(this);
|
||||
|
|
@ -1163,34 +1515,73 @@ draw(ctx, t, lighting = null) {
|
|||
const fuse = clamp((this.fuseTimer ?? 5) / Math.max(this.fuseMax || 5, 0.1), 0, 1);
|
||||
const flash = fuse < 0.35 ? (0.5 + Math.sin(t * 20 + this.seed) * 0.5) : 0;
|
||||
const timerText = String(Math.max(0, Math.ceil(this.fuseTimer ?? 5)));
|
||||
ctx.rotate(-0.22 + Math.sin(this.seed) * 0.12);
|
||||
ctx.fillStyle = flash > 0.65 ? "#ffed77" : (flameMode ? "#d8241f" : "#5a4744");
|
||||
ctx.strokeStyle = flameMode ? "rgba(96,18,18,0.88)" : "rgba(35,30,29,0.88)";
|
||||
ctx.lineWidth = 2;
|
||||
roundedRect(ctx, -this.r * 0.72, -this.r * 0.86, this.r * 1.44, this.r * 1.72, 5);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = flameMode ? "#ff6b32" : "#897065";
|
||||
ctx.fillRect(-this.r * 0.55, -this.r * 0.54, this.r * 1.1, this.r * 0.20);
|
||||
ctx.fillRect(-this.r * 0.55, this.r * 0.34, this.r * 1.1, this.r * 0.20);
|
||||
ctx.strokeStyle = "rgba(48,38,31,0.86)";
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -this.r * 0.88);
|
||||
ctx.quadraticCurveTo(this.r * 0.32, -this.r * 1.34, this.r * 0.92, -this.r * 1.42);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = flash > 0.1 ? "rgba(255,221,82,0.92)" : (flameMode ? "rgba(255,80,40,0.78)" : "rgba(188,148,88,0.62)");
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.r * 0.98, -this.r * 1.42, 2.5 + flash * 2.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "rgba(255,246,214,0.92)";
|
||||
ctx.strokeStyle = flameMode ? "rgba(84,18,14,0.92)" : "rgba(24,22,21,0.92)";
|
||||
ctx.save();
|
||||
ctx.rotate(-0.18 + Math.sin(this.seed) * 0.10);
|
||||
const bodyGrad = ctx.createLinearGradient(-this.r * 0.74, 0, this.r * 0.74, 0);
|
||||
if (flameMode) {
|
||||
bodyGrad.addColorStop(0, flash > 0.72 ? "#ffe36c" : "#6c1516");
|
||||
bodyGrad.addColorStop(0.28, "#d52c25");
|
||||
bodyGrad.addColorStop(0.56, "#f2522d");
|
||||
bodyGrad.addColorStop(1, "#7f1817");
|
||||
} else {
|
||||
bodyGrad.addColorStop(0, flash > 0.72 ? "#ffef91" : "#5e221f");
|
||||
bodyGrad.addColorStop(0.28, "#a73a30");
|
||||
bodyGrad.addColorStop(0.56, "#c74b3c");
|
||||
bodyGrad.addColorStop(1, "#67241f");
|
||||
}
|
||||
ctx.fillStyle = bodyGrad;
|
||||
ctx.strokeStyle = flameMode ? "rgba(80,16,14,0.96)" : "rgba(66,25,22,0.96)";
|
||||
ctx.lineWidth = 2.6;
|
||||
ctx.font = `bold ${Math.max(10, this.r * 0.72)}px ui-rounded, sans-serif`;
|
||||
roundedRect(ctx, -this.r * 0.66, -this.r * 0.88, this.r * 1.32, this.r * 1.76, this.r * 0.16);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = flameMode ? "#7f1717" : "#6d2822";
|
||||
for (const sy of [-1, 1]) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-this.r * 0.58, sy * this.r * 0.80);
|
||||
for (let i = 0; i <= 6; i++) {
|
||||
const xx = -this.r * 0.58 + i * this.r * 0.193;
|
||||
const yy = sy * this.r * (0.80 + (i % 2 ? 0.11 : 0.02));
|
||||
ctx.lineTo(xx, yy);
|
||||
}
|
||||
ctx.lineTo(this.r * 0.58, sy * this.r * 0.80);
|
||||
ctx.closePath(); ctx.fill();
|
||||
}
|
||||
ctx.fillStyle = flameMode ? "rgba(255,190,48,0.94)" : "rgba(245,222,180,0.90)";
|
||||
roundedRect(ctx, -this.r * 0.52, -this.r * 0.38, this.r * 1.04, this.r * 0.76, this.r * 0.08);
|
||||
ctx.fill();
|
||||
if (flameMode) {
|
||||
ctx.fillStyle = "#d43a22";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, this.r * 0.24);
|
||||
ctx.bezierCurveTo(-this.r * 0.24, this.r * 0.03, -this.r * 0.08, -this.r * 0.19, 0, -this.r * 0.29);
|
||||
ctx.bezierCurveTo(this.r * 0.28, -this.r * 0.06, this.r * 0.25, this.r * 0.12, 0, this.r * 0.24);
|
||||
ctx.fill();
|
||||
} else {
|
||||
ctx.strokeStyle = "#8a342b"; ctx.lineWidth = 2;
|
||||
ctx.beginPath(); ctx.moveTo(-this.r * 0.28, 0); ctx.lineTo(this.r * 0.28, 0); ctx.moveTo(0, -this.r * 0.22); ctx.lineTo(0, this.r * 0.22); ctx.stroke();
|
||||
}
|
||||
ctx.strokeStyle = "rgba(63,52,37,0.98)";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(this.r * 0.10, -this.r * 0.90);
|
||||
ctx.bezierCurveTo(this.r * 0.30, -this.r * 1.18, this.r * 0.58, -this.r * 1.34, this.r * 0.88, -this.r * 1.25);
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = "rgba(196,151,73,0.82)"; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.moveTo(this.r * 0.13, -this.r * 0.92); ctx.bezierCurveTo(this.r * 0.34, -this.r * 1.16, this.r * 0.60, -this.r * 1.27, this.r * 0.86, -this.r * 1.23); ctx.stroke();
|
||||
ctx.fillStyle = flash > 0.1 ? "rgba(255,235,104,0.98)" : "rgba(240,104,46,0.88)";
|
||||
ctx.shadowColor = flash > 0.1 ? "rgba(255,186,44,0.85)" : "transparent";
|
||||
ctx.shadowBlur = flash > 0.1 ? 8 : 0;
|
||||
ctx.beginPath(); ctx.arc(this.r * 0.90, -this.r * 1.24, 3 + flash * 2.3, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.fillStyle = "rgba(255,250,230,0.98)";
|
||||
ctx.strokeStyle = flameMode ? "rgba(92,20,15,0.98)" : "rgba(65,25,22,0.98)";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.font = `900 ${Math.max(11, this.r * 0.74)}px ui-rounded, sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.strokeText(timerText, 0, this.r * 0.02);
|
||||
ctx.fillText(timerText, 0, this.r * 0.02);
|
||||
ctx.restore();
|
||||
} else if (this.type === "fence_v" || this.type === "fence_h" || this.type === "glass_wall" || this.type === "bounce_fence" || this.type === "bounce_fence_v" || this.type === "gate_fence" || this.type === "one_way_fence") {
|
||||
const bounce = this.type === "bounce_fence" || this.type === "bounce_fence_v";
|
||||
const gate = this.type === "gate_fence";
|
||||
|
|
@ -1202,9 +1593,9 @@ draw(ctx, t, lighting = null) {
|
|||
const open = gate && this.gateOpen;
|
||||
ctx.save();
|
||||
ctx.rotate(angle);
|
||||
const wood = bounce ? (styleNight ? "#4e6f9f" : "#5aa2e0") : oneWay ? (styleNight ? "#6b5a86" : "#9b78c8") : gate ? (styleNight ? "#5d675f" : "#6f8b68") : (styleNight ? "#7a5b3c" : (styleWarm ? "#a96f35" : "#965f31"));
|
||||
const edge = bounce ? (styleNight ? "#243755" : "#286093") : oneWay ? (styleNight ? "#352943" : "#593a80") : gate ? (styleNight ? "#334038" : "#375737") : (styleNight ? "#4d3d2f" : "#5f3f25");
|
||||
const hi = bounce ? (styleNight ? "rgba(190,230,255,0.24)" : "rgba(235,252,255,0.54)") : oneWay ? (styleNight ? "rgba(232,220,255,0.25)" : "rgba(255,245,255,0.55)") : gate ? (styleNight ? "rgba(214,240,210,0.18)" : "rgba(220,255,210,0.34)") : (styleNight ? "rgba(198,166,116,0.20)" : "rgba(232,176,94,0.32)");
|
||||
const wood = bounce ? (styleNight ? "#9b4f76" : "#ef76aa") : oneWay ? (styleNight ? "#6b5a86" : "#9b78c8") : gate ? (styleNight ? "#5d675f" : "#6f8b68") : (styleNight ? "#7a5b3c" : (styleWarm ? "#a96f35" : "#965f31"));
|
||||
const edge = bounce ? (styleNight ? "#52253d" : "#a93f70") : oneWay ? (styleNight ? "#352943" : "#593a80") : gate ? (styleNight ? "#334038" : "#375737") : (styleNight ? "#4d3d2f" : "#5f3f25");
|
||||
const hi = bounce ? (styleNight ? "rgba(255,213,235,0.24)" : "rgba(255,238,248,0.58)") : oneWay ? (styleNight ? "rgba(232,220,255,0.25)" : "rgba(255,245,255,0.55)") : gate ? (styleNight ? "rgba(214,240,210,0.18)" : "rgba(220,255,210,0.34)") : (styleNight ? "rgba(198,166,116,0.20)" : "rgba(232,176,94,0.32)");
|
||||
const drawRail = (x0, y0, w, h, alpha = 1) => {
|
||||
ctx.save();
|
||||
ctx.globalAlpha *= alpha;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
poke: { id: "poke", label: "\u3064\u3064\u304f", placeable: false, scalable: false, iconText: "\u{1F448}", tooltip: "\u305f\u308a\u306a\u3044\u3092\u3064\u3064\u304f\u3002\u30dc\u30fc\u30eb\u3082\u3064\u3064\u3044\u3066\u8ee2\u304c\u305b\u308b\u3002" },
|
||||
pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "\u305f\u308a\u306a\u3044\u3084\u4e00\u90e8\u306e\u7f6e\u304d\u3082\u306e\u3092\u79fb\u52d5\u3055\u305b\u308b\u3002" },
|
||||
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" },
|
||||
acquired_tarinai: { id: "acquired_tarinai", label: "おちび", placeable: false, scalable: false, icon: "assets/ui/tool_acquired_tarinai.webp", tooltip: "近所の鬼威惨から貰ったおちび。後天的なたりないらしい。" },
|
||||
water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3084\u706b\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" },
|
||||
rope: { id: "rope", label: "\u30d2\u30e2", placeable: false, scalable: false, iconText: "\u2570", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3075\u306b\u3083\u3075\u306b\u3083\u3057\u305f\u30d2\u30e2\u3067\u7e4b\u3050\u3002" },
|
||||
rod: { id: "rod", label: "\u68d2", placeable: false, scalable: false, iconText: "\u2501", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u66f2\u304c\u3089\u306a\u3044\u68d2\u3067\u63a5\u7d9a\u3059\u308b\u3002" },
|
||||
|
|
@ -20,6 +21,12 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
wire: { id: "wire", label: "\u96fb\u7dda", placeable: false, scalable: false, iconText: "\u2301", tooltip: "\u691c\u77e5\u5668\u306e\u4fe1\u53f7\u3092\u96fb\u6c17\u3067\u4f1d\u3048\u308b\u3002\u89e6\u308b\u3068\u611f\u96fb\u3002" },
|
||||
insulated_wire: { id: "insulated_wire", label: "\u7d76\u7e01\u96fb\u7dda", placeable: false, scalable: false, iconText: "\u2301", tooltip: "\u691c\u77e5\u5668\u306e\u4fe1\u53f7\u3092\u96fb\u6c17\u3067\u4f1d\u3048\u308b\u3002\u611f\u96fb\u3057\u306a\u3044\u3002" },
|
||||
shoot: { id: "shoot", label: "\u5c04\u6483", placeable: false, scalable: false, icon: "assets/ui/tool_shoot.png", tooltip: "\u9283\u3067\u653b\u6483\u3059\u308b\u3002" },
|
||||
oil: { id: "oil", itemType: "oil", label: "\u6cb9", placeable: true, scalable: true, radius: 58, amount: 100, iconText: "\u6cb9", tooltip: "\u6469\u64e6\u3092\u5927\u304d\u304f\u4e0b\u3052\u308b\u3002\u706b\u3084\u7206\u767a\u3067\u5f15\u706b\u3057\u3001\u71c3\u3048\u5c3d\u304d\u308b\u3068\u6d88\u3048\u308b\u3002" },
|
||||
bear_trap: { id: "bear_trap", itemType: "bear_trap", label: "\u30c8\u30e9\u30d0\u30b5\u30df", placeable: true, scalable: false, radius: 34, amount: 999, iconText: "\u7f60", tooltip: "\u52d5\u304f\u3082\u306e\u3092\u6355\u3089\u3048\u3066\u3057\u3070\u3089\u304f\u96e2\u3055\u306a\u3044\u3002" },
|
||||
leakage_device: { id: "leakage_device", itemType: "leakage_device", label: "\u6f0f\u96fb\u5668", placeable: true, scalable: false, radius: 34, amount: 999, iconText: "\u6f0f", tooltip: "\u901a\u96fb\u4e2d\u3001\u5e83\u7bc4\u56f2\u306b\u96fb\u6483\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002" },
|
||||
poison_gas_can: { id: "poison_gas_can", itemType: "poison_gas_can", label: "\u6bd2\u30ac\u30b9\u7f36", placeable: true, scalable: true, radius: 28, amount: 999, iconText: "\u6bd2", tooltip: "\u6bd2\u30ac\u30b9\u3092\u653e\u51fa\u3059\u308b\u3002" },
|
||||
mamekusaredake: { id: "mamekusaredake", itemType: "mamekusaredake", label: "マメクサレダケ", placeable: true, scalable: false, radius: 34, amount: 999, icon: "assets/ui/tool_mamekusaredake.webp", tooltip: "胞子で感染する寄生キノコ。つまむと除去できる。" },
|
||||
mame_spore: { id: "mame_spore", itemType: "mame_spore", label: "マメクサレダケ胞子", placeable: false, scalable: false, radius: 28, amount: 100, iconText: "胞", tooltip: "マメクサレダケの胞子。" },
|
||||
robot_cleaner: { id: "robot_cleaner", itemType: "robot_cleaner", label: "\u30ed\u30dc\u6383\u9664\u6a5f", placeable: true, scalable: false, radius: 22, amount: 999, iconText: "\u25c9", tooltip: "\u30b4\u30df\u3084\u6c5a\u308c\u3092\u7247\u4ed8\u3051\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u8a73\u7d30\u8a2d\u5b9a\u3002" },
|
||||
zunchi: { id: "zunchi", itemType: "zunchi", label: "\u305a\u3093\u3061", placeable: true, scalable: true, radius: 14, amount: 240, icon: "assets/ui/tool_zunchi.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u305a\u3093\u3061\u3002\u75c5\u6c17\u3084\u8349\u306e\u80a5\u6599\u306b\u95a2\u308f\u308b\u3002" },
|
||||
sweet: { id: "sweet", itemType: "sweet", label: "\u305a\u3093\u3060\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_sweet.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u5927\u597d\u7269\u3002\u4e00\u90e8\u306e\u75c5\u6c17\u3092\u6cbb\u305b\u308b\u3002" },
|
||||
|
|
@ -78,6 +85,7 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
ant_corpse: { id: "ant_corpse", itemType: "ant_corpse", label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", placeable: false, scalable: false, radius: 4, amount: 24, simulationOnly: true },
|
||||
toilet: { id: "toilet", itemType: "toilet", label: "\u30c8\u30a4\u30ec\u306e\u7802", placeable: true, scalable: true, radius: 48, amount: 999, iconText: "\u25a3", tooltip: "\u4e0a\u306b\u6392\u6cc4\u3055\u308c\u305f\u305a\u3093\u3061\u304c\u76f4\u3061\u306b\u6d88\u6ec5\u3059\u308b\u3002" },
|
||||
rain_shelter: { id: "rain_shelter", itemType: "rain_shelter", label: "\u30d1\u30e9\u30bd\u30eb", placeable: true, scalable: true, radius: 50, amount: 999, iconText: "\u26f1", tooltip: "\u96e8\u3068\u65e5\u5dee\u3057\u3092\u9632\u304e\u3001\u4f53\u611f\u6c17\u6e29\u3092\u7a4f\u3084\u304b\u306b\u3059\u308b\u3002" },
|
||||
bell: { id: "bell", label: "呼び鈴", placeable: false, scalable: true, iconText: "🔔", tooltip: "周りのたりないを集める" },
|
||||
first_aid: { id: "first_aid", itemType: "first_aid", label: "\u6551\u6025\u85ac", placeable: true, scalable: true, radius: 13, amount: 58, iconText: "\u271a", tooltip: "\u98df\u3079\u308b\u3068HP\u3092\u5373\u6642\u56de\u5fa9\u3059\u308b\u3002" },
|
||||
sedative: { id: "sedative", itemType: "sedative", label: "\u93ae\u9759\u85ac", placeable: true, scalable: true, radius: 13, amount: 58, iconText: "\u25cc", tooltip: "\u98df\u3079\u308b\u3068\u6050\u6016\u3084\u3051\u3093\u304b\u6c17\u5206\u3092\u843d\u3061\u7740\u304b\u305b\u308b\u3002" },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,18 +5,18 @@
|
|||
const ITEM_TRAITS = Object.freeze({
|
||||
obstacle: new Set(["stone", "ball", "balloon", "fan", "nest_box", "bed", "duplicator", "dry_ice", "stove", "rain_shelter", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch", "circuit_board"]),
|
||||
food_interest: new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "slave_chain", "golden_crown", "sleep_drug"]),
|
||||
hazard: new Set(["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
|
||||
hazard: new Set(["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug", "mamekusaredake"]),
|
||||
pin: new Set(["pushpin", "oshibyo"]),
|
||||
kinematic: new Set(["ball", "balloon", "pushpin", "oshibyo", "zunchi"]),
|
||||
freeStackingMovable: new Set(["ball", "balloon", "pushpin", "oshibyo", "zunchi", "water", "zunda_juice", "mercury", "genkotsu", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "ant_corpse"]),
|
||||
sleepFurniture: new Set(["bed", "nest_box", "pipe", "grass_bed"]),
|
||||
draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "rain_shelter", "signboard", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch", "seesaw", "circuit_board"]),
|
||||
draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "rain_shelter", "signboard", "duplicator", "robot_cleaner", "oil", "bear_trap", "leakage_device", "poison_gas_can", "mamekusaredake", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch", "seesaw", "circuit_board"]),
|
||||
});
|
||||
|
||||
|
||||
const FENCE_ITEM_TYPES = Object.freeze(new Set(["fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence"]));
|
||||
const MECHANICAL_ITEM_TYPES = Object.freeze(new Set(["rotator", "poison_block", "reciprocator"]));
|
||||
const ELECTRIC_TOOL_IDS = Object.freeze(["pressure_switch", "wire", "insulated_wire", "circuit_board"]);
|
||||
const ELECTRIC_TOOL_IDS = Object.freeze(["pressure_switch", "wire", "insulated_wire", "circuit_board", "leakage_device"]);
|
||||
const PHYSICS_TOOL_IDS = Object.freeze(["rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch", "glass_wall", "bounce_fence", "gate_fence", "rotator", "poison_block", "reciprocator", "one_way_fence", "fence_h", "circuit_board"]);
|
||||
const PHYSICS_CATEGORY_TOOL_IDS = Object.freeze(PHYSICS_TOOL_IDS.filter(id => !ELECTRIC_TOOL_IDS.includes(id)));
|
||||
const ROTATABLE_ITEM_TYPES = Object.freeze(new Set([...FENCE_ITEM_TYPES, ...MECHANICAL_ITEM_TYPES, "fan", "circuit_board"]));
|
||||
|
|
@ -27,13 +27,13 @@
|
|||
});
|
||||
|
||||
const TOOL_CATEGORIES = Object.freeze([
|
||||
{ id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "new", "poke", "pinch", "copy", "undo", "redo", "delete", "area_delete", "water_hose", "clear_tool"] },
|
||||
{ id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "new", "poke", "pinch", "copy", "undo", "redo", "delete", "area_delete", "clear_tool", "water_hose", "bell"] },
|
||||
{ id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["food", "sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] },
|
||||
{ id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["first_aid", "sedative", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "slave_chain", "golden_crown"] },
|
||||
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "toilet", "rain_shelter", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "seesaw", "signboard"] },
|
||||
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["acquired_tarinai", "zunchi", "toilet", "rain_shelter", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "seesaw", "signboard"] },
|
||||
{ id: "physics", label: "\u7269\u7406", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_CATEGORY_TOOL_IDS },
|
||||
{ id: "electric", label: "\u96fb\u6c17", themeClass: "tool-category-electric", order: 46, toolIds: ELECTRIC_TOOL_IDS },
|
||||
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "pushpin", "oshibyo", "shoot", "ant_nest"] },
|
||||
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "oil", "bear_trap", "poison_gas_can", "mamekusaredake", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "pushpin", "oshibyo", "shoot", "ant_nest"] },
|
||||
]);
|
||||
|
||||
const TOOL_SIZE_ORDER = Object.freeze(["small", "medium", "large"]);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
if (type === "water" || type === "zunda_juice") item.roles.drink = true;
|
||||
if (["sweet", "water", "zunda_juice", "slave_chain", "golden_crown"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = true;
|
||||
if (type === "bed" || type === "nest_box" || type === "pipe") item.roles.sleepPlace = true;
|
||||
if (["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "splat"].includes(type)) item.roles.danger = true;
|
||||
if (["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "splat", "oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "mamekusaredake", "mame_spore"].includes(type)) item.roles.danger = true;
|
||||
if (type === "grass") item.roles.grassMaterial = true;
|
||||
if (type === "grass") {
|
||||
item.grassStage = Math.floor(rand(0, 2.999));
|
||||
|
|
@ -305,6 +305,14 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
item.amount = 999;
|
||||
}
|
||||
|
||||
if (type === "oil") { item.r = 58; item.amount = 100; item.oilBurning = false; item.oilBurnTimer = 0; item.nextOilFireAt = 0; item.deletable = true; }
|
||||
if (type === "bear_trap") { item.r = 34; item.amount = 999; item.trapTargetId = ""; item.trapTargetRef = null; item.trapTriggeredAt = 0; item.trapUntil = 0; item.trapClosed = false; item.trapDamageableTarget = false; item.trapHoldDuration = 0; item.nextTrapDamageAt = 0; item.deletable = true; }
|
||||
if (type === "leakage_device") { item.r = 34; item.amount = 999; item.nextLeakagePulseAt = 0; item.deletable = true; }
|
||||
if (type === "poison_gas_can") { item.r = 28; item.amount = 999; item.fuseMax = 5; item.fuseTimer = 5; item._gasReleased = false; item.deletable = true; }
|
||||
if (type === "poison_gas") { item.r = 96; item.amount = 100; item.gasConcentration = 1; item.gasAge = 0; item.gasLife = 22; item.gasExpansion = 72; item.vx = 0; item.vy = 0; item.deletable = false; }
|
||||
if (type === "mamekusaredake") { item.r = 34; item.amount = 999; item.mamePlanted = true; item.nextSporeAt = 0; item.deletable = true; item.roles.danger = true; }
|
||||
if (type === "mame_spore") { item.r = 28; item.amount = 100; item.sporeAge = 0; item.sporeLife = 9; item.sporeConcentration = 1; item.vx = 0; item.vy = 0; item.deletable = false; item.roles.danger = true; }
|
||||
|
||||
if (globalThis.TarinaiPhysicsBodySystem.isPhysicsType(type)) {
|
||||
globalThis.TarinaiPhysicsBodySystem.invalidateItem(item, "item-initialized");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
// helpers and the item scheduler both read this single policy instead of
|
||||
// duplicating item-type timing rules.
|
||||
(function (global) {
|
||||
const REALTIME_ITEM_TYPES = new Set(["ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "robot_cleaner"]);
|
||||
const REALTIME_ITEM_TYPES = new Set(["oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "mamekusaredake", "mame_spore", "ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "robot_cleaner"]);
|
||||
const PIN_ITEM_TYPES = new Set(["pushpin", "oshibyo"]);
|
||||
const SCHEDULED_ITEM_TYPES = Object.freeze([
|
||||
"ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "pushpin", "oshibyo",
|
||||
"oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "mamekusaredake", "mame_spore", "ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "pushpin", "oshibyo",
|
||||
"plushie", "grass_bed", "zunchi", "robot_cleaner", "duplicator", "water", "trace", "splat", "ant_corpse", "food",
|
||||
"sweet", "love_mochi", "fight_mochi", "sleep_drug", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
|
||||
"stone", "bed", "nest_box", "pipe", "toilet", "rain_shelter", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch",
|
||||
|
|
@ -38,9 +38,11 @@
|
|||
Number(item.lastShotAt || -999) || -999
|
||||
);
|
||||
if (now - lastActiveAt < 0.35) return true;
|
||||
// Fan wind is a continuous force. Scheduling it as an idle item turns the
|
||||
// accumulated dt into periodic gusts, so fans must remain in the realtime lane.
|
||||
if (item.type === "fan") return true;
|
||||
// Fan wind and bear-trap contact are continuous interactions. Scheduling
|
||||
// them as idle items can turn movement/contact into periodic samples and
|
||||
// let a moving object pass completely through between checks.
|
||||
if (item.type === "fan" || item.type === "bear_trap") return true;
|
||||
if (item.type === "poison_gas_can") return Number(item.fuseTimer ?? 5) > 0;
|
||||
if (item.type === "firecracker" || item.type === "flame_firecracker" || item.type === "sticky_bomb") return Boolean(item.fuseTimer > 0 || item.armed || item.active);
|
||||
if (item.type === "robot_cleaner") return Boolean(item.active || item.cleanerActive || item.targetId || item.targetRef || item.state === "clean" || item.state === "return");
|
||||
if (PIN_ITEM_TYPES.has(item.type)) return Boolean(item.pinState && item.pinState !== "idle" && item.pinState !== "ground");
|
||||
|
|
@ -50,6 +52,7 @@
|
|||
|
||||
function idleRealtimeInterval(item) {
|
||||
if (!item) return Infinity;
|
||||
if (item.type === "oil" || item.type === "bear_trap" || item.type === "leakage_device" || item.type === "poison_gas_can" || item.type === "poison_gas" || item.type === "mamekusaredake" || item.type === "mame_spore") return 0.18;
|
||||
if (item.type === "ball" || item.type === "balloon") return 0.24;
|
||||
if (item.type === "fan") return 0.60;
|
||||
if (item.type === "genkotsu") return 0.45;
|
||||
|
|
@ -89,8 +92,8 @@
|
|||
if (item.type === "grass") return Infinity;
|
||||
if (item.type === "duplicator") return adjustedInterval(0.5);
|
||||
if (item.type === "water") return adjustedInterval(1.8);
|
||||
if (item.type === "trace" || item.type === "splat") return adjustedInterval(1.2);
|
||||
if (item.type === "ant_corpse") return adjustedInterval(3.5);
|
||||
if (item.type === "trace" || item.type === "splat") return adjustedInterval(2.8);
|
||||
if (item.type === "ant_corpse") return adjustedInterval(5.5);
|
||||
if (typeof global.isServingFoodType === "function" && global.isServingFoodType(item.type)) return adjustedInterval(5.0);
|
||||
if (isServingFoodType(item.type)) return adjustedInterval(5.0);
|
||||
return Infinity;
|
||||
|
|
|
|||
|
|
@ -108,6 +108,20 @@
|
|||
return Math.max(1, Number(global.TarinaiPerf?.performanceProfile?.().scheduledItemBudget || 30));
|
||||
}
|
||||
|
||||
function beginTouchedFrame(worldRef, state) {
|
||||
if (!state.touchedFrame) state.touchedFrame = [];
|
||||
else state.touchedFrame.length = 0;
|
||||
if (!state.touchedSet) state.touchedSet = new Set();
|
||||
else state.touchedSet.clear();
|
||||
worldRef._itemUpdateTouched = state.touchedFrame;
|
||||
}
|
||||
|
||||
function recordTouched(state, item) {
|
||||
if (!item || item.dead || state.touchedSet.has(item)) return;
|
||||
state.touchedSet.add(item);
|
||||
state.touchedFrame.push(item);
|
||||
}
|
||||
|
||||
function runRealtime(worldRef, state, dt) {
|
||||
let ran = 0;
|
||||
const now = worldRef.time || 0;
|
||||
|
|
@ -123,6 +137,7 @@
|
|||
}
|
||||
if (interval <= 0) {
|
||||
global.TarinaiItemRuntime.updateOne(item, dt, worldRef);
|
||||
recordTouched(state, item);
|
||||
item._schedulerLastAt = now;
|
||||
ran += 1;
|
||||
const nextInterval = itemUpdateInterval(item);
|
||||
|
|
@ -155,6 +170,7 @@
|
|||
}
|
||||
const elapsed = Math.min(10, Math.max(0.001, now - (item._schedulerLastAt ?? entry.time - interval)));
|
||||
global.TarinaiItemRuntime.updateOne(item, elapsed, worldRef);
|
||||
recordTouched(state, item);
|
||||
item._schedulerLastAt = now;
|
||||
ran += 1;
|
||||
dueRuns += 1;
|
||||
|
|
@ -197,6 +213,7 @@
|
|||
const end = global.TarinaiPerf.begin("update.items");
|
||||
try {
|
||||
const state = ensure(worldRef);
|
||||
beginTouchedFrame(worldRef, state);
|
||||
const realtimeEnd = global.TarinaiPerf.begin("update.items.realtime");
|
||||
let realtimeRan = runRealtime(worldRef, state, dt);
|
||||
let ran = realtimeRan;
|
||||
|
|
@ -207,9 +224,11 @@
|
|||
ran += dueRan;
|
||||
if (dueEnd) dueEnd();
|
||||
global.TarinaiSignalSystem?.updateWorld?.(worldRef, dt);
|
||||
global.TarinaiItemEnvironmentHazardSystem?.updatePoweredHazards?.(worldRef, dt);
|
||||
const physicsEnd = global.TarinaiPerf.begin("update.physicsWorld");
|
||||
const physicsBudget = physicsBudgetFor(worldRef);
|
||||
const physicsWorld = global.TarinaiPhysicsWorldSystem.updateWorld(worldRef, dt, physicsBudget);
|
||||
const trapHeld = global.TarinaiItemEnvironmentHazardSystem?.enforceTrapHolds?.(worldRef) || 0;
|
||||
const mechanicalWorld = physicsWorld.mechanical || null;
|
||||
const constraintWorld = physicsWorld.constraintRan || 0;
|
||||
const postConstraintPairs = physicsWorld.postConstraintPairs || 0;
|
||||
|
|
@ -232,6 +251,7 @@
|
|||
physicsBudget,
|
||||
poisonContactBudget,
|
||||
poisonContactRemoved,
|
||||
trapHeld,
|
||||
physics: physicsWorld || mechanicalWorld,
|
||||
constraints: constraintWorld,
|
||||
postConstraintPairs,
|
||||
|
|
@ -245,7 +265,7 @@
|
|||
}
|
||||
|
||||
function trackedDynamicItems(worldRef) {
|
||||
return worldRef?._itemUpdateRealtime || [];
|
||||
return worldRef?._itemUpdateTouched || [];
|
||||
}
|
||||
|
||||
global.TarinaiItemUpdateScheduler = Object.freeze({ run, trackedDynamicItems, wakeItem });
|
||||
|
|
|
|||
12
js/main.js
|
|
@ -100,6 +100,7 @@ function loop() {
|
|||
window.TarinaiPerf.beginFrame();
|
||||
const endUpdate = window.TarinaiPerf.begin("update.total");
|
||||
world.update(dt);
|
||||
if (typeof tickColonyHistorySampling === "function") tickColonyHistorySampling();
|
||||
if (endUpdate) endUpdate();
|
||||
const endRender = window.TarinaiPerf.begin("render.total");
|
||||
render();
|
||||
|
|
@ -146,3 +147,14 @@ if ("serviceWorker" in navigator) {
|
|||
navigator.serviceWorker.register(swUrl, { updateViaCache: "none" }).catch(() => undefined);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Expose the active build on the root element so stale-cache reports can be verified immediately.
|
||||
(function markActiveBuild() {
|
||||
const apply = () => {
|
||||
if (!document?.documentElement) return;
|
||||
document.documentElement.dataset.tarinaiBuild = String(window.TARINAI_APP?.build || window.TARINAI_VERSION || "unknown");
|
||||
};
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", apply, { once: true });
|
||||
else apply();
|
||||
})();
|
||||
|
|
|
|||
187
js/mamekusaredake_system.js
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
"use strict";
|
||||
(function(global){
|
||||
function mushroomImage(){ return global.getRenderableImage?.("mamekusaredake", "") || global.images?.get?.("mamekusaredake") || null; }
|
||||
function growthOf(t){ return Math.max(0,Math.min(1,Number(t?.mamekusaredakeGrowth||0)||0)); }
|
||||
const hostAlphaCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
|
||||
function unit(seed="",salt=""){
|
||||
if(typeof global.stableUnit==="function") return global.stableUnit(String(seed||"mame"),String(salt||""));
|
||||
const s=`${seed}|${salt}`; let h=2166136261;
|
||||
for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619);}
|
||||
return ((h>>>0)%1000003)/1000003;
|
||||
}
|
||||
function alphaPointsForImage(img){
|
||||
if(!img)return null;
|
||||
if(hostAlphaCache?.has(img)) return hostAlphaCache.get(img);
|
||||
let out=null;
|
||||
try{
|
||||
const c=document.createElement("canvas"); c.width=64; c.height=64;
|
||||
const cx=c.getContext("2d",{willReadFrequently:true}); cx.clearRect(0,0,64,64); cx.drawImage(img,0,0,64,64);
|
||||
const data=cx.getImageData(0,0,64,64).data, pts=[];
|
||||
for(let y=3;y<61;y++) for(let x=3;x<61;x++) if(data[(y*64+x)*4+3]>=96) pts.push([x,y]);
|
||||
out=pts.length?pts:null;
|
||||
}catch(_){out=null;}
|
||||
hostAlphaCache?.set(img,out); return out;
|
||||
}
|
||||
function hostPoint(t,img,dw,dh,index=0){
|
||||
const seed=t?.birthSeed||t?.familyKey||t?.id||"mame-host";
|
||||
const pts=alphaPointsForImage(img);
|
||||
if(pts?.length){
|
||||
const k=Math.min(pts.length-1,Math.floor(unit(seed,`mame-pos-${index}`)*pts.length));
|
||||
const p=pts[k]||pts[0];
|
||||
return {x:(p[0]/63-.5)*dw,y:(p[1]/63-.5)*dh};
|
||||
}
|
||||
// Fallback stays inside the usual non-transparent body ellipse.
|
||||
const a=unit(seed,`mame-a-${index}`)*Math.PI*2, rr=Math.sqrt(unit(seed,`mame-r-${index}`))*.70;
|
||||
return {x:Math.cos(a)*dw*.34*rr,y:Math.sin(a)*dh*.28*rr+dh*.03};
|
||||
}
|
||||
function hostMushroomCount(g){ return g<=.001?0:Math.max(1,Math.min(5,Math.ceil(g*5))); }
|
||||
function infect(t, source=null){
|
||||
if(!t||t.dead||t.mamekusaredakeDisease) return false;
|
||||
t.mamekusaredakeDisease=true; t.mamekusaredakeGrowth=0; t.mamekusaredakeDeathPlanted=false;
|
||||
t.mamekusaredakeSpeechAt=(t.world?.time||0)+3+Math.random()*5;
|
||||
t.world?.log?.(`${t.name}がマメクサレダケ病に罹患した。`,"accident",{participants:[t]});
|
||||
return true;
|
||||
}
|
||||
function visualSizeForHost(t,g=growthOf(t)){
|
||||
const rendered=Number(t?._mameRenderedSize||0)||0;
|
||||
if(rendered>0) return Math.max(3,rendered);
|
||||
const radius=Math.max(16,Number(t?.radius||20)||20);
|
||||
return Math.max(3,radius*1.16*Math.max(.05,g));
|
||||
}
|
||||
function spawnDetached(world,t,growth=growthOf(t)){
|
||||
if(!world||!t||typeof Item==="undefined") return null;
|
||||
const side=t.facingDir?.()||1;
|
||||
const it=new Item("mamekusaredake", t.x+side*(t.radius||20)*0.46, t.y+(t.radius||20)*0.30);
|
||||
it.mameDetached=true; it.mamePlanted=false; it.mameLife=1.25; it.vx=side*(35+Math.random()*25); it.vy=-35-Math.random()*20;
|
||||
it.mameDetachedSize=visualSizeForHost(t,growth); it.r=Math.max(2,it.mameDetachedSize/2.15);
|
||||
world.addItem?.(it,"mame-detached",{terrain:false}); return it;
|
||||
}
|
||||
function recover(t, reason="キノコが取れた"){
|
||||
if(!t?.mamekusaredakeDisease) return false;
|
||||
const growth=growthOf(t);
|
||||
spawnDetached(t.world,t,growth);
|
||||
t.mamekusaredakeDisease=false; t.mamekusaredakeGrowth=0; t.mamekusaredakeSpeechAt=0; t.mamekusaredakeDeathPlanted=false;
|
||||
t.world?.spawnEffect?.("ring",t.x,t.y,{size:(t.radius||20)*(.55+.75*growth),life:.55,color:"rgba(190,153,80,.66)"});
|
||||
t.world?.log?.(`${t.name}のマメクサレダケが外れて治った。`,"event",{participants:[t]}); return true;
|
||||
}
|
||||
function plantAt(world,x,y,r=34){
|
||||
if(!world||typeof Item==="undefined")return null;
|
||||
const it=new Item("mamekusaredake",x,y); it.r=r; it.mamePlanted=true; it.mameDetached=false; it.nextSporeAt=(world.time||0)+1.2+Math.random();
|
||||
world.addItem?.(it,"mame-plant",{terrain:false}); return it;
|
||||
}
|
||||
function plantFromHost(t){
|
||||
if(!t?.world||t.mamekusaredakeDeathPlanted) return null;
|
||||
t.mamekusaredakeDeathPlanted=true;
|
||||
const g=growthOf(t);
|
||||
// 死亡サイズの50%以下なら地面には残らず消滅する。
|
||||
if(g<=.5) return null;
|
||||
// 表示上の本数とは無関係に、死亡時に植わるのは必ず1本だけ。
|
||||
return plantAt(t.world,t.x,t.y,Math.max(22,(t.radius||20)*(.62+.32*g)));
|
||||
}
|
||||
function spawnSpore(world,m){
|
||||
if(!world||typeof Item==="undefined")return null;
|
||||
const a=Math.random()*Math.PI*2,d=Math.random()*(m.r||34)*.35;
|
||||
const s=new Item("mame_spore",m.x+Math.cos(a)*d,m.y+Math.sin(a)*d*.55); s.r=22+Math.random()*18; s.sporeAge=0; s.sporeLife=7+Math.random()*5; s.sporeConcentration=.45+Math.random()*.25; s.vx=Math.cos(a)*(6+Math.random()*10); s.vy=-4+Math.sin(a)*5;
|
||||
world.addItem?.(s,"mame-spore",{terrain:false}); return s;
|
||||
}
|
||||
function updateTarinai(t,dt){
|
||||
if(!t||t.dead)return false;
|
||||
const now=t.world?.time||0;
|
||||
const sick=Boolean(t.zunchiDisease||t.sleepDisease||t.explosionDisease||t.fightDisease||t.mamekusaredakeDisease);
|
||||
if(t.mamekusaredakeDisease){
|
||||
if(t.state==="sunbath"||(t.sunbathTimer||0)>.04){
|
||||
t.sunbathTimer=0; t.sunbathLeisure=false; t.sunbathCooldown=Math.max(t.sunbathCooldown||0,20);
|
||||
t.visibleSpriteId=""; t.setActionState?.("idle",{target:null,reason:"マメクサレダケ病のため日光浴できない",sleeping:false,clearTarget:true});
|
||||
}
|
||||
t.mamekusaredakeGrowth=Math.min(1.08,(Number(t.mamekusaredakeGrowth||0)||0)+dt/90);
|
||||
const g=growthOf(t);
|
||||
if(g>=.5){
|
||||
const maxEnergy=Math.max(1,typeof global.tarinaiMaxEnergy==="function"?global.tarinaiMaxEnergy(t):(Number(t.maxEnergy)||100));
|
||||
const severity=Math.max(0,Math.min(1,(g-.5)/.5));
|
||||
const loss=dt*maxEnergy*(.0018+.0037*severity);
|
||||
t.energy=Math.max(0,(Number(t.energy)||0)-loss);
|
||||
if(loss>.01) t.showHpBar?.();
|
||||
if(t.energy<=.5){ t.die?.("マメクサレダケ病",{fromDamage:true}); return true; }
|
||||
}
|
||||
}
|
||||
if(t.isAcquiredTarinai && sick && now>=Number(t.mamekusaredakeSpeechAt||0) && (t.nextBubbleAt||0)<=now){
|
||||
const choices=[{text:"くるち、くるち",id:"voice_ochibi_disease_001"},{text:"おみじゅ...",id:"voice_ochibi_hot_007"},{text:"ぶるぶる",id:"voice_temp_cold"}];
|
||||
const c=choices[Math.floor(Math.random()*choices.length)]; if(c&&t.bubble?.(c.text,4.5,"rgba(96,72,48,.82)")){ const a=global.TarinaiAudio; a?.playSample?.(c.id,1.1,"voice")||a?.play?.(c.id,{category:"voice",minGap:1.1}); }
|
||||
t.mamekusaredakeSpeechAt=now+7+Math.random()*10;
|
||||
}
|
||||
if(t.mamekusaredakeDisease && t.mamekusaredakeGrowth>=1){ t.die?.("マメクサレダケ病"); }
|
||||
return true;
|
||||
}
|
||||
function updateItem(item,dt,world){
|
||||
if(item.type==="mamekusaredake"){
|
||||
if(item.mameDetached){ item.mameLife=(item.mameLife||1)-dt; item.x+=(item.vx||0)*dt; item.y+=(item.vy||0)*dt; item.vy=(item.vy||0)+55*dt; item.vx*=Math.pow(.6,dt); if(item.mameLife<=0)item.amount=0; return true; }
|
||||
item.mamePlanted=true; const now=world?.time||0; if(now>=Number(item.nextSporeAt||0)){ item.nextSporeAt=now+1.4+Math.random()*1.8; if(Math.random()<.75)spawnSpore(world,item); } return true;
|
||||
}
|
||||
if(item.type==="mame_spore"){
|
||||
item.sporeAge=(item.sporeAge||0)+dt; const life=Math.max(1,item.sporeLife||9); item.sporeConcentration=Math.max(0,1-item.sporeAge/life); item.x+=(item.vx||0)*dt; item.y+=(item.vy||0)*dt; item.vx*=Math.pow(.72,dt); item.vy*=Math.pow(.72,dt);
|
||||
for(const t of world?.tarinai||[]){ if(!t||t.dead||t.mamekusaredakeDisease)continue; if(distXY(item.x,item.y,t.x,t.y)<=item.r+(t.radius||18)*.45 && Math.random()<dt*.036*item.sporeConcentration) infect(t,item); }
|
||||
if(item.sporeConcentration<=.01)item.amount=0; return true;
|
||||
} return false;
|
||||
}
|
||||
function drawMushroom(ctx,x,y,size,alpha=1,rot=0){ const img=mushroomImage(); if(!img)return false; ctx.save();ctx.globalAlpha*=alpha;ctx.translate(x,y);ctx.rotate(rot);ctx.drawImage(img,-size*.44,-size*.55,size*.88,size);ctx.restore();return true; }
|
||||
function drawOnTarinai(ctx,t,dw,dh,faceRight=false,hostImage=null,renderInfo=null){
|
||||
if(!t?.mamekusaredakeDisease)return false;
|
||||
const g=growthOf(t); if(g<=.001)return false;
|
||||
const count=hostMushroomCount(g), seed=t.birthSeed||t.familyKey||t.id||"mame-host";
|
||||
const baseSize=Math.max(4,dh*.34*g);
|
||||
t._mameRenderedSize=baseSize;
|
||||
t._mameHostLocalTargets=[];
|
||||
t._mameHostWorldTargets=[];
|
||||
const originX=Number(renderInfo?.originX);
|
||||
const originY=Number(renderInfo?.originY);
|
||||
const renderAngle=Number(renderInfo?.angle||0)||0;
|
||||
const hasWorldTransform=Number.isFinite(originX)&&Number.isFinite(originY);
|
||||
const cosA=Math.cos(renderAngle), sinA=Math.sin(renderAngle);
|
||||
let drew=false;
|
||||
for(let i=0;i<count;i++){
|
||||
const p=hostPoint(t,hostImage,dw,dh,i);
|
||||
const size=baseSize*(.72+unit(seed,`mame-size-${i}`)*.34);
|
||||
const rot=(-40+unit(seed,`mame-rot-${i}`)*80)*Math.PI/180;
|
||||
t._mameHostLocalTargets.push({x:p.x,y:p.y,size});
|
||||
if(hasWorldTransform){
|
||||
// draw() mirrors local X when faceRight, then rotates the whole sprite.
|
||||
// Use the identical transform for hit testing so the first click lands
|
||||
// on the mushroom that is actually visible on screen.
|
||||
const lx=faceRight?-p.x:p.x, ly=p.y;
|
||||
t._mameHostWorldTargets.push({
|
||||
x:originX+cosA*lx-sinA*ly,
|
||||
y:originY+sinA*lx+cosA*ly,
|
||||
size
|
||||
});
|
||||
}
|
||||
drew=drawMushroom(ctx,p.x,p.y,size,Math.min(1,.32+g*.82),rot)||drew;
|
||||
}
|
||||
return drew;
|
||||
}
|
||||
function drawItem(ctx,item,time=0){
|
||||
if(item.type==="mamekusaredake"){
|
||||
const alpha=item.mameDetached?Math.max(0,Math.min(1,(item.mameLife||0)/.45)):1;
|
||||
const size=item.mameDetached&&Number(item.mameDetachedSize)>0?Number(item.mameDetachedSize):(item.r||34)*2.15;
|
||||
if(!item.mameDetached){
|
||||
ctx.save(); ctx.globalAlpha*=.34; ctx.fillStyle="rgba(38,28,18,.72)"; ctx.beginPath(); ctx.ellipse(size*.055,size*.41,size*.30,size*.085,0,0,Math.PI*2); ctx.fill(); ctx.restore();
|
||||
}
|
||||
return drawMushroom(ctx,0,0,size,alpha,item.mameDetached?(time*2.2):0);
|
||||
}
|
||||
if(item.type==="mame_spore"){ const r=item.r||28,c=Math.max(0,Math.min(1,item.sporeConcentration??1));ctx.save();ctx.globalAlpha*=.10+.22*c;ctx.fillStyle="rgba(181,139,65,.58)";for(let i=0;i<3;i++){const a=(item.seed||0)+i*2.2,rr=r*(.30+i*.06);ctx.beginPath();ctx.arc(Math.cos(a)*r*.18,Math.sin(a)*r*.12,rr,0,Math.PI*2);ctx.fill();}ctx.restore();return true;}
|
||||
return false;
|
||||
}
|
||||
function mushroomHit(t,x,y){
|
||||
if(!t?.mamekusaredakeDisease)return false;
|
||||
const worldTargets=Array.isArray(t._mameHostWorldTargets)?t._mameHostWorldTargets:[];
|
||||
for(const p of worldTargets){
|
||||
if(distXY(x,y,p.x,p.y)<=Math.max(8,(p.size||10)*.52)) return true;
|
||||
}
|
||||
// Fallback for the short interval before the first render after infection.
|
||||
// It intentionally stays conservative so pinching the body itself does not
|
||||
// accidentally cure the disease.
|
||||
const g=Math.max(.05,growthOf(t));
|
||||
const face=typeof t.facingDir==="function"?(t.facingDir()||1):(t.facing||1);
|
||||
return distXY(x,y,t.x+face*(t.radius||20)*.30,t.y+(t.radius||20)*.18)<=Math.max(7,(t.radius||20)*.34*g);
|
||||
}
|
||||
global.TarinaiMamekusaredakeSystem=Object.freeze({infect,recover,plantAt,plantFromHost,spawnSpore,updateTarinai,updateItem,drawOnTarinai,drawItem,mushroomHit});
|
||||
})(typeof window!=="undefined"?window:globalThis);
|
||||