mirror of
https://github.com/motajs/template.git
synced 2026-10-03 17:40:19 +08:00
feat: 录像记录 & CoreState 存储接口
This commit is contained in:
parent
5470f7a90e
commit
cee843981f
@ -1 +1 @@
|
||||
{"isolation":"harness-worktree","harness_flag":"isolation=\"worktree\"","phase":"03","plan":null,"written_at":1789138005150}
|
||||
{"isolation":"none","harness_flag":null,"phase":"06","plan":null,"written_at":1789346124877}
|
||||
@ -1,5 +1,5 @@
|
||||
import { isNil } from 'lodash-es';
|
||||
import { IDataCommon } from '@user/data-common';
|
||||
import { IDataCommon, ReplayCommandCode } from '@user/data-common';
|
||||
import {
|
||||
EquipStatus,
|
||||
IEquipmentState,
|
||||
@ -127,6 +127,11 @@ export class HeroEquipment<THero> implements IHeroEquipment<THero> {
|
||||
return void 0;
|
||||
}
|
||||
|
||||
// 记录录像
|
||||
// TODO: 可以考虑把禁用录像记录放到录像系统里面,避免每个可能由代码触发的行动都写一个 noRoute 参数
|
||||
const replay = this.state.replaySystem;
|
||||
replay.route.add(ReplayCommandCode.Equip, [uid]);
|
||||
|
||||
if (typeof slot === 'number') {
|
||||
// 数字槽位,直接进行指定替换
|
||||
const curr = this.equips.get(slot);
|
||||
@ -176,6 +181,10 @@ export class HeroEquipment<THero> implements IHeroEquipment<THero> {
|
||||
return void 0;
|
||||
}
|
||||
|
||||
// 记录录像
|
||||
const replay = this.state.replaySystem;
|
||||
replay.route.add(ReplayCommandCode.Unequip, [slot]);
|
||||
|
||||
this.unloadEquipEffect(state);
|
||||
this.equips.delete(slot);
|
||||
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { isNil } from 'lodash-es';
|
||||
import { IDataCommon, ItemCategory, SaveCompression } from '@user/data-common';
|
||||
import {
|
||||
IDataCommon,
|
||||
ItemCategory,
|
||||
ReplayCommandCode,
|
||||
SaveCompression
|
||||
} from '@user/data-common';
|
||||
import { HeroEquipsStore } from './equipStore';
|
||||
import {
|
||||
IHeroItems,
|
||||
@ -107,7 +112,7 @@ export class HeroItems<THero> implements IHeroItems<THero> {
|
||||
this.addItem(item, 1);
|
||||
}
|
||||
|
||||
useItem(item: number | string): boolean {
|
||||
useItem(item: number | string, noRoute: boolean = false): boolean {
|
||||
const state = this.internalGetItemState(item);
|
||||
if (!state) return false;
|
||||
|
||||
@ -121,6 +126,11 @@ export class HeroItems<THero> implements IHeroItems<THero> {
|
||||
|
||||
if (!raw.effect.canUse(raw)) return false;
|
||||
|
||||
if (!noRoute) {
|
||||
const replay = this.state.replaySystem;
|
||||
replay.route.add(ReplayCommandCode.UseItem, [raw.num]);
|
||||
}
|
||||
|
||||
raw.effect.useEffect(raw);
|
||||
|
||||
if (raw.category === ItemCategory.Consumable) {
|
||||
|
||||
@ -6,7 +6,8 @@ import {
|
||||
ObjectMoveStep,
|
||||
ObjectMover,
|
||||
ObjectMoveType,
|
||||
IDataCommon
|
||||
IDataCommon,
|
||||
ReplayCommandCode
|
||||
} from '@user/data-common';
|
||||
import {
|
||||
HeroMoveCode,
|
||||
@ -177,6 +178,29 @@ export class HeroMover<T extends IHeroLocation>
|
||||
type === ObjectMoveType.DirFace ||
|
||||
type === ObjectMoveType.Special
|
||||
) {
|
||||
// 录像记录
|
||||
if (!this.noRoute) {
|
||||
const replay = this.state.replaySystem;
|
||||
switch (handler.direction) {
|
||||
case FaceDirection.Up:
|
||||
replay.route.add(ReplayCommandCode.Up, []);
|
||||
break;
|
||||
case FaceDirection.Right:
|
||||
replay.route.add(ReplayCommandCode.Right, []);
|
||||
break;
|
||||
case FaceDirection.Left:
|
||||
replay.route.add(ReplayCommandCode.Left, []);
|
||||
break;
|
||||
case FaceDirection.Down:
|
||||
replay.route.add(ReplayCommandCode.Down, []);
|
||||
break;
|
||||
default:
|
||||
logger.warn(176);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动判断
|
||||
if (this.ignoreTerrain) {
|
||||
return HeroMoveCode.Step;
|
||||
} else {
|
||||
|
||||
@ -562,9 +562,10 @@ export interface IHeroItems<THero>
|
||||
/**
|
||||
* 使用道具,仅对 Constant 与 Consumable 类型生效。Consumable 类型使用后数量减一。
|
||||
* @param item 道具图块数字或字符串 id
|
||||
* @param noRoute 是否不计入录像
|
||||
* @returns 道具是否使用成功
|
||||
*/
|
||||
useItem(item: number | string): boolean;
|
||||
useItem(item: number | string, noRoute?: boolean): boolean;
|
||||
|
||||
/**
|
||||
* 获取指定道具的持有数量
|
||||
|
||||
@ -1,5 +1,25 @@
|
||||
import { IHookable, IHookBase } from '@motajs/common';
|
||||
|
||||
/** 录像指令码,数值属于录像格式的一部分 */
|
||||
export const enum ReplayCommandCode {
|
||||
/** 向上移动一步 */
|
||||
Up,
|
||||
/** 向右移动一步 */
|
||||
Right,
|
||||
/** 向下移动一步 */
|
||||
Down,
|
||||
/** 向左移动一步 */
|
||||
Left,
|
||||
/** 自动寻路瞬移至目标点 */
|
||||
Teleport,
|
||||
/** 使用道具 */
|
||||
UseItem,
|
||||
/** 装备物品 */
|
||||
Equip,
|
||||
/** 卸下装备 */
|
||||
Unequip
|
||||
}
|
||||
|
||||
export type ReplayParamValue = number | string | boolean | bigint;
|
||||
|
||||
export interface IReplayStepHandler {
|
||||
|
||||
@ -2,6 +2,7 @@ import { IDirectionMapper, ITileLocator } from '@motajs/common';
|
||||
import { IFaceManager, IRoleFaceBinder } from './common';
|
||||
import { IGameEventStore, IItemStore, IMapStore, ITileStore } from './store';
|
||||
import { ISaveSystem } from './save';
|
||||
import { IReplaySystem } from './replay';
|
||||
|
||||
export interface IEnemyAttr {
|
||||
/** 怪物生命值 */
|
||||
@ -60,6 +61,8 @@ export interface IDataCommon {
|
||||
readonly directionMapper: IDirectionMapper;
|
||||
/** 存档系统 */
|
||||
readonly saveSystem: ISaveSystem;
|
||||
/** 录像系统 */
|
||||
readonly replaySystem: IReplaySystem;
|
||||
}
|
||||
|
||||
export interface IDataCommonExtended {
|
||||
|
||||
@ -24,7 +24,8 @@ import {
|
||||
IMapStore,
|
||||
MapStore,
|
||||
IReplaySystem,
|
||||
ReplaySystem
|
||||
ReplaySystem,
|
||||
ReplayCommandCode
|
||||
} from '@user/data-common';
|
||||
import {
|
||||
EnemyManager,
|
||||
@ -83,7 +84,6 @@ import { isNil } from 'lodash-es';
|
||||
import { DefaultHeroMoveTopImpl } from './hero';
|
||||
import { createEventRegistrations } from './event/registrations';
|
||||
import {
|
||||
ReplayCommandCode,
|
||||
ReplayEquipCommand,
|
||||
ReplayMoveCommand,
|
||||
ReplayTeleportCommand,
|
||||
@ -101,6 +101,7 @@ export class CoreState implements ICoreState {
|
||||
readonly mapStore: IMapStore;
|
||||
readonly eventStore: IGameEventStore;
|
||||
readonly directionMapper: IDirectionMapper;
|
||||
readonly replaySystem: IReplaySystem;
|
||||
|
||||
// Layer 1 数据层,所有可存档内容都在这,一般用于数据存储
|
||||
readonly maps: IMapState;
|
||||
@ -112,7 +113,6 @@ export class CoreState implements ICoreState {
|
||||
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
|
||||
readonly eventSystem: IGameEventSystem;
|
||||
readonly pathfinding: IPathfindingSystem;
|
||||
readonly replaySystem: IReplaySystem;
|
||||
|
||||
// Layer 3 用户层,也就是最顶层的内容,一般仅用于初始化以及仅供渲染端调用的顶层模块
|
||||
readonly loadProgress: ILoadProgressTotal;
|
||||
@ -505,6 +505,37 @@ export class CoreState implements ICoreState {
|
||||
}
|
||||
}
|
||||
|
||||
saveState(compression: SaveCompression): ReadonlyMap<string, unknown> {
|
||||
const result = new Map<string, unknown>();
|
||||
for (const [key, value] of this.saveables) {
|
||||
const content = value.saveState(compression);
|
||||
result.set(key, content);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
loadState(
|
||||
state: ReadonlyMap<string, unknown>,
|
||||
compression: SaveCompression
|
||||
): void {
|
||||
for (const [key, value] of this.saveables) {
|
||||
// 使用 has 判断是否在映射中,而非值的非空判断,因为空值也有可能是存档的一部分
|
||||
if (!state.has(key)) {
|
||||
logger.warn(177, key);
|
||||
continue;
|
||||
}
|
||||
const data = state.get(key);
|
||||
value.loadState(data, compression);
|
||||
}
|
||||
const loaded = new Set<string>(state.keys());
|
||||
const total = new Set(this.saveables.keys());
|
||||
const remain = total.difference(loaded);
|
||||
if (remain.size > 0) {
|
||||
const ids = [...remain].join(' | ');
|
||||
logger.warn(178, ids);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,8 @@ import {
|
||||
beginReplaySafetyCollection,
|
||||
endReplaySafetyCollection,
|
||||
logReplaySafetyDetail,
|
||||
shouldReplay
|
||||
shouldReplay,
|
||||
ReplayCommandCode
|
||||
} from '@user/data-common';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
@ -20,7 +21,18 @@ import {
|
||||
ReplayUnequipCommand,
|
||||
ReplayUseItemCommand
|
||||
} from './commands';
|
||||
import { ReplayCommandCode, REPLAY_COMMAND_ORDER } from './types';
|
||||
|
||||
/** 供测试读取的稳定指令码顺序 */
|
||||
export const REPLAY_COMMAND_ORDER: readonly ReplayCommandCode[] = [
|
||||
ReplayCommandCode.Up,
|
||||
ReplayCommandCode.Right,
|
||||
ReplayCommandCode.Down,
|
||||
ReplayCommandCode.Left,
|
||||
ReplayCommandCode.Teleport,
|
||||
ReplayCommandCode.UseItem,
|
||||
ReplayCommandCode.Equip,
|
||||
ReplayCommandCode.Unequip
|
||||
];
|
||||
|
||||
function step(
|
||||
command: number,
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from './commands';
|
||||
export * from './types';
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
/** 顶层拥有的稳定录像指令码,数值属于录像格式的一部分 */
|
||||
export const enum ReplayCommandCode {
|
||||
/** 向上移动一步 */
|
||||
Up = 0,
|
||||
/** 向右移动一步 */
|
||||
Right = 1,
|
||||
/** 向下移动一步 */
|
||||
Down = 2,
|
||||
/** 向左移动一步 */
|
||||
Left = 3,
|
||||
/** 自动寻路至目标点 */
|
||||
Teleport = 4,
|
||||
/** 使用道具 */
|
||||
UseItem = 5,
|
||||
/** 装备物品 */
|
||||
Equip = 6,
|
||||
/** 卸下装备 */
|
||||
Unequip = 7
|
||||
}
|
||||
|
||||
/** 供测试读取的稳定指令码顺序 */
|
||||
export const REPLAY_COMMAND_ORDER: readonly ReplayCommandCode[] = [
|
||||
ReplayCommandCode.Up,
|
||||
ReplayCommandCode.Right,
|
||||
ReplayCommandCode.Down,
|
||||
ReplayCommandCode.Left,
|
||||
ReplayCommandCode.Teleport,
|
||||
ReplayCommandCode.UseItem,
|
||||
ReplayCommandCode.Equip,
|
||||
ReplayCommandCode.Unequip
|
||||
];
|
||||
@ -12,7 +12,8 @@ export interface ISaveableExecutor<T> {
|
||||
afterLoad(data: T, state: IStateBase): void;
|
||||
}
|
||||
|
||||
export interface ICoreState extends IStateSystem {
|
||||
export interface ICoreState
|
||||
extends IStateSystem, ISaveableContent<ReadonlyMap<string, unknown>> {
|
||||
/** 加载进度对象 */
|
||||
readonly loadProgress: ILoadProgressTotal;
|
||||
/** 数据端加载对象 */
|
||||
|
||||
@ -4,15 +4,27 @@ import {
|
||||
IReplaySandbox,
|
||||
IHeroAttr,
|
||||
ReplaySystem,
|
||||
SaveCompression
|
||||
SaveCompression,
|
||||
ReplayCommandCode
|
||||
} from '@user/data-common';
|
||||
import { IEnemy, IReadonlyHeroAttribute } from '@user/data-base';
|
||||
import { IReadonlyEnemyHandler } from '@user/data-system';
|
||||
import { createCoreState, CoreState } from '../src/core';
|
||||
import { MainDamageCalculator } from '../src/enemy/calculator';
|
||||
import { REPLAY_COMMAND_ORDER, ReplayCommandCode } from '../src/replay/types';
|
||||
import { createClosedLoopFixture } from './fixtures/closed-loop';
|
||||
|
||||
/** 供测试读取的稳定指令码顺序 */
|
||||
export const REPLAY_COMMAND_ORDER: readonly ReplayCommandCode[] = [
|
||||
ReplayCommandCode.Up,
|
||||
ReplayCommandCode.Right,
|
||||
ReplayCommandCode.Down,
|
||||
ReplayCommandCode.Left,
|
||||
ReplayCommandCode.Teleport,
|
||||
ReplayCommandCode.UseItem,
|
||||
ReplayCommandCode.Equip,
|
||||
ReplayCommandCode.Unequip
|
||||
];
|
||||
|
||||
vi.hoisted(() => {
|
||||
Map.prototype.getOrInsert ??= function <K, V>(
|
||||
this: Map<K, V>,
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { ITileLocator, logger } from '@motajs/common';
|
||||
import { IObjectMovable, IObjectMover } from '@user/data-common';
|
||||
import { IStateBase } from '@user/data-base';
|
||||
import {
|
||||
IObjectMovable,
|
||||
IObjectMover,
|
||||
ReplayCommandCode
|
||||
} from '@user/data-common';
|
||||
import { HeroMover, IStateBase } from '@user/data-base';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { PathfindingFinder } from './finder';
|
||||
import {
|
||||
@ -95,6 +99,15 @@ export class PathfindingSystem implements IPathfindingSystem {
|
||||
if (!this.policy || this.policy(path)) {
|
||||
return this.startMove(path, false);
|
||||
} else {
|
||||
// 录像记录
|
||||
// TODO:后续需要调整设计方式,最好不用 instanceof
|
||||
if (this.mover instanceof HeroMover) {
|
||||
const replay = this.state.replaySystem;
|
||||
replay.route.add(ReplayCommandCode.Teleport, [
|
||||
target.x,
|
||||
target.y
|
||||
]);
|
||||
}
|
||||
return this.startMove(path, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { IStateBase } from '@user/data-base';
|
||||
import { IEnemyContext } from './combat';
|
||||
import { IEnemyAttr, IHeroAttr, IReplaySystem } from '@user/data-common';
|
||||
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
|
||||
import { IGameEventSystem } from './event';
|
||||
import { IPathfindingSystem } from './path';
|
||||
|
||||
@ -11,8 +11,6 @@ export interface IStateSystem extends IStateBase {
|
||||
readonly eventSystem: IGameEventSystem;
|
||||
/** 已绑定勇士移动器的寻路系统 */
|
||||
readonly pathfinding: IPathfindingSystem;
|
||||
/** 当前 CoreState 独立拥有的录像系统 */
|
||||
readonly replaySystem: IReplaySystem;
|
||||
}
|
||||
|
||||
export interface IStateSystemExtended {
|
||||
|
||||
@ -250,6 +250,9 @@
|
||||
"173": "Pathfinding input is invalid or a required binding (map state, map layer) is missing. An empty result will be returned.",
|
||||
"174": "Pathfinding cost function returned a negative or NaN value. Default cost 1 will be used instead.",
|
||||
"175": "Replay error: Replay not executed execution failed. Command code: $1.",
|
||||
"176": "Cannot add move step to replay system since only 'up,right,down,left' directions are acceptable.",
|
||||
"177": "Save data for saveable content $1 is needed, but there's no data in save data. Maybe a version issue for it? System will try to load other contents to make game run normally.",
|
||||
"178": "Save data with keys of '$1' are saved but not be loaded, is there some issue for it?",
|
||||
"1001": "Event(setBlock): Unknown tile '$1' when setting block."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user