diff --git a/AI_MAP.md b/AI_MAP.md new file mode 100644 index 0000000..78a68e1 --- /dev/null +++ b/AI_MAP.md @@ -0,0 +1,58 @@ +# tarinai_ AI map + +Purpose: static Japanese browser simulation/game. Optimize context by reading this file only after `README.md`, then open the relevant subsystem files. For exact file expansion, use `scripts/ai_inventory.ps1` or `scripts/ai_inventory.py`. + +## Architecture + +- Entrypoint: `index.html` defines the app shell, canvas, dialogs, right panels, and exact script order. +- Runtime style: plain browser JS, global IIFEs, exports on `window`, no bundler/module loader. +- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.15.47`. +- Main loop: `js/main.js` initializes assets/audio/world/UI/save, then drives update/render. +- Update order: `js/system_order.js` calls phase facades from `js/world_update_phases.js` / `js/simulation_systems.js`. +- Command path: UI/input emits command objects; `js/command_dispatcher.js` routes tool, pointer, selection, save, reset, and UI intents. +- Persistence: save files are schema/codec/storage/coordinator modules; snapshot/history modules keep world restoration and stats consistent. + +## Subsystem Map + +- Core/bootstrap: `js/version.js`, `js/event_bus.js`, registries, `js/data.js`, `js/ground_types.js`, `js/health.js`, `js/main.js`, `js/debug_tools.js`, `js/perf_profiler.js`. +- Rendering/assets/audio: `js/assets.js`, `js/audio.js`, `js/render.js`, `js/tarinai_render.js`, `js/item_render_runtime.js`, `js/sound_pack.js`. +- Simulation phases: `js/sim_core.js`, `js/simulation*.js`, `js/world_update*.js`, `js/system_order.js`. +- World domain: `js/world*.js`, `js/weather_system.js`; covers state, camera/view, environment, combat/effects, placement/history, tools, family/social, ants, spatial budgets. +- Creature domain: `js/tarinai*.js`; covers state, identity, needs, actions, forced behavior, social life, item effects, disease/nests, movement/update pipeline, rendering. +- Item/structure domain: `js/item*.js`, `js/items.js`, `js/structures.js`, `js/structure_lifecycle.js`; covers definitions, spawning defaults, lifecycle, dynamic behavior, rendering. +- Physics/mechanics: `js/physics*.js`, `js/mechanical_system.js`, `js/constraint_system.js`, `js/collision_footprint_system.js`. +- UI/input: `js/ui*.js`, `js/text_catalog.js`, `js/command_dispatcher.js`; covers panels, tools, selected creature, logs, charts, family tree, mouse/touch, dialogs, labels. +- Family tree: `js/family_graph.js`, `js/ui_family_*.js`; data transform, layout, paths, async rendering, validation. +- Save/history: `js/save*.js`, `js/snapshot_system.js`, `js/restore_coordinator.js`, `js/history_system.js`. +- Scripts: `scripts/generate_app_files.py`, `scripts/generate_item_icons.py`, `scripts/regression_check.py`, `scripts/ai_inventory.py`, `scripts/ai_inventory.ps1`. + +## File Pattern Semantics + +- `*_pipeline.js`: ordered runner/facade for update or lifecycle work. +- `*_step_*.js`: narrow pipeline step wrapper. +- `*_system.js`: domain service, phase service, or compatibility facade. +- `*_runtime.js`: stable runtime-facing facade around split modules. +- `*_registry.js`: definition registry and lookup/normalization helpers. +- `world_*`: world-owned behavior; `tarinai_*`: creature-owned behavior; `item_*`: item-owned behavior; `ui_*`: DOM/input/panel behavior. +- `assets/sprites/tarinai_*.webp`: creature visual state sprites; numeric prefix is stable asset ID/order. +- `assets/ui/tool_*`: tool palette icons; `assets/ui/favicon*`, `apple-touch-icon`, `ecology_*` are app/help icons. +- `assets/objects/*`: field object sprites for ants, zunchi, pushpin, oshibyo, genkotsu, plushie. +- `assets/sounds/voice_*`: creature voice samples; `shoot_*`, `major_damage_*`, `firecracker_*` are SFX. + +## Task-Oriented Read Sets + +- UI/layout bug: `index.html`, relevant `css/*.css`, `js/ui_bind.js`, `js/ui_layout_dialogs.js`, matching `js/ui_*.js`. +- Tool behavior: `js/command_dispatcher.js`, `js/world_tool_actions.js`, `js/item_registry.js`, relevant `js/item_*` or `js/tarinai_item_*`. +- Creature behavior: `js/tarinai.js`, `js/tarinai_update_pipeline.js`, relevant `js/tarinai_*`, `js/system_order.js`. +- World/environment/combat: matching `js/world_*`, `js/simulation_*`, `js/system_order.js`. +- Save/load: `js/save_schema.js`, `js/save_codec.js`, `js/save_system.js`, `js/snapshot_system.js`, `js/restore_coordinator.js`. +- Family tree: `js/world_family_social.js`, `js/family_graph.js`, `js/ui_family_*.js`. +- Static app/cache/version: `app_manifest.json`, `scripts/generate_app_files.py`, `service-worker.js`, `js/version.js`, `index.html`. + +## Invariants + +- Preserve `index.html` script order unless all dependent manifests/generated files are updated together. +- Keep `app_manifest.json`, `js/version.js`, `service-worker.js`, and cache query versions synchronized. +- Keep global exports for compatibility with regression checks and browser runtime. +- Prefer focused edits in the owning subsystem; split modules are intentional compatibility boundaries. +- Run `node --check js/*.js` and `python scripts/regression_check.py` for behavior changes. diff --git a/FILES.json b/FILES.json new file mode 100644 index 0000000..3e7d929 --- /dev/null +++ b/FILES.json @@ -0,0 +1,91 @@ +{ + "purpose": "Token-efficient machine-readable routing for tarinai_. Use scripts/ai_inventory.py for exact current file expansion.", + "version": "39.15.47", + "entrypoints": { + "app": "index.html", + "runtime": "js/main.js", + "update_order": "js/system_order.js", + "manifest": "app_manifest.json", + "service_worker": "service-worker.js" + }, + "read_strategy": { + "default": ["README.md"], + "architecture": ["README.md", "AI_MAP.md"], + "exact_inventory": ["powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All", "python scripts/ai_inventory.py --all"], + "machine_routing": ["FILES.json"] + }, + "groups": [ + { + "id": "root", + "patterns": [".gitignore", ".htaccess", "app_manifest.json", "favicon.ico", "index.html", "README.md", "AI_MAP.md", "FILES.json", "service-worker.js"], + "meaning": "Repo/app shell metadata, server/static cache config, docs, manifest, entrypoint." + }, + { + "id": "css", + "patterns": ["css/*.css"], + "meaning": "Base, layout, panel, component, and mobile responsive styling." + }, + { + "id": "scripts", + "patterns": ["scripts/*.py", "scripts/*.ps1"], + "meaning": "Generators, regression checks, and token-aware AI inventory tooling." + }, + { + "id": "core", + "patterns": ["js/version.js", "js/event_bus.js", "js/registry_base.js", "js/disease_registry.js", "js/sound_pack.js", "js/data.js", "js/ground_types.js", "js/health.js", "js/input_mode_manager.js", "js/main.js", "js/math.js", "js/patch_helpers.js", "js/perf_profiler.js", "js/debug_tools.js"], + "meaning": "Bootstrap, shared constants/helpers, registries, config, profiling/debug." + }, + { + "id": "assets_runtime", + "patterns": ["js/assets.js", "js/audio.js", "js/render.js", "js/tarinai_render.js", "js/item_render_runtime.js"], + "meaning": "Image/audio loading and canvas rendering." + }, + { + "id": "simulation", + "patterns": ["js/sim_core.js", "js/simulation*.js", "js/system_order.js"], + "meaning": "Simulation primitives, concrete systems, phase ordering." + }, + { + "id": "world", + "patterns": ["js/world*.js", "js/weather_system.js"], + "meaning": "World state/view/update/tools/environment/combat/family/social/ants." + }, + { + "id": "physics", + "patterns": ["js/physics*.js", "js/mechanical_system.js", "js/constraint_system.js", "js/collision_footprint_system.js"], + "meaning": "Geometry, collisions, constraints, mechanical item behavior." + }, + { + "id": "items", + "patterns": ["js/item*.js", "js/items.js", "js/structures.js", "js/structure_lifecycle.js"], + "meaning": "Item/structure registry, lifecycle, dynamic behavior, rendering." + }, + { + "id": "tarinai", + "patterns": ["js/tarinai*.js", "js/ants.js"], + "meaning": "Creature state/actions/needs/social life/item effects/disease/rendering plus ant actor model." + }, + { + "id": "save", + "patterns": ["js/save*.js", "js/snapshot_system.js", "js/restore_coordinator.js", "js/history_system.js"], + "meaning": "Snapshot schema/codec/storage/restore/history." + }, + { + "id": "ui", + "patterns": ["js/ui*.js", "js/text_catalog.js", "js/command_dispatcher.js", "js/family_graph.js"], + "meaning": "DOM UI, input, tools, selected panel, logs, charts, family tree, text labels, command routing." + }, + { + "id": "assets", + "patterns": ["assets/sprites/tarinai_*.webp", "assets/ui/tool_*", "assets/ui/favicon*", "assets/ui/apple-touch-icon.png", "assets/ui/ecology_*.webp", "assets/objects/*", "assets/sounds/*"], + "meaning": "Creature sprites, UI/tool icons, field object images, voice/SFX samples." + } + ], + "conventions": { + "*_pipeline.js": "ordered runner/facade", + "*_step_*.js": "pipeline stage wrapper", + "*_system.js": "domain service or compatibility facade", + "*_runtime.js": "stable runtime-facing facade", + "*_registry.js": "definition lookup/normalization" + } +} diff --git a/README.md b/README.md index 08b101d..36d7f2d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,82 @@ -# tarinai v39.15.39 +# tarinai_ -## 変更点 +AI-first entrypoint for a static browser simulation/game. Keep this file small; read deeper docs only when needed. -- PC表示を再度ロールバックし、PC側の変更はクイックスクロールの「本体」「道具」ボタン非表示に限定。道具UI本体は維持。 -- PC上部ボタンを右詰め・やや大きめに調整し、ゲームタイトルがボタンに押されて省略されないように修正。 -- スマホ用のスクロールスナップ系指定を無効化し、通常スクロールを妨げないように修正。 -- スマホで「家系図」へ移動したとき、更新OFFでも手動ジャンプ扱いで現在の家系図を描画してからスクロールするように修正。 -- スマホの自動スクロールボタンは、初期折り畳み部分を開いてから目的地へ移動。ユーザーが手動で閉じた部分は開かない挙動を維持。 -- たりない/道具系ツールチップの最大幅・折り返し・画面端補正を維持。 -- 「333の部屋に移動」のリンク先は `https://2012r2.nishi.boats/~333/`。 -- 「観察」は「たりないデータ」の表記を維持。 +## Read Strategy -## 確認 +- Start here for architecture, script order constraints, and where to look next. +- For subsystem work, read `AI_MAP.md` plus only the matching files. +- For exact file inventory, run `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All` or narrow it: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 ui`. Python equivalent: `python scripts/ai_inventory.py --all`. +- For machine-readable routing, read `FILES.json`. +- Avoid loading all assets into context; asset filenames are mostly self-describing and grouped by glob. -- `node --check js/*.js` passed -- `python3 scripts/regression_check.py` passed -- version synchronized: `39.15.39` +## Runtime Shape + +- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.15.47`. +- Mutable runtime lives on `window`; primary entities are `World`, `Tarinai`, items, structures, ants, family graph, weather, and save snapshots. +- Update loop: `main.js` creates the world/UI loop; `World.update` delegates to `TarinaiSystemOrder.update`; `system_order.js` runs named phases. +- UI loop: pointer/tool/UI intents go through `command_dispatcher.js`; canvas drawing is in render modules; side panels are `ui_*.js`. +- Generated/static app shell: `app_manifest.json`, `service-worker.js`, and `js/version.js` must stay version-aligned. + +## File Routing + +- `index.html`: DOM shell, CSS/JS load order, panels/dialogs/canvas. +- `css/*.css`: base/layout/panel/components/mobile styling. +- `js/world*.js`: world state, view, update phases, tools, placement, combat, environment, family/social, ants. +- `js/tarinai*.js`: creature state, needs, actions, behavior, social life, item effects, update pipeline, rendering. +- `js/item*.js`, `js/structures*.js`: item registry/runtime/lifecycle/dynamic behavior/rendering plus structures. +- `js/simulation*.js`, `js/system_order.js`, `js/sim_core.js`: concrete update systems and phase ordering. +- `js/ui*.js`, `js/text_catalog.js`, `js/command_dispatcher.js`: DOM UI, input, selected panel, logs, charts, family tree, tools, labels. +- `js/save*.js`, `js/snapshot_system.js`, `js/restore_coordinator.js`, `js/history_system.js`: persistence and history. +- `js/physics*.js`, `js/mechanical_system.js`, `js/constraint_system.js`, `js/collision_footprint_system.js`: geometry, mechanics, constraints, collisions. +- `assets/sprites/tarinai_*.webp`: creature state sprites. +- `assets/ui/tool_*`: tool palette icons; other `assets/ui/*` are app/help icons. +- `assets/objects/*`: field object images. +- `assets/sounds/*`: voice and SFX samples. +- `scripts/*`: generators, regression checks, and AI inventory. + +## Conventions + +- `*_pipeline` = ordered runner/facade. +- `*_step_*` = pipeline stage wrapper. +- `*_system` = domain service or compatibility facade. +- `*_runtime` = stable runtime-facing facade. +- `*_registry` = definition lookup/normalization. +- Many split files preserve regression contracts; do not collapse them casually. + +## Checks + +- Syntax: `node --check js/*.js` +- Regression: `python scripts/regression_check.py` +- Inventory sanity: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All` + + +## v39.15.47 UI media isolation / scroll repair + +- PC / smartphone UI override rules are separated into `@media (min-width: 981px)` and `@media (max-width: 980px)`. +- PC family tree card is restored below the main game layout, not inside the right menu. +- Smartphone manual scroll no longer uses internal vertical scroll traps for event log / family tree content. +- Auto-scroll does not open collapsed cards. + + +## v39.15.47 event/family/oshibyo UI fixes + +- Event UI hides direct player item placement records while keeping those records in the internal log. +- Collapsed Event UI no longer leaves a large blank area from the log-card min-height. +- Family tree nodes show inherited stats in a 2x2 grid and move the tarinai portrait to the upper-right. +- Oshibyo lodged position is anchored to the left side of the tarinai sprite. + + +## v39.15.47 family / sprinkler polish + +- Family tree generation row spacing is tightened. +- Family tree personality tags also use a 2x2 grid (up to 4 visible tags). +- Sprinkler effective cleaning radius is increased to 3x the previous value and still filters targets by actual distance. +- Sprinkler rendering is redesigned to a more realistic yard-sprinkler look and now emits water-like spray effects when active. + + +## v39.15.47 owned object panic / shoot effect + +- Tarinai panic when their own plushie or simple grass bed is destroyed. +- The selected Tarinai data behavior text explicitly shows the owned-object panic reason. +- Shooting impact effects are larger and remain visible for slightly longer. diff --git a/SENSOR_IDEAS.md b/SENSOR_IDEAS.md deleted file mode 100644 index 59d2b36..0000000 --- a/SENSOR_IDEAS.md +++ /dev/null @@ -1,43 +0,0 @@ -# センサー系アイデア - -実験観察ゲームとして、直接介入ではなく「測る」ための道具案。 - -## 低コストで入れやすいもの - -| センサー | 役割 | 記録する値 | 観察上の使い道 | -|---|---|---|---| -| ストレスセンサー | 範囲内の緊張度を測る | 平均ストレス / 最大ストレス / 急上昇回数 | 地形・混雑・病気・王者個体の影響を見る | -| 通過カウンター | 置いた地点を通った回数を数える | 通過数 / 個体別通過数 | 道・縄張り・回避行動の発見 | -| 滞在センサー | 範囲内にいた時間を測る | 滞在秒数 / 個体ID / 世代 | 好まれる場所・避けられる場所の比較 | -| けんかセンサー | 範囲内の争いを記録 | 勝者 / 敗者 / 勝率変化 | 王者候補・危険地帯の検出 | -| 汚染センサー | ずんち・病気・汚れを見る | 汚染量 / 増加量 / 感染者数 | スプリンクラーや隔離の効果検証 | -| 出生センサー | 近くで生まれた個体を記録 | 親 / 子 / 遺伝値 | 繁殖条件・血統観察 | - -## 中コストのセンサー - -| センサー | 仕様 | -|---|---| -| 個体追跡ビーコン | 特定個体に付ける。移動経路、接触相手、食事、喧嘩、睡眠を時系列で記録する。 | -| 関係センサー | 範囲内で友好・敵対が増減した時だけログを残す。群れの分裂や王者の圧力を見やすくする。 | -| 遺伝スキャナー | 選択個体の寿命・サイズ・攻撃・速度の遺伝倍率を読み取る。家系図の補助UIにも使える。 | -| 行動理由センサー | 近くの個体が「なぜその行動を選んだか」を短く記録する。空腹、恐怖、王者回避、繁殖欲求など。 | -| 死因センサー | 範囲内の死亡時に、直前の状態・接触相手・地面・病気・道具をまとめる。事故検証用。 | - -## 高コストだが強いもの - -| センサー | 仕様 | -|---|---| -| ヒートマップ装置 | ストレス、滞在、喧嘩、死亡、汚染、出生を色で重ねて表示する。 | -| A/B実験レコーダー | 2つの区画に別条件を設定し、生存数・出生数・平均寿命・王者発生率を比較する。 | -| 血統観察端末 | 家系ごとの平均寿命、平均サイズ、勝率、移動速度、病気耐性をランキング表示する。 | -| 社会構造センサー | 群れの中心個体、孤立個体、王者、敵対ネットワークをグラフ化する。 | - -## 特に相性が良い実装順 - -1. 通過カウンター -2. ストレスセンサー -3. けんかセンサー -4. 遺伝スキャナー -5. ヒートマップ装置 - -最初は「画面内に結果を出す道具」ではなく、ログとグラフに測定値を流すだけでも十分に実験感が出る。 diff --git a/app_manifest.json b/app_manifest.json index 06bddab..f625184 100644 --- a/app_manifest.json +++ b/app_manifest.json @@ -1,5 +1,5 @@ { - "version": "39.15.39", + "version": "39.15.47", "css": [ "css/base.css", "css/layout.css", diff --git a/css/components.css b/css/components.css index 6eed2b6..0e998ff 100644 --- a/css/components.css +++ b/css/components.css @@ -801,3 +801,9 @@ max-width: min(280px, calc(100vw - 24px)); } } + +/* v39.15.47: collapsed event card must shrink to its header. + layout.css gives .log-card a min-height, which left a large blank panel when folded. */ +.collapsible-card.collapsed { min-height: 0 !important; } +.log-card.collapsed { min-height: 0 !important; height: auto !important; flex: 0 0 auto !important; flex-basis: auto !important; } +.log-card.collapsed .panel-card-body { display: none !important; } diff --git a/css/mobile.css b/css/mobile.css index a3c1f51..877dd25 100644 --- a/css/mobile.css +++ b/css/mobile.css @@ -59,7 +59,7 @@ .sound-panel input:disabled + span { opacity: .55; } .sound-master { font-weight: 700; } .sound-panel-divider { height: 1px; background: rgba(84,68,48,.12); margin: 2px 0; } -@media (max-width: 760px) { .sound-panel { right: auto; left: 0; } } +@media (max-width: 980px) { .sound-panel { right: auto; left: 0; } } /* Collapsed card sizing is shared by the base panel rules; no event-only shrink override. */ @@ -363,7 +363,7 @@ body.touch-mobile-ui .tool-tip-popover { } -/* v39.15.39: desktop rollback guard. Keep the normal PC layout; only remove duplicated quick-scroll buttons. */ +/* v39.15.47: desktop rollback guard. Keep the normal PC layout; only remove duplicated quick-scroll buttons. */ @media (min-width: 981px) { .topbar { display: flex; @@ -410,19 +410,281 @@ body.touch-mobile-ui .tool-tip-popover { } } -/* v39.15.39: mobile scroll must remain manual; disable any CSS scroll snapping. */ +/* v39.15.47: PC / smartphone UI isolation. All override UI placement is scoped by width. */ +@media (min-width: 981px) { + html, + body { + overflow-x: hidden; + overflow-y: auto; + } + #app { + width: min(1480px, 100vw); + padding: 18px; + overflow: visible; + } + .topbar { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 14px 18px; + } + .top-left { + flex: 1 0 auto; + min-width: max-content; + max-width: 100%; + } + .brand, + .brand h1, + h1 { + flex: 0 0 auto; + min-width: max-content; + max-width: none !important; + overflow: visible !important; + text-overflow: clip !important; + } + .top-actions { + flex: 1 1 560px; + margin-left: auto; + justify-content: flex-end; + align-items: center; + gap: 10px; + } + .top-actions .btn, + .top-actions .sound-menu-btn, + .room-link { + min-height: 42px; + padding: 10px 16px; + font-size: 13px; + line-height: 1.15; + } + .layout { + display: grid; + grid-template-columns: minmax(680px, 1fr) minmax(320px, 390px); + align-items: start; + } + .panel { + position: sticky; + top: 12px; + align-self: start; + max-height: calc(100vh - 24px); + overflow-y: auto; + overflow-x: hidden; + } + .panel-quick-tabs { + position: sticky; + top: 0; + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + .panel-quick-tabs [data-panel-tab="game"], + .panel-quick-tabs [data-panel-tab="tools"] { + display: none !important; + } + .lineage-main-card { + display: block; + visibility: visible; + position: relative; + z-index: 1; + width: auto; + margin: 16px 0 0; + overflow: hidden; + contain: layout paint; + } + .lineage-main-content { + height: min(78vh, 980px); + min-height: 520px; + max-height: calc(100vh - 120px); + overflow: auto; + overscroll-behavior: auto; + } +} + @media (max-width: 980px) { - html, body, #app, .layout, .stage-wrap, .panel, .card, .lineage-main-card { - scroll-snap-type: none !important; - scroll-snap-align: none !important; - scroll-snap-stop: normal !important; + html, + body { + width: 100%; + max-width: 100%; + min-height: 100%; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior-y: auto; + } + #app { + width: 100%; + max-width: 100%; + padding: 10px max(10px, env(safe-area-inset-right, 0px)) calc(env(safe-area-inset-bottom, 0px) + 104px) max(10px, env(safe-area-inset-left, 0px)); + overflow-x: clip; + overflow-y: visible; + } + .topbar, + .top-left, + .brand, + .layout, + .stage-wrap, + .panel, + .card, + .lineage-main-card { + min-width: 0; + max-width: 100%; + box-sizing: border-box; + } + .topbar { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 8px; + } + .top-left { + display: flex; + align-items: center; + gap: 8px; + } + .brand h1, + h1 { + max-width: 100%; + overflow: visible; + text-overflow: clip; + white-space: nowrap; + font-size: clamp(20px, 7vw, 30px); + } + .top-actions { + display: flex; + flex-wrap: nowrap; + justify-content: flex-start; + gap: 5px; + width: 100%; + overflow-x: auto; + overflow-y: hidden; + padding: 1px 0 5px; + -webkit-overflow-scrolling: touch; + } + .top-actions > .btn, + .top-actions > .sound-menu, + .top-actions .sound-menu-btn { + flex: 0 0 auto; + white-space: nowrap; + } + .top-actions .btn, + .top-actions .sound-menu-btn { + min-height: 34px; + padding: 7px 9px; + font-size: 11px; + } + .layout { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 10px; + position: relative; + z-index: 2; + overflow: visible; + } + .stage-wrap { + position: relative; + width: 100%; + overflow: hidden; + } + .panel { + position: relative !important; + top: auto !important; + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + max-height: none !important; + height: auto !important; + overflow: visible !important; + isolation: auto; + } + body.touch-mobile-ui .panel-quick-tabs, + .panel-quick-tabs { + position: fixed; + left: max(8px, env(safe-area-inset-left, 0px)); + right: max(8px, env(safe-area-inset-right, 0px)); + bottom: calc(env(safe-area-inset-bottom, 0px) + 8px); + top: auto; + z-index: 12002; + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 4px; + width: auto; + max-width: none; + margin: 0; + padding: 6px; + border: 1px solid rgba(102, 169, 215, 0.34); + border-radius: 16px; + background: rgba(244, 250, 247, 0.95); + box-shadow: 0 12px 28px rgba(42, 54, 64, 0.18); + } + .panel-quick-tabs button { + min-height: 34px; + padding: 5px 2px; + font-size: 10px; + } + body.touch-mobile-ui .mobile-mode-controls:not([hidden]) { + display: flex !important; + position: absolute; + left: 50%; + right: auto; + bottom: 10px; + transform: translateX(-50%); + z-index: 24; + max-width: calc(100% - 18px); + } + body.touch-mobile-ui.mobile-mode-auto #gameCanvas { + touch-action: pan-y pinch-zoom !important; + } + body.touch-mobile-ui.mobile-mode-camera #gameCanvas, + body.touch-mobile-ui.mobile-mode-tool #gameCanvas { + touch-action: none !important; + } + .log { + max-height: none !important; + height: auto !important; + overflow: visible !important; + overscroll-behavior-y: auto !important; } .lineage-main-card { display: block !important; visibility: visible !important; - margin-bottom: calc(env(safe-area-inset-bottom, 0px) + 132px); + position: relative; + z-index: 1; + width: 100%; + margin: 10px 0 calc(env(safe-area-inset-bottom, 0px) + 132px); + overflow: visible !important; + contain: none !important; + clear: both; } .lineage-main-content { + display: block !important; + height: auto !important; min-height: 360px; + max-height: none !important; + overflow-x: hidden !important; + overflow-y: visible !important; + overscroll-behavior-y: auto !important; + -webkit-overflow-scrolling: auto; + touch-action: pan-y pinch-zoom; + } + .lineage-family-tree { + max-width: 100%; + overflow-x: auto !important; + overflow-y: visible !important; + overscroll-behavior-x: contain; + overscroll-behavior-y: auto; + -webkit-overflow-scrolling: touch; + touch-action: pan-x pan-y pinch-zoom; + } + .lineage-family-tree .lineage-tree-panel { + max-width: none; + } + .selected-card, + .tool-card, + .colony-card, + .log-card, + .lineage-main-card, + .stage-wrap { + scroll-margin-top: 0 !important; + scroll-margin-bottom: calc(env(safe-area-inset-bottom, 0px) + 104px); } } diff --git a/css/panel.css b/css/panel.css index 81b8109..6cd69fd 100644 --- a/css/panel.css +++ b/css/panel.css @@ -121,19 +121,39 @@ body::before { } .family-genetic-stats { - display: block; + display: grid !important; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1px 5px; margin-top: 2px; font-size: 9px; - line-height: 1.16; + line-height: 1.12; color: rgba(88, 70, 48, 0.86); letter-spacing: -0.02em; white-space: normal; - max-width: 104px; + max-width: 82px; } -.family-genetic-stats b { +.family-genetic-stats span { + display: inline-flex; + gap: 1px; + min-width: 0; + white-space: nowrap; +} +.family-genetic-stats b, +.family-fight-stat b { font-weight: 800; color: rgba(120, 86, 30, 0.92); } +.family-fight-stat { + display: block; + margin-top: 1px; + max-width: 82px; + font-size: 9px; + line-height: 1.12; + color: rgba(88, 70, 48, 0.82); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} .lineage-family-tree .tree-node.champion { border-color: rgba(202, 146, 33, 0.58); box-shadow: inset 0 0 0 1px rgba(247, 200, 64, 0.30), 0 3px 8px rgba(120, 78, 12, 0.09); @@ -173,18 +193,19 @@ body::before { color: rgba(126, 80, 64, 0.92); } .family-personality-tags { - display: flex !important; + display: grid !important; + grid-template-columns: repeat(2, minmax(0, 1fr)); overflow: visible !important; text-overflow: clip !important; - flex-wrap: wrap; - gap: 2px; + gap: 2px 4px; margin-top: 2px; white-space: normal !important; - overflow: visible; max-height: none; + max-width: 84px; } .family-personality-tags em { - display: inline-block; + display: block; + min-width: 0; max-width: none; padding: 1px 4px; border-radius: 999px; @@ -194,8 +215,8 @@ body::before { font-size: 7.5px; font-style: normal; line-height: 1.25; - overflow: visible; - text-overflow: clip; + overflow: hidden; + text-overflow: ellipsis; white-space: nowrap; } .family-copy strong { font-size: 11px; } @@ -407,10 +428,11 @@ body::before { transform: none; } .lineage-family-tree .tree-node .family-portrait { - right: 0; - bottom: -6px; - width: 58px; - height: 58px; + right: 4px; + top: 4px; + bottom: auto; + width: 48px; + height: 48px; } .lineage-family-tree .tree-generation-label { position: absolute; @@ -450,3 +472,11 @@ body::before { .tool[data-tip] { position: relative; } + +.lineage-family-tree .tree-node .family-copy { + max-width: calc(100% - 46px); +} +.lineage-family-tree .tree-node .family-copy strong, +.lineage-family-tree .tree-node .family-copy > span:not(.family-genetic-stats):not(.family-fight-stat):not(.family-personality-tags) { + max-width: 100%; +} diff --git a/index.html b/index.html index 6ef0b14..d5ecc3d 100644 --- a/index.html +++ b/index.html @@ -7,11 +7,11 @@ - - - - - + + + + +
@@ -123,6 +123,10 @@
+ + + +
@@ -134,8 +138,7 @@
- - + @@ -193,141 +196,141 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/item_dynamic_pin_system.js b/js/item_dynamic_pin_system.js index ba96071..febcf4a 100644 --- a/js/item_dynamic_pin_system.js +++ b/js/item_dynamic_pin_system.js @@ -89,10 +89,10 @@ item.deletable = false; item.pinTargetId = t.id; const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1); - const visualSide = isOshibyo ? -faceDir : faceDir; + const visualSide = isOshibyo ? -1 : faceDir; item.pinVisualSide = visualSide; - item.pinAttachAngle = isOshibyo ? visualSide * 0.36 : Math.atan2(dy || -1, dx || visualSide); - item.pinAttachDistance = isOshibyo ? (t.radius || 16) * 0.72 : clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74); + item.pinAttachAngle = isOshibyo ? -0.46 : Math.atan2(dy || -1, dx || visualSide); + item.pinAttachDistance = isOshibyo ? (t.radius || 16) * 0.88 : clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74); item.pinOffsetY = isOshibyo ? (t.radius || 16) * 0.40 : clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38); item.pinDamageTick = 0; item.pinFallCheckTimer = 0; @@ -138,13 +138,13 @@ t.stuckPushpinId = item.id; item.deletable = false; const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1); - const visualSide = isOshibyo ? -faceDir : faceDir; + const visualSide = isOshibyo ? -1 : faceDir; item.pinVisualSide = visualSide; - item.x = isOshibyo ? t.x + visualSide * (t.radius || 16) * 0.72 : t.x + visualSide * (t.radius || 16) * 0.36; + item.x = isOshibyo ? t.x - (t.radius || 16) * 0.88 : t.x + visualSide * (t.radius || 16) * 0.36; item.y = isOshibyo ? t.y + (t.radius || 16) * 0.42 : t.y - (t.radius || 16) * 0.04; item.prevX = item.x; item.prevY = item.y; - item.spin = visualSide * (isOshibyo ? 0.52 : 0.28); + item.spin = isOshibyo ? -0.52 : visualSide * 0.28; item.vx = t.vx || 0; item.vy = t.vy || 0; if (isOshibyo) { diff --git a/js/item_dynamic_tool_system.js b/js/item_dynamic_tool_system.js index aaaad7f..38f1658 100644 --- a/js/item_dynamic_tool_system.js +++ b/js/item_dynamic_tool_system.js @@ -56,7 +56,27 @@ function sprinklerRange(item) { - return Math.max(96, (Number(item?.r || 28) || 28) * 4.6); + // v39.15.47: effective cleaning radius increased to 3x the prior radius. + // Previous formula: max(96, r * 4.6). Current: max(288, r * 13.8). + return Math.max(288, (Number(item?.r || 28) || 28) * 13.8); + } + + function spawnSprinklerWaterEffects(item, worldRef, range, flashStrength = 1) { + if (!worldRef?.effects || !item) return; + const burst = 8; + for (let i = 0; i < burst; i++) { + const base = (Math.PI * 2 * i) / burst + (Number(item.sprinklerSpin || 0) || 0) * 0.35; + const dist = Math.min(range * 0.55, 46 + i * 6); + const px = item.x + Math.cos(base) * dist; + const py = item.y + Math.sin(base) * dist * 0.72 - 6; + worldRef.effects.push(new Effect("ring", px, py, { + size: 6 + (i % 3) * 2, + life: 0.16 + (i % 2) * 0.05, + color: `rgba(120,204,245,${0.28 + 0.12 * flashStrength})`, + vx: Math.cos(base) * 8, + vy: Math.sin(base) * 6 - 10, + })); + } } function updateSprinkler(item, dt, worldRef) { @@ -77,9 +97,10 @@ if (distXY(item.x, item.y, it.x, it.y) <= range + r) targets.push(it); } const sprayAngle = global.deterministicAngle ? global.deterministicAngle(worldRef, "sprinkler-spray-angle", item) : Math.random() * Math.PI * 2; - const sprayX = item.x + Math.cos(sprayAngle) * Math.min(range * 0.42, 86); - const sprayY = item.y + Math.sin(sprayAngle) * Math.min(range * 0.42, 86); + const sprayX = item.x + Math.cos(sprayAngle) * Math.min(range * 0.42, 132); + const sprayY = item.y + Math.sin(sprayAngle) * Math.min(range * 0.42, 132); worldRef.effects?.push(new Effect("ring", sprayX, sprayY, { size: 14, life: 0.18, color: "rgba(95,166,230,0.22)" })); + spawnSprinklerWaterEffects(item, worldRef, range, 0.8); if (!targets.length) return true; const pickIndex = Math.floor((global.deterministicUnit ? global.deterministicUnit(worldRef, "sprinkler-pick", item, targets.length) : Math.random()) * targets.length) % targets.length; const target = targets[pickIndex]; @@ -87,8 +108,10 @@ target.amount = 0; target.hp = 0; target.removedReason = "sprinkler-clean"; - item.sprinklerFlash = 0.45; + item.sprinklerFlash = 0.55; + item.sprinklerJustActivated = 0.22; worldRef.effects?.push(new Effect("ring", target.x, target.y, { size: Math.max(18, (target.r || 12) * 1.6), life: 0.28, color: "rgba(95,166,230,0.34)" })); + spawnSprinklerWaterEffects(item, worldRef, range, 1); worldRef.markItemBucketsDirty?.("sprinkler-clean"); worldRef.markSpatialDirty?.("sprinkler-clean"); worldRef.updateItemCounts?.(); @@ -123,6 +146,7 @@ updateReciprocator, updatePoisonBlock, updateSprinkler, + sprinklerRange, updateFlexibleLink, updateRigidLink, resolveMechanicalInteractions, diff --git a/js/item_render_runtime.js b/js/item_render_runtime.js index 79846fe..107bf76 100644 --- a/js/item_render_runtime.js +++ b/js/item_render_runtime.js @@ -297,41 +297,115 @@ draw(ctx, t, lighting = null) { ctx.save(); const flash = clamp(Number(this.sprinklerFlash || 0) || 0, 0, 1); const spin = Number(this.sprinklerSpin || 0) || 0; - ctx.fillStyle = styleNight ? "#405b6f" : "#8fc7df"; - ctx.strokeStyle = styleNight ? "#20313f" : "#4e7f96"; - ctx.lineWidth = Math.max(1.6, this.r * 0.06); + const range = globalThis.TarinaiItemDynamicToolSystem?.sprinklerRange ? globalThis.TarinaiItemDynamicToolSystem.sprinklerRange(this) : Math.max(288, (Number(this.r || 28) || 28) * 13.8); + const metalA = styleNight ? "#6a7782" : "#d6dde3"; + const metalB = styleNight ? "#39434d" : "#8a969f"; + const darkMetal = styleNight ? "#1e252b" : "#53606a"; + const baseGreen = styleNight ? "#4f6a3a" : "#76a54b"; + const baseGreenDark = styleNight ? "#304324" : "#547635"; + const hose = styleNight ? "#2f3a42" : "#4b5963"; + // base shadow + ctx.fillStyle = "rgba(30,40,48,0.14)"; ctx.beginPath(); - ctx.ellipse(0, this.r * 0.22, this.r * 0.74, this.r * 0.34, 0, 0, Math.PI * 2); + ctx.ellipse(0, this.r * 0.70, this.r * 0.92, this.r * 0.26, 0, 0, Math.PI * 2); + ctx.fill(); + // ground stake and foot + ctx.fillStyle = baseGreen; + ctx.strokeStyle = baseGreenDark; + ctx.lineWidth = Math.max(1.6, this.r * 0.05); + roundedRect(ctx, -this.r * 0.56, this.r * 0.12, this.r * 1.12, this.r * 0.46, this.r * 0.12); ctx.fill(); ctx.stroke(); - ctx.fillStyle = styleNight ? "#587789" : "#b8e5f2"; - roundedRect(ctx, -this.r * 0.28, -this.r * 0.46, this.r * 0.56, this.r * 0.78, this.r * 0.12); + ctx.beginPath(); + ctx.moveTo(-this.r * 0.18, this.r * 0.10); + ctx.lineTo(-this.r * 0.38, this.r * 0.88); + ctx.lineTo(-this.r * 0.18, this.r * 0.88); + ctx.lineTo(-this.r * 0.02, this.r * 0.10); + ctx.closePath(); ctx.fill(); ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(this.r * 0.18, this.r * 0.10); + ctx.lineTo(this.r * 0.38, this.r * 0.88); + ctx.lineTo(this.r * 0.18, this.r * 0.88); + ctx.lineTo(this.r * 0.02, this.r * 0.10); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + // riser pipe + const pipeGrad = ctx.createLinearGradient(-this.r * 0.12, -this.r * 0.54, this.r * 0.12, this.r * 0.36); + pipeGrad.addColorStop(0, metalA); + pipeGrad.addColorStop(0.55, metalB); + pipeGrad.addColorStop(1, metalA); + ctx.fillStyle = pipeGrad; + ctx.strokeStyle = darkMetal; + roundedRect(ctx, -this.r * 0.12, -this.r * 0.30, this.r * 0.24, this.r * 0.62, this.r * 0.08); + ctx.fill(); + ctx.stroke(); + // head and rotor ctx.save(); + ctx.translate(0, -this.r * 0.40); ctx.rotate(spin); - ctx.strokeStyle = flash > 0 ? "rgba(174,226,255,0.95)" : (styleNight ? "rgba(142,190,214,0.72)" : "rgba(67,129,160,0.78)"); - ctx.lineWidth = Math.max(2.0, this.r * 0.075); + const armGrad = ctx.createLinearGradient(-this.r * 0.84, 0, this.r * 0.84, 0); + armGrad.addColorStop(0, metalB); + armGrad.addColorStop(0.5, metalA); + armGrad.addColorStop(1, metalB); + ctx.strokeStyle = armGrad; + ctx.lineWidth = Math.max(2.4, this.r * 0.09); ctx.lineCap = "round"; ctx.beginPath(); - ctx.moveTo(-this.r * 0.58, -this.r * 0.46); - ctx.lineTo(this.r * 0.58, -this.r * 0.46); - ctx.moveTo(0, -this.r * 0.72); - ctx.lineTo(0, -this.r * 0.20); + ctx.moveTo(-this.r * 0.72, 0); + ctx.lineTo(this.r * 0.72, 0); + ctx.stroke(); + ctx.strokeStyle = darkMetal; + ctx.lineWidth = Math.max(1.2, this.r * 0.04); + ctx.beginPath(); + ctx.moveTo(0, -this.r * 0.28); + ctx.lineTo(0, this.r * 0.20); ctx.stroke(); for (const sx of [-1, 1]) { + ctx.fillStyle = hose; ctx.beginPath(); - ctx.arc(sx * this.r * 0.68, -this.r * 0.46, this.r * 0.10, 0, Math.PI * 2); - ctx.fillStyle = "rgba(112,184,224,0.72)"; + ctx.arc(sx * this.r * 0.74, 0, this.r * 0.10, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = flash > 0 ? "rgba(170,225,255,0.95)" : "rgba(118,188,232,0.80)"; + ctx.beginPath(); + ctx.arc(sx * this.r * 0.80, 0, this.r * 0.06, 0, Math.PI * 2); ctx.fill(); } + if (flash > 0.02) { + ctx.strokeStyle = `rgba(142,216,255,${0.45 + flash * 0.35})`; + ctx.lineWidth = Math.max(1.0, this.r * 0.04); + for (const sx of [-1, 1]) { + for (let i = 0; i < 3; i++) { + const dir = sx < 0 ? -1 : 1; + const spread = (i - 1) * 0.18; + const len = this.r * (0.95 + i * 0.20); + ctx.beginPath(); + ctx.moveTo(sx * this.r * 0.80, 0); + ctx.quadraticCurveTo(sx * this.r * (1.10 + i * 0.14), -this.r * (0.10 + Math.abs(spread) * 0.4), sx * this.r * (1.36 + i * 0.18), -this.r * (0.18 + spread)); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(sx * this.r * (1.36 + i * 0.18), -this.r * (0.18 + spread), this.r * 0.035 * (1.0 + i * 0.14), 0, Math.PI * 2); + ctx.fillStyle = `rgba(164,228,255,${0.35 + flash * 0.25})`; + ctx.fill(); + } + } + } ctx.restore(); - const rangePulse = 0.30 + flash * 0.45; - ctx.strokeStyle = `rgba(88,157,214,${rangePulse})`; - ctx.lineWidth = 1.4; - ctx.setLineDash([4, 5]); + // top cap + ctx.fillStyle = metalA; + ctx.strokeStyle = darkMetal; ctx.beginPath(); - ctx.arc(0, 0, Math.max(this.r * 2.15, 44), 0, Math.PI * 2); + ctx.arc(0, -this.r * 0.40, this.r * 0.16, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + const rangePulse = 0.14 + flash * 0.30; + ctx.strokeStyle = `rgba(88,157,214,${rangePulse})`; + ctx.lineWidth = 1.2; + ctx.setLineDash([7, 6]); + ctx.beginPath(); + ctx.arc(0, 0, range, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } else if (this.type === "duplicator") { diff --git a/js/item_type_initializers.js b/js/item_type_initializers.js index 7690d65..408ef63 100644 --- a/js/item_type_initializers.js +++ b/js/item_type_initializers.js @@ -42,6 +42,7 @@ function initializeItemTypeState(item, type, x, y) { item.sprinklerTimer = rand(0.18, 0.72); item.sprinklerSpin = rand(0, Math.PI * 2); item.sprinklerFlash = 0; + item.sprinklerJustActivated = 0; } if (type === "ball") { item.vx = rand(-5, 5); diff --git a/js/structure_lifecycle.js b/js/structure_lifecycle.js index 1674217..28b4995 100644 --- a/js/structure_lifecycle.js +++ b/js/structure_lifecycle.js @@ -18,6 +18,73 @@ return true; } + function ownedStructureDestroyedLabel(structure) { + if (structure?.type === "plushie") return "ぬいぐるみ"; + if (structure?.type === "grass_bed") return "かんたんベッド"; + return structure?.label || "自分のもの"; + } + + function shouldPanicOwnerOnStructureGone(structure, reason = "") { + if (!structure || !structure.ownerId) return false; + if (!(structure.type === "plushie" || structure.type === "grass_bed")) return false; + if (String(reason || "") === "grass-bed-wake-used") return false; + return true; + } + + function triggerOwnedStructureDestroyedPanic(worldRef, structure, breaker = null, reason = "structure-destroyed") { + if (!worldRef || !shouldPanicOwnerOnStructureGone(structure, reason)) return false; + const owner = (worldRef.tarinai || []).find(t => t && !t.dead && t.id === structure.ownerId); + if (!owner) return false; + const label = ownedStructureDestroyedLabel(structure); + const detail = `自分の${label}が壊された`; + const panicReason = `自分の${label}を壊されてパニックになっている`; + const threat = breaker || { + id: `${structure.id || "structure"}:destroyed`, + x: Number(structure.x) || owner.x, + y: Number(structure.y) || owner.y, + type: "owned_structure_destroyed", + dead: false, + }; + owner.lastPanicDetail = detail; + owner.fearTimer = Math.max(owner.fearTimer || 0, structure.type === "plushie" ? 3.2 : 2.6); + owner.surpriseTimer = Math.max(owner.surpriseTimer || 0, 0.85); + owner.awakeLockTimer = Math.max(owner.awakeLockTimer || 0, 2.4); + if (owner.enterPanic) { + owner.enterPanic({ + threat, + target: owner.panicDestination?.(threat, true) || null, + forceDestination: true, + reason: panicReason, + fear: structure.type === "plushie" ? 1.65 : 1.35, + stress: structure.type === "plushie" ? 20 : 15, + wake: true, + cause: `${structure.type}_destroyed`, + surpriseTimer: 0.85, + awakeLockTimer: 2.4, + }); + } else { + owner.setActionState?.("panic", { target: owner.panicDestination?.(threat, true) || null, reason: panicReason, wake: true, sleeping: false }); + } + if (typeof global.setBehaviorText === "function") { + global.setBehaviorText(owner, { + need: "safety", + actionId: "panic_escape", + actionLabel: "パニックになっている", + reasonText: panicReason, + causeText: detail, + target: threat, + phase: "perform", + source: "behavior", + }); + } + owner.thought = panicReason; + owner.behaviorLockTimer = Math.max(owner.behaviorLockTimer || 0, 1.2); + worldRef.spawnBubble?.(owner.x, owner.y - (owner.radius || 20) * 1.20, "!?", "rgba(188,66,76,0.88)"); + worldRef.log?.(`${owner.name || "たりない"}は自分の${label}を壊されてパニックになった。`, "danger"); + worldRef.drawListDirty = true; + return true; + } + function releaseStructureUsers(worldRef, structure, opts = {}) { if (!worldRef || !structure) return 0; const reason = opts.reason || "\u306a\u304f\u306a\u3063\u305f"; @@ -51,8 +118,10 @@ if (!structure || structure.dead) return false; const reason = opts.reason || "structure-deleted"; releaseStructureUsers(worldRef, structure, { reason: opts.userReason || "\u7f6e\u304d\u3082\u306e\u304c\u306a\u304f\u306a\u3063\u305f", wake: opts.wake !== false }); + const ownerPanic = shouldPanicOwnerOnStructureGone(structure, reason); const changed = markStructureGone(structure, reason); if (!changed) return false; + if (ownerPanic && opts.panicOwner !== false) triggerOwnedStructureDestroyedPanic(worldRef, structure, opts.breaker || null, reason); markWorldAfterStructureChange(worldRef, reason); worldRef?.emit?.("structure:deleted", { structure, type: structure.type, reason }); return true; @@ -73,7 +142,7 @@ function deleteItem(worldRef, item, opts = {}) { if (!item || item.dead) return false; - if (item.isStructure) return deleteStructure(worldRef, item, { reason: opts.reason || "delete-tool", userReason: opts.userReason, wake: opts.wake }); + if (item.isStructure) return deleteStructure(worldRef, item, { reason: opts.reason || "delete-tool", userReason: opts.userReason, wake: opts.wake, breaker: opts.breaker || null, panicOwner: opts.panicOwner }); item.amount = 0; item.hp = 0; item.removedReason = String(opts.reason || "delete-tool"); @@ -87,5 +156,6 @@ deleteStructure, consumeGrassBedOnWake, deleteItem, + triggerOwnedStructureDestroyedPanic, }); })(window); diff --git a/js/structures.js b/js/structures.js index 87f082e..dab5885 100644 --- a/js/structures.js +++ b/js/structures.js @@ -98,6 +98,7 @@ this.dead = true; this.amount = 0; global.StructureRegistry?.get?.(this.type)?.onDestroyed?.(this, worldRef, breaker); + global.TarinaiStructureLifecycle?.triggerOwnedStructureDestroyedPanic?.(worldRef, this, breaker, "structure-damaged-destroyed"); return true; } diff --git a/js/ui_bind.js b/js/ui_bind.js index 44970bd..085426e 100644 --- a/js/ui_bind.js +++ b/js/ui_bind.js @@ -61,36 +61,18 @@ function bindUI() { } function openAutoCollapsedSectionsForMobileScroll() { - if (!isTouchMobileLayout()) return; - for (const card of document.querySelectorAll(".collapsible-card.collapsed")) { - if (card.dataset.userCollapsed === "1") continue; - markCollapsibleCardState(card, false); - } - for (const category of ui.toolPalette?.querySelectorAll(".tool-category.collapsed") || []) { - if (category.dataset.userCollapsed === "1") continue; - markToolCategoryState(category, false); - } + // v39.15.47: 自動スクロールは折り畳み状態を一切変更しない。 + return; } - function scrollWindowToElement(target, offset = 0, behavior = "smooth") { + function scrollWindowToElement(target, offset = 0, behavior = "auto") { if (!target) return; const scroller = document.scrollingElement || document.documentElement; - const clampTop = (value) => Math.max(0, Math.min(value, Math.max(0, scroller.scrollHeight - window.innerHeight))); - const desiredTop = () => clampTop(window.scrollY + target.getBoundingClientRect().top - offset); - const go = (b = behavior) => window.scrollTo({ top: desiredTop(), behavior: b }); - go(behavior); - // Explicit button scroll only. No scroll-snap listener is used; these - // delayed corrections are only for DOM height changes after opening cards - // or rendering the family graph. - let tries = 0; - const correct = () => { - tries += 1; - const rect = target.getBoundingClientRect(); - const delta = rect.top - offset; - if (Math.abs(delta) > 10) go("auto"); - if (tries < 5 && Math.abs(delta) > 10) window.setTimeout(correct, tries < 2 ? 180 : 320); - }; - window.setTimeout(correct, 180); + const rect = target.getBoundingClientRect?.(); + if (!rect) return; + const maxTop = Math.max(0, scroller.scrollHeight - window.innerHeight); + const top = Math.max(0, Math.min(window.scrollY + rect.top - offset, maxTop)); + window.scrollTo({ top, behavior: "auto" }); } function scrollPanelToTab(tab = "") { @@ -108,30 +90,18 @@ function bindUI() { if (!target) return false; target.classList?.remove?.("hidden"); if (tab === "family") { - // Auto update OFF should not make the family graph disappear. - // A direct jump to the family section is treated as a manual refresh, - // while continuous auto-refresh remains disabled. + // 更新OFFでも、ユーザーが家系図へ移動した時だけ現在スナップショットを描画する。 window.resetArchiveRenderState?.(); renderArchive?.(); } - if (isTouchMobileLayout()) openAutoCollapsedSectionsForMobileScroll(); - const collapsedCard = target.closest?.(".collapsible-card") || (target.classList?.contains?.("collapsible-card") ? target : null); - if (collapsedCard?.classList?.contains?.("collapsed") && collapsedCard.dataset.userCollapsed !== "1") { - markCollapsibleCardState(collapsedCard, false); - } - const tabsH = ui.panelQuickTabs?.offsetHeight || 0; - const style = window.getComputedStyle ? window.getComputedStyle(panel) : null; - const panelScrollable = panel.scrollHeight > panel.clientHeight + 4 && style?.overflowY !== "visible"; - const mobileOffset = isTouchMobileLayout() - ? ((ui.panelQuickTabs?.getBoundingClientRect?.().height || tabsH || 0) + 10) - : 0; - if (tab === "game") { - scrollWindowToElement(target, 8); - } else if (panelScrollable && !isTouchMobileLayout()) { + const isDesktop = window.matchMedia?.("(min-width: 981px)")?.matches ?? window.innerWidth >= 981; + if (isDesktop && tab !== "game" && tab !== "family") { + const tabsH = ui.panelQuickTabs?.offsetHeight || 0; const top = Math.max(0, target.offsetTop - tabsH - 8); - panel.scrollTo({ top, behavior: "smooth" }); + panel.scrollTo({ top, behavior: "auto" }); } else { - scrollWindowToElement(target, mobileOffset); + // スマホはページ全体スクロール。目的地UIの上端を画面上端へ合わせる。 + scrollWindowToElement(target, 0, "auto"); } setActivePanelTab(tab); return true; @@ -143,7 +113,7 @@ function bindUI() { const btn = e.target.closest("[data-panel-tab]"); if (!btn) return; const now = performance?.now?.() ?? Date.now(); - if (now - Number(ui.panelQuickTabs?.dataset?.lastActivateAt || 0) < 160) return; + if (now - Number(ui.panelQuickTabs?.dataset?.lastActivateAt || 0) < 50) return; if (ui.panelQuickTabs) ui.panelQuickTabs.dataset.lastActivateAt = String(now); e.preventDefault(); audio.uiClick?.(); diff --git a/js/ui_family_layout.js b/js/ui_family_layout.js index 0f4cee2..f622e90 100644 --- a/js/ui_family_layout.js +++ b/js/ui_family_layout.js @@ -42,12 +42,18 @@ function lineageTreeNodeHtml(n, x, y) { const reason = briefReason ? ` / ${briefReason}` : ""; const rawTags = live?.personalityTags ? live.personalityTags() : (Array.isArray(n?.personalityTags) ? n.personalityTags : []); const tags = rawTags.slice(); + const displayTags = tags.slice(0, 4); const tagText = tags.length ? ` / ${tags.join(" ")}` : ""; const title = escapeHtml(`${n?.name || "\u4e0d\u660e"} / \u4e16\u4ee3 ${n?.generation || "?"} / ${status}${reason}${tagText}`); const deathLine = briefReason ? `${escapeHtml(briefReason)}` : ""; - const tagLine = tags.length ? `${tags.map(tag => `${escapeHtml(tag)}`).join("")}` : ""; + const tagLine = displayTags.length ? `${displayTags.map(tag => `${escapeHtml(tag)}`).join("")}` : ""; const champion = !!(live?.isTarinaiChampion || n?.isTarinaiChampion); - const statLine = n ? `寿${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "life")))} ${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "size")))} ${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "attack")))} ${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "speed")))} ${escapeHtml(lineageFightRateText(n))}` : ""; + const statLine = n ? ` + 寿${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "life")))} + ${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "size")))} + ${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "attack")))} + ${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "speed")))} + ${escapeHtml(lineageFightRateText(n))}` : ""; const cls = n ? ((live || n.alive) ? "alive" : "dead") : "missing"; const childCls = (n?.parents || []).length ? " child" : ""; const iconBaseScale = live @@ -382,7 +388,7 @@ function lineageLayoutRows(rows, idSet) { const SIBLING_GAP = 32; const GROUP_GAP = 52; const PARTNER_GROUP_GAP = 24; - const ROW_GAP = 138; + const ROW_GAP = 108; const LEFT_PAD = 82; const TOP_PAD = 44; const RIGHT_PAD = 48; diff --git a/js/ui_family_render.js b/js/ui_family_render.js index 31e63b9..7e9ac59 100644 --- a/js/ui_family_render.js +++ b/js/ui_family_render.js @@ -55,7 +55,7 @@ function lineageLayoutRowsFast(rows, idSet) { const NODE_W = 150; const NODE_H = 104; const SIBLING_GAP = 22; - const ROW_GAP = 116; + const ROW_GAP = 92; const LEFT_PAD = 82; const TOP_PAD = 44; const RIGHT_PAD = 48; diff --git a/js/ui_input_shared.js b/js/ui_input_shared.js index 0ebfbac..c62ca64 100644 --- a/js/ui_input_shared.js +++ b/js/ui_input_shared.js @@ -18,6 +18,7 @@ mode = inputModeManager?.setMode?.(mode) || (["auto", "camera", "tool"].includes(mode) ? mode : "auto"); uiCache.mobileInputMode = mode; ui.mobileModeControls?.querySelectorAll("[data-mobile-mode]").forEach(btn => btn.classList.toggle("active", btn.dataset.mobileMode === mode)); + document.body.classList.toggle("mobile-mode-auto", mode === "auto"); document.body.classList.toggle("mobile-mode-camera", mode === "camera"); document.body.classList.toggle("mobile-mode-tool", mode === "tool"); return mode; diff --git a/js/ui_input_touch.js b/js/ui_input_touch.js index a1f2415..ed40896 100644 --- a/js/ui_input_touch.js +++ b/js/ui_input_touch.js @@ -5,12 +5,18 @@ if (!ctx?.canvas) return false; const { canvas, world, uiCache } = ctx; ctx.bindMobileModeControls(); + const isMobileAutoScrollMode = () => { + const mode = ctx.mobileInputMode?.() || "auto"; + const touchMobile = document.body.classList.contains("touch-mobile-ui") || window.matchMedia?.("(max-width: 980px)")?.matches; + return touchMobile && mode === "auto" && (!world.tool || world.tool === "observe"); + }; canvas.addEventListener("touchstart", (e) => { if (!e.touches?.length) return; - e.preventDefault(); uiCache.touchMoved = false; const mode = ctx.mobileInputMode(); + const autoPageScroll = isMobileAutoScrollMode(); + if (!autoPageScroll) e.preventDefault(); if (mode === "camera" && e.touches.length === 1) { const t = e.touches[0]; uiCache.touchStartX = t.clientX; @@ -62,12 +68,12 @@ return; } if (world.tool === "pinch" && p.inside && ctx.beginGrab(p, t.clientX, t.clientY, { touchMode: "grab" })) return; - uiCache.touchMode = mode === "tool" ? "toolTapOnly" : "panOrTap"; + uiCache.touchMode = autoPageScroll ? "autoTapOrScroll" : (mode === "tool" ? "toolTapOnly" : "panOrTap"); }, { passive: false }); canvas.addEventListener("touchmove", (e) => { if (!e.touches?.length || !uiCache.touchMode) return; - e.preventDefault(); + if (uiCache.touchMode !== "autoTapOrScroll") e.preventDefault(); if (uiCache.touchMode === "pinchZoom" && e.touches.length >= 2) { const c = ctx.touchCenter(e.touches); const distNow = Math.max(1, ctx.touchDistance(e.touches)); @@ -116,6 +122,10 @@ ctx.moveGrab(p, t.clientX, t.clientY, { touch: true, renderFrame: true }); return; } + if (uiCache.touchMode === "autoTapOrScroll") { + if (totalMove > 7) uiCache.touchMoved = true; + return; + } if (uiCache.touchMode === "panOrTap") { if (totalMove > 7) { uiCache.touchMoved = true; @@ -126,7 +136,7 @@ }, { passive: false }); canvas.addEventListener("touchend", (e) => { - e.preventDefault(); + if (uiCache.touchMode !== "autoTapOrScroll") e.preventDefault(); if (uiCache.areaDeleting) ctx.endAreaDelete(); if (uiCache.hosing) { ctx.endHose(); if (uiCache.hoseHoldTimer) { clearInterval(uiCache.hoseHoldTimer); uiCache.hoseHoldTimer = null; } } if (uiCache.shooting) ctx.endShoot(); @@ -138,6 +148,13 @@ window.TarinaiCommands?.dispatch?.(world, { type: "pointer.primary", x: p.x, y: p.y }); renderStats(); } + } else if (uiCache.touchMode === "autoTapOrScroll" && !uiCache.touchMoved) { + e.preventDefault(); + const p = ctx.screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY); + if (p.inside) { + window.TarinaiCommands?.dispatch?.(world, { type: "pointer.primary", x: p.x, y: p.y }); + renderStats(); + } } else if (uiCache.touchMode === "panOrTap" && !uiCache.touchMoved) { const p = ctx.screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY); if (p.inside) { diff --git a/js/ui_log.js b/js/ui_log.js index eb554a9..cae4a5c 100644 --- a/js/ui_log.js +++ b/js/ui_log.js @@ -83,12 +83,27 @@ function pushLogNotification(entry = {}) { window.pushLogNotification = pushLogNotification; +function shouldShowInEventLog(entry = {}) { + if (!entry) return false; + if (entry.eventType === "player_item_placement") return false; + const text = String(entry.text || ""); + const kind = entry.kind || ""; + // Older saves may contain direct placement records without eventType. + // Hide only participant-less player-like placement lines, preserving births, accidents, fights, and natural events. + if ((kind === "observe" || kind === "note") && !(entry.participants || []).length && /(?:を配置した。|を落とした。)$/.test(text)) return false; + return true; +} + function renderLog(logs) { ui.log.innerHTML = ""; const frag = document.createDocumentFragment(); const touchFirst = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980); const limit = touchFirst ? 34 : 80; - const rows = Array.isArray(logs) ? logs.slice(0, limit) : []; + const visibleLogs = Array.isArray(logs) ? logs.filter(raw => { + const entry = typeof raw === "string" ? { time: world.time, text: raw, kind: "note" } : raw; + return shouldShowInEventLog(entry); + }) : []; + const rows = visibleLogs.slice(0, limit); for (const raw of rows) { const entry = typeof raw === "string" ? { time: world.time, text: raw, kind: "note" } : raw; const kind = entry.kind || "note"; diff --git a/js/version.js b/js/version.js index 1a9a464..3e9f025 100644 --- a/js/version.js +++ b/js/version.js @@ -1,8 +1,8 @@ "use strict"; (function () { - const APP_VERSION = "39.15.39"; - const APP_BUILD = "tarinai-pc-rollback-mobile-family-fix-v39-15-39"; + const APP_VERSION = "39.15.47"; + const APP_BUILD = "tarinai-event-family-oshibyo-fix-v39-15-45"; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; diff --git a/js/world_placement_log.js b/js/world_placement_log.js index 419eb41..dec706e 100644 --- a/js/world_placement_log.js +++ b/js/world_placement_log.js @@ -1433,7 +1433,7 @@ if (!placed) return; forceImmediateToolVisualRefresh(this, "tool-place"); const label = toolLabel(itemType); - this.log(itemType === "genkotsu" || isPinType(itemType) ? `${label}\u3092\u843d\u3068\u3057\u305f\u3002` : `${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`); + this.log(itemType === "genkotsu" || isPinType(itemType) ? `${label}\u3092\u843d\u3068\u3057\u305f\u3002` : `${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`, "observe", { eventType: "player_item_placement" }); } }, diff --git a/js/world_tool_actions.js b/js/world_tool_actions.js index fdb9f89..eeb2689 100644 --- a/js/world_tool_actions.js +++ b/js/world_tool_actions.js @@ -378,8 +378,8 @@ if (!aim?.inside) return false; const hit = this.findShootToolTargetAt?.(aim.x, aim.y); audio.shoot?.(); - this.effects?.push(new Effect("shoot_impact", aim.x, aim.y, { size: hit ? 10 : 7, life: hit ? 0.20 : 0.14, color: hit ? "rgba(255,208,116,0.96)" : "rgba(190,212,240,0.58)" })); - this.effects?.push(new Effect("ring", aim.x, aim.y, { size: hit ? 8 : 5, life: hit ? 0.11 : 0.08, color: hit ? "rgba(255,246,206,0.72)" : "rgba(196,220,255,0.34)" })); + this.effects?.push(new Effect("shoot_impact", aim.x, aim.y, { size: hit ? 22 : 16, life: hit ? 0.28 : 0.22, color: hit ? "rgba(255,208,116,0.96)" : "rgba(190,212,240,0.64)" })); + this.effects?.push(new Effect("ring", aim.x, aim.y, { size: hit ? 20 : 14, life: hit ? 0.18 : 0.14, color: hit ? "rgba(255,246,206,0.74)" : "rgba(196,220,255,0.42)" })); let handled = false; if (hit?.kind === "tarinai") { const t = hit.target; diff --git a/scripts/ai_inventory.ps1 b/scripts/ai_inventory.ps1 new file mode 100644 index 0000000..beca69f --- /dev/null +++ b/scripts/ai_inventory.ps1 @@ -0,0 +1,104 @@ +param( + [string]$Query = "", + [switch]$All, + [switch]$Json +) + +$Root = Resolve-Path (Join-Path $PSScriptRoot "..") +$RoutingPath = Join-Path $Root "FILES.json" +$Routing = Get-Content -Raw -LiteralPath $RoutingPath | ConvertFrom-Json + +$Exact = @{ + ".gitignore" = "ignore rules for local/generated artifacts" + ".htaccess" = "Apache/static-host cache, MIME, and service-worker behavior" + "app_manifest.json" = "canonical version plus CSS/JS/static asset manifest" + "favicon.ico" = "legacy favicon" + "index.html" = "app DOM shell, canvas, panels, dialogs, and ordered script tags" + "README.md" = "small AI entrypoint and read strategy" + "AI_MAP.md" = "subsystem map and task-oriented read sets" + "FILES.json" = "machine-readable file routing and glob semantics" + "service-worker.js" = "generated offline/cache service worker" +} + +$JsExact = @{ + "ants.js" = "ant actor and ant nest worker behavior" + "assets.js" = "image loading, image metrics, and render caches" + "audio.js" = "sample playback and audio controls" + "command_dispatcher.js" = "central command routing for UI/tools/pointers/save" + "data.js" = "sprite/decor definitions, needs, config, field tuning" + "debug_tools.js" = "browser debug overlay and diagnostics" + "family_graph.js" = "lineage graph helpers" + "ground_types.js" = "ground definitions, multipliers, limits, labels" + "health.js" = "health, stress, need, damage, and death helpers" + "history_system.js" = "rolling world statistics/history" + "input_mode_manager.js" = "pointer/touch/mobile input classification" + "items.js" = "item constructors, food/grass helpers, item preview drawing" + "main.js" = "startup, restore, UI binding, update/render loop" + "render.js" = "main canvas renderer" + "system_order.js" = "named update phase order" + "text_catalog.js" = "Japanese UI/state/behavior labels" + "version.js" = "app version/cache/static query config" + "weather_system.js" = "weather selection and application" +} + +function Get-GroupId($Path) { + foreach ($Group in $Routing.groups) { + foreach ($Pattern in $Group.patterns) { + if ($Path -like $Pattern) { return $Group.id } + } + } + return ($Path -split "/")[0] +} + +function Get-Meaning($Path) { + if ($Exact.ContainsKey($Path)) { return $Exact[$Path] } + if ($Path.StartsWith("js/")) { + $Name = Split-Path -Leaf $Path + if ($JsExact.ContainsKey($Name)) { return $JsExact[$Name] } + if ($Name.StartsWith("world_")) { return "world state/view/update/tool/environment/combat/social subsystem" } + if ($Name.StartsWith("tarinai_")) { return "creature action/needs/social/item/update subsystem" } + if ($Name.StartsWith("item_lifecycle_step_")) { return "item lifecycle pipeline stage" } + if ($Name.StartsWith("item_lifecycle_")) { return "item lifecycle subsystem" } + if ($Name.StartsWith("item_dynamic_")) { return "dynamic item behavior subsystem" } + if ($Name.StartsWith("item_")) { return "item registry/runtime/update/render subsystem" } + if ($Name.StartsWith("simulation_")) { return "simulation phase subsystem" } + if ($Name.StartsWith("ui_family_")) { return "family tree UI subsystem" } + if ($Name.StartsWith("ui_input_")) { return "input handling subsystem" } + if ($Name.StartsWith("ui_")) { return "DOM/panel UI subsystem" } + if ($Name.StartsWith("save_")) { return "save/load persistence subsystem" } + if ($Name.StartsWith("physics_")) { return "physics helper/world subsystem" } + return "runtime JavaScript module" + } + if ($Path.StartsWith("scripts/")) { return "repo script/tooling" } + if ($Path.StartsWith("css/")) { return "stylesheet layer" } + if ($Path.StartsWith("assets/sprites/")) { return "creature state sprite" } + if ($Path.StartsWith("assets/ui/")) { return "UI/app/tool image asset" } + if ($Path.StartsWith("assets/objects/")) { return "field object image asset" } + if ($Path.StartsWith("assets/sounds/")) { return "voice or sound-effect sample" } + return "project file" +} + +$Files = Get-ChildItem -LiteralPath $Root -Recurse -File -Force | + Where-Object { $_.FullName -notmatch "\\.git\\" -and $_.FullName -notmatch "\\__pycache__\\" } | + ForEach-Object { $_.FullName.Substring($Root.Path.Length + 1).Replace("\", "/") } | + Sort-Object + +$Records = foreach ($Path in $Files) { + $Group = Get-GroupId $Path + $Meaning = Get-Meaning $Path + if ($Query -and ($Path.ToLowerInvariant() -notlike "*$($Query.ToLowerInvariant())*" -and $Group.ToLowerInvariant() -notlike "*$($Query.ToLowerInvariant())*" -and $Meaning.ToLowerInvariant() -notlike "*$($Query.ToLowerInvariant())*")) { + continue + } + [pscustomobject]@{ path = $Path; group = $Group; meaning = $Meaning } +} + +if ($Json) { + $Records | ConvertTo-Json -Depth 4 +} elseif ($All) { + $Records | ForEach-Object { "$($_.path): $($_.meaning)" } +} else { + $Records | Group-Object group | Sort-Object Name | ForEach-Object { + "$($_.Name): $($_.Count) files" + $_.Group | Select-Object -ExpandProperty meaning -Unique | Sort-Object | Select-Object -First 8 | ForEach-Object { " - $_" } + } +} diff --git a/scripts/ai_inventory.py b/scripts/ai_inventory.py new file mode 100644 index 0000000..63b4701 --- /dev/null +++ b/scripts/ai_inventory.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +import argparse +import fnmatch +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +ROUTING_FILE = ROOT / "FILES.json" + + +EXCLUDE_DIRS = {".git", "__pycache__"} + + +EXACT_DESCRIPTIONS = { + ".gitignore": "ignore rules for local/generated artifacts", + ".htaccess": "Apache/static-host cache, MIME, and service-worker behavior", + "app_manifest.json": "canonical version plus CSS/JS/static asset manifest", + "favicon.ico": "legacy favicon", + "index.html": "app DOM shell, canvas, panels, dialogs, and ordered script tags", + "README.md": "small AI entrypoint and read strategy", + "AI_MAP.md": "subsystem map and task-oriented read sets", + "FILES.json": "machine-readable file routing and glob semantics", + "service-worker.js": "generated offline/cache service worker", +} + + +GROUP_DESCRIPTIONS = { + "css": "stylesheet layer", + "scripts": "repo tooling", + "js": "runtime JavaScript", + "assets/sprites": "creature state sprite", + "assets/ui": "UI/app/tool image asset", + "assets/objects": "field object image asset", + "assets/sounds": "voice or sound-effect sample", +} + + +JS_PREFIX_DESCRIPTIONS = [ + ("world_", "world state/view/update/tool/environment/combat/social subsystem"), + ("tarinai_", "creature action/needs/social/item/update subsystem"), + ("item_lifecycle_step_", "item lifecycle pipeline stage"), + ("item_lifecycle_", "item lifecycle subsystem"), + ("item_dynamic_", "dynamic item behavior subsystem"), + ("item_", "item registry/runtime/update/render subsystem"), + ("simulation_", "simulation phase subsystem"), + ("ui_family_", "family tree UI subsystem"), + ("ui_input_", "input handling subsystem"), + ("ui_", "DOM/panel UI subsystem"), + ("save_", "save/load persistence subsystem"), + ("physics_", "physics helper/world subsystem"), +] + + +JS_EXACT_DESCRIPTIONS = { + "ants.js": "ant actor and ant nest worker behavior", + "assets.js": "image loading, image metrics, and render caches", + "audio.js": "sample playback and audio controls", + "collision_footprint_system.js": "collision footprint shapes and overlap tests", + "command_dispatcher.js": "central command routing for UI/tools/pointers/save", + "constraint_system.js": "attachments, ropes, push/pull, and collision constraints", + "data.js": "sprite/decor definitions, needs, config, field tuning", + "debug_tools.js": "browser debug overlay and diagnostics", + "disease_registry.js": "disease definitions and lookup", + "event_bus.js": "global event bus singleton", + "family_graph.js": "lineage graph helpers", + "ground_types.js": "ground definitions, multipliers, limits, labels", + "health.js": "health, stress, need, damage, and death helpers", + "history_system.js": "rolling world statistics/history", + "input_mode_manager.js": "pointer/touch/mobile input classification", + "items.js": "item constructors, food/grass helpers, item preview drawing", + "main.js": "startup, restore, UI binding, update/render loop", + "math.js": "shared numeric helpers", + "mechanical_system.js": "mechanical item forces, rails, ropes, pins, fences", + "patch_helpers.js": "compatibility patch helpers", + "perf_profiler.js": "performance profiler and quality adaptation", + "registry_base.js": "registry normalization helper base", + "render.js": "main canvas renderer", + "restore_coordinator.js": "post-load restore ordering/fixups", + "sim_core.js": "simulation primitives and shared helpers", + "sound_pack.js": "logical sound ID to asset mapping", + "snapshot_system.js": "world snapshot capture/restore", + "structures.js": "structure definitions and placement/runtime helpers", + "structure_lifecycle.js": "structure update lifecycle", + "system_order.js": "named update phase order", + "text_catalog.js": "Japanese UI/state/behavior labels", + "version.js": "app version/cache/static query config", + "weather_system.js": "weather selection and application", +} + + +def rel(path: Path) -> str: + return path.relative_to(ROOT).as_posix() + + +def iter_files() -> list[str]: + files: list[str] = [] + for path in ROOT.rglob("*"): + if not path.is_file(): + continue + if any(part in EXCLUDE_DIRS for part in path.relative_to(ROOT).parts): + continue + files.append(rel(path)) + return sorted(files) + + +def load_routing() -> dict: + return json.loads(ROUTING_FILE.read_text(encoding="utf-8")) + + +def group_for(path: str, routing: dict) -> str: + for group in routing.get("groups", []): + for pattern in group.get("patterns", []): + if fnmatch.fnmatch(path, pattern): + return group["id"] + return path.split("/", 1)[0] + + +def describe(path: str, routing: dict) -> str: + if path in EXACT_DESCRIPTIONS: + return EXACT_DESCRIPTIONS[path] + if path.startswith("js/"): + name = path.rsplit("/", 1)[-1] + if name in JS_EXACT_DESCRIPTIONS: + return JS_EXACT_DESCRIPTIONS[name] + for prefix, desc in JS_PREFIX_DESCRIPTIONS: + if name.startswith(prefix): + return desc + return "runtime JavaScript module" + if path.startswith("scripts/"): + return "repo script/tooling" + if path.startswith("css/"): + return "stylesheet layer" + if path.startswith("assets/"): + for prefix, desc in GROUP_DESCRIPTIONS.items(): + if path.startswith(prefix + "/"): + return desc + return "static asset" + group_id = group_for(path, routing) + for group in routing.get("groups", []): + if group.get("id") == group_id: + return group.get("meaning", "project file") + return "project file" + + +def matches_query(path: str, query: str, routing: dict) -> bool: + if not query: + return True + q = query.lower() + if q in path.lower(): + return True + group_id = group_for(path, routing).lower() + if q == group_id or q in group_id: + return True + return q in describe(path, routing).lower() + + +def build_records(query: str = "") -> list[dict]: + routing = load_routing() + records = [] + for path in iter_files(): + if not matches_query(path, query, routing): + continue + records.append({ + "path": path, + "group": group_for(path, routing), + "meaning": describe(path, routing), + }) + return records + + +def print_compact(records: list[dict]) -> None: + groups: dict[str, list[dict]] = {} + for record in records: + groups.setdefault(record["group"], []).append(record) + for group_id in sorted(groups): + print(f"{group_id}: {len(groups[group_id])} files") + meanings = sorted({record["meaning"] for record in groups[group_id]}) + for meaning in meanings[:8]: + print(f" - {meaning}") + + +def print_all(records: list[dict]) -> None: + for record in records: + print(f"{record['path']}: {record['meaning']}") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Print token-aware AI file inventory.") + parser.add_argument("query", nargs="?", default="", help="Optional group/path/meaning filter, e.g. ui, world, assets.") + parser.add_argument("--all", action="store_true", help="Print every matching file instead of grouped summary.") + parser.add_argument("--json", action="store_true", help="Print JSON records.") + args = parser.parse_args() + + records = build_records(args.query) + if args.json: + print(json.dumps(records, ensure_ascii=False, indent=2)) + elif args.all: + print_all(records) + else: + print_compact(records) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/service-worker.js b/service-worker.js index 31972d7..215e1ac 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,6 +1,6 @@ "use strict"; -const APP_VERSION = "39.15.39"; +const APP_VERSION = "39.15.47"; 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.