Compare commits

..

No commits in common. "eb34d63e96955ad35661ddbfec4b1ff06d76f2c6" and "931f610d37afe224c97b5f8c2d305386dd543ad0" have entirely different histories.

20 changed files with 255 additions and 515 deletions

2
dev.md
View File

@ -135,7 +135,7 @@
| 包 | 层级 | 说明 |
| ------------------- | ------- | ------------------------------------------------------------------------------- |
| `@user/data-common` | Layer 0 | 公共层,定义 `IDataCommon` 及公共无依赖接口 |
| `@user/data-common | Layer 0 | 公共层,定义 `IDataCommon` 及公共无依赖接口 |
| `@user/data-base` | Layer 1 | 数据层,定义 `IStateBase` 及可存档游戏数据(地图、怪物、玩家属性等) |
| `@user/data-system` | Layer 2 | 执行层,定义 `ICoreState`,依赖数据层实现玩家控制、战斗计算等影响游戏进程的动作 |
| `@user/data-state` | Layer 3 | 数据端的顶层模块,一般仅用于初始化以及仅供渲染端调用的顶层模块 |

View File

@ -43,11 +43,8 @@ export abstract class BaseHeroModifier<T, V> implements IHeroModifier<T, V, V> {
export class HeroAttribute<THero> implements IHeroAttribute<THero> {
/** 当前勇士属性修饰器 */
private readonly modifier: Map<keyof THero, IHeroModifier[]> = new Map();
/** 当前每个修饰器对应的属性名称 */
private readonly modifierName: Map<
IHeroModifier<THero[keyof THero]>,
keyof THero
> = new Map();
/** 当前每个修饰器对应的属性值 */
private readonly modifierName: Map<IHeroModifier, keyof THero> = new Map();
/** 当前勇士最终属性 */
private readonly finalAttribute: THero;
@ -147,7 +144,7 @@ export class HeroAttribute<THero> implements IHeroAttribute<THero> {
addModifier<K extends keyof THero>(
name: K,
modifier: IHeroModifier<THero[K]>
modifier: IHeroModifier<THero[K], unknown>
): void {
if (modifier.owner) {
const modiferName = modifier.constructor.name;
@ -184,7 +181,7 @@ export class HeroAttribute<THero> implements IHeroAttribute<THero> {
this.recalculateAttribute(name);
}
markModifierDirty(modifier: IHeroModifier<THero[keyof THero]>): void {
markModifierDirty(modifier: IHeroModifier): void {
const name = this.modifierName.get(modifier);
if (name === undefined) return;
this.markDirty(name);
@ -213,16 +210,9 @@ export class HeroAttribute<THero> implements IHeroAttribute<THero> {
return structuredClone(this.attribute);
}
*iterateModifiers(): IterableIterator<[PropertyKey, IHeroModifier]> {
*iterateModifiers(): IterableIterator<[keyof THero, IHeroModifier]> {
for (const [modifier, name] of this.modifierName) {
yield [name, modifier];
}
}
getModifiers<K extends keyof THero>(
name: K
): Iterable<IHeroModifier<THero[K]>> {
const arr = this.modifier.get(name) as IHeroModifier<THero[K]>[];
return arr ?? [];
}
}

View File

@ -1,10 +1,4 @@
import {
Hookable,
HookController,
IFacedTileLocator,
IHookController,
logger
} from '@motajs/common';
import { IFacedTileLocator, logger } from '@motajs/common';
import {
FaceDirection,
IDataCommon,
@ -17,7 +11,6 @@ import {
IHeroFollower,
IHeroFollowerSave,
IHeroFollowersController,
IHeroFollowersControllerHooks,
IHeroLocation,
IHeroRendering
} from './types';
@ -72,7 +65,6 @@ export class HeroFollower implements IHeroFollower {
saveState(compression: SaveCompression): IHeroFollowerSave {
return {
num: this.num,
rendering: this.rendering.saveState(compression),
location: this.location.saveState(compression)
};
@ -83,135 +75,3 @@ export class HeroFollower implements IHeroFollower {
this.location.loadState(state.location, compression);
}
}
export class HeroFollowersController
extends Hookable<IHeroFollowersControllerHooks>
implements IHeroFollowersController
{
readonly state: IDataCommon;
/** 跟随者列表 */
private readonly followers: HeroFollower[] = [];
/** 勇士位置对象,用于获取聚集目标位置 */
private readonly heroLocation: IHeroLocation;
/** 朝向处理器,传递给新创建的跟随者 */
private readonly faceHandler: IFaceHandler<FaceDirection>;
constructor(
state: IDataCommon,
heroLocation: IHeroLocation,
faceHandler: IFaceHandler<FaceDirection>
) {
super();
this.state = state;
this.heroLocation = heroLocation;
this.faceHandler = faceHandler;
}
protected createController(
hook: Partial<IHeroFollowersControllerHooks>
): IHookController<IHeroFollowersControllerHooks> {
return new HookController(this, hook);
}
addFollower(num: number | string): IHeroFollower {
const loc: IFacedTileLocator = {
x: this.heroLocation.x,
y: this.heroLocation.y,
direction: this.heroLocation.mover.faceDirection
};
const follower = new HeroFollower(num, loc, this.faceHandler, this);
this.followers.push(follower);
const index = this.followers.length - 1;
this.forEachHook(hook => {
hook.onAddFollower?.(follower, index);
});
return follower;
}
getFollower(index: number): IHeroFollower | null {
return this.followers[index] ?? null;
}
*getFollowersById(
num: number | string
): IterableIterator<[number, IHeroFollower]> {
const store = this.state.tileStore;
const target = typeof num === 'string' ? store.idToNumber(num)! : num;
for (let i = 0; i < this.followers.length; i++) {
if (this.followers[i].num === target) {
yield [i, this.followers[i]];
}
}
}
getAllFollowers(): IHeroFollower[] {
return this.followers.slice();
}
async removeFollower(index: number): Promise<void> {
const removed = this.followers.splice(index, 1);
if (removed.length > 0) {
this.forEachHook(hook => {
hook.onRemoveFollower?.(removed[0], index);
});
}
}
async removeAllFollowers(): Promise<void> {
const snapshot = this.followers.slice();
this.followers.length = 0;
this.forEachHook(hook => {
snapshot.forEach((v, i) => {
hook.onRemoveFollower?.(v, i);
});
});
}
async gatherFollowers(): Promise<void> {
const { x, y, mover } = this.heroLocation;
for (let i = 0; i < this.followers.length; i++) {
const follower = this.followers[i];
follower.location.mover.clear();
let skip = false;
for (let j = i - 1; j >= 0; j--) {
const dir = this.followers[j].location.mover.moveDirection;
if (dir === FaceDirection.Unknown) {
follower.location.mover.clear();
follower.location.mover.tp(x, y);
follower.location.mover.face(mover.faceDirection);
skip = true;
break;
} else {
follower.location.mover.step(dir);
}
}
if (!skip) {
follower.location.mover.step(mover.moveDirection);
}
}
const controllers = this.followers.map(
v => v.location.mover.start()?.onEnd
);
await Promise.all(controllers);
this.forEachHook(hook => {
hook.onGatherFollowers?.(false);
});
}
gatherFollowersSync(): void {
const { x, y } = this.heroLocation;
const dir = this.heroLocation.mover.faceDirection;
for (const follower of this.followers) {
const mover = follower.location.mover;
mover.setPos(x, y);
mover.setFaceDir(dir);
}
this.forEachHook(hook => {
hook.onGatherFollowers?.(true);
});
}
}

View File

@ -0,0 +1,146 @@
import {
Hookable,
HookController,
IFacedTileLocator,
IHookController
} from '@motajs/common';
import { FaceDirection, IDataCommon, IFaceHandler } from '@user/data-common';
import { HeroFollower } from './follower';
import {
IHeroFollowersController,
IHeroFollowersControllerHooks,
IHeroFollower,
IHeroLocation
} from './types';
export class HeroFollowersController
extends Hookable<IHeroFollowersControllerHooks>
implements IHeroFollowersController
{
readonly state: IDataCommon;
/** 跟随者列表 */
private readonly followers: HeroFollower[] = [];
/** 勇士位置对象,用于获取聚集目标位置 */
private readonly heroLocation: IHeroLocation;
/** 朝向处理器,传递给新创建的跟随者 */
private readonly faceHandler: IFaceHandler<FaceDirection>;
constructor(
state: IDataCommon,
heroLocation: IHeroLocation,
faceHandler: IFaceHandler<FaceDirection>
) {
super();
this.state = state;
this.heroLocation = heroLocation;
this.faceHandler = faceHandler;
}
protected createController(
hook: Partial<IHeroFollowersControllerHooks>
): IHookController<IHeroFollowersControllerHooks> {
return new HookController(this, hook);
}
addFollower(num: number | string): IHeroFollower {
const loc: IFacedTileLocator = {
x: this.heroLocation.x,
y: this.heroLocation.y,
direction: this.heroLocation.mover.faceDirection
};
const follower = new HeroFollower(num, loc, this.faceHandler, this);
this.followers.push(follower);
const index = this.followers.length - 1;
this.forEachHook(hook => {
hook.onAddFollower?.(follower, index);
});
return follower;
}
getFollower(index: number): IHeroFollower | null {
return this.followers[index] ?? null;
}
*getFollowersById(
num: number | string
): IterableIterator<[number, IHeroFollower]> {
const store = this.state.tileStore;
const target = typeof num === 'string' ? store.idToNumber(num)! : num;
for (let i = 0; i < this.followers.length; i++) {
if (this.followers[i].num === target) {
yield [i, this.followers[i]];
}
}
}
getAllFollowers(): IHeroFollower[] {
return this.followers.slice();
}
async removeFollower(index: number): Promise<void> {
const removed = this.followers.splice(index, 1);
if (removed.length > 0) {
this.forEachHook(hook => {
hook.onRemoveFollower?.(removed[0], index);
});
}
}
async removeAllFollowers(): Promise<void> {
const snapshot = this.followers.slice();
this.followers.length = 0;
this.forEachHook(hook => {
snapshot.forEach((v, i) => {
hook.onRemoveFollower?.(v, i);
});
});
}
async gatherFollowers(): Promise<void> {
const { x, y, mover } = this.heroLocation;
for (let i = 0; i < this.followers.length; i++) {
const follower = this.followers[i];
follower.location.mover.clear();
let skip = false;
for (let j = i - 1; j >= 0; j--) {
const dir = this.followers[j].location.mover.moveDirection;
if (dir === FaceDirection.Unknown) {
follower.location.mover.clear();
follower.location.mover.tp(x, y);
follower.location.mover.face(mover.faceDirection);
skip = true;
break;
} else {
follower.location.mover.step(dir);
}
}
if (!skip) {
follower.location.mover.step(mover.moveDirection);
}
}
const controllers = this.followers.map(
v => v.location.mover.start()?.onEnd
);
await Promise.all(controllers);
this.forEachHook(hook => {
hook.onGatherFollowers?.(false);
});
}
gatherFollowersSync(): void {
const targetX = this.heroLocation.x;
const targetY = this.heroLocation.y;
const targetDir = this.heroLocation.mover.faceDirection;
for (const follower of this.followers) {
follower.location.setPos(targetX, targetY);
follower.location.mover.face(targetDir).start();
}
this.forEachHook(hook => {
hook.onGatherFollowers?.(true);
});
}
}

View File

@ -1,5 +1,6 @@
export * from './attribute';
export * from './follower';
export * from './followersController';
export * from './location';
export * from './rendering';
export * from './mover';

View File

@ -43,6 +43,6 @@ export class HeroLocation implements IHeroLocation {
loadState(state: IHeroLocationSave): void {
this.x = state.x;
this.y = state.y;
this.mover.setFaceDir(state.direction);
this.mover.face(state.direction).start();
}
}

View File

@ -57,12 +57,17 @@ export class HeroMover<T extends IHeroLocation>
protected async onMoveStart(
_tile: IHeroLocation,
_controller: Readonly<IMoverController>
): Promise<void> {}
): Promise<void> {
// TODO: 通知渲染端开始移动,同步平滑视角
}
protected async onMoveEnd(
_tile: IHeroLocation,
_controller: Readonly<IMoverController>
): Promise<void> {}
): Promise<void> {
// TODO: 通知渲染端移动结束
// TODO: 清除自动寻路状态
}
protected async onStepStart(
step: ObjectMoveStep,

View File

@ -1,66 +1,39 @@
import { HeroAttribute } from './attribute';
import { HeroFollowersController } from './follower';
import { HeroLocation } from './location';
import { HeroRendering } from './rendering';
import {
IHeroAttribute,
IHeroFollowersController,
IHeroLocation,
IHeroModifier,
IHeroRendering,
IHeroMoveController,
IHeroState,
IHeroStateSave,
IModifierStateSave,
IReadonlyHeroAttribute
} from './types';
import {
FaceDirection,
IDataCommon,
IFaceHandler,
SaveCompression
} from '@user/data-common';
import { SaveCompression } from '@user/data-common';
import { IFacedTileLocator, logger } from '@motajs/common';
export class HeroState<THero> implements IHeroState<THero> {
/** 修饰器工厂函数注册表 */
private readonly registry: Map<
string,
<K extends keyof THero>() => IHeroModifier<THero[K]>
> = new Map();
readonly location: IHeroLocation;
readonly rendering: IHeroRendering;
readonly followers: IHeroFollowersController;
private readonly registry: Map<string, () => IHeroModifier> = new Map();
constructor(
state: IDataCommon,
faceHandler: IFaceHandler<FaceDirection>,
public mover: IHeroMoveController,
public attribute: IHeroAttribute<THero>
) {
this.rendering = new HeroRendering(state);
const defaultLoc: IFacedTileLocator = {
x: 0,
y: 0,
direction: FaceDirection.Down
};
this.location = new HeroLocation(state, defaultLoc, faceHandler);
this.followers = new HeroFollowersController(
state,
this.location,
faceHandler
);
) {}
attachMover(mover: IHeroMoveController): void {
this.mover = mover;
}
attachAttribute(attribute: IHeroAttribute<THero>): void {
this.attribute = attribute;
}
getHeroMover(): IHeroMoveController {
return this.mover;
}
getLocation(): IFacedTileLocator {
return {
x: this.location.x,
y: this.location.y,
direction: this.location.mover.faceDirection
};
return this.mover;
}
getModifiableAttribute(): IHeroAttribute<THero> {
@ -75,10 +48,7 @@ export class HeroState<THero> implements IHeroState<THero> {
return this.attribute.getModifiableClone();
}
registerModifier(
type: string,
cons: <K extends keyof THero>() => IHeroModifier<THero[K]>
): void {
registerModifier(type: string, cons: () => IHeroModifier): void {
this.registry.set(type, cons);
}
@ -87,8 +57,9 @@ export class HeroState<THero> implements IHeroState<THero> {
if (!cons) {
logger.warn(116, type);
return null;
} else {
return cons() as IHeroModifier<T, V>;
}
return cons() as IHeroModifier<T, V>;
}
createAndInsertModifier<K extends keyof THero, V>(
@ -102,23 +73,22 @@ export class HeroState<THero> implements IHeroState<THero> {
}
saveState(compression: SaveCompression): IHeroStateSave<THero> {
const modifiers: IModifierStateSave<THero>[] = [];
const modifiers: IModifierStateSave[] = [];
for (const [name, modifier] of this.attribute.iterateModifiers()) {
modifiers.push({
name: name as keyof THero,
name,
type: modifier.type,
state: modifier.saveState(compression)
});
}
const followerSaves = this.followers
.getAllFollowers()
.map(v => v.saveState(compression));
return {
attribute: this.attribute.toStructured(),
location: this.location.saveState(compression),
rendering: this.rendering.saveState(compression),
followers: followerSaves,
locator: {
x: this.mover.x,
y: this.mover.y,
direction: this.mover.direction
},
followers: structuredClone(this.mover.followers),
modifiers
};
}
@ -132,16 +102,19 @@ export class HeroState<THero> implements IHeroState<THero> {
const cons = this.registry.get(save.type);
if (!cons) continue;
const modifier = cons();
modifier.loadState(save.state, compression);
newAttribute.addModifier(save.name, modifier);
modifier.loadState(save.state as never, compression);
newAttribute.addModifier(
save.name as keyof THero,
modifier as unknown as IHeroModifier<THero[keyof THero]>
);
}
this.attribute = newAttribute;
this.location.loadState(state.location, compression);
this.rendering.loadState(state.rendering, compression);
void this.followers.removeAllFollowers();
for (const save of state.followers) {
const follower = this.followers.addFollower(save.num);
follower.loadState(save, compression);
}
this.mover.setPosition(state.locator.x, state.locator.y);
this.mover.turn(state.locator.direction);
this.mover.removeAllFollowers();
state.followers.forEach(follower => {
this.mover.addFollower(follower.num, follower.identifier);
this.mover.setFollowerAlpha(follower.identifier, follower.alpha);
});
}
}

View File

@ -9,9 +9,9 @@ import {
//#region 勇士属性
export interface IHeroModifier<
H = unknown, // 属性值类型
V = unknown, // 修饰器参数类型
Save = unknown // 存档类型
K = unknown,
V = unknown,
Save = unknown
> extends ISaveableContent<Save> {
/** 修饰器类型 */
readonly type: string;
@ -45,17 +45,17 @@ export interface IHeroModifier<
* @param baseValue
* @param name
*/
modify(value: H, baseValue: H, name: PropertyKey): H;
modify(value: K, baseValue: K, name: PropertyKey): K;
/**
*
*/
clone(): IHeroModifier<H, V>;
clone(): IHeroModifier<K, V>;
}
export interface IModifierStateSave<THero> {
export interface IModifierStateSave {
/** 属性名称 */
readonly name: keyof THero;
readonly name: PropertyKey;
/** 修饰器类型 */
readonly type: string;
/** 修饰器存档数据 */
@ -107,14 +107,6 @@ export interface IReadonlyHeroAttribute<THero> {
*
*/
iterateModifiers(): Iterable<[PropertyKey, IHeroModifier]>;
/**
*
* @param name
*/
getModifiers<K extends keyof THero>(
name: K
): Iterable<IHeroModifier<THero[K]>>;
}
export interface IHeroAttribute<THero> extends IReadonlyHeroAttribute<THero> {
@ -271,8 +263,6 @@ export interface IHeroRendering
//#region 勇士跟随者
export interface IHeroFollowerSave {
/** 跟随者图块数字 */
readonly num: number;
/** 跟随者渲染对象保存 */
readonly rendering: IHeroRenderingSave;
/** 跟随者位置保存 */
@ -379,13 +369,11 @@ export interface IHeroStateSave<THero> {
/** 勇士属性状态 */
readonly attribute: THero;
/** 勇士当前位置 */
readonly location: IHeroLocationSave;
/** 勇士渲染状态 */
readonly rendering: IHeroRenderingSave;
readonly locator: IHeroLocationSave;
/** 勇士当前的跟随者 */
readonly followers: readonly IHeroFollowerSave[];
/** 勇士属性修饰器状态 */
readonly modifiers: readonly IModifierStateSave<THero>[];
readonly modifiers: readonly IModifierStateSave[];
}
export interface IHeroState<THero> extends ISaveableContent<

View File

@ -4,18 +4,14 @@ import {
IDataCommon,
IMoverController,
IObjectMover,
IRoleFaceBinder,
ITileRawData
IRoleFaceBinder
} from '@user/data-common';
import { IDynamicLayer, IDynamicTile } from './types';
import { DynamicTileMover } from './mover';
import { logger } from '@motajs/common';
export class DynamicTile implements IDynamicTile {
readonly state: IDataCommon;
readonly mover: IObjectMover<IDynamicTile>;
raw: ITileRawData | null;
triggerType: number;
/** 当前的朝向绑定对象 */
@ -30,13 +26,6 @@ export class DynamicTile implements IDynamicTile {
this.state = layer.state;
this.mover = new DynamicTileMover(this);
this.triggerType = -1;
const data = this.state.tileStore.getData(num);
if (!data) {
logger.warn(143, num.toString());
this.raw = null;
} else {
this.raw = data;
}
}
setFaceBinder(binder: IRoleFaceBinder | null): void {

View File

@ -1,7 +1,6 @@
import { isNil } from 'lodash-es';
import {
IDynamicLayer,
ILayerLocation,
ILayerState,
IMapLayer,
IMapLayerData,
@ -56,10 +55,6 @@ export class MapLayer
this.dynamicLayer = new DynamicLayer(this);
}
inMap(x: number, y: number): boolean {
return x >= 0 && y >= 0 && x < this.width && y < this.height;
}
/**
*
*/
@ -155,32 +150,15 @@ export class MapLayer
}
getBlock(x: number, y: number): number {
if (!this.inMap(x, y)) {
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
// 不在地图内,返回 -1
return -1;
}
return this.mapArray[y * this.width + x];
}
getLocationData(x: number, y: number): ILayerLocation | null {
if (!this.inMap(x, y)) return null;
const index = y * this.width + x;
const num = this.mapArray[index];
const raw = this.state.tileStore.getData(num);
const dynamics = this.dynamicLayer.getDynamicTilesAt(x, y);
const trigger = this.triggerMap.get(index) ?? -1;
const data: ILayerLocation = {
tile: num,
raw,
trigger,
dynamics
};
return data;
}
getTriggerType(x: number, y: number): number {
if (!this.inMap(x, y)) {
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
return -1;
}
const index = y * this.width + x;
@ -191,7 +169,7 @@ export class MapLayer
}
setTriggerType(type: number, x: number, y: number): void {
if (!this.inMap(x, y)) {
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
return;
}
const index = y * this.width + x;
@ -203,7 +181,7 @@ export class MapLayer
}
revertTrigger(x: number, y: number): void {
if (this.inMap(x, y)) {
if (x >= 0 && y >= 0 && x < this.width && y < this.height) {
this.triggerMap.delete(y * this.width + x);
}
}

View File

@ -6,8 +6,7 @@ import {
IObjectMovable,
IObjectMover,
IRoleFaceBinder,
ISaveableContent,
ITileRawData
ISaveableContent
} from '@user/data-common';
import { ITileStore } from '@user/data-common';
@ -16,21 +15,10 @@ import { ITileStore } from '@user/data-common';
export interface IMapLayerData {
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
expired: boolean;
/** 地图图块数组,是对内部存储的直接引用,仅建议读取,不建议修改 */
/** 地图图块数组,是对内部存储的直接引用 */
array: Uint32Array;
}
export interface ILayerLocation {
/** 该点的静态图块数字 */
readonly tile: number;
/** 该点的静态图块信息 */
readonly raw: ITileRawData | null;
/** 该点的静态触发器,-1 表示未设置(即使用图块本身的触发器),否则表示该点的静态触发器,覆盖图块本身的触发器 */
readonly trigger: number;
/** 该点包含的所有动态图块 */
readonly dynamics: Iterable<IDynamicTile>;
}
export interface IMapLayerHooks extends IHookBase {
/**
* `resize`
@ -103,13 +91,6 @@ export interface IMapLayer
/** 此图层对应的动态图块图层z 层级与静态图块一致 */
readonly dynamicLayer: IDynamicLayer;
/**
*
* @param x
* @param y
*/
inMap(x: number, y: number): boolean;
/**
*
* @param block
@ -126,13 +107,6 @@ export interface IMapLayer
*/
getBlock(x: number, y: number): number;
/**
*
* @param x
* @param y
*/
getLocationData(x: number, y: number): ILayerLocation | null;
/**
* 退
* @param x
@ -538,21 +512,21 @@ export interface IDynamicLayerHooks extends IHookBase {
* @param tile
* @param layer
*/
onCreateTile?(tile: IDynamicTile, layer: IDynamicLayer): void;
onCreateTile(tile: IDynamicTile, layer: IDynamicLayer): void;
/**
*
* @param tile
* @param layer
*/
onDeleteTile?(tile: IDynamicTile, layer: IDynamicLayer): Promise<void>;
onDeleteTile(tile: IDynamicTile, layer: IDynamicLayer): Promise<void>;
/**
* 使 `mover`
* @param tile
* @param layer
*/
onUpdateTilePosition?(tile: IDynamicTile, layer: IDynamicLayer): void;
onUpdateTilePosition(tile: IDynamicTile, layer: IDynamicLayer): void;
}
export interface IDynamicLayer
@ -639,8 +613,6 @@ export interface IDynamicTile extends IObjectMovable, IDataCommonExtended {
readonly layer: IDynamicLayer;
/** 当前动态图块的移动器 */
readonly mover: IObjectMover<IDynamicTile>;
/** 当前动态图块的图块数据 */
readonly raw: ITileRawData | null;
/**
* {@link num}

View File

@ -205,31 +205,6 @@ export interface IObjectMoverHooks<T extends IObjectMovable> extends IHookBase {
tile: T,
mover: IObjectMover<T>
): Promise<void>;
/**
* `setPos`
* @param x
* @param y
* @param tile
* @param mover
*/
onSetPos?(x: number, y: number, tile: T, mover: IObjectMover<T>): void;
/**
* `setFaceDir`
* @param dir
* @param tile
* @param mover
*/
onSetFaceDir?(dir: FaceDirection, tile: T, mover: IObjectMover<T>): void;
/**
* `setMoveDir`
* @param dir
* @param tile
* @param mover
*/
onSetMoveDir?(dir: FaceDirection, tile: T, mover: IObjectMover<T>): void;
}
export interface IObjectMover<T extends IObjectMovable> extends IHookable<
@ -248,25 +223,6 @@ export interface IObjectMover<T extends IObjectMovable> extends IHookable<
/** 当前移动速度,单位毫秒 */
readonly currentSpeed: number;
/**
*
* @param x
* @param y
*/
setPos(x: number, y: number): void;
/**
*
* @param dir
*/
setFaceDir(dir: FaceDirection): void;
/**
*
* @param dir
*/
setMoveDir(dir: FaceDirection): void;
/**
*
* @param x
@ -468,7 +424,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
if (step.direction === ObjectSpecialStep.Backward) {
const opposite = this.faceHandler.opposite(dir);
this.moveDirection = opposite;
this.faceDirection = dir;
this.faceDirection = opposite;
} else {
this.moveDirection = dir;
this.faceDirection = dir;
@ -484,27 +440,6 @@ export abstract class ObjectMover<T extends IObjectMovable>
}
}
setPos(x: number, y: number): void {
this.tile.setPos(x, y);
this.forEachHook(hook => {
hook.onSetPos?.(x, y, this.tile, this);
});
}
setFaceDir(dir: FaceDirection): void {
this.faceDirection = dir;
this.forEachHook(hook => {
hook.onSetFaceDir?.(dir, this.tile, this);
});
}
setMoveDir(dir: FaceDirection): void {
this.moveDirection = dir;
this.forEachHook(hook => {
hook.onSetMoveDir?.(dir, this.tile, this);
});
}
tp(x: number, y: number, rel: boolean = false): this {
this.pushStep({
type: ObjectMoveStepType.Teleport,
@ -626,9 +561,7 @@ export abstract class ObjectMover<T extends IObjectMovable>
);
await Promise.all(stepStartHooks);
const loc = await this.onStepEnd(code, step, this.tile, controller);
if (this.tile.x !== loc.x && this.tile.y !== loc.y) {
this.tile.setPos(loc.x, loc.y);
}
this.tile.setPos(loc.x, loc.y);
const stepEndHooks = this.forEachHook(hook =>
hook.onStepEnd?.(code, step, this.tile, this)
);

View File

@ -1,5 +1,3 @@
//#region tile
export const enum TileType {
/** 未知或尚未归类的图块 */
Unknown,
@ -21,26 +19,6 @@ export const enum TileType {
Tileset
}
export const enum PassBit {
/** 上方向掩码 */
Up = 0b0001,
/** 右方向掩码 */
Right = 0b0010,
/** 下方向掩码 */
Down = 0b0100,
/** 左方向掩码 */
Left = 0b1000
}
export interface ITilePassData {
/** 是否仅当图块处在事件层时生效 */
readonly onlyEvents: boolean;
/** 可以离开的方向 */
readonly outPass: number;
/** 可以进入的方向 */
readonly inPass: number;
}
export interface ITileRawData {
/** 图块数字 */
readonly num: number;
@ -50,8 +28,6 @@ export interface ITileRawData {
readonly trigger: number;
/** 图块逻辑类型 */
readonly type: TileType;
/** 图块的通行性对象 */
readonly pass: ITilePassData;
}
export interface ITileLegacyConverter<TLegacy> {
@ -113,5 +89,3 @@ export interface ITileStore<TLegacy = unknown> {
*/
fromLegacy(num: number, legacy: TLegacy): ITileRawData;
}
//#endregion

View File

@ -17,6 +17,7 @@ import {
} from '@user/data-common';
import {
EnemyManager,
HeroMoveController,
IEnemyManager,
HeroAttribute,
HeroState,
@ -128,8 +129,9 @@ export class CoreState implements ICoreState {
this.maps = new MapStore(tileStore, this);
// 勇士
const heroMover = new HeroMoveController();
const heroAttribute = new HeroAttribute(HERO_DEFAULT_ATTRIBUTE);
const heroState = new HeroState(this, dir8, heroAttribute);
const heroState = new HeroState(heroMover, heroAttribute);
this.hero = heroState;
this.loadProgress = new LoadProgressTotal();

View File

@ -1,14 +1,21 @@
import {
ITileLegacyConverter,
ITilePassData,
ITileRawData,
PassBit,
TileType
} from '@user/data-common';
export type LegacyTileData = MapDataOf<keyof NumberToId>;
export class TileLegacyBridge implements ITileLegacyConverter<LegacyTileData> {
fromLegacy(num: number, legacy: LegacyTileData): ITileRawData {
return {
num,
id: legacy.id,
trigger: -1,
type: this.getTileType(num, legacy)
};
}
private getTileType(num: number, legacy: LegacyTileData): TileType {
if (num === 0) return TileType.None;
switch (legacy.cls) {
@ -33,76 +40,4 @@ export class TileLegacyBridge implements ITileLegacyConverter<LegacyTileData> {
return TileType.Unknown;
}
}
private getPass(num: number, legacy: LegacyTileData): ITilePassData {
if (num === 0) {
return {
onlyEvents: true,
inPass: 0b1111,
outPass: 0b1111
};
} else if (num === 17) {
return {
onlyEvents: false,
inPass: 0b0000,
outPass: 0b0000
};
} else {
if (legacy.noPass) {
return {
onlyEvents: true,
inPass: 0b0000,
outPass: 0b1111
};
} else if (legacy.cannotIn && legacy.cannotOut) {
let inPass = 0b1111;
let outPass = 0b1111;
if (legacy.cannotIn.includes('up')) {
inPass &= ~PassBit.Up;
}
if (legacy.cannotIn.includes('right')) {
inPass &= ~PassBit.Right;
}
if (legacy.cannotIn.includes('down')) {
inPass &= ~PassBit.Down;
}
if (legacy.cannotIn.includes('left')) {
inPass &= ~PassBit.Left;
}
if (legacy.cannotOut.includes('up')) {
outPass &= ~PassBit.Up;
}
if (legacy.cannotOut.includes('right')) {
outPass &= ~PassBit.Right;
}
if (legacy.cannotOut.includes('down')) {
outPass &= ~PassBit.Down;
}
if (legacy.cannotOut.includes('left')) {
outPass &= ~PassBit.Left;
}
return {
onlyEvents: false,
inPass,
outPass
};
} else {
return {
onlyEvents: true,
inPass: 0b1111,
outPass: 0b1111
};
}
}
}
fromLegacy(num: number, legacy: LegacyTileData): ITileRawData {
return {
num,
id: legacy.id,
trigger: -1,
type: this.getTileType(num, legacy),
pass: this.getPass(num, legacy)
};
}
}

View File

@ -700,7 +700,7 @@ export interface IEnemyContext<TEnemy, THero> extends IReadonlyEnemyContext<
*
* @param system
*/
attachDamageSystem(system: IDamageSystem<TEnemy, THero> | null): void;
attachDamageSystem(system: IDamageSystem<TEnemy, unknown> | null): void;
/**
*

View File

@ -199,7 +199,6 @@
"139": "Expected $1 binding before start battle flow, but got a null object.",
"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.",
"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."
"142": "Expected a specific tile number after convert id '$1' to number, but got null."
}
}

View File

@ -7,29 +7,30 @@
3. **以目的驱动,而非以接口驱动**:实现前先想清楚我为什么要这样设计接口、这个接口设计的目的是什么,而不是单纯地以将接口填满为目标。
4. **遇到歧义立即提问**:若思考或实现时遇到任何问题——例如描述模糊、接口不清晰、某些地方存在歧义等——应立即向我提问,而不是按自己的想法去写。
5. **接口缺失时停止并提问**:若完成需求所需的接口尚不存在,应立即停止实现,向我提出疑问,而不是擅自新增接口来推进。
6. **按我说的方式重构**:重构时**不要**擅自新增任何参数、方法或接口,**不要**仅通过新增兼容层应对重构。
- **彻底性重构**(新旧接口完全没有重合):按正常方式全新实现,旧代码仅作逻辑参考。
- **结构性重构**(新旧接口基本一致,细节有差距):将旧代码搬移到新接口上后进行微调。
7. **不要有任何"顺手"的想法**:任何时候,都不要出现顺手的想法,包括但不限于发现了一个 bug 然后**顺手**修复、发现一处类型错误然后**顺手**修复等,这种情况下应当遵循规则 1。
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. **按设计实现,勿自由发挥**:我已想好功能的基本思路和代码结构,你的任务是还原它,而非自行创造。
6. **按我说的方式重构**:若目标是重构某个接口,按照我指定的方式执行:
- **彻底性重构**(新旧接口完全没有重合):按正常方式全新实现,旧代码仅作逻辑与思路上的参考。
- **结构性重构**(新旧接口基本一致,细节有差距):将旧代码搬移到新接口上后进行微调。**不要**擅自新增任何参数、方法或接口,**不要**仅通过新增兼容层的方式应对重构。
7. **不要有任何“顺手”的想法**:任何时候,都不要出现顺手的想法,包括但不限于发现了一个 bug然后**顺手**修复、发现一处类型错误,然后**顺手**修复等,这种情况下应当遵循规则 1。
8. **写完代码后说明修改内容**:写完代码后,必须在对话中说明自己修改了哪些内容,包括所有的文件及修改了每个文件的哪些东西。
9. **关于 `dev.md` 中的一些补充**:为了提高代码质量和可读性,所有包含“一般不建议”或“尽量避免”等字样的,在此处要求为绝对不能使用,如果必须使用,需在使用前向我确认,`as` 关键字除外。所有包含“建议怎么做”或“最好怎么做”的内容,在此处要求为必须使用。
10. **关于文件修改**:任何时候要进行文件修改时,务必重新阅读文件以确保上下文中的文件是最新版,因为我可能会在两次对话间修改文件。
11. **参数最小化 / 级联获取**:不从外部传入可通过已有引用获取的对象。若对象已持有对父对象或相关对象的引用(如 `controller`),应直接通过该引用获取所需数据(如 `controller.state`),不应再单独传递参数。即“非必要不传参”。
12. **转换下沉**:数据的格式或类型转换(如 `string` 的 id 转 `number`)应放在最终消费该值的对象中完成,而非在调用方预先转换后再传入。这样可以减少调用方的职责并避免重复转换。
13. **通过公有接口操作,不直接赋值内部属性**:对某个对象内部状态的修改,必须通过该对象对外暴露的公有方法进行(如用 `mover.face(dir).start()` 设置朝向并发起移动),而不是通过类型断言访问其内部属性直接赋值(如 `mover.faceDirection = dir`)。
14. **map/filter 优于 for 循环**:对数组进行数据收集或转换时(如收集 Promise使用 `map`、`filter` 等函数式写法,比 `for` 循环 + `push` 的可读性更高。
# 建议规则
以下规则为建议性,尽量遵守,特殊情况下可灵活处理。
1. **参考但不盲从建议**:我给出的实现建议可作为方向参考,但不应不加思考地照搬
1. **合理参考建议**:我有时会在对话中给出实现建议,应合理参考,切忌滥用
2. **以类型标注为参考依据**:实现与类型标注有冲突时,以类型标注(一般是 `types.ts`)中的内容为准。
3. **发现接口问题时提问**:若认为类型标注中的接口设计有问题,或在实现中发现缺少某些接口,应向我提问是否添加,经我同意后方可添加。
4. **接口设计兼顾合理性与便捷性**:设计接口时不仅要考虑合理性,还要考虑使用便捷性。罕见场景应当被支持,但不应与常见场景共用同一接口——这只会增加常见场景的使用难度。
5. **避免多余的非空判断与类型守卫**:类型已满足约束时不应再额外判断或过滤。例如 `Promise.all` 接受 `(Promise<unknown> | void)[]`,无需写成 `Promise.all(arr.filter(v => !!v))`
6. **关于整个仓库的类型检查**:单个需求**不需要**跑全仓库的类型检查,仅在重构时需要。
5. **避免多余的非空判断与类型守卫**:若某个类型已满足目标的类型约束,不应再对其添加任何判断或过滤操作。典型例子:`Promise.all` 对数组元素类型没有任何限制,传入 `(Promise<unknown> | void)[]` 完全合法,无需多此一举地写成 `Promise.all(arr.filter(v => !!v))`
6. **关于整个仓库的类型检查**:一般情况下**不需要**跑全仓库的类型检查,每个需求都是独立的,不会影响外部内容。只有在进行重构的时候才有此需求。
**时刻谨记上述要求,避免一个需求反复修改仍无法满足预期。**
# 开发流程
@ -40,12 +41,7 @@
## 示例文档
对于新增接口/彻底性地重构接口,按照以下格式编写,其余需求自行组织。
- 如有需要可单独开设标题章节详述。
- 我会使用 `>` 引用块在文档中批注,因此**不要在文档中使用引用块**。
- 文档控制在 100-250 行,简洁但包含所有必要信息,不擅自修改示例文档格式。
- 一般不需要流程图;若必须画,使用 `mermaid`
对于新增接口/彻底性地重构接口按照以下格式编写其余需求按照自己的理解去写即可。如某部分需要详细描述可单独开设标题若某个接口内容较多也可以在文档中为其单独开一个章节进行讲述。我会使用引用块的形式在文档中提出建议或回答。Markdown 文档不需要刻意换行,我的编辑器有自动换行功能,正常写没有问题。文档保证简洁,不要过于啰嗦,但必须包含所有的必要信息,控制在 100-250 行,格式清晰,不要擅自修改示例文档的格式。一般情况下不需要画任何流程图,如果必须要画,使用 `mermaid` 图。不要在文档中使用引用块,因为这是我在给文档添加批注时使用的东西。
```md
# 需求综述
@ -54,11 +50,11 @@
# 接口设计分析
这是相当重要的一章。按接口逐一分析成员与方法的预期使用频率(高/中/低,指**用户编写此调用的频率**而非运行时频率),中高频成员需列出至少一个典型使用场景
这是相当重要的一章。需要按接口逐一列出其方法与成员,分析每个成员和方法的预期使用频率(高 / 中 / 低)并说明判断依据;对于中频和高频成员,还需列出至少一个典型使用场景。必须认真分析频率,想明白**用户**调用的频率,而不是**引擎**调用的频率,比如战斗函数在引擎中可能是中频,但是对用户来说,用户一般会使用系统默认行为来战斗,而**不会**自己调用,因此属于低频
**命名长度原则**高频 -> 一个单词;中频 -> 不超过三个单词;低频 -> 可稍长但不宜过长。
**命名长度原则**频率越高,成员名应越短。高频成员以一个单词为宜,最多不超过两个单词;中频成员不超过三个单词;低频成员可以稍长,但不宜过长。
我可能不会给完整接口设计,你需要自行分析需求补充设计,并在对话中明确指出哪些接口是你补充的
**频率的定义**:此处频率指**用户编写此调用的可能性或频率**,而非运行时的调用频率。例如某成员每秒被调用千次,但整个游戏中只在一处出现过,仍属低频
示例如下:
@ -85,7 +81,7 @@
### 可能风险
若需修改已有接口(即本次新增之外的接口),在此写明修改风险(如类型污染、其他接口出错等)。只写修改已有接口的风险,不写实现风险(实现风险放到问题节)。无风险则跳过此章节。
在任何时候,需要修改已有接口时,必须在文档中写明修改这一接口的风险。这里的已有接口指的是本次设计新增接口之外的接口,即所有已存在于代码中的接口。本章节**只写**关于修改已有接口可能导致的风险,例如导致大面积的类型污染、或部分其他接口会出现类型错误等,**不写任何关于实现的风险**,关于实现的风险应该写到最后的问题节,而不是这里。一些细小的内容不要写到这。如果没有任何风险,不要写无风险,直接跳过此章节。
# 实现思路

View File

@ -1282,7 +1282,6 @@ interface MapDataOf<T extends keyof NumberToId> {
*/
cls: ClsOf<NumberToId[T]>;
noPass?: boolean;
bigImage?: ImageIds;
faceIds?: Record<Dir, AllIds>;
animate?: number;