test(06-01): cover MapDamage and EnemyContext behavior

- add mapDamage.test.ts for sourceless add/delete, sourced conversion, reduction caching, warns 102/103/104 and the #06-01-2 deleteEnemy finding
- add context.test.ts for the enemy registry (register/lookup/unknown-null/delete/resize/scan) and effect-registry add/remove behavior
- extend the 06-TEST-FINDINGS.md sink with the #06-01-2 row
This commit is contained in:
unanmed 2026-09-14 08:50:05 +08:00
parent 15f329b61d
commit 484da2db19
3 changed files with 780 additions and 0 deletions

View File

@ -24,3 +24,4 @@
| 模块/接口 | 现象 | 最小复现 | 疑似原因 | 影响面 | 建议修复方向 | 关联 skip 用例 | 严重度 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `damage.ts` `DamageContext.calculateCritical` / `findNextCritical` | 产出的临界点 `info` 与 `nextValue` 不对应 | 假计算器 `damage = 100 - atk * 10`、`getCriticalLimit = 10`、当前 `atk = 0`;首个临界点 `nextValue = 1`(正确伤害应为 90),但 `info.damage = 0`、`damageDiff = -100` | `findNextCritical` 在 `middleInfo.damage < referenceDamage` 分支只更新 `right`,从不更新 `targetInfo`,`targetInfo` 一直停留在初始 `upperLimit` 处的计算结果 | 临界点查询(数值提示、战斗模拟)展示的伤害与对应属性值不匹配 | 在 `middleInfo.damage < referenceDamage` 分支同步 `targetInfo = middleInfo`,或二分结束后按最终 `right` 重新计算一次 | `damage.test.ts` `reports the damage info matching the yielded critical value`(#06-01-1) | 中 |
| `mapDamage.ts` `MapDamage.deleteEnemy` | 删除怪物后,该怪物产生的有来源地图伤害仍然存在 | 注册一个怪物视图并转换出伤害(假视图伤害 7)→ `getSeparatedDamage` 有 1 条 → `deleteEnemy(view)` → `getSeparatedDamage` 仍有 1 条(正确应为 0) | `viewStore` 与 `damageStore` 只在读/删路径出现,全文件没有任何 `.set(...)` 写入,因此 `deleteEnemy` / `removeEnemyAffecting` 中 `viewStore.get(viewItem)` 恒为 `undefined`,清理循环被整体跳过;`sourcedDamage` 点位与 `affectedBy` 都不会被移除 | 怪物死亡/离场后地图伤害不消失,`getSeparatedDamage` / `getReducedDamage` 继续返回幽灵伤害,并会污染后续 `refreshIndex` 的重新聚合 | 在 `refreshEnemy` / `refreshEnemyAndClearCache` 建立 sourced 伤害时同步写入 `viewStore.set(viewItem, { damages, enemy })` 与 `damageStore.set(damage, { sourceView, sourceEnemy, index })`,或改为在 `deleteEnemy` 中直接遍历 `sourcedDamage` 的 `affectedBy` 反向清理 | `mapDamage.test.ts` `removes enemy-sourced damage when the enemy is deleted`(#06-01-2) | 中 |

View File

@ -0,0 +1,339 @@
// 测试怪物上下文:注册与查询、未知查找、删除、resize、范围遍历与各类效果注册表增删
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { type IEnemy, type IStateBase } from '@user/data-base';
import {
type IAuraConverter,
type IEnemyCommonQueryEffect,
type IEnemyFinalEffect,
type IEnemySpecialQueryEffect
} from './types';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
Map.prototype.getOrInsertComputed ??= function <K, V>(
this: Map<K, V>,
key: K,
callback: (key: K) => V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
const value = callback(key);
this.set(key, value);
return value;
};
Map.prototype.getOrInsert ??= function <K, V>(
this: Map<K, V>,
key: K,
defaultValue: V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
this.set(key, defaultValue);
return defaultValue;
};
});
interface TestEnemyAttr {
/** 怪物生命值 */
hp: number;
/** 怪物攻击力 */
atk: number;
/** 怪物防御力 */
def: number;
}
interface TestHeroAttr {
/** 勇士生命值 */
hp: number;
/** 勇士攻击力 */
atk: number;
/** 勇士防御力 */
def: number;
}
interface TestModules {
EnemyContext: typeof import('./context').EnemyContext;
Enemy: typeof import('@user/data-base').Enemy;
HeroAttribute: typeof import('@user/data-base').HeroAttribute;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const contextModule = await import('./context');
const baseModule = await import('@user/data-base');
modules = {
EnemyContext: contextModule.EnemyContext,
Enemy: baseModule.Enemy,
HeroAttribute: baseModule.HeroAttribute
};
});
interface FakeSpecial {
/** 特殊属性代码 */
readonly code: number;
/**
* 深拷贝此特殊属性
*/
clone(): FakeSpecial;
}
/**
* 创建一个最小特殊属性对象,只用于驱动注册表的匹配路径
* @param code 特殊属性代码
*/
function createSpecial(code: number): FakeSpecial {
return { code, clone: () => createSpecial(code) };
}
/**
* 创建一个怪物对象
* @param id 怪物 id
* @param code 怪物图块数字
*/
function createEnemy(id: string, code: number): IEnemy<TestEnemyAttr> {
return new modules.Enemy<TestEnemyAttr>(id, code, {
hp: 10,
atk: 2,
def: 0
});
}
interface ContextFixture {
/** 被测怪物上下文 */
context: InstanceType<TestModules['EnemyContext']>;
/** 可修改勇士属性 */
hero: InstanceType<TestModules['HeroAttribute']>;
}
/**
* 创建怪物上下文测试夹具,尺寸默认按 4x3 初始化
* @param width 上下文宽度
* @param height 上下文高度
*/
function createContext(width: number = 4, height: number = 3): ContextFixture {
const state = {} as IStateBase;
const context = new modules.EnemyContext<TestEnemyAttr, TestHeroAttr>(
state
);
context.resize(width, height);
const hero = new modules.HeroAttribute<TestHeroAttr>({
hp: 100,
atk: 0,
def: 0
});
return { context, hero };
}
describe('EnemyContext registry', () => {
// 验证注册怪物后可按定位符、坐标与计算后怪物反查
it('registers an enemy and resolves it from locator, point and computed', () => {
const fixture = createContext();
const enemy = createEnemy('e1', 1);
fixture.context.setEnemyAt({ x: 1, y: 0 }, enemy);
const view = fixture.context.getEnemyByLocator({ x: 1, y: 0 });
expect(view).not.toBeNull();
expect(fixture.context.getEnemyByLoc(1, 0)).toBe(view);
expect(fixture.context.getEnemyLocatorByView(view!)).toEqual({
x: 1,
y: 0
});
expect(fixture.context.getEnemyLocator(enemy)).toEqual({ x: 1, y: 0 });
expect(fixture.context.getViewByComputed(view!.getComputedEnemy())).toBe(
view
);
});
// 验证未知定位符、未知坐标与未知怪物对象均返回 null
it('returns null for unknown lookups', () => {
const fixture = createContext();
const other = createEnemy('other', 2);
expect(fixture.context.getEnemyByLocator({ x: 3, y: 2 })).toBeNull();
expect(fixture.context.getEnemyByLoc(3, 2)).toBeNull();
expect(fixture.context.getViewByComputed(other)).toBeNull();
expect(fixture.context.getEnemyLocator(other)).toBeNull();
});
// 验证同一坐标重复注册会替换原有怪物及其映射
it('replaces the enemy registered at the same point', () => {
const fixture = createContext();
const first = createEnemy('first', 1);
const second = createEnemy('second', 2);
fixture.context.setEnemyAt({ x: 0, y: 0 }, first);
fixture.context.setEnemyAt({ x: 0, y: 0 }, second);
expect(fixture.context.getEnemyLocator(first)).toBeNull();
expect(fixture.context.getEnemyLocator(second)).toEqual({ x: 0, y: 0 });
});
// 验证删除怪物会同时移除视图与定位符映射
it('removes view and locator mappings when deleting an enemy', () => {
const fixture = createContext();
const enemy = createEnemy('e1', 1);
fixture.context.setEnemyAt({ x: 1, y: 0 }, enemy);
const view = fixture.context.getEnemyByLocator({ x: 1, y: 0 })!;
const computed = view.getComputedEnemy();
fixture.context.deleteEnemy({ x: 1, y: 0 });
expect(fixture.context.getEnemyByLocator({ x: 1, y: 0 })).toBeNull();
expect(fixture.context.getEnemyLocatorByView(view)).toBeNull();
expect(fixture.context.getEnemyLocator(enemy)).toBeNull();
expect(fixture.context.getViewByComputed(computed)).toBeNull();
});
// 验证 resize 会清空怪物并更新尺寸与索引宽度
it('resizes by clearing enemies and updating dimensions', () => {
const fixture = createContext(4, 3);
fixture.context.setEnemyAt({ x: 1, y: 0 }, createEnemy('e1', 1));
fixture.context.resize(5, 2);
expect(fixture.context.width).toBe(5);
expect(fixture.context.height).toBe(2);
expect(fixture.context.indexer.locToIndex(3, 1)).toBe(8);
expect(fixture.context.getEnemyByLocator({ x: 1, y: 0 })).toBeNull();
});
// 验证 scanRange 与 iterateEnemy 会遍历全部已注册怪物
it('scans and iterates all registered enemies', () => {
const fixture = createContext();
fixture.context.setEnemyAt({ x: 0, y: 0 }, createEnemy('first', 1));
fixture.context.setEnemyAt({ x: 2, y: 1 }, createEnemy('second', 2));
const range = {
bindHost: () => {},
autoDetect: (list: Set<number>) => [...list]
};
const scanned = [...fixture.context.scanRange(range as never, 0)];
const iterated = [...fixture.context.iterateEnemy()];
expect(scanned).toHaveLength(2);
expect(iterated).toHaveLength(2);
expect(iterated.map(([locator]) => locator)).toEqual([
{ x: 0, y: 0 },
{ x: 2, y: 1 }
]);
});
// 验证绑定与解绑勇士对象会同步更新查询结果
it('binds and clears the bound hero', () => {
const fixture = createContext();
fixture.context.bindHero(fixture.hero);
expect(fixture.context.getBindedHero()).toBe(fixture.hero);
fixture.context.bindHero(null);
expect(fixture.context.getBindedHero()).toBeNull();
});
});
describe('EnemyContext effect registries', () => {
// 验证注册的最终效果会在构建时执行,注销后不再执行
it('runs registered final effects until they are unregistered', () => {
const fixture = createContext();
fixture.context.bindHero(fixture.hero);
fixture.context.setEnemyAt({ x: 0, y: 0 }, createEnemy('e1', 1));
const applied: number[] = [];
const effect: IEnemyFinalEffect<TestEnemyAttr, TestHeroAttr> = {
priority: 1,
apply: () => {
applied.push(1);
}
};
fixture.context.registerFinalEffect(effect);
fixture.context.buildup();
expect(applied).toHaveLength(1);
fixture.context.unregisterFinalEffect(effect);
fixture.context.buildup();
expect(applied).toHaveLength(1);
});
// 验证注册的常规查询效果会按特殊属性代码执行,注销后不再执行
it('runs common query effects for matching specials until unregistered', () => {
const fixture = createContext();
fixture.context.bindHero(fixture.hero);
const enemy = createEnemy('e1', 1);
enemy.addSpecial(createSpecial(20) as never);
fixture.context.setEnemyAt({ x: 0, y: 0 }, enemy);
const queried: number[] = [];
const effect: IEnemyCommonQueryEffect<TestEnemyAttr, TestHeroAttr> = {
priority: 1,
apply: () => {
queried.push(1);
}
};
fixture.context.registerCommonQueryEffect(20, effect);
fixture.context.buildup();
expect(queried).toHaveLength(1);
fixture.context.unregisterCommonQueryEffect(20, effect);
fixture.context.buildup();
expect(queried).toHaveLength(1);
});
// 验证注册的特殊查询效果会在构建时构造修饰器,注销后不再构造
it('builds special query modifiers until unregistered', () => {
const fixture = createContext();
fixture.context.bindHero(fixture.hero);
fixture.context.setEnemyAt({ x: 0, y: 0 }, createEnemy('e1', 1));
const forCalls: number[] = [];
const modifier = {
add: () => [],
delete: () => [],
modify: () => false,
shouldQuery: () => false
};
const effect: IEnemySpecialQueryEffect<TestEnemyAttr, TestHeroAttr> = {
priority: 5,
for: () => {
forCalls.push(1);
return modifier;
}
};
fixture.context.registerSpecialQueryEffect(effect);
fixture.context.buildup();
expect(forCalls).toHaveLength(1);
fixture.context.unregisterSpecialQueryEffect(effect);
fixture.context.buildup();
expect(forCalls).toHaveLength(1);
});
// 验证注册的光环转换器会参与特殊属性转换,注销后不再参与
it('consults aura converters until they are unregistered', () => {
const fixture = createContext();
fixture.context.bindHero(fixture.hero);
const enemy = createEnemy('e1', 1);
enemy.addSpecial(createSpecial(20) as never);
fixture.context.setEnemyAt({ x: 0, y: 0 }, enemy);
const consulted: number[] = [];
const converter: IAuraConverter<TestEnemyAttr, TestHeroAttr> = {
shouldConvert: () => {
consulted.push(1);
return false;
},
convert: () => null as never
};
fixture.context.registerAuraConverter(converter);
fixture.context.buildup();
expect(consulted).toHaveLength(1);
fixture.context.unregisterAuraConverter(converter);
fixture.context.buildup();
expect(consulted).toHaveLength(1);
});
});

