|
|
|
@@ -0,0 +1,864 @@
|
|
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
|
|
|
|
call_deferred("_run")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _run() -> void:
|
|
|
|
|
await _test_single_two_leg_delivery()
|
|
|
|
|
await _test_one_unit_contention()
|
|
|
|
|
await _test_legacy_pre_pickup_restore_reroutes()
|
|
|
|
|
await _test_legacy_working_restore_reroutes()
|
|
|
|
|
await _test_post_pickup_restore_continuation()
|
|
|
|
|
_finish()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _test_single_two_leg_delivery() -> void:
|
|
|
|
|
var fixture := await _load_world()
|
|
|
|
|
var main_scene: Node = fixture["main"]
|
|
|
|
|
var manager: Node = fixture["manager"]
|
|
|
|
|
var view: Node = fixture["view"]
|
|
|
|
|
var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode
|
|
|
|
|
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
var dunja_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA)
|
|
|
|
|
var pantry: StorageStateRecord = manager.get_pantry()
|
|
|
|
|
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
var dunja_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
pantry_node != null
|
|
|
|
|
and zora_node != null
|
|
|
|
|
and dunja_node != null
|
|
|
|
|
and pantry != null
|
|
|
|
|
and zora_state != null
|
|
|
|
|
and dunja_state != null
|
|
|
|
|
),
|
|
|
|
|
"The delivery proof needs the authored pantry and both bound named goats",
|
|
|
|
|
)
|
|
|
|
|
if (
|
|
|
|
|
pantry_node == null
|
|
|
|
|
or zora_node == null
|
|
|
|
|
or dunja_node == null
|
|
|
|
|
or pantry == null
|
|
|
|
|
or zora_state == null
|
|
|
|
|
or dunja_state == null
|
|
|
|
|
):
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_prepare_population(manager, view)
|
|
|
|
|
_set_pantry_food(manager, 3.0)
|
|
|
|
|
zora_state.set_hunger(72.0)
|
|
|
|
|
dunja_state.set_hunger(30.0)
|
|
|
|
|
var caretaker: SimNPC = manager.npcs[0]
|
|
|
|
|
var origin := zora_node.get_interaction_position()
|
|
|
|
|
_prepare_feed_task(manager, view, caretaker, origin)
|
|
|
|
|
|
|
|
|
|
_check(
|
|
|
|
|
manager.resolve_npc_target(caretaker.id, origin),
|
|
|
|
|
"A caretaker should resolve a physical route for hungry Zora",
|
|
|
|
|
)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
caretaker.current_task == SimulationIds.ACTION_FEED_ANIMAL
|
|
|
|
|
and caretaker.target_id == SimulationIds.ANIMAL_ZORA
|
|
|
|
|
and zora_state.get_reserved_by() == caretaker.id
|
|
|
|
|
and caretaker.has_travel_target
|
|
|
|
|
and caretaker.travel_target_position.is_equal_approx(
|
|
|
|
|
pantry_node.get_interaction_position()
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
"The first feed leg should retain exact Zora authority while targeting the pantry",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var pantry_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
|
|
|
var observed_villages := []
|
|
|
|
|
var observed_food := []
|
|
|
|
|
var observed_states := []
|
|
|
|
|
manager.village_changed.connect(
|
|
|
|
|
func(changed_village):
|
|
|
|
|
observed_villages.append(changed_village)
|
|
|
|
|
observed_food.append(changed_village.food)
|
|
|
|
|
observed_states.append(manager.serialize_state())
|
|
|
|
|
)
|
|
|
|
|
_arrive_at_current_target(manager, view, caretaker)
|
|
|
|
|
var withdrawal_events := _events_for(
|
|
|
|
|
manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker.id
|
|
|
|
|
)
|
|
|
|
|
var observed_record: SimulationStateRecord
|
|
|
|
|
var observed_npc: SimNPC
|
|
|
|
|
if observed_states.size() == 1:
|
|
|
|
|
observed_record = SimulationStateRecord.from_json(String(observed_states[0]))
|
|
|
|
|
if observed_record != null:
|
|
|
|
|
var observed_npc_record := _find_npc_record(observed_record.npcs, caretaker.id)
|
|
|
|
|
if observed_npc_record != null:
|
|
|
|
|
observed_npc = observed_npc_record.restore(false)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before - 1.0)
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
|
|
|
|
|
and caretaker.current_task == SimulationIds.ACTION_FEED_ANIMAL
|
|
|
|
|
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
|
|
|
|
|
and caretaker.target_id == SimulationIds.ANIMAL_ZORA
|
|
|
|
|
and zora_state.get_reserved_by() == caretaker.id
|
|
|
|
|
and caretaker.has_travel_target
|
|
|
|
|
and caretaker.travel_target_position.is_equal_approx(
|
|
|
|
|
zora_node.get_interaction_position()
|
|
|
|
|
)
|
|
|
|
|
and observed_villages.size() == 1
|
|
|
|
|
and observed_villages[0] == manager.village
|
|
|
|
|
and is_equal_approx(float(observed_food[0]), pantry_before - 1.0)
|
|
|
|
|
and observed_record != null
|
|
|
|
|
and observed_npc != null
|
|
|
|
|
and is_equal_approx(observed_npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
|
|
|
|
|
and observed_npc.target_id == SimulationIds.ANIMAL_ZORA
|
|
|
|
|
and observed_npc.travel_target_position.is_equal_approx(
|
|
|
|
|
zora_node.get_interaction_position()
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"Pantry arrival should atomically expose synchronized food, carried inventory, "
|
|
|
|
|
+ "and the same claimed Zora delivery waypoint"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
withdrawal_events.size() == 1
|
|
|
|
|
and _is_exact_withdrawal(
|
|
|
|
|
withdrawal_events[0],
|
|
|
|
|
caretaker.id,
|
|
|
|
|
pantry_node.get_interaction_position(),
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
"The pickup leg should record one exact pantry-to-caretaker transfer at the pantry",
|
|
|
|
|
)
|
|
|
|
|
var carried_food := _get_carried_food(view, caretaker.id)
|
|
|
|
|
_check(
|
|
|
|
|
carried_food != null and carried_food.visible,
|
|
|
|
|
"Authoritative caretaker inventory should reveal the existing carried-food prop",
|
|
|
|
|
)
|
|
|
|
|
_check(
|
|
|
|
|
is_equal_approx(
|
|
|
|
|
(
|
|
|
|
|
pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
|
|
|
+ caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
|
|
|
|
),
|
|
|
|
|
pantry_before,
|
|
|
|
|
),
|
|
|
|
|
"Food should be conserved across the pantry pickup boundary",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var zora_hunger_before := zora_state.get_hunger()
|
|
|
|
|
var dunja_hunger_before := dunja_state.get_hunger()
|
|
|
|
|
_arrive_at_current_target(manager, view, caretaker)
|
|
|
|
|
_check(
|
|
|
|
|
caretaker.task_state == SimNPC.TASK_STATE_WORKING,
|
|
|
|
|
"Arrival at exact Zora should begin the feed work without another withdrawal",
|
|
|
|
|
)
|
|
|
|
|
var work_ticks := int(ceil(caretaker.task_duration))
|
|
|
|
|
for _tick in range(work_ticks):
|
|
|
|
|
manager.simulate_tick()
|
|
|
|
|
|
|
|
|
|
var feed_events := _events_for(manager, SimulationIds.EVENT_ANIMAL_FED, caretaker.id)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
is_equal_approx(
|
|
|
|
|
zora_state.get_hunger(),
|
|
|
|
|
(
|
|
|
|
|
zora_hunger_before
|
|
|
|
|
+ AnimalStateRecord.HUNGER_PER_TICK * work_ticks
|
|
|
|
|
- AnimalStateRecord.HUNGER_RELIEF
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
and is_equal_approx(
|
|
|
|
|
dunja_state.get_hunger(),
|
|
|
|
|
dunja_hunger_before + AnimalStateRecord.HUNGER_PER_TICK * work_ticks,
|
|
|
|
|
)
|
|
|
|
|
and zora_state.get_last_fed_tick() == manager.tick_count
|
|
|
|
|
and zora_state.get_reserved_by() == -1
|
|
|
|
|
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before - 1.0)
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
|
|
|
|
),
|
|
|
|
|
"Delivery completion should consume carried food and mutate only Zora beyond natural hunger",
|
|
|
|
|
)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
feed_events.size() == 1
|
|
|
|
|
and _is_exact_feed(feed_events[0], caretaker.id, SimulationIds.ANIMAL_ZORA, zora_state)
|
|
|
|
|
and (
|
|
|
|
|
_events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker.id).size()
|
|
|
|
|
== 1
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
"The complete delivery should retain one withdrawal followed by one exact inventory feed",
|
|
|
|
|
)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
feed_events.size() == 1
|
|
|
|
|
and is_equal_approx(
|
|
|
|
|
(
|
|
|
|
|
pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
|
|
|
+ caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
|
|
|
|
+ float(feed_events[0].data["amount"])
|
|
|
|
|
),
|
|
|
|
|
pantry_before,
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
"Exactly one delivered food should account for the unit that leaves tracked holdings",
|
|
|
|
|
)
|
|
|
|
|
carried_food = _get_carried_food(view, caretaker.id)
|
|
|
|
|
_check(
|
|
|
|
|
carried_food != null and not carried_food.visible,
|
|
|
|
|
"The carried-food prop should disappear after the exact goat receives it",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _test_one_unit_contention() -> void:
|
|
|
|
|
var fixture := await _load_world()
|
|
|
|
|
var main_scene: Node = fixture["main"]
|
|
|
|
|
var manager: Node = fixture["manager"]
|
|
|
|
|
var view: Node = fixture["view"]
|
|
|
|
|
var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode
|
|
|
|
|
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
var dunja_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA)
|
|
|
|
|
var pantry: StorageStateRecord = manager.get_pantry()
|
|
|
|
|
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
var dunja_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
|
|
|
|
if (
|
|
|
|
|
pantry_node == null
|
|
|
|
|
or zora_node == null
|
|
|
|
|
or dunja_node == null
|
|
|
|
|
or pantry == null
|
|
|
|
|
or zora_state == null
|
|
|
|
|
or dunja_state == null
|
|
|
|
|
):
|
|
|
|
|
_check(false, "The contention proof needs the authored pantry and paired goats")
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_prepare_population(manager, view)
|
|
|
|
|
_set_pantry_food(manager, 1.0)
|
|
|
|
|
zora_state.set_hunger(72.0)
|
|
|
|
|
dunja_state.set_hunger(72.0)
|
|
|
|
|
var winner: SimNPC = manager.npcs[0]
|
|
|
|
|
var loser: SimNPC = manager.npcs[1]
|
|
|
|
|
var shared_origin := zora_node.get_interaction_position()
|
|
|
|
|
_prepare_feed_task(manager, view, winner, shared_origin)
|
|
|
|
|
_prepare_feed_task(manager, view, loser, shared_origin)
|
|
|
|
|
var winner_resolved: bool = manager.resolve_npc_target(winner.id, shared_origin)
|
|
|
|
|
var loser_resolved: bool = manager.resolve_npc_target(loser.id, shared_origin)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
winner_resolved
|
|
|
|
|
and loser_resolved
|
|
|
|
|
and winner.target_id == SimulationIds.ANIMAL_ZORA
|
|
|
|
|
and loser.target_id == SimulationIds.ANIMAL_DUNJA
|
|
|
|
|
and zora_state.get_reserved_by() == winner.id
|
|
|
|
|
and dunja_state.get_reserved_by() == loser.id
|
|
|
|
|
and winner.travel_target_position.is_equal_approx(
|
|
|
|
|
pantry_node.get_interaction_position()
|
|
|
|
|
)
|
|
|
|
|
and loser.travel_target_position.is_equal_approx(pantry_node.get_interaction_position())
|
|
|
|
|
),
|
|
|
|
|
"Two caretakers should retain independent exact goat claims while sharing the pantry leg",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_arrive_at_current_target(manager, view, winner)
|
|
|
|
|
_arrive_at_current_target(manager, view, loser)
|
|
|
|
|
var all_withdrawals := _events_of_type(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN)
|
|
|
|
|
var loser_blocked := _events_for(manager, SimulationIds.EVENT_TASK_BLOCKED, loser.id)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
|
|
|
|
and is_equal_approx(winner.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
|
|
|
|
|
and is_equal_approx(loser.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
|
|
|
|
and all_withdrawals.size() == 1
|
|
|
|
|
and int(all_withdrawals[0].data["actor_id"]) == winner.id
|
|
|
|
|
),
|
|
|
|
|
"One pantry unit should produce exactly one winning pickup without duplication",
|
|
|
|
|
)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
winner.current_task == SimulationIds.ACTION_FEED_ANIMAL
|
|
|
|
|
and winner.target_id == SimulationIds.ANIMAL_ZORA
|
|
|
|
|
and zora_state.get_reserved_by() == winner.id
|
|
|
|
|
and loser.current_task != SimulationIds.ACTION_FEED_ANIMAL
|
|
|
|
|
and loser.get_inventory_amount(SimulationIds.RESOURCE_FOOD) == 0.0
|
|
|
|
|
and dunja_state.get_reserved_by() == -1
|
|
|
|
|
and loser_blocked.size() == 1
|
|
|
|
|
and (
|
|
|
|
|
StringName(loser_blocked[0].data["source_id"])
|
|
|
|
|
== SimulationIds.STORAGE_VILLAGE_PANTRY
|
|
|
|
|
)
|
|
|
|
|
and (StringName(loser_blocked[0].data["action_id"]) == SimulationIds.ACTION_FEED_ANIMAL)
|
|
|
|
|
and is_equal_approx(float(loser_blocked[0].data["required_amount"]), 1.0)
|
|
|
|
|
),
|
|
|
|
|
"The losing pantry arrival should record the shortfall, release only its claim, and not feed",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var zora_hunger_before := zora_state.get_hunger()
|
|
|
|
|
var dunja_hunger_before := dunja_state.get_hunger()
|
|
|
|
|
_arrive_at_current_target(manager, view, winner)
|
|
|
|
|
var work_ticks := int(ceil(winner.task_duration))
|
|
|
|
|
for _tick in range(work_ticks):
|
|
|
|
|
manager.simulate_tick()
|
|
|
|
|
var feed_events := _events_of_type(manager, SimulationIds.EVENT_ANIMAL_FED)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
feed_events.size() == 1
|
|
|
|
|
and _is_exact_feed(feed_events[0], winner.id, SimulationIds.ANIMAL_ZORA, zora_state)
|
|
|
|
|
and is_equal_approx(
|
|
|
|
|
zora_state.get_hunger(),
|
|
|
|
|
(
|
|
|
|
|
zora_hunger_before
|
|
|
|
|
+ AnimalStateRecord.HUNGER_PER_TICK * work_ticks
|
|
|
|
|
- AnimalStateRecord.HUNGER_RELIEF
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
and is_equal_approx(
|
|
|
|
|
dunja_state.get_hunger(),
|
|
|
|
|
dunja_hunger_before + AnimalStateRecord.HUNGER_PER_TICK * work_ticks,
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
"Contention should complete only the winning caretaker's exact Zora delivery",
|
|
|
|
|
)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
_events_of_type(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN).size() == 1
|
|
|
|
|
and is_equal_approx(
|
|
|
|
|
(
|
|
|
|
|
pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
|
|
|
+ winner.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
|
|
|
|
+ loser.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
|
|
|
|
+ float(feed_events[0].data["amount"])
|
|
|
|
|
),
|
|
|
|
|
1.0,
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
"One-unit contention should conserve exactly one food through final delivery",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _test_legacy_pre_pickup_restore_reroutes() -> void:
|
|
|
|
|
var fixture := await _load_world()
|
|
|
|
|
var main_scene: Node = fixture["main"]
|
|
|
|
|
var manager: Node = fixture["manager"]
|
|
|
|
|
var view: Node = fixture["view"]
|
|
|
|
|
var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode
|
|
|
|
|
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
var pantry: StorageStateRecord = manager.get_pantry()
|
|
|
|
|
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
if pantry_node == null or zora_node == null or pantry == null or zora_state == null:
|
|
|
|
|
_check(false, "The legacy-route proof needs the authored pantry and Zora")
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_prepare_population(manager, view)
|
|
|
|
|
_set_pantry_food(manager, 2.0)
|
|
|
|
|
zora_state.set_hunger(72.0)
|
|
|
|
|
var caretaker: SimNPC = manager.npcs[0]
|
|
|
|
|
var caretaker_id := caretaker.id
|
|
|
|
|
var origin := zora_node.get_interaction_position()
|
|
|
|
|
_prepare_feed_task(manager, view, caretaker, origin)
|
|
|
|
|
_check(
|
|
|
|
|
manager.resolve_npc_target(caretaker.id, origin),
|
|
|
|
|
"The legacy-route caretaker should establish an exact Zora claim",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Schema v11 predates the pantry leg, so its valid active feed records can
|
|
|
|
|
# retain an empty inventory while traveling directly to the claimed goat.
|
|
|
|
|
caretaker.travel_target_position = zora_node.get_interaction_position()
|
|
|
|
|
caretaker.has_travel_target = true
|
|
|
|
|
var legacy_route_json: String = manager.serialize_state()
|
|
|
|
|
var legacy_route_checksum: String = manager.get_state_checksum()
|
|
|
|
|
var pantry_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
|
|
|
_check(
|
|
|
|
|
manager.restore_state_from_json(legacy_route_json),
|
|
|
|
|
"A pre-delivery schema-v11 feed route should remain restorable",
|
|
|
|
|
)
|
|
|
|
|
_freeze_visuals(view)
|
|
|
|
|
caretaker = _find_npc(manager.npcs, caretaker_id)
|
|
|
|
|
pantry = manager.get_pantry()
|
|
|
|
|
zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
caretaker != null
|
|
|
|
|
and manager.get_state_checksum() == legacy_route_checksum
|
|
|
|
|
and caretaker.travel_target_position.is_equal_approx(
|
|
|
|
|
zora_node.get_interaction_position()
|
|
|
|
|
)
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
|
|
|
|
and zora_state.get_reserved_by() == caretaker_id
|
|
|
|
|
),
|
|
|
|
|
"Restore should first preserve the valid legacy route and exact claim",
|
|
|
|
|
)
|
|
|
|
|
if caretaker == null:
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_arrive_at_current_target(manager, view, caretaker)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before)
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
|
|
|
|
and _events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id).is_empty()
|
|
|
|
|
and caretaker.current_task == SimulationIds.ACTION_FEED_ANIMAL
|
|
|
|
|
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
|
|
|
|
|
and caretaker.target_id == SimulationIds.ANIMAL_ZORA
|
|
|
|
|
and zora_state.get_reserved_by() == caretaker_id
|
|
|
|
|
and caretaker.travel_target_position.is_equal_approx(
|
|
|
|
|
pantry_node.get_interaction_position()
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
"Legacy goat arrival should reroute to the pantry without a remote withdrawal",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_arrive_at_current_target(manager, view, caretaker)
|
|
|
|
|
var withdrawal_events := _events_for(
|
|
|
|
|
manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id
|
|
|
|
|
)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before - 1.0)
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
|
|
|
|
|
and withdrawal_events.size() == 1
|
|
|
|
|
and _is_exact_withdrawal(
|
|
|
|
|
withdrawal_events[0],
|
|
|
|
|
caretaker_id,
|
|
|
|
|
pantry_node.get_interaction_position(),
|
|
|
|
|
)
|
|
|
|
|
and caretaker.travel_target_position.is_equal_approx(
|
|
|
|
|
zora_node.get_interaction_position()
|
|
|
|
|
)
|
|
|
|
|
and zora_state.get_reserved_by() == caretaker_id
|
|
|
|
|
),
|
|
|
|
|
"Legacy continuation should withdraw only after physical pantry arrival",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _test_legacy_working_restore_reroutes() -> void:
|
|
|
|
|
var fixture := await _load_world()
|
|
|
|
|
var main_scene: Node = fixture["main"]
|
|
|
|
|
var manager: Node = fixture["manager"]
|
|
|
|
|
var view: Node = fixture["view"]
|
|
|
|
|
var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode
|
|
|
|
|
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
var pantry: StorageStateRecord = manager.get_pantry()
|
|
|
|
|
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
if pantry_node == null or zora_node == null or pantry == null or zora_state == null:
|
|
|
|
|
_check(false, "The legacy-working proof needs the authored pantry and Zora")
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_prepare_population(manager, view)
|
|
|
|
|
_set_pantry_food(manager, 2.0)
|
|
|
|
|
zora_state.set_hunger(72.0)
|
|
|
|
|
var caretaker: SimNPC = manager.npcs[0]
|
|
|
|
|
var caretaker_id := caretaker.id
|
|
|
|
|
var origin := zora_node.get_interaction_position()
|
|
|
|
|
_prepare_feed_task(manager, view, caretaker, origin)
|
|
|
|
|
_check(
|
|
|
|
|
manager.resolve_npc_target(caretaker.id, origin),
|
|
|
|
|
"The legacy-working caretaker should establish an exact Zora claim",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# The old one-leg flow could also be saved after goat arrival, while work
|
|
|
|
|
# was already in progress but before pantry-backed completion.
|
|
|
|
|
caretaker.task_state = SimNPC.TASK_STATE_WORKING
|
|
|
|
|
caretaker.task_progress = 2.0
|
|
|
|
|
caretaker.task_complete = false
|
|
|
|
|
caretaker.has_travel_target = false
|
|
|
|
|
caretaker.travel_target_position = zora_node.get_interaction_position()
|
|
|
|
|
var legacy_working_json: String = manager.serialize_state()
|
|
|
|
|
var legacy_working_checksum: String = manager.get_state_checksum()
|
|
|
|
|
var pantry_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
|
|
|
_check(
|
|
|
|
|
manager.restore_state_from_json(legacy_working_json),
|
|
|
|
|
"A working pre-delivery schema-v11 feed state should remain restorable",
|
|
|
|
|
)
|
|
|
|
|
_freeze_visuals(view)
|
|
|
|
|
caretaker = _find_npc(manager.npcs, caretaker_id)
|
|
|
|
|
pantry = manager.get_pantry()
|
|
|
|
|
zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
caretaker != null
|
|
|
|
|
and manager.get_state_checksum() == legacy_working_checksum
|
|
|
|
|
and caretaker.task_state == SimNPC.TASK_STATE_WORKING
|
|
|
|
|
and is_equal_approx(caretaker.task_progress, 2.0)
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
|
|
|
|
and zora_state.get_reserved_by() == caretaker_id
|
|
|
|
|
),
|
|
|
|
|
"Restore should initially preserve the legacy working state and checksum",
|
|
|
|
|
)
|
|
|
|
|
if caretaker == null:
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
manager.simulate_tick()
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before)
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
|
|
|
|
and _events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id).is_empty()
|
|
|
|
|
and caretaker.current_task == SimulationIds.ACTION_FEED_ANIMAL
|
|
|
|
|
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
|
|
|
|
|
and caretaker.target_id == SimulationIds.ANIMAL_ZORA
|
|
|
|
|
and caretaker.has_travel_target
|
|
|
|
|
and caretaker.travel_target_position.is_equal_approx(
|
|
|
|
|
pantry_node.get_interaction_position()
|
|
|
|
|
)
|
|
|
|
|
and is_equal_approx(caretaker.task_progress, 2.0)
|
|
|
|
|
and zora_state.get_reserved_by() == caretaker_id
|
|
|
|
|
),
|
|
|
|
|
"The first resumed tick should preserve work progress and reroute to the pantry",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_arrive_at_current_target(manager, view, caretaker)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before - 1.0)
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
|
|
|
|
|
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
|
|
|
|
|
and caretaker.travel_target_position.is_equal_approx(
|
|
|
|
|
zora_node.get_interaction_position()
|
|
|
|
|
)
|
|
|
|
|
and is_equal_approx(caretaker.task_progress, 2.0)
|
|
|
|
|
and zora_state.get_reserved_by() == caretaker_id
|
|
|
|
|
),
|
|
|
|
|
"Recovered work should pick up at the pantry and return to the same Zora claim",
|
|
|
|
|
)
|
|
|
|
|
_arrive_at_current_target(manager, view, caretaker)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
caretaker.task_state == SimNPC.TASK_STATE_WORKING
|
|
|
|
|
and is_equal_approx(caretaker.task_progress, 2.0)
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
|
|
|
|
|
and zora_state.get_reserved_by() == caretaker_id
|
|
|
|
|
),
|
|
|
|
|
"Legacy work should resume beside Zora without losing its saved progress",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _test_post_pickup_restore_continuation() -> void:
|
|
|
|
|
var fixture := await _load_world()
|
|
|
|
|
var main_scene: Node = fixture["main"]
|
|
|
|
|
var manager: Node = fixture["manager"]
|
|
|
|
|
var view: Node = fixture["view"]
|
|
|
|
|
var pantry_node := StorageNode.get_by_id(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageNode
|
|
|
|
|
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
var pantry: StorageStateRecord = manager.get_pantry()
|
|
|
|
|
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
if pantry_node == null or zora_node == null or pantry == null or zora_state == null:
|
|
|
|
|
_check(false, "The restore proof needs the authored pantry and Zora")
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_prepare_population(manager, view)
|
|
|
|
|
_set_pantry_food(manager, 2.0)
|
|
|
|
|
zora_state.set_hunger(72.0)
|
|
|
|
|
var caretaker: SimNPC = manager.npcs[0]
|
|
|
|
|
var caretaker_id := caretaker.id
|
|
|
|
|
var origin := zora_node.get_interaction_position()
|
|
|
|
|
_prepare_feed_task(manager, view, caretaker, origin)
|
|
|
|
|
_check(
|
|
|
|
|
manager.resolve_npc_target(caretaker.id, origin),
|
|
|
|
|
"The restore caretaker should resolve Zora's two-leg delivery",
|
|
|
|
|
)
|
|
|
|
|
_arrive_at_current_target(manager, view, caretaker)
|
|
|
|
|
|
|
|
|
|
var midpoint_json: String = manager.serialize_state()
|
|
|
|
|
var midpoint_checksum: String = manager.get_state_checksum()
|
|
|
|
|
var midpoint_position := caretaker.position
|
|
|
|
|
var midpoint_destination := caretaker.travel_target_position
|
|
|
|
|
var midpoint_pantry := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
caretaker.target_id == SimulationIds.ANIMAL_ZORA
|
|
|
|
|
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
|
|
|
|
|
and caretaker.has_travel_target
|
|
|
|
|
and caretaker.travel_target_position.is_equal_approx(
|
|
|
|
|
zora_node.get_interaction_position()
|
|
|
|
|
)
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
|
|
|
|
|
and zora_state.get_reserved_by() == caretaker.id
|
|
|
|
|
and (
|
|
|
|
|
_events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker.id).size()
|
|
|
|
|
== 1
|
|
|
|
|
)
|
|
|
|
|
and _events_for(manager, SimulationIds.EVENT_ANIMAL_FED, caretaker.id).is_empty()
|
|
|
|
|
),
|
|
|
|
|
"The save point should be after one real pickup and before the exact goat delivery",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_arrive_at_current_target(manager, view, caretaker)
|
|
|
|
|
var continuation_ticks := int(ceil(caretaker.task_duration))
|
|
|
|
|
for _tick in range(continuation_ticks):
|
|
|
|
|
manager.simulate_tick()
|
|
|
|
|
var uninterrupted_checksum: String = manager.get_state_checksum()
|
|
|
|
|
var uninterrupted_feed_events := _events_for(
|
|
|
|
|
manager, SimulationIds.EVENT_ANIMAL_FED, caretaker_id
|
|
|
|
|
)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
uninterrupted_feed_events.size() == 1
|
|
|
|
|
and (
|
|
|
|
|
_events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id).size()
|
|
|
|
|
== 1
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
"The uninterrupted control should finish with one pickup and one feed",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_check(
|
|
|
|
|
manager.restore_state_from_json(midpoint_json),
|
|
|
|
|
"A post-pickup in-progress feed state should pass the complete save schema",
|
|
|
|
|
)
|
|
|
|
|
_freeze_visuals(view)
|
|
|
|
|
caretaker = _find_npc(manager.npcs, caretaker_id)
|
|
|
|
|
pantry = manager.get_pantry()
|
|
|
|
|
zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
|
|
|
|
var restored_visual := _get_visual(view, caretaker_id)
|
|
|
|
|
var restored_carried_food := _get_carried_food(view, caretaker_id)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
caretaker != null
|
|
|
|
|
and pantry != null
|
|
|
|
|
and zora_state != null
|
|
|
|
|
and manager.get_state_checksum() == midpoint_checksum
|
|
|
|
|
and caretaker.position.is_equal_approx(midpoint_position)
|
|
|
|
|
and caretaker.travel_target_position.is_equal_approx(midpoint_destination)
|
|
|
|
|
and caretaker.current_task == SimulationIds.ACTION_FEED_ANIMAL
|
|
|
|
|
and caretaker.task_state == SimNPC.TASK_STATE_TRAVELING
|
|
|
|
|
and caretaker.target_id == SimulationIds.ANIMAL_ZORA
|
|
|
|
|
and caretaker.has_travel_target
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
|
|
|
|
|
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), midpoint_pantry)
|
|
|
|
|
and zora_state.get_reserved_by() == caretaker_id
|
|
|
|
|
and restored_visual != null
|
|
|
|
|
and restored_visual.global_position.is_equal_approx(midpoint_position)
|
|
|
|
|
and restored_visual.get("current_target") == midpoint_destination
|
|
|
|
|
and restored_carried_food != null
|
|
|
|
|
and restored_carried_food.visible
|
|
|
|
|
and (
|
|
|
|
|
_events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id).size()
|
|
|
|
|
== 1
|
|
|
|
|
)
|
|
|
|
|
and _events_for(manager, SimulationIds.EVENT_ANIMAL_FED, caretaker_id).is_empty()
|
|
|
|
|
),
|
|
|
|
|
"Restore should retain exact claim, route, carried food, pickup history, and checksum",
|
|
|
|
|
)
|
|
|
|
|
if caretaker == null:
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_arrive_at_current_target(manager, view, caretaker)
|
|
|
|
|
for _tick in range(continuation_ticks):
|
|
|
|
|
manager.simulate_tick()
|
|
|
|
|
var restored_feed_events := _events_for(manager, SimulationIds.EVENT_ANIMAL_FED, caretaker_id)
|
|
|
|
|
_check(
|
|
|
|
|
(
|
|
|
|
|
manager.get_state_checksum() == uninterrupted_checksum
|
|
|
|
|
and (
|
|
|
|
|
_events_for(manager, SimulationIds.EVENT_STORAGE_WITHDRAWN, caretaker_id).size()
|
|
|
|
|
== 1
|
|
|
|
|
)
|
|
|
|
|
and restored_feed_events.size() == 1
|
|
|
|
|
and _is_exact_feed(
|
|
|
|
|
restored_feed_events[0],
|
|
|
|
|
caretaker_id,
|
|
|
|
|
SimulationIds.ANIMAL_ZORA,
|
|
|
|
|
zora_state,
|
|
|
|
|
)
|
|
|
|
|
and is_equal_approx(caretaker.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
|
|
|
|
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), midpoint_pantry)
|
|
|
|
|
),
|
|
|
|
|
"Restored continuation should match uninterrupted state without replaying either transfer",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await _unload_world(main_scene)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _load_world() -> Dictionary:
|
|
|
|
|
var main_scene: Node = load("res://main.tscn").instantiate()
|
|
|
|
|
root.add_child(main_scene)
|
|
|
|
|
await process_frame
|
|
|
|
|
for _frame in 3:
|
|
|
|
|
await physics_frame
|
|
|
|
|
var manager: Node = main_scene.get_node("SimulationManager")
|
|
|
|
|
manager.set_process(false)
|
|
|
|
|
var view: Node = main_scene.get_node("WorldViewManager")
|
|
|
|
|
_freeze_visuals(view)
|
|
|
|
|
return {"main": main_scene, "manager": manager, "view": view}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _unload_world(main_scene: Node) -> void:
|
|
|
|
|
main_scene.queue_free()
|
|
|
|
|
await process_frame
|
|
|
|
|
await physics_frame
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _prepare_population(manager: Node, view: Node) -> void:
|
|
|
|
|
for npc in manager.npcs:
|
|
|
|
|
manager.release_npc_reservation(npc.id)
|
|
|
|
|
npc.inventory.clear()
|
|
|
|
|
npc.hunger = 20.0
|
|
|
|
|
npc.energy = 100.0
|
|
|
|
|
npc.is_starving = false
|
|
|
|
|
npc.starvation_ticks = 0
|
|
|
|
|
npc.set_task(SimulationIds.ACTION_WANDER, 1000.0)
|
|
|
|
|
npc.start_working()
|
|
|
|
|
(
|
|
|
|
|
manager
|
|
|
|
|
. emit_signal(
|
|
|
|
|
"npc_inventory_changed",
|
|
|
|
|
npc,
|
|
|
|
|
SimulationIds.RESOURCE_FOOD,
|
|
|
|
|
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
_freeze_visuals(view)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _prepare_feed_task(manager: Node, view: Node, npc: SimNPC, origin: Vector3) -> void:
|
|
|
|
|
manager.release_npc_reservation(npc.id)
|
|
|
|
|
npc.inventory.clear()
|
|
|
|
|
npc.target_id = &""
|
|
|
|
|
npc.has_travel_target = false
|
|
|
|
|
npc.position = origin
|
|
|
|
|
npc.set_task(SimulationIds.ACTION_FEED_ANIMAL)
|
|
|
|
|
var visual := _get_visual(view, npc.id)
|
|
|
|
|
if visual != null:
|
|
|
|
|
visual.global_position = origin
|
|
|
|
|
manager.synchronize_npc_position(npc.id, origin)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _set_pantry_food(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 _arrive_at_current_target(manager: Node, view: Node, npc: SimNPC) -> void:
|
|
|
|
|
var visual := _get_visual(view, npc.id)
|
|
|
|
|
if visual != null:
|
|
|
|
|
visual.set_physics_process(false)
|
|
|
|
|
visual.global_position = npc.travel_target_position
|
|
|
|
|
manager.synchronize_npc_position(npc.id, npc.travel_target_position)
|
|
|
|
|
manager.notify_npc_arrived(npc.id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _freeze_visuals(view: Node) -> void:
|
|
|
|
|
for visual in view.active_npc_visuals.values():
|
|
|
|
|
visual.set_physics_process(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _get_visual(view: Node, npc_id: int) -> Node3D:
|
|
|
|
|
return view.active_npc_visuals.get(npc_id) as Node3D
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _get_carried_food(view: Node, npc_id: int) -> MeshInstance3D:
|
|
|
|
|
var visual := _get_visual(view, npc_id)
|
|
|
|
|
if visual == null:
|
|
|
|
|
return null
|
|
|
|
|
return visual.get_node("CarriedFood") as MeshInstance3D
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _find_npc(npcs: Array[SimNPC], npc_id: int) -> SimNPC:
|
|
|
|
|
for npc in npcs:
|
|
|
|
|
if npc.id == npc_id:
|
|
|
|
|
return npc
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _find_npc_record(records: Array[NPCStateRecord], npc_id: int) -> NPCStateRecord:
|
|
|
|
|
for record in records:
|
|
|
|
|
if int(record.data["id"]) == npc_id:
|
|
|
|
|
return record
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _events_for(
|
|
|
|
|
manager: Node, event_type: StringName, actor_id: int
|
|
|
|
|
) -> Array[EconomicEventRecord]:
|
|
|
|
|
var matching: Array[EconomicEventRecord] = []
|
|
|
|
|
for event in manager.economic_events:
|
|
|
|
|
if (
|
|
|
|
|
StringName(event.data["event_type"]) == event_type
|
|
|
|
|
and int(event.data["actor_id"]) == actor_id
|
|
|
|
|
):
|
|
|
|
|
matching.append(event)
|
|
|
|
|
return matching
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _events_of_type(manager: Node, event_type: StringName) -> Array[EconomicEventRecord]:
|
|
|
|
|
var matching: Array[EconomicEventRecord] = []
|
|
|
|
|
for event in manager.economic_events:
|
|
|
|
|
if StringName(event.data["event_type"]) == event_type:
|
|
|
|
|
matching.append(event)
|
|
|
|
|
return matching
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _is_exact_withdrawal(
|
|
|
|
|
event: EconomicEventRecord, actor_id: int, pantry_position: Vector3
|
|
|
|
|
) -> bool:
|
|
|
|
|
return (
|
|
|
|
|
event != null
|
|
|
|
|
and int(event.data["actor_id"]) == actor_id
|
|
|
|
|
and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
|
|
|
|
|
and (StringName(event.data["destination_id"]) == SimulationIds.npc_inventory_id(actor_id))
|
|
|
|
|
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
|
|
|
|
and is_equal_approx(float(event.data["amount"]), 1.0)
|
|
|
|
|
and event.get_world_position().is_equal_approx(pantry_position)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _is_exact_feed(
|
|
|
|
|
event: EconomicEventRecord,
|
|
|
|
|
actor_id: int,
|
|
|
|
|
animal_id: StringName,
|
|
|
|
|
animal_state: AnimalStateRecord,
|
|
|
|
|
) -> bool:
|
|
|
|
|
return (
|
|
|
|
|
event != null
|
|
|
|
|
and animal_state != null
|
|
|
|
|
and int(event.data["actor_id"]) == actor_id
|
|
|
|
|
and StringName(event.data["source_id"]) == SimulationIds.npc_inventory_id(actor_id)
|
|
|
|
|
and StringName(event.data["destination_id"]) == animal_id
|
|
|
|
|
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
|
|
|
|
and is_equal_approx(float(event.data["amount"]), 1.0)
|
|
|
|
|
and event.get_world_position().is_equal_approx(animal_state.get_position())
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
|
|
|
if not condition:
|
|
|
|
|
failures.append(message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _finish() -> void:
|
|
|
|
|
if failures.is_empty():
|
|
|
|
|
print("[TEST] Animal food delivery passed")
|
|
|
|
|
quit(0)
|
|
|
|
|
return
|
|
|
|
|
for failure in failures:
|
|
|
|
|
push_error("[TEST] " + failure)
|
|
|
|
|
quit(1)
|