Phaser AE

Rooftop Runner 2D

Pro packv3.0.0-32DArcade

Forward-only rooftop survival: the runner keeps moving and speeding up, and the player only decides when to jump and for how long. Courses are seeded, and every one comes with real inputs that clear it, whether it is a finite challenge or an endless stream.

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

Released October 14, 2026 · updated October 19, 2026

Rooftop Runner 2D: 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

rooftopRunner2d

function rooftopRunner2d():Pack<{rooftopRunner2d:typeof rules}>

Install renderer-independent runner rules and generation.

Helpers

createRooftop

function createRooftop(config: RooftopConfig)

Start a run on config, validated and copied. The result has config, state (a copy), tick(input?) (one 1/60 s tick; does nothing once won or lost; returns the state), reset(), rewind(frames = 120), recording() (the inputs played up to now) and replay(frames) (plays a RooftopFrame[]; at most 24000 ticks). Throws on an invalid config.

createRooftopStream

function createRooftopStream(options: RooftopOptions)

Start an endless run. It begins like a 6-roof generateRooftop course, and each time the runner stands on the last roof it appends a certified 5-roof stretch. The result has state (the runner's state plus chunk, the stretches appended), config (the terrain still kept), tick(input?), reset(), rewind(frames = 120), recording() and solveAhead() (an input route to win the stretch ahead, or null).

generateRooftop

function generateRooftop(options: RooftopOptions): RooftopLevel

Make a course for options and prove it winnable by planning and replaying a solution. Throws on invalid options, or if no candidate course can be solved within 100 tries.

planRooftop

function planRooftop(config: RooftopConfig, maxFrames = 12000, start?: RooftopState): RooftopFrame[] | null

Searches takeoff time AND hold length at the actual incoming speed for each landing.

ROOFTOP_STEP

const ROOFTOP_STEP: number

Seconds in one fixed tick of the rules: 1/60.

rooftopStyles

const rooftopStyles: readonly ["skyline", "girders", "obstacles", "mixed"]

Every RooftopStyle, for menus and validation.

validateRooftop

function validateRooftop(c: RooftopConfig)

Check a course. Throws on the first fault: a number out of range or not finite, a roof or obstacle with a duplicate id, a bad shape or an unknown kind.

Types

Roof

type Roof = { … }

A roof the runner can land on, in world units with x to the right and y down.

  • id: string — Unique among the config's roofs and obstacles; RooftopState.support names it while the runner stands on it.
  • left: number — The x of the roof's left edge.
  • right: number — The x of the roof's right edge; at least 30 units past left.
  • top: number — The y of the walking surface; a larger y is lower.
  • kind: 'building' | 'girder' | 'tower' — 'building' and 'tower' have solid sides that end the run if struck below the surface; a 'girder''s side is only 18 units deep.

RoofObstacle

type RoofObstacle = { … }

Something on or above a roof that the runner must clear, in world units.

  • id: string — Unique among the config's roofs and obstacles; a clipped crate's id is listed in RooftopState.hits.
  • x: number — The x of the obstacle's centre.
  • y: number — The y of the obstacle's centre.
  • w: number — Width, at least 1.
  • h: number — Height, at least 1.
  • kind: 'crate' | 'hazard' | 'ceiling' — 'crate' is clipped once and costs speed; 'hazard' and 'ceiling' (an overhead beam) end the run on contact.

Rooftop

type Rooftop = ReturnType<typeof createRooftop>

A run made by createRooftop.

RooftopConfig

type RooftopConfig = { … }

A finite course and the runner's motor, in world units and seconds. validateRooftop states the accepted ranges.

  • roofs: Roof[] — The roofs, 1 to 200.
  • obstacles: RoofObstacle[] — The obstacles, 0 to 300.
  • start: { /** The x of the centre. */ x: number; /** The y of the centre; standing on a roof at \top` it is `top - 13`. */ y: number; }` — Where the runner's 16 by 26 body starts: its centre.
  • finish: number — The x the runner must reach while on a roof to win.
  • speed: number — The starting horizontal speed, in units a second; 100 to 700.
  • maxSpeed: number — The horizontal speed the run accelerates to, in units a second; from speed up to 900.
  • acceleration: number — Horizontal acceleration, in units a second squared; 0 to 100.
  • gravity: number — Downward acceleration, in units a second squared; 400 to 1800.
  • jumpSpeed: number — Upward speed a jump starts with, in units a second; 200 to 700.
  • deathY: number — Falling below this y loses the run.

RooftopFrame

type RooftopFrame = { … }

An input held for a run of ticks: a replay or a solution is a list of these.

  • input: RooftopInput — The input applied on every tick of the run.
  • frames: number — How many consecutive ticks of 1/60 s the input is held; a whole number from 1 to 24000.

RooftopInput

type RooftopInput = { … }

One tick's input.

  • jump?: boolean — Whether the jump button is held this tick: a press starts a jump, releasing while rising cuts it short. Anything but a boolean throws.

RooftopLevel

type RooftopLevel = { … }

A generated course with the input route that proves it can be won.

  • seed: number — The seed it was made from.
  • parameters: Required<RooftopOptions> — The options with every default filled in.
  • config: RooftopConfig — The course, ready for createRooftop.
  • solution: RooftopFrame[] — An input route, compressed into runs of ticks, that replays to a win.
  • metrics: { /** Ticks the solution takes to reach the finish. */ frames: number; /** Jumps the solution makes. */ jumps: number; /** Which candidate layout (counting from 0) was the first that could be solved. */ attempt: number; /** The roofs and obstacles as JSON: equal signatures mean equal layouts. */ signature: string; /** Roofs in the course. */ roofs: number; } — Facts about the solved course.

RooftopOptions

type RooftopOptions = { … }

What generateRooftop takes.

  • seed: number — Unsigned 32-bit integer; the same seed and options make the same course.
  • difficulty?: Difficulty — 'easy', 'medium', 'hard' or 'very-hard'; default 'medium'. Harder is faster, with wider gaps and bigger height changes.
  • style?: RooftopStyle — The kind of course; default 'mixed'.
  • roofs?: number — Roofs in the course, 3 to 30; default 6 plus 2 for each difficulty step plus the seed modulo 3.

RooftopState

type RooftopState = { … }

Where the runner is and how the run stands; plain data, copied on every read.

  • x: number — The x of the body's centre, in world units.
  • y: number — The y of the body's centre, in world units; a larger y is lower.
  • vx: number — Horizontal speed, in units a second.
  • vy: number — Vertical speed, in units a second; negative is rising.
  • frame: number — Ticks played since the start.
  • grounded: boolean — Whether the runner stands on a roof.
  • support: string | null — The id of the roof it stands on, or null in the air.
  • held: boolean — Whether jump was held on the last tick; a jump needs a fresh press.
  • coyote: number — Ticks left (at most 4) in which a jump is still allowed after leaving a roof.
  • jumps: number — Jumps made so far.
  • hits: string[] — The ids of the crates already clipped.
  • won: boolean — Whether the finish was reached.
  • lost: boolean — Whether the run has ended in a loss.
  • reason: string — A sentence on how the run stands, such as why it was lost.
  • distance: number — The furthest the runner has got from the start along x, in world units.

RooftopStream

type RooftopStream = ReturnType<typeof createRooftopStream>

An endless run made by createRooftopStream.

RooftopStyle

type RooftopStyle = 'skyline' | 'girders' | 'obstacles' | 'mixed'

The look of a generated course: 'skyline' buildings and towers, 'girders' girder roofs between buildings, 'obstacles' crates and hazards on the roofs, 'mixed' a bit of each.