feat: define reusable animal archetypes

This commit is contained in:
Rijad Zuzo
2026-08-12 20:50:52 +02:00
parent d5eb2b81f9
commit 4e4ae1a969
20 changed files with 1141 additions and 411 deletions
+234
View File
@@ -0,0 +1,234 @@
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_scene_path == sheep_definition.presentation_scene_path
),
"Both animals should reuse one grazer behavior and presentation contract"
)
_check(
(
goat_definition.supports_action(&"feed_animal")
and sheep_definition.supports_action(&"feed_animal")
),
"Both data definitions should advertise the existing care action"
)
var generic_scene := load(goat_definition.presentation_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)
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)
@@ -0,0 +1 @@
uid://dad5kvxbeg72u