Phaser AE

Stealth

Pro packv3.0.0-72DArcade

Guards with eyes, ears and a memory: they notice you by degrees, investigate what they heard, search where they last saw you, and call their friends when they are sure. The rules run on their own, so you can drive them from any renderer and tune how sharp or sleepy each level's guards are.

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

Stealth: 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

stealth

function stealth(): Pack<{ stealth: StealthApi; }>

Install pure rules and owned cancelable generation jobs.

Helpers

createInfiltration

function createInfiltration(seed: number, difficulty: Difficulty = 'medium', options: InfiltrationOptions = {})

Make an Infiltration level from a seed, a difficulty (default medium; harder levels have more guards and obstacles) and options. Throws stealth: … on options it cannot play.

createStealth

function createStealth(guards:readonly Guard[],walls?:readonly Rect[],options?:StealthOptions):Stealth

Guards, the walls that block sight and muffle hearing, then options: canSee, gain and memory. The 3.0.0-1 form: a bare canSee after the walls, and options after it. Implementation of the overloads above: guards, walls, then options or a bare canSee followed by options.

generateInfiltrationLevel

function generateInfiltrationLevel(seed: number, difficulty: Difficulty = 'medium', attempts = 24, options: InfiltrationOptions = {}): InfiltrationLevel

Bounded level generation with a complete real-input key-and-exit certificate.

guard

function guard(id:string,x:number,y:number,facing=0):Guard

A calm guard at (x, y) looking along facing radians (0 is +x), with sight range 200, a cone of 0.6π radians and hearing 200 world units; a starting point to adjust.

infiltrationBot

function infiltrationBot(game: Infiltration): InfiltrationInput

Real-input demonstration: prefer cover and quieter routes; no immunity or hidden-state movement overrides.

infiltrationTuning

function infiltrationTuning(difficulty: Difficulty = 'medium', options: InfiltrationOptions = {}): InfiltrationTuning

Fill in every default of options for a difficulty. Throws stealth: … naming each value that is not a usable number.

sightGain

function sightGain(o:SightGainOptions={}):Gain

The stock gain: a rate that can fall off with distance and scale with how visible the target is. sightGain() is the default, the original flat 1.1.

Classes

Infiltration

class Infiltration

A complete stealth level: a generated arena, a key to collect, an exit to reach and guards on patrol. Create with createInfiltration; it advances in fixed 1/60 s steps.

  • readonly layout: ArenaLayout — The generated arena: walls, open cells, the player's start and the exit.
  • readonly stealth: Stealth — The guards' sight, hearing and suspicion state.
  • readonly key: Point — Where the gold key lies, world units.
  • readonly patrols: Point[][] — Patrol waypoints for each guard, in the order of stealth.guards.
  • player: Point — The player's position, world units.
  • time: number — Seconds elapsed since the start.
  • noiseCooldown: number — Seconds until the player's footsteps can make noise again; 0 when ready.
  • decoys: number — Decoys left to throw; starts at tuning.decoys.count.
  • hasKey: boolean — Whether the player has picked up the key.
  • won: boolean — Whether the player reached the exit with the key.
  • lost: boolean — Whether the player was caught.
  • alarmTime: number — Seconds some guard has been on alert, draining at caught.drain times the rate when none is; the player is caught at caught.alarm.
  • readonly tuning: InfiltrationTuning — What this level plays by: the options with every default filled in.
  • message: string — The line of feedback for the player, updated as things happen.
  • constructor(readonly seed: number, readonly difficulty: Difficulty = 'medium', options: InfiltrationOptions = {}) — A level from a seed, a difficulty and options (see InfiltrationOptions; the defaults are the classic level).
  • tick(input: InfiltrationInput = {}) — Advance one fixed 1/60 s step with the player's input. Does nothing once the level is won or lost.

Stealth

class Stealth

