1use crate::churn::household_price;
11use crate::econ::rent_config;
12use crate::facilities::Kind;
13use crate::gameplay::{GameplayEvent, RemovalMethod};
14use crate::services::with_thousands;
15use crate::sim::{LogKind, Simulation};
16use crate::tower::UnitState;
17
18pub const ADD_CAR_COST: f64 = 40_000.0;
20pub const TRANSPORT_FLOOR_COST: f64 = 5_000.0;
22
23#[doc(hidden)]
24pub const NOT_ENOUGH_MONEY: &str = "Not enough money.";
25#[doc(hidden)]
26pub const ELEVATOR_GONE: &str = "That elevator is gone.";
27#[doc(hidden)]
28pub const ONLY_ELEVATOR_CARS: &str = "Only elevators have cars.";
29#[doc(hidden)]
30pub const ONLY_ELEVATOR_EXTEND: &str = "Only elevators can be extended.";
31#[doc(hidden)]
32pub const CAR_LIMIT: &str = "This elevator has all the cars it can hold.";
33#[doc(hidden)]
34pub const LAST_CAR: &str = "An elevator needs at least one car.";
35#[doc(hidden)]
36pub const FACILITY_GONE: &str = "That facility is gone.";
37#[doc(hidden)]
38pub const HOUSEKEEPING_PERMANENT: &str =
39 "Housekeeping rooms are permanent. The 1994 game does not let you remove them.";
40#[doc(hidden)]
41pub const CLASSIC_WEDDING_HALL_KEPT: &str = "The wedding hall can't be removed.";
43#[doc(hidden)]
44pub const SECURITY_PERMANENT: &str =
45 "Security offices are permanent. The 1994 game does not let you remove them.";
46#[doc(hidden)]
47pub const RECYCLING_PERMANENT: &str =
48 "Recycling centers are permanent. The 1994 game does not let you remove them.";
49#[doc(hidden)]
50pub const CINEMA_PERMANENT: &str =
51 "Movie theaters are permanent. The 1994 game does not let you remove them.";
52
53fn permanent_room_refusal(kind: Kind) -> &'static str {
55 match kind {
56 Kind::Security => SECURITY_PERMANENT,
57 Kind::Recycling => RECYCLING_PERMANENT,
58 Kind::Cinema => CINEMA_PERMANENT,
59 Kind::WeddingHall => CLASSIC_WEDDING_HALL_KEPT,
60 _ => HOUSEKEEPING_PERMANENT,
61 }
62}
63pub const INVALID_REQUEST: &str = "That request is not valid.";
68
69#[doc(hidden)]
70pub fn whole(x: f64) -> Option<i64> {
73 (x.fract() == 0.0 && x.abs() <= 9_007_199_254_740_991.0).then_some(x as i64)
74}
75
76#[doc(hidden)]
77pub fn car_resale_refund() -> f64 {
79 (ADD_CAR_COST * 0.5).floor()
80}
81
82#[derive(Clone, Debug, PartialEq)]
84pub struct ChargeResult {
85 pub ok: bool,
87 pub reason: Option<String>,
89 pub delta: f64,
92}
93
94impl ChargeResult {
95 pub(crate) fn refuse(reason: impl Into<String>) -> ChargeResult {
96 ChargeResult {
97 ok: false,
98 reason: Some(reason.into()),
99 delta: 0.0,
100 }
101 }
102
103 fn paid(delta: f64) -> ChargeResult {
104 ChargeResult {
105 ok: true,
106 reason: None,
107 delta,
108 }
109 }
110}
111
112#[derive(Clone, Debug, PartialEq)]
114pub struct ExtendResult {
115 pub charge: ChargeResult,
117 pub bottom: i64,
119 pub top: i64,
121 pub added: i64,
123}
124
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub enum ExtendEnd {
128 Up,
130 Down,
132}
133
134impl ExtendEnd {
135 pub fn parse(s: &str) -> Option<ExtendEnd> {
137 match s {
138 "up" => Some(ExtendEnd::Up),
139 "down" => Some(ExtendEnd::Down),
140 _ => None,
141 }
142 }
143}
144
145#[doc(hidden)]
146pub fn extend_bill(
149 cur: (i64, i64),
150 hwm: (i64, i64),
151 end: ExtendEnd,
152 target_floor: i64,
153 money: f64,
154 per_floor: f64,
155) -> (i64, i64, i64) {
156 let (mut nb, mut nt) = cur;
157 match end {
158 ExtendEnd::Up => nt = (cur.0 + 1).max(target_floor),
159 ExtendEnd::Down => nb = (cur.1 - 1).min(target_floor),
160 }
161 let budget = (money / per_floor).floor().max(0.0);
162 let budget = if budget > i64::MAX as f64 {
165 i64::MAX
166 } else {
167 budget as i64
168 };
169 if nt > hwm.1 {
170 nt = hwm.1 + (nt - hwm.1).min(budget);
171 }
172 if nb < hwm.0 {
173 nb = hwm.0 - (hwm.0 - nb).min(budget);
174 }
175 let added = (nt - hwm.1).max(0) + (hwm.0 - nb).max(0);
176 (nb, nt, added)
177}
178
179impl Simulation {
180 fn elevator(&self, id: i64, not_elevator: &str) -> Result<(Kind, i64, i64, i64), ChargeResult> {
183 let Some(t) = self.tower.get_transport(id) else {
184 return Err(ChargeResult::refuse(ELEVATOR_GONE));
185 };
186 if !t.kind.is_elevator() {
187 return Err(ChargeResult::refuse(not_elevator));
188 }
189 Ok((t.kind, t.cars, t.bottom, t.top))
190 }
191
192 pub fn add_car(&mut self, id: i64) -> ChargeResult {
195 let (kind, cars, _, _) = match self.elevator(id, ONLY_ELEVATOR_CARS) {
196 Ok(t) => t,
197 Err(r) => return r,
198 };
199 if cars >= kind.max_cars() {
200 return ChargeResult::refuse(CAR_LIMIT);
201 }
202 if self.money < ADD_CAR_COST {
203 return ChargeResult::refuse(NOT_ENOUGH_MONEY);
204 }
205 if !self.set_cars(id, cars + 1) {
206 return ChargeResult::refuse(CAR_LIMIT);
207 }
208 self.money -= ADD_CAR_COST;
209 self.note_edit_cash(-ADD_CAR_COST);
210 ChargeResult::paid(-ADD_CAR_COST)
211 }
212
213 pub fn remove_car(&mut self, id: i64) -> ChargeResult {
216 let (_, cars, _, _) = match self.elevator(id, ONLY_ELEVATOR_CARS) {
217 Ok(t) => t,
218 Err(r) => return r,
219 };
220 if cars <= 1 || !self.set_cars(id, cars - 1) {
221 return ChargeResult::refuse(LAST_CAR);
222 }
223 let refund = car_resale_refund();
224 self.money += refund;
225 self.note_edit_cash(refund);
226 ChargeResult::paid(refund)
227 }
228
229 pub fn begin_extend(&mut self, id: i64) -> bool {
235 self.extend_gesture = None;
236 match self.elevator(id, ONLY_ELEVATOR_EXTEND) {
237 Ok((_, _, bottom, top)) => {
238 self.extend_gesture = Some((id, bottom, top));
239 true
240 }
241 Err(_) => false,
242 }
243 }
244
245 #[cfg_attr(not(feature = "wasm"), allow(dead_code))]
250 pub(crate) fn restore_extend(&mut self, id: i64, bottom: i64, top: i64) {
251 self.extend_gesture = self
252 .elevator(id, ONLY_ELEVATOR_EXTEND)
253 .ok()
254 .map(|_| (id, bottom, top));
255 }
256
257 pub fn end_extend(&mut self) {
259 self.extend_gesture = None;
260 }
261
262 pub fn extend_transport(&mut self, id: i64, end: ExtendEnd, target_floor: i64) -> ExtendResult {
289 let (_, _, bottom, top) = match self.elevator(id, ONLY_ELEVATOR_EXTEND) {
290 Ok(t) => t,
291 Err(charge) => {
292 let (bottom, top) = self
293 .tower
294 .get_transport(id)
295 .map_or((0, 0), |t| (t.bottom, t.top));
296 return ExtendResult {
297 charge,
298 bottom,
299 top,
300 added: 0,
301 };
302 }
303 };
304 let gesture = self
305 .extend_gesture
306 .filter(|&(gid, _, _)| gid == id)
307 .map(|(_, b, t)| (b, t));
308 let (hb, ht) = gesture.unwrap_or((bottom, top));
309 let mark = (hb.min(bottom), ht.max(top));
310 let (nb, nt, added) = extend_bill(
311 (bottom, top),
312 mark,
313 end,
314 target_floor,
315 self.money,
316 TRANSPORT_FLOOR_COST,
317 );
318 let unchanged = |charge: ChargeResult| ExtendResult {
319 charge,
320 bottom,
321 top,
322 added: 0,
323 };
324 if nb == bottom && nt == top {
325 let wanted = match end {
326 ExtendEnd::Up => target_floor > mark.1,
327 ExtendEnd::Down => target_floor < mark.0,
328 };
329 return unchanged(if wanted {
330 ChargeResult::refuse(NOT_ENOUGH_MONEY)
331 } else {
332 ChargeResult::paid(0.0)
333 });
334 }
335 let r = self.resize_transport(id, nb, nt);
336 if !r.ok {
337 return unchanged(ChargeResult {
338 ok: false,
339 reason: r.reason,
340 delta: 0.0,
341 });
342 }
343 let cost = added as f64 * TRANSPORT_FLOOR_COST;
344 self.money -= cost;
345 self.note_edit_cash(-cost);
346 if gesture.is_some() {
347 self.extend_gesture = Some((id, mark.0.min(nb), mark.1.max(nt)));
348 }
349 ExtendResult {
350 charge: ChargeResult::paid(if cost == 0.0 { 0.0 } else { -cost }),
351 bottom: nb,
352 top: nt,
353 added,
354 }
355 }
356
357 pub fn remove_facility(&mut self, id: i64, method: RemovalMethod) -> ChargeResult {
363 if let Some(u) = self.tower.get_unit(id) {
364 let (kind, state) = (u.kind, u.state);
365 if state == UnitState::Fire {
366 return ChargeResult::refuse(format!(
367 "You can't {} a burning unit. Call fire rescue or let it burn out.",
368 method.as_str()
369 ));
370 }
371 if let Some(reason) = self.tower.removal_reason(id) {
372 return ChargeResult::refuse(reason);
373 }
374 if self.stays_for_good(kind, state) {
375 return ChargeResult::refuse(permanent_room_refusal(kind));
376 }
377 let refund = if state == UnitState::Gutted {
378 0.0
379 } else {
380 kind.resale_refund()
381 };
382 let buyback = self.sold_condo_buyback(id, state);
386 if buyback > 0.0 && self.money + refund < buyback {
387 return ChargeResult::refuse(format!(
388 "You can't {} this condo without buying it back for ${}.",
389 method.as_str(),
390 with_thousands(buyback)
391 ));
392 }
393 let floor = self.tower.get_unit(id).map(|u| u.floor).unwrap_or(0);
394 self.tower.remove_unit(id);
395 if buyback > 0.0 {
396 let msg = format!(
397 "You bought back Condominium on {} for ${} to remove it.",
398 self.floor_label(floor),
399 with_thousands(buyback)
400 );
401 self.emit(&msg, LogKind::Money);
402 }
403 let refund = refund - buyback;
404 self.money += refund;
405 self.note_edit_cash(refund);
406 if kind == Kind::WeddingHall
407 && self.tower.built_wedding_hall != Some(true)
408 && self.evaluated_tower != Some(true)
409 {
410 self.edit_vip_visit_day(-1.0);
411 }
412 self.gameplay
413 .push(GameplayEvent::FacilityRemoved { kind, method });
414 return ChargeResult::paid(refund);
415 }
416 let Some(kind) = self.tower.get_transport(id).map(|t| t.kind) else {
417 return ChargeResult::refuse(FACILITY_GONE);
418 };
419 self.tower.remove_transport(id);
420 let refund = kind.resale_refund();
421 self.money += refund;
422 self.note_edit_cash(refund);
423 self.gameplay
424 .push(GameplayEvent::FacilityRemoved { kind, method });
425 ChargeResult::paid(refund)
426 }
427
428 fn sold_condo_buyback(&self, id: i64, state: UnitState) -> f64 {
432 match self.tower.get_unit(id) {
433 Some(u) if u.kind == Kind::Condo && u.ever_occupied && state != UnitState::Gutted => {
434 let sale_price = u
435 .rent
436 .unwrap_or_else(|| rent_config(Kind::Condo).map(|c| c.default).unwrap_or(0.0));
437 household_price(sale_price, u.residents)
438 }
439 _ => 0.0,
440 }
441 }
442
443 pub(crate) fn stays_for_good(&self, kind: Kind, state: UnitState) -> bool {
448 state != UnitState::Gutted && self.mode.room_is_permanent(kind)
449 }
450}
451
452#[cfg(test)]
453#[path = "charges_tests.rs"]
454mod tests;
455
456#[cfg(test)]
457#[path = "charges_removal_tests.rs"]
458mod removal_tests;
459
460#[cfg(test)]
461#[path = "charges_schedule_tests.rs"]
462mod schedule_tests;