refactor: 地图前半部分

This commit is contained in:
unanmed 2026-09-28 23:36:54 +08:00
parent b3ac1d2164
commit bbdea04393
9 changed files with 109 additions and 152 deletions

2
dev.md
View File

@ -112,7 +112,7 @@
- 语句尽量不换行,除非必要,尤其注意三元运算符与 `private readonly` 类成员。
- 极度不建议写 `getter` 和 `setter`,但在极少数场景下还是可以使用的,一般是必须要提供运算符操作方法且需要监听 `get` 和 `set` 时才允许使用。
- 方法实现中,未使用的后置参数直接不填,不得写以下划线开头的参数名。
- 使用对象解构语法时,除非明确后续可能新增其他变量,否则不允许出现单个的对象解构,如 `const { value } = obj`,必须写成 `const value = obj.value`。
- 使用对象解构语法时,除非明确后续可能新增其他变量,否则不允许出现单个的对象解构,如 `const { value } = obj`,必须写成 `const value = obj.value`。但对于超过一个的情况,建议使用对象解构语法,但不得出现换行。
- 换行使用 `CRLF` 格式。
- 不得将一个对象上的函数声明为一个临时变量/常量,哪怕其没有 `this` 指向问题。
- 通常情况下,一个文件应只包含一个类,但如果一个类仅是另一个类的依赖,或一个类的长度远远小于另一个类等特殊情况时,允许将多个类放在同一个文件中。

View File

