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#[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 (¤t, &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 (¤t, &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 (¤t, &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 (¤t, &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 ¤t {
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#[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 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 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#[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#[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, Global, Persistent, }
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 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#[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 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 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#[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 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#[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, pub arousal: f32, }
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 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#[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#[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 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 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 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 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 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#[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 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 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 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#[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#[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#[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
5102pub 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 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 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}