You're reading the 2.x docs. quantum-forge 3.0 is out: this page in 3.x / migration guide

Skip to content

Packages API ​

Every subpath on this page belongs to quantum-forge-engine. They are plain imports, not separate installs.

Collision ​

typescript
import {
  rectOverlaps,
  rectOverlapsAny,
  getRectCollisions,
  circleOverlaps,
  circleOverlapsAny,
  pointInRect,
  pointInCircle,
  rectCircleOverlaps,
  distance,
  distanceSquared,
  lineIntersects,
  SpatialGrid,
} from "quantum-forge-engine/collision";

Shapes are plain objects: Rect is { x, y, width, height }, Circle is { x, y, radius }, Point is { x, y }.

Functions ​

FunctionReturnsDescription
rectOverlaps(a: Rect, b: Rect)booleanAABB overlap
rectOverlapsAny(rect: Rect, others: Rect[])booleanOverlap against a list
getRectCollisions(rect: Rect, candidates: Rect[])Rect[]Every candidate that overlaps
circleOverlaps(a: Circle, b: Circle)booleanCircle overlap
circleOverlapsAny(circle: Circle, others: Circle[])booleanOverlap against a list
pointInRect(point: Point, rect: Rect)booleanPoint containment
pointInCircle(point: Point, circle: Circle)booleanPoint containment
rectCircleOverlaps(rect: Rect, circle: Circle)booleanMixed shape overlap
distance(a: Point, b: Point)numberEuclidean distance
distanceSquared(a: Point, b: Point)numberSquared distance, no square root
lineIntersects(a1, a2, b1, b2)booleanSegment intersection, four Point arguments

SpatialGrid ​

Broad-phase bucketing over objects that carry x and y.

typescript
const grid = new SpatialGrid<Enemy>(50);  // cell size, default 50

grid.add(enemy);
grid.update(enemy, oldX, oldY);   // call after moving; re-buckets if the cell changed
grid.remove(enemy);
grid.clear();

const nearby = grid.getNearby(player.x, player.y);  // this cell plus the 8 around it
for (const enemy of nearby) {
  if (circleOverlaps(player, enemy)) { /* ... */ }
}

getNearby takes coordinates, not a rectangle, and returns candidates. Confirm real overlaps with one of the functions above.

Audio ​

typescript
import { AudioManager } from "quantum-forge-engine/audio";

Wrapper around Howler.js. howler is an optional peer dependency: install it before importing this subpath. The subpath also re-exports Howl and Howler.

Constructor ​

typescript
new AudioManager(config?: {
  masterVolume?: number;    // 0-1, default 1.0
  soundVolume?: number;     // 0-1, default 1.0
  musicVolume?: number;     // 0-1, default 0.5
  pauseOnHidden?: boolean;  // mute when the tab is hidden, default true
  logger?: LoggerInterface;
})

Methods ​

MethodReturnsDescription
loadSound(name, url, volume?)Promise<void>url is one URL or a list in preference order; Howler plays the first the browser can decode. Rejects when nothing loads, and drops the entry so play() reports it missing
loadSounds(sounds, options?)Promise<SoundLoadResult>Load { name, url, volume? }[] in parallel. From engine 1.4.0 it resolves with { loaded, failed } and logs a warning naming each failure, so one bad file cannot keep a game from starting; pass { throwOnFailure: true } to reject after everything has settled
play(name, volume?)voidPlay a loaded sound
playMusic(name)voidPlay a loaded track as music, stopping the current one
stopMusic()voidStop the current music
setMasterVolume(volume)void0-1, applies to everything
setSoundVolume(volume)void0-1, sound effects only
setMusicVolume(volume)void0-1, music only
mute()voidMute. Takes no argument
unmute()voidUnmute
isUnlocked()booleanWhether a user gesture has unlocked the audio context. On iOS and Safari nothing plays until this is true
destroy()voidUnload sounds and remove listeners
typescript
const audio = new AudioManager({ musicVolume: 0.4, logger });
await audio.loadSound("hit", "/sounds/hit.mp3");
audio.play("hit");

