Ships

Overview

Player-controlled spacecraft.

See also: Tick Processor for physics model, coordinate system, and movement calculations See also: RCS Indicators for RCS pod positions, firing bitmask, and visual rendering

Identification

Property Description
ship_id UUID, unique identifier
name Player’s username (MVP: ship name = player name)

Ships are visible and identifiable to other players by name.

Visibility

All ship data is visible to all players (MVP):

Data Visible
Position Yes
Velocity Yes
Attitude Yes
Thrust level Yes
Fuel level Yes
Name Yes
Ship class Yes

Ship Classes

Each ship class defines a unique set of physical properties: mass, thrust, specific impulse, inertia tensor, and attitude control parameters. The ship class determines the ship’s flight characteristics and visual appearance.

  • Ship class is stored as a ship_class string field on ship state (e.g., "fast_frigate", "cargo_hauler")
  • Class is set at spawn time and is immutable until next respawn
  • Players select their ship class during respawn (see Web Client — Ship Class Selection)
  • First-time players (registration) receive the Fast Frigate automatically

Cargo Hauler

Heavy modular hauler designed for bulk transport. Sluggish rotation but high delta-v and fuel capacity.

Physical Properties

Property Value Notes
Dry mass 100,000 kg Hull, cargo pods, systems, no fuel
Fuel capacity 60,000 kg Hydrogen propellant
Total mass (full) 160,000 kg  

Main Engine (Dual Fusion Drive)

Property Value Notes
Thrust 400 kN At full throttle (2 engines × 200 kN)
Specific impulse 15,000 s Fuel efficiency
Fuel consumption 2.72 kg/s At full throttle

Fuel consumption formula:

fuel_rate = thrust / (isp × g₀)
          = 400,000 / (15,000 × 9.80665)
          = 2.72 kg/s

Performance (Full Fuel)

Metric Value
Acceleration (full) 2.5 m/s²
Acceleration (empty) 4.0 m/s²
Delta-v (total) ~69,200 m/s

Delta-v formula:

Δv = isp × g₀ × ln(m_wet / m_dry)
   = 15,000 × 9.80665 × ln(160,000 / 100,000)
   = 147,100 × 0.4700
   ≈ 69,200 m/s

Attitude Control

System Specification
Reaction wheels Max torque: 2,000 N·m per axis
Reaction wheel capacity 40,000 N·m·s angular momentum per axis
RCS thrusters Max torque: 20,000 N·m per axis
RCS total thrust 20 kN (distributed across thruster pairs)
RCS fuel Shared with main propellant
RCS specific impulse 3,000 s
RCS fuel consumption 0.68 kg/s at full thrust

RCS fuel consumption formula (at full thrust):

rcs_fuel_rate_max = rcs_thrust / (rcs_isp × g₀)
                  = 20,000 / (3,000 × 9.80665)
                  = 0.68 kg/s

RCS fuel consumption when providing torque (proportional to torque applied):

thrust_fraction = rcs_torque_applied / max_rcs_torque
                = rcs_torque_applied / 20,000 N·m

rcs_fuel_rate = rcs_fuel_rate_max × thrust_fraction
              = 0.68 kg/s × (rcs_torque_applied / 20,000)

fuel_consumed_per_tick = rcs_fuel_rate × tick_duration

Desaturation (automatic when player manual input is zero and wheels >50% saturated; attitude controller output does not block desaturation):

  • RCS applies counter-torque to reduce wheel momentum toward zero
  • Desaturation torque: min(wheel_momentum / dt, max_rcs_torque)
  • Fuel consumption uses same formula above

RCS Translation

RCS thrusters provide 6-DOF translational thrust in the ship’s body frame, independent of rotational RCS. Translation uses half the total RCS thrust capacity per axis.

Property Value Notes
RCS translation thrust 10 kN per axis Half of 20 kN total RCS thrust
RCS translation fuel rate 0.34 kg/s At full thrust on one axis
RCS specific impulse 3,000 s Same as rotational RCS

RCS translation fuel consumption formula (per axis, at full input):

rcs_trans_fuel_rate = rcs_trans_thrust / (rcs_isp × g₀)
                   = 10,000 / (3,000 × 9.80665)
                   = 0.34 kg/s

Translation input is a Vec3 with each axis in [-1, 1]:

  • X axis: Right (+1) / Left (-1)
  • Y axis: Up (+1) / Down (-1)
  • Z axis: Forward (+1) / Backward (-1)

Fuel consumption is additive across active axes:

per_axis_fuel_rate = rcs_trans_fuel_rate × |input_axis|
total_trans_fuel_rate = sum(per_axis_fuel_rate for each axis)

Translation force is computed in body frame and rotated to ICRF via ship attitude quaternion. Translation thrust is independent of and additive with main engine thrust and rotational RCS fuel.

Combined Fuel Consumption

Main engine, rotational RCS, and translation RCS draw from the same fuel tank. When active simultaneously, rates are additive:

