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` | `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` | `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` |
| 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` |
| 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.
- **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.
- **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
4 changes: 2 additions & 2 deletions 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` | `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` | `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` |
| 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` |
| 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 All @@ -53,7 +53,7 @@ npm run size # Enforce bundle size budget
- Milestone 0.2 next targets crowd-flow integrations (RVO + flow fields) and behaviour-tree decorators for richer AI control.
- Milestone 0.4 plans a procedural + gameplay systems toolkit (Wave Function Collapse, dungeon suite, L-systems, game loop, camera, particles, inventory, combat, save/load, and more).

Examples live under `examples/` and can be executed with `tsx`/`ts-node` or compiled for the browser. See `examples/astar.ts`, `examples/flowField.ts`, `examples/navMesh.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`, `examples/steering.ts`, `examples/boids.ts`, `examples/requestDedup.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/search.ts`, `examples/graph.ts`, `examples/geometry.ts`, `examples/bresenham.ts`, `examples/visual.ts`, `examples/sat.ts`, `examples/simplex.ts`, and `examples/worley.ts` for quick starts. The `examples` registry exported from `src/index.ts` provides a typed index you can traverse programmatically.
Examples live under `examples/` and can be executed with `tsx`/`ts-node` or compiled for the browser. See `examples/astar.ts`, `examples/flowField.ts`, `examples/navMesh.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`, `examples/steering.ts`, `examples/boids.ts`, `examples/requestDedup.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/search.ts`, `examples/graph.ts`, `examples/geometry.ts`, `examples/bresenham.ts`, `examples/visual.ts`, `examples/sat.ts`, `examples/simplex.ts`, and `examples/worley.ts` for quick starts. The `examples` registry exported from `src/index.ts` provides a typed index you can traverse programmatically.

## Contributing
1. Fork the repository.
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
- [x] Tile map renderer helpers (chunking, layering, collision tags)
- [x] Shadowcasting field-of-view utilities and minimap helpers
- **Systems for gameplay loops**
- [ ] Inventory system primitives (stacking, filtering, persistence hooks)
- [x] Inventory system primitives (stacking, filtering, persistence hooks)
- [ ] Combat resolution helpers (cooldowns, damage formulas, status effects)
- [ ] Quest/dialog state machine utilities
- [ ] 2D lighting helpers (falloff, blending stubs)
Expand Down
74 changes: 74 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,80 @@ export function transparentFromTileMap(
passable: (tileId: number) => boolean
): (x: number, y: number) => boolean;

/**
* Inventory item representation.
* Use for: describing stackable items and metadata.
* Import: gameplay/inventory.ts
*/
export interface InventoryItem<TMeta = unknown> {
id: string;
quantity: number;
metadata?: TMeta;
}

/**
* Inventory slot container.
* Use for: iterating over slots and applying UI bindings.
* Import: gameplay/inventory.ts
*/
export interface InventorySlot<TMeta = unknown> {
item: InventoryItem<TMeta> | null;
}

/**
* Inventory configuration options.
* Use for: defining slot capacity, max stack size, and item filters.
* Import: gameplay/inventory.ts
*/
export interface InventoryOptions<TMeta = unknown> {
slots: number;
maxStack?: number;
filter?: (item: InventoryItem<TMeta>) => boolean;
}

/**
* Inventory snapshot serialisation.
* Use for: saving/loading inventory state.
* Import: gameplay/inventory.ts
*/
export interface InventorySnapshot<TMeta = unknown> {
slots: Array<InventoryItem<TMeta> | null>;
}

/**
* Inventory controller API.
* Use for: adding/removing items, filtering, and serialising state.
* Import: gameplay/inventory.ts
*/
export interface InventoryController<TMeta = unknown> {
addItem(item: AddItemOptions<TMeta>): number;
removeItem(id: string, quantity: number): number;
getTotalQuantity(id: string): number;
getSlots(): ReadonlyArray<InventorySlot<TMeta>>;
clear(): void;
filter(predicate: (item: InventoryItem<TMeta>) => boolean): InventoryItem<TMeta>[];
toJSON(): InventorySnapshot<TMeta>;
load(snapshot: InventorySnapshot<TMeta>): void;
}

/**
* Creates a stack-based inventory.
* Use for: RPG inventories, loot systems, crafting requirements.
* Import: gameplay/inventory.ts
*/
export function createInventory<TMeta>(options: InventoryOptions<TMeta>): InventoryController<TMeta>;

/**
* Item insertion payload used by the inventory controller.
* Use for: adding items with quantity and metadata.
* Import: gameplay/inventory.ts
*/
export interface AddItemOptions<TMeta = unknown> {
id: string;
quantity: number;
metadata?: TMeta;
}

