Compare commits
2 commits
e6507288ce
...
33234f32ae
| Author | SHA1 | Date | |
|---|---|---|---|
| 33234f32ae | |||
| 8eb66f2e66 |
21 changed files with 3954 additions and 944 deletions
24
.htaccess
Normal file
24
.htaccess
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType text/html "access plus 0 seconds"
|
||||
ExpiresByType text/css "access plus 0 seconds"
|
||||
ExpiresByType application/javascript "access plus 0 seconds"
|
||||
ExpiresByType application/json "access plus 0 seconds"
|
||||
ExpiresByType image/png "access plus 30 days"
|
||||
ExpiresByType image/webp "access plus 30 days"
|
||||
ExpiresByType audio/mpeg "access plus 30 days"
|
||||
ExpiresByType audio/wav "access plus 30 days"
|
||||
</IfModule>
|
||||
<IfModule mod_headers.c>
|
||||
<FilesMatch "\.(html?|css|js|json)$">
|
||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</FilesMatch>
|
||||
<FilesMatch "\.(png|webp|mp3|wav)$">
|
||||
Header set Cache-Control "public, max-age=2592000"
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json text/plain
|
||||
</IfModule>
|
||||
324
README.md
324
README.md
|
|
@ -1,210 +1,210 @@
|
|||
# たりないコロニー観察所
|
||||
# たりないコロニー観察所
|
||||
|
||||
ブラウザで動く、観察主体の小さなコロニーゲームです。
|
||||
外部ライブラリやサーバーは不要です。
|
||||
ブラウザで動く、観察主体の小さなコロニーゲームです。
|
||||
外部ライブラリやサーバーは不要です。
|
||||
|
||||
## 起動方法
|
||||
## 起動方法
|
||||
|
||||
1. ZIPを展開
|
||||
2. `index.html` をブラウザで開く
|
||||
1. ZIPを展開
|
||||
2. `index.html` をブラウザで開く
|
||||
|
||||
Chrome / Edge / Firefox / Safari の近年版を想定しています。
|
||||
Chrome / Edge / Firefox / Safari の近年版を想定しています。
|
||||
|
||||
## ゲーム内容
|
||||
## ゲーム内容
|
||||
|
||||
「たりない」たちが観察皿の中で勝手に暮らします。
|
||||
基本は眺めるだけですが、餌・水・草・石・寝床・などを配置して軽く介入できます。
|
||||
「たりない」たちが観察皿の中で勝手に暮らします。
|
||||
基本は眺めるだけですが、餌・水・草・石・寝床・などを配置して軽く介入できます。
|
||||
|
||||
## 主な要素
|
||||
## 主な要素
|
||||
|
||||
- 9種類のたりない画像を使用
|
||||
- 空腹・孤独・眠気・不安・たりない度
|
||||
- 餌探し、睡眠、群れ寄り、パニック、繁殖、死亡
|
||||
- 死後の「たりない跡」と草の発生
|
||||
- 個体選択による詳細観察
|
||||
- 観察ログ
|
||||
- 一時停止 / 速度変更 / 保存 / 読込
|
||||
- localStorageによるブラウザ内セーブ
|
||||
- 9種類のたりない画像を使用
|
||||
- 空腹・孤独・眠気・不安・たりない度
|
||||
- 餌探し、睡眠、群れ寄り、パニック、繁殖、死亡
|
||||
- 死後の「たりない跡」と草の発生
|
||||
- 個体選択による詳細観察
|
||||
- 観察ログ
|
||||
- 一時停止 / 速度変更 / 保存 / 読込
|
||||
- localStorageによるブラウザ内セーブ
|
||||
|
||||
## 操作
|
||||
## 操作
|
||||
|
||||
- 観察:個体クリックで詳細表示
|
||||
- ふつうの餌 / あまい餌 / 水滴 / 草 / ぬくい石 / ふわふわ:キャンバスクリックで配置
|
||||
- つつく:個体をクリックして一時的に不安を上げる
|
||||
- 迷い込み:指定位置に新しいたりないを追加
|
||||
- 観察:個体クリックで詳細表示
|
||||
- ふつうの餌 / あまい餌 / 水滴 / 草 / ぬくい石 / ふわふわ:キャンバスクリックで配置
|
||||
- つつく:個体をクリックして一時的に不安を上げる
|
||||
- 迷い込み:指定位置に新しいたりないを追加
|
||||
|
||||
## 素材について
|
||||
## 素材について
|
||||
|
||||
同梱画像は本プロトタイプ用に加工した「たりない」素材です。
|
||||
公開・配布・商用利用を行う場合は、元画像作成者・権利者の許諾範囲に従ってください。
|
||||
同梱画像は本プロトタイプ用に加工した「たりない」素材です。
|
||||
公開・配布・商用利用を行う場合は、元画像作成者・権利者の許諾範囲に従ってください。
|
||||
|
||||
## ファイル構成
|
||||
## ファイル構成
|
||||
|
||||
```text
|
||||
tarinai_colony_game/
|
||||
├─ index.html
|
||||
├─ css/
|
||||
│ └─ styles.css
|
||||
├─ js/
|
||||
│ └─ game.js
|
||||
├─ assets/
|
||||
│ └─ sprites/
|
||||
│ ├─ tarinai_01_smile.png
|
||||
│ ├─ ...
|
||||
│ └─ _contact_sheet.png
|
||||
└─ docs/
|
||||
└─ design_notes.md
|
||||
├─ index.html
|
||||
├─ css/
|
||||
│ └─ styles.css
|
||||
├─ js/
|
||||
│ └─ game.js
|
||||
├─ assets/
|
||||
│ └─ sprites/
|
||||
│ ├─ tarinai_01_smile.png
|
||||
│ ├─ ...
|
||||
│ └─ _contact_sheet.png
|
||||
└─ docs/
|
||||
└─ design_notes.md
|
||||
```
|
||||
|
||||
## 今後の拡張候補
|
||||
## 今後の拡張候補
|
||||
|
||||
- 個体ごとの家系図
|
||||
- 図鑑・実績
|
||||
- より精密なコロニー分裂
|
||||
- 草・死跡・餌の生態循環
|
||||
- 音と環境変化
|
||||
- 画像素材の追加差分
|
||||
- 個体ごとの家系図
|
||||
- 図鑑・実績
|
||||
- より精密なコロニー分裂
|
||||
- 草・死跡・餌の生態循環
|
||||
- 音と環境変化
|
||||
- 画像素材の追加差分
|
||||
|
||||
|
||||
## 更新内容(最新版)
|
||||
## 更新内容(最新版)
|
||||
|
||||
- たりないの表示サイズを大幅に縮小
|
||||
- 右移動時に左右反転
|
||||
- 食事中の専用エフェクト
|
||||
- つつきすぎによる死亡演出(06→09→死亡)
|
||||
- 死亡時の緑のしぶき跡
|
||||
- 感情に応じたスプライト自動切替
|
||||
- 昼夜サイクルと時間HUD
|
||||
- たりないの表示サイズを大幅に縮小
|
||||
- 右移動時に左右反転
|
||||
- 食事中の専用エフェクト
|
||||
- つつきすぎによる死亡演出(06→09→死亡)
|
||||
- 死亡時の緑のしぶき跡
|
||||
- 感情に応じたスプライト自動切替
|
||||
- 昼夜サイクルと時間HUD
|
||||
|
||||
|
||||
## 更新内容(v3)
|
||||
## 更新内容(v3)
|
||||
|
||||
- 出生・死亡バランスを再調整:飢餓速度を下げ、寿命と上限個体数を増やし、観察が長く続くように調整
|
||||
- 図鑑を追加:観測済みスプライト、イベント、死因を記録
|
||||
- 系譜を追加:繁殖時に親子関係・世代・出生時刻を保存
|
||||
- 選択個体に親・子数・気分・つつき耐久を表示
|
||||
- Web Audioによる簡易効果音を追加(音 ON ボタンで有効化)
|
||||
- 昼夜サイクルをログ・図鑑イベントにも反映
|
||||
- 食事、繁殖、死亡、つつき、時間帯切替のイベント記録を追加
|
||||
- 出生・死亡バランスを再調整:飢餓速度を下げ、寿命と上限個体数を増やし、観察が長く続くように調整
|
||||
- 図鑑を追加:観測済みスプライト、イベント、死因を記録
|
||||
- 系譜を追加:繁殖時に親子関係・世代・出生時刻を保存
|
||||
- 選択個体に親・子数・気分・つつき耐久を表示
|
||||
- Web Audioによる簡易効果音を追加(音 ON ボタンで有効化)
|
||||
- 昼夜サイクルをログ・図鑑イベントにも反映
|
||||
- 食事、繁殖、死亡、つつき、時間帯切替のイベント記録を追加
|
||||
|
||||
|
||||
## 更新内容(v4)
|
||||
## 更新内容(v4)
|
||||
|
||||
- 選択個体パネルから「足りないもの」「考えていること」を削除
|
||||
- 系譜をリスト形式から樹状図形式へ変更
|
||||
- 子個体が成長するまで親に追従する挙動を追加
|
||||
- 食事エフェクト量を抑制し、餌・あまい餌・草の色を反映
|
||||
- 昼夜サイクルの光を地面・アイテム・たりない本体へ反映
|
||||
- 近距離・低機嫌・過密時に発生する喧嘩を追加
|
||||
- 喧嘩イベントを図鑑イベントへ記録
|
||||
- 保存キーは `tarinai-colony-save-v4`
|
||||
- 選択個体パネルから「足りないもの」「考えていること」を削除
|
||||
- 系譜をリスト形式から樹状図形式へ変更
|
||||
- 子個体が成長するまで親に追従する挙動を追加
|
||||
- 食事エフェクト量を抑制し、餌・あまい餌・草の色を反映
|
||||
- 昼夜サイクルの光を地面・アイテム・たりない本体へ反映
|
||||
- 近距離・低機嫌・過密時に発生する喧嘩を追加
|
||||
- 喧嘩イベントを図鑑イベントへ記録
|
||||
- 保存キーは `tarinai-colony-save-v4`
|
||||
|
||||
|
||||
## 更新内容(v5)
|
||||
## 更新内容(v5)
|
||||
|
||||
- 草は自然発生するように変更
|
||||
- 草は自然には消えず、少しずつ伸びる
|
||||
- 草を食べると残量が減り、表示される本数も減る
|
||||
- 暗い時間帯にスプライトの透過部分へ矩形状の色が乗る問題を修正
|
||||
- HUDの観察日を「1日目」表記へ変更
|
||||
- HUDの「明るさ」表示を時刻表記へ変更
|
||||
- 図鑑UIを完全に削除
|
||||
- 系譜を右パネルからメイン画面下部へ移動
|
||||
- 保存キーは `tarinai-colony-save-v5`
|
||||
- 草は自然発生するように変更
|
||||
- 草は自然には消えず、少しずつ伸びる
|
||||
- 草を食べると残量が減り、表示される本数も減る
|
||||
- 暗い時間帯にスプライトの透過部分へ矩形状の色が乗る問題を修正
|
||||
- HUDの観察日を「1日目」表記へ変更
|
||||
- HUDの「明るさ」表示を時刻表記へ変更
|
||||
- 図鑑UIを完全に削除
|
||||
- 系譜を右パネルからメイン画面下部へ移動
|
||||
- 保存キーは `tarinai-colony-save-v5`
|
||||
|
||||
|
||||
## 更新内容(v6)
|
||||
## 更新内容(v6)
|
||||
|
||||
- 喧嘩中に頭突き攻撃を行うように変更
|
||||
- 草と石が昼夜サイクルの光に反応するように変更
|
||||
- 睡眠中は完全に移動停止
|
||||
- 09_stretch が見えるよう、つつき耐久を 9 段階に調整
|
||||
- 3 回目以降: 06_cry
|
||||
- 6 回目以降: 09_stretch
|
||||
- 9 回目: 死亡
|
||||
- たりないがカーソルに反応
|
||||
- 好意的な個体は 01_smile で寄ってくる
|
||||
- 敵対的・不安定な個体は逃げる
|
||||
- 保存キーは `tarinai-colony-save-v6`
|
||||
- 喧嘩中に頭突き攻撃を行うように変更
|
||||
- 草と石が昼夜サイクルの光に反応するように変更
|
||||
- 睡眠中は完全に移動停止
|
||||
- 09_stretch が見えるよう、つつき耐久を 9 段階に調整
|
||||
- 3 回目以降: 06_cry
|
||||
- 6 回目以降: 09_stretch
|
||||
- 9 回目: 死亡
|
||||
- たりないがカーソルに反応
|
||||
- 好意的な個体は 01_smile で寄ってくる
|
||||
- 敵対的・不安定な個体は逃げる
|
||||
- 保存キーは `tarinai-colony-save-v6`
|
||||
|
||||
|
||||
## 更新内容(v7)
|
||||
## 更新内容(v7)
|
||||
|
||||
- メイン画面の外観を「庭の観察」テイストへ刷新
|
||||
- 空・日差し・庭枠・土の質感・周辺の草花を追加
|
||||
- たりないに接地影を追加
|
||||
- 石・草・各アイテムにも影と周囲とのなじみを追加
|
||||
- 選択リングを観察レンズ風の二重リングに変更
|
||||
- 子個体と親個体の関係が見えやすいよう、近距離時に薄い追従ガイド線を追加
|
||||
- 右上HUDと全体UIを庭の観察ノート風に調整
|
||||
- 保存キーは `tarinai-colony-save-v7`
|
||||
- メイン画面の外観を「庭の観察」テイストへ刷新
|
||||
- 空・日差し・庭枠・土の質感・周辺の草花を追加
|
||||
- たりないに接地影を追加
|
||||
- 石・草・各アイテムにも影と周囲とのなじみを追加
|
||||
- 選択リングを観察レンズ風の二重リングに変更
|
||||
- 子個体と親個体の関係が見えやすいよう、近距離時に薄い追従ガイド線を追加
|
||||
- 右上HUDと全体UIを庭の観察ノート風に調整
|
||||
- 保存キーは `tarinai-colony-save-v7`
|
||||
|
||||
|
||||
## 更新内容(v8)
|
||||
## 更新内容(v8)
|
||||
|
||||
- アニメーションを調整し、呼吸感・移動感・喧嘩時の勢いを強化
|
||||
- 庭素材を追加(小石・苔・クローバー・落ち葉・周辺の草花)
|
||||
- 太陽や月そのものは描かず、時間帯に応じた投影影を追加
|
||||
- ずんちを追加。食事を重ねると排泄し、庭に残る
|
||||
- 09_stretch が通常個体でもはっきり表示されるよう補強
|
||||
- 系譜の接続線を太く・連続的にして見やすく修正
|
||||
- 未使用だった図鑑系の状態管理や関連スタイルを削除
|
||||
- 保存キーは `tarinai-colony-save-v8`
|
||||
- アニメーションを調整し、呼吸感・移動感・喧嘩時の勢いを強化
|
||||
- 庭素材を追加(小石・苔・クローバー・落ち葉・周辺の草花)
|
||||
- 太陽や月そのものは描かず、時間帯に応じた投影影を追加
|
||||
- ずんちを追加。食事を重ねると排泄し、庭に残る
|
||||
- 09_stretch が通常個体でもはっきり表示されるよう補強
|
||||
- 系譜の接続線を太く・連続的にして見やすく修正
|
||||
- 未使用だった図鑑系の状態管理や関連スタイルを削除
|
||||
- 保存キーは `tarinai-colony-save-v8`
|
||||
|
||||
|
||||
## 更新内容(v9)
|
||||
## 更新内容(v9)
|
||||
|
||||
- 投影影が見えにくい問題を修正
|
||||
- blur フィルタ依存をやめ、軽量な楕円影へ変更
|
||||
- たりない・石・草・アイテムの影を濃く調整
|
||||
- 重すぎる問題を軽量化
|
||||
- 庭背景の粒・草花・落ち葉描画数を削減
|
||||
- filter blur を廃止
|
||||
- 草の自動発生上限と発生頻度を抑制
|
||||
- アイテム上限を削減
|
||||
- 食事パーティクル頻度を抑制
|
||||
- 死亡後の緑のしぶきが時間経過でゆっくり薄く消えるように変更
|
||||
- 保存キーは `tarinai-colony-save-v9`
|
||||
- 投影影が見えにくい問題を修正
|
||||
- blur フィルタ依存をやめ、軽量な楕円影へ変更
|
||||
- たりない・石・草・アイテムの影を濃く調整
|
||||
- 重すぎる問題を軽量化
|
||||
- 庭背景の粒・草花・落ち葉描画数を削減
|
||||
- filter blur を廃止
|
||||
- 草の自動発生上限と発生頻度を抑制
|
||||
- アイテム上限を削減
|
||||
- 食事パーティクル頻度を抑制
|
||||
- 死亡後の緑のしぶきが時間経過でゆっくり薄く消えるように変更
|
||||
- 保存キーは `tarinai-colony-save-v9`
|
||||
|
||||
## 更新内容(v10)
|
||||
## 更新内容(v10)
|
||||
|
||||
- 新規スプライト5枚を追加
|
||||
- `10_hurt`: 痛い時・喧嘩中に打たれた時
|
||||
- `11_weak`: 低体力時
|
||||
- `12_back`: 奥方向へ進む時
|
||||
- `13_flee`: 喧嘩に負けて敗走する時
|
||||
- `14_zunda_eat`: ずんだ餅を食べる時
|
||||
- 追加画像はすべて透明PNG化し、`512x468` のキャンバスへ正規化
|
||||
- 追加スプライトは行動専用扱いにし、通常個体の基本タイプとしてはランダム出現しないように調整
|
||||
- 新規スプライト5枚を追加
|
||||
- `10_hurt`: 痛い時・喧嘩中に打たれた時
|
||||
- `11_weak`: 低体力時
|
||||
- `12_back`: 奥方向へ進む時
|
||||
- `13_flee`: 喧嘩に負けて敗走する時
|
||||
- `14_zunda_eat`: ずんだ餅を食べる時
|
||||
- 追加画像はすべて透明PNG化し、`512x468` のキャンバスへ正規化
|
||||
- 追加スプライトは行動専用扱いにし、通常個体の基本タイプとしてはランダム出現しないように調整
|
||||
|
||||
|
||||
## 更新内容(v11)
|
||||
## 更新内容(v11)
|
||||
|
||||
- 個体ごとの性格を追加
|
||||
- 臆病、食いしん坊、眠たがり、寂しがり、怒りっぽい、マイペース
|
||||
- 性格は睡眠、喧嘩、ずんだ餅反応、恐怖、対個体行動へ影響
|
||||
- 選択パネルに観察理由を追加
|
||||
- 現在の行動理由
|
||||
- 対象
|
||||
- 仲の良い個体
|
||||
- 避けている個体
|
||||
- 喧嘩記憶
|
||||
- 関係性と喧嘩記憶を追加
|
||||
- 近くで過ごす個体同士は親和度が上がる
|
||||
- 喧嘩に負けた相手を記憶し、しばらく避けやすくなる
|
||||
- 勝敗はログと選択パネルに反映
|
||||
- hay bed に快適度・混雑ペナルティを追加
|
||||
- 空いているベッドを選びやすくなる
|
||||
- 混雑時は回復効率が下がり、ストレスが上がる
|
||||
- 草の生態系を強化
|
||||
- 雨、水、ずんち由来の肥沃地で育ちやすくなる
|
||||
- 強い晴天で水分がないと弱りやすくなる
|
||||
- 草の周辺ではストレス・恐怖が少し下がる
|
||||
- 表情スプライトの優先順位を整理
|
||||
- ずんだ餅、被ダメージ、敗走、恐怖、低体力、睡眠、喧嘩、奥移動、通常の順に競合解決
|
||||
- ログを種類別に色分け
|
||||
- 誕生、死亡、喧嘩、食事、草、環境、関係、観察
|
||||
- 喧嘩で転ぶ演出を追加
|
||||
- 打撃時に低確率で転ぶ
|
||||
- 敗北時に転倒し、土煙が出る
|
||||
- 保存キーは `tarinai-colony-save-v11`
|
||||
- 個体ごとの性格を追加
|
||||
- 臆病、食いしん坊、眠たがり、寂しがり、怒りっぽい、マイペース
|
||||
- 性格は睡眠、喧嘩、ずんだ餅反応、恐怖、対個体行動へ影響
|
||||
- 選択パネルに観察理由を追加
|
||||
- 現在の行動理由
|
||||
- 対象
|
||||
- 仲の良い個体
|
||||
- 避けている個体
|
||||
- 喧嘩記憶
|
||||
- 関係性と喧嘩記憶を追加
|
||||
- 近くで過ごす個体同士は親和度が上がる
|
||||
- 喧嘩に負けた相手を記憶し、しばらく避けやすくなる
|
||||
- 勝敗はログと選択パネルに反映
|
||||
- hay bed に快適度・混雑ペナルティを追加
|
||||
- 空いているベッドを選びやすくなる
|
||||
- 混雑時は回復効率が下がり、ストレスが上がる
|
||||
- 草の生態系を強化
|
||||
- 雨、水、ずんち由来の肥沃地で育ちやすくなる
|
||||
- 強い晴天で水分がないと弱りやすくなる
|
||||
- 草の周辺ではストレス・恐怖が少し下がる
|
||||
- 表情スプライトの優先順位を整理
|
||||
- ずんだ餅、被ダメージ、敗走、恐怖、低体力、睡眠、喧嘩、奥移動、通常の順に競合解決
|
||||
- ログを種類別に色分け
|
||||
- 誕生、死亡、喧嘩、食事、草、環境、関係、観察
|
||||
- 喧嘩で転ぶ演出を追加
|
||||
- 打撃時に低確率で転ぶ
|
||||
- 敗北時に転倒し、土煙が出る
|
||||
- 保存キーは `tarinai-colony-save-v11`
|
||||
|
|
|
|||
BIN
assets/sprites/tarinai_28_zunchi_slave.png
Normal file
BIN
assets/sprites/tarinai_28_zunchi_slave.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 770 B After Width: | Height: | Size: 2.3 KiB |
619
css/styles.css
619
css/styles.css
|
|
@ -50,16 +50,40 @@ button {
|
|||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.room-link {
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background: rgba(255,255,255,0.86);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 3px;
|
||||
margin: 0;
|
||||
font-size: clamp(24px, 3.4vw, 42px);
|
||||
letter-spacing: 0.03em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
font-size: clamp(11px, 1.1vw, 13px);
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
|
|
@ -115,6 +139,7 @@ h1 {
|
|||
min-height: var(--field-min-height, 620px);
|
||||
display: block;
|
||||
cursor: crosshair;
|
||||
background: linear-gradient(180deg, #cfe3d3, #d9d0ad);
|
||||
}
|
||||
|
||||
.field-dialog {
|
||||
|
|
@ -161,6 +186,71 @@ h1 {
|
|||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.save-text-dialog-panel {
|
||||
width: min(760px, calc(100vw - 30px));
|
||||
}
|
||||
.save-text-area {
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.82);
|
||||
color: var(--ink);
|
||||
font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
resize: vertical;
|
||||
word-break: break-all;
|
||||
}
|
||||
.save-text-actions { flex-wrap: wrap; }
|
||||
.archive-reset-dialog-panel {
|
||||
width: min(420px, 100%);
|
||||
}
|
||||
.archive-reset-actions {
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.archive-reset-actions .btn {
|
||||
min-width: 132px;
|
||||
}
|
||||
|
||||
|
||||
.ecology-dialog-panel {
|
||||
width: min(760px, 100%);
|
||||
}
|
||||
.ecology-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.ecology-card {
|
||||
display: grid;
|
||||
grid-template-columns: 84px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.88), rgba(243,237,225,0.76));
|
||||
border: 1px solid rgba(84,68,48,0.12);
|
||||
}
|
||||
.ecology-card img {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 8px 18px rgba(56, 44, 32, 0.10));
|
||||
}
|
||||
.ecology-card strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.ecology-card p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.toast {
|
||||
|
|
@ -258,7 +348,118 @@ h1 {
|
|||
.tool[data-tool="fence_v"] { --tool-icon: url("../assets/ui/tool_fence_v.png"); }
|
||||
.tool[data-tool="fence_h"] { --tool-icon: url("../assets/ui/tool_fence_h.png"); }
|
||||
.tool[data-tool="bed"] { --tool-icon: url("../assets/ui/tool_bed.png"); }
|
||||
.tool[data-tool="poke"] { --tool-icon: url("../assets/ui/tool_poke.png"); }
|
||||
.tool[data-tool="ball"] { --tool-icon: none; }
|
||||
.tool[data-tool="ball"]::before {
|
||||
content: "\26bd ";
|
||||
background: none;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||
}
|
||||
.tool[data-tool="ball"]::after {
|
||||
content: "\26bd ";
|
||||
background: none;
|
||||
font-size: 48px;
|
||||
line-height: 1;
|
||||
opacity: 0.10;
|
||||
right: -6px;
|
||||
left: auto;
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
.tool[data-tool="poke"] { --tool-icon: none; }
|
||||
.tool[data-tool="poke"]::before {
|
||||
content: "\1f448 ";
|
||||
background: none;
|
||||
font-size: 25px;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||
}
|
||||
.tool[data-tool="poke"]::after {
|
||||
content: "\1f448 ";
|
||||
background: none;
|
||||
font-size: 48px;
|
||||
line-height: 1;
|
||||
opacity: 0.11;
|
||||
right: -6px;
|
||||
left: auto;
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
.tool[data-tool="pinch"] { --tool-icon: none; }
|
||||
.tool[data-tool="pinch"]::before {
|
||||
content: "\1f90f ";
|
||||
background: none;
|
||||
font-size: 25px;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||
}
|
||||
.tool[data-tool="pinch"]::after {
|
||||
content: "\1f90f ";
|
||||
background: none;
|
||||
font-size: 48px;
|
||||
line-height: 1;
|
||||
opacity: 0.10;
|
||||
right: -6px;
|
||||
left: auto;
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
.tool[data-tool="love_mochi"] { --tool-icon: none; }
|
||||
.tool[data-tool="love_mochi"]::before {
|
||||
content: "\1f495 ";
|
||||
background: none;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||
}
|
||||
.tool[data-tool="love_mochi"]::after {
|
||||
content: "\1f495 ";
|
||||
background: none;
|
||||
font-size: 46px;
|
||||
line-height: 1;
|
||||
opacity: 0.10;
|
||||
right: -6px;
|
||||
left: auto;
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
.tool[data-tool="fight_mochi"] { --tool-icon: none; }
|
||||
.tool[data-tool="fight_mochi"]::before {
|
||||
content: "\1f4a2 ";
|
||||
background: none;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||
}
|
||||
.tool[data-tool="fight_mochi"]::after {
|
||||
content: "\1f4a2 ";
|
||||
background: none;
|
||||
font-size: 46px;
|
||||
line-height: 1;
|
||||
opacity: 0.10;
|
||||
right: -6px;
|
||||
left: auto;
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
.tool[data-tool="delete"] { --tool-icon: url("../assets/ui/tool_delete.png"); }
|
||||
.tool[data-tool="new"] { --tool-icon: url("../assets/ui/tool_new.png"); }
|
||||
.tool.selected {
|
||||
|
|
@ -296,8 +497,8 @@ h1 {
|
|||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
}
|
||||
.tool-category-toggle::before { content: "▾"; display: inline-block; width: 16px; color: var(--muted); }
|
||||
.tool-category.collapsed .tool-category-toggle::before { content: "▸"; }
|
||||
.tool-category-toggle::before { content: "\25be "; display: inline-block; width: 16px; color: var(--muted); }
|
||||
.tool-category.collapsed .tool-category-toggle::before { content: "\25b8 "; }
|
||||
.tool-category-body { padding: 8px; }
|
||||
.tool-category.collapsed .tool-category-body { display: none; }
|
||||
.tool-size-badge {
|
||||
|
|
@ -388,6 +589,7 @@ meter {
|
|||
flex-direction: column;
|
||||
gap: 7px;
|
||||
font-size: 12px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.log-entry {
|
||||
padding: 8px 9px;
|
||||
|
|
@ -422,6 +624,8 @@ meter {
|
|||
@media (max-width: 980px) {
|
||||
#app { padding: 10px; }
|
||||
.topbar { align-items: flex-start; flex-direction: column; }
|
||||
.top-left { align-items: flex-start; flex-direction: column; }
|
||||
.brand { align-items: center; flex-direction: row; gap: 10px; }
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.panel { max-height: none; overflow: visible; }
|
||||
#gameCanvas { height: var(--field-mobile-height, 66vh); min-height: var(--field-mobile-min-height, 460px); }
|
||||
|
|
@ -458,15 +662,34 @@ meter {
|
|||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.lineage-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lineage-head h2 { margin-bottom: 0; }
|
||||
|
||||
.lineage-main-content {
|
||||
max-height: 560px;
|
||||
min-height: 320px;
|
||||
height: min(78vh, 980px);
|
||||
min-height: 520px;
|
||||
max-height: calc(100vh - 120px);
|
||||
overflow: auto;
|
||||
overscroll-behavior: auto;
|
||||
padding: 8px 4px 16px;
|
||||
overscroll-behavior: contain;
|
||||
padding: 10px 6px 24px;
|
||||
background: rgba(255,250,244,0.48);
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(84,68,48,0.10);
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
.lineage-forest {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.lineage-main-content .lineage-tree {
|
||||
|
|
@ -513,70 +736,19 @@ body::before {
|
|||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.55);
|
||||
}
|
||||
|
||||
/* Compact family board: parent ┳ parent / child, without unrelated founders. */
|
||||
.family-board { padding: 0; overflow: visible; }
|
||||
.family-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(246px, 1fr));
|
||||
gap: 10px 12px;
|
||||
align-items: start;
|
||||
padding: 2px 2px 10px;
|
||||
}
|
||||
.family-cluster {
|
||||
position: relative;
|
||||
min-height: 106px;
|
||||
padding: 7px 7px 8px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255,253,248,0.56);
|
||||
border: 1px solid rgba(84,68,48,0.09);
|
||||
contain: layout paint;
|
||||
}
|
||||
.family-parents {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.family-child-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 14px;
|
||||
}
|
||||
/* Family tree node primitives. The archive deliberately uses these nodes inside an SVG-backed tree, not the old card/grid UI. */
|
||||
.family-node {
|
||||
display: grid;
|
||||
grid-template-columns: 22px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
padding: 4px 6px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, rgba(255,253,248,0.96), rgba(248,241,229,0.90));
|
||||
border: 1px solid rgba(84,68,48,0.14);
|
||||
box-shadow: 0 3px 8px rgba(64,48,32,0.045);
|
||||
}
|
||||
.family-child-slot .family-node { width: min(150px, 68%); }
|
||||
.family-connector {
|
||||
position: relative;
|
||||
height: 12px;
|
||||
margin: 0 42px;
|
||||
}
|
||||
.family-connector::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 7px;
|
||||
height: 2px;
|
||||
background: rgba(107, 88, 61, 0.55);
|
||||
}
|
||||
.family-connector span {
|
||||
position: absolute;
|
||||
left: calc(50% - 1px);
|
||||
top: 7px;
|
||||
width: 2px;
|
||||
height: 16px;
|
||||
background: rgba(107, 88, 61, 0.55);
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
}
|
||||
.family-node.child {
|
||||
border-color: rgba(103, 178, 44, 0.38);
|
||||
|
|
@ -584,10 +756,26 @@ body::before {
|
|||
}
|
||||
.family-node[data-family-tarinai-id] { cursor: pointer; transition: transform .12s ease, border-color .12s ease, box-shadow .12s ease; }
|
||||
.family-node[data-family-tarinai-id]:hover { transform: translateY(-1px); border-color: rgba(112,178,66,0.42); box-shadow: 0 5px 12px rgba(64,48,32,0.08); }
|
||||
.family-node.dead { opacity: 0.68; filter: grayscale(0.35); }
|
||||
.family-node.dead { opacity: 0.9; filter: grayscale(0.32); }
|
||||
.family-node.missing { opacity: 0.48; border-style: dashed; }
|
||||
.family-node img { width: 22px; height: 22px; object-fit: contain; }
|
||||
.family-copy { min-width: 0; line-height: 1.12; }
|
||||
.family-portrait {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
bottom: -8px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.family-node.alive .family-portrait { opacity: 0.80; }
|
||||
.family-node.dead .family-portrait { opacity: 0.20; }
|
||||
.family-portrait img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
filter: saturate(0.95);
|
||||
}
|
||||
.family-copy { position: relative; z-index: 1; min-width: 0; line-height: 1.12; max-width: calc(100% - 8px); }
|
||||
.family-copy strong, .family-copy span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
|
|
@ -595,21 +783,15 @@ body::before {
|
|||
white-space: nowrap;
|
||||
}
|
||||
.family-copy .death-reason {
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.12;
|
||||
margin-top: 1px;
|
||||
color: rgba(126, 80, 64, 0.92);
|
||||
}
|
||||
.family-copy strong { font-size: 11px; }
|
||||
.family-copy span { color: var(--muted); font-size: 8.5px; margin-top: 1px; }
|
||||
@media (max-width: 760px) {
|
||||
.family-grid { grid-template-columns: 1fr; }
|
||||
.family-cluster { min-height: 102px; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.colony-card { overflow: visible; min-height: 0; flex: 0 0 auto; }
|
||||
|
|
@ -678,193 +860,168 @@ body::before {
|
|||
font-size: 13px;
|
||||
color: rgba(64,52,38,0.86);
|
||||
}
|
||||
.lineage-generation {
|
||||
display: grid;
|
||||
grid-template-columns: 62px 1fr;
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
padding: 8px 0;
|
||||
border-top: 1px dashed rgba(84,68,48,0.13);
|
||||
/* Family archive: actual top-down tree diagram. */
|
||||
.lineage-family-tree {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
padding: 10px 10px 22px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
.lineage-generation:first-of-type { border-top: 0; }
|
||||
.lineage-generation-label {
|
||||
.lineage-family-tree h3 {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
width: max-content;
|
||||
padding-right: 12px;
|
||||
background: rgba(255,253,248,0.82);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.lineage-tree-hint {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
margin: -2px 0 8px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 9px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
padding-top: 8px;
|
||||
background: rgba(255,255,255,0.62);
|
||||
border: 1px solid rgba(84,68,48,0.08);
|
||||
}
|
||||
.lineage-generation-nodes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.family-node.lineage-node {
|
||||
width: 132px;
|
||||
min-height: 54px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.lineage-children {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
font-size: 9px;
|
||||
color: rgba(117,106,94,0.84);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.lineage-empty-family {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
.lineage-tree-panel {
|
||||
.lineage-family-tree .lineage-tree-panel {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
min-width: 360px;
|
||||
margin-top: 6px;
|
||||
border-radius: 14px;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(84,68,48,0.045) 1px, transparent 1px),
|
||||
linear-gradient(180deg, rgba(84,68,48,0.040) 1px, transparent 1px),
|
||||
rgba(255,250,244,0.48);
|
||||
background-size: 34px 34px, 34px 34px, auto;
|
||||
border: 1px solid rgba(84,68,48,0.10);
|
||||
}
|
||||
.lineage-links {
|
||||
.lineage-family-tree .lineage-links {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
isolation: isolate;
|
||||
}
|
||||
.lineage-links path {
|
||||
.lineage-render-status {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 6;
|
||||
width: max-content;
|
||||
margin: 0 0 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
background: rgba(255,255,255,0.88);
|
||||
border: 1px solid rgba(84,68,48,0.10);
|
||||
box-shadow: 0 4px 10px rgba(64,48,32,0.06);
|
||||
}
|
||||
.lineage-stale-status {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 6;
|
||||
width: max-content;
|
||||
max-width: calc(100% - 12px);
|
||||
margin: 0 0 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
color: rgba(92, 75, 52, 0.82);
|
||||
background: rgba(255, 249, 232, 0.92);
|
||||
border: 1px solid rgba(150, 115, 46, 0.16);
|
||||
box-shadow: 0 4px 10px rgba(64,48,32,0.05);
|
||||
}
|
||||
.lineage-family-tree .lineage-links path {
|
||||
fill: none;
|
||||
stroke: rgba(107, 88, 61, 0.58);
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.lineage-links .partner-link { stroke-width: 2.4; stroke: rgba(98, 80, 56, 0.66); }
|
||||
.lineage-links .child-link { stroke-width: 1.9; stroke: rgba(107, 88, 61, 0.54); }
|
||||
.tree-node {
|
||||
.lineage-family-tree .lineage-links .partner-link {
|
||||
stroke-width: 2.4;
|
||||
stroke: rgba(98, 80, 56, 0.68);
|
||||
stroke-dasharray: 5 4;
|
||||
}
|
||||
.lineage-family-tree .lineage-links .child-link {
|
||||
stroke-width: 2;
|
||||
stroke: rgba(107, 88, 61, 0.58);
|
||||
}
|
||||
.lineage-family-tree .tree-node {
|
||||
position: absolute;
|
||||
width: 132px;
|
||||
min-height: 54px;
|
||||
height: 68px;
|
||||
min-height: 68px;
|
||||
box-sizing: border-box;
|
||||
align-items: flex-start;
|
||||
background: linear-gradient(180deg, #fffdf8, #f8f1e5);
|
||||
box-shadow: 0 2px 5px rgba(64,48,32,0.035);
|
||||
contain: layout paint style;
|
||||
z-index: 2;
|
||||
isolation: isolate;
|
||||
}
|
||||
.tree-generation-label {
|
||||
.lineage-family-tree .tree-node.child {
|
||||
border-color: rgba(103,178,44,0.34);
|
||||
}
|
||||
.lineage-family-tree .tree-node.dead {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
.lineage-family-tree .tree-node[data-family-tarinai-id]:hover {
|
||||
transform: none;
|
||||
}
|
||||
.lineage-family-tree .tree-node .family-portrait {
|
||||
right: 0;
|
||||
bottom: -6px;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
}
|
||||
.lineage-family-tree .tree-generation-label {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
left: 10px;
|
||||
width: 54px;
|
||||
padding: 3px 0;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
background: rgba(255,255,255,0.70);
|
||||
border: 1px solid rgba(84,68,48,0.08);
|
||||
z-index: 2;
|
||||
}
|
||||
.lineage-family { overflow-x: auto; }
|
||||
|
||||
.lineage-family-readable {
|
||||
overflow: visible;
|
||||
}
|
||||
.lineage-readable-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(300px, 1.05fr) minmax(280px, 0.95fr);
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
.lineage-member-column,
|
||||
.lineage-reproduction-column {
|
||||
min-width: 0;
|
||||
}
|
||||
.lineage-generation-block {
|
||||
padding: 8px 0;
|
||||
border-top: 1px dashed rgba(84,68,48,0.12);
|
||||
}
|
||||
.lineage-generation-block:first-child { border-top: 0; padding-top: 0; }
|
||||
.lineage-generation-block > b,
|
||||
.lineage-column-title {
|
||||
display: block;
|
||||
margin: 0 0 7px;
|
||||
color: rgba(84,68,48,0.72);
|
||||
font-size: 11px;
|
||||
}
|
||||
.lineage-person-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
.family-node.lineage-person {
|
||||
width: auto;
|
||||
min-height: 46px;
|
||||
}
|
||||
.lineage-pair-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.lineage-pair-card {
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(84,68,48,0.12);
|
||||
background: rgba(255,253,248,0.70);
|
||||
}
|
||||
.lineage-pair-parents,
|
||||
.lineage-pair-children {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
.lineage-pair-parents {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.lineage-pair-children {
|
||||
padding-top: 5px;
|
||||
border-top: 1px solid rgba(84,68,48,0.08);
|
||||
}
|
||||
.lineage-plus {
|
||||
color: rgba(84,68,48,0.44);
|
||||
font-weight: 700;
|
||||
line-height: 28px;
|
||||
}
|
||||
.lineage-birth-arrow {
|
||||
color: rgba(107, 88, 61, 0.62);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
padding: 1px 0 3px 13px;
|
||||
}
|
||||
.lineage-mini {
|
||||
display: inline-grid;
|
||||
grid-template-columns: 20px minmax(0, 1fr);
|
||||
grid-template-rows: auto auto;
|
||||
gap: 0 4px;
|
||||
align-items: center;
|
||||
max-width: 132px;
|
||||
min-width: 88px;
|
||||
padding: 4px 6px;
|
||||
.lineage-family-tree .lineage-empty-family {
|
||||
position: absolute;
|
||||
left: 72px;
|
||||
bottom: 8px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 9px;
|
||||
border: 1px solid rgba(84,68,48,0.12);
|
||||
background: rgba(255,255,255,0.64);
|
||||
background: rgba(255,255,255,0.70);
|
||||
}
|
||||
.lineage-mini.child {
|
||||
border-color: rgba(103,178,44,0.28);
|
||||
background: rgba(246,255,235,0.62);
|
||||
}
|
||||
.lineage-mini.dead { opacity: 0.64; filter: grayscale(0.25); }
|
||||
.lineage-mini img {
|
||||
grid-row: 1 / span 2;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.lineage-mini b,
|
||||
.lineage-mini small {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lineage-mini b { font-size: 10px; line-height: 1.1; }
|
||||
.lineage-mini small { color: var(--muted); font-size: 8px; line-height: 1.1; }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.lineage-readable-grid { grid-template-columns: 1fr; }
|
||||
@media (max-width: 720px) {
|
||||
.ecology-grid { grid-template-columns: 1fr; }
|
||||
.ecology-card { grid-template-columns: 68px minmax(0, 1fr); }
|
||||
.ecology-card img { width: 68px; height: 68px; }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.lineage-main-content {
|
||||
height: 70vh;
|
||||
min-height: 420px;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,12 @@ Use this map to open the smallest relevant file first.
|
|||
- `js/items.js`: `Item` lifecycle, item save/load restore, grass and zunchi behavior, and item drawing.
|
||||
- `js/tarinai.js`: `Tarinai` state, AI, movement, eating, fighting, panic, reproduction-adjacent behavior, and drawing.
|
||||
- `js/world.js`: `World` state, save/load, spawning, family registry normalization, spatial queries, tools, weather, and update loop coordination.
|
||||
- `js/sim.js`: compatibility export for `window.TarinaiSim`.
|
||||
|
||||
## UI And Rendering
|
||||
- `js/render.js`: canvas scene rendering, lighting, weather visuals, HUD, and selected-card overlay.
|
||||
- `js/ui.js`: DOM references, selected/log panels, labels, save/load buttons, tool controls, canvas input, and UI event binding.
|
||||
- `js/ui_charts.js`: colony history aggregation and chart rendering.
|
||||
- `js/ui_family.js`: family tree components, layout, archive rendering, and validation helper.
|
||||
- `js/ui_exports.js`: compatibility export for `window.TarinaiUI`.
|
||||
|
||||
## Common Tasks
|
||||
- Change hunger, sleep, fighting, panic, eating, or movement: start with `js/tarinai.js`.
|
||||
|
|
|
|||
|
|
@ -1,163 +1,163 @@
|
|||
# たりないコロニー観察所:仕様メモ
|
||||
# たりないコロニー観察所:仕様メモ
|
||||
|
||||
## コンセプト
|
||||
## コンセプト
|
||||
|
||||
「不足を解決するゲーム」ではなく、
|
||||
「不足を抱えた生き物が、それでも勝手に暮らす様子を眺めるゲーム」。
|
||||
「不足を解決するゲーム」ではなく、
|
||||
「不足を抱えた生き物が、それでも勝手に暮らす様子を眺めるゲーム」。
|
||||
|
||||
プレイヤーは神ではなく、観察者兼飼育係程度の立場。
|
||||
すべてを最適化するより、少し足りない状態の揺らぎを観察することを主目的にする。
|
||||
プレイヤーは神ではなく、観察者兼飼育係程度の立場。
|
||||
すべてを最適化するより、少し足りない状態の揺らぎを観察することを主目的にする。
|
||||
|
||||
## 現在実装しているループ
|
||||
## 現在実装しているループ
|
||||
|
||||
1. 個体が空腹・孤独・眠気・不安を内部状態として持つ
|
||||
2. 状態に応じて餌・仲間・寝床を探す
|
||||
3. プレイヤーは環境にアイテムを置いて介入する
|
||||
4. 介入と個体行動により、繁殖・死亡・観察ログが生まれる
|
||||
5. 死亡個体は「たりない跡」を残し、環境に記録として残る
|
||||
1. 個体が空腹・孤独・眠気・不安を内部状態として持つ
|
||||
2. 状態に応じて餌・仲間・寝床を探す
|
||||
3. プレイヤーは環境にアイテムを置いて介入する
|
||||
4. 介入と個体行動により、繁殖・死亡・観察ログが生まれる
|
||||
5. 死亡個体は「たりない跡」を残し、環境に記録として残る
|
||||
|
||||
## 個体状態
|
||||
## 個体状態
|
||||
|
||||
| 状態 | 内容 |
|
||||
| 状態 | 内容 |
|
||||
|---|---|
|
||||
| 空腹 | 高いほど餌・草を探す。上がりすぎると死亡 |
|
||||
| 孤独 | 高いほど仲間に寄る。近くに仲間がいると下がる |
|
||||
| 眠気残量 | 低いほど寝る。寝床があると回復が速い |
|
||||
| 不安 | 高いほど慌てる。水・石・寝床などで下がる |
|
||||
| たりない度 | 上記の総合的な不安定さ |
|
||||
| 空腹 | 高いほど餌・草を探す。上がりすぎると死亡 |
|
||||
| 孤独 | 高いほど仲間に寄る。近くに仲間がいると下がる |
|
||||
| 眠気残量 | 低いほど寝る。寝床があると回復が速い |
|
||||
| 不安 | 高いほど慌てる。水・石・寝床などで下がる |
|
||||
| たりない度 | 上記の総合的な不安定さ |
|
||||
|
||||
## 個体タイプ
|
||||
## 個体タイプ
|
||||
|
||||
9枚のスプライトに対応。
|
||||
9枚のスプライトに対応。
|
||||
|
||||
- にこたりない
|
||||
- いかりたりない
|
||||
- うるうるたりない
|
||||
- じとたりない
|
||||
- よだれたりない
|
||||
- ぐしゃたりない
|
||||
- すやたりない
|
||||
- ぽかんたりない
|
||||
- のびたりない
|
||||
- にこたりない
|
||||
- いかりたりない
|
||||
- うるうるたりない
|
||||
- じとたりない
|
||||
- よだれたりない
|
||||
- ぐしゃたりない
|
||||
- すやたりない
|
||||
- ぽかんたりない
|
||||
- のびたりない
|
||||
|
||||
各タイプは、空腹速度・社交性・睡眠傾向・不安耐性・移動速度に差がある。
|
||||
各タイプは、空腹速度・社交性・睡眠傾向・不安耐性・移動速度に差がある。
|
||||
|
||||
## 介入アイテム
|
||||
## 介入アイテム
|
||||
|
||||
| アイテム | 役割 |
|
||||
| アイテム | 役割 |
|
||||
|---|---|
|
||||
| ふつうの餌 | 空腹を直接下げる |
|
||||
| あまい餌 | 空腹と不安を下げるが依存的な挙動を生みやすい |
|
||||
| 水滴 | 不安を下げる。時間で蒸発する |
|
||||
| 草 | 軽い餌・落ち着き場・死後の循環要素 |
|
||||
| ぬくい石 | 周辺温度を上げ、不安を下げる |
|
||||
| ふわふわ | 寝床。睡眠回復を強める |
|
||||
| つつく | 個体に一時的な不安と速度を与える |
|
||||
| 迷い込み | 新規個体を追加する |
|
||||
| ふつうの餌 | 空腹を直接下げる |
|
||||
| あまい餌 | 空腹と不安を下げるが依存的な挙動を生みやすい |
|
||||
| 水滴 | 不安を下げる。時間で蒸発する |
|
||||
| 草 | 軽い餌・落ち着き場・死後の循環要素 |
|
||||
| ぬくい石 | 周辺温度を上げ、不安を下げる |
|
||||
| ふわふわ | 寝床。睡眠回復を強める |
|
||||
| つつく | 個体に一時的な不安と速度を与える |
|
||||
| 迷い込み | 新規個体を追加する |
|
||||
|
||||
## 生死
|
||||
## 生死
|
||||
|
||||
死亡条件は現在、主に以下。
|
||||
死亡条件は現在、主に以下。
|
||||
|
||||
- 空腹過多
|
||||
- 寿命
|
||||
- 不安過多
|
||||
- 空腹と眠気の複合
|
||||
- 空腹過多
|
||||
- 寿命
|
||||
- 不安過多
|
||||
- 空腹と眠気の複合
|
||||
|
||||
死亡時には「たりない跡」が生成され、一定確率で草も発生する。
|
||||
これは観察ゲームとして「失敗」ではなく、生態系の記録として扱う。
|
||||
死亡時には「たりない跡」が生成され、一定確率で草も発生する。
|
||||
これは観察ゲームとして「失敗」ではなく、生態系の記録として扱う。
|
||||
|
||||
## 繁殖
|
||||
## 繁殖
|
||||
|
||||
近距離にいる2個体が、以下を満たすと低確率で新個体が発生する。
|
||||
近距離にいる2個体が、以下を満たすと低確率で新個体が発生する。
|
||||
|
||||
- 空腹が高すぎない
|
||||
- 眠気残量が十分ある
|
||||
- 繁殖クールダウンが終わっている
|
||||
- コロニーが過密でない
|
||||
- 空腹が高すぎない
|
||||
- 眠気残量が十分ある
|
||||
- 繁殖クールダウンが終わっている
|
||||
- コロニーが過密でない
|
||||
|
||||
子は親のタイプ・足りないものを一部継承し、低確率で変異する。
|
||||
子は親のタイプ・足りないものを一部継承し、低確率で変異する。
|
||||
|
||||
## 設計上の注意
|
||||
## 設計上の注意
|
||||
|
||||
- 完全な最適化ゲームに寄せすぎない
|
||||
- 数値を見せつつ、ログ文で生態観察感を出す
|
||||
- 介入は強いが万能ではない
|
||||
- 死亡や衰退も観察価値として扱う
|
||||
- 「むらさき」は画像作成者名のため、ゲーム内の異常現象名には使用していない
|
||||
- 完全な最適化ゲームに寄せすぎない
|
||||
- 数値を見せつつ、ログ文で生態観察感を出す
|
||||
- 介入は強いが万能ではない
|
||||
- 死亡や衰退も観察価値として扱う
|
||||
- 「むらさき」は画像作成者名のため、ゲーム内の異常現象名には使用していない
|
||||
|
||||
|
||||
## v3設計メモ
|
||||
## v3設計メモ
|
||||
|
||||
- 観察時間を伸ばすため、空腹・孤独の上昇をやや抑制し、寿命を長めにした。
|
||||
- 図鑑は「達成」ではなく観察記録として扱う。スプライト、イベント、死因をゆるく解放する。
|
||||
- 系譜は全個体を複雑なツリーで描画せず、出生ログとして読みやすさを優先した。
|
||||
- 音は外部素材を使わず、Web Audioの短い合成音で実装した。ブラウザ仕様上、音 ON を押すまで鳴らない。
|
||||
- 観察時間を伸ばすため、空腹・孤独の上昇をやや抑制し、寿命を長めにした。
|
||||
- 図鑑は「達成」ではなく観察記録として扱う。スプライト、イベント、死因をゆるく解放する。
|
||||
- 系譜は全個体を複雑なツリーで描画せず、出生ログとして読みやすさを優先した。
|
||||
- 音は外部素材を使わず、Web Audioの短い合成音で実装した。ブラウザ仕様上、音 ON を押すまで鳴らない。
|
||||
|
||||
|
||||
## v4 仕様追記
|
||||
## v4 仕様追記
|
||||
|
||||
### 子の追従
|
||||
繁殖で生まれた子は `childGrowTime` の間、親のどちらか近い方を追従する。成長すると通常AIへ移行する。
|
||||
### 子の追従
|
||||
繁殖で生まれた子は `childGrowTime` の間、親のどちらか近い方を追従する。成長すると通常AIへ移行する。
|
||||
|
||||
### 系譜
|
||||
系譜は `family` レジストリで保持し、親子関係を樹状図で表示する。表示時は同じ個体を重複させず、共有された親子関係は複数の接続線で表す。
|
||||
### 系譜
|
||||
系譜は `family` レジストリで保持し、親子関係を樹状図で表示する。表示時は同じ個体を重複させず、共有された親子関係は複数の接続線で表す。
|
||||
|
||||
### 食事エフェクト
|
||||
食事パーティクルは表示頻度を抑え、対象アイテムの色を反映する。
|
||||
### 食事エフェクト
|
||||
食事パーティクルは表示頻度を抑え、対象アイテムの色を反映する。
|
||||
|
||||
### 喧嘩
|
||||
近距離、低気分、怒り個体、過密などを条件に小さな喧嘩が発生する。喧嘩中は 02_angry を優先表示し、互いにノックバックする。
|
||||
### 喧嘩
|
||||
近距離、低気分、怒り個体、過密などを条件に小さな喧嘩が発生する。喧嘩中は 02_angry を優先表示し、互いにノックバックする。
|
||||
|
||||
### 昼夜
|
||||
夜間・夕方の色を背景だけでなく、観察皿・アイテム・たりない本体にも重ねる。
|
||||
### 昼夜
|
||||
夜間・夕方の色を背景だけでなく、観察皿・アイテム・たりない本体にも重ねる。
|
||||
|
||||
|
||||
## v5 仕様追記
|
||||
## v5 仕様追記
|
||||
|
||||
### 草
|
||||
草はコロニー内に自動で発生する。草アイテムは `amount` を草量として持ち、時間でゆっくり伸びる。食べられると `amount` が減り、描画本数も減る。
|
||||
### 草
|
||||
草はコロニー内に自動で発生する。草アイテムは `amount` を草量として持ち、時間でゆっくり伸びる。食べられると `amount` が減り、描画本数も減る。
|
||||
|
||||
### 昼夜の描画修正
|
||||
スプライト本体への色補正は `source-atop` の矩形塗りを廃止し、`ctx.filter` に変更した。これにより透過部分へ矩形状の色が乗る問題を避ける。
|
||||
### 昼夜の描画修正
|
||||
スプライト本体への色補正は `source-atop` の矩形塗りを廃止し、`ctx.filter` に変更した。これにより透過部分へ矩形状の色が乗る問題を避ける。
|
||||
|
||||
### 系譜
|
||||
図鑑UIは削除し、系譜のみをメイン画面下部に配置する。
|
||||
### 系譜
|
||||
図鑑UIは削除し、系譜のみをメイン画面下部に配置する。
|
||||
|
||||
|
||||
## v6 仕様追記
|
||||
## v6 仕様追記
|
||||
|
||||
### 頭突き喧嘩
|
||||
喧嘩中は一定周期で相手へ突進し、近距離でノックバックと喧嘩エフェクトを発生させる。
|
||||
### 頭突き喧嘩
|
||||
喧嘩中は一定周期で相手へ突進し、近距離でノックバックと喧嘩エフェクトを発生させる。
|
||||
|
||||
### 睡眠
|
||||
睡眠状態では速度をゼロにし、移動処理を行わない。寝床が近くても寝ながら移動しない。
|
||||
### 睡眠
|
||||
睡眠状態では速度をゼロにし、移動処理を行わない。寝床が近くても寝ながら移動しない。
|
||||
|
||||
### カーソル反応
|
||||
カーソルが近い場合、気分の良い個体は笑顔で寄り、不安・怒り・つつき記憶のある個体は逃げる。
|
||||
### カーソル反応
|
||||
カーソルが近い場合、気分の良い個体は笑顔で寄り、不安・怒り・つつき記憶のある個体は逃げる。
|
||||
|
||||
|
||||
## v7 ビジュアル改修
|
||||
## v7 ビジュアル改修
|
||||
|
||||
- 背景をシャーレ風から庭の観察区画風へ変更。
|
||||
- 地面は木枠に囲まれた明るい土床とし、細かな土粒、苔色のムラ、縁の草、野花を加えた。
|
||||
- たりないとアイテムの接地影を追加し、庭の上に実在している感覚を強めた。
|
||||
- 右上HUDと下部ラベルを観察札のような見た目へ調整。
|
||||
- 親子の追従を視認しやすくするため、近距離では薄い点線ガイドを描く。
|
||||
- 背景をシャーレ風から庭の観察区画風へ変更。
|
||||
- 地面は木枠に囲まれた明るい土床とし、細かな土粒、苔色のムラ、縁の草、野花を加えた。
|
||||
- たりないとアイテムの接地影を追加し、庭の上に実在している感覚を強めた。
|
||||
- 右上HUDと下部ラベルを観察札のような見た目へ調整。
|
||||
- 親子の追従を視認しやすくするため、近距離では薄い点線ガイドを描く。
|
||||
|
||||
|
||||
## v8 ビジュアル / 実装改修
|
||||
## v8 ビジュアル / 実装改修
|
||||
|
||||
- 直射日光・月光の方向をもとに、たりないとアイテムへ投影影を付与。
|
||||
- 背景では天体本体は描かず、空色と影方向のみで昼夜を表現する。
|
||||
- 食事の蓄積で「ずんち」を排泄する消化処理を追加。
|
||||
- 09_stretch は通常描画でも横にやや広く扱い、視認性を上げた。
|
||||
- 系譜ツリーの接続線は擬似要素を再設計し、途切れにくい連結表現へ変更。
|
||||
- 旧図鑑用の状態管理(codex)と未使用スタイルを整理。
|
||||
- 直射日光・月光の方向をもとに、たりないとアイテムへ投影影を付与。
|
||||
- 背景では天体本体は描かず、空色と影方向のみで昼夜を表現する。
|
||||
- 食事の蓄積で「ずんち」を排泄する消化処理を追加。
|
||||
- 09_stretch は通常描画でも横にやや広く扱い、視認性を上げた。
|
||||
- 系譜ツリーの接続線は擬似要素を再設計し、途切れにくい連結表現へ変更。
|
||||
- 旧図鑑用の状態管理(codex)と未使用スタイルを整理。
|
||||
|
||||
|
||||
## v9 修正
|
||||
## v9 修正
|
||||
|
||||
- `ctx.filter = blur(...)` を使った影を廃止し、低負荷な楕円描画へ変更。
|
||||
- 背景装飾のループ数を削減し、毎フレームの描画負荷を下げた。
|
||||
- 死亡時の `splat` は amount を連続的なアルファ値へ反映し、数分かけてフェードアウトする。
|
||||
- `ctx.filter = blur(...)` を使った影を廃止し、低負荷な楕円描画へ変更。
|
||||
- 背景装飾のループ数を削減し、毎フレームの描画負荷を下げた。
|
||||
- 死亡時の `splat` は amount を連続的なアルファ値へ反映し、数分かけてフェードアウトする。
|
||||
|
|
|
|||
127
index.html
127
index.html
|
|
@ -3,22 +3,24 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>たりない観察箱</title>
|
||||
<link rel="stylesheet" href="css/styles.css" />
|
||||
<title>たりない観察</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=9.0.0" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<h1>たりない観察箱</h1>
|
||||
<p class="subtitle">たりないが暮らす小さな庭を観察します。食べ、眠り、関係を変えます。</p>
|
||||
<div class="top-left">
|
||||
<a class="btn room-link" href="https://host.nishi.boats/~333/">333の部屋に移動</a>
|
||||
<div class="brand">
|
||||
<h1>たりない観察</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<button id="pauseBtn" class="btn primary">一時停止</button>
|
||||
<button id="speedBtn" class="btn">速度 x1</button>
|
||||
<button id="ecologyBtn" class="btn">たりないの生態</button>
|
||||
<button id="soundBtn" class="btn">音 OFF</button>
|
||||
<button id="saveBtn" class="btn">保存</button>
|
||||
<button id="loadBtn" class="btn">読込</button>
|
||||
<button id="creditsBtn" class="btn">クレジット</button>
|
||||
<button id="resetBtn" class="btn danger">リセット</button>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -40,6 +42,7 @@
|
|||
<button class="tool selected" data-tool="observe"><span>O</span>観察</button>
|
||||
<button class="tool" data-tool="delete"><span>D</span>削除</button>
|
||||
<button class="tool" data-tool="poke"><span>P</span>つつく</button>
|
||||
<button class="tool" data-tool="pinch"><span>🤏</span>つまむ</button>
|
||||
<button class="tool" data-tool="new"><span>N</span>追加</button>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -48,6 +51,8 @@
|
|||
<div class="tool-grid tool-category-body">
|
||||
<button class="tool" data-tool="zunchi"><span>Z</span>ずんち</button>
|
||||
<button class="tool" data-tool="sweet"><span>S</span>ずんだ餅</button>
|
||||
<button class="tool" data-tool="love_mochi"><span>H</span>へこ餅</button>
|
||||
<button class="tool" data-tool="fight_mochi"><span>K</span>けんか餅</button>
|
||||
<button class="tool" data-tool="water"><span>W</span>水</button>
|
||||
<button class="tool" data-tool="grass"><span>G</span>草</button>
|
||||
</div>
|
||||
|
|
@ -57,12 +62,8 @@
|
|||
<div class="tool-grid tool-category-body">
|
||||
<button class="tool" data-tool="stone"><span>R</span>石</button>
|
||||
<button class="tool" data-tool="bed"><span>B</span>干草寝床</button>
|
||||
<button class="tool" data-tool="ball"><span>L</span>ボール</button>
|
||||
<button class="tool" data-tool="firecracker"><span>F</span>爆竹</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tool-category" data-tool-category="fence">
|
||||
<button class="tool-category-toggle" type="button" aria-expanded="true">柵</button>
|
||||
<div class="tool-grid tool-category-body">
|
||||
<button class="tool" data-tool="fence_v"><span>V</span>縦の柵</button>
|
||||
<button class="tool" data-tool="fence_h"><span>H</span>横の柵</button>
|
||||
</div>
|
||||
|
|
@ -76,9 +77,9 @@
|
|||
<h2>コロニー</h2>
|
||||
<div class="stats colony-stats">
|
||||
<div class="stat"><span>個体数</span><strong id="statPop">0</strong></div>
|
||||
<div class="stat"><span>死亡数</span><strong id="statDead">0</strong></div>
|
||||
<div class="stat"><span>累計死亡数</span><strong id="statDead">0</strong></div>
|
||||
</div>
|
||||
<div id="colonyChartTabs" class="chart-tabs"><button data-chart="population" class="active">人口</button><button data-chart="life">生活</button><button data-chart="events">事件</button><button data-chart="objects">オブジェクト</button></div>
|
||||
<div id="colonyChartTabs" class="chart-tabs"><button data-chart="population" class="active">人口</button><button data-chart="life">生活</button><button data-chart="events">イベント</button><button data-chart="objects">オブジェクト</button></div>
|
||||
<div id="colonyChartLegend" class="colony-chart-legend"></div>
|
||||
<div id="colonyChart" class="colony-chart" aria-label="コロニーの推移グラフ" role="img"></div>
|
||||
</section>
|
||||
|
|
@ -89,15 +90,17 @@
|
|||
</section>
|
||||
|
||||
<section class="card log-card">
|
||||
<h2>記録</h2>
|
||||
<h2>イベント</h2>
|
||||
<div id="log" class="log"></div>
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<section class="card lineage-main-card">
|
||||
<h2>家系図</h2>
|
||||
<p class="hint">子どもは成長するまで親についていきます。この領域をスクロールして家系を確認できます。</p>
|
||||
<div class="lineage-head">
|
||||
<h2>家系図</h2>
|
||||
<button id="archiveResetBtn" class="btn danger" type="button">家系図をリセット</button>
|
||||
</div>
|
||||
<div id="archiveContent" class="archive-content lineage-main-content"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -116,20 +119,80 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/data.js"></script>
|
||||
<script src="js/math.js"></script>
|
||||
<script src="js/assets.js"></script>
|
||||
<script src="js/audio.js"></script>
|
||||
<script src="js/render.js"></script>
|
||||
<script src="js/sim_core.js"></script>
|
||||
<script src="js/items.js"></script>
|
||||
<script src="js/tarinai.js"></script>
|
||||
<script src="js/world.js"></script>
|
||||
<script src="js/sim.js"></script>
|
||||
<script src="js/ui.js"></script>
|
||||
<script src="js/ui_charts.js"></script>
|
||||
<script src="js/ui_family.js"></script>
|
||||
<script src="js/ui_exports.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
|
||||
<div id="creditsDialog" class="field-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="creditsDialogTitle">
|
||||
<div class="field-dialog-panel credits-dialog-panel">
|
||||
<h2 id="creditsDialogTitle">クレジット</h2>
|
||||
<div class="credits-content">
|
||||
<p><strong>製作:</strong> 333</p>
|
||||
<p><strong>たりない:</strong> むらさき氏</p>
|
||||
<p><strong>効果音:</strong> VOICEVOX: ずんだもん 効果音ラボ: <a href="https://soundeffect-lab.info/" target="_blank" rel="noopener">https://soundeffect-lab.info/</a></p>
|
||||
</div>
|
||||
<div class="field-dialog-actions credits-actions">
|
||||
<button id="creditsCloseBtn" class="btn" type="button">閉じる</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="archiveResetDialog" class="field-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="archiveResetDialogTitle">
|
||||
<div class="field-dialog-panel archive-reset-dialog-panel">
|
||||
<h2 id="archiveResetDialogTitle">家系図をリセットしますか?</h2>
|
||||
<p class="hint">現在の親子関係と世代情報を消去します。個体そのものや庭の状態は消えません。</p>
|
||||
<div class="field-dialog-actions archive-reset-actions">
|
||||
<button id="archiveResetCancelBtn" class="btn" type="button">キャンセル</button>
|
||||
<button id="archiveResetConfirmBtn" class="btn danger" type="button">リセットする</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ecologyDialog" class="field-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="ecologyDialogTitle">
|
||||
<div class="field-dialog-panel ecology-dialog-panel">
|
||||
<h2 id="ecologyDialogTitle">たりないの生態</h2>
|
||||
<div class="ecology-grid">
|
||||
<section class="ecology-card">
|
||||
<img data-src="assets/sprites/tarinai_05_drool.png" loading="lazy" decoding="async" alt="">
|
||||
<div><strong>食べる</strong><p>お腹がすくと食べ物や草を探します。食べられない時間が長いと弱っていきます。</p></div>
|
||||
</section>
|
||||
<section class="ecology-card">
|
||||
<img data-src="assets/sprites/tarinai_07_sleep.png" loading="lazy" decoding="async" alt="">
|
||||
<div><strong>眠る</strong><p>疲れるとその場や寝床で眠ります。休めると元気を取り戻します。</p></div>
|
||||
</section>
|
||||
<section class="ecology-card">
|
||||
<img data-src="assets/sprites/tarinai_21_normal_happy.png" loading="lazy" decoding="async" alt="">
|
||||
<div><strong>なつく</strong><p>気分がよい個体はカーソルや仲間に寄ってきます。やさしく構うと落ち着きやすくなります。</p></div>
|
||||
</section>
|
||||
<section class="ecology-card">
|
||||
<img data-src="assets/sprites/tarinai_24_stress_dizzy.png" loading="lazy" decoding="async" alt="">
|
||||
<div><strong>関係が変わる</strong><p>近くで過ごすうちに仲良しや苦手が生まれます。強いストレスは行動や表情に出ます。</p></div>
|
||||
</section>
|
||||
<section class="ecology-card">
|
||||
<img data-src="assets/sprites/tarinai_27_birth_ritual.png" loading="lazy" decoding="async" alt="">
|
||||
<div><strong>増える</strong><p>落ち着いた相手どうしから新しい個体が生まれ、家系図に親子関係が残ります。</p></div>
|
||||
</section>
|
||||
<section class="ecology-card">
|
||||
<img data-src="assets/sprites/tarinai_18_fear.png" loading="lazy" decoding="async" alt="">
|
||||
<div><strong>環境の影響</strong><p>雨や食べ物不足、つつき、喧嘩などで暮らしぶりが変わります。庭の配置も大切です。</p></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="field-dialog-actions">
|
||||
<button id="ecologyCloseBtn" class="btn" type="button">閉じる</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/data.js?v=9.0.0"></script>
|
||||
<script src="js/math.js?v=9.0.0"></script>
|
||||
<script src="js/assets.js?v=9.0.0"></script>
|
||||
<script src="js/audio.js?v=9.0.0"></script>
|
||||
<script src="js/render.js?v=9.0.0"></script>
|
||||
<script src="js/sim_core.js?v=9.0.0"></script>
|
||||
<script src="js/items.js?v=9.0.0"></script>
|
||||
<script src="js/tarinai.js?v=9.0.0"></script>
|
||||
<script src="js/world.js?v=9.0.0"></script>
|
||||
<script src="js/ui.js?v=9.0.0"></script>
|
||||
<script src="js/ui_charts.js?v=9.0.0"></script>
|
||||
<script src="js/ui_family.js?v=9.0.0"></script>
|
||||
<script src="js/main.js?v=9.0.0"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
140
js/assets.js
140
js/assets.js
|
|
@ -2,10 +2,15 @@
|
|||
const images = new Map();
|
||||
const imageBoundsCache = new WeakMap();
|
||||
const imageMetrics = new Map();
|
||||
const imagePromises = new Map();
|
||||
const imageAssetIndex = new Map();
|
||||
|
||||
const scaledSpriteCache = new Map();
|
||||
const stainOverlayCache = new Map();
|
||||
|
||||
const INITIAL_IMAGE_IDS = new Set(["smile", "zunchi", "zunchi_02"]);
|
||||
const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "weak", "hurt", "hurt2", "zunchi", "zunchi_02"]);
|
||||
|
||||
function trimCanvasCache(cache, limit = 420) {
|
||||
while (cache.size > limit) {
|
||||
const first = cache.keys().next().value;
|
||||
|
|
@ -79,32 +84,111 @@ function getCachedStainOverlayCanvas(id, img, w, h, stain = 0, seed = 0.5) {
|
|||
return canvas;
|
||||
}
|
||||
|
||||
function loadImages() {
|
||||
const assets = [...SPRITES, ...DECOR_ASSETS];
|
||||
let loaded = 0;
|
||||
return new Promise((resolve) => {
|
||||
for (const s of assets) {
|
||||
const img = new Image();
|
||||
img.onload = img.onerror = () => {
|
||||
const w = img.naturalWidth || img.width || ALPHA_BOUNDS[s.id]?.imageW || 512;
|
||||
const h = img.naturalHeight || img.height || ALPHA_BOUNDS[s.id]?.imageH || 512;
|
||||
const bounds = ALPHA_BOUNDS[s.id] || { x: 0, y: 0, w, h, imageW: w, imageH: h };
|
||||
imageMetrics.set(s.id, {
|
||||
w,
|
||||
h,
|
||||
ratio: h / Math.max(w, 1),
|
||||
visibleBottomRatio: (bounds.y + bounds.h) / Math.max(bounds.imageH, 1),
|
||||
});
|
||||
loaded += 1;
|
||||
if (loaded === assets.length) resolve();
|
||||
};
|
||||
img.alphaBounds = ALPHA_BOUNDS[s.id] || null;
|
||||
img.src = s.path;
|
||||
images.set(s.id, img);
|
||||
}
|
||||
function primeImageMetrics(asset) {
|
||||
if (!asset || imageMetrics.has(asset.id)) return;
|
||||
const bounds = ALPHA_BOUNDS[asset.id];
|
||||
const w = bounds?.imageW || 512;
|
||||
const h = bounds?.imageH || 512;
|
||||
imageMetrics.set(asset.id, {
|
||||
w,
|
||||
h,
|
||||
ratio: h / Math.max(w, 1),
|
||||
visibleBottomRatio: bounds ? (bounds.y + bounds.h) / Math.max(bounds.imageH, 1) : 1,
|
||||
});
|
||||
}
|
||||
|
||||
function setupAssetIndex() {
|
||||
if (imageAssetIndex.size) return;
|
||||
for (const asset of [...SPRITES, ...DECOR_ASSETS]) {
|
||||
imageAssetIndex.set(asset.id, asset);
|
||||
primeImageMetrics(asset);
|
||||
}
|
||||
}
|
||||
|
||||
function markImageLoaded(asset, img) {
|
||||
const w = img.naturalWidth || img.width || ALPHA_BOUNDS[asset.id]?.imageW || 512;
|
||||
const h = img.naturalHeight || img.height || ALPHA_BOUNDS[asset.id]?.imageH || 512;
|
||||
const bounds = ALPHA_BOUNDS[asset.id] || { x: 0, y: 0, w, h, imageW: w, imageH: h };
|
||||
imageMetrics.set(asset.id, {
|
||||
w,
|
||||
h,
|
||||
ratio: h / Math.max(w, 1),
|
||||
visibleBottomRatio: (bounds.y + bounds.h) / Math.max(bounds.imageH, 1),
|
||||
});
|
||||
}
|
||||
|
||||
function isImageReady(id) {
|
||||
const img = images.get(id);
|
||||
return !!(img && img.complete && (img.naturalWidth || img.width));
|
||||
}
|
||||
|
||||
function loadImageAsset(asset, priority = "auto") {
|
||||
if (!asset) return Promise.resolve(null);
|
||||
setupAssetIndex();
|
||||
primeImageMetrics(asset);
|
||||
if (isImageReady(asset.id)) return Promise.resolve(images.get(asset.id));
|
||||
if (imagePromises.has(asset.id)) return imagePromises.get(asset.id);
|
||||
const img = images.get(asset.id) || new Image();
|
||||
img.decoding = "async";
|
||||
if ("fetchPriority" in img) img.fetchPriority = priority;
|
||||
img.alphaBounds = ALPHA_BOUNDS[asset.id] || null;
|
||||
images.set(asset.id, img);
|
||||
const promise = new Promise((resolve) => {
|
||||
img.onload = () => { markImageLoaded(asset, img); resolve(img); };
|
||||
img.onerror = () => { resolve(null); };
|
||||
});
|
||||
imagePromises.set(asset.id, promise);
|
||||
if (!img.src) img.src = asset.path;
|
||||
return promise;
|
||||
}
|
||||
|
||||
function ensureImage(id) {
|
||||
setupAssetIndex();
|
||||
const asset = imageAssetIndex.get(id);
|
||||
if (!asset) return null;
|
||||
primeImageMetrics(asset);
|
||||
const existing = images.get(id);
|
||||
if (!existing || !existing.src) loadImageAsset(asset, "low");
|
||||
return images.get(id) || null;
|
||||
}
|
||||
|
||||
function getRenderableImage(id, fallbackId = "smile") {
|
||||
if (id) ensureImage(id);
|
||||
if (isImageReady(id)) return images.get(id);
|
||||
if (fallbackId && fallbackId !== id) ensureImage(fallbackId);
|
||||
if (isImageReady(fallbackId)) return images.get(fallbackId);
|
||||
return null;
|
||||
}
|
||||
|
||||
function loadImages(ids = null) {
|
||||
setupAssetIndex();
|
||||
const requested = ids ? new Set(ids) : INITIAL_IMAGE_IDS;
|
||||
const tasks = [];
|
||||
for (const id of requested) {
|
||||
const asset = imageAssetIndex.get(id);
|
||||
if (asset) tasks.push(loadImageAsset(asset, "high"));
|
||||
}
|
||||
return Promise.all(tasks).then(() => undefined);
|
||||
}
|
||||
|
||||
function startBackgroundImageLoading() {
|
||||
setupAssetIndex();
|
||||
const allAssets = [...SPRITES, ...DECOR_ASSETS];
|
||||
const loadGroup = (ids, priority = "low") => {
|
||||
for (const id of ids) {
|
||||
const asset = imageAssetIndex.get(id);
|
||||
if (asset) loadImageAsset(asset, priority);
|
||||
}
|
||||
};
|
||||
const schedule = window.requestIdleCallback
|
||||
? (fn, timeout = 1200) => window.requestIdleCallback(fn, { timeout })
|
||||
: (fn) => setTimeout(fn, 60);
|
||||
schedule(() => loadGroup(EARLY_IMAGE_IDS, "auto"), 450);
|
||||
schedule(() => {
|
||||
for (const asset of allAssets) loadImageAsset(asset, "low");
|
||||
}, 2200);
|
||||
}
|
||||
|
||||
function imageAlphaBounds(img) {
|
||||
if (!img) return null;
|
||||
if (img.alphaBounds) return img.alphaBounds;
|
||||
|
|
@ -119,7 +203,12 @@ function imageAlphaBounds(img) {
|
|||
}
|
||||
|
||||
function getImageMetrics(idOrImg) {
|
||||
if (typeof idOrImg === "string") return imageMetrics.get(idOrImg) || null;
|
||||
if (typeof idOrImg === "string") {
|
||||
setupAssetIndex();
|
||||
const asset = imageAssetIndex.get(idOrImg);
|
||||
if (asset) primeImageMetrics(asset);
|
||||
return imageMetrics.get(idOrImg) || null;
|
||||
}
|
||||
if (!idOrImg) return null;
|
||||
for (const [id, img] of images) {
|
||||
if (img === idOrImg) return imageMetrics.get(id) || null;
|
||||
|
|
@ -129,5 +218,6 @@ function getImageMetrics(idOrImg) {
|
|||
return w && h ? { w, h, ratio: h / w, visibleBottomRatio: 1 } : null;
|
||||
}
|
||||
|
||||
setupAssetIndex();
|
||||
|
||||
window.TarinaiAssets = { images, imageBoundsCache, imageMetrics, scaledSpriteCache, stainOverlayCache, loadImages, imageAlphaBounds, getImageMetrics, getCachedSpriteCanvas, getCachedStainOverlayCanvas };
|
||||
window.TarinaiAssets = { images, imageBoundsCache, imageMetrics, imagePromises, scaledSpriteCache, stainOverlayCache, loadImages, startBackgroundImageLoading, ensureImage, isImageReady, getRenderableImage, imageAlphaBounds, getImageMetrics, getCachedSpriteCanvas, getCachedStainOverlayCanvas };
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const SPRITES = [
|
|||
{ id: "cry", label: "\u6ce3\u304d\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_06_cry.png" },
|
||||
{ id: "sleep", label: "\u7720\u308a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_07_sleep.png" },
|
||||
{ id: "pokan", label: "\u307d\u304b\u3093\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_08_pokan.png" },
|
||||
{ id: "stretch", label: "\u4f38\u3073\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_09_stretch.png" },
|
||||
{ id: "stretch", label: "\u4f38\u3073\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_09_stretch.png", actionOnly: true },
|
||||
{ id: "hurt", label: "\u75db\u304c\u308a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_10_hurt.png", actionOnly: true },
|
||||
{ id: "weak", label: "\u5f31\u308a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_11_weak.png", actionOnly: true },
|
||||
{ id: "back", label: "\u5965\u5411\u304d\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_12_back.png", actionOnly: true },
|
||||
|
|
@ -27,6 +27,7 @@ const SPRITES = [
|
|||
{ id: "stress_sweat", label: "\u9ad8\u30b9\u30c8\u30ec\u30b9\u305f\u308a\u306a\u3044\u30fb\u6c57", path: "assets/sprites/tarinai_25_stress_sweat.png", actionOnly: true },
|
||||
{ id: "intimidate", label: "\u5a01\u5687\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_26_intimidate.png", actionOnly: true },
|
||||
{ id: "birth_ritual", label: "\u7e41\u6b96\u524d\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_27_birth_ritual.png", actionOnly: true },
|
||||
{ id: "zunchi_slave", label: "\u305a\u3093\u3061\u3069\u308c\u3044", path: "assets/sprites/tarinai_28_zunchi_slave.png", displayOnly: true },
|
||||
];
|
||||
const DECOR_ASSETS = [
|
||||
{ id: "zunchi", path: "assets/objects/zunchi.png" },
|
||||
|
|
@ -59,6 +60,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 },
|
||||
zunchi_slave: { x: 17, y: 22, w: 478, h: 431, imageW: 512, imageH: 468 },
|
||||
zunchi: { x: 0, y: 0, w: 236, h: 178, imageW: 236, imageH: 178 },
|
||||
zunchi_02: { x: 8, y: 63, w: 220, h: 105, imageW: 236, imageH: 178 },
|
||||
};
|
||||
|
|
@ -70,6 +72,8 @@ const CONFIG = {
|
|||
initialPopulation: 18,
|
||||
maxPopulation: Number.MAX_SAFE_INTEGER,
|
||||
itemLimit: 260,
|
||||
fenceLimit: 72,
|
||||
maxFenceCollisionChecks: 18,
|
||||
zunchiLimit: 56,
|
||||
traceLimit: 64,
|
||||
splatLimit: 48,
|
||||
|
|
|
|||
292
js/items.js
292
js/items.js
|
|
@ -1,4 +1,4 @@
|
|||
"use strict";
|
||||
"use strict";
|
||||
|
||||
class Item {
|
||||
constructor(type, x, y) {
|
||||
|
|
@ -7,10 +7,10 @@ class Item {
|
|||
this.x = x;
|
||||
this.y = y;
|
||||
this.r = {
|
||||
food: 14, sweet: 13, water: 12, grass: 17, stone: 20, bed: 37, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14
|
||||
food: 14, sweet: 13, love_mochi: 13, fight_mochi: 13, water: 12, grass: 17, stone: 20, bed: 37, ball: 18, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14
|
||||
}[type] || 12;
|
||||
this.amount = {
|
||||
food: 85, sweet: 62, water: 64, grass: 120, stone: 999, bed: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240
|
||||
food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, water: 64, grass: 120, stone: 999, bed: 999, ball: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240
|
||||
}[type] || 80;
|
||||
this.age = 0;
|
||||
this.seed = Math.random() * 1000;
|
||||
|
|
@ -25,11 +25,26 @@ class Item {
|
|||
this.seedTimer = rand(18, 38);
|
||||
this.eatenAmount = 0;
|
||||
this.fertilityBoost = 0;
|
||||
this.lifeSpan = rand(420, 860);
|
||||
this.wither = 0;
|
||||
}
|
||||
if (type === "bed") {
|
||||
this.comfort = rand(0.86, 1.14);
|
||||
this.wear = 0;
|
||||
}
|
||||
if (type === "ball") {
|
||||
this.vx = rand(-5, 5);
|
||||
this.vy = rand(-5, 5);
|
||||
this.prevX = x;
|
||||
this.prevY = y;
|
||||
this.spin = rand(0, Math.PI * 2);
|
||||
this.spinVelocity = rand(-1.6, 1.6);
|
||||
this.lastKickedAt = -999;
|
||||
this.lastKickerId = "";
|
||||
this.lastPokedAt = -999;
|
||||
this.pokeCombo = 0;
|
||||
this.lastPokeAngle = rand(0, Math.PI * 2);
|
||||
}
|
||||
if (type === "zunchi") {
|
||||
this.stage = "fresh";
|
||||
this.stageTimer = 0;
|
||||
|
|
@ -53,7 +68,7 @@ class Item {
|
|||
}
|
||||
}
|
||||
if (this.type === "water") this.amount -= dt * 0.9;
|
||||
if (this.type === "sweet") this.amount -= dt * 0.12;
|
||||
if (["sweet", "love_mochi", "fight_mochi"].includes(this.type)) this.amount -= dt * 0.12;
|
||||
if (this.type === "trace") this.amount -= dt * 1.35;
|
||||
if (this.type === "splat") this.amount -= dt * 1.05;
|
||||
if (this.type === "firecracker") {
|
||||
|
|
@ -63,6 +78,7 @@ class Item {
|
|||
else this.amount = 0;
|
||||
}
|
||||
}
|
||||
if (this.type === "ball") this.updateBall(dt, worldRef);
|
||||
if (this.type === "grass") {
|
||||
this.lifecycleTimer += dt;
|
||||
const factor = worldRef.grassUpdateFactor ? worldRef.grassUpdateFactor() : 1;
|
||||
|
|
@ -82,6 +98,153 @@ class Item {
|
|||
}
|
||||
}
|
||||
}
|
||||
updateBall(dt, worldRef) {
|
||||
this.prevX = this.x;
|
||||
this.prevY = this.y;
|
||||
const maxSpeed = 1250;
|
||||
const clampVelocity = () => {
|
||||
const speed = Math.hypot(this.vx || 0, this.vy || 0);
|
||||
if (speed > maxSpeed) {
|
||||
this.vx = (this.vx || 0) / speed * maxSpeed;
|
||||
this.vy = (this.vy || 0) / speed * maxSpeed;
|
||||
}
|
||||
return Math.hypot(this.vx || 0, this.vy || 0);
|
||||
};
|
||||
const moving = clampVelocity();
|
||||
if (moving > 0.04) {
|
||||
this.x += (this.vx || 0) * dt;
|
||||
this.y += (this.vy || 0) * dt;
|
||||
const p = Math.max(28, CONFIG.worldPadding || 30);
|
||||
if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx || 0) * 0.72; this.spinVelocity *= -0.72; }
|
||||
if (this.x > worldRef.w - p) { this.x = worldRef.w - p; this.vx = -Math.abs(this.vx || 0) * 0.72; this.spinVelocity *= -0.72; }
|
||||
if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy || 0) * 0.72; this.spinVelocity *= -0.72; }
|
||||
if (this.y > worldRef.h - p) { this.y = worldRef.h - p; this.vy = -Math.abs(this.vy || 0) * 0.72; this.spinVelocity *= -0.72; }
|
||||
this.resolveBallObstacleCollisions(dt, worldRef);
|
||||
this.x = clamp(this.x, p, worldRef.w - p);
|
||||
this.y = clamp(this.y, p, worldRef.h - p);
|
||||
clampVelocity();
|
||||
const groundFriction = Math.pow(0.72, dt);
|
||||
this.vx *= groundFriction;
|
||||
this.vy *= groundFriction;
|
||||
}
|
||||
this.spin = (this.spin || 0) + (this.spinVelocity || 0) * dt + (this.vx || 0) * dt / Math.max(8, this.r || 18);
|
||||
this.spinVelocity *= Math.pow(0.65, dt);
|
||||
if (Math.hypot(this.vx || 0, this.vy || 0) < 0.18) { this.vx = 0; this.vy = 0; }
|
||||
}
|
||||
|
||||
resolveBallObstacleCollisions(dt, worldRef) {
|
||||
if (!worldRef?.nearbyItems) return;
|
||||
const speed = Math.hypot(this.vx || 0, this.vy || 0);
|
||||
const searchRadius = Math.max(130, (this.r || 18) + speed * Math.max(dt || 0.016, 0.016) + 96);
|
||||
const items = worldRef.nearbyItems(this.x, this.y, searchRadius) || [];
|
||||
let fenceChecks = 0;
|
||||
const maxFenceChecks = CONFIG.maxFenceCollisionChecks ?? 18;
|
||||
for (const it of items) {
|
||||
if (!it || it === this || it.dead) continue;
|
||||
if (it.type === "bed") {
|
||||
this.applyBallHayDrag(it, dt, worldRef);
|
||||
} else if (it.type === "stone") {
|
||||
this.resolveBallCircleBounce(it, (it.r || 20) * 1.08 + (this.r || 18), 0.82, worldRef);
|
||||
} else if (it.type === "fence_v" || it.type === "fence_h") {
|
||||
if (fenceChecks >= maxFenceChecks) continue;
|
||||
fenceChecks += 1;
|
||||
this.resolveBallFenceBounce(it, worldRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyBallHayDrag(bed, dt, worldRef) {
|
||||
const rx = Math.max(26, (bed.r || 37) * 1.72 + (this.r || 18) * 0.40);
|
||||
const ry = Math.max(18, (bed.r || 37) * 0.92 + (this.r || 18) * 0.34);
|
||||
const dx = this.x - bed.x;
|
||||
const dy = this.y - bed.y;
|
||||
const inside = (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry) <= 1;
|
||||
if (!inside) return;
|
||||
const slow = Math.pow(0.16, Math.max(0.016, dt || 0.016));
|
||||
this.vx *= slow;
|
||||
this.vy *= slow;
|
||||
this.spinVelocity *= Math.pow(0.24, Math.max(0.016, dt || 0.016));
|
||||
if ((this.lastHaySlowLogAt || -999) + 3.5 < (worldRef.time || 0) && Math.hypot(this.vx || 0, this.vy || 0) > 120) {
|
||||
this.lastHaySlowLogAt = worldRef.time || 0;
|
||||
worldRef.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(14, this.r * 0.95), life: 0.20, color: "rgba(214,184,96,0.42)" }));
|
||||
}
|
||||
}
|
||||
|
||||
resolveBallCircleBounce(obstacle, hitRadius, restitution, worldRef) {
|
||||
let dx = this.x - obstacle.x;
|
||||
let dy = this.y - obstacle.y;
|
||||
let d = Math.hypot(dx, dy);
|
||||
if (d >= hitRadius) return;
|
||||
if (d < 0.001) {
|
||||
const speed = Math.hypot(this.vx || 0, this.vy || 0);
|
||||
if (speed > 0.001) { dx = -(this.vx || 0) / speed; dy = -(this.vy || 0) / speed; d = 1; }
|
||||
else { dx = Math.cos(this.seed || 0); dy = Math.sin(this.seed || 0); d = 1; }
|
||||
}
|
||||
const nx = dx / d;
|
||||
const ny = dy / d;
|
||||
const toward = (this.vx || 0) * nx + (this.vy || 0) * ny;
|
||||
this.x = obstacle.x + nx * (hitRadius + 0.5);
|
||||
this.y = obstacle.y + ny * (hitRadius + 0.5);
|
||||
if (toward < 0) {
|
||||
this.vx = (this.vx || 0) - (1 + restitution) * toward * nx;
|
||||
this.vy = (this.vy || 0) - (1 + restitution) * toward * ny;
|
||||
} else {
|
||||
this.vx = (this.vx || 0) + nx * 24;
|
||||
this.vy = (this.vy || 0) + ny * 24;
|
||||
}
|
||||
this.vx *= 0.96;
|
||||
this.vy *= 0.96;
|
||||
this.spinVelocity = clamp((this.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 5.5, -38, 38);
|
||||
this.emitBallBounce(worldRef, obstacle);
|
||||
}
|
||||
|
||||
resolveBallFenceBounce(fence, worldRef) {
|
||||
const vertical = fence.type === "fence_v";
|
||||
const len = Math.max(112, (fence.r || 42) * 3.55);
|
||||
const thick = Math.max(10, (fence.r || 42) * 0.31);
|
||||
const halfLen = len / 2;
|
||||
const sx1 = vertical ? fence.x : fence.x - halfLen;
|
||||
const sy1 = vertical ? fence.y - halfLen : fence.y;
|
||||
const sx2 = vertical ? fence.x : fence.x + halfLen;
|
||||
const sy2 = vertical ? fence.y + halfLen : fence.y;
|
||||
const vx = sx2 - sx1;
|
||||
const vy = sy2 - sy1;
|
||||
const segLenSq = vx * vx + vy * vy || 1;
|
||||
const u = clamp(((this.x - sx1) * vx + (this.y - sy1) * vy) / segLenSq, 0, 1);
|
||||
const cx = sx1 + vx * u;
|
||||
const cy = sy1 + vy * u;
|
||||
const hitRadius = (this.r || 18) + thick / 2 + 2;
|
||||
let dx = this.x - cx;
|
||||
let dy = this.y - cy;
|
||||
let d = Math.hypot(dx, dy);
|
||||
if (d >= hitRadius) return;
|
||||
if (d < 0.001) {
|
||||
if (vertical) { dx = (this.vx || 0) >= 0 ? -1 : 1; dy = 0; }
|
||||
else { dx = 0; dy = (this.vy || 0) >= 0 ? -1 : 1; }
|
||||
d = 1;
|
||||
}
|
||||
const nx = dx / d;
|
||||
const ny = dy / d;
|
||||
const toward = (this.vx || 0) * nx + (this.vy || 0) * ny;
|
||||
this.x = cx + nx * (hitRadius + 0.5);
|
||||
this.y = cy + ny * (hitRadius + 0.5);
|
||||
if (toward < 0) {
|
||||
this.vx = (this.vx || 0) - 1.78 * toward * nx;
|
||||
this.vy = (this.vy || 0) - 1.78 * toward * ny;
|
||||
}
|
||||
this.vx *= 0.94;
|
||||
this.vy *= 0.94;
|
||||
this.spinVelocity = clamp((this.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 6.2, -38, 38);
|
||||
this.emitBallBounce(worldRef, fence);
|
||||
}
|
||||
|
||||
emitBallBounce(worldRef, obstacle) {
|
||||
const now = worldRef?.time || 0;
|
||||
if ((this.lastObstacleBounceAt || -999) + 0.08 > now) return;
|
||||
this.lastObstacleBounceAt = now;
|
||||
worldRef?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(13, (this.r || 18) * 0.82), life: 0.18, color: obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)" }));
|
||||
}
|
||||
|
||||
updateGrass(dt, worldRef) {
|
||||
const light = worldRef.lightLevel ? worldRef.lightLevel() : 0.7;
|
||||
const weather = worldRef.weather || "sunny";
|
||||
|
|
@ -124,13 +287,22 @@ class Item {
|
|||
const growthRate = (0.006 + light * 0.004 + weatherBoost * 0.01 + waterNear * 0.014 + this.fertilityBoost * 0.014) * dt * 0.5;
|
||||
this.growth = clamp((this.growth ?? 0.25) + growthRate - dryStress * dt, 0, 0.88);
|
||||
this.health = clamp((this.health ?? 1) + dt * (waterNear > 0.05 || weather === "light_rain" ? 0.017 : -0.0015 - dryStress * 0.28), 0.18, 1);
|
||||
const grassLimit = CONFIG.grassLimit ?? 96;
|
||||
const grassLimit = worldRef.grassLimit ? worldRef.grassLimit() : (CONFIG.grassLimit ?? 96);
|
||||
if (this.growth >= 0.86 && (worldRef.itemCounts?.grass || 0) < grassLimit) {
|
||||
const spawned = this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 2);
|
||||
if (spawned > 0) this.growth = rand(0.52, 0.68);
|
||||
this.seedTimer = rand(18, 34);
|
||||
}
|
||||
this.amount = clamp(this.growth * 120 * this.health, 3, 130);
|
||||
if (this.age > (this.lifeSpan || 640) * 0.82) {
|
||||
const fade = clamp((this.age - (this.lifeSpan || 640) * 0.82) / Math.max((this.lifeSpan || 640) * 0.18, 1), 0, 1);
|
||||
this.wither = fade;
|
||||
this.health = clamp((this.health ?? 1) - dt * (0.004 + fade * 0.020), 0, 1);
|
||||
this.growth = clamp((this.growth ?? 0.25) - dt * fade * 0.005, 0, 0.88);
|
||||
} else {
|
||||
this.wither = Math.max(0, (this.wither || 0) - dt * 0.08);
|
||||
}
|
||||
this.amount = clamp(this.growth * 120 * this.health, 0, 130);
|
||||
if (this.age > (this.lifeSpan || 640) * 1.05) this.amount -= dt * (1.2 + (this.wither || 0) * 24);
|
||||
this.seedTimer -= dt * (this.growth > 0.72 ? 1 : 0.25) * 0.5;
|
||||
if (this.seedTimer <= 0 && this.growth > 0.64 && (worldRef.itemCounts?.grass || 0) < grassLimit) {
|
||||
this.seedTimer = rand(24, 54);
|
||||
|
|
@ -139,7 +311,7 @@ class Item {
|
|||
}
|
||||
seedAround(worldRef, fertileSource = null, fertile = false, maxNew = 1) {
|
||||
let made = 0;
|
||||
const limit = CONFIG.grassLimit ?? 96;
|
||||
const limit = worldRef.grassLimit ? worldRef.grassLimit() : (CONFIG.grassLimit ?? 96);
|
||||
for (let n = 0; n < maxNew && (worldRef.itemCounts?.grass || 0) < limit; n++) {
|
||||
const source = fertile && fertileSource ? fertileSource : this;
|
||||
const spot = worldRef.findGrassSproutSpot
|
||||
|
|
@ -157,7 +329,9 @@ class Item {
|
|||
}
|
||||
|
||||
updateZunchi(dt, worldRef) {
|
||||
const rainBoost = worldRef.weather === "light_rain" ? 1.35 : 1;
|
||||
const rainy = worldRef.weather === "light_rain";
|
||||
const rainBoost = rainy ? 1.95 : 1;
|
||||
const decayBoost = rainy ? 2.15 : 1;
|
||||
let waterBoost = 0;
|
||||
const nearby = worldRef.nearbyItems ? worldRef.nearbyItems(this.x, this.y, 84) : (worldRef.items || []);
|
||||
for (const it of nearby) {
|
||||
|
|
@ -167,17 +341,19 @@ class Item {
|
|||
this.stageTimer += dt * 2.0 * (rainBoost + waterBoost * 1.3);
|
||||
if (this.stage === "fresh") {
|
||||
this.freshness = clamp(1 - this.stageTimer / 110, 0, 1);
|
||||
this.amount -= dt * 0.05 * decayBoost;
|
||||
if (this.stageTimer > 110) { this.stage = "dry"; this.stageTimer = 0; }
|
||||
} else if (this.stage === "dry") {
|
||||
this.freshness = clamp(0.55 - this.stageTimer / 220, 0.20, 0.55);
|
||||
this.amount -= dt * 0.09 * decayBoost;
|
||||
if (this.stageTimer > 150) { this.stage = "decomposing"; this.stageTimer = 0; this.fertility = 0.35; }
|
||||
} else if (this.stage === "decomposing") {
|
||||
this.fertility = clamp(this.fertility + dt * 0.015, 0, 1);
|
||||
this.amount -= dt * 0.12;
|
||||
this.amount -= dt * 0.12 * decayBoost;
|
||||
if (this.stageTimer > 190) { this.stage = "fertile_soil"; this.stageTimer = 0; this.amount = Math.min(this.amount, 140); }
|
||||
} else if (this.stage === "fertile_soil") {
|
||||
this.fertility = clamp(1 - this.stageTimer / 360, 0, 1);
|
||||
this.amount -= dt * 0.26;
|
||||
this.amount -= dt * 0.26 * decayBoost;
|
||||
}
|
||||
}
|
||||
get dead() {
|
||||
|
|
@ -187,8 +363,11 @@ class Item {
|
|||
return {
|
||||
type: this.type, x: this.x, y: this.y, amount: this.amount, age: this.age, seed: this.seed,
|
||||
stage: this.stage, stageTimer: this.stageTimer, fertility: this.fertility, freshness: this.freshness,
|
||||
growth: this.growth, health: this.health, seedTimer: this.seedTimer, eatenAmount: this.eatenAmount, fertilityBoost: this.fertilityBoost,
|
||||
comfort: this.comfort, wear: this.wear, zunchiVariant: this.zunchiVariant, toolSize: this.toolSize, blastScale: this.blastScale, fuseTimer: this.fuseTimer, fuseMax: this.fuseMax, dropTimer: this.dropTimer, dropMax: this.dropMax
|
||||
growth: this.growth, health: this.health, seedTimer: this.seedTimer, eatenAmount: this.eatenAmount, fertilityBoost: this.fertilityBoost, lifeSpan: this.lifeSpan, wither: this.wither,
|
||||
comfort: this.comfort, wear: this.wear, crawlOpen: this.crawlOpen, zunchiVariant: this.zunchiVariant, toolSize: this.toolSize, blastScale: this.blastScale, fuseTimer: this.fuseTimer, fuseMax: this.fuseMax, dropTimer: this.dropTimer, dropMax: this.dropMax,
|
||||
vx: this.vx, vy: this.vy, prevX: this.prevX, prevY: this.prevY,
|
||||
spin: this.spin, spinVelocity: this.spinVelocity, lastKickedAt: this.lastKickedAt, lastKickerId: this.lastKickerId,
|
||||
lastPokedAt: this.lastPokedAt, pokeCombo: this.pokeCombo, lastPokeAngle: this.lastPokeAngle
|
||||
};
|
||||
}
|
||||
static from(o) {
|
||||
|
|
@ -200,10 +379,10 @@ class Item {
|
|||
it.x = o.x;
|
||||
it.y = o.y;
|
||||
it.r = {
|
||||
food: 14, sweet: 13, water: 12, grass: 17, stone: 20, bed: 37, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14
|
||||
food: 14, sweet: 13, love_mochi: 13, fight_mochi: 13, water: 12, grass: 17, stone: 20, bed: 37, ball: 18, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14
|
||||
}[type] || 12;
|
||||
it.amount = o.amount ?? ({
|
||||
food: 85, sweet: 62, water: 64, grass: 120, stone: 999, bed: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240
|
||||
food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, water: 64, grass: 120, stone: 999, bed: 999, ball: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240
|
||||
}[type] || 80);
|
||||
it.age = o.age || 0;
|
||||
it.seed = o.seed || Math.random() * 1000;
|
||||
|
|
@ -223,6 +402,19 @@ class Item {
|
|||
it.comfort = 1;
|
||||
it.wear = 0;
|
||||
}
|
||||
if (type === "ball") {
|
||||
it.vx = o.vx || 0;
|
||||
it.vy = o.vy || 0;
|
||||
it.prevX = o.prevX ?? o.x;
|
||||
it.prevY = o.prevY ?? o.y;
|
||||
it.spin = o.spin || 0;
|
||||
it.spinVelocity = o.spinVelocity || 0;
|
||||
it.lastKickedAt = o.lastKickedAt ?? -999;
|
||||
it.lastKickerId = o.lastKickerId || "";
|
||||
it.lastPokedAt = o.lastPokedAt ?? -999;
|
||||
it.pokeCombo = o.pokeCombo || 0;
|
||||
it.lastPokeAngle = o.lastPokeAngle || rand(0, Math.PI * 2);
|
||||
}
|
||||
if (type === "zunchi") {
|
||||
it.stage = "fresh";
|
||||
it.stageTimer = 0;
|
||||
|
|
@ -243,8 +435,22 @@ class Item {
|
|||
it.seedTimer = o.seedTimer ?? it.seedTimer;
|
||||
it.eatenAmount = o.eatenAmount || 0;
|
||||
it.fertilityBoost = o.fertilityBoost || 0;
|
||||
it.lifeSpan = o.lifeSpan ?? it.lifeSpan;
|
||||
it.wither = o.wither ?? it.wither ?? 0;
|
||||
it.comfort = o.comfort ?? it.comfort;
|
||||
it.wear = o.wear ?? it.wear;
|
||||
it.crawlOpen = o.crawlOpen ?? it.crawlOpen;
|
||||
it.vx = o.vx ?? it.vx ?? 0;
|
||||
it.vy = o.vy ?? it.vy ?? 0;
|
||||
it.prevX = o.prevX ?? it.prevX ?? it.x;
|
||||
it.prevY = o.prevY ?? it.prevY ?? it.y;
|
||||
it.spin = o.spin ?? it.spin ?? 0;
|
||||
it.spinVelocity = o.spinVelocity ?? it.spinVelocity ?? 0;
|
||||
it.lastKickedAt = o.lastKickedAt ?? it.lastKickedAt ?? -999;
|
||||
it.lastKickerId = o.lastKickerId || it.lastKickerId || "";
|
||||
it.lastPokedAt = o.lastPokedAt ?? it.lastPokedAt ?? -999;
|
||||
it.pokeCombo = o.pokeCombo ?? it.pokeCombo ?? 0;
|
||||
it.lastPokeAngle = o.lastPokeAngle ?? it.lastPokeAngle ?? rand(0, Math.PI * 2);
|
||||
it.zunchiVariant = o.zunchiVariant || it.zunchiVariant || "zunchi";
|
||||
it.toolSize = o.toolSize || it.toolSize || "medium";
|
||||
it.blastScale = o.blastScale || it.blastScale || 1;
|
||||
|
|
@ -267,7 +473,7 @@ class Item {
|
|||
const shadow = projectedShadowParams(lightState, Math.max(0.4, this.r / 18));
|
||||
if (this.type !== "trace" && this.type !== "splat") {
|
||||
const zunchiId = this.type === "zunchi" ? (this.zunchiVariant || "zunchi") : null;
|
||||
const zunchiImg = zunchiId ? (images.get(zunchiId) || images.get("zunchi")) : null;
|
||||
const zunchiImg = zunchiId ? (typeof getRenderableImage === "function" ? getRenderableImage(zunchiId, "zunchi") : (images.get(zunchiId) || images.get("zunchi"))) : null;
|
||||
if (zunchiImg) {
|
||||
const metrics = getImageMetrics(zunchiId || "zunchi") || getImageMetrics("zunchi");
|
||||
const ratio = metrics?.ratio || 178 / 236;
|
||||
|
|
@ -303,10 +509,12 @@ class Item {
|
|||
ctx.save();
|
||||
ctx.translate(this.x, this.y + dropY);
|
||||
ctx.globalAlpha = visibleAlpha;
|
||||
if (this.type === "food" || this.type === "sweet") {
|
||||
if (this.type === "food" || this.type === "sweet" || this.type === "love_mochi" || this.type === "fight_mochi") {
|
||||
const sweet = this.type === "sweet";
|
||||
ctx.fillStyle = sweet ? "#fffaf1" : "#f1dfb7";
|
||||
ctx.strokeStyle = sweet ? "#9a8a72" : "#8d6f48";
|
||||
const loveMochi = this.type === "love_mochi";
|
||||
const fightMochi = this.type === "fight_mochi";
|
||||
ctx.fillStyle = loveMochi ? "#fff1f6" : (fightMochi ? "#fff3ec" : (sweet ? "#fffaf1" : "#f1dfb7"));
|
||||
ctx.strokeStyle = loveMochi ? "#bf6f8b" : (fightMochi ? "#bc6d42" : (sweet ? "#9a8a72" : "#8d6f48"));
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
const bob = 0;
|
||||
|
|
@ -321,6 +529,30 @@ class Item {
|
|||
ctx.beginPath();
|
||||
ctx.ellipse(-this.r * 0.32, -this.r * 0.16, this.r * 0.24, this.r * 0.10, -0.35, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (loveMochi) {
|
||||
ctx.fillStyle = "#ff6a9c";
|
||||
ctx.font = `${Math.round((this.r || 13) * 1.15)}px sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("\u2764", 0, 1);
|
||||
ctx.fillStyle = "rgba(255,255,255,0.55)";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-this.r * 0.28, -this.r * 0.18, this.r * 0.23, this.r * 0.10, -0.35, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (fightMochi) {
|
||||
ctx.fillStyle = "#d55632";
|
||||
ctx.font = `${Math.round((this.r || 13) * 1.02)}px sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("\u2726", 0, 1);
|
||||
ctx.strokeStyle = "rgba(168,76,42,0.72)";
|
||||
ctx.lineWidth = 1.4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-this.r * 0.44, -this.r * 0.42);
|
||||
ctx.lineTo(this.r * 0.44, this.r * 0.42);
|
||||
ctx.moveTo(-this.r * 0.44, this.r * 0.42);
|
||||
ctx.lineTo(this.r * 0.44, -this.r * 0.42);
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.fillStyle = "#fff7dc";
|
||||
for (let i = 0; i < 5; i++) {
|
||||
|
|
@ -343,7 +575,8 @@ class Item {
|
|||
const stage = growth < 0.22 ? "sprout" : growth < 0.48 ? "young" : "mature";
|
||||
const eaten = this.eatenAmount > 14 || growth < 0.16;
|
||||
const grassLoad = 1;
|
||||
ctx.strokeStyle = eaten ? "#7b8755" : (styleNight ? "#294b40" : (styleWarm ? "#64ad3f" : "#4f8438"));
|
||||
const wither = clamp(this.wither || 0, 0, 1);
|
||||
ctx.strokeStyle = eaten ? "#7b8755" : (wither > 0.08 ? `rgba(${Math.round(106 + wither * 40)}, ${Math.round(132 - wither * 32)}, ${Math.round(70 - wither * 18)}, 1)` : (styleNight ? "#294b40" : (styleWarm ? "#64ad3f" : "#4f8438")));
|
||||
ctx.shadowColor = "transparent";
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.lineCap = "round";
|
||||
|
|
@ -399,6 +632,27 @@ class Item {
|
|||
ctx.lineTo(x + Math.cos(a) * len * 0.5, y + Math.sin(a) * len * 0.5);
|
||||
ctx.stroke();
|
||||
}
|
||||
} else if (this.type === "ball") {
|
||||
const speed = Math.hypot(this.vx || 0, this.vy || 0);
|
||||
const moving = clamp(speed / 260, 0, 1);
|
||||
ctx.save();
|
||||
ctx.rotate(this.spin || 0);
|
||||
ctx.shadowColor = styleNight ? "rgba(0,0,0,0.34)" : "rgba(52,40,26,0.18)";
|
||||
ctx.shadowBlur = 2 + moving * 3;
|
||||
ctx.shadowOffsetY = 2;
|
||||
ctx.font = `${Math.round((this.r || 18) * 2.0)}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("\u26bd", 0, 1);
|
||||
if (moving > 0.55) {
|
||||
ctx.globalAlpha = 0.14 + moving * 0.10;
|
||||
ctx.shadowColor = "transparent";
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-this.r * 0.35, -this.r * 0.42, this.r * 0.25, this.r * 0.12, -0.45, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "firecracker") {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -48,5 +48,6 @@ loadImages().then(() => {
|
|||
render();
|
||||
renderStats();
|
||||
syncTopButtons();
|
||||
if (typeof startBackgroundImageLoading === "function") startBackgroundImageLoading();
|
||||
loop();
|
||||
});
|
||||
|
|
|
|||
96
js/render.js
96
js/render.js
|
|
@ -468,16 +468,58 @@ function drawAtmosphericLighting(ctx, w, h, lighting) {
|
|||
function drawRain(ctx, w, h, t) {
|
||||
if (world.weather !== "light_rain") return;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.28;
|
||||
ctx.strokeStyle = "rgba(197, 220, 230, 0.70)";
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < 34; i++) {
|
||||
const x = (randSeed(18000 + i, 0, w) + t * 26) % w;
|
||||
const y = (randSeed(18400 + i, 0, h) + t * 72) % h;
|
||||
const perf = world.performanceLevel ? world.performanceLevel() : 0;
|
||||
const count = perf >= 2 ? 58 : 118;
|
||||
ctx.lineCap = "round";
|
||||
for (let i = 0; i < count; i++) {
|
||||
const speed = randSeed(18100 + i, 132, 230);
|
||||
const cycle = h + 320;
|
||||
const len = randSeed(18500 + i, 13, 26);
|
||||
const tilt = randSeed(18600 + i, 5, 12);
|
||||
const fall = randSeed(18400 + i, -220, cycle) + t * speed;
|
||||
const y = ((fall % cycle) + cycle) % cycle - 140;
|
||||
// The droplet's movement vector matches the visible diagonal stroke.
|
||||
const driftPerY = -tilt / Math.max(1, len);
|
||||
const xCycle = w + 220;
|
||||
const rawX = randSeed(18000 + i, -70, w + 120) + fall * driftPerY + Math.sin(t * 0.9 + i * 0.43) * 4;
|
||||
const x = ((rawX % xCycle) + xCycle) % xCycle - 90;
|
||||
const alpha = y < 110 ? clamp((y + 68) / 178, 0, 1) : 1;
|
||||
ctx.globalAlpha = 0.34 + 0.30 * alpha;
|
||||
ctx.strokeStyle = "rgba(198, 226, 240, 0.92)";
|
||||
ctx.lineWidth = randSeed(18700 + i, 1.0, 1.8);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x - 4, y + 10);
|
||||
ctx.lineTo(x - tilt, y + len);
|
||||
ctx.stroke();
|
||||
if (perf < 3 && i % 2 === 0) {
|
||||
ctx.globalAlpha = 0.18 + 0.18 * alpha;
|
||||
ctx.fillStyle = "rgba(215, 237, 246, 0.92)";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x - tilt * 0.9, y + len + 2, 1.8, 3.0, -0.28, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
if (perf < 3) {
|
||||
const dripCount = perf >= 2 ? 10 : 18;
|
||||
for (let i = 0; i < dripCount; i++) {
|
||||
const phase = (t * randSeed(18800 + i, 0.55, 1.15) + randSeed(18900 + i, 0, 100)) % 1;
|
||||
const x = randSeed(19000 + i, 12, w - 12);
|
||||
const topY = -22 + phase * (h + 58);
|
||||
const bottomY = topY + randSeed(19100 + i, 14, 30);
|
||||
const lean = randSeed(19200 + i, -6, -2);
|
||||
ctx.globalAlpha = 0.44;
|
||||
ctx.strokeStyle = "rgba(206, 233, 247, 0.98)";
|
||||
ctx.lineWidth = randSeed(19300 + i, 1.2, 2.2);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, topY);
|
||||
ctx.lineTo(x + lean, bottomY);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 0.26;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x + lean, bottomY + 2, 2.1, 3.5, -0.18, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "rgba(220, 241, 248, 0.98)";
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
|
@ -486,6 +528,22 @@ function weatherLabel(weather) {
|
|||
return { sunny: "\u6674\u308c", cloudy: "\u66c7\u308a", light_rain: "\u5c0f\u96e8" }[weather] || "\u6674\u308c";
|
||||
}
|
||||
|
||||
function fillScreenFallback(ctx, w, h, lighting) {
|
||||
const light = lighting?.light ?? 0.7;
|
||||
ctx.save();
|
||||
const bg = ctx.createLinearGradient(0, 0, 0, Math.max(1, h));
|
||||
if (light > 0.42) {
|
||||
bg.addColorStop(0, "#cfe3d3");
|
||||
bg.addColorStop(1, "#d9d0ad");
|
||||
} else {
|
||||
bg.addColorStop(0, "#24355f");
|
||||
bg.addColorStop(1, "#4f5d71");
|
||||
}
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function render() {
|
||||
activeCtx = ctx;
|
||||
const sceneW = world.w, sceneH = world.h;
|
||||
|
|
@ -495,9 +553,9 @@ function render() {
|
|||
const cameraX = world.cameraX || 0;
|
||||
const cameraY = world.cameraY || 0;
|
||||
const viewScale = world.viewScale ? world.viewScale() : 1;
|
||||
ctx.clearRect(0, 0, screenW, screenH);
|
||||
|
||||
const lighting = getLightingState(world);
|
||||
fillScreenFallback(ctx, screenW, screenH, lighting);
|
||||
const light = lighting.light;
|
||||
const cachedBackground = ensureBackgroundCache(sceneW, sceneH, lighting);
|
||||
|
||||
|
|
@ -523,7 +581,13 @@ function render() {
|
|||
|
||||
beginFieldTransform();
|
||||
for (const it of world.items) {
|
||||
if (it.type !== "zunchi") it.draw(ctx, world.time, lighting);
|
||||
if (it.type === "trace" || it.type === "splat") it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const ef of world.effects) {
|
||||
if (ef.type === "bleed") ef.draw(ctx);
|
||||
}
|
||||
for (const it of world.items) {
|
||||
if (it.type !== "zunchi" && it.type !== "trace" && it.type !== "splat") it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const it of world.items) {
|
||||
if (it.type === "zunchi") it.draw(ctx, world.time, lighting);
|
||||
|
|
@ -546,7 +610,9 @@ function render() {
|
|||
|
||||
const sorted = world.sortedDrawList ? world.sortedDrawList() : world.tarinai;
|
||||
for (const t of sorted) t.draw(ctx, world.time, lighting);
|
||||
for (const ef of world.effects) ef.draw(ctx);
|
||||
for (const ef of world.effects) {
|
||||
if (ef.type !== "bleed") ef.draw(ctx);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Light affecting the whole visible scene, including characters and ground.
|
||||
|
|
@ -615,9 +681,9 @@ function drawSelectedCard(ctx, world, lighting) {
|
|||
const w = world.viewportW || world.w;
|
||||
const h = world.viewportH || world.h;
|
||||
const cardW = 152;
|
||||
const cardH = 66;
|
||||
const cardH = 80;
|
||||
let x = t.x + 18;
|
||||
let y = t.y - 86;
|
||||
let y = t.y - 100;
|
||||
x = clamp(x, 10, w - cardW - 10);
|
||||
y = clamp(y, 10, h - cardH - 10);
|
||||
ctx.save();
|
||||
|
|
@ -633,8 +699,10 @@ function drawSelectedCard(ctx, world, lighting) {
|
|||
const child = t.juvenile && t.parentToFollow?.() ? " / \u5b50" : "";
|
||||
const zHint = t.digest > 4.9 ? " / \u305a\u3093\u3061" : "";
|
||||
ctx.fillText(`${stateLabel(t.state)}${child}${zHint}`, x + 10, y + 35);
|
||||
const personality = t.personalityLabel ? t.personalityLabel() : (t.personality || "\u4e0d\u660e");
|
||||
ctx.fillText(`\u6027\u683c ${personality}`, x + 10, y + 52);
|
||||
const genLabel = (t.displayGeneration && t.displayGeneration()) ? ` \u4e16\u4ee3 ${t.generation}` : "";
|
||||
ctx.fillText(`\u6c17\u5206 ${fmt(t.mood)}${genLabel} \u4f53\u529b ${fmt(t.energy)}`, x + 10, y + 52);
|
||||
ctx.fillText(`\u6c17\u5206 ${fmt(t.mood)}${genLabel} \u4f53\u529b ${fmt(t.energy)}`, x + 10, y + 68);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
|
|
@ -649,4 +717,4 @@ function roundedRect(ctx, x, y, w, h, r) {
|
|||
}
|
||||
|
||||
|
||||
window.TarinaiRender = { randSeed, roundedBlob, getLightingState, projectedShadowParams, imageVisibleBottomFromTop, drawImageProjectedShadow, drawProjectedShadow, drawRadialGlow, drawItemGlow, drawCreatureGlow, drawDawnRimLight, drawGardenBackdrop, drawGardenBed, drawLightRays, drawAtmosphericLighting, drawRain, drawSelectedCard, render, roundedRect };
|
||||
window.TarinaiRender = { randSeed, roundedBlob, getLightingState, projectedShadowParams, imageVisibleBottomFromTop, drawImageProjectedShadow, drawProjectedShadow, drawRadialGlow, drawItemGlow, drawCreatureGlow, drawDawnRimLight, drawGardenBackdrop, drawGardenBed, drawLightRays, drawAtmosphericLighting, drawRain, fillScreenFallback, drawSelectedCard, render, roundedRect };
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
"use strict";
|
||||
"use strict";
|
||||
|
||||
window.TarinaiSim = { makeName, makeTrait, PERSONALITIES, SpatialGrid, Effect, Item, Tarinai, World, world };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"use strict";
|
||||
"use strict";
|
||||
function makeName(worldRef = null) {
|
||||
const existing = new Set((worldRef?.tarinai || []).filter(t => !t.dead).map(t => t.name));
|
||||
const build = () => {
|
||||
|
|
@ -65,12 +65,13 @@ function stableUnit(seed, salt = "") {
|
|||
}
|
||||
|
||||
const PERSONALITIES = {
|
||||
timid: { label: "\u81c6\u75c5", desc: "\u9003\u3052\u3084\u3059\u304f\u3001\u55a7\u5629\u3092\u907f\u3051\u308b", fear: 1.42, fight: 0.48, social: 0.96, relation: 1.04, sleep: 1.00, zunda: 1.00 },
|
||||
glutton: { label: "\u98df\u3044\u3057\u3093\u574a", desc: "\u305a\u3093\u3060\u9905\u3092\u512a\u5148\u3057\u3084\u3059\u3044", fear: 0.92, fight: 0.82, social: 1.00, relation: 1.00, sleep: 0.95, zunda: 1.42 },
|
||||
sleepy: { label: "\u7720\u305f\u304c\u308a", desc: "\u65e9\u3081\u306b\u5bdd\u5e8a\u3078\u5411\u304b\u3046", fear: 0.96, fight: 0.72, social: 0.92, relation: 0.96, sleep: 1.34, zunda: 0.92 },
|
||||
lonely: { label: "\u5bc2\u3057\u304c\u308a", desc: "\u4ed6\u306e\u305f\u308a\u306a\u3044\u3078\u5bc4\u308a\u3084\u3059\u3044", fear: 1.02, fight: 0.70, social: 1.34, relation: 1.38, sleep: 0.96, zunda: 1.00 },
|
||||
irritable: { label: "\u6012\u308a\u3063\u307d\u3044", desc: "\u55a7\u5629\u304c\u8d77\u304d\u3084\u3059\u3044", fear: 0.82, fight: 1.82, social: 0.78, relation: 0.78, sleep: 0.90, zunda: 0.94 },
|
||||
calm: { label: "\u30de\u30a4\u30da\u30fc\u30b9", desc: "\u53cd\u5fdc\u304c\u7a4f\u3084\u304b\u3067\u5b89\u5b9a\u3057\u3084\u3059\u3044", fear: 0.82, fight: 0.58, social: 1.02, relation: 1.08, sleep: 1.02, zunda: 0.94 },
|
||||
timid: { label: "\u81c6\u75c5", desc: "\u9003\u3052\u3084\u3059\u304f\u3001\u55a7\u5629\u3092\u907f\u3051\u308b", fear: 1.42, fight: 0.48, social: 0.96, relation: 1.04, sleep: 1.00, zunda: 1.00, play: 0.78 },
|
||||
glutton: { label: "\u98df\u3044\u3057\u3093\u574a", desc: "\u305a\u3093\u3060\u9905\u3092\u512a\u5148\u3057\u3084\u3059\u3044", fear: 0.92, fight: 0.82, social: 1.00, relation: 1.00, sleep: 0.95, zunda: 1.42, play: 0.84 },
|
||||
sleepy: { label: "\u7720\u305f\u304c\u308a", desc: "\u65e9\u3081\u306b\u5bdd\u5e8a\u3078\u5411\u304b\u3046", fear: 0.96, fight: 0.72, social: 0.92, relation: 0.96, sleep: 1.34, zunda: 0.92, play: 0.68 },
|
||||
lonely: { label: "\u5bc2\u3057\u304c\u308a", desc: "\u4ed6\u306e\u305f\u308a\u306a\u3044\u3078\u5bc4\u308a\u3084\u3059\u3044", fear: 1.02, fight: 0.70, social: 1.34, relation: 1.38, sleep: 0.96, zunda: 1.00, play: 1.08 },
|
||||
irritable: { label: "\u6012\u308a\u3063\u307d\u3044", desc: "\u55a7\u5629\u304c\u8d77\u304d\u3084\u3059\u3044", fear: 0.82, fight: 1.82, social: 0.78, relation: 0.78, sleep: 0.90, zunda: 0.94, play: 0.82 },
|
||||
playful: { label: "\u904a\u3073\u597d\u304d", desc: "\u30dc\u30fc\u30eb\u3084\u52d5\u304f\u3082\u306e\u306b\u5f15\u304d\u5bc4\u305b\u3089\u308c\u3084\u3059\u3044", fear: 0.92, fight: 0.74, social: 1.10, relation: 1.04, sleep: 0.94, zunda: 0.96, play: 1.72 },
|
||||
calm: { label: "\u30de\u30a4\u30da\u30fc\u30b9", desc: "\u53cd\u5fdc\u304c\u7a4f\u3084\u304b\u3067\u5b89\u5b9a\u3057\u3084\u3059\u3044", fear: 0.82, fight: 0.58, social: 1.02, relation: 1.08, sleep: 1.02, zunda: 0.94, play: 0.86 },
|
||||
};
|
||||
const PERSONALITY_IDS = Object.keys(PERSONALITIES);
|
||||
|
||||
|
|
@ -199,6 +200,19 @@ class Effect {
|
|||
ctx.arc(Math.cos(a) * r, Math.sin(a) * r, randSeed(this.seed + i + 12, 2.2, 5.2), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
} else if (this.type === "zunchi_miasma") {
|
||||
const drift = 1 - alpha;
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
ctx.fillStyle = this.color || "rgba(16,78,28,0.62)";
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const a = this.seed + i * 1.41 + drift * 1.8;
|
||||
const px = Math.cos(a) * this.size * randSeed(this.seed + i, 0.08, 0.62);
|
||||
const py = Math.sin(a * 0.8) * this.size * randSeed(this.seed + i + 7, 0.05, 0.42) - drift * this.size * 0.42;
|
||||
ctx.globalAlpha = alpha * randSeed(this.seed + i + 14, 0.10, 0.28);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(px, py, this.size * randSeed(this.seed + i + 21, 0.22, 0.46), this.size * randSeed(this.seed + i + 35, 0.16, 0.38), a, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
} else if (this.type === "bubble") {
|
||||
const w = Math.max(30, 14 + this.text.length * 7);
|
||||
const h = 20;
|
||||
|
|
|
|||
718
js/tarinai.js
718
js/tarinai.js
File diff suppressed because it is too large
Load diff
351
js/ui.js
351
js/ui.js
|
|
@ -5,10 +5,12 @@ const ctx = canvas.getContext("2d");
|
|||
const ui = {
|
||||
pauseBtn: document.getElementById("pauseBtn"),
|
||||
speedBtn: document.getElementById("speedBtn"),
|
||||
ecologyBtn: document.getElementById("ecologyBtn"),
|
||||
soundBtn: document.getElementById("soundBtn"),
|
||||
saveBtn: document.getElementById("saveBtn"),
|
||||
loadBtn: document.getElementById("loadBtn"),
|
||||
creditsBtn: document.getElementById("creditsBtn"),
|
||||
resetBtn: document.getElementById("resetBtn"),
|
||||
creditsDialog: document.getElementById("creditsDialog"),
|
||||
creditsCloseBtn: document.getElementById("creditsCloseBtn"),
|
||||
toolPalette: document.getElementById("toolPalette"),
|
||||
toolCard: document.getElementById("toolCard"),
|
||||
colonyChartTabs: document.getElementById("colonyChartTabs"),
|
||||
|
|
@ -20,8 +22,14 @@ const ui = {
|
|||
colonyChart: document.getElementById("colonyChart"),
|
||||
colonyChartLegend: document.getElementById("colonyChartLegend"),
|
||||
archiveContent: document.getElementById("archiveContent"),
|
||||
archiveResetBtn: document.getElementById("archiveResetBtn"),
|
||||
archiveResetDialog: document.getElementById("archiveResetDialog"),
|
||||
archiveResetCancelBtn: document.getElementById("archiveResetCancelBtn"),
|
||||
archiveResetConfirmBtn: document.getElementById("archiveResetConfirmBtn"),
|
||||
fieldDialog: document.getElementById("fieldDialog"),
|
||||
fieldCancelBtn: document.getElementById("fieldCancelBtn"),
|
||||
ecologyDialog: document.getElementById("ecologyDialog"),
|
||||
ecologyCloseBtn: document.getElementById("ecologyCloseBtn"),
|
||||
};
|
||||
|
||||
const uiCache = {
|
||||
|
|
@ -31,6 +39,17 @@ const uiCache = {
|
|||
toolButtons: [],
|
||||
archiveVersion: "",
|
||||
archiveRows: [],
|
||||
archiveFamilyVersion: null,
|
||||
archiveRelationSignature: "",
|
||||
archiveHtmlCache: new Map(),
|
||||
archiveRenderTimer: 0,
|
||||
archiveRenderForce: false,
|
||||
archiveScheduled: false,
|
||||
archivePendingOffscreen: false,
|
||||
archivePendingWhileRunning: false,
|
||||
archiveLastRenderedAt: 0,
|
||||
archiveLastScheduleFamilyVersion: null,
|
||||
archiveDiagnostics: { renders: 0, lastMs: 0, lastFamilies: 0, lastNodes: 0, skippedWhileRunning: 0 },
|
||||
archiveScrollScheduled: false,
|
||||
deferArchiveUntil: 0,
|
||||
lastArchiveWindow: "",
|
||||
|
|
@ -47,6 +66,15 @@ const uiCache = {
|
|||
panLastX: 0,
|
||||
panLastY: 0,
|
||||
panMoved: false,
|
||||
grabbing: false,
|
||||
grabTarget: null,
|
||||
grabKind: "",
|
||||
grabMoved: false,
|
||||
grabStartX: 0,
|
||||
grabStartY: 0,
|
||||
grabLastWorldX: 0,
|
||||
grabLastWorldY: 0,
|
||||
grabLastSpatialAt: 0,
|
||||
};
|
||||
|
||||
function renderLog(logs) {
|
||||
|
|
@ -65,7 +93,7 @@ function renderLog(logs) {
|
|||
time.textContent = formatLogTime(entry.time || 0);
|
||||
div.appendChild(tag);
|
||||
div.appendChild(time);
|
||||
div.append(document.createTextNode(entry.text || ""));
|
||||
div.append(document.createTextNode(formatLogDisplayText(entry.text || "")));
|
||||
frag.appendChild(div);
|
||||
}
|
||||
ui.log.appendChild(frag);
|
||||
|
|
@ -120,14 +148,22 @@ function renderSelected() {
|
|||
fmt(t.age), fmt(t.lifeSpan), fmt(t.hunger), fmt(t.loneliness),
|
||||
fmt(t.energy), fmt(t.stress), fmt(t.mood),
|
||||
t.parentNames?.join("+") || "", t.children?.length || 0, fmt(t.lack),
|
||||
rel.friend?.id || "", rel.friend?.score || 0, rel.friend?.id ? tarinaiSpritePathForId(rel.friend.id) : "", rel.fear?.id || "", rel.fear?.score || 0, rel.fear?.id ? tarinaiSpritePathForId(rel.fear.id) : "", fightRecord
|
||||
rel.friend?.id || "", rel.friend?.score || 0, rel.friend?.id ? tarinaiSpritePathForId(rel.friend.id) : "", rel.fear?.id || "", rel.fear?.score || 0, rel.fear?.id ? tarinaiSpritePathForId(rel.fear.id) : "", fightRecord,
|
||||
t.isZunchiSlave ? 1 : 0, fmt(t.totalFightLosses || 0), fmt(t.loveMochiTimer || 0), fmt(t.fightMochiTimer || 0),
|
||||
t.zunchiDisease ? 1 : 0, fmt(t.zunchiDiseaseSeverity || 0), fmt(t.zunchiStain || 0)
|
||||
].join("|");
|
||||
if (uiCache.selectedSnapshot === snapshot) return;
|
||||
uiCache.selectedSnapshot = snapshot;
|
||||
|
||||
ui.selectedInfo.className = "selected-info";
|
||||
const effectBadges = [];
|
||||
effectBadges.push(`<span class="badge">\u6027\u683c ${escapeHtml(t.personalityLabel ? t.personalityLabel() : t.personality || "\u4e0d\u660e")}</span>`);
|
||||
if (t.isZunchiSlave) effectBadges.push(`<span class="badge">\u305a\u3093\u3061\u3069\u308c\u3044</span>`);
|
||||
if (t.zunchiDisease) effectBadges.push(`<span class="badge">\u305a\u3093\u3061\u75c5 ${fmt((t.zunchiDiseaseSeverity || 0) * 100)}</span>`);
|
||||
if ((t.loveMochiTimer || 0) > 0.1) effectBadges.push(`<span class="badge">\u3078\u3053\u9905 ${fmt(t.loveMochiTimer)}</span>`);
|
||||
if ((t.fightMochiTimer || 0) > 0.1) effectBadges.push(`<span class="badge">\u3051\u3093\u304b\u9905 ${fmt(t.fightMochiTimer)}</span>`);
|
||||
ui.selectedInfo.innerHTML = `
|
||||
<div><span class="badge">${escapeHtml(t.label())}</span><span class="badge">${escapeHtml(t.name)}</span>${(t.displayGeneration ? t.displayGeneration() : ((t.hasPaired || t.parents?.length || t.children?.length) ? t.generation : null)) ? `<span class="badge">\u4e16\u4ee3 ${t.generation}</span>` : ""}</div>
|
||||
<div><span class="badge">${escapeHtml(t.label())}</span><span class="badge">${escapeHtml(t.name)}</span>${(t.displayGeneration ? t.displayGeneration() : ((t.hasPaired || t.parents?.length || t.children?.length) ? t.generation : null)) ? `<span class="badge">\u4e16\u4ee3 ${t.generation}</span>` : ""}${effectBadges.join("")}</div>
|
||||
<div class="info-row"><span>\u6027\u683c</span><strong>${escapeHtml(t.personalityLabel ? t.personalityLabel() : t.personality || "\u4e0d\u660e")}</strong></div>
|
||||
<div class="info-row"><span>\u72b6\u614b</span><strong>${escapeHtml(stateLabel(t.state))}</strong></div>
|
||||
<div class="info-row"><span>\u7406\u7531</span><strong>${escapeHtml(reasonLabel(t))}</strong></div>
|
||||
|
|
@ -137,10 +173,12 @@ function renderSelected() {
|
|||
<div class="info-row"><span>\u5bc2\u3057\u3055</span><strong>${fmt(t.loneliness)}</strong></div>
|
||||
<div class="info-row"><span>\u4f53\u529b</span><strong>${fmt(t.energy)}</strong></div>
|
||||
<div class="info-row"><span>\u30b9\u30c8\u30ec\u30b9</span><strong>${fmt(t.stress)}</strong></div>
|
||||
<div class="info-row"><span>\u305a\u3093\u3061\u6c5a\u308c</span><strong>${fmt(t.zunchiStain || 0)}</strong></div>
|
||||
<div class="info-row"><span>\u6c17\u5206</span><strong>${fmt(t.mood)}</strong></div>
|
||||
<div class="info-row relation-row"><span>\u4ef2\u826f\u3057</span><strong>${rel.friend ? `${relationIconHtml(rel.friend.id)}${escapeHtml(`${rel.friend.name} +${fmt(rel.friend.score)}`)}` : "\u306a\u3057"}</strong></div>
|
||||
<div class="info-row relation-row"><span>\u907f\u3051\u308b\u76f8\u624b</span><strong>${rel.fear ? `${relationIconHtml(rel.fear.id)}${escapeHtml(`${rel.fear.name} ${fmt(rel.fear.score)}`)}` : "\u306a\u3057"}</strong></div>
|
||||
<div class="info-row"><span>\u55a7\u5629\u8a18\u61b6</span><strong>${escapeHtml(fightRecord)}</strong></div>
|
||||
<div class="info-row"><span>\u7d2f\u8a08\u6557\u5317</span><strong>${t.totalFightLosses || 0}</strong></div>
|
||||
<div class="info-row"><span>\u89aa</span><strong>${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "\u4e0d\u660e")}</strong></div>
|
||||
<div class="info-row"><span>\u5b50</span><strong>${t.children?.length || 0}</strong></div>
|
||||
<div class="info-row"><span>\u305f\u308a\u306a\u3044</span><strong>${fmt(t.lack)}</strong></div>
|
||||
|
|
@ -151,7 +189,11 @@ function stateLabel(s) {
|
|||
return {
|
||||
idle: "\u5f85\u6a5f",
|
||||
seek_food: "\u98df\u3079\u7269\u3078\u5411\u304b\u3046",
|
||||
seek_bed: "\u5e72\u8349\u5bdd\u5e8a\u3078\u5411\u304b\u3046",
|
||||
seek_bed: "\u5bdd\u5e8a\u3078\u5411\u304b\u3046",
|
||||
play_ball: "\u30dc\u30fc\u30eb\u904a\u3073",
|
||||
seek_water: "\u6c34\u3092\u63a2\u3059",
|
||||
zunchi_sick: "\u305a\u3093\u3061\u75c5",
|
||||
system: "\u305a\u3093\u3061\u75c5",
|
||||
seek_friend: "\u4ef2\u9593\u3092\u63a2\u3059",
|
||||
follow_parent: "\u89aa\u306b\u3064\u3044\u3066\u3044\u304f",
|
||||
cursor_friend: "\u30ab\u30fc\u30bd\u30eb\u306b\u3064\u3044\u3066\u3044\u304f",
|
||||
|
|
@ -174,13 +216,15 @@ function reasonLabel(t) {
|
|||
if (t.state === "intimidate") return "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "birth_ritual") return "\u5b50\u304c\u73fe\u308c\u308b\u524d\u306b\u4f53\u3092\u3086\u3059\u3063\u3066\u3044\u308b";
|
||||
if (t.state === "seek_food") return "\u304a\u8179\u304c\u3059\u3044\u3066\u3044\u308b";
|
||||
if (t.state === "seek_water") return "\u305a\u3093\u3061\u75c5\u3067\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "zunchi_sick" || t.state === "system") return "\u305a\u3093\u3061\u75c5";
|
||||
return "\u5468\u56f2\u3092\u898b\u3066\u3044\u308b";
|
||||
}
|
||||
|
||||
function targetLabel(target) {
|
||||
if (!target) return "\u306a\u3057";
|
||||
if (target.name) return target.name;
|
||||
if (target.type) return { sweet: "\u305a\u3093\u3060\u9905", food: "\u98df\u4e8b", grass: "\u8349", water: "\u6c34", bed: "\u5e72\u8349\u5bdd\u5e8a", stone: "\u77f3", zunchi: "\u305a\u3093\u3061", trace: "\u8db3\u8de1", splat: "\u3057\u3076\u304d" }[target.type] || target.type;
|
||||
if (target.type) return { sweet: "\u305a\u3093\u3060\u9905", love_mochi: "\u3078\u3053\u9905", fight_mochi: "\u3051\u3093\u304b\u9905", food: "\u98df\u4e8b", grass: "\u8349", water: "\u6c34", bed: "\u5e72\u8349\u5bdd\u5e8a", ball: "\u30dc\u30fc\u30eb", stone: "\u77f3", zunchi: "\u305a\u3093\u3061", system: "\u305a\u3093\u3061\u75c5", zunchi_sick: "\u305a\u3093\u3061\u75c5", trace: "\u8db3\u8de1", splat: "\u3057\u3076\u304d" }[target.type] || target.type;
|
||||
if (Number.isFinite(target.x) && Number.isFinite(target.y)) return "\u30ab\u30fc\u30bd\u30eb";
|
||||
return "\u4e0d\u660e";
|
||||
}
|
||||
|
|
@ -218,20 +262,41 @@ function escapeHtml(value) {
|
|||
"'": "'"
|
||||
}[c]));
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
|
||||
function formatLogDisplayText(text) {
|
||||
let out = String(text ?? "");
|
||||
const names = new Set();
|
||||
for (const t of world?.tarinai || []) if (t?.name) names.add(t.name);
|
||||
for (const n of Object.values(world?.family || {})) if (n?.name) names.add(n.name);
|
||||
const sorted = [...names].filter(Boolean).sort((a, b) => b.length - a.length);
|
||||
for (const name of sorted) {
|
||||
const re = new RegExp(`(^|[^\u300c])(${escapeRegExp(name)})(?=[\u3068\u306f\u304c\u3092\u306b\u306e\u3082\u3078\u304b\u3089\u3067\u3001\u3002]|$)`, "g");
|
||||
out = out.replace(re, (m, prefix, found) => `${prefix}\u300c${found}\u300d`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function resizeCanvas() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const perf = world?.performanceLevel ? world.performanceLevel() : 0;
|
||||
const maxDpr = perf >= 2 ? 1 : (perf === 1 ? 1.5 : 2);
|
||||
const dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, maxDpr));
|
||||
const nextW = Math.floor(rect.width * dpr);
|
||||
const nextH = Math.floor(rect.height * dpr);
|
||||
const nextW = Math.max(1, Math.floor(rect.width * dpr));
|
||||
const nextH = Math.max(1, Math.floor(rect.height * dpr));
|
||||
if (canvas.width === nextW && canvas.height === nextH && uiCache.canvasDpr === dpr) return;
|
||||
uiCache.canvasDpr = dpr;
|
||||
canvas.width = nextW;
|
||||
canvas.height = nextH;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
world.setViewportSize(rect.width, rect.height);
|
||||
ctx.fillStyle = "#cfe3d3";
|
||||
ctx.fillRect(0, 0, Math.max(1, rect.width), Math.max(1, rect.height));
|
||||
world.setViewportSize(Math.max(1, rect.width), Math.max(1, rect.height));
|
||||
uiCache.lastChartDraw = "";
|
||||
if (typeof render === "function") render();
|
||||
}
|
||||
|
||||
function applyFieldLayout(fieldType = world.fieldType || "garden") {
|
||||
|
|
@ -255,6 +320,30 @@ function closeFieldDialog() {
|
|||
ui.fieldDialog?.classList.add("hidden");
|
||||
}
|
||||
|
||||
function hydrateLazyImages(root) {
|
||||
const scope = root || document;
|
||||
for (const img of scope.querySelectorAll("img[data-src]")) {
|
||||
if (!img.getAttribute("src")) img.setAttribute("src", img.dataset.src || "");
|
||||
}
|
||||
}
|
||||
|
||||
function openEcologyDialog() {
|
||||
hydrateLazyImages(ui.ecologyDialog);
|
||||
ui.ecologyDialog?.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function closeEcologyDialog() {
|
||||
ui.ecologyDialog?.classList.add("hidden");
|
||||
}
|
||||
|
||||
function openArchiveResetDialog() {
|
||||
ui.archiveResetDialog?.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function closeArchiveResetDialog() {
|
||||
ui.archiveResetDialog?.classList.add("hidden");
|
||||
}
|
||||
|
||||
function resetWithField(fieldType) {
|
||||
const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden;
|
||||
closeFieldDialog();
|
||||
|
|
@ -282,7 +371,7 @@ function resizeFieldFromWheel(e) {
|
|||
const sx = e.clientX - rect.left;
|
||||
const sy = e.clientY - rect.top;
|
||||
const focus = world.screenToWorld ? world.screenToWorld(sx, sy) : { x: sx, y: sy };
|
||||
const factor = e.deltaY > 0 ? 1.08 : 1 / 1.08;
|
||||
const factor = e.deltaY > 0 ? 1 / 1.08 : 1.08;
|
||||
if (!world.setFieldZoom((world.fieldZoom || 1) * factor)) return;
|
||||
const off = world.fieldScreenOffset ? world.fieldScreenOffset() : { x: 0, y: 0 };
|
||||
const scale = world.viewScale ? world.viewScale() : 1;
|
||||
|
|
@ -298,7 +387,7 @@ function selectTool(tool) {
|
|||
btn.classList.toggle("selected", btn.dataset.tool === tool);
|
||||
}
|
||||
syncToolSizeBadges();
|
||||
canvas.style.cursor = tool === "observe" ? "default" : (tool === "poke" || tool === "delete" ? "pointer" : "crosshair");
|
||||
canvas.style.cursor = tool === "observe" ? "default" : (tool === "pinch" ? "grab" : (tool === "poke" || tool === "delete" ? "pointer" : "crosshair"));
|
||||
}
|
||||
|
||||
function showToast(text) {
|
||||
|
|
@ -308,33 +397,12 @@ function showToast(text) {
|
|||
showToast._timer = setTimeout(() => ui.toast.classList.add("hidden"), 1400);
|
||||
}
|
||||
|
||||
function saveGame() {
|
||||
localStorage.setItem(CONFIG.saveKey, JSON.stringify(world.serialize()));
|
||||
showToast("\u4fdd\u5b58\u3057\u307e\u3057\u305f\u3002");
|
||||
function openCreditsDialog() {
|
||||
ui.creditsDialog?.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function loadGame() {
|
||||
const raw = localStorage.getItem(CONFIG.saveKey);
|
||||
if (!raw) { showToast("\u30bb\u30fc\u30d6\u30c7\u30fc\u30bf\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); return; }
|
||||
try {
|
||||
const t0 = performance.now();
|
||||
world.load(JSON.parse(raw));
|
||||
applyFieldLayout(world.fieldType || "garden");
|
||||
uiCache.archiveVersion = "";
|
||||
uiCache.deferArchiveUntil = performance.now() + 80;
|
||||
if (ui.archiveContent) ui.archiveContent.innerHTML = `<div class="selected-info empty">\u5bb6\u7cfb\u56f3\u3092\u8aad\u307f\u8fbc\u3093\u3067\u3044\u307e\u3059\u3002</div>`;
|
||||
renderSelected();
|
||||
renderStats();
|
||||
requestAnimationFrame(() => {
|
||||
uiCache.deferArchiveUntil = 0;
|
||||
renderArchive();
|
||||
});
|
||||
if (typeof console !== "undefined") console.debug?.(`loaded save in ${Math.round(performance.now() - t0)}ms`);
|
||||
showToast("\u8aad\u307f\u8fbc\u307f\u307e\u3057\u305f\u3002");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast("\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002");
|
||||
}
|
||||
function closeCreditsDialog() {
|
||||
ui.creditsDialog?.classList.add("hidden");
|
||||
}
|
||||
|
||||
function syncTopButtons() {
|
||||
|
|
@ -343,7 +411,7 @@ function syncTopButtons() {
|
|||
if (ui.soundBtn) ui.soundBtn.textContent = audio.enabled ? "\u97f3 ON" : "\u97f3 OFF";
|
||||
}
|
||||
|
||||
const SCALABLE_TOOLS = new Set(["zunchi", "sweet", "grass", "stone", "firecracker", "fence_v", "fence_h"]);
|
||||
const SCALABLE_TOOLS = new Set(["zunchi", "sweet", "love_mochi", "fight_mochi", "grass", "stone", "firecracker", "fence_v", "fence_h"]);
|
||||
const SIZE_ORDER = ["small", "medium", "large"];
|
||||
const SIZE_LABELS = { small: "\u5c0f", medium: "\u4e2d", large: "\u5927" };
|
||||
|
||||
|
|
@ -400,6 +468,22 @@ function bindUI() {
|
|||
});
|
||||
syncToolSizeBadges();
|
||||
ui.archiveContent?.addEventListener("scroll", scheduleArchiveWindowRender, { passive: true });
|
||||
window.addEventListener("scroll", schedulePendingArchiveRender, { passive: true });
|
||||
window.addEventListener("resize", schedulePendingArchiveRender, { passive: true });
|
||||
const bindArchiveHorizontal = (root) => {
|
||||
if (!root) return;
|
||||
root.addEventListener("wheel", (e) => {
|
||||
const scroller = e.target.closest(".lineage-family-tree, .lineage-main-content");
|
||||
if (!scroller) return;
|
||||
const canX = scroller.scrollWidth > scroller.clientWidth + 4;
|
||||
if (!canX) return;
|
||||
const dominant = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : (e.shiftKey ? e.deltaY : 0);
|
||||
if (!dominant) return;
|
||||
e.preventDefault();
|
||||
scroller.scrollLeft += dominant;
|
||||
}, { passive: false });
|
||||
};
|
||||
bindArchiveHorizontal(ui.archiveContent);
|
||||
ui.archiveContent?.addEventListener("click", (e) => {
|
||||
const node = e.target.closest("[data-family-tarinai-id]");
|
||||
if (!node) return;
|
||||
|
|
@ -412,24 +496,33 @@ function bindUI() {
|
|||
showToast(`${t.name}\u3092\u9078\u629e\u3057\u307e\u3057\u305f\u3002`);
|
||||
});
|
||||
|
||||
ui.pauseBtn.addEventListener("click", () => {
|
||||
ui.pauseBtn?.addEventListener("click", () => {
|
||||
world.paused = !world.paused;
|
||||
const archiveStale = uiCache.archiveFamilyVersion !== (world.familyVersion || 0);
|
||||
if (world.paused && (uiCache.archivePendingWhileRunning || archiveStale)) {
|
||||
uiCache.archivePendingWhileRunning = false;
|
||||
scheduleArchiveWindowRender();
|
||||
}
|
||||
syncTopButtons();
|
||||
});
|
||||
ui.speedBtn.addEventListener("click", () => {
|
||||
ui.speedBtn?.addEventListener("click", () => {
|
||||
const speeds = [1, 2, 4];
|
||||
const i = speeds.indexOf(world.speed);
|
||||
world.speed = speeds[(i + 1) % speeds.length];
|
||||
syncTopButtons();
|
||||
});
|
||||
ui.ecologyBtn?.addEventListener("click", openEcologyDialog);
|
||||
ui.soundBtn?.addEventListener("click", () => {
|
||||
const on = audio.toggle();
|
||||
ui.soundBtn.textContent = on ? "\u97f3 ON" : "\u97f3 OFF";
|
||||
showToast(on ? "\u97f3\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f\u3002" : "\u97f3\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f\u3002");
|
||||
});
|
||||
ui.saveBtn.addEventListener("click", saveGame);
|
||||
ui.loadBtn.addEventListener("click", loadGame);
|
||||
ui.resetBtn.addEventListener("click", () => {
|
||||
ui.creditsBtn?.addEventListener("click", openCreditsDialog);
|
||||
ui.creditsCloseBtn?.addEventListener("click", closeCreditsDialog);
|
||||
ui.creditsDialog?.addEventListener("click", (e) => {
|
||||
if (e.target === ui.creditsDialog) closeCreditsDialog();
|
||||
});
|
||||
ui.resetBtn?.addEventListener("click", () => {
|
||||
openFieldDialog();
|
||||
});
|
||||
ui.fieldCancelBtn?.addEventListener("click", closeFieldDialog);
|
||||
|
|
@ -439,8 +532,25 @@ function bindUI() {
|
|||
if (!btn) return;
|
||||
resetWithField(btn.dataset.fieldType || "garden");
|
||||
});
|
||||
ui.ecologyCloseBtn?.addEventListener("click", closeEcologyDialog);
|
||||
ui.ecologyDialog?.addEventListener("click", (e) => {
|
||||
if (e.target === ui.ecologyDialog) closeEcologyDialog();
|
||||
});
|
||||
ui.archiveResetBtn?.addEventListener("click", openArchiveResetDialog);
|
||||
ui.archiveResetCancelBtn?.addEventListener("click", closeArchiveResetDialog);
|
||||
ui.archiveResetDialog?.addEventListener("click", (e) => {
|
||||
if (e.target === ui.archiveResetDialog) closeArchiveResetDialog();
|
||||
});
|
||||
ui.archiveResetConfirmBtn?.addEventListener("click", () => {
|
||||
world.resetFamilyTree?.();
|
||||
uiCache.archiveVersion = "";
|
||||
closeArchiveResetDialog();
|
||||
renderArchive();
|
||||
renderSelected();
|
||||
showToast("\u5bb6\u7cfb\u56f3\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u307e\u3057\u305f\u3002");
|
||||
});
|
||||
|
||||
ui.toolPalette.addEventListener("click", (e) => {
|
||||
ui.toolPalette?.addEventListener("click", (e) => {
|
||||
const toggle = e.target.closest(".tool-category-toggle");
|
||||
if (toggle) {
|
||||
const category = toggle.closest(".tool-category");
|
||||
|
|
@ -468,41 +578,154 @@ function bindUI() {
|
|||
}
|
||||
}, { passive: true });
|
||||
};
|
||||
const containOwnVerticalScroll = (el) => {
|
||||
if (!el) return;
|
||||
el.addEventListener("wheel", (e) => {
|
||||
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
||||
const canScroll = el.scrollHeight > el.clientHeight + 1;
|
||||
if (!canScroll) return;
|
||||
const atTop = el.scrollTop <= 0;
|
||||
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
|
||||
const movingInside = (e.deltaY < 0 && !atTop) || (e.deltaY > 0 && !atBottom);
|
||||
if (!movingInside) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
el.scrollTop += e.deltaY;
|
||||
}, { passive: false });
|
||||
};
|
||||
chainScroll(document.querySelector(".panel"));
|
||||
chainScroll(ui.archiveContent);
|
||||
chainScroll(ui.log);
|
||||
containOwnVerticalScroll(ui.log);
|
||||
|
||||
canvas.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
canvas.addEventListener("mousedown", (e) => {
|
||||
canvas?.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
canvas?.addEventListener("mousedown", (e) => {
|
||||
if (e.button === 0 && world.tool === "pinch") {
|
||||
const p = screenToWorld(e);
|
||||
if (!p.inside) return;
|
||||
const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null;
|
||||
if (!target) {
|
||||
showToast("\u3064\u307e\u3081\u308b\u5bfe\u8c61\u304c\u3042\u308a\u307e\u305b\u3093\u3002");
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
uiCache.grabbing = true;
|
||||
uiCache.grabMoved = false;
|
||||
uiCache.grabTarget = target;
|
||||
uiCache.grabKind = target instanceof Tarinai ? "tarinai" : "item";
|
||||
uiCache.grabLastWorldX = p.x;
|
||||
uiCache.grabLastWorldY = p.y;
|
||||
uiCache.grabStartX = e.clientX;
|
||||
uiCache.grabStartY = e.clientY;
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
world.selected = target;
|
||||
target.sleeping = false;
|
||||
if (target.state === "sleep" || target.state === "seek_bed") target.state = "idle";
|
||||
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18);
|
||||
} else {
|
||||
target.dropTimer = 0;
|
||||
target.dropMax = 0;
|
||||
target.dropImpactDone = true;
|
||||
}
|
||||
canvas.style.cursor = "grabbing";
|
||||
renderStats();
|
||||
return;
|
||||
}
|
||||
if (e.button !== 2) return;
|
||||
e.preventDefault();
|
||||
uiCache.panning = true;
|
||||
uiCache.panMoved = false;
|
||||
uiCache.panButton = e.button;
|
||||
uiCache.panStartX = e.clientX;
|
||||
uiCache.panStartY = e.clientY;
|
||||
uiCache.panLastX = e.clientX;
|
||||
uiCache.panLastY = e.clientY;
|
||||
canvas.style.cursor = "grabbing";
|
||||
});
|
||||
window.addEventListener("mouseup", (e) => {
|
||||
if (e.button !== 2 && !uiCache.panning) return;
|
||||
if (uiCache.panning) {
|
||||
uiCache.panning = false;
|
||||
canvas.style.cursor = world.tool === "observe" ? "default" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair");
|
||||
if (uiCache.grabbing) {
|
||||
const target = uiCache.grabTarget;
|
||||
if (target) {
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
target.target = null;
|
||||
target.targetKey = "";
|
||||
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15);
|
||||
} else if (target.type === "ball") {
|
||||
target.prevX = target.x;
|
||||
target.prevY = target.y;
|
||||
}
|
||||
}
|
||||
uiCache.grabbing = false;
|
||||
uiCache.grabTarget = null;
|
||||
uiCache.grabKind = "";
|
||||
world.rebuildSpatial?.();
|
||||
uiCache.grabLastSpatialAt = 0;
|
||||
canvas.style.cursor = world.tool === "pinch" ? "grab" : (world.tool === "observe" ? "default" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair"));
|
||||
renderStats();
|
||||
return;
|
||||
}
|
||||
if (!uiCache.panning) return;
|
||||
if (uiCache.panButton !== null && e.button !== uiCache.panButton) return;
|
||||
uiCache.panning = false;
|
||||
uiCache.panButton = null;
|
||||
canvas.style.cursor = world.tool === "observe" ? "default" : (world.tool === "pinch" ? "grab" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair"));
|
||||
});
|
||||
window.addEventListener("mousemove", (e) => {
|
||||
if (uiCache.grabbing) {
|
||||
const target = uiCache.grabTarget;
|
||||
if (!target || target.dead) {
|
||||
uiCache.grabbing = false;
|
||||
uiCache.grabTarget = null;
|
||||
canvas.style.cursor = world.tool === "pinch" ? "grab" : "default";
|
||||
return;
|
||||
}
|
||||
const p = screenToWorld(e);
|
||||
const dx = p.x - (uiCache.grabLastWorldX ?? p.x);
|
||||
const dy = p.y - (uiCache.grabLastWorldY ?? p.y);
|
||||
uiCache.grabLastWorldX = p.x;
|
||||
uiCache.grabLastWorldY = p.y;
|
||||
const totalMove = Math.hypot(e.clientX - (uiCache.grabStartX || e.clientX), e.clientY - (uiCache.grabStartY || e.clientY));
|
||||
if (totalMove > 3) uiCache.grabMoved = true;
|
||||
const pad = target.radius || target.r || 16;
|
||||
target.x = clamp(p.x, CONFIG.worldPadding + pad * 0.15, world.w - CONFIG.worldPadding - pad * 0.15);
|
||||
target.y = clamp(p.y, CONFIG.worldPadding + pad * 0.15, world.h - CONFIG.worldPadding - pad * 0.15);
|
||||
target.vx = clamp(dx * 16, -160, 160);
|
||||
target.vy = clamp(dy * 16, -160, 160);
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
target.state = "idle";
|
||||
target.target = null;
|
||||
target.sleeping = false;
|
||||
} else if (target.type === "ball") {
|
||||
target.prevX = target.x - dx;
|
||||
target.prevY = target.y - dy;
|
||||
target.spinVelocity = clamp((target.spinVelocity || 0) + dx * 0.06, -38, 38);
|
||||
}
|
||||
world.drawListDirty = true;
|
||||
const now = performance.now();
|
||||
if (!uiCache.grabLastSpatialAt || now - uiCache.grabLastSpatialAt > 80) {
|
||||
world.rebuildSpatial?.();
|
||||
uiCache.grabLastSpatialAt = now;
|
||||
}
|
||||
canvas.style.cursor = "grabbing";
|
||||
return;
|
||||
}
|
||||
if (!uiCache.panning) return;
|
||||
const dx = e.clientX - uiCache.panLastX;
|
||||
const dy = e.clientY - uiCache.panLastY;
|
||||
uiCache.panLastX = e.clientX;
|
||||
uiCache.panLastY = e.clientY;
|
||||
if (Math.abs(dx) + Math.abs(dy) > 0.5) {
|
||||
const totalMove = Math.hypot(e.clientX - uiCache.panStartX, e.clientY - uiCache.panStartY);
|
||||
if (totalMove > 8) {
|
||||
canvas.style.cursor = "grabbing";
|
||||
world.panCamera?.(-dx, -dy);
|
||||
uiCache.panMoved = true;
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener("click", (e) => {
|
||||
canvas?.addEventListener("click", (e) => {
|
||||
if (uiCache.grabMoved) {
|
||||
uiCache.grabMoved = false;
|
||||
return;
|
||||
}
|
||||
if (uiCache.panMoved) {
|
||||
uiCache.panMoved = false;
|
||||
return;
|
||||
|
|
@ -513,15 +736,29 @@ function bindUI() {
|
|||
renderStats();
|
||||
});
|
||||
|
||||
canvas.addEventListener("mousemove", (e) => {
|
||||
canvas?.addEventListener("mousemove", (e) => {
|
||||
const p = screenToWorld(e);
|
||||
const prevX = world.pointer.x;
|
||||
const prevY = world.pointer.y;
|
||||
world.pointer.x = p.x;
|
||||
world.pointer.y = p.y;
|
||||
world.pointer.inside = !!p.inside;
|
||||
const delta = Math.hypot((p.x || 0) - (prevX || 0), (p.y || 0) - (prevY || 0));
|
||||
world.pointer.motion = clamp(delta / 18, 0, 2);
|
||||
world.pointer.movedAt = performance.now();
|
||||
});
|
||||
canvas.addEventListener("wheel", resizeFieldFromWheel, { passive: false });
|
||||
canvas.addEventListener("mouseleave", () => {
|
||||
canvas?.addEventListener("wheel", resizeFieldFromWheel, { passive: false });
|
||||
canvas?.addEventListener("mouseleave", () => {
|
||||
world.pointer.inside = false;
|
||||
world.pointer.motion = 0;
|
||||
});
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Escape") return;
|
||||
closeFieldDialog();
|
||||
closeEcologyDialog();
|
||||
closeArchiveResetDialog();
|
||||
closeCreditsDialog();
|
||||
});
|
||||
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"use strict";
|
||||
"use strict";
|
||||
|
||||
function renderStats() {
|
||||
const alive = world.tarinai;
|
||||
|
|
@ -18,7 +18,11 @@ function renderStats() {
|
|||
updateColonyHistory(values);
|
||||
drawColonyChart(values);
|
||||
renderSelected();
|
||||
renderArchive();
|
||||
const familyVersion = world.familyVersion || 0;
|
||||
if (uiCache.archiveLastScheduleFamilyVersion !== familyVersion) {
|
||||
uiCache.archiveLastScheduleFamilyVersion = familyVersion;
|
||||
scheduleArchiveWindowRender();
|
||||
}
|
||||
}
|
||||
|
||||
function updateColonyHistory(values) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
"use strict";
|
||||
"use strict";
|
||||
|
||||
window.TarinaiUI = { canvas, ctx, ui, renderLog, renderSelected, stateLabel, renderStats, escapeHtml, renderArchive, validateFamilyTree, resizeCanvas, applyFieldLayout, screenToWorld, selectTool, showToast, saveGame, loadGame, syncTopButtons, bindUI };
|
||||
|
|
|
|||
1253
js/ui_family.js
1253
js/ui_family.js
File diff suppressed because it is too large
Load diff
643
js/world.js
643
js/world.js
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue