test(06-02): cover aura converters and special registration

- CommonAura/CommonAuraConverter: range selection, buff math, capabilities
- GuardAura/GuardAuraConverter: 3x3 range, guard locator registration, own-locator and missing-source no-ops
- registerSpecials: code 0-27 registered once, guard default, name/description generation
This commit is contained in:
unanmed 2026-09-14 09:05:34 +08:00
parent 5b76cf4dc9
commit b36a4ee8a9
2 changed files with 567 additions and 0 deletions

View File

@ -0,0 +1,394 @@
// 测试光环转换器:光环范围与参数选择、属性加成结算、支援光环注册与守卫定位符添加
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,
type IEnemyHandler
} from '@user/data-system';
import {
type IEnemy,
type IReadonlyEnemy,
type ISpecial
} from '@user/data-base';
import { type IHaloValue } from './special';
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 {
CommonAura: typeof import('./aura').CommonAura;
CommonAuraConverter: typeof import('./aura').CommonAuraConverter;
GuardAura: typeof import('./aura').GuardAura;
GuardAuraConverter: typeof import('./aura').GuardAuraConverter;
FullRange: typeof import('@motajs/common').FullRange;
RectRange: typeof import('@motajs/common').RectRange;
ManhattanRange: typeof import('@motajs/common').ManhattanRange;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const auraModule = await import('./aura');
const motaModule = await import('@motajs/common');
modules = {
CommonAura: auraModule.CommonAura,
CommonAuraConverter: auraModule.CommonAuraConverter,
GuardAura: auraModule.GuardAura,
GuardAuraConverter: auraModule.GuardAuraConverter,
FullRange: motaModule.FullRange,
RectRange: motaModule.RectRange,
ManhattanRange: motaModule.ManhattanRange
};
});
interface FakeEnemy {
/** 可写怪物对象 */
enemy: IEnemy<IEnemyAttr>;
/** 当前怪物属性,供断言使用 */
attrs: IEnemyAttr;
}
/**
* 创建一个只实现光环所需能力的内联假怪物
* @param attrs 覆盖的怪物属性
*/
function createEnemy(attrs: Partial<IEnemyAttr> = {}): 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: () => false,
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 code 特殊属性代码
* @param value 特殊属性数值
*/
function createSpecial<T>(code: number, value: T): ISpecial<T> {
return { code, value } as never;
}
/**
* 创建一个只实现视图反查的内联假怪物上下文
* @param resolve 根据计算后怪物对象查找视图的回调
*/
function createContext(
resolve?: (enemy: IReadonlyEnemy<IEnemyAttr>) => IEnemyView<IEnemyAttr> | null
): IEnemyContext<IEnemyAttr, IHeroAttr> {
return {
width: 8,
height: 8,
getViewByComputed: (enemy: IReadonlyEnemy<IEnemyAttr>) =>
resolve ? resolve(enemy) : null
} as never;
}
/**
* 创建一个只用于占据视图位置的内联假视图
*/
function createView(): IEnemyView<IEnemyAttr> {
return {} as never;
}
/**
* 组装一个光环转换所需的最小只读信息对象
* @param enemy 怪物对象
* @param locator 怪物定位符
*/
function createReadonlyHandler(
enemy: IReadonlyEnemy<IEnemyAttr>,
locator: ITileLocator
): IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr> {
return { enemy, locator } as never;
}
/**
* 组装一个光环结算所需的最小可写信息对象
* @param enemy 可写怪物对象
* @param locator 怪物定位符
*/
function createWritableHandler(
enemy: IEnemy<IEnemyAttr>,
locator: ITileLocator
): IEnemyHandler<IEnemyAttr, IHeroAttr> {
return { enemy, locator } as never;
}
/**
* 构造一个完整的光环数值
* @param overrides 覆盖的光环字段
*/
function createHalo(overrides: Partial<IHaloValue> = {}): IHaloValue {
return {
haloRange: 0,
haloSquare: false,
hpBuff: 0,
atkBuff: 0,
defBuff: 0,
...overrides
};
}
describe('CommonAuraConverter', () => {
// 验证只有代码 25 的特殊属性会被光环转换器接管
it('converts only special code 25', () => {
const converter = new modules.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);
});
// 验证转换结果为携带怪物、特殊属性与定位符的光环视图
it('converts a code 25 special into a common aura', () => {
const converter = new modules.CommonAuraConverter();
const { enemy } = createEnemy();
const locator: ITileLocator = { x: 3, y: 4 };
const special = createSpecial(25, createHalo());
const aura = converter.convert(
special,
createReadonlyHandler(enemy, locator)
);
expect(aura).toBeInstanceOf(modules.CommonAura);
expect(aura.enemy).toBe(enemy);
expect(aura.locator).toBe(locator);
expect(aura.special).toBe(special);
});
});
describe('CommonAura range', () => {
// 验证光环范围不大于 0 时使用全图范围且无范围参数
it('uses the full range when the halo range is not positive', () => {
const { enemy } = createEnemy();
const aura = new modules.CommonAura(
enemy,
createSpecial(25, createHalo({ haloRange: 0 })),
{ x: 3, y: 4 }
);
expect(aura.range).toBeInstanceOf(modules.FullRange);
expect(aura.getRangeParam()).toBeUndefined();
});
// 验证九宫格光环使用以定位符为中心的矩形范围参数
it('uses a centered rect for a square halo', () => {
const { enemy } = createEnemy();
const aura = new modules.CommonAura(
enemy,
createSpecial(
25,
createHalo({ haloRange: 2, haloSquare: true })
),
{ x: 3, y: 4 }
);
expect(aura.range).toBeInstanceOf(modules.RectRange);
expect(aura.getRangeParam()).toEqual({ x: 1, y: 2, w: 5, h: 5 });
});
// 验证十字光环使用以定位符为中心的曼哈顿范围参数
it('uses a manhattan range for a cross halo', () => {
const { enemy } = createEnemy();
const aura = new modules.CommonAura(
enemy,
createSpecial(
25,
createHalo({ haloRange: 2, haloSquare: false })
),
{ x: 3, y: 4 }
);
expect(aura.range).toBeInstanceOf(modules.ManhattanRange);
expect(aura.getRangeParam()).toEqual({ cx: 3, cy: 4, radius: 2 });
});
});
describe('CommonAura apply', () => {
// 验证生命、攻击与防御加成按基础属性比例取整后加到目标怪物上
it('adds floored attribute buffs to the target enemy', () => {
const base = createEnemy({ hp: 100, atk: 50, def: 30 });
const target = createEnemy({ hp: 20, atk: 8, def: 5 });
const aura = new modules.CommonAura(
base.enemy,
createSpecial(
25,
createHalo({ hpBuff: 10, atkBuff: 50, defBuff: 20 })
),
{ x: 0, y: 0 }
);
aura.apply(createWritableHandler(target.enemy, { x: 1, y: 0 }), base.enemy);
expect(target.attrs.hp).toBe(30);
expect(target.attrs.atk).toBe(33);
expect(target.attrs.def).toBe(11);
});
// 验证加成为 0 时不修改目标怪物的任何属性
it('leaves the target untouched when every buff is zero', () => {
const base = createEnemy({ hp: 100, atk: 50, def: 30 });
const target = createEnemy({ hp: 20, atk: 8, def: 5 });
const aura = new modules.CommonAura(
base.enemy,
createSpecial(25, createHalo()),
{ x: 0, y: 0 }
);
aura.apply(createWritableHandler(target.enemy, { x: 1, y: 0 }), base.enemy);
expect(target.attrs).toMatchObject({ hp: 20, atk: 8, def: 5 });
});
// 验证光环的优先级与修饰能力声明,且不产生特殊属性修饰器
it('reports its priority, capabilities and no special modifier', () => {
const { enemy } = createEnemy();
const aura = new modules.CommonAura(
enemy,
createSpecial(25, createHalo()),
{ x: 0, y: 0 }
);
expect(aura.priority).toBe(25);
expect(aura.couldApplyBase).toBe(true);
expect(aura.couldApplySpecial).toBe(false);
expect(aura.applySpecial()).toBeNull();
});
});
describe('GuardAura', () => {
// 验证只有代码 26 的特殊属性会被支援光环转换器接管
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);
});
// 验证支援光环的范围参数为以定位符为中心的 3x3 矩形
it('uses a 3x3 rect centered on the locator', () => {
const { enemy } = createEnemy();
const aura = new modules.GuardAura(
createContext(() => createView()),
enemy,
createSpecial(26, undefined),
{ x: 3, y: 4 }
);
expect(aura.getRangeParam()).toEqual({ x: 2, y: 3, w: 3, h: 3 });
});
// 验证支援光环把来源定位符添加到周围怪物的支援集合
it('adds the source locator to a different target enemy guard set', () => {
const source = createEnemy();
const target = createEnemy();
const sourceLocator: ITileLocator = { x: 1, y: 1 };
const context = createContext(() => createView());
const aura = new modules.GuardAura(
context,
source.enemy,
createSpecial(26, undefined),
sourceLocator
);
aura.apply(createWritableHandler(target.enemy, { x: 2, y: 1 }));
expect((target.attrs.guard as Set<ITileLocator>).has(sourceLocator)).toBe(
true
);
});
// 验证支援光环不会把来源定位符添加到自身
it('does not add the source locator to the source itself', () => {
const source = createEnemy();
const sourceLocator: ITileLocator = { x: 1, y: 1 };
const aura = new modules.GuardAura(
createContext(() => createView()),
source.enemy,
createSpecial(26, undefined),
sourceLocator
);
aura.apply(createWritableHandler(source.enemy, sourceLocator));
expect(source.attrs.guard.size).toBe(0);
});
// 验证来源视图不存在时不添加任何支援定位符
it('does nothing when the source view is missing', () => {
const source = createEnemy();
const target = createEnemy();
const aura = new modules.GuardAura(
createContext(() => null),
source.enemy,
createSpecial(26, undefined),
{ x: 1, y: 1 }
);
aura.apply(createWritableHandler(target.enemy, { x: 2, y: 1 }));
expect(target.attrs.guard.size).toBe(0);
});
});

