mirror of
https://github.com/motajs/template.git
synced 2026-09-28 06:00:17 +08:00
287 lines
8.2 KiB
TypeScript
287 lines
8.2 KiB
TypeScript
import {
|
|
IDataCommon,
|
|
IMapRawData,
|
|
SaveCompression,
|
|
shouldReplay
|
|
} from '@user/data-common';
|
|
import { ITileStore } from '@user/data-common';
|
|
import {
|
|
IGameMap,
|
|
IGameMapSave,
|
|
IMapState,
|
|
IMapStoreSave,
|
|
MapArea
|
|
} from './types';
|
|
import { GameMap } from './gameMap';
|
|
import { logger } from '@motajs/common';
|
|
|
|
export class MapState implements IMapState {
|
|
/** 楼层 id 到状态对象的映射 */
|
|
private readonly mapData: Map<string, IGameMap> = new Map();
|
|
|
|
/** 所有楼层 id 的有序数组 */
|
|
readonly maps: string[] = [];
|
|
|
|
/** 是否已经调用过 compareWith 设置参考基准 */
|
|
private compared: boolean = false;
|
|
|
|
/** 分区列表 */
|
|
private areaList: Set<MapArea> = new Set();
|
|
|
|
/** 上一次调用 notifyEnterFloor 传入的楼层 id */
|
|
private lastFloorId: string | null = null;
|
|
|
|
/** 自动分区激活器开关 */
|
|
private autoActivitorEnabled: boolean = false;
|
|
|
|
constructor(
|
|
private readonly tileStore: ITileStore,
|
|
public readonly state: IDataCommon
|
|
) {}
|
|
|
|
//#region 楼层管理
|
|
|
|
fromRaw(raw: IMapRawData): IGameMap | null {
|
|
let length = 0;
|
|
const entries = Object.entries(raw.map);
|
|
for (const [_, map] of Object.entries(raw.map)) {
|
|
if (length > 0 && map.length !== length) {
|
|
logger.error(
|
|
60,
|
|
map.length.toString(),
|
|
length.toString(),
|
|
raw.floorId
|
|
);
|
|
return null;
|
|
} else {
|
|
length = map.length;
|
|
}
|
|
}
|
|
if (length % raw.width !== 0) {
|
|
logger.error(
|
|
61,
|
|
length.toString(),
|
|
raw.width.toString(),
|
|
raw.floorId
|
|
);
|
|
return null;
|
|
}
|
|
|
|
// 设置楼层图块数据
|
|
const height = length / raw.width;
|
|
const state = this.createMap(raw.floorId, raw.width, height);
|
|
for (const [zIndex, map] of entries) {
|
|
const z = Number(zIndex);
|
|
const layer = state.addLayer();
|
|
const alias = raw.layerAlias[z];
|
|
layer.setMapRef(new Uint32Array(map));
|
|
layer.setZIndex(z);
|
|
state.setLayerAlias(layer, alias);
|
|
|
|
// 设置坐标点事件
|
|
const events = raw.events[z];
|
|
for (const [index, tileEvents] of Object.entries(events)) {
|
|
const indexNum = Number(index);
|
|
const x = indexNum % raw.width;
|
|
const y = Math.floor(indexNum / raw.width);
|
|
const eventView = layer.event(x, y)!;
|
|
for (const [priority, id] of Object.entries(tileEvents)) {
|
|
const priorityNum = Number(priority);
|
|
eventView.set(priorityNum, id);
|
|
}
|
|
eventView.markPure();
|
|
}
|
|
if (alias === 'event') {
|
|
state.setEventLayer(layer);
|
|
}
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
@shouldReplay('Creating map should be replayed.')
|
|
createMap(id: string, width: number, height: number): IGameMap {
|
|
if (this.mapData.has(id)) {
|
|
logger.warn(121, id);
|
|
} else {
|
|
this.maps.push(id);
|
|
}
|
|
const tile = this.tileStore;
|
|
const state = new GameMap(this.state, tile, id, width, height);
|
|
// 若已设置参考基准,新楼层直接视为全脏
|
|
if (this.compared) {
|
|
state.markDirty(true);
|
|
}
|
|
this.mapData.set(id, state);
|
|
return state;
|
|
}
|
|
|
|
@shouldReplay('Setting map list should be replayed.')
|
|
setMapList(maps: string[]): void {
|
|
this.maps.length = 0;
|
|
this.maps.push(...maps);
|
|
}
|
|
|
|
getMap(id: string): IGameMap | null {
|
|
return this.mapData.get(id) ?? null;
|
|
}
|
|
|
|
getActiveMap(id: string): IGameMap | null {
|
|
const state = this.mapData.get(id);
|
|
if (!state || !state.active) return null;
|
|
return state;
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region 分区管理
|
|
|
|
setArea(areas: Set<MapArea>): void {
|
|
this.areaList = areas;
|
|
}
|
|
|
|
@shouldReplay('Activating area should be replayed.')
|
|
activeArea(id: string): void {
|
|
const idx = this.maps.indexOf(id);
|
|
if (idx === -1) return;
|
|
const area = this.findAreaByIndex(idx);
|
|
if (!area) return;
|
|
this.setAreaActive(area, true);
|
|
}
|
|
|
|
@shouldReplay('Deactivating area should be replayed.')
|
|
deactiveArea(id: string): void {
|
|
const idx = this.maps.indexOf(id);
|
|
if (idx === -1) return;
|
|
const area = this.findAreaByIndex(idx);
|
|
if (!area) return;
|
|
this.setAreaActive(area, false);
|
|
}
|
|
|
|
useAutoActivitor(enable: boolean): void {
|
|
this.autoActivitorEnabled = enable;
|
|
}
|
|
|
|
notifyEnterFloor(id: string): void {
|
|
if (!this.autoActivitorEnabled) return;
|
|
const idx = this.maps.indexOf(id);
|
|
if (idx === -1) return;
|
|
const area = this.findAreaByIndex(idx);
|
|
if (!area) return;
|
|
if (this.lastFloorId !== null) {
|
|
this.deactiveArea(this.lastFloorId);
|
|
}
|
|
this.activeArea(id);
|
|
this.lastFloorId = id;
|
|
}
|
|
|
|
/**
|
|
* 根据 maps 下标查找其所属的分区
|
|
* @param idx 楼层在 maps 中的下标
|
|
*/
|
|
private findAreaByIndex(idx: number): MapArea | null {
|
|
for (const area of this.areaList) {
|
|
for (const interval of area) {
|
|
if (idx >= interval.start && idx <= interval.end) {
|
|
return area;
|
|
}
|
|
}
|
|
}
|
|
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++) {
|
|
const floorId = this.maps[i];
|
|
if (floorId !== undefined) {
|
|
this.setMapActiveStatus(floorId, active);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region active 管理
|
|
|
|
isMapActive(id: string): boolean {
|
|
return this.mapData.get(id)?.active ?? false;
|
|
}
|
|
|
|
@shouldReplay('Setting map active status should be replayed.')
|
|
setMapActiveStatus(id: string, active: boolean): void {
|
|
this.mapData.get(id)?.setActiveStatus(active);
|
|
}
|
|
|
|
*iterateActiveMaps(): Iterable<[string, IGameMap]> {
|
|
for (const [id, state] of this.mapData) {
|
|
if (state.active) yield [id, state];
|
|
}
|
|
}
|
|
|
|
*iterateInactiveMaps(): Iterable<[string, IGameMap]> {
|
|
for (const [id, state] of this.mapData) {
|
|
if (!state.active) yield [id, state];
|
|
}
|
|
}
|
|
|
|
iterateAllMaps(): Iterable<[string, IGameMap]> {
|
|
return this.mapData;
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region 存档对比
|
|
|
|
compareWith(ref: Map<string, Map<number, Uint32Array>>): void {
|
|
if (this.compared) return;
|
|
this.compared = true;
|
|
|
|
for (const [id, state] of this.mapData) {
|
|
const refFloor = ref.get(id);
|
|
if (!refFloor) {
|
|
state.markDirty(true);
|
|
continue;
|
|
}
|
|
state.compareWith(refFloor);
|
|
}
|
|
}
|
|
|
|
//#region 存读档
|
|
|
|
saveState(compression: SaveCompression): IMapStoreSave {
|
|
const floors = new Map<string, IGameMapSave>();
|
|
for (const [id, state] of this.mapData) {
|
|
if (!state.active) continue;
|
|
floors.set(id, state.saveState(compression));
|
|
}
|
|
return { floors };
|
|
}
|
|
|
|
loadState(state: IMapStoreSave, compression: SaveCompression): void {
|
|
if (compression !== SaveCompression.NoCompression && !this.compared) {
|
|
logger.error(55);
|
|
return;
|
|
}
|
|
for (const [id, cur] of this.mapData) {
|
|
cur.setActiveStatus(state.floors.has(id));
|
|
}
|
|
for (const [id, save] of state.floors) {
|
|
const cur = this.mapData.get(id);
|
|
if (!cur) {
|
|
logger.warn(122, id);
|
|
continue;
|
|
}
|
|
cur.loadState(save, compression);
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
}
|