refactor: reorganize package structure and decouple framework packages (#338)

* 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
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View 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"
}

View 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"
}
}

View 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);
}

View 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;
}

View 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';

View 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';

View 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;
}

View 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);
}

View File

@@ -0,0 +1,17 @@
/**
* @zh 修改器模块
* @en Modifier Module
*/
export type {
ModifierOperation,
ModifierPriority,
IModifier,
IAttributeCalculator
} from './IModifier';
export {
NumericCalculator,
ModifierContainer,
createModifierContainer
} from './ModifierContainer';

View 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()]
])
};

View 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';

View 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"]
}

View File

@@ -0,0 +1,23 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
],
"references": [
{
"path": "../../framework/core"
},
{
"path": "../../framework/blueprint"
}
]
}

View 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'
});