diff --git a/packages-user/data-base/src/hero/location.ts b/packages-user/data-base/src/hero/location.ts index f949542..2165766 100644 --- a/packages-user/data-base/src/hero/location.ts +++ b/packages-user/data-base/src/hero/location.ts @@ -1,9 +1,22 @@ -import { IFacedTileLocator } from '@motajs/common'; +import { + Hookable, + HookController, + IFacedTileLocator, + IHookController +} from '@motajs/common'; import { FaceDirection, IDataCommon, IFaceHandler } from '@user/data-common'; import { HeroMover } from './mover'; -import { IHeroLocation, IHeroLocationSave, IHeroMover } from './types'; +import { + IHeroLocation, + IHeroLocationHook, + IHeroLocationSave, + IHeroMover +} from './types'; -export class HeroLocation implements IHeroLocation { +export class HeroLocation + extends Hookable + implements IHeroLocation +{ x: number; y: number; floorId: string | undefined = undefined; @@ -16,6 +29,7 @@ export class HeroLocation implements IHeroLocation { loc: IFacedTileLocator, faceHandler: IFaceHandler ) { + super(); this.state = state; this.x = loc.x; this.y = loc.y; @@ -24,9 +38,21 @@ export class HeroLocation implements IHeroLocation { this.mover = mover; } + protected createController( + hook: Partial + ): IHookController { + return new HookController(this, hook); + } + + setFloor(floorId: string | undefined): void { + this.floorId = floorId; + this.forEachHook(hook => hook.onSetFloor?.(floorId)); + } + setPos(x: number, y: number): void { this.x = x; this.y = y; + this.forEachHook(hook => hook.onSetPos?.(x, y)); } getCurrentFaceDirection(): FaceDirection { diff --git a/packages-user/data-base/src/hero/mover.ts b/packages-user/data-base/src/hero/mover.ts index baa3ef7..3bb6a35 100644 --- a/packages-user/data-base/src/hero/mover.ts +++ b/packages-user/data-base/src/hero/mover.ts @@ -40,7 +40,7 @@ export class HeroMover readonly tile: T, faceHandler: IFaceHandler ) { - super(faceHandler); + super(faceHandler, tile.getCurrentFaceDirection()); this.state = tile.state; } diff --git a/packages-user/data-base/src/hero/state.ts b/packages-user/data-base/src/hero/state.ts index 75e4165..b1ceaed 100644 --- a/packages-user/data-base/src/hero/state.ts +++ b/packages-user/data-base/src/hero/state.ts @@ -15,7 +15,9 @@ import { IHeroState, IHeroStateSave, IModifierStateSave, - IReadonlyHeroAttribute + IReadonlyHeroAttribute, + IHeroChangeFloorInfo, + IHeroStateHook } from './types'; import { FaceDirection, @@ -23,9 +25,18 @@ import { IFaceHandler, SaveCompression } from '@user/data-common'; -import { IFacedTileLocator, logger } from '@motajs/common'; +import { + Hookable, + HookController, + IFacedTileLocator, + IHookController, + logger +} from '@motajs/common'; -export class HeroState implements IHeroState { +export class HeroState + extends Hookable + implements IHeroState +{ /** 修饰器工厂函数注册表 */ private readonly registry: Map< string, @@ -43,6 +54,7 @@ export class HeroState implements IHeroState { faceHandler: IFaceHandler, public attribute: IHeroAttribute ) { + super(); this.rendering = new HeroRendering(state); const defaultLoc: IFacedTileLocator = { x: 0, @@ -59,8 +71,10 @@ export class HeroState implements IHeroState { this.equip = new HeroEquipment(this.items.equipment, this.attribute); } - attachAttribute(attribute: IHeroAttribute): void { - this.attribute = attribute; + protected createController( + hook: Partial + ): IHookController { + return new HookController(this, hook); } getLocation(): IFacedTileLocator { @@ -71,6 +85,12 @@ export class HeroState implements IHeroState { }; } + //#region 属性相关 + + attachAttribute(attribute: IHeroAttribute): void { + this.attribute = attribute; + } + getModifiableAttribute(): IHeroAttribute { return this.attribute; } @@ -109,6 +129,22 @@ export class HeroState implements IHeroState { return modifier; } + //#endregion + + async changeFloor(info: IHeroChangeFloorInfo): Promise { + await Promise.all( + this.forEachHook(hook => hook.onBeforeChangeFloor?.(info)) + ); + const mover = this.location.mover; + mover.setFaceDir(info.face); + mover.setMoveDir(info.face); + mover.setPos(info.x, info.y); + this.location.setFloor(info.target); + await Promise.all( + this.forEachHook(hook => hook.onAfterChangeFloor?.(info)) + ); + } + saveState(compression: SaveCompression): IHeroStateSave { const modifiers: IModifierStateSave[] = []; for (const [name, modifier] of this.attribute.iterateModifiers()) { diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index 9801841..fbedf1c 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -227,6 +227,21 @@ export interface IHeroAttribute extends IReadonlyHeroAttribute { //#region 勇士位置 +export interface IHeroLocationHook extends IHookBase { + /** + * 当设置勇士位置时触发 + * @param x 设置为的横坐标 + * @param y 设置为的纵坐标 + */ + onSetPos?(x: number, y: number): void; + + /** + * 当设置勇士所处楼层时触发 + * @param floorId 设置为的楼层 id,undefined 表示尚不处于任何楼层 + */ + onSetFloor?(floorId: string | undefined): void; +} + export interface IHeroLocationSave { /** 当前横坐标 */ readonly x: number; @@ -242,11 +257,18 @@ export interface IHeroLocation extends ISaveableContent, IObjectMovable, + IHookable, IDataCommonExtended { /** 当前所在楼层 id,undefined 表示尚不处于任何楼层 */ readonly floorId: string | undefined; /** 勇士的移动对象 */ readonly mover: IHeroMover; + + /** + * 设置勇士所在的楼层 id,注意此方法会引起数据变化,但是不会产生楼层切换动画 + * @param floorId 目标楼层 id + */ + setFloor(floorId: string | undefined): void; } //#endregion @@ -728,6 +750,33 @@ export interface IHeroEquipment //#region 勇士状态 +export interface IHeroChangeFloorInfo { + /** 要切换至的目标楼层 */ + readonly target: string; + /** 要切换至的目标位置横坐标 */ + readonly x: number; + /** 要切换至的目标位置纵坐标 */ + readonly y: number; + /** 要切换至的目标朝向 */ + readonly face: FaceDirection; +} + +export interface IHeroStateHook extends IHookBase { + /** + * 当勇士切换楼层前触发,此钩子执行完毕后会立刻触发 `IHeroLocation` 的 `onSetFloor` 钩子。 + * 此钩子应该用于切换楼层前的过渡及必要的数据准备,不应该修改勇士位置相关的数据。 + * @param info 勇士切换楼层信息对象 + */ + onBeforeChangeFloor?(info: IHeroChangeFloorInfo): Promise; + + /** + * 当勇士切换楼层后触发,此钩子会在 `IHeroLocation` 的 `onSetFloor` 钩子执行完毕后立刻触发。 + * 此钩子应该用于切换楼层后的过渡及必要的数据处理,不应该修改勇士位置相关的数据。 + * @param info 勇士切换楼层信息对象 + */ + onAfterChangeFloor?(info: IHeroChangeFloorInfo): Promise; +} + export interface IHeroStateSave { /** 勇士属性状态 */ readonly attribute: THero; @@ -745,9 +794,8 @@ export interface IHeroStateSave { readonly equip: IHeroEquipmentSave; } -export interface IHeroState extends ISaveableContent< - IHeroStateSave -> { +export interface IHeroState + extends ISaveableContent>, IHookable { /** 勇士移动对象 */ readonly location: IHeroLocation; /** 勇士属性对象 */ @@ -809,6 +857,12 @@ export interface IHeroState extends ISaveableContent< type: string, name: K ): IHeroModifier | null; + + /** + * 切换勇士所在楼层 + * @param info 楼层切换信息对象 + */ + changeFloor(info: IHeroChangeFloorInfo): Promise; } //#endregion diff --git a/packages-user/data-base/src/map/index.ts b/packages-user/data-base/src/map/index.ts index 55e9fdb..4ccc629 100644 --- a/packages-user/data-base/src/map/index.ts +++ b/packages-user/data-base/src/map/index.ts @@ -1,5 +1,5 @@ export * from './layerState'; export * from './mapLayer'; -export * from './mapStore'; +export * from './mapState'; export * from './mover'; export * from './types'; diff --git a/packages-user/data-base/src/map/mapStore.ts b/packages-user/data-base/src/map/mapState.ts similarity index 90% rename from packages-user/data-base/src/map/mapStore.ts rename to packages-user/data-base/src/map/mapState.ts index aef9cf6..5038447 100644 --- a/packages-user/data-base/src/map/mapStore.ts +++ b/packages-user/data-base/src/map/mapState.ts @@ -1,5 +1,5 @@ import { logger } from '@motajs/common'; -import { IDataCommon, SaveCompression } from '@user/data-common'; +import { IDataCommon, IMapRawData, SaveCompression } from '@user/data-common'; import { ITileStore } from '@user/data-common'; import { ILayerState, @@ -11,7 +11,7 @@ import { MapArea } from './types'; import { LayerState } from './layerState'; -import { uniq } from 'lodash-es'; +import { isNil, uniq } from 'lodash-es'; export class MapState implements IMapState { /** 楼层 id 到状态对象的映射 */ @@ -39,6 +39,56 @@ export class MapState implements IMapState { //#region 楼层管理 + fromRaw(raw: IMapRawData): ILayerState | null { + let length = 0; + const entries = Object.entries(raw.map); + for (const [_, map] of Object.entries(raw.map)) { + if (length > 0 && map.length !== length) { + logger.error(60, map.length.toString(), length.toString()); + return null; + } else { + length = map.length; + } + } + if (length % raw.width !== 0) { + logger.error(61, length.toString(), raw.width.toString()); + return null; + } + + // 设置楼层图块数据 + const height = length / raw.width; + const state = this.createLayerState(raw.floorId, raw.width, height); + for (const [zIndex, map] of entries) { + const z = Number(zIndex); + if (isNaN(z)) { + logger.error(62, 'IMapRawData.map', String(zIndex)); + continue; + } + const layer = state.addLayer(); + const alias = raw.layerAlias[z]; + layer.setMapRef(new Uint32Array(map)); + layer.setZIndex(z); + state.setLayerAlias(layer, alias); + + // 设置图块静态触发器 + const extra = raw.blockData[z]; + for (const [index, data] of Object.entries(extra)) { + const indexNum = Number(index); + if (isNaN(indexNum)) { + logger.error(62, 'IMapRawData.blockData', String(index)); + continue; + } + if (!isNil(data.trigger)) { + const x = indexNum % raw.width; + const y = Math.floor(indexNum / raw.width); + layer.setTriggerType(data.trigger, x, y); + } + } + } + + return state; + } + createLayerState(id: string, width: number, height: number): ILayerState { if (this.mapData.has(id)) { logger.warn(121, id); diff --git a/packages-user/data-base/src/map/mover.ts b/packages-user/data-base/src/map/mover.ts index a603865..8215c09 100644 --- a/packages-user/data-base/src/map/mover.ts +++ b/packages-user/data-base/src/map/mover.ts @@ -18,7 +18,7 @@ const enum DynamicMoveCode { export class DynamicTileMover extends ObjectMover { constructor(public readonly tile: IDynamicTile) { const face = tile.state.faceManager; - super(face.get(DYNAMIC_MOVER_FACE)!); + super(face.get(DYNAMIC_MOVER_FACE)!, tile.getCurrentFaceDirection()); } protected onMoveStart(): Promise { diff --git a/packages-user/data-base/src/map/types.ts b/packages-user/data-base/src/map/types.ts index 86080e8..99c05f2 100644 --- a/packages-user/data-base/src/map/types.ts +++ b/packages-user/data-base/src/map/types.ts @@ -1,7 +1,14 @@ -import { IHookable, IHookBase, IHookController } from '@motajs/common'; +import { + IHookable, + IHookBase, + IHookController, + ITileLocator +} from '@motajs/common'; import { FaceDirection, IDataCommonExtended, + IMapBlockRawData, + IMapRawData, IMoverController, IObjectMovable, IObjectMover, @@ -21,6 +28,8 @@ export interface IMapLayerData { } export interface ILayerLocation { + /** 该点的位置 */ + readonly locator: ITileLocator; /** 该点的静态图块数字 */ readonly tile: number; /** 该点的静态图块信息 */ @@ -29,6 +38,8 @@ export interface ILayerLocation { readonly trigger: number; /** 该点包含的所有动态图块 */ readonly dynamics: Iterable; + /** 该点的静态图块原始数据 */ + readonly block: IMapBlockRawData; } export interface IMapLayerHooks extends IHookBase { @@ -442,6 +453,12 @@ export interface IMapState */ getActiveMap(id: string): ILayerState | null; + /** + * 从楼层原始数据生成楼层状态 + * @param raw 楼层原始数据 + */ + fromRaw(raw: IMapRawData): ILayerState | null; + /** * 创建并注册一个空白楼层,若 id 已存在则警告并覆盖,返回楼层状态对象 * @param id 楼层 id diff --git a/packages-user/data-common/src/common/mover.ts b/packages-user/data-common/src/common/mover.ts index 65b8546..9df55a5 100644 --- a/packages-user/data-common/src/common/mover.ts +++ b/packages-user/data-common/src/common/mover.ts @@ -371,9 +371,13 @@ export abstract class ObjectMover readonly faceHandler: IFaceHandler; - constructor(faceHandler: IFaceHandler) { + constructor( + faceHandler: IFaceHandler, + direction: FaceDirection + ) { super(); this.faceHandler = faceHandler; + this.faceDirection = direction; } protected createController( @@ -651,7 +655,6 @@ export abstract class ObjectMover this.clear(); this.shouldStop = false; - this.faceDirection = this.tile.getCurrentFaceDirection(); this.moveDirection = FaceDirection.Unknown; const { promise, resolve } = Promise.withResolvers(); diff --git a/packages-user/data-common/src/store/types.ts b/packages-user/data-common/src/store/types.ts index 3b1798f..5caa893 100644 --- a/packages-user/data-common/src/store/types.ts +++ b/packages-user/data-common/src/store/types.ts @@ -1,5 +1,7 @@ //#region tile +import { IFacedTileLocator } from '@motajs/common'; + export const enum TileType { /** 未知或尚未归类的图块 */ Unknown, @@ -253,6 +255,54 @@ export interface IItemStore { //#region map +export const enum ChangeFloorType { + /** 指定具体位置的楼层切换方式 */ + Specified, + /** 仅指定相对关系的楼层切换方式 */ + Related +} + +export const enum ChangeFloorTarget { + /** 切换至前一层 */ + Previous, + /** 切换至后一层 */ + Next +} + +export const enum ChangeFloorPos { + /** 原地不动 */ + Stand, + /** 左右对称位置 */ + SymmetryX, + /** 上下对称位置 */ + SymmetryY, + /** 中心对称位置 */ + CentralSymmetry +} + +export interface IChangeFloorData { + /** 切换至的楼层 id 的切换方式 */ + readonly floorType: ChangeFloorType; + /** 指定具体位置的切换楼层方式下,切换至的具体楼层 */ + readonly targetFloor?: string; + /** 相对关系的楼层切换方式下,切换至的楼层代号 */ + readonly relatedFloor?: ChangeFloorTarget; + + /** 切换至的目标位置的切换方式 */ + readonly posType: ChangeFloorType; + /** 指定具体位置的位置切换方式下,切换至的具体位置,朝向填写 `FaceDirection.Unknown` 视为维持 */ + readonly targetPos?: IFacedTileLocator; + /** 相对关系的位置切换方式下,切换至的位置代号 */ + readonly relatedPos?: ChangeFloorPos; +} + +export interface IMapBlockRawData { + /** 此点的静态触发器类型(仅对该点生效,不会跟随任何图块移动) */ + readonly trigger?: number; + /** 楼层切换信息 */ + readonly changeFloor?: IChangeFloorData; +} + export interface IMapRawData { /** 楼层 id */ readonly floorId: string; @@ -262,6 +312,8 @@ export interface IMapRawData { readonly map: Record; /** 每个地图图层的字符串别名 */ readonly layerAlias: Record; + /** 每个地图图层中,指定位置图块的额外数据,外层 key 为图层 zIndex,内层 key 为图块位置索引 */ + readonly blockData: Record>; } export interface IMapStore { diff --git a/packages-user/data-state/src/content/triggers.ts b/packages-user/data-state/src/content/triggers.ts new file mode 100644 index 0000000..eae22e1 --- /dev/null +++ b/packages-user/data-state/src/content/triggers.ts @@ -0,0 +1,162 @@ +import { IFacedTileLocator, logger } from '@motajs/common'; +import { + IHeroChangeFloorInfo, + IHeroState, + IMapLayer, + IMapState, + IStateBase +} from '@user/data-base'; +import { + ChangeFloorPos, + ChangeFloorTarget, + ChangeFloorType, + FaceDirection, + IChangeFloorData, + IHeroAttr +} from '@user/data-common'; +import { + BaseTrigger, + ITrigger, + ITriggerCollection, + ITriggerHandler, + TriggerCollection +} from '@user/data-system'; +import { isNil } from 'lodash-es'; + +export const enum TriggerType { + /** 楼层切换触发器 */ + ChangeFloor +} + +export class ChangeFloorTrigger extends BaseTrigger implements ITrigger { + readonly type: number = TriggerType.ChangeFloor; + readonly priority: number = 10; + + constructor( + state: IStateBase, + readonly maps: IMapState, + readonly hero: IHeroState + ) { + super(state); + } + + /** + * 获取楼层切换的目标楼层 + * @param data 楼层切换数据 + */ + private getFloorTarget(data: IChangeFloorData): string | undefined { + if (data.floorType === ChangeFloorType.Specified) { + if (isNil(data.targetFloor)) { + logger.warn(165); + return void 0; + } + return data.targetFloor; + } else { + if (isNil(data.relatedFloor)) { + logger.warn(166); + return void 0; + } + const curr = this.hero.location.floorId; + if (isNil(curr)) { + logger.warn(167); + return void 0; + } + const index = this.maps.maps.indexOf(curr); + if (index === -1) { + logger.warn(168); + return void 0; + } + if (data.relatedFloor === ChangeFloorTarget.Next) { + const next = this.maps.maps[index + 1]; + if (isNil(next)) { + logger.warn(169); + return void 0; + } + return next; + } else { + const next = this.maps.maps[index - 1]; + if (isNil(next)) { + logger.warn(169); + return void 0; + } + } + } + } + + private getPosTarget( + data: IChangeFloorData, + layer: IMapLayer + ): IFacedTileLocator | undefined { + if (data.posType === ChangeFloorType.Specified) { + if (!data.targetPos) { + logger.warn(165); + return void 0; + } + return data.targetPos; + } else { + if (isNil(data.relatedPos)) { + logger.warn(166); + return void 0; + } + const { x, y, floorId } = this.hero.location; + if (isNil(floorId)) { + logger.warn(167); + return void 0; + } + const { width, height } = layer; + switch (data.relatedPos) { + case ChangeFloorPos.Stand: + return { + direction: FaceDirection.Unknown, + x, + y + }; + case ChangeFloorPos.SymmetryX: + return { + direction: FaceDirection.Unknown, + x: width - x - 1, + y + }; + case ChangeFloorPos.SymmetryY: + return { + direction: FaceDirection.Unknown, + x, + y: height - y - 1 + }; + case ChangeFloorPos.CentralSymmetry: + return { + direction: FaceDirection.Unknown, + x: width - x - 1, + y: height - y - 1 + }; + } + } + } + + async trigger(handler: ITriggerHandler): Promise { + if (!handler.layer || !handler.locator || !handler.mapLayer) { + logger.warn(164); + return; + } + const { x, y } = handler.locator; + const data = handler.mapLayer.getLocationData(x, y); + if (!data || !data.block.changeFloor) return; + const floor = this.getFloorTarget(data.block.changeFloor); + const pos = this.getPosTarget(data.block.changeFloor, handler.mapLayer); + if (isNil(floor) || isNil(pos)) return; + const hero = handler.state.hero; + const isUnknown = pos.direction === FaceDirection.Unknown; + const defaults = hero.location.mover.faceDirection; + const info: IHeroChangeFloorInfo = { + target: floor, + x: pos.x, + y: pos.y, + face: isUnknown ? defaults : pos.direction + }; + return handler.state.hero.changeFloor(info); + } + + collection(): ITriggerCollection { + return new TriggerCollection([this]); + } +} diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index 21b2c79..a029dab 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -225,7 +225,7 @@ export class CoreState implements ICoreState { this.initTileStore(core.maps.blocksInfo); this.initItemStore(core.items.items); this.initEnemyManager(enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80); - this.initMapStore( + this.initMapState( core.floorIds, core.floors as Record ); @@ -323,7 +323,7 @@ export class CoreState implements ICoreState { manager.compareWith(reference); } - private initMapStore( + private initMapState( floors: FloorIds[], data: Record ) { diff --git a/packages-user/data-system/src/trigger/index.ts b/packages-user/data-system/src/trigger/index.ts index c5307a9..2e136f1 100644 --- a/packages-user/data-system/src/trigger/index.ts +++ b/packages-user/data-system/src/trigger/index.ts @@ -1,4 +1,5 @@ export * from './collection'; export * from './collector'; export * from './registry'; +export * from './trigger'; export * from './types'; diff --git a/packages-user/data-system/src/trigger/trigger.ts b/packages-user/data-system/src/trigger/trigger.ts new file mode 100644 index 0000000..fad56a3 --- /dev/null +++ b/packages-user/data-system/src/trigger/trigger.ts @@ -0,0 +1,16 @@ +import { IStateBase } from '@user/data-base'; +import { ITrigger, ITriggerCollection, ITriggerHandler } from './types'; +import { TriggerCollection } from './collection'; + +export abstract class BaseTrigger implements ITrigger { + abstract type: number; + abstract priority: number; + + constructor(readonly state: IStateBase) {} + + abstract trigger(handler: ITriggerHandler): Promise; + + collection(): ITriggerCollection { + return new TriggerCollection([this]); + } +} diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index a1026d3..e929598 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -58,7 +58,10 @@ "56": "Cannot convert legacy tile data since no tile legacy converter is attached to TileStore.", "57": "Cannot convert legacy item data since no item legacy converter is attached to ItemStore.", "58": "Cannot find max equipment uid while loading equipment store.", - "59": "Cannot find item raw data for tile num $1." + "59": "Cannot find item raw data for tile num $1.", + "60": "Cannot create layer state from raw data since the area of every layer of the map does not equal: $1 vs $2.", + "61": "Cannot create layer state from raw data since map area cannot be divided by width evenly: area = $1 while divider = $2.", + "62": "Expected a number for the key of $1, but got $2." }, "warn": { "1": "Resource with type of 'none' is loaded.", @@ -223,6 +226,12 @@ "160": "Replay safety collection ending function is called outside replay safety collection unexpectedly.", "161": "Replay safety detected. Followings are simplified detect info. To get detailed info, run: '$1'.\n$2", "162": "Cannot find replay safety detail info of code $1. The detail info may expired, or it just has not been existed.", - "163": "Replay command code $1 has already been registered. This will not replace the old registry." + "163": "Replay command code $1 has already been registered. This will not replace the old registry.", + "164": "Change floor trigger needs specified locator.", + "165": "Target floor id or target position needs to be specified while change floor type is 'ChangeFloorType.Specified'.", + "166": "Related floor id or related position needs to be specified while change floor type is 'ChangeFloorType.Related'.", + "167": "Hero must at a specified floor before using related change floor method.", + "168": "The current floor of hero does not in the specified map list.", + "169": "The current floor is the last or the first floor in the map list, so related floor change method will not work." } }