feat: add player standing and quest generation from village needs
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
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.queue_free()
|
||||
stale_tree.queue_free()
|
||||
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)
|
||||
Reference in New Issue
Block a user