santa/
after_level.rs

1use abstutil::prettyprint_usize;
2use geom::{Distance, PolyLine, Pt2D, Tessellation};
3use widgetry::tools::{ColorLegend, PopupMsg};
4use widgetry::{
5    Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Panel,
6    SimpleState, State, Text, VerticalAlignment, Widget,
7};
8
9use crate::buildings::{BldgState, Buildings};
10use crate::levels::Level;
11use crate::title::TitleScreen;
12use crate::{App, Transition};
13
14const ZOOM: f64 = 2.0;
15
16pub struct Strategize {
17    unlock_messages: Option<Vec<String>>,
18    draw_all: Drawable,
19}
20
21impl Strategize {
22    pub fn new_state(
23        ctx: &mut EventCtx,
24        app: &mut App,
25        score: usize,
26        level: &Level,
27        bldgs: &Buildings,
28        path: RecordPath,
29    ) -> Box<dyn State<App>> {
30        ctx.canvas.cam_zoom = ZOOM;
31
32        let intersection_id = app
33            .map
34            .find_i_by_pt2d(app.map.localise_lon_lat_to_map(level.start))
35            .expect("Failed to get level start point");
36
37        let start = app.map.get_i(intersection_id).polygon.center();
38        ctx.canvas.center_on_map_pt(start);
39
40        let unlock_messages = app.session.record_score(level.title.clone(), score);
41
42        let mut txt = Text::new();
43        txt.add_line(Line(format!("Results for {}", level.title)).small_heading());
44        txt.add_line(format!(
45            "You delivered {} presents",
46            prettyprint_usize(score)
47        ));
48        txt.add_line("");
49        txt.add_line("High scores:");
50        for (idx, score) in app.session.high_scores[&level.title].iter().enumerate() {
51            txt.add_line(format!("{}) {}", idx + 1, prettyprint_usize(*score)));
52        }
53
54        // Partly duplicated with Buildings::new, but we want to label upzones and finished houses
55        // differently
56        let mut batch = GeomBatch::new();
57        for b in app.map.all_buildings() {
58            match bldgs.buildings[&b.id] {
59                BldgState::Undelivered(num_housing_units) => {
60                    batch.push(
61                        if num_housing_units > 5 {
62                            app.session.colors.apartment
63                        } else {
64                            app.session.colors.house
65                        },
66                        b.polygon.clone(),
67                    );
68                    if num_housing_units > 1 {
69                        batch.append(
70                            Text::from(Line(num_housing_units.to_string()).fg(Color::RED))
71                                .render_autocropped(ctx)
72                                .scale(0.2)
73                                .centered_on(b.label_center),
74                        );
75                    }
76                }
77                BldgState::Store => {
78                    batch.push(
79                        if bldgs.upzones.contains(&b.id) {
80                            Color::PINK
81                        } else {
82                            app.session.colors.store
83                        },
84                        b.polygon.clone(),
85                    );
86                }
87                BldgState::Done => {
88                    batch.push(Color::RED, b.polygon.clone());
89                }
90                BldgState::Ignore => {
91                    batch.push(app.session.colors.visited, b.polygon.clone());
92                }
93            }
94        }
95
96        batch.push(Color::CYAN, path.render(Distance::meters(2.0)));
97
98        let panel = Panel::new_builder(Widget::col(vec![
99            txt.into_widget(ctx),
100            ctx.style()
101                .btn_outline
102                .text("Back to title screen")
103                .hotkey(Key::Enter)
104                .build_def(ctx),
105            Widget::row(vec![
106                ColorLegend::row(ctx, app.session.colors.house, "house"),
107                ColorLegend::row(ctx, app.session.colors.apartment, "apartment"),
108                ColorLegend::row(ctx, app.session.colors.store, "store"),
109            ]),
110            Widget::row(vec![
111                ColorLegend::row(ctx, Color::PINK, "upzoned store"),
112                ColorLegend::row(ctx, Color::RED, "delivered!"),
113            ])
114            .evenly_spaced(),
115        ]))
116        .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
117        .build(ctx);
118        <dyn SimpleState<_>>::new_state(
119            panel,
120            Box::new(Strategize {
121                unlock_messages,
122                draw_all: ctx.upload(batch),
123            }),
124        )
125    }
126}
127
128impl SimpleState<App> for Strategize {
129    fn on_click(
130        &mut self,
131        ctx: &mut EventCtx,
132        app: &mut App,
133        x: &str,
134        _: &mut Panel,
135    ) -> Transition {
136        match x {
137            "Back to title screen" => {
138                let mut transitions = vec![
139                    Transition::Pop,
140                    Transition::Replace(TitleScreen::new_state(ctx, app)),
141                ];
142                if let Some(msgs) = self.unlock_messages.take() {
143                    transitions.push(Transition::Push(PopupMsg::new_state(
144                        ctx,
145                        "Level complete!",
146                        msgs,
147                    )));
148                }
149                Transition::Multi(transitions)
150            }
151            _ => unreachable!(),
152        }
153    }
154
155    fn other_event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
156        ctx.canvas_movement();
157        app.session.update_music(ctx);
158        Transition::Keep
159    }
160
161    fn draw(&self, g: &mut GfxCtx, app: &App) {
162        g.redraw(&self.draw_all);
163        app.session.music.draw(g);
164    }
165}
166
167pub struct Results;
168
169impl Results {
170    pub fn new_state(
171        ctx: &mut EventCtx,
172        app: &mut App,
173        score: usize,
174        level: &Level,
175    ) -> Box<dyn State<App>> {
176        let mut txt = Text::new();
177        if score < level.goal {
178            txt.add_line(Line("Not quite...").small_heading());
179            txt.add_line(format!(
180                "You only delivered {} / {} presents",
181                prettyprint_usize(score),
182                prettyprint_usize(level.goal)
183            ));
184            txt.add_line("Review your route and try again.");
185            txt.add_line("");
186            txt.add_line("Hint: look for any apartments you missed!");
187        } else {
188            txt.add_line(Line("Thank you, Santa!").small_heading());
189            txt.add_line(format!(
190                "You delivered {} presents, more than the goal of {}!",
191                prettyprint_usize(score),
192                prettyprint_usize(level.goal)
193            ));
194            let high_score = app.session.high_scores[&level.title][0];
195            if high_score == score {
196                txt.add_line("Wow, a new high score!");
197            } else {
198                txt.add_line(format!(
199                    "But can you beat the high score of {}?",
200                    prettyprint_usize(high_score)
201                ));
202            }
203        }
204
205        <dyn SimpleState<_>>::new_state(
206            Panel::new_builder(Widget::col(vec![
207                txt.into_widget(ctx),
208                ctx.style()
209                    .btn_solid_primary
210                    .text("OK")
211                    .hotkey(Key::Enter)
212                    .build_def(ctx),
213            ]))
214            .build(ctx),
215            Box::new(Results),
216        )
217    }
218}
219
220impl SimpleState<App> for Results {
221    fn on_click(&mut self, _: &mut EventCtx, _: &mut App, x: &str, _: &mut Panel) -> Transition {
222        match x {
223            "OK" => Transition::Pop,
224            _ => unreachable!(),
225        }
226    }
227
228    fn other_event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
229        app.session.update_music(ctx);
230        Transition::Keep
231    }
232
233    fn draw(&self, g: &mut GfxCtx, app: &App) {
234        app.session.music.draw(g);
235    }
236}
237
238pub struct RecordPath {
239    pts: Vec<Pt2D>,
240}
241
242impl RecordPath {
243    pub fn new() -> RecordPath {
244        RecordPath { pts: Vec::new() }
245    }
246
247    pub fn add_pt(&mut self, pt: Pt2D) {
248        // Do basic compression along the way
249        let len = self.pts.len();
250        if len >= 2 {
251            let same_line = self.pts[len - 2]
252                .angle_to(self.pts[len - 1])
253                .approx_eq(self.pts[len - 1].angle_to(pt), 0.1);
254            if same_line {
255                self.pts.pop();
256            }
257        }
258
259        self.pts.push(pt);
260    }
261
262    pub fn render(mut self, thickness: Distance) -> Tessellation {
263        self.pts.dedup();
264        PolyLine::unchecked_new(self.pts).thicken_tessellation(thickness)
265    }
266}