tessl install tessl/npm-ecos@0.2.0Entity Component System for JavaScript that enables component-based entity creation and management using a factory pattern
Presets allow you to create reusable configurations that can be applied to multiple factories. This guide shows you how to define and use presets to reduce duplication and maintain consistency across your entities.
Presets bundle together:
Presets enable you to define common entity patterns once and reuse them across multiple factories.
Before creating a preset, register the methods and extenders you want to include:
const { factory, extenders } = require('ecos');
// Register methods
factory.registerMethod('move', function(dx, dy) {
this.x += dx;
this.y += dy;
});
factory.registerMethod('draw', function() {
console.log(`Drawing at (${this.x}, ${this.y})`);
});
// Register extenders
factory.registerExtender('timestamps', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.createdAt = Date.now();
entity.updatedAt = Date.now();
}
});factory.registerPreset('positioned', {
props: ['x', 'y'],
methods: ['move', 'draw']
});
factory.registerPreset('tracked', {
extend: ['timestamps']
});const spriteFactory = factory.create({
name: 'sprite',
presets: ['positioned', 'tracked'],
default: { x: 0, y: 0 } // IMPORTANT: props from preset must be in default
});
const sprite = spriteFactory.create({ x: 10, y: 20 });
sprite.move(5, 3);
sprite.draw(); // 'Drawing at (15, 23)'
console.log(sprite.createdAt); // Timestamp from extenderCritical constraint: Preset props do NOT add new properties to entities. They only initialize properties that are already defined in the factory's default option.
// ✓ CORRECT: Props in preset also exist in default
factory.registerPreset('positioned', {
props: ['x', 'y']
});
const factory1 = factory.create({
name: 'entity',
presets: ['positioned'],
default: { x: 0, y: 0 } // Props from preset are in default
});// ✗ INCORRECT: Props in preset missing from default
factory.registerPreset('positioned', {
props: ['x', 'y']
});
const factory2 = factory.create({
name: 'entity',
presets: ['positioned']
// No default option - TypeError will occur!
});
const factory3 = factory.create({
name: 'entity',
presets: ['positioned'],
default: { x: 0 } // Missing 'y' - TypeError will occur!
});See Constraints for more details on this limitation.
Create a preset for entities that exist in 2D space:
factory.registerMethod('move', function(dx, dy) {
this.x += dx;
this.y += dy;
});
factory.registerMethod('distanceTo', function(other) {
const dx = this.x - other.x;
const dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
});
factory.registerPreset('positioned', {
props: ['x', 'y'],
methods: ['move', 'distanceTo']
});
// Use in factories
const playerFactory = factory.create({
name: 'player',
presets: ['positioned'],
default: { x: 0, y: 0 }
});
const enemyFactory = factory.create({
name: 'enemy',
presets: ['positioned'],
default: { x: 100, y: 100 }
});Create a preset for entities with health:
factory.registerMethod('takeDamage', function(amount) {
this.health = Math.max(0, this.health - amount);
return this.health;
});
factory.registerMethod('heal', function(amount) {
this.health = Math.min(this.maxHealth, this.health + amount);
return this.health;
});
factory.registerMethod('isAlive', function() {
return this.health > 0;
});
factory.registerPreset('mortal', {
props: ['health', 'maxHealth'],
methods: ['takeDamage', 'heal', 'isAlive']
});
// Use in factories
const characterFactory = factory.create({
name: 'character',
presets: ['mortal'],
default: { health: 100, maxHealth: 100 }
});Create a preset for entities with timestamps and IDs:
factory.registerExtender('timestamps', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.createdAt = Date.now();
entity.updatedAt = Date.now();
}
});
factory.registerExtender('uuid', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.uuid = Math.random().toString(36).substr(2, 9);
}
});
factory.registerPreset('tracked', {
extend: ['timestamps', 'uuid']
});
// Use in factories
const documentFactory = factory.create({
name: 'document',
presets: ['tracked'],
props: ['title', 'content'],
default: { title: '', content: '' }
});Apply multiple presets to a single factory:
const { factory } = require('ecos');
// Register presets (assuming methods/extenders are already registered)
factory.registerPreset('positioned', {
props: ['x', 'y'],
methods: ['move', 'draw']
});
factory.registerPreset('mortal', {
props: ['health', 'maxHealth'],
methods: ['takeDamage', 'heal', 'isAlive']
});
factory.registerPreset('tracked', {
extend: ['timestamps']
});
// Combine presets in a factory
const gameEntityFactory = factory.create({
name: 'gameEntity',
presets: ['positioned', 'mortal', 'tracked'],
default: {
x: 0,
y: 0,
health: 100,
maxHealth: 100
}
});
const entity = gameEntityFactory.create({ x: 10, y: 20, health: 150 });
entity.move(5, 5);
entity.takeDamage(30);
console.log(entity.isAlive()); // true
console.log(entity.createdAt); // timestampYou can extend a factory with additional configuration beyond what presets provide:
factory.registerPreset('positioned', {
props: ['x', 'y'],
methods: ['move']
});
// Add extra methods and extenders
factory.registerMethod('teleport', function(x, y) {
this.x = x;
this.y = y;
});
factory.registerExtender('boundedPosition', {
type: extenders.FUNCTION,
handler: function(entity) {
entity.bounds = { minX: 0, minY: 0, maxX: 800, maxY: 600 };
}
});
const extendedFactory = factory.create({
name: 'bounded',
presets: ['positioned'],
methods: ['teleport'], // Additional method
extend: ['boundedPosition'], // Additional extender
default: { x: 0, y: 0 }
});Build complex presets from simpler ones:
// Base presets
factory.registerPreset('positioned', {
props: ['x', 'y'],
methods: ['move']
});
factory.registerPreset('mortal', {
props: ['health', 'maxHealth'],
methods: ['takeDamage', 'heal']
});
// Composed preset (applied to factories directly)
const playerFactory = factory.create({
name: 'player',
presets: ['positioned', 'mortal', 'tracked'],
default: {
x: 0,
y: 0,
health: 100,
maxHealth: 100
}
});Create presets for specific domains or modules:
// UI component preset
factory.registerPreset('uiComponent', {
props: ['visible', 'enabled'],
methods: ['show', 'hide', 'enable', 'disable']
});
// Game entity preset
factory.registerPreset('gameEntity', {
props: ['active', 'layer'],
methods: ['activate', 'deactivate', 'setLayer']
});
// Network entity preset
factory.registerPreset('networkEntity', {
extend: ['uuid', 'timestamps'],
methods: ['serialize', 'deserialize']
});Each preset should represent a single concept or capability:
// Good - focused presets
factory.registerPreset('positioned', { ... });
factory.registerPreset('mortal', { ... });
factory.registerPreset('tracked', { ... });
// Avoid - too broad
factory.registerPreset('everything', {
props: ['x', 'y', 'health', 'mana', 'inventory'],
methods: ['move', 'attack', 'cast', 'pickup'],
extend: ['timestamps', 'uuid', 'validation']
});Follow naming conventions for clarity:
// Good
factory.registerPreset('positioned', { ... });
factory.registerPreset('mortal', { ... });
factory.registerPreset('tracked', { ... });
// Avoid
factory.registerPreset('pos', { ... });
factory.registerPreset('hp', { ... });
factory.registerPreset('t', { ... });When creating presets, clearly document the required default properties:
/**
* Positioned preset
*
* Provides movement and positioning capabilities for 2D entities.
*
* Required defaults:
* - x: number (default: 0)
* - y: number (default: 0)
*
* Provides methods:
* - move(dx, dy)
* - distanceTo(other)
*/
factory.registerPreset('positioned', {
props: ['x', 'y'],
methods: ['move', 'distanceTo']
});Group related preset registrations:
// game/presets/physics.js
function registerPhysicsPresets(factory) {
factory.registerPreset('positioned', { ... });
factory.registerPreset('velocity', { ... });
factory.registerPreset('collider', { ... });
}
// game/presets/combat.js
function registerCombatPresets(factory) {
factory.registerPreset('mortal', { ... });
factory.registerPreset('attacker', { ... });
factory.registerPreset('defender', { ... });
}
// Initialize
registerPhysicsPresets(factory);
registerCombatPresets(factory);When a factory uses presets, the preset configuration is merged with the factory configuration:
See Execution Order for details on how presets affect entity creation flow.
The factory module throws errors for invalid preset operations:
try {
factory.registerPreset('myPreset', {
props: ['x', 'y'],
methods: ['move']
});
} catch (error) {
console.error('Failed to register preset:', error.message);
}