mirror of
https://github.com/motajs/template.git
synced 2026-07-17 01:11:09 +08:00
Compare commits
4 Commits
3375d98634
...
7500f72a61
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7500f72a61 | ||
| 94a16b31ba | |||
| 3d72954bb2 | |||
| 327bdc840b |
72
docs/dev/template.md
Normal file
72
docs/dev/template.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
> 本示例文档以物体移动为例,代码参考 `packages-user/data-common/src/common/mover.ts`。文档较为简略,实际编写的时候可以更丰富一些。
|
||||||
|
|
||||||
|
# 需求综述
|
||||||
|
|
||||||
|
为提供更便捷的物体移动接口,故进行本设计。本设计提供统一的移动接口 `IObjectMover`,并完成其抽象实现 `abstract class ObjectMover`,以方便具体拓展。
|
||||||
|
|
||||||
|
# 接口设计分析
|
||||||
|
|
||||||
|
默认场景下,移动主要发生在两种物体上:地图动态图块及玩家操控的勇士。二者的主要差别在于后者由玩家操控,需要进行移动判定,例如需要进行通行性判定,事件触发判定等,而前者通常由系统代码控制,且常用于演出,并不需要进行判定。
|
||||||
|
|
||||||
|
## IObjectMover
|
||||||
|
|
||||||
|
### 设计意图
|
||||||
|
|
||||||
|
对于玩家操控,判定往往发生在移动前,因此为统一接口,设计出 `onStepStart` 与 `onStepEnd` 两个接口,前者进行判定,后者根据判定结果进行相应的操作。为了让后者知道判定结果,将 `onStepStart` 的返回值设计为 `Promise<number>`,`onStepEnd` 接收之并执行操作。
|
||||||
|
|
||||||
|
在实际情况中,常见的使用场景是先规划好线路,然后让物体移动,因此接口需要设计成计划式,计划完成后通过 `start` 接口开始移动,故需要两个接口 `onMoveStart` 和 `onMoveEnd` 分别在移动开始前和结束后执行某些操作。
|
||||||
|
|
||||||
|
为了能够在开始移动后对移动进行干预及监听,`start` 应返回 `IMoveController` 作为返回值,其中应包含临时在当前步后插入一步、在当前移动队列后插入一步、下一步结束后立刻停止移动、当移动结束后兑现的 `Promise` 等内容。
|
||||||
|
|
||||||
|
接下来考虑移动计划,最通用的移动方法是指定移动方向与物体的面朝朝向,然后物体面朝指定朝向,向移动方向移动一格,故需要 `stepFace` 接口。在绝大多数情况下,移动方向与朝向是一致的,故提供 `step` 接口。除了这种情况,还会包含前进一格的需求,故提供 `forward` 接口;与之对应,还提供 `backward` 后退接口,而且在多数情况下,后退往往代表面朝前向后走,这里也使用这一语义。在后退时,移动动画的播放进度应该相反,故提供 `animDir` 接口。在移动时,移动速度是一个重要参数,故提供 `speed` 接口。在某些情况下,物体可能会需要瞬移,故提供 `tp` 接口。除了这些,还需要提供转向接口 `face`,调整面朝朝向;以及一个相对比较常见的 `jump` 接口。
|
||||||
|
|
||||||
|
对于移动对象,仅需要 `x` `y` `setPos` `getCurrentFaceDirection` 接口即可完成所有上述行为,故将这几个接口包装为 `IObjectMovable`,同时 `interface IObjectMover<T extends IObjectMovable>` 允许自定义拓展。
|
||||||
|
|
||||||
|
### 接口分析
|
||||||
|
|
||||||
|
> 此处仅列出部分。
|
||||||
|
|
||||||
|
- `IObjectMover.forward()`:预期频率**高频**。向前移动一格是地图行走、动画演出等场景的核心需求,在逻辑与演出中都会频繁出现,故为高频。典型使用场景:演出中玩家或 NPC 沿某方向连续移动。
|
||||||
|
- `IObjectMover.speed()`:预期频率**中频**。移动中修改移速有一定使用场景,但远不及 `forward`、`step` 等移动接口的频率,通常只在特殊演出或逻辑中出现,故为中频。典型使用场景:NPC 逃离怪物时先定在原地,随后逐渐加速逃跑。
|
||||||
|
- `IObjectMover.stepFace()`:预期频率**低频**。移动方向与朝向不同的常见场景(后退)已由 `backward` 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。
|
||||||
|
|
||||||
|
### 预期体量
|
||||||
|
|
||||||
|
本节应当写出预期的代码体量,并分析原因。示例如下:
|
||||||
|
|
||||||
|
预期代码体量为 200-300 行。分析如下:
|
||||||
|
|
||||||
|
- `IObjectMover` 首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多,整体预计在 100 行左右。
|
||||||
|
- `IObjectMover` 还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂,预计需要 100-200 行。
|
||||||
|
|
||||||
|
## IMoveController
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
## 移动流程控制
|
||||||
|
|
||||||
|
核心难点在于如何进行移动控制,以下为核心思路。
|
||||||
|
|
||||||
|
1. 为实现移动流程,首先需要写一个大循环来处理每一个移动步。
|
||||||
|
2. 对于每一个移动步,首先调用 `onStepStart` 接口,获取移动 `code`,然后触发钩子,然后进行 `onStepEnd` 的执行,其返回值作为移动后的目标位置,最后触发钩子。
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
## 需要引用的文件
|
||||||
|
|
||||||
|
- `lodash-es`: 引用 `lodash` 工具库,使用某些工具函数。
|
||||||
|
- `@motajs/common`: 引用系统基本库,使用钩子相关接口。
|
||||||
|
- `types.ts`: 引用当前子模块的类型标注文件,使用相应的接口声明。
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `@user/data-common/src/common/mover.ts`
|
||||||
|
|
||||||
|
- [ ] 编写 `IObjectMover` 接口:按照本文描述完成设计
|
||||||
|
- [ ] 编写 `IObjectMovable` 接口:按照本文描述完成设计
|
||||||
|
- [ ] 编写 `IMoveController` 接口:按照本文描述完成设计
|
||||||
|
- [ ] 完成每个移动步的接口设计:每个移动步都需要一个接口对象,本文未明确提及,需包含每种移动计划
|
||||||
|
- [ ] 完成移动枚举的编写:每种移动计划都需要一个枚举类型来标注
|
||||||
|
- [ ] 编写 `ObjectMover` 抽象类:实现 `IObjectMover` 接口,作为移动基类
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { ITileLocator } from '@motajs/common';
|
import { ITileLocator, logger } from '@motajs/common';
|
||||||
import {
|
import {
|
||||||
FaceDirection,
|
FaceDirection,
|
||||||
IFaceHandler,
|
IFaceHandler,
|
||||||
@ -10,13 +10,11 @@ import {
|
|||||||
} from '@user/data-common';
|
} from '@user/data-common';
|
||||||
import {
|
import {
|
||||||
HeroMoveCode,
|
HeroMoveCode,
|
||||||
IHeroHitAction,
|
|
||||||
IHeroHitHandler,
|
|
||||||
IHeroLocation,
|
IHeroLocation,
|
||||||
IHeroMover,
|
IHeroMover,
|
||||||
IHeroMoverConfig,
|
IHeroMoverConfig,
|
||||||
IPassCheckerHandler,
|
IHeroMoveTopHandler,
|
||||||
ITerrainPassChecker
|
IHeroMoveTopImpl
|
||||||
} from './types';
|
} from './types';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
|
|
||||||
@ -26,17 +24,17 @@ export class HeroMover<T extends IHeroLocation>
|
|||||||
{
|
{
|
||||||
readonly state: IDataCommon;
|
readonly state: IDataCommon;
|
||||||
|
|
||||||
/** 本次移动是否不记录进路线系统 */
|
/** 是否不记录进路线系统 */
|
||||||
private noRoute: boolean = false;
|
private noRoute: boolean = false;
|
||||||
/** 本次移动是否忽略地形碰撞检测 */
|
/** 是否忽略地形碰撞检测 */
|
||||||
private ignoreTerrain: boolean = false;
|
private ignoreTerrain: boolean = false;
|
||||||
/** 本次移动是否在特定时机触发自动存档 */
|
/** 是否在特定时机触发自动存档 */
|
||||||
private autoSave: boolean = false;
|
private autoSave: boolean = false;
|
||||||
|
/** 是否允许到达地图外 */
|
||||||
|
private allowOutBound: boolean = false;
|
||||||
|
|
||||||
/** 地形通行判定器 */
|
/** 勇士移动顶层实现对象 */
|
||||||
private terrainChecker: ITerrainPassChecker | null = null;
|
private topImpl: IHeroMoveTopImpl | null = null;
|
||||||
/** 撞击行为对象 */
|
|
||||||
private hitAction: IHeroHitAction | null = null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly tile: T,
|
readonly tile: T,
|
||||||
@ -46,7 +44,7 @@ export class HeroMover<T extends IHeroLocation>
|
|||||||
this.state = tile.state;
|
this.state = tile.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
config(config: Readonly<IHeroMoverConfig>): this {
|
config(config: Partial<IHeroMoverConfig>): this {
|
||||||
if (!isNil(config.noRoute)) {
|
if (!isNil(config.noRoute)) {
|
||||||
this.noRoute = config.noRoute;
|
this.noRoute = config.noRoute;
|
||||||
}
|
}
|
||||||
@ -56,6 +54,9 @@ export class HeroMover<T extends IHeroLocation>
|
|||||||
if (!isNil(config.autoSave)) {
|
if (!isNil(config.autoSave)) {
|
||||||
this.autoSave = config.autoSave;
|
this.autoSave = config.autoSave;
|
||||||
}
|
}
|
||||||
|
if (!isNil(config.allowOutBound)) {
|
||||||
|
this.allowOutBound = config.allowOutBound;
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,95 +64,104 @@ export class HeroMover<T extends IHeroLocation>
|
|||||||
return {
|
return {
|
||||||
noRoute: this.noRoute,
|
noRoute: this.noRoute,
|
||||||
ignoreTerrain: this.ignoreTerrain,
|
ignoreTerrain: this.ignoreTerrain,
|
||||||
autoSave: this.autoSave
|
autoSave: this.autoSave,
|
||||||
|
allowOutBound: this.allowOutBound
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
useTerrainChecker(checker: ITerrainPassChecker | null): void {
|
useTopImplementation(impl: IHeroMoveTopImpl | null): void {
|
||||||
this.terrainChecker = checker;
|
this.topImpl = impl;
|
||||||
}
|
|
||||||
|
|
||||||
useHitAction(action: IHeroHitAction | null): void {
|
|
||||||
this.hitAction = action;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建通行性检查信息对象
|
* 创建顶层信息对象
|
||||||
* @param curr 勇士所在位置
|
* @param curr 勇士所在位置
|
||||||
* @param dir 勇士移动方向
|
* @param dir 勇士移动方向
|
||||||
* @param floorId 当前楼层 id
|
* @param floorId 当前楼层 id
|
||||||
*/
|
*/
|
||||||
private createPassHandler(
|
private createHandler(
|
||||||
curr: ITileLocator,
|
curr: ITileLocator,
|
||||||
dir: FaceDirection,
|
step: Readonly<ObjectMoveStep>,
|
||||||
floorId: string | undefined
|
floorId: string | undefined
|
||||||
): IPassCheckerHandler {
|
): IHeroMoveTopHandler {
|
||||||
const { x, y } = this.faceHandler.movement(dir);
|
switch (step.type) {
|
||||||
const nx = curr.x + x;
|
case ObjectMoveType.Dir:
|
||||||
const ny = curr.y + y;
|
case ObjectMoveType.DirFace:
|
||||||
return {
|
case ObjectMoveType.Special: {
|
||||||
currLoc: curr,
|
const { x, y } = this.faceHandler.movement(this.moveDirection);
|
||||||
nextLoc: { x: nx, y: ny },
|
const nx = curr.x + x;
|
||||||
direction: dir,
|
const ny = curr.y + y;
|
||||||
floorId,
|
return {
|
||||||
face: this.faceHandler,
|
currLoc: { x: curr.x, y: curr.y },
|
||||||
state: this.state
|
nextLoc: { x: nx, y: ny },
|
||||||
};
|
direction: this.moveDirection,
|
||||||
|
floorId,
|
||||||
|
face: this.faceHandler,
|
||||||
|
state: this.state
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case ObjectMoveType.Jump:
|
||||||
|
case ObjectMoveType.Teleport: {
|
||||||
|
const nx = step.rel ? curr.x + step.x : step.x;
|
||||||
|
const ny = step.rel ? curr.y + step.y : step.y;
|
||||||
|
return {
|
||||||
|
currLoc: { x: curr.x, y: curr.y },
|
||||||
|
nextLoc: { x: nx, y: ny },
|
||||||
|
direction: this.moveDirection,
|
||||||
|
floorId,
|
||||||
|
face: this.faceHandler,
|
||||||
|
state: this.state
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case ObjectMoveType.AnimDir:
|
||||||
|
case ObjectMoveType.Face:
|
||||||
|
case ObjectMoveType.Speed: {
|
||||||
|
return {
|
||||||
|
currLoc: { x: curr.x, y: curr.y },
|
||||||
|
nextLoc: { x: curr.x, y: curr.y },
|
||||||
|
direction: this.moveDirection,
|
||||||
|
floorId,
|
||||||
|
face: this.faceHandler,
|
||||||
|
state: this.state
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected async onMoveStart(): Promise<void> {
|
||||||
* 创建通行性检查信息对象
|
if (!this.topImpl) {
|
||||||
* @param curr 勇士所在位置
|
logger.warn(144);
|
||||||
* @param dir 勇士移动方向
|
}
|
||||||
* @param floorId 当前楼层 id
|
|
||||||
*/
|
|
||||||
private createHitHandler(
|
|
||||||
curr: ITileLocator,
|
|
||||||
dir: FaceDirection,
|
|
||||||
floorId: string | undefined
|
|
||||||
): IHeroHitHandler {
|
|
||||||
const { x, y } = this.faceHandler.movement(dir);
|
|
||||||
const nx = curr.x + x;
|
|
||||||
const ny = curr.y + y;
|
|
||||||
return {
|
|
||||||
currLoc: curr,
|
|
||||||
nextLoc: { x: nx, y: ny },
|
|
||||||
direction: dir,
|
|
||||||
floorId,
|
|
||||||
state: this.state
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onMoveStart(): Promise<void> {}
|
|
||||||
|
|
||||||
protected async onMoveEnd(): Promise<void> {}
|
protected async onMoveEnd(): Promise<void> {}
|
||||||
|
|
||||||
protected async onStepStart(
|
protected async onStepStart(
|
||||||
step: Readonly<ObjectMoveStep>,
|
step: Readonly<ObjectMoveStep>,
|
||||||
tile: IHeroLocation
|
tile: IHeroLocation
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
if (!this.terrainChecker) return HeroMoveCode.CannotMove;
|
if (!this.topImpl) return HeroMoveCode.Stop;
|
||||||
|
|
||||||
const type = step.type;
|
const type = step.type;
|
||||||
|
const handler = this.createHandler(tile, step, tile.floorId);
|
||||||
|
|
||||||
|
// 边界判断
|
||||||
|
const { x: nx, y: ny } = handler.nextLoc;
|
||||||
|
const inBound = this.topImpl.inBound(nx, ny, handler.floorId);
|
||||||
|
if (!this.allowOutBound && !inBound) return HeroMoveCode.CannotMove;
|
||||||
|
|
||||||
|
// 移动步判断
|
||||||
if (
|
if (
|
||||||
type === ObjectMoveType.Dir ||
|
type === ObjectMoveType.Dir ||
|
||||||
type === ObjectMoveType.DirFace ||
|
type === ObjectMoveType.DirFace ||
|
||||||
type === ObjectMoveType.Special
|
type === ObjectMoveType.Special
|
||||||
) {
|
) {
|
||||||
const dir = this.moveDirection;
|
|
||||||
const handler = this.createPassHandler(tile, dir, tile.floorId);
|
|
||||||
const { x: nx, y: ny } = handler.nextLoc;
|
|
||||||
const inBound = this.terrainChecker.inBound(nx, ny, tile.floorId);
|
|
||||||
// 目标点不在地图内
|
|
||||||
if (!inBound) return HeroMoveCode.CannotMove;
|
|
||||||
|
|
||||||
if (this.ignoreTerrain) {
|
if (this.ignoreTerrain) {
|
||||||
return HeroMoveCode.Step;
|
return HeroMoveCode.Step;
|
||||||
} else {
|
} else {
|
||||||
const canPass = this.terrainChecker.canPass(handler);
|
const canPass = this.topImpl.canPass(handler);
|
||||||
if (canPass) {
|
if (canPass) {
|
||||||
const hit = this.terrainChecker.shouldHit(handler);
|
const hit = this.topImpl.shouldHit(handler);
|
||||||
if (hit) return HeroMoveCode.Hit;
|
if (hit) return HeroMoveCode.Hit;
|
||||||
else return HeroMoveCode.Step;
|
else return HeroMoveCode.Step;
|
||||||
} else {
|
} else {
|
||||||
@ -160,14 +170,12 @@ export class HeroMover<T extends IHeroLocation>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳跃和瞬移仅需要进行边界判断
|
// 跳跃和瞬移
|
||||||
if (type === ObjectMoveType.Jump || type === ObjectMoveType.Teleport) {
|
if (type === ObjectMoveType.Jump || type === ObjectMoveType.Teleport) {
|
||||||
const nx = step.rel ? tile.x + step.x : step.x;
|
|
||||||
const ny = step.rel ? tile.y + step.y : step.y;
|
|
||||||
if (this.ignoreTerrain) {
|
if (this.ignoreTerrain) {
|
||||||
return HeroMoveCode.Step;
|
return HeroMoveCode.Step;
|
||||||
} else {
|
} else {
|
||||||
if (this.terrainChecker.inBound(nx, ny, tile.floorId)) {
|
if (inBound) {
|
||||||
return HeroMoveCode.Step;
|
return HeroMoveCode.Step;
|
||||||
} else {
|
} else {
|
||||||
return HeroMoveCode.Stop;
|
return HeroMoveCode.Stop;
|
||||||
@ -175,6 +183,7 @@ export class HeroMover<T extends IHeroLocation>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 其他的一律可以直接执行
|
||||||
return HeroMoveCode.Step;
|
return HeroMoveCode.Step;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +193,9 @@ export class HeroMover<T extends IHeroLocation>
|
|||||||
tile: IHeroLocation,
|
tile: IHeroLocation,
|
||||||
controller: Readonly<IMoverController>
|
controller: Readonly<IMoverController>
|
||||||
): Promise<ITileLocator> {
|
): Promise<ITileLocator> {
|
||||||
const type = step.type;
|
if (!this.topImpl) return { x: tile.x, y: tile.y };
|
||||||
|
|
||||||
|
const handler = this.createHandler(tile, step, tile.floorId);
|
||||||
|
|
||||||
// 不能移动或停止
|
// 不能移动或停止
|
||||||
if (code === HeroMoveCode.CannotMove || code === HeroMoveCode.Stop) {
|
if (code === HeroMoveCode.CannotMove || code === HeroMoveCode.Stop) {
|
||||||
@ -195,12 +206,7 @@ export class HeroMover<T extends IHeroLocation>
|
|||||||
|
|
||||||
// 撞击时也使用当前位置,同时处理撞击行为
|
// 撞击时也使用当前位置,同时处理撞击行为
|
||||||
if (code === HeroMoveCode.Hit) {
|
if (code === HeroMoveCode.Hit) {
|
||||||
// 执行撞击行为
|
await this.topImpl.hit(handler);
|
||||||
if (this.hitAction) {
|
|
||||||
const dir = this.moveDirection;
|
|
||||||
const handler = this.createHitHandler(tile, dir, tile.floorId);
|
|
||||||
await this.hitAction.hit(handler);
|
|
||||||
}
|
|
||||||
// 这里同样不能 await
|
// 这里同样不能 await
|
||||||
controller.stop();
|
controller.stop();
|
||||||
return { x: tile.x, y: tile.y };
|
return { x: tile.x, y: tile.y };
|
||||||
@ -208,30 +214,7 @@ export class HeroMover<T extends IHeroLocation>
|
|||||||
|
|
||||||
// 正常移动
|
// 正常移动
|
||||||
if (code === HeroMoveCode.Step) {
|
if (code === HeroMoveCode.Step) {
|
||||||
if (
|
return handler.nextLoc;
|
||||||
type === ObjectMoveType.Teleport ||
|
|
||||||
type === ObjectMoveType.Jump
|
|
||||||
) {
|
|
||||||
// 瞬移行为
|
|
||||||
if (step.rel) {
|
|
||||||
return { x: tile.x + step.x, y: tile.y + step.y };
|
|
||||||
} else {
|
|
||||||
return { x: step.x, y: step.y };
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
type === ObjectMoveType.Dir ||
|
|
||||||
type === ObjectMoveType.DirFace ||
|
|
||||||
type === ObjectMoveType.Special
|
|
||||||
) {
|
|
||||||
// 移动行为
|
|
||||||
const { x, y } = this.faceHandler.movement(this.moveDirection);
|
|
||||||
const nx = tile.x + x;
|
|
||||||
const ny = tile.y + y;
|
|
||||||
return { x: nx, y: ny };
|
|
||||||
} else {
|
|
||||||
// 其他行为
|
|
||||||
return { x: tile.x, y: tile.y };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { x: tile.x, y: tile.y };
|
return { x: tile.x, y: tile.y };
|
||||||
|
|||||||
@ -230,7 +230,7 @@ export const enum HeroMoveCode {
|
|||||||
CannotMove
|
CannotMove
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHeroMoveHandlerBase extends IDataCommonExtended {
|
export interface IHeroMoveTopHandler extends IDataCommonExtended {
|
||||||
/** 当前位置 */
|
/** 当前位置 */
|
||||||
readonly currLoc: ITileLocator;
|
readonly currLoc: ITileLocator;
|
||||||
/** 要移动至的位置 */
|
/** 要移动至的位置 */
|
||||||
@ -239,16 +239,11 @@ export interface IHeroMoveHandlerBase extends IDataCommonExtended {
|
|||||||
readonly direction: FaceDirection;
|
readonly direction: FaceDirection;
|
||||||
/** 当前楼层 id */
|
/** 当前楼层 id */
|
||||||
readonly floorId: string | undefined;
|
readonly floorId: string | undefined;
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPassCheckerHandler extends IHeroMoveHandlerBase {
|
|
||||||
/** 朝向管理对象 */
|
/** 朝向管理对象 */
|
||||||
readonly face: IFaceHandler<FaceDirection>;
|
readonly face: IFaceHandler<FaceDirection>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHeroHitHandler extends IHeroMoveHandlerBase {}
|
export interface IHeroMoveTopImpl {
|
||||||
|
|
||||||
export interface ITerrainPassChecker {
|
|
||||||
/**
|
/**
|
||||||
* 检查目标位置是否在地图范围内
|
* 检查目标位置是否在地图范围内
|
||||||
* @param x 横坐标
|
* @param x 横坐标
|
||||||
@ -261,30 +256,30 @@ export interface ITerrainPassChecker {
|
|||||||
* 判断在指定楼层中,从指定坐标向指定方向移动一格是否可通行
|
* 判断在指定楼层中,从指定坐标向指定方向移动一格是否可通行
|
||||||
* @param handler 通行性检查对象
|
* @param handler 通行性检查对象
|
||||||
*/
|
*/
|
||||||
canPass(handler: IPassCheckerHandler): boolean;
|
canPass(handler: IHeroMoveTopHandler): boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断在指定楼层中,从指定坐标向指定方向移动时是否应该产生撞击,撞击将会触发目标位置的触发器
|
* 判断在指定楼层中,从指定坐标向指定方向移动时是否应该产生撞击,撞击将会触发目标位置的触发器
|
||||||
* @param handler 通行性检查对象
|
* @param handler 通行性检查对象
|
||||||
*/
|
*/
|
||||||
shouldHit(handler: IPassCheckerHandler): boolean;
|
shouldHit(handler: IHeroMoveTopHandler): boolean;
|
||||||
}
|
|
||||||
|
|
||||||
export interface IHeroHitAction {
|
|
||||||
/**
|
/**
|
||||||
* 勇士撞击某一个图块时执行的内容,一般用于触发目标位置的触发器
|
* 勇士撞击某一个图块时执行的内容,一般用于触发目标位置的触发器
|
||||||
* @param handler 撞击行为对象
|
* @param handler 撞击行为对象
|
||||||
*/
|
*/
|
||||||
hit(handler: IHeroHitHandler): Promise<void>;
|
hit(handler: IHeroMoveTopHandler): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHeroMoverConfig {
|
export interface IHeroMoverConfig {
|
||||||
/** 本次移动是否不记录进路线系统 */
|
/** 是否不记录进路线系统 */
|
||||||
noRoute?: boolean;
|
noRoute: boolean;
|
||||||
/** 本次移动是否忽略地形碰撞检测 */
|
/** 是否忽略地形碰撞检测 */
|
||||||
ignoreTerrain?: boolean;
|
ignoreTerrain: boolean;
|
||||||
/** 本次移动是否在特定时机触发自动存档 */
|
/** 是否在特定时机触发自动存档 */
|
||||||
autoSave?: boolean;
|
autoSave: boolean;
|
||||||
|
/** 是否允许到达地图外 */
|
||||||
|
allowOutBound: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHeroMover<T extends IHeroLocation>
|
export interface IHeroMover<T extends IHeroLocation>
|
||||||
@ -293,7 +288,7 @@ export interface IHeroMover<T extends IHeroLocation>
|
|||||||
* 配置本次移动的行为模式
|
* 配置本次移动的行为模式
|
||||||
* @param config 配置对象,未传入的字段保持当前值
|
* @param config 配置对象,未传入的字段保持当前值
|
||||||
*/
|
*/
|
||||||
config(config: Readonly<IHeroMoverConfig>): this;
|
config(config: Partial<IHeroMoverConfig>): this;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前移动配置的只读快照
|
* 获取当前移动配置的只读快照
|
||||||
@ -301,16 +296,10 @@ export interface IHeroMover<T extends IHeroLocation>
|
|||||||
getConfig(): Readonly<IHeroMoverConfig>;
|
getConfig(): Readonly<IHeroMoverConfig>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置地形通行判定器,传入 `null` 移表示移除
|
* 设置勇士移动的顶层实现对象,主要用于进行各种判定与勇士行为
|
||||||
* @param checker 地形判定器
|
* @param impl 顶层实现对象
|
||||||
*/
|
*/
|
||||||
useTerrainChecker(checker: ITerrainPassChecker | null): void;
|
useTopImplementation(impl: IHeroMoveTopImpl | null): void;
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置勇士撞击行为的执行对象,传入 `null` 表示移除。
|
|
||||||
* @param action 撞击行为对象
|
|
||||||
*/
|
|
||||||
useHitAction(action: IHeroHitAction | null): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|||||||
@ -66,8 +66,7 @@ import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader';
|
|||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
import { ISaveSystem, SaveSystem } from './save';
|
import { ISaveSystem, SaveSystem } from './save';
|
||||||
import { DefaultPassChecker } from './hero';
|
import { DefaultHeroMoveTopImpl } from './hero';
|
||||||
import { DefaultHitAction } from './hero/hitAction';
|
|
||||||
|
|
||||||
export class CoreState implements ICoreState {
|
export class CoreState implements ICoreState {
|
||||||
// Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据
|
// Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据
|
||||||
@ -211,14 +210,8 @@ export class CoreState implements ICoreState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 勇士顶层初始化
|
// 勇士顶层初始化
|
||||||
const passChecker = new DefaultPassChecker(this.maps);
|
const heroMoveTopImpl = new DefaultHeroMoveTopImpl(this);
|
||||||
const hitAction = new DefaultHitAction(
|
this.hero.location.mover.useTopImplementation(heroMoveTopImpl);
|
||||||
this.maps,
|
|
||||||
this.triggerCollector,
|
|
||||||
this
|
|
||||||
);
|
|
||||||
this.hero.location.mover.useTerrainChecker(passChecker);
|
|
||||||
this.hero.location.mover.useHitAction(hitAction);
|
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
import {
|
|
||||||
IHeroHitAction,
|
|
||||||
IHeroHitHandler,
|
|
||||||
IMapStore,
|
|
||||||
IStateBase
|
|
||||||
} from '@user/data-base';
|
|
||||||
import { ITriggerCollector, ITriggerHandler } from '@user/data-system';
|
|
||||||
import { isNil } from 'lodash-es';
|
|
||||||
|
|
||||||
export class DefaultHitAction implements IHeroHitAction {
|
|
||||||
constructor(
|
|
||||||
readonly maps: IMapStore,
|
|
||||||
readonly collector: ITriggerCollector,
|
|
||||||
readonly state: IStateBase
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async hit(handler: IHeroHitHandler): Promise<void> {
|
|
||||||
if (isNil(handler.floorId)) return;
|
|
||||||
|
|
||||||
const map = this.maps.getLayerState(handler.floorId);
|
|
||||||
if (!map) return;
|
|
||||||
const event = map.eventLayer;
|
|
||||||
if (!event) return;
|
|
||||||
|
|
||||||
const { x, y } = handler.nextLoc;
|
|
||||||
const triggers = this.collector.collect(x, y, event);
|
|
||||||
|
|
||||||
const triggerHandler: ITriggerHandler = {
|
|
||||||
state: this.state,
|
|
||||||
layer: map,
|
|
||||||
mapLayer: event,
|
|
||||||
locator: handler.nextLoc
|
|
||||||
};
|
|
||||||
|
|
||||||
return triggers.trigger(triggerHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +1,2 @@
|
|||||||
export * from './passChecker';
|
export * from './moverImpl';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|||||||
@ -1,13 +1,28 @@
|
|||||||
|
import {
|
||||||
|
IHeroMoveTopHandler,
|
||||||
|
IHeroMoveTopImpl,
|
||||||
|
IMapStore
|
||||||
|
} from '@user/data-base';
|
||||||
import { FaceDirection, PassBit } from '@user/data-common';
|
import { FaceDirection, PassBit } from '@user/data-common';
|
||||||
import {
|
import {
|
||||||
IMapStore,
|
IStateSystem,
|
||||||
IPassCheckerHandler,
|
ITriggerCollector,
|
||||||
ITerrainPassChecker
|
ITriggerHandler
|
||||||
} from '@user/data-base';
|
} from '@user/data-system';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
|
|
||||||
export class DefaultPassChecker implements ITerrainPassChecker {
|
export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl {
|
||||||
constructor(readonly maps: IMapStore) {}
|
/** 地图存储对象 */
|
||||||
|
private readonly maps: IMapStore;
|
||||||
|
/** 触发器收集器对象 */
|
||||||
|
private readonly collector: ITriggerCollector;
|
||||||
|
|
||||||
|
constructor(private readonly state: IStateSystem) {
|
||||||
|
this.maps = state.maps;
|
||||||
|
this.collector = state.triggerCollector;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region 通行性判断
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将朝向转换为对应的通行性位掩码。
|
* 将朝向转换为对应的通行性位掩码。
|
||||||
@ -36,7 +51,7 @@ export class DefaultPassChecker implements ITerrainPassChecker {
|
|||||||
return x >= 0 && y >= 0 && x < width && y < height;
|
return x >= 0 && y >= 0 && x < width && y < height;
|
||||||
}
|
}
|
||||||
|
|
||||||
canPass(handler: IPassCheckerHandler): boolean {
|
canPass(handler: IHeroMoveTopHandler): boolean {
|
||||||
const { currLoc, nextLoc, direction, floorId, face } = handler;
|
const { currLoc, nextLoc, direction, floorId, face } = handler;
|
||||||
if (isNil(floorId)) return false;
|
if (isNil(floorId)) return false;
|
||||||
|
|
||||||
@ -96,7 +111,7 @@ export class DefaultPassChecker implements ITerrainPassChecker {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldHit(handler: IPassCheckerHandler): boolean {
|
shouldHit(handler: IHeroMoveTopHandler): boolean {
|
||||||
const { nextLoc, floorId } = handler;
|
const { nextLoc, floorId } = handler;
|
||||||
if (isNil(floorId)) return false;
|
if (isNil(floorId)) return false;
|
||||||
const layerState = this.maps.getLayerState(floorId);
|
const layerState = this.maps.getLayerState(floorId);
|
||||||
@ -110,4 +125,31 @@ export class DefaultPassChecker implements ITerrainPassChecker {
|
|||||||
if (!next || !next.raw) return false;
|
if (!next || !next.raw) return false;
|
||||||
return !next.raw.eventPass;
|
return !next.raw.eventPass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 触发器行为
|
||||||
|
|
||||||
|
async hit(handler: IHeroMoveTopHandler): Promise<void> {
|
||||||
|
if (isNil(handler.floorId)) return;
|
||||||
|
|
||||||
|
const map = this.maps.getLayerState(handler.floorId);
|
||||||
|
if (!map) return;
|
||||||
|
const event = map.eventLayer;
|
||||||
|
if (!event) return;
|
||||||
|
|
||||||
|
const { x, y } = handler.nextLoc;
|
||||||
|
const triggers = this.collector.collect(x, y, event);
|
||||||
|
|
||||||
|
const triggerHandler: ITriggerHandler = {
|
||||||
|
state: this.state,
|
||||||
|
layer: map,
|
||||||
|
mapLayer: event,
|
||||||
|
locator: handler.nextLoc
|
||||||
|
};
|
||||||
|
|
||||||
|
return triggers.trigger(triggerHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
}
|
}
|
||||||
@ -200,6 +200,7 @@
|
|||||||
"140": "Different priority of combat script is expected, but got a same one.",
|
"140": "Different priority of combat script is expected, but got a same one.",
|
||||||
"141": "Some issue may occur in binded damage system on combat flow, please check the console.",
|
"141": "Some issue may occur in binded damage system on combat flow, please check the console.",
|
||||||
"142": "Expected a specific tile number after convert id '$1' to number, but got null.",
|
"142": "Expected a specific tile number after convert id '$1' to number, but got null.",
|
||||||
"143": "Cannot create dynamic tile for $1 since its raw data is not found."
|
"143": "Cannot create dynamic tile for $1 since its raw data is not found.",
|
||||||
|
"144": "A hero move top implementation object binding is required for hero moving."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
prompt.md
43
prompt.md
@ -46,15 +46,17 @@
|
|||||||
1. 阅读当前代码,分析需求,将需求整理为一个 markdown 文档,放在 `docs/dev` 目录下,注意有可能在其子文件夹下,例如 `docs/dev/hero`,自行判断应该放到哪。文档中需明确标注需求细节,以及代码实现的大体思路。此阶段需考虑全面,遇到任何问题应向我提问并确认,不得自行假设。
|
1. 阅读当前代码,分析需求,将需求整理为一个 markdown 文档,放在 `docs/dev` 目录下,注意有可能在其子文件夹下,例如 `docs/dev/hero`,自行判断应该放到哪。文档中需明确标注需求细节,以及代码实现的大体思路。此阶段需考虑全面,遇到任何问题应向我提问并确认,不得自行假设。
|
||||||
2. 我会对文档进行全面阅读,确认实现细节与思路无误后,方允许开始实现。我可能会对文档进行细微调整,请在实现前重新仔细阅读最终版本。实现过程中如有任何问题,应向我提问,而不是自行决定。
|
2. 我会对文档进行全面阅读,确认实现细节与思路无误后,方允许开始实现。我可能会对文档进行细微调整,请在实现前重新仔细阅读最终版本。实现过程中如有任何问题,应向我提问,而不是自行决定。
|
||||||
|
|
||||||
## 示例文档
|
## 文档结构
|
||||||
|
|
||||||
对于新增接口/彻底性地重构接口,按照以下格式编写,其余需求自行组织。
|
一般情况下按照以下格式编写,其余需求自行组织。
|
||||||
|
|
||||||
- 如有需要可单独开设标题章节详述。
|
- 如有需要可单独开设标题章节详述。
|
||||||
- 我会使用 `>` 引用块在文档中批注,因此**不要在文档中使用引用块**。
|
- 我会使用 `>` 引用块在文档中批注,因此**不要在文档中使用引用块**。
|
||||||
- 文档控制在 100-250 行,简洁但包含所有必要信息,不擅自修改示例文档格式。
|
- 文档控制在 100-250 行,简洁但包含所有必要信息,不擅自修改示例文档格式。
|
||||||
- 一般不需要流程图;若必须画,使用 `mermaid`。
|
- 一般不需要流程图;若必须画,使用 `mermaid`。
|
||||||
|
|
||||||
|
以下为文档结构,示例文档参考 `docs/dev/template.md`
|
||||||
|
|
||||||
```md
|
```md
|
||||||
# 需求综述
|
# 需求综述
|
||||||
|
|
||||||
@ -62,48 +64,31 @@
|
|||||||
|
|
||||||
# 接口设计分析
|
# 接口设计分析
|
||||||
|
|
||||||
这是相当重要的一章。按接口逐一分析成员与方法的预期使用频率(高/中/低,指**用户编写此调用的频率**而非运行时频率),中高频成员需列出至少一个典型使用场景。
|
|
||||||
|
|
||||||
**命名长度原则**:高频 -> 一个单词;中频 -> 不超过三个单词;低频 -> 可稍长但不宜过长。
|
|
||||||
|
|
||||||
我可能不会给完整接口设计,你需要自行分析需求补充设计,并在对话中明确指出哪些接口是你补充的。
|
我可能不会给完整接口设计,你需要自行分析需求补充设计,并在对话中明确指出哪些接口是你补充的。
|
||||||
|
|
||||||
示例如下:
|
|
||||||
|
|
||||||
## IObjectMover
|
## IObjectMover
|
||||||
|
|
||||||
### 接口综述
|
### 设计意图
|
||||||
|
|
||||||
这里讲解接口的设计,注意不要直接写成 ts 代码,也不要简单地一个个列出,而是需要分析需求,然后经过推理自然地得出接口设计。
|
分析需求,然后在这里编写接口的设计意图。
|
||||||
|
|
||||||
### 接口分析
|
### 接口分析
|
||||||
|
|
||||||
- `IObjectMover.forward()`:预期频率**高频**。向前移动一格是地图行走、动画演出等场景的核心需求,在逻辑与演出中都会频繁出现,故为高频。典型使用场景:演出中玩家或 NPC 沿某方向连续移动。
|
按接口逐一分析成员与方法的预期使用频率(高/中/低,指**用户编写此调用的频率**而非运行时频率),中高频成员需列出至少一个典型使用场景。接口使用频率越高,其名称长度最好越短。
|
||||||
- `IObjectMover.speed()`:预期频率**中频**。移动中修改移速有一定使用场景,但远不及 `forward`、`step` 等移动接口的频率,通常只在特殊演出或逻辑中出现,故为中频。典型使用场景:NPC 逃离怪物时先定在原地,随后逐渐加速逃跑。
|
|
||||||
- `IObjectMover.stepFace()`:预期频率**低频**。移动方向与朝向不同的常见场景(后退)已由 `backward` 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。
|
|
||||||
|
|
||||||
### 预期体量
|
### 预期体量
|
||||||
|
|
||||||
本节应当写出预期的代码体量,并分析原因。示例如下:
|
本节应当写出预期的代码体量,并分析原因。
|
||||||
|
|
||||||
预期代码体量为 200-300 行。分析如下:
|
|
||||||
|
|
||||||
- `IObjectMover` 首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多,整体预计在 100 行左右。
|
|
||||||
- `IObjectMover` 还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂,预计需要 100-200 行。
|
|
||||||
|
|
||||||
### 可能风险
|
|
||||||
|
|
||||||
若需修改已有接口(即本次新增之外的接口),在此写明修改风险(如类型污染、其他接口出错等)。只写修改已有接口的风险,不写实现风险(实现风险放到问题节)。无风险则跳过此章节。
|
|
||||||
|
|
||||||
# 实现思路
|
# 实现思路
|
||||||
|
|
||||||
按照下面的格式分条描述实现思路。
|
对于复杂逻辑,按照下面的格式分条描述实现思路。
|
||||||
|
|
||||||
## 1. 完成 xxx
|
## 复杂需求 1
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
## 2. 完成 xxx
|
## 复杂需求 2
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -111,11 +96,7 @@
|
|||||||
|
|
||||||
## 需要引用的文件
|
## 需要引用的文件
|
||||||
|
|
||||||
按照第三方库 → 其他包 → 当前包的其他文件的顺序写。
|
按照第三方库 -> @motajs 包 → @user 包 → 当前包的其他文件的顺序写。不要写成接口列举,而是要写成需要什么类型的接口,示例如下。
|
||||||
|
|
||||||
- `xxx 库`: 引用第三方库,说明引用目的,以及需要的接口
|
|
||||||
- `@user/xxx`: 引用的目的,需要这个文件的哪些接口
|
|
||||||
- `xxx.ts`: 引用此文件的目的,需要这个文件的哪些接口
|
|
||||||
|
|
||||||
## 需要修改的文件
|
## 需要修改的文件
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user