Compare commits

...

2 Commits

Author SHA1 Message Date
1ce0283743 feat: 勇士默认撞击行为 2026-06-29 21:44:22 +08:00
c4bcecdf8a feat: 勇士撞击行为 2026-06-29 19:57:46 +08:00
14 changed files with 429 additions and 258 deletions

View File

@ -1,18 +1,21 @@
import { ITileLocator } from '@motajs/common'; import { ITileLocator } from '@motajs/common';
import { import {
FaceDirection, FaceDirection,
getFaceMovement,
IFaceHandler, IFaceHandler,
IMoverController, IMoverController,
ObjectMoveStep, ObjectMoveStep,
ObjectMover, ObjectMover,
ObjectMoveStepType ObjectMoveType,
IDataCommon
} from '@user/data-common'; } from '@user/data-common';
import { import {
HeroMoveCode, HeroMoveCode,
IHeroHitAction,
IHeroHitHandler,
IHeroLocation, IHeroLocation,
IHeroMover, IHeroMover,
IHeroMoverConfig, IHeroMoverConfig,
IPassCheckerHandler,
ITerrainPassChecker ITerrainPassChecker
} from './types'; } from './types';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
@ -21,20 +24,26 @@ export class HeroMover<T extends IHeroLocation>
extends ObjectMover<T> extends ObjectMover<T>
implements IHeroMover<T> implements IHeroMover<T>
{ {
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 terrainChecker: ITerrainPassChecker | null = null; private terrainChecker: ITerrainPassChecker | null = null;
/** 撞击行为对象 */
private hitAction: IHeroHitAction | null = null;
constructor( constructor(
readonly tile: T, readonly tile: T,
faceHandler: IFaceHandler<FaceDirection> faceHandler: IFaceHandler<FaceDirection>
) { ) {
super(faceHandler); super(faceHandler);
this.state = tile.state;
} }
config(config: Readonly<IHeroMoverConfig>): this { config(config: Readonly<IHeroMoverConfig>): this {
@ -62,38 +71,108 @@ export class HeroMover<T extends IHeroLocation>
this.terrainChecker = checker; this.terrainChecker = checker;
} }
protected async onMoveStart( useHitAction(action: IHeroHitAction | null): void {
_tile: IHeroLocation, this.hitAction = action;
_controller: Readonly<IMoverController> }
): Promise<void> {}
protected async onMoveEnd( /**
_tile: IHeroLocation, *
_controller: Readonly<IMoverController> * @param curr
): Promise<void> {} * @param dir
* @param floorId id
*/
private createPassHandler(
curr: ITileLocator,
dir: FaceDirection,
floorId: string | undefined
): IPassCheckerHandler {
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,
face: this.faceHandler,
state: this.state
};
}
/**
*
* @param curr
* @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 onStepStart( protected async onStepStart(
step: ObjectMoveStep, step: Readonly<ObjectMoveStep>,
tile: IHeroLocation, tile: IHeroLocation
controller: Readonly<IMoverController>
): Promise<number> { ): Promise<number> {
if (!this.terrainChecker) return HeroMoveCode.CannotMove;
const type = step.type; const type = step.type;
if ( if (
type !== ObjectMoveStepType.Dir && type === ObjectMoveType.Dir ||
type !== ObjectMoveStepType.DirFace && type === ObjectMoveType.DirFace ||
type !== ObjectMoveStepType.Special type === ObjectMoveType.Special
) { ) {
return HeroMoveCode.Step; 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) {
return HeroMoveCode.Step;
} else {
const canPass = this.terrainChecker.canPass(handler);
if (canPass) {
const hit = this.terrainChecker.shouldHit(handler);
if (hit) return HeroMoveCode.Hit;
else return HeroMoveCode.Step;
} else {
return HeroMoveCode.CannotMove;
}
}
} }
if (!this.ignoreTerrain) { // 跳跃和瞬移仅需要进行边界判断
const result = this.checkTerrain(tile, controller); if (type === ObjectMoveType.Jump || type === ObjectMoveType.Teleport) {
if (result !== HeroMoveCode.Step) return result; 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.autoSave) { return HeroMoveCode.Step;
// TODO: 检查当前步是否需要触发自动存档(如进入/离开地图伤害区域) } else {
if (this.terrainChecker.inBound(nx, ny, tile.floorId)) {
return HeroMoveCode.Step;
} else {
return HeroMoveCode.Stop;
}
}
} }
return HeroMoveCode.Step; return HeroMoveCode.Step;
@ -101,89 +180,60 @@ export class HeroMover<T extends IHeroLocation>
protected async onStepEnd( protected async onStepEnd(
code: number, code: number,
step: ObjectMoveStep, step: Readonly<ObjectMoveStep>,
tile: IHeroLocation, tile: IHeroLocation,
controller: Readonly<IMoverController> controller: Readonly<IMoverController>
): Promise<ITileLocator> { ): Promise<ITileLocator> {
const type = step.type; const type = step.type;
const isMovementStep = // 不能移动或停止
type === ObjectMoveStepType.Dir || if (code === HeroMoveCode.CannotMove || code === HeroMoveCode.Stop) {
type === ObjectMoveStepType.DirFace || // 这里不能 await因为其 Promise 会在当前步结束后兑现,如果 await 就会卡住
type === ObjectMoveStepType.Special; controller.stop();
if (
!isMovementStep &&
type !== ObjectMoveStepType.Teleport &&
type !== ObjectMoveStepType.Jump
) {
return { x: tile.x, y: tile.y }; return { x: tile.x, y: tile.y };
} }
if (code === HeroMoveCode.CannotMove || code === HeroMoveCode.Hit) { // 撞击时也使用当前位置,同时处理撞击行为
void controller.stop(); if (code === HeroMoveCode.Hit) {
// TODO: 处理撞击图块逻辑 // 执行撞击行为
if (this.hitAction) {
const dir = this.moveDirection;
const handler = this.createHitHandler(tile, dir, tile.floorId);
await this.hitAction.hit(handler);
}
// 这里同样不能 await
controller.stop();
return { x: tile.x, y: tile.y }; return { x: tile.x, y: tile.y };
} }
if (code === HeroMoveCode.Stop) { // 正常移动
void controller.stop(); if (code === HeroMoveCode.Step) {
// TODO: 清除自动寻路 if (
return { x: tile.x, y: tile.y }; 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 };
}
} }
if (type === ObjectMoveStepType.Teleport) { return { x: tile.x, y: tile.y };
return this.calcPos(tile, step.x, step.y, step.rel);
}
if (type === ObjectMoveStepType.Jump) {
return this.calcPos(tile, step.x, step.y, step.rel);
}
const offset = getFaceMovement(this.moveDirection);
const nx = tile.x + offset.x;
const ny = tile.y + offset.y;
// TODO: 路线记录
// TODO: 中毒伤害处理
// TODO: 步数累计
return { x: nx, y: ny };
}
/**
* /
* @param tile
* @param x
* @param y
* @param rel 使
*/
private calcPos(
tile: IHeroLocation,
x: number,
y: number,
rel: boolean
): ITileLocator {
if (rel) {
return { x: tile.x + x, y: tile.y + y };
}
return { x, y };
}
/**
*
*/
private checkTerrain(
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;
} }
} }

