elevator_core/dispatch/
etd.rs1use smallvec::SmallVec;
10
11use crate::components::{ElevatorPhase, Route};
12use crate::entity::EntityId;
13use crate::world::World;
14
15use super::{DispatchManifest, DispatchStrategy, ElevatorGroup};
16
17pub struct EtdDispatch {
25 pub wait_weight: f64,
27 pub delay_weight: f64,
29 pub door_weight: f64,
31 pending_positions: SmallVec<[f64; 16]>,
35}
36
37impl EtdDispatch {
38 #[must_use]
42 pub fn new() -> Self {
43 Self {
44 wait_weight: 1.0,
45 delay_weight: 1.0,
46 door_weight: 0.5,
47 pending_positions: SmallVec::new(),
48 }
49 }
50
51 #[must_use]
53 pub fn with_delay_weight(delay_weight: f64) -> Self {
54 Self {
55 wait_weight: 1.0,
56 delay_weight,
57 door_weight: 0.5,
58 pending_positions: SmallVec::new(),
59 }
60 }
61
62 #[must_use]
64 pub fn with_weights(wait_weight: f64, delay_weight: f64, door_weight: f64) -> Self {
65 Self {
66 wait_weight,
67 delay_weight,
68 door_weight,
69 pending_positions: SmallVec::new(),
70 }
71 }
72}
73
74impl Default for EtdDispatch {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80impl DispatchStrategy for EtdDispatch {
81 fn pre_dispatch(
82 &mut self,
83 group: &ElevatorGroup,
84 manifest: &DispatchManifest,
85 world: &mut World,
86 ) {
87 self.pending_positions.clear();
88 for &s in group.stop_entities() {
89 if manifest.has_demand(s)
90 && let Some(p) = world.stop_position(s)
91 {
92 self.pending_positions.push(p);
93 }
94 }
95 }
96
97 fn rank(
98 &mut self,
99 car: EntityId,
100 car_position: f64,
101 _stop: EntityId,
102 stop_position: f64,
103 _group: &ElevatorGroup,
104 _manifest: &DispatchManifest,
105 world: &World,
106 ) -> Option<f64> {
107 let cost = self.compute_cost(car, car_position, stop_position, world);
108 if cost.is_finite() { Some(cost) } else { None }
109 }
110}
111
112impl EtdDispatch {
113 fn compute_cost(
118 &self,
119 elev_eid: EntityId,
120 elev_pos: f64,
121 target_pos: f64,
122 world: &World,
123 ) -> f64 {
124 let Some(car) = world.elevator(elev_eid) else {
125 return f64::INFINITY;
126 };
127
128 let distance = (elev_pos - target_pos).abs();
129 let travel_time = if car.max_speed > 0.0 {
130 distance / car.max_speed
131 } else {
132 return f64::INFINITY;
133 };
134
135 let door_overhead_per_stop = f64::from(car.door_transition_ticks * 2 + car.door_open_ticks);
136
137 let (lo, hi) = if elev_pos < target_pos {
139 (elev_pos, target_pos)
140 } else {
141 (target_pos, elev_pos)
142 };
143 let intervening_stops = self
144 .pending_positions
145 .iter()
146 .filter(|p| **p > lo + 1e-9 && **p < hi - 1e-9)
147 .count() as f64;
148 let door_cost = intervening_stops * door_overhead_per_stop;
149
150 let mut existing_rider_delay = 0.0_f64;
151 for &rider_eid in car.riders() {
152 if let Some(dest) = world.route(rider_eid).and_then(Route::current_destination)
153 && let Some(dest_pos) = world.stop_position(dest)
154 {
155 let direct_dist = (elev_pos - dest_pos).abs();
156 let detour_dist = (elev_pos - target_pos).abs() + (target_pos - dest_pos).abs();
157 let extra = (detour_dist - direct_dist).max(0.0);
158 if car.max_speed > 0.0 {
159 existing_rider_delay += extra / car.max_speed;
160 }
161 }
162 }
163
164 let direction_bonus = match car.phase.moving_target() {
169 Some(current_target) => world.stop_position(current_target).map_or(0.0, |ctp| {
170 let moving_up = ctp > elev_pos;
171 let target_is_ahead = if moving_up {
172 target_pos > elev_pos && target_pos <= ctp
173 } else {
174 target_pos < elev_pos && target_pos >= ctp
175 };
176 if target_is_ahead {
177 -travel_time * 0.5
178 } else {
179 0.0
180 }
181 }),
182 None if car.phase == ElevatorPhase::Idle => -travel_time * 0.3,
183 _ => 0.0,
184 };
185
186 let raw = self.wait_weight.mul_add(
187 travel_time,
188 self.delay_weight.mul_add(
189 existing_rider_delay,
190 self.door_weight.mul_add(door_cost, direction_bonus),
191 ),
192 );
193 raw.max(0.0)
194 }
195}