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;