lib_efo/entity/implementations/
methods.rs

1//! # Entity Methods
2//!
3//! This module implements the public API of the `Entity` struct.  
4//! The API covers:
5//! 1. **Statistical reporting** – pretty tables and pretty‑printed trees.  
6//! 2. **Metadata manipulation** – links, links, governor/capital, prestige, etc.  
7//! 3. **Stat modification** – strict, fuzzy, delta, and randomised changes.  
8//! 4. **Entity creation** – deterministic or RNG‑based.  
9//! 5. **Utility helpers** – position handling, identifier matching.
10
11use cli_table::{Style, WithTitle};
12use colored::*;
13use rand::distr::Distribution;
14
15use crate::{
16    entity::*,
17    epoch::EpochInfo,
18    error::EpochError,
19    items_table,
20    stats::{self, Symbols},
21    utils::RngLevel,
22};
23use uuid::Uuid;
24
25impl Entity {
26    // Display the entity's statistics in a pretty table.
27    ///
28    /// The table contains both the permanent and temporary statistics of the
29    /// entity.  The table is formatted with a custom border and separator
30    /// using the `cli_table` crate.
31    ///
32    /// # Returns
33    /// A `Result` containing the table display on success, or an `EpochError` if the
34    /// conversion to a table string fails.
35    pub fn full_stats_to_table(&self) -> Result<cli_table::TableDisplay, EpochError> {
36        let items = if let Some(t) = &self.temp_stats {
37            vec![&self.stats, t]
38        } else {
39            vec![&self.stats]
40        };
41        self.stat_to_table(items)
42    }
43
44    /// Adds a link (UUID) to the entity's metadata.
45    ///
46    /// If `id_to_add` is already present in the `links` vector, this method does
47    /// nothing - it silently ignores duplicates.  The method is idempotent: calling
48    /// it repeatedly with the same UUID will not change the underlying state.
49    pub fn add_to_links(&mut self, id_to_add: stats::TypedLink) {
50        if !self.metadata.links.contains(&id_to_add) {
51            self.metadata.links.push(id_to_add);
52        }
53    }
54
55    /// Removes a link (UUID) from the entity's metadata.
56    ///
57    /// If the supplied `id_to_remove` exists in `self.metadata.links`, it is
58    /// removed and returned wrapped in `Some`.  If the UUID is not present,
59    /// the method returns `None` and leaves the link list unchanged.
60    pub fn del_from_links(&mut self, id_to_remove: stats::TypedLink) -> Option<stats::TypedLink> {
61        self.metadata
62            .links
63            .iter()
64            .position(|id| *id == id_to_remove)
65            .map(|pos| self.metadata.links.swap_remove(pos))
66    }
67    /// Display the entity's statistics in a pretty table (same format as
68    /// `full_stats_to_table`).
69    ///
70    /// This method is provided for backward compatibility and forwards all
71    /// work to `full_stats_to_table`.
72    ///
73    /// # Returns
74    /// A `Result` containing the table display on success, or an `EpochError` if the
75    /// conversion to a table string fails.
76    pub fn stats_to_table(&self) -> Result<cli_table::TableDisplay, EpochError> {
77        let items = if let Some(t) = &self.temp_stats {
78            vec![t]
79        } else {
80            vec![&self.stats]
81        };
82        self.stat_to_table(items)
83    }
84
85    /// Reset the temporary stats to a given base value.
86    ///
87    /// Useful before performing an update: you can set the temporary stats to
88    /// whatever snapshot you want to start from (or `None` to clear it).
89    pub fn reset_temporary_stats_to(&mut self, other: Option<OverallStats>) {
90        self.temp_stats = other;
91    }
92
93    /// Return an immutable reference to the entity’s live statistics.
94    pub fn get_stats(&self) -> &OverallStats {
95        &self.stats
96    }
97    /// Return an immutable reference to the temporary statistics snapshot,
98    /// if one is currently stored.
99    pub fn get_temp_stats(&self) -> &Option<OverallStats> {
100        &self.temp_stats
101    }
102
103    /// Display the entity's statistics in a pretty table (same format as
104    /// `full_stats_to_table`).
105    ///
106    /// This method is provided for backward compatibility and forwards all
107    /// work to `full_stats_to_table`.
108    ///
109    /// # Returns
110    /// A `Result` containing the table display on success, or an `EpochError` if the
111    /// conversion to a table string fails.
112    ///
113    pub(crate) fn stat_to_table(
114        &self,
115        items: Vec<&OverallStats>,
116    ) -> Result<cli_table::TableDisplay, EpochError> {
117        items_table!(items, self.name.clone(), self.id)
118    }
119
120    /// Pretty-print the entity as a coloured tree.
121    ///
122    /// The output is a `String` that visualises the entity's metadata, type,
123    /// duration, effect bounds, and any links.  Colours are applied using
124    /// `colored::Colorize`.
125    ///
126    /// # Returns
127    /// A `String` containing the formatted tree.
128    pub fn to_pretty_tree(&self, epoch_info: &EpochInfo) -> String {
129        // Helper that prefixes a line with the correct amount of vertical bars.
130        pub(crate) fn indent(level: usize, line: &str) -> String {
131            let bars = "│ ".repeat(level);
132            format!(" {}{}", bars, line)
133        }
134
135        let mut out = String::new();
136
137        out.push_str(&format!(
138            "[{} {} 󰻾[{}]]\n",
139            self.entity_type.to_symbol().blue().bold(),
140            self.name.cyan().bold(),
141            self.id.to_string().blue().bold()
142        ));
143
144        // ── Metadata section
145        out.push_str(&indent(0, &format!("├─ {}\n", "metadata".green())));
146        if let Some(capital) = self.metadata.capital {
147            out.push_str(&indent(
148                1,
149                &format!(
150                    "├─ {}: {}\n",
151                    "capital".bright_green(),
152                    implementations::uuid_to_symbol(*capital, epoch_info, &EntityType::Town)
153                ),
154            ));
155        }
156        if let Some(faction) = self.metadata.faction {
157            out.push_str(&indent(
158                1,
159                &format!(
160                    "├─ {} : {}{}\n",
161                    "faction".bright_green(),
162                    " ".yellow(),
163                    faction.to_string().yellow(),
164                ),
165            ));
166        }
167        if let Some(governor) = self.metadata.governor {
168            out.push_str(&indent(
169                1,
170                &format!(
171                    "├─ {}: {}\n",
172                    "governor".bright_green(),
173                    implementations::uuid_to_symbol(*governor, epoch_info, &EntityType::Character)
174                ),
175            ));
176        }
177        if *self.metadata.prestige > 0 {
178            out.push_str(&indent(
179                1,
180                &format!(
181                    "├─ {}: {}{}\n",
182                    "prestige".bright_green(),
183                    self.metadata.prestige.to_string().yellow(),
184                    " ".yellow()
185                ),
186            ));
187        }
188        out.push_str(&indent(
189            1,
190            &format!(
191                "└─ {}: {}\n",
192                "position".bright_green(),
193                self.metadata.position.to_symbol()
194            ),
195        ));
196
197        // ── Optional links
198        if !self.metadata.links.is_empty() {
199            out.push_str(&indent(0, &format!("├─ {}\n", "links".green())));
200            for (i, link) in self.metadata.links.iter().enumerate() {
201                let last = i == self.metadata.links.len() - 1;
202                let symbol = if last { "└─" } else { "├─" };
203                out.push_str(&indent(
204                    1,
205                    &format!(
206                        "{} {}\n",
207                        symbol,
208                        implementations::uuid_to_symbol(
209                            link.other_id,
210                            epoch_info,
211                            &link.other_type
212                        )
213                        .yellow()
214                    ),
215                ));
216            }
217        }
218        // ── Other top-level fields
219        out.push_str(&indent(
220            0,
221            &format!(
222                "├─ {}: {}\n",
223                "duration".green(),
224                self.duration.to_symbol().yellow()
225            ),
226        ));
227        out.push_str(&indent(
228            0,
229            &format!("├─ {}: {:?}\n", "effect_nature".green(), self.effect_nature),
230        ));
231        out.push_str(&indent(
232            0,
233            &format!(
234                "└─ {}: {}",
235                "effect_bounds".green(),
236                self.effect_bounds.to_symbol()
237            ),
238        ));
239
240        out
241    }
242
243    /// Create an entity using a specific RNG level.
244    ///
245    /// The entity is initialised with random statistics, a random position,
246    /// and a random prestige value derived from the supplied `RngLevel`.
247    ///
248    /// # Arguments
249    /// * `e_type` - The type of entity to create.
250    /// * `rng` - The RNG level to use when generating random values.
251    ///
252    /// # Returns
253    /// * `Ok(Self)` - The newly created entity.
254    /// * `Err(EpochError)` - Propagated from any randomisation failure.
255    ///
256    pub fn create_rng(e_type: &EntityType, rng: &RngLevel) -> Result<Self, EpochError> {
257        let mut r = rand::rng();
258        Ok(Self {
259            id: Uuid::new_v4(),
260            stats: OverallStats::create_rng(rng, &mut r)?,
261            temp_stats: None,
262            metadata: MetaStats {
263                prestige: stats::GenericStat(rng.to_rng()?.sample(&mut r) as isize),
264                position: stats::Position::create_rng(&mut r)?,
265                ..Default::default()
266            },
267            entity_type: *e_type,
268            ..Default::default()
269        })
270    }
271
272    /// Increment the time of the entity
273    pub(crate) fn increment_time(&mut self) {
274        match self.duration {
275            EntityDuration::Permanent => (),
276            EntityDuration::Duration(d) => {
277                if d <= 1 {
278                    self.duration = EntityDuration::Done;
279                } else {
280                    self.duration = EntityDuration::Duration(d.saturating_sub(1))
281                }
282            }
283            EntityDuration::Done => (),
284        }
285    }
286}