diff --git a/packages-user/data-base/src/map/mapLayer.test.ts b/packages-user/data-base/src/map/mapLayer.test.ts index 22f8a3c..b5bb02e 100644 --- a/packages-user/data-base/src/map/mapLayer.test.ts +++ b/packages-user/data-base/src/map/mapLayer.test.ts @@ -9,6 +9,7 @@ import { FaceGroup, FaceManager, RoleFaceBinder, + SaveCompression, TileStore, TileType } from '@user/data-common'; @@ -590,6 +591,30 @@ describe('MapLayer dynamic conversion', () => { expect(result.info.map(info => info.code)).toContain(130); await result.ret; }); + + // 验证 loadState 先清空既有动态块并触发删除钩子(#06-17-8 读档不累积) + it('clears existing dynamic tiles before loading', () => { + const { layer } = createFixture([0, 0, 0, 0]); + const deleted: IDynamicTile[] = []; + layer + .addHook({ + onDeleteDynamic: async tile => { + deleted.push(tile); + } + }) + .load(); + const first = layer.createDynamic(1, 0, 0); + const second = layer.createDynamic(1, 1, 0); + const saved = layer.saveState(SaveCompression.NoCompression); + const extra = layer.createDynamic(1, 0, 1); + + layer.loadState(saved, SaveCompression.NoCompression); + + expect([...layer.iterateDynamicTiles()]).toHaveLength(2); + expect([...layer.getDynamicTilesAt(0, 1)]).toHaveLength(0); + expect(deleted).toEqual([first, second, extra]); + expect([...layer.iterateDynamicTiles()]).not.toContain(first); + }); }); describe('MapLayer dirty state and doors', () => { diff --git a/packages-user/data-base/src/map/mapLayer.ts b/packages-user/data-base/src/map/mapLayer.ts index fa567b3..292cbbd 100644 --- a/packages-user/data-base/src/map/mapLayer.ts +++ b/packages-user/data-base/src/map/mapLayer.ts @@ -848,6 +848,19 @@ export class MapLayer } } + /** + * 清空本图层全部既有动态图块,使读档后的动态块与存档点一致 + * 逐块复用删除语义,会触发 `onDeleteDynamic` 钩子,但读档为同步流程,不等待钩子完成 + */ + private clearDynamics(): void { + const tiles = [...this.iterateDynamicTiles()]; + for (const tile of tiles) { + this.syncStaticEvent(tile, false); + this.removeTile(tile); + this.forEachHook(hook => hook.onDeleteDynamic?.(tile)); + } + } + /** * 以无压缩方式读取当前图层 * @param save 图层存档 @@ -920,6 +933,7 @@ export class MapLayer } loadState(save: IMapLayerSave, compression: SaveCompression): void { + this.clearDynamics(); if (compression === SaveCompression.HighCompression) { this.loadHighCompression(save); } else if (compression === SaveCompression.LowCompression) { diff --git a/packages-user/data-base/src/map/saveLoad.test.ts b/packages-user/data-base/src/map/saveLoad.test.ts index b4b0445..7a9ed3a 100644 --- a/packages-user/data-base/src/map/saveLoad.test.ts +++ b/packages-user/data-base/src/map/saveLoad.test.ts @@ -215,6 +215,24 @@ describe('MapLayer save and load round trips', () => { expect(lowResult.info.map(info => info.code)).toContain(124); expect(highResult.info.map(info => info.code)).toContain(124); }); + + // 验证动态块多于存档时读档后等于存档,且重复读档不累积(#06-17-8) + it('does not accumulate dynamic tiles when loading fewer than present', () => { + for (const compression of SAVE_COMPRESSIONS) { + const { layer } = createMapFixture(); + setLayerReference(layer); + layer.createDynamic(1, 0, 0); + const saved = layer.saveState(compression); + layer.createDynamic(1, 1, 0); + layer.createDynamic(1, 1, 0); + + layer.loadState(saved, compression); + expect([...layer.iterateDynamicTiles()]).toHaveLength(1); + + layer.loadState(saved, compression); + expect([...layer.iterateDynamicTiles()]).toHaveLength(1); + } + }); }); describe('GameMap save and load round trips', () => {