diff --git a/.changeset/cli-module-commands.md b/.changeset/cli-module-commands.md new file mode 100644 index 00000000..9f53d2f0 --- /dev/null +++ b/.changeset/cli-module-commands.md @@ -0,0 +1,9 @@ +--- +"@esengine/cli": minor +--- + +feat(cli): 添加模块管理命令 + +- 新增 `list` 命令:按分类显示可用模块 +- 新增 `add [modules...]` 命令:添加模块到项目,支持交互式选择 +- 新增 `remove [modules...]` 命令:从项目移除模块,支持确认提示 diff --git a/.changeset/spatial-aoi-fix.md b/.changeset/spatial-aoi-fix.md new file mode 100644 index 00000000..b7971704 --- /dev/null +++ b/.changeset/spatial-aoi-fix.md @@ -0,0 +1,8 @@ +--- +"@esengine/spatial": patch +--- + +fix(spatial): 修复 GridAOI 可见性更新问题 + +- 修复 `addObserver` 时现有观察者无法检测到新实体的问题 +- 修复实体远距离移动时观察者可见性未正确更新的问题 diff --git a/packages/framework/spatial/src/aoi/GridAOI.ts b/packages/framework/spatial/src/aoi/GridAOI.ts index 66402efc..cf3f9fa6 100644 --- a/packages/framework/spatial/src/aoi/GridAOI.ts +++ b/packages/framework/spatial/src/aoi/GridAOI.ts @@ -107,8 +107,13 @@ export class GridAOI implements IAOIManager { this._observers.set(entity, data); this._addToCell(cellKey, data); - // Initial visibility check + // Initial visibility check for this observer this._updateVisibility(data); + + // Notify other observers about this new entity + if (data.observable) { + this._updateObserversOfEntity(data); + } } /** @@ -398,40 +403,32 @@ export class GridAOI implements IAOIManager { * @en Update other observers' visibility of an entity */ private _updateObserversOfEntity(movedData: AOIObserverData): void { - const cellRadius = Math.ceil(this._getMaxViewRange() / this._cellSize) + 1; - const centerCell = this._getCellCoords(movedData.position); + // Check all observers for visibility changes + // This handles both: observers who can now see the entity (enter) + // and observers who could see it before but can't anymore (exit) + for (const [, otherData] of this._observers) { + if (otherData === movedData) continue; - for (let dx = -cellRadius; dx <= cellRadius; dx++) { - for (let dy = -cellRadius; dy <= cellRadius; dy++) { - const cellKey = `${centerCell.x + dx},${centerCell.y + dy}`; - const cell = this._cells.get(cellKey); - if (!cell) continue; + const distSq = distanceSquared(otherData.position, movedData.position); + const wasVisible = otherData.visibleEntities.has(movedData.entity); + const isVisible = distSq <= otherData.viewRangeSq; - for (const otherData of cell) { - if (otherData === movedData) continue; - - const distSq = distanceSquared(otherData.position, movedData.position); - const wasVisible = otherData.visibleEntities.has(movedData.entity); - const isVisible = distSq <= otherData.viewRangeSq; - - if (isVisible && !wasVisible) { - otherData.visibleEntities.add(movedData.entity); - this._emitEvent({ - type: 'enter', - observer: otherData.entity, - target: movedData.entity, - position: movedData.position - }, otherData); - } else if (!isVisible && wasVisible) { - otherData.visibleEntities.delete(movedData.entity); - this._emitEvent({ - type: 'exit', - observer: otherData.entity, - target: movedData.entity, - position: movedData.position - }, otherData); - } - } + if (isVisible && !wasVisible) { + otherData.visibleEntities.add(movedData.entity); + this._emitEvent({ + type: 'enter', + observer: otherData.entity, + target: movedData.entity, + position: movedData.position + }, otherData); + } else if (!isVisible && wasVisible) { + otherData.visibleEntities.delete(movedData.entity); + this._emitEvent({ + type: 'exit', + observer: otherData.entity, + target: movedData.entity, + position: movedData.position + }, otherData); } } } diff --git a/packages/tools/demos/src/fsm.demo.ts b/packages/tools/demos/src/fsm.demo.ts index f1f37880..33f3bcd8 100644 --- a/packages/tools/demos/src/fsm.demo.ts +++ b/packages/tools/demos/src/fsm.demo.ts @@ -3,22 +3,12 @@ */ import { createStateMachine } from '@esengine/fsm'; - -function assert(condition: boolean, message: string): void { - if (!condition) throw new Error(`FAILED: ${message}`); - console.log(` ✓ ${message}`); -} - -function section(name: string): void { - console.log(`\n▶ ${name}`); -} +import { assert, section, demoHeader, demoFooter } from './utils.js'; type PlayerState = 'idle' | 'walk' | 'run' | 'jump'; export async function runFSMDemo(): Promise { - console.log('═══════════════════════════════════════'); - console.log(' FSM Module Demo'); - console.log('═══════════════════════════════════════'); + demoHeader('FSM Module Demo'); // 1. Basic Creation section('1. createStateMachine()'); @@ -167,9 +157,7 @@ export async function runFSMDemo(): Promise { const history = fsmHist.getHistory(); assert(history.length >= 2, 'History recorded'); - console.log('\n═══════════════════════════════════════'); - console.log(' FSM Demo: ALL TESTS PASSED ✓'); - console.log('═══════════════════════════════════════\n'); + demoFooter('FSM Demo'); } runFSMDemo().catch(console.error); diff --git a/packages/tools/demos/src/pathfinding.demo.ts b/packages/tools/demos/src/pathfinding.demo.ts index 7c978418..50ce3b33 100644 --- a/packages/tools/demos/src/pathfinding.demo.ts +++ b/packages/tools/demos/src/pathfinding.demo.ts @@ -10,20 +10,10 @@ import { manhattanDistance, octileDistance } from '@esengine/pathfinding'; - -function assert(condition: boolean, message: string): void { - if (!condition) throw new Error(`FAILED: ${message}`); - console.log(` ✓ ${message}`); -} - -function section(name: string): void { - console.log(`\n▶ ${name}`); -} +import { assert, section, demoHeader, demoFooter } from './utils.js'; export async function runPathfindingDemo(): Promise { - console.log('═══════════════════════════════════════'); - console.log(' Pathfinding Module Demo'); - console.log('═══════════════════════════════════════'); + demoHeader('Pathfinding Module Demo'); // 1. Create Grid Map section('1. createGridMap()'); @@ -156,9 +146,7 @@ export async function runPathfindingDemo(): Promise { grid.reset(); assert(grid.isWalkable(5, 5), 'Grid reset - all walkable'); - console.log('\n═══════════════════════════════════════'); - console.log(' Pathfinding Demo: ALL TESTS PASSED ✓'); - console.log('═══════════════════════════════════════\n'); + demoFooter('Pathfinding Demo'); } runPathfindingDemo().catch(console.error); diff --git a/packages/tools/demos/src/procgen.demo.ts b/packages/tools/demos/src/procgen.demo.ts index 4a2fbd0f..ae3826fd 100644 --- a/packages/tools/demos/src/procgen.demo.ts +++ b/packages/tools/demos/src/procgen.demo.ts @@ -17,20 +17,10 @@ import { weightedPick, weightedPickFromMap } from '@esengine/procgen'; - -function assert(condition: boolean, message: string): void { - if (!condition) throw new Error(`FAILED: ${message}`); - console.log(` ✓ ${message}`); -} - -function section(name: string): void { - console.log(`\n▶ ${name}`); -} +import { assert, section, demoHeader, demoFooter } from './utils.js'; export async function runProcgenDemo(): Promise { - console.log('═══════════════════════════════════════'); - console.log(' Procgen Module Demo'); - console.log('═══════════════════════════════════════'); + demoHeader('Procgen Module Demo'); // 1. Perlin Noise section('1. createPerlinNoise()'); @@ -200,9 +190,7 @@ export async function runProcgenDemo(): Promise { const afterReset = rngReset.next(); assert(first === afterReset, 'Reset restores initial state'); - console.log('\n═══════════════════════════════════════'); - console.log(' Procgen Demo: ALL TESTS PASSED ✓'); - console.log('═══════════════════════════════════════\n'); + demoFooter('Procgen Demo'); } runProcgenDemo().catch(console.error); diff --git a/packages/tools/demos/src/spatial.demo.ts b/packages/tools/demos/src/spatial.demo.ts index 6a836369..4e6729c7 100644 --- a/packages/tools/demos/src/spatial.demo.ts +++ b/packages/tools/demos/src/spatial.demo.ts @@ -13,15 +13,7 @@ import { distance, distanceSquared } from '@esengine/spatial'; - -function assert(condition: boolean, message: string): void { - if (!condition) throw new Error(`FAILED: ${message}`); - console.log(` ✓ ${message}`); -} - -function section(name: string): void { - console.log(`\n▶ ${name}`); -} +import { assert, section, demoHeader, demoFooter } from './utils.js'; interface Entity { id: number; @@ -29,9 +21,7 @@ interface Entity { } export async function runSpatialDemo(): Promise { - console.log('═══════════════════════════════════════'); - console.log(' Spatial Module Demo'); - console.log('═══════════════════════════════════════'); + demoHeader('Spatial Module Demo'); // 1. Create Spatial Index section('1. createGridSpatialIndex()'); @@ -150,10 +140,6 @@ export async function runSpatialDemo(): Promise { aoi.addObserver(p1, { x: 100, y: 100 }, { viewRange: 200 }); aoi.addObserver(p2, { x: 150, y: 150 }, { viewRange: 200 }); - // Note: After adding p2, p2 can see p1, but p1's visibility isn't auto-updated - // We need to trigger an update for p1 to detect p2 - aoi.updatePosition(p1, { x: 100, y: 100 }); - // 15. getEntitiesInView section('15. getEntitiesInView()'); const visible = aoi.getEntitiesInView(p1); @@ -166,14 +152,11 @@ export async function runSpatialDemo(): Promise { // 17. updatePosition section('17. updatePosition()'); aoi.updatePosition(p2, { x: 1000, y: 1000 }); - // Refresh p1's visibility (implementation requires explicit update for distant moves) - aoi.updatePosition(p1, { x: 100, y: 100 }); assert(!aoi.canSee(p1, p2), 'p1 cannot see p2 after move'); // 18. getObserversOf section('18. getObserversOf()'); aoi.updatePosition(p2, { x: 120, y: 120 }); - aoi.updatePosition(p1, { x: 100, y: 100 }); // Refresh p1's visibility const observers = aoi.getObserversOf(p2); assert(observers.includes(p1), 'p1 observes p2'); @@ -184,16 +167,13 @@ export async function runSpatialDemo(): Promise { eventCount++; }); aoi.updatePosition(p2, { x: 2000, y: 2000 }); // Should trigger exit - aoi.updatePosition(p1, { x: 100, y: 100 }); // Refresh p1 aoi.updatePosition(p2, { x: 130, y: 130 }); // Should trigger enter - aoi.updatePosition(p1, { x: 100, y: 100 }); // Refresh p1 assert(eventCount >= 1, `Events triggered: ${eventCount}`); // 20. updateViewRange section('20. updateViewRange()'); aoi.updateViewRange(p1, 50); aoi.updatePosition(p2, { x: 200, y: 200 }); - aoi.updatePosition(p1, { x: 100, y: 100 }); // Refresh p1's visibility with new range assert(!aoi.canSee(p1, p2), 'Cannot see after view range reduced'); // 21. removeObserver @@ -238,9 +218,7 @@ export async function runSpatialDemo(): Promise { const dsq = distanceSquared({ x: 0, y: 0 }, { x: 3, y: 4 }); assert(dsq === 25, `Distance squared: ${dsq}`); - console.log('\n═══════════════════════════════════════'); - console.log(' Spatial Demo: ALL TESTS PASSED ✓'); - console.log('═══════════════════════════════════════\n'); + demoFooter('Spatial Demo'); } runSpatialDemo().catch(console.error); diff --git a/packages/tools/demos/src/timer.demo.ts b/packages/tools/demos/src/timer.demo.ts index f923761a..6156896c 100644 --- a/packages/tools/demos/src/timer.demo.ts +++ b/packages/tools/demos/src/timer.demo.ts @@ -3,20 +3,10 @@ */ import { createTimerService } from '@esengine/timer'; - -function assert(condition: boolean, message: string): void { - if (!condition) throw new Error(`FAILED: ${message}`); - console.log(` ✓ ${message}`); -} - -function section(name: string): void { - console.log(`\n▶ ${name}`); -} +import { assert, section, demoHeader, demoFooter } from './utils.js'; export async function runTimerDemo(): Promise { - console.log('═══════════════════════════════════════'); - console.log(' Timer Module Demo'); - console.log('═══════════════════════════════════════'); + demoHeader('Timer Module Demo'); // 1. Basic Creation section('1. createTimerService()'); @@ -111,9 +101,7 @@ export async function runTimerDemo(): Promise { const limited = createTimerService({ maxTimers: 2, maxCooldowns: 1 }); assert(limited !== null, 'Created with config'); - console.log('\n═══════════════════════════════════════'); - console.log(' Timer Demo: ALL TESTS PASSED ✓'); - console.log('═══════════════════════════════════════\n'); + demoFooter('Timer Demo'); } runTimerDemo().catch(console.error); diff --git a/packages/tools/demos/src/utils.ts b/packages/tools/demos/src/utils.ts new file mode 100644 index 00000000..0c00ad1e --- /dev/null +++ b/packages/tools/demos/src/utils.ts @@ -0,0 +1,41 @@ +/** + * @zh Demo 测试工具函数 + * @en Demo test utility functions + */ + +/** + * @zh 断言条件为真,否则抛出错误 + * @en Assert condition is true, otherwise throw error + */ +export function assert(condition: boolean, message: string): void { + if (!condition) throw new Error(`FAILED: ${message}`); + console.log(` ✓ ${message}`); +} + +/** + * @zh 打印测试章节标题 + * @en Print test section header + */ +export function section(name: string): void { + console.log(`\n▶ ${name}`); +} + +/** + * @zh 打印 Demo 开始标题 + * @en Print demo start header + */ +export function demoHeader(name: string): void { + console.log('═══════════════════════════════════════'); + console.log(` ${name}`); + console.log('═══════════════════════════════════════'); +} + +/** + * @zh 打印 Demo 结束标题 + * @en Print demo end header + */ +export function demoFooter(name: string): void { + console.log('\n═══════════════════════════════════════'); + console.log(` ${name}: ALL TESTS PASSED ✓`); + console.log('═══════════════════════════════════════\n'); +}