Phaser AE

Playfield shapes and edges

Pro packv3.0.0-32DArcade

What the edges of a playfield do, and what shape its space has. Wrapping screens, bouncing walls, gravity wells, a web of lanes, depth planes, rooms with doors and turning shield rings are each a small object with a clear rule.

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

Released October 14, 2026 · updated October 20, 2026

Playfield shapes and edges: 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

arena

function arena(): Pack<{ arena: ArenaApi }>

The arena pack. Pure logic, so it needs no backend and runs headless.

Helpers

createBarrier

function createBarrier(o: BarrierOptions): Barrier

Creates a Barrier.

createBarrierStack

function createBarrierStack(rings: BarrierOptions[]): BarrierStack

Creates a BarrierStack from one BarrierOptions per ring, outermost first.

createBounds

function createBounds(o: BoundsOptions): BoundedArena

Creates a BoundedArena from its options.

createDepthField

function createDepthField(o: DepthOptions): DepthField

Creates a DepthField from its options.

createRoomGraph

function createRoomGraph(): RoomGraph

Creates an empty room graph. Add rooms with room() and doors with link().

createWebGrid

function createWebGrid(o: WebGridOptions): WebGrid

Creates a WebGrid and builds its nodes.

createWell

function createWell(o: WellOptions): Well

Creates a Well from its options.

createWrapArena

function createWrapArena(o: WrapArenaOptions): WrapArena

Creates a WrapArena from its options.

Functions

applyWells

function applyWells(wells: readonly Well[], a: Mover, dt: number): void

Apply several wells at once.

Classes

Barrier

class Barrier

A ring of shield sections that turns, takes damage and grows back — the single most reimplemented object in the vector canon.

The interesting part is blocks(angle): a shot is tested against the SECTION the angle falls in, in the ring's own rotated frame, so nothing has to track which section is where.

  • readonly sections: BarrierSection[] — The sections, in ring order.
  • readonly maxHealth: number — Hits each section takes before it is destroyed.
  • radius: number — The radius of the ring. Default 100.
  • spin: number — Radians each second, + is clockwise on screen.
  • regen: number — Seconds before a destroyed section comes back. 0 = never.
  • rotation: number — Current rotation, radians.
  • constructor(o: BarrierOptions)
  • get count(): number — The number of sections in the ring.
  • get alive(): number — Sections still standing.
  • get down(): boolean — Nothing left — in Star Castle, the ring outside collapses inward next.
  • get gaps(): number[] — Which sections are missing — the gaps a shot can fly through.
  • get span(): number — Radians each section spans.
  • update(dt: number): void — Turns the ring and regenerates destroyed sections. Call once each frame.
  • sectionAt(angle: number): number — Which section a WORLD angle falls in, allowing for the ring's rotation.
  • angleOf(index: number): number — The world angle of a section's centre — where to draw it.
  • blocks(angle: number): boolean — Is the ring solid at this world angle?
  • hitAt(angle: number, damage = 1): BarrierHit — Damage the section at a world angle.
  • hit(index: number, damage = 1): BarrierHit — Damages the section at index. Returns what happened to it.
  • restore(): void — Everything back.

BarrierStack

class BarrierStack

Concentric rings, usually turning at different rates. The boss is exposed only when a gap lines up through EVERY ring at once — Star Castle's win condition and Zektor's rotating slots are the same test.

  • readonly rings: Barrier[] — The rings, outermost first.
  • constructor(rings: Barrier[])
  • update(dt: number): void — Advances every ring: rotation and regeneration. Call once each frame.
  • blockedBy(angle: number): number — The outermost ring that stops a shot at this angle, or -1 for a clear line.
  • open(angle: number): boolean — A clear line to the middle — the vulnerability window.
  • hitAt(angle: number, damage = 1): { ring: number; result: BarrierHit } — Hit the outermost blocking ring; returns the ring index and what happened.
  • get down(): boolean — Every ring flattened.

BoundedArena

class BoundedArena

A rectangle whose edges do something. The same arena serves an Omega Race boundary (bounce), an Armor Attack map edge (block), a Sundance grid (kill) and a variant rule set where only the shots wrap — pass the rules per edge, and per actor by simply having more than one of these.

  • x: number — The left edge of the arena.
  • y: number — The top edge of the arena.
  • w: number — The width of the arena.
  • h: number — The height of the arena.
  • restitution: number — The velocity kept on a bounce. Default 1 (perfectly elastic).
  • constructor(o: BoundsOptions)
  • rule(edge: Edge): EdgeRule — Gets the rule of one edge.
  • setRule(edge: Edge, rule: EdgeRule): void — Sets the rule of one edge — open a gap, or arm a wall mid-game.
  • apply(a: Mover, radius = 0): EdgeHit[] — Apply the edge rules to a mover, mutating it. Returns what happened — a 'kill' hit is reported, never acted on, because only the game knows whether that means a life, a score or a respawn.
  • inset(by: number): BoundedArena — Inset rectangle — an inner track wall, or a safe zone.
  • contains(p: XY, margin = 0): boolean — True while the point is inside the rectangle, grown by margin on every side.
  • get corners(): XY[] — The four corners, for drawing the boundary.

