feat(@projects/@magic-civilization): mark tutorial and hotkey sheets as complete

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-17 11:11:47 -07:00
parent 53bdd999d7
commit 763366d01c
9 changed files with 429 additions and 179 deletions

View file

@ -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 |

View file

@ -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.

View file

@ -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_<name>` 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.

View file

@ -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(

View file

@ -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<string, number>
quality_modifiers: Record<string, number>
growth_rate_base: number
growth_rate_multipliers: Record<string, number>
}
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<string, { values: string[]; description: string }>
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> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
export const ThemeProvider: unknown
const _default: unknown
export default _default
}

View file

@ -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
}
}
}
}

View file

@ -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<string, { attack?: number; defense?: number }>
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 ──────────────────────────────────────────────────────────────────

View file

@ -20,6 +20,9 @@
"paths": {
"@magic-civ/engine-ts": [
"../engine-ts/src/index.ts"
],
"@magic-civ/web-civmap": [
"../web-civmap/src/index.ts"
]
}
},

View file

@ -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);