This commit is contained in:
2026-01-19 23:44:32 -06:00
commit 11bd5aa72b
64 changed files with 13433 additions and 0 deletions

187
src/blocks/control.js Normal file
View File

@@ -0,0 +1,187 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
Blockly.Blocks["wait_one_frame"] = {
init: function () {
this.appendDummyInput().appendField("wait one frame");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("control_blocks");
},
};
Blockly.Blocks["wait_block"] = {
init: function () {
this.appendValueInput("AMOUNT").setCheck("Number").appendField("wait");
this.appendDummyInput().appendField(
new Blockly.FieldDropdown([
["seconds", "1000"],
["milliseconds", "1"],
]),
"MENU"
);
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("control_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["wait_one_frame"] = function (block) {
return `await waitOneFrame();\n`;
};
BlocklyJS.javascriptGenerator.forBlock["wait_block"] = function (
block,
generator
) {
const duration =
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0;
const menu = block.getFieldValue("MENU") || 0;
return `await wait(${duration} * ${+menu});\n`;
};
Blockly.Blocks["controls_thread_create"] = {
init: function () {
this.appendDummyInput().appendField("create thread");
this.appendStatementInput("code").setCheck("default");
this.setTooltip("Create and run the code specified in a new thread");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("control_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["controls_thread_create"] = function (
block,
generator
) {
const code = generator.statementToCode(block, "code");
return `Thread.getCurrentContext().spawn(async () => {\n${code}});`;
};
Blockly.Blocks["controls_thread_current"] = {
init: function () {
this.appendDummyInput().appendField("current thread");
this.setOutput(true, "ThreadID");
this.setStyle("control_blocks");
this.setTooltip("Return the ID of the currently running thread");
},
};
BlocklyJS.javascriptGenerator.forBlock["controls_thread_current"] = () => [
`Thread.getCurrentContext().id`,
BlocklyJS.Order.MEMBER,
];
Blockly.Blocks["controls_thread_set_var"] = {
init: function () {
this.appendValueInput("NAME")
.setCheck("String")
.appendField("set variable");
this.appendValueInput("VALUE").appendField("to");
this.appendValueInput("THREAD")
.setCheck("ThreadID")
.appendField("in thread");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setInputsInline(true);
this.setStyle("control_blocks");
this.setTooltip("Set a variable inside the given thread");
},
};
BlocklyJS.javascriptGenerator.forBlock["controls_thread_set_var"] = function (
block,
generator
) {
const threadId =
generator.valueToCode(block, "THREAD", BlocklyJS.Order.NONE) || "null";
const name =
generator.valueToCode(block, "NAME", BlocklyJS.Order.NONE) || '""';
const value =
generator.valueToCode(block, "VALUE", BlocklyJS.Order.NONE) || "undefined";
return `Thread.set(${threadId}, ${name}, ${value});\n`;
};
Blockly.Blocks["controls_thread_get_var"] = {
init: function () {
this.appendValueInput("NAME")
.setCheck("String")
.appendField("get variable");
this.appendValueInput("THREAD")
.setCheck("ThreadID")
.appendField("from thread");
this.setInputsInline(true);
this.setOutput(true, null);
this.setStyle("control_blocks");
this.setTooltip("Get a variable from the given thread");
},
};
BlocklyJS.javascriptGenerator.forBlock["controls_thread_get_var"] = function (
block,
generator
) {
const threadId =
generator.valueToCode(block, "THREAD", BlocklyJS.Order.NONE) || "null";
const name =
generator.valueToCode(block, "NAME", BlocklyJS.Order.NONE) || '""';
const code = `Thread.get(${threadId}, ${name})`;
return [code, BlocklyJS.Order.FUNCTION_CALL];
};
Blockly.Blocks["controls_thread_has_var"] = {
init: function () {
this.appendValueInput("NAME").setCheck("String").appendField("variable");
this.appendValueInput("THREAD")
.setCheck("ThreadID")
.appendField("exists in thread");
this.setInputsInline(true);
this.setOutput(true, null);
this.setStyle("control_blocks");
this.setTooltip("Checks if a variable exists in the given thread");
},
};
BlocklyJS.javascriptGenerator.forBlock["controls_thread_has_var"] = function (
block,
generator
) {
const threadId =
generator.valueToCode(block, "THREAD", BlocklyJS.Order.NONE) || "null";
const name =
generator.valueToCode(block, "NAME", BlocklyJS.Order.NONE) || '""';
const code = `Thread.has(${threadId}, ${name})`;
return [code, BlocklyJS.Order.FUNCTION_CALL];
};
Blockly.Blocks["controls_run_instantly"] = {
init: function () {
this.appendDummyInput().appendField("run instantly");
this.appendStatementInput("do");
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setStyle("control_blocks");
this.setTooltip("Run inside code without frame delay");
},
};
BlocklyJS.javascriptGenerator.forBlock["controls_run_instantly"] = function (
block
) {
const branch = BlocklyJS.javascriptGenerator.statementToCode(block, "do");
return `let _prevFast = fastExecution;
fastExecution = true;
${branch}fastExecution = _prevFast;\n`;
};
Blockly.Blocks["controls_stopscript"] = {
init: function () {
this.appendDummyInput().appendField("stop this script");
this.setPreviousStatement(true, "default");
this.setStyle("control_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["controls_stopscript"] = () =>
'throw new Error("shouldStop");\n';

172
src/blocks/event.js Normal file
View File

@@ -0,0 +1,172 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
Blockly.Blocks["when_flag_clicked"] = {
init: function () {
this.appendDummyInput()
.appendField("when")
.appendField(
new Blockly.FieldImage("icons/flag.svg", 25, 25, {
alt: "Green flag",
flipRtl: "FALSE",
})
)
.appendField("clicked");
this.appendStatementInput("DO").setCheck("default");
this.setStyle("events_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["when_flag_clicked"] = function (
block,
generator
) {
const branch = generator.statementToCode(block, "DO");
return `registerEvent("flag", null, async () => {\n${branch}});\n`;
};
const normalKeys = [
..."abcdefghijklmnopqrstuvwxyz",
..."abcdefghijklmnopqrstuvwxyz0123456789".toUpperCase(),
];
Blockly.Blocks["when_key_clicked"] = {
init: function () {
this.appendDummyInput()
.appendField("when")
.appendField(
new Blockly.FieldDropdown([
["any", "any"],
["space", " "],
["enter", "Enter"],
["escape", "Escape"],
["up arrow", "ArrowUp"],
["down arrow", "ArrowDown"],
["left arrow", "ArrowLeft"],
["right arrow", "ArrowRight"],
...normalKeys.map((i) => [i, i]),
]),
"KEY"
)
.appendField("key pressed");
this.appendStatementInput("DO").setCheck("default");
this.setStyle("events_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["when_key_clicked"] = function (
block,
generator
) {
const key = block.getFieldValue("KEY");
const safeKey = generator.quote_(key);
const branch = generator.statementToCode(block, "DO");
return `registerEvent("key", ${safeKey}, async () => {\n${branch}});\n`;
};
Blockly.Blocks["when_stage_clicked"] = {
init: function () {
this.appendDummyInput()
.appendField("when stage clicked");
this.appendStatementInput("DO").setCheck("default");
this.setStyle("events_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["when_stage_clicked"] = function (
block,
generator
) {
const branch = generator.statementToCode(block, "DO");
return `registerEvent("stageClick", null, async () => {\n${branch}});\n`;
};
Blockly.Blocks["project_timer"] = {
init: function () {
this.appendDummyInput().appendField("project timer");
this.setOutput(true, "Number");
this.setStyle("events_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["project_timer"] = function (block) {
return ["projectTime()", BlocklyJS.Order.NONE];
};
Blockly.Blocks["when_timer_reaches"] = {
init: function () {
this.appendDummyInput()
.appendField("when timer reaches")
.appendField(new Blockly.FieldNumber(2, 0), "VALUE")
.appendField("seconds");
this.appendStatementInput("DO").setCheck("default");
this.setStyle("events_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["when_timer_reaches"] = function (
block,
generator
) {
const value = block.getFieldValue("VALUE");
const branch = generator.statementToCode(block, "DO");
return `registerEvent("timer", ${value}, async () => {\n${branch}});\n`;
};
Blockly.Blocks["every_seconds"] = {
init: function () {
this.appendDummyInput()
.appendField("every")
.appendField(new Blockly.FieldNumber(2, 0.1), "SECONDS")
.appendField("seconds");
this.appendStatementInput("DO").setCheck("default");
this.setStyle("events_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["every_seconds"] = function (
block,
generator
) {
const seconds = block.getFieldValue("SECONDS");
const branch = generator.statementToCode(block, "DO");
return `registerEvent("interval", ${seconds}, async () => {\n${branch}});\n`;
};
Blockly.Blocks["when_custom_event_triggered"] = {
init: function () {
this.appendDummyInput()
.appendField("when")
.appendField(new Blockly.FieldTextInput("event_name"), "EVENT")
.appendField("triggered");
this.appendStatementInput("DO").setCheck("default");
this.setStyle("events_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["when_custom_event_triggered"] = function (
block,
generator
) {
const event = generator.quote_(block.getFieldValue("EVENT"));
const branch = generator.statementToCode(block, "DO");
return `registerEvent("custom", ${event}, async () => {\n${branch}});\n`;
};
Blockly.Blocks["trigger_custom_event"] = {
init: function () {
this.appendDummyInput()
.appendField("trigger")
.appendField(new Blockly.FieldTextInput("event_name"), "EVENT");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("events_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["trigger_custom_event"] = function (
block
) {
const event = BlocklyJS.javascriptGenerator.quote_(block.getFieldValue("EVENT"));
return `triggerCustomEvent(${event});\n`;
};

811
src/blocks/functions.js Normal file
View File

@@ -0,0 +1,811 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
const ARG_BLOCK_TYPE = "FunctionsArgumentBlock";
class CustomChecker extends Blockly.ConnectionChecker {
canConnect(a, b, isDragging, opt_distance) {
if (!isDragging) {
return super.canConnect(a, b, isDragging, opt_distance);
}
const existing = b.targetConnection && b.targetConnection.getSourceBlock();
if (
existing &&
existing.type === "functions_argument_block" &&
existing.isShadow()
) {
return false;
}
return super.canConnect(a, b, isDragging, opt_distance);
}
}
Blockly.registry.register(
Blockly.registry.Type.CONNECTION_CHECKER,
"CustomChecker",
CustomChecker,
true
);
class DuplicateOnDrag {
constructor(block) {
this.block = block;
}
isMovable() {
return true;
}
startDrag(e) {
const ws = this.block.workspace;
let typeToCreate = this.block.type;
if (this.block.argType_ === "statement") {
typeToCreate = "functions_statement_argument_block";
}
let data = this.block.toCopyData();
if (data?.blockState) {
data.blockState.type = typeToCreate;
} else {
data.blockState = { type: typeToCreate };
}
if (this.block.mutationToDom) {
const mutation = this.block.mutationToDom();
if (mutation) {
data.blockState.extraState = mutation.outerHTML;
}
}
this.copy = Blockly.clipboard.paste(data, ws);
this.baseStrat = new Blockly.dragging.BlockDragStrategy(this.copy);
this.copy.setDragStrategy(this.baseStrat);
this.baseStrat.startDrag(e);
}
drag(e) {
this.block.workspace
.getGesture(e)
.getCurrentDragger()
.setDraggable(this.copy);
this.baseStrat.drag(e);
}
endDrag(e) {
this.baseStrat?.endDrag(e);
}
revertDrag(e) {
this.copy?.dispose();
}
}
function typeToBlocklyCheck(type) {
return (
{
string: "String",
number: "Number",
boolean: "Boolean",
array: "Array",
object: "Object",
}[type] || null
);
}
function findDuplicateArgNames(types, names) {
const used = {};
const duplicates = [];
for (let i = 0; i < types.length; i++) {
const key = types[i] + ":" + names[i];
if (!names[i]) continue;
if (used[key]) duplicates.push(i);
else used[key] = true;
}
return duplicates;
}
function isValidIdentifier(name) {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
}
Blockly.Blocks["functions_argument_block"] = {
init() {
if (!this.argType_) this.argType_ = "string";
if (!this.argName_) this.argName_ = "arg";
this.setStyle("procedure_blocks");
this.appendDummyInput().appendField(
new Blockly.FieldLabel(this.argName_),
"ARG_NAME"
);
this.setOutput(true, null);
this.setMovable(true);
this.setDeletable(true);
setTimeout(() => {
if (this.setDragStrategy && this.isShadow()) {
this.setDragStrategy(new DuplicateOnDrag(this));
}
});
},
mutationToDom: function () {
const container = Blockly.utils.xml.createElement("mutation");
container.setAttribute("type", this.argType_ || "string");
container.setAttribute("name", this.argName_ || "arg");
return container;
},
domToMutation: function (xmlElement) {
const type = xmlElement.getAttribute("type") || "string";
const name = xmlElement.getAttribute("name") || "arg";
this.updateType_(type);
this.updateName_(name);
},
updateType_: function (type) {
this.argType_ = type;
if (type === "statement") {
this.setOutputShape(3);
this.setOutput(true, ARG_BLOCK_TYPE);
} else {
const outputType = typeToBlocklyCheck(type) || "String";
this.setOutput(true, [outputType, ARG_BLOCK_TYPE]);
}
},
updateName_: function (name) {
this.argName_ = name;
if (this.getField("ARG_NAME")) {
this.setFieldValue(name, "ARG_NAME");
} else {
this.appendDummyInput().appendField(
new Blockly.FieldLabel(name),
"ARG_NAME"
);
}
},
};
Blockly.Blocks["functions_statement_argument_block"] = {
init() {
if (!this.argName_) this.argName_ = "arg";
this.setStyle("procedure_blocks");
this.appendDummyInput().appendField(
new Blockly.FieldLabel(this.argName_),
"ARG_NAME"
);
this.setNextStatement(true, "default");
this.setPreviousStatement(true, "default");
},
mutationToDom: function () {
const container = Blockly.utils.xml.createElement("mutation");
container.setAttribute("name", this.argName_ || "arg");
return container;
},
domToMutation: function (xmlElement) {
const name = xmlElement.getAttribute("name") || "arg";
this.updateName_(name);
},
updateName_: function (name) {
this.argName_ = name;
if (this.getField("ARG_NAME")) {
this.setFieldValue(name, "ARG_NAME");
} else {
this.appendDummyInput().appendField(
new Blockly.FieldLabel(name),
"ARG_NAME"
);
}
},
};
Blockly.Blocks["functions_definition"] = {
init: function () {
this.setStyle("procedure_blocks");
this.setTooltip("Function definition with a variable number of inputs.");
this.setInputsInline(true);
this.functionId_ = Blockly.utils.idGenerator.genUid();
this.itemCount_ = 0;
this.argTypes_ = [];
this.argNames_ = [];
this.blockShape_ = "statement";
this.returnTypes_ = [];
this.updateShape_();
this.setMutator(
new Blockly.icons.MutatorIcon(["functions_args_generic"], this)
);
},
mutationToDom: function () {
const container = Blockly.utils.xml.createElement("mutation");
container.setAttribute("functionid", this.functionId_);
container.setAttribute("items", String(this.itemCount_));
container.setAttribute("shape", this.blockShape_ || "statement");
for (let i = 0; i < this.itemCount_; i++) {
const item = Blockly.utils.xml.createElement("item");
item.setAttribute("type", this.argTypes_[i]);
item.setAttribute("name", this.argNames_[i]);
container.appendChild(item);
}
return container;
},
domToMutation: function (xmlElement) {
const items = xmlElement.getAttribute("items");
this.itemCount_ = items ? parseInt(items, 10) : 0;
this.argTypes_ = [];
this.argNames_ = [];
const children = [...xmlElement.children].filter(
n => n.tagName.toLowerCase() === "item"
);
for (let i = 0; i < children.length; i++) {
this.argTypes_[i] = children[i].getAttribute("type");
this.argNames_[i] = children[i].getAttribute("name");
}
while (this.argTypes_.length < this.itemCount_)
this.argTypes_.push("label");
while (this.argNames_.length < this.itemCount_) this.argNames_.push("text");
this.functionId_ =
xmlElement.getAttribute("functionid") ||
Blockly.utils.idGenerator.genUid();
this.blockShape_ = xmlElement.getAttribute("shape") || "statement";
this.updateShape_();
},
saveExtraState: function () {
return {
functionId: this.functionId_,
itemCount: this.itemCount_,
argTypes: this.argTypes_,
argNames: this.argNames_,
shape: this.blockShape_,
returnTypes: this.returnTypes_,
};
},
loadExtraState: function (state) {
this.functionId_ = state.functionId || Blockly.utils.idGenerator.genUid();
this.itemCount_ = state.itemCount || 0;
this.argTypes_ = state.argTypes || [];
this.argNames_ = state.argNames || [];
this.blockShape_ = state.shape || "statement";
this.returnTypes_ = state.returnTypes || [];
this.updateShape_();
},
createDefaultArgBlock_: function (type, name = "arg") {
Blockly.Events.disable();
let block;
try {
const ws = this.workspace;
block = ws.newBlock("functions_argument_block");
block.setShadow(true);
block.setEditable(false);
block.updateType_(type);
block.updateName_(name);
if (ws?.rendered) {
block.initSvg();
block.render();
}
} catch (_) {}
Blockly.Events.enable();
return block;
},
updateShape_: function () {
let savedBody = null;
const bodyInput = this.getInput("BODY");
if (bodyInput && bodyInput.connection?.targetConnection) {
savedBody = bodyInput.connection.targetConnection;
}
if (bodyInput) this.removeInput("BODY");
if (this.getInput("EMPTY")) this.removeInput("EMPTY");
if (this.getInput("SHAPE")) this.removeInput("SHAPE");
[...this.inputList].forEach(input => {
const connection = input.connection?.targetConnection;
if (connection) connection.getSourceBlock()?.dispose(false);
this.removeInput(input.name);
});
let firstArgAdded = this.argTypes_[0] === "label";
for (let i = 0; i < this.itemCount_; i++) {
const type = this.argTypes_[i];
const name = this.argNames_[i];
if (type === "label") {
this.appendDummyInput().appendField(new Blockly.FieldLabel(name));
} else {
const input = this.appendValueInput(name).setCheck(
typeToBlocklyCheck(type)
);
if (!firstArgAdded) {
input.appendField("my block with");
firstArgAdded = true;
}
const reporter = this.createDefaultArgBlock_(type, name);
reporter.setFieldValue(name, "ARG_NAME");
try {
reporter.outputConnection.connect(input.connection);
} catch (e) {}
}
}
if (this.itemCount_ === 0) {
this.appendDummyInput("EMPTY").appendField("my block");
}
const newBody = this.appendStatementInput("BODY").setCheck("default");
if (savedBody) {
try {
newBody.connection.connect(savedBody);
} catch (e) {}
}
},
decompose: function (workspace) {
const containerBlock = workspace.newBlock("functions_args_container");
if (workspace.rendered) containerBlock.initSvg();
let connection = containerBlock.getInput("STACK").connection;
for (let i = 0; i < this.itemCount_; i++) {
const type = this.argTypes_[i] || "label";
const name = this.argNames_[i] || "text";
const itemBlock = workspace.newBlock("functions_args_generic");
itemBlock.setFieldValue(type, "ARG_TYPE");
itemBlock.setFieldValue(name, "ARG_NAME");
if (workspace.rendered) itemBlock.initSvg();
itemBlock.valueConnection_ = null;
connection.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection;
}
containerBlock.setFieldValue(this.blockShape_, "SHAPEMENU");
return containerBlock;
},
compose: function (containerBlock) {
const newTypes = [];
const newNames = [];
let itemBlock = containerBlock.getInputTargetBlock("STACK");
while (itemBlock) {
if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) {
const type = itemBlock.getFieldValue("ARG_TYPE");
const name = itemBlock.getFieldValue("ARG_NAME");
newTypes.push(type);
newNames.push(name);
}
itemBlock = itemBlock.getNextBlock();
}
const dups = findDuplicateArgNames(newTypes, newNames);
const invalid = [];
for (let i = 0; i < newTypes.length; i++) {
const type = newTypes[i];
const name = newNames[i];
if (type !== "label") {
if (!isValidIdentifier(name)) {
invalid.push(i);
}
}
}
itemBlock = containerBlock.getInputTargetBlock("STACK");
let index = 0;
while (itemBlock) {
if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) {
if (dups.includes(index)) {
itemBlock.setWarningText(
"This argument name is already used for this type."
);
} else if (invalid.includes(index)) {
itemBlock.setWarningText("This argument name is not a valid.");
} else {
itemBlock.setWarningText(null);
}
index++;
}
itemBlock = itemBlock.getNextBlock();
}
const newBlockShape =
containerBlock.getFieldValue("SHAPEMENU") || "statement";
if (dups.length > 0 || invalid.length > 0) return;
this.itemCount_ = newTypes.length;
this.argTypes_ = newTypes;
this.argNames_ = newNames;
this.blockShape_ = newBlockShape;
this.updateShape_();
},
saveConnections: function (containerBlock) {
let itemBlock = containerBlock.getInputTargetBlock("STACK");
let i = 0;
while (itemBlock) {
if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) {
const key = this.argTypes_[i] + "_" + this.argNames_[i];
const input = this.getInput(key);
itemBlock.valueConnection_ =
input && input.connection && input.connection.targetConnection;
i++;
}
itemBlock = itemBlock.getNextBlock();
}
},
updateReturnState_: function () {
const body = this.getInputTargetBlock("BODY");
const types = new Set();
function walk(block) {
if (!block) return;
if (block?.childBlocks_?.length > 0) block?.childBlocks_.forEach(walk);
if (block.type === "functions_return") {
const val = block.getInputTargetBlock("VALUE");
const checks = val?.outputConnection?.check;
if (checks !== undefined) {
(Array.isArray(checks) ? checks : [checks]).forEach(t =>
types.add(t)
);
}
}
walk(block.getNextBlock());
}
walk(body);
if (types.size === 0) this.returnTypes_ = [];
else this.returnTypes_ = [...types];
},
};
Blockly.Blocks["functions_args_container"] = {
init: function () {
this.setStyle("procedure_blocks");
this.appendDummyInput().appendField("arguments");
this.appendStatementInput("STACK");
this.appendDummyInput()
.appendField("shape")
.appendField(
new Blockly.FieldDropdown([
[
{
src: "icons/statement.svg",
width: 98 * 0.6,
height: 57 * 0.6,
alt: "A block with top and bottom connections",
},
"statement",
],
[
{
src: "icons/terminal.svg",
width: 98 * 0.6,
height: 48 * 0.6,
alt: "A block with only a top connection",
},
"terminal",
],
]),
"SHAPEMENU"
);
this.contextMenu = false;
},
};
Blockly.Blocks["functions_args_generic"] = {
init() {
this.setStyle("procedure_blocks");
this.appendDummyInput()
.appendField("argument")
.appendField(
new Blockly.FieldDropdown([
["label", "label"],
["string", "string"],
["number", "number"],
["boolean", "boolean"],
["array", "array"],
["object", "object"],
["statement", "statement"],
]),
"ARG_TYPE"
)
.appendField(new Blockly.FieldTextInput("arg"), "ARG_NAME");
this.setPreviousStatement(true);
this.setNextStatement(true);
this.contextMenu = false;
this.valueConnection_ = null;
},
};
Blockly.Blocks["functions_call"] = {
init: function () {
this.setStyle("procedure_blocks");
this.setInputsInline(true);
this.functionId_ = null;
this.blockShape_ = null;
this.argTypes_ = [];
this.argNames_ = [];
this.previousArgTypes_ = [];
this.previousArgNames_ = [];
this.returnTypes_ = [];
this.updateShape_();
},
mutationToDom: function () {
const container = Blockly.utils.xml.createElement("mutation");
container.setAttribute("functionid", this.functionId_);
container.setAttribute("items", this.argTypes_.length);
container.setAttribute("shape", this.blockShape_ || "statement");
container.setAttribute(
"returntypes",
JSON.stringify(this.returnTypes_ || [])
);
for (let i = 0; i < this.argTypes_.length; i++) {
const item = Blockly.utils.xml.createElement("item");
item.setAttribute("type", this.argTypes_[i]);
item.setAttribute("name", this.argNames_[i]);
container.appendChild(item);
}
return container;
},
domToMutation: function (xmlElement) {
this.functionId_ = xmlElement.getAttribute("functionid");
this.blockShape_ = xmlElement.getAttribute("shape") || "statement";
this.previousArgTypes_ = [...this.argTypes_];
this.previousArgNames_ = [...this.argNames_];
this.argTypes_ = [];
this.argNames_ = [];
this.returnTypes_;
try {
this.returnTypes_ = JSON.parse(
xmlElement.getAttribute("returntypes") || "[]"
);
} catch {
this.returnTypes_ = [];
}
const items = parseInt(xmlElement.getAttribute("items") || "0", 10);
for (let i = 0; i < items; i++) {
const item = xmlElement.children[i];
this.argTypes_[i] = item.getAttribute("type");
this.argNames_[i] = item.getAttribute("name");
}
this.updateShape_();
},
matchDefinition: function (defBlock) {
this.functionId_ = defBlock.functionId_;
this.previousArgTypes_ = [...this.argTypes_];
this.previousArgNames_ = [...this.argNames_];
this.argTypes_ = [...defBlock.argTypes_];
this.argNames_ = [...defBlock.argNames_];
this.blockShape_ = defBlock.blockShape_;
this.returnTypes_ = [...defBlock.returnTypes_];
this.updateShape_();
if (defBlock.workspace.rendered) this.render();
},
updateShape_: function () {
const oldConnections = {};
[...this.inputList].forEach(input => {
if (input.connection && input.connection.targetBlock()) {
oldConnections[input.name] = input.connection.targetConnection;
}
this.removeInput(input.name);
});
const shape = this.blockShape_ || "statement";
const nextConn = this.nextConnection;
const prevConn = this.previousConnection;
const outputConn = this.outputConnection;
const returnTypes = this.returnTypes_ || [];
if (returnTypes?.length > 0) {
if (prevConn && prevConn.isConnected()) {
const blockAbove = prevConn.targetBlock();
blockAbove.unplug(true);
}
if (nextConn && nextConn.isConnected()) {
const blockBelow = nextConn.targetBlock();
blockBelow.unplug(true);
}
this.setPreviousStatement(false);
this.setNextStatement(false);
this.setOutput(true, returnTypes);
} else {
if (outputConn && outputConn.isConnected()) {
outputConn.disconnect();
}
if (shape === "statement") {
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setOutput(false);
} else if (shape === "terminal") {
if (nextConn && nextConn.isConnected()) {
nextConn.targetBlock().unplug(true);
}
this.setNextStatement(false);
this.setPreviousStatement(true, "default");
this.setOutput(false);
}
}
if (!this.argTypes_ || this.argTypes_.length === 0) {
this.appendDummyInput("EMPTY").appendField("my block");
return;
}
let firstLabel = this.argTypes_[0] === "label";
for (let i = 0; i < this.argTypes_.length; i++) {
const type = this.argTypes_[i];
const name = this.argNames_[i];
if (!type || !name) continue;
if (type === "label") {
this.appendDummyInput().appendField(name);
continue;
}
if (!firstLabel) {
this.appendDummyInput().appendField("my block with");
firstLabel = true;
}
let input;
const key = type + "_" + name;
if (type === "statement") {
input = this.appendStatementInput(key).setCheck("default");
} else {
input = this.appendValueInput(key).setCheck(typeToBlocklyCheck(type));
}
if (oldConnections[key]) {
try {
input.connection.connect(
oldConnections[key].targetBlock()?.outputConnection ||
oldConnections[key]
);
} catch (e) {}
}
}
},
};
Blockly.Blocks["functions_return"] = {
init() {
this.setStyle("procedure_blocks");
this.appendValueInput("VALUE").appendField("return");
this.setPreviousStatement(true, "default");
this.setNextStatement(false);
this.setInputsInline(true);
},
update_() {
const def = this.getSurroundParent();
if (!def || def.type !== "functions_definition") return;
def.updateReturnState_();
def.workspace.updateAllFunctionCalls();
},
onchange(e) {
if (e.isUiEvent || e.isBlank) return;
this.update_();
},
};
BlocklyJS.javascriptGenerator.forBlock["functions_argument_block"] = block => [
block.argType_ + "_" + block.argName_,
BlocklyJS.Order.NONE,
];
BlocklyJS.javascriptGenerator.forBlock["functions_statement_argument_block"] =
block => "statement_" + block.argName_ + "();\n";
BlocklyJS.javascriptGenerator.forBlock["functions_definition"] = function (
block,
generator
) {
const params = block.argTypes_
.map((type, i) => {
if (type === "label") return null;
return type + "_" + block.argNames_[i];
})
.filter(Boolean);
const body = BlocklyJS.javascriptGenerator.statementToCode(block, "BODY");
return `MyFunctions[${generator.quote_(
block.functionId_
)}] = async (${params.join(", ")}) => {\n${body}};\n`;
};
BlocklyJS.javascriptGenerator.forBlock["functions_call"] = function (
block,
generator
) {
const args = [];
for (let i = 0; i < block.argTypes_.length; i++) {
const type = block.argTypes_[i];
const name = block.argNames_[i];
const key = `${type}_${name}`;
if (type === "label") continue;
if (type === "statement")
args.push(`async () => {${generator.statementToCode(block, key)}}`);
else
args.push(
generator.valueToCode(block, key, BlocklyJS.Order.NONE) || "null"
);
}
return `await MyFunctions[${generator.quote_(block.functionId_)}](${args.join(
", "
)});\n`;
};
BlocklyJS.javascriptGenerator.forBlock["functions_return"] = function (
block,
generator
) {
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.NONE);
return `return ${value || "null"};\n`;
};

512
src/blocks/json.js Normal file
View File

@@ -0,0 +1,512 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
const xmlUtils = Blockly.utils.xml;
Blockly.Blocks["json_get"] = {
init: function () {
this.appendValueInput("KEY").setCheck("String").appendField("value of");
this.appendValueInput("OBJECT").setCheck("Object").appendField("in object");
this.setOutput(true);
this.setInputsInline(true);
this.setStyle("json_category");
this.setTooltip("Returns the value of a key from a JSON object.");
},
};
BlocklyJS.javascriptGenerator.forBlock["json_get"] = function (block) {
const obj =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"OBJECT",
BlocklyJS.Order.MEMBER
) || "{}";
const key =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"KEY",
BlocklyJS.Order.NONE
) || '""';
return [`${obj}[${key}]`, BlocklyJS.Order.MEMBER];
};
Blockly.Blocks["json_set"] = {
init: function () {
this.appendValueInput("OBJECT").setCheck("Object").appendField("in object");
this.appendValueInput("KEY").setCheck("String").appendField("set");
this.appendValueInput("VALUE").appendField("to");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setInputsInline(true);
this.setStyle("json_category");
this.setTooltip("Sets a value to a key in a JSON object.");
},
};
BlocklyJS.javascriptGenerator.forBlock["json_set"] = function (block) {
const obj =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"OBJECT",
BlocklyJS.Order.MEMBER
) || "{}";
const key =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"KEY",
BlocklyJS.Order.NONE
) || '""';
const value =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"VALUE",
BlocklyJS.Order.ASSIGNMENT
) || "null";
return `${obj}[${key}] = ${value};\n`;
};
Blockly.Blocks["json_delete"] = {
init: function () {
this.appendValueInput("OBJECT").setCheck("Object").appendField("in object");
this.appendValueInput("KEY").setCheck("String").appendField("remove");
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setInputsInline(true);
this.setStyle("json_category");
this.setTooltip("Deletes a key from a JSON object.");
},
};
BlocklyJS.javascriptGenerator.forBlock["json_delete"] = function (block) {
const obj =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"OBJECT",
BlocklyJS.Order.MEMBER
) || "{}";
const key =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"KEY",
BlocklyJS.Order.NONE
) || '""';
return `delete ${obj}[${key}];\n`;
};
Blockly.Blocks["json_create_item"] = {
init: function () {
this.appendDummyInput().appendField("key and value");
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setStyle("json_category");
this.setTooltip("Add a key with a value to the object.");
this.contextMenu = false;
},
};
/* --- start deprecated --- */
Blockly.Blocks["json_key_value"] = {
init: function () {
this.appendValueInput("KEY").setCheck("String").appendField("key");
this.appendValueInput("VALUE").setCheck(null).appendField("value");
this.setInputsInline(true);
this.setStyle("json_category");
this.setTooltip("A single key with a value.");
this.setOutput(true, "ObjectItem");
this.setInputsInline(true);
},
};
BlocklyJS.javascriptGenerator.forBlock["json_key_value"] = function (block) {
const keyCode =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"KEY",
BlocklyJS.Order.NONE
) || '""';
const valCode =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"VALUE",
BlocklyJS.Order.ATOMIC
) || "null";
const code = `${keyCode}: ${valCode}`;
return [code, BlocklyJS.Order.ATOMIC];
};
Blockly.Blocks["json_create"] = {
init: function () {
this.setOutput(true, "Object");
this.setStyle("json_category");
this.itemCount_ = 0;
this.updateShape_();
this.setMutator(new Blockly.icons.MutatorIcon(["json_create_item"], this));
this.setTooltip("Create a JSON object with any number of keys.");
this.setInputsInline(false);
},
mutationToDom: function () {
const container = xmlUtils.createElement("mutation");
container.setAttribute("items", String(this.itemCount_));
return container;
},
domToMutation: function (xmlElement) {
const items = xmlElement.getAttribute("items");
if (!items) throw new TypeError("element did not have items");
this.itemCount_ = parseInt(items, 10);
this.updateShape_();
},
saveExtraState: function () {
return {
itemCount: this.itemCount_,
};
},
loadExtraState: function (state) {
this.itemCount_ = state["itemCount"];
this.updateShape_();
},
saveConnections: function (containerBlock) {
let itemBlock = containerBlock.getInputTargetBlock("STACK");
let i = 0;
while (itemBlock) {
const input = this.getInput("ITEM" + i);
itemBlock.valueConnection_ = input && input.connection.targetConnection;
itemBlock = itemBlock.nextConnection?.targetBlock();
i++;
}
},
compose: function (containerBlock) {
let itemBlock = containerBlock.getInputTargetBlock("STACK");
const connections = [];
while (itemBlock) {
if (itemBlock.isInsertionMarker()) {
itemBlock = itemBlock.getNextBlock();
continue;
}
connections.push(itemBlock.valueConnection_);
itemBlock = itemBlock.getNextBlock();
}
for (let i = 0; i < this.itemCount_; i++) {
const connection = this.getInput("ADD" + i)?.connection?.targetConnection;
if (connection && !connections.includes(connection)) {
connection.disconnect();
}
}
this.itemCount_ = connections.length;
this.updateShape_();
for (let i = 0; i < this.itemCount_; i++) {
connections[i]?.reconnect(this, "ADD" + i);
}
},
decompose: function (workspace) {
const containerBlock = workspace.newBlock("json_create_container");
containerBlock.initSvg();
let connection = containerBlock.getInput("STACK").connection;
for (let i = 0; i < this.itemCount_; i++) {
const itemBlock = workspace.newBlock("json_create_item");
itemBlock.initSvg();
if (!itemBlock.previousConnection) {
throw new Error("itemBlock has no previousConnection");
}
connection.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection;
}
return containerBlock;
},
saveConnections: function (containerBlock) {
let itemBlock = containerBlock.getInputTargetBlock("STACK");
let i = 0;
while (itemBlock) {
if (itemBlock.isInsertionMarker()) {
itemBlock = itemBlock.getNextBlock();
continue;
}
const input = this.getInput("ADD" + i);
itemBlock.valueConnection_ = input?.connection?.targetConnection;
itemBlock = itemBlock?.getNextBlock();
i++;
}
},
updateShape_: function () {
if (this.itemCount_ && this.getInput("EMPTY")) {
this.removeInput("EMPTY");
} else if (!this.itemCount_ && !this.getInput("EMPTY")) {
this.appendDummyInput("EMPTY").appendField("create empty object");
}
for (let i = 0; i < this.itemCount_; i++) {
if (!this.getInput("ADD" + i)) {
const input = this.appendValueInput("ADD" + i).setAlign(
Blockly.inputs.Align.RIGHT
);
input.setCheck("ObjectItem");
if (i === 0) input.appendField("create object with");
}
}
for (let i = this.itemCount_; this.getInput("ADD" + i); i++) {
this.removeInput("ADD" + i);
}
},
};
Blockly.Blocks["json_create_container"] = {
init: function () {
this.appendDummyInput().appendField("object");
this.appendStatementInput("STACK");
this.setStyle("json_category");
this.setTooltip(
"Add, remove, or reorder sections to configure this object block."
);
this.contextMenu = false;
},
};
BlocklyJS.javascriptGenerator.forBlock["json_create"] = function (block) {
const entries = [];
for (let i = 0; i < block.itemCount_; i++) {
const pairCode = BlocklyJS.javascriptGenerator.valueToCode(
block,
"ADD" + i,
BlocklyJS.Order.NONE
);
if (pairCode) {
entries.push(pairCode || "");
}
}
const code = `{ ${entries.join(", ")} }`;
return [code, BlocklyJS.Order.ATOMIC];
};
/* --- end deprecated --- */
Blockly.Blocks["json_create_statement"] = {
init: function () {
this.appendDummyInput().appendField("create object");
this.appendStatementInput("STACK").setCheck("json_key_value");
this.setOutput(true, "Object");
this.setStyle("json_category");
this.setTooltip("Create a JSON object using stacked key/value pairs.");
},
};
BlocklyJS.javascriptGenerator.forBlock["json_create_statement"] = function (
block
) {
const statements = BlocklyJS.javascriptGenerator.statementToCode(
block,
"STACK"
);
return [`{\n${statements}}`, BlocklyJS.Order.ATOMIC];
};
Blockly.Blocks["json_key_value_statement"] = {
init: function () {
this.appendValueInput("KEY").setCheck("String").appendField("key");
this.appendValueInput("VALUE").appendField("value");
this.setPreviousStatement(true, "json_key_value");
this.setNextStatement(true, "json_key_value");
this.setInputsInline(true);
this.setStyle("json_category");
this.setTooltip("A single key/value pair for a JSON object.");
},
};
BlocklyJS.javascriptGenerator.forBlock["json_key_value_statement"] = function (
block,
generator
) {
const key =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"KEY",
BlocklyJS.Order.ATOMIC
) || "";
const value =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"VALUE",
BlocklyJS.Order.ATOMIC
) || "null";
if (key) return `${key}: ${value},\n`;
else return "";
};
Blockly.Blocks["json_has_key"] = {
init: function () {
this.appendValueInput("OBJECT")
.setCheck("Object")
.appendField("does object");
this.appendValueInput("KEY").setCheck("String").appendField("have");
this.setOutput(true, "Boolean");
this.setInputsInline(true);
this.setStyle("json_category");
this.setTooltip("Returns true if the key exists in the object.");
},
};
BlocklyJS.javascriptGenerator.forBlock["json_has_key"] = function (block) {
const obj =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"OBJECT",
BlocklyJS.Order.MEMBER
) || "{}";
const key =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"KEY",
BlocklyJS.Order.NONE
) || '""';
return [`(${key} in ${obj})`, BlocklyJS.Order.RELATIONAL];
};
Blockly.Blocks["json_property_list"] = {
init: function () {
this.appendValueInput("OBJECT")
.setCheck("Object")
.appendField(
new Blockly.FieldDropdown([
["keys", "KEYS"],
["values", "VALUES"],
["entries", "ENTRIES"],
]),
"MODE"
)
.appendField("of");
this.setOutput(true, "Array");
this.setStyle("json_category");
this.setTooltip("Gets keys, values, or entries of the JSON object.");
},
};
BlocklyJS.javascriptGenerator.forBlock["json_property_list"] = function (
block
) {
const obj =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"OBJECT",
BlocklyJS.Order.FUNCTION_CALL
) || "{}";
const mode = block.getFieldValue("MODE");
let code;
switch (mode) {
case "KEYS":
code = `Object.keys(${obj})`;
break;
case "VALUES":
code = `Object.values(${obj})`;
break;
case "ENTRIES":
code = `Object.entries(${obj})`;
break;
default:
code = "[]";
}
return [code, BlocklyJS.Order.FUNCTION_CALL];
};
Blockly.Blocks["json_parse"] = {
init: function () {
const dropdown = new Blockly.FieldDropdown([
["object from text", "PARSE"],
["text from object", "STRINGIFY"],
]);
dropdown.setValidator((newMode) => {
this.updateType_(newMode);
});
this.setStyle("json_category");
this.appendValueInput("INPUT")
.setCheck("String")
.appendField("make")
.appendField(dropdown, "MODE");
this.setInputsInline(true);
this.setOutput(true, "Object");
this.setTooltip(() => {
const mode = this.getFieldValue("MODE");
if (mode === "PARSE") {
return "Convert a stringified object into an object.";
} else if (mode === "STRINGIFY") {
return "Convert an object into text representing an object.";
}
throw Error("Unknown mode: " + mode);
});
},
updateType_: function (newMode) {
const mode = this.getFieldValue("MODE");
if (mode !== newMode) {
const inputConnection = this.getInput("INPUT")?.connection;
inputConnection?.setShadowDom(null);
const inputBlock = inputConnection?.targetBlock();
if (inputBlock) {
inputConnection.disconnect();
if (inputBlock.isShadow()) {
inputBlock.dispose(false);
} else {
this.bumpNeighbours();
}
}
}
if (newMode === "PARSE") {
this.outputConnection.setCheck("Object");
this.getInput("INPUT").setCheck("String");
} else {
this.outputConnection.setCheck("String");
this.getInput("INPUT").setCheck("Object");
}
},
mutationToDom: function () {
const container = xmlUtils.createElement("mutation");
container.setAttribute("mode", this.getFieldValue("MODE"));
return container;
},
domToMutation: function (xmlElement) {
this.updateType_(xmlElement.getAttribute("mode"));
},
saveExtraState: function () {
return { mode: this.getFieldValue("MODE") };
},
loadExtraState: function (state) {
this.updateType_(state["mode"]);
},
};
BlocklyJS.javascriptGenerator.forBlock["json_parse"] = function (block) {
const input =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"INPUT",
BlocklyJS.Order.ATOMIC
) || "null";
const mode = block.getFieldValue("MODE");
if (mode === "PARSE") {
return [`JSON.parse(${input})`, BlocklyJS.Order.NONE];
} else if (mode === "STRINGIFY") {
return [`JSON.stringify(${input})`, BlocklyJS.Order.NONE];
}
};
Blockly.Blocks["json_clone"] = {
init: function () {
this.appendValueInput("OBJECT")
.setCheck("Object")
.appendField("clone object");
this.setOutput(true, "Object");
this.setStyle("json_category");
this.setTooltip("Creates a duplicate of a JSON object.");
},
};
BlocklyJS.javascriptGenerator.forBlock["json_clone"] = function (block) {
const obj =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"OBJECT",
BlocklyJS.Order.ATOMIC
) || "{}";
return [`JSON.parse(JSON.stringify(${obj}))`, BlocklyJS.Order.FUNCTION_CALL];
};

454
src/blocks/list.js Normal file
View File

@@ -0,0 +1,454 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
Blockly.Blocks["lists_filter"] = {
init: function () {
this.appendValueInput("list").setCheck("Array").appendField("filter list");
this.appendValueInput("method").setCheck("Boolean").appendField("by");
this.setInputsInline(true);
this.setOutput(true, "Array");
this.setStyle("list_blocks");
this.setTooltip(
"Remove all items in a list which doesn't match the boolean"
);
},
};
BlocklyJS.javascriptGenerator.forBlock["lists_filter"] = function (
block,
generator
) {
var val_list = generator.valueToCode(block, "list", BlocklyJS.Order.ATOMIC);
var val_method = generator.valueToCode(
block,
"method",
BlocklyJS.Order.ATOMIC
);
var code = `${val_list}.filter(findOrFilterItem => ${val_method})`;
return [code, BlocklyJS.Order.NONE];
};
Blockly.Blocks["lists_find"] = {
init: function () {
this.appendValueInput("list").setCheck("Array").appendField("in list");
this.appendValueInput("method")
.setCheck("Boolean")
.appendField("find first that matches");
this.setOutput(true, null);
this.setInputsInline(true);
this.setStyle("list_blocks");
this.setTooltip(
"Returns the first item in a list that matches the boolean"
);
},
};
BlocklyJS.javascriptGenerator.forBlock["lists_find"] = function (
block,
generator
) {
var val_list = generator.valueToCode(block, "list", BlocklyJS.Order.ATOMIC);
var val_method = generator.valueToCode(
block,
"method",
BlocklyJS.Order.ATOMIC
);
var code = `${val_list}.find(findOrFilterItem => ${val_method})`;
return [code, BlocklyJS.Order.NONE];
};
Blockly.Blocks["lists_filter_item"] = {
init: function () {
this.appendDummyInput("name").appendField("item in loop");
this.setInputsInline(true);
this.setOutput(true, null);
this.setStyle("list_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["lists_filter_item"] = () => [
"findOrFilterItem",
BlocklyJS.Order.NONE,
];
Blockly.Blocks["lists_merge"] = {
init: function () {
this.appendValueInput("list").setCheck("Array").appendField("merge list");
this.appendValueInput("list2").setCheck("Array").appendField("with");
this.setInputsInline(true);
this.setOutput(true, "Array");
this.setStyle("list_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["lists_merge"] = function (
block,
generator
) {
const val_list = generator.valueToCode(block, "list", BlocklyJS.Order.ATOMIC);
const val_list2 = generator.valueToCode(
block,
"list2",
BlocklyJS.Order.ATOMIC
);
const code = `${val_list}.concat(${val_list2})`;
return [code, BlocklyJS.Order.NONE];
};
Blockly.Blocks["lists_foreach"] = {
init: function () {
this.appendValueInput("LIST")
.setCheck("Array")
.appendField("for each item in list");
this.appendStatementInput("DO").appendField("do");
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setStyle("list_blocks");
this.setTooltip(
"Loops through every item in a list and runs the code inside for each one"
);
},
};
BlocklyJS.javascriptGenerator.forBlock["lists_foreach"] = function (
block,
generator
) {
const list =
generator.valueToCode(block, "LIST", BlocklyJS.Order.NONE) || "[]";
const branch = generator.statementToCode(block, "DO");
const code = `${list}.forEach(findOrFilterItem => {\n${branch}});\n`;
return code;
};
Blockly.Blocks["lists_getIndex_modified"] = {
init: function () {
const MODE_OPTIONS = [
[Blockly.Msg.LISTS_GET_INDEX_GET, "GET"],
[Blockly.Msg.LISTS_GET_INDEX_REMOVE, "REMOVE"],
];
this.WHERE_OPTIONS = [
[Blockly.Msg.LISTS_GET_INDEX_FROM_START, "FROM_START"],
[Blockly.Msg.LISTS_GET_INDEX_FROM_END, "FROM_END"],
[Blockly.Msg.LISTS_GET_INDEX_FIRST, "FIRST"],
[Blockly.Msg.LISTS_GET_INDEX_LAST, "LAST"],
[Blockly.Msg.LISTS_GET_INDEX_RANDOM, "RANDOM"],
];
this.setHelpUrl(Blockly.Msg.LISTS_GET_INDEX_HELPURL);
this.setStyle("list_blocks");
this.appendValueInput("VALUE")
.setCheck("Array")
.appendField(Blockly.Msg.LISTS_GET_INDEX_INPUT_IN_LIST);
const modeField = new Blockly.FieldDropdown(MODE_OPTIONS, newMode => {
this.updateMode_(newMode);
return newMode;
});
this.appendDummyInput()
.appendField(modeField, "MODE")
.appendField("", "SPACE");
const whereField = new Blockly.FieldDropdown(
this.WHERE_OPTIONS,
newWhere => {
const cur = this.getFieldValue("WHERE");
const newNeedsAt = newWhere === "FROM_START" || newWhere === "FROM_END";
const curNeedsAt = cur === "FROM_START" || cur === "FROM_END";
if (newNeedsAt !== curNeedsAt) this.updateAt_(newNeedsAt);
}
);
this.appendDummyInput().appendField(whereField, "WHERE");
this.appendDummyInput("AT");
this.setInputsInline(true);
this.updateAt_(true);
this.updateMode_(this.getFieldValue("MODE") || "GET");
this.setTooltip(() => {
const mode = this.getFieldValue("MODE");
const where = this.getFieldValue("WHERE");
if (mode === "GET") {
switch (where) {
case "FROM_START":
case "FROM_END":
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FROM;
case "FIRST":
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FIRST;
case "LAST":
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_LAST;
case "RANDOM":
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_RANDOM;
}
} else {
switch (where) {
case "FROM_START":
case "FROM_END":
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM;
case "FIRST":
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST;
case "LAST":
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST;
case "RANDOM":
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM;
}
}
return "";
});
},
mutationToDom: function () {
const container = document.createElement("mutation");
const input = this.getInput("AT");
const isValueInput = !!input && !!input.connection;
container.setAttribute("at", String(isValueInput));
container.setAttribute("mode", this.getFieldValue("MODE") || "GET");
return container;
},
domToMutation: function (xmlElement) {
const at = xmlElement.getAttribute("at") !== "false";
const mode = xmlElement.getAttribute("mode") || "GET";
this.updateAt_(at);
this.updateMode_(mode);
},
updateAt_: function (useValueInput) {
if (this.getInput("AT")) this.removeInput("AT", true);
if (this.getInput("ORDINAL")) this.removeInput("ORDINAL", true);
if (useValueInput) {
this.appendValueInput("AT").setCheck("Number");
if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
this.appendDummyInput("ORDINAL").appendField(
Blockly.Msg.ORDINAL_NUMBER_SUFFIX
);
}
} else {
this.appendDummyInput("AT");
}
},
updateMode_: function (mode) {
if (mode === "GET") {
if (!this.outputConnection) this.setOutput(true);
this.outputConnection.setCheck(null);
} else {
if (!this.outputConnection) this.setOutput(true);
this.outputConnection.setCheck("Array");
}
},
};
BlocklyJS.javascriptGenerator.forBlock["lists_getIndex_modified"] = function (
block,
generator
) {
const workspaceOneBased =
block.workspace.options && block.workspace.options.oneBasedIndex;
const mode = block.getFieldValue("MODE");
const where = block.getFieldValue("WHERE");
const listCode =
generator.valueToCode(block, "VALUE", BlocklyJS.Order.NONE) || "[]";
const atCodeRaw =
generator.valueToCode(block, "AT", BlocklyJS.Order.NONE) || "0";
let i;
switch (where) {
case "FIRST":
i = "0";
break;
case "LAST":
i = "-1";
break;
case "RANDOM":
i = `Math.floor(Math.random() * _.length)`;
break;
case "FROM_START":
i = workspaceOneBased ? `${atCodeRaw} - 1` : atCodeRaw;
break;
case "FROM_END":
i = workspaceOneBased ? `-${atCodeRaw}` : `-(${atCodeRaw} + 1)`;
break;
default:
i = "0";
}
return [
mode === "REMOVE"
? `${listCode}.toSpliced(${i}, 1)`
: `${listCode}.at(${i})`,
BlocklyJS.Order.FUNCTION_CALL,
];
};
Blockly.Blocks["lists_setIndex_modified"] = {
init: function () {
const MODE_OPTIONS = [
[Blockly.Msg.LISTS_SET_INDEX_SET, "SET"],
[Blockly.Msg.LISTS_SET_INDEX_INSERT, "INSERT"],
];
this.WHERE_OPTIONS = [
[Blockly.Msg.LISTS_GET_INDEX_FROM_START, "FROM_START"],
[Blockly.Msg.LISTS_GET_INDEX_FROM_END, "FROM_END"],
[Blockly.Msg.LISTS_GET_INDEX_FIRST, "FIRST"],
[Blockly.Msg.LISTS_GET_INDEX_LAST, "LAST"],
[Blockly.Msg.LISTS_GET_INDEX_RANDOM, "RANDOM"],
];
this.setHelpUrl(Blockly.Msg.LISTS_SET_INDEX_HELPURL);
this.setStyle("list_blocks");
this.appendValueInput("LIST")
.setCheck("Array")
.appendField(Blockly.Msg.LISTS_SET_INDEX_INPUT_IN_LIST);
const modeField = new Blockly.FieldDropdown(MODE_OPTIONS);
this.appendDummyInput()
.appendField(modeField, "MODE")
.appendField("", "SPACE");
const whereField = new Blockly.FieldDropdown(
this.WHERE_OPTIONS,
newWhere => {
this.updateAt_(newWhere === "FROM_START" || newWhere === "FROM_END");
return newWhere;
}
);
this.appendDummyInput().appendField(whereField, "WHERE");
this.appendDummyInput("AT");
this.appendValueInput("TO").appendField(
Blockly.Msg.LISTS_SET_INDEX_INPUT_TO
);
this.setInputsInline(true);
this.setOutput(true, "Array");
this.updateAt_(true);
this.setTooltip(() => {
const mode = this.getFieldValue("MODE");
const where = this.getFieldValue("WHERE");
if (mode === "SET") {
switch (where) {
case "FROM_START":
case "FROM_END":
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FROM;
case "FIRST":
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FIRST;
case "LAST":
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_LAST;
case "RANDOM":
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_RANDOM;
}
} else {
switch (where) {
case "FROM_START":
case "FROM_END":
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FROM;
case "FIRST":
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST;
case "LAST":
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_LAST;
case "RANDOM":
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM;
}
}
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP;
});
},
mutationToDom: function () {
const container = document.createElement("mutation");
const input = this.getInput("AT");
const isValueInput = !!input && !!input.connection;
container.setAttribute("at", String(isValueInput));
return container;
},
domToMutation: function (xmlElement) {
const at = xmlElement.getAttribute("at") !== "false";
this.updateAt_(at);
},
updateAt_: function (useValueInput) {
if (this.getInput("AT")) this.removeInput("AT", true);
if (this.getInput("ORDINAL")) this.removeInput("ORDINAL", true);
if (useValueInput) {
this.appendValueInput("AT").setCheck("Number");
if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
this.appendDummyInput("ORDINAL").appendField(
Blockly.Msg.ORDINAL_NUMBER_SUFFIX
);
}
} else {
this.appendDummyInput("AT");
}
try {
this.moveInputBefore("AT", "TO");
if (this.getInput("ORDINAL")) this.moveInputBefore("ORDINAL", "TO");
} catch (e) {}
},
};
BlocklyJS.javascriptGenerator.forBlock["lists_setIndex_modified"] = function (
block,
generator
) {
const oneBased = block.workspace.options?.oneBasedIndex;
const mode = block.getFieldValue("MODE");
const where = block.getFieldValue("WHERE");
const listCode =
generator.valueToCode(block, "LIST", BlocklyJS.Order.NONE) || "[]";
const valueCode =
generator.valueToCode(block, "TO", BlocklyJS.Order.NONE) || "undefined";
const atCode =
generator.valueToCode(block, "AT", BlocklyJS.Order.NONE) || "0";
let indexExpr;
switch (where) {
case "FIRST":
indexExpr = "0";
break;
case "LAST":
indexExpr = "-1";
break;
case "RANDOM":
indexExpr = "Math.floor(Math.random() * _.length)";
break;
case "FROM_START":
indexExpr = oneBased ? `(${atCode} - 1)` : atCode;
break;
case "FROM_END":
indexExpr = oneBased ? `-${atCode}` : `-(${atCode} + 1)`;
break;
default:
indexExpr = "0";
}
const code = `((a) => {
const _ = [...a];
let i = ${indexExpr};
if (i < 0) i += _.length;
if (i < 0) i = 0;
if (i > _.length) i = _.length;
return ${
mode === "INSERT"
? `_.toSpliced(i, 0, ${valueCode})`
: `i < _.length ? _.toSpliced(i, 1, ${valueCode}) : _`
};
})(${listCode})`;
return [code, BlocklyJS.Order.FUNCTION_CALL];
};

218
src/blocks/looks.js Normal file
View File

@@ -0,0 +1,218 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
function getAvailableCostumes() {
if (window.projectCostumes && window.projectCostumes.length > 0) {
return window.projectCostumes.map(costume => [costume, costume]);
}
return [["default", "default"]];
}
Blockly.Blocks["say_message"] = {
init: function () {
this.appendValueInput("MESSAGE").appendField("say");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setInputsInline(true);
this.setStyle("looks_blocks");
},
};
Blockly.Blocks["say_message_duration"] = {
init: function () {
this.appendValueInput("MESSAGE").appendField("say");
this.appendValueInput("DURATION").setCheck("Number").appendField("for");
this.appendDummyInput().appendField("seconds");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setInputsInline(true);
this.setStyle("looks_blocks");
},
};
Blockly.Blocks["switch_costume"] = {
init: function () {
this.appendDummyInput()
.appendField("switch costume to")
.appendField(new Blockly.FieldDropdown(
function() {
return getAvailableCostumes();
}
), "COSTUME_NAME");
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setStyle("looks_blocks");
},
};
Blockly.Blocks["set_size"] = {
init: function () {
this.appendValueInput("AMOUNT")
.setCheck("Number")
.appendField("set size to");
this.appendDummyInput().appendField("%");
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setStyle("looks_blocks");
},
};
Blockly.Blocks["change_size"] = {
init: function () {
this.appendValueInput("AMOUNT")
.setCheck("Number")
.appendField("change size by");
this.appendDummyInput().appendField("%");
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setStyle("looks_blocks");
},
};
Blockly.Blocks["get_costume_size"] = {
init: function () {
this.appendDummyInput()
.appendField("costume")
.appendField(
new Blockly.FieldDropdown([
["width", "width"],
["height", "height"],
]),
"MENU"
);
this.setOutput(true, "Number");
this.setStyle("looks_blocks");
},
};
Blockly.Blocks["get_sprite_scale"] = {
init: function () {
this.appendDummyInput().appendField("size");
this.setOutput(true, "Number");
this.setStyle("looks_blocks");
},
};
Blockly.Blocks["looks_hide_sprite"] = {
init: function () {
this.appendDummyInput().appendField("hide sprite");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("looks_blocks");
},
};
Blockly.Blocks["looks_show_sprite"] = {
init: function () {
this.appendDummyInput().appendField("show sprite");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("looks_blocks");
},
};
Blockly.Blocks["looks_isVisible"] = {
init: function () {
this.appendDummyInput().appendField("is visible");
this.setOutput(true, "Boolean");
this.setStyle("looks_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["say_message"] = function (
block,
generator
) {
const message =
generator.valueToCode(block, "MESSAGE", BlocklyJS.Order.NONE) || "";
return `sayMessage(${message});\n`;
};
BlocklyJS.javascriptGenerator.forBlock["say_message_duration"] = function (
block,
generator
) {
const message =
generator.valueToCode(block, "MESSAGE", BlocklyJS.Order.NONE) || "";
const duration =
generator.valueToCode(block, "DURATION", BlocklyJS.Order.ATOMIC) || 2;
return `sayMessage(${message}, ${duration});\n`;
};
BlocklyJS.javascriptGenerator.forBlock["switch_costume"] = function (
block,
generator
) {
var costume = generator.valueToCode(block, "COSTUME", BlocklyJS.Order.ATOMIC);
return `switchCostume(${costume});\n`;
};
BlocklyJS.javascriptGenerator.forBlock["switch_costume"] = function (block, generator) {
var costume = "'" + block.getFieldValue("COSTUME_NAME") + "'";
return `switchCostume(${costume});\n`;
};
BlocklyJS.javascriptGenerator.forBlock["set_size"] = function (
block,
generator
) {
const amount =
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 100;
return `setSize(${amount}, false);\n`;
};
BlocklyJS.javascriptGenerator.forBlock["change_size"] = function (
block,
generator
) {
const amount =
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 100;
return `setSize(${amount}, true);\n`;
};
BlocklyJS.javascriptGenerator.forBlock["get_costume_size"] = function (block) {
const menu = block.getFieldValue("MENU");
return [`getCostumeSize("${menu}")`, BlocklyJS.Order.NONE];
};
BlocklyJS.javascriptGenerator.forBlock["get_sprite_scale"] = function () {
return [`getSpriteScale()`, BlocklyJS.Order.NONE];
};
BlocklyJS.javascriptGenerator.forBlock["looks_hide_sprite"] = function () {
return "toggleVisibility(false);\n";
};
BlocklyJS.javascriptGenerator.forBlock["looks_show_sprite"] = function () {
return "toggleVisibility(true);\n";
};
BlocklyJS.javascriptGenerator.forBlock["looks_isVisible"] = () => [
"sprite.visible",
BlocklyJS.Order.NONE,
];
Blockly.Blocks["switch_backdrop"] = {
init: function () {
this.appendDummyInput()
.appendField("switch backdrop to")
.appendField(new Blockly.FieldDropdown(
function() {
if (window.projectBackdrops && window.projectBackdrops.length > 0) {
return window.projectBackdrops.map((backdrop) => [backdrop.name, backdrop.name]);
}
return [["no backdrops", ""]];
}
), "BACKDROP_NAME");
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setStyle("looks_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["switch_backdrop"] = function (block) {
const name = block.getFieldValue("BACKDROP_NAME");
return `setBackdropByName('${name}');\n`;
};

223
src/blocks/motion.js Normal file
View File

@@ -0,0 +1,223 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
Blockly.Blocks["move_steps"] = {
init: function () {
this.appendValueInput("STEPS").setCheck("Number").appendField("step");
this.appendDummyInput().appendField("times");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("motion_blocks");
},
};
Blockly.Blocks["change_position"] = {
init: function () {
this.appendValueInput("AMOUNT")
.setCheck("Number")
.appendField("change")
.appendField(
new Blockly.FieldDropdown([
["x", "x"],
["y", "y"],
]),
"MENU"
)
.appendField("by");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("motion_blocks");
},
};
Blockly.Blocks["set_position"] = {
init: function () {
this.appendValueInput("AMOUNT")
.setCheck("Number")
.appendField("set")
.appendField(
new Blockly.FieldDropdown([
["x", "x"],
["y", "y"],
]),
"MENU"
)
.appendField("to");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("motion_blocks");
},
};
Blockly.Blocks["goto_position"] = {
init: function () {
this.appendValueInput("x").setCheck("Number").appendField("go to x");
this.appendValueInput("y").setCheck("Number").appendField("y");
this.setInputsInline(true);
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("motion_blocks");
},
};
Blockly.Blocks["get_position"] = {
init: function () {
this.appendDummyInput().appendField(
new Blockly.FieldDropdown([
["x", "x"],
["y", "y"],
]),
"MENU"
);
this.setOutput(true, "Number");
this.setStyle("motion_blocks");
},
};
Blockly.Blocks["angle_turn"] = {
init: function () {
this.appendValueInput("AMOUNT")
.setCheck("Number")
.appendField("turn")
.appendField(
new Blockly.FieldDropdown([
[
{
src: "icons/right.svg",
height: 30,
width: 30,
alt: "A circular arrow rotating to the right",
},
"right",
],
[
{
src: "icons/left.svg",
height: 30,
width: 30,
alt: "A circular arrow rotating to the left",
},
"left",
],
]),
"DIRECTION"
);
this.appendDummyInput().appendField("degrees");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("motion_blocks");
},
};
Blockly.Blocks["angle_set"] = {
init: function () {
this.appendValueInput("AMOUNT")
.setCheck("Number")
.appendField("set angle to");
this.appendDummyInput().appendField("degrees");
this.setInputsInline(true);
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("motion_blocks");
},
};
Blockly.Blocks["point_towards"] = {
init: function () {
this.appendValueInput("x")
.setCheck("Number")
.appendField("point towards x");
this.appendValueInput("y").setCheck("Number").appendField("y");
this.setInputsInline(true);
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("motion_blocks");
},
};
Blockly.Blocks["get_angle"] = {
init: function () {
this.appendDummyInput().appendField("angle");
this.setOutput(true, "Number");
this.setStyle("motion_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["move_steps"] = function (
block,
generator
) {
const steps =
generator.valueToCode(block, "STEPS", BlocklyJS.Order.ATOMIC) || 0;
return `moveSteps(${steps});\n`;
};
BlocklyJS.javascriptGenerator.forBlock["change_position"] = function (
block,
generator
) {
const amount =
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0;
const menu = block.getFieldValue("MENU");
if (menu === "y") return `sprite["${menu}"] -= ${amount};\n`;
else return `sprite["${menu}"] += ${amount};\n`;
};
BlocklyJS.javascriptGenerator.forBlock["set_position"] = function (
block,
generator
) {
const amount =
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0;
const menu = block.getFieldValue("MENU");
if (menu === "y") return `sprite["${menu}"] = -${amount};\n`;
else return `sprite["${menu}"] = ${amount};\n`;
};
BlocklyJS.javascriptGenerator.forBlock["goto_position"] = function (
block,
generator
) {
const x = generator.valueToCode(block, "x", BlocklyJS.Order.ATOMIC) || 0;
const y = generator.valueToCode(block, "y", BlocklyJS.Order.ATOMIC) || 0;
return `sprite.x = ${x};\nsprite.y = -${y};\n`;
};
BlocklyJS.javascriptGenerator.forBlock["point_towards"] = function (
block,
generator
) {
const x = generator.valueToCode(block, "x", BlocklyJS.Order.ATOMIC) || 0;
const y = generator.valueToCode(block, "y", BlocklyJS.Order.ATOMIC) || 0;
return `pointsTowards(${x}, ${y});\n`;
};
BlocklyJS.javascriptGenerator.forBlock["get_position"] = function (block) {
const menu = block.getFieldValue("MENU");
return [`sprite["${menu}"]`, BlocklyJS.Order.NONE];
};
BlocklyJS.javascriptGenerator.forBlock["angle_turn"] = function (
block,
generator
) {
const direction = block.getFieldValue("DIRECTION");
let amount =
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0;
if (direction === "left") amount = `-(${amount})`;
return `setAngle(${amount}, true);\n`;
};
BlocklyJS.javascriptGenerator.forBlock["angle_set"] = function (
block,
generator
) {
const amount =
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0;
return `setAngle(${amount}, false);\n`;
};
BlocklyJS.javascriptGenerator.forBlock["get_angle"] = () => [
"sprite.angle",
BlocklyJS.Order.NONE,
];

115
src/blocks/pen.js Normal file
View File

@@ -0,0 +1,115 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
Blockly.Blocks["pen_down"] = {
init: function () {
this.appendDummyInput().appendField("pen down");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#0fbd8c");
this.setTooltip("Put the pen down to draw");
},
};
BlocklyJS.javascriptGenerator.forBlock["pen_down"] = function () {
return "setPenStatus(true);\n";
};
Blockly.Blocks["pen_up"] = {
init: function () {
this.appendDummyInput().appendField("pen up");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#0fbd8c");
this.setTooltip("Lift the pen up");
},
};
BlocklyJS.javascriptGenerator.forBlock["pen_up"] = function () {
return "setPenStatus(false);\n";
};
Blockly.Blocks["set_pen_color"] = {
init: function () {
this.appendDummyInput().appendField("set pen color");
this.appendValueInput("R").setCheck("Number").appendField("R");
this.appendValueInput("G").setCheck("Number").appendField("G");
this.appendValueInput("B").setCheck("Number").appendField("B");
this.setInputsInline(true);
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#0fbd8c");
this.setTooltip("Set the pen color to a RGB value");
},
};
BlocklyJS.javascriptGenerator.forBlock["set_pen_color"] = function (
block,
generator
) {
const r = generator.valueToCode(block, "R", BlocklyJS.Order.ATOMIC) || 0;
const g = generator.valueToCode(block, "G", BlocklyJS.Order.ATOMIC) || 0;
const b = generator.valueToCode(block, "B", BlocklyJS.Order.ATOMIC) || 0;
return `setPenColor(${r}, ${g}, ${b});\n`;
};
Blockly.Blocks["set_pen_color_combined"] = {
init: function () {
this.appendDummyInput("MODE")
.appendField("set pen color to")
.appendField(
new Blockly.FieldDropdown([
["RGB", "RGB"],
["HEX", "HEX"],
]),
"MODE"
);
this.appendValueInput("VALUE").setCheck(["String", "Number"]);
this.setInputsInline(true);
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#0fbd8c");
this.setTooltip("Set the pen color to a RGB or HEX value.");
},
};
BlocklyJS.javascriptGenerator.forBlock["set_pen_color_combined"] = function (
block,
generator
) {
const mode = block.getFieldValue("MODE");
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC);
if (mode === "HEX") return `setPenColorHex(${value});\n`;
else return `setPenColor(${value});\n`;
};
Blockly.Blocks["set_pen_size"] = {
init: function () {
this.appendValueInput("SIZE")
.setCheck("Number")
.appendField("set pen size to");
this.appendDummyInput().appendField("px");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#0fbd8c");
this.setTooltip("Set the pen thickness to a specific value in pixels");
},
};
BlocklyJS.javascriptGenerator.forBlock["set_pen_size"] = function (
block,
generator
) {
const size =
generator.valueToCode(block, "SIZE", BlocklyJS.Order.ATOMIC) || 1;
return `setPenSize("${size}");\n`;
};
Blockly.Blocks["clear_pen"] = {
init: function () {
this.appendDummyInput().appendField("clear pen");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#0fbd8c");
this.setTooltip("Clear all pen drawings");
},
};
BlocklyJS.javascriptGenerator.forBlock["clear_pen"] = () => "clearPen();\n";

353
src/blocks/set.js Normal file
View File

@@ -0,0 +1,353 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
Blockly.Blocks["sets_create_with"] = {
init: function () {
this.setStyle("set_blocks");
this.setHelpUrl("");
this.itemCount_ = 0;
this.updateShape_();
this.setOutput(true, "Set");
this.setMutator(
new Blockly.icons.MutatorIcon(["sets_create_with_item"], this)
);
this.setTooltip("Create a set with any number of elements.");
},
mutationToDom: function () {
const container = Blockly.utils.xml.createElement("mutation");
container.setAttribute("items", String(this.itemCount_));
return container;
},
domToMutation: function (xmlElement) {
const items = xmlElement.getAttribute("items");
this.itemCount_ = items ? parseInt(items, 10) : 0;
this.updateShape_();
},
saveExtraState: function () {
return { itemCount: this.itemCount_ };
},
loadExtraState: function (state) {
if (state && typeof state.itemCount === "number") {
this.itemCount_ = state.itemCount;
} else {
this.itemCount_ = 0;
}
this.updateShape_();
},
decompose: function (workspace) {
const containerBlock = workspace.newBlock("sets_create_with_container");
containerBlock.initSvg();
let connection = containerBlock.getInput("STACK").connection;
for (let i = 0; i < this.itemCount_; i++) {
const itemBlock = workspace.newBlock("sets_create_with_item");
itemBlock.initSvg();
connection.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection;
}
return containerBlock;
},
compose: function (containerBlock) {
let itemBlock = containerBlock.getInputTargetBlock("STACK");
const connections = [];
while (itemBlock) {
if (itemBlock.isInsertionMarker && itemBlock.isInsertionMarker()) {
itemBlock = itemBlock.getNextBlock();
continue;
}
connections.push(itemBlock.valueConnection_ || null);
itemBlock = itemBlock.getNextBlock();
}
for (let i = 0; i < this.itemCount_; i++) {
const input = this.getInput("ADD" + i);
const targetConnection =
input && input.connection && input.connection.targetConnection;
if (targetConnection && !connections.includes(targetConnection)) {
targetConnection.disconnect();
}
}
this.itemCount_ = connections.length;
this.updateShape_();
for (let i = 0; i < this.itemCount_; i++) {
if (connections[i]) {
connections[i].reconnect(this, "ADD" + i);
}
}
},
saveConnections: function (containerBlock) {
let itemBlock = containerBlock.getInputTargetBlock("STACK");
let i = 0;
while (itemBlock) {
if (itemBlock.isInsertionMarker && itemBlock.isInsertionMarker()) {
itemBlock = itemBlock.getNextBlock();
continue;
}
const input = this.getInput("ADD" + i);
itemBlock.valueConnection_ =
input && input.connection && input.connection.targetConnection;
itemBlock = itemBlock.getNextBlock();
i++;
}
},
updateShape_: function () {
if (this.itemCount_ === 0) {
if (!this.getInput("EMPTY")) {
this.appendDummyInput("EMPTY").appendField("create empty set");
}
} else {
if (this.getInput("EMPTY")) {
this.removeInput("EMPTY");
}
}
for (let i = 0; i < this.itemCount_; i++) {
if (!this.getInput("ADD" + i)) {
const input = this.appendValueInput("ADD" + i).setAlign(
Blockly.inputs.Align.RIGHT
);
if (i === 0) {
input.appendField("create set with");
}
}
}
let i = this.itemCount_;
while (this.getInput("ADD" + i)) {
this.removeInput("ADD" + i);
i++;
}
},
};
Blockly.Blocks["sets_create_with_container"] = {
init: function () {
this.setStyle("set_blocks");
this.appendDummyInput().appendField("set");
this.appendStatementInput("STACK");
this.contextMenu = false;
},
};
Blockly.Blocks["sets_create_with_item"] = {
init: function () {
this.setStyle("set_blocks");
this.appendDummyInput().appendField("element");
this.setPreviousStatement(true);
this.setNextStatement(true);
this.contextMenu = false;
/** @type {Blockly.Connection?} */
this.valueConnection_ = null;
},
};
BlocklyJS.javascriptGenerator.forBlock["sets_create_with"] = function (
block,
generator
) {
const elements = [];
for (let i = 0; i < block.itemCount_; i++) {
const code =
generator.valueToCode(block, "ADD" + i, BlocklyJS.Order.NONE) || "null";
elements.push(code);
}
let code;
if (elements.length === 0) {
code = "new Set()";
} else {
code = "new Set([" + elements.join(", ") + "])";
}
return [code, BlocklyJS.Order.NONE];
};
Blockly.Blocks["sets_convert"] = {
init: function () {
const dropdown = new Blockly.FieldDropdown([
["set from list", "SET"],
["list from set", "LIST"],
]);
dropdown.setValidator((newMode) => {
this.updateType_(newMode);
});
this.setStyle("set_blocks");
this.appendValueInput("INPUT")
.setCheck("String")
.appendField("make")
.appendField(dropdown, "MODE");
this.setInputsInline(true);
this.setOutput(true, "Set");
this.setTooltip(() => {
const mode = this.getFieldValue("MODE");
if (mode === "SET") {
return "Convert a list into a set (removes duplicates).";
} else if (mode === "LIST") {
return "Convert a set into a list.";
}
throw Error("Unknown mode: " + mode);
});
},
updateType_: function (newMode) {
const mode = this.getFieldValue("MODE");
if (mode !== newMode) {
const inputConnection = this.getInput("INPUT")?.connection;
inputConnection?.setShadowDom(null);
const inputBlock = inputConnection?.targetBlock();
if (inputBlock) {
inputConnection.disconnect();
if (inputBlock.isShadow()) {
inputBlock.dispose(false);
} else {
this.bumpNeighbours();
}
}
}
if (newMode === "SET") {
this.outputConnection.setCheck("Set");
this.getInput("INPUT").setCheck("Array");
} else {
this.outputConnection.setCheck("Array");
this.getInput("INPUT").setCheck("Set");
}
},
mutationToDom: function () {
const container = Blockly.utils.xml.createElement("mutation");
container.setAttribute("mode", this.getFieldValue("MODE"));
return container;
},
domToMutation: function (xmlElement) {
this.updateType_(xmlElement.getAttribute("mode"));
},
saveExtraState: function () {
return { mode: this.getFieldValue("MODE") };
},
loadExtraState: function (state) {
this.updateType_(state["mode"]);
},
};
BlocklyJS.javascriptGenerator.forBlock["sets_convert"] = function (block) {
const input =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"INPUT",
BlocklyJS.Order.ATOMIC
) || "null";
const mode = block.getFieldValue("MODE");
if (mode === "SET") {
return [`new Set(${input})`, BlocklyJS.Order.NONE];
} else if (mode === "LIST") {
return [`[...${input}]`, BlocklyJS.Order.NONE];
}
};
Blockly.Blocks["sets_add"] = {
init: function () {
this.appendValueInput("SET").setCheck("Set").appendField("in set");
this.appendValueInput("VALUE").setCheck(null).appendField("add");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setInputsInline(true);
this.setStyle("set_blocks");
this.setTooltip("Adds a value to the set.");
},
};
BlocklyJS.javascriptGenerator.forBlock["sets_add"] = function (
block,
generator
) {
const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC);
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC);
return `${set}.add(${value});\n`;
};
Blockly.Blocks["sets_delete"] = {
init: function () {
this.appendValueInput("SET").setCheck("Set").appendField("in set");
this.appendValueInput("VALUE").setCheck(null).appendField("delete");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setInputsInline(true);
this.setStyle("set_blocks");
this.setTooltip("Deletes a value from the set.");
},
};
BlocklyJS.javascriptGenerator.forBlock["sets_delete"] = function (
block,
generator
) {
const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC);
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC);
return `${set}.delete(${value});\n`;
};
Blockly.Blocks["sets_has"] = {
init: function () {
this.appendValueInput("SET").setCheck("Set").appendField("does set");
this.appendValueInput("VALUE").setCheck(null).appendField("have");
this.setOutput(true, "Boolean");
this.setInputsInline(true);
this.setStyle("set_blocks");
this.setTooltip("Returns true if the set contains the value.");
},
};
BlocklyJS.javascriptGenerator.forBlock["sets_has"] = function (
block,
generator
) {
const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC);
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC);
return [`${set}.has(${value})`, BlocklyJS.Order.NONE];
};
Blockly.Blocks["sets_size"] = {
init: function () {
this.appendValueInput("SET").setCheck("Set").appendField("size of set");
this.setOutput(true, "Number");
this.setInputsInline(true);
this.setStyle("set_blocks");
this.setTooltip("Returns how many items are in the set.");
},
};
BlocklyJS.javascriptGenerator.forBlock["sets_size"] = function (
block,
generator
) {
const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC);
return [`${set}.size`, BlocklyJS.Order.NONE];
};
Blockly.Blocks["sets_merge"] = {
init: function () {
this.appendValueInput("SET1").setCheck("Set").appendField("merge set");
this.appendValueInput("SET2").setCheck("Set").appendField("with");
this.setOutput(true, "Set");
this.setInputsInline(true);
this.setStyle("set_blocks");
this.setTooltip("Creates a new set combining all values from two sets");
},
};
BlocklyJS.javascriptGenerator.forBlock["sets_merge"] = function (
block,
generator
) {
const set1 = generator.valueToCode(block, "SET1", BlocklyJS.Order.ATOMIC);
const set2 = generator.valueToCode(block, "SET2", BlocklyJS.Order.ATOMIC);
return [`new Set([...${set1}, ...${set2}])`, BlocklyJS.Order.NONE];
};

137
src/blocks/sound.js Normal file
View File

@@ -0,0 +1,137 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
// Function to get available sounds - you'll need to implement this in your main editor
// This should return an array of sound names from your project
function getAvailableSounds() {
// This needs to be connected to your sound list
// For now, returning a default option
if (window.projectSounds && window.projectSounds.length > 0) {
return window.projectSounds.map(sound => [sound, sound]);
}
return [["no sounds", ""]];
}
Blockly.Blocks["play_sound"] = {
init: function () {
this.appendDummyInput()
.appendField("play sound")
.appendField(new Blockly.FieldDropdown(
function() {
return getAvailableSounds();
}
), "SOUND_NAME");
this.appendDummyInput().appendField(
new Blockly.FieldDropdown([
["until finished", "true"],
["without waiting", "false"],
]),
"wait"
);
this.setColour("#ff66ba");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
},
};
BlocklyJS.javascriptGenerator.forBlock["play_sound"] = function (block, generator) {
var name = "'" + block.getFieldValue("SOUND_NAME") + "'";
var wait = block.getFieldValue("wait");
return `await playSound(${name}, ${wait});\n`;
};
Blockly.Blocks["stop_sound"] = {
init: function () {
this.appendDummyInput()
.appendField("stop sound")
.appendField(new Blockly.FieldDropdown(
function() {
return getAvailableSounds();
}
), "SOUND_NAME");
this.setColour("#ff66ba");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
},
};
BlocklyJS.javascriptGenerator.forBlock["stop_sound"] = function (block, generator) {
var name = "'" + block.getFieldValue("SOUND_NAME") + "'";
return `stopSound(${name});\n`;
};
Blockly.Blocks["stop_all_sounds"] = {
init: function () {
this.appendDummyInput()
.appendField("stop")
.appendField(
new Blockly.FieldDropdown([
["all", "false"],
["my", "true"],
]),
"who"
)
.appendField("sounds");
this.setColour("#ff66ba");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
},
};
BlocklyJS.javascriptGenerator.forBlock["stop_all_sounds"] = function (block) {
var who = block.getFieldValue("who");
var code = `stopAllSounds(${who});\n`;
return code;
};
Blockly.Blocks["set_sound_property"] = {
init: function () {
this.appendValueInput("value")
.setCheck("Number")
.appendField("set")
.appendField(
new Blockly.FieldDropdown([
["volume", "volume"],
["speed", "speed"],
]),
"property"
)
.appendField("to");
this.appendDummyInput().appendField("%");
this.setColour("#ff66ba");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
},
};
BlocklyJS.javascriptGenerator.forBlock["set_sound_property"] = function (
block,
generator
) {
var value = generator.valueToCode(
block,
"value",
BlocklyJS.Order.ATOMIC
);
var property = block.getFieldValue("property");
return `setSoundProperty("${property}", ${value});\n`;
};
Blockly.Blocks["get_sound_property"] = {
init: function () {
this.appendDummyInput().appendField(
new Blockly.FieldDropdown([
["volume", "volume"],
["speed", "speed"],
]),
"property"
);
this.setColour("#ff66ba");
this.setOutput(true, "Number");
},
};
BlocklyJS.javascriptGenerator.forBlock["get_sound_property"] = function (block) {
var property = block.getFieldValue("property");
return [`soundProperties["${property}"]`, BlocklyJS.Order.NONE];
};

137
src/blocks/system.js Normal file
View File

@@ -0,0 +1,137 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
const normalKeys = [
..."abcdefghijklmnopqrstuvwxyz",
..."abcdefghijklmnopqrstuvwxyz0123456789".toUpperCase(),
];
Blockly.Blocks["key_pressed"] = {
init: function () {
this.appendDummyInput()
.appendField("is")
.appendField(
new Blockly.FieldDropdown([
["any", "any"],
["space", " "],
["enter", "Enter"],
["escape", "Escape"],
["up arrow", "ArrowUp"],
["down arrow", "ArrowDown"],
["left arrow", "ArrowLeft"],
["right arrow", "ArrowRight"],
...normalKeys.map((i) => [i, i]),
]),
"KEY"
)
.appendField("key down");
this.setOutput(true, "Boolean");
this.setColour("#5CB1D6");
},
};
Blockly.Blocks["get_mouse_position"] = {
init: function () {
this.appendDummyInput()
.appendField("mouse")
.appendField(
new Blockly.FieldDropdown([
["x", "x"],
["y", "y"],
]),
"MENU"
);
this.setOutput(true, "Number");
this.setColour("#5CB1D6");
},
};
Blockly.Blocks["mouse_button_pressed"] = {
init: function () {
this.appendDummyInput()
.appendField("is")
.appendField(
new Blockly.FieldDropdown([
["left", "0"],
["middle", "1"],
["right", "2"],
["back", "3"],
["forward", "4"],
["any", "any"],
]),
"BUTTON"
)
.appendField("mouse button down");
this.setOutput(true, "Boolean");
this.setColour("#5CB1D6");
},
};
Blockly.Blocks["all_keys_pressed"] = {
init: function () {
this.appendDummyInput().appendField("keys currently down");
this.setOutput(true, "Array");
this.setColour("#5CB1D6");
},
};
Blockly.Blocks["mouse_over"] = {
init: function () {
this.appendDummyInput().appendField("is cursor over me");
this.setOutput(true, "Boolean");
this.setColour("#5CB1D6");
},
};
BlocklyJS.javascriptGenerator.forBlock["key_pressed"] = function (block, generator) {
const key = block.getFieldValue("KEY");
const safeKey = generator.quote_(key);
return [`isKeyPressed(${safeKey})`, BlocklyJS.Order.NONE];
};
BlocklyJS.javascriptGenerator.forBlock["get_mouse_position"] = function (block) {
const menu = block.getFieldValue("MENU");
return [`getMousePosition("${menu}")`, BlocklyJS.Order.NONE];
};
BlocklyJS.javascriptGenerator.forBlock["mouse_button_pressed"] = function (
block,
generator
) {
const button = block.getFieldValue("BUTTON");
const safeButton = generator.quote_(button);
return [`isMouseButtonPressed(${safeButton})`, BlocklyJS.Order.NONE];
};
BlocklyJS.javascriptGenerator.forBlock["all_keys_pressed"] = () => [
"Object.keys(keysPressed).filter(k => keysPressed[k])",
BlocklyJS.Order.NONE,
];
BlocklyJS.javascriptGenerator.forBlock["mouse_over"] = () => [
"isMouseTouchingSprite()",
BlocklyJS.Order.NONE,
];
Blockly.Blocks["window_size"] = {
init: function () {
this.appendDummyInput()
.appendField("window")
.appendField(
new Blockly.FieldDropdown([
["width", "width"],
["height", "height"],
]),
"MENU"
);
this.setOutput(true, "Number");
this.setColour("#5CB1D6");
},
};
BlocklyJS.javascriptGenerator.forBlock["window_size"] = function (block) {
return [
`window.inner${block.getFieldValue("MENU") === "width" ? "Width" : "Height"}`,
BlocklyJS.Order.NONE,
];
};

291
src/blocks/tween.js Normal file
View File

@@ -0,0 +1,291 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
const TweenEasing = {
InLinear: (t) => t,
OutLinear: (t) => t,
InOutLinear: (t) => t,
InSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
OutSine: (t) => Math.sin((t * Math.PI) / 2),
InOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
InQuad: (t) => t * t,
OutQuad: (t) => 1 - (1 - t) * (1 - t),
InOutQuad: (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2),
InCubic: (t) => t * t * t,
OutCubic: (t) => 1 - Math.pow(1 - t, 3),
InOutCubic: (t) =>
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
InQuart: (t) => t * t * t * t,
OutQuart: (t) => 1 - Math.pow(1 - t, 4),
InOutQuart: (t) =>
t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2,
InQuint: (t) => t * t * t * t * t,
OutQuint: (t) => 1 - Math.pow(1 - t, 5),
InOutQuint: (t) =>
t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2,
InExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * t - 10)),
OutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
InOutExpo: (t) => {
if (t === 0) return 0;
if (t === 1) return 1;
return t < 0.5
? Math.pow(2, 20 * t - 10) / 2
: (2 - Math.pow(2, -20 * t + 10)) / 2;
},
InCirc: (t) => 1 - Math.sqrt(1 - Math.pow(t, 2)),
OutCirc: (t) => Math.sqrt(1 - Math.pow(t - 1, 2)),
InOutCirc: (t) =>
t < 0.5
? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2,
InBack: (t) => {
const c1 = 1.70158,
c3 = c1 + 1;
return c3 * t * t * t - c1 * t * t;
},
OutBack: (t) => {
const c1 = 1.70158,
c3 = c1 + 1;
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
},
InOutBack: (t) => {
const c1 = 1.70158,
c2 = c1 * 1.525;
return t < 0.5
? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
: (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (2 * t - 2) + c2) + 2) / 2;
},
InElastic: (t) => {
const c4 = (2 * Math.PI) / 3;
if (t === 0) return 0;
if (t === 1) return 1;
return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
},
OutElastic: (t) => {
const c4 = (2 * Math.PI) / 3;
if (t === 0) return 0;
if (t === 1) return 1;
return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
},
InOutElastic: (t) => {
const c5 = (2 * Math.PI) / 4.5;
if (t === 0) return 0;
if (t === 1) return 1;
return t < 0.5
? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2
: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1;
},
InBounce: (t) => 1 - TweenEasing.OutBounce(1 - t),
OutBounce: (t) => {
const n1 = 7.5625,
d1 = 2.75;
if (t < 1 / d1) {
return n1 * t * t;
} else if (t < 2 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
},
InOutBounce: (t) =>
t < 0.5
? (1 - TweenEasing.OutBounce(1 - 2 * t)) / 2
: (1 + TweenEasing.OutBounce(2 * t - 1)) / 2,
};
Object.defineProperty(window, "TweenEasing", {
value: Object.freeze(TweenEasing),
configurable: false,
writable: false,
enumerable: true
});
Blockly.Blocks["tween_block"] = {
init: function () {
this.appendValueInput("FROM").setCheck("Number").appendField("tween from");
this.appendValueInput("TO").setCheck("Number").appendField("to");
this.appendDummyInput().appendField("in");
this.appendValueInput("DURATION").setCheck("Number");
this.appendDummyInput().appendField("seconds using");
this.appendDummyInput()
.appendField(
new Blockly.FieldDropdown([
["linear", "Linear"],
["sine", "Sine"],
["quadratic", "Quad"],
["cubic", "Cubic"],
["quartic", "Quart"],
["quintic", "Quint"],
["expo", "Expo"],
["circ", "Circ"],
["back", "Back"],
["elastic", "Elastic"],
["bounce", "Bounce"],
]),
"EASING_TYPE"
)
.appendField(
new Blockly.FieldDropdown([
["in", "In"],
["out", "Out"],
["in-out", "InOut"],
]),
"EASING_MODE"
);
this.appendStatementInput("DO").setCheck("default");
this.appendDummyInput()
.setAlign(1)
.appendField(
new Blockly.FieldDropdown([
["wait", "WAIT"],
["don't wait", "DONT_WAIT"],
]),
"WAIT_MODE"
)
.appendField("until finished");
this.setInputsInline(true);
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#32a2c0");
this.setTooltip(
"Tween a value from one number to another over time using easing"
);
},
};
BlocklyJS.javascriptGenerator.forBlock["tween_block"] = function (block, generator) {
const easingType = block.getFieldValue("EASING_TYPE");
const easingMode = block.getFieldValue("EASING_MODE");
const from =
generator.valueToCode(block, "FROM", BlocklyJS.Order.ATOMIC) ||
"0";
const to =
generator.valueToCode(block, "TO", BlocklyJS.Order.ATOMIC) || "0";
const duration =
generator.valueToCode(block, "DURATION", BlocklyJS.Order.ATOMIC) ||
"1";
const waitMode = block.getFieldValue("WAIT_MODE");
let branch = BlocklyJS.javascriptGenerator.statementToCode(block, "DO");
branch = BlocklyJS.javascriptGenerator.addLoopTrap(branch, block);
const code = `await startTween({
from: ${from},
to: ${to},
duration: ${duration},
easing: "${easingMode + easingType}",
wait: ${waitMode === "WAIT"},
onUpdate: async (tweenValue) => {
${branch} }
});\n`;
return code;
};
Blockly.Blocks["tween_block_value"] = {
init: function () {
this.appendDummyInput("name").appendField("current tween value");
this.setInputsInline(true);
this.setColour("#32a2c0");
this.setOutput(true, "Number");
},
};
BlocklyJS.javascriptGenerator.forBlock["tween_block_value"] = () => [
"tweenValue",
BlocklyJS.Order.NONE,
];
Blockly.Blocks["tween_sprite_property"] = {
init: function () {
this.appendValueInput("TO")
.setCheck("Number")
.appendField("tween")
.appendField(
new Blockly.FieldDropdown([
["x position", "x"],
["y position", "y"],
["angle", "angle"],
["size", "size"],
]),
"PROPERTY"
)
.appendField("to");
this.appendValueInput("DURATION").setCheck("Number").appendField("in");
this.appendDummyInput()
.appendField("seconds using")
.appendField(
new Blockly.FieldDropdown([
["linear", "Linear"],
["sine", "Sine"],
["quadratic", "Quad"],
["cubic", "Cubic"],
["quartic", "Quart"],
["quintic", "Quint"],
["expo", "Expo"],
["circ", "Circ"],
["back", "Back"],
["elastic", "Elastic"],
["bounce", "Bounce"],
]),
"EASING_TYPE"
)
.appendField(
new Blockly.FieldDropdown([
["in", "In"],
["out", "Out"],
["in-out", "InOut"],
]),
"EASING_MODE"
);
this.setInputsInline(true);
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#32a2c0");
this.setTooltip(
"Tween a sprite property to a target value over time using easing"
);
},
};
BlocklyJS.javascriptGenerator.forBlock["tween_sprite_property"] = function (
block,
generator
) {
const prop = block.getFieldValue("PROPERTY");
const to =
generator.valueToCode(block, "TO", BlocklyJS.Order.ATOMIC) || "0";
const duration =
generator.valueToCode(block, "DURATION", BlocklyJS.Order.ATOMIC) ||
"1";
const easingType = block.getFieldValue("EASING_TYPE");
const easingMode = block.getFieldValue("EASING_MODE");
let fromGetter, setter;
if (prop === "size") {
fromGetter = "getSpriteScale()";
setter = `setSize(tweenValue, false)`;
} else if (prop === "angle") {
fromGetter = `sprite.angle`;
setter = `setAngle(tweenValue, false)`;
} else {
fromGetter = `sprite["${prop}"]`;
setter = `sprite["${prop}"] = tweenValue`;
}
setter = generator.addLoopTrap(setter, block);
const code = `await startTween({
from: ${fromGetter},
to: ${to},
duration: ${duration},
easing: "${easingMode + easingType}",
onUpdate: async (tweenValue) => {
${setter};
}
});\n`;
return code;
};

