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,23 @@
{
"id": "timer",
"name": "@esengine/timer",
"globalKey": "timer",
"displayName": "Timer & Cooldown",
"description": "定时器和冷却系统 | Timer and cooldown system",
"version": "1.0.0",
"category": "Other",
"icon": "Timer",
"tags": ["timer", "cooldown", "delay", "schedule"],
"isCore": false,
"defaultEnabled": true,
"isEngineModule": true,
"canContainContent": false,
"platforms": ["web", "desktop"],
"dependencies": ["core"],
"exports": {
"components": [],
"systems": []
},
"outputPath": "dist/index.js",
"pluginExport": "TimerPlugin"
}

View File

@@ -0,0 +1,40 @@
{
"name": "@esengine/timer",
"version": "1.0.0",
"description": "Timer and cooldown 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,294 @@
/**
* @zh 定时器服务接口
* @en Timer Service Interfaces
*
* @zh 提供定时器和冷却系统的核心接口
* @en Provides core interfaces for timer and cooldown systems
*/
// =============================================================================
// 定时器句柄 | Timer Handle
// =============================================================================
/**
* @zh 定时器句柄,用于取消定时器
* @en Timer handle for cancelling timers
*/
export interface TimerHandle {
/**
* @zh 定时器 ID
* @en Timer ID
*/
readonly id: string;
/**
* @zh 是否有效(未被取消)
* @en Whether the timer is still valid (not cancelled)
*/
readonly isValid: boolean;
/**
* @zh 取消定时器
* @en Cancel the timer
*/
cancel(): void;
}
// =============================================================================
// 定时器信息 | Timer Info
// =============================================================================
/**
* @zh 定时器信息
* @en Timer information
*/
export interface TimerInfo {
/**
* @zh 定时器 ID
* @en Timer ID
*/
readonly id: string;
/**
* @zh 剩余时间(毫秒)
* @en Remaining time in milliseconds
*/
readonly remaining: number;
/**
* @zh 是否重复执行
* @en Whether the timer repeats
*/
readonly repeating: boolean;
/**
* @zh 间隔时间(毫秒,仅重复定时器)
* @en Interval in milliseconds (only for repeating timers)
*/
readonly interval?: number;
}
// =============================================================================
// 冷却信息 | Cooldown Info
// =============================================================================
/**
* @zh 冷却信息
* @en Cooldown information
*/
export interface CooldownInfo {
/**
* @zh 冷却 ID
* @en Cooldown ID
*/
readonly id: string;
/**
* @zh 总持续时间(毫秒)
* @en Total duration in milliseconds
*/
readonly duration: number;
/**
* @zh 剩余时间(毫秒)
* @en Remaining time in milliseconds
*/
readonly remaining: number;
/**
* @zh 进度0-10 表示刚开始1 表示结束)
* @en Progress from 0 to 1 (0 = just started, 1 = finished)
*/
readonly progress: number;
/**
* @zh 是否已就绪(冷却完成)
* @en Whether the cooldown is ready (finished)
*/
readonly isReady: boolean;
}
// =============================================================================
// 定时器回调 | Timer Callbacks
// =============================================================================
/**
* @zh 定时器回调函数
* @en Timer callback function
*/
export type TimerCallback = () => void;
/**
* @zh 带时间参数的定时器回调
* @en Timer callback with time parameter
*/
export type TimerCallbackWithTime = (deltaTime: number) => void;
// =============================================================================
// 定时器服务接口 | Timer Service Interface
// =============================================================================
/**
* @zh 定时器服务接口
* @en Timer service interface
*
* @zh 提供定时器调度和冷却管理功能
* @en Provides timer scheduling and cooldown management
*/
export interface ITimerService {
// =========================================================================
// 定时器 API | Timer API
// =========================================================================
/**
* @zh 调度一次性定时器
* @en Schedule a one-time timer
*
* @param id - @zh 定时器标识 @en Timer identifier
* @param delay - @zh 延迟时间(毫秒)@en Delay in milliseconds
* @param callback - @zh 回调函数 @en Callback function
* @returns @zh 定时器句柄 @en Timer handle
*/
schedule(id: string, delay: number, callback: TimerCallback): TimerHandle;
/**
* @zh 调度重复定时器
* @en Schedule a repeating timer
*
* @param id - @zh 定时器标识 @en Timer identifier
* @param interval - @zh 间隔时间(毫秒)@en Interval in milliseconds
* @param callback - @zh 回调函数 @en Callback function
* @param immediate - @zh 是否立即执行一次 @en Whether to execute immediately
* @returns @zh 定时器句柄 @en Timer handle
*/
scheduleRepeating(
id: string,
interval: number,
callback: TimerCallback,
immediate?: boolean
): TimerHandle;
/**
* @zh 取消定时器
* @en Cancel a timer
*
* @param handle - @zh 定时器句柄 @en Timer handle
*/
cancel(handle: TimerHandle): void;
/**
* @zh 通过 ID 取消定时器
* @en Cancel timer by ID
*
* @param id - @zh 定时器标识 @en Timer identifier
*/
cancelById(id: string): void;
/**
* @zh 检查定时器是否存在
* @en Check if a timer exists
*
* @param id - @zh 定时器标识 @en Timer identifier
* @returns @zh 是否存在 @en Whether the timer exists
*/
hasTimer(id: string): boolean;
/**
* @zh 获取定时器信息
* @en Get timer information
*
* @param id - @zh 定时器标识 @en Timer identifier
* @returns @zh 定时器信息或 null @en Timer info or null
*/
getTimerInfo(id: string): TimerInfo | null;
// =========================================================================
// 冷却 API | Cooldown API
// =========================================================================
/**
* @zh 开始冷却
* @en Start a cooldown
*
* @param id - @zh 冷却标识 @en Cooldown identifier
* @param duration - @zh 持续时间(毫秒)@en Duration in milliseconds
*/
startCooldown(id: string, duration: number): void;
/**
* @zh 检查是否在冷却中
* @en Check if on cooldown
*
* @param id - @zh 冷却标识 @en Cooldown identifier
* @returns @zh 是否在冷却中 @en Whether on cooldown
*/
isOnCooldown(id: string): boolean;
/**
* @zh 检查冷却是否就绪
* @en Check if cooldown is ready
*
* @param id - @zh 冷却标识 @en Cooldown identifier
* @returns @zh 是否已就绪 @en Whether ready
*/
isCooldownReady(id: string): boolean;
/**
* @zh 获取剩余冷却时间
* @en Get remaining cooldown time
*
* @param id - @zh 冷却标识 @en Cooldown identifier
* @returns @zh 剩余时间毫秒0 表示无冷却 @en Remaining time in ms, 0 if no cooldown
*/
getCooldownRemaining(id: string): number;
/**
* @zh 获取冷却进度
* @en Get cooldown progress
*
* @param id - @zh 冷却标识 @en Cooldown identifier
* @returns @zh 进度0-11 表示完成或无冷却 @en Progress 0-1, 1 if done or no cooldown
*/
getCooldownProgress(id: string): number;
/**
* @zh 获取冷却信息
* @en Get cooldown information
*
* @param id - @zh 冷却标识 @en Cooldown identifier
* @returns @zh 冷却信息或 null @en Cooldown info or null
*/
getCooldownInfo(id: string): CooldownInfo | null;
/**
* @zh 重置冷却
* @en Reset a cooldown
*
* @param id - @zh 冷却标识 @en Cooldown identifier
*/
resetCooldown(id: string): void;
/**
* @zh 清除所有冷却
* @en Clear all cooldowns
*/
clearAllCooldowns(): void;
// =========================================================================
// 更新 | Update
// =========================================================================
/**
* @zh 更新定时器服务
* @en Update timer service
*
* @param deltaTime - @zh 距上次更新的时间(毫秒)@en Time since last update in ms
*/
update(deltaTime: number): void;
/**
* @zh 清除所有定时器和冷却
* @en Clear all timers and cooldowns
*/
clear(): void;
}

View File

@@ -0,0 +1,411 @@
/**
* @zh 定时器服务实现
* @en Timer Service Implementation
*
* @zh 提供定时器调度和冷却管理的默认实现
* @en Provides default implementation for timer scheduling and cooldown management
*/
import type {
ITimerService,
TimerHandle,
TimerInfo,
TimerCallback,
CooldownInfo
} from './ITimerService';
// =============================================================================
// 内部类型 | Internal Types
// =============================================================================
/**
* @zh 内部定时器数据
* @en Internal timer data
*/
interface InternalTimer {
id: string;
callback: TimerCallback;
remaining: number;
repeating: boolean;
interval: number;
cancelled: boolean;
}
/**
* @zh 内部冷却数据
* @en Internal cooldown data
*/
interface InternalCooldown {
id: string;
duration: number;
remaining: number;
}
// =============================================================================
// 定时器句柄实现 | Timer Handle Implementation
// =============================================================================
/**
* @zh 定时器句柄实现
* @en Timer handle implementation
*/
class TimerHandleImpl implements TimerHandle {
private timer: InternalTimer;
constructor(timer: InternalTimer) {
this.timer = timer;
}
get id(): string {
return this.timer.id;
}
get isValid(): boolean {
return !this.timer.cancelled;
}
cancel(): void {
this.timer.cancelled = true;
}
}
// =============================================================================
// 定时器服务实现 | Timer Service Implementation
// =============================================================================
/**
* @zh 定时器服务配置
* @en Timer service configuration
*/
export interface TimerServiceConfig {
/**
* @zh 最大定时器数量0 表示无限制)
* @en Maximum number of timers (0 for unlimited)
*/
maxTimers?: number;
/**
* @zh 最大冷却数量0 表示无限制)
* @en Maximum number of cooldowns (0 for unlimited)
*/
maxCooldowns?: number;
}
/**
* @zh 定时器服务实现
* @en Timer service implementation
*
* @example
* ```typescript
* const timerService = new TimerService();
*
* // 一次性定时器 | One-time timer
* const handle = timerService.schedule('myTimer', 1000, () => {
* console.log('Timer fired!');
* });
*
* // 重复定时器 | Repeating timer
* timerService.scheduleRepeating('heartbeat', 100, () => {
* console.log('Tick');
* });
*
* // 冷却系统 | Cooldown system
* timerService.startCooldown('skill_fireball', 5000);
* if (timerService.isCooldownReady('skill_fireball')) {
* // 可以使用技能 | Can use skill
* }
*
* // 每帧更新 | Update each frame
* timerService.update(deltaTime);
* ```
*/
export class TimerService implements ITimerService {
private timers: Map<string, InternalTimer> = new Map();
private cooldowns: Map<string, InternalCooldown> = new Map();
private config: Required<TimerServiceConfig>;
constructor(config: TimerServiceConfig = {}) {
this.config = {
maxTimers: config.maxTimers ?? 0,
maxCooldowns: config.maxCooldowns ?? 0
};
}
// =========================================================================
// 定时器 API | Timer API
// =========================================================================
schedule(id: string, delay: number, callback: TimerCallback): TimerHandle {
this.cancelById(id);
if (this.config.maxTimers > 0 && this.timers.size >= this.config.maxTimers) {
throw new Error(`Maximum timer limit reached: ${this.config.maxTimers}`);
}
const timer: InternalTimer = {
id,
callback,
remaining: Math.max(0, delay),
repeating: false,
interval: 0,
cancelled: false
};
this.timers.set(id, timer);
return new TimerHandleImpl(timer);
}
scheduleRepeating(
id: string,
interval: number,
callback: TimerCallback,
immediate = false
): TimerHandle {
this.cancelById(id);
if (this.config.maxTimers > 0 && this.timers.size >= this.config.maxTimers) {
throw new Error(`Maximum timer limit reached: ${this.config.maxTimers}`);
}
const safeInterval = Math.max(1, interval);
const timer: InternalTimer = {
id,
callback,
remaining: immediate ? 0 : safeInterval,
repeating: true,
interval: safeInterval,
cancelled: false
};
this.timers.set(id, timer);
return new TimerHandleImpl(timer);
}
cancel(handle: TimerHandle): void {
handle.cancel();
this.timers.delete(handle.id);
}
cancelById(id: string): void {
const timer = this.timers.get(id);
if (timer) {
timer.cancelled = true;
this.timers.delete(id);
}
}
hasTimer(id: string): boolean {
const timer = this.timers.get(id);
return timer !== undefined && !timer.cancelled;
}
getTimerInfo(id: string): TimerInfo | null {
const timer = this.timers.get(id);
if (!timer || timer.cancelled) {
return null;
}
return {
id: timer.id,
remaining: timer.remaining,
repeating: timer.repeating,
interval: timer.repeating ? timer.interval : undefined
};
}
// =========================================================================
// 冷却 API | Cooldown API
// =========================================================================
startCooldown(id: string, duration: number): void {
if (this.config.maxCooldowns > 0 && !this.cooldowns.has(id)) {
if (this.cooldowns.size >= this.config.maxCooldowns) {
throw new Error(`Maximum cooldown limit reached: ${this.config.maxCooldowns}`);
}
}
const safeDuration = Math.max(0, duration);
this.cooldowns.set(id, {
id,
duration: safeDuration,
remaining: safeDuration
});
}
isOnCooldown(id: string): boolean {
const cooldown = this.cooldowns.get(id);
return cooldown !== undefined && cooldown.remaining > 0;
}
isCooldownReady(id: string): boolean {
return !this.isOnCooldown(id);
}
getCooldownRemaining(id: string): number {
const cooldown = this.cooldowns.get(id);
return cooldown ? Math.max(0, cooldown.remaining) : 0;
}
getCooldownProgress(id: string): number {
const cooldown = this.cooldowns.get(id);
if (!cooldown || cooldown.duration <= 0) {
return 1;
}
const elapsed = cooldown.duration - cooldown.remaining;
return Math.min(1, Math.max(0, elapsed / cooldown.duration));
}
getCooldownInfo(id: string): CooldownInfo | null {
const cooldown = this.cooldowns.get(id);
if (!cooldown) {
return null;
}
const remaining = Math.max(0, cooldown.remaining);
const progress = cooldown.duration > 0
? Math.min(1, (cooldown.duration - remaining) / cooldown.duration)
: 1;
return {
id: cooldown.id,
duration: cooldown.duration,
remaining,
progress,
isReady: remaining <= 0
};
}
resetCooldown(id: string): void {
this.cooldowns.delete(id);
}
clearAllCooldowns(): void {
this.cooldowns.clear();
}
// =========================================================================
// 更新 | Update
// =========================================================================
update(deltaTime: number): void {
if (deltaTime <= 0) {
return;
}
this.updateTimers(deltaTime);
this.updateCooldowns(deltaTime);
}
private updateTimers(deltaTime: number): void {
const toRemove: string[] = [];
for (const [id, timer] of this.timers) {
if (timer.cancelled) {
toRemove.push(id);
continue;
}
timer.remaining -= deltaTime;
if (timer.remaining <= 0) {
try {
timer.callback();
} catch (error) {
console.error(`Timer callback error [${id}]:`, error);
}
if (timer.repeating && !timer.cancelled) {
timer.remaining += timer.interval;
if (timer.remaining < 0) {
timer.remaining = timer.interval;
}
} else {
timer.cancelled = true;
toRemove.push(id);
}
}
}
for (const id of toRemove) {
this.timers.delete(id);
}
}
private updateCooldowns(deltaTime: number): void {
const toRemove: string[] = [];
for (const [id, cooldown] of this.cooldowns) {
cooldown.remaining -= deltaTime;
if (cooldown.remaining <= 0) {
toRemove.push(id);
}
}
for (const id of toRemove) {
this.cooldowns.delete(id);
}
}
clear(): void {
for (const timer of this.timers.values()) {
timer.cancelled = true;
}
this.timers.clear();
this.cooldowns.clear();
}
// =========================================================================
// 调试 | Debug
// =========================================================================
/**
* @zh 获取活跃定时器数量
* @en Get active timer count
*/
get activeTimerCount(): number {
return this.timers.size;
}
/**
* @zh 获取活跃冷却数量
* @en Get active cooldown count
*/
get activeCooldownCount(): number {
return this.cooldowns.size;
}
/**
* @zh 获取所有活跃定时器 ID
* @en Get all active timer IDs
*/
getActiveTimerIds(): string[] {
return Array.from(this.timers.keys());
}
/**
* @zh 获取所有活跃冷却 ID
* @en Get all active cooldown IDs
*/
getActiveCooldownIds(): string[] {
return Array.from(this.cooldowns.keys());
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh 创建定时器服务
* @en Create timer service
*
* @param config - @zh 配置选项 @en Configuration options
* @returns @zh 定时器服务实例 @en Timer service instance
*/
export function createTimerService(config?: TimerServiceConfig): ITimerService {
return new TimerService(config);
}

View File

@@ -0,0 +1,60 @@
/**
* @zh @esengine/timer - 定时器和冷却系统
* @en @esengine/timer - Timer and Cooldown System
*
* @zh 提供定时器调度和冷却管理功能
* @en Provides timer scheduling and cooldown management
*/
// =============================================================================
// 接口和类型 | Interfaces and Types
// =============================================================================
export type {
TimerHandle,
TimerInfo,
CooldownInfo,
TimerCallback,
TimerCallbackWithTime,
ITimerService
} from './ITimerService';
// =============================================================================
// 实现 | Implementations
// =============================================================================
export type { TimerServiceConfig } from './TimerService';
export { TimerService, createTimerService } from './TimerService';
// =============================================================================
// 服务令牌 | Service Tokens
// =============================================================================
export { TimerServiceToken } from './tokens';
// =============================================================================
// 蓝图节点 | Blueprint Nodes
// =============================================================================
export {
// Templates
StartCooldownTemplate,
IsCooldownReadyTemplate,
GetCooldownProgressTemplate,
ResetCooldownTemplate,
GetCooldownInfoTemplate,
HasTimerTemplate,
CancelTimerTemplate,
GetTimerRemainingTemplate,
// Executors
StartCooldownExecutor,
IsCooldownReadyExecutor,
GetCooldownProgressExecutor,
ResetCooldownExecutor,
GetCooldownInfoExecutor,
HasTimerExecutor,
CancelTimerExecutor,
GetTimerRemainingExecutor,
// Collection
TimerNodeDefinitions
} from './nodes';

View File

@@ -0,0 +1,543 @@
/**
* @zh 定时器蓝图节点
* @en Timer Blueprint Nodes
*
* @zh 提供定时器和冷却功能的蓝图节点
* @en Provides blueprint nodes for timer and cooldown functionality
*/
import type { BlueprintNodeTemplate, BlueprintNode, INodeExecutor, ExecutionResult } from '@esengine/blueprint';
import type { ITimerService } from '../ITimerService';
// =============================================================================
// 执行上下文接口 | Execution Context Interface
// =============================================================================
/**
* @zh 定时器上下文
* @en Timer context
*/
interface TimerContext {
timerService: ITimerService;
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
setOutputs(nodeId: string, outputs: Record<string, unknown>): void;
}
// =============================================================================
// StartCooldown 节点 | StartCooldown Node
// =============================================================================
/**
* @zh StartCooldown 节点模板
* @en StartCooldown node template
*/
export const StartCooldownTemplate: BlueprintNodeTemplate = {
type: 'StartCooldown',
title: 'Start Cooldown',
category: 'time',
description: 'Start a cooldown timer / 开始冷却计时',
keywords: ['timer', 'cooldown', 'start', 'delay'],
menuPath: ['Timer', 'Start Cooldown'],
isPure: false,
inputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
},
{
name: 'id',
displayName: 'Cooldown ID',
type: 'string',
defaultValue: ''
},
{
name: 'duration',
displayName: 'Duration (ms)',
type: 'float',
defaultValue: 1000
}
],
outputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
}
],
color: '#00bcd4'
};
/**
* @zh StartCooldown 节点执行器
* @en StartCooldown node executor
*/
export class StartCooldownExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as TimerContext;
const id = ctx.evaluateInput(node.id, 'id', '') as string;
const duration = ctx.evaluateInput(node.id, 'duration', 1000) as number;
if (id && ctx.timerService) {
ctx.timerService.startCooldown(id, duration);
}
return {
outputs: {},
nextExec: 'exec'
};
}
}
// =============================================================================
// IsCooldownReady 节点 | IsCooldownReady Node
// =============================================================================
/**
* @zh IsCooldownReady 节点模板
* @en IsCooldownReady node template
*/
export const IsCooldownReadyTemplate: BlueprintNodeTemplate = {
type: 'IsCooldownReady',
title: 'Is Cooldown Ready',
category: 'time',
description: 'Check if cooldown is ready / 检查冷却是否就绪',
keywords: ['timer', 'cooldown', 'ready', 'check'],
menuPath: ['Timer', 'Is Cooldown Ready'],
isPure: true,
inputs: [
{
name: 'id',
displayName: 'Cooldown ID',
type: 'string',
defaultValue: ''
}
],
outputs: [
{
name: 'isReady',
displayName: 'Is Ready',
type: 'bool'
},
{
name: 'isOnCooldown',
displayName: 'Is On Cooldown',
type: 'bool'
}
],
color: '#00bcd4'
};
/**
* @zh IsCooldownReady 节点执行器
* @en IsCooldownReady node executor
*/
export class IsCooldownReadyExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as TimerContext;
const id = ctx.evaluateInput(node.id, 'id', '') as string;
const isReady = id ? ctx.timerService?.isCooldownReady(id) ?? true : true;
const isOnCooldown = !isReady;
return {
outputs: {
isReady,
isOnCooldown
}
};
}
}
// =============================================================================
// GetCooldownProgress 节点 | GetCooldownProgress Node
// =============================================================================
/**
* @zh GetCooldownProgress 节点模板
* @en GetCooldownProgress node template
*/
export const GetCooldownProgressTemplate: BlueprintNodeTemplate = {
type: 'GetCooldownProgress',
title: 'Get Cooldown Progress',
category: 'time',
description: 'Get cooldown progress (0-1) / 获取冷却进度 (0-1)',
keywords: ['timer', 'cooldown', 'progress', 'remaining'],
menuPath: ['Timer', 'Get Cooldown Progress'],
isPure: true,
inputs: [
{
name: 'id',
displayName: 'Cooldown ID',
type: 'string',
defaultValue: ''
}
],
outputs: [
{
name: 'progress',
displayName: 'Progress',
type: 'float'
},
{
name: 'remaining',
displayName: 'Remaining (ms)',
type: 'float'
},
{
name: 'isReady',
displayName: 'Is Ready',
type: 'bool'
}
],
color: '#00bcd4'
};
/**
* @zh GetCooldownProgress 节点执行器
* @en GetCooldownProgress node executor
*/
export class GetCooldownProgressExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as TimerContext;
const id = ctx.evaluateInput(node.id, 'id', '') as string;
const progress = id ? ctx.timerService?.getCooldownProgress(id) ?? 1 : 1;
const remaining = id ? ctx.timerService?.getCooldownRemaining(id) ?? 0 : 0;
const isReady = remaining <= 0;
return {
outputs: {
progress,
remaining,
isReady
}
};
}
}
// =============================================================================
// ResetCooldown 节点 | ResetCooldown Node
// =============================================================================
/**
* @zh ResetCooldown 节点模板
* @en ResetCooldown node template
*/
export const ResetCooldownTemplate: BlueprintNodeTemplate = {
type: 'ResetCooldown',
title: 'Reset Cooldown',
category: 'time',
description: 'Reset a cooldown (make it ready) / 重置冷却(使其就绪)',
keywords: ['timer', 'cooldown', 'reset', 'clear'],
menuPath: ['Timer', 'Reset Cooldown'],
isPure: false,
inputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
},
{
name: 'id',
displayName: 'Cooldown ID',
type: 'string',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
}
],
color: '#00bcd4'
};
/**
* @zh ResetCooldown 节点执行器
* @en ResetCooldown node executor
*/
export class ResetCooldownExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as TimerContext;
const id = ctx.evaluateInput(node.id, 'id', '') as string;
if (id && ctx.timerService) {
ctx.timerService.resetCooldown(id);
}
return {
outputs: {},
nextExec: 'exec'
};
}
}
// =============================================================================
// GetCooldownInfo 节点 | GetCooldownInfo Node
// =============================================================================
/**
* @zh GetCooldownInfo 节点模板
* @en GetCooldownInfo node template
*/
export const GetCooldownInfoTemplate: BlueprintNodeTemplate = {
type: 'GetCooldownInfo',
title: 'Get Cooldown Info',
category: 'time',
description: 'Get detailed cooldown information / 获取详细冷却信息',
keywords: ['timer', 'cooldown', 'info', 'details'],
menuPath: ['Timer', 'Get Cooldown Info'],
isPure: true,
inputs: [
{
name: 'id',
displayName: 'Cooldown ID',
type: 'string',
defaultValue: ''
}
],
outputs: [
{
name: 'exists',
displayName: 'Exists',
type: 'bool'
},
{
name: 'duration',
displayName: 'Duration (ms)',
type: 'float'
},
{
name: 'remaining',
displayName: 'Remaining (ms)',
type: 'float'
},
{
name: 'progress',
displayName: 'Progress',
type: 'float'
},
{
name: 'isReady',
displayName: 'Is Ready',
type: 'bool'
}
],
color: '#00bcd4'
};
/**
* @zh GetCooldownInfo 节点执行器
* @en GetCooldownInfo node executor
*/
export class GetCooldownInfoExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as TimerContext;
const id = ctx.evaluateInput(node.id, 'id', '') as string;
const info = id ? ctx.timerService?.getCooldownInfo(id) : null;
return {
outputs: {
exists: info !== null,
duration: info?.duration ?? 0,
remaining: info?.remaining ?? 0,
progress: info?.progress ?? 1,
isReady: info?.isReady ?? true
}
};
}
}
// =============================================================================
// HasTimer 节点 | HasTimer Node
// =============================================================================
/**
* @zh HasTimer 节点模板
* @en HasTimer node template
*/
export const HasTimerTemplate: BlueprintNodeTemplate = {
type: 'HasTimer',
title: 'Has Timer',
category: 'time',
description: 'Check if a timer exists / 检查定时器是否存在',
keywords: ['timer', 'exists', 'check', 'has'],
menuPath: ['Timer', 'Has Timer'],
isPure: true,
inputs: [
{
name: 'id',
displayName: 'Timer ID',
type: 'string',
defaultValue: ''
}
],
outputs: [
{
name: 'exists',
displayName: 'Exists',
type: 'bool'
}
],
color: '#00bcd4'
};
/**
* @zh HasTimer 节点执行器
* @en HasTimer node executor
*/
export class HasTimerExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as TimerContext;
const id = ctx.evaluateInput(node.id, 'id', '') as string;
const exists = id ? ctx.timerService?.hasTimer(id) ?? false : false;
return {
outputs: {
exists
}
};
}
}
// =============================================================================
// CancelTimer 节点 | CancelTimer Node
// =============================================================================
/**
* @zh CancelTimer 节点模板
* @en CancelTimer node template
*/
export const CancelTimerTemplate: BlueprintNodeTemplate = {
type: 'CancelTimer',
title: 'Cancel Timer',
category: 'time',
description: 'Cancel a timer by ID / 通过 ID 取消定时器',
keywords: ['timer', 'cancel', 'stop', 'clear'],
menuPath: ['Timer', 'Cancel Timer'],
isPure: false,
inputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
},
{
name: 'id',
displayName: 'Timer ID',
type: 'string',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
}
],
color: '#00bcd4'
};
/**
* @zh CancelTimer 节点执行器
* @en CancelTimer node executor
*/
export class CancelTimerExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as TimerContext;
const id = ctx.evaluateInput(node.id, 'id', '') as string;
if (id && ctx.timerService) {
ctx.timerService.cancelById(id);
}
return {
outputs: {},
nextExec: 'exec'
};
}
}
// =============================================================================
// GetTimerRemaining 节点 | GetTimerRemaining Node
// =============================================================================
/**
* @zh GetTimerRemaining 节点模板
* @en GetTimerRemaining node template
*/
export const GetTimerRemainingTemplate: BlueprintNodeTemplate = {
type: 'GetTimerRemaining',
title: 'Get Timer Remaining',
category: 'time',
description: 'Get remaining time for a timer / 获取定时器剩余时间',
keywords: ['timer', 'remaining', 'time', 'left'],
menuPath: ['Timer', 'Get Timer Remaining'],
isPure: true,
inputs: [
{
name: 'id',
displayName: 'Timer ID',
type: 'string',
defaultValue: ''
}
],
outputs: [
{
name: 'remaining',
displayName: 'Remaining (ms)',
type: 'float'
},
{
name: 'exists',
displayName: 'Exists',
type: 'bool'
}
],
color: '#00bcd4'
};
/**
* @zh GetTimerRemaining 节点执行器
* @en GetTimerRemaining node executor
*/
export class GetTimerRemainingExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as TimerContext;
const id = ctx.evaluateInput(node.id, 'id', '') as string;
const info = id ? ctx.timerService?.getTimerInfo(id) : null;
return {
outputs: {
remaining: info?.remaining ?? 0,
exists: info !== null
}
};
}
}
// =============================================================================
// 节点定义集合 | Node Definition Collection
// =============================================================================
/**
* @zh 定时器节点定义
* @en Timer node definitions
*/
export const TimerNodeDefinitions = [
{ template: StartCooldownTemplate, executor: new StartCooldownExecutor() },
{ template: IsCooldownReadyTemplate, executor: new IsCooldownReadyExecutor() },
{ template: GetCooldownProgressTemplate, executor: new GetCooldownProgressExecutor() },
{ template: ResetCooldownTemplate, executor: new ResetCooldownExecutor() },
{ template: GetCooldownInfoTemplate, executor: new GetCooldownInfoExecutor() },
{ template: HasTimerTemplate, executor: new HasTimerExecutor() },
{ template: CancelTimerTemplate, executor: new CancelTimerExecutor() },
{ template: GetTimerRemainingTemplate, executor: new GetTimerRemainingExecutor() }
];

