game/sandbox/gameplay/
fix_traffic_signals.rs

1use crate::ID;
2use geom::{Duration, Time};
3use map_model::IntersectionID;
4use widgetry::{
5    Color, EventCtx, GfxCtx, HorizontalAlignment, Image, Key, Line, Outcome, Panel, State, Text,
6    VerticalAlignment, Widget,
7};
8
9use crate::app::Transition;
10use crate::app::{App, FindDelayedIntersections};
11use crate::challenges::cutscene::{CutsceneBuilder, ShowMessage};
12use crate::challenges::HighScore;
13use crate::common::Warping;
14use crate::edit::EditMode;
15use crate::sandbox::gameplay::{challenge_header, FinalScore, GameplayMode, GameplayState};
16use crate::sandbox::{Actions, SandboxControls, SandboxMode};
17
18const THRESHOLD: Duration = Duration::const_seconds(20.0 * 60.0);
19
20pub struct FixTrafficSignals {
21    top_right: Panel,
22    time: Time,
23    worst: Option<(IntersectionID, Duration)>,
24    done_at: Option<Time>,
25    mode: GameplayMode,
26}
27
28impl FixTrafficSignals {
29    pub fn new_state(ctx: &mut EventCtx) -> Box<dyn GameplayState> {
30        Box::new(FixTrafficSignals {
31            top_right: Panel::empty(ctx),
32            time: Time::START_OF_DAY,
33            worst: None,
34            done_at: None,
35            mode: GameplayMode::FixTrafficSignals,
36        })
37    }
38
39    pub fn cutscene_pt1(ctx: &mut EventCtx, _: &App, _: &GameplayMode) -> Box<dyn State<App>> {
40        CutsceneBuilder::new("Traffic signal survivor")
41            .boss("I hope you've had your coffee. There's a huge mess downtown.")
42            .player("Did two buses get tangled together again?")
43            .boss("Worse. SCOOT along Mercer is going haywire.")
44            .player("SCOOT?")
45            .boss(
46                "You know, Split Cycle Offset Optimization Technique, the traffic signal \
47                 coordination system? Did you sleep through college or what?",
48            )
49            .boss(
50                "It's offline. All the traffic signals look like they've been reset to industry \
51                 defaults.",
52            )
53            .player("Uh oh. Too much scooter traffic overwhelm it? Eh? EHH?")
54            .boss("...")
55            .boss("You know, not every problem you will face in life is caused by a pun.")
56            .boss(
57                "Most, in fact, will be caused by me ruining your life because you won't take \
58                 your job seriously.",
59            )
60            .player("Sorry, boss.")
61            .extra(
62                "parents.svg.gz",
63                0.6,
64                "Hi, er, we're calling from Lower Queen Anne. What's going on?!",
65            )
66            .extra(
67                "parents.svg.gz",
68                0.6,
69                "We just missed a VERY important appointment. Nobody's moving an inch!",
70            )
71            .boss(
72                "Oh no... reports are coming in, ALL of the traffic signals downtown are screwed \
73                 up!",
74            )
75            .boss(
76                "You need to go fix all of them. But listen, you haven't got much time. Focus on \
77                 the worst problems first.",
78            )
79            .player("Sigh... it's going to be a long day.")
80            .build(ctx, Box::new(cutscene_pt1_task))
81    }
82}
83
84impl GameplayState for FixTrafficSignals {
85    fn event(
86        &mut self,
87        ctx: &mut EventCtx,
88        app: &mut App,
89        _: &mut SandboxControls,
90        _: &mut Actions,
91    ) -> Option<Transition> {
92        // Normally we just do this once at the beginning, but because there are other paths to
93        // resetting (like jump-to-time), it's safest just to do this.
94        if app.primary.sim_cb.is_none() {
95            app.primary.sim_cb = Some(Box::new(FindDelayedIntersections {
96                halt_limit: THRESHOLD,
97                report_limit: Duration::minutes(1),
98                currently_delayed: Vec::new(),
99            }));
100            app.primary.sim.set_periodic_callback(Duration::minutes(1));
101        }
102
103        if self.time != app.primary.sim.time() && self.done_at.is_none() {
104            self.time = app.primary.sim.time();
105
106            self.worst = None;
107            if let Some((i, t)) = app
108                .primary
109                .sim_cb
110                .as_mut()
111                .unwrap()
112                .downcast_mut::<FindDelayedIntersections>()
113                .unwrap()
114                .currently_delayed
115                .get(0)
116                .cloned()
117            {
118                self.worst = Some((i, app.primary.sim.time() - t));
119            }
120
121            if self
122                .worst
123                .map(|(_, delay)| delay >= THRESHOLD)
124                .unwrap_or(false)
125            {
126                self.done_at = Some(app.primary.sim.time());
127                self.recreate_panels(ctx, app);
128
129                return Some(Transition::Multi(vec![
130                    Transition::Push(final_score(ctx, app, self.mode.clone(), true)),
131                    Transition::Push(Warping::new_state(
132                        ctx,
133                        app.primary
134                            .canonical_point(ID::Intersection(self.worst.unwrap().0))
135                            .unwrap(),
136                        Some(10.0),
137                        None,
138                        &mut app.primary,
139                    )),
140                ]));
141            } else {
142                self.recreate_panels(ctx, app);
143            }
144
145            if app.primary.sim.is_done() {
146                self.done_at = Some(app.primary.sim.time());
147                // TODO The score is up to 1 min (report_limit) off.
148                return Some(Transition::Push(final_score(
149                    ctx,
150                    app,
151                    self.mode.clone(),
152                    false,
153                )));
154            }
155        }
156
157        if let Outcome::Clicked(x) = self.top_right.event(ctx) {
158            match x.as_ref() {
159                "edit map" => {
160                    return Some(Transition::Push(EditMode::new_state(
161                        ctx,
162                        app,
163                        self.mode.clone(),
164                    )));
165                }
166                "instructions" => {
167                    let contents = cutscene_pt1_task(ctx);
168                    return Some(Transition::Push(ShowMessage::new_state(
169                        ctx,
170                        contents,
171                        Color::WHITE,
172                    )));
173                }
174                "hint" => {
175                    // TODO Multiple hints. Point to layers.
176                    let mut txt = Text::from("Hint");
177                    txt.add_line("");
178                    txt.add_appended(vec![
179                        Line("Press "),
180                        Key::L.txt(ctx),
181                        Line(" to open layers. Try "),
182                        Key::D.txt(ctx),
183                        Line("elay or worst traffic "),
184                        Key::J.txt(ctx),
185                        Line("ams"),
186                    ]);
187                    let contents = txt.into_widget(ctx);
188                    return Some(Transition::Push(ShowMessage::new_state(
189                        ctx,
190                        contents,
191                        app.cs.panel_bg,
192                    )));
193                }
194                "try again" => {
195                    return Some(Transition::Replace(SandboxMode::simple_new(
196                        app,
197                        self.mode.clone(),
198                    )));
199                }
200                "go to slowest intersection" => {
201                    let i = app
202                        .primary
203                        .sim_cb
204                        .as_ref()
205                        .unwrap()
206                        .downcast_ref::<FindDelayedIntersections>()
207                        .unwrap()
208                        .currently_delayed[0]
209                        .0;
210                    return Some(Transition::Push(Warping::new_state(
211                        ctx,
212                        app.primary.canonical_point(ID::Intersection(i)).unwrap(),
213                        Some(10.0),
214                        None,
215                        &mut app.primary,
216                    )));
217                }
218                "explain score" => {
219                    // TODO Adjust wording
220                    return Some(Transition::Push(ShowMessage::new_state(
221                        ctx,
222                        Text::from_multiline(vec![
223                            Line("You changed some traffic signals in the middle of the day."),
224                            Line(
225                                "First see if you can survive for a full day, making changes \
226                                along the way.",
227                            ),
228                            Line("Then you should check if your changes work from midnight."),
229                        ])
230                        .into_widget(ctx),
231                        app.cs.panel_bg,
232                    )));
233                }
234                _ => unreachable!(),
235            }
236        }
237
238        None
239    }
240
241    fn draw(&self, g: &mut GfxCtx, _: &App) {
242        self.top_right.draw(g);
243    }
244
245    fn recreate_panels(&mut self, ctx: &mut EventCtx, app: &App) {
246        if let Some(time) = self.done_at {
247            self.top_right = Panel::new_builder(Widget::col(vec![
248                challenge_header(ctx, "Traffic signal survivor"),
249                Widget::row(vec![
250                    Line(format!("Delay exceeded {} at {}", THRESHOLD, time))
251                        .fg(Color::RED)
252                        .into_widget(ctx)
253                        .centered_vert(),
254                    ctx.style().btn_outline.text("try again").build_def(ctx),
255                ]),
256            ]))
257            .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
258            .build(ctx);
259        } else {
260            let meter = Widget::row(vec![
261                ctx.style()
262                    .btn_plain
263                    .icon("system/assets/tools/location.svg")
264                    .disabled(self.worst.is_none())
265                    .build_widget(ctx, "go to slowest intersection"),
266                Text::from_all(vec![
267                    Line("Worst delay: "),
268                    if let Some((_, delay)) = self.worst {
269                        Line(delay.to_string(&app.opts.units)).fg(if delay < Duration::minutes(5) {
270                            Color::hex("#F9EC51")
271                        } else if delay < Duration::minutes(15) {
272                            Color::hex("#EE702E")
273                        } else {
274                            app.cs.signal_banned_turn
275                        })
276                    } else {
277                        Line("none!").secondary()
278                    },
279                ])
280                .into_widget(ctx)
281                .centered_vert(),
282                if app.primary.dirty_from_edits {
283                    ctx.style()
284                        .btn_plain
285                        .icon("system/assets/tools/info.svg")
286                        .build_widget(ctx, "explain score")
287                        .align_right()
288                } else {
289                    Widget::nothing()
290                },
291            ]);
292
293            self.top_right = Panel::new_builder(Widget::col(vec![
294                challenge_header(ctx, "Traffic signal survivor"),
295                Widget::row(vec![
296                    Line(format!(
297                        "Keep delay at all intersections under {}",
298                        THRESHOLD
299                    ))
300                    .into_widget(ctx),
301                    ctx.style()
302                        .btn_plain
303                        .icon_text("system/assets/tools/lightbulb.svg", "Hint")
304                        .build_widget(ctx, "hint")
305                        .align_right(),
306                ]),
307                meter,
308            ]))
309            .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
310            .build(ctx);
311        }
312    }
313
314    fn on_destroy(&self, app: &mut App) {
315        assert!(app.primary.sim_cb.is_some());
316        app.primary.sim_cb = None;
317        app.primary.sim.unset_periodic_callback();
318    }
319}
320
321fn final_score(
322    ctx: &mut EventCtx,
323    app: &mut App,
324    mode: GameplayMode,
325    failed: bool,
326) -> Box<dyn State<App>> {
327    let score = app.primary.sim.time() - Time::START_OF_DAY;
328    HighScore {
329        goal: format!(
330            "make it {} without delay exceeding {}",
331            app.primary.sim.get_end_of_day() - Time::START_OF_DAY,
332            THRESHOLD
333        ),
334        score,
335        edits_name: app.primary.map.get_edits().edits_name.clone(),
336    }
337    .record(app, mode.clone());
338
339    let msg = if failed {
340        format!(
341            "You only made it {} before the traffic signals caused a jam. Lame!",
342            score
343        )
344    } else {
345        "Wow, you managed to fix the signals. Great job!".to_string()
346    };
347    FinalScore::new_state(ctx, msg, mode, None)
348}
349
350// TODO Can we automatically transform text and SVG colors?
351fn cutscene_pt1_task(ctx: &mut EventCtx) -> Widget {
352    let icon_builder = Image::empty().color(Color::BLACK).dims(50.0);
353    Widget::custom_col(vec![
354        Text::from_multiline(vec![
355            Line(format!(
356                "Don't let anyone be delayed by one traffic signal more than {}!",
357                THRESHOLD
358            ))
359            .fg(Color::BLACK),
360            Line("Survive as long as possible through 24 hours of a busy weekday.")
361                .fg(Color::BLACK),
362        ])
363        .into_widget(ctx)
364        .margin_below(30),
365        Widget::custom_row(vec![
366            Widget::col(vec![
367                Line("Time").fg(Color::BLACK).into_widget(ctx),
368                icon_builder
369                    .clone()
370                    .source_path("system/assets/tools/time.svg")
371                    .into_widget(ctx),
372                Line("24 hours").fg(Color::BLACK).into_widget(ctx),
373            ]),
374            Widget::col(vec![
375                Line("Goal").fg(Color::BLACK).into_widget(ctx),
376                icon_builder
377                    .clone()
378                    .source_path("system/assets/tools/location.svg")
379                    .into_widget(ctx),
380                Text::from_multiline(vec![
381                    Line("Keep delay at all intersections").fg(Color::BLACK),
382                    Line(format!("under {}", THRESHOLD)).fg(Color::BLACK),
383                ])
384                .into_widget(ctx),
385            ]),
386            Widget::col(vec![
387                Line("Score").fg(Color::BLACK).into_widget(ctx),
388                icon_builder
389                    .source_path("system/assets/tools/star.svg")
390                    .into_widget(ctx),
391                Line("How long you survive")
392                    .fg(Color::BLACK)
393                    .into_widget(ctx),
394            ]),
395        ])
396        .evenly_spaced(),
397    ])
398}