feat: project catalog interaction offers
This commit is contained in:
@@ -0,0 +1,167 @@
|
|||||||
|
class_name CatalogInteractionService
|
||||||
|
extends InteractionService
|
||||||
|
|
||||||
|
const CAPABILITY_PRIORITY_ATTRIBUTE := &"offer_priority"
|
||||||
|
const PARAM_CAPABILITY_ATTRIBUTES := &"capability_attributes"
|
||||||
|
const PARAM_DECISION_PARAMETERS := &"decision_parameters"
|
||||||
|
const PARAM_REASON_CODE := &"reason_code"
|
||||||
|
const PARAM_REASON_TRACE := &"reason_trace"
|
||||||
|
|
||||||
|
const REASON_SERVICE_INVALID := &"interaction_service_invalid"
|
||||||
|
const REASON_ACTOR_INVALID := &"interaction_actor_invalid"
|
||||||
|
const REASON_TARGET_STALE := &"interaction_target_stale"
|
||||||
|
const REASON_TARGET_INVALID := &"interaction_target_invalid"
|
||||||
|
const REASON_DECISION_INVALID := &"availability_decision_invalid"
|
||||||
|
|
||||||
|
var _registry: WorldTargetRegistry
|
||||||
|
var _catalog: ContentCatalog
|
||||||
|
var _availability_evaluator: Callable
|
||||||
|
var _last_query_reason_trace: Array[StringName] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
registry: WorldTargetRegistry = null,
|
||||||
|
catalog: ContentCatalog = null,
|
||||||
|
availability_evaluator: Callable = Callable()
|
||||||
|
) -> void:
|
||||||
|
_registry = registry
|
||||||
|
_catalog = catalog
|
||||||
|
_availability_evaluator = availability_evaluator
|
||||||
|
|
||||||
|
|
||||||
|
func is_configured() -> bool:
|
||||||
|
return _registry != null and _catalog != null and _catalog.is_valid()
|
||||||
|
|
||||||
|
|
||||||
|
func get_last_query_reason_trace() -> Array[StringName]:
|
||||||
|
return _last_query_reason_trace.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
# The optional evaluator is advisory and pure. It receives isolated copies of
|
||||||
|
# actor, target descriptor, action definition, and target capability, and must
|
||||||
|
# return an InteractionAvailabilityDecision. This service never receives an
|
||||||
|
# authority mutation callback.
|
||||||
|
func get_offers(actor: WorldEntityRef, target: WorldTargetHandle) -> Array[ActionOffer]:
|
||||||
|
_last_query_reason_trace.clear()
|
||||||
|
if not is_configured():
|
||||||
|
_last_query_reason_trace.append(REASON_SERVICE_INVALID)
|
||||||
|
return []
|
||||||
|
if not _actor_is_supported(actor):
|
||||||
|
_last_query_reason_trace.append(REASON_ACTOR_INVALID)
|
||||||
|
return []
|
||||||
|
if not _registry.is_handle_valid(target):
|
||||||
|
_last_query_reason_trace.append(REASON_TARGET_STALE)
|
||||||
|
return []
|
||||||
|
var descriptor := _registry.resolve_handle(target)
|
||||||
|
if descriptor == null or not descriptor.is_valid():
|
||||||
|
_last_query_reason_trace.append(REASON_TARGET_INVALID)
|
||||||
|
return []
|
||||||
|
|
||||||
|
var offers: Array[ActionOffer] = []
|
||||||
|
for capability_id in descriptor.get_capability_ids():
|
||||||
|
var definition := _catalog.get_action(capability_id)
|
||||||
|
if definition == null or not definition.validate().is_empty():
|
||||||
|
continue
|
||||||
|
var capability := descriptor.get_capability(capability_id)
|
||||||
|
if capability == null or not capability.is_valid():
|
||||||
|
continue
|
||||||
|
var priority_result := _offer_priority(capability)
|
||||||
|
var decision := _evaluate_availability(actor, descriptor, definition, capability)
|
||||||
|
var reason_trace: Array[StringName] = [
|
||||||
|
&"target_handle_live", &"target_capability_matched", &"action_definition_authored"
|
||||||
|
]
|
||||||
|
reason_trace.append_array(priority_result["reason_trace"])
|
||||||
|
reason_trace.append_array(decision.get_reason_trace())
|
||||||
|
var parameters := {
|
||||||
|
String(PARAM_CAPABILITY_ATTRIBUTES): capability.get_attributes(),
|
||||||
|
String(PARAM_DECISION_PARAMETERS): decision.get_parameters(),
|
||||||
|
String(PARAM_REASON_CODE): String(decision.get_reason_code()),
|
||||||
|
String(PARAM_REASON_TRACE): reason_trace,
|
||||||
|
}
|
||||||
|
var offer := ActionOffer.new(
|
||||||
|
_offer_id(target, capability_id),
|
||||||
|
capability_id,
|
||||||
|
_copy_handle(target),
|
||||||
|
definition.display_name,
|
||||||
|
decision.is_enabled(),
|
||||||
|
decision.get_reason_message() if not decision.is_enabled() else "",
|
||||||
|
parameters,
|
||||||
|
float(priority_result["priority"])
|
||||||
|
)
|
||||||
|
if offer.is_valid():
|
||||||
|
offers.append(offer)
|
||||||
|
offers.sort_custom(_offer_less)
|
||||||
|
_last_query_reason_trace = [&"offers_projected", &"stable_priority_action_order"]
|
||||||
|
return offers
|
||||||
|
|
||||||
|
|
||||||
|
func _evaluate_availability(
|
||||||
|
actor: WorldEntityRef,
|
||||||
|
descriptor: WorldTargetDescriptor,
|
||||||
|
definition: ActionDefinition,
|
||||||
|
capability: WorldTargetCapability
|
||||||
|
) -> InteractionAvailabilityDecision:
|
||||||
|
if not _availability_evaluator.is_valid():
|
||||||
|
return InteractionAvailabilityDecision.allowed([&"availability_default_allowed"])
|
||||||
|
var definition_copy := definition.duplicate(true) as ActionDefinition
|
||||||
|
var result: Variant = _availability_evaluator.call(
|
||||||
|
actor.duplicate_ref(), descriptor.copy(), definition_copy, capability.copy()
|
||||||
|
)
|
||||||
|
var decision := result as InteractionAvailabilityDecision
|
||||||
|
if decision != null and decision.is_valid():
|
||||||
|
return decision
|
||||||
|
return (
|
||||||
|
InteractionAvailabilityDecision
|
||||||
|
. denied(
|
||||||
|
REASON_DECISION_INVALID,
|
||||||
|
"Availability policy returned an invalid decision",
|
||||||
|
[&"availability_callback_invalid"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _offer_priority(capability: WorldTargetCapability) -> Dictionary:
|
||||||
|
var raw_priority: Variant = capability.get_attribute(CAPABILITY_PRIORITY_ATTRIBUTE, 0.0)
|
||||||
|
if raw_priority is int or raw_priority is float:
|
||||||
|
var priority := float(raw_priority)
|
||||||
|
if is_finite(priority):
|
||||||
|
return {"priority": priority, "reason_trace": [&"offer_priority_authored"]}
|
||||||
|
return {"priority": 0.0, "reason_trace": [&"offer_priority_defaulted"]}
|
||||||
|
|
||||||
|
|
||||||
|
func _offer_id(target: WorldTargetHandle, action_id: StringName) -> StringName:
|
||||||
|
var stable_source := (
|
||||||
|
"%s|%s|%s|%d|%s"
|
||||||
|
% [
|
||||||
|
target.get_context_id(),
|
||||||
|
target.get_target_kind(),
|
||||||
|
target.get_target_id(),
|
||||||
|
target.get_generation(),
|
||||||
|
action_id,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return StringName("offer_" + stable_source.sha256_text().substr(0, 24))
|
||||||
|
|
||||||
|
|
||||||
|
func _copy_handle(source: WorldTargetHandle) -> WorldTargetHandle:
|
||||||
|
return WorldTargetHandle.new(
|
||||||
|
source.get_registry_instance_id(),
|
||||||
|
source.get_context_id(),
|
||||||
|
source.get_target_id(),
|
||||||
|
source.get_target_kind(),
|
||||||
|
source.get_generation()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 _offer_less(first: ActionOffer, second: ActionOffer) -> bool:
|
||||||
|
if first.get_priority() != second.get_priority():
|
||||||
|
return first.get_priority() > second.get_priority()
|
||||||
|
return String(first.get_action_id()) < String(second.get_action_id())
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bii1h3n8sv10q
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
class_name InteractionAvailabilityDecision
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
var _enabled := true
|
||||||
|
var _reason_code: StringName
|
||||||
|
var _reason_message := ""
|
||||||
|
var _reason_trace: Array[StringName] = []
|
||||||
|
var _parameters: Dictionary = {}
|
||||||
|
var _valid := false
|
||||||
|
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
enabled: bool = true,
|
||||||
|
reason_code: StringName = &"",
|
||||||
|
reason_message: String = "",
|
||||||
|
reason_trace: Array = [],
|
||||||
|
parameters: Dictionary = {}
|
||||||
|
) -> void:
|
||||||
|
_enabled = enabled
|
||||||
|
_reason_code = reason_code
|
||||||
|
_reason_message = reason_message
|
||||||
|
_parameters = parameters.duplicate(true)
|
||||||
|
_valid = not WorldTargetCapability._contains_object(_parameters)
|
||||||
|
for trace_value in reason_trace:
|
||||||
|
if trace_value is not String and trace_value is not StringName:
|
||||||
|
_valid = false
|
||||||
|
continue
|
||||||
|
var trace_id := StringName(trace_value)
|
||||||
|
if trace_id.is_empty():
|
||||||
|
_valid = false
|
||||||
|
continue
|
||||||
|
_reason_trace.append(trace_id)
|
||||||
|
if not _enabled and (_reason_code.is_empty() or _reason_message.is_empty()):
|
||||||
|
_valid = false
|
||||||
|
|
||||||
|
|
||||||
|
static func allowed(
|
||||||
|
reason_trace: Array = [&"availability_allowed"], parameters: Dictionary = {}
|
||||||
|
) -> InteractionAvailabilityDecision:
|
||||||
|
return InteractionAvailabilityDecision.new(true, &"", "", reason_trace, parameters)
|
||||||
|
|
||||||
|
|
||||||
|
static func denied(
|
||||||
|
reason_code: StringName,
|
||||||
|
reason_message: String,
|
||||||
|
reason_trace: Array,
|
||||||
|
parameters: Dictionary = {}
|
||||||
|
) -> InteractionAvailabilityDecision:
|
||||||
|
return InteractionAvailabilityDecision.new(
|
||||||
|
false, reason_code, reason_message, reason_trace, parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func is_valid() -> bool:
|
||||||
|
return _valid
|
||||||
|
|
||||||
|
|
||||||
|
func is_enabled() -> bool:
|
||||||
|
return _enabled
|
||||||
|
|
||||||
|
|
||||||
|
func get_reason_code() -> StringName:
|
||||||
|
return _reason_code
|
||||||
|
|
||||||
|
|
||||||
|
func get_reason_message() -> String:
|
||||||
|
return _reason_message
|
||||||
|
|
||||||
|
|
||||||
|
func get_reason_trace() -> Array[StringName]:
|
||||||
|
return _reason_trace.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
func get_parameters() -> Dictionary:
|
||||||
|
return _parameters.duplicate(true)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d2cn8m03q4yvy
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
extends GutTest
|
||||||
|
|
||||||
|
const TARGET_ID := &"shared_activity"
|
||||||
|
|
||||||
|
var registry: WorldTargetRegistry
|
||||||
|
var catalog: ContentCatalog
|
||||||
|
var evaluated_actor_types: Array[StringName] = []
|
||||||
|
var availability_call_count := 0
|
||||||
|
|
||||||
|
|
||||||
|
func before_each() -> void:
|
||||||
|
registry = WorldTargetRegistry.new(&"interaction_context", &"test_world", &"test_location")
|
||||||
|
catalog = ContentCatalog.create_core()
|
||||||
|
evaluated_actor_types.clear()
|
||||||
|
availability_call_count = 0
|
||||||
|
assert_true(catalog.is_valid(), "%s" % [catalog.get_errors()])
|
||||||
|
|
||||||
|
|
||||||
|
func test_player_and_person_receive_identical_copied_stably_ordered_offers() -> void:
|
||||||
|
var handle := _register_target(
|
||||||
|
[
|
||||||
|
_capability(SimulationIds.ACTION_REST, 20.0),
|
||||||
|
_capability(&"missing_authored_action", 100.0),
|
||||||
|
_capability(SimulationIds.ACTION_STUDY, 20.0),
|
||||||
|
_capability(SimulationIds.ACTION_PATROL, 40.0),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
var service := CatalogInteractionService.new(registry, catalog)
|
||||||
|
var player_offers := service.get_offers(_player_ref(), handle)
|
||||||
|
var person_offers := service.get_offers(_person_ref(), handle)
|
||||||
|
|
||||||
|
assert_eq(
|
||||||
|
_action_ids(player_offers),
|
||||||
|
[SimulationIds.ACTION_PATROL, SimulationIds.ACTION_REST, SimulationIds.ACTION_STUDY],
|
||||||
|
)
|
||||||
|
assert_eq(_offer_snapshots(player_offers), _offer_snapshots(person_offers))
|
||||||
|
assert_eq(
|
||||||
|
_offer_snapshots(person_offers), _offer_snapshots(service.get_action_offers(&"7", handle))
|
||||||
|
)
|
||||||
|
assert_eq(player_offers[0].get_priority(), 40.0)
|
||||||
|
assert_eq(player_offers[1].get_priority(), 20.0)
|
||||||
|
assert_eq(player_offers[2].get_priority(), 20.0)
|
||||||
|
assert_ne(player_offers[0], person_offers[0], "Each query should return fresh offer values")
|
||||||
|
assert_ne(player_offers[0].get_target(), handle, "Offers should carry copied handles")
|
||||||
|
assert_not_null(registry.resolve_handle(player_offers[0].get_target()))
|
||||||
|
assert_eq(
|
||||||
|
service.get_last_query_reason_trace(),
|
||||||
|
[&"offers_projected", &"stable_priority_action_order"],
|
||||||
|
)
|
||||||
|
|
||||||
|
var caller_parameters := player_offers[0].get_parameters()
|
||||||
|
caller_parameters["capability_attributes"]["offer_priority"] = -500.0
|
||||||
|
assert_eq(
|
||||||
|
float(player_offers[0].get_parameters()["capability_attributes"]["offer_priority"]),
|
||||||
|
40.0,
|
||||||
|
"ActionOffer parameters should be isolated from caller mutation",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func test_availability_callback_exposes_disabled_offer_and_cannot_mutate_sources() -> void:
|
||||||
|
var handle := _register_target(
|
||||||
|
[
|
||||||
|
_capability(SimulationIds.ACTION_REST, 10.0),
|
||||||
|
_capability(SimulationIds.ACTION_STUDY, 10.0),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
var descriptor_before := registry.resolve_handle(handle).to_dictionary()
|
||||||
|
var authored_study_name := catalog.get_action(SimulationIds.ACTION_STUDY).display_name
|
||||||
|
var service := CatalogInteractionService.new(registry, catalog, _availability_policy)
|
||||||
|
var player_offers := service.get_offers(_player_ref(), handle)
|
||||||
|
var person_offers := service.get_offers(_person_ref(), handle)
|
||||||
|
|
||||||
|
assert_eq(_offer_snapshots(player_offers), _offer_snapshots(person_offers))
|
||||||
|
assert_eq(
|
||||||
|
evaluated_actor_types,
|
||||||
|
[
|
||||||
|
SimulationIds.ENTITY_PLAYER,
|
||||||
|
SimulationIds.ENTITY_PLAYER,
|
||||||
|
SimulationIds.ENTITY_PERSON,
|
||||||
|
SimulationIds.ENTITY_PERSON
|
||||||
|
]
|
||||||
|
)
|
||||||
|
var study_offer := _offer_for(player_offers, SimulationIds.ACTION_STUDY)
|
||||||
|
assert_not_null(study_offer)
|
||||||
|
assert_false(study_offer.is_enabled())
|
||||||
|
assert_eq(study_offer.get_rejection_reason(), "Actor lacks study permission")
|
||||||
|
var parameters := study_offer.get_parameters()
|
||||||
|
assert_eq(parameters["reason_code"], "permission_denied")
|
||||||
|
assert_has(parameters["reason_trace"], &"permission_checked")
|
||||||
|
assert_has(parameters["reason_trace"], &"permission_denied")
|
||||||
|
assert_eq(parameters["decision_parameters"], {"required_role": "scholar"})
|
||||||
|
assert_eq(
|
||||||
|
registry.resolve_handle(handle).to_dictionary(),
|
||||||
|
descriptor_before,
|
||||||
|
"The evaluator should receive a descriptor copy",
|
||||||
|
)
|
||||||
|
assert_eq(
|
||||||
|
catalog.get_action(SimulationIds.ACTION_STUDY).display_name,
|
||||||
|
authored_study_name,
|
||||||
|
"The evaluator should receive an authored-definition copy",
|
||||||
|
)
|
||||||
|
assert_eq(study_offer.get_display_name(), authored_study_name)
|
||||||
|
|
||||||
|
|
||||||
|
func test_stale_foreign_and_unsupported_actor_queries_are_rejected_before_policy() -> void:
|
||||||
|
var handle := _register_target([_capability(SimulationIds.ACTION_REST, 0.0)])
|
||||||
|
var service := CatalogInteractionService.new(registry, catalog, _counting_allow_policy)
|
||||||
|
assert_eq(service.get_offers(_player_ref(), handle).size(), 1)
|
||||||
|
assert_eq(availability_call_count, 1)
|
||||||
|
assert_true(registry.unregister_target(handle))
|
||||||
|
assert_true(service.get_offers(_player_ref(), handle).is_empty())
|
||||||
|
assert_eq(service.get_last_query_reason_trace(), [&"interaction_target_stale"])
|
||||||
|
|
||||||
|
var foreign_registry := WorldTargetRegistry.new(&"foreign_context")
|
||||||
|
var foreign_provider := Node.new()
|
||||||
|
add_child_autofree(foreign_provider)
|
||||||
|
var foreign_handle := (
|
||||||
|
foreign_registry
|
||||||
|
. register_target(
|
||||||
|
(
|
||||||
|
WorldTargetDescriptor
|
||||||
|
. new(
|
||||||
|
TARGET_ID,
|
||||||
|
&"activity",
|
||||||
|
[_capability(SimulationIds.ACTION_REST, 0.0)],
|
||||||
|
Vector3.ZERO,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
foreign_provider,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert_true(service.get_offers(_player_ref(), foreign_handle).is_empty())
|
||||||
|
assert_eq(service.get_last_query_reason_trace(), [&"interaction_target_stale"])
|
||||||
|
assert_true(
|
||||||
|
service.get_offers(WorldEntityRef.create(&"world", &"world_1"), foreign_handle).is_empty()
|
||||||
|
)
|
||||||
|
assert_eq(service.get_last_query_reason_trace(), [&"interaction_actor_invalid"])
|
||||||
|
assert_eq(
|
||||||
|
availability_call_count,
|
||||||
|
1,
|
||||||
|
"Invalid actor and target queries must not invoke the availability policy",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func test_invalid_availability_decision_is_a_disabled_explainable_offer() -> void:
|
||||||
|
var handle := _register_target([_capability(SimulationIds.ACTION_REST, NAN)])
|
||||||
|
var service := CatalogInteractionService.new(registry, catalog, _invalid_policy)
|
||||||
|
var offers := service.get_offers(_person_ref(), handle)
|
||||||
|
assert_eq(offers.size(), 1)
|
||||||
|
assert_false(offers[0].is_enabled())
|
||||||
|
assert_eq(offers[0].get_priority(), 0.0)
|
||||||
|
assert_eq(offers[0].get_rejection_reason(), "Availability policy returned an invalid decision")
|
||||||
|
var parameters := offers[0].get_parameters()
|
||||||
|
assert_eq(parameters["reason_code"], "availability_decision_invalid")
|
||||||
|
assert_has(parameters["reason_trace"], &"offer_priority_defaulted")
|
||||||
|
assert_has(parameters["reason_trace"], &"availability_callback_invalid")
|
||||||
|
|
||||||
|
|
||||||
|
func test_abstract_target_offers_do_not_depend_on_a_loaded_provider() -> void:
|
||||||
|
var handle := registry.register_target(
|
||||||
|
WorldTargetDescriptor.new(
|
||||||
|
&"abstract_activity",
|
||||||
|
&"activity",
|
||||||
|
[_capability(SimulationIds.ACTION_PATROL, 5.0)],
|
||||||
|
Vector3.ZERO,
|
||||||
|
"Abstract guard post"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert_true(registry.is_handle_valid(handle))
|
||||||
|
assert_false(registry.is_handle_valid(handle, true))
|
||||||
|
var offers := CatalogInteractionService.new(registry, catalog).get_offers(_person_ref(), handle)
|
||||||
|
assert_eq(offers.size(), 1)
|
||||||
|
assert_eq(offers[0].get_action_id(), SimulationIds.ACTION_PATROL)
|
||||||
|
assert_true(offers[0].is_enabled())
|
||||||
|
|
||||||
|
|
||||||
|
func _availability_policy(
|
||||||
|
actor: WorldEntityRef,
|
||||||
|
descriptor: WorldTargetDescriptor,
|
||||||
|
definition: ActionDefinition,
|
||||||
|
capability: WorldTargetCapability
|
||||||
|
) -> InteractionAvailabilityDecision:
|
||||||
|
evaluated_actor_types.append(actor.get_entity_type())
|
||||||
|
actor._entity_id = &"mutated_copy"
|
||||||
|
descriptor._display_name = "Mutated copy"
|
||||||
|
definition.display_name = "Mutated copy"
|
||||||
|
capability._attributes["offer_priority"] = -1000.0
|
||||||
|
if definition.action_id == SimulationIds.ACTION_STUDY:
|
||||||
|
return (
|
||||||
|
InteractionAvailabilityDecision
|
||||||
|
. denied(
|
||||||
|
&"permission_denied",
|
||||||
|
"Actor lacks study permission",
|
||||||
|
[&"permission_checked", &"permission_denied"],
|
||||||
|
{"required_role": "scholar"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return InteractionAvailabilityDecision.allowed([&"permission_checked", &"permission_allowed"])
|
||||||
|
|
||||||
|
|
||||||
|
func _counting_allow_policy(
|
||||||
|
_actor: WorldEntityRef,
|
||||||
|
_descriptor: WorldTargetDescriptor,
|
||||||
|
_definition: ActionDefinition,
|
||||||
|
_capability: WorldTargetCapability
|
||||||
|
) -> InteractionAvailabilityDecision:
|
||||||
|
availability_call_count += 1
|
||||||
|
return InteractionAvailabilityDecision.allowed()
|
||||||
|
|
||||||
|
|
||||||
|
func _invalid_policy(
|
||||||
|
_actor: WorldEntityRef,
|
||||||
|
_descriptor: WorldTargetDescriptor,
|
||||||
|
_definition: ActionDefinition,
|
||||||
|
_capability: WorldTargetCapability
|
||||||
|
) -> InteractionAvailabilityDecision:
|
||||||
|
return InteractionAvailabilityDecision.new(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _register_target(capabilities: Array) -> WorldTargetHandle:
|
||||||
|
var provider := Node.new()
|
||||||
|
add_child_autofree(provider)
|
||||||
|
return (
|
||||||
|
registry
|
||||||
|
. register_target(
|
||||||
|
WorldTargetDescriptor.new(
|
||||||
|
TARGET_ID, &"activity", capabilities, Vector3(3.0, 0.0, 2.0), "Town Hall"
|
||||||
|
),
|
||||||
|
provider,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _capability(action_id: StringName, priority: float) -> WorldTargetCapability:
|
||||||
|
return WorldTargetCapability.new(action_id, {"offer_priority": priority})
|
||||||
|
|
||||||
|
|
||||||
|
func _player_ref() -> WorldEntityRef:
|
||||||
|
return WorldEntityRef.create(SimulationIds.ENTITY_PLAYER, &"player")
|
||||||
|
|
||||||
|
|
||||||
|
func _person_ref() -> WorldEntityRef:
|
||||||
|
return WorldEntityRef.create(SimulationIds.ENTITY_PERSON, &"7")
|
||||||
|
|
||||||
|
|
||||||
|
func _action_ids(offers: Array[ActionOffer]) -> Array[StringName]:
|
||||||
|
var action_ids: Array[StringName] = []
|
||||||
|
for offer in offers:
|
||||||
|
action_ids.append(offer.get_action_id())
|
||||||
|
return action_ids
|
||||||
|
|
||||||
|
|
||||||
|
func _offer_for(offers: Array[ActionOffer], action_id: StringName) -> ActionOffer:
|
||||||
|
for offer in offers:
|
||||||
|
if offer.get_action_id() == action_id:
|
||||||
|
return offer
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _offer_snapshots(offers: Array[ActionOffer]) -> Array[Dictionary]:
|
||||||
|
var snapshots: Array[Dictionary] = []
|
||||||
|
for offer in offers:
|
||||||
|
(
|
||||||
|
snapshots
|
||||||
|
. append(
|
||||||
|
{
|
||||||
|
"offer_id": String(offer.get_offer_id()),
|
||||||
|
"action_id": String(offer.get_action_id()),
|
||||||
|
"display_name": offer.get_display_name(),
|
||||||
|
"enabled": offer.is_enabled(),
|
||||||
|
"rejection_reason": offer.get_rejection_reason(),
|
||||||
|
"parameters": offer.get_parameters(),
|
||||||
|
"priority": offer.get_priority(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return snapshots
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d4i4g5gkmnb87
|
||||||
Reference in New Issue
Block a user