1 use crate::app::App;
2 use crate::game::{DrawBaselayer, State, Transition};
3 use widgetry::{
4     hotkey, hotkeys, Btn, Color, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, Panel,
5     RewriteColor, Text, Widget,
6 };
7 
8 pub struct CutsceneBuilder {
9     name: String,
10     scenes: Vec<Scene>,
11 }
12 
13 enum Layout {
14     PlayerSpeaking,
15     BossSpeaking,
16     Extra(&'static str, f64),
17 }
18 
19 struct Scene {
20     layout: Layout,
21     msg: Text,
22 }
23 
24 impl CutsceneBuilder {
new(name: &str) -> CutsceneBuilder25     pub fn new(name: &str) -> CutsceneBuilder {
26         CutsceneBuilder {
27             name: name.to_string(),
28             scenes: Vec::new(),
29         }
30     }
31 
player<I: Into<String>>(mut self, msg: I) -> CutsceneBuilder32     pub fn player<I: Into<String>>(mut self, msg: I) -> CutsceneBuilder {
33         self.scenes.push(Scene {
34             layout: Layout::PlayerSpeaking,
35             msg: Text::from(Line(msg).fg(Color::BLACK)),
36         });
37         self
38     }
39 
boss<I: Into<String>>(mut self, msg: I) -> CutsceneBuilder40     pub fn boss<I: Into<String>>(mut self, msg: I) -> CutsceneBuilder {
41         self.scenes.push(Scene {
42             layout: Layout::BossSpeaking,
43             msg: Text::from(Line(msg).fg(Color::BLACK)),
44         });
45         self
46     }
47 
extra<I: Into<String>>( mut self, character: &'static str, scale: f64, msg: I, ) -> CutsceneBuilder48     pub fn extra<I: Into<String>>(
49         mut self,
50         character: &'static str,
51         scale: f64,
52         msg: I,
53     ) -> CutsceneBuilder {
54         self.scenes.push(Scene {
55             layout: Layout::Extra(character, scale),
56             msg: Text::from(Line(msg).fg(Color::BLACK)),
57         });
58         self
59     }
60 
build( self, ctx: &mut EventCtx, app: &App, make_task: Box<dyn Fn(&mut EventCtx) -> Widget>, ) -> Box<dyn State>61     pub fn build(
62         self,
63         ctx: &mut EventCtx,
64         app: &App,
65         make_task: Box<dyn Fn(&mut EventCtx) -> Widget>,
66     ) -> Box<dyn State> {
67         Box::new(CutscenePlayer {
68             panel: make_panel(ctx, app, &self.name, &self.scenes, &make_task, 0),
69             name: self.name,
70             scenes: self.scenes,
71             idx: 0,
72             make_task,
73         })
74     }
75 }
76 
77 struct CutscenePlayer {
78     name: String,
79     scenes: Vec<Scene>,
80     idx: usize,
81     panel: Panel,
82     make_task: Box<dyn Fn(&mut EventCtx) -> Widget>,
83 }
84 
85 impl State for CutscenePlayer {
event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition86     fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
87         match self.panel.event(ctx) {
88             Outcome::Clicked(x) => match x.as_ref() {
89                 "quit" => {
90                     // TODO Should SandboxMode use on_destroy for this?
91                     app.primary.clear_sim();
92                     app.set_prebaked(None);
93                     return Transition::Multi(vec![Transition::Pop, Transition::Pop]);
94                 }
95                 "back" => {
96                     self.idx -= 1;
97                     self.panel = make_panel(
98                         ctx,
99                         app,
100                         &self.name,
101                         &self.scenes,
102                         &self.make_task,
103                         self.idx,
104                     );
105                 }
106                 "next" => {
107                     self.idx += 1;
108                     self.panel = make_panel(
109                         ctx,
110                         app,
111                         &self.name,
112                         &self.scenes,
113                         &self.make_task,
114                         self.idx,
115                     );
116                 }
117                 "Skip cutscene" => {
118                     self.idx = self.scenes.len();
119                     self.panel = make_panel(
120                         ctx,
121                         app,
122                         &self.name,
123                         &self.scenes,
124                         &self.make_task,
125                         self.idx,
126                     );
127                 }
128                 "Start" => {
129                     return Transition::Pop;
130                 }
131                 _ => unreachable!(),
132             },
133             _ => {}
134         }
135         // TODO Should the Panel for text widgets with wrapping do this instead?
136         if ctx.input.is_window_resized() {
137             self.panel = make_panel(
138                 ctx,
139                 app,
140                 &self.name,
141                 &self.scenes,
142                 &self.make_task,
143                 self.idx,
144             );
145         }
146 
147         Transition::Keep
148     }
149 
draw_baselayer(&self) -> DrawBaselayer150     fn draw_baselayer(&self) -> DrawBaselayer {
151         DrawBaselayer::Custom
152     }
153 
draw(&self, g: &mut GfxCtx, app: &App)154     fn draw(&self, g: &mut GfxCtx, app: &App) {
155         // Happens to be a nice background color too ;)
156         g.clear(app.cs.grass);
157         self.panel.draw(g);
158     }
159 }
160 
make_panel( ctx: &mut EventCtx, app: &App, name: &str, scenes: &Vec<Scene>, make_task: &Box<dyn Fn(&mut EventCtx) -> Widget>, idx: usize, ) -> Panel161 fn make_panel(
162     ctx: &mut EventCtx,
163     app: &App,
164     name: &str,
165     scenes: &Vec<Scene>,
166     make_task: &Box<dyn Fn(&mut EventCtx) -> Widget>,
167     idx: usize,
168 ) -> Panel {
169     let prev = if idx > 0 {
170         Btn::svg(
171             "system/assets/tools/prev.svg",
172             RewriteColor::Change(Color::WHITE, app.cs.hovering),
173         )
174         .build(ctx, "back", hotkey(Key::LeftArrow))
175     } else {
176         Widget::draw_svg_transform(
177             ctx,
178             "system/assets/tools/prev.svg",
179             RewriteColor::ChangeAlpha(0.3),
180         )
181     };
182     let next = Btn::svg(
183         "system/assets/tools/next.svg",
184         RewriteColor::Change(Color::WHITE, app.cs.hovering),
185     )
186     .build(
187         ctx,
188         "next",
189         hotkeys(vec![Key::RightArrow, Key::Space, Key::Enter]),
190     );
191 
192     let inner = if idx == scenes.len() {
193         Widget::custom_col(vec![
194             (make_task)(ctx),
195             Btn::txt("Start", Text::from(Line("Start").fg(Color::BLACK)))
196                 .build_def(ctx, hotkey(Key::Enter))
197                 .centered_horiz()
198                 .align_bottom(),
199         ])
200     } else {
201         Widget::custom_col(vec![
202             match scenes[idx].layout {
203                 Layout::PlayerSpeaking => Widget::custom_row(vec![
204                     Widget::draw_batch(
205                         ctx,
206                         GeomBatch::load_svg(ctx.prerender, "system/assets/characters/boss.svg")
207                             .scale(0.75)
208                             .autocrop(),
209                     ),
210                     Widget::custom_row(vec![
211                         scenes[idx].msg.clone().wrap_to_pct(ctx, 30).draw(ctx),
212                         Widget::draw_svg(ctx, "system/assets/characters/player.svg"),
213                     ])
214                     .align_right(),
215                 ]),
216                 Layout::BossSpeaking => Widget::custom_row(vec![
217                     Widget::draw_batch(
218                         ctx,
219                         GeomBatch::load_svg(ctx.prerender, "system/assets/characters/boss.svg")
220                             .scale(0.75)
221                             .autocrop(),
222                     ),
223                     scenes[idx].msg.clone().wrap_to_pct(ctx, 30).draw(ctx),
224                     Widget::draw_svg(ctx, "system/assets/characters/player.svg").align_right(),
225                 ]),
226                 Layout::Extra(name, scale) => Widget::custom_row(vec![
227                     Widget::draw_batch(
228                         ctx,
229                         GeomBatch::load_svg(ctx.prerender, "system/assets/characters/boss.svg")
230                             .scale(0.75)
231                             .autocrop(),
232                     ),
233                     Widget::col(vec![
234                         Widget::draw_batch(
235                             ctx,
236                             GeomBatch::load_svg(
237                                 ctx.prerender,
238                                 &format!("system/assets/characters/{}.svg", name),
239                             )
240                             .scale(scale)
241                             .autocrop(),
242                         ),
243                         scenes[idx].msg.clone().wrap_to_pct(ctx, 30).draw(ctx),
244                     ]),
245                     Widget::draw_svg(ctx, "system/assets/characters/player.svg"),
246                 ])
247                 .evenly_spaced(),
248             }
249             .margin_above(100),
250             Widget::col(vec![
251                 Widget::row(vec![prev, next]).centered_horiz(),
252                 Btn::txt(
253                     "Skip cutscene",
254                     Text::from(Line("Skip cutscene").fg(Color::BLACK)),
255                 )
256                 .build_def(ctx, None)
257                 .centered_horiz(),
258             ])
259             .align_bottom(),
260         ])
261     };
262 
263     let col = vec![
264         // TODO Can't get this to alignment to work
265         Widget::custom_row(vec![
266             Btn::svg_def("system/assets/pregame/back.svg")
267                 .build(ctx, "quit", None)
268                 .margin_right(100),
269             Line(name).big_heading_styled().draw(ctx),
270         ])
271         .margin_below(40),
272         inner
273             .fill_height()
274             .padding(42)
275             .bg(Color::WHITE)
276             .outline(2.0, Color::BLACK),
277     ];
278 
279     Panel::new(Widget::custom_col(col))
280         .exact_size_percent(80, 80)
281         .build_custom(ctx)
282 }
283 
284 pub struct FYI {
285     panel: Panel,
286 }
287 
288 impl FYI {
new(ctx: &mut EventCtx, contents: Widget, bg: Color) -> Box<dyn State>289     pub fn new(ctx: &mut EventCtx, contents: Widget, bg: Color) -> Box<dyn State> {
290         Box::new(FYI {
291             panel: Panel::new(
292                 Widget::custom_col(vec![
293                     contents,
294                     Btn::txt("Okay", Text::from(Line("Okay").fg(Color::BLACK)))
295                         .build_def(ctx, hotkeys(vec![Key::Escape, Key::Space, Key::Enter]))
296                         .centered_horiz()
297                         .align_bottom(),
298                 ])
299                 .padding(16)
300                 .bg(bg),
301             )
302             .exact_size_percent(50, 50)
303             .build_custom(ctx),
304         })
305     }
306 }
307 
308 impl State for FYI {
event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition309     fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
310         match self.panel.event(ctx) {
311             Outcome::Clicked(x) => match x.as_ref() {
312                 "Okay" => Transition::Pop,
313                 _ => unreachable!(),
314             },
315             _ => Transition::Keep,
316         }
317     }
318 
draw(&self, g: &mut GfxCtx, app: &App)319     fn draw(&self, g: &mut GfxCtx, app: &App) {
320         State::grey_out_map(g, app);
321         self.panel.draw(g);
322     }
323 }
324