The pack
rimShooter
function rimShooter(): Pack<{ rimShooter: typeof api }>
Install the renderer-independent rim shooter API.
Helpers
bottomLane
function bottomLane(web: WebShape): number
The lane nearest the bottom of the web (largest y), where a tube game's claw starts.
CLASSIC_PALETTES
const CLASSIC_PALETTES: readonly RimPalette[]
Tempest's colour groups: blue, red, yellow, cyan, an invisible web for levels 65-80, then green.
CLASSIC_START_LEVELS
const CLASSIC_START_LEVELS: readonly number[]
Tempest's SkillStep starting levels (ALWELG LEVEL) and the bonus for completing each (BONPTM).
CLASSIC_WEBS
const CLASSIC_WEBS: readonly WebShape[]
The 16 wells of Tempest (Atari, 1981) in play order, transcribed from the released 6502 source (ALDIS2.MAC: the x/y well tables, HOLRAP open/closed flags, WELSEQ order, HOLEZL eye distances). Coordinates are the cabinet's 0..255 well space with y flipped to point down. Open wells: V, steps, U, flat, W and broken V.
classicKinds
function classicKinds(): RimEnemyKind[]
Tempest's enemy kinds: flipper, three tankers, spiker, fuseball and pulsar (which becomes a flipper at the rim).
createRimShooter
function createRimShooter(config: RimShooterConfig): RimShooter
Create an independent rim-shooter game. No renderer or kernel clock is required.
findWeb
function findWeb(webs: readonly WebShape[], id: string): WebShape
Find a web by id in a library; throws when it is missing.
generateWeb
function generateWeb(spec: WebSpec): WebShape
Generate a web from a short spec: closed polygons, stars and ellipses with corners on lane boundaries, open
lines, arcs, zigzags and spirals. The result is ordinary WebShape data in a -1..1 frame, so it can be
stored, edited and mixed with authored webs.
perspective
function perspective(depth: number, far: number, camera = 0): number
Perspective along a lane. A point at depth (0 the near rim, 1 the far rim) on the line from a near-rim
point N toward the eye E sits at E + (N - E) * perspective(depth, far, camera). camera moves the eye down
the tube (the fly-down zoom): at camera = depth a point is on the screen plane. Returns 0 or less for points
at or behind the camera (do not draw them).
T2K_PALETTES
const T2K_PALETTES: readonly RimPalette[]
Tempest 2000's seven level-group colour schemes (levels 1-16 blue, 17-32 red, 33-48 yellow, 49-64 rainbow,
65-80 pink, 81-96 orange, 97-100 white). near/far are the web's shaded-face gradient from the rim down to the
far end, line/rim its lane and rim highlights (Tempest 2000 draws solid, Gouraud-shaded webs); the enemy
colours are chosen to read well on each web (the source stores Jaguar CRY bytes, approximated here).
T2K_SEQUENCE
const T2K_SEQUENCE: readonly string[]
Tempest 2000's level-to-web sequence: level n uses T2K_SEQUENCE[(n - 1) % 32] (the source's raw_webs table
with webbase 16 and weband 31). Levels 7 and 8 really do repeat the lemon, and levels 16 and 21 the tiny nut.
T2K_WEBS
const T2K_WEBS: readonly WebShape[]
The 37 webs of Tempest 2000 (Atari Jaguar, 1994), transcribed from the released source (yak.s, web1..web37:
the x/y rim pairs, the open/closed connect word and the claw's starting lane). Authoring units are the source's
own grid (centre 8, 8; y down). Shapes that never appear in the Tempest 2000 sequence (they belong to its
Traditional mode) are included for custom level lists.
t2kKinds
function t2kKinds(): RimEnemyKind[]
Tempest 2000's enemy kinds. Speeds are level 1's; each level overrides them from the source's per-level tables.
Demon heads (beast in the source) take three hits and throw their horns on the first two; mirrors take five,
reflect each shot back down the lane and are pushed away by it; neither is touched by the superzapper.
Mutant flippers chase along the rim without pausing; super flippers spin all the way up the web.
tempest2000Preset
function tempest2000Preset(opts: T2KOptions = {}): RimShooterConfig
Tempest 2000's main game: the 37-web library in its 32-web level sequence, 100 levels of the source's waves, per-level speeds, pulse rates and power-up rates, seven colour groups, Mutant and super flippers by the source's probability curves, power-ups in their order (particle laser, bonus, jump, bonus, A.I. droid, warp token), Outta Here, the superzapper, the fly-down and a warp bonus round.
tempestPreset
function tempestPreset(opts: ClassicOptions = {}): RimShooterConfig
Tempest (1981): the 16 wells in order, colour changes every 16 levels (65-80 invisible), 99 levels of the cabinet's skill contour (enemy totals, per-kind minimum/maximum on the web, tanker cargo, flipper climb programs per well, spike lines, fire timers and caps, pulsar timing), SkillStep starts with their bonuses, superzapper (one clear, then one random kill per level), bonus life every 20,000 up to six. After level 99 a random level from 84 on is played. No power-ups, no jump, no bonus round.
validateRim
function validateRim(c: RimShooterConfig): void
Throw a descriptive error unless a definition is complete and consistent.
validateWeb
function validateWeb(web: WebShape): void
Throw unless a web is well formed (ids, point counts, finite numbers, start lane in range).
webLanes
function webLanes(web: WebShape): number
Playable lanes of a web: points for a closed web, points - 1 for an open one.
webView
function webView(web: WebShape): WebView
Prepare a web for drawing: the rim in unit space (the frame maps to -1..1), its vanishing point and depth
ratio. Feed path, closed, lanes, far and vanish to a tube generator (vector-tube's generateTube
takes them directly as path, closed, lanes, far * near and vanishX/Y).
withWebs
function withWebs(config: RimShooterConfig, webs: WebShape[]): RimShooterConfig
Replace the webs of every level in a definition: level n gets webs[(n - 1) % webs.length]. The library becomes webs plus the existing ones.
Classes
RimShooter
class RimShooter
Owns one rim-shooter game.
readonly config: RimShooterConfig— The validated definition (a private copy).constructor(config: RimShooterConfig)get state(): RimState— A copy of the whole state.get web(): WebShape— The web being played.get palette(): RimPalette— The current level's palette.get levelDef(): RimLevel— The current level definition.kind(id: string): RimEnemyKind— A kind by id.get pulsing(): boolean— Is the pulsar pulse lethal right now?get pulsePhase(): number— Pulse phase 0..1 (for drawing the pulsars' throb).get electrified(): number[]— Lanes electrified by pulsars right now.get clawDepth(): number— Depth the claw is at (its jump height, or its fly-down depth).hittableLane(e: RimEnemy): number | null— The lane a flipping or crossing enemy counts as being in for shots, or null when it is between lanes.startOptions(): Array<{ level: number; bonus: number }>— The level's starting levels and the bonus for each.wrap(l: number): number— Wrap (closed) or clamp (open) a lane index.delta(a: number, b: number): number— Signed lane distance fromatob, the short way round a closed web.tick(seconds: number, input: RimInput = {}): RimState— Advance by real seconds in fixed steps.drain(): RimEvent[]— Take the events since the last drain.snapshot(): { state: RimState; carry: number }— Serializable snapshot of the whole game.restore(snap: { state: RimState; carry: number }): void— Restore a snapshot taken from the same configuration.reset(level = this.config.start.levels[0] ?? 1): void— New game fromlevel(default the first start option). Applies that level's start bonus on clearing it.step(input: RimInput = {}): void— Advance exactly one fixed step.zap(free = false): number— Fire the superzapper now (as a press would). Returns the number killed.spawn(kindId: string, lane: number | null = null, depth = 1, mutate = true): RimEnemy— Release an enemy now (tests and custom directors): atlane(random when null) anddepth. Returns it.
Types
ClassicOptions
interface ClassicOptions
Options for tempestPreset.
lives?: number— Starting lives (operator setting, default 3).extraEvery?: number— Bonus life interval (operator setting, default 20,000; 0 none).difficulty?: "easy" | "medium" | "hard"— Operator difficulty: enemy speed and fire scaling (default medium).webs?: WebShape[]— Replace the web sequence: level n useswebs[(n - 1) % length].seed?: number— Random seed (default 1).
ClimbMode
type ClimbMode = "straight" | "random" | "spiral" | "chase" | "zigzag" | "avoid"
How a flipper-family enemy moves while climbing: never flips, flips randomly, spirals one way, flips toward the claw, alternates, or flips away from the claw.
EnemyBehaviour
type EnemyBehaviour = "flipper" | "tanker" | "spiker" | "fuseball" | "pulsar" | "spark" | "mirror" | "ufo"
What an enemy kind does.
flipper: climbs a lane (flipping end over end between lanes by its climb mode), then chases the claw along the rim and catches it.tanker: climbs one lane without flipping; splits into its cargo when shot or when it reaches the rim.spiker: climbs to a random height laying a spike behind it, slides back down and moves to another lane.fuseball: travels up the lane EDGES, crossing lanes now and then (only hittable mid-cross), and kills on contact at the rim.pulsar: climbs like a flipper and electrifies its lane while the pulse is on; at the rim it splits or becomes another kind.spark: runs along the rim lane to lane and kills on contact.mirror: climbs to a holding depth; each hit knocks it back and reflects the shot down the lane.ufo: descends the web and hovers just above the rim, patrolling and zapping lanes; only shots from above the rim reach it.
PowerKind
type PowerKind = "laser" | "jump" | "droid" | "zapper" | "warp" | "bonus"
A power-up kind. bonus pays points or, rarely, Outta Here; warp is a warp token.
RimArrival
type RimArrival = "chase" | { split: string; count?: number } | { become: string }
What a climber does at the rim: chase the claw, split into another kind, or turn into another kind.
RimBonusRound
interface RimBonusRound
The warp bonus round: fly through rings; one miss ends it.
rings: number— Rings to pass.gap: number— Seconds between rings.speed: number— Ring approach speed, depth/s.radius: number— Ring radius in the round's unit disc.steer: number— Crosshair speed, units per second.wander: number— Largest step between successive ring centres.points: number[]— Points for each ring (the last repeats).success: number— Points for completing the round.warp: number— Extra levels skipped on success.maxLevelForWarp: number— No warp from this level on.
RimDeath
type RimDeath = "caught" | "shot" | "fried" | "fuseball" | "spark" | "spike" | "reflect" | "horns" | "zapped"
How the claw died.
RimEnemy
interface RimEnemy
One enemy. Fuseballs use lane as an EDGE index (edge e is the left boundary of lane e).
id: number— Stable id.kind: string— Kind id.lane: number— Lane (edge for fuseballs).depth: number— Depth: 0 rim, 1 far end, negative above the rim.phase: "climb" | "rim" | "descend" | "hold" | "hover" | "zap"— Climbing, chasing on the rim, sliding back down, holding, hovering or zapping.flip: RimFlip | null— Flip or crossing in progress.timer: number— General timer (next flip, next action, rim time, next step).fireTimer: number— Seconds to the next shot.hp: number— Hits left.target: number— Spiker target depth / UFO target lane.dir: number— Direction memory: spiral side, spark travel, fuseball vertical motion.steps: number— Lane steps taken (sparks).cargo: boolean— Split from a tanker: makes one flip then climbs.
RimEnemyKind
interface RimEnemyKind
One enemy kind. Behaviour parameters not relevant to behaviour are ignored.
id: string— Unique id referenced by levels, cargo and mutations.behaviour: EnemyBehaviour— The behaviour this kind runs.score: number | number[]— Points when destroyed; an array picks one at random (fuseballs: 250/500/750).speed: number— Default climb speed in depth units per second (levels override per kind).hits?: number— Hits to destroy (default 1).zappable?: boolean— The superzapper destroys it (default true).fires?: boolean— Fires bullets up its lane (default false).fireRate?: number— Multiplies the level's fire interval for this kind (default 1; smaller fires more).climb?: ClimbMode— Climb pattern for flipper-family kinds (defaultstraight; a level may override per kind).flipEvery?: number— Seconds between flips while climbing (flipping modes). Default 1.flipTime?: number— Seconds one flip takes. Default 0.5.rimPause?: number— Seconds a rim chaser waits between flips. Default 0.15.rim?: RimArrival— What happens at the rim (defaultchasefor flippers and pulsars).splitInto?: string— Tanker cargo released when shot (and at the rim unlessrimsays otherwise).splitCount?: number— Cargo count (default 2).hitShot?: "horns" | "reflect"— A non-lethal hit sendshorns(only laser shots stop them) or areflected shot back down the lane.holdDepth?: number— Mirror: depth it climbs to and holds (default 0.25).knockback?: number— Mirror: depth a hit knocks it back (default 0.1).spike?: [number, number]— Spiker: shortest and longest spike it lays, as a fraction of the depth (default 0.2..0.75).crossTime?: number— Fuseball: seconds to cross a lane (default 0.35).rimTime?: number— Fuseball: seconds it rides the rim before dropping back (default 4).stepTime?: number— Spark: seconds per lane step (default 0.07).laps?: number— Spark: laps of the rim before it burns out (0 = never; default 1).hoverDepth?: number— UFO: hover depth above the rim, negative (default -0.09).zap?: [number, number]— UFO: seconds between lane zaps (default 2.5) and seconds a zap lasts (default 0.55).potency?: number— Pulsar: deepest depth at which the pulse is lethal (default 1, anywhere).avoidEnds?: boolean— Spawn away from the end lanes of an open web (tankers).
RimEnemyShot
interface RimEnemyShot
An enemy bullet, demon horns or a reflected shot travelling toward the rim.
id: number— Stable id.lane: number— Lane.depth: number— Depth.speed: number— Depth/s toward the rim.type: "bullet" | "horns" | "reflect"— What it is.
RimEvent
type RimEvent = | { kind: "level-start"; level: number; web: string; palette: number } | { kind: "message"; text: string; key: string } | { kind: "fire"; lane: number; depth: number; laser: boolean; droid: boolean } | { kind: "enemy-spawn"; id: number; enemy: string; lane: number } | { kind: "enemy-hit"; id: number; enemy: string; lane: number; depth: number } | { kind: "enemy-killed"; id: number; enemy: string; lane: number; depth: number; score: number; by: "shot" | "zap" | "droid" | "outta-here" } | { kind: "split"; id: number; enemy: string; into: string; lane: number; depth: number } | { kind: "spike-hit"; lane: number; height: number } | { kind: "bullet-killed"; lane: number; depth: number } | { kind: "enemy-fire"; lane: number; depth: number; type: RimEnemyShot["type"] } | { kind: "pulse"; on: boolean } | { kind: "ufo-zap"; id: number; lane: number } | { kind: "player-died"; cause: RimDeath; lane: number; depth: number } | { kind: "zap"; full: boolean; killed: number } | { kind: "token"; id: number; lane: number; depth: number } | { kind: "power"; power: PowerKind | "droid-next"; lane: number } | { kind: "extra-life"; lives: number } | { kind: "jump" } | { kind: "flydown" } | { kind: "level-clear"; level: number; bonus: number } | { kind: "bonus-start" } | { kind: "ring"; index: number; passed: boolean; score: number } | { kind: "bonus-end"; success: boolean; warp: number } | { kind: "game-over" } | { kind: "game-complete" }
Ordered notifications for presentation: drain() them each frame.
RimFlip
interface RimFlip
A flip (flippers, pulsars) or crossing (fuseballs) in progress.
to: number— Lane (or edge, for fuseballs) being moved to.t: number— Progress 0..1.dur: number— Seconds the move takes.
RimGenerator
interface RimGenerator
A timed release: one kind every every seconds (the first after half that).
kind: string— Kind to release.every: number— Seconds between releases.
RimInput
interface RimInput
One step of input.
move?: number— -1 / 0 / +1: step the claw toward lower / higher lane numbers (held = keep stepping).target?: number | null— Absolute lane to glide to (pointer or spinner); overridesmove. Fractions round.fire?: boolean— Fire held.zap?: boolean— Superzapper held (fires on the press).jump?: boolean— Jump held (jumps on the press, when available).aimX?: number— Bonus round crosshair velocity, -1..1 each axis (y down).aimY?: number— Bonus round crosshair velocity, -1..1 each axis (y down).
RimLevel
interface RimLevel
One level: the web, the enemy budget and how it is released, speeds, fire and timings.
web: string— Web id from the library.palette: number— Palette index.count: number— Enemies released in the level (tanker cargo is extra).spawn: { mode: "timers"; generators: RimGenerator[] } | { mode: "quota"; interval: number; quotas: RimQuota[] }— Release schedule: timed generators (Tempest 2000) or population quotas (Tempest).maxActive: number— Most enemies on the web at once.speed?: Record<string, number>— Climb speed per kind id, depth units per second (falls back to the kind).climb?: Record<string, ClimbMode>— Climb mode per kind id (falls back to the kind).fire: { every: number; maxShots: number; speed: number }— Enemy fire: seconds between an enemy's shots, most bullets in flight, bullet speed (depth/s).spikes?: number— Spike height every lane starts with (0..1).pulse?: { period: number; on: number }— Pulsar pulse: cycle seconds and the lethal fraction at the end of each cycle.powerEvery?: number— Kills per power-up release (power-ups must be configured).mutate?: RimMutation[]— Kind substitutions at release or in cargo.
RimMessageKey
type RimMessageKey = "levelStart" | "flydown" | "caught" | "shot" | "fried" | "fuseball" | "spike" | "collect" | "yes" | "excellent" | "warp" | "bonusFail"
Messages the rules emit as message events (omit a key for none).
RimMutation
interface RimMutation
A kind substitution applied at release (cargo: true applies to tanker cargo instead).
from: string— Kind replaced.to: string— Kind used instead.chance: number— Probability 0..1.cargo?: boolean— Apply to tanker cargo rather than to releases.
RimPalette
interface RimPalette
Colours for a level group; web: null is an invisible web (Tempest levels 65-80). Other keys are free for the renderer.
name: string— Display name.web: string | null— Web colour, or null when the web is not drawn.colors: Record<string, string>— Named colours (claw, flipper, tanker, spiker, spike, fuseball, pulsar, shot ...).
RimPowerups
interface RimPowerups
Power-up rules (Tempest 2000).
sequence: Array<{ power: PowerKind; message?: string }>— What successive catches in a level give; the last entry repeats.firstAfter: number— Kills before the first release in a level (default 1).speed: number— Speed a released token travels toward the rim, depth/s.catchDepth: number— Catch window: the token is caught in the claw's lane within this depth of the claw.bonusPoints: number— Points for abonuscatch.outtaHere: { chance: number; points: number; message?: string }— Chance abonuscatch is Outta Here instead, and its points.warpTokens: number— Tokens needed for the warp bonus round;warpZapsfires the superzapper on a token catch.warpZaps: boolean— A warp token catch also fires a full superzap.warpMessages?: string[]— Messages for the warp count: index = tokens still needed (0 = enabled).laser: { shotSpeed: number; fireInterval: number; spikeDamage: number }— Particle laser: shot speed, fire interval and spike damage while held.droid: { depth: number; laneTime: number; fireInterval: number; maxShots: number }— A.I. droid: depth it hovers at, seconds per lane, seconds between shots, shots in flight.droidOnFlydownCatch: boolean— Catching a token during the fly-down makes the next level's first catch the droid ("YES! YES! YES!").keepOnDeath?: boolean— Keep laser, jump and the sequence position after a death (default false: lost).
RimQuota
interface RimQuota
A population quota: keep between min and max of these kinds on the web while releasing.
kinds: string[]— Kinds counted together (one is chosen at random to release).min: number— Released first while fewer than this are active.max: number— Never released while this many are active.
RimRing
interface RimRing
A bonus-round ring.
index: number— Index in the round.x: number— Centre in the unit disc.y: number— Centre in the unit disc.depth: number— Depth: 1 far, 0 at the player.
RimShooterConfig
interface RimShooterConfig
Complete rim-shooter definition. Presets build the arcade ones.
hz: number— Fixed steps per second.seed?: number— Seed for every random decision.webs: WebShape[]— Web library.palettes: RimPalette[]— Palettes by index.kinds: RimEnemyKind[]— Enemy kinds.levels: RimLevel[]— Levels in order (level 1 is index 0).loopFrom?: number— After the last level, play a random level from this 1-based level onward forever; omit to end the game ("complete").claw: { /** Top lane speed (lanes/s) and the speed a move starts at. */ speed: number; /** Lanes/s gained per second of holding a direction. */ accel: number; /** Lanes/s when a move starts. */ startSpeed: number; /** Seconds between shots while fire is held. */ fireInterval: number; /** Claw shots in flight. */ maxShots: number; /** Shot speed, depth/s. */ shotSpeed: number; /** Jump physics (depth/s launch speed and gravity); null for no jump at all. \always` makes it available without a power-up. */ jump: { speed: number; gravity: number; always?: boolean } | null; }` — The claw.hitDepth: number— Collision half-depth between a shot and a target.spikeHit: { damage: number; points: number }— Spike height removed per standard hit, and points per hit.superzapper: { full: number; single: number; rechargeOnDeath: boolean } | null— Superzapper: full clears and single kills per level; recharge after a death. Null for none.lives: { start: number; extraEvery: number; max: number }— Lives, an extra life everyextraEverypoints (0 none), and the cap.powerups: RimPowerups | null— Power-ups, or null.flyDown: { start: number; accel: number; max: number }— The fly-down between levels: starting speed, acceleration and top speed (depth/s).bonus: RimBonusRound | null— Warp bonus round, or null.start: { levels: number[]; bonus: Record<number, number> }— Starting levels a player may choose and the bonus each pays when completed (Tempest's SkillStep).timing: { ready: number; death: number; caught: number; clear: number }— Seconds of the ready banner, death sequence, a catch drag, and the cleared pause.messages: Partial<Record<RimMessageKey, string>>— Message texts.
RimShot
interface RimShot
A claw or droid shot.
id: number— Stable id.lane: number— Lane.depth: number— Depth.laser: boolean— Fired with the particle laser.droid: boolean— Fired by the A.I. droid.
RimState
interface RimState
The whole serialisable game state; read it through state.
time: number— Seconds since the game began.status: RimStatus— Status.statusTime: number— Seconds in this status.resume: "playing" | "flydown"— Status to enter when READY ends (playing, or the fly-down after a spike death).level: number— Level number, from 1.def: number— Index of the level definition being played (differs from level - 1 past the end of a looping list).web: string— Current web id.lanes: number— Lanes on the current web.closed: boolean— The current web wraps.palette: number— Palette index.score: number— Score.lives: number— Lives left, including the claw in play.nextExtra: number— Score for the next extra life.claw: { pos: number; target: number; lane: number; height: number; vy: number; fireClock: number; held: number; lastMove: number }— The claw: continuous lane position, the lane it is heading to, its lane, jump height (negative) and speeds.fly: { depth: number; speed: number }— Depth of the claw in the fly-down, and its speed.shots: RimShot[]— Claw and droid shots.enemies: RimEnemy[]— Enemies.enemyShots: RimEnemyShot[]— Enemy bullets, horns and reflections.spikes: number[]— Spike height per lane, 0..1 from the far end.tokens: RimToken[]— Power-up tokens in flight.droid: { pos: number; target: number; moveClock: number; fireClock: number } | null— The A.I. droid.laser: boolean— Particle laser held.jump: boolean— Jump available.powerIndex: number— Power-ups caught this level (position in the sequence).powerCounter: number— Kills until the next token.warpTokens: number— Warp tokens held.droidUsed: boolean— A droid was had this level.droidNext: boolean— Next catch is the droid.zapUses: number— Superzapper uses this level.zapAge: number— Seconds since the last zap (for drawing the flash).pulseClock: number— Pulse clock (seconds).spawn: { released: number; count: number; clocks: number[]; requeue: string[]; requeueClock: number }— Release bookkeeping: enemies released, budget, generator clocks, kinds waiting to re-enter after a death.death: { cause: RimDeath; by: number } | null— Cause of the last death and the enemy responsible.bonus: { rings: RimRing[]; next: number; clock: number; aimX: number; aimY: number; passed: number; lastX: number; lastY: number } | null— Bonus round in progress.startBonus: { level: number; points: number } | null— Start-level bonus waiting for its level to be cleared.held: { zap: boolean; jump: boolean }— Edge memory for zap and jump.nextId: number— Next id.random: number[]— The kernel generator's state (four words fromcreateRng), advanced in place, so a snapshot carries the stream.
RimStatus
type RimStatus = "ready" | "playing" | "dying" | "flydown" | "clear" | "bonus" | "game-over" | "complete"
Status of the game.
RimToken
interface RimToken
A released power-up token.
id: number— Stable id.lane: number— Lane.depth: number— Depth.
T2KOptions
interface T2KOptions
Options for tempest2000Preset.
lives?: number— Starting lives (default 3).extraEvery?: number— Extra life every this many points (default 20,000).powerups?: boolean— Power-ups, the A.I. droid and warp tokens (default true).bonusRound?: boolean— The warp bonus round (default true; needs power-ups).speed?: number— Multiplies every enemy speed and divides release intervals (default 1).webs?: WebShape[]— Replace the web sequence: level n useswebs[(n - 1) % length].seed?: number— Random seed (default 1).
WebPoint
type WebPoint = [number, number]
A 2D point in a web's own authoring units, y down.
WebShape
interface WebShape
One web: an authored silhouette extruded away from the player.
points are the rim vertices in order. A closed web has one lane per point (the last joins the first); an
open web has one lane fewer than it has points and does not wrap. Lanes may have any length: the rules only
ever count lanes, and the renderer takes the geometry from here.
id: string— Unique id referenced by levels.name: string— Display name.closed: boolean— True when the last lane joins the first, so the claw wraps.points: WebPoint[]— Rim vertices in authoring units, y down. At least 2 (open) or 3 (closed).start?: number— Lane the claw starts on. Default: the lane nearest the bottom of the web.eye?: WebPoint— The point the web recedes toward (the camera's eye), in authoring units. Default: the frame centre for a closed web, above the shape for an open one.far?: number— Far rim size as a fraction of the near rim (0..1): smaller is a deeper tube. Default 0.25 closed, 0.32 open.frame?: { x: number; y: number; half: number }— The authoring frame the renderer fits to the screen: centre and half-size. Default the shape's own bounds. Sharing one frame across a library keeps small webs small.
WebSpec
interface WebSpec
What generateWeb builds.
kind: "polygon" | "star" | "ellipse" | "line" | "arc" | "zigzag" | "spiral"— Shape family.lanes: number— Lanes to produce (2..256).id?: string— Id of the generated web.sides?: number— polygon: corner count (default 4); star: points (default 5); zigzag: teeth (default 3); spiral: turns (default 1.2).ratio?: number— star inner radius / ellipse and arc height as a fraction (default 0.5 star, 0.6 ellipse, 1 arc).rotation?: number— Rotation in radians (closed shapes).sweep?: number— arc: swept angle in radians (default PI).far?: number— Far-rim ratio to store on the web.
WebView
interface WebView
A web prepared for drawing: unit-space rim, vanishing point and depth ratio.
path: Array<{ x: number; y: number }>— Rim vertices in unit space (the frame maps to -1..1), y down.closed: boolean— True when the web wraps.lanes: number— Playable lanes.far: number— Far rim size as a fraction of the near rim.vanish: { x: number; y: number }— Where the far rim's scaled copy sits, in unit space:farPoint = vanish + path * far.eye: { x: number; y: number }— The eye in unit space:farPoint = eye + (nearPoint - eye) * far.start: number— Lane the claw starts on.


