This commit is contained in:
33333-33333 2026-06-21 16:26:12 +09:00
commit 0b9ff1363b
45 changed files with 680 additions and 890 deletions

View file

@ -1,148 +1,19 @@
## 15.15.0
# Patch notes
- 巨大ファイル分割を実施。
- `world.js` をコアクラス、視点/家系/戦闘/環境/空間管理/アリ/更新/配置ログ/ブートストラップに分割。
- `tarinai.js` をコアクラス、人格・関係、病気・巣箱、欲求・アイテム、社会/移動/寿命、描画に分割。
- `ui.js` を状態、ログ、選択詳細、レイアウト/ダイアログ、道具、イベント束縛に分割。
- `ui_family.js` をデータ、非同期構築、レイアウト、パス生成、描画に分割。
- 既存グローバルAPI互換を維持し、挙動変更を避ける形で分割。
- Service Workerキャッシュと静的アセットバージョンを `15.15.0` に更新。
## 15.20.3
- Removed item-effect speech bubbles: `ドン`, `ぐるる`, `すっきり`, `Hg xN`, `ちまっ`, `でかっ`, `へにゃ`, `ムキッ`, and `mm...`.
- Reorganized the tool palette into color-coded collapsible groups: operation, food/water/grass, medicine/effects, habitat/living things, hazards/obstacles.
- Changed the tool UI ball icon to a soccer-ball emoji preview.
- Changed time phases: morning 05:00, day 10:00, evening 17:00, night 20:00.
- Increased genkotsu knockback for Tarinai and ants; genkotsu now also blasts zunchi away.
- Genkotsu damage now adds fear/stress and decreases Tarinai openness.
- Rain weather overlay is now diagonal streaks. Fallen rain-water items, zunda juice, and mercury remain horizontal oval item sprites.
- Tightened mobile operation UI visibility to smartphone-class compact touch devices.
- Sleep-disease Tarinai can be moved by ant hauling without snapping back to their sleep anchor.
## 15.9.1 修正
- 草を地面レイヤーとして扱い、既存の草の上にもオブジェクトを配置できるようにしました。草同士・草と障害物の植え付け制限は維持しています。
- けんか餅のゲーム内描画から星印を削除し、×印のみ表示するようにしました。
- ダメージ後に出る「こわい…」吹き出しを削除しました。
- 家系図に「更新」トグルを追加しました。デフォルトはOFFで、ONの間だけ家系図を描画・更新します。
- 死亡した個体のライブIDを再利用しないようにし、友達・敵対関係が別個体へ引き継がれて見える問題を防止しました。死亡時の一時ターゲット参照も掃除します。
- たりないデータUIをカテゴリ別の折り畳み表示に変更しました。
## 15.20.2 and earlier summary
# Patch Notes
## Encoding cleanup
- Normalized `index.html` back to UTF-8.
- Replaced mojibake strings with the intended labels.
- Escaped Japanese text in source files to avoid encoding-dependent corruption.
## Personality specification fixes
- Personality tags and data UI now use Japanese labels from the four-axis personality model.
- Threshold-crossing personality logs are now generated in Japanese and only when visible trait bands change.
- Daily personality caps now track increase and decrease separately per parameter.
- Current personality effects are neutral inside the -0.5 to +0.5 band.
- Direct conversion to the legacy timid personality after damage was removed; cowardly behavior is derived from high neuroticism plus low aggression.
- Corpse/splat contact now increases stress and slowly reduces neuroticism through habituation.
## Follow-up fixes
- Fixed CSS emoji escapes for tool UI icons. CSS now uses code-point escapes such as `\\1F448` instead of invalid surrogate escapes.
- Hid the detailed four-axis personality rows from the compact tarinai data UI.
- Restored `bestBedFor()` fallback so tarinai can choose hay beds when no usable nest box is available.
- Damage now wakes sleeping tarinai and forces tarinai hidden inside a nest box to leave the box.
- Added a short minimum stay window after entering a nest box to avoid immediate exit loops.
- Renamed the friend relation label from `\\u4ef2\\u826f\\u3057` to `\\u53cb\\u9054` in UI text and related logs.
## Personality cleanup v3
- Current live friend count now changes sociability drift. Family/friend proximity raises sociability more when the tarinai has more current friends, while a lonely tarinai with no current friends can slowly lose sociability.
- Removed the legacy named personality route from constructor, serialization, child inheritance, selected data UI, selected-card overlay, and family-tree titles. The game now uses only birthPersonality/currentPersonality four-axis data.
- Kept empty legacy stubs only for old-save compatibility so older saves cannot revive the pre-four-axis personality model.
## Follow-up fixes v4
- Restored compact personality information in the tarinai data panel: visible tags, birth personality, and current personality.
- Moved the focused-tarinai highlight outside the sprite rotation transform so the ring and label stay upright.
- Shortened ecology cards and added the HP/stress meter placement note: upper meter is HP, lower meter is stress.
- Raised the friendship threshold to 16 affinity and reused the same threshold for friend count, friend behavior, friend UI, and observe highlights.
- Observe mode now highlights friends of the selected tarinai as well as parents, children, and enemies.
## Ant nest update
- Added a new placeable `ant_nest` object and tool button.
- Added ant sprites cropped from the provided source image: queen, worker variants, and upside-down worker corpse.
- Each ant nest starts with 6 workers, caps at 10 workers, and sends at most 3 workers outside at once.
- Worker ants spawn above their home nest, random-walk, search for tarinai, and drag found tarinai back to the nest. Multiple workers dragging the same tarinai increase carrying speed.
- Dragged tarinai struggle automatically and can kill workers. Dead workers become edible `ant_corpse` items and slightly reduce hygiene.
- If a nest below capacity brings back a tarinai, its worker count increases by 1.
- If a full nest brings back another tarinai, a queen ant appears and creates a new ant nest in an open spot.
- Ant health does not recover.
- Added the requested Irasutoya credit entry.
## Ant nest follow-up v5
- Increased ant movement and hauling constants by 1.5x from the previous slow tuning.
- Kept queen spawning to at most one queen per nest per in-game day, while making spawned queens keep retrying until a new ant nest is built somewhere.
- Reduced the ant nest object radius from 34 to 17.
- Removed the separate ant corpse sprite asset. Ant corpses now render by flipping the worker ant sprite vertically and remain stationary while decaying.
- Added shared ant damage handling for firecracker explosions and dropped-object impacts.
- Kept ant worker HP in the nest worker pool so damaged workers remain damaged after returning to and leaving the nest.
## 15.11.0
- イベントUI下部にログ種別別のプッシュ通知設定を追加。ON時は左下最前面に通知表示
- 巣がなくなったアリは空きのある別巣へ所属変更。空き巣がない場合は死亡せず彷徨う
- 女王アリを大きめに表示
- 画像アセットをWebP化し、参照をWebPへ切り替え
- Service Workerによる静的アセットキャッシュを追加し、indexのno-cache指定を解除
## 15.12.0
- 優先度S/A/B相当の効果音イベントを追加。未指定音源はWeb Audioの仮SEで再生。
- 効果音カテゴリを「たりないの声」「通知」「操作」に分類し、イベントUI下部から個別ON/OFF可能にした。
- 音素材差し替え用の安定IDを `docs/SOUND_IDS.md` に整理。
- げんこつ、爆竹、喧嘩、ダメージ、死亡、誕生、アリ、病気、回復、水、草、zunchi、UI操作などに音フックを追加。
## 15.12.3
- プッシュ通知の保存キーを更新し、初期状態はOFFに戻した。
- 効果音設定は右上の音設定で、操作音・通知音のみデフォルトON、たりないの声はデフォルトOFFに変更。
- 操作音と通知音の内蔵仮SE音量を大幅に増加。
- つまむ操作でたりないデータカードが開く挙動を修正。
- 道具から配置する全オブジェクトで配置音が鳴るよう、placeItem側に配置音を統合。
- イベントUI折り畳み時の高さを圧縮。
## 15.12.4
- 効果音の仮SE音量を再度引き上げ。
- イベントUI内の効果音設定を廃止し、右上の音設定メニューへ統一。
- イベントログ欄が上下端に達した時、右メニュー全体へスクロールを受け渡すよう修正。
## 15.15.0
- バージョン/キャッシュ管理を `js/version.js``service-worker.js` に統一。
- Service Worker登録を `updateViaCache: "none"` に変更し、`.htaccess` でService Worker本体をno-cache化。
- `js/event_bus.js` を追加し、ログ/フェーズ/配置/地形dirty/FPS変化などをイベント化。
- `js/performance_manager.js` を追加し、FPSレベル、アリ更新間引き、エフェクト上限、統計更新間隔、地形再描画間隔を中央管理。
- 地面キャッシュを全走査ハッシュ方式から `terrainVersion` dirty方式へ変更。
- SpatialGridをアリ/障害物/食料/危険物セルに拡張。
- `?debug=1` の内部デバッグオーバーレイを追加。
- 音システムに音ID別ゲイン、音ID別最小間隔、同時発音上限、サンプル読み込み失敗イベントを追加。
## v15.15.0
- 個体パラメータ変化アイテムを追加: 下剤、プロテイン、ンイテロプ、弾薬、怪しい薬。
- 下剤: 効果時間中3秒ごとにずんちを放出し、食事で空腹度が下がらない。
- プロテイン/ンイテロプ: 永続の与ダメージ・病気発症確率補正を追加。両者は排他で、最後に食べたものだけ有効。
- 弾薬: 効果時間中の歩行速度10倍と、移動中のたりない/アリ衝突ノックバックを追加。
- 怪しい薬: 行動変容アイテム効果と病気の中からランダム2種を付与。
- たりないデータに「現在のアイテム効果」を追加。
- バージョンとService Workerキャッシュを15.15.0に更新。
## v15.18.0
- Implemented static `app_manifest.json` and `scripts/generate_app_files.py` for HTML/Service Worker static-list synchronization.
- Split `css/styles.css` into ordered CSS files while keeping a compatibility shim.
- Added `js/effect_registry.js` and routed item-effect labels/colors/random pools/selected display text through the registry.
- Added `js/sound_pack.js`; `audio.js` now consumes sound pack gains, cooldowns, and sample paths.
- Added mobile canvas input modes: 自動 / カメラ / 道具 / 家系図.
- Expanded debug overlay with Service Worker/cache/audio/effect diagnostics.
- Added dedicated tool icons for 下剤, プロテイン, ンイテロプ, 弾薬, 怪しい薬, 水銀, 巨大薬, 矮小薬, ずんだ汁.
## v15.19.1
- 効果音のマスター出力、カテゴリ別ゲイン、ID別ゲインを再調整。連続再生で小さくなりにくいよう同時発音上限とWeb Audio出力経路を変更。
- 食べ物の自然消耗を連続減少から半日に2回程度の離散消耗へ変更。衛生統計ペナルティも同じタイミングに抑制。
- 薬系アイテムの地面表示とUIアイコンを錠剤風に変更。
- プロテイン/ンイテロプの外観を山盛りの粉に変更。永続効果中は継続的なオーラ/粒子エフェクトを表示。
- ずんだ汁を緑の水滴外観へ変更。雨と水滴表示は灰色寄りへ変更。
- バージョンとService Workerキャッシュを 15.19.1 に更新。
## v15.19.1
- Removed embedded letters/sub-labels from new item sprites and tool icons.
- Refreshed parameter item UI icons; cache-busted updated icon URLs.
- Fixed firecracker placement triggering explosion sound through accident log inference.
- Made giant drug placed sprite larger and dwarf drug placed sprite smaller.
- Redrew mercury, zunda juice, and water as rain-drop style sprites.
- Changed protein/niteropu persistent visuals to red rising particles and purple falling particles.
- Fixed remaining colony chart tab label from 生活 to 精神.
- Consolidated documentation into dense English files; left `docs/prompt.txt` untouched.
- Restored rain, revised water/zunda/mercury item sprites, added zunchi-slave head zunchi, fight win rate, fight defeat chance, and post-wake lock.
- Earlier patches added parameter items, persistent item effects, WebP assets, sound categories, push notifications, ant systems, family tree UI, performance manager, event bus, manifest generation, CSS split, and large-file JS split.

171
README.md
View file

