elevator_core/components/patience.rs
1//! Patience and boarding preference components.
2
3use serde::{Deserialize, Serialize};
4
5/// Tracks how long a rider will wait before abandoning.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub struct Patience {
8 /// Maximum ticks the rider will wait before abandoning.
9 pub(crate) max_wait_ticks: u64,
10 /// Ticks waited so far (incremented while in `Waiting` phase).
11 pub(crate) waited_ticks: u64,
12}
13
14impl Patience {
15 /// Maximum ticks the rider will wait before abandoning.
16 #[must_use]
17 pub const fn max_wait_ticks(&self) -> u64 {
18 self.max_wait_ticks
19 }
20
21 /// Ticks waited so far (incremented while in `Waiting` phase).
22 #[must_use]
23 pub const fn waited_ticks(&self) -> u64 {
24 self.waited_ticks
25 }
26}
27
28impl Default for Patience {
29 fn default() -> Self {
30 Self {
31 max_wait_ticks: 600,
32 waited_ticks: 0,
33 }
34 }
35}
36
37/// Boarding preferences for a rider.
38#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
39pub struct Preferences {
40 /// If true, the rider will skip a crowded elevator and wait for the next.
41 pub(crate) skip_full_elevator: bool,
42 /// Maximum load factor (0.0-1.0) the rider will tolerate when boarding.
43 pub(crate) max_crowding_factor: f64,
44 /// Wait budget before the rider abandons. `None` disables time-
45 /// based abandonment; `Some(n)` causes the rider to enter
46 /// [`RiderPhase::Abandoned`](crate::components::RiderPhase) after
47 /// `n` ticks of being [`Waiting`](crate::components::RiderPhase::Waiting).
48 ///
49 /// The counter consulted is [`Patience::waited_ticks`] when a
50 /// [`Patience`] component is attached — that counter only
51 /// increments during `Waiting` and correctly excludes ride time for
52 /// multi-leg routes. Without `Patience`, the budget degrades to
53 /// lifetime ticks since spawn, which matches single-leg behavior.
54 #[serde(alias = "balk_threshold_ticks")]
55 pub(crate) abandon_after_ticks: Option<u32>,
56 /// Abandon on the first full-car skip, rather than silently
57 /// passing and continuing to wait. Default `false`.
58 ///
59 /// This is an **independent** abandonment axis from
60 /// [`abandon_after_ticks`](Self::abandon_after_ticks) — the two
61 /// do not compose or gate each other:
62 ///
63 /// - `abandon_on_full` is *event-triggered* from the loading phase
64 /// (`systems::loading`), firing on a full-car skip.
65 /// - `abandon_after_ticks` is *time-triggered* from the transient
66 /// phase (`systems::advance_transient`), firing when the rider's
67 /// wait budget elapses.
68 ///
69 /// Both paths set [`RiderPhase::Abandoned`](crate::components::RiderPhase);
70 /// whichever condition is reached first wins. Setting
71 /// `abandon_on_full = true` with `abandon_after_ticks = None` is
72 /// valid and abandons on the first full-car skip regardless of
73 /// wait time.
74 pub(crate) abandon_on_full: bool,
75}
76
77impl Preferences {
78 /// If true, the rider will skip a crowded elevator and wait for the next.
79 #[must_use]
80 pub const fn skip_full_elevator(&self) -> bool {
81 self.skip_full_elevator
82 }
83
84 /// Maximum load factor (0.0-1.0) the rider will tolerate when boarding.
85 #[must_use]
86 pub const fn max_crowding_factor(&self) -> f64 {
87 self.max_crowding_factor
88 }
89
90 /// Wait budget before the rider abandons. `None` disables time-
91 /// based abandonment.
92 #[must_use]
93 pub const fn abandon_after_ticks(&self) -> Option<u32> {
94 self.abandon_after_ticks
95 }
96
97 /// Should skipping a full car convert directly to abandonment?
98 #[must_use]
99 pub const fn abandon_on_full(&self) -> bool {
100 self.abandon_on_full
101 }
102
103 /// Builder: set `skip_full_elevator`.
104 #[must_use]
105 pub const fn with_skip_full_elevator(mut self, skip: bool) -> Self {
106 self.skip_full_elevator = skip;
107 self
108 }
109
110 /// Builder: set `max_crowding_factor`.
111 #[must_use]
112 pub const fn with_max_crowding_factor(mut self, factor: f64) -> Self {
113 self.max_crowding_factor = factor;
114 self
115 }
116
117 /// Builder: set `abandon_after_ticks`.
118 #[must_use]
119 pub const fn with_abandon_after_ticks(mut self, ticks: Option<u32>) -> Self {
120 self.abandon_after_ticks = ticks;
121 self
122 }
123
124 /// Builder: set `abandon_on_full`.
125 #[must_use]
126 pub const fn with_abandon_on_full(mut self, abandon: bool) -> Self {
127 self.abandon_on_full = abandon;
128 self
129 }
130}
131
132impl Default for Preferences {
133 fn default() -> Self {
134 Self {
135 skip_full_elevator: false,
136 max_crowding_factor: 0.8,
137 abandon_after_ticks: None,
138 abandon_on_full: false,
139 }
140 }
141}