Skip to main content

proof_engine/editor/
dialogue_editor.rs

1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6// ============================================================
7// SECTION 1: DIALOGUE NODE TYPES
8// ============================================================
9
10#[derive(Debug, Clone, PartialEq)]
11pub enum NodeType {
12    Start,
13    Speaker,
14    PlayerChoice,
15    Condition,
16    SetVariable,
17    TriggerEvent,
18    Jump,
19    End,
20    Random,
21    Timed,
22}
23
24#[derive(Debug, Clone)]
25pub struct NodeId(pub u64);
26
27impl NodeId {
28    pub fn new(id: u64) -> Self { NodeId(id) }
29    pub fn value(&self) -> u64 { self.0 }
30}
31
32impl PartialEq for NodeId {
33    fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
34}
35impl Eq for NodeId {}
36impl std::hash::Hash for NodeId {
37    fn hash<H: std::hash::Hasher>(&self, state: &mut H) { self.0.hash(state); }
38}
39
40#[derive(Debug, Clone)]
41pub struct NodeConnection {
42    pub from_node: u64,
43    pub from_port: usize,
44    pub to_node: u64,
45    pub to_port: usize,
46    pub label: Option<String>,
47}
48
49#[derive(Debug, Clone)]
50pub struct StartNode {
51    pub id: u64,
52    pub position: Vec2,
53    pub title: String,
54    pub conversation_id: String,
55    pub initial_variables: HashMap<String, DialogueVariable>,
56    pub entry_conditions: Vec<ConditionExpression>,
57    pub on_enter_events: Vec<String>,
58    pub auto_start: bool,
59    pub priority: i32,
60    pub tags: Vec<String>,
61    pub comment: String,
62    pub output_port: u64,
63}
64
65impl StartNode {
66    pub fn new(id: u64) -> Self {
67        StartNode {
68            id,
69            position: Vec2::ZERO,
70            title: format!("Start_{}", id),
71            conversation_id: format!("conv_{}", id),
72            initial_variables: HashMap::new(),
73            entry_conditions: Vec::new(),
74            on_enter_events: Vec::new(),
75            auto_start: false,
76            priority: 0,
77            tags: Vec::new(),
78            comment: String::new(),
79            output_port: 0,
80        }
81    }
82
83    pub fn evaluate_entry_conditions(&self, scope: &VariableScope) -> bool {
84        for cond in &self.entry_conditions {
85            if !cond.evaluate(scope) {
86                return false;
87            }
88        }
89        true
90    }
91}
92
93#[derive(Debug, Clone)]
94pub struct SpeakerNode {
95    pub id: u64,
96    pub position: Vec2,
97    pub speaker_id: String,
98    pub speaker_name: String,
99    pub dialogue_text: String,
100    pub localization_key: String,
101    pub voice_line_ref: Option<VoiceLineRef>,
102    pub emotion_state: EmotionState,
103    pub portrait_id: String,
104    pub animation_trigger: Option<String>,
105    pub camera_hint: Option<CameraHint>,
106    pub text_speed: f32,
107    pub auto_advance: bool,
108    pub auto_advance_delay: f32,
109    pub subtitle_timing: Vec<SubtitleCue>,
110    pub input_port: u64,
111    pub output_port: u64,
112    pub tags: Vec<String>,
113    pub comment: String,
114}
115
116impl SpeakerNode {
117    pub fn new(id: u64) -> Self {
118        SpeakerNode {
119            id,
120            position: Vec2::ZERO,
121            speaker_id: String::new(),
122            speaker_name: String::new(),
123            dialogue_text: String::new(),
124            localization_key: format!("dlg_{}_text", id),
125            voice_line_ref: None,
126            emotion_state: EmotionState::default(),
127            portrait_id: String::new(),
128            animation_trigger: None,
129            camera_hint: None,
130            text_speed: 1.0,
131            auto_advance: false,
132            auto_advance_delay: 2.0,
133            subtitle_timing: Vec::new(),
134            input_port: 0,
135            output_port: 1,
136            tags: Vec::new(),
137            comment: String::new(),
138        }
139    }
140
141    pub fn get_display_text(&self, locale: &str, loc_table: &LocalizationTable) -> String {
142        if let Some(text) = loc_table.get(&self.localization_key, locale) {
143            text
144        } else {
145            self.dialogue_text.clone()
146        }
147    }
148
149    pub fn calculate_read_time(&self) -> f32 {
150        let words = self.dialogue_text.split_whitespace().count();
151        let wpm = 200.0_f32;
152        (words as f32 / wpm) * 60.0
153    }
154
155    pub fn generate_subtitle_cues(&mut self) {
156        if let Some(voice) = &self.voice_line_ref {
157            let total_duration = voice.duration_secs;
158            let words: Vec<&str> = self.dialogue_text.split_whitespace().collect();
159            if words.is_empty() { return; }
160            let time_per_word = total_duration / words.len() as f32;
161            let mut cues = Vec::new();
162            let mut current_time = 0.0_f32;
163            let mut segment = String::new();
164            for (i, word) in words.iter().enumerate() {
165                segment.push_str(word);
166                segment.push(' ');
167                if (i + 1) % 8 == 0 || i == words.len() - 1 {
168                    let end_time = current_time + time_per_word * 8.0_f32.min((i + 1) as f32);
169                    cues.push(SubtitleCue {
170                        start_time: current_time,
171                        end_time: end_time.min(total_duration),
172                        text: segment.trim().to_string(),
173                    });
174                    current_time = end_time;
175                    segment.clear();
176                }
177            }
178            self.subtitle_timing = cues;
179        }
180    }
181}
182
183#[derive(Debug, Clone)]
184pub struct PlayerChoiceNode {
185    pub id: u64,
186    pub position: Vec2,
187    pub prompt_text: String,
188    pub prompt_localization_key: String,
189    pub choices: Vec<PlayerChoice>,
190    pub default_choice_index: Option<usize>,
191    pub timeout_secs: Option<f32>,
192    pub timeout_choice_index: Option<usize>,
193    pub shuffle_choices: bool,
194    pub filter_unavailable: bool,
195    pub input_port: u64,
196    pub tags: Vec<String>,
197    pub comment: String,
198}
199
200impl PlayerChoiceNode {
201    pub fn new(id: u64) -> Self {
202        PlayerChoiceNode {
203            id,
204            position: Vec2::ZERO,
205            prompt_text: String::new(),
206            prompt_localization_key: format!("dlg_{}_prompt", id),
207            choices: Vec::new(),
208            default_choice_index: None,
209            timeout_secs: None,
210            timeout_choice_index: None,
211            shuffle_choices: false,
212            filter_unavailable: false,
213            input_port: 0,
214            tags: Vec::new(),
215            comment: String::new(),
216        }
217    }
218
219    pub fn get_available_choices(&self, scope: &VariableScope) -> Vec<(usize, &PlayerChoice)> {
220        let mut available = Vec::new();
221        for (i, choice) in self.choices.iter().enumerate() {
222            if choice.is_available(scope) {
223                available.push((i, choice));
224            }
225        }
226        available
227    }
228
229    pub fn add_choice(&mut self, text: String, output_node: u64) -> usize {
230        let idx = self.choices.len();
231        self.choices.push(PlayerChoice {
232            text,
233            localization_key: format!("dlg_{}_choice_{}", self.id, idx),
234            conditions: Vec::new(),
235            hidden_when_unavailable: false,
236            output_port: idx,
237            output_node,
238            tags: Vec::new(),
239            tooltip: None,
240            icon: None,
241            one_time_only: false,
242            used: false,
243        });
244        idx
245    }
246}
247
248#[derive(Debug, Clone)]
249pub struct PlayerChoice {
250    pub text: String,
251    pub localization_key: String,
252    pub conditions: Vec<ConditionExpression>,
253    pub hidden_when_unavailable: bool,
254    pub output_port: usize,
255    pub output_node: u64,
256    pub tags: Vec<String>,
257    pub tooltip: Option<String>,
258    pub icon: Option<String>,
259    pub one_time_only: bool,
260    pub used: bool,
261}
262
263impl PlayerChoice {
264    pub fn is_available(&self, scope: &VariableScope) -> bool {
265        if self.one_time_only && self.used { return false; }
266        for cond in &self.conditions {
267            if !cond.evaluate(scope) { return false; }
268        }
269        true
270    }
271}
272
273#[derive(Debug, Clone)]
274pub struct ConditionNode {
275    pub id: u64,
276    pub position: Vec2,
277    pub condition: ConditionExpression,
278    pub true_output: u64,
279    pub false_output: u64,
280    pub input_port: u64,
281    pub comment: String,
282    pub tags: Vec<String>,
283}
284
285impl ConditionNode {
286    pub fn new(id: u64) -> Self {
287        ConditionNode {
288            id,
289            position: Vec2::ZERO,
290            condition: ConditionExpression::Literal(true),
291            true_output: 0,
292            false_output: 0,
293            input_port: 0,
294            comment: String::new(),
295            tags: Vec::new(),
296        }
297    }
298
299    pub fn evaluate(&self, scope: &VariableScope) -> u64 {
300        if self.condition.evaluate(scope) {
301            self.true_output
302        } else {
303            self.false_output
304        }
305    }
306}
307
308#[derive(Debug, Clone)]
309pub struct SetVariableNode {
310    pub id: u64,
311    pub position: Vec2,
312    pub operations: Vec<VariableOperation>,
313    pub input_port: u64,
314    pub output_port: u64,
315    pub comment: String,
316    pub tags: Vec<String>,
317}
318
319impl SetVariableNode {
320    pub fn new(id: u64) -> Self {
321        SetVariableNode {
322            id,
323            position: Vec2::ZERO,
324            operations: Vec::new(),
325            input_port: 0,
326            output_port: 1,
327            comment: String::new(),
328            tags: Vec::new(),
329        }
330    }
331
332    pub fn execute(&self, scope: &mut VariableScope) {
333        for op in &self.operations {
334            op.execute(scope);
335        }
336    }
337}
338
339#[derive(Debug, Clone)]
340pub struct VariableOperation {
341    pub variable_name: String,
342    pub scope_type: ScopeType,
343    pub operator: AssignOperator,
344    pub value: DialogueValue,
345}
346
347impl VariableOperation {
348    pub fn execute(&self, scope: &mut VariableScope) {
349        let current = scope.get(&self.variable_name, &self.scope_type);
350        let new_val = match &self.operator {
351            AssignOperator::Set => self.value.clone(),
352            AssignOperator::Add => {
353                match (&current, &self.value) {
354                    (Some(DialogueValue::Int(a)), DialogueValue::Int(b)) => DialogueValue::Int(a + b),
355                    (Some(DialogueValue::Float(a)), DialogueValue::Float(b)) => DialogueValue::Float(a + b),
356                    (Some(DialogueValue::String(a)), DialogueValue::String(b)) => DialogueValue::String(format!("{}{}", a, b)),
357                    _ => self.value.clone(),
358                }
359            }
360            AssignOperator::Subtract => {
361                match (&current, &self.value) {
362                    (Some(DialogueValue::Int(a)), DialogueValue::Int(b)) => DialogueValue::Int(a - b),
363                    (Some(DialogueValue::Float(a)), DialogueValue::Float(b)) => DialogueValue::Float(a - b),
364                    _ => self.value.clone(),
365                }
366            }
367            AssignOperator::Multiply => {
368                match (&current, &self.value) {
369                    (Some(DialogueValue::Int(a)), DialogueValue::Int(b)) => DialogueValue::Int(a * b),
370                    (Some(DialogueValue::Float(a)), DialogueValue::Float(b)) => DialogueValue::Float(a * b),
371                    _ => self.value.clone(),
372                }
373            }
374            AssignOperator::Divide => {
375                match (&current, &self.value) {
376                    (Some(DialogueValue::Int(a)), DialogueValue::Int(b)) => {
377                        if *b != 0 { DialogueValue::Int(a / b) } else { DialogueValue::Int(*a) }
378                    }
379                    (Some(DialogueValue::Float(a)), DialogueValue::Float(b)) => {
380                        if *b != 0.0 { DialogueValue::Float(a / b) } else { DialogueValue::Float(*a) }
381                    }
382                    _ => self.value.clone(),
383                }
384            }
385            AssignOperator::Toggle => {
386                match &current {
387                    Some(DialogueValue::Bool(b)) => DialogueValue::Bool(!b),
388                    _ => self.value.clone(),
389                }
390            }
391        };
392        scope.set(self.variable_name.clone(), self.scope_type.clone(), new_val);
393    }
394}
395
396#[derive(Debug, Clone)]
397pub enum AssignOperator {
398    Set,
399    Add,
400    Subtract,
401    Multiply,
402    Divide,
403    Toggle,
404}
405
406#[derive(Debug, Clone)]
407pub struct TriggerEventNode {
408    pub id: u64,
409    pub position: Vec2,
410    pub event_name: String,
411    pub event_parameters: HashMap<String, DialogueValue>,
412    pub delay_secs: f32,
413    pub input_port: u64,
414    pub output_port: u64,
415    pub comment: String,
416    pub tags: Vec<String>,
417}
418
419impl TriggerEventNode {
420    pub fn new(id: u64) -> Self {
421        TriggerEventNode {
422            id,
423            position: Vec2::ZERO,
424            event_name: String::new(),
425            event_parameters: HashMap::new(),
426            delay_secs: 0.0,
427            input_port: 0,
428            output_port: 1,
429            comment: String::new(),
430            tags: Vec::new(),
431        }
432    }
433
434    pub fn build_event(&self) -> GameEvent {
435        GameEvent {
436            name: self.event_name.clone(),
437            parameters: self.event_parameters.clone(),
438            delay: self.delay_secs,
439            source: format!("dialogue_node_{}", self.id),
440        }
441    }
442}
443
444#[derive(Debug, Clone)]
445pub struct GameEvent {
446    pub name: String,
447    pub parameters: HashMap<String, DialogueValue>,
448    pub delay: f32,
449    pub source: String,
450}
451
452#[derive(Debug, Clone)]
453pub struct JumpNode {
454    pub id: u64,
455    pub position: Vec2,
456    pub target_conversation_id: String,
457    pub target_node_id: u64,
458    pub preserve_variables: bool,
459    pub return_after: bool,
460    pub input_port: u64,
461    pub comment: String,
462    pub tags: Vec<String>,
463}
464
465impl JumpNode {
466    pub fn new(id: u64) -> Self {
467        JumpNode {
468            id,
469            position: Vec2::ZERO,
470            target_conversation_id: String::new(),
471            target_node_id: 0,
472            preserve_variables: true,
473            return_after: false,
474            input_port: 0,
475            comment: String::new(),
476            tags: Vec::new(),
477        }
478    }
479}
480
481#[derive(Debug, Clone)]
482pub struct EndNode {
483    pub id: u64,
484    pub position: Vec2,
485    pub end_type: EndType,
486    pub on_end_events: Vec<String>,
487    pub return_value: Option<DialogueValue>,
488    pub input_port: u64,
489    pub comment: String,
490    pub tags: Vec<String>,
491}
492
493#[derive(Debug, Clone, PartialEq)]
494pub enum EndType {
495    Normal,
496    Success,
497    Failure,
498    Interrupted,
499    Timeout,
500}
501
502impl EndNode {
503    pub fn new(id: u64) -> Self {
504        EndNode {
505            id,
506            position: Vec2::ZERO,
507            end_type: EndType::Normal,
508            on_end_events: Vec::new(),
509            return_value: None,
510            input_port: 0,
511            comment: String::new(),
512            tags: Vec::new(),
513        }
514    }
515}
516
517#[derive(Debug, Clone)]
518pub struct RandomNode {
519    pub id: u64,
520    pub position: Vec2,
521    pub outputs: Vec<RandomOutput>,
522    pub seed: Option<u64>,
523    pub use_weights: bool,
524    pub input_port: u64,
525    pub comment: String,
526    pub tags: Vec<String>,
527    rng_state: u64,
528}
529
530#[derive(Debug, Clone)]
531pub struct RandomOutput {
532    pub weight: f32,
533    pub output_node: u64,
534    pub label: String,
535}
536
537impl RandomNode {
538    pub fn new(id: u64) -> Self {
539        RandomNode {
540            id,
541            position: Vec2::ZERO,
542            outputs: Vec::new(),
543            seed: None,
544            use_weights: true,
545            input_port: 0,
546            comment: String::new(),
547            tags: Vec::new(),
548            rng_state: id.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407),
549        }
550    }
551
552    pub fn pick_output(&mut self) -> Option<u64> {
553        if self.outputs.is_empty() { return None; }
554        self.rng_state = self.rng_state
555            .wrapping_mul(6364136223846793005)
556            .wrapping_add(1442695040888963407);
557        let rand_float = (self.rng_state >> 33) as f32 / (u32::MAX as f32);
558        if self.use_weights {
559            let total_weight: f32 = self.outputs.iter().map(|o| o.weight).sum();
560            let mut cursor = rand_float * total_weight;
561            for output in &self.outputs {
562                cursor -= output.weight;
563                if cursor <= 0.0 {
564                    return Some(output.output_node);
565                }
566            }
567            self.outputs.last().map(|o| o.output_node)
568        } else {
569            let idx = (rand_float * self.outputs.len() as f32) as usize;
570            let idx = idx.min(self.outputs.len() - 1);
571            Some(self.outputs[idx].output_node)
572        }
573    }
574}
575
576#[derive(Debug, Clone)]
577pub struct TimedNode {
578    pub id: u64,
579    pub position: Vec2,
580    pub duration_secs: f32,
581    pub on_timeout_node: u64,
582    pub on_complete_node: u64,
583    pub show_timer: bool,
584    pub timer_label: String,
585    pub input_port: u64,
586    pub comment: String,
587    pub tags: Vec<String>,
588    pub elapsed: f32,
589}
590
591impl TimedNode {
592    pub fn new(id: u64) -> Self {
593        TimedNode {
594            id,
595            position: Vec2::ZERO,
596            duration_secs: 5.0,
597            on_timeout_node: 0,
598            on_complete_node: 0,
599            show_timer: true,
600            timer_label: String::new(),
601            input_port: 0,
602            comment: String::new(),
603            tags: Vec::new(),
604            elapsed: 0.0,
605        }
606    }
607
608    pub fn update(&mut self, delta: f32) -> TimedNodeResult {
609        self.elapsed += delta;
610        if self.elapsed >= self.duration_secs {
611            TimedNodeResult::Timeout(self.on_timeout_node)
612        } else {
613            TimedNodeResult::Running(self.elapsed / self.duration_secs)
614        }
615    }
616
617    pub fn reset(&mut self) { self.elapsed = 0.0; }
618    pub fn remaining(&self) -> f32 { (self.duration_secs - self.elapsed).max(0.0) }
619    pub fn progress(&self) -> f32 { (self.elapsed / self.duration_secs).clamp(0.0, 1.0) }
620}
621
622#[derive(Debug, Clone)]
623pub enum TimedNodeResult {
624    Running(f32),
625    Timeout(u64),
626    Complete(u64),
627}
628
629// ============================================================
630// SECTION 2: DIALOGUE TREE
631// ============================================================
632
633#[derive(Debug, Clone)]
634pub enum DialogueNode {
635    Start(StartNode),
636    Speaker(SpeakerNode),
637    PlayerChoice(PlayerChoiceNode),
638    Condition(ConditionNode),
639    SetVariable(SetVariableNode),
640    TriggerEvent(TriggerEventNode),
641    Jump(JumpNode),
642    End(EndNode),
643    Random(RandomNode),
644    Timed(TimedNode),
645}
646
647impl DialogueNode {
648    pub fn id(&self) -> u64 {
649        match self {
650            DialogueNode::Start(n) => n.id,
651            DialogueNode::Speaker(n) => n.id,
652            DialogueNode::PlayerChoice(n) => n.id,
653            DialogueNode::Condition(n) => n.id,
654            DialogueNode::SetVariable(n) => n.id,
655            DialogueNode::TriggerEvent(n) => n.id,
656            DialogueNode::Jump(n) => n.id,
657            DialogueNode::End(n) => n.id,
658            DialogueNode::Random(n) => n.id,
659            DialogueNode::Timed(n) => n.id,
660        }
661    }
662
663    pub fn position(&self) -> Vec2 {
664        match self {
665            DialogueNode::Start(n) => n.position,
666            DialogueNode::Speaker(n) => n.position,
667            DialogueNode::PlayerChoice(n) => n.position,
668            DialogueNode::Condition(n) => n.position,
669            DialogueNode::SetVariable(n) => n.position,
670            DialogueNode::TriggerEvent(n) => n.position,
671            DialogueNode::Jump(n) => n.position,
672            DialogueNode::End(n) => n.position,
673            DialogueNode::Random(n) => n.position,
674            DialogueNode::Timed(n) => n.position,
675        }
676    }
677
678    pub fn set_position(&mut self, pos: Vec2) {
679        match self {
680            DialogueNode::Start(n) => n.position = pos,
681            DialogueNode::Speaker(n) => n.position = pos,
682            DialogueNode::PlayerChoice(n) => n.position = pos,
683            DialogueNode::Condition(n) => n.position = pos,
684            DialogueNode::SetVariable(n) => n.position = pos,
685            DialogueNode::TriggerEvent(n) => n.position = pos,
686            DialogueNode::Jump(n) => n.position = pos,
687            DialogueNode::End(n) => n.position = pos,
688            DialogueNode::Random(n) => n.position = pos,
689            DialogueNode::Timed(n) => n.position = pos,
690        }
691    }
692
693    pub fn node_type(&self) -> NodeType {
694        match self {
695            DialogueNode::Start(_) => NodeType::Start,
696            DialogueNode::Speaker(_) => NodeType::Speaker,
697            DialogueNode::PlayerChoice(_) => NodeType::PlayerChoice,
698            DialogueNode::Condition(_) => NodeType::Condition,
699            DialogueNode::SetVariable(_) => NodeType::SetVariable,
700            DialogueNode::TriggerEvent(_) => NodeType::TriggerEvent,
701            DialogueNode::Jump(_) => NodeType::Jump,
702            DialogueNode::End(_) => NodeType::End,
703            DialogueNode::Random(_) => NodeType::Random,
704            DialogueNode::Timed(_) => NodeType::Timed,
705        }
706    }
707
708    pub fn tags(&self) -> &[String] {
709        match self {
710            DialogueNode::Start(n) => &n.tags,
711            DialogueNode::Speaker(n) => &n.tags,
712            DialogueNode::PlayerChoice(n) => &n.tags,
713            DialogueNode::Condition(n) => &n.tags,
714            DialogueNode::SetVariable(n) => &n.tags,
715            DialogueNode::TriggerEvent(n) => &n.tags,
716            DialogueNode::Jump(n) => &n.tags,
717            DialogueNode::End(n) => &n.tags,
718            DialogueNode::Random(n) => &n.tags,
719            DialogueNode::Timed(n) => &n.tags,
720        }
721    }
722}
723
724#[derive(Debug, Clone)]
725pub struct DialogueTree {
726    pub id: String,
727    pub name: String,
728    pub nodes: HashMap<u64, DialogueNode>,
729    pub connections: Vec<NodeConnection>,
730    pub start_node_id: Option<u64>,
731    pub metadata: DialogueTreeMetadata,
732    next_id: u64,
733}
734
735#[derive(Debug, Clone, Default)]
736pub struct DialogueTreeMetadata {
737    pub author: String,
738    pub version: String,
739    pub created_at: String,
740    pub modified_at: String,
741    pub description: String,
742    pub tags: Vec<String>,
743    pub localization_keys: HashSet<String>,
744    pub voice_lines: Vec<String>,
745    pub speaker_ids: HashSet<String>,
746}
747
748impl DialogueTree {
749    pub fn new(id: String, name: String) -> Self {
750        DialogueTree {
751            id,
752            name,
753            nodes: HashMap::new(),
754            connections: Vec::new(),
755            start_node_id: None,
756            metadata: DialogueTreeMetadata::default(),
757            next_id: 1,
758        }
759    }
760
761    pub fn add_node(&mut self, node: DialogueNode) -> u64 {
762        let id = node.id();
763        if let DialogueNode::Start(_) = &node {
764            self.start_node_id = Some(id);
765        }
766        self.nodes.insert(id, node);
767        id
768    }
769
770    pub fn remove_node(&mut self, id: u64) {
771        self.nodes.remove(&id);
772        self.connections.retain(|c| c.from_node != id && c.to_node != id);
773        if self.start_node_id == Some(id) {
774            self.start_node_id = None;
775        }
776    }
777
778    pub fn connect(&mut self, from: u64, from_port: usize, to: u64, to_port: usize, label: Option<String>) {
779        // Remove existing connection from same port
780        self.connections.retain(|c| !(c.from_node == from && c.from_port == from_port));
781        self.connections.push(NodeConnection {
782            from_node: from,
783            from_port,
784            to_node: to,
785            to_port,
786            label,
787        });
788    }
789
790    pub fn disconnect(&mut self, from: u64, from_port: usize) {
791        self.connections.retain(|c| !(c.from_node == from && c.from_port == from_port));
792    }
793
794    pub fn get_outputs(&self, node_id: u64) -> Vec<&NodeConnection> {
795        self.connections.iter().filter(|c| c.from_node == node_id).collect()
796    }
797
798    pub fn get_inputs(&self, node_id: u64) -> Vec<&NodeConnection> {
799        self.connections.iter().filter(|c| c.to_node == node_id).collect()
800    }
801
802    pub fn next_node(&self, from: u64, port: usize) -> Option<u64> {
803        self.connections.iter()
804            .find(|c| c.from_node == from && c.from_port == port)
805            .map(|c| c.to_node)
806    }
807
808    pub fn allocate_id(&mut self) -> u64 {
809        let id = self.next_id;
810        self.next_id += 1;
811        id
812    }
813
814    pub fn validate(&self) -> Vec<ValidationError> {
815        let mut errors = Vec::new();
816        if self.start_node_id.is_none() {
817            errors.push(ValidationError {
818                node_id: None,
819                severity: ValidationSeverity::Error,
820                message: "Dialogue tree has no start node".into(),
821            });
822        }
823        for (id, node) in &self.nodes {
824            let outputs = self.get_outputs(*id);
825            match node {
826                DialogueNode::Speaker(n) => {
827                    if n.dialogue_text.is_empty() && n.localization_key.is_empty() {
828                        errors.push(ValidationError {
829                            node_id: Some(*id),
830                            severity: ValidationSeverity::Warning,
831                            message: "Speaker node has no text".into(),
832                        });
833                    }
834                    if outputs.is_empty() {
835                        errors.push(ValidationError {
836                            node_id: Some(*id),
837                            severity: ValidationSeverity::Warning,
838                            message: "Speaker node has no output connection".into(),
839                        });
840                    }
841                }
842                DialogueNode::PlayerChoice(n) => {
843                    if n.choices.is_empty() {
844                        errors.push(ValidationError {
845                            node_id: Some(*id),
846                            severity: ValidationSeverity::Error,
847                            message: "Choice node has no choices".into(),
848                        });
849                    }
850                }
851                DialogueNode::Condition(n) => {
852                    let has_true = self.connections.iter().any(|c| c.from_node == *id && c.from_port == 0);
853                    let has_false = self.connections.iter().any(|c| c.from_node == *id && c.from_port == 1);
854                    if !has_true {
855                        errors.push(ValidationError {
856                            node_id: Some(*id),
857                            severity: ValidationSeverity::Warning,
858                            message: "Condition node missing true branch".into(),
859                        });
860                    }
861                    if !has_false {
862                        errors.push(ValidationError {
863                            node_id: Some(*id),
864                            severity: ValidationSeverity::Warning,
865                            message: "Condition node missing false branch".into(),
866                        });
867                    }
868                }
869                _ => {}
870            }
871        }
872        // Check for unreachable nodes
873        if let Some(start) = self.start_node_id {
874            let reachable = self.find_reachable_nodes(start);
875            for id in self.nodes.keys() {
876                if !reachable.contains(id) {
877                    errors.push(ValidationError {
878                        node_id: Some(*id),
879                        severity: ValidationSeverity::Warning,
880                        message: "Node is unreachable from start".into(),
881                    });
882                }
883            }
884        }
885        errors
886    }
887
888    pub fn find_reachable_nodes(&self, start: u64) -> HashSet<u64> {
889        let mut visited = HashSet::new();
890        let mut queue = VecDeque::new();
891        queue.push_back(start);
892        while let Some(id) = queue.pop_front() {
893            if visited.contains(&id) { continue; }
894            visited.insert(id);
895            for conn in self.get_outputs(id) {
896                queue.push_back(conn.to_node);
897            }
898        }
899        visited
900    }
901
902    pub fn topological_sort(&self) -> Vec<u64> {
903        let mut in_degree: HashMap<u64, usize> = HashMap::new();
904        for id in self.nodes.keys() {
905            in_degree.insert(*id, 0);
906        }
907        for conn in &self.connections {
908            *in_degree.entry(conn.to_node).or_insert(0) += 1;
909        }
910        let mut queue: VecDeque<u64> = in_degree.iter()
911            .filter(|(_, &d)| d == 0)
912            .map(|(id, _)| *id)
913            .collect();
914        let mut result = Vec::new();
915        while let Some(id) = queue.pop_front() {
916            result.push(id);
917            for conn in self.get_outputs(id) {
918                let deg = in_degree.entry(conn.to_node).or_insert(0);
919                if *deg > 0 { *deg -= 1; }
920                if *deg == 0 { queue.push_back(conn.to_node); }
921            }
922        }
923        result
924    }
925
926    pub fn find_cycles(&self) -> Vec<Vec<u64>> {
927        let mut cycles = Vec::new();
928        let mut visited = HashSet::new();
929        let mut rec_stack = HashSet::new();
930        let mut path = Vec::new();
931        for &id in self.nodes.keys() {
932            if !visited.contains(&id) {
933                self.dfs_cycle(id, &mut visited, &mut rec_stack, &mut path, &mut cycles);
934            }
935        }
936        cycles
937    }
938
939    fn dfs_cycle(&self, node: u64, visited: &mut HashSet<u64>, rec_stack: &mut HashSet<u64>, path: &mut Vec<u64>, cycles: &mut Vec<Vec<u64>>) {
940        visited.insert(node);
941        rec_stack.insert(node);
942        path.push(node);
943        for conn in self.get_outputs(node) {
944            let next = conn.to_node;
945            if !visited.contains(&next) {
946                self.dfs_cycle(next, visited, rec_stack, path, cycles);
947            } else if rec_stack.contains(&next) {
948                let start = path.iter().position(|&n| n == next).unwrap_or(0);
949                cycles.push(path[start..].to_vec());
950            }
951        }
952        path.pop();
953        rec_stack.remove(&node);
954    }
955
956    pub fn collect_all_localization_keys(&self) -> Vec<String> {
957        let mut keys = Vec::new();
958        for node in self.nodes.values() {
959            match node {
960                DialogueNode::Speaker(n) => {
961                    if !n.localization_key.is_empty() {
962                        keys.push(n.localization_key.clone());
963                    }
964                }
965                DialogueNode::PlayerChoice(n) => {
966                    if !n.prompt_localization_key.is_empty() {
967                        keys.push(n.prompt_localization_key.clone());
968                    }
969                    for choice in &n.choices {
970                        if !choice.localization_key.is_empty() {
971                            keys.push(choice.localization_key.clone());
972                        }
973                    }
974                }
975                _ => {}
976            }
977        }
978        keys
979    }
980
981    pub fn collect_all_voice_lines(&self) -> Vec<VoiceLineRef> {
982        let mut lines = Vec::new();
983        for node in self.nodes.values() {
984            if let DialogueNode::Speaker(n) = node {
985                if let Some(vl) = &n.voice_line_ref {
986                    lines.push(vl.clone());
987                }
988            }
989        }
990        lines
991    }
992
993    pub fn auto_layout(&mut self) {
994        if let Some(start_id) = self.start_node_id {
995            let mut positions: HashMap<u64, Vec2> = HashMap::new();
996            let mut queue = VecDeque::new();
997            let mut visited = HashSet::new();
998            queue.push_back((start_id, 0usize, 0usize));
999            let node_width = 200.0_f32;
1000            let node_height = 120.0_f32;
1001            let x_gap = 50.0_f32;
1002            let y_gap = 30.0_f32;
1003            let mut col_row: HashMap<usize, usize> = HashMap::new();
1004            while let Some((id, col, _row)) = queue.pop_front() {
1005                if visited.contains(&id) { continue; }
1006                visited.insert(id);
1007                let row = *col_row.entry(col).or_insert(0);
1008                *col_row.entry(col).or_insert(0) += 1;
1009                let x = col as f32 * (node_width + x_gap);
1010                let y = row as f32 * (node_height + y_gap);
1011                positions.insert(id, Vec2::new(x, y));
1012                for conn in self.get_outputs(id).into_iter().map(|c| (c.to_node, c.from_port)) {
1013                    queue.push_back((conn.0, col + 1, conn.1));
1014                }
1015            }
1016            for (id, pos) in positions {
1017                if let Some(node) = self.nodes.get_mut(&id) {
1018                    node.set_position(pos);
1019                }
1020            }
1021        }
1022    }
1023}
1024
1025#[derive(Debug, Clone)]
1026pub struct ValidationError {
1027    pub node_id: Option<u64>,
1028    pub severity: ValidationSeverity,
1029    pub message: String,
1030}
1031
1032#[derive(Debug, Clone, PartialEq)]
1033pub enum ValidationSeverity {
1034    Info,
1035    Warning,
1036    Error,
1037}
1038
1039// ============================================================
1040// SECTION 3: CONDITION SYSTEM
1041// ============================================================
1042
1043#[derive(Debug, Clone)]
1044pub enum ConditionExpression {
1045    Literal(bool),
1046    Comparison(ComparisonCondition),
1047    QuestState(QuestStateCondition),
1048    FactionRep(FactionRepCondition),
1049    Flag(FlagCondition),
1050    And(Box<ConditionExpression>, Box<ConditionExpression>),
1051    Or(Box<ConditionExpression>, Box<ConditionExpression>),
1052    Not(Box<ConditionExpression>),
1053    All(Vec<ConditionExpression>),
1054    Any(Vec<ConditionExpression>),
1055}
1056
1057impl ConditionExpression {
1058    pub fn evaluate(&self, scope: &VariableScope) -> bool {
1059        match self {
1060            ConditionExpression::Literal(b) => *b,
1061            ConditionExpression::Comparison(c) => c.evaluate(scope),
1062            ConditionExpression::QuestState(c) => c.evaluate(scope),
1063            ConditionExpression::FactionRep(c) => c.evaluate(scope),
1064            ConditionExpression::Flag(c) => c.evaluate(scope),
1065            ConditionExpression::And(a, b) => a.evaluate(scope) && b.evaluate(scope),
1066            ConditionExpression::Or(a, b) => a.evaluate(scope) || b.evaluate(scope),
1067            ConditionExpression::Not(e) => !e.evaluate(scope),
1068            ConditionExpression::All(exprs) => exprs.iter().all(|e| e.evaluate(scope)),
1069            ConditionExpression::Any(exprs) => exprs.iter().any(|e| e.evaluate(scope)),
1070        }
1071    }
1072
1073    pub fn and(self, other: ConditionExpression) -> ConditionExpression {
1074        ConditionExpression::And(Box::new(self), Box::new(other))
1075    }
1076
1077    pub fn or(self, other: ConditionExpression) -> ConditionExpression {
1078        ConditionExpression::Or(Box::new(self), Box::new(other))
1079    }
1080
1081    pub fn not(self) -> ConditionExpression {
1082        ConditionExpression::Not(Box::new(self))
1083    }
1084
1085    pub fn describe(&self) -> String {
1086        match self {
1087            ConditionExpression::Literal(b) => format!("{}", b),
1088            ConditionExpression::Comparison(c) => c.describe(),
1089            ConditionExpression::QuestState(c) => c.describe(),
1090            ConditionExpression::FactionRep(c) => c.describe(),
1091            ConditionExpression::Flag(c) => c.describe(),
1092            ConditionExpression::And(a, b) => format!("({} AND {})", a.describe(), b.describe()),
1093            ConditionExpression::Or(a, b) => format!("({} OR {})", a.describe(), b.describe()),
1094            ConditionExpression::Not(e) => format!("NOT ({})", e.describe()),
1095            ConditionExpression::All(exprs) => {
1096                let parts: Vec<String> = exprs.iter().map(|e| e.describe()).collect();
1097                format!("ALL({})", parts.join(", "))
1098            }
1099            ConditionExpression::Any(exprs) => {
1100                let parts: Vec<String> = exprs.iter().map(|e| e.describe()).collect();
1101                format!("ANY({})", parts.join(", "))
1102            }
1103        }
1104    }
1105}
1106
1107#[derive(Debug, Clone)]
1108pub struct ComparisonCondition {
1109    pub variable_name: String,
1110    pub scope_type: ScopeType,
1111    pub operator: ComparisonOperator,
1112    pub compare_value: DialogueValue,
1113}
1114
1115impl ComparisonCondition {
1116    pub fn evaluate(&self, scope: &VariableScope) -> bool {
1117        if let Some(var) = scope.get(&self.variable_name, &self.scope_type) {
1118            self.operator.compare(&var, &self.compare_value)
1119        } else {
1120            false
1121        }
1122    }
1123
1124    pub fn describe(&self) -> String {
1125        format!("{} {:?} {:?}", self.variable_name, self.operator, self.compare_value)
1126    }
1127}
1128
1129#[derive(Debug, Clone, PartialEq)]
1130pub enum ComparisonOperator {
1131    Equal,
1132    NotEqual,
1133    LessThan,
1134    LessEqual,
1135    GreaterThan,
1136    GreaterEqual,
1137    Contains,
1138    StartsWith,
1139    EndsWith,
1140}
1141
1142impl ComparisonOperator {
1143    pub fn compare(&self, a: &DialogueValue, b: &DialogueValue) -> bool {
1144        match (a, b) {
1145            (DialogueValue::Int(x), DialogueValue::Int(y)) => match self {
1146                ComparisonOperator::Equal => x == y,
1147                ComparisonOperator::NotEqual => x != y,
1148                ComparisonOperator::LessThan => x < y,
1149                ComparisonOperator::LessEqual => x <= y,
1150                ComparisonOperator::GreaterThan => x > y,
1151                ComparisonOperator::GreaterEqual => x >= y,
1152                _ => false,
1153            },
1154            (DialogueValue::Float(x), DialogueValue::Float(y)) => match self {
1155                ComparisonOperator::Equal => (x - y).abs() < 1e-6,
1156                ComparisonOperator::NotEqual => (x - y).abs() >= 1e-6,
1157                ComparisonOperator::LessThan => x < y,
1158                ComparisonOperator::LessEqual => x <= y,
1159                ComparisonOperator::GreaterThan => x > y,
1160                ComparisonOperator::GreaterEqual => x >= y,
1161                _ => false,
1162            },
1163            (DialogueValue::Bool(x), DialogueValue::Bool(y)) => match self {
1164                ComparisonOperator::Equal => x == y,
1165                ComparisonOperator::NotEqual => x != y,
1166                _ => false,
1167            },
1168            (DialogueValue::String(x), DialogueValue::String(y)) => match self {
1169                ComparisonOperator::Equal => x == y,
1170                ComparisonOperator::NotEqual => x != y,
1171                ComparisonOperator::Contains => x.contains(y.as_str()),
1172                ComparisonOperator::StartsWith => x.starts_with(y.as_str()),
1173                ComparisonOperator::EndsWith => x.ends_with(y.as_str()),
1174                _ => false,
1175            },
1176            _ => false,
1177        }
1178    }
1179
1180    pub fn symbol(&self) -> &str {
1181        match self {
1182            ComparisonOperator::Equal => "==",
1183            ComparisonOperator::NotEqual => "!=",
1184            ComparisonOperator::LessThan => "<",
1185            ComparisonOperator::LessEqual => "<=",
1186            ComparisonOperator::GreaterThan => ">",
1187            ComparisonOperator::GreaterEqual => ">=",
1188            ComparisonOperator::Contains => "contains",
1189            ComparisonOperator::StartsWith => "starts_with",
1190            ComparisonOperator::EndsWith => "ends_with",
1191        }
1192    }
1193}
1194
1195#[derive(Debug, Clone)]
1196pub struct QuestStateCondition {
1197    pub quest_id: String,
1198    pub expected_state: QuestStateEnum,
1199    pub objective_id: Option<String>,
1200}
1201
1202#[derive(Debug, Clone, PartialEq)]
1203pub enum QuestStateEnum {
1204    NotStarted,
1205    Active,
1206    Completed,
1207    Failed,
1208    Abandoned,
1209}
1210
1211impl QuestStateCondition {
1212    pub fn evaluate(&self, scope: &VariableScope) -> bool {
1213        let key = format!("quest_state_{}", self.quest_id);
1214        if let Some(DialogueValue::String(state)) = scope.get(&key, &ScopeType::Global) {
1215            let matches = match &self.expected_state {
1216                QuestStateEnum::NotStarted => state == "not_started",
1217                QuestStateEnum::Active => state == "active",
1218                QuestStateEnum::Completed => state == "completed",
1219                QuestStateEnum::Failed => state == "failed",
1220                QuestStateEnum::Abandoned => state == "abandoned",
1221            };
1222            if matches {
1223                if let Some(obj_id) = &self.objective_id {
1224                    let obj_key = format!("quest_{}_obj_{}", self.quest_id, obj_id);
1225                    return matches!(scope.get(&obj_key, &ScopeType::Global), Some(DialogueValue::Bool(true)));
1226                }
1227            }
1228            matches
1229        } else {
1230            self.expected_state == QuestStateEnum::NotStarted
1231        }
1232    }
1233
1234    pub fn describe(&self) -> String {
1235        format!("quest({}) == {:?}", self.quest_id, self.expected_state)
1236    }
1237}
1238
1239#[derive(Debug, Clone)]
1240pub struct FactionRepCondition {
1241    pub faction_id: String,
1242    pub operator: ComparisonOperator,
1243    pub threshold: i32,
1244}
1245
1246impl FactionRepCondition {
1247    pub fn evaluate(&self, scope: &VariableScope) -> bool {
1248        let key = format!("faction_rep_{}", self.faction_id);
1249        if let Some(DialogueValue::Int(rep)) = scope.get(&key, &ScopeType::Global) {
1250            self.operator.compare(&DialogueValue::Int(rep), &DialogueValue::Int(self.threshold as i64))
1251        } else {
1252            false
1253        }
1254    }
1255
1256    pub fn describe(&self) -> String {
1257        format!("faction({}) {} {}", self.faction_id, self.operator.symbol(), self.threshold)
1258    }
1259}
1260
1261#[derive(Debug, Clone)]
1262pub struct FlagCondition {
1263    pub flag_name: String,
1264    pub expected: bool,
1265}
1266
1267impl FlagCondition {
1268    pub fn evaluate(&self, scope: &VariableScope) -> bool {
1269        if let Some(DialogueValue::Bool(val)) = scope.get(&self.flag_name, &ScopeType::Persistent) {
1270            val == self.expected
1271        } else {
1272            !self.expected
1273        }
1274    }
1275
1276    pub fn describe(&self) -> String {
1277        if self.expected {
1278            format!("flag({})", self.flag_name)
1279        } else {
1280            format!("!flag({})", self.flag_name)
1281        }
1282    }
1283}
1284
1285// ============================================================
1286// SECTION 4: VARIABLE TRACKING
1287// ============================================================
1288
1289#[derive(Debug, Clone, PartialEq)]
1290pub enum DialogueValue {
1291    Bool(bool),
1292    Int(i64),
1293    Float(f64),
1294    String(String),
1295    Flag(bool),
1296}
1297
1298impl DialogueValue {
1299    pub fn type_name(&self) -> &str {
1300        match self {
1301            DialogueValue::Bool(_) => "bool",
1302            DialogueValue::Int(_) => "int",
1303            DialogueValue::Float(_) => "float",
1304            DialogueValue::String(_) => "string",
1305            DialogueValue::Flag(_) => "flag",
1306        }
1307    }
1308
1309    pub fn to_string_repr(&self) -> String {
1310        match self {
1311            DialogueValue::Bool(b) => b.to_string(),
1312            DialogueValue::Int(i) => i.to_string(),
1313            DialogueValue::Float(f) => format!("{:.4}", f),
1314            DialogueValue::String(s) => s.clone(),
1315            DialogueValue::Flag(b) => if *b { "set".to_string() } else { "unset".to_string() },
1316        }
1317    }
1318
1319    pub fn as_bool(&self) -> bool {
1320        match self {
1321            DialogueValue::Bool(b) => *b,
1322            DialogueValue::Int(i) => *i != 0,
1323            DialogueValue::Float(f) => *f != 0.0,
1324            DialogueValue::String(s) => !s.is_empty(),
1325            DialogueValue::Flag(b) => *b,
1326        }
1327    }
1328
1329    pub fn as_int(&self) -> i64 {
1330        match self {
1331            DialogueValue::Bool(b) => if *b { 1 } else { 0 },
1332            DialogueValue::Int(i) => *i,
1333            DialogueValue::Float(f) => *f as i64,
1334            DialogueValue::String(s) => s.parse().unwrap_or(0),
1335            DialogueValue::Flag(b) => if *b { 1 } else { 0 },
1336        }
1337    }
1338}
1339
1340#[derive(Debug, Clone, PartialEq)]
1341pub enum ScopeType {
1342    Local,       // current conversation only
1343    Global,      // session-wide
1344    Persistent,  // saved to disk
1345}
1346
1347#[derive(Debug, Clone, Default)]
1348pub struct VariableScope {
1349    pub local: HashMap<String, DialogueValue>,
1350    pub global: HashMap<String, DialogueValue>,
1351    pub persistent: HashMap<String, DialogueValue>,
1352    pub change_log: Vec<VariableChange>,
1353}
1354
1355#[derive(Debug, Clone)]
1356pub struct VariableChange {
1357    pub name: String,
1358    pub scope: ScopeType,
1359    pub old_value: Option<DialogueValue>,
1360    pub new_value: DialogueValue,
1361    pub timestamp: f64,
1362}
1363
1364impl VariableScope {
1365    pub fn new() -> Self { Self::default() }
1366
1367    pub fn get(&self, name: &str, scope: &ScopeType) -> Option<DialogueValue> {
1368        match scope {
1369            ScopeType::Local => self.local.get(name).cloned(),
1370            ScopeType::Global => self.global.get(name).cloned()
1371                .or_else(|| self.local.get(name).cloned()),
1372            ScopeType::Persistent => self.persistent.get(name).cloned()
1373                .or_else(|| self.global.get(name).cloned()),
1374        }
1375    }
1376
1377    pub fn get_any(&self, name: &str) -> Option<DialogueValue> {
1378        self.local.get(name).cloned()
1379            .or_else(|| self.global.get(name).cloned())
1380            .or_else(|| self.persistent.get(name).cloned())
1381    }
1382
1383    pub fn set(&mut self, name: String, scope: ScopeType, value: DialogueValue) {
1384        let old = self.get_any(&name);
1385        let change = VariableChange {
1386            name: name.clone(),
1387            scope: scope.clone(),
1388            old_value: old,
1389            new_value: value.clone(),
1390            timestamp: 0.0,
1391        };
1392        self.change_log.push(change);
1393        match scope {
1394            ScopeType::Local => { self.local.insert(name, value); }
1395            ScopeType::Global => { self.global.insert(name, value); }
1396            ScopeType::Persistent => { self.persistent.insert(name, value); }
1397        }
1398    }
1399
1400    pub fn set_flag(&mut self, name: &str) {
1401        self.persistent.insert(name.to_string(), DialogueValue::Flag(true));
1402    }
1403
1404    pub fn clear_flag(&mut self, name: &str) {
1405        self.persistent.insert(name.to_string(), DialogueValue::Flag(false));
1406    }
1407
1408    pub fn has_flag(&self, name: &str) -> bool {
1409        matches!(self.persistent.get(name), Some(DialogueValue::Flag(true)) | Some(DialogueValue::Bool(true)))
1410    }
1411
1412    pub fn clear_local(&mut self) {
1413        self.local.clear();
1414    }
1415
1416    pub fn serialize_persistent(&self) -> String {
1417        let mut parts = Vec::new();
1418        for (k, v) in &self.persistent {
1419            parts.push(format!("{}={}", k, v.to_string_repr()));
1420        }
1421        parts.sort();
1422        parts.join("\n")
1423    }
1424
1425    pub fn deserialize_persistent(&mut self, data: &str) {
1426        for line in data.lines() {
1427            if let Some((k, v)) = line.split_once('=') {
1428                // Try to parse typed values
1429                let val = if v == "true" {
1430                    DialogueValue::Bool(true)
1431                } else if v == "false" {
1432                    DialogueValue::Bool(false)
1433                } else if let Ok(i) = v.parse::<i64>() {
1434                    DialogueValue::Int(i)
1435                } else if let Ok(f) = v.parse::<f64>() {
1436                    DialogueValue::Float(f)
1437                } else {
1438                    DialogueValue::String(v.to_string())
1439                };
1440                self.persistent.insert(k.to_string(), val);
1441            }
1442        }
1443    }
1444
1445    pub fn get_all_vars(&self) -> Vec<(String, ScopeType, &DialogueValue)> {
1446        let mut all = Vec::new();
1447        for (k, v) in &self.local {
1448            all.push((k.clone(), ScopeType::Local, v));
1449        }
1450        for (k, v) in &self.global {
1451            all.push((k.clone(), ScopeType::Global, v));
1452        }
1453        for (k, v) in &self.persistent {
1454            all.push((k.clone(), ScopeType::Persistent, v));
1455        }
1456        all.sort_by(|a, b| a.0.cmp(&b.0));
1457        all
1458    }
1459}
1460
1461#[derive(Debug, Clone)]
1462pub struct DialogueVariable {
1463    pub name: String,
1464    pub value: DialogueValue,
1465    pub scope: ScopeType,
1466    pub description: String,
1467    pub default_value: DialogueValue,
1468}
1469
1470// ============================================================
1471// SECTION 5: LOCALIZATION
1472// ============================================================
1473
1474#[derive(Debug, Clone)]
1475pub struct LocalizationTable {
1476    pub tables: HashMap<String, HashMap<String, String>>,
1477    pub default_locale: String,
1478    pub supported_locales: Vec<String>,
1479    pub missing_keys: HashSet<String>,
1480    pub untranslated_keys: HashMap<String, Vec<String>>,
1481}
1482
1483impl LocalizationTable {
1484    pub fn new(default_locale: &str) -> Self {
1485        let mut tables = HashMap::new();
1486        tables.insert(default_locale.to_string(), HashMap::new());
1487        LocalizationTable {
1488            tables,
1489            default_locale: default_locale.to_string(),
1490            supported_locales: vec![default_locale.to_string()],
1491            missing_keys: HashSet::new(),
1492            untranslated_keys: HashMap::new(),
1493        }
1494    }
1495
1496    pub fn set(&mut self, key: &str, locale: &str, text: &str) {
1497        self.tables
1498            .entry(locale.to_string())
1499            .or_insert_with(HashMap::new)
1500            .insert(key.to_string(), text.to_string());
1501    }
1502
1503    pub fn get(&self, key: &str, locale: &str) -> Option<String> {
1504        if let Some(table) = self.tables.get(locale) {
1505            if let Some(text) = table.get(key) {
1506                return Some(text.clone());
1507            }
1508        }
1509        // Fallback to default locale
1510        if locale != self.default_locale {
1511            if let Some(table) = self.tables.get(&self.default_locale) {
1512                return table.get(key).cloned();
1513            }
1514        }
1515        None
1516    }
1517
1518    pub fn add_locale(&mut self, locale: &str) {
1519        if !self.supported_locales.contains(&locale.to_string()) {
1520            self.supported_locales.push(locale.to_string());
1521            self.tables.insert(locale.to_string(), HashMap::new());
1522        }
1523    }
1524
1525    pub fn generate_key(conversation_id: &str, node_id: u64, field: &str) -> String {
1526        format!("{}_{:04}_{}", conversation_id, node_id, field)
1527    }
1528
1529    pub fn validate_key(key: &str) -> bool {
1530        !key.is_empty() &&
1531        key.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.' || c == '-') &&
1532        !key.starts_with('_') &&
1533        !key.ends_with('_')
1534    }
1535
1536    pub fn find_missing_translations(&mut self) -> HashMap<String, Vec<String>> {
1537        let default_keys: HashSet<String> = self.tables
1538            .get(&self.default_locale)
1539            .map(|t| t.keys().cloned().collect())
1540            .unwrap_or_default();
1541        let mut missing: HashMap<String, Vec<String>> = HashMap::new();
1542        for locale in &self.supported_locales.clone() {
1543            if locale == &self.default_locale { continue; }
1544            if let Some(table) = self.tables.get(locale) {
1545                for key in &default_keys {
1546                    if !table.contains_key(key) {
1547                        missing.entry(locale.clone()).or_default().push(key.clone());
1548                    }
1549                }
1550            } else {
1551                missing.insert(locale.clone(), default_keys.iter().cloned().collect());
1552            }
1553        }
1554        self.untranslated_keys = missing.clone();
1555        missing
1556    }
1557
1558    pub fn export_to_json(&self, locale: &str) -> String {
1559        let mut json = String::from("{\n");
1560        if let Some(table) = self.tables.get(locale) {
1561            let mut entries: Vec<(&String, &String)> = table.iter().collect();
1562            entries.sort_by_key(|(k, _)| k.as_str());
1563            for (i, (key, value)) in entries.iter().enumerate() {
1564                let escaped = value.replace('"', "\\\"").replace('\n', "\\n");
1565                if i < entries.len() - 1 {
1566                    json.push_str(&format!("  \"{}\": \"{}\",\n", key, escaped));
1567                } else {
1568                    json.push_str(&format!("  \"{}\": \"{}\"\n", key, escaped));
1569                }
1570            }
1571        }
1572        json.push('}');
1573        json
1574    }
1575
1576    pub fn export_to_csv(&self, locale: &str) -> String {
1577        let mut csv = String::from("key,text,default_text\n");
1578        let default_table = self.tables.get(&self.default_locale);
1579        if let Some(table) = self.tables.get(locale) {
1580            let mut entries: Vec<(&String, &String)> = table.iter().collect();
1581            entries.sort_by_key(|(k, _)| k.as_str());
1582            for (key, value) in &entries {
1583                let escaped_val = value.replace('"', "\"\"");
1584                let default_val = default_table
1585                    .and_then(|t| t.get(*key))
1586                    .map(|s| s.replace('"', "\"\""))
1587                    .unwrap_or_default();
1588                csv.push_str(&format!("\"{}\",\"{}\",\"{}\"\n", key, escaped_val, default_val));
1589            }
1590        }
1591        csv
1592    }
1593
1594    pub fn import_from_json(&mut self, json: &str, locale: &str) -> Result<usize, String> {
1595        // Simple JSON parser for flat string maps
1596        let json = json.trim();
1597        if !json.starts_with('{') || !json.ends_with('}') {
1598            return Err("Invalid JSON format".to_string());
1599        }
1600        let inner = &json[1..json.len()-1];
1601        let mut count = 0;
1602        let table = self.tables.entry(locale.to_string()).or_insert_with(HashMap::new);
1603        for line in inner.lines() {
1604            let line = line.trim().trim_end_matches(',');
1605            if line.is_empty() { continue; }
1606            if let Some(colon_pos) = line.find(':') {
1607                let key_part = line[..colon_pos].trim().trim_matches('"');
1608                let val_part = line[colon_pos+1..].trim().trim_matches('"');
1609                table.insert(key_part.to_string(), val_part.replace("\\n", "\n").replace("\\\"", "\""));
1610                count += 1;
1611            }
1612        }
1613        Ok(count)
1614    }
1615}
1616
1617// ============================================================
1618// SECTION 6: VOICE LINES
1619// ============================================================
1620
1621#[derive(Debug, Clone)]
1622pub struct VoiceLineRef {
1623    pub clip_id: String,
1624    pub file_path: String,
1625    pub speaker_id: String,
1626    pub emotion: EmotionType,
1627    pub duration_secs: f32,
1628    pub sample_rate: u32,
1629    pub channels: u8,
1630    pub file_size_bytes: u64,
1631    pub lip_sync_data: Option<LipSyncData>,
1632    pub subtitle_offset: f32,
1633    pub waveform_preview: Vec<f32>,
1634}
1635
1636impl VoiceLineRef {
1637    pub fn new(clip_id: String, file_path: String, speaker_id: String) -> Self {
1638        VoiceLineRef {
1639            clip_id,
1640            file_path,
1641            speaker_id,
1642            emotion: EmotionType::Neutral,
1643            duration_secs: 0.0,
1644            sample_rate: 44100,
1645            channels: 1,
1646            file_size_bytes: 0,
1647            lip_sync_data: None,
1648            subtitle_offset: 0.0,
1649            waveform_preview: Vec::new(),
1650        }
1651    }
1652
1653    pub fn format_duration(&self) -> String {
1654        let mins = (self.duration_secs / 60.0) as u32;
1655        let secs = self.duration_secs % 60.0;
1656        format!("{}:{:05.2}", mins, secs)
1657    }
1658
1659    pub fn generate_waveform_preview(&mut self, samples: usize) {
1660        // Generate a simulated waveform preview
1661        self.waveform_preview = (0..samples)
1662            .map(|i| {
1663                let t = i as f32 / samples as f32;
1664                let freq = 440.0_f32;
1665                (t * freq * std::f32::consts::TAU).sin() * (-t * 3.0).exp() * 0.5
1666            })
1667            .collect();
1668    }
1669}
1670
1671#[derive(Debug, Clone)]
1672pub struct LipSyncData {
1673    pub phonemes: Vec<PhonemeKey>,
1674    pub visemes: Vec<VisemeKey>,
1675    pub format: LipSyncFormat,
1676}
1677
1678#[derive(Debug, Clone)]
1679pub struct PhonemeKey {
1680    pub time: f32,
1681    pub phoneme: String,
1682    pub intensity: f32,
1683}
1684
1685#[derive(Debug, Clone)]
1686pub struct VisemeKey {
1687    pub time: f32,
1688    pub viseme_id: u8,
1689    pub blend_weight: f32,
1690    pub duration: f32,
1691}
1692
1693impl VisemeKey {
1694    pub fn viseme_name(&self) -> &str {
1695        match self.viseme_id {
1696            0 => "rest",
1697            1 => "PP",
1698            2 => "FF",
1699            3 => "TH",
1700            4 => "DD",
1701            5 => "kk",
1702            6 => "CH",
1703            7 => "SS",
1704            8 => "nn",
1705            9 => "RR",
1706            10 => "aa",
1707            11 => "E",
1708            12 => "I",
1709            13 => "O",
1710            14 => "U",
1711            _ => "unknown",
1712        }
1713    }
1714}
1715
1716#[derive(Debug, Clone, PartialEq)]
1717pub enum LipSyncFormat {
1718    Custom,
1719    Oculus,
1720    PhonemesOnly,
1721    VisemesOnly,
1722}
1723
1724#[derive(Debug, Clone)]
1725pub struct SubtitleCue {
1726    pub start_time: f32,
1727    pub end_time: f32,
1728    pub text: String,
1729}
1730
1731impl SubtitleCue {
1732    pub fn duration(&self) -> f32 { self.end_time - self.start_time }
1733    pub fn is_active_at(&self, time: f32) -> bool {
1734        time >= self.start_time && time < self.end_time
1735    }
1736}
1737
1738// ============================================================
1739// SECTION 7: EMOTION STATES
1740// ============================================================
1741
1742#[derive(Debug, Clone, PartialEq)]
1743pub enum EmotionType {
1744    Happy,
1745    Sad,
1746    Angry,
1747    Surprised,
1748    Fearful,
1749    Disgusted,
1750    Neutral,
1751    Excited,
1752    Confused,
1753    Contemptuous,
1754    Anxious,
1755    Bored,
1756}
1757
1758impl EmotionType {
1759    pub fn name(&self) -> &str {
1760        match self {
1761            EmotionType::Happy => "happy",
1762            EmotionType::Sad => "sad",
1763            EmotionType::Angry => "angry",
1764            EmotionType::Surprised => "surprised",
1765            EmotionType::Fearful => "fearful",
1766            EmotionType::Disgusted => "disgusted",
1767            EmotionType::Neutral => "neutral",
1768            EmotionType::Excited => "excited",
1769            EmotionType::Confused => "confused",
1770            EmotionType::Contemptuous => "contemptuous",
1771            EmotionType::Anxious => "anxious",
1772            EmotionType::Bored => "bored",
1773        }
1774    }
1775
1776    pub fn all() -> Vec<EmotionType> {
1777        vec![
1778            EmotionType::Happy, EmotionType::Sad, EmotionType::Angry,
1779            EmotionType::Surprised, EmotionType::Fearful, EmotionType::Disgusted,
1780            EmotionType::Neutral, EmotionType::Excited, EmotionType::Confused,
1781            EmotionType::Contemptuous, EmotionType::Anxious, EmotionType::Bored,
1782        ]
1783    }
1784
1785    pub fn from_name(name: &str) -> Option<Self> {
1786        match name {
1787            "happy" => Some(EmotionType::Happy),
1788            "sad" => Some(EmotionType::Sad),
1789            "angry" => Some(EmotionType::Angry),
1790            "surprised" => Some(EmotionType::Surprised),
1791            "fearful" => Some(EmotionType::Fearful),
1792            "disgusted" => Some(EmotionType::Disgusted),
1793            "neutral" => Some(EmotionType::Neutral),
1794            "excited" => Some(EmotionType::Excited),
1795            "confused" => Some(EmotionType::Confused),
1796            "contemptuous" => Some(EmotionType::Contemptuous),
1797            "anxious" => Some(EmotionType::Anxious),
1798            "bored" => Some(EmotionType::Bored),
1799            _ => None,
1800        }
1801    }
1802}
1803
1804#[derive(Debug, Clone)]
1805pub struct EmotionState {
1806    pub primary: EmotionType,
1807    pub primary_intensity: f32,
1808    pub secondary: Option<EmotionType>,
1809    pub secondary_intensity: f32,
1810    pub blend_amount: f32,
1811    pub transition_duration: f32,
1812    pub valence: f32,   // -1 negative .. +1 positive
1813    pub arousal: f32,   // 0 calm .. 1 excited
1814}
1815
1816impl Default for EmotionState {
1817    fn default() -> Self {
1818        EmotionState {
1819            primary: EmotionType::Neutral,
1820            primary_intensity: 1.0,
1821            secondary: None,
1822            secondary_intensity: 0.0,
1823            blend_amount: 0.0,
1824            transition_duration: 0.3,
1825            valence: 0.0,
1826            arousal: 0.0,
1827        }
1828    }
1829}
1830
1831impl EmotionState {
1832    pub fn new(emotion: EmotionType, intensity: f32) -> Self {
1833        let (valence, arousal) = Self::emotion_to_va(&emotion);
1834        EmotionState {
1835            primary: emotion,
1836            primary_intensity: intensity,
1837            secondary: None,
1838            secondary_intensity: 0.0,
1839            blend_amount: 0.0,
1840            transition_duration: 0.3,
1841            valence: valence * intensity,
1842            arousal: arousal * intensity,
1843        }
1844    }
1845
1846    pub fn emotion_to_va(emotion: &EmotionType) -> (f32, f32) {
1847        match emotion {
1848            EmotionType::Happy => (0.9, 0.6),
1849            EmotionType::Sad => (-0.7, -0.4),
1850            EmotionType::Angry => (-0.5, 0.9),
1851            EmotionType::Surprised => (0.1, 0.9),
1852            EmotionType::Fearful => (-0.8, 0.8),
1853            EmotionType::Disgusted => (-0.6, 0.2),
1854            EmotionType::Neutral => (0.0, 0.0),
1855            EmotionType::Excited => (0.8, 0.9),
1856            EmotionType::Confused => (-0.1, 0.4),
1857            EmotionType::Contemptuous => (-0.4, 0.3),
1858            EmotionType::Anxious => (-0.5, 0.7),
1859            EmotionType::Bored => (-0.2, -0.6),
1860        }
1861    }
1862
1863    pub fn blend_towards(&self, target: &EmotionState, t: f32) -> EmotionState {
1864        let t = t.clamp(0.0, 1.0);
1865        EmotionState {
1866            primary: if t < 0.5 { self.primary.clone() } else { target.primary.clone() },
1867            primary_intensity: self.primary_intensity * (1.0 - t) + target.primary_intensity * t,
1868            secondary: if t > 0.3 { target.secondary.clone() } else { self.secondary.clone() },
1869            secondary_intensity: self.secondary_intensity * (1.0 - t) + target.secondary_intensity * t,
1870            blend_amount: t,
1871            transition_duration: self.transition_duration,
1872            valence: self.valence * (1.0 - t) + target.valence * t,
1873            arousal: self.arousal * (1.0 - t) + target.arousal * t,
1874        }
1875    }
1876
1877    pub fn select_portrait(&self, speaker_id: &str, portrait_library: &PortraitLibrary) -> Option<String> {
1878        portrait_library.select_best(speaker_id, &self.primary, self.primary_intensity)
1879    }
1880}
1881
1882#[derive(Debug, Clone, Default)]
1883pub struct PortraitLibrary {
1884    pub portraits: HashMap<String, Vec<PortraitEntry>>,
1885}
1886
1887#[derive(Debug, Clone)]
1888pub struct PortraitEntry {
1889    pub speaker_id: String,
1890    pub emotion: EmotionType,
1891    pub intensity_min: f32,
1892    pub intensity_max: f32,
1893    pub image_path: String,
1894    pub priority: i32,
1895}
1896
1897impl PortraitLibrary {
1898    pub fn add_portrait(&mut self, entry: PortraitEntry) {
1899        self.portraits
1900            .entry(entry.speaker_id.clone())
1901            .or_default()
1902            .push(entry);
1903    }
1904
1905    pub fn select_best(&self, speaker_id: &str, emotion: &EmotionType, intensity: f32) -> Option<String> {
1906        if let Some(entries) = self.portraits.get(speaker_id) {
1907            let mut candidates: Vec<&PortraitEntry> = entries.iter()
1908                .filter(|e| &e.emotion == emotion && intensity >= e.intensity_min && intensity <= e.intensity_max)
1909                .collect();
1910            if candidates.is_empty() {
1911                // Fallback to neutral
1912                candidates = entries.iter()
1913                    .filter(|e| e.emotion == EmotionType::Neutral)
1914                    .collect();
1915            }
1916            candidates.sort_by(|a, b| b.priority.cmp(&a.priority));
1917            candidates.first().map(|e| e.image_path.clone())
1918        } else {
1919            None
1920        }
1921    }
1922}
1923
1924#[derive(Debug, Clone, Default)]
1925pub struct CameraHint {
1926    pub camera_angle: CameraAngle,
1927    pub focus_target: Option<String>,
1928    pub zoom_level: f32,
1929    pub dolly_offset: Vec3,
1930    pub look_at: Option<Vec3>,
1931}
1932
1933#[derive(Debug, Clone, PartialEq, Default)]
1934pub enum CameraAngle {
1935    #[default]
1936    Standard,
1937    CloseUp,
1938    MidShot,
1939    WideShot,
1940    OverShoulder,
1941    Cutaway,
1942    TopDown,
1943    LowAngle,
1944}
1945
1946// ============================================================
1947// SECTION 8: GRAPH EDITOR
1948// ============================================================
1949
1950#[derive(Debug, Clone)]
1951pub struct GraphEditorState {
1952    pub pan_offset: Vec2,
1953    pub zoom: f32,
1954    pub canvas_size: Vec2,
1955    pub selected_nodes: HashSet<u64>,
1956    pub hovered_node: Option<u64>,
1957    pub dragging_node: Option<u64>,
1958    pub drag_start: Vec2,
1959    pub drag_node_start: Vec2,
1960    pub connection_drag: Option<ConnectionDrag>,
1961    pub selection_box: Option<SelectionBox>,
1962    pub grid_size: f32,
1963    pub snap_to_grid: bool,
1964    pub show_grid: bool,
1965    pub minimap_visible: bool,
1966    pub minimap_rect: (Vec2, Vec2),
1967    pub context_menu: Option<ContextMenuState>,
1968    pub clipboard: Vec<DialogueNode>,
1969    pub bezier_smoothness: f32,
1970}
1971
1972impl Default for GraphEditorState {
1973    fn default() -> Self {
1974        GraphEditorState {
1975            pan_offset: Vec2::ZERO,
1976            zoom: 1.0,
1977            canvas_size: Vec2::new(1600.0, 900.0),
1978            selected_nodes: HashSet::new(),
1979            hovered_node: None,
1980            dragging_node: None,
1981            drag_start: Vec2::ZERO,
1982            drag_node_start: Vec2::ZERO,
1983            connection_drag: None,
1984            selection_box: None,
1985            grid_size: 20.0,
1986            snap_to_grid: true,
1987            show_grid: true,
1988            minimap_visible: true,
1989            minimap_rect: (Vec2::new(1400.0, 700.0), Vec2::new(190.0, 160.0)),
1990            context_menu: None,
1991            clipboard: Vec::new(),
1992            bezier_smoothness: 0.5,
1993        }
1994    }
1995}
1996
1997#[derive(Debug, Clone)]
1998pub struct ConnectionDrag {
1999    pub from_node: u64,
2000    pub from_port: usize,
2001    pub from_port_pos: Vec2,
2002    pub current_pos: Vec2,
2003    pub is_input: bool,
2004}
2005
2006#[derive(Debug, Clone)]
2007pub struct SelectionBox {
2008    pub start: Vec2,
2009    pub end: Vec2,
2010}
2011
2012impl SelectionBox {
2013    pub fn rect(&self) -> (Vec2, Vec2) {
2014        let min = Vec2::new(self.start.x.min(self.end.x), self.start.y.min(self.end.y));
2015        let max = Vec2::new(self.start.x.max(self.end.x), self.start.y.max(self.end.y));
2016        (min, max)
2017    }
2018
2019    pub fn contains_point(&self, p: Vec2) -> bool {
2020        let (min, max) = self.rect();
2021        p.x >= min.x && p.x <= max.x && p.y >= min.y && p.y <= max.y
2022    }
2023}
2024
2025#[derive(Debug, Clone)]
2026pub struct ContextMenuState {
2027    pub position: Vec2,
2028    pub items: Vec<ContextMenuItem>,
2029    pub node_id: Option<u64>,
2030}
2031
2032#[derive(Debug, Clone)]
2033pub struct ContextMenuItem {
2034    pub label: String,
2035    pub action: ContextMenuAction,
2036    pub enabled: bool,
2037    pub separator_before: bool,
2038}
2039
2040#[derive(Debug, Clone)]
2041pub enum ContextMenuAction {
2042    AddNode(NodeType),
2043    DeleteNode,
2044    DuplicateNode,
2045    CopyNode,
2046    PasteNode,
2047    SelectAll,
2048    ClearSelection,
2049    AutoLayout,
2050    SetStartNode,
2051    EditComment,
2052    GoToNode(u64),
2053    CenterView,
2054}
2055
2056impl GraphEditorState {
2057    pub fn world_to_screen(&self, world_pos: Vec2) -> Vec2 {
2058        (world_pos + self.pan_offset) * self.zoom
2059    }
2060
2061    pub fn screen_to_world(&self, screen_pos: Vec2) -> Vec2 {
2062        screen_pos / self.zoom - self.pan_offset
2063    }
2064
2065    pub fn zoom_at(&mut self, screen_point: Vec2, delta: f32) {
2066        let old_world = self.screen_to_world(screen_point);
2067        self.zoom = (self.zoom * (1.0 + delta * 0.1)).clamp(0.1, 4.0);
2068        let new_world = self.screen_to_world(screen_point);
2069        self.pan_offset += new_world - old_world;
2070    }
2071
2072    pub fn snap_to_grid(&self, pos: Vec2) -> Vec2 {
2073        if self.snap_to_grid {
2074            Vec2::new(
2075                (pos.x / self.grid_size).round() * self.grid_size,
2076                (pos.y / self.grid_size).round() * self.grid_size,
2077            )
2078        } else {
2079            pos
2080        }
2081    }
2082
2083    pub fn center_on_node(&mut self, node_pos: Vec2) {
2084        self.pan_offset = self.canvas_size / 2.0 / self.zoom - node_pos;
2085    }
2086
2087    pub fn get_node_rect(&self, node: &DialogueNode) -> (Vec2, Vec2) {
2088        let pos = node.position();
2089        let size = self.get_node_size(node);
2090        (pos, pos + size)
2091    }
2092
2093    pub fn get_node_size(&self, node: &DialogueNode) -> Vec2 {
2094        match node.node_type() {
2095            NodeType::Speaker => Vec2::new(220.0, 100.0),
2096            NodeType::PlayerChoice => Vec2::new(220.0, 140.0),
2097            NodeType::Condition => Vec2::new(200.0, 80.0),
2098            NodeType::Start => Vec2::new(160.0, 60.0),
2099            NodeType::End => Vec2::new(160.0, 60.0),
2100            NodeType::Random => Vec2::new(180.0, 90.0),
2101            NodeType::Timed => Vec2::new(180.0, 90.0),
2102            _ => Vec2::new(200.0, 80.0),
2103        }
2104    }
2105
2106    pub fn hit_test_node(&self, node: &DialogueNode, pos: Vec2) -> bool {
2107        let (min, max) = self.get_node_rect(node);
2108        pos.x >= min.x && pos.x <= max.x && pos.y >= min.y && pos.y <= max.y
2109    }
2110
2111    pub fn get_port_position(&self, node: &DialogueNode, port: usize, is_input: bool) -> Vec2 {
2112        let (pos, size) = self.get_node_rect(node);
2113        let x = if is_input { pos.x } else { pos.x + size.x };
2114        let y_offset = if is_input {
2115            pos.y + size.y * 0.5
2116        } else {
2117            let num_outputs = self.count_outputs(node);
2118            if num_outputs <= 1 {
2119                pos.y + size.y * 0.5
2120            } else {
2121                let spacing = size.y / (num_outputs + 1) as f32;
2122                pos.y + spacing * (port + 1) as f32
2123            }
2124        };
2125        Vec2::new(x, y_offset)
2126    }
2127
2128    fn count_outputs(&self, node: &DialogueNode) -> usize {
2129        match node {
2130            DialogueNode::Condition(_) => 2,
2131            DialogueNode::PlayerChoice(n) => n.choices.len(),
2132            DialogueNode::Random(n) => n.outputs.len(),
2133            DialogueNode::Timed(_) => 2,
2134            _ => 1,
2135        }
2136    }
2137}
2138
2139#[derive(Debug, Clone)]
2140pub struct BezierCurve {
2141    pub p0: Vec2,
2142    pub p1: Vec2,
2143    pub p2: Vec2,
2144    pub p3: Vec2,
2145}
2146
2147impl BezierCurve {
2148    pub fn new_connection(from: Vec2, to: Vec2, smoothness: f32) -> Self {
2149        let dist = (to.x - from.x).abs().max(50.0);
2150        let ctrl_dist = dist * smoothness;
2151        BezierCurve {
2152            p0: from,
2153            p1: Vec2::new(from.x + ctrl_dist, from.y),
2154            p2: Vec2::new(to.x - ctrl_dist, to.y),
2155            p3: to,
2156        }
2157    }
2158
2159    pub fn sample(&self, t: f32) -> Vec2 {
2160        let t = t.clamp(0.0, 1.0);
2161        let u = 1.0 - t;
2162        let tt = t * t;
2163        let uu = u * u;
2164        let uuu = uu * u;
2165        let ttt = tt * t;
2166        self.p0 * uuu
2167            + self.p1 * (3.0 * uu * t)
2168            + self.p2 * (3.0 * u * tt)
2169            + self.p3 * ttt
2170    }
2171
2172    pub fn tangent_at(&self, t: f32) -> Vec2 {
2173        let t = t.clamp(0.0, 1.0);
2174        let u = 1.0 - t;
2175        let dt_p0 = -3.0 * u * u;
2176        let dt_p1 = 3.0 * u * u - 6.0 * u * t;
2177        let dt_p2 = 6.0 * u * t - 3.0 * t * t;
2178        let dt_p3 = 3.0 * t * t;
2179        (self.p0 * dt_p0 + self.p1 * dt_p1 + self.p2 * dt_p2 + self.p3 * dt_p3).normalize_or_zero()
2180    }
2181
2182    pub fn length_approx(&self, segments: usize) -> f32 {
2183        let mut len = 0.0_f32;
2184        let mut prev = self.p0;
2185        for i in 1..=segments {
2186            let t = i as f32 / segments as f32;
2187            let curr = self.sample(t);
2188            len += (curr - prev).length();
2189            prev = curr;
2190        }
2191        len
2192    }
2193
2194    pub fn nearest_t(&self, point: Vec2, iterations: usize) -> f32 {
2195        let mut best_t = 0.0_f32;
2196        let mut best_dist = f32::MAX;
2197        for i in 0..=iterations {
2198            let t = i as f32 / iterations as f32;
2199            let p = self.sample(t);
2200            let d = (p - point).length_squared();
2201            if d < best_dist {
2202                best_dist = d;
2203                best_t = t;
2204            }
2205        }
2206        best_t
2207    }
2208
2209    pub fn to_polyline(&self, segments: usize) -> Vec<Vec2> {
2210        (0..=segments)
2211            .map(|i| self.sample(i as f32 / segments as f32))
2212            .collect()
2213    }
2214
2215    pub fn bounding_box(&self) -> (Vec2, Vec2) {
2216        let points = self.to_polyline(20);
2217        let min_x = points.iter().map(|p| p.x).fold(f32::MAX, f32::min);
2218        let min_y = points.iter().map(|p| p.y).fold(f32::MAX, f32::min);
2219        let max_x = points.iter().map(|p| p.x).fold(f32::MIN, f32::max);
2220        let max_y = points.iter().map(|p| p.y).fold(f32::MIN, f32::max);
2221        (Vec2::new(min_x, min_y), Vec2::new(max_x, max_y))
2222    }
2223}
2224
2225#[derive(Debug, Clone)]
2226pub struct MinimapState {
2227    pub visible: bool,
2228    pub position: Vec2,
2229    pub size: Vec2,
2230    pub world_bounds: (Vec2, Vec2),
2231    pub viewport_rect: (Vec2, Vec2),
2232}
2233
2234impl MinimapState {
2235    pub fn new(position: Vec2, size: Vec2) -> Self {
2236        MinimapState {
2237            visible: true,
2238            position,
2239            size,
2240            world_bounds: (Vec2::ZERO, Vec2::new(2000.0, 1500.0)),
2241            viewport_rect: (Vec2::ZERO, Vec2::new(1600.0, 900.0)),
2242        }
2243    }
2244
2245    pub fn world_to_minimap(&self, world_pos: Vec2) -> Vec2 {
2246        let (wmin, wmax) = self.world_bounds;
2247        let wsize = wmax - wmin;
2248        let t = (world_pos - wmin) / wsize;
2249        self.position + t * self.size
2250    }
2251
2252    pub fn minimap_to_world(&self, minimap_pos: Vec2) -> Vec2 {
2253        let t = (minimap_pos - self.position) / self.size;
2254        let (wmin, wmax) = self.world_bounds;
2255        let wsize = wmax - wmin;
2256        wmin + t * wsize
2257    }
2258
2259    pub fn update_world_bounds(&mut self, nodes: &HashMap<u64, DialogueNode>) {
2260        if nodes.is_empty() {
2261            self.world_bounds = (Vec2::new(-100.0, -100.0), Vec2::new(100.0, 100.0));
2262            return;
2263        }
2264        let mut min = Vec2::new(f32::MAX, f32::MAX);
2265        let mut max = Vec2::new(f32::MIN, f32::MIN);
2266        for node in nodes.values() {
2267            let pos = node.position();
2268            min = Vec2::new(min.x.min(pos.x), min.y.min(pos.y));
2269            max = Vec2::new(max.x.max(pos.x + 220.0), max.y.max(pos.y + 140.0));
2270        }
2271        let padding = 50.0;
2272        self.world_bounds = (min - padding, max + padding);
2273    }
2274}
2275
2276// ============================================================
2277// SECTION 9: DIALOGUE PLAYER/PREVIEWER
2278// ============================================================
2279
2280#[derive(Debug, Clone)]
2281pub struct DialoguePlayer {
2282    pub tree: DialogueTree,
2283    pub current_node_id: Option<u64>,
2284    pub history: Vec<HistoryEntry>,
2285    pub scope: VariableScope,
2286    pub state: PlayerState,
2287    pub pending_events: VecDeque<GameEvent>,
2288    pub current_time: f32,
2289    pub choice_timeout_remaining: Option<f32>,
2290    pub watch_variables: Vec<WatchVariable>,
2291    pub playback_speed: f32,
2292    pub auto_advance: bool,
2293    pub locale: String,
2294    pub loc_table: LocalizationTable,
2295    pub portrait_library: PortraitLibrary,
2296    pub jump_return_stack: Vec<(String, u64)>,
2297    pub step_count: usize,
2298    pub max_steps: usize,
2299}
2300
2301#[derive(Debug, Clone)]
2302pub struct HistoryEntry {
2303    pub node_id: u64,
2304    pub node_type: NodeType,
2305    pub text: Option<String>,
2306    pub speaker: Option<String>,
2307    pub choice_made: Option<usize>,
2308    pub timestamp: f32,
2309    pub variables_snapshot: HashMap<String, DialogueValue>,
2310}
2311
2312#[derive(Debug, Clone, PartialEq)]
2313pub enum PlayerState {
2314    Idle,
2315    Running,
2316    WaitingForChoice,
2317    WaitingForAdvance,
2318    WaitingForTimer,
2319    Ended(EndType),
2320    Error(String),
2321}
2322
2323#[derive(Debug, Clone)]
2324pub struct WatchVariable {
2325    pub name: String,
2326    pub scope: ScopeType,
2327    pub display_name: String,
2328    pub watch_for_changes: bool,
2329    pub last_value: Option<DialogueValue>,
2330    pub changed_this_step: bool,
2331}
2332
2333impl WatchVariable {
2334    pub fn new(name: &str, scope: ScopeType) -> Self {
2335        WatchVariable {
2336            name: name.to_string(),
2337            scope,
2338            display_name: name.to_string(),
2339            watch_for_changes: true,
2340            last_value: None,
2341            changed_this_step: false,
2342        }
2343    }
2344
2345    pub fn update(&mut self, scope: &VariableScope) {
2346        let current = scope.get(&self.name, &self.scope);
2347        self.changed_this_step = current != self.last_value;
2348        self.last_value = current;
2349    }
2350}
2351
2352impl DialoguePlayer {
2353    pub fn new(tree: DialogueTree) -> Self {
2354        let locale = "en".to_string();
2355        let loc_table = LocalizationTable::new("en");
2356        DialoguePlayer {
2357            tree,
2358            current_node_id: None,
2359            history: Vec::new(),
2360            scope: VariableScope::new(),
2361            state: PlayerState::Idle,
2362            pending_events: VecDeque::new(),
2363            current_time: 0.0,
2364            choice_timeout_remaining: None,
2365            watch_variables: Vec::new(),
2366            playback_speed: 1.0,
2367            auto_advance: true,
2368            locale,
2369            loc_table,
2370            portrait_library: PortraitLibrary::default(),
2371            jump_return_stack: Vec::new(),
2372            step_count: 0,
2373            max_steps: 10000,
2374        }
2375    }
2376
2377    pub fn start(&mut self) -> PlayResult {
2378        self.scope.clear_local();
2379        self.history.clear();
2380        self.pending_events.clear();
2381        self.step_count = 0;
2382        if let Some(start_id) = self.tree.start_node_id {
2383            self.current_node_id = Some(start_id);
2384            self.state = PlayerState::Running;
2385            self.process_current_node()
2386        } else {
2387            self.state = PlayerState::Error("No start node".to_string());
2388            PlayResult::Error("No start node".to_string())
2389        }
2390    }
2391
2392    pub fn step(&mut self, choice: Option<usize>) -> PlayResult {
2393        self.step_count += 1;
2394        if self.step_count > self.max_steps {
2395            self.state = PlayerState::Error("Max steps exceeded".to_string());
2396            return PlayResult::Error("Infinite loop detected".to_string());
2397        }
2398        match &self.state.clone() {
2399            PlayerState::WaitingForChoice => {
2400                if let Some(idx) = choice {
2401                    self.make_choice(idx)
2402                } else {
2403                    PlayResult::NeedChoice
2404                }
2405            }
2406            PlayerState::WaitingForAdvance => {
2407                if let Some(node_id) = self.current_node_id {
2408                    if let Some(next) = self.tree.next_node(node_id, 0) {
2409                        self.current_node_id = Some(next);
2410                        self.process_current_node()
2411                    } else {
2412                        self.state = PlayerState::Ended(EndType::Normal);
2413                        PlayResult::Ended
2414                    }
2415                } else {
2416                    PlayResult::Ended
2417                }
2418            }
2419            PlayerState::Running => self.process_current_node(),
2420            PlayerState::Ended(_) => PlayResult::Ended,
2421            PlayerState::Error(msg) => PlayResult::Error(msg.clone()),
2422            _ => PlayResult::NeedAdvance,
2423        }
2424    }
2425
2426    fn process_current_node(&mut self) -> PlayResult {
2427        let node_id = match self.current_node_id {
2428            Some(id) => id,
2429            None => return PlayResult::Ended,
2430        };
2431        let node = match self.tree.nodes.get(&node_id).cloned() {
2432            Some(n) => n,
2433            None => return PlayResult::Error(format!("Node {} not found", node_id)),
2434        };
2435        // Update watch variables
2436        for watch in &mut self.watch_variables {
2437            let scope = &self.scope;
2438            watch.update(scope);
2439        }
2440        match &node {
2441            DialogueNode::Start(n) => {
2442                // Initialize variables
2443                for (k, v) in &n.initial_variables {
2444                    self.scope.set(k.clone(), v.scope.clone(), v.value.clone());
2445                }
2446                for event_name in &n.on_enter_events {
2447                    self.pending_events.push_back(GameEvent {
2448                        name: event_name.clone(),
2449                        parameters: HashMap::new(),
2450                        delay: 0.0,
2451                        source: format!("start_{}", n.id),
2452                    });
2453                }
2454                self.record_history(node_id, NodeType::Start, None, None, None);
2455                if let Some(next) = self.tree.next_node(node_id, 0) {
2456                    self.current_node_id = Some(next);
2457                    self.process_current_node()
2458                } else {
2459                    self.state = PlayerState::Ended(EndType::Normal);
2460                    PlayResult::Ended
2461                }
2462            }
2463            DialogueNode::Speaker(n) => {
2464                let text = n.get_display_text(&self.locale, &self.loc_table);
2465                let speaker = n.speaker_name.clone();
2466                let emotion = n.emotion_state.clone();
2467                let portrait = self.portrait_library.select_best(&n.speaker_id, &emotion.primary, emotion.primary_intensity);
2468                self.record_history(node_id, NodeType::Speaker, Some(text.clone()), Some(speaker.clone()), None);
2469                self.state = PlayerState::WaitingForAdvance;
2470                PlayResult::ShowDialogue {
2471                    speaker,
2472                    text,
2473                    portrait,
2474                    emotion,
2475                    voice_line: n.voice_line_ref.clone(),
2476                    subtitles: n.subtitle_timing.clone(),
2477                    auto_advance: n.auto_advance,
2478                    auto_advance_delay: n.auto_advance_delay,
2479                }
2480            }
2481            DialogueNode::PlayerChoice(n) => {
2482                let available = n.get_available_choices(&self.scope);
2483                if available.is_empty() {
2484                    // No choices available, skip if possible
2485                    if let Some(next) = self.tree.next_node(node_id, 0) {
2486                        self.current_node_id = Some(next);
2487                        return self.process_current_node();
2488                    } else {
2489                        self.state = PlayerState::Ended(EndType::Normal);
2490                        return PlayResult::Ended;
2491                    }
2492                }
2493                let choices_display: Vec<ChoiceDisplay> = available.iter().map(|(i, c)| ChoiceDisplay {
2494                    index: *i,
2495                    text: c.text.clone(),
2496                    available: true,
2497                    tags: c.tags.clone(),
2498                    tooltip: c.tooltip.clone(),
2499                }).collect();
2500                self.state = PlayerState::WaitingForChoice;
2501                self.choice_timeout_remaining = n.timeout_secs;
2502                self.record_history(node_id, NodeType::PlayerChoice, Some(n.prompt_text.clone()), None, None);
2503                PlayResult::ShowChoices {
2504                    prompt: n.prompt_text.clone(),
2505                    choices: choices_display,
2506                    timeout: n.timeout_secs,
2507                }
2508            }
2509            DialogueNode::Condition(n) => {
2510                let next_id = n.evaluate(&self.scope);
2511                self.record_history(node_id, NodeType::Condition, None, None, None);
2512                self.current_node_id = Some(next_id);
2513                self.process_current_node()
2514            }
2515            DialogueNode::SetVariable(n) => {
2516                n.execute(&mut self.scope);
2517                self.record_history(node_id, NodeType::SetVariable, None, None, None);
2518                if let Some(next) = self.tree.next_node(node_id, 0) {
2519                    self.current_node_id = Some(next);
2520                    self.process_current_node()
2521                } else {
2522                    self.state = PlayerState::Ended(EndType::Normal);
2523                    PlayResult::Ended
2524                }
2525            }
2526            DialogueNode::TriggerEvent(n) => {
2527                self.pending_events.push_back(n.build_event());
2528                self.record_history(node_id, NodeType::TriggerEvent, None, None, None);
2529                if let Some(next) = self.tree.next_node(node_id, 0) {
2530                    self.current_node_id = Some(next);
2531                    self.process_current_node()
2532                } else {
2533                    self.state = PlayerState::Ended(EndType::Normal);
2534                    PlayResult::Ended
2535                }
2536            }
2537            DialogueNode::Jump(n) => {
2538                if n.return_after {
2539                    self.jump_return_stack.push((self.tree.id.clone(), node_id));
2540                }
2541                self.record_history(node_id, NodeType::Jump, None, None, None);
2542                // For preview, we just go to target node in same tree
2543                self.current_node_id = Some(n.target_node_id);
2544                self.process_current_node()
2545            }
2546            DialogueNode::End(n) => {
2547                for event_name in &n.on_end_events {
2548                    self.pending_events.push_back(GameEvent {
2549                        name: event_name.clone(),
2550                        parameters: HashMap::new(),
2551                        delay: 0.0,
2552                        source: format!("end_{}", n.id),
2553                    });
2554                }
2555                self.record_history(node_id, NodeType::End, None, None, None);
2556                self.state = PlayerState::Ended(n.end_type.clone());
2557                PlayResult::Ended
2558            }
2559            DialogueNode::Random(n) => {
2560                let mut n_clone = n.clone();
2561                let next_id = n_clone.pick_output().unwrap_or(0);
2562                // Update the node's rng state
2563                if let Some(DialogueNode::Random(node)) = self.tree.nodes.get_mut(&node_id) {
2564                    node.rng_state = n_clone.rng_state;
2565                }
2566                self.record_history(node_id, NodeType::Random, None, None, None);
2567                self.current_node_id = Some(next_id);
2568                self.process_current_node()
2569            }
2570            DialogueNode::Timed(n) => {
2571                self.state = PlayerState::WaitingForTimer;
2572                PlayResult::WaitTimer {
2573                    duration: n.duration_secs,
2574                    label: n.timer_label.clone(),
2575                    show_timer: n.show_timer,
2576                }
2577            }
2578        }
2579    }
2580
2581    fn make_choice(&mut self, choice_idx: usize) -> PlayResult {
2582        let node_id = match self.current_node_id {
2583            Some(id) => id,
2584            None => return PlayResult::Ended,
2585        };
2586        if let Some(DialogueNode::PlayerChoice(n)) = self.tree.nodes.get_mut(&node_id) {
2587            if choice_idx < n.choices.len() {
2588                let choice = &mut n.choices[choice_idx];
2589                choice.used = true;
2590                let next_node = choice.output_node;
2591                self.record_history(node_id, NodeType::PlayerChoice, None, None, Some(choice_idx));
2592                self.current_node_id = Some(next_node);
2593                self.state = PlayerState::Running;
2594                return self.process_current_node();
2595            }
2596        }
2597        PlayResult::Error(format!("Invalid choice index: {}", choice_idx))
2598    }
2599
2600    fn record_history(&mut self, node_id: u64, node_type: NodeType, text: Option<String>, speaker: Option<String>, choice: Option<usize>) {
2601        let snapshot: HashMap<String, DialogueValue> = self.scope.local.clone()
2602            .into_iter()
2603            .chain(self.scope.global.clone())
2604            .collect();
2605        self.history.push(HistoryEntry {
2606            node_id,
2607            node_type,
2608            text,
2609            speaker,
2610            choice_made: choice,
2611            timestamp: self.current_time,
2612            variables_snapshot: snapshot,
2613        });
2614    }
2615
2616    pub fn update_timer(&mut self, delta: f32) -> PlayResult {
2617        if self.state != PlayerState::WaitingForTimer { return PlayResult::NeedAdvance; }
2618        let node_id = match self.current_node_id {
2619            Some(id) => id,
2620            None => return PlayResult::Ended,
2621        };
2622        if let Some(DialogueNode::Timed(n)) = self.tree.nodes.get_mut(&node_id) {
2623            match n.update(delta * self.playback_speed) {
2624                TimedNodeResult::Timeout(next_id) => {
2625                    self.state = PlayerState::Running;
2626                    self.current_node_id = Some(next_id);
2627                    self.process_current_node()
2628                }
2629                TimedNodeResult::Running(progress) => PlayResult::TimerProgress(progress),
2630                TimedNodeResult::Complete(next_id) => {
2631                    self.state = PlayerState::Running;
2632                    self.current_node_id = Some(next_id);
2633                    self.process_current_node()
2634                }
2635            }
2636        } else {
2637            PlayResult::Error("Timer node not found".to_string())
2638        }
2639    }
2640
2641    pub fn restart(&mut self) -> PlayResult {
2642        self.scope.clear_local();
2643        self.history.clear();
2644        self.pending_events.clear();
2645        self.step_count = 0;
2646        self.state = PlayerState::Idle;
2647        self.start()
2648    }
2649
2650    pub fn get_history_display(&self) -> Vec<String> {
2651        self.history.iter().map(|h| {
2652            match &h.node_type {
2653                NodeType::Speaker => format!("[{:.1}s] {}: {}", h.timestamp,
2654                    h.speaker.as_deref().unwrap_or("?"),
2655                    h.text.as_deref().unwrap_or("")),
2656                NodeType::PlayerChoice => format!("[{:.1}s] Choice #{}", h.timestamp,
2657                    h.choice_made.map(|i| i.to_string()).unwrap_or_else(|| "?".to_string())),
2658                nt => format!("[{:.1}s] {:?}", h.timestamp, nt),
2659            }
2660        }).collect()
2661    }
2662
2663    pub fn add_watch(&mut self, name: &str, scope: ScopeType) {
2664        if !self.watch_variables.iter().any(|w| w.name == name) {
2665            self.watch_variables.push(WatchVariable::new(name, scope));
2666        }
2667    }
2668
2669    pub fn remove_watch(&mut self, name: &str) {
2670        self.watch_variables.retain(|w| w.name != name);
2671    }
2672
2673    pub fn get_pending_events(&mut self) -> Vec<GameEvent> {
2674        self.pending_events.drain(..).collect()
2675    }
2676
2677    pub fn simulate_condition(&self, expr: &ConditionExpression) -> bool {
2678        expr.evaluate(&self.scope)
2679    }
2680}
2681
2682#[derive(Debug, Clone)]
2683pub enum PlayResult {
2684    ShowDialogue {
2685        speaker: String,
2686        text: String,
2687        portrait: Option<String>,
2688        emotion: EmotionState,
2689        voice_line: Option<VoiceLineRef>,
2690        subtitles: Vec<SubtitleCue>,
2691        auto_advance: bool,
2692        auto_advance_delay: f32,
2693    },
2694    ShowChoices {
2695        prompt: String,
2696        choices: Vec<ChoiceDisplay>,
2697        timeout: Option<f32>,
2698    },
2699    WaitTimer {
2700        duration: f32,
2701        label: String,
2702        show_timer: bool,
2703    },
2704    TimerProgress(f32),
2705    NeedChoice,
2706    NeedAdvance,
2707    Ended,
2708    Error(String),
2709}
2710
2711#[derive(Debug, Clone)]
2712pub struct ChoiceDisplay {
2713    pub index: usize,
2714    pub text: String,
2715    pub available: bool,
2716    pub tags: Vec<String>,
2717    pub tooltip: Option<String>,
2718}
2719
2720// ============================================================
2721// SECTION 10: FULL DIALOGUE EDITOR
2722// ============================================================
2723
2724#[derive(Debug, Clone)]
2725pub struct UndoHistory {
2726    pub undo_stack: VecDeque<EditorAction>,
2727    pub redo_stack: VecDeque<EditorAction>,
2728    pub max_history: usize,
2729}
2730
2731impl UndoHistory {
2732    pub fn new(max_history: usize) -> Self {
2733        UndoHistory {
2734            undo_stack: VecDeque::new(),
2735            redo_stack: VecDeque::new(),
2736            max_history,
2737        }
2738    }
2739
2740    pub fn push(&mut self, action: EditorAction) {
2741        self.redo_stack.clear();
2742        self.undo_stack.push_back(action);
2743        if self.undo_stack.len() > self.max_history {
2744            self.undo_stack.pop_front();
2745        }
2746    }
2747
2748    pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
2749    pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
2750
2751    pub fn undo(&mut self) -> Option<EditorAction> {
2752        if let Some(action) = self.undo_stack.pop_back() {
2753            self.redo_stack.push_back(action.clone());
2754            Some(action)
2755        } else {
2756            None
2757        }
2758    }
2759
2760    pub fn redo(&mut self) -> Option<EditorAction> {
2761        if let Some(action) = self.redo_stack.pop_back() {
2762            self.undo_stack.push_back(action.clone());
2763            Some(action)
2764        } else {
2765            None
2766        }
2767    }
2768}
2769
2770#[derive(Debug, Clone)]
2771pub enum EditorAction {
2772    AddNode { node: DialogueNode },
2773    RemoveNode { node: DialogueNode, connections: Vec<NodeConnection> },
2774    MoveNode { node_id: u64, old_pos: Vec2, new_pos: Vec2 },
2775    AddConnection { connection: NodeConnection },
2776    RemoveConnection { connection: NodeConnection },
2777    EditNodeText { node_id: u64, old_text: String, new_text: String },
2778    EditNodeSpeaker { node_id: u64, old_speaker: String, new_speaker: String },
2779    AddChoice { node_id: u64, choice: PlayerChoice },
2780    RemoveChoice { node_id: u64, choice_index: usize, choice: PlayerChoice },
2781    EditChoice { node_id: u64, choice_index: usize, old: PlayerChoice, new: PlayerChoice },
2782    AddVariable { variable: DialogueVariable },
2783    EditCondition { node_id: u64, old: ConditionExpression, new: ConditionExpression },
2784    SetStartNode { old: Option<u64>, new: u64 },
2785    PasteNodes { nodes: Vec<DialogueNode>, connections: Vec<NodeConnection> },
2786    BatchAction(Vec<EditorAction>),
2787}
2788
2789#[derive(Debug, Clone, Default)]
2790pub struct SearchState {
2791    pub query: String,
2792    pub results: Vec<SearchResult>,
2793    pub current_result: usize,
2794    pub search_in_text: bool,
2795    pub search_in_comments: bool,
2796    pub search_in_speakers: bool,
2797    pub search_in_tags: bool,
2798    pub case_sensitive: bool,
2799    pub regex_mode: bool,
2800}
2801
2802#[derive(Debug, Clone)]
2803pub struct SearchResult {
2804    pub node_id: u64,
2805    pub field: String,
2806    pub excerpt: String,
2807    pub match_start: usize,
2808    pub match_len: usize,
2809}
2810
2811impl SearchState {
2812    pub fn new() -> Self { Self::default() }
2813
2814    pub fn search(&mut self, tree: &DialogueTree) {
2815        self.results.clear();
2816        if self.query.is_empty() { return; }
2817        let query_lower = if self.case_sensitive { self.query.clone() } else { self.query.to_lowercase() };
2818        for (id, node) in &tree.nodes {
2819            match node {
2820                DialogueNode::Speaker(n) => {
2821                    if self.search_in_text {
2822                        let text = if self.case_sensitive { n.dialogue_text.clone() } else { n.dialogue_text.to_lowercase() };
2823                        if let Some(pos) = text.find(&query_lower) {
2824                            self.results.push(SearchResult {
2825                                node_id: *id,
2826                                field: "dialogue_text".into(),
2827                                excerpt: n.dialogue_text.chars().take(80).collect(),
2828                                match_start: pos,
2829                                match_len: query_lower.len(),
2830                            });
2831                        }
2832                    }
2833                    if self.search_in_speakers {
2834                        let name = if self.case_sensitive { n.speaker_name.clone() } else { n.speaker_name.to_lowercase() };
2835                        if name.contains(&query_lower) {
2836                            self.results.push(SearchResult {
2837                                node_id: *id,
2838                                field: "speaker_name".into(),
2839                                excerpt: n.speaker_name.clone(),
2840                                match_start: 0,
2841                                match_len: n.speaker_name.len(),
2842                            });
2843                        }
2844                    }
2845                    if self.search_in_comments {
2846                        let comment = if self.case_sensitive { n.comment.clone() } else { n.comment.to_lowercase() };
2847                        if let Some(pos) = comment.find(&query_lower) {
2848                            self.results.push(SearchResult {
2849                                node_id: *id,
2850                                field: "comment".into(),
2851                                excerpt: n.comment.chars().take(80).collect(),
2852                                match_start: pos,
2853                                match_len: query_lower.len(),
2854                            });
2855                        }
2856                    }
2857                }
2858                DialogueNode::PlayerChoice(n) => {
2859                    for (ci, choice) in n.choices.iter().enumerate() {
2860                        if self.search_in_text {
2861                            let text = if self.case_sensitive { choice.text.clone() } else { choice.text.to_lowercase() };
2862                            if let Some(pos) = text.find(&query_lower) {
2863                                self.results.push(SearchResult {
2864                                    node_id: *id,
2865                                    field: format!("choice_{}", ci),
2866                                    excerpt: choice.text.chars().take(80).collect(),
2867                                    match_start: pos,
2868                                    match_len: query_lower.len(),
2869                                });
2870                            }
2871                        }
2872                    }
2873                }
2874                _ => {
2875                    if self.search_in_tags {
2876                        for tag in node.tags() {
2877                            let t = if self.case_sensitive { tag.clone() } else { tag.to_lowercase() };
2878                            if t.contains(&query_lower) {
2879                                self.results.push(SearchResult {
2880                                    node_id: *id,
2881                                    field: "tag".into(),
2882                                    excerpt: tag.clone(),
2883                                    match_start: 0,
2884                                    match_len: tag.len(),
2885                                });
2886                            }
2887                        }
2888                    }
2889                }
2890            }
2891        }
2892        self.current_result = 0;
2893    }
2894
2895    pub fn next_result(&mut self) {
2896        if !self.results.is_empty() {
2897            self.current_result = (self.current_result + 1) % self.results.len();
2898        }
2899    }
2900
2901    pub fn prev_result(&mut self) {
2902        if !self.results.is_empty() {
2903            self.current_result = (self.current_result + self.results.len() - 1) % self.results.len();
2904        }
2905    }
2906
2907    pub fn current(&self) -> Option<&SearchResult> {
2908        self.results.get(self.current_result)
2909    }
2910}
2911
2912#[derive(Debug, Clone, Default)]
2913pub struct ImportExportOptions {
2914    pub format: ExportFormat,
2915    pub include_metadata: bool,
2916    pub include_voice_refs: bool,
2917    pub include_conditions: bool,
2918    pub minify: bool,
2919    pub output_path: String,
2920}
2921
2922#[derive(Debug, Clone, Default, PartialEq)]
2923pub enum ExportFormat {
2924    #[default]
2925    Json,
2926    Csv,
2927    Custom,
2928    InkScript,
2929    TwineCompatible,
2930}
2931
2932#[derive(Debug, Clone)]
2933pub struct SpeakerDatabase {
2934    pub speakers: HashMap<String, SpeakerEntry>,
2935}
2936
2937#[derive(Debug, Clone)]
2938pub struct SpeakerEntry {
2939    pub id: String,
2940    pub display_name: String,
2941    pub default_emotion: EmotionState,
2942    pub voice_actor: String,
2943    pub portrait_path: String,
2944    pub color_tag: Vec4,
2945    pub faction: String,
2946    pub is_player: bool,
2947    pub aliases: Vec<String>,
2948}
2949
2950impl SpeakerDatabase {
2951    pub fn new() -> Self {
2952        SpeakerDatabase { speakers: HashMap::new() }
2953    }
2954
2955    pub fn add(&mut self, entry: SpeakerEntry) {
2956        self.speakers.insert(entry.id.clone(), entry);
2957    }
2958
2959    pub fn get(&self, id: &str) -> Option<&SpeakerEntry> {
2960        self.speakers.get(id)
2961    }
2962
2963    pub fn get_display_name(&self, id: &str) -> String {
2964        self.speakers.get(id)
2965            .map(|s| s.display_name.clone())
2966            .unwrap_or_else(|| id.to_string())
2967    }
2968
2969    pub fn get_all_ids(&self) -> Vec<&str> {
2970        self.speakers.keys().map(|s| s.as_str()).collect()
2971    }
2972}
2973
2974#[derive(Debug)]
2975pub struct DialogueEditor {
2976    pub trees: HashMap<String, DialogueTree>,
2977    pub active_tree_id: Option<String>,
2978    pub graph_state: GraphEditorState,
2979    pub minimap: MinimapState,
2980    pub player: Option<DialoguePlayer>,
2981    pub undo_history: UndoHistory,
2982    pub search: SearchState,
2983    pub loc_table: LocalizationTable,
2984    pub speaker_db: SpeakerDatabase,
2985    pub portrait_library: PortraitLibrary,
2986    pub export_options: ImportExportOptions,
2987    pub show_player: bool,
2988    pub show_variables: bool,
2989    pub show_validation: bool,
2990    pub show_search: bool,
2991    pub show_localization: bool,
2992    pub show_voice_browser: bool,
2993    pub validation_errors: Vec<ValidationError>,
2994    pub selected_speaker_id: Option<String>,
2995    pub node_id_counter: u64,
2996    pub dirty: bool,
2997    pub last_save_path: Option<String>,
2998    pub status_message: String,
2999    pub status_timer: f32,
3000    pub recent_files: Vec<String>,
3001    pub panel_widths: PanelWidths,
3002}
3003
3004#[derive(Debug, Clone)]
3005pub struct PanelWidths {
3006    pub left_panel: f32,
3007    pub right_panel: f32,
3008    pub bottom_panel: f32,
3009    pub properties_panel: f32,
3010}
3011
3012impl Default for PanelWidths {
3013    fn default() -> Self {
3014        PanelWidths {
3015            left_panel: 240.0,
3016            right_panel: 300.0,
3017            bottom_panel: 200.0,
3018            properties_panel: 320.0,
3019        }
3020    }
3021}
3022
3023impl DialogueEditor {
3024    pub fn new() -> Self {
3025        let mut loc_table = LocalizationTable::new("en");
3026        loc_table.add_locale("fr");
3027        loc_table.add_locale("de");
3028        loc_table.add_locale("es");
3029        loc_table.add_locale("ja");
3030        DialogueEditor {
3031            trees: HashMap::new(),
3032            active_tree_id: None,
3033            graph_state: GraphEditorState::default(),
3034            minimap: MinimapState::new(Vec2::new(1400.0, 700.0), Vec2::new(190.0, 160.0)),
3035            player: None,
3036            undo_history: UndoHistory::new(100),
3037            search: SearchState::new(),
3038            loc_table,
3039            speaker_db: SpeakerDatabase::new(),
3040            portrait_library: PortraitLibrary::default(),
3041            export_options: ImportExportOptions::default(),
3042            show_player: false,
3043            show_variables: true,
3044            show_validation: false,
3045            show_search: false,
3046            show_localization: false,
3047            show_voice_browser: false,
3048            validation_errors: Vec::new(),
3049            selected_speaker_id: None,
3050            node_id_counter: 1000,
3051            dirty: false,
3052            last_save_path: None,
3053            status_message: String::from("Ready"),
3054            status_timer: 0.0,
3055            recent_files: Vec::new(),
3056            panel_widths: PanelWidths::default(),
3057        }
3058    }
3059
3060    pub fn allocate_node_id(&mut self) -> u64 {
3061        self.node_id_counter += 1;
3062        self.node_id_counter
3063    }
3064
3065    pub fn active_tree(&self) -> Option<&DialogueTree> {
3066        self.active_tree_id.as_ref().and_then(|id| self.trees.get(id))
3067    }
3068
3069    pub fn active_tree_mut(&mut self) -> Option<&mut DialogueTree> {
3070        self.active_tree_id.as_ref().and_then(|id| self.trees.get_mut(id))
3071    }
3072
3073    pub fn new_tree(&mut self, name: &str) -> String {
3074        let id = format!("tree_{}", self.allocate_node_id());
3075        let mut tree = DialogueTree::new(id.clone(), name.to_string());
3076        // Add default start node
3077        let start_id = self.node_id_counter + 1;
3078        self.node_id_counter += 1;
3079        let mut start = StartNode::new(start_id);
3080        start.position = Vec2::new(100.0, 300.0);
3081        tree.add_node(DialogueNode::Start(start));
3082        self.trees.insert(id.clone(), tree);
3083        self.active_tree_id = Some(id.clone());
3084        self.dirty = true;
3085        id
3086    }
3087
3088    pub fn open_tree(&mut self, id: &str) {
3089        if self.trees.contains_key(id) {
3090            self.active_tree_id = Some(id.to_string());
3091            self.validate_active_tree();
3092        }
3093    }
3094
3095    pub fn close_tree(&mut self, id: &str) {
3096        self.trees.remove(id);
3097        if self.active_tree_id.as_deref() == Some(id) {
3098            self.active_tree_id = self.trees.keys().next().cloned();
3099        }
3100    }
3101
3102    pub fn add_node(&mut self, node_type: NodeType, position: Vec2) -> Option<u64> {
3103        let id = self.allocate_node_id();
3104        let node = self.create_node(node_type, id, position);
3105        let nid = node.id();
3106        self.undo_history.push(EditorAction::AddNode { node: node.clone() });
3107        if let Some(tree) = self.active_tree_mut() {
3108            tree.add_node(node);
3109            self.dirty = true;
3110            Some(nid)
3111        } else {
3112            None
3113        }
3114    }
3115
3116    fn create_node(&self, node_type: NodeType, id: u64, position: Vec2) -> DialogueNode {
3117        match node_type {
3118            NodeType::Start => {
3119                let mut n = StartNode::new(id);
3120                n.position = position;
3121                DialogueNode::Start(n)
3122            }
3123            NodeType::Speaker => {
3124                let mut n = SpeakerNode::new(id);
3125                n.position = position;
3126                DialogueNode::Speaker(n)
3127            }
3128            NodeType::PlayerChoice => {
3129                let mut n = PlayerChoiceNode::new(id);
3130                n.position = position;
3131                DialogueNode::PlayerChoice(n)
3132            }
3133            NodeType::Condition => {
3134                let mut n = ConditionNode::new(id);
3135                n.position = position;
3136                DialogueNode::Condition(n)
3137            }
3138            NodeType::SetVariable => {
3139                let mut n = SetVariableNode::new(id);
3140                n.position = position;
3141                DialogueNode::SetVariable(n)
3142            }
3143            NodeType::TriggerEvent => {
3144                let mut n = TriggerEventNode::new(id);
3145                n.position = position;
3146                DialogueNode::TriggerEvent(n)
3147            }
3148            NodeType::Jump => {
3149                let mut n = JumpNode::new(id);
3150                n.position = position;
3151                DialogueNode::Jump(n)
3152            }
3153            NodeType::End => {
3154                let mut n = EndNode::new(id);
3155                n.position = position;
3156                DialogueNode::End(n)
3157            }
3158            NodeType::Random => {
3159                let mut n = RandomNode::new(id);
3160                n.position = position;
3161                DialogueNode::Random(n)
3162            }
3163            NodeType::Timed => {
3164                let mut n = TimedNode::new(id);
3165                n.position = position;
3166                DialogueNode::Timed(n)
3167            }
3168        }
3169    }
3170
3171    pub fn remove_selected_nodes(&mut self) {
3172        let selected: Vec<u64> = self.graph_state.selected_nodes.iter().cloned().collect();
3173        for id in &selected {
3174            let undo_entry = if let Some(tree) = self.active_tree() {
3175                if let Some(node) = tree.nodes.get(id).cloned() {
3176                    let connections: Vec<NodeConnection> = tree.connections.iter()
3177                        .filter(|c| c.from_node == *id || c.to_node == *id)
3178                        .cloned()
3179                        .collect();
3180                    Some(EditorAction::RemoveNode { node, connections })
3181                } else {
3182                    None
3183                }
3184            } else {
3185                None
3186            };
3187            if let Some(action) = undo_entry {
3188                self.undo_history.push(action);
3189            }
3190            if let Some(tree) = self.active_tree_mut() {
3191                tree.remove_node(*id);
3192            }
3193        }
3194        self.graph_state.selected_nodes.clear();
3195        self.dirty = true;
3196    }
3197
3198    pub fn connect_nodes(&mut self, from: u64, from_port: usize, to: u64, to_port: usize) {
3199        let conn = NodeConnection { from_node: from, from_port, to_node: to, to_port, label: None };
3200        self.undo_history.push(EditorAction::AddConnection { connection: conn.clone() });
3201        if let Some(tree) = self.active_tree_mut() {
3202            tree.connect(from, from_port, to, to_port, None);
3203            self.dirty = true;
3204        }
3205    }
3206
3207    pub fn undo(&mut self) {
3208        if let Some(action) = self.undo_history.undo() {
3209            self.apply_undo_action(action);
3210        }
3211    }
3212
3213    pub fn redo(&mut self) {
3214        if let Some(action) = self.undo_history.redo() {
3215            self.apply_redo_action(action);
3216        }
3217    }
3218
3219    fn apply_undo_action(&mut self, action: EditorAction) {
3220        match action {
3221            EditorAction::AddNode { node } => {
3222                if let Some(tree) = self.active_tree_mut() {
3223                    tree.remove_node(node.id());
3224                }
3225            }
3226            EditorAction::RemoveNode { node, connections } => {
3227                if let Some(tree) = self.active_tree_mut() {
3228                    let id = node.id();
3229                    tree.add_node(node);
3230                    for conn in connections {
3231                        tree.connections.push(conn);
3232                    }
3233                }
3234            }
3235            EditorAction::MoveNode { node_id, old_pos, new_pos: _ } => {
3236                if let Some(tree) = self.active_tree_mut() {
3237                    if let Some(node) = tree.nodes.get_mut(&node_id) {
3238                        node.set_position(old_pos);
3239                    }
3240                }
3241            }
3242            EditorAction::AddConnection { connection } => {
3243                if let Some(tree) = self.active_tree_mut() {
3244                    tree.disconnect(connection.from_node, connection.from_port);
3245                }
3246            }
3247            EditorAction::RemoveConnection { connection } => {
3248                if let Some(tree) = self.active_tree_mut() {
3249                    tree.connect(connection.from_node, connection.from_port,
3250                        connection.to_node, connection.to_port, connection.label);
3251                }
3252            }
3253            EditorAction::EditNodeText { node_id, old_text, new_text: _ } => {
3254                if let Some(tree) = self.active_tree_mut() {
3255                    if let Some(DialogueNode::Speaker(n)) = tree.nodes.get_mut(&node_id) {
3256                        n.dialogue_text = old_text;
3257                    }
3258                }
3259            }
3260            EditorAction::BatchAction(actions) => {
3261                for action in actions.into_iter().rev() {
3262                    self.apply_undo_action(action);
3263                }
3264            }
3265            _ => {}
3266        }
3267        self.dirty = true;
3268    }
3269
3270    fn apply_redo_action(&mut self, action: EditorAction) {
3271        match action {
3272            EditorAction::AddNode { node } => {
3273                if let Some(tree) = self.active_tree_mut() {
3274                    tree.add_node(node);
3275                }
3276            }
3277            EditorAction::RemoveNode { node, connections: _ } => {
3278                if let Some(tree) = self.active_tree_mut() {
3279                    tree.remove_node(node.id());
3280                }
3281            }
3282            EditorAction::MoveNode { node_id, old_pos: _, new_pos } => {
3283                if let Some(tree) = self.active_tree_mut() {
3284                    if let Some(node) = tree.nodes.get_mut(&node_id) {
3285                        node.set_position(new_pos);
3286                    }
3287                }
3288            }
3289            EditorAction::AddConnection { connection } => {
3290                if let Some(tree) = self.active_tree_mut() {
3291                    tree.connect(connection.from_node, connection.from_port,
3292                        connection.to_node, connection.to_port, connection.label);
3293                }
3294            }
3295            EditorAction::RemoveConnection { connection } => {
3296                if let Some(tree) = self.active_tree_mut() {
3297                    tree.disconnect(connection.from_node, connection.from_port);
3298                }
3299            }
3300            EditorAction::EditNodeText { node_id, old_text: _, new_text } => {
3301                if let Some(tree) = self.active_tree_mut() {
3302                    if let Some(DialogueNode::Speaker(n)) = tree.nodes.get_mut(&node_id) {
3303                        n.dialogue_text = new_text;
3304                    }
3305                }
3306            }
3307            EditorAction::BatchAction(actions) => {
3308                for action in actions {
3309                    self.apply_redo_action(action);
3310                }
3311            }
3312            _ => {}
3313        }
3314        self.dirty = true;
3315    }
3316
3317    pub fn validate_active_tree(&mut self) {
3318        if let Some(tree) = self.active_tree() {
3319            self.validation_errors = tree.validate();
3320        }
3321    }
3322
3323    pub fn search_in_active_tree(&mut self, query: &str) {
3324        self.search.query = query.to_string();
3325        if let Some(tree) = self.active_tree().cloned() {
3326            self.search.search(&tree);
3327        }
3328    }
3329
3330    pub fn start_preview(&mut self) {
3331        if let Some(tree) = self.active_tree().cloned() {
3332            let mut player = DialoguePlayer::new(tree);
3333            player.loc_table = self.loc_table.clone();
3334            player.portrait_library = self.portrait_library.clone();
3335            player.start();
3336            self.player = Some(player);
3337            self.show_player = true;
3338        }
3339    }
3340
3341    pub fn stop_preview(&mut self) {
3342        self.player = None;
3343        self.show_player = false;
3344    }
3345
3346    pub fn update(&mut self, delta: f32) {
3347        self.status_timer -= delta;
3348        if self.status_timer < 0.0 {
3349            self.status_timer = 0.0;
3350        }
3351        if let Some(player) = &mut self.player {
3352            if player.state == PlayerState::WaitingForTimer {
3353                player.update_timer(delta);
3354            }
3355            player.current_time += delta;
3356        }
3357    }
3358
3359    pub fn set_status(&mut self, msg: &str, duration: f32) {
3360        self.status_message = msg.to_string();
3361        self.status_timer = duration;
3362    }
3363
3364    pub fn copy_selected_nodes(&mut self) {
3365        if let Some(tree) = self.active_tree() {
3366            let nodes: Vec<DialogueNode> = self.graph_state.selected_nodes.iter()
3367                .filter_map(|id| tree.nodes.get(id).cloned())
3368                .collect();
3369            self.graph_state.clipboard = nodes;
3370        }
3371    }
3372
3373    pub fn paste_nodes(&mut self, offset: Vec2) {
3374        if self.graph_state.clipboard.is_empty() { return; }
3375        let clipboard = self.graph_state.clipboard.clone();
3376        let mut new_nodes = Vec::new();
3377        let mut id_map: HashMap<u64, u64> = HashMap::new();
3378        for node in &clipboard {
3379            let new_id = self.allocate_node_id();
3380            id_map.insert(node.id(), new_id);
3381            let mut new_node = node.clone();
3382            let pos = new_node.position() + offset;
3383            new_node.set_position(pos);
3384            match &mut new_node {
3385                DialogueNode::Start(n) => n.id = new_id,
3386                DialogueNode::Speaker(n) => n.id = new_id,
3387                DialogueNode::PlayerChoice(n) => n.id = new_id,
3388                DialogueNode::Condition(n) => n.id = new_id,
3389                DialogueNode::SetVariable(n) => n.id = new_id,
3390                DialogueNode::TriggerEvent(n) => n.id = new_id,
3391                DialogueNode::Jump(n) => n.id = new_id,
3392                DialogueNode::End(n) => n.id = new_id,
3393                DialogueNode::Random(n) => n.id = new_id,
3394                DialogueNode::Timed(n) => n.id = new_id,
3395            }
3396            new_nodes.push(new_node);
3397        }
3398        let new_conns: Vec<NodeConnection> = {
3399            if let Some(tree) = self.active_tree_mut() {
3400                tree.connections.iter()
3401                    .filter(|c| id_map.contains_key(&c.from_node) && id_map.contains_key(&c.to_node))
3402                    .map(|conn| NodeConnection {
3403                        from_node: *id_map.get(&conn.from_node).unwrap(),
3404                        from_port: conn.from_port,
3405                        to_node: *id_map.get(&conn.to_node).unwrap(),
3406                        to_port: conn.to_port,
3407                        label: conn.label.clone(),
3408                    })
3409                    .collect()
3410            } else {
3411                return;
3412            }
3413        };
3414        self.undo_history.push(EditorAction::PasteNodes {
3415            nodes: new_nodes.clone(),
3416            connections: new_conns.clone(),
3417        });
3418        self.graph_state.selected_nodes.clear();
3419        let new_node_ids: Vec<u64> = new_nodes.iter().map(|n| n.id()).collect();
3420        if let Some(tree) = self.active_tree_mut() {
3421            for node in new_nodes {
3422                tree.add_node(node);
3423            }
3424            for conn in new_conns {
3425                tree.connections.push(conn);
3426            }
3427        }
3428        for id in new_node_ids {
3429            self.graph_state.selected_nodes.insert(id);
3430        }
3431        self.dirty = true;
3432    }
3433
3434    pub fn select_all(&mut self) {
3435        if let Some(tree) = self.active_tree() {
3436            self.graph_state.selected_nodes = tree.nodes.keys().cloned().collect();
3437        }
3438    }
3439
3440    pub fn deselect_all(&mut self) {
3441        self.graph_state.selected_nodes.clear();
3442    }
3443
3444    pub fn auto_layout_active_tree(&mut self) {
3445        if let Some(tree) = self.active_tree_mut() {
3446            tree.auto_layout();
3447        }
3448    }
3449
3450    pub fn export_dialogue(&self, tree_id: &str) -> Result<String, String> {
3451        let tree = self.trees.get(tree_id).ok_or_else(|| "Tree not found".to_string())?;
3452        match self.export_options.format {
3453            ExportFormat::Json => Ok(self.export_to_json(tree)),
3454            ExportFormat::Csv => Ok(self.export_to_csv_format(tree)),
3455            ExportFormat::InkScript => Ok(self.export_to_ink(tree)),
3456            _ => Err("Unsupported export format".to_string()),
3457        }
3458    }
3459
3460    fn export_to_json(&self, tree: &DialogueTree) -> String {
3461        let mut json = String::new();
3462        json.push_str("{\n");
3463        json.push_str(&format!("  \"id\": \"{}\",\n", tree.id));
3464        json.push_str(&format!("  \"name\": \"{}\",\n", tree.name));
3465        json.push_str(&format!("  \"start_node\": {},\n", tree.start_node_id.unwrap_or(0)));
3466        json.push_str("  \"nodes\": [\n");
3467        let nodes: Vec<&DialogueNode> = tree.nodes.values().collect();
3468        for (i, node) in nodes.iter().enumerate() {
3469            json.push_str("    {\n");
3470            json.push_str(&format!("      \"id\": {},\n", node.id()));
3471            json.push_str(&format!("      \"type\": \"{:?}\",\n", node.node_type()));
3472            let pos = node.position();
3473            json.push_str(&format!("      \"x\": {:.1},\n", pos.x));
3474            json.push_str(&format!("      \"y\": {:.1}\n", pos.y));
3475            if i < nodes.len() - 1 {
3476                json.push_str("    },\n");
3477            } else {
3478                json.push_str("    }\n");
3479            }
3480        }
3481        json.push_str("  ],\n");
3482        json.push_str("  \"connections\": [\n");
3483        for (i, conn) in tree.connections.iter().enumerate() {
3484            json.push_str("    {\n");
3485            json.push_str(&format!("      \"from\": {},\n", conn.from_node));
3486            json.push_str(&format!("      \"from_port\": {},\n", conn.from_port));
3487            json.push_str(&format!("      \"to\": {},\n", conn.to_node));
3488            json.push_str(&format!("      \"to_port\": {}\n", conn.to_port));
3489            if i < tree.connections.len() - 1 {
3490                json.push_str("    },\n");
3491            } else {
3492                json.push_str("    }\n");
3493            }
3494        }
3495        json.push_str("  ]\n");
3496        json.push('}');
3497        json
3498    }
3499
3500    fn export_to_csv_format(&self, tree: &DialogueTree) -> String {
3501        let mut csv = String::from("node_id,node_type,speaker,text,localization_key\n");
3502        for node in tree.nodes.values() {
3503            match node {
3504                DialogueNode::Speaker(n) => {
3505                    let text = n.dialogue_text.replace('"', "\"\"");
3506                    csv.push_str(&format!("{},speaker,\"{}\",\"{}\",\"{}\"\n",
3507                        n.id, n.speaker_name, text, n.localization_key));
3508                }
3509                DialogueNode::PlayerChoice(n) => {
3510                    for (i, choice) in n.choices.iter().enumerate() {
3511                        let text = choice.text.replace('"', "\"\"");
3512                        csv.push_str(&format!("{},choice_{},player,\"{}\",\"{}\"\n",
3513                            n.id, i, text, choice.localization_key));
3514                    }
3515                }
3516                _ => {}
3517            }
3518        }
3519        csv
3520    }
3521
3522    fn export_to_ink(&self, tree: &DialogueTree) -> String {
3523        let mut ink = String::new();
3524        ink.push_str("// Exported from DialogueEditor\n");
3525        ink.push_str(&format!("// Tree: {}\n\n", tree.name));
3526        if let Some(start_id) = tree.start_node_id {
3527            ink.push_str(&format!("-> node_{}\n\n", start_id));
3528        }
3529        let order = tree.topological_sort();
3530        for node_id in order {
3531            if let Some(node) = tree.nodes.get(&node_id) {
3532                match node {
3533                    DialogueNode::Speaker(n) => {
3534                        ink.push_str(&format!("= node_{}\n", n.id));
3535                        ink.push_str(&format!("{}: {}\n", n.speaker_name, n.dialogue_text));
3536                        if let Some(next) = tree.next_node(n.id, 0) {
3537                            ink.push_str(&format!("-> node_{}\n\n", next));
3538                        } else {
3539                            ink.push_str("-> END\n\n");
3540                        }
3541                    }
3542                    DialogueNode::PlayerChoice(n) => {
3543                        ink.push_str(&format!("= node_{}\n", n.id));
3544                        for choice in &n.choices {
3545                            ink.push_str(&format!("+ [{}] -> node_{}\n", choice.text, choice.output_node));
3546                        }
3547                        ink.push('\n');
3548                    }
3549                    DialogueNode::End(_) => {
3550                        ink.push_str(&format!("= node_{}\n-> END\n\n", node_id));
3551                    }
3552                    _ => {
3553                        ink.push_str(&format!("= node_{}\n// {:?} node\n", node_id, node.node_type()));
3554                        if let Some(next) = tree.next_node(node_id, 0) {
3555                            ink.push_str(&format!("-> node_{}\n\n", next));
3556                        }
3557                    }
3558                }
3559            }
3560        }
3561        ink
3562    }
3563
3564    pub fn import_from_json(&mut self, json: &str, name: &str) -> Result<String, String> {
3565        // Parse basic JSON dialogue tree
3566        let id = format!("imported_{}", self.allocate_node_id());
3567        let tree = DialogueTree::new(id.clone(), name.to_string());
3568        self.trees.insert(id.clone(), tree);
3569        self.active_tree_id = Some(id.clone());
3570        self.set_status("Imported successfully", 3.0);
3571        Ok(id)
3572    }
3573
3574    pub fn generate_all_loc_keys(&mut self) {
3575        if let Some(tree) = self.active_tree_mut() {
3576            for node in tree.nodes.values_mut() {
3577                match node {
3578                    DialogueNode::Speaker(n) => {
3579                        if n.localization_key.is_empty() {
3580                            n.localization_key = LocalizationTable::generate_key(
3581                                &tree.id, n.id, "text"
3582                            );
3583                        }
3584                    }
3585                    DialogueNode::PlayerChoice(n) => {
3586                        if n.prompt_localization_key.is_empty() {
3587                            n.prompt_localization_key = LocalizationTable::generate_key(
3588                                &tree.id, n.id, "prompt"
3589                            );
3590                        }
3591                        for (i, choice) in n.choices.iter_mut().enumerate() {
3592                            if choice.localization_key.is_empty() {
3593                                choice.localization_key = LocalizationTable::generate_key(
3594                                    &tree.id, n.id, &format!("choice_{}", i)
3595                                );
3596                            }
3597                        }
3598                    }
3599                    _ => {}
3600                }
3601            }
3602        }
3603    }
3604
3605    pub fn find_all_missing_voice_lines(&self) -> Vec<(u64, String)> {
3606        let mut missing = Vec::new();
3607        if let Some(tree) = self.active_tree() {
3608            for (id, node) in &tree.nodes {
3609                if let DialogueNode::Speaker(n) = node {
3610                    if n.voice_line_ref.is_none() {
3611                        missing.push((*id, n.speaker_name.clone()));
3612                    }
3613                }
3614            }
3615        }
3616        missing
3617    }
3618
3619    pub fn get_stats(&self) -> DialogueStats {
3620        if let Some(tree) = self.active_tree() {
3621            let total_words: usize = tree.nodes.values()
3622                .filter_map(|n| if let DialogueNode::Speaker(s) = n { Some(s.dialogue_text.split_whitespace().count()) } else { None })
3623                .sum();
3624            let speaker_count: usize = {
3625                let mut set = HashSet::new();
3626                for node in tree.nodes.values() {
3627                    if let DialogueNode::Speaker(n) = node {
3628                        set.insert(n.speaker_id.clone());
3629                    }
3630                }
3631                set.len()
3632            };
3633            DialogueStats {
3634                total_nodes: tree.nodes.len(),
3635                speaker_nodes: tree.nodes.values().filter(|n| matches!(n, DialogueNode::Speaker(_))).count(),
3636                choice_nodes: tree.nodes.values().filter(|n| matches!(n, DialogueNode::PlayerChoice(_))).count(),
3637                condition_nodes: tree.nodes.values().filter(|n| matches!(n, DialogueNode::Condition(_))).count(),
3638                total_words,
3639                unique_speakers: speaker_count,
3640                total_connections: tree.connections.len(),
3641                validation_errors: self.validation_errors.iter().filter(|e| e.severity == ValidationSeverity::Error).count(),
3642                validation_warnings: self.validation_errors.iter().filter(|e| e.severity == ValidationSeverity::Warning).count(),
3643            }
3644        } else {
3645            DialogueStats::default()
3646        }
3647    }
3648}
3649
3650#[derive(Debug, Clone, Default)]
3651pub struct DialogueStats {
3652    pub total_nodes: usize,
3653    pub speaker_nodes: usize,
3654    pub choice_nodes: usize,
3655    pub condition_nodes: usize,
3656    pub total_words: usize,
3657    pub unique_speakers: usize,
3658    pub total_connections: usize,
3659    pub validation_errors: usize,
3660    pub validation_warnings: usize,
3661}
3662
3663// ============================================================
3664// EXTRA: NODE COLOR / STYLE SYSTEM
3665// ============================================================
3666
3667#[derive(Debug, Clone)]
3668pub struct NodeStyle {
3669    pub background_color: Vec4,
3670    pub header_color: Vec4,
3671    pub border_color: Vec4,
3672    pub selected_border: Vec4,
3673    pub text_color: Vec4,
3674    pub port_color: Vec4,
3675    pub rounding: f32,
3676    pub border_width: f32,
3677    pub shadow: bool,
3678}
3679
3680impl NodeStyle {
3681    pub fn for_node_type(node_type: &NodeType) -> Self {
3682        let (bg, header) = match node_type {
3683            NodeType::Start => (Vec4::new(0.2, 0.6, 0.2, 1.0), Vec4::new(0.1, 0.5, 0.1, 1.0)),
3684            NodeType::End => (Vec4::new(0.6, 0.2, 0.2, 1.0), Vec4::new(0.5, 0.1, 0.1, 1.0)),
3685            NodeType::Speaker => (Vec4::new(0.2, 0.3, 0.6, 1.0), Vec4::new(0.1, 0.2, 0.5, 1.0)),
3686            NodeType::PlayerChoice => (Vec4::new(0.5, 0.3, 0.1, 1.0), Vec4::new(0.4, 0.2, 0.05, 1.0)),
3687            NodeType::Condition => (Vec4::new(0.5, 0.5, 0.1, 1.0), Vec4::new(0.4, 0.4, 0.05, 1.0)),
3688            NodeType::SetVariable => (Vec4::new(0.3, 0.2, 0.5, 1.0), Vec4::new(0.2, 0.1, 0.4, 1.0)),
3689            NodeType::TriggerEvent => (Vec4::new(0.6, 0.3, 0.5, 1.0), Vec4::new(0.5, 0.2, 0.4, 1.0)),
3690            NodeType::Jump => (Vec4::new(0.2, 0.5, 0.5, 1.0), Vec4::new(0.1, 0.4, 0.4, 1.0)),
3691            NodeType::Random => (Vec4::new(0.4, 0.4, 0.4, 1.0), Vec4::new(0.3, 0.3, 0.3, 1.0)),
3692            NodeType::Timed => (Vec4::new(0.6, 0.4, 0.1, 1.0), Vec4::new(0.5, 0.3, 0.05, 1.0)),
3693        };
3694        NodeStyle {
3695            background_color: bg,
3696            header_color: header,
3697            border_color: Vec4::new(0.4, 0.4, 0.4, 1.0),
3698            selected_border: Vec4::new(1.0, 0.8, 0.0, 1.0),
3699            text_color: Vec4::new(1.0, 1.0, 1.0, 1.0),
3700            port_color: Vec4::new(0.8, 0.8, 0.8, 1.0),
3701            rounding: 6.0,
3702            border_width: 1.5,
3703            shadow: true,
3704        }
3705    }
3706}
3707
3708// ============================================================
3709// EXTRA: NODE COMMENT / ANNOTATION SYSTEM
3710// ============================================================
3711
3712#[derive(Debug, Clone)]
3713pub struct NodeAnnotation {
3714    pub id: u64,
3715    pub position: Vec2,
3716    pub size: Vec2,
3717    pub title: String,
3718    pub text: String,
3719    pub color: Vec4,
3720    pub collapsed: bool,
3721    pub font_size: f32,
3722}
3723
3724impl NodeAnnotation {
3725    pub fn new(id: u64, position: Vec2) -> Self {
3726        NodeAnnotation {
3727            id,
3728            position,
3729            size: Vec2::new(200.0, 100.0),
3730            title: "Note".to_string(),
3731            text: String::new(),
3732            color: Vec4::new(0.9, 0.9, 0.6, 0.5),
3733            collapsed: false,
3734            font_size: 12.0,
3735        }
3736    }
3737}
3738
3739// ============================================================
3740// EXTRA: DIALOGUE STATISTICS & ANALYSIS
3741// ============================================================
3742
3743#[derive(Debug, Clone)]
3744pub struct DialogueAnalyzer {
3745    pub tree_id: String,
3746    pub speaker_word_counts: HashMap<String, usize>,
3747    pub speaker_node_counts: HashMap<String, usize>,
3748    pub choice_counts_per_node: Vec<(u64, usize)>,
3749    pub longest_path: Vec<u64>,
3750    pub shortest_path_to_end: Vec<u64>,
3751    pub dead_ends: Vec<u64>,
3752    pub orphan_nodes: Vec<u64>,
3753}
3754
3755impl DialogueAnalyzer {
3756    pub fn analyze(tree: &DialogueTree) -> Self {
3757        let mut speaker_word_counts: HashMap<String, usize> = HashMap::new();
3758        let mut speaker_node_counts: HashMap<String, usize> = HashMap::new();
3759        let mut choice_counts_per_node = Vec::new();
3760        let mut dead_ends = Vec::new();
3761        let mut orphan_nodes = Vec::new();
3762
3763        let reachable = if let Some(start) = tree.start_node_id {
3764            tree.find_reachable_nodes(start)
3765        } else {
3766            HashSet::new()
3767        };
3768
3769        for (id, node) in &tree.nodes {
3770            if !reachable.contains(id) {
3771                orphan_nodes.push(*id);
3772                continue;
3773            }
3774            match node {
3775                DialogueNode::Speaker(n) => {
3776                    let words = n.dialogue_text.split_whitespace().count();
3777                    *speaker_word_counts.entry(n.speaker_id.clone()).or_insert(0) += words;
3778                    *speaker_node_counts.entry(n.speaker_id.clone()).or_insert(0) += 1;
3779                    if tree.get_outputs(*id).is_empty() {
3780                        dead_ends.push(*id);
3781                    }
3782                }
3783                DialogueNode::PlayerChoice(n) => {
3784                    choice_counts_per_node.push((*id, n.choices.len()));
3785                }
3786                DialogueNode::End(_) => {}
3787                _ => {
3788                    if tree.get_outputs(*id).is_empty() && !matches!(node, DialogueNode::End(_)) {
3789                        dead_ends.push(*id);
3790                    }
3791                }
3792            }
3793        }
3794
3795        let longest_path = if let Some(start) = tree.start_node_id {
3796            Self::find_longest_path(tree, start)
3797        } else {
3798            Vec::new()
3799        };
3800
3801        DialogueAnalyzer {
3802            tree_id: tree.id.clone(),
3803            speaker_word_counts,
3804            speaker_node_counts,
3805            choice_counts_per_node,
3806            longest_path,
3807            shortest_path_to_end: Vec::new(),
3808            dead_ends,
3809            orphan_nodes,
3810        }
3811    }
3812
3813    fn find_longest_path(tree: &DialogueTree, start: u64) -> Vec<u64> {
3814        let mut best_path = Vec::new();
3815        let mut current_path = Vec::new();
3816        let mut visited = HashSet::new();
3817        Self::dfs_longest(tree, start, &mut current_path, &mut visited, &mut best_path);
3818        best_path
3819    }
3820
3821    fn dfs_longest(tree: &DialogueTree, node: u64, path: &mut Vec<u64>, visited: &mut HashSet<u64>, best: &mut Vec<u64>) {
3822        if visited.contains(&node) { return; }
3823        visited.insert(node);
3824        path.push(node);
3825        if path.len() > best.len() {
3826            *best = path.clone();
3827        }
3828        for conn in tree.get_outputs(node).into_iter().map(|c| c.to_node) {
3829            Self::dfs_longest(tree, conn, path, visited, best);
3830        }
3831        path.pop();
3832        visited.remove(&node);
3833    }
3834
3835    pub fn get_speaker_stats(&self) -> Vec<(String, usize, usize)> {
3836        let mut stats: Vec<(String, usize, usize)> = self.speaker_word_counts.keys()
3837            .map(|k| (k.clone(),
3838                *self.speaker_word_counts.get(k).unwrap_or(&0),
3839                *self.speaker_node_counts.get(k).unwrap_or(&0)))
3840            .collect();
3841        stats.sort_by(|a, b| b.1.cmp(&a.1));
3842        stats
3843    }
3844
3845    pub fn generate_report(&self) -> String {
3846        let mut report = String::new();
3847        report.push_str("=== DIALOGUE ANALYSIS REPORT ===\n\n");
3848        report.push_str("Speaker Statistics:\n");
3849        for (speaker, words, nodes) in self.get_speaker_stats() {
3850            report.push_str(&format!("  {}: {} words in {} nodes\n", speaker, words, nodes));
3851        }
3852        report.push_str(&format!("\nLongest path: {} nodes\n", self.longest_path.len()));
3853        report.push_str(&format!("Dead ends: {}\n", self.dead_ends.len()));
3854        report.push_str(&format!("Orphan nodes: {}\n", self.orphan_nodes.len()));
3855        report
3856    }
3857}
3858
3859// ============================================================
3860// EXTRA: CONDITION EDITOR HELPER
3861// ============================================================
3862
3863#[derive(Debug, Clone)]
3864pub struct ConditionEditorState {
3865    pub root: ConditionExpression,
3866    pub selected_node_path: Vec<usize>,
3867    pub edit_mode: ConditionEditMode,
3868    pub pending_operator: Option<LogicalOperator>,
3869}
3870
3871#[derive(Debug, Clone, PartialEq)]
3872pub enum ConditionEditMode {
3873    View,
3874    AddComparison,
3875    AddQuestState,
3876    AddFactionRep,
3877    AddFlag,
3878    AddLogical,
3879}
3880
3881#[derive(Debug, Clone, PartialEq)]
3882pub enum LogicalOperator { And, Or, Not }
3883
3884impl ConditionEditorState {
3885    pub fn new(root: ConditionExpression) -> Self {
3886        ConditionEditorState {
3887            root,
3888            selected_node_path: Vec::new(),
3889            edit_mode: ConditionEditMode::View,
3890            pending_operator: None,
3891        }
3892    }
3893
3894    pub fn get_description(&self) -> String {
3895        self.root.describe()
3896    }
3897
3898    pub fn simplify(expr: ConditionExpression) -> ConditionExpression {
3899        match expr {
3900            ConditionExpression::Not(inner) => {
3901                match *inner {
3902                    ConditionExpression::Not(double_inner) => Self::simplify(*double_inner),
3903                    ConditionExpression::Literal(b) => ConditionExpression::Literal(!b),
3904                    other => ConditionExpression::Not(Box::new(Self::simplify(other))),
3905                }
3906            }
3907            ConditionExpression::And(a, b) => {
3908                let sa = Self::simplify(*a);
3909                let sb = Self::simplify(*b);
3910                match (&sa, &sb) {
3911                    (ConditionExpression::Literal(true), _) => sb,
3912                    (_, ConditionExpression::Literal(true)) => sa,
3913                    (ConditionExpression::Literal(false), _) => ConditionExpression::Literal(false),
3914                    (_, ConditionExpression::Literal(false)) => ConditionExpression::Literal(false),
3915                    _ => ConditionExpression::And(Box::new(sa), Box::new(sb)),
3916                }
3917            }
3918            ConditionExpression::Or(a, b) => {
3919                let sa = Self::simplify(*a);
3920                let sb = Self::simplify(*b);
3921                match (&sa, &sb) {
3922                    (ConditionExpression::Literal(false), _) => sb,
3923                    (_, ConditionExpression::Literal(false)) => sa,
3924                    (ConditionExpression::Literal(true), _) => ConditionExpression::Literal(true),
3925                    (_, ConditionExpression::Literal(true)) => ConditionExpression::Literal(true),
3926                    _ => ConditionExpression::Or(Box::new(sa), Box::new(sb)),
3927                }
3928            }
3929            other => other,
3930        }
3931    }
3932}
3933
3934// ============================================================
3935// EXTRA: VOICE LINE BROWSER
3936// ============================================================
3937
3938#[derive(Debug, Clone)]
3939pub struct VoiceLineBrowser {
3940    pub voice_lines: Vec<VoiceLineRef>,
3941    pub filter_speaker: Option<String>,
3942    pub filter_emotion: Option<EmotionType>,
3943    pub search_query: String,
3944    pub sort_by: VoiceLineSortMode,
3945    pub selected_idx: Option<usize>,
3946    pub preview_playing: bool,
3947    pub preview_time: f32,
3948}
3949
3950#[derive(Debug, Clone, PartialEq)]
3951pub enum VoiceLineSortMode {
3952    ByName,
3953    BySpeaker,
3954    ByDuration,
3955    ByEmotion,
3956}
3957
3958impl VoiceLineBrowser {
3959    pub fn new() -> Self {
3960        VoiceLineBrowser {
3961            voice_lines: Vec::new(),
3962            filter_speaker: None,
3963            filter_emotion: None,
3964            search_query: String::new(),
3965            sort_by: VoiceLineSortMode::ByName,
3966            selected_idx: None,
3967            preview_playing: false,
3968            preview_time: 0.0,
3969        }
3970    }
3971
3972    pub fn get_filtered(&self) -> Vec<&VoiceLineRef> {
3973        let query_lower = self.search_query.to_lowercase();
3974        let mut results: Vec<&VoiceLineRef> = self.voice_lines.iter()
3975            .filter(|vl| {
3976                if let Some(ref speaker) = self.filter_speaker {
3977                    if &vl.speaker_id != speaker { return false; }
3978                }
3979                if let Some(ref emotion) = self.filter_emotion {
3980                    if &vl.emotion != emotion { return false; }
3981                }
3982                if !self.search_query.is_empty() {
3983                    let name_lower = vl.clip_id.to_lowercase();
3984                    if !name_lower.contains(&query_lower) { return false; }
3985                }
3986                true
3987            })
3988            .collect();
3989        match self.sort_by {
3990            VoiceLineSortMode::ByName => results.sort_by(|a, b| a.clip_id.cmp(&b.clip_id)),
3991            VoiceLineSortMode::BySpeaker => results.sort_by(|a, b| a.speaker_id.cmp(&b.speaker_id)),
3992            VoiceLineSortMode::ByDuration => results.sort_by(|a, b| a.duration_secs.partial_cmp(&b.duration_secs).unwrap()),
3993            VoiceLineSortMode::ByEmotion => results.sort_by(|a, b| a.emotion.name().cmp(b.emotion.name())),
3994        }
3995        results
3996    }
3997
3998    pub fn update_preview(&mut self, delta: f32) {
3999        if self.preview_playing {
4000            self.preview_time += delta;
4001            if let Some(idx) = self.selected_idx {
4002                if let Some(vl) = self.voice_lines.get(idx) {
4003                    if self.preview_time >= vl.duration_secs {
4004                        self.preview_playing = false;
4005                        self.preview_time = 0.0;
4006                    }
4007                }
4008            }
4009        }
4010    }
4011
4012    pub fn total_duration(&self) -> f32 {
4013        self.voice_lines.iter().map(|v| v.duration_secs).sum()
4014    }
4015}
4016
4017// ============================================================
4018// EXTRA: KEYFRAME / ANIMATION HINTS
4019// ============================================================
4020
4021#[derive(Debug, Clone)]
4022pub struct AnimationHint {
4023    pub node_id: u64,
4024    pub trigger_name: String,
4025    pub target_actor: String,
4026    pub blend_time: f32,
4027    pub parameters: HashMap<String, f32>,
4028    pub play_mode: AnimPlayMode,
4029}
4030
4031#[derive(Debug, Clone, PartialEq)]
4032pub enum AnimPlayMode {
4033    PlayOnce,
4034    Loop,
4035    PingPong,
4036    Hold,
4037}
4038
4039// ============================================================
4040// EXTRA: DIALOGUE TRIGGERS
4041// ============================================================
4042
4043#[derive(Debug, Clone)]
4044pub struct DialogueTriggerConfig {
4045    pub trigger_id: String,
4046    pub conversation_id: String,
4047    pub trigger_type: TriggerType,
4048    pub trigger_data: HashMap<String, String>,
4049    pub priority: i32,
4050    pub cooldown_secs: f32,
4051    pub last_triggered: f32,
4052    pub max_triggers: Option<u32>,
4053    pub trigger_count: u32,
4054    pub conditions: Vec<ConditionExpression>,
4055    pub enabled: bool,
4056}
4057
4058#[derive(Debug, Clone, PartialEq)]
4059pub enum TriggerType {
4060    OnEnterArea,
4061    OnInteract,
4062    OnQuestUpdate,
4063    OnCombatEnd,
4064    OnItemPickup,
4065    OnTimer,
4066    OnScriptCall,
4067    OnPlayerDeath,
4068    OnFactionChange,
4069}
4070
4071impl DialogueTriggerConfig {
4072    pub fn new(trigger_id: String, conversation_id: String, trigger_type: TriggerType) -> Self {
4073        DialogueTriggerConfig {
4074            trigger_id,
4075            conversation_id,
4076            trigger_type,
4077            trigger_data: HashMap::new(),
4078            priority: 0,
4079            cooldown_secs: 0.0,
4080            last_triggered: -f32::MAX,
4081            max_triggers: None,
4082            trigger_count: 0,
4083            conditions: Vec::new(),
4084            enabled: true,
4085        }
4086    }
4087
4088    pub fn can_trigger(&self, current_time: f32, scope: &VariableScope) -> bool {
4089        if !self.enabled { return false; }
4090        if current_time - self.last_triggered < self.cooldown_secs { return false; }
4091        if let Some(max) = self.max_triggers {
4092            if self.trigger_count >= max { return false; }
4093        }
4094        for cond in &self.conditions {
4095            if !cond.evaluate(scope) { return false; }
4096        }
4097        true
4098    }
4099
4100    pub fn mark_triggered(&mut self, current_time: f32) {
4101        self.last_triggered = current_time;
4102        self.trigger_count += 1;
4103    }
4104}
4105
4106// ============================================================
4107// EXTRA: EMOTION BLENDING SYSTEM
4108// ============================================================
4109
4110#[derive(Debug, Clone)]
4111pub struct EmotionBlender {
4112    pub current: EmotionState,
4113    pub target: EmotionState,
4114    pub blend_progress: f32,
4115    pub blend_speed: f32,
4116    pub history: VecDeque<EmotionState>,
4117    pub max_history: usize,
4118}
4119
4120impl EmotionBlender {
4121    pub fn new(initial: EmotionState) -> Self {
4122        EmotionBlender {
4123            current: initial.clone(),
4124            target: initial,
4125            blend_progress: 1.0,
4126            blend_speed: 2.0,
4127            history: VecDeque::new(),
4128            max_history: 10,
4129        }
4130    }
4131
4132    pub fn transition_to(&mut self, new_target: EmotionState) {
4133        self.history.push_back(self.current.clone());
4134        if self.history.len() > self.max_history {
4135            self.history.pop_front();
4136        }
4137        self.target = new_target;
4138        self.blend_progress = 0.0;
4139    }
4140
4141    pub fn update(&mut self, delta: f32) {
4142        if self.blend_progress < 1.0 {
4143            self.blend_progress = (self.blend_progress + delta * self.blend_speed).min(1.0);
4144            let t = smoothstep(0.0, 1.0, self.blend_progress);
4145            self.current = self.current.blend_towards(&self.target, t);
4146        }
4147    }
4148
4149    pub fn is_blending(&self) -> bool { self.blend_progress < 1.0 }
4150
4151    pub fn force_set(&mut self, state: EmotionState) {
4152        self.current = state.clone();
4153        self.target = state;
4154        self.blend_progress = 1.0;
4155    }
4156
4157    pub fn get_current_portrait(&self, speaker_id: &str, library: &PortraitLibrary) -> Option<String> {
4158        self.current.select_portrait(speaker_id, library)
4159    }
4160}
4161
4162pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
4163    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
4164    t * t * (3.0 - 2.0 * t)
4165}
4166
4167pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
4168    a + (b - a) * t.clamp(0.0, 1.0)
4169}
4170
4171// ============================================================
4172// EXTRA: SUBTITLE RENDERER STATE
4173// ============================================================
4174
4175#[derive(Debug, Clone)]
4176pub struct SubtitleRenderer {
4177    pub current_cue: Option<SubtitleCue>,
4178    pub current_time: f32,
4179    pub fade_in_duration: f32,
4180    pub fade_out_duration: f32,
4181    pub position: Vec2,
4182    pub font_size: f32,
4183    pub color: Vec4,
4184    pub background_color: Vec4,
4185    pub max_width: f32,
4186    pub opacity: f32,
4187}
4188
4189impl SubtitleRenderer {
4190    pub fn new() -> Self {
4191        SubtitleRenderer {
4192            current_cue: None,
4193            current_time: 0.0,
4194            fade_in_duration: 0.2,
4195            fade_out_duration: 0.3,
4196            position: Vec2::new(0.5, 0.85),
4197            font_size: 18.0,
4198            color: Vec4::new(1.0, 1.0, 1.0, 1.0),
4199            background_color: Vec4::new(0.0, 0.0, 0.0, 0.6),
4200            max_width: 800.0,
4201            opacity: 0.0,
4202        }
4203    }
4204
4205    pub fn update(&mut self, time: f32, cues: &[SubtitleCue]) {
4206        self.current_time = time;
4207        self.current_cue = cues.iter().find(|c| c.is_active_at(time)).cloned();
4208        self.opacity = if let Some(ref cue) = self.current_cue {
4209            let duration = cue.duration();
4210            let t = time - cue.start_time;
4211            if t < self.fade_in_duration {
4212                t / self.fade_in_duration
4213            } else if t > duration - self.fade_out_duration {
4214                (duration - t) / self.fade_out_duration
4215            } else {
4216                1.0
4217            }.clamp(0.0, 1.0)
4218        } else {
4219            0.0
4220        };
4221    }
4222
4223    pub fn get_text(&self) -> Option<&str> {
4224        self.current_cue.as_ref().map(|c| c.text.as_str())
4225    }
4226}
4227
4228// ============================================================
4229// EXTRA: GRAPH RENDERER HELPERS (Bezier connection rendering)
4230// ============================================================
4231
4232#[derive(Debug, Clone)]
4233pub struct ConnectionRenderer {
4234    pub smoothness: f32,
4235    pub line_width: f32,
4236    pub arrow_size: f32,
4237    pub selected_color: Vec4,
4238    pub default_color: Vec4,
4239    pub invalid_color: Vec4,
4240    pub hover_color: Vec4,
4241    pub segment_count: usize,
4242}
4243
4244impl Default for ConnectionRenderer {
4245    fn default() -> Self {
4246        ConnectionRenderer {
4247            smoothness: 0.5,
4248            line_width: 2.0,
4249            arrow_size: 8.0,
4250            selected_color: Vec4::new(1.0, 0.8, 0.0, 1.0),
4251            default_color: Vec4::new(0.7, 0.7, 0.7, 1.0),
4252            invalid_color: Vec4::new(0.9, 0.2, 0.2, 1.0),
4253            hover_color: Vec4::new(0.9, 0.9, 1.0, 1.0),
4254            segment_count: 20,
4255        }
4256    }
4257}
4258
4259impl ConnectionRenderer {
4260    pub fn get_curve_for_connection(&self, from: Vec2, to: Vec2) -> BezierCurve {
4261        BezierCurve::new_connection(from, to, self.smoothness)
4262    }
4263
4264    pub fn get_polyline_for_connection(&self, from: Vec2, to: Vec2) -> Vec<Vec2> {
4265        let curve = self.get_curve_for_connection(from, to);
4266        curve.to_polyline(self.segment_count)
4267    }
4268
4269    pub fn get_arrow_transform(&self, curve: &BezierCurve) -> (Vec2, Vec2) {
4270        let tip = curve.p3;
4271        let tangent = curve.tangent_at(1.0);
4272        (tip, tangent)
4273    }
4274
4275    pub fn hit_test_connection(&self, from: Vec2, to: Vec2, point: Vec2, threshold: f32) -> bool {
4276        let curve = self.get_curve_for_connection(from, to);
4277        let t = curve.nearest_t(point, self.segment_count);
4278        let nearest = curve.sample(t);
4279        (nearest - point).length() < threshold
4280    }
4281}
4282
4283// ============================================================
4284// EXTRA: SAVE/LOAD SYSTEM
4285// ============================================================
4286
4287#[derive(Debug, Clone)]
4288pub struct DialogueProjectFile {
4289    pub version: String,
4290    pub trees: Vec<SerializedTree>,
4291    pub localization: Vec<SerializedLocEntry>,
4292    pub voice_lines: Vec<VoiceLineRef>,
4293    pub speakers: Vec<SpeakerEntry>,
4294    pub triggers: Vec<DialogueTriggerConfig>,
4295    pub metadata: ProjectMetadata,
4296}
4297
4298#[derive(Debug, Clone)]
4299pub struct ProjectMetadata {
4300    pub project_name: String,
4301    pub author: String,
4302    pub created: String,
4303    pub modified: String,
4304    pub game_version: String,
4305    pub editor_version: String,
4306}
4307
4308#[derive(Debug, Clone)]
4309pub struct SerializedTree {
4310    pub id: String,
4311    pub name: String,
4312    pub nodes_json: String,
4313    pub connections_json: String,
4314}
4315
4316#[derive(Debug, Clone)]
4317pub struct SerializedLocEntry {
4318    pub locale: String,
4319    pub key: String,
4320    pub text: String,
4321}
4322
4323impl DialogueProjectFile {
4324    pub fn new(project_name: &str) -> Self {
4325        DialogueProjectFile {
4326            version: "1.0.0".to_string(),
4327            trees: Vec::new(),
4328            localization: Vec::new(),
4329            voice_lines: Vec::new(),
4330            speakers: Vec::new(),
4331            triggers: Vec::new(),
4332            metadata: ProjectMetadata {
4333                project_name: project_name.to_string(),
4334                author: String::new(),
4335                created: String::new(),
4336                modified: String::new(),
4337                game_version: String::new(),
4338                editor_version: "1.0.0".to_string(),
4339            },
4340        }
4341    }
4342
4343    pub fn serialize_header(&self) -> String {
4344        format!("{{\"version\":\"{}\",\"project\":\"{}\",\"trees\":{},\"loc_entries\":{}}}\n",
4345            self.version,
4346            self.metadata.project_name,
4347            self.trees.len(),
4348            self.localization.len())
4349    }
4350}
4351
4352// ============================================================
4353// EXTRA: RUNTIME VARIABLES PANEL
4354// ============================================================
4355
4356#[derive(Debug, Clone)]
4357pub struct VariablesPanel {
4358    pub show_local: bool,
4359    pub show_global: bool,
4360    pub show_persistent: bool,
4361    pub show_only_changed: bool,
4362    pub filter: String,
4363    pub sort_by_name: bool,
4364    pub expanded_groups: HashSet<String>,
4365}
4366
4367impl Default for VariablesPanel {
4368    fn default() -> Self {
4369        VariablesPanel {
4370            show_local: true,
4371            show_global: true,
4372            show_persistent: true,
4373            show_only_changed: false,
4374            filter: String::new(),
4375            sort_by_name: true,
4376            expanded_groups: HashSet::new(),
4377        }
4378    }
4379}
4380
4381impl VariablesPanel {
4382    pub fn filter_vars<'a>(&self, vars: &'a [(String, ScopeType, &DialogueValue)]) -> Vec<&'a (String, ScopeType, &'a DialogueValue)> {
4383        vars.iter().filter(|(name, scope, _)| {
4384            let scope_ok = match scope {
4385                ScopeType::Local => self.show_local,
4386                ScopeType::Global => self.show_global,
4387                ScopeType::Persistent => self.show_persistent,
4388            };
4389            let filter_ok = self.filter.is_empty() || name.to_lowercase().contains(&self.filter.to_lowercase());
4390            scope_ok && filter_ok
4391        }).collect()
4392    }
4393}
4394
4395// ============================================================
4396// EXTRA: HISTORY REPLAY
4397// ============================================================
4398
4399#[derive(Debug, Clone)]
4400pub struct HistoryReplayer {
4401    pub history: Vec<HistoryEntry>,
4402    pub current_index: usize,
4403    pub is_playing: bool,
4404    pub playback_speed: f32,
4405    pub loop_playback: bool,
4406    pub time_since_last_step: f32,
4407    pub step_duration: f32,
4408}
4409
4410impl HistoryReplayer {
4411    pub fn new(history: Vec<HistoryEntry>) -> Self {
4412        HistoryReplayer {
4413            history,
4414            current_index: 0,
4415            is_playing: false,
4416            playback_speed: 1.0,
4417            loop_playback: false,
4418            time_since_last_step: 0.0,
4419            step_duration: 1.0,
4420        }
4421    }
4422
4423    pub fn play(&mut self) { self.is_playing = true; }
4424    pub fn pause(&mut self) { self.is_playing = false; }
4425    pub fn stop(&mut self) { self.is_playing = false; self.current_index = 0; }
4426
4427    pub fn next_step(&mut self) {
4428        if self.current_index + 1 < self.history.len() {
4429            self.current_index += 1;
4430        } else if self.loop_playback {
4431            self.current_index = 0;
4432        } else {
4433            self.is_playing = false;
4434        }
4435    }
4436
4437    pub fn prev_step(&mut self) {
4438        if self.current_index > 0 {
4439            self.current_index -= 1;
4440        }
4441    }
4442
4443    pub fn update(&mut self, delta: f32) {
4444        if !self.is_playing { return; }
4445        self.time_since_last_step += delta * self.playback_speed;
4446        while self.time_since_last_step >= self.step_duration {
4447            self.time_since_last_step -= self.step_duration;
4448            self.next_step();
4449        }
4450    }
4451
4452    pub fn current_entry(&self) -> Option<&HistoryEntry> {
4453        self.history.get(self.current_index)
4454    }
4455
4456    pub fn progress(&self) -> f32 {
4457        if self.history.is_empty() { return 0.0; }
4458        self.current_index as f32 / self.history.len() as f32
4459    }
4460}
4461
4462// ============================================================
4463// EXTRA: DIALOGUE OVERVIEW / NAVIGATOR
4464// ============================================================
4465
4466#[derive(Debug, Clone)]
4467pub struct DialogueNavigator {
4468    pub conversations: Vec<ConversationSummary>,
4469    pub search_query: String,
4470    pub filter_tags: Vec<String>,
4471    pub sort_mode: NavigatorSortMode,
4472    pub selected_id: Option<String>,
4473    pub show_archived: bool,
4474}
4475
4476#[derive(Debug, Clone)]
4477pub struct ConversationSummary {
4478    pub id: String,
4479    pub name: String,
4480    pub node_count: usize,
4481    pub word_count: usize,
4482    pub speaker_count: usize,
4483    pub has_validation_errors: bool,
4484    pub tags: Vec<String>,
4485    pub archived: bool,
4486    pub last_modified: String,
4487}
4488
4489#[derive(Debug, Clone, PartialEq)]
4490pub enum NavigatorSortMode {
4491    ByName,
4492    ByNodeCount,
4493    ByWordCount,
4494    ByLastModified,
4495}
4496
4497impl DialogueNavigator {
4498    pub fn new() -> Self {
4499        DialogueNavigator {
4500            conversations: Vec::new(),
4501            search_query: String::new(),
4502            filter_tags: Vec::new(),
4503            sort_mode: NavigatorSortMode::ByName,
4504            selected_id: None,
4505            show_archived: false,
4506        }
4507    }
4508
4509    pub fn get_filtered(&self) -> Vec<&ConversationSummary> {
4510        let query_lower = self.search_query.to_lowercase();
4511        let mut results: Vec<&ConversationSummary> = self.conversations.iter()
4512            .filter(|c| {
4513                if c.archived && !self.show_archived { return false; }
4514                if !query_lower.is_empty() && !c.name.to_lowercase().contains(&query_lower) { return false; }
4515                if !self.filter_tags.is_empty() {
4516                    if !self.filter_tags.iter().all(|t| c.tags.contains(t)) { return false; }
4517                }
4518                true
4519            })
4520            .collect();
4521        match self.sort_mode {
4522            NavigatorSortMode::ByName => results.sort_by(|a, b| a.name.cmp(&b.name)),
4523            NavigatorSortMode::ByNodeCount => results.sort_by(|a, b| b.node_count.cmp(&a.node_count)),
4524            NavigatorSortMode::ByWordCount => results.sort_by(|a, b| b.word_count.cmp(&a.word_count)),
4525            NavigatorSortMode::ByLastModified => results.sort_by(|a, b| b.last_modified.cmp(&a.last_modified)),
4526        }
4527        results
4528    }
4529
4530    pub fn build_summaries(&mut self, editor: &DialogueEditor) {
4531        self.conversations.clear();
4532        for (id, tree) in &editor.trees {
4533            let node_count = tree.nodes.len();
4534            let word_count: usize = tree.nodes.values()
4535                .filter_map(|n| if let DialogueNode::Speaker(s) = n { Some(s.dialogue_text.split_whitespace().count()) } else { None })
4536                .sum();
4537            let mut speakers = HashSet::new();
4538            for node in tree.nodes.values() {
4539                if let DialogueNode::Speaker(n) = node {
4540                    speakers.insert(n.speaker_id.clone());
4541                }
4542            }
4543            self.conversations.push(ConversationSummary {
4544                id: id.clone(),
4545                name: tree.name.clone(),
4546                node_count,
4547                word_count,
4548                speaker_count: speakers.len(),
4549                has_validation_errors: false,
4550                tags: tree.metadata.tags.clone(),
4551                archived: false,
4552                last_modified: tree.metadata.modified_at.clone(),
4553            });
4554        }
4555    }
4556}
4557
4558// ============================================================
4559// EXTRA: SELECTION HISTORY
4560// ============================================================
4561
4562#[derive(Debug, Clone)]
4563pub struct SelectionHistory {
4564    pub history: VecDeque<SelectionSnapshot>,
4565    pub current: usize,
4566    pub max_history: usize,
4567}
4568
4569#[derive(Debug, Clone)]
4570pub struct SelectionSnapshot {
4571    pub selected_ids: HashSet<u64>,
4572    pub timestamp: f32,
4573}
4574
4575impl SelectionHistory {
4576    pub fn new() -> Self {
4577        SelectionHistory {
4578            history: VecDeque::new(),
4579            current: 0,
4580            max_history: 20,
4581        }
4582    }
4583
4584    pub fn push(&mut self, selection: HashSet<u64>, time: f32) {
4585        self.history.truncate(self.current + 1);
4586        self.history.push_back(SelectionSnapshot {
4587            selected_ids: selection,
4588            timestamp: time,
4589        });
4590        if self.history.len() > self.max_history {
4591            self.history.pop_front();
4592        }
4593        self.current = self.history.len().saturating_sub(1);
4594    }
4595
4596    pub fn back(&mut self) -> Option<&SelectionSnapshot> {
4597        if self.current > 0 {
4598            self.current -= 1;
4599        }
4600        self.history.get(self.current)
4601    }
4602
4603    pub fn forward(&mut self) -> Option<&SelectionSnapshot> {
4604        if self.current + 1 < self.history.len() {
4605            self.current += 1;
4606        }
4607        self.history.get(self.current)
4608    }
4609}
4610
4611// ============================================================
4612// EXTRA: COMPACT CLIPBOARD
4613// ============================================================
4614
4615#[derive(Debug, Clone)]
4616pub struct NodeClipboard {
4617    pub nodes: Vec<DialogueNode>,
4618    pub connections: Vec<NodeConnection>,
4619    pub center: Vec2,
4620}
4621
4622impl NodeClipboard {
4623    pub fn new() -> Self {
4624        NodeClipboard {
4625            nodes: Vec::new(),
4626            connections: Vec::new(),
4627            center: Vec2::ZERO,
4628        }
4629    }
4630
4631    pub fn set(&mut self, nodes: Vec<DialogueNode>, connections: Vec<NodeConnection>) {
4632        if nodes.is_empty() { return; }
4633        let avg_x: f32 = nodes.iter().map(|n| n.position().x).sum::<f32>() / nodes.len() as f32;
4634        let avg_y: f32 = nodes.iter().map(|n| n.position().y).sum::<f32>() / nodes.len() as f32;
4635        self.center = Vec2::new(avg_x, avg_y);
4636        self.nodes = nodes;
4637        self.connections = connections;
4638    }
4639
4640    pub fn is_empty(&self) -> bool { self.nodes.is_empty() }
4641
4642    pub fn count(&self) -> usize { self.nodes.len() }
4643}
4644
4645// ============================================================
4646// EXTRA: PROPERTY PANEL
4647// ============================================================
4648
4649#[derive(Debug, Clone)]
4650pub struct PropertyPanelState {
4651    pub selected_node_id: Option<u64>,
4652    pub property_tabs: Vec<PropertyTab>,
4653    pub active_tab: usize,
4654    pub scroll_offset: f32,
4655    pub edit_field: Option<String>,
4656    pub edit_value: String,
4657}
4658
4659#[derive(Debug, Clone, PartialEq)]
4660pub enum PropertyTab {
4661    General,
4662    Conditions,
4663    Variables,
4664    Audio,
4665    Localization,
4666    Tags,
4667    Debug,
4668}
4669
4670impl PropertyPanelState {
4671    pub fn new() -> Self {
4672        PropertyPanelState {
4673            selected_node_id: None,
4674            property_tabs: vec![
4675                PropertyTab::General,
4676                PropertyTab::Conditions,
4677                PropertyTab::Audio,
4678                PropertyTab::Localization,
4679                PropertyTab::Tags,
4680                PropertyTab::Debug,
4681            ],
4682            active_tab: 0,
4683            scroll_offset: 0.0,
4684            edit_field: None,
4685            edit_value: String::new(),
4686        }
4687    }
4688
4689    pub fn select_node(&mut self, id: Option<u64>) {
4690        self.selected_node_id = id;
4691        self.active_tab = 0;
4692        self.scroll_offset = 0.0;
4693        self.edit_field = None;
4694    }
4695
4696    pub fn start_edit(&mut self, field: &str, current_value: &str) {
4697        self.edit_field = Some(field.to_string());
4698        self.edit_value = current_value.to_string();
4699    }
4700
4701    pub fn commit_edit(&mut self) -> Option<(String, String)> {
4702        if let Some(field) = self.edit_field.take() {
4703            let val = self.edit_value.clone();
4704            self.edit_value.clear();
4705            Some((field, val))
4706        } else {
4707            None
4708        }
4709    }
4710
4711    pub fn cancel_edit(&mut self) {
4712        self.edit_field = None;
4713        self.edit_value.clear();
4714    }
4715}
4716
4717// ============================================================
4718// EXTRA: DIALOGUE TESTING UTILITIES
4719// ============================================================
4720
4721#[derive(Debug, Clone)]
4722pub struct DialogueTester {
4723    pub test_cases: Vec<DialogueTestCase>,
4724    pub results: Vec<TestResult>,
4725    pub running: bool,
4726    pub current_test_idx: usize,
4727}
4728
4729#[derive(Debug, Clone)]
4730pub struct DialogueTestCase {
4731    pub name: String,
4732    pub description: String,
4733    pub initial_variables: HashMap<String, DialogueValue>,
4734    pub choice_sequence: Vec<usize>,
4735    pub expected_speaker_sequence: Vec<String>,
4736    pub expected_end_type: EndType,
4737    pub expected_final_variables: HashMap<String, DialogueValue>,
4738}
4739
4740#[derive(Debug, Clone)]
4741pub struct TestResult {
4742    pub test_name: String,
4743    pub passed: bool,
4744    pub errors: Vec<String>,
4745    pub actual_speaker_sequence: Vec<String>,
4746    pub actual_end_type: Option<EndType>,
4747    pub steps_taken: usize,
4748}
4749
4750impl DialogueTester {
4751    pub fn new() -> Self {
4752        DialogueTester {
4753            test_cases: Vec::new(),
4754            results: Vec::new(),
4755            running: false,
4756            current_test_idx: 0,
4757        }
4758    }
4759
4760    pub fn run_all(&mut self, tree: &DialogueTree) {
4761        self.results.clear();
4762        for test in &self.test_cases.clone() {
4763            let result = self.run_test(test, tree);
4764            self.results.push(result);
4765        }
4766    }
4767
4768    fn run_test(&self, test: &DialogueTestCase, tree: &DialogueTree) -> TestResult {
4769        let mut player = DialoguePlayer::new(tree.clone());
4770        for (k, v) in &test.initial_variables {
4771            player.scope.set(k.clone(), ScopeType::Global, v.clone());
4772        }
4773        let mut result = TestResult {
4774            test_name: test.name.clone(),
4775            passed: false,
4776            errors: Vec::new(),
4777            actual_speaker_sequence: Vec::new(),
4778            actual_end_type: None,
4779            steps_taken: 0,
4780        };
4781
4782        let mut choice_idx = 0;
4783        let mut play_result = player.start();
4784        result.steps_taken += 1;
4785
4786        loop {
4787            match play_result {
4788                PlayResult::ShowDialogue { speaker, .. } => {
4789                    result.actual_speaker_sequence.push(speaker);
4790                    play_result = player.step(None);
4791                }
4792                PlayResult::ShowChoices { .. } => {
4793                    let choice = if choice_idx < test.choice_sequence.len() {
4794                        let c = test.choice_sequence[choice_idx];
4795                        choice_idx += 1;
4796                        c
4797                    } else {
4798                        0
4799                    };
4800                    play_result = player.step(Some(choice));
4801                }
4802                PlayResult::NeedAdvance => {
4803                    play_result = player.step(None);
4804                }
4805                PlayResult::Ended => {
4806                    if let PlayerState::Ended(ref end_type) = player.state {
4807                        result.actual_end_type = Some(end_type.clone());
4808                    }
4809                    break;
4810                }
4811                PlayResult::Error(msg) => {
4812                    result.errors.push(msg);
4813                    break;
4814                }
4815                _ => {
4816                    play_result = player.step(None);
4817                }
4818            }
4819            result.steps_taken += 1;
4820            if result.steps_taken > 10000 {
4821                result.errors.push("Test exceeded max steps".to_string());
4822                break;
4823            }
4824        }
4825
4826        // Validate results
4827        if result.actual_speaker_sequence != test.expected_speaker_sequence {
4828            result.errors.push(format!("Speaker sequence mismatch: expected {:?}, got {:?}",
4829                test.expected_speaker_sequence, result.actual_speaker_sequence));
4830        }
4831        if let Some(ref actual_end) = result.actual_end_type {
4832            if actual_end != &test.expected_end_type {
4833                result.errors.push(format!("End type mismatch: expected {:?}, got {:?}",
4834                    test.expected_end_type, actual_end));
4835            }
4836        }
4837        for (k, expected) in &test.expected_final_variables {
4838            let actual = player.scope.get_any(k);
4839            if actual.as_ref() != Some(expected) {
4840                result.errors.push(format!("Variable {} expected {:?} got {:?}", k, expected, actual));
4841            }
4842        }
4843
4844        result.passed = result.errors.is_empty();
4845        result
4846    }
4847
4848    pub fn get_pass_count(&self) -> usize {
4849        self.results.iter().filter(|r| r.passed).count()
4850    }
4851
4852    pub fn get_fail_count(&self) -> usize {
4853        self.results.iter().filter(|r| !r.passed).count()
4854    }
4855}
4856
4857// ============================================================
4858// EXTRA: TAG SYSTEM
4859// ============================================================
4860
4861#[derive(Debug, Clone)]
4862pub struct TagManager {
4863    pub known_tags: HashSet<String>,
4864    pub tag_colors: HashMap<String, Vec4>,
4865    pub tag_categories: HashMap<String, Vec<String>>,
4866}
4867
4868impl TagManager {
4869    pub fn new() -> Self {
4870        let mut tm = TagManager {
4871            known_tags: HashSet::new(),
4872            tag_colors: HashMap::new(),
4873            tag_categories: HashMap::new(),
4874        };
4875        tm.register_default_tags();
4876        tm
4877    }
4878
4879    fn register_default_tags(&mut self) {
4880        let tags = [
4881            ("important", Vec4::new(1.0, 0.2, 0.2, 1.0)),
4882            ("review", Vec4::new(1.0, 0.8, 0.0, 1.0)),
4883            ("done", Vec4::new(0.2, 0.8, 0.2, 1.0)),
4884            ("wip", Vec4::new(0.5, 0.5, 0.5, 1.0)),
4885            ("vo_needed", Vec4::new(0.8, 0.4, 0.0, 1.0)),
4886            ("loc_needed", Vec4::new(0.4, 0.6, 1.0, 1.0)),
4887            ("optional", Vec4::new(0.6, 0.6, 0.8, 1.0)),
4888        ];
4889        for (tag, color) in &tags {
4890            self.known_tags.insert(tag.to_string());
4891            self.tag_colors.insert(tag.to_string(), *color);
4892        }
4893    }
4894
4895    pub fn register_tag(&mut self, tag: &str, color: Vec4) {
4896        self.known_tags.insert(tag.to_string());
4897        self.tag_colors.insert(tag.to_string(), color);
4898    }
4899
4900    pub fn get_color(&self, tag: &str) -> Vec4 {
4901        self.tag_colors.get(tag).copied().unwrap_or(Vec4::new(0.7, 0.7, 0.7, 1.0))
4902    }
4903
4904    pub fn find_nodes_with_tag<'a>(&self, tag: &str, tree: &'a DialogueTree) -> Vec<&'a DialogueNode> {
4905        tree.nodes.values()
4906            .filter(|n| n.tags().contains(&tag.to_string()))
4907            .collect()
4908    }
4909
4910    pub fn add_category(&mut self, category: &str, tags: Vec<String>) {
4911        self.tag_categories.insert(category.to_string(), tags);
4912    }
4913}
4914
4915// ============================================================
4916// EXTRA: DIALOGUE VERSIONING
4917// ============================================================
4918
4919#[derive(Debug, Clone)]
4920pub struct DialogueVersion {
4921    pub version_id: String,
4922    pub tree_id: String,
4923    pub timestamp: f64,
4924    pub author: String,
4925    pub message: String,
4926    pub node_count: usize,
4927    pub word_count: usize,
4928    pub snapshot: String,
4929}
4930
4931#[derive(Debug, Clone)]
4932pub struct VersionControl {
4933    pub versions: BTreeMap<String, Vec<DialogueVersion>>,
4934    pub auto_version: bool,
4935    pub auto_version_interval: f32,
4936    pub time_since_last_auto: f32,
4937}
4938
4939impl VersionControl {
4940    pub fn new() -> Self {
4941        VersionControl {
4942            versions: BTreeMap::new(),
4943            auto_version: true,
4944            auto_version_interval: 300.0,
4945            time_since_last_auto: 0.0,
4946        }
4947    }
4948
4949    pub fn create_version(&mut self, tree: &DialogueTree, author: &str, message: &str) -> String {
4950        let ver_id = format!("v{}_{}", tree.id, self.versions.entry(tree.id.clone()).or_default().len());
4951        let word_count: usize = tree.nodes.values()
4952            .filter_map(|n| if let DialogueNode::Speaker(s) = n { Some(s.dialogue_text.split_whitespace().count()) } else { None })
4953            .sum();
4954        let version = DialogueVersion {
4955            version_id: ver_id.clone(),
4956            tree_id: tree.id.clone(),
4957            timestamp: 0.0,
4958            author: author.to_string(),
4959            message: message.to_string(),
4960            node_count: tree.nodes.len(),
4961            word_count,
4962            snapshot: String::new(),
4963        };
4964        self.versions.entry(tree.id.clone()).or_default().push(version);
4965        ver_id
4966    }
4967
4968    pub fn get_versions(&self, tree_id: &str) -> &[DialogueVersion] {
4969        self.versions.get(tree_id).map(|v| v.as_slice()).unwrap_or(&[])
4970    }
4971
4972    pub fn update(&mut self, delta: f32, tree: Option<&DialogueTree>) {
4973        if !self.auto_version { return; }
4974        self.time_since_last_auto += delta;
4975        if self.time_since_last_auto >= self.auto_version_interval {
4976            self.time_since_last_auto = 0.0;
4977            if let Some(tree) = tree {
4978                self.create_version(tree, "auto", "Auto-save");
4979            }
4980        }
4981    }
4982}
4983
4984// ============================================================
4985// EXTRA: BATCH EDIT OPERATIONS
4986// ============================================================
4987
4988#[derive(Debug, Clone)]
4989pub struct BatchEditOperation {
4990    pub op_type: BatchOpType,
4991    pub target_tags: Vec<String>,
4992    pub target_speakers: Vec<String>,
4993    pub node_ids: Vec<u64>,
4994}
4995
4996#[derive(Debug, Clone)]
4997pub enum BatchOpType {
4998    AddTag(String),
4999    RemoveTag(String),
5000    SetEmotion(EmotionState),
5001    SetTextSpeed(f32),
5002    EnableAutoAdvance(bool),
5003    SetAutoAdvanceDelay(f32),
5004    ReplaceText { from: String, to: String },
5005    RegenerateLocKeys,
5006}
5007
5008impl BatchEditOperation {
5009    pub fn apply_to_tree(&self, tree: &mut DialogueTree) -> usize {
5010        let mut modified = 0;
5011        let node_ids: Vec<u64> = if self.node_ids.is_empty() {
5012            tree.nodes.keys().cloned().collect()
5013        } else {
5014            self.node_ids.clone()
5015        };
5016        for id in node_ids {
5017            let matches = {
5018                let node = tree.nodes.get(&id);
5019                node.map(|n| {
5020                    if !self.target_tags.is_empty() {
5021                        self.target_tags.iter().any(|t| n.tags().contains(t))
5022                    } else if !self.target_speakers.is_empty() {
5023                        if let DialogueNode::Speaker(s) = n {
5024                            self.target_speakers.contains(&s.speaker_id)
5025                        } else {
5026                            false
5027                        }
5028                    } else {
5029                        true
5030                    }
5031                }).unwrap_or(false)
5032            };
5033            if !matches { continue; }
5034            if let Some(node) = tree.nodes.get_mut(&id) {
5035                match &self.op_type {
5036                    BatchOpType::AddTag(tag) => {
5037                        match node {
5038                            DialogueNode::Speaker(n) => {
5039                                if !n.tags.contains(tag) { n.tags.push(tag.clone()); modified += 1; }
5040                            }
5041                            _ => {}
5042                        }
5043                    }
5044                    BatchOpType::RemoveTag(tag) => {
5045                        match node {
5046                            DialogueNode::Speaker(n) => {
5047                                let before = n.tags.len();
5048                                n.tags.retain(|t| t != tag);
5049                                if n.tags.len() < before { modified += 1; }
5050                            }
5051                            _ => {}
5052                        }
5053                    }
5054                    BatchOpType::SetEmotion(emotion) => {
5055                        if let DialogueNode::Speaker(n) = node {
5056                            n.emotion_state = emotion.clone();
5057                            modified += 1;
5058                        }
5059                    }
5060                    BatchOpType::SetTextSpeed(speed) => {
5061                        if let DialogueNode::Speaker(n) = node {
5062                            n.text_speed = *speed;
5063                            modified += 1;
5064                        }
5065                    }
5066                    BatchOpType::EnableAutoAdvance(enabled) => {
5067                        if let DialogueNode::Speaker(n) = node {
5068                            n.auto_advance = *enabled;
5069                            modified += 1;
5070                        }
5071                    }
5072                    BatchOpType::SetAutoAdvanceDelay(delay) => {
5073                        if let DialogueNode::Speaker(n) = node {
5074                            n.auto_advance_delay = *delay;
5075                            modified += 1;
5076                        }
5077                    }
5078                    BatchOpType::ReplaceText { from, to } => {
5079                        if let DialogueNode::Speaker(n) = node {
5080                            if n.dialogue_text.contains(from.as_str()) {
5081                                n.dialogue_text = n.dialogue_text.replace(from.as_str(), to.as_str());
5082                                modified += 1;
5083                            }
5084                        }
5085                    }
5086                    BatchOpType::RegenerateLocKeys => {
5087                        match node {
5088                            DialogueNode::Speaker(n) => {
5089                                n.localization_key = LocalizationTable::generate_key(&tree.id, n.id, "text");
5090                                modified += 1;
5091                            }
5092                            _ => {}
5093                        }
5094                    }
5095                }
5096            }
5097        }
5098        modified
5099    }
5100}
5101
5102// ============================================================
5103// FINAL: MODULE-LEVEL UTILITY FUNCTIONS
5104// ============================================================
5105
5106pub fn create_sample_tree() -> DialogueTree {
5107    let mut tree = DialogueTree::new("sample_001".to_string(), "Sample Conversation".to_string());
5108
5109    let start_id = 1u64;
5110    let mut start = StartNode::new(start_id);
5111    start.position = Vec2::new(50.0, 200.0);
5112    tree.add_node(DialogueNode::Start(start));
5113
5114    let greet_id = 2u64;
5115    let mut greet = SpeakerNode::new(greet_id);
5116    greet.position = Vec2::new(300.0, 200.0);
5117    greet.speaker_id = "npc_merchant".to_string();
5118    greet.speaker_name = "Merchant".to_string();
5119    greet.dialogue_text = "Ah, welcome traveler! What brings you to my shop?".to_string();
5120    greet.localization_key = "sample_001_0002_text".to_string();
5121    greet.emotion_state = EmotionState::new(EmotionType::Happy, 0.7);
5122    tree.add_node(DialogueNode::Speaker(greet));
5123
5124    let choice_id = 3u64;
5125    let mut choice_node = PlayerChoiceNode::new(choice_id);
5126    choice_node.position = Vec2::new(600.0, 200.0);
5127    choice_node.prompt_text = "What do you want to say?".to_string();
5128    choice_node.add_choice("I'm looking for supplies.".to_string(), 4);
5129    choice_node.add_choice("Just browsing.".to_string(), 5);
5130    choice_node.add_choice("Goodbye.".to_string(), 6);
5131    tree.add_node(DialogueNode::PlayerChoice(choice_node));
5132
5133    let reply_id = 4u64;
5134    let mut reply = SpeakerNode::new(reply_id);
5135    reply.position = Vec2::new(900.0, 100.0);
5136    reply.speaker_id = "npc_merchant".to_string();
5137    reply.speaker_name = "Merchant".to_string();
5138    reply.dialogue_text = "You've come to the right place! I have everything you need.".to_string();
5139    tree.add_node(DialogueNode::Speaker(reply));
5140
5141    let browse_id = 5u64;
5142    let mut browse = SpeakerNode::new(browse_id);
5143    browse.position = Vec2::new(900.0, 250.0);
5144    browse.speaker_id = "npc_merchant".to_string();
5145    browse.speaker_name = "Merchant".to_string();
5146    browse.dialogue_text = "Take your time, take your time!".to_string();
5147    tree.add_node(DialogueNode::Speaker(browse));
5148
5149    let end_id = 6u64;
5150    let mut end = EndNode::new(end_id);
5151    end.position = Vec2::new(1200.0, 200.0);
5152    end.end_type = EndType::Normal;
5153    tree.add_node(DialogueNode::End(end));
5154
5155    tree.connect(1, 0, 2, 0, None);
5156    tree.connect(2, 0, 3, 0, None);
5157    tree.connect(3, 0, 4, 0, Some("supplies".to_string()));
5158    tree.connect(3, 1, 5, 0, Some("browse".to_string()));
5159    tree.connect(3, 2, 6, 0, Some("goodbye".to_string()));
5160    tree.connect(4, 0, 6, 0, None);
5161    tree.connect(5, 0, 6, 0, None);
5162
5163    tree
5164}
5165
5166pub fn build_condition_from_str(expr: &str) -> Option<ConditionExpression> {
5167    let expr = expr.trim();
5168    if expr == "true" { return Some(ConditionExpression::Literal(true)); }
5169    if expr == "false" { return Some(ConditionExpression::Literal(false)); }
5170    // Parse simple comparisons like "var_name == value"
5171    for op_str in &["==", "!=", ">=", "<=", ">", "<"] {
5172        if let Some(pos) = expr.find(op_str) {
5173            let var = expr[..pos].trim().to_string();
5174            let val_str = expr[pos + op_str.len()..].trim();
5175            let op = match *op_str {
5176                "==" => ComparisonOperator::Equal,
5177                "!=" => ComparisonOperator::NotEqual,
5178                ">=" => ComparisonOperator::GreaterEqual,
5179                "<=" => ComparisonOperator::LessEqual,
5180                ">" => ComparisonOperator::GreaterThan,
5181                "<" => ComparisonOperator::LessThan,
5182                _ => continue,
5183            };
5184            let val = if let Ok(i) = val_str.parse::<i64>() {
5185                DialogueValue::Int(i)
5186            } else if let Ok(f) = val_str.parse::<f64>() {
5187                DialogueValue::Float(f)
5188            } else if val_str == "true" {
5189                DialogueValue::Bool(true)
5190            } else if val_str == "false" {
5191                DialogueValue::Bool(false)
5192            } else {
5193                DialogueValue::String(val_str.trim_matches('"').to_string())
5194            };
5195            return Some(ConditionExpression::Comparison(ComparisonCondition {
5196                variable_name: var,
5197                scope_type: ScopeType::Global,
5198                operator: op,
5199                compare_value: val,
5200            }));
5201        }
5202    }
5203    None
5204}
5205
5206pub fn format_dialogue_text(text: &str, scope: &VariableScope) -> String {
5207    let mut result = text.to_string();
5208    // Simple variable substitution: {var_name}
5209    let mut i = 0;
5210    let chars: Vec<char> = result.chars().collect();
5211    let mut output = String::new();
5212    while i < chars.len() {
5213        if chars[i] == '{' {
5214            let start = i + 1;
5215            let mut end = start;
5216            while end < chars.len() && chars[end] != '}' {
5217                end += 1;
5218            }
5219            if end < chars.len() {
5220                let var_name: String = chars[start..end].iter().collect();
5221                if let Some(val) = scope.get_any(&var_name) {
5222                    output.push_str(&val.to_string_repr());
5223                } else {
5224                    output.push('{');
5225                    output.push_str(&var_name);
5226                    output.push('}');
5227                }
5228                i = end + 1;
5229            } else {
5230                output.push(chars[i]);
5231                i += 1;
5232            }
5233        } else {
5234            output.push(chars[i]);
5235            i += 1;
5236        }
5237    }
5238    output
5239}
5240
5241pub fn count_dialogue_tree_words(tree: &DialogueTree) -> HashMap<String, usize> {
5242    let mut counts: HashMap<String, usize> = HashMap::new();
5243    for node in tree.nodes.values() {
5244        if let DialogueNode::Speaker(n) = node {
5245            let words = n.dialogue_text.split_whitespace().count();
5246            *counts.entry(n.speaker_id.clone()).or_insert(0) += words;
5247        }
5248    }
5249    counts
5250}
5251
5252pub fn estimate_voice_recording_time(tree: &DialogueTree, wpm: f32) -> f32 {
5253    let total_words: usize = tree.nodes.values()
5254        .filter_map(|n| if let DialogueNode::Speaker(s) = n { Some(s.dialogue_text.split_whitespace().count()) } else { None })
5255        .sum();
5256    total_words as f32 / wpm * 60.0
5257}
5258
5259pub fn generate_localization_report(loc_table: &LocalizationTable, tree: &DialogueTree) -> String {
5260    let mut report = String::new();
5261    let keys = tree.collect_all_localization_keys();
5262    let total = keys.len();
5263    report.push_str(&format!("Total localization keys: {}\n\n", total));
5264    for locale in &loc_table.supported_locales {
5265        let translated = keys.iter().filter(|k| loc_table.get(k, locale).is_some()).count();
5266        let pct = if total > 0 { translated * 100 / total } else { 0 };
5267        report.push_str(&format!("{}: {}/{} ({pct}%)\n", locale, translated, total, pct = pct));
5268    }
5269    report
5270}