542 lines
17 KiB
GDScript
542 lines
17 KiB
GDScript
class_name ActiveWorldAdapter
|
|
extends Node
|
|
|
|
const LoadedResourceSpatialIndexScript := preload(
|
|
"res://world/resource_nodes/LoadedResourceSpatialIndex.gd"
|
|
)
|
|
|
|
const DEFAULT_CONTEXT_ID := &"jajce_interior"
|
|
const DEFAULT_WORLD_ID := SimulationIds.REGIONAL_WORLD_BOSNIA
|
|
const DEFAULT_LOCATION_ID := SimulationIds.REGIONAL_LOCATION_JAJCE
|
|
const TARGET_KIND_RESOURCE := &"resource"
|
|
const TARGET_KIND_STORAGE := &"storage"
|
|
const TARGET_KIND_ACTIVITY := &"activity"
|
|
const TARGET_KIND_ANIMAL := &"animal"
|
|
const TARGET_GROUPS: Array[StringName] = [
|
|
&"resource_nodes", &"storage_nodes", &"activity_sites", &"animal_nodes"
|
|
]
|
|
|
|
@export var context_id: StringName = DEFAULT_CONTEXT_ID
|
|
@export var world_id: StringName = DEFAULT_WORLD_ID
|
|
@export var location_id: StringName = DEFAULT_LOCATION_ID
|
|
@export var pantry_storage: StorageNode
|
|
@export var woodpile_storage: StorageNode
|
|
@export_range(4.0, 128.0, 1.0) var resource_cell_size := 24.0
|
|
|
|
var _resource_index := LoadedResourceSpatialIndexScript.new()
|
|
var _target_registry: WorldTargetRegistry
|
|
var _target_handles_by_provider_id: Dictionary = {}
|
|
|
|
|
|
func _ready() -> void:
|
|
_resource_index.configure(resource_cell_size)
|
|
add_to_group("active_world_adapter")
|
|
_ensure_target_registry()
|
|
rebuild_target_registry()
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
_resource_index.clear()
|
|
_target_handles_by_provider_id.clear()
|
|
if _target_registry != null:
|
|
_target_registry.clear()
|
|
|
|
|
|
func configure_context_identity(
|
|
next_context_id: StringName, next_world_id: StringName, next_location_id: StringName
|
|
) -> bool:
|
|
if next_context_id.is_empty() or next_world_id.is_empty() or next_location_id.is_empty():
|
|
return false
|
|
context_id = next_context_id
|
|
world_id = next_world_id
|
|
location_id = next_location_id
|
|
_target_registry = WorldTargetRegistry.new(context_id, world_id, location_id)
|
|
_target_handles_by_provider_id.clear()
|
|
_resource_index.clear()
|
|
if is_node_ready():
|
|
rebuild_target_registry()
|
|
return true
|
|
|
|
|
|
func get_context_identity() -> Dictionary:
|
|
_ensure_target_registry()
|
|
return _target_registry.get_identity()
|
|
|
|
|
|
func get_target_registry() -> WorldTargetRegistry:
|
|
_ensure_target_registry()
|
|
return _target_registry
|
|
|
|
|
|
func get_target_handle(target_id: StringName) -> WorldTargetHandle:
|
|
_ensure_target_registry()
|
|
return _target_registry.get_handle(target_id)
|
|
|
|
|
|
func resolve_target_handle(handle: WorldTargetHandle) -> WorldTargetDescriptor:
|
|
_ensure_target_registry()
|
|
return _target_registry.resolve_handle(handle)
|
|
|
|
|
|
func get_target_provider(target_id: StringName, target_kind: StringName = &"") -> Object:
|
|
_ensure_target_registry()
|
|
var handle := _target_registry.get_handle(target_id)
|
|
if handle == null or (not target_kind.is_empty() and handle.get_target_kind() != target_kind):
|
|
return null
|
|
return _target_registry.get_provider(handle)
|
|
|
|
|
|
func owns_target_node(target: Node) -> bool:
|
|
return find_for_node(target) == self
|
|
|
|
|
|
static func find_for_node(target: Node) -> ActiveWorldAdapter:
|
|
if target == null:
|
|
return null
|
|
var current: Node = target
|
|
while current != null:
|
|
if current is ActiveWorldAdapter:
|
|
return current as ActiveWorldAdapter
|
|
for child in current.get_children():
|
|
if child is ActiveWorldAdapter:
|
|
return child as ActiveWorldAdapter
|
|
current = current.get_parent()
|
|
return null
|
|
|
|
|
|
static func nodes_share_context(first: Node, second: Node) -> bool:
|
|
if first == null or second == null:
|
|
return false
|
|
return find_for_node(first) == find_for_node(second)
|
|
|
|
|
|
func rebuild_target_registry() -> void:
|
|
_ensure_target_registry()
|
|
_target_registry.clear()
|
|
_target_handles_by_provider_id.clear()
|
|
_resource_index.clear()
|
|
if not is_inside_tree():
|
|
return
|
|
for group_id in TARGET_GROUPS:
|
|
for candidate in get_tree().get_nodes_in_group(group_id):
|
|
var provider := candidate as Node
|
|
if provider != null and owns_target_node(provider):
|
|
register_world_target(provider)
|
|
|
|
|
|
func rebuild_resource_index() -> void:
|
|
_resource_index.clear()
|
|
_ensure_target_registry()
|
|
for target_id in _target_registry.get_target_ids(TARGET_KIND_RESOURCE):
|
|
var node := get_target_provider(target_id, TARGET_KIND_RESOURCE) as ResourceNode
|
|
if node != null:
|
|
_resource_index.register_node(node, node.state)
|
|
|
|
|
|
func register_world_target(provider: Node) -> WorldTargetHandle:
|
|
if provider == null or not owns_target_node(provider):
|
|
return null
|
|
_bind_provider(provider)
|
|
var descriptor := _descriptor_for_provider(provider)
|
|
if descriptor == null:
|
|
return null
|
|
var handle := _register_descriptor(provider, descriptor)
|
|
if handle != null and provider is ResourceNode:
|
|
var resource := provider as ResourceNode
|
|
if not _resource_index.register_node(resource, resource.state):
|
|
unregister_world_target(provider)
|
|
return null
|
|
return handle
|
|
|
|
|
|
func unregister_world_target(provider: Node) -> bool:
|
|
if provider == null:
|
|
return false
|
|
_ensure_target_registry()
|
|
var provider_id := provider.get_instance_id()
|
|
var handle := _target_handles_by_provider_id.get(provider_id) as WorldTargetHandle
|
|
if handle == null or _target_registry.get_provider(handle) != provider:
|
|
_target_handles_by_provider_id.erase(provider_id)
|
|
return false
|
|
var target_id := handle.get_target_id()
|
|
var target_kind := handle.get_target_kind()
|
|
var removed := _target_registry.unregister_target(handle)
|
|
_target_handles_by_provider_id.erase(provider_id)
|
|
if removed and target_kind == TARGET_KIND_RESOURCE:
|
|
_resource_index.unregister_node(target_id)
|
|
return removed
|
|
|
|
|
|
func refresh_world_target_position(provider: Node) -> bool:
|
|
if provider == null or not owns_target_node(provider):
|
|
return false
|
|
_ensure_target_registry()
|
|
var handle := (
|
|
_target_handles_by_provider_id.get(provider.get_instance_id()) as WorldTargetHandle
|
|
)
|
|
if handle == null or not _target_registry.is_handle_valid(handle, true):
|
|
return register_world_target(provider) != null
|
|
var position := _interaction_position_for(provider)
|
|
if not _target_registry.update_target_position(handle, position):
|
|
return false
|
|
if provider is ResourceNode:
|
|
var resource := provider as ResourceNode
|
|
return _resource_index.register_node(resource, resource.state)
|
|
return true
|
|
|
|
|
|
func register_resource_node(node: ResourceNode, state: ResourceStateRecord = null) -> bool:
|
|
if node == null or not owns_target_node(node):
|
|
return false
|
|
_bind_provider(node)
|
|
var descriptor := _resource_descriptor(node, state)
|
|
var handle := _register_descriptor(node, descriptor)
|
|
return handle != null and _resource_index.register_node(node, state)
|
|
|
|
|
|
func unregister_resource_node(node_id: StringName, provider: ResourceNode = null) -> void:
|
|
if provider != null:
|
|
unregister_world_target(provider)
|
|
return
|
|
_ensure_target_registry()
|
|
var handle := _target_registry.get_handle(node_id)
|
|
if handle == null or handle.get_target_kind() != TARGET_KIND_RESOURCE:
|
|
return
|
|
var registered_provider := _target_registry.get_provider(handle)
|
|
if registered_provider != null:
|
|
_target_handles_by_provider_id.erase(registered_provider.get_instance_id())
|
|
if _target_registry.unregister_target(handle):
|
|
_resource_index.unregister_node(node_id)
|
|
|
|
|
|
func get_resource_candidates(action_id: StringName) -> Array[Dictionary]:
|
|
return _resource_index.get_all_candidates(action_id)
|
|
|
|
|
|
func get_resource_candidates_in_radius(
|
|
action_id: StringName,
|
|
origin: Vector3,
|
|
max_distance: float,
|
|
min_distance_exclusive: float = -1.0
|
|
) -> Array[Dictionary]:
|
|
return _resource_index.get_candidates_in_radius(
|
|
action_id, origin, max_distance, min_distance_exclusive
|
|
)
|
|
|
|
|
|
func get_resource_query_profile(action_id: StringName, origin: Vector3) -> Dictionary:
|
|
return _resource_index.get_query_profile(action_id, origin)
|
|
|
|
|
|
func get_resource_nodes_in_radius(origin: Vector3, max_distance: float) -> Array[ResourceNode]:
|
|
return _resource_index.get_nodes_in_radius(origin, max_distance)
|
|
|
|
|
|
func get_resource_index_stats() -> Dictionary:
|
|
return _resource_index.get_stats()
|
|
|
|
|
|
func get_animal_candidates(action_id: StringName) -> Array[Dictionary]:
|
|
_ensure_target_registry()
|
|
var candidates: Array[Dictionary] = []
|
|
for target_id in _target_registry.get_target_ids(TARGET_KIND_ANIMAL, action_id):
|
|
var descriptor := _target_registry.get_descriptor(target_id)
|
|
if descriptor == null:
|
|
continue
|
|
(
|
|
candidates
|
|
. append(
|
|
{
|
|
"target_id": String(target_id),
|
|
"position": descriptor.get_local_position(),
|
|
"display_name": descriptor.get_display_name(),
|
|
}
|
|
)
|
|
)
|
|
return candidates
|
|
|
|
|
|
func get_animal_routine_sites(species_id: StringName, animal_id: StringName) -> Array[Dictionary]:
|
|
var candidates: Array[Dictionary] = []
|
|
for site in AnimalRoutineSite.get_all():
|
|
if not owns_target_node(site) or not site.supports_animal(animal_id, species_id):
|
|
continue
|
|
(
|
|
candidates
|
|
. append(
|
|
{
|
|
"site_id": String(site.site_id),
|
|
"position": site.get_routine_position(),
|
|
"display_name": site.display_name,
|
|
}
|
|
)
|
|
)
|
|
candidates.sort_custom(
|
|
func(first: Dictionary, second: Dictionary) -> bool:
|
|
return String(first["site_id"]) < String(second["site_id"])
|
|
)
|
|
return candidates
|
|
|
|
|
|
func get_animal_nodes_in_radius(origin: Vector3, max_distance: float) -> Array[AnimalNode]:
|
|
_ensure_target_registry()
|
|
var nodes: Array[AnimalNode] = []
|
|
var maximum_distance_squared := maxf(max_distance, 0.0) * maxf(max_distance, 0.0)
|
|
for target_id in _target_registry.get_target_ids(TARGET_KIND_ANIMAL):
|
|
var descriptor := _target_registry.get_descriptor(target_id)
|
|
if (
|
|
descriptor == null
|
|
or (
|
|
origin.distance_squared_to(descriptor.get_local_position())
|
|
> maximum_distance_squared
|
|
)
|
|
):
|
|
continue
|
|
var node := get_target_provider(target_id, TARGET_KIND_ANIMAL) as AnimalNode
|
|
if node != null:
|
|
nodes.append(node)
|
|
nodes.sort_custom(
|
|
func(first: AnimalNode, second: AnimalNode) -> bool:
|
|
var first_distance := origin.distance_squared_to(first.get_interaction_position())
|
|
var second_distance := origin.distance_squared_to(second.get_interaction_position())
|
|
if not is_equal_approx(first_distance, second_distance):
|
|
return first_distance < second_distance
|
|
return String(first.animal_id) < String(second.animal_id)
|
|
)
|
|
return nodes
|
|
|
|
|
|
func get_activity_target(action_id: StringName, origin: Vector3 = Vector3.ZERO) -> Dictionary:
|
|
var best_candidate: Dictionary = {}
|
|
var best_distance := INF
|
|
for candidate in get_activity_candidates(action_id):
|
|
var position: Vector3 = candidate["position"]
|
|
var distance := origin.distance_squared_to(position)
|
|
if distance < best_distance:
|
|
best_distance = distance
|
|
best_candidate = candidate
|
|
if not best_candidate.is_empty():
|
|
return best_candidate
|
|
match action_id:
|
|
SimulationIds.ACTION_EAT:
|
|
return get_storage_target(SimulationIds.STORAGE_VILLAGE_PANTRY)
|
|
SimulationIds.ACTION_DEPOSIT_FOOD, SimulationIds.ACTION_WITHDRAW_FOOD:
|
|
return get_storage_target(SimulationIds.STORAGE_VILLAGE_PANTRY)
|
|
SimulationIds.ACTION_DEPOSIT_WOOD:
|
|
return get_storage_target(SimulationIds.STORAGE_VILLAGE_WOODPILE)
|
|
return {}
|
|
|
|
|
|
func get_activity_candidates(action_id: StringName) -> Array[Dictionary]:
|
|
_ensure_target_registry()
|
|
var candidates: Array[Dictionary] = []
|
|
for target_id in _target_registry.get_target_ids(TARGET_KIND_ACTIVITY, action_id):
|
|
var descriptor := _target_registry.get_descriptor(target_id)
|
|
if descriptor == null:
|
|
continue
|
|
(
|
|
candidates
|
|
. append(
|
|
{
|
|
"target_id": String(target_id),
|
|
"position": descriptor.get_local_position(),
|
|
"capacity": int(descriptor.get_metadata_value(&"capacity", 1)),
|
|
}
|
|
)
|
|
)
|
|
return candidates
|
|
|
|
|
|
func get_storage_target(storage_id: StringName) -> Dictionary:
|
|
_ensure_target_registry()
|
|
var descriptor := _target_registry.get_descriptor(storage_id)
|
|
if descriptor == null or descriptor.get_target_kind() != TARGET_KIND_STORAGE:
|
|
return {}
|
|
return {
|
|
"target_id": String(storage_id),
|
|
"position": descriptor.get_local_position(),
|
|
}
|
|
|
|
|
|
func _ensure_target_registry() -> void:
|
|
if _target_registry == null:
|
|
_target_registry = WorldTargetRegistry.new(context_id, world_id, location_id)
|
|
|
|
|
|
func _register_descriptor(provider: Node, descriptor: WorldTargetDescriptor) -> WorldTargetHandle:
|
|
_ensure_target_registry()
|
|
if provider == null or descriptor == null or not descriptor.is_valid():
|
|
return null
|
|
var provider_id := provider.get_instance_id()
|
|
var handle := _target_handles_by_provider_id.get(provider_id) as WorldTargetHandle
|
|
if handle != null and _target_registry.is_handle_valid(handle, true):
|
|
if (
|
|
handle.get_target_id() == descriptor.get_target_id()
|
|
and handle.get_target_kind() == descriptor.get_target_kind()
|
|
):
|
|
return handle if _target_registry.update_target(handle, descriptor) else null
|
|
unregister_world_target(provider)
|
|
|
|
var existing_handle := _target_registry.get_handle(descriptor.get_target_id())
|
|
if existing_handle != null:
|
|
if _target_registry.get_provider(existing_handle) != provider:
|
|
_target_registry.register_target(descriptor, provider)
|
|
return null
|
|
handle = existing_handle
|
|
if not _target_registry.update_target(handle, descriptor):
|
|
return null
|
|
else:
|
|
handle = _target_registry.register_target(descriptor, provider)
|
|
if handle != null:
|
|
_target_handles_by_provider_id[provider_id] = handle
|
|
return handle
|
|
|
|
|
|
func _bind_provider(provider: Node) -> void:
|
|
if provider.has_method("_bind_active_world_adapter"):
|
|
provider.call("_bind_active_world_adapter", self)
|
|
|
|
|
|
func _descriptor_for_provider(provider: Node) -> WorldTargetDescriptor:
|
|
if provider is ResourceNode:
|
|
var resource := provider as ResourceNode
|
|
return _resource_descriptor(resource, resource.state)
|
|
if provider is StorageNode:
|
|
return _storage_descriptor(provider as StorageNode)
|
|
if provider is ActivitySite:
|
|
return _activity_descriptor(provider as ActivitySite)
|
|
if provider is AnimalNode:
|
|
return _animal_descriptor(provider as AnimalNode)
|
|
return null
|
|
|
|
|
|
func _resource_descriptor(
|
|
node: ResourceNode, state: ResourceStateRecord = null
|
|
) -> WorldTargetDescriptor:
|
|
if node == null or node.node_id.is_empty():
|
|
return null
|
|
var action_id := state.get_action_id() if state != null else node.action_id
|
|
var resource_id := state.get_resource_id() if state != null else node.resource_id
|
|
var capability := (
|
|
WorldTargetCapability
|
|
. new(
|
|
action_id,
|
|
{
|
|
"resource_id": String(resource_id),
|
|
"can_npcs_use": node.can_npcs_use,
|
|
"can_player_use": node.can_player_use,
|
|
}
|
|
)
|
|
)
|
|
return (
|
|
WorldTargetDescriptor
|
|
. new(
|
|
node.node_id,
|
|
TARGET_KIND_RESOURCE,
|
|
[capability],
|
|
_interaction_position_for(node),
|
|
String(node.name),
|
|
{
|
|
"resource_id": String(resource_id),
|
|
"safety_risk": state.get_safety_risk() if state != null else node.safety_risk,
|
|
"comfort_distance":
|
|
state.get_comfort_distance() if state != null else node.comfort_distance,
|
|
"discovery_priority":
|
|
state.get_discovery_priority() if state != null else node.discovery_priority,
|
|
}
|
|
)
|
|
)
|
|
|
|
|
|
func _storage_descriptor(node: StorageNode) -> WorldTargetDescriptor:
|
|
if node == null or node.storage_id.is_empty():
|
|
return null
|
|
var capability_ids: Array[StringName] = []
|
|
if node.accepts_item(SimulationIds.RESOURCE_FOOD):
|
|
(
|
|
capability_ids
|
|
. append_array(
|
|
[
|
|
SimulationIds.ACTION_EAT,
|
|
SimulationIds.ACTION_DEPOSIT_FOOD,
|
|
SimulationIds.ACTION_WITHDRAW_FOOD,
|
|
]
|
|
)
|
|
)
|
|
if node.accepts_item(SimulationIds.RESOURCE_WOOD):
|
|
capability_ids.append(SimulationIds.ACTION_DEPOSIT_WOOD)
|
|
var capabilities: Array[WorldTargetCapability] = []
|
|
for capability_id in capability_ids:
|
|
capabilities.append(WorldTargetCapability.new(capability_id))
|
|
var accepted_item_ids: Array[String] = []
|
|
for item_id in node.accepted_items:
|
|
accepted_item_ids.append(String(item_id))
|
|
return WorldTargetDescriptor.new(
|
|
node.storage_id,
|
|
TARGET_KIND_STORAGE,
|
|
capabilities,
|
|
_interaction_position_for(node),
|
|
node.display_name,
|
|
{"accepted_item_ids": accepted_item_ids}
|
|
)
|
|
|
|
|
|
func _activity_descriptor(node: ActivitySite) -> WorldTargetDescriptor:
|
|
if node == null or node.site_id.is_empty() or not node.is_interaction_range_valid():
|
|
return null
|
|
var capability_ids: Array[StringName] = [node.action_id]
|
|
if node.action_id == SimulationIds.ACTION_PATROL:
|
|
capability_ids.append(SimulationIds.ACTION_DEFEND)
|
|
var capabilities: Array[WorldTargetCapability] = []
|
|
for capability_id in capability_ids:
|
|
capabilities.append(
|
|
WorldTargetCapability.new(capability_id, {"interaction_range": node.interaction_range})
|
|
)
|
|
return WorldTargetDescriptor.new(
|
|
node.site_id,
|
|
TARGET_KIND_ACTIVITY,
|
|
capabilities,
|
|
_interaction_position_for(node),
|
|
node.display_name,
|
|
{"capacity": node.capacity}
|
|
)
|
|
|
|
|
|
func _animal_descriptor(node: AnimalNode) -> WorldTargetDescriptor:
|
|
if node == null or node.animal_id.is_empty():
|
|
return null
|
|
var capabilities: Array[WorldTargetCapability] = []
|
|
for capability_id in node.supported_action_ids:
|
|
capabilities.append(WorldTargetCapability.new(capability_id))
|
|
return (
|
|
WorldTargetDescriptor
|
|
. new(
|
|
node.animal_id,
|
|
TARGET_KIND_ANIMAL,
|
|
capabilities,
|
|
_interaction_position_for(node),
|
|
node.display_name,
|
|
{
|
|
"animal_definition_id": String(node.animal_definition_id),
|
|
"species_id": String(node.species_id),
|
|
}
|
|
)
|
|
)
|
|
|
|
|
|
func _interaction_position_for(provider: Node) -> Vector3:
|
|
if provider is ResourceNode:
|
|
var resource := provider as ResourceNode
|
|
return (
|
|
resource.interaction_point.global_position
|
|
if resource.interaction_point != null
|
|
else resource.global_position
|
|
)
|
|
if provider is StorageNode:
|
|
return (provider as StorageNode).get_interaction_position()
|
|
if provider is ActivitySite:
|
|
return (provider as ActivitySite).get_interaction_position()
|
|
if provider is AnimalNode:
|
|
return (provider as AnimalNode).get_interaction_position()
|
|
return Vector3.ZERO
|