extends SceneTree const INSPECTION_REASON := "Inspection proof: pantry stores need a careful hand." const UPDATED_REASON := "Inspection proof: quiet study will help the village." const STUDY_DESK_ID := &"study_desk" 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 for _frame in 3: await physics_frame var manager: Node = main_scene.get_node("SimulationManager") var view: Node = main_scene.get_node("WorldViewManager") var player: CharacterBody3D = main_scene.get_node("Player") var interaction_hud := main_scene.get_node_or_null( "PlayerInteractionLayer/PlayerInteractionHud" ) var field_note := main_scene.get_node_or_null("VillagerInspectionLayer/VillagerFieldNote") manager.set_process(false) player.set_physics_process(false) _freeze_visuals(view) _check( ( manager.npcs.size() >= 2 and view.has_method("find_nearest_active_npc_id") and player.has_method("get_nearby_villager_inspection") and interaction_hud != null and field_note != null and field_note.has_method("refresh_note") ), "The real main scene should expose loaded villager inspection beside the E prompt", ) if ( manager.npcs.size() < 2 or not view.has_method("find_nearest_active_npc_id") or not player.has_method("get_nearby_villager_inspection") or interaction_hud == null or field_note == null or not field_note.has_method("refresh_note") ): _finish() return var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode _check(pantry_node != null, "The inspection proof needs the authored village pantry") if pantry_node == null: _finish() return var first: SimNPC = manager.npcs[0] var second: SimNPC = manager.npcs[1] var lower_id_npc := first if first.id < second.id else second var higher_id_npc := second if lower_id_npc == first else first var player_position: Vector3 = pantry_node.get_interaction_position() player.global_position = player_position player.set("interaction_range", 3.0) _move_population_away(manager, view, player_position) _set_loaded_position(manager, view, lower_id_npc, player_position + Vector3(1.5, 0.0, 0.0)) _set_loaded_position(manager, view, higher_id_npc, player_position + Vector3(-0.75, 0.0, 0.0)) _check( view.find_nearest_active_npc_id(player_position, 3.0) == higher_id_npc.id, "Loaded villager discovery should choose the nearest active presentation", ) _set_loaded_position(manager, view, lower_id_npc, player_position + Vector3(1.0, 0.0, 0.0)) _set_loaded_position(manager, view, higher_id_npc, player_position + Vector3(-1.0, 0.0, 0.0)) _check( view.find_nearest_active_npc_id(player_position, 3.0) == lower_id_npc.id, "Equal-distance loaded villagers should resolve by stable NPC ID", ) _set_loaded_position(manager, view, higher_id_npc, player_position + Vector3(30.0, 0.0, 0.0)) var selected := lower_id_npc _configure_deposit_inspection(manager, selected, pantry_node.get_interaction_position()) _set_loaded_position(manager, view, selected, player_position + Vector3(1.0, 0.0, 0.0)) var pantry_context := player.call("get_interaction_context") as PlayerInteractionResult var inspection := player.call("get_nearby_villager_inspection") as VillagerInspectionResult field_note.call("refresh_note", true) interaction_hud.call("refresh_prompt", true) var labels := _get_note_labels(field_note) var interaction_action := interaction_hud.get_node("Copy/Action") as Label _check( ( pantry_context != null and pantry_context.kind == PlayerInteractionResult.KIND_PANTRY and pantry_context.target_id == SimulationIds.STORAGE_VILLAGE_PANTRY and pantry_context.prompt_text in interaction_action.text and inspection != null and field_note.visible ), "The automatic villager field note should coexist with the existing pantry E action", ) if inspection == null or labels.is_empty(): _finish() return _check_deposit_facts(inspection, selected) _check_rendered_deposit_facts(labels, inspection, selected) var woodpile_node := ( StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_WOODPILE) as StorageNode ) _check(woodpile_node != null, "The inspection proof needs the authored village woodpile") if woodpile_node == null: _finish() return selected.set_task(SimulationIds.ACTION_DEPOSIT_WOOD, 1000.0) selected.target_id = SimulationIds.STORAGE_VILLAGE_WOODPILE selected.travel_target_position = woodpile_node.get_interaction_position() selected.has_travel_target = true selected.inventory.clear() selected.add_inventory(SimulationIds.RESOURCE_WOOD, 2.0) manager.latest_decisions[selected.id] = ( ActionSelectionResult . new( SimulationIds.ACTION_DEPOSIT_WOOD, -1.0, "Inspection proof: carry timber to the authored woodpile.", {SimulationIds.ACTION_DEPOSIT_WOOD: 5.0}, ) ) field_note.call("refresh_note", true) var wood_inspection := player.call("get_nearby_villager_inspection") as VillagerInspectionResult _check( ( wood_inspection != null and wood_inspection.target_id == SimulationIds.STORAGE_VILLAGE_WOODPILE and wood_inspection.target_name == "Village Woodpile" and woodpile_node.display_name == "Village Woodpile" and String(labels["target"].text) == "Target · Village Woodpile" ), "Wood delivery inspection should preserve the exact ID and authored woodpile name", ) selected.set_task(SimulationIds.ACTION_STUDY, 1000.0) selected.target_id = STUDY_DESK_ID selected.travel_target_position = ( ActivitySite.get_by_id(STUDY_DESK_ID).get_interaction_position() ) selected.has_travel_target = true selected.inventory.clear() selected.add_inventory(SimulationIds.RESOURCE_WOOD, 1.0) ( manager . emit_signal( "npc_inventory_changed", selected, SimulationIds.RESOURCE_WOOD, selected.get_inventory_amount(SimulationIds.RESOURCE_WOOD), ) ) field_note.call("refresh_note", true) var stale_reason_inspection := ( player.call("get_nearby_villager_inspection") as VillagerInspectionResult ) _check( ( stale_reason_inspection != null and stale_reason_inspection.action_id == SimulationIds.ACTION_STUDY and stale_reason_inspection.action_name == "Study" and stale_reason_inspection.task_state == SimNPC.TASK_STATE_TRAVELING and stale_reason_inspection.target_id == STUDY_DESK_ID and stale_reason_inspection.target_name == "Study Desk" and is_equal_approx( _carried_amount(stale_reason_inspection, SimulationIds.RESOURCE_FOOD), 0.0 ) and is_equal_approx( _carried_amount(stale_reason_inspection, SimulationIds.RESOURCE_WOOD), 1.0 ) and not stale_reason_inspection.has_decision_reason() and "await" in String(labels["reason"].text).to_lower() ), "Live task, target, and inventory should update while a stale decision reason is withheld", ) manager.latest_decisions[selected.id] = ( ActionSelectionResult . new( SimulationIds.ACTION_STUDY, -1.0, UPDATED_REASON, {SimulationIds.ACTION_STUDY: 4.0}, ) ) field_note.call("refresh_note", true) var updated_inspection := ( player.call("get_nearby_villager_inspection") as VillagerInspectionResult ) _check( ( updated_inspection != null and updated_inspection.has_decision_reason() and updated_inspection.decision_reason == UPDATED_REASON and UPDATED_REASON in labels["reason"].text and "wood" in updated_inspection.carrying_text().to_lower() and "1" in updated_inspection.carrying_text() and "Study" in labels["activity"].text and "Study Desk" in labels["target"].text and "wood" in String(labels["carrying"].text).to_lower() ), "A matching live decision should join the updated authoritative field-note facts", ) var note_position_before_resize: Vector2 = field_note.position root.size += Vector2i(120, 80) await process_frame field_note.call("refresh_note", true) var resize_tween := field_note.get("motion_tween") as Tween if resize_tween != null: await resize_tween.finished _check( ( is_equal_approx(field_note.position.x, note_position_before_resize.x) and is_equal_approx(field_note.position.y, note_position_before_resize.y + 80.0) ), "Field-note motion should preserve its bottom-anchored position after a window resize", ) field_note.set("player", null) field_note.call("refresh_note", true) var cancelled_exit := field_note.get("motion_tween") as Tween field_note.set("player", player) field_note.call("refresh_note", true) var reentry_tween := field_note.get("motion_tween") as Tween if reentry_tween != null: await reentry_tween.finished _check( ( cancelled_exit != null and not cancelled_exit.is_valid() and field_note.visible and not bool(field_note.get("is_hiding")) and player.call("get_nearby_villager_inspection") != null ), "Immediate re-entry should cancel the exit without leaving a suspended hide state", ) var checksum_before_queries: String = manager.get_state_checksum() var state_before_queries: String = manager.serialize_state() var events_before_queries: int = manager.economic_events.size() for _query in 3: view.find_nearest_active_npc_id(player_position, 3.0) player.call("get_nearby_villager_inspection") field_note.call("refresh_note", true) _check( ( manager.get_state_checksum() == checksum_before_queries and manager.serialize_state() == state_before_queries and manager.economic_events.size() == events_before_queries and UPDATED_REASON not in state_before_queries ), "Inspection queries and presentation refreshes must not mutate or serialize transient state", ) var selected_id := selected.id var selected_before_restore := selected var saved_json := state_before_queries _check(view.despawn_npc_visual(selected_id), "The selected villager visual should unload") await process_frame field_note.call("refresh_note", true) var hiding_tween := field_note.get("motion_tween") as Tween if hiding_tween != null: await hiding_tween.finished _check( ( view.find_nearest_active_npc_id(player_position, 3.0) == -1 and player.call("get_nearby_villager_inspection") == null and not field_note.visible and manager.get_state_checksum() == checksum_before_queries ), "Unloading the selected presentation should clear only the ephemeral nearby field note", ) selected.current_task = SimulationIds.ACTION_IDLE selected.task_state = SimNPC.TASK_STATE_IDLE selected.target_id = &"" selected.has_travel_target = false selected.inventory.clear() _check( manager.get_latest_decision(selected_id) != null, "The pre-restore proof should keep a transient decision available to discard", ) _check( manager.restore_state_from_json(saved_json), "The authoritative inspected villager facts should restore through the existing schema", ) await process_frame await physics_frame _freeze_visuals(view) var restored_npc := _find_npc(manager.npcs, selected_id) field_note.call("refresh_note", true) var restored_inspection := ( player.call("get_nearby_villager_inspection") as VillagerInspectionResult ) _check( ( restored_npc != null and restored_npc != selected_before_restore and view.active_npc_visuals.has(selected_id) and restored_inspection != null and restored_inspection.npc_id == selected_id and restored_inspection.npc_name == restored_npc.npc_name and restored_inspection.action_id == SimulationIds.ACTION_STUDY and restored_inspection.action_name == "Study" and restored_inspection.task_state == SimNPC.TASK_STATE_TRAVELING and restored_inspection.target_id == STUDY_DESK_ID and restored_inspection.target_name == "Study Desk" and is_equal_approx( _carried_amount(restored_inspection, SimulationIds.RESOURCE_WOOD), 1.0 ) and manager.get_latest_decision(selected_id) == null and not restored_inspection.has_decision_reason() and restored_inspection.decision_reason == VillagerInspectionResult.AWAITING_REASON and ( String(labels["reason"].text) == "Why · %s" % VillagerInspectionResult.AWAITING_REASON ) ), "Restore should reacquire the loaded villager and derive saved facts without reviving a reason", ) if restored_npc == null or restored_inspection == null: _finish() return var demo_controller := main_scene.get_node("DemoController") var debug_ui := main_scene.get_node("UI") as CanvasLayer demo_controller.call("set_debug_overlay_visible", false) field_note.call("refresh_note", true) interaction_hud.call("refresh_prompt", true) _check( ( not debug_ui.visible and field_note.visible and labels["kicker"].visible and labels["name"].visible and restored_npc.npc_name in labels["name"].text and interaction_hud.visible and pantry_context.prompt_text in interaction_action.text ), "Cinematic mode should preserve both player-facing contexts while hiding debug UI", ) field_note.set("player", null) field_note.call("refresh_note", true) var teardown_tween := field_note.get("motion_tween") as Tween if teardown_tween != null: await teardown_tween.finished _check( not field_note.visible, "A missing player should complete one field-note exit instead of restarting it every frame", ) _finish() func _configure_deposit_inspection(manager: Node, npc: SimNPC, pantry_position: Vector3) -> void: manager.release_npc_reservation(npc.id) npc.set_task(SimulationIds.ACTION_DEPOSIT_FOOD, 1000.0) npc.target_id = SimulationIds.STORAGE_VILLAGE_PANTRY npc.travel_target_position = pantry_position npc.has_travel_target = true npc.inventory.clear() npc.add_inventory(SimulationIds.RESOURCE_FOOD, 2.0) manager.latest_decisions[npc.id] = ( ActionSelectionResult . new( SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, INSPECTION_REASON, {SimulationIds.ACTION_DEPOSIT_FOOD: 5.0}, ) ) ( manager . emit_signal( "npc_inventory_changed", npc, SimulationIds.RESOURCE_FOOD, npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), ) ) func _check_deposit_facts(inspection: VillagerInspectionResult, npc: SimNPC) -> void: _check( ( inspection.npc_id == npc.id and inspection.npc_name == npc.npc_name and inspection.action_id == SimulationIds.ACTION_DEPOSIT_FOOD and inspection.action_name == "Deposit Food" and inspection.task_state == SimNPC.TASK_STATE_TRAVELING and inspection.target_id == SimulationIds.STORAGE_VILLAGE_PANTRY and inspection.target_name == "Village Pantry" and is_equal_approx(_carried_amount(inspection, SimulationIds.RESOURCE_FOOD), 2.0) and inspection.has_decision_reason() and inspection.decision_reason == INSPECTION_REASON and "food" in inspection.carrying_text().to_lower() and "2" in inspection.carrying_text() ), "The typed inspection should expose the selected villager's exact authoritative facts", ) func _check_rendered_deposit_facts( labels: Dictionary, inspection: VillagerInspectionResult, npc: SimNPC ) -> void: _check( ( "nearby" in String(labels["kicker"].text).to_lower() and npc.npc_name in labels["name"].text and inspection.action_name in labels["activity"].text and "Travel" in labels["activity"].text and inspection.target_name in labels["target"].text and "food" in String(labels["carrying"].text).to_lower() and "2" in labels["carrying"].text and inspection.decision_reason in labels["reason"].text ), "The restrained field note should render name, activity, target, carrying, and reason", ) func _move_population_away(manager: Node, view: Node, origin: Vector3) -> void: manager.latest_decisions.clear() for index in manager.npcs.size(): var npc: SimNPC = manager.npcs[index] _set_loaded_position( manager, view, npc, origin + Vector3(40.0 + float(index) * 2.0, 0.0, 20.0), ) func _set_loaded_position(manager: Node, view: Node, npc: SimNPC, position: Vector3) -> void: manager.synchronize_npc_position(npc.id, position) var visual := view.active_npc_visuals.get(npc.id) as Node3D if visual != null: visual.set_physics_process(false) if visual.has_method("stop_travel"): visual.stop_travel() visual.global_position = position func _freeze_visuals(view: Node) -> void: for visual in view.active_npc_visuals.values(): visual.set_physics_process(false) if visual.has_method("stop_travel"): visual.stop_travel() func _get_note_labels(field_note: Node) -> Dictionary: var paths := { "kicker": "Copy/Kicker", "name": "Copy/Name", "activity": "Copy/Activity", "target": "Copy/Target", "carrying": "Copy/Carrying", "reason": "Copy/Reason", } var labels := {} for key in paths: var label := field_note.get_node_or_null(paths[key]) as Label _check(label != null, "Villager field note should expose %s" % paths[key]) if label == null: return {} labels[key] = label return labels func _carried_amount(inspection: VillagerInspectionResult, item_id: StringName) -> float: if inspection.carried_amounts.has(item_id): return float(inspection.carried_amounts[item_id]) return float(inspection.carried_amounts.get(String(item_id), 0.0)) func _find_npc(npcs: Array[SimNPC], npc_id: int) -> SimNPC: for npc in npcs: if npc.id == npc_id: return npc return null func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) func _finish() -> void: if failures.is_empty(): print("[TEST] Player villager inspection passed") quit(0) return for failure in failures: push_error("[TEST] " + failure) quit(1)