mirror of
https://github.com/motajs/template.git
synced 2026-10-21 06:40:16 +08:00
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:
parent
d7837fcc24
commit
a66be585d1
@ -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>;
|
||||
|
||||
@ -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
|
||||
) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* 添加图层,使用楼层预设的宽高
|
||||
|
||||
@ -334,7 +334,7 @@ export interface IObjectMover<T extends IObjectMovable> extends IHookable<
|
||||
* 直接在当前的移动规划后追加新的移动步
|
||||
* @param steps 要追加的移动步
|
||||
*/
|
||||
push(steps: ObjectMoveStep[]): this;
|
||||
push(steps: readonly ObjectMoveStep[]): this;
|
||||
|
||||
/**
|
||||
* 清空尚未执行的步骤队列
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export { createEventRegistrations } from './registrations';
|
||||
export * from './event';
|
||||
export * from './hero';
|
||||
export * from './map';
|
||||
export * from './registrations';
|
||||
export * from './types';
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
// 事件控制
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user