mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-07-12 18:43:49 +00:00
327 lines
12 KiB
GDScript
327 lines
12 KiB
GDScript
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
|