1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
use crate::app::App;
use crate::challenges::{Challenge, HighScore};
use crate::common::{ContextualActions, Tab};
use crate::cutscene::{CutsceneBuilder, FYI};
use crate::edit::EditMode;
use crate::game::{State, Transition};
use crate::helpers::cmp_duration_shorter;
use crate::helpers::ID;
use crate::sandbox::gameplay::{challenge_header, FinalScore, GameplayMode, GameplayState};
use crate::sandbox::SandboxControls;
use ezgui::{
    Btn, Color, Composite, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, RewriteColor,
    Text, TextExt, VerticalAlignment, Widget,
};
use geom::{Duration, Time};
use sim::{OrigPersonID, PersonID, TripID};
use std::collections::BTreeMap;

// TODO Avoid hack entirely, or tune appearance
const METER_HACK: f64 = -15.0;

// TODO A nice level to unlock: specifying your own commute, getting to work on it

pub struct OptimizeCommute {
    top_center: Composite,
    meter: Composite,
    person: PersonID,
    mode: GameplayMode,
    goal: Duration,
    time: Time,
    done: bool,

    // Cache here for convenience
    trips: Vec<TripID>,

    once: bool,
}

impl OptimizeCommute {
    pub fn new(
        ctx: &mut EventCtx,
        app: &App,
        orig_person: OrigPersonID,
        goal: Duration,
    ) -> Box<dyn GameplayState> {
        let person = app.primary.sim.find_person_by_orig_id(orig_person).unwrap();
        let trips = app.primary.sim.get_person(person).trips.clone();
        Box::new(OptimizeCommute {
            top_center: Composite::new(Widget::col(vec![
                challenge_header(ctx, "Optimize the VIP's commute"),
                Widget::row(vec![
                    format!("Speed up the VIP's trips by {}", goal)
                        .draw_text(ctx)
                        .centered_vert(),
                    Btn::svg(
                        "system/assets/tools/hint.svg",
                        RewriteColor::Change(Color::WHITE, app.cs.hovering),
                    )
                    .build(ctx, "hint", None)
                    .align_right(),
                ]),
            ]))
            .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
            .build(ctx),
            meter: make_meter(ctx, Duration::ZERO, Duration::ZERO, 0, trips.len()),
            person,
            mode: GameplayMode::OptimizeCommute(orig_person, goal),
            goal,
            time: Time::START_OF_DAY,
            done: false,
            trips,
            once: true,
        })
    }

    pub fn cutscene_pt1(ctx: &mut EventCtx, app: &App, mode: &GameplayMode) -> Box<dyn State> {
        CutsceneBuilder::new("Optimize one commute: part 1")
            .boss("Listen up, I've got a special job for you today.")
            .player("What is it? The scooter coalition back with demands for more valet parking?")
            .boss("No, all the tax-funded valets are still busy with the kayakers.")
            .boss(
                "I've got a... friend who's tired of getting stuck in traffic. You've got to make \
                 their commute as fast as possible.",
            )
            .player("Uh, what's so special about them?")
            .boss(
                "That's none of your concern! I've anonymized their name, so don't even bother \
                 digging into what happened in Ballard --",
            )
            .boss("JUST GET TO WORK, KID!")
            .player(
                "(Somebody's blackmailing the boss. Guess it's time to help this Very Impatient \
                 Person.)",
            )
            .build(ctx, app, cutscene_task(mode))
    }

    pub fn cutscene_pt2(ctx: &mut EventCtx, app: &App, mode: &GameplayMode) -> Box<dyn State> {
        // TODO The person chosen for this currently has more of an issue needing PBLs, actually.
        CutsceneBuilder::new("Optimize one commute: part 2")
            .boss("I've got another, er, friend who's sick of this parking situation.")
            .player(
                "Yeah, why do we dedicate so much valuable land to storing unused cars? It's \
                 ridiculous!",
            )
            .boss(
                "No, I mean, they're tired of having to hunt for parking. You need to make it \
                 easier.",
            )
            .player(
                "What? We're trying to encourage people to be less car-dependent. Why's this \
                 \"friend\" more important than the city's carbon-neutral goals?",
            )
            .boss("Everyone's calling in favors these days. Just make it happen!")
            .player("(Too many people have dirt on the boss. Guess we have another VIP to help.)")
            .build(ctx, app, cutscene_task(mode))
    }
}

