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 var manager: Node = main_scene.get_node("SimulationManager") manager.set_process(false) var player := main_scene.get_node("Player") player.set_physics_process(false) for _frame in 3: await physics_frame var dunja_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA) var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA) var dunja_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA) var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA) var hud := main_scene.get_node_or_null("PlayerInteractionLayer/PlayerInteractionHud") _check( ( dunja_node != null and zora_node != null and dunja_state != null and zora_state != null and hud != null ), "The real main scene should expose both named goats and the player interaction HUD", ) if ( dunja_node == null or zora_node == null or dunja_state == null or zora_state == null or hud == null ): _finish() return dunja_node.set_physics_process(false) zora_node.set_physics_process(false) dunja_state.set_hunger(72.0) zora_state.set_hunger(72.0) var dunja_interaction_position := dunja_node.get_interaction_position() var zora_interaction_position := zora_node.get_interaction_position() var tie_position := (dunja_interaction_position + zora_interaction_position) * 0.5 var pair_range := dunja_interaction_position.distance_to(zora_interaction_position) + 0.5 player.set("interaction_range", pair_range) player.global_position = zora_interaction_position var nearest_context := player.call("get_interaction_context") as PlayerInteractionResult _check( ( nearest_context != null and nearest_context.kind == PlayerInteractionResult.KIND_ANIMAL and nearest_context.action_id == SimulationIds.ACTION_FEED_ANIMAL and nearest_context.target_id == SimulationIds.ANIMAL_ZORA and nearest_context.display_name == "Zora" and nearest_context.target_node == zora_node and nearest_context.is_available() and "Zora" in nearest_context.prompt_text ), "With both hungry goats in range, the context should name exact nearest goat Zora", ) hud.call("refresh_prompt") var action_label := hud.get_node("Copy/Action") as Label var detail_label := hud.get_node("Copy/Detail") as Label _check( nearest_context != null and action_label != null and detail_label != null, "The prompt proof needs a resolved context and both public HUD labels", ) if nearest_context == null or action_label == null or detail_label == null: _finish() return _check( ( nearest_context.prompt_text in action_label.text and detail_label.text == nearest_context.detail_text ), "The HUD prompt should render the same exact context that execution will use", ) player.global_position = tie_position var tie_context := player.call("get_interaction_context") as PlayerInteractionResult _check( ( tie_context != null and tie_position.distance_to(dunja_interaction_position) <= pair_range and tie_position.distance_to(zora_interaction_position) <= pair_range and tie_context.target_id == SimulationIds.ANIMAL_DUNJA and tie_context.display_name == "Dunja" and tie_context.target_node == dunja_node ), "An equal-distance pair should resolve by stable animal ID, choosing Dunja", ) player.global_position = zora_interaction_position var execution_context := player.call("get_interaction_context") as PlayerInteractionResult var pantry: StorageStateRecord = manager.get_pantry() var pantry_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD) var dunja_hunger_before := dunja_state.get_hunger() var zora_hunger_before := zora_state.get_hunger() var events_before: int = manager.economic_events.size() player.call("try_interact") var feed_event := _latest_feed_event(manager) _check( ( execution_context != null and execution_context.target_id == SimulationIds.ANIMAL_ZORA and is_equal_approx(dunja_state.get_hunger(), dunja_hunger_before) and is_equal_approx( zora_state.get_hunger(), zora_hunger_before - AnimalStateRecord.HUNGER_RELIEF ) and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before - 1.0) and manager.economic_events.size() == events_before + 1 ), "Interaction should feed the prompted Zora only and consume one real pantry food", ) _check( ( feed_event != null and int(feed_event.data["actor_id"]) == -1 and StringName(feed_event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY and StringName(feed_event.data["destination_id"]) == SimulationIds.ANIMAL_ZORA and StringName(feed_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD and is_equal_approx(float(feed_event.data["amount"]), 1.0) and feed_event.get_world_position().is_equal_approx(zora_state.get_position()) ), "The player feed event should preserve its exact pantry, named target, amount, and position", ) _check( bool(hud.get("feedback_active")) and bool(hud.get("feedback_succeeded")), "A successful exact interaction should surface transient positive HUD feedback", ) dunja_state.set_hunger(72.0) zora_state.set_hunger(72.0) pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD)) manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD) player.set("interaction_range", 100.0) player.global_position = zora_node.get_interaction_position() var blocked_context := player.call("get_interaction_context") as PlayerInteractionResult hud.call("refresh_prompt") _check( ( blocked_context != null and blocked_context.target_id == SimulationIds.ANIMAL_ZORA and blocked_context.display_name == "Zora" and not blocked_context.is_available() and not blocked_context.blocked_reason.is_empty() and "Zora" in (blocked_context.prompt_text + blocked_context.detail_text) and ( "pantry" in (blocked_context.blocked_reason + blocked_context.detail_text).to_lower() ) and blocked_context.prompt_text in action_label.text and detail_label.text == blocked_context.detail_text ), "An empty pantry should show a named blocked Zora prompt before input", ) var blocked_dunja_hunger := dunja_state.get_hunger() var blocked_zora_hunger := zora_state.get_hunger() var blocked_event_count: int = manager.economic_events.size() var blocked_safety: float = manager.village.safety var blocked_knowledge: float = manager.village.knowledge player.call("try_interact") _check( ( is_equal_approx(dunja_state.get_hunger(), blocked_dunja_hunger) and is_equal_approx(zora_state.get_hunger(), blocked_zora_hunger) and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0) and manager.economic_events.size() == blocked_event_count and is_equal_approx(manager.village.safety, blocked_safety) and is_equal_approx(manager.village.knowledge, blocked_knowledge) ), "A blocked animal interaction should consume input without mutation, events, or fallthrough", ) _check( bool(hud.get("feedback_active")) and not bool(hud.get("feedback_succeeded")), "The blocked exact interaction should surface transient negative HUD feedback", ) var blocked_json: String = manager.serialize_state() dunja_state.set_hunger(20.0) zora_state.set_hunger(20.0) pantry.deposit(SimulationIds.RESOURCE_FOOD, 2.0) manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD) _check( manager.restore_state_from_json(blocked_json), "The blocked two-goat context should restore through the complete state schema", ) dunja_state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA) zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA) pantry = manager.get_pantry() var restored_context := player.call("get_interaction_context") as PlayerInteractionResult _check( ( not bool(hud.get("feedback_active")) and restored_context != null and restored_context.target_id == SimulationIds.ANIMAL_ZORA and not restored_context.is_available() and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0) and is_equal_approx(dunja_state.get_hunger(), blocked_dunja_hunger) and is_equal_approx(zora_state.get_hunger(), blocked_zora_hunger) and restored_context.prompt_text in action_label.text and detail_label.text == restored_context.detail_text ), "Restore should clear transient feedback and re-derive the named blocked prompt", ) 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) _check( ( not debug_ui.visible and bool(hud.get("visible")) and action_label.visible and not action_label.text.is_empty() ), "The player interaction HUD should remain visible when development overlays are hidden", ) _finish() func _latest_feed_event(manager: Node) -> EconomicEventRecord: for index in range(manager.economic_events.size() - 1, -1, -1): var event := manager.economic_events[index] as EconomicEventRecord if StringName(event.data["event_type"]) == SimulationIds.EVENT_ANIMAL_FED: return event 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 context interaction passed") quit(0) return for failure in failures: push_error("[TEST] " + failure) quit(1)