feat: author emergent situations and dialogue

This commit is contained in:
Rijad Zuzo
2026-08-12 21:58:12 +02:00
parent 14120115d4
commit d12e9515fb
29 changed files with 856 additions and 143 deletions
+300 -3
View File
@@ -2,6 +2,15 @@ class_name ContentCatalog
extends RefCounted
const CORE_PACK_PATH := "res://simulation/definitions/packs/core.tres"
const DEFAULT_SITUATION_EVENT_PREDICATE_IDS: Array[StringName] = [
&"event_type", &"event_payload_equals", &"event_participant_id", &"event_pantry_supplied"
]
const DEFAULT_SITUATION_STATE_PREDICATE_IDS: Array[StringName] = [
&"state_number_lte", &"state_number_gte"
]
const DEFAULT_SITUATION_EXPIRY_PREDICATE_IDS: Array[StringName] = [
&"state_number_gt", &"age_reached"
]
var _valid := false
var _errors: Array[String] = []
@@ -14,6 +23,9 @@ var _resources: Array[ResourceDefinition] = []
var _storages: Array[StorageDefinition] = []
var _enemies: Array[EnemyDefinition] = []
var _animals: Array[AnimalDefinition] = []
var _situations: Array[SituationDefinition] = []
var _dialogue_intents: Array[ConversationIntentDefinition] = []
var _dialogue_template_catalogs: Array[ConversationTemplateCatalog] = []
var _packs_by_id: Dictionary = {}
var _actions_by_id: Dictionary = {}
var _professions_by_id: Dictionary = {}
@@ -23,11 +35,17 @@ var _resources_by_id: Dictionary = {}
var _storages_by_id: Dictionary = {}
var _enemies_by_id: Dictionary = {}
var _animals_by_id: Dictionary = {}
var _situations_by_id: Dictionary = {}
var _dialogue_intents_by_id: Dictionary = {}
var _dialogue_template_catalogs_by_id: Dictionary = {}
static func create_core(
supported_handler_ids: Array[StringName] = [],
supported_presentation_cue_ids: Array[StringName] = []
supported_presentation_cue_ids: Array[StringName] = [],
supported_situation_event_predicate_ids: Array[StringName] = [],
supported_situation_state_predicate_ids: Array[StringName] = [],
supported_situation_expiry_predicate_ids: Array[StringName] = []
) -> ContentCatalog:
var catalog := ContentCatalog.new()
var pack := load(CORE_PACK_PATH) as SimulationContentPack
@@ -35,14 +53,24 @@ static func create_core(
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, supported_presentation_cue_ids)
catalog.rebuild(
packs,
supported_handler_ids,
supported_presentation_cue_ids,
supported_situation_event_predicate_ids,
supported_situation_state_predicate_ids,
supported_situation_expiry_predicate_ids
)
return catalog
func rebuild(
content_packs: Array[SimulationContentPack],
supported_handler_ids: Array[StringName] = [],
supported_presentation_cue_ids: Array[StringName] = []
supported_presentation_cue_ids: Array[StringName] = [],
supported_situation_event_predicate_ids: Array[StringName] = [],
supported_situation_state_predicate_ids: Array[StringName] = [],
supported_situation_expiry_predicate_ids: Array[StringName] = []
) -> Array[String]:
_clear_published_content()
var errors: Array[String] = []
@@ -69,6 +97,24 @@ func rebuild(
errors.append("Duplicate supported presentation cue '%s'" % cue_id)
else:
supported_cues[cue_id] = true
var supported_event_predicates := _validated_id_set(
supported_situation_event_predicate_ids,
DEFAULT_SITUATION_EVENT_PREDICATE_IDS,
"situation event predicate",
errors
)
var supported_state_predicates := _validated_id_set(
supported_situation_state_predicate_ids,
DEFAULT_SITUATION_STATE_PREDICATE_IDS,
"situation state predicate",
errors
)
var supported_expiry_predicates := _validated_id_set(
supported_situation_expiry_predicate_ids,
DEFAULT_SITUATION_EXPIRY_PREDICATE_IDS,
"situation expiry predicate",
errors
)
var packs_by_id := {}
var actions_by_id := {}
@@ -79,6 +125,9 @@ func rebuild(
var storages_by_id := {}
var enemies_by_id := {}
var animals_by_id := {}
var situations_by_id := {}
var dialogue_intents_by_id := {}
var dialogue_template_catalogs_by_id := {}
var actions: Array[ActionDefinition] = []
var professions: Array[ProfessionDefinition] = []
var capability_tags: Array[CapabilityTagDefinition] = []
@@ -87,6 +136,9 @@ func rebuild(
var storages: Array[StorageDefinition] = []
var enemies: Array[EnemyDefinition] = []
var animals: Array[AnimalDefinition] = []
var situations: Array[SituationDefinition] = []
var dialogue_intents: Array[ConversationIntentDefinition] = []
var dialogue_template_catalogs: Array[ConversationTemplateCatalog] = []
for pack in pack_list:
if pack == null:
@@ -106,6 +158,11 @@ func rebuild(
_collect_storages(pack, storages, storages_by_id, errors)
_collect_enemies(pack, enemies, enemies_by_id, errors)
_collect_animals(pack, animals, animals_by_id, errors)
_collect_situations(pack, situations, situations_by_id, errors)
_collect_dialogue_intents(pack, dialogue_intents, dialogue_intents_by_id, errors)
_collect_dialogue_template_catalogs(
pack, dialogue_template_catalogs, dialogue_template_catalogs_by_id, errors
)
for pack in pack_list:
if pack == null:
@@ -149,6 +206,19 @@ func rebuild(
)
_validate_enemy_references(enemies, items_by_id, errors)
_validate_animal_references(animals, actions_by_id, supported_cues, errors)
_validate_situation_references(
situations,
actions_by_id,
items_by_id,
dialogue_intents_by_id,
supported_event_predicates,
supported_state_predicates,
supported_expiry_predicates,
errors
)
_validate_dialogue_references(
dialogue_intents, dialogue_intents_by_id, dialogue_template_catalogs_by_id, errors
)
errors.sort()
_errors = errors
if not errors.is_empty():
@@ -162,6 +232,9 @@ func rebuild(
storages.sort_custom(_sort_storages)
enemies.sort_custom(_sort_enemies)
animals.sort_custom(_sort_animals)
situations.sort_custom(_sort_situations)
dialogue_intents.sort_custom(_sort_dialogue_intents)
dialogue_template_catalogs.sort_custom(_sort_dialogue_template_catalogs)
_packs = pack_list
_actions = actions
_professions = professions
@@ -171,6 +244,9 @@ func rebuild(
_storages = storages
_enemies = enemies
_animals = animals
_situations = situations
_dialogue_intents = dialogue_intents
_dialogue_template_catalogs = dialogue_template_catalogs
_packs_by_id = packs_by_id
_actions_by_id = actions_by_id
_professions_by_id = professions_by_id
@@ -180,6 +256,9 @@ func rebuild(
_storages_by_id = storages_by_id
_enemies_by_id = enemies_by_id
_animals_by_id = animals_by_id
_situations_by_id = situations_by_id
_dialogue_intents_by_id = dialogue_intents_by_id
_dialogue_template_catalogs_by_id = dialogue_template_catalogs_by_id
_valid = true
return []
@@ -228,6 +307,18 @@ func get_animals() -> Array[AnimalDefinition]:
return _animals.duplicate()
func get_situations() -> Array[SituationDefinition]:
return _situations.duplicate()
func get_dialogue_intents() -> Array[ConversationIntentDefinition]:
return _dialogue_intents.duplicate()
func get_dialogue_template_catalogs() -> Array[ConversationTemplateCatalog]:
return _dialogue_template_catalogs.duplicate()
func get_pack(pack_id: StringName) -> SimulationContentPack:
return _packs_by_id.get(pack_id) as SimulationContentPack
@@ -264,6 +355,18 @@ func get_animal(animal_definition_id: StringName) -> AnimalDefinition:
return _animals_by_id.get(animal_definition_id) as AnimalDefinition
func get_situation(situation_definition_id: StringName) -> SituationDefinition:
return _situations_by_id.get(situation_definition_id) as SituationDefinition
func get_dialogue_intent(intent_id: StringName) -> ConversationIntentDefinition:
return _dialogue_intents_by_id.get(intent_id) as ConversationIntentDefinition
func get_dialogue_template_catalog(catalog_id: StringName) -> ConversationTemplateCatalog:
return _dialogue_template_catalogs_by_id.get(catalog_id) as ConversationTemplateCatalog
func get_authoring_report() -> Dictionary:
return {
"valid": _valid,
@@ -277,6 +380,9 @@ func get_authoring_report() -> Dictionary:
"storages": _ids_for(_storages, &"storage_id"),
"enemies": _ids_for(_enemies, &"enemy_id"),
"animals": _ids_for(_animals, &"animal_definition_id"),
"situations": _ids_for(_situations, &"situation_definition_id"),
"dialogue_intents": _ids_for(_dialogue_intents, &"intent_id"),
"dialogue_template_catalogs": _ids_for(_dialogue_template_catalogs, &"catalog_id"),
}
@@ -292,6 +398,9 @@ func _clear_published_content() -> void:
_storages.clear()
_enemies.clear()
_animals.clear()
_situations.clear()
_dialogue_intents.clear()
_dialogue_template_catalogs.clear()
_packs_by_id.clear()
_actions_by_id.clear()
_professions_by_id.clear()
@@ -301,6 +410,9 @@ func _clear_published_content() -> void:
_storages_by_id.clear()
_enemies_by_id.clear()
_animals_by_id.clear()
_situations_by_id.clear()
_dialogue_intents_by_id.clear()
_dialogue_template_catalogs_by_id.clear()
static func _collect_actions(
@@ -431,6 +543,56 @@ static func _collect_animals(
definitions.append(definition)
static func _collect_situations(
pack: SimulationContentPack,
definitions: Array[SituationDefinition],
definitions_by_id: Dictionary,
errors: Array[String]
) -> void:
for definition in pack.situations:
if definition == null:
continue
if definitions_by_id.has(definition.situation_definition_id):
errors.append(
"Duplicate situation_definition_id '%s'" % definition.situation_definition_id
)
continue
definitions_by_id[definition.situation_definition_id] = definition
definitions.append(definition)
static func _collect_dialogue_intents(
pack: SimulationContentPack,
definitions: Array[ConversationIntentDefinition],
definitions_by_id: Dictionary,
errors: Array[String]
) -> void:
for definition in pack.dialogue_intents:
if definition == null:
continue
if definitions_by_id.has(definition.intent_id):
errors.append("Duplicate dialogue intent_id '%s'" % definition.intent_id)
continue
definitions_by_id[definition.intent_id] = definition
definitions.append(definition)
static func _collect_dialogue_template_catalogs(
pack: SimulationContentPack,
definitions: Array[ConversationTemplateCatalog],
definitions_by_id: Dictionary,
errors: Array[String]
) -> void:
for definition in pack.dialogue_template_catalogs:
if definition == null:
continue
if definitions_by_id.has(definition.catalog_id):
errors.append("Duplicate dialogue template catalog_id '%s'" % definition.catalog_id)
continue
definitions_by_id[definition.catalog_id] = definition
definitions.append(definition)
static func _validate_action_references(
actions: Array[ActionDefinition],
actions_by_id: Dictionary,
@@ -730,6 +892,125 @@ static func _validate_animal_references(
)
static func _validate_situation_references(
situations: Array[SituationDefinition],
actions_by_id: Dictionary,
items_by_id: Dictionary,
dialogue_intents_by_id: Dictionary,
supported_event_predicates: Dictionary,
supported_state_predicates: Dictionary,
supported_expiry_predicates: Dictionary,
errors: Array[String]
) -> void:
var event_predicate_ids := _sorted_string_name_keys(supported_event_predicates)
var state_predicate_ids := _sorted_string_name_keys(supported_state_predicates)
var expiry_predicate_ids := _sorted_string_name_keys(supported_expiry_predicates)
for definition in situations:
for error in definition.validate(
event_predicate_ids, state_predicate_ids, expiry_predicate_ids
):
errors.append("Situation '%s': %s" % [definition.situation_definition_id, error])
for intent_id in definition.dialogue_intent_ids:
if not intent_id.is_empty() and not dialogue_intents_by_id.has(intent_id):
errors.append(
(
"Situation '%s' references unknown dialogue intent '%s'"
% [definition.situation_definition_id, intent_id]
)
)
for alternative in definition.alternatives:
if alternative == null:
continue
var action_id := StringName(alternative.commitment_terms.get("action_id", ""))
if not action_id.is_empty() and not actions_by_id.has(action_id):
errors.append(
(
"Situation alternative '%s' references unknown action '%s'"
% [alternative.alternative_id, action_id]
)
)
var item_id := StringName(alternative.commitment_terms.get("item_id", ""))
if not item_id.is_empty() and not items_by_id.has(item_id):
errors.append(
(
"Situation alternative '%s' references unknown item '%s'"
% [alternative.alternative_id, item_id]
)
)
static func _validate_dialogue_references(
intents: Array[ConversationIntentDefinition],
intents_by_id: Dictionary,
template_catalogs_by_id: Dictionary,
errors: Array[String]
) -> void:
var intent_ids_by_template_catalog := {}
for definition in intents:
for response_intent_id in definition.response_intent_ids:
if not intents_by_id.has(response_intent_id):
errors.append(
(
"Dialogue intent '%s' references unknown response intent '%s'"
% [definition.intent_id, response_intent_id]
)
)
if not template_catalogs_by_id.has(definition.template_catalog_id):
errors.append(
(
"Dialogue intent '%s' references unknown template catalog '%s'"
% [definition.intent_id, definition.template_catalog_id]
)
)
continue
var catalog_intent_ids: Array[StringName] = []
for existing_intent_id in intent_ids_by_template_catalog.get(
definition.template_catalog_id, []
):
catalog_intent_ids.append(StringName(existing_intent_id))
catalog_intent_ids.append(definition.intent_id)
intent_ids_by_template_catalog[definition.template_catalog_id] = catalog_intent_ids
for raw_catalog_id in template_catalogs_by_id:
var catalog_id := StringName(raw_catalog_id)
var catalog := template_catalogs_by_id.get(catalog_id) as ConversationTemplateCatalog
var expected_intent_ids: Array[StringName] = []
for expected_intent_id in intent_ids_by_template_catalog.get(catalog_id, []):
expected_intent_ids.append(StringName(expected_intent_id))
expected_intent_ids.sort_custom(
func(left: StringName, right: StringName) -> bool: return String(left) < String(right)
)
for error in catalog.validate(expected_intent_ids):
errors.append("Dialogue template catalog '%s': %s" % [catalog_id, error])
static func _validated_id_set(
provided_ids: Array[StringName],
default_ids: Array[StringName],
label: String,
errors: Array[String]
) -> Dictionary:
var result := {}
var ids := provided_ids if not provided_ids.is_empty() else default_ids
for id in ids:
if id.is_empty():
errors.append("Supported %s ID is empty" % label)
elif result.has(id):
errors.append("Duplicate supported %s '%s'" % [label, id])
else:
result[id] = true
return result
static func _sorted_string_name_keys(source: Dictionary) -> Array[StringName]:
var result: Array[StringName] = []
for key in source:
result.append(StringName(key))
result.sort_custom(
func(left: StringName, right: StringName) -> bool: return String(left) < String(right)
)
return result
static func _sort_packs(first: SimulationContentPack, second: SimulationContentPack) -> bool:
if first == null:
return second != null
@@ -772,6 +1053,22 @@ static func _sort_animals(first: AnimalDefinition, second: AnimalDefinition) ->
return String(first.animal_definition_id) < String(second.animal_definition_id)
static func _sort_situations(first: SituationDefinition, second: SituationDefinition) -> bool:
return String(first.situation_definition_id) < String(second.situation_definition_id)
static func _sort_dialogue_intents(
first: ConversationIntentDefinition, second: ConversationIntentDefinition
) -> bool:
return String(first.intent_id) < String(second.intent_id)
static func _sort_dialogue_template_catalogs(
first: ConversationTemplateCatalog, second: ConversationTemplateCatalog
) -> bool:
return String(first.catalog_id) < String(second.catalog_id)
static func _ids_for(definitions: Array, id_property: StringName) -> Array[StringName]:
var ids: Array[StringName] = []
for definition in definitions:
@@ -13,6 +13,9 @@ extends Resource
@export var storages: Array[StorageDefinition] = []
@export var enemies: Array[EnemyDefinition] = []
@export var animals: Array[AnimalDefinition] = []
@export var situations: Array[SituationDefinition] = []
@export var dialogue_intents: Array[ConversationIntentDefinition] = []
@export var dialogue_template_catalogs: Array[ConversationTemplateCatalog] = []
func validate() -> Array[String]:
@@ -33,6 +36,9 @@ func validate() -> Array[String]:
_validate_storages(errors)
_validate_enemies(errors)
_validate_animals(errors)
_validate_situations(errors)
_validate_dialogue_intents(errors)
_validate_dialogue_template_catalogs(errors)
return errors
@@ -108,6 +114,32 @@ func _validate_animals(errors: Array[String]) -> void:
errors.append("AnimalDefinition: " + error)
func _validate_situations(errors: Array[String]) -> void:
for definition in situations:
if definition == null:
errors.append("situations contains a null definition")
continue
for error in definition.validate():
errors.append("SituationDefinition: " + error)
func _validate_dialogue_intents(errors: Array[String]) -> void:
for definition in dialogue_intents:
if definition == null:
errors.append("dialogue_intents contains a null definition")
continue
for error in definition.validate():
errors.append("ConversationIntentDefinition: " + error)
func _validate_dialogue_template_catalogs(errors: Array[String]) -> void:
for catalog in dialogue_template_catalogs:
if catalog == null:
errors.append("dialogue_template_catalogs contains a null catalog")
elif catalog.catalog_id.is_empty():
errors.append("ConversationTemplateCatalog: catalog_id is empty")
static func _validate_dependency_ids(
ids: Array[StringName], label: String, errors: Array[String]
) -> void:
+22 -1
View File
@@ -1,4 +1,4 @@
[gd_resource type="Resource" script_class="SimulationContentPack" load_steps=42 format=3]
[gd_resource type="Resource" script_class="SimulationContentPack" load_steps=60 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"]
@@ -40,6 +40,24 @@
[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"]
[ext_resource type="Resource" path="res://simulation/situations/resources/pantry_shortage.tres" id="41_pantry_situation"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/accept.tres" id="42_accept"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/acknowledge_supersession.tres" id="43_acknowledge"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_wellbeing.tres" id="44_wellbeing"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_what_happened.tres" id="45_happened"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_who_else.tres" id="46_who"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_work.tres" id="47_work"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/decline.tres" id="48_decline"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/describe_need.tres" id="49_need"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/goodbye.tres" id="50_goodbye"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/greet.tres" id="51_greet"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/offer_help.tres" id="52_offer"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/renegotiate.tres" id="53_renegotiate"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/report_progress.tres" id="54_progress"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/reproach.tres" id="55_reproach"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/share_fact.tres" id="56_share"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/thank.tres" id="57_thank"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/jajce_conversation_templates.tres" id="58_templates"]
[resource]
script = ExtResource("1_pack")
@@ -53,3 +71,6 @@ resources = [ExtResource("33_berry_patch"), ExtResource("34_herb_patch"), ExtRes
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")]
situations = [ExtResource("41_pantry_situation")]
dialogue_intents = [ExtResource("42_accept"), ExtResource("43_acknowledge"), ExtResource("44_wellbeing"), ExtResource("45_happened"), ExtResource("46_who"), ExtResource("47_work"), ExtResource("48_decline"), ExtResource("49_need"), ExtResource("50_goodbye"), ExtResource("51_greet"), ExtResource("52_offer"), ExtResource("53_renegotiate"), ExtResource("54_progress"), ExtResource("55_reproach"), ExtResource("56_share"), ExtResource("57_thank")]
dialogue_template_catalogs = [ExtResource("58_templates")]