mirror of
https://github.com/motajs/template.git
synced 2026-09-27 13:40:18 +08:00
test(06-07): add small-map replay playback and second-recording comparison
- play a recorded small-map route with bounded waiting and recording disabled - assert a second recording equals the first step-by-step (code and param type/value) - trigger warn 176 and errors 2001-2008 through the real replay commands - append 06-07 code coverage map rows and record finding #06-07-1
This commit is contained in:
parent
edd4de34dc
commit
9f9dd11223
@ -85,6 +85,15 @@
|
||||
| 124 | map/saveLoad.test.ts | `warns code 124 when the compression reference is missing` | 06-09 |
|
||||
| 177 | data-state/test/saveablesRoundTrip.test.ts | `warns code 177 when the save data misses a saveable key` | 06-09 |
|
||||
| 178 | data-state/test/saveablesRoundTrip.test.ts | `warns code 178 when the save data misses a saveable key`(文案相反的正确预期见 `it.skip`,`#06-09-5`) | 06-09 |
|
||||
| 176 | hero/mover.ts | `warns 176 when the hero move direction is not orthogonal` | 06-07 |
|
||||
| 2001 | data-state/src/replay/commands.ts | `warns 2001 and 2002 for parameter count and type mismatches` | 06-07 |
|
||||
| 2002 | data-state/src/replay/commands.ts | `warns 2001 and 2002 for parameter count and type mismatches` | 06-07 |
|
||||
| 2003 | data-state/src/replay/commands.ts | `warns 2003 and 2004 for a moving hero and a missing controller` | 06-07 |
|
||||
| 2004 | data-state/src/replay/commands.ts | `warns 2003 and 2004 for a moving hero and a missing controller` | 06-07 |
|
||||
| 2005 | data-state/src/replay/commands.ts | `warns 2005 when teleport finds no path` | 06-07 |
|
||||
| 2006 | data-state/src/replay/commands.ts | `warns 2006 when using an item fails` | 06-07 |
|
||||
| 2007 | data-state/src/replay/commands.ts | `warns 2007 when equip fails to occupy the slot` | 06-07 |
|
||||
| 2008 | data-state/src/replay/commands.ts | `warns 2008 when unequip leaves the slot occupied` | 06-07 |
|
||||
|
||||
## 06-01 战斗系统(packages-user/data-system/src/combat)
|
||||
|
||||
@ -238,3 +247,24 @@ D-45:`@system/replay` 须已注册(执行前 Task 1 门禁确认);未注
|
||||
执行结果:6 个测试文件 41 通过 / 6 跳过(skip 均为 `#06-09-1..5` 的正确预期疑似缺陷,经临时取消
|
||||
skip 验证确为真实失败);本计划可达码 55/58/59/112/113/119/120/122/124/177/178 全部有触发断言。
|
||||
`pnpm test:ci` 存在**先于本计划**的既有失败(`#06-09` 阻断项),本计划未新增失败。
|
||||
|
||||
## 06-07 顶层集成(伤害组合 + 录像完整播放 + 二次录制比对)
|
||||
|
||||
模块归属:176 → `data-base/src/hero/mover.ts`(`HeroMover.onStepStart` 方向步记录时非
|
||||
上/右/下/左 的 `default` 分支);2001–2008 → `data-state/src/replay/commands.ts`
|
||||
(`BaseReplayCommand.assertParameter` 参数数量/类型校验、`ReplayMoveCommand`、`ReplayTeleportCommand`、
|
||||
`ReplayUseItemCommand`、`ReplayEquipCommand`、`ReplayUnequipCommand` 的失败分支)。
|
||||
|
||||
阶段 1(构件级)完成真实 API 下的单特殊属性伤害基线与单光环(`CommonAura`/`GuardAura`)基线;
|
||||
阶段 2(组合/流水线)完成顶层多特殊属性伤害组合、系统层光环基础/特殊效果与常规/特殊查询组合、
|
||||
final-effect 阶段顺序、同/跨优先级顺序(D-25),以及属性流水线结果经缓存伤害系统的
|
||||
`markDirty`/`deleteEnemy`/`with(hero)` 联动(D-26);
|
||||
阶段 3(完整/集成)完成真实小地图场景的录像录制与完整播放、重置录像后的**二次录制逐条比对**
|
||||
(步数 + 每步 code + 各 param 的 type/value),并观测 176 与 2001–2008。
|
||||
|
||||
D-32:仅用 `ReplaySystem.saveState()`/`loadState()`(`IReplaySystemSave`)做录像重置的最小使用,
|
||||
不测存读档本身;`ReplayArray` 不可存档(无 `saveState`/`loadState`),已由用例断言。
|
||||
D-40:真实英雄操作经 `replay.route.add(...)`(mover 方向步)录制;播放期间经
|
||||
`replaySystem.disable()`/`revert()` 抑制录制;准备步骤同样以 disable/revert 包裹。
|
||||
一条疑似缺陷 `#06-07-1`(`CoreState` 未向寻路 `finder` 注入地图状态/事件层/通行谓词,
|
||||
顶层录像瞬移恒返回 2005)按 D-05 以 `it.skip` 的正确预期用例登记,详见 `06-TEST-FINDINGS.md`。
|
||||
|
||||
@ -149,3 +149,21 @@ D-45:公开 `CoreState.saveState(compression)` / `loadState(state, compression
|
||||
**建议修复方向**:由用户在该提交归属的计划中补齐上述 4 个 `data-base` 测试的假 `replaySystem`
|
||||
(或为生产侧 `this.state.replaySystem` 增加空值守卫),随后重跑 `pnpm test:ci` 应可全绿。
|
||||
**严重度**:高(阻塞 06-09 的 D-44(c) 提交门禁与后续完整里程碑验证)。
|
||||
|
||||
## #06-07 顶层集成(伤害组合 + 录像完整播放 + 二次录制比对)
|
||||
|
||||
本计划按 D-43 三阶段(构件 → 组合/流水线 → 完整/集成)执行:阶段 1 真实 API 单特殊属性/单光环
|
||||
基线;阶段 2 顶层多特殊属性伤害组合 + 系统层光环/效果组合(含 final-effect 阶段顺序、同/跨优先级
|
||||
顺序与属性→伤害联动);阶段 3 真实小地图录像播放 + 二次录制逐条比对 + 176 + 2001–2008。
|
||||
2 个测试文件共 **24 通过 / 1 跳过**(skip 为本计划新增 `#06-07-1`),`pnpm test:ci` 全绿
|
||||
(66 文件 / 629 通过 / 20 跳过)。此前 `#06-09` 记录的既有 test:ci 回归已由后续提交
|
||||
(`2ff422a` / `c08f3f8`)修复,D-44(c) 现可全绿。
|
||||
发现 1 处疑似缺陷(`#06-07-1`),按 D-05 以正确预期的 `it.skip` 用例登记。
|
||||
|
||||
| 模块/接口 | 现象 | 最小复现 | 疑似原因 | 影响面 | 建议修复方向 | 关联 skip 用例 | 严重度 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `data-system/src/path/system.ts` `PathfindingSystem` / `PathfindingFinder` | 顶层 `CoreState` 上 `pathfinding.teleportTo` 恒返回 `null` 并告警 173,录像瞬移指令因此必发错误 2005 | `createCoreState()` → 构造小地图场景(不手工注入 finder)→ `state.pathfinding.teleportTo({ x: 1, y: 0 })` 返回 `null`;`logger` 先观测到 173,随后 `ReplayTeleportCommand` 发 2005,二次录像中的瞬移步无法播放 | `CoreState` 构造 `new PathfindingSystem(this)` 后仅调用 `useMover`,从未向 `finder` 注入 `useMapState`/`useMapLayer`/`usePassPredicate`;`PathfindingFinder.find` 在 `maps` 或 `layer` 为 `null` 时告警 173 并返回空数组 | 顶层录像中的瞬移指令无法播放(必失败);未手工注入 finder 时真实寻路入口不可用 | 在地图/事件层可用后(如 `initMapState` 或 `loaded` 钩子)为 `pathfinding.finder` 注入 `maps`、`eventLayer` 与 `DefaultPassPredicateImpl`;若设计上由客户端层注入,则应在契约中明确,避免录像瞬移静默失败 | `replayPlayback.test.ts` `plays a teleport step without manual finder wiring`(`it.skip`,修复后取消 skip) | 中 |
|
||||
|
||||
> 说明:本计划的录像播放用例为覆盖「真实寻路瞬移播放」路径,在**测试内**通过公开的
|
||||
> `state.pathfinding.finder.useMapState/useMapLayer/usePassPredicate` 完成注入;生产 `CoreState`
|
||||
> 目前不做该注入,故另立 `#06-07-1` 记录该接线缺口。
|
||||
|
||||
497
packages-user/data-state/test/replayPlayback.test.ts
Normal file
497
packages-user/data-state/test/replayPlayback.test.ts
Normal file
@ -0,0 +1,497 @@
|
||||
// 测试录像完整播放与二次录制比对:小地图场景、逐条相等判定、176 与 error 2001-2008
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { logger } from '@motajs/common';
|
||||
import {
|
||||
FaceDirection,
|
||||
ReplayCommandCode,
|
||||
SaveCompression,
|
||||
TileType,
|
||||
type IReplayArray,
|
||||
type IReplaySandbox,
|
||||
type IReplayStepHandler,
|
||||
type ReplayParamValue
|
||||
} from '@user/data-common';
|
||||
import { type CoreState, createCoreState } from '../src/core';
|
||||
import { DefaultPassPredicateImpl } from '../src/hero/predicate';
|
||||
import {
|
||||
ReplayEquipCommand,
|
||||
ReplayMoveCommand,
|
||||
ReplayTeleportCommand,
|
||||
ReplayUnequipCommand,
|
||||
ReplayUseItemCommand
|
||||
} from '../src/replay/commands';
|
||||
|
||||
vi.hoisted(() => {
|
||||
Map.prototype.getOrInsert ??= function <K, V>(
|
||||
this: Map<K, V>,
|
||||
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 <K, V>(
|
||||
this: Map<K, V>,
|
||||
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;
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
interface IRecordedParam {
|
||||
/** 参数运行时类型 */
|
||||
readonly type: string;
|
||||
/** 参数值 */
|
||||
readonly value: ReplayParamValue;
|
||||
}
|
||||
|
||||
interface IRecordedStep {
|
||||
/** 录像指令码 */
|
||||
readonly code: number;
|
||||
/** 本条录像步的参数 */
|
||||
readonly params: readonly IRecordedParam[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 在录像记录被禁用的状态下执行准备动作,避免准备步骤被误录
|
||||
* @param state 顶层状态对象
|
||||
* @param action 需要执行的准备动作
|
||||
*/
|
||||
function withReplayDisabled<T>(state: CoreState, action: () => T): T {
|
||||
state.replaySystem.disable();
|
||||
try {
|
||||
return action();
|
||||
} finally {
|
||||
state.replaySystem.revert();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用真实地图与勇士接口构造一个 3x3 小地图场景
|
||||
* @param state 顶层状态对象
|
||||
* @param wireFinder 是否向寻路 finder 手动注入地图状态、事件层与通行谓词
|
||||
*/
|
||||
function createSmallMapScene(state: CoreState, wireFinder: boolean = true) {
|
||||
state.tileStore.addTile({
|
||||
num: 1,
|
||||
id: 'floor',
|
||||
events: {},
|
||||
type: TileType.Terrain,
|
||||
pass: { onlyEvents: false, outPass: 0b1111, inPass: 0b1111 },
|
||||
eventPass: true
|
||||
});
|
||||
const map = state.maps.fromRaw({
|
||||
floorId: 'F1',
|
||||
width: 3,
|
||||
map: {
|
||||
0: [1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
20: [1, 1, 1, 1, 1, 1, 1, 1, 1]
|
||||
},
|
||||
layerAlias: { 0: 'bg', 20: 'event' },
|
||||
events: { 0: {}, 20: {} }
|
||||
});
|
||||
if (!map || !map.eventLayer) {
|
||||
throw new Error('small map scene was not created');
|
||||
}
|
||||
state.maps.setMapActiveStatus('F1', true);
|
||||
if (wireFinder) {
|
||||
state.pathfinding.finder.useMapState(state.maps);
|
||||
state.pathfinding.finder.useMapLayer(map.eventLayer);
|
||||
state.pathfinding.finder.usePassPredicate(
|
||||
new DefaultPassPredicateImpl(state.maps)
|
||||
);
|
||||
}
|
||||
resetHero(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把勇士恢复到小地图起点并清空待执行的移动队列
|
||||
* @param state 顶层状态对象
|
||||
*/
|
||||
function resetHero(state: CoreState): void {
|
||||
state.hero.location.setFloor('F1');
|
||||
state.hero.location.setPos(0, 0);
|
||||
state.hero.location.mover.clear();
|
||||
state.hero.location.mover.setFaceDir(FaceDirection.Right);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过真实勇士移动器执行一个方向步并等待其完成,从而录制一步录像
|
||||
* @param state 顶层状态对象
|
||||
* @param direction 移动方向
|
||||
*/
|
||||
async function runHeroStep(
|
||||
state: CoreState,
|
||||
direction: FaceDirection
|
||||
): Promise<void> {
|
||||
const controller = state.hero.location.mover.step(direction).start();
|
||||
if (!controller) throw new Error('hero mover was already running');
|
||||
await controller.onEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* 有界等待录像沙箱播放结束,避免非终止录像挂起测试
|
||||
* @param sandbox 录像沙箱
|
||||
*/
|
||||
async function waitForEnded(sandbox: IReplaySandbox): Promise<void> {
|
||||
for (let index = 0; index < 1000 && !sandbox.ended; index++) {
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
if (!sandbox.ended) throw new Error('replay sandbox did not end');
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放当前录像并等待结束,播放期间禁用录制以免影响录像本身
|
||||
* @param state 顶层状态对象
|
||||
*/
|
||||
async function playRoute(state: CoreState): Promise<void> {
|
||||
const replay = state.replaySystem;
|
||||
const sandbox = replay.createReplaySandbox({
|
||||
route: replay.route,
|
||||
reseter: { reset: () => resetHero(state) }
|
||||
});
|
||||
replay.disable();
|
||||
try {
|
||||
sandbox.play();
|
||||
await waitForEnded(sandbox);
|
||||
} finally {
|
||||
replay.revert();
|
||||
replay.releaseSandbox();
|
||||
}
|
||||
expect(sandbox.ended).toBe(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把录像数组快照为可逐条比较的纯数据(步数 + code + 各参数的 type/value)
|
||||
* @param route 录像数组
|
||||
*/
|
||||
function snapshotRoute(route: IReplayArray): IRecordedStep[] {
|
||||
const result: IRecordedStep[] = [];
|
||||
for (let index = 0; index < route.length; index++) {
|
||||
const step = route.get(index);
|
||||
result.push({
|
||||
code: step.command,
|
||||
params: step.params.map(value => ({
|
||||
type: typeof value,
|
||||
value
|
||||
}))
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐条断言两份录像完全相等(步数 + 每步 code + 各参数的 type/value)
|
||||
* @param actual 二次录制的录像
|
||||
* @param expected 首次录制的录像
|
||||
*/
|
||||
function expectReplayEqual(
|
||||
actual: readonly IRecordedStep[],
|
||||
expected: readonly IRecordedStep[]
|
||||
): void {
|
||||
expect(actual.length).toBe(expected.length);
|
||||
for (let index = 0; index < expected.length; index++) {
|
||||
expect(actual[index].code).toBe(expected[index].code);
|
||||
expect(actual[index].params.length).toBe(expected[index].params.length);
|
||||
for (let param = 0; param < expected[index].params.length; param++) {
|
||||
expect(actual[index].params[param].type).toBe(
|
||||
expected[index].params[param].type
|
||||
);
|
||||
expect(actual[index].params[param].value).toBe(
|
||||
expected[index].params[param].value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装一个手动触发使用的录像步信息对象
|
||||
* @param command 指令码
|
||||
* @param params 参数列表
|
||||
*/
|
||||
function step(
|
||||
command: number,
|
||||
params: IReplayStepHandler['params']
|
||||
): IReplayStepHandler {
|
||||
return { command, params, index: 0 };
|
||||
}
|
||||
|
||||
describe('replay recording and route read-back', () => {
|
||||
// 验证禁用录像时任何记录都不生效,嵌套 revert 全部恢复后才重新记录
|
||||
it('suppresses recording while disabled and resumes after all reverts', () => {
|
||||
const state = createCoreState();
|
||||
const replay = state.replaySystem;
|
||||
|
||||
replay.disable();
|
||||
replay.record(ReplayCommandCode.Right);
|
||||
expect(replay.route.length).toBe(0);
|
||||
|
||||
replay.disable();
|
||||
replay.revert();
|
||||
replay.record(ReplayCommandCode.Right);
|
||||
expect(replay.route.length).toBe(0);
|
||||
|
||||
replay.revert();
|
||||
replay.record(ReplayCommandCode.Right);
|
||||
expect(replay.route.length).toBe(1);
|
||||
});
|
||||
|
||||
// 验证八个稳定指令码及其参数都能经录像数组逐条读回
|
||||
it('reads back every stable command code and its params', () => {
|
||||
const state = createCoreState();
|
||||
const replay = state.replaySystem;
|
||||
replay.record(ReplayCommandCode.Up);
|
||||
replay.record(ReplayCommandCode.Teleport, 3, 4);
|
||||
replay.record(ReplayCommandCode.UseItem, 12);
|
||||
replay.record(ReplayCommandCode.Equip, 99, 1, true);
|
||||
replay.record(ReplayCommandCode.Unequip, 1);
|
||||
|
||||
expect(replay.route.length).toBe(5);
|
||||
expect(replay.route.get(0)).toMatchObject({
|
||||
command: ReplayCommandCode.Up,
|
||||
params: []
|
||||
});
|
||||
expect(replay.route.get(1)).toMatchObject({
|
||||
command: ReplayCommandCode.Teleport,
|
||||
params: [3, 4]
|
||||
});
|
||||
expect(replay.route.get(2)).toMatchObject({
|
||||
command: ReplayCommandCode.UseItem,
|
||||
params: [12]
|
||||
});
|
||||
expect(replay.route.get(3)).toMatchObject({
|
||||
command: ReplayCommandCode.Equip,
|
||||
params: [99, 1, true]
|
||||
});
|
||||
expect(replay.route.get(4)).toMatchObject({
|
||||
command: ReplayCommandCode.Unequip,
|
||||
params: [1]
|
||||
});
|
||||
});
|
||||
|
||||
// 验证真实勇士移动器按方向记录对应移动指令,且准备步骤不会被误录
|
||||
it('records hero moves through the real mover and ignores preparation', async () => {
|
||||
const state = createCoreState();
|
||||
|
||||
withReplayDisabled(state, () => createSmallMapScene(state));
|
||||
expect(state.replaySystem.route.length).toBe(0);
|
||||
|
||||
await runHeroStep(state, FaceDirection.Right);
|
||||
|
||||
expect(state.replaySystem.route.length).toBe(1);
|
||||
expect(state.replaySystem.route.get(0)).toMatchObject({
|
||||
command: ReplayCommandCode.Right,
|
||||
params: []
|
||||
});
|
||||
});
|
||||
|
||||
// 验证勇士移动方向不属于上右下左时记录告警 176
|
||||
it('warns 176 when the hero move direction is not orthogonal', async () => {
|
||||
const state = createCoreState();
|
||||
withReplayDisabled(state, () => createSmallMapScene(state));
|
||||
const warning = vi.spyOn(logger, 'warn');
|
||||
|
||||
const controller = state.hero.location.mover
|
||||
.step(FaceDirection.Unknown)
|
||||
.start();
|
||||
if (controller) await controller.onEnd;
|
||||
|
||||
expect(warning).toHaveBeenCalledWith(176);
|
||||
});
|
||||
});
|
||||
|
||||
describe('small-map replay playback and second recording', () => {
|
||||
// 验证小地图场景录制后正常播放,并在重置录像后二次录制与原录像逐条完全相等
|
||||
it('plays a recorded route and reproduces it exactly on a second recording', async () => {
|
||||
const state = createCoreState();
|
||||
const replay = state.replaySystem;
|
||||
const emptyReplay = replay.saveState(SaveCompression.NoCompression);
|
||||
withReplayDisabled(state, () => createSmallMapScene(state));
|
||||
|
||||
await runHeroStep(state, FaceDirection.Right);
|
||||
await runHeroStep(state, FaceDirection.Right);
|
||||
replay.record(ReplayCommandCode.Teleport, 1, 0);
|
||||
replay.record(ReplayCommandCode.Up);
|
||||
|
||||
const firstSteps = snapshotRoute(replay.route);
|
||||
expect(firstSteps.map(item => item.code)).toEqual([
|
||||
ReplayCommandCode.Right,
|
||||
ReplayCommandCode.Right,
|
||||
ReplayCommandCode.Teleport,
|
||||
ReplayCommandCode.Up
|
||||
]);
|
||||
expect(replay.route.get(2)).toMatchObject({
|
||||
command: ReplayCommandCode.Teleport,
|
||||
params: [1, 0]
|
||||
});
|
||||
|
||||
await playRoute(state);
|
||||
expect(state.hero.location.x).toBe(1);
|
||||
expect(state.hero.location.y).toBe(0);
|
||||
|
||||
replay.loadState(emptyReplay, SaveCompression.NoCompression);
|
||||
expect(replay.route.length).toBe(0);
|
||||
withReplayDisabled(state, () => resetHero(state));
|
||||
|
||||
await runHeroStep(state, FaceDirection.Right);
|
||||
await runHeroStep(state, FaceDirection.Right);
|
||||
replay.record(ReplayCommandCode.Teleport, 1, 0);
|
||||
replay.record(ReplayCommandCode.Up);
|
||||
|
||||
const secondSteps = snapshotRoute(replay.route);
|
||||
expectReplayEqual(secondSteps, firstSteps);
|
||||
|
||||
await playRoute(state);
|
||||
expect(state.hero.location.x).toBe(1);
|
||||
expect(state.hero.location.y).toBe(0);
|
||||
});
|
||||
|
||||
// 疑似缺陷 #06-07-1:CoreState 未向寻路 finder 注入地图状态/事件层/通行谓词,
|
||||
// 顶层录像瞬移恒返回 2005;本用例按正确预期编写,修复后取消 skip
|
||||
it.skip('plays a teleport step without manual finder wiring', async () => {
|
||||
const state = createCoreState();
|
||||
const replay = state.replaySystem;
|
||||
withReplayDisabled(state, () => createSmallMapScene(state, false));
|
||||
await runHeroStep(state, FaceDirection.Right);
|
||||
replay.record(ReplayCommandCode.Teleport, 1, 0);
|
||||
|
||||
await playRoute(state);
|
||||
|
||||
expect(state.hero.location.x).toBe(1);
|
||||
expect(state.hero.location.y).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replay playback error codes 2001-2008', () => {
|
||||
// 验证参数数量与参数类型不匹配分别记录错误码 2001 与 2002
|
||||
it('warns 2001 and 2002 for parameter count and type mismatches', async () => {
|
||||
const state = createCoreState();
|
||||
const error = vi.spyOn(logger, 'error');
|
||||
const move = new ReplayMoveCommand(state, FaceDirection.Up);
|
||||
const teleport = new ReplayTeleportCommand(state);
|
||||
|
||||
await expect(
|
||||
move.execute(step(ReplayCommandCode.Up, [1]))
|
||||
).resolves.toBe(false);
|
||||
expect(error).toHaveBeenCalledWith(2001, 'move', '0', '1');
|
||||
|
||||
await expect(
|
||||
teleport.execute(step(ReplayCommandCode.Teleport, ['x', 1]))
|
||||
).resolves.toBe(false);
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
2002,
|
||||
'teleport',
|
||||
'0',
|
||||
'number',
|
||||
'string'
|
||||
);
|
||||
});
|
||||
|
||||
// 验证移动已在进行中与移动控制器缺失分别记录错误码 2003 与 2004
|
||||
it('warns 2003 and 2004 for a moving hero and a missing controller', async () => {
|
||||
const state = createCoreState();
|
||||
const error = vi.spyOn(logger, 'error');
|
||||
const mover = state.hero.location.mover;
|
||||
const move = new ReplayMoveCommand(state, FaceDirection.Up);
|
||||
(mover as unknown as { moving: boolean }).moving = true;
|
||||
|
||||
await expect(
|
||||
move.execute(step(ReplayCommandCode.Up, []))
|
||||
).resolves.toBe(false);
|
||||
expect(error).toHaveBeenCalledWith(2003);
|
||||
|
||||
(mover as unknown as { moving: boolean }).moving = false;
|
||||
vi.spyOn(mover, 'start').mockReturnValueOnce(null);
|
||||
await expect(move.notExecuted()).resolves.toBe(false);
|
||||
expect(error).toHaveBeenCalledWith(2004);
|
||||
});
|
||||
|
||||
// 验证瞬移无路径时记录错误码 2005
|
||||
it('warns 2005 when teleport finds no path', async () => {
|
||||
const state = createCoreState();
|
||||
const error = vi.spyOn(logger, 'error');
|
||||
vi.spyOn(state.pathfinding, 'teleportTo').mockReturnValueOnce(null);
|
||||
const teleport = new ReplayTeleportCommand(state);
|
||||
|
||||
await expect(
|
||||
teleport.execute(step(ReplayCommandCode.Teleport, [4, 5]))
|
||||
).resolves.toBe(false);
|
||||
expect(error).toHaveBeenCalledWith(2005, '4', '5');
|
||||
});
|
||||
|
||||
// 验证使用物品失败时记录错误码 2006
|
||||
it('warns 2006 when using an item fails', async () => {
|
||||
const state = createCoreState();
|
||||
const error = vi.spyOn(logger, 'error');
|
||||
vi.spyOn(state.hero.items, 'useItem').mockReturnValueOnce(false);
|
||||
const useItem = new ReplayUseItemCommand(state);
|
||||
|
||||
await expect(
|
||||
useItem.execute(step(ReplayCommandCode.UseItem, [34]))
|
||||
).resolves.toBe(false);
|
||||
expect(error).toHaveBeenCalledWith(2006, '34');
|
||||
});
|
||||
|
||||
// 验证装备未落到指定槽位时记录错误码 2007
|
||||
it('warns 2007 when equip fails to occupy the slot', async () => {
|
||||
const state = createCoreState();
|
||||
const error = vi.spyOn(logger, 'error');
|
||||
const equipment = state.hero.equip;
|
||||
vi.spyOn(equipment, 'equip').mockImplementation(() => undefined);
|
||||
vi.spyOn(equipment, 'getEquipped').mockReturnValueOnce(undefined);
|
||||
const equip = new ReplayEquipCommand(state);
|
||||
|
||||
await expect(
|
||||
equip.execute(step(ReplayCommandCode.Equip, [99, 1, false]))
|
||||
).resolves.toBe(false);
|
||||
expect(error).toHaveBeenCalledWith(2007, '99', '1');
|
||||
});
|
||||
|
||||
// 验证卸下后槽位仍有装备时记录错误码 2008
|
||||
it('warns 2008 when unequip leaves the slot occupied', async () => {
|
||||
const state = createCoreState();
|
||||
const error = vi.spyOn(logger, 'error');
|
||||
const equipment = state.hero.equip;
|
||||
vi.spyOn(equipment, 'unequip').mockImplementation(() => undefined);
|
||||
vi.spyOn(equipment, 'getEquipped').mockReturnValueOnce(88);
|
||||
const unequip = new ReplayUnequipCommand(state);
|
||||
|
||||
await expect(
|
||||
unequip.execute(step(ReplayCommandCode.Unequip, [1]))
|
||||
).resolves.toBe(false);
|
||||
expect(error).toHaveBeenCalledWith(2008, '1');
|
||||
});
|
||||
|
||||
// 验证录像不可存档属主为 ReplaySystem,ReplayArray 不再提供 saveState/loadState
|
||||
it('keeps the saveable owner on ReplaySystem instead of ReplayArray', () => {
|
||||
const state = createCoreState();
|
||||
const route = state.replaySystem.route;
|
||||
const save = state.replaySystem.saveState(
|
||||
SaveCompression.NoCompression
|
||||
);
|
||||
|
||||
expect(save).toMatchObject({
|
||||
length: 0,
|
||||
commandWidth: route.commandWidth,
|
||||
commandArray: expect.any(ArrayBuffer),
|
||||
paramArray: expect.any(ArrayBuffer)
|
||||
});
|
||||
expect(
|
||||
(route as unknown as Record<string, unknown>).saveState
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
(route as unknown as Record<string, unknown>).loadState
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user