Skip to content

Commit b62663b

Browse files
committed
feat: add quest state machine helpers
1 parent 4ae56b2 commit b62663b

8 files changed

Lines changed: 362 additions & 3 deletions

File tree

PROJECT_DESCRIPTION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ npm run build
3939
| 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` |
4040
| Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` |
4141
| 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` |
42-
| 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` |
42+
| 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` |
4343
| Text & search | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` |
4444
| Data transforms & diffing | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` |
4545
| Graph traversal | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` |
@@ -94,7 +94,7 @@ Consistency between runtime code, documentation, and TypeScript declarations kee
9494
- **Procedural:** 2D/3D Perlin, Worley noise, Wave Function Collapse tile synthesis.
9595
- **Spatial:** Quadtree, AABB helpers, SAT convex polygon collision.
9696
- **Performance utilities:** Debounce, throttle, LRU cache, memoize, request deduplication, virtual scrolling, weighted alias sampling, object pooling, Fisher–Yates shuffle.
97-
- **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.
97+
- **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.
9898
- **Search:** Fuzzy search + scoring, Trie-based autocomplete, binary search, Levenshtein distance.
9999
- **Data tools:** Diff operations (LCS), deep clone, groupBy, JSON diff/patch helpers.
100100
- **Graph:** BFS distance map, DFS traversal, topological sort.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ CDN usage:
2929
| Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` |
3030
| 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` |
3131
| 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` |
32-
| 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` |
32+
| 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` |
3333
| Search & text | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` |
3434
| Data & diff pipelines | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` |
3535
| Graph algorithms | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` |

