342 lines
13 KiB
GDScript
342 lines
13 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 EMPTY_PANTRY_OUTPUT_PATH := "res://docs/baselines/simulation_garden_02_empty_pantry.png"
|
|
const RESTOCKED_PANTRY_OUTPUT_PATH := "res://docs/baselines/simulation_garden_02_restocked_pantry.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(main_scene)
|
|
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()
|
|
await _stage_wind_gust(main_scene)
|
|
|
|
var cinematic_analysis := _capture(CINEMATIC_OUTPUT_PATH)
|
|
if cinematic_analysis.is_empty():
|
|
quit(1)
|
|
return
|
|
_apply_pantry_capture_camera(main_scene)
|
|
await process_frame
|
|
await process_frame
|
|
var empty_pantry_analysis := _capture(EMPTY_PANTRY_OUTPUT_PATH)
|
|
if empty_pantry_analysis.is_empty() or not _stage_pantry_restock(main_scene):
|
|
quit(1)
|
|
return
|
|
for _frame in 12:
|
|
await process_frame
|
|
var restocked_pantry_analysis := _capture(RESTOCKED_PANTRY_OUTPUT_PATH)
|
|
if restocked_pantry_analysis.is_empty():
|
|
quit(1)
|
|
return
|
|
|
|
print(
|
|
(
|
|
(
|
|
"[TOOL] Saved Simulation Garden captures | debug %s | cinematic %s | "
|
|
+ "empty pantry %s | restocked pantry %s"
|
|
)
|
|
% [
|
|
debug_analysis,
|
|
cinematic_analysis,
|
|
empty_pantry_analysis,
|
|
restocked_pantry_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
|
|
if not _stage_pantry_need(main_scene, contributor, observer):
|
|
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")
|
|
var inspector_label := (
|
|
main_scene.get_node("UI/NpcInspectorPanel/MarginContainer/NpcInspectorLabel") as Label
|
|
)
|
|
if "Open village need" not in inspector_label.text or "Food 0 / 1" not in inspector_label.text:
|
|
push_error("Simulation Garden staging did not expose the selected person's need")
|
|
return false
|
|
return true
|
|
|
|
|
|
func _stage_pantry_need(main_scene: Node, actor: SimNPC, interested: SimNPC) -> bool:
|
|
var simulation_manager := main_scene.get_node("SimulationManager")
|
|
var world_view := main_scene.get_node("WorldViewManager")
|
|
var pantry_site := (
|
|
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
|
|
)
|
|
var actor_visual := world_view.active_npc_visuals.get(actor.id) as Node3D
|
|
var interested_visual := world_view.active_npc_visuals.get(interested.id) as Node3D
|
|
if actor_visual == null or interested_visual == null:
|
|
push_error("Simulation Garden pantry staging needs both active NPC visuals")
|
|
return false
|
|
var pantry_arrival := pantry_site.get_interaction_position()
|
|
actor_visual.global_position = pantry_arrival + Vector3(-0.65, 0.0, 0.15)
|
|
interested_visual.global_position = pantry_arrival + Vector3(0.65, 0.0, 0.15)
|
|
simulation_manager.synchronize_npc_position(actor.id, actor_visual.global_position)
|
|
simulation_manager.synchronize_npc_position(interested.id, interested_visual.global_position)
|
|
for npc in simulation_manager.npcs:
|
|
npc.hunger = minf(npc.hunger, 70.0)
|
|
actor.hunger = 60.0
|
|
interested.hunger = 85.0
|
|
var pantry: StorageStateRecord = simulation_manager.get_pantry()
|
|
pantry.withdraw(
|
|
SimulationIds.RESOURCE_FOOD, maxf(pantry.get_amount(SimulationIds.RESOURCE_FOOD) - 1.0, 0.0)
|
|
)
|
|
simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
|
var withdrawn: float = simulation_manager.economy.withdraw_to_inventory(
|
|
actor, SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
)
|
|
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
|
var village_need_label := (
|
|
main_scene.get_node("UI/VillagePanel/MarginContainer/VillageStatsLabel") as Label
|
|
)
|
|
if (
|
|
withdrawn <= 0.0
|
|
or opportunity == null
|
|
or opportunity.get_interested_npc_id() != interested.id
|
|
or "Village need" not in village_need_label.text
|
|
or "Food 0 / 1" not in village_need_label.text
|
|
):
|
|
push_error("Simulation Garden staging did not expose the real empty-pantry need")
|
|
return false
|
|
var previous_task := interested.current_task
|
|
if not interested.target_id.is_empty():
|
|
simulation_manager.release_npc_reservation(interested.id)
|
|
var selection: ActionSelectionResult = simulation_manager.action_selector.select_action(
|
|
interested,
|
|
simulation_manager.village,
|
|
simulation_manager.clock.time_of_day(),
|
|
simulation_manager.npcs
|
|
)
|
|
if selection == null or selection.action_id != SimulationIds.ACTION_GATHER_FOOD:
|
|
push_error("Simulation Garden staging could not replan the hungry villager")
|
|
return false
|
|
simulation_manager.latest_decisions[interested.id] = selection
|
|
interested.set_task(selection.action_id, selection.duration_override)
|
|
simulation_manager.npc_task_changed.emit(interested, previous_task, selection.action_id)
|
|
simulation_manager.npc_target_requested.emit(interested)
|
|
return true
|
|
|
|
|
|
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 _stage_pantry_restock(main_scene: Node) -> bool:
|
|
var simulation_manager := main_scene.get_node("SimulationManager")
|
|
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
|
if opportunity == null:
|
|
push_error("Simulation Garden restock staging needs an active pantry opportunity")
|
|
return false
|
|
var contributor: SimNPC = simulation_manager.npcs[0]
|
|
var deposited: float = simulation_manager.economy.deposit_inventory(
|
|
contributor, SimulationIds.RESOURCE_FOOD
|
|
)
|
|
var pantry_visual := (
|
|
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry/PantryPresentation")
|
|
as PantryStockVisual
|
|
)
|
|
var world_view := main_scene.get_node("WorldViewManager")
|
|
var interested_visual: Node3D = world_view.active_npc_visuals.get(
|
|
opportunity.get_interested_npc_id()
|
|
)
|
|
if (
|
|
deposited <= 0.0
|
|
or simulation_manager.get_active_opportunity() != null
|
|
or pantry_visual.get_stock_level() != PantryStockVisual.StockLevel.LOW
|
|
or not pantry_visual.get_node("RefillResponse").visible
|
|
or interested_visual.get_node("OpportunityConcernRoot").visible
|
|
):
|
|
push_error("Simulation Garden restock staging did not produce the real visual payoff")
|
|
return false
|
|
return true
|
|
|
|
|
|
func _settle_trust_reaction(main_scene: Node) -> void:
|
|
var simulation_manager := main_scene.get_node("SimulationManager")
|
|
var world_view := main_scene.get_node("WorldViewManager")
|
|
var observer: SimNPC = simulation_manager.npcs[1]
|
|
var observer_visual: Node3D = world_view.active_npc_visuals.get(observer.id)
|
|
var reaction_tween: Tween = observer_visual.get("trust_reaction_tween")
|
|
if reaction_tween != null and reaction_tween.is_running():
|
|
await reaction_tween.finished
|
|
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(-1.5, 1.2, -4.0), 128.0, 0.7)
|
|
camera_rig.set_physics_process(false)
|
|
|
|
var camera := main_scene.get_node("CameraRig/Camera3D") as Camera3D
|
|
camera.fov = 62.0
|
|
|
|
|
|
func _apply_pantry_capture_camera(main_scene: Node) -> void:
|
|
var pantry := (
|
|
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
|
|
)
|
|
var camera_rig := main_scene.get_node("CameraRig")
|
|
var pantry_focus := pantry.global_position + Vector3(0.0, 0.85, 0.72)
|
|
camera_rig.apply_presentation_preset(pantry_focus, 128.0, 0.0)
|
|
camera_rig.look_at(pantry_focus, Vector3.UP)
|
|
var camera := main_scene.get_node("CameraRig/Camera3D") as Camera3D
|
|
camera.fov = 44.0
|