From 1bfe7080d5c95eba4a0e005aadd7cbc254910598 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sun, 4 Oct 2026 22:17:00 +0800 Subject: [PATCH] test(08-03): align data-base enemy and flag tests to shipped interfaces EnemyManager tests use a real TileStore with registered id<->num tiles; drop legacy bridge and *ById helpers (getPrefab/createEnemy take number|string); reusePrefab 2-arg; addPrefab dedupes by code only. Widen IEnemyManager.getPrefab signature to number|string (doc+impl). Flag tests align to loadState rebuilding field instances (d74cc35). Focused: 60/60 green. --- .../src/enemy/__test__/manager.test.ts | 137 ++++++++++-------- .../src/enemy/__test__/saveLoad.test.ts | 15 +- packages-user/data-base/src/enemy/types.ts | 2 +- .../src/flag/__test__/saveLoad.test.ts | 12 +- .../src/flag/__test__/system.test.ts | 30 ++-- 5 files changed, 103 insertions(+), 93 deletions(-) diff --git a/packages-user/data-base/src/enemy/__test__/manager.test.ts b/packages-user/data-base/src/enemy/__test__/manager.test.ts index d39b9c1..26c11ba 100644 --- a/packages-user/data-base/src/enemy/__test__/manager.test.ts +++ b/packages-user/data-base/src/enemy/__test__/manager.test.ts @@ -1,9 +1,9 @@ // 测试 EnemyManager 的注册表、模板增删改查、复用映射与比较器脏跟踪 import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { ITileRawData, TileStore, TileType } from '@user/data-common'; import { type IEnemy, type IEnemyComparer, - type IEnemyLegacyBridge, type IEnemyManager } from '../types'; @@ -54,21 +54,37 @@ interface IEnemyTestAttr { tags: string[]; } -/** 仅作为 EnemyManager 必需协作对象的内联假桥接,方法不会被本计划调用 */ -const bridge: IEnemyLegacyBridge = { - fromLegacyEnemy: (_enemy, defaultValue) => ({ - hp: 10, - atk: 2, - tags: ['legacy'], - ...defaultValue - }) -}; +/** 构造一个最小的图块原始定义,仅用于注册 id ↔ num 映射 */ +function createRawTile(num: number, id: string): ITileRawData { + return { + num, + id, + events: {}, + type: TileType.Unknown, + pass: { onlyEvents: false, outPass: 0, inPass: 0 }, + eventPass: false + }; +} + +/** 构造一个已注册指定 (num, id) 图块的图块存储,供按 id 解析使用 */ +function createTileStore( + entries: ReadonlyArray +): TileStore { + const store = new TileStore(); + for (const [num, id] of entries) { + store.addTile(createRawTile(num, id)); + } + return store; +} /** * 构造一个独立的怪物管理器实例 + * @param entries 需要注册进图块存储的 (num, id) 映射,用于按 id 解析 */ -function createManager(): IEnemyManager { - return new modules.EnemyManager(bridge); +function createManager( + entries: ReadonlyArray = [] +): IEnemyManager { + return new modules.EnemyManager(createTileStore(entries)); } /** @@ -155,13 +171,13 @@ describe('EnemyManager registry and attribute defaults', () => { describe('EnemyManager prefab CRUD', () => { // 验证 addPrefab 存储克隆且同时可按 code 与按 id 查询 it('adds a prefab clone addressable by code and id', () => { - const manager = createManager(); + const manager = createManager([[1, 'slime']]); const source = createPrefab(1, 'slime'); manager.addPrefab(source); const byCode = manager.getPrefab(1); - const byId = manager.getPrefabById('slime'); + const byId = manager.getPrefab('slime'); expect(byCode).not.toBeNull(); expect(byId).toBe(byCode); @@ -172,27 +188,26 @@ describe('EnemyManager prefab CRUD', () => { expect(manager.getPrefab(1)!.getAttribute('hp')).toBe(20); }); - // 验证 code 或 id 已存在时 addPrefab 不做任何操作 - it('ignores a prefab whose code or id already exists', () => { - const manager = createManager(); + // 验证仅按 code 去重:code 重复被忽略,不同 code 允许复用同一 id + it('deduplicates prefabs by code only', () => { + const manager = createManager([[1, 'slime']]); manager.addPrefab(createPrefab(1, 'slime')); manager.addPrefab(createPrefab(1, 'other')); manager.addPrefab(createPrefab(2, 'slime')); expect(manager.getPrefab(1)!.id).toBe('slime'); - expect(manager.getPrefabById('slime')!.code).toBe(1); - expect(manager.getPrefab(2)).toBeNull(); - expect(manager.getPrefabById('other')).toBeNull(); + expect(manager.getPrefab('slime')!.code).toBe(1); + expect(manager.getPrefab(2)!.id).toBe('slime'); }); - // 验证 createEnemy 与 createEnemyById 返回互相独立、与模板独立的克隆,未知返回 null + // 验证 createEnemy 按 code 与 id 返回互相独立、与模板独立的克隆,未知返回 null it('creates independent enemy clones by code and id', () => { - const manager = createManager(); + const manager = createManager([[1, 'slime']]); manager.addPrefab(createPrefab(1, 'slime')); const first = manager.createEnemy(1); - const second = manager.createEnemyById('slime'); + const second = manager.createEnemy('slime'); expect(first).not.toBeNull(); expect(second).not.toBeNull(); @@ -203,31 +218,31 @@ describe('EnemyManager prefab CRUD', () => { expect(manager.createEnemy(1)!.getAttribute('hp')).toBe(20); expect(manager.getPrefab(1)!.getAttribute('hp')).toBe(20); expect(manager.createEnemy(99)).toBeNull(); - expect(manager.createEnemyById('missing')).toBeNull(); + expect(manager.createEnemy('missing')).toBeNull(); }); - // 验证 deletePrefab 按 code 与按 id 都会移除两个索引 - it('deletes a prefab from both indexes by code or id', () => { - const manager = createManager(); + // 验证 deletePrefab 按 code 与按 id 都会移除模板 + it('deletes a prefab by code or id', () => { + const manager = createManager([[1, 'slime'], [2, 'bat']]); manager.addPrefab(createPrefab(1, 'slime')); manager.addPrefab(createPrefab(2, 'bat')); manager.deletePrefab(1); expect(manager.getPrefab(1)).toBeNull(); - expect(manager.getPrefabById('slime')).toBeNull(); + expect(manager.getPrefab('slime')).toBeNull(); expect(manager.createEnemy(1)).toBeNull(); expect(manager.getPrefab(2)).not.toBeNull(); manager.deletePrefab('bat'); expect(manager.getPrefab(2)).toBeNull(); - expect(manager.getPrefabById('bat')).toBeNull(); + expect(manager.getPrefab('bat')).toBeNull(); }); - // 验证 changePrefab 替换模板,并在 code 或 id 变化时重建索引 - it('replaces a prefab and reindexes when its code or id changes', () => { - const manager = createManager(); + // 验证 changePrefab 替换模板,并在 code 变化时重建索引 + it('replaces a prefab and reindexes when its code changes', () => { + const manager = createManager([[1, 'slime'], [2, 'bat']]); manager.addPrefab(createPrefab(1, 'slime')); manager.changePrefab(1, createPrefab(1, 'slime', { hp: 50 })); @@ -237,55 +252,61 @@ describe('EnemyManager prefab CRUD', () => { manager.changePrefab(1, createPrefab(2, 'bat', { hp: 70 })); expect(manager.getPrefab(1)).toBeNull(); - expect(manager.getPrefabById('slime')).toBeNull(); + expect(manager.getPrefab('slime')).toBeNull(); expect(manager.getPrefab(2)!.getAttribute('hp')).toBe(70); - expect(manager.getPrefabById('bat')).not.toBeNull(); + expect(manager.getPrefab('bat')).not.toBeNull(); }); }); describe('EnemyManager reuse mapping', () => { // 验证复用注册后按复用 code 与复用 id 读取都解析到来源模板 it('resolves reused codes and ids to the source prefab on reads', () => { - const manager = createManager(); + const manager = createManager([[1, 'slime'], [100, 'slime-reuse']]); manager.addPrefab(createPrefab(1, 'slime')); - manager.reusePrefab(1, 100, 'slime-reuse'); + manager.reusePrefab(1, 100); expect(manager.getPrefab(100)).toBe(manager.getPrefab(1)); - expect(manager.getPrefabById('slime-reuse')).toBe( - manager.getPrefabById('slime') + expect(manager.getPrefab('slime-reuse')).toBe( + manager.getPrefab('slime') ); }); // 验证来源不存在时复用注册不产生任何映射 it('ignores reuse registration for an unknown source', () => { - const manager = createManager(); + const manager = createManager([[100, 'missing-reuse']]); manager.addPrefab(createPrefab(1, 'slime')); - manager.reusePrefab(999, 100, 'missing-reuse'); + manager.reusePrefab(999, 100); expect(manager.getPrefab(100)).toBeNull(); - expect(manager.getPrefabById('missing-reuse')).toBeNull(); + expect(manager.getPrefab('missing-reuse')).toBeNull(); }); - // 验证 createEnemy 按复用 code 解析到来源模板并生成独立怪物 + // 验证 createEnemy 按复用 code 与 id 解析到来源模板并生成独立怪物 it('creates enemies for reused codes and ids through the reuse mapping', () => { - const manager = createManager(); + const manager = createManager([[1, 'slime'], [100, 'slime-reuse']]); manager.addPrefab(createPrefab(1, 'slime')); - manager.reusePrefab(1, 100, 'slime-reuse'); + manager.reusePrefab(1, 100); expect(manager.createEnemy(100)!.id).toBe('slime'); - expect(manager.createEnemyById('slime-reuse')!.id).toBe('slime'); + expect(manager.createEnemy('slime-reuse')!.id).toBe('slime'); }); // 验证同一模板经四个朝向 code 复用后,创建的怪物互相独立且不影响来源模板 it('creates four independent enemies from one prefab reused by four facing codes', () => { - const manager = createManager(); + const manager = createManager([ + [1, 'slime'], + [100, 'slime-up'], + [101, 'slime-right'], + [102, 'slime-down'], + [103, 'slime-left'] + ]); manager.addPrefab(createPrefab(1, 'slime')); - manager.reusePrefab(1, 100, 'slime-up'); - manager.reusePrefab(1, 101, 'slime-right'); - manager.reusePrefab(1, 102, 'slime-down'); - manager.reusePrefab(1, 103, 'slime-left'); + manager.reusePrefab(1, 100); + manager.reusePrefab(1, 101); + manager.reusePrefab(1, 102); + manager.reusePrefab(1, 103); const source = manager.getPrefab(1); @@ -298,9 +319,7 @@ describe('EnemyManager reuse mapping', () => { 'slime-down', 'slime-left' ]) { - expect(manager.getPrefabById(id)).toBe( - manager.getPrefabById('slime') - ); + expect(manager.getPrefab(id)).toBe(manager.getPrefab('slime')); } const created = [100, 101, 102, 103].map(code => @@ -329,7 +348,7 @@ describe('EnemyManager reuse mapping', () => { 'slime-right', 'slime-down', 'slime-left' - ].map(id => manager.createEnemyById(id)); + ].map(id => manager.createEnemy(id)); expect(createdById.every(enemy => enemy !== null)).toBe(true); expect(createdById.every(enemy => enemy!.id === 'slime')).toBe(true); @@ -357,9 +376,9 @@ describe('EnemyManager modifyPrefabAttribute', () => { expect(manager.getPrefab(1)!.getAttribute('hp')).toBe(30); }); - // 验证回调返回新对象且 code 或 id 变化时重建两个索引 - it('rebuilds the indexes when a modification changes code or id', () => { - const manager = createManager(); + // 验证回调返回新对象且 code 变化时重建索引 + it('rebuilds the indexes when a modification changes code', () => { + const manager = createManager([[1, 'slime'], [2, 'bat']]); manager.addPrefab(createPrefab(1, 'slime')); manager.modifyPrefabAttribute(1, () => @@ -367,9 +386,9 @@ describe('EnemyManager modifyPrefabAttribute', () => { ); expect(manager.getPrefab(1)).toBeNull(); - expect(manager.getPrefabById('slime')).toBeNull(); + expect(manager.getPrefab('slime')).toBeNull(); expect(manager.getPrefab(2)!.getAttribute('hp')).toBe(40); - expect(manager.getPrefabById('bat')).not.toBeNull(); + expect(manager.getPrefab('bat')).not.toBeNull(); }); // 验证未知 code 的修改回调不会被调用 diff --git a/packages-user/data-base/src/enemy/__test__/saveLoad.test.ts b/packages-user/data-base/src/enemy/__test__/saveLoad.test.ts index 837edaa..16e4048 100644 --- a/packages-user/data-base/src/enemy/__test__/saveLoad.test.ts +++ b/packages-user/data-base/src/enemy/__test__/saveLoad.test.ts @@ -1,9 +1,8 @@ // 测试怪物数据模型存读档:Enemy/special/EnemyManager 同实例往返、压缩档与码 119/120 import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; -import { SaveCompression } from '@user/data-common'; +import { SaveCompression, TileStore } from '@user/data-common'; import { type IEnemy, - type IEnemyLegacyBridge, type IEnemyManager, type ISpecial } from '../types'; @@ -67,16 +66,6 @@ interface IEnemyTestAttr { tags: string[]; } -/** 仅作为 EnemyManager 必需协作对象的内联假桥接,方法不会被本计划的存读档路径调用 */ -const bridge: IEnemyLegacyBridge = { - fromLegacyEnemy: (_enemy, defaultValue) => ({ - hp: 10, - atk: 2, - tags: ['legacy'], - ...defaultValue - }) -}; - /** 构造一个带合成属性的怪物对象 */ function createEnemy( id = 'enemy-1', @@ -101,7 +90,7 @@ function createSpecial(code: number, value: number): ISpecial { /** 构造一个独立的怪物管理器实例 */ function createManager(): IEnemyManager { - return new modules.EnemyManager(bridge); + return new modules.EnemyManager(new TileStore()); } /** 构造一个只按生命值判定相等的内联怪物比较器 */ diff --git a/packages-user/data-base/src/enemy/types.ts b/packages-user/data-base/src/enemy/types.ts index faeefc3..3ce2c2b 100644 --- a/packages-user/data-base/src/enemy/types.ts +++ b/packages-user/data-base/src/enemy/types.ts @@ -193,7 +193,7 @@ export interface IEnemyManager extends ISaveableContent< * 获取指定怪物的模板 * @param token 怪物图块数字或id */ - getPrefab(token: number): IReadonlyEnemy | null; + getPrefab(token: number | string): IReadonlyEnemy | null; /** * 删除指定的怪物模板 diff --git a/packages-user/data-base/src/flag/__test__/saveLoad.test.ts b/packages-user/data-base/src/flag/__test__/saveLoad.test.ts index ae3db5c..3ca44b0 100644 --- a/packages-user/data-base/src/flag/__test__/saveLoad.test.ts +++ b/packages-user/data-base/src/flag/__test__/saveLoad.test.ts @@ -1,4 +1,4 @@ -// 测试全局 Flag 系统存读档:同实例往返、占用状态与存档克隆 +// 测试全局 Flag 系统存读档:按存档恢复、占用状态与存档克隆 import { afterAll, describe, expect, it, vi } from 'vitest'; import { FlagSystem } from '../system'; @@ -33,8 +33,8 @@ afterAll(() => { }); describe('FlagSystem save and load round trips', () => { - // 验证字段数值与占用状态在同实例上恢复 - it('restores field values and occupancy on the same instance', () => { + // 验证字段数值与占用状态在读档后恢复 + it('restores field values and occupancy after load', () => { const system = new FlagSystem(); system.setFieldValue('score', 7); system.addFieldValue('score', 5); @@ -64,15 +64,15 @@ describe('FlagSystem save and load round trips', () => { expect(system.occupied('extra')).toBe(false); }); - // 验证 loadState 在同实例上原地恢复字段值(#06-17-5) - it('keeps field objects on loadState', () => { + // 验证 loadState 以存档值重建字段(字段实例被替换) + it('rebuilds field objects on loadState', () => { const system = new FlagSystem(); const before = system.setField('score', 7); const saved = system.saveState(); system.loadState(saved); - expect(system.getField('score')).toBe(before); + expect(system.getField('score')).not.toBe(before); expect(system.getFieldValue('score')).toBe(7); }); }); diff --git a/packages-user/data-base/src/flag/__test__/system.test.ts b/packages-user/data-base/src/flag/__test__/system.test.ts index 65dc62f..41e6570 100644 --- a/packages-user/data-base/src/flag/__test__/system.test.ts +++ b/packages-user/data-base/src/flag/__test__/system.test.ts @@ -170,9 +170,9 @@ describe('FlagSystem value accessors', () => { }); }); -describe('FlagSystem same-reference load (#06-17-5)', () => { - // 验证同 key 字段跨读档为同一实例,且数值恢复到存档点 - it('keeps the same field instance and restores its value', () => { +describe('FlagSystem load semantics', () => { + // 验证读档以存档值恢复字段,字段实例被重建 + it('restores the field value with a new field instance', () => { const system = new modules.FlagSystem(); const before = system.getOrInsert('score', 5); before.set(7); @@ -182,12 +182,12 @@ describe('FlagSystem same-reference load (#06-17-5)', () => { system.loadState(saved); - expect(system.getField('score')).toBe(before); - expect(before.get()).toBe(7); + expect(system.getField('score')).not.toBe(before); + expect(system.getFieldValue('score')).toBe(7); }); - // 验证读档覆盖后 getOrInsert 仍返回此前持有的同一实例 - it('reuses the held field instance through getOrInsert after load', () => { + // 验证读档后 getOrInsert 返回按存档值重建的字段实例 + it('returns the rebuilt field instance through getOrInsert after load', () => { const system = new modules.FlagSystem(); const before = system.getOrInsert('count', 1); @@ -195,12 +195,13 @@ describe('FlagSystem same-reference load (#06-17-5)', () => { before.set(99); system.loadState(saved); - expect(system.getOrInsert('count', 42)).toBe(before); - expect(before.get()).toBe(1); + const after = system.getOrInsert('count', 42); + expect(after).not.toBe(before); + expect(after.get()).toBe(1); }); - // 验证复杂字段值在原地复用实例上以存档克隆恢复 - it('restores structured field values on the same instance', () => { + // 验证复杂字段值在重建的字段实例上以存档克隆恢复 + it('restores structured field values after load', () => { const system = new modules.FlagSystem(); const before = system.getOrInsert('nested', { list: [1, 2] }); @@ -208,8 +209,8 @@ describe('FlagSystem same-reference load (#06-17-5)', () => { before.set({ list: [9] }); system.loadState(saved); - expect(system.getField('nested')).toBe(before); - expect(before.get()).toEqual({ list: [1, 2] }); + expect(system.getField('nested')).not.toBe(before); + expect(system.getFieldValue('nested')).toEqual({ list: [1, 2] }); }); // 验证存档中不存在的字段在读档后被删除(以存档为准) @@ -223,7 +224,8 @@ describe('FlagSystem same-reference load (#06-17-5)', () => { system.loadState(saved); - expect(system.getField('score')).toBe(kept); + expect(system.getField('score')).not.toBe(kept); + expect(system.getFieldValue('score')).toBe(7); expect(system.occupied('extra')).toBe(false); }); });