verticopolis_engine/
sim_loop.rs1use crate::crowd::{motion, spawn, CROWD_SECONDS_PER_MINUTE};
5use crate::economy::films::fills_blockbuster_slot;
6use crate::facilities::Kind;
7use crate::sim::{LogKind, Simulation};
8use crate::tower::UnitState;
9
10pub const CROWD_MAX_STEP: f64 = 60.0;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Weather {
14 Clear,
15 Cloudy,
16 Rain,
17}
18
19pub fn weather_for(day: i64) -> Weather {
21 let mut h = (day as i32 as u32).wrapping_mul(2654435761);
22 h ^= h >> 13;
23 h = h.wrapping_mul(1274126177);
24 let r = ((h >> 8) & 0xffff) as f64 / 65536.0;
25 if r < 0.62 {
26 Weather::Clear
27 } else if r < 0.85 {
28 Weather::Cloudy
29 } else {
30 Weather::Rain
31 }
32}
33
34impl Simulation {
35 pub fn tick(&mut self, dt_minutes: f64) {
54 const EPS: f64 = 1e-6;
55 let mut remaining = dt_minutes;
56 while remaining > EPS {
57 let to_next_hour = 60.0 - (self.clock.minute_of_day() % 60.0);
58 let cap = if to_next_hour > EPS {
59 to_next_hour.min(30.0)
60 } else {
61 30.0
62 };
63 let step = remaining.min(cap);
64 self.advance_step(step);
65 remaining -= step;
66 }
67 }
68
69 #[doc(hidden)]
70 pub fn advance_step(&mut self, dt_minutes: f64) {
73 let paused = self.pause_undo_journal();
74 self.advance_step_inner(dt_minutes);
75 self.resume_undo_journal(paused);
76 }
77
78 fn advance_step_inner(&mut self, dt_minutes: f64) {
79 self.clock.advance(dt_minutes);
80 let rush = self.rush_factor();
81 let population = self.elevators.accumulate(&self.tower, dt_minutes, rush);
82 self.crowd.begin_step();
83 self.crowd.queue_view(&self.tower);
86 self.crowd.blockbusters = self
89 .blockbusters
90 .iter()
91 .filter(|b| b.fract() == 0.0)
92 .map(|b| *b as i64)
93 .collect();
94 spawn::spawn_step(
95 &mut self.crowd,
96 (dt_minutes * CROWD_SECONDS_PER_MINUTE).min(CROWD_MAX_STEP),
97 &mut self.tower,
98 &self.clock,
99 Some(self.weather),
100 population,
101 );
102 let move_minutes = dt_minutes.min(CROWD_MAX_STEP / CROWD_SECONDS_PER_MINUTE);
103 let mut left = move_minutes;
104 while left > 0.0 {
105 let chunk = left.min(2.5);
106 let calls = self.crowd.elevator_calls(&self.tower);
107 let (hour, weekend) = (self.clock.hour(), self.clock.is_weekend());
108 self.elevators
109 .move_cars(&mut self.tower, chunk, &calls, hour, weekend);
110 self.elevators.swap_closed_cars(&mut self.crowd.closed_cars);
111 motion::advance(
112 &mut self.crowd,
113 chunk * CROWD_SECONDS_PER_MINUTE,
114 &mut self.tower,
115 );
116 left -= chunk;
117 }
118 let staff_jobs = self.crowd.take_staff_results();
119 for job in &staff_jobs {
120 self.on_housekeeper_result(job.unit_id, job.ok);
121 }
122 if !staff_jobs.is_empty() {
123 self.dispatch_housekeepers();
124 }
125 self.finish_construction_and_search();
126
127 let hour = self.clock.hour();
128 if hour != self.last_hour {
129 self.last_hour = hour;
130 self.on_hour();
131 }
132 let day = self.clock.day();
133 if day != self.last_day {
134 self.last_day = day;
135 self.on_day();
136 }
137 }
138
139 #[doc(hidden)]
140 pub fn on_hour(&mut self) {
141 self.on_hour_runs += 1;
142 self.update_presence();
143 let hour = self.clock.hour();
144 if hour == 8 {
145 self.hotel_checkout();
146 }
147 if (14..17).contains(&hour) {
148 self.hotel_late_checkout();
149 }
150 let shift = self.mode.housekeeping_shift();
151 if hour >= shift.start && hour < shift.end {
152 self.resolve_extermination();
153 self.dispatch_housekeepers();
154 }
155 self.update_satisfaction();
156 self.attempt_move_ins();
157 self.collect_traffic_income();
158 self.sample_elevator_util();
159 self.evaluate_star();
160 }
161
162 #[doc(hidden)]
163 pub fn on_day(&mut self) {
164 self.weather = weather_for(self.clock.day());
165 let period =
166 (self.clock.day() as f64 / self.clock.calendar.maint_period_days as f64).floor() as i64;
167 if period != self.last_month {
168 self.last_month = period;
169 self.collect_monthly_rent();
170 self.pay_maintenance();
171 self.roll_condo_relocations();
172 }
173 let q = self.clock.quarter();
174 if q != self.last_quarter {
175 self.last_quarter = q;
176 self.last_quarter_money = self.money;
177 self.collect_rent();
178 }
179 self.maybe_random_event();
180 self.maybe_vip_stay();
181 self.check_vip();
182 self.report_move_ins();
183 self.check_milestones();
184 self.nudge_stranded();
185 self.nudge_metro_platform();
186 self.nudge_service_shortfalls();
187 self.roll_over_retail_day();
188 self.rebook_spent_films();
190 self.ledger.end_day();
191 }
192
193 pub(crate) fn finish_construction_and_search(&mut self) {
202 if self.events.bomb.is_some() && self.mode.security_joins_bomb_search() {
203 let now = self.clock.minutes;
204 for at in self.security_openings_by(now) {
205 self.clock.minutes = at;
206 self.advance_bomb_search();
207 self.finish_construction();
208 self.clock.minutes = now;
209 if self.events.bomb.is_none() {
210 break;
211 }
212 }
213 }
214 self.advance_bomb_search();
215 self.finish_construction();
216 }
217
218 fn security_openings_by(&self, now: f64) -> Vec<f64> {
221 let mut at: Vec<f64> = self
222 .constructing
223 .iter()
224 .filter_map(|id| self.tower.get_unit(*id))
225 .filter(|u| u.kind == Kind::Security && u.state == UnitState::Construction)
226 .filter_map(|u| u.extras().complete_at)
227 .filter(|t| *t <= now)
228 .collect();
229 at.sort_by(f64::total_cmp);
230 at
231 }
232
233 #[doc(hidden)]
234 pub fn finish_construction(&mut self) {
235 if self.constructing.is_empty() {
236 return;
237 }
238 let ids: Vec<i64> = self.constructing.iter().copied().collect();
239 for id in ids {
240 let Some(u) = self.tower.get_unit(id) else {
241 self.constructing.shift_remove(&id);
242 continue;
243 };
244 if u.state != UnitState::Construction {
245 self.constructing.shift_remove(&id);
246 continue;
247 }
248 let threshold = match (u.extras().complete_at, &u.extras().complete_at_raw) {
251 (Some(v), _) => v,
252 (None, Some(raw)) => crate::load::js_number(Some(raw)),
253 (None, None) => 0.0,
254 };
255 if self.clock.minutes >= threshold {
256 let (name, floor, kind) = (u.kind.facility().name, u.floor, u.kind);
257 let mode = self.tower.mode;
258 let (u, at) = self.tower.marked_by_id(id).unwrap();
259 u.set_state(at, UnitState::Empty);
260 u.extras_mut().complete_at = None;
261 u.extras_mut().complete_at_raw = None;
262 let full_house = kind == Kind::Cinema && fills_blockbuster_slot(mode, u);
263 self.constructing.shift_remove(&id);
264 let msg = format!(
265 "{} on {} is now open for business.",
266 name,
267 self.floor_label(floor)
268 );
269 self.emit(&msg, LogKind::Good);
270 if kind == Kind::Security {
271 self.join_bomb_search(floor);
272 }
273 if full_house && !self.blockbusters.contains(&(id as f64)) {
278 self.blockbusters.push(id as f64);
279 }
280 }
281 }
282 }
283
284 #[doc(hidden)]
285 pub fn floor_label(&self, floor: i64) -> String {
287 if floor >= 1 {
288 format!("floor {floor}")
289 } else {
290 format!("B{}", 1 - floor)
291 }
292 }
293}