initiative_core/reference/
command.rs

1use super::{Condition, Item, ItemCategory, MagicItem, Spell, Trait};
2use crate::app::{
3    AppMeta, Autocomplete, AutocompleteSuggestion, CommandMatches, ContextAwareParse, Runnable,
4};
5use crate::utils::CaseInsensitiveStr;
6use async_trait::async_trait;
7use caith::Roller;
8use std::fmt;
9use std::iter::repeat;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub enum ReferenceCommand {
13    Condition(Condition),
14    Item(Item),
15    ItemCategory(ItemCategory),
16    MagicItem(MagicItem),
17    OpenGameLicense,
18    Spell(Spell),
19    Spells,
20    Trait(Trait),
21}
22
23#[async_trait(?Send)]
24impl Runnable for ReferenceCommand {
25    async fn run(self, _input: &str, _app_meta: &mut AppMeta) -> Result<String, String> {
26        let (output, name) = match self {
27            Self::Condition(condition) => (format!("{}", condition), condition.get_name()),
28            Self::Item(item) => (format!("{}", item), item.get_name()),
29            Self::ItemCategory(category) => (format!("{}", category), "This listing"),
30            Self::MagicItem(magic_item) => (format!("{}", magic_item), magic_item.get_name()),
31            Self::OpenGameLicense => {
32                return Ok(include_str!("../../../data/ogl-1.0a.md")
33                    .trim_end()
34                    .to_string());
35            }
36            Self::Spell(spell) => (format!("{}", spell), spell.get_name()),
37            Self::Spells => (Spell::get_list().to_string(), "This listing"),
38            Self::Trait(t) => (t.to_string(), t.get_name()),
39        };
40
41        Ok(format!(
42            "{}\n\n*{} is Open Game Content subject to the `Open Game License`.*",
43            linkify_dice(&output),
44            name,
45        ))
46    }
47}
48
49#[async_trait(?Send)]
50impl ContextAwareParse for ReferenceCommand {
51    async fn parse_input(input: &str, _app_meta: &AppMeta) -> CommandMatches<Self> {
52        let mut matches = if input.eq_ci("Open Game License") {
53            CommandMatches::new_canonical(Self::OpenGameLicense)
54        } else if input.eq_ci("srd spells") {
55            CommandMatches::new_canonical(Self::Spells)
56        } else if let Some(condition) = input
57            .strip_prefix_ci("srd condition ")
58            .and_then(|s| s.parse().ok())
59        {
60            CommandMatches::new_canonical(Self::Condition(condition))
61        } else if let Some(item_category) = input
62            .strip_prefix_ci("srd item category ")
63            .and_then(|s| s.parse().ok())
64        {
65            CommandMatches::new_canonical(Self::ItemCategory(item_category))
66        } else if let Some(item) = input
67            .strip_prefix_ci("srd item ")
68            .and_then(|s| s.parse().ok())
69        {
70            CommandMatches::new_canonical(Self::Item(item))
71        } else if let Some(magic_item) = input
72            .strip_prefix_ci("srd magic item ")
73            .and_then(|s| s.parse().ok())
74        {
75            CommandMatches::new_canonical(Self::MagicItem(magic_item))
76        } else if let Some(spell) = input
77            .strip_prefix_ci("srd spell ")
78            .and_then(|s| s.parse().ok())
79        {
80            CommandMatches::new_canonical(Self::Spell(spell))
81        } else if let Some(character_trait) = input
82            .strip_prefix_ci("srd trait ")
83            .and_then(|s| s.parse().ok())
84        {
85            CommandMatches::new_canonical(Self::Trait(character_trait))
86        } else {
87            CommandMatches::default()
88        };
89
90        if let Ok(condition) = input.parse() {
91            matches.push_fuzzy(Self::Condition(condition));
92        }
93        if let Ok(item) = input.parse() {
94            matches.push_fuzzy(Self::Item(item));
95        }
96        if let Ok(category) = input.parse() {
97            matches.push_fuzzy(Self::ItemCategory(category));
98        }
99        if let Ok(magic_item) = input.parse() {
100            matches.push_fuzzy(Self::MagicItem(magic_item));
101        }
102        if let Ok(spell) = input.parse() {
103            matches.push_fuzzy(Self::Spell(spell));
104        }
105        if let Ok(character_trait) = input.parse() {
106            matches.push_fuzzy(Self::Trait(character_trait));
107        }
108        if input.eq_ci("spells") {
109            matches.push_fuzzy(Self::Spells);
110        }
111
112        matches
113    }
114}
115
116#[async_trait(?Send)]
117impl Autocomplete for ReferenceCommand {
118    async fn autocomplete(input: &str, _app_meta: &AppMeta) -> Vec<AutocompleteSuggestion> {
119        [
120            ("Open Game License", "SRD license"),
121            ("spells", "SRD index"),
122        ]
123        .into_iter()
124        .chain(Spell::get_words().zip(repeat("SRD spell")))
125        .chain(Condition::get_words().zip(repeat("SRD condition")))
126        .chain(Item::get_words().zip(repeat("SRD item")))
127        .chain(ItemCategory::get_words().zip(repeat("SRD item category")))
128        .chain(MagicItem::get_words().zip(repeat("SRD magic item")))
129        .chain(Trait::get_words().zip(repeat("SRD trait")))
130        .filter(|(term, _)| term.starts_with_ci(input))
131        .take(10)
132        .map(|(term, summary)| AutocompleteSuggestion::new(term, summary))
133        .collect()
134    }
135}
136
137impl fmt::Display for ReferenceCommand {
138    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
139        match self {
140            Self::Condition(condition) => write!(f, "srd condition {}", condition.get_name()),
141            Self::Item(item) => write!(f, "srd item {}", item.get_name()),
142            Self::ItemCategory(category) => write!(f, "srd item category {}", category.get_name()),
143            Self::MagicItem(item) => write!(f, "srd magic item {}", item.get_name()),
144            Self::OpenGameLicense => write!(f, "Open Game License"),
145            Self::Spell(spell) => write!(f, "srd spell {}", spell.get_name()),
146            Self::Spells => write!(f, "srd spells"),
147            Self::Trait(species_trait) => write!(f, "srd trait {}", species_trait.get_name()),
148        }
149    }
150}
151
152fn linkify_dice(input: &str) -> String {
153    let mut result = String::with_capacity(input.len());
154    let mut input_offset = 0;
155
156    let mut hold = String::new();
157    let mut hold_offset = 0;
158    let mut hold_active = false;
159
160    for part in input.split_inclusive(|c: char| c.is_whitespace() || c.is_ascii_punctuation()) {
161        if !hold_active
162            && part.contains(|c: char| c.is_ascii_digit())
163            && part.contains(&['d', 'D'][..])
164        {
165            hold_active = true;
166            hold_offset = input_offset;
167        } else if hold_active && part.contains(char::is_alphabetic) {
168            hold_active = false;
169        }
170
171        if hold_active {
172            hold.push_str(part);
173        } else {
174            while !hold.is_empty() {
175                let hold_trimmed = hold.trim();
176                if hold_trimmed.contains(&['d', 'D'][..])
177                    && Roller::new(hold_trimmed).is_ok_and(|r| r.roll().is_ok())
178                {
179                    result.push('`');
180                    result.push_str(hold_trimmed);
181                    result.push('`');
182                    result.push_str(&input[hold_offset + hold_trimmed.len()..input_offset]);
183                    hold.clear();
184                    break;
185                }
186
187                if let Some(pos) =
188                    hold.rfind(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
189                {
190                    hold.truncate(pos);
191
192                    if hold.is_empty() {
193                        result.push_str(&input[hold_offset..input_offset]);
194                    }
195                } else {
196                    result.push_str(&input[hold_offset..input_offset]);
197                    hold.clear();
198                }
199            }
200
201            result.push_str(part);
202        }
203
204        input_offset += part.len();
205    }
206
207    result.push_str(&hold);
208    result
209}
210
211#[cfg(test)]
212mod test {
213    use super::*;
214    use crate::test_utils as test;
215    use tokio_test::block_on;
216
217    #[test]
218    fn display_test() {
219        let app_meta = test::app_meta();
220
221        [
222            ReferenceCommand::Spell(Spell::Shield),
223            ReferenceCommand::Spells,
224            ReferenceCommand::Item(Item::Shield),
225            ReferenceCommand::ItemCategory(ItemCategory::Shields),
226            ReferenceCommand::MagicItem(MagicItem::DeckOfManyThings),
227            ReferenceCommand::OpenGameLicense,
228        ]
229        .into_iter()
230        .for_each(|command| {
231            let command_string = command.to_string();
232            assert_ne!("", command_string);
233
234            assert_eq!(
235                CommandMatches::new_canonical(command.clone()),
236                block_on(ReferenceCommand::parse_input(&command_string, &app_meta)),
237                "{}",
238                command_string,
239            );
240
241            assert_eq!(
242                CommandMatches::new_canonical(command),
243                block_on(ReferenceCommand::parse_input(
244                    &command_string.to_uppercase(),
245                    &app_meta,
246                )),
247                "{}",
248                command_string.to_uppercase(),
249            );
250        });
251    }
252}