use smallvec::SmallVec;
use crate::components::{ElevatorPhase, Route};
use crate::entity::EntityId;
use crate::world::World;
use super::{DispatchManifest, DispatchStrategy, ElevatorGroup, RankContext, pair_is_useful};
#[derive(serde::Serialize, serde::Deserialize)]
pub struct EtdDispatch {
pub wait_weight: f64,
pub delay_weight: f64,
pub door_weight: f64,
pub wait_squared_weight: f64,
pub age_linear_weight: f64,
#[serde(skip)]
pending_positions: SmallVec<[f64; 16]>,
}
impl EtdDispatch {
#[must_use]
pub fn new() -> Self {
Self {
wait_weight: 1.0,
delay_weight: 1.0,
door_weight: 0.5,
wait_squared_weight: 0.0,
age_linear_weight: 0.0,
pending_positions: SmallVec::new(),
}
}
#[must_use]
pub fn tuned() -> Self {
Self {
wait_weight: 1.0,
delay_weight: 1.0,
door_weight: 0.5,
wait_squared_weight: 0.0,
age_linear_weight: 0.005,
pending_positions: SmallVec::new(),
}
}
#[must_use]
pub fn with_delay_weight(delay_weight: f64) -> Self {
Self {
wait_weight: 1.0,
delay_weight,
door_weight: 0.5,
wait_squared_weight: 0.0,
age_linear_weight: 0.0,
pending_positions: SmallVec::new(),
}
}
#[must_use]
pub fn with_weights(wait_weight: f64, delay_weight: f64, door_weight: f64) -> Self {
Self {
wait_weight,
delay_weight,
door_weight,
wait_squared_weight: 0.0,
age_linear_weight: 0.0,
pending_positions: SmallVec::new(),
}
}
#[must_use]
pub fn with_wait_squared_weight(mut self, weight: f64) -> Self {
assert!(
weight.is_finite() && weight >= 0.0,
"wait_squared_weight must be finite and non-negative, got {weight}"
);
self.wait_squared_weight = weight;
self
}
#[must_use]
pub fn with_age_linear_weight(mut self, weight: f64) -> Self {
assert!(
weight.is_finite() && weight >= 0.0,
"age_linear_weight must be finite and non-negative, got {weight}"
);
self.age_linear_weight = weight;
self
}
}
impl Default for EtdDispatch {
fn default() -> Self {
Self::tuned()
}
}
impl DispatchStrategy for EtdDispatch {
fn pre_dispatch(
&mut self,
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &mut World,
) {
self.pending_positions.clear();
for &s in group.stop_entities() {
if manifest.has_demand(s)
&& let Some(p) = world.stop_position(s)
{
self.pending_positions.push(p);
}
}
}
fn rank(&mut self, ctx: &RankContext<'_>) -> Option<f64> {
if !pair_is_useful(ctx, false) {
return None;
}
let mut cost = self.compute_cost(ctx.car, ctx.car_position, ctx.stop_position, ctx.world);
if self.wait_squared_weight > 0.0 {
let wait_sq: f64 = ctx
.manifest
.waiting_riders_at(ctx.stop)
.iter()
.map(|r| {
let w = r.wait_ticks as f64;
w * w
})
.sum();
cost = crate::fp::fma(self.wait_squared_weight, -wait_sq, cost).max(0.0);
}
if self.age_linear_weight > 0.0 {
let wait_sum: f64 = ctx
.manifest
.waiting_riders_at(ctx.stop)
.iter()
.map(|r| r.wait_ticks as f64)
.sum();
cost = crate::fp::fma(self.age_linear_weight, -wait_sum, cost).max(0.0);
}
if cost.is_finite() { Some(cost) } else { None }
}
fn builtin_id(&self) -> Option<super::BuiltinStrategy> {
Some(super::BuiltinStrategy::Etd)
}
fn snapshot_config(&self) -> Option<String> {
ron::to_string(self).ok()
}
fn restore_config(&mut self, serialized: &str) -> Result<(), String> {
let restored: Self = ron::from_str(serialized).map_err(|e| e.to_string())?;
*self = restored;
Ok(())
}
}
impl EtdDispatch {
fn compute_cost(
&self,
elev_eid: EntityId,
elev_pos: f64,
target_pos: f64,
world: &World,
) -> f64 {
let Some(car) = world.elevator(elev_eid) else {
return f64::INFINITY;
};
let distance = (elev_pos - target_pos).abs();
let travel_time = if car.max_speed.value() > 0.0 {
distance / car.max_speed.value()
} else {
return f64::INFINITY;
};
let tick_rate = world
.resource::<crate::time::TickRate>()
.map_or(60.0, |r| r.0);
let door_overhead_per_stop =
f64::from(car.door_transition_ticks * 2 + car.door_open_ticks) / tick_rate;
let (lo, hi) = if elev_pos < target_pos {
(elev_pos, target_pos)
} else {
(target_pos, elev_pos)
};
let intervening_stops = self
.pending_positions
.iter()
.filter(|p| **p > lo + 1e-9 && **p < hi - 1e-9)
.count() as f64;
let door_cost = intervening_stops * door_overhead_per_stop;
let mut existing_rider_delay = 0.0_f64;
for &rider_eid in car.riders() {
if let Some(dest) = world.route(rider_eid).and_then(Route::current_destination)
&& let Some(dest_pos) = world.stop_position(dest)
{
let direct_dist = (elev_pos - dest_pos).abs();
let detour_dist = (elev_pos - target_pos).abs() + (target_pos - dest_pos).abs();
let extra = (detour_dist - direct_dist).max(0.0);
if car.max_speed.value() > 0.0 {
existing_rider_delay += extra / car.max_speed.value();
}
}
}
let direction_bonus = match car.phase.moving_target() {
Some(current_target) => world.stop_position(current_target).map_or(0.0, |ctp| {
let moving_up = ctp > elev_pos;
let target_is_ahead = if moving_up {
target_pos > elev_pos && target_pos <= ctp
} else {
target_pos < elev_pos && target_pos >= ctp
};
if target_is_ahead {
-travel_time * 0.5
} else {
0.0
}
}),
None if car.phase == ElevatorPhase::Idle => -travel_time * 0.3,
_ => 0.0,
};
let raw = crate::fp::fma(
self.wait_weight,
travel_time,
crate::fp::fma(
self.delay_weight,
existing_rider_delay,
crate::fp::fma(self.door_weight, door_cost, direction_bonus),
),
);
raw.max(0.0)
}
}