1use core::future::Future;
2use core::pin::Pin;
3
4use anyhow::Result;
5use rand_xorshift::XorShiftRng;
6
7use abstio::MapName;
8use abstutil::Timer;
9use geom::Duration;
10use map_model::{EditCmd, EditIntersectionControl, MapEdits};
11use sim::ScenarioGenerator;
12use synthpop::{OrigPersonID, Scenario, ScenarioModifier};
13use widgetry::{
14 lctrl, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, Panel, State, TextExt, Widget,
15};
16
17pub use self::freeform::spawn_agents_around;
18pub use self::tutorial::{Tutorial, TutorialPointer, TutorialState};
19use crate::app::App;
20use crate::app::Transition;
21use crate::challenges::{Challenge, ChallengesPicker};
22use crate::edit::SaveEdits;
23use crate::pregame::TitleScreen;
24use crate::sandbox::{Actions, SandboxControls, SandboxMode};
25
26mod actdev;
28pub mod commute;
29pub mod fix_traffic_signals;
30pub mod freeform;
31pub mod play_scenario;
32pub mod tutorial;
33
34#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
35pub enum GameplayMode {
36 Freeform(MapName),
38 PlayScenario(MapName, String, Vec<ScenarioModifier>),
40 FixTrafficSignals,
41 OptimizeCommute(OrigPersonID, Duration),
42 Actdev(MapName, String, bool),
44
45 Tutorial(TutorialPointer),
47}
48
49pub trait GameplayState: downcast_rs::Downcast {
50 fn event(
51 &mut self,
52 ctx: &mut EventCtx,
53 app: &mut App,
54 controls: &mut SandboxControls,
55 actions: &mut Actions,
56 ) -> Option<Transition>;
57 fn draw(&self, g: &mut GfxCtx, app: &App);
58 fn on_destroy(&self, _: &mut App) {}
59 fn recreate_panels(&mut self, ctx: &mut EventCtx, app: &App);
60
61 fn can_move_canvas(&self) -> bool {
62 true
63 }
64 fn can_examine_objects(&self) -> bool {
65 true
66 }
67 fn has_common(&self) -> bool {
68 true
69 }
70 fn has_tool_panel(&self) -> bool {
71 true
72 }
73 fn has_time_panel(&self) -> bool {
74 true
75 }
76 fn has_minimap(&self) -> bool {
77 true
78 }
79}
80downcast_rs::impl_downcast!(GameplayState);
81
82pub enum LoadScenario {
83 Nothing,
84 Path(String),
85 Scenario(Scenario),
86 #[cfg(target_arch = "wasm32")]
89 Future(Pin<Box<dyn Future<Output = Result<Box<dyn Send + FnOnce(&App) -> Scenario>>>>>),
90 #[cfg(not(target_arch = "wasm32"))]
91 Future(Pin<Box<dyn Send + Future<Output = Result<Box<dyn Send + FnOnce(&App) -> Scenario>>>>>),
92}
93
94impl GameplayMode {
95 pub fn map_name(&self) -> MapName {
96 match self {
97 GameplayMode::Freeform(ref name) => name.clone(),
98 GameplayMode::PlayScenario(ref name, _, _) => name.clone(),
99 GameplayMode::FixTrafficSignals => MapName::seattle("downtown"),
100 GameplayMode::OptimizeCommute(_, _) => MapName::seattle("montlake"),
101 GameplayMode::Tutorial(_) => MapName::seattle("montlake"),
102 GameplayMode::Actdev(ref name, _, _) => name.clone(),
103 }
104 }
105
106 pub fn scenario(&self, app: &App, mut rng: XorShiftRng, timer: &mut Timer) -> LoadScenario {
107 let map = &app.primary.map;
108 let name = match self {
109 GameplayMode::Freeform(_) => {
110 let mut s = Scenario::empty(map, "empty");
111 s.only_seed_buses = None;
112 return LoadScenario::Scenario(s);
113 }
114 GameplayMode::PlayScenario(_, ref scenario, _) => scenario.to_string(),
115 GameplayMode::Tutorial(current) => {
116 return match Tutorial::scenario(app, *current) {
117 Some(generator) => {
118 LoadScenario::Scenario(generator.generate(map, &mut rng, timer))
119 }
120 None => LoadScenario::Nothing,
121 };
122 }
123 GameplayMode::Actdev(_, ref scenario, bg_traffic) => {
124 if *bg_traffic {
125 format!("{}_with_bg", scenario)
126 } else {
127 scenario.to_string()
128 }
129 }
130 GameplayMode::FixTrafficSignals | GameplayMode::OptimizeCommute(_, _) => {
131 "weekday".to_string()
132 }
133 };
134 if name == "random" {
135 LoadScenario::Scenario(ScenarioGenerator::small_run(map).generate(map, &mut rng, timer))
136 } else if name == "home_to_work" {
137 LoadScenario::Scenario(ScenarioGenerator::proletariat_robot(map, &mut rng, timer))
138 } else if name == "census" {
139 let map_area = map.get_boundary_polygon().clone();
140 let map_bounds = map.get_gps_bounds().clone();
141 let mut rng = sim::fork_rng(&mut rng);
142
143 LoadScenario::Future(Box::pin(async move {
144 let areas = popdat::CensusArea::fetch_all_for_map(&map_area, &map_bounds).await?;
145
146 let scenario_from_app: Box<dyn Send + FnOnce(&App) -> Scenario> =
147 Box::new(move |app: &App| {
148 let config = popdat::Config::default();
149 popdat::generate_scenario(
150 "typical monday",
151 areas,
152 config,
153 &app.primary.map,
154 &mut rng,
155 )
156 });
157
158 Ok(scenario_from_app)
159 }))
160 } else {
161 LoadScenario::Path(abstio::path_scenario(map.get_name(), &name))
162 }
163 }
164
165 pub fn can_edit_roads(&self) -> bool {
166 !matches!(self, GameplayMode::FixTrafficSignals)
167 }
168
169 pub fn can_edit_stop_signs(&self) -> bool {
170 !matches!(self, GameplayMode::FixTrafficSignals)
171 }
172
173 pub fn can_jump_to_time(&self) -> bool {
174 !matches!(self, GameplayMode::Freeform(_))
175 }
176
177 pub fn allows(&self, edits: &MapEdits) -> bool {
178 for cmd in &edits.commands {
179 match cmd {
180 EditCmd::ChangeRoad { .. } => {
181 if !self.can_edit_roads() {
182 return false;
183 }
184 }
185 EditCmd::ChangeIntersection {
186 ref new, ref old, ..
187 } => {
188 match new.control {
189 EditIntersectionControl::StopSign(_) | EditIntersectionControl::Closed => {
191 if !self.can_edit_stop_signs() {
192 return false;
193 }
194 }
195 _ => {}
196 }
197 if old.crosswalks != new.crosswalks && !self.can_edit_stop_signs() {
199 return false;
200 }
201 }
202 EditCmd::ChangeRouteSchedule { .. } => {}
203 }
204 }
205 true
206 }
207
208 pub fn initialize(&self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn GameplayState> {
211 match self {
212 GameplayMode::Freeform(_) => freeform::Freeform::new_state(ctx, app),
213 GameplayMode::PlayScenario(_, ref scenario, ref modifiers) => {
214 play_scenario::PlayScenario::new_state(ctx, app, scenario, modifiers.clone())
215 }
216 GameplayMode::FixTrafficSignals => {
217 fix_traffic_signals::FixTrafficSignals::new_state(ctx)
218 }
219 GameplayMode::OptimizeCommute(p, goal) => {
220 commute::OptimizeCommute::new_state(ctx, app, *p, *goal)
221 }
222 GameplayMode::Tutorial(current) => Tutorial::make_gameplay(ctx, app, *current),
223 GameplayMode::Actdev(_, ref scenario, bg_traffic) => {
224 actdev::Actdev::new_state(ctx, scenario.clone(), *bg_traffic)
225 }
226 }
227 }
228}
229
230fn challenge_header(ctx: &mut EventCtx, title: &str) -> Widget {
231 Widget::row(vec![
232 Line(title).small_heading().into_widget(ctx).centered_vert(),
233 ctx.style()
234 .btn_plain
235 .icon("system/assets/tools/info.svg")
236 .build_widget(ctx, "instructions")
237 .centered_vert(),
238 Widget::vert_separator(ctx, 50.0),
239 ctx.style()
240 .btn_outline
241 .icon_text("system/assets/tools/pencil.svg", "Edit map")
242 .hotkey(lctrl(Key::E))
243 .build_widget(ctx, "edit map")
244 .centered_vert(),
245 ])
246 .padding(5)
247}
248
249pub struct FinalScore {
250 panel: Panel,
251 retry: GameplayMode,
252 next_mode: Option<GameplayMode>,
253
254 chose_next: bool,
255 chose_back_to_challenges: bool,
256}
257
258impl FinalScore {
259 pub fn new_state(
260 ctx: &mut EventCtx,
261 msg: String,
262 mode: GameplayMode,
263 next_mode: Option<GameplayMode>,
264 ) -> Box<dyn State<App>> {
265 Box::new(FinalScore {
266 panel: Panel::new_builder(Widget::row(vec![
267 GeomBatch::load_svg(ctx, "system/assets/characters/boss.svg.gz")
268 .scale(0.75)
269 .autocrop()
270 .into_widget(ctx)
271 .container()
272 .section(ctx),
273 Widget::col(vec![
274 msg.text_widget(ctx),
275 ctx.style()
277 .btn_outline
278 .text("Keep simulating")
279 .build_def(ctx),
280 ctx.style().btn_outline.text("Try again").build_def(ctx),
281 if next_mode.is_some() {
282 ctx.style()
283 .btn_solid_primary
284 .text("Next challenge")
285 .build_def(ctx)
286 } else {
287 Widget::nothing()
288 },
289 ctx.style()
290 .btn_outline
291 .text("Back to challenges")
292 .build_def(ctx),
293 ])
294 .section(ctx),
295 ]))
296 .build(ctx),
297 retry: mode,
298 next_mode,
299 chose_next: false,
300 chose_back_to_challenges: false,
301 })
302 }
303}
304
305impl State<App> for FinalScore {
306 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
307 if let Outcome::Clicked(x) = self.panel.event(ctx) {
308 match x.as_ref() {
309 "Keep simulating" => {
310 return Transition::Pop;
311 }
312 "Try again" => {
313 return Transition::Multi(vec![
314 Transition::Pop,
315 Transition::Replace(SandboxMode::simple_new(app, self.retry.clone())),
316 ]);
317 }
318 "Next challenge" => {
319 self.chose_next = true;
320 if app.primary.map.unsaved_edits() {
321 return Transition::Push(SaveEdits::new_state(
322 ctx,
323 app,
324 "Do you want to save your proposal first?",
325 true,
326 None,
327 Box::new(|_, _| {}),
328 ));
329 }
330 }
331 "Back to challenges" => {
332 self.chose_back_to_challenges = true;
333 if app.primary.map.unsaved_edits() {
334 return Transition::Push(SaveEdits::new_state(
335 ctx,
336 app,
337 "Do you want to save your proposal first?",
338 true,
339 None,
340 Box::new(|_, _| {}),
341 ));
342 }
343 }
344 _ => unreachable!(),
345 }
346 }
347
348 if self.chose_next || self.chose_back_to_challenges {
349 app.clear_everything(ctx);
350 }
351
352 if self.chose_next {
353 return Transition::Clear(vec![
354 TitleScreen::new_state(ctx, app),
355 SandboxMode::simple_new(app, self.next_mode.clone().unwrap()),
357 (Challenge::find(self.next_mode.as_ref().unwrap())
358 .0
359 .cutscene
360 .unwrap())(ctx, app, self.next_mode.as_ref().unwrap()),
361 ]);
362 }
363 if self.chose_back_to_challenges {
364 return Transition::Clear(vec![
365 TitleScreen::new_state(ctx, app),
366 ChallengesPicker::new_state(ctx, app),
367 ]);
368 }
369
370 Transition::Keep
371 }
372
373 fn draw(&self, g: &mut GfxCtx, _: &App) {
374 self.panel.draw(g);
375 }
376}