import { isNil } from 'lodash-es'; import { IDynamicBlockSave, IDynamicTile, IGameMap, ILayerLocation, IMapLayer, IMapLayerData, IMapLayerHookController, IMapLayerHooks, IMapLayerSave, IStaticBlockSave, IStaticTile } from './types'; import { Hookable, HookController, ITileLocator, logger } from '@motajs/common'; import { degradeFace, FaceDirection, IDataCommon, IRoleFaceBinder, SaveCompression } from '@user/data-common'; import { DynamicTile } from './dynamicTile'; import { StaticTile } from './staticTile'; export class MapLayer extends Hookable implements IMapLayer { readonly state: IDataCommon; width: number; height: number; empty: boolean = true; zIndex: number = 0; faceBinder: IRoleFaceBinder; /** 地图图块数组 */ private mapArray: Uint32Array; /** 地图数据引用 */ private mapData: IMapLayerData; /** 静态图块实例缓存,key = y * width + x */ private readonly staticTileCache: Map = new Map(); /** 坐标到动态图块集合的映射,外层 key = y,内层 key = x */ private readonly tilePosMap: Map>> = new Map(); /** 动态图块到其当前坐标的映射 */ private readonly posTileMap: Map = new Map(); /** 图层脏标记 */ private layerDirty: boolean = false; /** 图层参考基准,用于存档压缩对比 */ private refArray: Uint32Array | null = null; constructor( array: Uint32Array, width: number, height: number, public readonly map: IGameMap ) { super(); this.state = map.state; this.faceBinder = this.state.roleFace; this.width = width; this.height = height; const area = width * height; this.mapArray = new Uint32Array(area); this.mapArray.set(array); this.mapData = { expired: false, array: this.mapArray }; } /** * 将动态图块登记到指定坐标的索引表中 * @param tile 动态图块 * @param x 横坐标 * @param y 纵坐标 */ private addTileToPosMap(tile: IDynamicTile, x: number, y: number): void { const xMap = this.tilePosMap.getOrInsertComputed(y, () => new Map()); const set = xMap.getOrInsertComputed(x, () => new Set()); set.add(tile); } /** * 将动态图块从指定坐标的索引表中移除 * @param tile 动态图块 * @param x 横坐标 * @param y 纵坐标 */ private removeTileFromPosMap( tile: IDynamicTile, x: number, y: number ): void { this.tilePosMap.get(y)?.get(x)?.delete(tile); } /** * 从两个内部映射中移除图块记录 * @param tile 动态图块 */ private removeTile(tile: IDynamicTile): void { const pos = this.posTileMap.get(tile); if (pos) { this.removeTileFromPosMap(tile, pos.x, pos.y); } this.posTileMap.delete(tile); } /** * 将动态图块的触发器同步回当前静态格点 * @param tile 动态图块 * @param keepTrigger 是否保留触发器 */ private syncStaticTrigger(tile: IDynamicTile, keepTrigger: boolean): void { const staticTile = this.getTile(tile.x, tile.y); if (!staticTile) return; if (keepTrigger) { const triggers = tile.triggers; if (!triggers) { staticTile.clearTrigger(); } else { staticTile.clearTrigger(); for (const trigger of triggers) { staticTile.addTrigger(trigger); } } } else { staticTile.clearTrigger(); } } //#region 静态图层操作 inMap(x: number, y: number): boolean { return x >= 0 && y >= 0 && x < this.width && y < this.height; } setBlock(block: number, x: number, y: number): void { const index = y * this.width + x; if (block === this.mapArray[index]) return; this.mapArray[index] = block; this.layerDirty = true; this.forEachHook(hook => { hook.onUpdateBlock?.(block, x, y); }); if (block !== 0) { this.empty = false; } } getBlock(x: number, y: number): number { if (!this.inMap(x, y)) { return -1; } return this.mapArray[y * this.width + x]; } getTile(x: number, y: number): IStaticTile | null { if (!this.inMap(x, y)) return null; const index = this.map.indexer.locToIndex(x, y); let staticTile = this.staticTileCache.get(index); if (!staticTile) { staticTile = new StaticTile(x, y, this); this.staticTileCache.set(index, staticTile); } return staticTile; } getLocationData(x: number, y: number): ILayerLocation | null { const staticTile = this.getTile(x, y); if (!staticTile) return null; const num = staticTile.num(); const dynamics = this.getDynamicTilesAt(x, y); return { locator: { x, y }, tile: num, dynamics, static: staticTile }; } putMapData(array: Uint32Array, x: number, y: number, width: number): void { if (array.length % width !== 0) { logger.warn(8); } this.layerDirty = true; const h = Math.ceil(array.length / width); if (width === this.width && h === this.height) { this.mapArray.set(array); this.staticTileCache.clear(); this.forEachHook(hook => { hook.onUpdateArea?.(x, y, width, h); }); return; } const w = this.width; const r = x + width; const b = y + h; if (x < 0 || y < 0 || r > w || b > this.height) { logger.warn(9); } 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; let empty = true; for (let ny = 0; ny < nh; ny++) { const start = ny * nw; const offset = (ny + nt) * w + nl; const sub = array.subarray(start, start + nw); if (empty && sub.some(v => v !== 0)) { empty = false; } this.mapArray.set(array.subarray(start, start + nw), offset); } this.forEachHook(hook => { hook.onUpdateArea?.(x, y, width, h); }); this.empty &&= empty; } getMapData(): Uint32Array; getMapData( x: number, y: number, width: number, height: number ): Uint32Array; getMapData( x?: number, y?: number, width?: number, height?: number ): Uint32Array { if (isNil(x)) { return new Uint32Array(this.mapArray); } if (isNil(y) || isNil(width) || isNil(height)) { logger.warn(80); return new Uint32Array(); } const w = this.width; const h = this.height; const r = x + width; const b = y + height; if (x < 0 || y < 0 || r > w || b > h) { logger.warn(81); } const res = new Uint32Array(width * height); 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; const dy = ny - y; res.set(arr.subarray(lineStart, lineEnd), dy * width); } return res; } setMapRef(array: Uint32Array): void { if (array.length !== this.width * this.height) { logger.warn( 123, array.length.toString(), (this.width * this.height).toString() ); return; } this.mapData.expired = true; this.mapArray = array; this.staticTileCache.clear(); this.mapData = { expired: false, array: this.mapArray }; this.empty = !array.some(v => v !== 0); this.forEachHook(hook => { hook.onUpdateArea?.(0, 0, this.width, this.height); }); } getMapRef(): IMapLayerData { return this.mapData; } setStaticDirection(x: number, y: number, direction: FaceDirection): number { const tile = this.getTile(x, y); if (!tile) return -1; return tile.setFaceDirection(direction); } *iterateBlocks(): Iterable { for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { const num = this.getBlock(x, y); if (num !== 0) { yield this.getLocationData(x, y)!; } } } } //#endregion //#region 动态图层操作 createDynamic(num: number, x: number, y: number): IDynamicTile { const tile = new DynamicTile(num, x, y, this); const location = this.getLocationData(x, y); if (location) { const staticTriggers = location.static.triggers; if (staticTriggers) { for (const trigger of staticTriggers) { tile.addTrigger(trigger); } } } this.addTileToPosMap(tile, x, y); this.posTileMap.set(tile, { x, y }); this.forEachHook(hook => hook.onCreateDynamic?.(tile)); return tile; } transferToDynamic( x: number, y: number, keepTrigger: boolean = true ): IDynamicTile | null { if (!this.inMap(x, y)) { logger.warn(131, x.toString(), y.toString()); return null; } const num = this.getBlock(x, y); if (num === 0) { logger.warn(127, x.toString(), y.toString()); } this.setBlock(0, x, y); const tile = this.createDynamic(num, x, y); const staticTile = this.getTile(x, y); if (staticTile) { staticTile.clearTrigger(); } if (!keepTrigger) { tile.clearTrigger(); } return tile; } transferToStatic( tile: IDynamicTile, keepTrigger: boolean = true ): IStaticTile | null { const x = tile.x; const y = tile.y; if (x < 0 || y < 0 || x >= this.width || y >= this.height) { logger.warn(128, x.toString(), y.toString()); return null; } if (this.getBlock(x, y) !== 0) { logger.warn(129, x.toString(), y.toString()); } this.setBlock(tile.num(), x, y); this.syncStaticTrigger(tile, keepTrigger); this.removeTile(tile); this.forEachHook(hook => hook.onDeleteDynamic?.(tile)); return this.getTile(x, y); } transferToStaticIfSafe( tile: IDynamicTile, keepTrigger: boolean = true ): IStaticTile | null { const x = tile.x; const y = tile.y; if (x < 0 || y < 0 || x >= this.width || y >= this.height) { logger.warn(128, x.toString(), y.toString()); return null; } if (this.getBlock(tile.x, tile.y) !== 0) return null; this.setBlock(tile.num(), x, y); this.syncStaticTrigger(tile, keepTrigger); this.removeTile(tile); this.forEachHook(hook => hook.onDeleteDynamic?.(tile)); return this.getTile(x, y); } async deleteDynamic(tile: IDynamicTile): Promise { if (!this.posTileMap.has(tile)) { logger.warn(130); return; } this.removeTile(tile); const hooks = this.forEachHook(hook => hook.onDeleteDynamic?.(tile)); await Promise.all(hooks); } getDynamicTilesAt(x: number, y: number): Iterable { return this.tilePosMap.get(y)?.get(x) ?? new Set(); } iterateDynamicTiles(): Iterable { return this.posTileMap.keys(); } setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): number { const numBefore = tile.num(); tile.setFaceDirection(direction); if (tile.num() !== numBefore) return tile.num(); const degraded = degradeFace(direction); if (degraded !== direction) { tile.setFaceDirection(degraded); } return tile.num(); } updateDynamicTile(tile: IDynamicTile): void { const oldPos = this.posTileMap.get(tile); if (oldPos) { this.removeTileFromPosMap(tile, oldPos.x, oldPos.y); oldPos.x = tile.x; oldPos.y = tile.y; this.addTileToPosMap(tile, tile.x, tile.y); } else { this.addTileToPosMap(tile, tile.x, tile.y); this.posTileMap.set(tile, { x: tile.x, y: tile.y }); } this.forEachHook(hook => hook.onUpdateDynamicPosition?.(tile)); } //#endregion //#region 开关门 async openDoor(x: number, y: number): Promise { const index = y * this.width + x; const num = this.mapArray[index]; if (num === 0) return; await Promise.all( this.forEachHook(hook => { return hook.onOpenDoor?.(x, y); }) ); this.setBlock(0, x, y); } async closeDoor(num: number, x: number, y: number): Promise { const index = y * this.width + x; const nowNum = this.mapArray[index]; if (nowNum !== 0) { logger.error(46, x.toString(), y.toString()); return; } await Promise.all( this.forEachHook(hook => { return hook.onCloseDoor?.(num, x, y); }) ); this.setBlock(num, x, y); } //#endregion //#region 图层操作 setZIndex(zIndex: number): void { this.zIndex = zIndex; } setFaceBinder(binder: IRoleFaceBinder | null): void { if (!binder) return; this.faceBinder = binder; } dirty(): boolean { return this.layerDirty; } markDirty(dirty: boolean): void { this.layerDirty = dirty; } /** * 判断当前图层的地图矩阵是否与参考基准完全一致 */ private isEqualToRef(): boolean { const ref = this.refArray; if (!ref) return false; if (this.mapArray.length !== ref.length) return false; return !this.mapArray.some((v, i) => ref[i] !== v); } compareWith(data: Uint32Array): void { if (this.refArray) return; this.refArray = data; this.layerDirty = !this.isEqualToRef(); } protected createController( hook: Partial ): IMapLayerHookController { return new MapLayerHookController(this, hook); } resize(width: number, height: number): void { if (this.width === width && this.height === height) { return; } this.layerDirty = true; this.mapData.expired = true; const before = this.mapArray; const beforeWidth = this.width; const beforeHeight = this.height; const beforeArea = beforeWidth * beforeHeight; this.width = width; this.height = height; 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; newArray.set(before.subarray(begin, begin + width), ny * width); } } else { for (let ny = 0; ny < beforeHeight; ny++) { const begin = ny * beforeWidth; newArray.set( before.subarray(begin, begin + beforeWidth), ny * width ); } } this.mapData = { expired: false, array: this.mapArray }; this.forEachHook(hook => { hook.onResize?.(width, height); }); } resize2(width: number, height: number): void { this.layerDirty = true; 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.forEachHook(hook => { hook.onResize?.(width, height); }); } //#endregion //#region 存读档 /** * 保存静态图块实例 */ private saveStatics(): Map { const blocks = new Map(); for (const location of this.iterateBlocks()) { const tile = location.static; if (!tile.shouldSave()) continue; const index = this.map.indexer.locaterToIndex(location.locator); blocks.set(index, tile.saveState(SaveCompression.NoCompression)); } return blocks; } /** * 保存动态图块实例 */ private saveDynamics(): Map { const blocks = new Map(); for (const tile of this.iterateDynamicTiles()) { const index = this.map.indexer.locaterToIndex(tile.locator); const list = blocks.getOrInsert(index, []); list.push(tile.saveState(SaveCompression.NoCompression)); } return blocks; } /** * 与参考行比较,返回与参考基准不同的行 * @param refArray 参考基准数组 */ private diffRows(refArray: Uint32Array): Map { const rows = new Map(); for (let row = 0; row < this.height; row++) { const start = row * this.width; const end = start + this.width; const slice = this.mapArray.subarray(start, end); const refSlice = refArray.subarray(start, end); const same = refSlice.every((v, i) => slice[i] === v); if (!same) { rows.set(row, new Uint32Array(slice)); } } return rows; } /** * 以无压缩方式序列化当前图层 */ private saveNoCompression(): IMapLayerSave { return { width: this.width, height: this.height, fullMap: new Uint32Array(this.mapArray), staticBlocks: this.saveStatics(), dynamicBlocks: this.saveDynamics() }; } /** * 以低压缩方式序列化当前图层 */ private saveLowCompression(): IMapLayerSave { const staticBlocks = this.saveStatics(); const dynamicBlocks = this.saveDynamics(); if (this.layerDirty && (!this.refArray || !this.isEqualToRef())) { return { width: this.width, height: this.height, fullMap: new Uint32Array(this.mapArray), staticBlocks, dynamicBlocks }; } else { return { width: this.width, height: this.height, staticBlocks, dynamicBlocks }; } } /** * 以高压缩方式序列化当前图层 */ private saveHighCompression(): IMapLayerSave { const staticBlocks = this.saveStatics(); const dynamicBlocks = this.saveDynamics(); if (this.layerDirty) { if (this.refArray) { return { width: this.width, height: this.height, rows: this.diffRows(this.refArray), staticBlocks, dynamicBlocks }; } else { return { width: this.width, height: this.height, fullMap: new Uint32Array(this.mapArray), staticBlocks, dynamicBlocks }; } } else { return { width: this.width, height: this.height, staticBlocks, dynamicBlocks }; } } saveState(compression: SaveCompression): IMapLayerSave { if (compression === SaveCompression.HighCompression) { return this.saveHighCompression(); } else if (compression === SaveCompression.LowCompression) { return this.saveLowCompression(); } else { return this.saveNoCompression(); } } /** * 读取静态图块实例存档数据 * @param save 静态图块实例存档 */ private loadStatics(save: ReadonlyMap): void { for (const [index, tileSave] of save) { const { x, y } = this.map.indexer.indexToLocator(index); const location = this.getLocationData(x, y); if (!location) continue; location.static.loadState(tileSave, SaveCompression.NoCompression); } } /** * 读取动态图块实例存档数据 * @param save 动态图块实例存档 */ private loadDynamics(save: ReadonlyMap): void { for (const [index, dynamics] of save) { const { x, y } = this.map.indexer.indexToLocator(index); for (const block of dynamics) { const tile = this.createDynamic(block.num, x, y); tile.loadState(block, SaveCompression.NoCompression); } } } /** * 以无压缩方式读取当前图层 * @param save 图层存档 */ private loadNoCompression(save: IMapLayerSave): void { if (save.fullMap) { this.setMapRef(new Uint32Array(save.fullMap)); } if (save.staticBlocks) { this.loadStatics(save.staticBlocks); } if (save.dynamicBlocks) { this.loadDynamics(save.dynamicBlocks); } this.layerDirty = !this.isEqualToRef(); } /** * 以低压缩方式读取当前图层 * @param save 图层存档 */ private loadLowCompression(save: IMapLayerSave): void { if (save.fullMap) { this.setMapRef(new Uint32Array(save.fullMap)); this.layerDirty = true; } else if (this.refArray) { this.setMapRef(new Uint32Array(this.refArray)); this.layerDirty = false; } else { logger.warn(124, this.zIndex.toString()); } if (save.staticBlocks) { this.loadStatics(save.staticBlocks); } if (save.dynamicBlocks) { this.loadDynamics(save.dynamicBlocks); } } /** * 以高压缩方式读取当前图层 * @param save 图层存档 */ private loadHighCompression(save: IMapLayerSave): void { if (save.rows && save.rows.size > 0) { if (this.refArray) { const buf = new Uint32Array(this.refArray); for (const [rowIdx, rowData] of save.rows) { buf.set(rowData, rowIdx * this.width); } this.setMapRef(buf); this.layerDirty = true; } else { logger.warn(124, this.zIndex.toString()); } } else if (this.refArray) { this.setMapRef(new Uint32Array(this.refArray)); this.layerDirty = false; } else { logger.warn(124, this.zIndex.toString()); } if (save.staticBlocks) { this.loadStatics(save.staticBlocks); } if (save.dynamicBlocks) { this.loadDynamics(save.dynamicBlocks); } } loadState(save: IMapLayerSave, compression: SaveCompression): void { if (compression === SaveCompression.HighCompression) { this.loadHighCompression(save); } else if (compression === SaveCompression.LowCompression) { this.loadLowCompression(save); } else { this.loadNoCompression(save); } } //#endregion } class MapLayerHookController extends HookController implements IMapLayerHookController { hookable: MapLayer; constructor( readonly layer: MapLayer, hook: Partial ) { super(layer, hook); this.hookable = layer; } getMapData(): Readonly { return this.layer.getMapRef(); } }