1use std::collections::HashMap;
8
9#[derive(Clone, Debug)]
13pub struct DialogueNode {
14 pub id: String,
15 pub speaker: String,
16 pub text: String,
17 pub portrait: Option<String>,
19 pub emotion: DialogueEmotion,
21 pub next: DialogueNext,
23}
24
25#[derive(Clone, Debug)]
27pub enum DialogueNext {
28 Node(String),
30 Choice(Vec<Choice>),
32 End,
34 Auto { duration: f32, then: Box<DialogueNext> },
36}
37
38#[derive(Clone, Debug)]
40pub struct Choice {
41 pub text: String,
42 pub next: String, pub requires: Option<String>,
45 pub sets: Vec<(String, bool)>,
47}
48
49impl Choice {
50 pub fn new(text: impl Into<String>, next: impl Into<String>) -> Self {
51 Self { text: text.into(), next: next.into(), requires: None, sets: Vec::new() }
52 }
53
54 pub fn requires(mut self, flag: impl Into<String>) -> Self {
55 self.requires = Some(flag.into());
56 self
57 }
58
59 pub fn sets_flag(mut self, flag: impl Into<String>, value: bool) -> Self {
60 self.sets.push((flag.into(), value));
61 self
62 }
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
67pub enum DialogueEmotion {
68 #[default]
69 Neutral,
70 Happy,
71 Sad,
72 Angry,
73 Surprised,
74 Scared,
75 Suspicious,
76 Mysterious,
77}
78
79impl DialogueEmotion {
80 pub fn color(self) -> [f32; 4] {
82 match self {
83 DialogueEmotion::Neutral => [1.0, 1.0, 1.0, 1.0],
84 DialogueEmotion::Happy => [1.0, 0.95, 0.5, 1.0],
85 DialogueEmotion::Sad => [0.5, 0.6, 0.9, 1.0],
86 DialogueEmotion::Angry => [1.0, 0.3, 0.2, 1.0],
87 DialogueEmotion::Surprised => [0.9, 0.7, 1.0, 1.0],
88 DialogueEmotion::Scared => [0.6, 0.9, 0.7, 1.0],
89 DialogueEmotion::Suspicious => [0.8, 0.8, 0.4, 1.0],
90 DialogueEmotion::Mysterious => [0.5, 0.4, 0.9, 1.0],
91 }
92 }
93
94 pub fn name(self) -> &'static str {
95 match self {
96 DialogueEmotion::Neutral => "neutral",
97 DialogueEmotion::Happy => "happy",
98 DialogueEmotion::Sad => "sad",
99 DialogueEmotion::Angry => "angry",
100 DialogueEmotion::Surprised => "surprised",
101 DialogueEmotion::Scared => "scared",
102 DialogueEmotion::Suspicious => "suspicious",
103 DialogueEmotion::Mysterious => "mysterious",
104 }
105 }
106}
107
108#[derive(Clone, Debug, Default)]
112pub struct DialogueTree {
113 pub name: String,
114 pub nodes: HashMap<String, DialogueNode>,
115 pub start_node: String,
116}
117
118impl DialogueTree {
119 pub fn new(name: impl Into<String>) -> Self {
120 Self { name: name.into(), nodes: HashMap::new(), start_node: String::new() }
121 }
122
123 pub fn add_node(mut self, node: DialogueNode) -> Self {
124 if self.start_node.is_empty() {
125 self.start_node = node.id.clone();
126 }
127 self.nodes.insert(node.id.clone(), node);
128 self
129 }
130
131 pub fn with_start(mut self, id: impl Into<String>) -> Self {
132 self.start_node = id.into();
133 self
134 }
135
136 pub fn get(&self, id: &str) -> Option<&DialogueNode> {
137 self.nodes.get(id)
138 }
139}
140
141pub struct NodeBuilder {
145 id: String,
146 speaker: String,
147 text: String,
148 portrait: Option<String>,
149 emotion: DialogueEmotion,
150}
151
152impl NodeBuilder {
153 pub fn new(id: impl Into<String>, speaker: impl Into<String>, text: impl Into<String>) -> Self {
154 Self {
155 id: id.into(),
156 speaker: speaker.into(),
157 text: text.into(),
158 portrait: None,
159 emotion: DialogueEmotion::Neutral,
160 }
161 }
162
163 pub fn portrait(mut self, p: impl Into<String>) -> Self { self.portrait = Some(p.into()); self }
164 pub fn emotion(mut self, e: DialogueEmotion) -> Self { self.emotion = e; self }
165
166 pub fn end(self) -> DialogueNode {
167 DialogueNode { id: self.id, speaker: self.speaker, text: self.text,
168 portrait: self.portrait, emotion: self.emotion,
169 next: DialogueNext::End }
170 }
171
172 pub fn then(self, next_id: impl Into<String>) -> DialogueNode {
173 DialogueNode { id: self.id, speaker: self.speaker, text: self.text,
174 portrait: self.portrait, emotion: self.emotion,
175 next: DialogueNext::Node(next_id.into()) }
176 }
177
178 pub fn choices(self, choices: Vec<Choice>) -> DialogueNode {
179 DialogueNode { id: self.id, speaker: self.speaker, text: self.text,
180 portrait: self.portrait, emotion: self.emotion,
181 next: DialogueNext::Choice(choices) }
182 }
183
184 pub fn auto(self, duration: f32, then: DialogueNext) -> DialogueNode {
185 DialogueNode { id: self.id, speaker: self.speaker, text: self.text,
186 portrait: self.portrait, emotion: self.emotion,
187 next: DialogueNext::Auto { duration, then: Box::new(then) } }
188 }
189}
190
191#[derive(Clone, Debug)]
195pub struct TypewriterState {
196 pub full_text: String,
197 pub chars_shown: usize, pub chars_per_sec: f32,
199 pub accumulator: f32, pub complete: bool,
201 pub pause_timer: f32,
203}
204
205impl TypewriterState {
206 pub fn new(text: impl Into<String>, chars_per_sec: f32) -> Self {
207 let text = text.into();
208 let complete = text.is_empty();
209 Self {
210 full_text: text,
211 chars_shown: 0,
212 chars_per_sec,
213 accumulator: 0.0,
214 complete,
215 pause_timer: 0.0,
216 }
217 }
218
219 pub fn tick(&mut self, dt: f32) -> bool {
221 if self.complete { return false; }
222
223 if self.pause_timer > 0.0 {
225 self.pause_timer -= dt;
226 return false;
227 }
228
229 self.accumulator += dt * self.chars_per_sec;
230 let new_chars = self.accumulator as usize;
231 self.accumulator -= new_chars as f32;
232
233 for _ in 0..new_chars {
234 if self.chars_shown < self.full_text.len() {
235 let ch = self.full_text.chars().nth(self.chars_shown).unwrap_or(' ');
237 self.chars_shown += 1;
238 match ch {
239 '.' | '!' | '?' => self.pause_timer = 0.25,
240 ',' | ';' => self.pause_timer = 0.1,
241 _ => {}
242 }
243 if self.chars_shown >= self.full_text.chars().count() {
244 self.complete = true;
245 return true;
246 }
247 }
248 }
249 false
250 }
251
252 pub fn skip(&mut self) {
254 self.chars_shown = self.full_text.chars().count();
255 self.complete = true;
256 self.pause_timer = 0.0;
257 }
258
259 pub fn visible_text(&self) -> &str {
261 if self.chars_shown >= self.full_text.len() {
262 &self.full_text
263 } else {
264 &self.full_text[..self.char_byte_offset(self.chars_shown)]
265 }
266 }
267
268 fn char_byte_offset(&self, n: usize) -> usize {
269 self.full_text.char_indices().nth(n).map(|(i, _)| i).unwrap_or(self.full_text.len())
270 }
271
272 pub fn progress(&self) -> f32 {
274 let total = self.full_text.chars().count();
275 if total == 0 { 1.0 } else { self.chars_shown as f32 / total as f32 }
276 }
277}
278
279pub struct DialoguePlayer {
283 pub tree: DialogueTree,
284 pub current_node: Option<String>,
285 pub typewriter: Option<TypewriterState>,
286 pub state: DialogueState,
287 pub flags: HashMap<String, bool>,
288 pub history: Vec<String>, pub chars_per_sec: f32,
290 auto_timer: Option<f32>,
291 pub choices: Vec<Choice>,
293 pub selected_choice: usize,
294}
295
296#[derive(Clone, Copy, Debug, PartialEq, Eq)]
297pub enum DialogueState {
298 Idle,
299 Typing,
301 Waiting,
303 Choosing,
305 AutoTimer,
307 Finished,
309}
310
311impl DialoguePlayer {
312 pub fn new(tree: DialogueTree) -> Self {
313 Self {
314 tree,
315 current_node: None,
316 typewriter: None,
317 state: DialogueState::Idle,
318 flags: HashMap::new(),
319 history: Vec::new(),
320 chars_per_sec: 28.0,
321 auto_timer: None,
322 choices: Vec::new(),
323 selected_choice: 0,
324 }
325 }
326
327 pub fn with_speed(mut self, chars_per_sec: f32) -> Self {
328 self.chars_per_sec = chars_per_sec;
329 self
330 }
331
332 pub fn start(&mut self) {
334 let id = self.tree.start_node.clone();
335 self.goto(&id);
336 }
337
338 pub fn goto(&mut self, id: &str) {
340 if let Some(node) = self.tree.get(id).cloned() {
341 self.history.push(id.to_string());
342 self.current_node = Some(id.to_string());
343 self.typewriter = Some(TypewriterState::new(&node.text, self.chars_per_sec));
344 self.state = DialogueState::Typing;
345 self.choices.clear();
346 self.selected_choice = 0;
347 self.auto_timer = None;
348 }
349 }
350
351 pub fn is_finished(&self) -> bool { self.state == DialogueState::Finished }
352 pub fn is_typing(&self) -> bool { self.state == DialogueState::Typing }
353
354 pub fn visible_text(&self) -> &str {
356 self.typewriter.as_ref().map(|tw| tw.visible_text()).unwrap_or("")
357 }
358
359 pub fn current(&self) -> Option<&DialogueNode> {
361 self.current_node.as_deref().and_then(|id| self.tree.get(id))
362 }
363
364 pub fn tick(&mut self, dt: f32) -> Option<DialogueEvent> {
366 match self.state {
367 DialogueState::Typing => {
368 let done = self.typewriter.as_mut().map(|tw| tw.tick(dt)).unwrap_or(false);
369 if done {
370 let node = self.current_node.as_deref()
371 .and_then(|id| self.tree.get(id))
372 .cloned();
373 if let Some(node) = node {
374 match &node.next {
375 DialogueNext::End => {
376 self.state = DialogueState::Waiting;
377 }
378 DialogueNext::Node(_) => {
379 self.state = DialogueState::Waiting;
380 }
381 DialogueNext::Choice(choices) => {
382 let visible: Vec<Choice> = choices.iter()
383 .filter(|c| {
384 c.requires.as_ref()
385 .map(|f| self.flags.get(f.as_str()).copied().unwrap_or(false))
386 .unwrap_or(true)
387 })
388 .cloned()
389 .collect();
390 self.choices = visible;
391 self.state = DialogueState::Choosing;
392 return Some(DialogueEvent::ShowChoices(self.choices.clone()));
393 }
394 DialogueNext::Auto { duration, .. } => {
395 self.auto_timer = Some(*duration);
396 self.state = DialogueState::AutoTimer;
397 }
398 }
399 }
400 return Some(DialogueEvent::TypewriterDone);
401 }
402 }
403 DialogueState::AutoTimer => {
404 if let Some(ref mut timer) = self.auto_timer {
405 *timer -= dt;
406 if *timer <= 0.0 {
407 self.auto_timer = None;
408 return self.advance_auto();
409 }
410 }
411 }
412 _ => {}
413 }
414 None
415 }
416
417 fn advance_auto(&mut self) -> Option<DialogueEvent> {
418 let next = self.current_node.as_deref()
419 .and_then(|id| self.tree.get(id))
420 .map(|n| n.next.clone())?;
421
422 if let DialogueNext::Auto { then, .. } = next {
423 match *then {
424 DialogueNext::Node(id) => { self.goto(&id); Some(DialogueEvent::NodeChanged(id)) }
425 DialogueNext::End => { self.state = DialogueState::Finished; Some(DialogueEvent::Finished) }
426 _ => None,
427 }
428 } else {
429 None
430 }
431 }
432
433 pub fn advance(&mut self) -> Option<DialogueEvent> {
435 match self.state {
436 DialogueState::Typing => {
437 if let Some(tw) = &mut self.typewriter { tw.skip(); }
439 self.state = DialogueState::Waiting;
440 Some(DialogueEvent::TypewriterDone)
441 }
442 DialogueState::Waiting => {
443 let next = self.current_node.as_deref()
444 .and_then(|id| self.tree.get(id))
445 .map(|n| n.next.clone());
446 match next {
447 Some(DialogueNext::Node(id)) => {
448 self.goto(&id);
449 Some(DialogueEvent::NodeChanged(id))
450 }
451 Some(DialogueNext::End) | None => {
452 self.state = DialogueState::Finished;
453 Some(DialogueEvent::Finished)
454 }
455 _ => None,
456 }
457 }
458 DialogueState::Choosing => {
459 if self.choices.is_empty() {
460 self.state = DialogueState::Finished;
461 return Some(DialogueEvent::Finished);
462 }
463 let choice = self.choices[self.selected_choice].clone();
464 for (flag, val) in &choice.sets {
466 self.flags.insert(flag.clone(), *val);
467 }
468 let next_id = choice.next.clone();
469 self.goto(&next_id);
470 Some(DialogueEvent::ChoiceMade { index: self.selected_choice, next: next_id })
471 }
472 _ => None,
473 }
474 }
475
476 pub fn select_prev(&mut self) {
478 if !self.choices.is_empty() {
479 self.selected_choice = (self.selected_choice + self.choices.len() - 1) % self.choices.len();
480 }
481 }
482
483 pub fn select_next(&mut self) {
484 if !self.choices.is_empty() {
485 self.selected_choice = (self.selected_choice + 1) % self.choices.len();
486 }
487 }
488
489 pub fn select(&mut self, idx: usize) {
490 self.selected_choice = idx.min(self.choices.len().saturating_sub(1));
491 }
492
493 pub fn get_flag(&self, f: &str) -> bool { self.flags.get(f).copied().unwrap_or(false) }
494 pub fn set_flag(&mut self, f: impl Into<String>, v: bool) { self.flags.insert(f.into(), v); }
495}
496
497#[derive(Clone, Debug)]
499pub enum DialogueEvent {
500 TypewriterDone,
501 ShowChoices(Vec<Choice>),
502 ChoiceMade { index: usize, next: String },
503 NodeChanged(String),
504 Finished,
505}
506
507#[cfg(test)]
510mod tests {
511 use super::*;
512
513 fn make_tree() -> DialogueTree {
514 DialogueTree::new("test")
515 .add_node(NodeBuilder::new("intro", "Hero", "Hello there.").then("end"))
516 .add_node(NodeBuilder::new("end", "Hero", "Goodbye.").end())
517 }
518
519 #[test]
520 fn typewriter_advances() {
521 let mut tw = TypewriterState::new("Hello", 100.0);
522 tw.tick(0.5);
523 assert!(tw.chars_shown > 0);
524 }
525
526 #[test]
527 fn typewriter_skip() {
528 let mut tw = TypewriterState::new("Long text here", 10.0);
529 tw.skip();
530 assert!(tw.complete);
531 assert_eq!(tw.visible_text(), "Long text here");
532 }
533
534 #[test]
535 fn typewriter_progress() {
536 let mut tw = TypewriterState::new("ABCDE", 100.0);
537 tw.tick(0.02); assert!(tw.progress() > 0.0 && tw.progress() < 1.0);
539 }
540
541 #[test]
542 fn player_starts_and_types() {
543 let tree = make_tree();
544 let mut player = DialoguePlayer::new(tree);
545 player.start();
546 assert_eq!(player.state, DialogueState::Typing);
547 }
548
549 #[test]
550 fn player_skips_typewriter() {
551 let tree = make_tree();
552 let mut player = DialoguePlayer::new(tree);
553 player.start();
554 let ev = player.advance();
555 assert!(matches!(ev, Some(DialogueEvent::TypewriterDone)));
556 assert_eq!(player.state, DialogueState::Waiting);
557 }
558
559 #[test]
560 fn player_advances_node() {
561 let tree = make_tree();
562 let mut player = DialoguePlayer::new(tree);
563 player.start();
564 player.advance(); let ev = player.advance(); assert!(matches!(ev, Some(DialogueEvent::NodeChanged(_))));
567 }
568
569 #[test]
570 fn player_finishes() {
571 let tree = make_tree();
572 let mut player = DialoguePlayer::new(tree);
573 player.start();
574 player.advance(); player.advance(); player.advance(); let ev = player.advance(); assert!(matches!(ev, Some(DialogueEvent::Finished)));
579 assert!(player.is_finished());
580 }
581
582 #[test]
583 fn choice_node() {
584 let tree = DialogueTree::new("choices")
585 .add_node(NodeBuilder::new("q", "NPC", "What do you want?").choices(vec![
586 Choice::new("Fight", "fight_node"),
587 Choice::new("Talk", "talk_node"),
588 ]))
589 .add_node(NodeBuilder::new("fight_node", "NPC", "Let's fight!").end())
590 .add_node(NodeBuilder::new("talk_node", "NPC", "Let's talk!").end());
591
592 let mut player = DialoguePlayer::new(tree);
593 player.start();
594 player.advance(); assert_eq!(player.state, DialogueState::Choosing);
596 assert_eq!(player.choices.len(), 2);
597
598 player.select(1); let ev = player.advance();
600 assert!(matches!(ev, Some(DialogueEvent::ChoiceMade { index: 1, .. })));
601 }
602
603 #[test]
604 fn emotion_colors_defined() {
605 use DialogueEmotion::*;
606 for em in [Neutral, Happy, Sad, Angry, Surprised, Scared, Suspicious, Mysterious] {
607 let c = em.color();
608 assert!(c[3] > 0.0); }
610 }
611}