241 lines
8.0 KiB
GDScript
241 lines
8.0 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 state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
|
var pasture := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_DUNJA_PASTURE)
|
|
var shelter := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_DUNJA_SHELTER)
|
|
_check(
|
|
goat != null and state != null and pasture != null and shelter != null,
|
|
"The routine proof needs Dunja and both loaded stable-ID habitat sites"
|
|
)
|
|
if goat == null or state == null or pasture == null or shelter == null:
|
|
_finish()
|
|
return
|
|
|
|
_check(
|
|
not state.begin_travel(shelter.site_id, shelter.get_routine_position()),
|
|
"Animal state should reject travel back to its current stable site",
|
|
)
|
|
state.set_enabled(false)
|
|
_check(
|
|
not state.begin_travel(pasture.site_id, pasture.get_routine_position()),
|
|
"Animal state should reject travel while the animal is disabled",
|
|
)
|
|
state.set_enabled(true)
|
|
|
|
for npc in manager.npcs:
|
|
npc.set_task(SimulationIds.ACTION_WANDER, 1000.0)
|
|
npc.start_working()
|
|
|
|
var navigation_map: RID = main_scene.get_world_3d().navigation_map
|
|
await _wait_for_navigation_map(navigation_map)
|
|
NavigationServer3D.map_force_update(navigation_map)
|
|
var authored_path := NavigationServer3D.map_get_path(
|
|
navigation_map, shelter.get_routine_position(), pasture.get_routine_position(), true
|
|
)
|
|
_check(
|
|
not authored_path.is_empty(),
|
|
"The shelter and pasture should be connected by the real Jajce navigation map"
|
|
)
|
|
|
|
state.set_hunger(30.0)
|
|
_check(
|
|
state.schedule_routine_at(manager.tick_count),
|
|
"The test should be able to make the content goat's routine immediately due"
|
|
)
|
|
goat.move_speed = 4.0
|
|
manager.simulate_tick()
|
|
_check(
|
|
(
|
|
state.has_active_travel_target()
|
|
and state.get_routine_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_SHELTER
|
|
and state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE
|
|
and state.get_travel_target_position().is_equal_approx(pasture.get_routine_position())
|
|
),
|
|
"The due routine should deterministically choose pasture from shelter"
|
|
)
|
|
|
|
for _frame in 3:
|
|
await physics_frame
|
|
_check(
|
|
goat.is_navigating() and not goat.get_navigation_path().is_empty(),
|
|
"The loaded goat should follow a NavigationServer path rather than a straight state teleport"
|
|
)
|
|
var start_position := state.get_position()
|
|
for _frame in 22:
|
|
await physics_frame
|
|
var mid_route_position := state.get_position()
|
|
_check(
|
|
(
|
|
not mid_route_position.is_equal_approx(start_position)
|
|
and state.has_active_travel_target()
|
|
and goat.global_position.is_equal_approx(mid_route_position)
|
|
),
|
|
"Loaded movement should continuously report Dunja's authoritative mid-route position"
|
|
)
|
|
|
|
var mid_route_json: String = manager.serialize_state()
|
|
var mid_route_checksum: String = manager.get_state_checksum()
|
|
var random_streams_before_restore := JSON.stringify(
|
|
manager.create_state_record().simulation["wander_random_streams"]
|
|
)
|
|
var saved_target_position := state.get_travel_target_position()
|
|
_check(
|
|
manager.restore_state_from_json(mid_route_json),
|
|
"A schema-v11 save should accept Dunja's in-progress routine"
|
|
)
|
|
state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
|
_check(
|
|
(
|
|
manager.get_state_checksum() == mid_route_checksum
|
|
and state.get_position().is_equal_approx(mid_route_position)
|
|
and state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE
|
|
and state.get_travel_target_position().is_equal_approx(saved_target_position)
|
|
and goat.global_position.is_equal_approx(mid_route_position)
|
|
and (
|
|
JSON.stringify(manager.create_state_record().simulation["wander_random_streams"])
|
|
== random_streams_before_restore
|
|
)
|
|
),
|
|
"Mid-route restore should preserve exact position, target, checksum, and every NPC RNG stream"
|
|
)
|
|
|
|
var animals_parent := goat.get_parent()
|
|
goat.queue_free()
|
|
await process_frame
|
|
_check(
|
|
(
|
|
AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA) == null
|
|
and state.has_active_travel_target()
|
|
and state.get_position().is_equal_approx(mid_route_position)
|
|
and manager.get_state_checksum() == mid_route_checksum
|
|
),
|
|
"Unloading Dunja's presentation should leave her in-progress simulation state untouched"
|
|
)
|
|
var reloaded_goat := load("res://world/animals/goat/cozy_goat.tscn").instantiate() as AnimalNode
|
|
animals_parent.add_child(reloaded_goat)
|
|
# Freeze presentation movement while proving that binding itself is a pure
|
|
# reload. A physics step may otherwise land between add_child() and the next
|
|
# process frame and legitimately advance the authoritative route.
|
|
reloaded_goat.set_physics_process(false)
|
|
await process_frame
|
|
goat = reloaded_goat
|
|
goat.move_speed = 4.0
|
|
_check(
|
|
(
|
|
goat.state == state
|
|
and goat.global_position.is_equal_approx(mid_route_position)
|
|
and manager.get_state_checksum() == mid_route_checksum
|
|
),
|
|
"Reloading the self-contained goat scene should bind the same state without resetting it"
|
|
)
|
|
goat.set_physics_process(true)
|
|
for _frame in 3:
|
|
await physics_frame
|
|
_check(
|
|
goat.is_navigating() and not goat.get_navigation_path().is_empty(),
|
|
"Restore should rebuild the loaded navigation path from saved facts"
|
|
)
|
|
for _frame in 240:
|
|
if not state.has_active_travel_target():
|
|
break
|
|
await physics_frame
|
|
_check(
|
|
(
|
|
not state.has_active_travel_target()
|
|
and state.get_routine_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE
|
|
and state.get_position().is_equal_approx(pasture.get_routine_position())
|
|
and not goat.is_navigating()
|
|
),
|
|
(
|
|
"Arrival should atomically promote pasture and retain its exact position "
|
|
+ (
|
|
"(active=%s site=%s state=%s target=%s navigating=%s)"
|
|
% [
|
|
state.has_active_travel_target(),
|
|
state.get_routine_site_id(),
|
|
state.get_position(),
|
|
pasture.get_routine_position(),
|
|
goat.is_navigating(),
|
|
]
|
|
)
|
|
)
|
|
)
|
|
|
|
var arrived_json: String = manager.serialize_state()
|
|
var arrived_checksum: String = manager.get_state_checksum()
|
|
_check(
|
|
(
|
|
manager.restore_state_from_json(arrived_json)
|
|
and manager.get_state_checksum() == arrived_checksum
|
|
),
|
|
"An arrived routine should also restore without replaying movement"
|
|
)
|
|
state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
|
_check(
|
|
not state.has_active_travel_target() and not goat.is_navigating(),
|
|
"An arrived save should remain quietly at pasture after binding"
|
|
)
|
|
|
|
state.schedule_routine_at(manager.tick_count)
|
|
manager.simulate_tick()
|
|
_check(
|
|
(
|
|
state.has_active_travel_target()
|
|
and state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_SHELTER
|
|
and state.get_travel_target_position().is_equal_approx(shelter.get_routine_position())
|
|
),
|
|
"The next due routine should deterministically alternate back to shelter"
|
|
)
|
|
state.set_hunger(AnimalStateRecord.FEED_THRESHOLD - AnimalStateRecord.HUNGER_PER_TICK)
|
|
manager.simulate_tick()
|
|
_check(
|
|
not state.has_active_travel_target() and not goat.is_navigating() and state.needs_feed(),
|
|
"Becoming hungry should stop routine travel so NPC and player feeding target a still goat"
|
|
)
|
|
|
|
_finish()
|
|
|
|
|
|
func _wait_for_navigation_map(navigation_map: RID) -> void:
|
|
for _attempt in 30:
|
|
if (
|
|
NavigationServer3D.map_get_iteration_id(navigation_map) > 0
|
|
and not NavigationServer3D.map_get_regions(navigation_map).is_empty()
|
|
):
|
|
await physics_frame
|
|
return
|
|
await physics_frame
|
|
_check(false, "Jajce navigation map did not synchronize within 30 physics frames")
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append(message)
|
|
|
|
|
|
func _finish() -> void:
|
|
if failures.is_empty():
|
|
print("[TEST] Animal pasture/shelter routine passed")
|
|
quit(0)
|
|
return
|
|
for failure in failures:
|
|
push_error("[TEST] " + failure)
|
|
quit(1)
|