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