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}