initial game scripting
This commit is contained in:
parent
53e68c2279
commit
4b00dbfc00
|
|
@ -0,0 +1,17 @@
|
|||
import * as Blockly from "blockly";
|
||||
import { javascriptGenerator } from "blockly/javascript";
|
||||
|
||||
Blockly.common.defineBlocksWithJsonArray([{
|
||||
"type": "game_start_event",
|
||||
"message0": "On game start",
|
||||
"nextStatement": null,
|
||||
"style": "event_hat",
|
||||
"tooltip": "Runs when the game starts.",
|
||||
"helpUrl": ""
|
||||
}]);
|
||||
|
||||
javascriptGenerator.forBlock['game_start_event'] = function (_block, _generator) {
|
||||
return '';
|
||||
};
|
||||
|
||||
export { };
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './gameStart';
|
||||
export * from './objectTouchedMe';
|
||||
export * from './playerTouchedMe';
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import * as Blockly from "blockly";
|
||||
import { javascriptGenerator } from "blockly/javascript";
|
||||
import { getObjectTypeDropDownOptions } from "../fieldTypes/objectType";
|
||||
|
||||
Blockly.Blocks['object_touch_start_event'] = {
|
||||
init(this: Blockly.Block) {
|
||||
this.appendDummyInput()
|
||||
.appendField('When')
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown(getObjectTypeDropDownOptions),
|
||||
'OBJECT_TYPE',
|
||||
)
|
||||
.appendField('touches me');
|
||||
this.setStyle('event_hat');
|
||||
this.setNextStatement(true);
|
||||
this.setTooltip('Fires when an object touches me');
|
||||
}
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock['object_touch_start_event'] = function (_block, _generator) {
|
||||
return '';
|
||||
};
|
||||
|
||||
export { };
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import * as Blockly from "blockly";
|
||||
import { javascriptGenerator } from "blockly/javascript";
|
||||
|
||||
Blockly.Blocks['player_touch_start_event'] = {
|
||||
init(this: Blockly.Block) {
|
||||
this.appendDummyInput()
|
||||
.appendField('When player touches me');
|
||||
this.setStyle('event_hat');
|
||||
this.setNextStatement(true);
|
||||
this.setTooltip('Fires when player touches me');
|
||||
}
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock['player_touch_start_event'] = function (_block, _generator) {
|
||||
return '';
|
||||
};
|
||||
|
||||
export { };
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { state } from "../../state/rootState";
|
||||
import { toJS } from "mobx";
|
||||
|
||||
export function getObjectTypeDropDownOptions(): [string, string][] {
|
||||
const types = Object.values(toJS(state.world.data.objectTypes));
|
||||
const values: [string, string][] = [
|
||||
['Any object', '*'],
|
||||
...types.map(t => [t.name, t.id] as [string, string]),
|
||||
];
|
||||
return values;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export function startGame() {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import * as Blockly from "blockly";
|
||||
import { javascriptGenerator } from "blockly/javascript";
|
||||
|
||||
export function generateCode(workspace: Blockly.WorkspaceSvg): string {
|
||||
let result = '';
|
||||
for (const block of workspace.getTopBlocks(true)) {
|
||||
if (block.type.endsWith('_event')) {
|
||||
let body = '';
|
||||
let next = block.getNextBlock();
|
||||
while (next) {
|
||||
// opt_thisOnly=true prevents scrub_ from also chaining — we do it manually
|
||||
body += javascriptGenerator.blockToCode(next, true);
|
||||
next = next.getNextBlock();
|
||||
}
|
||||
body = javascriptGenerator.prefixLines(body, javascriptGenerator.INDENT);
|
||||
|
||||
const args = Array.from(block.getFields())
|
||||
.filter(field => field.name !== undefined)
|
||||
.map(field => `${field.name}: ${JSON.stringify(field.getValue())}`)
|
||||
.join(', ');
|
||||
|
||||
//TODO use args
|
||||
|
||||
result += `onGameEvent('${block.type.replace(/_event$/, '')}', function(context) {\n${body}});\n`;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from './events';
|
||||
export * from './values';
|
||||
export * from './theme';
|
||||
export * from './generateCode';
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import * as Blockly from "blockly";
|
||||
|
||||
export const gameTheme = Blockly.Theme.defineTheme(
|
||||
'gameTheme',
|
||||
{
|
||||
name: 'GameTheme',
|
||||
base: Blockly.Themes.Classic,
|
||||
blockStyles: {
|
||||
'event_hat': {
|
||||
colourPrimary: '#FFAB19',
|
||||
colourSecondary: '#CF8B17',
|
||||
colourTertiary: '#CF8B17',
|
||||
hat: 'cap',
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import * as Blockly from "blockly";
|
||||
import { javascriptGenerator, Order } from "blockly/javascript";
|
||||
|
||||
Blockly.Blocks['current_object_value'] = {
|
||||
init(this: Blockly.Block) {
|
||||
this.appendEndRowInput('NAME')
|
||||
.appendField('current object');
|
||||
this.setInputsInline(false)
|
||||
this.setOutput(true, 'String');
|
||||
this.setTooltip('asd');
|
||||
this.setHelpUrl('asd');
|
||||
this.setColour(315);
|
||||
}
|
||||
};
|
||||
|
||||
javascriptGenerator.forBlock['current_object_value'] = function (_block, _generator) {
|
||||
return ['object.id', Order.NONE];
|
||||
};
|
||||
|
||||
export { };
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './currentObject';
|
||||
|
|
@ -2,8 +2,8 @@ import { useEffect, useRef } from "react";
|
|||
import { create } from "nipplejs";
|
||||
import { joystickValues } from "../joystickInput";
|
||||
|
||||
// const isTouch = navigator.maxTouchPoints > 0;
|
||||
const isTouch = true; // debug
|
||||
const isTouch = navigator.maxTouchPoints > 0;
|
||||
// const isTouch = true; // debug
|
||||
|
||||
export function JoystickView() {
|
||||
const moveZoneRef = useRef<HTMLDivElement>(null);
|
||||
|
|
|
|||
|
|
@ -5,16 +5,19 @@ import * as Blockly from "blockly";
|
|||
import type { ObjectType } from "../../types";
|
||||
import './ScriptEditorView.scss';
|
||||
|
||||
import '../../blockly';
|
||||
import { gameTheme, generateCode } from '../../blockly';
|
||||
|
||||
const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
|
||||
kind: 'flyoutToolbox',
|
||||
contents: [
|
||||
{ kind: 'block', type: 'controls_if' },
|
||||
{ kind: 'block', type: 'controls_repeat_ext' },
|
||||
{ kind: 'block', type: 'logic_compare' },
|
||||
{ kind: 'block', type: 'math_number' },
|
||||
{ kind: 'block', type: 'math_arithmetic' },
|
||||
{ kind: 'block', type: 'text' },
|
||||
{ kind: 'block', type: 'text_print' },
|
||||
{ kind: 'block', type: 'game_start_event' },
|
||||
{ kind: 'block', type: 'object_touch_start_event' },
|
||||
{ kind: 'block', type: 'player_touch_start_event' },
|
||||
{ kind: 'block', type: 'current_object_value' },
|
||||
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -25,32 +28,46 @@ type ScriptEditorViewProps = {
|
|||
export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorViewProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
|
||||
const isLoadingRef = useRef(false);
|
||||
const objectTypeRef = useRef(objectType);
|
||||
objectTypeRef.current = objectType;
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const workspace = Blockly.inject(containerRef.current, { toolbox: TOOLBOX });
|
||||
const workspace = Blockly.inject(containerRef.current, { toolbox: TOOLBOX, theme: gameTheme });
|
||||
workspaceRef.current = workspace;
|
||||
|
||||
const listener = (e: Blockly.Events.Abstract) => {
|
||||
if (e.isUiEvent)
|
||||
return;
|
||||
|
||||
runInAction(() => {
|
||||
const current = objectTypeRef.current;
|
||||
const serialized = JSON.stringify(Blockly.serialization.workspaces.save(workspace));
|
||||
current.program = serialized;
|
||||
try {
|
||||
current.javascript = generateCode(workspace);
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
current.javascript = undefined;
|
||||
}
|
||||
console.log(current.javascript);
|
||||
});
|
||||
};
|
||||
workspace.addChangeListener(listener);
|
||||
|
||||
if (objectType.program) {
|
||||
try {
|
||||
isLoadingRef.current = true;
|
||||
Blockly.Events.disable();
|
||||
Blockly.serialization.workspaces.load(JSON.parse(objectType.program), workspace);
|
||||
} catch {
|
||||
// corrupt program, start fresh
|
||||
} finally {
|
||||
isLoadingRef.current = false;
|
||||
Blockly.Events.enable();
|
||||
}
|
||||
}
|
||||
|
||||
const listener = (e: Blockly.Events.Abstract) => {
|
||||
if (isLoadingRef.current || e.isUiEvent) return;
|
||||
const serialized = JSON.stringify(Blockly.serialization.workspaces.save(workspace));
|
||||
runInAction(() => { objectType.program = serialized; });
|
||||
};
|
||||
workspace.addChangeListener(listener);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => Blockly.svgResize(workspace));
|
||||
resizeObserver.observe(containerRef.current);
|
||||
|
||||
|
|
@ -66,16 +83,16 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
|
|||
const workspace = workspaceRef.current;
|
||||
if (!workspace) return;
|
||||
|
||||
isLoadingRef.current = true;
|
||||
workspace.clear();
|
||||
if (objectType.program) {
|
||||
try {
|
||||
try {
|
||||
Blockly.Events.disable();
|
||||
workspace.clear();
|
||||
if (objectType.program)
|
||||
Blockly.serialization.workspaces.load(JSON.parse(objectType.program), workspace);
|
||||
} catch {
|
||||
// corrupt program, start fresh
|
||||
}
|
||||
} catch {
|
||||
// corrupt program, start fresh
|
||||
} finally {
|
||||
Blockly.Events.enable();
|
||||
}
|
||||
isLoadingRef.current = false;
|
||||
}, [objectType.id]);
|
||||
|
||||
return <div className="script-editor">
|
||||
|
|
|
|||
|
|
@ -81,4 +81,11 @@ textarea {
|
|||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// Blockly appends its dropdown/widget overlays to document.body.
|
||||
// They need a z-index above .overlay-panels (15000) to be visible.
|
||||
.blocklyDropDownDiv,
|
||||
.blocklyWidgetDiv {
|
||||
z-index: 16000 !important;
|
||||
}
|
||||
|
|
@ -1,17 +1,54 @@
|
|||
import type { Game, RuntimeGameScene, RuntimeScene, World } from "../types";
|
||||
import type { Game, ObjectType, RuntimeGameScene, RuntimeObjectInstance, RuntimeScene, World } from "../types";
|
||||
import type { GameEventHandler, GameEventContext } from "../types/runtime/gameBus";
|
||||
import { clone } from "../utils";
|
||||
import { populateRuntimeScene } from "../utils/runtime";
|
||||
|
||||
type OwnedGameEventHandler = {
|
||||
handler: GameEventHandler;
|
||||
}
|
||||
|
||||
export class GameEventBus {
|
||||
private handlers = new Map<string, OwnedGameEventHandler[]>();
|
||||
|
||||
on(event: string, handler: GameEventHandler): void {
|
||||
console.log('subscribing to ' + event, handler);
|
||||
let set = this.handlers.get(event);
|
||||
if (!set) {
|
||||
set = [];
|
||||
this.handlers.set(event, set);
|
||||
}
|
||||
if (!set.some((h) => h.handler === handler))
|
||||
set.push({ handler });
|
||||
}
|
||||
|
||||
off(event: string, handler: GameEventHandler): void {
|
||||
console.log('unubscribing from ' + event, handler);
|
||||
const handlers = this.handlers.get(event);
|
||||
if (!handlers)
|
||||
return;
|
||||
|
||||
this.handlers.set(event, handlers.filter((h) => h.handler !== handler));
|
||||
}
|
||||
|
||||
emit(event: string, context: GameEventContext): void {
|
||||
console.log('emitting ' + event);
|
||||
console.log(Array.from(this.handlers.entries()));
|
||||
this.handlers.get(event)
|
||||
?.forEach((h) => h.handler(context));
|
||||
}
|
||||
}
|
||||
|
||||
export class GameFactory {
|
||||
|
||||
public static create(world: World): Game {
|
||||
const scene = populateRuntimeScene(clone(world.initialScene), world);
|
||||
|
||||
return {
|
||||
const game = {
|
||||
paused: false,
|
||||
time: 0,
|
||||
scene: GameFactory.initGameScene(scene),
|
||||
}
|
||||
};
|
||||
return game;
|
||||
}
|
||||
|
||||
public static load(): Game | undefined {
|
||||
|
|
@ -28,4 +65,33 @@ export class GameFactory {
|
|||
private static initGameScene(scene: RuntimeScene): RuntimeGameScene {
|
||||
return scene as RuntimeGameScene;
|
||||
}
|
||||
|
||||
public static evalGameScripts(
|
||||
world: World,
|
||||
scene: RuntimeGameScene,
|
||||
eventBus: GameEventBus,
|
||||
): void {
|
||||
|
||||
const internalBus = {
|
||||
onGameEvent: eventBus.on.bind(eventBus),
|
||||
offGameEvent: eventBus.off.bind(eventBus),
|
||||
};
|
||||
|
||||
try {
|
||||
Object.values(world.objectTypes)
|
||||
.filter((ot) => ot.javascript)
|
||||
.forEach((ot) => {
|
||||
const otCode = eval(`(function gameScript({onGameEvent, offGameEvent}, object, objectType) {${ot.javascript}})`);
|
||||
|
||||
Object.values(scene.objects)
|
||||
.filter((o) => o.typeId == ot.id)
|
||||
.forEach((o) => {
|
||||
otCode(internalBus, o, ot);
|
||||
})
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
console.log('Error running game script:\n' + err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { makeAutoObservable, reaction, toJS } from "mobx";
|
|||
import type { WorldState } from "./worldState";
|
||||
import type { Game, Pos3, V3, R3, RuntimeGameScene } from "../types";
|
||||
import type { CameraProps } from "@react-three/fiber";
|
||||
import { GameFactory } from "../model/gameFactory";
|
||||
import { GameEventBus, GameFactory } from "../model/gameFactory";
|
||||
import type { GameEventContext } from "../types/runtime/gameBus";
|
||||
|
||||
export class GameState {
|
||||
private readonly world: WorldState;
|
||||
|
|
@ -11,6 +12,8 @@ export class GameState {
|
|||
public time: number = 0;
|
||||
public scene: RuntimeGameScene;
|
||||
|
||||
private eventBus: GameEventBus;
|
||||
|
||||
private startAutoSave() {
|
||||
return reaction(
|
||||
() => (this.asGame),
|
||||
|
|
@ -31,13 +34,34 @@ export class GameState {
|
|||
|
||||
constructor(world: WorldState) {
|
||||
this.world = world;
|
||||
const game = GameFactory.create(toJS(this.world.data));
|
||||
|
||||
const rawWorld = toJS(this.world.data);
|
||||
|
||||
const game = GameFactory.create(rawWorld);
|
||||
this.isPaused = game.paused;
|
||||
this.time = game.time;
|
||||
this.scene = game.scene;
|
||||
|
||||
const eventBus = new GameEventBus();
|
||||
this.eventBus = eventBus;
|
||||
GameFactory.evalGameScripts(rawWorld, game.scene, eventBus);
|
||||
|
||||
this.emitEvent('game_start');
|
||||
|
||||
this._stopAutoSave = this.startAutoSave();
|
||||
makeAutoObservable(this);
|
||||
makeAutoObservable(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
public emitEvent(event: string) {
|
||||
this.eventBus.emit(
|
||||
event,
|
||||
{
|
||||
world: this.world.data,
|
||||
scene: this.scene,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public get asGame(): Game {
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ export class RootState {
|
|||
}
|
||||
|
||||
public startGame(): void {
|
||||
state.worldEditor.resetSelection();
|
||||
if (this.game)
|
||||
this.stopGame();
|
||||
this.game = new GameState(this.world),
|
||||
state.worldEditor.resetSelectedObject();
|
||||
this.game = new GameState(this.world);
|
||||
}
|
||||
|
||||
public stopGame(): void {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ export class WorldEditorState {
|
|||
|
||||
public setSelection(value: Selection | undefined): void {
|
||||
this.selection = value;
|
||||
console.log(JSON.stringify(this.selection));
|
||||
}
|
||||
|
||||
public setSelectedObject(value: Omit<SelectedObject, 'type'> | undefined): void {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export class WorldState {
|
|||
private saveData(data: World): void {
|
||||
console.log('Saving world...');
|
||||
const stack = new Error('Saving world...').stack!.split('\n').slice(1);
|
||||
const { objectTypes, voxelTypes, ...debug } = toJS(data);
|
||||
const { voxelTypes, initialScene, ...debug } = toJS(data);
|
||||
console.dir({ stack, debug });
|
||||
WorldFactory.save(toJS(this.data));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export type ObjectType = {
|
|||
name: string;
|
||||
voxels: Voxel[];
|
||||
program?: string;
|
||||
javascript?: string;
|
||||
}
|
||||
|
||||
export type ObjectInstance = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import type { Scene, World } from "../model";
|
||||
|
||||
export type GameEventContext = {
|
||||
world: World;
|
||||
scene: Scene;
|
||||
}
|
||||
|
||||
export type GameEventHandler = (context: GameEventContext) => void;
|
||||
|
||||
export type GameBus = {
|
||||
onGameEvent: (
|
||||
event: string,
|
||||
params: Record<string, unknown>,
|
||||
callback: GameEventHandler,
|
||||
) => void;
|
||||
}
|
||||
Loading…
Reference in New Issue