1use map_gui::tools::grey_out_map;
2use widgetry::{
3 hotkeys, ButtonStyle, Color, ControlState, EventCtx, GeomBatch, GfxCtx, Image, Key, Line,
4 Outcome, Panel, State, Text, Widget,
5};
6
7use crate::app::App;
8use crate::app::Transition;
9
10pub struct CutsceneBuilder {
11 name: String,
12 scenes: Vec<Scene>,
13}
14
15enum Layout {
16 PlayerSpeaking,
17 BossSpeaking,
18 Extra(&'static str, f64),
19}
20
21struct Scene {
22 layout: Layout,
23 msg: Text,
24}
25
26impl CutsceneBuilder {
27 pub fn new(name: &str) -> CutsceneBuilder {
28 CutsceneBuilder {
29 name: name.to_string(),
30 scenes: Vec::new(),
31 }
32 }
33
34 fn fg_color() -> Color {
35 ButtonStyle::outline_dark_fg().fg
36 }
37
38 pub fn player<I: Into<String>>(mut self, msg: I) -> CutsceneBuilder {
39 self.scenes.push(Scene {
40 layout: Layout::PlayerSpeaking,
41 msg: Text::from(Line(msg).fg(Self::fg_color())),
42 });
43 self
44 }
45
46 pub fn boss<I: Into<String>>(mut self, msg: I) -> CutsceneBuilder {
47 self.scenes.push(Scene {
48 layout: Layout::BossSpeaking,
49 msg: Text::from(Line(msg).fg(Self::fg_color())),
50 });
51 self
52 }
53
54 pub fn extra<I: Into<String>>(
55 mut self,
56 character: &'static str,
57 scale: f64,
58 msg: I,
59 ) -> CutsceneBuilder {
60 self.scenes.push(Scene {
61 layout: Layout::Extra(character, scale),
62 msg: Text::from(Line(msg).fg(Self::fg_color())),
63 });
64 self
65 }
66
67 pub fn build(
68 self,
69 ctx: &mut EventCtx,
70 make_task: Box<dyn Fn(&mut EventCtx) -> Widget>,
71 ) -> Box<dyn State<App>> {
72 Box::new(CutscenePlayer {
73 panel: make_panel(ctx, &self.name, &self.scenes, &make_task, 0),
74 name: self.name,
75 scenes: self.scenes,
76 idx: 0,
77 make_task,
78 })
79 }
80}
81
82struct CutscenePlayer {
83 name: String,
84 scenes: Vec<Scene>,
85 idx: usize,
86 panel: Panel,
87 make_task: Box<dyn Fn(&mut EventCtx) -> Widget>,
88}
89
90impl State<App> for CutscenePlayer {
91 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
92 if let Outcome::Clicked(x) = self.panel.event(ctx) {
93 match x.as_ref() {
94 "quit" => {
95 app.primary.clear_sim();
97 app.set_prebaked(None);
98 return Transition::Multi(vec![Transition::Pop, Transition::Pop]);
99 }
100 "back" => {
101 self.idx -= 1;
102 self.panel =
103 make_panel(ctx, &self.name, &self.scenes, &self.make_task, self.idx);
104 }
105 "next" => {
106 self.idx += 1;
107 self.panel =
108 make_panel(ctx, &self.name, &self.scenes, &self.make_task, self.idx);
109 }
110 "Skip cutscene" => {
111 self.idx = self.scenes.len();
112 self.panel =
113 make_panel(ctx, &self.name, &self.scenes, &self.make_task, self.idx);
114 }
115 "Start" => {
116 return Transition::Pop;
117 }
118 _ => unreachable!(),
119 }
120 }
121 if ctx.input.is_window_resized() {
123 self.panel = make_panel(ctx, &self.name, &self.scenes, &self.make_task, self.idx);
124 }
125
126 Transition::Keep
127 }
128
129 fn draw(&self, g: &mut GfxCtx, _: &App) {
130 self.panel.draw(g);
131 }
132}
133
134fn make_panel(
135 ctx: &mut EventCtx,
136 name: &str,
137 scenes: &[Scene],
138 make_task: &dyn Fn(&mut EventCtx) -> Widget,
139 idx: usize,
140) -> Panel {
141 let prev_builder = ButtonStyle::plain_dark_fg()
142 .icon("system/assets/tools/circled_prev.svg")
143 .image_dims(45.0)
144 .hotkey(Key::LeftArrow)
145 .bg_color(Color::CLEAR, ControlState::Disabled);
146
147 let next = prev_builder
148 .clone()
149 .image_path("system/assets/tools/circled_next.svg")
150 .hotkey(hotkeys(vec![Key::RightArrow, Key::Space, Key::Enter]))
151 .build_widget(ctx, "next");
152
153 let prev = prev_builder.disabled(idx == 0).build_widget(ctx, "back");
154
155 let inner = if idx == scenes.len() {
156 Widget::custom_col(vec![
157 (make_task)(ctx),
158 ctx.style()
159 .btn_solid_primary
160 .text("Start")
161 .hotkey(Key::Enter)
162 .build_def(ctx)
163 .centered_horiz()
164 .align_bottom(),
165 ])
166 } else {
167 Widget::custom_col(vec![
168 match scenes[idx].layout {
169 Layout::PlayerSpeaking => Widget::custom_row(vec![
170 GeomBatch::load_svg(ctx, "system/assets/characters/boss.svg.gz")
171 .scale(0.75)
172 .autocrop()
173 .into_widget(ctx),
174 Widget::custom_row(vec![
175 scenes[idx]
176 .msg
177 .clone()
178 .wrap_to_pct(ctx, 30)
179 .into_widget(ctx),
180 Image::from_path("system/assets/characters/player.svg")
181 .untinted()
182 .into_widget(ctx),
183 ])
184 .align_right(),
185 ]),
186 Layout::BossSpeaking => Widget::custom_row(vec![
187 GeomBatch::load_svg(ctx, "system/assets/characters/boss.svg.gz")
188 .scale(0.75)
189 .autocrop()
190 .into_widget(ctx),
191 scenes[idx]
192 .msg
193 .clone()
194 .wrap_to_pct(ctx, 30)
195 .into_widget(ctx),
196 Image::from_path("system/assets/characters/player.svg")
197 .untinted()
198 .into_widget(ctx)
199 .align_right(),
200 ]),
201 Layout::Extra(filename, scale) => Widget::custom_row(vec![
202 GeomBatch::load_svg(ctx, "system/assets/characters/boss.svg.gz")
203 .scale(0.75)
204 .autocrop()
205 .into_widget(ctx),
206 Widget::col(vec![
207 GeomBatch::load_svg(
208 ctx.prerender,
209 format!("system/assets/characters/{}", filename),
210 )
211 .scale(scale)
212 .autocrop()
213 .into_widget(ctx),
214 scenes[idx]
215 .msg
216 .clone()
217 .wrap_to_pct(ctx, 30)
218 .into_widget(ctx),
219 ]),
220 Image::from_path("system/assets/characters/player.svg")
221 .untinted()
222 .into_widget(ctx),
223 ])
224 .evenly_spaced(),
225 }
226 .margin_above(100),
227 Widget::col(vec![
228 Widget::row(vec![prev.margin_right(40), next]).centered_horiz(),
229 ButtonStyle::outline_dark_fg()
230 .text("Skip cutscene")
231 .build_def(ctx)
232 .centered_horiz(),
233 ])
234 .align_bottom(),
235 ])
236 };
237
238 let col = vec![
239 Widget::row(vec![
240 Line(name).small_heading().into_widget(ctx),
241 ctx.style()
242 .btn_back("Home")
243 .build_widget(ctx, "quit")
244 .align_right(),
245 ])
246 .margin_below(40),
247 inner
248 .fill_height()
249 .padding(42)
250 .bg(Color::WHITE)
251 .outline(ctx.style().btn_solid.outline),
252 ];
253
254 Panel::new_builder(Widget::col(col)).build(ctx)
255}
256
257pub struct ShowMessage {
258 panel: Panel,
259}
260
261impl ShowMessage {
262 pub fn new_state(ctx: &mut EventCtx, contents: Widget, bg: Color) -> Box<dyn State<App>> {
263 Box::new(ShowMessage {
264 panel: Panel::new_builder(
265 Widget::custom_col(vec![
266 contents,
267 ctx.style()
268 .btn_solid_primary
269 .text("OK")
270 .hotkey(hotkeys(vec![Key::Escape, Key::Space, Key::Enter]))
271 .build_def(ctx)
272 .centered_horiz()
273 .align_bottom(),
274 ])
275 .padding(16)
276 .bg(bg),
277 )
278 .exact_size_percent(50, 50)
279 .build_custom(ctx),
280 })
281 }
282}
283
284impl State<App> for ShowMessage {
285 fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
286 match self.panel.event(ctx) {
287 Outcome::Clicked(x) => match x.as_ref() {
288 "OK" => Transition::Pop,
289 _ => unreachable!(),
290 },
291 _ => Transition::Keep,
292 }
293 }
294
295 fn draw(&self, g: &mut GfxCtx, app: &App) {
296 grey_out_map(g, app);
297 self.panel.draw(g);
298 }
299}