feat: Add traps
This commit is contained in:
243
src/engine/ecs/EntityBuilder.ts
Normal file
243
src/engine/ecs/EntityBuilder.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user