249 lines
8.4 KiB
GDScript
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)
|