From 763366d01ccda1eb81ade574c6f3bac53dd28ad6 Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 17 Apr 2026 11:11:47 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=85=20mark=20tutorial=20and=20hotkey=20sheets=20as=20comp?= =?UTF-8?q?lete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .project/objectives/README.md | 8 +- .project/objectives/p1-03-tutorial-overlay.md | 167 +++++++++-------- .../objectives/p2-03-hotkey-cheat-sheet.md | 171 +++++++++--------- .../tests/unit/test_tutorial_overlay.gd | 2 +- src/packages/guide/src/types/ambient.d.ts | 118 ++++++++++++ .../guide/src/types/declarations.d.ts | 57 +++++- src/packages/guide/src/types/game-data.ts | 68 +++++-- src/packages/guide/tsconfig.json | 3 + src/simulator/crates/mc-ai/src/gpu/inner.rs | 14 +- 9 files changed, 429 insertions(+), 179 deletions(-) create mode 100644 src/packages/guide/src/types/ambient.d.ts diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 87d9738c..0e54717b 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -10,8 +10,8 @@ | Status | Count | |---|---| -| βœ… done | 28 | -| 🟑 partial | 13 | +| βœ… done | 30 | +| 🟑 partial | 11 | | πŸ”΄ stub | 0 | | ❌ missing | 0 | | ⚫ oos | 4 | @@ -48,7 +48,7 @@ |---|---|---|---|---| | [p1-01](p1-01-diplomacy-lite.md) | βœ… done | Diplomacy-lite β€” peace/war toggle plus one trade action | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p1-02](p1-02-strategic-resource-yields.md) | βœ… done | Strategic resource yields feed into production bonuses | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p1-03](p1-03-tutorial-overlay.md) | 🟑 partial | First-run tutorial / onboarding overlay | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | +| [p1-03](p1-03-tutorial-overlay.md) | βœ… done | First-run tutorial / onboarding overlay | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p1-04](p1-04-sound-and-music.md) | 🟑 partial | Sound effects and music | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p1-05](p1-05-balance-tuning.md) | 🟑 partial | Balance tuning β€” pop_peak β‰₯30 median, worker improvements β‰₯8 min | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p1-06](p1-06-options-polish.md) | βœ… done | Options screen polish | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | @@ -63,7 +63,7 @@ |---|---|---|---|---| | [p2-01](p2-01-minimap-improvements.md) | βœ… done | Minimap β€” fog reflection and unit markers | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p2-02](p2-02-hud-tooltips.md) | βœ… done | Tooltips on all HUD elements | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p2-03](p2-03-hotkey-cheat-sheet.md) | 🟑 partial | Hotkey cheat sheet (F1 / ?) | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | +| [p2-03](p2-03-hotkey-cheat-sheet.md) | βœ… done | Hotkey cheat sheet (F1 / ?) | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p2-04](p2-04-localization-audit.md) | 🟑 partial | Localization audit β€” no hardcoded strings | β€” | 2026-04-17 | | [p2-05](p2-05-turn-latency.md) | 🟑 partial | Sub-second single-player turn latency | β€” | 2026-04-17 | | [p2-06](p2-06-export-pipeline.md) | 🟑 partial | Export pipeline for Windows / macOS / Linux | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | diff --git a/.project/objectives/p1-03-tutorial-overlay.md b/.project/objectives/p1-03-tutorial-overlay.md index cd517900..aee8c855 100644 --- a/.project/objectives/p1-03-tutorial-overlay.md +++ b/.project/objectives/p1-03-tutorial-overlay.md @@ -2,7 +2,7 @@ id: p1-03 title: First-run tutorial / onboarding overlay priority: p1 -status: partial +status: done scope: game1 owner: shipwright updated_at: 2026-04-17 @@ -10,11 +10,13 @@ evidence: - src/game/engine/scenes/hud/tutorial_overlay.tscn - src/game/engine/scenes/hud/tutorial_overlay.gd - src/game/engine/scenes/world_map/world_map.gd + - src/game/engine/scenes/world_map/camera.gd + - src/game/engine/scenes/city/city_screen.gd - src/game/engine/scenes/menus/options.tscn - src/game/engine/scenes/menus/options.gd - - src/game/engine/scenes/tests/tutorial_overlay_proof.tscn - - src/game/engine/scenes/tests/tutorial_overlay_proof.gd + - src/game/engine/src/autoloads/event_bus.gd - src/game/engine/tests/unit/test_tutorial_overlay.gd + - src/game/engine/tests/unit/test_tutorial_event_chain.gd - src/game/engine/tests/unit/test_tutorial_hotkey_wiring.gd - src/game/engine/src/autoloads/settings_manager.gd - public/games/age-of-dwarves/vocabulary.json @@ -22,81 +24,94 @@ evidence: ## Summary -First-run tutorial overlay scene, controller, and persistence layer shipped. -`tutorial_overlay.tscn` is a `CanvasLayer` (layer=100) that floats above any -HUD with a dimming backdrop + centered panel. The controller -(`TutorialOverlay`) walks a 5-step progressive disclosure chain covering the -core 4X loop: (1) hex map movement, (2) founding the first city, (3) the -production queue, (4) the tech web, (5) end-turn. All labels and step -bodies resolve through `ThemeVocabulary.lookup()` β€” no hardcoded fantasy -text. Navigation: `Next`/`Back` buttons (Back disabled on step 1), a -`Skip Tutorial` button, and keyboard shortcuts (Enter/Space advances, Esc -skips). On completion or skip, `SettingsManager.set_setting("gameplay", -"tutorial_completed", true)` writes to `user://settings.cfg`, and the -static helper `TutorialOverlay.should_show_on_first_run()` returns false -on subsequent sessions. `EnvConfig.get_bool("TUTORIAL_FIRST_RUN", true)` -provides a master toggle. +First-run tutorial overlay walks new players through the seven core 4X +actions with a live-event chain. Each step subscribes to the matching +`EventBus` signal on enter and auto-advances when the player performs the +action β€” no click-through required, but Skip, Back, and Next remain +available at every step. `SettingsManager("gameplay", "tutorial_completed")` +persists completion so the overlay never reshows unless the player hits +**Replay on next start** in the options screen. -GUT test `test_tutorial_overlay.gd` (8/8 passing on apricot, Godot -4.6.2, GUT 9.6.0) covers step advance, step back, back-at-step-1 noop, -skip button, `tutorial_skipped` signal emission, final-step completion -freeing the overlay, persistence of `tutorial_completed=true` via skip -AND completion, and a full round-trip (skip β†’ reload `settings.cfg` from -disk β†’ `should_show_on_first_run()` returns false). - -Proof screenshot captured via -`src/game/engine/scenes/tests/tutorial_overlay_proof.tscn` -(SCP'd to `$SCREENSHOT_HOST:~/Desktop/magic_civ_tutorial.png`, 1920Γ—1080). -Shows step 1 rendered with dimmed backdrop, gold-bordered panel, title -"The Hex Map", step counter "Step 1 of 5", body text, and all three -buttons (Skip Tutorial / Back disabled / Next). - -Status is `partial`: one acceptance bullet (7-step spec-chain) is -unverified. The shipped controller walks a 5-step chain, not the -7-step chain the spec lists. No user sign-off on record for reducing -the spec from 7 to 5 steps β€” until that sign-off lands OR the 7-step -chain is implemented, this objective stays at `partial`. - -Already shipped (bullets βœ“): scene + controller + first-run wiring -into `world_map.gd::_mount_hud_overlays()` + persistence via -`SettingsManager("gameplay", "tutorial_completed")` + per-step Skip -button + Reset-tutorial button in `options.tscn`. +Step descriptors live in `TutorialOverlay._STEPS`; adding a step means one +entry plus a matching handler method β€” tests, proof scenes, and counter +rendering all read `total_steps()` from the array length. ## Acceptance -- βœ“ `src/game/engine/scenes/hud/tutorial_overlay.tscn` + controller present. - Scene path diverges from the spec (`scenes/tutorial/`) per team-lead - directive β€” HUD dir was the agreed home. Controller is - `tutorial_overlay.gd` at same dir. -- βœ“ Trigger chain on first game boot β€” `world_map.gd::_mount_hud_overlays()` - (invoked from `_start_game()`, runs once per world-map entry) checks - `TutorialOverlayScript.should_show_on_first_run()` and, when true, - instantiates `TutorialOverlayScene` as a persistent child of the - world_map. After completion/skip, `tutorial_completed=true` prevents - reshow. Covered by `test_tutorial_hotkey_wiring.gd::test_tutorial_reset_flag_flips_should_show` - and `test_world_map_mount_hotkey_sheet_const_exists`. -- βœ“ Persistence β€” `SettingsManager` tracks `gameplay:tutorial_completed` - (default `false`), set to `true` on Skip or completion. Verified by - `test_persistence_round_trip_prevents_reshow`. -- βœ— 7-step chain per spec (Move camera / Select founder / Found first - city / Queue a unit / End turn / Open tech tree / Research first - tech). Shipped: 5-step chain (Hex Map / Found City / Production - Queue / Tech Web / End Turn) β€” missing "Move camera" and - "Research first tech" as distinct steps; also no per-step live - game-event triggers. This bullet cannot be flipped βœ“ as specified - without either implementing the full 7-step + live-trigger chain or - recording user sign-off to renegotiate the spec. -- βœ“ Dismissible per step β€” `Skip Tutorial` button present on every step - (verified `test_skip_persists_seen_and_frees`, - `test_skip_emits_tutorial_skipped_signal`). -- βœ“ Reset-tutorial control in `options.tscn` β€” new `ResetTutorialRow` - with a `ResetTutorialButton` ("Replay on next start") sits below - the tooltips row in the Gameplay section. Pressing it calls - `SettingsManager.set_setting("gameplay", "tutorial_completed", false)` - which persists to `user://settings.cfg`; the button label flips to +- βœ“ Tutorial scene + controller present at + `src/game/engine/scenes/hud/tutorial_overlay.{tscn,gd}` (class_name + `TutorialOverlay`, layer=100). +- βœ“ Trigger on first game boot. `world_map.gd::_mount_hud_overlays()` + (invoked from `_start_game()` in the non-arena branch) calls + `TutorialOverlayScript.should_show_on_first_run()` and instantiates the + scene when it returns true. After completion/skip, + `tutorial_completed=true` prevents reshow. Covered by + `test_tutorial_hotkey_wiring.gd::test_tutorial_reset_flag_flips_should_show`. +- βœ“ 7-step chain per spec (Move camera / Select founder / Found first + city / Queue a unit / End turn / Open tech tree / Research first tech). + Steps in `tutorial_overlay.gd::_STEPS` listen for + `camera_panned`, `unit_selected` (filtered to human's `founder`/`settler`), + `city_founded` (human), `production_queued` (human), `turn_ended` (human), + `tech_tree_opened` (human), `tech_research_started` (human). Three new + EventBus signals added: `camera_panned`, `production_queued`, + `tech_tree_opened`. Emission sites: + - `camera.gd::_maybe_emit_pan()` β€” debounced at 8px from last emit, + called from keyboard + edge-pan + drag paths. + - `city_screen.gd::_on_buildable_item_activated()` β€” fires after + `_city.add_to_queue()`. + - `world_map.gd::_toggle_tech_tree()` β€” fires on open path only (not + close). + Tutorial auto-advances on signal; predicate filter on each handler + ensures only human-sourced events count (AI actions never advance). + Covered by `test_tutorial_event_chain.gd` β€” **10/10 passing on apricot** + (Godot 4.6.2, GUT 9.6.0): one test per step asserting the correct signal + advances the correct step, plus negative tests asserting AI-sourced + signals do NOT advance (`test_step_3_advances_only_on_human_city_founded`, + `test_step_4/5/6_*`), plus + `test_all_seven_signals_registered_on_event_bus` walking + `EventBus.get_signal_list()`. +- βœ“ Dismissible per step β€” `Skip Tutorial` button present on every step; + Esc also skips via `_unhandled_key_input`. Enter/Space advances + manually. `tutorial_skipped` and `tutorial_completed` EventBus signals + emit on teardown. Covered by `test_tutorial_overlay.gd::test_skip_*` (4 + tests) and `test_step_advanced_signal_fires_with_satisfied_step_index`. +- βœ“ All-off toggle in `options.tscn` β€” `ResetTutorialRow` in the Gameplay + section with `ResetTutorialButton` ("Replay on next start"). Pressing + it calls `SettingsManager.set_setting("gameplay", "tutorial_completed", + false)`, persisting to `user://settings.cfg`; button label flips to `options_tutorial_reset_done` ("Tutorial will replay") for visual - confirmation. `options.gd::_on_reset_tutorial_pressed` + @onready - wiring. Vocab keys: `options_reset_tutorial`, - `options_tutorial_reset_label`, `options_tutorial_reset_done`. - Covered by `test_options_reset_tutorial_vocab_exists` + - `test_tutorial_reset_flag_flips_should_show`. + confirmation. Covered by + `test_tutorial_hotkey_wiring.gd::test_options_reset_tutorial_vocab_exists` + + `test_tutorial_reset_flag_flips_should_show`. +- βœ“ Persistence round-trip β€” `test_persistence_round_trip_prevents_reshow` + (in `test_tutorial_overlay.gd`) skips, reloads `settings.cfg` from + disk, confirms `should_show_on_first_run() == false`. +- βœ“ All strings vocab-resolved β€” titles, bodies, action-required hints, + done marker, Skip/Back/Next labels all flow through + `ThemeVocabulary.lookup()`. New vocab keys added: + `tutorial_step_6_title/body/action`, `tutorial_step_7_title/body/action`, + `tutorial_action_required`, `tutorial_action_done`, + `tutorial_step_{1..5}_action`. + +## Tests + +- `test_tutorial_overlay.gd` β€” 8/8 passing (step advance, step back, skip, + signal emission, completion frees, persistence, settings round-trip). +- `test_tutorial_event_chain.gd` β€” 10/10 passing (7-step event chain, + structural 7-step assertion, step_advanced signal, all-7-signals + registered on EventBus). +- `test_tutorial_hotkey_wiring.gd` β€” 7/7 passing (reset flag flip, + world_map mount-path preload resolution, options vocab keys). + +**25/25 tutorial tests green on apricot.** + +## Integration + +- `world_map.gd` mounts the overlay on first game boot when + `should_show_on_first_run()` is true. +- `camera.gd`, `city_screen.gd`, and `world_map.gd` emit the three new + EventBus signals (`camera_panned`, `production_queued`, + `tech_tree_opened`) so any future consumer (telemetry, achievement + tracker) can subscribe alongside the tutorial. +- `options.tscn` + `options.gd` expose the reset control for players who + want to replay the tutorial after finishing it. diff --git a/.project/objectives/p2-03-hotkey-cheat-sheet.md b/.project/objectives/p2-03-hotkey-cheat-sheet.md index 3272d450..867e238e 100644 --- a/.project/objectives/p2-03-hotkey-cheat-sheet.md +++ b/.project/objectives/p2-03-hotkey-cheat-sheet.md @@ -2,7 +2,7 @@ id: p2-03 title: Hotkey cheat sheet (F1 / ?) priority: p2 -status: partial +status: done scope: game1 owner: shipwright updated_at: 2026-04-17 @@ -10,10 +10,10 @@ evidence: - src/game/engine/scenes/hud/hotkey_sheet.tscn - src/game/engine/scenes/hud/hotkey_sheet.gd - src/game/engine/scenes/hud/top_bar.gd + - src/game/engine/scenes/hud/overlay_panel.gd - src/game/engine/scenes/world_map/world_map.gd - - src/game/engine/scenes/tests/hotkey_sheet_proof.tscn - - src/game/engine/scenes/tests/hotkey_sheet_proof.gd - src/game/engine/tests/unit/test_hotkey_sheet.gd + - src/game/engine/tests/unit/test_hotkey_sheet_dynamic.gd - src/game/engine/tests/unit/test_tutorial_hotkey_wiring.gd - src/game/project.godot - public/games/age-of-dwarves/vocabulary.json @@ -21,91 +21,100 @@ evidence: ## Summary -Non-modal hotkey cheat-sheet overlay shipped. `hotkey_sheet.tscn` is a -`CanvasLayer` (layer=110, above all game HUD) that renders a two-column -binding table grouped into four contexts: **World Map**, **Map Overlays**, -**Menus & Panels**, **Turn Actions**. All 19 entries resolve through -`ThemeVocabulary.lookup("hotkey_*")` β€” zero hardcoded strings. The -controller exposes `toggle()` / `show_sheet()` / `hide_sheet()` / -`is_sheet_visible()` and routes `_unhandled_input` through the new -`ui_help` InputMap action (toggle) and `ui_cancel` (close-when-open). +Non-modal hotkey cheat-sheet overlay renders **dynamically** from +`InputMap.get_actions()`. Every action whose name begins with `ui_` is +bucketed into one of four spec-required context columns β€” **Map / City / +Combat / Menus** β€” via the `ACTION_PREFIX_BUCKET` constant. Adding a new +hotkey means one InputMap action declaration in `project.godot` plus one +`action_` vocab entry; no changes to `hotkey_sheet.gd`. -`project.godot` now declares an `[input]` section with `ui_help` bound -to **F1** and **Shift+/** (`?`). - -GUT test `test_hotkey_sheet.gd` (9/9 passing on apricot, Godot 4.6.2, -GUT 9.6.0, 0.476s) covers: starts-hidden invariant, toggle show/hide -round-trip, show_sheet / hide_sheet behavior, `InputMap.has_action` -confirmation, ui_help-opens, ui_help-closes, ui_cancel-closes, -ui_cancel-while-hidden is a no-op, and binding-table group coverage. - -Proof screenshot captured via -`src/game/engine/scenes/tests/hotkey_sheet_proof.tscn`, saved to -`$SCREENSHOT_HOST:~/Desktop/magic_civ_hotkey_sheet.png` (1920Γ—1080). -Verified in conversation: title, 4 grouped sections, all 19 bindings -visible, vocab-resolved labels, gold-bordered panel, footer. - -Status is `partial` for two reasons: - -1. **Context-group bullet not met as specified.** Spec lists four - groups "Map / City / Combat / Menus"; shipped four groups - "World Map / Map Overlays / Menus & Panels / Turn Actions". - The group names don't match, and the shipped set has no "City" - or "Combat" group (those surfaces have no keybinds yet). This - bullet needs either the spec's exact group names populated - (even if empty) OR user sign-off to renegotiate the group set. - -2. **Dynamic `InputMap.get_actions()` rendering not implemented.** - Spec says "overlay lists all bindings … [from InputMap]"; shipped - a curated `const BINDINGS` table. 95% of current hotkeys are - keycode-literal matches in `_unhandled_key_input` (world_map.gd, - overlay_panel.gd, camera.gd), not InputMap actions β€” so a dynamic - `InputMap.get_actions()` render would miss most bindings. Path - forward is either (a) migrate all hotkey handlers to InputMap - actions first, then render dynamically, or (b) user sign-off that - the curated table satisfies the spec intent. - -Already shipped (bullets βœ“): `ui_help` action bound to F1 + `?`, -closable with same key or ESC, F1-collision with encyclopedia -resolved (encyclopedia now on `ui_encyclopedia`/F2), sheet mounted -into world_map HUD via `_mount_hud_overlays()`. +`project.godot` `[input]` section now declares 15 `ui_*` actions covering +every previously-keycode-literal handler across `overlay_panel.gd` +(11 map-overlay toggles + cycle-view), `top_bar.gd` +(encyclopedia/diplomacy/stats), and `camera.gd` (WASD/arrows handled via +`Input.is_key_pressed` in `_process`, not migrated because they're +continuous-input reads, not discrete action presses β€” dynamic render still +picks up the rest). `ui_help` toggles the sheet itself; `ui_cancel` closes +it. ## Acceptance -- βœ“ `ui_help` input action bound to F1 and `?` β€” verified by - `test_ui_help_action_opens_sheet` asserting - `InputMap.has_action("ui_help")` and the action routing through - `_unhandled_input`. -- βœ— Overlay lists all bindings grouped by context (Map / City / - Combat / Menus). Shipped groups are "World Map / Map Overlays / - Menus & Panels / Turn Actions" β€” names differ from the spec and - "City" + "Combat" groups are absent (those surfaces have no - hotkeys yet). This bullet needs either the spec's exact group - set OR user sign-off to renegotiate. -- βœ— Bindings sourced dynamically from `InputMap.get_actions()` per - spec. Shipped: hardcoded `const BINDINGS` table in - `hotkey_sheet.gd`. Cannot flip βœ“ as specified without migrating - all existing keycode-literal handlers to InputMap actions first. +- βœ“ `ui_help` input action bound to F1 and `?` β€” declared in + `project.godot:[input]`; sheet subscribes via + `_unhandled_input(event.is_action_pressed("ui_help"))`. Covered by + `test_hotkey_sheet.gd::test_ui_help_action_opens_sheet`, + `test_ui_help_action_closes_sheet_when_open`. +- βœ“ Overlay lists all bindings grouped by context (Map / City / Combat / + Menus) β€” `hotkey_sheet.gd::ACTION_PREFIX_BUCKET` declares exactly four + buckets with spec-matching vocab keys `hotkey_group_map`, + `hotkey_group_city`, `hotkey_group_combat`, `hotkey_group_menus`. + Buckets with no actions yet still render their header plus an + `hotkey_group_empty` marker row so the four-column contract is visible + to players. Covered by + `test_hotkey_sheet_dynamic.gd::test_four_buckets_declared`, + `test_city_and_combat_buckets_declared_even_when_empty`, + `test_hotkey_sheet.gd::test_bindings_cover_required_groups`. - βœ“ Closable with same key or ESC β€” both paths tested (`test_ui_help_action_closes_sheet_when_open`, `test_ui_cancel_closes_sheet_when_open`). - βœ“ F1-collision with encyclopedia hotkey resolved β€” `project.godot` - `[input]` section now declares a `ui_encyclopedia` action bound to - **F2** (keycode 4194333). `top_bar.gd` moved the encyclopedia - handler out of the raw `_unhandled_key_input` keycode match and - into `_unhandled_input(event)` consuming the `ui_encyclopedia` - action. Raw `KEY_F1` check removed. `hotkey_sheet.gd:BINDINGS` - now lists **F1 β†’ hotkey_help**, **F2 β†’ hotkey_encyclopedia**, plus - the new **F8 β†’ hotkey_diplomacy** row that landed under p1-01. Tests: + declares `ui_encyclopedia` (F2, keycode 4194333). `top_bar.gd` + consumes it via `_unhandled_input(event.is_action_pressed(...))`; + the old raw `KEY_F1` keycode match is deleted. `F1` now routes + exclusively to `ui_help`. Covered by `test_tutorial_hotkey_wiring.gd::test_ui_encyclopedia_action_exists`, - `test_ui_encyclopedia_bound_to_f2`, `test_ui_help_still_bound`, - `test_hotkey_sheet_lists_help_and_encyclopedia_split` β€” all pass. + `test_ui_encyclopedia_bound_to_f2`, `test_ui_help_still_bound`. +- βœ“ Bindings sourced dynamically from `InputMap.get_actions()` per + spec. `hotkey_sheet.gd::collect_rows_by_bucket()` iterates + `InputMap.get_actions()`, filters to `ui_*` prefix, routes through + `ACTION_PREFIX_BUCKET`, and formats each event via + `OS.get_keycode_string` + modifier prefixes. Mouse-driven controls + (wheel, middle-drag, click) and continuous-input WASD pan live in + `STATIC_ROWS` and are appended post-dynamic β€” these aren't InputMap + actions because Godot doesn't represent wheel/drag gestures that way. + Covered by + `test_hotkey_sheet_dynamic.gd::test_dynamic_render_populates_map_bucket_from_ui_map_actions`, + `test_dynamic_render_routes_menu_actions_to_menus_bucket`, + `test_bucket_for_ui_map_prefix`, `test_bucket_for_ui_menu_prefix`, + `test_bucket_for_unknown_prefix_returns_empty`, + `test_every_ui_map_overlay_has_an_action`. - βœ“ Sheet wired into live game HUD β€” `world_map.gd::_mount_hud_overlays()` - (called from `_start_game()`) adds `HotkeySheetScene.instantiate()` - as a persistent child of the world_map before `TurnManager.start_turn()`. - Because `hotkey_sheet.tscn` sits on `layer=110` above all other HUD - CanvasLayers, its `ui_help` listener wins over any overlay's input - handling. Arena mode is correctly skipped (the mount is inside the - `if not _arena_mode:` branch). Verified by - `test_world_map_mount_hotkey_sheet_const_exists` (asserts both - preload paths resolve at runtime). + (called from `_start_game()`) adds `HotkeySheetScene.instantiate()` as + a persistent child of the world_map. Layer=110 so its `ui_help` + listener wins over other HUD CanvasLayers. Covered by + `test_tutorial_hotkey_wiring.gd::test_world_map_mount_hotkey_sheet_const_exists`. + +## Migrated handlers (keycode-literal β†’ InputMap action) + +- `overlay_panel.gd::_unhandled_key_input` β†’ `_unhandled_input`. 11 + raw `KEY_*` matches (T/M/W/Y/V/R/E/C/P/H/L/F) replaced by + `OVERLAY_ACTIONS` dict iterating 11 `ui_map_overlay_*` actions + + `ui_map_cycle_view`. +- `top_bar.gd::_unhandled_key_input` β†’ `_unhandled_input`. F1/F8/F9 raw + matches replaced by `ui_encyclopedia` / `ui_menu_diplomacy` / + `ui_menu_stats`. +- `camera.gd::_handle_keyboard_pan` unchanged: continuous-input + `Input.is_key_pressed` in `_process(delta)` is the correct Godot + pattern for sustained directional pan β€” not a discrete action event. + WASD/arrows are documented in the sheet's `STATIC_ROWS`. + +## Tests + +- `test_hotkey_sheet.gd` β€” 9/9 passing (existing tests, updated to read + `ACTION_PREFIX_BUCKET` instead of the removed `BINDINGS` constant). +- `test_hotkey_sheet_dynamic.gd` β€” 8/8 passing (new, 29 assertions + covering four-bucket contract, dynamic render, actionβ†’bucket routing, + and every `ui_map_overlay_*` action present). +- `test_tutorial_hotkey_wiring.gd` β€” 7/7 passing (F1/F2 collision, + `ui_encyclopedia` action, dynamic render includes F2). + +**24/24 hotkey tests green on apricot.** + +## Integration + +- `hotkey_sheet.gd` is now the canonical source of "what hotkeys exist" + for players β€” adding a new `ui_*` action automatically appears here + with no scene edit. +- `ACTION_PREFIX_BUCKET` is a documented extension point: to add a + future "City Actions" context, author `ui_city_*` actions in + `project.godot` and the City bucket auto-populates. diff --git a/src/game/engine/tests/unit/test_tutorial_overlay.gd b/src/game/engine/tests/unit/test_tutorial_overlay.gd index 7d80d59f..2683c521 100644 --- a/src/game/engine/tests/unit/test_tutorial_overlay.gd +++ b/src/game/engine/tests/unit/test_tutorial_overlay.gd @@ -51,7 +51,7 @@ func test_go_back_at_step_one_is_noop() -> void: func test_advance_past_final_step_completes_and_frees() -> void: - for _i: int in range(TutorialOverlayScript.TOTAL_STEPS): + for _i: int in range(_overlay.total_steps()): _overlay.advance() await get_tree().process_frame assert_false( diff --git a/src/packages/guide/src/types/ambient.d.ts b/src/packages/guide/src/types/ambient.d.ts new file mode 100644 index 00000000..70a7ac8f --- /dev/null +++ b/src/packages/guide/src/types/ambient.d.ts @@ -0,0 +1,118 @@ +// ─── @magic-civ/physics-rs ambient stub ─────────────────────────────────── +// +// The WASM build output lives at `src/simulator/pkg/` on the RUN host only +// (see CLAUDE.md two-host workflow: WASM builds run on apricot; EDIT host +// has no Rust toolchain and `pkg/` is gitignored there). Guide-engine's +// tsc --noEmit therefore can't resolve the real `.d.ts` shipped with the +// WASM pkg. +// +// This file is a pure ambient declaration file (no imports/exports β€” +// intentional, so these `declare module` blocks register globally). It +// types the WASM class surface that `runner.ts` re-exports, just enough +// for guide-engine typecheck to pass. The consumer app doesn't use this +// stub β€” its own vite alias resolves to the real JS/.d.ts. + +declare module '@magic-civ/physics-rs' { + export class WasmClimatePhysics { + constructor(paramsJson: string, terrainJson: string, specJson: string) + free(): void + processStep(grid: WasmGrid, turn: number, seed: number, dt: number): void + } + export class WasmEcologyPhysics { + constructor() + free(): void + processStep(grid: WasmGrid, dt: number): void + } + export class WasmMapGenerator { + constructor(paramsJson: string) + free(): void + generate(seed: number, mode: string): WasmGrid + } + export class WasmGrid { + constructor() + free(): void + toJSON(): unknown + static fromJSON(json: unknown): WasmGrid + } +} + +// ─── @resources/* JSON glob (Vite alias) ────────────────────────────────── +// +// The consumer app resolves `@resources/*` via Vite alias to +// `../../../../resources/*`. Guide-engine's tsc has no such alias β€” these +// module declarations type the handful of resource JSON files it imports +// directly. Schemas mirror the on-disk JSON shape as the guide reads it. + +declare module '@resources/ecology/traits/trait_constraints.json' { + interface InvalidPair { a: string; b: string; reason: string } + interface SpecialRule { condition: string; reason: string } + const value: { + invalid_pairs: InvalidPair[] + special_rules: SpecialRule[] + } + export default value +} + +declare module '@resources/ecology/traits/species_generation.json' { + const value: { + quality_base_by_size: Record + quality_modifiers: Record + growth_rate_base: number + growth_rate_multipliers: Record + } + export default value +} + +declare module '@resources/ecology/traits/trait_definitions.json' { + interface TierDef { tier: number; label: string; color: string; description: string } + const value: { + categories: Record + tier_system: { tiers: TierDef[] } + } + export default value +} + +declare module '@resources/ecology/biomes/*.json' { + const value: { + id: string + name: string + temp_range: [number, number] + moisture_range: [number, number] + [key: string]: unknown + } + export default value +} + +declare module '@resources/episodes.json' { + interface EpisodeManifest { + id: string + name: string + display_name: string + route?: string + [key: string]: unknown + } + const value: EpisodeManifest[] + export default value +} + +// Catch-all for any other @resources/* JSON the guide may import. +declare module '@resources/*' { + const value: unknown + export default value +} + +// ─── @lilith/ui-theme β€” used by theme helpers ───────────────────────────── +// +// Already installed transitively via pnpm but not in guide-engine's +// package.json. The guide-engine only uses a `DeepPartial` helper β€” stub +// it here to keep theme/buildTheme.ts + theme/RaceThemeProvider.tsx +// type-checking without pulling the full dep into guide-engine. + +declare module '@lilith/ui-theme' { + export type DeepPartial = T extends object + ? { [K in keyof T]?: DeepPartial } + : T + export const ThemeProvider: unknown + const _default: unknown + export default _default +} diff --git a/src/packages/guide/src/types/declarations.d.ts b/src/packages/guide/src/types/declarations.d.ts index 06f3b6d5..b6012220 100644 --- a/src/packages/guide/src/types/declarations.d.ts +++ b/src/packages/guide/src/types/declarations.d.ts @@ -1,2 +1,55 @@ -// Placeholder β€” add module declarations here as needed -export {} +// ─── styled-components DefaultTheme augmentation ─────────────────────────── +// +// The guide runtime theme is built by `theme/buildTheme.ts` for a given +// (race, gender, colorMode, dyslexicFont) combination. The consumer app's +// `RaceThemeProvider` passes the merged theme into styled-components. +// +// This augmentation describes the shape of that theme so every +// `${({ theme }) => theme.colors.xxx}` interpolation type-checks. + +import 'styled-components' + +declare module 'styled-components' { + export interface ColorTriad { + main: string + dark: string + light: string + } + + export interface DefaultTheme { + colors: { + primary: ColorTriad + accent: ColorTriad + background: { + primary: string + secondary: string + tertiary: string + } + surface: string + border: { + default: string + hover: string + } + text: { + primary: string + secondary: string + muted: string + tertiary: string + } + } + typography?: { + fontFamily: { + heading: string + body: string + mono: string + } + fontWeight: { + light: number + normal: number + medium: number + semibold: number + bold: number + } + } + } +} diff --git a/src/packages/guide/src/types/game-data.ts b/src/packages/guide/src/types/game-data.ts index 812a4837..ed786583 100644 --- a/src/packages/guide/src/types/game-data.ts +++ b/src/packages/guide/src/types/game-data.ts @@ -70,27 +70,51 @@ export interface UnitGender { export interface Unit { id: string name: string - combat_type: string - school: string | null + combat_type?: string + /** "player", "wild", "npc" β€” classification from JSON. */ + unit_type?: string + school?: string | null domain: string - armor_type: string + armor_type?: string attack_type: string - str: number - con: number - dex: number - int: number + /** Flat HP field used by wild-unit JSON schemas. */ + hp?: number + attack?: number + defense?: number + ranged_attack?: number + tier?: number + str?: number + con?: number + dex?: number + int?: number cost: number range: number movement: number vision: number tech_required: string | null + upgradeable_from?: string | null race_required: string | null faction: string | null - keywords: string[] + can_found_city?: boolean + can_build_improvements?: boolean + /** Open-ended capability tags (e.g. "wild", "apex_predator"). */ + flags?: string[] + /** Genus/function attributes from JSON. */ + attributes?: string[] + keywords?: string[] + terrain_bonus?: Record description: string - mana_cost: number | null + mana_cost?: number | null sprite?: string gender?: UnitGender | null + maintenance?: number + encyclopedia?: { + category: string + entry_type: string + detail_route?: string + tags?: string[] + } + [key: string]: unknown } // ─── Buildings ─────────────────────────────────────────────────────────────── @@ -397,20 +421,36 @@ export interface DisciplinesData { // ─── Tech Tree ────────────────────────────────────────────────────────────── +export interface TechUnlocks { + buildings: string[] + units: string[] + improvements: string[] + other: string[] +} + export interface Tech { id: string name: string cost: number - prereqs: string[] + /** JSON field `requires` β€” predecessor tech IDs that must be researched first. */ + requires: string[] pillar: string + era?: number tier: number - school: string | null + school?: string | null alignment_shift?: number blocks_tech?: string[] - unlocks_units: string[] - unlocks_buildings: string[] - unlocks_spells: string[] + /** JSON field `unlocks` β€” categorised content unlocked on research. */ + unlocks: TechUnlocks description: string + flavor?: string + encyclopedia?: { + category: string + entry_type: string + detail_route?: string + tags?: string[] + } + [key: string]: unknown } // ─── Items ────────────────────────────────────────────────────────────────── diff --git a/src/packages/guide/tsconfig.json b/src/packages/guide/tsconfig.json index 3fad8aaa..6b56ff14 100644 --- a/src/packages/guide/tsconfig.json +++ b/src/packages/guide/tsconfig.json @@ -20,6 +20,9 @@ "paths": { "@magic-civ/engine-ts": [ "../engine-ts/src/index.ts" + ], + "@magic-civ/web-civmap": [ + "../web-civmap/src/index.ts" ] } }, diff --git a/src/simulator/crates/mc-ai/src/gpu/inner.rs b/src/simulator/crates/mc-ai/src/gpu/inner.rs index 97dae387..67598b68 100644 --- a/src/simulator/crates/mc-ai/src/gpu/inner.rs +++ b/src/simulator/crates/mc-ai/src/gpu/inner.rs @@ -162,11 +162,23 @@ impl GpuContext { ..Default::default() }); + // Probe hardware first; fall back to software rasterizer (e.g. llvmpipe / + // lavapipe, WARP, swiftshader) so headless CI and Vulkan-software-only + // hosts can still exercise the GPU path for parity / regression testing. + // Production hosts pick the hardware adapter; software-only hosts pick + // the fallback. Either way `GpuContext::shared()` returns `Some`. let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface: None, force_fallback_adapter: false, - }))?; + })) + .or_else(|| { + block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::LowPower, + compatible_surface: None, + force_fallback_adapter: true, + })) + })?; let backend = format!("{:?}", adapter.get_info().backend);