diff --git a/public/games/age-of-dwarves/vocabulary.json b/public/games/age-of-dwarves/vocabulary.json index 522cb89f..9dd9a8e3 100644 --- a/public/games/age-of-dwarves/vocabulary.json +++ b/public/games/age-of-dwarves/vocabulary.json @@ -347,5 +347,10 @@ "hotkey_ingame_menu": "In-game menu / Close", "hotkey_help": "This cheat sheet", "hotkey_end_turn": "End turn", - "hotkey_select_move": "Select / move unit" + "hotkey_select_move": "Select / move unit", + "options_reset_tutorial": "Reset Tutorial", + "options_tutorial_reset_label": "Replay on next start", + "options_tutorial_reset_done": "Tutorial will replay", + "hotkey_encyclopedia_key": "F2", + "hotkey_diplomacy": "Diplomacy" } diff --git a/src/game/engine/scenes/hud/hotkey_sheet.gd b/src/game/engine/scenes/hud/hotkey_sheet.gd index d724abf5..873ad490 100644 --- a/src/game/engine/scenes/hud/hotkey_sheet.gd +++ b/src/game/engine/scenes/hud/hotkey_sheet.gd @@ -18,7 +18,9 @@ const BINDINGS: Array[Dictionary] = [ {"group": "hotkey_group_overlays", "keys": "R", "label": "hotkey_overlay_water"}, {"group": "hotkey_group_overlays", "keys": "E", "label": "hotkey_overlay_elevation"}, {"group": "hotkey_group_overlays", "keys": "L", "label": "hotkey_overlay_ley_line"}, - {"group": "hotkey_group_menus", "keys": "F1", "label": "hotkey_encyclopedia"}, + {"group": "hotkey_group_menus", "keys": "F1", "label": "hotkey_help"}, + {"group": "hotkey_group_menus", "keys": "F2", "label": "hotkey_encyclopedia"}, + {"group": "hotkey_group_menus", "keys": "F8", "label": "hotkey_diplomacy"}, {"group": "hotkey_group_menus", "keys": "F9", "label": "hotkey_stats"}, {"group": "hotkey_group_menus", "keys": "?", "label": "hotkey_help"}, {"group": "hotkey_group_menus", "keys": "Esc", "label": "hotkey_ingame_menu"}, diff --git a/src/game/engine/scenes/hud/top_bar.gd b/src/game/engine/scenes/hud/top_bar.gd index eb5f38c8..02a11509 100644 --- a/src/game/engine/scenes/hud/top_bar.gd +++ b/src/game/engine/scenes/hud/top_bar.gd @@ -293,6 +293,14 @@ func _on_happiness_hover_exit() -> void: _breakdown_popup = null +func _unhandled_input(event: InputEvent) -> void: + ## Encyclopedia now routes through the `ui_encyclopedia` InputMap action + ## (default: F2) so F1 stays free for `ui_help` / hotkey_sheet. + if event.is_action_pressed("ui_encyclopedia"): + get_viewport().set_input_as_handled() + _on_encyclopedia_pressed() + + func _unhandled_key_input(event: InputEvent) -> void: if not event is InputEventKey: return @@ -300,9 +308,6 @@ func _unhandled_key_input(event: InputEvent) -> void: if key.pressed and not key.echo and key.keycode == KEY_F9: get_viewport().set_input_as_handled() _on_stats_pressed() - elif key.pressed and not key.echo and key.keycode == KEY_F1: - get_viewport().set_input_as_handled() - _on_encyclopedia_pressed() elif key.pressed and not key.echo and key.keycode == KEY_F8: get_viewport().set_input_as_handled() _on_diplomacy_pressed() diff --git a/src/game/engine/scenes/menus/options.gd b/src/game/engine/scenes/menus/options.gd index a3d72f78..9d55ab9e 100644 --- a/src/game/engine/scenes/menus/options.gd +++ b/src/game/engine/scenes/menus/options.gd @@ -48,6 +48,7 @@ var _defaults: RefCounted @onready var _autosave_next: Button = %AutosaveIntervalNextButton @onready var _autosave_label: Label = %AutosaveIntervalValueLabel @onready var _tooltips_check: CheckBox = %TooltipsCheck +@onready var _reset_tutorial_button: Button = %ResetTutorialButton # -- Game Defaults -- @onready var _map_size_prev: Button = %DefaultMapSizePrevButton @@ -110,6 +111,7 @@ func _connect_signals() -> void: _autosave_prev.pressed.connect(_on_autosave_interval_prev) _autosave_next.pressed.connect(_on_autosave_interval_next) _tooltips_check.toggled.connect(_on_tooltips_toggled) + _reset_tutorial_button.pressed.connect(_on_reset_tutorial_pressed) _map_size_prev.pressed.connect(_defaults.on_map_size_prev) _map_size_next.pressed.connect(_defaults.on_map_size_next) @@ -422,6 +424,13 @@ func _on_tooltips_toggled(pressed: bool) -> void: SettingsManager.set_setting("gameplay", "show_tooltips", pressed) +## Clears the `tutorial_completed` flag so the first-run overlay shows again +## on the next world_map boot. Label flips to the "done" string as confirmation. +func _on_reset_tutorial_pressed() -> void: + SettingsManager.set_setting("gameplay", "tutorial_completed", false) + _reset_tutorial_button.text = ThemeVocabulary.lookup("options_tutorial_reset_done") + + # -- Privacy -- func _refresh_privacy() -> void: diff --git a/src/game/engine/scenes/menus/options.tscn b/src/game/engine/scenes/menus/options.tscn index f985a5ff..981a1d5e 100644 --- a/src/game/engine/scenes/menus/options.tscn +++ b/src/game/engine/scenes/menus/options.tscn @@ -748,6 +748,28 @@ size_flags_vertical = 4 button_pressed = true text = "Enabled" +[node name="ResetTutorialRow" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ContentVBox"] +layout_mode = 2 +custom_minimum_size = Vector2(0, 40) + +[node name="ResetTutorialLabel" type="Label" parent="MarginContainer/VBoxContainer/ScrollContainer/ContentVBox/ResetTutorialRow"] +layout_mode = 2 +custom_minimum_size = Vector2(220, 0) +theme_override_font_sizes/font_size = 15 +text = "Reset Tutorial" +vertical_alignment = 1 + +[node name="Spacer" type="Control" parent="MarginContainer/VBoxContainer/ScrollContainer/ContentVBox/ResetTutorialRow"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ResetTutorialButton" type="Button" parent="MarginContainer/VBoxContainer/ScrollContainer/ContentVBox/ResetTutorialRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 4 +custom_minimum_size = Vector2(160, 32) +text = "Replay on next start" + ; ===================== GAME DEFAULTS ===================== [node name="DefaultsSectionRow" type="HBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer/ContentVBox"] diff --git a/src/game/engine/scenes/world_map/world_map.gd b/src/game/engine/scenes/world_map/world_map.gd index df9793b8..8cca8bd3 100644 --- a/src/game/engine/scenes/world_map/world_map.gd +++ b/src/game/engine/scenes/world_map/world_map.gd @@ -17,6 +17,9 @@ const OverlayRendererScript: GDScript = preload( const FogRendererScript: GDScript = preload("res://engine/src/rendering/fog_renderer.gd") const CityScreenScene: PackedScene = preload("res://engine/scenes/city/city_screen.tscn") const ChroniclePanelScene: PackedScene = preload("res://engine/scenes/hud/chronicle_panel.tscn") +const TutorialOverlayScene: PackedScene = preload("res://engine/scenes/hud/tutorial_overlay.tscn") +const HotkeySheetScene: PackedScene = preload("res://engine/scenes/hud/hotkey_sheet.tscn") +const TutorialOverlayScript: GDScript = preload("res://engine/scenes/hud/tutorial_overlay.gd") const WorldMapCombatScript: GDScript = preload("res://engine/scenes/world_map/world_map_combat.gd") const WorldMapCityActionsScript: GDScript = preload( "res://engine/scenes/world_map/world_map_city_actions.gd" @@ -209,6 +212,7 @@ func _start_game() -> void: _sync_units() if not _arena_mode: _update_hud() + _mount_hud_overlays() var local_player_index: int = 0 if player != null: @@ -220,6 +224,15 @@ func _start_game() -> void: TurnManager.start_turn() +## Persistent HUD overlays mounted once per world-map boot. The hotkey sheet +## is always present so `ui_help` (F1 / ?) works in-game; the first-run +## tutorial is gated on `TutorialOverlay.should_show_on_first_run()`. +func _mount_hud_overlays() -> void: + add_child(HotkeySheetScene.instantiate()) + if TutorialOverlayScript.should_show_on_first_run(): + add_child(TutorialOverlayScene.instantiate()) + + func _update_fog(player: RefCounted, game_map: RefCounted) -> void: var arrays: Array = WorldMapVisionScript.build_fog_arrays(player, game_map) _hex_renderer.update_fog(arrays[0], arrays[1]) diff --git a/src/game/engine/tests/unit/ai/test_simple_heuristic_ai_war_gate.gd b/src/game/engine/tests/unit/ai/test_simple_heuristic_ai_war_gate.gd index 9a63daee..a86ecd42 100644 --- a/src/game/engine/tests/unit/ai/test_simple_heuristic_ai_war_gate.gd +++ b/src/game/engine/tests/unit/ai/test_simple_heuristic_ai_war_gate.gd @@ -32,26 +32,29 @@ func after_each() -> void: func _make_player(idx: int) -> RefCounted: - var p: RefCounted = PlayerScript.new() - p.player_index = idx + # PlayerScript._init signature: (p_index, p_player_name, p_race_id). + # `index` is the real field name (not player_index). + var p: RefCounted = PlayerScript.new(idx, "Player%d" % idx, "dwarf") p.units = [] p.cities = [] return p func _make_warrior(owner_idx: int, pos: Vector2i) -> RefCounted: - var u: RefCounted = UnitScript.new() - u.type_id = "dwarf_warrior" - u.owner_index = owner_idx - u.position = pos + # UnitScript._init signature: (p_unit_id, p_owner, p_position). `owner` + # is the real field name (not owner_index). `is_alive` is a method on + # the class, not a per-instance callable we can override. + var u: RefCounted = UnitScript.new("warrior", owner_idx, pos) u.hp = 10 - u.is_alive = func() -> bool: return u.hp > 0 + u.max_hp = 10 return u func _make_city(owner_idx: int, pos: Vector2i) -> RefCounted: + # CityScript exposes both `owner` (write-through setter) and + # `owner_index`. `owner` is the one the AI reads. var c: RefCounted = CityScript.new() - c.owner_index = owner_idx + c.owner = owner_idx c.position = pos return c diff --git a/src/game/engine/tests/unit/test_tutorial_hotkey_wiring.gd b/src/game/engine/tests/unit/test_tutorial_hotkey_wiring.gd new file mode 100644 index 00000000..78d97730 --- /dev/null +++ b/src/game/engine/tests/unit/test_tutorial_hotkey_wiring.gd @@ -0,0 +1,89 @@ +extends GutTest +## p1-03 tutorial wiring + p2-03 hotkey_sheet wiring + F1 collision migration. + +const TutorialOverlayScript: GDScript = preload("res://engine/scenes/hud/tutorial_overlay.gd") +const HotkeySheetScene: PackedScene = preload("res://engine/scenes/hud/hotkey_sheet.tscn") + + +func before_all() -> void: + DataLoader.load_theme("age-of-dwarves") + ThemeVocabulary.load_vocabulary("age-of-dwarves") + + +# -- p1-03: tutorial reset + first-run path -- + + +func test_tutorial_reset_flag_flips_should_show() -> void: + SettingsManager.set_setting("gameplay", "tutorial_completed", true) + assert_false(TutorialOverlayScript.should_show_on_first_run(), + "seen tutorial must NOT reshow") + SettingsManager.set_setting("gameplay", "tutorial_completed", false) + assert_true(TutorialOverlayScript.should_show_on_first_run(), + "after reset, tutorial should show again") + + +func test_world_map_mount_hotkey_sheet_const_exists() -> void: + ## world_map.gd preloads HotkeySheetScene + TutorialOverlayScene and calls + ## `_mount_hud_overlays()` in _start_game — verify the scene file exists + ## at the path the preload resolves. + assert_true(ResourceLoader.exists("res://engine/scenes/hud/hotkey_sheet.tscn"), + "hotkey_sheet.tscn must exist at the path world_map.gd preloads") + assert_true(ResourceLoader.exists("res://engine/scenes/hud/tutorial_overlay.tscn"), + "tutorial_overlay.tscn must exist at the path world_map.gd preloads") + + +# -- p2-03: ui_encyclopedia InputMap migration -- + + +func test_ui_encyclopedia_action_exists() -> void: + assert_true(InputMap.has_action("ui_encyclopedia"), + "ui_encyclopedia InputMap action must exist (F1 migration)") + + +func test_ui_encyclopedia_bound_to_f2() -> void: + var events: Array = InputMap.action_get_events("ui_encyclopedia") + var found_f2: bool = false + for event in events: + if event is InputEventKey: + var key: InputEventKey = event as InputEventKey + if key.keycode == KEY_F2: + found_f2 = true + break + assert_true(found_f2, "ui_encyclopedia must be bound to F2") + + +func test_ui_help_still_bound() -> void: + assert_true(InputMap.has_action("ui_help"), + "ui_help must still exist (F1 / ?)") + + +# -- p2-03: hotkey_sheet lists new bindings -- + + +func test_hotkey_sheet_lists_help_and_encyclopedia_split() -> void: + var sheet: CanvasLayer = HotkeySheetScene.instantiate() + add_child_autofree(sheet) + await wait_frames(1) + var bindings: Array = sheet.BINDINGS + var has_help_f1: bool = false + var has_encyclopedia_f2: bool = false + for entry: Dictionary in bindings: + if entry.get("keys") == "F1" and entry.get("label") == "hotkey_help": + has_help_f1 = true + if entry.get("keys") == "F2" and entry.get("label") == "hotkey_encyclopedia": + has_encyclopedia_f2 = true + assert_true(has_help_f1, "BINDINGS must include F1 → hotkey_help") + assert_true(has_encyclopedia_f2, + "BINDINGS must include F2 → hotkey_encyclopedia (migrated)") + + +# -- options reset checkbox -- + + +func test_options_reset_tutorial_vocab_exists() -> void: + assert_ne(ThemeVocabulary.lookup("options_reset_tutorial"), + "options_reset_tutorial", + "options_reset_tutorial vocab key must resolve") + assert_ne(ThemeVocabulary.lookup("options_tutorial_reset_done"), + "options_tutorial_reset_done", + "options_tutorial_reset_done vocab key must resolve") diff --git a/src/game/project.godot b/src/game/project.godot index 4d4d427a..1bf18b22 100644 --- a/src/game/project.godot +++ b/src/game/project.godot @@ -43,6 +43,11 @@ ui_help={ , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":47,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +ui_encyclopedia={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194333,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} [rendering]