test(06-13): tier hero save/load classes across all compressions (G-06-09-B)

- EquipmentState percentage round trip per tier (NoCompression green; Low/High correct-expectation it.skip anchored #06-09-1)
- HeroItems constant/consumable tables and HeroState attributes/location loop over the three compression tiers
- HeroState container covers HeroLocation/HeroRendering across tiers; HeroEquipment container expectation it.skip anchored #06-09-2
- Existing #06-09-1/#06-09-2 skips unchanged
This commit is contained in:
unanmed 2026-09-15 11:22:52 +08:00
parent 60be0c0a24
commit 6d7e57ab34

View File

@ -1,4 +1,4 @@
// 测试勇士各子系统存读档:同实例往返、压缩档与码 58/59
// 测试勇士吁E<EFBFBD><EFBFBD>系统存读档<EFBFBD>E<EFBFBD>同实例往返、压缩档与码E58/59
import { afterAll, describe, expect, it, vi } from 'vitest';
import {
type IDataCommon,
@ -54,7 +54,7 @@ afterAll(() => {
vi.unstubAllGlobals();
});
/** 存读档测试覆盖的三档压缩级别 */
/** 存读档测试要E<EFBFBD><EFBFBD>皁E<EFBFBD><EFBFBD>档压缩级别 */
const SAVE_COMPRESSIONS = [
SaveCompression.NoCompression,
SaveCompression.LowCompression,
@ -71,7 +71,7 @@ interface EquipEnv {
equipment: HeroEquipment<IHeroAttr>;
}
/** 构造一个含图块、道具存储与假录像系统的公共层假对象 */
/** 极E<EFBFBD><EFBFBD>一个含图块、E<EFBFBD><EFBFBD>具存储与假录像系统的公共层假对象 */
function createState(): IDataCommon {
const tileStore = new TileStore();
const itemStore = new ItemStore<IHeroAttr, unknown>();
@ -83,7 +83,7 @@ function createState(): IDataCommon {
} as never;
}
/** 构造一份合成的勇士基础属性 */
/** 极E<EFBFBD><EFBFBD>一份合<EFBFBD>E皁E<EFBFBD><EFBFBD>士基础属性 */
function createBaseAttr(): IHeroAttr {
return {
name: 'hero',
@ -99,7 +99,7 @@ function createBaseAttr(): IHeroAttr {
};
}
/** 构造一个带合成装备属性的道具定义 */
/** 极E<EFBFBD><EFBFBD>一个带合<EFBFBD>E裁E<EFBFBD><EFBFBD>E<EFBFBD><EFBFBD>性皁E<EFBFBD><EFBFBD>具定乁E*/
function createEquipItem(
num: number,
id: string,
@ -126,7 +126,7 @@ function createEquipItem(
};
}
/** 构造一个非装备类道具定义,用于覆盖背包分表存读档 */
/** 极E<EFBFBD><EFBFBD>一个非裁E<EFBFBD><EFBFBD>E<EFBFBD><EFBFBD>道<EFBFBD>E定义,用于要E<EFBFBD><EFBFBD>背包刁E<EFBFBD><EFBFBD>存读档 */
function createPlainItem(
num: number,
id: string,
@ -151,7 +151,7 @@ function createPlainItem(
};
}
/** 向图块与道具存储注册一个道具定义 */
/** 向图块与道具存储注册一个道<EFBFBD>E定乁E*/
function registerItem(env: EquipEnv, item: IItemRawData<IHeroAttr>): void {
env.tileStore.addTile({
num: item.num,
@ -164,7 +164,7 @@ function registerItem(env: EquipEnv, item: IItemRawData<IHeroAttr>): void {
env.itemStore.addItem(item);
}
/** 构造一个装配装备实例存储与勇士装备对象的测试环境 */
/** 极E<EFBFBD><EFBFBD>一个裁E<EFBFBD>E裁E<EFBFBD><EFBFBD>E<EFBFBD><EFBFBD>例存储与勇士裁E<EFBFBD><EFBFBD>E<EFBFBD><EFBFBD>象皁E<EFBFBD><EFBFBD>试环墁E*/
function createEquipEnv(): EquipEnv {
const state = createState();
const store = new HeroEquipsStore<IHeroAttr>(state);
@ -181,7 +181,7 @@ function createEquipEnv(): EquipEnv {
};
}
/** 构造一个注册了跟随者图块的公共层假对象 */
/** 极E<EFBFBD><EFBFBD>一个注册亁E<EFBFBD><EFBFBD>随老E<EFBFBD><EFBFBD>块的公共层假对象 */
function createFollowerState(): IDataCommon {
const state = createState();
state.tileStore.addTile({
@ -195,7 +195,7 @@ function createFollowerState(): IDataCommon {
return state;
}
/** 构造一个装配完成的勇士状态对象 */
/** 极E<EFBFBD><EFBFBD>一个裁E<EFBFBD>E完<EFBFBD>E皁E<EFBFBD><EFBFBD>士状态对象 */
function createHeroState(): IHeroState<IHeroAttr> {
return new HeroState<IHeroAttr>(
createState(),
@ -204,8 +204,26 @@ function createHeroState(): IHeroState<IHeroAttr> {
);
}
/** 在持E<E68C81><45>压缩档下对裁E<E8A381><45>E<EFBFBD><45>刁E<E58881><45>加成做一次同实例往返,返回读档后的修饰器值 */
function roundTripPercentageModifier(compression: SaveCompression): unknown {
const env = createEquipEnv();
const item = createEquipItem(10, 'sword', [0], [], [['hp', 0.5]]);
registerItem(env, item);
const state = new EquipmentState<IHeroAttr>(0, item);
const modifier = [...state.getModifiers()][0][1];
modifier.setValue(0.9);
const saved = state.saveState(compression);
state.loadState(saved, compression);
const restored = [...state.getModifiers()].find(
([, current]) => current instanceof PercentageModifier
);
return restored?.[1].getValue();
}
describe('Hero modifier save and load round trips', () => {
// 验证 ValueModifier 的数值在三个压缩档下均能同实例恢复
// 验证EValueModifier 皁E<E79A81><45>值在三个压缩档下均能同实例恢夁E
it('restores a value modifier across all compressions', () => {
for (const compression of SAVE_COMPRESSIONS) {
const modifier = new ValueModifier(5);
@ -218,7 +236,7 @@ describe('Hero modifier save and load round trips', () => {
}
});
// 验证 PercentageModifier 的数值在三个压缩档下均能同实例恢复
// 验证EPercentageModifier 皁E<E79A81><45>值在三个压缩档下均能同实例恢夁E
it('restores a percentage modifier across all compressions', () => {
for (const compression of SAVE_COMPRESSIONS) {
const modifier = new PercentageModifier(0.25);
@ -233,7 +251,7 @@ describe('Hero modifier save and load round trips', () => {
});
describe('HeroLocation save and load round trips', () => {
// 验证坐标、楼层与朝向在同实例上完整恢复
// 验证坐栁E<EFBFBD><EFBFBD>楼层与朝向在同实例上完整恢夁E
it('restores position, floor and direction on the same instance', () => {
const location = new HeroLocation(
createState(),
@ -258,7 +276,7 @@ describe('HeroLocation save and load round trips', () => {
});
describe('HeroRendering save and load round trips', () => {
// 验证不透明度在同实例上恢复
// 验证不透<EFBFBD>E度在同实例上恢夁E
it('restores alpha on the same instance', () => {
const rendering = new HeroRendering(createState());
rendering.setAlpha(0.25);
@ -272,7 +290,7 @@ describe('HeroRendering save and load round trips', () => {
});
describe('HeroEquipment save and load round trips', () => {
// 验证装备槽与已装备映射在同实例上恢复(存盘经结构化克隆模拟真实序列化)
// 验证裁E<EFBFBD><EFBFBD>E<EFBFBD><EFBFBD>与已裁E<EFBFBD><EFBFBD>E<EFBFBD><EFBFBD>封E<EFBFBD><EFBFBD>同实例上恢复(存盘经结构化<EFBFBD>E隁E<EFBFBD><EFBFBD>拟真实序<EFBFBD>E化!E
it('restores slots and equipped mapping on the same instance', () => {
const env = createEquipEnv();
registerItem(env, createEquipItem(10, 'sword', [0], [['atk', 5]]));
@ -290,7 +308,7 @@ describe('HeroEquipment save and load round trips', () => {
expect(env.equipment.slots).toEqual(['weapon']);
});
// 疑似 bug:saveState 直接返回内部 equipped/slots 引用而非深拷贝,详见 06-TEST-FINDINGS.md #06-09-2
// 疑似 bug<EFBFBD>E<EFBFBD>saveState 直接返回冁E<E58681><45> equipped/slots 引用而非深拷贝,详见E06-TEST-FINDINGS.md #06-09-2
it.skip('returns an equipment snapshot independent from the live state', () => {
const env = createEquipEnv();
registerItem(env, createEquipItem(10, 'sword', [0], [['atk', 5]]));
@ -308,25 +326,28 @@ describe('HeroEquipment save and load round trips', () => {
});
describe('EquipmentState save and load round trips', () => {
// 验证百分比加成的数值在同实例上恢复(无压缩档)
it('restores a percentage modifier on the same instance', () => {
const env = createEquipEnv();
const item = createEquipItem(10, 'sword', [0], [], [['hp', 0.5]]);
registerItem(env, item);
const state = new EquipmentState<IHeroAttr>(0, item);
const modifier = [...state.getModifiers()][0][1];
modifier.setValue(0.9);
const saved = state.saveState(SaveCompression.NoCompression);
state.loadState(saved, SaveCompression.NoCompression);
const restored = [...state.getModifiers()].find(
([, current]) => current instanceof PercentageModifier
// 验证百刁E<E58881><45>加成在无压缩档下同实例恢复到存档点
it('restores a percentage modifier on the same instance in NoCompression', () => {
expect(roundTripPercentageModifier(SaveCompression.NoCompression)).toBe(
0.5
);
expect(restored?.[1].getValue()).toBe(0.5);
});
// 疑似 bug:loadNoCompression 误从存档百分比表读取数值表,详见 06-TEST-FINDINGS.md #06-09-1
// 疑似 bug<75>E<EFBFBD>Low 压缩档读档未回退到裁E<E8A381><45>E<EFBFBD><45>始定义,未修改皁E<E79A81><45>刁E<E58881><45>加成丢失<E4B8A2>E<EFBFBD>详见E06-TEST-FINDINGS.md #06-09-1
it.skip('restores a percentage modifier on the same instance in LowCompression', () => {
expect(
roundTripPercentageModifier(SaveCompression.LowCompression)
).toBe(0.5);
});
// 疑似 bug<75>E<EFBFBD>High 压缩档读档未回退到裁E<E8A381><45>E<EFBFBD><45>始定义,未修改皁E<E79A81><45>刁E<E58881><45>加成丢失<E4B8A2>E<EFBFBD>详见E06-TEST-FINDINGS.md #06-09-1
it.skip('restores a percentage modifier on the same instance in HighCompression', () => {
expect(
roundTripPercentageModifier(SaveCompression.HighCompression)
).toBe(0.5);
});
// 疑似 bug<75>E<EFBFBD>loadNoCompression 误从存档百刁E<E58881><45>表读取数值表<E580BC>E<EFBFBD>详见E06-TEST-FINDINGS.md #06-09-1
it.skip('restores a value modifier on the same instance', () => {
const env = createEquipEnv();
const item = createEquipItem(10, 'sword', [0], [['atk', 5]]);
@ -344,7 +365,7 @@ describe('EquipmentState save and load round trips', () => {
expect(restored?.[1].getValue()).toBe(5);
});
// 疑似 bug:loadDiff 清空后未回退到装备原始定义,未修改的加成丢失,详见 06-TEST-FINDINGS.md #06-09-1
// 疑似 bug<EFBFBD>E<EFBFBD>loadDiff 渁E<E6B881><45>后未回退到裁E<E8A381><45>E<EFBFBD><45>始定义,未修改皁E<E79A81><45>成丢失<E4B8A2>E<EFBFBD>详见E06-TEST-FINDINGS.md #06-09-1
it.skip('keeps unchanged value modifiers for compressed snapshots', () => {
const env = createEquipEnv();
const item = createEquipItem(10, 'sword', [0], [['atk', 5]]);
@ -361,7 +382,7 @@ describe('EquipmentState save and load round trips', () => {
});
describe('HeroEquipsStore save and load round trips', () => {
// 验证装备实例在三个压缩档下同实例恢复并续接自增 uid
// 验证裁E<EFBFBD><EFBFBD>E<EFBFBD><EFBFBD>例在三个压缩档下同实例恢复并续接自墁Euid
it('restores equipment instances and the uid counter across all compressions', () => {
for (const compression of SAVE_COMPRESSIONS) {
const env = createEquipEnv();
@ -379,7 +400,7 @@ describe('HeroEquipsStore save and load round trips', () => {
}
});
// 验证存档缺失道具原始数据时经 logger.catch 观测到错误码 59
// 验证存档缺失道<EFBFBD>E原始数据时绁Elogger.catch 观测到错误码E59
it('warns code 59 when the item raw data is missing', () => {
const env = createEquipEnv();
const saved = {
@ -400,7 +421,7 @@ describe('HeroEquipsStore save and load round trips', () => {
expect(result.info.map(info => info.code)).toContain(59);
});
// 验证存档不含任何装备实例时经 logger.catch 观测到错误码 58
// 验证存档不含任何裁E<EFBFBD><EFBFBD>E<EFBFBD><EFBFBD>例时绁Elogger.catch 观测到错误码E58
it('warns code 58 when the max equipment uid cannot be found', () => {
const env = createEquipEnv();
@ -416,31 +437,36 @@ describe('HeroEquipsStore save and load round trips', () => {
});
describe('HeroItems save and load round trips', () => {
// 验证永久与消耗道具分表在同实例上恢复
it('restores constant and consumable item tables on the same instance', () => {
const env = createEquipEnv();
registerItem(env, createPlainItem(20, 'key', ItemCategory.Constant));
registerItem(
env,
createPlainItem(21, 'potion', ItemCategory.Consumable)
);
const items = new HeroItems<IHeroAttr>(env.state);
items.addItem(20, 3);
items.addItem(21, 2);
// 验证永乁E<E4B981><45>消耗道具刁E<E58881><45>在三个压缩档下同实例恢夁E
it('restores constant and consumable item tables across all compressions', () => {
for (const compression of SAVE_COMPRESSIONS) {
const env = createEquipEnv();
registerItem(
env,
createPlainItem(20, 'key', ItemCategory.Constant)
);
registerItem(
env,
createPlainItem(21, 'potion', ItemCategory.Consumable)
);
const items = new HeroItems<IHeroAttr>(env.state);
items.addItem(20, 3);
items.addItem(21, 2);
const saved = items.saveState(SaveCompression.NoCompression);
items.addItem(20, -3);
items.addItem(21, -2);
items.loadState(saved, SaveCompression.NoCompression);
const saved = items.saveState(compression);
items.addItem(20, -3);
items.addItem(21, -2);
items.loadState(saved, compression);
expect(items.itemCount(20)).toBe(3);
expect(items.itemCount(21)).toBe(2);
expect(items.getItemState(20)?.id).toBe('key');
expect(items.itemCount(20)).toBe(3);
expect(items.itemCount(21)).toBe(2);
expect(items.getItemState(20)?.id).toBe('key');
}
});
});
describe('HeroFollower save and load round trips', () => {
// 验证跟随者位置、楼层与渲染在同实例上恢复
// 验证跟随老E<EFBFBD><EFBFBD>置、楼层与渲染在同实例上恢夁E
it('restores follower location and rendering on the same instance', () => {
const state = createFollowerState();
const faceHandler = new Dir8FaceHandler();
@ -480,32 +506,38 @@ describe('HeroFollower save and load round trips', () => {
});
describe('HeroState save and load round trips', () => {
// 验证属性、修饰器与位置在同实例上恢复并重建属性对象
it('restores attributes, modifiers and location on the same instance', () => {
const hero = createHeroState();
hero.registerModifier('@system/value', () => new ValueModifier(5));
hero.createAndInsertModifier('@system/value', 'atk');
hero.getModifiableAttribute().set('hp', 88);
hero.location.setPos(1, 1);
hero.location.setFloor('F1');
hero.location.mover.setFaceDir(FaceDirection.Up);
// 验证属性、修饰器与位置在三个压缩档下同实例恢复并重建属性对象
it('restores attributes, modifiers and location across all compressions', () => {
for (const compression of SAVE_COMPRESSIONS) {
const hero = createHeroState();
hero.registerModifier('@system/value', () => new ValueModifier(5));
hero.createAndInsertModifier('@system/value', 'atk');
hero.getModifiableAttribute().set('hp', 88);
hero.location.setPos(1, 1);
hero.location.setFloor('F1');
hero.location.mover.setFaceDir(FaceDirection.Up);
const saved = hero.saveState(SaveCompression.NoCompression);
hero.getModifiableAttribute().set('hp', 1);
hero.location.setPos(9, 9);
hero.loadState(saved, SaveCompression.NoCompression);
const saved = hero.saveState(compression);
hero.getModifiableAttribute().set('hp', 1);
hero.location.setPos(9, 9);
hero.loadState(saved, compression);
expect(hero.getModifiableAttribute().getBaseAttribute('hp')).toBe(88);
expect(hero.getModifiableAttribute().getFinalAttribute('atk')).toBe(15);
expect(hero.getLocation()).toEqual({
x: 1,
y: 1,
direction: FaceDirection.Up
});
expect(hero.location.floorId).toBe('F1');
expect(hero.getModifiableAttribute().getBaseAttribute('hp')).toBe(
88
);
expect(hero.getModifiableAttribute().getFinalAttribute('atk')).toBe(
15
);
expect(hero.getLocation()).toEqual({
x: 1,
y: 1,
direction: FaceDirection.Up
});
expect(hero.location.floorId).toBe('F1');
}
});
// 验证存盘禁用(save=false)的修饰器不进入存档且读档后不再存在
// 验证存盘禁用<EFBFBD>E<EFBFBD>Eave=false<73>E<EFBFBD>的修饰器不进<E4B88D>E存档且读档后不<E5908E>E存在
it('excludes save-disabled modifiers from the snapshot', () => {
const hero = createHeroState();
hero.registerModifier('@system/value', () => new ValueModifier(5));
@ -525,3 +557,54 @@ describe('HeroState save and load round trips', () => {
expect(hero.getModifiableAttribute().getFinalAttribute('atk')).toBe(15);
});
});
describe('HeroState container save and load coverage for sub systems', () => {
// 验证不接受压缩参数皁E<E79A81><45>位与渲染经勇士容器三档往返均恢复到存档点
it('restores location and rendering through the container across all compressions', () => {
for (const compression of SAVE_COMPRESSIONS) {
const hero = createHeroState();
hero.location.setPos(2, 3);
hero.location.setFloor('F2');
hero.location.mover.setFaceDir(FaceDirection.Right);
hero.rendering.setAlpha(0.25);
const saved = hero.saveState(compression);
hero.location.setPos(9, 9);
hero.location.setFloor('F9');
hero.location.mover.setFaceDir(FaceDirection.Down);
hero.rendering.setAlpha(1);
hero.loadState(saved, compression);
expect(hero.location.x).toBe(2);
expect(hero.location.y).toBe(3);
expect(hero.location.floorId).toBe('F2');
expect(hero.location.getCurrentFaceDirection()).toBe(
FaceDirection.Right
);
expect(hero.rendering.alpha).toBe(0.25);
}
});
// 疑似 bug<75>E<EFBFBD>HeroEquipment.saveState 未深拷贁Eequipped/slots<74>E<EFBFBD>经容器三档往返均丢失已裁E<E8A381><45>E<EFBFBD><45>封E<E5B081><45>详见E06-TEST-FINDINGS.md #06-09-2
it.skip('restores the equipped mapping through the container across all compressions', () => {
for (const compression of SAVE_COMPRESSIONS) {
const env = createEquipEnv();
registerItem(env, createEquipItem(10, 'sword', [0], [['atk', 5]]));
const hero = new HeroState<IHeroAttr>(
env.state,
new Dir8FaceHandler(),
new HeroAttribute<IHeroAttr>(createBaseAttr())
);
hero.equip.setSlots(['weapon']);
const uid = hero.items.equipment.add(10);
hero.equip.equip(uid, 0);
const saved = hero.saveState(compression);
hero.equip.unequip(0);
hero.loadState(saved, compression);
expect(hero.equip.getEquipped(0)).toBe(uid);
expect(hero.equip.slots).toEqual(['weapon']);
}
});
});