feat: introduce identity-backed goat
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
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: StorageStateRecord = manager.get_pantry()
|
||||
var state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
||||
_check(goat != null, "Jajce should load one named goat presentation")
|
||||
_check(
|
||||
AnimalNode.get_all().size() == 1 and ResourceNode.get_all().size() == 18,
|
||||
"The goat should be one AnimalNode 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 caretaker.travel_target_position.is_equal_approx(goat.get_interaction_position())
|
||||
),
|
||||
"Animal target resolution should claim Dunja's loaded interaction point by stable ID"
|
||||
)
|
||||
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.STORAGE_VILLAGE_PANTRY
|
||||
)
|
||||
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 pantry-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 one-food feed operation and event contract"
|
||||
)
|
||||
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)
|
||||
Reference in New Issue
Block a user