Enemies spawn on the arena rim, walk radially at the tower and stop at the attack range. The spawner is cap-driven — it tops the field up to the target population instead of placing N enemies on a schedule — so the target population is the binding quantity the difficulty curve will stand on. The spatial grid of ADR-0004 lands with them, because it now has its first consumer and #17 needs it unchanged for target selection and projectile collision. One full rebuild per frame is the only pass over the enemy set; every consumer asks the grid. Soft separation is a clamped positional offset, not a force: overlap stays allowed, and capping the offset below one movement step is what keeps the tower ring from pulsing. Closes #16.
139 lines
5.5 KiB
Rust
139 lines
5.5 KiB
Rust
//! Balancing data (ADR-0001).
|
||
//!
|
||
//! This module holds **only data and data structures**. No gameplay logic, no Bevy systems.
|
||
//! Gameplay reads `Res<Balance>` 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(),
|
||
}
|
||
}
|
||
}
|