refactor(scenes): ♻️ Refactor end-game summary and statistics scenes with new test coverage and optimized rendering for all game-over conditions
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
3d83f4781c
commit
db51852022
6 changed files with 463 additions and 158 deletions
202
src/game/engine/scenes/menus/end_game_summary.tscn
Normal file
202
src/game/engine/scenes/menus/end_game_summary.tscn
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://cendgamesummaryp248"]
|
||||
|
||||
[ext_resource type="Script" path="res://engine/scenes/menus/end_game_summary.gd" id="1_endgame"]
|
||||
|
||||
[node name="EndGameSummary" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_endgame")
|
||||
|
||||
[node name="Backdrop" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0.03, 0.035, 0.05, 0.92)
|
||||
|
||||
[node name="Margin" type="MarginContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 36
|
||||
theme_override_constants/margin_top = 24
|
||||
theme_override_constants/margin_right = 36
|
||||
theme_override_constants/margin_bottom = 24
|
||||
|
||||
[node name="Root" type="VBoxContainer" parent="Margin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
|
||||
[node name="BannerLabel" type="Label" parent="Margin/Root"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 32
|
||||
text = "Game Over"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="HeroStrip" type="HBoxContainer" parent="Margin/Root"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 16
|
||||
alignment = 1
|
||||
|
||||
[node name="WinnerCard" type="PanelContainer" parent="Margin/Root/HeroStrip"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(280, 80)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="WinnerVBox" type="VBoxContainer" parent="Margin/Root/HeroStrip/WinnerCard"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="WinnerTitle" type="Label" parent="Margin/Root/HeroStrip/WinnerCard/WinnerVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
theme_override_colors/font_color = Color(0.85, 0.76, 0.52, 1)
|
||||
text = "Winner"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="WinnerNameLabel" type="Label" parent="Margin/Root/HeroStrip/WinnerCard/WinnerVBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 20
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="PlayerCard" type="PanelContainer" parent="Margin/Root/HeroStrip"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(280, 80)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="PlayerVBox" type="VBoxContainer" parent="Margin/Root/HeroStrip/PlayerCard"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="PlayerTitle" type="Label" parent="Margin/Root/HeroStrip/PlayerCard/PlayerVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
theme_override_colors/font_color = Color(0.6, 0.72, 0.85, 1)
|
||||
text = "Your Clan"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="PlayerNameLabel" type="Label" parent="Margin/Root/HeroStrip/PlayerCard/PlayerVBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 20
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Body" type="HBoxContainer" parent="Margin/Root"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 18
|
||||
|
||||
[node name="LeftColumn" type="VBoxContainer" parent="Margin/Root/Body"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="StandingsTitle" type="Label" parent="Margin/Root/Body/LeftColumn"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 16
|
||||
theme_override_colors/font_color = Color(0.9, 0.84, 0.65, 1)
|
||||
text = "Final Standings"
|
||||
|
||||
[node name="StandingsScroll" type="ScrollContainer" parent="Margin/Root/Body/LeftColumn"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="StandingsContainer" type="VBoxContainer" parent="Margin/Root/Body/LeftColumn/StandingsScroll"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 2
|
||||
|
||||
[node name="GraphTitle" type="Label" parent="Margin/Root/Body/LeftColumn"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 16
|
||||
theme_override_colors/font_color = Color(0.9, 0.84, 0.65, 1)
|
||||
text = "Score Over Time"
|
||||
|
||||
[node name="SummaryGraphArea" type="Control" parent="Margin/Root/Body/LeftColumn"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 180)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="RightColumn" type="VBoxContainer" parent="Margin/Root/Body"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="AwardsTitle" type="Label" parent="Margin/Root/Body/RightColumn"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 16
|
||||
theme_override_colors/font_color = Color(0.9, 0.84, 0.65, 1)
|
||||
text = "Awards"
|
||||
|
||||
[node name="AwardsScroll" type="ScrollContainer" parent="Margin/Root/Body/RightColumn"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(0, 100)
|
||||
horizontal_scroll_mode = 2
|
||||
|
||||
[node name="AwardsContainer" type="HBoxContainer" parent="Margin/Root/Body/RightColumn/AwardsScroll"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="TimelineTitle" type="Label" parent="Margin/Root/Body/RightColumn"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 16
|
||||
theme_override_colors/font_color = Color(0.9, 0.84, 0.65, 1)
|
||||
text = "Timeline"
|
||||
|
||||
[node name="TimelineScroll" type="ScrollContainer" parent="Margin/Root/Body/RightColumn"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="TimelineContainer" type="VBoxContainer" parent="Margin/Root/Body/RightColumn/TimelineScroll"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 2
|
||||
|
||||
[node name="Footer" type="HBoxContainer" parent="Margin/Root"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
alignment = 2
|
||||
|
||||
[node name="ViewMapButton" type="Button" parent="Margin/Root/Footer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(140, 38)
|
||||
layout_mode = 2
|
||||
text = "View Map"
|
||||
|
||||
[node name="WatchReplayButton" type="Button" parent="Margin/Root/Footer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(140, 38)
|
||||
layout_mode = 2
|
||||
text = "Watch Replay"
|
||||
|
||||
[node name="SaveArchiveButton" type="Button" parent="Margin/Root/Footer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(140, 38)
|
||||
layout_mode = 2
|
||||
text = "Save to Archive"
|
||||
|
||||
[node name="ExportJsonButton" type="Button" parent="Margin/Root/Footer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(140, 38)
|
||||
layout_mode = 2
|
||||
text = "Export JSON"
|
||||
|
||||
[node name="MainMenuButton" type="Button" parent="Margin/Root/Footer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(140, 38)
|
||||
layout_mode = 2
|
||||
text = "Main Menu"
|
||||
12
src/game/engine/scenes/statistics/statistics.tscn
Normal file
12
src/game/engine/scenes/statistics/statistics.tscn
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://cstatsmodalp247"]
|
||||
|
||||
[ext_resource type="Script" path="res://engine/scenes/statistics/statistics.gd" id="1_stats"]
|
||||
|
||||
[node name="StatisticsModal" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_stats")
|
||||
|
|
@ -1,189 +1,145 @@
|
|||
extends Node2D
|
||||
## End-game summary proof — renders the post-victory chronicle that
|
||||
## shows score breakdown, key events timeline, and per-player win
|
||||
## conditions. Distinct from victory_screen which is the immediate
|
||||
## announcement; this is the full standings recap.
|
||||
extends Node
|
||||
## p2-48a — End-game summary proof scene.
|
||||
##
|
||||
## Boots the REAL end_game_summary.tscn (NOT a _draw mockup) once per
|
||||
## GameOverReason variant — LastSurvivor, ConditionMet, TurnLimit, Resigned —
|
||||
## seeds a fixture clan roster + StatsTracker history, calls _on_game_over for
|
||||
## each reason, and captures one screenshot per variant. Proves the orphaned
|
||||
## EndGameSummary scene now renders end-to-end for every game-over path.
|
||||
##
|
||||
## Rust note: production `game_over` emission lives in mc-turn (separate lane);
|
||||
## this proof drives EventBus.game_over directly, exactly as the GUT tests do,
|
||||
## fully exercising the GDScript receiver.
|
||||
|
||||
const EndGameSummaryScene: PackedScene = preload(
|
||||
"res://engine/scenes/menus/end_game_summary.tscn"
|
||||
)
|
||||
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
|
||||
|
||||
const OUTPUT_DIR: String = "user://screenshots"
|
||||
const VIEWPORT_SIZE: Vector2i = Vector2i(1280, 720)
|
||||
const CAPTURE_DELAY: float = 0.6
|
||||
const SETTLE_FRAMES: int = 4
|
||||
const CAPTURE_DELAY: float = 0.4
|
||||
|
||||
const COLOR_BG: Color = Color(0.05, 0.06, 0.09)
|
||||
const COLOR_PANEL: Color = Color(0.10, 0.12, 0.16)
|
||||
const COLOR_PANEL_BORDER: Color = Color(0.28, 0.31, 0.40)
|
||||
const COLOR_TITLE: Color = Color(0.92, 0.78, 0.32)
|
||||
const COLOR_TEXT: Color = Color(0.92, 0.93, 0.96)
|
||||
const COLOR_DIM: Color = Color(0.62, 0.65, 0.72)
|
||||
const COLOR_WIN: Color = Color(0.95, 0.78, 0.32)
|
||||
const COLOR_EVENT: Color = Color(0.45, 0.75, 0.95)
|
||||
|
||||
const PLAYERS: Array[Dictionary] = [
|
||||
{
|
||||
"name": "Karak Ankor",
|
||||
"color": Color(0.95, 0.65, 0.15),
|
||||
"score": 107,
|
||||
"pop": 11,
|
||||
"cities": 3,
|
||||
"techs": 12,
|
||||
"wonders": 1,
|
||||
"win": "DOMINATION",
|
||||
},
|
||||
{
|
||||
"name": "Goldvein",
|
||||
"color": Color(0.95, 0.85, 0.25),
|
||||
"score": 97,
|
||||
"pop": 9,
|
||||
"cities": 2,
|
||||
"techs": 11,
|
||||
"wonders": 0,
|
||||
"win": "",
|
||||
},
|
||||
{
|
||||
"name": "Blackhammer",
|
||||
"color": Color(0.45, 0.25, 0.85),
|
||||
"score": 87,
|
||||
"pop": 7,
|
||||
"cities": 1,
|
||||
"techs": 9,
|
||||
"wonders": 0,
|
||||
"win": "",
|
||||
},
|
||||
## Each variant: (winner_index, reason). winner_index -1 = stalemate (Resigned).
|
||||
const VARIANTS: Array[Dictionary] = [
|
||||
{"winner": 0, "reason": "LastSurvivor"},
|
||||
{"winner": 1, "reason": "ConditionMet"},
|
||||
{"winner": 2, "reason": "TurnLimit"},
|
||||
{"winner": -1, "reason": "Resigned"},
|
||||
]
|
||||
|
||||
const EVENTS: Array[Dictionary] = [
|
||||
{"turn": 1, "text": "Karak Ankor founded — Capital of the Iron Clan", "color": "founding"},
|
||||
{"turn": 7, "text": "Goldvein discovers Mining", "color": "tech"},
|
||||
{"turn": 15, "text": "Blackhammer raids Karak outpost", "color": "combat"},
|
||||
{"turn": 22, "text": "Goldvein founds Mithril Hold", "color": "founding"},
|
||||
{"turn": 38, "text": "Karak completes the Great Anvil (Tier 4 wonder)", "color": "wonder"},
|
||||
{"turn": 52, "text": "War declared: Karak Ankor → Blackhammer", "color": "diplomacy"},
|
||||
{"turn": 71, "text": "Blackhammer's capital falls to Karak siege", "color": "combat"},
|
||||
{"turn": 87, "text": "Domination victory — Karak Ankor controls every founding capital", "color": "win"},
|
||||
const FIXTURE_CLANS: Array[Dictionary] = [
|
||||
{"name": "Karak Ankor", "color": Color(0.95, 0.65, 0.15)},
|
||||
{"name": "Goldvein", "color": Color(0.95, 0.85, 0.25)},
|
||||
{"name": "Blackhammer", "color": Color(0.45, 0.25, 0.85)},
|
||||
]
|
||||
|
||||
const EVENT_COLOR_MAP: Dictionary = {
|
||||
"founding": Color(0.55, 0.85, 0.45),
|
||||
"tech": Color(0.45, 0.65, 0.95),
|
||||
"combat": Color(0.85, 0.45, 0.35),
|
||||
"wonder": Color(0.92, 0.78, 0.32),
|
||||
"diplomacy": Color(0.75, 0.50, 0.95),
|
||||
"win": Color(1.00, 0.85, 0.32),
|
||||
}
|
||||
|
||||
var _captured: bool = false
|
||||
var _layer: CanvasLayer = null
|
||||
var _summary: Control = null
|
||||
|
||||
|
||||
func _make_player(idx: int, pname: String, c: Color, human: bool) -> Player:
|
||||
var p: Player = PlayerScript.new(idx, pname)
|
||||
p.color = c
|
||||
p.is_human = human
|
||||
return p
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
get_viewport().size = VIEWPORT_SIZE
|
||||
DisplayServer.window_set_size(VIEWPORT_SIZE)
|
||||
RenderingServer.set_default_clear_color(COLOR_BG)
|
||||
RenderingServer.set_default_clear_color(Color(0.03, 0.035, 0.05))
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
ThemeAssets.set_theme("age-of-dwarves")
|
||||
queue_redraw()
|
||||
await get_tree().create_timer(CAPTURE_DELAY).timeout
|
||||
_capture_and_quit()
|
||||
ThemeVocabulary.load_vocabulary("age-of-dwarves")
|
||||
_seed_fixture()
|
||||
_run_variants()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var font: Font = ThemeDB.fallback_font
|
||||
draw_string(
|
||||
font, Vector2(40, 50),
|
||||
"End Game Summary — Turn 87",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 26, COLOR_TITLE,
|
||||
)
|
||||
draw_string(
|
||||
font, Vector2(40, 76),
|
||||
"Seed 000042 — Continents, Standard, 3 Players",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 14, COLOR_DIM,
|
||||
)
|
||||
|
||||
# ── Standings table ─────────────────────────────────────────────────
|
||||
var table_y: int = 120
|
||||
var col_xs: PackedInt32Array = [60, 280, 380, 480, 580, 700, 880]
|
||||
var headers: PackedStringArray = [
|
||||
"Player", "Score", "Pop", "Cities", "Techs", "Wonders", "Win Condition",
|
||||
]
|
||||
draw_rect(Rect2(Vector2(40, table_y - 8), Vector2(1100, 36)), COLOR_PANEL)
|
||||
for i: int in range(headers.size()):
|
||||
draw_string(
|
||||
font, Vector2(col_xs[i], table_y + 18),
|
||||
headers[i],
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 14, COLOR_TITLE,
|
||||
)
|
||||
# Rows.
|
||||
var row_y: int = table_y + 56
|
||||
for i: int in range(PLAYERS.size()):
|
||||
var p: Dictionary = PLAYERS[i]
|
||||
# Color dot.
|
||||
draw_circle(Vector2(45, row_y + 6), 10, p["color"] as Color)
|
||||
var winning: bool = String(p["win"]).length() > 0
|
||||
var name_color: Color = COLOR_WIN if winning else COLOR_TEXT
|
||||
draw_string(
|
||||
font, Vector2(col_xs[0] + 8, row_y + 12),
|
||||
String(p["name"]),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 18, name_color,
|
||||
)
|
||||
draw_string(font, Vector2(col_xs[1], row_y + 12), str(int(p["score"])),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 18, name_color)
|
||||
draw_string(font, Vector2(col_xs[2], row_y + 12), str(int(p["pop"])),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 16, COLOR_TEXT)
|
||||
draw_string(font, Vector2(col_xs[3], row_y + 12), str(int(p["cities"])),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 16, COLOR_TEXT)
|
||||
draw_string(font, Vector2(col_xs[4], row_y + 12), str(int(p["techs"])),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 16, COLOR_TEXT)
|
||||
draw_string(font, Vector2(col_xs[5], row_y + 12), str(int(p["wonders"])),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 16, COLOR_TEXT)
|
||||
draw_string(font, Vector2(col_xs[6], row_y + 12), String(p["win"]),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 16, COLOR_WIN)
|
||||
row_y += 36
|
||||
|
||||
# ── Event timeline ──────────────────────────────────────────────────
|
||||
var timeline_y: int = row_y + 30
|
||||
draw_string(
|
||||
font, Vector2(40, timeline_y),
|
||||
"Chronicle of the Age",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 18, COLOR_TITLE,
|
||||
)
|
||||
var ev_y: int = timeline_y + 28
|
||||
for e: Dictionary in EVENTS:
|
||||
var dot_color: Color = EVENT_COLOR_MAP.get(String(e["color"]), COLOR_DIM) as Color
|
||||
draw_circle(Vector2(58, ev_y + 8), 6, dot_color)
|
||||
draw_string(
|
||||
font, Vector2(80, ev_y + 12),
|
||||
"Turn %d" % int(e["turn"]),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 14, COLOR_DIM,
|
||||
)
|
||||
draw_string(
|
||||
font, Vector2(160, ev_y + 12),
|
||||
String(e["text"]),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 14, COLOR_TEXT,
|
||||
)
|
||||
ev_y += 24
|
||||
func _seed_fixture() -> void:
|
||||
## Player roster — player 0 is the human so victory/defeat banners resolve.
|
||||
var players: Array = []
|
||||
for i: int in FIXTURE_CLANS.size():
|
||||
var clan: Dictionary = FIXTURE_CLANS[i]
|
||||
players.append(_make_player(
|
||||
i, clan["name"] as String, clan["color"] as Color, i == 0))
|
||||
GameState.players = players
|
||||
GameState.current_player_index = 0
|
||||
## Seed a short score history so standings/graph/timeline render.
|
||||
_seed_stats_history()
|
||||
|
||||
|
||||
func _capture_and_quit() -> void:
|
||||
if _captured:
|
||||
func _seed_stats_history() -> void:
|
||||
## Push fixture snapshots straight into StatsTracker's history (the same
|
||||
## shape _on_turn_ended produces) so Standings / Graph / Timeline render.
|
||||
if not _has_node_singleton("StatsTracker"):
|
||||
return
|
||||
_captured = true
|
||||
StatsTracker.reset()
|
||||
for turn: int in range(1, 9):
|
||||
var snap_players: Array = []
|
||||
for i: int in FIXTURE_CLANS.size():
|
||||
snap_players.append({
|
||||
"index": i,
|
||||
"score": 40 + turn * (6 - i),
|
||||
"population": 3 + turn + i,
|
||||
"military": turn,
|
||||
"cities": 1 + turn / 3,
|
||||
"techs": turn / 2,
|
||||
"tech_count": turn / 2,
|
||||
"wonders": 1 if (turn > 4 and i == 0) else 0,
|
||||
"wonder_count": 1 if (turn > 4 and i == 0) else 0,
|
||||
})
|
||||
StatsTracker._history.append({"turn": turn, "players": snap_players})
|
||||
|
||||
|
||||
func _has_node_singleton(node_name: String) -> bool:
|
||||
var tree: SceneTree = get_tree()
|
||||
return tree != null and tree.root != null and tree.root.has_node(node_name)
|
||||
|
||||
|
||||
func _run_variants() -> void:
|
||||
for variant: Dictionary in VARIANTS:
|
||||
await _capture_variant(
|
||||
int(variant["winner"]), variant["reason"] as String)
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func _capture_variant(winner_index: int, reason: String) -> void:
|
||||
_layer = CanvasLayer.new()
|
||||
_layer.layer = 25
|
||||
add_child(_layer)
|
||||
_summary = EndGameSummaryScene.instantiate() as Control
|
||||
_layer.add_child(_summary)
|
||||
await get_tree().process_frame
|
||||
## Drive the EventBus path the production receiver uses.
|
||||
_summary._on_game_over(winner_index, reason)
|
||||
|
||||
for _i: int in SETTLE_FRAMES:
|
||||
await get_tree().process_frame
|
||||
await get_tree().create_timer(CAPTURE_DELAY).timeout
|
||||
|
||||
_save_screenshot(reason)
|
||||
|
||||
get_tree().paused = false
|
||||
_layer.queue_free()
|
||||
_summary = null
|
||||
_layer = null
|
||||
await get_tree().process_frame
|
||||
|
||||
|
||||
func _save_screenshot(reason: String) -> void:
|
||||
DirAccess.make_dir_recursive_absolute(
|
||||
ProjectSettings.globalize_path(OUTPUT_DIR)
|
||||
)
|
||||
ProjectSettings.globalize_path(OUTPUT_DIR))
|
||||
var image: Image = get_viewport().get_texture().get_image()
|
||||
if image == null:
|
||||
push_error("end_game_summary_proof: viewport image null")
|
||||
get_tree().quit(1)
|
||||
return
|
||||
var timestamp: String = Time.get_datetime_string_from_system().replace(
|
||||
":", "-"
|
||||
).replace("T", "_")
|
||||
var path: String = "%s/end_game_summary_proof_%s.png" % [OUTPUT_DIR, timestamp]
|
||||
var path: String = "%s/end_game_summary_proof_%s.png" % [OUTPUT_DIR, reason]
|
||||
var abs_path: String = ProjectSettings.globalize_path(path)
|
||||
var err: Error = image.save_png(abs_path)
|
||||
if err == OK:
|
||||
print("SCREENSHOT_PATH:%s" % abs_path)
|
||||
print("end_game_summary_proof: %dx%d saved" % [
|
||||
image.get_width(), image.get_height()
|
||||
])
|
||||
else:
|
||||
push_error("end_game_summary_proof: save failed: %s" % error_string(err))
|
||||
get_tree().quit()
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
[ext_resource type="Script" path="res://engine/scenes/tests/end_game_summary_proof.gd" id="1"]
|
||||
|
||||
[node name="EndGameSummaryProof" type="Node2D"]
|
||||
[node name="EndGameSummaryProof" type="Node"]
|
||||
script = ExtResource("1")
|
||||
|
|
|
|||
129
src/game/engine/scenes/tests/statistics_proof.gd
Normal file
129
src/game/engine/scenes/tests/statistics_proof.gd
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
extends Node
|
||||
## p2-47 — Statistics modal proof scene.
|
||||
##
|
||||
## Boots the REAL statistics.tscn (the thin wrapper over the 709-line
|
||||
## self-building StatisticsModal) with a fixture clan roster + StatsTracker
|
||||
## history, then captures one screenshot per tab (Demographics, Graphs,
|
||||
## Rankings, Replay, Histories). Proves the modal that F9 / the info button /
|
||||
## the Stats menu open now renders end-to-end against a missing-resource-free
|
||||
## scene asset.
|
||||
##
|
||||
## Live data (Demographics/Graphs/Rankings) comes from StatsTracker here;
|
||||
## the GdGameHistory bridge (Histories tab) is Rust-pending and shows its
|
||||
## pending notice, exactly as in-game.
|
||||
|
||||
const StatisticsScene: PackedScene = preload(
|
||||
"res://engine/scenes/statistics/statistics.tscn"
|
||||
)
|
||||
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
|
||||
|
||||
const OUTPUT_DIR: String = "user://screenshots"
|
||||
const VIEWPORT_SIZE: Vector2i = Vector2i(1280, 720)
|
||||
const SETTLE_FRAMES: int = 4
|
||||
const CAPTURE_DELAY: float = 0.4
|
||||
|
||||
const TAB_NAMES: Array[String] = [
|
||||
"demographics", "graphs", "rankings", "replay", "histories",
|
||||
]
|
||||
|
||||
const FIXTURE_CLANS: Array[Dictionary] = [
|
||||
{"name": "Karak Ankor", "color": Color(0.95, 0.65, 0.15)},
|
||||
{"name": "Goldvein", "color": Color(0.95, 0.85, 0.25)},
|
||||
{"name": "Blackhammer", "color": Color(0.45, 0.25, 0.85)},
|
||||
{"name": "Ironforge", "color": Color(0.30, 0.60, 0.90)},
|
||||
]
|
||||
|
||||
|
||||
var _layer: CanvasLayer = null
|
||||
var _modal: Control = null
|
||||
|
||||
|
||||
func _make_player(idx: int, pname: String, c: Color) -> Player:
|
||||
var p: Player = PlayerScript.new(idx, pname)
|
||||
p.color = c
|
||||
return p
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
get_viewport().size = VIEWPORT_SIZE
|
||||
DisplayServer.window_set_size(VIEWPORT_SIZE)
|
||||
RenderingServer.set_default_clear_color(Color(0.05, 0.05, 0.05))
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
ThemeAssets.set_theme("age-of-dwarves")
|
||||
ThemeVocabulary.load_vocabulary("age-of-dwarves")
|
||||
## StatsTracker caches its category labels at autoload-init (before the proof
|
||||
## loads the theme above). Rebuild them now so the Demographics/Graphs/Rankings
|
||||
## column + metric labels resolve to vocabulary copy, not title-case keys.
|
||||
## In-game the theme is loaded before StatsTracker, so this ordering is
|
||||
## proof-only.
|
||||
if _has_node_singleton("StatsTracker") and StatsTracker.has_method("_rebuild_labels"):
|
||||
StatsTracker._rebuild_labels()
|
||||
_seed_fixture()
|
||||
await _run()
|
||||
|
||||
|
||||
func _seed_fixture() -> void:
|
||||
var players: Array = []
|
||||
for i: int in FIXTURE_CLANS.size():
|
||||
var clan: Dictionary = FIXTURE_CLANS[i]
|
||||
players.append(_make_player(
|
||||
i, clan["name"] as String, clan["color"] as Color))
|
||||
GameState.players = players
|
||||
GameState.current_player_index = 0
|
||||
if _has_node_singleton("StatsTracker"):
|
||||
StatsTracker.reset()
|
||||
for turn: int in range(1, 13):
|
||||
var snap_players: Array = []
|
||||
for i: int in FIXTURE_CLANS.size():
|
||||
snap_players.append({
|
||||
"index": i,
|
||||
"score": 30 + turn * (7 - i) + i * 2,
|
||||
"population": 2 + turn + i,
|
||||
"military": turn / 2 + i,
|
||||
"cities": 1 + turn / 4,
|
||||
"techs": turn / 2,
|
||||
"wonders": 1 if (turn > 6 and i == 0) else 0,
|
||||
})
|
||||
StatsTracker._history.append({"turn": turn, "players": snap_players})
|
||||
|
||||
|
||||
func _has_node_singleton(node_name: String) -> bool:
|
||||
var tree: SceneTree = get_tree()
|
||||
return tree != null and tree.root != null and tree.root.has_node(node_name)
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_layer = CanvasLayer.new()
|
||||
_layer.layer = 25
|
||||
add_child(_layer)
|
||||
_modal = StatisticsScene.instantiate() as Control
|
||||
_layer.add_child(_modal)
|
||||
await get_tree().process_frame
|
||||
|
||||
for tab: int in TAB_NAMES.size():
|
||||
## Keep the TabBar highlight in sync with the panel we drive directly
|
||||
## (in-game the player clicks the TabBar, which does this for us).
|
||||
_modal._tab_bar.current_tab = tab
|
||||
_modal._on_tab_changed(tab)
|
||||
for _i: int in SETTLE_FRAMES:
|
||||
await get_tree().process_frame
|
||||
await get_tree().create_timer(CAPTURE_DELAY).timeout
|
||||
_save_screenshot(TAB_NAMES[tab])
|
||||
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func _save_screenshot(tab_name: String) -> void:
|
||||
DirAccess.make_dir_recursive_absolute(
|
||||
ProjectSettings.globalize_path(OUTPUT_DIR))
|
||||
var image: Image = get_viewport().get_texture().get_image()
|
||||
if image == null:
|
||||
push_error("statistics_proof: viewport image null")
|
||||
return
|
||||
var path: String = "%s/statistics_proof_%s.png" % [OUTPUT_DIR, tab_name]
|
||||
var abs_path: String = ProjectSettings.globalize_path(path)
|
||||
var err: Error = image.save_png(abs_path)
|
||||
if err == OK:
|
||||
print("SCREENSHOT_PATH:%s" % abs_path)
|
||||
else:
|
||||
push_error("statistics_proof: save failed: %s" % error_string(err))
|
||||
6
src/game/engine/scenes/tests/statistics_proof.tscn
Normal file
6
src/game/engine/scenes/tests/statistics_proof.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://engine/scenes/tests/statistics_proof.gd" id="1"]
|
||||
|
||||
[node name="StatisticsProof" type="Node"]
|
||||
script = ExtResource("1")
|
||||
Loading…
Add table
Reference in a new issue