mirror of
https://github.com/motajs/template.git
synced 2026-11-12 14:04:31 +08:00
980 lines
31 KiB
TypeScript
980 lines
31 KiB
TypeScript
import { isNil } from 'lodash-es';
|
|
import {
|
|
IDynamicBlockSave,
|
|
IDynamicTile,
|
|
IGameMap,
|
|
ILayerEventView,
|
|
ILayerLocation,
|
|
IMapLayerData,
|
|
IMapLayerHookController,
|
|
IMapLayerHooks,
|
|
IMapLayerSave,
|
|
IResizableMapLayer,
|
|
IStaticBlockSave,
|
|
IStaticTile
|
|
} from './types';
|
|
import { Hookable, HookController, ITileLocator, logger } from '@motajs/common';
|
|
import {
|
|
FaceDirection,
|
|
IDataCommon,
|
|
ILocationHelper,
|
|
IRoleFaceBinder,
|
|
MapLocIndexer,
|
|
SaveCompression,
|
|
shouldReplay
|
|
} from '@user/data-common';
|
|
import { DynamicTile } from './dynamicTile';
|
|
import { LayerEventView } from './eventView';
|
|
import { StaticTile } from './staticTile';
|
|
|
|
export class MapLayer
|
|
extends Hookable<IMapLayerHooks, IMapLayerHookController>
|
|
implements IResizableMapLayer
|
|
{
|
|
readonly state: IDataCommon;
|
|
readonly indexer: ILocationHelper;
|
|
|
|
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<number, StaticTile> = new Map();
|
|
/** 坐标到动态图块集合的映射,外层 key = y,内层 key = x */
|
|
private readonly tilePosMap: Map<number, Map<number, Set<IDynamicTile>>> =
|
|
new Map();
|
|
/** 图层的所有已缓存位置的动态图块,目的是能够持有更新前的位置,从而在图块更新时能够更新内部存储 */
|
|
private readonly cachedDynamics: Map<IDynamicTile, ITileLocator> =
|
|
new Map();
|
|
/** 图层脏标记 */
|
|
private layerDirty: boolean = false;
|
|
/** 图层参考基准,用于存档压缩对比 */
|
|
private refArray: Uint32Array | null = null;
|
|
|
|
/** 点事件视图,key = y * width + x */
|
|
private readonly pointEvents: Map<number, ILayerEventView> = new Map();
|
|
|
|
constructor(
|
|
width: number,
|
|
height: number,
|
|
readonly map: IGameMap,
|
|
readonly alias: string
|
|
) {
|
|
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.mapData = {
|
|
expired: false,
|
|
array: this.mapArray
|
|
};
|
|
this.indexer = map.indexer;
|
|
}
|
|
|
|
protected createController(
|
|
hook: Partial<IMapLayerHooks>
|
|
): IMapLayerHookController {
|
|
return new MapLayerHookController(this, hook);
|
|
}
|
|
|
|
//#region 事件操作
|
|
|
|
/**
|
|
* 将所有已创建的点事件视图恢复到原始纯基准
|
|
*/
|
|
private resetPointEvents(): void {
|
|
this.pointEvents.forEach(v => v.reset());
|
|
}
|
|
|
|
event(x: number, y: number): ILayerEventView | null {
|
|
if (!this.inMap(x, y)) return null;
|
|
const index = this.indexer.index(x, y);
|
|
let eventView = this.pointEvents.get(index);
|
|
if (!eventView) {
|
|
eventView = new LayerEventView();
|
|
this.pointEvents.set(index, eventView);
|
|
}
|
|
return eventView;
|
|
}
|
|
|
|
getPointEvent(x: number, y: number): ReadonlyMap<number, string> | null {
|
|
return this.event(x, y)?.get() ?? null;
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region 静态图层操作
|
|
|
|
inMap(x: number, y: number): boolean {
|
|
return x >= 0 && y >= 0 && x < this.width && y < this.height;
|
|
}
|
|
|
|
@shouldReplay('Setting map layer block should be replayed.')
|
|
setBlock(block: number, x: number, y: number): void {
|
|
if (!this.inMap(x, y)) return;
|
|
const index = this.indexer.index(x, y);
|
|
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];
|
|
}
|
|
|
|
@shouldReplay('Removing map layer block should be replayed.')
|
|
removeBlock(x: number, y: number): number {
|
|
if (!this.inMap(x, y)) {
|
|
return -1;
|
|
}
|
|
const before = this.getBlock(x, y);
|
|
this.setBlock(0, x, y);
|
|
return before;
|
|
}
|
|
|
|
getTile(x: number, y: number): IStaticTile | null {
|
|
if (!this.inMap(x, y)) return null;
|
|
const index = this.map.indexer.index(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 {
|
|
if (!this.inMap(x, y)) return null;
|
|
const staticTile = this.getTile(x, y);
|
|
const num = staticTile?.num() ?? 0;
|
|
const dynamics = this.getDynamicTilesAt(x, y);
|
|
return {
|
|
locator: { x, y },
|
|
tile: num,
|
|
dynamics,
|
|
static: staticTile
|
|
};
|
|
}
|
|
|
|
@shouldReplay('Setting map layer data should be replayed.')
|
|
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 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 res = new Uint32Array(width * height);
|
|
const arr = this.mapArray;
|
|
for (let ny = nt; ny < nb; ny++) {
|
|
const lineStart = ny * w + nl;
|
|
const dy = ny - y;
|
|
res.set(arr.subarray(lineStart, lineStart + nw), 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;
|
|
}
|
|
|
|
@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;
|
|
return tile.setFaceDirection(direction);
|
|
}
|
|
|
|
*iterateBlocks(): Iterable<ILayerLocation> {
|
|
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 动态图层操作
|
|
|
|
/**
|
|
* 将动态图块登记到索引表中
|
|
* @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);
|
|
this.addDynamic(tile);
|
|
this.forEachHook(hook => hook.onCreateDynamic?.(tile));
|
|
return tile;
|
|
}
|
|
|
|
@shouldReplay('Transfering static tile to dynamic should be replayed.')
|
|
transferToDynamic(
|
|
x: number,
|
|
y: number,
|
|
keepEvent: boolean = true
|
|
): IDynamicTile | null {
|
|
if (!this.inMap(x, y)) {
|
|
logger.warn(128, 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) {
|
|
if (keepEvent) {
|
|
tile.syncTileEvent(staticTile);
|
|
}
|
|
staticTile.tileEvent().clear();
|
|
}
|
|
return tile;
|
|
}
|
|
|
|
/**
|
|
* 真正执行动态到静态的转换
|
|
* @param tile 动态图块实例
|
|
* @param keepEvent 是否保持图块事件至静态图层
|
|
*/
|
|
private toStatic(
|
|
tile: IDynamicTile,
|
|
keepEvent: boolean = true
|
|
): IStaticTile | null {
|
|
this.setBlock(tile.num(), tile.x, tile.y);
|
|
const staticTile = this.getTile(tile.x, tile.y);
|
|
if (!staticTile) return null;
|
|
if (keepEvent) {
|
|
staticTile.syncTileEvent(tile);
|
|
} else {
|
|
// 不保留事件时,将复用的静态图块重置为默认事件
|
|
staticTile.restoreDefaultEvents();
|
|
}
|
|
this.removeDynamic(tile, tile.x, tile.y);
|
|
this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
|
|
return staticTile;
|
|
}
|
|
|
|
@shouldReplay('Transfering dynamic tile to static should be replayed.')
|
|
transferToStatic(
|
|
tile: IDynamicTile,
|
|
keepEvent: boolean = true
|
|
): IStaticTile | null {
|
|
const { x, y } = tile;
|
|
if (!this.inMap(x, y)) {
|
|
logger.warn(128, x.toString(), y.toString());
|
|
return null;
|
|
}
|
|
if (this.getBlock(x, y) !== 0) {
|
|
logger.warn(129, x.toString(), y.toString());
|
|
}
|
|
return this.toStatic(tile, keepEvent);
|
|
}
|
|
|
|
@shouldReplay('Transfering dynamic tile to static should be replayed.')
|
|
transferToStaticIfSafe(
|
|
tile: IDynamicTile,
|
|
keepEvent: boolean = true
|
|
): IStaticTile | null {
|
|
if (!this.inMap(tile.x, tile.y)) {
|
|
logger.warn(128, tile.x.toString(), tile.y.toString());
|
|
return null;
|
|
}
|
|
if (this.getBlock(tile.x, tile.y) !== 0) return null;
|
|
return this.toStatic(tile, keepEvent);
|
|
}
|
|
|
|
@shouldReplay('Deleting dynamic tile should be replayed.')
|
|
async deleteDynamic(tile: IDynamicTile): Promise<void> {
|
|
this.removeDynamic(tile, tile.x, tile.y);
|
|
const hooks = this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
|
|
await Promise.all(hooks);
|
|
}
|
|
|
|
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile> {
|
|
return this.tilePosMap.get(y)?.get(x) ?? new Set();
|
|
}
|
|
|
|
iterateDynamicTiles(): Iterable<IDynamicTile> {
|
|
return this.cachedDynamics.keys();
|
|
}
|
|
|
|
@shouldReplay('Updating dynamic tile position should be replayed.')
|
|
updateDynamicTile(tile: IDynamicTile): void {
|
|
const oldPos = this.cachedDynamics.get(tile);
|
|
if (oldPos) {
|
|
this.removeDynamic(tile, oldPos.x, oldPos.y);
|
|
oldPos.x = tile.x;
|
|
oldPos.y = tile.y;
|
|
this.addDynamic(tile);
|
|
} else {
|
|
this.addDynamic(tile);
|
|
this.cachedDynamics.set(tile, { x: tile.x, y: tile.y });
|
|
}
|
|
this.forEachHook(hook => hook.onUpdateDynamicPosition?.(tile));
|
|
}
|
|
|
|
async moveBlock(
|
|
x: number,
|
|
y: number,
|
|
direction: FaceDirection,
|
|
count: number = 1,
|
|
time: number = 500,
|
|
keepEvent: boolean = true
|
|
): Promise<void> {
|
|
const dynamic = this.transferToDynamic(x, y, keepEvent);
|
|
if (!dynamic) return;
|
|
dynamic.mover.speed(time).step(direction, count);
|
|
await dynamic.mover.start()?.onEnd;
|
|
this.transferToStatic(dynamic, keepEvent);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region 开关门
|
|
|
|
@shouldReplay('Opening door should be replayed.')
|
|
async openDoor(x: number, y: number): Promise<void> {
|
|
const index = this.indexer.index(x, y);
|
|
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);
|
|
}
|
|
|
|
@shouldReplay('Closing door should be replayed.')
|
|
async closeDoor(num: number, x: number, y: number): Promise<void> {
|
|
const index = this.indexer.index(x, y);
|
|
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 图层操作
|
|
|
|
@shouldReplay('Setting z-index of map layer should be replayed.')
|
|
setZIndex(zIndex: number): void {
|
|
this.zIndex = zIndex;
|
|
}
|
|
|
|
@shouldReplay('Setting role face binder of map layer should be replayed.')
|
|
setFaceBinder(binder: IRoleFaceBinder | null): void {
|
|
if (!binder) return;
|
|
this.faceBinder = binder;
|
|
}
|
|
|
|
dirty(): boolean {
|
|
if (this.layerDirty) return true;
|
|
for (const eventView of this.pointEvents.values()) {
|
|
if (eventView.dirty()) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* 裁剪超出新图层范围的点事件,并按新宽度重建索引
|
|
* @param oldWidth 变更前的图层宽度,用于解码旧索引
|
|
*/
|
|
private cropPointEvents(oldWidth: number): void {
|
|
const oldIndex = new MapLocIndexer();
|
|
oldIndex.setWidth(oldWidth);
|
|
for (const [index, eventView] of [...this.pointEvents]) {
|
|
const { x, y } = oldIndex.locator(index);
|
|
this.pointEvents.delete(index);
|
|
if (this.inMap(x, y)) {
|
|
const index = this.indexer.index(x, y);
|
|
this.pointEvents.set(index, eventView);
|
|
}
|
|
}
|
|
}
|
|
|
|
@shouldReplay('Resizing map layer should be replayed.')
|
|
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;
|
|
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;
|
|
const end = begin + beforeWidth;
|
|
newArray.set(before.subarray(begin, end), ny * width);
|
|
}
|
|
}
|
|
this.mapData = {
|
|
expired: false,
|
|
array: this.mapArray
|
|
};
|
|
|
|
// 其他杂项清理
|
|
this.cropPointEvents(beforeWidth);
|
|
this.staticTileCache.clear();
|
|
this.forEachHook(hook => {
|
|
hook.onResize?.(width, height);
|
|
});
|
|
}
|
|
|
|
@shouldReplay('Resizing map layer should be replayed.')
|
|
resize2(width: number, height: number): void {
|
|
this.layerDirty = true;
|
|
if (this.width === width && this.height === height) {
|
|
this.empty = true;
|
|
this.mapArray.fill(0);
|
|
this.pointEvents.clear();
|
|
this.staticTileCache.clear();
|
|
return;
|
|
}
|
|
|
|
// 图层数组
|
|
this.mapData.expired = true;
|
|
this.width = width;
|
|
this.height = height;
|
|
this.mapArray = new Uint32Array(width * height);
|
|
this.mapData = {
|
|
expired: false,
|
|
array: this.mapArray
|
|
};
|
|
this.empty = true;
|
|
|
|
// 其他杂项清理
|
|
this.pointEvents.clear();
|
|
this.staticTileCache.clear();
|
|
this.forEachHook(hook => {
|
|
hook.onResize?.(width, height);
|
|
});
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region 存读档
|
|
|
|
/**
|
|
* 保存静态图块实例
|
|
* @param compression 压缩级别
|
|
*/
|
|
private saveStaticTiles(
|
|
compression: SaveCompression
|
|
): Map<number, IStaticBlockSave> {
|
|
const blocks = new Map<number, IStaticBlockSave>();
|
|
for (const location of this.iterateBlocks()) {
|
|
const tile = location.static;
|
|
if (!tile || !tile.shouldSave()) continue;
|
|
const index = this.map.indexer.locatorToIndex(location.locator);
|
|
blocks.set(index, tile.saveState(compression));
|
|
}
|
|
return blocks;
|
|
}
|
|
|
|
/**
|
|
* 保存动态图块实例
|
|
* @param compression 压缩级别
|
|
*/
|
|
private saveDynamicTiles(
|
|
compression: SaveCompression
|
|
): Map<number, IDynamicBlockSave[]> {
|
|
const blocks = new Map<number, IDynamicBlockSave[]>();
|
|
for (const tile of this.iterateDynamicTiles()) {
|
|
const index = this.map.indexer.locatorToIndex(tile.locator);
|
|
const list = blocks.getOrInsert(index, []);
|
|
list.push(tile.saveState(compression));
|
|
}
|
|
return blocks;
|
|
}
|
|
|
|
/**
|
|
* 与参考行比较,返回与参考基准不同的行
|
|
* @param refArray 参考基准数组
|
|
*/
|
|
private diffRows(refArray: Uint32Array): Map<number, Uint32Array> {
|
|
const rows = new Map<number, Uint32Array>();
|
|
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.saveStaticTiles(SaveCompression.NoCompression),
|
|
dynamicBlocks: this.saveDynamicTiles(SaveCompression.NoCompression),
|
|
pointEvents: this.savePointEvents()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 以低压缩方式序列化当前图层
|
|
*/
|
|
private saveLowCompression(): IMapLayerSave {
|
|
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: statics,
|
|
dynamicBlocks: dynamics,
|
|
pointEvents: this.savePointEvents()
|
|
};
|
|
} else {
|
|
return {
|
|
width: this.width,
|
|
height: this.height,
|
|
staticBlocks: statics,
|
|
dynamicBlocks: dynamics,
|
|
pointEvents: this.savePointEvents()
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 以高压缩方式序列化当前图层
|
|
*/
|
|
private saveHighCompression(): IMapLayerSave {
|
|
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: statics,
|
|
dynamicBlocks: dynamics,
|
|
pointEvents: this.savePointEvents()
|
|
};
|
|
} else {
|
|
return {
|
|
width: this.width,
|
|
height: this.height,
|
|
fullMap: new Uint32Array(this.mapArray),
|
|
staticBlocks: statics,
|
|
dynamicBlocks: dynamics,
|
|
pointEvents: this.savePointEvents()
|
|
};
|
|
}
|
|
} else {
|
|
return {
|
|
width: this.width,
|
|
height: this.height,
|
|
staticBlocks: statics,
|
|
dynamicBlocks: dynamics,
|
|
pointEvents: this.savePointEvents()
|
|
};
|
|
}
|
|
}
|
|
|
|
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 静态图块实例存档
|
|
* @param compression 压缩级别
|
|
*/
|
|
private loadStaticTiles(
|
|
save: ReadonlyMap<number, IStaticBlockSave>,
|
|
compression: SaveCompression
|
|
): void {
|
|
for (const [index, tileSave] of save) {
|
|
const { x, y } = this.map.indexer.locator(index);
|
|
const location = this.getLocationData(x, y);
|
|
if (!location?.static) continue;
|
|
location.static.loadState(tileSave, compression);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 读取动态图块实例存档数据
|
|
* @param save 动态图块实例存档
|
|
* @param compression 压缩级别
|
|
*/
|
|
private loadDynamics(
|
|
save: ReadonlyMap<number, IDynamicBlockSave[]>,
|
|
compression: SaveCompression
|
|
): void {
|
|
for (const [index, dynamics] of save) {
|
|
const { x, y } = this.map.indexer.locator(index);
|
|
for (const block of dynamics) {
|
|
const tile = this.createDynamic(block.num, x, y);
|
|
tile.loadState(block, compression);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 清空本图层全部既有动态图块,使读档后的动态块与存档点一致
|
|
* 逐块复用删除语义,会触发 `onDeleteDynamic` 钩子,但读档为同步流程,不等待钩子完成
|
|
*/
|
|
private clearDynamics(): void {
|
|
const tiles = [...this.iterateDynamicTiles()];
|
|
for (const tile of tiles) {
|
|
this.removeDynamic(tile, tile.x, tile.y);
|
|
this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 收集需要保存的点事件并复制其内部 Map
|
|
*/
|
|
private savePointEvents(): Map<number, ReadonlyMap<number, string>> {
|
|
const pointEvents = new Map<number, ReadonlyMap<number, string>>();
|
|
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<number, ReadonlyMap<number, string>>
|
|
): 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 图层存档
|
|
*/
|
|
private loadNoCompression(save: IMapLayerSave): void {
|
|
if (save.fullMap) {
|
|
this.setMapRef(new Uint32Array(save.fullMap));
|
|
}
|
|
if (save.staticBlocks) {
|
|
this.loadStaticTiles(
|
|
save.staticBlocks,
|
|
SaveCompression.NoCompression
|
|
);
|
|
}
|
|
if (save.dynamicBlocks) {
|
|
this.loadDynamics(
|
|
save.dynamicBlocks,
|
|
SaveCompression.NoCompression
|
|
);
|
|
}
|
|
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.loadStaticTiles(
|
|
save.staticBlocks,
|
|
SaveCompression.LowCompression
|
|
);
|
|
}
|
|
if (save.dynamicBlocks) {
|
|
this.loadDynamics(
|
|
save.dynamicBlocks,
|
|
SaveCompression.LowCompression
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 以高压缩方式读取当前图层
|
|
* @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.loadStaticTiles(
|
|
save.staticBlocks,
|
|
SaveCompression.HighCompression
|
|
);
|
|
}
|
|
if (save.dynamicBlocks) {
|
|
this.loadDynamics(
|
|
save.dynamicBlocks,
|
|
SaveCompression.HighCompression
|
|
);
|
|
}
|
|
}
|
|
|
|
loadState(save: IMapLayerSave, compression: SaveCompression): void {
|
|
this.clearDynamics();
|
|
if (compression === SaveCompression.HighCompression) {
|
|
this.loadHighCompression(save);
|
|
} else if (compression === SaveCompression.LowCompression) {
|
|
this.loadLowCompression(save);
|
|
} else {
|
|
this.loadNoCompression(save);
|
|
}
|
|
this.loadPointEvents(save.pointEvents);
|
|
}
|
|
|
|
//#endregion
|
|
}
|
|
|
|
class MapLayerHookController
|
|
extends HookController<IMapLayerHooks>
|
|
implements IMapLayerHookController
|
|
{
|
|
hookable: MapLayer;
|
|
|
|
constructor(
|
|
readonly layer: MapLayer,
|
|
hook: Partial<IMapLayerHooks>
|
|
) {
|
|
super(layer, hook);
|
|
this.hookable = layer;
|
|
}
|
|
|
|
getMapData(): Readonly<IMapLayerData> {
|
|
return this.layer.getMapRef();
|
|
}
|
|
}
|