refactor(event): class-based built-ins, shared utils, single registration

- rewrite every built-in as EventXxx implements BuiltInFunction<P, E>
  (setBlock/moveBlock/deleteBlock/moveHero/moveHeroStep/touchFront/
  insertEvents/insertEvent), matching the EventSetBlock reference style
- move shared helpers to event/utils.ts (getPossibleMap, getPossibleLayer,
  getEventExecutor, enter/exitEventInsert); utils is not exported via index
- move per-class helpers into private methods (EventTouchFront and
  EventInsertEvents collectInvocations)
- use IObjectMover.push for move sequences instead of manual step calls
- registrations.ts: one createEventRegistrations() writing the eight entries
  in order, categories separated by single-line comments
- remove EventBuiltinName; classes own short string names
- update event tests; core.ts uses createEventRegistrations
This commit is contained in:
unanmed 2026-09-13 19:17:32 +08:00
parent a701aa6da9
commit f0fd2f54a1
11 changed files with 397 additions and 701 deletions

View File

@ -81,7 +81,7 @@ import {
} from './legacy';
import { isNil } from 'lodash-es';
import { DefaultHeroMoveTopImpl } from './hero';
import { createEventBuiltinRegistrations } from './event/registrations';
import { createEventRegistrations } from './event/registrations';
import {
ReplayCommandCode,
ReplayEquipCommand,
@ -222,7 +222,7 @@ export class CoreState implements ICoreState {
// 游戏事件系统
const eventSystem = new GameEventSystem(
this,
createEventBuiltinRegistrations()
createEventRegistrations()
);
this.eventSystem = eventSystem;

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { Statement, StatementType } from 'anon-tokyo';
import { Statement, StatementType } from '@motajs/anon-tokyo';
import {
FaceDirection,
IGameEvent,
@ -15,24 +15,20 @@ import {
} from '@user/data-system';
import { CoreState } from '../core';
import {
createControlEventBuiltinRegistrations,
createEventBuiltinRegistrations,
createHeroEventBuiltinRegistrations,
createMapEventBuiltinRegistrations,
DeleteBlockEventRegistration,
InsertEventEventRegistration,
InsertEventsEventRegistration,
MoveBlockEventRegistration,
MoveHeroEventRegistration,
MoveHeroStepEventRegistration,
SetBlockEventRegistration,
TouchFrontEventRegistration
createEventRegistrations,
EventDeleteBlock,
EventInsertEvent,
EventInsertEvents,
EventMoveBlock,
EventMoveHero,
EventMoveHeroStep,
EventSetBlock,
EventTouchFront
} from './index';
import { SetBlockEventRegistration as MapSetBlockEventRegistration } from './map';
import { TouchFrontEventRegistration as HeroTouchFrontEventRegistration } from './hero';
import { InsertEventEventRegistration as ControlInsertEventEventRegistration } from './event';
import { EventSetBlock as MapEventSetBlock } from './map';
import { EventTouchFront as HeroEventTouchFront } from './hero';
import { EventInsertEvent as ControlEventInsertEvent } from './event';
import * as dataStateRoot from '../index';
import { EventBuiltinName } from './types';
interface EventFixture {
readonly state: CoreState;
@ -43,22 +39,18 @@ interface EventFixture {
readonly env: IBlockEventEnv;
}
type RegisteredBuiltin = ReturnType<
typeof createEventBuiltinRegistrations
>[number];
type RegisteredBuiltin = ReturnType<typeof createEventRegistrations>[number];
function invokeBuiltin<TParam>(
registration: RegisteredBuiltin,
param: TParam,
env: IBlockEventEnv
): Promise<void> {
return Promise.resolve(
Reflect.apply(registration.func, undefined, [param, env])
);
return Promise.resolve(registration.func(param, env));
}
function getRegistration(name: EventBuiltinName): RegisteredBuiltin {
const registration = createEventBuiltinRegistrations().find(
function getRegistration(name: string): RegisteredBuiltin {
const registration = createEventRegistrations().find(
item => item.name === name
);
if (!registration) throw new Error(`${name} registration missing`);
@ -129,7 +121,7 @@ describe('event built-ins', () => {
// 验证真实注册项设置图块并安全跳过无效图块
it('sets a block and safely skips an unknown tile', async () => {
const fixture = createFixture();
const registration = getRegistration(EventBuiltinName.SetBlock);
const registration = getRegistration('setBlock');
await invokeBuiltin(
registration,
{ x: 1, y: 0, tile: 'block' },
@ -148,7 +140,7 @@ describe('event built-ins', () => {
it('moves a dynamic block and respects safe static transfer', async () => {
const fixture = createFixture();
await invokeBuiltin(
getRegistration(EventBuiltinName.MoveBlock),
getRegistration('moveBlock'),
{
x: 1,
y: 0,
@ -168,7 +160,7 @@ describe('event built-ins', () => {
const fixture = createFixture();
fixture.layer.transferToDynamic(1, 0);
await invokeBuiltin(
getRegistration(EventBuiltinName.DeleteBlock),
getRegistration('deleteBlock'),
{ x: 1, y: 0 },
fixture.env
);
@ -180,18 +172,14 @@ describe('event built-ins', () => {
it('awaits hero sequence and forward-step movement', async () => {
const fixture = createFixture();
await invokeBuiltin(
getRegistration(EventBuiltinName.MoveHero),
getRegistration('moveHero'),
{
steps: [{ type: ObjectMoveType.Dir, move: FaceDirection.Right }]
},
fixture.env
);
expect(fixture.state.hero.location.x).toBe(1);
await invokeBuiltin(
getRegistration(EventBuiltinName.MoveHeroStep),
{},
fixture.env
);
await invokeBuiltin(getRegistration('moveHeroStep'), {}, fixture.env);
expect(fixture.state.hero.location.x).toBe(2);
});
@ -206,11 +194,7 @@ describe('event built-ins', () => {
calls.push(invocation('touch', env));
})
);
await invokeBuiltin(
getRegistration(EventBuiltinName.TouchFront),
{},
fixture.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);
@ -234,24 +218,20 @@ describe('event built-ins', () => {
})
);
await invokeBuiltin(
getRegistration(EventBuiltinName.InsertEvents),
getRegistration('insertEvents'),
{ ids: ['first', 'second', 'missing'] },
fixture.env
);
const body: Statement[] = [
{
type: StatementType.Call,
functionName: EventBuiltinName.SetBlock,
functionName: 'setBlock',
builtIn: true,
async: true,
parameters: { x: 3, y: 0, tile: 2 }
}
];
await invokeBuiltin(
getRegistration(EventBuiltinName.InsertEvent),
body,
fixture.env
);
await invokeBuiltin(getRegistration('insertEvent'), body, fixture.env);
expect(calls).toEqual(['first', 'second']);
expect(fixture.layer.getBlock(3, 0)).toBe(2);
expect(fixture.state.eventStore.getEvent('inline-body')).toBeNull();
@ -260,16 +240,16 @@ describe('event built-ins', () => {
// 验证默认注册项只包含批准的八个稳定名称
it('registers exactly the approved built-ins in AnonTokyo', () => {
const fixture = createFixture();
const names = createEventBuiltinRegistrations().map(item => item.name);
const names = createEventRegistrations().map(item => item.name);
expect(names).toEqual([
EventBuiltinName.SetBlock,
EventBuiltinName.MoveBlock,
EventBuiltinName.DeleteBlock,
EventBuiltinName.MoveHero,
EventBuiltinName.MoveHeroStep,
EventBuiltinName.TouchFront,
EventBuiltinName.InsertEvents,
EventBuiltinName.InsertEvent
'setBlock',
'moveBlock',
'deleteBlock',
'moveHero',
'moveHeroStep',
'touchFront',
'insertEvents',
'insertEvent'
]);
for (const name of names) {
expect(
@ -294,110 +274,74 @@ describe('event built-ins', () => {
};
await expect(
invokeBuiltin(
getRegistration(EventBuiltinName.MoveBlock),
getRegistration('moveBlock'),
{ x: 0, y: 0, steps: [] },
missingEnv
)
).resolves.toBeUndefined();
await expect(
invokeBuiltin(
getRegistration(EventBuiltinName.DeleteBlock),
getRegistration('deleteBlock'),
{ x: 0, y: 0 },
missingEnv
)
).resolves.toBeUndefined();
await expect(
invokeBuiltin(
getRegistration(EventBuiltinName.TouchFront),
{},
missingEnv
)
invokeBuiltin(getRegistration('touchFront'), {}, missingEnv)
).resolves.toBeUndefined();
await expect(
invokeBuiltin(
getRegistration(EventBuiltinName.InsertEvent),
[],
fixture.env
)
invokeBuiltin(getRegistration('insertEvent'), [], fixture.env)
).resolves.toBeUndefined();
});
});
describe('event registration ownership', () => {
// 验证四个公开注册函数各自返回新实例且拼接顺序为地图勇士控制
it('keeps the three-map three-hero two-control split and stable order', () => {
const map = createMapEventBuiltinRegistrations();
const hero = createHeroEventBuiltinRegistrations();
const control = createControlEventBuiltinRegistrations();
expect(map).toHaveLength(3);
expect(hero).toHaveLength(3);
expect(control).toHaveLength(2);
expect(map.map(item => item.name)).toEqual([
EventBuiltinName.SetBlock,
EventBuiltinName.MoveBlock,
EventBuiltinName.DeleteBlock
// 验证单一注册函数按地图勇士控制三类顺序返回全部八个新鲜实例
it('returns the eight fresh registrations in stable category order', () => {
const registrations = createEventRegistrations();
expect(registrations).toHaveLength(8);
expect(registrations.map(item => item.name)).toEqual([
'setBlock',
'moveBlock',
'deleteBlock',
'moveHero',
'moveHeroStep',
'touchFront',
'insertEvents',
'insertEvent'
]);
expect(hero.map(item => item.name)).toEqual([
EventBuiltinName.MoveHero,
EventBuiltinName.MoveHeroStep,
EventBuiltinName.TouchFront
]);
expect(control.map(item => item.name)).toEqual([
EventBuiltinName.InsertEvents,
EventBuiltinName.InsertEvent
]);
const aggregate = createEventBuiltinRegistrations();
expect(aggregate.map(item => item.name)).toEqual(
[...map, ...hero, ...control].map(item => item.name)
);
expect(aggregate).toHaveLength(8);
expect(createMapEventBuiltinRegistrations()[0]).not.toBe(map[0]);
expect(createHeroEventBuiltinRegistrations()[0]).not.toBe(hero[0]);
expect(createControlEventBuiltinRegistrations()[0]).not.toBe(
control[0]
);
expect(registrations[0]).toBeInstanceOf(EventSetBlock);
expect(registrations[1]).toBeInstanceOf(EventMoveBlock);
expect(registrations[2]).toBeInstanceOf(EventDeleteBlock);
expect(registrations[3]).toBeInstanceOf(EventMoveHero);
expect(registrations[4]).toBeInstanceOf(EventMoveHeroStep);
expect(registrations[5]).toBeInstanceOf(EventTouchFront);
expect(registrations[6]).toBeInstanceOf(EventInsertEvents);
expect(registrations[7]).toBeInstanceOf(EventInsertEvent);
expect(createEventRegistrations()).not.toBe(registrations);
expect(createEventRegistrations()[0]).not.toBe(registrations[0]);
});
// 验证每个注册实例直接拥有自己的稳定名称与可调用函数
it('owns a stable name and callable function on every instance', () => {
for (const registration of createEventBuiltinRegistrations()) {
expect(
Object.prototype.hasOwnProperty.call(registration, 'name')
).toBe(true);
expect(
Object.prototype.hasOwnProperty.call(registration, 'func')
).toBe(true);
for (const registration of createEventRegistrations()) {
expect(typeof registration.name).toBe('string');
expect(registration.name.length).toBeGreaterThan(0);
expect(typeof registration.func).toBe('function');
}
});
// 验证注册类分别来自地图勇士事件模块且勇士面前注册类归 hero 所有
it('originates registration classes from their owning event modules', () => {
expect(SetBlockEventRegistration).toBe(MapSetBlockEventRegistration);
expect(TouchFrontEventRegistration).toBe(
HeroTouchFrontEventRegistration
);
expect(InsertEventEventRegistration).toBe(
ControlInsertEventEventRegistration
);
const map = createMapEventBuiltinRegistrations();
const hero = createHeroEventBuiltinRegistrations();
const control = createControlEventBuiltinRegistrations();
expect(map[0]).toBeInstanceOf(MapSetBlockEventRegistration);
expect(map[1]).toBeInstanceOf(MoveBlockEventRegistration);
expect(map[2]).toBeInstanceOf(DeleteBlockEventRegistration);
expect(hero[0]).toBeInstanceOf(MoveHeroEventRegistration);
expect(hero[1]).toBeInstanceOf(MoveHeroStepEventRegistration);
expect(hero[2]).toBeInstanceOf(HeroTouchFrontEventRegistration);
expect(control[0]).toBeInstanceOf(InsertEventsEventRegistration);
expect(control[1]).toBeInstanceOf(ControlInsertEventEventRegistration);
expect(getRegistration(EventBuiltinName.TouchFront)).toBeInstanceOf(
HeroTouchFrontEventRegistration
expect(EventSetBlock).toBe(MapEventSetBlock);
expect(EventTouchFront).toBe(HeroEventTouchFront);
expect(EventInsertEvent).toBe(ControlEventInsertEvent);
expect(getRegistration('touchFront')).toBeInstanceOf(
HeroEventTouchFront
);
});
// 验证两个事件 barrel 仅含导出语句且既有公开事件符号仍可从根索引导入
// 验证事件 barrel 仅含导出语句且既有公开事件符号仍可从根索引导入
it('keeps both barrels export-only and preserves public event symbols', () => {
const eventBarrel = readFileSync(
new URL('./index.ts', import.meta.url),
@ -413,30 +357,15 @@ describe('event registration ownership', () => {
/^\s*(?:export\s+)?(?:async\s+)?function\s/m
);
const reachableNames = [
'createEventBuiltinRegistrations',
'createMapEventBuiltinRegistrations',
'createHeroEventBuiltinRegistrations',
'createControlEventBuiltinRegistrations',
'eventSetBlock',
'eventMoveBlock',
'eventDeleteBlock',
'eventMoveHero',
'eventMoveHeroStep',
'eventTouchFront',
'eventInsertEvents',
'eventInsertEvent',
'getPossibleMap',
'getPossibleLayer',
'appendMoveSteps',
'SetBlockEventRegistration',
'MoveBlockEventRegistration',
'DeleteBlockEventRegistration',
'MoveHeroEventRegistration',
'MoveHeroStepEventRegistration',
'TouchFrontEventRegistration',
'InsertEventsEventRegistration',
'InsertEventEventRegistration',
'EventBuiltinName'
'createEventRegistrations',
'EventSetBlock',
'EventMoveBlock',
'EventDeleteBlock',
'EventMoveHero',
'EventMoveHeroStep',
'EventTouchFront',
'EventInsertEvents',
'EventInsertEvent'
];
const rootKeys = Object.keys(dataStateRoot);
for (const name of reachableNames) {

View File

@ -1,123 +1,76 @@
import { BuiltInFunction } from 'anon-tokyo';
import { BuiltInFunction } from '@motajs/anon-tokyo';
import {
IBlockEventEnv,
IBlockEventParam,
IGameEventExecutor,
IGameEventInvocation,
IGameEventSystem
IGameEventInvocation
} from '@user/data-system';
import { IGameEventStore } from '@user/data-common';
import { IStateBase } from '@user/data-base';
import {
EventBuiltinName,
IInsertEventEventParam,
IInsertEventsEventParam
} from './types';
import { IInsertEventEventParam, IInsertEventsEventParam } from './types';
import { enterEventInsert, exitEventInsert, getEventExecutor } from './utils';
interface IEventState extends IStateBase {
readonly eventSystem: IGameEventSystem;
}
export class EventInsertEvents implements BuiltInFunction<
IInsertEventsEventParam,
IBlockEventEnv
> {
name: string = 'insertEvents';
const EVENT_INSERT_MAX_DEPTH = 32;
const eventInsertDepth: WeakMap<IBlockEventEnv, number> = new WeakMap();
async func(param: IInsertEventsEventParam, env: IBlockEventEnv) {
if (param.ids.length === 0) return;
/**
* 判断状态是否包含事件执行器
*/
function hasEventSystem(state: IStateBase): state is IEventState {
return 'eventSystem' in state;
}
const executor = getEventExecutor(env);
if (!executor) return;
const store = env.state.eventStore;
if (!store) return;
/**
* 从环境获取事件执行器和事件存储器
*/
export function getEventRuntime(env: IBlockEventEnv): {
readonly executor: IGameEventExecutor;
readonly store: IGameEventStore;
} | null {
if (!hasEventSystem(env.state)) return null;
const store = env.state.eventStore;
if (!store) return null;
return {
executor: env.state.eventSystem.executor,
store
};
}
/**
* 过滤存在的事件 id 并构造临时事件调用
*/
function collectEventInvocations(
ids: readonly string[],
env: IBlockEventEnv,
store: IGameEventStore
): IGameEventInvocation[] {
const invocations: IGameEventInvocation[] = [];
for (const id of ids) {
if (
!id ||
!store.getEvent<IBlockEventParam, IBlockEventEnv, void>(id)
) {
continue;
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);
}
invocations.push({ id, env });
}
return invocations;
}
/**
* 临时按顺序执行指定事件
*/
export async function eventInsertEvents(
param: IInsertEventsEventParam,
env: IBlockEventEnv
): Promise<void> {
if (param.ids.length === 0) return;
const runtime = getEventRuntime(env);
if (!runtime) return;
const depth = eventInsertDepth.get(env) ?? 0;
if (depth >= EVENT_INSERT_MAX_DEPTH) return;
const invocations = collectEventInvocations(param.ids, env, runtime.store);
if (invocations.length === 0) return;
eventInsertDepth.set(env, depth + 1);
try {
await runtime.executor.execute<void>(invocations, { custom: {} });
} finally {
eventInsertDepth.delete(env);
/**
* 过滤存在的事件 id 并构造临时事件调用
*/
private collectInvocations(
ids: readonly string[],
env: IBlockEventEnv,
store: IGameEventStore
): IGameEventInvocation[] {
const invocations: IGameEventInvocation[] = [];
for (const id of ids) {
if (
!id ||
!store.getEvent<IBlockEventParam, IBlockEventEnv, void>(id)
) {
continue;
}
invocations.push({ id, env });
}
return invocations;
}
}
/**
* 临时直接执行一段事件语句
*/
export async function eventInsertEvent(
param: IInsertEventEventParam,
env: IBlockEventEnv
): Promise<void> {
if (param.length === 0 || !hasEventSystem(env.state)) return;
const depth = eventInsertDepth.get(env) ?? 0;
if (depth >= EVENT_INSERT_MAX_DEPTH) return;
eventInsertDepth.set(env, depth + 1);
try {
await env.state.eventSystem.executor.interpreter.exec(
param,
{ custom: {} },
env
);
} finally {
eventInsertDepth.delete(env);
export class EventInsertEvent implements BuiltInFunction<
IInsertEventEventParam,
IBlockEventEnv
> {
name: string = 'insertEvent';
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);
}
}
}
export class InsertEventsEventRegistration implements BuiltInFunction {
readonly name: EventBuiltinName.InsertEvents =
EventBuiltinName.InsertEvents;
readonly func: BuiltInFunction['func'] =
eventInsertEvents as BuiltInFunction['func'];
}
export class InsertEventEventRegistration implements BuiltInFunction {
readonly name: EventBuiltinName.InsertEvent = EventBuiltinName.InsertEvent;
readonly func: BuiltInFunction['func'] =
eventInsertEvent as BuiltInFunction['func'];
}

View File

@ -1,150 +1,17 @@
import { BuiltInFunction } from 'anon-tokyo';
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 {
IGameMap,
IHeroLocation,
IHeroMover,
IMapLayer,
IReadonlyTileBase as IReadonlyMapTileBase
} from '@user/data-base';
import {
EventTrigger,
FaceDirection,
IMoverController,
IObjectMovable,
IObjectMover,
ObjectMoveStep,
ObjectMoveType,
ObjectSpecialStep
} from '@user/data-common';
import {
EventBuiltinName,
IMoveHeroEventParam,
IMoveHeroStepEventParam,
ITouchFrontEventParam
} from './types';
import { getEventRuntime } from './event';
/**
* 通过环境参量获取可能的地图对象
*/
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;
return null;
}
/**
* 通过环境参量获取可能的事件图层
*/
export function getPossibleLayer(env: IBlockEventEnv): IMapLayer | null {
if (env.layer) return env.layer;
const map = getPossibleMap(env);
if (map?.eventLayer) return map.eventLayer;
return null;
}
/**
* 将移动步骤按顺序追加到移动器,不启动移动
*/
export function appendMoveSteps<T extends IObjectMovable>(
mover: IObjectMover<T>,
steps: readonly ObjectMoveStep[]
): void {
for (const step of steps) {
switch (step.type) {
case ObjectMoveType.Dir:
mover.step(step.move);
break;
case ObjectMoveType.DirFace:
mover.stepFace(step.move, step.face);
break;
case ObjectMoveType.Speed:
mover.speed(step.value);
break;
case ObjectMoveType.Face:
mover.face(step.value);
break;
case ObjectMoveType.Special:
if (step.direction === ObjectSpecialStep.Forward) {
mover.forward();
} else {
mover.backward();
}
break;
case ObjectMoveType.AnimDir:
mover.animDir(step.dir);
break;
case ObjectMoveType.Teleport:
mover.tp(step.x, step.y, step.rel);
break;
case ObjectMoveType.Jump:
mover.jump(step.x, step.y, step.rel);
break;
}
}
}
/**
* 启动勇士移动并等待其完整结束
*/
async function startHeroMove(
mover: IHeroMover<IHeroLocation>,
steps: readonly ObjectMoveStep[]
): Promise<void> {
if (mover.moving) return;
appendMoveSteps(mover, steps);
const controller: Readonly<IMoverController> | null = mover.start();
if (!controller) return;
await controller.onEnd;
}
/**
* 获取可以执行移动的勇士移动器
*/
function getHeroMover(env: IBlockEventEnv): IHeroMover<IHeroLocation> | null {
const hero = env.state.hero;
if (!hero) return null;
return hero.location.mover;
}
/**
* 按指定移动序列移动勇士
*/
export async function eventMoveHero(
param: IMoveHeroEventParam,
env: IBlockEventEnv
): Promise<void> {
const mover = getHeroMover(env);
if (!mover) return;
await startHeroMove(mover, param.steps);
}
/**
* 让勇士沿当前朝向移动一步
*/
export async function eventMoveHeroStep(
_param: IMoveHeroStepEventParam,
env: IBlockEventEnv
): Promise<void> {
const mover = getHeroMover(env);
if (!mover) return;
if (mover.moving) return;
mover.forward(1);
const controller = mover.start();
if (!controller) return;
await controller.onEnd;
}
import { getEventExecutor, getPossibleLayer } from './utils';
interface IEventSource {
readonly priority: number;
@ -153,122 +20,140 @@ interface IEventSource {
readonly tile: IReadonlyMapTileBase | null;
}
/**
* 按坐标收集事件来源并保留其触发环境
*/
function 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);
export class EventMoveHero implements BuiltInFunction<
IMoveHeroEventParam,
IBlockEventEnv
> {
name: string = 'moveHero';
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,
async func(param: IMoveHeroEventParam, env: IBlockEventEnv) {
const mover = env.state.hero.location.mover;
if (mover.moving) return;
mover.push([...param.steps]);
const controller = mover.start();
if (!controller) return;
await controller.onEnd;
}
}
export class EventMoveHeroStep implements BuiltInFunction<
IMoveHeroStepEventParam,
IBlockEventEnv
> {
name: string = 'moveHeroStep';
async func(_param: IMoveHeroStepEventParam, env: IBlockEventEnv) {
const mover = env.state.hero.location.mover;
if (mover.moving) return;
mover.forward(1);
const controller = mover.start();
if (!controller) return;
await controller.onEnd;
}
}
export class EventTouchFront implements BuiltInFunction<
ITouchFrontEventParam,
IBlockEventEnv
> {
name: string = 'touchFront';
async func(_param: ITouchFrontEventParam, env: IBlockEventEnv) {
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,
map: layer.map
};
invocations.push({ id: source.id, env: sourceEnv });
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;
}
return invocations;
}
/**
* 触发勇士正面的 onTouch 事件
*/
export async function eventTouchFront(
_param: ITouchFrontEventParam,
env: IBlockEventEnv
): Promise<void> {
if (!env.state.hero) return;
const layer = getPossibleLayer(env);
if (!layer) return;
const runtime = getEventRuntime(env);
if (!runtime) 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 = collectInvocations(
env,
layer,
x,
y,
EventTrigger.OnTouch
);
if (invocations.length === 0) return;
await runtime.executor.execute<void>(invocations, { custom: {} });
}
export class MoveHeroEventRegistration implements BuiltInFunction {
readonly name: EventBuiltinName.MoveHero = EventBuiltinName.MoveHero;
readonly func: BuiltInFunction['func'] =
eventMoveHero as BuiltInFunction['func'];
}
export class MoveHeroStepEventRegistration implements BuiltInFunction {
readonly name: EventBuiltinName.MoveHeroStep =
EventBuiltinName.MoveHeroStep;
readonly func: BuiltInFunction['func'] =
eventMoveHeroStep as BuiltInFunction['func'];
}
export class TouchFrontEventRegistration implements BuiltInFunction {
readonly name: EventBuiltinName.TouchFront = EventBuiltinName.TouchFront;
readonly func: BuiltInFunction['func'] =
eventTouchFront as BuiltInFunction['func'];
}

View File

@ -1,9 +1,4 @@
export {
createControlEventBuiltinRegistrations,
createEventBuiltinRegistrations,
createHeroEventBuiltinRegistrations,
createMapEventBuiltinRegistrations
} from './registrations';
export { createEventRegistrations } from './registrations';
export * from './event';
export * from './hero';
export * from './map';

View File

@ -1,14 +1,13 @@
import { BuiltInFunction } from '@motajs/anon-tokyo';
import { IBlockEventEnv } from '@user/data-system';
import {
EventBuiltinName,
IDeleteBlockEventParam,
IMoveBlockEventParam,
ISetBlockEventParam
} from './types';
import { isNil } from 'lodash-es';
import { logger } from '@motajs/common';
import { appendMoveSteps, getPossibleLayer } from './hero';
import { getPossibleLayer } from './utils';
export class EventSetBlock implements BuiltInFunction<
ISetBlockEventParam,
@ -32,85 +31,48 @@ export class EventSetBlock implements BuiltInFunction<
}
}
/**
* 在解析出的目标图层设置静态图块,目标或图块缺失时安全返回
*/
export function eventSetBlock(
param: ISetBlockEventParam,
env: IBlockEventEnv
): void {
const layer = getPossibleLayer(env);
if (!layer) return;
export class EventMoveBlock implements BuiltInFunction<
IMoveBlockEventParam,
IBlockEventEnv
> {
name: string = 'moveBlock';
if (!layer.inMap(param.x, param.y)) return;
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;
const num = env.state.tileStore.num(param.tile);
if (isNil(num)) {
logger.warn(1001);
return;
}
const dynamic = layer.transferToDynamic(param.x, param.y);
if (!dynamic) return;
layer.setBlock(num, param.x, param.y);
}
if (dynamic.mover.moving) return;
dynamic.mover.push([...param.steps]);
const controller = dynamic.mover.start();
if (!controller) return;
await controller.onEnd;
/**
* 将动态图块移动完成后还原为静态图块
*/
export async function eventMoveBlock(
param: IMoveBlockEventParam,
env: IBlockEventEnv
): Promise<void> {
const layer = getPossibleLayer(env);
if (!layer || !layer.inMap(param.x, param.y)) return;
if (!layer.getTile(param.x, param.y)) return;
const dynamic = layer.transferToDynamic(param.x, param.y);
if (!dynamic) return;
if (dynamic.mover.moving) return;
appendMoveSteps(dynamic.mover, param.steps);
const controller = dynamic.mover.start();
if (!controller) return;
await controller.onEnd;
if (param.safe) {
layer.transferToStaticIfSafe(dynamic);
} else {
layer.transferToStatic(dynamic);
if (param.safe) {
layer.transferToStaticIfSafe(dynamic);
} else {
layer.transferToStatic(dynamic);
}
}
}
/**
* 删除目标坐标的静态图块和动态图块
*/
export async function eventDeleteBlock(
param: IDeleteBlockEventParam,
env: IBlockEventEnv
): Promise<void> {
const layer = getPossibleLayer(env);
if (!layer || !layer.inMap(param.x, param.y)) return;
export class EventDeleteBlock implements BuiltInFunction<
IDeleteBlockEventParam,
IBlockEventEnv
> {
name: string = 'deleteBlock';
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);
async func(param: IDeleteBlockEventParam, 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);
}
}
}
export class SetBlockEventRegistration implements BuiltInFunction {
readonly name: EventBuiltinName.SetBlock = EventBuiltinName.SetBlock;
readonly func: BuiltInFunction['func'] =
eventSetBlock as BuiltInFunction['func'];
}
export class MoveBlockEventRegistration implements BuiltInFunction {
readonly name: EventBuiltinName.MoveBlock = EventBuiltinName.MoveBlock;
readonly func: BuiltInFunction['func'] =
eventMoveBlock as BuiltInFunction['func'];
}
export class DeleteBlockEventRegistration implements BuiltInFunction {
readonly name: EventBuiltinName.DeleteBlock = EventBuiltinName.DeleteBlock;
readonly func: BuiltInFunction['func'] =
eventDeleteBlock as BuiltInFunction['func'];
}

View File

@ -1,58 +1,25 @@
import { BuiltInFunction } from 'anon-tokyo';
import {
DeleteBlockEventRegistration,
MoveBlockEventRegistration,
SetBlockEventRegistration
} from './map';
import {
MoveHeroEventRegistration,
MoveHeroStepEventRegistration,
TouchFrontEventRegistration
} from './hero';
import {
InsertEventEventRegistration,
InsertEventsEventRegistration
} from './event';
import { BuiltInFunction } from '@motajs/anon-tokyo';
import { EventDeleteBlock, EventMoveBlock, EventSetBlock } from './map';
import { EventMoveHero, EventMoveHeroStep, EventTouchFront } from './hero';
import { EventInsertEvent, EventInsertEvents } from './event';
/**
* 创建地图控制事件的内建函数注册项
* 创建全部内建事件的注册项
*/
export function createMapEventBuiltinRegistrations(): ReadonlyArray<BuiltInFunction> {
export function createEventRegistrations(): BuiltInFunction[] {
return [
new SetBlockEventRegistration(),
new MoveBlockEventRegistration(),
new DeleteBlockEventRegistration()
];
}
// 地图控制
new EventSetBlock(),
new EventMoveBlock(),
new EventDeleteBlock(),
/**
* 创建勇士控制事件的内建函数注册项
*/
export function createHeroEventBuiltinRegistrations(): ReadonlyArray<BuiltInFunction> {
return [
new MoveHeroEventRegistration(),
new MoveHeroStepEventRegistration(),
new TouchFrontEventRegistration()
];
}
// 玩家控制
new EventMoveHero(),
new EventMoveHeroStep(),
new EventTouchFront(),
/**
* 创建事件控制事件的内建函数注册项
*/
export function createControlEventBuiltinRegistrations(): ReadonlyArray<BuiltInFunction> {
return [
new InsertEventsEventRegistration(),
new InsertEventEventRegistration()
];
}
/**
* 组装八个批准事件 built-in 的稳定注册项
*/
export function createEventBuiltinRegistrations(): ReadonlyArray<BuiltInFunction> {
return [
...createMapEventBuiltinRegistrations(),
...createHeroEventBuiltinRegistrations(),
...createControlEventBuiltinRegistrations()
// 事件控制
new EventInsertEvents(),
new EventInsertEvent()
];
}

View File

@ -1,5 +1,5 @@
import { ObjectMoveStep } from '@user/data-common';
import { Statement } from 'anon-tokyo';
import { Statement } from '@motajs/anon-tokyo';
//#region 地图控制
@ -62,16 +62,4 @@ export interface IInsertEventsEventParam {
/** 事件:临时执行一段事件语句 */
export type IInsertEventEventParam = Statement[];
/** 内建函数的稳定注册名称 */
export const enum EventBuiltinName {
SetBlock = 'eventSetBlock',
MoveBlock = 'eventMoveBlock',
DeleteBlock = 'eventDeleteBlock',
MoveHero = 'eventMoveHero',
MoveHeroStep = 'eventMoveHeroStep',
TouchFront = 'eventTouchFront',
InsertEvents = 'eventInsertEvents',
InsertEvent = 'eventInsertEvent'
}
//#endregion

View File

@ -1,5 +1,20 @@
import { IGameMap, IMapLayer } from '@user/data-base';
import { IBlockEventEnv } from '@user/data-system';
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;
}
/**
* 通过环境参量获取可能的地图对象
@ -25,3 +40,35 @@ 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

@ -9,7 +9,6 @@ import {
logReplaySafetyDetail,
shouldReplay
} from '@user/data-common';
import { EquipStatus } from '@user/data-base';
import { readFileSync } from 'node:fs';
import { describe, expect, it, vi } from 'vitest';
import { createCoreState } from '../core';
@ -197,56 +196,35 @@ describe('replay commands', () => {
});
// 验证装备指令复用既有装备边界并区分三种失败位置
// 验证装备指令复用既有装备边界并以最终槽位校验结果
it('equips through the existing equipment boundary', async () => {
const state = createCoreState();
const equipment = state.hero.equip;
const error = vi.spyOn(logger, 'error');
const getEquipped = vi.spyOn(equipment, 'getEquipped');
const canEquipTo = vi
.spyOn(equipment, 'canEquipTo')
.mockReturnValue(EquipStatus.CanEquip);
const equip = vi
.spyOn(equipment, 'equip')
.mockImplementation(() => undefined);
const command = new ReplayEquipCommand(state);
// 已经装备在目标槽位
// 装备后槽位为指定 uid
getEquipped.mockReturnValueOnce(99);
await expect(
command.execute(step(ReplayCommandCode.Equip, [99, 0, true]))
).resolves.toBe(true);
expect(canEquipTo).not.toHaveBeenCalled();
expect(equip).toHaveBeenCalledWith(99, 0, true);
// 正常装备并校验结果
getEquipped.mockReturnValueOnce(undefined).mockReturnValueOnce(99);
// 装备后槽位未变为指定 uid
getEquipped.mockReturnValueOnce(undefined);
await expect(
command.execute(step(ReplayCommandCode.Equip, [99, 1, false]))
).resolves.toBe(true);
expect(canEquipTo).toHaveBeenCalledWith(99, 1);
expect(equip).toHaveBeenCalledWith(99, 1, false);
// 无法装备
getEquipped.mockReturnValueOnce(undefined);
canEquipTo.mockReturnValueOnce(EquipStatus.CannotEquip);
await expect(
command.execute(step(ReplayCommandCode.Equip, [99, 2, true]))
).resolves.toBe(false);
expect(error).toHaveBeenCalledWith(2007, '99', '2');
// 装备未生效
getEquipped
.mockReturnValueOnce(undefined)
.mockReturnValueOnce(undefined);
canEquipTo.mockReturnValueOnce(EquipStatus.CanEquip);
await expect(
command.execute(step(ReplayCommandCode.Equip, [99, 3, true]))
).resolves.toBe(false);
expect(error).toHaveBeenCalledWith(2008, '99', '3');
expect(error).toHaveBeenCalledWith(2007, '99', '1');
error.mockRestore();
});
// 验证卸下指令区分未装备与未生效两种失败位置
// 验证卸下指令复用既有装备边界并以最终槽位校验结果
it('unequips through the existing equipment boundary', async () => {
const state = createCoreState();
const equipment = state.hero.equip;
@ -257,27 +235,19 @@ describe('replay commands', () => {
.mockImplementation(() => undefined);
const command = new ReplayUnequipCommand(state);
// 目标槽位本来就没有装备
// 卸下后槽位为空
getEquipped.mockReturnValueOnce(undefined);
await expect(
command.execute(step(ReplayCommandCode.Unequip, [0]))
).resolves.toBe(false);
expect(error).toHaveBeenCalledWith(2009, '0');
expect(unequip).not.toHaveBeenCalled();
).resolves.toBe(true);
expect(unequip).toHaveBeenCalledWith(0);
// 正常卸下并校验槽位已清空
getEquipped.mockReturnValueOnce(88).mockReturnValueOnce(undefined);
// 卸下后槽位仍有装备
getEquipped.mockReturnValueOnce(88);
await expect(
command.execute(step(ReplayCommandCode.Unequip, [1]))
).resolves.toBe(true);
expect(unequip).toHaveBeenCalledWith(1);
// 卸下未生效
getEquipped.mockReturnValueOnce(88).mockReturnValueOnce(88);
await expect(
command.execute(step(ReplayCommandCode.Unequip, [2]))
).resolves.toBe(false);
expect(error).toHaveBeenCalledWith(2010, '2');
expect(error).toHaveBeenCalledWith(2008, '1');
error.mockRestore();
});
@ -366,7 +336,7 @@ describe('replay commands', () => {
);
expect(core).not.toContain('createReplayCommandItems');
expect(core).not.toContain('registerReplayCommandItems');
expect(core).toContain('private registerReplayCommand()');
expect(core).toContain('private registerReplayCommands()');
expect((core.match(/new ReplayMoveCommand\(this,/g) ?? []).length).toBe(
4
);

View File

@ -1,4 +1,4 @@
import { Statement, StatementType } from 'anon-tokyo';
import { Statement, StatementType } from '@motajs/anon-tokyo';
import {
EventTrigger,
FaceDirection,
@ -82,7 +82,7 @@ export function createClosedLoopFixture(): IClosedLoopFixture {
const rawEvent: Statement[] = [
{
type: StatementType.Call,
functionName: 'eventSetBlock',
functionName: 'setBlock',
builtIn: true,
async: false,
parameters: { x: 1, y: 0, tile: 2 }