game/sandbox/gameplay/
mod.rs

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
26// TODO pub so challenges can grab cutscenes and SandboxMode can dispatch to actions. Weird?
27mod 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    // TODO Maybe this should be "sandbox"
37    Freeform(MapName),
38    // Map name, scenario name
39    PlayScenario(MapName, String, Vec<ScenarioModifier>),
40    FixTrafficSignals,
41    OptimizeCommute(OrigPersonID, Duration),
42    // Map name, scenario name, background traffic
43    Actdev(MapName, String, bool),
44
45    // current
46    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    // wasm futures are not `Send`, since they all ultimately run on the browser's single threaded
87    // runloop
88    #[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                        // TODO Conflating construction
190                        EditIntersectionControl::StopSign(_) | EditIntersectionControl::Closed => {
191                            if !self.can_edit_stop_signs() {
192                                return false;
193                            }
194                        }
195                        _ => {}
196                    }
197                    // TODO Another hack to see if we can only edit signal timing
198                    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    /// Must be called after the scenario has been setup. The caller will call recreate_panels
209    /// after this, so each constructor doesn't need to.
210    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                    // TODO Adjust wording
276                    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                // Constructing the cutscene doesn't require the map/scenario to be loaded.
356                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}