refactor: 合并 follower 和 followersController

This commit is contained in:
unanmed 2026-06-28 16:39:42 +08:00
parent 867a3c480f
commit 3e150c4a1d
5 changed files with 143 additions and 156 deletions

View File

@ -1,4 +1,10 @@
import { IFacedTileLocator, logger } from '@motajs/common';
import {
Hookable,
HookController,
IFacedTileLocator,
IHookController,
logger
} from '@motajs/common';
import {
FaceDirection,
IDataCommon,
@ -11,6 +17,7 @@ import {
IHeroFollower,
IHeroFollowerSave,
IHeroFollowersController,
IHeroFollowersControllerHooks,
IHeroLocation,
IHeroRendering
} from './types';
@ -76,3 +83,135 @@ export class HeroFollower implements IHeroFollower {
this.location.loadState(state.location, compression);
}
}
export class HeroFollowersController
extends Hookable<IHeroFollowersControllerHooks>
implements IHeroFollowersController
{
readonly state: IDataCommon;
/** 跟随者列表 */
private readonly followers: HeroFollower[] = [];
/** 勇士位置对象,用于获取聚集目标位置 */
private readonly heroLocation: IHeroLocation;
/** 朝向处理器,传递给新创建的跟随者 */
private readonly faceHandler: IFaceHandler<FaceDirection>;
constructor(
state: IDataCommon,
heroLocation: IHeroLocation,
faceHandler: IFaceHandler<FaceDirection>
) {
super();
this.state = state;
this.heroLocation = heroLocation;
this.faceHandler = faceHandler;
}
protected createController(
hook: Partial<IHeroFollowersControllerHooks>
): IHookController<IHeroFollowersControllerHooks> {
return new HookController(this, hook);
}
addFollower(num: number | string): IHeroFollower {
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);
this.followers.push(follower);
const index = this.followers.length - 1;
this.forEachHook(hook => {
hook.onAddFollower?.(follower, index);
});
return follower;
}
getFollower(index: number): IHeroFollower | null {
return this.followers[index] ?? null;
}
*getFollowersById(
num: number | string
): IterableIterator<[number, IHeroFollower]> {
const store = this.state.tileStore;
const target = typeof num === 'string' ? store.idToNumber(num)! : num;
for (let i = 0; i < this.followers.length; i++) {
if (this.followers[i].num === target) {
yield [i, this.followers[i]];
}
}
}
getAllFollowers(): IHeroFollower[] {
return this.followers.slice();
}
async removeFollower(index: number): Promise<void> {
const removed = this.followers.splice(index, 1);
if (removed.length > 0) {
this.forEachHook(hook => {
hook.onRemoveFollower?.(removed[0], index);
});
}
}
async removeAllFollowers(): Promise<void> {
const snapshot = this.followers.slice();
this.followers.length = 0;
this.forEachHook(hook => {
snapshot.forEach((v, i) => {
hook.onRemoveFollower?.(v, i);
});
});
}
async gatherFollowers(): Promise<void> {
const { x, y, mover } = this.heroLocation;
for (let i = 0; i < this.followers.length; i++) {
const follower = this.followers[i];
follower.location.mover.clear();
let skip = false;
for (let j = i - 1; j >= 0; j--) {
const dir = this.followers[j].location.mover.moveDirection;
if (dir === FaceDirection.Unknown) {
follower.location.mover.clear();
follower.location.mover.tp(x, y);
follower.location.mover.face(mover.faceDirection);
skip = true;
break;
} else {
follower.location.mover.step(dir);
}
}
if (!skip) {
follower.location.mover.step(mover.moveDirection);
}
}
const controllers = this.followers.map(
v => v.location.mover.start()?.onEnd
);
await Promise.all(controllers);
this.forEachHook(hook => {
hook.onGatherFollowers?.(false);
});
}
gatherFollowersSync(): void {
const { x, y } = this.heroLocation;
const dir = this.heroLocation.mover.faceDirection;
for (const follower of this.followers) {
const mover = follower.location.mover;
mover.setPos(x, y);
mover.setFaceDir(dir);
}
this.forEachHook(hook => {
hook.onGatherFollowers?.(true);
});
}
}

View File