Ship MP3. Safari cannot decode Ogg Vorbis, and a game that awaits its sounds before starting never starts there. If you keep Ogg for the browsers that play it, list both and Howler picks the first playable source: url: ["/sounds/hit.ogg", "/sounds/hit.mp3"].

Particles ​

typescript
import { ParticleSystem } from "quantum-forge-engine/particles";

Constructor ​

typescript
new ParticleSystem(config?: {
  maxParticles?: number;  // default: 1000
  gravity?: number;       // default: 0
  logger?: LoggerInterface;
})

Methods ​

MethodDescription
emit(config: EmitConfig)Full control. Everything else is a preset over this
burst(x, y, direction, count?, color?)Directional burst. Defaults: 10 particles, "#0ff"
trail(x, y, count?, color?)Backwards trail. Defaults: 3 particles, "#fff"
explode(x, y, count?, color?)Radial explosion. Defaults: 20 particles, "#ff0"
update(deltaTime)Advance the simulation. deltaTime in seconds
render(ctx: CanvasRenderingContext2D)Draw to a 2D context
renderToGraphics(graphics: Graphics)Draw to a PixiJS Graphics
getParticles(): ReadonlyArray<Particle>Live particle list
getCount(): numberLive particle count
clear()Remove every particle

burst, trail, and explode take positional arguments, not an options object.

EmitConfig ​

typescript
interface EmitConfig {
  x: number;
  y: number;
  count: number;
  color: string;
  size?: number;
  speed?: number;
  spread?: number;     // angle spread in radians
  direction?: number;  // base direction in radians, 0 = right
  life?: number;       // particle lifetime in seconds
}

The lifetime field is life, in seconds.

typescript
const particles = new ParticleSystem({ gravity: 200 });

particles.emit({ x, y, count: 20, color: "#a855f7", speed: 100, life: 0.5 });
particles.burst(x, y, Math.PI / 2, 12, "#a855f7");

particles.update(dt);
particles.render(ctx);

Animation ​

typescript
import {
  Animation,
  Tween,
  MultiTween,
  AnimationManager,
  Easing,
  animateValue,
  animateValues,
  lerp,
  smoothstep,
} from "quantum-forge-engine/animation";

Easing functions live on the Easing object, not as individual exports: Easing.linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeInElastic, easeOutElastic, easeInBounce, easeOutBounce.

Tween ​

Tween animates a single number. duration is in milliseconds and update() takes a timestamp, not a delta.

typescript
const tween = new Tween({
  from: 0,
  to: 100,
  duration: 500,
  easing: Easing.easeOutCubic,
  onUpdate: (value) => { sprite.x = value; },
  onComplete: () => { /* done */ },
});

tween.start();  // required before the first update

function frame(now: number) {
  tween.update(now);  // pass performance.now()
  if (tween.isActive()) requestAnimationFrame(frame);
}
MethodReturnsDescription
start()voidBegin. Resets elapsed time
update(currentTime)booleanAdvance to a timestamp. Returns whether still running
pause() / resume() / stop()voidPlayback control
isActive()booleanWhether the animation is running. There is no isComplete()
getProgress()number0-1
getValue()numberCurrent value

MultiTween ​

For object-shaped animations, use MultiTween with a values map.

typescript
const move = new MultiTween({
  values: { x: { from: 0, to: 100 }, y: { from: 0, to: 200 } },
  duration: 500,
  easing: Easing.easeOutCubic,
  onUpdate: (values) => { sprite.x = values.x; sprite.y = values.y; },
});
move.start();

getValues() returns the whole map, getValue(key) a single entry.

AnimationManager ​

Drives a set of animations from one call. add(), and the tween/multiTween shorthands, call start() for you, and finished animations drop out of the set.

typescript
const anims = new AnimationManager();
const t = anims.tween({ from: 0, to: 1, duration: 300, onUpdate: (v) => {} });
const m = anims.multiTween({ values: { x: { from: 0, to: 10 } }, duration: 300 });