docs/index.d.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,6 +1695,86 @@ export function updateStatusEffects(
16951695
effects: ActiveStatusEffect[],
16961696
delta: number
16971697
): ActiveStatusEffect[];
1698+
1699+
/**
1700+
* Quest state node definition.
1701+
* Use for: describing quest/dialog states with hooks.
1702+
* Import: gameplay/questMachine.ts
1703+
*/
1704+
export interface QuestStateNode<TContext extends Record<string, unknown>> {
1705+
id: string;
1706+
terminal?: boolean;
1707+
onEnter?: (context: TContext, payload?: unknown) => void;
1708+
onExit?: (context: TContext, payload?: unknown) => void;
1709+
}
1710+
1711+
/**
1712+
* Quest transition definition.
1713+
* Use for: wiring events to state transitions in quests/dialogs.
1714+
* Import: gameplay/questMachine.ts
1715+
*/
1716+
export interface QuestTransition<
1717+
TContext extends Record<string, unknown>,
1718+
TEvent = unknown
1719+
> {
1720+
from: string;
1721+
to: string;
1722+
event: string;
1723+
condition?: (context: TContext, event: TEvent) => boolean;
1724+
action?: (context: TContext, event: TEvent) => void;
1725+
}
1726+
1727+
/**
1728+
* Quest machine configuration options.
1729+
* Use for: instantiating a quest/dialog state machine.
1730+
* Import: gameplay/questMachine.ts
1731+
*/
1732+
export interface QuestMachineOptions<
1733+
TContext extends Record<string, unknown>,
1734+
TEvent = unknown
1735+
> {
1736+
states: ReadonlyArray<QuestStateNode<TContext>>;
1737+
transitions: ReadonlyArray<QuestTransition<TContext, TEvent>>;
1738+
initial: string;
1739+
context: TContext;
1740+
}
1741+
1742+
/**
1743+
* Quest machine snapshot payload.
1744+
* Use for: serialising quest progress.
1745+
* Import: gameplay/questMachine.ts
1746+
*/
1747+
export interface QuestMachineSnapshot<TContext extends Record<string, unknown>> {
1748+
state: string;
1749+
context: TContext;
1750+
}
1751+
1752+
/**
1753+
* Quest machine controller API.
1754+
* Use for: driving quest/dialog progression.
1755+
* Import: gameplay/questMachine.ts
1756+
*/
1757+
export interface QuestMachine<
1758+
TContext extends Record<string, unknown>,
1759+
TEvent = unknown
1760+
> {
1761+
send(event: string, payload?: TEvent): boolean;
1762+
getState(): string;
1763+
getContext(): TContext;
1764+
isCompleted(): boolean;
1765+
reset(snapshot?: QuestMachineSnapshot<TContext>): void;
1766+
toJSON(): QuestMachineSnapshot<TContext>;
1767+
}
1768+
1769+
/**
1770+
* Creates a quest/dialog state machine.
1771+
* Use for: branching dialogue, quest progression, narrative scripting.
1772+
* Import: gameplay/questMachine.ts
1773+
*/
1774+
export function createQuestMachine<
1775+
TContext extends Record<string, unknown>,
1776+
TEvent = unknown
1777+
>(options: QuestMachineOptions<TContext, TEvent>): QuestMachine<TContext, TEvent>;
16981778
/**
16991779
* Item insertion payload used by the inventory controller.
17001780
* Use for: adding items with quantity and metadata.

examples/quest.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { createQuestMachine } from '../src/index.js';
2+
3+
const quest = createQuestMachine({
4+
initial: 'start',
5+
context: { reputation: 0, reward: 0 },
6+
states: [
7+
{ id: 'start' },
8+
{
9+
id: 'accepted',
10+
onEnter: (ctx) => {
11+
ctx.reputation += 5;
12+
console.log('Quest accepted');
13+
},
14+
},
15+
{
16+
id: 'completed',
17+
terminal: true,
18+
onEnter: (ctx) => {
19+
ctx.reward = 150;
20+
console.log('Quest completed!');
21+
},
22+
},
23+
],
24+
transitions: [
25+
{ from: 'start', to: 'accepted', event: 'accept' },
26+
{
27+
from: 'accepted',
28+
to: 'completed',
29+
event: 'turn-in',
30+
condition: (_ctx, payload) => Boolean(payload?.hasItem),
31+
},
32+
],
33+
});
34+
35+
quest.send('accept');
36+
quest.send('turn-in', { hasItem: true });
37+
console.log('Final context:', quest.getContext());

src/gameplay/questMachine.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
export interface QuestStateNode<TContext> {
2+
id: string;
3+
terminal?: boolean;
4+
onEnter?: (context: TContext, payload?: unknown) => void;
5+
onExit?: (context: TContext, payload?: unknown) => void;
6+
}
7+
8+
export interface QuestTransition<TContext, TEvent = unknown> {
9+
from: string;
10+
to: string;
11+
event: string;
12+
condition?: (context: TContext, event: TEvent) => boolean;
13+
action?: (context: TContext, event: TEvent) => void;
14+
}
15+
16+
export interface QuestMachineOptions<
17+
TContext extends Record<string, unknown>,
18+
TEvent = unknown
19+
> {
20+
states: ReadonlyArray<QuestStateNode<TContext>>;
21+
transitions: ReadonlyArray<QuestTransition<TContext, TEvent>>;
22+
initial: string;
23+
context: TContext;
24+
}
25+
26+
export interface QuestMachineSnapshot<TContext> {
27+
state: string;
28+
context: TContext;
29+
}
30+
31+
export interface QuestMachine<
32+
TContext extends Record<string, unknown>,
33+
TEvent = unknown
34+
> {
35+
send(event: string, payload?: TEvent): boolean;
36+
getState(): string;
37+
getContext(): TContext;
38+
isCompleted(): boolean;
39+
reset(snapshot?: QuestMachineSnapshot<TContext>): void;
40+
toJSON(): QuestMachineSnapshot<TContext>;
41+
}
42+
43+
function deepClone<T>(value: T): T {
44+
return JSON.parse(JSON.stringify(value)) as T;
45+
}
46+
47+
export function createQuestMachine<TContext extends Record<string, unknown>, TEvent = unknown>(
48+
options: QuestMachineOptions<TContext, TEvent>
49+
): QuestMachine<TContext, TEvent> {
50+
if (!Array.isArray(options.states) || options.states.length === 0) {
51+
throw new Error('states must contain at least one state.');
52+
}
53+
if (!Array.isArray(options.transitions)) {
54+
throw new Error('transitions must be an array.');
55+
}
56+
57+
const stateMap = new Map<string, QuestStateNode<TContext>>();
58+
const states = options.states as ReadonlyArray<QuestStateNode<TContext>>;
59+
for (const state of states) {
60+
if (stateMap.has(state.id)) {
61+
throw new Error(`Duplicate state id: ${state.id}`);
62+
}
63+
stateMap.set(state.id, state);
64+
}
65+
if (!stateMap.has(options.initial)) {
66+
throw new Error(`Unknown initial state: ${options.initial}`);
67+
}
68+
69+
const transitionsByEvent = new Map<string, QuestTransition<TContext, TEvent>[] >();
70+
const transitions = options.transitions as ReadonlyArray<QuestTransition<TContext, TEvent>>;
71+
for (const transition of transitions) {
72+
if (!stateMap.has(transition.from)) {
73+
throw new Error(`Transition references unknown state: ${transition.from}`);
74+
}
75+
if (!stateMap.has(transition.to)) {
76+
throw new Error(`Transition references unknown state: ${transition.to}`);
77+
}
78+
const list = transitionsByEvent.get(transition.event) ?? [];
79+
list.push(transition);
80+
transitionsByEvent.set(transition.event, list);
81+
}
82+
83+
const initialContext = deepClone(options.context);
84+
let context = deepClone(options.context);
85+
let currentStateId = options.initial;
86+
87+
stateMap.get(currentStateId)?.onEnter?.(context);
88+
89+
function getStateNode(id: string): QuestStateNode<TContext> {
90+
const state = stateMap.get(id);
91+
if (!state) {
92+
throw new Error(`Unknown state: ${id}`);
93+
}
94+
return state;
95+
}
96+
97+
function send(event: string, payload?: TEvent): boolean {
98+
const candidates = transitionsByEvent.get(event);
99+
if (!candidates) {
100+
return false;
101+
}
102+
103+
for (const transition of candidates) {
104+
if (transition.from !== currentStateId) {
105+
continue;
106+
}
107+
if (transition.condition && !transition.condition(context, payload as TEvent)) {
108+
continue;
109+
}
110+
const previousState = getStateNode(currentStateId);
111+
const nextState = getStateNode(transition.to);
112+
113+
previousState.onExit?.(context, payload);
114+
transition.action?.(context, payload as TEvent);
115+
currentStateId = nextState.id;
116+
nextState.onEnter?.(context, payload);
117+
return true;
118+
}
119+
120+
return false;
121+
}
122+
123+
function reset(snapshot?: QuestMachineSnapshot<TContext>): void {
124+
const source = snapshot ? snapshot.context : initialContext;
125+
context = deepClone(source);
126+
const nextStateId = snapshot ? snapshot.state : options.initial;
127+
if (!stateMap.has(nextStateId)) {
128+
throw new Error(`Unknown state in snapshot: ${nextStateId}`);
129+
}
130+
currentStateId = nextStateId;
131+
stateMap.get(currentStateId)?.onEnter?.(context);
132+
}
133+
134+
return {
135+
send,
136+
getState: () => currentStateId,
137+
getContext: () => context,
138+
isCompleted: () => Boolean(stateMap.get(currentStateId)?.terminal),
139+
reset,
140+
toJSON: () => ({ state: currentStateId, context: deepClone(context) }),
141+
};
142+
}

src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export const examples = {
103103
createInventory: 'examples/inventory.ts',
104104
calculateDamage: 'examples/combat.ts',
105105
createCooldownController: 'examples/combat.ts',
106+
createQuestMachine: 'examples/quest.ts',
106107
},
107108
ai: {
108109
seek: 'examples/steering.ts',
@@ -669,6 +670,21 @@ export type {
669670
ActiveStatusEffect,
670671
} from './gameplay/combat.js';
671672

673+
/**
674+
* Quest/dialog state machine utilities.
675+
*
676+
* Example file: examples/quest.ts
677+
*/
678+
export { createQuestMachine } from './gameplay/questMachine.js';
679+
680+
export type {
681+
QuestStateNode,
682+
QuestTransition,
683+
QuestMachineOptions,
684+
QuestMachine,
685+
QuestMachineSnapshot,
686+
} from './gameplay/questMachine.js';
687+
672688

673689
// ============================================================================
674690
// 🔍 SEARCH & STRING UTILITIES

tests/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ describe('package entry point', () => {
114114
| 'createInventory'
115115
| 'calculateDamage'
116116
| 'createCooldownController'
117+
| 'updateStatusEffects'
118+
| 'createQuestMachine'
117119
>();
118120
});
119121
});

0 commit comments

Comments
 (0)