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}