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 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 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 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 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 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 return Some(Transition::Keep);
174 }
175 }
176 }
177
178 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 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 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 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 let mut tut = app.session.tutorial.take().unwrap();
365
366 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 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 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 }
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 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 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 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 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 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 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 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 .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 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 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 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 .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 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 }
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
1413fn bldg(id: i64) -> osm::OsmID {
1415 osm::OsmID::Way(osm::WayID(id))
1416}