1use std::collections::HashMap;
7
8use super::runtime::{
9 AlterCategory, AlterPresenceState, AnimaState, FrontingState,
10 PluralSystem, RealityLayer,
11};
12
13#[derive(Debug, Clone)]
19pub struct DialogueTree {
20 pub id: String,
22 pub nodes: HashMap<String, DialogueNode>,
24 pub entry_node: String,
26 pub variables: HashMap<String, DialogueValue>,
28 pub speaker: SpeakerInfo,
30}
31
32#[derive(Debug, Clone)]
34pub struct DialogueNode {
35 pub id: String,
37 pub content: DialogueContent,
39 pub conditions: Vec<DialogueCondition>,
41 pub effects: Vec<DialogueEffect>,
43 pub responses: Vec<DialogueResponse>,
45 pub next: Option<String>,
47 pub tags: Vec<String>,
49}
50
51#[derive(Debug, Clone)]
53pub struct DialogueContent {
54 pub base_text: String,
56 pub alter_variations: HashMap<String, AlterDialogueVariation>,
58 pub layer_variations: HashMap<RealityLayer, String>,
60 pub emotional_variations: Vec<EmotionalVariation>,
62 pub voice_cues: Vec<String>,
64 pub animations: Vec<String>,
66}
67
68#[derive(Debug, Clone)]
70pub struct AlterDialogueVariation {
71 pub text: String,
73 pub observations: Vec<String>,
75 pub tone: DialogueTone,
77 pub recognition: Option<RecognitionEvent>,
79}
80
81#[derive(Debug, Clone)]
83pub struct EmotionalVariation {
84 pub condition: EmotionalCondition,
86 pub text: String,
88}
89
90#[derive(Debug, Clone)]
92pub enum EmotionalCondition {
93 HighDissociation(f32),
95 LowStability(f32),
97 HighArousal(f32),
99 AnimaState { pleasure: (f32, f32), arousal: (f32, f32), dominance: (f32, f32) },
101 RecentTrigger(String),
103}
104
105#[derive(Debug, Clone, PartialEq)]
107pub enum DialogueTone {
108 Neutral,
109 Warm,
110 Cold,
111 Suspicious,
112 Aggressive,
113 Fearful,
114 Curious,
115 Analytical,
116 Playful,
117 Guarded,
118}
119
120#[derive(Debug, Clone)]
122pub struct RecognitionEvent {
123 pub target: String,
125 pub recognition_type: RecognitionType,
127 pub intensity: f32,
129}
130
131#[derive(Debug, Clone)]
133pub enum RecognitionType {
134 Abuser,
136 SafePerson,
138 Place,
140 TraumaObject,
142 Familiar,
144}
145
146#[derive(Debug, Clone)]
152pub struct DialogueResponse {
153 pub id: String,
155 pub text: String,
157 pub alter_variations: HashMap<String, String>,
159 pub conditions: Vec<DialogueCondition>,
161 pub target_node: String,
163 pub effects: Vec<DialogueEffect>,
165 pub internal: bool,
167 pub required_traits: Vec<String>,
169 pub forbidden_traits: Vec<String>,
171}
172
173#[derive(Debug, Clone)]
179pub enum DialogueCondition {
180 AlterFronting(String),
182 CategoryFronting(AlterCategory),
184 AlterCoConscious(String),
186 RealityLayer(RealityLayer),
188 StabilityAbove(f32),
190 DissociationBelow(f32),
192 Variable { name: String, op: CompareOp, value: DialogueValue },
194 FlagSet(String),
196 AlterHasTrait(String),
198 AnimaInRange { pleasure: (f32, f32), arousal: (f32, f32), dominance: (f32, f32) },
200 HasItem(String),
202 HasAbility(String),
204 NodeVisited(String),
206 All(Vec<DialogueCondition>),
208 Any(Vec<DialogueCondition>),
210 Not(Box<DialogueCondition>),
212}
213
214#[derive(Debug, Clone)]
216pub enum CompareOp {
217 Eq,
218 Ne,
219 Lt,
220 Le,
221 Gt,
222 Ge,
223}
224
225#[derive(Debug, Clone, PartialEq)]
227pub enum DialogueValue {
228 Bool(bool),
229 Int(i32),
230 Float(f32),
231 String(String),
232}
233
234#[derive(Debug, Clone)]
236pub enum DialogueEffect {
237 SetVariable { name: String, value: DialogueValue },
239 SetFlag(String),
241 ClearFlag(String),
243 ModifyAnima { pleasure: f32, arousal: f32, dominance: f32 },
245 ModifyStability(f32),
247 ModifyDissociation(f32),
249 RequestSwitch { alter_id: String, urgency: f32 },
251 ActivateTrigger(String),
253 GiveItem(String),
255 TakeItem(String),
257 UnlockAbility(String),
259 ShiftReality { target: RealityLayer, rate: f32 },
261 PlaySound(String),
263 StartCutscene(String),
265 GrantInsight { id: String, amount: f32 },
267 EndDialogue,
269}
270
271#[derive(Debug, Clone)]
277pub struct SpeakerInfo {
278 pub id: String,
280 pub name: String,
282 pub portrait: String,
284 pub alter_portraits: HashMap<String, String>,
286 pub layer_portraits: HashMap<RealityLayer, String>,
288}
289
290pub struct DialogueManager {
296 pub current_tree: Option<DialogueTree>,
298 pub current_node: Option<String>,
300 pub visited_nodes: Vec<String>,
302 pub variables: HashMap<String, DialogueValue>,
304 pub flags: HashMap<String, bool>,
306 trees: HashMap<String, DialogueTree>,
308}
309
310impl DialogueManager {
311 pub fn new() -> Self {
313 Self {
314 current_tree: None,
315 current_node: None,
316 visited_nodes: Vec::new(),
317 variables: HashMap::new(),
318 flags: HashMap::new(),
319 trees: HashMap::new(),
320 }
321 }
322
323 pub fn load_tree(&mut self, tree: DialogueTree) {
325 self.trees.insert(tree.id.clone(), tree);
326 }
327
328 pub fn start_dialogue(&mut self, tree_id: &str) -> Result<(), DialogueError> {
330 let tree = self.trees.get(tree_id)
331 .ok_or_else(|| DialogueError::TreeNotFound(tree_id.to_string()))?
332 .clone();
333
334 self.current_node = Some(tree.entry_node.clone());
335 self.current_tree = Some(tree);
336 self.visited_nodes.clear();
337
338 Ok(())
339 }
340
341 pub fn get_current_content(&self, system: &PluralSystem) -> Result<ResolvedDialogue, DialogueError> {
343 let tree = self.current_tree.as_ref()
344 .ok_or(DialogueError::NoActiveDialogue)?;
345
346 let node_id = self.current_node.as_ref()
347 .ok_or(DialogueError::NoActiveDialogue)?;
348
349 let node = tree.nodes.get(node_id)
350 .ok_or_else(|| DialogueError::NodeNotFound(node_id.clone()))?;
351
352 let text = self.resolve_content(&node.content, system, &tree.speaker);
354 let responses = self.resolve_responses(&node.responses, system);
355 let speaker = self.resolve_speaker(&tree.speaker, system);
356
357 Ok(ResolvedDialogue {
358 node_id: node_id.clone(),
359 text,
360 responses,
361 speaker,
362 voice_cues: node.content.voice_cues.clone(),
363 animations: node.content.animations.clone(),
364 })
365 }
366
367 pub fn select_response(&mut self, response_id: &str, system: &mut PluralSystem) -> Result<DialogueResult, DialogueError> {
369 let (response_effects, response_conditions, response_target, node_id_clone) = {
371 let tree = self.current_tree.as_ref()
372 .ok_or(DialogueError::NoActiveDialogue)?;
373
374 let node_id = self.current_node.as_ref()
375 .ok_or(DialogueError::NoActiveDialogue)?;
376
377 let node = tree.nodes.get(node_id)
378 .ok_or_else(|| DialogueError::NodeNotFound(node_id.clone()))?;
379
380 let response = node.responses.iter()
382 .find(|r| r.id == response_id)
383 .ok_or_else(|| DialogueError::ResponseNotFound(response_id.to_string()))?;
384
385 (
386 response.effects.clone(),
387 response.conditions.clone(),
388 response.target_node.clone(),
389 node_id.clone(),
390 )
391 };
392
393 if !self.check_conditions(&response_conditions, system) {
395 return Err(DialogueError::ConditionsNotMet);
396 }
397
398 let effects = self.apply_effects(&response_effects, system)?;
400
401 self.visited_nodes.push(node_id_clone);
403
404 if effects.iter().any(|e| matches!(e, AppliedEffect::EndDialogue)) {
406 self.end_dialogue();
407 return Ok(DialogueResult::Ended);
408 }
409
410 self.current_node = Some(response_target);
412
413 Ok(DialogueResult::Continue(effects))
414 }
415
416 pub fn advance(&mut self, system: &mut PluralSystem) -> Result<DialogueResult, DialogueError> {
418 let (node_effects, node_next, has_responses, node_id_clone) = {
420 let tree = self.current_tree.as_ref()
421 .ok_or(DialogueError::NoActiveDialogue)?;
422
423 let node_id = self.current_node.as_ref()
424 .ok_or(DialogueError::NoActiveDialogue)?;
425
426 let node = tree.nodes.get(node_id)
427 .ok_or_else(|| DialogueError::NodeNotFound(node_id.clone()))?;
428
429 (
430 node.effects.clone(),
431 node.next.clone(),
432 !node.responses.is_empty(),
433 node_id.clone(),
434 )
435 };
436
437 let effects = self.apply_effects(&node_effects, system)?;
439
440 self.visited_nodes.push(node_id_clone);
442
443 if effects.iter().any(|e| matches!(e, AppliedEffect::EndDialogue)) {
445 self.end_dialogue();
446 return Ok(DialogueResult::Ended);
447 }
448
449 match node_next {
451 Some(next_id) => {
452 self.current_node = Some(next_id);
453 Ok(DialogueResult::Continue(effects))
454 }
455 None if !has_responses => {
456 self.end_dialogue();
457 Ok(DialogueResult::Ended)
458 }
459 None => Ok(DialogueResult::AwaitingChoice(effects)),
460 }
461 }
462
463 pub fn end_dialogue(&mut self) {
465 self.current_tree = None;
466 self.current_node = None;
467 }
468
469 pub fn is_active(&self) -> bool {
471 self.current_tree.is_some()
472 }
473
474 fn resolve_content(&self, content: &DialogueContent, system: &PluralSystem, _speaker: &SpeakerInfo) -> String {
480 if let Some(fronter_id) = self.get_fronter_id(system) {
482 if let Some(variation) = content.alter_variations.get(&fronter_id) {
483 return variation.text.clone();
484 }
485 }
486
487 if let Some(layer_text) = content.layer_variations.get(&system.reality_layer) {
489 return layer_text.clone();
490 }
491
492 for variation in &content.emotional_variations {
494 if self.check_emotional_condition(&variation.condition, system) {
495 return variation.text.clone();
496 }
497 }
498
499 content.base_text.clone()
501 }
502
503 fn resolve_responses(&self, responses: &[DialogueResponse], system: &PluralSystem) -> Vec<ResolvedResponse> {
505 let fronter_id = self.get_fronter_id(system);
506
507 responses.iter()
508 .filter(|r| self.check_conditions(&r.conditions, system))
509 .filter(|r| self.check_trait_requirements(r, system))
510 .map(|r| {
511 let text = fronter_id.as_ref()
513 .and_then(|id| r.alter_variations.get(id))
514 .cloned()
515 .unwrap_or_else(|| r.text.clone());
516
517 ResolvedResponse {
518 id: r.id.clone(),
519 text,
520 internal: r.internal,
521 }
522 })
523 .collect()
524 }
525
526 fn resolve_speaker(&self, speaker: &SpeakerInfo, system: &PluralSystem) -> ResolvedSpeaker {
528 let portrait = self.get_fronter_id(system)
530 .and_then(|id| speaker.alter_portraits.get(&id))
531 .cloned()
532 .or_else(|| speaker.layer_portraits.get(&system.reality_layer).cloned())
534 .unwrap_or_else(|| speaker.portrait.clone());
536
537 ResolvedSpeaker {
538 name: speaker.name.clone(),
539 portrait,
540 }
541 }
542
543 fn check_conditions(&self, conditions: &[DialogueCondition], system: &PluralSystem) -> bool {
549 conditions.iter().all(|c| self.check_condition(c, system))
550 }
551
552 fn check_condition(&self, condition: &DialogueCondition, system: &PluralSystem) -> bool {
554 match condition {
555 DialogueCondition::AlterFronting(id) => {
556 self.get_fronter_id(system).as_ref() == Some(id)
557 }
558 DialogueCondition::CategoryFronting(category) => {
559 if let Some(fronter_id) = self.get_fronter_id(system) {
560 system.alters.get(&fronter_id)
561 .map(|a| &a.category == category)
562 .unwrap_or(false)
563 } else {
564 false
565 }
566 }
567 DialogueCondition::AlterCoConscious(id) => {
568 system.alters.get(id)
569 .map(|a| matches!(a.state, AlterPresenceState::CoConscious))
570 .unwrap_or(false)
571 }
572 DialogueCondition::RealityLayer(layer) => {
573 &system.reality_layer == layer
574 }
575 DialogueCondition::StabilityAbove(threshold) => {
576 system.stability >= *threshold
577 }
578 DialogueCondition::DissociationBelow(threshold) => {
579 system.dissociation < *threshold
580 }
581 DialogueCondition::Variable { name, op, value } => {
582 self.variables.get(name)
583 .map(|v| self.compare_values(v, value, op))
584 .unwrap_or(false)
585 }
586 DialogueCondition::FlagSet(flag) => {
587 self.flags.get(flag).copied().unwrap_or(false)
588 }
589 DialogueCondition::AlterHasTrait(trait_name) => {
590 if let Some(fronter_id) = self.get_fronter_id(system) {
591 system.alters.get(&fronter_id)
592 .map(|a| a.abilities.contains(trait_name))
593 .unwrap_or(false)
594 } else {
595 false
596 }
597 }
598 DialogueCondition::AnimaInRange { pleasure, arousal, dominance } => {
599 let anima = &system.anima;
600 anima.pleasure >= pleasure.0 && anima.pleasure <= pleasure.1 &&
601 anima.arousal >= arousal.0 && anima.arousal <= arousal.1 &&
602 anima.dominance >= dominance.0 && anima.dominance <= dominance.1
603 }
604 DialogueCondition::HasItem(_item_id) => {
605 true
607 }
608 DialogueCondition::HasAbility(_ability_id) => {
609 true
611 }
612 DialogueCondition::NodeVisited(node_id) => {
613 self.visited_nodes.contains(node_id)
614 }
615 DialogueCondition::All(conditions) => {
616 conditions.iter().all(|c| self.check_condition(c, system))
617 }
618 DialogueCondition::Any(conditions) => {
619 conditions.iter().any(|c| self.check_condition(c, system))
620 }
621 DialogueCondition::Not(condition) => {
622 !self.check_condition(condition, system)
623 }
624 }
625 }
626
627 fn check_trait_requirements(&self, response: &DialogueResponse, system: &PluralSystem) -> bool {
629 let fronter_id = match self.get_fronter_id(system) {
630 Some(id) => id,
631 None => return response.required_traits.is_empty(),
632 };
633
634 let alter = match system.alters.get(&fronter_id) {
635 Some(a) => a,
636 None => return response.required_traits.is_empty(),
637 };
638
639 let has_required = response.required_traits.iter()
641 .all(|t| alter.abilities.contains(t));
642
643 let has_forbidden = response.forbidden_traits.iter()
645 .any(|t| alter.abilities.contains(t));
646
647 has_required && !has_forbidden
648 }
649
650 fn check_emotional_condition(&self, condition: &EmotionalCondition, system: &PluralSystem) -> bool {
652 match condition {
653 EmotionalCondition::HighDissociation(threshold) => {
654 system.dissociation >= *threshold
655 }
656 EmotionalCondition::LowStability(threshold) => {
657 system.stability < *threshold
658 }
659 EmotionalCondition::HighArousal(threshold) => {
660 system.anima.arousal >= *threshold
661 }
662 EmotionalCondition::AnimaState { pleasure, arousal, dominance } => {
663 let anima = &system.anima;
664 anima.pleasure >= pleasure.0 && anima.pleasure <= pleasure.1 &&
665 anima.arousal >= arousal.0 && anima.arousal <= arousal.1 &&
666 anima.dominance >= dominance.0 && anima.dominance <= dominance.1
667 }
668 EmotionalCondition::RecentTrigger(trigger_id) => {
669 system.active_triggers.iter().any(|t| &t.id == trigger_id)
670 }
671 }
672 }
673
674 fn compare_values(&self, a: &DialogueValue, b: &DialogueValue, op: &CompareOp) -> bool {
676 match (a, b) {
677 (DialogueValue::Bool(a), DialogueValue::Bool(b)) => match op {
678 CompareOp::Eq => a == b,
679 CompareOp::Ne => a != b,
680 _ => false,
681 },
682 (DialogueValue::Int(a), DialogueValue::Int(b)) => match op {
683 CompareOp::Eq => a == b,
684 CompareOp::Ne => a != b,
685 CompareOp::Lt => a < b,
686 CompareOp::Le => a <= b,
687 CompareOp::Gt => a > b,
688 CompareOp::Ge => a >= b,
689 },
690 (DialogueValue::Float(a), DialogueValue::Float(b)) => match op {
691 CompareOp::Eq => (a - b).abs() < f32::EPSILON,
692 CompareOp::Ne => (a - b).abs() >= f32::EPSILON,
693 CompareOp::Lt => a < b,
694 CompareOp::Le => a <= b,
695 CompareOp::Gt => a > b,
696 CompareOp::Ge => a >= b,
697 },
698 (DialogueValue::String(a), DialogueValue::String(b)) => match op {
699 CompareOp::Eq => a == b,
700 CompareOp::Ne => a != b,
701 _ => false,
702 },
703 _ => false,
704 }
705 }
706
707 fn apply_effects(&mut self, effects: &[DialogueEffect], system: &mut PluralSystem) -> Result<Vec<AppliedEffect>, DialogueError> {
713 let mut applied = Vec::new();
714
715 for effect in effects {
716 match effect {
717 DialogueEffect::SetVariable { name, value } => {
718 self.variables.insert(name.clone(), value.clone());
719 applied.push(AppliedEffect::VariableSet(name.clone()));
720 }
721 DialogueEffect::SetFlag(flag) => {
722 self.flags.insert(flag.clone(), true);
723 applied.push(AppliedEffect::FlagSet(flag.clone()));
724 }
725 DialogueEffect::ClearFlag(flag) => {
726 self.flags.insert(flag.clone(), false);
727 applied.push(AppliedEffect::FlagCleared(flag.clone()));
728 }
729 DialogueEffect::ModifyAnima { pleasure, arousal, dominance } => {
730 system.anima.pleasure = (system.anima.pleasure + pleasure).clamp(-1.0, 1.0);
731 system.anima.arousal = (system.anima.arousal + arousal).clamp(-1.0, 1.0);
732 system.anima.dominance = (system.anima.dominance + dominance).clamp(-1.0, 1.0);
733 applied.push(AppliedEffect::AnimaModified);
734 }
735 DialogueEffect::ModifyStability(delta) => {
736 system.stability = (system.stability + delta).clamp(0.0, 1.0);
737 applied.push(AppliedEffect::StabilityModified);
738 }
739 DialogueEffect::ModifyDissociation(delta) => {
740 system.dissociation = (system.dissociation + delta).clamp(0.0, 1.0);
741 applied.push(AppliedEffect::DissociationModified);
742 }
743 DialogueEffect::RequestSwitch { alter_id, urgency } => {
744 system.request_switch(alter_id, *urgency, false);
745 applied.push(AppliedEffect::SwitchRequested(alter_id.clone()));
746 }
747 DialogueEffect::ActivateTrigger(trigger_id) => {
748 applied.push(AppliedEffect::TriggerActivated(trigger_id.clone()));
750 }
751 DialogueEffect::GiveItem(item_id) => {
752 applied.push(AppliedEffect::ItemGiven(item_id.clone()));
753 }
754 DialogueEffect::TakeItem(item_id) => {
755 applied.push(AppliedEffect::ItemTaken(item_id.clone()));
756 }
757 DialogueEffect::UnlockAbility(ability_id) => {
758 applied.push(AppliedEffect::AbilityUnlocked(ability_id.clone()));
759 }
760 DialogueEffect::ShiftReality { target, rate } => {
761 applied.push(AppliedEffect::RealityShifted(target.clone()));
763 }
764 DialogueEffect::PlaySound(sound_id) => {
765 applied.push(AppliedEffect::SoundPlayed(sound_id.clone()));
766 }
767 DialogueEffect::StartCutscene(cutscene_id) => {
768 applied.push(AppliedEffect::CutsceneStarted(cutscene_id.clone()));
769 }
770 DialogueEffect::GrantInsight { id, amount } => {
771 applied.push(AppliedEffect::InsightGranted(id.clone(), *amount));
772 }
773 DialogueEffect::EndDialogue => {
774 applied.push(AppliedEffect::EndDialogue);
775 }
776 }
777 }
778
779 Ok(applied)
780 }
781
782 fn get_fronter_id(&self, system: &PluralSystem) -> Option<String> {
788 match &system.fronting {
789 FrontingState::Single(id) => Some(id.clone()),
790 FrontingState::Blended(ids) => ids.first().cloned(),
791 _ => None,
792 }
793 }
794}
795
796impl Default for DialogueManager {
797 fn default() -> Self {
798 Self::new()
799 }
800}
801
802#[derive(Debug, Clone)]
808pub struct ResolvedDialogue {
809 pub node_id: String,
811 pub text: String,
813 pub responses: Vec<ResolvedResponse>,
815 pub speaker: ResolvedSpeaker,
817 pub voice_cues: Vec<String>,
819 pub animations: Vec<String>,
821}
822
823#[derive(Debug, Clone)]
825pub struct ResolvedResponse {
826 pub id: String,
828 pub text: String,
830 pub internal: bool,
832}
833
834#[derive(Debug, Clone)]
836pub struct ResolvedSpeaker {
837 pub name: String,
839 pub portrait: String,
841}
842
843#[derive(Debug)]
845pub enum DialogueResult {
846 Continue(Vec<AppliedEffect>),
848 AwaitingChoice(Vec<AppliedEffect>),
850 Ended,
852}
853
854#[derive(Debug, Clone)]
856pub enum AppliedEffect {
857 VariableSet(String),
858 FlagSet(String),
859 FlagCleared(String),
860 AnimaModified,
861 StabilityModified,
862 DissociationModified,
863 SwitchRequested(String),
864 TriggerActivated(String),
865 ItemGiven(String),
866 ItemTaken(String),
867 AbilityUnlocked(String),
868 RealityShifted(RealityLayer),
869 SoundPlayed(String),
870 CutsceneStarted(String),
871 InsightGranted(String, f32),
872 EndDialogue,
873}
874
875#[derive(Debug)]
881pub enum DialogueError {
882 TreeNotFound(String),
884 NoActiveDialogue,
886 NodeNotFound(String),
888 ResponseNotFound(String),
890 ConditionsNotMet,
892}
893
894impl std::fmt::Display for DialogueError {
895 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
896 match self {
897 DialogueError::TreeNotFound(id) => write!(f, "Dialogue tree not found: {}", id),
898 DialogueError::NoActiveDialogue => write!(f, "No active dialogue"),
899 DialogueError::NodeNotFound(id) => write!(f, "Dialogue node not found: {}", id),
900 DialogueError::ResponseNotFound(id) => write!(f, "Response not found: {}", id),
901 DialogueError::ConditionsNotMet => write!(f, "Conditions not met for this action"),
902 }
903 }
904}
905
906impl std::error::Error for DialogueError {}
907
908pub struct DialogueTreeBuilder {
914 id: String,
915 nodes: HashMap<String, DialogueNode>,
916 entry_node: Option<String>,
917 speaker: Option<SpeakerInfo>,
918}
919
920impl DialogueTreeBuilder {
921 pub fn new(id: &str) -> Self {
923 Self {
924 id: id.to_string(),
925 nodes: HashMap::new(),
926 entry_node: None,
927 speaker: None,
928 }
929 }
930
931 pub fn entry(mut self, node_id: &str) -> Self {
933 self.entry_node = Some(node_id.to_string());
934 self
935 }
936
937 pub fn speaker(mut self, speaker: SpeakerInfo) -> Self {
939 self.speaker = Some(speaker);
940 self
941 }
942
943 pub fn node(mut self, node: DialogueNode) -> Self {
945 self.nodes.insert(node.id.clone(), node);
946 self
947 }
948
949 pub fn build(self) -> Result<DialogueTree, String> {
951 let entry_node = self.entry_node.ok_or("No entry node specified")?;
952 let speaker = self.speaker.ok_or("No speaker specified")?;
953
954 if !self.nodes.contains_key(&entry_node) {
955 return Err(format!("Entry node '{}' not found in nodes", entry_node));
956 }
957
958 Ok(DialogueTree {
959 id: self.id,
960 nodes: self.nodes,
961 entry_node,
962 variables: HashMap::new(),
963 speaker,
964 })
965 }
966}
967
968pub struct DialogueNodeBuilder {
970 id: String,
971 base_text: String,
972 alter_variations: HashMap<String, AlterDialogueVariation>,
973 layer_variations: HashMap<RealityLayer, String>,
974 emotional_variations: Vec<EmotionalVariation>,
975 conditions: Vec<DialogueCondition>,
976 effects: Vec<DialogueEffect>,
977 responses: Vec<DialogueResponse>,
978 next: Option<String>,
979 voice_cues: Vec<String>,
980 animations: Vec<String>,
981 tags: Vec<String>,
982}
983
984impl DialogueNodeBuilder {
985 pub fn new(id: &str, text: &str) -> Self {
987 Self {
988 id: id.to_string(),
989 base_text: text.to_string(),
990 alter_variations: HashMap::new(),
991 layer_variations: HashMap::new(),
992 emotional_variations: Vec::new(),
993 conditions: Vec::new(),
994 effects: Vec::new(),
995 responses: Vec::new(),
996 next: None,
997 voice_cues: Vec::new(),
998 animations: Vec::new(),
999 tags: Vec::new(),
1000 }
1001 }
1002
1003 pub fn alter_variation(mut self, alter_id: &str, text: &str, tone: DialogueTone) -> Self {
1005 self.alter_variations.insert(alter_id.to_string(), AlterDialogueVariation {
1006 text: text.to_string(),
1007 observations: Vec::new(),
1008 tone,
1009 recognition: None,
1010 });
1011 self
1012 }
1013
1014 pub fn layer_variation(mut self, layer: RealityLayer, text: &str) -> Self {
1016 self.layer_variations.insert(layer, text.to_string());
1017 self
1018 }
1019
1020 pub fn condition(mut self, condition: DialogueCondition) -> Self {
1022 self.conditions.push(condition);
1023 self
1024 }
1025
1026 pub fn effect(mut self, effect: DialogueEffect) -> Self {
1028 self.effects.push(effect);
1029 self
1030 }
1031
1032 pub fn response(mut self, response: DialogueResponse) -> Self {
1034 self.responses.push(response);
1035 self
1036 }
1037
1038 pub fn next(mut self, node_id: &str) -> Self {
1040 self.next = Some(node_id.to_string());
1041 self
1042 }
1043
1044 pub fn voice_cue(mut self, cue: &str) -> Self {
1046 self.voice_cues.push(cue.to_string());
1047 self
1048 }
1049
1050 pub fn animation(mut self, anim: &str) -> Self {
1052 self.animations.push(anim.to_string());
1053 self
1054 }
1055
1056 pub fn tag(mut self, tag: &str) -> Self {
1058 self.tags.push(tag.to_string());
1059 self
1060 }
1061
1062 pub fn build(self) -> DialogueNode {
1064 DialogueNode {
1065 id: self.id,
1066 content: DialogueContent {
1067 base_text: self.base_text,
1068 alter_variations: self.alter_variations,
1069 layer_variations: self.layer_variations,
1070 emotional_variations: self.emotional_variations,
1071 voice_cues: self.voice_cues,
1072 animations: self.animations,
1073 },
1074 conditions: self.conditions,
1075 effects: self.effects,
1076 responses: self.responses,
1077 next: self.next,
1078 tags: self.tags,
1079 }
1080 }
1081}
1082
1083#[cfg(test)]
1088mod tests {
1089 use super::*;
1090
1091 fn create_test_system() -> PluralSystem {
1092 let mut system = PluralSystem::new("Test System");
1093 system.add_alter(super::super::runtime::Alter {
1094 id: "host".to_string(),
1095 name: "Host".to_string(),
1096 category: AlterCategory::Council,
1097 state: AlterPresenceState::Fronting,
1098 anima: AnimaState::default(),
1099 base_arousal: 0.0,
1100 base_dominance: 0.0,
1101 time_since_front: 0,
1102 triggers: Vec::new(),
1103 abilities: std::collections::HashSet::from(["analysis".to_string()]),
1104 preferred_reality: RealityLayer::Grounded,
1105 memory_access: super::super::runtime::MemoryAccess::Full,
1106 });
1107 system.fronting = FrontingState::Single("host".to_string());
1108 system
1109 }
1110
1111 fn create_test_tree() -> DialogueTree {
1112 let speaker = SpeakerInfo {
1113 id: "marcus".to_string(),
1114 name: "Father Marcus".to_string(),
1115 portrait: "marcus_default".to_string(),
1116 alter_portraits: HashMap::new(),
1117 layer_portraits: HashMap::new(),
1118 };
1119
1120 let node1 = DialogueNodeBuilder::new("start", "Greetings, traveler.")
1121 .alter_variation("host", "Welcome, child. I sense... complexity within you.", DialogueTone::Warm)
1122 .response(DialogueResponse {
1123 id: "ask_help".to_string(),
1124 text: "I need your help.".to_string(),
1125 alter_variations: HashMap::new(),
1126 conditions: Vec::new(),
1127 target_node: "help_offered".to_string(),
1128 effects: Vec::new(),
1129 internal: false,
1130 required_traits: Vec::new(),
1131 forbidden_traits: Vec::new(),
1132 })
1133 .response(DialogueResponse {
1134 id: "analyze".to_string(),
1135 text: "[Analyze] Something seems off about you...".to_string(),
1136 alter_variations: HashMap::new(),
1137 conditions: vec![DialogueCondition::AlterHasTrait("analysis".to_string())],
1138 target_node: "analysis_result".to_string(),
1139 effects: Vec::new(),
1140 internal: false,
1141 required_traits: vec!["analysis".to_string()],
1142 forbidden_traits: Vec::new(),
1143 })
1144 .build();
1145
1146 let node2 = DialogueNodeBuilder::new("help_offered", "How may I assist you?")
1147 .effect(DialogueEffect::EndDialogue)
1148 .build();
1149
1150 let node3 = DialogueNodeBuilder::new("analysis_result", "You... you can see it, can't you?")
1151 .layer_variation(RealityLayer::Fractured, "The priest's form wavers. Behind his smile, shadow teeth.")
1152 .effect(DialogueEffect::ModifyAnima { pleasure: -0.1, arousal: 0.2, dominance: 0.0 })
1153 .effect(DialogueEffect::EndDialogue)
1154 .build();
1155
1156 DialogueTreeBuilder::new("marcus_greeting")
1157 .entry("start")
1158 .speaker(speaker)
1159 .node(node1)
1160 .node(node2)
1161 .node(node3)
1162 .build()
1163 .unwrap()
1164 }
1165
1166 #[test]
1167 fn test_dialogue_manager_creation() {
1168 let manager = DialogueManager::new();
1169 assert!(!manager.is_active());
1170 }
1171
1172 #[test]
1173 fn test_start_dialogue() {
1174 let mut manager = DialogueManager::new();
1175 manager.load_tree(create_test_tree());
1176
1177 let result = manager.start_dialogue("marcus_greeting");
1178 assert!(result.is_ok());
1179 assert!(manager.is_active());
1180 }
1181
1182 #[test]
1183 fn test_get_current_content() {
1184 let mut manager = DialogueManager::new();
1185 manager.load_tree(create_test_tree());
1186 manager.start_dialogue("marcus_greeting").unwrap();
1187
1188 let system = create_test_system();
1189 let content = manager.get_current_content(&system).unwrap();
1190
1191 assert!(content.text.contains("Welcome, child"));
1193 assert_eq!(content.responses.len(), 2);
1194 }
1195
1196 #[test]
1197 fn test_select_response() {
1198 let mut manager = DialogueManager::new();
1199 manager.load_tree(create_test_tree());
1200 manager.start_dialogue("marcus_greeting").unwrap();
1201
1202 let mut system = create_test_system();
1203 let result = manager.select_response("ask_help", &mut system);
1205 assert!(matches!(result, Ok(DialogueResult::Continue(_))));
1206
1207 let result = manager.advance(&mut system);
1209 assert!(matches!(result, Ok(DialogueResult::Ended)));
1210 }
1211
1212 #[test]
1213 fn test_condition_checking() {
1214 let manager = DialogueManager::new();
1215 let system = create_test_system();
1216
1217 let condition = DialogueCondition::AlterFronting("host".to_string());
1219 assert!(manager.check_condition(&condition, &system));
1220
1221 let condition = DialogueCondition::AlterHasTrait("analysis".to_string());
1223 assert!(manager.check_condition(&condition, &system));
1224
1225 let condition = DialogueCondition::RealityLayer(RealityLayer::Grounded);
1227 assert!(manager.check_condition(&condition, &system));
1228 }
1229
1230 #[test]
1231 fn test_trait_filtered_responses() {
1232 let mut manager = DialogueManager::new();
1233 manager.load_tree(create_test_tree());
1234 manager.start_dialogue("marcus_greeting").unwrap();
1235
1236 let system = create_test_system();
1237 let content = manager.get_current_content(&system).unwrap();
1238
1239 let has_analyze = content.responses.iter().any(|r| r.id == "analyze");
1241 assert!(has_analyze);
1242 }
1243}