291 lines
9.7 KiB
GDScript
291 lines
9.7 KiB
GDScript
extends SceneTree
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
_test_core_pack()
|
|
_test_deterministic_enumeration_and_dependencies()
|
|
_test_duplicate_ids_fail_without_overrides()
|
|
_test_unknown_references_categories_and_handlers()
|
|
_finish()
|
|
|
|
|
|
func _test_core_pack() -> void:
|
|
var catalog := ContentCatalog.create_core()
|
|
_check(catalog.is_valid(), "Core pack should validate: %s" % [catalog.get_errors()])
|
|
_check(
|
|
(
|
|
_action_ids(catalog.get_actions())
|
|
== [
|
|
&"defend",
|
|
&"deposit_food",
|
|
&"deposit_wood",
|
|
&"eat",
|
|
&"feed_animal",
|
|
&"gather_food",
|
|
&"gather_wood",
|
|
&"patrol",
|
|
&"rest",
|
|
&"sleep",
|
|
&"study",
|
|
&"wander",
|
|
&"withdraw_food",
|
|
]
|
|
),
|
|
"Core actions should enumerate by stable ID"
|
|
)
|
|
_check(
|
|
(
|
|
_profession_ids(catalog.get_professions())
|
|
== [&"farmer", &"guard", &"scholar", &"wanderer", &"woodcutter"]
|
|
),
|
|
"Core professions should enumerate by stable ID"
|
|
)
|
|
_check(
|
|
_item_ids(catalog.get_items()) == [&"food", &"herb", &"item_claw", &"item_sword", &"wood"],
|
|
"Core items should include legacy items and herbs in stable-ID order"
|
|
)
|
|
_check(
|
|
_enemy_ids(catalog.get_enemies()) == [&"enemy_boar", &"enemy_raider", &"enemy_wolf"],
|
|
"Core enemies should enumerate by stable ID"
|
|
)
|
|
var food := catalog.get_item(SimulationIds.RESOURCE_FOOD)
|
|
var wood := catalog.get_item(SimulationIds.RESOURCE_WOOD)
|
|
var sword := catalog.get_item(SimulationIds.ITEM_SWORD)
|
|
var claw := catalog.get_item(SimulationIds.ITEM_CLAW)
|
|
_check(food != null and food.is_resource(), "Food should be a resource item")
|
|
_check(wood != null and wood.is_resource(), "Wood should be a resource item")
|
|
_check(
|
|
sword != null and sword.is_weapon() and is_equal_approx(sword.damage, 24.0),
|
|
"Sword should preserve its weapon contract"
|
|
)
|
|
_check(
|
|
claw != null and claw.is_weapon() and is_equal_approx(claw.reach, 1.2),
|
|
"Claw should preserve its weapon contract"
|
|
)
|
|
var wolf := catalog.get_enemy(&"enemy_wolf")
|
|
var boar := catalog.get_enemy(&"enemy_boar")
|
|
_check(
|
|
wolf != null and wolf.weapon_id == SimulationIds.ITEM_CLAW,
|
|
"Enemy references should resolve against catalog items"
|
|
)
|
|
_check(
|
|
boar != null and boar.behavior_profile_id == wolf.behavior_profile_id,
|
|
"New enemy types should be able to reuse an existing behavior profile"
|
|
)
|
|
|
|
|
|
func _test_deterministic_enumeration_and_dependencies() -> void:
|
|
var core := load(ContentCatalog.CORE_PACK_PATH) as SimulationContentPack
|
|
var addon := _pack(&"addon", "Addon")
|
|
addon.required_pack_ids = [&"core"]
|
|
addon.required_handler_ids = [&"test_handler"]
|
|
addon.items = [_resource_item(&"amber", "Amber")]
|
|
var packs: Array[SimulationContentPack] = [addon, core]
|
|
var catalog := ContentCatalog.new()
|
|
var handlers: Array[StringName] = [&"test_handler"]
|
|
var errors := catalog.rebuild(packs, handlers)
|
|
_check(errors.is_empty(), "Known pack and handler dependencies should validate: %s" % [errors])
|
|
_check(
|
|
_pack_ids(catalog.get_packs()) == [&"addon", &"core"],
|
|
"Pack enumeration should not depend on input order"
|
|
)
|
|
_check(
|
|
(
|
|
_item_ids(catalog.get_items())
|
|
== [&"amber", &"food", &"herb", &"item_claw", &"item_sword", &"wood"]
|
|
),
|
|
"Merged item enumeration should be globally stable-ID sorted"
|
|
)
|
|
var returned_items := catalog.get_items()
|
|
returned_items.clear()
|
|
_check(
|
|
catalog.get_items().size() == 6,
|
|
"Callers should not be able to mutate the catalog's ordered arrays"
|
|
)
|
|
|
|
|
|
func _test_duplicate_ids_fail_without_overrides() -> void:
|
|
var first := _pack(&"first", "First")
|
|
first.items = [_resource_item(&"shared_item", "First value")]
|
|
var second := _pack(&"second", "Second")
|
|
second.items = [_resource_item(&"shared_item", "Second value")]
|
|
var packs: Array[SimulationContentPack] = [second, first]
|
|
var catalog := ContentCatalog.new()
|
|
var errors := catalog.rebuild(packs)
|
|
_check(_has_error(errors, "Duplicate item_id 'shared_item'"), "Duplicate item IDs should fail")
|
|
_check(
|
|
(
|
|
not catalog.is_valid()
|
|
and catalog.get_item(&"shared_item") == null
|
|
and catalog.get_items().is_empty()
|
|
),
|
|
"An invalid build should publish neither duplicate value nor partial content"
|
|
)
|
|
|
|
var duplicate_pack := _pack(&"first", "Duplicate pack")
|
|
var duplicate_packs: Array[SimulationContentPack] = [first, duplicate_pack]
|
|
errors = catalog.rebuild(duplicate_packs)
|
|
_check(_has_error(errors, "Duplicate pack_id 'first'"), "Duplicate pack IDs should fail")
|
|
|
|
|
|
func _test_unknown_references_categories_and_handlers() -> void:
|
|
var broken := _pack(&"broken", "Broken")
|
|
broken.required_pack_ids = [&"missing_pack"]
|
|
broken.required_handler_ids = [&"missing_handler"]
|
|
var invalid_item := _resource_item(&"strange_item", "Strange")
|
|
invalid_item.category = &"unknown_category"
|
|
var ordinary_resource := _resource_item(&"ordinary_resource", "Ordinary resource")
|
|
var missing_profession := _action(&"missing_profession")
|
|
missing_profession.preferred_profession_id = &"unknown_profession"
|
|
var missing_action := _action(&"missing_action")
|
|
missing_action.target_type = SimulationIds.TARGET_RESOURCE
|
|
missing_action.resource_action_id = &"unknown_resource_action"
|
|
var missing_cost_item := _action(&"missing_cost")
|
|
missing_cost_item.completion_cost_resource_id = &"unknown_cost_item"
|
|
missing_cost_item.completion_cost_amount = 1.0
|
|
missing_cost_item.selection_policy_id = &"unknown_selection"
|
|
missing_cost_item.target_policy_id = &"unknown_targeting"
|
|
missing_cost_item.completion_handler_id = &"unknown_completion"
|
|
var invalid_effect := ActionEffect.new()
|
|
invalid_effect.effect_id = &"broken_transfer"
|
|
invalid_effect.effect_kind = ActionEffect.KIND_INVENTORY_TRANSFER
|
|
missing_cost_item.effects = [invalid_effect]
|
|
var weapon_item := _weapon_item(&"test_weapon", "Test weapon")
|
|
var weapon_cost := _action(&"weapon_cost")
|
|
weapon_cost.completion_cost_resource_id = weapon_item.item_id
|
|
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)
|
|
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]
|
|
var packs: Array[SimulationContentPack] = [broken]
|
|
var known_handlers: Array[StringName] = [&"known_handler"]
|
|
var catalog := ContentCatalog.new()
|
|
var errors := catalog.rebuild(packs, known_handlers)
|
|
for expected in [
|
|
"unknown required pack 'missing_pack'",
|
|
"unknown required handler 'missing_handler'",
|
|
"category 'unknown_category' is invalid",
|
|
"unknown profession 'unknown_profession'",
|
|
"unknown resource action 'unknown_resource_action'",
|
|
"unknown completion cost item 'unknown_cost_item'",
|
|
"unknown selection policy 'unknown_selection'",
|
|
"unknown target policy 'unknown_targeting'",
|
|
"unknown completion handler 'unknown_completion'",
|
|
"needs an item and positive amount",
|
|
"needs source/destination roles",
|
|
"non-resource completion cost item 'test_weapon'",
|
|
"unknown weapon 'unknown_weapon'",
|
|
"non-weapon item 'ordinary_resource'",
|
|
]:
|
|
_check(_has_error(errors, expected), "Catalog should report %s: %s" % [expected, errors])
|
|
_check(not catalog.is_valid(), "Unknown references should invalidate the complete catalog")
|
|
|
|
|
|
func _pack(pack_id: StringName, display_name: String) -> SimulationContentPack:
|
|
var pack := SimulationContentPack.new()
|
|
pack.pack_id = pack_id
|
|
pack.display_name = display_name
|
|
return pack
|
|
|
|
|
|
func _resource_item(item_id: StringName, display_name: String) -> ItemDefinition:
|
|
var item := ItemDefinition.new()
|
|
item.item_id = item_id
|
|
item.display_name = display_name
|
|
item.category = ItemDefinition.CATEGORY_RESOURCE
|
|
return item
|
|
|
|
|
|
func _weapon_item(item_id: StringName, display_name: String) -> ItemDefinition:
|
|
var item := ItemDefinition.new()
|
|
item.item_id = item_id
|
|
item.display_name = display_name
|
|
item.category = ItemDefinition.CATEGORY_WEAPON
|
|
item.equip_slot = SimulationIds.EQUIP_SLOT_HAND
|
|
item.damage = 1.0
|
|
item.reach = 1.0
|
|
item.attack_cooldown = 1.0
|
|
return item
|
|
|
|
|
|
func _action(action_id: StringName) -> ActionDefinition:
|
|
var action := ActionDefinition.new()
|
|
action.action_id = action_id
|
|
action.display_name = String(action_id).capitalize()
|
|
action.target_type = SimulationIds.TARGET_ACTIVITY
|
|
return action
|
|
|
|
|
|
func _enemy(enemy_id: StringName, weapon_id: StringName) -> EnemyDefinition:
|
|
var enemy := EnemyDefinition.new()
|
|
enemy.enemy_id = enemy_id
|
|
enemy.display_name = String(enemy_id).capitalize()
|
|
enemy.kind = SimulationIds.COMBATANT_KIND_RAIDER
|
|
enemy.weapon_id = weapon_id
|
|
return enemy
|
|
|
|
|
|
func _pack_ids(packs: Array[SimulationContentPack]) -> Array[StringName]:
|
|
var ids: Array[StringName] = []
|
|
for pack in packs:
|
|
ids.append(pack.pack_id)
|
|
return ids
|
|
|
|
|
|
func _action_ids(actions: Array[ActionDefinition]) -> Array[StringName]:
|
|
var ids: Array[StringName] = []
|
|
for action in actions:
|
|
ids.append(action.action_id)
|
|
return ids
|
|
|
|
|
|
func _profession_ids(professions: Array[ProfessionDefinition]) -> Array[StringName]:
|
|
var ids: Array[StringName] = []
|
|
for profession in professions:
|
|
ids.append(profession.profession_id)
|
|
return ids
|
|
|
|
|
|
func _item_ids(items: Array[ItemDefinition]) -> Array[StringName]:
|
|
var ids: Array[StringName] = []
|
|
for item in items:
|
|
ids.append(item.item_id)
|
|
return ids
|
|
|
|
|
|
func _enemy_ids(enemies: Array[EnemyDefinition]) -> Array[StringName]:
|
|
var ids: Array[StringName] = []
|
|
for enemy in enemies:
|
|
ids.append(enemy.enemy_id)
|
|
return ids
|
|
|
|
|
|
func _has_error(errors: Array[String], fragment: String) -> bool:
|
|
for error in errors:
|
|
if fragment in error:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append(message)
|
|
|
|
|
|
func _finish() -> void:
|
|
if failures.is_empty():
|
|
print("[TEST] Content catalog passed")
|
|
quit(0)
|
|
return
|
|
for failure in failures:
|
|
push_error("[TEST] " + failure)
|
|
quit(1)
|