fix(07-13): prune deleted views and empty sourceless points

- deleteEnemy removes the deleted view from point.affectedBy and drops its point.damages entry so lazy refreshIndex never revisits a dead view (IN-01)
- deleteMapDamage removes the sourcelessDamage IPointInfo once both damages and affectedBy are empty, keeping markDirtyIndex
- add IN-01 regressions: dead-view witness (getDamageWithoutCheck not called) and the two arms of the sourceless empty-point cleanup
This commit is contained in:
unanmed 2026-09-17 17:23:01 +08:00
parent cff4291512
commit c1df1e0116
2 changed files with 52 additions and 0 deletions

View File

@ -404,6 +404,35 @@ describe('MapDamage sourceless damage', () => {
...fixture.damage.getSeparatedDamage({ x: 3, y: 2 })
]).toHaveLength(0);
});
// 验证删除无来源伤害的两条臂:删一条保留剩余,删净后返回空且可再次添加(IN-01 空点清理)
it('prunes the sourceless point once every damage is deleted', () => {
const fixture = createFixture();
fixture.damage.useReducer(fixture.reducer);
const first = createInfo(5);
const second = createInfo(3);
fixture.damage.addMapDamage(fixture.locator, first);
fixture.damage.addMapDamage(fixture.locator, second);
expect(fixture.damage.getReducedDamage(fixture.locator)?.damage).toBe(
8
);
fixture.damage.deleteMapDamage(fixture.locator, first);
expect(fixture.damage.getReducedDamage(fixture.locator)?.damage).toBe(
3
);
fixture.damage.deleteMapDamage(fixture.locator, second);
expect(fixture.damage.getReducedDamage(fixture.locator)).toBeNull();
expect([
...fixture.damage.getSeparatedDamage(fixture.locator)
]).toHaveLength(0);
fixture.damage.addMapDamage(fixture.locator, createInfo(6));
expect(fixture.damage.getReducedDamage(fixture.locator)?.damage).toBe(
6
);
});
});
describe('MapDamage sourced conversion and reduction', () => {
@ -545,6 +574,22 @@ describe('MapDamage sourced conversion and reduction', () => {
]).toHaveLength(0);
});
// 验证删除怪物后惰性重算不再触达被删视图(IN-01 的删除端剪除)
it('does not rebuild damage from a deleted view after pruning', () => {
const fixture = createFixture();
fixture.damage.useReducer(fixture.reducer);
fixture.damage.useConverter(fixture.converter);
const spy = vi.spyOn(fixture.damageView, 'getDamageWithoutCheck');
fixture.damage.deleteEnemy(fixture.view);
expect(fixture.damage.getReducedDamage(fixture.locator)).toBeNull();
expect(spy).not.toHaveBeenCalled();
expect([
...fixture.damage.getSeparatedDamage(fixture.locator)
]).toHaveLength(0);
});
// 验证伤害来源消失后旧坐标不再返回幽灵伤害(CR-02 视图集变空)
it('drops ghost damage when the enemy view set becomes empty', () => {
const fixture = createFixture();

View File

@ -116,6 +116,9 @@ export class MapDamage<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
const current = this.sourcelessDamage.get(index);
if (!current) return;
current.damages.delete(info);
if (current.damages.size === 0 && current.affectedBy.size === 0) {
this.sourcelessDamage.delete(index);
}
this.markDirtyIndex(index);
}
@ -157,6 +160,10 @@ export class MapDamage<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
affecting.damages.forEach((dam, index) => {
this.damageStore.delete(dam);
collection.add(index);
const point = this.sourcedDamage.get(index);
if (!point) return;
point.affectedBy.delete(viewItem);
point.damages.delete(dam);
});
this.viewStore.delete(viewItem);
}