Files
gamedev-the-steward/tools/capture_simulation_garden.gd
T
2026-07-12 11:42:10 +02:00

222 lines
7.7 KiB
GDScript

extends SceneTree
const SCENE_PATH := "res://main.tscn"
const DEBUG_OUTPUT_PATH := "res://docs/baselines/simulation_garden_01_debug.png"
const CINEMATIC_OUTPUT_PATH := "res://docs/baselines/simulation_garden_01_cinematic.png"
const CAPTURE_SIZE := Vector2i(1280, 720)
const WARMUP_FRAMES := 540
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
root.size = CAPTURE_SIZE
var scene := load(SCENE_PATH) as PackedScene
if scene == null:
push_error("Could not load %s" % SCENE_PATH)
quit(1)
return
var main_scene := scene.instantiate()
var simulation_manager := main_scene.get_node("SimulationManager")
simulation_manager.debug_logs = false
root.add_child(main_scene)
await process_frame
_apply_capture_camera(main_scene)
for _frame in WARMUP_FRAMES:
await process_frame
_apply_capture_camera(main_scene)
var active_npcs := main_scene.get_node("ActiveNPCs")
var demo_controller := main_scene.get_node("DemoController")
if simulation_manager.npcs.is_empty() or active_npcs.get_child_count() == 0:
push_error("Simulation Garden capture needs active NPCs")
quit(1)
return
if demo_controller.has_method("toggle_debug_overlay"):
if not demo_controller.debug_overlay_visible:
demo_controller.toggle_debug_overlay()
if not _stage_social_history(main_scene):
quit(1)
return
await _stage_wind_gust(main_scene)
await _settle_trust_reaction()
var debug_analysis := _capture(DEBUG_OUTPUT_PATH)
if debug_analysis.is_empty():
quit(1)
return
if demo_controller.has_method("toggle_debug_overlay") and demo_controller.debug_overlay_visible:
demo_controller.toggle_debug_overlay()
if not _restart_trust_reaction(main_scene):
quit(1)
return
await _stage_wind_gust(main_scene)
await _settle_trust_reaction()
var cinematic_analysis := _capture(CINEMATIC_OUTPUT_PATH)
if cinematic_analysis.is_empty():
quit(1)
return
print(
(
"[TOOL] Saved Simulation Garden captures | debug %s | cinematic %s"
% [debug_analysis, cinematic_analysis]
)
)
quit(0)
func _stage_social_history(main_scene: Node) -> bool:
var simulation_manager := main_scene.get_node("SimulationManager")
var world_view := main_scene.get_node("WorldViewManager")
if simulation_manager.npcs.size() < 2:
push_error("Simulation Garden social-history staging needs two NPCs")
return false
var contributor: SimNPC = simulation_manager.npcs[0]
var observer: SimNPC = simulation_manager.npcs[1]
var contributor_visual: Node3D = world_view.active_npc_visuals.get(contributor.id)
var observer_visual: Node3D = world_view.active_npc_visuals.get(observer.id)
if contributor_visual == null or observer_visual == null:
push_error("Simulation Garden social-history staging needs both active NPC visuals")
return false
var relationship: RelationshipStateRecord = (
simulation_manager.relationship_system.get_relationship(observer.id, contributor.id)
)
if relationship == null:
push_error("Simulation Garden staging pair needs an existing directed relationship")
return false
var previous_trust := relationship.get_trust()
simulation_manager.set_process(false)
contributor_visual.set_physics_process(false)
observer_visual.set_physics_process(false)
observer_visual.global_position = contributor_visual.global_position + Vector3(1.0, 0.0, 0.0)
simulation_manager.synchronize_npc_position(contributor.id, contributor_visual.global_position)
simulation_manager.synchronize_npc_position(observer.id, observer_visual.global_position)
observer.hunger = 85.0
contributor.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
var deposited: float = simulation_manager.economy.deposit_inventory(
contributor, SimulationIds.RESOURCE_FOOD
)
if not is_equal_approx(deposited, 1.0):
push_error("Simulation Garden staging could not deposit one food")
return false
simulation_manager.emit_signal("village_changed", simulation_manager.village)
var contributor_events: Array = simulation_manager.get_npc_events(contributor.id, 1)
if contributor_events.is_empty():
push_error("Simulation Garden staging did not record the deposit event")
return false
var deposit_event := contributor_events[0] as EconomicEventRecord
var retained_memories: Array = simulation_manager.get_retained_memory_events(observer.id, 1)
var reaction_root := observer_visual.get_node_or_null("TrustReactionRoot") as Node3D
if (
StringName(deposit_event.data["event_type"]) != SimulationIds.EVENT_STORAGE_DEPOSITED
or relationship.get_trust() <= previous_trust
or relationship.get_last_trust_cause_event_id() != int(deposit_event.data["event_id"])
or retained_memories.is_empty()
or retained_memories[0] != deposit_event
or not simulation_manager.is_known_event_lasting(
observer.id, int(deposit_event.data["event_id"])
)
or reaction_root == null
or not reaction_root.visible
):
push_error("Simulation Garden staging did not produce the retained trust consequence")
return false
var village_ui := main_scene.get_node("UI")
village_ui.selected_npc_index = simulation_manager.npcs.find(observer)
village_ui.call("_refresh_npc_inspector")
return true
func _restart_trust_reaction(main_scene: Node) -> bool:
var simulation_manager := main_scene.get_node("SimulationManager")
var world_view := main_scene.get_node("WorldViewManager")
if simulation_manager.npcs.size() < 2:
push_error("Simulation Garden trust-reaction staging needs two NPCs")
return false
var observer: SimNPC = simulation_manager.npcs[1]
var observer_visual: Node3D = world_view.active_npc_visuals.get(observer.id)
if observer_visual == null or not observer_visual.has_method("play_trust_gain_reaction"):
push_error("Simulation Garden trust-reaction staging needs an active observer visual")
return false
observer_visual.play_trust_gain_reaction()
return observer_visual.get_node("TrustReactionRoot").visible
func _stage_wind_gust(main_scene: Node) -> void:
var gust_field := (
main_scene.get_node_or_null("JajceWorld/AtmosphereRoot/WindGustField") as WindGustField
)
if gust_field == null:
return
gust_field.trigger_gust_at(Vector3(1.5, 0.0, -1.0), true, 0.5)
for _frame in 2:
await process_frame
func _settle_trust_reaction() -> void:
for _frame in 12:
await process_frame
func _capture(output_path: String) -> Dictionary:
var image := root.get_texture().get_image()
if image == null or image.is_empty():
push_error("Simulation Garden capture produced no image")
return {}
var analysis := _analyze_image(image)
if analysis.get("range", 0.0) < 0.05:
push_error("Simulation Garden capture is too uniform: %s" % analysis)
return {}
var error := image.save_png(output_path)
if error != OK:
push_error("Could not save %s: %s" % [output_path, error_string(error)])
return {}
return analysis
func _analyze_image(image: Image) -> Dictionary:
var min_luma := INF
var max_luma := -INF
var total_luma := 0.0
var samples := 0
var step_x = maxi(1, image.get_width() / 64)
var step_y = maxi(1, image.get_height() / 36)
for y in range(0, image.get_height(), step_y):
for x in range(0, image.get_width(), step_x):
var color := image.get_pixel(x, y)
var luma := color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722
min_luma = minf(min_luma, luma)
max_luma = maxf(max_luma, luma)
total_luma += luma
samples += 1
return {
"size": "%sx%s" % [image.get_width(), image.get_height()],
"samples": samples,
"average_luma": total_luma / samples,
"range": max_luma - min_luma,
}
func _apply_capture_camera(main_scene: Node) -> void:
var camera_rig := main_scene.get_node("CameraRig")
if camera_rig.has_method("apply_presentation_preset"):
camera_rig.pitch_degrees = -42.0
camera_rig.apply_presentation_preset(Vector3(-2.0, 1.2, -0.5), 128.0, 0.95)
camera_rig.set_physics_process(false)
var camera := main_scene.get_node("CameraRig/Camera3D") as Camera3D
camera.fov = 62.0