diff --git a/packages-user/data-base/src/enemy/enemy.test.ts b/packages-user/data-base/src/enemy/enemy.test.ts new file mode 100644 index 0000000..c274e5e --- /dev/null +++ b/packages-user/data-base/src/enemy/enemy.test.ts @@ -0,0 +1,114 @@ +// 测试 Enemy 数据模型:属性单元、特殊属性增删查、克隆与复制行为 +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { type IEnemy } from './types'; + +vi.hoisted(() => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + Map.prototype.getOrInsertComputed ??= function ( + this: Map, + 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; + }; +}); + +interface TestModules { + Enemy: typeof import('./enemy').Enemy; + CommonSerializableSpecial: typeof import('./special').CommonSerializableSpecial; + logger: typeof import('@motajs/common').logger; +} + +let modules: TestModules; + +beforeAll(async () => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + const enemyModule = await import('./enemy'); + const specialModule = await import('./special'); + const commonModule = await import('@motajs/common'); + modules = { + Enemy: enemyModule.Enemy, + CommonSerializableSpecial: specialModule.CommonSerializableSpecial, + logger: commonModule.logger + }; +}); + +afterAll(() => { + vi.unstubAllGlobals(); +}); + +interface IEnemyTestAttr { + hp: number; + atk: number; + tags: string[]; +} + +/** + * 构造一个带合成属性的怪物对象 + * @param id 怪物标识符 + * @param code 怪物图块数字 + * @param attributes 初始属性 + */ +function createEnemy( + id = 'enemy-1', + code = 1, + attributes: IEnemyTestAttr = { hp: 20, atk: 8, tags: ['base'] } +): IEnemy { + return new modules.Enemy( + id, + code, + structuredClone(attributes) + ); +} + +describe('Enemy attribute units', () => { + // 验证 getAttribute 按单个属性键返回构造时写入的初值 + it('reads each attribute key independently', () => { + const enemy = createEnemy(); + + expect(enemy.getAttribute('hp')).toBe(20); + expect(enemy.getAttribute('atk')).toBe(8); + expect(enemy.getAttribute('tags')).toEqual(['base']); + }); + + // 验证 setAttribute 只修改指定键且不影响其它属性 + it('updates only the selected attribute key', () => { + const enemy = createEnemy(); + + enemy.setAttribute('hp', 35); + + expect(enemy.getAttribute('hp')).toBe(35); + expect(enemy.getAttribute('atk')).toBe(8); + expect(enemy.getAttribute('tags')).toEqual(['base']); + }); + + // 验证 addAttribute 对数字键做增减且支持负值 + it('adds positive and negative deltas to a numeric key', () => { + const enemy = createEnemy(); + + enemy.addAttribute('hp', 5); + enemy.addAttribute('atk', -3); + + expect(enemy.getAttribute('hp')).toBe(25); + expect(enemy.getAttribute('atk')).toBe(5); + }); + + // 验证 cloneAttributes 返回深拷贝而非内部属性的别名 + it('returns a deep copy instead of an internal alias', () => { + const enemy = createEnemy(); + + const cloned = enemy.cloneAttributes(); + cloned.hp = 999; + cloned.tags.push('mutated'); + + expect(enemy.getAttribute('hp')).toBe(20); + expect(enemy.getAttribute('tags')).toEqual(['base']); + expect(enemy.cloneAttributes()).not.toBe(enemy.cloneAttributes()); + }); +}); diff --git a/packages-user/data-base/src/enemy/special.test.ts b/packages-user/data-base/src/enemy/special.test.ts new file mode 100644 index 0000000..78aef86 --- /dev/null +++ b/packages-user/data-base/src/enemy/special.test.ts @@ -0,0 +1,136 @@ +// 测试 special 数据模型:可序列化特殊属性的数值/名称/描述单元,以及无属性特殊属性 +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { type ICommonSpecialConfig } from './special'; + +vi.hoisted(() => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + Map.prototype.getOrInsertComputed ??= function ( + this: Map, + 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; + }; +}); + +interface TestModules { + defineCommonSerializableSpecial: typeof import('./special').defineCommonSerializableSpecial; + defineNonePropertySpecial: typeof import('./special').defineNonePropertySpecial; +} + +let modules: TestModules; + +beforeAll(async () => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + const specialModule = await import('./special'); + modules = { + defineCommonSerializableSpecial: + specialModule.defineCommonSerializableSpecial, + defineNonePropertySpecial: specialModule.defineNonePropertySpecial + }; +}); + +afterAll(() => { + vi.unstubAllGlobals(); +}); + +/** + * 构造一个按当前数值生成名称与描述的可序列化特殊属性配置 + */ +function makeCommonConfig(): ICommonSpecialConfig { + return { + getSpecialName: special => `连击${special.getValue()}`, + getDescription: special => `怪物每回合攻击${special.getValue()}次。`, + fromLegacyEnemy: () => 0 + }; +} + +/** + * 构造一个使用固定名称与描述的无属性特殊属性配置 + */ +function makeNoneConfig(): ICommonSpecialConfig { + return { + getSpecialName: () => '先攻', + getDescription: () => '怪物首先攻击。', + fromLegacyEnemy: () => undefined + }; +} + +describe('CommonSerializableSpecial units', () => { + // 验证 setValue 更新数值且 getValue 返回最新数值 + it('writes and reads the serializable value', () => { + const creation = modules.defineCommonSerializableSpecial( + 6, + 4, + makeCommonConfig() + ); + const special = creation(undefined as never); + + expect(special.getValue()).toBe(4); + + special.setValue(5); + + expect(special.getValue()).toBe(5); + }); + + // 验证 getSpecialName 委托给配置并反映当前数值 + it('delegates the special name to the config with the current value', () => { + const creation = modules.defineCommonSerializableSpecial( + 6, + 4, + makeCommonConfig() + ); + const special = creation(undefined as never); + + expect(special.getSpecialName()).toBe('连击4'); + + special.setValue(5); + + expect(special.getSpecialName()).toBe('连击5'); + }); + + // 验证 getDescription 委托给配置并反映当前数值 + it('delegates the description to the config with the current value', () => { + const creation = modules.defineCommonSerializableSpecial( + 6, + 2, + makeCommonConfig() + ); + const special = creation(undefined as never); + + expect(special.getDescription()).toBe('怪物每回合攻击2次。'); + + special.setValue(3); + + expect(special.getDescription()).toBe('怪物每回合攻击3次。'); + }); +}); + +describe('NonePropertySpecial units', () => { + // 验证无属性特殊属性数值恒为 undefined 且 setValue 不产生效果 + it('keeps an undefined value regardless of setValue', () => { + const creation = modules.defineNonePropertySpecial(1, makeNoneConfig()); + const special = creation(undefined as never); + + expect(special.getValue()).toBeUndefined(); + + special.setValue(undefined); + + expect(special.getValue()).toBeUndefined(); + }); + + // 验证无属性特殊属性使用配置提供的固定名称与描述 + it('reports the fixed name and description from the config', () => { + const creation = modules.defineNonePropertySpecial(1, makeNoneConfig()); + const special = creation(undefined as never); + + expect(special.getSpecialName()).toBe('先攻'); + expect(special.getDescription()).toBe('怪物首先攻击。'); + }); +});