class_name Ship extends RigidBody3D @export_group("Movement") @export var thrust_power = 150.0 # Main thruster power @export var maneuvering_thrust = 75.0 # Side/vertical thruster power @export var vertical_thrust = 120.0 # Up/down thruster power @export var turbo_multiplier = 2.5 # Turbo boost multiplier @export var max_speed = 35.0 # Maximum velocity @export var rotation_power = 20.0 # Angular thrust power @export var max_angular_speed = 3.0 # Maximum rotation speed @export var drag_coefficient = 0.98 # Linear drag (air resistance) @export var angular_drag = 0.95 # Rotational drag @export_group("Camera") var camera_distance = 8.0 var camera_height = 4.0 var camera_smoothing = 10.0 @onready var camera : Camera3D = get_node("Camera3D") @onready var ball = get_parent().get_node("Ball") # HUD Nodes @onready var timer_label = get_node("HUD/Control/TimerLabel") @onready var speed_label = get_node("HUD/Control/InfoPanel/SpeedLabel") @onready var altitude_label = get_node("HUD/Control/InfoPanel/AltitudeLabel") @onready var angular_vel_label = get_node("HUD/Control/InfoPanel/AngularVelLabel") @onready var camera_mode_label = get_node("HUD/Control/InfoPanel/CameraModeLabel") @onready var attitude_label = get_node("HUD/Control/InfoPanel/AttitudeLabel") @onready var heading_label = get_node("HUD/Control/InfoPanel/HeadingLabel") @onready var thrust_label = get_node("HUD/Control/InfoPanel/ThrustLabel") var ball_cam_enabled = true func _ready(): mass = 5 gravity_scale = 1.0 # Set custom inertia for better rotation # Physics: I = m * r² (moment of inertia = mass × radius²) # Lower inertia = easier to rotate, higher inertia = more stable inertia = Vector3(1.0, 1.0, 1.0) # Create and apply low-friction physics material # Physics: F_friction = μ * N (friction force = coefficient × normal force) # Lower μ (friction coefficient) = less resistance to sliding var ship_material = PhysicsMaterial.new() ship_material.friction = 0.1 # Very low friction ship_material.bounce = 0.2 # Slight bounce physics_material_override = ship_material # Make sure the RigidBody is completely free to move and rotate freeze = false lock_rotation = false # Ensure all axes can rotate axis_lock_angular_x = false axis_lock_angular_y = false axis_lock_angular_z = false var game_scene = get_parent() if game_scene and game_scene.has_signal("timer_updated"): game_scene.timer_updated.connect(_on_timer_updated) else: print("Game scene not found or doesn't have timer_updated signal") print("Ship physics configured - Mass: ", mass, " Gravity scale: ", gravity_scale, " Inertia: ", inertia) print("Rotation locks - X:", axis_lock_angular_x, " Y:", axis_lock_angular_y, " Z:", axis_lock_angular_z) func _input(event): if event.is_action_pressed("ui_accept"): # Enter key ball_cam_enabled = !ball_cam_enabled func _on_timer_updated(minutes: int, seconds: int): timer_label.text = "%02d:%02d" % [minutes, seconds] func _physics_process(delta): if camera: _update_camera(delta) _update_hud() func _update_camera(delta): if ball_cam_enabled and ball: _update_ball_cam(delta) else: _update_ship_cam(delta) func _update_ball_cam(delta): # In ball cam, camera positions itself so the ship is between camera and ball # Physics: Vector mathematics for 3D positioning var ship_pos = global_transform.origin var ball_pos = ball.global_transform.origin # Calculate direction from ball to ship # Physics: Vector subtraction and normalization # Direction vector: d̂ = (P₂ - P₁) / |P₂ - P₁| var ball_to_ship = (ship_pos - ball_pos).normalized() # Position camera behind the ship relative to the ball's position # This ensures the ship is always between the camera and ball # Physics: Vector addition for position calculation # P_camera = P_ship + d̂ * distance + height_offset var camera_target_pos = ship_pos + ball_to_ship * camera_distance + Vector3.UP * camera_height # Smoothly move camera to target position # Physics: Linear interpolation (LERP) for smooth motion # P(t) = P₀ + t * (P₁ - P₀), where t ∈ [0,1] # This creates exponential approach to target position camera.global_transform.origin = camera.global_transform.origin.lerp(camera_target_pos, camera_smoothing * delta) # Make camera look at the ball if camera.global_transform.origin.distance_to(ball_pos) > 0.1: # Calculate direction to ball var camera_pos = camera.global_transform.origin var to_ball = (ball_pos - camera_pos).normalized() # Create look-at transform manually # Physics: 3D rotation matrices and basis vectors # Uses right-hand rule: forward = -Z, up = Y, right = X # Basis matrix transforms local coordinates to world coordinates var camera_transform = Transform3D() camera_transform.origin = camera_pos camera_transform.basis = Basis.looking_at(to_ball, Vector3.UP) # Apply the rotation smoothly # Physics: Spherical linear interpolation (SLERP) for rotation # SLERP provides smooth rotation along great circle on unit sphere # Maintains constant angular velocity during interpolation camera.global_transform.basis = camera.global_transform.basis.slerp(camera_transform.basis, camera_smoothing * delta) func _update_ship_cam(delta): # In ship cam, camera follows and looks in the same direction as the ship var ship_pos = global_transform.origin var ship_forward = -global_transform.basis.z # Position camera behind and above the ship var camera_target_pos = ship_pos - ship_forward * camera_distance + Vector3.UP * camera_height # Smoothly move camera camera.global_transform.origin = camera.global_transform.origin.lerp(camera_target_pos, camera_smoothing * delta) # Make camera look in the same direction as the ship var look_target = ship_pos + ship_forward * 10.0 # Look ahead of the ship camera.look_at(look_target, Vector3.UP) func _integrate_forces(state): # Get thruster input var thrust_input = get_thrust_input() var rotation_input = get_rotation_input() # === TRANSLATION (Movement) === apply_thruster_forces(state, thrust_input) # === ROTATION (Turning) === apply_rotation_forces(state, rotation_input) # === DRAG AND LIMITS === apply_drag_and_limits(state, rotation_input) func get_thrust_input() -> Vector3: var thrust = Vector3.ZERO # Forward/Backward thrust (main engines) if Input.is_action_pressed("move_forward"): thrust.z += 1.0 if Input.is_action_pressed("move_back"): thrust.z -= 1.0 # Strafe thrusters (left/right) if Input.is_action_pressed("move_left"): thrust.x -= 1.0 if Input.is_action_pressed("move_right"): thrust.x += 1.0 # Vertical thrusters (up/down) if Input.is_action_pressed("move_up"): thrust.y += 1.0 if Input.is_action_pressed("move_down"): thrust.y -= 1.0 return thrust func get_rotation_input() -> Vector3: var rotation = Vector3.ZERO # Yaw (turn left/right around Y axis) - only use these if they exist if Input.is_action_pressed("turn_left"): rotation.y += 1.0 if Input.is_action_pressed("turn_right"): rotation.y -= 1.0 # Pitch (nose up/down around X axis) if Input.is_action_pressed("pitch_up"): rotation.x -= 1.0 if Input.is_action_pressed("pitch_down"): rotation.x += 1.0 # Roll (bank left/right around Z axis) if Input.is_action_pressed("roll_left"): rotation.z += 1.0 if Input.is_action_pressed("roll_right"): rotation.z -= 1.0 return rotation func apply_thruster_forces(state: PhysicsDirectBodyState3D, thrust_input: Vector3): if thrust_input.length() < 0.01: return # Convert thrust input to world space forces based on ship orientation # Physics: F = m * a (Newton's Second Law: Force = mass × acceleration) # World force = Local force × Rotation matrix (basis transformation) var ship_basis = global_transform.basis var world_thrust = Vector3.ZERO # All thrusters should work relative to ship orientation # Physics: Vector transformation from local to world coordinates # F_world = R * F_local (where R is rotation matrix) # Forward/backward thrust (main engines) world_thrust += -ship_basis.z * thrust_input.z * thrust_power # Strafe thrust (left/right maneuvering thrusters) world_thrust += ship_basis.x * thrust_input.x * maneuvering_thrust # Vertical thrust (up/down thrusters relative to ship orientation) world_thrust += ship_basis.y * thrust_input.y * vertical_thrust # Check for turbo var is_turbo = Input.is_action_pressed("turbo") and thrust_input.z > 0 if is_turbo: world_thrust *= turbo_multiplier # Apply the force # Physics: Δv = F * Δt / m (change in velocity = force × time / mass) state.apply_central_force(world_thrust) func apply_rotation_forces(state: PhysicsDirectBodyState3D, rotation_input: Vector3): if rotation_input.length() < 0.01: return # Apply torque for rotation - simple and effective # Physics: τ = I * α (torque = moment of inertia × angular acceleration) # Also: α = τ / I (angular acceleration = torque / moment of inertia) # Lower inertia = higher angular acceleration for same torque var torque = Vector3( rotation_input.x * rotation_power, # Pitch (rotation around X-axis) rotation_input.y * rotation_power, # Yaw (rotation around Y-axis) rotation_input.z * rotation_power # Roll (rotation around Z-axis) ) # Physics: Δω = τ * Δt / I (change in angular velocity = torque × time / inertia) state.apply_torque(torque) func apply_drag_and_limits(state: PhysicsDirectBodyState3D, rotation_input: Vector3): # Linear drag (air resistance) # Physics: F_drag = -½ * ρ * v² * C_d * A (drag force equation) # Simplified: v_new = v_old * drag_coefficient (exponential decay) # This simulates air resistance reducing velocity over time state.linear_velocity *= drag_coefficient # Angular drag (rotational resistance) # Physics: Similar to linear drag but for rotational motion # τ_drag = -C_angular * ω² (angular drag torque) # Simplified: ω_new = ω_old * angular_drag (exponential decay) if rotation_input.length() < 0.01: # More drag when not actively rotating to stop quicker state.angular_velocity *= 0.9 else: # Normal drag when actively rotating state.angular_velocity *= angular_drag # Limit maximum speeds # Physics: Terminal velocity concept - maximum achievable speed # When thrust force = drag force, acceleration = 0, velocity = constant if state.linear_velocity.length() > max_speed: # Normalize to unit vector, then scale to max speed # Physics: v̂ = v / |v| (unit vector), v_limited = v̂ * v_max state.linear_velocity = state.linear_velocity.normalized() * max_speed if state.angular_velocity.length() > max_angular_speed: # Same concept for angular velocity # Physics: ω̂ = ω / |ω|, ω_limited = ω̂ * ω_max state.angular_velocity = state.angular_velocity.normalized() * max_angular_speed func _update_hud(): # Update speed display # Physics: |v| = √(vₓ² + vᵧ² + vᵤ²) (magnitude of velocity vector) var current_speed = linear_velocity.length() speed_label.text = "Speed: %.1f m/s" % current_speed # Update altitude (Y position relative to ground level) # Physics: Height measurement from reference point (y = 0) var current_altitude = global_transform.origin.y altitude_label.text = "Altitude: %.1f m" % current_altitude # Update angular velocity # Physics: |ω| = √(ωₓ² + ωᵧ² + ωᵤ²) (magnitude of angular velocity vector) var angular_speed = angular_velocity.length() angular_vel_label.text = "Angular Vel: %.2f rad/s" % angular_speed # Update camera mode var camera_mode = "Ball Cam" if ball_cam_enabled else "Ship Cam" camera_mode_label.text = "Camera: %s" % camera_mode # Calculate attitude (pitch and roll from ship orientation) # Physics: Euler angles from rotation matrix # Pitch = rotation around X-axis, Roll = rotation around Z-axis var ship_rotation = global_transform.basis.get_euler(EULER_ORDER_XYZ) var pitch_deg = rad_to_deg(ship_rotation.x) var roll_deg = rad_to_deg(ship_rotation.z) attitude_label.text = "Pitch: %.0f° Roll: %.0f°" % [pitch_deg, roll_deg] # Calculate heading (yaw - direction ship is facing) # Physics: Yaw = rotation around Y-axis (compass heading) # Convert to 0-360° range for traditional compass display var yaw_deg = rad_to_deg(ship_rotation.y) var heading = fmod(yaw_deg + 360.0, 360.0) # Normalize to 0-360° heading_label.text = "Heading: %.0f°" % heading # Calculate current thrust percentage # Physics: Thrust output as percentage of maximum available thrust var thrust_input = get_thrust_input() var thrust_magnitude = thrust_input.length() var thrust_percent = thrust_magnitude * 100.0 thrust_label.text = "Thrust: %.0f%%" % thrust_percent