feat(@projects/@magic-civilization): add tutorial reset and diplomacy hotkeys

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-17 08:15:18 -07:00
parent 986cf87f5f
commit 032602805b
9 changed files with 166 additions and 13 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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