feat(@projects/@magic-civilization): ✨ add menu flow and happiness system integration
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
8f30d4d389
commit
8b44d3bdca
12 changed files with 698 additions and 3 deletions
|
|
@ -52,3 +52,9 @@
|
|||
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.
|
||||
2026-04-16 15:15 BATCH 10 (BASE_CITY_HP 320 + revert suppress 3 + HP 320): 11 PASS / 3 FAIL. Victories 2/3 in range (67%), median TTV 194 (6 below 200 target), combats 221, pop 20, tiles 76, techs 28, loot 1. Fails: TTV, worker/seed, T100-both-players. ttv-v2-dev's math model predicted TTV 160-170, empirical 194 — model under-predicted by 30 turns (field-buildup phase longer than math assumed). Next: ttv-v2-dev applying melee_fraction 0.30 + wall tier 2 for +15-25 turns. Expected batch 11 median TTV ~210-220 → should PASS TTV target.
|
||||
2026-04-16 15:26 BATCH 11 (melee_frac 0.35 + HP 260 revert): **12 PASS / 2 FAIL — BEST YET**.
|
||||
- victories 2/3 (67%, IN RANGE)
|
||||
- **median TTV 280 (IN 200-350 RANGE — FIRST TIME)**
|
||||
- pop_peak 26, combats 401, techs 38, tiles 74
|
||||
- worker improvements min 8 (was 0 — task #29 + #46 fixes cascaded)
|
||||
Remaining 2 FAILS: loot_dropped 0 (wilds not engaging this batch — variance), both-players-T100 1/3 (persistent structural). Stop criterion needs FULL 14/14 for 2 consecutive batches — we're 2 short.
|
||||
|
|
|
|||
120
src/game/engine/scenes/encyclopedia/encyclopedia_panel.gd
Normal file
120
src/game/engine/scenes/encyclopedia/encyclopedia_panel.gd
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
extends Control
|
||||
## Encyclopedia overlay: searchable reference for units, buildings, techs, spells.
|
||||
## Read-only display — tabbed entry list + detail pane.
|
||||
|
||||
const CATEGORIES: Array[Dictionary] = [
|
||||
{"key": "units", "label": "Units"}, {"key": "buildings", "label": "Buildings"},
|
||||
{"key": "techs", "label": "Techs"}, {"key": "spells", "label": "Spells"},
|
||||
]
|
||||
var _current_category: int = 0
|
||||
var _search_text: String = ""
|
||||
|
||||
@onready var _tab_bar: TabBar = %TabBar
|
||||
@onready var _search_field: LineEdit = %SearchField
|
||||
@onready var _entry_list: ItemList = %EntryList
|
||||
@onready var _detail_name: Label = %DetailName
|
||||
@onready var _detail_meta: Label = %DetailMeta
|
||||
@onready var _detail_body: RichTextLabel = %DetailBody
|
||||
@onready var _close_button: Button = %CloseButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for cat: Dictionary in CATEGORIES:
|
||||
_tab_bar.add_tab(cat.label)
|
||||
_tab_bar.tab_changed.connect(_on_tab_changed)
|
||||
_search_field.text_changed.connect(_on_search_changed)
|
||||
_entry_list.item_selected.connect(_on_entry_selected)
|
||||
_close_button.pressed.connect(_on_close)
|
||||
_clear_detail()
|
||||
_refresh_list()
|
||||
|
||||
|
||||
func _on_close() -> void:
|
||||
var main: Node = get_tree().root.get_node_or_null("Main")
|
||||
if main != null and main.has_method("pop_overlay"):
|
||||
main.pop_overlay()
|
||||
else:
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_tab_changed(tab: int) -> void:
|
||||
_current_category = tab
|
||||
_clear_detail()
|
||||
_refresh_list()
|
||||
|
||||
|
||||
func _on_search_changed(text: String) -> void:
|
||||
_search_text = text.to_lower().strip_edges()
|
||||
_refresh_list()
|
||||
|
||||
|
||||
func _on_entry_selected(index: int) -> void:
|
||||
var entry: Dictionary = _entry_list.get_item_metadata(index)
|
||||
if not entry.is_empty():
|
||||
_show_detail(entry)
|
||||
|
||||
|
||||
func _refresh_list() -> void:
|
||||
_entry_list.clear()
|
||||
var entries: Array = _load_entries(CATEGORIES[_current_category].key)
|
||||
entries.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return String(a.get("name", a.get("id", ""))) < String(b.get("name", b.get("id", "")))
|
||||
)
|
||||
for entry: Dictionary in entries:
|
||||
var name_str: String = entry.get("name", entry.get("id", ""))
|
||||
if _search_text != "" and not name_str.to_lower().contains(_search_text):
|
||||
continue
|
||||
_entry_list.set_item_metadata(_entry_list.add_item(name_str), entry)
|
||||
|
||||
|
||||
func _load_entries(category: String) -> Array:
|
||||
match category:
|
||||
"units": return DataLoader.get_all_units()
|
||||
"buildings": return DataLoader.get_all_buildings()
|
||||
"techs": return DataLoader.get_all_techs()
|
||||
"spells": return DataLoader.get_all_spells()
|
||||
return []
|
||||
|
||||
|
||||
func _show_detail(entry: Dictionary) -> void:
|
||||
_detail_name.text = entry.get("name", entry.get("id", ""))
|
||||
var bits: Array[String] = []
|
||||
if entry.has("tier"): bits.append("Tier %d" % int(entry.tier))
|
||||
var school: String = String(entry.get("school", ""))
|
||||
if school != "" and school != "<null>": bits.append(school.capitalize())
|
||||
var pillar: String = String(entry.get("pillar", ""))
|
||||
if pillar != "": bits.append(pillar.capitalize())
|
||||
if entry.get("cost", null) != null: bits.append("Cost: %s" % str(entry.cost))
|
||||
if entry.get("mana_cost", null) != null: bits.append("Mana: %s" % str(entry.mana_cost))
|
||||
_detail_meta.text = " | ".join(bits)
|
||||
|
||||
var parts: Array[String] = []
|
||||
var desc: String = String(entry.get("description", ""))
|
||||
if desc != "": parts.append(desc)
|
||||
var req: String = String(entry.get("tech_required", ""))
|
||||
if req != "" and req != "<null>": parts.append("[b]Requires:[/b] %s" % req)
|
||||
var reqs: Array = entry.get("requires", [])
|
||||
if reqs is Array and not reqs.is_empty():
|
||||
parts.append("[b]Requires:[/b] %s" % ", ".join(reqs))
|
||||
var unlocks: Dictionary = entry.get("unlocks", {})
|
||||
if unlocks is Dictionary and not unlocks.is_empty():
|
||||
var unlocked: Array[String] = []
|
||||
for key: String in unlocks:
|
||||
var arr: Array = unlocks[key]
|
||||
if arr is Array and not arr.is_empty():
|
||||
unlocked.append("%s: %s" % [key, ", ".join(arr)])
|
||||
if not unlocked.is_empty():
|
||||
parts.append("[b]Unlocks:[/b] %s" % "; ".join(unlocked))
|
||||
_detail_body.text = "\n\n".join(parts)
|
||||
|
||||
|
||||
func _clear_detail() -> void:
|
||||
_detail_name.text = "Select an entry"
|
||||
_detail_meta.text = ""
|
||||
_detail_body.text = ""
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
get_viewport().set_input_as_handled()
|
||||
_on_close()
|
||||
105
src/game/engine/scenes/encyclopedia/encyclopedia_panel.tscn
Normal file
105
src/game/engine/scenes/encyclopedia/encyclopedia_panel.tscn
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://cenpdy0encc01"]
|
||||
|
||||
[ext_resource type="Script" path="res://engine/scenes/encyclopedia/encyclopedia_panel.gd" id="1_encyc"]
|
||||
|
||||
[node name="EncyclopediaPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 1
|
||||
script = ExtResource("1_encyc")
|
||||
|
||||
[node name="Backdrop" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
color = Color(0, 0, 0, 0.55)
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -440.0
|
||||
offset_top = -300.0
|
||||
offset_right = 440.0
|
||||
offset_bottom = 300.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Margin" type="MarginContainer" parent="PanelContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 12
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 12
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="PanelContainer/Margin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="TopBar" type="HBoxContainer" parent="PanelContainer/Margin/VBox"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Title" type="Label" parent="PanelContainer/Margin/VBox/TopBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Encyclopedia"
|
||||
theme_override_font_sizes/font_size = 20
|
||||
|
||||
[node name="CloseButton" type="Button" parent="PanelContainer/Margin/VBox/TopBar"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Close"
|
||||
|
||||
[node name="TabBar" type="TabBar" parent="PanelContainer/Margin/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SearchField" type="LineEdit" parent="PanelContainer/Margin/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
placeholder_text = "Search..."
|
||||
clear_button_enabled = true
|
||||
|
||||
[node name="Split" type="HSplitContainer" parent="PanelContainer/Margin/VBox"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
split_offset = 300
|
||||
|
||||
[node name="EntryList" type="ItemList" parent="PanelContainer/Margin/VBox/Split"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="DetailPane" type="VBoxContainer" parent="PanelContainer/Margin/VBox/Split"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="DetailName" type="Label" parent="PanelContainer/Margin/VBox/Split/DetailPane"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Select an entry"
|
||||
theme_override_font_sizes/font_size = 18
|
||||
|
||||
[node name="DetailMeta" type="Label" parent="PanelContainer/Margin/VBox/Split/DetailPane"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 12
|
||||
theme_override_colors/font_color = Color(0.75, 0.72, 0.65, 1)
|
||||
|
||||
[node name="DetailBody" type="RichTextLabel" parent="PanelContainer/Margin/VBox/Split/DetailPane"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
bbcode_enabled = true
|
||||
fit_content = true
|
||||
scroll_active = true
|
||||
|
|
@ -190,6 +190,29 @@ func _draw_fog(game_map: RefCounted, player_index: int) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _draw_owner_tiles(game_map: RefCounted) -> void:
|
||||
## Tint each owned tile with the controlling player's color (subtle overlay).
|
||||
var player_colors: Dictionary = {}
|
||||
for p: Variant in GameState.players:
|
||||
if p is PlayerScript:
|
||||
player_colors[(p as PlayerScript).index] = (p as PlayerScript).color
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var raw_tile: RefCounted = game_map.get_tile(axial) as RefCounted
|
||||
if raw_tile == null:
|
||||
continue
|
||||
var owner_idx: int = int(raw_tile.get("owner"))
|
||||
if owner_idx < 0 or not player_colors.has(owner_idx):
|
||||
continue
|
||||
var tint: Color = player_colors[owner_idx]
|
||||
tint.a = OWNER_TINT_ALPHA
|
||||
var pixel_pos: Vector2 = _world_to_mini(
|
||||
HexUtilsScript.axial_to_pixel(axial) + Vector2(
|
||||
HexUtilsScript.HEX_WIDTH * 0.5, HexUtilsScript.HEX_HEIGHT * 0.5
|
||||
)
|
||||
)
|
||||
_overlay_rect.draw_rect(Rect2(pixel_pos - OWNER_TILE_SIZE * 0.5, OWNER_TILE_SIZE), tint)
|
||||
|
||||
|
||||
func _get_tile_visibility(game_map: RefCounted, axial: Vector2i, player_index: int) -> int:
|
||||
var raw_tile: RefCounted = game_map.get_tile(axial) as RefCounted
|
||||
if raw_tile == null:
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ func _ready() -> void:
|
|||
|
||||
%BugReportButton.pressed.connect(_on_bug_report_pressed)
|
||||
%StatsButton.pressed.connect(_on_stats_pressed)
|
||||
%EncyclopediaButton.pressed.connect(_on_encyclopedia_pressed)
|
||||
|
||||
_refresh_all()
|
||||
|
||||
|
|
@ -74,7 +75,12 @@ func _refresh_all() -> void:
|
|||
|
||||
|
||||
func _update_turn() -> void:
|
||||
%TurnLabel.text = "%s %d" % [ThemeVocabulary.lookup("turn"), GameState.turn_number]
|
||||
var turn_word: String = ThemeVocabulary.lookup("turn")
|
||||
var limit: int = int(GameState.game_settings.get("turn_limit", 0))
|
||||
if limit > 0:
|
||||
%TurnLabel.text = "%s %d / %d" % [turn_word, GameState.turn_number, limit]
|
||||
else:
|
||||
%TurnLabel.text = "%s %d" % [turn_word, GameState.turn_number]
|
||||
|
||||
|
||||
func _update_era() -> void:
|
||||
|
|
@ -158,6 +164,9 @@ 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()
|
||||
|
||||
|
||||
func _format_signed(value: int) -> String:
|
||||
|
|
@ -190,3 +199,9 @@ func _on_bug_report_pressed() -> void:
|
|||
var main: Node = get_tree().root.get_node_or_null("Main")
|
||||
if main != null and main.has_method("push_overlay"):
|
||||
main.push_overlay("res://engine/scenes/ui/bug_report.tscn")
|
||||
|
||||
|
||||
func _on_encyclopedia_pressed() -> void:
|
||||
var main: Node = get_tree().root.get_node_or_null("Main")
|
||||
if main != null and main.has_method("push_overlay"):
|
||||
main.push_overlay("res://engine/scenes/encyclopedia/encyclopedia_panel.tscn")
|
||||
|
|
|
|||
|
|
@ -135,6 +135,15 @@ text = "S"
|
|||
theme_override_font_sizes/font_size = 14
|
||||
theme_override_colors/font_color = Color(0.5, 0.8, 0.9, 1)
|
||||
|
||||
[node name="EncyclopediaButton" type="Button" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(32, 32)
|
||||
tooltip_text = "Encyclopedia (F1)"
|
||||
text = "?"
|
||||
theme_override_font_sizes/font_size = 14
|
||||
theme_override_colors/font_color = Color(0.85, 0.75, 0.45, 1)
|
||||
|
||||
[node name="BugReportButton" type="Button" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
|
|
|||
87
src/game/engine/scenes/menus/settings.gd
Normal file
87
src/game/engine/scenes/menus/settings.gd
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
extends Control
|
||||
## Focused settings screen: volume sliders, resolution, language stub.
|
||||
## Persists through SettingsManager; resolution applied via DisplayServer.
|
||||
|
||||
const RESOLUTIONS: Array[Vector2i] = [
|
||||
Vector2i(1280, 720),
|
||||
Vector2i(1600, 900),
|
||||
Vector2i(1920, 1080),
|
||||
Vector2i(2560, 1440),
|
||||
Vector2i(3840, 2160),
|
||||
]
|
||||
|
||||
@onready var _master: HSlider = %MasterSlider
|
||||
@onready var _master_val: Label = %MasterValue
|
||||
@onready var _music: HSlider = %MusicSlider
|
||||
@onready var _music_val: Label = %MusicValue
|
||||
@onready var _sfx: HSlider = %SfxSlider
|
||||
@onready var _sfx_val: Label = %SfxValue
|
||||
@onready var _resolution: OptionButton = %ResolutionOption
|
||||
@onready var _language: OptionButton = %LanguageOption
|
||||
@onready var _back: Button = %BackButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for res in RESOLUTIONS:
|
||||
_resolution.add_item("%d x %d" % [res.x, res.y])
|
||||
_language.add_item("English")
|
||||
_language.disabled = true
|
||||
_refresh()
|
||||
_master.value_changed.connect(_on_master)
|
||||
_music.value_changed.connect(_on_music)
|
||||
_sfx.value_changed.connect(_on_sfx)
|
||||
_resolution.item_selected.connect(_on_resolution)
|
||||
_back.pressed.connect(_on_back)
|
||||
_back.grab_focus()
|
||||
|
||||
|
||||
func _refresh() -> void:
|
||||
var m: int = int(SettingsManager.get_setting("audio", "master_volume"))
|
||||
var mu: int = int(SettingsManager.get_setting("audio", "music_volume"))
|
||||
var s: int = int(SettingsManager.get_setting("audio", "sfx_volume"))
|
||||
_master.value = m
|
||||
_master_val.text = "%d%%" % m
|
||||
_music.value = mu
|
||||
_music_val.text = "%d%%" % mu
|
||||
_sfx.value = s
|
||||
_sfx_val.text = "%d%%" % s
|
||||
var current: Vector2i = DisplayServer.window_get_size()
|
||||
var idx: int = RESOLUTIONS.find(current)
|
||||
_resolution.select(idx if idx >= 0 else 2)
|
||||
_language.select(0)
|
||||
|
||||
|
||||
func _on_master(v: float) -> void:
|
||||
var iv: int = int(v)
|
||||
SettingsManager.set_setting("audio", "master_volume", iv)
|
||||
_master_val.text = "%d%%" % iv
|
||||
|
||||
|
||||
func _on_music(v: float) -> void:
|
||||
var iv: int = int(v)
|
||||
SettingsManager.set_setting("audio", "music_volume", iv)
|
||||
_music_val.text = "%d%%" % iv
|
||||
|
||||
|
||||
func _on_sfx(v: float) -> void:
|
||||
var iv: int = int(v)
|
||||
SettingsManager.set_setting("audio", "sfx_volume", iv)
|
||||
_sfx_val.text = "%d%%" % iv
|
||||
|
||||
|
||||
func _on_resolution(idx: int) -> void:
|
||||
if idx < 0 or idx >= RESOLUTIONS.size():
|
||||
return
|
||||
DisplayServer.window_set_size(RESOLUTIONS[idx])
|
||||
|
||||
|
||||
func _on_back() -> 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/main_menu.tscn")
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
get_viewport().set_input_as_handled()
|
||||
_on_back()
|
||||
183
src/game/engine/scenes/menus/settings.tscn
Normal file
183
src/game/engine/scenes/menus/settings.tscn
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://engine/scenes/menus/settings.gd" id="1"]
|
||||
|
||||
[node name="Settings" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
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 = 240
|
||||
theme_override_constants/margin_top = 60
|
||||
theme_override_constants/margin_right = 240
|
||||
theme_override_constants/margin_bottom = 60
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="Margin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 18
|
||||
|
||||
[node name="Title" type="Label" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 34
|
||||
theme_override_colors/font_color = Color(0.95, 0.82, 0.3, 1)
|
||||
text = "Settings"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Rule" type="ColorRect" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(0, 1)
|
||||
color = Color(0.6, 0.45, 0.12, 0.6)
|
||||
|
||||
[node name="MasterRow" type="HBoxContainer" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="MasterLabel" type="Label" parent="Margin/VBox/MasterRow"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(220, 0)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "Master Volume"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="MasterSlider" type="HSlider" parent="Margin/VBox/MasterRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
min_value = 0.0
|
||||
max_value = 100.0
|
||||
step = 1.0
|
||||
value = 80.0
|
||||
|
||||
[node name="MasterValue" type="Label" parent="Margin/VBox/MasterRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(60, 0)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "80%"
|
||||
horizontal_alignment = 2
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="MusicRow" type="HBoxContainer" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="MusicLabel" type="Label" parent="Margin/VBox/MusicRow"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(220, 0)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "Music Volume"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="MusicSlider" type="HSlider" parent="Margin/VBox/MusicRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
min_value = 0.0
|
||||
max_value = 100.0
|
||||
step = 1.0
|
||||
value = 70.0
|
||||
|
||||
[node name="MusicValue" type="Label" parent="Margin/VBox/MusicRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(60, 0)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "70%"
|
||||
horizontal_alignment = 2
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="SfxRow" type="HBoxContainer" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="SfxLabel" type="Label" parent="Margin/VBox/SfxRow"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(220, 0)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "SFX Volume"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="SfxSlider" type="HSlider" parent="Margin/VBox/SfxRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
min_value = 0.0
|
||||
max_value = 100.0
|
||||
step = 1.0
|
||||
value = 70.0
|
||||
|
||||
[node name="SfxValue" type="Label" parent="Margin/VBox/SfxRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(60, 0)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "70%"
|
||||
horizontal_alignment = 2
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="ResolutionRow" type="HBoxContainer" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="ResolutionLabel" type="Label" parent="Margin/VBox/ResolutionRow"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(220, 0)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "Resolution"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="ResolutionOption" type="OptionButton" parent="Margin/VBox/ResolutionRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="LanguageRow" type="HBoxContainer" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="LanguageLabel" type="Label" parent="Margin/VBox/LanguageRow"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(220, 0)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "Language"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="LanguageOption" type="OptionButton" parent="Margin/VBox/LanguageRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Spacer" type="Control" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="BackButton" type="Button" parent="Margin/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(200, 48)
|
||||
size_flags_horizontal = 4
|
||||
theme_override_colors/font_color = Color(1.0, 0.92, 0.4, 1)
|
||||
theme_override_font_sizes/font_size = 17
|
||||
text = "Back to Menu"
|
||||
56
src/game/engine/scenes/tests/menu_flow_proof.gd
Normal file
56
src/game/engine/scenes/tests/menu_flow_proof.gd
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
extends Node
|
||||
## HOW TO PLAY PROOF: navigates main_menu → how_to_play, captures screenshot
|
||||
## showing clan personality panel + Back/Guide buttons.
|
||||
|
||||
const MainMenuScene: PackedScene = preload("res://engine/scenes/menus/main_menu.tscn")
|
||||
const HowToPlayScene: PackedScene = preload("res://engine/scenes/menus/how_to_play.tscn")
|
||||
|
||||
var _captured: bool = false
|
||||
var _screenshot_name: String = "menu_flow_proof"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
RenderingServer.set_default_clear_color(Color(0.06, 0.05, 0.04))
|
||||
get_viewport().size = Vector2i(1920, 1080)
|
||||
DisplayServer.window_set_size(Vector2i(1920, 1080))
|
||||
|
||||
var env_name: String = OS.get_environment("SCREENSHOT_NAME")
|
||||
if not env_name.is_empty():
|
||||
_screenshot_name = env_name
|
||||
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
await get_tree().process_frame
|
||||
|
||||
var menu: Control = MainMenuScene.instantiate()
|
||||
add_child(menu)
|
||||
for _i: int in range(4):
|
||||
await get_tree().process_frame
|
||||
print("MainMenu buttons visible: %s" % str(menu.get_node("%HowToPlayButton").text))
|
||||
|
||||
menu.queue_free()
|
||||
await get_tree().process_frame
|
||||
|
||||
var htp: Control = HowToPlayScene.instantiate()
|
||||
add_child(htp)
|
||||
for _i: int in range(12):
|
||||
await get_tree().process_frame
|
||||
print("HowToPlay clans populated: %d" % htp.get_node("%ClansContainer").get_child_count())
|
||||
|
||||
_capture_and_quit()
|
||||
|
||||
|
||||
func _capture_and_quit() -> void:
|
||||
if _captured:
|
||||
return
|
||||
_captured = true
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path("user://screenshots"))
|
||||
var image: Image = get_viewport().get_texture().get_image()
|
||||
if image == null:
|
||||
get_tree().quit(1)
|
||||
return
|
||||
var timestamp: String = Time.get_datetime_string_from_system().replace(":", "-").replace("T", "_")
|
||||
var rel_path: String = "user://screenshots/%s_%s.png" % [_screenshot_name, timestamp]
|
||||
var abs_path: String = ProjectSettings.globalize_path(rel_path)
|
||||
if image.save_png(abs_path) == OK:
|
||||
print("SCREENSHOT_PATH:%s" % abs_path)
|
||||
get_tree().quit()
|
||||
6
src/game/engine/scenes/tests/menu_flow_proof.tscn
Normal file
6
src/game/engine/scenes/tests/menu_flow_proof.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://bmenuflow0proof"]
|
||||
|
||||
[ext_resource type="Script" path="res://engine/scenes/tests/menu_flow_proof.gd" id="1_script"]
|
||||
|
||||
[node name="MenuFlowProof" type="Node"]
|
||||
script = ExtResource("1_script")
|
||||
|
|
@ -51,6 +51,12 @@ static func process_turn(player: RefCounted, _game_map: RefCounted) -> void:
|
|||
var building_happiness: int = TurnProcessorHelpersScript.sum_building_effects(
|
||||
player, "happiness"
|
||||
)
|
||||
# happiness_per_city (wonder effect) — +N happiness per owned city, applied
|
||||
# once per building that carries the effect (royal_runestone +1/city etc.).
|
||||
var per_city_bonus: int = TurnProcessorHelpersScript.sum_building_effects(
|
||||
player, "happiness_per_city"
|
||||
)
|
||||
building_happiness += per_city_bonus * player.cities.size()
|
||||
var ascension_active: bool = bool(player.ascension_active)
|
||||
var growth_tier: String = player.growth_tier if player.growth_tier != "" else "balanced"
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,17 @@ func _process_production(player: RefCounted) -> void: # Player
|
|||
var yields: Dictionary = c.get_yields(tile_json)
|
||||
# Add building production bonuses (forge +2, barracks +1, etc.)
|
||||
var building_prod: int = _sum_city_building_effect(c, "production")
|
||||
var prod: int = int((yields.get("production", 1) + building_prod) * prod_modifier)
|
||||
# production_from_hills — +N prod per worked hills tile (first_mineshaft etc.).
|
||||
var prod_hills: int = _sum_city_building_effect(c, "production_from_hills")
|
||||
if prod_hills > 0:
|
||||
for tile_pos: Vector2i in c.get_worked_tiles():
|
||||
var tile: Resource = game_map.get_tile(tile_pos)
|
||||
if tile != null and tile.biome_id == "hills":
|
||||
building_prod += prod_hills
|
||||
var prod_pct: float = _sum_city_building_effect_float(c, "production_percent")
|
||||
var prod: int = int(
|
||||
(yields.get("production", 1) + building_prod) * (1.0 + prod_pct) * prod_modifier
|
||||
)
|
||||
# Capture current item before apply_production pops it on completion.
|
||||
var current: Dictionary = (
|
||||
c.production_queue.front() as Dictionary
|
||||
|
|
@ -145,7 +155,12 @@ func _process_research(player: RefCounted) -> void: # Player
|
|||
)
|
||||
var yields: Dictionary = city.get_yields(tile_json)
|
||||
var building_sci: int = _sum_city_building_effect(city as CityScript, "science")
|
||||
player.research_progress += int((yields.get("science", 0) + building_sci) * sci_modifier)
|
||||
var sci_pct: float = _sum_city_building_effect_float(
|
||||
city as CityScript, "science_percent"
|
||||
)
|
||||
player.research_progress += int(
|
||||
(yields.get("science", 0) + building_sci) * (1.0 + sci_pct) * sci_modifier
|
||||
)
|
||||
|
||||
# Check if researching a spell (not a tech)
|
||||
var spell_data: Dictionary = DataLoader.get_spell(player.researching)
|
||||
|
|
@ -262,12 +277,52 @@ func _sum_city_building_effect_float(city: CityScript, effect_type: String) -> f
|
|||
func _apply_building_bonuses(city: CityScript, building_id: String) -> void:
|
||||
var bdata: Dictionary = DataLoader.get_building(building_id)
|
||||
var effects: Array = bdata.get("effects", [])
|
||||
var owner_player: RefCounted = GameState.get_player(city.owner) if city.owner >= 0 else null
|
||||
for effect: Dictionary in effects:
|
||||
var etype: String = effect.get("type", "")
|
||||
var value: int = int(effect.get("value", 0))
|
||||
if etype == "hp_bonus" and value > 0:
|
||||
city.set_max_hp(city.max_hp + value)
|
||||
city.heal(value)
|
||||
elif etype == "city_hp" and value > 0 and owner_player != null:
|
||||
# Empire-wide max HP bump from mundane wonder (iron_bulwark +100).
|
||||
for other_ref: Variant in owner_player.cities:
|
||||
if other_ref is CityScript:
|
||||
var other: CityScript = other_ref as CityScript
|
||||
other.set_max_hp(other.max_hp + value)
|
||||
other.heal(value)
|
||||
elif etype == "free_tech" and value > 0 and owner_player != null:
|
||||
_grant_free_tech(owner_player, value)
|
||||
elif etype == "free_golden_age_on_build" and value > 0 and owner_player != null:
|
||||
owner_player.golden_age_active = true
|
||||
owner_player.golden_age_turns = HappinessScript.GOLDEN_AGE_DURATION
|
||||
EventBus.golden_age_started.emit(owner_player.index)
|
||||
|
||||
|
||||
func _grant_free_tech(player: RefCounted, count: int) -> void:
|
||||
## Pick cheapest unresearched techs the player can currently access and
|
||||
## grant them instantly. Wonder flavor: "instant shortcut" rather than a
|
||||
## free high-tier tech so pacing stays intact.
|
||||
var researched: Array = player.researched_techs if player.researched_techs != null else []
|
||||
var candidates: Array[Dictionary] = []
|
||||
for t: Dictionary in DataLoader.get_all_techs():
|
||||
var tid: String = str(t.get("id", ""))
|
||||
if tid == "" or tid in researched:
|
||||
continue
|
||||
var prereqs: Array = t.get("prerequisites", []) as Array
|
||||
var all_met: bool = true
|
||||
for pr: Variant in prereqs:
|
||||
if not (str(pr) in researched):
|
||||
all_met = false
|
||||
break
|
||||
if all_met:
|
||||
candidates.append(t)
|
||||
candidates.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return int(a.get("cost", 999999)) < int(b.get("cost", 999999))
|
||||
)
|
||||
for i: int in range(mini(count, candidates.size())):
|
||||
player.add_tech(str(candidates[i].get("id", "")))
|
||||
EventBus.tech_researched.emit(str(candidates[i].get("id", "")), player.index)
|
||||
|
||||
|
||||
func _process_city_healing(player: RefCounted) -> void:
|
||||
|
|
@ -369,6 +424,17 @@ func _process_economy(player: RefCounted, game_map: RefCounted) -> void: # Play
|
|||
var yields: Dictionary = c.get_yields(tile_json)
|
||||
var building_gold: int = _sum_city_building_effect(c, "gold")
|
||||
var city_gold: int = int(yields.get("gold", 0)) + building_gold
|
||||
# gold_per_city_pop (wonder effect) — flat gold per pop point.
|
||||
var gold_per_pop: int = _sum_city_building_effect(c, "gold_per_city_pop")
|
||||
if gold_per_pop > 0:
|
||||
city_gold += gold_per_pop * int(c.population)
|
||||
# gold_from_mines — +N gold per mine improvement in owned tiles.
|
||||
var gold_mine: int = _sum_city_building_effect(c, "gold_from_mines")
|
||||
if gold_mine > 0:
|
||||
for tile_pos: Vector2i in c.owned_tiles:
|
||||
var tile: Resource = game_map.get_tile(tile_pos)
|
||||
if tile != null and tile.improvement == "mine":
|
||||
city_gold += gold_mine
|
||||
# Apply percentage bonuses (marketplace +25% = 0.25)
|
||||
var gold_pct: float = _sum_city_building_effect_float(c, "gold_percent")
|
||||
if gold_pct > 0.0:
|
||||
|
|
@ -416,7 +482,20 @@ func _process_culture(player: RefCounted, game_map: RefCounted) -> void:
|
|||
continue
|
||||
var c: CityScript = city_ref as CityScript
|
||||
var tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map)
|
||||
var pre_culture: float = c.get_culture_stored()
|
||||
var can_expand: bool = c.process_culture(tile_json)
|
||||
# Apply culture_percent and border_growth_percent bonuses to the
|
||||
# culture gained this turn. process_culture already added raw culture;
|
||||
# we top up the stockpile by (raw_gain * total_pct) so wonders scale.
|
||||
var cult_pct: float = _sum_city_building_effect_float(c, "culture_percent")
|
||||
var border_pct: float = _sum_city_building_effect_float(c, "border_growth_percent")
|
||||
var total_pct: float = cult_pct + border_pct
|
||||
if total_pct > 0.0:
|
||||
var post_culture: float = c.get_culture_stored()
|
||||
var gained: float = post_culture - pre_culture
|
||||
if gained > 0.0:
|
||||
c.set_culture_stored(post_culture + gained * total_pct)
|
||||
can_expand = c.get_can_expand()
|
||||
if not can_expand:
|
||||
continue
|
||||
# Build candidates JSON for Rust border expansion
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue