Files
gamedev-the-steward/simulation/actions/ActionTargetResolver.gd
T
2026-07-26 22:26:00 +02:00

245 lines
7.8 KiB
GDScript

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_ANIMAL:
return _resolve_animal(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 _resolve_animal(
npc: SimNPC, origin: Vector3, simulation_manager: Node, active_world_adapter: Node
) -> Dictionary:
if not active_world_adapter.has_method("get_animal_candidates"):
return {}
var best: Dictionary = {}
var best_distance := INF
var candidates: Array[Dictionary] = active_world_adapter.get_animal_candidates(npc.current_task)
for candidate in candidates:
var animal_id := StringName(candidate["target_id"])
var state: AnimalStateRecord = simulation_manager.animal_care.get_state(animal_id)
if state == null or not state.can_npc_feed() or not state.is_available_for(npc.id):
continue
var position: Vector3 = candidate["position"]
var distance := origin.distance_squared_to(position)
if distance < best_distance:
best_distance = distance
best = candidate
if best.is_empty():
return {}
var target_id := StringName(best["target_id"])
if not simulation_manager.animal_care.reserve(target_id, npc.id):
return {}
return best
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()
)