fix(07-12): compareEquip 在克隆属性上按对象定位删除目标(Q4 仅修 (i))

- 原属性索引为负或克隆槽位越界时不删除任何修饰器
- 删除目标改为克隆属性同槽位的自身对象,不再跨对象拼接索引
- 回归构造 rebuildModifiers 重建后的负索引路径,含触发机制断言
This commit is contained in:
unanmed 2026-09-17 16:06:53 +08:00
parent d8fb6f088c
commit 3e68880e82
2 changed files with 43 additions and 1 deletions

View File

@ -7,11 +7,13 @@ import {
ItemCategory,
ItemStore,
ReplayCode,
SaveCompression,
TileStore,
TileType
} from '@user/data-common';
import { logger } from '@motajs/common';
import { HeroAttribute } from './attribute';
import { ValueModifier } from './modifier';
import { HeroEquipsStore } from './equipStore';
import { HeroEquipment } from './equipment';
import { EquipStatus } from './types';
@ -429,6 +431,41 @@ describe('HeroEquipment compare and guards', () => {
expect(diff.def).toBe(-3);
});
// 验证装备实例经 rebuildModifiers 重建后原属性已不含其修饰器,克隆上不会误删同名旁的修饰器
it('keeps foreign modifiers when the equipped instance was rebuilt', () => {
const env = createEnv();
registerItem(env, createItem(10, 'sword', [0], [['atk', 5]]));
registerItem(env, createItem(11, 'axe', [0], [['atk', 12]]));
registerItem(env, createItem(12, 'bow', [0], [['def', 3]]));
env.equipment.setSlots(['weapon']);
const sword = env.store.add(10);
// 英雄侧同名修饰器优先级低于装备修饰器,重建后排在克隆属性数组末尾
env.attribute.addModifier('atk', new ValueModifier(7, -1));
env.equipment.equip(sword, 0);
expect(env.attribute.getFinalAttribute('atk')).toBe(22);
const axe = env.store.add(11);
const bow = env.store.add(12);
// 现役装备的修饰器仍在原属性上时可被定位删除,差值等于斧加成减现役剑加成
expect(env.equipment.compareEquip(axe, bow, 0).atk).toBe(7);
// 让现役装备走一次读档重建,其修饰器变为新对象,而原属性上仍是旧对象
env.store.loadState(
env.store.saveState(SaveCompression.NoCompression),
SaveCompression.NoCompression
);
const rebuilt = [...env.store.get(sword)!.getModifiers()][0][1];
// 触发机制:重建后的修饰器无法在原属性上解析,删除目标只能退化为按槽位取值,
// 旧实现把该索引直接用在克隆属性上,负索引会删掉克隆属性末尾的英雄侧修饰器
expect(env.attribute.getModifierIndex(rebuilt)).toBe(-1);
const diff = env.equipment.compareEquip(axe, bow, 0);
// 原属性不含现役装备的修饰器时克隆上不删除任何修饰器,故差值只体现斧与弓的加成
expect(diff.atk).toBe(12);
expect(diff.def).toBe(-3);
});
// 验证装备实例缺失时卸下告警 146
it('warns code 146 when the equipped instance is missing', () => {
const env = createEnv();

View File

@ -279,8 +279,13 @@ export class HeroEquipment<THero> implements IHeroEquipment<THero> {
return {} as Partial<THero>;
}
for (const [name, modifier] of state.getModifiers()) {
// 索引取自原属性,为负时说明原属性已不含该修饰器对象,克隆上不得删除任何修饰器
const index = this.attribute.getModifierIndex(modifier);
clone.deleteModifierByIndex(name, index);
if (index < 0) continue;
// 克隆体按原属性同顺序重建修饰器数组,故取克隆自己的同槽位新对象作为删除目标
const cloned = [...clone.getModifiers(name)][index];
if (!cloned) continue;
clone.deleteModifier(name, cloned);
}
}