waitable blockly scripts. new waitForSeconds block
This commit is contained in:
parent
830c2bdde6
commit
3bdde094f8
|
|
@ -18,5 +18,14 @@ export default defineConfig([
|
|||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules: {
|
||||
"padding-line-between-statements": [
|
||||
"warn",
|
||||
{ blankLine: 'always', prev: '*', next: 'block' },
|
||||
{ blankLine: 'always', prev: 'block', next: '*' },
|
||||
{ blankLine: 'always', prev: '*', next: 'block-like' },
|
||||
{ blankLine: 'always', prev: 'block-like', next: '*' },
|
||||
]
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Blockly.Blocks['console_log_action'] = {
|
|||
init(this: Blockly.Block) {
|
||||
this.appendValueInput('VALUE')
|
||||
.setAlign(Blockly.inputs.Align.RIGHT)
|
||||
.setCheck('ObjectType')
|
||||
// .setCheck('String')
|
||||
.appendField('print');
|
||||
this.setInputsInline(false)
|
||||
this.setPreviousStatement(true, null);
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from './consoleLog';
|
||||
export * from './physics';
|
||||
export * from './waitSeconds';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import * as Blockly from "blockly";
|
||||
import { javascriptGenerator } from "blockly/javascript";
|
||||
|
||||
Blockly.Blocks['wait_seconds_action'] = {
|
||||
init(this: Blockly.Block) {
|
||||
this.appendDummyInput()
|
||||
.appendField('Wait')
|
||||
.appendField(new Blockly.FieldNumber(1, 0, 3600, 0.1), 'SECONDS')
|
||||
.appendField('seconds');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setTooltip('Pause execution for N seconds');
|
||||
this.setColour(120);
|
||||
}
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock['wait_seconds_action'] = function (block) {
|
||||
const seconds = block.getFieldValue('SECONDS');
|
||||
return `yield wait(${seconds});\n`;
|
||||
};
|
||||
|
||||
export { };
|
||||
|
|
@ -12,7 +12,7 @@ export function generateCode(workspace: Blockly.WorkspaceSvg): string {
|
|||
body += javascriptGenerator.blockToCode(next, true);
|
||||
next = next.getNextBlock();
|
||||
}
|
||||
body = javascriptGenerator.prefixLines(body, javascriptGenerator.INDENT);
|
||||
body = javascriptGenerator.prefixLines(body, javascriptGenerator.INDENT.repeat(2));
|
||||
|
||||
// const args = Array.from(block.getFields())
|
||||
// .filter(field => field.name !== undefined)
|
||||
|
|
@ -23,12 +23,10 @@ export function generateCode(workspace: Blockly.WorkspaceSvg): string {
|
|||
|
||||
result += `onGameEvent(
|
||||
'${block.type.replace(/_event$/, '')}',
|
||||
function(context) {
|
||||
// console.dir({context, api, object, objectType});
|
||||
function*(context) {
|
||||
if (context.object && (context.object.id !== object.id))
|
||||
return;
|
||||
${body}
|
||||
}
|
||||
${body} },
|
||||
);
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,8 +45,8 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
|
|||
colour: '20',
|
||||
contents: [
|
||||
{ kind: 'block', type: 'console_log_action' },
|
||||
{ kind: 'block', type: 'text_print' },
|
||||
{ kind: 'block', type: 'physics_apply_impulse_action' },
|
||||
{ kind: 'block', type: 'wait_seconds_action' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
export type YieldCommand =
|
||||
| { type: 'waitSeconds'; seconds: number }
|
||||
| { type: 'waitFrames'; frames: number }
|
||||
| { type: 'waitUntil'; condition: () => boolean };
|
||||
|
||||
type Coroutine =
|
||||
& { gen: Generator<YieldCommand, void, void> }
|
||||
& (| { wakeType: 'time'; remaining: number }
|
||||
| { wakeType: 'frames'; remaining: number }
|
||||
| { wakeType: 'condition'; test: () => boolean });
|
||||
|
||||
export class GameScheduler {
|
||||
private coroutines: Coroutine[] = [];
|
||||
|
||||
tick(deltaTime: number): void {
|
||||
const next: Coroutine[] = [];
|
||||
for (const co of this.coroutines) {
|
||||
if (this.isReady(co)) this.step(co, next);
|
||||
else next.push(this.countdown(co, deltaTime));
|
||||
}
|
||||
this.coroutines = next;
|
||||
}
|
||||
|
||||
startCoroutine(gen: Generator<YieldCommand, void, void>): void {
|
||||
const result = gen.next();
|
||||
if (!result.done)
|
||||
this.coroutines.push(this.make(gen, result.value));
|
||||
}
|
||||
|
||||
reset(): void { this.coroutines = []; }
|
||||
|
||||
private isReady(co: Coroutine): boolean {
|
||||
switch (co.wakeType) {
|
||||
case 'time': return co.remaining <= 0;
|
||||
case 'frames': return co.remaining <= 0;
|
||||
case 'condition': return co.test();
|
||||
}
|
||||
}
|
||||
|
||||
private countdown(co: Coroutine, deltaTime: number): Coroutine {
|
||||
switch (co.wakeType) {
|
||||
case 'time': return { ...co, remaining: co.remaining - deltaTime };
|
||||
case 'frames': return { ...co, remaining: co.remaining - 1 };
|
||||
default: return co;
|
||||
}
|
||||
}
|
||||
|
||||
private step(co: Coroutine, list: Coroutine[]): void {
|
||||
const result = co.gen.next();
|
||||
if (!result.done) list.push(this.make(co.gen, result.value));
|
||||
}
|
||||
|
||||
private make(gen: Generator<YieldCommand, void, void>, cmd: YieldCommand): Coroutine {
|
||||
switch (cmd.type) {
|
||||
case 'waitSeconds':
|
||||
return { gen, wakeType: 'time', remaining: cmd.seconds };
|
||||
case 'waitFrames':
|
||||
return { gen, wakeType: 'frames', remaining: cmd.frames };
|
||||
case 'waitUntil':
|
||||
return { gen, wakeType: 'condition', test: cmd.condition };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import type { Game, RuntimeGameScene, RuntimeScene, World } from "../types";
|
||||
import type { GameEventHandler, GameEventContext, InternalGameEventBus } from "../types/runtime/gameBus";
|
||||
import type { GameEventContext, GameEventHandler, InternalGameEventBus } from "../types/runtime/gameBus";
|
||||
import { clone } from "../utils";
|
||||
import { populateRuntimeScene } from "../utils/runtime";
|
||||
import { ObjectApi } from "../utils/runtime/objectApi";
|
||||
import type { GameScheduler, YieldCommand } from "./GameScheduler";
|
||||
|
||||
export class GameEventBus {
|
||||
private handlers = new Map<string, GameEventHandler[]>();
|
||||
|
|
@ -66,25 +67,41 @@ export class GameFactory {
|
|||
world: World,
|
||||
scene: RuntimeGameScene,
|
||||
eventBus: GameEventBus,
|
||||
|
||||
scheduler: GameScheduler,
|
||||
): void {
|
||||
|
||||
const handlerWrappers = new WeakMap<Function, (ctx: GameEventContext) => void>();
|
||||
|
||||
const wait = (seconds: number): YieldCommand => ({ type: 'waitSeconds', seconds });
|
||||
const waitFrames = (frames: number): YieldCommand => ({ type: 'waitFrames', frames });
|
||||
const waitUntil = (condition: () => boolean): YieldCommand => ({ type: 'waitUntil', condition });
|
||||
|
||||
const internalBus: InternalGameEventBus = {
|
||||
onGameEvent: eventBus.on.bind(eventBus),
|
||||
offGameEvent: eventBus.off.bind(eventBus),
|
||||
onGameEvent(event, genFn) {
|
||||
const wrapper: GameEventHandler = (ctx) => scheduler.startCoroutine(genFn(ctx) as Generator<YieldCommand, void, void>);
|
||||
handlerWrappers.set(genFn, wrapper);
|
||||
eventBus.on(event, wrapper);
|
||||
},
|
||||
offGameEvent(event, genFn) {
|
||||
const wrapper = handlerWrappers.get(genFn);
|
||||
if (wrapper) {
|
||||
eventBus.off(event, wrapper);
|
||||
handlerWrappers.delete(genFn);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
Object.values(world.objectTypes)
|
||||
.filter((ot) => ot.javascript)
|
||||
.forEach((ot) => {
|
||||
const gameScript = eval(`(function gameScript({onGameEvent, offGameEvent }, {api, object, objectType}) {${ot.javascript}})`);
|
||||
const gameScript = eval(`(function gameScript({onGameEvent, offGameEvent, wait, waitFrames, waitUntil}, {api, object, objectType}) {${ot.javascript}})`);
|
||||
|
||||
Object.values(scene.objects)
|
||||
.filter((o) => o.typeId == ot.id)
|
||||
.forEach((o) => {
|
||||
const api = new ObjectApi(o, ot, world, scene);
|
||||
gameScript(internalBus, { api, object: o, objectType: ot });
|
||||
gameScript({ ...internalBus, wait, waitFrames, waitUntil }, { api, object: o, objectType: ot });
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { WorldState } from "./worldState";
|
|||
import type { Game, Pos3, RuntimeGameScene } from "../types";
|
||||
import type { CameraProps } from "@react-three/fiber";
|
||||
import { GameEventBus, GameFactory } from "../model/gameFactory";
|
||||
import { GameScheduler } from "../model/GameScheduler";
|
||||
import type { GameEventContext } from "../types/runtime/gameBus";
|
||||
|
||||
export class GameState {
|
||||
|
|
@ -14,6 +15,7 @@ export class GameState {
|
|||
public scene: RuntimeGameScene;
|
||||
|
||||
private eventBus: GameEventBus;
|
||||
private scheduler: GameScheduler;
|
||||
|
||||
private startAutoSave() {
|
||||
return reaction(
|
||||
|
|
@ -46,11 +48,12 @@ export class GameState {
|
|||
|
||||
const eventBus = new GameEventBus();
|
||||
this.eventBus = eventBus;
|
||||
this.scheduler = new GameScheduler();
|
||||
|
||||
this._stopAutoSave = this.startAutoSave();
|
||||
makeAutoObservable(this);
|
||||
|
||||
GameFactory.evalGameScripts(rawWorld, this.scene, eventBus);
|
||||
GameFactory.evalGameScripts(rawWorld, this.scene, eventBus, this.scheduler);
|
||||
}
|
||||
|
||||
public emitEvent(event: string, context: Omit<GameEventContext, 'world' | 'scene'> = {}) {
|
||||
|
|
@ -126,6 +129,7 @@ export class GameState {
|
|||
if (this.isPaused)
|
||||
return;
|
||||
|
||||
this.scheduler.tick(deltaTime);
|
||||
this.time += deltaTime;
|
||||
|
||||
if (this.isFirstTick) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ export type GameEventContext = {
|
|||
|
||||
export type GameEventHandler = (context: GameEventContext) => void;
|
||||
|
||||
export type GeneratorEventHandler = (context: GameEventContext) => Generator<unknown, void, void>;
|
||||
|
||||
export type InternalGameEventBus = {
|
||||
onGameEvent: (event: string, handler: GameEventHandler) => void;
|
||||
offGameEvent: (event: string, handler: GameEventHandler) => void;
|
||||
onGameEvent: (event: string, handler: GeneratorEventHandler) => void;
|
||||
offGameEvent: (event: string, handler: GeneratorEventHandler) => void;
|
||||
}
|
||||
Loading…
Reference in New Issue