class_name ActionTargetResolver extends RefCounted var last_resource_query_stats: Dictionary = {} func resolve( npc: SimNPC, origin: Vector3, simulation_manager: Node, active_world_adapter: Node ) -> Dictionary: var definition := SimulationDefinitions.get_action(npc.current_task) if definition == null: return {} match definition.target_type: SimulationIds.TARGET_RESOURCE: return _resolve_resource( npc, origin, definition, simulation_manager, active_world_adapter ) SimulationIds.TARGET_ACTIVITY: return _resolve_activity(npc, origin, simulation_manager, active_world_adapter) SimulationIds.TARGET_FREE: return { "target_id": "", "position": origin + simulation_manager.get_wander_offset(npc.id) } return {} func _resolve_resource( npc: SimNPC, origin: Vector3, definition: ActionDefinition, simulation_manager: Node, active_world_adapter: Node ) -> Dictionary: last_resource_query_stats = {} if ( active_world_adapter.has_method("get_resource_candidates_in_radius") and active_world_adapter.has_method("get_resource_query_profile") ): return _resolve_resource_spatial( npc, origin, definition, simulation_manager, active_world_adapter ) return _resolve_resource_linear( npc, origin, definition, simulation_manager, active_world_adapter ) func _resolve_resource_linear( npc: SimNPC, origin: Vector3, definition: ActionDefinition, simulation_manager: Node, active_world_adapter: Node ) -> Dictionary: var best: Dictionary = {} var best_score := INF var candidates: Array[Dictionary] = active_world_adapter.get_resource_candidates( definition.resource_action_id ) for candidate in candidates: var node_id := StringName(candidate["target_id"]) var state: ResourceStateRecord = simulation_manager.get_resource_state(node_id) if state == null or not state.can_npc_use() or not state.is_available_for(npc.id): continue var position: Vector3 = candidate["position"] var score := score_resource_candidate(npc, origin, position, state) if score < best_score: best_score = score best = candidate last_resource_query_stats = { "mode": "linear", "loaded_candidate_count": candidates.size(), "candidates_inspected": candidates.size(), "range_pass_count": 1, } return _reserve_resource_candidate(best, npc, simulation_manager) func _resolve_resource_spatial( npc: SimNPC, origin: Vector3, definition: ActionDefinition, simulation_manager: Node, active_world_adapter: Node ) -> Dictionary: var profile: Dictionary = active_world_adapter.get_resource_query_profile( definition.resource_action_id, origin ) var loaded_count := int(profile.get("candidate_count", 0)) if loaded_count == 0: last_resource_query_stats = { "mode": "spatial", "loaded_candidate_count": 0, "candidates_inspected": 0, "range_pass_count": 0, } return {} var best: Dictionary = {} var best_score := INF var best_registration_order := 9223372036854775807 var maximum_distance := maxf(float(profile["max_distance"]), 0.0) var radius := minf(maxf(float(profile["initial_radius"]), 0.1), maximum_distance) var previous_radius := -1.0 var candidates_inspected := 0 var range_pass_count := 0 while true: var candidates: Array[Dictionary] = active_world_adapter.get_resource_candidates_in_radius( definition.resource_action_id, origin, radius, previous_radius ) range_pass_count += 1 candidates_inspected += candidates.size() for candidate in candidates: var node_id := StringName(candidate["target_id"]) var state: ResourceStateRecord = simulation_manager.get_resource_state(node_id) if state == null or not state.can_npc_use() or not state.is_available_for(npc.id): continue var position: Vector3 = candidate["position"] var score := score_resource_candidate(npc, origin, position, state) var registration_order := int(candidate["registration_order"]) if ( score < best_score or (score == best_score and registration_order < best_registration_order) ): best_score = score best_registration_order = registration_order best = candidate if radius >= maximum_distance: break if ( not best.is_empty() and _minimum_resource_score_beyond(npc, radius, profile) > best_score ): break previous_radius = radius var next_radius := minf(maximum_distance, radius * 2.0) if next_radius <= radius: break radius = next_radius last_resource_query_stats = { "mode": "spatial", "loaded_candidate_count": loaded_count, "candidates_inspected": candidates_inspected, "range_pass_count": range_pass_count, "final_radius": radius, } return _reserve_resource_candidate(best, npc, simulation_manager) func _minimum_resource_score_beyond(npc: SimNPC, radius: float, profile: Dictionary) -> float: var comfort_overage := maxf(radius - float(profile["max_comfort_distance"]), 0.0) var risk_weight := 20.0 + maxf(100.0 - npc.energy, 0.0) * 0.2 return ( radius + comfort_overage * 2.5 + float(profile["min_safety_risk"]) * risk_weight - float(profile["max_discovery_priority"]) ) func _reserve_resource_candidate( candidate: Dictionary, npc: SimNPC, simulation_manager: Node ) -> Dictionary: if candidate.is_empty(): return {} var target_id := StringName(candidate["target_id"]) if not simulation_manager.reserve_resource(target_id, npc.id): return {} return candidate func _resolve_activity( npc: SimNPC, origin: Vector3, simulation_manager: Node, active_world_adapter: Node ) -> Dictionary: if not active_world_adapter.has_method("get_activity_candidates"): return active_world_adapter.get_activity_target(npc.current_task, origin) var best: Dictionary = {} var best_distance := INF var candidates: Array[Dictionary] = active_world_adapter.get_activity_candidates( npc.current_task ) for candidate in candidates: var target_id := StringName(candidate["target_id"]) var capacity := maxi(int(candidate.get("capacity", 1)), 1) if ( simulation_manager.has_method("get_activity_target_claim_count") and simulation_manager.get_activity_target_claim_count(target_id, npc.id) >= capacity ): continue var position: Vector3 = candidate["position"] var distance := origin.distance_squared_to(position) if distance < best_distance: best_distance = distance best = candidate if not best.is_empty(): return best if not candidates.is_empty(): return {} return active_world_adapter.get_activity_target(npc.current_task, origin) func score_resource_candidate( npc: SimNPC, origin: Vector3, position: Vector3, state: ResourceStateRecord ) -> float: var distance := origin.distance_to(position) var comfort_overage := maxf(distance - state.get_comfort_distance(), 0.0) var risk_weight := 20.0 + maxf(100.0 - npc.energy, 0.0) * 0.2 return ( distance + comfort_overage * 2.5 + state.get_safety_risk() * risk_weight - state.get_discovery_priority() )