Sight uses the existing visibility query. Hearing is attenuated by walls; alert propagation preserves last known position. The Stealth owns its guards: it clones them, and guards / guard(id) are the live records tick reads and writes. A game that moves guards itself hands each pose over with update before tick and reads beliefs back from guard(id).

  • readonly guards:Guard[] — The live guards, cloned from the constructor's; tick reads and writes them.
  • readonly events: StealthEvent[] — Events raised since the last drain.
  • readonly visibility:Visibility2d — The visibility query built from the walls; it answers sight and wall-muffled hearing unless a canSee was given.
  • readonly canSee?:CanSee — The caller's own line-of-sight test, when one was given.
  • readonly memory:number — Seconds a sighting or a noise is remembered (StealthOptions.memory).
  • readonly cooling:Settled<Cooling> — The rates in force, defaults filled in (StealthOptions.cooling).
  • readonly hearing:Settled<Hearing> — The hearing rule in force (StealthOptions.hearing).
  • readonly radio:Settled<Radio> — The radio in force (StealthOptions.radio).
  • constructor(guards:readonly Guard[],walls?:readonly Rect[],options?:StealthOptions) — Guards, the walls that block sight and muffle hearing, then options. The 3.0.0-1 form: a bare canSee after the walls, and options after it.
  • constructor(guards:readonly Guard[],walls:readonly Rect[],canSee:CanSee|undefined,options?:StealthOptions) — Guards, the walls that block sight and muffle hearing, then options. The 3.0.0-1 form: a bare canSee after the walls, and options after it.
  • constructor(guards:readonly Guard[],walls:readonly Rect[]=[],options:StealthOptions|CanSee|undefined={},more:StealthOptions={}) — Guards, the walls that block sight and muffle hearing, then options. The 3.0.0-1 form: a bare canSee after the walls, and options after it.
  • sees(g:Guard,t:StealthTarget) — Whether guard g sees target t now: the target is visible, within range × visibility, inside the view cone, and not behind a wall.
  • gain(g:Guard,t:StealthTarget):number — Suspicion per second g gains from t now: the gain while it sees it, 0 when it cannot. For a detection meter or an AI's own decisions.
  • guard(id:string):Guard — The live guard with this id. Throws for an unknown id.
  • update(id:string,pose:GuardUpdate):Guard — Set a guard's pose, and any sense to retune, before tick. Checked as a whole before anything changes; returns the live guard.
  • add(guard:Guard):Guard — Add a guard mid-game (reinforcements), cloned and checked like the constructor's. Returns the live guard.
  • remove(id:string):Guard — Take a guard out (knocked out, off shift). Throws for an unknown id; returns the removed guard.
  • noise(n:Noise) — Queue a noise for the next tick. Throws unless radius is finite and at least 0 and strength is within 0..1.
  • tick(dt:number,target:StealthTarget) — Advance every guard by dt seconds (0..1) against the target and the queued noises, then pass radio reports; clears the noises and appends to events. Throws for a dt outside 0..1 or a target visibility outside 0..2.
  • drain() — Take and clear the events raised since the last drain, oldest first.

Types

CanSee

type CanSee=(a:Point,b:Point)=>boolean

A line-of-sight test from a to b, for a game that already owns occlusion.

Cooling

interface Cooling

Suspicion lost per second out of sight. The defaults are the original fixed rates.

  • remembering?:number|undefined — While the guard still remembers (its memory is above 0): it stays on edge. At least 0; default 0.06.
  • forgetting?:number|undefined — After it forgets: it calms down. At least 0; default 0.28.

Gain

type Gain=(guard:Guard,target:StealthTarget,distance:number)=>number

Suspicion per second (1 raises the alarm) that guard gains while it sees target, distance away. Only asked while it does.

Guard

interface Guard extends Point

A guard: where it stands and looks, what it can sense, and what it believes. World units and radians.

  • id:string — Unique and non-empty.
  • facing:number — Direction of view, radians; 0 is +x.
  • range:number — Sight distance for a target of visibility 1; at least 1.
  • fov:number — Full cone angle, radians, 0..2π.
  • hearing:number — Hearing distance; at least 1. Walls cut it to 35%.
  • suspicion:number — 0 calm to 1 identified, which raises the alarm.
  • mode:'patrol'|'investigate'|'search'|'alert' — What the guard is doing: patrol (calm), investigate (suspicious, going to what it saw or heard), search (heading for the last-known position after losing the target or hearing a radio report), alert (target identified, suspicion 1).
  • lastKnown:Point|null — Where the guard last saw or heard the target, or was told of it by radio; null once calm.
  • memory:number — Seconds left before an unseen target is forgotten: set to the Stealth's memory by a sighting or a noise, counted down out of sight.

GuardUpdate

interface GuardUpdate

What update takes: any of a guard's pose and senses. Other fields are ignored, so a game's own guard record can be passed as it is.

  • x?:number|undefined — New x position, world units.
  • y?:number|undefined — New y position, world units.
  • facing?:number|undefined — New direction of view, radians; 0 is +x.
  • range?:number|undefined — New sight distance for a target of visibility 1, world units; at least 1.
  • fov?:number|undefined — New full cone angle, radians, 0..2π.
  • hearing?:number|undefined — New hearing distance, world units; at least 1.

Hearing

interface Hearing

