mirror of
https://github.com/motajs/template.git
synced 2026-11-03 07:24:30 +08:00
refactor(event): centralize point event references
This commit is contained in:
1 parent
a297311ea6
commit
64efcf2555
14 files changed
+617
-167
No files matched your search
@@ -45,20 +45,6 @@ export class DynamicTile
|
||||
this.restoreDefaultEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前图块原始数据恢复默认事件并建立干净基准
|
||||
*/
|
||||
private restoreDefaultEvents(): void {
|
||||
this.tileEvent().clear();
|
||||
const data = this.raw();
|
||||
if (data) {
|
||||
for (const [priority, id] of Object.entries(data.events)) {
|
||||
this.tileEvent().set(Number(priority), id);
|
||||
}
|
||||
}
|
||||
this.tileEvent().markPure();
|
||||
}
|
||||
|
||||
num(): number {
|
||||
return this.tileNum;
|
||||
}
|
||||
@@ -115,10 +101,11 @@ export class DynamicTile
|
||||
|
||||
saveState(): Readonly<IDynamicBlockSave> {
|
||||
let save: IDynamicBlockSave;
|
||||
if (this.tileEvent().dirty()) {
|
||||
const eventView = this.tileEvent();
|
||||
if (eventView.dirty()) {
|
||||
save = {
|
||||
num: this.num(),
|
||||
events: new Map(this.tileEvent().get())
|
||||
events: new Map(eventView.get())
|
||||
};
|
||||
} else {
|
||||
save = {
|
||||
@@ -131,9 +118,10 @@ export class DynamicTile
|
||||
loadState(save: Readonly<IDynamicBlockSave>): void {
|
||||
this.restoreDefaultEvents();
|
||||
if (save.events) {
|
||||
this.tileEvent().clear();
|
||||
const eventView = this.tileEvent();
|
||||
eventView.clear();
|
||||
for (const [priority, id] of save.events) {
|
||||
this.tileEvent().set(priority, id);
|
||||
eventView.set(priority, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ describe('MapState raw event path', () => {
|
||||
expect(layer?.getPointEvent(1, 0)).toEqual(
|
||||
new Map([[5, 'point-event']])
|
||||
);
|
||||
expect(layer?.event(1, 0)?.ref()).toEqual(
|
||||
new Map([[5, 'point-event']])
|
||||
);
|
||||
expect(layer?.getLocationData(1, 0)?.static.tileEvent().get()).toEqual(
|
||||
new Map()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { LayerEventView } from './eventView';
|
||||
|
||||
vi.hoisted(() => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
vi.stubGlobal('location', { origin: 'http://localhost' });
|
||||
});
|
||||
|
||||
describe('LayerEventView reference tracking', () => {
|
||||
it('keeps dirty state in sync with set, delete, and clear', () => {
|
||||
const view = new LayerEventView();
|
||||
|
||||
expect(view.ref()).toEqual(new Map());
|
||||
expect(view.dirty()).toBe(false);
|
||||
|
||||
view.set(10, 'first');
|
||||
expect(view.dirty()).toBe(true);
|
||||
view.delete(10);
|
||||
expect(view.dirty()).toBe(false);
|
||||
|
||||
view.set(10, 'first');
|
||||
view.markPure();
|
||||
view.set(20, 'second');
|
||||
expect(view.dirty()).toBe(true);
|
||||
view.clear();
|
||||
expect(view.dirty()).toBe(true);
|
||||
view.set(10, 'first');
|
||||
expect(view.dirty()).toBe(false);
|
||||
});
|
||||
|
||||
it('exposes a stable reference snapshot and restores it without markPure', () => {
|
||||
const view = new LayerEventView();
|
||||
view.set(10, 'first');
|
||||
view.markPure();
|
||||
const reference = view.ref();
|
||||
|
||||
view.set(10, 'changed');
|
||||
view.clear();
|
||||
for (const [priority, id] of reference) {
|
||||
view.set(priority, id);
|
||||
}
|
||||
|
||||
expect(view.ref()).toBe(reference);
|
||||
expect(view.ref()).toEqual(new Map([[10, 'first']]));
|
||||
expect(view.get()).toEqual(new Map([[10, 'first']]));
|
||||
expect(view.dirty()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -5,44 +5,70 @@ export class LayerEventView implements ILayerEventView {
|
||||
/** 当前绑定的事件 */
|
||||
private readonly store: Map<number, string> = new Map();
|
||||
/** 用于判断事件是否变化的参考基准 */
|
||||
private reference: Map<number, string> | null = null;
|
||||
private reference: Map<number, string> = new Map();
|
||||
/** 当前存储与参考基准不一致的条目数量 */
|
||||
private dirtyEntries: number = 0;
|
||||
|
||||
get(): ReadonlyMap<number, string> {
|
||||
return this.store;
|
||||
}
|
||||
|
||||
ref(): ReadonlyMap<number, string> {
|
||||
return this.reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定优先级当前是否偏离参考基准
|
||||
* @param priority 事件优先级
|
||||
*/
|
||||
private isEntryDirty(priority: number): boolean {
|
||||
const storeHas = this.store.has(priority);
|
||||
const referenceHas = this.reference.has(priority);
|
||||
return (
|
||||
storeHas !== referenceHas ||
|
||||
(storeHas &&
|
||||
this.store.get(priority) !== this.reference.get(priority))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定条目的变更更新脏条目数量
|
||||
* @param priority 事件优先级
|
||||
* @param before 条目变更前是否脏
|
||||
*/
|
||||
private updateDirtyEntry(priority: number, before: boolean): void {
|
||||
const after = this.isEntryDirty(priority);
|
||||
if (before !== after) {
|
||||
this.dirtyEntries += after ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
set(priority: number, event: string): void {
|
||||
if (this.store.has(priority)) {
|
||||
logger.warn(136, priority.toString());
|
||||
}
|
||||
const before = this.isEntryDirty(priority);
|
||||
this.store.set(priority, event);
|
||||
this.updateDirtyEntry(priority, before);
|
||||
}
|
||||
|
||||
delete(priority: number): void {
|
||||
const before = this.isEntryDirty(priority);
|
||||
this.store.delete(priority);
|
||||
this.updateDirtyEntry(priority, before);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.store.clear();
|
||||
this.dirtyEntries = this.reference.size;
|
||||
}
|
||||
|
||||
markPure(): void {
|
||||
this.reference = new Map(this.store);
|
||||
this.dirtyEntries = 0;
|
||||
}
|
||||
|
||||
dirty(): boolean {
|
||||
const reference = this.reference;
|
||||
if (!reference) {
|
||||
return this.store.size > 0;
|
||||
}
|
||||
if (this.store.size !== reference.size) {
|
||||
return true;
|
||||
}
|
||||
for (const [priority, event] of this.store) {
|
||||
if (reference.get(priority) !== event) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return this.dirtyEntries > 0;
|
||||
}
|
||||
}
|
||||
@@ -47,14 +47,6 @@ export class MapLayer
|
||||
/** 坐标到动态图块集合的映射,外层 key = y,内层 key = x */
|
||||
private readonly tilePosMap: Map<number, Map<number, Set<IDynamicTile>>> =
|
||||
new Map();
|
||||
/** 点事件视图,外层 key = y,内层 key = x */
|
||||
private readonly pointEvents: Map<number, Map<number, ILayerEventView>> =
|
||||
new Map();
|
||||
/** 点事件的原始纯基准,外层 key = y,内层 key = x */
|
||||
private readonly pointEventBaselines: Map<
|
||||
number,
|
||||
Map<number, ReadonlyMap<number, string>>
|
||||
> = new Map();
|
||||
/** 动态图块到其当前坐标的映射 */
|
||||
private readonly posTileMap: Map<IDynamicTile, ITileLocator> = new Map();
|
||||
/** 图层脏标记 */
|
||||
@@ -62,6 +54,11 @@ export class MapLayer
|
||||
/** 图层参考基准,用于存档压缩对比 */
|
||||
private refArray: Uint32Array | null = null;
|
||||
|
||||
//#region 点事件操作
|
||||
|
||||
/** 点事件视图,key = y * width + x */
|
||||
private readonly pointEvents: Map<number, ILayerEventView> = new Map();
|
||||
|
||||
constructor(
|
||||
array: Uint32Array,
|
||||
width: number,
|
||||
@@ -138,80 +135,45 @@ export class MapLayer
|
||||
if (!staticTile) return;
|
||||
staticTile.set(staticTile.num());
|
||||
if (keepEvent) {
|
||||
staticTile.tileEvent().clear();
|
||||
for (const [priority, id] of tile.tileEvent().get()) {
|
||||
staticTile.tileEvent().set(priority, id);
|
||||
const staticEvent = staticTile.tileEvent();
|
||||
const dynamicEvent = tile.tileEvent();
|
||||
staticEvent.clear();
|
||||
for (const [priority, id] of dynamicEvent.get()) {
|
||||
staticEvent.set(priority, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录尚未保存过的点事件纯基准
|
||||
* @param x 点事件横坐标
|
||||
* @param y 点事件纵坐标
|
||||
* @param eventView 点事件视图
|
||||
*/
|
||||
private capturePointEventBaseline(
|
||||
x: number,
|
||||
y: number,
|
||||
eventView: ILayerEventView
|
||||
): void {
|
||||
const yMap = this.pointEventBaselines.get(y);
|
||||
if (yMap?.has(x)) return;
|
||||
if (eventView.dirty()) return;
|
||||
const target = yMap ?? new Map<number, ReadonlyMap<number, string>>();
|
||||
target.set(x, new Map(eventView.get()));
|
||||
if (!yMap) this.pointEventBaselines.set(y, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将所有已创建的点事件视图恢复到原始纯基准
|
||||
*/
|
||||
private resetPointEvents(): void {
|
||||
for (const [y, xMap] of this.pointEvents) {
|
||||
for (const [x, eventView] of xMap) {
|
||||
this.capturePointEventBaseline(x, y, eventView);
|
||||
}
|
||||
}
|
||||
for (const [y, xMap] of this.pointEvents) {
|
||||
for (const [x, eventView] of xMap) {
|
||||
eventView.clear();
|
||||
const baseline = this.pointEventBaselines.get(y)?.get(x);
|
||||
if (baseline) {
|
||||
for (const [priority, id] of baseline) {
|
||||
eventView.set(priority, id);
|
||||
}
|
||||
}
|
||||
eventView.markPure();
|
||||
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 {
|
||||
for (const [y, xMap] of this.pointEvents) {
|
||||
if (y >= height) {
|
||||
this.pointEvents.delete(y);
|
||||
continue;
|
||||
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);
|
||||
}
|
||||
for (const x of xMap.keys()) {
|
||||
if (x >= width) xMap.delete(x);
|
||||
}
|
||||
if (xMap.size === 0) this.pointEvents.delete(y);
|
||||
}
|
||||
for (const [y, xMap] of this.pointEventBaselines) {
|
||||
if (y >= height) {
|
||||
this.pointEventBaselines.delete(y);
|
||||
continue;
|
||||
}
|
||||
for (const x of xMap.keys()) {
|
||||
if (x >= width) xMap.delete(x);
|
||||
}
|
||||
if (xMap.size === 0) this.pointEventBaselines.delete(y);
|
||||
this.pointEvents.clear();
|
||||
for (const [index, eventView] of pointEvents) {
|
||||
this.pointEvents.set(index, eventView);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,12 +182,9 @@ export class MapLayer
|
||||
*/
|
||||
private savePointEvents(): Map<number, ReadonlyMap<number, string>> {
|
||||
const pointEvents = new Map<number, ReadonlyMap<number, string>>();
|
||||
for (const [y, xMap] of this.pointEvents) {
|
||||
for (const [x, eventView] of xMap) {
|
||||
if (!eventView.dirty()) continue;
|
||||
const index = this.map.indexer.locToIndex(x, y);
|
||||
pointEvents.set(index, new Map(eventView.get()));
|
||||
}
|
||||
for (const [index, eventView] of this.pointEvents) {
|
||||
if (!eventView.dirty()) continue;
|
||||
pointEvents.set(index, new Map(eventView.get()));
|
||||
}
|
||||
return pointEvents;
|
||||
}
|
||||
@@ -240,7 +199,8 @@ export class MapLayer
|
||||
this.resetPointEvents();
|
||||
if (!save) return;
|
||||
for (const [index, events] of save) {
|
||||
const { x, y } = this.map.indexer.indexToLocator(index);
|
||||
const x = index % this.width;
|
||||
const y = Math.floor(index / this.width);
|
||||
const eventView = this.event(x, y);
|
||||
if (!eventView) continue;
|
||||
eventView.clear();
|
||||
@@ -250,6 +210,31 @@ export class MapLayer
|
||||
}
|
||||
}
|
||||
|
||||
event(x: number, y: number): ILayerEventView | null {
|
||||
if (!this.inMap(x, y)) return null;
|
||||
const index = y * this.width + x;
|
||||
const getOrInsertComputed = this.pointEvents.getOrInsertComputed;
|
||||
if (getOrInsertComputed) {
|
||||
return getOrInsertComputed.call(
|
||||
this.pointEvents,
|
||||
index,
|
||||
() => new LayerEventView()
|
||||
);
|
||||
}
|
||||
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 {
|
||||
@@ -432,9 +417,11 @@ export class MapLayer
|
||||
const tile = new DynamicTile(num, x, y, this);
|
||||
const location = this.getLocationData(x, y);
|
||||
if (location) {
|
||||
tile.tileEvent().clear();
|
||||
for (const [priority, id] of location.static.tileEvent().get()) {
|
||||
tile.tileEvent().set(priority, id);
|
||||
const tileEvent = tile.tileEvent();
|
||||
const staticEvent = location.static.tileEvent();
|
||||
tileEvent.clear();
|
||||
for (const [priority, id] of staticEvent.get()) {
|
||||
tileEvent.set(priority, id);
|
||||
}
|
||||
}
|
||||
this.addTileToPosMap(tile, x, y);
|
||||
@@ -580,27 +567,6 @@ export class MapLayer
|
||||
this.setBlock(num, x, y);
|
||||
}
|
||||
|
||||
event(x: number, y: number): ILayerEventView | null {
|
||||
if (!this.inMap(x, y)) return null;
|
||||
let xMap = this.pointEvents.get(y);
|
||||
if (!xMap) {
|
||||
xMap = new Map();
|
||||
this.pointEvents.set(y, xMap);
|
||||
}
|
||||
let eventView = xMap.get(x);
|
||||
if (!eventView) {
|
||||
eventView = new LayerEventView();
|
||||
xMap.set(x, eventView);
|
||||
} else {
|
||||
this.capturePointEventBaseline(x, y, eventView);
|
||||
}
|
||||
return eventView;
|
||||
}
|
||||
|
||||
getPointEvent(x: number, y: number): ReadonlyMap<number, string> | null {
|
||||
return this.event(x, y)?.get() ?? null;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 图层操作
|
||||
@@ -616,10 +582,8 @@ export class MapLayer
|
||||
|
||||
dirty(): boolean {
|
||||
if (this.layerDirty) return true;
|
||||
for (const xMap of this.pointEvents.values()) {
|
||||
for (const eventView of xMap.values()) {
|
||||
if (eventView.dirty()) return true;
|
||||
}
|
||||
for (const eventView of this.pointEvents.values()) {
|
||||
if (eventView.dirty()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -693,7 +657,6 @@ export class MapLayer
|
||||
resize2(width: number, height: number): void {
|
||||
this.layerDirty = true;
|
||||
this.pointEvents.clear();
|
||||
this.pointEventBaselines.clear();
|
||||
if (this.width === width && this.height === height) {
|
||||
this.empty = true;
|
||||
this.mapArray.fill(0);
|
||||
|
||||
@@ -267,4 +267,33 @@ describe('MapLayer point event lifecycle', () => {
|
||||
layer.resize2(1, 1);
|
||||
expect(layer.getPointEvent(0, 0)).toEqual(new Map());
|
||||
});
|
||||
|
||||
it('reindexes preserved point events when the layer width changes', () => {
|
||||
const { layer } = createMapState({
|
||||
1: { 1: 'top-right' },
|
||||
3: { 1: 'bottom-right' }
|
||||
});
|
||||
|
||||
layer.resize(3, 2);
|
||||
|
||||
expect(layer.getPointEvent(1, 0)).toEqual(new Map([[1, 'top-right']]));
|
||||
expect(layer.getPointEvent(1, 1)).toEqual(
|
||||
new Map([[1, 'bottom-right']])
|
||||
);
|
||||
|
||||
layer.event(1, 1)!.set(2, 'runtime-event');
|
||||
expect(
|
||||
layer.saveState(SaveCompression.NoCompression).pointEvents
|
||||
).toEqual(
|
||||
new Map([
|
||||
[
|
||||
4,
|
||||
new Map([
|
||||
[1, 'bottom-right'],
|
||||
[2, 'runtime-event']
|
||||
])
|
||||
]
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -16,20 +16,6 @@ export class StaticTile
|
||||
this.restoreDefaultEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前图块原始数据恢复默认事件并建立干净基准
|
||||
*/
|
||||
private restoreDefaultEvents(): void {
|
||||
this.tileEvent().clear();
|
||||
const data = this.raw();
|
||||
if (data) {
|
||||
for (const [priority, id] of Object.entries(data.events)) {
|
||||
this.tileEvent().set(Number(priority), id);
|
||||
}
|
||||
}
|
||||
this.tileEvent().markPure();
|
||||
}
|
||||
|
||||
num(): number {
|
||||
return this.layer.getBlock(this.locator.x, this.locator.y);
|
||||
}
|
||||
@@ -48,14 +34,16 @@ export class StaticTile
|
||||
}
|
||||
|
||||
shouldSave(): boolean {
|
||||
return this.tileEvent().dirty();
|
||||
const eventView = this.tileEvent();
|
||||
return eventView.dirty();
|
||||
}
|
||||
|
||||
saveState(): Readonly<IStaticBlockSave> {
|
||||
let save: IStaticBlockSave;
|
||||
if (this.tileEvent().dirty()) {
|
||||
const eventView = this.tileEvent();
|
||||
if (eventView.dirty()) {
|
||||
save = {
|
||||
events: new Map(this.tileEvent().get())
|
||||
events: new Map(eventView.get())
|
||||
};
|
||||
} else {
|
||||
save = {};
|
||||
@@ -66,9 +54,10 @@ export class StaticTile
|
||||
loadState(save: Readonly<IStaticBlockSave>): void {
|
||||
this.restoreDefaultEvents();
|
||||
if (save.events) {
|
||||
this.tileEvent().clear();
|
||||
const eventView = this.tileEvent();
|
||||
eventView.clear();
|
||||
for (const [priority, id] of save.events) {
|
||||
this.tileEvent().set(priority, id);
|
||||
eventView.set(priority, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,21 @@ export abstract class MapTileBase<TSave extends IMapBlockSaveBase>
|
||||
|
||||
abstract set(num: number): void;
|
||||
|
||||
/**
|
||||
* 根据当前图块原始数据恢复默认事件并建立干净基准
|
||||
*/
|
||||
protected restoreDefaultEvents(): void {
|
||||
const eventView = this.tileEvent();
|
||||
eventView.clear();
|
||||
const data = this.raw();
|
||||
if (data) {
|
||||
for (const [priority, id] of Object.entries(data.events)) {
|
||||
eventView.set(Number(priority), id);
|
||||
}
|
||||
}
|
||||
eventView.markPure();
|
||||
}
|
||||
|
||||
setFaceDirection(direction: FaceDirection): number {
|
||||
const cur = this.num();
|
||||
const next = this.layer.faceBinder.getFaceOf(cur, direction);
|
||||
|
||||
Reference in new issue
Block a user