1use std::collections::HashMap;
7
8use super::runtime::{
9 AlterCategory, AlterPresenceState, AnimaState, FrontingState, PluralSystem, RealityLayer,
10};
11
12#[derive(Debug, Clone)]
18pub struct DialogueTree {
19 pub id: String,
21 pub nodes: HashMap<String, DialogueNode>,
23 pub entry_node: String,
25 pub variables: HashMap<String, DialogueValue>,
27 pub speaker: SpeakerInfo,
29}
30
31#[derive(Debug, Clone)]
33pub struct DialogueNode {
34 pub id: String,
36 pub content: DialogueContent,
38 pub conditions: Vec<DialogueCondition>,
40 pub effects: Vec<DialogueEffect>,
42 pub responses: Vec<DialogueResponse>,
44 pub next: Option<String>,
46 pub tags: Vec<String>,
48}
49
50#[derive(Debug, Clone)]
52pub struct DialogueContent {
53 pub base_text: String,
55 pub alter_variations: HashMap<String, AlterDialogueVariation>,
57 pub layer_variations: HashMap<RealityLayer, String>,
59 pub emotional_variations: Vec<EmotionalVariation>,
61 pub voice_cues: Vec<String>,
63 pub animations: Vec<String>,
65}
66
67#[derive(Debug, Clone)]
69pub struct AlterDialogueVariation {
70 pub text: String,
72 pub observations: Vec<String>,
74 pub tone: DialogueTone,
76 pub recognition: Option<RecognitionEvent>,
78}
79
80#[derive(Debug, Clone)]
82pub struct EmotionalVariation {
83 pub condition: EmotionalCondition,
85 pub text: String,
87}
88
89#[derive(Debug, Clone)]
91pub enum EmotionalCondition {
92 HighDissociation(f32),
94 LowStability(f32),
96 HighArousal(f32),
98 AnimaState {
100 pleasure: (f32, f32),
101 arousal: (f32, f32),
102 dominance: (f32, f32),
103 },
104 RecentTrigger(String),
106}
107
108#[derive(Debug, Clone, PartialEq)]
110pub enum DialogueTone {
111 Neutral,
112 Warm,
113 Cold,
114 Suspicious,
115 Aggressive,
116 Fearful,
117 Curious,
118 Analytical,
119 Playful,
120 Guarded,
121}
122
123#[derive(Debug, Clone)]
125pub struct RecognitionEvent {
126 pub target: String,
128 pub recognition_type: RecognitionType,
130 pub intensity: f32,
132}
133
134#[derive(Debug, Clone)]
136pub enum RecognitionType {
137 Abuser,
139 SafePerson,
141 Place,
143 TraumaObject,
145 Familiar,
147}
148
149#[derive(Debug, Clone)]
155pub struct DialogueResponse {
156 pub id: String,
158 pub text: String,
160 pub alter_variations: HashMap<String, String>,
162 pub conditions: Vec<DialogueCondition>,
164 pub target_node: String,
166 pub effects: Vec<DialogueEffect>,
168 pub internal: bool,
170 pub required_traits: Vec<String>,
172 pub forbidden_traits: Vec<String>,
174}
175
176#[derive(Debug, Clone)]
182pub enum DialogueCondition {
183 AlterFronting(String),
185 CategoryFronting(AlterCategory),
187 AlterCoConscious(String),
189 RealityLayer(RealityLayer),
191 StabilityAbove(f32),
193 DissociationBelow(f32),
195 Variable {
197 name: String,
198 op: CompareOp,
199 value: DialogueValue,
200 },
201 FlagSet(String),
203 AlterHasTrait(String),
205 AnimaInRange {
207 pleasure: (f32, f32),
208 arousal: (f32, f32),
209 dominance: (f32, f32),
210 },
211 HasItem(String),
213 HasAbility(String),
215 NodeVisited(String),
217 All(Vec<DialogueCondition>),
219 Any(Vec<DialogueCondition>),
221 Not(Box<DialogueCondition>),
223}
224
225#[derive(Debug, Clone)]
227pub enum CompareOp {
228 Eq,
229 Ne,
230 Lt,
231 Le,
232 Gt,
233 Ge,
234}
235
236#[derive(Debug, Clone, PartialEq)]
238pub enum DialogueValue {
239 Bool(bool),
240 Int(i32),
241 Float(f32),
242 String(String),
243}
244
245#[derive(Debug, Clone)]
247pub enum DialogueEffect {
248 SetVariable { name: String, value: DialogueValue },
250 SetFlag(String),
252 ClearFlag(String),
254 ModifyAnima {
256 pleasure: f32,
257 arousal: f32,
258 dominance: f32,
259 },
260 ModifyStability(f32),
262 ModifyDissociation(f32),
264 RequestSwitch { alter_id: String, urgency: f32 },
266 ActivateTrigger(String),
268 GiveItem(String),
270 TakeItem(String),
272 UnlockAbility(String),
274 ShiftReality { target: RealityLayer, rate: f32 },
276 PlaySound(String),
278 StartCutscene(String),
280 GrantInsight { id: String, amount: f32 },
282 EndDialogue,
284}
285
286#[derive(Debug, Clone)]
292pub struct SpeakerInfo {
293 pub id: String,
295 pub name: String,
297 pub portrait: String,
299 pub alter_portraits: HashMap<String, String>,
301 pub layer_portraits: HashMap<RealityLayer, String>,
303}
304
305pub struct DialogueManager {
311 pub current_tree: Option<DialogueTree>,
313 pub current_node: Option<String>,
315 pub visited_nodes: Vec<String>,
317 pub variables: HashMap<String, DialogueValue>,
319 pub flags: HashMap<String, bool>,
321 trees: HashMap<String, DialogueTree>,
323}
324
325impl DialogueManager {
326 pub fn new() -> Self {
328 Self {
329 current_tree: None,
330 current_node: None,
331 visited_nodes: Vec::new(),
332 variables: HashMap::new(),
333 flags: HashMap::new(),
334 trees: HashMap::new(),
335 }
336 }
337
338 pub fn load_tree(&mut self, tree: DialogueTree) {
340 self.trees.insert(tree.id.clone(), tree);
341 }
342
343 pub fn start_dialogue(&mut self, tree_id: &str) -> Result<(), DialogueError> {
345 let tree = self
346 .trees
347 .get(tree_id)
348 .ok_or_else(|| DialogueError::TreeNotFound(tree_id.to_string()))?
349 .clone();
350
351 self.current_node = Some(tree.entry_node.clone());
352 self.current_tree = Some(tree);
353 self.visited_nodes.clear();
354
355 Ok(())
356 }
357
358 pub fn get_current_content(
360 &self,
361 system: &PluralSystem,
362 ) -> Result<ResolvedDialogue, DialogueError> {
363 let tree = self
364 .current_tree
365 .as_ref()
366 .ok_or(DialogueError::NoActiveDialogue)?;
367
368 let node_id = self
369 .current_node
370 .as_ref()
371 .ok_or(DialogueError::NoActiveDialogue)?;
372
373 let node = tree
374 .nodes
375 .get(node_id)
376 .ok_or_else(|| DialogueError::NodeNotFound(node_id.clone()))?;
377
378 let text = self.resolve_content(&node.content, system, &tree.speaker);
380 let responses = self.resolve_responses(&node.responses, system);
381 let speaker = self.resolve_speaker(&tree.speaker, system);
382
383 Ok(ResolvedDialogue {
384 node_id: node_id.clone(),
385 text,
386 responses,
387 speaker,
388 voice_cues: node.content.voice_cues.clone(),
389 animations: node.content.animations.clone(),
390 })
391 }
392
393 pub fn select_response(
395 &mut self,
396 response_id: &str,
397 system: &mut PluralSystem,
398 ) -> Result<DialogueResult, DialogueError> {
399 let (response_effects, response_conditions, response_target, node_id_clone) = {
401 let tree = self
402 .current_tree
403 .as_ref()
404 .ok_or(DialogueError::NoActiveDialogue)?;
405
406 let node_id = self
407 .current_node
408 .as_ref()
409 .ok_or(DialogueError::NoActiveDialogue)?;
410
411 let node = tree
412 .nodes
413 .get(node_id)
414 .ok_or_else(|| DialogueError::NodeNotFound(node_id.clone()))?;
415
416 let response = node
418 .responses
419 .iter()
420 .find(|r| r.id == response_id)
421 .ok_or_else(|| DialogueError::ResponseNotFound(response_id.to_string()))?;
422
423 (
424 response.effects.clone(),
425 response.conditions.clone(),
426 response.target_node.clone(),
427 node_id.clone(),
428 )
429 };
430
431 if !self.check_conditions(&response_conditions, system) {
433 return Err(DialogueError::ConditionsNotMet);
434 }
435
436 let effects = self.apply_effects(&response_effects, system)?;
438
439 self.visited_nodes.push(node_id_clone);
441
442 if effects
444 .iter()
445 .any(|e| matches!(e, AppliedEffect::EndDialogue))
446 {
447 self.end_dialogue();
448 return Ok(DialogueResult::Ended);
449 }
450
451 self.current_node = Some(response_target);
453
454 Ok(DialogueResult::Continue(effects))
455 }
456
457 pub fn advance(&mut self, system: &mut PluralSystem) -> Result<DialogueResult, DialogueError> {
459 let (node_effects, node_next, has_responses, node_id_clone) = {
461 let tree = self
462 .current_tree
463 .as_ref()
464 .ok_or(DialogueError::NoActiveDialogue)?;
465
466 let node_id = self
467 .current_node
468 .as_ref()
469 .ok_or(DialogueError::NoActiveDialogue)?;
470
471 let node = tree
472 .nodes
473 .get(node_id)
474 .ok_or_else(|| DialogueError::NodeNotFound(node_id.clone()))?;
475
476 (
477 node.effects.clone(),
478 node.next.clone(),
479 !node.responses.is_empty(),
480 node_id.clone(),
481 )
482 };
483
484 let effects = self.apply_effects(&node_effects, system)?;
486
487 self.visited_nodes.push(node_id_clone);
489
490 if effects
492 .iter()
493 .any(|e| matches!(e, AppliedEffect::EndDialogue))
494 {
495 self.end_dialogue();
496 return Ok(DialogueResult::Ended);
497 }
498
499 match node_next {
501 Some(next_id) => {
502 self.current_node = Some(next_id);
503 Ok(DialogueResult::Continue(effects))
504 }
505 None if !has_responses => {
506 self.end_dialogue();
507 Ok(DialogueResult::Ended)
508 }
509 None => Ok(DialogueResult::AwaitingChoice(effects)),
510 }
511 }
512
513 pub fn end_dialogue(&mut self) {
515 self.current_tree = None;
516 self.current_node = None;
517 }
518
519 pub fn is_active(&self) -> bool {
521 self.current_tree.is_some()
522 }
523
524 fn resolve_content(
530 &self,
531 content: &DialogueContent,
532 system: &PluralSystem,
533 _speaker: &SpeakerInfo,
534 ) -> String {
535 if let Some(fronter_id) = self.get_fronter_id(system) {
537 if let Some(variation) = content.alter_variations.get(&fronter_id) {
538 return variation.text.clone();
539 }
540 }
541
542 if let Some(layer_text) = content.layer_variations.get(&system.reality_layer) {
544 return layer_text.clone();
545 }
546
547 for variation in &content.emotional_variations {
549 if self.check_emotional_condition(&variation.condition, system) {
550 return variation.text.clone();
551 }
552 }
553
554 content.base_text.clone()
556 }
557
558 fn resolve_responses(
560 &self,
561 responses: &[DialogueResponse],
562 system: &PluralSystem,
563 ) -> Vec<ResolvedResponse> {
564 let fronter_id = self.get_fronter_id(system);
565
566 responses
567 .iter()
568 .filter(|r| self.check_conditions(&r.conditions, system))
569 .filter(|r| self.check_trait_requirements(r, system))
570 .map(|r| {
571 let text = fronter_id
573 .as_ref()
574 .and_then(|id| r.alter_variations.get(id))
575 .cloned()
576 .unwrap_or_else(|| r.text.clone());
577
578 ResolvedResponse {
579 id: r.id.clone(),
580 text,
581 internal: r.internal,
582 }
583 })
584 .collect()
585 }
586
587 fn resolve_speaker(&self, speaker: &SpeakerInfo, system: &PluralSystem) -> ResolvedSpeaker {
589 let portrait = self
591 .get_fronter_id(system)
592 .and_then(|id| speaker.alter_portraits.get(&id))
593 .cloned()
594 .or_else(|| speaker.layer_portraits.get(&system.reality_layer).cloned())
596 .unwrap_or_else(|| speaker.portrait.clone());
598
599 ResolvedSpeaker {
600 name: speaker.name.clone(),
601 portrait,
602 }
603 }
604
605 fn check_conditions(&self, conditions: &[DialogueCondition], system: &PluralSystem) -> bool {
611 conditions.iter().all(|c| self.check_condition(c, system))
612 }
613
614 fn check_condition(&self, condition: &DialogueCondition, system: &PluralSystem) -> bool {
616 match condition {
617 DialogueCondition::AlterFronting(id) => {
618 self.get_fronter_id(system).as_ref() == Some(id)
619 }
620 DialogueCondition::CategoryFronting(category) => {
621 if let Some(fronter_id) = self.get_fronter_id(system) {
622 system
623 .alters
624 .get(&fronter_id)
625 .map(|a| &a.category == category)
626 .unwrap_or(false)
627 } else {
628 false
629 }
630 }
631 DialogueCondition::AlterCoConscious(id) => system
632 .alters
633 .get(id)
634 .map(|a| matches!(a.state, AlterPresenceState::CoConscious))
635 .unwrap_or(false),
636 DialogueCondition::RealityLayer(layer) => &system.reality_layer == layer,
637 DialogueCondition::StabilityAbove(threshold) => system.stability >= *threshold,
638 DialogueCondition::DissociationBelow(threshold) => system.dissociation < *threshold,
639 DialogueCondition::Variable { name, op, value } => self
640 .variables
641 .get(name)
642 .map(|v| self.compare_values(v, value, op))
643 .unwrap_or(false),
644 DialogueCondition::FlagSet(flag) => self.flags.get(flag).copied().unwrap_or(false),
645 DialogueCondition::AlterHasTrait(trait_name) => {
646 if let Some(fronter_id) = self.get_fronter_id(system) {
647 system
648 .alters
649 .get(&fronter_id)
650 .map(|a| a.abilities.contains(trait_name))
651 .unwrap_or(false)
652 } else {
653 false
654 }
655 }
656 DialogueCondition::AnimaInRange {
657 pleasure,
658 arousal,
659 dominance,
660 } => {
661 let anima = &system.anima;
662 anima.pleasure >= pleasure.0
663 && anima.pleasure <= pleasure.1
664 && anima.arousal >= arousal.0
665 && anima.arousal <= arousal.1
666 && anima.dominance >= dominance.0
667 && anima.dominance <= dominance.1
668 }
669 DialogueCondition::HasItem(_item_id) => {
670 true
672 }
673 DialogueCondition::HasAbility(_ability_id) => {
674 true
676 }
677 DialogueCondition::NodeVisited(node_id) => self.visited_nodes.contains(node_id),
678 DialogueCondition::All(conditions) => {
679 conditions.iter().all(|c| self.check_condition(c, system))
680 }
681 DialogueCondition::Any(conditions) => {
682 conditions.iter().any(|c| self.check_condition(c, system))
683 }
684 DialogueCondition::Not(condition) => !self.check_condition(condition, system),
685 }
686 }
687
688 fn check_trait_requirements(&self, response: &DialogueResponse, system: &PluralSystem) -> bool {
690 let fronter_id = match self.get_fronter_id(system) {
691 Some(id) => id,
692 None => return response.required_traits.is_empty(),
693 };
694
695 let alter = match system.alters.get(&fronter_id) {
696 Some(a) => a,
697 None => return response.required_traits.is_empty(),
698 };
699
700 let has_required = response
702 .required_traits
703 .iter()
704 .all(|t| alter.abilities.contains(t));
705
706 let has_forbidden = response
708 .forbidden_traits
709 .iter()
710 .any(|t| alter.abilities.contains(t));
711
712 has_required && !has_forbidden
713 }
714
715 fn check_emotional_condition(
717 &self,
718 condition: &EmotionalCondition,
719 system: &PluralSystem,
720 ) -> bool {
721 match condition {
722 EmotionalCondition::HighDissociation(threshold) => system.dissociation >= *threshold,
723 EmotionalCondition::LowStability(threshold) => system.stability < *threshold,
724 EmotionalCondition::HighArousal(threshold) => system.anima.arousal >= *threshold,
725 EmotionalCondition::AnimaState {
726 pleasure,
727 arousal,
728 dominance,
729 } => {
730 let anima = &system.anima;
731 anima.pleasure >= pleasure.0
732 && anima.pleasure <= pleasure.1
733 && anima.arousal >= arousal.0
734 && anima.arousal <= arousal.1
735 && anima.dominance >= dominance.0
736 && anima.dominance <= dominance.1
737 }
738 EmotionalCondition::RecentTrigger(trigger_id) => {
739 system.active_triggers.iter().any(|t| &t.id == trigger_id)
740 }
741 }
742 }
743
744 fn compare_values(&self, a: &DialogueValue, b: &DialogueValue, op: &CompareOp) -> bool {
746 match (a, b) {
747 (DialogueValue::Bool(a), DialogueValue::Bool(b)) => match op {
748 CompareOp::Eq => a == b,
749 CompareOp::Ne => a != b,
750 _ => false,
751 },
752 (DialogueValue::Int(a), DialogueValue::Int(b)) => match op {
753 CompareOp::Eq => a == b,
754 CompareOp::Ne => a != b,
755 CompareOp::Lt => a < b,
756 CompareOp::Le => a <= b,
757 CompareOp::Gt => a > b,
758 CompareOp::Ge => a >= b,
759 },
760 (DialogueValue::Float(a), DialogueValue::Float(b)) => match op {
761 CompareOp::Eq => (a - b).abs() < f32::EPSILON,
762 CompareOp::Ne => (a - b).abs() >= f32::EPSILON,
763 CompareOp::Lt => a < b,
764 CompareOp::Le => a <= b,
765 CompareOp::Gt => a > b,
766 CompareOp::Ge => a >= b,
767 },
768 (DialogueValue::String(a), DialogueValue::String(b)) => match op {
769 CompareOp::Eq => a == b,
770 CompareOp::Ne => a != b,
771 _ => false,
772 },
773 _ => false,
774 }
775 }
776
777 fn apply_effects(
783 &mut self,
784 effects: &[DialogueEffect],
785 system: &mut PluralSystem,
786 ) -> Result<Vec<AppliedEffect>, DialogueError> {
787 let mut applied = Vec::new();
788
789 for effect in effects {
790 match effect {
791 DialogueEffect::SetVariable { name, value } => {
792 self.variables.insert(name.clone(), value.clone());
793 applied.push(AppliedEffect::VariableSet(name.clone()));
794 }
795 DialogueEffect::SetFlag(flag) => {
796 self.flags.insert(flag.clone(), true);
797 applied.push(AppliedEffect::FlagSet(flag.clone()));
798 }
799 DialogueEffect::ClearFlag(flag) => {
800 self.flags.insert(flag.clone(), false);
801 applied.push(AppliedEffect::FlagCleared(flag.clone()));
802 }
803 DialogueEffect::ModifyAnima {
804 pleasure,
805 arousal,
806 dominance,
807 } => {
808 system.anima.pleasure = (system.anima.pleasure + pleasure).clamp(-1.0, 1.0);
809 system.anima.arousal = (system.anima.arousal + arousal).clamp(-1.0, 1.0);
810 system.anima.dominance = (system.anima.dominance + dominance).clamp(-1.0, 1.0);
811 applied.push(AppliedEffect::AnimaModified);
812 }
813 DialogueEffect::ModifyStability(delta) => {
814 system.stability = (system.stability + delta).clamp(0.0, 1.0);
815 applied.push(AppliedEffect::StabilityModified);
816 }
817 DialogueEffect::ModifyDissociation(delta) => {
818 system.dissociation = (system.dissociation + delta).clamp(0.0, 1.0);
819 applied.push(AppliedEffect::DissociationModified);
820 }
821 DialogueEffect::RequestSwitch { alter_id, urgency } => {
822 system.request_switch(alter_id, *urgency, false);
823 applied.push(AppliedEffect::SwitchRequested(alter_id.clone()));
824 }
825 DialogueEffect::ActivateTrigger(trigger_id) => {
826 applied.push(AppliedEffect::TriggerActivated(trigger_id.clone()));
828 }
829 DialogueEffect::GiveItem(item_id) => {
830 applied.push(AppliedEffect::ItemGiven(item_id.clone()));
831 }
832 DialogueEffect::TakeItem(item_id) => {
833 applied.push(AppliedEffect::ItemTaken(item_id.clone()));
834 }
835 DialogueEffect::UnlockAbility(ability_id) => {
836 applied.push(AppliedEffect::AbilityUnlocked(ability_id.clone()));
837 }
838 DialogueEffect::ShiftReality { target, rate } => {
839 applied.push(AppliedEffect::RealityShifted(target.clone()));
841 }
842 DialogueEffect::PlaySound(sound_id) => {
843 applied.push(AppliedEffect::SoundPlayed(sound_id.clone()));
844 }
845 DialogueEffect::StartCutscene(cutscene_id) => {
846 applied.push(AppliedEffect::CutsceneStarted(cutscene_id.clone()));
847 }
848 DialogueEffect::GrantInsight { id, amount } => {
849 applied.push(AppliedEffect::InsightGranted(id.clone(), *amount));
850 }
851 DialogueEffect::EndDialogue => {
852 applied.push(AppliedEffect::EndDialogue);
853 }
854 }
855 }
856
857 Ok(applied)
858 }
859
860 fn get_fronter_id(&self, system: &PluralSystem) -> Option<String> {
866 match &system.fronting {
867 FrontingState::Single(id) => Some(id.clone()),
868 FrontingState::Blended(ids) => ids.first().cloned(),
869 _ => None,
870 }
871 }
872}
873
874impl Default for DialogueManager {
875 fn default() -> Self {
876 Self::new()
877 }
878}
879
880#[derive(Debug, Clone)]
886pub struct ResolvedDialogue {
887 pub node_id: String,
889 pub text: String,
891 pub responses: Vec<ResolvedResponse>,
893 pub speaker: ResolvedSpeaker,
895 pub voice_cues: Vec<String>,
897 pub animations: Vec<String>,
899}
900
901#[derive(Debug, Clone)]
903pub struct ResolvedResponse {
904 pub id: String,
906 pub text: String,
908 pub internal: bool,
910}
911
912#[derive(Debug, Clone)]
914pub struct ResolvedSpeaker {
915 pub name: String,
917 pub portrait: String,
919}
920
921#[derive(Debug)]
923pub enum DialogueResult {
924 Continue(Vec<AppliedEffect>),
926 AwaitingChoice(Vec<AppliedEffect>),
928 Ended,
930}
931
932#[derive(Debug, Clone)]
934pub enum AppliedEffect {
935 VariableSet(String),
936 FlagSet(String),
937 FlagCleared(String),
938 AnimaModified,
939 StabilityModified,
940 DissociationModified,
941 SwitchRequested(String),
942 TriggerActivated(String),
943 ItemGiven(String),
944 ItemTaken(String),
945 AbilityUnlocked(String),
946 RealityShifted(RealityLayer),
947 SoundPlayed(String),
948 CutsceneStarted(String),
949 InsightGranted(String, f32),
950 EndDialogue,
951}
952
953#[derive(Debug)]
959pub enum DialogueError {
960 TreeNotFound(String),
962 NoActiveDialogue,
964 NodeNotFound(String),
966 ResponseNotFound(String),
968 ConditionsNotMet,
970}
971
972impl std::fmt::Display for DialogueError {
973 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
974 match self {
975 DialogueError::TreeNotFound(id) => write!(f, "Dialogue tree not found: {}", id),
976 DialogueError::NoActiveDialogue => write!(f, "No active dialogue"),
977 DialogueError::NodeNotFound(id) => write!(f, "Dialogue node not found: {}", id),
978 DialogueError::ResponseNotFound(id) => write!(f, "Response not found: {}", id),
979 DialogueError::ConditionsNotMet => write!(f, "Conditions not met for this action"),
980 }
981 }
982}
983
984impl std::error::Error for DialogueError {}
985
986pub struct DialogueTreeBuilder {
992 id: String,
993 nodes: HashMap<String, DialogueNode>,
994 entry_node: Option<String>,
995 speaker: Option<SpeakerInfo>,
996}
997
998impl DialogueTreeBuilder {
999 pub fn new(id: &str) -> Self {
1001 Self {
1002 id: id.to_string(),
1003 nodes: HashMap::new(),
1004 entry_node: None,
1005 speaker: None,
1006 }
1007 }
1008
1009 pub fn entry(mut self, node_id: &str) -> Self {
1011 self.entry_node = Some(node_id.to_string());
1012 self
1013 }
1014
1015 pub fn speaker(mut self, speaker: SpeakerInfo) -> Self {
1017 self.speaker = Some(speaker);
1018 self
1019 }
1020
1021 pub fn node(mut self, node: DialogueNode) -> Self {
1023 self.nodes.insert(node.id.clone(), node);
1024 self
1025 }
1026
1027 pub fn build(self) -> Result<DialogueTree, String> {
1029 let entry_node = self.entry_node.ok_or("No entry node specified")?;
1030 let speaker = self.speaker.ok_or("No speaker specified")?;
1031
1032 if !self.nodes.contains_key(&entry_node) {
1033 return Err(format!("Entry node '{}' not found in nodes", entry_node));
1034 }
1035
1036 Ok(DialogueTree {
1037 id: self.id,
1038 nodes: self.nodes,
1039 entry_node,
1040 variables: HashMap::new(),
1041 speaker,
1042 })
1043 }
1044}
1045
1046pub struct DialogueNodeBuilder {
1048 id: String,
1049 base_text: String,
1050 alter_variations: HashMap<String, AlterDialogueVariation>,
1051 layer_variations: HashMap<RealityLayer, String>,
1052 emotional_variations: Vec<EmotionalVariation>,
1053 conditions: Vec<DialogueCondition>,
1054 effects: Vec<DialogueEffect>,
1055 responses: Vec<DialogueResponse>,
1056 next: Option<String>,
1057 voice_cues: Vec<String>,
1058 animations: Vec<String>,
1059 tags: Vec<String>,
1060}
1061
1062impl DialogueNodeBuilder {
1063 pub fn new(id: &str, text: &str) -> Self {
1065 Self {
1066 id: id.to_string(),
1067 base_text: text.to_string(),
1068 alter_variations: HashMap::new(),
1069 layer_variations: HashMap::new(),
1070 emotional_variations: Vec::new(),
1071 conditions: Vec::new(),
1072 effects: Vec::new(),
1073 responses: Vec::new(),
1074 next: None,
1075 voice_cues: Vec::new(),
1076 animations: Vec::new(),
1077 tags: Vec::new(),
1078 }
1079 }
1080
1081 pub fn alter_variation(mut self, alter_id: &str, text: &str, tone: DialogueTone) -> Self {
1083 self.alter_variations.insert(
1084 alter_id.to_string(),
1085 AlterDialogueVariation {
1086 text: text.to_string(),
1087 observations: Vec::new(),
1088 tone,
1089 recognition: None,
1090 },
1091 );
1092 self
1093 }
1094
1095 pub fn layer_variation(mut self, layer: RealityLayer, text: &str) -> Self {
1097 self.layer_variations.insert(layer, text.to_string());
1098 self
1099 }
1100
1101 pub fn condition(mut self, condition: DialogueCondition) -> Self {
1103 self.conditions.push(condition);
1104 self
1105 }
1106
1107 pub fn effect(mut self, effect: DialogueEffect) -> Self {
1109 self.effects.push(effect);
1110 self
1111 }
1112
1113 pub fn response(mut self, response: DialogueResponse) -> Self {
1115 self.responses.push(response);
1116 self
1117 }
1118
1119 pub fn next(mut self, node_id: &str) -> Self {
1121 self.next = Some(node_id.to_string());
1122 self
1123 }
1124
1125 pub fn voice_cue(mut self, cue: &str) -> Self {
1127 self.voice_cues.push(cue.to_string());
1128 self
1129 }
1130
1131 pub fn animation(mut self, anim: &str) -> Self {
1133 self.animations.push(anim.to_string());
1134 self
1135 }
1136
1137 pub fn tag(mut self, tag: &str) -> Self {
1139 self.tags.push(tag.to_string());
1140 self
1141 }
1142
1143 pub fn build(self) -> DialogueNode {
1145 DialogueNode {
1146 id: self.id,
1147 content: DialogueContent {
1148 base_text: self.base_text,
1149 alter_variations: self.alter_variations,
1150 layer_variations: self.layer_variations,
1151 emotional_variations: self.emotional_variations,
1152 voice_cues: self.voice_cues,
1153 animations: self.animations,
1154 },
1155 conditions: self.conditions,
1156 effects: self.effects,
1157 responses: self.responses,
1158 next: self.next,
1159 tags: self.tags,
1160 }
1161 }
1162}
1163
1164#[cfg(test)]
1169mod tests {
1170 use super::*;
1171
1172 fn create_test_system() -> PluralSystem {
1173 let mut system = PluralSystem::new("Test System");
1174 system.add_alter(super::super::runtime::Alter {
1175 id: "host".to_string(),
1176 name: "Host".to_string(),
1177 category: AlterCategory::Council,
1178 state: AlterPresenceState::Fronting,
1179 anima: AnimaState::default(),
1180 base_arousal: 0.0,
1181 base_dominance: 0.0,
1182 time_since_front: 0,
1183 triggers: Vec::new(),
1184 abilities: std::collections::HashSet::from(["analysis".to_string()]),
1185 preferred_reality: RealityLayer::Grounded,
1186 memory_access: super::super::runtime::MemoryAccess::Full,
1187 });
1188 system.fronting = FrontingState::Single("host".to_string());
1189 system
1190 }
1191
1192 fn create_test_tree() -> DialogueTree {
1193 let speaker = SpeakerInfo {
1194 id: "marcus".to_string(),
1195 name: "Father Marcus".to_string(),
1196 portrait: "marcus_default".to_string(),
1197 alter_portraits: HashMap::new(),
1198 layer_portraits: HashMap::new(),
1199 };
1200
1201 let node1 = DialogueNodeBuilder::new("start", "Greetings, traveler.")
1202 .alter_variation(
1203 "host",
1204 "Welcome, child. I sense... complexity within you.",
1205 DialogueTone::Warm,
1206 )
1207 .response(DialogueResponse {
1208 id: "ask_help".to_string(),
1209 text: "I need your help.".to_string(),
1210 alter_variations: HashMap::new(),
1211 conditions: Vec::new(),
1212 target_node: "help_offered".to_string(),
1213 effects: Vec::new(),
1214 internal: false,
1215 required_traits: Vec::new(),
1216 forbidden_traits: Vec::new(),
1217 })
1218 .response(DialogueResponse {
1219 id: "analyze".to_string(),
1220 text: "[Analyze] Something seems off about you...".to_string(),
1221 alter_variations: HashMap::new(),
1222 conditions: vec![DialogueCondition::AlterHasTrait("analysis".to_string())],
1223 target_node: "analysis_result".to_string(),
1224 effects: Vec::new(),
1225 internal: false,
1226 required_traits: vec!["analysis".to_string()],
1227 forbidden_traits: Vec::new(),
1228 })
1229 .build();
1230
1231 let node2 = DialogueNodeBuilder::new("help_offered", "How may I assist you?")
1232 .effect(DialogueEffect::EndDialogue)
1233 .build();
1234
1235 let node3 =
1236 DialogueNodeBuilder::new("analysis_result", "You... you can see it, can't you?")
1237 .layer_variation(
1238 RealityLayer::Fractured,
1239 "The priest's form wavers. Behind his smile, shadow teeth.",
1240 )
1241 .effect(DialogueEffect::ModifyAnima {
1242 pleasure: -0.1,
1243 arousal: 0.2,
1244 dominance: 0.0,
1245 })
1246 .effect(DialogueEffect::EndDialogue)
1247 .build();
1248
1249 DialogueTreeBuilder::new("marcus_greeting")
1250 .entry("start")
1251 .speaker(speaker)
1252 .node(node1)
1253 .node(node2)
1254 .node(node3)
1255 .build()
1256 .unwrap()
1257 }
1258
1259 #[test]
1260 fn test_dialogue_manager_creation() {
1261 let manager = DialogueManager::new();
1262 assert!(!manager.is_active());
1263 }
1264
1265 #[test]
1266 fn test_start_dialogue() {
1267 let mut manager = DialogueManager::new();
1268 manager.load_tree(create_test_tree());
1269
1270 let result = manager.start_dialogue("marcus_greeting");
1271 assert!(result.is_ok());
1272 assert!(manager.is_active());
1273 }
1274
1275 #[test]
1276 fn test_get_current_content() {
1277 let mut manager = DialogueManager::new();
1278 manager.load_tree(create_test_tree());
1279 manager.start_dialogue("marcus_greeting").unwrap();
1280
1281 let system = create_test_system();
1282 let content = manager.get_current_content(&system).unwrap();
1283
1284 assert!(content.text.contains("Welcome, child"));
1286 assert_eq!(content.responses.len(), 2);
1287 }
1288
1289 #[test]
1290 fn test_select_response() {
1291 let mut manager = DialogueManager::new();
1292 manager.load_tree(create_test_tree());
1293 manager.start_dialogue("marcus_greeting").unwrap();
1294
1295 let mut system = create_test_system();
1296 let result = manager.select_response("ask_help", &mut system);
1298 assert!(matches!(result, Ok(DialogueResult::Continue(_))));
1299
1300 let result = manager.advance(&mut system);
1302 assert!(matches!(result, Ok(DialogueResult::Ended)));
1303 }
1304
1305 #[test]
1306 fn test_condition_checking() {
1307 let manager = DialogueManager::new();
1308 let system = create_test_system();
1309
1310 let condition = DialogueCondition::AlterFronting("host".to_string());
1312 assert!(manager.check_condition(&condition, &system));
1313
1314 let condition = DialogueCondition::AlterHasTrait("analysis".to_string());
1316 assert!(manager.check_condition(&condition, &system));
1317
1318 let condition = DialogueCondition::RealityLayer(RealityLayer::Grounded);
1320 assert!(manager.check_condition(&condition, &system));
1321 }
1322
1323 #[test]
1324 fn test_trait_filtered_responses() {
1325 let mut manager = DialogueManager::new();
1326 manager.load_tree(create_test_tree());
1327 manager.start_dialogue("marcus_greeting").unwrap();
1328
1329 let system = create_test_system();
1330 let content = manager.get_current_content(&system).unwrap();
1331
1332 let has_analyze = content.responses.iter().any(|r| r.id == "analyze");
1334 assert!(has_analyze);
1335 }
1336}