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 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 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 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 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
350fn 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}