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
@@ -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)
+43 -1
View File
@@ -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:
+8 -57
View File
@@ -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]:
@@ -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")]