mirror of
https://github.com/motajs/template.git
synced 2026-10-21 14:50:26 +08:00
fix(07-12): loadState 用 disable/revert 包裹重新装备(WR-06)
- HeroEquipment.loadState 在重新装备循环外包一层录像禁用窗口,try/finally 保证恢复 - 录像桩补禁用计数语义,读档期间写入的指令被丢弃 - 回归覆盖:读档不产生录像指令、禁用与恢复配对平衡、读档后 equip/unequip 记录仍保留
This commit is contained in:
parent
ab18aa3ec7
commit
d8fb6f088c
@ -6,6 +6,7 @@ import {
|
|||||||
type IItemRawData,
|
type IItemRawData,
|
||||||
ItemCategory,
|
ItemCategory,
|
||||||
ItemStore,
|
ItemStore,
|
||||||
|
ReplayCode,
|
||||||
TileStore,
|
TileStore,
|
||||||
TileType
|
TileType
|
||||||
} from '@user/data-common';
|
} from '@user/data-common';
|
||||||
@ -54,6 +55,46 @@ interface TestEnv {
|
|||||||
store: HeroEquipsStore<IHeroAttr>;
|
store: HeroEquipsStore<IHeroAttr>;
|
||||||
equipment: HeroEquipment<IHeroAttr>;
|
equipment: HeroEquipment<IHeroAttr>;
|
||||||
attribute: HeroAttribute<IHeroAttr>;
|
attribute: HeroAttribute<IHeroAttr>;
|
||||||
|
replaySystem: ReplaySystemStub;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 录像路由桩,只保留真正进入录像的指令,禁用窗口内的写入被丢弃 */
|
||||||
|
interface ReplayRouteStub {
|
||||||
|
/**
|
||||||
|
* 写入一条录像指令
|
||||||
|
* @param code 指令码
|
||||||
|
* @param params 指令参数
|
||||||
|
*/
|
||||||
|
add(code: ReplayCode, params: unknown[]): void;
|
||||||
|
/** 未被禁用窗口丢弃、真正进入录像的指令,按写入顺序排列 */
|
||||||
|
readonly commands: [ReplayCode, unknown[]][];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 录像系统桩,用禁用计数复现录像数组在禁用窗口内丢弃指令的行为 */
|
||||||
|
interface ReplaySystemStub {
|
||||||
|
route: ReplayRouteStub;
|
||||||
|
disable: ReturnType<typeof vi.fn>;
|
||||||
|
revert: ReturnType<typeof vi.fn>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构造一个用禁用计数复现录像数组丢弃行为的录像系统桩 */
|
||||||
|
function createReplaySystem(): ReplaySystemStub {
|
||||||
|
let disabled = 0;
|
||||||
|
const commands: [ReplayCode, unknown[]][] = [];
|
||||||
|
const route: ReplayRouteStub = {
|
||||||
|
add(code, params) {
|
||||||
|
if (disabled > 0) return;
|
||||||
|
commands.push([code, params]);
|
||||||
|
},
|
||||||
|
commands
|
||||||
|
};
|
||||||
|
const disable = vi.fn(() => {
|
||||||
|
disabled++;
|
||||||
|
});
|
||||||
|
const revert = vi.fn(() => {
|
||||||
|
if (disabled > 0) disabled--;
|
||||||
|
});
|
||||||
|
return { route, disable, revert };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 构造一份合成的勇士基础属性 */
|
/** 构造一份合成的勇士基础属性 */
|
||||||
@ -104,16 +145,20 @@ function createEnv(): TestEnv {
|
|||||||
const tileStore = new TileStore();
|
const tileStore = new TileStore();
|
||||||
const itemStore = new ItemStore<IHeroAttr, unknown>();
|
const itemStore = new ItemStore<IHeroAttr, unknown>();
|
||||||
// 录像系统桩,用于满足装备/卸下时的 route.add 记录与临时禁用录像
|
// 录像系统桩,用于满足装备/卸下时的 route.add 记录与临时禁用录像
|
||||||
const replaySystem = {
|
const replaySystem = createReplaySystem();
|
||||||
route: { add: vi.fn() },
|
|
||||||
disable: vi.fn(),
|
|
||||||
revert: vi.fn()
|
|
||||||
};
|
|
||||||
const state = { tileStore, itemStore, replaySystem } as never;
|
const state = { tileStore, itemStore, replaySystem } as never;
|
||||||
const attribute = new HeroAttribute<IHeroAttr>(createBaseAttr());
|
const attribute = new HeroAttribute<IHeroAttr>(createBaseAttr());
|
||||||
const store = new HeroEquipsStore<IHeroAttr>(state);
|
const store = new HeroEquipsStore<IHeroAttr>(state);
|
||||||
const equipment = new HeroEquipment<IHeroAttr>(store, attribute);
|
const equipment = new HeroEquipment<IHeroAttr>(store, attribute);
|
||||||
return { state, tileStore, itemStore, store, equipment, attribute };
|
return {
|
||||||
|
state,
|
||||||
|
tileStore,
|
||||||
|
itemStore,
|
||||||
|
store,
|
||||||
|
equipment,
|
||||||
|
attribute,
|
||||||
|
replaySystem
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 向图块与道具存储注册一个装备道具定义 */
|
/** 向图块与道具存储注册一个装备道具定义 */
|
||||||
@ -315,6 +360,58 @@ describe('HeroEquipment equip and unequip', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('HeroEquipment replay isolation on load', () => {
|
||||||
|
// 验证读档重新装备期间禁用录像且不产生任何录像指令,禁用与恢复次数配对平衡
|
||||||
|
it('suppresses replay recording while loading the equipment state', () => {
|
||||||
|
const env = createEnv();
|
||||||
|
registerItem(env, createItem(10, 'sword', [0], [['atk', 5]]));
|
||||||
|
env.equipment.setSlots(['weapon']);
|
||||||
|
const uid = env.store.add(10);
|
||||||
|
env.equipment.equip(uid, 0);
|
||||||
|
const saved = env.equipment.saveState();
|
||||||
|
expect(env.attribute.getFinalAttribute('atk')).toBe(15);
|
||||||
|
|
||||||
|
env.equipment.unequip(0);
|
||||||
|
env.replaySystem.route.commands.length = 0;
|
||||||
|
env.replaySystem.disable.mockClear();
|
||||||
|
env.replaySystem.revert.mockClear();
|
||||||
|
|
||||||
|
env.equipment.loadState(saved);
|
||||||
|
|
||||||
|
const disableCount = env.replaySystem.disable.mock.calls.length;
|
||||||
|
const revertCount = env.replaySystem.revert.mock.calls.length;
|
||||||
|
expect(env.replaySystem.route.commands).toEqual([]);
|
||||||
|
expect(disableCount).toBeGreaterThan(0);
|
||||||
|
expect(revertCount).toBe(disableCount);
|
||||||
|
expect(env.equipment.getEquipped(0)).toBe(uid);
|
||||||
|
expect(env.attribute.getFinalAttribute('atk')).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证读档包裹窗口结束后装备与卸下的录像记录能力仍保留
|
||||||
|
it('keeps recording equip and unequip after loading', () => {
|
||||||
|
const env = createEnv();
|
||||||
|
registerItem(env, createItem(10, 'sword', [0], [['atk', 5]]));
|
||||||
|
env.equipment.setSlots(['weapon']);
|
||||||
|
const uid = env.store.add(10);
|
||||||
|
env.equipment.equip(uid, 0);
|
||||||
|
const saved = env.equipment.saveState();
|
||||||
|
env.equipment.unequip(0);
|
||||||
|
env.equipment.loadState(saved);
|
||||||
|
|
||||||
|
env.replaySystem.route.commands.length = 0;
|
||||||
|
env.equipment.unequip(0);
|
||||||
|
expect(env.replaySystem.route.commands).toEqual([
|
||||||
|
[ReplayCode.Unequip, [0]]
|
||||||
|
]);
|
||||||
|
|
||||||
|
env.equipment.equip(uid, 0);
|
||||||
|
expect(env.replaySystem.route.commands).toEqual([
|
||||||
|
[ReplayCode.Unequip, [0]],
|
||||||
|
[ReplayCode.Equip, [uid]]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('HeroEquipment compare and guards', () => {
|
describe('HeroEquipment compare and guards', () => {
|
||||||
// 验证 compareEquip 逐属性输出两个装备的最终属性差
|
// 验证 compareEquip 逐属性输出两个装备的最终属性差
|
||||||
it('diffs the final attributes of two equipment instances', () => {
|
it('diffs the final attributes of two equipment instances', () => {
|
||||||
|
|||||||
@ -341,8 +341,16 @@ export class HeroEquipment<THero> implements IHeroEquipment<THero> {
|
|||||||
});
|
});
|
||||||
// 由于装备修饰器不进存档,所以此时的勇士处于没有任何装备修饰器的状态,故可以安全清除
|
// 由于装备修饰器不进存档,所以此时的勇士处于没有任何装备修饰器的状态,故可以安全清除
|
||||||
this.equips.clear();
|
this.equips.clear();
|
||||||
for (const [index, uid] of state.equipped) {
|
// 读档期间经 equip 恢复槽位会产生真实录像指令,故暂时禁用录像记录
|
||||||
this.equip(uid, index);
|
const replay = this.state.replaySystem;
|
||||||
|
replay.disable();
|
||||||
|
try {
|
||||||
|
for (const [index, uid] of state.equipped) {
|
||||||
|
this.equip(uid, index);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// 必须保证所有路径都恢复录像记录,否则会永久吞掉后续录像
|
||||||
|
replay.revert();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user