mirror of
https://github.com/motajs/template.git
synced 2026-09-18 21:20:20 +08:00
feat(02-02): build pathfinding directed graph with injected pass predicate and authorized types fix
This commit is contained in:
parent
e1f61013ac
commit
0cd6ab6a2d
387
packages-user/data-system/src/path/graph.test.ts
Normal file
387
packages-user/data-system/src/path/graph.test.ts
Normal file
@ -0,0 +1,387 @@
|
||||
// 测试寻路有向图构建:邻域方向组、单向门、终端节点分类与边界守卫
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { FaceDirection } from '@user/data-common';
|
||||
import {
|
||||
type IDataCommon,
|
||||
type IFaceHandler,
|
||||
type IGameMap,
|
||||
type IPassCheckHandler,
|
||||
type IPassPredicate,
|
||||
type ITileStore
|
||||
} from '@user/data-common';
|
||||
import { InternalDirectionGroup } from '@motajs/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 {
|
||||
PathfindingGraphBuilder: typeof import('./graph').PathfindingGraphBuilder;
|
||||
MapState: typeof import('@user/data-base').MapState;
|
||||
TileStore: typeof import('@user/data-common').TileStore;
|
||||
FaceManager: typeof import('@user/data-common').FaceManager;
|
||||
Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler;
|
||||
RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder;
|
||||
DirectionMapper: typeof import('@motajs/common').DirectionMapper;
|
||||
logger: typeof import('@motajs/common').logger;
|
||||
}
|
||||
|
||||
let modules: TestModules;
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
vi.stubGlobal('location', { origin: 'http://localhost' });
|
||||
const graphModule = await import('./graph');
|
||||
const baseModule = await import('@user/data-base');
|
||||
const commonModule = await import('@user/data-common');
|
||||
const motaModule = await import('@motajs/common');
|
||||
modules = {
|
||||
PathfindingGraphBuilder: graphModule.PathfindingGraphBuilder,
|
||||
MapState: baseModule.MapState,
|
||||
TileStore: commonModule.TileStore,
|
||||
FaceManager: commonModule.FaceManager,
|
||||
Dir8FaceHandler: commonModule.Dir8FaceHandler,
|
||||
RoleFaceBinder: commonModule.RoleFaceBinder,
|
||||
DirectionMapper: motaModule.DirectionMapper,
|
||||
logger: motaModule.logger
|
||||
};
|
||||
});
|
||||
|
||||
/** 测试图块定义,键为图块数字 */
|
||||
interface TestTileDefinition {
|
||||
/** 图块数字 */
|
||||
num: number;
|
||||
/** 图块字符串 id */
|
||||
id: string;
|
||||
/** 可以离开的方向 */
|
||||
outPass: number;
|
||||
/** 可以进入的方向 */
|
||||
inPass: number;
|
||||
/** 事件可通行性,`false` 表示撞击触发 */
|
||||
eventPass: boolean;
|
||||
}
|
||||
|
||||
/** 开阔图块:四向可进可出 */
|
||||
const OPEN_TILE: TestTileDefinition = {
|
||||
num: 1,
|
||||
id: 'open',
|
||||
outPass: 15,
|
||||
inPass: 15,
|
||||
eventPass: true
|
||||
};
|
||||
|
||||
/** 单向门图块:仅可向右离开,不可从任何方向进入 */
|
||||
const ONEWAY_TILE: TestTileDefinition = {
|
||||
num: 3,
|
||||
id: 'oneway',
|
||||
outPass: 0b0010,
|
||||
inPass: 0
|
||||
};
|
||||
|
||||
/** 汇入图块:仅可从左方向进入,不可离开 */
|
||||
const SINK_TILE: TestTileDefinition = {
|
||||
num: 4,
|
||||
id: 'sink',
|
||||
outPass: 0,
|
||||
inPass: 0b1000,
|
||||
eventPass: true
|
||||
};
|
||||
|
||||
/** 撞击图块:四向可进可出但事件不通行,构成终端节点 */
|
||||
const HIT_TILE: TestTileDefinition = {
|
||||
num: 5,
|
||||
id: 'hit',
|
||||
outPass: 15,
|
||||
inPass: 15,
|
||||
eventPass: false
|
||||
};
|
||||
|
||||
const ALL_TILES: TestTileDefinition[] = [
|
||||
OPEN_TILE,
|
||||
ONEWAY_TILE,
|
||||
SINK_TILE,
|
||||
HIT_TILE
|
||||
];
|
||||
|
||||
/**
|
||||
* 复刻 DefaultHeroMoveTopImpl 掩码语义的测试谓词:
|
||||
* 事件层恒参与判定,其余层仅当 onlyEvents 为真时参与
|
||||
*/
|
||||
class FixturePredicate implements IPassPredicate {
|
||||
/** 绑定的楼层地图对象 */
|
||||
private readonly map: IGameMap;
|
||||
/** 朝向管理对象,用于求相反方向 */
|
||||
private readonly face: IFaceHandler<FaceDirection>;
|
||||
|
||||
constructor(map: IGameMap, face: IFaceHandler<FaceDirection>) {
|
||||
this.map = map;
|
||||
this.face = face;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将朝向转换为对应的通行性位掩码
|
||||
* @param dir 朝向
|
||||
*/
|
||||
private passBit(dir: FaceDirection): number {
|
||||
switch (dir) {
|
||||
case FaceDirection.Up:
|
||||
return 0b0001;
|
||||
case FaceDirection.Right:
|
||||
return 0b0010;
|
||||
case FaceDirection.Down:
|
||||
return 0b0100;
|
||||
case FaceDirection.Left:
|
||||
return 0b1000;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
canPass(handler: IPassCheckHandler): boolean {
|
||||
const event = this.map.eventLayer;
|
||||
if (!event) return false;
|
||||
const { currLoc, nextLoc, direction } = handler;
|
||||
|
||||
// 四角朝向直接判定为可通行,与 moverImpl 语义一致
|
||||
if (
|
||||
direction === FaceDirection.LeftDown ||
|
||||
direction === FaceDirection.LeftUp ||
|
||||
direction === FaceDirection.RightDown ||
|
||||
direction === FaceDirection.RightUp
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const opposite = this.face.opposite(direction);
|
||||
const leaveMask = this.passBit(direction);
|
||||
const enterMask = this.passBit(opposite);
|
||||
|
||||
let canLeave = true;
|
||||
let canEnter = true;
|
||||
|
||||
// 判断事件层
|
||||
const curr = event.getLocationData(currLoc.x, currLoc.y);
|
||||
const next = event.getLocationData(nextLoc.x, nextLoc.y);
|
||||
const currRaw = curr?.static.raw();
|
||||
const nextRaw = next?.static.raw();
|
||||
if (currRaw) {
|
||||
canLeave = !!(leaveMask & currRaw.pass.outPass);
|
||||
}
|
||||
if (nextRaw) {
|
||||
canEnter = !!(enterMask & nextRaw.pass.inPass);
|
||||
}
|
||||
if (!canLeave || !canEnter) return false;
|
||||
|
||||
// 判断其他层,仅 onlyEvents 图块参与判定
|
||||
for (const layer of this.map.layerList) {
|
||||
if (layer === event) continue;
|
||||
const other = layer.getLocationData(currLoc.x, currLoc.y);
|
||||
const otherNext = layer.getLocationData(nextLoc.x, nextLoc.y);
|
||||
const otherRaw = other?.static.raw();
|
||||
const otherNextRaw = otherNext?.static.raw();
|
||||
if (otherRaw?.pass.onlyEvents) {
|
||||
canLeave = !!(leaveMask & otherRaw.pass.outPass);
|
||||
}
|
||||
if (otherNextRaw?.pass.onlyEvents) {
|
||||
canEnter = !!(enterMask & otherNextRaw.pass.inPass);
|
||||
}
|
||||
if (!canLeave || !canEnter) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
shouldHit(handler: IPassCheckHandler): boolean {
|
||||
const event = this.map.eventLayer;
|
||||
if (!event) return false;
|
||||
const { nextLoc } = handler;
|
||||
const next = event.getLocationData(nextLoc.x, nextLoc.y);
|
||||
const nextRaw = next?.static.raw();
|
||||
if (!nextRaw) return false;
|
||||
return !nextRaw.eventPass;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试地图与有向图构建器
|
||||
* @param rows 每行图块数字,行长为宽度乘高度
|
||||
* @param width 地图宽度
|
||||
* @param predicate 注入的通行性谓词,传入 `null` 表示不注入
|
||||
*/
|
||||
function createFixture(
|
||||
rows: number[],
|
||||
width: number,
|
||||
predicate: IPassPredicate | null
|
||||
) {
|
||||
const tileStore: ITileStore = new modules.TileStore() as never;
|
||||
for (const tile of ALL_TILES) {
|
||||
tileStore.addTile({
|
||||
num: tile.num,
|
||||
id: tile.id,
|
||||
events: {},
|
||||
type: 0,
|
||||
pass: {
|
||||
onlyEvents: false,
|
||||
outPass: tile.outPass,
|
||||
inPass: tile.inPass
|
||||
},
|
||||
eventPass: tile.eventPass
|
||||
});
|
||||
}
|
||||
const faceManager = new modules.FaceManager();
|
||||
faceManager.register(1, new modules.Dir8FaceHandler());
|
||||
const commonState: IDataCommon = {
|
||||
tileStore,
|
||||
itemStore: {},
|
||||
mapStore: {},
|
||||
eventStore: {},
|
||||
roleFace: new modules.RoleFaceBinder(),
|
||||
faceManager,
|
||||
saveSystem: {}
|
||||
} as never;
|
||||
const maps = new modules.MapState(tileStore, commonState);
|
||||
const map = maps.fromRaw({
|
||||
floorId: 'F1',
|
||||
width,
|
||||
map: { 0: rows },
|
||||
layerAlias: { 0: 'event' },
|
||||
events: { 0: {} }
|
||||
});
|
||||
const layer = map!.getLayerByAlias('event')!;
|
||||
const builder = new modules.PathfindingGraphBuilder();
|
||||
builder.useMapState(maps);
|
||||
builder.useMapLayer(layer);
|
||||
if (predicate) {
|
||||
builder.usePassPredicate(predicate);
|
||||
}
|
||||
return { maps, map: map!, layer, builder };
|
||||
}
|
||||
|
||||
describe('pathfinding graph building', () => {
|
||||
// 验证未注入谓词时图节点齐备但所有边均不可行
|
||||
it('builds nodes without edges when no predicate is injected', () => {
|
||||
const { map, builder } = createFixture(
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
3,
|
||||
null
|
||||
);
|
||||
builder.useMapLayer(map.getLayerByAlias('event'));
|
||||
const graph = builder.build();
|
||||
|
||||
expect(graph.width).toBe(3);
|
||||
expect(graph.height).toBe(3);
|
||||
expect(graph.nodes.size).toBe(9);
|
||||
for (const node of graph.nodes.values()) {
|
||||
expect(node.edges).toHaveLength(0);
|
||||
expect(node.terminal).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 验证注入谓词后中心节点邻域方向数与 DirectionMapper 四正交组一致为 4
|
||||
it('resolves four orthogonal neighbor edges for the center node', () => {
|
||||
const { map, builder } = createFixture(
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
3,
|
||||
null
|
||||
);
|
||||
const mapper = new modules.DirectionMapper();
|
||||
const expected = [...mapper.map(InternalDirectionGroup.Dir4)];
|
||||
const predicate = new FixturePredicate(
|
||||
map,
|
||||
new modules.Dir8FaceHandler()
|
||||
);
|
||||
builder.usePassPredicate(predicate);
|
||||
const graph = builder.build();
|
||||
|
||||
const center = graph.nodes.get(1 * 3 + 1)!;
|
||||
expect(center.edges).toHaveLength(expected.length);
|
||||
expect(center.edges).toHaveLength(4);
|
||||
const dirs = new Set(center.edges.map(edge => edge.dir));
|
||||
expect(dirs).toEqual(
|
||||
new Set([
|
||||
FaceDirection.Up,
|
||||
FaceDirection.Down,
|
||||
FaceDirection.Left,
|
||||
FaceDirection.Right
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
// 验证 useDirGroup 注入八方向组后中心节点拥有 8 条邻域边
|
||||
it('expands neighbor edges to eight when Dir8 group is injected', () => {
|
||||
const { map, builder } = createFixture(
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
3,
|
||||
null
|
||||
);
|
||||
const predicate = new FixturePredicate(
|
||||
map,
|
||||
new modules.Dir8FaceHandler()
|
||||
);
|
||||
builder.usePassPredicate(predicate);
|
||||
builder.useDirGroup(InternalDirectionGroup.Dir8);
|
||||
const graph = builder.build();
|
||||
|
||||
const center = graph.nodes.get(1 * 3 + 1)!;
|
||||
expect(center.edges).toHaveLength(8);
|
||||
});
|
||||
|
||||
// 验证单向门图边方向性:A→B 可行而 B→A 不可行
|
||||
it('keeps one-way gates directional from A to B only', () => {
|
||||
const { map, builder } = createFixture([3, 4], 2, null);
|
||||
const predicate = new FixturePredicate(
|
||||
map,
|
||||
new modules.Dir8FaceHandler()
|
||||
);
|
||||
builder.usePassPredicate(predicate);
|
||||
const graph = builder.build();
|
||||
|
||||
const source = graph.nodes.get(0)!;
|
||||
const sink = graph.nodes.get(1)!;
|
||||
expect(source.edges).toEqual([{ dir: FaceDirection.Right, to: 1 }]);
|
||||
expect(sink.edges).toHaveLength(0);
|
||||
expect(sink.terminal).toBe(false);
|
||||
});
|
||||
|
||||
// 验证 canPass 为真但 shouldHit 为真的图块被分类为仅可作路径终点的终端节点
|
||||
it('marks can-pass but should-hit blocks as terminal nodes', () => {
|
||||
const { map, builder } = createFixture([1, 5, 1], 3, null);
|
||||
const predicate = new FixturePredicate(
|
||||
map,
|
||||
new modules.Dir8FaceHandler()
|
||||
);
|
||||
builder.usePassPredicate(predicate);
|
||||
const graph = builder.build();
|
||||
|
||||
const hit = graph.nodes.get(1)!;
|
||||
expect(hit.terminal).toBe(true);
|
||||
// 终端节点仍保留入边,可作为路径终点
|
||||
expect(graph.nodes.get(0)!.edges).toEqual([
|
||||
{ dir: FaceDirection.Right, to: 1 }
|
||||
]);
|
||||
expect(graph.nodes.get(0)!.terminal).toBe(false);
|
||||
expect(graph.nodes.get(2)!.terminal).toBe(false);
|
||||
});
|
||||
|
||||
// 验证图层未绑定时构建入口告警新码 173 并返回空图而非异常
|
||||
it('warns the registered code and returns an empty graph without layer', () => {
|
||||
const builder = new modules.PathfindingGraphBuilder();
|
||||
builder.useMapLayer(null);
|
||||
|
||||
const result = modules.logger.catch(() => builder.build());
|
||||
|
||||
expect(result.ret.nodes.size).toBe(0);
|
||||
expect(result.info.map(info => info.code)).toContain(173);
|
||||
});
|
||||
});
|
||||
223
packages-user/data-system/src/path/graph.ts
Normal file
223
packages-user/data-system/src/path/graph.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import {
|
||||
DirectionMapper,
|
||||
IDirectionDescriptor,
|
||||
IDirectionMapper,
|
||||
InternalDirectionGroup,
|
||||
logger
|
||||
} from '@motajs/common';
|
||||
import { FaceDirection, IDataCommon } from '@user/data-common';
|
||||
import {
|
||||
ILayerLocation,
|
||||
IMapLayer,
|
||||
IMapState,
|
||||
IPassCheckHandler,
|
||||
IPassPredicate
|
||||
} from '@user/data-base';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
//#region 图结构
|
||||
|
||||
export interface IPathGraphEdge {
|
||||
/** 本条边对应的移动方向 */
|
||||
readonly dir: FaceDirection;
|
||||
/** 边指向的节点索引,值为 y * width + x */
|
||||
readonly to: number;
|
||||
}
|
||||
|
||||
export interface IPathGraphNode {
|
||||
/** 节点索引,值为 y * width + x */
|
||||
readonly index: number;
|
||||
/** 节点横坐标 */
|
||||
readonly x: number;
|
||||
/** 节点纵坐标 */
|
||||
readonly y: number;
|
||||
/** 节点对应的位置信息,用于损失计算 */
|
||||
readonly block: ILayerLocation;
|
||||
/** 该节点是否仅可作为路径终点,不可作为中间节点 */
|
||||
readonly terminal: boolean;
|
||||
/** 该节点的全部出边 */
|
||||
readonly edges: readonly IPathGraphEdge[];
|
||||
}
|
||||
|
||||
export interface IPathGraph {
|
||||
/** 图宽度 */
|
||||
readonly width: number;
|
||||
/** 图高度 */
|
||||
readonly height: number;
|
||||
/** 图内全部节点,键为节点索引,值为 y * width + x */
|
||||
readonly nodes: ReadonlyMap<number, IPathGraphNode>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 方向解析
|
||||
|
||||
/**
|
||||
* 将方向描述器的坐标增量解析为对应的朝向
|
||||
* @param x 横坐标增量
|
||||
* @param y 纵坐标增量
|
||||
*/
|
||||
function directionOf(x: number, y: number): FaceDirection {
|
||||
if (x === 0 && y === -1) return FaceDirection.Up;
|
||||
if (x === 0 && y === 1) return FaceDirection.Down;
|
||||
if (x === -1 && y === 0) return FaceDirection.Left;
|
||||
if (x === 1 && y === 0) return FaceDirection.Right;
|
||||
if (x === -1 && y === -1) return FaceDirection.LeftUp;
|
||||
if (x === 1 && y === -1) return FaceDirection.RightUp;
|
||||
if (x === -1 && y === 1) return FaceDirection.LeftDown;
|
||||
if (x === 1 && y === 1) return FaceDirection.RightDown;
|
||||
return FaceDirection.Unknown;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 有向图构建
|
||||
|
||||
/**
|
||||
* 寻路有向图构建器,将地图图层转换为以通行性谓词判定边的有向图。
|
||||
* 邻域方向由注入的方向组决定,默认仅包含四正交方向;
|
||||
* 谓词未注入时所有边均不可通行
|
||||
*/
|
||||
export class PathfindingGraphBuilder {
|
||||
/** 绑定的地图状态对象,用于解析图层所属楼层 id */
|
||||
private maps: IMapState | null = null;
|
||||
/** 绑定的地图图层,图节点来源 */
|
||||
private layer: IMapLayer | null = null;
|
||||
/** 注入的通行性谓词,用于判定边的可行性与终端节点 */
|
||||
private predicate: IPassPredicate | null = null;
|
||||
/** 邻域方向组别,默认四正交方向 */
|
||||
private group: number = InternalDirectionGroup.Dir4;
|
||||
|
||||
/** 方向组解析对象 */
|
||||
private readonly mapper: IDirectionMapper = new DirectionMapper();
|
||||
|
||||
/**
|
||||
* 绑定地图状态对象,用于解析楼层 id
|
||||
* @param maps 地图状态对象,传入 `null` 解绑
|
||||
*/
|
||||
useMapState(maps: IMapState | null): void {
|
||||
this.maps = maps;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定构建有向图所用的地图图层
|
||||
* @param layer 地图图层对象,传入 `null` 解绑
|
||||
*/
|
||||
useMapLayer(layer: IMapLayer | null): void {
|
||||
this.layer = layer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入判定边可行性的通行性谓词
|
||||
* @param predicate 通行性谓词,传入 `null` 解绑
|
||||
*/
|
||||
usePassPredicate(predicate: IPassPredicate | null): void {
|
||||
this.predicate = predicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置邻域使用的方向组
|
||||
* @param group 朝向组
|
||||
*/
|
||||
useDirGroup(group: number): void {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建有向图。图层未绑定时告警并返回空图,
|
||||
* 不包含任何节点与边
|
||||
* @returns 构建的有向图
|
||||
*/
|
||||
build(): IPathGraph {
|
||||
const layer = this.layer;
|
||||
if (isNil(layer)) {
|
||||
logger.warn(173);
|
||||
return { width: 0, height: 0, nodes: new Map() };
|
||||
}
|
||||
|
||||
const width = layer.width;
|
||||
const height = layer.height;
|
||||
const floorId = this.resolveFloorId();
|
||||
const state: IDataCommon = layer.state;
|
||||
const blocks: (ILayerLocation | null)[] = new Array(
|
||||
width * height
|
||||
).fill(null);
|
||||
|
||||
// 收集图内全部图块作为图节点
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (!layer.inMap(x, y)) continue;
|
||||
const loc = layer.getLocationData(x, y);
|
||||
if (!loc) continue;
|
||||
blocks[y * width + x] = loc;
|
||||
}
|
||||
}
|
||||
|
||||
const terminals: Set<number> = new Set();
|
||||
const adjacency: Map<number, IPathGraphEdge[]> = new Map();
|
||||
const dirs: IDirectionDescriptor[] = [...this.mapper.map(this.group)];
|
||||
|
||||
// 逐节点判定邻域边可行性
|
||||
for (let index = 0; index < blocks.length; index++) {
|
||||
const block = blocks[index];
|
||||
if (!block) continue;
|
||||
const x = index % width;
|
||||
const y = Math.floor(index / width);
|
||||
const edges: IPathGraphEdge[] = [];
|
||||
for (const desc of dirs) {
|
||||
const dir = directionOf(desc.x, desc.y);
|
||||
if (dir === FaceDirection.Unknown) continue;
|
||||
const nx = x + desc.x;
|
||||
const ny = y + desc.y;
|
||||
if (!layer.inMap(nx, ny)) continue;
|
||||
const next = blocks[ny * width + nx];
|
||||
if (!next) continue;
|
||||
const handler: IPassCheckHandler = {
|
||||
currLoc: block.locator,
|
||||
nextLoc: next.locator,
|
||||
direction: dir,
|
||||
floorId,
|
||||
state
|
||||
};
|
||||
const predicate = this.predicate;
|
||||
if (isNil(predicate) || !predicate.canPass(handler)) continue;
|
||||
if (predicate.shouldHit(handler)) {
|
||||
terminals.add(ny * width + nx);
|
||||
}
|
||||
edges.push({ dir, to: ny * width + nx });
|
||||
}
|
||||
adjacency.set(index, edges);
|
||||
}
|
||||
|
||||
const nodes: Map<number, IPathGraphNode> = new Map();
|
||||
for (let index = 0; index < blocks.length; index++) {
|
||||
const block = blocks[index];
|
||||
if (!block) continue;
|
||||
nodes.set(index, {
|
||||
index,
|
||||
x: index % width,
|
||||
y: Math.floor(index / width),
|
||||
block,
|
||||
terminal: terminals.has(index),
|
||||
edges: adjacency.get(index) ?? []
|
||||
});
|
||||
}
|
||||
return { width, height, nodes };
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析绑定图层所属的楼层 id
|
||||
* @returns 楼层 id,无法解析时为 `undefined`
|
||||
*/
|
||||
private resolveFloorId(): string | undefined {
|
||||
const maps = this.maps;
|
||||
const layer = this.layer;
|
||||
if (!maps || !layer) return undefined;
|
||||
for (const [floorId, map] of maps.iterateAllMaps()) {
|
||||
if (map === layer.map) return floorId;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
@ -106,14 +106,14 @@ export interface IPathfindingSystem extends IDataBaseExtended {
|
||||
* @param target 目标坐标
|
||||
* @returns 移动控制器。无法寻路、无路径或已有移动进行中时返回 `null`
|
||||
*/
|
||||
moveTo(target: ITileLocator): IPathfindingController;
|
||||
moveTo(target: ITileLocator): IPathfindingController | null;
|
||||
|
||||
/**
|
||||
* 瞬移至目标位置。瞬移前经回退策略判定,判定需要回退则自动退为逐步寻路
|
||||
* @param target 目标坐标
|
||||
* @returns 移动控制器;无法寻路、无路径或已有移动进行中时返回 `null`
|
||||
*/
|
||||
teleportTo(target: ITileLocator): IPathfindingController;
|
||||
teleportTo(target: ITileLocator): IPathfindingController | null;
|
||||
|
||||
/**
|
||||
* 打断当前自动寻路。新的方向输入或新的寻路调用可随时打断并接管
|
||||
|
||||
@ -237,6 +237,7 @@
|
||||
"169": "The current floor is the last or the first floor in the map list, so related floor change method will not work.",
|
||||
"170": "Game event id '$1' has already been used, old event will be overridden.",
|
||||
"171": "Event id '$1' not found in event store, event will be skipped.",
|
||||
"172": "Event returned non-boolean value '$1' during reduction. JavaScript short-circuit semantics will be used."
|
||||
"172": "Event returned non-boolean value '$1' during reduction. JavaScript short-circuit semantics will be used.",
|
||||
"173": "Pathfinding input is invalid or a required binding (map state, map layer) is missing. An empty result will be returned."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user