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
@@ -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