mirror of
https://github.com/motajs/template.git
synced 2026-09-14 02:48:51 +08:00
fix(01-07): dispatch events with source-aware trigger matching
- Carry each event id with its real point or tile environment. - Filter mismatched triggers before execution and include every dynamic tile in priority order.
This commit is contained in:
parent
87f2252ccc
commit
22056140bd
@ -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<R = void> extends IReadonlyGameEvent<
|
||||
IBlockEventParam,
|
||||
IBlockEventEnv,
|
||||
|
||||
@ -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<void> {
|
||||
@ -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<void>(invocations, param);
|
||||
}
|
||||
|
||||
async enter(handler: IHeroMoveTopHandler): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
const { x, y } = handler.nextLoc;
|
||||
return this.commonTrigger(TriggerType.CannotEnter, handler, x, y);
|
||||
/**
|
||||
* 新事件触发器没有无法进入的对应项,保留空实现以满足移动接口
|
||||
*/
|
||||
async cannotEnter(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
436
packages-user/data-system/src/event/eventDispatch.test.ts
Normal file
436
packages-user/data-system/src/event/eventDispatch.test.ts
Normal file
@ -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<number, Record<number, string>> = {},
|
||||
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<string, object>();
|
||||
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<string, object>,
|
||||
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<void>(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 });
|
||||
});
|
||||
});
|
||||
76
packages-user/data-system/src/event/executor.ts
Normal file
76
packages-user/data-system/src/event/executor.ts
Normal file
@ -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<R = void>(
|
||||
events: IGameEventInvocation[],
|
||||
param: IBlockEventParam
|
||||
): Promise<R> {
|
||||
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<IBlockEventParam, IBlockEventEnv, any>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
90
packages-user/data-system/src/event/types.ts
Normal file
90
packages-user/data-system/src/event/types.ts
Normal file
@ -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<R = void>(
|
||||
events: IGameEventInvocation[],
|
||||
param: IBlockEventParam
|
||||
): Promise<R>;
|
||||
}
|
||||
|
||||
export interface IGameEventSystem extends IDataBaseExtended {
|
||||
/** 游戏事件执行器 */
|
||||
readonly executor: IGameEventExecutor;
|
||||
/** 事件系统使用的存储器 */
|
||||
readonly store: IGameEventStore | null;
|
||||
|
||||
/**
|
||||
* 设置系统使用的事件存储器
|
||||
* @param store 事件存储器
|
||||
*/
|
||||
useStore(store: IGameEventStore | null): void;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user