lib_efo/
naming.rs

1//! Autonaming generator
2//! Supposed to be loaded with the world if needed
3use once_cell::sync::Lazy;
4use rand::prelude::*;
5use serde::{Deserialize, Serialize};
6use std::{
7    fs, io,
8    path::{Path, PathBuf},
9};
10
11use crate::Displayable;
12use crate::entity::{EffectApplication, EffectBounds, EntityDuration};
13use crate::error::EpochError;
14use crate::stats::{EntityType, StatPair, Symbols};
15
16/// The structure that holds everything we need for name generation.
17#[derive(Debug, Serialize, Deserialize, Default)]
18pub struct NamingData {
19    /// First name for characters
20    pub first_names: Vec<String>,
21    /// Last names for characters
22    pub last_names: Vec<String>,
23    /// Good prefixes for towns
24    pub town_prefixes: Vec<String>,
25    /// Good suffixes for town names
26    pub town_suffixes: Vec<String>,
27
28    /// Good blocker prefixes
29    pub blocker_prefixes: Vec<String>,
30    /// Good blocker suffixes
31    pub blocker_suffixes: Vec<String>,
32    /// Character adjectives
33    pub character_adjectives: Vec<String>,
34    /// Name prefixes
35    pub character_prefixes: Vec<String>,
36
37    // Exotics names
38    /// Exotic name prefixes
39    pub exotic_prefixes: Vec<String>,
40    /// Exotic infixes
41    pub exotic_infixes: Vec<String>,
42    /// Exotic suffixes
43    pub exotic_suffixes: Vec<String>,
44    /// Mighty mystical adjectives
45    pub mystical_adjectives: Vec<String>,
46    /// A list of event ideas and characteristic associated to it
47    pub event_ideas: Vec<EventDescription>,
48}
49
50#[derive(Debug, Serialize, Deserialize)]
51/// An event with a description
52pub struct EventDescription {
53    /// A short and meaninful name (an id of a sort)
54    pub name: String,
55
56    /// A short description of the event
57    pub description: String,
58
59    /// The stats affected by the event, and how much (-20 to +20)
60    /// (100 being the maximum of a stat, event a change of -10 is meaningful)
61    pub stats: Vec<StatPair>,
62
63    /// The nature of the event duration
64    pub duration: EntityDuration,
65
66    /// How the event will be (re)applied
67    pub application: EffectApplication,
68
69    /// The maximum bounds of the event
70    pub effect_bounds: EffectBounds,
71}
72
73impl Displayable for EventDescription {
74    fn oneline_display(&self) -> String {
75        let (description, _) = self.description.split_at(110.min(self.description.len()));
76        let line1 = format!(
77            "├─ {} {} ──────> {}{} \n\
78            │   ├─  {}",
79            self.name,
80            EntityType::Event.to_symbol(),
81            self.duration.to_symbol(),
82            self.effect_bounds.to_symbol(),
83            description,
84        );
85
86        let mut line2 = "│   └─  Stats: ".to_string();
87        for effect in &self.stats {
88            line2.push_str(&format!("{}:{} ", effect.stat.to_shortest(), effect.value,));
89        }
90
91        format!("{line1}\n{line2}")
92    }
93}
94
95impl NamingData {
96    /// Load the data from the supplied path.  
97    /// If the file does **not** exist, a default (embedded) version is written
98    /// to that path and returned.
99    pub fn load_or_create<P: AsRef<Path>>(path: P) -> io::Result<Self> {
100        let path = path.as_ref();
101
102        if path.exists() {
103            // File exists → read & parse
104            let contents = fs::read_to_string(path)?;
105            let data: Self = serde_json::from_str(&contents)?;
106            Ok(data)
107        } else {
108            // File does *not* exist → create from embedded default
109            let default =
110                serde_json::from_str(DEFAULT_JSON_STR).expect("Default JSON is malformed");
111            // Ensure parent dirs exist
112            if let Some(parent) = path.parent() {
113                fs::create_dir_all(parent)?;
114            }
115            let json = serde_json::to_string_pretty(&default)?;
116            fs::write(path, json)?;
117            Ok(default)
118        }
119    }
120
121    /// Generate a random **character** name.
122    pub fn generate_character_name(&self) -> String {
123        let mut rng = rand::rng();
124
125        let binding = "Bobman".into();
126        let first = self.first_names.choose(&mut rng).unwrap_or(&binding);
127        let binding = "McBobintown".into();
128        let last = self.last_names.choose(&mut rng).unwrap_or(&binding);
129        format!("{} {}", first, last).trim().to_string()
130    }
131
132    /// Generate a random **town** name.
133    pub fn generate_town_name(&self) -> String {
134        let mut rng = rand::rng();
135        let binding = "Bobtown".into();
136        let prefix = self.town_prefixes.choose(&mut rng).unwrap_or(&binding);
137        let suffix = self.town_suffixes.choose(&mut rng).unwrap_or(&binding);
138        format!("{}{}", prefix, suffix).trim().to_string()
139    }
140
141    /// Pick a random event idea
142    #[allow(clippy::unwrap_used)]
143    pub fn random_event_idea(&self) -> Result<&EventDescription, EpochError> {
144        let mut rng = rand::rng();
145        let event_desc = self
146            .event_ideas
147            .choose(&mut rng)
148            .ok_or(EpochError::RNGPickError {
149                cause: "Picking a random event".into(),
150                help: None,
151            })?;
152        // let mut e = Entity::default();
153
154        // e.name = event_desc.name.clone();
155        // e.change_description(event_desc.description.clone());
156        Ok(event_desc)
157    }
158
159    /// Pick a random event name
160    #[allow(clippy::unwrap_used)]
161    pub fn random_event_name(&self) -> String {
162        if let Ok(event) = self.random_event_idea() {
163            event.name.clone()
164        } else {
165            "The Great Bobbening".to_owned()
166        }
167    }
168
169    /// Generate a random **blocker** name (e.g. “Stone-Gate”, “Witch-Wall”).
170    ///
171    /// The name is formed by choosing a random prefix *and* suffix from the
172    /// corresponding lists.  If either list is empty, a sensible default is
173    /// returned.
174    pub fn generate_blocker_name(&self) -> String {
175        let mut rng = rand::rng();
176
177        let binding = "Mystic ".to_string();
178        let prefix = self.blocker_prefixes.choose(&mut rng).unwrap_or(&binding);
179
180        let binding = "Barrier".to_string();
181        let suffix = self.blocker_suffixes.choose(&mut rng).unwrap_or(&binding);
182
183        format!("{} {}", prefix, suffix).trim().to_string()
184    }
185
186    /// Generate a *calamity* name.
187    ///
188    /// A calamity is a **complex exotic** word (prefix + 2-4 infixes + optional suffix)
189    /// followed by a *mystical* adjective.
190    ///
191    /// Example output:  “Umbral-Shatterfall Cataclysmic”
192    pub fn generate_calamity_name(&self) -> String {
193        let mut rng = rand::rng();
194
195        /* ----- 1️⃣  Build the exotic base --------------------------------- */
196        // Pick a prefix (or fall back to "Umbral")
197        let prefix = self
198            .exotic_prefixes
199            .choose(&mut rng)
200            .unwrap_or(&"Umbral".to_string())
201            .clone();
202
203        // Pick 2-4 infixes (this gives the “complex” feel)
204        let infix_count = rng.random_range(2..=4);
205        let infixes: String = (0..infix_count)
206            .map(|_| {
207                self.exotic_infixes
208                    .choose(&mut rng)
209                    .expect("exotic_infixes must not be empty")
210                    .clone()
211            })
212            .collect();
213
214        // Optional suffix - if you don't have a list, this will be an empty string
215        let suffix = self
216            .exotic_suffixes
217            .choose(&mut rng)
218            .cloned()
219            .unwrap_or_else(String::new);
220
221        // Combine into the base calamity word
222        let base = format!("{}{}{}", prefix, infixes, suffix);
223
224        /* ----- 2️⃣  Append a mystical adjective --------------------------- */
225        let adjective = self
226            .mystical_adjectives
227            .choose(&mut rng)
228            .unwrap_or(&"Cataclysmic".to_string())
229            .clone();
230
231        format!("{} {}", base, adjective).trim().to_string()
232    }
233
234    /// Generates something like:
235    ///   “The Magnificent Sir Aurelius Blackthorn the Swift”
236    /// Random adjective, prefix, base name and suffix are all optional.
237    /// If a list is empty, that part is omitted.
238    pub fn generate_long_character_name(&self) -> String {
239        let mut rng = rand::rng();
240
241        // Helper: get a random element or None
242        let adjective = self.character_adjectives.choose(&mut rng).cloned();
243        let prefix = self.character_prefixes.choose(&mut rng).cloned();
244
245        // Base name (first + last)
246        let base = self.generate_character_name();
247
248        // Assemble pieces
249        let mut parts = Vec::new();
250
251        if let Some(pre) = prefix
252            && rand::random_bool(0.3)
253        {
254            parts.push(pre);
255        }
256
257        parts.push(base);
258
259        if let Some(adj) = adjective
260            && rand::random_bool(0.3)
261        {
262            parts.push(adj);
263        }
264
265        parts.join(" ")
266    }
267
268    /// Generate a exotic character name
269    #[allow(clippy::unwrap_used)]
270    pub fn generate_exotic_character_name(&self) -> String {
271        let mut rng = rand::rng();
272
273        // Pick a random number of syllables for the *first* part
274        let first_len = rng.random_range(2..=4);
275        let pref: &String = self.exotic_prefixes.choose(&mut rng).unwrap();
276        let pref2: &String = self.exotic_prefixes.choose(&mut rng).unwrap();
277
278        let first: String = (0..first_len)
279            .map(|_| self.exotic_infixes.choose(&mut rng).unwrap())
280            .cloned()
281            .collect();
282
283        // Same for the *last* part
284        let last_len = rng.random_range(1..=3);
285        let last: String = (0..last_len)
286            .map(|_| self.exotic_infixes.choose(&mut rng).unwrap())
287            .cloned()
288            .collect();
289
290        // Combine with a space - you could also use a hyphen or nothing
291        format!("{pref}{} {pref2}{}", &first, &last)
292    }
293}
294
295// /// Helper to capitalize the first letter of a word
296// fn capitalize(word: &str) -> String {
297//     let mut chars = word.chars();
298//     match chars.next() {
299//         None => String::new(),
300//         Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
301//     }
302// }
303
304/// The JSON that ships with the binary.  
305/// `include_str!` pulls the file at compile time.
306const DEFAULT_JSON_STR: &str = include_str!("../naming.json");
307
308/// Helper that determines the *runtime* location of the `naming.json` file.  
309fn naming_file_path() -> PathBuf {
310    PathBuf::from("naming.json")
311}
312
313/// Global instance that is lazily initialised on first use.
314pub static NAMING: Lazy<NamingData> = Lazy::new(|| {
315    let path = naming_file_path();
316    NamingData::load_or_create(path).expect("Failed to load naming data")
317});
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use crate::stats::ListStats;
323    use std::collections::HashMap;
324
325    #[test]
326    fn try_load() {}
327    #[test]
328    fn basic_testing() {
329        // 1. Generate a character name
330        let character = NAMING.generate_character_name();
331        println!("Character: {}", character);
332
333        // 2. Generate a town name
334        let town = NAMING.generate_town_name();
335        println!("Town: {}", town);
336
337        // 3. Pick an event idea
338        let character = NAMING.generate_long_character_name();
339        println!("Event idea: {}", character);
340
341        let _ = NAMING.random_event_idea();
342    }
343
344    #[test]
345    // TODO[Work]: Add this to a recap function
346    fn test_all_events() {
347        let mut values: HashMap<ListStats, usize> = HashMap::new();
348        for event in &NAMING.event_ideas {
349            println!("Testing event: {:?}", event);
350            assert!(event.stats.len() > 1);
351
352            for stat in &event.stats {
353                if let s @ (ListStats::Demographics
354                | ListStats::Military
355                | ListStats::Economy
356                | ListStats::Social
357                | ListStats::Infrastructure) = stat.stat
358                {
359                    panic!("Bad stat used {s:?}")
360                };
361                match stat.value.abs() {
362                    0..5 => (),
363                    5..10 => (),
364                    10..20 => println!("Big stat"),
365                    s => panic!("The event has a stat that is way too big {s}"),
366                }
367                let base_value = values.get(&stat.stat).unwrap_or(&0);
368                values.insert(stat.stat, base_value + 1);
369            }
370        }
371        println!("{values:#?}");
372        // panic!()
373    }
374    #[test]
375    fn test_most_naming() {
376        let _ = NAMING.generate_character_name();
377        let _ = NAMING.generate_town_name();
378        let _ = NAMING.random_event_idea().expect("Couldn't generate event");
379        let _ = NAMING.random_event_name();
380        let _ = NAMING.generate_blocker_name();
381        let _ = NAMING.generate_calamity_name();
382        let _ = NAMING.generate_long_character_name();
383        let _ = NAMING.generate_exotic_character_name();
384    }
385
386    #[test]
387    fn event_description() {
388        let event = NAMING.random_event_idea().expect("Generating event");
389        let _ = event.oneline_display();
390    }
391}