test(06): fix lint and type errors in data-layer test files

This commit is contained in:
unanmed 2026-09-14 14:03:25 +08:00
parent 693b337e24
commit 136a9845d6
8 changed files with 284 additions and 189 deletions

View File

@ -103,13 +103,17 @@ function createEnemy(attrs: Partial<IEnemyAttr> = {}): FakeEnemy {
addSpecial: () => {},
deleteSpecial: () => {},
setAttribute: (key: string, value: unknown) => {
(values as never)[key] = value;
// 假怪物按动态字符串键读写属性,固定形状的 IEnemyAttr 无法表达字符串索引
(values as unknown as Record<string, unknown>)[key] = value;
},
addAttribute: (key: string, value: number) => {
(values as never)[key] += value;
(values as unknown as Record<string, number>)[key] += value;
},
copyFrom: () => {},
saveState: () => ({ attrs: structuredClone(values), specials: new Map() }),
saveState: () => ({
attrs: structuredClone(values),
specials: new Map()
}),
loadState: () => {}
} as never;
return { enemy, attrs: values };
@ -129,7 +133,9 @@ function createSpecial<T>(code: number, value: T): ISpecial<T> {
* @param resolve 根据计算后怪物对象查找视图的回调
*/
function createContext(
resolve?: (enemy: IReadonlyEnemy<IEnemyAttr>) => IEnemyView<IEnemyAttr> | null
resolve?: (
enemy: IReadonlyEnemy<IEnemyAttr>
) => IEnemyView<IEnemyAttr> | null
): IEnemyContext<IEnemyAttr, IHeroAttr> {
return {
width: 8,
@ -192,8 +198,8 @@ describe('CommonAuraConverter', () => {
const halo = createSpecial(25, createHalo());
const guard = createSpecial(26, undefined);
expect(converter.shouldConvert(halo, {} as never)).toBe(true);
expect(converter.shouldConvert(guard, {} as never)).toBe(false);
expect(converter.shouldConvert(halo)).toBe(true);
expect(converter.shouldConvert(guard)).toBe(false);
});
// 验证转换结果为携带怪物、特殊属性与定位符的光环视图
@ -234,10 +240,7 @@ describe('CommonAura range', () => {
const { enemy } = createEnemy();
const aura = new modules.CommonAura(
enemy,
createSpecial(
25,
createHalo({ haloRange: 2, haloSquare: true })
),
createSpecial(25, createHalo({ haloRange: 2, haloSquare: true })),
{ x: 3, y: 4 }
);
@ -250,10 +253,7 @@ describe('CommonAura range', () => {
const { enemy } = createEnemy();
const aura = new modules.CommonAura(
enemy,
createSpecial(
25,
createHalo({ haloRange: 2, haloSquare: false })
),
createSpecial(25, createHalo({ haloRange: 2, haloSquare: false })),
{ x: 3, y: 4 }
);
@ -276,7 +276,10 @@ describe('CommonAura apply', () => {
{ x: 0, y: 0 }
);
aura.apply(createWritableHandler(target.enemy, { x: 1, y: 0 }), base.enemy);
aura.apply(
createWritableHandler(target.enemy, { x: 1, y: 0 }),
base.enemy
);
expect(target.attrs.hp).toBe(30);
expect(target.attrs.atk).toBe(33);
@ -293,7 +296,10 @@ describe('CommonAura apply', () => {
{ x: 0, y: 0 }
);
aura.apply(createWritableHandler(target.enemy, { x: 1, y: 0 }), base.enemy);
aura.apply(
createWritableHandler(target.enemy, { x: 1, y: 0 }),
base.enemy
);
expect(target.attrs).toMatchObject({ hp: 20, atk: 8, def: 5 });
});
@ -319,12 +325,12 @@ describe('GuardAuraConverter', () => {
it('converts only special code 26', () => {
const converter = new modules.GuardAuraConverter();
expect(
converter.shouldConvert(createSpecial(26, undefined), {} as never)
).toBe(true);
expect(
converter.shouldConvert(createSpecial(25, undefined), {} as never)
).toBe(false);
expect(converter.shouldConvert(createSpecial(26, undefined))).toBe(
true
);
expect(converter.shouldConvert(createSpecial(25, undefined))).toBe(
false
);
});
// 验证转换结果为携带上下文、怪物、特殊属性与定位符的支援光环视图
@ -376,9 +382,9 @@ describe('GuardAura', () => {
aura.apply(createWritableHandler(target.enemy, { x: 2, y: 1 }));
expect((target.attrs.guard as Set<ITileLocator>).has(sourceLocator)).toBe(
true
);
expect(
(target.attrs.guard as Set<ITileLocator>).has(sourceLocator)
).toBe(true);
});
// 验证支援光环不会把来源定位符添加到自身

View File

@ -97,7 +97,7 @@ function createEnemy(options: FakeEnemyOptions = {}): FakeEnemy {
code,
value: specials.get(code),
deepEqualsTo: () => false
} as ISpecial<any>)
} as unknown as ISpecial<any>)
: null,
hasSpecial: (code: number) => specials.has(code),
iterateSpecials: () => [],
@ -107,10 +107,11 @@ function createEnemy(options: FakeEnemyOptions = {}): FakeEnemy {
addSpecial: () => {},
deleteSpecial: () => {},
setAttribute: (key: string, value: unknown) => {
(attrs as never)[key] = value;
// 假怪物按动态字符串键读写属性,固定形状的 IEnemyAttr 无法表达字符串索引
(attrs as unknown as Record<string, unknown>)[key] = value;
},
addAttribute: (key: string, value: number) => {
(attrs as never)[key] += value;
(attrs as unknown as Record<string, number>)[key] += value;
},
copyFrom: () => {},
saveState: () => ({
@ -145,7 +146,7 @@ function createHero(
return {
getBaseAttribute: (name: string) => (values as never)[name],
getFinalAttribute: (name: string) => (values as never)[name]
} as IReadonlyHeroAttribute<IHeroAttr>;
} as unknown as IReadonlyHeroAttribute<IHeroAttr>;
}
interface FakeStateOptions {
@ -196,12 +197,10 @@ function createContext(
* 将一个计算后怪物对象包装为只提供该对象读取的假视图
* @param computed 计算后怪物对象
*/
function createView(
computed: IEnemy<IEnemyAttr>
): IEnemyView<IEnemyAttr> {
function createView(computed: IEnemy<IEnemyAttr>): IEnemyView<IEnemyAttr> {
return {
getComputedEnemy: () => computed
} as IEnemyView<IEnemyAttr>;
} as unknown as IEnemyView<IEnemyAttr>;
}
interface HandlerOptions {
@ -244,9 +243,7 @@ function specialsOf(entries: Array<[number, unknown]>): Map<number, unknown> {
/**
* 创建一个计算器实例
*/
function createCalculator(): InstanceType<
TestModules['MainDamageCalculator']
> {
function createCalculator(): InstanceType<TestModules['MainDamageCalculator']> {
return new modules.MainDamageCalculator();
}
@ -273,7 +270,9 @@ describe('MainDamageCalculator base and defeat branches', () => {
// 验证无敌怪物在没有十字架时不可战胜,持有十字架后恢复正常计算
it('blocks 无敌 without a cross and allows it with one', () => {
const { enemy } = createEnemy({ specials: specialsOf([[20, undefined]]) });
const { enemy } = createEnemy({
specials: specialsOf([[20, undefined]])
});
const calculator = createCalculator();
const blocked = calculator.calculate(createHandler({ enemy }));
@ -287,7 +286,9 @@ describe('MainDamageCalculator base and defeat branches', () => {
// 验证魔攻怪物的每轮伤害不减免勇士防御
it('ignores hero defense for 魔攻', () => {
const { enemy } = createEnemy({ specials: specialsOf([[2, undefined]]) });
const { enemy } = createEnemy({
specials: specialsOf([[2, undefined]])
});
const calculator = createCalculator();
const info = calculator.calculate(createHandler({ enemy }));
@ -301,12 +302,12 @@ describe('MainDamageCalculator base and defeat branches', () => {
const double = createEnemy({ specials: specialsOf([[4, undefined]]) });
const triple = createEnemy({ specials: specialsOf([[5, undefined]]) });
expect(calculator.calculate(createHandler({ enemy: double.enemy }))).toEqual(
{ damage: 6, turn: 2 }
);
expect(calculator.calculate(createHandler({ enemy: triple.enemy }))).toEqual(
{ damage: 9, turn: 2 }
);
expect(
calculator.calculate(createHandler({ enemy: double.enemy }))
).toEqual({ damage: 6, turn: 2 });
expect(
calculator.calculate(createHandler({ enemy: triple.enemy }))
).toEqual({ damage: 9, turn: 2 });
});
// 验证多段伤害按特殊属性数值倍乘每轮伤害
@ -334,9 +335,7 @@ describe('MainDamageCalculator support and additive branches', () => {
});
const calculator = createCalculator();
const info = calculator.calculate(
createHandler({ enemy, context })
);
const info = calculator.calculate(createHandler({ enemy, context }));
expect(info).toEqual({ damage: 12, turn: 4 });
expect(calls).toEqual(['1,0']);
@ -378,7 +377,9 @@ describe('MainDamageCalculator support and additive branches', () => {
// 验证先攻会额外附加一次每轮伤害
it('adds one enemy hit for 先攻', () => {
const { enemy } = createEnemy({ specials: specialsOf([[1, undefined]]) });
const { enemy } = createEnemy({
specials: specialsOf([[1, undefined]])
});
const calculator = createCalculator();
const info = calculator.calculate(createHandler({ enemy }));
@ -500,7 +501,9 @@ describe('MainDamageCalculator vampire and bypass branches', () => {
// 验证仇恨按 flag 字段值附加伤害且不受魔防影响
it('adds the hatred flag value for 仇恨', () => {
const { enemy } = createEnemy({ specials: specialsOf([[17, undefined]]) });
const { enemy } = createEnemy({
specials: specialsOf([[17, undefined]])
});
const calculator = createCalculator();
const info = calculator.calculate(

View File

@ -2,10 +2,7 @@
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { type IEnemyAttr, type IHeroAttr } from '@user/data-common';
import { type IEnemyHandler } from '@user/data-system';
import {
type IEnemy,
type IReadonlyHeroAttribute
} from '@user/data-base';
import { type IEnemy, type IReadonlyHeroAttribute } from '@user/data-base';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
@ -86,13 +83,17 @@ function createEnemy(
addSpecial: () => {},
deleteSpecial: () => {},
setAttribute: (key: string, value: unknown) => {
(values as never)[key] = value;
// 假怪物按动态字符串键读写属性,固定形状的 IEnemyAttr 无法表达字符串索引
(values as unknown as Record<string, unknown>)[key] = value;
},
addAttribute: (key: string, value: number) => {
(values as never)[key] += value;
(values as unknown as Record<string, number>)[key] += value;
},
copyFrom: () => {},
saveState: () => ({ attrs: structuredClone(values), specials: new Map() }),
saveState: () => ({
attrs: structuredClone(values),
specials: new Map()
}),
loadState: () => {}
} as never;
return { enemy, attrs: values };

View File

@ -6,11 +6,9 @@ import {
type IDamageSystem,
type IEnemyContext,
type IEnemyView,
type IMapDamageInfo,
type IReadonlyEnemyHandler
type IMapDamageInfo
} from '@user/data-system';
import {
type IEnemy,
type IReadonlyEnemy,
type IReadonlyHeroAttribute,
type ISpecial
@ -77,10 +75,19 @@ beforeAll(async () => {
AmbushDamageView: mapDamageModule.AmbushDamageView,
MainMapDamageConverter: mapDamageModule.MainMapDamageConverter,
MainMapDamageReducer: mapDamageModule.MainMapDamageReducer,
MapDamageType: typesModule.MapDamageType,
MapDamageType: {
Unknown: typesModule.MapDamageType.Unknown,
Zone: typesModule.MapDamageType.Zone,
Layer: typesModule.MapDamageType.Layer,
Repulse: typesModule.MapDamageType.Repulse,
Between: typesModule.MapDamageType.Between
},
ManhattanRange: motaModule.ManhattanRange,
RectRange: motaModule.RectRange,
FaceGroup: commonModule.FaceGroup
FaceGroup: {
Dir4: commonModule.FaceGroup.Dir4,
Dir8: commonModule.FaceGroup.Dir8
}
};
});
@ -144,7 +151,8 @@ function createContext(
): IEnemyContext<IEnemyAttr, IHeroAttr> {
const width = options.width ?? 6;
const height = options.height ?? 6;
const enemies = options.enemies ?? new Map<string, IEnemyView<IEnemyAttr>>();
const enemies =
options.enemies ?? new Map<string, IEnemyView<IEnemyAttr>>();
return {
width,
height,
@ -177,9 +185,9 @@ function createView(
* 创建一个按固定方向表返回移动描述的假朝向处理器
* @param dirs 朝向与坐标增量对
*/
function createFace(
dirs: Array<[number, { x: number; y: number }]>
): { mapMovement: () => Array<[number, { x: number; y: number }]> } {
function createFace(dirs: Array<[number, { x: number; y: number }]>): {
mapMovement: () => Array<[number, { x: number; y: number }]>;
} {
return { mapMovement: () => dirs };
}
@ -212,11 +220,15 @@ describe('ZoneDamageView', () => {
zoneSquare: false,
range: 1
});
const view = new modules.ZoneDamageView(createContext(), { x: 2, y: 3 }, special);
const view = new modules.ZoneDamageView(
createContext(),
{ x: 2, y: 3 },
special
);
expect(view.getRange()).toBeInstanceOf(modules.ManhattanRange);
expect(view.getRangeParam()).toEqual({ cx: 2, cy: 3, radius: 1 });
expect(view.getDamageWithoutCheck({ x: 0, y: 0 })).toEqual(
expect(view.getDamageWithoutCheck()).toEqual(
createInfo(7, modules.MapDamageType.Zone)
);
});
@ -228,11 +240,15 @@ describe('ZoneDamageView', () => {
zoneSquare: true,
range: 2
});
const view = new modules.ZoneDamageView(createContext(), { x: 2, y: 3 }, special);
const view = new modules.ZoneDamageView(
createContext(),
{ x: 2, y: 3 },
special
);
expect(view.getRange()).toBeInstanceOf(modules.RectRange);
expect(view.getRangeParam()).toEqual({ x: 0, y: 1, w: 5, h: 5 });
expect(view.getDamageWithoutCheck({ x: 0, y: 0 })).toEqual(
expect(view.getDamageWithoutCheck()).toEqual(
createInfo(4, modules.MapDamageType.Zone)
);
});
@ -244,7 +260,11 @@ describe('ZoneDamageView', () => {
zoneSquare: false,
range: 1
});
const view = new modules.ZoneDamageView(createContext(), { x: 2, y: 3 }, special);
const view = new modules.ZoneDamageView(
createContext(),
{ x: 2, y: 3 },
special
);
expect(view.getDamageAt({ x: 2, y: 4 })).toEqual(
createInfo(7, modules.MapDamageType.Zone)
@ -258,7 +278,11 @@ describe('RepulseDamageView', () => {
it('uses a radius one range and returns null at the source', () => {
const special = createSpecial(18, 4);
const source: ITileLocator = { x: 2, y: 3 };
const view = new modules.RepulseDamageView(createContext(), source, special);
const view = new modules.RepulseDamageView(
createContext(),
source,
special
);
expect(view.getRangeParam()).toEqual({ cx: 2, cy: 3, radius: 1 });
expect(view.getDamageWithoutCheck(source)).toBeNull();
@ -268,7 +292,11 @@ describe('RepulseDamageView', () => {
it('returns repulse damage carrying the source locator', () => {
const special = createSpecial(18, 4);
const source: ITileLocator = { x: 2, y: 3 };
const view = new modules.RepulseDamageView(createContext(), source, special);
const view = new modules.RepulseDamageView(
createContext(),
source,
special
);
const info = view.getDamageWithoutCheck({ x: 3, y: 3 });
@ -308,9 +336,12 @@ describe('LaserDamageView', () => {
expect(view.getRangeParam()).toEqual({
cx: 2,
cy: 3,
dir: [{ x: 0, y: 0 }, { x: 1, y: 0 }]
dir: [
{ x: 0, y: 0 },
{ x: 1, y: 0 }
]
});
expect(view.getDamageWithoutCheck({ x: 5, y: 3 })).toEqual(
expect(view.getDamageWithoutCheck()).toEqual(
createInfo(9, modules.MapDamageType.Layer)
);
});
@ -462,7 +493,7 @@ describe('AmbushDamageView', () => {
const locator: ITileLocator = { x: 2, y: 3 };
const view = new modules.AmbushDamageView(createContext(), locator);
const info = view.getDamageWithoutCheck(locator);
const info = view.getDamageWithoutCheck();
expect(info?.damage).toBe(0);
expect(info?.type).toBe(modules.MapDamageType.Unknown);
@ -531,18 +562,15 @@ describe('MainMapDamageReducer', () => {
const caught: ITileLocator = { x: 0, y: 1 };
const reducer = new modules.MainMapDamageReducer();
const info = reducer.reduce(
[
createInfo(3, modules.MapDamageType.Zone),
createInfo(5, modules.MapDamageType.Repulse, {
repulse: new Set([repulse])
}),
createInfo(5, modules.MapDamageType.Between, {
catch: new Set([caught])
})
],
{ x: 0, y: 0 }
);
const info = reducer.reduce([
createInfo(3, modules.MapDamageType.Zone),
createInfo(5, modules.MapDamageType.Repulse, {
repulse: new Set([repulse])
}),
createInfo(5, modules.MapDamageType.Between, {
catch: new Set([caught])
})
]);
expect(info.damage).toBe(13);
expect(info.type).toBe(modules.MapDamageType.Repulse);
@ -554,7 +582,7 @@ describe('MainMapDamageReducer', () => {
it('returns zero unknown damage for an empty input', () => {
const reducer = new modules.MainMapDamageReducer();
const info = reducer.reduce([], { x: 0, y: 0 });
const info = reducer.reduce([]);
expect(info.damage).toBe(0);
expect(info.type).toBe(modules.MapDamageType.Unknown);

View File

@ -139,9 +139,15 @@ class FakeScript implements ICombatScript<TestEnemyAttr, TestHeroAttr> {
}
}
type TestCombatFlow = import('./combat').CombatFlow<
TestEnemyAttr,
TestHeroAttr
>;
type TestHeroAttribute = import('@user/data-base').HeroAttribute<TestHeroAttr>;
interface CombatFixture {
/** 被测战斗流程对象 */
flow: InstanceType<TestModules['CombatFlow']>;
flow: TestCombatFlow;
/** 数据层状态假对象 */
state: IStateBase;
/** 怪物上下文假对象 */
@ -149,7 +155,7 @@ interface CombatFixture {
/** 伤害上下文假对象 */
damage: IDamageContext<TestEnemyAttr, TestHeroAttr>;
/** 可修改勇士属性 */
hero: InstanceType<TestModules['HeroAttribute']>;
hero: TestHeroAttribute;
/** 怪物视图假对象 */
view: IEnemyView<TestEnemyAttr>;
/** 计算后怪物 */

View File

@ -4,19 +4,20 @@ import { type IRange } from '@motajs/common';
import {
type IEnemy,
type IReadonlyEnemy,
type IReadonlyHeroAttribute,
type IStateBase,
type ISpecial
} from '@user/data-base';
import {
type IAuraConverter,
type IAuraView,
type IEnemyAuraView,
type IEnemyCommonQueryEffect,
type IEnemyFinalEffect,
type IEnemyHandler,
type IEnemySpecialModifier,
type IEnemySpecialQueryEffect,
type IEnemyView,
type IReadonlyHeroAttribute
type IEnemyView
} from './types';
vi.hoisted(() => {
@ -127,11 +128,17 @@ function createEnemy(
});
}
type TestEnemyContext = import('./context').EnemyContext<
TestEnemyAttr,
TestHeroAttr
>;
type TestHeroAttribute = import('@user/data-base').HeroAttribute<TestHeroAttr>;
interface ContextFixture {
/** 被测怪物上下文 */
context: InstanceType<TestModules['EnemyContext']>;
context: TestEnemyContext;
/** 可修改勇士属性 */
hero: InstanceType<TestModules['HeroAttribute']>;
hero: TestHeroAttribute;
/** 数据层状态假对象 */
state: IStateBase;
}
@ -146,7 +153,9 @@ function createContextFixture(
height: number = 3
): ContextFixture {
const state = {} as IStateBase;
const context = new modules.EnemyContext<TestEnemyAttr, TestHeroAttr>(state);
const context = new modules.EnemyContext<TestEnemyAttr, TestHeroAttr>(
state
);
context.resize(width, height);
const hero = new modules.HeroAttribute<TestHeroAttr>({
hp: 100,
@ -255,9 +264,18 @@ class FakeConverter implements IAuraConverter<TestEnemyAttr, TestHeroAttr> {
return this.codes.includes(special.code);
}
convert(special: ISpecial<any>): IAuraView<TestEnemyAttr> | null {
convert(special: ISpecial<any>): IEnemyAuraView<TestEnemyAttr, any, any> {
this.convertCalls.push(special.code);
return this.factory ? this.factory(special.code) : null;
if (!this.factory) {
// 接口约定 convert 必返回光环视图,但测试需覆盖“无工厂则跳过转换”的 null 分支
return null as unknown as IEnemyAuraView<TestEnemyAttr, any, any>;
}
// FakeAura 为最小测试替身,不携带 enemy/special/locator,无法完整满足 IEnemyAuraView
return this.factory(special.code) as unknown as IEnemyAuraView<
TestEnemyAttr,
any,
any
>;
}
}
@ -353,9 +371,9 @@ describe('EnemyContext registry and lookups', () => {
y: 0
});
expect(fixture.context.getEnemyLocator(enemy)).toEqual({ x: 1, y: 0 });
expect(fixture.context.getViewByComputed(view!.getComputedEnemy())).toBe(
view
);
expect(
fixture.context.getViewByComputed(view!.getComputedEnemy())
).toBe(view);
});
// 验证未知定位符、未知坐标与未知怪物对象均返回 null
@ -691,7 +709,10 @@ describe('EnemyContext single aura ranges', () => {
fixture.context.buildup();
expect(
fixture.context.getEnemyByLoc(0, 0)!.getComputedEnemy().hasSpecial(30)
fixture.context
.getEnemyByLoc(0, 0)!
.getComputedEnemy()
.hasSpecial(30)
).toBe(true);
});
});
@ -902,7 +923,9 @@ describe('EnemyContext aura pipeline', () => {
fixture.context.buildup();
const computed = fixture.context.getEnemyByLoc(0, 0)!.getComputedEnemy();
const computed = fixture.context
.getEnemyByLoc(0, 0)!
.getComputedEnemy();
expect(converter.convertCalls).toContain(21);
expect(computed.hasSpecial(21)).toBe(true);
expect(computed.getAttribute('atk')).toBe(9);
@ -957,12 +980,16 @@ describe('EnemyContext aura pipeline', () => {
enemy.addSpecial(createSpecial(20) as never);
fixture.context.setEnemyAt({ x: 0, y: 0 }, enemy);
fixture.context.registerAuraConverter(
new FakeConverter([20], () => new FakeAura({
priority: 10,
range: new modules.FullRange(),
param: undefined,
couldApplyBase: false
}))
new FakeConverter(
[20],
() =>
new FakeAura({
priority: 10,
range: new modules.FullRange(),
param: undefined,
couldApplyBase: false
})
)
);
fixture.context.registerSpecialQueryEffect({
priority: 10,
@ -987,12 +1014,16 @@ describe('EnemyContext aura pipeline', () => {
enemy.addSpecial(createSpecial(20) as never);
fixture.context.setEnemyAt({ x: 0, y: 0 }, enemy);
fixture.context.registerAuraConverter(
new FakeConverter([20], () => new FakeAura({
priority: 10,
range: new modules.FullRange(),
param: undefined,
couldApplyBase: false
}))
new FakeConverter(
[20],
() =>
new FakeAura({
priority: 10,
range: new modules.FullRange(),
param: undefined,
couldApplyBase: false
})
)
);
fixture.context.registerSpecialQueryEffect({
priority: 10,
@ -1019,7 +1050,13 @@ describe('EnemyContext effect stage ordering', () => {
enemy.addSpecial(createSpecial(20) as never);
fixture.context.setEnemyAt({ x: 0, y: 0 }, enemy);
const order: string[] = [];
const seen = { queryAtk: -1, queryDef: -1, queryHp: -1, finalAtk: -1, finalDef: -1 };
const seen = {
queryAtk: -1,
queryDef: -1,
queryHp: -1,
finalAtk: -1,
finalDef: -1
};
const aura = new FakeAura({
priority: 1,
range: new modules.FullRange(),
@ -1034,7 +1071,9 @@ describe('EnemyContext effect stage ordering', () => {
return { add: () => [], delete: () => [], modify: () => false };
}
});
fixture.context.registerAuraConverter(new FakeConverter([20], () => aura));
fixture.context.registerAuraConverter(
new FakeConverter([20], () => aura)
);
fixture.context.registerCommonQueryEffect(20, {
priority: 1,
apply: handler => {
@ -1196,8 +1235,12 @@ describe('EnemyContext refresh paths and DEV warnings', () => {
param: undefined,
onApply: handler => handler.enemy.addAttribute('atk', 50)
});
fixture.context.registerAuraConverter(new FakeConverter([20], createAura));
fixture.context.registerAuraConverter(new FakeConverter([20], createAura));
fixture.context.registerAuraConverter(
new FakeConverter([20], createAura)
);
fixture.context.registerAuraConverter(
new FakeConverter([20], createAura)
);
const result = modules.logger.catch(() => fixture.context.buildup());
@ -1246,11 +1289,15 @@ describe('EnemyContext refresh paths and DEV warnings', () => {
fixture.context.bindHero(fixture.hero);
fixture.context.setEnemyAt({ x: 0, y: 0 }, createEnemy('e1'));
fixture.context.registerAuraConverter(
new FakeConverter([40], () => new FakeAura({
priority: 1,
range: new modules.FullRange(),
param: undefined
}))
new FakeConverter(
[40],
() =>
new FakeAura({
priority: 1,
range: new modules.FullRange(),
param: undefined
})
)
);
fixture.context.registerSpecialQueryEffect({
priority: 1,
@ -1278,11 +1325,15 @@ describe('EnemyContext refresh paths and DEV warnings', () => {
fixture.context.bindHero(fixture.hero);
fixture.context.setEnemyAt({ x: 0, y: 0 }, createEnemy('e1'));
fixture.context.registerAuraConverter(
new FakeConverter([40], () => new FakeAura({
priority: 50,
range: new modules.FullRange(),
param: undefined
}))
new FakeConverter(
[40],
() =>
new FakeAura({
priority: 50,
range: new modules.FullRange(),
param: undefined
})
)
);
fixture.context.registerSpecialQueryEffect({
priority: 1,

View File

@ -1,14 +1,17 @@
// 测试战斗伤害上下文与伤害系统:结果展开、handler 身份、告警码 106/107、缓存与 with、临界生成、属性到伤害联动(D-26)
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { type ITileLocator } from '@motajs/common';
import { type IEnemy, type IStateBase } from '@user/data-base';
import {
type IEnemy,
type IReadonlyEnemy,
type IStateBase
} from '@user/data-base';
import {
type CriticalableHeroStatus,
type IDamageCalculator,
type IEnemyContext,
type IEnemyDamageInfoBase,
type IEnemyView,
type IReadonlyEnemy,
type IReadonlyEnemyHandler
} from './types';
@ -92,9 +95,7 @@ beforeAll(async () => {
/**
* 固定伤害计算器:每点攻击减免固定伤害,用于验证结果展开与临界生成
*/
class FakeCalculator
implements IDamageCalculator<TestEnemyAttr, TestHeroAttr>
{
class FakeCalculator implements IDamageCalculator<TestEnemyAttr, TestHeroAttr> {
/** 每点攻击减免的伤害值 */
readonly damagePerAtk: number;
/** calculate 调用次数 */
@ -123,13 +124,19 @@ class FakeCalculator
}
}
type TestHeroAttribute = import('@user/data-base').HeroAttribute<TestHeroAttr>;
type TestEnemyContext = import('./context').EnemyContext<
TestEnemyAttr,
TestHeroAttr
>;
interface DamageFixture {
/** 怪物上下文假对象 */
context: IEnemyContext<TestEnemyAttr, TestHeroAttr>;
/** 固定伤害计算器 */
calculator: FakeCalculator;
/** 可修改勇士属性 */
hero: InstanceType<TestModules['HeroAttribute']>;
hero: TestHeroAttribute;
/** 怪物视图假对象 */
view: IEnemyView<TestEnemyAttr>;
/** 计算后怪物 */
@ -234,9 +241,10 @@ function createSpecial(code: number): never {
/**
* 创建一个按怪物攻击力线性换算伤害的计算器,用于验证属性到伤害的联动
*/
class EnemyAttributeCalculator
implements IDamageCalculator<TestEnemyAttr, TestHeroAttr>
{
class EnemyAttributeCalculator implements IDamageCalculator<
TestEnemyAttr,
TestHeroAttr
> {
/** calculate 调用次数 */
calls: number = 0;
/** 每点怪物攻击力换算的伤害 */
@ -289,9 +297,7 @@ interface FakeConverterShape {
* 创建一个可驱动光环流水线的真实怪物上下文
* @param converter 赋予上下文的测试用光环转换器
*/
function createEnemyContext(
converter: FakeConverterShape
): InstanceType<TestModules['EnemyContext']> {
function createEnemyContext(converter: FakeConverterShape): TestEnemyContext {
const context = new modules.EnemyContext<TestEnemyAttr, TestHeroAttr>(
{} as IStateBase
);
@ -507,8 +513,9 @@ describe('DamageContext critical generation', () => {
fixture.hero
);
const first = context.calculateCritical(fixture.view, 'atk').next()
.value!;
const first = context
.calculateCritical(fixture.view, 'atk')
.next().value!;
expect(first.baseValue).toBe(0);
expect(first.nextValue).toBe(1);
@ -577,8 +584,9 @@ describe('DamageContext critical generation', () => {
fixture.hero
);
const first = context.calculateCritical(fixture.view, 'atk').next()
.value!;
const first = context
.calculateCritical(fixture.view, 'atk')
.next().value!;
expect(first.nextValue).toBe(1);
expect(first.info.damage).toBe(90);
@ -705,10 +713,7 @@ describe('DamageSystem attribute linkage', () => {
context.buildup();
expect(
context
.getEnemyByLoc(0, 0)!
.getComputedEnemy()
.getAttribute('atk')
context.getEnemyByLoc(0, 0)!.getComputedEnemy().getAttribute('atk')
).toBe(17);
});
});

View File

@ -165,9 +165,10 @@ class FakeView implements IMapDamageView<number> {
/**
* 固定输出伤害视图列表的测试转换器
*/
class FakeConverter
implements IMapDamageConverter<TestEnemyAttr, TestHeroAttr>
{
class FakeConverter implements IMapDamageConverter<
TestEnemyAttr,
TestHeroAttr
> {
/** 转换输出的伤害视图 */
readonly views: IMapDamageView<number>[];
/** convert 调用次数 */
@ -190,9 +191,7 @@ class FakeReducer implements IMapDamageReducer {
/** reduce 调用次数 */
calls: number = 0;
reduce(
info: Iterable<Readonly<IMapDamageInfo>>
): Readonly<IMapDamageInfo> {
reduce(info: Iterable<Readonly<IMapDamageInfo>>): Readonly<IMapDamageInfo> {
this.calls++;
let damage = 0;
for (const item of info) {
@ -249,10 +248,9 @@ function createFixture(): MapDamageFixture {
markDirty: () => {}
};
enemies.set(view, locator);
const damageView = new FakeView(
7,
[indexer.locToIndex(locator.x, locator.y)]
);
const damageView = new FakeView(7, [
indexer.locToIndex(locator.x, locator.y)
]);
const context = {
state,
indexer,
@ -264,17 +262,13 @@ function createFixture(): MapDamageFixture {
getEnemyLocator: (target: IEnemy<TestEnemyAttr>) =>
target === origin ? locator : null,
getViewByComputed: () => view,
*iterateEnemy(): Iterable<
[ITileLocator, IEnemyView<TestEnemyAttr>]
> {
*iterateEnemy(): Iterable<[ITileLocator, IEnemyView<TestEnemyAttr>]> {
for (const [target, targetLocator] of enemies) {
yield [targetLocator, target];
}
}
} as never;
const damage = new modules.MapDamage<TestEnemyAttr, TestHeroAttr>(
context
);
const damage = new modules.MapDamage<TestEnemyAttr, TestHeroAttr>(context);
return {
damage,
context,
@ -290,7 +284,9 @@ function createFixture(): MapDamageFixture {
/**
* 创建一个未注册到夹具中的怪物视图,用于验证未知定位符路径
*/
function createUnknownView(fixture: MapDamageFixture): IEnemyView<TestEnemyAttr> {
function createUnknownView(
fixture: MapDamageFixture
): IEnemyView<TestEnemyAttr> {
return {
context: {} as never,
reset: () => {},
@ -309,14 +305,14 @@ describe('MapDamage sourceless damage', () => {
const info = createInfo(5);
fixture.damage.addMapDamage({ x: 0, y: 0 }, info);
expect(
[...fixture.damage.getSeparatedDamage({ x: 0, y: 0 })]
).toContain(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);
expect([
...fixture.damage.getSeparatedDamage({ x: 0, y: 0 })
]).toHaveLength(0);
});
// 验证删除不存在的无来源伤害是安全操作
@ -326,9 +322,9 @@ describe('MapDamage sourceless damage', () => {
fixture.damage.deleteMapDamage({ x: 3, y: 2 }, createInfo(5));
expect(
[...fixture.damage.getSeparatedDamage({ x: 3, y: 2 })]
).toHaveLength(0);
expect([
...fixture.damage.getSeparatedDamage({ x: 3, y: 2 })
]).toHaveLength(0);
});
});
@ -357,9 +353,9 @@ describe('MapDamage sourced conversion and reduction', () => {
fixture.damage.useConverter(fixture.converter);
fixture.damage.addMapDamage(fixture.locator, createInfo(3));
expect(
[...fixture.damage.getSeparatedDamage(fixture.locator)]
).toHaveLength(2);
expect([
...fixture.damage.getSeparatedDamage(fixture.locator)
]).toHaveLength(2);
const reduced = fixture.damage.getReducedDamage(fixture.locator);
expect(reduced?.damage).toBe(10);
@ -445,15 +441,14 @@ describe('MapDamage sourced conversion and reduction', () => {
const fixture = createFixture();
fixture.damage.useReducer(fixture.reducer);
fixture.damage.useConverter(fixture.converter);
const before = [
...fixture.damage.getSeparatedDamage(fixture.locator)
].length;
const before = [...fixture.damage.getSeparatedDamage(fixture.locator)]
.length;
fixture.damage.deleteEnemy(createUnknownView(fixture));
expect(
[...fixture.damage.getSeparatedDamage(fixture.locator)]
).toHaveLength(before);
expect([
...fixture.damage.getSeparatedDamage(fixture.locator)
]).toHaveLength(before);
});
// 疑似 bug:deleteEnemy 应移除该怪物带来的有来源地图伤害,详见 06-TEST-FINDINGS.md #06-01-2,修复后取消 skip
@ -461,14 +456,14 @@ describe('MapDamage sourced conversion and reduction', () => {
const fixture = createFixture();
fixture.damage.useReducer(fixture.reducer);
fixture.damage.useConverter(fixture.converter);
expect(
[...fixture.damage.getSeparatedDamage(fixture.locator)]
).toHaveLength(1);
expect([
...fixture.damage.getSeparatedDamage(fixture.locator)
]).toHaveLength(1);
fixture.damage.deleteEnemy(fixture.view);
expect(
[...fixture.damage.getSeparatedDamage(fixture.locator)]
).toHaveLength(0);
expect([
...fixture.damage.getSeparatedDamage(fixture.locator)
]).toHaveLength(0);
});
});