feat: add paired goat care
This commit is contained in:
@@ -20,10 +20,10 @@ func _run() -> void:
|
||||
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(goat != null, "Jajce should load Dunja's named 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"
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
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 dunja_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA)
|
||||
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
|
||||
var dunja_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
||||
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
||||
var shared_pasture := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_DUNJA_PASTURE)
|
||||
var dunja_shelter := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_DUNJA_SHELTER)
|
||||
var zora_shelter := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_ZORA_SHELTER)
|
||||
_check(
|
||||
(
|
||||
AnimalNode.get_all().size() == 2
|
||||
and manager.animal_care.states.size() == 2
|
||||
and dunja_node != null
|
||||
and zora_node != null
|
||||
and dunja_state != null
|
||||
and zora_state != null
|
||||
and dunja_node.state == dunja_state
|
||||
and zora_node.state == zora_state
|
||||
),
|
||||
"The real main scene should bind exactly two stable named-goat records",
|
||||
)
|
||||
_check(
|
||||
shared_pasture != null and dunja_shelter != null and zora_shelter != null,
|
||||
"The pair proof needs one shared pasture and one authored shelter per goat",
|
||||
)
|
||||
if (
|
||||
dunja_node == null
|
||||
or zora_node == null
|
||||
or dunja_state == null
|
||||
or zora_state == null
|
||||
or shared_pasture == null
|
||||
or dunja_shelter == null
|
||||
or zora_shelter == null
|
||||
):
|
||||
_finish()
|
||||
return
|
||||
|
||||
for npc in manager.npcs:
|
||||
_park_npc(npc)
|
||||
|
||||
var adapter := manager.active_world_adapter as ActiveWorldAdapter
|
||||
var dunja_sites: Array[Dictionary] = adapter.get_animal_routine_sites(
|
||||
SimulationIds.SPECIES_GOAT, SimulationIds.ANIMAL_DUNJA
|
||||
)
|
||||
var zora_sites: Array[Dictionary] = adapter.get_animal_routine_sites(
|
||||
SimulationIds.SPECIES_GOAT, SimulationIds.ANIMAL_ZORA
|
||||
)
|
||||
_check(
|
||||
(
|
||||
(
|
||||
_site_ids(dunja_sites)
|
||||
== [
|
||||
SimulationIds.ANIMAL_SITE_DUNJA_PASTURE,
|
||||
SimulationIds.ANIMAL_SITE_DUNJA_SHELTER,
|
||||
]
|
||||
)
|
||||
and (
|
||||
_site_ids(zora_sites)
|
||||
== [
|
||||
SimulationIds.ANIMAL_SITE_DUNJA_PASTURE,
|
||||
SimulationIds.ANIMAL_SITE_ZORA_SHELTER,
|
||||
]
|
||||
)
|
||||
),
|
||||
"Routine discovery should expose the shared pasture plus only each goat's own shelter",
|
||||
)
|
||||
|
||||
zora_state.set_hunger(30.0)
|
||||
_check(
|
||||
zora_state.schedule_routine_at(manager.tick_count),
|
||||
"Zora's routine should be schedulable immediately from her authored shelter",
|
||||
)
|
||||
manager.simulate_tick()
|
||||
_check(
|
||||
(
|
||||
zora_state.has_active_travel_target()
|
||||
and zora_state.get_routine_site_id() == SimulationIds.ANIMAL_SITE_ZORA_SHELTER
|
||||
and (zora_state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE)
|
||||
and (zora_state.get_travel_target_position().is_equal_approx(
|
||||
shared_pasture.get_routine_position()
|
||||
))
|
||||
),
|
||||
"Zora should deterministically choose the shared pasture from her own shelter",
|
||||
)
|
||||
_check(
|
||||
zora_state.complete_travel(shared_pasture.get_routine_position(), manager.tick_count),
|
||||
"The routine proof should be able to complete Zora's pasture leg directly",
|
||||
)
|
||||
_check(
|
||||
zora_state.schedule_routine_at(manager.tick_count),
|
||||
"Zora's return leg should be schedulable without waiting for long navigation",
|
||||
)
|
||||
manager.simulate_tick()
|
||||
_check(
|
||||
(
|
||||
zora_state.has_active_travel_target()
|
||||
and (zora_state.get_routine_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE)
|
||||
and (zora_state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_ZORA_SHELTER)
|
||||
and (zora_state.get_travel_target_position().is_equal_approx(
|
||||
zora_shelter.get_routine_position()
|
||||
))
|
||||
),
|
||||
"Zora should return to her own shelter rather than entering Dunja's shelter",
|
||||
)
|
||||
zora_state.cancel_travel(manager.tick_count + AnimalCareSystem.ROUTINE_RETRY_TICKS)
|
||||
|
||||
dunja_state.set_hunger(72.0)
|
||||
zora_state.set_hunger(72.0)
|
||||
var zora_caretaker: SimNPC = manager.npcs[0]
|
||||
var dunja_caretaker: SimNPC = manager.npcs[1]
|
||||
_prepare_feed_task(manager, zora_caretaker)
|
||||
_prepare_feed_task(manager, dunja_caretaker)
|
||||
var shared_origin := zora_node.get_interaction_position()
|
||||
zora_caretaker.position = shared_origin
|
||||
dunja_caretaker.position = shared_origin
|
||||
_check(
|
||||
manager.resolve_npc_target(zora_caretaker.id, shared_origin),
|
||||
"The first caretaker should resolve a real hungry animal target",
|
||||
)
|
||||
_check(
|
||||
(
|
||||
zora_caretaker.target_id == SimulationIds.ANIMAL_ZORA
|
||||
and zora_state.get_reserved_by() == zora_caretaker.id
|
||||
and dunja_state.get_reserved_by() == -1
|
||||
),
|
||||
"The first caretaker should claim the nearest hungry goat, Zora, by exact ID",
|
||||
)
|
||||
_check(
|
||||
manager.resolve_npc_target(dunja_caretaker.id, shared_origin),
|
||||
"The second caretaker should resolve the remaining available hungry goat",
|
||||
)
|
||||
_check(
|
||||
(
|
||||
dunja_caretaker.target_id == SimulationIds.ANIMAL_DUNJA
|
||||
and dunja_state.get_reserved_by() == dunja_caretaker.id
|
||||
and zora_state.get_reserved_by() == zora_caretaker.id
|
||||
),
|
||||
"Simultaneous hunger should retain two independent exact animal reservations",
|
||||
)
|
||||
|
||||
var claimed_json: String = manager.serialize_state()
|
||||
var claimed_checksum: String = manager.get_state_checksum()
|
||||
var zora_caretaker_id := zora_caretaker.id
|
||||
var dunja_caretaker_id := dunja_caretaker.id
|
||||
_check(
|
||||
manager.restore_state_from_json(claimed_json),
|
||||
"A save containing both exact feed claims should pass the complete schema",
|
||||
)
|
||||
dunja_state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
||||
zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
||||
zora_caretaker = _find_npc(manager.npcs, zora_caretaker_id)
|
||||
dunja_caretaker = _find_npc(manager.npcs, dunja_caretaker_id)
|
||||
_check(
|
||||
(
|
||||
manager.get_state_checksum() == claimed_checksum
|
||||
and zora_node.state == zora_state
|
||||
and dunja_node.state == dunja_state
|
||||
and zora_caretaker != null
|
||||
and dunja_caretaker != null
|
||||
and zora_caretaker.target_id == SimulationIds.ANIMAL_ZORA
|
||||
and dunja_caretaker.target_id == SimulationIds.ANIMAL_DUNJA
|
||||
and zora_state.get_reserved_by() == zora_caretaker_id
|
||||
and dunja_state.get_reserved_by() == dunja_caretaker_id
|
||||
),
|
||||
"Restore should retain both claims, bindings, target IDs, and the exact checksum",
|
||||
)
|
||||
if zora_caretaker == null or dunja_caretaker == null:
|
||||
_finish()
|
||||
return
|
||||
|
||||
var pantry: StorageStateRecord = manager.get_pantry()
|
||||
var pantry_food_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||
var zora_hunger_before := zora_state.get_hunger()
|
||||
var zora_fed_tick_before := zora_state.get_last_fed_tick()
|
||||
var dunja_hunger_before := dunja_state.get_hunger()
|
||||
var dunja_fed_tick_before := dunja_state.get_last_fed_tick()
|
||||
var dunja_reservation_before := dunja_state.get_reserved_by()
|
||||
_check(
|
||||
manager.feed_animal(SimulationIds.ANIMAL_ZORA, zora_caretaker_id),
|
||||
"The restored exact Zora claim should complete one real feed operation",
|
||||
)
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(
|
||||
zora_state.get_hunger(), zora_hunger_before - AnimalStateRecord.HUNGER_RELIEF
|
||||
)
|
||||
and zora_state.get_last_fed_tick() == manager.tick_count
|
||||
and zora_state.get_last_fed_tick() != zora_fed_tick_before
|
||||
and zora_state.get_reserved_by() == -1
|
||||
and is_equal_approx(dunja_state.get_hunger(), dunja_hunger_before)
|
||||
and dunja_state.get_last_fed_tick() == dunja_fed_tick_before
|
||||
and dunja_state.get_reserved_by() == dunja_reservation_before
|
||||
),
|
||||
"Feeding Zora should mutate only Zora's hunger, feed tick, and reservation",
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_food_before - 1.0),
|
||||
"Feeding exactly one goat should consume exactly one pantry food",
|
||||
)
|
||||
var feed_event := _latest_feed_event(manager)
|
||||
_check(
|
||||
(
|
||||
feed_event != null
|
||||
and int(feed_event.data["actor_id"]) == zora_caretaker_id
|
||||
and StringName(feed_event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
and StringName(feed_event.data["destination_id"]) == SimulationIds.ANIMAL_ZORA
|
||||
and StringName(feed_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
||||
and is_equal_approx(float(feed_event.data["amount"]), 1.0)
|
||||
and feed_event.get_world_position().is_equal_approx(zora_state.get_position())
|
||||
),
|
||||
"The feed event should record the exact caretaker, pantry, Zora destination, and amount",
|
||||
)
|
||||
|
||||
_finish()
|
||||
|
||||
|
||||
func _park_npc(npc: SimNPC) -> void:
|
||||
npc.set_task(SimulationIds.ACTION_WANDER, 1000.0)
|
||||
npc.start_working()
|
||||
|
||||
|
||||
func _prepare_feed_task(manager: Node, npc: SimNPC) -> void:
|
||||
manager.release_npc_reservation(npc.id)
|
||||
npc.target_id = &""
|
||||
npc.has_travel_target = false
|
||||
npc.set_task(SimulationIds.ACTION_FEED_ANIMAL)
|
||||
|
||||
|
||||
func _site_ids(candidates: Array[Dictionary]) -> Array[StringName]:
|
||||
var ids: Array[StringName] = []
|
||||
for candidate in candidates:
|
||||
ids.append(StringName(candidate["site_id"]))
|
||||
return ids
|
||||
|
||||
|
||||
func _find_npc(npcs: Array[SimNPC], npc_id: int) -> SimNPC:
|
||||
for npc in npcs:
|
||||
if npc.id == npc_id:
|
||||
return npc
|
||||
return null
|
||||
|
||||
|
||||
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] Two-goat vertical slice passed")
|
||||
quit(0)
|
||||
return
|
||||
for failure in failures:
|
||||
push_error("[TEST] " + failure)
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://drh502csu7pj6
|
||||
@@ -397,29 +397,39 @@ func _run() -> void:
|
||||
_check(outskirt_context_count >= 4, "Jajce discovery should include outskirts/river contexts")
|
||||
_check(farming_context_count >= 1, "Jajce food discovery should include farming contexts")
|
||||
var animal_root := world.get_node("WorldObjects/Animals")
|
||||
var goat := animal_root.get_node("Dunja") as AnimalNode
|
||||
var dunja := animal_root.get_node("Dunja") as AnimalNode
|
||||
var zora := animal_root.get_node("Zora") as AnimalNode
|
||||
_check(
|
||||
animal_root.get_child_count() == 1 and goat != null,
|
||||
"Jajce should contain one bounded loaded-animal presentation"
|
||||
animal_root.get_child_count() == 2 and dunja != null and zora != null,
|
||||
"Jajce should contain the bounded pair of loaded-animal presentations"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
goat.animal_id == SimulationIds.ANIMAL_DUNJA
|
||||
and goat.species_id == SimulationIds.SPECIES_GOAT
|
||||
and goat.display_name == "Dunja"
|
||||
dunja.animal_id == SimulationIds.ANIMAL_DUNJA
|
||||
and dunja.species_id == SimulationIds.SPECIES_GOAT
|
||||
and dunja.display_name == "Dunja"
|
||||
and zora.animal_id == SimulationIds.ANIMAL_ZORA
|
||||
and zora.species_id == SimulationIds.SPECIES_GOAT
|
||||
and zora.display_name == "Zora"
|
||||
),
|
||||
"The authored goat should preserve its stable identity and readable name"
|
||||
"The authored goats should preserve unique stable identities and readable names"
|
||||
)
|
||||
_check(
|
||||
not goat.is_in_group("resource_nodes") and ResourceNode.get_by_id(goat.animal_id) == null,
|
||||
"The identity-backed goat should remain separate from finite resources"
|
||||
(
|
||||
not dunja.is_in_group("resource_nodes")
|
||||
and not zora.is_in_group("resource_nodes")
|
||||
and ResourceNode.get_by_id(dunja.animal_id) == null
|
||||
and ResourceNode.get_by_id(zora.animal_id) == null
|
||||
),
|
||||
"The identity-backed goats should remain separate from finite resources"
|
||||
)
|
||||
_check(
|
||||
goat.is_in_group("grass_interactors"),
|
||||
"Dunja should bend camera-local grass through the shared presentation group"
|
||||
dunja.is_in_group("grass_interactors") and zora.is_in_group("grass_interactors"),
|
||||
"Both goats should bend camera-local grass through the shared presentation group"
|
||||
)
|
||||
var habitat_root := world.get_node("WorldObjects/AnimalHabitats")
|
||||
var habitat := habitat_root.get_node("DunjaHabitat")
|
||||
var dunja_habitat := habitat_root.get_node("DunjaHabitat")
|
||||
var zora_shelter_context := habitat_root.get_node("ZoraShelter")
|
||||
var routine_sites: Array[AnimalRoutineSite] = []
|
||||
for site in AnimalRoutineSite.get_all():
|
||||
routine_sites.append(site)
|
||||
@@ -428,16 +438,25 @@ func _run() -> void:
|
||||
return String(first.site_id) < String(second.site_id)
|
||||
)
|
||||
_check(
|
||||
habitat_root.get_child_count() == 1 and habitat != null and routine_sites.size() == 2,
|
||||
"Jajce should keep Dunja's two routine anchors inside one self-contained habitat scene"
|
||||
(
|
||||
habitat_root.get_child_count() == 2
|
||||
and dunja_habitat != null
|
||||
and zora_shelter_context != null
|
||||
and routine_sites.size() == 3
|
||||
),
|
||||
"Jajce should author one shared pasture and a private shelter context for each goat"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
routine_sites.size() == 2
|
||||
routine_sites.size() == 3
|
||||
and routine_sites[0].site_id == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE
|
||||
and routine_sites[1].site_id == SimulationIds.ANIMAL_SITE_DUNJA_SHELTER
|
||||
and routine_sites[2].site_id == SimulationIds.ANIMAL_SITE_ZORA_SHELTER
|
||||
and routine_sites[0].resident_animal_id.is_empty()
|
||||
and routine_sites[1].resident_animal_id == SimulationIds.ANIMAL_DUNJA
|
||||
and routine_sites[2].resident_animal_id == SimulationIds.ANIMAL_ZORA
|
||||
),
|
||||
"Dunja's pasture and shelter should expose deterministic stable IDs"
|
||||
"Routine sites should expose stable IDs with shared pasture and exact shelter ownership"
|
||||
)
|
||||
_check(world.has_node("WorldObjects/StorageSites"), "JajceWorld should expose StorageSites")
|
||||
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
|
||||
@@ -491,7 +510,8 @@ func _run() -> void:
|
||||
var destinations: Array[Vector3] = []
|
||||
for resource in resources:
|
||||
destinations.append((resource as ResourceNode).interaction_point.global_position)
|
||||
destinations.append(goat.get_interaction_position())
|
||||
destinations.append(dunja.get_interaction_position())
|
||||
destinations.append(zora.get_interaction_position())
|
||||
for site in routine_sites:
|
||||
destinations.append(site.get_routine_position())
|
||||
destinations.append(pantry.get_interaction_position())
|
||||
|
||||
Reference in New Issue
Block a user