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;