Battle
The Kessrel Line battle, as data. Three thousand human line troopers in ten regiments hold a fortified canyon against waves of Vesh — thousands alive at once — for seven waves, while the last ship loads at the Gate of Harrow behind them.
game.sim.Battle
The Kessrel Line battle, as data. Three thousand human line troopers in ten regiments hold a fortified canyon against waves of Vesh — thousands alive at once — for seven waves, while the last ship loads at the Gate of Harrow behind them.
Nothing in here is an entity and nothing is physics. The whole field is one flat
unit table (game.sim.units) that the crowd renderer reads, and the rules run
over regiment blocks and alien packs at a fixed fifteen-hertz tick.
The scene entrypoint puts this on mount_sim. It takes no fields but one:
entity.find("mount_sim").component.get("game.sim.Battle"):setField("debugDraw", true)
What it simulates
Regiments. Ten blocks of three hundred, thirty across and ten deep. A block
carries a count, a morale, a stance and a post; its soldiers are the first
count slots of its range, ordered front rank first, so a regiment that loses
men empties from the rear the way a real formation steps up.
Packs. The Vesh arrive as packs — forty skitters, ten brutes, one titan — each owning one contiguous block of its caste's index range. A pack has a centroid, a heading, a hit-point pool and a fixed scatter arrangement chosen from eight per caste, so a hundred packs are not a hundred copies of one clump. Unit positions are the centroid plus that scatter rotated by the pack's heading; no per-unit steering runs at all.
Movement. Far from its target a pack is carried down the canyon by the
canyon; close in it hunts (FUNNEL_NEAR / FUNNEL_FAR). Packs of a caste push
off each other over a coarse spatial grid so the tide spreads rather than
stacking, and the canyon walls (K.halfWidthAt) hold it in.
Fire. A regiment concentrates on the three nearest packs inside its range AND its arc, in a 58 / 26 / 16 split, at triple weight inside its close band. The arc is why where a regiment stands decides what it can answer, and the close band is why holding a line is worth more than spreading thin.
Melee, morale, breaking. A pack in contact kills soldiers; losses and time in
contact drain morale; under K.BREAK_MORALE a regiment breaks, turns south and
runs, and the tide stops chasing it — the hole it leaves is what costs the Gate.
A broken regiment recovers slowly on its own and instantly under Rally, then
marches back to its post.
Mines. A posted sapper regiment lays an arc of fourteen mines ahead of itself after holding for three seconds. A pack that crosses one sets it off.
Abilities. cmd.ability fires Orbital Lance (a delayed strike that clears a
crater), Mortar Barrage (five walking shells) and Rally (the selection's morale
back to full). Each reports fired, impact and ready on battle.ability.
The Gate and the endings. Aliens within ten metres of the Gate face chew its
hit points down. Zero is defeat; no regiment left standing is defeat; the seventh
wave clearing is victory. Each emits game.end and sets the ending phase.
The bus
Reads cmd.move, cmd.stance, cmd.ability; starts on game.battle and resets
on game.restart. Emits battle.wave, battle.waveEnd, battle.regiment,
battle.gate, battle.ability, battle.kills, battle.titan, game.end, and
battle.mine { x, z } — the one event this slice adds beyond the list in
game.shared. It writes K.battle every tick, including fireIntensity and
meleeIntensity for the sound bed, and speaks its lines through K.say.
Cost
The rule tick and the per-unit pass are measured and readable at runtime:
entity.find("mount_sim").component.get("game.sim.Battle"):stats()
Against a 4 ms budget, with the unit table full (9,825 live records, 271 packs): rule tick 1.2 ms average and 2.3 ms peak, per-frame work 1.6 ms average and 3.1 ms peak. At a played wave 7 (3,400 aliens): tick 0.97 ms, frame 1.5 ms.
That holds because the rules run over ~280 packs and 10 regiments rather than
over 10,608 units; the per-unit work is four writef32 calls and six multiplies,
sharded across three frames; and the whole 339 KB table goes to the GPU in one
writeBytes costing 0.06 ms.
The test surface
debugForceWave(n), debugEndWaveNow(), debugClearField(), debugFillField(),
debugPushTide(z), debugHurtGate(hp), debugHurtRegiment(n, id?),
debugHurtMorale(amount, id?) and resetStats() exist so the endings and the
late waves are provable in seconds rather than in the eight minutes a played
battle takes. Nothing in the game calls them.
Scoped to this part · feeds back into the world's score.