diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index ecfeaa9..b19c2e2 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -311,13 +311,13 @@ export interface IHeroMoveTopImpl { inBound(x: number, y: number, floorId: string | undefined): boolean; /** - * 勇士正常移动到某个图块上时触发进入触发器 + * 勇士正常移动到某个图块上时触发进入触发器,此时勇士位置处于目标位置 * @param handler 移动信息对象 */ enter(handler: IHeroMoveTopHandler): Promise; /** - * 勇士将要离开当前图块时触发离开触发器 + * 勇士将要离开当前图块时触发离开触发器,此时勇士位置处于目标位置 * @param handler 移动信息对象 */ leave(handler: IHeroMoveTopHandler): Promise; diff --git a/packages-user/data-base/src/map/gameMap.ts b/packages-user/data-base/src/map/gameMap.ts index 98dcc82..024ad75 100644 --- a/packages-user/data-base/src/map/gameMap.ts +++ b/packages-user/data-base/src/map/gameMap.ts @@ -48,6 +48,7 @@ export class GameMap extends Hookable implements IGameMap { constructor( public readonly state: IDataCommon, public readonly tileStore: ITileStore, + public readonly floorId: string, public width: number, public height: number ) { diff --git a/packages-user/data-base/src/map/mapLayer.ts b/packages-user/data-base/src/map/mapLayer.ts index be45f3e..e626454 100644 --- a/packages-user/data-base/src/map/mapLayer.ts +++ b/packages-user/data-base/src/map/mapLayer.ts @@ -234,6 +234,7 @@ export class MapLayer } setBlock(block: number, x: number, y: number): void { + if (!this.inMap(x, y)) return; const index = y * this.width + x; if (block === this.mapArray[index]) return; this.mapArray[index] = block; @@ -253,6 +254,15 @@ export class MapLayer return this.mapArray[y * this.width + x]; } + removeBlock(x: number, y: number): number { + if (!this.inMap(x, y)) { + return -1; + } + const before = this.getBlock(x, y); + this.setBlock(0, x, y); + return before; + } + getTile(x: number, y: number): IStaticTile | null { if (!this.inMap(x, y)) return null; const index = this.map.indexer.locToIndex(x, y); diff --git a/packages-user/data-base/src/map/mapState.ts b/packages-user/data-base/src/map/mapState.ts index 2ef4a97..d1ccf20 100644 --- a/packages-user/data-base/src/map/mapState.ts +++ b/packages-user/data-base/src/map/mapState.ts @@ -252,7 +252,8 @@ export class MapState implements IMapState { } else { this.maps.push(id); } - const state = new GameMap(this.state, this.tileStore, width, height); + const tile = this.tileStore; + const state = new GameMap(this.state, tile, id, width, height); // 若已设置参考基准,新楼层直接视为全脏 if (this.compared) { state.markDirty(true); diff --git a/packages-user/data-base/src/map/types.ts b/packages-user/data-base/src/map/types.ts index 6a44392..8e724c1 100644 --- a/packages-user/data-base/src/map/types.ts +++ b/packages-user/data-base/src/map/types.ts @@ -275,7 +275,7 @@ export interface IMapLayerHookController extends IHookController interface ILayerStatic { /** - * 设置某一点的图块,会标记图层为脏 + * 设置某一点的静态图块,会标记图层为脏 * @param block 图块数字 * @param x 图块横坐标 * @param y 图块纵坐标 @@ -283,13 +283,20 @@ interface ILayerStatic { setBlock(block: number, x: number, y: number): void; /** - * 获取指定点的图块数字 + * 获取指定点的静态图块数字 * @param x 图块横坐标 * @param y 图块纵坐标 * @returns 指定点的图块,如果没有图块,返回 0,如果不在地图上,返回 -1 */ getBlock(x: number, y: number): number; + /** + * 移除某一点的静态图块 + * @param x 图块横坐标 + * @param y 图块纵坐标 + */ + removeBlock(x: number, y: number): number; + /** * 获取指定点的静态图块实例 * @param x 图块横坐标 @@ -365,7 +372,7 @@ interface ILayerDynamic { * 从静态图层读取并清除指定位置的图块,创建对应动态图块并返回 * @param x 横坐标 * @param y 纵坐标 - * @param keepEvent 是否将静态图块的事件保留至动态图块,不论是否保留,此格的静态事件都会被清空 + * @param keepEvent 是否将静态图块的图块事件保留至动态图块,不论是否保留,此格的静态图块事件都会被清空 */ transferToDynamic( x: number, @@ -376,7 +383,7 @@ interface ILayerDynamic { /** * 将动态图块还原为静态图块 * @param tile 要还原的动态图块 - * @param keepEvent 是否保留动态图块的事件至静态图块,若不保留,那么此格将回退为静态图块本身的事件 + * @param keepEvent 是否保留动态图块的图块事件至静态图块,若不保留,那么此格将回退为静态图块原始的图块事件 */ transferToStatic( tile: IDynamicTile, @@ -386,7 +393,7 @@ interface ILayerDynamic { /** * 仅当目标位置不存在静态图块时才还原为静态图块,否则不转换 * @param tile 要还原的动态图块 - * @param keepEvent 是否保留动态图块的事件至静态图块,若不保留,那么此格将回退为静态图块本身的事件 + * @param keepEvent 是否保留动态图块的图块事件至静态图块,若不保留,那么此格将回退为静态图块原始的图块事件 */ transferToStaticIfSafe( tile: IDynamicTile, @@ -626,6 +633,8 @@ export interface IGameMap readonly height: number; /** 当前楼层的默认事件层 */ readonly eventLayer: IMapLayer | null; + /** 当前楼层的楼层 id */ + readonly floorId: string; /** * 添加图层,使用楼层预设的宽高 diff --git a/packages-user/data-common/src/common/mover.ts b/packages-user/data-common/src/common/mover.ts index b686df0..43a0ad8 100644 --- a/packages-user/data-common/src/common/mover.ts +++ b/packages-user/data-common/src/common/mover.ts @@ -334,7 +334,7 @@ export interface IObjectMover extends IHookable< * 直接在当前的移动规划后追加新的移动步 * @param steps 要追加的移动步 */ - push(steps: ObjectMoveStep[]): this; + push(steps: readonly ObjectMoveStep[]): this; /** * 清空尚未执行的步骤队列 diff --git a/packages-user/data-state/src/event/event.test.ts b/packages-user/data-state/src/event/event.test.ts index a44c971..633721e 100644 --- a/packages-user/data-state/src/event/event.test.ts +++ b/packages-user/data-state/src/event/event.test.ts @@ -8,20 +8,16 @@ import { EventTrigger, TileType } from '@user/data-common'; -import { - BlockEventType, - IBlockEventEnv, - IGameEventInvocation -} from '@user/data-system'; +import { BlockEventType, IBlockEventEnv } from '@user/data-system'; import { CoreState } from '../core'; import { createEventRegistrations, - EventDeleteBlock, + EventRemoveBlock, EventInsertEvent, EventInsertEvents, EventMoveBlock, EventMoveHero, - EventMoveHeroStep, + EventStepHero, EventSetBlock, EventTouchFront } from './index'; @@ -86,6 +82,7 @@ function createFixture(): EventFixture { state, type: BlockEventType.CommonEvent, trigger: EventTrigger.None, + system: state.eventSystem, heroLocator: state.hero.getLocation(), heroFloor: 'F1', triggerLocator: null, @@ -113,10 +110,6 @@ function createEvent( }; } -function invocation(id: string, env: IBlockEventEnv): IGameEventInvocation { - return { id, env }; -} - describe('event built-ins', () => { // 验证真实注册项设置图块并安全跳过无效图块 it('sets a block and safely skips an unknown tile', async () => { @@ -155,17 +148,26 @@ describe('event built-ins', () => { expect(fixture.layer.getBlock(2, 0)).toBe(1); }); - // 验证真实注册项删除图块并清理静态与动态图块 - it('deletes static and dynamic blocks at a coordinate', async () => { + // 验证真实注册项移除静态图块,并可按 dynamic 选项一并移除动态图块 + it('removes static blocks and optionally dynamic blocks', async () => { const fixture = createFixture(); - fixture.layer.transferToDynamic(1, 0); + fixture.layer.setBlock(2, 1, 0); await invokeBuiltin( - getRegistration('deleteBlock'), + getRegistration('removeBlock'), { x: 1, y: 0 }, fixture.env ); expect(fixture.layer.getBlock(1, 0)).toBe(0); + + fixture.layer.setBlock(1, 1, 0); + fixture.layer.transferToDynamic(1, 0); + await invokeBuiltin( + getRegistration('removeBlock'), + { x: 1, y: 0, dynamic: true }, + fixture.env + ); expect([...fixture.layer.getDynamicTilesAt(1, 0)]).toHaveLength(0); + expect(fixture.layer.getBlock(1, 0)).toBe(0); }); // 验证真实注册项的勇士移动序列和向前一步都等待移动结束 @@ -179,25 +181,15 @@ describe('event built-ins', () => { fixture.env ); expect(fixture.state.hero.location.x).toBe(1); - await invokeBuiltin(getRegistration('moveHeroStep'), {}, fixture.env); + await invokeBuiltin(getRegistration('stepHero'), {}, fixture.env); expect(fixture.state.hero.location.x).toBe(2); }); - // 验证真实注册项触发面前 onTouch 且不移动勇士 - it('triggers front onTouch events without moving the hero', async () => { + // 验证真实注册项让勇士向前一步,前方撞击判定由移动器负责 + it('steps the hero forward through touchFront', async () => { const fixture = createFixture(); - const calls: IGameEventInvocation[] = []; - fixture.layer.event(1, 0)!.set(10, 'touch'); - fixture.state.eventStore.addEvent( - 'touch', - createEvent(fixture.state, EventTrigger.OnTouch, async env => { - calls.push(invocation('touch', env)); - }) - ); await invokeBuiltin(getRegistration('touchFront'), {}, fixture.env); - expect(calls).toHaveLength(1); - expect(calls[0].env.trigger).toBe(EventTrigger.OnTouch); - expect(fixture.state.hero.location.x).toBe(0); + expect(fixture.state.hero.location.x).toBe(1); }); // 验证真实注册项按顺序等待临时事件序列并直接执行语句体 @@ -244,9 +236,9 @@ describe('event built-ins', () => { expect(names).toEqual([ 'setBlock', 'moveBlock', - 'deleteBlock', + 'removeBlock', 'moveHero', - 'moveHeroStep', + 'stepHero', 'touchFront', 'insertEvents', 'insertEvent' @@ -281,7 +273,7 @@ describe('event built-ins', () => { ).resolves.toBeUndefined(); await expect( invokeBuiltin( - getRegistration('deleteBlock'), + getRegistration('removeBlock'), { x: 0, y: 0 }, missingEnv ) @@ -303,18 +295,18 @@ describe('event registration ownership', () => { expect(registrations.map(item => item.name)).toEqual([ 'setBlock', 'moveBlock', - 'deleteBlock', + 'removeBlock', 'moveHero', - 'moveHeroStep', + 'stepHero', 'touchFront', 'insertEvents', 'insertEvent' ]); expect(registrations[0]).toBeInstanceOf(EventSetBlock); expect(registrations[1]).toBeInstanceOf(EventMoveBlock); - expect(registrations[2]).toBeInstanceOf(EventDeleteBlock); + expect(registrations[2]).toBeInstanceOf(EventRemoveBlock); expect(registrations[3]).toBeInstanceOf(EventMoveHero); - expect(registrations[4]).toBeInstanceOf(EventMoveHeroStep); + expect(registrations[4]).toBeInstanceOf(EventStepHero); expect(registrations[5]).toBeInstanceOf(EventTouchFront); expect(registrations[6]).toBeInstanceOf(EventInsertEvents); expect(registrations[7]).toBeInstanceOf(EventInsertEvent); @@ -360,9 +352,9 @@ describe('event registration ownership', () => { 'createEventRegistrations', 'EventSetBlock', 'EventMoveBlock', - 'EventDeleteBlock', + 'EventRemoveBlock', 'EventMoveHero', - 'EventMoveHeroStep', + 'EventStepHero', 'EventTouchFront', 'EventInsertEvents', 'EventInsertEvent' diff --git a/packages-user/data-state/src/event/event.ts b/packages-user/data-state/src/event/event.ts index af02916..eed6a02 100644 --- a/packages-user/data-state/src/event/event.ts +++ b/packages-user/data-state/src/event/event.ts @@ -6,7 +6,6 @@ import { } from '@user/data-system'; import { IGameEventStore } from '@user/data-common'; import { IInsertEventEventParam, IInsertEventsEventParam } from './types'; -import { enterEventInsert, exitEventInsert, getEventExecutor } from './utils'; export class EventInsertEvents implements BuiltInFunction< IInsertEventsEventParam, @@ -17,19 +16,10 @@ export class EventInsertEvents implements BuiltInFunction< async func(param: IInsertEventsEventParam, env: IBlockEventEnv) { if (param.ids.length === 0) return; - const executor = getEventExecutor(env); - if (!executor) return; const store = env.state.eventStore; - if (!store) return; - - if (!enterEventInsert(env)) return; - try { - const invocations = this.collectInvocations(param.ids, env, store); - if (invocations.length === 0) return; - await executor.execute(invocations, { custom: {} }); - } finally { - exitEventInsert(env); - } + const invocations = this.collectInvocations(param.ids, env, store); + if (invocations.length === 0) return; + await env.system.executor.execute(invocations, { custom: {} }); } /** @@ -42,13 +32,9 @@ export class EventInsertEvents implements BuiltInFunction< ): IGameEventInvocation[] { const invocations: IGameEventInvocation[] = []; for (const id of ids) { - if ( - !id || - !store.getEvent(id) - ) { - continue; + if (store.getEvent(id)) { + invocations.push({ id, env }); } - invocations.push({ id, env }); } return invocations; } @@ -63,14 +49,6 @@ export class EventInsertEvent implements BuiltInFunction< async func(param: IInsertEventEventParam, env: IBlockEventEnv) { if (param.length === 0) return; - const executor = getEventExecutor(env); - if (!executor) return; - - if (!enterEventInsert(env)) return; - try { - await executor.interpreter.exec(param, { custom: {} }, env); - } finally { - exitEventInsert(env); - } + await env.system.executor.interpreter.exec(param, { custom: {} }, env); } } diff --git a/packages-user/data-state/src/event/hero.ts b/packages-user/data-state/src/event/hero.ts index e73f26d..40649a7 100644 --- a/packages-user/data-state/src/event/hero.ts +++ b/packages-user/data-state/src/event/hero.ts @@ -1,24 +1,11 @@ import { BuiltInFunction } from '@motajs/anon-tokyo'; -import { - BlockEventType, - IBlockEventEnv, - IGameEventInvocation -} from '@user/data-system'; -import { IReadonlyTileBase as IReadonlyMapTileBase } from '@user/data-base'; -import { EventTrigger, FaceDirection } from '@user/data-common'; +import { IBlockEventEnv } from '@user/data-system'; import { IMoveHeroEventParam, - IMoveHeroStepEventParam, + IStepHeroEventParam, ITouchFrontEventParam } from './types'; -import { getEventExecutor, getPossibleLayer } from './utils'; - -interface IEventSource { - readonly priority: number; - readonly id: string; - readonly type: BlockEventType; - readonly tile: IReadonlyMapTileBase | null; -} +import { getPossibleLayer } from './utils'; export class EventMoveHero implements BuiltInFunction< IMoveHeroEventParam, @@ -37,13 +24,13 @@ export class EventMoveHero implements BuiltInFunction< } } -export class EventMoveHeroStep implements BuiltInFunction< - IMoveHeroStepEventParam, +export class EventStepHero implements BuiltInFunction< + IStepHeroEventParam, IBlockEventEnv > { - name: string = 'moveHeroStep'; + name: string = 'stepHero'; - async func(_param: IMoveHeroStepEventParam, env: IBlockEventEnv) { + async func(_param: IStepHeroEventParam, env: IBlockEventEnv) { const mover = env.state.hero.location.mover; if (mover.moving) return; @@ -64,96 +51,12 @@ export class EventTouchFront implements BuiltInFunction< const layer = getPossibleLayer(env); if (!layer) return; - const executor = getEventExecutor(env); - if (!executor) return; - - const hero = env.state.hero.getLocation(); const mover = env.state.hero.location.mover; - const direction = mover.tile.getCurrentFaceDirection(); - if (direction === FaceDirection.Unknown) return; - const movement = mover.faceHandler.movement(direction); - const x = hero.x + movement.x; - const y = hero.y + movement.y; - if (!layer.inMap(x, y)) return; - const invocations = this.collectInvocations( - env, - layer, - x, - y, - EventTrigger.OnTouch - ); - if (invocations.length === 0) return; - await executor.execute(invocations, { custom: {} }); - } - - /** - * 按坐标收集事件来源并保留其触发环境 - */ - private collectInvocations( - env: IBlockEventEnv, - layer: NonNullable, - x: number, - y: number, - trigger: EventTrigger - ): IGameEventInvocation[] { - const pointSources: IEventSource[] = []; - const tileSources: IEventSource[] = []; - const point = layer.getPointEvent(x, y); - const location = layer.getLocationData(x, y); - if (point) { - for (const [priority, id] of point) { - pointSources.push({ - priority, - id, - type: BlockEventType.PointEvent, - tile: null - }); - } - } - if (location) { - if (location.static) { - for (const [priority, id] of location.static - .tileEvent() - .get()) { - tileSources.push({ - priority, - id, - type: BlockEventType.TileEvent, - tile: location.static - }); - } - } - for (const tile of location.dynamics) { - for (const [priority, id] of tile.tileEvent().get()) { - tileSources.push({ - priority, - id, - type: BlockEventType.TileEvent, - tile - }); - } - } - } - pointSources.sort((a, b) => b.priority - a.priority); - tileSources.sort((a, b) => b.priority - a.priority); - - const hero = env.state.hero.getLocation(); - const invocations: IGameEventInvocation[] = []; - for (const source of [...pointSources, ...tileSources]) { - const sourceEnv: IBlockEventEnv = { - state: env.state, - type: source.type, - trigger, - heroLocator: hero, - heroFloor: env.heroFloor, - triggerLocator: { x, y }, - tile: source.tile, - layer, - map: layer.map - }; - invocations.push({ id: source.id, env: sourceEnv }); - } - return invocations; + if (mover.moving) return; + mover.forward(); + const controller = mover.start(); + if (!controller) return; + await controller.onEnd; } } diff --git a/packages-user/data-state/src/event/index.ts b/packages-user/data-state/src/event/index.ts index 1a9c181..bc53be1 100644 --- a/packages-user/data-state/src/event/index.ts +++ b/packages-user/data-state/src/event/index.ts @@ -1,5 +1,5 @@ -export { createEventRegistrations } from './registrations'; export * from './event'; export * from './hero'; export * from './map'; +export * from './registrations'; export * from './types'; diff --git a/packages-user/data-state/src/event/map.ts b/packages-user/data-state/src/event/map.ts index bf1fbb1..2b59ced 100644 --- a/packages-user/data-state/src/event/map.ts +++ b/packages-user/data-state/src/event/map.ts @@ -1,7 +1,7 @@ import { BuiltInFunction } from '@motajs/anon-tokyo'; import { IBlockEventEnv } from '@user/data-system'; import { - IDeleteBlockEventParam, + IRemoveBlockEventParam, IMoveBlockEventParam, ISetBlockEventParam } from './types'; @@ -39,40 +39,40 @@ export class EventMoveBlock implements BuiltInFunction< async func(param: IMoveBlockEventParam, env: IBlockEventEnv) { const layer = getPossibleLayer(env); - if (!layer || !layer.inMap(param.x, param.y)) return; - if (!layer.getTile(param.x, param.y)) return; + if (!layer) return; - const dynamic = layer.transferToDynamic(param.x, param.y); + const { x, y, keepEvent } = param; + const dynamic = layer.transferToDynamic(x, y, keepEvent); if (!dynamic) return; - if (dynamic.mover.moving) return; - dynamic.mover.push([...param.steps]); + dynamic.mover.push(param.steps); const controller = dynamic.mover.start(); if (!controller) return; await controller.onEnd; if (param.safe) { - layer.transferToStaticIfSafe(dynamic); + layer.transferToStaticIfSafe(dynamic, keepEvent); } else { - layer.transferToStatic(dynamic); + layer.transferToStatic(dynamic, keepEvent); } } } -export class EventDeleteBlock implements BuiltInFunction< - IDeleteBlockEventParam, +export class EventRemoveBlock implements BuiltInFunction< + IRemoveBlockEventParam, IBlockEventEnv > { - name: string = 'deleteBlock'; + name: string = 'removeBlock'; - async func(param: IDeleteBlockEventParam, env: IBlockEventEnv) { + async func(param: IRemoveBlockEventParam, env: IBlockEventEnv) { const layer = getPossibleLayer(env); if (!layer || !layer.inMap(param.x, param.y)) return; - const dynamics = [...layer.getDynamicTilesAt(param.x, param.y)]; - await Promise.all(dynamics.map(tile => layer.deleteDynamic(tile))); - if (layer.getTile(param.x, param.y)) { - layer.setBlock(0, param.x, param.y); + if (param.dynamic) { + const dynamics = [...layer.getDynamicTilesAt(param.x, param.y)]; + await Promise.all(dynamics.map(tile => layer.deleteDynamic(tile))); } + + layer.removeBlock(param.x, param.y); } } diff --git a/packages-user/data-state/src/event/registrations.ts b/packages-user/data-state/src/event/registrations.ts index c4ceb28..b2ba178 100644 --- a/packages-user/data-state/src/event/registrations.ts +++ b/packages-user/data-state/src/event/registrations.ts @@ -1,6 +1,6 @@ import { BuiltInFunction } from '@motajs/anon-tokyo'; -import { EventDeleteBlock, EventMoveBlock, EventSetBlock } from './map'; -import { EventMoveHero, EventMoveHeroStep, EventTouchFront } from './hero'; +import { EventRemoveBlock, EventMoveBlock, EventSetBlock } from './map'; +import { EventMoveHero, EventStepHero, EventTouchFront } from './hero'; import { EventInsertEvent, EventInsertEvents } from './event'; /** @@ -11,11 +11,11 @@ export function createEventRegistrations(): BuiltInFunction[] { // 地图控制 new EventSetBlock(), new EventMoveBlock(), - new EventDeleteBlock(), + new EventRemoveBlock(), // 玩家控制 new EventMoveHero(), - new EventMoveHeroStep(), + new EventStepHero(), new EventTouchFront(), // 事件控制 diff --git a/packages-user/data-state/src/event/types.ts b/packages-user/data-state/src/event/types.ts index 7d00303..4eb7eff 100644 --- a/packages-user/data-state/src/event/types.ts +++ b/packages-user/data-state/src/event/types.ts @@ -21,16 +21,20 @@ export interface IMoveBlockEventParam { readonly y: number; /** 移动步骤 */ readonly steps: readonly ObjectMoveStep[]; + /** 是否保留图块的图块事件 */ + readonly keepEvent?: boolean; /** 是否仅在目标位置安全时转回静态图块 */ readonly safe?: boolean; } /** 事件:删除图块 */ -export interface IDeleteBlockEventParam { +export interface IRemoveBlockEventParam { /** 横坐标 */ readonly x: number; /** 纵坐标 */ readonly y: number; + /** 是否一并删除动态图块 */ + readonly dynamic?: boolean; } //#endregion @@ -44,7 +48,7 @@ export interface IMoveHeroEventParam { } /** 事件:向前移动一步 */ -export interface IMoveHeroStepEventParam {} +export interface IStepHeroEventParam {} /** 事件:触发勇士面前的 onTouch */ export interface ITouchFrontEventParam {} diff --git a/packages-user/data-state/src/event/utils.ts b/packages-user/data-state/src/event/utils.ts index 580f065..d9735e6 100644 --- a/packages-user/data-state/src/event/utils.ts +++ b/packages-user/data-state/src/event/utils.ts @@ -1,20 +1,6 @@ -import { IGameMap, IMapLayer, IStateBase } from '@user/data-base'; -import { - IBlockEventEnv, - IGameEventExecutor, - IGameEventSystem -} from '@user/data-system'; - -interface IEventState extends IStateBase { - readonly eventSystem: IGameEventSystem; -} - -/** - * 判断状态对象是否包含事件系统 - */ -function hasEventSystem(state: IStateBase): state is IEventState { - return 'eventSystem' in state; -} +import { IGameMap, IMapLayer } from '@user/data-base'; +import { IBlockEventEnv } from '@user/data-system'; +import { isNil } from 'lodash-es'; /** * 通过环境参量获取可能的地图对象 @@ -23,8 +9,10 @@ export function getPossibleMap(env: IBlockEventEnv): IGameMap | null { if (env.map) return env.map; if (env.layer) return env.layer.map; - const map = env.state.maps.getMap(env.heroFloor); - if (map) return map; + if (!isNil(env.heroFloor)) { + const map = env.state.maps.getMap(env.heroFloor); + if (map) return map; + } return null; } @@ -40,35 +28,3 @@ export function getPossibleLayer(env: IBlockEventEnv): IMapLayer | null { return null; } - -/** - * 通过环境参量获取事件执行器 - */ -export function getEventExecutor( - env: IBlockEventEnv -): IGameEventExecutor | null { - if (!hasEventSystem(env.state)) return null; - return env.state.eventSystem.executor; -} - -/** 事件插入允许的最大嵌套深度 */ -const EVENT_INSERT_MAX_DEPTH = 32; -/** 每个环境当前的事件插入深度 */ -const eventInsertDepth: WeakMap = new WeakMap(); - -/** - * 进入一层事件插入,超过最大嵌套深度时返回 false - */ -export function enterEventInsert(env: IBlockEventEnv): boolean { - const depth = eventInsertDepth.get(env) ?? 0; - if (depth >= EVENT_INSERT_MAX_DEPTH) return false; - eventInsertDepth.set(env, depth + 1); - return true; -} - -/** - * 退出一层事件插入 - */ -export function exitEventInsert(env: IBlockEventEnv): void { - eventInsertDepth.delete(env); -} diff --git a/packages-user/data-state/src/hero/moverImpl.ts b/packages-user/data-state/src/hero/moverImpl.ts index 1a42479..bece098 100644 --- a/packages-user/data-state/src/hero/moverImpl.ts +++ b/packages-user/data-state/src/hero/moverImpl.ts @@ -1,30 +1,14 @@ -import { ITileLocator } from '@motajs/common'; import { IHeroMoveTopHandler, IHeroMoveTopImpl, IMapState, - IPassPredicate, - IReadonlyTileBase + IPassPredicate } from '@user/data-base'; import { EventTrigger } from '@user/data-common'; -import { - IGameEventExecutor, - IStateSystem, - BlockEventType, - IBlockEventEnv, - IBlockEventParam, - IGameEventInvocation -} from '@user/data-system'; +import { IGameEventExecutor, IStateSystem } from '@user/data-system'; import { DefaultPassPredicate, DefaultPassPredicateImpl } from './predicate'; import { isNil } from 'lodash-es'; -interface IEventSource { - readonly priority: number; - readonly id: string; - readonly type: BlockEventType; - readonly tile: IReadonlyTileBase | null; -} - export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl { /** 地图存储对象 */ private readonly maps: IMapState; @@ -61,14 +45,12 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl { * 统一收集、排序并执行指定位置的点事件与图块事件。 * @param trigger 事件触发条件 * @param handler 移动信息对象 - * @param heroLoc 触发事件时勇士的位置 * @param x 收集横坐标 * @param y 收集纵坐标 */ private async commonTrigger( trigger: EventTrigger, handler: IHeroMoveTopHandler, - heroLoc: ITileLocator, x: number, y: number ): Promise { @@ -79,96 +61,25 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl { const event = map.eventLayer; if (!event) return Promise.resolve(); - const point = event.getPointEvent(x, y); - const loc = event.getLocationData(x, y); - const pointSources: IEventSource[] = []; - const tileSources: IEventSource[] = []; - if (point) { - for (const [priority, id] of point) { - pointSources.push({ - priority, - id, - type: BlockEventType.PointEvent, - tile: null - }); - } - } - if (loc) { - if (loc.static) { - for (const [priority, id] of loc.static.tileEvent().get()) { - tileSources.push({ - priority, - id, - type: BlockEventType.TileEvent, - tile: loc.static - }); - } - } - for (const tile of loc.dynamics) { - for (const [priority, id] of tile.tileEvent().get()) { - tileSources.push({ - priority, - id, - type: BlockEventType.TileEvent, - tile - }); - } - } - } - pointSources.sort((a, b) => b.priority - a.priority); - tileSources.sort((a, b) => b.priority - a.priority); + const system = this.state.eventSystem; + const invocations = system.collectEvent(event, trigger, x, y); - const param: IBlockEventParam = { custom: {} }; - const invocations: IGameEventInvocation[] = []; - for (const source of [...pointSources, ...tileSources]) { - const env: IBlockEventEnv = { - state: this.state, - type: source.type, - trigger, - heroLocator: heroLoc, - heroFloor: handler.floorId, - triggerLocator: { x, y }, - tile: source.tile, - layer: event, - map - }; - invocations.push({ id: source.id, env }); - } - - await this.executor.execute(invocations, param); + await this.executor.execute(invocations, { custom: {} }); } async enter(handler: IHeroMoveTopHandler): Promise { const { x, y } = handler.nextLoc; - return this.commonTrigger( - EventTrigger.OnEnter, - handler, - handler.nextLoc, - x, - y - ); + return this.commonTrigger(EventTrigger.OnEnter, handler, x, y); } async leave(handler: IHeroMoveTopHandler): Promise { const { x, y } = handler.currLoc; - return this.commonTrigger( - EventTrigger.OnLeave, - handler, - handler.currLoc, - x, - y - ); + return this.commonTrigger(EventTrigger.OnLeave, handler, x, y); } async hit(handler: IHeroMoveTopHandler): Promise { const { x, y } = handler.nextLoc; - return this.commonTrigger( - EventTrigger.OnTouch, - handler, - handler.currLoc, - x, - y - ); + return this.commonTrigger(EventTrigger.OnTouch, handler, x, y); } /** diff --git a/packages-user/data-system/src/event/eventDispatch.test.ts b/packages-user/data-system/src/event/eventDispatch.test.ts index 53aa230..a54bbef 100644 --- a/packages-user/data-system/src/event/eventDispatch.test.ts +++ b/packages-user/data-system/src/event/eventDispatch.test.ts @@ -59,6 +59,7 @@ const eventReduceModes = { interface TestModules { EventExecutor: typeof import('./executor').EventExecutor; + GameEventSystem: typeof import('./system').GameEventSystem; DefaultHeroMoveTopImpl: typeof import('@user/data-state').DefaultHeroMoveTopImpl; MapState: typeof import('@user/data-base').MapState; TileStore: typeof import('@user/data-common').TileStore; @@ -87,11 +88,13 @@ beforeAll(async () => { vi.stubGlobal('main', { replayChecking: true }); vi.stubGlobal('location', { origin: 'http://localhost' }); const executorModule = await import('./executor'); + const systemModule = await import('./system'); const stateModule = await import('../../../data-state/src/hero'); const baseModule = await import('@user/data-base'); const commonModule = await import('@user/data-common'); modules = { EventExecutor: executorModule.EventExecutor, + GameEventSystem: systemModule.GameEventSystem, DefaultHeroMoveTopImpl: stateModule.DefaultHeroMoveTopImpl, MapState: baseModule.MapState, TileStore: commonModule.TileStore, @@ -166,15 +169,26 @@ function createFixture( {} as AnonTokyoInterpreter, () => store ); - const state = { + const heroLocation: { + x: number; + y: number; + floorId: string | undefined; + } = { x: 0, y: 0, floorId: 'F1' }; + const state: Record = { maps, - eventSystem: { executor } + hero: { location: heroLocation }, + eventStore: store, + eventSystem: null }; const fixtureState: IStateSystem & IDataCommon = state as never; + const eventSystem = new modules.GameEventSystem(fixtureState, []); + state.eventSystem = eventSystem; const mover = new modules.DefaultHeroMoveTopImpl(fixtureState); return { events, executor, + eventSystem, + heroLocation, layer, map: map!, dynamic, @@ -207,7 +221,7 @@ function addEvent( trigger: env.trigger, type: env.type, tile: env.tile, - hero: env.heroLocator, + hero: { x: env.heroLocator.x, y: env.heroLocator.y }, triggerLocator: env.triggerLocator }); return result; @@ -220,6 +234,7 @@ function invocation(id: string, trigger: EventTrigger): IGameEventInvocation { state: {} as IStateBase, type: BlockEventType.CommonEvent, trigger, + system: {} as never, heroLocator: { x: 0, y: 0 }, heroFloor: 'Unknown', triggerLocator: null, @@ -244,6 +259,8 @@ describe('source-aware matching dispatch', () => { ); expect(fixture.map.eventLayer).toBe(fixture.layer); + fixture.heroLocation.x = 1; + fixture.heroLocation.y = 0; await fixture.mover.enter({ state: fixture.state, currLoc: { x: 0, y: 0 }, @@ -299,6 +316,8 @@ describe('source-aware matching dispatch', () => { calls ); + fixture.heroLocation.x = 1; + fixture.heroLocation.y = 0; await fixture.mover.enter({ state: fixture.state, currLoc: { x: 0, y: 0 }, @@ -350,7 +369,7 @@ describe('source-aware matching dispatch', () => { trigger: env.trigger, type: env.type, tile: env.tile, - hero: env.heroLocator, + hero: { x: env.heroLocator.x, y: env.heroLocator.y }, triggerLocator: env.triggerLocator }); await pending; @@ -551,8 +570,14 @@ describe('enter leave hit trigger hooks', () => { floorId: 'F1', face: new modules.Dir8FaceHandler() }; + fixture.heroLocation.x = 1; + fixture.heroLocation.y = 0; await fixture.mover.enter(handler); + fixture.heroLocation.x = 0; + fixture.heroLocation.y = 0; await fixture.mover.leave(handler); + fixture.heroLocation.x = 0; + fixture.heroLocation.y = 0; await fixture.mover.hit(handler); expect(calls.map(call => call.id)).toEqual([ diff --git a/packages-user/data-system/src/event/system.ts b/packages-user/data-system/src/event/system.ts index 81db5f1..c12c1fc 100644 --- a/packages-user/data-system/src/event/system.ts +++ b/packages-user/data-system/src/event/system.ts @@ -1,8 +1,21 @@ -import { IStateBase } from '@user/data-base'; -import { IGameEventStore } from '@user/data-common'; +import { IMapLayer, IReadonlyTileBase, IStateBase } from '@user/data-base'; +import { EventTrigger, IGameEventStore } from '@user/data-common'; import { AnonTokyoInterpreter, BuiltInFunction } from '@motajs/anon-tokyo'; import { EventExecutor } from './executor'; -import { IGameEventExecutor, IGameEventSystem } from './types'; +import { + BlockEventType, + IBlockEventEnv, + IGameEventExecutor, + IGameEventInvocation, + IGameEventSystem +} from './types'; + +interface IEventSource { + readonly priority: number; + readonly id: string; + readonly type: BlockEventType; + readonly tile: IReadonlyTileBase | null; +} export class GameEventSystem implements IGameEventSystem { readonly executor: IGameEventExecutor; @@ -23,4 +36,73 @@ export class GameEventSystem implements IGameEventSystem { useStore(store: IGameEventStore | null): void { this.store = store; } + + collectEvent( + layer: IMapLayer, + trigger: EventTrigger, + x: number, + y: number + ): IGameEventInvocation[] { + if (!layer.inMap(x, y)) return []; + + const pointSources: IEventSource[] = []; + const tileSources: IEventSource[] = []; + + const point = layer.getPointEvent(x, y); + if (point) { + for (const [priority, id] of point) { + pointSources.push({ + priority, + id, + type: BlockEventType.PointEvent, + tile: null + }); + } + } + + const loc = layer.getLocationData(x, y); + if (loc) { + if (loc.static) { + for (const [priority, id] of loc.static.tileEvent().get()) { + tileSources.push({ + priority, + id, + type: BlockEventType.TileEvent, + tile: loc.static + }); + } + } + for (const tile of loc.dynamics) { + for (const [priority, id] of tile.tileEvent().get()) { + tileSources.push({ + priority, + id, + type: BlockEventType.TileEvent, + tile + }); + } + } + } + pointSources.sort((a, b) => b.priority - a.priority); + tileSources.sort((a, b) => b.priority - a.priority); + + const invocations: IGameEventInvocation[] = []; + for (const source of [...pointSources, ...tileSources]) { + const env: IBlockEventEnv = { + state: this.state, + type: source.type, + trigger, + system: this, + heroLocator: this.state.hero.location, + heroFloor: this.state.hero.location.floorId, + triggerLocator: { x, y }, + tile: source.tile, + layer, + map: layer.map + }; + invocations.push({ id: source.id, env }); + } + + return invocations; + } } diff --git a/packages-user/data-system/src/event/types.ts b/packages-user/data-system/src/event/types.ts index 04c6da8..1105366 100644 --- a/packages-user/data-system/src/event/types.ts +++ b/packages-user/data-system/src/event/types.ts @@ -34,8 +34,10 @@ export interface IBlockEventEnv extends IDataBaseExtended { readonly trigger: EventTrigger; /** 触发事件时玩家的位置 */ readonly heroLocator: Readonly; + /** 事件系统对象 */ + readonly system: IGameEventSystem; /** 触发事件时玩家的位置 */ - readonly heroFloor: string; + readonly heroFloor: string | undefined; /** 触发事件时触发者的位置,有可能不存在 */ readonly triggerLocator: Readonly | null; /** 触发事件的图块,有可能不存在 */ @@ -145,4 +147,19 @@ export interface IGameEventSystem extends IDataBaseExtended { * @param store 事件存储器 */ useStore(store: IGameEventStore | null): void; + + /** + * 收集指定图层在指定坐标下的所有事件,按照先点事件,后图块事件的顺序排序, + * 分别对于点事件和图块事件,不论是否在动态图块上,均按照事件优先级排序。 + * @param layer 要收集的图层 + * @param trigger 要收集的触发器 + * @param x 横坐标 + * @param y 纵坐标 + */ + collectEvent( + layer: IMapLayer, + trigger: EventTrigger, + x: number, + y: number + ): IGameEventInvocation[]; }