feat: Adding should replay flag

This commit is contained in:
unanmed 2026-09-23 13:09:43 +08:00
parent 7743795948
commit d74cc35c8e
16 changed files with 86 additions and 186 deletions

View File

@ -1,3 +1,4 @@
import { shouldReplay } from '@user/data-common';
import { IFlagCommonField, IFlagSystem } from './types';
import { logger } from '@motajs/common';
@ -16,10 +17,12 @@ export class FlagCommonField<T> implements IFlagCommonField<T> {
this.value = value;
}
@shouldReplay('Flag field value set should be replayed.')
set(value: T): void {
this.value = value;
}
@shouldReplay('Flag field value set should be replayed.')
add(value: number): void {
if (typeof this.value !== 'number') {
logger.warn(111, String(this.key));

View File

@ -1,5 +1,6 @@
import { IFlagCommonField, IFlagSystem, IFlagSystemSave } from './types';
import { FlagCommonField } from './field';
import { shouldReplay } from '@user/data-common';
export class FlagSystem implements IFlagSystem {
private readonly fieldMap: Map<PropertyKey, IFlagCommonField<any>> =
@ -9,6 +10,7 @@ export class FlagSystem implements IFlagSystem {
return this.fieldMap.has(field);
}
@shouldReplay('Flag field set should be replayed.')
setField<T>(field: PropertyKey, value: T): IFlagCommonField<T> {
return this.getOrInsert(field, value);
}
@ -34,6 +36,7 @@ export class FlagSystem implements IFlagSystem {
);
}
@shouldReplay('Flag field deleting should be replayed.')
deleteField(field: PropertyKey): void {
this.fieldMap.delete(field);
}
@ -63,26 +66,11 @@ export class FlagSystem implements IFlagSystem {
}
loadState(state: IFlagSystemSave): void {
// 按 key 复用现有字段原地写值,使外部持有的引用跨读档仍然有效
this.fieldMap.clear();
for (const [key, data] of state.fields) {
const existing = this.fieldMap.get(key);
if (existing) {
existing.fromStructured(data);
} else {
const field = new FlagCommonField<unknown>(
this,
key,
undefined
);
field.fromStructured(data);
this.fieldMap.set(key, field);
}
}
// 以存档为准:存档中不存在的字段一律删除
for (const key of this.fieldMap.keys()) {
if (!state.fields.has(key)) {
this.fieldMap.delete(key);
}
const field = new FlagCommonField(this, key, void 0);
field.fromStructured(data);
this.fieldMap.set(key, field);
}
}
}

View File

@ -3,7 +3,8 @@ import {
IDataCommon,
ItemCategory,
ReplayCode,
SaveCompression
SaveCompression,
shouldReplay
} from '@user/data-common';
import { HeroEquipsStore } from './equipStore';
import {
@ -75,6 +76,7 @@ export class HeroItems<THero> implements IHeroItems<THero> {
return this.getItemState(item)?.count ?? 0;
}
@shouldReplay('Adding item to hero should be replayed.')
addItem(item: number | string, count: number = 1): void {
const num = this.resolveNum(item);
if (isNil(num)) return;
@ -113,6 +115,7 @@ export class HeroItems<THero> implements IHeroItems<THero> {
this.addItem(item, 1);
}
@shouldReplay('Using item should be replayed.')
useItem(item: number | string): boolean {
const state = this.internalGetItemState(item);
if (!state) return false;

View File

@ -7,7 +7,8 @@ import {
import {
FaceDirection,
IDataCommon,
IFacedTileLocator
IFacedTileLocator,
shouldReplay
} from '@user/data-common';
import { HeroMover } from './mover';
import {
@ -50,12 +51,14 @@ export class HeroLocation
return new HookController(this, hook);
}
@shouldReplay('Setting hero floor should be replayed.')
setFloor(map: IGameMap): void {
this.floorId = map.floorId;
this.map = map;
this.forEachHook(hook => hook.onSetFloor?.(map));
}
@shouldReplay('Setting hero position should be replayed.')
setPos(x: number, y: number): void {
this.x = x;
this.y = y;

View File

@ -6,7 +6,8 @@ import {
ObjectMover,
ObjectMoveType,
IDataCommon,
ReplayCode
ReplayCode,
shouldReplay
} from '@user/data-common';
import {
HeroMoveCode,
@ -152,6 +153,7 @@ export class HeroMover<T extends IHeroLocation>
protected async onMoveEnd(): Promise<void> {}
@shouldReplay('Hero moving step should be replayed.')
protected async onStepStart(
step: Readonly<ObjectMoveStep>,
tile: IHeroLocation
@ -238,6 +240,7 @@ export class HeroMover<T extends IHeroLocation>
return HeroMoveCode.Step;
}
@shouldReplay('Hero moving step should be replayed.')
protected async onStepEnd(
code: number,
step: Readonly<ObjectMoveStep>,
@ -279,6 +282,7 @@ export class HeroMover<T extends IHeroLocation>
return { x: tile.x, y: tile.y };
}
@shouldReplay('Hero moving step should be replayed.')
protected async onStepSettled(
before: ITileLocator,
curr: ITileLocator,

View File

@ -3,7 +3,8 @@ import {
FaceDirection,
IMoverController,
IObjectMover,
ITileRawData
ITileRawData,
shouldReplay
} from '@user/data-common';
import {
IDynamicBlockSave,
@ -53,6 +54,7 @@ export class DynamicTile
return this.tileRaw;
}
@shouldReplay('Setting dynamic block num should be replayed.')
set(num: number): void {
this.tileNum = num;
const data = this.state.tileStore.getData(num);
@ -65,6 +67,7 @@ export class DynamicTile
this.restoreDefaultEvents();
}
@shouldReplay('Setting dynamic block position should be replayed.')
setPos(x: number, y: number): void {
this.x = x;
this.y = y;
@ -81,10 +84,12 @@ export class DynamicTile
}
}
@shouldReplay('Transfering dynamic tile to static should be replayed.')
toStatic(): IStaticTile | null {
return this.layer.transferToStatic(this);
}
@shouldReplay('Transfering dynamic tile to static should be replayed.')
toStaticIfSafe(): IStaticTile | null {
return this.layer.transferToStaticIfSafe(this);
}

View File

@ -1,5 +1,6 @@
import { logger } from '@motajs/common';
import { ILayerEventView } from './types';
import { shouldReplay } from '@user/data-common';
export class LayerEventView implements ILayerEventView {
/** 当前绑定的事件 */
@ -43,6 +44,7 @@ export class LayerEventView implements ILayerEventView {
}
}
@shouldReplay('Setting layer event view should be replayed.')
set(priority: number, event: string): void {
if (this.store.has(priority)) {
logger.warn(136, priority.toString());
@ -52,6 +54,7 @@ export class LayerEventView implements ILayerEventView {
this.updateDirtyEntry(priority, before);
}
@shouldReplay('Deleting layer event view should be replayed.')
delete(priority: number): void {
const before = this.isEntryDirty(priority);
this.store.delete(priority);

View File

@ -19,7 +19,8 @@ import {
ILocationIndexer,
ITileStore,
MapLocIndexer,
SaveCompression
SaveCompression,
shouldReplay
} from '@user/data-common';
import { MapLayer } from './mapLayer';
@ -56,6 +57,7 @@ export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
this.indexer.setWidth(width);
}
@shouldReplay('Adding game map layer should be replayed.')
addLayer(): IMapLayer {
const array = new Uint32Array(this.width * this.height);
const layer = new MapLayer(array, this.width, this.height, this);
@ -69,6 +71,7 @@ export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
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);
@ -109,6 +112,7 @@ export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
return this.layerAliasMap.get(layer);
}
@shouldReplay('Resizing game map layer should be replayed.')
resizeLayer(
width: number,
height: number,
@ -141,6 +145,7 @@ export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
this.active = active;
}
@shouldReplay("Setting game map's event layer should be replayed.")
setEventLayer(layer: IMapLayer | null): void {
if (!layer) {
this.eventLayer = null;

View File

@ -19,7 +19,8 @@ import {
FaceDirection,
IDataCommon,
IRoleFaceBinder,
SaveCompression
SaveCompression,
shouldReplay
} from '@user/data-common';
import { DynamicTile } from './dynamicTile';
import { LayerEventView } from './eventView';
@ -233,6 +234,7 @@ export class MapLayer
return x >= 0 && y >= 0 && x < this.width && y < this.height;
}
@shouldReplay('Setting map layer block should be replayed.')
setBlock(block: number, x: number, y: number): void {
if (!this.inMap(x, y)) return;
const index = y * this.width + x;
@ -254,6 +256,7 @@ export class MapLayer
return this.mapArray[y * this.width + x];
}
@shouldReplay('Removing map layer block should be replayed.')
removeBlock(x: number, y: number): number {
if (!this.inMap(x, y)) {
return -1;
@ -287,6 +290,7 @@ export class MapLayer
};
}
@shouldReplay('Setting map layer data should be replayed.')
putMapData(array: Uint32Array, x: number, y: number, width: number): void {
if (array.length % width !== 0) {
logger.warn(8);
@ -394,6 +398,7 @@ export class MapLayer
return this.mapData;
}
@shouldReplay('Setting map layer block direction should be replayed.')
setStaticDirection(x: number, y: number, direction: FaceDirection): number {
const tile = this.getTile(x, y);
if (!tile) return -1;
@ -415,6 +420,7 @@ export class MapLayer
//#region 动态图层操作
@shouldReplay('Creating dynamic tile should be replayed.')
createDynamic(num: number, x: number, y: number): IDynamicTile {
const tile = new DynamicTile(num, x, y, this);
const location = this.getLocationData(x, y);
@ -432,6 +438,7 @@ export class MapLayer
return tile;
}
@shouldReplay('Transfering static tile to dynamic should be replayed.')
transferToDynamic(
x: number,
y: number,
@ -457,6 +464,7 @@ export class MapLayer
return tile;
}
@shouldReplay('Transfering dynamic tile to static should be replayed.')
transferToStatic(
tile: IDynamicTile,
keepEvent: boolean = true
@ -477,6 +485,7 @@ export class MapLayer
return this.getTile(x, y);
}
@shouldReplay('Transfering dynamic tile to static should be replayed.')
transferToStaticIfSafe(
tile: IDynamicTile,
keepEvent: boolean = true
@ -495,6 +504,7 @@ export class MapLayer
return this.getTile(x, y);
}
@shouldReplay('Deleting dynamic tile should be replayed.')
async deleteDynamic(tile: IDynamicTile): Promise<void> {
if (!this.posTileMap.has(tile)) {
logger.warn(130);
@ -513,6 +523,7 @@ export class MapLayer
return this.posTileMap.keys();
}
@shouldReplay('Setting map layer block direction should be replayed.')
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): number {
const numBefore = tile.num();
tile.setFaceDirection(direction);
@ -524,6 +535,7 @@ export class MapLayer
return tile.num();
}
@shouldReplay('Updating dynamic tile position should be replayed.')
updateDynamicTile(tile: IDynamicTile): void {
const oldPos = this.posTileMap.get(tile);
if (oldPos) {
@ -542,6 +554,7 @@ export class MapLayer
//#region 开关门
@shouldReplay('Opening door should be replayed.')
async openDoor(x: number, y: number): Promise<void> {
const index = y * this.width + x;
const num = this.mapArray[index];
@ -554,6 +567,7 @@ export class MapLayer
this.setBlock(0, x, y);
}
@shouldReplay('Closing door should be replayed.')
async closeDoor(num: number, x: number, y: number): Promise<void> {
const index = y * this.width + x;
const nowNum = this.mapArray[index];
@ -573,10 +587,12 @@ export class MapLayer
//#region 图层操作
@shouldReplay('Setting z-index of map layer should be replayed.')
setZIndex(zIndex: number): void {
this.zIndex = zIndex;
}
@shouldReplay('Setting role face binder of map layer should be replayed.')
setFaceBinder(binder: IRoleFaceBinder | null): void {
if (!binder) return;
this.faceBinder = binder;
@ -616,6 +632,7 @@ export class MapLayer
return new MapLayerHookController(this, hook);
}
@shouldReplay('Resizing map layer should be replayed.')
resize(width: number, height: number): void {
if (this.width === width && this.height === height) {
return;
@ -656,6 +673,7 @@ export class MapLayer
});
}
@shouldReplay('Resizing map layer should be replayed.')
resize2(width: number, height: number): void {
this.layerDirty = true;
this.pointEvents.clear();

View File

@ -1,5 +1,9 @@
import { uniq } from 'lodash-es';
import { IDataCommon, IMapRawData, SaveCompression } from '@user/data-common';
import {
IDataCommon,
IMapRawData,
SaveCompression,
shouldReplay
} from '@user/data-common';
import { ITileStore } from '@user/data-common';
import {
IGameMap,
@ -35,160 +39,9 @@ export class MapState implements IMapState {
public readonly state: IDataCommon
) {}
/**
* 判断原始数据中的值是否为可枚举的对象容器
* @param value 待判断的运行时值
*/
private isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
/**
* 判断字符串键是否能无歧义地转换为有限数字
* @param key 原始对象键
*/
private isNumericKey(key: string): boolean {
return key.trim() !== '' && Number.isFinite(Number(key));
}
/**
* 在创建地图前验证外部原始地图及事件结构
* @param raw 待验证的楼层原始数据
*/
private validateRaw(raw: IMapRawData): boolean {
const rawMap: unknown = raw.map;
const rawEvents: unknown = raw.events;
const rawAliases: unknown = raw.layerAlias;
if (!this.isRecord(rawMap)) {
logger.error(63, 'map', raw.floorId);
return false;
}
if (!this.isRecord(rawEvents)) {
logger.error(63, 'events', raw.floorId);
return false;
}
if (!this.isRecord(rawAliases)) {
logger.error(63, 'layerAlias', raw.floorId);
return false;
}
if (!Number.isInteger(raw.width) || raw.width <= 0) {
logger.error(64, 'width', raw.floorId);
return false;
}
let length = 0;
for (const [zIndex, map] of Object.entries(rawMap)) {
if (!this.isNumericKey(zIndex)) {
logger.error(
62,
'layer',
raw.floorId,
'IMapRawData.map',
zIndex
);
return false;
}
if (!Array.isArray(map)) {
logger.error(64, 'map layer', raw.floorId);
return false;
}
if (
!map.every(
value =>
typeof value === 'number' &&
Number.isFinite(value) &&
Number.isInteger(value) &&
value >= 0
)
) {
logger.error(64, 'map value', raw.floorId);
return false;
}
if (length > 0 && map.length !== length) {
logger.error(
60,
map.length.toString(),
length.toString(),
raw.floorId
);
return false;
}
length = map.length;
const alias = rawAliases[zIndex];
if (typeof alias !== 'string') {
logger.error(64, 'layer alias', raw.floorId);
return false;
}
const events = rawEvents[zIndex];
if (!this.isRecord(events)) {
logger.error(63, `events layer ${zIndex}`, raw.floorId);
return false;
}
for (const [index, tileEvents] of Object.entries(events)) {
if (!this.isNumericKey(index)) {
logger.error(
62,
'event',
raw.floorId,
'IMapRawData.events',
index
);
return false;
}
const indexNum = Number(index);
if (
!Number.isInteger(indexNum) ||
indexNum < 0 ||
indexNum >= length
) {
logger.error(64, 'event position', raw.floorId);
return false;
}
if (!this.isRecord(tileEvents)) {
logger.error(63, `events position ${index}`, raw.floorId);
return false;
}
for (const [priority, id] of Object.entries(tileEvents)) {
if (!this.isNumericKey(priority)) {
logger.error(
62,
'event',
raw.floorId,
'IMapRawData.events',
priority
);
return false;
}
if (typeof id !== 'string') {
logger.error(64, 'event id', raw.floorId);
return false;
}
}
}
}
if (length % raw.width !== 0) {
logger.error(
61,
length.toString(),
raw.width.toString(),
raw.floorId
);
return false;
}
for (const zIndex of Object.keys(rawEvents)) {
if (!this.isNumericKey(zIndex) || !Object.hasOwn(rawMap, zIndex)) {
logger.error(64, 'event layer', raw.floorId);
return false;
}
}
return true;
}
//#region 楼层管理
fromRaw(raw: IMapRawData): IGameMap | null {
if (!this.validateRaw(raw)) return null;
let length = 0;
const entries = Object.entries(raw.map);
for (const [_, map] of Object.entries(raw.map)) {
@ -246,6 +99,7 @@ export class MapState implements IMapState {
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);
@ -262,9 +116,10 @@ export class MapState implements IMapState {
return state;
}
@shouldReplay('Setting map list should be replayed.')
setMapList(maps: string[]): void {
this.maps.length = 0;
this.maps.push(...uniq(maps));
this.maps.push(...maps);
}
getMap(id: string): IGameMap | null {
@ -285,6 +140,7 @@ export class MapState implements IMapState {
this.areaList = areas;
}
@shouldReplay('Activating area should be replayed.')
activeArea(id: string): void {
const idx = this.maps.indexOf(id);
if (idx === -1) return;
@ -293,6 +149,7 @@ export class MapState implements IMapState {
this.setAreaActive(area, true);
}
@shouldReplay('Deactivating area should be replayed.')
deactiveArea(id: string): void {
const idx = this.maps.indexOf(id);
if (idx === -1) return;
@ -357,6 +214,7 @@ export class MapState implements IMapState {
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);
}

View File

@ -3,7 +3,8 @@ import {
getFaceMovement,
ObjectMover,
ObjectMoveStep,
ObjectMoveType
ObjectMoveType,
shouldReplay
} from '@user/data-common';
import { IDynamicTile } from './types';
import { DYNAMIC_MOVER_FACE } from '../shared';
@ -31,6 +32,7 @@ export class DynamicTileMover extends ObjectMover<IDynamicTile> {
return Promise.resolve(DynamicMoveCode.Success);
}
@shouldReplay('Dynamic tile moving step should be replayed.')
protected onStepEnd(
code: number,
step: ObjectMoveStep,

View File

@ -1,4 +1,4 @@
import { ITileRawData } from '@user/data-common';
import { ITileRawData, shouldReplay } from '@user/data-common';
import {
IDynamicTile,
IMapLayer,
@ -24,11 +24,13 @@ export class StaticTile
return this.state.tileStore.getData(this.num());
}
@shouldReplay('Setting static tile num should be replayed.')
set(num: number): void {
this.layer.setBlock(num, this.locator.x, this.locator.y);
this.restoreDefaultEvents();
}
@shouldReplay('Transfering static tile to dynamic should be replayed.')
toDynamic(): IDynamicTile {
return this.layer.transferToDynamic(this.locator.x, this.locator.y)!;
}

View File

@ -4,7 +4,8 @@ import {
IDataCommon,
IDataCommonExtended,
ISaveableContent,
ITileRawData
ITileRawData,
shouldReplay
} from '@user/data-common';
import { LayerEventView } from './eventView';
import {
@ -52,6 +53,7 @@ export abstract class MapTileBase<TSave extends IMapBlockSaveBase>
eventView.markPure();
}
@shouldReplay('Setting map tile face direction should be replayed.')
setFaceDirection(direction: FaceDirection): number {
const cur = this.num();
const next = this.layer.faceBinder.getFaceOf(cur, direction);

View File

@ -23,6 +23,7 @@ import {
IReadonlyEnemy,
IStateBase
} from '@user/data-base';
import { shouldReplay } from '@user/data-common';
export class CombatFlow<TEnemy, THero>
extends Hookable<ICombatFlowHooks<TEnemy, THero>>
@ -193,6 +194,7 @@ export class CombatFlow<TEnemy, THero>
return damage;
}
@shouldReplay('Battle with enemy should be replayed.')
battle(
enemy: IEnemyView<TEnemy>
): Promise<IEnemyDamageInfo<TEnemy, THero> | null> {
@ -214,6 +216,7 @@ export class CombatFlow<TEnemy, THero>
return this.combatFlow(handler, eHandler);
}
@shouldReplay('Battle with enemy should be replayed.')
battleComputed(
enemy: IReadonlyEnemy<TEnemy>
): Promise<IEnemyDamageInfo<TEnemy, THero> | null> {

View File

@ -1,5 +1,5 @@
import { logger } from '@motajs/common';
import { IGameEventStore, ignoreReplay } from '@user/data-common';
import { IGameEventStore, shouldReplay } from '@user/data-common';
import { AnonTokyoInterpreter } from '@motajs/anon-tokyo';
import {
EventExecuteMode,
@ -52,8 +52,7 @@ export class EventExecutor implements IGameEventExecutor {
}
}
// @ts-expect-error 泛型无法推导
@ignoreReplay('事件一般由其他动作被动触发,不应计入录像')
@shouldReplay('Executing event should be replayed.')
async execute<R = void>(
events: IGameEventInvocation[],
param: IBlockEventParam

View File

@ -1,5 +1,5 @@
import { ITileLocator, logger } from '@motajs/common';
import { IObjectMovable, IObjectMover } from '@user/data-common';
import { IObjectMovable, IObjectMover, shouldReplay } from '@user/data-common';
import { IStateBase } from '@user/data-base';
import { PathFinder } from './finder';
import {
@ -79,12 +79,14 @@ export class PathfindingSystem implements IPathfindingSystem {
return result;
}
@shouldReplay('Moving hero by path finding system should be replayed.')
moveTo(target: ITileLocator): IPathfindingController | null {
const path = this.getPath(target);
if (path.status !== PathfindingStatus.Success) return null;
return this.startMove(path, false);
}
@shouldReplay('Teleporting hero by path finding system should be replayed.')
teleportTo(target: ITileLocator): IPathfindingController | null {
const path = this.getPath(target);
if (path.status !== PathfindingStatus.Success) return null;