lib_efo/stats.rs
1//! Module that holds all statistics for an entity and the helper methods that operate on them.
2//!
3//! The statistics are grouped into thematic sections (Demographics, Economy, Social, Military,
4//! Infrastructure). Each field is a `GenericStat`, an internal wrapper around a numeric value
5//! that provides convenience methods such as `max()`, `clamp()`, and arithmetic ops.
6//!
7//! The documentation for each field follows the same three-part template:
8//! * **Purpose** - What the stat represents.
9//! * **Effect** - Gameplay consequence of the stat.
10//! * **Source** - Formula or function that generates the stat value.
11
12#![deny(clippy::missing_docs_in_private_items)]
13
14use crate::{entity::EntityIdentifier, error::EpochError, thin_newtype, utils::RngLevel};
15use cli_table::{CellStruct, Color, Style, Table};
16use colored::Colorize;
17use derive_more::{Add, Display, Mul, Sub};
18use rand::{Rng, distr::Distribution, rngs::ThreadRng};
19use serde::{Deserialize, Serialize};
20use uuid::Uuid;
21mod implementations;
22
23/// Aggregated statistics for a single entity.
24///
25/// The struct is intentionally flat to keep access fast; the fields are logically grouped
26/// by comment blocks that describe each thematic section.
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28// Derive more
29#[derive(Table, Add, Sub)]
30pub struct OverallStats {
31 // ──────────────────────── Demographics ────────────────────────
32 /// Total economic health of the entity.
33 ///
34 /// **Purpose** - A single value summarizing all economic indicators.
35 /// **Effect** - Drives tax revenue, spending power, and overall economic influence.
36 /// **Source** - Sum of `industry`, `science`, `trade`, and other economic contributors.
37 #[table(title = "DEM", customize_fn = "color_cell_categories")]
38 pub demographics: GenericStat,
39 /// Population count.
40 ///
41 /// **Purpose** - The number of citizens living in the entity.
42 /// **Effect** - Determines resource consumption, tax base, and potential unrest.
43 /// **Source** - Updated by the growth/decay formulas each turn.
44 #[table(title = "POP", customize_fn = "color_cell")]
45 pub population: GenericStat,
46
47 /// Per-turn change in population.
48 ///
49 /// **Purpose** - The growth rate of the population.
50 /// **Effect** - Predicts future needs and signals potential social unrest.
51 /// **Source** - Derived from `population`, `happiness`, and `stability`.
52 #[table(title = "GRW", customize_fn = "color_cell")]
53 pub growth_rate: GenericStat,
54
55 /// Crime level (0-1 scale).
56 ///
57 /// **Purpose** - How frequently crimes occur.
58 /// **Effect** - Can trigger riots and reduce stability.
59 /// **Source** - Result of the crime function (influenced by class disparity and law enforcement).
60 #[table(title = "CRM", customize_fn = "color_cell")]
61 pub crime: GenericStat,
62
63 /// Base happiness of the population (0-1 scale).
64 ///
65 /// **Purpose** - A measure of citizen contentment.
66 /// **Effect** - Influences productivity, tax compliance, and unrest.
67 /// **Source** - Output of the happiness function (affected by class, war, infrastructure).
68 #[table(title = "HPY", customize_fn = "color_cell")]
69 pub happiness: GenericStat,
70
71 // ──────────────────────── Economy ────────────────────────
72 /// Aggregate economic health of the entity.
73 ///
74 /// **Purpose** - A single value summarizing all economic metrics.
75 /// **Effect** - Drives revenue, spending, and empire-wide economic influence.
76 /// **Source** - Sum of `food`, `industry`, `science`, `trade`, etc.
77 #[table(title = "ECO", customize_fn = "color_cell_categories")]
78 pub economy: GenericStat,
79
80 /// Food produced by the entity.
81 ///
82 /// **Purpose** - Amount of consumable resources available.
83 /// **Effect** - Keeps the population alive and productive.
84 /// **Source** - `food = farm_yield + tech_level`.
85 #[table(title = "FOD", customize_fn = "color_cell")]
86 pub food: GenericStat,
87
88 /// Strength of the industry.
89 ///
90 /// **Purpose** - Industrial production capacity.
91 /// **Effect** - Drives tax revenue and unit production.
92 /// **Source** - `industry = factory_output + tech_level`.
93 #[table(title = "IND", customize_fn = "color_cell")]
94 pub industry: GenericStat,
95
96 /// Scientific output.
97 ///
98 /// **Purpose** - Rate of research progress.
99 /// **Effect** - Determines how quickly new technologies are unlocked.
100 /// **Source** - `science = research_center_output + tech_level`.
101 #[table(title = "SCI", customize_fn = "color_cell")]
102 pub science: GenericStat,
103
104 /// Trade volume / exports.
105 ///
106 /// **Purpose** - Measure of export activity.
107 /// **Effect** - Influences influence and wealth.
108 /// **Source** - `trade = trade_network_output`.
109 #[table(title = "TRD", customize_fn = "color_cell")]
110 pub trade: GenericStat,
111 // // ──────────────────────── Social ────────────────────────
112 // /// Overall social prestige.
113 // ///
114 // /// **Purpose** - Reflects the cultural standing of the entity.
115 // /// **Effect** - Affects diplomacy and citizen morale.
116 // /// **Source** - Sum of `culture`, `education`, `tourism`, etc.
117 // #[table(title = "SOC", color = "Color::Blue", bold, customize_fn = "color_cell")]
118 // pub social: GenericStat,
119
120 // /// Cultural output from arts, theatres, museums, etc.
121 // ///
122 // /// **Purpose** - Measure of cultural production.
123 // /// **Effect** - Drives tourism, prestige, and happiness.
124 // /// **Source** - `culture = Σ(cultural_building_output)`.
125 // #[table(title = "CLU", customize_fn = "color_cell")]
126 // pub culture: GenericStat,
127
128 // /// Level of education/literacy.
129 // ///
130 // /// **Purpose** - Measure of the citizenry's knowledge.
131 // /// **Effect** - Improves productivity, science output, and reduces unrest.
132 // /// **Source** - `education = Σ(education_building_output)`.
133 // #[table(title = "EDU", customize_fn = "color_cell")]
134 // pub education: GenericStat,
135
136 // /// Tourism revenue.
137 // ///
138 // /// **Purpose** - Income generated by visitors.
139 // /// **Effect** - Provides an additional income stream.
140 // /// **Source** - `tourism = number_of_tourists x price_per_tourist`.
141 // #[table(title = "TOR", customize_fn = "color_cell")]
142 // pub tourism: GenericStat,
143
144 // /// Cultural influence over the region.
145 // ///
146 // /// **Purpose** - Measure of soft power.
147 // /// **Effect** - Grants diplomatic bonuses and increases sway in the empire.
148 // /// **Source** - `influence = culture x population`.
149 // #[table(title = "INF", customize_fn = "color_cell")]
150 // pub influence: GenericStat,
151
152 // ──────────────────────── Military ────────────────────────
153 /// Overall military power.
154 ///
155 /// **Purpose** - Composite measure of combat capability.
156 /// **Effect** - Determines defensive and offensive strength.
157 /// **Source** - Sum of `army`, `strength`, `fortification`, etc.
158 #[table(title = "MIL", customize_fn = "color_cell_categories")]
159 pub military: GenericStat,
160
161 /// Number of battle-ready units.
162 ///
163 /// **Purpose** - Size of the active army.
164 /// **Effect** - Directly dictates defense and attack potential.
165 /// **Source** - Sum of all trained and equipped units.
166 #[table(title = "ARM", customize_fn = "color_cell")]
167 pub army: GenericStat,
168
169 /// Composite strength of the army.
170 ///
171 /// **Purpose** - Tech, training, and equipment bonuses aggregated.
172 /// **Effect** - Determines combat effectiveness and required force to defeat an enemy.
173 /// **Source** - `strength = Σ(unit_strength) x tech_bonus x training_bonus`.
174 #[table(title = "STR", customize_fn = "color_cell")]
175 pub strength: GenericStat,
176
177 /// Defensive bonus from fortifications.
178 ///
179 /// **Purpose** - Wall and tower defenses.
180 /// **Effect** - Adds a multiplier to overall defense.
181 /// **Source** - `fortification = Σ(fortification_building_bonus)`.
182 #[table(title = "FRT", customize_fn = "color_cell")]
183 pub fortification: GenericStat,
184
185 /// Threat level facing the entity.
186 ///
187 /// **Purpose** - Likelihood of being attacked.
188 /// **Effect** - Triggers early warning alerts and strategic decisions.
189 /// **Source** - `threat = (enemy_proximity + enemy_strength) / defense`.
190 #[table(title = "THR", customize_fn = "color_cell")]
191 pub threat: GenericStat,
192
193 // ──────────────────────── Infrastructure ────────────────────────
194 /// Total infrastructure power.
195 ///
196 /// **Purpose** - Overall built-environment capacity.
197 /// **Effect** - Supports population size and efficiency.
198 /// **Source** - Sum of all infrastructure components.
199 #[table(title = "IFR", customize_fn = "color_cell_categories")]
200 pub infrastructure: GenericStat,
201
202 /// Bureaucratic power.
203 ///
204 /// **Purpose** - Efficiency of the city's administrative machinery.
205 /// **Effect** - Improves resource management and policy implementation.
206 /// **Source** - `bureaucracy = Σ(bureaucracy_building_output)`.
207 #[table(title = "BUR", customize_fn = "color_cell")]
208 pub bureaucracy: GenericStat,
209
210 /// Transportation level.
211 ///
212 /// **Purpose** - Availability and quality of transport infrastructure.
213 /// **Effect** - Impacts logistics, mobility, and trade.
214 /// **Source** - `transportation = Σ(transport_building_output)`.
215 #[table(title = "TRP", customize_fn = "color_cell")]
216 pub transportation: GenericStat,
217
218 /// Sprawl / urban density.
219 ///
220 /// **Purpose** - Measure of city sprawl and clutter.
221 /// **Effect** - Affects health, happiness, and infrastructure efficiency.
222 /// **Source** - `sprawl = Σ(sprawl_factor)`.
223 #[table(title = "SPR", customize_fn = "color_cell")]
224 pub sprawl: GenericStat,
225
226 /// Health infrastructure quality.
227 ///
228 /// **Purpose** - Public health capacity.
229 /// **Effect** - Affects population health and can generate unrest.
230 /// **Source** - `health = output_of_health_function` (influenced by sanitation, disease spread, etc.).
231 #[table(title = "HLT", customize_fn = "color_cell")]
232 pub health: GenericStat,
233}
234
235/// Color the base cells
236fn color_cell(cell: CellStruct, value: &GenericStat) -> CellStruct {
237 match (**value).cmp(&0) {
238 std::cmp::Ordering::Less => cell.foreground_color(Some(Color::Red)),
239 std::cmp::Ordering::Equal => cell,
240 std::cmp::Ordering::Greater => cell, //.foreground_color(Some(Color::Green)),
241 }
242}
243
244/// Color the cell categories
245fn color_cell_categories(cell: CellStruct, value: &GenericStat) -> CellStruct {
246 match (**value).cmp(&0) {
247 std::cmp::Ordering::Less => cell.foreground_color(Some(Color::Red)).bold(true),
248 std::cmp::Ordering::Equal => cell.foreground_color(Some(Color::Blue)).bold(true),
249 std::cmp::Ordering::Greater => cell.foreground_color(Some(Color::Green)).bold(true),
250 }
251}
252
253impl OverallStats {
254 /// Create Stats with a given rng distribution
255 pub(crate) fn create_rng(
256 rng: &RngLevel,
257 mut rng_t: &mut ThreadRng,
258 ) -> Result<OverallStats, EpochError> {
259 let r = rng.to_rng()?;
260 let mut overall_stats = Self {
261 demographics: 0.into(),
262 population: (r.sample(&mut rng_t) as isize).into(),
263 growth_rate: (r.sample(&mut rng_t) as isize).into(),
264 crime: (r.sample(&mut rng_t) as isize).into(),
265 happiness: (r.sample(&mut rng_t) as isize).into(),
266 economy: 0.into(),
267 food: (r.sample(&mut rng_t) as isize).into(),
268 industry: (r.sample(&mut rng_t) as isize).into(),
269 science: (r.sample(&mut rng_t) as isize).into(),
270 trade: (r.sample(&mut rng_t) as isize).into(),
271 military: 0.into(),
272 army: (r.sample(&mut rng_t) as isize).into(),
273 strength: (r.sample(&mut rng_t) as isize).into(),
274 fortification: (r.sample(&mut rng_t) as isize).into(),
275 threat: (r.sample(&mut rng_t) as isize).into(),
276 infrastructure: 0.into(),
277 bureaucracy: (r.sample(&mut rng_t) as isize).into(),
278 transportation: (r.sample(&mut rng_t) as isize).into(),
279 sprawl: (r.sample(&mut rng_t) as isize).into(),
280 health: (r.sample(&mut rng_t) as isize).into(),
281 };
282 overall_stats.update();
283 Ok(overall_stats)
284 }
285
286 /// Updates the inner meta values
287 // TODO: Force the use of this ?
288 pub fn update(&mut self) {
289 self.demographics =
290 (*(self.population + self.growth_rate + self.happiness - self.crime) / 4).into();
291 self.economy = (*(self.food + self.industry + self.trade + self.science) / 4).into();
292 self.military =
293 (*(self.army + self.strength + self.fortification + self.threat) / 4).into();
294 self.infrastructure =
295 (*(self.bureaucracy + self.transportation + self.sprawl + self.health) / 4).into();
296 }
297
298 /// Updates with events
299 /// Updates with leader
300 /// etc.
301 // TODO: Fill this
302 #[allow(dead_code)]
303 fn apply_exteriors(&mut self) {}
304
305 #[cfg(test)]
306 pub(crate) fn linear(base: isize) -> OverallStats {
307 use crate::stats;
308
309 Self {
310 demographics: stats::GenericStat(1 + base),
311 population: stats::GenericStat(2 + base),
312 growth_rate: stats::GenericStat(3 + base),
313 crime: stats::GenericStat(4 + base),
314 happiness: stats::GenericStat(5 + base),
315 economy: stats::GenericStat(6 + base),
316 food: stats::GenericStat(7 + base),
317 industry: stats::GenericStat(8 + base),
318 science: stats::GenericStat(9 + base),
319 trade: stats::GenericStat(10 + base),
320 military: stats::GenericStat(11 + base),
321 army: stats::GenericStat(12 + base),
322 strength: stats::GenericStat(13 + base),
323 fortification: stats::GenericStat(14 + base),
324 threat: stats::GenericStat(15 + base),
325 infrastructure: stats::GenericStat(16 + base),
326 bureaucracy: stats::GenericStat(17 + base),
327 transportation: stats::GenericStat(18 + base),
328 sprawl: stats::GenericStat(19 + base),
329 health: stats::GenericStat(20 + base),
330 }
331 }
332}
333
334/// Strategic context and meta-data for an entity.
335///
336/// All fields are intentionally simple - the logic that interprets them
337/// lives in separate modules (e.g. `map`, `network`, `governance`).
338#[derive(Debug, Clone, Default, Serialize, Deserialize)]
339pub struct MetaStats {
340 // ──── Location & Spatial Data (hidden) ───────────────────────────────
341 /// Cartesian coordinates of the entity on the world map.
342 ///
343 /// **Purpose** - Position of the entity in the simulation grid.
344 /// **Effect** - Used for distance calculations, visibility, and
345 /// map-based event logic.
346 /// **Source** - Supplied by the map generator.
347 pub position: Position,
348
349 /// Identifier for the capital
350 pub capital: Option<TownId>,
351
352 // /// List of nearby resource nodes (mines, forests, farms, etc.).
353 // ///
354 // /// **Purpose** - Resource map around the entity.
355 // /// **Effect** - Drives the entity's specialization and determines
356 // /// the default output mix.
357 // /// **Source** - `resources = filter(resource_list, distance ≤ detection_radius)`.
358 // pub resources: Vec<String>,
359
360 // /// The entity's chosen specialization tag
361 // /// (e.g., “Agricultural Hub”, “Industrial Center”).
362 // ///
363 // /// **Purpose** - Identity that dictates default production multipliers.
364 // /// **Effect** - Alters output curves and trade attractiveness.
365 // /// **Source** - `specialization = tag_assigned_by_player_or_ai`.
366 // pub specialization: String,
367
368 // ──── Strategic Positioning (hidden + public) ─────────────────────
369 /// Current governor or leader assigned to the entity.
370 ///
371 /// **Purpose** - Leadership identity.
372 /// **Effect** - Adds personal bonuses and may modify policies
373 /// or diplomatic stance.
374 /// **Source** - `governor = selected_leader_uuid`.
375 pub governor: Option<CharacterId>,
376
377 // /// The entity's classification (capital, trade hub, frontier post, etc.).
378 // ///
379 // /// **Purpose** - Functional role within the empire.
380 // /// **Effect** - Alters base production, tax rates, and
381 // /// certain event probabilities.
382 // /// **Source** - `role = tag_assigned_by_player_or_ai`.
383 // pub role: String,
384 /// The faction or empire to which the entity currently belongs.
385 ///
386 /// **Purpose** - Political affiliation.
387 /// **Effect** - Determines diplomatic relations, war status,
388 /// and access to empire-wide bonuses.
389 /// **Source** - `faction = current_faction_uuid`.
390 pub faction: Option<FactionId>,
391
392 /// UIDs of all entities the entity is linked to (roads, trade routes,
393 /// alliances, war declarations, etc.).
394 pub links: Vec<TypedLink>,
395
396 /// Prestige / reputation of the entity in the eyes of the empire
397 /// and foreign powers.
398 ///
399 /// **Purpose** - Social standing metric.
400 /// **Effect** - Affects diplomatic influence, trade bonuses,
401 /// and event outcomes.
402 /// **Source** - `prestige = calculate_prestige(...)`.
403 pub prestige: GenericStat,
404
405 /// The reach of the entity, wether local or worldly
406 ///
407 /// **Purpose** - Event validation
408 pub reach: Reach,
409}
410
411/// A generic stat pair with (ListStats, GenericStat), used mainly for clap
412/// Accepts two forms
413/// 1. "cri 10" (two separate tokens)
414/// 2. "cri:10" (one token with a separator)
415#[derive(Debug, Serialize, Deserialize, Clone)]
416pub struct StatPair {
417 /// The stat selected
418 pub stat: ListStats,
419 /// The value of the modifier
420 pub value: GenericStat,
421}
422
423/// A two-dimensional point on the world map.
424///
425/// The struct is a thin wrapper around a pair of `isize` values
426/// representing the X and Y coordinates.
427#[derive(Debug, Default, Serialize, Deserialize, Clone, Add, Sub, Mul)]
428pub struct Position(isize, isize);
429
430/// A grid-aligned position used for chunk look-ups.
431///
432/// A `PositionGrid` stores the coordinates of a chunk in the world
433/// grid. Each component is the index of the chunk in the X and Y
434/// direction.
435#[derive(Debug, Default, Serialize, Deserialize, Clone, Add, Sub, Mul)]
436pub struct PositionGrid(isize, isize);
437
438/// The size of the whole world in pixels.
439///
440/// These constants are internal to the module; they are not part
441/// of the public API.
442// TODO(9): Change to be at least initialized with the world ?
443const GRID_SIZE: (i32, i32) = (1920, 1080);
444/// The size of one world chunk in pixels.
445const CHUNK_SIZE: isize = 200;
446
447impl Position {
448 /// Calculates the Euclidean distance between two points.
449 ///
450 /// The distance is computed in the 2-D Euclidean space.
451 pub fn distance(&self, other: &Self) -> isize {
452 ((self.0 - other.0).pow(2) + (self.1 - other.1).pow(2)).isqrt()
453 }
454
455 /// Converts this point to its corresponding grid position.
456 ///
457 /// The conversion divides the world coordinates by the
458 /// `CHUNK_SIZE` to obtain the chunk indices.
459 pub fn to_grid_distance(&self) -> PositionGrid {
460 PositionGrid(self.0 / CHUNK_SIZE, self.1 / CHUNK_SIZE)
461 }
462
463 /// Returns the inner tuple representation of the position.
464 ///
465 /// This is a convenience method to obtain the raw `(isize, isize)`
466 /// coordinates without wrapping them in the struct.
467 pub fn to_inner(&self) -> (isize, isize) {
468 (self.0, self.1)
469 }
470
471 /// Generates a random position within the world bounds.
472 ///
473 /// The position is chosen uniformly at random using the supplied
474 /// random number generator.
475 pub fn create_rng(r: &mut ThreadRng) -> Result<Self, EpochError> {
476 let x = r.random_range(0..GRID_SIZE.0);
477 let y = r.random_range(0..GRID_SIZE.1);
478 Ok(Self(x as isize, y as isize))
479 }
480}
481
482impl PositionGrid {
483 /// Calculates the Euclidean distance between two grid positions.
484 ///
485 /// The distance is computed by converting both grid positions to
486 /// world coordinates and then using `Position::distance`.
487 pub fn distance(&self, other: &PositionGrid) -> isize {
488 self.to_position().distance(&other.to_position())
489 }
490
491 /// Converts the grid coordinates to world coordinates.
492 ///
493 /// Each component is multiplied by `CHUNK_SIZE` to obtain the
494 /// corresponding world position.
495 pub fn to_position(&self) -> Position {
496 Position(CHUNK_SIZE * self.0, CHUNK_SIZE * self.1)
497 }
498}
499/// A summary of the stats, only for display purposes
500#[allow(clippy::missing_docs_in_private_items)]
501#[derive(Table)]
502pub struct SummaryStats {
503 #[table(title = "Demo")]
504 demographics: GenericStat,
505 #[table(title = "Eco")]
506 economy: GenericStat,
507 // #[table(title = "Social")]
508 // social: GenericStat,
509 #[table(title = "Milit")]
510 military: GenericStat,
511 #[table(title = "Infra")]
512 infrastructure: GenericStat,
513}
514
515impl From<OverallStats> for SummaryStats {
516 fn from(value: OverallStats) -> Self {
517 Self {
518 demographics: value.demographics,
519 economy: value.economy,
520 military: value.military,
521 infrastructure: value.infrastructure,
522 }
523 }
524}
525
526/// Bridge between the CLI and the statistics logic.
527///
528/// Helps convert clap arguments to OverallStats
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
530#[allow(missing_docs)]
531pub enum ListStats {
532 Demographics,
533 Population,
534 Growthrate,
535 Crime,
536 Happiness,
537 Economy,
538 Food,
539 Industry,
540 Science,
541 Trade,
542 Social,
543 Culture,
544 Education,
545 Tourism,
546 Influence,
547 Military,
548 Army,
549 Strength,
550 Fortification,
551 Threat,
552 Infrastructure,
553 Bureaucracy,
554 Transportation,
555 Sprawl,
556 Health,
557}
558
559/// The list of all types of entities
560#[allow(missing_docs)]
561#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
562pub enum EntityType {
563 Character,
564 Player,
565 Town,
566 Monster,
567 Blocker,
568 #[default]
569 Event,
570}
571
572/// Trait to implement some symbol representations
573pub trait Symbols {
574 /// Have a small Symbol to resume it
575 fn to_symbol(&self) -> String;
576 /// Have the shortest display possible
577 fn to_shortest(&self) -> String {
578 self.to_symbol()
579 }
580}
581
582// impl<T: Symbols> Display for T {
583// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584// write!(f, "{}", self.to_symbol())
585// }
586// }
587
588// A stat that is shown to the player *and* can be modified manually.
589thin_newtype!(GenericStat, isize, derives: [Add, Sub]);
590thin_newtype!(TownId, Uuid);
591thin_newtype!(CharacterId, Uuid);
592thin_newtype!(FactionId, usize, derives: [Add, Sub]);
593
594#[derive(Copy, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display, Default)]
595/// A typed link between two entities.
596///
597/// A link is a lightweight association that ties an entity's **identifier**
598/// (`id`) to a *semantic role* (`role`). The role is expressed by the
599/// [`Role`] enum and defines what the link actually means in the
600/// domain model.
601#[display("{}{}({}{})",role, role.to_symbol(),other_id.to_symbol(), other_type.to_symbol())]
602pub struct TypedLink {
603 /// The UUID of the entity that is linked.
604 ///
605 /// The link is *unidirectional*; the `TypedLink` itself does not
606 /// keep a back-reference to the source entity.
607 pub other_id: Uuid,
608 /// The semantic role that this link plays.
609 ///
610 /// For example, a link with `Role::Capital` indicates that the entity
611 /// identified by `id` is the capital of the entity that owns the link.
612 pub role: Role,
613 /// The Type of the entity linked
614 ///
615 /// Even if it can mostly be inferred, some edge cases can be squashed
616 /// early in exchange of some data storred
617 pub other_type: EntityType,
618}
619
620impl TypedLink {
621 /// Simple initializer for the TypedLink type
622 pub fn new(other_id: Uuid, role: Role, other_type: EntityType) -> Self {
623 Self {
624 other_id,
625 role,
626 other_type,
627 }
628 }
629}
630
631#[derive(Copy, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display, Default)]
632/// Semantic role of a link.
633///
634/// The role encodes *what the link represents* in the model. The
635/// variants are intentionally limited to the most common use-cases,
636/// but the enum can be extended if the domain evolves.
637pub enum Role {
638 /// The linked entity has governing authority over the link owner
639 Govern,
640 /// The linked entity is the capital of the link owner
641 Capital,
642 #[default]
643 /// No role has been assigned.
644 Undefined,
645}
646
647// --- TRAITS ---------------------------------------------------------------
648/// A stat is a *game value* that can be converted to two different
649/// normalised ranges - a 100-point range for “gaming” logic and a
650/// 10-point range for “roll & UI” logic. The trait also exposes the
651/// underlying raw value and its visibility.
652///
653// TODO(7): Change this with Deref+From
654pub trait GameStat {
655 /// Convert the raw integer to a 50-point scale.
656 fn to_100(&self) -> isize {
657 (self.get_value().clamp(0, 120) - 10).min(100)
658 }
659
660 /// Convert the raw integer to a 10-point scale.
661 fn to_10(&self) -> isize {
662 self.to_100() / 10
663 }
664
665 /// As a colored delta (when the stat is used as a delta)
666 fn to_delta(&self) -> String {
667 let get_value = self.get_value();
668 if get_value < 0 {
669 format!("{}", get_value.to_string().red())
670 } else {
671 format!("{}", get_value.to_string().green())
672 }
673 }
674
675 /// Return the raw integer value, always bound to 0-70
676 fn get_value(&self) -> isize;
677
678 /// Return the raw integer value, always bound to 0-70
679 fn change_value(&mut self, value: isize);
680
681 /// Create a Gamestat from it's underlying value
682 fn from_value(value: isize) -> Self
683 where
684 Self: Sized;
685}
686
687/// Trait for objects that can expose a stat via a string key.
688/// The trait returns a *boxed* trait object so callers do not need to know
689/// the concrete type that was stored.
690pub trait Accessible {
691 /// Return the requested stat as a `Box<dyn GameStat>`.
692 /// `None` if the key does not exist.
693 fn get_val(&self, key: &str) -> Option<Box<dyn GameStat>>;
694}
695
696/// Represents a position that can be identified in three different ways.
697///
698/// The string format is deliberately simple and is designed to be easy to write
699/// by hand while still being unambiguous to a machine.
700///
701/// | Variant | Syntax in a string |
702/// |---------|--------------------|
703/// | `Exact` | `X,Y` |
704/// | `Grid` | `~X,Y` or `gX,Y` |
705/// | `Id` | `@{uuid}` |
706#[derive(Debug, Clone)]
707pub enum PositionIdentifier {
708 /// Exact position specified as `X,Y`.
709 Exact(Position),
710
711 /// Grid-aligned position specified as `~X,Y` or `gX,Y`.
712 Grid(PositionGrid),
713
714 /// Reference to an epoch by UUID, written as `@{uuid}`.
715 Id(EntityIdentifier),
716}
717
718#[derive(Debug, Clone, Default, Serialize, Deserialize)]
719/// The reach of the entity
720pub enum Reach {
721 /// The entity can affect **any** target - no positional or faction
722 /// restrictions. Use this when the effect is truly global.
723 Global,
724 /// The entity has a *limited* area of effect. The `usize` usually
725 /// represents the radius around the entity's position that the effect covers.
726 Local(usize),
727 /// The entity can only affect members of a particular faction.
728 Faction(FactionId),
729 // /// The entity only affect a specific entity
730 // Entity(EntityIdentifier),
731 /// No reach - the entity should not be applied globally.
732 #[default]
733 None,
734}
735
736/// A description of how a value should be updated.
737///
738/// The enum is intentionally tiny – only the three actions that
739/// matter in our state‑diff engine. Each variant is documented
740/// with a short, practical example so you know exactly when
741/// you’d pick it.
742///
743/// The enum is `Copy`, so you can freely pass it around without
744/// ownership worries. The `From<&str>` implementation below
745/// lets you read the operation name from configuration files or
746/// CLI flags:
747#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
748pub enum Modification {
749 /// The modification will be a *delta*, i.e. an increment.
750 ///
751 /// For numeric values this is equivalent to adding the new
752 /// value to the existing one. For collections it usually
753 /// means “append”.
754 Delta,
755
756 /// The modification will *replace* the existing value.
757 ///
758 /// This is a hard overwrite: the old value is dropped
759 /// entirely and the new one is written in its place.
760 Replace,
761
762 /// The modification will *reset* the underlying value.
763 ///
764 /// For plain types this is typically a default/zero value.
765 /// For `Option<T>` it sets the field to `None`. It can
766 /// also be interpreted as “delete the field entirely”.
767 Delete,
768}