test(06-02): cover damage calculator, final effect and comparer

- MainDamageCalculator: base, defeat, all special branches, guard recursion/warn 137, negative-damage flag, critical limit
- MainEnemyFinalEffect: solid/mimic/unchanged branches and priority
- MainEnemyComparer: base attributes, special count/code/value deep equality
This commit is contained in:
unanmed 2026-09-14 09:04:23 +08:00
parent 50d868760a
commit 5b76cf4dc9
3 changed files with 892 additions and 0 deletions

View File

@ -0,0 +1,551 @@
// 测试内置伤害计算器:基础伤害、无敌、魔攻、连击、多段、支援、先攻、破甲、反击、净化、吸血、负伤、固伤、仇恨与临界上界
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { type ITileLocator } from '@motajs/common';
import { type IEnemyAttr, type IHeroAttr } from '@user/data-common';
import {
type IEnemyContext,
type IEnemyView,
type IReadonlyEnemyHandler
} from '@user/data-system';
import {
type IEnemy,
type IReadonlyHeroAttribute,
type ISpecial,
type IStateBase
} from '@user/data-base';
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 TestModules {
MainDamageCalculator: typeof import('./calculator').MainDamageCalculator;
logger: typeof import('@motajs/common').logger;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const calculatorModule = await import('./calculator');
const motaModule = await import('@motajs/common');
modules = {
MainDamageCalculator: calculatorModule.MainDamageCalculator,
logger: motaModule.logger
};
});
interface FakeEnemy {
/** 可写怪物对象 */
enemy: IEnemy<IEnemyAttr>;
/** 当前怪物属性,供断言与后续修改 */
attrs: IEnemyAttr;
}
interface FakeEnemyOptions {
/** 覆盖的怪物属性 */
attrs?: Partial<IEnemyAttr>;
/** 怪物特殊属性表,键为代码,值为该特殊属性的数值 */
specials?: Map<number, unknown>;
}
/**
*
* @param options
*/
function createEnemy(options: FakeEnemyOptions = {}): FakeEnemy {
const specials = options.specials ?? new Map<number, unknown>();
const attrs: IEnemyAttr = {
hp: 20,
atk: 8,
def: 5,
money: 2,
exp: 3,
point: 1,
guard: new Set(),
...options.attrs
};
const enemy = {
id: 'test-enemy',
code: 1,
getSpecial: (code: number): ISpecial<any> | null =>
specials.has(code)
? ({
code,
value: specials.get(code),
deepEqualsTo: () => false
} as ISpecial<any>)
: null,
hasSpecial: (code: number) => specials.has(code),
iterateSpecials: () => [],
getAttribute: (key: string) => (attrs as never)[key],
cloneAttributes: () => structuredClone(attrs),
clone: () => enemy,
addSpecial: () => {},
deleteSpecial: () => {},
setAttribute: (key: string, value: unknown) => {
(attrs as never)[key] = value;
},
addAttribute: (key: string, value: number) => {
(attrs as never)[key] += value;
},
copyFrom: () => {},
saveState: () => ({
attrs: structuredClone(attrs),
specials: new Map()
}),
loadState: () => {}
} as IEnemy<IEnemyAttr>;
return { enemy, attrs };
}
/**
*
* @param attrs
*/
function createHero(
attrs: Partial<IHeroAttr> = {}
): IReadonlyHeroAttribute<IHeroAttr> {
const values: IHeroAttr = {
name: 'hero',
hp: 100,
hpmax: 100,
atk: 20,
def: 5,
mdef: 0,
mana: 0,
manamax: 0,
money: 0,
exp: 0,
...attrs
};
return {
getBaseAttribute: (name: string) => (values as never)[name],
getFinalAttribute: (name: string) => (values as never)[name]
} as IReadonlyHeroAttribute<IHeroAttr>;
}
interface FakeStateOptions {
/** 十字架数量,未配置表示背包中没有十字架 */
cross?: number;
/** flag 字段值表 */
flags?: Record<string, unknown>;
}
/**
*
* @param options flag
*/
function createState(options: FakeStateOptions = {}): IStateBase {
const flags = options.flags ?? {};
return {
hero: {
items: {
getItemState: (id: string) =>
id === 'cross' && options.cross !== undefined
? { count: options.cross }
: null
}
},
flags: {
getFieldValueDefaults: (name: string, defaultValue: unknown) =>
name in flags ? flags[name] : defaultValue
}
} as IStateBase;
}
/**
*
* @param resolve
*/
function createContext(
resolve?: (locator: ITileLocator) => IEnemyView<IEnemyAttr> | null
): IEnemyContext<IEnemyAttr, IHeroAttr> {
return {
width: 8,
height: 8,
getEnemyByLocator: (locator: ITileLocator) =>
resolve ? resolve(locator) : null
} as IEnemyContext<IEnemyAttr, IHeroAttr>;
}
/**
*
* @param computed
*/
function createView(
computed: IEnemy<IEnemyAttr>
): IEnemyView<IEnemyAttr> {
return {
getComputedEnemy: () => computed
} as IEnemyView<IEnemyAttr>;
}
interface HandlerOptions {
/** 怪物对象 */
enemy: IEnemy<IEnemyAttr>;
/** 勇士属性 */
hero?: IReadonlyHeroAttribute<IHeroAttr>;
/** 状态对象 */
state?: IStateBase;
/** 地图上下文 */
context?: IEnemyContext<IEnemyAttr, IHeroAttr>;
/** 怪物定位符 */
locator?: ITileLocator;
}
/**
*
* @param options
*/
function createHandler(
options: HandlerOptions
): IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr> {
return {
enemy: options.enemy,
context: options.context ?? createContext(),
locator: options.locator ?? { x: 0, y: 0 },
hero: options.hero ?? createHero(),
state: options.state ?? createState()
} as IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>;
}
/**
*
* @param entries
*/
function specialsOf(entries: Array<[number, unknown]>): Map<number, unknown> {
return new Map(entries);
}
/**
*
*/
function createCalculator(): InstanceType<
TestModules['MainDamageCalculator']
> {
return new modules.MainDamageCalculator();
}
describe('MainDamageCalculator base and defeat branches', () => {
// 验证无特殊属性时按攻防差计算每轮伤害与回合数
it('computes damage and turns from the attack and defense difference', () => {
const { enemy } = createEnemy();
const calculator = createCalculator();
const info = calculator.calculate(createHandler({ enemy }));
expect(info).toEqual({ damage: 3, turn: 2 });
});
// 验证勇士无法破防时返回无敌伤害与 0 回合
it('returns infinite damage when the hero cannot break the defense', () => {
const { enemy } = createEnemy({ attrs: { def: 20 } });
const calculator = createCalculator();
const info = calculator.calculate(createHandler({ enemy }));
expect(info).toEqual({ damage: Infinity, turn: 0 });
});
// 验证无敌怪物在没有十字架时不可战胜,持有十字架后恢复正常计算
it('blocks 无敌 without a cross and allows it with one', () => {
const { enemy } = createEnemy({ specials: specialsOf([[20, undefined]]) });
const calculator = createCalculator();
const blocked = calculator.calculate(createHandler({ enemy }));
const allowed = calculator.calculate(
createHandler({ enemy, state: createState({ cross: 1 }) })
);
expect(blocked).toEqual({ damage: Infinity, turn: 0 });
expect(allowed).toEqual({ damage: 3, turn: 2 });
});
// 验证魔攻怪物的每轮伤害不减免勇士防御
it('ignores hero defense for 魔攻', () => {
const { enemy } = createEnemy({ specials: specialsOf([[2, undefined]]) });
const calculator = createCalculator();
const info = calculator.calculate(createHandler({ enemy }));
expect(info).toEqual({ damage: 8, turn: 2 });
});
// 验证 2连击与 3连击分别把每轮伤害乘以 2 和 3
it('multiplies enemy damage for 2连击 and 3连击', () => {
const calculator = createCalculator();
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 }
);
});
// 验证多段伤害按特殊属性数值倍乘每轮伤害
it('multiplies enemy damage by the 多段 value', () => {
const { enemy } = createEnemy({ specials: specialsOf([[6, 4]]) });
const calculator = createCalculator();
const info = calculator.calculate(createHandler({ enemy }));
expect(info).toEqual({ damage: 12, turn: 2 });
});
});
describe('MainDamageCalculator support and additive branches', () => {
// 验证支援怪会递归累加其回合与伤害,且同一支援怪只被计算一次
it('adds the guard turn and damage through recursion', () => {
const guard = createEnemy();
const { enemy } = createEnemy({
attrs: { guard: new Set<ITileLocator>([{ x: 1, y: 0 }]) }
});
const calls: string[] = [];
const context = createContext(locator => {
calls.push(`${locator.x},${locator.y}`);
return locator.x === 1 ? createView(guard.enemy) : null;
});
const calculator = createCalculator();
const info = calculator.calculate(
createHandler({ enemy, context })
);
expect(info).toEqual({ damage: 12, turn: 4 });
expect(calls).toEqual(['1,0']);
});
// 验证同一计算器连续两次顶层计算互不影响,支援标记不会泄漏
it('does not leak the in-guard flag across top-level calls', () => {
const guard = createEnemy();
const { enemy } = createEnemy({
attrs: { guard: new Set<ITileLocator>([{ x: 1, y: 0 }]) }
});
const context = createContext(locator =>
locator.x === 1 ? createView(guard.enemy) : null
);
const calculator = createCalculator();
const handler = createHandler({ enemy, context });
const first = calculator.calculate(handler);
const second = calculator.calculate(handler);
expect(first).toEqual({ damage: 12, turn: 4 });
expect(second).toEqual(first);
});
// 验证支援怪不存在时告警 137 并跳过该支援
it('warns 137 when a guard locator has no enemy', () => {
const { enemy } = createEnemy({
attrs: { guard: new Set<ITileLocator>([{ x: 2, y: 0 }]) }
});
const calculator = createCalculator();
const result = modules.logger.catch(() =>
calculator.calculate(createHandler({ enemy }))
);
expect(result.ret).toEqual({ damage: 3, turn: 2 });
expect(result.info.map(item => item.code)).toContain(137);
});
// 验证先攻会额外附加一次每轮伤害
it('adds one enemy hit for 先攻', () => {
const { enemy } = createEnemy({ specials: specialsOf([[1, undefined]]) });
const calculator = createCalculator();
const info = calculator.calculate(createHandler({ enemy }));
expect(info).toEqual({ damage: 6, turn: 2 });
});
// 验证破甲按比例附加勇士防御作为伤害
it('adds a share of hero defense for 破甲', () => {
const { enemy } = createEnemy({ specials: specialsOf([[7, 100]]) });
const calculator = createCalculator();
const info = calculator.calculate(createHandler({ enemy }));
expect(info).toEqual({ damage: 8, turn: 2 });
});
// 验证伤害最终向下取整
it('floors the final damage value', () => {
const { enemy } = createEnemy({ specials: specialsOf([[7, 33]]) });
const calculator = createCalculator();
const info = calculator.calculate(createHandler({ enemy }));
expect(info).toEqual({ damage: 4, turn: 2 });
});
// 验证反击按比例把勇士攻击附加到每轮伤害
it('adds a share of hero attack for 反击', () => {
const { enemy } = createEnemy({ specials: specialsOf([[8, 50]]) });
const calculator = createCalculator();
const info = calculator.calculate(createHandler({ enemy }));
expect(info).toEqual({ damage: 13, turn: 2 });
});
// 验证净化按倍数附加勇士魔防,且随后的减伤仍扣除一次魔防
it('adds hero mdef for 净化 and then subtracts it once', () => {
const { enemy } = createEnemy({ specials: specialsOf([[9, 2]]) });
const calculator = createCalculator();
const info = calculator.calculate(
createHandler({ enemy, hero: createHero({ mdef: 3 }) })
);
expect(info).toEqual({ damage: 6, turn: 2 });
});
});
describe('MainDamageCalculator vampire and bypass branches', () => {
// 验证吸血按勇士生命上限比例附加伤害,不回复自身时不改变怪物血量
it('adds vampire damage without healing the enemy by default', () => {
const { enemy } = createEnemy({
specials: specialsOf([[11, { vampire: 10, add: false }]])
});
const calculator = createCalculator();
const info = calculator.calculate(
createHandler({ enemy, hero: createHero({ hp: 200 }) })
);
expect(info).toEqual({ damage: 23, turn: 2 });
});
// 验证开启回复后吸血数值加到怪物血量上并延长回合数
it('adds vampire damage to the enemy health when add is true', () => {
const { enemy } = createEnemy({
specials: specialsOf([[11, { vampire: 10, add: true }]])
});
const calculator = createCalculator();
const info = calculator.calculate(
createHandler({ enemy, hero: createHero({ hp: 200 }) })
);
expect(info).toEqual({ damage: 26, turn: 3 });
});
// 验证未开启负伤时负伤害被夹到 0
it('clamps negative damage to zero when negative damage is disabled', () => {
const { enemy } = createEnemy({ attrs: { atk: 0 } });
const calculator = createCalculator();
const info = calculator.calculate(
createHandler({ enemy, hero: createHero({ mdef: 5 }) })
);
expect(info).toEqual({ damage: 0, turn: 2 });
});
// 验证开启负伤后负伤害被保留
it('keeps negative damage when negative damage is enabled', () => {
const { enemy } = createEnemy({ attrs: { atk: 0 } });
const calculator = createCalculator();
const info = calculator.calculate(
createHandler({
enemy,
hero: createHero({ mdef: 5 }),
state: createState({ flags: { enableNegativeDamage: true } })
})
);
expect(info).toEqual({ damage: -5, turn: 2 });
});
// 验证固伤在扣除魔防之后附加,不受魔防影响
it('adds 固伤 after the mdef subtraction', () => {
const { enemy } = createEnemy({ specials: specialsOf([[22, 7]]) });
const calculator = createCalculator();
const info = calculator.calculate(
createHandler({ enemy, hero: createHero({ mdef: 2 }) })
);
expect(info).toEqual({ damage: 8, turn: 2 });
});
// 验证仇恨按 flag 字段值附加伤害且不受魔防影响
it('adds the hatred flag value for 仇恨', () => {
const { enemy } = createEnemy({ specials: specialsOf([[17, undefined]]) });
const calculator = createCalculator();
const info = calculator.calculate(
createHandler({
enemy,
hero: createHero({ mdef: 2 }),
state: createState({ flags: { hatred: 5 } })
})
);
expect(info).toEqual({ damage: 6, turn: 2 });
});
});
describe('MainDamageCalculator critical limit', () => {
// 验证攻击临界上界为怪物防御加生命,坚固怪物则为无穷
it('returns def plus hp for atk, and Infinity for 坚固', () => {
const normal = createEnemy();
const solid = createEnemy({ specials: specialsOf([[3, undefined]]) });
const calculator = createCalculator();
expect(
calculator.getCriticalLimit(
createHandler({ enemy: normal.enemy }),
'atk'
)
).toBe(25);
expect(
calculator.getCriticalLimit(
createHandler({ enemy: solid.enemy }),
'atk'
)
).toBe(Infinity);
});
// 验证非攻击属性直接返回勇士对应最终属性
it('returns the hero final attribute for other attributes', () => {
const { enemy } = createEnemy();
const calculator = createCalculator();
const limit = calculator.getCriticalLimit(
createHandler({ enemy, hero: createHero({ def: 7 }) }),
'def'
);
expect(limit).toBe(7);
});
});