DepthField

class DepthField

Named depth planes with a projection and a same-plane test. In 3D Mine Storm the plane a mine is on decides whether your shot can touch it, which makes depth a RULE and not a look — and the reason it belongs here rather than in the renderer.

  • readonly planes: number — How many planes the field has. Plane 0 is nearest.
  • readonly origin: XY — The screen point the far planes converge to.
  • readonly spacing: number — The scale lost per plane. Default 0.16.
  • readonly fade: number — The brightness lost per plane, 0..1. Default 0.22.
  • constructor(o: DepthOptions)
  • scaleOf(plane: number): number — Returns the scale of a plane: 1 at plane 0, less with depth, never below 0.02.
  • brightOf(plane: number): number — Returns the brightness of a plane, 0..1: 1 at plane 0, less with depth.
  • place(x: number, y: number, plane: number): DepthPose — Where a point on plane lands on the screen. plane may be fractional.
  • same(a: { plane: number }, b: { plane: number }, tol = 0.5): boolean — The rule: a shot only hits what shares its plane.
  • clamp(plane: number): number — Move between planes without leaving the field.

RoomGraph

class RoomGraph

Rooms joined by doors, with routing that respects what is actually passable. Covers the maze titles, the map screen, and the "which way out" question a timed escape asks — route() after the reactor is armed is the escape route, and it is the same call.

  • readonly rooms: Map<string, Room> — Every room, by id.
  • readonly doors: Door[] — Every door, in the order the links were made.
  • current: string | null — Where the player is.
  • room(id: string, def: RoomDef = {}): Room — Adds a room with the given id and returns it, not visited and not cleared.
  • link(a: string, b: string, def: DoorDef = {}): Door — Adds a door between rooms a and b and returns it.
  • doorsOf(id: string): Door[] — Every door touching a room, including the ones it cannot use yet.
  • door(a: string, b: string): Door | null — Gets the door between two rooms, in either direction, or null.
  • through(door: Door, from: string): string — The room on the other side.
  • passable(door: Door, from: string): boolean — True when the player can go through the door from the given room now.
  • exits(id: string): string[] — Rooms reachable in one step right now.
  • unlock(a: string, b: string): boolean — Unlock a door. Returns false if there is no such door.
  • drill(a: string, b: string): boolean — Open a locked door the hard way. Fails on one that is not drillable.
  • enter(id: string): Room | null — Move the player, marking the room visited and revealing its doors.
  • route(from: string, to: string): string[] | null — Shortest passable route inclusive of both ends, or null.
  • tagged(tag: string): Room[] — Rooms with a tag — reactor rooms, exits, fuel depots.
  • get visited(): Room[] — Every room the player has entered.
  • get all(): Room[] — Every room in the graph, as an array.
  • layout(start?: string, step = 1): this — Give every room an x/y for the map screen: breadth-first from start, each room taking the first free cell next to the one it was reached from. Deterministic, and enough for a map — supply coordinates yourself when the map is the level design rather than a readout.

WebGrid

class WebGrid

A polar lattice: rings × spokes nodes plus a hub, with movement along the strands. Rings wrap round; the spoke index does not run off the end.

Not to be confused with vecTube.generateTube(), which is the Tempest playfield — a rim with a DEPTH axis. This one is flat and you move about inside it.

  • readonly x: number — The x position of the hub.
  • readonly y: number — The y position of the hub.
  • readonly rings: number — How many concentric rings the web has, not counting the hub.
  • readonly spokes: number — How many strands go out of the hub.
  • readonly radius: number — The radius of the outermost ring.
  • readonly inner: number — The radius of ring 0. Default radius / (rings + 1).
  • readonly rotation: number — The turn of the whole web, in radians. Default 0.
  • readonly nodes: WebNode[] — Every node, in ring-major order: index ring * spokes + spoke.
  • constructor(o: WebGridOptions)
  • ringRadius(r: number): number — Radius of ring r, evenly spaced from inner out to radius.
  • at(ring: number, spoke: number): XY — World position of a (possibly fractional) ring/spoke pair.
  • node(ring: number, spoke: number): WebNode | null — Gets the node at a ring/spoke pair. The spoke wraps round; a ring outside the web returns null.
  • neighbours(n: WebNode): WebNode[] — The four strand neighbours: in, out, and round each way.
  • nearest(p: XY): WebNode — Nearest node to a world point — for placing a spawn, or a mouse pick.
  • path(from: WebNode, to: WebNode): WebNode[] | null — Shortest strand route, inclusive of both ends, or null.
  • strands(): [XY, XY][] — Every strand as a pair of points — the thing you draw.

