From ebb984d3547461bd50eb624332db1bf54bc9856d Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Thu, 25 Dec 2025 12:29:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(timer):=20=E6=B7=BB=E5=8A=A0=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E5=99=A8=E5=92=8C=E5=86=B7=E5=8D=B4=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=20(#325)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 ITimerService 接口和 TimerService 实现 - 支持一次性定时器和重复定时器 - 支持冷却系统 (startCooldown/isCooldownReady/getCooldownProgress) - 添加 8 个蓝图节点: - StartCooldown, IsCooldownReady, GetCooldownProgress - ResetCooldown, GetCooldownInfo - HasTimer, CancelTimer, GetTimerRemaining --- packages/timer/module.json | 23 ++ packages/timer/package.json | 40 ++ packages/timer/src/ITimerService.ts | 294 +++++++++++++ packages/timer/src/TimerService.ts | 411 +++++++++++++++++++ packages/timer/src/index.ts | 60 +++ packages/timer/src/nodes/TimerNodes.ts | 543 +++++++++++++++++++++++++ packages/timer/src/nodes/index.ts | 27 ++ packages/timer/src/tokens.ts | 16 + packages/timer/tsconfig.build.json | 22 + packages/timer/tsconfig.json | 14 + packages/timer/tsup.config.ts | 14 + pnpm-lock.yaml | 28 ++ 12 files changed, 1492 insertions(+) create mode 100644 packages/timer/module.json create mode 100644 packages/timer/package.json create mode 100644 packages/timer/src/ITimerService.ts create mode 100644 packages/timer/src/TimerService.ts create mode 100644 packages/timer/src/index.ts create mode 100644 packages/timer/src/nodes/TimerNodes.ts create mode 100644 packages/timer/src/nodes/index.ts create mode 100644 packages/timer/src/tokens.ts create mode 100644 packages/timer/tsconfig.build.json create mode 100644 packages/timer/tsconfig.json create mode 100644 packages/timer/tsup.config.ts diff --git a/packages/timer/module.json b/packages/timer/module.json new file mode 100644 index 00000000..4c5cf95e --- /dev/null +++ b/packages/timer/module.json @@ -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" +} diff --git a/packages/timer/package.json b/packages/timer/package.json new file mode 100644 index 00000000..a3749e94 --- /dev/null +++ b/packages/timer/package.json @@ -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" + } +} diff --git a/packages/timer/src/ITimerService.ts b/packages/timer/src/ITimerService.ts new file mode 100644 index 00000000..537a5892 --- /dev/null +++ b/packages/timer/src/ITimerService.ts @@ -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-1,0 表示刚开始,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-1),1 表示完成或无冷却 @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; +} diff --git a/packages/timer/src/TimerService.ts b/packages/timer/src/TimerService.ts new file mode 100644 index 00000000..f24cdc0d --- /dev/null +++ b/packages/timer/src/TimerService.ts @@ -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 = new Map(); + private cooldowns: Map = new Map(); + private config: Required; + + 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); +} diff --git a/packages/timer/src/index.ts b/packages/timer/src/index.ts new file mode 100644 index 00000000..a6ce3769 --- /dev/null +++ b/packages/timer/src/index.ts @@ -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'; diff --git a/packages/timer/src/nodes/TimerNodes.ts b/packages/timer/src/nodes/TimerNodes.ts new file mode 100644 index 00000000..5efe9131 --- /dev/null +++ b/packages/timer/src/nodes/TimerNodes.ts @@ -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): 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() } +]; diff --git a/packages/timer/src/nodes/index.ts b/packages/timer/src/nodes/index.ts new file mode 100644 index 00000000..c54647f9 --- /dev/null +++ b/packages/timer/src/nodes/index.ts @@ -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'; diff --git a/packages/timer/src/tokens.ts b/packages/timer/src/tokens.ts new file mode 100644 index 00000000..cc98ba42 --- /dev/null +++ b/packages/timer/src/tokens.ts @@ -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('timerService'); diff --git a/packages/timer/tsconfig.build.json b/packages/timer/tsconfig.build.json new file mode 100644 index 00000000..bf8abf7b --- /dev/null +++ b/packages/timer/tsconfig.build.json @@ -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"] +} diff --git a/packages/timer/tsconfig.json b/packages/timer/tsconfig.json new file mode 100644 index 00000000..f852ad22 --- /dev/null +++ b/packages/timer/tsconfig.json @@ -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" } + ] +} diff --git a/packages/timer/tsup.config.ts b/packages/timer/tsup.config.ts new file mode 100644 index 00000000..21a01689 --- /dev/null +++ b/packages/timer/tsup.config.ts @@ -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' +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d95162d2..e0d002a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1897,6 +1897,34 @@ importers: 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)) + packages/timer: + 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/worker-generator: dependencies: chalk: