From 8707942c75e151bc3102f58c9050dcdb9202723a Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Wed, 12 Aug 2026 21:19:04 +0200 Subject: [PATCH] refactor: unify animal content packs --- docs/SIMULATION_DEFINITIONS.md | 7 +-- simulation/animals/AnimalCatalog.gd | 12 ++--- simulation/definitions/ContentCatalog.gd | 54 +++++++++++++++++++ .../definitions/SimulationContentPack.gd | 11 ++++ simulation/definitions/packs/core.tres | 5 +- tests/content_catalog_test.gd | 35 ++++++++++++ 6 files changed, 114 insertions(+), 10 deletions(-) diff --git a/docs/SIMULATION_DEFINITIONS.md b/docs/SIMULATION_DEFINITIONS.md index cee325b..48b1a98 100644 --- a/docs/SIMULATION_DEFINITIONS.md +++ b/docs/SIMULATION_DEFINITIONS.md @@ -113,9 +113,10 @@ item table. definition-instance-state-presentation contract to domestic animals. Goat and sheep reuse the `grazer_routine`, feeding capability, generic CozyGrazer scene, state migration, and care system; definition and visual parameters are the only -type-specific pieces. The animal catalog remains isolated until the next -shared content-pack migration adds that record family without destabilizing -the already validated core pack. +type-specific pieces. Both definitions now live in the shared core content +pack; `AnimalCatalog.create_core()` is a typed compatibility view over +`ContentCatalog`, so animal content cannot drift into a second unvalidated +manifest. ## Situations and dialogue diff --git a/simulation/animals/AnimalCatalog.gd b/simulation/animals/AnimalCatalog.gd index a5a60e0..e0dc399 100644 --- a/simulation/animals/AnimalCatalog.gd +++ b/simulation/animals/AnimalCatalog.gd @@ -1,8 +1,6 @@ class_name AnimalCatalog extends Resource -const CORE_CATALOG_PATH := "res://simulation/animals/definitions/core_animals.tres" - @export var animals: Array[AnimalDefinition] = [] var _definitions_by_id: Dictionary = {} @@ -11,10 +9,12 @@ var _errors: Array[String] = [] static func create_core() -> AnimalCatalog: - var catalog := load(CORE_CATALOG_PATH) as AnimalCatalog - if catalog != null: - catalog.rebuild() - return catalog + var content := ContentCatalog.create_core() + if content == null or not content.is_valid(): + return null + var catalog := AnimalCatalog.new() + catalog.animals = content.get_animals() + return catalog if catalog.rebuild().is_empty() else null func rebuild() -> Array[String]: diff --git a/simulation/definitions/ContentCatalog.gd b/simulation/definitions/ContentCatalog.gd index 0634cb8..7bfc256 100644 --- a/simulation/definitions/ContentCatalog.gd +++ b/simulation/definitions/ContentCatalog.gd @@ -13,6 +13,7 @@ var _items: Array[ItemDefinition] = [] var _resources: Array[ResourceDefinition] = [] var _storages: Array[StorageDefinition] = [] var _enemies: Array[EnemyDefinition] = [] +var _animals: Array[AnimalDefinition] = [] var _packs_by_id: Dictionary = {} var _actions_by_id: Dictionary = {} var _professions_by_id: Dictionary = {} @@ -21,6 +22,7 @@ var _items_by_id: Dictionary = {} var _resources_by_id: Dictionary = {} var _storages_by_id: Dictionary = {} var _enemies_by_id: Dictionary = {} +var _animals_by_id: Dictionary = {} static func create_core( @@ -76,6 +78,7 @@ func rebuild( var resources_by_id := {} var storages_by_id := {} var enemies_by_id := {} + var animals_by_id := {} var actions: Array[ActionDefinition] = [] var professions: Array[ProfessionDefinition] = [] var capability_tags: Array[CapabilityTagDefinition] = [] @@ -83,6 +86,7 @@ func rebuild( var resources: Array[ResourceDefinition] = [] var storages: Array[StorageDefinition] = [] var enemies: Array[EnemyDefinition] = [] + var animals: Array[AnimalDefinition] = [] for pack in pack_list: if pack == null: @@ -101,6 +105,7 @@ func rebuild( _collect_resources(pack, resources, resources_by_id, errors) _collect_storages(pack, storages, storages_by_id, errors) _collect_enemies(pack, enemies, enemies_by_id, errors) + _collect_animals(pack, animals, animals_by_id, errors) for pack in pack_list: if pack == null: @@ -143,6 +148,7 @@ func rebuild( storages, items_by_id, actions_by_id, capability_tags_by_id, supported_cues, errors ) _validate_enemy_references(enemies, items_by_id, errors) + _validate_animal_references(animals, actions_by_id, errors) errors.sort() _errors = errors if not errors.is_empty(): @@ -155,6 +161,7 @@ func rebuild( resources.sort_custom(_sort_resources) storages.sort_custom(_sort_storages) enemies.sort_custom(_sort_enemies) + animals.sort_custom(_sort_animals) _packs = pack_list _actions = actions _professions = professions @@ -163,6 +170,7 @@ func rebuild( _resources = resources _storages = storages _enemies = enemies + _animals = animals _packs_by_id = packs_by_id _actions_by_id = actions_by_id _professions_by_id = professions_by_id @@ -171,6 +179,7 @@ func rebuild( _resources_by_id = resources_by_id _storages_by_id = storages_by_id _enemies_by_id = enemies_by_id + _animals_by_id = animals_by_id _valid = true return [] @@ -215,6 +224,10 @@ func get_enemies() -> Array[EnemyDefinition]: return _enemies.duplicate() +func get_animals() -> Array[AnimalDefinition]: + return _animals.duplicate() + + func get_pack(pack_id: StringName) -> SimulationContentPack: return _packs_by_id.get(pack_id) as SimulationContentPack @@ -247,6 +260,10 @@ func get_enemy(enemy_id: StringName) -> EnemyDefinition: return _enemies_by_id.get(enemy_id) as EnemyDefinition +func get_animal(animal_definition_id: StringName) -> AnimalDefinition: + return _animals_by_id.get(animal_definition_id) as AnimalDefinition + + func get_authoring_report() -> Dictionary: return { "valid": _valid, @@ -259,6 +276,7 @@ func get_authoring_report() -> Dictionary: "resources": _ids_for(_resources, &"resource_id"), "storages": _ids_for(_storages, &"storage_id"), "enemies": _ids_for(_enemies, &"enemy_id"), + "animals": _ids_for(_animals, &"animal_definition_id"), } @@ -273,6 +291,7 @@ func _clear_published_content() -> void: _resources.clear() _storages.clear() _enemies.clear() + _animals.clear() _packs_by_id.clear() _actions_by_id.clear() _professions_by_id.clear() @@ -281,6 +300,7 @@ func _clear_published_content() -> void: _resources_by_id.clear() _storages_by_id.clear() _enemies_by_id.clear() + _animals_by_id.clear() static func _collect_actions( @@ -395,6 +415,22 @@ static func _collect_enemies( definitions.append(definition) +static func _collect_animals( + pack: SimulationContentPack, + definitions: Array[AnimalDefinition], + definitions_by_id: Dictionary, + errors: Array[String] +) -> void: + for definition in pack.animals: + if definition == null: + continue + if definitions_by_id.has(definition.animal_definition_id): + errors.append("Duplicate animal_definition_id '%s'" % definition.animal_definition_id) + continue + definitions_by_id[definition.animal_definition_id] = definition + definitions.append(definition) + + static func _validate_action_references( actions: Array[ActionDefinition], actions_by_id: Dictionary, @@ -671,6 +707,20 @@ static func _validate_enemy_references( ) +static func _validate_animal_references( + animals: Array[AnimalDefinition], actions_by_id: Dictionary, errors: Array[String] +) -> void: + for definition in animals: + for action_id in definition.supported_action_ids: + if not actions_by_id.has(action_id): + errors.append( + ( + "Animal '%s' references unknown action '%s'" + % [definition.animal_definition_id, action_id] + ) + ) + + static func _sort_packs(first: SimulationContentPack, second: SimulationContentPack) -> bool: if first == null: return second != null @@ -709,6 +759,10 @@ static func _sort_enemies(first: EnemyDefinition, second: EnemyDefinition) -> bo return String(first.enemy_id) < String(second.enemy_id) +static func _sort_animals(first: AnimalDefinition, second: AnimalDefinition) -> bool: + return String(first.animal_definition_id) < String(second.animal_definition_id) + + static func _ids_for(definitions: Array, id_property: StringName) -> Array[StringName]: var ids: Array[StringName] = [] for definition in definitions: diff --git a/simulation/definitions/SimulationContentPack.gd b/simulation/definitions/SimulationContentPack.gd index 9b700aa..9537bd8 100644 --- a/simulation/definitions/SimulationContentPack.gd +++ b/simulation/definitions/SimulationContentPack.gd @@ -12,6 +12,7 @@ extends Resource @export var resources: Array[ResourceDefinition] = [] @export var storages: Array[StorageDefinition] = [] @export var enemies: Array[EnemyDefinition] = [] +@export var animals: Array[AnimalDefinition] = [] func validate() -> Array[String]: @@ -31,6 +32,7 @@ func validate() -> Array[String]: _validate_resources(errors) _validate_storages(errors) _validate_enemies(errors) + _validate_animals(errors) return errors @@ -97,6 +99,15 @@ func _validate_enemies(errors: Array[String]) -> void: errors.append("EnemyDefinition: " + error) +func _validate_animals(errors: Array[String]) -> void: + for definition in animals: + if definition == null: + errors.append("animals contains a null definition") + continue + for error in definition.validate(): + errors.append("AnimalDefinition: " + error) + + static func _validate_dependency_ids( ids: Array[StringName], label: String, errors: Array[String] ) -> void: diff --git a/simulation/definitions/packs/core.tres b/simulation/definitions/packs/core.tres index 2722f24..2f6511d 100644 --- a/simulation/definitions/packs/core.tres +++ b/simulation/definitions/packs/core.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="SimulationContentPack" load_steps=40 format=3] +[gd_resource type="Resource" script_class="SimulationContentPack" load_steps=42 format=3] [ext_resource type="Script" path="res://simulation/definitions/SimulationContentPack.gd" id="1_pack"] [ext_resource type="Resource" path="res://simulation/definitions/actions/defend.tres" id="2_defend"] @@ -38,6 +38,8 @@ [ext_resource type="Resource" path="res://simulation/definitions/storages/apothecary.tres" id="36_apothecary"] [ext_resource type="Resource" path="res://simulation/definitions/storages/pantry.tres" id="37_pantry"] [ext_resource type="Resource" path="res://simulation/definitions/storages/woodpile.tres" id="38_woodpile"] +[ext_resource type="Resource" path="res://simulation/animals/definitions/domestic_goat.tres" id="39_goat"] +[ext_resource type="Resource" path="res://simulation/animals/definitions/domestic_sheep.tres" id="40_sheep"] [resource] script = ExtResource("1_pack") @@ -50,3 +52,4 @@ items = [ExtResource("20_claw"), ExtResource("21_food"), ExtResource("22_sword") resources = [ExtResource("33_berry_patch"), ExtResource("34_herb_patch"), ExtResource("35_tree")] storages = [ExtResource("36_apothecary"), ExtResource("37_pantry"), ExtResource("38_woodpile")] enemies = [ExtResource("24_raider"), ExtResource("25_wolf"), ExtResource("26_boar")] +animals = [ExtResource("39_goat"), ExtResource("40_sheep")] diff --git a/tests/content_catalog_test.gd b/tests/content_catalog_test.gd index 0d14138..bacbae8 100644 --- a/tests/content_catalog_test.gd +++ b/tests/content_catalog_test.gd @@ -54,6 +54,10 @@ func _test_core_pack() -> void: _enemy_ids(catalog.get_enemies()) == [&"enemy_boar", &"enemy_raider", &"enemy_wolf"], "Core enemies should enumerate by stable ID" ) + _check( + _animal_ids(catalog.get_animals()) == [&"domestic_goat", &"domestic_sheep"], + "Core animals should enumerate through the same content pack" + ) var food := catalog.get_item(SimulationIds.RESOURCE_FOOD) var wood := catalog.get_item(SimulationIds.RESOURCE_WOOD) var sword := catalog.get_item(SimulationIds.ITEM_SWORD) @@ -133,6 +137,17 @@ func _test_duplicate_ids_fail_without_overrides() -> void: errors = catalog.rebuild(duplicate_packs) _check(_has_error(errors, "Duplicate pack_id 'first'"), "Duplicate pack IDs should fail") + first = _pack(&"first", "First") + second = _pack(&"second", "Second") + first.animals = [_animal(&"shared_animal", &"feed_animal")] + second.animals = [_animal(&"shared_animal", &"feed_animal")] + var duplicate_animal_packs: Array[SimulationContentPack] = [second, first] + errors = catalog.rebuild(duplicate_animal_packs) + _check( + _has_error(errors, "Duplicate animal_definition_id 'shared_animal'"), + "Duplicate animal definitions should fail without an override" + ) + func _test_unknown_references_categories_and_handlers() -> void: var broken := _pack(&"broken", "Broken") @@ -162,9 +177,11 @@ func _test_unknown_references_categories_and_handlers() -> void: weapon_cost.completion_cost_amount = 1.0 var missing_weapon := _enemy(&"missing_weapon_enemy", &"unknown_weapon") var non_weapon := _enemy(&"non_weapon_enemy", ordinary_resource.item_id) + var missing_animal_action := _animal(&"unsupported_animal", &"unknown_animal_action") broken.items = [invalid_item, ordinary_resource, weapon_item] broken.actions = [missing_profession, missing_action, missing_cost_item, weapon_cost] broken.enemies = [missing_weapon, non_weapon] + broken.animals = [missing_animal_action] var packs: Array[SimulationContentPack] = [broken] var known_handlers: Array[StringName] = [&"known_handler"] var catalog := ContentCatalog.new() @@ -184,6 +201,7 @@ func _test_unknown_references_categories_and_handlers() -> void: "non-resource completion cost item 'test_weapon'", "unknown weapon 'unknown_weapon'", "non-weapon item 'ordinary_resource'", + "Animal 'unsupported_animal' references unknown action 'unknown_animal_action'", ]: _check(_has_error(errors, expected), "Catalog should report %s: %s" % [expected, errors]) _check(not catalog.is_valid(), "Unknown references should invalidate the complete catalog") @@ -233,6 +251,16 @@ func _enemy(enemy_id: StringName, weapon_id: StringName) -> EnemyDefinition: return enemy +func _animal(animal_definition_id: StringName, action_id: StringName) -> AnimalDefinition: + var animal := AnimalDefinition.new() + animal.animal_definition_id = animal_definition_id + animal.species_id = &"test_species" + animal.display_name = String(animal_definition_id).capitalize() + animal.supported_action_ids = [action_id] + animal.presentation_scene_path = "res://world/animals/grazer/cozy_grazer.tscn" + return animal + + func _pack_ids(packs: Array[SimulationContentPack]) -> Array[StringName]: var ids: Array[StringName] = [] for pack in packs: @@ -268,6 +296,13 @@ func _enemy_ids(enemies: Array[EnemyDefinition]) -> Array[StringName]: return ids +func _animal_ids(animals: Array[AnimalDefinition]) -> Array[StringName]: + var ids: Array[StringName] = [] + for animal in animals: + ids.append(animal.animal_definition_id) + return ids + + func _has_error(errors: Array[String], fragment: String) -> bool: for error in errors: if fragment in error: