From 6f63d56c614ccfb2f7169585acf7826183b10def Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 16 Apr 2026 15:12:15 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20wild=20unit=20variants=20and=20guide=20update?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .project/iteration_log.md | 1 + src/game/engine/scenes/hud/end_turn_button.gd | 1 + src/game/engine/scenes/hud/world_map_hud.gd | 4 + src/game/engine/scenes/menus/how_to_play.gd | 51 ++++++++ src/game/engine/scenes/menus/how_to_play.tscn | 112 ++++++++++++++++++ src/game/engine/scenes/menus/main_menu.gd | 18 ++- src/game/engine/scenes/menus/main_menu.tscn | 33 +++++- src/game/project.godot | 1 + src/simulator/crates/mc-city/src/city.rs | 20 ++-- 9 files changed, 227 insertions(+), 14 deletions(-) create mode 100644 src/game/engine/scenes/menus/how_to_play.gd create mode 100644 src/game/engine/scenes/menus/how_to_play.tscn diff --git a/.project/iteration_log.md b/.project/iteration_log.md index 23233975..2566ead1 100644 --- a/.project/iteration_log.md +++ b/.project/iteration_log.md @@ -50,3 +50,4 @@ 2026-04-16 14:36 Task #12 MCTS FOUNDATION complete: new src/simulator/crates/mc-ai/src/mcts_tree.rs (138 lines) + tests (78 lines). Arena-allocated tree with UCB1 select/expand/simulate/backpropagate. Existing mcts.rs bandit left untouched. 19/19 tests pass. Not wired to GDExtension yet — foundation only. Future work: connect to game state + define Action type from actual game decisions. (mcts-dev) 2026-04-16 14:47 Task #17 T50 DETERMINISM — CRITICAL BREAKTHROUGH: root cause was SEED INGESTION bug in game_state.gd, NOT mc-combat. `game_settings["seed"]` was read but never written to `GameState.map_seed`. RNG fell through to hash(Time.get_unix_time_from_system()) → wall-clock seed each run. Prior "byte-identical" reports from tasks #6/#9 were ILLUSIONS (same-second wall-clock coincidence, OR self-comparison). 3-line fix in game_state.gd:113-115 ingests settings_seed if nonzero. Verified: 2x seed=1 52-turn runs truly byte-identical. T1 rng_seed=1 both (was 3059205916/2794811774). T50 save equal. Combat counts match at T50. (t50-determinism-dev) 2026-04-16 14:47 Task #18 PLAYER GUIDE UPDATE complete: new Personality Axes page at /empire/personality in guide web app. PersonalityAxesPage.tsx renders 6-axis explainer + race strategic axes grid. Pulls from @resources/races/strategic_axes.json (no hardcoded data). Wired through lazy-pages.ts, App.tsx route, nav.tsx sidebar. pnpm typecheck clean. Visual verification blocked by WASM not built on macOS (environment, not code). (guide-dev) +2026-04-16 15:06 BATCH 9 (post ttv-v2 heal_suppress 5): 10 PASS / 4 FAIL. Seeds T145/T182/T157 victory. Median TTV 157 (below 200 target, WORSE than batch 8's 166). victories 3/3=100% (above 80% target). Heal-suppress extension had WRONG direction — longer suppress made cities die faster because damage accumulates. Need to REVERT 5→3 or raise BASE_CITY_HP to push TTV up. Seed 1 has imp=0 (task #29 worker fix didn't cascade here — some seed still lacks worker). Batch baselines: checklist stayed at 10/4 pre→post. diff --git a/src/game/engine/scenes/hud/end_turn_button.gd b/src/game/engine/scenes/hud/end_turn_button.gd index a18a294f..84f8b2c6 100644 --- a/src/game/engine/scenes/hud/end_turn_button.gd +++ b/src/game/engine/scenes/hud/end_turn_button.gd @@ -11,6 +11,7 @@ func _ready() -> void: _apply_panel_style() _apply_button_style() _end_turn_btn.pressed.connect(_on_end_turn_pressed) + _end_turn_btn.tooltip_text = ThemeVocabulary.lookup("end_turn") EventBus.turn_started.connect(_on_turn_started) _update_turn_display() diff --git a/src/game/engine/scenes/hud/world_map_hud.gd b/src/game/engine/scenes/hud/world_map_hud.gd index a010df10..8d51d65f 100644 --- a/src/game/engine/scenes/hud/world_map_hud.gd +++ b/src/game/engine/scenes/hud/world_map_hud.gd @@ -67,6 +67,7 @@ func _build_top_bar() -> void: _tech_button = Button.new() _tech_button.name = "TechButton" _tech_button.text = ThemeVocabulary.lookup("tech_tree") + _tech_button.tooltip_text = "%s [T]" % ThemeVocabulary.lookup("tech_tree") _tech_button.custom_minimum_size = Vector2(100, 36) _tech_button.pressed.connect(_on_tech_button_pressed) top_bar.add_child(_tech_button) @@ -74,6 +75,7 @@ func _build_top_bar() -> void: _chronicle_button = Button.new() _chronicle_button.name = "ChronicleButton" _chronicle_button.text = ThemeVocabulary.lookup("chronicle") + _chronicle_button.tooltip_text = "%s [C]" % ThemeVocabulary.lookup("chronicle") _chronicle_button.custom_minimum_size = Vector2(100, 36) _chronicle_button.pressed.connect(_on_chronicle_button_pressed) top_bar.add_child(_chronicle_button) @@ -81,6 +83,7 @@ func _build_top_bar() -> void: _end_turn_button = Button.new() _end_turn_button.name = "EndTurnButton" _end_turn_button.text = ThemeVocabulary.lookup("end_turn") + _end_turn_button.tooltip_text = ThemeVocabulary.lookup("end_turn") _end_turn_button.custom_minimum_size = Vector2(120, 36) _end_turn_button.pressed.connect(_on_end_turn_button_pressed) top_bar.add_child(_end_turn_button) @@ -121,6 +124,7 @@ func _build_unit_panel() -> void: _build_improvement_button = Button.new() _build_improvement_button.name = "BuildImprovementButton" _build_improvement_button.text = ThemeVocabulary.lookup("build_improvement") + _build_improvement_button.tooltip_text = "%s [B]" % ThemeVocabulary.lookup("build_improvement") _build_improvement_button.custom_minimum_size = Vector2(140, 32) _build_improvement_button.visible = false _build_improvement_button.pressed.connect(_on_build_improvement_button_pressed) diff --git a/src/game/engine/scenes/menus/how_to_play.gd b/src/game/engine/scenes/menus/how_to_play.gd new file mode 100644 index 00000000..7e355ca4 --- /dev/null +++ b/src/game/engine/scenes/menus/how_to_play.gd @@ -0,0 +1,51 @@ +extends Control +## How-to-play tutorial overlay. Shows controls, goal, and clan personalities +## for first-time players. Reachable from main menu. + +const AI_PERSONALITIES_PATH: String = "res://public/games/age-of-dwarves/data/ai_personalities.json" +const GUIDE_URL: String = "https://magic-civilization.com/guide" + +@onready var _back_button: Button = %BackButton +@onready var _guide_link_button: Button = %GuideLinkButton +@onready var _clans_container: VBoxContainer = %ClansContainer + + +func _ready() -> void: + _back_button.pressed.connect(_on_back_pressed) + _guide_link_button.pressed.connect(_on_guide_link_pressed) + _populate_clans() + _back_button.grab_focus() + + +func _populate_clans() -> void: + var file: FileAccess = FileAccess.open(AI_PERSONALITIES_PATH, FileAccess.READ) + if file == null: + return + var parsed: Dictionary = JSON.parse_string(file.get_as_text()) + file.close() + if parsed.is_empty(): + return + for clan_id: String in parsed.keys(): + var clan: Dictionary = parsed[clan_id] + var row: VBoxContainer = VBoxContainer.new() + row.add_theme_constant_override("separation", 2) + var name_label: Label = Label.new() + name_label.text = clan.get("name", clan_id) + name_label.add_theme_font_size_override("font_size", 16) + name_label.add_theme_color_override("font_color", Color(0.95, 0.82, 0.3)) + row.add_child(name_label) + var desc_label: Label = Label.new() + desc_label.text = clan.get("description", "") + desc_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + desc_label.add_theme_font_size_override("font_size", 13) + desc_label.add_theme_color_override("font_color", Color(0.85, 0.8, 0.7)) + row.add_child(desc_label) + _clans_container.add_child(row) + + +func _on_back_pressed() -> void: + get_tree().change_scene_to_file("res://engine/scenes/menus/main_menu.tscn") + + +func _on_guide_link_pressed() -> void: + OS.shell_open(GUIDE_URL) diff --git a/src/game/engine/scenes/menus/how_to_play.tscn b/src/game/engine/scenes/menus/how_to_play.tscn new file mode 100644 index 00000000..3292d3e4 --- /dev/null +++ b/src/game/engine/scenes/menus/how_to_play.tscn @@ -0,0 +1,112 @@ +[gd_scene load_steps=3 format=3 uid="uid://b1h2p3l4y0g1d"] + +[ext_resource type="Script" path="res://engine/scenes/menus/how_to_play.gd" id="1"] +[ext_resource type="Theme" path="res://public/games/age-of-dwarves/ui_theme.tres" id="2_theme"] + +[node name="HowToPlay" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme = ExtResource("2_theme") +script = ExtResource("1") + +[node name="Background" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.055, 0.04, 0.09, 1) + +[node name="Margin" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +theme_override_constants/margin_left = 64 +theme_override_constants/margin_right = 64 +theme_override_constants/margin_top = 32 +theme_override_constants/margin_bottom = 32 + +[node name="VBox" type="VBoxContainer" parent="Margin"] +layout_mode = 2 +theme_override_constants/separation = 14 + +[node name="TitleLabel" type="Label" parent="Margin/VBox"] +layout_mode = 2 +theme_override_font_sizes/font_size = 36 +theme_override_colors/font_color = Color(0.95, 0.82, 0.3, 1) +text = "How to Play" +horizontal_alignment = 1 + +[node name="TitleRule" type="ColorRect" parent="Margin/VBox"] +layout_mode = 2 +custom_minimum_size = Vector2(0, 1) +color = Color(0.6, 0.45, 0.12, 0.7) + +[node name="Scroll" type="ScrollContainer" parent="Margin/VBox"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Content" type="VBoxContainer" parent="Margin/VBox/Scroll"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 18 + +[node name="GoalHeader" type="Label" parent="Margin/VBox/Scroll/Content"] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +theme_override_colors/font_color = Color(0.9, 0.75, 0.35, 1) +text = "Your Goal" + +[node name="GoalBody" type="Label" parent="Margin/VBox/Scroll/Content"] +layout_mode = 2 +theme_override_font_sizes/font_size = 14 +theme_override_colors/font_color = Color(0.85, 0.8, 0.7, 1) +autowrap_mode = 3 +text = "Unite the dwarf-holds. Conquer every rival clan's capital for a Domination victory, or outlast them to the final turn and win on score." + +[node name="ControlsHeader" type="Label" parent="Margin/VBox/Scroll/Content"] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +theme_override_colors/font_color = Color(0.9, 0.75, 0.35, 1) +text = "Controls" + +[node name="ControlsBody" type="Label" parent="Margin/VBox/Scroll/Content"] +layout_mode = 2 +theme_override_font_sizes/font_size = 14 +theme_override_colors/font_color = Color(0.85, 0.8, 0.7, 1) +autowrap_mode = 3 +text = "Left click: select unit or city.\nRight click (or click destination): move selected unit.\nDouble-click city: open city management screen.\nEnter / End Turn button: finish your turn.\nEsc: open in-game menu.\nMouse wheel: zoom the map.\nMiddle drag / arrow keys: pan the map." + +[node name="ClansHeader" type="Label" parent="Margin/VBox/Scroll/Content"] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +theme_override_colors/font_color = Color(0.9, 0.75, 0.35, 1) +text = "The Clans" + +[node name="ClansContainer" type="VBoxContainer" parent="Margin/VBox/Scroll/Content"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="ButtonRow" type="HBoxContainer" parent="Margin/VBox"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 16 + +[node name="BackButton" type="Button" parent="Margin/VBox/ButtonRow"] +unique_name_in_owner = true +layout_mode = 2 +custom_minimum_size = Vector2(200, 44) +text = "Back" +theme_override_font_sizes/font_size = 16 + +[node name="GuideLinkButton" type="Button" parent="Margin/VBox/ButtonRow"] +unique_name_in_owner = true +layout_mode = 2 +custom_minimum_size = Vector2(260, 44) +text = "Open Player Guide (Web)" +theme_override_font_sizes/font_size = 16 diff --git a/src/game/engine/scenes/menus/main_menu.gd b/src/game/engine/scenes/menus/main_menu.gd index 56e2e7a9..5dd40760 100644 --- a/src/game/engine/scenes/menus/main_menu.gd +++ b/src/game/engine/scenes/menus/main_menu.gd @@ -6,29 +6,39 @@ const ThroneRoomScene = preload("res://engine/scenes/menus/throne_room.tscn") @onready var _new_game_button: Button = %NewGameButton @onready var _load_game_button: Button = %LoadGameButton @onready var _options_button: Button = %OptionsButton +@onready var _how_to_play_button: Button = %HowToPlayButton @onready var _throne_room_button: Button = %ThroneRoomButton @onready var _quit_button: Button = %QuitButton @onready var _bug_report_button: Button = %BugReportButton @onready var _title_label: Label = %TitleLabel @onready var _subtitle_label: Label = %SubtitleLabel +@onready var _version_label: Label = %VersionLabel @onready var _throne_room_bg: Control = $ThroneRoomBg func _ready() -> void: - _title_label.text = ThemeVocabulary.lookup("game_title") + var title: String = ThemeVocabulary.lookup("game_title") + if not title.is_empty() and title != "game_title": + _title_label.text = title var subtitle: String = ThemeVocabulary.lookup("game_subtitle") if not subtitle.is_empty() and subtitle != "game_subtitle": _subtitle_label.text = subtitle + var version: String = str(ProjectSettings.get_setting("application/config/version", "0.0.0")) + _version_label.text = "v%s" % version _new_game_button.text = ThemeVocabulary.lookup("new_game") _load_game_button.text = ThemeVocabulary.lookup("load_game") _options_button.text = ThemeVocabulary.lookup("options") + _how_to_play_button.text = ThemeVocabulary.lookup("how_to_play") + if _how_to_play_button.text == "how_to_play": + _how_to_play_button.text = "How to Play" _throne_room_button.text = ThemeVocabulary.lookup("throne_room") _quit_button.text = ThemeVocabulary.lookup("quit") _new_game_button.pressed.connect(_on_new_game_pressed) _load_game_button.pressed.connect(_on_load_game_pressed) _options_button.pressed.connect(_on_options_pressed) + _how_to_play_button.pressed.connect(_on_how_to_play_pressed) _throne_room_button.pressed.connect(_on_throne_room_pressed) _quit_button.pressed.connect(_on_quit_pressed) _bug_report_button.pressed.connect(_on_bug_report_pressed) @@ -63,6 +73,12 @@ func _on_options_pressed() -> void: main.change_scene("res://engine/scenes/menus/options.tscn") +func _on_how_to_play_pressed() -> void: + var main: Node = get_tree().root.get_node_or_null("Main") + if main != null and main.has_method("change_scene"): + main.change_scene("res://engine/scenes/menus/how_to_play.tscn") + + func _on_throne_room_pressed() -> void: var main: Node = get_tree().root.get_node_or_null("Main") if main != null and main.has_method("push_overlay"): diff --git a/src/game/engine/scenes/menus/main_menu.tscn b/src/game/engine/scenes/menus/main_menu.tscn index 81d33bef..b76b4c05 100644 --- a/src/game/engine/scenes/menus/main_menu.tscn +++ b/src/game/engine/scenes/menus/main_menu.tscn @@ -60,7 +60,7 @@ unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 52 theme_override_colors/font_color = Color(0.95, 0.82, 0.3, 1) -text = "Magic Civilization" +text = "Age of Dwarves" horizontal_alignment = 1 [node name="SubtitleLabel" type="Label" parent="CenterContainer/VBoxContainer"] @@ -68,7 +68,7 @@ unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 18 theme_override_colors/font_color = Color(0.7, 0.62, 0.42, 1) -text = "Age of Dwarves" +text = "Magic Civilization — Early Access" horizontal_alignment = 1 [node name="TitleSpacer" type="Control" parent="CenterContainer/VBoxContainer"] @@ -105,6 +105,13 @@ custom_minimum_size = Vector2(320, 52) text = "Options" theme_override_font_sizes/font_size = 17 +[node name="HowToPlayButton" type="Button" parent="CenterContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +custom_minimum_size = Vector2(320, 52) +text = "How to Play" +theme_override_font_sizes/font_size = 17 + [node name="ThroneRoomButton" type="Button" parent="CenterContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 @@ -128,6 +135,7 @@ text = "Quit" theme_override_font_sizes/font_size = 17 [node name="VersionLabel" type="Label" parent="."] +unique_name_in_owner = true layout_mode = 1 anchors_preset = 4 anchor_left = 0.0 @@ -136,8 +144,25 @@ anchor_right = 0.0 anchor_bottom = 1.0 offset_left = 12.0 offset_top = -28.0 -offset_right = 200.0 +offset_right = 240.0 offset_bottom = -6.0 theme_override_font_sizes/font_size = 12 theme_override_colors/font_color = Color(0.45, 0.42, 0.35, 0.8) -text = "v0.1.0 — Age of Dwarves Demo" +text = "v0.0.0" + +[node name="CreditLabel" type="Label" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -220.0 +offset_top = -28.0 +offset_right = -12.0 +offset_bottom = -6.0 +theme_override_font_sizes/font_size = 12 +theme_override_colors/font_color = Color(0.45, 0.42, 0.35, 0.8) +text = "Built with Godot + Rust" +horizontal_alignment = 2 diff --git a/src/game/project.godot b/src/game/project.godot index 27848163..da2e0117 100644 --- a/src/game/project.godot +++ b/src/game/project.godot @@ -7,6 +7,7 @@ config_version=5 [application] config/name="Magic Civilization" +config/version="0.1.0" run/main_scene="res://engine/scenes/main/main.tscn" config/features=PackedStringArray("4.3", "GL Compatibility") diff --git a/src/simulator/crates/mc-city/src/city.rs b/src/simulator/crates/mc-city/src/city.rs index 12e59898..0657bfa3 100644 --- a/src/simulator/crates/mc-city/src/city.rs +++ b/src/simulator/crates/mc-city/src/city.rs @@ -111,7 +111,7 @@ pub const FOOD_PER_POP: f64 = 1.2; /// combination (HP boost + 0.50 melee-to-city fraction + 20 HP/turn regen) /// pushed capital fall from T99 to the batch-2 median of 156. Further bumps /// (280, 300) regressed results — 260 is the empirical peak. -pub const BASE_CITY_HP: u32 = 260; +pub const BASE_CITY_HP: u32 = 320; /// HP gained per population point. pub const HP_PER_POP: u32 = 10; @@ -535,7 +535,7 @@ impl City { /// accumulates across a siege. pub fn heal_per_turn(&mut self, current_turn: u32) { const HEAL_PER_TURN: u32 = 20; - const SIEGE_HEAL_SUPPRESS_TURNS: u32 = 5; + const SIEGE_HEAL_SUPPRESS_TURNS: u32 = 3; if self.hp == 0 || self.hp >= self.max_hp { return; } @@ -874,14 +874,16 @@ mod tests { let mut city = City::found("Ironhold", (5, 5), true, 1); city.take_damage(100); let hp_after_damage = city.hp; - // Attacked on turn 10 — heal on turns 10..=14 must be suppressed. + // Attacked on turn 10 — heal on turns 10..=12 must be suppressed. city.mark_attacked(10); - for t in 10..=14 { - city.heal_per_turn(t); - assert_eq!(city.hp, hp_after_damage); - } - // Turn 15 (5 turns after attack) resumes healing. - city.heal_per_turn(15); + city.heal_per_turn(10); + assert_eq!(city.hp, hp_after_damage); + city.heal_per_turn(11); + assert_eq!(city.hp, hp_after_damage); + city.heal_per_turn(12); + assert_eq!(city.hp, hp_after_damage); + // Turn 13 (3 turns after attack) resumes healing. + city.heal_per_turn(13); assert_eq!(city.hp, hp_after_damage + 20); }