mirror of
https://github.com/motajs/template.git
synced 2026-09-12 09:48:49 +08:00
fix(02-03): support pathfinding interruption handoff
- Queue new movement until the stopped controller settles - Register pathfinding controller diagnostics and regression coverage
This commit is contained in:
parent
6cde84ec31
commit
7a3b6e35c1
@ -190,6 +190,29 @@ function addEvent(
|
||||
});
|
||||
}
|
||||
|
||||
function addBlockingEvent(
|
||||
events: Map<string, object>,
|
||||
id: string,
|
||||
trigger: number,
|
||||
calls: EventCall[],
|
||||
blocked: Promise<void>
|
||||
): void {
|
||||
events.set(id, {
|
||||
trigger,
|
||||
execute: async (
|
||||
_param: unknown,
|
||||
env: {
|
||||
trigger: number;
|
||||
heroLocator: Readonly<{ x: number; y: number }>;
|
||||
}
|
||||
) => {
|
||||
calls.push({ id, trigger: env.trigger, hero: env.heroLocator });
|
||||
await blocked;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('hero pathfinding integration', () => {
|
||||
// 验证勇士按最小路径逐步移动并按事件链顺序执行途经事件
|
||||
it('moves the hero to the target and triggers the traversed event', async () => {
|
||||
@ -282,4 +305,54 @@ describe('hero pathfinding integration', () => {
|
||||
y: 0
|
||||
});
|
||||
});
|
||||
|
||||
// 验证新寻路会停止旧移动并从兑现后的最新坐标接管
|
||||
it('hands over a moving path to a new target', async () => {
|
||||
const calls: EventCall[] = [];
|
||||
let release: () => void = () => {};
|
||||
const blocked = new Promise<void>(resolve => {
|
||||
release = resolve;
|
||||
});
|
||||
const fixture = createFixture();
|
||||
addBlockingEvent(fixture.events, 'middle-enter', 2, calls, blocked);
|
||||
|
||||
fixture.pathfinding.moveTo({ x: 2, y: 0 });
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
const next = fixture.pathfinding.moveTo({ x: 0, y: 0 });
|
||||
expect(next).not.toBeNull();
|
||||
release();
|
||||
await next!.controller.onEnd;
|
||||
|
||||
expect({ x: fixture.hero.x, y: fixture.hero.y }).toEqual({
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
});
|
||||
|
||||
// 验证显式打断后移动器可再次启动新的寻路
|
||||
it('leaves the mover restartable after explicit interruption', async () => {
|
||||
const calls: EventCall[] = [];
|
||||
let release: () => void = () => {};
|
||||
const blocked = new Promise<void>(resolve => {
|
||||
release = resolve;
|
||||
});
|
||||
const fixture = createFixture();
|
||||
addBlockingEvent(fixture.events, 'middle-enter', 2, calls, blocked);
|
||||
|
||||
fixture.pathfinding.moveTo({ x: 2, y: 0 });
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
const interrupted = fixture.pathfinding.interrupt();
|
||||
release();
|
||||
await interrupted;
|
||||
|
||||
const resumed = fixture.pathfinding.moveTo({ x: 0, y: 0 });
|
||||
expect(resumed).not.toBeNull();
|
||||
await resumed!.controller.onEnd;
|
||||
expect({ x: fixture.hero.x, y: fixture.hero.y }).toEqual({
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ITileLocator } from '@motajs/common';
|
||||
import { ITileLocator, logger } from '@motajs/common';
|
||||
import {
|
||||
BlockEventType,
|
||||
IBlockEventEnv,
|
||||
@ -87,11 +87,74 @@ class HeroPathfindingController implements IHeroPathfindingController {
|
||||
}
|
||||
}
|
||||
|
||||
class QueuedHeroPathfindingController implements IPathfindingController {
|
||||
private current: IPathfindingController | null = null;
|
||||
private currentPath: IPathfindingStep[] = [];
|
||||
private cancelled: boolean = false;
|
||||
private completed: boolean = false;
|
||||
private readonly completion: Promise<void>;
|
||||
|
||||
constructor(start: () => Promise<IPathfindingController | null>) {
|
||||
this.completion = Promise.resolve()
|
||||
.then(start)
|
||||
.then(async result => {
|
||||
if (!result) {
|
||||
this.completed = true;
|
||||
return;
|
||||
}
|
||||
this.current = result;
|
||||
this.currentPath = [...result.path];
|
||||
if (this.cancelled) {
|
||||
await result.controller.stop();
|
||||
this.completed = true;
|
||||
return;
|
||||
}
|
||||
await result.controller.onEnd;
|
||||
this.completed = true;
|
||||
});
|
||||
}
|
||||
|
||||
get controller(): Readonly<IMoverController> {
|
||||
return this;
|
||||
}
|
||||
|
||||
get path(): readonly IPathfindingStep[] {
|
||||
return this.currentPath;
|
||||
}
|
||||
|
||||
get done(): boolean {
|
||||
return this.completed;
|
||||
}
|
||||
|
||||
get onEnd(): Promise<void> {
|
||||
return this.completion;
|
||||
}
|
||||
|
||||
isCancelled(): boolean {
|
||||
return this.cancelled;
|
||||
}
|
||||
|
||||
push(...steps: Readonly<ObjectMoveStep>[]): void {
|
||||
this.current?.controller.push(...steps);
|
||||
}
|
||||
|
||||
insert(...steps: Readonly<ObjectMoveStep>[]): void {
|
||||
this.current?.controller.insert(...steps);
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
this.cancelled = true;
|
||||
if (this.current) return this.current.controller.stop();
|
||||
return this.completion;
|
||||
}
|
||||
}
|
||||
|
||||
export class HeroPathfinding implements IHeroPathfinding {
|
||||
readonly state: IHeroPathfindingState;
|
||||
readonly finder: IPathfinder;
|
||||
|
||||
private readonly system: PathfindingSystem;
|
||||
private active: IPathfindingController | null = null;
|
||||
|
||||
constructor(state: IHeroPathfindingState, topImpl: IHeroMoveTopImpl) {
|
||||
this.state = state;
|
||||
@ -149,35 +212,67 @@ export class HeroPathfinding implements IHeroPathfinding {
|
||||
}
|
||||
|
||||
moveTo(target: ITileLocator): IPathfindingController | null {
|
||||
if (this.isMoving()) return this.queueAfterInterrupt(target, false);
|
||||
this.bindCurrentLayer();
|
||||
const resolved = this.resolvePath(target);
|
||||
if (resolved.path.length === 0) return null;
|
||||
const destination = resolved.adjacent ?? target;
|
||||
const result = this.system.moveTo(destination);
|
||||
if (!result) return null;
|
||||
if (!resolved.adjacent || !resolved.target) return result;
|
||||
return {
|
||||
controller: new HeroPathfindingController(result.controller, () =>
|
||||
this.finishAdjacentTouch(resolved)
|
||||
),
|
||||
path: result.path
|
||||
};
|
||||
return this.startResolved(target, resolved, false);
|
||||
}
|
||||
|
||||
teleportTo(target: ITileLocator): IPathfindingController | null {
|
||||
if (this.isMoving()) return this.queueAfterInterrupt(target, true);
|
||||
this.bindCurrentLayer();
|
||||
const resolved = this.resolvePath(target);
|
||||
return this.startResolved(target, resolved, true);
|
||||
}
|
||||
|
||||
private isMoving(): boolean {
|
||||
return this.active !== null && !this.active.controller.done;
|
||||
}
|
||||
|
||||
private queueAfterInterrupt(
|
||||
target: ITileLocator,
|
||||
teleport: boolean
|
||||
): IPathfindingController {
|
||||
const queued = new QueuedHeroPathfindingController(async () => {
|
||||
await this.system.interrupt();
|
||||
if (queued.isCancelled()) return null;
|
||||
this.bindCurrentLayer();
|
||||
const resolved = this.resolvePath(target);
|
||||
return this.startResolved(target, resolved, teleport);
|
||||
});
|
||||
this.active = queued;
|
||||
return queued;
|
||||
}
|
||||
|
||||
private startResolved(
|
||||
target: ITileLocator,
|
||||
resolved: IResolvedPath,
|
||||
teleport: boolean
|
||||
): IPathfindingController | null {
|
||||
if (resolved.path.length === 0) return null;
|
||||
const destination = resolved.adjacent ?? target;
|
||||
const result = this.system.teleportTo(destination);
|
||||
if (!result) return null;
|
||||
if (!resolved.adjacent || !resolved.target) return result;
|
||||
return {
|
||||
controller: new HeroPathfindingController(result.controller, () =>
|
||||
this.finishAdjacentTouch(resolved)
|
||||
),
|
||||
path: result.path
|
||||
};
|
||||
const result = teleport
|
||||
? this.system.teleportTo(destination)
|
||||
: this.system.moveTo(destination);
|
||||
if (!result) {
|
||||
logger.error(65);
|
||||
return null;
|
||||
}
|
||||
const wrapped =
|
||||
resolved.adjacent && resolved.target
|
||||
? {
|
||||
controller: new HeroPathfindingController(
|
||||
result.controller,
|
||||
() => this.finishAdjacentTouch(resolved)
|
||||
),
|
||||
path: result.path
|
||||
}
|
||||
: result;
|
||||
this.active = wrapped;
|
||||
void wrapped.controller.onEnd.then(() => {
|
||||
if (this.active === wrapped) this.active = null;
|
||||
});
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
private resolvePath(target: ITileLocator): IResolvedPath {
|
||||
@ -298,7 +393,10 @@ export class HeroPathfinding implements IHeroPathfinding {
|
||||
}
|
||||
|
||||
interrupt(): Promise<void> {
|
||||
return this.system.interrupt();
|
||||
const active = this.active;
|
||||
this.active = null;
|
||||
if (!active) return this.system.interrupt();
|
||||
return active.controller.stop().then(() => this.system.interrupt());
|
||||
}
|
||||
|
||||
useMapState(maps: IMapState | null): void {
|
||||
|
||||
@ -63,7 +63,8 @@
|
||||
"61": "Cannot create game map '$3' from raw data since map area cannot be divided by width evenly: area = $1 while divider = $2.",
|
||||
"62": "Cannot add $1 from raw data for game map '$2' since Expected a number for the key of $3, but got $4.",
|
||||
"63": "Cannot create game map '$2' from raw data since the '$1' container is missing, null, or not an object.",
|
||||
"64": "Cannot create game map '$2' from raw data since the '$1' value has an invalid type or range."
|
||||
"64": "Cannot create game map '$2' from raw data since the '$1' value has an invalid type or range.",
|
||||
"65": "Cannot start pathfinding movement because the mover is already active."
|
||||
},
|
||||
"warn": {
|
||||
"1": "Resource with type of 'none' is loaded.",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user