View File

@ -7,6 +7,7 @@ import {
import { import {
FaceDirection, FaceDirection,
IDataCommonExtended, IDataCommonExtended,
IFaceHandler,
IObjectMovable, IObjectMovable,
IObjectMover, IObjectMover,
ISaveableContent ISaveableContent
@ -211,7 +212,7 @@ export interface IHeroLocation
/** 当前所在楼层 idundefined 表示尚不处于任何楼层 */ /** 当前所在楼层 idundefined 表示尚不处于任何楼层 */
readonly floorId: string | undefined; readonly floorId: string | undefined;
/** 勇士的移动对象 */ /** 勇士的移动对象 */
readonly mover: IObjectMover<this>; readonly mover: IHeroMover<this>;
} }
//#endregion //#endregion
@ -229,21 +230,52 @@ export const enum HeroMoveCode {
CannotMove CannotMove
} }
export interface IHeroMoveHandlerBase extends IDataCommonExtended {
/** 当前位置 */
readonly currLoc: ITileLocator;
/** 要移动至的位置 */
readonly nextLoc: ITileLocator;
/** 移动方向 */
readonly direction: FaceDirection;
/** 当前楼层 id */
readonly floorId: string | undefined;
}
export interface IPassCheckerHandler extends IHeroMoveHandlerBase {
/** 朝向管理对象 */
readonly face: IFaceHandler<FaceDirection>;
}
export interface IHeroHitHandler extends IHeroMoveHandlerBase {}
export interface ITerrainPassChecker { export interface ITerrainPassChecker {
/** 对应的勇士状态对象 */ /**
readonly hero: IHeroState<unknown>; *
* @param x
* @param y
* @param floorId id
*/
inBound(x: number, y: number, floorId: string | undefined): boolean;
/** /**
* *
* @param locator * @param handler
* @param direction
* @param floorId id undefined
*/ */
canPass( canPass(handler: IPassCheckerHandler): boolean;
locator: ITileLocator,
direction: FaceDirection, /**
floorId: string | undefined *
): boolean; * @param handler
*/
shouldHit(handler: IPassCheckerHandler): boolean;
}
export interface IHeroHitAction {
/**
*
* @param handler
*/
hit(handler: IHeroHitHandler): Promise<void>;
} }
export interface IHeroMoverConfig { export interface IHeroMoverConfig {
@ -255,7 +287,8 @@ export interface IHeroMoverConfig {
autoSave?: boolean; autoSave?: boolean;
} }
export interface IHeroMover<T extends IHeroLocation> extends IObjectMover<T> { export interface IHeroMover<T extends IHeroLocation>
extends IObjectMover<T>, IDataCommonExtended {
/** /**
* *
* @param config * @param config
@ -268,10 +301,16 @@ export interface IHeroMover<T extends IHeroLocation> extends IObjectMover<T> {
getConfig(): Readonly<IHeroMoverConfig>; getConfig(): Readonly<IHeroMoverConfig>;
/** /**
* null * `null`
* @param checker null * @param checker
*/ */
useTerrainChecker(checker: ITerrainPassChecker | null): void; useTerrainChecker(checker: ITerrainPassChecker | null): void;
/**
* `null`
* @param action
*/
useHitAction(action: IHeroHitAction | null): void;
} }
//#endregion //#endregion

View File

@ -3,7 +3,7 @@ import {
getFaceMovement, getFaceMovement,
ObjectMover, ObjectMover,
ObjectMoveStep, ObjectMoveStep,
ObjectMoveStepType ObjectMoveType
} from '@user/data-common'; } from '@user/data-common';
import { IDynamicTile } from './types'; import { IDynamicTile } from './types';
import { DYNAMIC_MOVER_FACE } from '../shared'; import { DYNAMIC_MOVER_FACE } from '../shared';
@ -47,33 +47,33 @@ export class DynamicTileMover extends ObjectMover<IDynamicTile> {
y: tile.y y: tile.y
}; };
switch (step.type) { switch (step.type) {
case ObjectMoveStepType.Dir: { case ObjectMoveType.Dir: {
const { x, y } = getFaceMovement(step.move); const { x, y } = getFaceMovement(step.move);
tile.setFaceDirection(step.move); tile.setFaceDirection(step.move);
locator.x += x; locator.x += x;
locator.y += y; locator.y += y;
break; break;
} }
case ObjectMoveStepType.DirFace: { case ObjectMoveType.DirFace: {
const { x, y } = getFaceMovement(step.move); const { x, y } = getFaceMovement(step.move);
tile.setFaceDirection(step.face); tile.setFaceDirection(step.face);
locator.x += x; locator.x += x;
locator.y += y; locator.y += y;
break; break;
} }
case ObjectMoveStepType.Face: { case ObjectMoveType.Face: {
tile.setFaceDirection(step.value); tile.setFaceDirection(step.value);
break; break;
} }
case ObjectMoveStepType.Special: { case ObjectMoveType.Special: {
const { x, y } = getFaceMovement(this.moveDirection); const { x, y } = getFaceMovement(this.moveDirection);
tile.setFaceDirection(this.faceDirection); tile.setFaceDirection(this.faceDirection);
locator.x += x; locator.x += x;
locator.y += y; locator.y += y;
break; break;
} }
case ObjectMoveStepType.Teleport: case ObjectMoveType.Teleport:
case ObjectMoveStepType.Jump: { case ObjectMoveType.Jump: {
const { x, y, rel } = step; const { x, y, rel } = step;
tile.setFaceDirection(this.faceDirection); tile.setFaceDirection(this.faceDirection);
if (rel) { if (rel) {

View File

@ -40,7 +40,7 @@ export interface IStateBase extends IDataCommon {
getSaveableContent<T>(id: string): ISaveableContent<T> | null; getSaveableContent<T>(id: string): ISaveableContent<T> | null;
} }
export interface IStateBaseExtended { export interface IDataBaseExtended {
/** 当前对象对应的数据层对象Layer 1 对象) */ /** 当前对象对应的数据层对象Layer 1 对象) */
readonly state: IStateBase; readonly state: IStateBase;
} }

View File

@ -11,7 +11,7 @@ import { IFaceHandler } from './faceManager';
//#region 对象移动 //#region 对象移动
export const enum ObjectMoveStepType { export const enum ObjectMoveType {
/** 绝对方向步,同步更新移动方向与朝向 */ /** 绝对方向步,同步更新移动方向与朝向 */
Dir, Dir,
/** 绝对方向步,显式指定朝向 */ /** 绝对方向步,显式指定朝向 */
@ -65,14 +65,14 @@ export interface IObjectMovable {
export interface IObjectMoveStepDir { export interface IObjectMoveStepDir {
/** 步骤类型 */ /** 步骤类型 */
type: ObjectMoveStepType.Dir; type: ObjectMoveType.Dir;
/** 本步移动方向 */ /** 本步移动方向 */
move: FaceDirection; move: FaceDirection;
} }
export interface IObjectMoveStepDirFace { export interface IObjectMoveStepDirFace {
/** 步骤类型 */ /** 步骤类型 */
type: ObjectMoveStepType.DirFace; type: ObjectMoveType.DirFace;
/** 本步移动方向 */ /** 本步移动方向 */
move: FaceDirection; move: FaceDirection;
/** 本步显式朝向 */ /** 本步显式朝向 */
@ -81,35 +81,35 @@ export interface IObjectMoveStepDirFace {
export interface IObjectMoveStepSpeed { export interface IObjectMoveStepSpeed {
/** 步骤类型 */ /** 步骤类型 */
type: ObjectMoveStepType.Speed; type: ObjectMoveType.Speed;
/** 后续移动的每格耗时,单位为 ms */ /** 后续移动的每格耗时,单位为 ms */
value: number; value: number;
} }
export interface IObjectMoveStepFace { export interface IObjectMoveStepFace {
/** 步骤类型 */ /** 步骤类型 */
type: ObjectMoveStepType.Face; type: ObjectMoveType.Face;
/** 要设置的朝向 */ /** 要设置的朝向 */
value: FaceDirection; value: FaceDirection;
} }
export interface IObjectMoveStepSpecial { export interface IObjectMoveStepSpecial {
/** 步骤类型 */ /** 步骤类型 */
type: ObjectMoveStepType.Special; type: ObjectMoveType.Special;
/** 特殊步方向 */ /** 特殊步方向 */
direction: ObjectSpecialStep; direction: ObjectSpecialStep;
} }
export interface IObjectMoveAnimDir { export interface IObjectMoveAnimDir {
/** 步骤类型 */ /** 步骤类型 */
type: ObjectMoveStepType.AnimDir; type: ObjectMoveType.AnimDir;
/** 动画播放方向 */ /** 动画播放方向 */
dir: ObjectAnimDirection; dir: ObjectAnimDirection;
} }
export interface IObjectMoveTP { export interface IObjectMoveTP {
/** 步骤类型 */ /** 步骤类型 */
type: ObjectMoveStepType.Teleport; type: ObjectMoveType.Teleport;
/** 目标横坐标 */ /** 目标横坐标 */
x: number; x: number;
/** 目标纵坐标 */ /** 目标纵坐标 */
@ -120,7 +120,7 @@ export interface IObjectMoveTP {
export interface IObjectMoveJump { export interface IObjectMoveJump {
/** 步骤类型 */ /** 步骤类型 */
type: ObjectMoveStepType.Jump; type: ObjectMoveType.Jump;
/** 目标横坐标 */ /** 目标横坐标 */
x: number; x: number;
/** 目标纵坐标 */ /** 目标纵坐标 */
@ -149,13 +149,13 @@ export interface IMoverController {
* *
* @param steps * @param steps
*/ */
push(...steps: ObjectMoveStep[]): void; push(...steps: Readonly<ObjectMoveStep>[]): void;
/** /**
* *
* @param steps * @param steps
*/ */
insert(...steps: ObjectMoveStep[]): void; insert(...steps: Readonly<ObjectMoveStep>[]): void;
/** /**
* *
@ -187,7 +187,7 @@ export interface IObjectMoverHooks<T extends IObjectMovable> extends IHookBase {
*/ */
onStepStart?( onStepStart?(
code: number, code: number,
step: ObjectMoveStep, step: Readonly<ObjectMoveStep>,
tile: T, tile: T,
mover: IObjectMover<T> mover: IObjectMover<T>
): Promise<void>; ): Promise<void>;
@ -201,7 +201,7 @@ export interface IObjectMoverHooks<T extends IObjectMovable> extends IHookBase {
*/ */
onStepEnd?( onStepEnd?(
code: number, code: number,
step: ObjectMoveStep, step: Readonly<ObjectMoveStep>,
tile: T, tile: T,
mover: IObjectMover<T> mover: IObjectMover<T>
): Promise<void>; ): Promise<void>;
@ -409,7 +409,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
* @param controller * @param controller
*/ */
protected abstract onStepStart( protected abstract onStepStart(
step: ObjectMoveStep, step: Readonly<ObjectMoveStep>,
tile: T, tile: T,
controller: Readonly<IMoverController> controller: Readonly<IMoverController>
): Promise<number>; ): Promise<number>;
@ -423,7 +423,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
*/ */
protected abstract onStepEnd( protected abstract onStepEnd(
code: number, code: number,
step: ObjectMoveStep, step: Readonly<ObjectMoveStep>,
tile: T, tile: T,
controller: Readonly<IMoverController> controller: Readonly<IMoverController>
): Promise<ITileLocator>; ): Promise<ITileLocator>;
@ -451,20 +451,20 @@ export abstract class ObjectMover<T extends IObjectMovable>
* *
* @param step * @param step
*/ */
private prepareStep(step: ObjectMoveStep): void { private prepareStep(step: Readonly<ObjectMoveStep>): void {
switch (step.type) { switch (step.type) {
case ObjectMoveStepType.Dir: case ObjectMoveType.Dir:
this.moveDirection = step.move; this.moveDirection = step.move;
this.faceDirection = step.move; this.faceDirection = step.move;
break; break;
case ObjectMoveStepType.DirFace: case ObjectMoveType.DirFace:
this.moveDirection = step.move; this.moveDirection = step.move;
this.faceDirection = step.face; this.faceDirection = step.face;
break; break;
case ObjectMoveStepType.Face: case ObjectMoveType.Face:
this.faceDirection = step.value; this.faceDirection = step.value;
break; break;
case ObjectMoveStepType.Special: { case ObjectMoveType.Special: {
const dir = this.getCurrentDirection(); const dir = this.getCurrentDirection();
if (step.direction === ObjectSpecialStep.Backward) { if (step.direction === ObjectSpecialStep.Backward) {
const opposite = this.faceHandler.opposite(dir); const opposite = this.faceHandler.opposite(dir);
@ -476,10 +476,10 @@ export abstract class ObjectMover<T extends IObjectMovable>
} }
break; break;
} }
case ObjectMoveStepType.AnimDir: case ObjectMoveType.AnimDir:
this.currAnimDir = step.dir; this.currAnimDir = step.dir;
break; break;
case ObjectMoveStepType.Speed: case ObjectMoveType.Speed:
this.currentSpeed = step.value; this.currentSpeed = step.value;
break; break;
} }
@ -508,7 +508,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
tp(x: number, y: number, rel: boolean = false): this { tp(x: number, y: number, rel: boolean = false): this {
this.pushStep({ this.pushStep({
type: ObjectMoveStepType.Teleport, type: ObjectMoveType.Teleport,
x, x,
y, y,
rel rel
@ -518,7 +518,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
jump(x: number, y: number, rel: boolean = false): this { jump(x: number, y: number, rel: boolean = false): this {
this.pushStep({ this.pushStep({
type: ObjectMoveStepType.Jump, type: ObjectMoveType.Jump,
x, x,
y, y,
rel rel
@ -529,7 +529,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
step(dir: FaceDirection, count: number = 1): this { step(dir: FaceDirection, count: number = 1): this {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
this.pushStep({ this.pushStep({
type: ObjectMoveStepType.Dir, type: ObjectMoveType.Dir,
move: dir move: dir
}); });
} }
@ -543,7 +543,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
): this { ): this {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
this.pushStep({ this.pushStep({
type: ObjectMoveStepType.DirFace, type: ObjectMoveType.DirFace,
move, move,
face face
}); });
@ -554,7 +554,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
forward(count: number = 1): this { forward(count: number = 1): this {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
this.pushStep({ this.pushStep({
type: ObjectMoveStepType.Special, type: ObjectMoveType.Special,
direction: ObjectSpecialStep.Forward direction: ObjectSpecialStep.Forward
}); });
} }
@ -564,7 +564,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
backward(count: number = 1): this { backward(count: number = 1): this {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
this.pushStep({ this.pushStep({
type: ObjectMoveStepType.Special, type: ObjectMoveType.Special,
direction: ObjectSpecialStep.Backward direction: ObjectSpecialStep.Backward
}); });
} }
@ -573,7 +573,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
speed(value: number): this { speed(value: number): this {
this.pushStep({ this.pushStep({
type: ObjectMoveStepType.Speed, type: ObjectMoveType.Speed,
value value
}); });
return this; return this;
@ -581,7 +581,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
face(dir: FaceDirection): this { face(dir: FaceDirection): this {
this.pushStep({ this.pushStep({
type: ObjectMoveStepType.Face, type: ObjectMoveType.Face,
value: dir value: dir
}); });
return this; return this;
@ -589,7 +589,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
animDir(dir: ObjectAnimDirection): this { animDir(dir: ObjectAnimDirection): this {
this.pushStep({ this.pushStep({
type: ObjectMoveStepType.AnimDir, type: ObjectMoveType.AnimDir,
dir dir
}); });
return this; return this;
@ -605,7 +605,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
* @param queue * @param queue
*/ */
private async moveProgress( private async moveProgress(
queue: ObjectMoveStep[], queue: Readonly<ObjectMoveStep>[],
controller: Readonly<IMoverController> controller: Readonly<IMoverController>
) { ) {
// 移动开始 // 移动开始

View File

@ -1,6 +1,7 @@
import { FaceDirection } from './types'; import { FaceDirection } from './types';
/** /**
* @deprecated `IFaceHandler`
* *
* @param dir * @param dir
*/ */
@ -28,6 +29,7 @@ export function getFaceMovement(dir: FaceDirection): Loc {
} }
/** /**
* @deprecated `IFaceHandler`
* *
* @param dir * @param dir
* @param unknown `FaceDirection.Unknown` * @param unknown `FaceDirection.Unknown`
@ -52,6 +54,7 @@ export function degradeFace(
} }
/** /**
* @deprecated `IFaceHandler`
* *
* @param dir * @param dir
* @param anticlockwise * @param anticlockwise
@ -155,6 +158,7 @@ export function nextFaceDirection(
} }
/** /**
* @deprecated `IFaceHandler`
* *
* @param dir * @param dir
*/ */

View File

@ -52,6 +52,11 @@ export interface ITileRawData {
readonly type: TileType; readonly type: TileType;
/** 图块的通行性对象 */ /** 图块的通行性对象 */
readonly pass: ITilePassData; readonly pass: ITilePassData;
/**
* `false`
* `true` `noPass`
*/
readonly eventPass: boolean;
} }
export interface ITileLegacyConverter<TLegacy> { export interface ITileLegacyConverter<TLegacy> {

View File

@ -66,6 +66,8 @@ 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 { DefaultHitAction } from './hero/hitAction';
export class CoreState implements ICoreState { export class CoreState implements ICoreState {
// Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据 // Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据
@ -208,6 +210,16 @@ export class CoreState implements ICoreState {
); );
}); });
// 勇士顶层初始化
const passChecker = new DefaultPassChecker(this.maps);
const hitAction = new DefaultHitAction(
this.maps,
this.triggerCollector,
this
);
this.hero.location.mover.useTerrainChecker(passChecker);
this.hero.location.mover.useHitAction(hitAction);
//#endregion //#endregion
} }

View File

@ -0,0 +1,37 @@
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);
}
}

View File

@ -1,72 +1,13 @@
import { ITileLocator } from '@motajs/common';
import { FaceDirection, PassBit } from '@user/data-common'; import { FaceDirection, PassBit } from '@user/data-common';
import { IHeroState, IStateBase, ITerrainPassChecker } from '@user/data-base'; import {
IMapStore,
IPassCheckerHandler,
ITerrainPassChecker
} from '@user/data-base';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
export class DefaultPassChecker implements ITerrainPassChecker { export class DefaultPassChecker implements ITerrainPassChecker {
constructor( constructor(readonly maps: IMapStore) {}
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;
}
/** /**
* *
@ -86,4 +27,87 @@ export class DefaultPassChecker implements ITerrainPassChecker {
return 0; return 0;
} }
} }
inBound(x: number, y: number, floorId: string | undefined): boolean {
if (isNil(floorId)) return false;
const layerState = this.maps.getLayerState(floorId);
if (!layerState) return false;
const { width, height } = layerState;
return x >= 0 && y >= 0 && x < width && y < height;
}
canPass(handler: IPassCheckerHandler): boolean {
const { currLoc, nextLoc, direction, floorId, face } = 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.getLayerState(floorId);
if (!map) return false;
const event = map.eventLayer;
if (!event) return false;
const { x, y } = currLoc;
const { x: nx, y: ny } = nextLoc;
const opposite = face.opposite(direction);
const leaveMask = this.directionToPassBit(direction);
const enterMask = this.directionToPassBit(opposite);
let canLeave = true;
let canEnter = true;
// 判断事件层
const curr = event.getLocationData(x, y);
const next = event.getLocationData(nx, ny);
if (curr && curr.raw) {
canLeave = !!(leaveMask & curr.raw.pass.outPass);
}
if (next && next.raw) {
canEnter = !!(enterMask & next.raw.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;
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;
}
shouldHit(handler: IPassCheckerHandler): boolean {
const { nextLoc, floorId } = handler;
if (isNil(floorId)) return false;
const layerState = this.maps.getLayerState(floorId);
if (!layerState) return false;
const eventLayer = layerState.eventLayer;
if (!eventLayer) return false;
const { x: nx, y: ny } = nextLoc;
const next = eventLayer.getLocationData(nx, ny);
if (!next || !next.raw) return false;
return !next.raw.eventPass;
}
} }

View File

@ -36,25 +36,22 @@ export class TileLegacyBridge implements ITileLegacyConverter<LegacyTileData> {
private getPass(num: number, legacy: LegacyTileData): ITilePassData { private getPass(num: number, legacy: LegacyTileData): ITilePassData {
if (num === 0) { if (num === 0) {
// 空图块
return { return {
onlyEvents: true, onlyEvents: true,
inPass: 0b1111, inPass: 0b1111,
outPass: 0b1111 outPass: 0b1111
}; };
} else if (num === 17) { } else if (num === 17) {
// 空气墙
return { return {
onlyEvents: false, onlyEvents: false,
inPass: 0b0000, inPass: 0b0000,
outPass: 0b0000 outPass: 0b0000
}; };
} else { } else {
if (legacy.noPass) { // 正常图块
return { if (legacy.cannotIn && legacy.cannotOut) {
onlyEvents: true,
inPass: 0b0000,
outPass: 0b1111
};
} else if (legacy.cannotIn && legacy.cannotOut) {
let inPass = 0b1111; let inPass = 0b1111;
let outPass = 0b1111; let outPass = 0b1111;
if (legacy.cannotIn.includes('up')) { if (legacy.cannotIn.includes('up')) {
@ -102,7 +99,8 @@ export class TileLegacyBridge implements ITileLegacyConverter<LegacyTileData> {
id: legacy.id, id: legacy.id,
trigger: -1, trigger: -1,
type: this.getTileType(num, legacy), type: this.getTileType(num, legacy),
pass: this.getPass(num, legacy) pass: this.getPass(num, legacy),
eventPass: !legacy.noPass
}; };
} }
} }

View File

@ -5,14 +5,13 @@ import {
ISpecial, ISpecial,
IReadonlyHeroAttribute, IReadonlyHeroAttribute,
IHeroAttribute, IHeroAttribute,
IStateBase, IDataBaseExtended
IStateBaseExtended
} from '@user/data-base'; } from '@user/data-base';
import { ILocationHelper } from '@user/data-common'; import { ILocationHelper } from '@user/data-common';
//#region 辅助接口 //#region 辅助接口
export interface IEnemyHandler<TEnemy, THero> { export interface IEnemyHandler<TEnemy, THero> extends IDataBaseExtended {
/** 怪物属性信息 */ /** 怪物属性信息 */
readonly enemy: IEnemy<TEnemy>; readonly enemy: IEnemy<TEnemy>;
/** 怪物上下文 */ /** 怪物上下文 */
@ -21,11 +20,12 @@ export interface IEnemyHandler<TEnemy, THero> {
readonly locator: ITileLocator; readonly locator: ITileLocator;
/** 勇士属性信息 */ /** 勇士属性信息 */
readonly hero: IReadonlyHeroAttribute<THero>; readonly hero: IReadonlyHeroAttribute<THero>;
/** 当前全局状态对象 */
readonly state: IStateBase;
} }
export interface IReadonlyEnemyHandler<TEnemy, THero> { export interface IReadonlyEnemyHandler<
TEnemy,
THero
> extends IDataBaseExtended {
/** 怪物属性信息 */ /** 怪物属性信息 */
readonly enemy: IReadonlyEnemy<TEnemy>; readonly enemy: IReadonlyEnemy<TEnemy>;
/** 怪物上下文 */ /** 怪物上下文 */
@ -34,8 +34,6 @@ export interface IReadonlyEnemyHandler<TEnemy, THero> {
readonly locator: ITileLocator; readonly locator: ITileLocator;
/** 勇士属性信息 */ /** 勇士属性信息 */
readonly hero: IReadonlyHeroAttribute<THero>; readonly hero: IReadonlyHeroAttribute<THero>;
/** 当前全局状态对象 */
readonly state: IStateBase;
} }
//#endregion //#endregion
@ -283,7 +281,7 @@ export interface IMapDamageReducer {
): Readonly<IMapDamageInfo>; ): Readonly<IMapDamageInfo>;
} }
export interface IMapDamage<TEnemy, THero> extends IStateBaseExtended { export interface IMapDamage<TEnemy, THero> extends IDataBaseExtended {
/** 当前绑定的怪物上下文 */ /** 当前绑定的怪物上下文 */
readonly context: IEnemyContext<TEnemy, THero>; readonly context: IEnemyContext<TEnemy, THero>;
@ -406,7 +404,7 @@ export interface IDamageCalculator<TEnemy, THero> {
): number; ): number;
} }
export interface IDamageContext<TEnemy, THero> extends IStateBaseExtended { export interface IDamageContext<TEnemy, THero> extends IDataBaseExtended {
/** /**
* *
* @param enemy * @param enemy
@ -499,7 +497,7 @@ export interface IDamageSystem<TEnemy, THero> extends IDamageContext<
export interface IReadonlyEnemyContext< export interface IReadonlyEnemyContext<
TEnemy, TEnemy,
THero THero
> extends IStateBaseExtended { > extends IDataBaseExtended {
/** 怪物上下文宽度 */ /** 怪物上下文宽度 */
readonly width: number; readonly width: number;
/** 怪物上下文高度 */ /** 怪物上下文高度 */
@ -739,7 +737,7 @@ export interface IEnemyContext<TEnemy, THero> extends IReadonlyEnemyContext<
//#region 战斗流程 //#region 战斗流程
export interface ICombatFlowHandler<TEnemy, THero> { export interface ICombatFlowHandler<TEnemy, THero> extends IDataBaseExtended {
/** 战斗的怪物是否在地图上 */ /** 战斗的怪物是否在地图上 */
readonly onMap: boolean; readonly onMap: boolean;
/** 可修改勇士对象 */ /** 可修改勇士对象 */
@ -750,8 +748,6 @@ export interface ICombatFlowHandler<TEnemy, THero> {
readonly context: IEnemyContext<TEnemy, THero>; readonly context: IEnemyContext<TEnemy, THero>;
/** 怪物位置 */ /** 怪物位置 */
readonly locator: ITileLocator; readonly locator: ITileLocator;
/** 全局状态对象 */
readonly state: IStateBase;
} }
export interface ICombatFlowHook<TEnemy, THero> extends IHookBase { export interface ICombatFlowHook<TEnemy, THero> extends IHookBase {
@ -803,7 +799,7 @@ export interface ICombatResult<TEnemy, THero> {
} }
export interface ICombatFlow<TEnemy, THero> export interface ICombatFlow<TEnemy, THero>
extends IHookable<ICombatFlowHook<TEnemy, THero>>, IStateBaseExtended { extends IHookable<ICombatFlowHook<TEnemy, THero>>, IDataBaseExtended {
/** 勇士属性对象 */ /** 勇士属性对象 */
readonly hero: IReadonlyHeroAttribute<THero> | null; readonly hero: IReadonlyHeroAttribute<THero> | null;
/** 怪物上下文对象 */ /** 怪物上下文对象 */

View File

@ -3,7 +3,7 @@ import {
ILayerState, ILayerState,
IMapLayer, IMapLayer,
IStateBase, IStateBase,
IStateBaseExtended IDataBaseExtended
} from '@user/data-base'; } from '@user/data-base';
export interface ITriggerHandler { export interface ITriggerHandler {
@ -19,7 +19,7 @@ export interface ITriggerHandler {
export type TriggerFactory = (type: number, state: IStateBase) => ITrigger; export type TriggerFactory = (type: number, state: IStateBase) => ITrigger;
export interface ITrigger extends IStateBaseExtended { export interface ITrigger extends IDataBaseExtended {
/** 触发器类型标识 */ /** 触发器类型标识 */
readonly type: number; readonly type: number;
/** 触发器优先级 */ /** 触发器优先级 */
@ -37,7 +37,7 @@ export interface ITrigger extends IStateBaseExtended {
collection(): ITriggerCollection; collection(): ITriggerCollection;
} }
export interface ITriggerRegistry extends IStateBaseExtended { export interface ITriggerRegistry extends IDataBaseExtended {
/** /**
* *
* @param type * @param type

View File

@ -1,26 +1,32 @@
# 核心规则
你的身份不是架构设计者,而是长期维护该项目的核心开发者。
该项目的架构已经经过大量设计与验证,你不需要深刻理解架构,当前架构已经是经过深思熟虑的结果。除非明确要求,否则不要试图重新设计架构,也不要主动优化已有设计。你的职责是在保持整体风格和架构稳定的前提下,完成需求并自然扩展已有系统。
1. **遵循最小修改原则**
- 默认不要重新设计已有代码。
- 默认不要重构已有实现。
- 默认不要为了"更优雅"而改变已有模式。
- 新代码应当尽可能像是在原有代码基础上的自然延续,而不是重新实现
2. **现有代码最重要**。当前文件就是最高优先级规范。当 Prompt、已有代码、自身知识冲突时优先遵循已有代码。
3. **不要主动创造新的模式**。遵循已有的设计与代码风格,不要在不同风格之间来回跳跃。
4. **优先优化维护成本,而不是代码长度**。允许局部重复,允许多几个变量,允许多几个 if。但绝不鼓励为了减少几行代码增加抽象为了减少重复增加函数跳转为了"现代 TS"使用复杂语法。
5. **当不确定时,选择最保守的方案**。如果两种方案都能实现需求,默认选择修改最少、风险最低、最接近已有实现的方案。
# 必须规则 # 必须规则
以下规则必须时刻遵守,任何情况下都不允许违反。 以下规则必须时刻遵守,任何情况下都不允许违反。
1. **不擅自修改已有内容**:将我已经写好的内容视为绝对正确。除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。若认为我的代码存在错误,应在对话中提出,而不是直接修改。 1. **不擅自修改已有内容**:将我已经写好的内容视为绝对正确。除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。若认为我的代码存在错误,应在对话中提出,而不是直接修改。
2. **不恢复我的修改**:我做的任何代码修改都是有原因的。若我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。 2. **不恢复我的修改**:我做的任何代码修改都是有原因的。若我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。
3. **以目的驱动,而非以接口驱动**:实现前先想清楚我为什么要这样设计接口、这个接口设计的目的是什么,而不是单纯地以将接口填满为目标。 3. **遇到歧义立即提问**:若思考或实现时遇到任何问题——例如描述模糊、接口不清晰、某些地方存在歧义等——应立即向我提问,而不是按自己的想法去写。若完成需求所需的接口尚不存在,也应当立即停止实现,向我提出疑问,而不是擅自新增接口来推进。
4. **遇到歧义立即提问**:若思考或实现时遇到任何问题——例如描述模糊、接口不清晰、某些地方存在歧义等——应立即向我提问,而不是按自己的想法去写。 4. **修改文件前重读文件,修改后说明内容**:修改文件前务必重新阅读以确保内容是最新版。修改后必须在对话中说明所有改动的文件及具体改动内容。在修改文件时,**不得修改**文档的 `涉及文件` 节未提及的文件。
5. **接口缺失时停止并提问**:若完成需求所需的接口尚不存在,应立即停止实现,向我提出疑问,而不是擅自新增接口来推进。 5. **关于 `dev.md` 的强度升级**`dev.md` 中"一般不建议/尽量避免"的条目在此处视为**绝对禁止**"建议/最好"的条目在此处视为**必须遵守**。
6. **按我说的方式重构**:重构时**不要**擅自新增任何参数、方法或接口,**不要**仅通过新增兼容层应对重构。 6. **渲染端永远处于被动**:任何情况下都不可能会出现数据端主动通知渲染端进行更新的场景,渲染端仅通过钩子与数据端通信,通过钩子被动获取信息,并在某些情况下影响数据端行为。
- **彻底性重构**(新旧接口完全没有重合):按正常方式全新实现,旧代码仅作逻辑参考。 7. **关于非空判断**:对于对象,直接使用 `if (!object)` 的方式进行判断,对于字面量,使用 `lodash` 接口 `isNil` 判断 `if (isNil(value))`,不要使用 `if (value === undefined)` 这种方式。
- **结构性重构**(新旧接口基本一致,细节有差距):将旧代码搬移到新接口上后进行微调。 8. **方法顺序**:对于类的方法,根据功能进行排序,而不是按照修饰词进行排序。对于较长或功能较多的类,使用 `#region` 进行分区,分区按照方法功能进行划分。所有类的成员必须写到类的开头,并按照功能排序。
7. **不要有任何"顺手"的想法**:任何时候,都不要出现顺手的想法,包括但不限于发现了一个 bug 然后**顺手**修复、发现一处类型错误然后**顺手**修复等,这种情况下应当遵循规则 1。 9. **注释要求**:所有私有方法和私有成员必须添加 jsDoc 注释,受保护、公共成员与方法必须在源头处添加注释,继承而来的,除非功能或描述有变化,否则不得添加注释。即要求注释具有唯一性:已经在某些地方添加过注释的,不在其他地方添加。
8. **修改文件前重读文件,修改后说明内容**:修改文件前务必重新阅读以确保内容是最新版。修改后必须在对话中说明所有改动的文件及具体改动内容。在修改文件时,**不得修改**文档的 `涉及文件` 节未提及的文件。
9. **关于 `dev.md` 的强度升级**`dev.md` 中"一般不建议/尽量避免"的条目在此处视为**绝对禁止**"建议/最好"的条目在此处视为**必须遵守**。`as` 关键字不受此升级约束。
10. **参数最小化 / 级联获取**:不从外部传入可通过已有引用获取的对象。若对象已持有对父对象或相关对象的引用(如 `controller`),应直接通过该引用获取所需数据(如 `controller.state`),不应再单独传递参数。即"非必要不传参"。
11. **转换下沉**:数据的格式或类型转换(如 `string` 的 id 转 `number`)应放在最终消费该值的对象中完成,而非在调用方预先转换后再传入。这样可以减少调用方的职责并避免重复转换。
12. **通过公有接口操作,不直接赋值内部属性**:对某个对象内部状态的修改,必须通过该对象对外暴露的公有方法进行(如用 `mover.face(dir).start()` 设置朝向并发起移动),而不是通过类型断言访问其内部属性直接赋值(如 `mover.faceDirection = dir`)。
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)` 这种方式。
# 建议规则 # 建议规则