// 测试 GameMap 的图层生命周期、别名、背景、事件层、脏标记与尺寸变化 import { afterAll, describe, expect, it, vi } from 'vitest'; import { type IDataCommon, type ITileRawData, Dir8FaceHandler, FaceGroup, FaceManager, RoleFaceBinder, TileStore, TileType } from '@user/data-common'; import { logger } from '@motajs/common'; import { type IMapLayer } from '../types'; import { GameMap } from '../gameMap'; vi.hoisted(() => { vi.stubGlobal('main', { replayChecking: true }); vi.stubGlobal('location', { origin: 'http://localhost' }); Map.prototype.getOrInsert ??= function ( this: Map, key: K, value: V ): V { const existing = this.get(key); if (existing !== undefined) return existing; this.set(key, value); return value; }; Map.prototype.getOrInsertComputed ??= function ( this: Map, 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; }; }); afterAll(() => { vi.unstubAllGlobals(); }); /** * 构造一条最小图块原始数据 * @param num 图块数字 * @param id 图块字符串 id * @param events 默认事件映射 */ function createTileData( num: number, id: string, events: Record = {} ): ITileRawData { return { num, id, events, type: TileType.Terrain, pass: { onlyEvents: false, outPass: 15, inPass: 15 }, eventPass: true }; } interface GameMapFixture { map: GameMap; tileStore: TileStore; } /** 构造一个空的 GameMap 及其底层公共层对象 */ function createFixture(): GameMapFixture { const tileStore = new TileStore(); tileStore.addTile(createTileData(1, 'base', { 10: 'base-event' })); const faceManager = new FaceManager(); faceManager.register(FaceGroup.Dir8, new Dir8FaceHandler()); const state: IDataCommon = { tileStore, itemStore: {}, mapStore: {}, eventStore: {}, roleFace: new RoleFaceBinder(), faceManager, saveSystem: {} } as never; return { map: new GameMap(state, 'F1', 2, 2), tileStore }; } describe('GameMap layer lifecycle', () => { // 验证 addLayer/removeLayer/hasLayer 与图层列表钩子 it('adds and removes layers and notifies the layer list hook', () => { const { map } = createFixture(); const sizes: number[] = []; map.addHook({ onUpdateLayer: list => { sizes.push(list.size); } }); const layer = map.addLayer('layer'); expect(map.hasLayer(layer)).toBe(true); expect(sizes).toEqual([1]); map.removeLayer(layer); expect(map.hasLayer(layer)).toBe(false); expect(sizes).toEqual([1, 0]); }); // 验证图层别名随加层绑定并可由别名映射取回(旧码 84 已随 setLayerAlias 删除) it('binds layer aliases at addLayer time', () => { const { map } = createFixture(); const first = map.addLayer('event'); const second = map.addLayer('other'); expect(map.aliasLayerMap.get('event')).toBe(first); expect(first.alias).toBe('event'); expect(map.aliasLayerMap.get('other')).toBe(second); expect(second.alias).toBe('other'); }); // 验证移除带别名的图层会一并清除别名映射 it('drops the alias mapping when the layer is removed', () => { const { map } = createFixture(); const layer = map.addLayer('event'); map.removeLayer(layer); expect(map.aliasLayerMap.get('event')).toBeUndefined(); expect(map.hasLayer(layer)).toBe(false); }); }); describe('GameMap settings', () => { // 验证背景设置、读取与背景变化钩子 it('stores the background and notifies the background hook', () => { const { map } = createFixture(); const backgrounds: number[] = []; map.addHook({ onChangeBackground: tile => { backgrounds.push(tile); } }); expect(map.getBackground()).toBe(0); map.setBackground(7); expect(map.getBackground()).toBe(7); expect(backgrounds).toEqual([7]); }); // 验证激活状态切换 it('toggles the active flag', () => { const { map } = createFixture(); expect(map.active).toBe(false); map.setActiveStatus(true); expect(map.active).toBe(true); }); // 验证事件层只接受本楼层的图层,越权告警 131,空值清空 it('accepts own layers, warns 131 for a foreign layer and clears on null', () => { const { map } = createFixture(); const layer = map.addLayer('event'); const foreign = new GameMap(map.state, 'other', 2, 2).addLayer( 'foreign' ); const result = logger.catch(() => map.setEventLayer(foreign)); expect(result.info.map(info => info.code)).toContain(131); expect(map.eventLayer).toBeNull(); map.setEventLayer(layer); expect(map.eventLayer).toBe(layer); map.setEventLayer(null); expect(map.eventLayer).toBeNull(); }); }); describe('GameMap dirty state and resizing', () => { // 验证自身脏标记与图层脏标记的合并判定 it('reports dirty from itself or any layer', () => { const { map } = createFixture(); const layer = map.addLayer('dirty'); expect(map.dirty()).toBe(false); map.markDirty(true); expect(map.dirty()).toBe(true); map.markDirty(false); expect(map.dirty()).toBe(false); layer.setBlock(1, 0, 0); expect(map.dirty()).toBe(true); }); // 验证 resizeLayer 按 keepBlock 保留或清空图块并触发尺寸钩子 it('resizes layers keeping or clearing blocks and notifies hooks', () => { const { map } = createFixture(); const layer = map.addLayer('resizable'); layer.setBlock(1, 0, 0); const resizes: [number, number][] = []; map.addHook({ onResizeLayer: (_layer: IMapLayer, width, height) => { resizes.push([width, height]); } }); map.resizeLayer(3, 2, true); expect(map.width).toBe(3); expect(map.height).toBe(2); expect(layer.getBlock(0, 0)).toBe(1); expect(resizes).toEqual([[3, 2]]); map.resizeLayer(1, 1, false); expect(layer.getBlock(0, 0)).toBe(0); expect(resizes).toEqual([ [3, 2], [1, 1] ]); }); // 验证 compareWith 对缺失参考的图层标脏,对匹配参考的图层保持干净 it('marks layers dirty for a missing reference and compares matching ones', () => { const { map } = createFixture(); const layer = map.addLayer('layer'); layer.setZIndex(0); map.compareWith(new Map()); expect(layer.dirty()).toBe(true); const compared = createFixture().map; const comparedLayer = compared.addLayer('compared'); comparedLayer.setZIndex(0); compared.compareWith(new Map([[0, new Uint32Array(4)]])); expect(comparedLayer.dirty()).toBe(false); }); // 验证多个不同 zIndex 图层并存、各自块数据独立并按 zIndex 参考比较分别判定 it('keeps layers of different z-index coexisting with independent data', () => { const { map } = createFixture(); const low = map.addLayer('low'); const mid = map.addLayer('mid'); const high = map.addLayer('high'); low.setZIndex(1); mid.setZIndex(5); high.setZIndex(9); low.setBlock(3, 0, 0); mid.setBlock(4, 1, 0); high.setBlock(5, 0, 1); expect(map.aliasLayerMap.get('low')).toBe(low); expect(map.aliasLayerMap.get('mid')).toBe(mid); expect(map.aliasLayerMap.get('high')).toBe(high); expect(low.zIndex).toBe(1); expect(mid.zIndex).toBe(5); expect(high.zIndex).toBe(9); expect(low.getBlock(0, 0)).toBe(3); expect(low.getBlock(1, 0)).toBe(0); expect(low.getBlock(0, 1)).toBe(0); expect(mid.getBlock(1, 0)).toBe(4); expect(mid.getBlock(0, 0)).toBe(0); expect(high.getBlock(0, 1)).toBe(5); expect(high.getBlock(0, 0)).toBe(0); expect(high.getBlock(1, 0)).toBe(0); expect(map.dirty()).toBe(true); map.compareWith( new Map([ [1, new Uint32Array([3, 0, 0, 0])], [5, new Uint32Array([0, 4, 0, 0])] ]) ); expect(low.dirty()).toBe(false); expect(mid.dirty()).toBe(false); expect(high.dirty()).toBe(true); }); });