Skip to main content

elevator_core/sim/
lifecycle.rs

1//! Rider lifecycle, population queries, and entity state control.
2//!
3//! Covers reroute/settle/despawn/disable/enable, population queries,
4//! per-entity metrics, service mode, and route invalidation. Split out
5//! from `sim.rs` to keep each concern readable.
6
7use std::collections::HashSet;
8
9use crate::components::{Elevator, ElevatorPhase, RiderPhase, RiderPhaseKind, Route};
10use crate::entity::{ElevatorId, EntityId, RiderId};
11use crate::error::SimError;
12use crate::events::Event;
13use crate::ids::GroupId;
14
15use super::Simulation;
16
17impl Simulation {
18    // ── Extension restore ────────────────────────────────────────────
19
20    /// Deserialize extension components from a snapshot.
21    ///
22    /// Call this after restoring from a snapshot and registering all
23    /// extension types via `world.register_ext::<T>(key)`.
24    ///
25    /// Returns the names of any extension types present in the snapshot
26    /// that were not registered. An empty vec means all extensions were
27    /// deserialized successfully.
28    ///
29    /// Prefer [`load_extensions_with`](Self::load_extensions_with) which
30    /// combines registration and loading in one call.
31    #[must_use]
32    pub fn load_extensions(&mut self) -> Vec<String> {
33        let Some(pending) = self
34            .world
35            .remove_resource::<crate::snapshot::PendingExtensions>()
36        else {
37            return Vec::new();
38        };
39        let unregistered = self.world.unregistered_ext_names(pending.0.keys());
40        self.world.deserialize_extensions(&pending.0);
41        unregistered
42    }
43
44    /// Register extension types and load their data from a snapshot
45    /// in one step.
46    ///
47    /// This is the recommended way to restore extensions. It replaces the
48    /// manual 3-step ceremony of `register_ext` → `load_extensions`:
49    ///
50    /// ```ignore
51    /// // Before (3-step ceremony):
52    /// let mut sim = snapshot.restore(None)?;
53    /// sim.world_mut().register_ext::<VipTag>(ExtKey::from_type_name());
54    /// sim.world_mut().register_ext::<TeamId>(ExtKey::from_type_name());
55    /// sim.load_extensions();
56    ///
57    /// // After:
58    /// let mut sim = snapshot.restore(None)?;
59    /// let unregistered = sim.load_extensions_with(|world| {
60    ///     register_extensions!(world, VipTag, TeamId);
61    /// });
62    /// assert!(unregistered.is_empty(), "missing: {unregistered:?}");
63    /// ```
64    ///
65    /// Returns the names of any extension types in the snapshot that were
66    /// not registered. This catches "forgot to register" bugs at load time.
67    #[must_use]
68    pub fn load_extensions_with<F>(&mut self, register: F) -> Vec<String>
69    where
70        F: FnOnce(&mut crate::world::World),
71    {
72        register(&mut self.world);
73        self.load_extensions()
74    }
75
76    // ── Helpers ──────────────────────────────────────────────────────
77
78    /// Extract the `GroupId` from the current leg of a route.
79    ///
80    /// For Walk legs, looks ahead to the next leg to find the group.
81    /// Falls back to `GroupId(0)` when no route exists or no group leg is found.
82    pub(super) fn group_from_route(&self, route: Option<&Route>) -> GroupId {
83        if let Some(route) = route {
84            // Scan forward from current_leg looking for a Group or Line transport mode.
85            for leg in route.legs.iter().skip(route.current_leg) {
86                match leg.via {
87                    crate::components::TransportMode::Group(g) => return g,
88                    crate::components::TransportMode::Line(l) => {
89                        if let Some(line) = self.world.line(l) {
90                            return line.group();
91                        }
92                    }
93                    crate::components::TransportMode::Walk => {}
94                }
95            }
96        }
97        GroupId(0)
98    }
99
100    // ── Re-routing ───────────────────────────────────────────────────
101
102    /// Change a rider's destination mid-route.
103    ///
104    /// Replaces remaining route legs with a single direct leg to `new_destination`,
105    /// keeping the rider's current stop as origin.
106    ///
107    /// Returns `Err` if the rider does not exist or is not in `Waiting` phase
108    /// (riding/boarding riders cannot be rerouted until they exit).
109    ///
110    /// # Errors
111    ///
112    /// Returns [`SimError::EntityNotFound`] if `rider` does not exist.
113    /// Returns [`SimError::WrongRiderPhase`] if the rider is not in
114    /// [`RiderPhase::Waiting`], or [`SimError::RiderHasNoStop`] if the
115    /// rider has no current stop.
116    pub fn reroute(&mut self, rider: RiderId, new_destination: EntityId) -> Result<(), SimError> {
117        let rider = rider.entity();
118        let r = self
119            .world
120            .rider(rider)
121            .ok_or(SimError::EntityNotFound(rider))?;
122
123        if r.phase != RiderPhase::Waiting {
124            return Err(SimError::WrongRiderPhase {
125                rider,
126                expected: RiderPhaseKind::Waiting,
127                actual: r.phase.kind(),
128            });
129        }
130
131        let origin = r.current_stop.ok_or(SimError::RiderHasNoStop(rider))?;
132
133        let group = self.group_from_route(self.world.route(rider));
134        self.world
135            .set_route(rider, Route::direct(origin, new_destination, group));
136
137        self.events.emit(Event::RiderRerouted {
138            rider,
139            new_destination,
140            tick: self.tick,
141        });
142
143        Ok(())
144    }
145
146    /// Replace a rider's entire remaining route.
147    ///
148    /// # Errors
149    ///
150    /// Returns [`SimError::EntityNotFound`] if `rider` does not exist.
151    pub fn set_rider_route(&mut self, rider: EntityId, route: Route) -> Result<(), SimError> {
152        if self.world.rider(rider).is_none() {
153            return Err(SimError::EntityNotFound(rider));
154        }
155        self.world.set_route(rider, route);
156        Ok(())
157    }
158
159    // ── Rider settlement & population ─────────────────────────────
160
161    /// Transition an `Arrived` or `Abandoned` rider to `Resident` at their
162    /// current stop.
163    ///
164    /// Resident riders are parked — invisible to dispatch and loading, but
165    /// queryable via [`residents_at()`](Self::residents_at). They can later
166    /// be given a new route via [`reroute_rider()`](Self::reroute_rider).
167    ///
168    /// # Errors
169    ///
170    /// Returns [`SimError::EntityNotFound`] if `id` does not exist.
171    /// Returns [`SimError::WrongRiderPhase`] if the rider is not in
172    /// `Arrived` or `Abandoned` phase, or [`SimError::RiderHasNoStop`]
173    /// if the rider has no current stop.
174    pub fn settle_rider(&mut self, id: RiderId) -> Result<(), SimError> {
175        let id = id.entity();
176        let rider = self.world.rider(id).ok_or(SimError::EntityNotFound(id))?;
177
178        let old_phase = rider.phase;
179        match old_phase {
180            RiderPhase::Arrived | RiderPhase::Abandoned => {}
181            _ => {
182                return Err(SimError::WrongRiderPhase {
183                    rider: id,
184                    expected: RiderPhaseKind::Arrived,
185                    actual: old_phase.kind(),
186                });
187            }
188        }
189
190        let stop = rider.current_stop.ok_or(SimError::RiderHasNoStop(id))?;
191
192        // Update index: remove from old partition (only Abandoned is indexed).
193        if old_phase == RiderPhase::Abandoned {
194            self.rider_index.remove_abandoned(stop, id);
195        }
196        self.rider_index.insert_resident(stop, id);
197
198        if let Some(r) = self.world.rider_mut(id) {
199            r.phase = RiderPhase::Resident;
200        }
201
202        self.metrics.record_settle();
203        self.events.emit(Event::RiderSettled {
204            rider: id,
205            stop,
206            tick: self.tick,
207        });
208        Ok(())
209    }
210
211    /// Give a `Resident` rider a new route, transitioning them to `Waiting`.
212    ///
213    /// The rider begins waiting at their current stop for an elevator
214    /// matching the route's transport mode. If the rider has a
215    /// [`Patience`](crate::components::Patience) component, its
216    /// `waited_ticks` is reset to zero.
217    ///
218    /// # Errors
219    ///
220    /// Returns [`SimError::EntityNotFound`] if `id` does not exist.
221    /// Returns [`SimError::WrongRiderPhase`] if the rider is not in `Resident`
222    /// phase, [`SimError::EmptyRoute`] if the route has no legs, or
223    /// [`SimError::RouteOriginMismatch`] if the route's first leg origin does
224    /// not match the rider's current stop.
225    pub fn reroute_rider(&mut self, id: EntityId, route: Route) -> Result<(), SimError> {
226        let rider = self.world.rider(id).ok_or(SimError::EntityNotFound(id))?;
227
228        if rider.phase != RiderPhase::Resident {
229            return Err(SimError::WrongRiderPhase {
230                rider: id,
231                expected: RiderPhaseKind::Resident,
232                actual: rider.phase.kind(),
233            });
234        }
235
236        let stop = rider.current_stop.ok_or(SimError::RiderHasNoStop(id))?;
237
238        let new_destination = route.final_destination().ok_or(SimError::EmptyRoute)?;
239
240        // Validate that the route departs from the rider's current stop.
241        if let Some(leg) = route.current()
242            && leg.from != stop
243        {
244            return Err(SimError::RouteOriginMismatch {
245                expected_origin: stop,
246                route_origin: leg.from,
247            });
248        }
249
250        self.rider_index.remove_resident(stop, id);
251        self.rider_index.insert_waiting(stop, id);
252
253        if let Some(r) = self.world.rider_mut(id) {
254            r.phase = RiderPhase::Waiting;
255        }
256        self.world.set_route(id, route);
257
258        // Reset patience if present.
259        if let Some(p) = self.world.patience_mut(id) {
260            p.waited_ticks = 0;
261        }
262
263        self.metrics.record_reroute();
264        self.events.emit(Event::RiderRerouted {
265            rider: id,
266            new_destination,
267            tick: self.tick,
268        });
269        Ok(())
270    }
271
272    /// Remove a rider from the simulation entirely.
273    ///
274    /// Cleans up the population index, metric tags, and elevator cross-references
275    /// (if the rider is currently aboard). Emits [`Event::RiderDespawned`].
276    ///
277    /// All rider removal should go through this method rather than calling
278    /// `world.despawn()` directly, to keep the population index consistent.
279    ///
280    /// # Errors
281    ///
282    /// Returns [`SimError::EntityNotFound`] if `id` does not exist or is
283    /// not a rider.
284    pub fn despawn_rider(&mut self, id: RiderId) -> Result<(), SimError> {
285        let id = id.entity();
286        let rider = self.world.rider(id).ok_or(SimError::EntityNotFound(id))?;
287
288        // Targeted index removal based on current phase (O(1) vs O(n) scan).
289        if let Some(stop) = rider.current_stop {
290            match rider.phase {
291                RiderPhase::Waiting => self.rider_index.remove_waiting(stop, id),
292                RiderPhase::Resident => self.rider_index.remove_resident(stop, id),
293                RiderPhase::Abandoned => self.rider_index.remove_abandoned(stop, id),
294                _ => {} // Boarding/Riding/Exiting/Walking/Arrived — not indexed
295            }
296        }
297
298        if let Some(tags) = self
299            .world
300            .resource_mut::<crate::tagged_metrics::MetricTags>()
301        {
302            tags.remove_entity(id);
303        }
304
305        self.world.despawn(id);
306
307        self.events.emit(Event::RiderDespawned {
308            rider: id,
309            tick: self.tick,
310        });
311        Ok(())
312    }
313
314    // ── Access control ──────────────────────────────────────────────
315
316    /// Set the allowed stops for a rider.
317    ///
318    /// When set, the rider will only be allowed to board elevators that
319    /// can take them to a stop in the allowed set. See
320    /// [`AccessControl`](crate::components::AccessControl) for details.
321    ///
322    /// # Errors
323    ///
324    /// Returns [`SimError::EntityNotFound`] if the rider does not exist.
325    pub fn set_rider_access(
326        &mut self,
327        rider: EntityId,
328        allowed_stops: HashSet<EntityId>,
329    ) -> Result<(), SimError> {
330        if self.world.rider(rider).is_none() {
331            return Err(SimError::EntityNotFound(rider));
332        }
333        self.world
334            .set_access_control(rider, crate::components::AccessControl::new(allowed_stops));
335        Ok(())
336    }
337
338    /// Set the restricted stops for an elevator.
339    ///
340    /// Riders whose current destination is in this set will be rejected
341    /// with [`RejectionReason::AccessDenied`](crate::error::RejectionReason::AccessDenied)
342    /// during the loading phase.
343    ///
344    /// # Errors
345    ///
346    /// Returns [`SimError::EntityNotFound`] if the elevator does not exist.
347    pub fn set_elevator_restricted_stops(
348        &mut self,
349        elevator: EntityId,
350        restricted_stops: HashSet<EntityId>,
351    ) -> Result<(), SimError> {
352        let car = self
353            .world
354            .elevator_mut(elevator)
355            .ok_or(SimError::EntityNotFound(elevator))?;
356        car.restricted_stops = restricted_stops;
357        Ok(())
358    }
359
360    // ── Population queries ──────────────────────────────────────────
361
362    /// Iterate over resident rider IDs at a stop (O(1) lookup).
363    pub fn residents_at(&self, stop: EntityId) -> impl Iterator<Item = EntityId> + '_ {
364        self.rider_index.residents_at(stop).iter().copied()
365    }
366
367    /// Count of residents at a stop (O(1)).
368    #[must_use]
369    pub fn resident_count_at(&self, stop: EntityId) -> usize {
370        self.rider_index.resident_count_at(stop)
371    }
372
373    /// Iterate over waiting rider IDs at a stop (O(1) lookup).
374    pub fn waiting_at(&self, stop: EntityId) -> impl Iterator<Item = EntityId> + '_ {
375        self.rider_index.waiting_at(stop).iter().copied()
376    }
377
378    /// Count of waiting riders at a stop (O(1)).
379    #[must_use]
380    pub fn waiting_count_at(&self, stop: EntityId) -> usize {
381        self.rider_index.waiting_count_at(stop)
382    }
383
384    /// Iterate over abandoned rider IDs at a stop (O(1) lookup).
385    pub fn abandoned_at(&self, stop: EntityId) -> impl Iterator<Item = EntityId> + '_ {
386        self.rider_index.abandoned_at(stop).iter().copied()
387    }
388
389    /// Count of abandoned riders at a stop (O(1)).
390    #[must_use]
391    pub fn abandoned_count_at(&self, stop: EntityId) -> usize {
392        self.rider_index.abandoned_count_at(stop)
393    }
394
395    /// Get the rider entities currently aboard an elevator.
396    ///
397    /// Returns an empty slice if the elevator does not exist.
398    #[must_use]
399    pub fn riders_on(&self, elevator: EntityId) -> &[EntityId] {
400        self.world
401            .elevator(elevator)
402            .map_or(&[], |car| car.riders())
403    }
404
405    /// Get the number of riders aboard an elevator.
406    ///
407    /// Returns 0 if the elevator does not exist.
408    #[must_use]
409    pub fn occupancy(&self, elevator: EntityId) -> usize {
410        self.world
411            .elevator(elevator)
412            .map_or(0, |car| car.riders().len())
413    }
414
415    // ── Entity lifecycle ────────────────────────────────────────────
416
417    /// Disable an entity. Disabled entities are skipped by all systems.
418    ///
419    /// If the entity is an elevator in motion, it is reset to `Idle` with
420    /// zero velocity to prevent stale target references on re-enable.
421    ///
422    /// If the entity is a stop, any `Resident` riders parked there are
423    /// transitioned to `Abandoned` and appropriate events are emitted.
424    ///
425    /// Emits `EntityDisabled`. Returns `Err` if the entity does not exist.
426    ///
427    /// # Errors
428    ///
429    /// Returns [`SimError::EntityNotFound`] if `id` does not refer to a
430    /// living entity.
431    pub fn disable(&mut self, id: EntityId) -> Result<(), SimError> {
432        if !self.world.is_alive(id) {
433            return Err(SimError::EntityNotFound(id));
434        }
435        // If this is an elevator, eject all riders and reset state.
436        if let Some(car) = self.world.elevator(id) {
437            let rider_ids = car.riders.clone();
438            let pos = self.world.position(id).map_or(0.0, |p| p.value);
439            let nearest_stop = self.world.find_nearest_stop(pos);
440
441            for rid in &rider_ids {
442                if let Some(r) = self.world.rider_mut(*rid) {
443                    r.phase = RiderPhase::Waiting;
444                    r.current_stop = nearest_stop;
445                    r.board_tick = None;
446                }
447                if let Some(stop) = nearest_stop {
448                    self.rider_index.insert_waiting(stop, *rid);
449                    self.events.emit(Event::RiderEjected {
450                        rider: *rid,
451                        elevator: id,
452                        stop,
453                        tick: self.tick,
454                    });
455                }
456            }
457
458            let had_load = self
459                .world
460                .elevator(id)
461                .is_some_and(|c| c.current_load.value() > 0.0);
462            let capacity = self.world.elevator(id).map(|c| c.weight_capacity.value());
463            if let Some(car) = self.world.elevator_mut(id) {
464                car.riders.clear();
465                car.current_load = crate::components::Weight::ZERO;
466                car.phase = ElevatorPhase::Idle;
467                car.target_stop = None;
468            }
469            if had_load && let Some(cap) = capacity {
470                self.events.emit(Event::CapacityChanged {
471                    elevator: id,
472                    current_load: ordered_float::OrderedFloat(0.0),
473                    capacity: ordered_float::OrderedFloat(cap),
474                    tick: self.tick,
475                });
476            }
477        }
478        if let Some(vel) = self.world.velocity_mut(id) {
479            vel.value = 0.0;
480        }
481
482        // If this is a stop, abandon resident riders and invalidate routes.
483        if self.world.stop(id).is_some() {
484            let resident_ids: Vec<EntityId> =
485                self.rider_index.residents_at(id).iter().copied().collect();
486            for rid in resident_ids {
487                self.rider_index.remove_resident(id, rid);
488                self.rider_index.insert_abandoned(id, rid);
489                if let Some(r) = self.world.rider_mut(rid) {
490                    r.phase = RiderPhase::Abandoned;
491                }
492                self.events.emit(Event::RiderAbandoned {
493                    rider: rid,
494                    stop: id,
495                    tick: self.tick,
496                });
497            }
498            self.invalidate_routes_for_stop(id);
499        }
500
501        self.world.disable(id);
502        self.events.emit(Event::EntityDisabled {
503            entity: id,
504            tick: self.tick,
505        });
506        Ok(())
507    }
508
509    /// Re-enable a disabled entity.
510    ///
511    /// Emits `EntityEnabled`. Returns `Err` if the entity does not exist.
512    ///
513    /// # Errors
514    ///
515    /// Returns [`SimError::EntityNotFound`] if `id` does not refer to a
516    /// living entity.
517    pub fn enable(&mut self, id: EntityId) -> Result<(), SimError> {
518        if !self.world.is_alive(id) {
519            return Err(SimError::EntityNotFound(id));
520        }
521        self.world.enable(id);
522        self.events.emit(Event::EntityEnabled {
523            entity: id,
524            tick: self.tick,
525        });
526        Ok(())
527    }
528
529    /// Invalidate routes for all riders referencing a disabled stop.
530    ///
531    /// Attempts to reroute riders to the nearest enabled alternative stop.
532    /// If no alternative exists, emits `RouteInvalidated` with `NoAlternative`.
533    fn invalidate_routes_for_stop(&mut self, disabled_stop: EntityId) {
534        use crate::events::RouteInvalidReason;
535
536        // Find the group this stop belongs to.
537        let group_stops: Vec<EntityId> = self
538            .groups
539            .iter()
540            .filter(|g| g.stop_entities().contains(&disabled_stop))
541            .flat_map(|g| g.stop_entities().iter().copied())
542            .filter(|&s| s != disabled_stop && !self.world.is_disabled(s))
543            .collect();
544
545        // Find all Waiting riders whose route references this stop.
546        // Riding riders are skipped — they'll be rerouted when they exit.
547        let rider_ids: Vec<EntityId> = self.world.rider_ids();
548        for rid in rider_ids {
549            let is_waiting = self
550                .world
551                .rider(rid)
552                .is_some_and(|r| r.phase == RiderPhase::Waiting);
553
554            if !is_waiting {
555                continue;
556            }
557
558            let references_stop = self.world.route(rid).is_some_and(|route| {
559                route
560                    .legs
561                    .iter()
562                    .skip(route.current_leg)
563                    .any(|leg| leg.to == disabled_stop || leg.from == disabled_stop)
564            });
565
566            if !references_stop {
567                continue;
568            }
569
570            // Try to find nearest alternative (excluding rider's current stop).
571            let rider_current_stop = self.world.rider(rid).and_then(|r| r.current_stop);
572
573            let disabled_stop_pos = self.world.stop(disabled_stop).map_or(0.0, |s| s.position);
574
575            let alternative = group_stops
576                .iter()
577                .filter(|&&s| Some(s) != rider_current_stop)
578                .filter_map(|&s| {
579                    self.world
580                        .stop(s)
581                        .map(|stop| (s, (stop.position - disabled_stop_pos).abs()))
582                })
583                .min_by(|a, b| a.1.total_cmp(&b.1))
584                .map(|(s, _)| s);
585
586            if let Some(alt_stop) = alternative {
587                // Reroute to nearest alternative.
588                let origin = rider_current_stop.unwrap_or(alt_stop);
589                let group = self.group_from_route(self.world.route(rid));
590                self.world
591                    .set_route(rid, Route::direct(origin, alt_stop, group));
592                self.events.emit(Event::RouteInvalidated {
593                    rider: rid,
594                    affected_stop: disabled_stop,
595                    reason: RouteInvalidReason::StopDisabled,
596                    tick: self.tick,
597                });
598            } else {
599                // No alternative — rider abandons immediately.
600                let abandon_stop = rider_current_stop.unwrap_or(disabled_stop);
601                self.events.emit(Event::RouteInvalidated {
602                    rider: rid,
603                    affected_stop: disabled_stop,
604                    reason: RouteInvalidReason::NoAlternative,
605                    tick: self.tick,
606                });
607                if let Some(r) = self.world.rider_mut(rid) {
608                    r.phase = RiderPhase::Abandoned;
609                }
610                if let Some(stop) = rider_current_stop {
611                    self.rider_index.remove_waiting(stop, rid);
612                    self.rider_index.insert_abandoned(stop, rid);
613                }
614                self.events.emit(Event::RiderAbandoned {
615                    rider: rid,
616                    stop: abandon_stop,
617                    tick: self.tick,
618                });
619            }
620        }
621    }
622
623    /// Check if an entity is disabled.
624    #[must_use]
625    pub fn is_disabled(&self, id: EntityId) -> bool {
626        self.world.is_disabled(id)
627    }
628
629    // ── Entity type queries ─────────────────────────────────────────
630
631    /// Check if an entity is an elevator.
632    ///
633    /// ```
634    /// use elevator_core::prelude::*;
635    ///
636    /// let sim = SimulationBuilder::demo().build().unwrap();
637    /// let stop = sim.stop_entity(StopId(0)).unwrap();
638    /// assert!(!sim.is_elevator(stop));
639    /// assert!(sim.is_stop(stop));
640    /// ```
641    #[must_use]
642    pub fn is_elevator(&self, id: EntityId) -> bool {
643        self.world.elevator(id).is_some()
644    }
645
646    /// Check if an entity is a rider.
647    #[must_use]
648    pub fn is_rider(&self, id: EntityId) -> bool {
649        self.world.rider(id).is_some()
650    }
651
652    /// Check if an entity is a stop.
653    #[must_use]
654    pub fn is_stop(&self, id: EntityId) -> bool {
655        self.world.stop(id).is_some()
656    }
657
658    // ── Aggregate queries ───────────────────────────────────────────
659
660    /// Count of elevators currently in the [`Idle`](ElevatorPhase::Idle) phase.
661    ///
662    /// Excludes disabled elevators (whose phase is reset to `Idle` on disable).
663    ///
664    /// ```
665    /// use elevator_core::prelude::*;
666    ///
667    /// let sim = SimulationBuilder::demo().build().unwrap();
668    /// assert_eq!(sim.idle_elevator_count(), 1);
669    /// ```
670    #[must_use]
671    pub fn idle_elevator_count(&self) -> usize {
672        self.world.iter_idle_elevators().count()
673    }
674
675    /// Current total weight aboard an elevator, or `None` if the entity is
676    /// not an elevator.
677    ///
678    /// ```
679    /// use elevator_core::prelude::*;
680    ///
681    /// let sim = SimulationBuilder::demo().build().unwrap();
682    /// let stop = sim.stop_entity(StopId(0)).unwrap();
683    /// assert_eq!(sim.elevator_load(ElevatorId::from(stop)), None); // not an elevator
684    /// ```
685    #[must_use]
686    pub fn elevator_load(&self, id: ElevatorId) -> Option<f64> {
687        let id = id.entity();
688        self.world.elevator(id).map(|e| e.current_load.value())
689    }
690
691    /// Whether the elevator's up-direction indicator lamp is lit.
692    ///
693    /// Returns `None` if the entity is not an elevator. See
694    /// [`Elevator::going_up`] for semantics.
695    #[must_use]
696    pub fn elevator_going_up(&self, id: EntityId) -> Option<bool> {
697        self.world.elevator(id).map(Elevator::going_up)
698    }
699
700    /// Whether the elevator's down-direction indicator lamp is lit.
701    ///
702    /// Returns `None` if the entity is not an elevator. See
703    /// [`Elevator::going_down`] for semantics.
704    #[must_use]
705    pub fn elevator_going_down(&self, id: EntityId) -> Option<bool> {
706        self.world.elevator(id).map(Elevator::going_down)
707    }
708
709    /// Direction the elevator is currently signalling, derived from the
710    /// indicator-lamp pair. Returns `None` if the entity is not an elevator.
711    #[must_use]
712    pub fn elevator_direction(&self, id: EntityId) -> Option<crate::components::Direction> {
713        self.world.elevator(id).map(Elevator::direction)
714    }
715
716    /// Count of rounded-floor transitions for an elevator (passing-floor
717    /// crossings plus arrivals). Returns `None` if the entity is not an
718    /// elevator.
719    #[must_use]
720    pub fn elevator_move_count(&self, id: EntityId) -> Option<u64> {
721        self.world.elevator(id).map(Elevator::move_count)
722    }
723
724    /// Distance the elevator would travel while braking to a stop from its
725    /// current velocity, at its configured deceleration rate.
726    ///
727    /// Uses the standard `v² / (2·a)` kinematic formula. A stationary
728    /// elevator returns `Some(0.0)`. Returns `None` if the entity is not
729    /// an elevator or lacks a velocity component.
730    ///
731    /// Useful for writing opportunistic dispatch strategies (e.g. "stop at
732    /// this floor if we can brake in time") without duplicating the physics
733    /// computation.
734    #[must_use]
735    pub fn braking_distance(&self, id: EntityId) -> Option<f64> {
736        let car = self.world.elevator(id)?;
737        let vel = self.world.velocity(id)?.value;
738        Some(crate::movement::braking_distance(
739            vel,
740            car.deceleration.value(),
741        ))
742    }
743
744    /// The position where the elevator would come to rest if it began braking
745    /// this instant. Current position plus a signed braking distance in the
746    /// direction of travel.
747    ///
748    /// Returns `None` if the entity is not an elevator or lacks the required
749    /// components.
750    #[must_use]
751    pub fn future_stop_position(&self, id: EntityId) -> Option<f64> {
752        let pos = self.world.position(id)?.value;
753        let vel = self.world.velocity(id)?.value;
754        let car = self.world.elevator(id)?;
755        let dist = crate::movement::braking_distance(vel, car.deceleration.value());
756        Some(vel.signum().mul_add(dist, pos))
757    }
758
759    /// Count of elevators currently in the given phase.
760    ///
761    /// Excludes disabled elevators (whose phase is reset to `Idle` on disable).
762    ///
763    /// ```
764    /// use elevator_core::prelude::*;
765    ///
766    /// let sim = SimulationBuilder::demo().build().unwrap();
767    /// assert_eq!(sim.elevators_in_phase(ElevatorPhase::Idle), 1);
768    /// assert_eq!(sim.elevators_in_phase(ElevatorPhase::Loading), 0);
769    /// ```
770    #[must_use]
771    pub fn elevators_in_phase(&self, phase: ElevatorPhase) -> usize {
772        self.world
773            .iter_elevators()
774            .filter(|(id, _, e)| e.phase() == phase && !self.world.is_disabled(*id))
775            .count()
776    }
777
778    // ── Service mode ────────────────────────────────────────────────
779
780    /// Set the service mode for an elevator.
781    ///
782    /// Emits [`Event::ServiceModeChanged`] if the mode actually changes.
783    ///
784    /// # Errors
785    ///
786    /// Returns [`SimError::EntityNotFound`] if the elevator does not exist.
787    pub fn set_service_mode(
788        &mut self,
789        elevator: EntityId,
790        mode: crate::components::ServiceMode,
791    ) -> Result<(), SimError> {
792        if self.world.elevator(elevator).is_none() {
793            return Err(SimError::EntityNotFound(elevator));
794        }
795        let old = self
796            .world
797            .service_mode(elevator)
798            .copied()
799            .unwrap_or_default();
800        if old == mode {
801            return Ok(());
802        }
803        // Leaving Manual: clear the pending velocity command and zero
804        // the velocity component. Otherwise a car moving at transition
805        // time is stranded — the Normal movement system only runs for
806        // MovingToStop/Repositioning phases, so velocity would linger
807        // forever without producing any position change.
808        if old == crate::components::ServiceMode::Manual {
809            if let Some(car) = self.world.elevator_mut(elevator) {
810                car.manual_target_velocity = None;
811            }
812            if let Some(v) = self.world.velocity_mut(elevator) {
813                v.value = 0.0;
814            }
815        }
816        self.world.set_service_mode(elevator, mode);
817        self.events.emit(Event::ServiceModeChanged {
818            elevator,
819            from: old,
820            to: mode,
821            tick: self.tick,
822        });
823        Ok(())
824    }
825
826    /// Get the current service mode for an elevator.
827    #[must_use]
828    pub fn service_mode(&self, elevator: EntityId) -> crate::components::ServiceMode {
829        self.world
830            .service_mode(elevator)
831            .copied()
832            .unwrap_or_default()
833    }
834}