HMR for blockly blocks

This commit is contained in:
azykov@mail.ru
2026-06-13 15:29:55 +03:00
parent 1ed3203ec5
commit a61b5f0105
36 changed files with 533 additions and 463 deletions
+18 -20
View File
@@ -1,24 +1,22 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['console_log_action'] = {
init(this: Blockly.Block) {
this.appendValueInput('VALUE')
.setAlign(Blockly.inputs.Align.RIGHT)
// .setCheck('String')
.appendField('print');
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Print value to console');
this.setHelpUrl('');
this.setColour(150);
export const consoleLogAction: BlockDef = {
category: 'Actions',
block: {
init(this: Blockly.Block) {
this.appendValueInput('VALUE')
.setAlign(Blockly.inputs.Align.RIGHT)
.appendField('print');
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Print value to console');
}
},
code(block, generator) {
const value = generator.valueToCode(block, 'VALUE', Order.ATOMIC);
return `console.log(${value});\n`;
}
};
javascriptGenerator.forBlock['console_log_action'] = function (block, generator) {
const valueValue = generator.valueToCode(block, 'VALUE', Order.ATOMIC);
return `console.log(${valueValue});\n`;
};
export { };
+2 -1
View File
@@ -1,3 +1,4 @@
export * from './consoleLog';
export * from './physics';
export * from './stopGame';
export * from './waitSeconds';
export * from './physics';
@@ -1,29 +1,25 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../../toolbox";
Blockly.Blocks['physics_apply_impulse_action'] = {
init(this: Blockly.Block) {
this.appendDummyInput('')
.appendField('Push me in direction of');
this.appendValueInput('POSITION')
.setCheck('DPos');
this.appendDummyInput('')
.appendField('with force')
.appendField(new Blockly.FieldNumber(1, -10, 10, 0.01), 'FORCE');
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Push me in a direction');
this.setColour(150);
export const physicsApplyImpulseAction: BlockDef = {
category: 'Actions',
block: {
init(this: Blockly.Block) {
this.appendDummyInput('').appendField('Push me in direction of');
this.appendValueInput('POSITION').setCheck('DPos');
this.appendDummyInput('')
.appendField('with force')
.appendField(new Blockly.FieldNumber(1, -10, 10, 0.01), 'FORCE');
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Push me in a direction');
}
},
code(block, generator) {
const position = generator.valueToCode(block, 'POSITION', Order.ATOMIC);
const force = block.getFieldValue('FORCE');
return `yield helpers.actions.ApplyImpulse(${position}, ${force});\n`;
}
};
javascriptGenerator.forBlock['physics_apply_impulse_action'] = function (block, generator) {
const positionValue = generator.valueToCode(block, 'POSITION', Order.ATOMIC);
const forceValue = block.getFieldValue('FORCE');
return `yield helpers.actions.ApplyImpulse(${positionValue}, ${forceValue});\n`;
};
export { };
@@ -1,3 +1,3 @@
export * from './applyImpulse';
export * from './moveInDirection';
export * from './moveToPosition';
export * from './moveToPosition';
@@ -1,30 +1,28 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../../toolbox";
Blockly.Blocks['move_in_direction_action'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('Move me in direction');
this.appendValueInput('DIRECTION')
.setCheck('DPos');
this.appendDummyInput()
.appendField('for')
.appendField(new Blockly.FieldNumber(5, 0.01, 10000, 0.1), 'DISTANCE')
.appendField('with speed')
.appendField(new Blockly.FieldNumber(5, 0.01, 1000, 0.1), 'SPEED');
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Move this object in the given direction for the given distance at the given speed (units/sec), blocking until done');
this.setColour(20);
export const moveInDirectionAction: BlockDef = {
category: 'Actions',
block: {
init(this: Blockly.Block) {
this.appendDummyInput().appendField('Move me in direction');
this.appendValueInput('DIRECTION').setCheck('DPos');
this.appendDummyInput()
.appendField('for')
.appendField(new Blockly.FieldNumber(5, 0.01, 10000, 0.1), 'DISTANCE')
.appendField('with speed')
.appendField(new Blockly.FieldNumber(5, 0.01, 1000, 0.1), 'SPEED');
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Move this object in the given direction for the given distance at the given speed (units/sec), blocking until done');
}
},
code(block, generator) {
const direction = generator.valueToCode(block, 'DIRECTION', Order.ATOMIC) || '[0,0,1]';
const distance = block.getFieldValue('DISTANCE');
const speed = block.getFieldValue('SPEED');
return `yield helpers.actions.MoveInDirection(${direction}, ${distance}, ${speed});\n`;
}
};
javascriptGenerator.forBlock['move_in_direction_action'] = function (block, generator) {
const direction = generator.valueToCode(block, 'DIRECTION', Order.ATOMIC) || '[0,0,1]';
const distance = block.getFieldValue('DISTANCE');
const speed = block.getFieldValue('SPEED');
return `yield helpers.actions.MoveInDirection(${direction}, ${distance}, ${speed});\n`;
};
export { };
@@ -1,27 +1,25 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../../toolbox";
Blockly.Blocks['move_to_position_action'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('Move me to position');
this.appendValueInput('TARGET_POSITION')
.setCheck('Pos');
this.appendDummyInput()
.appendField('with speed')
.appendField(new Blockly.FieldNumber(5, 0.01, 1000, 0.1), 'SPEED');
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Move this object to the target position at the given speed (units/sec), blocking until arrived');
this.setColour(20);
export const moveToPositionAction: BlockDef = {
category: 'Actions',
block: {
init(this: Blockly.Block) {
this.appendDummyInput().appendField('Move me to position');
this.appendValueInput('TARGET_POSITION').setCheck('Pos');
this.appendDummyInput()
.appendField('with speed')
.appendField(new Blockly.FieldNumber(5, 0.01, 1000, 0.1), 'SPEED');
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Move this object to the target position at the given speed (units/sec), blocking until arrived');
}
},
code(block, generator) {
const targetPosition = generator.valueToCode(block, 'TARGET_POSITION', Order.ATOMIC) || '[0,0,0]';
const speed = block.getFieldValue('SPEED');
return `yield helpers.actions.MoveToPosition(${targetPosition}, ${speed});\n`;
}
};
javascriptGenerator.forBlock['move_to_position_action'] = function (block, generator) {
const targetPosition = generator.valueToCode(block, 'TARGET_POSITION', Order.ATOMIC) || '[0,0,0]';
const speed = block.getFieldValue('SPEED');
return `yield helpers.actions.MoveToPosition(${targetPosition}, ${speed});\n`;
};
export { };
+18
View File
@@ -0,0 +1,18 @@
import * as Blockly from "blockly";
import type { BlockDef } from "../toolbox";
export const stopGameAction: BlockDef = {
category: 'Actions',
block: {
init(this: Blockly.Block) {
this.appendDummyInput().appendField('Stop game');
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Stop game');
}
},
code(_block, _generator) {
return `yield helpers.actions.StopGame();\n`;
}
};
+17 -18
View File
@@ -1,22 +1,21 @@
import * as Blockly from "blockly";
import { javascriptGenerator } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
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);
export const waitSecondsAction: BlockDef = {
category: 'Actions',
block: {
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');
}
},
code(block, _generator) {
const seconds = block.getFieldValue('SECONDS');
return `yield helpers.actions.WaitSeconds(${seconds});\n`;
}
};
javascriptGenerator.forBlock['wait_seconds_action'] = function (block) {
const seconds = block.getFieldValue('SECONDS');
return `yield helpers.actions.WaitSeconds(${seconds});\n`;
};
export { };
+14 -14
View File
@@ -1,17 +1,17 @@
import * as Blockly from "blockly";
import { javascriptGenerator } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
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 const gameStartEvent: BlockDef = {
category: 'Events',
block: {
init(this: Blockly.Block) {
this.appendDummyInput().appendField('On game start');
this.setNextStatement(true, null);
this.setStyle('event_hat');
this.setTooltip('Runs when the game starts.');
}
},
code(_block, _generator) {
return '';
}
};
export { };
+13 -14
View File
@@ -1,18 +1,17 @@
import * as Blockly from "blockly";
import { javascriptGenerator } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['object_clicked_event'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('When I\'m clicked');
this.setStyle('event_hat');
this.setNextStatement(true);
this.setTooltip('Fires when an object is clicked');
export const objectClickedEvent: BlockDef = {
category: 'Events',
block: {
init(this: Blockly.Block) {
this.appendDummyInput().appendField('When I\'m clicked');
this.setStyle('event_hat');
this.setNextStatement(true);
this.setTooltip('Fires when an object is clicked');
}
},
code(_block, _generator) {
return '';
}
};
javascriptGenerator.forBlock['object_clicked_event'] = function (_block, _generator) {
return '';
};
export { };
+18 -20
View File
@@ -1,27 +1,25 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['object_touch_start_event'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('When');
this.appendValueInput('OTHER_OBJECT')
.setCheck('Object');
this.appendDummyInput()
.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) {
const otherObjectValue = generator.valueToCode(block, 'OTHER_OBJECT', Order.NONE) || 'undefined';
return `
export const objectTouchStartEvent: BlockDef = {
category: 'Events',
block: {
init(this: Blockly.Block) {
this.appendDummyInput().appendField('When');
this.appendValueInput('OTHER_OBJECT').setCheck('Object');
this.appendDummyInput().appendField('touches me');
this.setStyle('event_hat');
this.setNextStatement(true);
this.setTooltip('Fires when an object touches me');
}
},
code(block, generator) {
const otherObjectValue = generator.valueToCode(block, 'OTHER_OBJECT', Order.NONE) || 'undefined';
return `
const touchTarget = ${otherObjectValue};
if (!context.other || !touchTarget || context.other.id !== touchTarget.id)
return;
`;
}
};
export { };
+13 -14
View File
@@ -1,18 +1,17 @@
import * as Blockly from "blockly";
import { javascriptGenerator } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
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');
export const playerTouchStartEvent: BlockDef = {
category: 'Events',
block: {
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');
}
},
code(_block, _generator) {
return '';
}
};
javascriptGenerator.forBlock['player_touch_start_event'] = function (_block, _generator) {
return '';
};
export { };
+3 -3
View File
@@ -1,13 +1,13 @@
import * as Blockly from "blockly";
import { javascriptGenerator } from "blockly/javascript";
export function generateCode(workspace: Blockly.WorkspaceSvg): string {
export function generateCode(workspace: Blockly.Workspace): string {
javascriptGenerator.init(workspace);
let result = '';
for (const block of workspace.getTopBlocks(true)) {
if (block.type.endsWith('_event')) {
if (block.type.endsWith('Event')) {
const eventName = block.type.replace(/_event$/, '');
const eventName = block.type.replace(/Event$/, '');
let body = javascriptGenerator.blockToCode(block, true) as string;
+2 -1
View File
@@ -1,7 +1,8 @@
export * from './events';
export * from './values';
export * from './actions';
export * from './loops';
export * from './theme';
export * from './generateCode';
export * from './fieldTypes';
export * from './loops';
export * from './toolbox';
+20
View File
@@ -0,0 +1,20 @@
import * as Blockly from "blockly";
import { javascriptGenerator } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
export const foreverLoop: BlockDef = {
category: 'Loops',
block: {
init(this: Blockly.Block) {
this.appendDummyInput().appendField('Repeat forever');
this.appendStatementInput('BODY').setCheck(null);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Repeat the enclosed actions forever');
}
},
code(block, generator) {
const body = generator.statementToCode(block, 'BODY');
return `while (true) {\n${body}${javascriptGenerator.INDENT}yield helpers.actions.WaitFrames(1);\n}\n`;
}
};
+1 -1
View File
@@ -1 +1 @@
export * from './repeatForever';
export * from './forever';
@@ -1,22 +0,0 @@
import * as Blockly from "blockly";
import { javascriptGenerator } from "blockly/javascript";
Blockly.Blocks['loop_forever'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('Repeat forever');
this.appendStatementInput('BODY')
.setCheck(null);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Repeat the enclosed actions forever');
this.setColour(120);
}
};
javascriptGenerator.forBlock['loop_forever'] = function (block, generator) {
const body = generator.statementToCode(block, 'BODY');
return `while (true) {\n${body}${generator.INDENT}yield helpers.actions.WaitFrames(1);\n}\n`;
};
export { };
+21
View File
@@ -0,0 +1,21 @@
import * as Blockly from "blockly";
import { runInAction } from "mobx";
import type { ObjectType } from "../types";
import * as blocklyBarrel from "./index";
export function recompileAll(objectTypes: Record<string, ObjectType>): void {
for (const objectType of Object.values(objectTypes)) {
if (!objectType.program)
continue;
const workspace = new Blockly.Workspace();
try {
Blockly.serialization.workspaces.load(JSON.parse(objectType.program), workspace);
const js = blocklyBarrel.generateCode(workspace);
runInAction(() => { objectType.javascript = js; });
} catch {
runInAction(() => { objectType.javascript = undefined; });
} finally {
workspace.dispose();
}
}
}
+12
View File
@@ -0,0 +1,12 @@
import * as barrel from './index';
import { registerBarrel } from './toolbox';
registerBarrel(barrel as Record<string, unknown>);
if (import.meta.hot) {
import.meta.hot.accept('./index', (newBarrel) => {
if (newBarrel)
registerBarrel(newBarrel as Record<string, unknown>);
window.dispatchEvent(new CustomEvent('blockly-hmr'));
});
}
+71
View File
@@ -0,0 +1,71 @@
import * as Blockly from "blockly";
import { javascriptGenerator, type JavascriptGenerator, type Order } from "blockly/javascript";
export const CATEGORY_COLOURS = {
Events: '180',
Values: '260',
Loops: '50',
Actions: '20',
} as const;
export type CategoryName = keyof typeof CATEGORY_COLOURS;
export type BlockDef = {
category: CategoryName;
block: { init(this: Blockly.Block): void };
code: (block: Blockly.Block, generator: JavascriptGenerator) => string | [string, Order];
};
function isCategoryName(v: unknown): v is CategoryName {
return typeof v === 'string' && v in CATEGORY_COLOURS;
}
function isBlockDef(v: unknown): v is BlockDef {
if (typeof v !== 'object' || v === null)
return false;
const r = v as Record<string, unknown>;
return (
isCategoryName(r.category) &&
typeof r.block === 'object' && r.block !== null &&
typeof r.code === 'function'
);
}
export function registerBarrel(barrel: Record<string, unknown>): void {
for (const [name, value] of Object.entries(barrel)) {
if (!isBlockDef(value))
continue;
const colour = CATEGORY_COLOURS[value.category];
Blockly.Blocks[name] = {
init(this: Blockly.Block) {
value.block.init.call(this);
this.setColour(colour);
}
};
javascriptGenerator.forBlock[name] = value.code;
}
}
export function barrelToToolbox(
barrel: Record<string, unknown>
): Blockly.utils.toolbox.ToolboxDefinition {
const categories = new Map<CategoryName, string[]>();
for (const [name, value] of Object.entries(barrel)) {
if (!isBlockDef(value))
continue;
if (!categories.has(value.category))
categories.set(value.category, []);
categories.get(value.category)!.push(name);
}
return {
kind: 'categoryToolbox',
contents: Array.from(categories.entries()).map(([name, blocks]) => ({
kind: 'category',
name,
colour: CATEGORY_COLOURS[name],
contents: blocks.map(type => ({ kind: 'block', type })),
})),
};
}
+15 -17
View File
@@ -1,21 +1,19 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['any_object_of_type_value'] = {
init(this: Blockly.Block) {
this.appendDummyInput('NAME')
.appendField('Any object of type');
this.appendValueInput('NAME')
.setCheck('ObjectType');
this.setInputsInline(true);
this.setOutput(true, 'Object');
this.setTooltip('Any object of type');
this.setColour(315);
export const anyObjectOfTypeValue: BlockDef = {
category: 'Values',
block: {
init(this: Blockly.Block) {
this.appendDummyInput('NAME').appendField('Any object of type');
this.appendValueInput('NAME').setCheck('ObjectType');
this.setInputsInline(true);
this.setOutput(true, 'Object');
this.setTooltip('Any object of type');
}
},
code(_block, _generator) {
return ['123', Order.NONE];
}
};
javascriptGenerator.forBlock['any_object_of_type_value'] = function (_block, _generator) {
return ['123', Order.NONE];
};
export { };
+14 -15
View File
@@ -1,19 +1,18 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['current_object_value'] = {
init(this: Blockly.Block) {
this.appendEndRowInput('NAME')
.appendField('Me');
this.setInputsInline(false);
this.setOutput(true, 'Object');
this.setTooltip('Returns current object instance');
this.setColour(315);
export const currentObjectValue: BlockDef = {
category: 'Values',
block: {
init(this: Blockly.Block) {
this.appendEndRowInput('NAME').appendField('Me');
this.setInputsInline(false);
this.setOutput(true, 'Object');
this.setTooltip('Returns current object instance');
}
},
code(_block, _generator) {
return ['thisObject', Order.NONE];
}
};
javascriptGenerator.forBlock['current_object_value'] = function (_block, _generator) {
return ['thisObject', Order.NONE];
};
export { };
@@ -1,19 +1,18 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['current_object_type_value'] = {
init(this: Blockly.Block) {
this.appendEndRowInput('NAME')
.appendField('My type');
this.setInputsInline(false);
this.setOutput(true, 'ObjectType');
this.setTooltip('Returns current object type');
this.setColour(315);
export const currentObjectTypeValue: BlockDef = {
category: 'Values',
block: {
init(this: Blockly.Block) {
this.appendEndRowInput('NAME').appendField('My type');
this.setInputsInline(false);
this.setOutput(true, 'ObjectType');
this.setTooltip('Returns current object type');
}
},
code(_block, _generator) {
return ['thisObjectType', Order.NONE];
}
};
javascriptGenerator.forBlock['current_object_type_value'] = function (_block, _generator) {
return ['thisObjectType', Order.NONE];
};
export { };
+24 -25
View File
@@ -1,29 +1,28 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['dpos_value'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('Direction [')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'X')
.appendField(',')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Y')
.appendField(',')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Z');
this.appendDummyInput()
.appendField(']');
this.setInputsInline(true);
this.setOutput(true, 'DPos');
this.setTooltip('Direction [dx,dy,dz]');
this.setColour(315);
export const dposValue: BlockDef = {
category: 'Values',
block: {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('Direction [')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'X')
.appendField(',')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Y')
.appendField(',')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Z');
this.appendDummyInput().appendField(']');
this.setInputsInline(true);
this.setOutput(true, 'DPos');
this.setTooltip('Direction [dx,dy,dz]');
}
},
code(block, _generator) {
const x = block.getFieldValue('X');
const y = block.getFieldValue('Y');
const z = block.getFieldValue('Z');
return [`[${x},${y},${z}]`, Order.ATOMIC];
}
};
javascriptGenerator.forBlock['dpos_value'] = function (block, _generator) {
const xValue = block.getFieldValue('X');
const yValue = block.getFieldValue('Y');
const zValue = block.getFieldValue('Z');
return [`[${xValue},${yValue},${zValue}]`, Order.ATOMIC];
};
export { };
@@ -1,22 +1,20 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['direction_to_position_value'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('Direction to');
this.appendValueInput('TARGET')
.setCheck('Pos');
this.setInputsInline(true);
this.setOutput(true, 'DPos');
this.setTooltip('Direction to a position');
this.setColour(315);
export const directionToPositionValue: BlockDef = {
category: 'Values',
block: {
init(this: Blockly.Block) {
this.appendDummyInput().appendField('Direction to');
this.appendValueInput('TARGET').setCheck('Pos');
this.setInputsInline(true);
this.setOutput(true, 'DPos');
this.setTooltip('Direction to a position');
}
},
code(block, generator) {
const targetValue = generator.valueToCode(block, 'TARGET', Order.ATOMIC);
return [`(t=>{const p=helpers.positionOfObject(thisObject); const dir = [t[0]-p[0],t[1]-p[1],t[2]-p[2]]; /* helpers.debug.show('${block.id}.targetPosition', ${targetValue}); helpers.debug.show('${block.id}.result', dir); helpers.debug.show('${block.id}.thisObject.position', p); */ return dir;})(${targetValue})`, Order.NONE];
}
};
javascriptGenerator.forBlock['direction_to_position_value'] = function (block, generator) {
const targetValue = generator.valueToCode(block, 'TARGET', Order.ATOMIC);
return [`(t=>{const p=helpers.positionOfObject(thisObject); const dir = [t[0]-p[0],t[1]-p[1],t[2]-p[2]]; /* helpers.debug.show('${block.id}.targetPosition', ${targetValue}); helpers.debug.show('${block.id}.result', dir); helpers.debug.show('${block.id}.thisObject.position', p); */ return dir;})(${targetValue})`, Order.NONE];
};
export { };
+17 -17
View File
@@ -1,21 +1,21 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['object_by_id_value'] = {
init(this: Blockly.Block) {
this.appendEndRowInput()
.appendField('Object with id')
.appendField(new Blockly.FieldTextInput(''), 'TARGET_ID');
this.setInputsInline(false);
this.setOutput(true, 'Object');
this.setTooltip('Returns object by id, if any');
this.setColour(315);
export const objectByIdValue: BlockDef = {
category: 'Values',
block: {
init(this: Blockly.Block) {
this.appendEndRowInput()
.appendField('Object with id')
.appendField(new Blockly.FieldTextInput(''), 'TARGET_ID');
this.setInputsInline(false);
this.setOutput(true, 'Object');
this.setTooltip('Returns object by id, if any');
}
},
code(block, _generator) {
const targetId = block.getFieldValue('TARGET_ID');
return [`context.scene.objects['${targetId}']`, Order.ATOMIC];
}
};
javascriptGenerator.forBlock['object_by_id_value'] = function (block, _generator) {
const targetIdValue = block.getFieldValue('TARGET_ID');
return [`context.scene.objects['${targetIdValue}']`, Order.ATOMIC];
};
export { };
+19 -19
View File
@@ -1,7 +1,8 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import { toJS } from "mobx";
import { state } from "../../state";
import type { BlockDef } from "../toolbox";
export function getObjectTypeDropDownOptions(): [string, string][] {
const types = Object.values(toJS(state.world.data.objectTypes));
@@ -12,23 +13,22 @@ export function getObjectTypeDropDownOptions(): [string, string][] {
return values;
}
Blockly.Blocks['object_type_value'] = {
init(this: Blockly.Block) {
this.appendEndRowInput()
.appendField(
new Blockly.FieldDropdown(getObjectTypeDropDownOptions),
'OBJECT_TYPE',
);
this.setInputsInline(false);
this.setOutput(true, 'ObjectType');
this.setTooltip('Object type');
this.setColour(315);
export const objectTypeValue: BlockDef = {
category: 'Values',
block: {
init(this: Blockly.Block) {
this.appendEndRowInput()
.appendField(
new Blockly.FieldDropdown(getObjectTypeDropDownOptions),
'OBJECT_TYPE',
);
this.setInputsInline(false);
this.setOutput(true, 'ObjectType');
this.setTooltip('Object type');
}
},
code(block, _generator) {
const objectTypeValue = block.getFieldValue('OBJECT_TYPE');
return [`world.objectTypes[${objectTypeValue}]`, Order.NONE];
}
};
javascriptGenerator.forBlock['object_type_value'] = function (block, _generator) {
const objectTypeValue = block.getFieldValue('OBJECT_TYPE');
return [`world.objectTypes[${objectTypeValue}]`, Order.NONE];
};
export { };
+14 -15
View File
@@ -1,19 +1,18 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['player_object_value'] = {
init(this: Blockly.Block) {
this.appendEndRowInput()
.appendField('Player');
this.setInputsInline(false);
this.setOutput(true, 'Object');
this.setTooltip('Returns object that is controlled by player');
this.setColour(315);
export const playerObjectValue: BlockDef = {
category: 'Values',
block: {
init(this: Blockly.Block) {
this.appendEndRowInput().appendField('Player');
this.setInputsInline(false);
this.setOutput(true, 'Object');
this.setTooltip('Returns object that is controlled by player');
}
},
code(_block, _generator) {
return ['(scene.playerObjectId?scene.objects[scene.playerObjectId]:undefined)', Order.ATOMIC];
}
};
javascriptGenerator.forBlock['player_object_value'] = function (_block, _generator) {
return ['(scene.playerObjectId?scene.objects[scene.playerObjectId]:undefined)', Order.ATOMIC];
};
export { };
+24 -25
View File
@@ -1,29 +1,28 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['pos_value'] = {
init(this: Blockly.Block) {
this.appendDummyInput('POSITION')
.appendField('Position [')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'X')
.appendField(',')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Y')
.appendField(',')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Z');
this.appendDummyInput()
.appendField(']');
this.setInputsInline(true);
this.setOutput(true, 'Pos');
this.setTooltip('Position [x,y,z]');
this.setColour(315);
export const posValue: BlockDef = {
category: 'Values',
block: {
init(this: Blockly.Block) {
this.appendDummyInput('POSITION')
.appendField('Position [')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'X')
.appendField(',')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Y')
.appendField(',')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Z');
this.appendDummyInput().appendField(']');
this.setInputsInline(true);
this.setOutput(true, 'Pos');
this.setTooltip('Position [x,y,z]');
}
},
code(block, _generator) {
const x = block.getFieldValue('X');
const y = block.getFieldValue('Y');
const z = block.getFieldValue('Z');
return [`[${x},${y},${z}]`, Order.ATOMIC];
}
};
javascriptGenerator.forBlock['pos_value'] = function (block, _generator) {
const xValue = block.getFieldValue('X');
const yValue = block.getFieldValue('Y');
const zValue = block.getFieldValue('Z');
return [`[${xValue},${yValue},${zValue}]`, Order.ATOMIC];
};
export { };
@@ -1,22 +1,20 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { Order } from "blockly/javascript";
import type { BlockDef } from "../toolbox";
Blockly.Blocks['position_of_object_value'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('Position of');
this.appendValueInput('TARGET')
.setCheck('Object');
this.setInputsInline(true);
this.setOutput(true, 'Pos');
this.setTooltip('Position of an object');
this.setColour(315);
export const positionOfObjectValue: BlockDef = {
category: 'Values',
block: {
init(this: Blockly.Block) {
this.appendDummyInput().appendField('Position of');
this.appendValueInput('TARGET').setCheck('Object');
this.setInputsInline(true);
this.setOutput(true, 'Pos');
this.setTooltip('Position of an object');
}
},
code(block, generator) {
const targetValue = generator.valueToCode(block, 'TARGET', Order.ATOMIC);
return [`helpers.positionOfObject(${targetValue})`, Order.NONE];
}
};
javascriptGenerator.forBlock['position_of_object_value'] = function (block, generator) {
const targetValue = generator.valueToCode(block, 'TARGET', Order.ATOMIC);
return [`helpers.positionOfObject(${targetValue})`, Order.NONE];
};
export { };
+1 -1
View File
@@ -18,7 +18,7 @@ export const GameObjectView = observer(function (props: GameObjectViewProps) {
return <PlayerObjectView object={props.object as RuntimeGameObjectInstance} objectType={objectType} />;
function handleClick(): void {
state.game?.emitEvent('object_clicked', { object: props.object });
state.game?.emitEvent('objectClicked', { object: props.object });
}
return <ObjectViewInternal
@@ -1,4 +1,4 @@
import type { ObjectType, ObjectInstanceRigidBody, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
import type { ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
import { useEffect, useImperativeHandle, useRef, type Ref } from "react";
import { Euler, Quaternion, type Group } from "three";
import { Instance, Instances } from "@react-three/drei";
@@ -94,9 +94,9 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
return;
const otherObj = Object.values(game.scene.objects)
.find((o) => o.rb()?.handle === otherHandle);
game.emitEvent('object_touch_start', { object: gameObj, other: otherObj });
game.emitEvent('objectTouchStart', { object: gameObj, other: otherObj });
if (otherObj?.id === game.scene.playerObjectId)
game.emitEvent('player_touch_start', { object: gameObj, other: otherObj });
game.emitEvent('playerTouchStart', { object: gameObj, other: otherObj });
}}
onSync={(data) => {
runInAction(() => {
@@ -5,70 +5,11 @@ 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: 'categoryToolbox',
contents: [
{
kind: 'category',
name: 'Events',
colour: '180',
contents: [
{ kind: 'block', type: 'game_start_event' },
{ kind: 'block', type: 'object_touch_start_event' },
{ kind: 'block', type: 'player_touch_start_event' },
{ kind: 'block', type: 'object_clicked_event' },
],
},
{
kind: 'category',
name: 'Values',
colour: '260',
contents: [
{ kind: 'block', type: 'current_object_value' },
{ kind: 'block', type: 'current_object_type_value' },
{ kind: 'block', type: 'object_type_value' },
{ kind: 'block', type: 'any_object_of_type_value' },
{ kind: 'block', type: 'position_of_object_value' },
{ kind: 'block', type: 'pos_value' },
{ kind: 'block', type: 'dpos_value' },
{ kind: 'block', type: 'direction_to_position_value' },
{ kind: 'block', type: 'object_by_id_value' },
{ kind: 'block', type: 'player_object_value' },
],
},
{
kind: 'category',
name: 'Loops',
colour: '50',
contents: [
{ kind: 'block', type: 'loop_forever' },
],
},
{
kind: 'category',
name: 'Actions',
colour: '20',
contents: [
{ kind: 'block', type: 'console_log_action' },
{ kind: 'block', type: 'move_to_position_action' },
{ kind: 'block', type: 'move_in_direction_action' },
{ kind: 'block', type: 'physics_apply_impulse_action' },
{ kind: 'block', type: 'wait_seconds_action' },
],
},
{
kind: 'category',
name: 'Text',
colour: '160',
contents: [
{ kind: 'block', type: 'text' },
],
},
],
};
import '../../blockly/registrar';
import * as blocklyBarrel from '../../blockly';
import { gameTheme } from '../../blockly';
import { recompileAll } from '../../blockly/recompile';
import { state } from '../../state';
type ScriptEditorViewProps = {
objectType: ObjectType;
@@ -76,29 +17,44 @@ type ScriptEditorViewProps = {
export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorViewProps) {
const [popup, setPopup] = useState<{ title: string; content: string } | null>(null);
const [blocklyKey, setBlocklyKey] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
const objectTypeRef = useRef(objectType);
useLayoutEffect(() => { objectTypeRef.current = objectType; });
useEffect(() => {
if (!import.meta.hot)
return;
const handler = () => {
recompileAll(state.world.data.objectTypes);
setBlocklyKey(k => k + 1);
};
window.addEventListener('blockly-hmr', handler);
return () => window.removeEventListener('blockly-hmr', handler);
}, []);
useEffect(() => {
if (!containerRef.current)
return;
const workspace = Blockly.inject(containerRef.current, { toolbox: TOOLBOX, theme: gameTheme });
const toolbox = blocklyBarrel.barrelToToolbox(blocklyBarrel as Record<string, unknown>);
const workspace = Blockly.inject(containerRef.current, { toolbox, theme: gameTheme });
workspaceRef.current = workspace;
const listener = (e: Blockly.Events.Abstract) => {
if (e.isUiEvent)
return;
console.log('change event');
runInAction(() => {
const current = objectTypeRef.current;
const serialized = JSON.stringify(Blockly.serialization.workspaces.save(workspace));
current.program = serialized;
try {
current.javascript = generateCode(workspace);
current.javascript = blocklyBarrel.generateCode(workspace);
}
catch (err) {
console.log(err);
@@ -128,7 +84,9 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
workspace.dispose();
workspaceRef.current = null;
};
}, [objectType.program]);
// workspace should not reload when objectType.program changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [blocklyKey]);
useEffect(() => {
const workspace = workspaceRef.current;
@@ -145,7 +103,7 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
} finally {
Blockly.Events.enable();
}
}, [objectType.id, objectType.program]);
}, [objectType.id]); // eslint-disable-line react-hooks/exhaustive-deps
const showProgram = () => {
const raw = objectType.program;
+4 -3
View File
@@ -1,6 +1,7 @@
export * from './applyImpulseAction';
export * from './moveInDirectionAction';
export * from './moveToPositionAction';
export * from './stopGameAction';
export * from './waitConditionAction';
export * from './waitFramesAction';
export * from './waitSecondsAction';
export * from './applyImpulseAction';
export * from './moveToPositionAction';
export * from './moveInDirectionAction';
@@ -0,0 +1,15 @@
import { state } from "../../state";
import { ScriptAction, type ScriptCoroutineContext } from "../../types";
export class StopGameScriptAction extends ScriptAction {
constructor(coroutine: ScriptCoroutineContext) {
super(coroutine);
}
public tick(_deltaTime: number): boolean {
state.stopGame();
return true; // one shot
}
}
+1 -1
View File
@@ -134,7 +134,7 @@ export class GameState {
if (this.isFirstTick) {
this.isFirstTick = false;
this.emitEvent('game_start');
this.emitEvent('gameStart');
}
}
}