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}
194
195/// Default inspection speed factor (25% of normal speed).
196const fn default_inspection_speed_factor() -> f64 {
197 0.25
198}
199
200/// Default value for direction indicator fields (both lamps on = idle/either direction).
201const fn default_true() -> bool {
202 true
203}
204
205impl Elevator {
206 /// Current operational phase.
207 #[must_use]
208 pub const fn phase(&self) -> ElevatorPhase {
209 self.phase
210 }
211
212 /// Door finite-state machine.
213 #[must_use]
214 pub const fn door(&self) -> &DoorState {
215 &self.door
216 }
217
218 /// Maximum travel speed (distance/tick).
219 #[must_use]
220 pub const fn max_speed(&self) -> Speed {
221 self.max_speed
222 }
223
224 /// Acceleration rate (distance/tick^2).
225 #[must_use]
226 pub const fn acceleration(&self) -> Accel {
227 self.acceleration
228 }
229
230 /// Deceleration rate (distance/tick^2).
231 #[must_use]
232 pub const fn deceleration(&self) -> Accel {
233 self.deceleration
234 }
235
236 /// Maximum weight the car can carry.
237 #[must_use]
238 pub const fn weight_capacity(&self) -> Weight {
239 self.weight_capacity
240 }
241
242 /// Total weight of riders currently aboard.
243 #[must_use]
244 pub const fn current_load(&self) -> Weight {
245 self.current_load
246 }
247
248 /// Entity IDs of riders currently aboard.
249 #[must_use]
250 pub fn riders(&self) -> &[EntityId] {
251 &self.riders
252 }
253
254 /// Stop entity the car is heading toward, if any.
255 #[must_use]
256 pub const fn target_stop(&self) -> Option<EntityId> {
257 self.target_stop
258 }
259
260 /// Ticks for a door open/close transition.
261 #[must_use]
262 pub const fn door_transition_ticks(&self) -> u32 {
263 self.door_transition_ticks
264 }
265
266 /// Ticks the door stays fully open.
267 #[must_use]
268 pub const fn door_open_ticks(&self) -> u32 {
269 self.door_open_ticks
270 }
271
272 /// Line entity this car belongs to.
273 #[must_use]
274 pub const fn line(&self) -> EntityId {
275 self.line
276 }
277
278 /// Whether this elevator is currently repositioning (not serving a dispatch).
279 #[must_use]
280 pub const fn repositioning(&self) -> bool {
281 self.repositioning
282 }
283
284 /// Stop entity IDs this elevator cannot serve (access restriction).
285 #[must_use]
286 pub const fn restricted_stops(&self) -> &HashSet<EntityId> {
287 &self.restricted_stops
288 }
289
290 /// Speed multiplier applied during Inspection mode.
291 #[must_use]
292 pub const fn inspection_speed_factor(&self) -> f64 {
293 self.inspection_speed_factor
294 }
295
296 /// Load-ratio threshold above which upward-direction pickups are
297 /// skipped (full-load bypass). `None` disables the bypass. See the
298 /// field docs on [`Elevator`] for the underlying model.
299 #[must_use]
300 pub const fn bypass_load_up_pct(&self) -> Option<f64> {
301 self.bypass_load_up_pct
302 }
303
304 /// Load-ratio threshold above which downward-direction pickups are
305 /// skipped. `None` disables the bypass.
306 #[must_use]
307 pub const fn bypass_load_down_pct(&self) -> Option<f64> {
308 self.bypass_load_down_pct
309 }
310
311 /// Mutator for the upward full-load bypass threshold.
312 pub const fn set_bypass_load_up_pct(&mut self, pct: Option<f64>) {
313 self.bypass_load_up_pct = pct;
314 }
315
316 /// Mutator for the downward full-load bypass threshold.
317 pub const fn set_bypass_load_down_pct(&mut self, pct: Option<f64>) {
318 self.bypass_load_down_pct = pct;
319 }
320
321 /// Whether this car's up-direction indicator lamp is lit.
322 ///
323 /// A lit up-lamp signals the car will serve upward-travelling riders.
324 /// Both lamps lit means the car is idle and will accept either direction.
325 #[must_use]
326 pub const fn going_up(&self) -> bool {
327 self.going_up
328 }
329
330 /// Whether this car's down-direction indicator lamp is lit.
331 ///
332 /// A lit down-lamp signals the car will serve downward-travelling riders.
333 /// Both lamps lit means the car is idle and will accept either direction.
334 #[must_use]
335 pub const fn going_down(&self) -> bool {
336 self.going_down
337 }
338
339 /// Direction this car is currently committed to, derived from the pair
340 /// of indicator-lamp flags.
341 ///
342 /// - `Direction::Up` — only `going_up` is set
343 /// - `Direction::Down` — only `going_down` is set
344 /// - `Direction::Either` — both lamps lit (car is idle / accepting
345 /// either direction), or neither is set (treated as `Either` too,
346 /// though the dispatch phase normally keeps at least one lit)
347 #[must_use]
348 pub const fn direction(&self) -> Direction {
349 match (self.going_up, self.going_down) {
350 (true, false) => Direction::Up,
351 (false, true) => Direction::Down,
352 _ => Direction::Either,
353 }
354 }
355
356 /// Count of rounded-floor transitions this elevator has made
357 /// (both passing-floor crossings and arrivals).
358 #[must_use]
359 pub const fn move_count(&self) -> u64 {
360 self.move_count
361 }
362
363 /// Pending manual door-control commands for this elevator.
364 ///
365 /// Populated by
366 /// [`Simulation::open_door`](crate::sim::Simulation::open_door)
367 /// and its siblings. Commands are drained at the start of each doors-phase
368 /// tick; any that aren't yet valid remain queued.
369 #[must_use]
370 pub fn door_command_queue(&self) -> &[DoorCommand] {
371 &self.door_command_queue
372 }
373
374 /// Currently commanded target velocity for
375 /// [`ServiceMode::Manual`](crate::components::ServiceMode::Manual).
376 ///
377 /// Returns `None` if no target is set, meaning the car coasts to a
378 /// stop using the configured deceleration. Positive values command
379 /// upward travel, negative values command downward travel.
380 #[must_use]
381 pub const fn manual_target_velocity(&self) -> Option<f64> {
382 self.manual_target_velocity
383 }
384}