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 atindex. 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 bymarginon 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 onplanelands on the screen.planemay 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 roomsaandband 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 fromstart, 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. Defaultradius / (rings + 1).readonly rotation: number— The turn of the whole web, in radians. Default 0.readonly nodes: WebNode[]— Every node, in ring-major order: indexring * spokes + spoke.constructor(o: WebGridOptions)ringRadius(r: number): number— Radius of ringr, evenly spaced frominnerout toradius.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² atunitdistance. Negative repels.unit: number— The distancestrengthis 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 fromatob, across the seam if that is nearer.distance(a: XY, b: XY): number— Shortest distance fromatob, across the seam if that is nearer.angle(a: XY, b: XY): number— Heading fromatob, 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 withinmarginof. 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 leastclearaway from all ofavoid(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()ordrill()opens it.drillable: boolean— True whendrill()can open the door while it is locked.oneWay: boolean— True when the door is passable fromatobonly.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'sDoorDef, 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 fromatobonly. 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'sRoomDef— whattagged()searches.data: unknown— Game data attached by the room'sRoomDef, or null.visited: boolean— True after the player enters the room. Set byenter().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 andlayout()fills them in.y?: number— The map-screen y position. Default 0.tags?: string[]— Tags to find the room by withtagged(). 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. Defaultradius / (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 intonodes.
WellOptions
interface WellOptions extends XY
What createWell() accepts. x/y place the well's centre.
strength: number— Pull in units/s² atunitdistance. Negative repels.unit?: number— The distancestrengthis 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.


