dialogos/
lib.rs

1#![deny(warnings, missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use regex::Regex;
5use std::collections::HashMap;
6
7const SPLIT_PAT: &str = "||";
8const VAR_PAT: &str = r"__\w*";
9
10/// Parses and evaluates simple math expressions.
11/// The expressions must look like this: "n1 operator n2"
12///
13/// Supported operators: +, -, *, /, %, <, >, <=, >=, ==, !=
14/// ```
15/// # use dialogos::*;
16/// assert_eq!(calc("15 + 25").unwrap(), 40.0);
17/// assert_eq!(calc("15 - 25").unwrap(), -10.0);
18/// assert_eq!(calc("10 * 20").unwrap(), 200.0);
19/// assert_eq!(calc("10 / 20").unwrap(), 0.5);
20/// assert_eq!(calc("10 % 20").unwrap(), 10.0);
21/// assert_eq!(calc("6 < 6").unwrap(), 0.0);
22/// assert_eq!(calc("6 > 6").unwrap(), 0.0);
23/// assert_eq!(calc("6 <= 6").unwrap(), 1.0);
24/// assert_eq!(calc("6 >= 6").unwrap(), 1.0);
25/// assert_eq!(calc("6 == 6").unwrap(), 1.0);
26/// assert_eq!(calc("6 != 6").unwrap(), 0.0);
27/// ```
28pub fn calc(s: &str) -> Option<f64> {
29    let boolf64 = |b| if b { 1.0 } else { 0.0 };
30    let args: Vec<&str> = s.trim().split(' ').collect();
31    if args.len() == 3 {
32        if let Ok(n1) = args[0].parse::<f64>() {
33            if let Ok(n2) = args[2].parse::<f64>() {
34                match args[1] {
35                    "+" => Some(n1 + n2),
36                    "-" => Some(n1 - n2),
37                    "*" => Some(n1 * n2),
38                    "/" => Some(n1 / n2),
39                    "%" => Some(n1 % n2),
40                    "<" => Some(boolf64(n1 < n2)),
41                    ">" => Some(boolf64(n1 > n2)),
42                    "<=" => Some(boolf64(n1 <= n2)),
43                    ">=" => Some(boolf64(n1 >= n2)),
44                    "==" => Some(boolf64(n1 == n2)),
45                    "!=" => Some(boolf64(n1 != n2)),
46                    _ => None,
47                }
48            } else {
49                None
50            }
51        } else {
52            None
53        }
54    } else {
55        None
56    }
57}
58
59/// Splits a string with the dialogue split pattern.
60/// ```
61/// # use dialogos::*;
62/// assert!(split("Hello||Gamer||Sisters").len() == 3);
63/// ```
64pub fn split(s: &str) -> Vec<String> {
65    s.trim().split(SPLIT_PAT).map(|s| s.to_string()).collect()
66}
67
68/// The line types.
69#[derive(Clone, Debug, Eq, PartialEq)]
70pub enum LineType {
71    /// Represents the end of a dialogue.
72    End,
73    /// Represents text in a dialogue.
74    Text,
75    /// Represents a position in a dialogue.
76    Label,
77    /// Represents a position change in a dialogue.
78    Jump,
79    /// Represents a choice in a dialogue.
80    Menu,
81    /// Represents a variable creation in a dialogue.
82    Variable,
83    /// Represents a conditional statement in a dialogue.
84    Check,
85}
86
87/// The dialogue line structure.
88#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct Line {
90    /// The type of a line determines how its data will be used.
91    pub t: LineType,
92    /// Information about the line.
93    pub info: String,
94    /// The content of the line.
95    pub cont: String,
96}
97
98/// Creates an end line.
99pub fn end() -> Line {
100    Line {
101        t: LineType::End,
102        info: String::new(),
103        cont: String::new(),
104    }
105}
106
107/// Creates a text line.
108pub fn text(info: &str, cont: &str) -> Line {
109    Line {
110        t: LineType::Text,
111        info: String::from(info),
112        cont: String::from(cont),
113    }
114}
115
116/// Creates a label line.
117pub fn label(cont: &str) -> Line {
118    Line {
119        t: LineType::Label,
120        info: String::new(),
121        cont: String::from(cont),
122    }
123}
124
125/// Creates a jump line.
126pub fn jump(cont: &str) -> Line {
127    Line {
128        t: LineType::Jump,
129        info: String::new(),
130        cont: String::from(cont),
131    }
132}
133
134/// Creates a menu line.
135pub fn menu(info: &str, cont: &str) -> Line {
136    Line {
137        t: LineType::Menu,
138        info: String::from(info),
139        cont: String::from(cont),
140    }
141}
142
143/// Creates a variable line.
144pub fn variable(info: &str, cont: &str) -> Line {
145    Line {
146        t: LineType::Variable,
147        info: String::from(info),
148        cont: String::from(cont),
149    }
150}
151
152/// Creates a check line.
153pub fn check(cont: &str) -> Line {
154    Line {
155        t: LineType::Check,
156        info: String::new(),
157        cont: String::from(cont),
158    }
159}
160
161/// The dialogue structure.
162#[derive(Clone, Debug)]
163pub struct Dialogue {
164    idx: usize,
165    lines: Vec<Line>,
166    labels: HashMap<String, usize>,
167    /// The variables of the dialogue.
168    pub vars: HashMap<String, String>,
169}
170
171impl Dialogue {
172    /// Creates a new dialogue.
173    pub fn new(lines: Vec<Line>) -> Self {
174        let mut labels = HashMap::new();
175        let mut lines = lines;
176        lines.push(end());
177        for (i, line) in lines.iter().enumerate() {
178            if line.t == LineType::Label {
179                labels.insert(line.cont.clone(), i);
180            }
181        }
182        let mut d = Dialogue {
183            idx: 0,
184            lines,
185            labels,
186            vars: HashMap::new(),
187        };
188        d.update();
189        d
190    }
191
192    fn update(&mut self) {
193        let line = self.line();
194        match line.t {
195            LineType::Label => {
196                self.next();
197            }
198
199            LineType::Jump => {
200                self.idx = match self.labels.get(&line.cont) {
201                    Some(idx) => *idx,
202                    None => self.idx + 1,
203                };
204                self.update();
205            }
206
207            LineType::Variable => {
208                let val = if let Some(val) = calc(&line.cont) {
209                    val.to_string()
210                } else {
211                    line.cont
212                };
213                self.vars.insert(line.info, val);
214                self.next();
215            }
216
217            LineType::Check => {
218                let val = if let Some(val) = calc(&line.cont) {
219                    val
220                } else {
221                    0.0
222                };
223                if val == 1.0 {
224                    self.next();
225                } else {
226                    self.idx += 2;
227                    if self.idx >= self.lines.len() {
228                        self.idx = self.lines.len() - 1;
229                    }
230                    self.update();
231                }
232            }
233
234            _ => {}
235        }
236    }
237
238    /// Returns the index of the dialogue.
239    pub fn idx(&self) -> usize {
240        self.idx
241    }
242
243    /// Returns the lines of the dialogue.
244    pub fn lines(&self) -> &Vec<Line> {
245        &self.lines
246    }
247
248    /// Returns the labels of the dialogue.
249    pub fn labels(&self) -> &HashMap<String, usize> {
250        &self.labels
251    }
252
253    /// Returns true if the current line is an end line.
254    pub fn has_end(&self) -> bool {
255        self.lines[self.idx].t == LineType::End
256    }
257
258    /// Returns true if the current line is a menu line.
259    pub fn has_menu(&self) -> bool {
260        self.lines[self.idx].t == LineType::Menu
261    }
262
263    /// Resets the dialogue index.
264    pub fn reset(&mut self) {
265        self.idx = 0;
266    }
267
268    /// Changes the lines of the dialogue.
269    pub fn change(&mut self, lines: Vec<Line>) {
270        self.reset();
271        self.labels.clear();
272        self.lines = lines;
273        self.lines.push(end());
274        for (i, line) in self.lines.iter().enumerate() {
275            if line.t == LineType::Label {
276                self.labels.insert(line.cont.clone(), i);
277            }
278        }
279    }
280
281    /// Returns the current line of the dialogue.
282    /// ```
283    /// # use dialogos::*;
284    /// let mut d = Dialogue::new(vec![
285    ///     text("uwu", "My recomendation is..."),
286    ///     text("owo", "ubunchu!"),
287    /// ]);
288    ///
289    /// assert_eq!(d.line().info, "uwu");
290    /// ```
291    pub fn line(&self) -> Line {
292        let re = Regex::new(VAR_PAT).unwrap();
293        let line = &self.lines[self.idx];
294
295        let mut result = line.clone();
296        for caps in re.captures_iter(&line.info) {
297            let word = caps.get(0).unwrap().as_str();
298            if let Some(val) = self.vars.get(&word[2..]) {
299                result.info = result.info.replace(word, val);
300            }
301        }
302        for caps in re.captures_iter(&line.cont) {
303            let word = caps.get(0).unwrap().as_str();
304            if let Some(val) = self.vars.get(&word[2..]) {
305                result.cont = result.cont.replace(word, val);
306            }
307        }
308        result
309    }
310
311    /// Advances the index by one.
312    /// ```
313    /// # use dialogos::*;
314    /// let mut d = Dialogue::new(vec![
315    ///     text("Line 1", "This is a line."),
316    ///     text("Line 2", "And this is also a line."),
317    /// ]);
318    ///
319    /// assert_eq!(d.line().info, "Line 1");
320    /// d.next();
321    /// assert_eq!(d.line().info, "Line 2");
322    /// ```
323    pub fn next(&mut self) {
324        self.idx += 1;
325        self.update();
326    }
327
328    /// Changes the index by using a label.
329    /// ```
330    /// # use dialogos::*;
331    /// let merchant = |cont| text("Merchant", cont);
332    ///
333    /// let mut d = Dialogue::new(vec![
334    ///     label("Buy"),
335    ///     merchant("What're ya buyin?"),
336    ///     jump("End"),
337    ///     label("Sell"),
338    ///     merchant("What're ya sellin?"),
339    ///     label("End"),
340    ///     merchant("Heh heh heh... Thank you!"),
341    /// ]);
342    ///
343    /// d.jump("Sell");
344    /// assert_eq!(d.line().cont, "What're ya sellin?");
345    /// ```
346    pub fn jump(&mut self, label: &str) {
347        self.idx = self.labels[label];
348        self.update();
349    }
350
351    /// Returns the choices of a menu line.
352    pub fn choices(&self) -> Vec<String> {
353        split(&self.line().cont)
354    }
355
356    /// Chooses an item from a menu line.
357    pub fn choose(&mut self, choice: usize) {
358        self.jump(&split(&self.line().info)[choice]);
359    }
360}