diff --git a/packages-user/data-base/src/enemy/enemy.test.ts b/packages-user/data-base/src/enemy/enemy.test.ts index c274e5e..a590ac7 100644 --- a/packages-user/data-base/src/enemy/enemy.test.ts +++ b/packages-user/data-base/src/enemy/enemy.test.ts @@ -1,6 +1,6 @@ // 测试 Enemy 数据模型:属性单元、特殊属性增删查、克隆与复制行为 import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; -import { type IEnemy } from './types'; +import { type IEnemy, type ISpecial } from './types'; vi.hoisted(() => { vi.stubGlobal('main', { replayChecking: true }); @@ -67,6 +67,19 @@ function createEnemy( ); } +/** + * 构造一个可序列化的数值特殊属性,仅使用内联配置 + * @param code 特殊属性代码 + * @param value 特殊属性数值 + */ +function createSpecial(code: number, value: number): ISpecial { + return new modules.CommonSerializableSpecial(code, value, { + getSpecialName: () => `special-${code}`, + getDescription: () => `description-${code}`, + fromLegacyEnemy: () => value + }); +} + describe('Enemy attribute units', () => { // 验证 getAttribute 按单个属性键返回构造时写入的初值 it('reads each attribute key independently', () => { @@ -112,3 +125,107 @@ describe('Enemy attribute units', () => { expect(enemy.cloneAttributes()).not.toBe(enemy.cloneAttributes()); }); }); + +describe('Enemy special composition', () => { + // 验证 addSpecial 后可按代码查询、判断存在并迭代取出 + it('adds a special and resolves it by code', () => { + const enemy = createEnemy(); + const special = createSpecial(6, 3); + + enemy.addSpecial(special); + + expect(enemy.hasSpecial(6)).toBe(true); + expect(enemy.getSpecial(6)).toBe(special); + expect([...enemy.iterateSpecials()].map(item => item.code)).toEqual([ + 6 + ]); + expect(enemy.hasSpecial(7)).toBe(false); + expect(enemy.getSpecial(7)).toBeNull(); + }); + + // 验证重复 code 告警 96 且不替换既有特殊属性 + it('warns 96 and keeps the existing special on a duplicate code', () => { + const enemy = createEnemy(); + enemy.addSpecial(createSpecial(6, 3)); + + const result = modules.logger.catch(() => + enemy.addSpecial(createSpecial(6, 9)) + ); + + expect(result.info.map(info => info.code)).toContain(96); + expect(enemy.getSpecial(6)!.getValue()).toBe(3); + expect([...enemy.iterateSpecials()]).toHaveLength(1); + }); + + // 验证 deleteSpecial 支持按对象与按代码删除且未知代码安全无操作 + it('deletes specials by object or code and ignores unknown codes', () => { + const enemy = createEnemy(); + const first = createSpecial(6, 3); + enemy.addSpecial(first); + enemy.addSpecial(createSpecial(7, 5)); + + enemy.deleteSpecial(first); + + expect(enemy.hasSpecial(6)).toBe(false); + expect(enemy.getSpecial(6)).toBeNull(); + + enemy.deleteSpecial(7); + enemy.deleteSpecial(99); + + expect([...enemy.iterateSpecials()]).toHaveLength(0); + }); + + // 验证 clone 复制属性与克隆后的特殊属性且与来源互相独立 + it('clones attributes and specials into an independent enemy', () => { + const source = createEnemy('source', 1, { + hp: 20, + atk: 8, + tags: ['base'] + }); + source.addSpecial(createSpecial(6, 3)); + + const clone = source.clone(); + + expect(clone).not.toBe(source); + expect(clone.id).toBe('source'); + expect(clone.code).toBe(1); + expect(clone.getSpecial(6)).not.toBe(source.getSpecial(6)); + expect(clone.getAttribute('tags')).not.toBe( + source.getAttribute('tags') + ); + + clone.setAttribute('hp', 99); + clone.getSpecial(6)!.setValue(9); + clone.getAttribute('tags').push('cloned'); + + expect(source.getAttribute('hp')).toBe(20); + expect(source.getSpecial(6)!.getValue()).toBe(3); + expect(source.getAttribute('tags')).toEqual(['base']); + }); + + // 验证 copyFrom 以来源的属性与克隆后的特殊属性整体替换当前对象 + it('copies attributes and specials from another enemy', () => { + const target = createEnemy('target', 2, { hp: 1, atk: 1, tags: [] }); + target.addSpecial(createSpecial(7, 5)); + const source = createEnemy('source', 1, { + hp: 77, + atk: 5, + tags: ['copied'] + }); + source.addSpecial(createSpecial(6, 3)); + + target.copyFrom(source); + + expect(target.id).toBe('target'); + expect(target.code).toBe(2); + expect(target.getAttribute('hp')).toBe(77); + expect(target.getAttribute('tags')).toEqual(['copied']); + expect(target.hasSpecial(7)).toBe(false); + expect(target.getSpecial(6)).not.toBe(source.getSpecial(6)); + expect(target.getSpecial(6)!.getValue()).toBe(3); + + source.setAttribute('hp', 1); + + expect(target.getAttribute('hp')).toBe(77); + }); +}); diff --git a/packages-user/data-base/src/enemy/special.test.ts b/packages-user/data-base/src/enemy/special.test.ts index 78aef86..14b07e2 100644 --- a/packages-user/data-base/src/enemy/special.test.ts +++ b/packages-user/data-base/src/enemy/special.test.ts @@ -1,6 +1,7 @@ // 测试 special 数据模型:可序列化特殊属性的数值/名称/描述单元,以及无属性特殊属性 import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { type ICommonSpecialConfig } from './special'; +import { type ISpecial } from './types'; vi.hoisted(() => { vi.stubGlobal('main', { replayChecking: true }); @@ -62,6 +63,30 @@ function makeNoneConfig(): ICommonSpecialConfig { }; } +/** + * 构造一个内联配置的可序列化数值特殊属性实例 + * @param code 特殊属性代码 + * @param value 特殊属性数值 + */ +function createValueSpecial(code: number, value: number): ISpecial { + return modules.defineCommonSerializableSpecial( + code, + value, + makeCommonConfig() + )(undefined as never); +} + +/** + * 构造一个内联配置的无属性特殊属性实例 + * @param code 特殊属性代码 + */ +function createNoneSpecial(code: number): ISpecial { + return modules.defineNonePropertySpecial( + code, + makeNoneConfig() + )(undefined as never); +} + describe('CommonSerializableSpecial units', () => { // 验证 setValue 更新数值且 getValue 返回最新数值 it('writes and reads the serializable value', () => { @@ -134,3 +159,45 @@ describe('NonePropertySpecial units', () => { expect(special.getDescription()).toBe('怪物首先攻击。'); }); }); + +describe('Special clone and deep equality', () => { + // 验证可序列化特殊属性的 clone 深拷贝数值且与来源互相独立 + it('clones a serializable special without aliasing its value', () => { + const source = createValueSpecial(6, 3); + const clone = source.clone(); + + expect(clone).not.toBe(source); + expect(clone.code).toBe(6); + expect(clone.getValue()).toBe(3); + + clone.setValue(9); + + expect(source.getValue()).toBe(3); + expect(clone.getValue()).toBe(9); + }); + + // 验证可序列化特殊属性按 code 与深比较数值判定相等 + it('compares serializable specials by code and value', () => { + expect( + createValueSpecial(6, 3).deepEqualsTo(createValueSpecial(6, 3)) + ).toBe(true); + expect( + createValueSpecial(6, 3).deepEqualsTo(createValueSpecial(6, 5)) + ).toBe(false); + expect( + createValueSpecial(6, 3).deepEqualsTo(createValueSpecial(7, 3)) + ).toBe(false); + }); + + // 验证无属性特殊属性的 clone 独立且 deepEqualsTo 仅按 code 判定 + it('clones a none-property special and compares by code only', () => { + const source = createNoneSpecial(1); + const clone = source.clone(); + + expect(clone).not.toBe(source); + expect(clone.code).toBe(1); + expect(clone.getValue()).toBeUndefined(); + expect(clone.deepEqualsTo(source)).toBe(true); + expect(clone.deepEqualsTo(createNoneSpecial(2))).toBe(false); + }); +});