@ -3,12 +3,6 @@ import { IMotaDataLoader, IMotaDataLoaderHooks } from '@user/data-state';
import { IClientConfig } from '../common';
export class RenderLoaderHooks implements IMotaDataLoaderHooks {
constructor() {}
awake(): void {}
destroy(): void {}
onCoreConfigLoaded?(loader: IMotaDataLoader): Promise<void> {
const client = loader.getConfig<IClientConfig>('client');
if (!client) {

View File

@ -114,18 +114,23 @@ export class DynamicTile
return save;
}
/**
* 从存档恢复动态图块:先经 `set` 还原图块数字(同时重取原始图块并重建默认事件),
* 再按存档逐条覆盖事件;`num` 与 `events` 即 `IDynamicBlockSave` 的全部字段。
*/
loadState(save: Readonly<IDynamicBlockSave>): void {
this.set(save.num);
const eventView = this.tileEvent();
eventView.clear();
if (save.events) {
const eventView = this.tileEvent();
eventView.clear();
// 包含事件存档,那么需要读取
for (const [priority, id] of save.events) {
eventView.set(priority, id);
}
} else {
// 不包含事件存档,那么需要从原始数据中重建
const raw = this.raw();
if (!raw) return;
for (const [priority, id] of Object.entries(raw.events)) {
const p = Number(priority);
eventView.set(p, id);
}
}
}
}

View File

@ -23,13 +23,9 @@ export class LayerEventView implements ILayerEventView {
* @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))
);
const stored = this.store.get(priority);
const ref = this.reference.get(priority);
return stored !== ref;
}
/**
@ -61,6 +57,14 @@ export class LayerEventView implements ILayerEventView {
this.updateDirtyEntry(priority, before);
}
@shouldReplay('Reseting layer event view should be replayed.')
reset(): void {
this.store.clear();
for (const [priority, id] of this.reference) {
this.store.set(priority, id);
}
}
clear(): void {
this.store.clear();
this.dirtyEntries = this.reference.size;

View File

@ -26,10 +26,10 @@ import { MapLayer } from './mapLayer';
export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
readonly layerList: Set<IResizableMapLayer> = new Set();
/** 图层到图层别名映射 */
readonly layerAliasMap: WeakMap<IMapLayer, string> = new WeakMap();
readonly tileStore: ITileStore;
/** 图层别名到图层的映射 */
readonly aliasLayerMap: Map<symbol, IMapLayer> = new Map();
readonly aliasLayerMap: Map<string, IMapLayer> = new Map();
/** 背景图块 */
private backgroundTile: number = 0;
@ -48,19 +48,19 @@ export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
constructor(
public readonly state: IDataCommon,
public readonly tileStore: ITileStore,
public readonly floorId: string,
public width: number,
public height: number
) {
super();
this.tileStore = state.tileStore;
this.indexer.setWidth(width);
}
@shouldReplay('Adding game map layer should be replayed.')
addLayer(): IMapLayer {
addLayer(alias: string): IMapLayer {
const array = new Uint32Array(this.width * this.height);
const layer = new MapLayer(array, this.width, this.height, this);
const layer = new MapLayer(array, this.width, this.height, this, alias);
this.layerList.add(layer);
this.forEachHook(hook => {
hook.onUpdateLayer?.(this.layerList);
@ -68,18 +68,14 @@ export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
const controller = layer.addHook(new StateMapLayerHook(this, layer));
this.layerHookMap.set(layer, controller);
controller.load();
this.aliasLayerMap.set(alias, layer);
return layer;
}
@shouldReplay('Removing game map layer should be replayed.')
removeLayer(layer: IMapLayer): void {
this.layerList.delete(layer as IResizableMapLayer);
const alias = this.layerAliasMap.get(layer);
if (alias) {
const symbol = Symbol.for(alias);
this.aliasLayerMap.delete(symbol);
this.layerAliasMap.delete(layer);
}
this.aliasLayerMap.delete(layer.alias);
this.forEachHook(hook => {
hook.onUpdateLayer?.(this.layerList);
});
@ -93,25 +89,6 @@ export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
return this.layerList.has(layer as IResizableMapLayer);
}
setLayerAlias(layer: IMapLayer, alias: string): void {
const symbol = Symbol.for(alias);
if (this.aliasLayerMap.has(symbol)) {
logger.warn(84, alias);
return;
}
this.layerAliasMap.set(layer, alias);
this.aliasLayerMap.set(symbol, layer);
}
getLayerByAlias(alias: string): IMapLayer | null {
const symbol = Symbol.for(alias);
return this.aliasLayerMap.get(symbol) ?? null;
}
getLayerAlias(layer: IMapLayer): string | undefined {
return this.layerAliasMap.get(layer);
}
@shouldReplay('Resizing game map layer should be replayed.')
resizeLayer(
width: number,
@ -198,8 +175,9 @@ export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
const layers = new Map<number, IMapLayerSave>();
for (const layer of this.layerList) {
const save = layer.saveState(compression);
if (this.isEmptyLayerSave(save)) continue;
layers.set(layer.zIndex, save);
if (!this.isEmptyLayerSave(save)) {
layers.set(layer.zIndex, save);
}
}
return {
background: this.backgroundTile,

View File

@ -47,8 +47,9 @@ export class MapLayer
/** 坐标到动态图块集合的映射,外层 key = y,内层 key = x */
private readonly tilePosMap: Map<number, Map<number, Set<IDynamicTile>>> =
new Map();
/** 动态图块到其当前坐标的映射 */
private readonly posTileMap: Map<IDynamicTile, ITileLocator> = new Map();
/** 图层的所有已缓存位置的动态图块,目的是能够持有更新前的位置,从而在图块更新时能够更新内部存储 */
private readonly cachedDynamics: Map<IDynamicTile, ITileLocator> =
new Map();
/** 图层脏标记 */
private layerDirty: boolean = false;
/** 图层参考基准,用于存档压缩对比 */
@ -61,7 +62,8 @@ export class MapLayer
array: Uint32Array,
width: number,
height: number,
public readonly map: IGameMap
readonly map: IGameMap,
readonly alias: string
) {
super();
this.state = map.state;
@ -78,52 +80,31 @@ export class MapLayer
}
/**
* 将动态图块登记到指定坐标的索引表中
* 将动态图块登记到索引表中
* @param tile 动态图块
* @param x 横坐标
* @param y 纵坐标
*/
private addTileToPosMap(tile: IDynamicTile, x: number, y: number): void {
let xMap = this.tilePosMap.get(y);
if (!xMap) {
xMap = new Map();
this.tilePosMap.set(y, xMap);
}
let set = xMap.get(x);
if (!set) {
set = new Set();
xMap.set(x, set);
}
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 removeTileFromPosMap(
tile: IDynamicTile,
x: number,
y: number
): void {
private removeDynamic(tile: IDynamicTile, x: number, y: number): void {
this.cachedDynamics.delete(tile);
this.tilePosMap.get(y)?.get(x)?.delete(tile);
}
/**
* 从两个内部映射中移除图块记录
* @param tile 动态图块
*/
private removeTile(tile: IDynamicTile): void {
const pos = this.posTileMap.get(tile);
if (pos) {
this.removeTileFromPosMap(tile, pos.x, pos.y);
}
this.posTileMap.delete(tile);
}
//#region 点事件操作
//#region 事件操作
/**
* 将动态图块的事件同步回当前静态格点
@ -431,8 +412,7 @@ export class MapLayer
tileEvent.set(priority, id);
}
}
this.addTileToPosMap(tile, x, y);
this.posTileMap.set(tile, { x, y });
this.addDynamicToPosMap(tile);
this.forEachHook(hook => hook.onCreateDynamic?.(tile));
return tile;
}
@ -463,25 +443,45 @@ export class MapLayer
return tile;
}
/**
* 真正执行动态到静态的转换
* @param tile 动态图块实例
* @param keepEvent 是否保持图块事件至静态图层
*/
private toStatic(
tile: IDynamicTile,
keepEvent: boolean = true
): IStaticTile | null {
this.setBlock(tile.num(), tile.x, tile.y);
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);
}
}
this.removeDynamic(tile, tile.x, tile.y);
this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
return staticTile;
}
@shouldReplay('Transfering dynamic tile to static should be replayed.')
transferToStatic(
tile: IDynamicTile,
keepEvent: boolean = true
): IStaticTile | null {
const x = tile.x;
const y = tile.y;
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
const { x, y } = tile;
if (!this.inMap(x, y)) {
logger.warn(128, x.toString(), y.toString());
return null;
}
if (this.getBlock(x, y) !== 0) {
logger.warn(129, x.toString(), y.toString());
}
this.setBlock(tile.num(), x, y);
this.syncStaticEvent(tile, keepEvent);
this.removeTile(tile);
this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
return this.getTile(x, y);
return this.toStatic(tile, keepEvent);
}
@shouldReplay('Transfering dynamic tile to static should be replayed.')
@ -489,27 +489,17 @@ export class MapLayer
tile: IDynamicTile,
keepEvent: boolean = true
): IStaticTile | null {
const x = tile.x;
const y = tile.y;
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
logger.warn(128, x.toString(), y.toString());
if (!this.inMap(tile.x, tile.y)) {
logger.warn(128, tile.x.toString(), tile.y.toString());
return null;
}
if (this.getBlock(tile.x, tile.y) !== 0) return null;
this.setBlock(tile.num(), x, y);
this.syncStaticEvent(tile, keepEvent);
this.removeTile(tile);
this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
return this.getTile(x, y);
return this.toStatic(tile, keepEvent);
}
@shouldReplay('Deleting dynamic tile should be replayed.')
async deleteDynamic(tile: IDynamicTile): Promise<void> {
if (!this.posTileMap.has(tile)) {
logger.warn(130);
return;
}
this.removeTile(tile);
this.removeDynamic(tile, tile.x, tile.y);
const hooks = this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
await Promise.all(hooks);
}
@ -519,7 +509,7 @@ export class MapLayer
}
iterateDynamicTiles(): Iterable<IDynamicTile> {
return this.posTileMap.keys();
return this.cachedDynamics.keys();
}
@shouldReplay('Setting map layer block direction should be replayed.')
@ -529,15 +519,15 @@ export class MapLayer
@shouldReplay('Updating dynamic tile position should be replayed.')
updateDynamicTile(tile: IDynamicTile): void {
const oldPos = this.posTileMap.get(tile);
const oldPos = this.cachedDynamics.get(tile);
if (oldPos) {
this.removeTileFromPosMap(tile, oldPos.x, oldPos.y);
this.removeDynamic(tile, oldPos.x, oldPos.y);
oldPos.x = tile.x;
oldPos.y = tile.y;
this.addTileToPosMap(tile, tile.x, tile.y);
this.addDynamicToPosMap(tile);
} else {
this.addTileToPosMap(tile, tile.x, tile.y);
this.posTileMap.set(tile, { x: tile.x, y: tile.y });
this.addDynamicToPosMap(tile);
this.cachedDynamics.set(tile, { x: tile.x, y: tile.y });
}
this.forEachHook(hook => hook.onUpdateDynamicPosition?.(tile));
}
@ -866,7 +856,7 @@ export class MapLayer
const tiles = [...this.iterateDynamicTiles()];
for (const tile of tiles) {
this.syncStaticEvent(tile, false);
this.removeTile(tile);
this.removeDynamic(tile, tile.x, tile.y);
this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
}
}

