Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions PROJECT_DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ npm run build
| Procedural textures & terrain | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon`, `generateRecursiveMaze`, `generatePrimMaze`, `generateKruskalMaze`, `generateWilsonMaze`, `generateAldousBroderMaze`, `generateRecursiveDivisionMaze` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts`, `examples/waveFunctionCollapse.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts`, `examples/mazeRecursive.ts`, `examples/mazePrim.ts`, `examples/mazeKruskal.ts`, `examples/mazeWilson.ts`, `examples/mazeAldous.ts`, `examples/mazeDivision.ts` |
| Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` |
| Web performance & UI throttling | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler`, `createObjectPool`, `fisherYatesShuffle` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts`, `examples/fisherYates.ts` |
| Gameplay systems | `createDeltaTimeManager`, `createFixedTimestepLoop`, `createCamera2D`, `createParticleSystem`, `createSpriteAnimation`, `createTweenSystem`, `createPlatformerController`, `createTopDownController`, `createTileMapController`, `computeFieldOfView`, `createInventory` | `util/deltaTime.ts`, `util/fixedTimestep.ts`, `gameplay/camera2D.ts`, `gameplay/particleSystem.ts`, `gameplay/spriteAnimation.ts`, `gameplay/tween.ts`, `gameplay/platformerPhysics.ts`, `gameplay/topDownMovement.ts`, `gameplay/tileMap.ts`, `gameplay/shadowcasting.ts`, `gameplay/inventory.ts` | `examples/deltaTime.ts`, `examples/fixedTimestep.ts`, `examples/camera2D.ts`, `examples/particleSystem.ts`, `examples/spriteAnimation.ts`, `examples/tween.ts`, `examples/platformerPhysics.ts`, `examples/topDownMovement.ts`, `examples/tileMap.ts`, `examples/shadowcasting.ts`, `examples/inventory.ts` |
| Gameplay systems | `createDeltaTimeManager`, `createFixedTimestepLoop`, `createCamera2D`, `createParticleSystem`, `createSpriteAnimation`, `createTweenSystem`, `createPlatformerController`, `createTopDownController`, `createTileMapController`, `computeFieldOfView`, `createInventory`, `calculateDamage`, `createCooldownController`, `updateStatusEffects`, `createQuestMachine` | `util/deltaTime.ts`, `util/fixedTimestep.ts`, `gameplay/camera2D.ts`, `gameplay/particleSystem.ts`, `gameplay/spriteAnimation.ts`, `gameplay/tween.ts`, `gameplay/platformerPhysics.ts`, `gameplay/topDownMovement.ts`, `gameplay/tileMap.ts`, `gameplay/shadowcasting.ts`, `gameplay/inventory.ts`, `gameplay/combat.ts`, `gameplay/questMachine.ts` | `examples/deltaTime.ts`, `examples/fixedTimestep.ts`, `examples/camera2D.ts`, `examples/particleSystem.ts`, `examples/spriteAnimation.ts`, `examples/tween.ts`, `examples/platformerPhysics.ts`, `examples/topDownMovement.ts`, `examples/tileMap.ts`, `examples/shadowcasting.ts`, `examples/inventory.ts`, `examples/combat.ts`, `examples/quest.ts` |
| Text & search | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` |
| Data transforms & diffing | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` |
| Graph traversal | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` |
Expand Down Expand Up @@ -94,7 +94,7 @@ Consistency between runtime code, documentation, and TypeScript declarations kee
- **Procedural:** 2D/3D Perlin, Worley noise, Wave Function Collapse tile synthesis.
- **Spatial:** Quadtree, AABB helpers, SAT convex polygon collision.
- **Performance utilities:** Debounce, throttle, LRU cache, memoize, request deduplication, virtual scrolling, weighted alias sampling, object pooling, Fisher–Yates shuffle.
- **Gameplay systems:** Delta-time manager, fixed timestep loop, 2D camera with smoothing and shake, particle system with configurable emitters, sprite animation controller with frame events, tween system with easing and repeats, platformer physics helper with coyote time and jump buffering, top-down movement controller with acceleration and drag, tile map renderer with chunking and collision tags, shadowcasting FOV utility, inventory system primitives.
- **Gameplay systems:** Delta-time manager, fixed timestep loop, 2D camera with smoothing and shake, particle system with configurable emitters, sprite animation controller with frame events, tween system with easing and repeats, platformer physics helper with coyote time and jump buffering, top-down movement controller with acceleration and drag, tile map renderer with chunking and collision tags, shadowcasting FOV utility, inventory system primitives, combat helpers (damage/cooldowns/status), quest/dialog state machine.
- **Search:** Fuzzy search + scoring, Trie-based autocomplete, binary search, Levenshtein distance.
- **Data tools:** Diff operations (LCS), deep clone, groupBy, JSON diff/patch helpers.
- **Graph:** BFS distance map, DFS traversal, topological sort.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ CDN usage:
| Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` |
| AI behaviours & crowds | `seek`, `flee`, `arrive`, `pursue`, `wander`, `updateBoids`, `BehaviorTree`, `rvoStep` | `ai/steering.ts`, `ai/boids.ts`, `ai/behaviorTree.ts`, `ai/rvo.ts` | `examples/steering.ts`, `examples/boids.ts`, `examples/rvo.ts` |
| Web performance & UI | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler`, `createObjectPool`, `fisherYatesShuffle` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts`, `examples/fisherYates.ts` |
| Gameplay systems | `createDeltaTimeManager`, `createFixedTimestepLoop`, `createCamera2D`, `createParticleSystem`, `createSpriteAnimation`, `createTweenSystem`, `createPlatformerController`, `createTopDownController`, `createTileMapController`, `computeFieldOfView`, `createInventory` | `util/deltaTime.ts`, `util/fixedTimestep.ts`, `gameplay/camera2D.ts`, `gameplay/particleSystem.ts`, `gameplay/spriteAnimation.ts`, `gameplay/tween.ts`, `gameplay/platformerPhysics.ts`, `gameplay/topDownMovement.ts`, `gameplay/tileMap.ts`, `gameplay/shadowcasting.ts`, `gameplay/inventory.ts` | `examples/deltaTime.ts`, `examples/fixedTimestep.ts`, `examples/camera2D.ts`, `examples/particleSystem.ts`, `examples/spriteAnimation.ts`, `examples/tween.ts`, `examples/platformerPhysics.ts`, `examples/topDownMovement.ts`, `examples/tileMap.ts`, `examples/shadowcasting.ts`, `examples/inventory.ts` |
| Gameplay systems | `createDeltaTimeManager`, `createFixedTimestepLoop`, `createCamera2D`, `createParticleSystem`, `createSpriteAnimation`, `createTweenSystem`, `createPlatformerController`, `createTopDownController`, `createTileMapController`, `computeFieldOfView`, `createInventory`, `calculateDamage`, `createCooldownController`, `updateStatusEffects`, `createQuestMachine` | `util/deltaTime.ts`, `util/fixedTimestep.ts`, `gameplay/camera2D.ts`, `gameplay/particleSystem.ts`, `gameplay/spriteAnimation.ts`, `gameplay/tween.ts`, `gameplay/platformerPhysics.ts`, `gameplay/topDownMovement.ts`, `gameplay/tileMap.ts`, `gameplay/shadowcasting.ts`, `gameplay/inventory.ts`, `gameplay/combat.ts`, `gameplay/questMachine.ts` | `examples/deltaTime.ts`, `examples/fixedTimestep.ts`, `examples/camera2D.ts`, `examples/particleSystem.ts`, `examples/spriteAnimation.ts`, `examples/tween.ts`, `examples/platformerPhysics.ts`, `examples/topDownMovement.ts`, `examples/tileMap.ts`, `examples/shadowcasting.ts`, `examples/inventory.ts`, `examples/combat.ts`, `examples/quest.ts` |
| Search & text | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` |
| Data & diff pipelines | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` |
| Graph algorithms | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` |
Expand Down
80 changes: 80 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,86 @@ export function updateStatusEffects(
effects: ActiveStatusEffect[],
delta: number
): ActiveStatusEffect[];

