magicciv/.project/objectives/p2-03-hotkey-cheat-sheet.md
Natalie 763366d01c feat(@projects/@magic-civilization): mark tutorial and hotkey sheets as complete
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-04-17 11:11:47 -07:00

5.8 KiB

id title priority status scope owner updated_at evidence
p2-03 Hotkey cheat sheet (F1 / ?) p2 done game1 shipwright 2026-04-17
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/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

Summary

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 [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 ? — 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 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.
  • ✓ 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. 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.