main_engine_rate = 2.72 kg/s × thrust_level
rcs_rotation_rate = 0.68 kg/s × rcs_thrust_fraction
rcs_translation_rate = 0.34 kg/s × sum(|input_axis|)

total_fuel_rate = main_engine_rate + rcs_rotation_rate + rcs_translation_rate
fuel_consumed_per_tick = total_fuel_rate × tick_duration
Scenario Fuel Rate
Full thrust, no rotation 2.72 kg/s
No thrust, full rotational RCS 0.68 kg/s
Full thrust + full rotational RCS 3.40 kg/s
No thrust, full translation 1 axis 0.34 kg/s
No thrust, full translation 3 axes 1.02 kg/s
Full thrust + full rot RCS + full trans 3 axes 4.42 kg/s (maximum)
50% thrust + 50% RCS 1.70 kg/s

Inertia Tensor

Modular hauler — elongated central spine with cargo pods adding off-axis mass.

Dry (empty) inertia tensor:

I_dry = | 4,000,000    0          0       |  kg·m²
        |    0      4,000,000     0       |
        |    0         0       800,000    |

Full (with fuel) inertia tensor:

I_full = | 6,400,000    0          0       |  kg·m²
         |    0      6,400,000     0       |
         |    0         0       1,280,000  |

Dynamic calculation: Linear interpolation based on fuel fraction:

fuel_fraction = current_fuel / fuel_capacity
I_current = I_dry + fuel_fraction × (I_full - I_dry)

The hauler’s ~20× larger inertia combined with only 2× actuator torque results in roughly 10× slower angular acceleration than the Fast Frigate at max actuator torque, making it noticeably more sluggish to rotate.

Fast Frigate

Lightweight escort craft. Fast and agile with high thrust-to-weight ratio, but limited fuel reserves.

Physical Properties

Property Value Notes
Dry mass 8,000 kg Hull, systems, no fuel
Fuel capacity 15,000 kg Hydrogen propellant
Total mass (full) 23,000 kg  

Main Engine (Fusion Drive)

Property Value Notes
Thrust 600 kN At full throttle
Specific impulse 20,000 s Fuel efficiency
Fuel consumption 3.06 kg/s At full throttle

Fuel consumption formula:

fuel_rate = thrust / (isp × g₀)
          = 600,000 / (20,000 × 9.80665)
          = 3.06 kg/s

Performance (Full Fuel)

Metric Value
Acceleration (full) 26.1 m/s² (~2.7g)
Acceleration (empty) 75 m/s²
Delta-v (total) ~207 km/s

Delta-v formula:

Δv = isp × g₀ × ln(m_wet / m_dry)
   = 20,000 × 9.80665 × ln(23,000 / 8,000)
   = 196,133 × 1.0560
   ≈ 207,100 m/s

Attitude Control

System Specification
Reaction wheels Max torque: 500 N·m per axis
Reaction wheel capacity 5,000 N·m·s angular momentum per axis
RCS thrusters Max torque: 8,000 N·m per axis
RCS total thrust 8 kN (distributed across thruster pairs)
RCS fuel Shared with main propellant
RCS specific impulse 3,000 s
RCS fuel consumption 0.27 kg/s at full thrust

RCS fuel consumption formula (at full thrust):

rcs_fuel_rate_max = rcs_thrust / (rcs_isp × g₀)
                  = 8,000 / (3,000 × 9.80665)
                  = 0.27 kg/s

RCS fuel consumption when providing torque (proportional to torque applied):

thrust_fraction = rcs_torque_applied / max_rcs_torque
                = rcs_torque_applied / 8,000 N·m

rcs_fuel_rate = rcs_fuel_rate_max × thrust_fraction
              = 0.27 kg/s × (rcs_torque_applied / 8,000)

fuel_consumed_per_tick = rcs_fuel_rate × tick_duration

Desaturation (automatic when player manual input is zero and wheels >50% saturated):

  • RCS applies counter-torque to reduce wheel momentum toward zero
  • Desaturation torque: min(wheel_momentum / dt, max_rcs_torque)
  • Fuel consumption uses same formula above

RCS Translation

Property Value Notes
RCS translation thrust 4 kN per axis Half of 8 kN total RCS thrust
RCS translation fuel rate 0.14 kg/s At full thrust on one axis
RCS specific impulse 3,000 s Same as rotational RCS

RCS translation fuel consumption formula (per axis, at full input):

rcs_trans_fuel_rate = rcs_trans_thrust / (rcs_isp × g₀)
                   = 4,000 / (3,000 × 9.80665)
                   = 0.14 kg/s

See Cargo Hauler RCS Translation for detailed axis mapping and fuel computation.

Combined Fuel Consumption

Main engine, rotational RCS, and translation RCS draw from the same fuel tank. When active simultaneously, rates are additive:

main_engine_rate = 3.06 kg/s × thrust_level
rcs_rotation_rate = 0.27 kg/s × rcs_thrust_fraction
rcs_translation_rate = 0.14 kg/s × sum(|input_axis|)

