Skip to main content

elevator_core/components/
rider.rs

1//! Rider (passenger/cargo) core data and lifecycle.
2
3use serde::{Deserialize, Serialize};
4
5use super::units::Weight;
6use crate::entity::EntityId;
7
8/// Lifecycle phase of a rider entity.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[non_exhaustive]
11pub enum RiderPhase {
12    /// Waiting at a stop.
13    Waiting,
14    /// Boarding an elevator (transient, one tick).
15    Boarding(EntityId),
16    /// Riding in an elevator.
17    Riding(EntityId),
18    /// Exiting an elevator (transient, one tick).
19    #[serde(alias = "Alighting")]
20    Exiting(EntityId),
21    /// Walking between transfer stops.
22    Walking,
23    /// Reached final destination.
24    Arrived,
25    /// Gave up waiting.
26    Abandoned,
27    /// Parked at a stop, not seeking an elevator.
28    Resident,
29}
30
31impl RiderPhase {
32    /// True when the rider is currently inside or transitioning through an
33    /// elevator cab — i.e., [`Boarding`](Self::Boarding),
34    /// [`Riding`](Self::Riding), or [`Exiting`](Self::Exiting).
35    ///
36    /// Useful for code that needs to treat all three mid-elevator phases
37    /// uniformly (rendering, per-elevator population counts, skipping
38    /// stop-queue updates) without writing a three-arm `match`.
39    #[must_use]
40    pub const fn is_aboard(&self) -> bool {
41        matches!(self, Self::Boarding(_) | Self::Riding(_) | Self::Exiting(_))
42    }
43
44    /// True when the rider is currently at a stop — i.e., one of the
45    /// phases for which [`Rider::current_stop`] is expected to be `Some`:
46    /// [`Waiting`](Self::Waiting), [`Boarding`](Self::Boarding),
47    /// [`Exiting`](Self::Exiting), [`Arrived`](Self::Arrived),
48    /// [`Abandoned`](Self::Abandoned), or [`Resident`](Self::Resident).
49    ///
50    /// Companion to [`is_aboard`](Self::is_aboard). Note `Boarding` and
51    /// `Exiting` are *both* aboard and at a stop: the rider is mid-transfer
52    /// between a stop and an elevator cab.
53    #[must_use]
54    pub const fn is_at_stop(&self) -> bool {
55        matches!(
56            self,
57            Self::Waiting
58                | Self::Boarding(_)
59                | Self::Exiting(_)
60                | Self::Arrived
61                | Self::Abandoned
62                | Self::Resident
63        )
64    }
65}
66
67/// Data-less companion to [`RiderPhase`] for error messages and pattern matching
68/// without requiring the inner `EntityId`.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70#[non_exhaustive]
71pub enum RiderPhaseKind {
72    /// Waiting at a stop.
73    Waiting,
74    /// Boarding an elevator.
75    Boarding,
76    /// Riding in an elevator.
77    Riding,
78    /// Exiting an elevator.
79    Exiting,
80    /// Walking between transfer stops.
81    Walking,
82    /// Reached final destination.
83    Arrived,
84    /// Gave up waiting.
85    Abandoned,
86    /// Parked at a stop.
87    Resident,
88}
89
90impl RiderPhase {
91    /// Return the data-less kind of this phase.
92    #[must_use]
93    pub const fn kind(&self) -> RiderPhaseKind {
94        match self {
95            Self::Waiting => RiderPhaseKind::Waiting,
96            Self::Boarding(_) => RiderPhaseKind::Boarding,
97            Self::Riding(_) => RiderPhaseKind::Riding,
98            Self::Exiting(_) => RiderPhaseKind::Exiting,
99            Self::Walking => RiderPhaseKind::Walking,
100            Self::Arrived => RiderPhaseKind::Arrived,
101            Self::Abandoned => RiderPhaseKind::Abandoned,
102            Self::Resident => RiderPhaseKind::Resident,
103        }
104    }
105}
106
107impl std::fmt::Display for RiderPhaseKind {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            Self::Waiting => write!(f, "Waiting"),
111            Self::Boarding => write!(f, "Boarding"),
112            Self::Riding => write!(f, "Riding"),
113            Self::Exiting => write!(f, "Exiting"),
114            Self::Walking => write!(f, "Walking"),
115            Self::Arrived => write!(f, "Arrived"),
116            Self::Abandoned => write!(f, "Abandoned"),
117            Self::Resident => write!(f, "Resident"),
118        }
119    }
120}
121
122impl std::fmt::Display for RiderPhase {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        match self {
125            Self::Waiting => write!(f, "Waiting"),
126            Self::Boarding(id) => write!(f, "Boarding({id:?})"),
127            Self::Riding(id) => write!(f, "Riding({id:?})"),
128            Self::Exiting(id) => write!(f, "Exiting({id:?})"),
129            Self::Walking => write!(f, "Walking"),
130            Self::Arrived => write!(f, "Arrived"),
131            Self::Abandoned => write!(f, "Abandoned"),
132            Self::Resident => write!(f, "Resident"),
133        }
134    }
135}
136
137/// Core component for any entity that rides elevators.
138///
139/// This is the minimum data the simulation needs. Games attach
140/// additional components (`VipTag`, `FreightData`, `PersonData`, etc.)
141/// for game-specific behavior. An entity with `Rider` but no
142/// Route component can be boarded/exited manually by game code.
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144pub struct Rider {
145    /// Weight contributed to elevator load.
146    pub(crate) weight: Weight,
147    /// Current rider lifecycle phase.
148    pub(crate) phase: RiderPhase,
149    /// The stop entity this rider is currently at (while Waiting/Arrived/Abandoned/Resident).
150    pub(crate) current_stop: Option<EntityId>,
151    /// Tick when this rider was spawned.
152    pub(crate) spawn_tick: u64,
153    /// Tick when this rider boarded (for ride-time metrics).
154    pub(crate) board_tick: Option<u64>,
155    /// Opaque consumer-attached tag. The engine doesn't interpret
156    /// this value — it survives snapshot round-trip so consumers
157    /// can correlate riders with external identifiers (e.g. a
158    /// game-side sim id, a player id, a freight-shipment id) without
159    /// maintaining a parallel map keyed by `RiderId`. Defaults to 0
160    /// (no tag); `0` is reserved by convention for "untagged."
161    #[serde(default)]
162    pub(crate) tag: u64,
163}
164
165impl Rider {
166    /// Weight contributed to elevator load.
167    #[must_use]
168    pub const fn weight(&self) -> Weight {
169        self.weight
170    }
171
172    /// Current rider lifecycle phase.
173    #[must_use]
174    pub const fn phase(&self) -> RiderPhase {
175        self.phase
176    }
177
178    /// The stop entity this rider is currently at (while Waiting/Arrived/Abandoned/Resident).
179    #[must_use]
180    pub const fn current_stop(&self) -> Option<EntityId> {
181        self.current_stop
182    }
183
184    /// Tick when this rider was spawned.
185    #[must_use]
186    pub const fn spawn_tick(&self) -> u64 {
187        self.spawn_tick
188    }
189
190    /// Tick when this rider boarded (for ride-time metrics).
191    #[must_use]
192    pub const fn board_tick(&self) -> Option<u64> {
193        self.board_tick
194    }
195
196    /// Opaque consumer-attached tag. The engine doesn't interpret this
197    /// value; consumers use it to correlate riders with external
198    /// identifiers (e.g. a game-side sim id, a player id, a freight
199    /// shipment id) without maintaining a parallel `RiderId → u64` map.
200    /// Defaults to `0`, which is reserved by convention for "untagged."
201    #[must_use]
202    pub const fn tag(&self) -> u64 {
203        self.tag
204    }
205}