feat: add emergent world foundations

This commit is contained in:
Rijad Zuzo
2026-08-12 20:06:51 +02:00
parent fa04137a04
commit ea00f60165
44 changed files with 2665 additions and 7 deletions
+340
View File
@@ -0,0 +1,340 @@
class_name ContentCatalog
extends RefCounted
const CORE_PACK_PATH := "res://simulation/definitions/packs/core.tres"
var _valid := false
var _errors: Array[String] = []
var _packs: Array[SimulationContentPack] = []
var _actions: Array[ActionDefinition] = []
var _professions: Array[ProfessionDefinition] = []
var _items: Array[ItemDefinition] = []
var _enemies: Array[EnemyDefinition] = []
var _packs_by_id: Dictionary = {}
var _actions_by_id: Dictionary = {}
var _professions_by_id: Dictionary = {}
var _items_by_id: Dictionary = {}
var _enemies_by_id: Dictionary = {}
static func create_core(supported_handler_ids: Array[StringName] = []) -> ContentCatalog:
var catalog := ContentCatalog.new()
var pack := load(CORE_PACK_PATH) as SimulationContentPack
if pack == null:
catalog._errors = ["Core content pack failed to load from '%s'" % CORE_PACK_PATH]
return catalog
var packs: Array[SimulationContentPack] = [pack]
catalog.rebuild(packs, supported_handler_ids)
return catalog
func rebuild(
content_packs: Array[SimulationContentPack], supported_handler_ids: Array[StringName] = []
) -> Array[String]:
_clear_published_content()
var errors: Array[String] = []
var pack_list := content_packs.duplicate()
pack_list.sort_custom(_sort_packs)
var supported_handlers := {}
for handler_id in supported_handler_ids:
if handler_id.is_empty():
errors.append("Supported handler ID is empty")
elif supported_handlers.has(handler_id):
errors.append("Duplicate supported handler '%s'" % handler_id)
else:
supported_handlers[handler_id] = true
var packs_by_id := {}
var actions_by_id := {}
var professions_by_id := {}
var items_by_id := {}
var enemies_by_id := {}
var actions: Array[ActionDefinition] = []
var professions: Array[ProfessionDefinition] = []
var items: Array[ItemDefinition] = []
var enemies: Array[EnemyDefinition] = []
for pack in pack_list:
if pack == null:
errors.append("Content pack list contains a null pack")
continue
for error in pack.validate():
errors.append("Pack '%s': %s" % [pack.pack_id, error])
if packs_by_id.has(pack.pack_id):
errors.append("Duplicate pack_id '%s'" % pack.pack_id)
else:
packs_by_id[pack.pack_id] = pack
_collect_actions(pack, actions, actions_by_id, errors)
_collect_professions(pack, professions, professions_by_id, errors)
_collect_items(pack, items, items_by_id, errors)
_collect_enemies(pack, enemies, enemies_by_id, errors)
for pack in pack_list:
if pack == null:
continue
for required_pack_id in pack.required_pack_ids:
if not required_pack_id.is_empty() and not packs_by_id.has(required_pack_id):
errors.append(
(
"Pack '%s' references unknown required pack '%s'"
% [pack.pack_id, required_pack_id]
)
)
for required_handler_id in pack.required_handler_ids:
if (
not required_handler_id.is_empty()
and not supported_handlers.has(required_handler_id)
):
errors.append(
(
"Pack '%s' references unknown required handler '%s'"
% [pack.pack_id, required_handler_id]
)
)
_validate_action_references(actions, actions_by_id, professions_by_id, items_by_id, errors)
_validate_enemy_references(enemies, items_by_id, errors)
errors.sort()
_errors = errors
if not errors.is_empty():
return get_errors()
actions.sort_custom(_sort_actions)
professions.sort_custom(_sort_professions)
items.sort_custom(_sort_items)
enemies.sort_custom(_sort_enemies)
_packs = pack_list
_actions = actions
_professions = professions
_items = items
_enemies = enemies
_packs_by_id = packs_by_id
_actions_by_id = actions_by_id
_professions_by_id = professions_by_id
_items_by_id = items_by_id
_enemies_by_id = enemies_by_id
_valid = true
return []
func is_valid() -> bool:
return _valid
func get_errors() -> Array[String]:
return _errors.duplicate()
func get_packs() -> Array[SimulationContentPack]:
return _packs.duplicate()
func get_actions() -> Array[ActionDefinition]:
return _actions.duplicate()
func get_professions() -> Array[ProfessionDefinition]:
return _professions.duplicate()
func get_items() -> Array[ItemDefinition]:
return _items.duplicate()
func get_enemies() -> Array[EnemyDefinition]:
return _enemies.duplicate()
func get_pack(pack_id: StringName) -> SimulationContentPack:
return _packs_by_id.get(pack_id) as SimulationContentPack
func get_action(action_id: StringName) -> ActionDefinition:
return _actions_by_id.get(action_id) as ActionDefinition
func get_profession(profession_id: StringName) -> ProfessionDefinition:
return _professions_by_id.get(profession_id) as ProfessionDefinition
func get_item(item_id: StringName) -> ItemDefinition:
return _items_by_id.get(item_id) as ItemDefinition
func get_enemy(enemy_id: StringName) -> EnemyDefinition:
return _enemies_by_id.get(enemy_id) as EnemyDefinition
func _clear_published_content() -> void:
_valid = false
_errors.clear()
_packs.clear()
_actions.clear()
_professions.clear()
_items.clear()
_enemies.clear()
_packs_by_id.clear()
_actions_by_id.clear()
_professions_by_id.clear()
_items_by_id.clear()
_enemies_by_id.clear()
static func _collect_actions(
pack: SimulationContentPack,
definitions: Array[ActionDefinition],
definitions_by_id: Dictionary,
errors: Array[String]
) -> void:
for definition in pack.actions:
if definition == null:
continue
if definitions_by_id.has(definition.action_id):
errors.append("Duplicate action_id '%s'" % definition.action_id)
continue
definitions_by_id[definition.action_id] = definition
definitions.append(definition)
static func _collect_professions(
pack: SimulationContentPack,
definitions: Array[ProfessionDefinition],
definitions_by_id: Dictionary,
errors: Array[String]
) -> void:
for definition in pack.professions:
if definition == null:
continue
if definitions_by_id.has(definition.profession_id):
errors.append("Duplicate profession_id '%s'" % definition.profession_id)
continue
definitions_by_id[definition.profession_id] = definition
definitions.append(definition)
static func _collect_items(
pack: SimulationContentPack,
definitions: Array[ItemDefinition],
definitions_by_id: Dictionary,
errors: Array[String]
) -> void:
for definition in pack.items:
if definition == null:
continue
if definitions_by_id.has(definition.item_id):
errors.append("Duplicate item_id '%s'" % definition.item_id)
continue
definitions_by_id[definition.item_id] = definition
definitions.append(definition)
static func _collect_enemies(
pack: SimulationContentPack,
definitions: Array[EnemyDefinition],
definitions_by_id: Dictionary,
errors: Array[String]
) -> void:
for definition in pack.enemies:
if definition == null:
continue
if definitions_by_id.has(definition.enemy_id):
errors.append("Duplicate enemy_id '%s'" % definition.enemy_id)
continue
definitions_by_id[definition.enemy_id] = definition
definitions.append(definition)
static func _validate_action_references(
actions: Array[ActionDefinition],
actions_by_id: Dictionary,
professions_by_id: Dictionary,
items_by_id: Dictionary,
errors: Array[String]
) -> void:
for definition in actions:
if (
not definition.preferred_profession_id.is_empty()
and not professions_by_id.has(definition.preferred_profession_id)
):
errors.append(
(
"Action '%s' references unknown profession '%s'"
% [definition.action_id, definition.preferred_profession_id]
)
)
if (
definition.target_type == SimulationIds.TARGET_RESOURCE
and not actions_by_id.has(definition.resource_action_id)
):
errors.append(
(
"Action '%s' references unknown resource action '%s'"
% [definition.action_id, definition.resource_action_id]
)
)
if (
not definition.completion_cost_resource_id.is_empty()
and not items_by_id.has(definition.completion_cost_resource_id)
):
errors.append(
(
"Action '%s' references unknown completion cost item '%s'"
% [definition.action_id, definition.completion_cost_resource_id]
)
)
elif not definition.completion_cost_resource_id.is_empty():
var cost_item := (
items_by_id.get(definition.completion_cost_resource_id) as ItemDefinition
)
if cost_item != null and not cost_item.is_resource():
errors.append(
(
"Action '%s' references non-resource completion cost item '%s'"
% [definition.action_id, definition.completion_cost_resource_id]
)
)
static func _validate_enemy_references(
enemies: Array[EnemyDefinition], items_by_id: Dictionary, errors: Array[String]
) -> void:
for definition in enemies:
var weapon := items_by_id.get(definition.weapon_id) as ItemDefinition
if weapon == null:
errors.append(
(
"Enemy '%s' references unknown weapon '%s'"
% [definition.enemy_id, definition.weapon_id]
)
)
elif not weapon.is_weapon():
errors.append(
(
"Enemy '%s' references non-weapon item '%s'"
% [definition.enemy_id, definition.weapon_id]
)
)
static func _sort_packs(first: SimulationContentPack, second: SimulationContentPack) -> bool:
if first == null:
return second != null
if second == null:
return false
return String(first.pack_id) < String(second.pack_id)
static func _sort_actions(first: ActionDefinition, second: ActionDefinition) -> bool:
return String(first.action_id) < String(second.action_id)
static func _sort_professions(first: ProfessionDefinition, second: ProfessionDefinition) -> bool:
return String(first.profession_id) < String(second.profession_id)
static func _sort_items(first: ItemDefinition, second: ItemDefinition) -> bool:
return String(first.item_id) < String(second.item_id)
static func _sort_enemies(first: EnemyDefinition, second: EnemyDefinition) -> bool:
return String(first.enemy_id) < String(second.enemy_id)
@@ -0,0 +1 @@
uid://csvigy66784n
+28 -7
View File
@@ -1,13 +1,17 @@
class_name ItemDefinition
extends Resource
const CATEGORY_RESOURCE := &"resource"
const CATEGORY_WEAPON := SimulationIds.ITEM_CATEGORY_WEAPON
const VALID_CATEGORIES := [CATEGORY_RESOURCE, CATEGORY_WEAPON]
@export var item_id: StringName
@export var display_name: String
@export var category: StringName = SimulationIds.ITEM_CATEGORY_WEAPON
@export var equip_slot: StringName = SimulationIds.EQUIP_SLOT_HAND
@export var category: StringName = CATEGORY_RESOURCE
@export var equip_slot: StringName
@export_range(0.0, 1000.0, 0.1) var damage := 0.0
@export_range(0.0, 50.0, 0.1) var reach := 1.5
@export_range(0.0, 5.0, 0.05) var attack_cooldown := 0.6
@export_range(0.0, 50.0, 0.1) var reach := 0.0
@export_range(0.0, 5.0, 0.05) var attack_cooldown := 0.0
func validate() -> Array[String]:
@@ -16,10 +20,27 @@ func validate() -> Array[String]:
errors.append("item_id is empty")
if display_name.is_empty():
errors.append("display_name is empty for '%s'" % item_id)
if damage <= 0.0:
errors.append("damage must be positive for '%s'" % item_id)
if category not in VALID_CATEGORIES:
errors.append("category '%s' is invalid for '%s'" % [category, item_id])
elif category == CATEGORY_WEAPON:
if equip_slot != SimulationIds.EQUIP_SLOT_HAND:
errors.append(
"weapon '%s' must equip in '%s'" % [item_id, SimulationIds.EQUIP_SLOT_HAND]
)
if not is_finite(damage) or damage <= 0.0:
errors.append("damage must be positive for '%s'" % item_id)
if not is_finite(reach) or reach <= 0.0:
errors.append("reach must be positive for '%s'" % item_id)
if not is_finite(attack_cooldown) or attack_cooldown <= 0.0:
errors.append("attack_cooldown must be positive for '%s'" % item_id)
elif not equip_slot.is_empty():
errors.append("resource item '%s' cannot declare equip_slot '%s'" % [item_id, equip_slot])
return errors
func is_weapon() -> bool:
return category == SimulationIds.ITEM_CATEGORY_WEAPON
return category == CATEGORY_WEAPON
func is_resource() -> bool:
return category == CATEGORY_RESOURCE
@@ -0,0 +1,78 @@
class_name SimulationContentPack
extends Resource
@export var pack_id: StringName
@export var display_name: String
@export var required_pack_ids: Array[StringName] = []
@export var required_handler_ids: Array[StringName] = []
@export var actions: Array[ActionDefinition] = []
@export var professions: Array[ProfessionDefinition] = []
@export var items: Array[ItemDefinition] = []
@export var enemies: Array[EnemyDefinition] = []
func validate() -> Array[String]:
var errors: Array[String] = []
if pack_id.is_empty():
errors.append("pack_id is empty")
if display_name.is_empty():
errors.append("display_name is empty for pack '%s'" % pack_id)
_validate_dependency_ids(required_pack_ids, "required pack", errors)
_validate_dependency_ids(required_handler_ids, "required handler", errors)
if pack_id in required_pack_ids:
errors.append("pack '%s' cannot require itself" % pack_id)
_validate_actions(errors)
_validate_professions(errors)
_validate_items(errors)
_validate_enemies(errors)
return errors
func _validate_actions(errors: Array[String]) -> void:
for definition in actions:
if definition == null:
errors.append("actions contains a null definition")
continue
for error in definition.validate():
errors.append("ActionDefinition: " + error)
func _validate_professions(errors: Array[String]) -> void:
for definition in professions:
if definition == null:
errors.append("professions contains a null definition")
continue
for error in definition.validate():
errors.append("ProfessionDefinition: " + error)
func _validate_items(errors: Array[String]) -> void:
for definition in items:
if definition == null:
errors.append("items contains a null definition")
continue
for error in definition.validate():
errors.append("ItemDefinition: " + error)
func _validate_enemies(errors: Array[String]) -> void:
for definition in enemies:
if definition == null:
errors.append("enemies contains a null definition")
continue
for error in definition.validate():
errors.append("EnemyDefinition: " + error)
static func _validate_dependency_ids(
ids: Array[StringName], label: String, errors: Array[String]
) -> void:
var seen := {}
for dependency_id in ids:
if dependency_id.is_empty():
errors.append("%s ID is empty" % label)
continue
if seen.has(dependency_id):
errors.append("duplicate %s '%s'" % [label, dependency_id])
continue
seen[dependency_id] = true
@@ -0,0 +1 @@
uid://j65xpgmfakin
@@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="EnemyDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/EnemyDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
enemy_id = &"enemy_raider"
display_name = "Raider"
kind = &"raider"
weapon_id = &"item_sword"
health = 60.0
move_speed = 3.5
body_color = Color(0.4, 0.13, 0.1, 1)
accent_color = Color(0.55, 0.55, 0.58, 1)
+15
View File
@@ -0,0 +1,15 @@
[gd_resource type="Resource" script_class="EnemyDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/EnemyDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
enemy_id = &"enemy_wolf"
display_name = "Wolf"
kind = &"wolf"
weapon_id = &"item_claw"
health = 40.0
move_speed = 4.2
body_color = Color(0.32, 0.32, 0.34, 1)
accent_color = Color(0.95, 0.5, 0.12, 1)
visual_scale = Vector3(0.8, 0.8, 0.8)
+13
View File
@@ -0,0 +1,13 @@
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ItemDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"item_claw"
display_name = "Claw"
category = &"weapon"
equip_slot = &"hand"
damage = 10.0
reach = 1.2
attack_cooldown = 0.4
+9
View File
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ItemDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"food"
display_name = "Food"
category = &"resource"
+13
View File
@@ -0,0 +1,13 @@
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ItemDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"item_sword"
display_name = "Sword"
category = &"weapon"
equip_slot = &"hand"
damage = 24.0
reach = 2.4
attack_cooldown = 0.55
+9
View File
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/definitions/ItemDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"wood"
display_name = "Wood"
category = &"resource"
+36
View File
@@ -0,0 +1,36 @@
[gd_resource type="Resource" script_class="SimulationContentPack" load_steps=27 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"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/deposit_food.tres" id="3_deposit_food"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/deposit_wood.tres" id="4_deposit_wood"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/eat.tres" id="5_eat"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/feed_animal.tres" id="6_feed_animal"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/gather_food.tres" id="7_gather_food"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/gather_wood.tres" id="8_gather_wood"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/patrol.tres" id="9_patrol"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/rest.tres" id="10_rest"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/sleep.tres" id="11_sleep"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/study.tres" id="12_study"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/wander.tres" id="13_wander"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/withdraw_food.tres" id="14_withdraw_food"]
[ext_resource type="Resource" path="res://simulation/definitions/professions/farmer.tres" id="15_farmer"]
[ext_resource type="Resource" path="res://simulation/definitions/professions/guard.tres" id="16_guard"]
[ext_resource type="Resource" path="res://simulation/definitions/professions/scholar.tres" id="17_scholar"]
[ext_resource type="Resource" path="res://simulation/definitions/professions/wanderer.tres" id="18_wanderer"]
[ext_resource type="Resource" path="res://simulation/definitions/professions/woodcutter.tres" id="19_woodcutter"]
[ext_resource type="Resource" path="res://simulation/definitions/items/claw.tres" id="20_claw"]
[ext_resource type="Resource" path="res://simulation/definitions/items/food.tres" id="21_food"]
[ext_resource type="Resource" path="res://simulation/definitions/items/sword.tres" id="22_sword"]
[ext_resource type="Resource" path="res://simulation/definitions/items/wood.tres" id="23_wood"]
[ext_resource type="Resource" path="res://simulation/definitions/enemies/raider.tres" id="24_raider"]
[ext_resource type="Resource" path="res://simulation/definitions/enemies/wolf.tres" id="25_wolf"]
[resource]
script = ExtResource("1_pack")
pack_id = &"core"
display_name = "The Steward Core"
actions = [ExtResource("2_defend"), ExtResource("3_deposit_food"), ExtResource("4_deposit_wood"), ExtResource("5_eat"), ExtResource("6_feed_animal"), ExtResource("7_gather_food"), ExtResource("8_gather_wood"), ExtResource("9_patrol"), ExtResource("10_rest"), ExtResource("11_sleep"), ExtResource("12_study"), ExtResource("13_wander"), ExtResource("14_withdraw_food")]
professions = [ExtResource("15_farmer"), ExtResource("16_guard"), ExtResource("17_scholar"), ExtResource("18_wanderer"), ExtResource("19_woodcutter")]
items = [ExtResource("20_claw"), ExtResource("21_food"), ExtResource("22_sword"), ExtResource("23_wood")]
enemies = [ExtResource("24_raider"), ExtResource("25_wolf")]