docs(modules): 添加框架模块文档 (#350)

* docs(modules): 添加框架模块文档

添加以下模块的完整文档:
- FSM (状态机): 状态定义、转换条件、优先级、事件监听
- Timer (定时器): 定时器调度、冷却系统、服务令牌
- Spatial (空间索引): GridSpatialIndex、AOI 兴趣区域管理
- Pathfinding (寻路): A* 算法、网格地图、导航网格、路径平滑
- Procgen (程序化生成): 噪声函数、种子随机数、加权随机

所有文档均基于实际源码 API 编写,包含:
- 快速开始示例
- 完整 API 参考
- 实际使用案例
- 蓝图节点说明
- 最佳实践建议

* docs(modules): 添加 Blueprint 模块文档和所有模块英文版

新增中文文档:
- Blueprint (蓝图可视化脚本): VM、自定义节点、组合系统、触发器

新增英文文档 (docs/en/modules/):
- FSM: State machine API, transitions, ECS integration
- Timer: Timers, cooldowns, service tokens
- Spatial: Grid spatial index, AOI management
- Pathfinding: A*, grid map, NavMesh, path smoothing
- Procgen: Noise functions, seeded random, weighted random
- Blueprint: Visual scripting, custom nodes, composition

所有文档均基于实际源码 API 编写。
This commit is contained in:
YHH
2025-12-26 20:02:21 +08:00
committed by GitHub
parent 76691cc198
commit 4a16e30794
12 changed files with 5071 additions and 0 deletions

View File

@@ -0,0 +1,404 @@
# Blueprint Visual Scripting
`@esengine/blueprint` provides a full-featured visual scripting system supporting node-based programming, event-driven execution, and blueprint composition.
## Installation
```bash
npm install @esengine/blueprint
```
## Quick Start
```typescript
import {
createBlueprintSystem,
createBlueprintComponentData,
NodeRegistry,
RegisterNode
} from '@esengine/blueprint';
// Create blueprint system
const blueprintSystem = createBlueprintSystem(scene);
// Load blueprint asset
const blueprint = await loadBlueprintAsset('player.bp');
// Create blueprint component data
const componentData = createBlueprintComponentData();
componentData.blueprintAsset = blueprint;
// Update in game loop
function gameLoop(dt: number) {
blueprintSystem.process(entities, dt);
}
```
## Core Concepts
### Blueprint Asset Structure
Blueprints are saved as `.bp` files:
```typescript
interface BlueprintAsset {
version: number; // Format version
type: 'blueprint'; // Asset type
metadata: BlueprintMetadata; // Metadata
variables: BlueprintVariable[]; // Variable definitions
nodes: BlueprintNode[]; // Node instances
connections: BlueprintConnection[]; // Connections
}
```
### Node Categories
| Category | Description | Color |
|----------|-------------|-------|
| `event` | Event nodes (entry points) | Red |
| `flow` | Flow control | Gray |
| `entity` | Entity operations | Blue |
| `component` | Component access | Cyan |
| `math` | Math operations | Green |
| `logic` | Logic operations | Red |
| `variable` | Variable access | Purple |
| `time` | Time utilities | Cyan |
| `debug` | Debug utilities | Gray |
### Pin Types
Nodes connect through pins:
```typescript
interface BlueprintPinDefinition {
name: string; // Pin name
type: PinDataType; // Data type
direction: 'input' | 'output';
isExec?: boolean; // Execution pin
defaultValue?: unknown;
}
type PinDataType =
| 'exec' // Execution flow
| 'boolean' // Boolean
| 'number' // Number
| 'string' // String
| 'vector2' // 2D vector
| 'vector3' // 3D vector
| 'entity' // Entity reference
| 'component' // Component reference
| 'any'; // Any type
```
### Variable Scopes
```typescript
type VariableScope =
| 'local' // Per execution
| 'instance' // Per entity
| 'global'; // Shared globally
```
## Virtual Machine API
### BlueprintVM
The virtual machine executes blueprint graphs:
```typescript
import { BlueprintVM } from '@esengine/blueprint';
const vm = new BlueprintVM(blueprintAsset, entity, scene);
vm.start(); // Start (triggers BeginPlay)
vm.tick(deltaTime); // Update (triggers Tick)
vm.stop(); // Stop (triggers EndPlay)
vm.pause();
vm.resume();
// Trigger events
vm.triggerEvent('EventCollision', { other: otherEntity });
vm.triggerCustomEvent('OnDamage', { amount: 50 });
// Debug mode
vm.debug = true;
```
### Execution Context
```typescript
interface ExecutionContext {
blueprint: BlueprintAsset;
entity: Entity;
scene: IScene;
deltaTime: number;
time: number;
getInput<T>(nodeId: string, pinName: string): T;
setOutput(nodeId: string, pinName: string, value: unknown): void;
getVariable<T>(name: string): T;
setVariable(name: string, value: unknown): void;
}
```
### Execution Result
```typescript
interface ExecutionResult {
outputs?: Record<string, unknown>; // Output values
nextExec?: string | null; // Next exec pin
delay?: number; // Delay execution (ms)
yield?: boolean; // Pause until next frame
error?: string; // Error message
}
```
## Custom Nodes
### Define Node Template
```typescript
import { BlueprintNodeTemplate } from '@esengine/blueprint';
const MyNodeTemplate: BlueprintNodeTemplate = {
type: 'MyCustomNode',
title: 'My Custom Node',
category: 'custom',
description: 'A custom node example',
keywords: ['custom', 'example'],
inputs: [
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
{ name: 'value', type: 'number', direction: 'input', defaultValue: 0 }
],
outputs: [
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
{ name: 'result', type: 'number', direction: 'output' }
]
};
```
### Implement Node Executor
```typescript
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const value = context.getInput<number>(node.id, 'value');
const result = value * 2;
return {
outputs: { result },
nextExec: 'exec'
};
}
}
```
### Registration Methods
```typescript
// Method 1: Decorator
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor { ... }
// Method 2: Manual registration
NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());
```
## Node Registry
```typescript
import { NodeRegistry } from '@esengine/blueprint';
const registry = NodeRegistry.instance;
const allTemplates = registry.getAllTemplates();
const mathNodes = registry.getTemplatesByCategory('math');
const results = registry.searchTemplates('add');
if (registry.has('MyCustomNode')) { ... }
```
## Built-in Nodes
### Event Nodes
| Node | Description |
|------|-------------|
| `EventBeginPlay` | Triggered on blueprint start |
| `EventTick` | Triggered every frame |
| `EventEndPlay` | Triggered on blueprint stop |
| `EventCollision` | Triggered on collision |
| `EventInput` | Triggered on input |
| `EventTimer` | Triggered by timer |
### Time Nodes
| Node | Description |
|------|-------------|
| `Delay` | Delay execution |
| `GetDeltaTime` | Get frame delta |
| `GetTime` | Get total runtime |
### Math Nodes
| Node | Description |
|------|-------------|
| `Add`, `Subtract`, `Multiply`, `Divide` | Basic operations |
| `Abs`, `Clamp`, `Lerp`, `Min`, `Max` | Utility functions |
### Debug Nodes
| Node | Description |
|------|-------------|
| `Print` | Print to console |
## Blueprint Composition
### Blueprint Fragments
Encapsulate reusable logic as fragments:
```typescript
import { createFragment } from '@esengine/blueprint';
const healthFragment = createFragment('HealthSystem', {
inputs: [
{ name: 'damage', type: 'number', internalNodeId: 'input1', internalPinName: 'value' }
],
outputs: [
{ name: 'isDead', type: 'boolean', internalNodeId: 'output1', internalPinName: 'value' }
],
graph: { nodes: [...], connections: [...], variables: [...] }
});
```
### Compose Blueprints
```typescript
import { createComposer, FragmentRegistry } from '@esengine/blueprint';
// Register fragments
FragmentRegistry.instance.register('health', healthFragment);
FragmentRegistry.instance.register('movement', movementFragment);
// Create composer
const composer = createComposer('PlayerBlueprint');
// Add fragments to slots
composer.addFragment(healthFragment, 'slot1', { position: { x: 0, y: 0 } });
composer.addFragment(movementFragment, 'slot2', { position: { x: 400, y: 0 } });
// Connect slots
composer.connect('slot1', 'onDeath', 'slot2', 'disable');
// Validate
const validation = composer.validate();
if (!validation.isValid) {
console.error(validation.errors);
}
// Compile to blueprint
const blueprint = composer.compile();
```
## Trigger System
### Define Trigger Conditions
```typescript
import { TriggerCondition, TriggerDispatcher } from '@esengine/blueprint';
const lowHealthCondition: TriggerCondition = {
type: 'comparison',
left: { type: 'variable', name: 'health' },
operator: '<',
right: { type: 'constant', value: 20 }
};
```
### Use Trigger Dispatcher
```typescript
const dispatcher = new TriggerDispatcher();
dispatcher.register('lowHealth', lowHealthCondition, (context) => {
context.triggerEvent('OnLowHealth');
});
dispatcher.evaluate(context);
```
## ECS Integration
### Using Blueprint System
```typescript
import { createBlueprintSystem } from '@esengine/blueprint';
class GameScene {
private blueprintSystem: BlueprintSystem;
initialize() {
this.blueprintSystem = createBlueprintSystem(this.scene);
}
update(dt: number) {
this.blueprintSystem.process(this.entities, dt);
}
}
```
### Triggering Blueprint Events
```typescript
import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';
triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });
```
## Serialization
### Save Blueprint
```typescript
import { validateBlueprintAsset } from '@esengine/blueprint';
function saveBlueprint(blueprint: BlueprintAsset, path: string): void {
if (!validateBlueprintAsset(blueprint)) {
throw new Error('Invalid blueprint structure');
}
const json = JSON.stringify(blueprint, null, 2);
fs.writeFileSync(path, json);
}
```
### Load Blueprint
```typescript
async function loadBlueprint(path: string): Promise<BlueprintAsset> {
const json = await fs.readFile(path, 'utf-8');
const asset = JSON.parse(json);
if (!validateBlueprintAsset(asset)) {
throw new Error('Invalid blueprint file');
}
return asset;
}
```
## Best Practices
1. **Use fragments for reusable logic**
2. **Choose appropriate variable scopes**
- `local`: Temporary calculations
- `instance`: Entity state (e.g., health)
- `global`: Game-wide state
3. **Avoid infinite loops** - VM has max steps per frame (default 1000)
4. **Debug techniques**
- Enable `vm.debug = true` for execution logs
- Use Print nodes for intermediate values
5. **Performance optimization**
- Pure nodes (`isPure: true`) cache outputs
- Avoid heavy computation in Tick

View File

@@ -0,0 +1,316 @@
# State Machine (FSM)
`@esengine/fsm` provides a type-safe finite state machine implementation for characters, AI, or any scenario requiring state management.
## Installation
```bash
npm install @esengine/fsm
```
## Quick Start
```typescript
import { createStateMachine } from '@esengine/fsm';
// Define state types
type PlayerState = 'idle' | 'walk' | 'run' | 'jump';
// Create state machine
const fsm = createStateMachine<PlayerState>('idle');
// Define states with callbacks
fsm.defineState('idle', {
onEnter: (ctx, from) => console.log(`Entered idle from ${from}`),
onExit: (ctx, to) => console.log(`Exiting idle to ${to}`),
onUpdate: (ctx, dt) => { /* Update every frame */ }
});
fsm.defineState('walk', {
onEnter: () => console.log('Started walking')
});
// Manual transition
fsm.transition('walk');
console.log(fsm.current); // 'walk'
```
## Core Concepts
### State Configuration
Each state can be configured with the following callbacks:
```typescript
interface StateConfig<TState, TContext> {
name: TState; // State name
onEnter?: (context: TContext, from: TState | null) => void; // Enter callback
onExit?: (context: TContext, to: TState) => void; // Exit callback
onUpdate?: (context: TContext, deltaTime: number) => void; // Update callback
tags?: string[]; // State tags
metadata?: Record<string, unknown>; // Metadata
}
```
### Transition Conditions
Define conditional state transitions:
```typescript
interface Context {
isMoving: boolean;
isRunning: boolean;
isGrounded: boolean;
}
const fsm = createStateMachine<PlayerState, Context>('idle', {
context: { isMoving: false, isRunning: false, isGrounded: true }
});
// Define transition conditions
fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving);
fsm.defineTransition('walk', 'run', (ctx) => ctx.isRunning);
fsm.defineTransition('walk', 'idle', (ctx) => !ctx.isMoving);
// Automatically evaluate and execute matching transitions
fsm.evaluateTransitions();
```
### Transition Priority
When multiple transitions are valid, higher priority executes first:
```typescript
// Higher priority number = higher priority
fsm.defineTransition('idle', 'attack', (ctx) => ctx.isAttacking, 10);
fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving, 1);
// If both conditions are met, 'attack' (priority 10) is tried first
```
## API Reference
### createStateMachine
```typescript
function createStateMachine<TState extends string, TContext = unknown>(
initialState: TState,
options?: StateMachineOptions<TContext>
): IStateMachine<TState, TContext>
```
**Parameters:**
- `initialState` - Initial state
- `options.context` - Context object, accessible in callbacks
- `options.maxHistorySize` - Maximum history entries (default 100)
- `options.enableHistory` - Enable history tracking (default true)
### State Machine Properties
| Property | Type | Description |
|----------|------|-------------|
| `current` | `TState` | Current state |
| `previous` | `TState \| null` | Previous state |
| `context` | `TContext` | Context object |
| `isTransitioning` | `boolean` | Whether currently transitioning |
| `currentStateDuration` | `number` | Current state duration (ms) |
### State Machine Methods
#### State Definition
```typescript
// Define state
fsm.defineState('idle', {
onEnter: (ctx, from) => {},
onExit: (ctx, to) => {},
onUpdate: (ctx, dt) => {}
});
// Check if state exists
fsm.hasState('idle'); // true
// Get state configuration
fsm.getStateConfig('idle');
// Get all states
fsm.getStates(); // ['idle', 'walk', ...]
```
#### Transition Operations
```typescript
// Define transition
fsm.defineTransition('idle', 'walk', condition, priority);
// Remove transition
fsm.removeTransition('idle', 'walk');
// Get transitions from state
fsm.getTransitionsFrom('idle');
// Check if transition is possible
fsm.canTransition('walk'); // true/false
// Manual transition
fsm.transition('walk');
// Force transition (ignore conditions)
fsm.transition('walk', true);
// Auto-evaluate transition conditions
fsm.evaluateTransitions();
```
#### Lifecycle
```typescript
// Update state machine (calls current state's onUpdate)
fsm.update(deltaTime);
// Reset state machine
fsm.reset(); // Reset to current state
fsm.reset('idle'); // Reset to specified state
```
#### Event Listeners
```typescript
// Listen to entering specific state
const unsubscribe = fsm.onEnter('walk', (from) => {
console.log(`Entered walk from ${from}`);
});
// Listen to exiting specific state
fsm.onExit('walk', (to) => {
console.log(`Exiting walk to ${to}`);
});
// Listen to any state change
fsm.onChange((event) => {
console.log(`${event.from} -> ${event.to} at ${event.timestamp}`);
});
// Unsubscribe
unsubscribe();
```
#### Debugging
```typescript
// Get state history
const history = fsm.getHistory();
// [{ from: 'idle', to: 'walk', timestamp: 1234567890 }, ...]
// Clear history
fsm.clearHistory();
// Get debug info
const info = fsm.getDebugInfo();
// { current, previous, duration, stateCount, transitionCount, historySize }
```
## Practical Examples
### Character State Machine
```typescript
import { createStateMachine } from '@esengine/fsm';
type CharacterState = 'idle' | 'walk' | 'run' | 'jump' | 'fall' | 'attack';
interface CharacterContext {
velocity: { x: number; y: number };
isGrounded: boolean;
isAttacking: boolean;
speed: number;
}
const characterFSM = createStateMachine<CharacterState, CharacterContext>('idle', {
context: {
velocity: { x: 0, y: 0 },
isGrounded: true,
isAttacking: false,
speed: 0
}
});
// Define states
characterFSM.defineState('idle', {
onEnter: (ctx) => { ctx.speed = 0; }
});
characterFSM.defineState('walk', {
onEnter: (ctx) => { ctx.speed = 100; }
});
characterFSM.defineState('run', {
onEnter: (ctx) => { ctx.speed = 200; }
});
// Define transitions
characterFSM.defineTransition('idle', 'walk', (ctx) => Math.abs(ctx.velocity.x) > 0);
characterFSM.defineTransition('walk', 'idle', (ctx) => ctx.velocity.x === 0);
characterFSM.defineTransition('walk', 'run', (ctx) => Math.abs(ctx.velocity.x) > 150);
// Jump has highest priority
characterFSM.defineTransition('idle', 'jump', (ctx) => !ctx.isGrounded, 10);
characterFSM.defineTransition('walk', 'jump', (ctx) => !ctx.isGrounded, 10);
// Game loop usage
function gameUpdate(dt: number) {
// Update context
characterFSM.context.velocity.x = getInputVelocity();
characterFSM.context.isGrounded = checkGrounded();
// Evaluate transitions
characterFSM.evaluateTransitions();
// Update current state
characterFSM.update(dt);
}
```
### ECS Integration
```typescript
import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework';
import { createStateMachine, type IStateMachine } from '@esengine/fsm';
// State machine component
class FSMComponent extends Component {
fsm: IStateMachine<string>;
constructor(initialState: string) {
super();
this.fsm = createStateMachine(initialState);
}
}
// State machine system
class FSMSystem extends EntitySystem {
constructor() {
super(Matcher.all(FSMComponent));
}
protected processEntity(entity: Entity, dt: number): void {
const fsmComp = entity.getComponent(FSMComponent);
fsmComp.fsm.evaluateTransitions();
fsmComp.fsm.update(dt);
}
}
```
## Blueprint Nodes
The FSM module provides blueprint nodes for visual scripting:
- `GetCurrentState` - Get current state
- `TransitionTo` - Transition to specified state
- `CanTransition` - Check if transition is possible
- `IsInState` - Check if in specified state
- `WasInState` - Check if was ever in specified state
- `GetStateDuration` - Get state duration
- `EvaluateTransitions` - Evaluate transition conditions
- `ResetStateMachine` - Reset state machine

View File

@@ -0,0 +1,299 @@
# Pathfinding System
`@esengine/pathfinding` provides a complete 2D pathfinding solution including A* algorithm, grid maps, navigation meshes, and path smoothing.
## Installation
```bash
npm install @esengine/pathfinding
```
## Quick Start
### Grid Map Pathfinding
```typescript
import { createGridMap, createAStarPathfinder } from '@esengine/pathfinding';
// Create 20x20 grid
const grid = createGridMap(20, 20);
// Set obstacles
grid.setWalkable(5, 5, false);
grid.setWalkable(5, 6, false);
// Create pathfinder
const pathfinder = createAStarPathfinder(grid);
// Find path
const result = pathfinder.findPath(0, 0, 15, 15);
if (result.found) {
console.log('Path found!');
console.log('Path:', result.path);
console.log('Cost:', result.cost);
}
```
### NavMesh Pathfinding
```typescript
import { createNavMesh } from '@esengine/pathfinding';
const navmesh = createNavMesh();
// Add polygon areas
navmesh.addPolygon([
{ x: 0, y: 0 }, { x: 10, y: 0 },
{ x: 10, y: 10 }, { x: 0, y: 10 }
]);
navmesh.addPolygon([
{ x: 10, y: 0 }, { x: 20, y: 0 },
{ x: 20, y: 10 }, { x: 10, y: 10 }
]);
// Auto-build connections
navmesh.build();
// Find path
const result = navmesh.findPath(1, 1, 18, 8);
```
## Core Concepts
### IPathResult
```typescript
interface IPathResult {
readonly found: boolean; // Path found
readonly path: readonly IPoint[];// Path points
readonly cost: number; // Total cost
readonly nodesSearched: number; // Nodes searched
}
```
### IPathfindingOptions
```typescript
interface IPathfindingOptions {
maxNodes?: number; // Max search nodes (default 10000)
heuristicWeight?: number; // Heuristic weight (>1 faster but may be suboptimal)
allowDiagonal?: boolean; // Allow diagonal movement (default true)
avoidCorners?: boolean; // Avoid corner cutting (default true)
}
```
## Heuristic Functions
| Function | Use Case | Description |
|----------|----------|-------------|
| `manhattanDistance` | 4-directional | Manhattan distance |
| `euclideanDistance` | Any direction | Euclidean distance |
| `chebyshevDistance` | 8-directional | Diagonal cost = 1 |
| `octileDistance` | 8-directional | Diagonal cost = √2 (default) |
## Grid Map API
### createGridMap
```typescript
function createGridMap(
width: number,
height: number,
options?: IGridMapOptions
): GridMap
```
**Options:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `allowDiagonal` | `boolean` | `true` | Allow diagonal movement |
| `diagonalCost` | `number` | `√2` | Diagonal movement cost |
| `avoidCorners` | `boolean` | `true` | Avoid corner cutting |
| `heuristic` | `HeuristicFunction` | `octileDistance` | Heuristic function |
### Map Operations
```typescript
// Check/set walkability
grid.isWalkable(x, y);
grid.setWalkable(x, y, false);
// Set movement cost (e.g., swamp, sand)
grid.setCost(x, y, 2);
// Set rectangle region
grid.setRectWalkable(0, 0, 5, 5, false);
// Load from array (0=walkable, non-0=blocked)
grid.loadFromArray([
[0, 0, 0, 1, 0],
[0, 1, 0, 1, 0]
]);
// Load from string (.=walkable, #=blocked)
grid.loadFromString(`
.....
.#.#.
`);
// Export and reset
console.log(grid.toString());
grid.reset();
```
## A* Pathfinder API
```typescript
const pathfinder = createAStarPathfinder(grid);
const result = pathfinder.findPath(
startX, startY,
endX, endY,
{ maxNodes: 5000, heuristicWeight: 1.5 }
);
// Pathfinder is reusable
pathfinder.findPath(0, 0, 10, 10);
pathfinder.findPath(5, 5, 15, 15);
```
## NavMesh API
```typescript
const navmesh = createNavMesh();
// Add convex polygons
const id1 = navmesh.addPolygon(vertices1);
const id2 = navmesh.addPolygon(vertices2);
// Auto-detect shared edges
navmesh.build();
// Or manually set connections
navmesh.setConnection(id1, id2, {
left: { x: 10, y: 0 },
right: { x: 10, y: 10 }
});
// Query and pathfind
const polygon = navmesh.findPolygonAt(5, 5);
navmesh.isWalkable(5, 5);
const result = navmesh.findPath(1, 1, 18, 8);
```
## Path Smoothing
### Line of Sight Smoothing
Remove unnecessary waypoints:
```typescript
import { createLineOfSightSmoother } from '@esengine/pathfinding';
const smoother = createLineOfSightSmoother();
const smoothedPath = smoother.smooth(result.path, grid);
```
### Curve Smoothing
Catmull-Rom spline:
```typescript
import { createCatmullRomSmoother } from '@esengine/pathfinding';
const smoother = createCatmullRomSmoother(5, 0.5);
const curvedPath = smoother.smooth(result.path, grid);
```
### Combined Smoothing
```typescript
import { createCombinedSmoother } from '@esengine/pathfinding';
const smoother = createCombinedSmoother(5, 0.5);
const finalPath = smoother.smooth(result.path, grid);
```
### Line of Sight Functions
```typescript
import { bresenhamLineOfSight, raycastLineOfSight } from '@esengine/pathfinding';
const hasLOS = bresenhamLineOfSight(x1, y1, x2, y2, grid);
const hasLOS2 = raycastLineOfSight(x1, y1, x2, y2, grid, 0.5);
```
## Practical Examples
### Dynamic Obstacles
```typescript
class DynamicPathfinding {
private grid: GridMap;
private pathfinder: AStarPathfinder;
private dynamicObstacles: Set<string> = new Set();
addDynamicObstacle(x: number, y: number): void {
this.dynamicObstacles.add(`${x},${y}`);
this.grid.setWalkable(x, y, false);
}
removeDynamicObstacle(x: number, y: number): void {
this.dynamicObstacles.delete(`${x},${y}`);
this.grid.setWalkable(x, y, true);
}
}
```
### Terrain Costs
```typescript
const grid = createGridMap(50, 50);
// Normal ground - cost 1 (default)
// Sand - cost 2
for (let y = 10; y < 20; y++) {
for (let x = 0; x < 50; x++) {
grid.setCost(x, y, 2);
}
}
// Swamp - cost 4
for (let y = 30; y < 35; y++) {
for (let x = 20; x < 30; x++) {
grid.setCost(x, y, 4);
}
}
```
## Blueprint Nodes
- `FindPath` - Find path
- `FindPathSmooth` - Find and smooth path
- `IsWalkable` - Check walkability
- `GetPathLength` - Get path point count
- `GetPathDistance` - Get total path distance
- `GetPathPoint` - Get specific path point
- `MoveAlongPath` - Move along path
- `HasLineOfSight` - Check line of sight
## Performance Tips
1. **Limit search range**: `{ maxNodes: 1000 }`
2. **Use heuristic weight**: `{ heuristicWeight: 1.5 }` (faster but may not be optimal)
3. **Reuse pathfinder instances**
4. **Use NavMesh for complex terrain**
5. **Choose appropriate heuristic for movement type**
## Grid vs NavMesh
| Feature | GridMap | NavMesh |
|---------|---------|---------|
| Use Case | Regular tile maps | Complex polygon terrain |
| Memory | Higher (width × height) | Lower (polygon count) |
| Precision | Grid-aligned | Continuous coordinates |
| Dynamic Updates | Easy | Requires rebuild |
| Setup Complexity | Simple | More complex |

View File

@@ -0,0 +1,396 @@
# Procedural Generation (Procgen)
`@esengine/procgen` provides core tools for procedural content generation, including noise functions, seeded random numbers, and various random utilities.
## Installation
```bash
npm install @esengine/procgen
```
## Quick Start
### Noise Generation
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
// Create Perlin noise
const perlin = createPerlinNoise(12345); // seed
// Sample 2D noise
const value = perlin.noise2D(x * 0.1, y * 0.1);
console.log(value); // [-1, 1]
// Use FBM for more natural results
const fbm = createFBM(perlin, {
octaves: 6,
persistence: 0.5
});
const height = fbm.noise2D(x * 0.01, y * 0.01);
```
### Seeded Random
```typescript
import { createSeededRandom } from '@esengine/procgen';
// Create deterministic random generator
const rng = createSeededRandom(42);
// Same seed always produces same sequence
console.log(rng.next()); // 0.xxx
console.log(rng.nextInt(1, 100)); // 1-100
console.log(rng.nextBool(0.3)); // 30% true
```
### Weighted Random
```typescript
import { createWeightedRandom, createSeededRandom } from '@esengine/procgen';
const rng = createSeededRandom(42);
const loot = createWeightedRandom([
{ value: 'common', weight: 60 },
{ value: 'uncommon', weight: 25 },
{ value: 'rare', weight: 10 },
{ value: 'legendary', weight: 5 }
]);
const drop = loot.pick(rng);
console.log(drop); // Likely 'common'
```
## Noise Functions
### Perlin Noise
Classic gradient noise, output range [-1, 1]:
```typescript
import { createPerlinNoise } from '@esengine/procgen';
const perlin = createPerlinNoise(seed);
const value2D = perlin.noise2D(x, y);
const value3D = perlin.noise3D(x, y, z);
```
### Simplex Noise
Faster than Perlin, less directional bias:
```typescript
import { createSimplexNoise } from '@esengine/procgen';
const simplex = createSimplexNoise(seed);
const value = simplex.noise2D(x, y);
```
### Worley Noise
Cell-based noise for stone, cell textures:
```typescript
import { createWorleyNoise } from '@esengine/procgen';
const worley = createWorleyNoise(seed);
const distance = worley.noise2D(x, y);
```
### FBM (Fractal Brownian Motion)
Layer multiple noise octaves for richer detail:
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
const baseNoise = createPerlinNoise(seed);
const fbm = createFBM(baseNoise, {
octaves: 6, // Layer count (more = richer detail)
lacunarity: 2.0, // Frequency multiplier
persistence: 0.5, // Amplitude decay
frequency: 1.0, // Initial frequency
amplitude: 1.0 // Initial amplitude
});
// Standard FBM
const value = fbm.noise2D(x, y);
// Ridged FBM (for mountains)
const ridged = fbm.ridged2D(x, y);
// Turbulence
const turb = fbm.turbulence2D(x, y);
// Billowed (for clouds)
const cloud = fbm.billowed2D(x, y);
```
## Seeded Random API
### SeededRandom
Deterministic PRNG based on xorshift128+:
```typescript
import { createSeededRandom } from '@esengine/procgen';
const rng = createSeededRandom(42);
```
### Basic Methods
```typescript
rng.next(); // [0, 1) float
rng.nextInt(1, 10); // [min, max] integer
rng.nextFloat(0, 100); // [min, max) float
rng.nextBool(); // 50%
rng.nextBool(0.3); // 30%
rng.reset(); // Reset to initial state
```
### Distribution Methods
```typescript
// Normal distribution (Gaussian)
rng.nextGaussian(); // mean 0, stdDev 1
rng.nextGaussian(100, 15); // mean 100, stdDev 15
// Exponential distribution
rng.nextExponential(); // λ = 1
rng.nextExponential(0.5); // λ = 0.5
```
### Geometry Methods
```typescript
// Uniform point in circle
const point = rng.nextPointInCircle(50); // { x, y }
// Point on circle edge
const edge = rng.nextPointOnCircle(50); // { x, y }
// Uniform point in sphere
const point3D = rng.nextPointInSphere(50); // { x, y, z }
// Random direction vector
const dir = rng.nextDirection2D(); // { x, y }, length 1
```
## Weighted Random API
### WeightedRandom
Precomputed cumulative weights for efficient selection:
```typescript
import { createWeightedRandom } from '@esengine/procgen';
const selector = createWeightedRandom([
{ value: 'apple', weight: 5 },
{ value: 'banana', weight: 3 },
{ value: 'cherry', weight: 2 }
]);
const result = selector.pick(rng);
const result2 = selector.pickRandom(); // Uses Math.random
console.log(selector.getProbability(0)); // 0.5 (5/10)
console.log(selector.size); // 3
console.log(selector.totalWeight); // 10
```
### Convenience Functions
```typescript
import { weightedPick, weightedPickFromMap } from '@esengine/procgen';
const item = weightedPick([
{ value: 'a', weight: 1 },
{ value: 'b', weight: 2 }
], rng);
const item2 = weightedPickFromMap({
'common': 60,
'rare': 30,
'epic': 10
}, rng);
```
## Shuffle and Sampling
### shuffle / shuffleCopy
Fisher-Yates shuffle:
```typescript
import { shuffle, shuffleCopy } from '@esengine/procgen';
const arr = [1, 2, 3, 4, 5];
shuffle(arr, rng); // In-place
const shuffled = shuffleCopy(arr, rng); // Copy
```
### pickOne
```typescript
import { pickOne } from '@esengine/procgen';
const item = pickOne(['a', 'b', 'c', 'd'], rng);
```
### sample / sampleWithReplacement
```typescript
import { sample, sampleWithReplacement } from '@esengine/procgen';
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const unique = sample(arr, 3, rng); // 3 unique
const withRep = sampleWithReplacement(arr, 5, rng); // 5 with replacement
```
### randomIntegers
```typescript
import { randomIntegers } from '@esengine/procgen';
// 5 unique random integers from 1-100
const nums = randomIntegers(1, 100, 5, rng);
```
### weightedSample
```typescript
import { weightedSample } from '@esengine/procgen';
const items = ['A', 'B', 'C', 'D', 'E'];
const weights = [10, 8, 6, 4, 2];
const selected = weightedSample(items, weights, 3, rng);
```
## Practical Examples
### Procedural Terrain
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
class TerrainGenerator {
private fbm: FBM;
private moistureFbm: FBM;
constructor(seed: number) {
const heightNoise = createPerlinNoise(seed);
const moistureNoise = createPerlinNoise(seed + 1000);
this.fbm = createFBM(heightNoise, {
octaves: 8,
persistence: 0.5,
frequency: 0.01
});
this.moistureFbm = createFBM(moistureNoise, {
octaves: 4,
persistence: 0.6,
frequency: 0.02
});
}
getHeight(x: number, y: number): number {
let height = this.fbm.noise2D(x, y);
height += this.fbm.ridged2D(x * 0.5, y * 0.5) * 0.3;
return (height + 1) * 0.5; // Normalize to [0, 1]
}
getBiome(x: number, y: number): string {
const height = this.getHeight(x, y);
const moisture = (this.moistureFbm.noise2D(x, y) + 1) * 0.5;
if (height < 0.3) return 'water';
if (height < 0.4) return 'beach';
if (height > 0.8) return 'mountain';
if (moisture < 0.3) return 'desert';
if (moisture > 0.7) return 'forest';
return 'grassland';
}
}
```
### Loot System
```typescript
import { createSeededRandom, createWeightedRandom } from '@esengine/procgen';
class LootSystem {
private rng: SeededRandom;
private raritySelector: WeightedRandom<string>;
constructor(seed: number) {
this.rng = createSeededRandom(seed);
this.raritySelector = createWeightedRandom([
{ value: 'common', weight: 60 },
{ value: 'uncommon', weight: 25 },
{ value: 'rare', weight: 10 },
{ value: 'legendary', weight: 5 }
]);
}
generateLoot(count: number): LootItem[] {
const loot: LootItem[] = [];
for (let i = 0; i < count; i++) {
const rarity = this.raritySelector.pick(this.rng);
// Get item from rarity table...
loot.push(item);
}
return loot;
}
}
```
## Blueprint Nodes
### Noise Nodes
- `SampleNoise2D` - Sample 2D noise
- `SampleFBM` - Sample FBM noise
### Random Nodes
- `SeededRandom` - Generate random float
- `SeededRandomInt` - Generate random integer
- `WeightedPick` - Weighted random selection
- `ShuffleArray` - Shuffle array
- `PickRandom` - Pick random element
- `SampleArray` - Sample from array
- `RandomPointInCircle` - Random point in circle
## Best Practices
1. **Use seeds for reproducibility**
```typescript
const seed = Date.now();
const rng = createSeededRandom(seed);
saveSeed(seed);
```
2. **Precompute weighted selectors**
```typescript
// Good: Create once, use many times
const selector = createWeightedRandom(items);
for (let i = 0; i < 1000; i++) {
selector.pick(rng);
}
```
3. **Choose appropriate noise**
- Perlin: Smooth terrain, clouds
- Simplex: Performance-critical
- Worley: Cell textures, stone
- FBM: Natural multi-detail effects
4. **Tune FBM parameters**
- `octaves`: More = richer detail, higher cost
- `persistence`: 0.5 is common, higher = more high-frequency detail
- `lacunarity`: Usually 2, controls frequency growth

View File

@@ -0,0 +1,322 @@
# Spatial Index System
`@esengine/spatial` provides efficient spatial querying and indexing, including range queries, nearest neighbor queries, raycasting, and AOI (Area of Interest) management.
## Installation
```bash
npm install @esengine/spatial
```
## Quick Start
### Spatial Index
```typescript
import { createGridSpatialIndex } from '@esengine/spatial';
// Create spatial index (cell size 100)
const spatialIndex = createGridSpatialIndex<Entity>(100);
// Insert objects
spatialIndex.insert(player, { x: 100, y: 200 });
spatialIndex.insert(enemy1, { x: 150, y: 250 });
spatialIndex.insert(enemy2, { x: 500, y: 600 });
// Find objects within radius
const nearby = spatialIndex.findInRadius({ x: 100, y: 200 }, 100);
console.log(nearby); // [player, enemy1]
// Find nearest object
const nearest = spatialIndex.findNearest({ x: 100, y: 200 });
console.log(nearest); // enemy1
// Update position
spatialIndex.update(player, { x: 120, y: 220 });
```
### AOI (Area of Interest)
```typescript
import { createGridAOI } from '@esengine/spatial';
// Create AOI manager
const aoi = createGridAOI<Entity>(100);
// Add observers
aoi.addObserver(player, { x: 100, y: 100 }, { viewRange: 200 });
aoi.addObserver(npc, { x: 150, y: 150 }, { viewRange: 150 });
// Listen to enter/exit events
aoi.addListener((event) => {
if (event.type === 'enter') {
console.log(`${event.observer} saw ${event.target}`);
} else if (event.type === 'exit') {
console.log(`${event.target} left ${event.observer}'s view`);
}
});
// Update position (triggers enter/exit events)
aoi.updatePosition(player, { x: 200, y: 200 });
// Get visible entities
const visible = aoi.getEntitiesInView(player);
```
## Core Concepts
### Spatial Index vs AOI
| Feature | SpatialIndex | AOI |
|---------|--------------|-----|
| Purpose | General spatial queries | Entity visibility tracking |
| Events | No event notification | Enter/exit events |
| Direction | One-way query | Two-way tracking |
| Use Cases | Collision, range attacks | MMO sync, NPC AI perception |
### IBounds
```typescript
interface IBounds {
readonly minX: number;
readonly minY: number;
readonly maxX: number;
readonly maxY: number;
}
```
### IRaycastHit
```typescript
interface IRaycastHit<T> {
readonly target: T; // Hit object
readonly point: IVector2; // Hit point
readonly normal: IVector2;// Hit normal
readonly distance: number;// Distance from origin
}
```
## Spatial Index API
### createGridSpatialIndex
```typescript
function createGridSpatialIndex<T>(cellSize?: number): GridSpatialIndex<T>
```
**Choosing cellSize:**
- Too small: High memory, reduced query efficiency
- Too large: Many objects per cell, slow iteration
- Recommended: 1-2x average object spacing
### Management Methods
```typescript
spatialIndex.insert(entity, position);
spatialIndex.remove(entity);
spatialIndex.update(entity, newPosition);
spatialIndex.clear();
```
### Query Methods
#### findInRadius
```typescript
const enemies = spatialIndex.findInRadius(
{ x: 100, y: 200 },
50,
(entity) => entity.type === 'enemy' // Optional filter
);
```
#### findInRect
```typescript
import { createBounds } from '@esengine/spatial';
const bounds = createBounds(0, 0, 200, 200);
const entities = spatialIndex.findInRect(bounds);
```
#### findNearest
```typescript
const nearest = spatialIndex.findNearest(
playerPosition,
500, // maxDistance
(entity) => entity.type === 'enemy'
);
```
#### findKNearest
```typescript
const nearestEnemies = spatialIndex.findKNearest(
playerPosition,
5, // k
500, // maxDistance
(entity) => entity.type === 'enemy'
);
```
#### raycast / raycastFirst
```typescript
const hits = spatialIndex.raycast(origin, direction, maxDistance);
const firstHit = spatialIndex.raycastFirst(origin, direction, maxDistance);
```
## AOI API
### createGridAOI
```typescript
function createGridAOI<T>(cellSize?: number): GridAOI<T>
```
### Observer Management
```typescript
// Add observer
aoi.addObserver(player, position, {
viewRange: 200,
observable: true // Can be seen by others
});
// Remove observer
aoi.removeObserver(player);
// Update position
aoi.updatePosition(player, newPosition);
// Update view range
aoi.updateViewRange(player, 300);
```
### Query Methods
```typescript
// Get entities in observer's view
const visible = aoi.getEntitiesInView(player);
// Get observers who can see entity
const observers = aoi.getObserversOf(monster);
// Check visibility
if (aoi.canSee(player, enemy)) { ... }
```
### Event System
```typescript
// Global event listener
aoi.addListener((event) => {
switch (event.type) {
case 'enter': /* entered view */ break;
case 'exit': /* left view */ break;
}
});
// Entity-specific listener
aoi.addEntityListener(player, (event) => {
if (event.type === 'enter') {
sendToClient(player, 'entity_enter', event.target);
}
});
```
## Utility Functions
### Bounds Creation
```typescript
import {
createBounds,
createBoundsFromCenter,
createBoundsFromCircle
} from '@esengine/spatial';
const bounds1 = createBounds(0, 0, 100, 100);
const bounds2 = createBoundsFromCenter({ x: 50, y: 50 }, 100, 100);
const bounds3 = createBoundsFromCircle({ x: 50, y: 50 }, 50);
```
### Geometry Checks
```typescript
import {
isPointInBounds,
boundsIntersect,
boundsIntersectsCircle,
distance,
distanceSquared
} from '@esengine/spatial';
if (isPointInBounds(point, bounds)) { ... }
if (boundsIntersect(boundsA, boundsB)) { ... }
if (boundsIntersectsCircle(bounds, center, radius)) { ... }
const dist = distance(pointA, pointB);
const distSq = distanceSquared(pointA, pointB); // Faster
```
## Practical Examples
### Range Attack Detection
```typescript
class CombatSystem {
private spatialIndex: ISpatialIndex<Entity>;
dealAreaDamage(center: IVector2, radius: number, damage: number): void {
const targets = this.spatialIndex.findInRadius(
center, radius,
(entity) => entity.hasComponent(HealthComponent)
);
for (const target of targets) {
target.getComponent(HealthComponent).takeDamage(damage);
}
}
}
```
### MMO Sync System
```typescript
class SyncSystem {
private aoi: IAOIManager<Player>;
constructor() {
this.aoi = createGridAOI<Player>(100);
this.aoi.addListener((event) => {
const packet = this.createSyncPacket(event);
this.sendToPlayer(event.observer, packet);
});
}
onPlayerMove(player: Player, newPosition: IVector2): void {
this.aoi.updatePosition(player, newPosition);
}
}
```
## Blueprint Nodes
### Spatial Query Nodes
- `FindInRadius`, `FindInRect`, `FindNearest`, `FindKNearest`
- `Raycast`, `RaycastFirst`
### AOI Nodes
- `GetEntitiesInView`, `GetObserversOf`, `CanSee`
- `OnEntityEnterView`, `OnEntityExitView`
## Service Tokens
```typescript
import { SpatialIndexToken, AOIManagerToken } from '@esengine/spatial';
services.register(SpatialIndexToken, createGridSpatialIndex(100));
services.register(AOIManagerToken, createGridAOI(100));
```

View File

@@ -0,0 +1,352 @@
# Timer System
`@esengine/timer` provides a flexible timer and cooldown system for delayed execution, repeating tasks, skill cooldowns, and more.
## Installation
```bash
npm install @esengine/timer
```
## Quick Start
```typescript
import { createTimerService } from '@esengine/timer';
// Create timer service
const timerService = createTimerService();
// One-time timer (executes after 1 second)
const handle = timerService.schedule('myTimer', 1000, () => {
console.log('Timer fired!');
});
// Repeating timer (every 100ms)
timerService.scheduleRepeating('heartbeat', 100, () => {
console.log('Tick');
});
// Cooldown system (5 second cooldown)
timerService.startCooldown('skill_fireball', 5000);
if (timerService.isCooldownReady('skill_fireball')) {
useFireball();
timerService.startCooldown('skill_fireball', 5000);
}
// Update in game loop
function gameLoop(deltaTime: number) {
timerService.update(deltaTime);
}
```
## Core Concepts
### Timer vs Cooldown
| Feature | Timer | Cooldown |
|---------|-------|----------|
| Purpose | Delayed code execution | Rate limiting |
| Callback | Has callback function | No callback |
| Repeat | Supports repeating | One-time |
| Query | Query remaining time | Query progress/ready status |
### TimerHandle
Handle object returned when scheduling a timer:
```typescript
interface TimerHandle {
readonly id: string; // Timer ID
readonly isValid: boolean; // Whether valid (not cancelled)
cancel(): void; // Cancel timer
}
```
### TimerInfo
Timer information object:
```typescript
interface TimerInfo {
readonly id: string; // Timer ID
readonly remaining: number; // Remaining time (ms)
readonly repeating: boolean; // Whether repeating
readonly interval?: number; // Interval (repeating only)
}
```
### CooldownInfo
Cooldown information object:
```typescript
interface CooldownInfo {
readonly id: string; // Cooldown ID
readonly duration: number; // Total duration (ms)
readonly remaining: number; // Remaining time (ms)
readonly progress: number; // Progress (0-1, 0=started, 1=finished)
readonly isReady: boolean; // Whether ready
}
```
## API Reference
### createTimerService
```typescript
function createTimerService(config?: TimerServiceConfig): ITimerService
```
**Configuration:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `maxTimers` | `number` | `0` | Maximum timer count (0 = unlimited) |
| `maxCooldowns` | `number` | `0` | Maximum cooldown count (0 = unlimited) |
### Timer API
#### schedule
Schedule a one-time timer:
```typescript
const handle = timerService.schedule('explosion', 2000, () => {
createExplosion();
});
// Cancel early
handle.cancel();
```
#### scheduleRepeating
Schedule a repeating timer:
```typescript
// Execute every second
timerService.scheduleRepeating('regen', 1000, () => {
player.hp += 5;
});
// Execute immediately once, then repeat every second
timerService.scheduleRepeating('tick', 1000, () => {
console.log('Tick');
}, true); // immediate = true
```
#### cancel / cancelById
Cancel timers:
```typescript
// Cancel by handle
handle.cancel();
// or
timerService.cancel(handle);
// Cancel by ID
timerService.cancelById('regen');
```
#### hasTimer
Check if timer exists:
```typescript
if (timerService.hasTimer('explosion')) {
console.log('Explosion is pending');
}
```
#### getTimerInfo
Get timer information:
```typescript
const info = timerService.getTimerInfo('explosion');
if (info) {
console.log(`Remaining: ${info.remaining}ms`);
console.log(`Repeating: ${info.repeating}`);
}
```
### Cooldown API
#### startCooldown
Start a cooldown:
```typescript
timerService.startCooldown('skill_fireball', 5000);
```
#### isCooldownReady / isOnCooldown
Check cooldown status:
```typescript
if (timerService.isCooldownReady('skill_fireball')) {
castFireball();
timerService.startCooldown('skill_fireball', 5000);
}
if (timerService.isOnCooldown('skill_fireball')) {
console.log('On cooldown...');
}
```
#### getCooldownProgress / getCooldownRemaining
Get cooldown progress:
```typescript
// Progress 0-1 (0=started, 1=complete)
const progress = timerService.getCooldownProgress('skill_fireball');
console.log(`Progress: ${(progress * 100).toFixed(0)}%`);
// Remaining time (ms)
const remaining = timerService.getCooldownRemaining('skill_fireball');
console.log(`Remaining: ${(remaining / 1000).toFixed(1)}s`);
```
#### getCooldownInfo
Get complete cooldown info:
```typescript
const info = timerService.getCooldownInfo('skill_fireball');
if (info) {
console.log(`Duration: ${info.duration}ms`);
console.log(`Remaining: ${info.remaining}ms`);
console.log(`Progress: ${info.progress}`);
console.log(`Ready: ${info.isReady}`);
}
```
#### resetCooldown / clearAllCooldowns
Reset cooldowns:
```typescript
// Reset single cooldown
timerService.resetCooldown('skill_fireball');
// Clear all cooldowns (e.g., on respawn)
timerService.clearAllCooldowns();
```
### Lifecycle
#### update
Update timer service (call every frame):
```typescript
function gameLoop(deltaTime: number) {
timerService.update(deltaTime); // deltaTime in ms
}
```
#### clear
Clear all timers and cooldowns:
```typescript
timerService.clear();
```
### Debug Properties
```typescript
console.log(timerService.activeTimerCount);
console.log(timerService.activeCooldownCount);
const timerIds = timerService.getActiveTimerIds();
const cooldownIds = timerService.getActiveCooldownIds();
```
## Practical Examples
### Skill Cooldown System
```typescript
import { createTimerService, type ITimerService } from '@esengine/timer';
class SkillSystem {
private timerService: ITimerService;
private skills: Map<string, SkillData> = new Map();
constructor() {
this.timerService = createTimerService();
}
useSkill(skillId: string): boolean {
const skill = this.skills.get(skillId);
if (!skill) return false;
if (!this.timerService.isCooldownReady(skillId)) {
const remaining = this.timerService.getCooldownRemaining(skillId);
console.log(`Skill ${skillId} on cooldown, ${remaining}ms remaining`);
return false;
}
this.executeSkill(skill);
this.timerService.startCooldown(skillId, skill.cooldown);
return true;
}
update(dt: number): void {
this.timerService.update(dt);
}
}
```
### DOT Effects
```typescript
class EffectSystem {
private timerService: ITimerService;
applyDOT(target: Entity, damage: number, duration: number): void {
const dotId = `dot_${target.id}_${Date.now()}`;
let elapsed = 0;
this.timerService.scheduleRepeating(dotId, 1000, () => {
elapsed += 1000;
target.takeDamage(damage);
if (elapsed >= duration) {
this.timerService.cancelById(dotId);
}
});
}
}
```
## Blueprint Nodes
### Cooldown Nodes
- `StartCooldown` - Start cooldown
- `IsCooldownReady` - Check if cooldown is ready
- `GetCooldownProgress` - Get cooldown progress
- `GetCooldownInfo` - Get cooldown info
- `ResetCooldown` - Reset cooldown
### Timer Nodes
- `HasTimer` - Check if timer exists
- `CancelTimer` - Cancel timer
- `GetTimerRemaining` - Get timer remaining time
## Service Token
For dependency injection:
```typescript
import { TimerServiceToken, createTimerService } from '@esengine/timer';
services.register(TimerServiceToken, createTimerService());
const timerService = services.get(TimerServiceToken);
```