bin_efo/commands/
entity_generic.rs

1//! ## Guide
2//!
3//! The **entity** command manipulates the lists of game objects that live inside an *epoch*.  
4//! An *epoch* is a snapshot of the game state that can be written to disk or rolled back to an earlier one.  
5//! Every entity type (`character`, `town`, `event`, …) has the same set of operations:
6//!
7//! ## Summary
8//!
9//! | Sub‑command | Usage | What it changes |
10//! |-------------|-------|-----------------|
11//! | **add** | `<entity> add [OPTIONS] [IDENTIFIER]` | Creates a new `<entity>`, pushes it to the epoch’s list, and writes the epoch. |
12//! | **del** | `<entity> del <IDENTIFIER>` | Removes the matching `<entity>` from the list and writes the epoch. |
13//! | **edit** | `<entity> edit <IDENTIFIER> [OPTIONS]` | Modifies the stats/position/duration/effect/bounds of the chosen `<entity>` and writes the epoch. |
14//! | **find** | `<entity> find <IDENTIFIER>` | Prints full details of a single `<entity>`. |
15//! | **show** | `<entity> show [<IDENTIFIER>]` | Lists all entities on one line, or shows one `<entity>` in full detail. |
16//! | **diff** | `<entity> diff <IDENTIFIER>` | Prints the difference between the current and previous epoch for that `<entity>`. |
17//!
18//! *`IDENTIFIER`* is either a **name** or a **@tag** (`@1234`).  
19//! If omitted, a random name is generated for the type you are working on.
20//!
21//! ### Options
22//!
23//! | Flag | Description |
24//! |------|-------------|
25//! | `-d, --delta` | Apply a delta to the supplied stats (add or subtract) instead of overwriting them. |
26//! | `-v, --verbose…` | Increase output verbosity. |
27//! | `--rng <RNG>` | How to fill unspecified stats. <br>Possible values: `zero`, `low`, `med`, `high`, `legendary`, `mixed`, `flawed`. Default: `zero`. |
28//! | `--pos-exact <POS>` / `--pos <POS>` | Place the new entity at a specific `x,y` coordinate (the epoch must be in a map context). |
29//! | `--duration <DURATION>` | Lifespan of the entity (e.g. `Permanent`, `Temporary`, `Duration(1)` for a one‑turn event). |
30//! | `--bounds <BOUNDS>` | Effect limits, given as `min,max` (e.g. `10,80`). |
31//! | `--effect <EFFECT>` | How the effect is applied:<br>`recurring`, `when-present`, `permanent-effect-to-apply`, `permanent-effect-applied`. |
32//! | `-s, --stats <STATS>` | One or more `key:value` pairs, e.g. `fod,10 military:8`. |
33//!
34//! ## Usage
35//! All the following example use `chr` as a choice of entity but can be safely substitued to any entity: `event`, `town`, `blocker` etc...
36//!
37//! ```bash
38//! # Create a character named “Aria”
39//! chr add Aria \
40//!   --stats fod:10 mil:8 \
41//!   --rng mixed
42//!
43//! # Edit that character: bump military, shorten duration
44//! chr edit Aria \
45//!   --stats mil:5 \
46//!   --duration Duration:5
47//!
48//! # Show all characters
49//! chr show
50//!
51//! # Show one character in detail
52//! chr show Aria
53//!
54//! # Delete a character
55//! chr del Aria
56//!
57//! # Diff a character between two epochs
58//! chr diff Aria
59//! ```
60//!
61//! > **Tip** – `<entity> diff` automatically loads the current epoch and the previous one, so you don’t need to specify epoch numbers.
62//!
63//! *All changes are persisted immediately to the epoch file after the command finishes.*
64
65use lib_efo::entity::{EffectApplication, EffectBounds, EntityDuration, EntityIdentifier};
66use lib_efo::epoch::epoch_traits::{ConfigWritable, Diffable};
67use lib_efo::naming::NAMING;
68use lib_efo::stats::{Position, Symbols};
69
70use clap::{Args, Subcommand};
71use lib_efo::Displayable;
72use lib_efo::utils::RngLevel;
73use lib_efo::{
74    entity::Entity,
75    epoch::EpochConfig,
76    error::EpochError,
77    stats::{EntityType, StatPair},
78};
79use log::info;
80
81use crate::error::AppError;
82use crate::success::display_success;
83
84/// Commands that operate on an entity.
85///
86/// Each variant represents a different operation that can be performed on an
87/// entity. The variant holds the arguments that are common to all operations.
88#[derive(Subcommand, Debug)]
89pub enum EntitySubCommands {
90    /// Add an entity
91    Add(EntityArgs),
92    /// Delete an entity
93    Del(EntityArgs),
94    /// Edit an existing entity
95    Edit(EntityArgs),
96    /// Find an entity by its identifier
97    Find(EntityArgs),
98    /// Show the details of an entity
99    Show(EntityArgs),
100    /// Show differences between two entities
101    Diff(EntityArgs),
102}
103
104/// Arguments required for any entity operation.
105///
106/// All fields are required. The `rng` field determines how unspecified stat
107/// values are handled when an entity is created or edited.
108#[derive(Args, Debug)]
109pub struct EntityArgs {
110    /// The identifier either a name or a @tag
111    pub identifier: Option<EntityIdentifier>,
112
113    // /// A name to change to
114    // #[arg(long, group = "identifier")]
115    // pub new_name: Option<String>,
116    /// Applies a delta to stats instead of setting the values
117    #[arg(long, short, action = clap::ArgAction::SetTrue, default_value_t = false)]
118    pub delta: bool,
119
120    /// Non specified stat Repartition
121    #[arg(value_enum, long, default_value_t = RngLevel::Zero)]
122    pub rng: RngLevel,
123
124    /// Adds at a specific position
125    #[arg(long, group = "position")]
126    pub pos_exact: Option<Position>,
127
128    /// Adds at a specific position
129    #[arg(long, group = "position")]
130    pub pos: Option<Position>,
131
132    /// The duration of the entity
133    #[arg(long)]
134    pub duration: Option<EntityDuration>,
135
136    /// The bounds of the effect of the entity
137    #[arg(long)]
138    pub bounds: Option<EffectBounds>,
139
140    /// How is the effect of the entity applied
141    #[arg(long)]
142    pub effect: Option<EffectApplication>,
143
144    /// All stats pairs involved
145    #[arg(short, long)]
146    pub stats: Vec<StatPair>,
147}
148
149/// Handles an entity subcommand.
150///
151/// This function loads the main epoch configuration, then dispatches the
152/// provided subcommand to the appropriate handler. It returns an
153/// `AppError` if any operation fails.
154///
155/// # Arguments
156/// * `entity_sub_commands` - The subcommand chosen by the user.
157/// * `e_type` - The type of entity to operate on.
158///
159/// # Returns
160/// A `Result` indicating success (`Ok(())`) or the encountered `AppError`.
161pub fn handle_entity_commands(
162    entity_sub_commands: &EntitySubCommands,
163    e_type: &EntityType,
164) -> Result<(), AppError> {
165    let mut main_epoch = EpochConfig::new_from_path(None)?;
166    match entity_sub_commands {
167        EntitySubCommands::Add(entity_args) => {
168            handle_create_entity(entity_args, e_type, &mut main_epoch)
169        }
170        EntitySubCommands::Del(entity_args) => {
171            handle_delete_entity(entity_args, e_type, &mut main_epoch)
172        }
173        EntitySubCommands::Edit(entity_args) => {
174            handle_edit_entity(entity_args, e_type, &mut main_epoch)
175        }
176        EntitySubCommands::Find(entity_args) => {
177            handle_find_entity(entity_args, e_type, &mut main_epoch)
178        }
179        EntitySubCommands::Show(entity_args) => {
180            handle_show_entity(entity_args, e_type, &mut main_epoch)
181        }
182        EntitySubCommands::Diff(entity_args) => {
183            handle_diff_entity(entity_args, e_type, &mut main_epoch)
184        }
185    }?;
186    Ok(())
187}
188
189/// Function handler for the create subcommand
190fn handle_create_entity(
191    entity_args: &EntityArgs,
192    e_type: &EntityType,
193    main_epoch: &mut EpochConfig,
194) -> Result<(), EpochError> {
195    let epoch_info = main_epoch.try_load_current_as_mut()?;
196
197    if find_entity(entity_args, e_type, epoch_info).is_ok() {
198        Err(EpochError::EntityAlreadyExist(
199            entity_args.identifier.clone(),
200        ))?;
201    };
202
203    let vec_entities: &mut Vec<Entity> = vec_entities_mut(e_type, epoch_info);
204
205    let mut entity = Entity::create_rng(e_type, &entity_args.rng)?;
206    update_stats(entity_args, &mut entity);
207    // Specifying a name means the name needs to be changed
208    // Specifying an id however means nothing and should be ignored
209
210    update_defaults(entity_args, &mut entity);
211
212    info!("Created character: {:#?}", entity);
213    entity.print_full();
214    let message = &crate::success::SuccessMessages::CreatedEntity(
215        entity.name.clone(),
216        entity.entity_type.to_shortest(),
217        entity.id.to_string(),
218    );
219    vec_entities.push(entity);
220
221    // No need to change main epoch that didn't change a serialized field
222    epoch_info.save_with_context("Created entity", &lib_efo::epoch::SaveMode::Erase)?;
223    display_success(message);
224
225    Ok(())
226}
227
228/// Function handler for the delete subcommand
229fn handle_delete_entity(
230    entity_args: &EntityArgs,
231    e_type: &EntityType,
232    main_epoch: &mut EpochConfig,
233) -> Result<(), EpochError> {
234    let epoch_info = main_epoch.try_load_current_as_mut()?;
235
236    let vec_entities: &mut Vec<Entity> = vec_entities_mut(e_type, epoch_info);
237    let index = vec_entities
238        .iter()
239        .position(|e| find_by_args(e, entity_args))
240        .ok_or(EpochError::EntityNotFound(
241            e_type.clone(),
242            entity_args.identifier.clone(),
243        ))?;
244
245    let entity = vec_entities.swap_remove(index);
246
247    epoch_info.save_with_context("Deleted entity", &lib_efo::epoch::SaveMode::Erase)?;
248    display_success(&crate::success::SuccessMessages::DeletedEntity(
249        entity.name,
250        entity.entity_type.to_shortest(),
251        entity.id.to_string(),
252    ));
253    Ok(())
254}
255
256/// Function handler for the diff subcommand
257fn handle_diff_entity(
258    entity_args: &EntityArgs,
259    e_type: &EntityType,
260    main_epoch: &mut EpochConfig,
261) -> Result<(), EpochError> {
262    // Load the epochs to diff
263    let id = main_epoch.epoch_id;
264    let id2 = main_epoch.epoch_id.add_to_epoch(-1);
265    main_epoch.try_load(&id)?;
266    main_epoch.try_load(&id2)?;
267
268    let epoch_current = unsafe { main_epoch.try_load_id(id) }?;
269    let epoch_past = unsafe { main_epoch.try_load_id(id2) }?;
270    let current_entity = find_entity(entity_args, e_type, epoch_current)?;
271    let past_entity = find_entity(entity_args, e_type, epoch_past)?;
272
273    let diff = current_entity.delta(past_entity);
274    display_success(&crate::success::SuccessMessages::DiffBetweenEpoch(id, id2));
275    diff.print_full();
276
277    Ok(())
278}
279
280/// Function handler for the show subcommand
281// TODO: Add filters else this will become very clustered
282fn handle_show_entity(
283    entity_args: &EntityArgs,
284    e_type: &EntityType,
285    main_epoch: &mut EpochConfig,
286) -> Result<(), EpochError> {
287    let epoch_info = main_epoch.try_load_current()?;
288    if entity_args.identifier.is_none() {
289        for entity in vec_entities(e_type, epoch_info) {
290            entity.print_oneline();
291        }
292    } else {
293        let entity = find_entity(entity_args, e_type, epoch_info)?;
294        entity.print_full();
295    }
296    Ok(())
297}
298
299/// Function handler for the find subcommand
300fn handle_find_entity(
301    entity_args: &EntityArgs,
302    e_type: &EntityType,
303    main_epoch: &mut EpochConfig,
304) -> Result<(), EpochError> {
305    handle_show_entity(entity_args, e_type, main_epoch)
306}
307
308/// Function handler for the edit subcommand
309fn handle_edit_entity(
310    entity_args: &EntityArgs,
311    e_type: &EntityType,
312    main_epoch: &mut EpochConfig,
313) -> Result<(), EpochError> {
314    let epoch_info = main_epoch.try_load_current_as_mut()?;
315    let vec_entities = vec_entities_mut(e_type, epoch_info);
316    let entity = vec_entities
317        .iter_mut()
318        .find(|e| find_by_args(e, entity_args))
319        .ok_or(EpochError::EntityNotFound(
320            e_type.clone(),
321            entity_args.identifier.clone(),
322        ))?;
323
324    for stat in &entity_args.stats {
325        entity.change_stat(stat.clone());
326    }
327    if let Some(duration) = &entity_args.duration {
328        entity.modify_duration(duration.clone());
329    }
330    // // Only update the name if an id was given
331    // if entity_args.id.is_some()
332    //     && let Some(name) = &entity_args.name
333    // {
334    //     entity.name = name.clone();
335    // }
336    if let Some(effect) = &entity_args.effect {
337        entity.modify_effect_application(effect.clone());
338    }
339    if let Some(bounds) = &entity_args.bounds {
340        entity.modify_effect_bounds(bounds.clone());
341    }
342
343    entity.print_full();
344    let message = &crate::success::SuccessMessages::EditedEntity(
345        entity.name.clone(),
346        entity.entity_type.to_shortest(),
347        entity.id.to_string(),
348    );
349    epoch_info.save_with_context("Deleted entity", &lib_efo::epoch::SaveMode::Erase)?;
350
351    display_success(message);
352    Ok(())
353}
354
355/// Update the given stats with the given argumenbts
356/// Note that this function will not change name & id due to the usage variance between caller
357fn update_stats(entity_args: &EntityArgs, e: &mut Entity) {
358    if entity_args.delta {
359        for stat in &entity_args.stats {
360            e.delta_stat(stat.clone());
361        }
362    } else {
363        for stat in &entity_args.stats {
364            e.change_stat(stat.clone());
365        }
366    }
367}
368
369/// Looks up an entity inside the given `epoch_info` according to the
370/// supplied arguments.
371///
372/// The function returns a reference to the matching entity or an
373/// `EpochError::EntityNotFound` if no entity matches.
374///
375/// # Arguments
376/// * `entity_args` - The arguments that identify the entity.
377/// * `e_type` - The type of entity to look for.
378/// * `epoch_info` - The epoch that contains all entities.
379///
380/// # Returns
381/// * `Ok(&Entity)` - The entity that matches the arguments.
382/// * `Err(EpochError::EntityNotFound)` - No matching entity was found.
383fn find_entity<'a>(
384    entity_args: &'a EntityArgs,
385    e_type: &'a EntityType,
386    epoch_info: &'a lib_efo::epoch::EpochInfo,
387) -> Result<&'a Entity, EpochError> {
388    vec_entities(e_type, epoch_info)
389        .iter()
390        .find(|&e| find_by_args(e, entity_args))
391        .ok_or(EpochError::EntityNotFound(
392            e_type.clone(),
393            entity_args.identifier.clone(),
394        ))
395}
396
397/// Returns a slice over the entities of the requested type from the given `EpochInfo`.
398///
399/// The function matches the supplied `e_type` to the appropriate collection
400/// inside `epoch_info`. For unimplemented entity types the call will panic
401/// because of the `todo!()` placeholder.
402///
403/// # Arguments
404/// * `e_type` - The type of entities to retrieve.
405/// * `epoch_info` - The epoch information containing the collections.
406///
407/// # Returns
408/// A reference to the vector containing the requested entities.  
409/// The lifetime of the returned slice is tied to that of `epoch_info`.
410#[inline]
411fn vec_entities<'a>(
412    e_type: &'a EntityType,
413    epoch_info: &'a lib_efo::epoch::EpochInfo,
414) -> &'a Vec<Entity> {
415    match e_type {
416        EntityType::Character => &epoch_info.characters,
417        EntityType::Player => todo!(),
418        EntityType::Town => &epoch_info.towns,
419        EntityType::Monster => &epoch_info.calamities,
420        EntityType::Blocker => &epoch_info.blockers,
421        EntityType::Event => &epoch_info.events,
422    }
423}
424
425#[inline]
426/// Returns a mutable slice over the entities of the requested type from the given
427/// `EpochInfo`.
428///
429/// Like `vec_entities`, this function selects the appropriate collection
430/// based on `e_type`. Unimplemented variants will panic due to the `todo!()`.
431///
432/// # Arguments
433/// * `e_type` - The type of entities to retrieve.
434/// * `epoch_info` - The epoch information containing the collections.
435///
436/// # Returns
437/// A mutable reference to the vector containing the requested entities.  
438/// The lifetime of the returned slice is tied to that of `epoch_info`.
439fn vec_entities_mut<'a>(
440    e_type: &'a EntityType,
441    epoch_info: &'a mut lib_efo::epoch::EpochInfo,
442) -> &'a mut Vec<Entity> {
443    match e_type {
444        EntityType::Character => &mut epoch_info.characters,
445        EntityType::Player => todo!(),
446        EntityType::Town => &mut epoch_info.towns,
447        EntityType::Monster => &mut epoch_info.calamities,
448        EntityType::Blocker => &mut epoch_info.blockers,
449        EntityType::Event => &mut epoch_info.events,
450    }
451}
452
453/// Helper function finding by name, note that a lot more helpers could be done to find by other criterias
454fn find_by_args(entity: &Entity, entity_args: &EntityArgs) -> bool {
455    match entity_args.identifier {
456        Some(EntityIdentifier::Name(ref name)) => entity.name.starts_with(name),
457        Some(EntityIdentifier::Id(ref id)) => entity.id.hyphenated().to_string().starts_with(id),
458        None => false,
459    }
460}
461
462/// Find an entity matching a tag
463#[allow(dead_code)]
464fn find_by_id<'a>(entities: &'a [Entity], id: &'a String) -> Option<&'a Entity> {
465    entities
466        .iter()
467        .find(|e| e.id.hyphenated().to_string().starts_with(id))
468}
469
470/// Find an entity matching a name
471#[allow(dead_code)]
472fn find_by_name<'a>(entities: &'a [Entity], id: &'a String) -> Option<&'a Entity> {
473    entities.iter().find(|e| e.name.starts_with(id))
474}
475
476/// Update the default values of the entity with sensible defaults
477fn update_defaults(entity_args: &EntityArgs, e: &mut Entity) {
478    let entity_type = &e.entity_type;
479
480    let (duration, effect, bounds, name) = match entity_type {
481        EntityType::Character => (
482            lib_efo::entity::EntityDuration::Permanent,
483            lib_efo::entity::EffectApplication::WhenPresent,
484            EffectBounds { min: 20, max: 110 },
485            NAMING.generate_character_name(),
486        ),
487        EntityType::Player => (
488            lib_efo::entity::EntityDuration::Permanent,
489            lib_efo::entity::EffectApplication::WhenPresent,
490            EffectBounds { min: 0, max: 120 },
491            NAMING.generate_exotic_character_name(),
492        ),
493        EntityType::Town => (
494            lib_efo::entity::EntityDuration::Permanent,
495            lib_efo::entity::EffectApplication::WhenPresent,
496            EffectBounds { min: 20, max: 60 },
497            NAMING.generate_town_name(),
498        ),
499        EntityType::Monster => (
500            lib_efo::entity::EntityDuration::Permanent,
501            lib_efo::entity::EffectApplication::WhenPresent,
502            EffectBounds { min: 20, max: 80 },
503            NAMING.generate_calamity_name(),
504        ),
505        EntityType::Blocker => (
506            lib_efo::entity::EntityDuration::Permanent,
507            lib_efo::entity::EffectApplication::WhenPresent,
508            EffectBounds { min: 0, max: 120 },
509            NAMING.generate_blocker_name(),
510        ),
511        EntityType::Event => (
512            lib_efo::entity::EntityDuration::Duration(1),
513            lib_efo::entity::EffectApplication::PermanentEffectToApply,
514            EffectBounds { min: 10, max: 80 },
515            NAMING.random_event_name(),
516        ),
517    };
518    // This is the sensible default for all creatures & characters
519    if let Some(duration) = &entity_args.duration {
520        e.modify_duration(duration.clone());
521    } else {
522        e.modify_duration(duration);
523    }
524
525    if let Some(EntityIdentifier::Name(name)) = &entity_args.identifier {
526        e.name = name.clone()
527    } else {
528        e.name = name
529    }
530
531    // This is the sensible default for all creatures & characters
532    if let Some(effect) = &entity_args.effect {
533        e.modify_effect_application(effect.clone());
534    } else {
535        e.modify_effect_application(effect);
536    }
537
538    // This is the sensible default for characters
539    if let Some(bounds) = &entity_args.bounds {
540        e.modify_effect_bounds(bounds.clone());
541    } else {
542        e.modify_effect_bounds(bounds);
543    }
544}