/**
* Least recently used cache.
* Use for: memoizing responses, data loaders, pagination caches.
Expand Down
13 changes: 13 additions & 0 deletions examples/inventory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createInventory } from '../src/index.js';

const inventory = createInventory<{ rarity: string }>({
slots: 4,
maxStack: 5,
filter: (item) => item.metadata?.rarity !== 'cursed',
});

inventory.addItem({ id: 'potion', quantity: 3, metadata: { rarity: 'common' } });
inventory.addItem({ id: 'potion', quantity: 4, metadata: { rarity: 'common' } });
inventory.addItem({ id: 'elixir', quantity: 2, metadata: { rarity: 'rare' } });

console.log('inventory:', inventory.toJSON());
211 changes: 211 additions & 0 deletions src/gameplay/inventory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
export interface InventoryItem<TMeta = unknown> {
id: string;
quantity: number;
metadata?: TMeta;
}

export interface InventorySlot<TMeta = unknown> {
item: InventoryItem<TMeta> | null;
}

export interface InventoryOptions<TMeta = unknown> {
slots: number;
maxStack?: number;
filter?: (item: InventoryItem<TMeta>) => boolean;
}

export interface AddItemOptions<TMeta = unknown> {
id: string;
quantity: number;
metadata?: TMeta;
}

export interface InventorySnapshot<TMeta = unknown> {
slots: Array<InventoryItem<TMeta> | null>;
}

export interface InventoryController<TMeta = unknown> {
addItem(item: AddItemOptions<TMeta>): number;
removeItem(id: string, quantity: number): number;
getTotalQuantity(id: string): number;
getSlots(): ReadonlyArray<InventorySlot<TMeta>>;
clear(): void;
filter(predicate: (item: InventoryItem<TMeta>) => boolean): InventoryItem<TMeta>[];
toJSON(): InventorySnapshot<TMeta>;
load(snapshot: InventorySnapshot<TMeta>): void;
}

function assertPositiveInt(value: number, label: string): void {
if (!Number.isInteger(value) || value <= 0) {
throw new Error(`${label} must be a positive integer.`);
}
}

function normalizeOptions<TMeta>(options: InventoryOptions<TMeta>): Required<InventoryOptions<TMeta>> {
assertPositiveInt(options.slots, 'slots');
const maxStack = options.maxStack ?? Number.POSITIVE_INFINITY;
if (maxStack <= 0) {
throw new Error('maxStack must be greater than 0.');
}
const filter = options.filter ?? (() => true);
return {
slots: options.slots,
maxStack,
filter,
};
}

function createEmptySlots<TMeta>(count: number): InventorySlot<TMeta>[] {
return Array.from({ length: count }, () => ({ item: null }));
}

function cloneItem<TMeta>(item: InventoryItem<TMeta>): InventoryItem<TMeta> {
return {
id: item.id,
quantity: item.quantity,
metadata: item.metadata,
};
}

/**
* Creates a stack-based inventory with optional filtering and serialization helpers.
* Useful for: RPG inventories, loot systems, and crafting components.
*/
export function createInventory<TMeta>(options: InventoryOptions<TMeta>): InventoryController<TMeta> {
const config = normalizeOptions(options);
const slots = createEmptySlots<TMeta>(config.slots);

function addItem(item: AddItemOptions<TMeta>): number {
if (!Number.isInteger(item.quantity) || item.quantity <= 0) {
throw new Error('quantity must be a positive integer.');
}
if (!config.filter({ id: item.id, quantity: item.quantity, metadata: item.metadata })) {
return item.quantity;
}

let remaining = item.quantity;

for (const slot of slots) {
if (!slot.item) {
continue;
}
if (slot.item.id !== item.id) {
continue;
}
const available = config.maxStack - slot.item.quantity;
if (available <= 0) {
continue;
}
const toTransfer = Math.min(available, remaining);
slot.item.quantity += toTransfer;
remaining -= toTransfer;
if (remaining === 0) {
return 0;
}
}

for (const slot of slots) {
if (slot.item !== null) {
continue;
}
const toTransfer = Math.min(config.maxStack, remaining);
slot.item = {
id: item.id,
quantity: toTransfer,
metadata: item.metadata,
};
remaining -= toTransfer;
if (remaining === 0) {
break;
}
}

return remaining;
}

function removeItem(id: string, quantity: number): number {
if (!Number.isInteger(quantity) || quantity <= 0) {
throw new Error('quantity must be a positive integer.');
}

let remaining = quantity;
for (const slot of slots) {
if (!slot.item || slot.item.id !== id) {
continue;
}
const toRemove = Math.min(slot.item.quantity, remaining);
slot.item.quantity -= toRemove;
remaining -= toRemove;
if (slot.item.quantity === 0) {
slot.item = null;
}
if (remaining === 0) {
break;
}
}
return quantity - remaining;
}

function getTotalQuantity(id: string): number {
let total = 0;
for (const slot of slots) {
if (slot.item && slot.item.id === id) {
total += slot.item.quantity;
}
}
return total;
}

function getSlots(): ReadonlyArray<InventorySlot<TMeta>> {
return slots;
}

function clear(): void {
for (const slot of slots) {
slot.item = null;
}
}

function filter(predicate: (item: InventoryItem<TMeta>) => boolean): InventoryItem<TMeta>[] {
const results: InventoryItem<TMeta>[] = [];
for (const slot of slots) {
if (slot.item && predicate(slot.item)) {
results.push(cloneItem(slot.item));
}
}
return results;
}

function toJSON(): InventorySnapshot<TMeta> {
return {
slots: slots.map((slot) => (slot.item ? cloneItem(slot.item) : null)),
};
}

function load(snapshot: InventorySnapshot<TMeta>): void {
if (!Array.isArray(snapshot.slots) || snapshot.slots.length !== config.slots) {
throw new Error('snapshot slots length mismatch.');
}
snapshot.slots.forEach((item, index) => {
if (item) {
if (!Number.isInteger(item.quantity) || item.quantity < 0) {
throw new Error('snapshot item quantity must be a non-negative integer.');
}
slots[index].item = cloneItem(item);
} else {
slots[index].item = null;
}
});
}

return {
addItem,
removeItem,
getTotalQuantity,
getSlots,
clear,
filter,
toJSON,
load,
};
}
Loading