sigil_parser/plurality/
dialogue.rs

1//! # Dialogue System for DAEMONIORUM
2//!
3//! An alter-aware dialogue system that adapts responses based on
4//! which alter is fronting, their relationships, and system state.
5
6use std::collections::HashMap;
7
8use super::runtime::{
9    AlterCategory, AlterPresenceState, AnimaState, FrontingState, PluralSystem, RealityLayer,
10};
11
12// ============================================================================
13// DIALOGUE TREE STRUCTURE
14// ============================================================================
15
16/// A complete dialogue tree for an NPC or interaction
17#[derive(Debug, Clone)]
18pub struct DialogueTree {
19    /// Unique identifier
20    pub id: String,
21    /// All nodes in the tree
22    pub nodes: HashMap<String, DialogueNode>,
23    /// Entry point node
24    pub entry_node: String,
25    /// Variables set during dialogue
26    pub variables: HashMap<String, DialogueValue>,
27    /// Speaker information
28    pub speaker: SpeakerInfo,
29}
30
31/// A single node in the dialogue tree
32#[derive(Debug, Clone)]
33pub struct DialogueNode {
34    /// Node identifier
35    pub id: String,
36    /// The content of this node
37    pub content: DialogueContent,
38    /// Conditions for this node to be valid
39    pub conditions: Vec<DialogueCondition>,
40    /// Effects that trigger when this node is visited
41    pub effects: Vec<DialogueEffect>,
42    /// Possible responses/choices
43    pub responses: Vec<DialogueResponse>,
44    /// Next node if no responses (linear flow)
45    pub next: Option<String>,
46    /// Tags for filtering/searching
47    pub tags: Vec<String>,
48}
49
50/// Content displayed in a dialogue node
51#[derive(Debug, Clone)]
52pub struct DialogueContent {
53    /// Base text (fallback)
54    pub base_text: String,
55    /// Alter-specific text variations
56    pub alter_variations: HashMap<String, AlterDialogueVariation>,
57    /// Reality layer variations
58    pub layer_variations: HashMap<RealityLayer, String>,
59    /// Emotional state variations
60    pub emotional_variations: Vec<EmotionalVariation>,
61    /// Voice/expression cues
62    pub voice_cues: Vec<String>,
63    /// Animation triggers
64    pub animations: Vec<String>,
65}
66
67/// Variation of dialogue for a specific alter
68#[derive(Debug, Clone)]
69pub struct AlterDialogueVariation {
70    /// The text for this alter
71    pub text: String,
72    /// Unique observations this alter would make
73    pub observations: Vec<String>,
74    /// Emotional coloring
75    pub tone: DialogueTone,
76    /// Whether this alter recognizes something others wouldn't
77    pub recognition: Option<RecognitionEvent>,
78}
79
80/// Emotional variation based on system state
81#[derive(Debug, Clone)]
82pub struct EmotionalVariation {
83    /// Condition for this variation
84    pub condition: EmotionalCondition,
85    /// Text when condition is met
86    pub text: String,
87}
88
89/// Emotional conditions for dialogue variation
90#[derive(Debug, Clone)]
91pub enum EmotionalCondition {
92    /// High dissociation
93    HighDissociation(f32),
94    /// Low stability
95    LowStability(f32),
96    /// High arousal
97    HighArousal(f32),
98    /// Specific anima state
99    AnimaState {
100        pleasure: (f32, f32),
101        arousal: (f32, f32),
102        dominance: (f32, f32),
103    },
104    /// Recently triggered
105    RecentTrigger(String),
106}
107
108/// Tone of dialogue
109#[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/// Recognition event when an alter sees something familiar
124#[derive(Debug, Clone)]
125pub struct RecognitionEvent {
126    /// What is recognized
127    pub target: String,
128    /// Type of recognition
129    pub recognition_type: RecognitionType,
130    /// Intensity (0-1)
131    pub intensity: f32,
132}
133
134/// Types of recognition
135#[derive(Debug, Clone)]
136pub enum RecognitionType {
137    /// Recognizes an abuser's traits
138    Abuser,
139    /// Recognizes a safe person's traits
140    SafePerson,
141    /// Recognizes a place from memory
142    Place,
143    /// Recognizes an object from trauma
144    TraumaObject,
145    /// Generic familiarity
146    Familiar,
147}
148
149// ============================================================================
150// DIALOGUE RESPONSES
151// ============================================================================
152
153/// A response option in dialogue
154#[derive(Debug, Clone)]
155pub struct DialogueResponse {
156    /// Response identifier
157    pub id: String,
158    /// Displayed text
159    pub text: String,
160    /// Alter-specific response variations
161    pub alter_variations: HashMap<String, String>,
162    /// Conditions to show this response
163    pub conditions: Vec<DialogueCondition>,
164    /// Target node when selected
165    pub target_node: String,
166    /// Effects when selected
167    pub effects: Vec<DialogueEffect>,
168    /// Whether this is a "system" response (internal)
169    pub internal: bool,
170    /// Required alter traits to show
171    pub required_traits: Vec<String>,
172    /// Forbidden alter traits (hide if present)
173    pub forbidden_traits: Vec<String>,
174}
175
176// ============================================================================
177// CONDITIONS AND EFFECTS
178// ============================================================================
179
180/// Conditions for dialogue options
181#[derive(Debug, Clone)]
182pub enum DialogueCondition {
183    /// Specific alter is fronting
184    AlterFronting(String),
185    /// Alter category is fronting
186    CategoryFronting(AlterCategory),
187    /// Specific alter is co-conscious
188    AlterCoConscious(String),
189    /// Reality layer requirement
190    RealityLayer(RealityLayer),
191    /// System stability threshold
192    StabilityAbove(f32),
193    /// Dissociation threshold
194    DissociationBelow(f32),
195    /// Dialogue variable check
196    Variable {
197        name: String,
198        op: CompareOp,
199        value: DialogueValue,
200    },
201    /// Flag is set
202    FlagSet(String),
203    /// Alter has trait
204    AlterHasTrait(String),
205    /// Anima state in range
206    AnimaInRange {
207        pleasure: (f32, f32),
208        arousal: (f32, f32),
209        dominance: (f32, f32),
210    },
211    /// Item in inventory
212    HasItem(String),
213    /// Ability unlocked
214    HasAbility(String),
215    /// Previous dialogue node visited
216    NodeVisited(String),
217    /// All conditions must be true
218    All(Vec<DialogueCondition>),
219    /// Any condition must be true
220    Any(Vec<DialogueCondition>),
221    /// Invert condition
222    Not(Box<DialogueCondition>),
223}
224
225/// Comparison operators
226#[derive(Debug, Clone)]
227pub enum CompareOp {
228    Eq,
229    Ne,
230    Lt,
231    Le,
232    Gt,
233    Ge,
234}
235
236/// Dialogue variable values
237#[derive(Debug, Clone, PartialEq)]
238pub enum DialogueValue {
239    Bool(bool),
240    Int(i32),
241    Float(f32),
242    String(String),
243}
244
245/// Effects triggered by dialogue
246#[derive(Debug, Clone)]
247pub enum DialogueEffect {
248    /// Set a dialogue variable
249    SetVariable { name: String, value: DialogueValue },
250    /// Set a global flag
251    SetFlag(String),
252    /// Clear a flag
253    ClearFlag(String),
254    /// Modify anima state
255    ModifyAnima {
256        pleasure: f32,
257        arousal: f32,
258        dominance: f32,
259    },
260    /// Modify system stability
261    ModifyStability(f32),
262    /// Modify dissociation
263    ModifyDissociation(f32),
264    /// Trigger a switch request
265    RequestSwitch { alter_id: String, urgency: f32 },
266    /// Activate a trigger
267    ActivateTrigger(String),
268    /// Add item to inventory
269    GiveItem(String),
270    /// Remove item from inventory
271    TakeItem(String),
272    /// Unlock an ability
273    UnlockAbility(String),
274    /// Shift reality layer
275    ShiftReality { target: RealityLayer, rate: f32 },
276    /// Play sound
277    PlaySound(String),
278    /// Start cutscene
279    StartCutscene(String),
280    /// Grant experience/insight
281    GrantInsight { id: String, amount: f32 },
282    /// End dialogue
283    EndDialogue,
284}
285
286// ============================================================================
287// SPEAKER INFORMATION
288// ============================================================================
289
290/// Information about who is speaking
291#[derive(Debug, Clone)]
292pub struct SpeakerInfo {
293    /// Speaker identifier
294    pub id: String,
295    /// Display name
296    pub name: String,
297    /// Portrait/avatar
298    pub portrait: String,
299    /// Alter-specific portraits (if speaker appears differently)
300    pub alter_portraits: HashMap<String, String>,
301    /// Reality layer portraits
302    pub layer_portraits: HashMap<RealityLayer, String>,
303}
304
305// ============================================================================
306// DIALOGUE MANAGER
307// ============================================================================
308
309/// Manages active dialogue sessions
310pub struct DialogueManager {
311    /// Currently active dialogue tree
312    pub current_tree: Option<DialogueTree>,
313    /// Current node ID
314    pub current_node: Option<String>,
315    /// History of visited nodes
316    pub visited_nodes: Vec<String>,
317    /// Dialogue variables
318    pub variables: HashMap<String, DialogueValue>,
319    /// Global flags
320    pub flags: HashMap<String, bool>,
321    /// Loaded dialogue trees
322    trees: HashMap<String, DialogueTree>,
323}
324
325impl DialogueManager {
326    /// Create a new dialogue manager
327    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    /// Load a dialogue tree
339    pub fn load_tree(&mut self, tree: DialogueTree) {
340        self.trees.insert(tree.id.clone(), tree);
341    }
342
343    /// Start a dialogue
344    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    /// Get the current dialogue content, adapted for the system state
359    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        // Resolve the content based on system state
379        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    /// Select a response and advance the dialogue
394    pub fn select_response(
395        &mut self,
396        response_id: &str,
397        system: &mut PluralSystem,
398    ) -> Result<DialogueResult, DialogueError> {
399        // Extract data we need before mutable borrow
400        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            // Find the response
417            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        // Check conditions
432        if !self.check_conditions(&response_conditions, system) {
433            return Err(DialogueError::ConditionsNotMet);
434        }
435
436        // Apply effects
437        let effects = self.apply_effects(&response_effects, system)?;
438
439        // Track visited
440        self.visited_nodes.push(node_id_clone);
441
442        // Check for dialogue end
443        if effects
444            .iter()
445            .any(|e| matches!(e, AppliedEffect::EndDialogue))
446        {
447            self.end_dialogue();
448            return Ok(DialogueResult::Ended);
449        }
450
451        // Move to next node
452        self.current_node = Some(response_target);
453
454        Ok(DialogueResult::Continue(effects))
455    }
456
457    /// Advance to next node (for linear dialogue)
458    pub fn advance(&mut self, system: &mut PluralSystem) -> Result<DialogueResult, DialogueError> {
459        // Extract data we need before mutable borrow
460        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        // Apply node effects
485        let effects = self.apply_effects(&node_effects, system)?;
486
487        // Track visited
488        self.visited_nodes.push(node_id_clone);
489
490        // Check for dialogue end
491        if effects
492            .iter()
493            .any(|e| matches!(e, AppliedEffect::EndDialogue))
494        {
495            self.end_dialogue();
496            return Ok(DialogueResult::Ended);
497        }
498
499        // Move to next node
500        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    /// End the current dialogue
514    pub fn end_dialogue(&mut self) {
515        self.current_tree = None;
516        self.current_node = None;
517    }
518
519    /// Check if dialogue is active
520    pub fn is_active(&self) -> bool {
521        self.current_tree.is_some()
522    }
523
524    // ========================================================================
525    // CONTENT RESOLUTION
526    // ========================================================================
527
528    /// Resolve dialogue content based on system state
529    fn resolve_content(
530        &self,
531        content: &DialogueContent,
532        system: &PluralSystem,
533        _speaker: &SpeakerInfo,
534    ) -> String {
535        // Check for alter-specific variation
536        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        // Check for reality layer variation
543        if let Some(layer_text) = content.layer_variations.get(&system.reality_layer) {
544            return layer_text.clone();
545        }
546
547        // Check for emotional variations
548        for variation in &content.emotional_variations {
549            if self.check_emotional_condition(&variation.condition, system) {
550                return variation.text.clone();
551            }
552        }
553
554        // Default to base text
555        content.base_text.clone()
556    }
557
558    /// Resolve available responses based on system state
559    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                // Get alter-specific text if available
572                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    /// Resolve speaker appearance based on system state
588    fn resolve_speaker(&self, speaker: &SpeakerInfo, system: &PluralSystem) -> ResolvedSpeaker {
589        // Check for alter-specific portrait
590        let portrait = self
591            .get_fronter_id(system)
592            .and_then(|id| speaker.alter_portraits.get(&id))
593            .cloned()
594            // Check for reality layer portrait
595            .or_else(|| speaker.layer_portraits.get(&system.reality_layer).cloned())
596            // Default to base portrait
597            .unwrap_or_else(|| speaker.portrait.clone());
598
599        ResolvedSpeaker {
600            name: speaker.name.clone(),
601            portrait,
602        }
603    }
604
605    // ========================================================================
606    // CONDITION CHECKING
607    // ========================================================================
608
609    /// Check all conditions
610    fn check_conditions(&self, conditions: &[DialogueCondition], system: &PluralSystem) -> bool {
611        conditions.iter().all(|c| self.check_condition(c, system))
612    }
613
614    /// Check a single condition
615    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                // Would check inventory - simplified for now
671                true
672            }
673            DialogueCondition::HasAbility(_ability_id) => {
674                // Would check abilities - simplified for now
675                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    /// Check trait requirements for a response
689    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        // Check required traits
701        let has_required = response
702            .required_traits
703            .iter()
704            .all(|t| alter.abilities.contains(t));
705
706        // Check forbidden traits
707        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    /// Check emotional condition
716    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    /// Compare dialogue values
745    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    // ========================================================================
778    // EFFECT APPLICATION
779    // ========================================================================
780
781    /// Apply dialogue effects
782    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                    // Would create and add trigger
827                    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                    // Would begin reality transition
840                    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    // ========================================================================
861    // HELPERS
862    // ========================================================================
863
864    /// Get the currently fronting alter's ID
865    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// ============================================================================
881// RESULT TYPES
882// ============================================================================
883
884/// Resolved dialogue for display
885#[derive(Debug, Clone)]
886pub struct ResolvedDialogue {
887    /// Current node ID
888    pub node_id: String,
889    /// Resolved text to display
890    pub text: String,
891    /// Available responses
892    pub responses: Vec<ResolvedResponse>,
893    /// Speaker information
894    pub speaker: ResolvedSpeaker,
895    /// Voice cues for audio
896    pub voice_cues: Vec<String>,
897    /// Animations to trigger
898    pub animations: Vec<String>,
899}
900
901/// Resolved response option
902#[derive(Debug, Clone)]
903pub struct ResolvedResponse {
904    /// Response ID
905    pub id: String,
906    /// Display text
907    pub text: String,
908    /// Whether this is internal (system-only)
909    pub internal: bool,
910}
911
912/// Resolved speaker info
913#[derive(Debug, Clone)]
914pub struct ResolvedSpeaker {
915    /// Display name
916    pub name: String,
917    /// Portrait to show
918    pub portrait: String,
919}
920
921/// Result of dialogue action
922#[derive(Debug)]
923pub enum DialogueResult {
924    /// Continue to next node
925    Continue(Vec<AppliedEffect>),
926    /// Awaiting player choice
927    AwaitingChoice(Vec<AppliedEffect>),
928    /// Dialogue ended
929    Ended,
930}
931
932/// Applied effect for tracking
933#[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// ============================================================================
954// ERRORS
955// ============================================================================
956
957/// Dialogue system errors
958#[derive(Debug)]
959pub enum DialogueError {
960    /// Tree not found
961    TreeNotFound(String),
962    /// No active dialogue
963    NoActiveDialogue,
964    /// Node not found
965    NodeNotFound(String),
966    /// Response not found
967    ResponseNotFound(String),
968    /// Conditions not met for action
969    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
986// ============================================================================
987// BUILDERS
988// ============================================================================
989
990/// Builder for creating dialogue trees
991pub struct DialogueTreeBuilder {
992    id: String,
993    nodes: HashMap<String, DialogueNode>,
994    entry_node: Option<String>,
995    speaker: Option<SpeakerInfo>,
996}
997
998impl DialogueTreeBuilder {
999    /// Create a new builder
1000    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    /// Set the entry node
1010    pub fn entry(mut self, node_id: &str) -> Self {
1011        self.entry_node = Some(node_id.to_string());
1012        self
1013    }
1014
1015    /// Set the speaker
1016    pub fn speaker(mut self, speaker: SpeakerInfo) -> Self {
1017        self.speaker = Some(speaker);
1018        self
1019    }
1020
1021    /// Add a node
1022    pub fn node(mut self, node: DialogueNode) -> Self {
1023        self.nodes.insert(node.id.clone(), node);
1024        self
1025    }
1026
1027    /// Build the dialogue tree
1028    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
1046/// Builder for creating dialogue nodes
1047pub 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    /// Create a new builder
1064    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    /// Add an alter-specific variation
1082    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    /// Add a reality layer variation
1096    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    /// Add a condition
1102    pub fn condition(mut self, condition: DialogueCondition) -> Self {
1103        self.conditions.push(condition);
1104        self
1105    }
1106
1107    /// Add an effect
1108    pub fn effect(mut self, effect: DialogueEffect) -> Self {
1109        self.effects.push(effect);
1110        self
1111    }
1112
1113    /// Add a response
1114    pub fn response(mut self, response: DialogueResponse) -> Self {
1115        self.responses.push(response);
1116        self
1117    }
1118
1119    /// Set next node for linear flow
1120    pub fn next(mut self, node_id: &str) -> Self {
1121        self.next = Some(node_id.to_string());
1122        self
1123    }
1124
1125    /// Add a voice cue
1126    pub fn voice_cue(mut self, cue: &str) -> Self {
1127        self.voice_cues.push(cue.to_string());
1128        self
1129    }
1130
1131    /// Add an animation
1132    pub fn animation(mut self, anim: &str) -> Self {
1133        self.animations.push(anim.to_string());
1134        self
1135    }
1136
1137    /// Add a tag
1138    pub fn tag(mut self, tag: &str) -> Self {
1139        self.tags.push(tag.to_string());
1140        self
1141    }
1142
1143    /// Build the node
1144    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// ============================================================================
1165// TESTS
1166// ============================================================================
1167
1168#[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        // Should get alter variation since host is fronting
1285        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        // Selecting response moves to target node
1297        let result = manager.select_response("ask_help", &mut system);
1298        assert!(matches!(result, Ok(DialogueResult::Continue(_))));
1299
1300        // Advancing executes the target node's effects (EndDialogue)
1301        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        // Test alter fronting condition
1311        let condition = DialogueCondition::AlterFronting("host".to_string());
1312        assert!(manager.check_condition(&condition, &system));
1313
1314        // Test alter has trait condition
1315        let condition = DialogueCondition::AlterHasTrait("analysis".to_string());
1316        assert!(manager.check_condition(&condition, &system));
1317
1318        // Test reality layer condition
1319        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        // Should see the analyze response since host has "analysis" trait
1333        let has_analyze = content.responses.iter().any(|r| r.id == "analyze");
1334        assert!(has_analyze);
1335    }
1336}