Phaser AE

Marching fleet shooter

Pro packv3.0.0-12DArcade

The rules of a fixed-screen rack shooter: a fleet that marches one invader at a time, so it speeds up by itself as you thin it, typed alien shots, a mystery saucer and shields that wear away. The 1978 arcade game is a preset you can reshape, and your game draws it however it likes.

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

Marching fleet 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

marchingFleet

function marchingFleet(): Pack<{ marchingFleet: MarchingFleetApi }>

Install the renderer-independent marching-fleet rules (Space Invaders). Its shields are destructible-cover masks.

Helpers

createInvaders

function createInvaders(config: InvadersConfig)

Create an independent marching-fleet game. No renderer or kernel clock is required.

invadersPreset

function invadersPreset(o: InvadersPresetOptions = {}): InvadersConfig

Space Invaders (Taito, 1978) as data. Values from the ROM disassembly (computerarcheology.com): 224x256 screen; 11x5 rack of squid (30), crab (20), octopus (10) on 16-pixel centres; one invader moved per frame, 2 px (the last invader 3 px right, 2 left), 8 px drop at an edge; rounds start lower from the AlienStartTable; one player shot at 4 px/frame that needs the button released; rolling (aimed at the column over the cannon), plunger and squiggly shots from the ColFireTable, each stepping 4 px every third frame (5 px with 8 or fewer invaders), reloading by score; the saucer every 1536 frames with 8+ invaders, its score from the 15-entry wrap of SaucerScrTab; four 22x16 shields eroded by the ROM's explosion stencils; 3 cannons and one extra at 1500.

Classes

Invaders

class Invaders

Owns a marching fleet game: rack, cursor march, shot types, saucer, eroding cover, lives and rounds.

  • readonly config: InvadersConfig — The validated definition (a private copy).
  • readonly cover: DestructibleCover — The shields, a destructible-cover instance owned by this game.
  • constructor(config: InvadersConfig)
  • get state(): InvadersState — A read-only copy of the whole state.
  • box(inv: InvaderState) — Live invaders' hit boxes, for renderers and tests.
  • reloadSteps(score = this.s.score) — Current reload threshold (steps) for the score.
  • saucerValue() — Score the saucer would award if hit now.
  • tick(seconds: number, input: InvadersInput = {}) — Advance by real seconds (fixed frames inside); returns the state.
  • step(input: InvadersInput = {}) — Advance exactly one simulation frame.
  • drain() — Take the events since the last drain.
  • snapshot() — Serializable snapshot of the whole game.
  • restore(snap: ReturnType<Invaders["snapshot"]>) — Restore a snapshot taken from the same configuration.
  • reset(round = 0) — Start a new game with the same configuration, optionally at a later (0-based) round.

Types

InvaderKind

interface InvaderKind

One invader type: hitbox (centred in its fleet cell) and score.

  • id: string — Kind id used by fleet rows.
  • score: number — Points for shooting one.
  • width: number — Hit-box width.
  • height: number — Hit-box height.

InvadersConfig

interface InvadersConfig

