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,
10    PluralSystem, RealityLayer,
11};
12
13// ============================================================================
14// DIALOGUE TREE STRUCTURE
15// ============================================================================
16
17/// A complete dialogue tree for an NPC or interaction
18#[derive(Debug, Clone)]
19pub struct DialogueTree {
20    /// Unique identifier
21    pub id: String,
22    /// All nodes in the tree
23    pub nodes: HashMap<String, DialogueNode>,
24    /// Entry point node
25    pub entry_node: String,
26    /// Variables set during dialogue
27    pub variables: HashMap<String, DialogueValue>,
28    /// Speaker information
29    pub speaker: SpeakerInfo,
30}
31
32/// A single node in the dialogue tree
33#[derive(Debug, Clone)]
34pub struct DialogueNode {
35    /// Node identifier
36    pub id: String,
37    /// The content of this node
38    pub content: DialogueContent,
39    /// Conditions for this node to be valid
40    pub conditions: Vec<DialogueCondition>,
41    /// Effects that trigger when this node is visited
42    pub effects: Vec<DialogueEffect>,
43    /// Possible responses/choices
44    pub responses: Vec<DialogueResponse>,
45    /// Next node if no responses (linear flow)
46    pub next: Option<String>,
47    /// Tags for filtering/searching
48    pub tags: Vec<String>,
49}
50
51/// Content displayed in a dialogue node
52#[derive(Debug, Clone)]
53pub struct DialogueContent {
54    /// Base text (fallback)
55    pub base_text: String,
56    /// Alter-specific text variations
57    pub alter_variations: HashMap<String, AlterDialogueVariation>,
58    /// Reality layer variations
59    pub layer_variations: HashMap<RealityLayer, String>,
60    /// Emotional state variations
61    pub emotional_variations: Vec<EmotionalVariation>,
62    /// Voice/expression cues
63    pub voice_cues: Vec<String>,
64    /// Animation triggers
65    pub animations: Vec<String>,
66}
67
68/// Variation of dialogue for a specific alter
69#[derive(Debug, Clone)]
70pub struct AlterDialogueVariation {
71    /// The text for this alter
72    pub text: String,
73    /// Unique observations this alter would make
74    pub observations: Vec<String>,
75    /// Emotional coloring
76    pub tone: DialogueTone,
77    /// Whether this alter recognizes something others wouldn't
78    pub recognition: Option<RecognitionEvent>,
79}
80
81/// Emotional variation based on system state
82#[derive(Debug, Clone)]
83pub struct EmotionalVariation {
84    /// Condition for this variation
85    pub condition: EmotionalCondition,
86    /// Text when condition is met
87    pub text: String,
88}
89
90/// Emotional conditions for dialogue variation
91#[derive(Debug, Clone)]
92pub enum EmotionalCondition {
93    /// High dissociation
94    HighDissociation(f32),
95    /// Low stability
96    LowStability(f32),
97    /// High arousal
98    HighArousal(f32),
99    /// Specific anima state
100    AnimaState { pleasure: (f32, f32), arousal: (f32, f32), dominance: (f32, f32) },
101    /// Recently triggered
102    RecentTrigger(String),
103}
104
105/// Tone of dialogue
106#[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/// Recognition event when an alter sees something familiar
121#[derive(Debug, Clone)]
122pub struct RecognitionEvent {
123    /// What is recognized
124    pub target: String,
125    /// Type of recognition
126    pub recognition_type: RecognitionType,
127    /// Intensity (0-1)
128    pub intensity: f32,
129}
130
131/// Types of recognition
132#[derive(Debug, Clone)]
133pub enum RecognitionType {
134    /// Recognizes an abuser's traits
135    Abuser,
136    /// Recognizes a safe person's traits
137    SafePerson,
138    /// Recognizes a place from memory
139    Place,
140    /// Recognizes an object from trauma
141    TraumaObject,
142    /// Generic familiarity
143    Familiar,
144}
145
146// ============================================================================
147// DIALOGUE RESPONSES
148// ============================================================================
149
150/// A response option in dialogue
151#[derive(Debug, Clone)]
152pub struct DialogueResponse {
153    /// Response identifier
154    pub id: String,
155    /// Displayed text
156    pub text: String,
157    /// Alter-specific response variations
158    pub alter_variations: HashMap<String, String>,
159    /// Conditions to show this response
160    pub conditions: Vec<DialogueCondition>,
161    /// Target node when selected
162    pub target_node: String,
163    /// Effects when selected
164    pub effects: Vec<DialogueEffect>,
165    /// Whether this is a "system" response (internal)
166    pub internal: bool,
167    /// Required alter traits to show
168    pub required_traits: Vec<String>,
169    /// Forbidden alter traits (hide if present)
170    pub forbidden_traits: Vec<String>,
171}
172
173// ============================================================================
174// CONDITIONS AND EFFECTS
175// ============================================================================
176
177/// Conditions for dialogue options
178#[derive(Debug, Clone)]
179pub enum DialogueCondition {
180    /// Specific alter is fronting
181    AlterFronting(String),
182    /// Alter category is fronting
183    CategoryFronting(AlterCategory),
184    /// Specific alter is co-conscious
185    AlterCoConscious(String),
186    /// Reality layer requirement
187    RealityLayer(RealityLayer),
188    /// System stability threshold
189    StabilityAbove(f32),
190    /// Dissociation threshold
191    DissociationBelow(f32),
192    /// Dialogue variable check
193    Variable { name: String, op: CompareOp, value: DialogueValue },
194    /// Flag is set
195    FlagSet(String),
196    /// Alter has trait
197    AlterHasTrait(String),
198    /// Anima state in range
199    AnimaInRange { pleasure: (f32, f32), arousal: (f32, f32), dominance: (f32, f32) },
200    /// Item in inventory
201    HasItem(String),
202    /// Ability unlocked
203    HasAbility(String),
204    /// Previous dialogue node visited
205    NodeVisited(String),
206    /// All conditions must be true
207    All(Vec<DialogueCondition>),
208    /// Any condition must be true
209    Any(Vec<DialogueCondition>),
210    /// Invert condition
211    Not(Box<DialogueCondition>),
212}
213
214/// Comparison operators
215#[derive(Debug, Clone)]
216pub enum CompareOp {
217    Eq,
218    Ne,
219    Lt,
220    Le,
221    Gt,
222    Ge,
223}
224
225/// Dialogue variable values
226#[derive(Debug, Clone, PartialEq)]
227pub enum DialogueValue {
228    Bool(bool),
229    Int(i32),
230    Float(f32),
231    String(String),
232}
233
234/// Effects triggered by dialogue
235#[derive(Debug, Clone)]
236pub enum DialogueEffect {
237    /// Set a dialogue variable
238    SetVariable { name: String, value: DialogueValue },
239    /// Set a global flag
240    SetFlag(String),
241    /// Clear a flag
242    ClearFlag(String),
243    /// Modify anima state
244    ModifyAnima { pleasure: f32, arousal: f32, dominance: f32 },
245    /// Modify system stability
246    ModifyStability(f32),
247    /// Modify dissociation
248    ModifyDissociation(f32),
249    /// Trigger a switch request
250    RequestSwitch { alter_id: String, urgency: f32 },
251    /// Activate a trigger
252    ActivateTrigger(String),
253    /// Add item to inventory
254    GiveItem(String),
255    /// Remove item from inventory
256    TakeItem(String),
257    /// Unlock an ability
258    UnlockAbility(String),
259    /// Shift reality layer
260    ShiftReality { target: RealityLayer, rate: f32 },
261    /// Play sound
262    PlaySound(String),
263    /// Start cutscene
264    StartCutscene(String),
265    /// Grant experience/insight
266    GrantInsight { id: String, amount: f32 },
267    /// End dialogue
268    EndDialogue,
269}
270
271// ============================================================================
272// SPEAKER INFORMATION
273// ============================================================================
274
275/// Information about who is speaking
276#[derive(Debug, Clone)]
277pub struct SpeakerInfo {
278    /// Speaker identifier
279    pub id: String,
280    /// Display name
281    pub name: String,
282    /// Portrait/avatar
283    pub portrait: String,
284    /// Alter-specific portraits (if speaker appears differently)
285    pub alter_portraits: HashMap<String, String>,
286    /// Reality layer portraits
287    pub layer_portraits: HashMap<RealityLayer, String>,
288}
289
290// ============================================================================
291// DIALOGUE MANAGER
292// ============================================================================
293
294/// Manages active dialogue sessions
295pub struct DialogueManager {
296    /// Currently active dialogue tree
297    pub current_tree: Option<DialogueTree>,
298    /// Current node ID
299    pub current_node: Option<String>,
300    /// History of visited nodes
301    pub visited_nodes: Vec<String>,
302    /// Dialogue variables
303    pub variables: HashMap<String, DialogueValue>,
304    /// Global flags
305    pub flags: HashMap<String, bool>,
306    /// Loaded dialogue trees
307    trees: HashMap<String, DialogueTree>,
308}
309
310impl DialogueManager {
311    /// Create a new dialogue manager
312    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    /// Load a dialogue tree
324    pub fn load_tree(&mut self, tree: DialogueTree) {
325        self.trees.insert(tree.id.clone(), tree);
326    }
327
328    /// Start a dialogue
329    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    /// Get the current dialogue content, adapted for the system state
342    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        // Resolve the content based on system state
353        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    /// Select a response and advance the dialogue
368    pub fn select_response(&mut self, response_id: &str, system: &mut PluralSystem) -> Result<DialogueResult, DialogueError> {
369        // Extract data we need before mutable borrow
370        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            // Find the response
381            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        // Check conditions
394        if !self.check_conditions(&response_conditions, system) {
395            return Err(DialogueError::ConditionsNotMet);
396        }
397
398        // Apply effects
399        let effects = self.apply_effects(&response_effects, system)?;
400
401        // Track visited
402        self.visited_nodes.push(node_id_clone);
403
404        // Check for dialogue end
405        if effects.iter().any(|e| matches!(e, AppliedEffect::EndDialogue)) {
406            self.end_dialogue();
407            return Ok(DialogueResult::Ended);
408        }
409
410        // Move to next node
411        self.current_node = Some(response_target);
412
413        Ok(DialogueResult::Continue(effects))
414    }
415
416    /// Advance to next node (for linear dialogue)
417    pub fn advance(&mut self, system: &mut PluralSystem) -> Result<DialogueResult, DialogueError> {
418        // Extract data we need before mutable borrow
419        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        // Apply node effects
438        let effects = self.apply_effects(&node_effects, system)?;
439
440        // Track visited
441        self.visited_nodes.push(node_id_clone);
442
443        // Check for dialogue end
444        if effects.iter().any(|e| matches!(e, AppliedEffect::EndDialogue)) {
445            self.end_dialogue();
446            return Ok(DialogueResult::Ended);
447        }
448
449        // Move to next node
450        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    /// End the current dialogue
464    pub fn end_dialogue(&mut self) {
465        self.current_tree = None;
466        self.current_node = None;
467    }
468
469    /// Check if dialogue is active
470    pub fn is_active(&self) -> bool {
471        self.current_tree.is_some()
472    }
473
474    // ========================================================================
475    // CONTENT RESOLUTION
476    // ========================================================================
477
478    /// Resolve dialogue content based on system state
479    fn resolve_content(&self, content: &DialogueContent, system: &PluralSystem, _speaker: &SpeakerInfo) -> String {
480        // Check for alter-specific variation
481        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        // Check for reality layer variation
488        if let Some(layer_text) = content.layer_variations.get(&system.reality_layer) {
489            return layer_text.clone();
490        }
491
492        // Check for emotional variations
493        for variation in &content.emotional_variations {
494            if self.check_emotional_condition(&variation.condition, system) {
495                return variation.text.clone();
496            }
497        }
498
499        // Default to base text
500        content.base_text.clone()
501    }
502
503    /// Resolve available responses based on system state
504    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                // Get alter-specific text if available
512                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    /// Resolve speaker appearance based on system state
527    fn resolve_speaker(&self, speaker: &SpeakerInfo, system: &PluralSystem) -> ResolvedSpeaker {
528        // Check for alter-specific portrait
529        let portrait = self.get_fronter_id(system)
530            .and_then(|id| speaker.alter_portraits.get(&id))
531            .cloned()
532            // Check for reality layer portrait
533            .or_else(|| speaker.layer_portraits.get(&system.reality_layer).cloned())
534            // Default to base portrait
535            .unwrap_or_else(|| speaker.portrait.clone());
536
537        ResolvedSpeaker {
538            name: speaker.name.clone(),
539            portrait,
540        }
541    }
542
543    // ========================================================================
544    // CONDITION CHECKING
545    // ========================================================================
546
547    /// Check all conditions
548    fn check_conditions(&self, conditions: &[DialogueCondition], system: &PluralSystem) -> bool {
549        conditions.iter().all(|c| self.check_condition(c, system))
550    }
551
552    /// Check a single condition
553    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                // Would check inventory - simplified for now
606                true
607            }
608            DialogueCondition::HasAbility(_ability_id) => {
609                // Would check abilities - simplified for now
610                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    /// Check trait requirements for a response
628    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        // Check required traits
640        let has_required = response.required_traits.iter()
641            .all(|t| alter.abilities.contains(t));
642
643        // Check forbidden traits
644        let has_forbidden = response.forbidden_traits.iter()
645            .any(|t| alter.abilities.contains(t));
646
647        has_required && !has_forbidden
648    }
649
650    /// Check emotional condition
651    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    /// Compare dialogue values
675    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    // ========================================================================
708    // EFFECT APPLICATION
709    // ========================================================================
710
711    /// Apply dialogue effects
712    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                    // Would create and add trigger
749                    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                    // Would begin reality transition
762                    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    // ========================================================================
783    // HELPERS
784    // ========================================================================
785
786    /// Get the currently fronting alter's ID
787    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// ============================================================================
803// RESULT TYPES
804// ============================================================================
805
806/// Resolved dialogue for display
807#[derive(Debug, Clone)]
808pub struct ResolvedDialogue {
809    /// Current node ID
810    pub node_id: String,
811    /// Resolved text to display
812    pub text: String,
813    /// Available responses
814    pub responses: Vec<ResolvedResponse>,
815    /// Speaker information
816    pub speaker: ResolvedSpeaker,
817    /// Voice cues for audio
818    pub voice_cues: Vec<String>,
819    /// Animations to trigger
820    pub animations: Vec<String>,
821}
822
823/// Resolved response option
824#[derive(Debug, Clone)]
825pub struct ResolvedResponse {
826    /// Response ID
827    pub id: String,
828    /// Display text
829    pub text: String,
830    /// Whether this is internal (system-only)
831    pub internal: bool,
832}
833
834/// Resolved speaker info
835#[derive(Debug, Clone)]
836pub struct ResolvedSpeaker {
837    /// Display name
838    pub name: String,
839    /// Portrait to show
840    pub portrait: String,
841}
842
843/// Result of dialogue action
844#[derive(Debug)]
845pub enum DialogueResult {
846    /// Continue to next node
847    Continue(Vec<AppliedEffect>),
848    /// Awaiting player choice
849    AwaitingChoice(Vec<AppliedEffect>),
850    /// Dialogue ended
851    Ended,
852}
853
854/// Applied effect for tracking
855#[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// ============================================================================
876// ERRORS
877// ============================================================================
878
879/// Dialogue system errors
880#[derive(Debug)]
881pub enum DialogueError {
882    /// Tree not found
883    TreeNotFound(String),
884    /// No active dialogue
885    NoActiveDialogue,
886    /// Node not found
887    NodeNotFound(String),
888    /// Response not found
889    ResponseNotFound(String),
890    /// Conditions not met for action
891    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
908// ============================================================================
909// BUILDERS
910// ============================================================================
911
912/// Builder for creating dialogue trees
913pub struct DialogueTreeBuilder {
914    id: String,
915    nodes: HashMap<String, DialogueNode>,
916    entry_node: Option<String>,
917    speaker: Option<SpeakerInfo>,
918}
919
920impl DialogueTreeBuilder {
921    /// Create a new builder
922    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    /// Set the entry node
932    pub fn entry(mut self, node_id: &str) -> Self {
933        self.entry_node = Some(node_id.to_string());
934        self
935    }
936
937    /// Set the speaker
938    pub fn speaker(mut self, speaker: SpeakerInfo) -> Self {
939        self.speaker = Some(speaker);
940        self
941    }
942
943    /// Add a node
944    pub fn node(mut self, node: DialogueNode) -> Self {
945        self.nodes.insert(node.id.clone(), node);
946        self
947    }
948
949    /// Build the dialogue tree
950    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
968/// Builder for creating dialogue nodes
969pub 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    /// Create a new builder
986    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    /// Add an alter-specific variation
1004    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    /// Add a reality layer variation
1015    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    /// Add a condition
1021    pub fn condition(mut self, condition: DialogueCondition) -> Self {
1022        self.conditions.push(condition);
1023        self
1024    }
1025
1026    /// Add an effect
1027    pub fn effect(mut self, effect: DialogueEffect) -> Self {
1028        self.effects.push(effect);
1029        self
1030    }
1031
1032    /// Add a response
1033    pub fn response(mut self, response: DialogueResponse) -> Self {
1034        self.responses.push(response);
1035        self
1036    }
1037
1038    /// Set next node for linear flow
1039    pub fn next(mut self, node_id: &str) -> Self {
1040        self.next = Some(node_id.to_string());
1041        self
1042    }
1043
1044    /// Add a voice cue
1045    pub fn voice_cue(mut self, cue: &str) -> Self {
1046        self.voice_cues.push(cue.to_string());
1047        self
1048    }
1049
1050    /// Add an animation
1051    pub fn animation(mut self, anim: &str) -> Self {
1052        self.animations.push(anim.to_string());
1053        self
1054    }
1055
1056    /// Add a tag
1057    pub fn tag(mut self, tag: &str) -> Self {
1058        self.tags.push(tag.to_string());
1059        self
1060    }
1061
1062    /// Build the node
1063    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// ============================================================================
1084// TESTS
1085// ============================================================================
1086
1087#[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        // Should get alter variation since host is fronting
1192        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        // Selecting response moves to target node
1204        let result = manager.select_response("ask_help", &mut system);
1205        assert!(matches!(result, Ok(DialogueResult::Continue(_))));
1206
1207        // Advancing executes the target node's effects (EndDialogue)
1208        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        // Test alter fronting condition
1218        let condition = DialogueCondition::AlterFronting("host".to_string());
1219        assert!(manager.check_condition(&condition, &system));
1220
1221        // Test alter has trait condition
1222        let condition = DialogueCondition::AlterHasTrait("analysis".to_string());
1223        assert!(manager.check_condition(&condition, &system));
1224
1225        // Test reality layer condition
1226        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        // Should see the analyze response since host has "analysis" trait
1240        let has_analyze = content.responses.iter().any(|r| r.id == "analyze");
1241        assert!(has_analyze);
1242    }
1243}