@ -1,156 +1,35 @@
# Tarinai Colony Observation Game
Single-page, dependency-free browser simulation. Open `index.html`; no build step, package manager, server API, transpiler, or module loader is required. Runtime is plain HTML/CSS/JS loaded by ordered `<script>` tags.
Buildless single-page browser simulation. Open `index.html`; no package manager, transpiler, server API, or module loader is required. Runtime order is explicit `<script defer>` order generated from `app_manifest.json`.
## Codex operating model
## Edit contract
Read only these first: `README.md`, `docs/AI_CONTEXT.md`, `docs/code_map.md`. Then open the smallest implementation file named by the map. Avoid broad grep unless a symbol is not mapped. Keep Japanese UI strings escaped in source (`\uXXXX` in JS, entities in HTML/Markdown/CSS where applicable). After edits run `node --check js/*.js` and verify `index.html` script order.
- Preserve Japanese in-game UI tone and strings.
- Do not edit `docs/prompt.txt`; it is external user material.
- Use `app_manifest.json` as the source of truth for CSS/JS load order and service-worker cache entries.
- Run `python3 scripts/generate_app_files.py` after version or asset-list changes.
- Run `node --check js/*.js service-worker.js` before zipping.
- Verify ZIP integrity with `zip -T`.
## Core simulation
## Documentation map
- Entities: `Tarinai` agents with hunger, sleep/energy, loneliness, stress, health, age, personality, family links, relationships, diseases, favorite flag, target/need AI, transient animation timers.
- World: bounded field with weather, time, lighting, temperature, spatial-grid queries, items, effects, logs, archive/family data, placement and collision rules.
- Observation loop: agents autonomously seek food/water/sleep/social targets, react to zunchi, tools, rain, cold, crowding, relationships, disease, damage, conflict, reproduction, and player input.
- Records: observation log and family tree store births, deaths, parentage, cause-of-death labels, favorite state, and key state changes.
- `docs/ARCHITECTURE.md`: runtime architecture, file ownership, system boundaries.
- `docs/CONTENT_GUIDE.md`: item visuals, UI/field visual parity, audio, bubble text inventory.
- `docs/MAINTENANCE.md`: release process, validation, debugging, cache/version rules.
- `docs/prompt.txt`: untouched user prompt/context archive.
## Important mechanics
## Core systems
- Personality uses four axes: aggression, openness, sociability, and neuroticism. Children inherit from the parents' current four-axis personality average with variation.
- Relationships track affinity/fear/fight outcomes and influence approach, avoidance, intimidation, conflict, co-parent behavior, and UI summaries.
- Death causes are normalized by `js/health.js`: fight, hunger, disease, stress overload, over-poking, firecracker, accident, lifespan. Major damage within 10 seconds can become `<cause> de suyaku` in UI text (`...\u3067\u8870\u5f31`).
- Water-curable diseases force high-priority water seeking. Zunda-curable diseases force high-priority zunda seeking.
- Favorite agents show a star above the sprite and can be toggled from the observation panel.
- `World`: `js/world.js` plus `js/world_*.js`; time, weather, placement, effects, family records, spatial queries, ants, combat, logging.
- `Tarinai`: `js/tarinai.js` plus `js/tarinai_*.js`; identity, personality, needs, items, disease, movement, social behavior, rendering.
- `Item`: `js/items.js`; lifecycle, scaling, field rendering. Field rendering is the visual source of truth for tool icons.
- `EffectRegistry`: `js/effect_registry.js`; item-effect names, exclusivity, display metadata, random-effect candidates.
- `Audio`: `js/audio.js` + `js/sound_pack.js`; category toggles, synthetic fallbacks, sample paths, per-ID volume/cooldown.
- `Performance`: `js/performance_manager.js`; FPS quality level, ant stride, effect budget, terrain redraw cadence.
## Diseases
## Current high-risk areas
- Zunchi disease: zunchi-derived contamination; slows movement, increases stress, emits miasma effect, cured by water.
- Sleep disease: caused by sleep drug; stops all behavior and sleeps indefinitely; cured by damage.
- Explosion disease: low-probability result of explosion knockback; explodes after half a day; cured by water or zunda mochi.
- Fight-wound disease: low-probability result after bleeding for five in-game hours; spins and wanders aimlessly; cured by zunda mochi.
## Tools and items
- Direct tools: observe, delete, poke, pinch, spawn.
- Food/medicine: zunchi, zunda mochi, heko mochi, kenka mochi, water, water bowl, sleep drug, grass.
- Environment/objects: stone, hay bed, nest box, cleaning tool, ball, firecracker, vertical fence, horizontal fence.
- Cleaning removes zunchi/blood/traces while pushing agents and applying water effects. Water bowl is persistent water. Nest box lowers stress nearby and improves sleep quality. Rain increases falling water items.
## File map
```text
index.html Static DOM shell; ordered script tags; no long ecology/body text.
css/styles.css Layout, HUD, dialogs, tooltips, responsive rules.
js/data.js Sprite/object assets, metrics, config, field presets.
js/text_catalog.js Escaped Japanese UI catalog: tooltips and ecology cards.
js/math.js Generic math helpers.
js/assets.js Lazy image loading, caches, fallback render images.
js/audio.js WebAudio/sample effects.
js/render.js Background, weather, lighting, HUD, global canvas render.
js/sim_core.js Names, personalities, relationships, effects, spatial grid.
js/health.js Damage memory, death-cause normalization, damage application.
js/items.js Item state, lifecycle, drawing, grass/zunchi/ball/firecracker logic.
js/tarinai.js Tarinai constructor/core state only.
js/tarinai_identity_social.js Agent identity, records, personality, relations, stress/damage wrappers.
js/tarinai_disease_nest.js Diseases, water effects, nest-box entry/exit.
js/tarinai_needs_items.js Needs AI, eating/drinking, item interaction, facing, zunchi behavior.
js/tarinai_social_move_life.js Social interaction, movement, life/death.
js/tarinai_render.js Agent drawing and hit testing.
js/world.js World constructor/core state only.
js/world_view.js Field scale, camera, viewport, tool sizing, time/phase helpers.
js/world_family_social.js Family tree, birth, fights, social conflict entry points.
js/world_combat_effects.js Damage, explosions, water hose, effects, balls, collision outcomes.
js/world_environment.js Obstacles, fences, grass placement, temperature, nearest queries.
js/world_spatial_budget.js Spatial grid, draw list, budgets, compaction, performance helpers.
js/world_ants_system.js Ant updates, nest haul/founding, queen spawning.
js/world_update.js Main world update, weather, out-of-bounds sanitation, pointer info.
js/world_placement_log.js Placement, click handling, log classification/dispatch.
js/world_bootstrap.js Creates global `world` after World mixins load.
js/ui.js UI state/cache references only.
js/ui_log.js Log rendering, push notification controls, sound setting sync.
js/ui_selected.js Selected Tarinai data panel and text formatting.
js/ui_layout_dialogs.js Canvas resize, dialogs, field switching, top buttons.
js/ui_tools.js Tool size badges and tool size cycling.
js/ui_bind.js Event listeners and input binding.
js/ui_charts.js Population/statistics charts.
js/ui_family*.js Family/archive data, async build, layout, paths, render.
docs/AI_CONTEXT.md Minimal implementation context for AI edits.
docs/code_map.md Symbol-to-file lookup table.
```
## Edit rules
- Preserve zero-build script order. Add new JS files explicitly in `index.html` before dependents.
- Keep global symbols intentional; this codebase is not ESM.
- Prefer catalog/config/helper extraction over adding large inline blocks to `index.html` or `tarinai.js`.
- Do not reintroduce removed compatibility files (`sim.js`, `ui_exports.js`) or broad JS/CSS caching for active code.
- For public uploads, delete old server files before replacing the directory, then upload the full new ZIP contents.
## v15 notes
- Colony chart tab `events` was removed. `environment` now reports security, hygiene, and happiness.
- Selected Tarinai UI includes per-individual records attached by structured observation events.
- Observation highlights selected relatives and major social relations: parent, child, friend, enemy.
- Explosions impart velocity to nearby zunchi items; cursor contact reduces stress and emits dense heart effects.
## v4 \u8ffd\u52a0\u4fee\u6b63
- \u5de3\u7bb1\u5165\u9000\u5ba4\u3092\u5c02\u7528\u72b6\u614b\u5316\u3057\u3001\u900f\u660e\u5316\u30fb\u5f53\u305f\u308a\u5224\u5b9a\u6d88\u6ec5\u30fb\u500b\u4f53\u6570\u53cd\u6620\u30fb\u9000\u51fa\u5fa9\u5e30\u3092\u5b89\u5b9a\u5316\u3002
- \u7761\u7720\u6642\u306e\u512a\u5148\u9806\u4f4d\u3092\u300c\u5de3\u7bb1 \u2192 \u5e72\u3057\u8349\u30d9\u30c3\u30c9 \u2192 \u305d\u306e\u5834\u300d\u306b\u5909\u66f4\u3002
- \u305f\u308a\u306a\u3044\u30c7\u30fc\u30bfUI\u306e\u53cb\u9054/\u6575\u5bfe\u3092\u5168\u4ef6\u30c1\u30c3\u30d7\u8868\u793a\u306b\u3057\u3001\u30af\u30ea\u30c3\u30af\u3067\u500b\u4f53\u3078\u79fb\u52d5\u3067\u304d\u308b\u3088\u3046\u306b\u5909\u66f4\u3002
## 15.18.0
- イベントUI下部にログ種別別のプッシュ通知設定を追加。ON時は左下最前面に通知表示
- 巣がなくなったアリは空きのある別巣へ所属変更。空き巣がない場合は死亡せず彷徨う
- 女王アリを大きめに表示
- 画像アセットをWebP化し、参照をWebPへ切り替え
- Service Workerによる静的アセットキャッシュを追加し、indexのno-cache指定を解除
### 効果音差し替え
v15.18.0以降、`js/audio.js``samplePaths``sfx_genkotsu_impact` などの音IDとファイルパスを追加すると、内蔵の仮SEよりその音源が優先されます。ID一覧は `docs/SOUND_IDS.md` を参照してください。
## 15.18.0
- プッシュ通知の保存キーを更新し、初期状態はOFFに戻した。
- 効果音設定は右上の音設定で、操作音・通知音のみデフォルトON、たりないの声はデフォルトOFFに変更。
- 操作音と通知音の内蔵仮SE音量を大幅に増加。
- つまむ操作でたりないデータカードが開く挙動を修正。
- 道具から配置する全オブジェクトで配置音が鳴るよう、placeItem側に配置音を統合。
- イベントUI折り畳み時の高さを圧縮。
## 15.18.0
- 効果音の仮SE音量を再度引き上げ。
- イベントUI内の効果音設定を廃止し、右上の音設定メニューへ統一。
- イベントログ欄が上下端に達した時、右メニュー全体へスクロールを受け渡すよう修正。
## 15.18.0 内部システム更新
- バージョン/キャッシュ管理、イベントバス、PerformanceManager、地形dirtyキャッシュ、拡張SpatialGrid、デバッグモードを追加しました。
- `?debug=1` をURLに付けるとFPS/負荷レベル/エンティティ数/イベント履歴を確認できます。
- 詳細は `docs/INTERNAL_SYSTEMS.md` を参照してください。
### v15.18.0 個体効果アイテム
- 下剤、プロテイン、ンイテロプ、弾薬、怪しい薬を追加。
- たりないデータの「現在のアイテム効果」で、永続効果と残り時間つき効果を確認できます。
### v15.18.0 internal maintenance
- Static loading order is centralized in `app_manifest.json`.
- Run `python3 scripts/generate_app_files.py` after adding/removing CSS or JS files.
- Sound replacement is centralized in `js/sound_pack.js`.
- Item/disease effect metadata is centralized in `js/effect_registry.js`.
## v15.19.1
- 効果音のマスター出力、カテゴリ別ゲイン、ID別ゲインを再調整。連続再生で小さくなりにくいよう同時発音上限とWeb Audio出力経路を変更。
- 食べ物の自然消耗を連続減少から半日に2回程度の離散消耗へ変更。衛生統計ペナルティも同じタイミングに抑制。
- 薬系アイテムの地面表示とUIアイコンを錠剤風に変更。
- プロテイン/ンイテロプの外観を山盛りの粉に変更。永続効果中は継続的なオーラ/粒子エフェクトを表示。
- ずんだ汁を緑の水滴外観へ変更。雨と水滴表示は灰色寄りへ変更。
- バージョンとService Workerキャッシュを 15.19.1 に更新。
### v15.19.1
- 新アイテム系スプライト/道具アイコンから文字表示を削除。
- 爆竹配置時に爆発音が鳴る問題を修正。
- 巨大薬/矮小薬の設置サイズを視覚的に差別化。
- 水銀/ずんだ汁/水滴を降雨の水滴表現に合わせて再描画。
- プロテイン/ンイテロプの永続演出を粒子表現へ変更。
- 統計タブの「生活」表記残りを「精神」に修正。
- Item effects touch `effect_registry`, `tarinai_item_effects`, `tarinai_needs_items`, `items`, and selected-info UI.
- Mobile input must remain hidden on desktop-class devices; use body class `touch-mobile-ui` only for compact no-hover coarse-pointer devices.
- Weather rain overlay is diagonal line streaks. Fallen water items, zunda juice, and mercury share the same horizontal oval item-droplet silhouette; recolor only.
- Tool UI icons must be generated previews of `js/items.js` drawing helpers, never separate designs.

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 338 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 394 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 280 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 510 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 324 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 278 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 280 B

Before After
Before After

View file

