mirror of
https://github.com/motajs/template.git
synced 2026-10-13 00:00:19 +08:00
refactor: 地图 MapLayer
This commit is contained in:
parent
122567b050
commit
b262918082
2
dev.md
2
dev.md
@ -116,7 +116,7 @@
|
||||
- 换行使用 `CRLF` 格式。
|
||||
- 不得将一个对象上的函数声明为一个临时变量/常量,哪怕其没有 `this` 指向问题。
|
||||
- 通常情况下,一个文件应只包含一个类,但如果一个类仅是另一个类的依赖,或一个类的长度远远小于另一个类等特殊情况时,允许将多个类放在同一个文件中。
|
||||
- 私有方法应该放到调用之的方法之前,同时保证其处在合理的 region 内。
|
||||
- 私有方法应该放到调用之的方法之前,同时保证其处在合理的 region 内。此条不是说私有方法必须放到类的开头,这是完全错误的,必须在保证其处于调用之的函数之前,但同时处在合理的 region 内。region 分区按照方法功能分区,也就是说方法必须在合理的功能分区中。
|
||||
- 对象的非空判断用 `!obj`,字面量的非空判断用 `!isNil(value)`。
|
||||
- 任何情况下都不建议写 `try-catch-finally`,哪怕有时会因为报错导致某些关键状态产生错误,产生错误就应该让用户去修,而不是让系统兜底。
|
||||
|
||||
|
||||
@ -10,6 +10,10 @@ export class LayerEventView implements ILayerEventView {
|
||||
/** 当前存储与参考基准不一致的条目数量 */
|
||||
private dirtyEntries: number = 0;
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.store.entries();
|
||||
}
|
||||
|
||||
get(): ReadonlyMap<number, string> {
|
||||
return this.store;
|
||||
}
|
||||
|
||||
@ -59,8 +59,7 @@ export class GameMap extends Hookable<IGameMapHooks> 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);
|
||||
|
||||
@ -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<IMapLayerHooks, IMapLayerHookController>
|
||||
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<number, ILayerEventView> = 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<IMapLayerHooks>
|
||||
): 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<number, ILayerEventView>();
|
||||
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<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 = 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<void> {
|
||||
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<void> {
|
||||
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<IMapLayerHooks>
|
||||
): 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<number, IStaticBlockSave> {
|
||||
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.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<number, IDynamicBlockSave[]> {
|
||||
private saveDynamicTiles(
|
||||
compression: SaveCompression
|
||||
): Map<number, IDynamicBlockSave[]> {
|
||||
const blocks = new Map<number, IDynamicBlockSave[]>();
|
||||
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<number, IStaticBlockSave>): void {
|
||||
private loadStaticTiles(
|
||||
save: ReadonlyMap<number, IStaticBlockSave>,
|
||||
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<number, IDynamicBlockSave[]>): void {
|
||||
private loadDynamics(
|
||||
save: ReadonlyMap<number, IDynamicBlockSave[]>,
|
||||
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<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 图层存档
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
ILayerEventView,
|
||||
IMapBlockSaveBase,
|
||||
IMapLayer,
|
||||
IReadonlyTileBase,
|
||||
ITileBase
|
||||
} from './types';
|
||||
|
||||
@ -81,6 +82,14 @@ export abstract class MapTileBase<TSave extends IMapBlockSaveBase>
|
||||
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<TSave>;
|
||||
|
||||
abstract loadState(save: Readonly<TSave>): void;
|
||||
|
||||
@ -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<ILayerLocation>;
|
||||
}
|
||||
@ -423,14 +431,6 @@ interface ILayerDynamic {
|
||||
*/
|
||||
iterateDynamicTiles(): Iterable<IDynamicTile>;
|
||||
|
||||
/**
|
||||
* 设置动态图块的朝向
|
||||
* @param tile 要设置朝向的动态图块
|
||||
* @param direction 目标朝向
|
||||
* @returns 更新朝向后该动态图块的图块数字
|
||||
*/
|
||||
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): number;
|
||||
|
||||
/**
|
||||
* 更新动态图块位置,用于在图块发生移动时更新内部存储,一般不需要手动调用
|
||||
* @param tile 动态图块
|
||||
@ -477,6 +477,9 @@ export interface IMapLayer
|
||||
ILayerStatic,
|
||||
ILayerDynamic,
|
||||
ISaveableContent<IMapLayerSave> {
|
||||
/** 地图使用的坐标索引器 */
|
||||
readonly indexer: ILocationHelper;
|
||||
|
||||
/** 地图宽度 */
|
||||
readonly width: number;
|
||||
/** 地图高度 */
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
|
||||
@ -222,7 +222,7 @@ export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
getEnemyLocator(enemy: IEnemy<TEnemy>): Readonly<ITileLocator> | 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<TEnemy, THero> implements IEnemyContext<
|
||||
): Readonly<ITileLocator> | 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<TEnemy> | 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<TEnemy> | 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<TEnemy, THero> implements IEnemyContext<
|
||||
}
|
||||
|
||||
setEnemyAt(locator: ITileLocator, enemy: IEnemy<TEnemy>): 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<TEnemy>(enemy, this);
|
||||
@ -299,7 +299,7 @@ export class EnemyContext<TEnemy, THero> 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<TEnemy, THero> 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<TEnemy, THero> implements IEnemyContext<
|
||||
|
||||
*iterateEnemy(): Iterable<[ITileLocator, IEnemyView<TEnemy>]> {
|
||||
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<TEnemy, THero> 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<TEnemy, THero> 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<TEnemy, THero> 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<TEnemy, THero> 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);
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -102,7 +102,7 @@ export class MapDamage<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
}
|
||||
|
||||
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<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
}
|
||||
|
||||
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<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
}
|
||||
|
||||
markDirty(locator: ITileLocator): void {
|
||||
this.markDirtyIndex(this.indexer.locaterToIndex(locator));
|
||||
this.markDirtyIndex(this.indexer.locatorToIndex(locator));
|
||||
}
|
||||
|
||||
markEnemyDirty(view: IEnemyView<TEnemy>): void {
|
||||
@ -179,7 +179,7 @@ export class MapDamage<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
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<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
getSeparatedDamage(
|
||||
locator: ITileLocator
|
||||
): Iterable<Readonly<IMapDamageInfo>> {
|
||||
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<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
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<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -89,7 +89,7 @@ export class MapGraphBuilder implements IMapGraphBuilder {
|
||||
const terminals: Set<number> = new Set();
|
||||
const adjacency: Map<number, IPathGraphEdge[]> = new Map();
|
||||
const mapped: Set<number> = 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<number, IPathGraphNode> = 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,
|
||||
|
||||
11
perf.md
11
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。
|
||||
- 使用哪个都可以: 函数
|
||||
|
||||
Loading…
Reference in New Issue
Block a user