Complete marching-fleet definition. Every original value is a preset in invadersPreset.

  • width: number — Playfield width in model pixels.
  • height: number — Playfield height in model pixels.
  • hz: number — Simulation frames per second.
  • seed?: number — Seed for random-column shots.
  • kinds: InvaderKind[] — Invader kinds.
  • fleet: { columns: number; /** Kind id per row, TOP row first. */ rows: string[]; /** Cell size; invaders' hitboxes are centred in their cell. */ cellWidth: number; cellHeight: number; /** Distance between column / row origins. */ spacingX: number; spacingY: number; /** Left edge of column 0 at the start of a round. */ left: number; /** Top of the BOTTOM row per round; the last entry repeats from \repeatFrom`. */ startY: number[]; repeatFrom?: number; }` — Rack layout and the start height of each round.
  • march: { /** Invaders moved per frame (original 1: one invader per frame). */ movesPerFrame: number; stepX: number; /** Horizontal steps of the very last invader, right and left (original 3 and 2). */ lastStepRight: number; lastStepLeft: number; /** Drop applied in the sweep after the rack touches an edge. */ stepDown: number; /** Playfield edges that reverse the rack. */ minX: number; maxX: number; /** Frames the rack freezes while a shot invader explodes. Original: 16. */ killPause: number; /** Invaders erase any cover pixels they overlap (original behaviour). */ eraseCover: boolean; } — Cursor march rules.
  • invasionY: number — An invader's bottom reaching this line lands the invasion: game over regardless of lives.
  • player: { /** Top of the cannon. */ y: number; width: number; height: number; /** Pixels per frame. */ speed: number; minX: number; maxX: number; startX: number; lives: number; /** Extra cannons awarded at these scores (once each). Original: 1500. */ extraLives: number[]; /** Frames of the death explosion, during which the rack and its fire freeze. */ deathFrames: number; /** Frames before the new cannon appears after the explosion. */ respawnFrames: number; } — Cannon rules.
  • playerShot: { /** Pixels per frame. Original: 4. */ speed: number; /** Simultaneous player shots. Original: 1. */ maxActive: number; /** Fire must be released between shots (original). */ requireRelease: boolean; /** The shot explodes here (top of the playfield). */ ceiling: number; width: number; height: number; /** Player shots can destroy alien shots they touch (original). */ hitsAlienShots: boolean; /** Frames the shot's explosion lingers before another shot may fire. */ explodeFrames: number; } — Player shot rules.
  • alienShots: { types: InvaderShotType[]; /** Each shot type takes a turn every \cadence` frames, offset by its index. Original: 3. / cadence: number; /* Pixels per step, and the faster step once `fastAtOrBelow` invaders remain. Original: 4, 5 and 8. / step: number; fastStep: number; fastAtOrBelow: number; /* * Reload: a type may fire only when every other in-flight shot has taken MORE than this many steps. * Looked up by score: `rates[i]` applies while score < `scores[i]`, the last rate beyond. * Original: scores 200/1000/2000/3000, rates 48/16/11/8/7. / reload: { scores: number[]; rates: number[] }; /* Frames at the start of each life/round before aliens may fire. / initialDelay: number; /* Shots are lost below this line (the ground). */ floor: number; }` — Alien shot types, cadence, speed and reload.
  • saucer: SaucerRules | null — Mystery ship rules, or null for none.
  • cover: CoverDefinition[] — Damageable shields (destructible-cover definitions).
  • splats: { player: string[] | null; alien: string[] | null; inset: number } — Explosion stencils (null = destroy the single contacted cell), centred inset pixels into the cover.
  • roundDelay: number — Frames between clearing a round and the next rack.
  • restoreCover: boolean — Cover restored for each new round (original).

InvadersEvent

type InvadersEvent = | { kind: "player-fire"; x: number; y: number } | { kind: "invader-killed"; id: string; invader: string; score: number; x: number; y: number } | { kind: "saucer-start"; dir: number } | { kind: "saucer-killed"; score: number; x: number; y: number } | { kind: "saucer-gone" } | { kind: "shot-exploded"; team: "player" | "alien"; x: number; y: number; on: "cover" | "ceiling" | "floor" | "shot" } | { kind: "player-hit"; x: number; y: number } | { kind: "extra-life"; lives: number } | { kind: "march"; sweep: number } | { kind: "round-clear"; round: number } | { kind: "round-start"; round: number } | { kind: "game-over"; reason: "lives" | "invasion" }

Ordered notifications for presentation: drain() them each frame.

InvaderShot

interface InvaderShot

One alien shot in flight.

  • id: number — Stable id for drawing.
  • type: string — Its shot type id.
  • x: number — Centre x.
  • y: number — Top y.
  • steps: number — Steps taken (reload bookkeeping and animation).
  • active: boolean — In flight.

InvaderShotSelect

type InvaderShotSelect = /** The lowest live invader in the column over the player's centre (the "rolling" shot). */ | { kind: "player-column" } /** The lowest live invader in the next column of a looping 1-based column table. */ | { kind: "column-table"; table: number[]; start?: number } /** The lowest live invader in a seeded random column (not in the original; a variant policy). */ | { kind: "random" }

How an alien shot type picks the invader that drops it.

InvaderShotType

interface InvaderShotType

One alien shot type. At most one shot of each type is in flight, so N types cap alien fire at N shots.

  • id: string — Shot type id (also its drawing key).
  • select: InvaderShotSelect — Which invader drops it.
  • disableAtOrBelow?: number — Do not fire this type while this many invaders or fewer remain (the plunger stops at 1).
  • sharesSaucerSlot?: boolean — This type's turn is shared with the saucer, which waits for it to land (the squiggly shot).
  • width?: number — Hit-box width (default 3).
  • height?: number — Hit-box height (default 7).

InvadersInput

interface InvadersInput

One frame of player input.

  • move?: number — -1 left, 0 still, +1 right.
  • fire?: boolean — Fire held.

InvadersPresetOptions

interface InvadersPresetOptions

Options for invadersPreset: the classic values unless overridden.

  • columns?: number — Invaders per row (11 = classic).
  • rows?: string[] — Kind ids per row, top first. Default: squid, crab, crab, octopus, octopus.
  • lives?: number — Starting cannons (3 = classic).
  • bunkers?: number — Shields (4 = classic, at the original positions).
  • saucer?: boolean — Include the mystery ship (default true).
  • movesPerFrame?: number — Invaders moved per frame (1 = original; raise for a faster, smoother march).
  • playerShots?: number — Simultaneous player shots (1 = original).
  • shotTypes?: ("rolling" | "plunger" | "squiggly")[] — Which original shot types to use.
  • extraLives?: number[] — Scores awarding an extra cannon ([1500] = classic).
  • seed?: number — Seed for random-column shots.

InvadersState

interface InvadersState

Serializable game state; read it through state.

  • frame: number — Frames since the game began.
  • round: number — Rack number, from 0.
  • score: number — Score.
  • lives: number — Cannons left, including the one in play.
  • extraAwarded: number — Extra cannons awarded so far.
  • status: InvadersStatus — Game status.
  • gameOverReason: "" | "lives" | "invasion" — Why the game ended.
  • statusFrames: number — Frames left in the current status.
  • player: { x: number; alive: boolean; visible: boolean } — The cannon: left x, alive and visible.
  • invaders: InvaderState[] — Every invader in march order (bottom row first).
  • alive: number — Invaders left.
  • cursor: number — Cursor into the bottom-up march order; direction +1 right, -1 left.
  • dir: number — Rack direction: +1 right, -1 left.
  • sweepDown: number — Drop applied during the current sweep.
  • bumped: boolean — A bump was detected during this sweep: next sweep drops and reverses.
  • killPause: number — Frames the rack stays frozen for an explosion.
  • sweeps: number — Total completed march sweeps (presentation: the four-note fleet sound).
  • shots: InvaderShot[] — Alien shots in flight.
  • playerShots: PlayerShotState[] — Player shots in flight.
  • shotsFired: number — Player shots fired this game (drives the saucer table and direction).
  • fireHeld: boolean — Fire was held on the last frame (release rule).
  • shotLock: number — Frames before another player shot may fire.
  • fireDelay: number — Frames before aliens may fire.
  • tablePointers: Record<string, number> — Column-table position per shot type.
  • saucer: SaucerState — Mystery ship.
  • nextId: number — Next shot id.
  • random: number[] — The kernel generator's state (four words from createRng), advanced in place, so a snapshot carries the stream.

InvadersStatus

type InvadersStatus = | "playing" | "player-dying" | "round-clear" | "game-over"

Playing, the cannon exploding, between racks, or over.

InvaderState

interface InvaderState

One invader.

  • id: string — Stable id r<row>c<col>.
  • kind: string — Kind id.
  • col: number — Fleet column.
  • row: number — Fleet row, 0 at the top.
  • x: number — Top-left of the invader's cell.
  • y: number — Top of its cell.
  • alive: boolean — Not yet shot.
  • frame: number — Two-frame march animation; flips each time this invader moves.

MarchingFleetApi

type MarchingFleetApi = typeof invaders

game.marchingFleet: the marching-fleet rules owner and its preset.

PlayerShotState

interface PlayerShotState

One player shot in flight.

  • id: number — Stable id for drawing.
  • x: number — Centre x.
  • y: number — Top y.

SaucerRules

interface SaucerRules

The mystery ship.

  • interval: number — Frames between saucer launches. Original: 0x600 = 1536 (25.6 s).
  • minInvaders: number — No saucer when fewer invaders remain. Original: 8.
  • y: number — Top of the saucer's 16x8 box.
  • width: number — Saucer width.
  • height: number — Saucer height.
  • step: number — Pixels per step; the saucer steps on its shared shot turn. Original: 2.
  • scores: number[] — Score table indexed by player shots fired, wrapping after wrap entries.
  • wrap: number — Table entries used before the index wraps (original: 15 of 16, a ROM quirk).
  • directionByShots: boolean — Odd shot count → enter from the left, even → from the right (original). Otherwise alternate.
  • timerBelowY?: number — The saucer timer only runs while some live invader's top is below this y (original: the rack has dropped since round 1's start).
  • scoreFrames: number — Frames the score is shown after a hit (presentation hint and saucer-slot lockout).

SaucerState

interface SaucerState

Mystery ship state.

  • active: boolean — Crossing now.
  • x: number — Left edge.
  • dir: number — +1 moving right, -1 left.
  • timer: number — Frames since the last saucer left or was hit.
  • shown: number — Score shown after a hit, counting down showFrames.
  • showFrames: number — Frames the shown score lasts.
  • showX: number — Where the score is shown.