mirror of
https://github.com/motajs/template.git
synced 2026-09-18 13:10:21 +08:00
fix(07-11): #06-17-4 preserve equipment instances across loadState
- HeroEquipsStore.loadState 按 uid 复用现有 EquipmentState 原地读档 - 同 uid 但装备图块数字不同时才替换该实例 - 存档中不存在的装备实例一律删除(以存档为准) - IHeroEquipsStore.loadState jsDoc 补写同引用与删除语义 - 新增同实例保真、删除多余实例、num 不同替换三条回归用例
This commit is contained in:
parent
94ceb50cfe
commit
153f4a490a
@ -6,12 +6,17 @@ import {
|
||||
type IItemRawData,
|
||||
ItemCategory,
|
||||
ItemStore,
|
||||
SaveCompression,
|
||||
TileStore,
|
||||
TileType
|
||||
} from '@user/data-common';
|
||||
import { HeroEquipsStore } from './equipStore';
|
||||
import { PercentageModifier, ValueModifier } from './modifier';
|
||||
import { type IEquipmentSortHandler, type IEquipmentSorter } from './types';
|
||||
import {
|
||||
type IEquipmentSortHandler,
|
||||
type IEquipmentSorter,
|
||||
type IEquipmentStateSave
|
||||
} from './types';
|
||||
|
||||
vi.hoisted(() => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
@ -45,6 +50,13 @@ afterAll(() => {
|
||||
|
||||
type HeroKey = SelectKey<IHeroAttr, number>;
|
||||
|
||||
/** 存读档测试覆盖的三档压缩级别 */
|
||||
const SAVE_COMPRESSIONS = [
|
||||
SaveCompression.NoCompression,
|
||||
SaveCompression.LowCompression,
|
||||
SaveCompression.HighCompression
|
||||
] as const;
|
||||
|
||||
interface TestEnv {
|
||||
state: IDataCommon;
|
||||
tileStore: IDataCommon['tileStore'];
|
||||
@ -217,6 +229,82 @@ describe('HeroEquipsStore instances', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/** 构造一份仅指定数值加成的无压缩装备存档,用于把活实例改到指定存档点 */
|
||||
function createValueSave(
|
||||
uid: number,
|
||||
atk: number
|
||||
): IEquipmentStateSave<IHeroAttr> {
|
||||
return {
|
||||
uid,
|
||||
num: 10,
|
||||
value: new Map<HeroKey, number>([['atk', atk]]),
|
||||
percentage: new Map<HeroKey, number>()
|
||||
};
|
||||
}
|
||||
|
||||
describe('HeroEquipsStore same-reference load (#06-17-4)', () => {
|
||||
// 验证同 uid 装备实例在三档压缩读档前后为同一实例,且数值恢复到存档点
|
||||
it('keeps the same instance and restores values across all compressions', () => {
|
||||
for (const compression of SAVE_COMPRESSIONS) {
|
||||
const env = createEnv();
|
||||
registerItem(env, createItem(10, 'sword', [['atk', 5]]));
|
||||
const uid = env.store.add(10);
|
||||
const before = env.store.get(uid)!;
|
||||
// 装备数值只能经读档改变,先用一份自定义存档把活实例改到 9 作为存档点
|
||||
before.loadState(
|
||||
createValueSave(uid, 9),
|
||||
SaveCompression.NoCompression
|
||||
);
|
||||
const saved = env.store.saveState(compression);
|
||||
before.loadState(
|
||||
createValueSave(uid, 1),
|
||||
SaveCompression.NoCompression
|
||||
);
|
||||
|
||||
env.store.loadState(saved, compression);
|
||||
|
||||
expect(env.store.get(uid)).toBe(before);
|
||||
expect([...before.getModifiers()][0][1].getValue()).toBe(9);
|
||||
}
|
||||
});
|
||||
|
||||
// 验证存档中不存在的装备实例在读档后被删除(以存档为准)
|
||||
it('deletes instances absent from the save', () => {
|
||||
const env = createEnv();
|
||||
registerItem(env, createItem(10, 'sword'));
|
||||
const kept = env.store.add(10);
|
||||
const before = env.store.get(kept)!;
|
||||
|
||||
const saved = env.store.saveState(SaveCompression.NoCompression);
|
||||
const extra = env.store.add(10);
|
||||
|
||||
env.store.loadState(saved, SaveCompression.NoCompression);
|
||||
|
||||
expect(env.store.get(kept)).toBe(before);
|
||||
expect(env.store.get(extra)).toBeNull();
|
||||
expect(env.store.count(10)).toBe(1);
|
||||
});
|
||||
|
||||
// 验证同 uid 但装备图块数字不同的存档无法复用实例,改为替换该实例
|
||||
it('replaces the instance when the saved item number differs', () => {
|
||||
const env = createEnv();
|
||||
registerItem(env, createItem(10, 'sword', [['atk', 5]]));
|
||||
registerItem(env, createItem(11, 'axe', [['atk', 3]]));
|
||||
const uid = env.store.add(10);
|
||||
const before = env.store.get(uid)!;
|
||||
|
||||
const saved = env.store.saveState(SaveCompression.NoCompression);
|
||||
const swapped = {
|
||||
equipments: saved.equipments.map(v => ({ ...v, num: 11 }))
|
||||
};
|
||||
|
||||
env.store.loadState(swapped, SaveCompression.NoCompression);
|
||||
|
||||
expect(env.store.get(uid)).not.toBe(before);
|
||||
expect(env.store.get(uid)?.item.num).toBe(11);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EquipmentState modifiers', () => {
|
||||
// 验证装备实例按定义生成数值与百分比修饰器
|
||||
it('builds value and percentage modifiers from the item', () => {
|
||||
|
||||
@ -268,16 +268,34 @@ export class HeroEquipsStore<THero> implements IHeroEquipsStore<THero> {
|
||||
state: IHeroEquipsStoreSave<THero>,
|
||||
compression: SaveCompression
|
||||
): void {
|
||||
this.instanceMap.clear();
|
||||
// 按 uid 复用现有实例原地读档,使外部持有的引用跨读档仍然有效;
|
||||
// 仅当 uid 相同但装备图块数字不同时才替换实例
|
||||
const savedUids = new Set<number>();
|
||||
for (const save of state.equipments) {
|
||||
const raw = this.state.itemStore.getData(save.num);
|
||||
if (!raw) {
|
||||
logger.error(59, save.num.toString());
|
||||
continue;
|
||||
}
|
||||
const instance = new EquipmentState<THero>(save.uid, raw, false);
|
||||
instance.loadState(save, compression);
|
||||
this.instanceMap.set(save.uid, instance);
|
||||
savedUids.add(save.uid);
|
||||
const existing = this.instanceMap.get(save.uid);
|
||||
if (existing && existing.item.num === save.num) {
|
||||
existing.loadState(save, compression);
|
||||
} else {
|
||||
const instance = new EquipmentState<THero>(
|
||||
save.uid,
|
||||
raw,
|
||||
false
|
||||
);
|
||||
instance.loadState(save, compression);
|
||||
this.instanceMap.set(save.uid, instance);
|
||||
}
|
||||
}
|
||||
// 以存档为准:存档中不存在的装备实例一律删除
|
||||
for (const uid of this.instanceMap.keys()) {
|
||||
if (!savedUids.has(uid)) {
|
||||
this.instanceMap.delete(uid);
|
||||
}
|
||||
}
|
||||
const maxUid = maxBy(state.equipments, 'uid');
|
||||
if (!maxUid) {
|
||||
|
||||
@ -690,3 +690,54 @@ describe('HeroState same-reference attribute load (#06-17-1)', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeroState same-reference equipment load (#06-17-4)', () => {
|
||||
// 验证经勇士容器三档往返后装备实例为同一实例,且数值恢复到存档点
|
||||
it('keeps the equipment instance and restores values across all compressions', () => {
|
||||
for (const compression of SAVE_COMPRESSIONS) {
|
||||
const hero = createEquipHero();
|
||||
const uid = hero.items.equipment.add(10);
|
||||
const before = hero.items.equipment.get(uid)!;
|
||||
// 装备数值只能经读档改变,先用一份自定义存档把活实例改到 9 作为存档点
|
||||
before.loadState(
|
||||
{
|
||||
uid,
|
||||
num: 10,
|
||||
value: new Map<HeroKey, number>([['atk', 9]]),
|
||||
percentage: new Map<HeroKey, number>()
|
||||
},
|
||||
SaveCompression.NoCompression
|
||||
);
|
||||
const saved = hero.saveState(compression);
|
||||
before.loadState(
|
||||
{
|
||||
uid,
|
||||
num: 10,
|
||||
value: new Map<HeroKey, number>([['atk', 1]]),
|
||||
percentage: new Map<HeroKey, number>()
|
||||
},
|
||||
SaveCompression.NoCompression
|
||||
);
|
||||
|
||||
hero.loadState(saved, compression);
|
||||
|
||||
expect(hero.items.equipment.get(uid)).toBe(before);
|
||||
expect([...before.getModifiers()][0][1].getValue()).toBe(9);
|
||||
}
|
||||
});
|
||||
|
||||
// 验证存档中不存在的装备实例在读档后被删除(以存档为准)
|
||||
it('deletes equipment instances absent from the save', () => {
|
||||
const hero = createEquipHero();
|
||||
const kept = hero.items.equipment.add(10);
|
||||
const before = hero.items.equipment.get(kept)!;
|
||||
|
||||
const saved = hero.saveState(SaveCompression.NoCompression);
|
||||
const extra = hero.items.equipment.add(10);
|
||||
|
||||
hero.loadState(saved, SaveCompression.NoCompression);
|
||||
|
||||
expect(hero.items.equipment.get(kept)).toBe(before);
|
||||
expect(hero.items.equipment.get(extra)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@ -7,7 +7,8 @@ import {
|
||||
IItemRawData,
|
||||
IObjectMovable,
|
||||
IObjectMover,
|
||||
ISaveableContent
|
||||
ISaveableContent,
|
||||
SaveCompression
|
||||
} from '@user/data-common';
|
||||
import { IPassPredicate } from '../map';
|
||||
|
||||
@ -710,6 +711,19 @@ export interface IHeroEquipsStore<THero>
|
||||
* @param item 道具图块数字或字符串 id
|
||||
*/
|
||||
count(item: number | string): number;
|
||||
|
||||
/**
|
||||
* 读取装备实例仓库存档。存档中已存在的 uid 会复用原实例并在其实例上原地读档,
|
||||
* 因此外部通过 {@link get} / {@link instancesOf} / {@link instances} 持有的实例跨读档仍然有效;
|
||||
* 仅当 uid 相同但装备图块数字不同的存档无法复用实例时,才替换该实例。
|
||||
* 存档中不存在的实例一律删除,即读档结果以存档为准。
|
||||
* @param state 装备实例仓库存档
|
||||
* @param compression 压缩级别
|
||||
*/
|
||||
loadState(
|
||||
state: IHeroEquipsStoreSave<THero>,
|
||||
compression: SaveCompression
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface IHeroEquipmentSave {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user