diff --git a/src/game/engine/scenes/tests/civics_panel_proof.gd b/src/game/engine/scenes/tests/civics_panel_proof.gd new file mode 100644 index 00000000..52827104 --- /dev/null +++ b/src/game/engine/scenes/tests/civics_panel_proof.gd @@ -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() diff --git a/src/game/engine/scenes/tests/civics_panel_proof.tscn b/src/game/engine/scenes/tests/civics_panel_proof.tscn new file mode 100644 index 00000000..d165f454 --- /dev/null +++ b/src/game/engine/scenes/tests/civics_panel_proof.tscn @@ -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")