1use std::collections::BTreeMap;
2
3use geom::{Duration, Percent};
4use synthpop::OrigPersonID;
5use widgetry::{EventCtx, Key, Line, Panel, SimpleState, State, Text, TextExt, Widget};
6
7use crate::app::App;
8use crate::app::Transition;
9use crate::sandbox::gameplay::Tutorial;
10use crate::sandbox::{GameplayMode, SandboxMode};
11
12pub mod cutscene;
13pub mod prebake;
14
15pub struct Challenge {
17 title: String,
18 pub description: Vec<String>,
19 pub alias: String,
20 pub gameplay: GameplayMode,
21 pub cutscene: Option<fn(&mut EventCtx, &App, &GameplayMode) -> Box<dyn State<App>>>,
22}
23
24pub struct HighScore {
25 pub goal: String,
27 pub score: Duration,
29 pub edits_name: String,
30}
31
32impl HighScore {
33 pub fn record(self, app: &mut App, mode: GameplayMode) {
34 let scores = app.session.high_scores.entry(mode).or_insert_with(Vec::new);
38 scores.push(self);
39 scores.sort_by_key(|s| s.score);
40 scores.reverse();
41 }
42}
43
44impl Challenge {
45 pub fn all() -> BTreeMap<String, Vec<Challenge>> {
46 let mut tree = BTreeMap::new();
47 tree.insert(
48 "Optimize one commute".to_string(),
49 vec![
51 Challenge {
52 title: "Part 1".to_string(),
53 description: vec!["Speed up one VIP's daily commute, at any cost!".to_string()],
54 alias: "commute/pt1".to_string(),
55 gameplay: GameplayMode::OptimizeCommute(
56 OrigPersonID(140824, 2),
57 Duration::minutes(2) + Duration::seconds(30.0),
58 ),
59 cutscene: Some(
60 crate::sandbox::gameplay::commute::OptimizeCommute::cutscene_pt1,
61 ),
62 },
63 Challenge {
64 title: "Part 2".to_string(),
65 description: vec!["Speed up another VIP's commute".to_string()],
66 alias: "commute/pt2".to_string(),
67 gameplay: GameplayMode::OptimizeCommute(
68 OrigPersonID(141039, 2),
69 Duration::minutes(5),
70 ),
71 cutscene: Some(
72 crate::sandbox::gameplay::commute::OptimizeCommute::cutscene_pt2,
73 ),
74 },
75 ],
76 );
77 tree.insert(
78 "Traffic signal survivor".to_string(),
79 vec![Challenge {
80 title: "Traffic signal survivor".to_string(),
81 description: vec!["Fix traffic signal timing and unblock vehicles".to_string()],
82 alias: "trafficsig/pt1".to_string(),
83 gameplay: GameplayMode::FixTrafficSignals,
84 cutscene: Some(
85 crate::sandbox::gameplay::fix_traffic_signals::FixTrafficSignals::cutscene_pt1,
86 ),
87 }],
88 );
89
90 tree
91 }
92
93 pub fn find(mode: &GameplayMode) -> (Challenge, Option<Challenge>) {
95 for (_, stages) in Challenge::all() {
97 let mut current = None;
98 for challenge in stages {
99 if let Some(c) = current {
100 return (c, Some(challenge));
101 }
102 if &challenge.gameplay == mode {
103 current = Some(challenge);
104 }
105 }
106 if let Some(c) = current {
107 return (c, None);
108 }
109 }
110 unreachable!()
111 }
112}
113
114pub struct ChallengesPicker {
115 links: BTreeMap<String, (String, usize)>,
116 challenge: Option<Challenge>,
117}
118
119impl ChallengesPicker {
120 pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
121 ChallengesPicker::make(ctx, app, None)
122 }
123
124 fn make(
125 ctx: &mut EventCtx,
126 app: &App,
127 challenge_and_stage: Option<(String, usize)>,
128 ) -> Box<dyn State<App>> {
129 let mut links = BTreeMap::new();
130 let mut master_col = vec![
131 Widget::row(vec![
132 Line("Challenges").small_heading().into_widget(ctx),
133 ctx.style().btn_close_widget(ctx),
134 ]),
135 ctx.style()
136 .btn_outline
137 .text("Introduction and tutorial")
138 .build_def(ctx)
139 .container()
140 .section(ctx),
141 ];
142
143 let mut flex_row = Vec::new();
145 for (idx, (name, _)) in Challenge::all().into_iter().enumerate() {
146 let is_current_stage = challenge_and_stage
147 .as_ref()
148 .map(|(n, _)| n == &name)
149 .unwrap_or(false);
150 flex_row.push(
151 ctx.style()
152 .btn_outline
153 .text(&name)
154 .disabled(is_current_stage)
155 .hotkey(Key::NUM_KEYS[idx])
156 .build_def(ctx),
157 );
158 links.insert(name.clone(), (name, 0));
159 }
160 master_col.push(
161 Widget::custom_row(flex_row)
162 .flex_wrap(ctx, Percent::int(80))
163 .section(ctx),
164 );
165
166 let mut main_row = Vec::new();
167
168 if let Some((ref name, current)) = challenge_and_stage {
170 let mut col = Vec::new();
171 for (idx, stage) in Challenge::all()
172 .remove(name)
173 .unwrap()
174 .into_iter()
175 .enumerate()
176 {
177 col.push(
178 ctx.style()
179 .btn_outline
180 .text(&stage.title)
181 .disabled(current == idx)
182 .build_def(ctx),
183 );
184 links.insert(stage.title, (name.to_string(), idx));
185 }
186 main_row.push(Widget::col(col).section(ctx));
187 }
188
189 let mut current_challenge = None;
191 if let Some((ref name, current)) = challenge_and_stage {
192 let challenge = Challenge::all().remove(name).unwrap().remove(current);
193 let mut txt = Text::new();
194 for l in &challenge.description {
195 txt.add_line(l);
196 }
197
198 let mut inner_col = vec![
199 txt.into_widget(ctx),
200 ctx.style()
201 .btn_outline
202 .text("Start!")
203 .hotkey(Key::Enter)
204 .build_def(ctx),
205 ];
206
207 if let Some(scores) = app.session.high_scores.get(&challenge.gameplay) {
208 let mut txt = Text::from(format!("{} high scores:", scores.len()));
209 txt.add_line(format!("Goal: {}", scores[0].goal));
210 let mut idx = 1;
211 for score in scores {
212 txt.add_line(format!(
213 "{}) {}, using proposal: {}",
214 idx, score.score, score.edits_name
215 ));
216 idx += 1;
217 }
218 inner_col.push(txt.into_widget(ctx));
219 } else {
220 inner_col.push("No attempts yet".text_widget(ctx));
221 }
222
223 main_row.push(Widget::col(inner_col).section(ctx));
224 current_challenge = Some(challenge);
225 }
226
227 master_col.push(Widget::row(main_row));
228
229 let panel = Panel::new_builder(Widget::col(master_col)).build(ctx);
230 <dyn SimpleState<_>>::new_state(
231 panel,
232 Box::new(ChallengesPicker {
233 links,
234 challenge: current_challenge,
235 }),
236 )
237 }
238}
239
240impl SimpleState<App> for ChallengesPicker {
241 fn on_click(
242 &mut self,
243 ctx: &mut EventCtx,
244 app: &mut App,
245 x: &str,
246 _: &mut Panel,
247 ) -> Transition {
248 match x {
249 "close" => Transition::Pop,
250 "Introduction and tutorial" => Transition::Replace(Tutorial::start(ctx, app)),
251 "Start!" => {
252 #[cfg(not(target_arch = "wasm32"))]
253 {
254 let map_name = self
255 .challenge
256 .as_ref()
257 .map(|c| c.gameplay.map_name())
258 .unwrap();
259 if !abstio::file_exists(map_name.path()) {
260 return map_gui::tools::prompt_to_download_missing_data(
263 ctx,
264 map_name,
265 Box::new(|ctx, _| {
266 Transition::Replace(widgetry::tools::PopupMsg::new_state(
267 ctx,
268 "Download complete",
269 vec!["Download complete. Click 'Start!' again"],
270 ))
271 }),
272 );
273 }
274 }
275
276 let challenge = self.challenge.take().unwrap();
277 let sandbox = SandboxMode::simple_new(app, challenge.gameplay.clone());
279 if let Some(cutscene) = challenge.cutscene {
280 Transition::Multi(vec![
281 Transition::Replace(sandbox),
282 Transition::Push(cutscene(ctx, app, &challenge.gameplay)),
283 ])
284 } else {
285 Transition::Replace(sandbox)
286 }
287 }
288 x => Transition::Replace(ChallengesPicker::make(ctx, app, self.links.remove(x))),
289 }
290 }
291}