initiative_core/reference/
command.rs1use 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}