mirror of
https://github.com/motajs/template.git
synced 2026-09-13 02:08:50 +08:00
feat(02-03): wire hero pathfinding movement
- Bind the hero predicate and event layer into the L2 finder - Execute translated paths through the hero mover and export the L3 entry
This commit is contained in:
parent
68a150882c
commit
2759df531e
@ -3,10 +3,16 @@
|
||||
## Deferred Items
|
||||
|
||||
- data-base/src/hero/mover.ts 的 2 处既有 TypeScript 诊断(TS2339:IHeroMoveTopImpl 上不存在 canPass/shouldHit)
|
||||
status: open
|
||||
status: resolved
|
||||
**What:** 用户提交 7a011b2 将 `IHeroMoveTopImpl` 重构为 `predicate(): IPassPredicate` 形态后,
|
||||
消费方 `data-base/src/hero/mover.ts:183/185` 仍直接调用 `canPass/shouldHit`,产生既有诊断。
|
||||
02-02 计划的 `check:type` 过滤式类型门(`mover\.ts`)会命中该既有文件;按计划声明
|
||||
「仓库既有诊断不在本计划范围」与本执行器的范围边界规则不修复,留给 02-03 Task 1
|
||||
(「掩码语义谓词由 02-03 Task 1 从 moverImpl 重构为 predicate() 后经 usePassPredicate 注入」)一并收口。
|
||||
02-02 计划内文件(`src/path/`、`data-common/src/common/mover.ts`)类型诊断为 0。
|
||||
02-02 计划内文件(`src/path/`、`data-common/src/common/mover.ts`)类型诊断为 0。
|
||||
|
||||
- data-state/src/core.ts 的 2 处既有 TypeScript 诊断(TS2322/TS2345:TileStore 泛型实现与 ITileStore 不兼容)
|
||||
status: open
|
||||
**What:** `CoreState` 原有 `TileStore<LegacyTileData>` 装配在 `this.tileStore` 与 `MapState` 构造处
|
||||
被仓库当前接口泛型诊断命中;本计划仅在同一构造器追加寻路接线,不改变既有 TileStore 契约,
|
||||
按范围边界规则留给后续数据层类型收口。
|
||||
|
||||
@ -184,7 +184,8 @@ export class HeroMover<T extends IHeroLocation>
|
||||
currLoc: handler.currLoc,
|
||||
nextLoc: handler.nextLoc,
|
||||
direction: handler.direction,
|
||||
floorId: handler.floorId
|
||||
floorId: handler.floorId,
|
||||
state: handler.state
|
||||
};
|
||||
const predicate = this.topImpl.predicate();
|
||||
const canPass = predicate.canPass(passHandler);
|
||||
|
||||
@ -77,6 +77,7 @@ import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { logger } from '@motajs/common';
|
||||
import { DefaultHeroMoveTopImpl } from './hero';
|
||||
import { HeroPathfinding } from './path';
|
||||
|
||||
export class CoreState implements ICoreState {
|
||||
// Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据
|
||||
@ -101,6 +102,8 @@ export class CoreState implements ICoreState {
|
||||
// Layer 3 用户层,也就是最顶层的内容,一般仅用于初始化以及仅供渲染端调用的顶层模块
|
||||
readonly loadProgress: ILoadProgressTotal;
|
||||
readonly dataLoader: IMotaDataLoader;
|
||||
/** 勇士寻路入口 */
|
||||
readonly pathfinding: HeroPathfinding;
|
||||
|
||||
/** 可存档对象映射 */
|
||||
private readonly saveables: Map<string, ISaveableContent<any>> = new Map();
|
||||
@ -235,6 +238,7 @@ export class CoreState implements ICoreState {
|
||||
// 勇士顶层初始化
|
||||
const heroMoveTopImpl = new DefaultHeroMoveTopImpl(this);
|
||||
this.hero.location.mover.useTopImplementation(heroMoveTopImpl);
|
||||
this.pathfinding = new HeroPathfinding(this, heroMoveTopImpl);
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export * from './enemy';
|
||||
export * from './hero';
|
||||
export * from './path';
|
||||
|
||||
export * from './core';
|
||||
export * from './ins';
|
||||
|
||||
192
packages-user/data-state/src/path/heroPathfinding.test.ts
Normal file
192
packages-user/data-state/src/path/heroPathfinding.test.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { ITileStore } from '@user/data-common';
|
||||
|
||||
vi.hoisted(() => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
vi.stubGlobal('location', { origin: 'http://localhost' });
|
||||
Map.prototype.getOrInsertComputed ??= function <K, V>(
|
||||
this: Map<K, V>,
|
||||
key: K,
|
||||
callback: (key: K) => V
|
||||
): V {
|
||||
const existing = this.get(key);
|
||||
if (existing !== undefined) return existing;
|
||||
const value = callback(key);
|
||||
this.set(key, value);
|
||||
return value;
|
||||
};
|
||||
});
|
||||
|
||||
interface TestModules {
|
||||
EventExecutor: typeof import('@user/data-system').EventExecutor;
|
||||
DefaultHeroMoveTopImpl: typeof import('../hero').DefaultHeroMoveTopImpl;
|
||||
HeroMover: typeof import('@user/data-base').HeroMover;
|
||||
HeroPathfinding: typeof import('./heroPathfinding').HeroPathfinding;
|
||||
MapState: typeof import('@user/data-base').MapState;
|
||||
TileStore: typeof import('@user/data-common').TileStore;
|
||||
Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler;
|
||||
FaceManager: typeof import('@user/data-common').FaceManager;
|
||||
RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder;
|
||||
}
|
||||
|
||||
interface EventCall {
|
||||
readonly id: string;
|
||||
readonly trigger: number;
|
||||
readonly hero: Readonly<{ x: number; y: number }>;
|
||||
}
|
||||
|
||||
interface HeroFixture {
|
||||
x: number;
|
||||
y: number;
|
||||
floorId: string;
|
||||
state: unknown;
|
||||
mover?: unknown;
|
||||
setPos(x: number, y: number): void;
|
||||
getCurrentFaceDirection(): number;
|
||||
}
|
||||
|
||||
let modules: TestModules;
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
vi.stubGlobal('location', { origin: 'http://localhost' });
|
||||
const base = await import('@user/data-base');
|
||||
const common = await import('@user/data-common');
|
||||
const state = await import('../hero');
|
||||
const path = await import('./heroPathfinding');
|
||||
const event = await import('@user/data-system');
|
||||
modules = {
|
||||
EventExecutor: event.EventExecutor,
|
||||
DefaultHeroMoveTopImpl: state.DefaultHeroMoveTopImpl,
|
||||
HeroMover: base.HeroMover,
|
||||
HeroPathfinding: path.HeroPathfinding,
|
||||
MapState: base.MapState,
|
||||
TileStore: common.TileStore,
|
||||
Dir8FaceHandler: common.Dir8FaceHandler,
|
||||
FaceManager: common.FaceManager,
|
||||
RoleFaceBinder: common.RoleFaceBinder
|
||||
};
|
||||
});
|
||||
|
||||
function createFixture() {
|
||||
const tileStore: ITileStore = new modules.TileStore() as never;
|
||||
tileStore.addTile({
|
||||
num: 1,
|
||||
id: 'floor',
|
||||
events: {},
|
||||
type: 0,
|
||||
pass: { onlyEvents: false, outPass: 15, inPass: 15 },
|
||||
eventPass: true
|
||||
});
|
||||
const faceManager = new modules.FaceManager();
|
||||
const faceHandler = new modules.Dir8FaceHandler();
|
||||
faceManager.register(1, faceHandler);
|
||||
const commonState = {
|
||||
tileStore,
|
||||
itemStore: {},
|
||||
mapStore: {},
|
||||
eventStore: {},
|
||||
roleFace: new modules.RoleFaceBinder(),
|
||||
faceManager,
|
||||
saveSystem: {}
|
||||
} as never;
|
||||
const maps = new modules.MapState(tileStore, commonState);
|
||||
const eventMap = maps.fromRaw({
|
||||
floorId: 'F1',
|
||||
width: 3,
|
||||
map: { 0: [1, 1, 1] },
|
||||
layerAlias: { 0: 'event' },
|
||||
events: { 0: { 1: { 30: 'middle-enter' } } }
|
||||
})!;
|
||||
const events = new Map<string, object>();
|
||||
const store = {
|
||||
addEvent() {},
|
||||
getEvent<
|
||||
_P extends Record<string, unknown>,
|
||||
_E extends Record<string, unknown>,
|
||||
_R = void
|
||||
>(id: string) {
|
||||
return events.get(id) as never;
|
||||
}
|
||||
};
|
||||
const executor = new modules.EventExecutor({} as never, () => store);
|
||||
const state = {
|
||||
maps,
|
||||
eventSystem: { executor }
|
||||
};
|
||||
const hero: HeroFixture = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
floorId: 'F1',
|
||||
state,
|
||||
setPos(x: number, y: number) {
|
||||
hero.x = x;
|
||||
hero.y = y;
|
||||
},
|
||||
getCurrentFaceDirection() {
|
||||
return 4;
|
||||
}
|
||||
};
|
||||
const mover = new modules.HeroMover(hero as never, faceHandler);
|
||||
hero.mover = mover;
|
||||
const heroState = { location: hero };
|
||||
const fixtureState = { ...state, hero: heroState } as never;
|
||||
const topImpl = new modules.DefaultHeroMoveTopImpl(fixtureState);
|
||||
mover.useTopImplementation(topImpl);
|
||||
const pathfinding = new modules.HeroPathfinding(fixtureState, topImpl);
|
||||
return {
|
||||
eventMap,
|
||||
events,
|
||||
executor,
|
||||
hero: heroState.location,
|
||||
pathfinding,
|
||||
state: fixtureState
|
||||
};
|
||||
}
|
||||
|
||||
function addEvent(
|
||||
events: Map<string, object>,
|
||||
id: string,
|
||||
trigger: number,
|
||||
calls: EventCall[]
|
||||
): 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 });
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('hero pathfinding integration', () => {
|
||||
// 验证勇士按最小路径逐步移动并按事件链顺序执行途经事件
|
||||
it('moves the hero to the target and triggers the traversed event', async () => {
|
||||
const calls: EventCall[] = [];
|
||||
const fixture = createFixture();
|
||||
addEvent(fixture.events, 'middle-enter', 2, calls);
|
||||
|
||||
const path = fixture.pathfinding.getPath({ x: 2, y: 0 });
|
||||
expect(path.map(step => step.to)).toEqual([
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 2, y: 0 }
|
||||
]);
|
||||
const result = fixture.pathfinding.moveTo({ x: 2, y: 0 });
|
||||
expect(result).not.toBeNull();
|
||||
await result!.controller.onEnd;
|
||||
|
||||
expect({ x: fixture.hero.x, y: fixture.hero.y }).toEqual({
|
||||
x: 2,
|
||||
y: 0
|
||||
});
|
||||
expect(calls).toEqual([
|
||||
{ id: 'middle-enter', trigger: 2, hero: { x: 1, y: 0 } }
|
||||
]);
|
||||
});
|
||||
});
|
||||
92
packages-user/data-state/src/path/heroPathfinding.ts
Normal file
92
packages-user/data-state/src/path/heroPathfinding.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { ITileLocator } from '@motajs/common';
|
||||
import {
|
||||
IHeroMoveTopImpl,
|
||||
IHeroState,
|
||||
IMapLayer,
|
||||
IMapState,
|
||||
IPassPredicate
|
||||
} from '@user/data-base';
|
||||
import { IHeroAttr, IObjectMovable, IObjectMover } from '@user/data-common';
|
||||
import {
|
||||
IPathfinder,
|
||||
IPathfindingController,
|
||||
IPathfindingStep,
|
||||
IPathfindingSystem,
|
||||
PathCostFunction,
|
||||
PathFallbackPolicy,
|
||||
PathfindingSystem
|
||||
} from '@user/data-system';
|
||||
import { IStateSystem } from '@user/data-system';
|
||||
|
||||
interface IHeroPathfindingState extends IStateSystem {
|
||||
readonly hero: IHeroState<IHeroAttr>;
|
||||
}
|
||||
|
||||
interface IHeroPathfinding extends IPathfindingSystem {}
|
||||
|
||||
export class HeroPathfinding implements IHeroPathfinding {
|
||||
readonly state: IHeroPathfindingState;
|
||||
readonly finder: IPathfinder;
|
||||
|
||||
private readonly system: PathfindingSystem;
|
||||
|
||||
constructor(state: IHeroPathfindingState, topImpl: IHeroMoveTopImpl) {
|
||||
this.state = state;
|
||||
this.system = new PathfindingSystem(state);
|
||||
this.finder = this.system.finder;
|
||||
this.system.useMover(state.hero.location.mover);
|
||||
this.finder.useMapState(state.maps);
|
||||
this.finder.usePassPredicate(topImpl.predicate());
|
||||
this.bindCurrentLayer();
|
||||
}
|
||||
|
||||
private bindCurrentLayer(): void {
|
||||
const floorId = this.state.hero.location.floorId;
|
||||
const map = floorId ? this.state.maps.getMap(floorId) : null;
|
||||
const layer: IMapLayer | null = map?.eventLayer ?? null;
|
||||
this.finder.useMapLayer(layer);
|
||||
}
|
||||
|
||||
useMover(mover: IObjectMover<IObjectMovable> | null): void {
|
||||
this.system.useMover(mover);
|
||||
}
|
||||
|
||||
useFallbackPolicy(policy: PathFallbackPolicy | null): void {
|
||||
this.system.useFallbackPolicy(policy);
|
||||
}
|
||||
|
||||
getPath(target: ITileLocator): IPathfindingStep[] {
|
||||
this.bindCurrentLayer();
|
||||
return this.system.getPath(target);
|
||||
}
|
||||
|
||||
moveTo(target: ITileLocator): IPathfindingController | null {
|
||||
this.bindCurrentLayer();
|
||||
return this.system.moveTo(target);
|
||||
}
|
||||
|
||||
teleportTo(target: ITileLocator): IPathfindingController | null {
|
||||
this.bindCurrentLayer();
|
||||
return this.system.teleportTo(target);
|
||||
}
|
||||
|
||||
interrupt(): Promise<void> {
|
||||
return this.system.interrupt();
|
||||
}
|
||||
|
||||
useMapState(maps: IMapState | null): void {
|
||||
this.finder.useMapState(maps);
|
||||
}
|
||||
|
||||
useMapLayer(layer: IMapLayer | null): void {
|
||||
this.finder.useMapLayer(layer);
|
||||
}
|
||||
|
||||
useCostFunction(cost: PathCostFunction | null): void {
|
||||
this.finder.useCostFunction(cost);
|
||||
}
|
||||
|
||||
usePassPredicate(predicate: IPassPredicate | null): void {
|
||||
this.finder.usePassPredicate(predicate);
|
||||
}
|
||||
}
|
||||
1
packages-user/data-state/src/path/index.ts
Normal file
1
packages-user/data-state/src/path/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './heroPathfinding';
|
||||
Loading…
Reference in New Issue
Block a user