total_fuel_rate = main_engine_rate + rcs_rotation_rate + rcs_translation_rate
fuel_consumed_per_tick = total_fuel_rate × tick_duration
Scenario Fuel Rate
Full thrust, no rotation 3.06 kg/s
No thrust, full rotational RCS 0.27 kg/s
Full thrust + full rotational RCS 3.33 kg/s
No thrust, full translation 1 axis 0.14 kg/s
No thrust, full translation 3 axes 0.42 kg/s
Full thrust + full rot RCS + full trans 3 axes 3.75 kg/s (maximum)
50% thrust + 50% RCS 1.67 kg/s

Inertia Tensor

Lightweight elongated hull — small and agile.

Dry (empty) inertia tensor:

I_dry = | 40,000    0       0    |  kg·m²
        |    0    40,000    0    |
        |    0       0    15,000 |

Full (with fuel) inertia tensor:

I_full = | 80,000    0       0    |  kg·m²
         |    0    80,000    0    |
         |    0       0    30,000 |

Dynamic calculation: Linear interpolation based on fuel fraction:

fuel_fraction = current_fuel / fuel_capacity
I_current = I_dry + fuel_fraction × (I_full - I_dry)

The frigate’s ~5× smaller inertia combined with comparable actuator torque results in fast angular acceleration, settling attitude in ~8 seconds.

Long-Range Explorer

Deep-space endurance vessel. Highest delta-v of any ship via very high Isp and large fuel fraction, but low acceleration. Largest vessel in the fleet.

Physical Properties

Property Value Notes
Dry mass 50,000 kg Lighter than hauler (no cargo pods), heavier than frigate
Fuel capacity 120,000 kg 70% fuel fraction — range is the point
Total mass (full) 170,000 kg  

Main Engine (High-Efficiency Fusion Drive)

Property Value Notes
Thrust 200 kN Single high-efficiency drive — low but steady
Specific impulse 50,000 s 2.5× frigate, the key advantage
Fuel consumption 0.408 kg/s At full throttle

Fuel consumption formula:

fuel_rate = thrust / (isp × g₀)
          = 200,000 / (50,000 × 9.80665)
          = 0.408 kg/s

Performance (Full Fuel)

Metric Value
Acceleration (full) 1.18 m/s²
Acceleration (empty) 4.0 m/s²
Delta-v (total) ~599 km/s

Delta-v formula:

Δv = isp × g₀ × ln(m_wet / m_dry)
   = 50,000 × 9.80665 × ln(170,000 / 50,000)
   = 490,333 × 1.2238
   ≈ 599,900 m/s

Attitude Control

System Specification
Reaction wheels Max torque: 1,000 N·m per axis
Reaction wheel capacity 20,000 N·m·s angular momentum per axis
RCS thrusters Max torque: 12,000 N·m per axis
RCS total thrust 12 kN (distributed across thruster pairs)
RCS fuel Shared with main propellant
RCS specific impulse 3,000 s
RCS fuel consumption 0.41 kg/s at full thrust

RCS fuel consumption formula (at full thrust):

rcs_fuel_rate_max = rcs_thrust / (rcs_isp × g₀)
                  = 12,000 / (3,000 × 9.80665)
                  = 0.41 kg/s

RCS fuel consumption when providing torque (proportional to torque applied):

thrust_fraction = rcs_torque_applied / max_rcs_torque
                = rcs_torque_applied / 12,000 N·m

rcs_fuel_rate = rcs_fuel_rate_max × thrust_fraction
              = 0.41 kg/s × (rcs_torque_applied / 12,000)

fuel_consumed_per_tick = rcs_fuel_rate × tick_duration

Desaturation (automatic when player manual input is zero and wheels >50% saturated):

  • RCS applies counter-torque to reduce wheel momentum toward zero
  • Desaturation torque: min(wheel_momentum / dt, max_rcs_torque)
  • Fuel consumption uses same formula above

RCS Translation

Property Value Notes
RCS translation thrust 6 kN per axis Half of 12 kN total RCS thrust
RCS translation fuel rate 0.20 kg/s At full thrust on one axis
RCS specific impulse 3,000 s Same as rotational RCS

RCS translation fuel consumption formula (per axis, at full input):

rcs_trans_fuel_rate = rcs_trans_thrust / (rcs_isp × g₀)
                   = 6,000 / (3,000 × 9.80665)
                   = 0.20 kg/s

See Cargo Hauler RCS Translation for detailed axis mapping and fuel computation.

Combined Fuel Consumption

Main engine, rotational RCS, and translation RCS draw from the same fuel tank. When active simultaneously, rates are additive:

main_engine_rate = 0.408 kg/s × thrust_level
rcs_rotation_rate = 0.41 kg/s × rcs_thrust_fraction
rcs_translation_rate = 0.20 kg/s × sum(|input_axis|)

