Skip to main content

elevator_core/sim/
runtime.rs

1//! Runtime elevator upgrades (speed, accel, decel, capacity, door timings).
2//!
3//! Part of the [`super::Simulation`] API surface; extracted from the
4//! monolithic `sim.rs` for readability. See the parent module for the
5//! overarching essential-API summary.
6
7use crate::components::{Accel, Speed, Weight};
8use crate::entity::{ElevatorId, EntityId};
9use crate::error::SimError;
10use crate::stop::StopRef;
11
12impl super::Simulation {
13    // ── Runtime elevator upgrades ────────────────────────────────────
14    //
15    // Games that want to mutate elevator parameters at runtime (e.g.
16    // an RPG speed-upgrade purchase, a scripted capacity boost) go
17    // through these setters rather than poking `Elevator` directly via
18    // `world_mut()`. Each setter validates its input, updates the
19    // underlying component, and emits an [`Event::ElevatorUpgraded`]
20    // so game code can react without polling.
21    //
22    // ### Semantics
23    //
24    // - `max_speed`, `acceleration`, `deceleration`: applied on the next
25    //   movement integration step. The car's **current velocity is
26    //   preserved** — there is no instantaneous jerk. If `max_speed`
27    //   is lowered below the current velocity, the movement integrator
28    //   clamps velocity to the new cap on the next tick.
29    // - `weight_capacity`: applied immediately. If the new capacity is
30    //   below `current_load` the car ends up temporarily overweight —
31    //   no riders are ejected, but the next boarding pass will reject
32    //   any rider that would push the load further over the new cap.
33    // - `door_transition_ticks`, `door_open_ticks`: applied on the
34    //   **next** door cycle. An in-progress door transition keeps its
35    //   original timing, so setters never cause visual glitches.
36
37    /// Set the maximum travel speed for an elevator at runtime.
38    ///
39    /// The new value applies on the next movement integration step;
40    /// the car's current velocity is preserved (see the
41    /// [runtime upgrades section](crate#runtime-upgrades) of the crate
42    /// docs). If the new cap is below the current velocity, the movement
43    /// system clamps velocity down on the next tick.
44    ///
45    /// # Errors
46    ///
47    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
48    /// - [`SimError::InvalidConfig`] if `speed` is not a positive finite number.
49    ///
50    /// # Example
51    ///
52    /// ```
53    /// use elevator_core::prelude::*;
54    ///
55    /// let mut sim = SimulationBuilder::demo().build().unwrap();
56    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
57    /// sim.set_max_speed(elev, 4.0).unwrap();
58    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().max_speed().value(), 4.0);
59    /// ```
60    pub fn set_max_speed(&mut self, elevator: ElevatorId, speed: f64) -> Result<(), SimError> {
61        let elevator = elevator.entity();
62        Self::validate_positive_finite_f64(speed, "elevators.max_speed")?;
63        let old = self.require_elevator(elevator)?.max_speed.value();
64        let speed = Speed::from(speed);
65        if let Some(car) = self.world.elevator_mut(elevator) {
66            car.max_speed = speed;
67        }
68        self.emit_upgrade(
69            elevator,
70            crate::events::UpgradeField::MaxSpeed,
71            crate::events::UpgradeValue::float(old),
72            crate::events::UpgradeValue::float(speed.value()),
73        );
74        Ok(())
75    }
76
77    /// Set the acceleration rate for an elevator at runtime.
78    ///
79    /// See [`set_max_speed`](Self::set_max_speed) for the general
80    /// velocity-preservation rules that apply to kinematic setters.
81    ///
82    /// # Errors
83    ///
84    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
85    /// - [`SimError::InvalidConfig`] if `accel` is not a positive finite number.
86    ///
87    /// # Example
88    ///
89    /// ```
90    /// use elevator_core::prelude::*;
91    ///
92    /// let mut sim = SimulationBuilder::demo().build().unwrap();
93    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
94    /// sim.set_acceleration(elev, 3.0).unwrap();
95    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().acceleration().value(), 3.0);
96    /// ```
97    pub fn set_acceleration(&mut self, elevator: ElevatorId, accel: f64) -> Result<(), SimError> {
98        let elevator = elevator.entity();
99        Self::validate_positive_finite_f64(accel, "elevators.acceleration")?;
100        let old = self.require_elevator(elevator)?.acceleration.value();
101        let accel = Accel::from(accel);
102        if let Some(car) = self.world.elevator_mut(elevator) {
103            car.acceleration = accel;
104        }
105        self.emit_upgrade(
106            elevator,
107            crate::events::UpgradeField::Acceleration,
108            crate::events::UpgradeValue::float(old),
109            crate::events::UpgradeValue::float(accel.value()),
110        );
111        Ok(())
112    }
113
114    /// Set the deceleration rate for an elevator at runtime.
115    ///
116    /// See [`set_max_speed`](Self::set_max_speed) for the general
117    /// velocity-preservation rules that apply to kinematic setters.
118    ///
119    /// # Errors
120    ///
121    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
122    /// - [`SimError::InvalidConfig`] if `decel` is not a positive finite number.
123    ///
124    /// # Example
125    ///
126    /// ```
127    /// use elevator_core::prelude::*;
128    ///
129    /// let mut sim = SimulationBuilder::demo().build().unwrap();
130    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
131    /// sim.set_deceleration(elev, 3.5).unwrap();
132    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().deceleration().value(), 3.5);
133    /// ```
134    pub fn set_deceleration(&mut self, elevator: ElevatorId, decel: f64) -> Result<(), SimError> {
135        let elevator = elevator.entity();
136        Self::validate_positive_finite_f64(decel, "elevators.deceleration")?;
137        let old = self.require_elevator(elevator)?.deceleration.value();
138        let decel = Accel::from(decel);
139        if let Some(car) = self.world.elevator_mut(elevator) {
140            car.deceleration = decel;
141        }
142        self.emit_upgrade(
143            elevator,
144            crate::events::UpgradeField::Deceleration,
145            crate::events::UpgradeValue::float(old),
146            crate::events::UpgradeValue::float(decel.value()),
147        );
148        Ok(())
149    }
150
151    /// Set the weight capacity for an elevator at runtime.
152    ///
153    /// Applied immediately. If the new capacity is below the car's
154    /// current load the car is temporarily overweight; no riders are
155    /// ejected, but subsequent boarding attempts that would push load
156    /// further over the cap will be rejected as
157    /// [`RejectionReason::OverCapacity`](crate::error::RejectionReason::OverCapacity).
158    ///
159    /// # Errors
160    ///
161    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
162    /// - [`SimError::InvalidConfig`] if `capacity` is not a positive finite number.
163    ///
164    /// # Example
165    ///
166    /// ```
167    /// use elevator_core::prelude::*;
168    ///
169    /// let mut sim = SimulationBuilder::demo().build().unwrap();
170    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
171    /// sim.set_weight_capacity(elev, 1200.0).unwrap();
172    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().weight_capacity().value(), 1200.0);
173    /// ```
174    pub fn set_weight_capacity(
175        &mut self,
176        elevator: ElevatorId,
177        capacity: f64,
178    ) -> Result<(), SimError> {
179        let elevator = elevator.entity();
180        Self::validate_positive_finite_f64(capacity, "elevators.weight_capacity")?;
181        let old = self.require_elevator(elevator)?.weight_capacity.value();
182        let capacity = Weight::from(capacity);
183        if let Some(car) = self.world.elevator_mut(elevator) {
184            car.weight_capacity = capacity;
185        }
186        self.emit_upgrade(
187            elevator,
188            crate::events::UpgradeField::WeightCapacity,
189            crate::events::UpgradeValue::float(old),
190            crate::events::UpgradeValue::float(capacity.value()),
191        );
192        Ok(())
193    }
194
195    /// Set the door open/close transition duration for an elevator.
196    ///
197    /// Applied on the **next** door cycle — an in-progress transition
198    /// keeps its original timing to avoid visual glitches.
199    ///
200    /// # Errors
201    ///
202    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
203    /// - [`SimError::InvalidConfig`] if `ticks` is zero.
204    ///
205    /// # Example
206    ///
207    /// ```
208    /// use elevator_core::prelude::*;
209    ///
210    /// let mut sim = SimulationBuilder::demo().build().unwrap();
211    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
212    /// sim.set_door_transition_ticks(elev, 3).unwrap();
213    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().door_transition_ticks(), 3);
214    /// ```
215    pub fn set_door_transition_ticks(
216        &mut self,
217        elevator: ElevatorId,
218        ticks: u32,
219    ) -> Result<(), SimError> {
220        let elevator = elevator.entity();
221        Self::validate_nonzero_u32(ticks, "elevators.door_transition_ticks")?;
222        let old = self.require_elevator(elevator)?.door_transition_ticks;
223        if let Some(car) = self.world.elevator_mut(elevator) {
224            car.door_transition_ticks = ticks;
225        }
226        self.emit_upgrade(
227            elevator,
228            crate::events::UpgradeField::DoorTransitionTicks,
229            crate::events::UpgradeValue::ticks(old),
230            crate::events::UpgradeValue::ticks(ticks),
231        );
232        Ok(())
233    }
234
235    /// Set how long doors hold fully open for an elevator.
236    ///
237    /// Applied on the **next** door cycle — a door that is currently
238    /// holding open will complete its original dwell before the new
239    /// value takes effect.
240    ///
241    /// # Errors
242    ///
243    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
244    /// - [`SimError::InvalidConfig`] if `ticks` is zero.
245    ///
246    /// # Example
247    ///
248    /// ```
249    /// use elevator_core::prelude::*;
250    ///
251    /// let mut sim = SimulationBuilder::demo().build().unwrap();
252    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
253    /// sim.set_door_open_ticks(elev, 20).unwrap();
254    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().door_open_ticks(), 20);
255    /// ```
256    pub fn set_door_open_ticks(
257        &mut self,
258        elevator: ElevatorId,
259        ticks: u32,
260    ) -> Result<(), SimError> {
261        let elevator = elevator.entity();
262        Self::validate_nonzero_u32(ticks, "elevators.door_open_ticks")?;
263        let old = self.require_elevator(elevator)?.door_open_ticks;
264        if let Some(car) = self.world.elevator_mut(elevator) {
265            car.door_open_ticks = ticks;
266        }
267        self.emit_upgrade(
268            elevator,
269            crate::events::UpgradeField::DoorOpenTicks,
270            crate::events::UpgradeValue::ticks(old),
271            crate::events::UpgradeValue::ticks(ticks),
272        );
273        Ok(())
274    }
275
276    // ── Per-elevator home stop ───────────────────────────────────────
277
278    /// Pin an elevator to a specific home stop. Whenever the car is
279    /// idle and off-position, the reposition phase routes it to
280    /// `home_stop` regardless of the group's reposition strategy. Pass
281    /// any `Into<StopRef>` (e.g. [`StopId`](crate::stop::StopId) or
282    /// [`EntityId`]).
283    ///
284    /// Use [`clear_elevator_home_stop`](Self::clear_elevator_home_stop)
285    /// to remove the pin and let the strategy own the decision again.
286    ///
287    /// # Errors
288    ///
289    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator
290    ///   entity.
291    /// - [`SimError::StopNotFound`] if the resolved stop does not
292    ///   exist in the building.
293    /// - [`SimError::InvalidConfig`] if the resolved stop is not
294    ///   served by the elevator's line — pinning a car to a stop it
295    ///   physically can't reach is almost always a bug, so we surface
296    ///   it loudly.
297    pub fn set_elevator_home_stop(
298        &mut self,
299        elevator: ElevatorId,
300        home_stop: impl Into<StopRef>,
301    ) -> Result<(), SimError> {
302        let elevator = elevator.entity();
303        let home_stop_eid = self.resolve_stop(home_stop.into())?;
304        // Reject pinning to a stop the elevator's line can't serve.
305        let line = self.require_elevator(elevator)?.line;
306        let line_serves = self
307            .groups
308            .iter()
309            .flat_map(|g| g.lines().iter())
310            .find(|li| li.entity() == line)
311            .is_some_and(|li| li.serves().contains(&home_stop_eid));
312        if !line_serves {
313            return Err(SimError::InvalidConfig {
314                field: "home_stop",
315                reason: "home stop is not served by this elevator's line".into(),
316            });
317        }
318        if let Some(car) = self.world.elevator_mut(elevator) {
319            car.home_stop = Some(home_stop_eid);
320        }
321        Ok(())
322    }
323
324    /// Remove the home-stop pin from an elevator. Reposition decisions
325    /// for this car return to the group's reposition strategy.
326    ///
327    /// Idempotent — calling on an unpinned car is a no-op.
328    ///
329    /// # Errors
330    ///
331    /// Returns [`SimError::NotAnElevator`] if `elevator` is not an
332    /// elevator entity.
333    pub fn clear_elevator_home_stop(&mut self, elevator: ElevatorId) -> Result<(), SimError> {
334        let elevator = elevator.entity();
335        self.require_elevator(elevator)?;
336        if let Some(car) = self.world.elevator_mut(elevator) {
337            car.home_stop = None;
338        }
339        Ok(())
340    }
341
342    /// Read the home-stop pin (if any) for an elevator. Returns
343    /// `Ok(None)` when the car has no pin set, `Ok(Some(stop))` when it
344    /// does.
345    ///
346    /// # Errors
347    ///
348    /// Returns [`SimError::NotAnElevator`] if `elevator` is not an
349    /// elevator entity.
350    pub fn elevator_home_stop(&self, elevator: ElevatorId) -> Result<Option<EntityId>, SimError> {
351        let elevator = elevator.entity();
352        Ok(self.require_elevator(elevator)?.home_stop)
353    }
354}