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