santa/
before_level.rs

1use std::collections::{BTreeSet, HashSet};
2
3use rand::seq::SliceRandom;
4use rand::SeedableRng;
5use rand_xorshift::XorShiftRng;
6
7use abstutil::prettyprint_usize;
8use geom::Time;
9use map_gui::load::MapLoader;
10use map_gui::ID;
11use map_model::BuildingID;
12use widgetry::tools::PopupMsg;
13use widgetry::{
14    ButtonBuilder, Color, ControlState, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment,
15    Image, Key, Line, Outcome, Panel, RewriteColor, State, Text, TextExt, VerticalAlignment,
16    Widget,
17};
18
19use crate::buildings::{BldgState, Buildings};
20use crate::game::Game;
21use crate::levels::Level;
22use crate::meters::{custom_bar, make_bar};
23use crate::vehicles::Vehicle;
24use crate::{App, Transition};
25
26const ZOOM: f64 = 2.0;
27
28pub struct Picker {
29    vehicle_panel: Panel,
30    instructions_panel: Panel,
31    upzone_panel: Panel,
32    level: Level,
33    bldgs: Buildings,
34    current_picks: BTreeSet<BuildingID>,
35    draw_start: Drawable,
36}
37
38impl Picker {
39    pub fn new_state(ctx: &mut EventCtx, app: &App, level: Level) -> Box<dyn State<App>> {
40        MapLoader::new_state(
41            ctx,
42            app,
43            level.map.clone(),
44            Box::new(move |ctx, app| {
45                app.session.music.change_song(&level.music);
46
47                ctx.canvas.cam_zoom = ZOOM;
48
49                let intersection_id = app
50                    .map
51                    .find_i_by_pt2d(app.map.localise_lon_lat_to_map(level.start))
52                    .expect("Failed to get level start point");
53
54                let start = app.map.get_i(intersection_id).polygon.center();
55                ctx.canvas.center_on_map_pt(start);
56
57                let bldgs = Buildings::new(ctx, app, HashSet::new());
58
59                let mut txt = Text::new();
60                txt.add_line(Line(format!("Ready for {}?", level.title)).small_heading());
61                txt.add_line(format!(
62                    "Goal: deliver {} presents",
63                    prettyprint_usize(level.goal)
64                ));
65                txt.add_line(format!("Time limit: {}", level.time_limit));
66                txt.add_appended(vec![
67                    Line("Deliver presents to "),
68                    Line("single-family homes").fg(app.cs.residential_building),
69                    Line(" and "),
70                    Line("apartments").fg(app.session.colors.apartment),
71                ]);
72                txt.add_appended(vec![
73                    Line("Raise your blood sugar by visiting "),
74                    Line("stores").fg(app.session.colors.store),
75                ]);
76
77                let instructions_panel = Panel::new_builder(Widget::col(vec![
78                    txt.into_widget(ctx),
79                    Widget::row(vec![
80                        GeomBatch::load_svg_bytes(
81                            &ctx.prerender,
82                            widgetry::include_labeled_bytes!(
83                                "../../../widgetry/icons/arrow_keys.svg"
84                            ),
85                        )
86                        .into_widget(ctx),
87                        Text::from_all(vec![
88                            Line("arrow keys").fg(ctx.style().text_hotkey_color),
89                            Line(" to move (or "),
90                            Line("WASD").fg(ctx.style().text_hotkey_color),
91                            Line(")"),
92                        ])
93                        .into_widget(ctx),
94                    ]),
95                    Widget::row(vec![
96                        Image::from_path("system/assets/tools/mouse.svg").into_widget(ctx),
97                        Text::from_all(vec![
98                            Line("mouse scroll wheel or touchpad")
99                                .fg(ctx.style().text_hotkey_color),
100                            Line(" to zoom in or out"),
101                        ])
102                        .into_widget(ctx),
103                    ]),
104                    Text::from_all(vec![
105                        Line("Escape key").fg(ctx.style().text_hotkey_color),
106                        Line(" to pause"),
107                    ])
108                    .into_widget(ctx),
109                ]))
110                .aligned(HorizontalAlignment::LeftInset, VerticalAlignment::TopInset)
111                .build(ctx);
112
113                let draw_start = map_gui::tools::start_marker(ctx, start, 3.0);
114
115                let current_picks = app
116                    .session
117                    .upzones_per_level
118                    .get(level.title.clone())
119                    .clone();
120                let upzone_panel = make_upzone_panel(ctx, app, current_picks.len());
121
122                Transition::Replace(Box::new(Picker {
123                    vehicle_panel: make_vehicle_panel(ctx, app),
124                    upzone_panel,
125                    instructions_panel,
126                    level,
127                    bldgs,
128                    current_picks,
129                    draw_start: ctx.upload(draw_start),
130                }))
131            }),
132        )
133    }
134
135    fn randomly_pick_upzones(&mut self, app: &App) {
136        let mut choices = Vec::new();
137        for (b, state) in &self.bldgs.buildings {
138            if let BldgState::Undelivered(_) = state {
139                if !self.current_picks.contains(b) {
140                    choices.push(*b);
141                }
142            }
143        }
144        let mut rng = XorShiftRng::seed_from_u64(42);
145        choices.shuffle(&mut rng);
146        let n = app.session.upzones_unlocked - self.current_picks.len();
147        // Maps are definitely large enough for this to be fine
148        assert!(choices.len() >= n);
149        self.current_picks.extend(choices.into_iter().take(n));
150    }
151}
152
153impl State<App> for Picker {
154    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
155        if app.session.upzones_unlocked > 0 && !app.session.upzones_explained {
156            app.session.upzones_explained = true;
157            return explain_upzoning(ctx);
158        }
159
160        ctx.canvas_movement();
161
162        if ctx.redo_mouseover() {
163            app.current_selection = app.mouseover_unzoomed_buildings(ctx).filter(|id| {
164                let b = match id {
165                    ID::Building(b) => b,
166                    _ => panic!("Can't call as_building on {:?}", id),
167                };
168                matches!(self.bldgs.buildings[&b], BldgState::Undelivered(_))
169            });
170        }
171        if let Some(ID::Building(b)) = app.current_selection {
172            if ctx.normal_left_click() {
173                if self.current_picks.contains(&b) {
174                    self.current_picks.remove(&b);
175                } else if self.current_picks.len() < app.session.upzones_unlocked {
176                    self.current_picks.insert(b);
177                }
178                self.upzone_panel = make_upzone_panel(ctx, app, self.current_picks.len());
179            }
180        }
181
182        if let Outcome::Clicked(x) = self.upzone_panel.event(ctx) {
183            match x.as_ref() {
184                "Start game" => {
185                    app.current_selection = None;
186                    app.session
187                        .upzones_per_level
188                        .set(self.level.title.clone(), self.current_picks.clone());
189                    app.session.save();
190
191                    return Transition::Replace(Game::new_state(
192                        ctx,
193                        app,
194                        self.level.clone(),
195                        Vehicle::get(&app.session.current_vehicle),
196                        self.current_picks.clone().into_iter().collect(),
197                    ));
198                }
199                "Randomly choose upzones" => {
200                    self.randomly_pick_upzones(app);
201                    self.upzone_panel = make_upzone_panel(ctx, app, self.current_picks.len());
202                }
203                "Clear upzones" => {
204                    self.current_picks.clear();
205                    self.upzone_panel = make_upzone_panel(ctx, app, self.current_picks.len());
206                }
207                "help" => {
208                    return explain_upzoning(ctx);
209                }
210                _ => unreachable!(),
211            }
212        }
213
214        if let Outcome::Clicked(x) = self.vehicle_panel.event(ctx) {
215            app.session.current_vehicle = x;
216            self.vehicle_panel = make_vehicle_panel(ctx, app);
217        }
218
219        app.session.update_music(ctx);
220
221        Transition::Keep
222    }
223
224    fn draw(&self, g: &mut GfxCtx, app: &App) {
225        self.vehicle_panel.draw(g);
226        self.upzone_panel.draw(g);
227        self.instructions_panel.draw(g);
228        app.session.music.draw(g);
229        g.redraw(&self.bldgs.draw_all);
230        for b in &self.current_picks {
231            g.draw_polygon(Color::PINK, app.map.get_b(*b).polygon.clone());
232        }
233        // This covers up the current selection, so...
234        if let Some(ID::Building(b)) = app.current_selection {
235            g.draw_polygon(app.cs.selected, app.map.get_b(b).polygon.clone());
236        }
237        g.redraw(&self.draw_start);
238    }
239}
240
241fn make_vehicle_panel(ctx: &mut EventCtx, app: &App) -> Panel {
242    let mut buttons = Vec::new();
243    for name in &app.session.vehicles_unlocked {
244        let vehicle = Vehicle::get(name);
245        let batch = vehicle
246            .animate(ctx.prerender, Time::START_OF_DAY)
247            .scale(10.0);
248
249        buttons.push(
250            if name == &app.session.current_vehicle {
251                batch
252                    .into_widget(ctx)
253                    .container()
254                    .padding(5)
255                    .outline((2.0, Color::WHITE))
256            } else {
257                let normal = batch.clone().color(RewriteColor::MakeGrayscale);
258                let hovered = batch;
259                ButtonBuilder::new()
260                    .custom_batch(normal, ControlState::Default)
261                    .custom_batch(hovered, ControlState::Hovered)
262                    .build_widget(ctx, name)
263            }
264            .centered_vert(),
265        );
266        buttons.push(Widget::vert_separator(ctx, 150.0));
267    }
268    buttons.pop();
269
270    let vehicle = Vehicle::get(&app.session.current_vehicle);
271    let (max_speed, max_energy) = Vehicle::max_stats();
272
273    Panel::new_builder(Widget::col(vec![
274        Line("Pick Santa's vehicle")
275            .small_heading()
276            .into_widget(ctx),
277        Widget::row(buttons),
278        Line(&vehicle.name).small_heading().into_widget(ctx),
279        Widget::row(vec![
280            "Speed:".text_widget(ctx),
281            custom_bar(
282                ctx,
283                app.session.colors.boost,
284                vehicle.speed / max_speed,
285                Text::new(),
286            )
287            .align_right(),
288        ]),
289        Widget::row(vec![
290            "Energy:".text_widget(ctx),
291            custom_bar(
292                ctx,
293                app.session.colors.energy,
294                (vehicle.max_energy as f64) / (max_energy as f64),
295                Text::new(),
296            )
297            .align_right(),
298        ]),
299    ]))
300    .aligned(HorizontalAlignment::RightInset, VerticalAlignment::TopInset)
301    .build(ctx)
302}
303
304fn make_upzone_panel(ctx: &mut EventCtx, app: &App, num_picked: usize) -> Panel {
305    // Don't overwhelm players on the very first level.
306    if app.session.upzones_unlocked == 0 {
307        return Panel::new_builder(
308            ctx.style()
309                .btn_solid_primary
310                .text("Start game")
311                .hotkey(Key::Enter)
312                .build_def(ctx)
313                .container(),
314        )
315        .aligned(
316            HorizontalAlignment::RightInset,
317            VerticalAlignment::BottomInset,
318        )
319        .build(ctx);
320    }
321
322    Panel::new_builder(Widget::col(vec![
323        Widget::row(vec![
324            Line("Upzoning").small_heading().into_widget(ctx),
325            ctx.style()
326                .btn_plain
327                .icon("system/assets/tools/info.svg")
328                .build_widget(ctx, "help")
329                .align_right(),
330        ]),
331        Widget::row(vec![
332            Image::from_path("system/assets/tools/mouse.svg").into_widget(ctx),
333            Line("Select the houses you want to turn into stores")
334                .fg(ctx.style().text_hotkey_color)
335                .into_widget(ctx),
336        ]),
337        Widget::row(vec![
338            "Upzones chosen:".text_widget(ctx),
339            make_bar(ctx, Color::PINK, num_picked, app.session.upzones_unlocked),
340        ]),
341        Widget::row(vec![
342            ctx.style()
343                .btn_outline
344                .text("Randomly choose upzones")
345                .disabled(num_picked == app.session.upzones_unlocked)
346                .build_def(ctx),
347            ctx.style()
348                .btn_outline
349                .text("Clear upzones")
350                .disabled(num_picked == 0)
351                .build_def(ctx)
352                .align_right(),
353        ]),
354        if num_picked == app.session.upzones_unlocked {
355            ctx.style()
356                .btn_solid_primary
357                .text("Start game")
358                .hotkey(Key::Enter)
359                .build_def(ctx)
360        } else {
361            ctx.style()
362                .btn_solid_primary
363                .text("Finish upzoning before playing")
364                .disabled(true)
365                .build_def(ctx)
366        },
367    ]))
368    .aligned(
369        HorizontalAlignment::RightInset,
370        VerticalAlignment::BottomInset,
371    )
372    .build(ctx)
373}
374
375fn explain_upzoning(ctx: &mut EventCtx) -> Transition {
376    Transition::Push(PopupMsg::new_state(
377        ctx,
378        "Upzoning power unlocked",
379        vec![
380            "It's hard to deliver to houses far away from shops, isn't it?",
381            "You've gained the power to change the zoning code for a residential building.",
382            "You can now transform a single-family house into a multi-use building,",
383            "with shops on the ground floor, and people living above.",
384            "",
385            "Where should you place the new store?",
386        ],
387    ))
388}