diff --git a/packages-user/data-base/src/map/types.ts b/packages-user/data-base/src/map/types.ts index 1530baf..1bb7813 100644 --- a/packages-user/data-base/src/map/types.ts +++ b/packages-user/data-base/src/map/types.ts @@ -54,6 +54,13 @@ export interface IBlockEventEnv extends IDataCommonExtended { readonly map: IGameMap | null; } +export interface IGameEventInvocation { + /** 事件在 `IGameEventStore` 中的 id */ + readonly id: string; + /** 此次调用对应的真实来源环境 */ + readonly env: IBlockEventEnv; +} + export interface IReadonlyBlockEvent extends IReadonlyGameEvent< IBlockEventParam, IBlockEventEnv, diff --git a/packages-user/data-state/src/hero/moverImpl.ts b/packages-user/data-state/src/hero/moverImpl.ts index 288871d..c21d281 100644 --- a/packages-user/data-state/src/hero/moverImpl.ts +++ b/packages-user/data-state/src/hero/moverImpl.ts @@ -1,26 +1,34 @@ +import { ITileLocator } from '@motajs/common'; import { + BlockEventType, + IBlockEventEnv, + IBlockEventParam, + IGameEventInvocation, IHeroMoveTopHandler, IHeroMoveTopImpl, - IMapState + IMapState, + IReadonlyTileBase } from '@user/data-base'; -import { FaceDirection, PassBit } from '@user/data-common'; -import { - IStateSystem, - ITriggerCollector, - ITriggerHandler, - TriggerType -} from '@user/data-system'; +import { EventTrigger, FaceDirection, PassBit } from '@user/data-common'; +import { IGameEventExecutor, IStateSystem } from '@user/data-system'; 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; - /** 触发器收集器对象 */ - private readonly collector: ITriggerCollector; + /** 游戏事件执行器 */ + private readonly executor: IGameEventExecutor; - constructor(private readonly state: IStateSystem) { + constructor(state: IStateSystem) { this.maps = state.maps; - this.collector = state.triggerCollector; + this.executor = state.eventSystem.executor; } //#region 通行性判断 @@ -84,11 +92,13 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl { // 判断事件层 const curr = event.getLocationData(x, y); const next = event.getLocationData(nx, ny); - if (curr && curr.raw) { - canLeave = !!(leaveMask & curr.raw.pass.outPass); + const currRaw = curr?.static.raw(); + const nextRaw = next?.static.raw(); + if (currRaw) { + canLeave = !!(leaveMask & currRaw.pass.outPass); } - if (next && next.raw) { - canEnter = !!(enterMask & next.raw.pass.inPass); + if (nextRaw) { + canEnter = !!(enterMask & nextRaw.pass.inPass); } if (!canLeave || !canEnter) return false; @@ -100,11 +110,13 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl { 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); + const currRaw = curr?.static.raw(); + const nextRaw = next?.static.raw(); + if (currRaw?.pass.onlyEvents) { + canLeave = !!(leaveMask & currRaw.pass.outPass); } - if (next && next.raw && next.raw.pass.onlyEvents) { - canEnter = !!(enterMask & next.raw.pass.inPass); + if (nextRaw?.pass.onlyEvents) { + canEnter = !!(enterMask & nextRaw.pass.inPass); } if (!canLeave || !canEnter) return false; } @@ -123,24 +135,27 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl { const { x: nx, y: ny } = nextLoc; const next = eventLayer.getLocationData(nx, ny); - if (!next || !next.raw) return false; - return !next.raw.eventPass; + const nextRaw = next?.static.raw(); + if (!nextRaw) return false; + return !nextRaw.eventPass; } //#endregion - //#region 触发器行为 + //#region 事件触发行为 /** - * 统一的触发器收集与执行流程 - * @param type 触发条件 + * 统一收集、排序并执行指定位置的点事件与图块事件。 + * @param trigger 事件触发条件 * @param handler 移动信息对象 + * @param heroLoc 触发事件时勇士的位置 * @param x 收集横坐标 * @param y 收集纵坐标 */ - private commonTrigger( - type: TriggerType, + private async commonTrigger( + trigger: EventTrigger, handler: IHeroMoveTopHandler, + heroLoc: ITileLocator, x: number, y: number ): Promise { @@ -151,36 +166,100 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl { const event = map.eventLayer; if (!event) return Promise.resolve(); - const triggers = this.collector.collect(x, y, event); + 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) { + 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 triggerHandler: ITriggerHandler = { - state: this.state, - layer: map, - mapLayer: event, - locator: { x, y } - }; + const param: IBlockEventParam = { custom: {} }; + const invocations: IGameEventInvocation[] = []; + for (const source of [...pointSources, ...tileSources]) { + const env: IBlockEventEnv = { + state: handler.state, + type: source.type, + trigger, + heroLocator: heroLoc, + triggerLocator: { x, y }, + tile: source.tile, + layer: event, + map + }; + invocations.push({ id: source.id, env }); + } - return triggers.trigger(type, triggerHandler); + await this.executor.execute(invocations, param); } async enter(handler: IHeroMoveTopHandler): Promise { const { x, y } = handler.nextLoc; - return this.commonTrigger(TriggerType.Enter, handler, x, y); + return this.commonTrigger( + EventTrigger.OnEnter, + handler, + handler.nextLoc, + x, + y + ); } async leave(handler: IHeroMoveTopHandler): Promise { const { x, y } = handler.currLoc; - return this.commonTrigger(TriggerType.Leave, handler, x, y); + return this.commonTrigger( + EventTrigger.OnLeave, + handler, + handler.currLoc, + x, + y + ); } async hit(handler: IHeroMoveTopHandler): Promise { const { x, y } = handler.nextLoc; - return this.commonTrigger(TriggerType.Hit, handler, x, y); + return this.commonTrigger( + EventTrigger.OnTouch, + handler, + handler.currLoc, + x, + y + ); } - async cannotEnter(handler: IHeroMoveTopHandler): Promise { - const { x, y } = handler.nextLoc; - return this.commonTrigger(TriggerType.CannotEnter, handler, x, y); + /** + * 新事件触发器没有无法进入的对应项,保留空实现以满足移动接口 + */ + async cannotEnter(): Promise { + return Promise.resolve(); } //#endregion diff --git a/packages-user/data-system/src/event/eventDispatch.test.ts b/packages-user/data-system/src/event/eventDispatch.test.ts new file mode 100644 index 0000000..353181e --- /dev/null +++ b/packages-user/data-system/src/event/eventDispatch.test.ts @@ -0,0 +1,436 @@ +import { beforeAll, describe, expect, it, vi } from 'vitest'; + +interface TestModules { + EventExecutor: typeof import('./executor').EventExecutor; + DefaultHeroMoveTopImpl: typeof import('@user/data-state/hero/moverImpl').DefaultHeroMoveTopImpl; + MapState: typeof import('@user/data-base').MapState; + TileStore: typeof import('@user/data-common').TileStore; + RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder; + FaceManager: typeof import('@user/data-common').FaceManager; + Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler; + EventTrigger: typeof import('@user/data-common').EventTrigger; + BlockEventType: typeof import('@user/data-base').BlockEventType; + EventExecuteMode: typeof import('./types').EventExecuteMode; + EventReduceMode: typeof import('./types').EventReduceMode; +} + +interface EventCall { + readonly id: string; + readonly trigger: number; + readonly type: number; + readonly tile: object | null; + readonly hero: Readonly<{ x: number; y: number }>; + readonly triggerLocator: Readonly<{ x: number; y: number }> | null; +} + +let modules: TestModules; + +beforeAll(async () => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + const executorModule = await import('./executor'); + const moverModule = await import('@user/data-state/hero/moverImpl'); + const baseModule = await import('@user/data-base'); + const commonModule = await import('@user/data-common'); + const eventModule = await import('./types'); + modules = { + EventExecutor: executorModule.EventExecutor, + DefaultHeroMoveTopImpl: moverModule.DefaultHeroMoveTopImpl, + MapState: baseModule.MapState, + TileStore: commonModule.TileStore, + RoleFaceBinder: commonModule.RoleFaceBinder, + FaceManager: commonModule.FaceManager, + Dir8FaceHandler: commonModule.Dir8FaceHandler, + EventTrigger: commonModule.EventTrigger, + BlockEventType: baseModule.BlockEventType, + EventExecuteMode: eventModule.EventExecuteMode, + EventReduceMode: eventModule.EventReduceMode + }; +}); + +function createFixture( + pointEvents: Record> = {}, + staticEvent: string = 'static-enter', + dynamicEvent: string = 'dynamic-enter' +) { + const tileStore = new modules.TileStore(); + tileStore.addTile({ + num: 1, + id: 'static', + events: { 30: staticEvent }, + type: 0, + pass: { onlyEvents: false, outPass: 15, inPass: 15 }, + eventPass: true + }); + tileStore.addTile({ + num: 2, + id: 'dynamic', + events: { 40: dynamicEvent }, + type: 0, + pass: { onlyEvents: false, outPass: 15, inPass: 15 }, + eventPass: true + }); + const faceManager = new modules.FaceManager(); + faceManager.register(1, new modules.Dir8FaceHandler()); + const commonState = { + tileStore, + itemStore: {}, + mapStore: {}, + eventStore: {}, + roleFace: new modules.RoleFaceBinder(), + faceManager, + saveSystem: {} + }; + const maps = new modules.MapState(tileStore, commonState); + const map = maps.fromRaw({ + floorId: 'F1', + width: 3, + map: { 0: [1, 1, 1] }, + layerAlias: { 0: 'event' }, + events: { 0: pointEvents } + }); + const layer = map!.getLayerByAlias('event')!; + const dynamic = layer.createDynamic(2, 1, 0); + dynamic.set(2); + const events = new Map(); + const store = { + getEvent(id: string) { + return events.get(id) ?? null; + } + }; + const executor = new modules.EventExecutor({}, () => store); + const state = { + maps, + eventSystem: { executor } + }; + const mover = new modules.DefaultHeroMoveTopImpl(state); + return { events, executor, layer, dynamic, mover, state }; +} + +function addEvent( + events: Map, + id: string, + trigger: number, + result: unknown, + calls: EventCall[] +): void { + events.set(id, { + trigger, + execute: async ( + _param: unknown, + env: { + trigger: number; + type: number; + tile: object | null; + heroLocator: Readonly<{ x: number; y: number }>; + triggerLocator: Readonly<{ x: number; y: number }> | null; + } + ) => { + calls.push({ + id, + trigger: env.trigger, + type: env.type, + tile: env.tile, + hero: env.heroLocator, + triggerLocator: env.triggerLocator + }); + return result; + } + }); +} + +function invocation(id: string, trigger: number) { + return { id, env: { trigger } }; +} + +describe('source-aware matching dispatch', () => { + it('source-aware matching dispatch', async () => { + const calls: EventCall[] = []; + const fixture = createFixture({ + 1: { 50: 'point-enter', 60: 'leave-only' } + }); + addEvent( + fixture.events, + 'point-enter', + modules.EventTrigger.OnEnter, + true, + calls + ); + addEvent( + fixture.events, + 'leave-only', + modules.EventTrigger.OnLeave, + true, + calls + ); + addEvent( + fixture.events, + 'static-enter', + modules.EventTrigger.OnEnter, + true, + calls + ); + addEvent( + fixture.events, + 'dynamic-enter', + modules.EventTrigger.OnEnter, + true, + calls + ); + + await fixture.mover.enter({ + state: fixture.state, + currLoc: { x: 0, y: 0 }, + nextLoc: { x: 1, y: 0 }, + direction: 0, + floorId: 'F1', + face: new modules.Dir8FaceHandler() + }); + + expect(calls.map(call => call.id)).toEqual([ + 'point-enter', + 'dynamic-enter', + 'static-enter' + ]); + expect(calls.map(call => call.type)).toEqual([ + modules.BlockEventType.PointEvent, + modules.BlockEventType.TileEvent, + modules.BlockEventType.TileEvent + ]); + expect(calls[0].tile).toBeNull(); + expect(calls[1].tile).toBe(fixture.dynamic); + expect(calls[2].tile).toBe(fixture.layer.getLocationData(1, 0)!.static); + }); + + it('awaits each source before continuing to the next one', async () => { + const calls: EventCall[] = []; + let release: () => void = () => {}; + const pending = new Promise(resolve => { + release = resolve; + }); + const fixture = createFixture({ 1: { 50: 'slow-point' } }); + fixture.events.set('slow-point', { + trigger: modules.EventTrigger.OnEnter, + execute: async ( + _param: unknown, + env: { + trigger: number; + type: number; + tile: object | null; + heroLocator: Readonly<{ x: number; y: number }>; + triggerLocator: Readonly<{ x: number; y: number }> | null; + } + ) => { + calls.push({ + id: 'slow-point', + trigger: env.trigger, + type: env.type, + tile: env.tile, + hero: env.heroLocator, + triggerLocator: env.triggerLocator + }); + await pending; + return true; + } + }); + addEvent( + fixture.events, + 'static-enter', + modules.EventTrigger.OnEnter, + true, + calls + ); + addEvent( + fixture.events, + 'dynamic-enter', + modules.EventTrigger.OnEnter, + true, + calls + ); + + const running = fixture.mover.enter({ + state: fixture.state, + currLoc: { x: 0, y: 0 }, + nextLoc: { x: 1, y: 0 }, + direction: 0, + floorId: 'F1', + face: new modules.Dir8FaceHandler() + }); + await Promise.resolve(); + expect(calls.map(call => call.id)).toEqual(['slow-point']); + release(); + await running; + expect(calls.map(call => call.id)).toEqual([ + 'slow-point', + 'dynamic-enter', + 'static-enter' + ]); + }); +}); + +describe('event execute modes and reductions', () => { + it('applies cut and reduce only to matching events', async () => { + const calls: string[] = []; + const fixture = createFixture(); + fixture.events.set('wrong-trigger', { + trigger: modules.EventTrigger.OnLeave, + execute: async () => { + calls.push('wrong-trigger'); + return true; + } + }); + fixture.events.set('false', { + trigger: modules.EventTrigger.OnEnter, + execute: async () => { + calls.push('false'); + return false; + } + }); + fixture.events.set('true', { + trigger: modules.EventTrigger.OnEnter, + execute: async () => { + calls.push('true'); + return true; + } + }); + const enter = modules.EventTrigger.OnEnter; + fixture.executor.setMode(modules.EventExecuteMode.Normal); + fixture.executor.setReduce(modules.EventReduceMode.NoReduce); + await expect( + fixture.executor.execute( + [ + invocation('wrong-trigger', enter), + invocation('false', enter) + ], + { custom: {} } + ) + ).resolves.toEqual([false]); + expect(calls).toEqual(['false']); + + calls.length = 0; + fixture.executor.setMode(modules.EventExecuteMode.CutIfFalsy); + await fixture.executor.execute( + [ + invocation('wrong-trigger', enter), + invocation('false', enter), + invocation('true', enter) + ], + { custom: {} } + ); + expect(calls).toEqual(['false']); + + calls.length = 0; + fixture.executor.setMode(modules.EventExecuteMode.CutIfTruthy); + await fixture.executor.execute( + [ + invocation('wrong-trigger', enter), + invocation('true', enter), + invocation('false', enter) + ], + { custom: {} } + ); + expect(calls).toEqual(['true']); + + fixture.executor.setMode(modules.EventExecuteMode.Normal); + fixture.executor.setReduce(modules.EventReduceMode.OrReduce); + await expect( + fixture.executor.execute( + [invocation('false', enter), invocation('true', enter)], + { custom: {} } + ) + ).resolves.toBe(true); + fixture.executor.setReduce(modules.EventReduceMode.AndReduce); + await expect( + fixture.executor.execute( + [invocation('true', enter), invocation('false', enter)], + { custom: {} } + ) + ).resolves.toBe(false); + }); + + it('warns for unknown ids and continues with valid events', async () => { + const calls: string[] = []; + const fixture = createFixture(); + fixture.events.set('valid', { + trigger: modules.EventTrigger.OnEnter, + execute: async () => { + calls.push('valid'); + return true; + } + }); + fixture.executor.setReduce(modules.EventReduceMode.NoReduce); + await expect( + fixture.executor.execute( + [ + invocation('missing', modules.EventTrigger.OnEnter), + invocation('valid', modules.EventTrigger.OnEnter) + ], + { custom: {} } + ) + ).resolves.toEqual([true]); + expect(calls).toEqual(['valid']); + }); +}); + +describe('enter leave hit trigger hooks', () => { + it('maps enter leave and hit to their approved triggers and coordinates', async () => { + const calls: EventCall[] = []; + const fixture = createFixture( + { + 0: { 10: 'leave-event' }, + 1: { 10: 'enter-event' } + }, + 'touch-event', + 'unused' + ); + addEvent( + fixture.events, + 'leave-event', + modules.EventTrigger.OnLeave, + true, + calls + ); + addEvent( + fixture.events, + 'enter-event', + modules.EventTrigger.OnEnter, + true, + calls + ); + addEvent( + fixture.events, + 'touch-event', + modules.EventTrigger.OnTouch, + true, + calls + ); + const handler = { + state: fixture.state, + currLoc: { x: 0, y: 0 }, + nextLoc: { x: 1, y: 0 }, + direction: 0, + floorId: 'F1', + face: new modules.Dir8FaceHandler() + }; + await fixture.mover.enter(handler); + await fixture.mover.leave(handler); + await fixture.mover.hit(handler); + + expect(calls.map(call => call.id)).toEqual([ + 'enter-event', + 'leave-event', + 'touch-event' + ]); + expect(calls.map(call => call.trigger)).toEqual([ + modules.EventTrigger.OnEnter, + modules.EventTrigger.OnLeave, + modules.EventTrigger.OnTouch + ]); + expect(calls[0].hero).toEqual({ x: 1, y: 0 }); + expect(calls[1].hero).toEqual({ x: 0, y: 0 }); + expect(calls[2].hero).toEqual({ x: 0, y: 0 }); + expect(calls[0].triggerLocator).toEqual({ x: 1, y: 0 }); + expect(calls[1].triggerLocator).toEqual({ x: 0, y: 0 }); + expect(calls[2].triggerLocator).toEqual({ x: 1, y: 0 }); + }); +}); diff --git a/packages-user/data-system/src/event/executor.ts b/packages-user/data-system/src/event/executor.ts new file mode 100644 index 0000000..659f47b --- /dev/null +++ b/packages-user/data-system/src/event/executor.ts @@ -0,0 +1,76 @@ +import { logger } from '@motajs/common'; +import { IBlockEventParam, IGameEventInvocation } from '@user/data-base'; +import { IGameEventStore } from '@user/data-common'; +import { AnonTokyoInterpreter } from 'anon-tokyo'; +import { EventExecuteMode, EventReduceMode, IGameEventExecutor } from './types'; + +export class EventExecutor implements IGameEventExecutor { + mode: EventExecuteMode = EventExecuteMode.Normal; + reduce: EventReduceMode = EventReduceMode.NoReduce; + + constructor( + readonly interpreter: AnonTokyoInterpreter, + private readonly storeRef: () => IGameEventStore | null + ) {} + + setMode(mode: EventExecuteMode): void { + this.mode = mode; + } + + setReduce(reduce: EventReduceMode): void { + this.reduce = reduce; + } + + async execute( + events: IGameEventInvocation[], + param: IBlockEventParam + ): Promise { + const results: any[] = []; + for (const invocation of events) { + const id = invocation.id; + const store = this.storeRef(); + if (!store) { + logger.warn(171, id); + continue; + } + const event = store.getEvent( + id + ); + if (!event) { + logger.warn(171, id); + continue; + } + if (event.trigger !== invocation.env.trigger) continue; + + const result = await event.execute(param, invocation.env); + if ( + this.reduce !== EventReduceMode.NoReduce && + typeof result !== 'boolean' + ) { + logger.warn(172, String(result)); + } + results.push(result); + if (this.mode === EventExecuteMode.CutIfFalsy && !result) { + break; + } else if (this.mode === EventExecuteMode.CutIfTruthy && result) { + break; + } + } + + let reduced: any = results; + if (this.reduce === EventReduceMode.OrReduce) { + reduced = false; + for (const result of results) { + reduced ||= result; + if (reduced) break; + } + } else if (this.reduce === EventReduceMode.AndReduce) { + reduced = true; + for (const result of results) { + reduced &&= result; + if (!reduced) break; + } + } + return reduced; + } +} diff --git a/packages-user/data-system/src/event/types.ts b/packages-user/data-system/src/event/types.ts new file mode 100644 index 0000000..eaf9a8a --- /dev/null +++ b/packages-user/data-system/src/event/types.ts @@ -0,0 +1,90 @@ +import { + IBlockEventEnv, + IBlockEventParam, + IDataBaseExtended, + IGameEventInvocation +} from '@user/data-base'; +import { IGameEventStore } from '@user/data-common'; +import { AnonTokyoInterpreter } from 'anon-tokyo'; + +export const enum EventExecuteMode { + /** 正常顺序执行,执行完前一个后执行后一个 */ + Normal, + /** 若事件执行过程中有任意事件返回了 `falsy` 值,立刻结束事件的运行 */ + CutIfFalsy, + /** 若事件执行过程中有任意事件返回了 `truthy` 值,立刻结束事件的运行 */ + CutIfTruthy +} + +export const enum EventReduceMode { + /** 不对事件的返回值做折叠处理,按照执行顺序将事件返回值组成一个列表返回 */ + NoReduce, + /** + * 将每个事件的返回值取或后返回,如果返回值类型不是布尔值, + * 那么按照正常短路运算符规则输出返回值,并抛出警告。 + */ + OrReduce, + /** + * 将每个事件的返回值取与后返回,如果返回值类型不是布尔值, + * 那么按照正常短路运算符规则输出返回值,并抛出警告 + */ + AndReduce +} + +export type GameEventBuiltinFunction = ( + param: IBlockEventParam, + env: IBlockEventEnv +) => any; + +export interface IGameEventInit { + /** + * 向事件系统添加内建函数 + * @param name 函数名称 + * @param func 函数内容 + */ + addBuiltinFunction(name: string, func: GameEventBuiltinFunction): void; +} + +export interface IGameEventExecutor { + /** 当前的执行器执行模式 */ + readonly mode: EventExecuteMode; + /** 当前的执行器返回值折叠方式 */ + readonly reduce: EventReduceMode; + /** 事件解释器 */ + readonly interpreter: AnonTokyoInterpreter; + + /** + * 设置执行器的执行模式 + * @param mode 执行模式 + */ + setMode(mode: EventExecuteMode): void; + + /** + * 设置执行器的返回值折叠方式 + * @param reduce 折叠方式 + */ + setReduce(reduce: EventReduceMode): void; + + /** + * 执行指定的事件列表 + * @param events 带有来源环境的事件调用列表 + * @param param 传递给事件的参数 + */ + execute( + events: IGameEventInvocation[], + param: IBlockEventParam + ): Promise; +} + +export interface IGameEventSystem extends IDataBaseExtended { + /** 游戏事件执行器 */ + readonly executor: IGameEventExecutor; + /** 事件系统使用的存储器 */ + readonly store: IGameEventStore | null; + + /** + * 设置系统使用的事件存储器 + * @param store 事件存储器 + */ + useStore(store: IGameEventStore | null): void; +}