From d12e9515fbbb40b29ee6adc88fc46d5092ef8c64 Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Wed, 12 Aug 2026 21:58:12 +0200 Subject: [PATCH] feat: author emergent situations and dialogue --- simulation/definitions/ContentCatalog.gd | 303 +++++++++++++++++- .../definitions/SimulationContentPack.gd | 32 ++ simulation/definitions/packs/core.tres | 23 +- .../dialogue/ConversationIntentDefinition.gd | 11 + .../dialogue/ConversationTemplateCatalog.gd | 39 +++ .../dialogue/resources/intents/accept.tres | 9 + .../intents/acknowledge_supersession.tres | 9 + .../resources/intents/ask_wellbeing.tres | 9 + .../resources/intents/ask_what_happened.tres | 9 + .../resources/intents/ask_who_else.tres | 9 + .../dialogue/resources/intents/ask_work.tres | 9 + .../dialogue/resources/intents/decline.tres | 9 + .../resources/intents/describe_need.tres | 9 + .../dialogue/resources/intents/goodbye.tres | 8 + .../dialogue/resources/intents/greet.tres | 9 + .../resources/intents/offer_help.tres | 9 + .../resources/intents/renegotiate.tres | 9 + .../resources/intents/report_progress.tres | 9 + .../dialogue/resources/intents/reproach.tres | 9 + .../resources/intents/share_fact.tres | 9 + .../dialogue/resources/intents/thank.tres | 9 + .../resources/jajce_conversation_intents.tres | 98 +----- .../jajce_conversation_templates.tres | 1 + .../SituationAlternativeDefinition.gd | 15 + simulation/situations/SituationDefinition.gd | 44 ++- simulation/situations/SituationSystem.gd | 65 +--- .../situations/resources/pantry_shortage.tres | 70 ++++ ...est_authored_situation_dialogue_content.gd | 154 +++++++++ ...authored_situation_dialogue_content.gd.uid | 1 + 29 files changed, 856 insertions(+), 143 deletions(-) create mode 100644 simulation/dialogue/resources/intents/accept.tres create mode 100644 simulation/dialogue/resources/intents/acknowledge_supersession.tres create mode 100644 simulation/dialogue/resources/intents/ask_wellbeing.tres create mode 100644 simulation/dialogue/resources/intents/ask_what_happened.tres create mode 100644 simulation/dialogue/resources/intents/ask_who_else.tres create mode 100644 simulation/dialogue/resources/intents/ask_work.tres create mode 100644 simulation/dialogue/resources/intents/decline.tres create mode 100644 simulation/dialogue/resources/intents/describe_need.tres create mode 100644 simulation/dialogue/resources/intents/goodbye.tres create mode 100644 simulation/dialogue/resources/intents/greet.tres create mode 100644 simulation/dialogue/resources/intents/offer_help.tres create mode 100644 simulation/dialogue/resources/intents/renegotiate.tres create mode 100644 simulation/dialogue/resources/intents/report_progress.tres create mode 100644 simulation/dialogue/resources/intents/reproach.tres create mode 100644 simulation/dialogue/resources/intents/share_fact.tres create mode 100644 simulation/dialogue/resources/intents/thank.tres create mode 100644 simulation/situations/resources/pantry_shortage.tres create mode 100644 tests/unit/test_authored_situation_dialogue_content.gd create mode 100644 tests/unit/test_authored_situation_dialogue_content.gd.uid diff --git a/simulation/definitions/ContentCatalog.gd b/simulation/definitions/ContentCatalog.gd index 4064351..9fdc69a 100644 --- a/simulation/definitions/ContentCatalog.gd +++ b/simulation/definitions/ContentCatalog.gd @@ -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: diff --git a/simulation/definitions/SimulationContentPack.gd b/simulation/definitions/SimulationContentPack.gd index 9537bd8..1f70090 100644 --- a/simulation/definitions/SimulationContentPack.gd +++ b/simulation/definitions/SimulationContentPack.gd @@ -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: diff --git a/simulation/definitions/packs/core.tres b/simulation/definitions/packs/core.tres index 2f6511d..cecad48 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=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")] diff --git a/simulation/dialogue/ConversationIntentDefinition.gd b/simulation/dialogue/ConversationIntentDefinition.gd index dd1c004..8d8779e 100644 --- a/simulation/dialogue/ConversationIntentDefinition.gd +++ b/simulation/dialogue/ConversationIntentDefinition.gd @@ -3,12 +3,15 @@ extends Resource @export var intent_id: StringName @export var response_intent_ids: Array[StringName] = [] +@export var template_catalog_id: StringName func validate() -> Array[String]: var errors: Array[String] = [] if not ConversationSemantic.is_valid_id(intent_id): errors.append("intent_id is invalid") + if template_catalog_id.is_empty(): + errors.append("template_catalog_id is empty for '%s'" % intent_id) var seen: Dictionary = {} for response_intent_id in response_intent_ids: if not ConversationSemantic.is_valid_id(response_intent_id): @@ -20,3 +23,11 @@ func validate() -> Array[String]: else: seen[response_intent_id] = true return errors + + +func duplicate_definition() -> ConversationIntentDefinition: + var copy := ConversationIntentDefinition.new() + copy.intent_id = intent_id + copy.response_intent_ids = response_intent_ids.duplicate() + copy.template_catalog_id = template_catalog_id + return copy diff --git a/simulation/dialogue/ConversationTemplateCatalog.gd b/simulation/dialogue/ConversationTemplateCatalog.gd index 940d13c..bcc0c43 100644 --- a/simulation/dialogue/ConversationTemplateCatalog.gd +++ b/simulation/dialogue/ConversationTemplateCatalog.gd @@ -1,12 +1,15 @@ class_name ConversationTemplateCatalog extends Resource +@export var catalog_id: StringName @export var line_templates: Dictionary = {} @export var option_templates: Dictionary = {} func validate(intent_ids: Array[StringName] = ConversationIntentIds.ALL) -> Array[String]: var errors: Array[String] = [] + if catalog_id.is_empty(): + errors.append("catalog_id is empty") _validate_templates(line_templates, "line", intent_ids, errors) _validate_templates(option_templates, "option", intent_ids, errors) errors.sort() @@ -21,6 +24,14 @@ func get_option_templates(intent_id: StringName) -> Array[String]: return _get_templates(option_templates, intent_id) +func get_line_template_ids() -> Array[StringName]: + return _template_ids(line_templates) + + +func get_option_template_ids() -> Array[StringName]: + return _template_ids(option_templates) + + func _get_templates(source: Dictionary, intent_id: StringName) -> Array[String]: var result: Array[String] = [] var values: Variant = source.get(String(intent_id), []) @@ -34,6 +45,23 @@ func _get_templates(source: Dictionary, intent_id: StringName) -> Array[String]: func _validate_templates( source: Dictionary, kind: String, intent_ids: Array[StringName], errors: Array[String] ) -> void: + var allowed_ids := {} + for intent_id in intent_ids: + allowed_ids[intent_id] = true + for raw_intent_id in source: + if not raw_intent_id is String and not raw_intent_id is StringName: + errors.append("Non-string %s template intent ID" % kind) + continue + var source_intent_id := StringName(raw_intent_id) + if not allowed_ids.has(source_intent_id): + errors.append("Unknown %s template intent '%s'" % [kind, source_intent_id]) + var raw_templates: Variant = source[raw_intent_id] + if not raw_templates is Array and not raw_templates is PackedStringArray: + errors.append("Invalid %s templates for '%s'" % [kind, source_intent_id]) + continue + for raw_template in raw_templates: + if not raw_template is String: + errors.append("Non-string %s template for '%s'" % [kind, source_intent_id]) for intent_id in intent_ids: var templates := _get_templates(source, intent_id) if templates.is_empty(): @@ -42,3 +70,14 @@ func _validate_templates( for template in templates: if template.strip_edges().is_empty(): errors.append("Empty %s template for '%s'" % [kind, intent_id]) + + +func _template_ids(source: Dictionary) -> Array[StringName]: + var result: Array[StringName] = [] + for raw_id in source: + if raw_id is String or raw_id is StringName: + result.append(StringName(raw_id)) + result.sort_custom( + func(left: StringName, right: StringName) -> bool: return String(left) < String(right) + ) + return result diff --git a/simulation/dialogue/resources/intents/accept.tres b/simulation/dialogue/resources/intents/accept.tres new file mode 100644 index 0000000..a9e35d7 --- /dev/null +++ b/simulation/dialogue/resources/intents/accept.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"accept" +response_intent_ids = Array[StringName]([&"thank", &"report_progress", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/acknowledge_supersession.tres b/simulation/dialogue/resources/intents/acknowledge_supersession.tres new file mode 100644 index 0000000..2c41ac5 --- /dev/null +++ b/simulation/dialogue/resources/intents/acknowledge_supersession.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"acknowledge_supersession" +response_intent_ids = Array[StringName]([&"thank", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/ask_wellbeing.tres b/simulation/dialogue/resources/intents/ask_wellbeing.tres new file mode 100644 index 0000000..6270428 --- /dev/null +++ b/simulation/dialogue/resources/intents/ask_wellbeing.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"ask_wellbeing" +response_intent_ids = Array[StringName]([&"describe_need", &"share_fact", &"offer_help", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/ask_what_happened.tres b/simulation/dialogue/resources/intents/ask_what_happened.tres new file mode 100644 index 0000000..f816d88 --- /dev/null +++ b/simulation/dialogue/resources/intents/ask_what_happened.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"ask_what_happened" +response_intent_ids = Array[StringName]([&"share_fact", &"ask_who_else", &"offer_help", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/ask_who_else.tres b/simulation/dialogue/resources/intents/ask_who_else.tres new file mode 100644 index 0000000..a1b5a16 --- /dev/null +++ b/simulation/dialogue/resources/intents/ask_who_else.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"ask_who_else" +response_intent_ids = Array[StringName]([&"share_fact", &"offer_help", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/ask_work.tres b/simulation/dialogue/resources/intents/ask_work.tres new file mode 100644 index 0000000..b6e018e --- /dev/null +++ b/simulation/dialogue/resources/intents/ask_work.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"ask_work" +response_intent_ids = Array[StringName]([&"describe_need", &"report_progress", &"share_fact", &"ask_who_else", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/decline.tres b/simulation/dialogue/resources/intents/decline.tres new file mode 100644 index 0000000..77f242a --- /dev/null +++ b/simulation/dialogue/resources/intents/decline.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"decline" +response_intent_ids = Array[StringName]([&"reproach", &"renegotiate", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/describe_need.tres b/simulation/dialogue/resources/intents/describe_need.tres new file mode 100644 index 0000000..0f28a43 --- /dev/null +++ b/simulation/dialogue/resources/intents/describe_need.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"describe_need" +response_intent_ids = Array[StringName]([&"offer_help", &"decline", &"ask_who_else", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/goodbye.tres b/simulation/dialogue/resources/intents/goodbye.tres new file mode 100644 index 0000000..da1237d --- /dev/null +++ b/simulation/dialogue/resources/intents/goodbye.tres @@ -0,0 +1,8 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"goodbye" +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/greet.tres b/simulation/dialogue/resources/intents/greet.tres new file mode 100644 index 0000000..fc66283 --- /dev/null +++ b/simulation/dialogue/resources/intents/greet.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"greet" +response_intent_ids = Array[StringName]([&"ask_work", &"ask_wellbeing", &"ask_what_happened", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/offer_help.tres b/simulation/dialogue/resources/intents/offer_help.tres new file mode 100644 index 0000000..dc4764b --- /dev/null +++ b/simulation/dialogue/resources/intents/offer_help.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"offer_help" +response_intent_ids = Array[StringName]([&"accept", &"decline", &"renegotiate"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/renegotiate.tres b/simulation/dialogue/resources/intents/renegotiate.tres new file mode 100644 index 0000000..6189e09 --- /dev/null +++ b/simulation/dialogue/resources/intents/renegotiate.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"renegotiate" +response_intent_ids = Array[StringName]([&"accept", &"decline", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/report_progress.tres b/simulation/dialogue/resources/intents/report_progress.tres new file mode 100644 index 0000000..a296b75 --- /dev/null +++ b/simulation/dialogue/resources/intents/report_progress.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"report_progress" +response_intent_ids = Array[StringName]([&"thank", &"renegotiate", &"acknowledge_supersession", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/reproach.tres b/simulation/dialogue/resources/intents/reproach.tres new file mode 100644 index 0000000..0208a21 --- /dev/null +++ b/simulation/dialogue/resources/intents/reproach.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"reproach" +response_intent_ids = Array[StringName]([&"acknowledge_supersession", &"renegotiate", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/share_fact.tres b/simulation/dialogue/resources/intents/share_fact.tres new file mode 100644 index 0000000..0453807 --- /dev/null +++ b/simulation/dialogue/resources/intents/share_fact.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"share_fact" +response_intent_ids = Array[StringName]([&"ask_what_happened", &"ask_who_else", &"acknowledge_supersession", &"thank", &"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/intents/thank.tres b/simulation/dialogue/resources/intents/thank.tres new file mode 100644 index 0000000..0a65c3c --- /dev/null +++ b/simulation/dialogue/resources/intents/thank.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3] + +[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"] + +[resource] +script = ExtResource("1") +intent_id = &"thank" +response_intent_ids = Array[StringName]([&"goodbye"]) +template_catalog_id = &"jajce_conversation_templates" diff --git a/simulation/dialogue/resources/jajce_conversation_intents.tres b/simulation/dialogue/resources/jajce_conversation_intents.tres index d284da0..f6d4d38 100644 --- a/simulation/dialogue/resources/jajce_conversation_intents.tres +++ b/simulation/dialogue/resources/jajce_conversation_intents.tres @@ -1,87 +1,23 @@ [gd_resource type="Resource" script_class="ConversationIntentCatalog" load_steps=18 format=3] [ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentCatalog.gd" id="1_catalog"] -[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="2_intent"] - -[sub_resource type="Resource" id="Intent_greet"] -script = ExtResource("2_intent") -intent_id = &"greet" -response_intent_ids = Array[StringName]([&"ask_work", &"ask_wellbeing", &"ask_what_happened", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_ask_work"] -script = ExtResource("2_intent") -intent_id = &"ask_work" -response_intent_ids = Array[StringName]([&"describe_need", &"report_progress", &"share_fact", &"ask_who_else", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_ask_wellbeing"] -script = ExtResource("2_intent") -intent_id = &"ask_wellbeing" -response_intent_ids = Array[StringName]([&"describe_need", &"share_fact", &"offer_help", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_ask_what_happened"] -script = ExtResource("2_intent") -intent_id = &"ask_what_happened" -response_intent_ids = Array[StringName]([&"share_fact", &"ask_who_else", &"offer_help", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_share_fact"] -script = ExtResource("2_intent") -intent_id = &"share_fact" -response_intent_ids = Array[StringName]([&"ask_what_happened", &"ask_who_else", &"acknowledge_supersession", &"thank", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_describe_need"] -script = ExtResource("2_intent") -intent_id = &"describe_need" -response_intent_ids = Array[StringName]([&"offer_help", &"decline", &"ask_who_else", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_ask_who_else"] -script = ExtResource("2_intent") -intent_id = &"ask_who_else" -response_intent_ids = Array[StringName]([&"share_fact", &"offer_help", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_offer_help"] -script = ExtResource("2_intent") -intent_id = &"offer_help" -response_intent_ids = Array[StringName]([&"accept", &"decline", &"renegotiate"]) - -[sub_resource type="Resource" id="Intent_accept"] -script = ExtResource("2_intent") -intent_id = &"accept" -response_intent_ids = Array[StringName]([&"thank", &"report_progress", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_decline"] -script = ExtResource("2_intent") -intent_id = &"decline" -response_intent_ids = Array[StringName]([&"reproach", &"renegotiate", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_report_progress"] -script = ExtResource("2_intent") -intent_id = &"report_progress" -response_intent_ids = Array[StringName]([&"thank", &"renegotiate", &"acknowledge_supersession", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_renegotiate"] -script = ExtResource("2_intent") -intent_id = &"renegotiate" -response_intent_ids = Array[StringName]([&"accept", &"decline", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_thank"] -script = ExtResource("2_intent") -intent_id = &"thank" -response_intent_ids = Array[StringName]([&"goodbye"]) - -[sub_resource type="Resource" id="Intent_reproach"] -script = ExtResource("2_intent") -intent_id = &"reproach" -response_intent_ids = Array[StringName]([&"acknowledge_supersession", &"renegotiate", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_acknowledge_supersession"] -script = ExtResource("2_intent") -intent_id = &"acknowledge_supersession" -response_intent_ids = Array[StringName]([&"thank", &"goodbye"]) - -[sub_resource type="Resource" id="Intent_goodbye"] -script = ExtResource("2_intent") -intent_id = &"goodbye" +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/accept.tres" id="2_accept"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/acknowledge_supersession.tres" id="3_acknowledge"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_wellbeing.tres" id="4_wellbeing"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_what_happened.tres" id="5_happened"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_who_else.tres" id="6_who"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_work.tres" id="7_work"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/decline.tres" id="8_decline"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/describe_need.tres" id="9_need"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/goodbye.tres" id="10_goodbye"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/greet.tres" id="11_greet"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/offer_help.tres" id="12_offer"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/renegotiate.tres" id="13_renegotiate"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/report_progress.tres" id="14_progress"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/reproach.tres" id="15_reproach"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/share_fact.tres" id="16_share"] +[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/thank.tres" id="17_thank"] [resource] script = ExtResource("1_catalog") -intents = Array[ExtResource("2_intent")]([SubResource("Intent_greet"), SubResource("Intent_ask_work"), SubResource("Intent_ask_wellbeing"), SubResource("Intent_ask_what_happened"), SubResource("Intent_share_fact"), SubResource("Intent_describe_need"), SubResource("Intent_ask_who_else"), SubResource("Intent_offer_help"), SubResource("Intent_accept"), SubResource("Intent_decline"), SubResource("Intent_report_progress"), SubResource("Intent_renegotiate"), SubResource("Intent_thank"), SubResource("Intent_reproach"), SubResource("Intent_acknowledge_supersession"), SubResource("Intent_goodbye")]) +intents = [ExtResource("11_greet"), ExtResource("7_work"), ExtResource("4_wellbeing"), ExtResource("5_happened"), ExtResource("16_share"), ExtResource("9_need"), ExtResource("6_who"), ExtResource("12_offer"), ExtResource("2_accept"), ExtResource("8_decline"), ExtResource("14_progress"), ExtResource("13_renegotiate"), ExtResource("17_thank"), ExtResource("15_reproach"), ExtResource("3_acknowledge"), ExtResource("10_goodbye")] diff --git a/simulation/dialogue/resources/jajce_conversation_templates.tres b/simulation/dialogue/resources/jajce_conversation_templates.tres index ab03616..edaa6fb 100644 --- a/simulation/dialogue/resources/jajce_conversation_templates.tres +++ b/simulation/dialogue/resources/jajce_conversation_templates.tres @@ -4,6 +4,7 @@ [resource] script = ExtResource("1_catalog") +catalog_id = &"jajce_conversation_templates" line_templates = { "accept": ["That works for me.", "Yes, let us do that."], "acknowledge_supersession": ["Things have changed since then; I understand.", "That news has overtaken our earlier plan."], diff --git a/simulation/situations/SituationAlternativeDefinition.gd b/simulation/situations/SituationAlternativeDefinition.gd index aa2f148..c648d71 100644 --- a/simulation/situations/SituationAlternativeDefinition.gd +++ b/simulation/situations/SituationAlternativeDefinition.gd @@ -17,12 +17,27 @@ func validate() -> Array[String]: errors.append("display_name is empty for alternative '%s'" % alternative_id) if resolution_predicate_ids.is_empty(): errors.append("alternative '%s' has no resolution predicates" % alternative_id) + var seen_predicate_ids := {} for predicate_id in resolution_predicate_ids: if predicate_id.is_empty(): errors.append("alternative '%s' has an empty resolution predicate ID" % alternative_id) + elif seen_predicate_ids.has(predicate_id): + errors.append( + ( + "alternative '%s' has duplicate resolution predicate '%s'" + % [alternative_id, predicate_id] + ) + ) + seen_predicate_ids[predicate_id] = true + var seen_topic_ids := {} for topic_id in dialogue_topic_ids: if topic_id.is_empty(): errors.append("alternative '%s' has an empty dialogue topic ID" % alternative_id) + elif seen_topic_ids.has(topic_id): + errors.append( + "alternative '%s' has duplicate dialogue topic '%s'" % [alternative_id, topic_id] + ) + seen_topic_ids[topic_id] = true for term_key in commitment_terms: if not term_key is String and not term_key is StringName: errors.append("alternative '%s' has a non-string commitment term key" % alternative_id) diff --git a/simulation/situations/SituationDefinition.gd b/simulation/situations/SituationDefinition.gd index 1bc3186..6241df1 100644 --- a/simulation/situations/SituationDefinition.gd +++ b/simulation/situations/SituationDefinition.gd @@ -13,6 +13,7 @@ extends Resource @export_range(0, 1000000, 1) var expiry_ticks := 0 @export_range(0, 1000000, 1) var cooldown_ticks := 0 @export var dialogue_topic_ids: Array[StringName] = [] +@export var dialogue_intent_ids: Array[StringName] = [] @export var alternatives: Array[SituationAlternativeDefinition] = [] @@ -63,11 +64,24 @@ func validate( ) _validate_unique_names(dedupe_key_fields, "dedupe key", errors) _validate_unique_names(dialogue_topic_ids, "dialogue topic", errors) + _validate_unique_names(dialogue_intent_ids, "dialogue intent", errors) + var predicate_ids := {} + for predicate in trigger_predicates + resolution_predicates: + if predicate == null: + continue + if predicate_ids.has(predicate.predicate_id): + errors.append( + ( + "'%s' has duplicate predicate '%s' across trigger/resolution predicates" + % [situation_definition_id, predicate.predicate_id] + ) + ) + predicate_ids[predicate.predicate_id] = true var alternatives_by_id: Dictionary = {} for alternative in alternatives: if alternative == null: errors.append("'%s' contains a null alternative" % situation_definition_id) - continue + continue for error in alternative.validate(): errors.append("Alternative '%s': %s" % [alternative.alternative_id, error]) if alternatives_by_id.has(alternative.alternative_id): @@ -89,6 +103,34 @@ func validate( return errors +func duplicate_definition() -> SituationDefinition: + var copy := SituationDefinition.new() + copy.situation_definition_id = situation_definition_id + copy.display_name = display_name + copy.description = description + copy.severity = severity + copy.priority = priority + for predicate in trigger_predicates: + copy.trigger_predicates.append( + predicate.duplicate_predicate() if predicate != null else null + ) + for predicate in resolution_predicates: + copy.resolution_predicates.append( + predicate.duplicate_predicate() if predicate != null else null + ) + copy.expiry_predicate_ids = expiry_predicate_ids.duplicate() + copy.dedupe_key_fields = dedupe_key_fields.duplicate() + copy.expiry_ticks = expiry_ticks + copy.cooldown_ticks = cooldown_ticks + copy.dialogue_topic_ids = dialogue_topic_ids.duplicate() + copy.dialogue_intent_ids = dialogue_intent_ids.duplicate() + for alternative in alternatives: + copy.alternatives.append( + alternative.duplicate_alternative() if alternative != null else null + ) + return copy + + func get_predicate(predicate_id: StringName) -> SituationPredicate: for predicate in trigger_predicates: if predicate != null and predicate.predicate_id == predicate_id: diff --git a/simulation/situations/SituationSystem.gd b/simulation/situations/SituationSystem.gd index 5ab4b90..d6a646e 100644 --- a/simulation/situations/SituationSystem.gd +++ b/simulation/situations/SituationSystem.gd @@ -2,6 +2,7 @@ class_name SituationSystem extends RefCounted const MAX_CONCURRENT_SITUATIONS := 3 +const PANTRY_SHORTAGE_DEFINITION_PATH := "res://simulation/situations/resources/pantry_shortage.tres" const DEFINITION_PANTRY_SHORTAGE := &"situation_pantry_shortage" const PREDICATE_EVENT_TYPE := &"event_type" const PREDICATE_EVENT_PAYLOAD_EQUALS := &"event_payload_equals" @@ -26,68 +27,18 @@ var _next_situation_id := 0 static func create_default() -> SituationSystem: var system := SituationSystem.new() - var definitions: Array[SituationDefinition] = [create_pantry_shortage_definition()] + var definitions: Array[SituationDefinition] = [] + var catalog := ContentCatalog.create_core() + if catalog.is_valid(): + for definition in catalog.get_situations(): + definitions.append(definition.duplicate_definition()) system.configure(definitions) return system static func create_pantry_shortage_definition() -> SituationDefinition: - var definition := SituationDefinition.new() - definition.situation_definition_id = DEFINITION_PANTRY_SHORTAGE - definition.display_name = "The Pantry Is Empty" - definition.description = "The village pantry has run out of food." - definition.severity = 70 - definition.priority = 80 - definition.trigger_predicates = [ - _event_predicate( - &"pantry_withdrawal", PREDICATE_EVENT_TYPE, {"event_type": "storage_withdrawn"} - ), - _event_predicate( - &"withdrawal_from_pantry", - PREDICATE_EVENT_PARTICIPANT_ID, - {"role": "source", "entity_id": "village_pantry"} - ), - _event_predicate( - &"withdrawal_of_food", - PREDICATE_EVENT_PAYLOAD_EQUALS, - {"key": "item_id", "value": "food"} - ), - _state_predicate( - &"pantry_empty", PREDICATE_STATE_NUMBER_LTE, {"fact_key": "pantry.food", "value": 0.0} - ), - ] - definition.resolution_predicates = [ - _event_predicate( - &"pantry_restocked", - PREDICATE_EVENT_PANTRY_SUPPLIED, - {"target_id": "village_pantry", "item_id": "food", "amount": 1.0} - ) - ] - definition.expiry_predicate_ids = [EXPIRY_AGE_REACHED] - definition.dedupe_key_fields = [&"world_id", &"source_id"] - definition.expiry_ticks = 200 - definition.cooldown_ticks = 50 - definition.dialogue_topic_ids = [DIALOGUE_TOPIC_PANTRY_SHORTAGE] - var restock := SituationAlternativeDefinition.new() - restock.alternative_id = ALTERNATIVE_RESTOCK - restock.display_name = "Restock the pantry" - restock.description = "Bring at least one food item to the village pantry." - restock.resolution_predicate_ids = [&"pantry_restocked"] - restock.dialogue_topic_ids = [DIALOGUE_TOPIC_PANTRY_SHORTAGE] - restock.commitment_terms = { - "action_id": "deposit_food", - "target_id": "village_pantry", - "item_id": "food", - "amount": 1.0, - } - var endure := SituationAlternativeDefinition.new() - endure.alternative_id = ALTERNATIVE_ENDURE - endure.display_name = "Endure the shortage" - endure.description = "Make no promise and let the immediate concern pass." - endure.resolution_predicate_ids = [&"pantry_restocked"] - endure.dialogue_topic_ids = [DIALOGUE_TOPIC_PANTRY_SHORTAGE] - definition.alternatives = [restock, endure] - return definition + var authored := load(PANTRY_SHORTAGE_DEFINITION_PATH) as SituationDefinition + return authored.duplicate_definition() if authored != null else null func configure(definitions: Array[SituationDefinition]) -> Array[String]: diff --git a/simulation/situations/resources/pantry_shortage.tres b/simulation/situations/resources/pantry_shortage.tres new file mode 100644 index 0000000..64e693c --- /dev/null +++ b/simulation/situations/resources/pantry_shortage.tres @@ -0,0 +1,70 @@ +[gd_resource type="Resource" script_class="SituationDefinition" load_steps=10 format=3] + +[ext_resource type="Script" path="res://simulation/situations/SituationDefinition.gd" id="1_definition"] +[ext_resource type="Script" path="res://simulation/situations/SituationPredicate.gd" id="2_predicate"] +[ext_resource type="Script" path="res://simulation/situations/SituationAlternativeDefinition.gd" id="3_alternative"] + +[sub_resource type="Resource" id="Predicate_pantry_withdrawal"] +script = ExtResource("2_predicate") +predicate_id = &"pantry_withdrawal" +evaluator_id = &"event_type" +parameters = {"event_type": "storage_withdrawn"} + +[sub_resource type="Resource" id="Predicate_withdrawal_from_pantry"] +script = ExtResource("2_predicate") +predicate_id = &"withdrawal_from_pantry" +evaluator_id = &"event_participant_id" +parameters = {"entity_id": "village_pantry", "role": "source"} + +[sub_resource type="Resource" id="Predicate_withdrawal_of_food"] +script = ExtResource("2_predicate") +predicate_id = &"withdrawal_of_food" +evaluator_id = &"event_payload_equals" +parameters = {"key": "item_id", "value": "food"} + +[sub_resource type="Resource" id="Predicate_pantry_empty"] +script = ExtResource("2_predicate") +predicate_id = &"pantry_empty" +evaluator_id = &"state_number_lte" +source = "state" +parameters = {"fact_key": "pantry.food", "value": 0.0} + +[sub_resource type="Resource" id="Predicate_pantry_restocked"] +script = ExtResource("2_predicate") +predicate_id = &"pantry_restocked" +evaluator_id = &"event_pantry_supplied" +parameters = {"amount": 1.0, "item_id": "food", "target_id": "village_pantry"} + +[sub_resource type="Resource" id="Alternative_restock"] +script = ExtResource("3_alternative") +alternative_id = &"restock_pantry" +display_name = "Restock the pantry" +description = "Bring at least one food item to the village pantry." +resolution_predicate_ids = Array[StringName]([&"pantry_restocked"]) +dialogue_topic_ids = Array[StringName]([&"topic_pantry_shortage"]) +commitment_terms = {"action_id": "deposit_food", "amount": 1.0, "item_id": "food", "target_id": "village_pantry"} + +[sub_resource type="Resource" id="Alternative_endure"] +script = ExtResource("3_alternative") +alternative_id = &"endure_shortage" +display_name = "Endure the shortage" +description = "Make no promise and let the immediate concern pass." +resolution_predicate_ids = Array[StringName]([&"pantry_restocked"]) +dialogue_topic_ids = Array[StringName]([&"topic_pantry_shortage"]) + +[resource] +script = ExtResource("1_definition") +situation_definition_id = &"situation_pantry_shortage" +display_name = "The Pantry Is Empty" +description = "The village pantry has run out of food." +severity = 70 +priority = 80 +trigger_predicates = [SubResource("Predicate_pantry_withdrawal"), SubResource("Predicate_withdrawal_from_pantry"), SubResource("Predicate_withdrawal_of_food"), SubResource("Predicate_pantry_empty")] +resolution_predicates = [SubResource("Predicate_pantry_restocked")] +expiry_predicate_ids = Array[StringName]([&"age_reached"]) +dedupe_key_fields = Array[StringName]([&"world_id", &"source_id"]) +expiry_ticks = 200 +cooldown_ticks = 50 +dialogue_topic_ids = Array[StringName]([&"topic_pantry_shortage"]) +dialogue_intent_ids = Array[StringName]([&"accept", &"decline", &"describe_need", &"offer_help", &"report_progress"]) +alternatives = [SubResource("Alternative_restock"), SubResource("Alternative_endure")] diff --git a/tests/unit/test_authored_situation_dialogue_content.gd b/tests/unit/test_authored_situation_dialogue_content.gd new file mode 100644 index 0000000..d526b45 --- /dev/null +++ b/tests/unit/test_authored_situation_dialogue_content.gd @@ -0,0 +1,154 @@ +extends GutTest + + +func test_core_pack_owns_sorted_situation_intent_and_template_content() -> void: + var catalog := ContentCatalog.create_core() + assert_true(catalog.is_valid(), "%s" % [catalog.get_errors()]) + assert_eq(_situation_ids(catalog.get_situations()), [&"situation_pantry_shortage"]) + assert_eq(_intent_ids(catalog.get_dialogue_intents()), _sorted_ids(ConversationIntentIds.ALL)) + assert_eq( + _template_catalog_ids(catalog.get_dialogue_template_catalogs()), + [&"jajce_conversation_templates"] + ) + + var authored := catalog.get_situation(SituationSystem.DEFINITION_PANTRY_SHORTAGE) + var compatibility_copy := SituationSystem.create_pantry_shortage_definition() + assert_not_null(authored) + assert_not_same(authored, compatibility_copy) + assert_eq(authored.display_name, compatibility_copy.display_name) + assert_eq(authored.priority, compatibility_copy.priority) + assert_eq(authored.dialogue_intent_ids, compatibility_copy.dialogue_intent_ids) + compatibility_copy.priority = -100 + assert_eq(authored.priority, 80, "Compatibility callers cannot mutate authored content") + + var legacy_catalog := ( + load("res://simulation/dialogue/resources/jajce_conversation_intents.tres") + as ConversationIntentCatalog + ) + assert_not_null(legacy_catalog) + assert_true(legacy_catalog.rebuild().is_empty(), "%s" % [legacy_catalog.get_errors()]) + assert_eq(legacy_catalog.get_intent_ids(), _intent_ids(catalog.get_dialogue_intents())) + var templates := catalog.get_dialogue_template_catalog(&"jajce_conversation_templates") + assert_not_null(templates) + assert_true(templates.validate(legacy_catalog.get_intent_ids()).is_empty()) + + +func test_invalid_cross_references_fail_without_partial_publication() -> void: + var core := load(ContentCatalog.CORE_PACK_PATH) as SimulationContentPack + var addon := SimulationContentPack.new() + addon.pack_id = &"broken_authored_content" + addon.display_name = "Broken authored content" + addon.required_pack_ids = [&"core"] + + var broken_situation := _broken_situation() + assert_eq(broken_situation.alternatives.size(), 2) + assert_eq( + ( + broken_situation + . validate() + . filter(func(error: String) -> bool: return "alternative" in error.to_lower()) + . size() + ), + 3 + ) + addon.situations = [core.situations[0], broken_situation] + var broken_intent := ConversationIntentDefinition.new() + broken_intent.intent_id = &"broken_intent" + broken_intent.response_intent_ids = [&"missing_response"] + broken_intent.template_catalog_id = &"missing_template_catalog" + addon.dialogue_intents = [core.dialogue_intents[0], broken_intent] + var rogue_templates := ConversationTemplateCatalog.new() + rogue_templates.catalog_id = &"rogue_templates" + rogue_templates.line_templates = {"ghost_intent": ["Ghost line"]} + rogue_templates.option_templates = {"ghost_intent": ["Ghost option"]} + addon.dialogue_template_catalogs = [core.dialogue_template_catalogs[0], rogue_templates] + + var catalog := ContentCatalog.new() + var packs: Array[SimulationContentPack] = [addon, core] + var errors := catalog.rebuild(packs) + for expected in [ + "Duplicate situation_definition_id 'situation_pantry_shortage'", + "duplicate predicate 'shared_predicate' across trigger/resolution predicates", + "unsupported event predicate 'unknown_predicate_handler'", + "duplicate alternative 'duplicate_alternative'", + "references unknown resolution predicate 'missing_predicate'", + "references unknown dialogue intent 'missing_intent'", + "Duplicate dialogue intent_id 'accept'", + "references unknown response intent 'missing_response'", + "references unknown template catalog 'missing_template_catalog'", + "Duplicate dialogue template catalog_id 'jajce_conversation_templates'", + "Unknown line template intent 'ghost_intent'", + "Unknown option template intent 'ghost_intent'", + ]: + assert_true(_has_error(errors, expected), "%s missing from %s" % [expected, errors]) + + assert_false(catalog.is_valid()) + assert_true(catalog.get_situations().is_empty()) + assert_true(catalog.get_dialogue_intents().is_empty()) + assert_true(catalog.get_dialogue_template_catalogs().is_empty()) + assert_null(catalog.get_situation(&"broken_situation")) + assert_null(catalog.get_dialogue_intent(&"broken_intent")) + + +func _broken_situation() -> SituationDefinition: + var trigger := SituationPredicate.new() + trigger.predicate_id = &"shared_predicate" + trigger.evaluator_id = &"unknown_predicate_handler" + trigger.source = String(SituationPredicate.SOURCE_EVENT) + var resolution := SituationPredicate.new() + resolution.predicate_id = &"shared_predicate" + resolution.evaluator_id = &"event_type" + resolution.source = String(SituationPredicate.SOURCE_EVENT) + + var first_alternative := SituationAlternativeDefinition.new() + first_alternative.alternative_id = &"duplicate_alternative" + first_alternative.display_name = "First alternative" + first_alternative.resolution_predicate_ids = [&"missing_predicate"] + var second_alternative := first_alternative.duplicate_alternative() + + var definition := SituationDefinition.new() + definition.situation_definition_id = &"broken_situation" + definition.display_name = "Broken situation" + definition.trigger_predicates = [trigger] + definition.resolution_predicates = [resolution] + definition.dialogue_intent_ids = [&"missing_intent"] + definition.alternatives = [first_alternative, second_alternative] + return definition + + +func _situation_ids(definitions: Array[SituationDefinition]) -> Array[StringName]: + var ids: Array[StringName] = [] + for definition in definitions: + ids.append(definition.situation_definition_id) + return ids + + +func _intent_ids(definitions: Array[ConversationIntentDefinition]) -> Array[StringName]: + var ids: Array[StringName] = [] + for definition in definitions: + ids.append(definition.intent_id) + return ids + + +func _template_catalog_ids( + definitions: Array[ConversationTemplateCatalog], +) -> Array[StringName]: + var ids: Array[StringName] = [] + for definition in definitions: + ids.append(definition.catalog_id) + return ids + + +func _sorted_ids(source: Array[StringName]) -> Array[StringName]: + var ids := source.duplicate() + ids.sort_custom( + func(left: StringName, right: StringName) -> bool: return String(left) < String(right) + ) + return ids + + +func _has_error(errors: Array[String], fragment: String) -> bool: + for error in errors: + if fragment in error: + return true + return false diff --git a/tests/unit/test_authored_situation_dialogue_content.gd.uid b/tests/unit/test_authored_situation_dialogue_content.gd.uid new file mode 100644 index 0000000..15bb1a8 --- /dev/null +++ b/tests/unit/test_authored_situation_dialogue_content.gd.uid @@ -0,0 +1 @@ +uid://bupkjb7j38smr