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 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 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 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}