lib_efo/entity.rs
1//! # Entity System
2//!
3//! The `Entity` type represents any object that can participate in the game world.
4//! It encapsulates runtime statistics, temporary bonuses, and metadata,
5//! along with information about its lifetime and the nature of its effects.
6//!
7//! The design intentionally groups behaviour into two primary components,
8//! `OverallStats` and `MetaStats`. The `entity_type` field is a placeholder
9//! for future trait-based dispatch - the enum will eventually be replaced
10//! by a type-safe system. For the moment it provides a quick lookup for
11//! logic that distinguishes creatures, items, terrain, etc.
12//!
13
14use std::str::FromStr;
15
16use derive_more::{Add, Display, Sub};
17use serde::{Deserialize, Serialize};
18use uuid::Uuid;
19
20use crate::{
21 error::EpochError,
22 stats::{self, EntityType, MetaStats, OverallStats, Symbols, TypedLink},
23};
24pub mod implementations;
25
26/// An entity that can be present in the game world.
27///
28/// Entities are mutable only through the entity system's public API.
29/// Their fields are private to prevent accidental state corruption.
30#[derive(Serialize, Deserialize, Debug, Clone, Default)]
31pub struct Entity {
32 /// The name of the entity
33 pub name: String,
34
35 /// A short description of the entity
36 pub description: String,
37
38 /// The global identifier for the entity
39 pub id: Uuid,
40
41 /// Runtime statistics that determine the entity's behaviour.
42 ///
43 /// These values are updated by the game loop and reflect the
44 /// entity's current state (e.g., health, speed, attack).
45 stats: OverallStats,
46
47 /// Temporary statistics that include bonuses from events,
48 /// leaders, or other temporary modifiers.
49 ///
50 /// `temp_stats` is recalculated each turn and does not persist
51 /// beyond the entity's lifetime.
52 #[serde(skip)]
53 temp_stats: Option<OverallStats>,
54
55 /// Non-statistical metadata such as name, rarity, and faction.
56 ///
57 /// This information is not directly part of the entity's combat
58 /// calculations but is useful for UI and logic.
59 metadata: MetaStats,
60
61 /// The categorical type of the entity.
62 ///
63 /// Currently an enum that distinguishes creatures, items,
64 /// terrain, etc. It will be phased out in favor of a trait-based
65 /// system for greater type safety.
66 pub entity_type: EntityType,
67
68 /// The lifetime of the entity within the game world.
69 ///
70 /// The game loop checks this value during each tick to decide
71 /// whether to keep or remove the entity.
72 duration: EntityDuration,
73
74 /// Describes the effect applied by the entity.
75 ///
76 /// This determines when the effect is applied and whether it
77 /// persists after the entity is removed.
78 effect_nature: EffectApplication,
79
80 /// Limits that clamp any stat changes caused by the entity's effect.
81 effect_bounds: EffectBounds,
82}
83impl Entity {
84 /// Iterate over all the links and do the appropriate action
85 pub fn remove_all_links(
86 &self,
87 epoch_info: &mut crate::epoch::EpochInfo,
88 ) -> Result<(), EpochError> {
89 for TypedLink {
90 other_id,
91 role,
92 other_type,
93 } in &self.metadata.links
94 {
95 let entity: &mut Entity =
96 epoch_info.find_entity_strict_as_mut(*other_id, Some(*other_type))?;
97 let id = match role {
98 stats::Role::Govern => entity.remove_governor().map(|i| *i),
99 stats::Role::Capital => entity.remove_capital().map(|i| *i),
100 stats::Role::Undefined => None,
101 };
102 // TODO: Add the other way cleaning, not useful for now but if ever links can be two way,
103 // it needs to be done
104 log::info!(
105 "Removed entity {}{} [{}] from {}{}{} -> {id:?}",
106 other_id.to_symbol(),
107 other_type.to_symbol(),
108 role.to_symbol(),
109 entity.name,
110 entity.id.to_symbol(),
111 entity.entity_type.to_symbol()
112 );
113 }
114 Ok(())
115 }
116}
117
118/// Limits for the maximum and minimum value that an effect may
119/// modify a statistic. The bounds are applied after the effect has
120/// been calculated to keep stats within a sane range.
121///
122/// `min` and `max` are inclusive.
123#[derive(Debug, Serialize, Deserialize, Clone, Default, Add, Sub)]
124#[allow(missing_docs)]
125pub struct EffectBounds {
126 pub min: usize,
127 pub max: usize,
128}
129
130/// How the entity's effect is applied over time.
131#[derive(Debug, Serialize, Deserialize, Clone, Default)]
132pub enum EffectApplication {
133 /// Repeatedly applied every turn while the entity is present.
134 Recurring,
135
136 /// Modifies the attached entity's stats only while present.
137 WhenPresent,
138
139 /// Permanently modifies stats
140 #[default]
141 PermanentEffectToApply,
142
143 /// Indicates that a permanent effect has already been applied.
144 PermanentEffectApplied,
145}
146
147impl FromStr for EffectApplication {
148 type Err = EpochError;
149
150 fn from_str(s: &str) -> Result<Self, Self::Err> {
151 match s.to_ascii_lowercase().as_str() {
152 _ if s.starts_with("re") => Ok(EffectApplication::Recurring),
153 _ if s.starts_with("wh") => Ok(EffectApplication::WhenPresent),
154 _ if s.contains("present") => Ok(EffectApplication::WhenPresent),
155 _ if s.contains("toap") => Ok(EffectApplication::WhenPresent),
156 _ if s.starts_with("appl") => Ok(EffectApplication::WhenPresent),
157 "done" | "ok" => Ok(EffectApplication::PermanentEffectApplied),
158 "todo" => Ok(EffectApplication::PermanentEffectToApply),
159 _ => Err(EpochError::ParsingArgument {
160 argument_parsed: s.to_string(),
161 while_parsing: "EffectApplication",
162 }),
163 }
164 }
165}
166
167/// Represents how long an entity remains active in the world.
168#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
169pub enum EntityDuration {
170 /// The entity persists until it is explicitly destroyed.
171 Permanent,
172
173 /// The entity remains for a fixed number of turns.
174 /// The counter is decremented each tick and the entity becomes
175 /// `Done` once it reaches zero.
176 Duration(usize),
177
178 /// The entity's lifecycle has finished and it should no longer
179 /// be updated. This variant is typically used for one-off
180 /// events or buffs that expire after their final turn.
181 #[default]
182 Done,
183}
184
185/// An identifier that can be supplied on the command line.
186///
187/// The enum is deliberately small so it can be used as a Clap argument type.
188/// It accepts **either**
189/// * a plain name (e.g. `"bob"`), or
190/// * a UUID that is prefixed with an `@` (e.g. `"@3f4c6b7e-d4d1-4b3f-a7a4-0d5c6b8e1f9b"`).
191///
192/// The two variants are kept separate so that downstream code can decide
193/// whether it should treat the value as a human-readable name or a
194/// machine-identifiable UUID.
195#[derive(Debug, Clone, PartialEq, Eq, Display)]
196pub enum EntityIdentifier {
197 /// A human-readable name.
198 Name(String),
199 /// A UUID that was supplied with a leading `@`. The `@` is stripped
200 /// when the enum is constructed - only the raw UUID string is stored.
201 Id(String),
202}
203
204#[allow(clippy::unwrap_used)]
205#[cfg(test)]
206mod test;