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, 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, 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 balking-
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 pub(crate) balk_threshold_ticks: Option<u32>,
55 /// Abandon on the first full-car skip, rather than silently
56 /// passing and continuing to wait. Default `false`.
57 ///
58 /// This is an **independent** abandonment axis from
59 /// [`balk_threshold_ticks`](Self::balk_threshold_ticks) — the two
60 /// do not compose or gate each other:
61 ///
62 /// - `abandon_on_full` is *event-triggered* from the loading phase
63 /// (`systems::loading`), firing on a full-car balk.
64 /// - `balk_threshold_ticks` is *time-triggered* from the transient
65 /// phase (`systems::advance_transient`), firing when the rider's
66 /// wait budget elapses.
67 ///
68 /// Both paths set [`RiderPhase::Abandoned`](crate::components::RiderPhase);
69 /// whichever condition is reached first wins. Setting
70 /// `abandon_on_full = true` with `balk_threshold_ticks = None` is
71 /// valid and abandons on the first full-car skip regardless of
72 /// wait time.
73 pub(crate) abandon_on_full: bool,
74}
75
76impl Preferences {
77 /// If true, the rider will skip a crowded elevator and wait for the next.
78 #[must_use]
79 pub const fn skip_full_elevator(&self) -> bool {
80 self.skip_full_elevator
81 }
82
83 /// Maximum load factor (0.0-1.0) the rider will tolerate when boarding.
84 #[must_use]
85 pub const fn max_crowding_factor(&self) -> f64 {
86 self.max_crowding_factor
87 }
88
89 /// Wait budget before the rider abandons. `None` disables balking-
90 /// based abandonment.
91 #[must_use]
92 pub const fn balk_threshold_ticks(&self) -> Option<u32> {
93 self.balk_threshold_ticks
94 }
95
96 /// Should balking a full car convert directly to abandonment?
97 #[must_use]
98 pub const fn abandon_on_full(&self) -> bool {
99 self.abandon_on_full
100 }
101
102 /// Builder: set `balk_threshold_ticks`.
103 #[must_use]
104 pub const fn with_balk_threshold_ticks(mut self, ticks: Option<u32>) -> Self {
105 self.balk_threshold_ticks = ticks;
106 self
107 }
108
109 /// Builder: set `abandon_on_full`.
110 #[must_use]
111 pub const fn with_abandon_on_full(mut self, abandon: bool) -> Self {
112 self.abandon_on_full = abandon;
113 self
114 }
115}
116
117impl Default for Preferences {
118 fn default() -> Self {
119 Self {
120 skip_full_elevator: false,
121 max_crowding_factor: 0.8,
122 balk_threshold_ticks: None,
123 abandon_on_full: false,
124 }
125 }
126}