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