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_classstring 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
INCRcommand - 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:
- Find reference body via SOI containment (smallest Hill sphere containing ship)
- Compute the target direction vector based on mode (see below)
- 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
- Compute target quaternion to align ship +Z axis with target direction
- Set attitude mode and target
- 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:
ω_nis 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), withsub_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:
- Call
GetShipState(ship_id)on physics service - If response contains valid
ship_idmatching the request, ship exists - If gRPC error or empty response, ship does not exist
- 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):
- Record the ship as destroyed (ship_id, player_id, body_name)
- Filter destroyed ships from the physics batch write (do not write stale state)
- Delete the ship’s Redis hash (
ship:{ship_id}) - Publish
ship.destroyedevent togalaxy:shipsRedis stream - API gateway broadcasts
ship.destroyedto all WebSocket clients - API gateway notifies players service via
NotifyShipDestroyedgRPC call - Players service removes ship from fleet DB and Redis fleet set
- 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