extends SceneTree var failures: Array[String] = [] var opened_quest_ids: Array[int] = [] var completed_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_completed.connect(_on_quest_completed) manager.player_quest_expired.connect(_on_quest_expired) var berry := _register_berry(manager, &"quest_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 actor.hunger = 60.0 interested.hunger = 90.0 manager.economy.withdraw_to_inventory(actor, SimulationIds.RESOURCE_FOOD, 1.0) _prepare_shared_patrol(actor, interested, Vector3(20.0, 0.0, 0.0)) _check( manager.try_communicate_at_shared_activity(actor.id, interested.id), "The quest proof should open the same known pantry shortage" ) _check(manager.get_active_opportunity() != null, "The quest proof needs an active opportunity") var quest: PlayerQuestRecord = manager.get_active_player_quest() _check( ( quest != null and quest.get_requester_npc_id() == interested.id and quest.get_quest_type() == SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY and quest.get_resource_id() == SimulationIds.RESOURCE_FOOD and quest.get_target_id() == SimulationIds.STORAGE_VILLAGE_PANTRY and is_equal_approx(quest.get_target_amount(), 1.0) and quest.get_standing_reward() > 0.0 and quest.is_open() and opened_quest_ids == [quest.get_quest_id()] ), "A player-actionable shortage should generate one quest for its worried villager", ) if quest == null: manager.free() _finish() return var no_helper_route: OpportunityPlayerResponseResult = ( manager.get_active_opportunity_player_response() ) var standing_before: PlayerStandingRecord = manager.get_player_standing() _check( ( no_helper_route != null and is_equal_approx(standing_before.get_standing(), 0.0) and standing_before.get_resolved_needs() == 0 and is_equal_approx(standing_before.get_gratitude(interested.id), 0.0) ), "Standing should start at zero with no gratitude before any player help", ) var checksum_before_resolve: String = manager.get_state_checksum() var serialized_before: String = manager.serialize_state() var berry_harvested: float = manager.harvest_resource_node(berry) _check( is_equal_approx(berry_harvested, 1.0), "Completing the quest should go through the ordinary player harvest command" ) var standing_after: PlayerStandingRecord = manager.get_player_standing() var completed: PlayerQuestRecord = manager.get_active_player_quest() var latest: PlayerQuestRecord = manager.get_latest_player_quest_for_requester(interested.id) _check( ( completed == null and latest != null and latest.get_status() == PlayerQuestRecord.STATUS_COMPLETED and completed_quest_ids == [latest.get_quest_id()] and is_equal_approx(standing_after.get_standing(), quest.get_standing_reward()) and standing_after.get_resolved_needs() == 1 and is_equal_approx(standing_after.get_gratitude(interested.id), 0.1) and manager.get_state_checksum() != checksum_before_resolve ), "A real player supply should complete the quest, grant standing, and raise gratitude", ) var saved_json: String = serialized_before var pre_completion_state: String = manager.serialize_state() var restored := _create_manager(902) _check( restored.restore_state_from_json(pre_completion_state), "The completed quest and standing should restore through the current schema" ) _check( ( restored.get_state_checksum() == manager.get_state_checksum() and restored.get_active_player_quest() == null and restored.get_latest_player_quest_for_requester(interested.id) != null and is_equal_approx( restored.get_player_standing().get_standing(), quest.get_standing_reward() ) and is_equal_approx(restored.get_player_standing().get_gratitude(interested.id), 0.1) ), "Restore should re-derive completed quest and standing without reopening the need", ) restored.free() var invalidated := _create_manager(905) invalidated.player_quest_expired.connect(_on_quest_expired) var stale_tree := _register_tree(invalidated, &"quest_stale_tree") var stale_setup := _open_missing_wood_need(invalidated) _check( invalidated.get_active_player_quest() != null, "The invalidation branch needs an open quest" ) var stale_quest: PlayerQuestRecord = invalidated.get_active_player_quest() var stale_interested_id: int = stale_setup["interested"].id var review_interval: int = invalidated.get_knowledge_review_interval() invalidated.tick_count = stale_quest.get_created_tick() + review_interval invalidated.call("_maintain_event_knowledge", true) invalidated.simulate_tick() _check( ( expired_quest_ids.has(stale_quest.get_quest_id()) and invalidated.get_active_player_quest() == null and is_equal_approx( invalidated.get_player_standing().get_gratitude(stale_interested_id), 0.0 ) ), "A stale need should expire its quest without granting standing", ) berry.free() stale_tree.free() var animal_manager := _create_manager(906) animal_manager.player_quest_opened.connect(_on_quest_opened) animal_manager.player_quest_completed.connect(_on_quest_completed) var caretaker: SimNPC = animal_manager.npcs[0] var pantry: StorageStateRecord = animal_manager.get_pantry() pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD)) pantry.deposit(SimulationIds.RESOURCE_FOOD, 5.0) animal_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD) caretaker.position = Vector3.ZERO var goat := AnimalNode.new() goat.name = "Dunja" goat.animal_id = SimulationIds.ANIMAL_DUNJA goat.display_name = "Dunja" goat.species_id = SimulationIds.SPECIES_GOAT goat.initial_enabled = true goat.can_npcs_feed = true goat.can_player_feed = true goat.initial_hunger = 0.0 root.add_child(goat) _check( animal_manager.animal_care.register_node(goat), "The animal-care quest branch should register a real named goat" ) var animal_state: AnimalStateRecord = animal_manager.animal_care.get_state( SimulationIds.ANIMAL_DUNJA ) _check(animal_state != null, "The animal-care quest branch needs Dunja's state") if animal_state == null: _finish() return animal_state.set_hunger(animal_state.FEED_THRESHOLD + 1.0) animal_manager.simulate_tick() var animal_quest: PlayerQuestRecord = animal_manager.get_active_player_quest() _check( ( animal_quest != null and animal_quest.has_animal_target() and animal_quest.get_animal_id() == SimulationIds.ANIMAL_DUNJA and animal_quest.get_quest_type() == SimulationIds.OPPORTUNITY_FEED_HUNGRY_ANIMAL and animal_quest.get_requester_npc_id() == caretaker.id ), "A hungry, player-feedable goat should generate an animal-care quest for a nearby villager", ) var standing_before_animal: float = animal_manager.get_player_standing().get_standing() _check( animal_manager.feed_animal(SimulationIds.ANIMAL_DUNJA), "The player should resolve the animal quest through the ordinary feed command" ) var animal_latest: PlayerQuestRecord = animal_manager.get_latest_player_quest_for_requester( caretaker.id ) _check( ( completed_quest_ids.has(animal_quest.get_quest_id()) and animal_latest != null and animal_latest.get_status() == PlayerQuestRecord.STATUS_COMPLETED and is_equal_approx( animal_manager.get_player_standing().get_standing(), standing_before_animal + animal_quest.get_standing_reward() ) ), "Feeding the goat should complete its quest and grant standing", ) var animal_saved: String = animal_manager.serialize_state() var animal_restored := _create_manager(907) _check( animal_restored.restore_state_from_json(animal_saved), "The completed animal-care quest should restore through the current schema" ) _check( ( animal_restored.get_state_checksum() == animal_manager.get_state_checksum() and animal_restored.get_active_player_quest() == null ), "Restored animal-care state should preserve checksum and closed quest", ) animal_restored.free() goat.free() animal_manager.free() _finish() manager.free() invalidated.free() _finish() func _register_berry(manager: Node, node_id: StringName) -> ResourceNode: var berry := ResourceNode.new() berry.name = "QuestBerryBush" berry.node_id = node_id berry.action_id = SimulationIds.ACTION_GATHER_FOOD berry.resource_id = SimulationIds.RESOURCE_FOOD berry.initial_amount = 1.0 berry.yield_per_action = 1.0 berry.debug_label_enabled = false var interaction_point := Marker3D.new() interaction_point.name = "InteractionPoint" berry.add_child(interaction_point) root.add_child(berry) _check( manager.register_resource_node(berry), "The quest proof should bind a real finite food ResourceNode" ) return berry func _register_tree(manager: Node, node_id: StringName) -> ResourceNode: var tree := ResourceNode.new() tree.name = "QuestTree" tree.node_id = node_id tree.action_id = SimulationIds.ACTION_GATHER_WOOD tree.resource_id = SimulationIds.RESOURCE_WOOD tree.initial_amount = 1.0 tree.yield_per_action = 1.0 tree.debug_label_enabled = false var interaction_point := Marker3D.new() interaction_point.name = "InteractionPoint" tree.add_child(interaction_point) root.add_child(tree) _check( manager.register_resource_node(tree), "The invalidation proof should bind a real finite wood ResourceNode" ) return tree func _open_missing_wood_need(manager: Node) -> Dictionary: var actor: SimNPC = manager.npcs[0] var witness: SimNPC = manager.npcs[1] actor.position = Vector3.ZERO witness.position = Vector3(4.0, 0.0, 0.0) var woodpile: StorageStateRecord = manager.get_woodpile() woodpile.withdraw(SimulationIds.RESOURCE_WOOD, woodpile.get_amount(SimulationIds.RESOURCE_WOOD)) woodpile.deposit(SimulationIds.RESOURCE_WOOD, 0.5) manager.economy.sync_resource(SimulationIds.RESOURCE_WOOD) actor.set_task(SimulationIds.ACTION_PATROL, 1.0) actor.start_working() manager.simulate_tick() var opportunity: OpportunityStateRecord = manager.get_active_opportunity() _check( opportunity != null, "The invalidation branch needs the performer's known blocked-work need" ) return {"actor": actor, "interested": actor} 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 _open_via_communication(manager: Node) -> Dictionary: 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 actor.hunger = 60.0 interested.hunger = 90.0 manager.economy.withdraw_to_inventory(actor, SimulationIds.RESOURCE_FOOD, 1.0) _prepare_shared_patrol(actor, interested, Vector3(20.0, 0.0, 0.0)) _check( manager.try_communicate_at_shared_activity(actor.id, interested.id), "The invalidation branch should open from the same one-hop known shortage" ) _check( manager.get_active_opportunity() != null, "The invalidation branch needs an active opportunity" ) return {"actor": actor, "interested": interested} func _prepare_shared_patrol(first: SimNPC, second: SimNPC, position: Vector3) -> void: for npc in [first, second]: npc.set_task(SimulationIds.ACTION_PATROL) npc.target_id = &"guard_post" npc.start_working() first.position = position second.position = position + Vector3(1.0, 0.0, 0.0) 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_completed(quest: PlayerQuestRecord) -> void: completed_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 quest generation and standing passed") quit(0) return for failure in failures: push_error("[TEST] " + failure) quit(1)