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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/inventory.ts`, `examples/combat.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/combat.ts`, `examples/quest.ts`, `examples/lighting.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
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@
- **Systems for gameplay loops**
- [x] Inventory system primitives (stacking, filtering, persistence hooks)
- [x] Combat resolution helpers (cooldowns, damage formulas, status effects)
- [ ] Quest/dialog state machine utilities
- [ ] 2D lighting helpers (falloff, blending stubs)
- [x] Quest/dialog state machine utilities
- [x] 2D lighting helpers (falloff, blending stubs)
- [ ] Wave spawner utilities for encounter pacing
- [ ] Sound manager stubs (channel limiting, priority)
- [ ] Input manager abstraction (keyboard/mouse/pad remapping)
Expand Down
62 changes: 62 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1775,6 +1775,68 @@ export function createQuestMachine<
TContext extends Record<string, unknown>,
TEvent = unknown
>(options: QuestMachineOptions<TContext, TEvent>): QuestMachine<TContext, TEvent>;

/**
* Lighting falloff mode identifiers.
* Use for: controlling light intensity attenuation.
* Import: gameplay/lighting.ts
*/
export type FalloffMode = 'linear' | 'quadratic' | 'smoothstep';

/**
* Point light definition for lighting grids.
* Use for: positioning lights with radius and color.
* Import: gameplay/lighting.ts
*/
export interface PointLight {
x: number;
y: number;
radius: number;
intensity?: number;
falloff?: FalloffMode;
color?: [number, number, number];
}

/**
* Lighting grid configuration.
* Use for: computing lightmaps for tile-based scenes.
* Import: gameplay/lighting.ts
*/
export interface LightingGridOptions {
width: number;
height: number;
tileSize: number;
ambient?: number;
lights: ReadonlyArray<PointLight>;
obstacles?: (x: number, y: number) => boolean;
}

/**
* Lighting cell output containing intensity and blended color.
* Import: gameplay/lighting.ts
*/
export interface LightingCell {
light: number;
color: [number, number, number];
}

/**
* Lighting grid computation result.
* Import: gameplay/lighting.ts
*/
export interface LightingGridResult {
width: number;
height: number;
cells: LightingCell[];
}

/**
* Computes a lighting grid with point lights and ambient light.
* Use for: tile map lighting, fog-of-war, and shading overlays.
* Performance: O(width × height × lights).
* Import: gameplay/lighting.ts
*/
export function computeLightingGrid(options: LightingGridOptions): LightingGridResult;
/**
* Item insertion payload used by the inventory controller.
* Use for: adding items with quantity and metadata.
Expand Down
14 changes: 14 additions & 0 deletions examples/lighting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { computeLightingGrid } from '../src/index.js';

const lighting = computeLightingGrid({
width: 5,
height: 5,
tileSize: 16,
ambient: 0.2,
lights: [
{ x: 40, y: 40, radius: 64, intensity: 1, color: [1, 0.9, 0.7] },
{ x: 80, y: 16, radius: 48, intensity: 0.8, color: [0.6, 0.8, 1] },
],
});

console.log(lighting);
127 changes: 127 additions & 0 deletions src/gameplay/lighting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
export type FalloffMode = 'linear' | 'quadratic' | 'smoothstep';

export interface PointLight {
x: number;
y: number;
radius: number;
intensity?: number;
falloff?: FalloffMode;
color?: [number, number, number];
}

export interface LightingGridOptions {
width: number;
height: number;
tileSize: number;
ambient?: number;
lights: ReadonlyArray<PointLight>;
obstacles?: (x: number, y: number) => boolean;
}

export interface LightingCell {
light: number;
color: [number, number, number];
}

export interface LightingGridResult {
width: number;
height: number;
cells: LightingCell[];
}

function assertPositive(value: number, label: string): void {
if (typeof value !== 'number' || Number.isNaN(value) || !Number.isFinite(value) || value <= 0) {
throw new Error(`${label} must be a positive finite number.`);
}
}

function clamp(value: number, min: number, max: number): number {
if (value < min) return min;
if (value > max) return max;
return value;
}

function falloff(distance: number, radius: number, mode: FalloffMode): number {
const t = clamp(distance / radius, 0, 1);
switch (mode) {
case 'linear':
return 1 - t;
case 'quadratic':
return 1 - t * t;
case 'smoothstep':
return 1 - (t * t * (3 - 2 * t));
default:
return 1 - t;
}
}

function blendColor(base: [number, number, number], add: [number, number, number], weight: number): [number, number, number] {
return [
clamp(base[0] + add[0] * weight, 0, 1),
clamp(base[1] + add[1] * weight, 0, 1),
clamp(base[2] + add[2] * weight, 0, 1),
];
}

function defaultColor(): [number, number, number] {
return [0, 0, 0];
}

/**
* Calculates a lighting grid for 2D tile maps with simple falloff.
* Useful for: top-down games, roguelike rendering, and fog-of-war overlays.
*/
export function computeLightingGrid(options: LightingGridOptions): LightingGridResult {
if (!Array.isArray(options.lights) || options.lights.length === 0) {
throw new Error('lights must contain at least one point light.');
}
assertPositive(options.width, 'width');
assertPositive(options.height, 'height');
assertPositive(options.tileSize, 'tileSize');

const ambient = clamp(options.ambient ?? 0.1, 0, 1);
const obstacles = options.obstacles ?? (() => false);

const cells: LightingCell[] = [];

const lights = options.lights as ReadonlyArray<PointLight>;

for (let y = 0; y < options.height; y += 1) {
for (let x = 0; x < options.width; x += 1) {
let light = ambient;
let color: [number, number, number] = defaultColor();
color = blendColor(color, [ambient, ambient, ambient], 1);

if (obstacles(x, y)) {
cells.push({ light, color });
continue;
}

const worldX = (x + 0.5) * options.tileSize;
const worldY = (y + 0.5) * options.tileSize;

for (const point of lights) {
assertPositive(point.radius, 'light.radius');
const dx = worldX - point.x;
const dy = worldY - point.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > point.radius) {
continue;
}
const intensity = clamp(point.intensity ?? 1, 0, 10);
const mode = point.falloff ?? 'smoothstep';
const percent = falloff(distance, point.radius, mode) * intensity;
light = clamp(light + percent, 0, 1);
color = blendColor(color, point.color ?? [1, 0.95, 0.8], percent);
}

cells.push({ light, color });
}
}

