feat(@projects/@magic-civilization): ✅ mark tutorial and hotkey sheets as complete
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
53bdd999d7
commit
763366d01c
9 changed files with 429 additions and 179 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
118
src/packages/guide/src/types/ambient.d.ts
vendored
Normal file
118
src/packages/guide/src/types/ambient.d.ts
vendored
Normal 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
|
||||
}
|
||||
57
src/packages/guide/src/types/declarations.d.ts
vendored
57
src/packages/guide/src/types/declarations.d.ts
vendored
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ──────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@
|
|||
"paths": {
|
||||
"@magic-civ/engine-ts": [
|
||||
"../engine-ts/src/index.ts"
|
||||
],
|
||||
"@magic-civ/web-civmap": [
|
||||
"../web-civmap/src/index.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue