mirror of
https://github.com/motajs/template.git
synced 2026-09-12 09:30:19 +08:00
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
This commit is contained in:
parent
e9e89fdfdb
commit
3134537d30
@ -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
|
||||
|
||||
230
packages-user/data-state/src/event/event.test.ts
Normal file
230
packages-user/data-state/src/event/event.test.ts
Normal file
@ -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<CoreState['maps']['createMap']>;
|
||||
readonly layer: ReturnType<
|
||||
ReturnType<CoreState['maps']['createMap']>['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<void>
|
||||
): IGameEvent<Record<string, never>, 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();
|
||||
});
|
||||
});
|
||||
@ -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<IBlockEventEnv, number> = 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<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;
|
||||
}
|
||||
|
||||
/** 触发勇士正面的 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: {} });
|
||||
}
|
||||
|
||||
/** 过滤存在的事件 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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/** 临时执行指定事件 */
|
||||
export async function eventInsertEvent(
|
||||
param: IInsertEventEventParam,
|
||||
env: IBlockEventEnv
|
||||
): Promise<void> {
|
||||
if (!param.id) return;
|
||||
await eventInsertEvents({ ids: [param.id] }, env);
|
||||
}
|
||||
@ -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<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;
|
||||
}
|
||||
@ -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<BuiltInFunction['func']>[0];
|
||||
type BuiltinEnvironment = Parameters<BuiltInFunction['func']>[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<void>;
|
||||
|
||||
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<BuiltInFunction> {
|
||||
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';
|
||||
@ -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<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);
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除目标坐标的静态图块和动态图块 */
|
||||
export async function eventDeleteBlock(
|
||||
param: IDeleteBlockEventParam,
|
||||
env: IBlockEventEnv
|
||||
): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export * from './enemy';
|
||||
export * from './event';
|
||||
export * from './hero';
|
||||
|
||||
export * from './core';
|
||||
|
||||
@ -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<BuiltInFunction> = []
|
||||
) {
|
||||
this.store = state.eventStore;
|
||||
const interpreter = new AnonTokyoInterpreter({
|
||||
builtInFunctions: [],
|
||||
builtInFunctions: [...builtins],
|
||||
globalFunctions: []
|
||||
});
|
||||
this.executor = new EventExecutor(interpreter, () => this.store);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user