game/challenges/
cutscene.rs

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                    // TODO Should SandboxMode use on_destroy for this?
96                    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        // TODO Should the Panel for text widgets with wrapping do this instead?
122        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}