View File

@ -0,0 +1,153 @@
// 测试怪物比较器:基础属性全部一致且特殊属性集合深比较相等时判定为相同
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { type IEnemyAttr } from '@user/data-common';
import { type IReadonlyEnemy, type ISpecial } from '@user/data-base';
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 TestModules {
MainEnemyComparer: typeof import('./comparer').MainEnemyComparer;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const comparerModule = await import('./comparer');
modules = {
MainEnemyComparer: comparerModule.MainEnemyComparer
};
});
/**
*
* @param code
* @param value
*/
function createSpecial(code: number, value: unknown): ISpecial<any> {
return {
code,
value,
deepEqualsTo: (other: ISpecial<any>) =>
other.code === code &&
JSON.stringify(other.value) === JSON.stringify(value)
} as never;
}
/**
*
* @param attrs
* @param specials
*/
function createEnemy(
attrs: Partial<IEnemyAttr> = {},
specials: ISpecial<any>[] = []
): IReadonlyEnemy<IEnemyAttr> {
const values: IEnemyAttr = {
hp: 20,
atk: 8,
def: 5,
money: 2,
exp: 3,
point: 1,
guard: new Set(),
...attrs
};
return {
id: 'test-enemy',
code: 1,
getSpecial: (code: number) =>
specials.find(special => special.code === code) ?? null,
hasSpecial: (code: number) =>
specials.some(special => special.code === code),
iterateSpecials: () => specials,
getAttribute: (key: string) => (values as never)[key],
cloneAttributes: () => structuredClone(values),
clone: () => createEnemy(attrs, specials)
} as never;
}
describe('MainEnemyComparer', () => {
// 验证基础属性与特殊属性都一致时判定为相同
it('returns true for identical attributes and specials', () => {
const comparer = new modules.MainEnemyComparer();
const enemyA = createEnemy({}, [createSpecial(6, 3)]);
const enemyB = createEnemy({}, [createSpecial(6, 3)]);
expect(comparer.compare(enemyA, enemyB)).toBe(true);
});
// 验证任一基础属性不同都会判定为不同
it('returns false when any base attribute differs', () => {
const comparer = new modules.MainEnemyComparer();
const base = createEnemy();
const cases: Array<[keyof IEnemyAttr, number]> = [
['hp', 21],
['atk', 9],
['def', 6],
['money', 3],
['exp', 4],
['point', 2]
];
for (const [key, value] of cases) {
const other = createEnemy({ [key]: value });
expect(comparer.compare(base, other)).toBe(false);
}
});
// 验证特殊属性数量不同时判定为不同
it('returns false when the special counts differ', () => {
const comparer = new modules.MainEnemyComparer();
const enemyA = createEnemy({}, [createSpecial(6, 3)]);
const enemyB = createEnemy({}, [
createSpecial(6, 3),
createSpecial(7, 0)
]);
expect(comparer.compare(enemyA, enemyB)).toBe(false);
});
// 验证特殊属性代码集合不同时判定为不同
it('returns false when the special codes differ', () => {
const comparer = new modules.MainEnemyComparer();
const enemyA = createEnemy({}, [createSpecial(6, 3)]);
const enemyB = createEnemy({}, [createSpecial(7, 3)]);
expect(comparer.compare(enemyA, enemyB)).toBe(false);
});
// 验证同代码特殊属性数值不同时判定为不同
it('returns false when a special deep comparison fails', () => {
const comparer = new modules.MainEnemyComparer();
const enemyA = createEnemy({}, [createSpecial(6, 3)]);
const enemyB = createEnemy({}, [createSpecial(6, 5)]);
expect(comparer.compare(enemyA, enemyB)).toBe(false);
});
});

