test(07-09): #06-17-2 combat-side same-reference hero attribute regression

- migrate the top-level snapshot assertion to heroSave.attribute.values.hp
- add CoreState enemyContext.getBindedHero() regressions: same live attribute before/after load, no rebinding, across all compressions
This commit is contained in:
unanmed 2026-09-16 18:47:39 +08:00
parent 1f0af69004
commit 940e31f293
2 changed files with 101 additions and 2 deletions

View File

@ -3,9 +3,12 @@ import {
IEnemyAttr,
IReplaySandbox,
IHeroAttr,
IItemRawData,
ItemCategory,
ReplaySystem,
SaveCompression,
ReplayCommandCode
ReplayCommandCode,
TileType
} from '@user/data-common';
import { IEnemy, IReadonlyHeroAttribute } from '@user/data-base';
import { IReadonlyEnemyHandler } from '@user/data-system';
@ -113,6 +116,48 @@ async function waitForEnded(sandbox: IReplaySandbox): Promise<void> {
if (!sandbox.ended) throw new Error('closure replay did not end');
}
/** 向顶层状态注册一件 atk+5 的装备并装备至 0 号槽,返回装备实例 uid */
function equipAtkItem(state: CoreState): number {
state.tileStore.addTile({
num: 9001,
id: 'closure-sword',
events: {},
type: TileType.Item,
pass: { onlyEvents: false, inPass: 15, outPass: 15 },
eventPass: true
});
const value: [SelectKey<IHeroAttr, number>, number][] = [['atk', 5]];
const item: IItemRawData<IHeroAttr> = {
num: 9001,
id: 'closure-sword',
category: ItemCategory.Equipment,
name: 'closure-sword',
text: 'closure-sword',
hideInToolbox: false,
effect: { useEvent: null, useEffect: () => {}, canUse: () => false },
equip: {
slots: [0],
animate: 'sword',
value: new Map(value),
percentage: new Map(),
loadEvent: null,
unloadEvent: null
}
};
state.itemStore.addItem(item);
state.hero.equip.setSlots(['weapon']);
const uid = state.hero.items.equipment.add(9001);
state.hero.equip.equip(uid, 0);
return uid;
}
/** 存读档测试覆盖的三档压缩级别 */
const SAVE_COMPRESSIONS = [
SaveCompression.NoCompression,
SaveCompression.LowCompression,
SaveCompression.HighCompression
] as const;
describe('DATA-01 closure', () => {
// 验证敌人管理器能够创建敌人、修改属性并完成独立存档恢复
it('creates, mutates, and saves an enemy through the public manager', () => {
@ -257,3 +302,57 @@ describe('DATA-01 closure', () => {
expect(laterExecuted).toBe(false);
});
});
describe('CoreState hero attribute same-reference load (#06-17-2)', () => {
// 验证读档前战斗侧绑定与活属性为同一实例且反映装备加成
it('binds the combat context to the live hero attribute before load', () => {
const state = createCoreState();
equipAtkItem(state);
const bound = state.enemyContext.getBindedHero();
const attribute = state.hero.getModifiableAttribute();
expect(bound).toBe(attribute);
expect(attribute.getFinalAttribute('atk')).toBe(5);
});
// 验证读档后战斗侧无需重绑即读到同一活属性(#06-17-2)
it('reads the live attribute after load without rebinding', () => {
const state = createCoreState();
const uid = equipAtkItem(state);
const saved = state.hero.saveState(SaveCompression.NoCompression);
state.hero.equip.unequip(0);
state.hero.getModifiableAttribute().set('atk', 1);
state.hero.loadState(saved, SaveCompression.NoCompression);
expect(state.enemyContext.getBindedHero()).toBe(
state.hero.getModifiableAttribute()
);
expect(
state.enemyContext.getBindedHero()!.getFinalAttribute('atk')
).toBe(5);
expect(state.hero.equip.getEquipped(0)).toBe(uid);
});
// 验证三档压缩下战斗侧始终读到同一活属性并反映读档值
it('reads the same live attribute across all compressions', () => {
for (const compression of SAVE_COMPRESSIONS) {
const state = createCoreState();
const uid = equipAtkItem(state);
const saved = state.hero.saveState(compression);
state.hero.equip.unequip(0);
state.hero.getModifiableAttribute().set('atk', 1);
state.hero.loadState(saved, compression);
expect(state.enemyContext.getBindedHero()).toBe(
state.hero.getModifiableAttribute()
);
expect(
state.enemyContext.getBindedHero()!.getFinalAttribute('atk')
).toBe(5);
expect(state.hero.equip.getEquipped(0)).toBe(uid);
}
});
});

View File

@ -251,7 +251,7 @@ describe('CoreState top-level save and load round trips', () => {
'@system/hero'
) as IHeroStateSave<IHeroAttr>;
const mapSave = snapshot.get('@system/maps') as IMapStoreSave;
expect(heroSave.attribute.hp).toBe(88);
expect(heroSave.attribute.values.hp).toBe(88);
expect(mapSave.floors.get('F1')!.layers.get(0)!.fullMap![0]).toBe(5);
});