1use std::collections::BTreeMap;
2
3use geom::{Duration, Time};
4use sim::{PersonID, TripID};
5use synthpop::OrigPersonID;
6use widgetry::{
7 Color, EventCtx, GfxCtx, HorizontalAlignment, Image, Line, Outcome, Panel, State, Text,
8 TextExt, VerticalAlignment, Widget,
9};
10
11use crate::app::App;
12use crate::app::Transition;
13use crate::challenges::cutscene::{CutsceneBuilder, ShowMessage};
14use crate::challenges::{Challenge, HighScore};
15use crate::common::cmp_duration_shorter;
16use crate::edit::EditMode;
17use crate::info::Tab;
18use crate::sandbox::gameplay::{challenge_header, FinalScore, GameplayMode, GameplayState};
19use crate::sandbox::{Actions, SandboxControls};
20
21pub struct OptimizeCommute {
24 top_right: Panel,
25 person: PersonID,
26 mode: GameplayMode,
27 goal: Duration,
28 time: Time,
29 done: bool,
30
31 trips: Vec<TripID>,
33
34 once: bool,
35}
36
37impl OptimizeCommute {
38 pub fn new_state(
39 ctx: &mut EventCtx,
40 app: &App,
41 orig_person: OrigPersonID,
42 goal: Duration,
43 ) -> Box<dyn GameplayState> {
44 let person = app.primary.sim.find_person_by_orig_id(orig_person).unwrap();
45 let trips = app.primary.sim.get_person(person).trips.clone();
46 Box::new(OptimizeCommute {
47 top_right: Panel::empty(ctx),
48 person,
49 mode: GameplayMode::OptimizeCommute(orig_person, goal),
50 goal,
51 time: Time::START_OF_DAY,
52 done: false,
53 trips,
54 once: true,
55 })
56 }
57
58 pub fn cutscene_pt1(ctx: &mut EventCtx, _: &App, mode: &GameplayMode) -> Box<dyn State<App>> {
59 CutsceneBuilder::new("Optimize one commute: part 1")
60 .boss("Listen up, I've got a special job for you today.")
61 .player("What is it? The scooter coalition back with demands for more valet parking?")
62 .boss("No, all the tax-funded valets are still busy with the kayakers.")
63 .boss(
64 "I've got a... friend who's tired of getting stuck in traffic. You've got to make \
65 their commute as fast as possible.",
66 )
67 .player("Uh, what's so special about them?")
68 .boss(
69 "That's none of your concern! I've anonymized their name, so don't even bother \
70 digging into what happened in Ballard --",
71 )
72 .boss("JUST GET TO WORK, KID!")
73 .player(
74 "(Somebody's blackmailing the boss. Guess it's time to help this Very Impatient \
75 Person.)",
76 )
77 .build(ctx, cutscene_task(mode))
78 }
79
80 pub fn cutscene_pt2(ctx: &mut EventCtx, _: &App, mode: &GameplayMode) -> Box<dyn State<App>> {
81 CutsceneBuilder::new("Optimize one commute: part 2")
83 .boss("I've got another, er, friend who's sick of this parking situation.")
84 .player(
85 "Yeah, why do we dedicate so much valuable land to storing unused cars? It's \
86 ridiculous!",
87 )
88 .boss(
89 "No, I mean, they're tired of having to hunt for parking. You need to make it \
90 easier.",
91 )
92 .player(
93 "What? We're trying to encourage people to be less car-dependent. Why's this \
94 \"friend\" more important than the city's carbon-neutral goals?",
95 )
96 .boss("Everyone's calling in favors these days. Just make it happen!")
97 .player("(Too many people have dirt on the boss. Guess we have another VIP to help.)")
98 .build(ctx, cutscene_task(mode))
99 }
100}
101
102impl GameplayState for OptimizeCommute {
103 fn event(
104 &mut self,
105 ctx: &mut EventCtx,
106 app: &mut App,
107 controls: &mut SandboxControls,
108 actions: &mut Actions,
109 ) -> Option<Transition> {
110 if self.once {
111 self.once = false;
112 controls.common.as_mut().unwrap().launch_info_panel(
113 ctx,
114 app,
115 Tab::PersonTrips(self.person, BTreeMap::new()),
116 actions,
117 );
118 }
119
120 if self.time != app.primary.sim.time() && !self.done {
121 self.time = app.primary.sim.time();
122 self.recreate_panels(ctx, app);
123
124 let (before, after, done) = get_score(app, &self.trips);
125 if done == self.trips.len() {
126 self.done = true;
127 return Some(Transition::Push(final_score(
128 ctx,
129 app,
130 self.mode.clone(),
131 before,
132 after,
133 self.goal,
134 )));
135 }
136 }
137
138 if let Outcome::Clicked(x) = self.top_right.event(ctx) {
139 match x.as_ref() {
140 "edit map" => {
141 return Some(Transition::Push(EditMode::new_state(
142 ctx,
143 app,
144 self.mode.clone(),
145 )));
146 }
147 "instructions" => {
148 let contents = (cutscene_task(&self.mode))(ctx);
149 return Some(Transition::Push(ShowMessage::new_state(
150 ctx,
151 contents,
152 Color::WHITE,
153 )));
154 }
155 "hint" => {
156 let mut txt = Text::from("Hints");
158 txt.add_line("");
159 txt.add_line("Use the locator at the top right to find the VIP.");
160 txt.add_line("You can wait for one of their trips to begin or end.");
161 txt.add_line("Focus on trips spent mostly waiting");
162 let contents = txt.into_widget(ctx);
163 return Some(Transition::Push(ShowMessage::new_state(
164 ctx,
165 contents,
166 app.cs.panel_bg,
167 )));
168 }
169 "locate VIP" => {
170 controls.common.as_mut().unwrap().launch_info_panel(
171 ctx,
172 app,
173 Tab::PersonTrips(self.person, BTreeMap::new()),
174 actions,
175 );
176 }
177 _ => unreachable!(),
178 }
179 }
180
181 None
182 }
183
184 fn draw(&self, g: &mut GfxCtx, _: &App) {
185 self.top_right.draw(g);
186 }
187
188 fn recreate_panels(&mut self, ctx: &mut EventCtx, app: &App) {
189 let (before, after, done) = get_score(app, &self.trips);
190 let mut txt = Text::from(format!("Total time: {} (", after));
191 txt.append_all(cmp_duration_shorter(app, after, before));
192 txt.append(Line(")"));
193
194 self.top_right = Panel::new_builder(Widget::col(vec![
195 challenge_header(ctx, "Optimize the VIP's commute"),
196 Widget::row(vec![
197 format!("Speed up the VIP's trips by {}", self.goal)
198 .text_widget(ctx)
199 .centered_vert(),
200 ctx.style()
201 .btn_plain
202 .icon_text("system/assets/tools/lightbulb.svg", "Hint")
203 .build_widget(ctx, "hint")
204 .align_right(),
205 ]),
206 Widget::row(vec![
207 ctx.style()
208 .btn_plain
209 .icon("system/assets/tools/location.svg")
210 .build_widget(ctx, "locate VIP"),
211 format!("{}/{} trips done", done, self.trips.len()).text_widget(ctx),
212 txt.into_widget(ctx),
213 ])
214 .centered(),
215 ]))
216 .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
217 .build(ctx);
218 }
219}
220
221fn get_score(app: &App, trips: &[TripID]) -> (Duration, Duration, usize) {
223 let mut done = 0;
224 let mut before = Duration::ZERO;
225 let mut after = Duration::ZERO;
226 for t in trips {
227 if let Some((total, _, _)) = app.primary.sim.finished_trip_details(*t) {
228 done += 1;
229 after += total;
230 before += app.prebaked().finished_trip_time(*t).unwrap();
232 }
233 }
234 (before, after, done)
235}
236
237fn final_score(
238 ctx: &mut EventCtx,
239 app: &mut App,
240 mode: GameplayMode,
241 before: Duration,
242 after: Duration,
243 goal: Duration,
244) -> Box<dyn State<App>> {
245 let mut next_mode: Option<GameplayMode> = None;
246
247 let msg = if before == after {
248 format!(
249 "The VIP's commute still takes a total of {}. Were you asleep on the job? Try \
250 changing something!",
251 before
252 )
253 } else if after > before {
254 format!(
256 "The VIP's commute went from {} total to {}. You utter dunce! Are you trying to screw \
257 me over?!",
258 before, after
259 )
260 } else if before - after < goal {
261 format!(
262 "The VIP's commute went from {} total to {}. Hmm... that's {} faster. But didn't I \
263 tell you to speed things up by {} at least?",
264 before,
265 after,
266 before - after,
267 goal
268 )
269 } else {
270 HighScore {
271 goal: format!("make VIP's commute at least {} faster", goal),
272 score: before - after,
273 edits_name: app.primary.map.get_edits().edits_name.clone(),
274 }
275 .record(app, mode.clone());
276
277 next_mode = Challenge::find(&mode).1.map(|c| c.gameplay);
278
279 format!(
280 "Alright, you somehow managed to shave {} down from the VIP's original commute of {}. \
281 I guess that'll do. Maybe you're not totally useless after all.",
282 before - after,
283 before
284 )
285 };
286
287 FinalScore::new_state(ctx, msg, mode, next_mode)
288}
289
290fn cutscene_task(mode: &GameplayMode) -> Box<dyn Fn(&mut EventCtx) -> Widget> {
291 let goal = match mode {
292 GameplayMode::OptimizeCommute(_, d) => *d,
293 _ => unreachable!(),
294 };
295
296 Box::new(move |ctx| {
297 let icon_builder = Image::empty().color(Color::BLACK).dims(50.0);
298 Widget::custom_col(vec![
299 Text::from_multiline(vec![
300 Line(format!("Speed up the VIP's trips by a total of {}", goal)).fg(Color::BLACK),
301 Line("Ignore the damage done to everyone else.").fg(Color::BLACK),
302 ])
303 .into_widget(ctx)
304 .margin_below(30),
305 Widget::row(vec![
306 Widget::col(vec![
307 Line("Time").fg(Color::BLACK).into_widget(ctx),
308 icon_builder
309 .clone()
310 .source_path("system/assets/tools/time.svg")
311 .into_widget(ctx),
312 Text::from_multiline(vec![
313 Line("Until the VIP's").fg(Color::BLACK),
314 Line("last trip is done").fg(Color::BLACK),
315 ])
316 .into_widget(ctx),
317 ]),
318 Widget::col(vec![
319 Line("Goal").fg(Color::BLACK).into_widget(ctx),
320 icon_builder
321 .clone()
322 .source_path("system/assets/tools/location.svg")
323 .into_widget(ctx),
324 Text::from_multiline(vec![
325 Line("Speed up the VIP's trips").fg(Color::BLACK),
326 Line(format!("by at least {}", goal)).fg(Color::BLACK),
327 ])
328 .into_widget(ctx),
329 ]),
330 Widget::col(vec![
331 Line("Score").fg(Color::BLACK).into_widget(ctx),
332 icon_builder
333 .source_path("system/assets/tools/star.svg")
334 .into_widget(ctx),
335 Text::from_multiline(vec![
336 Line("How much time").fg(Color::BLACK),
337 Line("the VIP saves").fg(Color::BLACK),
338 ])
339 .into_widget(ctx),
340 ]),
341 ])
342 .evenly_spaced(),
343 ])
344 })
345}