feat: add app skeleton with balance module, arena and tower

This commit is contained in:
2026-08-10 15:50:58 +02:00
parent 524f3fdaef
commit bcc6d333d4
5 changed files with 391 additions and 2 deletions

120
src/balance/mod.rs Normal file
View File

@@ -0,0 +1,120 @@
//! 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,
}
/// 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,
},
combat: CombatBalance {
hit_radius: values::HIT_RADIUS,
},
meta: MetaBalance {
gain_per_kill: values::META_GAIN_PER_KILL,
},
caps: Caps::default(),
}
}
}