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:
autocommit 2026-06-04 18:19:11 -07:00
parent 3d83f4781c
commit db51852022
6 changed files with 463 additions and 158 deletions

View 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"

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

View file

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

View file

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

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

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