Skip to main content

elevator_core/components/
elevator.rs

1//! Elevator state and configuration component.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5
6use crate::door::{DoorCommand, DoorState};
7use crate::entity::EntityId;
8
9/// Maximum number of manual door commands queued per elevator.
10///
11/// Beyond this cap, the oldest entry is dropped (after adjacent-duplicate
12/// collapsing). Prevents runaway growth if a game submits commands faster
13/// than the sim can apply them.
14pub const DOOR_COMMAND_QUEUE_CAP: usize = 16;
15
16/// Direction an elevator's indicator lamps are signalling.
17///
18/// Derived from the pair of `going_up` / `going_down` flags on [`Elevator`].
19/// `Either` corresponds to both lamps lit — the car is idle and will accept
20/// riders heading either way. `Up` / `Down` correspond to an actively
21/// committed direction.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[non_exhaustive]
24pub enum Direction {
25    /// Car will serve upward trips only.
26    Up,
27    /// Car will serve downward trips only.
28    Down,
29    /// Car will serve either direction (idle).
30    Either,
31}
32
33impl std::fmt::Display for Direction {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            Self::Up => write!(f, "Up"),
37            Self::Down => write!(f, "Down"),
38            Self::Either => write!(f, "Either"),
39        }
40    }
41}
42
43/// Operational phase of an elevator.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[non_exhaustive]
46pub enum ElevatorPhase {
47    /// Parked with no pending requests.
48    Idle,
49    /// Travelling toward a specific stop in response to a dispatch
50    /// assignment (carrying or about to pick up riders).
51    MovingToStop(EntityId),
52    /// Travelling toward a stop for repositioning — no rider service
53    /// obligation, will transition directly to [`Idle`] on arrival
54    /// without opening doors. Distinct from [`MovingToStop`] so that
55    /// downstream code (dispatch, UI, metrics) can treat opportunistic
56    /// moves differently from scheduled trips.
57    ///
58    /// [`MovingToStop`]: Self::MovingToStop
59    /// [`Idle`]: Self::Idle
60    Repositioning(EntityId),
61    /// Doors are currently opening.
62    DoorOpening,
63    /// Doors open; riders may board or exit.
64    Loading,
65    /// Doors are currently closing.
66    DoorClosing,
67    /// Stopped at a floor (doors closed, awaiting dispatch).
68    Stopped,
69}
70
71impl ElevatorPhase {
72    /// Whether the elevator is currently travelling (in either a dispatched
73    /// or a repositioning move).
74    #[must_use]
75    pub const fn is_moving(&self) -> bool {
76        matches!(self, Self::MovingToStop(_) | Self::Repositioning(_))
77    }
78
79    /// The target stop of a moving elevator, if any.
80    ///
81    /// Returns `Some(stop)` for both [`MovingToStop`] and [`Repositioning`]
82    /// variants; `None` otherwise.
83    ///
84    /// [`MovingToStop`]: Self::MovingToStop
85    /// [`Repositioning`]: Self::Repositioning
86    #[must_use]
87    pub const fn moving_target(&self) -> Option<EntityId> {
88        match self {
89            Self::MovingToStop(s) | Self::Repositioning(s) => Some(*s),
90            _ => None,
91        }
92    }
93}
94
95impl std::fmt::Display for ElevatorPhase {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            Self::Idle => write!(f, "Idle"),
99            Self::MovingToStop(id) => write!(f, "MovingToStop({id:?})"),
100            Self::Repositioning(id) => write!(f, "Repositioning({id:?})"),
101            Self::DoorOpening => write!(f, "DoorOpening"),
102            Self::Loading => write!(f, "Loading"),
103            Self::DoorClosing => write!(f, "DoorClosing"),
104            Self::Stopped => write!(f, "Stopped"),
105        }
106    }
107}
108
109/// Component for an elevator entity.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct Elevator {
112    /// Current operational phase.
113    pub(crate) phase: ElevatorPhase,
114    /// Door finite-state machine.
115    pub(crate) door: DoorState,
116    /// Maximum travel speed (distance/tick).
117    pub(crate) max_speed: f64,
118    /// Acceleration rate (distance/tick^2).
119    pub(crate) acceleration: f64,
120    /// Deceleration rate (distance/tick^2).
121    pub(crate) deceleration: f64,
122    /// Maximum weight the car can carry.
123    pub(crate) weight_capacity: f64,
124    /// Total weight of riders currently aboard.
125    pub(crate) current_load: f64,
126    /// Entity IDs of riders currently aboard.
127    pub(crate) riders: Vec<EntityId>,
128    /// Stop entity the car is heading toward, if any.
129    pub(crate) target_stop: Option<EntityId>,
130    /// Ticks for a door open/close transition.
131    pub(crate) door_transition_ticks: u32,
132    /// Ticks the door stays fully open.
133    pub(crate) door_open_ticks: u32,
134    /// Line entity this car belongs to.
135    #[serde(alias = "group")]
136    pub(crate) line: EntityId,
137    /// Whether this elevator is currently repositioning (not serving a dispatch).
138    #[serde(default)]
139    pub(crate) repositioning: bool,
140    /// Stop entity IDs this elevator cannot serve (access restriction).
141    #[serde(default)]
142    pub(crate) restricted_stops: HashSet<EntityId>,
143    /// Speed multiplier for Inspection mode (0.0..1.0).
144    #[serde(default = "default_inspection_speed_factor")]
145    pub(crate) inspection_speed_factor: f64,
146    /// Up-direction indicator lamp: whether this car will serve upward trips.
147    ///
148    /// Auto-managed by the dispatch phase: set true when heading up (or idle),
149    /// false while actively committed to a downward trip. Affects boarding:
150    /// a rider whose next leg goes up will not board a car with `going_up=false`.
151    #[serde(default = "default_true")]
152    pub(crate) going_up: bool,
153    /// Down-direction indicator lamp: whether this car will serve downward trips.
154    ///
155    /// Auto-managed by the dispatch phase: set true when heading down (or idle),
156    /// false while actively committed to an upward trip. Affects boarding:
157    /// a rider whose next leg goes down will not board a car with `going_down=false`.
158    #[serde(default = "default_true")]
159    pub(crate) going_down: bool,
160    /// Count of rounded-floor transitions (passing-floors + arrivals).
161    /// Useful as a scoring axis for efficiency — fewer moves per delivery
162    /// means less wasted travel.
163    #[serde(default)]
164    pub(crate) move_count: u64,
165    /// Pending manual door-control commands. Processed at the start of the
166    /// doors phase; commands that aren't yet valid remain queued.
167    #[serde(default)]
168    pub(crate) door_command_queue: Vec<DoorCommand>,
169    /// Target velocity commanded by the game while in
170    /// [`ServiceMode::Manual`](crate::components::ServiceMode::Manual).
171    ///
172    /// `None` means no command is active — the car coasts to a stop using
173    /// `deceleration`. Read the current target via
174    /// [`Elevator::manual_target_velocity`].
175    #[serde(default)]
176    pub(crate) manual_target_velocity: Option<f64>,
177}
178
179/// Default inspection speed factor (25% of normal speed).
180const fn default_inspection_speed_factor() -> f64 {
181    0.25
182}
183
184/// Default value for direction indicator fields (both lamps on = idle/either direction).
185const fn default_true() -> bool {
186    true
187}
188
189impl Elevator {
190    /// Current operational phase.
191    #[must_use]
192    pub const fn phase(&self) -> ElevatorPhase {
193        self.phase
194    }
195
196    /// Door finite-state machine.
197    #[must_use]
198    pub const fn door(&self) -> &DoorState {
199        &self.door
200    }
201
202    /// Maximum travel speed (distance/tick).
203    #[must_use]
204    pub const fn max_speed(&self) -> f64 {
205        self.max_speed
206    }
207
208    /// Acceleration rate (distance/tick^2).
209    #[must_use]
210    pub const fn acceleration(&self) -> f64 {
211        self.acceleration
212    }
213
214    /// Deceleration rate (distance/tick^2).
215    #[must_use]
216    pub const fn deceleration(&self) -> f64 {
217        self.deceleration
218    }
219
220    /// Maximum weight the car can carry.
221    #[must_use]
222    pub const fn weight_capacity(&self) -> f64 {
223        self.weight_capacity
224    }
225
226    /// Total weight of riders currently aboard.
227    #[must_use]
228    pub const fn current_load(&self) -> f64 {
229        self.current_load
230    }
231
232    /// Entity IDs of riders currently aboard.
233    #[must_use]
234    pub fn riders(&self) -> &[EntityId] {
235        &self.riders
236    }
237
238    /// Stop entity the car is heading toward, if any.
239    #[must_use]
240    pub const fn target_stop(&self) -> Option<EntityId> {
241        self.target_stop
242    }
243
244    /// Ticks for a door open/close transition.
245    #[must_use]
246    pub const fn door_transition_ticks(&self) -> u32 {
247        self.door_transition_ticks
248    }
249
250    /// Ticks the door stays fully open.
251    #[must_use]
252    pub const fn door_open_ticks(&self) -> u32 {
253        self.door_open_ticks
254    }
255
256    /// Line entity this car belongs to.
257    #[must_use]
258    pub const fn line(&self) -> EntityId {
259        self.line
260    }
261
262    /// Whether this elevator is currently repositioning (not serving a dispatch).
263    #[must_use]
264    pub const fn repositioning(&self) -> bool {
265        self.repositioning
266    }
267
268    /// Stop entity IDs this elevator cannot serve (access restriction).
269    #[must_use]
270    pub const fn restricted_stops(&self) -> &HashSet<EntityId> {
271        &self.restricted_stops
272    }
273
274    /// Speed multiplier applied during Inspection mode.
275    #[must_use]
276    pub const fn inspection_speed_factor(&self) -> f64 {
277        self.inspection_speed_factor
278    }
279
280    /// Whether this car's up-direction indicator lamp is lit.
281    ///
282    /// A lit up-lamp signals the car will serve upward-travelling riders.
283    /// Both lamps lit means the car is idle and will accept either direction.
284    #[must_use]
285    pub const fn going_up(&self) -> bool {
286        self.going_up
287    }
288
289    /// Whether this car's down-direction indicator lamp is lit.
290    ///
291    /// A lit down-lamp signals the car will serve downward-travelling riders.
292    /// Both lamps lit means the car is idle and will accept either direction.
293    #[must_use]
294    pub const fn going_down(&self) -> bool {
295        self.going_down
296    }
297
298    /// Direction this car is currently committed to, derived from the pair
299    /// of indicator-lamp flags.
300    ///
301    /// - `Direction::Up` — only `going_up` is set
302    /// - `Direction::Down` — only `going_down` is set
303    /// - `Direction::Either` — both lamps lit (car is idle / accepting
304    ///   either direction), or neither is set (treated as `Either` too,
305    ///   though the dispatch phase normally keeps at least one lit)
306    #[must_use]
307    pub const fn direction(&self) -> Direction {
308        match (self.going_up, self.going_down) {
309            (true, false) => Direction::Up,
310            (false, true) => Direction::Down,
311            _ => Direction::Either,
312        }
313    }
314
315    /// Count of rounded-floor transitions this elevator has made
316    /// (both passing-floor crossings and arrivals).
317    #[must_use]
318    pub const fn move_count(&self) -> u64 {
319        self.move_count
320    }
321
322    /// Pending manual door-control commands for this elevator.
323    ///
324    /// Populated by
325    /// [`Simulation::open_door`](crate::sim::Simulation::open_door)
326    /// and its siblings. Commands are drained at the start of each doors-phase
327    /// tick; any that aren't yet valid remain queued.
328    #[must_use]
329    pub fn door_command_queue(&self) -> &[DoorCommand] {
330        &self.door_command_queue
331    }
332
333    /// Currently commanded target velocity for
334    /// [`ServiceMode::Manual`](crate::components::ServiceMode::Manual).
335    ///
336    /// Returns `None` if no target is set, meaning the car coasts to a
337    /// stop using the configured deceleration. Positive values command
338    /// upward travel, negative values command downward travel.
339    #[must_use]
340    pub const fn manual_target_velocity(&self) -> Option<f64> {
341        self.manual_target_velocity
342    }
343}