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, RankContext, pair_can_do_work};
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(&mut self, ctx: &RankContext<'_>) -> Option<f64> {
98 if !pair_can_do_work(ctx) {
106 return None;
107 }
108 let cost = self.compute_cost(ctx.car, ctx.car_position, ctx.stop_position, ctx.world);
109 if cost.is_finite() { Some(cost) } else { None }
110 }
111}
112
113impl EtdDispatch {
114 fn compute_cost(
119 &self,
120 elev_eid: EntityId,
121 elev_pos: f64,
122 target_pos: f64,
123 world: &World,
124 ) -> f64 {
125 let Some(car) = world.elevator(elev_eid) else {
126 return f64::INFINITY;
127 };
128
129 let distance = (elev_pos - target_pos).abs();
130 let travel_time = if car.max_speed.value() > 0.0 {
131 distance / car.max_speed.value()
132 } else {
133 return f64::INFINITY;
134 };
135
136 let door_overhead_per_stop = f64::from(car.door_transition_ticks * 2 + car.door_open_ticks);
137
138 let (lo, hi) = if elev_pos < target_pos {
140 (elev_pos, target_pos)
141 } else {
142 (target_pos, elev_pos)
143 };
144 let intervening_stops = self
145 .pending_positions
146 .iter()
147 .filter(|p| **p > lo + 1e-9 && **p < hi - 1e-9)
148 .count() as f64;
149 let door_cost = intervening_stops * door_overhead_per_stop;
150
151 let mut existing_rider_delay = 0.0_f64;
152 for &rider_eid in car.riders() {
153 if let Some(dest) = world.route(rider_eid).and_then(Route::current_destination)
154 && let Some(dest_pos) = world.stop_position(dest)
155 {
156 let direct_dist = (elev_pos - dest_pos).abs();
157 let detour_dist = (elev_pos - target_pos).abs() + (target_pos - dest_pos).abs();
158 let extra = (detour_dist - direct_dist).max(0.0);
159 if car.max_speed.value() > 0.0 {
160 existing_rider_delay += extra / car.max_speed.value();
161 }
162 }
163 }
164
165 let direction_bonus = match car.phase.moving_target() {
170 Some(current_target) => world.stop_position(current_target).map_or(0.0, |ctp| {
171 let moving_up = ctp > elev_pos;
172 let target_is_ahead = if moving_up {
173 target_pos > elev_pos && target_pos <= ctp
174 } else {
175 target_pos < elev_pos && target_pos >= ctp
176 };
177 if target_is_ahead {
178 -travel_time * 0.5
179 } else {
180 0.0
181 }
182 }),
183 None if car.phase == ElevatorPhase::Idle => -travel_time * 0.3,
184 _ => 0.0,
185 };
186
187 let raw = self.wait_weight.mul_add(
188 travel_time,
189 self.delay_weight.mul_add(
190 existing_rider_delay,
191 self.door_weight.mul_add(door_cost, direction_bonus),
192 ),
193 );
194 raw.max(0.0)
195 }
196}