Skip to main content

elevator_core/
events.rs

1//! Simulation event bus and typed event channels.
2
3use crate::components::CallDirection;
4use crate::entity::EntityId;
5use crate::error::{RejectionContext, RejectionReason};
6use crate::ids::GroupId;
7use ordered_float::OrderedFloat;
8use serde::{Deserialize, Serialize};
9
10/// Events emitted by the simulation during ticks.
11///
12/// All entity references use `EntityId`. Games can look up additional
13/// component data on the referenced entity if needed.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[non_exhaustive]
16pub enum Event {
17    // -- Elevator events --
18    /// An elevator departed from a stop.
19    ElevatorDeparted {
20        /// The elevator that departed.
21        elevator: EntityId,
22        /// The stop it departed from.
23        from_stop: EntityId,
24        /// The tick when departure occurred.
25        tick: u64,
26    },
27    /// An elevator arrived at a stop.
28    ElevatorArrived {
29        /// The elevator that arrived.
30        elevator: EntityId,
31        /// The stop it arrived at.
32        at_stop: EntityId,
33        /// The tick when arrival occurred.
34        tick: u64,
35    },
36    /// An elevator's doors finished opening.
37    DoorOpened {
38        /// The elevator whose doors opened.
39        elevator: EntityId,
40        /// The tick when the doors opened.
41        tick: u64,
42    },
43    /// An elevator's doors finished closing.
44    DoorClosed {
45        /// The elevator whose doors closed.
46        elevator: EntityId,
47        /// The tick when the doors closed.
48        tick: u64,
49    },
50    /// Emitted when an elevator passes a stop without stopping.
51    /// Games/dispatch can use this to decide whether to add stops mid-travel.
52    PassingFloor {
53        /// The elevator passing by.
54        elevator: EntityId,
55        /// The stop being passed.
56        stop: EntityId,
57        /// Direction: true = moving up, false = moving down.
58        moving_up: bool,
59        /// The tick when the pass occurred.
60        tick: u64,
61    },
62
63    // -- Rider events (unified: passengers, cargo, any rideable entity) --
64    /// A new rider appeared at a stop and wants to travel.
65    RiderSpawned {
66        /// The spawned rider entity.
67        rider: EntityId,
68        /// The stop where the rider appeared.
69        origin: EntityId,
70        /// The stop the rider wants to reach.
71        destination: EntityId,
72        /// The tick when the rider spawned.
73        tick: u64,
74    },
75    /// A rider boarded an elevator.
76    RiderBoarded {
77        /// The rider that boarded.
78        rider: EntityId,
79        /// The elevator the rider boarded.
80        elevator: EntityId,
81        /// The tick when boarding occurred.
82        tick: u64,
83    },
84    /// A rider exited an elevator at a stop.
85    #[serde(alias = "RiderAlighted")]
86    RiderExited {
87        /// The rider that exited.
88        rider: EntityId,
89        /// The elevator the rider exited.
90        elevator: EntityId,
91        /// The stop where the rider exited.
92        stop: EntityId,
93        /// The tick when exiting occurred.
94        tick: u64,
95    },
96    /// A rider was rejected from boarding (e.g., over capacity).
97    RiderRejected {
98        /// The rider that was rejected.
99        rider: EntityId,
100        /// The elevator that rejected the rider.
101        elevator: EntityId,
102        /// The reason for rejection.
103        reason: RejectionReason,
104        /// Additional numeric context for the rejection.
105        context: Option<RejectionContext>,
106        /// The tick when rejection occurred.
107        tick: u64,
108    },
109    /// A rider gave up waiting and left the stop.
110    RiderAbandoned {
111        /// The rider that abandoned.
112        rider: EntityId,
113        /// The stop the rider left.
114        stop: EntityId,
115        /// The tick when abandonment occurred.
116        tick: u64,
117    },
118
119    /// A rider was ejected from an elevator (due to disable or despawn).
120    ///
121    /// The rider is moved to `Waiting` phase at the nearest stop.
122    RiderEjected {
123        /// The rider that was ejected.
124        rider: EntityId,
125        /// The elevator the rider was ejected from.
126        elevator: EntityId,
127        /// The stop the rider was placed at.
128        stop: EntityId,
129        /// The tick when ejection occurred.
130        tick: u64,
131    },
132
133    // -- Dispatch events --
134    /// An elevator was assigned to serve a stop by the dispatcher.
135    ElevatorAssigned {
136        /// The elevator that was assigned.
137        elevator: EntityId,
138        /// The stop it was assigned to serve.
139        stop: EntityId,
140        /// The tick when the assignment occurred.
141        tick: u64,
142    },
143
144    // -- Topology lifecycle events --
145    /// A new stop was added to the simulation.
146    StopAdded {
147        /// The new stop entity.
148        stop: EntityId,
149        /// The line the stop was added to.
150        line: EntityId,
151        /// The group the stop was added to.
152        group: GroupId,
153        /// The tick when the stop was added.
154        tick: u64,
155    },
156    /// A new elevator was added to the simulation.
157    ElevatorAdded {
158        /// The new elevator entity.
159        elevator: EntityId,
160        /// The line the elevator was added to.
161        line: EntityId,
162        /// The group the elevator was added to.
163        group: GroupId,
164        /// The tick when the elevator was added.
165        tick: u64,
166    },
167    /// An entity was disabled.
168    EntityDisabled {
169        /// The entity that was disabled.
170        entity: EntityId,
171        /// The tick when it was disabled.
172        tick: u64,
173    },
174    /// An entity was re-enabled.
175    EntityEnabled {
176        /// The entity that was re-enabled.
177        entity: EntityId,
178        /// The tick when it was enabled.
179        tick: u64,
180    },
181    /// A rider's route was invalidated due to topology change.
182    ///
183    /// Emitted when a stop on a rider's route is disabled or removed.
184    /// If no alternative is found, the rider will abandon after a grace period.
185    RouteInvalidated {
186        /// The affected rider.
187        rider: EntityId,
188        /// The stop that caused the invalidation.
189        affected_stop: EntityId,
190        /// Why the route was invalidated.
191        reason: RouteInvalidReason,
192        /// The tick when invalidation occurred.
193        tick: u64,
194    },
195    /// A rider was manually rerouted via `sim.reroute()` or `sim.reroute_rider()`.
196    RiderRerouted {
197        /// The rerouted rider.
198        rider: EntityId,
199        /// The new destination stop.
200        new_destination: EntityId,
201        /// The tick when rerouting occurred.
202        tick: u64,
203    },
204
205    /// A rider settled at a stop, becoming a resident.
206    RiderSettled {
207        /// The rider that settled.
208        rider: EntityId,
209        /// The stop where the rider settled.
210        stop: EntityId,
211        /// The tick when settlement occurred.
212        tick: u64,
213    },
214    /// A rider was removed from the simulation.
215    RiderDespawned {
216        /// The rider that was removed.
217        rider: EntityId,
218        /// The tick when despawn occurred.
219        tick: u64,
220    },
221
222    // -- Line lifecycle events --
223    /// A line was added to the simulation.
224    LineAdded {
225        /// The new line entity.
226        line: EntityId,
227        /// The group the line was added to.
228        group: GroupId,
229        /// The tick when the line was added.
230        tick: u64,
231    },
232    /// A line was removed from the simulation.
233    LineRemoved {
234        /// The removed line entity.
235        line: EntityId,
236        /// The group the line belonged to.
237        group: GroupId,
238        /// The tick when the line was removed.
239        tick: u64,
240    },
241    /// A line was reassigned to a different group.
242    LineReassigned {
243        /// The line entity that was reassigned.
244        line: EntityId,
245        /// The group the line was previously in.
246        old_group: GroupId,
247        /// The group the line was moved to.
248        new_group: GroupId,
249        /// The tick when reassignment occurred.
250        tick: u64,
251    },
252    /// An elevator was reassigned to a different line.
253    ElevatorReassigned {
254        /// The elevator that was reassigned.
255        elevator: EntityId,
256        /// The line the elevator was previously on.
257        old_line: EntityId,
258        /// The line the elevator was moved to.
259        new_line: EntityId,
260        /// The tick when reassignment occurred.
261        tick: u64,
262    },
263
264    // -- Repositioning events --
265    /// An elevator is being repositioned to improve coverage.
266    ///
267    /// Emitted when an idle elevator begins moving to a new position
268    /// as decided by the [`RepositionStrategy`](crate::dispatch::RepositionStrategy).
269    ElevatorRepositioning {
270        /// The elevator being repositioned.
271        elevator: EntityId,
272        /// The stop it is being sent to.
273        to_stop: EntityId,
274        /// The tick when repositioning began.
275        tick: u64,
276    },
277    /// An elevator completed repositioning and arrived at its target.
278    ///
279    /// Note: this is detected by the movement system — the elevator
280    /// arrives just like any other movement. Games can distinguish
281    /// repositioning arrivals from dispatch arrivals by tracking
282    /// which elevators received `ElevatorRepositioning` events.
283    ElevatorRepositioned {
284        /// The elevator that completed repositioning.
285        elevator: EntityId,
286        /// The stop it arrived at.
287        at_stop: EntityId,
288        /// The tick when it arrived.
289        tick: u64,
290    },
291
292    /// An elevator's service mode was changed.
293    ServiceModeChanged {
294        /// The elevator whose mode changed.
295        elevator: EntityId,
296        /// The previous service mode.
297        from: crate::components::ServiceMode,
298        /// The new service mode.
299        to: crate::components::ServiceMode,
300        /// The tick when the change occurred.
301        tick: u64,
302    },
303
304    // -- Observability events --
305    /// Energy consumed/regenerated by an elevator this tick.
306    ///
307    /// Requires the `energy` feature.
308    #[cfg(feature = "energy")]
309    EnergyConsumed {
310        /// The elevator that consumed energy.
311        elevator: EntityId,
312        /// Energy consumed this tick.
313        consumed: OrderedFloat<f64>,
314        /// Energy regenerated this tick.
315        regenerated: OrderedFloat<f64>,
316        /// The tick when energy was recorded.
317        tick: u64,
318    },
319
320    /// An elevator's load changed (rider boarded or exited).
321    ///
322    /// Emitted immediately after [`RiderBoarded`](Self::RiderBoarded) or
323    /// [`RiderExited`](Self::RiderExited). Useful for real-time capacity
324    /// bar displays in game UIs.
325    ///
326    /// Load values use [`OrderedFloat`] for
327    /// `Eq` compatibility. Dereference to get the inner `f64`:
328    ///
329    /// ```rust,ignore
330    /// use elevator_core::events::Event;
331    ///
332    /// if let Event::CapacityChanged { current_load, capacity, .. } = event {
333    ///     let pct = *current_load / *capacity * 100.0;
334    ///     println!("Elevator at {pct:.0}% capacity");
335    /// }
336    /// ```
337    CapacityChanged {
338        /// The elevator whose load changed.
339        elevator: EntityId,
340        /// Current total weight aboard after the change.
341        current_load: OrderedFloat<f64>,
342        /// Maximum weight capacity of the elevator.
343        capacity: OrderedFloat<f64>,
344        /// The tick when the change occurred.
345        tick: u64,
346    },
347
348    /// An elevator became idle (no more assignments or repositioning).
349    ElevatorIdle {
350        /// The elevator that became idle.
351        elevator: EntityId,
352        /// The stop where it became idle (if at a stop).
353        at_stop: Option<EntityId>,
354        /// The tick when it became idle.
355        tick: u64,
356    },
357
358    /// An elevator's direction indicator lamps changed.
359    ///
360    /// Emitted by the dispatch phase when the pair
361    /// `(going_up, going_down)` transitions to a new value — e.g. when
362    /// a car is assigned a target above it (up-only), below it (down-only),
363    /// or returns to idle (both lamps lit).
364    ///
365    /// Games can use this for UI (lighting up-arrow / down-arrow indicators
366    /// on a car) without polling the elevator each tick.
367    DirectionIndicatorChanged {
368        /// The elevator whose indicator lamps changed.
369        elevator: EntityId,
370        /// New state of the up-direction lamp.
371        going_up: bool,
372        /// New state of the down-direction lamp.
373        going_down: bool,
374        /// The tick when the change occurred.
375        tick: u64,
376    },
377
378    /// An elevator was permanently removed from the simulation.
379    ///
380    /// Distinct from [`Event::EntityDisabled`] — a disabled elevator can be
381    /// re-enabled, but a removed elevator is despawned.
382    ElevatorRemoved {
383        /// The elevator that was removed.
384        elevator: EntityId,
385        /// The line it belonged to.
386        line: EntityId,
387        /// The group it belonged to.
388        group: GroupId,
389        /// The tick when removal occurred.
390        tick: u64,
391    },
392
393    /// A stop was queued as a destination for an elevator.
394    ///
395    /// Emitted by [`Simulation::push_destination`](crate::sim::Simulation::push_destination),
396    /// [`Simulation::push_destination_front`](crate::sim::Simulation::push_destination_front),
397    /// and the built-in dispatch phase whenever it actually appends a stop
398    /// to an elevator's [`DestinationQueue`](crate::components::DestinationQueue).
399    /// Adjacent-duplicate pushes (that are deduplicated) do not emit.
400    DestinationQueued {
401        /// The elevator whose queue was updated.
402        elevator: EntityId,
403        /// The stop that was queued.
404        stop: EntityId,
405        /// The tick when the push occurred.
406        tick: u64,
407    },
408
409    /// A stop was permanently removed from the simulation.
410    ///
411    /// Distinct from [`Event::EntityDisabled`] — a disabled stop can be
412    /// re-enabled, but a removed stop is despawned.
413    StopRemoved {
414        /// The stop that was removed.
415        stop: EntityId,
416        /// The tick when removal occurred.
417        tick: u64,
418    },
419
420    /// A manual door-control command was received and either applied
421    /// immediately or stored for later.
422    ///
423    /// Emitted by
424    /// [`Simulation::request_door_open`](crate::sim::Simulation::request_door_open),
425    /// [`Simulation::request_door_close`](crate::sim::Simulation::request_door_close),
426    /// [`Simulation::hold_door_open`](crate::sim::Simulation::hold_door_open),
427    /// and [`Simulation::cancel_door_hold`](crate::sim::Simulation::cancel_door_hold)
428    /// when the command is accepted. Paired with
429    /// [`Event::DoorCommandApplied`] when the command eventually takes effect.
430    DoorCommandQueued {
431        /// The elevator targeted by the command.
432        elevator: EntityId,
433        /// The command that was queued.
434        command: crate::door::DoorCommand,
435        /// The tick when the command was submitted.
436        tick: u64,
437    },
438    /// A queued door-control command actually took effect — doors began
439    /// opening/closing or a hold was applied.
440    DoorCommandApplied {
441        /// The elevator the command applied to.
442        elevator: EntityId,
443        /// The command that was applied.
444        command: crate::door::DoorCommand,
445        /// The tick when the command was applied.
446        tick: u64,
447    },
448
449    /// An elevator parameter was mutated at runtime via one of the
450    /// `Simulation::set_*` upgrade setters (e.g. buying a speed upgrade
451    /// in an RPG, or a scripted event changing capacity mid-game).
452    ///
453    /// Emitted immediately when the setter succeeds. Games can use this
454    /// to trigger score popups, SFX, or UI updates.
455    ElevatorUpgraded {
456        /// The elevator whose parameter changed.
457        elevator: EntityId,
458        /// Which field was changed.
459        field: UpgradeField,
460        /// Previous value of the field.
461        old: UpgradeValue,
462        /// New value of the field.
463        new: UpgradeValue,
464        /// The tick when the upgrade was applied.
465        tick: u64,
466    },
467
468    /// A velocity command was submitted to an elevator running in
469    /// [`ServiceMode::Manual`](crate::components::ServiceMode::Manual).
470    ///
471    /// Emitted by
472    /// [`Simulation::set_target_velocity`](crate::sim::Simulation::set_target_velocity)
473    /// and [`Simulation::emergency_stop`](crate::sim::Simulation::emergency_stop).
474    ManualVelocityCommanded {
475        /// The elevator targeted by the command.
476        elevator: EntityId,
477        /// The new target velocity (clamped to `[-max_speed, max_speed]`),
478        /// or `None` when the command clears the target.
479        target_velocity: Option<ordered_float::OrderedFloat<f64>>,
480        /// The tick when the command was submitted.
481        tick: u64,
482    },
483
484    // -- Hall & car call events --
485    /// A hall button was pressed. Fires immediately, before any ack
486    /// latency delay. Games use this for button-light animations or SFX.
487    HallButtonPressed {
488        /// Stop where the press occurred.
489        stop: EntityId,
490        /// Direction the button requests service in.
491        direction: CallDirection,
492        /// Tick of the press.
493        tick: u64,
494    },
495    /// A hall call has elapsed its ack latency and is now visible to
496    /// dispatch. Useful for UI confirmations ("your call has been
497    /// received"). In groups with `ack_latency_ticks = 0` this fires on
498    /// the same tick as `HallButtonPressed`.
499    HallCallAcknowledged {
500        /// Stop the call was placed at.
501        stop: EntityId,
502        /// Direction of the call.
503        direction: CallDirection,
504        /// Tick acknowledgement completed.
505        tick: u64,
506    },
507    /// A hall call was cleared when an assigned car arrived at the stop
508    /// with matching direction indicators. Corresponds to the real-world
509    /// button-light turning off.
510    HallCallCleared {
511        /// Stop whose call was cleared.
512        stop: EntityId,
513        /// Direction of the cleared call.
514        direction: CallDirection,
515        /// Car that cleared the call by arriving.
516        car: EntityId,
517        /// Tick the call was cleared.
518        tick: u64,
519    },
520    /// A rider inside a car pressed a floor button (Classic mode only).
521    /// In Destination mode riders reveal destinations at the hall
522    /// kiosk, so no car buttons are pressed.
523    CarButtonPressed {
524        /// Elevator the button was pressed inside.
525        car: EntityId,
526        /// Floor the rider requested.
527        floor: EntityId,
528        /// Rider who pressed the button. `None` when the press is
529        /// synthetic — e.g. issued via
530        /// [`Simulation::press_car_button`](crate::sim::Simulation::press_car_button)
531        /// for scripted events, player input, or cutscene cues with no
532        /// associated rider entity.
533        rider: Option<EntityId>,
534        /// Tick of the press.
535        tick: u64,
536    },
537    /// A rider balked at boarding a car they considered too crowded
538    /// (their preferences filtered the car out). The rider remains
539    /// Waiting and may board a later car.
540    RiderBalked {
541        /// Rider who balked.
542        rider: EntityId,
543        /// Elevator they declined to board.
544        elevator: EntityId,
545        /// Stop where the balking happened.
546        at_stop: EntityId,
547        /// Tick of the balk.
548        tick: u64,
549    },
550}
551
552/// Identifies which elevator parameter was changed in an
553/// [`Event::ElevatorUpgraded`].
554#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
555#[non_exhaustive]
556pub enum UpgradeField {
557    /// Maximum travel speed (distance/tick).
558    MaxSpeed,
559    /// Acceleration rate (distance/tick^2).
560    Acceleration,
561    /// Deceleration rate (distance/tick^2).
562    Deceleration,
563    /// Maximum weight the car can carry.
564    WeightCapacity,
565    /// Ticks for a door open/close transition.
566    DoorTransitionTicks,
567    /// Ticks the door stays fully open.
568    DoorOpenTicks,
569}
570
571impl std::fmt::Display for UpgradeField {
572    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
573        match self {
574            Self::MaxSpeed => write!(f, "max_speed"),
575            Self::Acceleration => write!(f, "acceleration"),
576            Self::Deceleration => write!(f, "deceleration"),
577            Self::WeightCapacity => write!(f, "weight_capacity"),
578            Self::DoorTransitionTicks => write!(f, "door_transition_ticks"),
579            Self::DoorOpenTicks => write!(f, "door_open_ticks"),
580        }
581    }
582}
583
584/// Old-or-new value carried by [`Event::ElevatorUpgraded`].
585///
586/// Uses [`OrderedFloat`] for the float variant so the event enum
587/// remains `Eq`-comparable alongside the other observability events.
588#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
589#[non_exhaustive]
590pub enum UpgradeValue {
591    /// A floating-point parameter value (speed, accel, decel, capacity).
592    Float(OrderedFloat<f64>),
593    /// An integral tick-count parameter value (door timings).
594    Ticks(u32),
595}
596
597impl UpgradeValue {
598    /// Construct a float-valued upgrade payload.
599    #[must_use]
600    pub const fn float(v: f64) -> Self {
601        Self::Float(OrderedFloat(v))
602    }
603
604    /// Construct a tick-valued upgrade payload.
605    #[must_use]
606    pub const fn ticks(v: u32) -> Self {
607        Self::Ticks(v)
608    }
609}
610
611impl std::fmt::Display for UpgradeValue {
612    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
613        match self {
614            Self::Float(v) => write!(f, "{}", **v),
615            Self::Ticks(v) => write!(f, "{v}"),
616        }
617    }
618}
619
620/// Reason a rider's route was invalidated.
621#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
622#[non_exhaustive]
623pub enum RouteInvalidReason {
624    /// A stop on the route was disabled.
625    StopDisabled,
626    /// No alternative stop is available in the same group.
627    NoAlternative,
628}
629
630/// Coarse-grained classification of an [`Event`].
631///
632/// Exposes the same grouping that the `Event` variants are already
633/// commented under, so consumers can filter a drained event stream with
634/// one match arm per category rather than enumerating ~25 variants.
635#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
636#[non_exhaustive]
637pub enum EventCategory {
638    /// Elevator motion, arrival/departure, and door state.
639    Elevator,
640    /// Rider lifecycle: spawn, board, exit, reject, abandon, despawn, settle, reroute.
641    Rider,
642    /// Dispatch decisions and bookkeeping.
643    Dispatch,
644    /// Runtime topology mutations (entities/lines/groups added, removed, reassigned).
645    Topology,
646    /// Idle-elevator repositioning activity.
647    Reposition,
648    /// Direction indicator lamp state changes.
649    Direction,
650    /// Observability and misc signals (capacity, service mode, energy, etc.).
651    Observability,
652}
653
654impl Event {
655    /// Classify this event into a coarse-grained [`EventCategory`].
656    ///
657    /// Useful when a consumer only cares about, say, rider activity and
658    /// wants to skip elevator-motion and topology chatter without
659    /// enumerating every variant. The exhaustive match inside guarantees
660    /// the method stays in sync when new variants are added.
661    #[must_use]
662    pub const fn category(&self) -> EventCategory {
663        match self {
664            Self::ElevatorDeparted { .. }
665            | Self::ElevatorArrived { .. }
666            | Self::DoorOpened { .. }
667            | Self::DoorClosed { .. }
668            | Self::DoorCommandQueued { .. }
669            | Self::DoorCommandApplied { .. }
670            | Self::PassingFloor { .. }
671            | Self::ElevatorIdle { .. } => EventCategory::Elevator,
672            Self::RiderSpawned { .. }
673            | Self::RiderBoarded { .. }
674            | Self::RiderExited { .. }
675            | Self::RiderRejected { .. }
676            | Self::RiderAbandoned { .. }
677            | Self::RiderEjected { .. }
678            | Self::RouteInvalidated { .. }
679            | Self::RiderRerouted { .. }
680            | Self::RiderSettled { .. }
681            | Self::RiderDespawned { .. } => EventCategory::Rider,
682            Self::ElevatorAssigned { .. } | Self::DestinationQueued { .. } => {
683                EventCategory::Dispatch
684            }
685            Self::StopAdded { .. }
686            | Self::StopRemoved { .. }
687            | Self::ElevatorAdded { .. }
688            | Self::ElevatorRemoved { .. }
689            | Self::EntityDisabled { .. }
690            | Self::EntityEnabled { .. }
691            | Self::LineAdded { .. }
692            | Self::LineRemoved { .. }
693            | Self::LineReassigned { .. }
694            | Self::ElevatorReassigned { .. } => EventCategory::Topology,
695            Self::ElevatorRepositioning { .. } | Self::ElevatorRepositioned { .. } => {
696                EventCategory::Reposition
697            }
698            Self::DirectionIndicatorChanged { .. } => EventCategory::Direction,
699            Self::ServiceModeChanged { .. }
700            | Self::CapacityChanged { .. }
701            | Self::ElevatorUpgraded { .. }
702            | Self::ManualVelocityCommanded { .. } => EventCategory::Observability,
703            #[cfg(feature = "energy")]
704            Self::EnergyConsumed { .. } => EventCategory::Observability,
705            Self::HallButtonPressed { .. }
706            | Self::HallCallAcknowledged { .. }
707            | Self::HallCallCleared { .. }
708            | Self::CarButtonPressed { .. } => EventCategory::Dispatch,
709            Self::RiderBalked { .. } => EventCategory::Rider,
710        }
711    }
712}
713
714/// Collects simulation events for consumers to drain.
715#[derive(Debug, Default)]
716pub struct EventBus {
717    /// The pending events not yet consumed.
718    events: Vec<Event>,
719}
720
721impl EventBus {
722    /// Pushes a new event onto the bus.
723    pub fn emit(&mut self, event: Event) {
724        self.events.push(event);
725    }
726
727    /// Returns and clears all pending events.
728    pub fn drain(&mut self) -> Vec<Event> {
729        std::mem::take(&mut self.events)
730    }
731
732    /// Returns a slice of all pending events without clearing them.
733    #[must_use]
734    pub fn peek(&self) -> &[Event] {
735        &self.events
736    }
737}
738
739/// A typed event channel for game-specific events.
740///
741/// Games insert this as a global resource on `World`:
742///
743/// ```
744/// use elevator_core::world::World;
745/// use elevator_core::events::EventChannel;
746///
747/// #[derive(Debug)]
748/// enum MyGameEvent { Foo, Bar }
749///
750/// let mut world = World::new();
751/// world.insert_resource(EventChannel::<MyGameEvent>::new());
752/// // Later:
753/// world.resource_mut::<EventChannel<MyGameEvent>>().unwrap().emit(MyGameEvent::Foo);
754/// ```
755#[derive(Debug)]
756pub struct EventChannel<T> {
757    /// Pending events not yet consumed.
758    events: Vec<T>,
759}
760
761impl<T> EventChannel<T> {
762    /// Create an empty event channel.
763    #[must_use]
764    pub const fn new() -> Self {
765        Self { events: Vec::new() }
766    }
767
768    /// Emit an event into the channel.
769    pub fn emit(&mut self, event: T) {
770        self.events.push(event);
771    }
772
773    /// Drain and return all pending events.
774    pub fn drain(&mut self) -> Vec<T> {
775        std::mem::take(&mut self.events)
776    }
777
778    /// Peek at pending events without clearing.
779    #[must_use]
780    pub fn peek(&self) -> &[T] {
781        &self.events
782    }
783
784    /// Check if the channel has no pending events.
785    #[must_use]
786    pub const fn is_empty(&self) -> bool {
787        self.events.is_empty()
788    }
789
790    /// Number of pending events.
791    #[must_use]
792    pub const fn len(&self) -> usize {
793        self.events.len()
794    }
795}
796
797impl<T> Default for EventChannel<T> {
798    fn default() -> Self {
799        Self::new()
800    }
801}