game/sandbox/gameplay/
tutorial.rs

1use std::collections::BTreeSet;
2
3use crate::ID;
4use abstio::MapName;
5use abstutil::Timer;
6use geom::{ArrowCap, Distance, Duration, PolyLine, Pt2D, Time};
7use map_gui::load::MapLoader;
8use map_gui::tools::Minimap;
9use map_model::{osm, BuildingID, Map, OriginalRoad, Position};
10use sim::{AgentID, BorderSpawnOverTime, CarID, ScenarioGenerator, SpawnOverTime, VehicleType};
11use synthpop::{IndividTrip, PersonSpec, Scenario, TripEndpoint, TripMode, TripPurpose};
12use widgetry::tools::PopupMsg;
13use widgetry::{
14    hotkeys, lctrl, Color, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Image, Key, Line,
15    Outcome, Panel, ScreenPt, State, Text, TextExt, VerticalAlignment, Widget,
16};
17
18use crate::app::{App, Transition};
19use crate::challenges::cutscene::CutsceneBuilder;
20use crate::common::{tool_panel, Warping};
21use crate::edit::EditMode;
22use crate::sandbox::gameplay::{GameplayMode, GameplayState};
23use crate::sandbox::{
24    maybe_exit_sandbox, spawn_agents_around, Actions, MinimapController, SandboxControls,
25    SandboxMode, TimePanel,
26};
27
28const ESCORT: CarID = CarID {
29    id: 0,
30    vehicle_type: VehicleType::Car,
31};
32const CAR_BIKE_CONTENTION_GOAL: Duration = Duration::const_seconds(15.0);
33
34pub struct Tutorial {
35    top_right: Panel,
36    last_finished_task: Task,
37
38    msg_panel: Option<Panel>,
39    warped: bool,
40}
41
42#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
43pub struct TutorialPointer {
44    pub stage: usize,
45    // Index into messages. messages.len() means the actual task.
46    pub part: usize,
47}
48
49impl TutorialPointer {
50    pub fn new(stage: usize, part: usize) -> TutorialPointer {
51        TutorialPointer { stage, part }
52    }
53}
54
55impl Tutorial {
56    /// Launches the tutorial gameplay along with its cutscene
57    pub fn start(ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
58        MapLoader::new_state(
59            ctx,
60            app,
61            MapName::seattle("montlake"),
62            Box::new(|ctx, app| {
63                Tutorial::initialize(ctx, app);
64
65                Transition::Multi(vec![
66                    Transition::Pop,
67                    Transition::Push(SandboxMode::simple_new(
68                        app,
69                        GameplayMode::Tutorial(
70                            app.session
71                                .tutorial
72                                .as_ref()
73                                .map(|tut| tut.current)
74                                .unwrap_or_else(|| TutorialPointer::new(0, 0)),
75                        ),
76                    )),
77                    Transition::Push(intro_story(ctx)),
78                ])
79            }),
80        )
81    }
82
83    /// Idempotent. This must be called before `make_gameplay` or `scenario`. The current map must
84    /// be montlake.
85    pub fn initialize(ctx: &mut EventCtx, app: &mut App) {
86        if app.session.tutorial.is_none() {
87            app.session.tutorial = Some(TutorialState::new(ctx, app));
88        }
89    }
90
91    pub fn make_gameplay(
92        ctx: &mut EventCtx,
93        app: &mut App,
94        current: TutorialPointer,
95    ) -> Box<dyn GameplayState> {
96        let mut tut = app.session.tutorial.take().unwrap();
97        tut.current = current;
98        let state = tut.make_state(ctx, app);
99        app.session.tutorial = Some(tut);
100        state
101    }
102
103    pub fn scenario(app: &App, current: TutorialPointer) -> Option<ScenarioGenerator> {
104        app.session.tutorial.as_ref().unwrap().stages[current.stage]
105            .make_scenario
106            .clone()
107    }
108
109    fn inner_event(
110        &mut self,
111        ctx: &mut EventCtx,
112        app: &mut App,
113        controls: &mut SandboxControls,
114        tut: &mut TutorialState,
115    ) -> Option<Transition> {
116        // First of all, might need to initiate warping
117        if !self.warped {
118            if let Some((ref id, zoom)) = tut.stage().warp_to {
119                self.warped = true;
120                return Some(Transition::Push(Warping::new_state(
121                    ctx,
122                    app.primary.canonical_point(id.clone()).unwrap(),
123                    Some(zoom),
124                    None,
125                    &mut app.primary,
126                )));
127            }
128        }
129
130        if let Outcome::Clicked(x) = self.top_right.event(ctx) {
131            match x.as_ref() {
132                "Quit" => {
133                    return Some(maybe_exit_sandbox(ctx));
134                }
135                "previous tutorial" => {
136                    tut.current = TutorialPointer::new(tut.current.stage - 1, 0);
137                    return Some(transition(app, tut));
138                }
139                "next tutorial" => {
140                    tut.current = TutorialPointer::new(tut.current.stage + 1, 0);
141                    return Some(transition(app, tut));
142                }
143                "instructions" => {
144                    tut.current = TutorialPointer::new(tut.current.stage, 0);
145                    return Some(transition(app, tut));
146                }
147                "edit map" => {
148                    // TODO Ideally this would be an inactive button in message states
149                    if self.msg_panel.is_none() {
150                        let mode = GameplayMode::Tutorial(tut.current);
151                        return Some(Transition::Push(EditMode::new_state(ctx, app, mode)));
152                    }
153                }
154                _ => unreachable!(),
155            }
156        }
157
158        if let Some(ref mut msg) = self.msg_panel {
159            match msg.event(ctx) {
160                Outcome::Clicked(x) => match x.as_ref() {
161                    "previous message" => {
162                        tut.prev();
163                        return Some(transition(app, tut));
164                    }
165                    "next message" | "Try it" => {
166                        tut.next();
167                        return Some(transition(app, tut));
168                    }
169                    _ => unreachable!(),
170                },
171                _ => {
172                    // Don't allow other interactions
173                    return Some(Transition::Keep);
174                }
175            }
176        }
177
178        // Interaction things
179        if tut.interaction() == Task::Camera {
180            if app.primary.current_selection == Some(ID::Building(tut.fire_station))
181                && app.per_obj.left_click(ctx, "put out the... fire?")
182            {
183                tut.next();
184                return Some(transition(app, tut));
185            }
186        } else if tut.interaction() == Task::InspectObjects {
187            // TODO Have to wiggle the mouse or something after opening the panel, because of the
188            // order in SandboxMode.
189            match controls.common.as_ref().unwrap().info_panel_open(app) {
190                Some(ID::Lane(l)) => {
191                    if app.primary.map.get_l(l).is_biking() && !tut.inspected_bike_lane {
192                        tut.inspected_bike_lane = true;
193                        self.top_right = tut.make_top_right(ctx, false);
194                    }
195                }
196                Some(ID::Building(_)) => {
197                    if !tut.inspected_building {
198                        tut.inspected_building = true;
199                        self.top_right = tut.make_top_right(ctx, false);
200                    }
201                }
202                Some(ID::Intersection(i)) => {
203                    let i = app.primary.map.get_i(i);
204                    if i.is_stop_sign() && !tut.inspected_stop_sign {
205                        tut.inspected_stop_sign = true;
206                        self.top_right = tut.make_top_right(ctx, false);
207                    }
208                    if i.is_border() && !tut.inspected_border {
209                        tut.inspected_border = true;
210                        self.top_right = tut.make_top_right(ctx, false);
211                    }
212                }
213                _ => {}
214            }
215            if tut.inspected_bike_lane
216                && tut.inspected_building
217                && tut.inspected_stop_sign
218                && tut.inspected_border
219            {
220                tut.next();
221                return Some(transition(app, tut));
222            }
223        } else if tut.interaction() == Task::TimeControls {
224            if app.primary.sim.time() >= Time::START_OF_DAY + Duration::hours(17) {
225                tut.next();
226                return Some(transition(app, tut));
227            }
228        } else if tut.interaction() == Task::PauseResume {
229            let is_paused = controls.time_panel.as_ref().unwrap().is_paused();
230            if tut.was_paused && !is_paused {
231                tut.was_paused = false;
232            }
233            if !tut.was_paused && is_paused {
234                tut.num_pauses += 1;
235                tut.was_paused = true;
236                self.top_right = tut.make_top_right(ctx, false);
237            }
238            if tut.num_pauses == 3 {
239                tut.next();
240                return Some(transition(app, tut));
241            }
242        } else if tut.interaction() == Task::Escort {
243            let following_car =
244                controls.common.as_ref().unwrap().info_panel_open(app) == Some(ID::Car(ESCORT));
245            let is_parked = app
246                .primary
247                .sim
248                .agent_to_trip(AgentID::Car(ESCORT))
249                .is_none();
250            if !tut.car_parked && is_parked && tut.following_car {
251                tut.car_parked = true;
252                self.top_right = tut.make_top_right(ctx, false);
253            }
254
255            if following_car && !tut.following_car {
256                // TODO There's a delay of one event before the checklist updates, because the
257                // info panel opening happens at the end of the event. Not a big deal.
258                tut.following_car = true;
259                self.top_right = tut.make_top_right(ctx, false);
260            }
261
262            if tut.prank_done {
263                tut.next();
264                return Some(transition(app, tut));
265            }
266        } else if tut.interaction() == Task::LowParking {
267            if tut.parking_found {
268                tut.next();
269                return Some(transition(app, tut));
270            }
271        } else if tut.interaction() == Task::WatchBikes {
272            if app.primary.sim.time() >= Time::START_OF_DAY + Duration::minutes(3) {
273                tut.next();
274                return Some(transition(app, tut));
275            }
276        } else if tut.interaction() == Task::FixBikes {
277            if app.primary.sim.is_done() {
278                let mut before = Duration::ZERO;
279                let mut after = Duration::ZERO;
280                for (_, b, a, _) in app
281                    .primary
282                    .sim
283                    .get_analytics()
284                    .both_finished_trips(app.primary.sim.get_end_of_day(), app.prebaked())
285                {
286                    before = before.max(b);
287                    after = after.max(a);
288                }
289                if !tut.score_delivered {
290                    tut.score_delivered = true;
291                    if before == after {
292                        return Some(Transition::Push(PopupMsg::new_state(
293                            ctx,
294                            "All trips completed",
295                            vec![
296                                "Your changes didn't affect anything!",
297                                "Try editing the map to create some bike lanes.",
298                            ],
299                        )));
300                    }
301                    if after > before {
302                        return Some(Transition::Push(PopupMsg::new_state(
303                            ctx,
304                            "All trips completed",
305                            vec![
306                                "Your changes made things worse!".to_string(),
307                                format!(
308                                    "All trips originally finished in {}, but now they took {}",
309                                    before, after
310                                ),
311                                "".to_string(),
312                                "Try again!".to_string(),
313                            ],
314                        )));
315                    }
316                    if before - after < CAR_BIKE_CONTENTION_GOAL {
317                        return Some(Transition::Push(PopupMsg::new_state(
318                            ctx,
319                            "All trips completed",
320                            vec![
321                                "Nice, you helped things a bit!".to_string(),
322                                format!(
323                                    "All trips originally took {}, but now they took {}",
324                                    before, after
325                                ),
326                                "".to_string(),
327                                "See if you can do a little better though.".to_string(),
328                            ],
329                        )));
330                    }
331                    return Some(Transition::Push(PopupMsg::new_state(
332                        ctx,
333                        "All trips completed",
334                        vec![format!(
335                            "Awesome! All trips originally took {}, but now they only took {}",
336                            before, after
337                        )],
338                    )));
339                }
340                if before - after >= CAR_BIKE_CONTENTION_GOAL {
341                    tut.next();
342                }
343                return Some(transition(app, tut));
344            }
345        } else if tut.interaction() == Task::Done {
346            // If the player chooses to stay here, at least go back to the message panel.
347            tut.prev();
348            return Some(maybe_exit_sandbox(ctx));
349        }
350
351        None
352    }
353}
354
355impl GameplayState for Tutorial {
356    fn event(
357        &mut self,
358        ctx: &mut EventCtx,
359        app: &mut App,
360        controls: &mut SandboxControls,
361        _: &mut Actions,
362    ) -> Option<Transition> {
363        // Dance around borrow-checker issues
364        let mut tut = app.session.tutorial.take().unwrap();
365
366        // The arrows get screwy when window size changes.
367        let window_dims = (ctx.canvas.window_width, ctx.canvas.window_height);
368        if window_dims != tut.window_dims {
369            tut.stages = TutorialState::new(ctx, app).stages;
370            tut.window_dims = window_dims;
371        }
372
373        let result = self.inner_event(ctx, app, controls, &mut tut);
374        app.session.tutorial = Some(tut);
375        result
376    }
377
378    fn draw(&self, g: &mut GfxCtx, app: &App) {
379        let tut = app.session.tutorial.as_ref().unwrap();
380
381        self.top_right.draw(g);
382
383        if let Some(ref msg) = self.msg_panel {
384            // Arrows underneath the message panel, but on top of other panels
385            if let Some(msg) = tut.message() {
386                if let Some(ref fxn) = msg.arrow {
387                    let pt = (fxn)(g, app);
388                    g.fork_screenspace();
389                    if let Ok(pl) = PolyLine::new(vec![
390                        self.msg_panel
391                            .as_ref()
392                            .unwrap()
393                            .center_of("next message")
394                            .to_pt(),
395                        pt,
396                    ]) {
397                        g.draw_polygon(
398                            Color::RED,
399                            pl.make_arrow(Distance::meters(20.0), ArrowCap::Triangle),
400                        );
401                    }
402                    g.unfork();
403                }
404            }
405
406            msg.draw(g);
407        }
408
409        // Special things
410        if tut.interaction() == Task::Camera {
411            let fire = GeomBatch::load_svg(g, "system/assets/tools/fire.svg")
412                .scale(if g.canvas.is_unzoomed() { 0.2 } else { 0.1 })
413                .autocrop()
414                .centered_on(app.primary.map.get_b(tut.fire_station).polygon.polylabel());
415            let offset = -fire.get_dims().height / 2.0;
416            fire.translate(0.0, offset).draw(g);
417
418            g.draw_polygon(
419                Color::hex("#FEDE17"),
420                app.primary.map.get_b(tut.fire_station).polygon.clone(),
421            );
422        } else if tut.interaction() == Task::Escort {
423            GeomBatch::load_svg(g, "system/assets/tools/star.svg")
424                .scale(0.1)
425                .centered_on(
426                    app.primary
427                        .sim
428                        .canonical_pt_for_agent(AgentID::Car(ESCORT), &app.primary.map)
429                        .unwrap(),
430                )
431                .draw(g);
432        }
433    }
434
435    fn recreate_panels(&mut self, ctx: &mut EventCtx, app: &App) {
436        let tut = app.session.tutorial.as_ref().unwrap();
437        self.top_right = tut.make_top_right(ctx, self.last_finished_task >= Task::WatchBikes);
438
439        // Time can't pass while self.msg_panel is active
440    }
441
442    fn can_move_canvas(&self) -> bool {
443        self.msg_panel.is_none()
444    }
445    fn can_examine_objects(&self) -> bool {
446        self.last_finished_task >= Task::WatchBikes
447    }
448    fn has_common(&self) -> bool {
449        self.last_finished_task >= Task::Camera
450    }
451    fn has_tool_panel(&self) -> bool {
452        true
453    }
454    fn has_time_panel(&self) -> bool {
455        self.last_finished_task >= Task::InspectObjects
456    }
457    fn has_minimap(&self) -> bool {
458        self.last_finished_task >= Task::Escort
459    }
460}
461
462#[derive(PartialEq, PartialOrd, Clone, Copy)]
463enum Task {
464    Nil,
465    Camera,
466    InspectObjects,
467    TimeControls,
468    PauseResume,
469    Escort,
470    LowParking,
471    WatchBikes,
472    FixBikes,
473    Done,
474}
475
476impl Task {
477    fn top_txt(self, ctx: &EventCtx, state: &TutorialState) -> Text {
478        let hotkey_color = ctx.style().text_hotkey_color;
479
480        let simple = match self {
481            Task::Nil => unreachable!(),
482            Task::Camera => "Put out the fire at the fire station",
483            Task::InspectObjects => {
484                let mut txt = Text::from("Find one of each:");
485                for (name, done) in [
486                    ("bike lane", state.inspected_bike_lane),
487                    ("building", state.inspected_building),
488                    ("intersection with stop sign", state.inspected_stop_sign),
489                    ("intersection on the map border", state.inspected_border),
490                ] {
491                    if done {
492                        txt.add_line(Line(format!("[X] {}", name)).fg(hotkey_color));
493                    } else {
494                        txt.add_line(format!("[ ] {}", name));
495                    }
496                }
497                return txt;
498            }
499            Task::TimeControls => "Wait until after 5pm",
500            Task::PauseResume => {
501                let mut txt = Text::from("[ ] Pause/resume ");
502                txt.append(Line(format!("{} times", 3 - state.num_pauses)).fg(hotkey_color));
503                return txt;
504            }
505            Task::Escort => {
506                // Inspect the target car, wait for them to park, draw WASH ME on the window
507                let mut txt = Text::new();
508                if state.following_car {
509                    txt.add_line(Line("[X] follow the target car").fg(hotkey_color));
510                } else {
511                    txt.add_line("[ ] follow the target car");
512                }
513                if state.car_parked {
514                    txt.add_line(Line("[X] wait for them to park").fg(hotkey_color));
515                } else {
516                    txt.add_line("[ ] wait for them to park");
517                }
518                if state.prank_done {
519                    txt.add_line(
520                        Line("[X] click car and press c to draw WASH ME").fg(hotkey_color),
521                    );
522                } else {
523                    txt.add_line("[ ] click car and press ");
524                    txt.append(Line(Key::C.describe()).fg(hotkey_color));
525                    txt.append(Line(" to draw WASH ME"));
526                }
527                return txt;
528            }
529            Task::LowParking => {
530                let mut txt = Text::from("1) Find a road with almost no parking spots available");
531                txt.add_line("2) Click it and press ");
532                txt.append(Line(Key::C.describe()).fg(hotkey_color));
533                txt.append(Line(" to check the occupancy"));
534                return txt;
535            }
536            Task::WatchBikes => "Watch for 3 minutes",
537            Task::FixBikes => {
538                return Text::from(format!(
539                    "[ ] Complete all trips {} faster",
540                    CAR_BIKE_CONTENTION_GOAL
541                ));
542            }
543            Task::Done => "Tutorial complete!",
544        };
545        Text::from(simple)
546    }
547
548    fn label(self) -> &'static str {
549        match self {
550            Task::Nil => unreachable!(),
551            Task::Camera => "Moving the drone",
552            Task::InspectObjects => "Interacting with objects",
553            Task::TimeControls => "Passing the time",
554            Task::PauseResume => "Pausing/resuming",
555            Task::Escort => "Following people",
556            Task::LowParking => "Exploring map layers",
557            Task::WatchBikes => "Observing a problem",
558            Task::FixBikes => "Editing lanes",
559            Task::Done => "Tutorial complete!",
560        }
561    }
562}
563
564struct Stage {
565    messages: Vec<Message>,
566    task: Task,
567    warp_to: Option<(ID, f64)>,
568    custom_spawn: Option<Box<dyn Fn(&mut App)>>,
569    make_scenario: Option<ScenarioGenerator>,
570}
571
572struct Message {
573    txt: Text,
574    aligned: HorizontalAlignment,
575    arrow: Option<Box<dyn Fn(&GfxCtx, &App) -> Pt2D>>,
576    icon: Option<&'static str>,
577}
578
579impl Message {
580    fn new(txt: Text) -> Message {
581        Message {
582            txt,
583            aligned: HorizontalAlignment::Center,
584            arrow: None,
585            icon: None,
586        }
587    }
588
589    fn arrow(mut self, pt: ScreenPt) -> Message {
590        self.arrow = Some(Box::new(move |_, _| pt.to_pt()));
591        self
592    }
593
594    fn dynamic_arrow(mut self, arrow: Box<dyn Fn(&GfxCtx, &App) -> Pt2D>) -> Message {
595        self.arrow = Some(arrow);
596        self
597    }
598
599    fn icon(mut self, path: &'static str) -> Message {
600        self.icon = Some(path);
601        self
602    }
603
604    fn left_aligned(mut self) -> Message {
605        self.aligned = HorizontalAlignment::Left;
606        self
607    }
608}
609
610impl Stage {
611    fn new(task: Task) -> Stage {
612        Stage {
613            messages: Vec::new(),
614            task,
615            warp_to: None,
616            custom_spawn: None,
617            make_scenario: None,
618        }
619    }
620
621    fn msg(mut self, msg: Message) -> Stage {
622        self.messages.push(msg);
623        self
624    }
625
626    fn warp_to(mut self, id: ID, zoom: Option<f64>) -> Stage {
627        assert!(self.warp_to.is_none());
628        self.warp_to = Some((id, zoom.unwrap_or(4.0)));
629        self
630    }
631
632    fn custom_spawn(mut self, cb: Box<dyn Fn(&mut App)>) -> Stage {
633        assert!(self.custom_spawn.is_none());
634        self.custom_spawn = Some(cb);
635        self
636    }
637
638    fn scenario(mut self, generator: ScenarioGenerator) -> Stage {
639        assert!(self.make_scenario.is_none());
640        self.make_scenario = Some(generator);
641        self
642    }
643}
644
645pub struct TutorialState {
646    stages: Vec<Stage>,
647    pub current: TutorialPointer,
648
649    window_dims: (f64, f64),
650
651    // Goofy state for just some stages.
652    inspected_bike_lane: bool,
653    inspected_building: bool,
654    inspected_stop_sign: bool,
655    inspected_border: bool,
656
657    was_paused: bool,
658    num_pauses: usize,
659
660    following_car: bool,
661    car_parked: bool,
662    prank_done: bool,
663
664    parking_found: bool,
665
666    score_delivered: bool,
667
668    fire_station: BuildingID,
669}
670
671fn make_bike_lane_scenario(map: &Map) -> ScenarioGenerator {
672    let mut s = ScenarioGenerator::empty("car vs bike contention");
673    s.border_spawn_over_time.push(BorderSpawnOverTime {
674        num_peds: 0,
675        num_cars: 10,
676        num_bikes: 10,
677        percent_use_transit: 0.0,
678        start_time: Time::START_OF_DAY,
679        stop_time: Time::START_OF_DAY + Duration::seconds(10.0),
680        start_from_border: map.find_i_by_osm_id(osm::NodeID(3005680098)).unwrap(),
681        goal: Some(TripEndpoint::Building(
682            map.find_b_by_osm_id(bldg(217699501)).unwrap(),
683        )),
684    });
685    s
686}
687
688fn transition(app: &mut App, tut: &mut TutorialState) -> Transition {
689    tut.reset_state();
690    let mode = GameplayMode::Tutorial(tut.current);
691    Transition::Replace(SandboxMode::simple_new(app, mode))
692}
693
694impl TutorialState {
695    // These're mutex to each state, but still important to reset. Otherwise if you go back to a
696    // previous interaction stage, it'll just be automatically marked done.
697    fn reset_state(&mut self) {
698        self.inspected_bike_lane = false;
699        self.inspected_building = false;
700        self.inspected_stop_sign = false;
701        self.inspected_border = false;
702        self.was_paused = true;
703        self.num_pauses = 0;
704        self.score_delivered = false;
705        self.following_car = false;
706        self.car_parked = false;
707        self.prank_done = false;
708        self.parking_found = false;
709    }
710
711    fn stage(&self) -> &Stage {
712        &self.stages[self.current.stage]
713    }
714
715    fn interaction(&self) -> Task {
716        let stage = self.stage();
717        if self.current.part == stage.messages.len() {
718            stage.task
719        } else {
720            Task::Nil
721        }
722    }
723    fn message(&self) -> Option<&Message> {
724        let stage = self.stage();
725        if self.current.part == stage.messages.len() {
726            None
727        } else {
728            Some(&stage.messages[self.current.part])
729        }
730    }
731
732    fn next(&mut self) {
733        self.current.part += 1;
734        if self.current.part == self.stage().messages.len() + 1 {
735            self.current = TutorialPointer::new(self.current.stage + 1, 0);
736        }
737    }
738    fn prev(&mut self) {
739        if self.current.part == 0 {
740            self.current = TutorialPointer::new(
741                self.current.stage - 1,
742                self.stages[self.current.stage - 1].messages.len(),
743            );
744        } else {
745            self.current.part -= 1;
746        }
747    }
748
749    fn make_top_right(&self, ctx: &mut EventCtx, edit_map: bool) -> Panel {
750        let mut col = vec![Widget::row(vec![
751            Line("Tutorial").small_heading().into_widget(ctx),
752            Widget::vert_separator(ctx, 50.0),
753            ctx.style()
754                .btn_prev()
755                .disabled(self.current.stage == 0)
756                .build_widget(ctx, "previous tutorial"),
757            {
758                let mut txt = Text::from(format!("Task {}", self.current.stage + 1));
759                // TODO Smaller font and use alpha for the "/9" part
760                txt.append(Line(format!("/{}", self.stages.len())).fg(Color::grey(0.7)));
761                txt.into_widget(ctx)
762            },
763            ctx.style()
764                .btn_next()
765                .disabled(self.current.stage == self.stages.len() - 1)
766                .build_widget(ctx, "next tutorial"),
767            ctx.style().btn_outline.text("Quit").build_def(ctx),
768        ])
769        .centered()];
770        {
771            let task = self.interaction();
772            if task != Task::Nil {
773                col.push(Widget::row(vec![
774                    Text::from(
775                        Line(format!(
776                            "Task {}: {}",
777                            self.current.stage + 1,
778                            self.stage().task.label()
779                        ))
780                        .small_heading(),
781                    )
782                    .into_widget(ctx),
783                    // TODO also text saying "instructions"... can we layout two things easily to
784                    // make a button?
785                    ctx.style()
786                        .btn_plain
787                        .icon("system/assets/tools/info.svg")
788                        .build_widget(ctx, "instructions")
789                        .centered_vert()
790                        .align_right(),
791                ]));
792                col.push(task.top_txt(ctx, self).into_widget(ctx));
793            }
794        }
795        if edit_map {
796            col.push(
797                ctx.style()
798                    .btn_outline
799                    .icon_text("system/assets/tools/pencil.svg", "Edit map")
800                    .hotkey(lctrl(Key::E))
801                    .build_widget(ctx, "edit map"),
802            );
803        }
804
805        Panel::new_builder(Widget::col(col))
806            .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
807            .build(ctx)
808    }
809
810    fn make_state(&self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn GameplayState> {
811        if self.interaction() == Task::Nil {
812            app.primary.current_selection = None;
813        }
814
815        if let Some(ref cb) = self.stage().custom_spawn {
816            (cb)(app);
817            app.primary
818                .sim
819                .tiny_step(&app.primary.map, &mut app.primary.sim_cb);
820        }
821        // If this stage has a scenario, it's instantiated when SandboxMode gets created.
822
823        let last_finished_task = if self.current.stage == 0 {
824            Task::Nil
825        } else {
826            self.stages[self.current.stage - 1].task
827        };
828
829        Box::new(Tutorial {
830            top_right: self.make_top_right(ctx, last_finished_task >= Task::WatchBikes),
831            last_finished_task,
832
833            msg_panel: if let Some(msg) = self.message() {
834                let mut col = vec![{
835                    let mut txt = Text::new();
836                    txt.add_line(Line(self.stage().task.label()).small_heading());
837                    txt.add_line("");
838                    txt.into_widget(ctx)
839                }];
840                if let Some(icon) = msg.icon {
841                    col.push(Image::from_path(icon).dims(30.0).into_widget(ctx));
842                }
843                col.push(msg.txt.clone().wrap_to_pct(ctx, 30).into_widget(ctx));
844                let mut controls = vec![Widget::row(vec![
845                    ctx.style()
846                        .btn_prev()
847                        .disabled(self.current.part == 0)
848                        .hotkey(Key::LeftArrow)
849                        .build_widget(ctx, "previous message"),
850                    format!("{}/{}", self.current.part + 1, self.stage().messages.len())
851                        .text_widget(ctx)
852                        .centered_vert(),
853                    ctx.style()
854                        .btn_next()
855                        .disabled(self.current.part == self.stage().messages.len() - 1)
856                        .hotkey(Key::RightArrow)
857                        .build_widget(ctx, "next message"),
858                ])];
859                if self.current.part == self.stage().messages.len() - 1 {
860                    controls.push(
861                        ctx.style()
862                            .btn_solid_primary
863                            .text("Try it")
864                            .hotkey(hotkeys(vec![Key::RightArrow, Key::Space, Key::Enter]))
865                            .build_def(ctx),
866                    );
867                }
868                col.push(Widget::col(controls).align_bottom());
869
870                Some(
871                    Panel::new_builder(Widget::col(col).outline((5.0, Color::WHITE)))
872                        .exact_size_percent(40, 40)
873                        .aligned(msg.aligned, VerticalAlignment::Center)
874                        .build(ctx),
875                )
876            } else {
877                None
878            },
879            warped: false,
880        })
881    }
882
883    fn new(ctx: &mut EventCtx, app: &App) -> TutorialState {
884        let mut state = TutorialState {
885            stages: Vec::new(),
886            current: TutorialPointer::new(0, 0),
887            window_dims: (ctx.canvas.window_width, ctx.canvas.window_height),
888
889            inspected_bike_lane: false,
890            inspected_building: false,
891            inspected_stop_sign: false,
892            inspected_border: false,
893            was_paused: true,
894            num_pauses: 0,
895            following_car: false,
896            car_parked: false,
897            prank_done: false,
898            parking_found: false,
899            score_delivered: false,
900
901            fire_station: app.primary.map.find_b_by_osm_id(bldg(731238736)).unwrap(),
902        };
903
904        let tool_panel = tool_panel(ctx);
905        let time = TimePanel::new(ctx, app);
906        // The minimap is hidden at low zoom levels
907        let orig_zoom = ctx.canvas.cam_zoom;
908        ctx.canvas.cam_zoom = 100.0;
909        let minimap = Minimap::new(ctx, app, MinimapController);
910        ctx.canvas.cam_zoom = orig_zoom;
911
912        let map = &app.primary.map;
913
914        state.stages.push(
915            Stage::new(Task::Camera)
916                .warp_to(
917                    ID::Intersection(map.find_i_by_osm_id(osm::NodeID(53096945)).unwrap()),
918                    None,
919                )
920                .msg(Message::new(Text::from_multiline(vec![
921                    "Let's start by piloting your fancy new drone.",
922                    "",
923                    "- Click and drag to pan around the map",
924                    "- Use your scroll wheel or touchpad to zoom in and out.",
925                ])))
926                .msg(
927                    Message::new(Text::from(
928                        "If the controls feel wrong, try adjusting the settings.",
929                    ))
930                    .arrow(tool_panel.center_of("settings")),
931                )
932                .msg(Message::new(Text::from_multiline(vec![
933                    "Let's try the drone ou--",
934                    "",
935                    "WHOA, THERE'S A FIRE STATION ON FIRE!",
936                    "GO CLICK ON IT, QUICK!",
937                ])))
938                .msg(Message::new(Text::from_multiline(vec![
939                    "Hint:",
940                    "- Look around for an unusually red building",
941                    "- You have to zoom in to interact with anything on the map.",
942                ]))),
943        );
944
945        state.stages.push(
946            Stage::new(Task::InspectObjects)
947                .msg(Message::new(Text::from(
948                    "What, no fire? Er, sorry about that. Just a little joke we like to play on \
949                     the new recruits.",
950                )))
951                .msg(Message::new(Text::from_multiline(vec![
952                    "Now, let's learn how to inspect and interact with objects in the map.",
953                    "",
954                    "Find one of each:",
955                    "[ ] bike lane",
956                    "[ ] building",
957                    "[ ] intersection with stop sign",
958                    "[ ] intersection on the map border",
959                    "- Hint: You have to zoom in before you can select anything.",
960                ]))),
961        );
962
963        state.stages.push(
964            Stage::new(Task::TimeControls)
965                .warp_to(
966                    ID::Intersection(map.find_i_by_osm_id(osm::NodeID(53096945)).unwrap()),
967                    Some(6.5),
968                )
969                .msg(
970                    Message::new(Text::from_multiline(vec![
971                        "Inspection complete!",
972                        "",
973                        "You'll work day and night, watching traffic patterns unfold.",
974                    ]))
975                    .arrow(time.panel.center_of_panel()),
976                )
977                .msg(
978                    Message::new({
979                        let mut txt = Text::from(Line("You can pause or resume time"));
980                        txt.add_line("");
981                        txt.add_line("Hint: Press ");
982                        txt.append(Line(Key::Space.describe()).fg(ctx.style().text_hotkey_color));
983                        txt.append(Line(" to pause/resume"));
984                        txt
985                    })
986                    .arrow(time.panel.center_of("pause"))
987                    .icon("system/assets/speed/pause.svg"),
988                )
989                .msg(
990                    Message::new({
991                        let mut txt = Text::from(Line("Speed things up"));
992                        txt.add_line("");
993                        txt.add_line("Hint: Press ");
994                        txt.append(
995                            Line(Key::LeftArrow.describe()).fg(ctx.style().text_hotkey_color),
996                        );
997                        txt.append(Line(" to slow down, "));
998                        txt.append(
999                            Line(Key::RightArrow.describe()).fg(ctx.style().text_hotkey_color),
1000                        );
1001                        txt.append(Line(" to speed up"));
1002                        txt
1003                    })
1004                    .arrow(time.panel.center_of("30x speed"))
1005                    .icon("system/assets/speed/triangle.svg"),
1006                )
1007                .msg(
1008                    Message::new(Text::from("Advance time by certain amounts"))
1009                        .arrow(time.panel.center_of("step forwards")),
1010                )
1011                .msg(
1012                    Message::new(Text::from("And jump to the beginning of the day"))
1013                        .arrow(time.panel.center_of("reset to midnight"))
1014                        .icon("system/assets/speed/reset.svg"),
1015                )
1016                .msg(Message::new(Text::from(
1017                    "Let's try these controls out. Wait until 5pm or later.",
1018                ))),
1019        );
1020
1021        state.stages.push(
1022            Stage::new(Task::PauseResume)
1023                .msg(Message::new(Text::from(
1024                    "Whew, that took a while! (Hopefully not though...)",
1025                )))
1026                .msg(
1027                    Message::new(Text::from_multiline(vec![
1028                        "You might've figured it out already,",
1029                        "But you'll be pausing/resuming time VERY frequently",
1030                    ]))
1031                    .arrow(time.panel.center_of("pause"))
1032                    .icon("system/assets/speed/pause.svg"),
1033                )
1034                .msg(Message::new(Text::from(
1035                    "Just reassure me and pause/resume time a few times, alright?",
1036                ))),
1037        );
1038
1039        state.stages.push(
1040            Stage::new(Task::Escort)
1041                // Don't center on where the agents are, be a little offset
1042                .warp_to(
1043                    ID::Building(map.find_b_by_osm_id(bldg(217700459)).unwrap()),
1044                    Some(8.0),
1045                )
1046                .custom_spawn(Box::new(move |app| {
1047                    // Seed a specific target car, and fill up the target building's private
1048                    // parking to force the target to park on-street.
1049                    let map = &app.primary.map;
1050                    let goal_bldg = map.find_b_by_osm_id(bldg(217701875)).unwrap();
1051                    let start_lane = {
1052                        let r = map.get_r(
1053                            map.find_r_by_osm_id(OriginalRoad::new(36952952, (53128049, 53101726)))
1054                                .unwrap(),
1055                        );
1056                        assert_eq!(r.lanes.len(), 6);
1057                        r.lanes[2].id
1058                    };
1059                    let spawn_by_goal_bldg = {
1060                        let pos = map.get_b(goal_bldg).driving_connection(map).unwrap().0;
1061                        Position::new(pos.lane(), Distance::ZERO)
1062                    };
1063
1064                    let mut scenario = Scenario::empty(map, "prank");
1065                    scenario.people.push(PersonSpec {
1066                        orig_id: None,
1067                        trips: vec![IndividTrip::new(
1068                            Time::START_OF_DAY,
1069                            TripPurpose::Shopping,
1070                            TripEndpoint::SuddenlyAppear(Position::new(
1071                                start_lane,
1072                                map.get_l(start_lane).length() * 0.8,
1073                            )),
1074                            TripEndpoint::Building(goal_bldg),
1075                            TripMode::Drive,
1076                        )],
1077                    });
1078                    // Will definitely get there first
1079                    for _ in 0..map.get_b(goal_bldg).num_parking_spots() {
1080                        scenario.people.push(PersonSpec {
1081                            orig_id: None,
1082                            trips: vec![IndividTrip::new(
1083                                Time::START_OF_DAY,
1084                                TripPurpose::Shopping,
1085                                TripEndpoint::SuddenlyAppear(spawn_by_goal_bldg),
1086                                TripEndpoint::Building(goal_bldg),
1087                                TripMode::Drive,
1088                            )],
1089                        });
1090                    }
1091                    let mut rng = app.primary.current_flags.sim_flags.make_rng();
1092                    app.primary.sim.instantiate(
1093                        &scenario,
1094                        map,
1095                        &mut rng,
1096                        &mut Timer::new("spawn trip"),
1097                    );
1098                    app.primary.sim.tiny_step(map, &mut app.primary.sim_cb);
1099
1100                    // And add some noise
1101                    spawn_agents_around(
1102                        app.primary
1103                            .map
1104                            .find_i_by_osm_id(osm::NodeID(53101726))
1105                            .unwrap(),
1106                        app,
1107                    );
1108                }))
1109                .msg(Message::new(Text::from(
1110                    "Alright alright, no need to wear out your spacebar.",
1111                )))
1112                .msg(Message::new(Text::from_multiline(vec![
1113                    "Oh look, some people appeared!",
1114                    "We've got pedestrians, bikes, and cars moving around now.",
1115                ])))
1116                .msg(
1117                    Message::new(Text::from_multiline(vec![
1118                        "Why don't you follow this car to their destination,",
1119                        "see where they park, and then play a little... prank?",
1120                    ]))
1121                    .dynamic_arrow(Box::new(|g, app| {
1122                        g.canvas
1123                            .map_to_screen(
1124                                app.primary
1125                                    .sim
1126                                    .canonical_pt_for_agent(AgentID::Car(ESCORT), &app.primary.map)
1127                                    .unwrap(),
1128                            )
1129                            .to_pt()
1130                    }))
1131                    .left_aligned(),
1132                )
1133                .msg(
1134                    Message::new(Text::from_multiline(vec![
1135                        "You don't have to manually chase them; just click to follow.",
1136                        "",
1137                        "(If you do lose track of them, just reset)",
1138                    ]))
1139                    .arrow(time.panel.center_of("reset to midnight"))
1140                    .icon("system/assets/speed/reset.svg"),
1141                ),
1142        );
1143
1144        state.stages.push(
1145            Stage::new(Task::LowParking)
1146                // TODO Actually, we ideally just want a bunch of parked cars, not all these trips
1147                .scenario(ScenarioGenerator {
1148                    scenario_name: "low parking".to_string(),
1149                    only_seed_buses: Some(BTreeSet::new()),
1150                    spawn_over_time: vec![SpawnOverTime {
1151                        num_agents: 1000,
1152                        start_time: Time::START_OF_DAY,
1153                        stop_time: Time::START_OF_DAY + Duration::hours(3),
1154                        goal: None,
1155                        percent_driving: 1.0,
1156                        percent_biking: 0.0,
1157                        percent_use_transit: 0.0,
1158                    }],
1159                    border_spawn_over_time: Vec::new(),
1160                })
1161                .msg(
1162                    Message::new(Text::from_multiline(vec![
1163                        "What an immature prank. You should re-evaluate your life decisions.",
1164                        "",
1165                        "The map is quite large, so to help you orient, the minimap shows you an \
1166                         overview of all activity. You can click and drag it just like the normal \
1167                         map.",
1168                    ]))
1169                    .arrow(minimap.get_panel().center_of("minimap"))
1170                    .left_aligned(),
1171                )
1172                .msg(
1173                    Message::new(Text::from_multiline(vec![
1174                        "You can apply different layers to the map, to find things like:",
1175                        "",
1176                        "- roads with high traffic",
1177                        "- bus stops",
1178                        "- how much parking is filled up",
1179                    ]))
1180                    .arrow(minimap.get_panel().center_of("change layers"))
1181                    .icon("system/assets/tools/layers.svg")
1182                    .left_aligned(),
1183                )
1184                .msg(Message::new(Text::from_multiline(vec![
1185                    "Let's try these out.",
1186                    "There are lots of cars parked everywhere. Can you find a road that's almost \
1187                     out of parking spots?",
1188                ]))),
1189        );
1190
1191        let bike_lane_scenario = make_bike_lane_scenario(map);
1192        let bike_lane_focus_pt = map.find_b_by_osm_id(bldg(217699496)).unwrap();
1193
1194        state.stages.push(
1195            Stage::new(Task::WatchBikes)
1196                .warp_to(ID::Building(bike_lane_focus_pt), None)
1197                .scenario(bike_lane_scenario.clone())
1198                .msg(Message::new(Text::from_multiline(vec![
1199                    "Well done!",
1200                    "",
1201                    "Something's about to happen over here. Follow along and figure out what the \
1202                     problem is, at whatever speed you'd like.",
1203                ]))),
1204        );
1205
1206        let top_right = state.make_top_right(ctx, true);
1207        state.stages.push(
1208            Stage::new(Task::FixBikes)
1209                .scenario(bike_lane_scenario)
1210                .warp_to(ID::Building(bike_lane_focus_pt), None)
1211                .msg(Message::new(Text::from_multiline(vec![
1212                    "Looks like lots of cars and bikes trying to go to a house by the playfield.",
1213                    "",
1214                    "When lots of cars and bikes share the same lane, cars are delayed (assuming \
1215                     there's no room to pass) and the cyclist probably feels unsafe too.",
1216                ])))
1217                .msg(Message::new(Text::from(
1218                    "Luckily, you have the power to modify lanes! What if you could transform the \
1219                     parking lanes that aren't being used much into bike lanes?",
1220                )))
1221                .msg(
1222                    Message::new(Text::from(
1223                        "To edit lanes, click 'edit map' and then select a lane.",
1224                    ))
1225                    .arrow(top_right.center_of("edit map")),
1226                )
1227                .msg(Message::new(Text::from_multiline(vec![
1228                    "When you finish making edits, time will jump to the beginning of the next \
1229                     day. You can't make most changes in the middle of the day.",
1230                    "",
1231                    "Seattleites are really boring; they follow the exact same schedule everyday. \
1232                     They're also stubborn, so even if you try to influence their decision \
1233                     whether to drive, walk, bike, or take a bus, they'll do the same thing. For \
1234                     now, you're just trying to make things better, assuming people stick to \
1235                     their routine.",
1236                ])))
1237                .msg(
1238                    // TODO Deliberately vague with the measurement.
1239                    Message::new(Text::from_multiline(vec![
1240                        format!(
1241                            "So adjust lanes and speed up the slowest trip by at least {}.",
1242                            CAR_BIKE_CONTENTION_GOAL
1243                        ),
1244                        "".to_string(),
1245                        "You can explore results as trips finish. When everyone's finished, \
1246                         you'll get your final score."
1247                            .to_string(),
1248                    ]))
1249                    .arrow(minimap.get_panel().center_of("more data")),
1250                ),
1251        );
1252
1253        state.stages.push(
1254            Stage::new(Task::Done).msg(Message::new(Text::from_multiline(vec![
1255                "You're ready for the hard stuff now.",
1256                "",
1257                "- Try out some challenges",
1258                "- Explore larger parts of Seattle in the sandbox, and try out any ideas you've \
1259                 got.",
1260                "- Check out community proposals, and submit your own",
1261                "",
1262                "Go have the appropriate amount of fun!",
1263            ]))),
1264        );
1265
1266        state
1267
1268        // TODO Multi-modal trips -- including parking. (Cars per bldg, ownership)
1269        // TODO Explain the finished trip data
1270        // The city is in total crisis. You've only got 10 days to do something before all hell
1271        // breaks loose and people start kayaking / ziplining / crab-walking / cartwheeling to
1272        // work.
1273    }
1274
1275    pub fn scenarios_to_prebake(map: &Map) -> Vec<ScenarioGenerator> {
1276        vec![make_bike_lane_scenario(map)]
1277    }
1278}
1279
1280pub fn actions(app: &App, id: ID) -> Vec<(Key, String)> {
1281    match (app.session.tutorial.as_ref().unwrap().interaction(), id) {
1282        (Task::LowParking, ID::Lane(_)) => {
1283            vec![(Key::C, "check the parking occupancy".to_string())]
1284        }
1285        (Task::Escort, ID::Car(_)) => vec![(Key::C, "draw WASH ME".to_string())],
1286        _ => Vec::new(),
1287    }
1288}
1289
1290pub fn execute(ctx: &mut EventCtx, app: &mut App, id: ID, action: &str) -> Transition {
1291    let tut = app.session.tutorial.as_mut().unwrap();
1292    let response = match (id, action) {
1293        (ID::Car(c), "draw WASH ME") => {
1294            let is_parked = app
1295                .primary
1296                .sim
1297                .agent_to_trip(AgentID::Car(ESCORT))
1298                .is_none();
1299            if c == ESCORT {
1300                if is_parked {
1301                    tut.prank_done = true;
1302                    PopupMsg::new_state(
1303                        ctx,
1304                        "Prank in progress",
1305                        vec!["You quickly scribble on the window..."],
1306                    )
1307                } else {
1308                    PopupMsg::new_state(
1309                        ctx,
1310                        "Not yet!",
1311                        vec![
1312                            "You're going to run up to an occupied car and draw on their windows?",
1313                            "Sounds like we should be friends.",
1314                            "But, er, wait for the car to park. (You can speed up time!)",
1315                        ],
1316                    )
1317                }
1318            } else if c.vehicle_type == VehicleType::Bike {
1319                PopupMsg::new_state(
1320                    ctx,
1321                    "That's a bike",
1322                    vec![
1323                        "Achievement unlocked: You attempted to draw WASH ME on a cyclist.",
1324                        "This game is PG-13 or something, so I can't really describe what happens \
1325                         next.",
1326                        "But uh, don't try this at home.",
1327                    ],
1328                )
1329            } else {
1330                PopupMsg::new_state(
1331                    ctx,
1332                    "Wrong car",
1333                    vec![
1334                        "You're looking at the wrong car.",
1335                        "Use the 'reset to midnight' (key binding 'X') to start over, if you lost \
1336                         the car to follow.",
1337                    ],
1338                )
1339            }
1340        }
1341        (ID::Lane(l), "check the parking occupancy") => {
1342            let lane = app.primary.map.get_l(l);
1343            if lane.is_parking() {
1344                let percent = (app.primary.sim.get_free_onstreet_spots(l).len() as f64)
1345                    / (lane.number_parking_spots(app.primary.map.get_config()) as f64);
1346                if percent > 0.1 {
1347                    PopupMsg::new_state(
1348                        ctx,
1349                        "Not quite",
1350                        vec![
1351                            format!("This lane has {:.0}% spots free", percent * 100.0),
1352                            "Try using the 'parking occupancy' layer from the minimap controls"
1353                                .to_string(),
1354                        ],
1355                    )
1356                } else {
1357                    tut.parking_found = true;
1358                    PopupMsg::new_state(
1359                        ctx,
1360                        "Noice",
1361                        vec!["Yup, parallel parking would be tough here!"],
1362                    )
1363                }
1364            } else {
1365                PopupMsg::new_state(ctx, "Uhh..", vec!["That's not even a parking lane"])
1366            }
1367        }
1368        _ => unreachable!(),
1369    };
1370    Transition::Push(response)
1371}
1372
1373fn intro_story(ctx: &mut EventCtx) -> Box<dyn State<App>> {
1374    CutsceneBuilder::new("Introduction")
1375        .boss(
1376            "Argh, the mayor's on my case again about the West Seattle bridge. This day couldn't \
1377             get any worse.",
1378        )
1379        .player("Er, hello? Boss? I'm --")
1380        .boss("Yet somehow it did.. You're the new recruit. Yeah, yeah. Come in.")
1381        .boss(
1382            "Due to budget cuts, we couldn't hire a real traffic engineer, so we just called some \
1383             know-it-all from Reddit who seems to think they can fix Seattle traffic.",
1384        )
1385        .player("Yes, hi, my name is --")
1386        .boss("We can't afford name-tags, didn't you hear, budget cuts? Your name doesn't matter.")
1387        .player("What about my Insta handle?")
1388        .boss("-glare-")
1389        .boss(
1390            "Look, you think fixing traffic is easy? Hah! You can't fix one intersection without \
1391             breaking ten more.",
1392        )
1393        .boss(
1394            "And everybody wants something different! Bike lanes here! More parking! Faster \
1395             buses! Cheaper housing! Less rain! Free this, subsidized that!",
1396        )
1397        .boss("Light rail and robot cars aren't here to save the day! Know what you'll be using?")
1398        .extra("drone.svg", 1.0, "The traffic drone")
1399        .player("Is that... duct tape?")
1400        .boss(
1401            "Can't spit anymore cause of COVID and don't get me started on prayers. Well, off to \
1402             training for you!",
1403        )
1404        .build(
1405            ctx,
1406            Box::new(|ctx| {
1407                Text::from(Line("Use the tutorial to learn the basic controls.").fg(Color::BLACK))
1408                    .into_widget(ctx)
1409            }),
1410        )
1411}
1412
1413// Assumes ways
1414fn bldg(id: i64) -> osm::OsmID {
1415    osm::OsmID::Way(osm::WayID(id))
1416}