extends CharacterBody3D ## Kestrel EC-01 capsule — flight, cameras, enter/exit. ## Hull clips + plumes live on child "Kestrel" via kestrel_driver.gd. @onready var interaction_area: Area3D = $InteractionArea @onready var camera_pivot: Node3D = get_node_or_null("CameraPivot") @onready var capsule_camera: Camera3D = get_node_or_null("CameraPivot/CapsuleCamera") @onready var third_person_camera: Camera3D = $ThirdPersonCamera @onready var interact_prompt: Label = get_node_or_null("/root/Node3D/HUD/InteractPrompt") @onready var pause_menu: Control = get_node_or_null("/root/Node3D/HUD/PauseMenu") @onready var body_collision: CollisionShape3D = get_node_or_null("CollisionShape3D") @onready var flight_label: Label = get_node_or_null("/root/Node3D/HUD/FlightLabel") @onready var kestrel: Node3D = get_node_or_null("Kestrel") var player_inside: bool = false var player_ref: CharacterBody3D = null var using_third_person: bool = false var mass: float = 6500.0 var thrust_force: float = 180000.0 var rotation_speed: float = 1.8 var surface_friction: float = 6.0 const MOON_RADIUS: float = 173700.0 const MOON_G: float = 1.62 const MOON_SOI: float = 400000.0 const EARTH_RADIUS: float = 637100.0 const EARTH_G: float = 9.81 const EARTH_SOI: float = 20000000.0 const CAM_FAR: float = 1000000.0 const NEAR_SURFACE_MARGIN: float = 300.0 var _moon_colliders: Array[CollisionObject3D] = [] var _crust_exception: bool = false var free_look: bool = false var look_sensitivity: float = 0.003 var orbit_yaw: float = 0.0 var orbit_pitch: float = 0.4 var orbit_distance: float = 14.0 var orbit_pitch_min: float = -1.2 var orbit_pitch_max: float = 1.2 var cockpit_yaw: float = 0.0 var cockpit_pitch: float = 0.0 var cockpit_look_limit_yaw: float = 1.45 var cockpit_look_limit_up: float = 1.0 var cockpit_look_limit_down: float = 0.55 var head_local: Transform3D = Transform3D.IDENTITY var telem_body: String = "—" var telem_alt: float = 0.0 var telem_speed: float = 0.0 var telem_vspeed: float = 0.0 var telem_hspeed: float = 0.0 var telem_vcirc: float = 0.0 var telem_dh: float = 0.0 var telem_earth_rng: float = 0.0 var telem_thrust: bool = false var telem_has_alt: bool = false var _gear_down: bool = true var _hatch_crew_open: bool = false var _hatch_dock_open: bool = false var _ladder_out: bool = false var _cabin_dim: bool = false var _thrust_input: float = 0.0 func _ready() -> void: GameplayInput.command_requested.connect(_handle_vehicle_command) GameplayInput.transient_input_cleared.connect(_clear_vehicle_input) if interaction_area: interaction_area.monitoring = true interaction_area.monitorable = true interaction_area.body_entered.connect(_on_body_entered) interaction_area.body_exited.connect(_on_body_exited) if camera_pivot: camera_pivot.top_level = false head_local = camera_pivot.transform if capsule_camera: capsule_camera.near = 0.05 capsule_camera.far = CAM_FAR capsule_camera.current = false if third_person_camera: third_person_camera.near = 0.1 third_person_camera.far = CAM_FAR third_person_camera.current = false if flight_label: flight_label.visible = false _bind_kestrel() _collect_moon_colliders() call_deferred("_update_crust_exception") func _bind_kestrel() -> void: if kestrel == null: kestrel = get_node_or_null("Kestrel") print("Kestrel hull=", kestrel, " driver=", kestrel.get_script() if kestrel else null) func _hull() -> Node: return kestrel if kestrel else null func _set_hull(prop: String, value: Variant) -> void: var h := _hull() if h and prop in h: h.set(prop, value) func _moon() -> Node3D: return get_node_or_null("/root/Node3D/SolarSystem/Moon") as Node3D func _earth() -> Node3D: return get_node_or_null("/root/Node3D/SolarSystem/Earth") as Node3D func _habitat() -> Node3D: return get_node_or_null("/root/Node3D/SolarSystem/Moon/LovellAnchor/Station/Habitat") as Node3D func _collect_moon_colliders() -> void: _moon_colliders.clear() var moon := _moon() if moon == null: return for n in moon.find_children("*", "PhysicsBody3D", true, false): if n == self: continue if str(n.name).begins_with("Shell"): continue if str(n.name).begins_with("Moon") and n is CollisionObject3D: _moon_colliders.append(n as CollisionObject3D) func _inside_young() -> bool: var habitat := _habitat() if habitat == null: return false var lp: Vector3 = habitat.to_local(global_position) var radial: float = Vector2(lp.x, lp.z).length() return radial < 2000.0 and lp.y < 80.0 and lp.y > -120.0 func _update_crust_exception() -> void: var want: bool = _inside_young() if want == _crust_exception: return _crust_exception = want for body in _moon_colliders: if not is_instance_valid(body): continue if want: add_collision_exception_with(body) else: remove_collision_exception_with(body) func _active_gravity() -> Dictionary: var moon := _moon() if moon: var d_m: float = global_position.distance_to(moon.global_position) if d_m >= 1.0 and d_m <= MOON_SOI: var to_c: Vector3 = moon.global_position - global_position return { "name": "Luna", "center": moon.global_position, "radius": MOON_RADIUS, "g": MOON_G, "accel": to_c.normalized() * MOON_G, "dist": d_m } var earth := _earth() if earth: var d_e: float = global_position.distance_to(earth.global_position) if d_e >= 1.0 and d_e <= EARTH_SOI: var to_c2: Vector3 = earth.global_position - global_position return { "name": "Earth", "center": earth.global_position, "radius": EARTH_RADIUS, "g": EARTH_G, "accel": to_c2.normalized() * EARTH_G, "dist": d_e } return {"name": "—", "center": Vector3.ZERO, "radius": 0.0, "g": 0.0, "accel": Vector3.ZERO, "dist": 0.0} func _gravity_accel() -> Vector3: return _active_gravity()["accel"] func _update_telemetry() -> void: telem_speed = velocity.length() telem_thrust = _thrust_input > 0.0 var earth := _earth() if earth: telem_earth_rng = maxf(0.0, global_position.distance_to(earth.global_position) - EARTH_RADIUS) else: telem_earth_rng = 0.0 var body: Dictionary = _active_gravity() telem_body = str(body["name"]) if telem_body == "—": telem_has_alt = false telem_alt = 0.0 telem_vspeed = 0.0 telem_hspeed = telem_speed telem_vcirc = 0.0 telem_dh = 0.0 return telem_has_alt = true var r: float = float(body["dist"]) var rad: float = float(body["radius"]) var g: float = float(body["g"]) telem_alt = r - rad var radial: Vector3 = (global_position - body["center"]).normalized() telem_vspeed = velocity.dot(radial) telem_hspeed = (velocity - radial * telem_vspeed).length() telem_vcirc = sqrt(g * rad * rad / r) if r > 1.0 else 0.0 telem_dh = telem_hspeed - telem_vcirc func _update_flight_hud() -> void: if flight_label == null or not player_inside: return flight_label.visible = true var mode := "3P" if using_third_person else "1P" var thr := "THRUST" if telem_thrust else "IDLE" var vs_sign := "+" if telem_vspeed >= 0.0 else "" var alt_line: String var vcirc_line: String var dh_line: String if telem_has_alt: alt_line = "ALT %8.0f m\n" % telem_alt vcirc_line = "VCIRC %8.1f m/s\n" % telem_vcirc var dh_sign := "+" if telem_dh >= 0.0 else "" dh_line = "ΔH %s%7.1f m/s\n" % [dh_sign, telem_dh] else: alt_line = "ALT —\n" vcirc_line = "VCIRC —\n" dh_line = "ΔH —\n" flight_label.text = ( "[%s] %s | BODY %s\n" % [mode, thr, telem_body] + alt_line + "SPD %8.1f m/s\n" % telem_speed + "VS %s%7.1f m/s\n" % [vs_sign, telem_vspeed] + "H %8.1f m/s\n" % telem_hspeed + vcirc_line + dh_line + "EARTH %8.0f m" % telem_earth_rng ) func _clear_flight_hud() -> void: if flight_label: flight_label.visible = false flight_label.text = "" func _on_body_entered(body: Node3D) -> void: if body.is_in_group("player"): player_ref = body if interact_prompt and not player_inside: interact_prompt.text = "[%s] Enter" % GameplayInput.binding_label(&"interact") interact_prompt.visible = true func _on_body_exited(body: Node3D) -> void: if body.is_in_group("player") and not player_inside: player_ref = null if interact_prompt: interact_prompt.visible = false ## Handles semantic vehicle commands; proximity permits entry only on foot. func _handle_vehicle_command(action: StringName, command_context: int) -> void: if command_context != GameplayInput.context: return if action == &"interact": if player_inside: exit_capsule() elif player_ref and interaction_area.overlaps_body(player_ref) and GameplayInput.context == GameplayInput.Context.ON_FOOT: enter_capsule() return if not player_inside or GameplayInput.context != GameplayInput.Context.VEHICLE: return match action: &"toggle_view": toggle_camera() &"vehicle_toggle_gear": _gear_down = not _gear_down _set_hull("gear_deploy", 1.0 if _gear_down else 0.0) &"vehicle_toggle_crew_hatch": _hatch_crew_open = not _hatch_crew_open _set_hull("hatch_crew_open", 1.0 if _hatch_crew_open else 0.0) &"vehicle_toggle_dock_hatch": _hatch_dock_open = not _hatch_dock_open _set_hull("hatch_dock_open", 1.0 if _hatch_dock_open else 0.0) &"capsule_toggle_ladder": _ladder_out = not _ladder_out _set_hull("hatch_ladder_deploy", 1.0 if _ladder_out else 0.0) &"vehicle_activate_instruments": _set_hull("instruments_demo", 1.0) &"vehicle_toggle_cabin_lights": _cabin_dim = not _cabin_dim _set_hull("cabin_dim", _cabin_dim) ## Clears held cockpit look and visual thrust on menus, handoffs, or focus loss. func _clear_vehicle_input() -> void: free_look = false cockpit_yaw = 0.0 cockpit_pitch = 0.0 _thrust_input = 0.0 if player_inside: _set_hull("abort_burn", false) _set_hull("att_pitch", 0.0) _set_hull("att_yaw", 0.0) _set_hull("att_roll", 0.0) ## Updates active vehicle input/cameras while retaining the original flight timing. func _process(delta: float) -> void: if not player_inside: if player_ref and interact_prompt: interact_prompt.text = "[%s] Enter" % GameplayInput.binding_label(&"interact") _update_crust_exception() return _update_vehicle_look(delta) handle_flight(delta) _update_telemetry() _update_flight_hud() if using_third_person: update_third_person_camera() if third_person_camera and not third_person_camera.current: third_person_camera.make_current() else: update_cockpit_camera() if capsule_camera and not capsule_camera.current: capsule_camera.make_current() ## Updates cockpit/orbit look without letting menu input reach the vehicle camera. func _update_vehicle_look(delta: float) -> void: var cursor_released := GameplayInput.is_held(&"vehicle_release_cursor") if GameplayInput.gameplay_enabled(): Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if cursor_released else Input.MOUSE_MODE_CAPTURED var previous_freelook := free_look free_look = GameplayInput.is_held(&"vehicle_freelook") if previous_freelook and not free_look and not using_third_person: cockpit_yaw = 0.0 cockpit_pitch = 0.0 var mouse_displacement := GameplayInput.consume_mouse_displacement() if cursor_released: return var mouse_turn := mouse_displacement * look_sensitivity var controller_turn := GameplayInput.read_controller_look(delta) if using_third_person: _apply_vehicle_look(mouse_turn + controller_turn) else: _apply_vehicle_look((mouse_turn if free_look else Vector2.ZERO) + controller_turn) ## Applies angular displacement in radians while preserving existing camera limits. func _apply_vehicle_look(look_delta: Vector2) -> void: if using_third_person: orbit_yaw -= look_delta.x orbit_pitch += look_delta.y orbit_pitch = clampf(orbit_pitch, orbit_pitch_min, orbit_pitch_max) else: cockpit_yaw -= look_delta.x cockpit_pitch -= look_delta.y cockpit_yaw = clampf(cockpit_yaw, -cockpit_look_limit_yaw, cockpit_look_limit_yaw) cockpit_pitch = clampf(cockpit_pitch, -cockpit_look_limit_down, cockpit_look_limit_up) func enter_capsule() -> void: if player_inside or player_ref == null: return player_inside = true if interact_prompt: interact_prompt.visible = false player_ref.reparent(self) player_ref.position = Vector3(0, 0.5, 0) player_ref.rotation = Vector3.ZERO player_ref.set_physics_process(false) player_ref.set_process(true) player_ref.set_process_input(false) player_ref.set_process_unhandled_input(false) if player_ref.has_method("set_in_vehicle"): player_ref.set_in_vehicle(true) var player_col = player_ref.get_node_or_null("CollisionShape3D") if player_col: player_col.disabled = true var player_cam = player_ref.get_node_or_null("CameraPivot/Camera3D") if player_cam: player_cam.current = false player_ref.visible = false cockpit_yaw = 0.0 cockpit_pitch = 0.0 orbit_yaw = 0.0 orbit_pitch = 0.4 using_third_person = false free_look = false if camera_pivot: camera_pivot.top_level = false camera_pivot.transform = head_local if third_person_camera: third_person_camera.current = false if capsule_camera: capsule_camera.near = 0.05 capsule_camera.far = CAM_FAR capsule_camera.current = true capsule_camera.make_current() velocity = Vector3.ZERO FloatingOrigin.set_focus(self) Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if flight_label: flight_label.visible = true _update_crust_exception() GameplayInput.set_context(GameplayInput.Context.VEHICLE) func exit_capsule() -> void: if not player_inside or player_ref == null: return var exiting_player: CharacterBody3D = player_ref player_inside = false velocity = Vector3.ZERO free_look = false using_third_person = false _set_hull("abort_burn", false) _set_hull("rcs_direction", 0) _clear_flight_hud() if body_collision: body_collision.disabled = false if is_instance_valid(camera_pivot): camera_pivot.top_level = false camera_pivot.transform = head_local if is_instance_valid(capsule_camera): capsule_camera.current = false if is_instance_valid(third_person_camera): third_person_camera.current = false var station := get_node_or_null("/root/Node3D/SolarSystem/Moon/LovellAnchor/Station") var dest: Node = station if station else get_tree().current_scene exiting_player.reparent(dest) exiting_player.global_position = global_position + global_transform.basis.y * 4.0 var player_cam_pivot = exiting_player.get_node_or_null("CameraPivot") if player_cam_pivot: player_cam_pivot.rotation = Vector3.ZERO exiting_player.visible = true exiting_player.set_physics_process(true) exiting_player.set_process(true) exiting_player.set_process_input(true) exiting_player.set_process_unhandled_input(true) exiting_player.process_mode = Node.PROCESS_MODE_INHERIT var player_col = exiting_player.get_node_or_null("CollisionShape3D") if player_col: player_col.disabled = false if exiting_player.has_method("set_in_vehicle"): exiting_player.set_in_vehicle(false) var player_cam = exiting_player.get_node_or_null("CameraPivot/Camera3D") if player_cam: player_cam.current = true player_cam.make_current() if exiting_player.has_method("restore_from_vehicle"): exiting_player.restore_from_vehicle() FloatingOrigin.set_focus(exiting_player) Input.mouse_mode = Input.MOUSE_MODE_CAPTURED player_ref = null GameplayInput.set_context(GameplayInput.Context.ON_FOOT) func toggle_camera() -> void: using_third_person = not using_third_person if using_third_person: if capsule_camera: capsule_camera.current = false if third_person_camera: third_person_camera.near = 0.1 third_person_camera.far = CAM_FAR third_person_camera.make_current() else: if third_person_camera: third_person_camera.current = false if camera_pivot: camera_pivot.top_level = false camera_pivot.transform = head_local if capsule_camera: capsule_camera.make_current() func update_cockpit_camera() -> void: if not camera_pivot: return camera_pivot.top_level = false var b: Basis = head_local.basis b = b.rotated(b.x, cockpit_pitch) b = b.rotated(b.y, cockpit_yaw) b = b.orthonormalized() if abs(b.determinant()) < 0.5: camera_pivot.transform = head_local return camera_pivot.transform = Transform3D(b, head_local.origin) func update_third_person_camera() -> void: if not third_person_camera: return var cy := cos(orbit_yaw) var sy := sin(orbit_yaw) var cp := cos(orbit_pitch) var sp := sin(orbit_pitch) var offset := Vector3(sy * cp, sp, cy * cp) * orbit_distance var local_off := Vector3(offset.x, -offset.z, offset.y) var cam_pos := global_position + global_transform.basis * local_off third_person_camera.global_position = cam_pos var target := global_position + global_transform.basis.y * 1.5 var forward := target - cam_pos if forward.length_squared() < 0.0001: return forward = forward.normalized() var ref_up := global_transform.basis.z if abs(forward.dot(ref_up)) > 0.92: ref_up = global_transform.basis.x var right := ref_up.cross(forward) if right.length_squared() < 0.0001: return right = right.normalized() var up := forward.cross(right).normalized() var cam_basis := Basis(-right, up, -forward).orthonormalized() if abs(cam_basis.determinant()) < 0.5: return third_person_camera.global_basis = cam_basis ## Reads signed flight intent while preserving the existing flight equations/timing. func handle_flight(delta: float) -> void: _update_crust_exception() var input_pitch := GameplayInput.read_axis(&"vehicle_pitch_up", &"vehicle_pitch_down") var input_roll := GameplayInput.read_axis(&"vehicle_roll_left", &"vehicle_roll_right") var input_yaw := GameplayInput.read_axis(&"vehicle_yaw_right", &"vehicle_yaw_left") rotate_object_local(Vector3.RIGHT, input_pitch * rotation_speed * delta) rotate_object_local(Vector3.FORWARD, -input_yaw * rotation_speed * delta) rotate_object_local(Vector3.UP, input_roll * rotation_speed * delta) _thrust_input = GameplayInput.read_strength(&"vehicle_thrust") var thrusting := _thrust_input > 0.0 _set_hull("abort_burn", thrusting) _set_hull("att_pitch", input_pitch) _set_hull("att_yaw", input_yaw) _set_hull("att_roll", input_roll) _set_hull("rcs_direction", 0) var acceleration: Vector3 = _gravity_accel() if thrusting: acceleration += global_transform.basis.y * (thrust_force / mass) * _thrust_input velocity += acceleration * delta var near_surface := is_on_floor() or _crust_exception if not near_surface: var moon := _moon() if moon and global_position.distance_to(moon.global_position) < MOON_RADIUS + NEAR_SURFACE_MARGIN: near_surface = true var earth := _earth() if earth and global_position.distance_to(earth.global_position) < EARTH_RADIUS + NEAR_SURFACE_MARGIN: near_surface = true if near_surface: if body_collision: body_collision.disabled = false move_and_slide() if is_on_floor(): velocity.x = move_toward(velocity.x, 0.0, surface_friction * delta) velocity.z = move_toward(velocity.z, 0.0, surface_friction * delta) if velocity.length() < 0.08: velocity = Vector3.ZERO else: if body_collision: body_collision.disabled = true global_position += velocity * delta PhysicsServer3D.body_set_state( get_rid(), PhysicsServer3D.BODY_STATE_TRANSFORM, global_transform ) PhysicsServer3D.body_set_state( get_rid(), PhysicsServer3D.BODY_STATE_LINEAR_VELOCITY, velocity )