impl GameplayState for OptimizeCommute {
    fn event(
        &mut self,
        ctx: &mut EventCtx,
        app: &mut App,
        controls: &mut SandboxControls,
    ) -> Option<Transition> {
        if self.once {
            self.once = false;
            controls.common.as_mut().unwrap().launch_info_panel(
                ctx,
                app,
                Tab::PersonTrips(self.person, BTreeMap::new()),
                &mut Actions {
                    paused: controls.speed.as_ref().unwrap().is_paused(),
                },
            );
        }

        self.meter.align_below(
            ctx,
            &controls.agent_meter.as_ref().unwrap().composite,
            METER_HACK,
        );

        if self.time != app.primary.sim.time() && !self.done {
            self.time = app.primary.sim.time();

            let (before, after, done) = get_score(app, &self.trips);
            self.meter = make_meter(ctx, before, after, done, self.trips.len());
            self.meter.align_below(
                ctx,
                &controls.agent_meter.as_ref().unwrap().composite,
                METER_HACK,
            );

            if done == self.trips.len() {
                self.done = true;
                return Some(Transition::Push(final_score(
                    ctx,
                    app,
                    self.mode.clone(),
                    before,
                    after,
                    self.goal,
                )));
            }
        }

        match self.top_center.event(ctx) {
            Outcome::Clicked(x) => match x.as_ref() {
                "edit map" => {
                    return Some(Transition::Push(EditMode::new(ctx, app, self.mode.clone())));
                }
                "instructions" => {
                    let contents = (cutscene_task(&self.mode))(ctx);
                    return Some(Transition::Push(FYI::new(ctx, contents, Color::WHITE)));
                }
                "hint" => {
                    // TODO Multiple hints. Point to follow button.
                    let mut txt = Text::from(Line("Hints"));
                    txt.add(Line(""));
                    txt.add(Line("Use the locator at the top right to find the VIP."));
                    txt.add(Line("You can wait for one of their trips to begin or end."));
                    txt.add(Line("Focus on trips spent mostly waiting"));
                    let contents = txt.draw(ctx);
                    return Some(Transition::Push(FYI::new(ctx, contents, app.cs.panel_bg)));
                }
                _ => unreachable!(),
            },
            _ => {}
        }
        match self.meter.event(ctx) {
            Outcome::Clicked(x) => match x.as_ref() {
                "locate VIP" => {
                    controls.common.as_mut().unwrap().launch_info_panel(
                        ctx,
                        app,
                        Tab::PersonTrips(self.person, BTreeMap::new()),
                        &mut Actions {
                            paused: controls.speed.as_ref().unwrap().is_paused(),
                        },
                    );
                }
                _ => unreachable!(),
            },
            _ => {}
        }

        None
    }

    fn draw(&self, g: &mut GfxCtx, _: &App) {
        self.top_center.draw(g);
        self.meter.draw(g);
    }
}

// Returns (before, after, number of trips done)
fn get_score(app: &App, trips: &Vec<TripID>) -> (Duration, Duration, usize) {
    let mut done = 0;
    let mut before = Duration::ZERO;
    let mut after = Duration::ZERO;
    for t in trips {
        if let Some((total, _)) = app.primary.sim.finished_trip_time(*t) {
            done += 1;
            after += total;
            // Assume all trips completed before changes
            before += app.prebaked().finished_trip_time(*t).unwrap();
        }
    }
    (before, after, done)
}

fn make_meter(
    ctx: &mut EventCtx,
    before: Duration,
    after: Duration,
    done: usize,
    trips: usize,
) -> Composite {
    let mut txt = Text::from(Line(format!("Total time: {} (", after)));
    txt.append_all(cmp_duration_shorter(after, before));
    txt.append(Line(")"));

    Composite::new(Widget::col(vec![
        Widget::horiz_separator(ctx, 0.2),
        Widget::row(vec![
            Btn::svg_def("system/assets/tools/location.svg").build(ctx, "locate VIP", None),
            format!("{}/{} trips done", done, trips).draw_text(ctx),
            txt.draw(ctx),
        ]),
    ]))
    .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
    .build(ctx)
}