View File

@ -66,6 +66,11 @@ export interface ILayerEventView extends IReadonlyEventView {
*/
delete(priority: number): void;
/**
* 将点事件重置为参考基准
*/
reset(): void;
/**
* 清空所有事件
*/
@ -152,7 +157,7 @@ export interface IDynamicTile
ISaveableContent<Readonly<IDynamicBlockSave>> {
/** 该动态图块的移动器 */
readonly mover: IObjectMover<IDynamicTile>;
/** 该动态图块所属的动态图层 */
/** 该动态图块所属的图层 */
readonly layer: IMapLayer;
/**
@ -210,7 +215,7 @@ export interface IMapLayerHooks extends IHookBase {
* @param width 地图宽度
* @param height 地图高度
*/
onResize(width: number, height: number): void;
onResize?(width: number, height: number): void;
/**
* 当更新某个区域的图块时执行,对应于 `putMapData` 方法
@ -219,7 +224,7 @@ export interface IMapLayerHooks extends IHookBase {
* @param width 更新区域宽度
* @param height 更新区域高度
*/
onUpdateArea(x: number, y: number, width: number, height: number): void;
onUpdateArea?(x: number, y: number, width: number, height: number): void;
/**
* 当更新某个点的图块时执行,如果设置的图块与原先一样,则不会触发此方法,对应于 `setBlock` 方法
@ -227,14 +232,14 @@ export interface IMapLayerHooks extends IHookBase {
* @param x 更新点横坐标
* @param y 更新点纵坐标
*/
onUpdateBlock(block: number, x: number, y: number): void;
onUpdateBlock?(block: number, x: number, y: number): void;
/**
* 当开门时触发,返回一个 `Promise`,当相关动画执行完毕后兑现
* @param x 门横坐标
* @param y 门纵坐标
*/
onOpenDoor(x: number, y: number): Promise<void>;
onOpenDoor?(x: number, y: number): Promise<void>;
/**
* 当关门时触发,返回一个 `Promise`,当相关动画执行完毕后兑现
@ -242,7 +247,7 @@ export interface IMapLayerHooks extends IHookBase {
* @param x 门横坐标
* @param y 门纵坐标
*/
onCloseDoor(num: number, x: number, y: number): Promise<void>;
onCloseDoor?(num: number, x: number, y: number): Promise<void>;
/**
* 当动态图块被创建时触发,包括从静态图块转换为动态图块
@ -489,6 +494,9 @@ export interface IMapLayer
/** 该图层使用的朝向绑定器 */
readonly faceBinder: IRoleFaceBinder;
/** 图层别名,可以用于从 `IGameMap` 获取图层实例 */
readonly alias: string;
/**
* 设置图层的参考基准,存档时会根据参考基准进行必要的压缩处理,仅可调用一次,多次调用无效
* @param data 图层的静态地图矩阵
@ -638,8 +646,9 @@ export interface IGameMap
/**
* 添加图层,使用楼层预设的宽高
* @param alias 图层别名,用于获取图层实例
*/
addLayer(): IMapLayer;
addLayer(alias: string): IMapLayer;
/**
* 移除指定图层
@ -653,25 +662,6 @@ export interface IGameMap
*/
hasLayer(layer: IMapLayer): boolean;
/**
* 设置图层别名
* @param layer 图层对象
* @param alias 图层别名
*/
setLayerAlias(layer: IMapLayer, alias: string): void;
/**
* 根据图层别名获取图层对象
* @param alias 图层别名
*/
getLayerByAlias(alias: string): IMapLayer | null;
/**
* 获取图层对象的别名
* @param layer 图层对象
*/
getLayerAlias(layer: IMapLayer): string | undefined;
/**
* 设置地图的参考基准,存档时会根据参考基准进行必要的压缩,仅可调用一次,多次调用无效
* @param data 地图的静态地图矩阵,键表示纵深,值表示对应图层的矩阵

View File

@ -3,10 +3,6 @@ import { ICoreStateCoreConfig } from '../common';
import { IMotaDataLoader, IMotaDataLoaderHooks } from './types';
export class DefaultDataLoaderHook implements IMotaDataLoaderHooks {
awake(): void {}
destroy(): void {}
onCoreConfigLoaded(loader: IMotaDataLoader): Promise<void> {
const coreConfig = loader.getConfig<ICoreStateCoreConfig>('core');
if (!coreConfig) {

View File

@ -69,13 +69,13 @@ export interface IHookBase {
* 加载此钩子对象
* @param controller 钩子控制器对象
*/
awake(controller: IHookController<this>): void;
awake?(controller: IHookController<this>): void;
/**
* 摧毁此钩子对象
* @param controller 钩子控制器对象
*/
destroy(controller: IHookController<this>): void;
destroy?(controller: IHookController<this>): void;
}
export interface IHookable<