Well

class Well

An inverse-square attractor. The unit distance is what makes it tunable: "80 units/s² at 100 units away" is a number you can reason about, where a raw gravitational constant is not.

  • x: number — The x position of the well's centre.
  • y: number — The y position of the well's centre.
  • strength: number — The pull in units/s² at unit distance. Negative repels.
  • unit: number — The distance strength is quoted at. Default 100.
  • range: number — The distance beyond which the well applies no pull. Default Infinity.
  • radius: number — The contact radius — swallows() reads true inside it. Default 0.
  • constructor(o: WellOptions)
  • accelAt(p: XY): XY — Acceleration this well applies at a point (no dt).
  • apply(a: Mover, dt: number): void — Add this well's pull to a mover.
  • swallows(p: XY, r = 0): boolean — Inside the contact radius — the sun has eaten it.

WrapArena

class WrapArena

A screen whose edges are joined. Three things follow from that and all three are easy to get wrong:

  • the SHORTEST distance between two points may go across the seam (delta), or an enemy at the left edge chases a player at the right edge the long way round the whole screen;
  • an object straddling an edge must be DRAWN more than once (ghosts), or it vanishes from one side before appearing at the other;
  • a collision test must use the wrapped delta too (overlap).

Wrapping the position itself is the easy third of it, and the only third most remakes do.

  • x: number — The left edge of the arena.
  • y: number — The top edge of the arena.
  • w: number — The width of the arena — the wrap span on x.
  • h: number — The height of the arena — the wrap span on y.
  • constructor(o: WrapArenaOptions)
  • wrap(p: XY): boolean — Bring a point back inside. Returns true if it moved.
  • contains(p: XY, margin = 0): boolean — True while the point is inside without wrapping — for shots that expire.
  • delta(a: XY, b: XY): XY — Shortest vector from a to b, across the seam if that is nearer.
  • distance(a: XY, b: XY): number — Shortest distance from a to b, across the seam if that is nearer.
  • angle(a: XY, b: XY): number — Heading from a to b, seam-aware — what a seeking enemy steers by.
  • overlap(a: XY, ar: number, b: XY, br: number): boolean — Circle overlap using the wrapped distance.
  • ghosts(x: number, y: number, margin = 0): XY[] — Every position this object must be DRAWN at: itself, plus a copy across each edge it is within margin of. One to four entries. ts for (const p of arena.ghosts(rock.x, rock.y, rock.radius)) { shape.x = p.x; shape.y = p.y; draw(); }
  • spawnClear(avoid: readonly XY[], clear: number, rng: () => number, tries = 40): XY — A random point at least clear away from all of avoid (wrapped).

Types

ArenaApi

type ArenaApi = typeof kit

game.arena — every function and type this pack carries, as one namespace.

BarrierHit

type BarrierHit = 'none' | 'damaged' | 'destroyed'

What a hit did: missed a dead section, damaged one, or destroyed one.

BarrierOptions

interface BarrierOptions

Configures a Barrier.

  • sections: number — Sections round the ring.
  • health?: number — Hits each section takes. Default 1.
  • spin?: number — Radians per second, + is clockwise on screen. Default 0.
  • regen?: number — Seconds before a destroyed section comes back. 0 = never. Default 0.
  • radius?: number — The radius of the ring. Default 100.
  • rotation?: number — Where the ring's section 0 starts. Default 0.

BarrierSection

interface BarrierSection

One section of a Barrier ring.

  • index: number — The section's position round the ring, from 0.
  • health: number — Hits left. 0 = destroyed.
  • cooling: number — Seconds until it returns; 0 when alive or when regen is off.

BoundsOptions

interface BoundsOptions

What createBounds() accepts.

  • x?: number — The left edge of the arena. Default 0.
  • y?: number — The top edge of the arena. Default 0.
  • w: number — The width of the arena.
  • h: number — The height of the arena.
  • rule?: EdgeRule | Partial<Record<Edge, EdgeRule>> — One rule for every edge, or a rule per edge. Default 'block'.
  • restitution?: number — Velocity kept on a bounce. Default 1 (perfectly elastic).

