mirror of
https://github.com/motajs/template.git
synced 2026-11-09 11:44:30 +08:00
feat: 静态触发器存储
This commit is contained in:
1 parent
13fc4e1b7c
commit
bf781c5ee8
9 files changed
+741
-174
No files matched your search
@@ -36,22 +36,42 @@ export class DynamicLayer
|
||||
|
||||
createDynamic(num: number, x: number, y: number): IDynamicTile {
|
||||
const tile = new DynamicTile(num, x, y, this);
|
||||
tile.setTriggerType(this.layer.getTriggerType(x, y));
|
||||
this.addTileToPosMap(tile, x, y);
|
||||
this.posTileMap.set(tile, { x, y });
|
||||
this.forEachHook(hook => hook.onCreateTile?.(tile, this));
|
||||
return tile;
|
||||
}
|
||||
|
||||
transferToDynamic(x: number, y: number): IDynamicTile {
|
||||
transferToDynamic(
|
||||
x: number,
|
||||
y: number,
|
||||
keepTrigger: boolean = true
|
||||
): IDynamicTile {
|
||||
const num = this.layer.getBlock(x, y);
|
||||
const triggerType = keepTrigger ? this.layer.getTriggerType(x, y) : -1;
|
||||
if (num === 0) {
|
||||
logger.warn(127, x.toString(), y.toString());
|
||||
}
|
||||
this.layer.setBlock(0, x, y);
|
||||
return this.createDynamic(num, x, y);
|
||||
this.layer.revertTrigger(x, y);
|
||||
const tile = this.createDynamic(num, x, y);
|
||||
tile.setTriggerType(triggerType);
|
||||
return tile;
|
||||
}
|
||||
|
||||
transferToStatic(tile: IDynamicTile): void {
|
||||
/**
|
||||
* 将动态图块上的触发器同步回当前静态格点
|
||||
*/
|
||||
private syncStaticTrigger(tile: IDynamicTile, keepTrigger: boolean): void {
|
||||
if (keepTrigger) {
|
||||
this.layer.setTriggerType(tile.triggerType, tile.x, tile.y);
|
||||
} else {
|
||||
this.layer.revertTrigger(tile.x, tile.y);
|
||||
}
|
||||
}
|
||||
|
||||
transferToStatic(tile: IDynamicTile, keepTrigger: boolean = true): void {
|
||||
const { x, y } = tile;
|
||||
const { width, height } = this.layer;
|
||||
if (x < 0 || y < 0 || x >= width || y >= height) {
|
||||
@@ -62,16 +82,26 @@ export class DynamicLayer
|
||||
logger.warn(129, x.toString(), y.toString());
|
||||
}
|
||||
this.layer.setBlock(tile.num, x, y);
|
||||
this.syncStaticTrigger(tile, keepTrigger);
|
||||
this.removeTile(tile);
|
||||
this.forEachHook(hook => {
|
||||
void hook.onDeleteTile?.(tile, this);
|
||||
});
|
||||
this.forEachHook(hook => hook.onDeleteTile?.(tile, this));
|
||||
}
|
||||
|
||||
transferToStaticIfSafe(tile: IDynamicTile): boolean {
|
||||
transferToStaticIfSafe(
|
||||
tile: IDynamicTile,
|
||||
keepTrigger: boolean = true
|
||||
): boolean {
|
||||
const { x, y } = tile;
|
||||
const { width, height } = this.layer;
|
||||
if (x < 0 || y < 0 || x >= width || y >= height) {
|
||||
logger.warn(128, x.toString(), y.toString());
|
||||
return false;
|
||||
}
|
||||
if (this.layer.getBlock(tile.x, tile.y) !== 0) return false;
|
||||
this.layer.setBlock(tile.num, tile.x, tile.y);
|
||||
this.deleteDynamic(tile);
|
||||
this.layer.setBlock(tile.num, x, y);
|
||||
this.syncStaticTrigger(tile, keepTrigger);
|
||||
this.removeTile(tile);
|
||||
this.forEachHook(hook => hook.onDeleteTile?.(tile, this));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -117,12 +147,18 @@ export class DynamicLayer
|
||||
this.forEachHook(hook => hook.onUpdateTilePosition?.(tile, this));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将动态图块登记到指定坐标的索引表中
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将动态图块从指定坐标的索引表中移除
|
||||
*/
|
||||
private removeTileFromPosMap(
|
||||
tile: IDynamicTile,
|
||||
x: number,
|
||||
@@ -131,7 +167,9 @@ export class DynamicLayer
|
||||
this.tilePosMap.get(y)?.get(x)?.delete(tile);
|
||||
}
|
||||
|
||||
/** 从两个内部映射中移除图块记录 */
|
||||
/**
|
||||
* 从两个内部映射中移除图块记录
|
||||
*/
|
||||
private removeTile(tile: IDynamicTile): void {
|
||||
const pos = this.posTileMap.get(tile);
|
||||
if (pos) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DynamicTileMover } from './mover';
|
||||
|
||||
export class DynamicTile implements IDynamicTile {
|
||||
readonly mover: IObjectMover<IDynamicTile>;
|
||||
triggerType: number;
|
||||
|
||||
/** 当前的朝向绑定对象 */
|
||||
private face: IRoleFaceBinder | null = null;
|
||||
@@ -21,6 +22,7 @@ export class DynamicTile implements IDynamicTile {
|
||||
public readonly layer: IDynamicLayer
|
||||
) {
|
||||
this.mover = new DynamicTileMover(this);
|
||||
this.triggerType = -1;
|
||||
}
|
||||
|
||||
setFaceBinder(binder: IRoleFaceBinder | null): void {
|
||||
@@ -36,6 +38,10 @@ export class DynamicTile implements IDynamicTile {
|
||||
return this.num;
|
||||
}
|
||||
|
||||
setTriggerType(type: number): void {
|
||||
this.triggerType = type;
|
||||
}
|
||||
|
||||
delete(): Promise<void> {
|
||||
return this.layer.deleteDynamic(this);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
IMapLayerHookController,
|
||||
IMapLayerHooks
|
||||
} from './types';
|
||||
import { ITileStore } from '../store';
|
||||
import { MapLayer } from './mapLayer';
|
||||
|
||||
export class LayerState
|
||||
@@ -38,6 +39,7 @@ export class LayerState
|
||||
private dirty: boolean = false;
|
||||
|
||||
constructor(
|
||||
public readonly tileStore: ITileStore,
|
||||
public width: number,
|
||||
public height: number
|
||||
) {
|
||||
@@ -46,7 +48,12 @@ export class LayerState
|
||||
|
||||
addLayer(): IMapLayer {
|
||||
const array = new Uint32Array(this.width * this.height);
|
||||
const layer = new MapLayer(array, this.width, this.height);
|
||||
const layer = new MapLayer(
|
||||
array,
|
||||
this.width,
|
||||
this.height,
|
||||
this.tileStore
|
||||
);
|
||||
this.layerList.add(layer);
|
||||
this.mapLayerList.add(layer);
|
||||
this.forEachHook(hook => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from './types';
|
||||
import { Hookable, HookController, logger } from '@motajs/common';
|
||||
import { DynamicLayer } from './dynamicLayer';
|
||||
import { ITileStore } from '../store';
|
||||
|
||||
// todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList,以及前景背景的接口
|
||||
|
||||
@@ -24,10 +25,17 @@ export class MapLayer
|
||||
private mapArray: Uint32Array;
|
||||
/** 地图数据引用 */
|
||||
private mapData: IMapLayerData;
|
||||
/** 手动触发器覆盖映射,key = y * width + x */
|
||||
private triggerMap: Map<number, number> = new Map();
|
||||
|
||||
readonly dynamicLayer: IDynamicLayer;
|
||||
|
||||
constructor(array: Uint32Array, width: number, height: number) {
|
||||
constructor(
|
||||
array: Uint32Array,
|
||||
width: number,
|
||||
height: number,
|
||||
private readonly tileStore: ITileStore
|
||||
) {
|
||||
super();
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
@@ -42,6 +50,25 @@ export class MapLayer
|
||||
this.dynamicLayer = new DynamicLayer(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在地图尺寸变化后重新映射手动触发器覆盖表
|
||||
*/
|
||||
private remapTriggerMap(
|
||||
beforeWidth: number,
|
||||
width: number,
|
||||
height: number
|
||||
): Map<number, number> {
|
||||
const next = new Map<number, number>();
|
||||
for (const [index, type] of this.triggerMap) {
|
||||
const x = index % beforeWidth;
|
||||
const y = Math.floor(index / beforeWidth);
|
||||
if (x < width && y < height) {
|
||||
next.set(y * width + x, type);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
resize(width: number, height: number): void {
|
||||
if (this.width === width && this.height === height) {
|
||||
return;
|
||||
@@ -55,6 +82,7 @@ export class MapLayer
|
||||
this.height = height;
|
||||
const area = width * height;
|
||||
const newArray = new Uint32Array(area);
|
||||
this.triggerMap = this.remapTriggerMap(beforeWidth, width, height);
|
||||
this.mapArray = newArray;
|
||||
// 将原来的地图数组赋值给现在的
|
||||
if (beforeArea > area) {
|
||||
@@ -84,13 +112,16 @@ export class MapLayer
|
||||
|
||||
resize2(width: number, height: number): void {
|
||||
if (this.width === width && this.height === height) {
|
||||
this.empty = true;
|
||||
this.mapArray.fill(0);
|
||||
this.triggerMap.clear();
|
||||
return;
|
||||
}
|
||||
this.mapData.expired = true;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mapArray = new Uint32Array(width * height);
|
||||
this.triggerMap.clear();
|
||||
this.mapData = {
|
||||
expired: false,
|
||||
array: this.mapArray
|
||||
@@ -121,6 +152,39 @@ export class MapLayer
|
||||
return this.mapArray[y * this.width + x];
|
||||
}
|
||||
|
||||
getTriggerType(x: number, y: number): number {
|
||||
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
|
||||
return -1;
|
||||
}
|
||||
const index = y * this.width + x;
|
||||
if (this.triggerMap.has(index)) {
|
||||
return this.triggerMap.get(index)!;
|
||||
}
|
||||
return this.tileStore.getTrigger(this.mapArray[index]);
|
||||
}
|
||||
|
||||
setTriggerType(type: number, x: number, y: number): void {
|
||||
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
|
||||
return;
|
||||
}
|
||||
const index = y * this.width + x;
|
||||
if (this.tileStore.getTrigger(this.mapArray[index]) === type) {
|
||||
this.triggerMap.delete(index);
|
||||
} else {
|
||||
this.triggerMap.set(index, type);
|
||||
}
|
||||
}
|
||||
|
||||
revertTrigger(x: number, y: number): void {
|
||||
if (x >= 0 && y >= 0 && x < this.width && y < this.height) {
|
||||
this.triggerMap.delete(y * this.width + x);
|
||||
}
|
||||
}
|
||||
|
||||
clearTrigger(): void {
|
||||
this.triggerMap.clear();
|
||||
}
|
||||
|
||||
putMapData(array: Uint32Array, x: number, y: number, width: number): void {
|
||||
if (array.length % width !== 0) {
|
||||
logger.warn(8);
|
||||
@@ -201,13 +265,6 @@ export class MapLayer
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地图数据的内部存储直接引用
|
||||
*/
|
||||
getMapRef(): IMapLayerData {
|
||||
return this.mapData;
|
||||
}
|
||||
|
||||
setMapRef(array: Uint32Array): void {
|
||||
if (array.length !== this.width * this.height) {
|
||||
logger.warn(
|
||||
@@ -229,6 +286,18 @@ export class MapLayer
|
||||
});
|
||||
}
|
||||
|
||||
getMapRef(): IMapLayerData {
|
||||
return this.mapData;
|
||||
}
|
||||
|
||||
setTriggerRef(triggers: Map<number, number>): void {
|
||||
this.triggerMap = triggers;
|
||||
}
|
||||
|
||||
getTriggerRef(): ReadonlyMap<number, number> {
|
||||
return this.triggerMap;
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IMapLayerHooks>
|
||||
): IMapLayerHookController {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { SaveCompression } from '../common';
|
||||
import { ITileStore } from '../store';
|
||||
import {
|
||||
ILayerState,
|
||||
ILayerStateSave,
|
||||
@@ -31,6 +32,8 @@ export class MapStore implements IMapStore {
|
||||
/** 自动分区激活器开关 */
|
||||
private autoActivitorEnabled: boolean = false;
|
||||
|
||||
constructor(private readonly tileStore: ITileStore) {}
|
||||
|
||||
//#region 楼层管理
|
||||
|
||||
createLayerState(id: string, width: number, height: number): ILayerState {
|
||||
@@ -39,7 +42,7 @@ export class MapStore implements IMapStore {
|
||||
} else {
|
||||
this.maps.push(id);
|
||||
}
|
||||
const state = new LayerState(width, height);
|
||||
const state = new LayerState(this.tileStore, width, height);
|
||||
// 若 refData 已存在,新楼层直接视为全脏
|
||||
if (this.refData !== null) {
|
||||
state.setDirty(true);
|
||||
@@ -117,6 +120,10 @@ export class MapStore implements IMapStore {
|
||||
this.lastFloorId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 maps 下标查找其所属的分区
|
||||
* @param idx 楼层在 maps 中的下标
|
||||
*/
|
||||
private findAreaByIndex(idx: number): MapArea | null {
|
||||
for (const area of this.areaList) {
|
||||
for (const interval of area) {
|
||||
@@ -128,6 +135,11 @@ export class MapStore implements IMapStore {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置一个分区内所有楼层的激活状态
|
||||
* @param area 目标分区
|
||||
* @param active 要设置的激活状态
|
||||
*/
|
||||
private setAreaActive(area: MapArea, active: boolean): void {
|
||||
for (const interval of area) {
|
||||
for (let i = interval.start; i <= interval.end; i++) {
|
||||
@@ -169,7 +181,7 @@ export class MapStore implements IMapStore {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 存档及压缩
|
||||
//#region 存档工具
|
||||
|
||||
compareWith(ref: Map<string, Map<number, Uint32Array>>): void {
|
||||
if (this.refData !== null) return;
|
||||
@@ -202,6 +214,116 @@ export class MapStore implements IMapStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取楼层内所有图层的静态触发器覆盖映射
|
||||
* @param state 目标楼层状态
|
||||
*/
|
||||
private getTriggerMap(state: ILayerState) {
|
||||
const triggers = new Map<number, Map<number, number>>();
|
||||
for (const layer of state.layerList) {
|
||||
const map = layer.getTriggerRef();
|
||||
if (map.size > 0) {
|
||||
triggers.set(layer.zIndex, new Map(map));
|
||||
}
|
||||
}
|
||||
return triggers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造仅包含背景和静态触发器的空楼层存档
|
||||
* @param state 目标楼层状态
|
||||
*/
|
||||
private emptySave(state: ILayerState): ILayerStateSave {
|
||||
return {
|
||||
background: state.getBackground(),
|
||||
layers: new Map(),
|
||||
triggers: this.getTriggerMap(state)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将单个图层序列化为完整地图存档
|
||||
* @param layer 目标图层
|
||||
*/
|
||||
private fullLayer(layer: IMapLayer): IMapLayerSave {
|
||||
return {
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
fullMap: new Uint32Array(layer.getMapRef().array)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将楼层所有图层全量序列化(NoCompression / LowCompression 用)
|
||||
* @param state 目标楼层状态
|
||||
*/
|
||||
private saveLayerStateFull(state: LayerState): ILayerStateSave {
|
||||
const background = state.getBackground();
|
||||
const layers = new Map<number, IMapLayerSave>();
|
||||
const triggers = new Map<number, Map<number, number>>();
|
||||
for (const layer of state.layerList) {
|
||||
const arr = layer.getMapRef().array;
|
||||
layers.set(layer.zIndex, {
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
fullMap: new Uint32Array(arr)
|
||||
});
|
||||
triggers.set(layer.zIndex, new Map(layer.getTriggerRef()));
|
||||
}
|
||||
return { background, layers, triggers };
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅返回与参考基准不同的行(HighCompression 用)
|
||||
* @param layer 目标图层
|
||||
* @param refArray 该图层对应的参考地图数据
|
||||
*/
|
||||
private diffRows(
|
||||
layer: IMapLayer,
|
||||
refArray: Uint32Array
|
||||
): Map<number, Uint32Array> {
|
||||
const rows = new Map<number, Uint32Array>();
|
||||
const arr = layer.getMapRef().array;
|
||||
|
||||
for (let row = 0; row < layer.height; row++) {
|
||||
const start = row * layer.width;
|
||||
const end = start + layer.width;
|
||||
const slice = arr.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断楼层所有图层是否与参考基准完全一致(LowCompression 去误判用)
|
||||
* @param id 楼层 id
|
||||
* @param state 目标楼层状态
|
||||
*/
|
||||
private isStateEqualToRef(id: string, state: LayerState): boolean {
|
||||
const refFloor = this.refData?.get(id);
|
||||
if (!refFloor) return false;
|
||||
for (const layer of state.layerList) {
|
||||
const refArray = refFloor.get(layer.zIndex);
|
||||
if (!refArray) return false;
|
||||
const cur = layer.getMapRef().array;
|
||||
if (cur.length !== refArray.length) return false;
|
||||
if (cur.some((v, i) => v !== refArray[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 存档
|
||||
|
||||
/**
|
||||
* 以无压缩方式序列化所有激活楼层
|
||||
*/
|
||||
private saveNoCompression(): IMapStoreSave {
|
||||
const floors = new Map<string, ILayerStateSave>();
|
||||
for (const [id, state] of this.mapData) {
|
||||
@@ -211,77 +333,119 @@ export class MapStore implements IMapStore {
|
||||
return { floors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 以低压缩方式序列化所有激活楼层
|
||||
*/
|
||||
private saveLowCompression(): IMapStoreSave {
|
||||
const floors = new Map<string, ILayerStateSave>();
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (!state.active) continue;
|
||||
// 非 dirty 或 dirty 但与参考基准完全一致 → 空 layers(读档时从参考基准恢复)
|
||||
if (
|
||||
!state.isDirty() ||
|
||||
(this.refData && this.isStateEqualToRef(id, state))
|
||||
) {
|
||||
floors.set(id, {
|
||||
background: state.getBackground(),
|
||||
layers: new Map()
|
||||
});
|
||||
} else {
|
||||
floors.set(id, this.saveLayerStateFull(state));
|
||||
if (this.refData) {
|
||||
// 包含参考标准时需要对比
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (!state.active) continue;
|
||||
if (state.isDirty() && this.isStateEqualToRef(id, state)) {
|
||||
floors.set(id, this.saveLayerStateFull(state));
|
||||
} else {
|
||||
floors.set(id, this.emptySave(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
return { floors };
|
||||
}
|
||||
|
||||
private saveHighCompression(): IMapStoreSave {
|
||||
const floors = new Map<string, ILayerStateSave>();
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (!state.active) continue;
|
||||
if (!state.isDirty()) {
|
||||
floors.set(id, {
|
||||
background: state.getBackground(),
|
||||
layers: new Map()
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
// 不包含参考标准时仅看 dirty 标记
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (!state.active) continue;
|
||||
if (state.isDirty()) {
|
||||
floors.set(id, this.saveLayerStateFull(state));
|
||||
} else {
|
||||
floors.set(id, this.emptySave(state));
|
||||
}
|
||||
}
|
||||
const refFloor = this.refData?.get(id);
|
||||
const layersMap = new Map<number, IMapLayerSave>();
|
||||
for (const layer of state.layerList) {
|
||||
const refArray = refFloor?.get(layer.zIndex);
|
||||
const rows = this.diffRows(layer, refArray);
|
||||
if (rows.size === 0 && refArray) continue; // 与参考完全一致
|
||||
layersMap.set(layer.zIndex, {
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
rows
|
||||
});
|
||||
}
|
||||
floors.set(id, {
|
||||
background: state.getBackground(),
|
||||
layers: layersMap
|
||||
});
|
||||
}
|
||||
return { floors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 以高压缩方式序列化所有激活楼层
|
||||
*/
|
||||
private saveHighCompression(): IMapStoreSave {
|
||||
const floors = new Map<string, ILayerStateSave>();
|
||||
// 没有参考标准,直接退回低压缩级别
|
||||
if (!this.refData) return this.saveLowCompression();
|
||||
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (!state.active) continue;
|
||||
if (state.isDirty()) {
|
||||
const refFloor = this.refData.get(id);
|
||||
const layersMap = new Map<number, IMapLayerSave>();
|
||||
// 对每一个地图的每一行进行遍历,然后仅存储有差别的行
|
||||
for (const layer of state.layerList) {
|
||||
const refArray = refFloor?.get(layer.zIndex);
|
||||
if (refArray) {
|
||||
const rows = this.diffRows(layer, refArray);
|
||||
if (rows.size === 0) continue;
|
||||
layersMap.set(layer.zIndex, {
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
rows
|
||||
});
|
||||
} else {
|
||||
layersMap.set(layer.zIndex, this.fullLayer(layer));
|
||||
}
|
||||
}
|
||||
floors.set(id, {
|
||||
background: state.getBackground(),
|
||||
layers: layersMap,
|
||||
triggers: this.getTriggerMap(state)
|
||||
});
|
||||
} else {
|
||||
floors.set(id, this.emptySave(state));
|
||||
}
|
||||
}
|
||||
|
||||
return { floors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载指定图层的触发器
|
||||
* @param save 楼层存档数据
|
||||
* @param layer 目标图层
|
||||
*/
|
||||
private loadTriggers(save: ILayerStateSave, layer: IMapLayer) {
|
||||
const triggers = save.triggers.get(layer.zIndex);
|
||||
layer.clearTrigger();
|
||||
if (triggers) {
|
||||
layer.setTriggerRef(new Map(triggers));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NoCompression 读档:每个图层均有 fullMap,直接转移所有权,无需参考基准。
|
||||
* @param state 整体地图存档数据
|
||||
*/
|
||||
private loadNoCompression(state: IMapStoreSave): void {
|
||||
for (const [id, cur] of this.mapData) {
|
||||
cur.setActiveStatus(state.floors.has(id));
|
||||
}
|
||||
for (const [id, layerStateSave] of state.floors) {
|
||||
for (const [id, save] of state.floors) {
|
||||
const cur = this.mapData.get(id);
|
||||
if (!cur) {
|
||||
logger.warn(122, id);
|
||||
continue;
|
||||
}
|
||||
cur.setBackground(layerStateSave.background);
|
||||
cur.setBackground(save.background);
|
||||
for (const layer of cur.layerList) {
|
||||
const layerSave = layerStateSave.layers.get(layer.zIndex);
|
||||
if (!layerSave?.fullMap) continue;
|
||||
layer.setMapRef(new Uint32Array(layerSave.fullMap));
|
||||
// 地图
|
||||
const layerSave = save.layers.get(layer.zIndex);
|
||||
if (layerSave?.fullMap) {
|
||||
layer.setMapRef(new Uint32Array(layerSave.fullMap));
|
||||
}
|
||||
// 触发器
|
||||
this.loadTriggers(save, layer);
|
||||
}
|
||||
// 需要额外进行判断是否与参考地图相同
|
||||
if (this.isStateEqualToRef(id, cur)) {
|
||||
cur.setDirty(false);
|
||||
} else {
|
||||
cur.setDirty(true);
|
||||
}
|
||||
cur.setDirty(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +453,7 @@ export class MapStore implements IMapStore {
|
||||
* LowCompression 读档:
|
||||
* - layers 有数据(dirty 楼层)→ fullMap 直接转移所有权
|
||||
* - layers 为空(非 dirty 楼层)→ 从参考基准恢复
|
||||
* @param state 整体地图存档数据
|
||||
*/
|
||||
private loadLowCompression(state: IMapStoreSave): void {
|
||||
if (!this.refData) {
|
||||
@@ -298,7 +463,7 @@ export class MapStore implements IMapStore {
|
||||
for (const [id, cur] of this.mapData) {
|
||||
cur.setActiveStatus(state.floors.has(id));
|
||||
}
|
||||
for (const [id, layerStateSave] of state.floors) {
|
||||
for (const [id, save] of state.floors) {
|
||||
const cur = this.mapData.get(id);
|
||||
const refFloor = this.refData.get(id);
|
||||
if (!cur) {
|
||||
@@ -309,21 +474,26 @@ export class MapStore implements IMapStore {
|
||||
logger.warn(124, id);
|
||||
continue;
|
||||
}
|
||||
cur.setBackground(layerStateSave.background);
|
||||
cur.setBackground(save.background);
|
||||
let shouldDirty = false;
|
||||
for (const layer of cur.layerList) {
|
||||
const layerSave = layerStateSave.layers.get(layer.zIndex);
|
||||
// 地图
|
||||
const layerSave = save.layers.get(layer.zIndex);
|
||||
if (layerSave?.fullMap) {
|
||||
layer.setMapRef(layerSave.fullMap);
|
||||
layer.setMapRef(new Uint32Array(layerSave.fullMap));
|
||||
shouldDirty = true;
|
||||
} else {
|
||||
const refArray = refFloor?.get(layer.zIndex);
|
||||
const refArray = refFloor.get(layer.zIndex);
|
||||
if (!refArray) {
|
||||
logger.warn(124, id);
|
||||
return;
|
||||
}
|
||||
layer.setMapRef(new Uint32Array(refArray));
|
||||
}
|
||||
// 触发器
|
||||
this.loadTriggers(save, layer);
|
||||
}
|
||||
cur.setDirty(false);
|
||||
cur.setDirty(shouldDirty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +501,7 @@ export class MapStore implements IMapStore {
|
||||
* HighCompression 读档:
|
||||
* - layers 有数据(dirty 楼层)→ 以参考基准为底,叠加差分行
|
||||
* - layers 为空(非 dirty 楼层)或图层无变化(rows 缺失)→ 从参考基准恢复
|
||||
* @param state 整体地图存档数据
|
||||
*/
|
||||
private loadHighCompression(state: IMapStoreSave): void {
|
||||
if (!this.refData) {
|
||||
@@ -340,7 +511,7 @@ export class MapStore implements IMapStore {
|
||||
for (const [id, cur] of this.mapData) {
|
||||
cur.setActiveStatus(state.floors.has(id));
|
||||
}
|
||||
for (const [id, layerStateSave] of state.floors) {
|
||||
for (const [id, save] of state.floors) {
|
||||
const cur = this.mapData.get(id);
|
||||
const refFloor = this.refData.get(id);
|
||||
if (!cur) {
|
||||
@@ -351,34 +522,32 @@ export class MapStore implements IMapStore {
|
||||
logger.warn(124, id);
|
||||
continue;
|
||||
}
|
||||
cur.setBackground(layerStateSave.background);
|
||||
let isMapDirty = true;
|
||||
cur.setBackground(save.background);
|
||||
let shouldDirty = false;
|
||||
for (const layer of cur.layerList) {
|
||||
const refArray = refFloor.get(layer.zIndex);
|
||||
if (!refArray) {
|
||||
logger.warn(124, id);
|
||||
continue;
|
||||
}
|
||||
const layerSave = layerStateSave.layers.get(layer.zIndex);
|
||||
// 地图
|
||||
const layerSave = save.layers.get(layer.zIndex);
|
||||
if (!layerSave?.rows || layerSave.rows.size === 0) {
|
||||
// 图层无变化或非 dirty 楼层,从参考基准恢复
|
||||
layer.setMapRef(new Uint32Array(refArray));
|
||||
} else {
|
||||
// 以参考基准为底,叠加差分行
|
||||
isMapDirty = false;
|
||||
const size = layer.width * layer.height;
|
||||
const buf = new Uint32Array(size);
|
||||
if (refArray) buf.set(refArray.subarray(0, size));
|
||||
shouldDirty = true;
|
||||
const buf = new Uint32Array(refArray);
|
||||
for (const [rowIdx, rowData] of layerSave.rows) {
|
||||
buf.set(
|
||||
rowData.subarray(0, layer.width),
|
||||
rowIdx * layer.width
|
||||
);
|
||||
buf.set(rowData, rowIdx * layer.width);
|
||||
}
|
||||
layer.setMapRef(buf);
|
||||
}
|
||||
// 触发器
|
||||
this.loadTriggers(save, layer);
|
||||
}
|
||||
cur.setDirty(isMapDirty);
|
||||
cur.setDirty(shouldDirty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,71 +571,5 @@ export class MapStore implements IMapStore {
|
||||
}
|
||||
}
|
||||
|
||||
//#region 内部方法
|
||||
|
||||
/**
|
||||
* 将楼层所有图层全量序列化(NoCompression / LowCompression 用)
|
||||
*/
|
||||
private saveLayerStateFull(state: LayerState): ILayerStateSave {
|
||||
const layersMap = new Map<number, IMapLayerSave>();
|
||||
for (const layer of state.layerList) {
|
||||
const arr = layer.getMapRef().array;
|
||||
layersMap.set(layer.zIndex, {
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
fullMap: new Uint32Array(arr)
|
||||
});
|
||||
}
|
||||
return { background: state.getBackground(), layers: layersMap };
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅返回与参考基准不同的行(HighCompression 用)
|
||||
*/
|
||||
private diffRows(
|
||||
layer: IMapLayer,
|
||||
refArray?: Uint32Array
|
||||
): Map<number, Uint32Array> {
|
||||
const rows = new Map<number, Uint32Array>();
|
||||
const arr = layer.getMapRef().array;
|
||||
if (refArray) {
|
||||
for (let row = 0; row < layer.height; row++) {
|
||||
const start = row * layer.width;
|
||||
const end = start + layer.width;
|
||||
const slice = arr.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));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let row = 0; row < layer.height; row++) {
|
||||
const start = row * layer.width;
|
||||
const end = start + layer.width;
|
||||
rows.set(row, new Uint32Array(arr.subarray(start, end)));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断楼层所有图层是否与参考基准完全一致(LowCompression 去误判用)
|
||||
*/
|
||||
private isStateEqualToRef(id: string, state: LayerState): boolean {
|
||||
const refFloor = this.refData?.get(id);
|
||||
if (!refFloor) return false;
|
||||
for (const layer of state.layerList) {
|
||||
const refArray = refFloor.get(layer.zIndex);
|
||||
if (!refArray) return false;
|
||||
const cur = layer.getMapRef().array;
|
||||
if (cur.length !== refArray.length) return false;
|
||||
for (let i = 0; i < cur.length; i++) {
|
||||
if (cur[i] !== refArray[i]) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IRoleFaceBinder,
|
||||
ISaveableContent
|
||||
} from '../common';
|
||||
import { ITileStore } from '../store';
|
||||
|
||||
//#region 静态图层
|
||||
|
||||
@@ -103,6 +104,33 @@ export interface IMapLayer extends IHookable<
|
||||
*/
|
||||
getBlock(x: number, y: number): number;
|
||||
|
||||
/**
|
||||
* 获取指定点的静态图块对应的有效触发器类型,若手动覆盖不存在则回退到图块默认触发器
|
||||
* @param x 图块横坐标
|
||||
* @param y 图块纵坐标
|
||||
*/
|
||||
getTriggerType(x: number, y: number): number;
|
||||
|
||||
/**
|
||||
* 设置指定点静态图块的触发器
|
||||
* @param type 触发器类型
|
||||
* @param x 图块横坐标
|
||||
* @param y 图块纵坐标
|
||||
*/
|
||||
setTriggerType(type: number, x: number, y: number): void;
|
||||
|
||||
/**
|
||||
* 删除指定点静态图块的触发器,回退为图块默认触发器
|
||||
* @param x 图块横坐标
|
||||
* @param y 图块纵坐标
|
||||
*/
|
||||
revertTrigger(x: number, y: number): void;
|
||||
|
||||
/**
|
||||
* 清空地图上所有静态图块的手动设置的触发器,恢复为图块默认触发器
|
||||
*/
|
||||
clearTrigger(): void;
|
||||
|
||||
/**
|
||||
* 设置地图图块
|
||||
* @param array 地图图块数组
|
||||
@@ -130,11 +158,31 @@ export interface IMapLayer extends IHookable<
|
||||
height: number
|
||||
): Uint32Array;
|
||||
|
||||
/**
|
||||
* 直接替换内部图块数组引用,跳过拷贝,高性能但风险较高。
|
||||
* 一般仅供 `MapStore` 读档时内部使用,外部正常情况下不应调用。
|
||||
* 调用方需确保传入数组的长度与 `width * height` 匹配,
|
||||
* 且调用后不得再持有或修改传入的数组。
|
||||
* @param array 地图数组,会直接替换内部引用
|
||||
*/
|
||||
setMapRef(array: Uint32Array): void;
|
||||
|
||||
/**
|
||||
* 获取整个地图的地图数组,是对内部数组的直接引用
|
||||
*/
|
||||
getMapRef(): IMapLayerData;
|
||||
|
||||
/**
|
||||
* 直接设置内部触发器映射对象,一般仅供内部存读档使用,外部正常情况下不应调用
|
||||
* @param triggers 触发器映射
|
||||
*/
|
||||
setTriggerRef(triggers: Map<number, number>): void;
|
||||
|
||||
/**
|
||||
* 获取静态触发器覆盖映射,一般仅供内部存档逻辑使用
|
||||
*/
|
||||
getTriggerRef(): ReadonlyMap<number, number>;
|
||||
|
||||
/**
|
||||
* 设置地图纵深,会影响渲染的遮挡顺序
|
||||
* @param zIndex 纵深
|
||||
@@ -155,15 +203,6 @@ export interface IMapLayer extends IHookable<
|
||||
* @param y 门纵坐标
|
||||
*/
|
||||
closeDoor(num: number, x: number, y: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* 直接替换内部图块数组引用,跳过拷贝,高性能但风险较高。
|
||||
* 一般仅供 `MapStore` 读档时内部使用,外部正常情况下不应调用。
|
||||
* 调用方需确保传入数组的长度与 `width * height` 匹配,
|
||||
* 且调用后不得再持有或修改传入的数组。
|
||||
* @param array 地图数组,会直接替换内部引用
|
||||
*/
|
||||
setMapRef(array: Uint32Array): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
@@ -225,6 +264,8 @@ export interface ILayerStateHooks extends IHookBase {
|
||||
export interface ILayerState extends IHookable<ILayerStateHooks> {
|
||||
/** 地图列表 */
|
||||
readonly layerList: Set<IMapLayer>;
|
||||
/** 当前楼层共享的图块定义 store */
|
||||
readonly tileStore: ITileStore;
|
||||
/** 此楼层是否处于激活状态 */
|
||||
readonly active: boolean;
|
||||
/** 此楼层的地图宽度 */
|
||||
@@ -316,7 +357,6 @@ export interface ILayerState extends IHookable<ILayerStateHooks> {
|
||||
|
||||
//#region 楼层管理
|
||||
|
||||
/** 单个 MapLayer 的存档数据 */
|
||||
export interface IMapLayerSave {
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
@@ -332,27 +372,27 @@ export interface IMapLayerSave {
|
||||
readonly fullMap?: Uint32Array;
|
||||
}
|
||||
|
||||
/** 单个楼层的存档数据 */
|
||||
export interface ILayerStateSave {
|
||||
/** 楼层背景 */
|
||||
readonly background: number;
|
||||
|
||||
/** key = zIndex,value = 对应图层存档数据 */
|
||||
readonly layers: ReadonlyMap<number, IMapLayerSave>;
|
||||
/** 静态触发器覆盖映射,仅在存在覆盖时写入 */
|
||||
readonly triggers: ReadonlyMap<number, ReadonlyMap<number, number>>;
|
||||
}
|
||||
|
||||
/** 整个 MapStore 的存档数据 */
|
||||
export interface IMapStoreSave {
|
||||
/** key = 楼层 id,只包含 active 的楼层,inactive 的楼层不写入,读档时无需处理 */
|
||||
readonly floors: ReadonlyMap<string, ILayerStateSave>;
|
||||
}
|
||||
|
||||
/** 单段闭区间 [start, end],start 和 end 均为 maps 下标 */
|
||||
export interface IMapAreaInterval {
|
||||
/** 区域起始索引,包含 */
|
||||
readonly start: number;
|
||||
/** 区域结束索引,包含 */
|
||||
readonly end: number;
|
||||
}
|
||||
|
||||
/** 一个区域由一个或多个独立区间组成 */
|
||||
export type MapArea = IMapAreaInterval[];
|
||||
|
||||
export interface IMapStore extends ISaveableContent<IMapStoreSave> {
|
||||
@@ -504,21 +544,25 @@ export interface IDynamicLayer extends IHookable<IDynamicLayerHooks> {
|
||||
* @param y 纵坐标
|
||||
* @returns 创建的动态图块引用
|
||||
*/
|
||||
transferToDynamic(x: number, y: number): IDynamicTile;
|
||||
transferToDynamic(
|
||||
x: number,
|
||||
y: number,
|
||||
keepTrigger?: boolean
|
||||
): IDynamicTile;
|
||||
|
||||
/**
|
||||
* 将动态图块还原为静态图块。坐标越界则警告并放弃,
|
||||
* 否则写回静态图层并触发 {@link IDynamicLayerHooks.onDeleteTile}
|
||||
* @param tile 要还原的动态图块
|
||||
*/
|
||||
transferToStatic(tile: IDynamicTile): void;
|
||||
transferToStatic(tile: IDynamicTile, keepTrigger?: boolean): void;
|
||||
|
||||
/**
|
||||
* 仅当目标位置静态图块为 0(空白)时才还原为静态图块,否则不转换
|
||||
* @param tile 要还原的动态图块
|
||||
* @returns 是否转换成功
|
||||
*/
|
||||
transferToStaticIfSafe(tile: IDynamicTile): boolean;
|
||||
transferToStaticIfSafe(tile: IDynamicTile, keepTrigger?: boolean): boolean;
|
||||
|
||||
/**
|
||||
* 删除指定动态图块,触发 {@link IDynamicLayerHooks.onDeleteTile} 钩子。
|
||||
@@ -557,6 +601,8 @@ export interface IDynamicLayer extends IHookable<IDynamicLayerHooks> {
|
||||
export interface IDynamicTile extends IObjectMovable {
|
||||
/** 当前图块数字 */
|
||||
readonly num: number;
|
||||
/** 当前动态图块携带的触发器类型,-1 表示无触发器 */
|
||||
readonly triggerType: number;
|
||||
/** 当前图块所属的动态图层 */
|
||||
readonly layer: IDynamicLayer;
|
||||
/** 当前动态图块的移动器 */
|
||||
@@ -568,6 +614,12 @@ export interface IDynamicTile extends IObjectMovable {
|
||||
*/
|
||||
setFaceDirection(direction: FaceDirection): number;
|
||||
|
||||
/**
|
||||
* 设置当前动态图块的触发器类型
|
||||
* @param type 触发器类型
|
||||
*/
|
||||
setTriggerType(type: number): void;
|
||||
|
||||
/**
|
||||
* 直接删除此图块
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user