Phaser AE

Wave arena shooter

Pro packv3.0.0-12DArcade

The rules of an open-arena shooter: one screen, eight-way movement and independent eight-way fire, waves of enemy families placed around the player, and people to rescue for a rising bonus. The arena, the enemies, the waves and their timings are plain data, and the pack draws nothing.

This is a Pro pack. Upgrade to Phaser Pro to use it in your games.See Phaser Pro →

Released October 19, 2026 · updated October 20, 2026

Wave arena shooter: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

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 from config.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, following repeatFrom past 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, as waveDifficulty("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's createRng state, 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 use rescue.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 in projectiles.
  • 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.