How noises are heard. A guard hears a noise within the smaller of its own hearing and the noise's radius. The defaults are the original rule.

  • walls?:number|undefined — 0..1, the share of that reach left when a wall is in the way. Default 0.35.
  • scale?:number|undefined — A noise raises suspicion to its strength × this, at least 0. Default 0.7.
  • cap?:number|undefined — 0..1: a noise raises suspicion to this at most, so below 1 a noise alone never raises the alarm. It never lowers it. Default 0.95.

InfiltrationInput

interface InfiltrationInput

One step of player input for Infiltration.tick.

  • x?: number — Movement along x, -1 (left) to 1 (right); the x and y pair is scaled down to length 1 when longer. Default 0.
  • y?: number — Movement along y, -1 to 1 (+y is down the screen); the x and y pair is scaled down to length 1 when longer. Default 0.
  • sneak?: boolean — Sneak: move at player.sneakSpeed instead of player.speed, make no footstep noise and be player.sneakVisibility as visible. Default false.
  • decoy?: Point — World point to throw a decoy at; used only within decoys.reach of the player and while decoys remain. It makes a loud noise there (decoys.radius, decoys.strength).

InfiltrationLevel

interface InfiltrationLevel

A generated level with a replayable proof that it can be won.

  • seed: number — The seed that was asked for.
  • actualSeed: number — The seed that produced the level: seed itself, or a derived one when seed had no winnable level.
  • difficulty: Difficulty — The difficulty the level was made at.
  • options: InfiltrationOptions — The options the level was made with; pass them, actualSeed and difficulty to createInfiltration to play it.
  • solution: InfiltrationInput[] — The inputs, one per 1/60 s step, that collect the key and reach the exit when replayed from the start.
  • attempts: number — How many seeds were tried, counting the one that worked.

InfiltrationOptions

interface InfiltrationOptions

How an infiltration level plays. Every number has a default, and the defaults are the recipe's classic level; the difficulty sets the room's obstacles and the guards' count, sight range and field of view unless they are given.

  • arena?: Omit<ArenaOptions, 'seed'> — The room: arena-layout's options, the seed aside. Default arena-layout's, with obstacles from the difficulty: 7 to 12 on easy, 2 more each step harder.
  • guards?: number — Guards on patrol, 1 or more. Default 4 on easy, one more each step harder.
  • guardRange?: number — How far a guard sees, world units. Default 130 on easy, 22 more each step harder.
  • guardFov?: number — A guard's field of view, radians. Default 1.1 on easy, 0.1 more each step harder.
  • spacing?: number — Least distance from the player's start to the key and to each guard's starting place, world units. Default 180.
  • guardSpeed?: { patrol?: number; search?: number; alert?: number } — Guards' speeds by mode, world units a second. Default patrol 40, search 70, alert 115.
  • lookAround?: { patrol?: number; search?: number } — How fast a guard that has reached its goal turns to look round, radians a second. Default patrol 0.4, search 1.4.
  • patrol?: { cells?: number; every?: number; dwell?: number } — Patrol routes: from the guard's cells nearest open cells, every every-th is a waypoint, held dwell seconds. Default 14, 3 and 4.
  • player?: { speed?: number; sneakSpeed?: number; radius?: number; sneakVisibility?: number } — The player: speed and sneakSpeed in world units a second, radius in world units, and sneakVisibility, how visible a sneaking player is (1 fully). Default 150, 72, 10 and 0.55.
  • footsteps?: { radius?: number; strength?: number; every?: number } — Footsteps while running: a noise of radius world units and strength (0 to 1) at most every every seconds. Default 95, 0.35 and 0.5.
  • decoys?: { count?: number; reach?: number; radius?: number; strength?: number } — Decoys: count to throw, thrown at most reach world units, each a noise of radius and strength. Default 3, 260, 300 and 0.8.
  • caught?: { distance?: number; alarm?: number; drain?: number } — Being caught: an alerted guard within distance world units, or alarm seconds of alert in all (it drains at drain times the rate while no guard is alert). Default 22, 3 and 0.5.
  • reach?: { key?: number; exit?: number } — How close the player must come to take the key and to leave by the exit, world units. Default 22 and 25.

InfiltrationTuning

interface InfiltrationTuning

InfiltrationOptions with every default filled in for a difficulty: what a level plays by.

  • arena: Omit<ArenaOptions, 'seed'> — The room's options, obstacles filled in.
  • guards: number — Guards on patrol.
  • guardRange: number — A guard's sight range, world units.
  • guardFov: number — A guard's field of view, radians.
  • spacing: number — Least distance from the player's start to the key and the guards, world units.
  • guardSpeed: { patrol: number; search: number; alert: number } — Guards' speeds by mode, world units a second.
  • lookAround: { patrol: number; search: number } — Look-round turn rates at a goal, radians a second.
  • patrol: { cells: number; every: number; dwell: number } — Patrol route making and the seconds held at each waypoint.
  • player: { speed: number; sneakSpeed: number; radius: number; sneakVisibility: number } — The player's speeds, size and sneaking visibility.
  • footsteps: { radius: number; strength: number; every: number } — Footstep noise while running.
  • decoys: { count: number; reach: number; radius: number; strength: number } — Decoys and their noise.
  • caught: { distance: number; alarm: number; drain: number } — What catches the player.
  • reach: { key: number; exit: number } — Pick-up and exit distances, world units.

