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