@ -1,146 +0,0 @@
import {
Hookable,
HookController,
IFacedTileLocator,
IHookController
} from '@motajs/common';
import { FaceDirection, IDataCommon, IFaceHandler } from '@user/data-common';
import { HeroFollower } from './follower';
import {
IHeroFollowersController,
IHeroFollowersControllerHooks,
IHeroFollower,
IHeroLocation
} from './types';
export class HeroFollowersController
extends Hookable<IHeroFollowersControllerHooks>
implements IHeroFollowersController
{
readonly state: IDataCommon;
/** 跟随者列表 */
private readonly followers: HeroFollower[] = [];
/** 勇士位置对象,用于获取聚集目标位置 */
private readonly heroLocation: IHeroLocation;
/** 朝向处理器,传递给新创建的跟随者 */
private readonly faceHandler: IFaceHandler<FaceDirection>;
constructor(
state: IDataCommon,
heroLocation: IHeroLocation,
faceHandler: IFaceHandler<FaceDirection>
) {
super();
this.state = state;
this.heroLocation = heroLocation;
this.faceHandler = faceHandler;
}
protected createController(
hook: Partial<IHeroFollowersControllerHooks>
): IHookController<IHeroFollowersControllerHooks> {
return new HookController(this, hook);
}
addFollower(num: number | string): IHeroFollower {
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);
this.followers.push(follower);
const index = this.followers.length - 1;
this.forEachHook(hook => {
hook.onAddFollower?.(follower, index);
});
return follower;
}
getFollower(index: number): IHeroFollower | null {
return this.followers[index] ?? null;
}
*getFollowersById(
num: number | string
): IterableIterator<[number, IHeroFollower]> {
const store = this.state.tileStore;
const target = typeof num === 'string' ? store.idToNumber(num)! : num;
for (let i = 0; i < this.followers.length; i++) {
if (this.followers[i].num === target) {
yield [i, this.followers[i]];
}
}
}
getAllFollowers(): IHeroFollower[] {
return this.followers.slice();
}
async removeFollower(index: number): Promise<void> {
const removed = this.followers.splice(index, 1);
if (removed.length > 0) {
this.forEachHook(hook => {
hook.onRemoveFollower?.(removed[0], index);
});
}
}
async removeAllFollowers(): Promise<void> {
const snapshot = this.followers.slice();
this.followers.length = 0;
this.forEachHook(hook => {
snapshot.forEach((v, i) => {
hook.onRemoveFollower?.(v, i);
});
});
}
async gatherFollowers(): Promise<void> {
const { x, y, mover } = this.heroLocation;
for (let i = 0; i < this.followers.length; i++) {
const follower = this.followers[i];
follower.location.mover.clear();
let skip = false;
for (let j = i - 1; j >= 0; j--) {
const dir = this.followers[j].location.mover.moveDirection;
if (dir === FaceDirection.Unknown) {
follower.location.mover.clear();
follower.location.mover.tp(x, y);
follower.location.mover.face(mover.faceDirection);
skip = true;
break;
} else {
follower.location.mover.step(dir);
}
}
if (!skip) {
follower.location.mover.step(mover.moveDirection);
}
}
const controllers = this.followers.map(
v => v.location.mover.start()?.onEnd
);
await Promise.all(controllers);
this.forEachHook(hook => {
hook.onGatherFollowers?.(false);
});
}
gatherFollowersSync(): void {
const { x, y } = this.heroLocation;
const dir = this.heroLocation.mover.faceDirection;
for (const follower of this.followers) {
const mover = follower.location.mover;
mover.setPos(x, y);
mover.setFaceDir(dir);
}
this.forEachHook(hook => {
hook.onGatherFollowers?.(true);
});
}
}

View File

@ -1,6 +1,5 @@
export * from './attribute';
export * from './follower';
export * from './followersController';
export * from './location';
export * from './rendering';
export * from './mover';

View File

@ -57,17 +57,12 @@ export class HeroMover<T extends IHeroLocation>
protected async onMoveStart(
_tile: IHeroLocation,
_controller: Readonly<IMoverController>
): Promise<void> {
// TODO: 通知渲染端开始移动,同步平滑视角
}
): Promise<void> {}
protected async onMoveEnd(
_tile: IHeroLocation,
_controller: Readonly<IMoverController>
): Promise<void> {
// TODO: 通知渲染端移动结束
// TODO: 清除自动寻路状态
}
): Promise<void> {}
protected async onStepStart(
step: ObjectMoveStep,

View File

@ -1,5 +1,5 @@
import { HeroAttribute } from './attribute';
import { HeroFollowersController } from './followersController';
import { HeroFollowersController } from './follower';
import { HeroLocation } from './location';
import { HeroRendering } from './rendering';
import {