feat: plan typed action effects
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
class_name ActionEffectPlanner
|
||||
extends RefCounted
|
||||
|
||||
enum Strategy {
|
||||
METRIC_DELTA,
|
||||
NEED_DELTA,
|
||||
INVENTORY_TRANSFER,
|
||||
DAMAGE,
|
||||
HEALING,
|
||||
RELATIONSHIP_DELTA,
|
||||
SCHEDULE_ACTION,
|
||||
SITE_CONDITION,
|
||||
}
|
||||
|
||||
var _strategies_by_kind: Dictionary = {}
|
||||
var _last_errors: Array[String] = []
|
||||
var _last_reason_trace: Array[StringName] = []
|
||||
|
||||
|
||||
func _init(register_default_strategies: bool = true) -> void:
|
||||
if register_default_strategies:
|
||||
for effect_kind in ActionEffect.VALID_KINDS:
|
||||
register_builtin_strategy(effect_kind)
|
||||
|
||||
|
||||
func register_builtin_strategy(effect_kind: StringName) -> bool:
|
||||
var strategy := _builtin_strategy(effect_kind)
|
||||
if strategy < 0:
|
||||
return false
|
||||
_strategies_by_kind[effect_kind] = strategy
|
||||
return true
|
||||
|
||||
|
||||
func unregister_strategy(effect_kind: StringName) -> bool:
|
||||
return _strategies_by_kind.erase(effect_kind)
|
||||
|
||||
|
||||
func has_strategy(effect_kind: StringName) -> bool:
|
||||
return _strategies_by_kind.has(effect_kind)
|
||||
|
||||
|
||||
func get_last_errors() -> Array[String]:
|
||||
return _last_errors.duplicate()
|
||||
|
||||
|
||||
func get_last_reason_trace() -> Array[StringName]:
|
||||
return _last_reason_trace.duplicate()
|
||||
|
||||
|
||||
func plan(definition: ActionDefinition, actor: WorldEntityRef) -> ActionEffectPlan:
|
||||
_last_errors.clear()
|
||||
_last_reason_trace = [&"planning_started"]
|
||||
if not _actor_is_supported(actor):
|
||||
return _fail("actor must be a player or person reference")
|
||||
_last_reason_trace.append(&"shared_actor_contract_validated")
|
||||
if definition == null:
|
||||
return _fail("action definition is missing")
|
||||
var definition_errors := definition.validate()
|
||||
if not definition_errors.is_empty():
|
||||
for error in definition_errors:
|
||||
_last_errors.append("ActionDefinition: " + error)
|
||||
return _finish_failure()
|
||||
if definition.effects.is_empty():
|
||||
return _fail("action definition has no effects")
|
||||
_last_reason_trace.append(&"action_definition_validated")
|
||||
|
||||
var operations: Array[Dictionary] = []
|
||||
var seen_effect_ids := {}
|
||||
for authored_order in definition.effects.size():
|
||||
var effect := definition.effects[authored_order] as ActionEffect
|
||||
if effect == null:
|
||||
return _fail("effect %d is not an ActionEffect" % authored_order)
|
||||
if seen_effect_ids.has(effect.effect_id):
|
||||
return _fail("duplicate effect_id '%s'" % effect.effect_id)
|
||||
seen_effect_ids[effect.effect_id] = true
|
||||
if not _strategies_by_kind.has(effect.effect_kind):
|
||||
return _fail("missing strategy for effect kind '%s'" % effect.effect_kind)
|
||||
if not ActionEffectPlan.is_primitive_record(effect.parameters):
|
||||
return _fail("effect '%s' parameters are not primitive" % effect.effect_id)
|
||||
var operation := _plan_effect(effect, authored_order)
|
||||
if operation.is_empty():
|
||||
return _fail("strategy failed for effect '%s'" % effect.effect_id)
|
||||
operations.append(operation)
|
||||
_last_reason_trace.append(StringName("strategy_%s_planned" % effect.effect_kind))
|
||||
|
||||
_last_reason_trace.append(&"authored_order_preserved")
|
||||
var plan_trace := _last_reason_trace.duplicate()
|
||||
plan_trace.append(&"plan_validated")
|
||||
var result := ActionEffectPlan.new(definition.action_id, operations, plan_trace)
|
||||
if not result.is_valid():
|
||||
_last_errors.append_array(result.get_errors())
|
||||
return _finish_failure()
|
||||
_last_reason_trace = result.get_reason_trace()
|
||||
return result
|
||||
|
||||
|
||||
func _plan_effect(effect: ActionEffect, authored_order: int) -> Dictionary:
|
||||
var strategy := int(_strategies_by_kind.get(effect.effect_kind, -1))
|
||||
var payload := {}
|
||||
match strategy:
|
||||
Strategy.METRIC_DELTA, Strategy.NEED_DELTA, Strategy.RELATIONSHIP_DELTA:
|
||||
payload = {
|
||||
"subject_key": String(effect.subject_key),
|
||||
"delta": effect.value,
|
||||
"parameters": effect.parameters.duplicate(true),
|
||||
}
|
||||
Strategy.INVENTORY_TRANSFER:
|
||||
payload = {
|
||||
"item_id": String(effect.item_id),
|
||||
"amount": effect.amount,
|
||||
"source_role": String(effect.source_role),
|
||||
"destination_role": String(effect.destination_role),
|
||||
"parameters": effect.parameters.duplicate(true),
|
||||
}
|
||||
Strategy.DAMAGE, Strategy.HEALING:
|
||||
payload = {"amount": effect.amount, "parameters": effect.parameters.duplicate(true)}
|
||||
Strategy.SCHEDULE_ACTION:
|
||||
payload = {
|
||||
"scheduled_action_id": String(effect.scheduled_action_id),
|
||||
"parameters": effect.parameters.duplicate(true),
|
||||
}
|
||||
Strategy.SITE_CONDITION:
|
||||
payload = {
|
||||
"subject_key": String(effect.subject_key),
|
||||
"value": effect.value,
|
||||
"parameters": effect.parameters.duplicate(true),
|
||||
}
|
||||
_:
|
||||
return {}
|
||||
return {
|
||||
"operation_id": "%s:%03d" % [effect.effect_id, authored_order],
|
||||
"effect_id": String(effect.effect_id),
|
||||
"operation_kind": String(effect.effect_kind),
|
||||
"authored_order": authored_order,
|
||||
"payload": payload,
|
||||
}
|
||||
|
||||
|
||||
func _fail(message: String) -> ActionEffectPlan:
|
||||
_last_errors.append(message)
|
||||
return _finish_failure()
|
||||
|
||||
|
||||
func _finish_failure() -> ActionEffectPlan:
|
||||
_last_errors.sort()
|
||||
_last_reason_trace.append(&"planning_failed_closed")
|
||||
return null
|
||||
|
||||
|
||||
static func _actor_is_supported(actor: WorldEntityRef) -> bool:
|
||||
return (
|
||||
actor != null
|
||||
and actor.is_valid()
|
||||
and actor.get_entity_type() in [SimulationIds.ENTITY_PLAYER, SimulationIds.ENTITY_PERSON]
|
||||
)
|
||||
|
||||
|
||||
static func _builtin_strategy(effect_kind: StringName) -> int:
|
||||
match effect_kind:
|
||||
ActionEffect.KIND_METRIC_DELTA:
|
||||
return Strategy.METRIC_DELTA
|
||||
ActionEffect.KIND_NEED_DELTA:
|
||||
return Strategy.NEED_DELTA
|
||||
ActionEffect.KIND_INVENTORY_TRANSFER:
|
||||
return Strategy.INVENTORY_TRANSFER
|
||||
ActionEffect.KIND_DAMAGE:
|
||||
return Strategy.DAMAGE
|
||||
ActionEffect.KIND_HEALING:
|
||||
return Strategy.HEALING
|
||||
ActionEffect.KIND_RELATIONSHIP_DELTA:
|
||||
return Strategy.RELATIONSHIP_DELTA
|
||||
ActionEffect.KIND_SCHEDULE_ACTION:
|
||||
return Strategy.SCHEDULE_ACTION
|
||||
ActionEffect.KIND_SITE_CONDITION:
|
||||
return Strategy.SITE_CONDITION
|
||||
return -1
|
||||
Reference in New Issue
Block a user