1use geom::Percent;
2use widgetry::tools::open_browser;
3use widgetry::{
4 ButtonBuilder, Color, ControlState, EdgeInsets, EventCtx, GeomBatch, GfxCtx, Key, Line, Panel,
5 RewriteColor, SimpleState, State, Text, TextExt, Widget,
6};
7
8use crate::levels::Level;
9use crate::{App, Transition};
10
11pub struct TitleScreen;
12
13impl TitleScreen {
14 pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
15 let mut level_buttons = Vec::new();
16 for (idx, level) in app.session.levels.iter().enumerate() {
17 if idx < app.session.levels_unlocked {
18 level_buttons.push(unlocked_level(ctx, app, level, idx).margin_below(16));
19 } else {
20 level_buttons.push(locked_level(ctx, app, level, idx).margin_below(16));
21 }
22 }
23
24 <dyn SimpleState<_>>::new_state(
25 Panel::new_builder(Widget::col(vec![
26 Line("15-minute Santa")
27 .display_title()
28 .into_widget(ctx)
29 .container()
30 .padding(16)
31 .bg(Color::BLACK.alpha(0.8))
32 .centered_horiz(),
33 Text::from(
34 Line(
35 "Time for Santa to deliver presents in Seattle! But... COVID means no \
36 stopping in houses to munch on cookies (gluten-free and paleo, \
37 obviously). When your blood sugar gets low, you'll have to stop and \
38 refill your supply from a store. Those are close to where people live... \
39 right?",
40 )
41 .small_heading(),
42 )
43 .wrap_to_pct(ctx, 50)
44 .into_widget(ctx)
45 .container()
46 .padding(16)
47 .bg(Color::BLACK.alpha(0.8))
48 .centered_horiz(),
49 Widget::custom_row(level_buttons).flex_wrap(ctx, Percent::int(80)),
50 Widget::row(vec![
51 map_gui::tools::home_btn(ctx),
52 ctx.style()
53 .btn_outline
54 .text("Credits")
55 .build_def(ctx)
56 .centered_vert(),
57 "Created by Dustin Carlino, Yuwen Li, & Michael Kirk"
58 .text_widget(ctx)
59 .centered_vert(),
60 ])
61 .centered_horiz()
62 .align_bottom()
63 .bg(Color::BLACK.alpha(0.8)),
64 ]))
65 .build_custom(ctx),
66 Box::new(TitleScreen),
67 )
68 }
69}
70
71impl SimpleState<App> for TitleScreen {
72 fn on_click(
73 &mut self,
74 ctx: &mut EventCtx,
75 app: &mut App,
76 x: &str,
77 _: &mut Panel,
78 ) -> Transition {
79 match x {
80 "Home" => Transition::Clear(vec![map_gui::tools::TitleScreen::new_state(
81 ctx,
82 app,
83 map_gui::tools::Executable::Santa,
84 Box::new(|ctx, app, _| Self::new_state(ctx, app)),
85 )]),
86 "Credits" => Transition::Push(Credits::new_state(ctx)),
87 x => {
88 for level in &app.session.levels {
89 if x == level.title {
90 #[cfg(not(target_arch = "wasm32"))]
91 {
92 let map_name = level.map.clone();
93 if !abstio::file_exists(map_name.path()) {
94 let level = level.clone();
95 return map_gui::tools::prompt_to_download_missing_data(
96 ctx,
97 map_name,
98 Box::new(move |ctx, app| {
99 Transition::Replace(crate::before_level::Picker::new_state(
100 ctx, app, level,
101 ))
102 }),
103 );
104 }
105 }
106
107 return Transition::Push(crate::before_level::Picker::new_state(
108 ctx,
109 app,
110 level.clone(),
111 ));
112 }
113 }
114 panic!("Unknown action {}", x);
115 }
116 }
117 }
118
119 fn other_event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
120 app.session.update_music(ctx);
121 Transition::Keep
122 }
123
124 fn draw(&self, g: &mut GfxCtx, app: &App) {
125 app.session.music.draw(g);
126 }
127}
128
129fn level_btn(ctx: &mut EventCtx, app: &App, level: &Level, idx: usize) -> GeomBatch {
130 let mut txt = Text::new();
131 txt.add_line(Line(format!("LEVEL {}", idx + 1)).small_heading());
132 txt.add_line(Line(&level.title).small_heading());
133 txt.add_line(&level.description);
134 let batch = txt.wrap_to_pct(ctx, 15).render_autocropped(ctx);
135
136 let (mut batch, hitbox) = batch
138 .batch()
139 .container()
140 .padding(EdgeInsets {
141 top: 20.0,
142 bottom: 20.0,
143 left: 10.0,
144 right: 10.0,
145 })
146 .into_geom(ctx, None);
147 batch.unshift(app.cs.unzoomed_bike, hitbox);
148 batch
149}
150
151fn locked_level(ctx: &mut EventCtx, app: &App, level: &Level, idx: usize) -> Widget {
153 let mut batch = level_btn(ctx, app, level, idx);
154 let hitbox = batch.get_bounds().get_rectangle();
155 let center = hitbox.center();
156 batch.push(app.cs.fade_map_dark, hitbox);
157 batch.append(GeomBatch::load_svg(ctx, "system/assets/tools/locked.svg").centered_on(center));
158 batch.into_widget(ctx)
159}
160
161fn unlocked_level(ctx: &mut EventCtx, app: &App, level: &Level, idx: usize) -> Widget {
162 let normal = level_btn(ctx, app, level, idx);
163 let hovered = normal
164 .clone()
165 .color(RewriteColor::Change(Color::WHITE, Color::WHITE.alpha(0.6)));
166
167 ButtonBuilder::new()
168 .custom_batch(normal, ControlState::Default)
169 .custom_batch(hovered, ControlState::Hovered)
170 .build_widget(ctx, &level.title)
171}
172
173struct Credits;
174
175impl Credits {
176 fn new_state(ctx: &mut EventCtx) -> Box<dyn State<App>> {
177 <dyn SimpleState<_>>::new_state(
178 Panel::new_builder(Widget::col(vec![
179 Widget::row(vec![
180 Line("15-minute Santa").big_heading_plain().into_widget(ctx),
181 ctx.style().btn_close_widget(ctx),
182 ]),
183 link(
184 ctx,
185 "Created by the A/B Street team",
186 "https://abstreet.org"
187 ),
188 Text::from_multiline(vec![
189 Line("Lead: Dustin Carlino"),
190 Line("Programming & game design: Michael Kirk"),
191 Line("UI/UX: Yuwen Li"),
192 ]).into_widget(ctx),
193 link(
194 ctx,
195 "Santa made by @parallaxcreativedesign",
196 "https://www.instagram.com/parallaxcreativedesign/",
197 ),
198 link(
199 ctx,
200 "Map data thanks to OpenStreetMap contributors",
201 "https://www.openstreetmap.org/about"),
202 link(ctx, "Land use data from Seattle GeoData", "https://data-seattlecitygis.opendata.arcgis.com/datasets/current-land-use-zoning-detail"),
203 link(ctx, "Music from various sources", "https://github.com/a-b-street/abstreet/tree/master/data/system/assets/music/sources.md"),
204 link(ctx, "Fonts and icons by various sources", "https://a-b-street.github.io/docs/howto/#data-source-licensing"),
205 "Playtesting by Fridgehaus".text_widget(ctx),
206 ctx.style().btn_outline.text("Back").hotkey(Key::Enter).build_def(ctx).centered_horiz(),
207 ]))
208 .build(ctx), Box::new(Credits))
209 }
210}
211
212fn link(ctx: &mut EventCtx, label: &str, url: &str) -> Widget {
213 ctx.style()
214 .btn_plain
215 .text(label)
216 .build_widget(ctx, format!("open {}", url))
217}
218
219impl SimpleState<App> for Credits {
220 fn on_click(&mut self, _: &mut EventCtx, _: &mut App, x: &str, _: &mut Panel) -> Transition {
221 match x {
222 "close" | "Back" => Transition::Pop,
223 x => {
224 if let Some(url) = x.strip_prefix("open ") {
225 open_browser(url);
226 return Transition::Keep;
227 }
228
229 unreachable!()
230 }
231 }
232 }
233
234 fn other_event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
235 app.session.update_music(ctx);
236 Transition::Keep
237 }
238
239 fn draw(&self, g: &mut GfxCtx, app: &App) {
240 app.session.music.draw(g);
241 }
242}