Skip to main content
Verticopolis engineGitHub

verticopolis_engine/
facilities.rs

1//! What can be built: [`Kind`] names every floor tile, room and transport,
2//! and the constants here give the lot's size.
3//!
4//! Prices, sizes, star requirements and build limits for a mode come from
5//! [`crate::catalog::catalog`], which a frontend reads instead of keeping
6//! tables of its own.
7
8/// The lot's width in tiles. A tile column `x` runs from 0 to `LOT_WIDTH - 1`.
9pub const LOT_WIDTH: i64 = 375;
10/// The top floor.
11pub const MAX_FLOOR: i64 = 100;
12/// The lowest basement floor. Floor 1 is the ground floor and floor 0 the
13/// first basement.
14pub const MIN_FLOOR: i64 = -9;
15/// Sky lobbies go every this many floors above the ground floor.
16pub const LOBBY_INTERVAL: i64 = 15;
17#[doc(hidden)]
18/// The wedding hall stands on floor `MAX_FLOOR` and rises four stories above it,
19/// the only facility that does. Grids that hold every story of every unit size
20/// to this.
21pub const WEDDING_HALL_TOP_FLOOR: i64 = MAX_FLOOR + 4;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
24/// Everything a player can build.
25///
26/// [`Kind::as_str`] and [`Kind::parse`] convert to and from the names the
27/// save, the catalog and the JavaScript binding use (`lobby`, `hotelSingle`,
28/// `elevatorExpress`, ...).
29pub enum Kind {
30    /// Lobby. Floor 1 is all lobby; a lobby every fifteenth floor above it is a sky lobby.
31    Lobby,
32    /// Floor. An empty floor tile, which rooms stand on.
33    Floor,
34    /// Office.
35    Office,
36    /// Condominium.
37    Condo,
38    /// Single Room.
39    HotelSingle,
40    /// Double Room.
41    HotelDouble,
42    /// Suite.
43    HotelSuite,
44    /// Fast Food.
45    FastFood,
46    /// Restaurant.
47    Restaurant,
48    /// Food Hall. Modern only.
49    FoodHall,
50    /// Retail Shop.
51    Shop,
52    /// Cinema.
53    Cinema,
54    /// Party Hall.
55    PartyHall,
56    /// Amusements. Modern only.
57    Amusements,
58    /// Boutique Bay. Modern only.
59    BoutiqueBay,
60    /// Fitness Club. Modern only.
61    FitnessClub,
62    /// Clinic. Modern only.
63    Clinic,
64    /// Nightclub. Modern only.
65    Nightclub,
66    /// Spa. Modern only.
67    Spa,
68    /// Sky Bar. Modern only.
69    SkyBar,
70    /// Aquatic Center. Modern only.
71    AquaticCenter,
72    /// Daycare. Modern only.
73    Daycare,
74    /// Stairway. Spans two floors.
75    Stairs,
76    /// Escalator. Spans two floors.
77    Escalator,
78    /// Standard Elevator.
79    ElevatorStandard,
80    /// Service Elevator. Staff only.
81    ElevatorService,
82    /// Express Elevator. Stops only at lobbies and the floors it is set to serve, and spans the whole tower.
83    ElevatorExpress,
84    /// Parking Ramp.
85    ParkingRamp,
86    /// Parking Space.
87    Parking,
88    /// Security.
89    Security,
90    /// Medical Center.
91    Medical,
92    /// Housekeeping.
93    Housekeeping,
94    /// Recycling Center.
95    Recycling,
96    /// Metro Station.
97    Metro,
98    /// Wedding Hall. Stands on the top floor.
99    WeddingHall,
100    /// Studio. A rental home. Modern only.
101    RentalStudio,
102    /// Apartment. A rental home. Modern only.
103    RentalApartment,
104}
105
106#[doc(hidden)]
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub enum Category {
109    Structure,
110    Transport,
111    Office,
112    Residential,
113    Hotel,
114    Food,
115    Retail,
116    Entertainment,
117    Service,
118    Special,
119}
120
121#[doc(hidden)]
122#[derive(Clone, Copy, Debug)]
123pub struct Facility {
124    pub kind: Kind,
125    pub key: &'static str,
126    pub category: Category,
127    pub name: &'static str,
128    pub width: i64,
129    pub floors: Option<i64>,
130    pub cost: f64,
131    pub min_star: i64,
132    pub population: i64,
133    pub attendance: Option<i64>,
134    pub modern_only: bool,
135    pub transport: bool,
136    pub staff_only: bool,
137    pub basement: bool,
138}
139
140impl Facility {
141    #[doc(hidden)]
142    /// Whether a tower of `mode` builds this kind at all, the star gate
143    /// aside: a Modern-only kind is built only where the mode's rule
144    /// (`GameMode::builds_modern_only_kinds`) allows it. The build gate
145    /// (`is_unlocked`) and the catalog's `available` both read it.
146    pub fn available_in(&self, mode: crate::clock::GameMode) -> bool {
147        !self.modern_only || mode.builds_modern_only_kinds()
148    }
149}
150
151mod predicates;
152mod table;
153#[doc(hidden)]
154pub use table::FACILITIES;
155
156impl Kind {
157    #[doc(hidden)]
158    /// The catalog row. `FACILITIES` is laid out in `Kind` order, which the
159    /// `catalog_order` test pins, so the lookup is an index.
160    pub fn facility(self) -> &'static Facility {
161        &FACILITIES[self as usize]
162    }
163
164    /// The kind's name in saves and the catalog, such as `fastFood`.
165    pub fn as_str(self) -> &'static str {
166        self.facility().key
167    }
168
169    /// The kind with this name, as [`as_str`](Kind::as_str) writes it.
170    ///
171    /// ```
172    /// use verticopolis_engine::facilities::Kind;
173    ///
174    /// assert_eq!(Kind::parse("hotelSuite"), Some(Kind::HotelSuite));
175    /// assert_eq!(Kind::HotelSuite.as_str(), "hotelSuite");
176    /// ```
177    pub fn parse(s: &str) -> Option<Kind> {
178        FACILITIES.iter().find(|f| f.key == s).map(|f| f.kind)
179    }
180
181    /// A floor or lobby tile.
182    pub fn is_structural(self) -> bool {
183        matches!(self, Kind::Floor | Kind::Lobby)
184    }
185
186    /// An elevator, stairs or an escalator.
187    pub fn is_transport(self) -> bool {
188        self.facility().transport
189    }
190
191    /// One of the three elevator kinds.
192    pub fn is_elevator(self) -> bool {
193        matches!(
194            self,
195            Kind::ElevatorStandard | Kind::ElevatorService | Kind::ElevatorExpress
196        )
197    }
198
199    #[doc(hidden)]
200    pub fn is_hotel(self) -> bool {
201        matches!(
202            self,
203            Kind::HotelSingle | Kind::HotelDouble | Kind::HotelSuite
204        )
205    }
206
207    #[doc(hidden)]
208    pub fn is_rental(self) -> bool {
209        matches!(self, Kind::RentalStudio | Kind::RentalApartment)
210    }
211
212    /// Anything that is neither a floor tile nor a transport.
213    pub fn is_room(self) -> bool {
214        !self.is_structural() && !self.is_transport()
215    }
216
217    #[doc(hidden)]
218    /// `facilityFloors`.
219    pub fn floors(self) -> i64 {
220        self.facility().floors.unwrap_or(1)
221    }
222
223    #[doc(hidden)]
224    /// The highest story a unit of this kind may occupy: `MAX_FLOOR`, except the
225    /// wedding hall, which stands on floor `MAX_FLOOR` and rises above it.
226    pub fn highest_story(self) -> i64 {
227        if self == Kind::WeddingHall {
228            MAX_FLOOR + self.floors() - 1
229        } else {
230            MAX_FLOOR
231        }
232    }
233
234    #[doc(hidden)]
235    /// The highest floor a unit of this kind may stand on (its bottom story).
236    pub fn highest_base_floor(self) -> i64 {
237        self.highest_story() - (self.floors() - 1)
238    }
239
240    #[doc(hidden)]
241    /// `maxSpanFor`.
242    pub fn max_span(self) -> i64 {
243        match self {
244            Kind::Stairs | Kind::Escalator => 1,
245            Kind::ElevatorExpress => MAX_FLOOR - MIN_FLOOR,
246            _ => 30,
247        }
248    }
249
250    #[doc(hidden)]
251    pub fn is_fixed_span(self) -> bool {
252        self.is_transport() && self.max_span() == 1
253    }
254
255    #[doc(hidden)]
256    pub fn max_cars(self) -> i64 {
257        8
258    }
259
260    #[doc(hidden)]
261    /// `buildMinutes`.
262    pub fn build_minutes(self) -> f64 {
263        if self.is_structural() {
264            return 0.0;
265        }
266        let f = self.facility();
267        (8.0_f64 * 60.0).min(crate::jsmath::round(
268            60.0 + f.width as f64 * 8.0 + f.cost / 5000.0,
269        ))
270    }
271
272    #[doc(hidden)]
273    /// `resaleRefund`.
274    pub fn resale_refund(self) -> f64 {
275        (self.facility().cost * 0.5).floor()
276    }
277
278    #[doc(hidden)]
279    /// `subtypeListFor`.
280    pub fn subtype_list(self) -> Option<&'static [&'static str]> {
281        Some(match self {
282            Kind::Restaurant => &[
283                "English Pub",
284                "French",
285                "Chinese",
286                "Sushi Bar",
287                "Steak House",
288            ],
289            Kind::FastFood => &[
290                "Japanese Soba",
291                "Chinese Cafe",
292                "Hamburger Stand",
293                "Ice Cream",
294                "Coffee Shop",
295            ],
296            Kind::Shop => &[
297                "Men's Clothing",
298                "Pet Store",
299                "Flower Shop",
300                "Book Store",
301                "Drug Store",
302                "Boutique",
303                "Electronics",
304                "Bank",
305                "Hair Salon",
306                "Post Office",
307                "Sports Gear",
308            ],
309            Kind::FoodHall => &[
310                "Ramen Bar",
311                "Taco Stand",
312                "Bubble Tea",
313                "Poke Bowl",
314                "Deli Counter",
315                "Coffee Cart",
316            ],
317            Kind::Amusements => &["Classic Arcade", "VR Lounge", "Claw Parlor", "Mini Golf"],
318            Kind::BoutiqueBay => &[
319                "Florist",
320                "Barber",
321                "Phone Repair",
322                "Vintage",
323                "Tattoo",
324                "Record Store",
325                "Gallery",
326            ],
327            Kind::FitnessClub => &[
328                "Weight Floor",
329                "Yoga Studio",
330                "Spin Studio",
331                "Boxing Gym",
332                "Climbing Wall",
333            ],
334            Kind::Clinic => &["Dental", "Urgent Care", "Optometry", "Pharmacy", "Physio"],
335            _ => return None,
336        })
337    }
338}
339
340impl Category {
341    #[doc(hidden)]
342    /// The category as the TypeScript engine spells it.
343    pub fn as_str(self) -> &'static str {
344        match self {
345            Category::Structure => "structure",
346            Category::Transport => "transport",
347            Category::Office => "office",
348            Category::Residential => "residential",
349            Category::Hotel => "hotel",
350            Category::Food => "food",
351            Category::Retail => "retail",
352            Category::Entertainment => "entertainment",
353            Category::Service => "service",
354            Category::Special => "special",
355        }
356    }
357}
358
359#[doc(hidden)]
360/// `BUILD_CAPS`.
361pub fn build_cap(kind: Kind) -> Option<i64> {
362    match kind {
363        Kind::Metro => Some(1),
364        Kind::WeddingHall => Some(1),
365        Kind::Security => Some(10),
366        Kind::Medical => Some(10),
367        Kind::Cinema => Some(16),
368        Kind::PartyHall => Some(16),
369        Kind::AquaticCenter => Some(8),
370        _ => None,
371    }
372}
373
374#[doc(hidden)]
375pub struct Pool {
376    pub kinds: &'static [Kind],
377    pub cap: i64,
378    pub label: &'static str,
379}
380
381#[doc(hidden)]
382/// `POOLED_CAPS`.
383pub static POOLED_CAPS: [Pool; 2] = [
384    Pool {
385        kinds: &[
386            Kind::ElevatorStandard,
387            Kind::ElevatorService,
388            Kind::ElevatorExpress,
389        ],
390        cap: 24,
391        label: "elevator shafts",
392    },
393    Pool {
394        kinds: &[Kind::Stairs, Kind::Escalator],
395        cap: 64,
396        label: "stairs/escalators",
397    },
398];
399
400#[doc(hidden)]
401pub fn is_lobby_floor(floor: i64) -> bool {
402    floor == 1 || (floor > 1 && floor % LOBBY_INTERVAL == 0)
403}
404
405#[doc(hidden)]
406pub fn is_sky_lobby_floor(floor: i64) -> bool {
407    floor >= 2 && floor % LOBBY_INTERVAL == 0
408}
409
410#[doc(hidden)]
411pub fn ground_floor_structure_kind(kind: Kind, floor: i64) -> Kind {
412    if kind == Kind::Floor && floor == 1 {
413        Kind::Lobby
414    } else {
415        kind
416    }
417}
418
419#[doc(hidden)]
420/// `NO_BASEMENT_KINDS`.
421pub fn no_basement(kind: Kind) -> bool {
422    matches!(
423        kind,
424        Kind::Office
425            | Kind::Condo
426            | Kind::HotelSingle
427            | Kind::HotelDouble
428            | Kind::HotelSuite
429            | Kind::Housekeeping
430    )
431}
432
433#[doc(hidden)]
434pub fn covers_ground_floor(floor: i64, hgt: i64) -> bool {
435    floor <= 1 && floor + hgt > 1
436}
437
438#[doc(hidden)]
439pub const NEEDS_FLOORS: &str = "Transport must run through built floors. Lay floors first.";
440#[doc(hidden)]
441pub const SHAFT_OVERLAP: &str = "Transport shafts cannot overlap.";
442
443#[cfg(test)]
444mod tests;