* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
444 lines
13 KiB
TypeScript
444 lines
13 KiB
TypeScript
/**
|
|
* @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);
|
|
}
|