Files
esengine/packages/framework/timer/src/TimerService.ts
YHH 155411e743 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
2025-12-26 14:50:35 +08:00

412 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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);
}