feat(@projects/@magic-civilization): add procedural city/unit overlay test

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-10 13:18:51 -07:00
parent 97f6aaea45
commit e2fdd69823
3 changed files with 104 additions and 1 deletions

View file

@ -0,0 +1,86 @@
extends Node2D
## Procedural overlay layer for `gameplay_arc_proof`.
##
## CityRenderer + UnitRenderer fall back to plain colored circles when the
## authored PNGs under `sprites/buildings/` and `sprites/cities/` are
## missing (Game-1 alpha state). That makes every city look the same dot
## and every placed-building a tiny letter square. This overlay reads the
## same `GameState.players` data and paints procedural textures via
## `ProceduralRenderer.make_*_texture` *on top* of the production
## renderers, giving each city + building + unit a deterministic but
## visually-distinct silhouette without modifying production code.
const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
const CityScript: GDScript = preload("res://engine/src/entities/city.gd")
## Set by the arc scene each step before queue_redraw().
var cities: Array = []
var units: Array = []
func refresh(new_cities: Array, new_units: Array) -> void:
cities = new_cities
units = new_units
queue_redraw()
func _draw() -> void:
# Cities — procedural city texture sized by pop tier.
for c_var: Variant in cities:
var c: CityScript = c_var as CityScript
if c == null:
continue
var pixel: Vector2 = (
HexUtilsScript.axial_to_pixel(c.position) + HexUtilsScript.hex_center
)
var pop: int = c.population
var tier: int = clampi((pop + 1) / 2, 1, 5)
var ctex: Texture2D = ProceduralRenderer.make_city_texture(c.id, tier)
if ctex != null:
var tex_size: Vector2 = ctex.get_size()
draw_texture(ctex, pixel - tex_size * 0.5)
# Buildings — procedural building texture per placed_buildings entry.
for bid: String in c.placed_buildings:
var b_axial: Vector2i = c.placed_buildings[bid] as Vector2i
var b_pixel: Vector2 = (
HexUtilsScript.axial_to_pixel(b_axial) + HexUtilsScript.hex_center
)
var btex: Texture2D = ProceduralRenderer.make_building_texture(bid)
if btex != null:
var bt_size: Vector2 = btex.get_size()
# Scale building icons to ~120px so multiple fit on adjacent
# hexes without overlapping.
var scale: float = 120.0 / maxf(bt_size.x, bt_size.y)
var draw_size: Vector2 = bt_size * scale
draw_texture_rect(
btex,
Rect2(b_pixel - draw_size * 0.5, draw_size),
false,
)
# Units — procedural unit silhouette per unit.
for u_var: Variant in units:
var u: UnitScript = u_var as UnitScript
if u == null:
continue
var u_pixel: Vector2 = (
HexUtilsScript.axial_to_pixel(u.position) + HexUtilsScript.hex_center
)
var uid: String = u.unit_id if u.unit_id != "" else u.type_id
if uid == "":
uid = u.unit_type
var race: String = "dwarves"
var utex: Texture2D = ProceduralRenderer.make_unit_texture(uid, race, "m")
if utex != null:
var ut_size: Vector2 = utex.get_size()
var u_scale: float = 100.0 / maxf(ut_size.x, ut_size.y)
var u_draw: Vector2 = ut_size * u_scale
# Offset slightly so the unit silhouette doesn't fight the
# CityRenderer's colored dot on the same hex.
draw_texture_rect(
utex,
Rect2(u_pixel - u_draw * 0.5 + Vector2(0, -20), u_draw),
false,
)

View file

@ -19,6 +19,9 @@ const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
const HexRendererScript: GDScript = preload("res://engine/src/rendering/hex_renderer.gd")
const UnitRendererScript: GDScript = preload("res://engine/src/rendering/unit_renderer.gd")
const CityRendererScript: GDScript = preload("res://engine/src/rendering/city_renderer.gd")
const ProceduralOverlayScript: GDScript = preload(
"res://engine/scenes/tests/gameplay_arc_overlay.gd"
)
const OUTPUT_DIR: String = "user://screenshots/gameplay_arc"
const VIEWPORT_SIZE: Vector2i = Vector2i(1920, 1080)
@ -26,6 +29,7 @@ const VIEWPORT_SIZE: Vector2i = Vector2i(1920, 1080)
var _hex: Node2D = null
var _units: Node2D = null
var _cities: Node2D = null
var _overlay: Node2D = null
var _cam: Camera2D = null
var _game_map: RefCounted = null
var _all_positions: Array[Vector2i] = []
@ -302,6 +306,13 @@ func _setup_renderers() -> void:
_cities = CityRendererScript.new()
_cities.name = "CityRenderer"
add_child(_cities)
# Procedural overlay paints differentiated city/building/unit silhouettes
# on top of the production renderers' baseline circles. See
# gameplay_arc_overlay.gd for why.
_overlay = ProceduralOverlayScript.new()
_overlay.name = "ProceduralOverlay"
_overlay.z_index = 50
add_child(_overlay)
func _sync_renderers() -> void:
@ -313,6 +324,7 @@ func _sync_renderers() -> void:
all_cities.append_array((p as PlayerScript).cities)
_units.call("sync_units", all_units)
_cities.call("sync_cities", all_cities)
_overlay.call("refresh", all_cities, all_units)
var empty_fog: Array[Vector2i] = []
_hex.update_fog(_all_positions, empty_fog)
_hex.queue_redraw()

View file

@ -53,7 +53,12 @@
"Bash(git worktree *)",
"mcp__experts__loop_stop",
"Read(//private/tmp/mc-divergence/inspect/**)",
"Read(//private/tmp/mc-divergence/**)"
"Read(//private/tmp/mc-divergence/**)",
"Bash(shasum -a 256 magic_civ_*.png)",
"Bash(awk '{print $1}')",
"Bash(rm -f magic_civ_gameplay_demo.zip)",
"Bash(zip -j magic_civ_gameplay_demo.zip magic_civ_gameplay_arc_*.png magic_civ_demo_*.png)",
"Bash(unzip -l magic_civ_gameplay_demo.zip)"
]
}
}