feat: author emergent situations and dialogue
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")]
|
||||
Reference in New Issue
Block a user