From 26f7ad6b2ee48e2aadf75f6d256b75da3c1b4ca0 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Thu, 10 Sep 2026 11:21:53 +0800 Subject: [PATCH] refactor(02-06): align pathfinding structure with user scope --- .planning/ROADMAP.md | 2 + .planning/STATE.md | 15 +- .../phases/02-pathfinding/02-06-SUMMARY.md | 68 +++ .../02-pathfinding/02-DISCUSSION-LOG.md | 13 + .../phases/02-pathfinding/02-VERIFICATION.md | 231 ++++++++++ .../data-base/src/map/eventPath.test.ts | 3 + .../data-base/src/map/mapLifecycle.test.ts | 6 +- packages-user/data-common/src/types.ts | 4 +- packages-user/data-state/src/core.ts | 9 +- packages-user/data-state/src/hero/index.ts | 1 + .../data-state/src/hero/moverImpl.ts | 119 +---- .../data-state/src/hero/predicate.ts | 114 +++++ packages-user/data-state/src/index.ts | 1 - .../src/path/heroPathfinding.test.ts | 358 --------------- .../data-state/src/path/heroPathfinding.ts | 421 ------------------ packages-user/data-state/src/path/index.ts | 1 - .../src/event/eventDispatch.test.ts | 3 + packages-user/data-system/src/path/finder.ts | 8 +- .../data-system/src/path/graph.test.ts | 1 + packages-user/data-system/src/path/graph.ts | 89 +--- .../data-system/src/path/performance.test.ts | 6 +- .../data-system/src/path/system.test.ts | 3 + packages-user/data-system/src/path/types.ts | 72 +++ 23 files changed, 556 insertions(+), 992 deletions(-) create mode 100644 .planning/phases/02-pathfinding/02-06-SUMMARY.md create mode 100644 .planning/phases/02-pathfinding/02-VERIFICATION.md create mode 100644 packages-user/data-state/src/hero/predicate.ts delete mode 100644 packages-user/data-state/src/path/heroPathfinding.test.ts delete mode 100644 packages-user/data-state/src/path/heroPathfinding.ts delete mode 100644 packages-user/data-state/src/path/index.ts diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2a425ee..f6eb129 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -81,6 +81,8 @@ Plans: 3. 寻路正确避开不可通行格(碰撞/障碍/墙体) **Plans**: 5/5 plans executed + +**Scope correction (2026-09-10):** 用户审查确认本阶段不交付未授权的 `HeroPathfinding` L3 勇士封装;当前阶段保留 L2 寻路核心、共享通行性谓词与 DataCommon 方向映射依赖,勇士或渲染侧接线不作为本阶段实现内容。 **UI hint**: yes Plans: diff --git a/.planning/STATE.md b/.planning/STATE.md index a5bc0b3..a5c82a3 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,10 +4,10 @@ milestone: v1.0 current_phase: 02 current_phase_name: 寻路系统 status: verifying -stopped_at: Completed 02-05-PLAN.md -last_updated: "2026-09-10T01:49:11.198Z" -last_activity: 2026-09-09 -last_activity_desc: Phase 02 execution started +stopped_at: Completed direct execution correction 02-06 +last_updated: "2026-09-10T11:06:00.000Z" +last_activity: 2026-09-10 +last_activity_desc: Phase 02 structural correction executed from user review state_head: 154df6a0697b0ecf1b4b96b2a151aa9e00094964 progress: total_phases: 6 @@ -30,8 +30,8 @@ See: .planning/PROJECT.md (updated 2026-09-07) Phase: 02 (寻路系统) — EXECUTING Plan: 3 of 3 -Status: Phase complete — ready for verification -Last activity: 2026-09-09 — Phase 02 execution started +Status: Phase complete — verification must be regenerated after direct scope correction +Last activity: 2026-09-10 — Phase 02 structural correction executed from user review Progress: [█████████░] 92% @@ -122,6 +122,9 @@ Recent decisions affecting current work: - [Phase 02]: Phase 02 Plan 05 sets Vitest testTimeout and hookTimeout to 30 seconds to cover full-suite beforeAll import cost. - [Phase 02]: Phase 02 Plan 05 adds deterministic pnpm test:ci while preserving interactive pnpm test. - [Phase 02]: Phase 02 Plan 05 preserves D-11 by modifying no client click adapter or Phase 1 file. +- [Phase 02]: 2026-09-10 用户结构审查修正:graph 类型及注释归回 path/types.ts;删除未授权的 HeroPathfinding L3 封装及其接线。 +- [Phase 02]: 2026-09-10 用户结构审查修正:DirectionMapper 由 IDataCommon 主对象共享注入;通行性谓词提取为 predicate.ts 的 DefaultPassPredicate。 +- [Phase 02]: 2026-09-10 直接执行摘要 02-06:旧的 L3 HeroPathfinding 相关验证记录仅代表历史实现,必须按修正后的 L2 范围重新验证。 ### Pending Todos diff --git a/.planning/phases/02-pathfinding/02-06-SUMMARY.md b/.planning/phases/02-pathfinding/02-06-SUMMARY.md new file mode 100644 index 0000000..4fe280f --- /dev/null +++ b/.planning/phases/02-pathfinding/02-06-SUMMARY.md @@ -0,0 +1,68 @@ +--- +phase: 02-pathfinding +plan: 06 +type: execute +execution_mode: direct-user-correction +subsystem: pathfinding-structure +tags: [pathfinding, structure, data-common, predicate, scope-correction] + +requires: + - phase: 02-pathfinding + provides: Existing L2 pathfinding implementation and Phase 2 execution artifacts +provides: + - Graph contracts and their API documentation restored to data-system/src/path/types.ts + - Shared DirectionMapper mounted on IDataCommon and consumed from the map state + - Generic DefaultPassPredicate extracted to predicate.ts + - Unauthorized HeroPathfinding L3 wrapper, tests, barrel, and CoreState wiring removed +affects: [phase-02 verification, phase-04-rendering] + +user_constraints: + - Graph type declarations and comments belong in types.ts + - HeroPathfinding is outside the requested Phase 2 scope and must not exist + - DirectionMapper is a shared DataCommon dependency, not a per-builder instance + - Pass predicate is generic and is named DefaultPassPredicate + +files_modified: + - .planning/STATE.md + - .planning/phases/02-pathfinding/02-DISCUSSION-LOG.md + - packages-user/data-common/src/types.ts + - packages-user/data-state/src/core.ts + - packages-user/data-state/src/hero/index.ts + - packages-user/data-state/src/hero/moverImpl.ts + - packages-user/data-state/src/hero/predicate.ts + - packages-user/data-state/src/index.ts + - packages-user/data-system/src/path/types.ts + - packages-user/data-system/src/path/graph.ts + - packages-user/data-system/src/path/finder.ts + - DataCommon test fixtures using IDataCommon + +files_deleted: + - packages-user/data-state/src/path/heroPathfinding.ts + - packages-user/data-state/src/path/heroPathfinding.test.ts + - packages-user/data-state/src/path/index.ts + +verification: + - command: pnpm test:ci + result: 10 files passed, 58 tests passed + - command: pnpm exec eslint + result: pass + - command: pnpm check:type + result: repository baseline remains non-zero; no new diagnostic in path, predicate, or IDataCommon changes + +status: complete +completed: 2026-09-10 +--- + +# Phase 02 Direct Execution Correction + +## Summary + +This direct execution followed the user's structural review and intentionally skipped a new Plan phase. The L2 pathfinding contracts remain in `types.ts`, while graph construction now consumes the shared `IDataCommon.directionMapper`. The pass predicate is no longer named or declared as hero-specific and is implemented in `hero/predicate.ts` through the `DefaultPassPredicate` interface. + +The previously added `HeroPathfinding` L3 wrapper was outside the requested scope. Its implementation, integration tests, barrel, and `CoreState` exposure were removed. Historical 02-03/02-05 artifacts describe the earlier implementation and are not evidence that the deleted wrapper remains in the codebase; the phase verification artifact must be regenerated against this corrected scope. + +## Verification Notes + +- `pnpm test:ci` passed with 10 test files and 58 tests. +- Targeted ESLint passed with no problems. +- `pnpm check:type` remains blocked by pre-existing repository diagnostics in client/legacy/TileStore areas; the changed path and predicate code introduced no reported diagnostics. diff --git a/.planning/phases/02-pathfinding/02-DISCUSSION-LOG.md b/.planning/phases/02-pathfinding/02-DISCUSSION-LOG.md index 6296b72..7ec2973 100644 --- a/.planning/phases/02-pathfinding/02-DISCUSSION-LOG.md +++ b/.planning/phases/02-pathfinding/02-DISCUSSION-LOG.md @@ -77,3 +77,16 @@ ## Deferred Ideas - 移动端点击地图触发寻路的渲染端接线 → Phase 4 渲染适配 + +--- + +## 执行修正(2026-09-10) + +本次执行直接处理用户对 Phase 2 产物的结构审查意见,跳过新的 Plan 阶段;以下四项为本次执行的约束与验收依据: + +1. `IPathGraphEdge`、`IPathGraphNode`、`IPathGraph`、`IPathfindingGraphBuilder` 的类型声明与注释必须位于 `data-system/src/path/types.ts`,不得放在 `graph.ts`。 +2. 删除未获用户要求的 `data-state/src/path/heroPathfinding.ts` 及其测试、barrel 和 `CoreState` 接线;Phase 2 不交付勇士专用 L3 寻路封装。 +3. `DirectionMapper` 必须作为 `IDataCommon` 的共享依赖由主对象挂载,`PathfindingGraphBuilder` 不得自行构造。 +4. 通行性谓词不得命名或实现为勇士专属对象;提取到独立 `predicate.ts`,接口命名为 `DefaultPassPredicate`。 + +本记录作为本次直接执行的范围来源;执行结果写入 `02-06-SUMMARY.md`,验证以修改后的 L2 代码和现有测试为准。 diff --git a/.planning/phases/02-pathfinding/02-VERIFICATION.md b/.planning/phases/02-pathfinding/02-VERIFICATION.md new file mode 100644 index 0000000..baa383e --- /dev/null +++ b/.planning/phases/02-pathfinding/02-VERIFICATION.md @@ -0,0 +1,231 @@ +--- +phase: 02-pathfinding +verified: 2026-09-10T11:14:00Z +status: passed +score: 5/5 must-haves verified +covered_files: + - .planning/REQUIREMENTS.md + - .planning/ROADMAP.md + - .planning/STATE.md + - .planning/phases/02-pathfinding/02-01-PLAN.md + - .planning/phases/02-pathfinding/02-01-SUMMARY.md + - .planning/phases/02-pathfinding/02-02-PLAN.md + - .planning/phases/02-pathfinding/02-02-SUMMARY.md + - .planning/phases/02-pathfinding/02-03-PLAN.md + - .planning/phases/02-pathfinding/02-03-SUMMARY.md + - .planning/phases/02-pathfinding/02-04-PLAN.md + - .planning/phases/02-pathfinding/02-04-SUMMARY.md + - .planning/phases/02-pathfinding/02-05-PLAN.md + - .planning/phases/02-pathfinding/02-05-SUMMARY.md + - .planning/phases/02-pathfinding/02-06-SUMMARY.md + - .planning/phases/02-pathfinding/02-CONTEXT.md + - .planning/phases/02-pathfinding/02-DISCUSSION-LOG.md + - .planning/phases/02-pathfinding/02-INTERFACE-DRAFT.md + - .planning/phases/02-pathfinding/02-RESEARCH.md + - .planning/phases/02-pathfinding/02-VALIDATION.md + - .planning/phases/02-pathfinding/deferred-items.md + - package.json + - vite.config.ts + - packages/common/src/logger.json + - packages-user/data-base/src/hero/mover.ts + - packages-user/data-base/src/map/eventPath.test.ts + - packages-user/data-base/src/map/mapLifecycle.test.ts + - packages-user/data-common/src/common/mover.test.ts + - packages-user/data-common/src/common/mover.ts + - packages-user/data-common/src/types.ts + - packages-user/data-state/src/core.ts + - packages-user/data-state/src/hero/index.ts + - packages-user/data-state/src/hero/moverImpl.ts + - packages-user/data-state/src/hero/predicate.ts + - packages-user/data-state/src/index.ts + - packages-user/data-system/src/event/eventDispatch.test.ts + - packages-user/data-system/src/index.ts + - packages-user/data-system/src/path/finder.ts + - packages-user/data-system/src/path/graph.test.ts + - packages-user/data-system/src/path/graph.ts + - packages-user/data-system/src/path/index.ts + - packages-user/data-system/src/path/performance.test.ts + - packages-user/data-system/src/path/system.test.ts + - packages-user/data-system/src/path/system.ts + - packages-user/data-system/src/path/types.ts +covered_digest: "v1:sha256:9a240d6d07ec1d218c07f9e39ad36d4e2cf57344dd575db44c8063fac61f3945" +behavior_unverified: 0 +overrides_applied: 0 +re_verification: + previous_status: passed + previous_score: 23/24 + gaps_closed: [] + gaps_remaining: [] + regressions: [] +deferred: + - truth: "移动端点击地图上的可达格时,角色自动寻路移动到该格" + addressed_in: "Phase 4" + evidence: "ROADMAP.md scope correction and D-11 assign rendering/mobile click wiring to Phase 4." +decision_coverage: + honored: 11 + total: 11 + not_honored: [] +--- + +# Phase 2: 寻路系统 Verification Report + +**Phase Goal:** 引擎支持自动寻路,移动端点击地图即可触发移动 +**Verified:** 2026-09-10T11:14:00Z +**Status:** passed +**Re-verification:** Yes — after the direct user-correction scope reset + +## Verification Scope + +This report verifies the corrected **L2-only** Phase 2 scope. The direct correction is authoritative: + +- Graph contracts and their API documentation are in `packages-user/data-system/src/path/types.ts`. +- `HeroPathfinding`, its tests/barrel, and `CoreState` wiring were unauthorized and are intentionally absent. Their absence is not a regression and does not create a gap. +- `DirectionMapper` is supplied by `IDataCommon`; graph construction reads `layer.state.directionMapper` and does not instantiate a mapper. +- The generic pass predicate is in `packages-user/data-state/src/hero/predicate.ts` as `DefaultPassPredicate`; it is not hero-named. + +The historical 02-03/02-04 artifacts mention the removed L3 wrapper, but those claims were not used as evidence. The live source and live tests below are authoritative. + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +|---|---|---|---| +| 1 | L2 builds a reachable directed graph from map data, honors injected pass decisions, and marks terminal nodes | ✓ VERIFIED | `path/graph.ts` performs BFS from the start over injected `IPassPredicate`; `graph.test.ts` passed 12 tests covering directionality, walls, reachability, and terminal nodes. | +| 2 | L2 finds minimum-loss paths, supports custom costs, path-only retrieval, movement mode selection, fallback policy, and interruption | ✓ VERIFIED | `path/finder.ts` and `path/system.ts` are substantive and wired; `system.test.ts` passed 13 tests. Named movement and interruption tests each passed independently. | +| 3 | Graph construction consumes the shared `IDataCommon.directionMapper`, with no per-builder `DirectionMapper` construction | ✓ VERIFIED | `IDataCommon.directionMapper` is declared in `data-common/src/types.ts`, initialized once by `CoreState`, exposed through `IMapLayer.state`, and read at `graph.ts:114`; static scope check found no mapper construction in `path/graph.ts`. | +| 4 | Graph contracts and documentation live in `data-system/src/path/types.ts` and are exported through the L2 barrel | ✓ VERIFIED | `IPathGraphEdge`, `IPathGraphNode`, `IPathGraph`, and `IPathfindingGraphBuilder` plus their JSDoc are in `path/types.ts`; `path/index.ts` and `data-system/src/index.ts` export the path API. | +| 5 | The pass predicate is generic, lives in `data-state/src/hero/predicate.ts`, and is consumed through the existing mover predicate contract | ✓ VERIFIED | `DefaultPassPredicate`/`DefaultPassPredicateImpl` are the only predicate names in the live implementation; `moverImpl.ts` imports and constructs it, and `HeroMover` calls `predicate().canPass/shouldHit`. | +| 6 | Role-specific L3 automatic movement through `HeroPathfinding` | ⏭ OUT OF SCOPE | Explicitly removed by the direct user correction. `data-state/src/path/` is absent, `CoreState` has no pathfinding property/import, and no live source reference remains. Do not recreate it. | +| 7 | Mobile map click triggers role movement | ⏭ DEFERRED | No click adapter is present by design; D-11 and the roadmap assign this rendering integration to Phase 4. | + +**Score:** 5/5 corrected in-scope truths verified. The two excluded/deferred rows are not counted as L2 must-haves. + +### Scope Exclusions + +| Item | Disposition | Evidence | +|---|---|---| +| `HeroPathfinding` implementation, tests, barrel, and `CoreState` wiring | Explicitly out of scope; intentionally deleted | `02-06-SUMMARY.md`, `02-DISCUSSION-LOG.md`, ROADMAP scope correction, and live absence checks. | +| Hero-specific end-to-end movement claims from historical plans | Not verified and not claimed | No live HeroPathfinding artifact exists; L2 tests use generic movable fixtures. | + +### Deferred Items + +| # | Item | Addressed In | Evidence | +|---|---|---|---| +| 1 | Mobile click → L2 pathfinding/movement adapter | Phase 4 | ROADMAP Phase 4 rendering/mobile scope and D-11. | + +## Required Artifacts + +| Artifact | Expected | Status | Details | +|---|---|---|---| +| `packages-user/data-system/src/path/types.ts` | Public graph and path contracts with API documentation | ✓ VERIFIED | Graph helper contracts and JSDoc are present in the corrected source file. | +| `packages-user/data-system/src/path/graph.ts` | Reachable directed graph builder | ✓ VERIFIED | Uses real `IMapLayer` locations, shared direction mapper, injected predicate, costs, and terminal-node classification. | +| `packages-user/data-system/src/path/finder.ts` | Minimum-loss path search | ✓ VERIFIED | Consumes `IPathGraph` and returns real `IPathfindingStep[]`; no static fallback. | +| `packages-user/data-system/src/path/system.ts` | L2 pathfinding system | ✓ VERIFIED | Wires finder, generic movable/mover bridge, path-only access, movement modes, fallback, and interruption. | +| `packages-user/data-system/src/path/index.ts` | L2 path barrel | ✓ VERIFIED | Exports finder, graph, system, and types. | +| `packages-user/data-common/src/types.ts` | Shared `IDataCommon.directionMapper` dependency | ✓ VERIFIED | `IDirectionMapper` field is part of the shared contract. | +| `packages-user/data-state/src/core.ts` | Shared mapper initialization | ✓ VERIFIED | Constructs one `DirectionMapper` for the common state; HeroPathfinding wiring is correctly absent. | +| `packages-user/data-state/src/hero/predicate.ts` | Generic `DefaultPassPredicate` contract/implementation | ✓ VERIFIED | Substantive pass-mask and hit logic, no hero-specific class/interface name. | +| `packages-user/data-state/src/hero/moverImpl.ts` | Existing mover consumes generic predicate and preserves event-source behavior | ✓ VERIFIED | Imports `DefaultPassPredicate`, guards nullable static sources, and retains dynamic event collection/order. | +| `packages-user/data-system/src/path/graph.test.ts` | Graph behavior coverage | ✓ VERIFIED | 12 active tests passed. | +| `packages-user/data-system/src/path/system.test.ts` | L2 path/system behavior coverage | ✓ VERIFIED | 13 active tests passed. | +| `packages-user/data-system/src/path/performance.test.ts` | Performance sanity coverage | ✓ VERIFIED | 3 active tests passed, including map-size and real-map cases. | +| `package.json` / `vite.config.ts` | Deterministic test gate | ✓ VERIFIED | `test:ci` runs `vitest run`; test and hook budgets are 30 seconds. | + +The deleted `packages-user/data-state/src/path/*` files are intentionally not required artifacts for the corrected phase and are not reported as missing. + +## Key Link Verification + +| From | To | Via | Status | Details | +|---|---|---|---|---| +| `IMapLayer.state` | `IDataCommon.directionMapper` | `layer.state.directionMapper.map(this.group)` | ✓ WIRED | `IMapLayer` extends `IDataCommonExtended`; `MapLayer.state` is the shared common state. | +| `PathfindingGraphBuilder` | graph contracts | imports from `./types` | ✓ WIRED | `graph.ts` imports all graph interfaces from `types.ts`; no duplicate declarations remain. | +| `PathfindingFinder` | `PathfindingGraphBuilder` | builder construction and `build(start)` | ✓ WIRED | Each find constructs a graph from current map/predicate/cost state, then searches it. | +| `PathfindingSystem` | `PathfindingFinder` / `IObjectMover` | finder calls and mover controller | ✓ WIRED | L2 tests exercise path-only, step, teleport, fallback, and stop behavior with a generic movable fixture. | +| `DefaultHeroMoveTopImpl` | `DefaultPassPredicate` | constructor + `predicate()` | ✓ WIRED | Existing mover behavior obtains the generic predicate implementation from `predicate.ts`. | +| `data-system/src/index.ts` | `data-system/src/path/index.ts` | `export * from './path'` | ✓ WIRED | L2 path API is publicly reachable. | +| `HeroPathfinding` | L2 pathfinding | — | ⏭ OUT OF SCOPE | No link is expected after the authorized deletion. | +| Mobile click adapter | L2 pathfinding | — | ⏭ DEFERRED | Rendering integration belongs to Phase 4. | + +## Data-Flow Trace (Level 4) + +| Artifact | Data variable | Source | Produces Real Data | Status | +|---|---|---|---|---| +| `PathfindingGraphBuilder` | nodes/edges | Current `IMapLayer` locations + shared direction mapper + injected predicate | Yes | ✓ FLOWING | +| `PathfindingFinder` | path steps | Reachable graph + node costs from current map state | Yes | ✓ FLOWING | +| `PathfindingSystem` | movement controller/path | Current generic `IObjectMover` position → finder → mover queue | Yes | ✓ FLOWING | +| `DefaultPassPredicate` | pass/hit decisions | `IMapState` event/layer pass data | Yes | ✓ FLOWING | +| Mobile click flow | target coordinate | No client pointer/touch source in this phase | No | ⏭ DEFERRED | + +## Behavioral Spot-Checks + +| Behavior | Command | Result | Status | +|---|---|---|---| +| Full corrected phase regression suite | `pnpm test:ci` | Exit 0; 10 files passed, 58 tests passed, 0 skipped, 0 failed | ✓ PASS | +| L2 movement transition | `pnpm exec vitest run packages-user/data-system/src/path/system.test.ts -t "moves step by step to the target and wraps the controller"` | 1 named test passed | ✓ PASS | +| L2 interruption transition | `pnpm exec vitest run packages-user/data-system/src/path/system.test.ts -t "interrupts the ongoing pathfinding move safely"` | 1 named test passed | ✓ PASS | +| Targeted implementation/fixture ESLint | `pnpm exec eslint` over changed implementation and fixture files | Exit 0; no problems | ✓ PASS | +| Scope/static checks | `git diff --check` plus live-source checks for HeroPathfinding, graph-local mapper construction, and hero-named predicate | No production scope violations; diff whitespace warnings only on planning files | ✓ PASS | + +Filtered named-test runs report 12 Vitest-filtered tests; those are test-selection exclusions, not disabled tests. The full suite reports zero skipped tests. + +## Probe Execution + +No phase-declared or conventional `probe-*.sh` probe exists. Probe execution was not applicable. + +## Requirements Coverage + +| Requirement | Source Plan | Description | Status | Evidence | +|---|---|---|---|---| +| PATH-01 | 02-01/02-02/02-03/02-04/02-05 | Engine supports automatic pathfinding | ✓ SATISFIED (corrected L2 scope) | Directed reachable graph, minimum-loss search, obstacle avoidance, path-only and movement APIs are live and covered by 12 graph tests, 13 system tests, 3 performance tests, and the full suite. | +| PATH-02 | 02-01/02-02/02-03/02-05 | Mobile map click triggers automatic movement | ⏭ DEFERRED | Mobile click wiring is explicitly excluded by D-11 and assigned to Phase 4; no click implementation is claimed. | + +No orphaned Phase 2 requirements were found. + +## Decision Coverage + +`check.decision-coverage-verify` reported **11/11** trackable context decisions honored. This is a non-blocking warning gate and produced no unhonored decisions. + +## Test Quality Audit + +| Test File | Linked Req | Active | Skipped | Circular | Assertion Level | Verdict | +|---|---|---:|---:|---:|---|---| +| `data-system/src/path/graph.test.ts` | PATH-01 | 12 | 0 | 0 | Behavioral/value | PASS | +| `data-system/src/path/system.test.ts` | PATH-01 | 13 | 0 | 0 | Behavioral/value | PASS | +| `data-system/src/path/performance.test.ts` | PATH-01 | 3 | 0 | 0 | Status/value | PASS | +| `data-common/src/common/mover.test.ts` | PATH-01 regression | 4 | 0 | 0 | Behavioral/value | PASS | + +No disabled-test pattern was found in the path test files. No requirement-linked test generates expected values from the system under test. + +## Quality Gates and Baseline Diagnostics + +- `pnpm test:ci`: **PASS** — 10 files, 58 tests, no skipped or failed tests. +- Targeted ESLint: **PASS** — no problems in changed implementation/fixture files. +- `pnpm check:type`: **baseline non-zero** — unrelated client/legacy and TileStore diagnostics remain. `CoreState` still reports the pre-existing TileStore TS2322/TS2345 pair; no path, predicate, `moverImpl.ts`, or `IDataCommon` diagnostic was reported. +- `pnpm check:circular`: **baseline non-zero** — 18 existing cycles reported; none traverses `data-system/src/path` or `data-state/src/hero/predicate.ts`. The existing `data-common/src/types.ts` cycle is not new: that file already imported `@motajs/common` before this correction. +- `git diff --check`: no code whitespace errors; it reports only existing LF→CRLF warnings for modified planning documents. +- Runtime warnings during tests: stale Browserslist data and expected fixture/logger warnings; none caused test failure. + +## Anti-Patterns Found + +| File | Line | Pattern | Severity | Impact | +|---|---:|---|---|---| +| `packages-user/data-state/src/core.ts` | 154 | Existing `TODO` for serialized event registration | Info | Pre-existing Phase 1/deferred work; not a `TBD`/`FIXME`/`XXX` debt marker and unrelated to L2 pathfinding. | +| `packages-user/data-system/src/path/finder.ts`, `system.ts` | guard returns | `return []` / `return null` | Info | Contract-prescribed invalid/unreachable/no-controller results; directly covered by tests, not stubs. | + +No unreferenced `TBD`, `FIXME`, or `XXX` marker was found in the corrected implementation files. No placeholder or console-only pathfinding implementation was found. + +## Human Verification Required + +None. This is an infrastructure/data-system phase with no user-facing UI in the corrected scope. Visual/mobile click verification belongs to Phase 4, not this report. + +## Gaps Summary + +The corrected L2 deliverable is present, substantive, wired, and behaviorally covered. The graph contracts and JSDoc are in the user-required `path/types.ts`; graph construction consumes the shared `IDataCommon.directionMapper`; and the generic `DefaultPassPredicate` is extracted without hero-specific naming. + +The deleted `HeroPathfinding` L3 implementation and its CoreState/barrel/test wiring are intentional scope exclusions, not gaps. Mobile click integration remains a Phase 4 deferred item. Repository-wide type and circular-dependency failures are documented baseline diagnostics outside the corrected L2 path deliverable. + +--- + +_Verified: 2026-09-10T11:14:00Z_ +_Verifier: the agent (gsd-verifier)_ diff --git a/packages-user/data-base/src/map/eventPath.test.ts b/packages-user/data-base/src/map/eventPath.test.ts index c56ce08..5a0a6e3 100644 --- a/packages-user/data-base/src/map/eventPath.test.ts +++ b/packages-user/data-base/src/map/eventPath.test.ts @@ -10,6 +10,7 @@ interface TestModules { MapState: typeof import('./mapState').MapState; TileStore: typeof import('@user/data-common').TileStore; RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder; + DirectionMapper: typeof import('@motajs/common').DirectionMapper; logger: typeof import('@motajs/common').logger; } @@ -35,6 +36,7 @@ beforeAll(async () => { MapState: mapModule.MapState, TileStore: commonModule.TileStore, RoleFaceBinder: commonModule.RoleFaceBinder, + DirectionMapper: loggerModule.DirectionMapper, logger: loggerModule.logger }; }); @@ -58,6 +60,7 @@ function createMapState() { eventStore: {}, roleFace: new modules.RoleFaceBinder(), faceManager: {}, + directionMapper: new modules.DirectionMapper(), saveSystem: {} } as never; return new modules.MapState(tileStore, state); diff --git a/packages-user/data-base/src/map/mapLifecycle.test.ts b/packages-user/data-base/src/map/mapLifecycle.test.ts index 35518a6..f50160a 100644 --- a/packages-user/data-base/src/map/mapLifecycle.test.ts +++ b/packages-user/data-base/src/map/mapLifecycle.test.ts @@ -29,6 +29,7 @@ interface TestModules { RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder; FaceManager: typeof import('@user/data-common').FaceManager; Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler; + DirectionMapper: typeof import('@motajs/common').DirectionMapper; } let modules: TestModules; @@ -38,12 +39,14 @@ beforeAll(async () => { vi.stubGlobal('location', { origin: 'http://localhost' }); const mapModule = await import('./mapState'); const commonModule = await import('@user/data-common'); + const motaModule = await import('@motajs/common'); modules = { MapState: mapModule.MapState, TileStore: commonModule.TileStore, RoleFaceBinder: commonModule.RoleFaceBinder, FaceManager: commonModule.FaceManager, - Dir8FaceHandler: commonModule.Dir8FaceHandler + Dir8FaceHandler: commonModule.Dir8FaceHandler, + DirectionMapper: motaModule.DirectionMapper }; }); @@ -77,6 +80,7 @@ function createMapState( eventStore: {}, roleFace: new modules.RoleFaceBinder(), faceManager, + directionMapper: new modules.DirectionMapper(), saveSystem: {} } as never; const mapState = new modules.MapState(tileStore, state); diff --git a/packages-user/data-common/src/types.ts b/packages-user/data-common/src/types.ts index 4ce7d2b..6d50297 100644 --- a/packages-user/data-common/src/types.ts +++ b/packages-user/data-common/src/types.ts @@ -1,4 +1,4 @@ -import { ITileLocator } from '@motajs/common'; +import { IDirectionMapper, ITileLocator } from '@motajs/common'; import { IFaceManager, IRoleFaceBinder } from './common'; import { IGameEventStore, IItemStore, IMapStore, ITileStore } from './store'; import { ISaveSystem } from './save'; @@ -56,6 +56,8 @@ export interface IDataCommon { readonly roleFace: IRoleFaceBinder; /** 朝向管理 */ readonly faceManager: IFaceManager; + /** 方向组映射器 */ + readonly directionMapper: IDirectionMapper; /** 存档系统 */ readonly saveSystem: ISaveSystem; } diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index 2e7c463..675cec0 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -75,9 +75,8 @@ import { } from './legacy'; import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader'; import { isNil } from 'lodash-es'; -import { logger } from '@motajs/common'; +import { DirectionMapper, IDirectionMapper, logger } from '@motajs/common'; import { DefaultHeroMoveTopImpl } from './hero'; -import { HeroPathfinding } from './path'; export class CoreState implements ICoreState { // Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据 @@ -88,6 +87,7 @@ export class CoreState implements ICoreState { readonly itemStore: IItemStore; readonly mapStore: IMapStore; readonly eventStore: IGameEventStore; + readonly directionMapper: IDirectionMapper; // Layer 1 数据层,所有可存档内容都在这,一般用于数据存储 readonly maps: IMapState; @@ -102,9 +102,6 @@ export class CoreState implements ICoreState { // Layer 3 用户层,也就是最顶层的内容,一般仅用于初始化以及仅供渲染端调用的顶层模块 readonly loadProgress: ILoadProgressTotal; readonly dataLoader: IMotaDataLoader; - /** 勇士寻路入口 */ - readonly pathfinding: HeroPathfinding; - /** 可存档对象映射 */ private readonly saveables: Map> = new Map(); /** 所有已添加的可存档对象 */ @@ -153,6 +150,7 @@ export class CoreState implements ICoreState { // 游戏事件 const eventStore = new GameEventStore(); this.eventStore = eventStore; + this.directionMapper = new DirectionMapper(); // TODO: 后续在此初始化路径注册外部序列化事件定义与地图事件 id 绑定。 //#endregion @@ -238,7 +236,6 @@ export class CoreState implements ICoreState { // 勇士顶层初始化 const heroMoveTopImpl = new DefaultHeroMoveTopImpl(this); this.hero.location.mover.useTopImplementation(heroMoveTopImpl); - this.pathfinding = new HeroPathfinding(this, heroMoveTopImpl); //#endregion } diff --git a/packages-user/data-state/src/hero/index.ts b/packages-user/data-state/src/hero/index.ts index daad7fc..17352e4 100644 --- a/packages-user/data-state/src/hero/index.ts +++ b/packages-user/data-state/src/hero/index.ts @@ -1,2 +1,3 @@ export * from './moverImpl'; +export * from './predicate'; export * from './types'; diff --git a/packages-user/data-state/src/hero/moverImpl.ts b/packages-user/data-state/src/hero/moverImpl.ts index 7b6ec40..ae13fc0 100644 --- a/packages-user/data-state/src/hero/moverImpl.ts +++ b/packages-user/data-state/src/hero/moverImpl.ts @@ -7,12 +7,12 @@ import { IHeroMoveTopHandler, IHeroMoveTopImpl, IMapState, - IPassCheckHandler, IPassPredicate, IReadonlyTileBase } from '@user/data-base'; -import { EventTrigger, FaceDirection, PassBit } from '@user/data-common'; +import { EventTrigger } from '@user/data-common'; import { IGameEventExecutor, IStateSystem } from '@user/data-system'; +import { DefaultPassPredicate, DefaultPassPredicateImpl } from './predicate'; import { isNil } from 'lodash-es'; interface IEventSource { @@ -22,129 +22,18 @@ interface IEventSource { readonly tile: IReadonlyTileBase | null; } -interface IDefaultHeroPassPredicate extends IPassPredicate {} - -class DefaultHeroPassPredicate implements IDefaultHeroPassPredicate { - constructor(private readonly maps: IMapState) {} - - private directionToPassBit(dir: FaceDirection): number { - switch (dir) { - case FaceDirection.Up: - return PassBit.Up; - case FaceDirection.Right: - return PassBit.Right; - case FaceDirection.Down: - return PassBit.Down; - case FaceDirection.Left: - return PassBit.Left; - default: - return 0; - } - } - - private oppositeDirection(dir: FaceDirection): FaceDirection { - switch (dir) { - case FaceDirection.Up: - return FaceDirection.Down; - case FaceDirection.Right: - return FaceDirection.Left; - case FaceDirection.Down: - return FaceDirection.Up; - case FaceDirection.Left: - return FaceDirection.Right; - case FaceDirection.LeftUp: - return FaceDirection.RightDown; - case FaceDirection.RightUp: - return FaceDirection.LeftDown; - case FaceDirection.LeftDown: - return FaceDirection.RightUp; - case FaceDirection.RightDown: - return FaceDirection.LeftUp; - default: - return FaceDirection.Unknown; - } - } - - canPass(handler: IPassCheckHandler): boolean { - const { currLoc, nextLoc, direction, floorId } = handler; - if (isNil(floorId)) return false; - - if ( - direction === FaceDirection.LeftDown || - direction === FaceDirection.LeftUp || - direction === FaceDirection.RightDown || - direction === FaceDirection.RightUp - ) { - return true; - } - - const map = this.maps.getMap(floorId); - if (!map) return false; - const event = map.eventLayer; - if (!event) return false; - - const { x, y } = currLoc; - const { x: nx, y: ny } = nextLoc; - const leaveMask = this.directionToPassBit(direction); - const enterMask = this.directionToPassBit( - this.oppositeDirection(direction) - ); - let canLeave = true; - let canEnter = true; - - const curr = event.getLocationData(x, y); - const next = event.getLocationData(nx, ny); - 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; - - for (const layer of map.layerList) { - if (layer === event) continue; - const curr = layer.getLocationData(x, y); - const next = layer.getLocationData(nx, ny); - let canLeave = true; - let canEnter = true; - const currRaw = curr?.static?.raw(); - const nextRaw = next?.static?.raw(); - if (currRaw?.pass.onlyEvents) { - canLeave = !!(leaveMask & currRaw.pass.outPass); - } - if (nextRaw?.pass.onlyEvents) { - canEnter = !!(enterMask & nextRaw.pass.inPass); - } - if (!canLeave || !canEnter) return false; - } - - return true; - } - - shouldHit(handler: IPassCheckHandler): boolean { - const { nextLoc, floorId } = handler; - if (isNil(floorId)) return false; - const map = this.maps.getMap(floorId); - if (!map) return false; - const eventLayer = map.eventLayer; - if (!eventLayer) return false; - const next = eventLayer.getLocationData(nextLoc.x, nextLoc.y); - const nextRaw = next?.static?.raw(); - return !!nextRaw && !nextRaw.eventPass; - } -} - export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl { /** 地图存储对象 */ private readonly maps: IMapState; /** 游戏事件执行器 */ private readonly executor: IGameEventExecutor; /** 勇士移动使用的通行性谓词 */ - private readonly passPredicate: IPassPredicate; + private readonly passPredicate: DefaultPassPredicate; constructor(state: IStateSystem) { this.maps = state.maps; this.executor = state.eventSystem.executor; - this.passPredicate = new DefaultHeroPassPredicate(this.maps); + this.passPredicate = new DefaultPassPredicateImpl(this.maps); } //#region 通行性判断 diff --git a/packages-user/data-state/src/hero/predicate.ts b/packages-user/data-state/src/hero/predicate.ts new file mode 100644 index 0000000..ee9a953 --- /dev/null +++ b/packages-user/data-state/src/hero/predicate.ts @@ -0,0 +1,114 @@ +import { IMapState, IPassCheckHandler, IPassPredicate } from '@user/data-base'; +import { FaceDirection, PassBit } from '@user/data-common'; +import { isNil } from 'lodash-es'; + +export interface DefaultPassPredicate extends IPassPredicate {} + +export class DefaultPassPredicateImpl implements DefaultPassPredicate { + constructor(private readonly maps: IMapState) {} + + private directionToPassBit(dir: FaceDirection): number { + switch (dir) { + case FaceDirection.Up: + return PassBit.Up; + case FaceDirection.Right: + return PassBit.Right; + case FaceDirection.Down: + return PassBit.Down; + case FaceDirection.Left: + return PassBit.Left; + default: + return 0; + } + } + + private oppositeDirection(dir: FaceDirection): FaceDirection { + switch (dir) { + case FaceDirection.Up: + return FaceDirection.Down; + case FaceDirection.Right: + return FaceDirection.Left; + case FaceDirection.Down: + return FaceDirection.Up; + case FaceDirection.Left: + return FaceDirection.Right; + case FaceDirection.LeftUp: + return FaceDirection.RightDown; + case FaceDirection.RightUp: + return FaceDirection.LeftDown; + case FaceDirection.LeftDown: + return FaceDirection.RightUp; + case FaceDirection.RightDown: + return FaceDirection.LeftUp; + default: + return FaceDirection.Unknown; + } + } + + canPass(handler: IPassCheckHandler): boolean { + const { currLoc, nextLoc, direction, floorId } = handler; + if (isNil(floorId)) return false; + + if ( + direction === FaceDirection.LeftDown || + direction === FaceDirection.LeftUp || + direction === FaceDirection.RightDown || + direction === FaceDirection.RightUp + ) { + return true; + } + + const map = this.maps.getMap(floorId); + if (!map) return false; + const event = map.eventLayer; + if (!event) return false; + + const { x, y } = currLoc; + const { x: nx, y: ny } = nextLoc; + const leaveMask = this.directionToPassBit(direction); + const enterMask = this.directionToPassBit( + this.oppositeDirection(direction) + ); + let canLeave = true; + let canEnter = true; + + const curr = event.getLocationData(x, y); + const next = event.getLocationData(nx, ny); + 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; + + for (const layer of map.layerList) { + if (layer === event) continue; + const curr = layer.getLocationData(x, y); + const next = layer.getLocationData(nx, ny); + let canLeave = true; + let canEnter = true; + const currRaw = curr?.static?.raw(); + const nextRaw = next?.static?.raw(); + if (currRaw?.pass.onlyEvents) { + canLeave = !!(leaveMask & currRaw.pass.outPass); + } + if (nextRaw?.pass.onlyEvents) { + canEnter = !!(enterMask & nextRaw.pass.inPass); + } + if (!canLeave || !canEnter) return false; + } + + return true; + } + + shouldHit(handler: IPassCheckHandler): boolean { + const { nextLoc, floorId } = handler; + if (isNil(floorId)) return false; + const map = this.maps.getMap(floorId); + if (!map) return false; + const eventLayer = map.eventLayer; + if (!eventLayer) return false; + const next = eventLayer.getLocationData(nextLoc.x, nextLoc.y); + const nextRaw = next?.static?.raw(); + return !!nextRaw && !nextRaw.eventPass; + } +} diff --git a/packages-user/data-state/src/index.ts b/packages-user/data-state/src/index.ts index fc9de2e..89c9dbb 100644 --- a/packages-user/data-state/src/index.ts +++ b/packages-user/data-state/src/index.ts @@ -1,6 +1,5 @@ export * from './enemy'; export * from './hero'; -export * from './path'; export * from './core'; export * from './ins'; diff --git a/packages-user/data-state/src/path/heroPathfinding.test.ts b/packages-user/data-state/src/path/heroPathfinding.test.ts deleted file mode 100644 index 8976b99..0000000 --- a/packages-user/data-state/src/path/heroPathfinding.test.ts +++ /dev/null @@ -1,358 +0,0 @@ -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 ( - this: Map, - 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?: { readonly faceDirection: number }; - 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 - }; -}); - -interface FixtureOptions { - readonly middleEvent?: boolean; - readonly targetNoPass?: boolean; - readonly sealedTarget?: boolean; -} - -function createFixture(options: FixtureOptions = {}) { - 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 - }); - tileStore.addTile({ - num: 2, - id: 'wall', - events: { 40: 'wall-touch' }, - type: 0, - pass: { onlyEvents: false, outPass: 0, inPass: 0 }, - 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: options.sealedTarget - ? [1, 2, 2] - : options.targetNoPass - ? [1, 1, 2] - : [1, 1, 1] - }, - layerAlias: { 0: 'event' }, - events: { - 0: - options.middleEvent === false - ? {} - : { 1: { 30: 'middle-enter' } } - } - })!; - const events = new Map(); - const store = { - addEvent() {}, - getEvent< - _P extends Record, - _E extends Record, - _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, - 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; - } - }); -} - -function addBlockingEvent( - events: Map, - id: string, - trigger: number, - calls: EventCall[], - blocked: Promise -): 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 () => { - 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 } } - ]); - }); - - // 验证无事件路径的瞬移一步到达目标 - it('teleports directly when the path has no events', async () => { - const fixture = createFixture({ middleEvent: false }); - const result = fixture.pathfinding.teleportTo({ 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 - }); - }); - - // 验证默认策略检测途经事件并自动回退为逐步移动 - it('falls back to step movement when the path has an event', async () => { - const calls: EventCall[] = []; - const fixture = createFixture(); - addEvent(fixture.events, 'middle-enter', 2, calls); - - const result = fixture.pathfinding.teleportTo({ 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 } } - ]); - }); - - // 验证 no-pass 目标移动至相邻格、面朝目标并派发 OnTouch - it('touches a no-pass target from its reachable adjacent cell', async () => { - const calls: EventCall[] = []; - const fixture = createFixture({ - middleEvent: false, - targetNoPass: true - }); - addEvent(fixture.events, 'wall-touch', 1, calls); - - 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: 1, - y: 0 - }); - expect(fixture.hero.mover!.faceDirection).toBe(3); - expect(calls).toEqual([ - { id: 'wall-touch', trigger: 1, hero: { x: 1, y: 0 } } - ]); - }); - - // 验证四邻无可达格时不可达目标不移动且不触发事件 - it('ignores a no-pass target without a reachable adjacent cell', () => { - const fixture = createFixture({ sealedTarget: true }); - const path = fixture.pathfinding.getPath({ x: 2, y: 0 }); - - expect(path).toEqual([]); - expect(fixture.pathfinding.moveTo({ x: 2, y: 0 })).toBeNull(); - expect({ x: fixture.hero.x, y: fixture.hero.y }).toEqual({ - x: 0, - y: 0 - }); - }); - - // 验证新寻路会停止旧移动并从兑现后的最新坐标接管 - it('hands over a moving path to a new target', async () => { - const calls: EventCall[] = []; - let release: () => void = () => {}; - const blocked = new Promise(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(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 - }); - }); -}); diff --git a/packages-user/data-state/src/path/heroPathfinding.ts b/packages-user/data-state/src/path/heroPathfinding.ts deleted file mode 100644 index 9696659..0000000 --- a/packages-user/data-state/src/path/heroPathfinding.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { ITileLocator, logger } from '@motajs/common'; -import { - BlockEventType, - IBlockEventEnv, - IBlockEventParam, - IGameEventInvocation, - IHeroMoveTopImpl, - IHeroState, - IMapLayer, - IMapState, - IReadonlyTileBase, - IPassPredicate -} from '@user/data-base'; -import { - EventTrigger, - FaceDirection, - IHeroAttr, - IMoverController, - IObjectMovable, - IObjectMover, - ObjectMoveStep -} 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; -} - -interface IHeroPathfinding extends IPathfindingSystem {} - -interface IEventSource { - readonly priority: number; - readonly id: string; - readonly type: BlockEventType; - readonly tile: IReadonlyTileBase | null; -} - -interface IResolvedPath { - readonly path: IPathfindingStep[]; - readonly adjacent: Readonly | null; - readonly target: Readonly | null; -} - -interface IHeroPathfindingController extends IMoverController {} - -class HeroPathfindingController implements IHeroPathfindingController { - private completed: boolean = false; - private readonly completion: Promise; - - constructor( - private readonly delegate: Readonly, - afterMove: () => Promise - ) { - this.completion = delegate.onEnd.then(async () => { - await afterMove(); - this.completed = true; - }); - } - - get done(): boolean { - return this.completed; - } - - get onEnd(): Promise { - return this.completion; - } - - push(...steps: Readonly[]): void { - this.delegate.push(...steps); - } - - insert(...steps: Readonly[]): void { - this.delegate.insert(...steps); - } - - stop(): Promise { - return this.delegate.stop(); - } -} - -class QueuedHeroPathfindingController implements IPathfindingController { - private current: IPathfindingController | null = null; - private currentPath: IPathfindingStep[] = []; - private cancelled: boolean = false; - private completed: boolean = false; - private readonly completion: Promise; - - constructor(start: () => Promise) { - 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 { - return this; - } - - get path(): readonly IPathfindingStep[] { - return this.currentPath; - } - - get done(): boolean { - return this.completed; - } - - get onEnd(): Promise { - return this.completion; - } - - isCancelled(): boolean { - return this.cancelled; - } - - push(...steps: Readonly[]): void { - this.current?.controller.push(...steps); - } - - insert(...steps: Readonly[]): void { - this.current?.controller.insert(...steps); - } - - stop(): Promise { - 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; - 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.system.useFallbackPolicy(path => this.hasEvent(path)); - this.bindCurrentLayer(); - } - - private hasEvent(path: readonly IPathfindingStep[]): boolean { - const layer = this.finderLayer(); - if (!layer) return false; - for (const step of path) { - const loc = layer.getLocationData(step.to.x, step.to.y); - if (!loc) continue; - const point = layer.getPointEvent(step.to.x, step.to.y); - if (point && point.size > 0) return true; - if (loc.static && loc.static.tileEvent().get().size > 0) { - return true; - } - for (const tile of loc.dynamics) { - if (tile.tileEvent().get().size > 0) return true; - } - } - return false; - } - - private finderLayer(): IMapLayer | null { - const floorId = this.state.hero.location.floorId; - const map = floorId ? this.state.maps.getMap(floorId) : null; - return map?.eventLayer ?? null; - } - - 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 | null): void { - this.system.useMover(mover); - } - - useMovable(movable: IObjectMovable | null): void { - this.system.useMovable(movable); - } - - useFallbackPolicy(policy: PathFallbackPolicy | null): void { - this.system.useFallbackPolicy(policy); - } - - getPath(target: ITileLocator): IPathfindingStep[] { - this.bindCurrentLayer(); - return this.resolvePath(target).path; - } - - moveTo(target: ITileLocator): IPathfindingController | null { - if (this.isMoving()) return this.queueAfterInterrupt(target, false); - this.bindCurrentLayer(); - const resolved = this.resolvePath(target); - 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 = 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 { - const path = this.system.getPath(target); - if (path.length > 0) { - return { path, adjacent: null, target: null }; - } - const layer = this.finderLayer(); - if (!layer || !this.isNoPass(layer, target)) { - return { path: [], adjacent: null, target: null }; - } - - const start = this.state.hero.location; - const candidates: ReadonlyArray> = [ - { x: target.x, y: target.y - 1 }, - { x: target.x + 1, y: target.y }, - { x: target.x, y: target.y + 1 }, - { x: target.x - 1, y: target.y } - ]; - for (const adjacent of candidates) { - if (!layer.inMap(adjacent.x, adjacent.y)) continue; - const adjacentPath = this.finder.find( - { x: start.x, y: start.y }, - adjacent - ); - if (adjacentPath.length > 0) { - return { path: adjacentPath, adjacent, target }; - } - } - return { path: [], adjacent: null, target: null }; - } - - private isNoPass(layer: IMapLayer, target: ITileLocator): boolean { - const raw = layer.getLocationData(target.x, target.y)?.static?.raw(); - if (!raw) return false; - return ( - !raw.eventPass || raw.pass.inPass === 0 || raw.pass.outPass === 0 - ); - } - - private directionTo(target: ITileLocator): FaceDirection { - const hero = this.state.hero.location; - if (target.x > hero.x) return FaceDirection.Right; - if (target.x < hero.x) return FaceDirection.Left; - if (target.y > hero.y) return FaceDirection.Down; - return FaceDirection.Up; - } - - private async finishAdjacentTouch(resolved: IResolvedPath): Promise { - const adjacent = resolved.adjacent!; - const target = resolved.target!; - const mover = this.state.hero.location.mover; - mover.setFaceDir(this.directionTo(target)); - await this.dispatchTouch(adjacent, target); - } - - private async dispatchTouch( - heroLoc: Readonly, - target: Readonly - ): Promise { - const layer = this.finderLayer(); - if (!layer) return; - const pointSources: IEventSource[] = []; - const tileSources: IEventSource[] = []; - const point = layer.getPointEvent(target.x, target.y); - const loc = layer.getLocationData(target.x, target.y); - if (point) { - for (const [priority, id] of point) { - pointSources.push({ - priority, - id, - type: BlockEventType.PointEvent, - tile: null - }); - } - } - if (loc) { - if (loc.static) { - for (const [priority, id] of loc.static.tileEvent().get()) { - tileSources.push({ - priority, - id, - type: BlockEventType.TileEvent, - tile: loc.static - }); - } - } - for (const tile of loc.dynamics) { - for (const [priority, id] of tile.tileEvent().get()) { - tileSources.push({ - priority, - id, - type: BlockEventType.TileEvent, - tile - }); - } - } - } - pointSources.sort((a, b) => b.priority - a.priority); - tileSources.sort((a, b) => b.priority - a.priority); - const invocations: IGameEventInvocation[] = []; - for (const source of [...pointSources, ...tileSources]) { - const env: IBlockEventEnv = { - state: this.state, - type: source.type, - trigger: EventTrigger.OnTouch, - heroLocator: heroLoc, - triggerLocator: target, - tile: source.tile, - layer, - map: layer.map - }; - invocations.push({ id: source.id, env }); - } - if (invocations.length === 0) return; - const param: IBlockEventParam = { custom: {} }; - await this.state.eventSystem.executor.execute(invocations, param); - } - - interrupt(): Promise { - 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 { - 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); - } -} diff --git a/packages-user/data-state/src/path/index.ts b/packages-user/data-state/src/path/index.ts deleted file mode 100644 index 858e89d..0000000 --- a/packages-user/data-state/src/path/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './heroPathfinding'; diff --git a/packages-user/data-system/src/event/eventDispatch.test.ts b/packages-user/data-system/src/event/eventDispatch.test.ts index 848fd28..2df3cfa 100644 --- a/packages-user/data-system/src/event/eventDispatch.test.ts +++ b/packages-user/data-system/src/event/eventDispatch.test.ts @@ -63,6 +63,7 @@ interface TestModules { RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder; FaceManager: typeof import('@user/data-common').FaceManager; Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler; + DirectionMapper: typeof import('@motajs/common').DirectionMapper; EventTrigger: typeof eventTriggers; BlockEventType: typeof blockEventTypes; EventExecuteMode: typeof eventExecuteModes; @@ -95,6 +96,7 @@ beforeAll(async () => { RoleFaceBinder: commonModule.RoleFaceBinder, FaceManager: commonModule.FaceManager, Dir8FaceHandler: commonModule.Dir8FaceHandler, + DirectionMapper: (await import('@motajs/common')).DirectionMapper, EventTrigger: eventTriggers, BlockEventType: blockEventTypes, EventExecuteMode: eventExecuteModes, @@ -133,6 +135,7 @@ function createFixture( eventStore: {}, roleFace: new modules.RoleFaceBinder(), faceManager, + directionMapper: new modules.DirectionMapper(), saveSystem: {} } as never; const maps = new modules.MapState(tileStore, commonState); diff --git a/packages-user/data-system/src/path/finder.ts b/packages-user/data-system/src/path/finder.ts index ed8e87e..fa7e3b2 100644 --- a/packages-user/data-system/src/path/finder.ts +++ b/packages-user/data-system/src/path/finder.ts @@ -7,8 +7,12 @@ import { } from '@user/data-base'; import { isNil } from 'lodash-es'; import { PathfindingGraphBuilder } from './graph'; -import { IPathfinder, IPathfindingStep, PathCostFunction } from './types'; -import { IPathGraph } from './graph'; +import { + IPathfinder, + IPathfindingStep, + IPathGraph, + PathCostFunction +} from './types'; interface IDistanceHeapEntry { /** 条目的键值,堆中键值最小的条目最先取出 */ diff --git a/packages-user/data-system/src/path/graph.test.ts b/packages-user/data-system/src/path/graph.test.ts index a9df592..eff197e 100644 --- a/packages-user/data-system/src/path/graph.test.ts +++ b/packages-user/data-system/src/path/graph.test.ts @@ -260,6 +260,7 @@ function createFixture( eventStore: {}, roleFace: new modules.RoleFaceBinder(), faceManager, + directionMapper: new modules.DirectionMapper(), saveSystem: {} } as never; const maps = new modules.MapState(tileStore, commonState); diff --git a/packages-user/data-system/src/path/graph.ts b/packages-user/data-system/src/path/graph.ts index 3b9bf83..5f322f3 100644 --- a/packages-user/data-system/src/path/graph.ts +++ b/packages-user/data-system/src/path/graph.ts @@ -1,7 +1,5 @@ import { - DirectionMapper, IDirectionDescriptor, - IDirectionMapper, InternalDirectionGroup, ITileLocator, logger @@ -15,79 +13,13 @@ import { IPassPredicate } from '@user/data-base'; import { isNil } from 'lodash-es'; -import { PathCostFunction } from './types'; - -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 cost: number; - /** 该节点是否仅可作为路径终点,不可作为中间节点 */ - readonly terminal: boolean; - /** 该节点的全部出边 */ - readonly edges: readonly IPathGraphEdge[]; -} - -export interface IPathGraph { - /** 图宽度 */ - readonly width: number; - /** 图高度 */ - readonly height: number; - /** 图内全部节点,键为节点索引 */ - readonly nodes: ReadonlyMap; -} - -export interface IPathfindingGraphBuilder { - /** - * 绑定地图状态对象,用于解析图层所属楼层 id - * @param maps 地图状态对象 - */ - useMapState(maps: IMapState | null): void; - - /** - * 绑定构建有向图所用的地图图层 - * @param layer 地图图层对象 - */ - useMapLayer(layer: IMapLayer | null): void; - - /** - * 设置构建图时使用的损失函数,未注入时每格损失 1 - * @param cost 损失函数 - */ - useCostFunction(cost: PathCostFunction | null): void; - - /** - * 注入判定边可行性的通行性谓词 - * @param predicate 通行性谓词 - */ - usePassPredicate(predicate: IPassPredicate | null): void; - - /** - * 设置邻域使用的方向组 - * @param group 朝向组 - */ - useDirGroup(group: number): void; - - /** - * 以起始位置为中心构建有向图:沿可通行有向边 BFS 扩展, - * 仅包含从起始位置可达的节点。图层未绑定或起始位置越界时告警并返回空图 - * @param start BFS 起始位置 - */ - build(start: ITileLocator): IPathGraph; -} +import { + IPathGraph, + IPathGraphEdge, + IPathGraphNode, + IPathfindingGraphBuilder, + PathCostFunction +} from './types'; /** * 将方向描述器的坐标增量解析为对应的朝向 @@ -118,9 +50,6 @@ export class PathfindingGraphBuilder implements IPathfindingGraphBuilder { /** 邻域方向组别,默认四正交方向 */ private group: number = InternalDirectionGroup.Dir4; - /** 方向组解析对象 */ - private readonly mapper: IDirectionMapper = new DirectionMapper(); - useMapState(maps: IMapState | null): void { this.maps = maps; } @@ -181,7 +110,9 @@ export class PathfindingGraphBuilder implements IPathfindingGraphBuilder { const height = layer.height; const floorId = this.resolveFloorId(); const state = layer.state; - const dirs: IDirectionDescriptor[] = [...this.mapper.map(this.group)]; + const dirs: IDirectionDescriptor[] = [ + ...layer.state.directionMapper.map(this.group) + ]; const terminals: Set = new Set(); const adjacency: Map = new Map(); diff --git a/packages-user/data-system/src/path/performance.test.ts b/packages-user/data-system/src/path/performance.test.ts index 856b80f..bb9aebd 100644 --- a/packages-user/data-system/src/path/performance.test.ts +++ b/packages-user/data-system/src/path/performance.test.ts @@ -40,6 +40,7 @@ interface TestModules { ObjectMover: typeof import('@user/data-common').ObjectMover; FaceManager: typeof import('@user/data-common').FaceManager; Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler; + DirectionMapper: typeof import('@motajs/common').DirectionMapper; RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder; } @@ -51,6 +52,7 @@ beforeAll(async () => { const systemModule = await import('./system'); const baseModule = await import('@user/data-base'); const commonModule = await import('@user/data-common'); + const motaModule = await import('@motajs/common'); modules = { PathfindingSystem: systemModule.PathfindingSystem, MapState: baseModule.MapState, @@ -58,7 +60,8 @@ beforeAll(async () => { ObjectMover: commonModule.ObjectMover, FaceManager: commonModule.FaceManager, Dir8FaceHandler: commonModule.Dir8FaceHandler, - RoleFaceBinder: commonModule.RoleFaceBinder + RoleFaceBinder: commonModule.RoleFaceBinder, + DirectionMapper: motaModule.DirectionMapper }; }); @@ -289,6 +292,7 @@ function createPerformanceSystem( eventStore: {}, roleFace: new modules.RoleFaceBinder(), faceManager, + directionMapper: new modules.DirectionMapper(), saveSystem: {} } as never; const maps = new modules.MapState(tileStore, commonState); diff --git a/packages-user/data-system/src/path/system.test.ts b/packages-user/data-system/src/path/system.test.ts index 433298f..0a5da52 100644 --- a/packages-user/data-system/src/path/system.test.ts +++ b/packages-user/data-system/src/path/system.test.ts @@ -43,6 +43,7 @@ interface TestModules { ObjectMover: typeof import('@user/data-common').ObjectMover; FaceManager: typeof import('@user/data-common').FaceManager; Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler; + DirectionMapper: typeof import('@motajs/common').DirectionMapper; RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder; logger: typeof import('@motajs/common').logger; } @@ -63,6 +64,7 @@ beforeAll(async () => { ObjectMover: commonModule.ObjectMover, FaceManager: commonModule.FaceManager, Dir8FaceHandler: commonModule.Dir8FaceHandler, + DirectionMapper: motaModule.DirectionMapper, RoleFaceBinder: commonModule.RoleFaceBinder, logger: motaModule.logger }; @@ -319,6 +321,7 @@ function createSystem(rows: number[], width: number): SystemFixture { eventStore: {}, roleFace: new modules.RoleFaceBinder(), faceManager, + directionMapper: new modules.DirectionMapper(), saveSystem: {} } as never; const maps = new modules.MapState(tileStore, commonState); diff --git a/packages-user/data-system/src/path/types.ts b/packages-user/data-system/src/path/types.ts index c663e3d..a9faeca 100644 --- a/packages-user/data-system/src/path/types.ts +++ b/packages-user/data-system/src/path/types.ts @@ -12,6 +12,78 @@ import { IObjectMovable } from '@user/data-common'; +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 cost: number; + /** 该节点是否仅可作为路径终点,不可作为中间节点 */ + readonly terminal: boolean; + /** 该节点的全部出边 */ + readonly edges: readonly IPathGraphEdge[]; +} + +export interface IPathGraph { + /** 图宽度 */ + readonly width: number; + /** 图高度 */ + readonly height: number; + /** 图内全部节点,键为节点索引 */ + readonly nodes: ReadonlyMap; +} + +export interface IPathfindingGraphBuilder { + /** + * 绑定地图状态对象,用于解析图层所属楼层 id + * @param maps 地图状态对象 + */ + useMapState(maps: IMapState | null): void; + + /** + * 绑定构建有向图所用的地图图层 + * @param layer 地图图层对象 + */ + useMapLayer(layer: IMapLayer | null): void; + + /** + * 设置构建图时使用的损失函数,未注入时每格损失 1 + * @param cost 损失函数 + */ + useCostFunction(cost: PathCostFunction | null): void; + + /** + * 注入判定边可行性的通行性谓词 + * @param predicate 通行性谓词 + */ + usePassPredicate(predicate: IPassPredicate | null): void; + + /** + * 设置邻域使用的方向组 + * @param group 朝向组 + */ + useDirGroup(group: number): void; + + /** + * 以起始位置为中心构建有向图:沿可通行有向边 BFS 扩展, + * 仅包含从起始位置可达的节点。图层未绑定或起始位置越界时告警并返回空图 + * @param start BFS 起始位置 + */ + build(start: ITileLocator): IPathGraph; +} + export interface IPathfindingStep { /** 移动方向 */ readonly dir: FaceDirection;