feat(game): Stellaris-style single-ship control with real flight physics
Some checks failed
CI / TypeScript Check (docs) (push) Has been cancelled
CI / TypeScript Check (site) (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Audit (push) Has been cancelled

The player ship was never wired to its own movement systems — it lacked
MaxSpeed/TurnRate (so steer_to_target skipped it) and the Player marker
(so click-to-move skipped it). Only integrate_velocity matched, leaving
the ship drifting on a hardcoded undock vector and ignoring all input.
Left-click was also bound to camera drag, selection, and move at once,
and "Approach" started a cosmetic timer that moved nothing.

New control scheme (single ship, like Stellaris minus fleet selection):
- Right-click issues a move order (was left-click, which conflicted
  with selection + camera drag).
- Left-click cursor-selects the target under the cursor (was
  nearest-to-camera) and deselects on empty space.
- "Approach" now actually flies to the selected target, homing onto it
  via ApproachTarget even as it orbits.
- Smooth acceleration + stopping-distance arrival deceleration +
  turn-rate-limited banking replace instant velocity snapping.
- A pulsing destination waypoint ring gives on-world move-order feedback.
- Undock holds position (zero velocity + at-ship MoveTarget) instead of
  drifting off on a stale vector.
- Follow-cam now allows free-look rotate/zoom so the player can find
  and click targets while the camera tracks the ship.

Steering is shared: the player ship and AI ships use one physics model.
AI spawn bundle gets the new required components (Acceleration,
ArrivalRadius).

Also fixes a B0001 panic in the destination-marker system: the marker
query now carries Without<PlayerShip> so it is provably disjoint from
the player Transform read.

Includes the in-system action-panel rebuild-on-change optimization
(buttons and their Interaction state persist between frames so clicks
register), which the Approach/Dock flow depends on.

Flight tuning lives in in_system/scene.rs (PLAYER_MAX_SPEED,
PLAYER_ACCELERATION, PLAYER_TURN_RATE, PLAYER_ARRIVAL_RADIUS).
This commit is contained in:
2026-06-18 17:57:03 -04:00
parent 2330044ec3
commit 828ebf089a
12 changed files with 513 additions and 129 deletions

View File

@@ -1,5 +1,7 @@
use bevy::prelude::*;
use crate::state::AppState;
pub mod components;
pub mod input;
pub mod kinematic;
@@ -9,18 +11,24 @@ pub struct MovementPlugin;
impl Plugin for MovementPlugin {
fn build(&self, app: &mut App) {
// No state gating for slice 1: systems are no-ops when no Player exists.
// When AppState::InGame is wired up, gate these on `in_state(AppState::InGame)`
// and consider moving them to FixedUpdate for SpacetimeDB determinism.
app.add_systems(
Update,
(
input::click_to_move,
kinematic::steer_to_target,
kinematic::integrate_velocity,
orbit::update_orbits,
)
.chain(),
);
app.add_event::<input::MoveOrderEvent>()
// Orbital motion is scene-wide (used by the galaxy inspection scene
// too), so it runs in every state.
.add_systems(Update, orbit::update_orbits)
// Flight steering chain: order → track approach → steer → integrate.
// Gated to InGame — these only do something while entities with the
// flight-physics components exist. Right-click input additionally
// requires the player to be in flight, not docked.
.add_systems(
Update,
(
input::right_click_to_move.run_if(input::when_in_flight),
kinematic::track_approach_target,
kinematic::steer_to_target,
kinematic::integrate_velocity,
)
.chain()
.run_if(in_state(AppState::InGame)),
);
}
}