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