anims.update();  // once per frame, reads performance.now() itself
anims.getActiveCount();
anims.remove(t);
anims.clear();

await anims.animate({ from: 0, to: 1, duration: 300 });  // resolves on complete

animateValue(from, to, duration, onUpdate, config?) and animateValues(values, duration, onUpdate, config?) build a Tween or MultiTween and return it. config is { easing?, onComplete? }. They do not start or drive the animation, so hand the result to an AnimationManager or call start() and update() yourself. lerp(start, end, t) and smoothstep(edge0, edge1, x) are plain math helpers.

Entities ​

typescript
import { EntityManager, createEntity } from "quantum-forge-engine/entities";

An Entity is any object with a string id, an optional active flag, and whatever else your game needs.

typescript
const entities = new EntityManager<Enemy>();
entities.add(createEntity<Enemy>("enemy-1", { x: 10, y: 20, type: "grunt" }));
MethodReturnsDescription
add(entity)voidAdd. A duplicate id logs a warning and replaces
remove(id: string)booleanRemove by id, not by object
get(id)T | undefinedLook up by id
has(id)booleanExistence check
getAll()T[]Every entity
getActive()T[]Entities whose active is not false
filter(predicate)T[]Filter
find(predicate)T | undefinedFirst match
forEach(fn)voidIterate
count()numberHow many
clear()voidRemove everything
getByType(type: string)T[]Entities whose type field matches
getInRect(x, y, width, height)T[]Entities inside a rectangle
getInRadius(centerX, centerY, radius)T[]Entities inside a circle
getNearest(x, y, maxDistance?)T | undefinedClosest entity

createEntity<T>(id, props) builds { id, ...props }.

State machine ​

typescript
import { StateMachine } from "quantum-forge-engine/state-machine";

Config-driven, with a required context object that every hook and guard receives.

typescript
const fsm = new StateMachine<"idle" | "running" | "jumping", "START" | "STOP" | "JUMP" | "LAND", Ctx>({
  initial: "idle",
  context: { stamina: 100 },
  states: {
    idle: { onEnter: (ctx) => { /* ... */ } },
    running: { onUpdate: (ctx, dt) => { ctx.stamina -= dt; } },
    jumping: { onExit: (ctx) => { /* ... */ } },
  },
  transitions: [
    { from: "idle", event: "START", to: "running" },
    { from: "running", event: "STOP", to: "idle" },
    { from: "running", event: "JUMP", to: "jumping", guard: (ctx) => ctx.stamina > 0 },
    { from: "jumping", event: "LAND", to: "running" },
  ],
  logger,
});

fsm.send("START");     // true if a transition fired
fsm.getState();        // "running"
fsm.is("running");     // true
fsm.update(dt);        // runs the current state's onUpdate

The constructor calls the initial state's onEnter right away.

FieldDescription
initialStarting state name
contextRequired. Shared object passed to every hook, guard, and onTransition
statesMap of state name to { onEnter?, onExit?, onUpdate? }
transitionsArray of { from, event, to, guard?, onTransition? }. from accepts one state or an array
loggerOptional logger
MethodReturnsDescription
send(event)booleanWhether a transition fired
getState()TStateCurrent state name
getContext()TContextThe context object
is(state)booleanState check. There is no matches()
can(event)booleanWhether the event would fire a transition now
getAvailableEvents()TEvent[]Events with a passing guard from here
update(dt)voidRun the current state's onUpdate
forceTransition(state)voidJump to a state, running exit and enter hooks

Timer ​

typescript
import { TimerManager } from "quantum-forge-engine/timer";

Timers driven by update(dt), not setTimeout, so pausing the game pauses them. Delays are in seconds, matching GameLoop delta time.

typescript
const timers = new TimerManager({ logger });

timers.after(2, () => spawnWave());          // once, after 2s of game time
timers.every(0.5, () => tick());             // forever, every 0.5s
const t = timers.every(1, () => blink(), 3); // three times, then stops

