game/sandbox/gameplay/
commute.rs

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
21// TODO A nice level to unlock: specifying your own commute, getting to work on it
22
23pub struct OptimizeCommute {
24    top_right: Panel,
25    person: PersonID,
26    mode: GameplayMode,
27    goal: Duration,
28    time: Time,
29    done: bool,
30
31    // Cache here for convenience
32    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        // TODO The person chosen for this currently has more of an issue needing PBLs, actually.
82        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                    // TODO Multiple hints. Point to follow button.
157                    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
221// Returns (before, after, number of trips done)
222fn 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            // Assume all trips completed before changes
231            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        // TODO mad lib insults
255        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}