diff --git a/dev.md b/dev.md index ac86178..2835d49 100644 --- a/dev.md +++ b/dev.md @@ -116,7 +116,7 @@ - 换行使用 `CRLF` 格式。 - 不得将一个对象上的函数声明为一个临时变量/常量,哪怕其没有 `this` 指向问题。 - 通常情况下,一个文件应只包含一个类,但如果一个类仅是另一个类的依赖,或一个类的长度远远小于另一个类等特殊情况时,允许将多个类放在同一个文件中。 -- 私有方法应该放到调用之的方法之前,同时保证其处在合理的 region 内。 +- 私有方法应该放到调用之的方法之前,同时保证其处在合理的 region 内。此条不是说私有方法必须放到类的开头,这是完全错误的,必须在保证其处于调用之的函数之前,但同时处在合理的 region 内。region 分区按照方法功能分区,也就是说方法必须在合理的功能分区中。 - 对象的非空判断用 `!obj`,字面量的非空判断用 `!isNil(value)`。 - 任何情况下都不建议写 `try-catch-finally`,哪怕有时会因为报错导致某些关键状态产生错误,产生错误就应该让用户去修,而不是让系统兜底。 diff --git a/packages-user/data-base/src/map/eventView.ts b/packages-user/data-base/src/map/eventView.ts index f2df536..d6f32de 100644 --- a/packages-user/data-base/src/map/eventView.ts +++ b/packages-user/data-base/src/map/eventView.ts @@ -10,6 +10,10 @@ export class LayerEventView implements ILayerEventView { /** 当前存储与参考基准不一致的条目数量 */ private dirtyEntries: number = 0; + [Symbol.iterator]() { + return this.store.entries(); + } + get(): ReadonlyMap { return this.store; } diff --git a/packages-user/data-base/src/map/gameMap.ts b/packages-user/data-base/src/map/gameMap.ts index 8163775..418b151 100644 --- a/packages-user/data-base/src/map/gameMap.ts +++ b/packages-user/data-base/src/map/gameMap.ts @@ -59,8 +59,7 @@ export class GameMap extends Hookable implements IGameMap { @shouldReplay('Adding game map layer should be replayed.') addLayer(alias: string): IMapLayer { - const array = new Uint32Array(this.width * this.height); - const layer = new MapLayer(array, this.width, this.height, this, alias); + const layer = new MapLayer(this.width, this.height, this, alias); this.layerList.add(layer); this.forEachHook(hook => { hook.onUpdateLayer?.(this.layerList); diff --git a/packages-user/data-base/src/map/mapLayer.ts b/packages-user/data-base/src/map/mapLayer.ts index 28c1529..2fc2ad9 100644 --- a/packages-user/data-base/src/map/mapLayer.ts +++ b/packages-user/data-base/src/map/mapLayer.ts @@ -5,11 +5,11 @@ import { IGameMap, ILayerEventView, ILayerLocation, - IMapLayer, IMapLayerData, IMapLayerHookController, IMapLayerHooks, IMapLayerSave, + IResizableMapLayer, IStaticBlockSave, IStaticTile } from './types'; @@ -17,6 +17,7 @@ import { Hookable, HookController, ITileLocator, logger } from '@motajs/common'; import { FaceDirection, IDataCommon, + ILocationHelper, IRoleFaceBinder, SaveCompression, shouldReplay @@ -27,9 +28,10 @@ import { StaticTile } from './staticTile'; export class MapLayer extends Hookable - implements IMapLayer + implements IResizableMapLayer { readonly state: IDataCommon; + readonly indexer: ILocationHelper; width: number; height: number; @@ -59,7 +61,6 @@ export class MapLayer private readonly pointEvents: Map = new Map(); constructor( - array: Uint32Array, width: number, height: number, readonly map: IGameMap, @@ -72,128 +73,31 @@ export class MapLayer this.height = height; const area = width * height; this.mapArray = new Uint32Array(area); - this.mapArray.set(array); this.mapData = { expired: false, array: this.mapArray }; + this.indexer = map.indexer; } - /** - * 将动态图块登记到索引表中 - * @param tile 动态图块 - */ - private addDynamicToPosMap(tile: IDynamicTile): void { - const { x, y } = tile; - const xMap = this.tilePosMap.getOrInsertComputed(y, () => new Map()); - const set = xMap.getOrInsertComputed(x, () => new Set()); - set.add(tile); - // 需要使用不同引用的 locator,避免与 tile 本身的 locator 同引用导致缓存位置失效 - // 缓存位置的目的是能在动态图块位置更新时还持有旧位置,能够执行内部存储的移动 - this.cachedDynamics.set(tile, { x, y }); - } - - /** - * 将动态图块从指定缓存坐标的索引表中移除 - * @param tile 动态图块 - * @param x 横坐标 - * @param y 纵坐标 - */ - private removeDynamic(tile: IDynamicTile, x: number, y: number): void { - this.cachedDynamics.delete(tile); - this.tilePosMap.get(y)?.get(x)?.delete(tile); + protected createController( + hook: Partial + ): IMapLayerHookController { + return new MapLayerHookController(this, hook); } //#region 事件操作 - /** - * 将动态图块的事件同步回当前静态格点 - * @param tile 动态图块 - * @param keepEvent 是否保留事件 - */ - private syncStaticEvent(tile: IDynamicTile, keepEvent: boolean): void { - const staticTile = this.getTile(tile.x, tile.y); - if (!staticTile) return; - staticTile.set(staticTile.num()); - if (keepEvent) { - const staticEvent = staticTile.tileEvent(); - const dynamicEvent = tile.tileEvent(); - staticEvent.clear(); - for (const [priority, id] of dynamicEvent.get()) { - staticEvent.set(priority, id); - } - } - } - /** * 将所有已创建的点事件视图恢复到原始纯基准 */ private resetPointEvents(): void { - for (const eventView of this.pointEvents.values()) { - const reference = eventView.ref(); - eventView.clear(); - for (const [priority, id] of reference) { - eventView.set(priority, id); - } - } - } - - /** - * 裁剪超出新图层范围的点事件,并按新宽度重建索引 - * @param width 新图层宽度 - * @param height 新图层高度 - */ - private cropPointEvents(width: number, height: number): void { - const pointEvents = new Map(); - for (const [index, eventView] of this.pointEvents) { - const x = index % this.width; - const y = Math.floor(index / this.width); - if (x < width && y < height) { - pointEvents.set(y * width + x, eventView); - } - } - this.pointEvents.clear(); - for (const [index, eventView] of pointEvents) { - this.pointEvents.set(index, eventView); - } - } - - /** - * 收集需要保存的点事件并复制其内部 Map - */ - private savePointEvents(): Map> { - const pointEvents = new Map>(); - for (const [index, eventView] of this.pointEvents) { - if (!eventView.dirty()) continue; - pointEvents.set(index, new Map(eventView.get())); - } - return pointEvents; - } - - /** - * 在图层基准上叠加点事件存档 - * @param save 点事件存档,可省略 - */ - private loadPointEvents( - save?: ReadonlyMap> - ): void { - this.resetPointEvents(); - if (!save) return; - for (const [index, events] of save) { - const x = index % this.width; - const y = Math.floor(index / this.width); - const eventView = this.event(x, y); - if (!eventView) continue; - eventView.clear(); - for (const [priority, id] of events) { - eventView.set(priority, id); - } - } + this.pointEvents.forEach(v => v.reset()); } event(x: number, y: number): ILayerEventView | null { if (!this.inMap(x, y)) return null; - const index = y * this.width + x; + const index = this.indexer.index(x, y); let eventView = this.pointEvents.get(index); if (!eventView) { eventView = new LayerEventView(); @@ -217,7 +121,7 @@ export class MapLayer @shouldReplay('Setting map layer block should be replayed.') setBlock(block: number, x: number, y: number): void { if (!this.inMap(x, y)) return; - const index = y * this.width + x; + const index = this.indexer.index(x, y); if (block === this.mapArray[index]) return; this.mapArray[index] = block; this.layerDirty = true; @@ -248,7 +152,7 @@ export class MapLayer getTile(x: number, y: number): IStaticTile | null { if (!this.inMap(x, y)) return null; - const index = this.map.indexer.locToIndex(x, y); + const index = this.map.indexer.index(x, y); let staticTile = this.staticTileCache.get(index); if (!staticTile) { staticTile = new StaticTile(x, y, this); @@ -260,7 +164,7 @@ export class MapLayer getLocationData(x: number, y: number): ILayerLocation | null { if (!this.inMap(x, y)) return null; const staticTile = this.getTile(x, y); - const num = staticTile?.num() ?? -1; + const num = staticTile?.num() ?? 0; const dynamics = this.getDynamicTilesAt(x, y); return { locator: { x, y }, @@ -340,14 +244,18 @@ export class MapLayer if (x < 0 || y < 0 || r > w || b > h) { logger.warn(81); } - const res = new Uint32Array(width * height); + const nl = Math.max(x, 0); + const nt = Math.max(y, 0); + const nr = Math.min(r, w); + const nb = Math.min(b, this.height); + const nw = nr - nl; + const nh = nb - nt; + const res = new Uint32Array(nw * nh); const arr = this.mapArray; - const nb = Math.min(b, h); - for (let ny = y; ny < nb; ny++) { - const lineStart = ny * w + x; - const lineEnd = lineStart + width; + for (let ny = nt; ny < nb; ny++) { + const lineStart = ny * w + nl; const dy = ny - y; - res.set(arr.subarray(lineStart, lineEnd), dy * width); + res.set(arr.subarray(lineStart, nr), dy * width); } return res; } @@ -378,7 +286,7 @@ export class MapLayer return this.mapData; } - @shouldReplay('Setting map layer block direction should be replayed.') + @shouldReplay('Setting map layer block direction should be replayed.') setStaticDirection(x: number, y: number, direction: FaceDirection): number { const tile = this.getTile(x, y); if (!tile) return -1; @@ -400,19 +308,36 @@ export class MapLayer //#region 动态图层操作 + /** + * 将动态图块登记到索引表中 + * @param tile 动态图块 + */ + private addDynamic(tile: IDynamicTile): void { + const { x, y } = tile; + const xMap = this.tilePosMap.getOrInsertComputed(y, () => new Map()); + const set = xMap.getOrInsertComputed(x, () => new Set()); + set.add(tile); + // 需要使用不同引用的 locator,避免与 tile 本身的 locator 同引用导致缓存位置失效 + // 缓存位置的目的是能在动态图块位置更新时还持有旧位置,能够执行内部存储的移动 + this.cachedDynamics.set(tile, { x, y }); + } + + /** + * 将动态图块从指定缓存坐标的索引表中移除,注意动态图块本身的位置与其所在的索引表可能不同, + * 在动态图块位置更新时,指定位置可以用来移除旧位置的动态图块 + * @param tile 动态图块 + * @param x 横坐标 + * @param y 纵坐标 + */ + private removeDynamic(tile: IDynamicTile, x: number, y: number): void { + this.cachedDynamics.delete(tile); + this.tilePosMap.get(y)?.get(x)?.delete(tile); + } + @shouldReplay('Creating dynamic tile should be replayed.') createDynamic(num: number, x: number, y: number): IDynamicTile { const tile = new DynamicTile(num, x, y, this); - const location = this.getLocationData(x, y); - if (location?.static) { - const tileEvent = tile.tileEvent(); - const staticEvent = location.static.tileEvent(); - tileEvent.clear(); - for (const [priority, id] of staticEvent.get()) { - tileEvent.set(priority, id); - } - } - this.addDynamicToPosMap(tile); + this.addDynamic(tile); this.forEachHook(hook => hook.onCreateDynamic?.(tile)); return tile; } @@ -433,12 +358,12 @@ export class MapLayer } this.setBlock(0, x, y); const tile = this.createDynamic(num, x, y); - const staticTile = this.getTile(x, y); - if (staticTile) { - staticTile.tileEvent().clear(); - } - if (!keepEvent) { - tile.tileEvent().clear(); + if (keepEvent) { + const staticTile = this.getTile(x, y); + if (staticTile) { + tile.syncTileEvent(staticTile); + staticTile.tileEvent().clear(); + } } return tile; } @@ -456,12 +381,7 @@ export class MapLayer const staticTile = this.getTile(tile.x, tile.y); if (!staticTile) return null; if (keepEvent) { - const staticEvent = staticTile.tileEvent(); - const dynamicEvent = tile.tileEvent(); - staticEvent.clear(); - for (const [priority, id] of dynamicEvent.get()) { - staticEvent.set(priority, id); - } + staticTile.syncTileEvent(tile); } this.removeDynamic(tile, tile.x, tile.y); this.forEachHook(hook => hook.onDeleteDynamic?.(tile)); @@ -512,11 +432,6 @@ export class MapLayer return this.cachedDynamics.keys(); } - @shouldReplay('Setting map layer block direction should be replayed.') - setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): number { - return tile.setFaceDirection(direction); - } - @shouldReplay('Updating dynamic tile position should be replayed.') updateDynamicTile(tile: IDynamicTile): void { const oldPos = this.cachedDynamics.get(tile); @@ -524,9 +439,9 @@ export class MapLayer this.removeDynamic(tile, oldPos.x, oldPos.y); oldPos.x = tile.x; oldPos.y = tile.y; - this.addDynamicToPosMap(tile); + this.addDynamic(tile); } else { - this.addDynamicToPosMap(tile); + this.addDynamic(tile); this.cachedDynamics.set(tile, { x: tile.x, y: tile.y }); } this.forEachHook(hook => hook.onUpdateDynamicPosition?.(tile)); @@ -538,7 +453,7 @@ export class MapLayer @shouldReplay('Opening door should be replayed.') async openDoor(x: number, y: number): Promise { - const index = y * this.width + x; + const index = this.indexer.index(x, y); const num = this.mapArray[index]; if (num === 0) return; await Promise.all( @@ -551,7 +466,7 @@ export class MapLayer @shouldReplay('Closing door should be replayed.') async closeDoor(num: number, x: number, y: number): Promise { - const index = y * this.width + x; + const index = this.indexer.index(x, y); const nowNum = this.mapArray[index]; if (nowNum !== 0) { logger.error(46, x.toString(), y.toString()); @@ -608,10 +523,18 @@ export class MapLayer this.layerDirty = !this.isEqualToRef(); } - protected createController( - hook: Partial - ): IMapLayerHookController { - return new MapLayerHookController(this, hook); + /** + * 裁剪超出新图层范围的点事件,并按新宽度重建索引 + * @param width 新图层宽度 + * @param height 新图层高度 + */ + private cropPointEvents(width: number, height: number): void { + for (const [index, eventView] of [...this.pointEvents]) { + const { x, y } = this.indexer.locator(index); + if (x < width && y < height) { + this.pointEvents.set(index, eventView); + } + } } @shouldReplay('Resizing map layer should be replayed.') @@ -619,8 +542,9 @@ export class MapLayer if (this.width === width && this.height === height) { return; } + + // 图层数组 this.layerDirty = true; - this.cropPointEvents(width, height); this.mapData.expired = true; const before = this.mapArray; const beforeWidth = this.width; @@ -631,7 +555,6 @@ export class MapLayer const area = width * height; const newArray = new Uint32Array(area); this.mapArray = newArray; - this.staticTileCache.clear(); if (beforeArea > area) { for (let ny = 0; ny < height; ny++) { const begin = ny * beforeWidth; @@ -640,16 +563,18 @@ export class MapLayer } else { for (let ny = 0; ny < beforeHeight; ny++) { const begin = ny * beforeWidth; - newArray.set( - before.subarray(begin, begin + beforeWidth), - ny * width - ); + const end = begin + beforeWidth; + newArray.set(before.subarray(begin, end), ny * width); } } this.mapData = { expired: false, array: this.mapArray }; + + // 其他杂项清理 + this.cropPointEvents(width, height); + this.staticTileCache.clear(); this.forEachHook(hook => { hook.onResize?.(width, height); }); @@ -658,23 +583,27 @@ export class MapLayer @shouldReplay('Resizing map layer should be replayed.') resize2(width: number, height: number): void { this.layerDirty = true; - this.pointEvents.clear(); if (this.width === width && this.height === height) { this.empty = true; this.mapArray.fill(0); this.staticTileCache.clear(); return; } + + // 图层数组 this.mapData.expired = true; this.width = width; this.height = height; this.mapArray = new Uint32Array(width * height); - this.staticTileCache.clear(); this.mapData = { expired: false, array: this.mapArray }; this.empty = true; + + // 其他杂项清理 + this.pointEvents.clear(); + this.staticTileCache.clear(); this.forEachHook(hook => { hook.onResize?.(width, height); }); @@ -686,31 +615,33 @@ export class MapLayer /** * 保存静态图块实例 + * @param compression 压缩级别 */ - private saveStatics(): Map { + private saveStaticTiles( + compression: SaveCompression + ): Map { const blocks = new Map(); for (const location of this.iterateBlocks()) { const tile = location.static; if (!tile || !tile.shouldSave()) continue; - const index = this.map.indexer.locaterToIndex(location.locator); - blocks.set(index, tile.saveState(SaveCompression.NoCompression)); + const index = this.map.indexer.locatorToIndex(location.locator); + blocks.set(index, tile.saveState(compression)); } return blocks; } /** * 保存动态图块实例 + * @param compression 压缩级别 */ - private saveDynamics(): Map { + private saveDynamicTiles( + compression: SaveCompression + ): Map { const blocks = new Map(); for (const tile of this.iterateDynamicTiles()) { - const index = this.map.indexer.locaterToIndex(tile.locator); - let list = blocks.get(index); - if (!list) { - list = []; - blocks.set(index, list); - } - list.push(tile.saveState(SaveCompression.NoCompression)); + const index = this.map.indexer.locatorToIndex(tile.locator); + const list = blocks.getOrInsert(index, []); + list.push(tile.saveState(compression)); } return blocks; } @@ -742,8 +673,8 @@ export class MapLayer width: this.width, height: this.height, fullMap: new Uint32Array(this.mapArray), - staticBlocks: this.saveStatics(), - dynamicBlocks: this.saveDynamics(), + staticBlocks: this.saveStaticTiles(SaveCompression.NoCompression), + dynamicBlocks: this.saveDynamicTiles(SaveCompression.NoCompression), pointEvents: this.savePointEvents() }; } @@ -752,23 +683,23 @@ export class MapLayer * 以低压缩方式序列化当前图层 */ private saveLowCompression(): IMapLayerSave { - const staticBlocks = this.saveStatics(); - const dynamicBlocks = this.saveDynamics(); + const statics = this.saveStaticTiles(SaveCompression.LowCompression); + const dynamics = this.saveDynamicTiles(SaveCompression.LowCompression); if (this.layerDirty && (!this.refArray || !this.isEqualToRef())) { return { width: this.width, height: this.height, fullMap: new Uint32Array(this.mapArray), - staticBlocks, - dynamicBlocks, + staticBlocks: statics, + dynamicBlocks: dynamics, pointEvents: this.savePointEvents() }; } else { return { width: this.width, height: this.height, - staticBlocks, - dynamicBlocks, + staticBlocks: statics, + dynamicBlocks: dynamics, pointEvents: this.savePointEvents() }; } @@ -778,16 +709,16 @@ export class MapLayer * 以高压缩方式序列化当前图层 */ private saveHighCompression(): IMapLayerSave { - const staticBlocks = this.saveStatics(); - const dynamicBlocks = this.saveDynamics(); + const statics = this.saveStaticTiles(SaveCompression.HighCompression); + const dynamics = this.saveDynamicTiles(SaveCompression.HighCompression); if (this.layerDirty) { if (this.refArray) { return { width: this.width, height: this.height, rows: this.diffRows(this.refArray), - staticBlocks, - dynamicBlocks, + staticBlocks: statics, + dynamicBlocks: dynamics, pointEvents: this.savePointEvents() }; } else { @@ -795,8 +726,8 @@ export class MapLayer width: this.width, height: this.height, fullMap: new Uint32Array(this.mapArray), - staticBlocks, - dynamicBlocks, + staticBlocks: statics, + dynamicBlocks: dynamics, pointEvents: this.savePointEvents() }; } @@ -804,8 +735,8 @@ export class MapLayer return { width: this.width, height: this.height, - staticBlocks, - dynamicBlocks, + staticBlocks: statics, + dynamicBlocks: dynamics, pointEvents: this.savePointEvents() }; } @@ -824,26 +755,34 @@ export class MapLayer /** * 读取静态图块实例存档数据 * @param save 静态图块实例存档 + * @param compression 压缩级别 */ - private loadStatics(save: ReadonlyMap): void { + private loadStaticTiles( + save: ReadonlyMap, + compression: SaveCompression + ): void { for (const [index, tileSave] of save) { - const { x, y } = this.map.indexer.indexToLocator(index); + const { x, y } = this.map.indexer.locator(index); const location = this.getLocationData(x, y); if (!location?.static) continue; - location.static.loadState(tileSave, SaveCompression.NoCompression); + location.static.loadState(tileSave, compression); } } /** * 读取动态图块实例存档数据 * @param save 动态图块实例存档 + * @param compression 压缩级别 */ - private loadDynamics(save: ReadonlyMap): void { + private loadDynamics( + save: ReadonlyMap, + compression: SaveCompression + ): void { for (const [index, dynamics] of save) { - const { x, y } = this.map.indexer.indexToLocator(index); + const { x, y } = this.map.indexer.locator(index); for (const block of dynamics) { const tile = this.createDynamic(block.num, x, y); - tile.loadState(block, SaveCompression.NoCompression); + tile.loadState(block, compression); } } } @@ -855,12 +794,45 @@ export class MapLayer private clearDynamics(): void { const tiles = [...this.iterateDynamicTiles()]; for (const tile of tiles) { - this.syncStaticEvent(tile, false); this.removeDynamic(tile, tile.x, tile.y); this.forEachHook(hook => hook.onDeleteDynamic?.(tile)); } } + /** + * 收集需要保存的点事件并复制其内部 Map + */ + private savePointEvents(): Map> { + const pointEvents = new Map>(); + for (const [index, eventView] of this.pointEvents) { + if (!eventView.dirty()) continue; + pointEvents.set(index, new Map(eventView.get())); + } + return pointEvents; + } + + /** + * 在图层基准上叠加点事件存档 + * @param save 点事件存档,可省略 + */ + private loadPointEvents( + save?: ReadonlyMap> + ): void { + this.resetPointEvents(); + if (!save) return; + for (const [index, events] of save) { + const { x, y } = this.indexer.locator(index); + const eventView = this.event(x, y); + if (!eventView) continue; + eventView.clear(); + for (const [priority, id] of events) { + eventView.set(priority, id); + } + } + } + + // TODO: 不同大小的图层读取 + /** * 以无压缩方式读取当前图层 * @param save 图层存档 @@ -870,10 +842,16 @@ export class MapLayer this.setMapRef(new Uint32Array(save.fullMap)); } if (save.staticBlocks) { - this.loadStatics(save.staticBlocks); + this.loadStaticTiles( + save.staticBlocks, + SaveCompression.NoCompression + ); } if (save.dynamicBlocks) { - this.loadDynamics(save.dynamicBlocks); + this.loadDynamics( + save.dynamicBlocks, + SaveCompression.NoCompression + ); } this.layerDirty = !this.isEqualToRef(); } @@ -894,10 +872,16 @@ export class MapLayer } if (save.staticBlocks) { - this.loadStatics(save.staticBlocks); + this.loadStaticTiles( + save.staticBlocks, + SaveCompression.LowCompression + ); } if (save.dynamicBlocks) { - this.loadDynamics(save.dynamicBlocks); + this.loadDynamics( + save.dynamicBlocks, + SaveCompression.LowCompression + ); } } @@ -925,10 +909,16 @@ export class MapLayer } if (save.staticBlocks) { - this.loadStatics(save.staticBlocks); + this.loadStaticTiles( + save.staticBlocks, + SaveCompression.HighCompression + ); } if (save.dynamicBlocks) { - this.loadDynamics(save.dynamicBlocks); + this.loadDynamics( + save.dynamicBlocks, + SaveCompression.HighCompression + ); } } diff --git a/packages-user/data-base/src/map/tile.ts b/packages-user/data-base/src/map/tile.ts index e5aede3..68082ff 100644 --- a/packages-user/data-base/src/map/tile.ts +++ b/packages-user/data-base/src/map/tile.ts @@ -13,6 +13,7 @@ import { ILayerEventView, IMapBlockSaveBase, IMapLayer, + IReadonlyTileBase, ITileBase } from './types'; @@ -81,6 +82,14 @@ export abstract class MapTileBase return this.layer.event(this.locator.x, this.locator.y); } + syncTileEvent(origin: IReadonlyTileBase): void { + const ev = this.tileEvents; + ev.clear(); + for (const [priority, id] of origin.tileEvent()) { + ev.set(priority, id); + } + } + abstract saveState(): Readonly; abstract loadState(save: Readonly): void; diff --git a/packages-user/data-base/src/map/types.ts b/packages-user/data-base/src/map/types.ts index 88ecb47..192ba16 100644 --- a/packages-user/data-base/src/map/types.ts +++ b/packages-user/data-base/src/map/types.ts @@ -35,6 +35,8 @@ export interface IDynamicBlockSave extends IMapBlockSaveBase { } export interface IReadonlyEventView { + [Symbol.iterator](): Iterator<[priority: number, event: string]>; + /** * 获取所有的事件 * @returns 键表示优先级,值表示事件在 `IGameEventStore` 的索引 @@ -131,6 +133,12 @@ export interface ITileBase extends IReadonlyTileBase { * 获取此图块所在位置的点事件 */ pointEvent(): ILayerEventView | null; + + /** + * 复制某个图块的图块事件至当前图块 + * @param origin 要从哪个图块复制 + */ + syncTileEvent(origin: IReadonlyTileBase): void; } export interface IStaticTile @@ -359,7 +367,7 @@ interface ILayerStatic { setStaticDirection(x: number, y: number, direction: FaceDirection): number; /** - * 迭代所有的图块 + * 迭代所有的非空图块。非空定义为此格图块数字不为 0,因此哪怕有事件也会视为空图块 */ iterateBlocks(): Iterable; } @@ -423,14 +431,6 @@ interface ILayerDynamic { */ iterateDynamicTiles(): Iterable; - /** - * 设置动态图块的朝向 - * @param tile 要设置朝向的动态图块 - * @param direction 目标朝向 - * @returns 更新朝向后该动态图块的图块数字 - */ - setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): number; - /** * 更新动态图块位置,用于在图块发生移动时更新内部存储,一般不需要手动调用 * @param tile 动态图块 @@ -477,6 +477,9 @@ export interface IMapLayer ILayerStatic, ILayerDynamic, ISaveableContent { + /** 地图使用的坐标索引器 */ + readonly indexer: ILocationHelper; + /** 地图宽度 */ readonly width: number; /** 地图高度 */ diff --git a/packages-user/data-common/src/common/indexer.test.ts b/packages-user/data-common/src/common/indexer.test.ts index cfc0514..09ad7ac 100644 --- a/packages-user/data-common/src/common/indexer.test.ts +++ b/packages-user/data-common/src/common/indexer.test.ts @@ -14,11 +14,11 @@ describe('MapLocIndexer', () => { it('agrees between the coordinate and locator forms', () => { const indexer = createIndexer(5); - expect(indexer.locToIndex(0, 0)).toBe(0); - expect(indexer.locToIndex(2, 3)).toBe(17); - expect(indexer.locaterToIndex({ x: 2, y: 3 })).toBe(17); - expect(indexer.locaterToIndex({ x: 2, y: 3 })).toBe( - indexer.locToIndex(2, 3) + expect(indexer.index(0, 0)).toBe(0); + expect(indexer.index(2, 3)).toBe(17); + expect(indexer.locatorToIndex({ x: 2, y: 3 })).toBe(17); + expect(indexer.locatorToIndex({ x: 2, y: 3 })).toBe( + indexer.index(2, 3) ); }); @@ -27,9 +27,9 @@ describe('MapLocIndexer', () => { const indexer = createIndexer(5); for (let index = 0; index < 20; index++) { - const locator = indexer.indexToLocator(index); - expect(indexer.locToIndex(locator.x, locator.y)).toBe(index); - expect(indexer.locaterToIndex(locator)).toBe(index); + const locator = indexer.locator(index); + expect(indexer.index(locator.x, locator.y)).toBe(index); + expect(indexer.locatorToIndex(locator)).toBe(index); } }); @@ -37,21 +37,21 @@ describe('MapLocIndexer', () => { it('splits a flat index into its column and row', () => { const indexer = createIndexer(4); - expect(indexer.indexToLocator(0)).toEqual({ x: 0, y: 0 }); - expect(indexer.indexToLocator(3)).toEqual({ x: 3, y: 0 }); - expect(indexer.indexToLocator(4)).toEqual({ x: 0, y: 1 }); - expect(indexer.indexToLocator(9)).toEqual({ x: 1, y: 2 }); + expect(indexer.locator(0)).toEqual({ x: 0, y: 0 }); + expect(indexer.locator(3)).toEqual({ x: 3, y: 0 }); + expect(indexer.locator(4)).toEqual({ x: 0, y: 1 }); + expect(indexer.locator(9)).toEqual({ x: 1, y: 2 }); }); // 验证 setWidth 改变行步长后,同一坐标映射到新索引 it('changes the row stride after setWidth', () => { const indexer = createIndexer(4); - expect(indexer.locToIndex(1, 2)).toBe(9); + expect(indexer.index(1, 2)).toBe(9); indexer.setWidth(10); - expect(indexer.locToIndex(1, 2)).toBe(21); - expect(indexer.indexToLocator(21)).toEqual({ x: 1, y: 2 }); + expect(indexer.index(1, 2)).toBe(21); + expect(indexer.locator(21)).toEqual({ x: 1, y: 2 }); }); }); diff --git a/packages-user/data-common/src/common/indexer.ts b/packages-user/data-common/src/common/indexer.ts index c5532f3..ba3973b 100644 --- a/packages-user/data-common/src/common/indexer.ts +++ b/packages-user/data-common/src/common/indexer.ts @@ -8,19 +8,19 @@ export interface ILocationHelper { * @param x 横坐标 * @param y 纵坐标 */ - locToIndex(x: number, y: number): number; + index(x: number, y: number): number; /** * 定位符 -> 索引 * @param locator 定位符 */ - locaterToIndex(locator: ITileLocator): number; + locatorToIndex(locator: ITileLocator): number; /** * 索引 -> 定位符 * @param index 索引 */ - indexToLocator(index: number): ITileLocator; + locator(index: number): ITileLocator; } export interface ILocationIndexer extends ILocationHelper { @@ -42,15 +42,15 @@ export class MapLocIndexer implements ILocationIndexer { this.width = width; } - locToIndex(x: number, y: number): number { + index(x: number, y: number): number { return y * this.width + x; } - locaterToIndex(locator: ITileLocator): number { + locatorToIndex(locator: ITileLocator): number { return locator.y * this.width + locator.x; } - indexToLocator(index: number): ITileLocator { + locator(index: number): ITileLocator { return { x: index % this.width, y: Math.floor(index / this.width) diff --git a/packages-user/data-system/src/combat/context.test.ts b/packages-user/data-system/src/combat/context.test.ts index f2f81fb..972f472 100644 --- a/packages-user/data-system/src/combat/context.test.ts +++ b/packages-user/data-system/src/combat/context.test.ts @@ -425,7 +425,7 @@ describe('EnemyContext registry and lookups', () => { expect(fixture.context.width).toBe(5); expect(fixture.context.height).toBe(2); - expect(fixture.context.indexer.locToIndex(3, 1)).toBe(8); + expect(fixture.context.indexer.index(3, 1)).toBe(8); expect(fixture.context.getEnemyByLocator({ x: 1, y: 0 })).toBeNull(); }); diff --git a/packages-user/data-system/src/combat/context.ts b/packages-user/data-system/src/combat/context.ts index 2eb2a00..c4fd859 100644 --- a/packages-user/data-system/src/combat/context.ts +++ b/packages-user/data-system/src/combat/context.ts @@ -222,7 +222,7 @@ export class EnemyContext implements IEnemyContext< getEnemyLocator(enemy: IEnemy): Readonly | null { const index = this.locatorEnemyMap.get(enemy); if (index === undefined) return null; - return this.indexer.indexToLocator(index); + return this.indexer.locator(index); } getEnemyLocatorByView( @@ -230,16 +230,16 @@ export class EnemyContext implements IEnemyContext< ): Readonly | null { const index = this.locatorViewMap.get(view); if (index === undefined) return null; - return this.indexer.indexToLocator(index); + return this.indexer.locator(index); } getEnemyByLocator(locator: ITileLocator): IEnemyView | null { - const index = this.indexer.locToIndex(locator.x, locator.y); + const index = this.indexer.index(locator.x, locator.y); return this.enemyViewMap.get(index) ?? null; } getEnemyByLoc(x: number, y: number): IEnemyView | null { - const index = this.indexer.locToIndex(x, y); + const index = this.indexer.index(x, y); return this.enemyViewMap.get(index) ?? null; } @@ -278,7 +278,7 @@ export class EnemyContext implements IEnemyContext< } setEnemyAt(locator: ITileLocator, enemy: IEnemy): void { - const index = this.indexer.locToIndex(locator.x, locator.y); + const index = this.indexer.index(locator.x, locator.y); this.deleteEnemyAt(index); const view = new EnemyView(enemy, this); @@ -299,7 +299,7 @@ export class EnemyContext implements IEnemyContext< } deleteEnemy(locator: ITileLocator): void { - const index = this.indexer.locToIndex(locator.x, locator.y); + const index = this.indexer.index(locator.x, locator.y); this.deleteEnemyAt(index); } @@ -319,7 +319,7 @@ export class EnemyContext implements IEnemyContext< for (const index of matched) { const view = viewMap.get(index); if (view) { - const locator = this.indexer.indexToLocator(index); + const locator = this.indexer.locator(index); yield [locator, view]; } } @@ -334,7 +334,7 @@ export class EnemyContext implements IEnemyContext< *iterateEnemy(): Iterable<[ITileLocator, IEnemyView]> { for (const [index, view] of this.enemyViewMap) { - const locator = this.indexer.indexToLocator(index); + const locator = this.indexer.locator(index); yield [locator, view]; } } @@ -514,7 +514,7 @@ export class EnemyContext implements IEnemyContext< const modifier = effect.for(this); for (const [index, view] of this.enemyViewMap) { - const locator = this.indexer.indexToLocator(index); + const locator = this.indexer.locator(index); const enemy = view.getComputingEnemy(); const handler = this.createHandler(enemy, locator); @@ -568,7 +568,7 @@ export class EnemyContext implements IEnemyContext< for (const [index, view] of this.enemyViewMap) { const enemy = view.getComputingEnemy(); - const locator = this.indexer.indexToLocator(index); + const locator = this.indexer.locator(index); const handler = this.createHandler(enemy, locator); for (const special of enemy.iterateSpecials()) { @@ -647,7 +647,7 @@ export class EnemyContext implements IEnemyContext< private buildupQuery(): void { for (const [index, view] of this.enemyViewMap) { const enemy = view.getComputingEnemy(); - const locator = this.indexer.indexToLocator(index); + const locator = this.indexer.locator(index); const handler = this.createHandler(enemy, locator); let queried = false; const query = () => { @@ -673,7 +673,7 @@ export class EnemyContext implements IEnemyContext< private buildupFinal(): void { for (const [index, view] of this.enemyViewMap) { const enemy = view.getComputingEnemy(); - const locator = this.indexer.indexToLocator(index); + const locator = this.indexer.locator(index); const handler = this.createHandler(enemy, locator); for (const effect of this.finalEffects) { effect.apply(handler); diff --git a/packages-user/data-system/src/combat/mapDamage.test.ts b/packages-user/data-system/src/combat/mapDamage.test.ts index b82a2ef..303708d 100644 --- a/packages-user/data-system/src/combat/mapDamage.test.ts +++ b/packages-user/data-system/src/combat/mapDamage.test.ts @@ -326,9 +326,7 @@ function createFixture(): MapDamageFixture { markDirty: () => {} }; enemies.set(view, locator); - const damageView = new FakeView(7, [ - indexer.locToIndex(locator.x, locator.y) - ]); + const damageView = new FakeView(7, [indexer.index(locator.x, locator.y)]); const context = { state, indexer, @@ -611,11 +609,11 @@ describe('MapDamage sourced conversion and reduction', () => { // 验证伤害范围收缩后跌出范围的坐标不再保留旧缓存(CR-02 范围收缩) it('drops stale damage on indexes that fall out of a shrunken range', () => { const fixture = createFixture(); - const indexA = fixture.context.indexer.locToIndex( + const indexA = fixture.context.indexer.index( fixture.locator.x, fixture.locator.y ); - const indexB = fixture.context.indexer.locToIndex(2, 1); + const indexB = fixture.context.indexer.index(2, 1); const locatorB: ITileLocator = { x: 2, y: 1 }; const view = new FakeView(7, [indexA, indexB]); fixture.damage.useReducer(fixture.reducer); @@ -674,7 +672,7 @@ describe('MapDamage multi-source stacking', () => { // 验证同一点上两条有来源伤害会分别保留并求和合并 it('stacks two sourced damages at the same point', () => { const fixture = createFixture(); - const index = fixture.context.indexer.locToIndex( + const index = fixture.context.indexer.index( fixture.locator.x, fixture.locator.y ); @@ -717,7 +715,7 @@ describe('MapDamage multi-source stacking', () => { // 验证多来源(有来源与无来源混合)叠加后伤害求和、类型取最大、额外标记取并集 it('merges mixed sourced and sourceless damages into one reduced result', () => { const fixture = createFixture(); - const index = fixture.context.indexer.locToIndex( + const index = fixture.context.indexer.index( fixture.locator.x, fixture.locator.y ); diff --git a/packages-user/data-system/src/combat/mapDamage.ts b/packages-user/data-system/src/combat/mapDamage.ts index 204348b..5e17fb6 100644 --- a/packages-user/data-system/src/combat/mapDamage.ts +++ b/packages-user/data-system/src/combat/mapDamage.ts @@ -102,7 +102,7 @@ export class MapDamage implements IMapDamage { } addMapDamage(locator: ITileLocator, info: IMapDamageInfo): void { - const index = this.indexer.locaterToIndex(locator); + const index = this.indexer.locatorToIndex(locator); const store = this.sourcelessDamage.getOrInsertComputed(index, () => ({ affectedBy: new Set(), damages: new Set() @@ -112,7 +112,7 @@ export class MapDamage implements IMapDamage { } deleteMapDamage(locator: ITileLocator, info: IMapDamageInfo): void { - const index = this.indexer.locaterToIndex(locator); + const index = this.indexer.locatorToIndex(locator); const current = this.sourcelessDamage.get(index); if (!current) return; current.damages.delete(info); @@ -132,7 +132,7 @@ export class MapDamage implements IMapDamage { } markDirty(locator: ITileLocator): void { - this.markDirtyIndex(this.indexer.locaterToIndex(locator)); + this.markDirtyIndex(this.indexer.locatorToIndex(locator)); } markEnemyDirty(view: IEnemyView): void { @@ -179,7 +179,7 @@ export class MapDamage implements IMapDamage { return null; } - const index = this.indexer.locaterToIndex(locator); + const index = this.indexer.locatorToIndex(locator); if (this.dirtyIndexes.has(index)) { this.refreshIndex(index); } @@ -221,7 +221,7 @@ export class MapDamage implements IMapDamage { getSeparatedDamage( locator: ITileLocator ): Iterable> { - const index = this.indexer.locaterToIndex(locator); + const index = this.indexer.locatorToIndex(locator); if (this.dirtyIndexes.has(index)) { this.refreshIndex(index); } @@ -319,7 +319,7 @@ export class MapDamage implements IMapDamage { const param = viewItem.getRangeParam(); range.bindHost(this.context); for (const index of range.iterateLoc(param)) { - const loc = this.indexer.indexToLocator(index); + const loc = this.indexer.locator(index); const point = this.sourcedDamage.getOrInsertComputed( index, () => ({ @@ -383,7 +383,7 @@ export class MapDamage implements IMapDamage { this.dirtyIndexes.delete(index); this.reducedCache.delete(index); - const locator = this.indexer.indexToLocator(index); + const locator = this.indexer.locator(index); const point = this.sourcedDamage.get(index); if (!point) return; diff --git a/packages-user/data-system/src/path/finder.ts b/packages-user/data-system/src/path/finder.ts index 64be77b..9e2f281 100644 --- a/packages-user/data-system/src/path/finder.ts +++ b/packages-user/data-system/src/path/finder.ts @@ -138,8 +138,8 @@ export class PathFinder implements IPathFinder { target: ITileLocator ): IPathfindingStep[] { const indexer = this.graph.indexer; - const startIndex = indexer.locaterToIndex(start); - const targetIndex = indexer.locaterToIndex(target); + const startIndex = indexer.locatorToIndex(start); + const targetIndex = indexer.locatorToIndex(target); if (startIndex === targetIndex) return []; if (!graph.nodes.has(startIndex) || !graph.nodes.has(targetIndex)) { return []; @@ -190,7 +190,7 @@ export class PathFinder implements IPathFinder { const step = prev.get(curr); if (!step) return []; steps.push(step); - curr = indexer.locaterToIndex(step.from); + curr = indexer.locatorToIndex(step.from); } steps.reverse(); return steps; diff --git a/packages-user/data-system/src/path/graph.ts b/packages-user/data-system/src/path/graph.ts index f89211c..f7d3fef 100644 --- a/packages-user/data-system/src/path/graph.ts +++ b/packages-user/data-system/src/path/graph.ts @@ -89,7 +89,7 @@ export class MapGraphBuilder implements IMapGraphBuilder { const terminals: Set = new Set(); const adjacency: Map = new Map(); const mapped: Set = new Set(); - const startIndex = indexer.locaterToIndex(start); + const startIndex = indexer.locatorToIndex(start); adjacency.set(startIndex, []); // 以起始位置为中心 BFS,仅沿可通行有向边扩展,不可达区域不入图 @@ -97,7 +97,7 @@ export class MapGraphBuilder implements IMapGraphBuilder { let head = 0; while (head < queue.length) { const { x, y } = queue[head++]!; - const index = indexer.locToIndex(x, y); + const index = indexer.index(x, y); const edges: IPathGraphEdge[] = []; for (const [dir, desc] of face.mapMovement()) { // 如果连接原地,那么应该忽略,避免陷入死循环 @@ -106,7 +106,7 @@ export class MapGraphBuilder implements IMapGraphBuilder { const ny = y + desc.y; if (!layer.inMap(nx, ny)) continue; - const nextIndex = indexer.locToIndex(nx, ny); + const nextIndex = indexer.index(nx, ny); const next: ITileLocator = { x: nx, y: ny }; const handler: IPassCheckHandler = { currLoc: { x, y }, @@ -132,7 +132,7 @@ export class MapGraphBuilder implements IMapGraphBuilder { const nodes: Map = new Map(); for (const index of mapped) { - const { x, y } = indexer.indexToLocator(index); + const { x, y } = indexer.locator(index); const block = layer.getLocationData(x, y)!; nodes.set(index, { x, diff --git a/perf.md b/perf.md index 3f6a302..15aa7eb 100644 --- a/perf.md +++ b/perf.md @@ -1,5 +1,16 @@ # `Map.getOrInsert` vs `Map.getOrInsertComputed` +所有需要默认值的 Map get 操作,全部使用 `Map.getOrInsert` 或 `Map.getOrInsertComputed`,不得自己写类似下面这种的代码: + +```ts +let list = map.get(key); +if (!list) { + list = []; + map.set(key, list); +} +return list; +``` + - 使用 `Map.getOrInsert`: 数字、短字符串(约 100 以内)、布尔值、空数组、空对象、对象引用 - 使用 `Map.getOrInsertComputed`: 长字符串、空 Set、空 Map、自定义类构建出的实例(new Class())、有内容的数组、有内容的对象、有内容的 Set、有内容的 Map。 - 使用哪个都可以: 函数