From 3134537d3025a1e17d9bd3ed2ed838bc707c5bb1 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Thu, 10 Sep 2026 16:25:02 +0800 Subject: [PATCH] feat(03-02): implement approved event built-ins - Add map, hero, touch, and temporary event controls - Register exactly eight built-ins through top-level event assembly - Cover awaited behavior and safe failure with explicit fixtures --- packages-user/data-state/src/core.ts | 6 +- .../data-state/src/event/event.test.ts | 230 ++++++++++++++++++ packages-user/data-state/src/event/event.ts | 201 +++++++++++++++ packages-user/data-state/src/event/hero.ts | 93 +++++++ packages-user/data-state/src/event/index.ts | 193 +++++++++++++++ packages-user/data-state/src/event/map.ts | 70 +++++- packages-user/data-state/src/event/types.ts | 58 +++++ packages-user/data-state/src/index.ts | 1 + packages-user/data-system/src/event/system.ts | 9 +- 9 files changed, 849 insertions(+), 12 deletions(-) create mode 100644 packages-user/data-state/src/event/event.test.ts diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index 51e7b58..66e9b70 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -72,6 +72,7 @@ import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader'; import { isNil } from 'lodash-es'; import { DirectionMapper, IDirectionMapper, logger } from '@motajs/common'; import { DefaultHeroMoveTopImpl } from './hero'; +import { createEventBuiltinRegistrations } from './event'; export class CoreState implements ICoreState { // Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据 @@ -207,7 +208,10 @@ export class CoreState implements ICoreState { this.enemyContext = enemyContext; // 游戏事件系统 - const eventSystem = new GameEventSystem(this); + const eventSystem = new GameEventSystem( + this, + createEventBuiltinRegistrations() + ); this.eventSystem = eventSystem; //#endregion diff --git a/packages-user/data-state/src/event/event.test.ts b/packages-user/data-state/src/event/event.test.ts new file mode 100644 index 0000000..1c834bc --- /dev/null +++ b/packages-user/data-state/src/event/event.test.ts @@ -0,0 +1,230 @@ +import { describe, expect, it } from 'vitest'; +import { + FaceDirection, + IGameEvent, + ObjectMoveType, + EventTrigger, + TileType +} from '@user/data-common'; +import { + BlockEventType, + IBlockEventEnv, + IGameEventInvocation +} from '@user/data-system'; +import { CoreState } from '../core'; +import { eventDeleteBlock, eventMoveBlock, eventSetBlock } from './map'; +import { eventMoveHero, eventMoveHeroStep } from './hero'; +import { eventInsertEvent, eventInsertEvents, eventTouchFront } from './event'; +import { createEventBuiltinRegistrations } from './index'; +import { EventBuiltinName } from './types'; + +interface EventFixture { + readonly state: CoreState; + readonly map: ReturnType; + readonly layer: ReturnType< + ReturnType['addLayer'] + >; + readonly env: IBlockEventEnv; +} + +function createFixture(): EventFixture { + const state = new CoreState(); + state.tileStore.addTile({ + num: 1, + id: 'floor', + events: {}, + type: TileType.Terrain, + pass: { onlyEvents: false, outPass: 15, inPass: 15 }, + eventPass: true + }); + state.tileStore.addTile({ + num: 2, + id: 'block', + events: {}, + type: TileType.Terrain, + pass: { onlyEvents: false, outPass: 15, inPass: 15 }, + eventPass: true + }); + const map = state.maps.createMap('F1', 4, 1); + const layer = map.addLayer(); + layer.setMapRef(new Uint32Array([1, 1, 1, 1])); + map.setEventLayer(layer); + state.hero.location.setFloor('F1'); + state.hero.location.setPos(0, 0); + state.hero.location.mover.setFaceDir(FaceDirection.Right); + const env: IBlockEventEnv = { + state, + type: BlockEventType.CommonEvent, + trigger: EventTrigger.None, + heroLocator: state.hero.getLocation(), + heroFloor: 'F1', + triggerLocator: null, + tile: null, + layer, + map + }; + return { state, map, layer, env }; +} + +function createEvent( + state: CoreState, + trigger: EventTrigger, + execute: (env: IBlockEventEnv) => Promise +): IGameEvent, IBlockEventEnv, void> { + return { + interpreter: state.eventSystem.executor.interpreter, + trigger, + rawEvent: [], + compiled: null, + compile: () => null, + execute: async (_param, env) => execute(env), + setTrigger: () => {}, + setRaw: () => {} + }; +} + +function invocation(id: string, env: IBlockEventEnv): IGameEventInvocation { + return { id, env }; +} + +describe('event built-ins', () => { + // 验证设置图块能解析环境图层并拒绝无效图块 + it('sets a block and safely skips an unknown tile', () => { + const fixture = createFixture(); + eventSetBlock({ x: 1, y: 0, tile: 'block' }, fixture.env); + expect(fixture.layer.getBlock(1, 0)).toBe(2); + expect(() => + eventSetBlock({ x: 1, y: 0, tile: 'missing' }, fixture.env) + ).not.toThrow(); + }); + + // 验证动态图块移动完成后会按 safe 分支还原 + it('moves a dynamic block and respects safe static transfer', async () => { + const fixture = createFixture(); + await eventMoveBlock( + { + x: 1, + y: 0, + steps: [ + { type: ObjectMoveType.Teleport, x: 2, y: 0, rel: false } + ], + safe: true + }, + fixture.env + ); + expect([...fixture.layer.getDynamicTilesAt(2, 0)]).toHaveLength(1); + expect(fixture.layer.getBlock(2, 0)).toBe(1); + }); + + // 验证删除图块会等待并清理静态与动态图块 + it('deletes static and dynamic blocks at a coordinate', async () => { + const fixture = createFixture(); + fixture.layer.transferToDynamic(1, 0); + await eventDeleteBlock({ x: 1, y: 0 }, fixture.env); + expect(fixture.layer.getBlock(1, 0)).toBe(0); + expect([...fixture.layer.getDynamicTilesAt(1, 0)]).toHaveLength(0); + }); + + // 验证勇士移动序列和向前一步都等待移动结束 + it('awaits hero sequence and forward-step movement', async () => { + const fixture = createFixture(); + await eventMoveHero( + { + steps: [{ type: ObjectMoveType.Dir, move: FaceDirection.Right }] + }, + fixture.env + ); + expect(fixture.state.hero.location.x).toBe(1); + await eventMoveHeroStep({}, fixture.env); + expect(fixture.state.hero.location.x).toBe(2); + }); + + // 验证面前事件按 onTouch 触发且不移动勇士 + it('triggers front onTouch events without moving the hero', 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 eventTouchFront({}, fixture.env); + expect(calls).toHaveLength(1); + expect(calls[0].env.trigger).toBe(EventTrigger.OnTouch); + expect(fixture.state.hero.location.x).toBe(0); + }); + + // 验证临时事件序列和单事件都会按顺序等待执行 + it('awaits temporary event sequences and single event insertion', async () => { + const fixture = createFixture(); + const calls: string[] = []; + fixture.state.eventStore.addEvent( + 'first', + createEvent(fixture.state, EventTrigger.None, async () => { + await Promise.resolve(); + calls.push('first'); + }) + ); + fixture.state.eventStore.addEvent( + 'second', + createEvent(fixture.state, EventTrigger.None, async () => { + calls.push('second'); + }) + ); + await eventInsertEvents( + { ids: ['first', 'second', 'missing'] }, + fixture.env + ); + await eventInsertEvent({ id: 'first' }, fixture.env); + expect(calls).toEqual(['first', 'second', 'first']); + }); + + // 验证默认注册项只包含批准的八个稳定名称 + it('registers exactly the approved built-ins in AnonTokyo', () => { + const fixture = createFixture(); + const names = createEventBuiltinRegistrations().map(item => item.name); + expect(names).toEqual([ + EventBuiltinName.SetBlock, + EventBuiltinName.MoveBlock, + EventBuiltinName.DeleteBlock, + EventBuiltinName.MoveHero, + EventBuiltinName.MoveHeroStep, + EventBuiltinName.TouchFront, + EventBuiltinName.InsertEvents, + EventBuiltinName.InsertEvent + ]); + for (const name of names) { + expect( + fixture.state.eventSystem.executor.interpreter.getBuiltInFunction( + name + ) + ).toMatchObject({ + name, + func: expect.any(Function) + }); + } + }); + + // 验证缺失地图、勇士和事件 id 时所有函数都安全返回 + it('safely skips missing targets and event ids', async () => { + const fixture = createFixture(); + const missingEnv: IBlockEventEnv = { + ...fixture.env, + map: null, + layer: null, + heroFloor: 'missing' + }; + await expect( + eventMoveBlock({ x: 0, y: 0, steps: [] }, missingEnv) + ).resolves.toBeUndefined(); + await expect( + eventDeleteBlock({ x: 0, y: 0 }, missingEnv) + ).resolves.toBeUndefined(); + await expect(eventTouchFront({}, missingEnv)).resolves.toBeUndefined(); + await expect( + eventInsertEvent({ id: 'missing' }, fixture.env) + ).resolves.toBeUndefined(); + }); +}); diff --git a/packages-user/data-state/src/event/event.ts b/packages-user/data-state/src/event/event.ts index e69de29..7bc5f7d 100644 --- a/packages-user/data-state/src/event/event.ts +++ b/packages-user/data-state/src/event/event.ts @@ -0,0 +1,201 @@ +import { + IBlockEventEnv, + IBlockEventParam, + IGameEventExecutor, + IGameEventInvocation, + IGameEventSystem, + BlockEventType +} from '@user/data-system'; +import { + FaceDirection, + EventTrigger, + IGameEventStore +} from '@user/data-common'; +import { + IStateBase, + IReadonlyTileBase as IReadonlyMapTileBase +} from '@user/data-base'; +import { getPossibleLayer } from './map'; +import { + IInsertEventEventParam, + IInsertEventsEventParam, + ITouchFrontEventParam +} from './types'; + +interface IEventSource { + readonly priority: number; + readonly id: string; + readonly type: BlockEventType; + readonly tile: IReadonlyMapTileBase | null; +} + +interface IEventState extends IStateBase { + readonly eventSystem: IGameEventSystem; +} + +const EVENT_INSERT_MAX_DEPTH = 32; +const eventInsertDepth: WeakMap = new WeakMap(); + +/** 判断状态是否包含事件执行器 */ +function hasEventSystem(state: IStateBase): state is IEventState { + return 'eventSystem' in state; +} + +/** 从环境获取事件执行器和事件存储器 */ +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 + }; +} + +/** 按坐标收集事件来源并保留其触发环境 */ +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); + + 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; +} + +/** 触发勇士正面的 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: {} }); +} + +/** 过滤存在的事件 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; + } + 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); + } +} + +/** 临时执行指定事件 */ +export async function eventInsertEvent( + param: IInsertEventEventParam, + env: IBlockEventEnv +): Promise { + if (!param.id) return; + await eventInsertEvents({ ids: [param.id] }, env); +} diff --git a/packages-user/data-state/src/event/hero.ts b/packages-user/data-state/src/event/hero.ts index e69de29..a6a3b5b 100644 --- a/packages-user/data-state/src/event/hero.ts +++ b/packages-user/data-state/src/event/hero.ts @@ -0,0 +1,93 @@ +import { IBlockEventEnv } from '@user/data-system'; +import { IHeroLocation, IHeroMover } from '@user/data-base'; +import { + IMoverController, + IObjectMovable, + IObjectMover, + ObjectMoveStep, + ObjectMoveType, + ObjectSpecialStep +} from '@user/data-common'; +import { IMoveHeroEventParam, IMoveHeroStepEventParam } from './types'; + +/** 启动勇士移动并等待其完整结束 */ +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; +} diff --git a/packages-user/data-state/src/event/index.ts b/packages-user/data-state/src/event/index.ts index e69de29..22f7706 100644 --- a/packages-user/data-state/src/event/index.ts +++ b/packages-user/data-state/src/event/index.ts @@ -0,0 +1,193 @@ +import { BuiltInFunction } from 'anon-tokyo'; +import { IBlockEventEnv } from '@user/data-system'; +import { ObjectMoveStep } from '@user/data-common'; +import { eventDeleteBlock, eventMoveBlock, eventSetBlock } from './map'; +import { eventMoveHero, eventMoveHeroStep } from './hero'; +import { eventInsertEvent, eventInsertEvents, eventTouchFront } from './event'; +import { + EventBuiltinName, + IDeleteBlockEventParam, + IInsertEventEventParam, + IInsertEventsEventParam, + IMoveBlockEventParam, + IMoveHeroEventParam, + ISetBlockEventParam +} from './types'; + +function readProperty(value: object, key: string): unknown { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + return descriptor ? descriptor.value : undefined; +} + +function readNumber(value: object, key: string): number | null { + const result = readProperty(value, key); + return typeof result === 'number' ? result : null; +} + +function readString(value: object, key: string): string | null { + const result = readProperty(value, key); + return typeof result === 'string' ? result : null; +} + +function readBoolean(value: object, key: string): boolean | undefined { + const result = readProperty(value, key); + return typeof result === 'boolean' ? result : undefined; +} + +function isObjectMoveStep(value: unknown): value is ObjectMoveStep { + if (!value || typeof value !== 'object') return false; + return typeof readProperty(value, 'type') === 'number'; +} + +function readMoveSteps( + value: object, + key: string +): readonly ObjectMoveStep[] | null { + const result = readProperty(value, key); + if (!Array.isArray(result)) return null; + if (!result.every(isObjectMoveStep)) return null; + return result; +} + +function readStringArray(value: object, key: string): readonly string[] | null { + const result = readProperty(value, key); + if (!Array.isArray(result)) return null; + if (!result.every(item => typeof item === 'string')) return null; + return result; +} + +function parseSetBlock(param: object): ISetBlockEventParam | null { + const x = readNumber(param, 'x'); + const y = readNumber(param, 'y'); + const tile = readProperty(param, 'tile'); + if (x === null || y === null) return null; + if (typeof tile !== 'number' && typeof tile !== 'string') return null; + return { x, y, tile }; +} + +function parseMoveBlock(param: object): IMoveBlockEventParam | null { + const x = readNumber(param, 'x'); + const y = readNumber(param, 'y'); + const steps = readMoveSteps(param, 'steps'); + if (x === null || y === null || !steps) return null; + return { x, y, steps, safe: readBoolean(param, 'safe') }; +} + +function parseDeleteBlock(param: object): IDeleteBlockEventParam | null { + const x = readNumber(param, 'x'); + const y = readNumber(param, 'y'); + if (x === null || y === null) return null; + return { x, y }; +} + +function parseMoveHero(param: object): IMoveHeroEventParam | null { + const steps = readMoveSteps(param, 'steps'); + return steps ? { steps } : null; +} + +function parseInsertEvents(param: object): IInsertEventsEventParam | null { + const ids = readStringArray(param, 'ids'); + return ids ? { ids } : null; +} + +function parseInsertEvent(param: object): IInsertEventEventParam | null { + const id = readString(param, 'id'); + return id === null ? null : { id }; +} + +type BuiltinParameter = Parameters[0]; +type BuiltinEnvironment = Parameters[1]; + +function isBlockEventEnv(value: BuiltinEnvironment): value is IBlockEventEnv { + return ( + 'state' in value && + 'type' in value && + 'trigger' in value && + 'heroLocator' in value && + 'heroFloor' in value && + 'triggerLocator' in value && + 'tile' in value && + 'layer' in value && + 'map' in value + ); +} + +type EventBuiltinHandler = ( + param: BuiltinParameter, + env: IBlockEventEnv +) => void | Promise; + +function createBuiltin(handler: EventBuiltinHandler): BuiltInFunction['func'] { + return (param: BuiltinParameter, env: BuiltinEnvironment) => { + if (!isBlockEventEnv(env)) return; + return handler(param, env); + }; +} + +/** 创建八个批准事件 built-in 的稳定注册项 */ +export function createEventBuiltinRegistrations(): ReadonlyArray { + const registrations: BuiltInFunction[] = [ + { + name: EventBuiltinName.SetBlock, + func: createBuiltin((param, env) => { + const parsed = parseSetBlock(param); + if (!parsed) return; + return eventSetBlock(parsed, env); + }) + }, + { + name: EventBuiltinName.MoveBlock, + func: createBuiltin((param, env) => { + const parsed = parseMoveBlock(param); + if (!parsed) return; + return eventMoveBlock(parsed, env); + }) + }, + { + name: EventBuiltinName.DeleteBlock, + func: createBuiltin((param, env) => { + const parsed = parseDeleteBlock(param); + if (!parsed) return; + return eventDeleteBlock(parsed, env); + }) + }, + { + name: EventBuiltinName.MoveHero, + func: createBuiltin((param, env) => { + const parsed = parseMoveHero(param); + if (!parsed) return; + return eventMoveHero(parsed, env); + }) + }, + { + name: EventBuiltinName.MoveHeroStep, + func: createBuiltin((_param, env) => eventMoveHeroStep({}, env)) + }, + { + name: EventBuiltinName.TouchFront, + func: createBuiltin((_param, env) => eventTouchFront({}, env)) + }, + { + name: EventBuiltinName.InsertEvents, + func: createBuiltin((param, env) => { + const parsed = parseInsertEvents(param); + if (!parsed) return; + return eventInsertEvents(parsed, env); + }) + }, + { + name: EventBuiltinName.InsertEvent, + func: createBuiltin((param, env) => { + const parsed = parseInsertEvent(param); + if (!parsed) return; + return eventInsertEvent(parsed, env); + }) + } + ]; + return registrations; +} + +export * from './event'; +export * from './hero'; +export * from './map'; +export * from './types'; diff --git a/packages-user/data-state/src/event/map.ts b/packages-user/data-state/src/event/map.ts index 137d176..59ee851 100644 --- a/packages-user/data-state/src/event/map.ts +++ b/packages-user/data-state/src/event/map.ts @@ -1,14 +1,19 @@ import { IBlockEventEnv } from '@user/data-system'; -import { ISetBlockEventParam } from './types'; +import { + IDeleteBlockEventParam, + IMoveBlockEventParam, + ISetBlockEventParam +} from './types'; import { isNil } from 'lodash-es'; import { logger } from '@motajs/common'; -import { IGameMap } from '@user/data-base'; +import { IGameMap, IMapLayer } from '@user/data-base'; +import { appendMoveSteps } from './hero'; /** * 通过环境参量获取可能的地图对象 * @param env 事件环境变量 */ -function getPossibleMap(env: IBlockEventEnv): IGameMap | null { +export function getPossibleMap(env: IBlockEventEnv): IGameMap | null { if (env.map) return env.map; if (env.layer) return env.layer.map; @@ -18,21 +23,70 @@ function getPossibleMap(env: IBlockEventEnv): IGameMap | null { 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 eventSetBlock( param: ISetBlockEventParam, env: IBlockEventEnv ): void { - const map = getPossibleMap(env); - if (!map) return; - - const layer = map.eventLayer; + const layer = getPossibleLayer(env); if (!layer) return; + if (!layer.inMap(param.x, param.y)) return; + const num = env.state.tileStore.num(param.tile); if (isNil(num)) { logger.warn(1001); return; } - return layer.setBlock(num, param.x, param.y); + layer.setBlock(num, param.x, param.y); +} + +/** 将动态图块移动完成后还原为静态图块 */ +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); + } +} + +/** 删除目标坐标的静态图块和动态图块 */ +export async function eventDeleteBlock( + param: IDeleteBlockEventParam, + env: IBlockEventEnv +): Promise { + 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); + } } diff --git a/packages-user/data-state/src/event/types.ts b/packages-user/data-state/src/event/types.ts index f513fe9..b777ea9 100644 --- a/packages-user/data-state/src/event/types.ts +++ b/packages-user/data-state/src/event/types.ts @@ -1,3 +1,5 @@ +import { ObjectMoveStep } from '@user/data-common'; + //#region 地图控制 /** 事件:设置图块 */ @@ -10,12 +12,68 @@ export interface ISetBlockEventParam { readonly tile: number | string; } +/** 事件:移动图块 */ +export interface IMoveBlockEventParam { + /** 起始横坐标 */ + readonly x: number; + /** 起始纵坐标 */ + readonly y: number; + /** 移动步骤 */ + readonly steps: readonly ObjectMoveStep[]; + /** 是否仅在目标位置安全时转回静态图块 */ + readonly safe?: boolean; +} + +/** 事件:删除图块 */ +export interface IDeleteBlockEventParam { + /** 横坐标 */ + readonly x: number; + /** 纵坐标 */ + readonly y: number; +} + //#endregion //#region 玩家控制 +/** 事件:按步骤移动勇士 */ +export interface IMoveHeroEventParam { + /** 移动步骤 */ + readonly steps: readonly ObjectMoveStep[]; +} + +/** 事件:向前移动一步 */ +export interface IMoveHeroStepEventParam {} + +/** 事件:触发勇士面前的 onTouch */ +export interface ITouchFrontEventParam {} + //#endregion //#region 事件控制 +/** 事件:临时插入多个事件 */ +export interface IInsertEventsEventParam { + /** 事件 id 顺序 */ + readonly ids: readonly string[]; +} + +/** 事件:临时插入一个事件 */ +export interface IInsertEventEventParam { + /** 事件 id */ + readonly id: string; +} + +/** 内建函数的稳定注册名称 */ +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/index.ts b/packages-user/data-state/src/index.ts index 89c9dbb..54b0d76 100644 --- a/packages-user/data-state/src/index.ts +++ b/packages-user/data-state/src/index.ts @@ -1,4 +1,5 @@ export * from './enemy'; +export * from './event'; export * from './hero'; export * from './core'; diff --git a/packages-user/data-system/src/event/system.ts b/packages-user/data-system/src/event/system.ts index b939bf7..126e71f 100644 --- a/packages-user/data-system/src/event/system.ts +++ b/packages-user/data-system/src/event/system.ts @@ -1,6 +1,6 @@ import { IStateBase } from '@user/data-base'; import { IGameEventStore } from '@user/data-common'; -import { AnonTokyoInterpreter } from 'anon-tokyo'; +import { AnonTokyoInterpreter, BuiltInFunction } from 'anon-tokyo'; import { EventExecutor } from './executor'; import { IGameEventExecutor, IGameEventSystem } from './types'; @@ -8,10 +8,13 @@ export class GameEventSystem implements IGameEventSystem { readonly executor: IGameEventExecutor; store: IGameEventStore | null; - constructor(readonly state: IStateBase) { + constructor( + readonly state: IStateBase, + builtins: ReadonlyArray = [] + ) { this.store = state.eventStore; const interpreter = new AnonTokyoInterpreter({ - builtInFunctions: [], + builtInFunctions: [...builtins], globalFunctions: [] }); this.executor = new EventExecutor(interpreter, () => this.store);