244 lines
5.9 KiB
TypeScript
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;
|
|
}
|
|
}
|