feat(@projects/@magic-civilization): add wild unit variants and guide updates

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-16 15:12:15 -07:00
parent 400425585a
commit 6f63d56c61
9 changed files with 227 additions and 14 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
}