Files
esengine/packages/rendering/effect/src/core/EffectContainer.ts

444 lines
13 KiB
TypeScript
Raw Normal View History

/**
* @zh
* @en Effect Container
*
* @zh
* @en Manages all effects on a single entity
*/
import type {
IEffectInstance,
IEffectDefinition,
IEffectEvent,
EffectEventListener,
EffectEventType,
IEffectHandler
} from './IEffect';
let instanceCounter = 0;
function generateInstanceId(): string {
return `effect_${Date.now()}_${++instanceCounter}`;
}
/**
* @zh
* @en Effect container
*/
export class EffectContainer<TTarget = unknown> {
private readonly _effects: Map<string, IEffectInstance> = new Map();
private readonly _effectsByType: Map<string, Set<string>> = new Map();
private readonly _effectsByTag: Map<string, Set<string>> = new Map();
private readonly _handlers: Map<string, IEffectHandler<TTarget>> = new Map();
private readonly _listeners: Map<EffectEventType, Set<EffectEventListener>> = new Map();
private readonly _target: TTarget;
private readonly _targetId: string;
/**
* @zh
* @en Create effect container
*
* @param target - @zh @en Target object
* @param targetId - @zh ID @en Target ID
*/
constructor(target: TTarget, targetId: string) {
this._target = target;
this._targetId = targetId;
}
/**
* @zh
* @en Get target object
*/
get target(): TTarget {
return this._target;
}
/**
* @zh ID
* @en Get target ID
*/
get targetId(): string {
return this._targetId;
}
/**
* @zh
* @en Get effect count
*/
get count(): number {
return this._effects.size;
}
/**
* @zh
* @en Register effect handler
*/
registerHandler(typeId: string, handler: IEffectHandler<TTarget>): void {
this._handlers.set(typeId, handler);
}
/**
* @zh
* @en Unregister effect handler
*/
unregisterHandler(typeId: string): void {
this._handlers.delete(typeId);
}
/**
* @zh
* @en Add event listener
*/
addEventListener(type: EffectEventType, listener: EffectEventListener): void {
if (!this._listeners.has(type)) {
this._listeners.set(type, new Set());
}
this._listeners.get(type)!.add(listener);
}
/**
* @zh
* @en Remove event listener
*/
removeEventListener(type: EffectEventType, listener: EffectEventListener): void {
this._listeners.get(type)?.delete(listener);
}
private _emitEvent(type: EffectEventType, effect: IEffectInstance, data?: Record<string, unknown>): void {
const event: IEffectEvent = {
type,
effect,
targetId: this._targetId,
timestamp: Date.now(),
data
};
this._listeners.get(type)?.forEach(listener => listener(event));
}
/**
* @zh
* @en Apply effect
*
* @param definition - @zh @en Effect definition
* @param sourceId - @zh ID @en Source ID
* @param initialData - @zh @en Initial data
* @returns @zh null @en Effect instance or null
*/
apply(
definition: IEffectDefinition,
sourceId?: string,
initialData?: Record<string, unknown>
): IEffectInstance | null {
// Handle exclusive tags - remove conflicting effects
if (definition.exclusiveTags) {
for (const tag of definition.exclusiveTags) {
this.removeByTag(tag);
}
}
// Check for existing effect of same type
const existingIds = this._effectsByType.get(definition.typeId);
if (existingIds && existingIds.size > 0) {
const existingId = existingIds.values().next().value as string;
const existing = this._effects.get(existingId);
if (existing) {
return this._handleStacking(existing, definition);
}
}
// Create new effect instance
const instance = this._createInstance(definition, sourceId, initialData);
// Add to collections
this._effects.set(instance.instanceId, instance);
if (!this._effectsByType.has(definition.typeId)) {
this._effectsByType.set(definition.typeId, new Set());
}
this._effectsByType.get(definition.typeId)!.add(instance.instanceId);
for (const tag of definition.tags) {
if (!this._effectsByTag.has(tag)) {
this._effectsByTag.set(tag, new Set());
}
this._effectsByTag.get(tag)!.add(instance.instanceId);
}
// Call handler
const handler = this._handlers.get(definition.typeId);
handler?.onApply?.(instance, this._target);
// Emit event
this._emitEvent('applied', instance);
return instance;
}
private _createInstance(
definition: IEffectDefinition,
sourceId?: string,
initialData?: Record<string, unknown>
): IEffectInstance {
const duration = definition.duration;
let remainingTime = Infinity;
if (duration.type === 'timed' && duration.duration !== undefined) {
remainingTime = duration.duration;
}
return {
instanceId: generateInstanceId(),
definition,
sourceId,
stacks: 1,
remainingTime,
nextTickTime: definition.tickInterval ?? 0,
data: { ...initialData },
isActive: true,
appliedAt: Date.now()
};
}
private _handleStacking(existing: IEffectInstance, definition: IEffectDefinition): IEffectInstance | null {
const rule = definition.stacking.rule;
switch (rule) {
case 'refresh':
// Reset duration
if (definition.duration.type === 'timed' && definition.duration.duration !== undefined) {
existing.remainingTime = definition.duration.duration;
}
this._handlers.get(definition.typeId)?.onRefresh?.(existing, this._target);
this._emitEvent('refreshed', existing);
return existing;
case 'stack':
// Add stacks
const maxStacks = definition.stacking.maxStacks ?? Infinity;
if (existing.stacks < maxStacks) {
existing.stacks++;
this._handlers.get(definition.typeId)?.onStack?.(existing, this._target, existing.stacks);
this._emitEvent('stacked', existing, { stacks: existing.stacks });
}
// Also refresh duration
if (definition.duration.type === 'timed' && definition.duration.duration !== undefined) {
existing.remainingTime = definition.duration.duration;
}
return existing;
case 'replace':
// Remove existing and apply new
this.remove(existing.instanceId);
return null; // Will be created as new
case 'ignore':
// Do nothing
return existing;
case 'independent':
default:
// Allow multiple instances - will create new one
return null;
}
}
/**
* @zh
* @en Remove effect
*
* @param instanceId - @zh ID @en Instance ID
*/
remove(instanceId: string): boolean {
const effect = this._effects.get(instanceId);
if (!effect) return false;
effect.isActive = false;
// Call handler
const handler = this._handlers.get(effect.definition.typeId);
handler?.onRemove?.(effect, this._target);
// Remove from collections
this._effects.delete(instanceId);
this._effectsByType.get(effect.definition.typeId)?.delete(instanceId);
for (const tag of effect.definition.tags) {
this._effectsByTag.get(tag)?.delete(instanceId);
}
// Emit event
this._emitEvent('removed', effect);
return true;
}
/**
* @zh
* @en Remove effects by type
*/
removeByType(typeId: string): number {
const ids = this._effectsByType.get(typeId);
if (!ids) return 0;
let count = 0;
for (const id of [...ids]) {
if (this.remove(id)) count++;
}
return count;
}
/**
* @zh
* @en Remove effects by tag
*/
removeByTag(tag: string): number {
const ids = this._effectsByTag.get(tag);
if (!ids) return 0;
let count = 0;
for (const id of [...ids]) {
if (this.remove(id)) count++;
}
return count;
}
/**
* @zh
* @en Remove all effects
*/
removeAll(): void {
for (const id of [...this._effects.keys()]) {
this.remove(id);
}
}
/**
* @zh
* @en Get effect instance
*/
get(instanceId: string): IEffectInstance | undefined {
return this._effects.get(instanceId);
}
/**
* @zh
* @en Get effects by type
*/
getByType(typeId: string): IEffectInstance[] {
const ids = this._effectsByType.get(typeId);
if (!ids) return [];
return [...ids].map(id => this._effects.get(id)!).filter(Boolean);
}
/**
* @zh
* @en Get effects by tag
*/
getByTag(tag: string): IEffectInstance[] {
const ids = this._effectsByTag.get(tag);
if (!ids) return [];
return [...ids].map(id => this._effects.get(id)!).filter(Boolean);
}
/**
* @zh
* @en Check if has effect of specified type
*/
hasType(typeId: string): boolean {
const ids = this._effectsByType.get(typeId);
return ids !== undefined && ids.size > 0;
}
/**
* @zh
* @en Check if has effect with specified tag
*/
hasTag(tag: string): boolean {
const ids = this._effectsByTag.get(tag);
return ids !== undefined && ids.size > 0;
}
/**
* @zh
* @en Get stack count for specified type
*/
getStacks(typeId: string): number {
const effects = this.getByType(typeId);
return effects.reduce((sum, e) => sum + e.stacks, 0);
}
/**
* @zh
* @en Get all effects
*/
getAll(): IEffectInstance[] {
return [...this._effects.values()];
}
/**
* @zh
* @en Update effects (called every frame)
*
* @param deltaTime - @zh @en Delta time in seconds
*/
update(deltaTime: number): void {
const toRemove: string[] = [];
for (const effect of this._effects.values()) {
if (!effect.isActive) continue;
const definition = effect.definition;
const handler = this._handlers.get(definition.typeId);
// Update remaining time
if (definition.duration.type === 'timed') {
effect.remainingTime -= deltaTime;
if (effect.remainingTime <= 0) {
this._emitEvent('expired', effect);
toRemove.push(effect.instanceId);
continue;
}
}
// Check conditional duration
if (definition.duration.type === 'conditional') {
const condition = definition.duration.condition;
if (condition && !condition()) {
this._emitEvent('expired', effect);
toRemove.push(effect.instanceId);
continue;
}
}
// Handle periodic tick
if (definition.tickInterval && definition.tickInterval > 0) {
effect.nextTickTime -= deltaTime;
if (effect.nextTickTime <= 0) {
handler?.onTick?.(effect, this._target, deltaTime);
this._emitEvent('ticked', effect);
effect.nextTickTime = definition.tickInterval;
}
}
// Call update handler
handler?.onUpdate?.(effect, this._target, deltaTime);
}
// Remove expired effects
for (const id of toRemove) {
this.remove(id);
}
}
}
/**
* @zh
* @en Create effect container
*/
export function createEffectContainer<TTarget>(target: TTarget, targetId: string): EffectContainer<TTarget> {
return new EffectContainer(target, targetId);
}