/**
* Quest state node definition.
* Use for: describing quest/dialog states with hooks.
* Import: gameplay/questMachine.ts
*/
export interface QuestStateNode<TContext extends Record<string, unknown>> {
id: string;
terminal?: boolean;
onEnter?: (context: TContext, payload?: unknown) => void;
onExit?: (context: TContext, payload?: unknown) => void;
}

/**
* Quest transition definition.
* Use for: wiring events to state transitions in quests/dialogs.
* Import: gameplay/questMachine.ts
*/
export interface QuestTransition<
TContext extends Record<string, unknown>,
TEvent = unknown
> {
from: string;
to: string;
event: string;
condition?: (context: TContext, event: TEvent) => boolean;
action?: (context: TContext, event: TEvent) => void;
}

/**
* Quest machine configuration options.
* Use for: instantiating a quest/dialog state machine.
* Import: gameplay/questMachine.ts
*/
export interface QuestMachineOptions<
TContext extends Record<string, unknown>,
TEvent = unknown
> {
states: ReadonlyArray<QuestStateNode<TContext>>;
transitions: ReadonlyArray<QuestTransition<TContext, TEvent>>;
initial: string;
context: TContext;
}

/**
* Quest machine snapshot payload.
* Use for: serialising quest progress.
* Import: gameplay/questMachine.ts
*/
export interface QuestMachineSnapshot<TContext extends Record<string, unknown>> {
state: string;
context: TContext;
}

