1use serde::Serialize;
16
17use super::visitors::{
18 FADING_FILM_DRAW, FILM_SPENT_DAYS, NEW_FILM_DRAW, NEW_FILM_FULL_DRAW_DAYS, SPENT_FILM_DRAW,
19};
20use crate::clock::GameMode;
21use crate::facilities::Kind;
22use crate::services::with_thousands;
23use crate::sim::{LogKind, Simulation};
24use crate::tower::{Extras, Unit};
25
26pub const NEW_FILM_PRICE: f64 = 300_000.0;
28pub const CLASSIC_FILM_PRICE: f64 = 150_000.0;
30pub const MODERN_REVIVAL_PRICE: f64 = 60_000.0;
32pub const MODERN_FEATURE_PRICE: f64 = 120_000.0;
33pub const MODERN_BLOCKBUSTER_PRICE: f64 = 300_000.0;
34pub const BLOCKBUSTER_INCOME_MULT: f64 = 2.2;
36pub const REVIVAL_INCOME_MULT: f64 = 0.6;
38pub const FADING_INCOME_SHARE: f64 = 0.6;
41pub const SPENT_INCOME_SHARE: f64 = 0.25;
42pub const MODERN_FULL_DRAW_DAYS: i64 = 28;
44pub const MODERN_SPENT_DAYS: i64 = 42;
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum FilmTier {
50 New,
51 Classic,
52 Blockbuster,
53}
54
55impl FilmTier {
56 pub const ALL: [FilmTier; 3] = [FilmTier::New, FilmTier::Classic, FilmTier::Blockbuster];
58
59 pub const fn key(self) -> &'static str {
61 match self {
62 FilmTier::New => "new",
63 FilmTier::Classic => "classic",
64 FilmTier::Blockbuster => "blockbuster",
65 }
66 }
67
68 pub fn showing(extras: &Extras) -> FilmTier {
70 if extras.film_classic {
71 FilmTier::Classic
72 } else if extras.film_blockbuster {
73 FilmTier::Blockbuster
74 } else {
75 FilmTier::New
76 }
77 }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub enum FilmStage {
83 Full,
84 Fading,
85 Spent,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub struct FilmRun {
91 pub full_draw_days: i64,
93 pub spent_days: i64,
95}
96
97impl FilmRun {
98 pub fn stage(self, days_shown: i64) -> FilmStage {
100 if days_shown < self.full_draw_days {
101 FilmStage::Full
102 } else if days_shown < self.spent_days {
103 FilmStage::Fading
104 } else {
105 FilmStage::Spent
106 }
107 }
108
109 pub fn draw(self, days_shown: i64, tier: FilmTier) -> i64 {
113 let draw = match self.stage(days_shown) {
114 FilmStage::Full => NEW_FILM_DRAW,
115 FilmStage::Fading => FADING_FILM_DRAW,
116 FilmStage::Spent => SPENT_FILM_DRAW,
117 };
118 if tier == FilmTier::Classic {
119 draw.min(FADING_FILM_DRAW)
120 } else {
121 draw
122 }
123 }
124}
125
126#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
128#[serde(rename_all = "camelCase")]
129pub struct FilmOffer {
130 #[serde(skip)]
131 pub tier: FilmTier,
132 pub key: &'static str,
134 pub label: &'static str,
136 pub price: f64,
138 #[serde(skip)]
140 pub opening: &'static str,
141 #[serde(skip)]
144 pub income_mult: f64,
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
151#[serde(rename_all = "camelCase")]
152pub struct FilmRebook {
153 pub policy: &'static str,
155 pub film: Option<&'static str>,
157 #[serde(skip)]
158 pub tier: Option<FilmTier>,
159}
160
161#[derive(Debug)]
163pub struct FilmRules {
164 pub offers: &'static [FilmOffer],
166 pub run: FilmRun,
167 pub blockbuster_tier: FilmTier,
169 pub rebooks: &'static [FilmRebook],
173 pub rebook_default: Option<&'static str>,
176 pub income_follows_film: bool,
179}
180
181impl FilmRules {
182 pub fn offer(&self, tier: FilmTier) -> Option<&'static FilmOffer> {
184 self.offers.iter().find(|o| o.tier == tier)
185 }
186
187 pub fn rebooked(&self, policy: Option<&str>) -> Option<FilmTier> {
191 let listed = |p: &str| self.rebooks.iter().find(|r| r.policy == p);
192 policy
193 .and_then(listed)
194 .or_else(|| self.rebook_default.and_then(listed))
195 .and_then(|r| r.tier)
196 }
197}
198
199pub static CLASSIC_FILMS: FilmRules = FilmRules {
200 offers: &[
201 FilmOffer {
202 tier: FilmTier::New,
203 key: "new",
204 label: "New film",
205 price: NEW_FILM_PRICE,
206 opening: "A new film",
207 income_mult: 1.0,
208 },
209 FilmOffer {
210 tier: FilmTier::Classic,
211 key: "classic",
212 label: "Classic film",
213 price: CLASSIC_FILM_PRICE,
214 opening: "A classic film",
215 income_mult: 1.0,
216 },
217 ],
218 run: FilmRun {
219 full_draw_days: NEW_FILM_FULL_DRAW_DAYS,
220 spent_days: FILM_SPENT_DAYS,
221 },
222 blockbuster_tier: FilmTier::New,
223 rebooks: &[],
224 rebook_default: None,
225 income_follows_film: false,
226};
227
228pub static MODERN_FILMS: FilmRules = FilmRules {
229 offers: &[
230 FilmOffer {
231 tier: FilmTier::Classic,
232 key: "classic",
233 label: "Revival",
234 price: MODERN_REVIVAL_PRICE,
235 opening: "A revival",
236 income_mult: REVIVAL_INCOME_MULT,
237 },
238 FilmOffer {
239 tier: FilmTier::New,
240 key: "new",
241 label: "Feature",
242 price: MODERN_FEATURE_PRICE,
243 opening: "A feature",
244 income_mult: 1.0,
245 },
246 FilmOffer {
247 tier: FilmTier::Blockbuster,
248 key: "blockbuster",
249 label: "Blockbuster",
250 price: MODERN_BLOCKBUSTER_PRICE,
251 opening: "A blockbuster",
252 income_mult: BLOCKBUSTER_INCOME_MULT,
253 },
254 ],
255 run: FilmRun {
256 full_draw_days: MODERN_FULL_DRAW_DAYS,
257 spent_days: MODERN_SPENT_DAYS,
258 },
259 blockbuster_tier: FilmTier::Blockbuster,
260 rebooks: &[
261 FilmRebook {
262 policy: "off",
263 film: None,
264 tier: None,
265 },
266 FilmRebook {
267 policy: "revival",
268 film: Some("classic"),
269 tier: Some(FilmTier::Classic),
270 },
271 FilmRebook {
272 policy: "feature",
273 film: Some("new"),
274 tier: Some(FilmTier::New),
275 },
276 FilmRebook {
277 policy: "blockbuster",
278 film: Some("blockbuster"),
279 tier: Some(FilmTier::Blockbuster),
280 },
281 ],
282 rebook_default: Some("feature"),
283 income_follows_film: true,
284};
285
286#[derive(Debug, Clone, Copy, PartialEq)]
288pub enum FilmRefusal {
289 Unavailable,
291 NotTheater,
293 Closed,
295 Funds {
297 cost: f64,
299 },
300}
301
302impl FilmRefusal {
303 pub fn reason(&self) -> &'static str {
305 match self {
306 FilmRefusal::Unavailable => "unavailable",
307 FilmRefusal::NotTheater => "notTheater",
308 FilmRefusal::Closed => "closed",
309 FilmRefusal::Funds { .. } => "funds",
310 }
311 }
312}
313
314fn showing(u: &Unit) -> (FilmTier, i64) {
316 let extras = u.extras();
317 (
318 FilmTier::showing(extras),
319 i64::from(extras.film_days.unwrap_or(0)),
320 )
321}
322
323pub(crate) fn fills_blockbuster_slot(mode: GameMode, u: &Unit) -> bool {
326 let films = mode.films();
327 let (tier, days) = showing(u);
328 tier == films.blockbuster_tier && films.run.stage(days) == FilmStage::Full
329}
330
331pub(crate) fn audience_draw(mode: GameMode, u: &Unit) -> i64 {
333 let (tier, days) = showing(u);
334 mode.films().run.draw(days, tier)
335}
336
337pub(crate) fn film_income_mult(mode: GameMode, u: &Unit, in_slot: bool) -> f64 {
340 let films = mode.films();
341 if !films.income_follows_film {
342 return if in_slot {
343 BLOCKBUSTER_INCOME_MULT
344 } else {
345 1.0
346 };
347 }
348 let (tier, days) = showing(u);
349 let peak = films.offer(tier).map_or(1.0, |o| o.income_mult);
350 let share = match films.run.stage(days) {
351 FilmStage::Full => 1.0,
352 FilmStage::Fading => FADING_INCOME_SHARE,
353 FilmStage::Spent => SPENT_INCOME_SHARE,
354 };
355 peak * share
356}
357
358impl Simulation {
359 pub fn change_film(&mut self, id: i64, tier: FilmTier) -> Result<f64, FilmRefusal> {
364 let mode = self.tower.mode;
365 let offer = mode.films().offer(tier).ok_or(FilmRefusal::Unavailable)?;
366 let u = self
367 .tower
368 .get_unit(id)
369 .filter(|u| u.kind == Kind::Cinema)
370 .ok_or(FilmRefusal::NotTheater)?;
371 if !u.is_operational() {
372 return Err(FilmRefusal::Closed);
373 }
374 let cost = offer.price;
375 if self.money < cost {
376 return Err(FilmRefusal::Funds { cost });
377 }
378 self.money -= cost;
379 self.record_money("entertainment", -cost);
380 let fills_slot = {
381 let u = self.tower.get_unit_mut(id).ok_or(FilmRefusal::NotTheater)?;
382 let mut extras = u.extras_mut();
383 extras.film_days = Some(0);
384 extras.film_classic = tier == FilmTier::Classic;
385 extras.film_blockbuster = tier == FilmTier::Blockbuster;
386 drop(extras);
387 fills_blockbuster_slot(mode, u)
388 };
389 let key = id as f64;
390 self.blockbusters.retain(|&b| b != key);
391 if fills_slot {
392 self.blockbusters.push(key);
393 }
394 self.emit(
395 &format!(
396 "๐ฌ {} opens at the movie theater: ${} charged.",
397 offer.opening,
398 with_thousands(cost)
399 ),
400 LogKind::Money,
401 );
402 Ok(cost)
403 }
404
405 pub fn rebook_spent_films(&mut self) {
411 let films = self.tower.mode.films();
412 if films.rebooks.is_empty() {
413 return;
414 }
415 let due: Vec<(i64, FilmTier, bool, i64)> = self
416 .tower
417 .units_of_kind(Kind::Cinema)
418 .filter(|u| u.is_operational())
419 .filter_map(|u| {
420 let (_, days) = showing(u);
421 let tier = films.rebooked(u.extras().film_policy)?;
422 (days >= films.run.spent_days).then_some((
423 u.id,
424 tier,
425 days == films.run.spent_days,
426 u.floor,
427 ))
428 })
429 .collect();
430 for (id, tier, first_spent_morning, floor) in due {
431 if let Err(FilmRefusal::Funds { cost }) = self.change_film(id, tier) {
432 if first_spent_morning {
433 let label = films.offer(tier).map_or("film", |o| o.label);
434 let msg = format!(
435 "๐ฌ The movie theater on {} can't afford its next {} (${}).",
436 self.floor_label(floor),
437 label.to_lowercase(),
438 with_thousands(cost)
439 );
440 self.emit(&msg, LogKind::Bad);
441 }
442 }
443 }
444 }
445}
446
447#[cfg(test)]
448#[path = "films_tests.rs"]
449mod films_tests;
450
451#[cfg(test)]
452#[path = "films_modern_tests.rs"]
453mod films_modern_tests;