feat(effect): 添加效果系统模块 (#332)
实现 Buff/Debuff 效果管理和属性修改器系统: 核心功能: - 效果容器管理效果的应用、移除和更新 - 支持持续时间类型: 永久/计时/条件 - 支持叠加规则: 刷新/叠加/独立/替换/忽略 - 效果标签系统用于分类和查询 - 效果优先级和互斥标签支持 - 效果事件系统 (应用/移除/叠加/刷新/跳动/过期) 修改器系统: - 属性修改器支持加法/乘法/覆盖/最小值/最大值操作 - 优先级分层计算 (基础/加法/乘法/最终) - 数值计算器自动按优先级应用修改器 蓝图节点 (12个): - ApplyEffect: 应用效果 - RemoveEffect/RemoveEffectByTag: 移除效果 - HasEffect/HasEffectTag: 检查效果 - GetEffectStacks/GetEffectRemainingTime/GetEffectCount: 查询效果 - ClearAllEffects: 清除所有效果 - OnEffectApplied/OnEffectRemoved/OnEffectTick: 效果事件
This commit is contained in:
24
packages/effect/module.json
Normal file
24
packages/effect/module.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"id": "effect",
|
||||||
|
"name": "@esengine/effect",
|
||||||
|
"globalKey": "effect",
|
||||||
|
"displayName": "Effect System",
|
||||||
|
"description": "效果系统,支持持续时间、叠加规则和属性修改器 | Effect system with duration, stacking rules, and attribute modifiers",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "Gameplay",
|
||||||
|
"icon": "Sparkles",
|
||||||
|
"tags": ["effect", "buff", "debuff", "modifier", "status"],
|
||||||
|
"isCore": false,
|
||||||
|
"defaultEnabled": true,
|
||||||
|
"isEngineModule": true,
|
||||||
|
"canContainContent": false,
|
||||||
|
"platforms": ["web", "desktop"],
|
||||||
|
"dependencies": ["core"],
|
||||||
|
"exports": {
|
||||||
|
"components": ["EffectContainer"],
|
||||||
|
"systems": ["EffectSystem"]
|
||||||
|
},
|
||||||
|
"requiresWasm": false,
|
||||||
|
"outputPath": "dist/index.js",
|
||||||
|
"pluginExport": "EffectPlugin"
|
||||||
|
}
|
||||||
40
packages/effect/package.json
Normal file
40
packages/effect/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@esengine/effect",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Effect system for ECS Framework / ECS 框架的效果系统",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"module.json"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"build:watch": "tsup --watch",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"clean": "rimraf dist"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
|
"@esengine/blueprint": "workspace:*",
|
||||||
|
"@esengine/build-config": "workspace:*",
|
||||||
|
"@types/node": "^20.19.17",
|
||||||
|
"rimraf": "^5.0.0",
|
||||||
|
"tsup": "^8.0.0",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
443
packages/effect/src/core/EffectContainer.ts
Normal file
443
packages/effect/src/core/EffectContainer.ts
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
306
packages/effect/src/core/IEffect.ts
Normal file
306
packages/effect/src/core/IEffect.ts
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
/**
|
||||||
|
* @zh 效果接口定义
|
||||||
|
* @en Effect Interface Definitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 持续时间类型 | Duration Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 持续时间类型
|
||||||
|
* @en Duration type
|
||||||
|
*/
|
||||||
|
export type DurationType = 'permanent' | 'timed' | 'conditional';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 持续时间配置
|
||||||
|
* @en Duration configuration
|
||||||
|
*/
|
||||||
|
export interface IEffectDuration {
|
||||||
|
/**
|
||||||
|
* @zh 持续时间类型
|
||||||
|
* @en Duration type
|
||||||
|
*/
|
||||||
|
readonly type: DurationType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 持续时间(秒),仅 timed 类型有效
|
||||||
|
* @en Duration in seconds, only valid for timed type
|
||||||
|
*/
|
||||||
|
readonly duration?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 剩余时间(秒)
|
||||||
|
* @en Remaining time in seconds
|
||||||
|
*/
|
||||||
|
remainingTime?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 条件检查函数,仅 conditional 类型有效
|
||||||
|
* @en Condition check function, only valid for conditional type
|
||||||
|
*/
|
||||||
|
readonly condition?: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 叠加规则 | Stacking Rules
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 叠加规则类型
|
||||||
|
* @en Stacking rule type
|
||||||
|
*/
|
||||||
|
export type StackingRule = 'refresh' | 'stack' | 'independent' | 'replace' | 'ignore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 叠加配置
|
||||||
|
* @en Stacking configuration
|
||||||
|
*/
|
||||||
|
export interface IStackingConfig {
|
||||||
|
/**
|
||||||
|
* @zh 叠加规则
|
||||||
|
* @en Stacking rule
|
||||||
|
*/
|
||||||
|
readonly rule: StackingRule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 最大叠加层数(stack 规则)
|
||||||
|
* @en Maximum stack count (for stack rule)
|
||||||
|
*/
|
||||||
|
readonly maxStacks?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 每层效果强度倍率(stack 规则)
|
||||||
|
* @en Effect intensity multiplier per stack (for stack rule)
|
||||||
|
*/
|
||||||
|
readonly stackMultiplier?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 效果接口 | Effect Interface
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果定义
|
||||||
|
* @en Effect definition
|
||||||
|
*/
|
||||||
|
export interface IEffectDefinition {
|
||||||
|
/**
|
||||||
|
* @zh 效果类型 ID
|
||||||
|
* @en Effect type ID
|
||||||
|
*/
|
||||||
|
readonly typeId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 显示名称
|
||||||
|
* @en Display name
|
||||||
|
*/
|
||||||
|
readonly displayName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 描述
|
||||||
|
* @en Description
|
||||||
|
*/
|
||||||
|
readonly description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 图标
|
||||||
|
* @en Icon
|
||||||
|
*/
|
||||||
|
readonly icon?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 标签(用于分组、互斥、增强)
|
||||||
|
* @en Tags (for grouping, exclusion, enhancement)
|
||||||
|
*/
|
||||||
|
readonly tags: readonly string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 持续时间配置
|
||||||
|
* @en Duration configuration
|
||||||
|
*/
|
||||||
|
readonly duration: IEffectDuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 叠加配置
|
||||||
|
* @en Stacking configuration
|
||||||
|
*/
|
||||||
|
readonly stacking: IStackingConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 周期性触发间隔(秒),0 表示不周期触发
|
||||||
|
* @en Periodic trigger interval in seconds, 0 means no periodic trigger
|
||||||
|
*/
|
||||||
|
readonly tickInterval?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 互斥标签(拥有这些标签的效果会被移除)
|
||||||
|
* @en Exclusive tags (effects with these tags will be removed)
|
||||||
|
*/
|
||||||
|
readonly exclusiveTags?: readonly string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果优先级(用于处理顺序)
|
||||||
|
* @en Effect priority (for processing order)
|
||||||
|
*/
|
||||||
|
readonly priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果实例
|
||||||
|
* @en Effect instance
|
||||||
|
*/
|
||||||
|
export interface IEffectInstance {
|
||||||
|
/**
|
||||||
|
* @zh 实例唯一 ID
|
||||||
|
* @en Instance unique ID
|
||||||
|
*/
|
||||||
|
readonly instanceId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果定义
|
||||||
|
* @en Effect definition
|
||||||
|
*/
|
||||||
|
readonly definition: IEffectDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果来源(施加者 ID)
|
||||||
|
* @en Effect source (applier ID)
|
||||||
|
*/
|
||||||
|
readonly sourceId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 当前叠加层数
|
||||||
|
* @en Current stack count
|
||||||
|
*/
|
||||||
|
stacks: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 剩余时间(秒)
|
||||||
|
* @en Remaining time in seconds
|
||||||
|
*/
|
||||||
|
remainingTime: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 下次触发时间(秒)
|
||||||
|
* @en Next tick time in seconds
|
||||||
|
*/
|
||||||
|
nextTickTime: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果数据
|
||||||
|
* @en Effect data
|
||||||
|
*/
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果是否激活
|
||||||
|
* @en Whether the effect is active
|
||||||
|
*/
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 应用时间戳
|
||||||
|
* @en Application timestamp
|
||||||
|
*/
|
||||||
|
readonly appliedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 效果事件 | Effect Events
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果事件类型
|
||||||
|
* @en Effect event type
|
||||||
|
*/
|
||||||
|
export type EffectEventType = 'applied' | 'removed' | 'stacked' | 'refreshed' | 'ticked' | 'expired';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果事件
|
||||||
|
* @en Effect event
|
||||||
|
*/
|
||||||
|
export interface IEffectEvent {
|
||||||
|
/**
|
||||||
|
* @zh 事件类型
|
||||||
|
* @en Event type
|
||||||
|
*/
|
||||||
|
readonly type: EffectEventType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果实例
|
||||||
|
* @en Effect instance
|
||||||
|
*/
|
||||||
|
readonly effect: IEffectInstance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 目标实体 ID
|
||||||
|
* @en Target entity ID
|
||||||
|
*/
|
||||||
|
readonly targetId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 事件时间戳
|
||||||
|
* @en Event timestamp
|
||||||
|
*/
|
||||||
|
readonly timestamp: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 额外数据
|
||||||
|
* @en Extra data
|
||||||
|
*/
|
||||||
|
readonly data?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果事件监听器
|
||||||
|
* @en Effect event listener
|
||||||
|
*/
|
||||||
|
export type EffectEventListener = (event: IEffectEvent) => void;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 效果处理器 | Effect Handler
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果处理器接口
|
||||||
|
* @en Effect handler interface
|
||||||
|
*/
|
||||||
|
export interface IEffectHandler<TTarget = unknown> {
|
||||||
|
/**
|
||||||
|
* @zh 效果应用时调用
|
||||||
|
* @en Called when effect is applied
|
||||||
|
*/
|
||||||
|
onApply?(effect: IEffectInstance, target: TTarget): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果移除时调用
|
||||||
|
* @en Called when effect is removed
|
||||||
|
*/
|
||||||
|
onRemove?(effect: IEffectInstance, target: TTarget): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果叠加时调用
|
||||||
|
* @en Called when effect is stacked
|
||||||
|
*/
|
||||||
|
onStack?(effect: IEffectInstance, target: TTarget, newStacks: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果刷新时调用
|
||||||
|
* @en Called when effect is refreshed
|
||||||
|
*/
|
||||||
|
onRefresh?(effect: IEffectInstance, target: TTarget): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果周期触发时调用
|
||||||
|
* @en Called on periodic tick
|
||||||
|
*/
|
||||||
|
onTick?(effect: IEffectInstance, target: TTarget, deltaTime: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 效果更新时调用(每帧)
|
||||||
|
* @en Called on update (every frame)
|
||||||
|
*/
|
||||||
|
onUpdate?(effect: IEffectInstance, target: TTarget, deltaTime: number): void;
|
||||||
|
}
|
||||||
19
packages/effect/src/core/index.ts
Normal file
19
packages/effect/src/core/index.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* @zh 效果核心模块
|
||||||
|
* @en Effect Core Module
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type {
|
||||||
|
DurationType,
|
||||||
|
IEffectDuration,
|
||||||
|
StackingRule,
|
||||||
|
IStackingConfig,
|
||||||
|
IEffectDefinition,
|
||||||
|
IEffectInstance,
|
||||||
|
EffectEventType,
|
||||||
|
IEffectEvent,
|
||||||
|
EffectEventListener,
|
||||||
|
IEffectHandler
|
||||||
|
} from './IEffect';
|
||||||
|
|
||||||
|
export { EffectContainer, createEffectContainer } from './EffectContainer';
|
||||||
80
packages/effect/src/index.ts
Normal file
80
packages/effect/src/index.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* @esengine/effect
|
||||||
|
*
|
||||||
|
* @zh 效果系统
|
||||||
|
* @en Effect System
|
||||||
|
*
|
||||||
|
* @zh 提供 Buff/Debuff 效果管理和属性修改器
|
||||||
|
* @en Provides Buff/Debuff effect management and attribute modifiers
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Core | 核心
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export type {
|
||||||
|
DurationType,
|
||||||
|
IEffectDuration,
|
||||||
|
StackingRule,
|
||||||
|
IStackingConfig,
|
||||||
|
IEffectDefinition,
|
||||||
|
IEffectInstance,
|
||||||
|
EffectEventType,
|
||||||
|
IEffectEvent,
|
||||||
|
EffectEventListener,
|
||||||
|
IEffectHandler
|
||||||
|
} from './core';
|
||||||
|
|
||||||
|
export { EffectContainer, createEffectContainer } from './core';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Modifiers | 修改器
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ModifierOperation,
|
||||||
|
ModifierPriority,
|
||||||
|
IModifier,
|
||||||
|
IAttributeCalculator
|
||||||
|
} from './modifiers';
|
||||||
|
|
||||||
|
export {
|
||||||
|
NumericCalculator,
|
||||||
|
ModifierContainer,
|
||||||
|
createModifierContainer
|
||||||
|
} from './modifiers';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Blueprint Nodes | 蓝图节点
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export {
|
||||||
|
// Templates
|
||||||
|
ApplyEffectTemplate,
|
||||||
|
RemoveEffectTemplate,
|
||||||
|
RemoveEffectByTagTemplate,
|
||||||
|
HasEffectTemplate,
|
||||||
|
HasEffectTagTemplate,
|
||||||
|
GetEffectStacksTemplate,
|
||||||
|
GetEffectRemainingTimeTemplate,
|
||||||
|
GetEffectCountTemplate,
|
||||||
|
ClearAllEffectsTemplate,
|
||||||
|
OnEffectAppliedTemplate,
|
||||||
|
OnEffectRemovedTemplate,
|
||||||
|
OnEffectTickTemplate,
|
||||||
|
// Executors
|
||||||
|
ApplyEffectExecutor,
|
||||||
|
RemoveEffectExecutor,
|
||||||
|
RemoveEffectByTagExecutor,
|
||||||
|
HasEffectExecutor,
|
||||||
|
HasEffectTagExecutor,
|
||||||
|
GetEffectStacksExecutor,
|
||||||
|
GetEffectRemainingTimeExecutor,
|
||||||
|
GetEffectCountExecutor,
|
||||||
|
ClearAllEffectsExecutor,
|
||||||
|
OnEffectAppliedExecutor,
|
||||||
|
OnEffectRemovedExecutor,
|
||||||
|
OnEffectTickExecutor,
|
||||||
|
// Collection
|
||||||
|
EffectNodeDefinitions
|
||||||
|
} from './nodes';
|
||||||
84
packages/effect/src/modifiers/IModifier.ts
Normal file
84
packages/effect/src/modifiers/IModifier.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* @zh 修改器接口定义
|
||||||
|
* @en Modifier Interface Definitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 修改器类型 | Modifier Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 修改器操作类型
|
||||||
|
* @en Modifier operation type
|
||||||
|
*/
|
||||||
|
export type ModifierOperation = 'add' | 'multiply' | 'override' | 'min' | 'max';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 修改器优先级
|
||||||
|
* @en Modifier priority
|
||||||
|
*/
|
||||||
|
export type ModifierPriority = 'base' | 'add' | 'multiply' | 'final';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 修改器接口
|
||||||
|
* @en Modifier interface
|
||||||
|
*/
|
||||||
|
export interface IModifier<T = number> {
|
||||||
|
/**
|
||||||
|
* @zh 修改器 ID
|
||||||
|
* @en Modifier ID
|
||||||
|
*/
|
||||||
|
readonly id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 修改器来源(效果实例 ID)
|
||||||
|
* @en Modifier source (effect instance ID)
|
||||||
|
*/
|
||||||
|
readonly sourceId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 修改的属性名
|
||||||
|
* @en Modified attribute name
|
||||||
|
*/
|
||||||
|
readonly attribute: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 操作类型
|
||||||
|
* @en Operation type
|
||||||
|
*/
|
||||||
|
readonly operation: ModifierOperation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 优先级
|
||||||
|
* @en Priority
|
||||||
|
*/
|
||||||
|
readonly priority: ModifierPriority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 修改值
|
||||||
|
* @en Modifier value
|
||||||
|
*/
|
||||||
|
value: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 是否激活
|
||||||
|
* @en Whether active
|
||||||
|
*/
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 属性值计算器
|
||||||
|
* @en Attribute value calculator
|
||||||
|
*/
|
||||||
|
export interface IAttributeCalculator<T = number> {
|
||||||
|
/**
|
||||||
|
* @zh 计算最终属性值
|
||||||
|
* @en Calculate final attribute value
|
||||||
|
*
|
||||||
|
* @param baseValue - @zh 基础值 @en Base value
|
||||||
|
* @param modifiers - @zh 修改器列表 @en Modifier list
|
||||||
|
* @returns @zh 最终值 @en Final value
|
||||||
|
*/
|
||||||
|
calculate(baseValue: T, modifiers: IModifier<T>[]): T;
|
||||||
|
}
|
||||||
304
packages/effect/src/modifiers/ModifierContainer.ts
Normal file
304
packages/effect/src/modifiers/ModifierContainer.ts
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
/**
|
||||||
|
* @zh 修改器容器
|
||||||
|
* @en Modifier Container
|
||||||
|
*
|
||||||
|
* @zh 管理属性修改器并计算最终值
|
||||||
|
* @en Manages attribute modifiers and calculates final values
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { IModifier, ModifierOperation, ModifierPriority, IAttributeCalculator } from './IModifier';
|
||||||
|
|
||||||
|
let modifierCounter = 0;
|
||||||
|
|
||||||
|
function generateModifierId(): string {
|
||||||
|
return `mod_${Date.now()}_${++modifierCounter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 默认数值计算器
|
||||||
|
* @en Default numeric calculator
|
||||||
|
*/
|
||||||
|
export class NumericCalculator implements IAttributeCalculator<number> {
|
||||||
|
calculate(baseValue: number, modifiers: IModifier<number>[]): number {
|
||||||
|
if (modifiers.length === 0) return baseValue;
|
||||||
|
|
||||||
|
// Sort by priority
|
||||||
|
const sorted = [...modifiers].sort((a, b) => {
|
||||||
|
const priorityOrder: Record<ModifierPriority, number> = {
|
||||||
|
base: 0,
|
||||||
|
add: 1,
|
||||||
|
multiply: 2,
|
||||||
|
final: 3
|
||||||
|
};
|
||||||
|
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
||||||
|
});
|
||||||
|
|
||||||
|
let value = baseValue;
|
||||||
|
let addSum = 0;
|
||||||
|
let multiplyProduct = 1;
|
||||||
|
|
||||||
|
for (const mod of sorted) {
|
||||||
|
if (!mod.isActive) continue;
|
||||||
|
|
||||||
|
switch (mod.operation) {
|
||||||
|
case 'add':
|
||||||
|
addSum += mod.value;
|
||||||
|
break;
|
||||||
|
case 'multiply':
|
||||||
|
multiplyProduct *= mod.value;
|
||||||
|
break;
|
||||||
|
case 'override':
|
||||||
|
value = mod.value;
|
||||||
|
addSum = 0;
|
||||||
|
multiplyProduct = 1;
|
||||||
|
break;
|
||||||
|
case 'min':
|
||||||
|
value = Math.min(value, mod.value);
|
||||||
|
break;
|
||||||
|
case 'max':
|
||||||
|
value = Math.max(value, mod.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply in order: base → add → multiply
|
||||||
|
return (value + addSum) * multiplyProduct;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 修改器容器
|
||||||
|
* @en Modifier container
|
||||||
|
*/
|
||||||
|
export class ModifierContainer<T = number> {
|
||||||
|
private readonly _modifiers: Map<string, IModifier<T>> = new Map();
|
||||||
|
private readonly _modifiersByAttribute: Map<string, Set<string>> = new Map();
|
||||||
|
private readonly _modifiersBySource: Map<string, Set<string>> = new Map();
|
||||||
|
private readonly _calculator: IAttributeCalculator<T>;
|
||||||
|
private readonly _baseValues: Map<string, T> = new Map();
|
||||||
|
private readonly _cachedValues: Map<string, T> = new Map();
|
||||||
|
private _isDirty = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 创建修改器容器
|
||||||
|
* @en Create modifier container
|
||||||
|
*
|
||||||
|
* @param calculator - @zh 属性计算器 @en Attribute calculator
|
||||||
|
*/
|
||||||
|
constructor(calculator?: IAttributeCalculator<T>) {
|
||||||
|
this._calculator = calculator ?? new NumericCalculator() as unknown as IAttributeCalculator<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 设置属性基础值
|
||||||
|
* @en Set attribute base value
|
||||||
|
*/
|
||||||
|
setBaseValue(attribute: string, value: T): void {
|
||||||
|
this._baseValues.set(attribute, value);
|
||||||
|
this._invalidateAttribute(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取属性基础值
|
||||||
|
* @en Get attribute base value
|
||||||
|
*/
|
||||||
|
getBaseValue(attribute: string): T | undefined {
|
||||||
|
return this._baseValues.get(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 添加修改器
|
||||||
|
* @en Add modifier
|
||||||
|
*/
|
||||||
|
addModifier(
|
||||||
|
attribute: string,
|
||||||
|
operation: ModifierOperation,
|
||||||
|
value: T,
|
||||||
|
sourceId: string,
|
||||||
|
priority: ModifierPriority = 'add'
|
||||||
|
): IModifier<T> {
|
||||||
|
const modifier: IModifier<T> = {
|
||||||
|
id: generateModifierId(),
|
||||||
|
sourceId,
|
||||||
|
attribute,
|
||||||
|
operation,
|
||||||
|
priority,
|
||||||
|
value,
|
||||||
|
isActive: true
|
||||||
|
};
|
||||||
|
|
||||||
|
this._modifiers.set(modifier.id, modifier);
|
||||||
|
|
||||||
|
// Index by attribute
|
||||||
|
if (!this._modifiersByAttribute.has(attribute)) {
|
||||||
|
this._modifiersByAttribute.set(attribute, new Set());
|
||||||
|
}
|
||||||
|
this._modifiersByAttribute.get(attribute)!.add(modifier.id);
|
||||||
|
|
||||||
|
// Index by source
|
||||||
|
if (!this._modifiersBySource.has(sourceId)) {
|
||||||
|
this._modifiersBySource.set(sourceId, new Set());
|
||||||
|
}
|
||||||
|
this._modifiersBySource.get(sourceId)!.add(modifier.id);
|
||||||
|
|
||||||
|
this._invalidateAttribute(attribute);
|
||||||
|
|
||||||
|
return modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 移除修改器
|
||||||
|
* @en Remove modifier
|
||||||
|
*/
|
||||||
|
removeModifier(modifierId: string): boolean {
|
||||||
|
const modifier = this._modifiers.get(modifierId);
|
||||||
|
if (!modifier) return false;
|
||||||
|
|
||||||
|
this._modifiers.delete(modifierId);
|
||||||
|
this._modifiersByAttribute.get(modifier.attribute)?.delete(modifierId);
|
||||||
|
this._modifiersBySource.get(modifier.sourceId)?.delete(modifierId);
|
||||||
|
|
||||||
|
this._invalidateAttribute(modifier.attribute);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 按来源移除修改器
|
||||||
|
* @en Remove modifiers by source
|
||||||
|
*/
|
||||||
|
removeBySource(sourceId: string): number {
|
||||||
|
const ids = this._modifiersBySource.get(sourceId);
|
||||||
|
if (!ids) return 0;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
for (const id of [...ids]) {
|
||||||
|
if (this.removeModifier(id)) count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._modifiersBySource.delete(sourceId);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 按属性移除修改器
|
||||||
|
* @en Remove modifiers by attribute
|
||||||
|
*/
|
||||||
|
removeByAttribute(attribute: string): number {
|
||||||
|
const ids = this._modifiersByAttribute.get(attribute);
|
||||||
|
if (!ids) return 0;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
for (const id of [...ids]) {
|
||||||
|
if (this.removeModifier(id)) count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取属性的所有修改器
|
||||||
|
* @en Get all modifiers for an attribute
|
||||||
|
*/
|
||||||
|
getModifiersForAttribute(attribute: string): IModifier<T>[] {
|
||||||
|
const ids = this._modifiersByAttribute.get(attribute);
|
||||||
|
if (!ids) return [];
|
||||||
|
return [...ids].map(id => this._modifiers.get(id)!).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取来源的所有修改器
|
||||||
|
* @en Get all modifiers from a source
|
||||||
|
*/
|
||||||
|
getModifiersFromSource(sourceId: string): IModifier<T>[] {
|
||||||
|
const ids = this._modifiersBySource.get(sourceId);
|
||||||
|
if (!ids) return [];
|
||||||
|
return [...ids].map(id => this._modifiers.get(id)!).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 计算属性最终值
|
||||||
|
* @en Calculate attribute final value
|
||||||
|
*/
|
||||||
|
getValue(attribute: string): T {
|
||||||
|
// Check cache
|
||||||
|
if (!this._isDirty && this._cachedValues.has(attribute)) {
|
||||||
|
return this._cachedValues.get(attribute)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseValue = this._baseValues.get(attribute);
|
||||||
|
if (baseValue === undefined) {
|
||||||
|
throw new Error(`No base value set for attribute: ${attribute}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiers = this.getModifiersForAttribute(attribute);
|
||||||
|
const finalValue = this._calculator.calculate(baseValue, modifiers);
|
||||||
|
|
||||||
|
this._cachedValues.set(attribute, finalValue);
|
||||||
|
return finalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 尝试获取属性最终值
|
||||||
|
* @en Try to get attribute final value
|
||||||
|
*/
|
||||||
|
tryGetValue(attribute: string, defaultValue: T): T {
|
||||||
|
try {
|
||||||
|
return this.getValue(attribute);
|
||||||
|
} catch {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 检查属性是否有修改器
|
||||||
|
* @en Check if attribute has modifiers
|
||||||
|
*/
|
||||||
|
hasModifiers(attribute: string): boolean {
|
||||||
|
const ids = this._modifiersByAttribute.get(attribute);
|
||||||
|
return ids !== undefined && ids.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取所有已修改的属性
|
||||||
|
* @en Get all modified attributes
|
||||||
|
*/
|
||||||
|
getModifiedAttributes(): string[] {
|
||||||
|
return [...this._modifiersByAttribute.keys()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 清除所有修改器
|
||||||
|
* @en Clear all modifiers
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this._modifiers.clear();
|
||||||
|
this._modifiersByAttribute.clear();
|
||||||
|
this._modifiersBySource.clear();
|
||||||
|
this._cachedValues.clear();
|
||||||
|
this._isDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _invalidateAttribute(attribute: string): void {
|
||||||
|
this._cachedValues.delete(attribute);
|
||||||
|
this._isDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 标记缓存已更新
|
||||||
|
* @en Mark cache as updated
|
||||||
|
*/
|
||||||
|
markClean(): void {
|
||||||
|
this._isDirty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 创建修改器容器
|
||||||
|
* @en Create modifier container
|
||||||
|
*/
|
||||||
|
export function createModifierContainer<T = number>(
|
||||||
|
calculator?: IAttributeCalculator<T>
|
||||||
|
): ModifierContainer<T> {
|
||||||
|
return new ModifierContainer(calculator);
|
||||||
|
}
|
||||||
17
packages/effect/src/modifiers/index.ts
Normal file
17
packages/effect/src/modifiers/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* @zh 修改器模块
|
||||||
|
* @en Modifier Module
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ModifierOperation,
|
||||||
|
ModifierPriority,
|
||||||
|
IModifier,
|
||||||
|
IAttributeCalculator
|
||||||
|
} from './IModifier';
|
||||||
|
|
||||||
|
export {
|
||||||
|
NumericCalculator,
|
||||||
|
ModifierContainer,
|
||||||
|
createModifierContainer
|
||||||
|
} from './ModifierContainer';
|
||||||
489
packages/effect/src/nodes/EffectNodes.ts
Normal file
489
packages/effect/src/nodes/EffectNodes.ts
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
/**
|
||||||
|
* @zh 效果系统蓝图节点
|
||||||
|
* @en Effect System Blueprint Nodes
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BlueprintNodeTemplate, BlueprintNode, INodeExecutor, ExecutionResult } from '@esengine/blueprint';
|
||||||
|
import type { EffectContainer } from '../core/EffectContainer';
|
||||||
|
import type { IEffectDefinition, IEffectInstance, EffectEventType } from '../core/IEffect';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 执行上下文接口 | Execution Context Interface
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
interface EffectContext {
|
||||||
|
entity: {
|
||||||
|
getComponent<T>(type: new (...args: unknown[]) => T): T | null;
|
||||||
|
};
|
||||||
|
getEffectContainer(): EffectContainer | null;
|
||||||
|
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
|
||||||
|
setOutputs(nodeId: string, outputs: Record<string, unknown>): void;
|
||||||
|
triggerOutput(nodeId: string, pinName: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ApplyEffect 节点 | ApplyEffect Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const ApplyEffectTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'ApplyEffect',
|
||||||
|
title: 'Apply Effect',
|
||||||
|
category: 'custom',
|
||||||
|
description: 'Apply an effect to target / 对目标应用效果',
|
||||||
|
keywords: ['effect', 'buff', 'debuff', 'apply', 'status'],
|
||||||
|
menuPath: ['Effect', 'Apply Effect'],
|
||||||
|
inputs: [
|
||||||
|
{ name: 'exec', displayName: '', type: 'exec' },
|
||||||
|
{ name: 'effectTypeId', displayName: 'Effect Type', type: 'string' },
|
||||||
|
{ name: 'duration', displayName: 'Duration', type: 'float' },
|
||||||
|
{ name: 'sourceId', displayName: 'Source ID', type: 'string' }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'exec', displayName: '', type: 'exec' },
|
||||||
|
{ name: 'success', displayName: 'Success', type: 'bool' },
|
||||||
|
{ name: 'instanceId', displayName: 'Instance ID', type: 'string' }
|
||||||
|
],
|
||||||
|
color: '#e91e63'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ApplyEffectExecutor implements INodeExecutor {
|
||||||
|
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
const ctx = context as EffectContext;
|
||||||
|
const effectTypeId = ctx.evaluateInput(node.id, 'effectTypeId', '') as string;
|
||||||
|
const duration = ctx.evaluateInput(node.id, 'duration', 0) as number;
|
||||||
|
const sourceId = ctx.evaluateInput(node.id, 'sourceId', '') as string;
|
||||||
|
|
||||||
|
const container = ctx.getEffectContainer();
|
||||||
|
if (!container || !effectTypeId) {
|
||||||
|
return {
|
||||||
|
outputs: { success: false, instanceId: '' },
|
||||||
|
nextExec: 'exec'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a basic effect definition
|
||||||
|
const definition: IEffectDefinition = {
|
||||||
|
typeId: effectTypeId,
|
||||||
|
displayName: effectTypeId,
|
||||||
|
tags: [],
|
||||||
|
duration: duration > 0
|
||||||
|
? { type: 'timed', duration, remainingTime: duration }
|
||||||
|
: { type: 'permanent' },
|
||||||
|
stacking: { rule: 'refresh' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = container.apply(definition, sourceId || undefined);
|
||||||
|
|
||||||
|
return {
|
||||||
|
outputs: {
|
||||||
|
success: instance !== null,
|
||||||
|
instanceId: instance?.instanceId ?? ''
|
||||||
|
},
|
||||||
|
nextExec: 'exec'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// RemoveEffect 节点 | RemoveEffect Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const RemoveEffectTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'RemoveEffect',
|
||||||
|
title: 'Remove Effect',
|
||||||
|
category: 'custom',
|
||||||
|
description: 'Remove effect from target / 从目标移除效果',
|
||||||
|
keywords: ['effect', 'remove', 'clear', 'dispel'],
|
||||||
|
menuPath: ['Effect', 'Remove Effect'],
|
||||||
|
inputs: [
|
||||||
|
{ name: 'exec', displayName: '', type: 'exec' },
|
||||||
|
{ name: 'effectTypeId', displayName: 'Effect Type', type: 'string' }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'exec', displayName: '', type: 'exec' },
|
||||||
|
{ name: 'removed', displayName: 'Removed', type: 'int' }
|
||||||
|
],
|
||||||
|
color: '#e91e63'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RemoveEffectExecutor implements INodeExecutor {
|
||||||
|
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
const ctx = context as EffectContext;
|
||||||
|
const effectTypeId = ctx.evaluateInput(node.id, 'effectTypeId', '') as string;
|
||||||
|
|
||||||
|
const container = ctx.getEffectContainer();
|
||||||
|
if (!container || !effectTypeId) {
|
||||||
|
return { outputs: { removed: 0 }, nextExec: 'exec' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const removed = container.removeByType(effectTypeId);
|
||||||
|
|
||||||
|
return { outputs: { removed }, nextExec: 'exec' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// RemoveEffectByTag 节点 | RemoveEffectByTag Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const RemoveEffectByTagTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'RemoveEffectByTag',
|
||||||
|
title: 'Remove Effect By Tag',
|
||||||
|
category: 'custom',
|
||||||
|
description: 'Remove effects with specific tag / 移除带有指定标签的效果',
|
||||||
|
keywords: ['effect', 'remove', 'tag', 'dispel'],
|
||||||
|
menuPath: ['Effect', 'Remove Effect By Tag'],
|
||||||
|
inputs: [
|
||||||
|
{ name: 'exec', displayName: '', type: 'exec' },
|
||||||
|
{ name: 'tag', displayName: 'Tag', type: 'string' }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'exec', displayName: '', type: 'exec' },
|
||||||
|
{ name: 'removed', displayName: 'Removed', type: 'int' }
|
||||||
|
],
|
||||||
|
color: '#e91e63'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RemoveEffectByTagExecutor implements INodeExecutor {
|
||||||
|
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
const ctx = context as EffectContext;
|
||||||
|
const tag = ctx.evaluateInput(node.id, 'tag', '') as string;
|
||||||
|
|
||||||
|
const container = ctx.getEffectContainer();
|
||||||
|
if (!container || !tag) {
|
||||||
|
return { outputs: { removed: 0 }, nextExec: 'exec' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const removed = container.removeByTag(tag);
|
||||||
|
|
||||||
|
return { outputs: { removed }, nextExec: 'exec' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HasEffect 节点 | HasEffect Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const HasEffectTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'HasEffect',
|
||||||
|
title: 'Has Effect',
|
||||||
|
category: 'custom',
|
||||||
|
description: 'Check if target has effect / 检查目标是否有效果',
|
||||||
|
keywords: ['effect', 'check', 'has', 'status'],
|
||||||
|
menuPath: ['Effect', 'Has Effect'],
|
||||||
|
isPure: true,
|
||||||
|
inputs: [
|
||||||
|
{ name: 'effectTypeId', displayName: 'Effect Type', type: 'string' }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'hasEffect', displayName: 'Has Effect', type: 'bool' }
|
||||||
|
],
|
||||||
|
color: '#e91e63'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class HasEffectExecutor implements INodeExecutor {
|
||||||
|
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
const ctx = context as EffectContext;
|
||||||
|
const effectTypeId = ctx.evaluateInput(node.id, 'effectTypeId', '') as string;
|
||||||
|
|
||||||
|
const container = ctx.getEffectContainer();
|
||||||
|
if (!container || !effectTypeId) {
|
||||||
|
return { outputs: { hasEffect: false } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasEffect = container.hasType(effectTypeId);
|
||||||
|
|
||||||
|
return { outputs: { hasEffect } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HasEffectTag 节点 | HasEffectTag Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const HasEffectTagTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'HasEffectTag',
|
||||||
|
title: 'Has Effect Tag',
|
||||||
|
category: 'custom',
|
||||||
|
description: 'Check if target has effect with tag / 检查目标是否有带标签的效果',
|
||||||
|
keywords: ['effect', 'check', 'tag', 'status'],
|
||||||
|
menuPath: ['Effect', 'Has Effect Tag'],
|
||||||
|
isPure: true,
|
||||||
|
inputs: [
|
||||||
|
{ name: 'tag', displayName: 'Tag', type: 'string' }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'hasTag', displayName: 'Has Tag', type: 'bool' }
|
||||||
|
],
|
||||||
|
color: '#e91e63'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class HasEffectTagExecutor implements INodeExecutor {
|
||||||
|
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
const ctx = context as EffectContext;
|
||||||
|
const tag = ctx.evaluateInput(node.id, 'tag', '') as string;
|
||||||
|
|
||||||
|
const container = ctx.getEffectContainer();
|
||||||
|
if (!container || !tag) {
|
||||||
|
return { outputs: { hasTag: false } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasTag = container.hasTag(tag);
|
||||||
|
|
||||||
|
return { outputs: { hasTag } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// GetEffectStacks 节点 | GetEffectStacks Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const GetEffectStacksTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'GetEffectStacks',
|
||||||
|
title: 'Get Effect Stacks',
|
||||||
|
category: 'custom',
|
||||||
|
description: 'Get effect stack count / 获取效果叠加层数',
|
||||||
|
keywords: ['effect', 'stacks', 'count', 'layers'],
|
||||||
|
menuPath: ['Effect', 'Get Effect Stacks'],
|
||||||
|
isPure: true,
|
||||||
|
inputs: [
|
||||||
|
{ name: 'effectTypeId', displayName: 'Effect Type', type: 'string' }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'stacks', displayName: 'Stacks', type: 'int' }
|
||||||
|
],
|
||||||
|
color: '#e91e63'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GetEffectStacksExecutor implements INodeExecutor {
|
||||||
|
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
const ctx = context as EffectContext;
|
||||||
|
const effectTypeId = ctx.evaluateInput(node.id, 'effectTypeId', '') as string;
|
||||||
|
|
||||||
|
const container = ctx.getEffectContainer();
|
||||||
|
if (!container || !effectTypeId) {
|
||||||
|
return { outputs: { stacks: 0 } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const stacks = container.getStacks(effectTypeId);
|
||||||
|
|
||||||
|
return { outputs: { stacks } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// GetEffectRemainingTime 节点 | GetEffectRemainingTime Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const GetEffectRemainingTimeTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'GetEffectRemainingTime',
|
||||||
|
title: 'Get Effect Remaining Time',
|
||||||
|
category: 'custom',
|
||||||
|
description: 'Get remaining time of effect / 获取效果剩余时间',
|
||||||
|
keywords: ['effect', 'time', 'remaining', 'duration'],
|
||||||
|
menuPath: ['Effect', 'Get Effect Remaining Time'],
|
||||||
|
isPure: true,
|
||||||
|
inputs: [
|
||||||
|
{ name: 'effectTypeId', displayName: 'Effect Type', type: 'string' }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'remainingTime', displayName: 'Remaining Time', type: 'float' },
|
||||||
|
{ name: 'hasEffect', displayName: 'Has Effect', type: 'bool' }
|
||||||
|
],
|
||||||
|
color: '#e91e63'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GetEffectRemainingTimeExecutor implements INodeExecutor {
|
||||||
|
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
const ctx = context as EffectContext;
|
||||||
|
const effectTypeId = ctx.evaluateInput(node.id, 'effectTypeId', '') as string;
|
||||||
|
|
||||||
|
const container = ctx.getEffectContainer();
|
||||||
|
if (!container || !effectTypeId) {
|
||||||
|
return { outputs: { remainingTime: 0, hasEffect: false } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const effects = container.getByType(effectTypeId);
|
||||||
|
if (effects.length === 0) {
|
||||||
|
return { outputs: { remainingTime: 0, hasEffect: false } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return max remaining time among all instances
|
||||||
|
const remainingTime = Math.max(...effects.map(e => e.remainingTime));
|
||||||
|
|
||||||
|
return { outputs: { remainingTime, hasEffect: true } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// GetEffectCount 节点 | GetEffectCount Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const GetEffectCountTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'GetEffectCount',
|
||||||
|
title: 'Get Effect Count',
|
||||||
|
category: 'custom',
|
||||||
|
description: 'Get total effect count / 获取效果总数',
|
||||||
|
keywords: ['effect', 'count', 'total'],
|
||||||
|
menuPath: ['Effect', 'Get Effect Count'],
|
||||||
|
isPure: true,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'count', displayName: 'Count', type: 'int' }
|
||||||
|
],
|
||||||
|
color: '#e91e63'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GetEffectCountExecutor implements INodeExecutor {
|
||||||
|
execute(_node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
const ctx = context as EffectContext;
|
||||||
|
const container = ctx.getEffectContainer();
|
||||||
|
|
||||||
|
return { outputs: { count: container?.count ?? 0 } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ClearAllEffects 节点 | ClearAllEffects Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const ClearAllEffectsTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'ClearAllEffects',
|
||||||
|
title: 'Clear All Effects',
|
||||||
|
category: 'custom',
|
||||||
|
description: 'Remove all effects from target / 移除目标所有效果',
|
||||||
|
keywords: ['effect', 'clear', 'remove', 'all'],
|
||||||
|
menuPath: ['Effect', 'Clear All Effects'],
|
||||||
|
inputs: [
|
||||||
|
{ name: 'exec', displayName: '', type: 'exec' }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'exec', displayName: '', type: 'exec' }
|
||||||
|
],
|
||||||
|
color: '#e91e63'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ClearAllEffectsExecutor implements INodeExecutor {
|
||||||
|
execute(_node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
const ctx = context as EffectContext;
|
||||||
|
const container = ctx.getEffectContainer();
|
||||||
|
container?.removeAll();
|
||||||
|
|
||||||
|
return { nextExec: 'exec' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// OnEffectApplied 事件节点 | OnEffectApplied Event Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const OnEffectAppliedTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'OnEffectApplied',
|
||||||
|
title: 'On Effect Applied',
|
||||||
|
category: 'event',
|
||||||
|
description: 'Triggered when effect is applied / 效果应用时触发',
|
||||||
|
keywords: ['effect', 'event', 'applied', 'add'],
|
||||||
|
menuPath: ['Effect', 'Events', 'On Effect Applied'],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'exec', displayName: '', type: 'exec' },
|
||||||
|
{ name: 'effectTypeId', displayName: 'Effect Type', type: 'string' },
|
||||||
|
{ name: 'instanceId', displayName: 'Instance ID', type: 'string' },
|
||||||
|
{ name: 'stacks', displayName: 'Stacks', type: 'int' }
|
||||||
|
],
|
||||||
|
color: '#ff5722'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class OnEffectAppliedExecutor implements INodeExecutor {
|
||||||
|
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
return { nextExec: 'exec' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// OnEffectRemoved 事件节点 | OnEffectRemoved Event Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const OnEffectRemovedTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'OnEffectRemoved',
|
||||||
|
title: 'On Effect Removed',
|
||||||
|
category: 'event',
|
||||||
|
description: 'Triggered when effect is removed / 效果移除时触发',
|
||||||
|
keywords: ['effect', 'event', 'removed', 'expire'],
|
||||||
|
menuPath: ['Effect', 'Events', 'On Effect Removed'],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'exec', displayName: '', type: 'exec' },
|
||||||
|
{ name: 'effectTypeId', displayName: 'Effect Type', type: 'string' },
|
||||||
|
{ name: 'instanceId', displayName: 'Instance ID', type: 'string' }
|
||||||
|
],
|
||||||
|
color: '#ff5722'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class OnEffectRemovedExecutor implements INodeExecutor {
|
||||||
|
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
return { nextExec: 'exec' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// OnEffectTick 事件节点 | OnEffectTick Event Node
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const OnEffectTickTemplate: BlueprintNodeTemplate = {
|
||||||
|
type: 'OnEffectTick',
|
||||||
|
title: 'On Effect Tick',
|
||||||
|
category: 'event',
|
||||||
|
description: 'Triggered on effect periodic tick / 效果周期触发时调用',
|
||||||
|
keywords: ['effect', 'event', 'tick', 'periodic'],
|
||||||
|
menuPath: ['Effect', 'Events', 'On Effect Tick'],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [
|
||||||
|
{ name: 'exec', displayName: '', type: 'exec' },
|
||||||
|
{ name: 'effectTypeId', displayName: 'Effect Type', type: 'string' },
|
||||||
|
{ name: 'instanceId', displayName: 'Instance ID', type: 'string' },
|
||||||
|
{ name: 'stacks', displayName: 'Stacks', type: 'int' }
|
||||||
|
],
|
||||||
|
color: '#ff5722'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class OnEffectTickExecutor implements INodeExecutor {
|
||||||
|
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||||
|
return { nextExec: 'exec' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 节点定义集合 | Node Definition Collection
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const EffectNodeDefinitions = {
|
||||||
|
templates: [
|
||||||
|
ApplyEffectTemplate,
|
||||||
|
RemoveEffectTemplate,
|
||||||
|
RemoveEffectByTagTemplate,
|
||||||
|
HasEffectTemplate,
|
||||||
|
HasEffectTagTemplate,
|
||||||
|
GetEffectStacksTemplate,
|
||||||
|
GetEffectRemainingTimeTemplate,
|
||||||
|
GetEffectCountTemplate,
|
||||||
|
ClearAllEffectsTemplate,
|
||||||
|
OnEffectAppliedTemplate,
|
||||||
|
OnEffectRemovedTemplate,
|
||||||
|
OnEffectTickTemplate
|
||||||
|
],
|
||||||
|
executors: new Map<string, INodeExecutor>([
|
||||||
|
['ApplyEffect', new ApplyEffectExecutor()],
|
||||||
|
['RemoveEffect', new RemoveEffectExecutor()],
|
||||||
|
['RemoveEffectByTag', new RemoveEffectByTagExecutor()],
|
||||||
|
['HasEffect', new HasEffectExecutor()],
|
||||||
|
['HasEffectTag', new HasEffectTagExecutor()],
|
||||||
|
['GetEffectStacks', new GetEffectStacksExecutor()],
|
||||||
|
['GetEffectRemainingTime', new GetEffectRemainingTimeExecutor()],
|
||||||
|
['GetEffectCount', new GetEffectCountExecutor()],
|
||||||
|
['ClearAllEffects', new ClearAllEffectsExecutor()],
|
||||||
|
['OnEffectApplied', new OnEffectAppliedExecutor()],
|
||||||
|
['OnEffectRemoved', new OnEffectRemovedExecutor()],
|
||||||
|
['OnEffectTick', new OnEffectTickExecutor()]
|
||||||
|
])
|
||||||
|
};
|
||||||
35
packages/effect/src/nodes/index.ts
Normal file
35
packages/effect/src/nodes/index.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* @zh 效果蓝图节点模块
|
||||||
|
* @en Effect Blueprint Nodes Module
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
// Templates
|
||||||
|
ApplyEffectTemplate,
|
||||||
|
RemoveEffectTemplate,
|
||||||
|
RemoveEffectByTagTemplate,
|
||||||
|
HasEffectTemplate,
|
||||||
|
HasEffectTagTemplate,
|
||||||
|
GetEffectStacksTemplate,
|
||||||
|
GetEffectRemainingTimeTemplate,
|
||||||
|
GetEffectCountTemplate,
|
||||||
|
ClearAllEffectsTemplate,
|
||||||
|
OnEffectAppliedTemplate,
|
||||||
|
OnEffectRemovedTemplate,
|
||||||
|
OnEffectTickTemplate,
|
||||||
|
// Executors
|
||||||
|
ApplyEffectExecutor,
|
||||||
|
RemoveEffectExecutor,
|
||||||
|
RemoveEffectByTagExecutor,
|
||||||
|
HasEffectExecutor,
|
||||||
|
HasEffectTagExecutor,
|
||||||
|
GetEffectStacksExecutor,
|
||||||
|
GetEffectRemainingTimeExecutor,
|
||||||
|
GetEffectCountExecutor,
|
||||||
|
ClearAllEffectsExecutor,
|
||||||
|
OnEffectAppliedExecutor,
|
||||||
|
OnEffectRemovedExecutor,
|
||||||
|
OnEffectTickExecutor,
|
||||||
|
// Collection
|
||||||
|
EffectNodeDefinitions
|
||||||
|
} from './EffectNodes';
|
||||||
22
packages/effect/tsconfig.build.json
Normal file
22
packages/effect/tsconfig.build.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ES2020",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
14
packages/effect/tsconfig.json
Normal file
14
packages/effect/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../core" },
|
||||||
|
{ "path": "../blueprint" }
|
||||||
|
]
|
||||||
|
}
|
||||||
12
packages/effect/tsup.config.ts
Normal file
12
packages/effect/tsup.config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['esm'],
|
||||||
|
dts: true,
|
||||||
|
splitting: false,
|
||||||
|
sourcemap: true,
|
||||||
|
clean: true,
|
||||||
|
treeshake: true,
|
||||||
|
tsconfig: 'tsconfig.build.json'
|
||||||
|
});
|
||||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -923,6 +923,34 @@ importers:
|
|||||||
specifier: ^5.0.8
|
specifier: ^5.0.8
|
||||||
version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1))
|
version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1))
|
||||||
|
|
||||||
|
packages/effect:
|
||||||
|
dependencies:
|
||||||
|
tslib:
|
||||||
|
specifier: ^2.8.1
|
||||||
|
version: 2.8.1
|
||||||
|
devDependencies:
|
||||||
|
'@esengine/blueprint':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../blueprint
|
||||||
|
'@esengine/build-config':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../build-config
|
||||||
|
'@esengine/ecs-framework':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^20.19.17
|
||||||
|
version: 20.19.25
|
||||||
|
rimraf:
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.0.10
|
||||||
|
tsup:
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1)
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.8.3
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
packages/engine:
|
packages/engine:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
rimraf:
|
rimraf:
|
||||||
|
|||||||
Reference in New Issue
Block a user