Noise

interface Noise extends Point

A sound made at a point (world units), queued with Stealth.noise and heard on the next tick.

  • radius:number — How far it carries, world units, at least 0; a guard hears it within the smaller of this and its own hearing.
  • strength:number — Loudness, 0..1: it raises a hearing guard's suspicion to this × the hearing scale, capped by the hearing cap.

Radio

interface Radio

The radio. When a guard raises the alarm, the others in reach search where it saw the target, without seeing it themselves. The defaults are the original rule.

  • reach?:number|undefined — How far a report carries, as a multiple of the larger hearing of the two guards; 0 is no radio. At least 0; default 1.5.
  • suspicion?:number|undefined — 0..1, the suspicion a report leaves a guard with at least. Default 0.75.
  • memory?:number|undefined — Seconds a report is remembered, at least 0. Default memory + 1, time to get there.

Settled

type Settled<T>={readonly [K in keyof T]-?:Exclude<T[K],undefined>}

An options group with every default filled in.

SightGainOptions

interface SightGainOptions

What sightGain takes. With none, the original rule: 1.1 a second, the same anywhere in the cone.

  • rate?:number|undefined — Suspicion per second at full exposure; at least 0. Default 1.1.
  • falloff?:number|undefined — 0..1, how much distance slows it. 0 (default): the same anywhere in the cone. 1: full at the guard, nothing at the edge of its effective range (range × visibility). Linear between.
  • visibility?:number|undefined — 0..1, how much the target's visibility scales it, beyond the range it already shrinks. 0 (default): not at all. 1: in proportion, so 0.5 fills at half the rate and 1.5 at one and a half times.

StealthApi

type StealthApi = typeof rules & { /** `generateInfiltrationLevel` in an owned worker: cancel with `job.signal`; `job.options` are the level's options and `job.attempts` the seeds to try (default 24). */ generateInfiltrationLevelAsync(seed: number, difficulty?: import('../../action-utils.js').Difficulty, job?: { signal?: AbortSignal; options?: infiltration.InfiltrationOptions; attempts?: number; }): Promise<infiltration.InfiltrationLevel>; }

Pure policies and cancelable generation methods exposed by the composed pack.

StealthEvent

interface StealthEvent

A change of a guard's state, drained with Stealth.drain.

  • guard:string — Id of the guard concerned.
  • kind:'sight'|'hearing'|'alarm'|'lost'|'calmed' — sight: a patrolling guard saw the target; hearing: it heard a noise; alarm: it identified the target or got a radio report; lost: an alerted guard lost sight and went to search; calmed: its search expired and it went back to patrol.
  • reason:string — A short sentence saying why, for a log or a debug view.
  • position:Point — The place involved, world units: where the target was seen, the noise was made, or the target was last known.

StealthOptions

interface StealthOptions

What createStealth takes after the walls (or after a canSee). All optional; the defaults are the original fixed rule.

  • canSee?:CanSee|undefined — Your own line-of-sight test in place of the walls' visibility query.
  • gain?:Gain|undefined — How fast sight fills suspicion: the one place it is tuned. Suspicion per second while the guard sees the target, given the guard, the target (its visibility) and the distance between them. Default sightGain(): a flat 1.1 anywhere in the cone, about 0.9 s from calm to alarm. Build one with sightGain({rate, falloff, visibility}), or write your own so a far or dim glimpse fills slower than a close one, a captain faster than a recruit (the guard is passed), or a floor sharper once the alarm has sounded. It must return a finite number; below 0 counts as 0.
  • memory?:number|undefined — Seconds a guard remembers a sighting or a noise before its suspicion starts draining fast. At least 0; default 4.
  • cooling?:Cooling|undefined — How fast suspicion drains while a guard cannot see the target.
  • hearing?:Hearing|undefined — How a guard hears a noise.
  • radio?:Radio|undefined — The radio report a guard sends the others when it raises the alarm.

StealthTarget

interface StealthTarget extends Point

What the guards watch for: a position (world units) and how visible it is.

  • visibility:number — How visible the target is, 0..2: it scales every guard's sight range (1 is normal, 0 is invisible, below 1 is crouching or in shadow).