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