Post-Touchdown Physics
Overview
Extends landing mechanics (#745) with realistic rigid-body dynamics after surface contact. Instead of snapping to upright on touchdown, the ship enters a contact state where bounce, slide, and topple are simulated over multiple ticks. The player retains thrust control throughout.
States
The ship’s surface interaction now uses three states:
| State | landed_body_name |
surface_contact_state |
Description |
|---|---|---|---|
| Flying | "" |
"" |
Normal orbital/atmospheric flight |
| Contact | body name | "contact" |
Ship touching surface, dynamics active |
| Landed | body name | "landed" |
Ship stable on surface, attached |
| Tipped | body name | "tipped" |
Ship fallen over, needs RCS to right |
State Transitions
Flying → Contact Surface contact detected (any speed < crash threshold)
Angular velocity zeroed, attitude aligned to surface normal
Contact → Landed Ship stable: tilt < 5°, angular velocity < 0.01 rad/s,
vertical speed < 0.5 m/s, horizontal speed < 0.5 m/s
Contact → Tipped Ship tilt exceeds 80° (past center of mass over footprint)
Contact → Flying Ship bounces above surface or player thrusts off
Contact → Crash Vertical speed exceeds crash threshold (50 m/s)
Tipped → Contact Player fires RCS torque, ship lifts off tipped state
Landed → Flying Main engine + RCS upward thrust > weight
Crash Thresholds
| Condition | Threshold | Result |
|---|---|---|
| Vertical speed at initial contact | > 50 m/s | Crash (too fast) |
| Re-contact after bounce | > 50 m/s | Crash |
| Tipped ship contacts ground at high speed | > 50 m/s | Crash |
The original 10 m/s landing threshold becomes the bounce threshold: impacts below 10 m/s settle without bouncing, 10–50 m/s bounce.
Contact Physics Model
Coordinate Frame
All contact physics operates in the body’s surface-local frame:
- Radial axis (
r_hat): unit vector from body center through contact point (local “up”) - Tangent plane: perpendicular to radial axis (local “ground”)
- Velocities decomposed into radial (vertical) and tangential (horizontal) components
Ground Reaction Forces
When the ship is at or below the surface, a penalty-based contact force pushes it outward:
# Penetration depth (positive when below surface)
penetration = surface_radius - distance_to_body_center
if penetration > 0:
# Spring-damper normal force (along r_hat)
F_normal = k_spring * penetration - c_damper * v_radial
# Clamp to non-negative (ground can push, not pull)
F_normal = max(0, F_normal)
Spring-damper parameters (per ship class, derived from mass):
# Critical damping for settling without oscillation
k_spring = ship_mass * 100.0 # N/m — stiff enough for 1-tick settling at low speed
c_damper = 2 * sqrt(k_spring * ship_mass) # Critical damping coefficient
Friction
Coulomb friction model with static/kinetic coefficients:
# Tangential velocity (horizontal motion on surface)
v_tangential = v_rel - dot(v_rel, r_hat) * r_hat
v_tan_mag = |v_tangential|
# Friction force opposes tangential motion
if v_tan_mag > 0.01: # kinetic friction
F_friction = -mu_kinetic * F_normal * normalize(v_tangential)
else: # static friction (arrest residual motion)
F_friction = -v_tangential * ship_mass / dt # cancel tangential velocity
# Clamp to static friction limit
if |F_friction| > mu_static * F_normal:
F_friction = -mu_kinetic * F_normal * normalize(v_tangential)
Friction coefficients (all ship classes):
| Coefficient | Value | Notes |
|---|---|---|
mu_static |
0.8 | Regolith-on-metal, prevents sliding on moderate slopes |
mu_kinetic |
0.5 | Sliding friction |
Slope Effects
On a sloped surface, gravity has a tangential component that causes sliding:
# Local gravity vector
g_vec = -G * body_mass / r^2 * r_hat # points toward body center
# Decompose into normal and tangential
g_normal = dot(g_vec, surface_normal) * surface_normal
g_tangential = g_vec - g_normal
# Ship slides if tangential gravity exceeds static friction
# (handled automatically by the friction model above)
The surface normal at the contact point accounts for terrain slope (terrain normal, not just radial direction). For the initial implementation, surface normal equals r_hat (radial direction). Terrain-normal slope effects can be added later when per-vertex normals are available.
Bounce
When a ship contacts the surface with vertical speed between 10 and 50 m/s, the spring-damper produces a bounce. The coefficient of restitution is implicit in the damping ratio:
# With critical damping (zeta = 1.0), bounce is minimal
# For visible bounce, use underdamped system (zeta = 0.3):
c_damper = 2 * zeta * sqrt(k_spring * ship_mass)
Damping ratio: zeta = 0.3 (underdamped — produces 1–2 visible bounces before settling)
Topple Dynamics
Topple uses the ship’s full inertia tensor to compute angular acceleration from gravitational torque:
# Contact point: feet of the ship (center of mass is above by ground_contact_offset)
# When tilted, gravity through CoM creates a torque about the contact point
# CoM offset from contact point in ICRF
com_offset = ship_attitude.rotate(Vec3(0, 0, ground_contact_offset))
# Gravity force at CoM
F_gravity = -local_g * ship_mass * r_hat
# Torque about contact point
tau_gravity = cross(com_offset, F_gravity)
# Angular acceleration using inertia tensor (body frame)
tau_body = ship_attitude.conjugate().rotate(tau_gravity)
I = ship.get_inertia_tensor()
alpha_body = I_inverse @ tau_body
# Convert back to ICRF
alpha_icrf = ship_attitude.rotate(alpha_body)
The ship topples when the center of mass moves outside the support polygon (landing leg footprint). This happens when the tilt angle exceeds arctan(leg_radius / com_height) — approximately 37° for most ships.
Recovery torque: The player can fire RCS rotation to oppose the gravitational torque. The attitude controller operates normally during contact state, so existing RCS torque commands work.
Tipped State
A ship enters tipped state when tilt exceeds 80° during contact. In this state:
- Ship is resting on its side, oriented by the gravitational torque settling
- Position is pinned to the surface (same as landed)
- RCS rotation is enabled — player can fire torque to right the ship
- When RCS torque lifts the ship enough that tilt < 80° AND angular velocity has a righting component, transition back to
contactstate - Main engine thrust still checked for launch (if thrust axis happens to point away from surface)
Integration
Contact physics runs within the existing ship update loop (_update_ship). When surface_contact_state == "contact":
def _update_ship_contact(ship, body, dt):
"""Simulate contact dynamics for one tick."""
# 1. Compute surface position and penetration
r_vec = ship.position - body.position
r = |r_vec|
r_hat = r_vec / r
surface_r = body.radius + terrain_height + ground_contact_offset
penetration = surface_r - r
# 2. Relative velocity (subtract body velocity + rotation)
v_rel = ship.velocity - body.velocity - cross(body_omega, r_vec)
v_radial = dot(v_rel, r_hat)
v_tangential = v_rel - v_radial * r_hat
# 3. Normal force (spring-damper)
F_normal_mag = max(0, k_spring * penetration - c_damper * v_radial)
F_normal = F_normal_mag * r_hat
# 4. Friction force
F_friction = compute_friction(v_tangential, F_normal_mag, ship.mass, dt)
# 5. Gravity
F_gravity = -local_g * ship.mass * r_hat
# 6. Player thrust (if any)
F_thrust = compute_thrust(ship)
# 7. Total force → linear acceleration
F_total = F_normal + F_friction + F_gravity + F_thrust
accel = F_total / ship.mass
ship.velocity += accel * dt
ship.position += ship.velocity * dt
# 8. Gravitational torque about contact point
tau_gravity = compute_gravity_torque(ship, r_hat, local_g)
tau_rcs = compute_rcs_torque(ship) # player input
tau_total = tau_gravity + tau_rcs
# 9. Angular acceleration using inertia tensor
I = ship.get_inertia_tensor()
alpha = I_inverse @ attitude.conjugate().rotate(tau_total)
ship.angular_velocity += attitude.rotate(alpha) * dt
ship.attitude = integrate_quaternion(ship.attitude, ship.angular_velocity, dt)
# 10. State transition checks
check_contact_transitions(ship, penetration, v_radial, tilt_angle)
Fuel Consumption
During contact state:
- Main engine and RCS consume fuel normally when active
- Reaction wheels consume no fuel (same as always)
- Attitude controller is active (responds to hold modes)
Ship State Fields
New field on Ship model:
| Field | Type | Default | Description |
|---|---|---|---|
surface_contact_state |
string | "" |
"" (flying), "contact", "landed", "tipped" |
The existing landed_body_name field is reused — set to the body name for all surface states (contact, landed, tipped), empty when flying.
Events
ship.contact
Published to galaxy:ships Redis stream on initial surface contact.
event: ship.contact
ship_id: <uuid>
player_id: <uuid>
body: <string>
ship.landed
Existing event, now published when transitioning from contact → landed (not on initial touch).
ship.tipped
Published when ship topples past recovery angle.
event: ship.tipped
ship_id: <uuid>
player_id: <uuid>
body: <string>
ship.righted
Published when ship recovers from tipped state.
event: ship.righted
ship_id: <uuid>
player_id: <uuid>
body: <string>
Client Display
Contact State
- Landing HUD shows active descent/velocity data
- Ship mesh is positioned by server state (bouncing/sliding visible)
- Status shows “CONTACT” in yellow
Tipped State
- Status shows “TIPPED” in red
- Landing HUD shows tilt angle prominently
- Hint text: “Use RCS rotation to right the ship”
Landed State
- Same as current (status “LANDED”, HUD shows “Landed”)
Proto Changes
Add surface_contact_state to ShipState protobuf message:
string surface_contact_state = 30; // "", "contact", "landed", "tipped"
Configuration
New settings in services/physics/src/config.py:
| Setting | Default | Description |
|---|---|---|
contact_crash_speed |
50.0 | Max vertical speed before crash (m/s) |
contact_bounce_speed |
10.0 | Vertical speed above which ship bounces (m/s) |
contact_settle_tilt_deg |
5.0 | Max tilt for transition to landed (degrees) |
contact_settle_angular_vel |
0.01 | Max angular velocity for settled (rad/s) |
contact_settle_linear_vel |
0.5 | Max linear velocity for settled (m/s) |
contact_tipped_angle_deg |
80.0 | Tilt angle for tipped state (degrees) |
contact_damping_ratio |
0.3 | Spring-damper damping ratio |
contact_friction_static |
0.8 | Static friction coefficient |
contact_friction_kinetic |
0.5 | Kinetic friction coefficient |
Implementation Files
Physics Service
services/physics/src/simulation.py— Contact physics in_update_ship()services/physics/src/contact.py— New module: contact forces, friction, torqueservices/physics/src/models.py— Addsurface_contact_statefieldservices/physics/src/config.py— Contact parametersservices/physics/src/redis_state.py— Serialize contact stateservices/physics/proto/physics.proto— Add contact state to ShipState
Web Client
services/web-client/src/cockpitLandingHud.js— Contact/tipped displayservices/web-client/src/fleetHud.js— Status text for contact/tipped
Tick Engine
services/tick-engine/src/maneuver_landing.py— Detect contact state for autopilot completion
Dependencies
- #745 Landing mechanics (done)
- #744 Server-side terrain height (done)
Out of Scope
- Leg compression animation: Visual-only effect, deferred
- Terrain-normal slope: Uses radial direction for now; per-vertex terrain normals later
- Multi-contact-point: Single contact point at ship center; multi-leg contact later
- Damage model: Tipped ships survive intact; damage system is a separate feature