extends SceneTree var failures: Array[String] = [] func _initialize() -> void: call_deferred("_run") func _run() -> void: var main_scene: Node = load("res://main.tscn").instantiate() root.add_child(main_scene) await process_frame await physics_frame await physics_frame var manager: Node = main_scene.get_node("SimulationManager") var view: Node = main_scene.get_node("WorldViewManager") var ui: CanvasLayer = main_scene.get_node("UI") manager.set_process(false) var npc: SimNPC = manager.npcs[0] var visual: Node3D = view.active_npc_visuals[npc.id] visual.set_physics_process(false) var profession_definition := SimulationDefinitions.get_profession(npc.profession) var body: MeshInstance3D = visual.get_node("MeshInstance3D") _check( body.material_override.albedo_color == profession_definition.visual_color, "NPC body color should come from its profession definition" ) _check( visual.get_node("ProfessionProp").mesh != null and profession_definition.display_name in visual.get_node("ProfessionLabel").text, "NPC visual should expose a profession prop and readable label" ) var test_decision := ActionSelectionResult.new( SimulationIds.ACTION_GATHER_FOOD, -1.0, "Test shortage reason", {SimulationIds.ACTION_GATHER_FOOD: 4.5} ) manager.latest_decisions[npc.id] = test_decision manager.emit_signal("npc_decision_recorded", npc, test_decision) var inspector_label: Label = ui.get_node( "NpcInspectorPanel/MarginContainer/NpcInspectorLabel" ) _check( "Test shortage reason" in inspector_label.text and npc.npc_name in inspector_label.text, "NPC inspector should show the selected villager's real decision reason" ) var carried_food: MeshInstance3D = visual.get_node("CarriedFood") _check(not carried_food.visible, "NPC should begin without a carried-food prop") npc.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0) manager.emit_signal("npc_inventory_changed", npc, SimulationIds.RESOURCE_FOOD, 1.0) _check(carried_food.visible, "Carried food should be visible on the NPC") npc.remove_inventory(SimulationIds.RESOURCE_FOOD, 1.0) manager.emit_signal("npc_inventory_changed", npc, SimulationIds.RESOURCE_FOOD, 0.0) _check(not carried_food.visible, "Deposited or eaten food should be hidden") var resource_state: ResourceStateRecord = manager.get_resource_state(&"berry_bush_01") npc.set_task(SimulationIds.ACTION_GATHER_FOOD) npc.target_id = resource_state.get_node_id() npc.travel_target_position = Vector3(-6.0, 0.0, -8.0) npc.has_travel_target = true resource_state.reserve(npc.id) manager.request_current_travel(npc.id) var saved_action := npc.current_task var saved_target_id := npc.target_id var saved_target_position := npc.travel_target_position var authoritative_position := Vector3(-2.5, 0.5, 1.75) visual.global_position = authoritative_position _check(view.despawn_npc_visual(npc.id), "Visual should unload") await process_frame _check( npc.position.is_equal_approx(authoritative_position), "Unload should synchronize the last active position" ) _check( ( npc.current_task == saved_action and npc.target_id == saved_target_id and npc.travel_target_position.is_equal_approx(saved_target_position) ), "Unload should preserve action and target state" ) _check( resource_state.get_reserved_by() == npc.id, "Unload should preserve the resource reservation" ) var unloaded_checksum: String = manager.get_state_checksum() _check(view.reload_npc_visual(npc.id), "Visual should reload") var reloaded: Node3D = view.active_npc_visuals[npc.id] reloaded.set_physics_process(false) _check( reloaded.global_position.is_equal_approx(authoritative_position), "Reloaded visual should use authoritative position" ) var resumed_target: Vector3 = reloaded.get("current_target") _check( resumed_target.is_equal_approx(saved_target_position), "Reloaded visual should resume the persisted travel target" ) _check( manager.get_state_checksum() == unloaded_checksum, "Loading a visual must not mutate simulation state" ) _check(resource_state.get_reserved_by() == npc.id, "Reload should preserve the reservation") var moved_position := authoritative_position + Vector3(0.5, 0.0, 0.25) reloaded.emit_signal("position_changed", npc.id, moved_position) _check( npc.position.is_equal_approx(moved_position), "Active position callback should synchronize simulation state" ) var saved_state: String = manager.serialize_state() var visual_before_restore: Node3D = view.active_npc_visuals[npc.id] npc.hunger = 99.0 _check( manager.restore_state_from_json(saved_state), "Visual lifecycle scenario should restore valid state" ) var visual_after_restore: Node3D = view.active_npc_visuals[npc.id] _check( visual_after_restore != visual_before_restore, "State restoration should rebuild active NPC presentation" ) _check( visual_after_restore.global_position.is_equal_approx(npc.position), "Rebuilt presentation should use the restored authoritative position" ) if failures.is_empty(): print("[TEST] NPC visual lifecycle passed") quit(0) return for failure in failures: push_error("[TEST] " + failure) quit(1) func _check(condition: bool, message: String) -> void: if not condition: failures.append(message)