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 return Some(self.finish_typing());
371 }
372 }
373 DialogueState::AutoTimer => {
374 if let Some(ref mut timer) = self.auto_timer {
375 *timer -= dt;
376 if *timer <= 0.0 {
377 self.auto_timer = None;
378 return self.advance_auto();
379 }
380 }
381 }
382 _ => {}
383 }
384 None
385 }
386
387 fn finish_typing(&mut self) -> DialogueEvent {
390 let node = self.current_node.as_deref()
391 .and_then(|id| self.tree.get(id))
392 .cloned();
393 self.state = DialogueState::Waiting;
394 if let Some(node) = node {
395 match &node.next {
396 DialogueNext::End | DialogueNext::Node(_) => {}
397 DialogueNext::Choice(choices) => {
398 let visible: Vec<Choice> = choices.iter()
399 .filter(|c| {
400 c.requires.as_ref()
401 .map(|f| self.flags.get(f.as_str()).copied().unwrap_or(false))
402 .unwrap_or(true)
403 })
404 .cloned()
405 .collect();
406 self.choices = visible;
407 self.state = DialogueState::Choosing;
408 return DialogueEvent::ShowChoices(self.choices.clone());
409 }
410 DialogueNext::Auto { duration, .. } => {
411 self.auto_timer = Some(*duration);
412 self.state = DialogueState::AutoTimer;
413 }
414 }
415 }
416 DialogueEvent::TypewriterDone
417 }
418
419 fn advance_auto(&mut self) -> Option<DialogueEvent> {
420 let next = self.current_node.as_deref()
421 .and_then(|id| self.tree.get(id))
422 .map(|n| n.next.clone())?;
423
424 if let DialogueNext::Auto { then, .. } = next {
425 match *then {
426 DialogueNext::Node(id) => { self.goto(&id); Some(DialogueEvent::NodeChanged(id)) }
427 DialogueNext::End => { self.state = DialogueState::Finished; Some(DialogueEvent::Finished) }
428 _ => None,
429 }
430 } else {
431 None
432 }
433 }
434
435 pub fn advance(&mut self) -> Option<DialogueEvent> {
437 match self.state {
438 DialogueState::Typing => {
439 if let Some(tw) = &mut self.typewriter { tw.skip(); }
443 Some(self.finish_typing())
444 }
445 DialogueState::Waiting => {
446 let next = self.current_node.as_deref()
447 .and_then(|id| self.tree.get(id))
448 .map(|n| n.next.clone());
449 match next {
450 Some(DialogueNext::Node(id)) => {
451 self.goto(&id);
452 Some(DialogueEvent::NodeChanged(id))
453 }
454 Some(DialogueNext::End) | None => {
455 self.state = DialogueState::Finished;
456 Some(DialogueEvent::Finished)
457 }
458 _ => None,
459 }
460 }
461 DialogueState::Choosing => {
462 if self.choices.is_empty() {
463 self.state = DialogueState::Finished;
464 return Some(DialogueEvent::Finished);
465 }
466 let index = self.selected_choice;
467 let choice = self.choices[index].clone();
468 for (flag, val) in &choice.sets {
470 self.flags.insert(flag.clone(), *val);
471 }
472 let next_id = choice.next.clone();
473 self.goto(&next_id);
474 Some(DialogueEvent::ChoiceMade { index, next: next_id })
477 }
478 _ => None,
479 }
480 }
481
482 pub fn select_prev(&mut self) {
484 if !self.choices.is_empty() {
485 self.selected_choice = (self.selected_choice + self.choices.len() - 1) % self.choices.len();
486 }
487 }
488
489 pub fn select_next(&mut self) {
490 if !self.choices.is_empty() {
491 self.selected_choice = (self.selected_choice + 1) % self.choices.len();
492 }
493 }
494
495 pub fn select(&mut self, idx: usize) {
496 self.selected_choice = idx.min(self.choices.len().saturating_sub(1));
497 }
498
499 pub fn get_flag(&self, f: &str) -> bool { self.flags.get(f).copied().unwrap_or(false) }
500 pub fn set_flag(&mut self, f: impl Into<String>, v: bool) { self.flags.insert(f.into(), v); }
501}
502
503#[derive(Clone, Debug)]
505pub enum DialogueEvent {
506 TypewriterDone,
507 ShowChoices(Vec<Choice>),
508 ChoiceMade { index: usize, next: String },
509 NodeChanged(String),
510 Finished,
511}
512
513#[cfg(test)]
516mod tests {
517 use super::*;
518
519 fn make_tree() -> DialogueTree {
520 DialogueTree::new("test")
521 .add_node(NodeBuilder::new("intro", "Hero", "Hello there.").then("end"))
522 .add_node(NodeBuilder::new("end", "Hero", "Goodbye.").end())
523 }
524
525 #[test]
526 fn typewriter_advances() {
527 let mut tw = TypewriterState::new("Hello", 100.0);
528 tw.tick(0.5);
529 assert!(tw.chars_shown > 0);
530 }
531
532 #[test]
533 fn typewriter_skip() {
534 let mut tw = TypewriterState::new("Long text here", 10.0);
535 tw.skip();
536 assert!(tw.complete);
537 assert_eq!(tw.visible_text(), "Long text here");
538 }
539
540 #[test]
541 fn typewriter_progress() {
542 let mut tw = TypewriterState::new("ABCDE", 100.0);
543 tw.tick(0.02); assert!(tw.progress() > 0.0 && tw.progress() < 1.0);
545 }
546
547 #[test]
548 fn player_starts_and_types() {
549 let tree = make_tree();
550 let mut player = DialoguePlayer::new(tree);
551 player.start();
552 assert_eq!(player.state, DialogueState::Typing);
553 }
554
555 #[test]
556 fn player_skips_typewriter() {
557 let tree = make_tree();
558 let mut player = DialoguePlayer::new(tree);
559 player.start();
560 let ev = player.advance();
561 assert!(matches!(ev, Some(DialogueEvent::TypewriterDone)));
562 assert_eq!(player.state, DialogueState::Waiting);
563 }
564
565 #[test]
566 fn player_advances_node() {
567 let tree = make_tree();
568 let mut player = DialoguePlayer::new(tree);
569 player.start();
570 player.advance(); let ev = player.advance(); assert!(matches!(ev, Some(DialogueEvent::NodeChanged(_))));
573 }
574
575 #[test]
576 fn player_finishes() {
577 let tree = make_tree();
578 let mut player = DialoguePlayer::new(tree);
579 player.start();
580 player.advance(); player.advance(); player.advance(); let ev = player.advance(); assert!(matches!(ev, Some(DialogueEvent::Finished)));
585 assert!(player.is_finished());
586 }
587
588 #[test]
589 fn choice_node() {
590 let tree = DialogueTree::new("choices")
591 .add_node(NodeBuilder::new("q", "NPC", "What do you want?").choices(vec![
592 Choice::new("Fight", "fight_node"),
593 Choice::new("Talk", "talk_node"),
594 ]))
595 .add_node(NodeBuilder::new("fight_node", "NPC", "Let's fight!").end())
596 .add_node(NodeBuilder::new("talk_node", "NPC", "Let's talk!").end());
597
598 let mut player = DialoguePlayer::new(tree);
599 player.start();
600 player.advance(); assert_eq!(player.state, DialogueState::Choosing);
602 assert_eq!(player.choices.len(), 2);
603
604 player.select(1); let ev = player.advance();
606 assert!(matches!(ev, Some(DialogueEvent::ChoiceMade { index: 1, .. })));
607 }
608
609 #[test]
610 fn emotion_colors_defined() {
611 use DialogueEmotion::*;
612 for em in [Neutral, Happy, Sad, Angry, Surprised, Scared, Suspicious, Mysterious] {
613 let c = em.color();
614 assert!(c[3] > 0.0); }
616 }
617}