mirror of
https://github.com/motajs/template.git
synced 2026-10-09 14:00:18 +08:00
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.
This commit is contained in:
parent
17e292274b
commit
1bfe7080d5
@ -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<IEnemyTestAttr> = {
|
||||
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<readonly [number, string]>
|
||||
): 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<IEnemyTestAttr> {
|
||||
return new modules.EnemyManager<IEnemyTestAttr>(bridge);
|
||||
function createManager(
|
||||
entries: ReadonlyArray<readonly [number, string]> = []
|
||||
): IEnemyManager<IEnemyTestAttr> {
|
||||
return new modules.EnemyManager<IEnemyTestAttr>(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 的修改回调不会被调用
|
||||
|
||||
@ -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<IEnemyTestAttr> = {
|
||||
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<number> {
|
||||
|
||||
/** 构造一个独立的怪物管理器实例 */
|
||||
function createManager(): IEnemyManager<IEnemyTestAttr> {
|
||||
return new modules.EnemyManager<IEnemyTestAttr>(bridge);
|
||||
return new modules.EnemyManager<IEnemyTestAttr>(new TileStore());
|
||||
}
|
||||
|
||||
/** 构造一个只按生命值判定相等的内联怪物比较器 */
|
||||
|
||||
@ -193,7 +193,7 @@ export interface IEnemyManager<TAttr> extends ISaveableContent<
|
||||
* 获取指定怪物的模板
|
||||
* @param token 怪物图块数字或id
|
||||
*/
|
||||
getPrefab(token: number): IReadonlyEnemy<TAttr> | null;
|
||||
getPrefab(token: number | string): IReadonlyEnemy<TAttr> | null;
|
||||
|
||||
/**
|
||||
* 删除指定的怪物模板
|
||||
|
||||
@ -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<number>('score')).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<number>('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<number>('score')).toBe(7);
|
||||
expect(system.occupied('extra')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user