class_name CombatPresentation extends Node const HOSTILE_SCENE := preload("res://world/combat/HostileCombatant.tscn") @export var simulation_manager: Node @export var hostiles_parent: Node3D @export var wolf_spawn_positions: PackedVector3Array = PackedVector3Array() var hostiles: Dictionary = {} var _initialized := false var _wolves_spawned := false func _ready() -> void: add_to_group("combat_presentation") if simulation_manager == null: push_error("CombatPresentation: simulation_manager missing") return simulation_manager.combatant_spawned.connect(_on_combatant_spawned) simulation_manager.combatant_died.connect(_on_combatant_died) simulation_manager.state_restored.connect(_on_state_restored) _initialized = true call_deferred("_spawn_wolves") _refresh_from_state() func _spawn_wolves() -> void: if _wolves_spawned or simulation_manager == null: return _wolves_spawned = true for position in wolf_spawn_positions: if simulation_manager.has_method("spawn_wolf"): simulation_manager.spawn_wolf(position) func _process(_delta: float) -> void: if not _initialized: return for combatant_id in _hostile_ids(): if not hostiles.has(combatant_id): _spawn_visual(combatant_id) func _hostile_ids() -> Array: var ids: Array[StringName] = [] var living: Array = simulation_manager.get_living_hostile_combatants() for combatant in living: ids.append(combatant.get_combatant_id()) return ids func _spawn_visual(combatant_id: StringName) -> void: if hostiles.has(combatant_id): return var scene := HOSTILE_SCENE.instantiate() as HostileCombatant hostiles_parent.add_child(scene) scene.bind(combatant_id, simulation_manager) hostiles[combatant_id] = scene func _on_combatant_spawned(combatant_id: StringName) -> void: _spawn_visual(combatant_id) func _on_combatant_died(combatant_id: StringName) -> void: var scene := hostiles.get(combatant_id) as HostileCombatant if scene != null: scene._play_death() hostiles.erase(combatant_id) func _on_state_restored() -> void: for combatant_id in hostiles.keys(): var scene := hostiles[combatant_id] as Node if is_instance_valid(scene): scene.queue_free() hostiles.clear() _refresh_from_state() func _refresh_from_state() -> void: if simulation_manager == null: return for combatant_id in _hostile_ids(): _spawn_visual(combatant_id) func find_hostile_in_arc( origin: Vector3, facing: Vector3, max_distance: float, angle_degrees: float ) -> CombatantStateRecord: var living: Array = simulation_manager.get_living_hostile_combatants() var best: CombatantStateRecord var best_score := INF var facing_flat := Vector3(facing.x, 0.0, facing.z).normalized() var half_cos := cos(deg_to_rad(angle_degrees) * 0.5) for combatant in living: var candidate := combatant as CombatantStateRecord var offset := candidate.get_position() - origin var horizontal := Vector2(offset.x, offset.z) if horizontal.length() > max_distance: continue var direction := Vector3(offset.x, 0.0, offset.z).normalized() if facing_flat.dot(direction) < half_cos: continue var distance := horizontal.length() if distance < best_score: best = candidate best_score = distance return best