@ -399,13 +399,13 @@ h1 {
.tool[data-tool="fight_mochi"] { --tool-icon: url("../assets/ui/tool_fight_mochi.webp"); }
.tool[data-tool="water_bowl"] { --tool-icon: url("../assets/ui/tool_water_bowl.webp"); }
/* v15.19.1: updated parameter item tool icons */
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.19.1"); }
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.19.1"); }
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.19.1"); }
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.19.1"); }
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.19.1"); }
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.19.1"); }
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.19.1"); }
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.19.1"); }
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.19.1"); }
/* v15.20.3: updated parameter item tool icons */
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.20.3"); }
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.20.3"); }
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.20.3"); }
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.20.3"); }
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.20.3"); }
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.20.3"); }
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.20.3"); }
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.20.3"); }
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.20.3"); }

View file

@ -364,3 +364,27 @@ meter {
border: 1px solid rgba(84,68,48,0.10);
}
/* v15.20.3: clearer tool category grouping. */
.tool-category[data-tool-category="operate"] { --tool-cat-a: rgba(224,239,255,0.82); --tool-cat-b: rgba(203,226,255,0.74); --tool-cat-line: rgba(82,128,190,0.26); }
.tool-category[data-tool-category="food"] { --tool-cat-a: rgba(236,252,219,0.82); --tool-cat-b: rgba(214,244,194,0.74); --tool-cat-line: rgba(96,156,72,0.26); }
.tool-category[data-tool-category="medicine"] { --tool-cat-a: rgba(243,231,255,0.84); --tool-cat-b: rgba(230,214,252,0.78); --tool-cat-line: rgba(136,94,184,0.28); }
.tool-category[data-tool-category="habitat"] { --tool-cat-a: rgba(255,245,218,0.84); --tool-cat-b: rgba(250,229,186,0.76); --tool-cat-line: rgba(174,125,54,0.28); }
.tool-category[data-tool-category="hazard"] { --tool-cat-a: rgba(255,230,224,0.84); --tool-cat-b: rgba(250,207,197,0.78); --tool-cat-line: rgba(189,88,66,0.30); }
.tool-category { border-color: var(--tool-cat-line, rgba(84,68,48,0.10)); background: linear-gradient(180deg, var(--tool-cat-a, rgba(255,255,255,0.36)), rgba(255,255,255,0.28)); }
.tool-category-toggle { background: linear-gradient(180deg, var(--tool-cat-a, rgba(255,252,244,0.66)), var(--tool-cat-b, rgba(255,252,244,0.54))); border-bottom-color: var(--tool-cat-line, rgba(84,68,48,0.08)); }
.tool[data-tool="ball"] { --tool-icon: none; }
.tool[data-tool="ball"]::before {
content: "⚽";
background: none;
display: grid;
place-items: center;
font-size: 28px;
line-height: 1;
opacity: 1;
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
}
.tool[data-tool="ball"]::after { background: none; }
@media (min-width: 921px) {
.mobile-mode-controls { display: none !important; }
}

View file

@ -180,15 +180,15 @@
}
/* v15.18.0: item icons and mobile input mode */
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.19.1"); }
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.19.1"); }
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.19.1"); }
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.19.1"); }
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.19.1"); }
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.19.1"); }
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.19.1"); }
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.19.1"); }
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.19.1"); }
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.20.3"); }
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.20.3"); }
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.20.3"); }
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.20.3"); }
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.20.3"); }
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.20.3"); }
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.20.3"); }
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.20.3"); }
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.20.3"); }
.mobile-mode-controls {
display: none;
position: absolute;
@ -218,9 +218,8 @@
background: linear-gradient(180deg, #ffe9a8, #ffd376);
box-shadow: 0 3px 10px rgba(150, 105, 30, .20);
}
@media (pointer: coarse), (max-width: 760px) {
.mobile-mode-controls { display: flex; }
body.mobile-mode-camera #gameCanvas { cursor: grab; }
body.mobile-mode-tool #gameCanvas { cursor: crosshair; }
body.mobile-mode-family .lineage-main-card { z-index: 9; }
}
body.touch-mobile-ui .mobile-mode-controls:not([hidden]) { display: flex; }
body:not(.touch-mobile-ui) .mobile-mode-controls { display: none !important; }
body.touch-mobile-ui.mobile-mode-camera #gameCanvas { cursor: grab; }
body.touch-mobile-ui.mobile-mode-tool #gameCanvas { cursor: crosshair; }
body.touch-mobile-ui.mobile-mode-family .lineage-main-card { z-index: 9; }

View file

@ -1,21 +0,0 @@
# AI_CONTEXT
Purpose: dense orientation for code-editing agents. Browser-only simulation; no build system. Source uses globals and strict script order, not ESM.
Load order from `index.html`: `version -> event_bus -> performance_manager -> data -> math -> assets -> audio -> render -> sim_core -> items -> ants -> health -> tarinai core/mixins -> world core/mixins -> world_bootstrap -> text_catalog -> ui state/helpers/bind -> ui_charts -> ui_family modules -> main -> debug_tools`.
Primary objects:
- `World` (`js/world.js` + `js/world_*.js`): constructor is in `world.js`; methods are split by concern into `world_view`, `world_family_social`, `world_combat_effects`, `world_environment`, `world_spatial_budget`, `world_ants_system`, `world_update`, and `world_placement_log`.
- `Tarinai` (`js/tarinai.js` + `js/tarinai_*.js`): constructor is in `tarinai.js`; methods are split into identity/social, disease/nest, needs/items, social/move/life, and render modules.
- `Item` (`js/items.js`): item state, draw, lifecycle.
- `HEALTH` (`js/health.js`): damage memory, major-damage window, death-cause classification, damage application.
- `TEXT_CATALOG` (`js/text_catalog.js`): Japanese tooltips and ecology cards. Keep Japanese escaped.
Behavior hotspots:
- Need selection / target priority: `Tarinai.resolveNeeds` and related target helpers.
- Disease state: constructor fields plus `infect*`, `recover*`, `updateZunchiDisease`, `updateSpecialDiseases`.
- Conflict: intimidation/fight fields in `Tarinai`, global fight resolution in `World`.
- Cause of death: never duplicate regexes in `tarinai.js`; update `HEALTH`.
- Public load performance: avoid eager image loads; use `assets.js` lazy/fallback path.
Validation checklist: `node --check js/*.js`; search for accidental raw Japanese in `.js/.html/.css/.md/.json`; load `index.html` after cache-busting version changes.

31
docs/ARCHITECTURE.md Normal file
View file

@ -0,0 +1,31 @@
# Architecture
Build target: static browser page. No bundler. Globals and ordered deferred scripts are intentional.
## Load order
`app_manifest.json` owns CSS/JS order. Regenerate `index.html` and `service-worker.js` with `python3 scripts/generate_app_files.py` after version/order changes. `js/version.js` exposes `TARINAI_APP.version`, cache name, and query helper.
## Runtime objects
- `World`: constructor in `js/world.js`; methods split into view, family/social, combat/effects, environment, spatial/budget, ants, update, placement/log, bootstrap.
- `Tarinai`: constructor in `js/tarinai.js`; methods split into identity/social, disease/nest, item effects, needs/items, social/move/life, render.
- `Item`: item state, lifecycle, scaling, and field rendering in `js/items.js`.
- `Effect`: transient visuals in `js/sim_core.js`; combat/weather/food effects are spawned through `World` helpers.
- `TarinaiEvents`: global event bus for logs, terrain dirtiness, audio failures, phase/performance changes.
- `TarinaiPerformance`: FPS quality manager; provides effect caps, ant stride, terrain redraw cadence, UI intervals.
## Data ownership
- `js/data.js`: item/tool definitions, config, image metrics, field presets.
- `js/text_catalog.js`: Japanese UI copy/tooltips. Keep JS strings escaped where existing style uses escapes.
- `js/effect_registry.js`: item-effect display names, exclusivity, random-effect candidates, UI colors.
- `js/sound_pack.js`: sound IDs, category, volume multiplier, cooldown, optional sample path.
## State loops
`main.js` starts asset preload, world bootstrap, animation loop, deferred image loading, service-worker registration. `World.update(dt)` advances time, weather, items, ants, Tarinai, effects, budgets, terrain dirtiness. `render()` draws cached background/terrain, visible entities, weather overlay, cards, HUD, and debug overlays.
## Spatial/performance rules
Use `World.nearby*` helpers rather than full-array scans in hot paths. Terrain cache uses dirty/version flags; do not reintroduce every-frame full terrain hashing. Performance degradation should be centralized in `performance_manager.js`.

37
docs/CONTENT_GUIDE.md Normal file
View file

@ -0,0 +1,37 @@
# Content and visual guide
## Visual source of truth
Do not invent tool-UI-only item designs. Field visuals in `js/items.js` are primary. Tool icons in `assets/ui/tool_*.webp` are generated previews and must match field rendering. Update `scripts/generate_item_icons.py` when changing item helper silhouettes; regenerate icons before shipping.
Canonical helper mapping:
- Weather rain overlay: `drawRain` in `render.js`; diagonal blue line streaks only, never item droplets.
- Horizontal item droplet: `drawOvalWaterDropSprite`; used by fallen water items, zunda juice, mercury. Same silhouette; recolor only.
- Sleep drug: `drawSleepTabletSprite`.
- Laxative: `drawLaxativeTabletSprite`; zunchi-motif tablet.
- Mystery drug: `drawMysteryTabletSprite`; question-mark tablet.
- Ammo: `drawBulletSprite`; bullet silhouette.
- Protein/niteropu: powder-pile rendering in the parameter-item branch.
## UI copy and labels
Most visible UI remains Japanese. Keep user-facing naming consistent: `生活` has been renamed to `精神`; selected Tarinai combat summary shows fight win rate, not cumulative defeats.
## Bubble text inventory
Active Tarinai/world bubble texts:
- Symbols: `!`, `?`, `♡`, `...`.
- Sleep: `Zzz...`.
- Eating/poop: `もぐ`, `ぶりゅっ`, `ぶりゅ…`.
- Fear/fight: `はぁぁぁ!`, `はうー!`, `はっ...はっ...`.
- Disease/recovery: `うぅ…`, `はう`.
- Random small voice: `はう`, `はうぅ`, `はうっ`, `はぅ`.
- Item effects: `???`.
When adding bubbles, keep them short; every bubble may also trigger `audio.bubble`.
## Audio
Sound settings are in the top-right sound menu, not inside Event UI. Categories: Tarinai voice, notification, operation. File-backed sound packs belong in `js/sound_pack.js`; missing samples fall back to synthetic audio. Notification and operation sounds are intentionally louder than voice sounds.

View file

@ -1,69 +0,0 @@
# Internal systems v15.18.0
Implemented systems except full save/load serialization.
## Version/cache
- `js/version.js` owns the runtime app version.
- `index.html` and `service-worker.js` use `15.18.0` cache-busting consistently.
- `service-worker.js` is registered with `updateViaCache: "none"`.
- `.htaccess` marks `service-worker.js` as no-cache/no-store and static assets as immutable.
## Event bus
- `js/event_bus.js` exposes `window.TarinaiEvents`.
- `World.emit/on` forward to the shared event bus.
- Current emitted events include `log:entry`, `world:phase`, `tool:place`, `terrain:dirty`, `performance:level`, `ant:nest-built`, and `audio:sample-error`.
## Performance manager
- `js/performance_manager.js` exposes `window.TarinaiPerformance`.
- FPS samples are averaged and converted into `level` 0-3.
- Ant update stride, effect cap, terrain redraw interval, and UI/stats intervals can now be tuned from one place.
## Terrain dirty cache
- Terrain cache no longer hashes every cached terrain item every frame.
- `World.markTerrainDirty(reason)` increments `terrainVersion`.
- The cached terrain layer redraws from `terrainVersion`, light bucket, field, and canvas size.
## Spatial grid
- `SpatialGrid` now maintains `itemCells`, `tarinaiCells`, `antCells`, `obstacleCells`, `foodCells`, and `hazardCells`.
- `World.nearbyAnts/nearbyObstacles/nearbyFood/nearbyHazards` are available for future behavior/collision work.
## Debug mode
- Add `?debug=1` to the URL to show a lightweight overlay with FPS, performance level, entity counts, terrain/spatial versions, and recent event bus entries.
## Audio system
- Audio has per-ID gain, per-ID minimum gap, a concurrent voice cap, sample load failure reporting, and a test event hook: `TarinaiEvents.emit("audio:test", { id: "sfx_notify" })`.
## File split in 15.18.0
Large legacy files were split into mixin-style modules. `world.js` and `tarinai.js` now define constructors only; prototype methods are attached by adjacent `world_*` and `tarinai_*` files before `world_bootstrap.js` creates the global `world` instance. This preserves the no-build browser deployment model while reducing edit conflicts and improving maintainability.
## v15.18.0 internal system pass
### Static manifest
- `app_manifest.json` is the source of truth for CSS and JS loading order.
- `scripts/generate_app_files.py` regenerates `index.html` link/script tags and `service-worker.js` cache lists.
- This prevents drift between HTML, Service Worker, and versioned cache assets.
### Effect registry
- `js/effect_registry.js` defines item/disease effect metadata in one place: label, color, effect kind, exclusivity group, and display text.
- `tarinai_item_effects.js` now reads labels, colors, random-effect candidates, and selected-data display strings from `TarinaiEffectRegistry`.
- Future item effects should be registered here first, then only behavior-specific logic should be added to the Tarinai methods.
### Sound pack
- `js/sound_pack.js` defines category gains, ID gains, cooldowns, and optional audio file paths.
- `js/audio.js` consumes `window.TARINAI_SOUND_PACK` at startup.
- Material replacement should normally edit `sound_pack.js`, not core audio synthesis code.
### CSS split
- `css/styles.css` is now a compatibility shim.
- Actual styles are loaded in order: `base.css`, `layout.css`, `panel.css`, `components.css`, `mobile.css`.
- Preserve this order unless selector precedence is audited.
### Mobile input modes
- Coarse pointer/mobile displays show a small canvas mode selector.
- Modes: automatic, camera, tool, family-tree priority.
- Camera mode treats one-finger drag as camera pan; tool mode suppresses accidental panning and favors tap/tool use.
### Diagnostics
- `?debug=1` now shows cache name, Service Worker control state, sound pack version, recent audio IDs, and registered effect count.

27
docs/MAINTENANCE.md Normal file
View file

@ -0,0 +1,27 @@
# Maintenance
## Release process
1. Edit source files.
2. Bump `app_manifest.json` and `js/version.js` to the same version.
3. Regenerate static references: `python3 scripts/generate_app_files.py`.
4. Regenerate item icons if item visuals changed: `python3 scripts/generate_item_icons.py`.
5. Check versions: no stale `v=<old>` in `index.html`, CSS icon URLs, `service-worker.js`.
6. Validate syntax: `node --check js/*.js service-worker.js`.
7. Zip from project root and run `zip -T`.
## Cache rules
`service-worker.js` must remain no-cache/no-store in `.htaccess`; versioned JS/CSS/WebP can be immutable. Register service worker with `updateViaCache: "none"`.
## Debugging
Use `?debug=1` for FPS, performance level, counts, cache/SW state, last audio ID, effect registry count, terrain/spatial version, and recent event-bus history.
## Mobile input
The mobile mode controls are only for compact no-hover coarse-pointer devices. Desktop browsers, including narrow windows, should not show the mobile controls unless the device is genuinely compact touch-first.
## Prompt archive
`docs/prompt.txt` is external user material. Do not rewrite, translate, trim, or delete it.

View file

@ -1,55 +0,0 @@
# 効果音ID一覧 v15.12.0
音素材を差し替える場合は `js/audio.js``samplePaths` に、下記IDとファイルパスを追加してください。未指定IDはブラウザ内蔵音で仮再生します。
## たりないの声
- `voice_hau`
- `voice_flee`
- `voice_poop`
- `voice_stress`
- `sfx_eat`
- `sfx_sleep`
- `sfx_wake`
- `sfx_birth`
- `sfx_death`
- `sfx_damage_soft`
- `sfx_damage_heavy`
- `sfx_fight_hit`
- `sfx_fight_finish`
- `sfx_disease`
- `sfx_heal`
- `sfx_ant_drag`
- `sfx_ant_die`
- `sfx_queen_ant`
## 通知
- `sfx_notify`
- `sfx_log_push`
- `sfx_phase`
## 操作
- `sfx_ui_click`
- `sfx_ui_fold`
- `sfx_place`
- `sfx_delete`
- `sfx_poke`
- `sfx_grab`
- `sfx_drop`
- `sfx_water_hose`
- `sfx_water_clean`
- `sfx_drink`
- `sfx_ball_hit`
- `sfx_stone_impact`
- `sfx_firecracker_fuse`
- `sfx_firecracker_explode`
- `sfx_genkotsu_fall`
- `sfx_genkotsu_impact`
- `sfx_ground_rumble`
- `sfx_grass`
- `sfx_zunchi`
- `sfx_ant_nest`
## v15.18.0 sound pack
Sound paths, per-ID volume multipliers, category multipliers, and minimum replay intervals are now centralized in `js/sound_pack.js`.
For normal sound replacement, edit `TARINAI_SOUND_PACK.samplePaths` with the same stable sound ID instead of changing `js/audio.js`.

View file

@ -1,25 +0,0 @@
# Code map
Use this as the first lookup table before opening large files.
| Need | File / symbol |
|---|---|
| Script order / DOM shell | `index.html` |
| Global config, sprites, object metrics | `js/data.js` / `CONFIG`, `SPRITES`, `IMAGE_METRICS` |
| Lazy image loading | `js/assets.js` / `loadInitialImages`, `loadDeferredImages`, `getRenderableImage` |
| UI Japanese text catalog | `js/text_catalog.js` / `TEXT_CATALOG` |
| Core agent state | `js/tarinai.js` / `class Tarinai` constructor |
| Agent AI target choice | `js/tarinai_needs_items.js` / `resolveNeeds`, target helpers |
| Disease state transitions | `js/tarinai_disease_nest.js` / `infect*`, `recover*`, `update*Disease*` |
| Damage/death cause rules | `js/health.js` / `HEALTH` |
| Item creation/lifecycle/draw | `js/items.js` / `class Item` |
| Cleaning, rain, placement | `js/world_combat_effects.js`, `js/world_update.js`, `js/world_placement_log.js` |
| Fence collision | `js/world_environment.js` / `resolveFenceCollision` |
| Logs/archive | `js/world_placement_log.js`, `js/ui_family*.js` |
| Main renderer | `js/render.js` / `render` helpers |
| Agent drawing | `js/tarinai_render.js` / `draw` |
| Input/tools/dialogs | `js/ui.js`, `js/ui_bind.js`, `js/ui_layout_dialogs.js`, `js/ui_tools.js` |
| Statistics UI | `js/ui_charts.js` |
| Family tree UI | `js/ui_family_data.js`, `js/ui_family_async.js`, `js/ui_family_layout.js`, `js/ui_family_paths.js`, `js/ui_family_render.js` |
Refactor guidance: extract isolated rule sets into small helper files, then delegate from `Tarinai`/`World`. Keep shared domain terms centralized (`HEALTH`, `TEXT_CATALOG`, `CONFIG`) to reduce repeated prompt context.

View file

@ -1,163 +0,0 @@
# &#x305F;&#x308A;&#x306A;&#x3044;&#x30B3;&#x30ED;&#x30CB;&#x30FC;&#x89B3;&#x5BDF;&#x6240;&#xFF1A;&#x4ED5;&#x69D8;&#x30E1;&#x30E2;
## &#x30B3;&#x30F3;&#x30BB;&#x30D7;&#x30C8;
&#x300C;&#x4E0D;&#x8DB3;&#x3092;&#x89E3;&#x6C7A;&#x3059;&#x308B;&#x30B2;&#x30FC;&#x30E0;&#x300D;&#x3067;&#x306F;&#x306A;&#x304F;&#x3001;
&#x300C;&#x4E0D;&#x8DB3;&#x3092;&#x62B1;&#x3048;&#x305F;&#x751F;&#x304D;&#x7269;&#x304C;&#x3001;&#x305D;&#x308C;&#x3067;&#x3082;&#x52DD;&#x624B;&#x306B;&#x66AE;&#x3089;&#x3059;&#x69D8;&#x5B50;&#x3092;&#x773A;&#x3081;&#x308B;&#x30B2;&#x30FC;&#x30E0;&#x300D;&#x3002;
&#x30D7;&#x30EC;&#x30A4;&#x30E4;&#x30FC;&#x306F;&#x795E;&#x3067;&#x306F;&#x306A;&#x304F;&#x3001;&#x89B3;&#x5BDF;&#x8005;&#x517C;&#x98FC;&#x80B2;&#x4FC2;&#x7A0B;&#x5EA6;&#x306E;&#x7ACB;&#x5834;&#x3002;
&#x3059;&#x3079;&#x3066;&#x3092;&#x6700;&#x9069;&#x5316;&#x3059;&#x308B;&#x3088;&#x308A;&#x3001;&#x5C11;&#x3057;&#x8DB3;&#x308A;&#x306A;&#x3044;&#x72B6;&#x614B;&#x306E;&#x63FA;&#x3089;&#x304E;&#x3092;&#x89B3;&#x5BDF;&#x3059;&#x308B;&#x3053;&#x3068;&#x3092;&#x4E3B;&#x76EE;&#x7684;&#x306B;&#x3059;&#x308B;&#x3002;
## &#x73FE;&#x5728;&#x5B9F;&#x88C5;&#x3057;&#x3066;&#x3044;&#x308B;&#x30EB;&#x30FC;&#x30D7;
1. &#x500B;&#x4F53;&#x304C;&#x7A7A;&#x8179;&#x30FB;&#x5B64;&#x72EC;&#x30FB;&#x7720;&#x6C17;&#x30FB;&#x4E0D;&#x5B89;&#x3092;&#x5185;&#x90E8;&#x72B6;&#x614B;&#x3068;&#x3057;&#x3066;&#x6301;&#x3064;
2. &#x72B6;&#x614B;&#x306B;&#x5FDC;&#x3058;&#x3066;&#x990C;&#x30FB;&#x4EF2;&#x9593;&#x30FB;&#x5BDD;&#x5E8A;&#x3092;&#x63A2;&#x3059;
3. &#x30D7;&#x30EC;&#x30A4;&#x30E4;&#x30FC;&#x306F;&#x74B0;&#x5883;&#x306B;&#x30A2;&#x30A4;&#x30C6;&#x30E0;&#x3092;&#x7F6E;&#x3044;&#x3066;&#x4ECB;&#x5165;&#x3059;&#x308B;
4. &#x4ECB;&#x5165;&#x3068;&#x500B;&#x4F53;&#x884C;&#x52D5;&#x306B;&#x3088;&#x308A;&#x3001;&#x7E41;&#x6B96;&#x30FB;&#x6B7B;&#x4EA1;&#x30FB;&#x89B3;&#x5BDF;&#x30ED;&#x30B0;&#x304C;&#x751F;&#x307E;&#x308C;&#x308B;
5. &#x6B7B;&#x4EA1;&#x500B;&#x4F53;&#x306F;&#x300C;&#x305F;&#x308A;&#x306A;&#x3044;&#x8DE1;&#x300D;&#x3092;&#x6B8B;&#x3057;&#x3001;&#x74B0;&#x5883;&#x306B;&#x8A18;&#x9332;&#x3068;&#x3057;&#x3066;&#x6B8B;&#x308B;
## &#x500B;&#x4F53;&#x72B6;&#x614B;
| &#x72B6;&#x614B; | &#x5185;&#x5BB9; |
|---|---|
| &#x7A7A;&#x8179; | &#x9AD8;&#x3044;&#x307B;&#x3069;&#x990C;&#x30FB;&#x8349;&#x3092;&#x63A2;&#x3059;&#x3002;&#x4E0A;&#x304C;&#x308A;&#x3059;&#x304E;&#x308B;&#x3068;&#x6B7B;&#x4EA1; |
| &#x5B64;&#x72EC; | &#x9AD8;&#x3044;&#x307B;&#x3069;&#x4EF2;&#x9593;&#x306B;&#x5BC4;&#x308B;&#x3002;&#x8FD1;&#x304F;&#x306B;&#x4EF2;&#x9593;&#x304C;&#x3044;&#x308B;&#x3068;&#x4E0B;&#x304C;&#x308B; |
| &#x7720;&#x6C17;&#x6B8B;&#x91CF; | &#x4F4E;&#x3044;&#x307B;&#x3069;&#x5BDD;&#x308B;&#x3002;&#x5BDD;&#x5E8A;&#x304C;&#x3042;&#x308B;&#x3068;&#x56DE;&#x5FA9;&#x304C;&#x901F;&#x3044; |
| &#x4E0D;&#x5B89; | &#x9AD8;&#x3044;&#x307B;&#x3069;&#x614C;&#x3066;&#x308B;&#x3002;&#x6C34;&#x30FB;&#x77F3;&#x30FB;&#x5BDD;&#x5E8A;&#x306A;&#x3069;&#x3067;&#x4E0B;&#x304C;&#x308B; |
| &#x305F;&#x308A;&#x306A;&#x3044;&#x5EA6; | &#x4E0A;&#x8A18;&#x306E;&#x7DCF;&#x5408;&#x7684;&#x306A;&#x4E0D;&#x5B89;&#x5B9A;&#x3055; |
## &#x500B;&#x4F53;&#x30BF;&#x30A4;&#x30D7;
9&#x679A;&#x306E;&#x30B9;&#x30D7;&#x30E9;&#x30A4;&#x30C8;&#x306B;&#x5BFE;&#x5FDC;&#x3002;
- &#x306B;&#x3053;&#x305F;&#x308A;&#x306A;&#x3044;
- &#x3044;&#x304B;&#x308A;&#x305F;&#x308A;&#x306A;&#x3044;
- &#x3046;&#x308B;&#x3046;&#x308B;&#x305F;&#x308A;&#x306A;&#x3044;
- &#x3058;&#x3068;&#x305F;&#x308A;&#x306A;&#x3044;
- &#x3088;&#x3060;&#x308C;&#x305F;&#x308A;&#x306A;&#x3044;
- &#x3050;&#x3057;&#x3083;&#x305F;&#x308A;&#x306A;&#x3044;
- &#x3059;&#x3084;&#x305F;&#x308A;&#x306A;&#x3044;
- &#x307D;&#x304B;&#x3093;&#x305F;&#x308A;&#x306A;&#x3044;
- &#x306E;&#x3073;&#x305F;&#x308A;&#x306A;&#x3044;
&#x5404;&#x30BF;&#x30A4;&#x30D7;&#x306F;&#x3001;&#x7A7A;&#x8179;&#x901F;&#x5EA6;&#x30FB;&#x793E;&#x4EA4;&#x6027;&#x30FB;&#x7761;&#x7720;&#x50BE;&#x5411;&#x30FB;&#x4E0D;&#x5B89;&#x8010;&#x6027;&#x30FB;&#x79FB;&#x52D5;&#x901F;&#x5EA6;&#x306B;&#x5DEE;&#x304C;&#x3042;&#x308B;&#x3002;
## &#x4ECB;&#x5165;&#x30A2;&#x30A4;&#x30C6;&#x30E0;
| &#x30A2;&#x30A4;&#x30C6;&#x30E0; | &#x5F79;&#x5272; |
|---|---|
| &#x3075;&#x3064;&#x3046;&#x306E;&#x990C; | &#x7A7A;&#x8179;&#x3092;&#x76F4;&#x63A5;&#x4E0B;&#x3052;&#x308B; |
| &#x3042;&#x307E;&#x3044;&#x990C; | &#x7A7A;&#x8179;&#x3068;&#x4E0D;&#x5B89;&#x3092;&#x4E0B;&#x3052;&#x308B;&#x304C;&#x4F9D;&#x5B58;&#x7684;&#x306A;&#x6319;&#x52D5;&#x3092;&#x751F;&#x307F;&#x3084;&#x3059;&#x3044; |
| &#x6C34;&#x6EF4; | &#x4E0D;&#x5B89;&#x3092;&#x4E0B;&#x3052;&#x308B;&#x3002;&#x6642;&#x9593;&#x3067;&#x84B8;&#x767A;&#x3059;&#x308B; |
| &#x8349; | &#x8EFD;&#x3044;&#x990C;&#x30FB;&#x843D;&#x3061;&#x7740;&#x304D;&#x5834;&#x30FB;&#x6B7B;&#x5F8C;&#x306E;&#x5FAA;&#x74B0;&#x8981;&#x7D20; |
| &#x306C;&#x304F;&#x3044;&#x77F3; | &#x5468;&#x8FBA;&#x6E29;&#x5EA6;&#x3092;&#x4E0A;&#x3052;&#x3001;&#x4E0D;&#x5B89;&#x3092;&#x4E0B;&#x3052;&#x308B; |
| &#x3075;&#x308F;&#x3075;&#x308F; | &#x5BDD;&#x5E8A;&#x3002;&#x7761;&#x7720;&#x56DE;&#x5FA9;&#x3092;&#x5F37;&#x3081;&#x308B; |
| &#x3064;&#x3064;&#x304F; | &#x500B;&#x4F53;&#x306B;&#x4E00;&#x6642;&#x7684;&#x306A;&#x4E0D;&#x5B89;&#x3068;&#x901F;&#x5EA6;&#x3092;&#x4E0E;&#x3048;&#x308B; |
| &#x8FF7;&#x3044;&#x8FBC;&#x307F; | &#x65B0;&#x898F;&#x500B;&#x4F53;&#x3092;&#x8FFD;&#x52A0;&#x3059;&#x308B; |
## &#x751F;&#x6B7B;
&#x6B7B;&#x4EA1;&#x6761;&#x4EF6;&#x306F;&#x73FE;&#x5728;&#x3001;&#x4E3B;&#x306B;&#x4EE5;&#x4E0B;&#x3002;
- &#x7A7A;&#x8179;&#x904E;&#x591A;
- &#x5BFF;&#x547D;
- &#x4E0D;&#x5B89;&#x904E;&#x591A;
- &#x7A7A;&#x8179;&#x3068;&#x7720;&#x6C17;&#x306E;&#x8907;&#x5408;
&#x6B7B;&#x4EA1;&#x6642;&#x306B;&#x306F;&#x300C;&#x305F;&#x308A;&#x306A;&#x3044;&#x8DE1;&#x300D;&#x304C;&#x751F;&#x6210;&#x3055;&#x308C;&#x3001;&#x4E00;&#x5B9A;&#x78BA;&#x7387;&#x3067;&#x8349;&#x3082;&#x767A;&#x751F;&#x3059;&#x308B;&#x3002;
&#x3053;&#x308C;&#x306F;&#x89B3;&#x5BDF;&#x30B2;&#x30FC;&#x30E0;&#x3068;&#x3057;&#x3066;&#x300C;&#x5931;&#x6557;&#x300D;&#x3067;&#x306F;&#x306A;&#x304F;&#x3001;&#x751F;&#x614B;&#x7CFB;&#x306E;&#x8A18;&#x9332;&#x3068;&#x3057;&#x3066;&#x6271;&#x3046;&#x3002;
## &#x7E41;&#x6B96;
&#x8FD1;&#x8DDD;&#x96E2;&#x306B;&#x3044;&#x308B;2&#x500B;&#x4F53;&#x304C;&#x3001;&#x4EE5;&#x4E0B;&#x3092;&#x6E80;&#x305F;&#x3059;&#x3068;&#x4F4E;&#x78BA;&#x7387;&#x3067;&#x65B0;&#x500B;&#x4F53;&#x304C;&#x767A;&#x751F;&#x3059;&#x308B;&#x3002;
- &#x7A7A;&#x8179;&#x304C;&#x9AD8;&#x3059;&#x304E;&#x306A;&#x3044;
- &#x7720;&#x6C17;&#x6B8B;&#x91CF;&#x304C;&#x5341;&#x5206;&#x3042;&#x308B;
- &#x7E41;&#x6B96;&#x30AF;&#x30FC;&#x30EB;&#x30C0;&#x30A6;&#x30F3;&#x304C;&#x7D42;&#x308F;&#x3063;&#x3066;&#x3044;&#x308B;
- &#x30B3;&#x30ED;&#x30CB;&#x30FC;&#x304C;&#x904E;&#x5BC6;&#x3067;&#x306A;&#x3044;
&#x5B50;&#x306F;&#x89AA;&#x306E;&#x30BF;&#x30A4;&#x30D7;&#x30FB;&#x8DB3;&#x308A;&#x306A;&#x3044;&#x3082;&#x306E;&#x3092;&#x4E00;&#x90E8;&#x7D99;&#x627F;&#x3057;&#x3001;&#x4F4E;&#x78BA;&#x7387;&#x3067;&#x5909;&#x7570;&#x3059;&#x308B;&#x3002;
## &#x8A2D;&#x8A08;&#x4E0A;&#x306E;&#x6CE8;&#x610F;
- &#x5B8C;&#x5168;&#x306A;&#x6700;&#x9069;&#x5316;&#x30B2;&#x30FC;&#x30E0;&#x306B;&#x5BC4;&#x305B;&#x3059;&#x304E;&#x306A;&#x3044;
- &#x6570;&#x5024;&#x3092;&#x898B;&#x305B;&#x3064;&#x3064;&#x3001;&#x30ED;&#x30B0;&#x6587;&#x3067;&#x751F;&#x614B;&#x89B3;&#x5BDF;&#x611F;&#x3092;&#x51FA;&#x3059;
- &#x4ECB;&#x5165;&#x306F;&#x5F37;&#x3044;&#x304C;&#x4E07;&#x80FD;&#x3067;&#x306F;&#x306A;&#x3044;
- &#x6B7B;&#x4EA1;&#x3084;&#x8870;&#x9000;&#x3082;&#x89B3;&#x5BDF;&#x4FA1;&#x5024;&#x3068;&#x3057;&#x3066;&#x6271;&#x3046;
- &#x300C;&#x3080;&#x3089;&#x3055;&#x304D;&#x300D;&#x306F;&#x753B;&#x50CF;&#x4F5C;&#x6210;&#x8005;&#x540D;&#x306E;&#x305F;&#x3081;&#x3001;&#x30B2;&#x30FC;&#x30E0;&#x5185;&#x306E;&#x7570;&#x5E38;&#x73FE;&#x8C61;&#x540D;&#x306B;&#x306F;&#x4F7F;&#x7528;&#x3057;&#x3066;&#x3044;&#x306A;&#x3044;
## v3&#x8A2D;&#x8A08;&#x30E1;&#x30E2;
- &#x89B3;&#x5BDF;&#x6642;&#x9593;&#x3092;&#x4F38;&#x3070;&#x3059;&#x305F;&#x3081;&#x3001;&#x7A7A;&#x8179;&#x30FB;&#x5B64;&#x72EC;&#x306E;&#x4E0A;&#x6607;&#x3092;&#x3084;&#x3084;&#x6291;&#x5236;&#x3057;&#x3001;&#x5BFF;&#x547D;&#x3092;&#x9577;&#x3081;&#x306B;&#x3057;&#x305F;&#x3002;
- &#x56F3;&#x9451;&#x306F;&#x300C;&#x9054;&#x6210;&#x300D;&#x3067;&#x306F;&#x306A;&#x304F;&#x89B3;&#x5BDF;&#x8A18;&#x9332;&#x3068;&#x3057;&#x3066;&#x6271;&#x3046;&#x3002;&#x30B9;&#x30D7;&#x30E9;&#x30A4;&#x30C8;&#x3001;&#x30A4;&#x30D9;&#x30F3;&#x30C8;&#x3001;&#x6B7B;&#x56E0;&#x3092;&#x3086;&#x308B;&#x304F;&#x89E3;&#x653E;&#x3059;&#x308B;&#x3002;
- &#x7CFB;&#x8B5C;&#x306F;&#x5168;&#x500B;&#x4F53;&#x3092;&#x8907;&#x96D1;&#x306A;&#x30C4;&#x30EA;&#x30FC;&#x3067;&#x63CF;&#x753B;&#x305B;&#x305A;&#x3001;&#x51FA;&#x751F;&#x30ED;&#x30B0;&#x3068;&#x3057;&#x3066;&#x8AAD;&#x307F;&#x3084;&#x3059;&#x3055;&#x3092;&#x512A;&#x5148;&#x3057;&#x305F;&#x3002;
- &#x97F3;&#x306F;&#x5916;&#x90E8;&#x7D20;&#x6750;&#x3092;&#x4F7F;&#x308F;&#x305A;&#x3001;Web Audio&#x306E;&#x77ED;&#x3044;&#x5408;&#x6210;&#x97F3;&#x3067;&#x5B9F;&#x88C5;&#x3057;&#x305F;&#x3002;&#x30D6;&#x30E9;&#x30A6;&#x30B6;&#x4ED5;&#x69D8;&#x4E0A;&#x3001;&#x97F3; ON &#x3092;&#x62BC;&#x3059;&#x307E;&#x3067;&#x9CF4;&#x3089;&#x306A;&#x3044;&#x3002;
## v4 &#x4ED5;&#x69D8;&#x8FFD;&#x8A18;
### &#x5B50;&#x306E;&#x8FFD;&#x5F93;
&#x7E41;&#x6B96;&#x3067;&#x751F;&#x307E;&#x308C;&#x305F;&#x5B50;&#x306F; `childGrowTime` &#x306E;&#x9593;&#x3001;&#x89AA;&#x306E;&#x3069;&#x3061;&#x3089;&#x304B;&#x8FD1;&#x3044;&#x65B9;&#x3092;&#x8FFD;&#x5F93;&#x3059;&#x308B;&#x3002;&#x6210;&#x9577;&#x3059;&#x308B;&#x3068;&#x901A;&#x5E38;AI&#x3078;&#x79FB;&#x884C;&#x3059;&#x308B;&#x3002;
### &#x7CFB;&#x8B5C;
&#x7CFB;&#x8B5C;&#x306F; `family` &#x30EC;&#x30B8;&#x30B9;&#x30C8;&#x30EA;&#x3067;&#x4FDD;&#x6301;&#x3057;&#x3001;&#x89AA;&#x5B50;&#x95A2;&#x4FC2;&#x3092;&#x6A39;&#x72B6;&#x56F3;&#x3067;&#x8868;&#x793A;&#x3059;&#x308B;&#x3002;&#x8868;&#x793A;&#x6642;&#x306F;&#x540C;&#x3058;&#x500B;&#x4F53;&#x3092;&#x91CD;&#x8907;&#x3055;&#x305B;&#x305A;&#x3001;&#x5171;&#x6709;&#x3055;&#x308C;&#x305F;&#x89AA;&#x5B50;&#x95A2;&#x4FC2;&#x306F;&#x8907;&#x6570;&#x306E;&#x63A5;&#x7D9A;&#x7DDA;&#x3067;&#x8868;&#x3059;&#x3002;
### &#x98DF;&#x4E8B;&#x30A8;&#x30D5;&#x30A7;&#x30AF;&#x30C8;
&#x98DF;&#x4E8B;&#x30D1;&#x30FC;&#x30C6;&#x30A3;&#x30AF;&#x30EB;&#x306F;&#x8868;&#x793A;&#x983B;&#x5EA6;&#x3092;&#x6291;&#x3048;&#x3001;&#x5BFE;&#x8C61;&#x30A2;&#x30A4;&#x30C6;&#x30E0;&#x306E;&#x8272;&#x3092;&#x53CD;&#x6620;&#x3059;&#x308B;&#x3002;
### &#x55A7;&#x5629;
&#x8FD1;&#x8DDD;&#x96E2;&#x3001;&#x4F4E;&#x6C17;&#x5206;&#x3001;&#x6012;&#x308A;&#x500B;&#x4F53;&#x3001;&#x904E;&#x5BC6;&#x306A;&#x3069;&#x3092;&#x6761;&#x4EF6;&#x306B;&#x5C0F;&#x3055;&#x306A;&#x55A7;&#x5629;&#x304C;&#x767A;&#x751F;&#x3059;&#x308B;&#x3002;&#x55A7;&#x5629;&#x4E2D;&#x306F; 02_angry &#x3092;&#x512A;&#x5148;&#x8868;&#x793A;&#x3057;&#x3001;&#x4E92;&#x3044;&#x306B;&#x30CE;&#x30C3;&#x30AF;&#x30D0;&#x30C3;&#x30AF;&#x3059;&#x308B;&#x3002;
### &#x663C;&#x591C;
&#x591C;&#x9593;&#x30FB;&#x5915;&#x65B9;&#x306E;&#x8272;&#x3092;&#x80CC;&#x666F;&#x3060;&#x3051;&#x3067;&#x306A;&#x304F;&#x3001;&#x89B3;&#x5BDF;&#x76BF;&#x30FB;&#x30A2;&#x30A4;&#x30C6;&#x30E0;&#x30FB;&#x305F;&#x308A;&#x306A;&#x3044;&#x672C;&#x4F53;&#x306B;&#x3082;&#x91CD;&#x306D;&#x308B;&#x3002;
## v5 &#x4ED5;&#x69D8;&#x8FFD;&#x8A18;
### &#x8349;
&#x8349;&#x306F;&#x30B3;&#x30ED;&#x30CB;&#x30FC;&#x5185;&#x306B;&#x81EA;&#x52D5;&#x3067;&#x767A;&#x751F;&#x3059;&#x308B;&#x3002;&#x8349;&#x30A2;&#x30A4;&#x30C6;&#x30E0;&#x306F; `amount` &#x3092;&#x8349;&#x91CF;&#x3068;&#x3057;&#x3066;&#x6301;&#x3061;&#x3001;&#x6642;&#x9593;&#x3067;&#x3086;&#x3063;&#x304F;&#x308A;&#x4F38;&#x3073;&#x308B;&#x3002;&#x98DF;&#x3079;&#x3089;&#x308C;&#x308B;&#x3068; `amount` &#x304C;&#x6E1B;&#x308A;&#x3001;&#x63CF;&#x753B;&#x672C;&#x6570;&#x3082;&#x6E1B;&#x308B;&#x3002;
### &#x663C;&#x591C;&#x306E;&#x63CF;&#x753B;&#x4FEE;&#x6B63;
&#x30B9;&#x30D7;&#x30E9;&#x30A4;&#x30C8;&#x672C;&#x4F53;&#x3078;&#x306E;&#x8272;&#x88DC;&#x6B63;&#x306F; `source-atop` &#x306E;&#x77E9;&#x5F62;&#x5857;&#x308A;&#x3092;&#x5EC3;&#x6B62;&#x3057;&#x3001;`ctx.filter` &#x306B;&#x5909;&#x66F4;&#x3057;&#x305F;&#x3002;&#x3053;&#x308C;&#x306B;&#x3088;&#x308A;&#x900F;&#x904E;&#x90E8;&#x5206;&#x3078;&#x77E9;&#x5F62;&#x72B6;&#x306E;&#x8272;&#x304C;&#x4E57;&#x308B;&#x554F;&#x984C;&#x3092;&#x907F;&#x3051;&#x308B;&#x3002;
### &#x7CFB;&#x8B5C;
&#x56F3;&#x9451;UI&#x306F;&#x524A;&#x9664;&#x3057;&#x3001;&#x7CFB;&#x8B5C;&#x306E;&#x307F;&#x3092;&#x30E1;&#x30A4;&#x30F3;&#x753B;&#x9762;&#x4E0B;&#x90E8;&#x306B;&#x914D;&#x7F6E;&#x3059;&#x308B;&#x3002;
## v6 &#x4ED5;&#x69D8;&#x8FFD;&#x8A18;
### &#x982D;&#x7A81;&#x304D;&#x55A7;&#x5629;
&#x55A7;&#x5629;&#x4E2D;&#x306F;&#x4E00;&#x5B9A;&#x5468;&#x671F;&#x3067;&#x76F8;&#x624B;&#x3078;&#x7A81;&#x9032;&#x3057;&#x3001;&#x8FD1;&#x8DDD;&#x96E2;&#x3067;&#x30CE;&#x30C3;&#x30AF;&#x30D0;&#x30C3;&#x30AF;&#x3068;&#x55A7;&#x5629;&#x30A8;&#x30D5;&#x30A7;&#x30AF;&#x30C8;&#x3092;&#x767A;&#x751F;&#x3055;&#x305B;&#x308B;&#x3002;
### &#x7761;&#x7720;
&#x7761;&#x7720;&#x72B6;&#x614B;&#x3067;&#x306F;&#x901F;&#x5EA6;&#x3092;&#x30BC;&#x30ED;&#x306B;&#x3057;&#x3001;&#x79FB;&#x52D5;&#x51E6;&#x7406;&#x3092;&#x884C;&#x308F;&#x306A;&#x3044;&#x3002;&#x5BDD;&#x5E8A;&#x304C;&#x8FD1;&#x304F;&#x3066;&#x3082;&#x5BDD;&#x306A;&#x304C;&#x3089;&#x79FB;&#x52D5;&#x3057;&#x306A;&#x3044;&#x3002;
### &#x30AB;&#x30FC;&#x30BD;&#x30EB;&#x53CD;&#x5FDC;
&#x30AB;&#x30FC;&#x30BD;&#x30EB;&#x304C;&#x8FD1;&#x3044;&#x5834;&#x5408;&#x3001;&#x6C17;&#x5206;&#x306E;&#x826F;&#x3044;&#x500B;&#x4F53;&#x306F;&#x7B11;&#x9854;&#x3067;&#x5BC4;&#x308A;&#x3001;&#x4E0D;&#x5B89;&#x30FB;&#x6012;&#x308A;&#x30FB;&#x3064;&#x3064;&#x304D;&#x8A18;&#x61B6;&#x306E;&#x3042;&#x308B;&#x500B;&#x4F53;&#x306F;&#x9003;&#x3052;&#x308B;&#x3002;
## v7 &#x30D3;&#x30B8;&#x30E5;&#x30A2;&#x30EB;&#x6539;&#x4FEE;
- &#x80CC;&#x666F;&#x3092;&#x30B7;&#x30E3;&#x30FC;&#x30EC;&#x98A8;&#x304B;&#x3089;&#x5EAD;&#x306E;&#x89B3;&#x5BDF;&#x533A;&#x753B;&#x98A8;&#x3078;&#x5909;&#x66F4;&#x3002;
- &#x5730;&#x9762;&#x306F;&#x6728;&#x67A0;&#x306B;&#x56F2;&#x307E;&#x308C;&#x305F;&#x660E;&#x308B;&#x3044;&#x571F;&#x5E8A;&#x3068;&#x3057;&#x3001;&#x7D30;&#x304B;&#x306A;&#x571F;&#x7C92;&#x3001;&#x82D4;&#x8272;&#x306E;&#x30E0;&#x30E9;&#x3001;&#x7E01;&#x306E;&#x8349;&#x3001;&#x91CE;&#x82B1;&#x3092;&#x52A0;&#x3048;&#x305F;&#x3002;
- &#x305F;&#x308A;&#x306A;&#x3044;&#x3068;&#x30A2;&#x30A4;&#x30C6;&#x30E0;&#x306E;&#x63A5;&#x5730;&#x5F71;&#x3092;&#x8FFD;&#x52A0;&#x3057;&#x3001;&#x5EAD;&#x306E;&#x4E0A;&#x306B;&#x5B9F;&#x5728;&#x3057;&#x3066;&#x3044;&#x308B;&#x611F;&#x899A;&#x3092;&#x5F37;&#x3081;&#x305F;&#x3002;
- &#x53F3;&#x4E0A;HUD&#x3068;&#x4E0B;&#x90E8;&#x30E9;&#x30D9;&#x30EB;&#x3092;&#x89B3;&#x5BDF;&#x672D;&#x306E;&#x3088;&#x3046;&#x306A;&#x898B;&#x305F;&#x76EE;&#x3078;&#x8ABF;&#x6574;&#x3002;
- &#x89AA;&#x5B50;&#x306E;&#x8FFD;&#x5F93;&#x3092;&#x8996;&#x8A8D;&#x3057;&#x3084;&#x3059;&#x304F;&#x3059;&#x308B;&#x305F;&#x3081;&#x3001;&#x8FD1;&#x8DDD;&#x96E2;&#x3067;&#x306F;&#x8584;&#x3044;&#x70B9;&#x7DDA;&#x30AC;&#x30A4;&#x30C9;&#x3092;&#x63CF;&#x304F;&#x3002;
## v8 &#x30D3;&#x30B8;&#x30E5;&#x30A2;&#x30EB; / &#x5B9F;&#x88C5;&#x6539;&#x4FEE;
- &#x76F4;&#x5C04;&#x65E5;&#x5149;&#x30FB;&#x6708;&#x5149;&#x306E;&#x65B9;&#x5411;&#x3092;&#x3082;&#x3068;&#x306B;&#x3001;&#x305F;&#x308A;&#x306A;&#x3044;&#x3068;&#x30A2;&#x30A4;&#x30C6;&#x30E0;&#x3078;&#x6295;&#x5F71;&#x5F71;&#x3092;&#x4ED8;&#x4E0E;&#x3002;
- &#x80CC;&#x666F;&#x3067;&#x306F;&#x5929;&#x4F53;&#x672C;&#x4F53;&#x306F;&#x63CF;&#x304B;&#x305A;&#x3001;&#x7A7A;&#x8272;&#x3068;&#x5F71;&#x65B9;&#x5411;&#x306E;&#x307F;&#x3067;&#x663C;&#x591C;&#x3092;&#x8868;&#x73FE;&#x3059;&#x308B;&#x3002;
- &#x98DF;&#x4E8B;&#x306E;&#x84C4;&#x7A4D;&#x3067;&#x300C;&#x305A;&#x3093;&#x3061;&#x300D;&#x3092;&#x6392;&#x6CC4;&#x3059;&#x308B;&#x6D88;&#x5316;&#x51E6;&#x7406;&#x3092;&#x8FFD;&#x52A0;&#x3002;
- 09_stretch &#x306F;&#x901A;&#x5E38;&#x63CF;&#x753B;&#x3067;&#x3082;&#x6A2A;&#x306B;&#x3084;&#x3084;&#x5E83;&#x304F;&#x6271;&#x3044;&#x3001;&#x8996;&#x8A8D;&#x6027;&#x3092;&#x4E0A;&#x3052;&#x305F;&#x3002;
- &#x7CFB;&#x8B5C;&#x30C4;&#x30EA;&#x30FC;&#x306E;&#x63A5;&#x7D9A;&#x7DDA;&#x306F;&#x64EC;&#x4F3C;&#x8981;&#x7D20;&#x3092;&#x518D;&#x8A2D;&#x8A08;&#x3057;&#x3001;&#x9014;&#x5207;&#x308C;&#x306B;&#x304F;&#x3044;&#x9023;&#x7D50;&#x8868;&#x73FE;&#x3078;&#x5909;&#x66F4;&#x3002;
- &#x65E7;&#x56F3;&#x9451;&#x7528;&#x306E;&#x72B6;&#x614B;&#x7BA1;&#x7406;&#xFF08;codex&#xFF09;&#x3068;&#x672A;&#x4F7F;&#x7528;&#x30B9;&#x30BF;&#x30A4;&#x30EB;&#x3092;&#x6574;&#x7406;&#x3002;
## v9 &#x4FEE;&#x6B63;
- `ctx.filter = blur(...)` &#x3092;&#x4F7F;&#x3063;&#x305F;&#x5F71;&#x3092;&#x5EC3;&#x6B62;&#x3057;&#x3001;&#x4F4E;&#x8CA0;&#x8377;&#x306A;&#x6955;&#x5186;&#x63CF;&#x753B;&#x3078;&#x5909;&#x66F4;&#x3002;
- &#x80CC;&#x666F;&#x88C5;&#x98FE;&#x306E;&#x30EB;&#x30FC;&#x30D7;&#x6570;&#x3092;&#x524A;&#x6E1B;&#x3057;&#x3001;&#x6BCE;&#x30D5;&#x30EC;&#x30FC;&#x30E0;&#x306E;&#x63CF;&#x753B;&#x8CA0;&#x8377;&#x3092;&#x4E0B;&#x3052;&#x305F;&#x3002;
- &#x6B7B;&#x4EA1;&#x6642;&#x306E; `splat` &#x306F; amount &#x3092;&#x9023;&#x7D9A;&#x7684;&#x306A;&#x30A2;&#x30EB;&#x30D5;&#x30A1;&#x5024;&#x3078;&#x53CD;&#x6620;&#x3057;&#x3001;&#x6570;&#x5206;&#x304B;&#x3051;&#x3066;&#x30D5;&#x30A7;&#x30FC;&#x30C9;&#x30A2;&#x30A6;&#x30C8;&#x3059;&#x308B;&#x3002;

View file

@ -4,11 +4,11 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>&#x305F;&#x308A;&#x306A;&#x3044;&#x89B3;&#x5BDF;</title>
<link rel="stylesheet" href="css/base.css?v=15.19.1" />
<link rel="stylesheet" href="css/layout.css?v=15.19.1" />
<link rel="stylesheet" href="css/panel.css?v=15.19.1" />
<link rel="stylesheet" href="css/components.css?v=15.19.1" />
<link rel="stylesheet" href="css/mobile.css?v=15.19.1" />
<link rel="stylesheet" href="css/base.css?v=15.20.3" />
<link rel="stylesheet" href="css/layout.css?v=15.20.3" />
<link rel="stylesheet" href="css/panel.css?v=15.20.3" />
<link rel="stylesheet" href="css/components.css?v=15.20.3" />
<link rel="stylesheet" href="css/mobile.css?v=15.20.3" />
</head>
<body>
<div id="loadingScreen" class="loading-screen" aria-live="polite">
@ -75,26 +75,32 @@
<div class="panel-card-body">
<div class="tool-body">
<div id="toolPalette" class="tool-palette">
<section class="tool-category" data-tool-category="observe">
<button class="tool-category-toggle" type="button" aria-expanded="true">&#x89B3;&#x5BDF;</button>
<section class="tool-category" data-tool-category="operate">
<button class="tool-category-toggle" type="button" aria-expanded="true">操作・観察</button>
<div class="tool-grid tool-category-body">
<button class="tool selected" data-tool="observe">&#x89B3;&#x5BDF;</button>
<button class="tool" data-tool="delete">&#x524A;&#x9664;</button>
<button class="tool" data-tool="poke">&#x3064;&#x3064;&#x304F;</button>
<button class="tool" data-tool="pinch">&#x3064;&#x307E;&#x3080;</button>
<button class="tool" data-tool="new">&#x8FFD;&#x52A0;</button>
<button class="tool" data-tool="water_hose">&#x6D17;&#x6D44;</button>
<button class="tool selected" data-tool="observe">観察</button>
<button class="tool" data-tool="delete">削除</button>
<button class="tool" data-tool="poke">つつく</button>
<button class="tool" data-tool="pinch">つまむ</button>
<button class="tool" data-tool="new">追加</button>
<button class="tool" data-tool="water_hose">洗浄</button>
</div>
</section>
<section class="tool-category" data-tool-category="food">
<button class="tool-category-toggle" type="button" aria-expanded="true">&#x98DF;&#x3079;&#x7269;&#x30FB;&#x81EA;&#x7136;</button>
<button class="tool-category-toggle" type="button" aria-expanded="true">食べ物・水・草</button>
<div class="tool-grid tool-category-body">
<button class="tool" data-tool="zunchi">&#x305A;&#x3093;&#x3061;</button>
<button class="tool" data-tool="sweet">&#x305A;&#x3093;&#x3060;&#x9905;</button>
<button class="tool" data-tool="love_mochi">&#x3078;&#x3053;&#x9905;</button>
<button class="tool" data-tool="fight_mochi">&#x3051;&#x3093;&#x304B;&#x9905;</button>
<button class="tool" data-tool="water_bowl">&#x6C34;&#x306E;&#x76BF;</button>
<button class="tool" data-tool="sleep_drug">&#x306D;&#x3080;&#x308A;&#x85AC;</button>
<button class="tool" data-tool="sweet">ずんだ餅</button>
<button class="tool" data-tool="love_mochi">へこ餅</button>
<button class="tool" data-tool="fight_mochi">けんか餅</button>
<button class="tool" data-tool="grass"></button>
<button class="tool" data-tool="water_bowl">水の皿</button>
<button class="tool" data-tool="zunda_juice">ずんだ汁</button>
</div>
</section>
<section class="tool-category" data-tool-category="medicine">
<button class="tool-category-toggle" type="button" aria-expanded="true">薬・個体効果</button>
<div class="tool-grid tool-category-body">
<button class="tool" data-tool="sleep_drug">ねむり薬</button>
<button class="tool" data-tool="laxative">下剤</button>
<button class="tool" data-tool="protein">プロテイン</button>
<button class="tool" data-tool="niteropu">ンイテロプ</button>
@ -103,22 +109,26 @@
<button class="tool" data-tool="mercury">水銀</button>
<button class="tool" data-tool="giant_drug">巨大薬</button>
<button class="tool" data-tool="dwarf_drug">矮小薬</button>
<button class="tool" data-tool="zunda_juice">ずんだ汁</button>
<button class="tool" data-tool="grass">&#x8349;</button>
</div>
</section>
<section class="tool-category" data-tool-category="object">
<button class="tool-category-toggle" type="button" aria-expanded="true">&#x30AA;&#x30D6;&#x30B8;&#x30A7;&#x30AF;&#x30C8;</button>
<section class="tool-category" data-tool-category="habitat">
<button class="tool-category-toggle" type="button" aria-expanded="true">生活・生き物</button>
<div class="tool-grid tool-category-body">
<button class="tool" data-tool="stone">&#x77F3;</button>
<button class="tool" data-tool="bed">&#x5E72;&#x8349;&#x5BDD;&#x5E8A;</button>
<button class="tool" data-tool="nest_box">&#x5DE3;&#x7BB1;</button>
<button class="tool" data-tool="ant_nest">&#x30A2;&#x30EA;&#x306E;&#x5DE3;</button>
<button class="tool" data-tool="ball">&#x30DC;&#x30FC;&#x30EB;</button>
<button class="tool" data-tool="genkotsu">&#x3052;&#x3093;&#x3053;&#x3064;</button>
<button class="tool" data-tool="firecracker">&#x7206;&#x7AF9;</button>
<button class="tool" data-tool="fence_v">&#x7E26;&#x306E;&#x67F5;</button>
<button class="tool" data-tool="fence_h">&#x6A2A;&#x306E;&#x67F5;</button>
<button class="tool" data-tool="zunchi">ずんち</button>
<button class="tool" data-tool="bed">干草寝床</button>
<button class="tool" data-tool="nest_box">巣箱</button>
<button class="tool" data-tool="ant_nest">アリの巣</button>
<button class="tool" data-tool="ball">ボール</button>
</div>
</section>
<section class="tool-category" data-tool-category="hazard">
<button class="tool-category-toggle" type="button" aria-expanded="true">危険・障害物</button>
<div class="tool-grid tool-category-body">
<button class="tool" data-tool="stone"></button>
<button class="tool" data-tool="genkotsu">げんこつ</button>
<button class="tool" data-tool="firecracker">爆竹</button>
<button class="tool" data-tool="fence_v">縦の柵</button>
<button class="tool" data-tool="fence_h">横の柵</button>
</div>
</section>
</div>
@ -227,52 +237,52 @@
</div>
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
<script src="js/version.js?v=15.19.1" defer></script>
<script src="js/event_bus.js?v=15.19.1" defer></script>
<script src="js/performance_manager.js?v=15.19.1" defer></script>
<script src="js/effect_registry.js?v=15.19.1" defer></script>
<script src="js/sound_pack.js?v=15.19.1" defer></script>
<script src="js/data.js?v=15.19.1" defer></script>
<script src="js/math.js?v=15.19.1" defer></script>
<script src="js/assets.js?v=15.19.1" defer></script>
<script src="js/audio.js?v=15.19.1" defer></script>
<script src="js/render.js?v=15.19.1" defer></script>
<script src="js/sim_core.js?v=15.19.1" defer></script>
<script src="js/items.js?v=15.19.1" defer></script>
<script src="js/ants.js?v=15.19.1" defer></script>
<script src="js/health.js?v=15.19.1" defer></script>
<script src="js/tarinai.js?v=15.19.1" defer></script>
<script src="js/tarinai_identity_social.js?v=15.19.1" defer></script>
<script src="js/tarinai_disease_nest.js?v=15.19.1" defer></script>
<script src="js/tarinai_item_effects.js?v=15.19.1" defer></script>
<script src="js/tarinai_needs_items.js?v=15.19.1" defer></script>
<script src="js/tarinai_social_move_life.js?v=15.19.1" defer></script>
<script src="js/tarinai_render.js?v=15.19.1" defer></script>
<script src="js/world.js?v=15.19.1" defer></script>
<script src="js/world_view.js?v=15.19.1" defer></script>
<script src="js/world_family_social.js?v=15.19.1" defer></script>
<script src="js/world_combat_effects.js?v=15.19.1" defer></script>
<script src="js/world_environment.js?v=15.19.1" defer></script>
<script src="js/world_spatial_budget.js?v=15.19.1" defer></script>
<script src="js/world_ants_system.js?v=15.19.1" defer></script>
<script src="js/world_update.js?v=15.19.1" defer></script>
<script src="js/world_placement_log.js?v=15.19.1" defer></script>
<script src="js/world_bootstrap.js?v=15.19.1" defer></script>
<script src="js/text_catalog.js?v=15.19.1" defer></script>
<script src="js/ui.js?v=15.19.1" defer></script>
<script src="js/ui_log.js?v=15.19.1" defer></script>
<script src="js/ui_selected.js?v=15.19.1" defer></script>
<script src="js/ui_layout_dialogs.js?v=15.19.1" defer></script>
<script src="js/ui_tools.js?v=15.19.1" defer></script>
<script src="js/ui_bind.js?v=15.19.1" defer></script>
<script src="js/ui_charts.js?v=15.19.1" defer></script>
<script src="js/ui_family.js?v=15.19.1" defer></script>
<script src="js/ui_family_data.js?v=15.19.1" defer></script>
<script src="js/ui_family_async.js?v=15.19.1" defer></script>
<script src="js/ui_family_layout.js?v=15.19.1" defer></script>
<script src="js/ui_family_paths.js?v=15.19.1" defer></script>
<script src="js/ui_family_render.js?v=15.19.1" defer></script>
<script src="js/main.js?v=15.19.1" defer></script>
<script src="js/debug_tools.js?v=15.19.1" defer></script>
<script src="js/version.js?v=15.20.3" defer></script>
<script src="js/event_bus.js?v=15.20.3" defer></script>
<script src="js/performance_manager.js?v=15.20.3" defer></script>
<script src="js/effect_registry.js?v=15.20.3" defer></script>
<script src="js/sound_pack.js?v=15.20.3" defer></script>
<script src="js/data.js?v=15.20.3" defer></script>
<script src="js/math.js?v=15.20.3" defer></script>
<script src="js/assets.js?v=15.20.3" defer></script>
<script src="js/audio.js?v=15.20.3" defer></script>
<script src="js/render.js?v=15.20.3" defer></script>
<script src="js/sim_core.js?v=15.20.3" defer></script>
<script src="js/items.js?v=15.20.3" defer></script>
<script src="js/ants.js?v=15.20.3" defer></script>
<script src="js/health.js?v=15.20.3" defer></script>
<script src="js/tarinai.js?v=15.20.3" defer></script>
<script src="js/tarinai_identity_social.js?v=15.20.3" defer></script>
<script src="js/tarinai_disease_nest.js?v=15.20.3" defer></script>
<script src="js/tarinai_item_effects.js?v=15.20.3" defer></script>
<script src="js/tarinai_needs_items.js?v=15.20.3" defer></script>
<script src="js/tarinai_social_move_life.js?v=15.20.3" defer></script>
<script src="js/tarinai_render.js?v=15.20.3" defer></script>
<script src="js/world.js?v=15.20.3" defer></script>
<script src="js/world_view.js?v=15.20.3" defer></script>
<script src="js/world_family_social.js?v=15.20.3" defer></script>
<script src="js/world_combat_effects.js?v=15.20.3" defer></script>
<script src="js/world_environment.js?v=15.20.3" defer></script>
<script src="js/world_spatial_budget.js?v=15.20.3" defer></script>
<script src="js/world_ants_system.js?v=15.20.3" defer></script>
<script src="js/world_update.js?v=15.20.3" defer></script>
<script src="js/world_placement_log.js?v=15.20.3" defer></script>
<script src="js/world_bootstrap.js?v=15.20.3" defer></script>
<script src="js/text_catalog.js?v=15.20.3" defer></script>
<script src="js/ui.js?v=15.20.3" defer></script>
<script src="js/ui_log.js?v=15.20.3" defer></script>
<script src="js/ui_selected.js?v=15.20.3" defer></script>
<script src="js/ui_layout_dialogs.js?v=15.20.3" defer></script>
<script src="js/ui_tools.js?v=15.20.3" defer></script>
<script src="js/ui_bind.js?v=15.20.3" defer></script>
<script src="js/ui_charts.js?v=15.20.3" defer></script>
<script src="js/ui_family.js?v=15.20.3" defer></script>
<script src="js/ui_family_data.js?v=15.20.3" defer></script>
<script src="js/ui_family_async.js?v=15.20.3" defer></script>
<script src="js/ui_family_layout.js?v=15.20.3" defer></script>
<script src="js/ui_family_paths.js?v=15.20.3" defer></script>
<script src="js/ui_family_render.js?v=15.20.3" defer></script>
<script src="js/main.js?v=15.20.3" defer></script>
<script src="js/debug_tools.js?v=15.20.3" defer></script>
</body>
</html>

View file

@ -265,13 +265,21 @@ class AntActor {
}
const haulers = Math.max(1, this.activeHaulersForTarget().length);
const leadHauler = this.isLeadHauler();
const targetWasSleeping = Boolean(target.sleeping || target.state === "sleep");
target.antDraggedTimer = Math.max(target.antDraggedTimer || 0, 0.35);
target.antDraggedBy = this.id;
target.state = "panic";
target.sleeping = false;
target.target = null;
target.fearTimer = Math.max(target.fearTimer || 0, 0.36);
target.thought = "\u30a2\u30ea\u306b\u5de3\u3078\u904b\u3070\u308c\u3066\u3044\u308b";
if (targetWasSleeping) {
target.sleeping = true;
target.state = "sleep";
target.sleepTimer = Math.max(target.sleepTimer || 0, 0.8);
target.thought = "眠ったままアリに運ばれている";
} else {
target.state = "panic";
target.sleeping = false;
target.fearTimer = Math.max(target.fearTimer || 0, 0.36);
target.thought = "アリに巣へ運ばれている";
}
const dHome = distXY(target.x, target.y, home.x, home.y) || 1;
if (dHome < Math.max(18, home.r * 0.66)) {
this.world.completeAntHaul?.(home, target);
@ -298,10 +306,11 @@ class AntActor {
this.vy = uy * speed;
const aggression = Math.max(0, target.currentPersonality?.aggression || 0);
const aggressiveAttack = target.shouldApplyPersonalityBehavior?.("aggression", 1) || aggression >= 0.5 || target.state === "ant_attack";
const struggle = aggressiveAttack
const stillSleeping = Boolean(target.sleeping || targetWasSleeping || target.state === "sleep");
const aggressiveAttack = !stillSleeping && (target.shouldApplyPersonalityBehavior?.("aggression", 1) || aggression >= 0.5 || target.state === "ant_attack");
const struggle = stillSleeping ? 0 : (aggressiveAttack
? (1.25 + aggression * 1.6 + clamp((target.energy || 0) / 100, 0, 1) * 0.35) / Math.max(1, haulers * 0.9)
: (0.42 + clamp((target.energy || 0) / 100, 0, 1) * 0.18) / Math.max(1, haulers);
: (0.42 + clamp((target.energy || 0) / 100, 0, 1) * 0.18) / Math.max(1, haulers));
this.hp -= dt * struggle;
if ((this.world.time || 0) - (this.carryLogAt || -999) > 8 && Math.random() < dt * 0.04) {

View file

@ -51,14 +51,14 @@ const TOOL_DEFINITIONS = Object.freeze({
water_bowl: { id: "water_bowl", itemType: "water_bowl", label: "\u6c34\u306e\u76bf", placeable: true, scalable: false, radius: 20, amount: 999, icon: "assets/ui/tool_water_bowl.webp", tooltip: "\u885b\u751f\u7684\u306a\u6c34\u304c\u5165\u3063\u305f\u76bf\u3002\u4e00\u90e8\u306e\u75c5\u6c17\u3092\u6cbb\u305b\u308b\u3002" },
sleep_drug: { id: "sleep_drug", itemType: "sleep_drug", label: "\u306d\u3080\u308a\u85ac", placeable: true, scalable: true, radius: 13, amount: 58, icon: "assets/ui/tool_sleep_drug.webp", tooltip: "\u98df\u3079\u308b\u3068\u306d\u3080\u308a\u75c5\u306b\u306a\u308b\u3002\u30c0\u30e1\u30fc\u30b8\u3067\u5b8c\u6cbb\u3059\u308b\u3002" },
laxative: { id: "laxative", itemType: "laxative", label: "下剤", placeable: true, scalable: true, radius: 13, amount: 58, icon: "assets/ui/tool_laxative.webp", tooltip: "食べると一定時間、3秒ごとにずんちを放出し、食べても空腹度が下がらなくなる。" },
protein: { id: "protein", itemType: "protein", label: "プロテイン", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_protein.webp", tooltip: "永続。与えるダメージ1.75倍、病気発症確率半減。ンイテロプとは最新効果だけ有効。" },
niteropu: { id: "niteropu", itemType: "niteropu", label: "ンイテロプ", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_niteropu.webp", tooltip: "永続。与えるダメージ0.5倍、病気発症確率2倍。プロテインとは最新効果だけ有効。" },
ammo: { id: "ammo", itemType: "ammo", label: "弾薬", placeable: true, scalable: true, radius: 12, amount: 48, icon: "assets/ui/tool_ammo.webp", tooltip: "効果時間中、歩行速度10倍。移動中にたりないやアリへ衝突すると弾き飛ばす。" },
mystery_drug: { id: "mystery_drug", itemType: "mystery_drug", label: "怪しい薬", placeable: true, scalable: true, radius: 13, amount: 52, icon: "assets/ui/tool_mystery_drug.webp", tooltip: "行動を変えるアイテム効果と病気の中からランダムで2つ付与する。" },
mercury: { id: "mercury", itemType: "mercury", label: "水銀", placeable: true, scalable: true, radius: 13, amount: 44, icon: "assets/ui/tool_mercury.webp", tooltip: "食べるたびに寿命が1.3倍ずつ乗算される。ずんだ汁で解除できる。" },
giant_drug: { id: "giant_drug", itemType: "giant_drug", label: "巨大薬", placeable: true, scalable: true, radius: 22, amount: 52, icon: "assets/ui/tool_giant_drug.webp", tooltip: "永続。サイズ3倍、攻撃力2倍、移動速度1.5倍。毎秒1ダメージ。矮小薬とは最新効果だけ有効。" },
dwarf_drug: { id: "dwarf_drug", itemType: "dwarf_drug", label: "矮小薬", placeable: true, scalable: true, radius: 8, amount: 52, icon: "assets/ui/tool_dwarf_drug.webp", tooltip: "永続。サイズ0.25倍、攻撃力0.25倍、移動速度0.5倍。毎秒1ダメージ。巨大薬とは最新効果だけ有効。" },
laxative: { id: "laxative", itemType: "laxative", label: "下剤", placeable: true, scalable: true, radius: 13, amount: 58, icon: "assets/ui/tool_laxative.webp", tooltip: "ずんちを大量に排泄させる。何かを食べてもすぐに出て行ってしまう。" },
protein: { id: "protein", itemType: "protein", label: "プロテイン", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_protein.webp", tooltip: "体をたくましくし、病気に強くなる。" },
niteropu: { id: "niteropu", itemType: "niteropu", label: "ンイテロプ", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_niteropu.webp", tooltip: "体を頼りなくし、病気に弱くなる。" },
ammo: { id: "ammo", itemType: "ammo", label: "弾薬", placeable: true, scalable: true, radius: 12, amount: 48, icon: "assets/ui/tool_ammo.webp", tooltip: "全てを加速させる。" },
mystery_drug: { id: "mystery_drug", itemType: "mystery_drug", label: "怪しい薬", placeable: true, scalable: true, radius: 13, amount: 52, icon: "assets/ui/tool_mystery_drug.webp", tooltip: "何かが起こる。" },
mercury: { id: "mercury", itemType: "mercury", label: "水銀", placeable: true, scalable: true, radius: 13, amount: 44, icon: "assets/ui/tool_mercury.webp", tooltip: "長生きの秘訣。" },
giant_drug: { id: "giant_drug", itemType: "giant_drug", label: "巨大薬", placeable: true, scalable: true, radius: 22, amount: 52, icon: "assets/ui/tool_giant_drug.webp", tooltip: "体を大きく強くするが、負担が大きい。" },
dwarf_drug: { id: "dwarf_drug", itemType: "dwarf_drug", label: "矮小薬", placeable: true, scalable: true, radius: 8, amount: 52, icon: "assets/ui/tool_dwarf_drug.webp", tooltip: "体を小さく弱くするが、負担も大きい。" },
zunda_juice: { id: "zunda_juice", itemType: "zunda_juice", label: "ずんだ汁", placeable: true, scalable: true, radius: 15, amount: 70, icon: "assets/ui/tool_zunda_juice.webp", tooltip: "食べると全てのアイテム効果を除去する。" },
grass: { id: "grass", itemType: "grass", label: "\u8349", placeable: true, scalable: true, radius: 17, amount: 120, icon: "assets/ui/tool_grass.webp", tooltip: "\u98df\u3079\u7269\u3002\u305f\u308a\u306a\u3044\u304c\u5b89\u5fc3\u3059\u308b\u3002" },
stone: { id: "stone", itemType: "stone", label: "\u77f3", placeable: true, scalable: true, radius: 20, amount: 999, icon: "assets/ui/tool_stone.webp", tooltip: "\u91cd\u3044\u77f3\u3002\u843d\u3068\u3059\u3068\u5371\u306a\u3044\u3002" },

View file

@ -27,6 +27,137 @@ function passiveFoodDecayRate(item) {
return PASSIVE_FOOD_DECAY_PER_SECOND * sizeMul * typeMul;
}
function drawOvalWaterDropSprite(ctx, r, palette = {}) {
const fill = palette.fill || "rgba(86, 157, 224, 0.82)";
const stroke = palette.stroke || "rgba(45, 101, 168, 0.52)";
const highlight = palette.highlight || "rgba(255,255,255,0.64)";
ctx.save();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = Math.max(1.6, r * 0.13);
// Horizontal oval item droplet silhouette for fallen water / zunda juice / mercury; rain overlay itself is a separate diagonal-line effect.
ctx.beginPath();
ctx.ellipse(0, 0, r * 1.42, r * 0.56, -0.04, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.globalAlpha = 0.72;
ctx.fillStyle = highlight;
ctx.beginPath();
ctx.ellipse(-r * 0.46, -r * 0.16, r * 0.34, r * 0.13, -0.18, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawSleepTabletSprite(ctx, r) {
ctx.save();
ctx.rotate(-0.18);
ctx.fillStyle = "#ebe4ff";
ctx.strokeStyle = "rgba(74, 60, 144, 0.72)";
ctx.lineWidth = Math.max(1.8, r * 0.14);
ctx.beginPath();
ctx.ellipse(0, 0, r * 0.92, r * 0.66, 0, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
ctx.fillStyle = "rgba(112, 91, 188, 0.82)";
ctx.beginPath();
ctx.arc(r * 0.12, -r * 0.04, r * 0.38, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#ebe4ff";
ctx.beginPath();
ctx.arc(r * 0.28, -r * 0.11, r * 0.38, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 0.52;
ctx.strokeStyle = "rgba(255,255,255,0.94)";
ctx.lineWidth = Math.max(1.2, r * 0.08);
ctx.beginPath();
ctx.moveTo(-r * 0.56, -r * 0.28);
ctx.quadraticCurveTo(-r * 0.14, -r * 0.48, r * 0.48, -r * 0.20);
ctx.stroke();
ctx.restore();
}
function drawLaxativeTabletSprite(ctx, r) {
ctx.save();
ctx.fillStyle = "#f4d49a";
ctx.strokeStyle = "rgba(98, 61, 29, 0.82)";
ctx.lineWidth = Math.max(1.8, r * 0.14);
roundedRect(ctx, -r * 1.02, -r * 0.54, r * 2.04, r * 1.08, r * 0.34);
ctx.fill(); ctx.stroke();
// Embedded zunchi mound motif: silhouette-based, not color-only.
ctx.fillStyle = "rgba(96, 70, 38, 0.88)";
ctx.strokeStyle = "rgba(56, 39, 22, 0.42)";
ctx.lineWidth = Math.max(1.0, r * 0.07);
const lumps = [
[-0.38, 0.11, 0.25, 0.17],
[-0.12, -0.03, 0.29, 0.20],
[0.20, 0.10, 0.27, 0.18],
[0.03, -0.23, 0.20, 0.14],
];
for (const [x,y,w,h] of lumps) {
ctx.beginPath();
ctx.ellipse(r*x, r*y, r*w, r*h, 0, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
}
ctx.globalAlpha = 0.45;
ctx.strokeStyle = "rgba(255,255,255,0.90)";
ctx.lineWidth = Math.max(1.0, r * 0.07);
ctx.beginPath();
ctx.moveTo(-r * 0.68, -r * 0.32);
ctx.quadraticCurveTo(-r * 0.24, -r * 0.48, r * 0.46, -r * 0.26);
ctx.stroke();
ctx.restore();
}
function drawMysteryTabletSprite(ctx, r) {
ctx.save();
ctx.fillStyle = "#f3ecff";
ctx.strokeStyle = "rgba(91, 55, 150, 0.84)";
ctx.lineWidth = Math.max(1.9, r * 0.15);
ctx.beginPath();
ctx.ellipse(0, 0, r * 0.94, r * 0.78, 0.10, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
ctx.fillStyle = "rgba(91,55,150,0.92)";
ctx.font = `bold ${Math.round(r * 1.35)}px ui-rounded, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("?", 0, -r * 0.04);
ctx.globalAlpha = 0.52;
ctx.strokeStyle = "rgba(255,255,255,0.90)";
ctx.lineWidth = Math.max(1.1, r * 0.08);
ctx.beginPath();
ctx.moveTo(-r * 0.52, -r * 0.34);
ctx.quadraticCurveTo(-r * 0.12, -r * 0.54, r * 0.42, -r * 0.28);
ctx.stroke();
ctx.restore();
}
function drawBulletSprite(ctx, r) {
ctx.save();
ctx.rotate(0.0);
const g = ctx.createLinearGradient(-r, 0, r, 0);
g.addColorStop(0, "#b06f23");
g.addColorStop(0.48, "#f4c46a");
g.addColorStop(1, "#a05a1f");
ctx.fillStyle = g;
ctx.strokeStyle = "rgba(87, 51, 22, 0.78)";
ctx.lineWidth = Math.max(1.7, r * 0.13);
ctx.beginPath();
ctx.moveTo(r * 0.86, 0);
ctx.quadraticCurveTo(r * 0.42, -r * 0.52, -r * 0.34, -r * 0.52);
ctx.lineTo(-r * 0.88, -r * 0.40);
ctx.lineTo(-r * 0.88, r * 0.40);
ctx.lineTo(-r * 0.34, r * 0.52);
ctx.quadraticCurveTo(r * 0.42, r * 0.52, r * 0.86, 0);
ctx.closePath();
ctx.fill(); ctx.stroke();
ctx.strokeStyle = "rgba(255,237,176,0.62)";
ctx.lineWidth = Math.max(1.0, r * 0.07);
ctx.beginPath();
ctx.moveTo(-r * 0.44, -r * 0.34);
ctx.lineTo(r * 0.42, -r * 0.20);
ctx.stroke();
ctx.restore();
}
class Item {
constructor(type, x, y) {
this.id = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
@ -624,15 +755,15 @@ class Item {
}
} else if (typeof isParamEffectItemType === "function" && isParamEffectItemType(this.type)) {
const meta = {
laxative: { fill: "#eef0b8", stroke: "#879046", accent: "#dfe770", kind: "pill" },
laxative: { fill: "#f1d18a", stroke: "#774c1f", accent: "#5f3a1b", kind: "laxative" },
protein: { fill: "#f4a24f", stroke: "#b45b27", accent: "#ffe2b9", kind: "powder" },
niteropu: { fill: "#8a79d0", stroke: "#4c3f91", accent: "#d8d1ff", kind: "powder" },
ammo: { fill: "#ffe8b6", stroke: "#b37a26", accent: "#d0942f", kind: "box" },
mystery_drug: { fill: "#e9ffe8", stroke: "#4a9b65", accent: "#9de7ad", kind: "pill" },
mercury: { fill: "rgba(216,219,220,0.98)", stroke: "rgba(198,202,204,0.98)", accent: "rgba(162,169,174,0.98)", kind: "droplet" },
ammo: { fill: "#f4c46a", stroke: "#6f431f", accent: "#ffe8a8", kind: "bullet" },
mystery_drug: { fill: "#dff4ff", stroke: "#27587b", accent: "#437c97", kind: "mystery" },
mercury: { fill: "rgba(210,216,222,0.94)", stroke: "rgba(112,122,132,0.62)", accent: "rgba(255,255,255,0.72)", kind: "droplet" },
giant_drug: { fill: "#ffe3cf", stroke: "#c9632f", accent: "#ffb47b", kind: "pill" },
dwarf_drug: { fill: "#e9e9ff", stroke: "#6967ac", accent: "#b8b5f4", kind: "pill" },
zunda_juice: { fill: "rgba(134,224,91,0.98)", stroke: "rgba(76,168,57,0.98)", accent: "rgba(202,255,186,0.98)", kind: "droplet" },
zunda_juice: { fill: "rgba(125,218,82,0.94)", stroke: "rgba(63,150,50,0.62)", accent: "rgba(238,255,226,0.72)", kind: "droplet" },
}[this.type] || { fill: "#fffaf1", stroke: "#8d6f48", accent: "#ddd", kind: "pill" };
const localR = this.r * (this.type === "giant_drug" ? 1.08 : (this.type === "dwarf_drug" ? 0.92 : 1));
ctx.rotate(Math.sin(this.seed) * 0.10);
@ -662,29 +793,13 @@ class Item {
ctx.fill();
ctx.restore();
} else if (meta.kind === "droplet") {
ctx.save();
ctx.lineCap = "round";
ctx.strokeStyle = meta.stroke;
ctx.lineWidth = Math.max(2.2, localR * 0.16);
ctx.globalAlpha = 0.92;
const lean = -localR * 0.34;
ctx.beginPath();
ctx.moveTo(localR * 0.18, -localR * 0.90);
ctx.lineTo(localR * 0.18 + lean, localR * 0.42);
ctx.stroke();
ctx.globalAlpha = 0.70;
ctx.fillStyle = meta.fill;
ctx.beginPath();
ctx.ellipse(localR * 0.18 + lean, localR * 0.54, localR * 0.24, localR * 0.38, -0.18, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 0.50;
ctx.strokeStyle = meta.accent;
ctx.lineWidth = Math.max(1.0, localR * 0.07);
ctx.beginPath();
ctx.moveTo(localR * 0.10, -localR * 0.72);
ctx.lineTo(localR * 0.10 + lean * 0.72, localR * 0.18);
ctx.stroke();
ctx.restore();
drawOvalWaterDropSprite(ctx, localR, { fill: meta.fill, stroke: meta.stroke, highlight: meta.accent });
} else if (meta.kind === "laxative") {
drawLaxativeTabletSprite(ctx, localR);
} else if (meta.kind === "mystery") {
drawMysteryTabletSprite(ctx, localR);
} else if (meta.kind === "bullet") {
drawBulletSprite(ctx, localR);
} else if (meta.kind === "box") {
ctx.fillStyle = meta.fill;
ctx.strokeStyle = meta.stroke;
@ -721,19 +836,7 @@ class Item {
ctx.restore();
}
} else if (this.type === "sleep_drug") {
ctx.fillStyle = "#f5f0ff";
ctx.strokeStyle = "rgba(112, 83, 170, 0.55)";
ctx.lineWidth = 2;
ctx.rotate(-0.45);
roundedRect(ctx, -this.r * 0.95, -this.r * 0.45, this.r * 1.90, this.r * 0.90, this.r * 0.45);
ctx.fill(); ctx.stroke();
ctx.fillStyle = "rgba(152, 116, 218, 0.58)";
ctx.fillRect(-1, -this.r * 0.42, 2, this.r * 0.84);
ctx.rotate(0.45);
ctx.fillStyle = "rgba(255,255,255,0.58)";
ctx.beginPath();
ctx.ellipse(-this.r * 0.36, -this.r * 0.16, this.r * 0.25, this.r * 0.09, -0.20, 0, Math.PI * 2);
ctx.fill();
drawSleepTabletSprite(ctx, this.r);
} else if (this.type === "genkotsu") {
const img = typeof getRenderableImage === "function" ? getRenderableImage("genkotsu", "genkotsu") : images.get("genkotsu");
const impact = clamp(this.impactFlash || 0, 0, 1);
@ -786,22 +889,11 @@ class Item {
ctx.fillStyle = "rgba(255,255,255,0.54)";
ctx.beginPath(); ctx.arc(-this.r * 0.32, -this.r * 0.18, 2.8, 0, Math.PI * 2); ctx.fill();
} else if (this.type === "water") {
ctx.save();
ctx.lineCap = "round";
ctx.strokeStyle = "rgba(198, 202, 204, 0.98)";
ctx.lineWidth = Math.max(2.0, this.r * 0.16);
const lean = -this.r * 0.34;
ctx.globalAlpha = 0.86;
ctx.beginPath();
ctx.moveTo(this.r * 0.18, -this.r * 0.90);
ctx.lineTo(this.r * 0.18 + lean, this.r * 0.42);
ctx.stroke();
ctx.globalAlpha = 0.58;
ctx.fillStyle = "rgba(216, 219, 220, 0.98)";
ctx.beginPath();
ctx.ellipse(this.r * 0.18 + lean, this.r * 0.54, this.r * 0.24, this.r * 0.38, -0.18, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
drawOvalWaterDropSprite(ctx, this.r, {
fill: "rgba(86, 157, 224, 0.82)",
stroke: "rgba(45, 101, 168, 0.52)",
highlight: "rgba(255,255,255,0.68)",
});
} else if (this.type === "grass") {
const growth = this.growth ?? clamp(this.amount / 120, 0, 1);
const stage = growth < 0.22 ? "sprout" : growth < 0.48 ? "young" : "mature";

View file

@ -133,7 +133,7 @@ loadImages(null, (done, total) => {
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
const swUrl = window.TARINAI_APP?.withVersion?.("./service-worker.js") || "./service-worker.js?v=15.19.1";
const swUrl = window.TARINAI_APP?.withVersion?.("./service-worker.js") || "./service-worker.js?v=15.20.3";
navigator.serviceWorker.register(swUrl, { updateViaCache: "none" }).catch(() => undefined);
});
}

View file

@ -636,56 +636,38 @@ function drawRain(ctx, w, h, t) {
if (world.weather !== "light_rain") return;
ctx.save();
const perf = world.performanceLevel ? world.performanceLevel() : 0;
const count = perf >= 2 ? 58 : 118;
ctx.lineCap = "round";
const count = perf >= 2 ? 64 : 138;
const angle = -0.32; // Rain visual effect: diagonal streaks, separate from fallen water item sprites.
const slant = Math.tan(angle);
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(196, 200, 202, 0.92)";
ctx.lineWidth = randSeed(18700 + i, 1.0, 1.8);
const speed = randSeed(18100 + i, 360, 620);
const len = randSeed(18500 + i, 14, 28);
const cycleY = h + 260;
const cycleX = w + 260;
const fall = randSeed(18400 + i, -220, cycleY) + t * speed;
const y = ((fall % cycleY) + cycleY) % cycleY - 120;
const drift = fall * slant * 0.42;
const rawX = randSeed(18000 + i, -140, w + 180) + drift;
const x = ((rawX % cycleX) + cycleX) % cycleX - 130;
const alpha = y < 60 ? clamp((y + 90) / 150, 0, 1) : 1;
const dx = Math.sin(angle) * len;
const dy = Math.cos(angle) * len;
ctx.globalAlpha = (0.18 + 0.34 * randSeed(19000 + i, 0.35, 1.0)) * alpha;
ctx.strokeStyle = "rgba(88, 150, 218, 0.86)";
ctx.lineWidth = randSeed(18700 + i, 0.8, 1.45);
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x - tilt, y + len);
ctx.moveTo(x - dx * 0.5, y - dy * 0.5);
ctx.lineTo(x + dx * 0.5, y + dy * 0.5);
ctx.stroke();
if (perf < 3 && i % 2 === 0) {
ctx.globalAlpha = 0.18 + 0.18 * alpha;
ctx.fillStyle = "rgba(213, 216, 218, 0.92)";
if (perf < 3 && i % 4 === 0) {
ctx.globalAlpha = 0.13 * alpha;
ctx.strokeStyle = "rgba(255,255,255,0.70)";
ctx.lineWidth = 0.7;
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(198, 202, 204, 0.98)";
ctx.lineWidth = randSeed(19300 + i, 1.2, 2.2);
ctx.beginPath();
ctx.moveTo(x, topY);
ctx.lineTo(x + lean, bottomY);
ctx.moveTo(x - dx * 0.5 - 1.5, y - dy * 0.5);
ctx.lineTo(x + dx * 0.5 - 1.5, y + dy * 0.5);
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(216, 219, 220, 0.98)";
ctx.fill();
}
}
ctx.restore();
@ -746,7 +728,6 @@ function render() {
const perf = world.performanceLevel ? world.performanceLevel() : 0;
if (perf < 2) drawLightRays(ctx, screenW, screenH, lighting);
if (perf < 3 || world.weather === "light_rain") drawRain(ctx, screenW, screenH, world.time);
const visibleRect = visibleWorldRect(world, 180);
const terrainLayer = ensureTerrainCache(world, lighting);
@ -792,6 +773,7 @@ function render() {
// Light affecting the whole visible scene, including characters and ground.
drawAtmosphericLighting(ctx, screenW, screenH, lighting);
if (world.weather === "light_rain") drawRain(ctx, screenW, screenH, world.time);
const drawAtScreenPosition = (entity, drawFn) => {
if (!entity) return;

View file

@ -75,6 +75,8 @@ class Tarinai { constructor(world, opts = {}) {
this.reproductionTimer = rand(4, CONFIG.reproductionCooldown);
this.lastLog = 0;
this.sleeping = false;
this.awakeLockTimer = opts.awakeLockTimer ?? 0;
this.wasSleepingLastTick = false;
this.blink = rand(0, 10);
this.lastSocial = 0;
this.goodMode = opts.goodMode ?? stableChoice(this.familyKey, "good-mode", displayNormalSprites()) ?? "smile";

View file

@ -102,6 +102,7 @@
this.sleepDiseaseAnchorY = null;
this.state = "idle";
this.sleeping = false;
this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 10);
this.thought = reason || "\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f";
audio.heal?.();
this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.65);
@ -164,6 +165,7 @@
freezeSleepDiseaseMotion(dt = 0) {
if (!this.sleepDisease) return false;
const beingDraggedByAnt = Boolean((this.antDraggedTimer || 0) > 0 && this.antDraggedBy);
if (!Number.isFinite(this.sleepDiseaseAnchorX)) this.sleepDiseaseAnchorX = this.x;
if (!Number.isFinite(this.sleepDiseaseAnchorY)) this.sleepDiseaseAnchorY = this.y;
this.state = "sleep";
@ -171,10 +173,15 @@
this.target = null;
this.vx = 0;
this.vy = 0;
this.x = this.sleepDiseaseAnchorX;
this.y = this.sleepDiseaseAnchorY;
if (beingDraggedByAnt) {
this.sleepDiseaseAnchorX = this.x;
this.sleepDiseaseAnchorY = this.y;
} else {
this.x = this.sleepDiseaseAnchorX;
this.y = this.sleepDiseaseAnchorY;
}
this.wanderAngle = this.wanderAngle || 0;
this.thought = "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b";
this.thought = beingDraggedByAnt ? "眠ったままアリに運ばれている" : "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b";
if (this.world && this.world.time > (this.nextSleepBubbleAt || 0)) {
this.nextSleepBubbleAt = this.world.time + 5.0;
this.world.spawnBubble?.(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)");

View file

@ -89,7 +89,6 @@
this.powerItemMode = mode;
this.powerEffectKind = mode;
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 3.2);
this.bubble(mode === "protein" ? "ムキッ" : "へにゃ", 1.0, ITEM_EFFECT_COLORS[mode]);
this.spawnPowerItemEffect(mode, 1.5);
return true;
},
@ -102,7 +101,6 @@
this.refreshEffectiveSize();
this.powerEffectKind = mode;
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 4.0);
this.bubble(mode === "giant_drug" ? "でかっ" : "ちまっ", 1.0, ITEM_EFFECT_COLORS[mode]);
this.spawnPowerItemEffect(mode, mode === "giant_drug" ? 1.9 : 1.2);
return true;
},
@ -118,7 +116,6 @@
if (Number.isFinite(this.mercuryLifeSpanBase)) this.lifeSpan = Math.max(this.age + 5, this.mercuryLifeSpanBase * this.mercuryLifeMultiplier);
this.powerEffectKind = "mercury";
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 2.7);
this.bubble(`Hg x${this.mercuryLifeMultiplier.toFixed(2)}`, 1.2, ITEM_EFFECT_COLORS.mercury);
this.spawnPowerItemEffect("mercury", 0.95);
return true;
},
@ -142,7 +139,6 @@
this.powerEffectKind = "zunda_juice";
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 2.2);
this.refreshEffectiveSize();
this.bubble("すっきり", 1.2, ITEM_EFFECT_COLORS.zunda_juice);
this.spawnPowerItemEffect("zunda_juice", 1.4);
return true;
},
@ -153,7 +149,6 @@
let applied = [];
if (type === "laxative") {
this.setTimedItemEffect("laxative", Math.round(60 * scale));
this.bubble("ぐるる", 1.2, ITEM_EFFECT_COLORS.laxative);
this.spawnPowerItemEffect("laxative", 0.75);
applied.push("laxative");
} else if (type === "protein") {
@ -164,7 +159,6 @@
applied.push("niteropu");
} else if (type === "ammo") {
this.setTimedItemEffect("ammo", Math.round(18 * scale));
this.bubble("ドン", 0.9, ITEM_EFFECT_COLORS.ammo);
this.spawnPowerItemEffect("ammo", 1.1);
applied.push("ammo");
} else if (type === "mercury") {

View file

@ -232,6 +232,9 @@
this.energy = clamp(this.energy, 0, 100);
this.stress = clamp(this.stress, 0, 130);
const wasSleepingAtFrameStart = this.state === "sleep" || this.sleeping;
this.awakeLockTimer = Math.max(0, (this.awakeLockTimer || 0) - dt);
if (this.freezeSleepDiseaseMotion(dt)) {
this.checkLife(dt);
return;
@ -269,6 +272,8 @@
if (this.ammoCollisionKnockback) this.ammoCollisionKnockback(dt);
this.updateNestBoxPresence(dt);
if (this.world.resolveFenceCollision) this.world.resolveFenceCollision(this);
const sleepingAtFrameEnd = this.state === "sleep" || this.sleeping;
if (wasSleepingAtFrameStart && !sleepingAtFrameEnd && !this.dead) this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 10);
this.checkLife(dt);
},
@ -622,8 +627,9 @@
}
const sleepBias = this.personalityProfile().sleep;
const wakeLocked = (this.awakeLockTimer || 0) > 0 && !wasSleeping;
const keepSleeping = wasSleeping && (personalSleep || this.energy < 66 + 8 * sleepBias);
if (keepSleeping || (personalSleep && this.energy < 90 + 8 * sleepBias) || this.energy < 28 + 8 * sleepBias) {
if (!wakeLocked && (keepSleeping || (personalSleep && this.energy < 90 + 8 * sleepBias) || this.energy < 28 + 8 * sleepBias)) {
let bed = this.isSleepFurniture(previousTarget) ? previousTarget : null;
if (!bed && friendNearby && this.isSleepFurniture(friendNearby.target) && (friendNearby.state === "sleep" || friendNearby.state === "seek_bed")) bed = friendNearby.target;
if (!bed) bed = this.world.bestBedFor(this, wasSleeping ? 480 : 420);
@ -963,7 +969,7 @@
if (this.world.time > this.nextSleepBubbleAt) {
this.nextSleepBubbleAt = this.world.time + 4.5;
const crowded = occ > 5;
this.world.spawnBubble(this.x, this.y - this.radius * 1.18, crowded ? "mm..." : "Zzz...", "rgba(65,70,92,0.72)");
this.world.spawnBubble(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
}
this.energy += dt * (1.9 + comfort * 1.4);
this.stress += dt * (occ > 5 ? 0.55 : -1.3 * comfort);

View file

@ -284,6 +284,19 @@
ctx.restore();
}
}
if (sprite === "zunchi_slave") {
const zImg = typeof getRenderableImage === "function" ? getRenderableImage("zunchi", "zunchi") : (images && images.get ? images.get("zunchi") : null);
if (zImg) {
ctx.save();
ctx.shadowColor = "rgba(46, 36, 24, 0.28)";
ctx.shadowBlur = 5;
ctx.shadowOffsetY = Math.max(1, drawH * 0.012);
const zw = Math.max(12, drawW * 0.30);
const zh = zw * 0.74;
ctx.drawImage(zImg, -zw * 0.50, -dh * 0.48 - zh * 0.25, zw, zh);
ctx.restore();
}
}
drawDawnRimLight(ctx, dw, dh, lightState);
if (faceRight) ctx.scale(-1, 1);

View file

@ -67,8 +67,12 @@
o.vy += ny * rand(34, 64);
this.stress = clamp(this.stress + 1.6, 0, 130);
o.stress = clamp(o.stress + 2.8, 0, 130);
this.damage(o.outgoingDamage ? o.outgoingDamage(1.2) : 1.2, "\u55a7\u5629");
o.damage(this.outgoingDamage ? this.outgoingDamage(1.6) : 1.6, "\u55a7\u5629");
const damageToThis = o.outgoingDamage ? o.outgoingDamage(1.2) : 1.2;
const damageToOther = this.outgoingDamage ? this.outgoingDamage(1.6) : 1.6;
this.damage(damageToThis, "\u55a7\u5629");
o.damage(damageToOther, "\u55a7\u5629");
this.world.maybeDefeatFromFightDamage?.(this, o, damageToThis);
this.world.maybeDefeatFromFightDamage?.(o, this, damageToOther);
this.hurtTimer = Math.max(this.hurtTimer, 1.4);
o.hurtTimer = Math.max(o.hurtTimer, 1.8);
this.adjustRelation(o, -0.16, 0.12, "fight");
@ -80,8 +84,7 @@
this.world.spawnFallEffect(o.x, o.y + o.radius * 0.45, 0.55);
}
if (o.energy + o.mood * 0.25 < this.energy + this.mood * 0.25 - 8) {
o.defeatedById = this.id;
o.fightWinnerId = this.id;
this.world.maybeDefeatFromFightDamage?.(o, this, Math.max(damageToOther, 2.4));
}
this.world.spawnHeadbuttEffect((this.x + o.x) / 2, (this.y + o.y) / 2 - 5);
this.nextHeadbutt = this.world.time + rand(0.34, 0.55);

View file

@ -274,7 +274,23 @@ function bindUI() {
document.body.classList.toggle("mobile-mode-tool", mode === "tool");
document.body.classList.toggle("mobile-mode-family", mode === "family");
};
const updateTouchMobileUiClass = () => {
const ua = String(navigator.userAgent || "");
const coarse = Boolean(window.matchMedia && window.matchMedia("(pointer: coarse)").matches);
const noHover = Boolean(window.matchMedia && window.matchMedia("(hover: none)").matches);
const touchPoints = Number(navigator.maxTouchPoints || 0);
const w = window.innerWidth || 9999;
const h = window.innerHeight || 9999;
const mobileUa = /Android|iPhone|iPod|Mobile|Silk/i.test(ua);
const compactPhoneViewport = Math.min(w, h) <= 640 && Math.max(w, h) <= 980;
const enabled = Boolean(coarse && noHover && touchPoints > 0 && compactPhoneViewport && mobileUa);
document.body.classList.toggle("touch-mobile-ui", enabled);
if (ui.mobileModeControls) ui.mobileModeControls.hidden = !enabled;
};
const mobileInputMode = () => uiCache.mobileInputMode || "auto";
updateTouchMobileUiClass();
window.addEventListener("resize", updateTouchMobileUiClass, { passive: true });
window.matchMedia?.("(pointer: coarse)")?.addEventListener?.("change", updateTouchMobileUiClass);
setMobileInputMode(uiCache.mobileInputMode || "auto");
ui.mobileModeControls?.addEventListener("click", (e) => {
const btn = e.target.closest("[data-mobile-mode]");

View file

@ -147,7 +147,7 @@ function renderSelected() {
<div class="info-row relation-row relation-list-row"><span>\u53cb\u9054</span><strong>${relationListHtml(t, "friend")}</strong></div>
<div class="info-row relation-row relation-list-row"><span>\u6575\u5bfe</span><strong>${relationListHtml(t, "enemy")}</strong></div>
<div class="info-row"><span>\u55a7\u5629\u8a18\u61b6</span><strong>${escapeHtml(fightRecord)}</strong></div>
<div class="info-row"><span>\u7d2f\u8a08\u6557\u5317</span><strong>${t.totalFightLosses || 0}</strong></div>
<div class="info-row"><span>けんか勝率</span><strong>${escapeHtml(fightWinRateLabel(t))}</strong></div>
`;
const familyHtml = `
<div class="info-row"><span>\u89aa</span><strong>${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "\u4e0d\u660e")}</strong></div>
@ -233,6 +233,14 @@ function relationFightRecord(t) {
return won || lost ? `\u52dd${won} / \u8ca0${lost}` : "\u306a\u3057";
}
function fightWinRateLabel(t) {
const won = Number(t.totalFightWins || 0);
const lost = Number(t.totalFightLosses || 0);
const total = won + lost;
if (!total) return "なし";
return `${Math.round((won / total) * 100)}%${won}${lost}敗)`;
}
function setTextIfChanged(el, key, value) {
if (!el) return;
const next = String(value);

View file

@ -1,7 +1,7 @@
"use strict";
(function () {
const APP_VERSION = "15.19.1";
const APP_VERSION = "15.20.3";
const APP_BUILD = "systems-split";
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;

View file

@ -405,6 +405,35 @@
return true;
},
maybeDefeatFromFightDamage(victim, attacker, damage = 0) {
if (!victim || !attacker || victim.dead || attacker.dead || victim.id === attacker.id) return false;
if ((victim.fightTimer || 0) <= 0.04 || (attacker.fightTimer || 0) <= 0.04) return false;
if (victim.defeatedById || (victim.defeatedTimer || 0) > 0.04) return false;
const profile = victim.personalityProfile ? victim.personalityProfile() : { fight: 1 };
const aggressionHold = clamp((profile.fight || 1) - 1, -0.8, 1.3);
const healthRisk = clamp((42 - (victim.energy || 0)) / 42, 0, 1.25);
const damageRisk = clamp((damage || 0) / Math.max(2.5, 9 + (victim.energy || 0) * 0.045), 0, 1.4);
const stressRisk = clamp(((victim.stress || 0) - 70) / 70, 0, 0.8);
const chance = clamp(0.025 + damageRisk * 0.23 + healthRisk * 0.34 + stressRisk * 0.10 - aggressionHold * 0.12, 0.015, 0.78);
if (Math.random() >= chance) return false;
victim.defeatedById = attacker.id;
victim.fightWinnerId = attacker.id;
attacker.fightWinnerId = attacker.id;
attacker.defeatedById = null;
victim.fightTimer = Math.min(victim.fightTimer || 0, rand(0.20, 0.42));
attacker.fightTimer = Math.min(attacker.fightTimer || 0, rand(0.20, 0.42));
victim.state = "panic";
victim.target = attacker;
victim.fearTimer = Math.max(victim.fearTimer || 0, 1.1 * (profile.fear || 1));
const dx = victim.x - attacker.x;
const dy = victim.y - attacker.y;
const d = Math.hypot(dx, dy) || 1;
victim.vx += dx / d * rand(38, 76);
victim.vy += dy / d * rand(22, 56);
this.spawnBubble?.(victim.x, victim.y - victim.radius * 1.25, "はぁぁぁ!", "rgba(84,66,86,0.80)");
return true;
},
resolveFightOutcome(winner, loser) {
if (!winner || !loser || winner.dead || loser.dead) return;
const key = `${winner.id}:${loser.id}:${Math.floor(this.time / 2)}`;
@ -442,9 +471,9 @@
if (!it || it.dead || it.type === "water") return;
const isStone = it.type === "stone";
const isGenkotsu = it.type === "genkotsu";
const radius = isGenkotsu ? Math.max(132, (it.r || 88) * 1.74) : (isStone ? 64 : Math.max(34, (it.r || 14) * 1.75));
const radius = isGenkotsu ? Math.max(164, (it.r || 88) * 2.05) : (isStone ? 64 : Math.max(34, (it.r || 14) * 1.75));
const damage = isGenkotsu ? rand(38, 58) : (isStone ? rand(20, 34) : rand(3, 7));
const push = isGenkotsu ? rand(360, 520) : (isStone ? rand(145, 210) : rand(54, 92));
const push = isGenkotsu ? rand(760, 1040) : (isStone ? rand(145, 210) : rand(54, 92));
const reason = isGenkotsu ? "\u3052\u3093\u3053\u3064" : (isStone ? "\u77f3\u304c\u843d\u3061\u305f" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f");
if (isGenkotsu) { it.impactFlash = 1; it.lingerTimer = Math.max(it.lingerDuration || 2, 2); this.triggerGroundShake?.(2.0, 10); }
let hit = 0;
@ -461,8 +490,15 @@
ny = Math.sin(a);
}
t.vx += nx * push * (0.35 + p);
t.vy += ny * push * (0.35 + p) - (isGenkotsu ? 92 : (isStone ? 42 : 18)) * p;
t.damage(damage * (0.35 + p * 0.65), reason);
t.vy += ny * push * (0.35 + p) - (isGenkotsu ? 190 : (isStone ? 42 : 18)) * p;
const dealtDamage = damage * (0.35 + p * 0.65);
t.damage(dealtDamage, reason);
if (isGenkotsu) {
t.fearTimer = Math.max(t.fearTimer || 0, 4.0 + p * 2.2);
t.stress = clamp((t.stress || 0) + 10 + p * 18, 0, 130);
t.adjustPersonality?.("openness", -(0.020 + p * 0.030), "after being struck by genkotsu.");
t.thought = "げんこつがこわい";
}
t.hurtTimer = Math.max(t.hurtTimer, isGenkotsu ? 3.4 : (isStone ? 2.7 : 1.25));
t.fallTimer = Math.max(t.fallTimer, isGenkotsu ? 0.86 : (isStone ? 0.62 : 0.26));
t.fallMax = Math.max(t.fallMax || t.fallTimer, t.fallTimer);
@ -470,9 +506,10 @@
hit += 1;
}
const antHit = this.damageAntsInRadius(it.x, it.y, radius + 54, p => damage * (0.35 + p * 0.65), reason, {
push: p => push * (0.35 + p),
push: p => push * (0.62 + p * 1.25),
});
if (isGenkotsu) {
this.blastZunchiFrom?.(it.x, it.y, radius + 92, 2.75);
for (let r = 0; r < 4; r++) this.effects.push(new Effect("ring", it.x, it.y, { size: 32 + r * 34, life: 0.34 + r * 0.07, color: r % 2 ? "rgba(255,203,54,0.72)" : "rgba(74,57,42,0.42)" }));
for (let i = 0; i < 12; i++) this.effects.push(new Effect("fight", it.x + rand(-38, 38), it.y + rand(-22, 28), { size: rand(12, 22), life: rand(0.25, 0.48), color: "rgba(255,205,46,0.82)" }));
} else {

View file

@ -713,21 +713,14 @@
b.fightTargetIds = b.fightTargetIds.slice(-4);
a.fightTargetId = a.fightTargetIds[0];
b.fightTargetId = b.fightTargetIds[0];
const aAggressiveIntent = a.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
const bAggressiveIntent = b.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
const aScore = a.energy * 0.52 + a.mood * 0.30 + (a.type === "angry" ? 10 : 0) + aAggressiveIntent * 5 + ((a.fightMochiTimer || 0) > 0.04 ? 12 : 0) + (a.isZunchiSlave ? -14 : 0) + rand(-7, 7);
const bScore = b.energy * 0.52 + b.mood * 0.30 + (b.type === "angry" ? 10 : 0) + bAggressiveIntent * 5 + ((b.fightMochiTimer || 0) > 0.04 ? 12 : 0) + (b.isZunchiSlave ? -14 : 0) + rand(-7, 7);
const loser = aScore <= bScore ? a : b;
const winner = loser === a ? b : a;
loser.defeatedById = winner.id;
loser.fightWinnerId = winner.id;
winner.fightWinnerId = winner.id;
winner.defeatedById = null;
winner.defeatedTimer = 0;
a.addStress ? a.addStress(rand(8, 18), { threshold: 8 }) : (a.stress = clamp(a.stress + rand(8, 18), 0, 130));
b.addStress ? b.addStress(rand(8, 18), { threshold: 8 }) : (b.stress = clamp(b.stress + rand(8, 18), 0, 130));
a.damage(b.outgoingDamage ? b.outgoingDamage(rand(2, 7)) : rand(2, 7), "\u55a7\u5629");
b.damage(a.outgoingDamage ? a.outgoingDamage(rand(2, 7)) : rand(2, 7), "\u55a7\u5629");
const damageToA = b.outgoingDamage ? b.outgoingDamage(rand(2, 7)) : rand(2, 7);
const damageToB = a.outgoingDamage ? a.outgoingDamage(rand(2, 7)) : rand(2, 7);
a.damage(damageToA, "\u55a7\u5629");
b.damage(damageToB, "\u55a7\u5629");
this.maybeDefeatFromFightDamage?.(a, b, damageToA);
this.maybeDefeatFromFightDamage?.(b, a, damageToB);
const dx = b.x - a.x, dy = b.y - a.y;
const d = Math.hypot(dx, dy) || 1;
a.vx -= dx / d * rand(32, 62); a.vy -= dy / d * rand(32, 62);

View file

@ -70,10 +70,10 @@
},
phaseName() {
const p = this.dayProgress();
if (p < 0.20) return "\u671d";
if (p < 0.48) return "\u663c";
if (p < 0.68) return "\u5915\u65b9";
const hour = this.dayProgress() * 24;
if (hour >= 5 && hour < 10) return "\u671d";
if (hour >= 10 && hour < 17) return "\u663c";
if (hour >= 17 && hour < 20) return "\u5915\u65b9";
return "\u591c";
},
@ -285,12 +285,14 @@
},
toolSizeFor(type = "") {
if (!["firecracker", "fence_v", "fence_h", "stone", "grass", "food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "zunchi"].includes(type)) return "medium";
const def = typeof itemDefinition === "function" ? itemDefinition(type) : null;
if (!def?.scalable) return "medium";
return (this.toolSizes && this.toolSizes[type]) || this.toolSize || "medium";
},
toolSizeScale(type = "") {
if (!["firecracker", "fence_v", "fence_h", "stone", "grass", "food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "zunchi"].includes(type)) return 1;
const def = typeof itemDefinition === "function" ? itemDefinition(type) : null;
if (!def?.scalable) return 1;
return { small: 0.68, medium: 1.0, large: 1.48 }[this.toolSizeFor(type)] || 1;
},

View file

@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Generate UI icons for item designs that must match js/items.js.
The game is buildless, but these icons are generated snapshots of the field
appearance. Do not invent separate tool-UI-only designs.
"""
from __future__ import annotations
from pathlib import Path
import math
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "assets" / "ui"
def save_icon(name: str, draw_fn) -> None:
im = Image.new("RGBA", (96, 96), (0, 0, 0, 0))
d = ImageDraw.Draw(im, "RGBA")
draw_fn(d)
im.save(OUT / name, "WEBP", lossless=True, quality=95)
def draw_drop(fill, stroke, highlight):
def fn(d: ImageDraw.ImageDraw) -> None:
d.ellipse([18, 60, 78, 74], fill=(60, 45, 35, 32))
# Same horizontal oval as fallen water/zunda/mercury item sprites; rain overlay is separate diagonal lines.
d.ellipse([12, 33, 84, 63], fill=fill, outline=stroke, width=4)
d.ellipse([24, 38, 45, 48], fill=highlight)
return fn
def draw_laxative(d: ImageDraw.ImageDraw) -> None:
d.rounded_rectangle([14, 28, 82, 68], radius=15, fill=(244, 212, 154, 255), outline=(98, 61, 29, 235), width=4)
# Zunchi mound motif inside the tablet.
for box in ([26, 47, 44, 60], [38, 36, 60, 53], [54, 47, 74, 61], [46, 29, 61, 41]):
d.ellipse(box, fill=(96, 70, 38, 230), outline=(56, 39, 22, 130), width=2)
d.arc([22, 27, 72, 50], 205, 330, fill=(255, 255, 255, 135), width=3)
def star_points(cx: float, cy: float, r1: float, r2: float, n: int = 5, rot: float = -math.pi / 2):
return [(cx + math.cos(rot + i * math.pi / n) * (r1 if i % 2 == 0 else r2), cy + math.sin(rot + i * math.pi / n) * (r1 if i % 2 == 0 else r2)) for i in range(n * 2)]
def draw_mystery(d: ImageDraw.ImageDraw) -> None:
d.ellipse([18, 23, 78, 73], fill=(243, 236, 255, 255), outline=(91, 55, 150, 240), width=5)
try:
from PIL import ImageFont
font = ImageFont.truetype("DejaVuSans-Bold.ttf", 44)
except Exception:
font = None
d.text((48, 48), "?", anchor="mm", fill=(91, 55, 150, 245), font=font)
d.arc([25, 24, 74, 54], 205, 330, fill=(255, 255, 255, 135), width=3)
def draw_bullet(d: ImageDraw.ImageDraw) -> None:
d.ellipse([20, 62, 76, 77], fill=(60, 45, 35, 38))
pts = [(76, 48), (60, 28), (27, 28), (16, 35), (16, 61), (27, 68), (60, 68)]
d.polygon(pts, fill=(244, 196, 106, 255), outline=(87, 51, 22, 240))
d.line(pts + [pts[0]], fill=(87, 51, 22, 240), width=4)
d.rectangle([16, 35, 29, 61], fill=(176, 111, 35, 255), outline=(87, 51, 22, 220), width=3)
d.line([(34, 36), (64, 42)], fill=(255, 237, 176, 170), width=4)
d.line([(34, 60), (58, 56)], fill=(142, 82, 30, 110), width=3)
def draw_sleep(d: ImageDraw.ImageDraw) -> None:
d.ellipse([17, 28, 79, 68], fill=(235, 228, 255, 255), outline=(74, 60, 144, 230), width=4)
d.ellipse([41, 31, 65, 56], fill=(112, 91, 188, 220))
d.ellipse([52, 27, 74, 52], fill=(235, 228, 255, 255))
d.arc([24, 27, 74, 57], 200, 330, fill=(255, 255, 255, 190), width=3)
def main() -> None:
save_icon("tool_laxative.webp", draw_laxative)
save_icon("tool_mystery_drug.webp", draw_mystery)
save_icon("tool_ammo.webp", draw_bullet)
save_icon("tool_sleep_drug.webp", draw_sleep)
save_icon("tool_water.webp", draw_drop((86, 157, 224, 220), (45, 101, 168, 150), (255, 255, 255, 150)))
save_icon("tool_zunda_juice.webp", draw_drop((125, 218, 82, 235), (63, 150, 50, 160), (238, 255, 226, 160)))
save_icon("tool_mercury.webp", draw_drop((210, 216, 222, 240), (112, 122, 132, 165), (255, 255, 255, 170)))
if __name__ == "__main__":
main()

View file

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