1use indexmap::IndexSet;
18use serde_json::Value;
19
20use std::collections::HashMap;
21
22use crate::clock::{resolve_calendar, CalendarKind, Clock, GameMode};
23use crate::crowd::Crowd;
24use crate::dispatch::ElevatorDispatch;
25use crate::events::EventSystem;
26use crate::gameplay::{GameplayEvent, GameplayEvents};
27use crate::housekeeping::Housekeeping;
28use crate::ledger::Ledger;
29use crate::rng::Rng;
30use crate::sim_loop::{weather_for, Weather};
31use crate::telemetry::ElevatorTelemetry;
32use crate::tower::Tower;
33
34#[doc(hidden)]
35#[derive(Clone, Copy, Debug, Default)]
36pub struct MoveIns {
37 pub offices: i64,
38 pub condos: i64,
39 pub rooms: i64,
40 pub fitness: i64,
41 pub clinic: i64,
42 pub rentals: i64,
43}
44
45pub const SAVE_VERSION: i64 = 7;
48pub const STARTING_MONEY: f64 = 2_000_000.0;
50pub const LOG_RING_CAP: usize = 300;
52#[doc(hidden)]
53pub const LOG_SAVE_CAP: usize = LOG_RING_CAP;
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum LogKind {
58 Info,
60 Good,
62 Bad,
64 Money,
66}
67
68impl LogKind {
69 pub fn as_str(self) -> &'static str {
71 match self {
72 LogKind::Info => "info",
73 LogKind::Good => "good",
74 LogKind::Bad => "bad",
75 LogKind::Money => "money",
76 }
77 }
78}
79
80#[derive(Clone, Debug)]
82pub struct LogEntry {
83 pub minute: f64,
85 pub text: String,
87 pub kind: LogKind,
89}
90
91#[doc(hidden)]
92#[derive(Clone, Copy, Debug, Default, PartialEq)]
97pub struct PointFx {
98 pub floor: i64,
99 pub x: f64,
100 pub seq: i64,
101}
102
103#[doc(hidden)]
104#[derive(Clone, Copy, Debug, PartialEq)]
105pub struct ThiefFx {
106 pub caught: bool,
107 pub floor: i64,
108 pub seq: i64,
109}
110
111#[doc(hidden)]
112#[derive(Clone, Copy, Debug, PartialEq)]
113pub struct FxState {
114 pub santa_seq: i64,
115 pub explosion: PointFx,
116 pub thief: ThiefFx,
117 pub treasure: PointFx,
118 pub vip_seq: i64,
119}
120
121impl Default for FxState {
122 fn default() -> FxState {
123 FxState {
124 santa_seq: 0,
125 explosion: PointFx::default(),
126 thief: ThiefFx {
127 caught: false,
128 floor: 1,
129 seq: 0,
130 },
131 treasure: PointFx::default(),
132 vip_seq: 0,
133 }
134 }
135}
136
137pub struct Simulation {
150 #[doc(hidden)]
151 pub rng: Rng,
152 pub mode: GameMode,
154 pub modern_calendar: CalendarKind,
157 pub auto_bridge: bool,
160 pub founder: bool,
163 pub clock: Clock,
165 pub crowd: Crowd,
167 pub events: EventSystem,
169 #[doc(hidden)]
170 pub ledger: Ledger,
171 pub tower: Tower,
174 pub money: f64,
176 pub star: i64,
178 #[doc(hidden)]
179 pub last_quarter_money: f64,
180 #[doc(hidden)]
181 pub evaluated_tower: Option<bool>,
183 #[doc(hidden)]
184 pub vip_visit_day: f64,
187 #[doc(hidden)]
188 pub vip_favorable: bool,
189 #[doc(hidden)]
190 pub vip_visits: i64,
191 #[doc(hidden)]
192 pub last_vip_nag_day: i64,
193 #[doc(hidden)]
194 pub treasures_found: f64,
195 #[doc(hidden)]
196 pub extermination_due_day: Option<f64>,
197 #[doc(hidden)]
198 pub extermination_room_ids: Option<Vec<i64>>,
201 #[doc(hidden)]
202 pub excavated: Vec<String>,
204 #[doc(hidden)]
205 pub blockbusters: Vec<f64>,
208 pub milestones: Vec<String>,
211 pub log: Vec<LogEntry>,
214 #[doc(hidden)]
215 pub log_seq: i64,
216 #[doc(hidden)]
217 pub constructing: IndexSet<i64>,
219 #[doc(hidden)]
221 pub elevators: ElevatorDispatch,
222 #[doc(hidden)]
223 pub telemetry: ElevatorTelemetry,
225 #[doc(hidden)]
226 pub housekeeping: Housekeeping,
227 #[doc(hidden)]
228 pub weather: Weather,
229 #[doc(hidden)]
230 pub fx: FxState,
232 #[doc(hidden)]
233 pub last_hour: i64,
234 #[doc(hidden)]
235 pub last_day: i64,
236 #[doc(hidden)]
237 pub last_month: i64,
238 #[doc(hidden)]
239 pub last_quarter: i64,
240 #[doc(hidden)]
241 pub on_hour_runs: i64,
242 #[doc(hidden)]
243 pub move_ins_today: MoveIns,
244 #[doc(hidden)]
245 pub waste_nudged: bool,
246 #[doc(hidden)]
247 pub suite_parking_nudged: bool,
248 #[doc(hidden)]
249 pub stranded_nudged: bool,
250 #[doc(hidden)]
251 pub metro_platform_nudged: bool,
252 #[doc(hidden)]
253 pub reach_memo: (i64, HashMap<i64, bool>),
255 #[doc(hidden)]
256 pub noise_memo: (i64, HashMap<i64, bool>),
258 #[doc(hidden)]
259 pub diag_memo: crate::diagnostics::DiagMemo,
262 #[doc(hidden)]
263 pub view: Option<Value>,
265 pub gameplay: GameplayEvents,
268 #[doc(hidden)]
269 pub extend_gesture: Option<(i64, i64, i64)>,
273}
274
275impl Simulation {
276 #[doc(hidden)]
277 pub fn new(
279 seed: u32,
280 mode: GameMode,
281 modern_calendar: CalendarKind,
282 start_unbridged: bool,
283 ) -> Simulation {
284 let bridging_toggleable = mode == GameMode::Modern;
285 let modern_calendar = if mode == GameMode::Classic {
286 CalendarKind::RealWorld
287 } else {
288 modern_calendar
289 };
290 Simulation {
291 rng: Rng::new(seed),
292 mode,
293 modern_calendar,
294 auto_bridge: if bridging_toggleable {
295 !start_unbridged
296 } else {
297 true
298 },
299 founder: false,
300 clock: Clock::new(0.0, resolve_calendar(mode, modern_calendar)),
301 crowd: Crowd::new(seed),
302 events: EventSystem::new(seed),
303 ledger: Ledger::new(),
304 tower: {
305 let mut t = Tower::new();
306 t.allows_escalator_on_office_floors = mode == GameMode::Modern;
307 t.mode = mode;
308 t
309 },
310 money: STARTING_MONEY,
311 star: 1,
312 last_quarter_money: 0.0,
313 evaluated_tower: Some(false),
314 vip_visit_day: -1.0,
315 vip_favorable: false,
316 vip_visits: 0,
317 last_vip_nag_day: -100,
318 treasures_found: 0.0,
319 extermination_due_day: None,
320 excavated: Vec::new(),
321 blockbusters: Vec::new(),
322 milestones: Vec::new(),
323 log: Vec::new(),
324 log_seq: 0,
325 constructing: IndexSet::new(),
326 elevators: ElevatorDispatch::new(),
327 telemetry: ElevatorTelemetry::default(),
328 housekeeping: Housekeeping::default(),
329 weather: weather_for(0),
330 fx: FxState::default(),
331 last_hour: -1,
332 last_day: 0,
333 last_month: -1,
334 last_quarter: -1,
335 on_hour_runs: 0,
336 move_ins_today: MoveIns::default(),
337 waste_nudged: false,
338 suite_parking_nudged: false,
339 stranded_nudged: false,
340 metro_platform_nudged: false,
341 extermination_room_ids: None,
342 reach_memo: (-1, HashMap::new()),
343 noise_memo: (-1, HashMap::new()),
344 diag_memo: Default::default(),
345 view: None,
346 gameplay: GameplayEvents::default(),
347 extend_gesture: None,
348 }
349 }
350
351 #[doc(hidden)]
352 pub fn record_money(&mut self, cat: &'static str, amount: f64) {
354 self.ledger.record(cat, amount);
355 }
356
357 pub fn population(&self) -> i64 {
360 self.tower.total_population()
361 }
362
363 #[doc(hidden)]
364 pub fn floor_reachable(&mut self, floor: i64) -> bool {
366 if floor == 1 {
367 return true;
368 }
369 if self.reach_memo.0 != self.tower.revision {
370 self.reach_memo = (self.tower.revision, HashMap::new());
371 }
372 if let Some(&hit) = self.reach_memo.1.get(&floor) {
373 return hit;
374 }
375 let hit = self.crowd.floor_reachable(&self.tower, floor);
376 self.reach_memo.1.insert(floor, hit);
377 hit
378 }
379
380 #[doc(hidden)]
381 pub fn position_reachable(&mut self, floor: i64, x: i64) -> bool {
383 if floor == 1 {
384 return true;
385 }
386 if self.tower.segments_of(floor).len() <= 1 {
387 return self.floor_reachable(floor);
388 }
389 self.crowd.position_reachable(&self.tower, floor, x)
390 }
391
392 #[doc(hidden)]
393 pub fn unit_reachable(&mut self, floor: i64, x: i64) -> bool {
395 self.position_reachable(floor, x)
396 }
397
398 pub fn new_game(seed: u32, mode: GameMode) -> Simulation {
413 Simulation::new_game_with(seed, mode, CalendarKind::RealWorld, false)
414 }
415
416 pub fn new_game_with(
428 seed: u32,
429 mode: GameMode,
430 modern_calendar: CalendarKind,
431 start_unbridged: bool,
432 ) -> Simulation {
433 let mut sim = Simulation::new(seed, mode, modern_calendar, start_unbridged);
434 sim.emit(
435 "Welcome! Lay a lobby on the ground line to open your tower.",
436 LogKind::Info,
437 );
438 sim.gameplay
439 .push(GameplayEvent::TowerFounded { mode: sim.mode });
440 sim
441 }
442
443 #[doc(hidden)]
444 pub fn emit(&mut self, text: &str, kind: LogKind) {
446 self.log.push(LogEntry {
447 minute: self.clock.minutes,
448 text: text.to_string(),
449 kind,
450 });
451 self.log_seq += 1;
452 if self.log.len() > LOG_RING_CAP {
453 self.log.remove(0);
454 }
455 }
456}