mirror of
https://github.com/motajs/template.git
synced 2026-11-09 19:54:30 +08:00
- sandbox: drop obsolete playing=true bypass, align expired-step case, assert codes 73/194 - tileStore: assert codes 133/134 and add ItemStore/EnemyStore code 181 cases - map/mover: assert code 192 when Dir8 handler missing - hero/items: assert code 193 on missing item id registration - mapLifecycle: drive resize via GameMap.resizeLayer so shared indexer width syncs
322 lines
11 KiB
TypeScript
322 lines
11 KiB
TypeScript
// 测试图块默认事件、点事件存读档、压缩聚合和尺寸变化
|
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
import { type IResizableMapLayer } from '../types';
|
|
import {
|
|
type IDataCommon,
|
|
type ITileStore,
|
|
SaveCompression
|
|
} from '@user/data-common';
|
|
|
|
vi.hoisted(() => {
|
|
vi.stubGlobal('main', { replayChecking: true });
|
|
vi.stubGlobal('location', { origin: 'http://localhost' });
|
|
Map.prototype.getOrInsertComputed ??= function <K, V>(
|
|
this: Map<K, V>,
|
|
key: K,
|
|
callback: (key: K) => V
|
|
): V {
|
|
const existing = this.get(key);
|
|
if (existing !== undefined) return existing;
|
|
const value = callback(key);
|
|
this.set(key, value);
|
|
return value;
|
|
};
|
|
});
|
|
|
|
interface TestModules {
|
|
MapState: typeof import('../mapState').MapState;
|
|
TileStore: typeof import('@user/data-common').TileStore;
|
|
RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder;
|
|
FaceManager: typeof import('@user/data-common').FaceManager;
|
|
Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler;
|
|
}
|
|
|
|
let modules: TestModules;
|
|
|
|
beforeAll(async () => {
|
|
vi.stubGlobal('main', { replayChecking: true });
|
|
vi.stubGlobal('location', { origin: 'http://localhost' });
|
|
const mapModule = await import('../mapState');
|
|
const commonModule = await import('@user/data-common');
|
|
modules = {
|
|
MapState: mapModule.MapState,
|
|
TileStore: commonModule.TileStore,
|
|
RoleFaceBinder: commonModule.RoleFaceBinder,
|
|
FaceManager: commonModule.FaceManager,
|
|
Dir8FaceHandler: commonModule.Dir8FaceHandler
|
|
};
|
|
});
|
|
|
|
function createMapState(
|
|
pointEvents: Record<number, Record<number, string>> = {},
|
|
blocks: number[] = [1, 1, 1, 1]
|
|
) {
|
|
const tileStore: ITileStore = new modules.TileStore() as never;
|
|
tileStore.addTile({
|
|
num: 1,
|
|
id: 'base',
|
|
events: { 10: 'base-event' },
|
|
type: 0,
|
|
pass: { onlyEvents: false, outPass: 15, inPass: 15 },
|
|
eventPass: true
|
|
});
|
|
tileStore.addTile({
|
|
num: 2,
|
|
id: 'alternate',
|
|
events: { 20: 'alternate-event' },
|
|
type: 0,
|
|
pass: { onlyEvents: false, outPass: 15, inPass: 15 },
|
|
eventPass: true
|
|
});
|
|
const faceManager = new modules.FaceManager();
|
|
faceManager.register(1, new modules.Dir8FaceHandler());
|
|
const state: IDataCommon = {
|
|
tileStore,
|
|
itemStore: {},
|
|
mapStore: {},
|
|
eventStore: {},
|
|
roleFace: new modules.RoleFaceBinder(),
|
|
faceManager,
|
|
saveSystem: {}
|
|
} as never;
|
|
const mapState = new modules.MapState(state);
|
|
const map = mapState.fromRaw({
|
|
floorId: 'F1',
|
|
width: 2,
|
|
map: { 0: blocks },
|
|
layerAlias: { 0: 'event' },
|
|
events: { 0: pointEvents }
|
|
});
|
|
return {
|
|
map: map!,
|
|
layer: [...map!.layerList].find(
|
|
layer => layer.alias === 'event'
|
|
)! as IResizableMapLayer
|
|
};
|
|
}
|
|
|
|
describe('MapLayer tile defaults snapshots conversion and movement', () => {
|
|
// 验证静态/动态图块默认事件、存档快照、转换和移动后的点事件归属
|
|
it('tile defaults snapshots conversion and movement', () => {
|
|
const { layer } = createMapState();
|
|
const staticTile = layer.getTile(0, 0)!;
|
|
expect(staticTile.tileEvent().get()).toEqual(
|
|
new Map([[10, 'base-event']])
|
|
);
|
|
expect(staticTile.tileEvent().dirty()).toBe(false);
|
|
|
|
staticTile.set(2);
|
|
expect(staticTile.tileEvent().get()).toEqual(
|
|
new Map([[20, 'alternate-event']])
|
|
);
|
|
expect(staticTile.tileEvent().dirty()).toBe(false);
|
|
|
|
const dynamicTile = layer.createDynamic(1, 1, 0);
|
|
expect(dynamicTile.tileEvent().get()).toEqual(
|
|
new Map([[10, 'base-event']])
|
|
);
|
|
dynamicTile.set(2);
|
|
expect(dynamicTile.tileEvent().get()).toEqual(
|
|
new Map([[20, 'alternate-event']])
|
|
);
|
|
expect(dynamicTile.tileEvent().dirty()).toBe(false);
|
|
|
|
const cleanSave = dynamicTile.saveState(SaveCompression.NoCompression);
|
|
expect(cleanSave.events).toBeUndefined();
|
|
dynamicTile.loadState(cleanSave, SaveCompression.NoCompression);
|
|
expect(dynamicTile.tileEvent().get()).toEqual(
|
|
new Map([[20, 'alternate-event']])
|
|
);
|
|
expect(dynamicTile.tileEvent().dirty()).toBe(false);
|
|
|
|
dynamicTile.tileEvent().set(30, 'override-event');
|
|
const overrideSave = dynamicTile.saveState(
|
|
SaveCompression.NoCompression
|
|
);
|
|
const savedEvents = new Map(overrideSave.events!);
|
|
dynamicTile.tileEvent().set(30, 'changed-after-save');
|
|
expect(overrideSave.events).toEqual(savedEvents);
|
|
dynamicTile.loadState(overrideSave, SaveCompression.NoCompression);
|
|
expect(dynamicTile.tileEvent().get()).toEqual(
|
|
new Map([
|
|
[20, 'alternate-event'],
|
|
[30, 'override-event']
|
|
])
|
|
);
|
|
expect(dynamicTile.tileEvent().dirty()).toBe(true);
|
|
|
|
staticTile.set(1);
|
|
const discardDynamic = layer.transferToDynamic(0, 0)!;
|
|
discardDynamic.tileEvent().set(30, 'discarded-event');
|
|
const restoredStatic = layer.transferToStatic(discardDynamic, false)!;
|
|
expect(restoredStatic.tileEvent().get()).toEqual(
|
|
new Map([[10, 'base-event']])
|
|
);
|
|
expect(restoredStatic.tileEvent().dirty()).toBe(false);
|
|
|
|
// 先物化 (1, 0) 的静态图块,使转换时能保留其默认事件(与 StaticTile.toDynamic 用法一致)
|
|
layer.getTile(1, 0);
|
|
const keptDynamic = layer.transferToDynamic(1, 0)!;
|
|
keptDynamic.tileEvent().set(30, 'kept-event');
|
|
const keptStatic = layer.transferToStatic(keptDynamic, true)!;
|
|
expect(keptStatic.tileEvent().get()).toEqual(
|
|
new Map([
|
|
[10, 'base-event'],
|
|
[30, 'kept-event']
|
|
])
|
|
);
|
|
expect(keptStatic.tileEvent().dirty()).toBe(true);
|
|
|
|
const point = layer.event(0, 1)!;
|
|
point.set(5, 'point-event');
|
|
point.markPure();
|
|
layer.getPointEvent(0, 1);
|
|
const moving = layer.transferToDynamic(0, 1)!;
|
|
moving.setPos(1, 1);
|
|
expect(layer.getPointEvent(0, 1)).toEqual(
|
|
new Map([[5, 'point-event']])
|
|
);
|
|
expect(layer.getPointEvent(1, 1)).toEqual(new Map());
|
|
});
|
|
});
|
|
|
|
describe('MapLayer point event lifecycle', () => {
|
|
// 验证点事件的 dirty、存档覆盖、基准恢复和不同压缩模式
|
|
it('point event lifecycle', () => {
|
|
const { layer } = createMapState({ 1: { 5: 'raw-point' } });
|
|
const point = layer.event(1, 0)!;
|
|
expect(point.dirty()).toBe(false);
|
|
|
|
point.set(7, 'runtime-point');
|
|
expect(layer.dirty()).toBe(true);
|
|
point.delete(7);
|
|
expect(point.dirty()).toBe(false);
|
|
point.set(5, 'changed-point');
|
|
expect(point.dirty()).toBe(true);
|
|
point.set(5, 'raw-point');
|
|
expect(point.dirty()).toBe(false);
|
|
point.clear();
|
|
expect(point.dirty()).toBe(true);
|
|
point.set(5, 'raw-point');
|
|
expect(point.dirty()).toBe(false);
|
|
|
|
const compressionLevels = [
|
|
SaveCompression.NoCompression,
|
|
SaveCompression.LowCompression,
|
|
SaveCompression.HighCompression
|
|
];
|
|
for (const compression of compressionLevels) {
|
|
point.set(7, 'saved-point');
|
|
const save = layer.saveState(compression);
|
|
expect(save.pointEvents?.get(1)).toEqual(
|
|
new Map([
|
|
[5, 'raw-point'],
|
|
[7, 'saved-point']
|
|
])
|
|
);
|
|
point.set(7, 'changed-after-save');
|
|
layer.loadState(save, compression);
|
|
expect(point.get()).toEqual(
|
|
new Map([
|
|
[5, 'raw-point'],
|
|
[7, 'saved-point']
|
|
])
|
|
);
|
|
expect(point.dirty()).toBe(true);
|
|
point.set(8, 'after-load');
|
|
expect(save.pointEvents?.get(1)).toEqual(
|
|
new Map([
|
|
[5, 'raw-point'],
|
|
[7, 'saved-point']
|
|
])
|
|
);
|
|
point.delete(8);
|
|
}
|
|
|
|
point.set(8, 'runtime-only');
|
|
layer.loadState(
|
|
{
|
|
width: 2,
|
|
height: 2,
|
|
fullMap: new Uint32Array([1, 1, 1, 1])
|
|
},
|
|
SaveCompression.NoCompression
|
|
);
|
|
expect(point.get()).toEqual(new Map([[5, 'raw-point']]));
|
|
expect(point.dirty()).toBe(false);
|
|
});
|
|
|
|
// 验证只有点事件变化时 GameMap 仍保留对应图层存档
|
|
it('map saves layers containing only point events', () => {
|
|
const { map, layer } = createMapState({}, [0, 0, 0, 0]);
|
|
layer.setZIndex(7);
|
|
const point = layer.event(1, 0)!;
|
|
|
|
expect(layer.dirty()).toBe(false);
|
|
point.set(7, 'point-only');
|
|
expect(layer.dirty()).toBe(true);
|
|
|
|
const compressionLevels = [
|
|
SaveCompression.LowCompression,
|
|
SaveCompression.HighCompression
|
|
];
|
|
for (const compression of compressionLevels) {
|
|
const save = map.saveState(compression);
|
|
const layerSave = save.layers.get(7);
|
|
expect(layerSave).toBeDefined();
|
|
expect(layerSave?.fullMap).toBeUndefined();
|
|
expect(layerSave?.rows).toBeUndefined();
|
|
expect(layerSave?.staticBlocks?.size).toBe(0);
|
|
expect(layerSave?.dynamicBlocks?.size).toBe(0);
|
|
expect(layerSave?.pointEvents).toEqual(
|
|
new Map([[1, new Map([[7, 'point-only']])]])
|
|
);
|
|
}
|
|
});
|
|
|
|
// 验证 resize 裁剪范围外事件,resize2 清空全部点事件
|
|
it('resize preserves in-range point events and resize2 clears them', () => {
|
|
const { layer } = createMapState({
|
|
0: { 1: 'top-left' },
|
|
3: { 1: 'bottom-right' }
|
|
});
|
|
layer.resize(1, 1);
|
|
expect(layer.getPointEvent(0, 0)).toEqual(new Map([[1, 'top-left']]));
|
|
expect(layer.getPointEvent(1, 1)).toBeNull();
|
|
|
|
layer.resize2(1, 1);
|
|
expect(layer.getPointEvent(0, 0)).toEqual(new Map());
|
|
});
|
|
|
|
// 验证改变图层宽度后,保留的点事件会使用新索引继续查询
|
|
it('reindexes preserved point events when the layer width changes', () => {
|
|
const { map, layer } = createMapState({
|
|
1: { 1: 'top-right' },
|
|
3: { 1: 'bottom-right' }
|
|
});
|
|
|
|
// 经 GameMap.resizeLayer 变更尺寸,使共享索引器宽度与图层同步后重建点事件索引
|
|
map.resizeLayer(3, 2, true);
|
|
|
|
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']
|
|
])
|
|
]
|
|
])
|
|
);
|
|
});
|
|
});
|