mirror of
https://github.com/motajs/template.git
synced 2026-10-09 22:10:23 +08:00
fix(07-11): #06-17-6 preserve follower instances across loadState
- HeroFollowersController 新增 restoreFollowers 保留式恢复入口 - 同索引同图块数字原地 loadState 并保留实例;否则替换槽位并按既有语义触发增删钩子 - 存档中不存在的末尾跟随者按存档为准删除 - HeroState.loadState 改用 restoreFollowers,不再 removeAllFollowers 全量重建(保持同步) - IHeroFollowersController 补声明与 jsDoc;新增同实例/替换/删除/补齐回归用例
This commit is contained in:
parent
9964326210
commit
78f6796915
@ -5,6 +5,7 @@ import {
|
||||
Dir8FaceHandler,
|
||||
FaceDirection,
|
||||
ItemStore,
|
||||
SaveCompression,
|
||||
TileStore,
|
||||
TileType
|
||||
} from '@user/data-common';
|
||||
@ -266,3 +267,103 @@ describe('HeroFollowersController gathering', () => {
|
||||
expect(result.ret.num).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeroFollowersController same-reference restore (#06-17-6)', () => {
|
||||
// 验证同索引同图块数字的跟随者在原地读档,且不触发增删钩子
|
||||
it('keeps the follower instance when the index and number match', () => {
|
||||
const env = createController();
|
||||
const added: number[] = [];
|
||||
const removed: number[] = [];
|
||||
env.controller
|
||||
.addHook({
|
||||
onAddFollower: (_follower, index) => added.push(index),
|
||||
onRemoveFollower: (_follower, index) => removed.push(index)
|
||||
})
|
||||
.load();
|
||||
const follower = env.controller.addFollower(100);
|
||||
follower.location.setPos(2, 3);
|
||||
follower.rendering.setAlpha(0.5);
|
||||
|
||||
const saved = [follower.saveState(SaveCompression.NoCompression)];
|
||||
follower.location.setPos(9, 9);
|
||||
follower.rendering.setAlpha(1);
|
||||
|
||||
env.controller.restoreFollowers(saved, SaveCompression.NoCompression);
|
||||
|
||||
expect(env.controller.getFollower(0)).toBe(follower);
|
||||
expect(follower.location.x).toBe(2);
|
||||
expect(follower.location.y).toBe(3);
|
||||
expect(follower.rendering.alpha).toBe(0.5);
|
||||
expect(added).toEqual([0]);
|
||||
expect(removed).toEqual([]);
|
||||
});
|
||||
|
||||
// 验证同索引但图块数字不同的槽位替换实例并通知移除与新增钩子
|
||||
it('replaces a slot with a different number and notifies both hooks', () => {
|
||||
const env = createController();
|
||||
const added: number[] = [];
|
||||
const removed: number[] = [];
|
||||
env.controller
|
||||
.addHook({
|
||||
onAddFollower: (_follower, index) => added.push(index),
|
||||
onRemoveFollower: (_follower, index) => removed.push(index)
|
||||
})
|
||||
.load();
|
||||
const follower = env.controller.addFollower(100);
|
||||
const save = follower.saveState(SaveCompression.NoCompression);
|
||||
|
||||
env.controller.restoreFollowers(
|
||||
[{ ...save, num: 101 }],
|
||||
SaveCompression.NoCompression
|
||||
);
|
||||
|
||||
expect(env.controller.getFollower(0)).not.toBe(follower);
|
||||
expect(env.controller.getFollower(0)!.num).toBe(101);
|
||||
expect(added).toEqual([0, 0]);
|
||||
expect(removed).toEqual([0]);
|
||||
});
|
||||
|
||||
// 验证存档短于现有列表时末尾跟随者被删除并通知移除钩子
|
||||
it('deletes trailing followers absent from the save', () => {
|
||||
const env = createController();
|
||||
const removed: number[] = [];
|
||||
env.controller
|
||||
.addHook({
|
||||
onRemoveFollower: (_follower, index) => removed.push(index)
|
||||
})
|
||||
.load();
|
||||
const first = env.controller.addFollower(100);
|
||||
env.controller.addFollower(100);
|
||||
|
||||
env.controller.restoreFollowers(
|
||||
[first.saveState(SaveCompression.NoCompression)],
|
||||
SaveCompression.NoCompression
|
||||
);
|
||||
|
||||
expect(env.controller.getAllFollowers()).toEqual([first]);
|
||||
expect(removed).toEqual([1]);
|
||||
});
|
||||
|
||||
// 验证存档长于现有列表时按索引补齐新跟随者并通知新增钩子
|
||||
it('appends followers missing from the live list', () => {
|
||||
const env = createController();
|
||||
const added: number[] = [];
|
||||
env.controller
|
||||
.addHook({
|
||||
onAddFollower: (_follower, index) => added.push(index)
|
||||
})
|
||||
.load();
|
||||
const first = env.controller.addFollower(100);
|
||||
const save = first.saveState(SaveCompression.NoCompression);
|
||||
|
||||
env.controller.restoreFollowers(
|
||||
[save, { ...save }],
|
||||
SaveCompression.NoCompression
|
||||
);
|
||||
|
||||
const all = env.controller.getAllFollowers();
|
||||
expect(all[0]).toBe(first);
|
||||
expect(all.length).toBe(2);
|
||||
expect(added).toEqual([0, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -113,13 +113,21 @@ export class HeroFollowersController
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
addFollower(num: number | string): IHeroFollower {
|
||||
/**
|
||||
* 在勇士当前位置创建一个跟随者对象,此时不加入跟随者列表
|
||||
* @param num 跟随者的图块数字或图块 id
|
||||
*/
|
||||
private createFollower(num: number | string): HeroFollower {
|
||||
const loc: IFacedTileLocator = {
|
||||
x: this.heroLocation.x,
|
||||
y: this.heroLocation.y,
|
||||
direction: this.heroLocation.mover.faceDirection
|
||||
};
|
||||
const follower = new HeroFollower(num, loc, this.faceHandler, this);
|
||||
return new HeroFollower(num, loc, this.faceHandler, this);
|
||||
}
|
||||
|
||||
addFollower(num: number | string): IHeroFollower {
|
||||
const follower = this.createFollower(num);
|
||||
this.followers.push(follower);
|
||||
const index = this.followers.length - 1;
|
||||
this.forEachHook(hook => {
|
||||
@ -128,6 +136,41 @@ export class HeroFollowersController
|
||||
return follower;
|
||||
}
|
||||
|
||||
restoreFollowers(
|
||||
saves: readonly IHeroFollowerSave[],
|
||||
compression: SaveCompression
|
||||
): void {
|
||||
// 以存档为准:先删除末尾多出的跟随者,并按原索引通知移除
|
||||
const extra = this.followers.splice(saves.length);
|
||||
for (let i = 0; i < extra.length; i++) {
|
||||
const index = saves.length + i;
|
||||
this.forEachHook(hook => {
|
||||
hook.onRemoveFollower?.(extra[i], index);
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < saves.length; i++) {
|
||||
const save = saves[i];
|
||||
const current = this.followers[i];
|
||||
// 索引相同且图块数字相同:在保留的实例上原地读档,此时不触发增删钩子
|
||||
if (current && current.num === save.num) {
|
||||
current.loadState(save, compression);
|
||||
continue;
|
||||
}
|
||||
// 索引不同或图块数字不同:该槽位替换为新实例,并按既有钩子语义通知增删
|
||||
if (current) {
|
||||
this.forEachHook(hook => {
|
||||
hook.onRemoveFollower?.(current, i);
|
||||
});
|
||||
}
|
||||
const follower = this.createFollower(save.num);
|
||||
follower.loadState(save, compression);
|
||||
this.followers[i] = follower;
|
||||
this.forEachHook(hook => {
|
||||
hook.onAddFollower?.(follower, i);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getFollower(index: number): IHeroFollower | null {
|
||||
return this.followers[index] ?? null;
|
||||
}
|
||||
|
||||
@ -204,6 +204,15 @@ function createHeroState(): IHeroState<IHeroAttr> {
|
||||
);
|
||||
}
|
||||
|
||||
/** 构造一个已注册跟随者图块、可直接添加跟随者的勇士状态对象 */
|
||||
function createFollowerHero(): IHeroState<IHeroAttr> {
|
||||
return new HeroState<IHeroAttr>(
|
||||
createFollowerState(),
|
||||
new Dir8FaceHandler(),
|
||||
new HeroAttribute<IHeroAttr>(createBaseAttr())
|
||||
);
|
||||
}
|
||||
|
||||
/** 构造一个已注册装备定义并设置好装备槽的勇士状态对象(基础 atk 为 10) */
|
||||
function createEquipHero(): IHeroState<IHeroAttr> {
|
||||
const env = createEquipEnv();
|
||||
@ -741,3 +750,43 @@ describe('HeroState same-reference equipment load (#06-17-4)', () => {
|
||||
expect(hero.items.equipment.get(extra)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeroState same-reference follower load (#06-17-6)', () => {
|
||||
// 验证经勇士容器三档往返后同索引同图块数字的跟随者为同一实例,位置与渲染恢复到存档点
|
||||
it('keeps the follower instance and restores its state across all compressions', () => {
|
||||
for (const compression of SAVE_COMPRESSIONS) {
|
||||
const hero = createFollowerHero();
|
||||
const follower = hero.followers.addFollower(100);
|
||||
follower.location.setPos(2, 3);
|
||||
follower.location.mover.setFaceDir(FaceDirection.Up);
|
||||
follower.rendering.setAlpha(0.5);
|
||||
|
||||
const saved = hero.saveState(compression);
|
||||
follower.location.setPos(9, 9);
|
||||
follower.location.mover.setFaceDir(FaceDirection.Down);
|
||||
follower.rendering.setAlpha(1);
|
||||
|
||||
hero.loadState(saved, compression);
|
||||
|
||||
expect(hero.followers.getFollower(0)).toBe(follower);
|
||||
expect(follower.location.x).toBe(2);
|
||||
expect(follower.location.y).toBe(3);
|
||||
expect(follower.rendering.alpha).toBe(0.5);
|
||||
}
|
||||
});
|
||||
|
||||
// 验证跟随者数量以存档为准:存档中不存在的跟随者读档后被删除
|
||||
it('deletes followers absent from the save', () => {
|
||||
const hero = createFollowerHero();
|
||||
const kept = hero.followers.addFollower(100);
|
||||
|
||||
const saved = hero.saveState(SaveCompression.NoCompression);
|
||||
hero.followers.addFollower(100);
|
||||
|
||||
hero.loadState(saved, SaveCompression.NoCompression);
|
||||
|
||||
const all = hero.followers.getAllFollowers();
|
||||
expect(all).toHaveLength(1);
|
||||
expect(all[0]).toBe(kept);
|
||||
});
|
||||
});
|
||||
|
||||
@ -145,10 +145,7 @@ export class HeroState<THero>
|
||||
this.rendering.loadState(state.rendering, compression);
|
||||
this.items.loadState(state.items, compression);
|
||||
this.equip.loadState(state.equip, compression);
|
||||
void this.followers.removeAllFollowers();
|
||||
for (const save of state.followers) {
|
||||
const follower = this.followers.addFollower(save.num);
|
||||
follower.loadState(save, compression);
|
||||
}
|
||||
// 跟随者保留式恢复:同索引同图块数字的实例原地读档,其余按存档替换或删除
|
||||
this.followers.restoreFollowers(state.followers, compression);
|
||||
}
|
||||
}
|
||||
|
||||
@ -506,6 +506,19 @@ export interface IHeroFollowersController
|
||||
*/
|
||||
addFollower(num: number | string): IHeroFollower;
|
||||
|
||||
/**
|
||||
* 按存档恢复跟随者列表:索引相同且图块数字相同的跟随者在其原实例上原地读档,
|
||||
* 索引相同但图块数字不同的槽位替换为新实例,存档中不存在的末尾跟随者按存档为准删除。
|
||||
* 仅发生替换或删除时触发 `onRemoveFollower`,仅新建实例时触发 `onAddFollower`,
|
||||
* 原地读档的跟随者不触发增删钩子。
|
||||
* @param saves 跟随者存档列表
|
||||
* @param compression 存档压缩级别
|
||||
*/
|
||||
restoreFollowers(
|
||||
saves: readonly IHeroFollowerSave[],
|
||||
compression: SaveCompression
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 根据跟随者的索引数字获取跟随者对象
|
||||
* @param index 跟随者的索引数字
|
||||
|
||||
Loading…
Reference in New Issue
Block a user