258 lines
7.7 KiB
GDScript
258 lines
7.7 KiB
GDScript
extends SceneTree
|
|
|
|
|
|
class FakeRoutineWorld:
|
|
extends Node
|
|
|
|
func get_animal_routine_sites(
|
|
species_id: StringName, animal_id: StringName
|
|
) -> Array[Dictionary]:
|
|
return [
|
|
{
|
|
"site_id": &"%s_%s_meadow" % [species_id, animal_id],
|
|
"position": Vector3(3.0 if species_id == &"goat" else -3.0, 0.0, 2.0),
|
|
}
|
|
]
|
|
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var catalog := AnimalCatalog.create_core()
|
|
_check(catalog != null and catalog.rebuild().is_empty(), "Core animal content should validate")
|
|
if catalog == null:
|
|
_finish()
|
|
return
|
|
var definition_ids := catalog.get_definition_ids()
|
|
_check(
|
|
definition_ids == [&"domestic_goat", &"domestic_sheep"],
|
|
"Animal definitions should iterate in deterministic stable-ID order"
|
|
)
|
|
var goat_definition := catalog.get_definition(&"domestic_goat")
|
|
var sheep_definition := catalog.get_definition(&"domestic_sheep")
|
|
_check(
|
|
(
|
|
goat_definition != null
|
|
and sheep_definition != null
|
|
and goat_definition.species_id == &"goat"
|
|
and sheep_definition.species_id == &"sheep"
|
|
),
|
|
"Goat and sheep should be distinct data definitions"
|
|
)
|
|
if goat_definition == null or sheep_definition == null:
|
|
_finish()
|
|
return
|
|
_check(
|
|
(
|
|
goat_definition.behavior_id == AnimalDefinition.BEHAVIOR_GRAZER_ROUTINE
|
|
and sheep_definition.behavior_id == AnimalDefinition.BEHAVIOR_GRAZER_ROUTINE
|
|
and goat_definition.presentation_cue_id == PresentationCueIds.ANIMAL_DOMESTIC_GOAT
|
|
and sheep_definition.presentation_cue_id == PresentationCueIds.ANIMAL_DOMESTIC_SHEEP
|
|
),
|
|
"Both animals should reuse one grazer behavior with stable presentation cues"
|
|
)
|
|
_check(
|
|
(
|
|
goat_definition.supports_action(&"feed_animal")
|
|
and sheep_definition.supports_action(&"feed_animal")
|
|
),
|
|
"Both data definitions should advertise the existing care action"
|
|
)
|
|
|
|
var presentation_catalog := PresentationCatalog.create_core()
|
|
_check(
|
|
presentation_catalog != null and presentation_catalog.is_valid(),
|
|
"The presentation-side catalog should validate independently"
|
|
)
|
|
if presentation_catalog == null or not presentation_catalog.is_valid():
|
|
_finish()
|
|
return
|
|
var goat_cue := presentation_catalog.get_definition(goat_definition.presentation_cue_id)
|
|
var sheep_cue := presentation_catalog.get_definition(sheep_definition.presentation_cue_id)
|
|
_check(
|
|
(
|
|
goat_cue != null
|
|
and sheep_cue != null
|
|
and goat_cue.scene_path == sheep_cue.scene_path
|
|
and goat_cue.scene_path == "res://world/animals/grazer/cozy_grazer.tscn"
|
|
),
|
|
"Presentation cues should map both data-only animals to the shared grazer scene"
|
|
)
|
|
if goat_cue == null or sheep_cue == null:
|
|
_finish()
|
|
return
|
|
var generic_scene := load(goat_cue.scene_path) as PackedScene
|
|
var generic := generic_scene.instantiate() as AnimalNode
|
|
_check(
|
|
(
|
|
generic != null
|
|
and generic.animal_id.is_empty()
|
|
and generic.animal_definition_id.is_empty()
|
|
and generic.species_id.is_empty()
|
|
and generic.display_name == "Animal"
|
|
and (generic.get_node("DebugLabel") as Label3D).text == "Animal"
|
|
),
|
|
"The reusable grazer scene should contain no Dunja-specific identity"
|
|
)
|
|
if generic != null:
|
|
generic.free()
|
|
|
|
var factory := AnimalFactory.new(catalog, presentation_catalog)
|
|
var goat := (
|
|
factory
|
|
. create_presentation(
|
|
&"domestic_goat",
|
|
&"test_goat",
|
|
"Dunja Test",
|
|
{
|
|
"initial_hunger": 10.0,
|
|
"initial_routine_site_id": &"goat_barn",
|
|
"initial_next_routine_tick": 0,
|
|
"debug_label_enabled": false,
|
|
}
|
|
)
|
|
)
|
|
var sheep := (
|
|
factory
|
|
. create_presentation(
|
|
&"domestic_sheep",
|
|
&"test_sheep",
|
|
"Mila Test",
|
|
{
|
|
"initial_hunger": 10.0,
|
|
"initial_routine_site_id": &"sheep_fold",
|
|
"initial_next_routine_tick": 0,
|
|
"debug_label_enabled": false,
|
|
}
|
|
)
|
|
)
|
|
_check(goat != null and sheep != null, "The shared factory should create both definitions")
|
|
if goat == null or sheep == null:
|
|
if goat != null:
|
|
goat.free()
|
|
if sheep != null:
|
|
sheep.free()
|
|
_finish()
|
|
return
|
|
root.add_child(goat)
|
|
root.add_child(sheep)
|
|
_check(
|
|
(
|
|
goat.get_script() == sheep.get_script()
|
|
and goat.get_node("Visual").get_script() == sheep.get_node("Visual").get_script()
|
|
),
|
|
"A second animal should add no movement or visual-controller system"
|
|
)
|
|
_check(
|
|
(
|
|
goat.get_node("Visual/HeadRoot/HornLeft").visible
|
|
and not sheep.get_node("Visual/HeadRoot/HornLeft").visible
|
|
and not sheep.get_node("Visual/HeadRoot/Beard").visible
|
|
),
|
|
"Definition data should configure bounded presentation differences"
|
|
)
|
|
|
|
var routine_world := FakeRoutineWorld.new()
|
|
root.add_child(routine_world)
|
|
var care := AnimalCareSystem.new()
|
|
care.configure(null, routine_world)
|
|
_check(care.configure_animal_catalog(catalog), "Animal care should accept the isolated catalog")
|
|
var goat_registered := care.register_node(goat)
|
|
var sheep_registered := care.register_node(sheep)
|
|
_check(
|
|
goat_registered and sheep_registered,
|
|
"Existing AnimalCareSystem should bind both shared-contract nodes"
|
|
)
|
|
var goat_state := care.get_state(&"test_goat")
|
|
var sheep_state := care.get_state(&"test_sheep")
|
|
_check(
|
|
(
|
|
goat_state != null
|
|
and sheep_state != null
|
|
and goat_state.get_animal_definition_id() == &"domestic_goat"
|
|
and sheep_state.get_animal_definition_id() == &"domestic_sheep"
|
|
and goat_state.get_script() == sheep_state.get_script()
|
|
),
|
|
"Authoritative records should retain stable animal definition IDs"
|
|
)
|
|
if goat_state != null and sheep_state != null:
|
|
var restored_sheep := AnimalStateRecord.from_dictionary(sheep_state.to_dictionary())
|
|
_check(
|
|
(
|
|
restored_sheep != null
|
|
and restored_sheep.get_animal_definition_id() == &"domestic_sheep"
|
|
and restored_sheep.get_species_id() == &"sheep"
|
|
),
|
|
"The second animal should round-trip through the existing animal record system"
|
|
)
|
|
var started: Array[StringName] = []
|
|
care.routine_started.connect(
|
|
func(animal_id: StringName, _site_id: StringName, _position: Vector3) -> void:
|
|
started.append(animal_id)
|
|
)
|
|
care.advance(0)
|
|
started.sort_custom(
|
|
func(left: StringName, right: StringName) -> bool: return String(left) < String(right)
|
|
)
|
|
_check(
|
|
(
|
|
started == [&"test_goat", &"test_sheep"]
|
|
and goat_state.has_active_travel_target()
|
|
and sheep_state.has_active_travel_target()
|
|
),
|
|
"Goat and sheep should execute the same deterministic routine scheduler"
|
|
)
|
|
_test_nested_record_migration(goat_state)
|
|
|
|
goat.free()
|
|
sheep.free()
|
|
routine_world.free()
|
|
_finish()
|
|
|
|
|
|
func _test_nested_record_migration(goat_state: AnimalStateRecord) -> void:
|
|
var previous_record := goat_state.to_dictionary()
|
|
previous_record["schema_version"] = AnimalStateRecord.ROUTINE_SCHEMA_VERSION
|
|
previous_record.erase("animal_definition_id")
|
|
var migrated := AnimalStateRecord.from_dictionary(previous_record)
|
|
_check(
|
|
(
|
|
migrated != null
|
|
and migrated.get_animal_definition_id() == &"domestic_goat"
|
|
and migrated.to_dictionary()["schema_version"] == AnimalStateRecord.SCHEMA_VERSION
|
|
),
|
|
"Nested v2 animal records should migrate to stable goat definition identity"
|
|
)
|
|
var mismatched := goat_state.to_dictionary()
|
|
mismatched["animal_definition_id"] = "domestic_sheep"
|
|
var mismatched_record := AnimalStateRecord.from_dictionary(mismatched)
|
|
_check(
|
|
(
|
|
mismatched_record != null
|
|
and not AnimalCatalog.create_core().matches_identity(
|
|
mismatched_record.get_animal_definition_id(), mismatched_record.get_species_id()
|
|
)
|
|
),
|
|
"Data-only restore should remain extensible while the catalog detects identity mismatches"
|
|
)
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append(message)
|
|
|
|
|
|
func _finish() -> void:
|
|
if failures.is_empty():
|
|
print("[TEST] Shared animal definition contract passed")
|
|
quit(0)
|
|
return
|
|
for failure in failures:
|
|
push_error("[TEST] " + failure)
|
|
quit(1)
|