View File

@ -0,0 +1,440 @@
// 测试地图伤害:无来源伤害增删、有来源转换与合并、告警码 102/103/104、分离伤害合并与 deleteEnemy
import { beforeAll, describe, expect, it, vi } from 'vitest';
import {
type IRange,
type ITileLocator,
type IRangeHost
} from '@motajs/common';
import { type IEnemy, type IStateBase } from '@user/data-base';
import {
type IEnemyContext,
type IEnemyView,
type IMapDamageConverter,
type IMapDamageInfo,
type IMapDamageReducer,
type IMapDamageView
} from './types';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
Map.prototype.getOrInsertComputed ??= function <K, V>(
this: Map<K, V>,
key: K,
callback: (key: K) => V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
const value = callback(key);
this.set(key, value);
return value;
};
Map.prototype.getOrInsert ??= function <K, V>(
this: Map<K, V>,
key: K,
defaultValue: V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
this.set(key, defaultValue);
return defaultValue;
};
});
interface TestEnemyAttr {
/** 怪物生命值 */
hp: number;
/** 怪物攻击力 */
atk: number;
/** 怪物防御力 */
def: number;
}
interface TestHeroAttr {
/** 勇士生命值 */
hp: number;
/** 勇士攻击力 */
atk: number;
/** 勇士防御力 */
def: number;
}
interface TestModules {
MapDamage: typeof import('./mapDamage').MapDamage;
MapLocIndexer: typeof import('@user/data-common').MapLocIndexer;
HeroAttribute: typeof import('@user/data-base').HeroAttribute;
Enemy: typeof import('@user/data-base').Enemy;
logger: typeof import('@motajs/common').logger;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const mapDamageModule = await import('./mapDamage');
const commonModule = await import('@user/data-common');
const motaModule = await import('@motajs/common');
const baseModule = await import('@user/data-base');
modules = {
MapDamage: mapDamageModule.MapDamage,
MapLocIndexer: commonModule.MapLocIndexer,
HeroAttribute: baseModule.HeroAttribute,
Enemy: baseModule.Enemy,
logger: motaModule.logger
};
});
/**
* 构造一条地图伤害信息
* @param damage 伤害值
*/
function createInfo(damage: number): IMapDamageInfo {
return {
damage,
type: 0,
extra: { catch: new Set(), repulse: new Set() }
};
}
interface IFakeRange {
/**
* 绑定宿主对象
* @param host 宿主对象
*/
bindHost(host: IRangeHost): void;
/**
* 迭代范围内的坐标索引
* @param param 范围参数
*/
iterateLoc(param: number): Iterable<number>;
}
/**
* 只覆盖固定坐标索引集合的测试范围,用于驱动有来源伤害转换
*/
class FakeRange implements IFakeRange {
/** 该范围覆盖的坐标索引 */
readonly indexes: number[];
constructor(indexes: number[]) {
this.indexes = indexes;
}
bindHost(_host: IRangeHost): void {}
*iterateLoc(): Iterable<number> {
yield* this.indexes;
}
}
/**
* 固定返回一条地图伤害的测试视图
*/
class FakeView implements IMapDamageView<number> {
/** 该视图产生的伤害值 */
readonly value: number;
/** 该视图影响的范围 */
readonly range: FakeRange;
constructor(value: number, indexes: number[]) {
this.value = value;
this.range = new FakeRange(indexes);
}
getRange(): IRange<number> {
return this.range as never;
}
getRangeParam(): number {
return 0;
}
getDamageAt(_locator: ITileLocator): Readonly<IMapDamageInfo> | null {
return createInfo(this.value);
}
getDamageWithoutCheck(
_locator: ITileLocator
): Readonly<IMapDamageInfo> | null {
return createInfo(this.value);
}
}
/**
* 固定输出伤害视图列表的测试转换器
*/
class FakeConverter
implements IMapDamageConverter<TestEnemyAttr, TestHeroAttr>
{
/** 转换输出的伤害视图 */
readonly views: IMapDamageView<number>[];
constructor(views: IMapDamageView<number>[]) {
this.views = views;
}
convert(): IMapDamageView<number>[] {
return this.views;
}
}
/**
* 求和式测试合并器,并记录调用次数
*/
class FakeReducer implements IMapDamageReducer {
/** reduce 调用次数 */
calls: number = 0;
reduce(
info: Iterable<Readonly<IMapDamageInfo>>
): Readonly<IMapDamageInfo> {
this.calls++;
let damage = 0;
for (const item of info) {
damage += item.damage;
}
return createInfo(damage);
}
}
interface MapDamageFixture {
/** 被测地图伤害对象 */
damage: InstanceType<TestModules['MapDamage']>;
/** 怪物上下文假对象 */
context: IEnemyContext<TestEnemyAttr, TestHeroAttr>;
/** 测试转换器 */
converter: FakeConverter;
/** 测试合并器 */
reducer: FakeReducer;
/** 已注册的怪物视图 */
view: IEnemyView<TestEnemyAttr>;
/** 转换出的伤害视图 */
damageView: FakeView;
/** 已注册怪物的位置 */
locator: ITileLocator;
/** 可修改原始怪物 */
origin: IEnemy<TestEnemyAttr>;
}
/**
* 创建地图伤害测试夹具,怪物上下文只实现被测路径所需的最小接口
*/
function createFixture(): MapDamageFixture {
const state = {} as IStateBase;
const indexer = new modules.MapLocIndexer();
indexer.setWidth(4);
const hero = new modules.HeroAttribute<TestHeroAttr>({
hp: 100,
atk: 0,
def: 0
});
const origin = new modules.Enemy<TestEnemyAttr>('enemy-1', 1, {
hp: 10,
atk: 2,
def: 0
});
const locator: ITileLocator = { x: 1, y: 0 };
const enemies = new Map<IEnemyView<TestEnemyAttr>, ITileLocator>();
const view: IEnemyView<TestEnemyAttr> = {
context: {} as never,
reset: () => {},
getBaseEnemy: () => origin,
getComputedEnemy: () => origin,
getModifiableEnemy: () => origin,
markDirty: () => {}
};
enemies.set(view, locator);
const damageView = new FakeView(
7,
[indexer.locToIndex(locator.x, locator.y)]
);
const context = {
state,
indexer,
width: 4,
height: 3,
getBindedHero: () => hero,
getEnemyLocatorByView: (target: IEnemyView<TestEnemyAttr>) =>
enemies.get(target) ?? null,
getEnemyLocator: (target: IEnemy<TestEnemyAttr>) =>
target === origin ? locator : null,
getViewByComputed: () => view,
*iterateEnemy(): Iterable<
[ITileLocator, IEnemyView<TestEnemyAttr>]
> {
for (const [target, targetLocator] of enemies) {
yield [targetLocator, target];
}
}
} as never;
const damage = new modules.MapDamage<TestEnemyAttr, TestHeroAttr>(
context
);
return {
damage,
context,
converter: new FakeConverter([damageView]),
reducer: new FakeReducer(),
view,
damageView,
locator,
origin
};
}
/**
* 创建一个未注册到夹具中的怪物视图,用于验证未知定位符路径
*/
function createUnknownView(fixture: MapDamageFixture): IEnemyView<TestEnemyAttr> {
return {
context: {} as never,
reset: () => {},
getBaseEnemy: () => fixture.origin,
getComputedEnemy: () => fixture.origin,
getModifiableEnemy: () => fixture.origin,
markDirty: () => {}
};
}
describe('MapDamage sourceless damage', () => {
// 验证添加与删除无来源地图伤害会同步更新分离伤害列表
it('adds and deletes sourceless damage at a point', () => {
const fixture = createFixture();
fixture.damage.useReducer(fixture.reducer);
const info = createInfo(5);
fixture.damage.addMapDamage({ x: 0, y: 0 }, info);
expect(
[...fixture.damage.getSeparatedDamage({ x: 0, y: 0 })]
).toContain(info);
fixture.damage.deleteMapDamage({ x: 0, y: 0 }, info);
expect(
[...fixture.damage.getSeparatedDamage({ x: 0, y: 0 })]
).toHaveLength(0);
});
// 验证删除不存在的无来源伤害是安全操作
it('ignores deleting sourceless damage that was never added', () => {
const fixture = createFixture();
fixture.damage.useReducer(fixture.reducer);
fixture.damage.deleteMapDamage({ x: 3, y: 2 }, createInfo(5));
expect(
[...fixture.damage.getSeparatedDamage({ x: 3, y: 2 })]
).toHaveLength(0);
});
});
describe('MapDamage sourced conversion and reduction', () => {
// 验证转换出的有来源伤害可被分离查询与合并查询获取
it('converts sourced damage and reduces it through the reducer', () => {
const fixture = createFixture();
fixture.damage.useReducer(fixture.reducer);
fixture.damage.useConverter(fixture.converter);
const separated = [
...fixture.damage.getSeparatedDamage(fixture.locator)
];
const reduced = fixture.damage.getReducedDamage(fixture.locator);
expect(separated).toHaveLength(1);
expect(separated[0].damage).toBe(7);
expect(reduced?.damage).toBe(7);
expect(fixture.reducer.calls).toBe(1);
});
// 验证同一坐标上无来源与有来源伤害会被合并查询同时返回
it('unions sourceless and sourced damage at the same point', () => {
const fixture = createFixture();
fixture.damage.useReducer(fixture.reducer);
fixture.damage.useConverter(fixture.converter);
fixture.damage.addMapDamage(fixture.locator, createInfo(3));
expect(
[...fixture.damage.getSeparatedDamage(fixture.locator)]
).toHaveLength(2);
const reduced = fixture.damage.getReducedDamage(fixture.locator);
expect(reduced?.damage).toBe(10);
});
// 验证合并结果命中缓存,markDirty 后按新数据重新合并
it('caches the reduced damage until the point is marked dirty', () => {
const fixture = createFixture();
fixture.damage.useReducer(fixture.reducer);
fixture.damage.addMapDamage({ x: 0, y: 0 }, createInfo(3));
const first = fixture.damage.getReducedDamage({ x: 0, y: 0 });
const second = fixture.damage.getReducedDamage({ x: 0, y: 0 });
expect(second).toBe(first);
expect(fixture.reducer.calls).toBe(1);
fixture.damage.markDirty({ x: 0, y: 0 });
const third = fixture.damage.getReducedDamage({ x: 0, y: 0 });
expect(third).not.toBe(first);
expect(third?.damage).toBe(3);
expect(fixture.reducer.calls).toBe(2);
});
// 验证没有任何分离伤害时合并查询返回 null
it('returns null when a point has no separated damage', () => {
const fixture = createFixture();
fixture.damage.useReducer(fixture.reducer);
expect(fixture.damage.getReducedDamage({ x: 3, y: 2 })).toBeNull();
});
// 验证未设置转换器时全量刷新告警 102 并直接返回
it('warns 102 when the converter is missing during a full refresh', () => {
const fixture = createFixture();
const result = modules.logger.catch(() => fixture.damage.refreshAll());
expect(result.info.map(v => v.code)).toContain(102);
});
// 验证未设置合并器时合并查询告警 103 并返回 null
it('warns 103 when the reducer is missing', () => {
const fixture = createFixture();
const result = modules.logger.catch(() =>
fixture.damage.getReducedDamage({ x: 0, y: 0 })
);
expect(result.ret).toBeNull();
expect(result.info.map(v => v.code)).toContain(103);
});
// 验证既无伤害来源又无定位符时标记怪物脏会告警 104
it('warns 104 when marking an unregistered enemy dirty', () => {
const fixture = createFixture();
const result = modules.logger.catch(() =>
fixture.damage.markEnemyDirty(createUnknownView(fixture))
);
expect(result.info.map(v => v.code)).toContain(104);
});
// 疑似 bug:deleteEnemy 应移除该怪物带来的有来源地图伤害,详见 06-TEST-FINDINGS.md #06-01-2,修复后取消 skip
it.skip('removes enemy-sourced damage when the enemy is deleted', () => {
const fixture = createFixture();
fixture.damage.useReducer(fixture.reducer);
fixture.damage.useConverter(fixture.converter);
expect(
[...fixture.damage.getSeparatedDamage(fixture.locator)]
).toHaveLength(1);
fixture.damage.deleteEnemy(fixture.view);
expect(
[...fixture.damage.getSeparatedDamage(fixture.locator)]
).toHaveLength(0);
});
});