From f0fd2f54a14137a500a073f1af9e48577d8565eb Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sun, 13 Sep 2026 19:17:32 +0800 Subject: [PATCH] refactor(event): class-based built-ins, shared utils, single registration - rewrite every built-in as EventXxx implements BuiltInFunction (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 --- packages-user/data-state/src/core.ts | 4 +- .../data-state/src/event/event.test.ts | 229 ++++------- packages-user/data-state/src/event/event.ts | 165 +++----- packages-user/data-state/src/event/hero.ts | 389 ++++++------------ packages-user/data-state/src/event/index.ts | 7 +- packages-user/data-state/src/event/map.ts | 110 ++--- .../data-state/src/event/registrations.ts | 67 +-- packages-user/data-state/src/event/types.ts | 14 +- packages-user/data-state/src/event/utils.ts | 51 ++- .../data-state/src/replay/commands.test.ts | 58 +-- .../data-state/test/fixtures/closed-loop.ts | 4 +- 11 files changed, 397 insertions(+), 701 deletions(-) diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index b574f34..6ce3141 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -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; diff --git a/packages-user/data-state/src/event/event.test.ts b/packages-user/data-state/src/event/event.test.ts index d3dc689..a44c971 100644 --- a/packages-user/data-state/src/event/event.test.ts +++ b/packages-user/data-state/src/event/event.test.ts @@ -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[number]; function invokeBuiltin( registration: RegisteredBuiltin, param: TParam, env: IBlockEventEnv ): Promise { - 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) { diff --git a/packages-user/data-state/src/event/event.ts b/packages-user/data-state/src/event/event.ts index 21af0d8..af02916 100644 --- a/packages-user/data-state/src/event/event.ts +++ b/packages-user/data-state/src/event/event.ts @@ -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 = 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(id) - ) { - continue; + if (!enterEventInsert(env)) return; + try { + const invocations = this.collectInvocations(param.ids, env, store); + if (invocations.length === 0) return; + await executor.execute(invocations, { custom: {} }); + } finally { + exitEventInsert(env); } - invocations.push({ id, env }); } - return invocations; -} -/** - * 临时按顺序执行指定事件 - */ -export async function eventInsertEvents( - param: IInsertEventsEventParam, - env: IBlockEventEnv -): Promise { - 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(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(id) + ) { + continue; + } + invocations.push({ id, env }); + } + return invocations; } } -/** - * 临时直接执行一段事件语句 - */ -export async function eventInsertEvent( - param: IInsertEventEventParam, - env: IBlockEventEnv -): Promise { - 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']; -} diff --git a/packages-user/data-state/src/event/hero.ts b/packages-user/data-state/src/event/hero.ts index cb7be2f..e73f26d 100644 --- a/packages-user/data-state/src/event/hero.ts +++ b/packages-user/data-state/src/event/hero.ts @@ -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( - mover: IObjectMover, - 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, - steps: readonly ObjectMoveStep[] -): Promise { - if (mover.moving) return; - appendMoveSteps(mover, steps); - const controller: Readonly | null = mover.start(); - if (!controller) return; - await controller.onEnd; -} - -/** - * 获取可以执行移动的勇士移动器 - */ -function getHeroMover(env: IBlockEventEnv): IHeroMover | null { - const hero = env.state.hero; - if (!hero) return null; - return hero.location.mover; -} - -/** - * 按指定移动序列移动勇士 - */ -export async function eventMoveHero( - param: IMoveHeroEventParam, - env: IBlockEventEnv -): Promise { - const mover = getHeroMover(env); - if (!mover) return; - await startHeroMove(mover, param.steps); -} - -/** - * 让勇士沿当前朝向移动一步 - */ -export async function eventMoveHeroStep( - _param: IMoveHeroStepEventParam, - env: IBlockEventEnv -): Promise { - 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, - 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(invocations, { custom: {} }); + } + + /** + * 按坐标收集事件来源并保留其触发环境 + */ + private collectInvocations( + env: IBlockEventEnv, + layer: NonNullable, + 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 { - 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(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']; } diff --git a/packages-user/data-state/src/event/index.ts b/packages-user/data-state/src/event/index.ts index e0833cb..1a9c181 100644 --- a/packages-user/data-state/src/event/index.ts +++ b/packages-user/data-state/src/event/index.ts @@ -1,9 +1,4 @@ -export { - createControlEventBuiltinRegistrations, - createEventBuiltinRegistrations, - createHeroEventBuiltinRegistrations, - createMapEventBuiltinRegistrations -} from './registrations'; +export { createEventRegistrations } from './registrations'; export * from './event'; export * from './hero'; export * from './map'; diff --git a/packages-user/data-state/src/event/map.ts b/packages-user/data-state/src/event/map.ts index c3bfea9..bf1fbb1 100644 --- a/packages-user/data-state/src/event/map.ts +++ b/packages-user/data-state/src/event/map.ts @@ -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 { - 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 { - 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']; -} diff --git a/packages-user/data-state/src/event/registrations.ts b/packages-user/data-state/src/event/registrations.ts index 0eb49df..c4ceb28 100644 --- a/packages-user/data-state/src/event/registrations.ts +++ b/packages-user/data-state/src/event/registrations.ts @@ -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 { +export function createEventRegistrations(): BuiltInFunction[] { return [ - new SetBlockEventRegistration(), - new MoveBlockEventRegistration(), - new DeleteBlockEventRegistration() - ]; -} + // 地图控制 + new EventSetBlock(), + new EventMoveBlock(), + new EventDeleteBlock(), -/** - * 创建勇士控制事件的内建函数注册项 - */ -export function createHeroEventBuiltinRegistrations(): ReadonlyArray { - return [ - new MoveHeroEventRegistration(), - new MoveHeroStepEventRegistration(), - new TouchFrontEventRegistration() - ]; -} + // 玩家控制 + new EventMoveHero(), + new EventMoveHeroStep(), + new EventTouchFront(), -/** - * 创建事件控制事件的内建函数注册项 - */ -export function createControlEventBuiltinRegistrations(): ReadonlyArray { - return [ - new InsertEventsEventRegistration(), - new InsertEventEventRegistration() - ]; -} - -/** - * 组装八个批准事件 built-in 的稳定注册项 - */ -export function createEventBuiltinRegistrations(): ReadonlyArray { - return [ - ...createMapEventBuiltinRegistrations(), - ...createHeroEventBuiltinRegistrations(), - ...createControlEventBuiltinRegistrations() + // 事件控制 + new EventInsertEvents(), + new EventInsertEvent() ]; } diff --git a/packages-user/data-state/src/event/types.ts b/packages-user/data-state/src/event/types.ts index 3f06a57..7d00303 100644 --- a/packages-user/data-state/src/event/types.ts +++ b/packages-user/data-state/src/event/types.ts @@ -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 diff --git a/packages-user/data-state/src/event/utils.ts b/packages-user/data-state/src/event/utils.ts index 8a739ea..580f065 100644 --- a/packages-user/data-state/src/event/utils.ts +++ b/packages-user/data-state/src/event/utils.ts @@ -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 = 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); +} diff --git a/packages-user/data-state/src/replay/commands.test.ts b/packages-user/data-state/src/replay/commands.test.ts index ff0dfbb..9d4e707 100644 --- a/packages-user/data-state/src/replay/commands.test.ts +++ b/packages-user/data-state/src/replay/commands.test.ts @@ -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 ); diff --git a/packages-user/data-state/test/fixtures/closed-loop.ts b/packages-user/data-state/test/fixtures/closed-loop.ts index c8cead7..0281eda 100644 --- a/packages-user/data-state/test/fixtures/closed-loop.ts +++ b/packages-user/data-state/test/fixtures/closed-loop.ts @@ -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 }