fn final_score(
    ctx: &mut EventCtx,
    app: &mut App,
    mode: GameplayMode,
    before: Duration,
    after: Duration,
    goal: Duration,
) -> Box<dyn State> {
    let mut next_mode: Option<GameplayMode> = None;

    let msg = if before == after {
        format!(
            "The VIP's commute still takes a total of {}. Were you asleep on the job? Try \
             changing something!",
            before
        )
    } else if after > before {
        // TODO mad lib insults
        format!(
            "The VIP's commute went from {} total to {}. You utter dunce! Are you trying to screw \
             me over?!",
            before, after
        )
    } else if before - after < goal {
        format!(
            "The VIP's commute went from {} total to {}. Hmm... that's {} faster. But didn't I \
             tell you to speed things up by {} at least?",
            before,
            after,
            before - after,
            goal
        )
    } else {
        HighScore {
            goal: format!("make VIP's commute at least {} faster", goal),
            score: before - after,
            edits_name: app.primary.map.get_edits().edits_name.clone(),
        }
        .record(app, mode.clone());

        next_mode = Challenge::find(&mode).1.map(|c| c.gameplay);

        format!(
            "Alright, you somehow managed to shave {} down from the VIP's original commute of {}. \
             I guess that'll do. Maybe you're not totally useless after all.",
            before - after,
            before
        )
    };

    FinalScore::new(ctx, app, msg, mode, next_mode)
}

// TODO Probably refactor this for most challenge modes, or have SandboxMode pass in Actions
struct Actions {
    paused: bool,
}

impl ContextualActions for Actions {
    fn actions(&self, _: &App, _: ID) -> Vec<(Key, String)> {
        Vec::new()
    }
    fn execute(
        &mut self,
        _: &mut EventCtx,
        _: &mut App,
        _: ID,
        _: String,
        _: &mut bool,
    ) -> Transition {
        unreachable!()
    }
    fn is_paused(&self) -> bool {
        self.paused
    }
}

fn cutscene_task(mode: &GameplayMode) -> Box<dyn Fn(&mut EventCtx) -> Widget> {
    let goal = match mode {
        GameplayMode::OptimizeCommute(_, d) => *d,
        _ => unreachable!(),
    };

    Box::new(move |ctx| {
        Widget::custom_col(vec![
            Text::from_multiline(vec![
                Line(format!("Speed up the VIP's trips by a total of {}", goal)).fg(Color::BLACK),
                Line("Ignore the damage done to everyone else.").fg(Color::BLACK),
            ])
            .draw(ctx)
            .margin_below(30),
            Widget::row(vec![
                Widget::col(vec![
                    Line("Time").fg(Color::BLACK).draw(ctx),
                    Widget::draw_svg_transform(
                        ctx,
                        "system/assets/tools/time.svg",
                        RewriteColor::ChangeAll(Color::BLACK),
                    ),
                    Text::from_multiline(vec![
                        Line("Until the VIP's").fg(Color::BLACK),
                        Line("last trip is done").fg(Color::BLACK),
                    ])
                    .draw(ctx),
                ]),
                Widget::col(vec![
                    Line("Goal").fg(Color::BLACK).draw(ctx),
                    Widget::draw_svg_transform(
                        ctx,
                        "system/assets/tools/location.svg",
                        RewriteColor::ChangeAll(Color::BLACK),
                    ),
                    Text::from_multiline(vec![
                        Line("Speed up the VIP's trips").fg(Color::BLACK),
                        Line(format!("by at least {}", goal)).fg(Color::BLACK),
                    ])
                    .draw(ctx),
                ]),
                Widget::col(vec![
                    Line("Score").fg(Color::BLACK).draw(ctx),
                    Widget::draw_svg_transform(
                        ctx,
                        "system/assets/tools/star.svg",
                        RewriteColor::ChangeAll(Color::BLACK),
                    ),
                    Text::from_multiline(vec![
                        Line("How much time").fg(Color::BLACK),
                        Line("the VIP saves").fg(Color::BLACK),
                    ])
                    .draw(ctx),
                ]),
            ])
            .evenly_spaced(),
        ])
    })
}