timers.update(dt);  // call each frame
t.cancel();
MethodReturnsDescription
after(delay, callback)TimerHandleOne-shot
every(interval, callback, limit?)TimerHandleRepeating, optionally capped at limit runs
update(dt)voidAdvance timers by scaled delta time
pause() / resume()voidFreeze and unfreeze every timer
isPaused()booleanPaused state
setTimeScale(scale)voidSlow motion or fast forward. 1 is normal
getTimeScale()numberCurrent scale
cancel(handle)voidCancel one timer, by handle or id
cancelAll()voidCancel everything
getActiveCount()numberLive timer count

TimerHandle is { cancel(): void }.

Save ​

typescript
import { SaveManager } from "quantum-forge-engine/save";

Named slots on a pluggable storage backend, with versioning and migration.

Constructor ​

typescript
new SaveManager<TState>({
  prefix?: string;        // key prefix, default "save"
  version?: number;       // default 1
  storage?: StorageBackend;  // default localStorage
  migrate?: (data: SaveData<unknown>, fromVersion: number) => TState;
  logger?: LoggerInterface;
})

migrate runs on load when the stored version differs from the configured one. Without it, load() returns the stored state as it was written, whatever version it carries.

A StorageBackend is { getItem, setItem, removeItem, keys }, so in-memory or server-backed storage drops in.

Methods ​

MethodReturnsDescription
save(slot: string, state)voidWrite a slot
load(slot: string)TState | nullRead a slot, migrating if needed
delete(slot: string)voidRemove a slot
has(slot: string)booleanSlot exists
listSlots()string[]Every slot name under the prefix
enableAutoSave(slot, stateProvider, interval?)voidAuto-save a slot. interval in seconds, default 60
disableAutoSave()voidStop auto-saving
update(dt)voidDrives auto-save. Call each frame
exportAll()stringEvery slot as JSON, for backup or transfer
importAll(json)numberRestore from exportAll output, returns slots written
typescript
const saves = new SaveManager<GameState>({ prefix: "my-game", version: 2 });

saves.save("slot-1", engine.getState());
const restored = saves.load("slot-1");

saves.enableAutoSave("autosave", () => engine.getState(), 30);

Slots are strings and every method takes one. There is no key, autoSave, autoSaveInterval, or maxSlots config.

Scenes ​

typescript
import {
  SceneManager,
  fadeTransition,
  instantTransition,
} from "quantum-forge-engine/scenes";

Stack-based scene lifecycle. There is no registration step: you pass the scene object when you push it.

Scene ​

typescript
interface Scene<TContext = unknown> {
  onEnter?(context: TContext): void | Promise<void>;
  onExit?(): void | Promise<void>;
  onPause?(): void;   // another scene pushed on top
  onResume?(): void;  // the scene above popped
  update(dt: number): void;  // required
  render(): void;            // required
}

SceneManager ​

typescript
const scenes = new SceneManager<GameContext>({ logger });

await scenes.push("menu", menuScene, context);
await scenes.push("game", gameScene, context, fadeTransition(0.3, drawFade));
await scenes.pop();
await scenes.replace("end", endScene, context);

Transitions advance inside update(dt), so a transition's duration uses the same units as the delta you pass in. With GameLoop that is seconds.

MethodReturnsDescription
push(name, scene, context, transition?)Promise<void>Pause the current scene, push and enter a new one
pop(transition?)Promise<void>Exit the top scene and resume the one below
replace(name, scene, context, transition?)Promise<void>Swap the top of the stack
update(dt)voidUpdate the top scene and any running transition
render()voidRender the top scene
isTransitioning()booleanPush, pop, and replace are ignored while true
getTransitionProgress()number0-1
getCurrentName()string | undefinedName of the top scene
getStackDepth()numberStack size

fadeTransition(duration, onUpdate) builds a TransitionEffect that reports 0-1 progress to your own fade drawing. instantTransition() is a zero-duration effect. A TransitionEffect is { duration, onUpdate, onStart?, onComplete? }, so you can write your own.

Powered by Quantum Forge