feat: add enemies, spatial grid and soft separation

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.
This commit is contained in:
2026-08-11 09:03:36 +02:00
parent afeb09bdcd
commit 0f93a939c0
5 changed files with 604 additions and 6 deletions

View File

@@ -57,6 +57,18 @@ pub struct EnemyBalance {
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.
@@ -107,6 +119,12 @@ impl Balance {
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,