mirror of
https://github.com/motajs/template.git
synced 2026-07-17 01:11:09 +08:00
Compare commits
1 Commits
3c3f1ff43e
...
d9d97f2d63
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9d97f2d63 |
2
dev.md
2
dev.md
@ -131,7 +131,7 @@
|
||||
|
||||
**Layer 1 — 数据层**:包含所有会影响游戏存档与流程的数据内容,如地图、怪物、玩家属性等。本层通过统一接口 `IStateBase` 对外暴露数据访问能力,各类数据模块均以此接口为核心组织。
|
||||
|
||||
**Layer 2 — 执行层**:直接引用 Layer 1,负责产生影响游戏进程的动作,如玩家控制、战斗计算等。本层内容不会进入存档,仅通过修改 Layer 1 的数据来影响游戏状态。
|
||||
**Layer 2 — 执行层**:直接引用 Layer 1,负责产生影响游戏进程的动作,如玩家控制、战斗计算等。本层内容不会进入存档,仅通过修改 Layer 1 的数据来影响游戏状态,并通过统一接口 `ICoreState` 对外暴露执行能力。
|
||||
|
||||
| 包 | 层级 | 说明 |
|
||||
| ------------------- | ------- | ------------------------------------------------------------------------------- |
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
# 需求综述
|
||||
|
||||
`HeroLocation` 当前仅有坐标信息(`x`/`y`),无法确定勇士当前所处的楼层,导致 `HeroMover.checkTerrain` 无法定位到正确的楼层进行地形判定。同时 `checkTerrain` 仍为 TODO 存根,直接返回 `HeroMoveCode.Step`,无法完成通行判定。
|
||||
|
||||
本次需要完成以下内容:
|
||||
|
||||
1. 为 `HeroLocation` 新增楼层标识信息,保存楼层 id(`string | undefined`)而非 `ILayerState` 对象。勇士允许不处在任何楼层,此时 `floorId` 为 `undefined`。
|
||||
2. 完成 `HeroMover` 的地形通行判定逻辑,并引入可插拔的 `ITerrainPassChecker` 接口,允许用户注入自定义判定器。当未注入判定器时,地形判定直接返回不可通行。
|
||||
|
||||
# 接口设计分析
|
||||
|
||||
## ITerrainPassChecker
|
||||
|
||||
### 接口综述
|
||||
|
||||
`ITerrainPassChecker` 是地形通行判定的可插拔抽象,仅含一个判定方法。HeroMover 每步移动前调用它来判定前方一格是否可通行(高频操作)。用户通过在 `HeroMover` 上调用 `useTerrainChecker` 注入判定器。若未注入(`null`),则 `checkTerrain` 直接返回 `HeroMoveCode.CannotMove`,即所有地形均不可通行。
|
||||
|
||||
该接口定义在 Layer 1(`@user/data-base`)。由于 `IHeroLocation` / `IHeroMover` 同为 Layer 1 对象,无法持有 `IStateBase` 引用(`IStateBase` 是 Layer 1 的顶层接口,其中 `maps` 需要 `IStateBase` 才可访问),因此 `canPass` 仅接收 `floorId` 作为楼层标识,具体如何获取 `ILayerState` 由判定器实现方自行处理(判定器实现一般在 Layer 2,可持有 `IStateBase` 引用)。
|
||||
|
||||
参考 `IDamageCalculator` 的设计模式:职责单一、实例注入、无外部依赖。
|
||||
|
||||
### 接口分析
|
||||
|
||||
- `ITerrainPassChecker.canPass(locator, direction, floorId)`:预期频率**高频**。每步移动都需要判定前方地形是否可通行,故为单单词命名。典型使用场景:`HeroMover.onStepStart` 中拿到当前步的 `moveDirection` 后调用 `canPass` 进行判定。`floorId` 可能为 `undefined`(勇士不处于任何楼层时),判定器需自行处理。
|
||||
|
||||
### 预期体量
|
||||
|
||||
接口定义约 6 行。HeroMover 中 `checkTerrain` 修改约 10 行,`useTerrainChecker` 约 5 行。
|
||||
|
||||
## IHeroLocation
|
||||
|
||||
### 接口变化
|
||||
|
||||
`IHeroLocation` 新增 `readonly floorId: string | undefined` 只读成员。这是楼层标识符,与 `IMapStore.getLayerState(id)` 的 `id` 参数一致。`undefined` 表示勇士尚未处于任何楼层。
|
||||
|
||||
## IHeroMover
|
||||
|
||||
### 接口变化
|
||||
|
||||
`IHeroMover` 新增 `useTerrainChecker(checker)` 方法,传入 `null` 移除判定器(默认状态为 `null`)。
|
||||
|
||||
预期频率:**低频**。一般仅在初始化时调用一次以注入自定义判定逻辑。
|
||||
|
||||
## 默认判定器
|
||||
|
||||
默认判定器(基于图块 `inPass`/`outPass` 的标准通行判定)作为 `ITerrainPassChecker` 的独立实现,放在 `@user/data-state/src/hero/` 下(Layer 2)。该实现持有 `IStateBase` 引用,可在 `canPass` 中通过 `floorId` 查找 `ILayerState`,进而获取事件层图块数据完成通行判定。
|
||||
|
||||
由于默认判定器不在本次需求的核心范围内,仅在设计层面预留位置,后续单独实现。
|
||||
|
||||
# 实现思路
|
||||
|
||||
## 1. 在 `types.ts` 中新增接口与类型
|
||||
|
||||
新增 `ITerrainPassChecker` 接口:
|
||||
|
||||
```ts
|
||||
export interface ITerrainPassChecker {
|
||||
canPass(
|
||||
locator: ITileLocator,
|
||||
direction: FaceDirection,
|
||||
floorId: string | undefined
|
||||
): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
修改 `IHeroLocation`:新增 `readonly floorId: string | undefined`。
|
||||
|
||||
修改 `IHeroLocationSave`:新增 `readonly floorId: string | undefined`。由于原定义为 `extends Readonly<IFacedTileLocator>`,新增 `floorId` 后需展开为显式成员声明,避免将 `floorId` 类型污染到 `IFacedTileLocator`。
|
||||
|
||||
修改 `IHeroMover`:新增 `useTerrainChecker(checker: ITerrainPassChecker | null): void`。
|
||||
|
||||
## 2. 修改 `HeroLocation` 类
|
||||
|
||||
- 新增 `floorId: string | undefined` 成员,初始值 `undefined`。
|
||||
- `saveState` 中将 `floorId` 写入 `IHeroLocationSave`。
|
||||
- `loadState` 中从 `IHeroLocationSave` 恢复 `floorId`。
|
||||
|
||||
由于暂不提供 `setFloorId`,`floorId` 的赋值留待后续处理。
|
||||
|
||||
## 3. 修改 `HeroMover` 类
|
||||
|
||||
- 新增 `private terrainChecker: ITerrainPassChecker | null = null`。
|
||||
- 实现 `useTerrainChecker(checker)`:`this.terrainChecker = checker ?? null`。
|
||||
- 修改 `checkTerrain` 方法:
|
||||
1. 若 `terrainChecker` 为 `null`,直接返回 `HeroMoveCode.CannotMove`。
|
||||
2. 否则以当前坐标(`{ x: tile.x, y: tile.y }`)、`this.moveDirection`、`tile.floorId` 为参数调用 `terrainChecker.canPass`。
|
||||
3. 若 `canPass` 返回 `false`,返回 `HeroMoveCode.CannotMove`;返回 `true` 则返回 `HeroMoveCode.Step`。
|
||||
|
||||
## 4. 导出
|
||||
|
||||
`ITerrainPassChecker` 已定义在 `hero/types.ts`,通过 `hero/index.ts` 导出,无需额外操作。
|
||||
|
||||
# 涉及文件
|
||||
|
||||
## 需要引用的文件
|
||||
|
||||
- `@motajs/common`:引用 `ITileLocator`。
|
||||
- `@user/data-common`:引用 `FaceDirection`、`IObjectMover`、`IObjectMovable`、`IFaceHandler`、`IMoverController`、`ObjectMoveStep`、`ObjectMoveStepType`、`getFaceMovement`。
|
||||
- `@user/data-base`:引用 `IHeroLocation`、`IHeroMover`、`IHeroMoverConfig`、`HeroMoveCode`。
|
||||
|
||||
## 需要修改的文件
|
||||
|
||||
### `packages-user/data-base/src/hero/types.ts`
|
||||
|
||||
- [ ] 新增 `ITerrainPassChecker` 接口:提供可插拔的地形通行判定能力。
|
||||
- [ ] 修改 `IHeroLocation`:新增 `readonly floorId: string | undefined`。
|
||||
- [ ] 修改 `IHeroLocationSave`:新增 `readonly floorId: string | undefined`。将 `extends Readonly<IFacedTileLocator>` 展开为显式成员声明 `readonly x: number`、`readonly y: number`、`readonly direction: FaceDirection`、`readonly floorId: string | undefined`。
|
||||
- [ ] 修改 `IHeroMover`:新增 `useTerrainChecker(checker: ITerrainPassChecker | null): void`。
|
||||
|
||||
### `packages-user/data-base/src/hero/location.ts`
|
||||
|
||||
- [ ] 新增 `floorId: string | undefined` 成员,初始 `undefined`。
|
||||
- [ ] 修改 `saveState`,新增 `floorId` 字段。
|
||||
- [ ] 修改 `loadState`,恢复 `floorId` 字段。
|
||||
|
||||
### `packages-user/data-base/src/hero/mover.ts`
|
||||
|
||||
- [ ] 新增 `private terrainChecker: ITerrainPassChecker | null = null`。
|
||||
- [ ] 实现 `useTerrainChecker` 方法。
|
||||
- [ ] 修改 `checkTerrain` 方法:无判定器时返回 `CannotMove`,有判定器时委托调用 `canPass`。
|
||||
@ -6,7 +6,6 @@ import { IHeroLocation, IHeroLocationSave, IHeroMover } from './types';
|
||||
export class HeroLocation implements IHeroLocation {
|
||||
x: number;
|
||||
y: number;
|
||||
floorId: string | undefined = undefined;
|
||||
|
||||
readonly state: IDataCommon;
|
||||
readonly mover: IHeroMover<this>;
|
||||
@ -37,15 +36,13 @@ export class HeroLocation implements IHeroLocation {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
direction: this.mover.faceDirection,
|
||||
floorId: this.floorId
|
||||
direction: this.mover.faceDirection
|
||||
};
|
||||
}
|
||||
|
||||
loadState(state: IHeroLocationSave): void {
|
||||
this.x = state.x;
|
||||
this.y = state.y;
|
||||
this.floorId = state.floorId;
|
||||
this.mover.setFaceDir(state.direction);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,39 +12,35 @@ import {
|
||||
HeroMoveCode,
|
||||
IHeroLocation,
|
||||
IHeroMover,
|
||||
IHeroMoverConfig,
|
||||
ITerrainPassChecker
|
||||
IHeroMoverConfig
|
||||
} from './types';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
export class HeroMover<T extends IHeroLocation>
|
||||
extends ObjectMover<T>
|
||||
implements IHeroMover<T>
|
||||
{
|
||||
readonly tile: T;
|
||||
|
||||
/** 本次移动是否不记录进路线系统 */
|
||||
private noRoute: boolean = false;
|
||||
/** 本次移动是否忽略地形碰撞检测 */
|
||||
private ignoreTerrain: boolean = false;
|
||||
/** 本次移动是否在特定时机触发自动存档 */
|
||||
private autoSave: boolean = false;
|
||||
/** 自定义地形通行判定器 */
|
||||
private terrainChecker: ITerrainPassChecker | null = null;
|
||||
|
||||
constructor(
|
||||
readonly tile: T,
|
||||
faceHandler: IFaceHandler<FaceDirection>
|
||||
) {
|
||||
constructor(tile: T, faceHandler: IFaceHandler<FaceDirection>) {
|
||||
super(faceHandler);
|
||||
this.tile = tile;
|
||||
}
|
||||
|
||||
config(config: Readonly<IHeroMoverConfig>): this {
|
||||
if (!isNil(config.noRoute)) {
|
||||
if (config.noRoute !== undefined) {
|
||||
this.noRoute = config.noRoute;
|
||||
}
|
||||
if (!isNil(config.ignoreTerrain)) {
|
||||
if (config.ignoreTerrain !== undefined) {
|
||||
this.ignoreTerrain = config.ignoreTerrain;
|
||||
}
|
||||
if (!isNil(config.autoSave)) {
|
||||
if (config.autoSave !== undefined) {
|
||||
this.autoSave = config.autoSave;
|
||||
}
|
||||
return this;
|
||||
@ -58,10 +54,6 @@ export class HeroMover<T extends IHeroLocation>
|
||||
};
|
||||
}
|
||||
|
||||
useTerrainChecker(checker: ITerrainPassChecker | null): void {
|
||||
this.terrainChecker = checker;
|
||||
}
|
||||
|
||||
protected async onMoveStart(
|
||||
_tile: IHeroLocation,
|
||||
_controller: Readonly<IMoverController>
|
||||
@ -89,7 +81,9 @@ export class HeroMover<T extends IHeroLocation>
|
||||
|
||||
if (!this.ignoreTerrain) {
|
||||
const result = this.checkTerrain(tile, controller);
|
||||
if (result !== HeroMoveCode.Step) return result;
|
||||
if (result !== HeroMoveCode.Step) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoSave) {
|
||||
@ -174,16 +168,10 @@ export class HeroMover<T extends IHeroLocation>
|
||||
* 检测当前移动方向前方一格的地形是否可通行
|
||||
*/
|
||||
private checkTerrain(
|
||||
tile: IHeroLocation,
|
||||
_tile: IHeroLocation,
|
||||
_controller: Readonly<IMoverController>
|
||||
): HeroMoveCode {
|
||||
if (!this.terrainChecker) return HeroMoveCode.CannotMove;
|
||||
const locator: ITileLocator = { x: tile.x, y: tile.y };
|
||||
const pass = this.terrainChecker.canPass(
|
||||
locator,
|
||||
this.moveDirection,
|
||||
tile.floorId
|
||||
);
|
||||
return pass ? HeroMoveCode.Step : HeroMoveCode.CannotMove;
|
||||
// TODO: 需要地形检测接口(替代 core.noPass / core.canMoveHero)
|
||||
return HeroMoveCode.Step;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
import { IFacedTileLocator, IHookBase, IHookable } from '@motajs/common';
|
||||
import {
|
||||
IFacedTileLocator,
|
||||
IHookBase,
|
||||
IHookable,
|
||||
ITileLocator
|
||||
} from '@motajs/common';
|
||||
import {
|
||||
FaceDirection,
|
||||
IDataCommonExtended,
|
||||
IObjectMovable,
|
||||
IObjectMover,
|
||||
@ -192,24 +186,13 @@ export interface IHeroAttribute<THero> extends IReadonlyHeroAttribute<THero> {
|
||||
|
||||
//#region 勇士位置
|
||||
|
||||
export interface IHeroLocationSave {
|
||||
/** 当前横坐标 */
|
||||
readonly x: number;
|
||||
/** 当前纵坐标 */
|
||||
readonly y: number;
|
||||
/** 当前朝向 */
|
||||
readonly direction: FaceDirection;
|
||||
/** 当前所在楼层 id,undefined 表示尚不处于任何楼层 */
|
||||
readonly floorId: string | undefined;
|
||||
}
|
||||
export interface IHeroLocationSave extends Readonly<IFacedTileLocator> {}
|
||||
|
||||
export interface IHeroLocation
|
||||
extends
|
||||
ISaveableContent<IHeroLocationSave>,
|
||||
IObjectMovable,
|
||||
IDataCommonExtended {
|
||||
/** 当前所在楼层 id,undefined 表示尚不处于任何楼层 */
|
||||
readonly floorId: string | undefined;
|
||||
/** 勇士的移动对象 */
|
||||
readonly mover: IObjectMover<this>;
|
||||
}
|
||||
@ -229,23 +212,6 @@ export const enum HeroMoveCode {
|
||||
CannotMove
|
||||
}
|
||||
|
||||
export interface ITerrainPassChecker {
|
||||
/** 对应的勇士状态对象 */
|
||||
readonly hero: IHeroState<unknown>;
|
||||
|
||||
/**
|
||||
* 判断在指定楼层中,从指定坐标向指定方向移动一格是否可通行
|
||||
* @param locator 当前位置
|
||||
* @param direction 移动方向
|
||||
* @param floorId 当前楼层 id,可能为 undefined
|
||||
*/
|
||||
canPass(
|
||||
locator: ITileLocator,
|
||||
direction: FaceDirection,
|
||||
floorId: string | undefined
|
||||
): boolean;
|
||||
}
|
||||
|
||||
export interface IHeroMoverConfig {
|
||||
/** 本次移动是否不记录进路线系统 */
|
||||
noRoute?: boolean;
|
||||
@ -266,12 +232,6 @@ export interface IHeroMover<T extends IHeroLocation> extends IObjectMover<T> {
|
||||
* 获取当前移动配置的只读快照
|
||||
*/
|
||||
getConfig(): Readonly<IHeroMoverConfig>;
|
||||
|
||||
/**
|
||||
* 设置地形通行判定器,传入 null 移除判定器
|
||||
* @param checker 地形判定器,null 表示移除
|
||||
*/
|
||||
useTerrainChecker(checker: ITerrainPassChecker | null): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -247,8 +247,6 @@ export interface IObjectMover<T extends IObjectMovable> extends IHookable<
|
||||
readonly currAnimDir: ObjectAnimDirection;
|
||||
/** 当前移动速度,单位毫秒 */
|
||||
readonly currentSpeed: number;
|
||||
/** 朝向信息对象 */
|
||||
readonly faceHandler: IFaceHandler<FaceDirection>;
|
||||
|
||||
/**
|
||||
* 立刻设置移动对象的位置
|
||||
@ -369,7 +367,8 @@ export abstract class ObjectMover<T extends IObjectMovable>
|
||||
/** 是否调用了 `IMoverController.stop` 接口 */
|
||||
private shouldStop: boolean = false;
|
||||
|
||||
readonly faceHandler: IFaceHandler<FaceDirection>;
|
||||
/** 朝向处理 */
|
||||
private readonly faceHandler: IFaceHandler<FaceDirection>;
|
||||
|
||||
constructor(faceHandler: IFaceHandler<FaceDirection>) {
|
||||
super();
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from './passChecker';
|
||||
export * from './types';
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
import { ITileLocator } from '@motajs/common';
|
||||
import { FaceDirection, PassBit } from '@user/data-common';
|
||||
import { IHeroState, IStateBase, ITerrainPassChecker } from '@user/data-base';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
export class DefaultPassChecker implements ITerrainPassChecker {
|
||||
constructor(
|
||||
readonly state: IStateBase,
|
||||
readonly hero: IHeroState<unknown>
|
||||
) {}
|
||||
|
||||
canPass(
|
||||
locator: ITileLocator,
|
||||
direction: FaceDirection,
|
||||
floorId: string | undefined
|
||||
): boolean {
|
||||
if (isNil(floorId)) return false;
|
||||
if (
|
||||
direction === FaceDirection.LeftDown ||
|
||||
direction === FaceDirection.LeftUp ||
|
||||
direction === FaceDirection.RightDown ||
|
||||
direction === FaceDirection.RightUp
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const layerState = this.state.maps.getLayerState(floorId);
|
||||
if (!layerState) return false;
|
||||
const eventLayer = layerState.eventLayer;
|
||||
if (!eventLayer) return false;
|
||||
|
||||
const { x, y } = locator;
|
||||
const face = this.hero.location.mover.faceHandler;
|
||||
const { x: dx, y: dy } = face.movement(direction);
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
const opposite = face.opposite(direction);
|
||||
const leaveMask = this.directionToPassBit(direction);
|
||||
const enterMask = this.directionToPassBit(opposite);
|
||||
|
||||
let canLeave = true;
|
||||
let canEnter = true;
|
||||
|
||||
const eventCurr = eventLayer.getLocationData(x, y);
|
||||
const eventNext = eventLayer.getLocationData(nx, ny);
|
||||
if (eventCurr && eventCurr.raw) {
|
||||
canLeave = !!(leaveMask & eventCurr.raw.pass.outPass);
|
||||
}
|
||||
if (eventNext && eventNext.raw) {
|
||||
canEnter = !!(enterMask & eventNext.raw.pass.inPass);
|
||||
}
|
||||
|
||||
if (!canLeave || !canEnter) return false;
|
||||
|
||||
for (const layer of layerState.layerList) {
|
||||
if (layer === eventLayer) continue;
|
||||
const curr = layer.getLocationData(x, y);
|
||||
const next = layer.getLocationData(nx, ny);
|
||||
if (curr && curr.raw && curr.raw.pass.onlyEvents) {
|
||||
canLeave = !!(leaveMask & curr.raw.pass.outPass);
|
||||
}
|
||||
if (next && next.raw && next.raw.pass.onlyEvents) {
|
||||
canEnter = !!(enterMask & next.raw.pass.inPass);
|
||||
}
|
||||
if (!canLeave || !canEnter) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将朝向转换为对应的通行性位掩码。
|
||||
* @param dir 朝向
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -19,8 +19,6 @@
|
||||
13. **map/filter 优于 for 循环**:对数组进行数据收集或转换时(如收集 Promise),使用 `map`、`filter` 等函数式写法,比 `for` 循环 + `push` 的可读性更高。
|
||||
14. **渲染端永远处于被动**:任何情况下都不可能会出现数据端主动通知渲染端进行更新的场景,渲染端仅通过钩子与数据端通信,通过钩子被动获取信息,并在某些情况下影响数据端行为。
|
||||
15. **按设计实现,勿自由发挥**:我已想好功能的基本思路和代码结构,你的任务是还原它,而非自行创造。
|
||||
16. **代码首先得看着舒服**:代码的第一读者是人,必须一眼就能看清整体结构,不需要反复来回读才能理顺逻辑。不出现过长的表达式,不进行不必要的函数拆分。不允许出现 3 层以上的 `if` 嵌套。
|
||||
17. **关于非空判断**:对于对象,直接使用 `if (!object)` 的方式进行判断,对于字面量,使用 `lodash` 接口 `isNil` 判断 `if (isNil(value))`,不要使用 `if (value === undefined)` 这种方式。
|
||||
|
||||
# 建议规则
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user