class_name PlayerController extends Node @export var current_controlled : Boat: set(value): if current_controlled != null and current_controlled != value: current_controlled.desired_throttle = 0.0 current_controlled.steer_input = 0.0 current_controlled.fire_intents.clear() current_controlled.aim_point = null current_controlled = value if is_node_ready(): if current_controlled != null: desired_throttle = current_controlled.throttle _possess() @export var world_manager : WorldManager: set(value): world_manager = value if is_node_ready(): _possess() @export var camera_rig : CameraRig: set(value): camera_rig = value if is_node_ready(): _possess() @export var hud : Hud: set(value): hud = value if is_node_ready(): _possess() @export var throttle_ramp := 0.5 @export var aim_ray_length := 2000.0 @export_flags_3d_physics var aim_collision_mask := 0b1100111 # terrain, player, enemies, water, subsystems @export var aim_marker: Node3D var desired_throttle := 0.0 func _ready() -> void: if current_controlled != null: # Only happens on intial loading if boat already moves desired_throttle = current_controlled.throttle _possess() func _physics_process(_delta: float) -> void: if current_controlled == null: if aim_marker != null: aim_marker.visible = false return current_controlled.aim_point = _compute_aim_point() if aim_marker != null: var p = current_controlled.aim_point aim_marker.visible = p != null if p != null: aim_marker.global_position = p func _process(delta: float) -> void: if current_controlled == null: return if Input.is_action_pressed("throttle_increase"): desired_throttle = move_toward(desired_throttle, 1, throttle_ramp * delta) elif Input.is_action_pressed("throttle_decrease"): desired_throttle = move_toward(desired_throttle, -1, throttle_ramp * delta) elif Input.is_action_pressed("throttle_stop"): desired_throttle = 0.0 var steer_input := Input.get_axis("rudder_right", "rudder_left") current_controlled.desired_throttle = desired_throttle current_controlled.steer_input = steer_input current_controlled.fire_intents[0] = Input.is_action_pressed("fire") func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed(&"debug_possess_next"): _possess_next() func _possess() -> void: if camera_rig != null: camera_rig.target = current_controlled if world_manager != null: world_manager.focus = current_controlled if hud != null: hud.boat = current_controlled func _possess_next() -> void: var boats := get_tree().get_nodes_in_group(&"boats") if boats.is_empty(): current_controlled = null return var idx := boats.find(current_controlled) current_controlled = boats[(idx + 1) % boats.size()] func _compute_aim_point(): if camera_rig == null or camera_rig.cam == null: return null var cam := camera_rig.cam var mouse := get_viewport().get_mouse_position() var from := cam.project_ray_origin(mouse) var dir := cam.project_ray_normal(mouse) var query := PhysicsRayQueryParameters3D.create( from, from + dir * aim_ray_length, aim_collision_mask, current_controlled.aim_ray_exclusions() ) var hit := current_controlled.get_world_3d().direct_space_state.intersect_ray(query) if hit.is_empty(): return from + dir * aim_ray_length # above the horizon if hit.collider.is_in_group(&"water"): # Flat-plane hit → refine to the actual wave surface. var ocean := get_tree().get_first_node_in_group(&"ocean") as Ocean if ocean != null: var refined = Plane(Vector3.UP, ocean.get_wave_height(hit.position)).intersects_ray(from, dir) if refined != null: return refined return hit.position