class_name AIController extends Node @export var standoff_distance := 30.0 # throttle down inside this @export var fire_range := 120.0 # open fire inside this @export var full_rudder_angle_deg := 20.0 # bearing error at which rudder saturates var _boat: Boat var _target: Boat # node reference — shift-safe; never store a position func _ready() -> void: _boat = get_parent() as Boat func _physics_process(_delta: float) -> void: if _boat == null: return if _player_has_boat(): return # player possessed us — write nothing, resume when they leave if not is_instance_valid(_target): _target = _pick_target() if _target == null: _boat.desired_throttle = 0.0 _boat.steer_input = 0.0 _boat.aim_point = null # stow turrets, per the aim contract _boat.fire_intents[0] = false return # All geometry computed fresh this tick — origin-shift safe. var to_target: Vector3 = _target.global_position - _boat.global_position to_target.y = 0.0 var distance := to_target.length() var forward := -_boat.global_basis.z forward.y = 0.0 var bearing_error := forward.signed_angle_to(to_target, Vector3.UP) _boat.steer_input = clampf( bearing_error / deg_to_rad(full_rudder_angle_deg), -1.0, 1.0) _boat.desired_throttle = 1.0 if distance > standoff_distance else 0.2 _boat.aim_point = _target.global_position _boat.fire_intents[0] = distance < fire_range func _player_has_boat() -> bool: var pc := get_tree().get_first_node_in_group(&"player_controllers") as PlayerController return pc != null and pc.current_controlled == _boat func _pick_target() -> Boat: var best: Boat = null var best_d := INF for node in get_tree().get_nodes_in_group(&"boats"): var other := node as Boat if other == null or other == _boat: continue var d := _boat.global_position.distance_squared_to(other.global_position) if d < best_d: best_d = d best = other return best