85
src/blocks/variable.js Normal file
View File

@@ -0,0 +1,85 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
import { projectVariables } from "../scripts/editor";
function getVariables() {
if (Object.keys(projectVariables).length === 0)
return [["unknown", "unknown"]];
else return Object.keys(projectVariables).map((name) => [name, name]);
}
Blockly.Blocks["get_global_var"] = {
init: function () {
this.appendDummyInput().appendField(
new Blockly.FieldDropdown(() => getVariables()),
"VAR"
);
this.setOutput(true);
this.setTooltip("Get a global variable");
this.setStyle("variable_blocks");
this.customContextMenu = function (options) {
const varName = this.getFieldValue("VAR");
options.push({
text: `Delete "${varName}" variable`,
enabled: true,
callback: () => {
delete projectVariables[varName];
this.workspace.refreshToolboxSelection();
},
});
};
},
};
Blockly.Blocks["set_global_var"] = {
init: function () {
this.appendValueInput("VALUE")
.setCheck(null)
.appendField("set")
.appendField(new Blockly.FieldDropdown(() => getVariables()), "VAR")
.appendField("to");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("variable_blocks");
},
};
Blockly.Blocks["change_global_var"] = {
init: function () {
this.appendValueInput("VALUE")
.setCheck("Number")
.appendField("change")
.appendField(new Blockly.FieldDropdown(() => getVariables()), "VAR")
.appendField("by");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("variable_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["get_global_var"] = function (block) {
const name = block.getFieldValue("VAR");
return [`projectVariables["${name}"]`, BlocklyJS.Order.ATOMIC];
};
BlocklyJS.javascriptGenerator.forBlock["set_global_var"] = function (block) {
const name = block.getFieldValue("VAR");
const value =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"VALUE",
BlocklyJS.Order.ASSIGNMENT
) || "0";
return `projectVariables["${name}"] = ${value};\n`;
};
BlocklyJS.javascriptGenerator.forBlock["change_global_var"] = function (block) {
const name = block.getFieldValue("VAR");
const value =
BlocklyJS.javascriptGenerator.valueToCode(
block,
"VALUE",
BlocklyJS.Order.ATOMIC
) || "0";
return `projectVariables["${name}"] += ${value};\n`;
};