mirror of
https://github.com/motajs/template.git
synced 2026-10-09 22:10:23 +08:00
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:
parent
5b76cf4dc9
commit
b36a4ee8a9
394
packages-user/data-state/src/enemy/aura.test.ts
Normal file
394
packages-user/data-state/src/enemy/aura.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
173
packages-user/data-state/src/enemy/special.test.ts
Normal file
173
packages-user/data-state/src/enemy/special.test.ts
Normal 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('怪物首先攻击。');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user