Skip to main content

shuyi_ui/dialogue/
player.rs

1//! Playing a dialogue [`Scene`] one text box at a time. [`DialoguePlayer`]
2//! is the state machine only (who stands where, what is said, which reply
3//! choice is open, which music the script asked for, and what the game's
4//! own lines have set: its [`DialogueHooks`]); a dialogue screen draws its
5//! [`View`] and asks for the music.
6
7use crate::prelude::*;
8use alloc::borrow::Cow;
9use alloc::sync::Arc;
10use core::fmt::Debug;
11
12use shuyi_assets::dialogue::{
13    CharacterId, ChoiceOption, Directive, LineId, MusicLine, Present, Scene, Side, Step,
14};
15use shuyi_assets::names::Names;
16
17/// What a game adds to playing a scene: its own lines
18/// ([`Directive`]s, such as a place behind the talk) and what they have set
19/// so far (its [`Stage`](Self::Stage)), and the tokens it fills into the
20/// text (the hero's name, say).
21pub trait DialogueHooks: Clone + Debug + PartialEq + Eq {
22    /// The game's own lines.
23    type Directive: Directive;
24
25    /// What the game's lines have set so far: nothing until the first.
26    type Stage: Clone + Debug + Default + PartialEq + Eq;
27
28    /// `text` as shown: the game's tokens filled in. The scene keeps them.
29    ///
30    /// A game with tokens of its own and name tokens (`{n:<id>}`) fills
31    /// both: its own, and the names with [`Names::substitute`]. A game
32    /// with only name tokens plays with [`Named`].
33    fn fill<'a>(&self, text: &'a str) -> Cow<'a, str>;
34
35    /// Plays the game's line `directive` on `stage`, when the scene reaches
36    /// it (read or skipped).
37    fn direct(&self, stage: &mut Self::Stage, directive: &Self::Directive);
38}
39
40/// The hooks of a game that adds nothing: no lines of its own, no tokens,
41/// not even names: a `{n:<id>}` shows as written. A game with name tokens
42/// plays with [`Named`].
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
44pub struct Plain;
45
46impl DialogueHooks for Plain {
47    type Directive = shuyi_assets::dialogue::NoDirective;
48    type Stage = ();
49
50    fn fill<'a>(&self, text: &'a str) -> Cow<'a, str> {
51        Cow::Borrowed(text)
52    }
53
54    fn direct(&self, (): &mut (), directive: &Self::Directive) {
55        match *directive {}
56    }
57}
58
59/// The hooks of a game whose only tokens are the names table's: no lines
60/// of its own, and `{n:<id>}` and `{N:<id>}` are filled in with
61/// [`Names::substitute`]. It goes with the check's
62/// [`NoTokens`](shuyi_assets::dialogue::NoTokens).
63///
64/// ```
65/// use std::sync::Arc;
66/// use shuyi_assets::names::Names;
67/// use shuyi_ui::dialogue::{DialogueHooks, Named};
68///
69/// let mut names = Names::default();
70/// names.names.insert("smith".into(), "ada brook".into());
71/// let hooks = Named::new(Arc::new(names));
72/// assert_eq!(hooks.fill("Hello, {n:smith}."), "Hello, ada brook.");
73/// ```
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Named {
76    names: Arc<Names>,
77}
78
79impl Named {
80    /// The hooks that fill in the names of `names`.
81    #[must_use]
82    pub fn new(names: Arc<Names>) -> Self {
83        Self { names }
84    }
85}
86
87impl DialogueHooks for Named {
88    type Directive = shuyi_assets::dialogue::NoDirective;
89    type Stage = ();
90
91    fn fill<'a>(&self, text: &'a str) -> Cow<'a, str> {
92        self.names.substitute(text)
93    }
94
95    fn direct(&self, (): &mut (), directive: &Self::Directive) {
96        match *directive {}
97    }
98}
99
100/// A character standing on one side of the screen.
101#[derive(Debug, Clone, PartialEq, Eq)]
102struct Placed {
103    character: CharacterId,
104    expression: String,
105}
106
107impl Placed {
108    fn portrait(&self) -> Portrait<'_> {
109        Portrait {
110            character: &self.character,
111            expression: &self.expression,
112        }
113    }
114}
115
116/// A portrait on screen: who, with which expression.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub struct Portrait<'a> {
119    /// The character.
120    pub character: &'a CharacterId,
121    /// Their current expression.
122    pub expression: &'a str,
123}
124
125/// What the screen shows for the current text box, with the game's tokens
126/// filled in.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct View<'a, S> {
129    /// The left portrait, if any.
130    pub left: Option<Portrait<'a>>,
131    /// The right portrait, if any.
132    pub right: Option<Portrait<'a>>,
133    /// Which side is speaking (`None` for narration, or when finished).
134    pub speaker: Option<Side>,
135    /// The text box's text (`None` once the scene is finished). While a
136    /// choice is open, the text box before it (the line being answered).
137    pub text: Option<Cow<'a, str>>,
138    /// The location/time caption, if one has been shown.
139    pub caption: Option<Cow<'a, str>>,
140    /// What the game's own lines have set so far.
141    pub stage: &'a S,
142    /// Whether the text is narration (no speaker).
143    pub narration: bool,
144    /// The lead's replies, in menu order, while a choice is open: pick
145    /// one with [`DialoguePlayer::choose`].
146    pub choices: Option<Vec<Cow<'a, str>>>,
147}
148
149/// Where a step is: in the scene, or in a reply's reaction.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151struct At {
152    /// Index in the scene's steps (for a reaction: of its `Choice`).
153    step: usize,
154    /// For a reaction: the option and the step's index in it.
155    reaction: Option<(usize, usize)>,
156}
157
158/// What is on screen.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160enum Showing {
161    /// A text box: the speech or narration step at `At`.
162    Text(At),
163    /// The choice at this scene step, answering the text box `At` (if any).
164    Choice(usize, Option<At>),
165    /// The scene is over.
166    Finished,
167}
168
169/// A reply being played: its reaction. When the reaction ends the scene
170/// goes on after the choice, with the portraits as the reaction left them
171/// (the script sets any expression change for the rejoin).
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173struct Reaction {
174    /// The scene step of the `Choice`.
175    choice: usize,
176    /// The option picked.
177    option: usize,
178    /// The next reaction step.
179    next: usize,
180}
181
182/// Plays a scene: each [`advance`](Self::advance) moves to the next text
183/// box (speech or narration), applying the portrait and caption steps in
184/// between. At a reply choice it waits for [`choose`](Self::choose), plays
185/// that reply's reaction, then rejoins the scene after the choice. The
186/// `@music` lines passed on the way are kept for [`take_music`](Self::take_music).
187/// It plays the scene as it is for those present when it starts: the
188/// lines of an `@if` block whose character isn't there are no part of it,
189/// read or skipped. The game's own lines play on its stage as the scene
190/// reaches them ([`DialogueHooks::direct`]).
191/// Pure: no drawing, no clock, no sound. Cheap to clone (the scene is
192/// shared), so a copy can be kept as the scene was at a line (a dialogue
193/// log).
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct DialoguePlayer<H: DialogueHooks> {
196    /// The scene for those present: it has no `@if` blocks.
197    scene: Arc<Scene<H::Directive>>,
198    /// Fills in the game's tokens and plays its lines.
199    hooks: H,
200    /// Index of the next scene step (after the text box on screen, or the
201    /// open choice's).
202    next: usize,
203    /// The reply being played, if any.
204    reaction: Option<Reaction>,
205    showing: Showing,
206    /// The last text box shown, for an open choice.
207    last_text: Option<At>,
208    left: Option<Placed>,
209    right: Option<Placed>,
210    caption: Option<String>,
211    /// The last `@music` line reached and not yet taken.
212    music: Option<MusicLine>,
213    /// What the game's lines have set so far.
214    stage: H::Stage,
215}
216
217impl<H: DialogueHooks> DialoguePlayer<H> {
218    /// Starts `scene` as it plays for those `present`, at its first text
219    /// box, with `hooks` filling its text and playing the game's lines. A
220    /// scene with nothing to say for them is finished at once.
221    pub fn new(scene: &Scene<H::Directive>, hooks: H, present: &Present) -> Self {
222        let mut player = Self {
223            scene: Arc::new(scene.resolved(present)),
224            hooks,
225            next: 0,
226            reaction: None,
227            showing: Showing::Finished,
228            last_text: None,
229            left: None,
230            right: None,
231            caption: None,
232            music: None,
233            stage: H::Stage::default(),
234        };
235        player.step_on();
236        player
237    }
238
239    /// What fills the text and plays the game's lines.
240    pub fn hooks(&self) -> &H {
241        &self.hooks
242    }
243
244    /// What the game's lines have set so far.
245    pub fn stage(&self) -> &H::Stage {
246        &self.stage
247    }
248
249    /// What the game's lines have set so far, for the game to take what it
250    /// has played from it.
251    pub fn stage_mut(&mut self) -> &mut H::Stage {
252        &mut self.stage
253    }
254
255    /// `text` as shown: the game's tokens filled in. The scene keeps them.
256    ///
257    /// A game with tokens of its own and name tokens (`{n:<id>}`) fills
258    /// both: its own, and the names with [`Names::substitute`]. A game
259    /// with only name tokens plays with [`Named`].
260    fn fill<'a>(&self, text: &'a str) -> Cow<'a, str> {
261        self.hooks.fill(text)
262    }
263
264    /// The id of the scene being played.
265    pub fn scene_id(&self) -> &str {
266        &self.scene.id
267    }
268
269    /// The scene being played: the one it was started with, as it is for
270    /// those present.
271    pub fn scene(&self) -> &Scene<H::Directive> {
272        &self.scene
273    }
274
275    /// The step at `at`.
276    fn step(&self, at: At) -> Option<&Step<H::Directive>> {
277        let step = self.scene.steps.get(at.step)?;
278        match (step, at.reaction) {
279            (_, None) => Some(step),
280            (Step::Choice { options }, Some((option, i))) => options.get(option)?.steps.get(i),
281            _ => None,
282        }
283    }
284
285    /// The options of the choice at scene step `index`.
286    fn options(&self, index: usize) -> &[ChoiceOption<H::Directive>] {
287        match self.scene.steps.get(index) {
288            Some(Step::Choice { options }) => options,
289            _ => &[],
290        }
291    }
292
293    /// What to show now.
294    pub fn current(&self) -> View<'_, H::Stage> {
295        let (text_at, choices) = match self.showing {
296            Showing::Text(at) => (Some(at), None),
297            Showing::Choice(index, before) => {
298                let options = self.options(index);
299                let texts = options.iter().map(|o| self.fill(&o.text));
300                (before, Some(texts.collect()))
301            }
302            Showing::Finished => (None, None),
303        };
304        let step = text_at.and_then(|at| self.step(at));
305        let speaker = match step {
306            Some(Step::Say { speaker, .. }) => self.side_of(speaker),
307            _ => None,
308        };
309        View {
310            left: self.left.as_ref().map(Placed::portrait),
311            right: self.right.as_ref().map(Placed::portrait),
312            speaker,
313            text: step.and_then(Step::text).map(|t| self.fill(t)),
314            caption: self.caption.as_deref().map(|c| self.fill(c)),
315            stage: &self.stage,
316            narration: matches!(step, Some(Step::Narrate { .. })),
317            choices,
318        }
319    }
320
321    /// The id of the line in the text box (while a choice is open, of the
322    /// line being answered); `None` once the scene is finished.
323    pub fn line(&self) -> Option<&LineId> {
324        let at = match self.showing {
325            Showing::Text(at) => Some(at),
326            Showing::Choice(_, before) => before,
327            Showing::Finished => None,
328        };
329        match at.and_then(|at| self.step(at)) {
330            Some(Step::Say { line, .. } | Step::Narrate { line, .. }) => Some(line),
331            _ => None,
332        }
333    }
334
335    /// The id of reply `option` of the open choice, if a choice is open and
336    /// has it.
337    pub fn reply_line(&self, option: usize) -> Option<&LineId> {
338        let Showing::Choice(index, _) = self.showing else {
339            return None;
340        };
341        self.options(index).get(option).map(|o| &o.line)
342    }
343
344    /// The music the scene has asked for and [`take_music`](Self::take_music)
345    /// hasn't taken yet, without taking it.
346    pub fn pending_music(&self) -> Option<&MusicLine> {
347        self.music.as_ref()
348    }
349
350    /// Moves to the next text box, applying every portrait, caption and
351    /// place step before it. After the last one the scene is finished; advancing a
352    /// finished scene does nothing. While a choice is open this does
353    /// nothing either: [`choose`](Self::choose) a reply.
354    pub fn advance(&mut self) {
355        if matches!(self.showing, Showing::Text(_)) {
356            self.step_on();
357        }
358    }
359
360    /// Picks reply `option` of the open choice: plays its reaction, then
361    /// the scene after the choice. Does nothing if no choice is open or
362    /// there is no such option.
363    pub fn choose(&mut self, option: usize) {
364        let Showing::Choice(index, _) = self.showing else {
365            return;
366        };
367        if option >= self.options(index).len() {
368            return;
369        }
370        self.reaction = Some(Reaction {
371            choice: index,
372            option,
373            next: 0,
374        });
375        self.step_on();
376    }
377
378    /// Advances until a choice is open or the scene is over (skipping a
379    /// scene stops at each choice).
380    pub fn skip_to_choice(&mut self) {
381        while matches!(self.showing, Showing::Text(_)) {
382            let before = (self.next, self.reaction);
383            self.step_on();
384            // Each text box moves the position on; never spin in place.
385            if (self.next, self.reaction) == before {
386                break;
387            }
388        }
389    }
390
391    /// The music the scene has asked for since the last call: the last
392    /// `@music` line reached, whether the lines before it were read or
393    /// skipped. `None` if no `@music` line was reached since.
394    pub fn take_music(&mut self) -> Option<MusicLine> {
395        self.music.take()
396    }
397
398    /// Whether a reply choice is waiting for [`choose`](Self::choose).
399    pub fn is_choosing(&self) -> bool {
400        matches!(self.showing, Showing::Choice(..))
401    }
402
403    /// Whether every text box has been shown and advanced past.
404    pub fn is_finished(&self) -> bool {
405        self.showing == Showing::Finished
406    }
407
408    /// Applies steps up to the next text box or choice.
409    fn step_on(&mut self) {
410        if let Some(r) = self.reaction {
411            let steps = self
412                .options(r.choice)
413                .get(r.option)
414                .map_or(&[][..], |o| &o.steps[..]);
415            let rest: Vec<Step<H::Directive>> = steps.iter().skip(r.next).cloned().collect();
416            for (i, step) in (r.next..).zip(rest) {
417                let at = At {
418                    step: r.choice,
419                    reaction: Some((r.option, i)),
420                };
421                if self.apply(&step, at) {
422                    self.reaction = Some(Reaction { next: i + 1, ..r });
423                    return;
424                }
425            }
426            // The reaction is over: on with the scene.
427            self.reaction = None;
428        }
429        let rest: Vec<(usize, Step<H::Directive>)> = self
430            .scene
431            .steps
432            .iter()
433            .cloned()
434            .enumerate()
435            .skip(self.next)
436            .collect();
437        for (index, step) in rest {
438            if let Step::Choice { options } = &step {
439                // A choice without replies (which the validator rejects)
440                // is passed over rather than waiting forever.
441                if !options.is_empty() {
442                    self.showing = Showing::Choice(index, self.last_text);
443                    self.next = index + 1;
444                    return;
445                }
446                continue;
447            }
448            let at = At {
449                step: index,
450                reaction: None,
451            };
452            if self.apply(&step, at) {
453                self.next = index + 1;
454                return;
455            }
456        }
457        self.showing = Showing::Finished;
458    }
459
460    /// Applies `step` (found at `at`). Returns whether it is a text box,
461    /// which is now shown.
462    fn apply(&mut self, step: &Step<H::Directive>, at: At) -> bool {
463        match step {
464            Step::Caption { text } => self.caption = Some(text.clone()),
465            Step::Place {
466                side,
467                character,
468                expression,
469            } => {
470                let placed = Some(Placed {
471                    character: character.clone(),
472                    expression: expression.clone(),
473                });
474                match side {
475                    Side::Left => self.left = placed,
476                    Side::Right => self.right = placed,
477                }
478            }
479            Step::Clear { side: Side::Left } => self.left = None,
480            Step::Clear { side: Side::Right } => self.right = None,
481            Step::Say {
482                speaker,
483                expression,
484                ..
485            } => {
486                if let Some(expression) = expression {
487                    for p in [&mut self.left, &mut self.right].into_iter().flatten() {
488                        if &p.character == speaker {
489                            p.expression.clone_from(expression);
490                        }
491                    }
492                }
493            }
494            Step::Music(music) => self.music = Some(music.clone()),
495            Step::Directive(directive) => self.hooks.direct(&mut self.stage, directive),
496            // The scene was resolved for those present: it has no blocks.
497            Step::Narrate { .. } | Step::Choice { .. } | Step::If { .. } => {}
498        }
499        if step.text().is_some() {
500            self.showing = Showing::Text(at);
501            self.last_text = Some(at);
502            return true;
503        }
504        false
505    }
506
507    /// The side `character` stands on.
508    fn side_of(&self, character: &CharacterId) -> Option<Side> {
509        let on = |p: &Option<Placed>| p.as_ref().is_some_and(|p| &p.character == character);
510        if on(&self.left) {
511            Some(Side::Left)
512        } else if on(&self.right) {
513            Some(Side::Right)
514        } else {
515            None
516        }
517    }
518}
519
520#[cfg(test)]
521mod tests;