total_fuel_rate = main_engine_rate + rcs_rotation_rate + rcs_translation_rate
fuel_consumed_per_tick = total_fuel_rate × tick_duration
Scenario Fuel Rate
Full thrust, no rotation 0.408 kg/s
No thrust, full rotational RCS 0.41 kg/s
Full thrust + full rotational RCS 0.82 kg/s
No thrust, full translation 1 axis 0.20 kg/s
No thrust, full translation 3 axes 0.60 kg/s
Full thrust + full rot RCS + full trans 3 axes 1.42 kg/s (maximum)
50% thrust + 50% RCS 0.41 kg/s

Inertia Tensor

Large elongated hull with massive fuel tanks adding off-axis mass.

Dry (empty) inertia tensor:

I_dry = | 2,000,000    0          0       |  kg·m²
        |    0      2,000,000     0       |
        |    0         0       400,000    |

Full (with fuel) inertia tensor:

I_full = | 8,000,000    0          0       |  kg·m²
         |    0      8,000,000     0       |
         |    0         0       1,600,000  |

Dynamic calculation: Linear interpolation based on fuel fraction:

fuel_fraction = current_fuel / fuel_capacity
I_current = I_dry + fuel_fraction × (I_full - I_dry)

The explorer’s large inertia combined with moderate actuator torque results in slow but steady rotation, settling attitude in ~16 seconds (ω_n = 0.25 rad/s).

Planetary Lander

Small, lightweight lander designed for Luna surface operations. Descends from medium orbit to the surface with controlled hover/descent, lands on visual landing legs, and returns to orbit after surface refueling.

Physical Properties

Property Value Notes
Dry mass 3,000 kg Lightest ship class
Fuel capacity 2,000 kg Sufficient for orbit-to-surface + ascent after refuel
Total mass (full) 5,000 kg  

Main Engine (Bipropellant Thruster Array)

Property Value Notes
Thrust 50 kN 4 × 12.5 kN gimballed engines
Specific impulse 3,000 s Chemical-class, high thrust priority
Fuel consumption 1.70 kg/s At full throttle

Performance (Full Fuel)

Metric Value
Acceleration (full) 10.0 m/s²
Acceleration (empty) 16.7 m/s²
Lunar TWR (full, surface) 6.2
Delta-v (total) ~15,100 m/s

Attitude Control

System Specification
Reaction wheels Max torque: 200 N·m per axis
Reaction wheel capacity 2,000 N·m·s angular momentum per axis
RCS thrusters Max torque: 5,000 N·m per axis
RCS total thrust 5 kN
RCS fuel consumption 0.17 kg/s at full thrust

RCS Translation

Property Value Notes
RCS translation thrust 2.5 kN per axis Half of 5 kN total RCS thrust
RCS translation fuel rate 0.085 kg/s At full thrust on one axis

Inertia Tensor

Compact upright body — symmetric about Z axis, wider at base due to landing legs.

Dry (empty) inertia tensor:

I_dry = | 6,000    0       0    |  kg·m²
        |    0   6,000     0    |
        |    0      0    3,000  |

Full (with fuel) inertia tensor:

I_full = | 10,000    0       0    |  kg·m²
         |    0    10,000    0    |
         |    0       0    5,000  |
Ship Class ω_n (rad/s) Settling Time
Planetary Lander 0.5 ~8 s
Fast Frigate 0.5 ~8 s
Long-Range Explorer 0.25 ~16 s
Cargo Hauler 0.15 ~27 s

Atmospheric Drag Properties

Each ship class has drag properties used for atmospheric drag computation (see Atmospheric Drag):

Ship Class Cd Area (m^2)
Fast Frigate 2.2 15
Cargo Hauler 2.5 80
Long-Range Explorer 2.2 40
Planetary Lander 2.0 8

Fuel Depletion

When a ship runs out of fuel:

  • Main engine cannot fire
  • RCS thrusters cannot fire
  • Reaction wheels continue to function (no fuel required)
  • Ship drifts on current trajectory

Recovery (Initial Release)

Player convenience services (no cost or penalty):

Service Effect
Fuel service Instantly refills propellant to capacity
Reset Returns ship to orbit around a chosen body, refills fuel, matches body-relative orbital velocity

Reset places ship in circular orbit at a curated altitude above the selected body (default: Earth at 400 km).

Spawn Body Selection

The reset service accepts an optional target_body parameter specifying which celestial body to orbit:

  • If omitted or empty, defaults to Earth
  • If set to Sun, spawns at 10,000,000 km (~0.07 AU) in ecliptic-plane circular orbit, inside Mercury’s orbit
  • If set to an unrecognized body name, returns error E003

Curated Orbit Altitudes

Each body has a curated spawn altitude chosen for a safe, visually interesting low orbit:

