//! Balancing data (ADR-0001). //! //! This module holds **only data and data structures**. No gameplay logic, no Bevy systems. //! Gameplay reads `Res` and never sees the constants in [`values`]. //! //! The numbers themselves live in [`values`] and are all placeholders; this file only gives //! them their shape. `Balance` is deliberately built from plain fields, numbers and vectors — //! no `Handle`, no `Entity` — so it could be serialized without a redesign if the revision //! triggers of ADR-0001 ever fire. A `serde` derive is explicitly *not* added now. mod values; use bevy::prelude::*; /// The numeric type of every stat and balancing value (ADR-0002 §4). /// /// An alias, not a newtype. `f32` carries seven significant digits, which is enough for the /// slice; ADR-0002 revision trigger 1 says when this is reconsidered. pub type Stat = f32; /// All balancing data of the game, in one place. /// /// Built by exactly one function, [`Balance::from_constants`]. Nothing else constructs a /// `Balance`, which is what makes that function the seam an asset loading path would replace. #[derive(Resource, Debug, Clone)] pub struct Balance { pub arena: ArenaBalance, pub tower: TowerBalance, pub enemy: EnemyBalance, pub combat: CombatBalance, pub meta: MetaBalance, pub caps: Caps, } /// The playing field. #[derive(Debug, Clone)] pub struct ArenaBalance { /// Distance from the tower in the centre to the rim enemies spawn on, in world units. pub radius: Stat, } /// Base values of the tower, before any modifier from ADR-0002 is applied. #[derive(Debug, Clone)] pub struct TowerBalance { /// Damage of a single shot. pub damage: Stat, /// Seconds between two shots — an interval, not a rate (ADR-0002 §3). pub attack_interval: Stat, /// Tower health. The tower dying is what ends a run. pub health: Stat, } /// Base values of the single enemy archetype the skeleton knows. #[derive(Debug, Clone)] pub struct EnemyBalance { /// Movement speed in world units per second. pub speed: Stat, /// Health at wave 1. pub health: Stat, /// Distance from the tower centre at which an enemy stops walking. pub attack_range: Stat, /// Enemies closer than this push each other apart (ADR-0004 §5). pub separation_radius: Stat, /// ‹k›: how many neighbours one enemy takes into account (ADR-0004 §5). pub separation_neighbours: usize, /// ‹f› < 1: the separation offset is clamped to `f x (speed x dt)` (ADR-0004 §5). pub separation_clamp: Stat, /// Living enemies the spawner fills the field up to. Becomes `target_population(wave)`. pub target_population: usize, /// `HARD_ENEMY_CAP`: the measured performance limit, never exceeded by anything. pub hard_cap: usize, } /// Values that describe how things touch each other. #[derive(Debug, Clone)] pub struct CombatBalance { /// A projectile within this distance of an enemy counts as a hit, in world units. pub hit_radius: Stat, } /// Meta progression income — the currency that survives a run. #[derive(Debug, Clone)] pub struct MetaBalance { /// Meta currency granted per killed enemy. pub gain_per_kill: Stat, } /// Upper bounds for the percentage-point sums of the stat pipeline (ADR-0002 §2). /// /// Empty on purpose. ADR-0002 §2 requires every clamp limit to be a balancing value living /// here — one per clamped stat — with no global clamp and no default in code. The skeleton /// has none of the stats that carry a cap (`crit_chance`, `defense_percent`, /// `cooldown_reduction`, `wave_cooldown_reduction`, `free_upgrade_chance`), so there is /// nothing to put in yet. /// // ponytail: an empty struct rather than five caps nothing reads. It exists so the place is // named and reachable as `balance.caps.*`; the fields land with the stats that need them in // the thickening phase, and the compiler will point at this struct when they do. #[derive(Debug, Clone, Default)] pub struct Caps {} impl Balance { /// Builds `Balance` from the constants in [`values`]. /// /// **The one and only constructor** (ADR-0001, decision point 3). Everything that wants /// balancing data goes through the resource this returns; nothing else assembles a /// `Balance`. That makes this function the single seam a later asset loading path — or a /// debug panel that mutates `Balance` in the running process — attaches to. pub fn from_constants() -> Self { Self { arena: ArenaBalance { radius: values::ARENA_RADIUS, }, tower: TowerBalance { damage: values::TOWER_DAMAGE, attack_interval: values::TOWER_ATTACK_INTERVAL, health: values::TOWER_HEALTH, }, enemy: EnemyBalance { speed: values::ENEMY_SPEED, health: values::ENEMY_HEALTH, attack_range: values::ENEMY_ATTACK_RANGE, separation_radius: values::ENEMY_SEPARATION_RADIUS, separation_neighbours: values::ENEMY_SEPARATION_NEIGHBOURS, separation_clamp: values::ENEMY_SEPARATION_CLAMP, target_population: values::ENEMY_TARGET_POPULATION, hard_cap: values::HARD_ENEMY_CAP, }, combat: CombatBalance { hit_radius: values::HIT_RADIUS, }, meta: MetaBalance { gain_per_kill: values::META_GAIN_PER_KILL, }, caps: Caps::default(), } } }