feat(event): rework built-ins and centralize trigger collection

- rename deleteBlock -> removeBlock (dynamic tiles need dynamic:true),
  moveHeroStep -> stepHero; touchFront steps forward and lets the mover bump
- GameEventSystem.collectEvent owns point/tile source collection; moverImpl
  enter/leave/hit reuse it with OnEnter/OnLeave/OnTouch
- IBlockEventEnv gains required system field and nullable heroFloor
- MapLayer.removeBlock added and setBlock guarded by inMap; GameMap owns floorId
- IObjectMover.push accepts readonly steps
- update event/eventDispatch tests to the current implementation (fixtures
  supply system, hero location snapshots, renamed registrations)

Verified: data type gate 0 in-scope; vitest 19 files / 112 tests pass; prettier
and eslint clean on touched files.
This commit is contained in:
unanmed 2026-09-13 20:37:32 +08:00
parent d7837fcc24
commit a66be585d1
18 changed files with 252 additions and 363 deletions

View File

@ -311,13 +311,13 @@ export interface IHeroMoveTopImpl {
inBound(x: number, y: number, floorId: string | undefined): boolean;
/**
* 勇士正常移动到某个图块上时触发进入触发器
* 勇士正常移动到某个图块上时触发进入触发器,此时勇士位置处于目标位置
* @param handler 移动信息对象
*/
enter(handler: IHeroMoveTopHandler): Promise<void>;
/**
* 勇士将要离开当前图块时触发离开触发器
* 勇士将要离开当前图块时触发离开触发器,此时勇士位置处于目标位置
* @param handler 移动信息对象
*/
leave(handler: IHeroMoveTopHandler): Promise<void>;

View File

@ -48,6 +48,7 @@ export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
constructor(
public readonly state: IDataCommon,
public readonly tileStore: ITileStore,
public readonly floorId: string,
public width: number,
public height: number
) {

View File

@ -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);

View File

@ -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);

View File

@ -275,7 +275,7 @@ export interface IMapLayerHookController extends IHookController<IMapLayerHooks>
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;
/**
* 添加图层,使用楼层预设的宽高

View File

@ -334,7 +334,7 @@ export interface IObjectMover<T extends IObjectMovable> extends IHookable<
* 直接在当前的移动规划后追加新的移动步
* @param steps 要追加的移动步
*/
push(steps: ObjectMoveStep[]): this;
push(steps: readonly ObjectMoveStep[]): this;
/**
* 清空尚未执行的步骤队列

View File

@ -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'

View File

@ -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<void>(invocations, { custom: {} });
} finally {
exitEventInsert(env);
}
const invocations = this.collectInvocations(param.ids, env, store);
if (invocations.length === 0) return;
await env.system.executor.execute<void>(invocations, { custom: {} });
}
/**
@ -42,13 +32,9 @@ export class EventInsertEvents implements BuiltInFunction<
): IGameEventInvocation[] {
const invocations: IGameEventInvocation[] = [];
for (const id of ids) {
if (
!id ||
!store.getEvent<IBlockEventParam, IBlockEventEnv, void>(id)
) {
continue;
if (store.getEvent<IBlockEventParam, IBlockEventEnv, void>(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);
}
}

View File

@ -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<void>(invocations, { custom: {} });
}
/**
* 按坐标收集事件来源并保留其触发环境
*/
private collectInvocations(
env: IBlockEventEnv,
layer: NonNullable<IBlockEventEnv['layer']>,
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;
}
}

View File

@ -1,5 +1,5 @@
export { createEventRegistrations } from './registrations';
export * from './event';
export * from './hero';
export * from './map';
export * from './registrations';
export * from './types';

View File

@ -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);
}
}

View File

@ -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(),
// 事件控制

View File

@ -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 {}

View File

@ -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<IBlockEventEnv, number> = 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);
}

View File

@ -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<void> {
@ -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<void>(invocations, param);
await this.executor.execute<void>(invocations, { custom: {} });
}
async enter(handler: IHeroMoveTopHandler): Promise<void> {
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<void> {
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<void> {
const { x, y } = handler.nextLoc;
return this.commonTrigger(
EventTrigger.OnTouch,
handler,
handler.currLoc,
x,
y
);
return this.commonTrigger(EventTrigger.OnTouch, handler, x, y);
}
/**

View File

@ -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<string, unknown> = {
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([

View File

@ -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;
}
}

View File

@ -34,8 +34,10 @@ export interface IBlockEventEnv extends IDataBaseExtended {
readonly trigger: EventTrigger;
/** 触发事件时玩家的位置 */
readonly heroLocator: Readonly<ITileLocator>;
/** 事件系统对象 */
readonly system: IGameEventSystem;
/** 触发事件时玩家的位置 */
readonly heroFloor: string;
readonly heroFloor: string | undefined;
/** 触发事件时触发者的位置,有可能不存在 */
readonly triggerLocator: Readonly<ITileLocator> | 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[];
}