feat(@projects/@magic-civilization): add full-game visual demo proof scene

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-10 04:00:36 -07:00
parent 70490781f3
commit 63ec984c36

View file

@ -0,0 +1,156 @@
extends Node2D
## p2-66 Full-game visual demo proof.
##
## Bypasses world_map.tscn's SubViewport tiering (which renders black under
## headless llvmpipe when mounted as a child node) and instead drives the
## hex / unit / city renderers directly with a Camera2D pointed at the map
## centre. All tiles are forced fully visible (fog disabled). Captures a
## non-black 1920×1080 screenshot proving the gameplay surface renders.
const MapGeneratorScript: GDScript = preload("res://engine/src/generation/map_generator.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")
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 OUTPUT_DIR: String = "user://screenshots"
const VIEWPORT_SIZE: Vector2i = Vector2i(1920, 1080)
var _captured: bool = false
func _ready() -> void:
OS.set_environment("MC_USE_PROCEDURAL_SPRITES", "1")
OS.set_environment("FORCE_DISABLE_FOGOFWAR", "true")
DisplayServer.window_set_size(VIEWPORT_SIZE)
get_viewport().size = VIEWPORT_SIZE
RenderingServer.set_default_clear_color(Color(0.05, 0.07, 0.10))
DataLoader.load_theme("age-of-dwarves")
DataLoader.load_world("earth")
ThemeAssets.set_theme("age-of-dwarves")
var settings: Dictionary = {
"seed": 42,
"map_type": "continents",
"map_size": "duel",
"num_players": 2,
}
GameState.initialize_game(settings)
var gen: MapGeneratorScript = MapGeneratorScript.new()
var game_map: RefCounted = gen.generate(settings)
if game_map == null:
push_error("full_game_demo: MapGenerator returned null")
get_tree().quit(1)
return
print("full_game_demo: Map %dx%d (%d tiles)" % [
game_map.width, game_map.height, game_map.tiles.size()
])
var primary: Dictionary = GameState.get_primary_layer()
primary["map"] = game_map
# Spawn 2 players + a founder unit + a capital city per player at
# their map start positions so the unit + city renderers have data.
for i: int in range(2):
var player: PlayerScript = PlayerScript.new()
player.index = i
player.is_human = (i == 0)
player.player_name = "Player %d" % (i + 1)
player.race_id = "dwarf"
player.color = Color(0.85, 0.65, 0.2) if i == 0 else Color(0.2, 0.55, 0.85)
GameState.players.append(player)
var start: Vector2i = Vector2i.ZERO
if i < game_map.start_positions.size():
start = game_map.start_positions[i]
var unit: UnitScript = UnitScript.new("dwarf_warrior", i, start)
unit.id = "unit_%d_warrior" % i
player.units.append(unit)
var city: CityScript = CityScript.new()
city.player_index = i
city.position = start
city.id = "city_%d_capital" % i
city.city_name = "%s Capital" % player.player_name
city.population = 3 + i
player.cities.append(city)
# Build the renderers as direct children — no SubViewport hierarchy.
var hex: Node2D = HexRendererScript.new()
hex.name = "HexRenderer"
add_child(hex)
# Force-mark every tile as fully visible by feeding update_fog all positions.
hex.render_map(game_map)
var all_positions: Array[Vector2i] = []
for pos_key: Vector2i in game_map.tiles:
all_positions.append(pos_key)
hex.update_fog(all_positions, [])
var units: Node2D = UnitRendererScript.new()
units.name = "UnitRenderer"
add_child(units)
if units.has_method("setup_visibility"):
units.call("setup_visibility", 0, game_map)
if units.has_method("render_units"):
units.call("render_units", game_map)
elif units.has_method("refresh"):
units.call("refresh")
var cities: Node2D = CityRendererScript.new()
cities.name = "CityRenderer"
add_child(cities)
if cities.has_method("refresh"):
cities.call("refresh")
elif cities.has_method("render_cities"):
cities.call("render_cities")
# Camera centred on the map middle, zoomed out enough to see the
# whole continent.
var cam: Camera2D = Camera2D.new()
cam.name = "DemoCamera"
var mid_axial: Vector2i = Vector2i(game_map.width / 2, game_map.height / 2)
var mid_pixel: Vector2 = HexUtilsScript.axial_to_pixel(mid_axial)
cam.position = mid_pixel
# axial_to_pixel scales each tile to ~150px; duel map (40×24) = ~6000×3600 px.
# Zoom 0.18 fits the whole map into 1920×1080.
cam.zoom = Vector2(0.18, 0.18)
cam.make_current()
add_child(cam)
# Wait for the renderers to flush their _draw passes.
for _i: int in range(20):
await get_tree().process_frame
await get_tree().create_timer(1.5).timeout
_capture_and_quit()
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("full_game_demo: viewport image null")
get_tree().quit(1)
return
var timestamp: String = Time.get_datetime_string_from_system().replace(
":", "-"
).replace("T", "_")
var rel: String = "%s/full_game_demo_%s.png" % [OUTPUT_DIR, timestamp]
var abs_path: String = ProjectSettings.globalize_path(rel)
var err: Error = image.save_png(abs_path)
if err == OK:
print("SCREENSHOT_PATH:%s" % abs_path)
print("full_game_demo: %dx%d saved" % [image.get_width(), image.get_height()])
else:
push_error("full_game_demo: save failed: %s" % error_string(err))
get_tree().quit()