The pack
waveArena
function waveArena(): Pack<{ waveArena: WaveArenaApi }>
Install the renderer-independent wave-arena rules (Robotron). Projectiles are flown by projectile-patterns.
Helpers
arenaSiegePreset
function arenaSiegePreset(o: ArenaSiegePresetOptions = {}): WaveArenaConfig
A materially different wave-arena game on the same rules: a wide arena, a faster player with six lasers, eight authored waves where the mines (electrodes) must be cleared too, one kind of civilian with a flatter rescue table, drone swarms that are grunts of a different size and score, brutes (hulks) that hunt only the player, and motherships (spheroids) that carry more hunters.
createWaveArena
function createWaveArena(config: WaveArenaConfig): WaveArena
Create a wave-arena game.
robotronKinds
function robotronKinds(): WaveKind[]
The ten 1982 kinds plus the three family members. Sizes are the original sprites' boxes in pixels.
robotronPreset
function robotronPreset(o: RobotronPresetOptions = {}): WaveArenaConfig
Robotron: 2084 (Williams, 1982) as a wave-arena definition.
robotronWaveParams
function robotronWaveParams(wave: number): WaveParams
The original's per-wave parameters for a 1-based wave (1..40).
robotronWaves
function robotronWaves(): WaveDef[]
The original's 40 authored waves (after wave 40 the game repeats waves 21-40).
validateWaveArena
function validateWaveArena(c: WaveArenaConfig): void
Throw a descriptive error for an unsupported configuration.
waveDifficulty
function waveDifficulty(name: "easy" | "normal" | "arcade"): WaveDifficulty
Named difficulty settings: "arcade" is the original tables, "normal" a playable ramp, "easy" a gentle one.
Classes
WaveArena
class WaveArena
The wave-arena rules owner. Create with createWaveArena.
readonly config: WaveArenaConfig— The definition this game runs.readonly projectiles: Projectiles— The projectile owner that flies and collides every shot.readonly difficulty: WaveDifficulty— The difficulty in force (resolved fromconfig.difficulty).constructor(config: WaveArenaConfig)get state(): WaveArenaState— A read-only copy of the whole state.kind(id: string): WaveKind— The kind definition for an id.waveDef(wave: number): WaveDef— The definition of a 1-based wave, followingrepeatFrompast the authored list.waveCounts(wave: number): Record<string, number>— The counts a 1-based wave starts with after difficulty shaping: each non-human count is multiplied by min(maxPopulation, population + ramp x (wave - 1)), rounded, kept at least 1 when the table has any, and capped at maxCount. With the arcade difficulty these are the authored counts.required(kind: string): boolean— True when this kind must be destroyed to clear the wave.shotPosition(id: number): { x: number; y: number; vx: number; vy: number } | null— Projectile positions by id (for drawing).reset(wave = 1): void— Start a new game at a 1-based wave.tick(seconds: number, input: WaveArenaInput = {}): void— Advance by real seconds, running whole frames (at most 8 per call).step(input: WaveArenaInput = {}): void— Advance exactly one frame.drain(): WaveArenaEvent[]— Drain queued events.snapshot()— Serializable snapshot of the whole game.restore(snap: ReturnType<WaveArena["snapshot"]>): void— Restore a snapshot taken from the same configuration.
Types
ArenaSiegePresetOptions
interface ArenaSiegePresetOptions
Options for arenaSiegePreset.
width?: number— Arena width in pixels (default 360).height?: number— Arena height in pixels (default 216).lives?: number— Starting lives (default 5).difficulty?: "easy" | "normal" | "arcade" | WaveDifficulty— Difficulty (default "arcade": the variant's authored waves as written).seed?: number— Seed.
RobotronPresetOptions
interface RobotronPresetOptions
Options for robotronPreset.
lives?: number— Starting lives. Original default: 3.extraEvery?: number— Extra life every N points (0 = none). Original default: 25,000.laserMax?: number— Most lasers in flight. Original: 4.laserRepeat?: number— Frames between held shots. Original: 8.rescue?: number[]— Rescue score table. Original: 1000..5000.difficulty?: "easy" | "normal" | "arcade" | WaveDifficulty— Difficulty: "arcade" (default, the original tables exactly), "normal", "easy", or explicit values.seed?: number— Seed.
WaveActor
interface WaveActor
An actor in the arena (enemy, electrode or human).
id: string— Unique id, never reused.kind: string— Kind id.behaviour: WaveBehaviour— Family.x: number— Centre.y: number— Centre.w: number— Hit box.h: number— Hit box.vx: number— Velocity (px per frame) for gliding families.vy: number— Velocity (px per frame).dir: Dir8— Facing / heading for presenters.anim: number— Animation step counter.wait: number— Process countdown (frames or family ticks).timer: number— Secondary countdown (turns, shots).timer2: number— Tertiary countdown (drops, re-aims).carry: number— Count left to drop (spheroids, quarks).phase: string— Family phase: grow, roam, drop, escape, reprogram.target: string— Target actor id ("" = the player).ox: number— Target offsets.oy: number— Target offsets.
WaveArenaApi
type WaveArenaApi = typeof waves & typeof presets & { dir8: typeof dir8; overlaps: typeof overlaps }
game.waveArena: the wave-arena rules owner, its presets and helpers.
WaveArenaConfig
interface WaveArenaConfig
The complete wave-arena definition. robotronPreset supplies the 1982 values.
arena: { left: number; top: number; right: number; bottom: number }— Playfield bounds in model pixels.fps: number— Simulation frames per second. Original: 60.player: { /** Start centre. */ x: number; /** Start centre. */ y: number; /** Hit box. */ w: number; /** Hit box. */ h: number; /** Pixels per frame on each axis. Original: 1 and 1. */ speedX: number; /** Pixels per frame on each axis. Original: 1 and 1. */ speedY: number; /** Starting lives (including the one in play). */ lives: number; /** Extra life every this many points (0 = none). Original: 25,000. */ extraEvery: number; }— The player.laser: { /** Most lasers in flight. Original: 4. */ max: number; /** Pixels per frame on each moving axis. Original: 6. */ speed: number; /** Frames a direction must be held before the first shot. Original: 2. */ firstDelay: number; /** Frames between shots while held. Original: 8. */ repeat: number; /** Hit radius. */ radius: number; }— The player's lasers.kinds: WaveKind[]— All kinds.waves: WaveDef[]— The authored waves, played in order.repeatFrom: number— After the last wave, play continues from this wave index (0-based). Original: 20 (wave 21).rescue: { scores: number[] }— Rescue scoring: the nth human saved since the last death or wave start pays scores[min(n, len) - 1].grunt: { /** Frames between grunt passes. Original: 4. */ passFrames: number; /** Step size. Original: 4 px across, 4 px down. */ stepX: number; /** Step size. */ stepY: number; /** Frames between speed-ups. Original: 225 (15 x 15). */ speedUpFrames: number; /** Speed-ups stop while at least this many grunts are alive. Original: 30. */ speedUpBelow: number; /** Countdown factor applied each time a grunt is shot. Original: 0.875. */ killFactor: number; /** Grunts die (and destroy the electrode) when they walk into an electrode. */ electrodesKill: boolean; }— Grunt family.hulk: { /** Alternating horizontal step sizes. Original: 3, 4. */ stepX: number[]; /** Vertical step size. Original: 2. */ stepY: number; /** Steps between turns: random 1..this. Original: 32. */ turnSteps: number; /** Chance a hulk hunts a human rather than the player. Original: 0.75. */ humanChance: number; /** Random target offset (+/- px). Original: 16 bytes = 32 px horizontally, 16 px vertically. */ jitterX: number; /** Random target offset (+/- px). */ jitterY: number; /** Knock-back from a laser hit, doubled at random. Original: 2 px across, 1 px down. */ knockX: number; /** Knock-back. */ knockY: number; }— Hulk family.human: { /** Frames between steps. Original: 8. */ stepFrames: number; /** Alternating horizontal step sizes. Original: 2, 1. */ stepX: number[]; /** Vertical step. Original: 1. */ stepY: number; /** Steps between turns: random 1..this. Original: 128. */ turnSteps: number; }— Human family.brain: { /** Horizontal step. Original: 2 px. */ stepX: number; /** Vertical step. Original: 1 px. */ stepY: number; /** Reprogramming reach (px). */ reach: number; /** Frames to reprogram a human. Original: 80. */ reprogramFrames: number; /** Kind a reprogrammed human becomes. */ prog: string; }— Brain family.prog: { /** Frames between steps. Original: 3. */ stepFrames: number; /** Step size on the chosen axis. Original: 4 px. */ step: number; /** Chance per step of choosing a new heading. Original: 27/256. */ turnChance: number; }— Prog family.missile: WaveShotKind & { /** Frames between moves. Original: 2. */ moveFrames: number; /** Steps per move. Original: 2. */ steps: number; /** Step size across / down. Original: 2 and 1 px. */ stepX: number; /** Step size down. */ stepY: number; /** Moves between re-aims: random 1..this. Original: 7. */ retarget: number; /** Trail points kept for drawing. Original: 8. */ trail: number; }— Cruise missiles (brains).spheroid: { /** Frames per tick. Original: 2. */ tickFrames: number; /** Velocity caps (px per frame). Original: 2 across, 2 down. */ maxVX: number; /** Velocity cap down. */ maxVY: number; /** Kind it drops. */ drops: string; /** Most of the dropped kind alive at once. Original: 8. */ dropCap: number; }— Spheroid family.enforcer: { /** Frames spent growing after a drop. */ growFrames: number; /** Frames per tick. Original: 3. */ tickFrames: number; /** Ticks between new headings: random 1..this. Original: 32. */ retarget: number; /** Target spread around the player (+/- px). */ spread: number; }— Enforcer family.spark: WaveShotKind & { /** Life in frames: random from..to. Original: 80..140. */ life: [number, number]; /** Aim spread (+/- px). */ spread: number; /** Curve: random acceleration magnitude (px per frame per frame). */ curve: number; }— Enforcer sparks.quark: { /** Frames per tick. Original: 3. */ tickFrames: number; /** Kind it drops. */ drops: string; /** Most of the dropped kind alive at once. Original: 20. */ dropCap: number; /** Grow frames for a dropped tank. Original: 48. */ growFrames: number; }— Quark family.tank: { /** Frames per step. Original: 2. */ stepFrames: number; /** Step size (px). Original: 1. */ step: number; /** Chance a new heading seeks the player. Original: 96/256. */ seekChance: number; /** Steps between turns: random 1..this. Original: 32. */ turnSteps: number; /** Chance a shell is aimed directly rather than banked off a wall. Original: 0.5. */ directChance: number; }— Tank family.shell: WaveShotKind & { /** Life in frames: random from..to. Original: 96..158. */ life: [number, number]; }— Tank shells.mercy?: { wave: number; spheroidDrop: number; enforcerShot: number; gruntDelay: number; gruntMinDelay: number }[]— Mercy table for weak players (the original's "bozo" table): waves 1..n, used on a restart on the last life.introFrames: number— Frames of the wave-start appearance (nothing moves).clearFrames: number— Frames between the wave clearing and the next wave.deathFrames: number— Frames of the death sequence.difficulty?: WaveDifficulty— Difficulty shaping (default: the authored tables exactly, aswaveDifficulty("arcade")).seed?: number— Seed for every random choice.
WaveArenaEvent
type WaveArenaEvent = | { kind: "wave-start"; wave: number } | { kind: "wave-clear"; wave: number } | { kind: "killed"; id: string; actor: string; x: number; y: number; score: number; by: "laser" | "electrode" | "hulk" } | { kind: "shot-down"; shot: WaveShot["kind"]; x: number; y: number; score: number } | { kind: "hulk-hit"; id: string; x: number; y: number } | { kind: "rescued"; id: string; actor: string; x: number; y: number; score: number } | { kind: "human-lost"; id: string; actor: string; x: number; y: number; by: "hulk" | "brain" } | { kind: "reprogrammed"; id: string; x: number; y: number } | { kind: "spawned"; id: string; actor: string; x: number; y: number } | { kind: "fired"; shot: WaveShot["kind"]; x: number; y: number } | { kind: "player-killed"; x: number; y: number; by: string } | { kind: "extra-life"; lives: number } | { kind: "game-over"; score: number }
Events for presenters (sounds, explosions, score pop-ups).
WaveArenaInput
interface WaveArenaInput
Input for one frame.
move?: Dir8— Eight-way movement (-1..1 each axis).fire?: Dir8— Eight-way fire direction; {0,0} = not firing.
WaveArenaState
interface WaveArenaState
The whole serialisable state.
status: WaveStatus— Current phase.statusFrames: number— Frames in this phase.frame: number— Total frames.wave: number— 1-based wave number.score: number— Score.lives: number— Lives including the one in play.nextExtra: number— Next extra-life score.saved: number— Humans saved since the wave started or the player last died.player: { x: number; y: number; alive: boolean; facing: Dir8; anim: number; fireDir: Dir8; fireHeld: number }— The player.actors: WaveActor[]— Actors.shots: WaveShot[]— Projectiles with family data.gruntDelay: number— Grunt countdown range now.gruntMinDelay: number— Fastest grunt range now.speedUpIn: number— Frames to the next grunt speed-up check.scoredSinceCheck: boolean— The player scored since the last speed-up check.gruntPass: number— Grunt pass countdown.nextId: number— Next id.random: number[]— Seeded random state: four words of the kernel'screateRngstate, advanced in place.carry: Record<string, number>— Survivors carried to the restart after a death (kind → count).gameOverReason: string— Why the game ended.grace: number— Grace frames left in this wave (enemies hold still).actorClock: number— Fractional enemy-clock carry for the speed scale.
WaveBehaviour
type WaveBehaviour = | "grunt" | "electrode" | "hulk" | "brain" | "prog" | "spheroid" | "enforcer" | "quark" | "tank" | "human"
Built-in enemy families. A kind picks one and the family's parameter block drives it.
WaveDef
interface WaveDef
One authored wave: counts by kind id.
counts: Record<string, number>— Counts of each kind at the start of the wave (kind id → count).params: WaveParams— Timing for this wave.safeZone?: [number, number, number, number]— Grunt/hulk/tank exclusion box around the player's start [left, right, top, bottom] (model pixels).
WaveDifficulty
interface WaveDifficulty
Difficulty shaping layered over the authored waves. The all-neutral values (waveDifficulty("arcade"))
play the authored tables exactly; every other value is a deliberate departure a consumer opts into.
population: number— Multiplier on every enemy and electrode count at wave 1 (humans are never scaled). Arcade: 1.ramp: number— Added to the multiplier for every later wave: how fast the waves escalate towards the full tables. Arcade: 0.maxPopulation: number— Ceiling for the multiplier. Arcade: 1.maxCount: number— Most of any one kind in a wave after scaling (0 = no cap). Arcade: 0.speed: number— Enemy speed scale: every family's clock (moves, drops, re-aims) and enemy shot speed. Arcade: 1.fire: number— Enemy fire-rate scale: shot timers are divided by it (0.5 = half as often). Arcade: 1.grace: number— Frames after the wave intro in which the player moves and fires but enemies hold still. Arcade: 0.safeRadius: number— No lethal actor is placed with any part nearer than this to the player's centre (px). Arcade: 0.
WaveKind
interface WaveKind
One kind of actor: its family, size and score.
id: string— Kind id used by wave rows and spawns (unique).behaviour: WaveBehaviour— The family whose rules move it.score: number— Points for destroying it with a laser (rescues userescue.scores).w: number— Hit-box width.h: number— Hit-box height.required?: boolean— Must be destroyed (or leave) to clear the wave. Default: grunts, brains, spheroids, enforcers, quarks and tanks — the original's end-of-wave count. Hulks, electrodes, Progs and humans never block the end.
WaveParams
interface WaveParams
Per-wave timing parameters (the original's WVTAB columns). Units are noted per field.
gruntDelay: number— Grunt step countdown range (each grunt waits 1..this grunt passes between steps). Original ROBSPD.gruntMinDelay: number— Fastest the countdown range may become as the wave goes on. Original RMXSPD.carry: number— Enforcers (or tanks) a spheroid (or quark) carries: random 1..this, halved rounding up. Original ENFNUM.enforcerShot: number— Enforcer shot timer range, in enforcer ticks. Original ENSTIM.spheroidDrop: number— Spheroid ticks before it starts dropping enforcers (random 1..this). Original CDPTIM.hulkDelay: number— Frames between hulk steps. Original HLKSPD.brainShot: number— Brain cruise-missile timer range, in brain steps. Original BSHTIM.brainDelay: number— Frames between brain steps. Original BRNSPD.tankShot: number— Tank steps between shells. Original TNKSHT.shellSpeed: number— Shell speed factor (0..255). Original SHLSPD.quarkDrop: number— Quark ticks before it starts dropping tanks (random 1..this). Original TDPTIM.quarkSpeed: number— Quark speed range (0..255). Original SQSPD.
WaveShot
interface WaveShot
A projectile with its family data. Position and velocity live in projectiles.
id: number— Projectile id inprojectiles.kind: "laser" | "spark" | "missile" | "shell"— laser, spark, missile or shell.dir: Dir8— Direction (lasers).life: number— Frames left.ax: number— Acceleration (sparks), px per frame per frame.ay: number— Acceleration (sparks).retarget: number— Moves left before re-aim (missiles).mx: number— Missile heading.my: number— Missile heading.trail: { x: number; y: number }[]— Missile trail, newest first.age: number— Frame counter.
WaveShotKind
interface WaveShotKind
Projectile kinds fired by the families.
score: number— Points for shooting it down. Original: 25.radius: number— Hit radius.cap: number— Most of this kind alive at once (the original's caps: 20 sparks, 8 missiles, 21 shells).
WaveStatus
type WaveStatus = "intro" | "playing" | "clear" | "dying" | "game-over"
Game phase.