Body Alt (km) Body Alt (km) Body Alt (km)
Sun 10,000,000 (0.07 AU)        
Mercury 200 Jupiter 1,000 Uranus 800
Venus 250 Io 100 Miranda 20
Earth 400 Europa 100 Ariel 50
Luna 100 Ganymede 200 Umbriel 50
Mars 250 Callisto 200 Titania 50
Phobos 3 Saturn 1,000 Oberon 50
Deimos 5 Mimas 20 Neptune 800
    Enceladus 50 Triton 100
    Tethys 50    
    Dione 50    
    Rhea 50    
    Titan 300    
    Iapetus 50    

Reset Behavior Details

Reset always applies the full reset state (never a no-op):

Property Reset Value
Position New spawn offset position (sequential, see below)
Velocity Orbital velocity relative to Earth (7,672 m/s)
Attitude +Z axis aligned with velocity vector
Angular velocity Zero
Thrust level 0
Fuel Refilled to capacity (per ship class)
Wheel momentum Zeroed (all axes)

Rate limit: 1 reset per minute (returns E004 if exceeded)

Idempotency: Calling reset when already in LEO still performs reset (new spawn position, fuel topped off, state normalized).

Spawn Orbit Calculation

Orbital velocity formula:

v = √(GM/r)

Where M is the target body’s mass and r = body radius + curated altitude.

Example (Earth): | Symbol | Value | Description | |——–|——-|————-| | G | 6.67430 × 10⁻¹¹ m³/(kg·s²) | Gravitational constant | | M | 5.972 × 10²⁴ kg | Earth mass | | r | 6.771 × 10⁶ m | Earth radius (6.371 × 10⁶) + altitude (400 km) | | v | 7,672 m/s | Resulting orbital velocity |

Spawn state:

  • Position: Curated altitude above the target body’s surface at sequential orbital offset
  • Velocity: Circular orbital velocity perpendicular to position vector (prograde)
  • Attitude: +Z axis aligned with velocity vector (facing direction of travel)
  • Angular velocity: Zero
  • All values relative to the target body’s current position/velocity in simulation

Attitude quaternion calculation:

To compute a quaternion that aligns the ship’s +Z axis with the velocity direction:

# Normalize velocity to get target direction
target = normalize(velocity)

# Ship's default forward direction (+Z axis)
forward = [0, 0, 1]

# Compute rotation axis (cross product)
axis = cross(forward, target)

# Handle edge case: vectors are parallel
if length(axis) < 1e-6:
    if dot(forward, target) > 0:
        # Same direction, identity quaternion
        q = [1, 0, 0, 0]
    else:
        # Opposite direction, rotate 180° around any perpendicular axis
        q = [0, 1, 0, 0]  # 180° around X axis
else:
    axis = normalize(axis)

    # Compute rotation angle
    angle = acos(clamp(dot(forward, target), -1, 1))

    # Convert axis-angle to quaternion
    half_angle = angle / 2
    q = [cos(half_angle),
         axis[0] * sin(half_angle),
         axis[1] * sin(half_angle),
         axis[2] * sin(half_angle)]

The resulting quaternion q = [w, x, y, z] orients the ship with nose (+Z) facing the direction of travel.

Sequential spawn offset (prevents spawn collisions for up to 16 players):

spawn_index = total_spawns_ever % 16
spawn_angle = spawn_index × 22.5°

# Position and velocity in flat orbital plane
pos_flat = orbital_radius × [cos(spawn_angle), sin(spawn_angle), 0]
vel_flat = orbital_velocity × [-sin(spawn_angle), cos(spawn_angle), 0]

# Tilt to equatorial plane using proper spin axis quaternion.
# For planets: use the body's own spin axis from BODY_SPIN_AXES.
# For moons: use body's own axis if defined in BODY_SPIN_AXES,
#   else fall through to parent planet's spin axis.
spin_axis = BODY_SPIN_AXES.get(body_name) or BODY_SPIN_AXES[parent]
tilt_rotation = quaternion_rotating_ecliptic_north_to_spin_axis(spin_axis)
relative_position = tilt_rotation.rotate(pos_flat)
relative_velocity = tilt_rotation.rotate(vel_flat)

# Convert to solar system barycenter coordinates
position = body_position + relative_position
velocity = body_velocity + relative_velocity

Equatorial prograde orbit: Ships spawn in the target body’s equatorial plane (tilted by the body’s obliquity from the ecliptic) orbiting prograde. For Earth, this is tilted 23.44° matching real-world equatorial launches.

Exception: Luna. Luna has its own explicit spin axis in BODY_SPIN_AXES ([0.0220, 0.0154, 0.9996], only 1.54° from the ecliptic pole) because its orbit is inclined ~5.1° to the ecliptic rather than to Earth’s equatorial plane. Ships spawning around Luna orbit near the ecliptic plane, not tilted 23°. See celestial-bodies.md Spin Axes.

The total_spawns_ever counter increments on every spawn/reset, ensuring each spawn gets a unique position even if players reset multiple times.

