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 f_was_down: 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 _v_was: bool = false func _ready() -> void: _ensure_actions() 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 = Input.is_key_pressed(KEY_SPACE) 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 = "Press F to Enter" 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 func _unhandled_input(event: InputEvent) -> void: if not player_inside: return if event.is_action_pressed("ui_cancel"): if pause_menu and pause_menu.has_method("open_pause") and player_ref: pause_menu.open_pause(player_ref) get_viewport().set_input_as_handled() return if _event_toggles(event, "capsule_gear", KEY_G): _gear_down = not _gear_down _set_hull("gear_deploy", 1.0 if _gear_down else 0.0) get_viewport().set_input_as_handled() elif _event_toggles(event, "capsule_hatch_crew", KEY_H): _hatch_crew_open = not _hatch_crew_open _set_hull("hatch_crew_open", 1.0 if _hatch_crew_open else 0.0) get_viewport().set_input_as_handled() elif _event_toggles(event, "capsule_hatch_dock", KEY_J): _hatch_dock_open = not _hatch_dock_open _set_hull("hatch_dock_open", 1.0 if _hatch_dock_open else 0.0) get_viewport().set_input_as_handled() elif _event_toggles(event, "capsule_ladder", KEY_L): _ladder_out = not _ladder_out _set_hull("hatch_ladder_deploy", 1.0 if _ladder_out else 0.0) get_viewport().set_input_as_handled() elif _event_toggles(event, "capsule_instruments", KEY_I): _set_hull("instruments_demo", 1.0) get_viewport().set_input_as_handled() elif _event_toggles(event, "capsule_cabin_light", KEY_K): _cabin_dim = not _cabin_dim _set_hull("cabin_dim", _cabin_dim) get_viewport().set_input_as_handled() func _ensure_actions() -> void: var binds := { "interact": KEY_F, "toggle_first_person": KEY_V, "capsule_gear": KEY_G, "capsule_hatch_crew": KEY_H, "capsule_hatch_dock": KEY_J, "capsule_ladder": KEY_L, "capsule_instruments": KEY_I, "capsule_cabin_light": KEY_K, } for action in binds.keys(): if InputMap.has_action(action): continue InputMap.add_action(action) var ev := InputEventKey.new() ev.physical_keycode = binds[action] InputMap.action_add_event(action, ev) func _event_toggles(event: InputEvent, action: String, key: Key) -> bool: if InputMap.has_action(action) and event.is_action_pressed(action): return true if event is InputEventKey and event.pressed and not event.echo and event.physical_keycode == key: return true return false func _act_pressed(action: String) -> bool: return InputMap.has_action(action) and Input.is_action_pressed(action) func _act_just(action: String) -> bool: return InputMap.has_action(action) and Input.is_action_just_pressed(action) func _process(delta: float) -> void: var interact_now := _act_just("interact") or _just_key(KEY_F) if not player_inside and player_ref and interact_now: f_was_down = true enter_capsule() return if not player_inside: f_was_down = Input.is_physical_key_pressed(KEY_F) _update_crust_exception() return if interact_now and not f_was_down: f_was_down = true exit_capsule() return f_was_down = _act_pressed("interact") or Input.is_physical_key_pressed(KEY_F) if _act_just("toggle_first_person"): toggle_camera() elif not InputMap.has_action("toggle_first_person") and _just_key(KEY_V): toggle_camera() var was_free = free_look free_look = Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) if was_free and not free_look and not using_third_person: cockpit_yaw = 0.0 cockpit_pitch = 0.0 if Input.is_key_pressed(KEY_ALT): Input.mouse_mode = Input.MOUSE_MODE_VISIBLE else: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED 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() func _just_key(key: Key) -> bool: var down := Input.is_physical_key_pressed(key) if key == KEY_V: var edge := down and not _v_was _v_was = down return edge return false func _input(event: InputEvent) -> void: if not player_inside: return if Input.is_key_pressed(KEY_ALT): return if event is InputEventMouseMotion: if using_third_person: orbit_yaw -= event.relative.x * look_sensitivity orbit_pitch += event.relative.y * look_sensitivity orbit_pitch = clampf(orbit_pitch, orbit_pitch_min, orbit_pitch_max) elif free_look: cockpit_yaw -= event.relative.x * look_sensitivity cockpit_pitch -= event.relative.y * look_sensitivity cockpit_yaw = clamp(cockpit_yaw, -cockpit_look_limit_yaw, cockpit_look_limit_yaw) cockpit_pitch = clamp(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 f_was_down = true 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() 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 f_was_down = 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 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 func handle_flight(delta: float) -> void: _update_crust_exception() var input_pitch := 0.0 var input_roll := 0.0 var input_yaw := 0.0 if Input.is_key_pressed(KEY_W): input_pitch -= 1.0 if Input.is_key_pressed(KEY_S): input_pitch += 1.0 if Input.is_key_pressed(KEY_A): input_yaw += 1.0 if Input.is_key_pressed(KEY_D): input_yaw -= 1.0 if Input.is_key_pressed(KEY_Q): input_roll -= 1.0 if Input.is_key_pressed(KEY_E): input_roll += 1.0 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) var thrusting := Input.is_key_pressed(KEY_SPACE) _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) 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 )