Files
gamedev-the-steward/tests/animal_feeding_vertical_slice_test.gd
T
2026-07-31 00:19:41 +02:00

249 lines
8.4 KiB
GDScript

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)
for _frame in 3:
await physics_frame
var goat := main_scene.get_node("JajceWorld/WorldObjects/Animals/Dunja") as AnimalNode
var player := main_scene.get_node("Player") as Node3D
var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode
var pantry: StorageStateRecord = manager.get_pantry()
var state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
_check(goat != null, "Jajce should load Dunja's named presentation")
_check(
AnimalNode.get_all().size() == 2 and ResourceNode.get_all().size() == 18,
"Named goats should remain AnimalNodes without changing the finite-resource count"
)
_check(
not goat.is_in_group("resource_nodes") and ResourceNode.get_by_id(goat.animal_id) == null,
"Dunja must not masquerade as a ResourceNode",
)
_check(
(
goat.animal_id == SimulationIds.ANIMAL_DUNJA
and goat.display_name == "Dunja"
and goat.species_id == SimulationIds.SPECIES_GOAT
and state != null
and goat.state == state
),
"The loaded presentation should bind the stable named-goat identity"
)
if state == null:
_finish()
return
var authored_position := state.get_position()
_check(
goat.global_position.is_equal_approx(authored_position),
"Animal state should own the loaded presentation position"
)
state.set_position(authored_position + Vector3(0.25, 0.0, 0.0))
_check(
goat.global_position.is_equal_approx(state.get_position()),
"A simulation position change should move the bound goat presentation"
)
state.set_position(authored_position)
for index in range(1, manager.npcs.size()):
_park_npc(manager.npcs[index])
var caretaker: SimNPC = manager.npcs[0]
manager.release_npc_reservation(caretaker.id)
caretaker.inventory.clear()
caretaker.hunger = 20.0
caretaker.energy = 90.0
caretaker.current_task = SimulationIds.ACTION_IDLE
caretaker.task_state = SimNPC.TASK_STATE_IDLE
caretaker.task_complete = true
caretaker.target_id = &""
caretaker.has_travel_target = false
state.set_hunger(72.0)
var pantry_before_npc := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
manager.simulate_tick()
_check(
caretaker.current_task == SimulationIds.ACTION_FEED_ANIMAL,
"An available villager should autonomously choose to feed hungry Dunja"
)
if caretaker.target_id.is_empty():
manager.resolve_npc_target(caretaker.id, caretaker.position)
_check(
(
caretaker.target_id == SimulationIds.ANIMAL_DUNJA
and state.get_reserved_by() == caretaker.id
and pantry_node != null
and caretaker.travel_target_position.is_equal_approx(
pantry_node.get_interaction_position()
)
),
"Animal target resolution should retain Dunja's exact claim while routing to the pantry"
)
manager.notify_npc_arrived(caretaker.id)
_check(
(
is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
and caretaker.target_id == SimulationIds.ANIMAL_DUNJA
and state.get_reserved_by() == caretaker.id
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
and caretaker.travel_target_position.is_equal_approx(goat.get_interaction_position())
),
"Pantry arrival should carry one real food toward the same reserved Dunja",
)
manager.notify_npc_arrived(caretaker.id)
manager.simulate_tick()
var hunger_before_completion := state.get_hunger()
manager.simulate_tick()
_check(
is_equal_approx(
state.get_hunger(),
(
hunger_before_completion
+ AnimalStateRecord.HUNGER_PER_TICK
- AnimalStateRecord.HUNGER_RELIEF
)
),
"NPC feeding should apply the exact simulation-owned hunger relief"
)
_check(
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before_npc - 1.0),
"NPC feeding should withdraw exactly one real pantry food"
)
_check(
state.get_last_fed_tick() == manager.tick_count and state.get_reserved_by() == -1,
"Completed feeding should retain its deterministic tick and release the claim"
)
var npc_feed_event := _latest_feed_event(manager)
_check(
(
npc_feed_event != null
and int(npc_feed_event.data["actor_id"]) == caretaker.id
and (
StringName(npc_feed_event.data["source_id"])
== SimulationIds.npc_inventory_id(caretaker.id)
)
and (StringName(npc_feed_event.data["destination_id"]) == SimulationIds.ANIMAL_DUNJA)
and StringName(npc_feed_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
and is_equal_approx(float(npc_feed_event.data["amount"]), 1.0)
and npc_feed_event.get_world_position().is_equal_approx(state.get_position())
),
"NPC feeding should record one exact carried-inventory-to-animal transfer fact"
)
_check(
(
not goat.get_node("Visual/HungerCueRoot").visible
and goat.get_node("Visual/FedResponseRoot").visible
),
"Real feeding should clear the stable hunger cue and play one cozy response"
)
var npc_feed_json: String = manager.serialize_state()
var npc_feed_checksum: String = manager.get_state_checksum()
var saved_hunger := state.get_hunger()
var saved_fed_tick := state.get_last_fed_tick()
_check(
manager.restore_state_from_json(npc_feed_json),
"NPC-fed animal state should pass the complete schema and restore"
)
state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
pantry = manager.get_pantry()
_check(
(
goat.state == state
and state.get_position().is_equal_approx(authored_position)
and is_equal_approx(state.get_hunger(), saved_hunger)
and state.get_last_fed_tick() == saved_fed_tick
and manager.get_state_checksum() == npc_feed_checksum
),
"Animal position, hunger, feed tick, and checksum should round-trip deterministically"
)
_check(
not goat.get_node("Visual/FedResponseRoot").visible,
"Restore should rebuild stable animal cues without replaying transient praise"
)
state.set_hunger(72.0)
var pantry_before_player := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
player.global_position = goat.get_interaction_position()
player.call("try_interact")
var player_feed_event := _latest_feed_event(manager)
_check(
(
is_equal_approx(
pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before_player - 1.0
)
and is_equal_approx(state.get_hunger(), 72.0 - AnimalStateRecord.HUNGER_RELIEF)
and player_feed_event != null
and int(player_feed_event.data["actor_id"]) == -1
and (StringName(player_feed_event.data["destination_id"]) == SimulationIds.ANIMAL_DUNJA)
),
"The player should use the same exact one-food animal mutation"
)
var player_feed_json: String = manager.serialize_state()
_check(
manager.restore_state_from_json(player_feed_json),
"Player-fed animal history should restore through the same schema"
)
state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
pantry = manager.get_pantry()
_check(
not goat.get_node("Visual/FedResponseRoot").visible,
"Player feed feedback should also remain transient across restore"
)
state.set_hunger(72.0)
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
var events_before_failed_feed: int = manager.economic_events.size()
player.global_position = goat.get_interaction_position()
player.call("try_interact")
_check(
(
is_equal_approx(state.get_hunger(), 72.0)
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0)
and manager.economic_events.size() == events_before_failed_feed
),
"An empty pantry should block feeding without inventing food or animal relief"
)
_finish()
func _park_npc(npc: SimNPC) -> void:
npc.set_task(SimulationIds.ACTION_WANDER, 1000.0)
npc.start_working()
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] Animal feeding vertical slice passed")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)