Files
rogue/src/engine/ecs/EntityBuilder.ts
Peter Stockings 9552364a60 feat: Add traps
2026-01-25 16:37:46 +11:00

244 lines
5.9 KiB
TypeScript

import { type ECSWorld } from "./World";
import { type ComponentMap } from "./components";
import { type EntityId, type Stats, type EnemyAIState, type ActorType } from "../../core/types";
import { GAME_CONFIG } from "../../core/config/GameConfig";
/**
* Fluent builder for creating ECS entities with components.
* Makes entity creation declarative and easy to extend.
*
* @example
* // Create a simple trap
* EntityBuilder.create(world)
* .withPosition(5, 10)
* .asTrap(15)
* .build();
*
* @example
* // Create an enemy
* EntityBuilder.create(world)
* .withPosition(3, 7)
* .asEnemy("rat")
* .build();
*/
export class EntityBuilder {
private world: ECSWorld;
private entityId: EntityId;
private components: Partial<{ [K in keyof ComponentMap]: ComponentMap[K] }> = {};
private constructor(world: ECSWorld) {
this.world = world;
this.entityId = world.createEntity();
}
/**
* Start building a new entity.
*/
static create(world: ECSWorld): EntityBuilder {
return new EntityBuilder(world);
}
/**
* Add a position component.
*/
withPosition(x: number, y: number): this {
this.components.position = { x, y };
return this;
}
/**
* Add a name component.
*/
withName(name: string): this {
this.components.name = { name };
return this;
}
/**
* Add a sprite component.
*/
withSprite(texture: string, index: number): this {
this.components.sprite = { texture, index };
return this;
}
/**
* Add stats component with partial stats (fills defaults).
*/
withStats(stats: Partial<Stats>): this {
const defaultStats: Stats = {
maxHp: 10, hp: 10,
maxMana: 0, mana: 0,
attack: 1, defense: 0,
level: 1, exp: 0, expToNextLevel: 10,
critChance: 0, critMultiplier: 100, accuracy: 100, lifesteal: 0,
evasion: 0, blockChance: 0, luck: 0,
statPoints: 0, skillPoints: 0,
strength: 10, dexterity: 10, intelligence: 10,
passiveNodes: []
};
this.components.stats = { ...defaultStats, ...stats };
return this;
}
/**
* Add energy component for turn scheduling.
*/
withEnergy(speed: number, current: number = 0): this {
this.components.energy = { current, speed };
return this;
}
/**
* Add AI component for enemy behavior.
*/
withAI(state: EnemyAIState = "wandering"): this {
this.components.ai = { state };
return this;
}
/**
* Add player tag component.
*/
asPlayer(): this {
this.components.player = {};
this.components.actorType = { type: "player" };
return this;
}
/**
* Configure as an enemy with stats from GameConfig.
*/
asEnemy(type: ActorType): this {
if (type === "player") {
throw new Error("Use asPlayer() for player entities");
}
this.components.actorType = { type };
this.withAI("wandering");
// Apply enemy stats from config
const config = GAME_CONFIG.enemies[type as keyof typeof GAME_CONFIG.enemies];
if (config) {
const speed = config.minSpeed + Math.random() * (config.maxSpeed - config.minSpeed);
this.withStats({
maxHp: config.baseHp,
hp: config.baseHp,
attack: config.baseAttack,
defense: config.baseDefense
});
this.withEnergy(speed);
}
return this;
}
/**
* Configure as a trap that deals damage when stepped on.
*/
asTrap(damage: number, oneShot: boolean = false): this {
this.components.trigger = {
onEnter: true,
oneShot,
damage
};
return this;
}
/**
* Configure as a trigger zone (pressure plate, etc).
*/
asTrigger(options: {
onEnter?: boolean;
onExit?: boolean;
oneShot?: boolean;
effect?: string;
effectDuration?: number;
}): this {
this.components.trigger = {
onEnter: options.onEnter ?? true,
onExit: options.onExit,
oneShot: options.oneShot,
effect: options.effect,
effectDuration: options.effectDuration
};
return this;
}
/**
* Configure as a destructible object.
*/
asDestructible(hp: number, maxHp?: number, options?: {
destroyedTile?: number;
lootTable?: string;
}): this {
this.components.destructible = {
hp,
maxHp: maxHp ?? hp,
destroyedTile: options?.destroyedTile,
lootTable: options?.lootTable
};
return this;
}
/**
* Configure as a collectible (exp orb, etc).
*/
asCollectible(type: "exp_orb", amount: number): this {
this.components.collectible = { type, amount };
return this;
}
/**
* Configure as an item on the ground.
*/
asGroundItem(itemId: string, quantity: number = 1): this {
this.components.groundItem = { itemId, quantity };
return this;
}
/**
* Add initial status effects.
*/
withStatusEffects(effects: ComponentMap["statusEffects"]["effects"]): this {
this.components.statusEffects = { effects };
return this;
}
/**
* Add combat tracking component.
*/
withCombat(): this {
this.components.combat = {};
return this;
}
/**
* Add a raw component directly.
*/
with<K extends keyof ComponentMap>(type: K, data: ComponentMap[K]): this {
this.components[type] = data;
return this;
}
/**
* Finalize and register all components with the ECS world.
* @returns The created entity ID
*/
build(): EntityId {
for (const [type, data] of Object.entries(this.components)) {
if (data !== undefined) {
this.world.addComponent(this.entityId, type as keyof ComponentMap, data as any);
}
}
return this.entityId;
}
/**
* Get the entity ID (even before build is called).
*/
getId(): EntityId {
return this.entityId;
}
}