extends SceneTree var failures: Array[String] = [] var opened_quest_ids: Array[int] = [] var expired_quest_ids: Array[int] = [] func _initialize() -> void: call_deferred("_run") func _run() -> void: var manager := _create_manager() manager.player_quest_opened.connect(_on_quest_opened) manager.player_quest_expired.connect(_on_quest_expired) var berry := _register_resource(manager, &"talk_berry_bush") var actor: SimNPC = manager.npcs[0] var interested: SimNPC = manager.npcs[2] _set_pantry_amount(manager, 1.0) for npc in manager.npcs: npc.position = Vector3(40.0 + npc.id * 10.0, 0.0, 0.0) actor.position = Vector3.ZERO interested.position = Vector3(4.0, 0.0, 0.0) actor.hunger = 60.0 interested.hunger = 90.0 manager.economy.withdraw_to_inventory(actor, SimulationIds.RESOURCE_FOOD, 1.0) var opportunity: OpportunityStateRecord = manager.get_active_opportunity() _check( opportunity != null and opportunity.get_interested_npc_id() == interested.id, "The talk proof needs an open need owned by the inspected villager", ) if opportunity == null: berry.free() manager.free() _finish() return var talk: PlayerTalkResult = manager.get_villager_talk(interested.id) _check( ( talk != null and talk.npc_id == interested.id and talk.npc_name == interested.npc_name and talk.has_need and talk.can_accept and interested.npc_name in talk.greeting ), "Talking should greet the villager and surface their real need", ) var checksum_before: String = manager.get_state_checksum() var accepted: PlayerQuestRecord = manager.accept_villager_request(interested.id) print( " debug accepted=", accepted, " opened=", opened_quest_ids, " active=", manager.get_active_player_quest(), " checksum_same=", manager.get_state_checksum() == checksum_before, " resolved=", manager.get_player_standing().get_resolved_needs(), ) _check( ( accepted != null and accepted.is_open() and accepted.get_requester_npc_id() == interested.id and opened_quest_ids == [accepted.get_quest_id()] and manager.get_state_checksum() == checksum_before and manager.get_player_standing().get_resolved_needs() == 0 ), "Accepting a request should surface the same open quest without granting standing yet", ) var accepted_again: PlayerQuestRecord = manager.accept_villager_request(interested.id) _check( accepted_again != null and accepted_again.get_quest_id() == accepted.get_quest_id(), "Re-accepting should not duplicate the open quest", ) var carry_state: PlayerStateRecord = manager.get_player_state() carry_state.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0) var deposited: float = manager.player_deposit(SimulationIds.RESOURCE_FOOD) _check( ( is_equal_approx(deposited, 1.0) and manager.get_active_player_quest() == null and manager.get_player_standing().get_resolved_needs() == 1 ), "Completing the accepted request through a real deposit should resolve it", ) var declined := _create_manager(904) declined.player_quest_opened.connect(_on_quest_opened) declined.player_quest_expired.connect(_on_quest_expired) var berry2 := _register_resource(declined, &"talk_berry_bush_2") var actor2: SimNPC = declined.npcs[0] var interested2: SimNPC = declined.npcs[2] _set_pantry_amount(declined, 1.0) for npc in declined.npcs: npc.position = Vector3(40.0 + npc.id * 10.0, 0.0, 0.0) actor2.position = Vector3.ZERO interested2.position = Vector3(4.0, 0.0, 0.0) actor2.hunger = 60.0 interested2.hunger = 90.0 declined.economy.withdraw_to_inventory(actor2, SimulationIds.RESOURCE_FOOD, 1.0) var declined_quest: PlayerQuestRecord = declined.get_active_player_quest() _check(declined_quest != null, "The decline branch needs an open quest") if declined_quest == null: berry.free() berry2.free() manager.free() declined.free() _finish() return var gratitude_before: float = declined.get_player_standing().get_gratitude(interested2.id) var refused: PlayerQuestRecord = declined.decline_villager_request(interested2.id) _check( ( refused != null and refused.get_quest_id() == declined_quest.get_quest_id() and expired_quest_ids.has(declined_quest.get_quest_id()) and declined.get_active_player_quest() == null and declined.get_player_standing().get_gratitude(interested2.id) <= gratitude_before ), "Declining a personal request should close the quest without granting gratitude", ) berry.free() berry2.free() manager.free() declined.free() _finish() func _register_resource(manager: Node, node_id: StringName) -> ResourceNode: var node := ResourceNode.new() node.name = "TalkResource" node.node_id = node_id node.action_id = SimulationIds.ACTION_GATHER_FOOD node.resource_id = SimulationIds.RESOURCE_FOOD node.initial_amount = 2.0 node.yield_per_action = 2.0 node.debug_label_enabled = false var interaction_point := Marker3D.new() interaction_point.name = "InteractionPoint" node.add_child(interaction_point) root.add_child(node) _check( manager.register_resource_node(node), "The talk proof should bind a real finite food ResourceNode" ) return node func _create_manager(seed_value: int = 901) -> Node: var manager: Node = load("res://simulation/SimulationManager.gd").new() manager.simulation_seed = seed_value manager.debug_logs = false var home_positions: Array[Vector3] = [ Vector3(0.0, 0.0, 0.0), Vector3(2.0, 0.0, 0.0), Vector3(10.0, 0.0, 0.0), Vector3(12.0, 0.0, 0.0), Vector3(30.0, 0.0, 30.0), Vector3(40.0, 0.0, 40.0), ] manager.home_positions = home_positions root.add_child(manager) manager.set_process(false) return manager func _set_pantry_amount(manager: Node, amount: float) -> void: var pantry: StorageStateRecord = manager.get_pantry() pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD)) pantry.deposit(SimulationIds.RESOURCE_FOOD, amount) manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD) func _on_quest_opened(quest: PlayerQuestRecord) -> void: opened_quest_ids.append(quest.get_quest_id()) func _on_quest_expired(quest: PlayerQuestRecord) -> void: expired_quest_ids.append(quest.get_quest_id()) func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) func _finish() -> void: if failures.is_empty(): print("[TEST] Player talk and personal negotiation passed") quit(0) return for failure in failures: push_error("[TEST] " + failure) quit(1)