DepthOptions

interface DepthOptions

What createDepthField() accepts.

  • planes: number — How many planes. Plane 0 is nearest.
  • origin: XY — Where the far planes converge to.
  • spacing?: number — Scale lost per plane. Default 0.16.
  • fade?: number — Brightness lost per plane, 0..1. Default 0.22.

DepthPose

interface DepthPose extends XY

The screen position, scale and brightness of a point, returned by DepthField.place().

  • scale: number — Multiply your art by this.
  • bright: number — 1 at the near plane, falling with depth — a width/alpha multiplier.

Door

interface Door

A door in a RoomGraph, made by link(). Its flags can change during play.

  • a: string — The id of the room on one side — the entry side of a one-way door.
  • b: string — The id of the room on the other side.
  • locked: boolean — True while the door is shut. unlock() or drill() opens it.
  • drillable: boolean — True when drill() can open the door while it is locked.
  • oneWay: boolean — True when the door is passable from a to b only.
  • hidden: boolean — True while the door is not shown on the map. Cleared when a room next to it is entered.
  • data: unknown — Game data attached by the door's DoorDef, or null.

DoorDef

interface DoorDef

What RoomGraph.link() accepts to describe a new door.

  • locked?: boolean — Shut until opened. Default false.
  • drillable?: boolean — A drill or a bomb can open it even while locked. Default false.
  • oneWay?: boolean — Passable from a to b only. Default false.
  • hidden?: boolean — Not shown on the map until used. Default false.
  • data?: unknown — Game data to attach to the door. Default null.

Edge

type Edge = 'left' | 'right' | 'top' | 'bottom'

One of the four edges of a BoundedArena.

EdgeHit

interface EdgeHit

One edge contact reported by BoundedArena.apply().

  • edge: Edge — The edge the mover hit.
  • rule: EdgeRule — The rule that edge applied.
  • speed: number — Approach speed into the wall, in units/s — gate the impact sound on it.

EdgeRule

type EdgeRule = 'block' | 'bounce' | 'wrap' | 'kill' | 'ignore'

What an edge does to a mover that crosses it. A 'kill' is reported, never acted on.

Mover

interface Mover extends XY

Anything with a position and a velocity: a ship, a shot, a rock.

  • vx: number — The x velocity, in world units each second.
  • vy: number — The y velocity, in world units each second.

Room

interface Room

A room in a RoomGraph, made by room().

  • id: string — The id the room was added under.
  • x: number — The map-screen x position.
  • y: number — The map-screen y position.
  • tags: string[] — The tags from the room's RoomDef — what tagged() searches.
  • data: unknown — Game data attached by the room's RoomDef, or null.
  • visited: boolean — True after the player enters the room. Set by enter().
  • cleared: boolean — True after the game marks the room cleared. The graph never sets it.

RoomDef

interface RoomDef

What RoomGraph.room() accepts to describe a new room.

  • x?: number — Map-screen position. Omit every one of them and layout() fills them in.
  • y?: number — The map-screen y position. Default 0.
  • tags?: string[] — Tags to find the room by with tagged(). Default none.
  • data?: unknown — Game data to attach to the room. Default null.

WebGridOptions

interface WebGridOptions extends XY

What createWebGrid() accepts. x/y place the hub.

  • rings: number — Concentric rings, not counting the hub.
  • spokes: number — Strands out of the hub.
  • radius: number — Radius of the outermost ring.
  • inner?: number — Radius of ring 0. Default radius / (rings + 1).
  • rotation?: number — Turn the whole web. Default 0.

WebNode

interface WebNode

A node of a WebGrid — a ring/spoke crossing with its world position.

  • ring: number — The ring the node is on. Ring 0 is innermost.
  • spoke: number — The spoke the node is on.
  • x: number — The world x position of the node.
  • y: number — The world y position of the node.
  • index: number — Index into nodes.

WellOptions

interface WellOptions extends XY

What createWell() accepts. x/y place the well's centre.

  • strength: number — Pull in units/s² at unit distance. Negative repels.
  • unit?: number — The distance strength is quoted at. Default 100.
  • range?: number — No pull beyond this. Default Infinity.
  • radius?: number — Contact radius — swallows() reads true inside it. Default 0.

WrapArenaOptions

interface WrapArenaOptions

What createWrapArena() accepts.

  • x?: number — The left edge of the arena. Default 0.
  • y?: number — The top edge of the arena. Default 0.
  • w: number — The width of the arena.
  • h: number — The height of the arena.

XY

interface XY

A 2D point in world units.

  • x: number — The x position, in world units.
  • y: number — The y position, in world units.