Counter storage and ownership:

  • Stored in Redis: game:total_spawns
  • Owned by: physics service
  • Incremented atomically via Redis INCR command
  • Persisted in snapshots for recovery
  • Reset to 0 on admin factory reset

State Persistence

Ship state persists across player login/logout sessions.

Persistence Behavior

Event Ship State
Player logout Ship remains in physics simulation with all state preserved
Player login (ship exists) Reconnect to existing ship, no state changes
Player login (ship missing) Re-spawn new ship at default position

Persisted State

All ship properties are persisted in Redis and survive player disconnection:

Property Persisted Notes
Ship class Yes Via Redis ship:{id} hash field
Position Yes Via Redis ship:{id}:position
Velocity Yes Via Redis ship:{id}:velocity
Attitude Yes Quaternion [w, x, y, z]
Angular velocity Yes Rotation rates persist across sessions
Rotation input Yes Player’s commanded rotation rates
Translation input Yes Player’s commanded translation axes [-1, 1]
Thrust level Yes Player’s commanded thrust
Fuel Yes Current fuel mass
Wheel momentum Yes Reaction wheel state
Attitude hold Yes Whether attitude hold is enabled
Attitude mode Yes Current attitude mode (none, hold, prograde, retrograde, normal, antinormal, radial, antiradial, local_horizontal, local_vertical, target, anti_target)
Attitude target Yes Target quaternion for attitude control (when mode != none)
Attitude target ID Yes Entity ID for TARGET mode (body name, ship_id, station_id)
Attitude target type Yes Entity type for TARGET mode (“body”, “ship”, “station”)

Attitude Control Modes

Ships support automatic attitude control through various modes:

Mode Description Trigger
none No automatic control H key (when hold active)
hold Rate damping only (stop rotation) H key (when hold inactive)
prograde Orient +Z axis toward velocity P key
retrograde Orient +Z axis opposite velocity Shift+P key
normal Orient +Z axis along orbit normal (h = r × v) J key
antinormal Orient +Z axis opposite orbit normal (-h) Shift+J key
radial Orient +Z axis perpendicular to velocity in orbital plane (v̂ × ĥ) K key
antiradial Orient +Z axis opposite radial direction (ĥ × v̂) Shift+K key
local_horizontal Orient +Z axis along horizontal velocity component (v − (v·r̂)r̂) L key
local_vertical Orient +Z axis along radial direction from body center (r̂) Shift+L key
target Orient +Z axis toward selected target’s position G key
anti_target Orient +Z axis away from selected target’s position Shift+G key
target_prograde Orient +Z axis toward relative velocity with target Y key
target_retrograde Orient +Z axis opposite relative velocity with target Shift+Y key

Attitude Tracking Behavior

Reference body selection uses Hill sphere (SOI) containment, preferring the most specific body (smallest SOI) that contains the ship. Falls back to gravitational influence (M/r²) if no SOI contains the ship. Hill sphere radius: r_SOI = d × (m_body / (3 × m_parent))^(1/3).

Important: Reference body lookup must use body positions that are time-consistent with ship positions. In the tick loop, body N-body integration runs before ship updates, so the reference body lookup uses pre-update body positions (same time as ship positions) to avoid position mismatches that exceed small Hill spheres (e.g. Phobos orbits at 2.1 km/s but its Hill sphere is only 16.7 km).

When any tracking mode command is issued:

  1. Find reference body via SOI containment (smallest Hill sphere containing ship)
  2. Compute the target direction vector based on mode (see below)
  3. If the direction vector is degenerate (see thresholds below):
    • Body-relative modes (prograde, retrograde, etc.): command is silently ignored (returns success)
    • Target modes (target, anti_target, target_prograde, target_retrograde): command fails with specific error code (E022/E023/E024) — see Target Attitude Error Codes
  4. Compute target quaternion to align ship +Z axis with target direction
  5. Set attitude mode and target
  6. Attitude hold controller tracks toward target

Reference body lookup failure during tick update: During each tick, the physics service recomputes attitude targets for ships in tracking modes. If _find_reference_body() returns None (ship has escaped all SOI and M/r^2 fallback also fails), a warning must be logged with the ship ID and current attitude mode. The attitude target update is skipped (preserving the stale target), and the ship’s attitude mode is set to HOLD to prevent indefinite stale-target tracking. This ensures ships that escape all SOI gracefully degrade to holding their current orientation rather than silently tracking a stale direction.

Target direction computation by mode:

Mode Direction Vector Degenerate When
prograde normalize(v_rel) |v_rel| < 1.0 m/s
retrograde -normalize(v_rel) |v_rel| < 1.0 m/s
normal normalize(r × v) where r = ship_pos - body_pos, v = v_rel |r × v| < 1.0
antinormal -normalize(r × v) |r × v| < 1.0
radial normalize(v̂ × ĥ) where ĥ = normalize(r × v) |r × v| < 1.0 or |v_rel| < 1.0
antiradial normalize(ĥ × v̂) = -radial |r × v| < 1.0 or |v_rel| < 1.0
local_horizontal normalize(v - (v·r̂)r̂) |v_horiz| < 1.0
local_vertical normalize(r) where r = ship_pos - body_pos |r| < 1.0
target normalize(target_pos - ship_pos) |target_pos - ship_pos| < 1.0
anti_target -normalize(target_pos - ship_pos) |target_pos - ship_pos| < 1.0
target_prograde normalize(v_ship - v_target) |v_ship - v_target| < 1.0 m/s
target_retrograde -normalize(v_ship - v_target) |v_ship - v_target| < 1.0 m/s