View File

@ -0,0 +1,173 @@
// 测试怪物特殊属性注册:代码 0-27 全量注册、守卫默认值、名称与描述生成
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { type ISpecial } from '@user/data-base';
import { type IHaloValue } from './special';
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 {
registerSpecials: typeof import('./special').registerSpecials;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const specialModule = await import('./special');
modules = { registerSpecials: specialModule.registerSpecials };
});
type SpecialCreation = (enemy: unknown) => ISpecial<any>;
interface FakeEnemyManager {
/** 记录注册的属性默认值 */
defaults: Map<string, unknown>;
/** 记录注册的代码与创建函数 */
specials: Map<number, SpecialCreation>;
/** 记录每个代码的注册次数 */
registerCounts: Map<number, number>;
/** 注册一个特殊属性 */
registerSpecial(code: number, cons: SpecialCreation): void;
/** 注册一个属性默认值 */
setAttributeDefaults(name: string, value: unknown): void;
}
/**
* 创建一个只实现注册接口的内联假怪物管理器
*/
function createManager(): FakeEnemyManager {
const defaults = new Map<string, unknown>();
const specials = new Map<number, SpecialCreation>();
const registerCounts = new Map<number, number>();
return {
defaults,
specials,
registerCounts,
registerSpecial: (code, cons) => {
specials.set(code, cons);
registerCounts.set(code, (registerCounts.get(code) ?? 0) + 1);
},
setAttributeDefaults: (name, value) => {
defaults.set(name, value);
}
};
}
/**
* 取出注册表中的一个特殊属性创建函数并实例化
* @param manager 假怪物管理器
* @param code 特殊属性代码
*/
function createSpecial(
manager: FakeEnemyManager,
code: number
): ISpecial<any> {
const creation = manager.specials.get(code);
if (!creation) throw new Error(`special ${code} was not registered`);
return creation(undefined);
}
describe('registerSpecials', () => {
// 验证代码 0 到 27 每个都被恰好注册一次
it('registers every special code from 0 to 27 exactly once', () => {
const manager = createManager();
modules.registerSpecials(manager as never);
const codes = [...manager.specials.keys()].sort((a, b) => a - b);
const counts = [...manager.registerCounts.values()];
expect(codes).toEqual([...Array(28).keys()]);
expect(counts).toEqual(Array(28).fill(1));
});
// 验证注册时写入守卫属性默认值为空集合
it('registers an empty set default for the guard attribute', () => {
const manager = createManager();
modules.registerSpecials(manager as never);
expect(manager.defaults.has('guard')).toBe(true);
expect(manager.defaults.get('guard')).toBeInstanceOf(Set);
expect((manager.defaults.get('guard') as Set<unknown>).size).toBe(0);
});
// 验证每个注册的特殊属性都提供名称与描述方法
it('provides a name and description accessor for every special', () => {
const manager = createManager();
modules.registerSpecials(manager as never);
for (const code of manager.specials.keys()) {
const special = createSpecial(manager, code);
expect(typeof special.getSpecialName).toBe('function');
expect(typeof special.getDescription).toBe('function');
}
});
// 验证可序列化特殊属性按当前数值生成名称与描述
it('builds the name and description of a serializable special', () => {
const manager = createManager();
modules.registerSpecials(manager as never);
const combo = createSpecial(manager, 6);
expect(combo.getSpecialName()).toBe('4连击');
combo.setValue(5);
expect(combo.getSpecialName()).toBe('5连击');
expect(combo.getDescription()).toBe('怪物每回合攻击5次。');
});
// 验证光环特殊属性按范围与加成生成描述
it('builds the description of the halo special from its value', () => {
const manager = createManager();
modules.registerSpecials(manager as never);
const halo = createSpecial(manager, 25);
expect(halo.getSpecialName()).toBe('光环');
expect(halo.getDescription()).toContain('同楼层所有怪物');
expect(halo.getDescription()).toContain('线性叠加。');
const value: IHaloValue = {
haloRange: 2,
haloSquare: true,
hpBuff: 10,
atkBuff: 0,
defBuff: 0
};
halo.setValue(value);
expect(halo.getDescription()).toContain('九宫格2格范围内所有怪物');
expect(halo.getDescription()).toContain('生命提升10%');
});
// 验证无属性特殊属性提供固定名称与描述
it('provides a fixed name and description for a none-property special', () => {
const manager = createManager();
modules.registerSpecials(manager as never);
const firstStrike = createSpecial(manager, 1);
expect(firstStrike.getSpecialName()).toBe('先攻');
expect(firstStrike.getDescription()).toBe('怪物首先攻击。');
});
});