feat(@projects/@magic-civilization): add civics panel proof test scene

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-10 23:39:50 -07:00
parent b10ff38df5
commit 9405415f7d
2 changed files with 221 additions and 0 deletions

View file

@ -0,0 +1,215 @@
extends Node2D
## Civics panel proof — visualises the 3 civic axes (Authority, Labor,
## Economy) with current choice + anarchy countdown, mirroring the data
## a real civics panel would display from `PlayerState.civic_state`.
## Self-contained with mock state; no GameState autoload required.
const OUTPUT_DIR: String = "user://screenshots"
const VIEWPORT_SIZE: Vector2i = Vector2i(1280, 720)
const CAPTURE_DELAY: float = 0.6
const COLOR_BG: Color = Color(0.06, 0.07, 0.10)
const COLOR_PANEL: Color = Color(0.12, 0.13, 0.18)
const COLOR_ACCENT: Color = Color(0.90, 0.78, 0.32)
const COLOR_AXIS_BORDER: Color = Color(0.28, 0.31, 0.40)
const COLOR_TEXT: Color = Color(0.92, 0.93, 0.96)
const COLOR_DIM: Color = Color(0.55, 0.58, 0.66)
const COLOR_ANARCHY: Color = Color(0.85, 0.30, 0.30)
const AXES: Array[Dictionary] = [
{
"name": "Authority",
"current": "monarchy",
"choices": ["chieftainship", "monarchy", "republic"],
"description": "How power flows through the throne. Active: Monarchy.",
},
{
"name": "Labor",
"current": "guilds",
"choices": ["labor_pool", "guilds", "serfdom"],
"description": "How citizens are organised for production. Active: Guilds.",
},
{
"name": "Economy",
"current": "mercantilism",
"choices": ["mercantilism", "tradition", "free_market"],
"description": "How wealth circulates through the empire. Active: Mercantilism.",
},
]
const ANARCHY_TURNS_REMAINING: int = 3
const ANARCHY_DURATION: int = 5
var _captured: bool = false
func _ready() -> void:
get_viewport().size = VIEWPORT_SIZE
DisplayServer.window_set_size(VIEWPORT_SIZE)
RenderingServer.set_default_clear_color(COLOR_BG)
# Theme load is harmless even though we render via _draw; future
# extensions may want labelled iconography from ThemeAssets.
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()
func _draw() -> void:
var font: Font = ThemeDB.fallback_font
# ── Title ────────────────────────────────────────────────────────────
draw_string(
font,
Vector2(40, 50),
"Civics — Government Axes",
HORIZONTAL_ALIGNMENT_LEFT,
-1,
28,
COLOR_ACCENT,
)
draw_string(
font,
Vector2(40, 78),
"Current government composition + anarchy state",
HORIZONTAL_ALIGNMENT_LEFT,
-1,
14,
COLOR_DIM,
)
# ── Axis cards ───────────────────────────────────────────────────────
var card_w: int = 380
var card_h: int = 240
var card_y: int = 130
var gap: int = 24
for i: int in range(AXES.size()):
var x: int = 40 + i * (card_w + gap)
_draw_axis_card(font, Vector2(x, card_y), card_w, card_h, AXES[i] as Dictionary)
# ── Anarchy banner ───────────────────────────────────────────────────
var anarchy_y: int = card_y + card_h + 40
_draw_anarchy_banner(font, Vector2(40, anarchy_y), 1200, 80)
# ── Footer note ──────────────────────────────────────────────────────
draw_string(
font,
Vector2(40, anarchy_y + 110),
"Switching an axis triggers 5 turns of Anarchy (gold income → 0, production → 50%%).",
HORIZONTAL_ALIGNMENT_LEFT,
-1,
13,
COLOR_DIM,
)
func _draw_axis_card(
font: Font, origin: Vector2, w: int, h: int, axis: Dictionary
) -> void:
draw_rect(Rect2(origin, Vector2(w, h)), COLOR_PANEL)
draw_rect(Rect2(origin, Vector2(w, h)), COLOR_AXIS_BORDER, false, 2.0)
draw_string(
font,
origin + Vector2(16, 32),
String(axis["name"]),
HORIZONTAL_ALIGNMENT_LEFT, -1, 22, COLOR_ACCENT,
)
# Choices list — current one highlighted.
var current: String = String(axis["current"])
var y_off: int = 70
for choice: String in (axis["choices"] as Array):
var is_current: bool = choice == current
var label: String = "" if is_current else ""
label += _format_choice(choice)
var color: Color = COLOR_ACCENT if is_current else COLOR_TEXT
draw_string(
font,
origin + Vector2(20, y_off),
label,
HORIZONTAL_ALIGNMENT_LEFT, -1, 18,
color,
)
y_off += 32
# Description footer.
draw_string(
font,
origin + Vector2(16, h - 32),
String(axis["description"]),
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, COLOR_DIM,
)
func _draw_anarchy_banner(font: Font, origin: Vector2, w: int, h: int) -> void:
if ANARCHY_TURNS_REMAINING <= 0:
draw_rect(Rect2(origin, Vector2(w, h)), COLOR_PANEL)
draw_rect(Rect2(origin, Vector2(w, h)), COLOR_AXIS_BORDER, false, 2.0)
draw_string(
font,
origin + Vector2(16, h / 2 + 6),
"Stable government — no anarchy active.",
HORIZONTAL_ALIGNMENT_LEFT, -1, 18, COLOR_TEXT,
)
return
# Red-tinted banner with countdown bar.
var bg: Color = Color(COLOR_ANARCHY.r, COLOR_ANARCHY.g, COLOR_ANARCHY.b, 0.18)
draw_rect(Rect2(origin, Vector2(w, h)), bg)
draw_rect(Rect2(origin, Vector2(w, h)), COLOR_ANARCHY, false, 2.0)
draw_string(
font,
origin + Vector2(16, 26),
"ANARCHY",
HORIZONTAL_ALIGNMENT_LEFT, -1, 20, COLOR_ANARCHY,
)
var line: String = "%d / %d turns remaining" % [
ANARCHY_TURNS_REMAINING, ANARCHY_DURATION
]
draw_string(
font,
origin + Vector2(16, 50),
line,
HORIZONTAL_ALIGNMENT_LEFT, -1, 16, COLOR_TEXT,
)
# Countdown bar (filled = remaining duration).
var bar_origin: Vector2 = origin + Vector2(200, 28)
var bar_w: int = w - 220
var bar_h: int = 22
draw_rect(Rect2(bar_origin, Vector2(bar_w, bar_h)), Color(0.18, 0.18, 0.20))
var fill_w: int = int(bar_w * (float(ANARCHY_TURNS_REMAINING) / float(ANARCHY_DURATION)))
draw_rect(Rect2(bar_origin, Vector2(fill_w, bar_h)), COLOR_ANARCHY)
func _format_choice(id: String) -> String:
# Snake_case → Title Case.
var parts: PackedStringArray = id.split("_")
for i: int in range(parts.size()):
var word: String = parts[i]
if word.length() > 0:
parts[i] = word.substr(0, 1).to_upper() + word.substr(1)
return " ".join(parts)
func _capture_and_quit() -> void:
if _captured:
return
_captured = true
DirAccess.make_dir_recursive_absolute(
ProjectSettings.globalize_path(OUTPUT_DIR)
)
var image: Image = get_viewport().get_texture().get_image()
if image == null:
push_error("civics_panel_proof: viewport image null")
get_tree().quit(1)
return
var timestamp: String = Time.get_datetime_string_from_system().replace(
":", "-"
).replace("T", "_")
var rel_path: String = "%s/civics_panel_proof_%s.png" % [OUTPUT_DIR, timestamp]
var abs_path: String = ProjectSettings.globalize_path(rel_path)
var err: Error = image.save_png(abs_path)
if err == OK:
print("SCREENSHOT_PATH:%s" % abs_path)
print("civics_panel_proof: %dx%d saved" % [
image.get_width(), image.get_height()
])
else:
push_error("civics_panel_proof: save failed: %s" % error_string(err))
get_tree().quit()

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://engine/scenes/tests/civics_panel_proof.gd" id="1"]
[node name="CivicsPanelProof" type="Node2D"]
script = ExtResource("1")