Where v_rel = ship_velocity - body_velocity, r = ship_position - body_position.

TARGET / ANTI_TARGET / TARGET_PROGRADE / TARGET_RETROGRADE mode details:

The target mode orients the ship toward a selected target entity. The anti_target mode orients the ship away from the target (opposite direction). The target_prograde mode orients the ship along the relative velocity vector with respect to the target (v_ship - v_target). The target_retrograde mode orients the ship opposite to the relative velocity vector. Unlike other tracking modes which use the reference body, these modes look up the target’s position and velocity directly by attitude_target_id and attitude_target_type:

Target type Position/velocity source
body Celestial body position/velocity from simulation state
ship Other ship’s position/velocity from Redis
station Station position/velocity from Redis
lagrange Computed from body pair positions/masses (see below)

Lagrange point targeting:

Lagrange point IDs use the key format "PrimaryName-SecondaryName-Lx" (e.g., "Sun-Earth-L4"). The physics service parses this key to extract the primary body name, secondary body name, and L-point label (L1–L5), then computes the Lagrange point position at runtime using the same algorithm as the client:

  • L1: primary_pos + unit_vec * d * (1 - alpha) (between bodies)
  • L2: primary_pos + unit_vec * d * (1 + alpha) (beyond secondary)
  • L3: primary_pos - unit_vec * d * (1 + 5*mu/12) (opposite side)
  • L4/L5: Rodrigues’ rotation of separation vector by ±60° around orbit normal

Where alpha = cbrt(m2 / (3 * m1)), mu = m2 / (m1 + m2), unit_vec = normalized direction from primary to secondary, and orbit normal = normalize(r × v_rel).

Lagrange point velocity is approximated as zero for relative velocity computation (matching client behavior).

Ship state fields for TARGET modes:

Field Type Description
attitude_target_id string Target entity ID (body name, ship_id, station_id, or lagrange key)
attitude_target_type string "body", "ship", "station", "lagrange"

These fields are persisted in Redis alongside other ship state and cleared when attitude mode changes away from target, anti_target, target_prograde, or target_retrograde.

Target Attitude Error Codes

When a target attitude mode command fails due to a missing target or degenerate condition, the physics service returns success=false with a specific error code. The API gateway forwards this as an error message to the client via WebSocket.

Error Code Condition Message
E022 Target entity not found (no body/ship/station/lagrange matching target_id) “Target not found”
E023 Relative velocity too low for target_prograde/target_retrograde (|v_ship - v_target| < 1.0 m/s) “Relative velocity too low for target prograde/retrograde”
E024 Too close to target for target/anti_target (|target_pos - ship_pos| < 1.0 m) “Too close to target”

The client displays these messages in the status bar for 5 seconds via the existing error message handler.

Target quaternion calculation:

# Get velocity relative to reference body (SOI containment)
relative_velocity = ship_velocity - nearest_body_velocity

# Skip if velocity too small
if magnitude(relative_velocity) < 1.0:
    return  # Command ignored

# Prograde: target direction is velocity direction
# Retrograde: target direction is opposite velocity
target_direction = normalize(relative_velocity)  # prograde
target_direction = -normalize(relative_velocity) # retrograde

# Compute quaternion to align +Z with target direction
# (Same algorithm as spawn attitude calculation)
forward = [0, 0, 1]
axis = cross(forward, target_direction)
if length(axis) < 1e-6:
    if dot(forward, target_direction) > 0:
        target_quaternion = [1, 0, 0, 0]  # Identity
    else:
        target_quaternion = [0, 1, 0, 0]  # 180° around X
else:
    axis = normalize(axis)
    angle = acos(clamp(dot(forward, target_direction), -1, 1))
    half = angle / 2
    target_quaternion = [cos(half), axis[0]*sin(half), axis[1]*sin(half), axis[2]*sin(half)]

Attitude Hold Controller

When attitude mode is not none, the controller computes desired torque in physical units (N·m) using inertia-compensated PD control. This ensures consistent dynamics regardless of axis or fuel level.

Controller design:

  • Natural frequency: ω_n is per-ship-class (see individual ship class sections), determining settling time ~4/ω_n seconds in linear regime
  • Damping ratio: ζ = 1.0 (critically damped — no oscillation)
  • Gains are multiplied by per-axis inertia for uniform response across axes
Ship Class ω_n (rad/s) Settling Time
Fast Frigate 0.5 ~8 s
Cargo Hauler 0.15 ~27 s

