feat: define authoritative action effects
This commit is contained in:
@@ -2,33 +2,38 @@ class_name ActionCommand
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
var _command_id: StringName
|
var _command_id: StringName
|
||||||
var _actor_id: StringName
|
var _actor: WorldEntityRef
|
||||||
var _action_id: StringName
|
var _action_id: StringName
|
||||||
var _target: WorldTargetHandle
|
var _target: WorldTargetHandle
|
||||||
var _parameters: Dictionary
|
var _parameters: Dictionary
|
||||||
|
var _expected_state_revision: int
|
||||||
|
|
||||||
|
|
||||||
func _init(
|
func _init(
|
||||||
command_id: StringName = &"",
|
command_id: StringName = &"",
|
||||||
actor_id: StringName = &"",
|
actor: Variant = null,
|
||||||
action_id: StringName = &"",
|
action_id: StringName = &"",
|
||||||
target: WorldTargetHandle = null,
|
target: WorldTargetHandle = null,
|
||||||
parameters: Dictionary = {}
|
parameters: Dictionary = {},
|
||||||
|
expected_state_revision: int = -1
|
||||||
) -> void:
|
) -> void:
|
||||||
_command_id = command_id
|
_command_id = command_id
|
||||||
_actor_id = actor_id
|
_actor = _normalize_actor(actor)
|
||||||
_action_id = action_id
|
_action_id = action_id
|
||||||
_target = target
|
_target = target
|
||||||
_parameters = parameters.duplicate(true)
|
_parameters = parameters.duplicate(true)
|
||||||
|
_expected_state_revision = expected_state_revision
|
||||||
|
|
||||||
|
|
||||||
func is_valid() -> bool:
|
func is_valid() -> bool:
|
||||||
return (
|
return (
|
||||||
not _command_id.is_empty()
|
not _command_id.is_empty()
|
||||||
and not _actor_id.is_empty()
|
and _actor != null
|
||||||
|
and _actor.is_valid()
|
||||||
and not _action_id.is_empty()
|
and not _action_id.is_empty()
|
||||||
and _target != null
|
and _target != null
|
||||||
and _target.is_well_formed()
|
and _target.is_well_formed()
|
||||||
|
and _expected_state_revision >= -1
|
||||||
and not WorldTargetCapability._contains_object(_parameters)
|
and not WorldTargetCapability._contains_object(_parameters)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,7 +43,11 @@ func get_command_id() -> StringName:
|
|||||||
|
|
||||||
|
|
||||||
func get_actor_id() -> StringName:
|
func get_actor_id() -> StringName:
|
||||||
return _actor_id
|
return _actor.get_entity_id() if _actor != null else &""
|
||||||
|
|
||||||
|
|
||||||
|
func get_actor() -> WorldEntityRef:
|
||||||
|
return _actor.duplicate_ref() if _actor != null else null
|
||||||
|
|
||||||
|
|
||||||
func get_action_id() -> StringName:
|
func get_action_id() -> StringName:
|
||||||
@@ -51,3 +60,15 @@ func get_target() -> WorldTargetHandle:
|
|||||||
|
|
||||||
func get_parameters() -> Dictionary:
|
func get_parameters() -> Dictionary:
|
||||||
return _parameters.duplicate(true)
|
return _parameters.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func get_expected_state_revision() -> int:
|
||||||
|
return _expected_state_revision
|
||||||
|
|
||||||
|
|
||||||
|
static func _normalize_actor(actor: Variant) -> WorldEntityRef:
|
||||||
|
if actor is WorldEntityRef:
|
||||||
|
return (actor as WorldEntityRef).duplicate_ref()
|
||||||
|
if actor is StringName or actor is String:
|
||||||
|
return WorldEntityRef.create(SimulationIds.ENTITY_PERSON, StringName(actor))
|
||||||
|
return null
|
||||||
|
|||||||
@@ -6,5 +6,9 @@ signal command_resolved(result: ActionResult)
|
|||||||
|
|
||||||
# Implementations must resolve the transient target handle and revalidate every
|
# Implementations must resolve the transient target handle and revalidate every
|
||||||
# precondition. An earlier ActionOffer never grants authority by itself.
|
# precondition. An earlier ActionOffer never grants authority by itself.
|
||||||
func submit_command(_command: ActionCommand) -> ActionResult:
|
func try_execute(_command: ActionCommand) -> ActionResult:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func submit_command(command: ActionCommand) -> ActionResult:
|
||||||
|
return try_execute(command)
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
class_name ActionEffect
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
const KIND_METRIC_DELTA := &"metric_delta"
|
||||||
|
const KIND_NEED_DELTA := &"need_delta"
|
||||||
|
const KIND_INVENTORY_TRANSFER := &"inventory_transfer"
|
||||||
|
const KIND_DAMAGE := &"damage"
|
||||||
|
const KIND_HEALING := &"healing"
|
||||||
|
const KIND_RELATIONSHIP_DELTA := &"relationship_delta"
|
||||||
|
const KIND_SCHEDULE_ACTION := &"schedule_action"
|
||||||
|
const KIND_SITE_CONDITION := &"site_condition"
|
||||||
|
const VALID_KINDS := [
|
||||||
|
KIND_METRIC_DELTA,
|
||||||
|
KIND_NEED_DELTA,
|
||||||
|
KIND_INVENTORY_TRANSFER,
|
||||||
|
KIND_DAMAGE,
|
||||||
|
KIND_HEALING,
|
||||||
|
KIND_RELATIONSHIP_DELTA,
|
||||||
|
KIND_SCHEDULE_ACTION,
|
||||||
|
KIND_SITE_CONDITION,
|
||||||
|
]
|
||||||
|
|
||||||
|
@export var effect_id: StringName
|
||||||
|
@export var effect_kind: StringName
|
||||||
|
@export var subject_key: StringName
|
||||||
|
@export var value := 0.0
|
||||||
|
@export var item_id: StringName
|
||||||
|
@export var amount := 0.0
|
||||||
|
@export var source_role: StringName
|
||||||
|
@export var destination_role: StringName
|
||||||
|
@export var scheduled_action_id: StringName
|
||||||
|
@export var parameters: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func validate() -> Array[String]:
|
||||||
|
var errors: Array[String] = []
|
||||||
|
if effect_id.is_empty():
|
||||||
|
errors.append("effect_id is empty")
|
||||||
|
if effect_kind not in VALID_KINDS:
|
||||||
|
errors.append("effect_kind '%s' is invalid for '%s'" % [effect_kind, effect_id])
|
||||||
|
if not is_finite(value) or not is_finite(amount):
|
||||||
|
errors.append("numeric values must be finite for '%s'" % effect_id)
|
||||||
|
match effect_kind:
|
||||||
|
KIND_METRIC_DELTA, KIND_NEED_DELTA, KIND_RELATIONSHIP_DELTA, KIND_SITE_CONDITION:
|
||||||
|
if subject_key.is_empty():
|
||||||
|
errors.append("subject_key is empty for '%s'" % effect_id)
|
||||||
|
KIND_INVENTORY_TRANSFER:
|
||||||
|
if item_id.is_empty() or amount <= 0.0:
|
||||||
|
errors.append(
|
||||||
|
"inventory transfer '%s' needs an item and positive amount" % effect_id
|
||||||
|
)
|
||||||
|
if source_role.is_empty() or destination_role.is_empty():
|
||||||
|
errors.append("inventory transfer '%s' needs source/destination roles" % effect_id)
|
||||||
|
KIND_DAMAGE, KIND_HEALING:
|
||||||
|
if amount <= 0.0:
|
||||||
|
errors.append("effect '%s' needs a positive amount" % effect_id)
|
||||||
|
KIND_SCHEDULE_ACTION:
|
||||||
|
if scheduled_action_id.is_empty():
|
||||||
|
errors.append("scheduled action is empty for '%s'" % effect_id)
|
||||||
|
if WorldTargetCapability._contains_object(parameters):
|
||||||
|
errors.append("parameters contain a runtime object for '%s'" % effect_id)
|
||||||
|
return errors
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c7wc7p30s3fsw
|
||||||
@@ -4,5 +4,9 @@ extends RefCounted
|
|||||||
|
|
||||||
# Offers are advisory presentation values. Implementations must not mutate
|
# Offers are advisory presentation values. Implementations must not mutate
|
||||||
# authoritative state while answering this query.
|
# authoritative state while answering this query.
|
||||||
func get_action_offers(_actor_id: StringName, _target: WorldTargetHandle) -> Array[ActionOffer]:
|
func get_offers(_actor: WorldEntityRef, _target: WorldTargetHandle) -> Array[ActionOffer]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
func get_action_offers(actor_id: StringName, target: WorldTargetHandle) -> Array[ActionOffer]:
|
||||||
|
return get_offers(WorldEntityRef.create(SimulationIds.ENTITY_PERSON, actor_id), target)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
class_name ActionDefinition
|
class_name ActionDefinition
|
||||||
extends Resource
|
extends Resource
|
||||||
|
|
||||||
|
const ActionEffectScript := preload("res://simulation/commands/ActionEffect.gd")
|
||||||
|
|
||||||
@export var action_id: StringName
|
@export var action_id: StringName
|
||||||
@export var display_name: String
|
@export var display_name: String
|
||||||
@export_range(0.0, 1000.0, 0.1) var default_duration := 1.0
|
@export_range(0.0, 1000.0, 0.1) var default_duration := 1.0
|
||||||
@@ -9,6 +11,14 @@ extends Resource
|
|||||||
@export var resource_action_id: StringName
|
@export var resource_action_id: StringName
|
||||||
@export var completion_cost_resource_id: StringName
|
@export var completion_cost_resource_id: StringName
|
||||||
@export_range(0.0, 1000.0, 0.1) var completion_cost_amount := 0.0
|
@export_range(0.0, 1000.0, 0.1) var completion_cost_amount := 0.0
|
||||||
|
@export var actor_capability_tags: Array[StringName] = []
|
||||||
|
@export var target_capability_tags: Array[StringName] = []
|
||||||
|
@export var selection_policy_id: StringName
|
||||||
|
@export var target_policy_id: StringName
|
||||||
|
@export var completion_handler_id: StringName
|
||||||
|
@export var effects: Array[Resource] = []
|
||||||
|
@export var event_type_id: StringName
|
||||||
|
@export var presentation_cue_id: StringName
|
||||||
|
|
||||||
|
|
||||||
func validate() -> Array[String]:
|
func validate() -> Array[String]:
|
||||||
@@ -35,8 +45,34 @@ func validate() -> Array[String]:
|
|||||||
errors.append("completion cost resource is required for '%s'" % action_id)
|
errors.append("completion cost resource is required for '%s'" % action_id)
|
||||||
if not completion_cost_resource_id.is_empty() and completion_cost_amount <= 0.0:
|
if not completion_cost_resource_id.is_empty() and completion_cost_amount <= 0.0:
|
||||||
errors.append("completion cost amount must be positive for '%s'" % action_id)
|
errors.append("completion cost amount must be positive for '%s'" % action_id)
|
||||||
|
_validate_unique_tags(actor_capability_tags, "actor capability", errors)
|
||||||
|
_validate_unique_tags(target_capability_tags, "target capability", errors)
|
||||||
|
var effect_ids := {}
|
||||||
|
for effect in effects:
|
||||||
|
if effect == null or effect.get_script() != ActionEffectScript:
|
||||||
|
errors.append("effects contains a null definition for '%s'" % action_id)
|
||||||
|
continue
|
||||||
|
for error in effect.call("validate"):
|
||||||
|
errors.append("ActionEffect: " + error)
|
||||||
|
var effect_id: StringName = effect.get("effect_id")
|
||||||
|
if effect_ids.has(effect_id):
|
||||||
|
errors.append("duplicate effect_id '%s' for '%s'" % [effect_id, action_id])
|
||||||
|
effect_ids[effect_id] = true
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
func has_completion_cost() -> bool:
|
func has_completion_cost() -> bool:
|
||||||
return not completion_cost_resource_id.is_empty() and completion_cost_amount > 0.0
|
return not completion_cost_resource_id.is_empty() and completion_cost_amount > 0.0
|
||||||
|
|
||||||
|
|
||||||
|
static func _validate_unique_tags(
|
||||||
|
tags: Array[StringName], label: String, errors: Array[String]
|
||||||
|
) -> void:
|
||||||
|
var seen := {}
|
||||||
|
for tag in tags:
|
||||||
|
if tag.is_empty():
|
||||||
|
errors.append("%s is empty" % label)
|
||||||
|
elif seen.has(tag):
|
||||||
|
errors.append("duplicate %s '%s'" % [label, tag])
|
||||||
|
else:
|
||||||
|
seen[tag] = true
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ func rebuild(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
_validate_action_references(actions, actions_by_id, professions_by_id, items_by_id, errors)
|
_validate_action_references(
|
||||||
|
actions, actions_by_id, professions_by_id, items_by_id, supported_handlers, errors
|
||||||
|
)
|
||||||
_validate_enemy_references(enemies, items_by_id, errors)
|
_validate_enemy_references(enemies, items_by_id, errors)
|
||||||
errors.sort()
|
errors.sort()
|
||||||
_errors = errors
|
_errors = errors
|
||||||
@@ -249,9 +251,23 @@ static func _validate_action_references(
|
|||||||
actions_by_id: Dictionary,
|
actions_by_id: Dictionary,
|
||||||
professions_by_id: Dictionary,
|
professions_by_id: Dictionary,
|
||||||
items_by_id: Dictionary,
|
items_by_id: Dictionary,
|
||||||
|
supported_handlers: Dictionary,
|
||||||
errors: Array[String]
|
errors: Array[String]
|
||||||
) -> void:
|
) -> void:
|
||||||
for definition in actions:
|
for definition in actions:
|
||||||
|
for handler_field in [
|
||||||
|
["selection policy", definition.selection_policy_id],
|
||||||
|
["target policy", definition.target_policy_id],
|
||||||
|
["completion handler", definition.completion_handler_id],
|
||||||
|
]:
|
||||||
|
var handler_id: StringName = handler_field[1]
|
||||||
|
if not handler_id.is_empty() and not supported_handlers.has(handler_id):
|
||||||
|
errors.append(
|
||||||
|
(
|
||||||
|
"Action '%s' references unknown %s '%s'"
|
||||||
|
% [definition.action_id, handler_field[0], handler_id]
|
||||||
|
)
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
not definition.preferred_profession_id.is_empty()
|
not definition.preferred_profession_id.is_empty()
|
||||||
and not professions_by_id.has(definition.preferred_profession_id)
|
and not professions_by_id.has(definition.preferred_profession_id)
|
||||||
|
|||||||
@@ -141,6 +141,13 @@ func _test_unknown_references_categories_and_handlers() -> void:
|
|||||||
var missing_cost_item := _action(&"missing_cost")
|
var missing_cost_item := _action(&"missing_cost")
|
||||||
missing_cost_item.completion_cost_resource_id = &"unknown_cost_item"
|
missing_cost_item.completion_cost_resource_id = &"unknown_cost_item"
|
||||||
missing_cost_item.completion_cost_amount = 1.0
|
missing_cost_item.completion_cost_amount = 1.0
|
||||||
|
missing_cost_item.selection_policy_id = &"unknown_selection"
|
||||||
|
missing_cost_item.target_policy_id = &"unknown_targeting"
|
||||||
|
missing_cost_item.completion_handler_id = &"unknown_completion"
|
||||||
|
var invalid_effect := ActionEffect.new()
|
||||||
|
invalid_effect.effect_id = &"broken_transfer"
|
||||||
|
invalid_effect.effect_kind = ActionEffect.KIND_INVENTORY_TRANSFER
|
||||||
|
missing_cost_item.effects = [invalid_effect]
|
||||||
var weapon_item := _weapon_item(&"test_weapon", "Test weapon")
|
var weapon_item := _weapon_item(&"test_weapon", "Test weapon")
|
||||||
var weapon_cost := _action(&"weapon_cost")
|
var weapon_cost := _action(&"weapon_cost")
|
||||||
weapon_cost.completion_cost_resource_id = weapon_item.item_id
|
weapon_cost.completion_cost_resource_id = weapon_item.item_id
|
||||||
@@ -161,6 +168,11 @@ func _test_unknown_references_categories_and_handlers() -> void:
|
|||||||
"unknown profession 'unknown_profession'",
|
"unknown profession 'unknown_profession'",
|
||||||
"unknown resource action 'unknown_resource_action'",
|
"unknown resource action 'unknown_resource_action'",
|
||||||
"unknown completion cost item 'unknown_cost_item'",
|
"unknown completion cost item 'unknown_cost_item'",
|
||||||
|
"unknown selection policy 'unknown_selection'",
|
||||||
|
"unknown target policy 'unknown_targeting'",
|
||||||
|
"unknown completion handler 'unknown_completion'",
|
||||||
|
"needs an item and positive amount",
|
||||||
|
"needs source/destination roles",
|
||||||
"non-resource completion cost item 'test_weapon'",
|
"non-resource completion cost item 'test_weapon'",
|
||||||
"unknown weapon 'unknown_weapon'",
|
"unknown weapon 'unknown_weapon'",
|
||||||
"non-weapon item 'ordinary_resource'",
|
"non-weapon item 'ordinary_resource'",
|
||||||
|
|||||||
Reference in New Issue
Block a user