View File

@ -0,0 +1,188 @@
// 测试怪物最终效果:坚固把防御提升到勇士攻击减一、模仿复制勇士攻防、无相关特殊属性时保持不变
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';
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 TestModules {
MainEnemyFinalEffect: typeof import('./final').MainEnemyFinalEffect;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const finalModule = await import('./final');
modules = {
MainEnemyFinalEffect: finalModule.MainEnemyFinalEffect
};
});
interface FakeEnemy {
/** 可写怪物对象 */
enemy: IEnemy<IEnemyAttr>;
/** 当前怪物属性,供断言使用 */
attrs: IEnemyAttr;
}
/**
*
* @param attrs
* @param specials
*/
function createEnemy(
attrs: Partial<IEnemyAttr> = {},
specials: number[] = []
): FakeEnemy {
const values: IEnemyAttr = {
hp: 20,
atk: 8,
def: 5,
money: 2,
exp: 3,
point: 1,
guard: new Set(),
...attrs
};
const enemy = {
id: 'test-enemy',
code: 1,
getSpecial: () => null,
hasSpecial: (code: number) => specials.includes(code),
iterateSpecials: () => [],
getAttribute: (key: string) => (values as never)[key],
cloneAttributes: () => structuredClone(values),
clone: () => enemy,
addSpecial: () => {},
deleteSpecial: () => {},
setAttribute: (key: string, value: unknown) => {
(values as never)[key] = value;
},
addAttribute: (key: string, value: number) => {
(values as never)[key] += value;
},
copyFrom: () => {},
saveState: () => ({ attrs: structuredClone(values), specials: new Map() }),
loadState: () => {}
} as never;
return { enemy, attrs: values };
}
/**
*
* @param attrs
*/
function createHero(
attrs: Partial<IHeroAttr> = {}
): IReadonlyHeroAttribute<IHeroAttr> {
const values: IHeroAttr = {
name: 'hero',
hp: 100,
hpmax: 100,
atk: 20,
def: 7,
mdef: 0,
mana: 0,
manamax: 0,
money: 0,
exp: 0,
...attrs
};
return {
getBaseAttribute: (name: string) => (values as never)[name],
getFinalAttribute: (name: string) => (values as never)[name]
} as never;
}
/**
*
* @param enemy
* @param hero
*/
function createHandler(
enemy: IEnemy<IEnemyAttr>,
hero: IReadonlyHeroAttribute<IHeroAttr> = createHero()
): IEnemyHandler<IEnemyAttr, IHeroAttr> {
return { enemy, hero } as never;
}
describe('MainEnemyFinalEffect', () => {
// 验证最终效果的优先级为 0
it('reports priority 0', () => {
const effect = new modules.MainEnemyFinalEffect();
expect(effect.priority).toBe(0);
});
// 验证坚固把低于勇士攻击减一的防御提升到该值
it('raises defense to hero attack minus one for 坚固', () => {
const { enemy, attrs } = createEnemy({ def: 5 }, [3]);
const effect = new modules.MainEnemyFinalEffect();
effect.apply(createHandler(enemy));
expect(attrs.def).toBe(19);
});
// 验证坚固不会降低已高于勇士攻击减一的防御
it('keeps a defense already above hero attack minus one for 坚固', () => {
const { enemy, attrs } = createEnemy({ def: 25 }, [3]);
const effect = new modules.MainEnemyFinalEffect();
effect.apply(createHandler(enemy));
expect(attrs.def).toBe(25);
});
// 验证模仿把怪物的攻击与防御改成勇士的最终攻击与防御
it('copies hero attack and defense for 模仿', () => {
const { enemy, attrs } = createEnemy({ atk: 3, def: 3 }, [10]);
const effect = new modules.MainEnemyFinalEffect();
effect.apply(createHandler(enemy));
expect(attrs.atk).toBe(20);
expect(attrs.def).toBe(7);
});
// 验证没有相关特殊属性时怪物的攻击与防御保持不变
it('leaves enemies without the effect specials untouched', () => {
const { enemy, attrs } = createEnemy({ atk: 3, def: 3 }, [1, 2]);
const effect = new modules.MainEnemyFinalEffect();
effect.apply(createHandler(enemy));
expect(attrs.atk).toBe(3);
expect(attrs.def).toBe(3);
});
});