View File

@@ -0,0 +1,27 @@
/**
* @zh 定时器蓝图节点导出
* @en Timer Blueprint Nodes Export
*/
export {
// Templates
StartCooldownTemplate,
IsCooldownReadyTemplate,
GetCooldownProgressTemplate,
ResetCooldownTemplate,
GetCooldownInfoTemplate,
HasTimerTemplate,
CancelTimerTemplate,
GetTimerRemainingTemplate,
// Executors
StartCooldownExecutor,
IsCooldownReadyExecutor,
GetCooldownProgressExecutor,
ResetCooldownExecutor,
GetCooldownInfoExecutor,
HasTimerExecutor,
CancelTimerExecutor,
GetTimerRemainingExecutor,
// Collection
TimerNodeDefinitions
} from './TimerNodes';

View File

@@ -0,0 +1,16 @@
/**
* @zh 定时器服务令牌
* @en Timer Service Tokens
*/
import { createServiceToken } from '@esengine/ecs-framework';
import type { ITimerService } from './ITimerService';
/**
* @zh 定时器服务令牌
* @en Timer service token
*
* @zh 用于注入定时器服务
* @en Used for injecting timer service
*/
export const TimerServiceToken = createServiceToken<ITimerService>('timerService');

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,14 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
sourcemap: true,
clean: true,
external: [
'@esengine/ecs-framework',
'@esengine/blueprint'
],
tsconfig: 'tsconfig.build.json'
});