mirror of
https://github.com/motajs/template.git
synced 2026-10-02 00:20:18 +08:00
test(06): fix lint and type errors in data-layer test files
This commit is contained in:
parent
693b337e24
commit
136a9845d6
@ -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);
|
||||
});
|
||||
|
||||
// 验证支援光环不会把来源定位符添加到自身
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>;
|
||||
/** 计算后怪物 */
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user