use crate::components::{ElevatorPhase, Route};
use crate::door::DoorState;
use crate::entity::EntityId;
use crate::events::{Event, EventBus};
use crate::metrics::Metrics;
use crate::movement::tick_movement;
use crate::world::{SortedStops, World};
use super::PhaseContext;
use super::dispatch::update_indicators;
fn direction_from_remaining_work(world: &World, eid: EntityId, stop_pos: f64) -> (bool, bool) {
let Some(car) = world.elevator(eid) else {
return (true, true);
};
let mut needs_up = false;
let mut needs_down = false;
for &rid in &car.riders {
if let Some(dest) = world.route(rid).and_then(Route::current_destination)
&& let Some(dest_pos) = world.stop_position(dest)
{
if dest_pos > stop_pos {
needs_up = true;
} else if dest_pos < stop_pos {
needs_down = true;
}
}
}
if let Some(q) = world.destination_queue(eid) {
for &next_stop in q {
if let Some(next_pos) = world.stop_position(next_stop) {
if next_pos > stop_pos {
needs_up = true;
} else if next_pos < stop_pos {
needs_down = true;
}
}
}
}
if !needs_up && !needs_down {
return (true, true);
}
(needs_up, needs_down)
}
#[allow(clippy::too_many_lines)]
pub fn run(
world: &mut World,
events: &mut EventBus,
ctx: &PhaseContext,
elevator_ids: &[crate::entity::EntityId],
metrics: &mut Metrics,
) {
for &eid in elevator_ids {
if world.is_disabled(eid) {
continue;
}
if world
.service_mode(eid)
.is_some_and(|m| *m == crate::components::ServiceMode::Manual)
{
tick_manual(world, events, ctx, eid, metrics);
continue;
}
let target_stop_eid = match world.elevator(eid) {
Some(car) => match car.phase {
ElevatorPhase::MovingToStop(stop_eid) | ElevatorPhase::Repositioning(stop_eid) => {
stop_eid
}
_ => continue,
},
None => continue,
};
let Some(target_pos) = world.stop_position(target_stop_eid) else {
continue;
};
let Some(pos_comp) = world.position(eid) else {
continue;
};
let pos = pos_comp.value;
let Some(vel_comp) = world.velocity(eid) else {
continue;
};
let vel = vel_comp.value;
let is_inspection = world
.service_mode(eid)
.is_some_and(|m| *m == crate::components::ServiceMode::Inspection);
let Some(car) = world.elevator(eid) else {
continue;
};
let max_speed = if is_inspection {
car.max_speed.value() * car.inspection_speed_factor
} else {
car.max_speed.value()
};
let acceleration = car.acceleration.value();
let deceleration = car.deceleration.value();
let door_transition_ticks = car.door_transition_ticks;
let door_open_ticks = car.door_open_ticks;
let is_repositioning = matches!(car.phase, ElevatorPhase::Repositioning(_));
debug_assert_eq!(
is_repositioning, car.repositioning,
"ElevatorPhase::Repositioning and car.repositioning flag diverged at eid={eid:?}"
);
let result = tick_movement(
pos,
vel,
target_pos,
max_speed,
acceleration,
deceleration,
ctx.dt,
);
let old_pos = pos;
let new_pos = result.position;
if let Some(p) = world.position_mut(eid) {
p.value = new_pos;
}
if let Some(v) = world.velocity_mut(eid) {
v.value = result.velocity;
}
if is_repositioning {
let dist = (new_pos - old_pos).abs();
if dist > 0.0 {
metrics.record_reposition_distance(dist);
}
}
let mut passing_moves: u64 = 0;
if !result.arrived {
let moving_up = new_pos > old_pos;
let (lo, hi) = if moving_up {
(old_pos, new_pos)
} else {
(new_pos, old_pos)
};
if let Some(sorted) = world.resource::<SortedStops>() {
let start = sorted.0.partition_point(|&(p, _)| p <= lo + 1e-9);
let end = sorted.0.partition_point(|&(p, _)| p < hi - 1e-9);
for &(_, stop_eid) in &sorted.0[start..end] {
if stop_eid == target_stop_eid {
continue;
}
events.emit(Event::PassingFloor {
elevator: eid,
stop: stop_eid,
moving_up,
tick: ctx.tick,
});
passing_moves += 1;
}
}
}
if passing_moves > 0 {
if let Some(car) = world.elevator_mut(eid) {
car.move_count += passing_moves;
metrics.total_moves += passing_moves;
}
}
if result.arrived {
if let Some(q) = world.destination_queue_mut(eid)
&& q.front() == Some(target_stop_eid)
{
q.pop_front();
}
let mut reposition_arrived = false;
let Some(car) = world.elevator_mut(eid) else {
continue;
};
car.move_count += 1;
metrics.total_moves += 1;
if is_repositioning {
let indicators_dirty = !(car.going_up && car.going_down);
car.phase = ElevatorPhase::Idle;
car.target_stop = None;
car.repositioning = false;
car.going_up = true;
car.going_down = true;
events.emit(Event::ElevatorRepositioned {
elevator: eid,
at_stop: target_stop_eid,
tick: ctx.tick,
});
events.emit(Event::ElevatorIdle {
elevator: eid,
at_stop: Some(target_stop_eid),
tick: ctx.tick,
});
if indicators_dirty {
events.emit(Event::DirectionIndicatorChanged {
elevator: eid,
going_up: true,
going_down: true,
tick: ctx.tick,
});
}
reposition_arrived = true;
} else {
car.phase = ElevatorPhase::DoorOpening;
car.door = DoorState::request_open(door_transition_ticks, door_open_ticks);
events.emit(Event::ElevatorArrived {
elevator: eid,
at_stop: target_stop_eid,
tick: ctx.tick,
});
let (new_up, new_down) = direction_from_remaining_work(world, eid, target_pos);
update_indicators(world, events, eid, new_up, new_down, ctx.tick);
}
if reposition_arrived
&& let Some(cooldowns) =
world.resource_mut::<crate::dispatch::reposition::RepositionCooldowns>()
{
cooldowns.record_arrival(eid, ctx.tick);
}
}
}
}
fn tick_manual(
world: &mut World,
events: &mut EventBus,
ctx: &PhaseContext,
eid: crate::entity::EntityId,
metrics: &mut Metrics,
) {
let Some(car) = world.elevator(eid) else {
return;
};
let target = car.manual_target_velocity.unwrap_or(0.0);
let accel = car.acceleration.value();
let decel = car.deceleration.value();
let max_speed = car.max_speed.value();
let Some(pos_comp) = world.position(eid) else {
return;
};
let old_pos = pos_comp.value;
let Some(vel_comp) = world.velocity(eid) else {
return;
};
let vel = vel_comp.value;
let target = target.clamp(-max_speed, max_speed);
let slowing = target.abs() < vel.abs() || target.signum() * vel.signum() < 0.0;
let rate = if slowing { decel } else { accel };
let dv_max = rate * ctx.dt;
let new_vel = if (target - vel).abs() <= dv_max {
target
} else if target > vel {
vel + dv_max
} else {
vel - dv_max
};
let new_pos = crate::fp::fma(new_vel, ctx.dt, old_pos);
if let Some(p) = world.position_mut(eid) {
p.value = new_pos;
}
if let Some(v) = world.velocity_mut(eid) {
v.value = new_vel;
}
let mut passing_moves: u64 = 0;
let (lo, hi) = if new_pos >= old_pos {
(old_pos, new_pos)
} else {
(new_pos, old_pos)
};
let moving_up = new_pos > old_pos;
if (hi - lo) > 1e-9
&& let Some(sorted) = world.resource::<SortedStops>()
{
let start = sorted.0.partition_point(|&(p, _)| p <= lo + 1e-9);
let end = sorted.0.partition_point(|&(p, _)| p < hi - 1e-9);
for &(_, stop_eid) in &sorted.0[start..end] {
events.emit(Event::PassingFloor {
elevator: eid,
stop: stop_eid,
moving_up,
tick: ctx.tick,
});
passing_moves += 1;
}
}
if passing_moves > 0
&& let Some(car) = world.elevator_mut(eid)
{
car.move_count += passing_moves;
metrics.total_moves += passing_moves;
}
}