feat(physics): collision resolution + proximity queries (P2)

Implement the kinematic physics & collision layer per ARCH-7 (no physics
engine; distance-based collision + spatial queries).

physics/
- components.rs (new): BodyMass, ProximitySensor, CollisionEvent, ProximityEvent
- geometry.rs: add sphere_overlap() + split_by_inverse_mass() with unit tests
- systems.rs: collision pipeline (detect_collisions -> resolve_collisions)
  with mass-weighted separation and inbound-velocity cancel, plus
  update_proximity_sensors spatial query and a log_physics_events observer.
  Core math factored into pure plan_collisions() with unit tests.
- mod.rs: PhysicsPlugin registers events, FrameCollisions resource, and the
  InGame systems ordered after kinematic integration.

in_system/scene.rs: give the player ship a BoundingVolume collider, BodyMass,
and a ProximitySensor so it participates in collision and spatial queries.

Bodies with a Velocity are movable; scenery is immovable. 19 unit tests pass;
no new compiler/clippy warnings introduced.
This commit is contained in:
2026-06-16 21:30:27 -04:00
parent 407bc4f790
commit b8e8f934d3
5 changed files with 629 additions and 15 deletions

View File

@@ -13,6 +13,7 @@ use crate::gameplay::galaxy::{
SystemContents,
};
use crate::gameplay::in_system::DockedState;
use crate::gameplay::physics::{BodyMass, ProximitySensor};
/// Tracks the currently active system for gameplay.
#[derive(Resource, Debug, Clone, Default)]
@@ -45,6 +46,16 @@ pub struct DockingTarget {
/// Offset from station where player ship spawns when docked.
const DOCKED_OFFSET: Vec3 = Vec3::new(1.5, 0.0, 0.0);
/// World-space collision radius for the player ship. Matches its scaled
/// visual extent (a ~0.12-scale cuboid) so it cannot overlap stations,
/// planets, or the star.
const PLAYER_COLLIDER_RADIUS: f32 = 0.15;
/// World-space radius of the player ship's proximity sensor — the range at
/// which nearby bodies (stations, belts, the star) register as in-range via
/// `ProximityEvent`s.
const PLAYER_SCAN_RADIUS: f32 = 8.0;
/// Represents a docking target (either a station or a habitable planet).
#[derive(Debug, Clone)]
struct DockingTargetInfo {
@@ -352,6 +363,15 @@ fn spawn_player_ship_docked(
Docked {
station_entity: Entity::PLACEHOLDER, // Will be set when we find the actual station
},
// Physical presence: a collider for the collision pipeline and a
// proximity sensor for spatial queries. `BodyMass` weights
// ship-to-ship separation; the ship has no `Velocity` while docked
// so the resolver treats it as immovable until undock.
BoundingVolume {
radius: PLAYER_COLLIDER_RADIUS,
},
BodyMass(1.0),
ProximitySensor::new(PLAYER_SCAN_RADIUS),
Transform::from_translation(ship_position).with_scale(Vec3::splat(0.12)),
Visibility::default(),
InheritedVisibility::default(),