Mode: hold (rate damping only)

  • Desired torque: τ = -2ζω_n × I_axis × angular_velocity
  • Ship rotation damps to zero without oscillation

Mode: all tracking modes (prograde, retrograde, normal, antinormal, radial, antiradial, local_horizontal, local_vertical, target, anti_target)

  • Compute attitude error quaternion between current and target
  • Desired torque per axis: τ = (ω_n² × error - 2ζω_n × ω) × I_axis
  • Target is continuously updated each tick to track current velocity direction
  • For large errors, torque saturates at actuator limits (bang-bang behavior), then transitions smoothly to linear PD as error decreases

Torque allocation:

  • Desired torque is allocated to reaction wheels first, RCS overflow second
  • No normalized [-1, 1] clamping — torque is computed and clamped at actuator level

Spin-trap prevention: At high angular rates, the proportional gain is smoothly reduced to prevent the proportional term from overpowering the derivative (braking) term when torque actuators saturate. The proportional gain scales as kp_eff = kp * max(0, 1 - |ω| / ω_n), transitioning to pure rate damping when |ω| ≥ ω_n. This prevents a self-reinforcing spin trap where saturated torque alternately brakes and accelerates the spin, with the accelerating phase slightly longer.

Attitude sub-stepping for time-scale independence:

At higher time scales (dt > 1.0s), the forward Euler integration of the PD controller can become marginally stable or oscillate. To ensure consistent attitude dynamics regardless of time scale:

  • Max attitude sub-step: 1.0 seconds (MAX_ATTITUDE_DT = 1.0)
  • Sub-step count: N = ceil(dt / MAX_ATTITUDE_DT), with sub_dt = dt / N
  • At time_scale=1 (dt=1s), N=1 — no overhead
  • At time_scale=2 (dt=2s), N=2 — two half-steps
  • Attitude target is computed once per tick (body positions don’t change during sub-steps)
  • PD torque is re-evaluated each sub-step — attitude error changes after each integration step
  • Only attitude control + integration are sub-stepped — position/velocity leapfrog remains at full dt (symplectic, stable at these time scales)
  • RCS fuel consumption is summed across sub-steps

Manual override:

  • Rotation input from player temporarily overrides automatic control
  • When rotation input is zero, automatic control resumes

Ship Existence Check

On player login, the players service checks if the ship exists in the physics service:

  1. Call GetShipState(ship_id) on physics service
  2. If response contains valid ship_id matching the request, ship exists
  3. If gRPC error or empty response, ship does not exist
  4. Only re-spawn if ship is confirmed missing

This ensures players reconnect to their existing ship with all state intact, rather than getting a fresh ship on every login.

Ship Removal

Ships are removed from the physics simulation when:

Trigger Timing
Player deletes account Immediate
Admin deletes player Immediate
Server restart (without snapshot) All ships lost

Note: Player logout does NOT remove the ship. Ships persist indefinitely until explicitly removed.

Collision and Destruction

Body Collision Detection

Each physics tick, after position integration, the simulation checks whether any ship has entered a celestial body (distance from body center < body radius). If so, the ship is destroyed and immediately respawned at a safe orbit.

Detection: For each ship, for each body: dist(ship.position, body.position) < body.radius.

On collision (permanent destruction):

  1. Record the ship as destroyed (ship_id, player_id, body_name)
  2. Filter destroyed ships from the physics batch write (do not write stale state)
  3. Delete the ship’s Redis hash (ship:{ship_id})
  4. Publish ship.destroyed event to galaxy:ships Redis stream
  5. API gateway broadcasts ship.destroyed to all WebSocket clients
  6. API gateway notifies players service via NotifyShipDestroyed gRPC call
  7. Players service removes ship from fleet DB and Redis fleet set
  8. If destroyed ship was the player’s active ship, switch to another ship (or clear active ship if none remain)

See Ship Destruction spec for full details.

Event format (galaxy:ships stream):

event: ship.destroyed
ship_id: <uuid>
player_id: <uuid>
body: <string>    # Name of body the ship crashed into (e.g., "Earth", "Luna")
reason: <string>  # "collision", "combat", etc.

Client handling: The API gateway forwards ship.destroyed events to all WebSocket clients. The client displays a destruction notification, removes the ship from views, and refreshes the fleet list.

Constants

Constant Value Description
Spawn altitude (Earth) 400 km Safe orbit for respawn
Spawn altitude (Luna) 100 km Safe orbit for respawn
Spawn altitude (Mars) 300 km Safe orbit for respawn
Spawn altitude (default) 200 km Safe orbit for respawn

Future Releases

  • Additional ship classes
  • Ship customization
  • Ship construction/purchase
  • Cargo capacity and cargo gameplay
  • Fuel depots / realistic refueling

Back to top

Galaxy — Kubernetes-based multiplayer space game

This site uses Just the Docs, a documentation theme for Jekyll.