Packages API
Every subpath on this page belongs to quantum-forge-engine. They are plain imports, not separate installs.
Collision
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
| Function | Returns | Description |
|---|---|---|
rectOverlaps(a: Rect, b: Rect) | boolean | AABB overlap |
rectOverlapsAny(rect: Rect, others: Rect[]) | boolean | Overlap against a list |
getRectCollisions(rect: Rect, candidates: Rect[]) | Rect[] | Every candidate that overlaps |
circleOverlaps(a: Circle, b: Circle) | boolean | Circle overlap |
circleOverlapsAny(circle: Circle, others: Circle[]) | boolean | Overlap against a list |
pointInRect(point: Point, rect: Rect) | boolean | Point containment |
pointInCircle(point: Point, circle: Circle) | boolean | Point containment |
rectCircleOverlaps(rect: Rect, circle: Circle) | boolean | Mixed shape overlap |
distance(a: Point, b: Point) | number | Euclidean distance |
distanceSquared(a: Point, b: Point) | number | Squared distance, no square root |
lineIntersects(a1, a2, b1, b2) | boolean | Segment intersection, four Point arguments |
SpatialGrid
Broad-phase bucketing over objects that carry x and y.
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
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
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
| Method | Returns | Description |
|---|---|---|
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?) | void | Play a loaded sound |
playMusic(name) | void | Play a loaded track as music, stopping the current one |
stopMusic() | void | Stop the current music |
setMasterVolume(volume) | void | 0-1, applies to everything |
setSoundVolume(volume) | void | 0-1, sound effects only |
setMusicVolume(volume) | void | 0-1, music only |
mute() | void | Mute. Takes no argument |
unmute() | void | Unmute |
isUnlocked() | boolean | Whether a user gesture has unlocked the audio context. On iOS and Safari nothing plays until this is true |
destroy() | void | Unload sounds and remove listeners |
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
import { ParticleSystem } from "quantum-forge-engine/particles";Constructor
new ParticleSystem(config?: {
maxParticles?: number; // default: 1000
gravity?: number; // default: 0
logger?: LoggerInterface;
})Methods
| Method | Description |
|---|---|
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(): number | Live particle count |
clear() | Remove every particle |
burst, trail, and explode take positional arguments, not an options object.
EmitConfig
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.
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
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.
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);
}| Method | Returns | Description |
|---|---|---|
start() | void | Begin. Resets elapsed time |
update(currentTime) | boolean | Advance to a timestamp. Returns whether still running |
pause() / resume() / stop() | void | Playback control |
isActive() | boolean | Whether the animation is running. There is no isComplete() |
getProgress() | number | 0-1 |
getValue() | number | Current value |
MultiTween
For object-shaped animations, use MultiTween with a values map.
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.
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 completeanimateValue(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
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.
const entities = new EntityManager<Enemy>();
entities.add(createEntity<Enemy>("enemy-1", { x: 10, y: 20, type: "grunt" }));| Method | Returns | Description |
|---|---|---|
add(entity) | void | Add. A duplicate id logs a warning and replaces |
remove(id: string) | boolean | Remove by id, not by object |
get(id) | T | undefined | Look up by id |
has(id) | boolean | Existence check |
getAll() | T[] | Every entity |
getActive() | T[] | Entities whose active is not false |
filter(predicate) | T[] | Filter |
find(predicate) | T | undefined | First match |
forEach(fn) | void | Iterate |
count() | number | How many |
clear() | void | Remove 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 | undefined | Closest entity |
createEntity<T>(id, props) builds { id, ...props }.
State machine
import { StateMachine } from "quantum-forge-engine/state-machine";Config-driven, with a required context object that every hook and guard receives.
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 onUpdateThe constructor calls the initial state's onEnter right away.
| Field | Description |
|---|---|
initial | Starting state name |
context | Required. Shared object passed to every hook, guard, and onTransition |
states | Map of state name to { onEnter?, onExit?, onUpdate? } |
transitions | Array of { from, event, to, guard?, onTransition? }. from accepts one state or an array |
logger | Optional logger |
| Method | Returns | Description |
|---|---|---|
send(event) | boolean | Whether a transition fired |
getState() | TState | Current state name |
getContext() | TContext | The context object |
is(state) | boolean | State check. There is no matches() |
can(event) | boolean | Whether the event would fire a transition now |
getAvailableEvents() | TEvent[] | Events with a passing guard from here |
update(dt) | void | Run the current state's onUpdate |
forceTransition(state) | void | Jump to a state, running exit and enter hooks |
Timer
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.
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();| Method | Returns | Description |
|---|---|---|
after(delay, callback) | TimerHandle | One-shot |
every(interval, callback, limit?) | TimerHandle | Repeating, optionally capped at limit runs |
update(dt) | void | Advance timers by scaled delta time |
pause() / resume() | void | Freeze and unfreeze every timer |
isPaused() | boolean | Paused state |
setTimeScale(scale) | void | Slow motion or fast forward. 1 is normal |
getTimeScale() | number | Current scale |
cancel(handle) | void | Cancel one timer, by handle or id |
cancelAll() | void | Cancel everything |
getActiveCount() | number | Live timer count |
TimerHandle is { cancel(): void }.
Save
import { SaveManager } from "quantum-forge-engine/save";Named slots on a pluggable storage backend, with versioning and migration.
Constructor
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
| Method | Returns | Description |
|---|---|---|
save(slot: string, state) | void | Write a slot |
load(slot: string) | TState | null | Read a slot, migrating if needed |
delete(slot: string) | void | Remove a slot |
has(slot: string) | boolean | Slot exists |
listSlots() | string[] | Every slot name under the prefix |
enableAutoSave(slot, stateProvider, interval?) | void | Auto-save a slot. interval in seconds, default 60 |
disableAutoSave() | void | Stop auto-saving |
update(dt) | void | Drives auto-save. Call each frame |
exportAll() | string | Every slot as JSON, for backup or transfer |
importAll(json) | number | Restore from exportAll output, returns slots written |
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
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
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
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.
| Method | Returns | Description |
|---|---|---|
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) | void | Update the top scene and any running transition |
render() | void | Render the top scene |
isTransitioning() | boolean | Push, pop, and replace are ignored while true |
getTransitionProgress() | number | 0-1 |
getCurrentName() | string | undefined | Name of the top scene |
getStackDepth() | number | Stack 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.