return {
width: options.width,
height: options.height,
cells,
};
}
16 changes: 16 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const examples = {
calculateDamage: 'examples/combat.ts',
createCooldownController: 'examples/combat.ts',
createQuestMachine: 'examples/quest.ts',
computeLightingGrid: 'examples/lighting.ts',
},
ai: {
seek: 'examples/steering.ts',
Expand Down Expand Up @@ -685,6 +686,21 @@ export type {
QuestMachineSnapshot,
} from './gameplay/questMachine.js';

/**
* 2D lighting helpers for tile maps.
*
* Example file: examples/lighting.ts
*/
export { computeLightingGrid } from './gameplay/lighting.js';

export type {
LightingGridOptions,
LightingGridResult,
LightingCell,
PointLight,
FalloffMode,
} from './gameplay/lighting.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 @@ -115,6 +115,7 @@ describe('package entry point', () => {
| 'calculateDamage'
| 'createCooldownController'
| 'createQuestMachine'
| 'computeLightingGrid'
>();
});
});
40 changes: 40 additions & 0 deletions tests/lighting.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest';

import { computeLightingGrid } from '../src/index.js';

describe('computeLightingGrid', () => {
it('applies falloff and ambient light', () => {
const result = computeLightingGrid({
width: 3,
height: 3,
tileSize: 1,
ambient: 0.1,
lights: [
{ x: 1.5, y: 1.5, radius: 2, intensity: 1, falloff: 'linear', color: [1, 0.8, 0.6] },
],
});

expect(result.cells).toHaveLength(9);
const center = result.cells[4];
expect(center.light).toBeGreaterThan(0.9);
const corner = result.cells[0];
expect(corner.light).toBeGreaterThan(0.1);
expect(corner.light).toBeLessThan(center.light);
});

it('respects obstacles', () => {
const result = computeLightingGrid({
width: 3,
height: 3,
tileSize: 1,
ambient: 0,
lights: [{ x: 0.5, y: 0.5, radius: 3 }],
obstacles: (x, y) => x === 1 && y === 0,
});

const blocked = result.cells[1];
expect(blocked.light).toBe(0);
const next = result.cells[2];
expect(next.light).toBeGreaterThan(0);
});
});
2 changes: 1 addition & 1 deletion tests/questMachine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('createQuestMachine', () => {
it('advances states based on events and conditions', () => {
const context = { reputation: 0, reward: 0 };

const machine = createQuestMachine<typeof context, { hasItem?: boolean }>({
const machine = createQuestMachine<{ reputation: number; reward: number }, { hasItem?: boolean }>({
context,
initial: 'start',
states: [
Expand Down