/**
* Quest machine controller API.
* Use for: driving quest/dialog progression.
* Import: gameplay/questMachine.ts
*/
export interface QuestMachine<
TContext extends Record<string, unknown>,
TEvent = unknown
> {
send(event: string, payload?: TEvent): boolean;
getState(): string;
getContext(): TContext;
isCompleted(): boolean;
reset(snapshot?: QuestMachineSnapshot<TContext>): void;
toJSON(): QuestMachineSnapshot<TContext>;
}

/**
* Creates a quest/dialog state machine.
* Use for: branching dialogue, quest progression, narrative scripting.
* Import: gameplay/questMachine.ts
*/
export function createQuestMachine<
TContext extends Record<string, unknown>,
TEvent = unknown
>(options: QuestMachineOptions<TContext, TEvent>): QuestMachine<TContext, TEvent>;
/**
* Item insertion payload used by the inventory controller.
* Use for: adding items with quantity and metadata.
Expand Down
37 changes: 37 additions & 0 deletions examples/quest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createQuestMachine } from '../src/index.js';

const quest = createQuestMachine<{ reputation: number; reward: number }, { hasItem?: boolean }>({
initial: 'start',
context: { reputation: 0, reward: 0 },
states: [
{ id: 'start' },
{
id: 'accepted',
onEnter: (ctx) => {
ctx.reputation += 5;
console.log('Quest accepted');
},
},
{
id: 'completed',
terminal: true,
onEnter: (ctx) => {
ctx.reward = 150;
console.log('Quest completed!');
},
},
],
transitions: [
{ from: 'start', to: 'accepted', event: 'accept' },
{
from: 'accepted',
to: 'completed',
event: 'turn-in',
condition: (_ctx, payload) => Boolean(payload?.hasItem),
},
],
});

quest.send('accept');
quest.send('turn-in', { hasItem: true });
console.log('Final context:', quest.getContext());
142 changes: 142 additions & 0 deletions src/gameplay/questMachine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
export interface QuestStateNode<TContext> {
id: string;
terminal?: boolean;
onEnter?: (context: TContext, payload?: unknown) => void;
onExit?: (context: TContext, payload?: unknown) => void;
}

export interface QuestTransition<TContext, TEvent = unknown> {
from: string;
to: string;
event: string;
condition?: (context: TContext, event: TEvent) => boolean;
action?: (context: TContext, event: TEvent) => void;
}

export interface QuestMachineOptions<
TContext extends Record<string, unknown>,
TEvent = unknown
> {
states: ReadonlyArray<QuestStateNode<TContext>>;
transitions: ReadonlyArray<QuestTransition<TContext, TEvent>>;
initial: string;
context: TContext;
}

export interface QuestMachineSnapshot<TContext> {
state: string;
context: TContext;
}

export interface QuestMachine<
TContext extends Record<string, unknown>,
TEvent = unknown
> {
send(event: string, payload?: TEvent): boolean;
getState(): string;
getContext(): TContext;
isCompleted(): boolean;
reset(snapshot?: QuestMachineSnapshot<TContext>): void;
toJSON(): QuestMachineSnapshot<TContext>;
}

function deepClone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}

export function createQuestMachine<TContext extends Record<string, unknown>, TEvent = unknown>(
options: QuestMachineOptions<TContext, TEvent>
): QuestMachine<TContext, TEvent> {
if (!Array.isArray(options.states) || options.states.length === 0) {
throw new Error('states must contain at least one state.');
}
if (!Array.isArray(options.transitions)) {
throw new Error('transitions must be an array.');
}

const stateMap = new Map<string, QuestStateNode<TContext>>();
const states = options.states as ReadonlyArray<QuestStateNode<TContext>>;
for (const state of states) {
if (stateMap.has(state.id)) {
throw new Error(`Duplicate state id: ${state.id}`);
}
stateMap.set(state.id, state);
}
if (!stateMap.has(options.initial)) {
throw new Error(`Unknown initial state: ${options.initial}`);
}

const transitionsByEvent = new Map<string, QuestTransition<TContext, TEvent>[] >();
const transitions = options.transitions as ReadonlyArray<QuestTransition<TContext, TEvent>>;
for (const transition of transitions) {
if (!stateMap.has(transition.from)) {
throw new Error(`Transition references unknown state: ${transition.from}`);
}
if (!stateMap.has(transition.to)) {
throw new Error(`Transition references unknown state: ${transition.to}`);
}
const list = transitionsByEvent.get(transition.event) ?? [];
list.push(transition);
transitionsByEvent.set(transition.event, list);
}

const initialContext = deepClone(options.context);
let context = deepClone(options.context);
let currentStateId = options.initial;

stateMap.get(currentStateId)?.onEnter?.(context);

function getStateNode(id: string): QuestStateNode<TContext> {
const state = stateMap.get(id);
if (!state) {
throw new Error(`Unknown state: ${id}`);
}
return state;
}

function send(event: string, payload?: TEvent): boolean {
const candidates = transitionsByEvent.get(event);
if (!candidates) {
return false;
}

for (const transition of candidates) {
if (transition.from !== currentStateId) {
continue;
}
if (transition.condition && !transition.condition(context, payload as TEvent)) {
continue;
}
const previousState = getStateNode(currentStateId);
const nextState = getStateNode(transition.to);

previousState.onExit?.(context, payload);
transition.action?.(context, payload as TEvent);
currentStateId = nextState.id;
nextState.onEnter?.(context, payload);
return true;
}

return false;
}

function reset(snapshot?: QuestMachineSnapshot<TContext>): void {
const source = snapshot ? snapshot.context : initialContext;
context = deepClone(source);
const nextStateId = snapshot ? snapshot.state : options.initial;
if (!stateMap.has(nextStateId)) {
throw new Error(`Unknown state in snapshot: ${nextStateId}`);
}
currentStateId = nextStateId;
stateMap.get(currentStateId)?.onEnter?.(context);
}

return {
send,
getState: () => currentStateId,
getContext: () => context,
isCompleted: () => Boolean(stateMap.get(currentStateId)?.terminal),
reset,
toJSON: () => ({ state: currentStateId, context: deepClone(context) }),
};
}
16 changes: 16 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const examples = {
createInventory: 'examples/inventory.ts',
calculateDamage: 'examples/combat.ts',
createCooldownController: 'examples/combat.ts',
createQuestMachine: 'examples/quest.ts',
},
ai: {
seek: 'examples/steering.ts',
Expand Down Expand Up @@ -669,6 +670,21 @@ export type {
ActiveStatusEffect,
} from './gameplay/combat.js';

/**
* Quest/dialog state machine utilities.
*
* Example file: examples/quest.ts
*/
export { createQuestMachine } from './gameplay/questMachine.js';

export type {
QuestStateNode,
QuestTransition,
QuestMachineOptions,
QuestMachine,
QuestMachineSnapshot,
} from './gameplay/questMachine.js';


// ============================================================================
// 🔍 SEARCH & STRING UTILITIES
Expand Down
1 change: 1 addition & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ describe('package entry point', () => {
| 'createInventory'
| 'calculateDamage'
| 'createCooldownController'
| 'createQuestMachine'
>();
});
});
Loading