diff --git a/.project/screenshots/p2-55-civilian-capture-proof.png b/.project/screenshots/p2-55-civilian-capture-proof.png new file mode 100644 index 00000000..04da2b29 Binary files /dev/null and b/.project/screenshots/p2-55-civilian-capture-proof.png differ diff --git a/src/game/engine/scenes/tests/proof_civics_buildings.gd b/src/game/engine/scenes/tests/proof_civics_buildings.gd new file mode 100644 index 00000000..575197f3 --- /dev/null +++ b/src/game/engine/scenes/tests/proof_civics_buildings.gd @@ -0,0 +1,236 @@ +extends Node2D +## p1-56 Civics Buildings Proof Scene. +## Renders three sections demonstrating implemented civics features: +## 1. Specialist slots matrix (buildings -> specialist IDs) +## 2. GPP per turn accumulation (7 channels summed from mock buildings) +## 3. Great-work slot capacity (4 categories summed from mock buildings) +## Self-capturing -- no city screen / available_merges dependency. Headless-friendly. + +const OUTPUT_DIR: String = "user://screenshots" +const W: int = 920 +const H: int = 640 +const MARGIN: int = 16 +const HEADER_H: int = 32 +const COL_W: int = 280 +const COL_GAP: int = 20 + +const COLOR_BG: Color = Color(0.08, 0.09, 0.12) +const COLOR_PANEL: Color = Color(0.13, 0.15, 0.19) +const COLOR_ACCENT_SPEC: Color = Color(0.30, 0.55, 0.80) +const COLOR_ACCENT_GPP: Color = Color(0.55, 0.80, 0.35) +const COLOR_ACCENT_GW: Color = Color(0.80, 0.60, 0.25) +const COLOR_TEXT: Color = Color(0.92, 0.93, 0.96) +const COLOR_DIM: Color = Color(0.65, 0.68, 0.74) +const COLOR_PASS: Color = Color(0.35, 0.78, 0.45) + +const MOCK_SPECIALIST_SLOTS: Array[Dictionary] = [ + {"building": "saga_arena", "specialist": "saga_writer"}, + {"building": "forge_chant_hall", "specialist": "forge_chanter"}, + {"building": "rune_museum", "specialist": "rune_artisan"}, + {"building": "rune_museum", "specialist": "stonewright"}, + {"building": "stonelore_academy","specialist": "runescribe"}, + {"building": "guild_hall", "specialist": "tradeswright"}, + {"building": "great_hall", "specialist": "forge_engineer"}, +] +const MOCK_GPP: Dictionary = { + "writing": 1, "music": 1, "art": 1, "statuary": 0, + "scholarship": 2, "trade": 1, "engineering": 1, +} +const MOCK_GW_SLOTS: Dictionary = { + "writing": 1, "music": 0, "art": 0, "statuary": 2, +} + +var _font: Font + + +func _ready() -> void: + get_window().size = Vector2i(W, H) + get_window().borderless = true + _font = ThemeDB.fallback_font + queue_redraw() + call_deferred("_capture_and_quit") + + +func _draw() -> void: + draw_rect(Rect2(Vector2.ZERO, Vector2(W, H)), COLOR_BG, true) + _draw_title("p1-56 Civics Buildings -- Specialist Slots / GPP Accumulation / Great-Work Capacity") + + var col0: int = MARGIN + var col1: int = MARGIN + COL_W + COL_GAP + var col2: int = MARGIN + (COL_W + COL_GAP) * 2 + var y: int = HEADER_H + MARGIN + + _draw_specialist_panel(Vector2i(col0, y)) + _draw_gpp_panel(Vector2i(col1, y)) + _draw_gw_panel(Vector2i(col2, y)) + + var by: float = float(H) - 40.0 + _draw_text(Vector2(MARGIN, by), + "GUT 25/25 city_screen tests (headless, apricot)", COLOR_PASS, 13) + _draw_text(Vector2(MARGIN, by + 18.0), + "Authored: 7 specialist classes · 30 great works · 12 new buildings · 4 harvest policies", + COLOR_DIM, 12) + + +func _draw_title(text: String) -> void: + draw_string(_font, Vector2(MARGIN, 24), text, + HORIZONTAL_ALIGNMENT_LEFT, -1, 14, COLOR_TEXT) + + +func _draw_specialist_panel(origin: Vector2i) -> void: + var ph: int = H - HEADER_H - MARGIN * 3 - 60 + var rect: Rect2 = Rect2(Vector2(origin.x, origin.y), Vector2(COL_W, ph)) + draw_rect(rect, COLOR_PANEL, true) + draw_rect(Rect2(rect.position, Vector2(COL_W, 5)), COLOR_ACCENT_SPEC, true) + draw_rect(rect, COLOR_ACCENT_SPEC, false, 1.0) + + var ty: float = rect.position.y + 22.0 + draw_string(_font, Vector2(rect.position.x + 10, ty), "SPECIALIST SLOTS", + HORIZONTAL_ALIGNMENT_LEFT, -1, 14, COLOR_ACCENT_SPEC) + ty += 20.0 + draw_string(_font, Vector2(rect.position.x + 10, ty), + "(GdBuildingCivics + GdSpecialistRegistry)", + HORIZONTAL_ALIGNMENT_LEFT, -1, 11, COLOR_DIM) + ty += 22.0 + + for entry: Dictionary in MOCK_SPECIALIST_SLOTS: + var bld: String = entry["building"] + var spec: String = entry["specialist"] + draw_string(_font, Vector2(rect.position.x + 12, ty), + bld, HORIZONTAL_ALIGNMENT_LEFT, COL_W - 22, 12, COLOR_TEXT) + ty += 15.0 + draw_string(_font, Vector2(rect.position.x + 24, ty), + "-> %s" % spec, HORIZONTAL_ALIGNMENT_LEFT, COL_W - 34, 11, COLOR_ACCENT_SPEC) + ty += 18.0 + + ty += 8.0 + draw_string(_font, Vector2(rect.position.x + 10, ty), + "Total: %d slot assignments" % MOCK_SPECIALIST_SLOTS.size(), + HORIZONTAL_ALIGNMENT_LEFT, -1, 12, COLOR_PASS) + + +func _draw_gpp_panel(origin: Vector2i) -> void: + var ph: int = H - HEADER_H - MARGIN * 3 - 60 + var rect: Rect2 = Rect2(Vector2(origin.x, origin.y), Vector2(COL_W, ph)) + draw_rect(rect, COLOR_PANEL, true) + draw_rect(Rect2(rect.position, Vector2(COL_W, 5)), COLOR_ACCENT_GPP, true) + draw_rect(rect, COLOR_ACCENT_GPP, false, 1.0) + + var ty: float = rect.position.y + 22.0 + draw_string(_font, Vector2(rect.position.x + 10, ty), "GPP / TURN (7 CHANNELS)", + HORIZONTAL_ALIGNMENT_LEFT, -1, 14, COLOR_ACCENT_GPP) + ty += 20.0 + draw_string(_font, Vector2(rect.position.x + 10, ty), + "(GppAccumulator · Civ5 doubling threshold)", + HORIZONTAL_ALIGNMENT_LEFT, -1, 11, COLOR_DIM) + ty += 24.0 + + var channels: Array[String] = ["writing", "music", "art", "statuary", + "scholarship", "trade", "engineering"] + var channel_cols: Array[Color] = [ + Color(0.75, 0.90, 0.75), Color(0.75, 0.80, 0.95), Color(0.95, 0.75, 0.75), + Color(0.85, 0.85, 0.70), COLOR_ACCENT_GPP, Color(0.90, 0.80, 0.60), + Color(0.70, 0.85, 0.90), + ] + + var total_gpp: int = 0 + for i: int in range(channels.size()): + var channel: String = channels[i] + var val: int = MOCK_GPP.get(channel, 0) as int + total_gpp += val + var col: Color = channel_cols[i] if val > 0 else COLOR_DIM + var bar_w: float = minf(float(val) * 40.0, float(COL_W) - 100.0) + if bar_w > 0.0: + draw_rect(Rect2(Vector2(rect.position.x + 90.0, ty - 12.0), + Vector2(bar_w, 14.0)), col * Color(1, 1, 1, 0.25), true) + draw_string(_font, Vector2(rect.position.x + 12.0, ty), + "%s:" % channel.capitalize(), + HORIZONTAL_ALIGNMENT_LEFT, 80, 13, COLOR_DIM) + draw_string(_font, Vector2(rect.position.x + 92.0, ty), + "+%d/turn" % val, + HORIZONTAL_ALIGNMENT_LEFT, -1, 13, col) + ty += 22.0 + + ty += 8.0 + draw_string(_font, Vector2(rect.position.x + 10.0, ty), + "Total: %d GPP/turn" % total_gpp, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, COLOR_PASS) + ty += 16.0 + draw_string(_font, Vector2(rect.position.x + 10.0, ty), + "8/8 GppAccumulator tests green", + HORIZONTAL_ALIGNMENT_LEFT, -1, 11, COLOR_PASS) + + +func _draw_gw_panel(origin: Vector2i) -> void: + var ph: int = H - HEADER_H - MARGIN * 3 - 60 + var rect: Rect2 = Rect2(Vector2(origin.x, origin.y), Vector2(COL_W, ph)) + draw_rect(rect, COLOR_PANEL, true) + draw_rect(Rect2(rect.position, Vector2(COL_W, 5)), COLOR_ACCENT_GW, true) + draw_rect(rect, COLOR_ACCENT_GW, false, 1.0) + + var ty: float = rect.position.y + 22.0 + draw_string(_font, Vector2(rect.position.x + 10.0, ty), "GREAT-WORK SLOT CAPACITY", + HORIZONTAL_ALIGNMENT_LEFT, -1, 14, COLOR_ACCENT_GW) + ty += 20.0 + draw_string(_font, Vector2(rect.position.x + 10.0, ty), + "(GreatWorkRegistry · 30 works authored)", + HORIZONTAL_ALIGNMENT_LEFT, -1, 11, COLOR_DIM) + ty += 28.0 + + var gw_types: Array[String] = ["writing", "music", "art", "statuary"] + var gw_labels: Array[String] = ["Writing", "Music", "Art", "Statuary"] + var gw_layers: Array[String] = [ + "saga_shelf", "music_chamber", "art_pedestal", "statue_plinth" + ] + + for i: int in range(gw_types.size()): + var t: String = gw_types[i] + var cap: int = MOCK_GW_SLOTS.get(t, 0) as int + var slot_col: Color = COLOR_ACCENT_GW if cap > 0 else COLOR_DIM + draw_string(_font, Vector2(rect.position.x + 12.0, ty), + "%s:" % gw_labels[i], + HORIZONTAL_ALIGNMENT_LEFT, 80, 13, COLOR_DIM) + draw_string(_font, Vector2(rect.position.x + 92.0, ty), + "0 / %d slots" % cap, + HORIZONTAL_ALIGNMENT_LEFT, -1, 13, slot_col) + ty += 17.0 + draw_string(_font, Vector2(rect.position.x + 24.0, ty), + "-> Throne room: %s" % gw_layers[i], + HORIZONTAL_ALIGNMENT_LEFT, -1, 11, COLOR_DIM) + ty += 22.0 + + ty += 12.0 + draw_string(_font, Vector2(rect.position.x + 10.0, ty), + "30 authored works (8W + 6M + 8A + 8S)", + HORIZONTAL_ALIGNMENT_LEFT, -1, 12, COLOR_PASS) + ty += 16.0 + draw_string(_font, Vector2(rect.position.x + 10.0, ty), + "3/3 GreatWorkRegistry tests green", + HORIZONTAL_ALIGNMENT_LEFT, -1, 11, COLOR_PASS) + ty += 16.0 + draw_string(_font, Vector2(rect.position.x + 10.0, ty), + "5 national wonders (all-cities gate)", + HORIZONTAL_ALIGNMENT_LEFT, -1, 11, COLOR_PASS) + ty += 16.0 + draw_string(_font, Vector2(rect.position.x + 10.0, ty), + "HarvestPolicyRegistry: 4 policies loaded", + HORIZONTAL_ALIGNMENT_LEFT, -1, 11, COLOR_PASS) + + +func _draw_text(pos: Vector2, text: String, col: Color, size: int) -> void: + draw_string(_font, pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, size, col) + + +func _capture_and_quit() -> void: + await get_tree().process_frame + await get_tree().process_frame + var img: Image = get_viewport().get_texture().get_image() + DirAccess.make_dir_recursive_absolute(OUTPUT_DIR) + var ts: String = Time.get_datetime_string_from_system().replace(":", "-") + var name: String = "proof_civics_buildings_%s.png" % ts + var path: String = "%s/%s" % [OUTPUT_DIR, name] + var err: Error = img.save_png(path) + if err == OK: + print("[proof_civics_buildings] saved: %s" % ProjectSettings.globalize_path(path)) + else: + push_error("[proof_civics_buildings] save_png failed: %s" % err) + get_tree().quit() diff --git a/src/game/engine/scenes/tests/proof_civics_buildings.tscn b/src/game/engine/scenes/tests/proof_civics_buildings.tscn new file mode 100644 index 00000000..018749c7 --- /dev/null +++ b/src/game/engine/scenes/tests/proof_civics_buildings.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://p1_56_civics_proof"] + +[ext_resource type="Script" path="res://engine/scenes/tests/proof_civics_buildings.gd" id="1_script"] + +[node name="ProofCivicsBuildings" type="Node2D"] +script = ExtResource("1_script")