pxt-arcade
Version:
Small arcade editor for MakeCode
445 lines (440 loc) • 24 kB
JavaScript
/// <reference path="../node_modules/pxt-core/localtypings/pxteditor.d.ts" />
var pxt;
(function (pxt) {
var editor;
(function (editor) {
function patchBlocks(pkgTargetVersion, dom) {
// Perform the following upgrades for sprite event blocks:
// - Change variables_get_reporter shadows into argument_reporter_custom shadows for sprite
// event blocks
// - Delete variables_get blocks that are connected to a shadow on a sprite event block
// - If a variables_get block inside an event handler has the same name as an event handler
// argument name, change the variables_get block to an argument_reporter_custom block
/*
Old event blocks (variables_get_reporter):
<block type="spritesoverlap">
<value name="HANDLER_DRAG_PARAM_sprite">
<shadow type="variables_get_reporter">
<field name="VAR">sprite</field>
</shadow>
<block type="variables_get">
<field name="VAR">myVariable</field>
</block>
</value>
...
<value name="HANDLER_DRAG_PARAM_otherSprite">
<shadow type="variables_get_reporter">
<field name="VAR">otherSprite</field>
</shadow>
</value>
...
<statement name="HANDLER">
<block type="spritesetpos">
<value name="sprite">
<block type="variables_get">
<field name="VAR">myVariable</field>
</block>
</value>
...
</block>
</statement>
</block>
New event blocks (argument_reporter_custom):
<block type="spritesoverlap" x="490" y="470">
<value name="HANDLER_DRAG_PARAM_sprite">
<shadow type="argument_reporter_custom">
<mutation typename="Sprite"></mutation>
<field name="VALUE">sprite</field>
</shadow>
</value>
...
<value name="HANDLER_DRAG_PARAM_otherSprite">
<shadow type="argument_reporter_custom">
<mutation typename="Sprite"></mutation>
<field name="VALUE">otherSprite</field>
</shadow>
</value>
...
<statement name="HANDLER">
<block type="spritesetpos">
<value name="sprite">
<block type="argument_reporter_custom">
<mutation typename="Sprite"></mutation>
<field name="VALUE">sprite</field>
</block>
</value>
...
</block>
</statement>
</block>
*/
const allEventNodes = pxt.U.toArray(dom.querySelectorAll("block[type=spritesoverlap]"))
.concat(pxt.U.toArray(dom.querySelectorAll("block[type=spritesoncreated]")))
.concat(pxt.U.toArray(dom.querySelectorAll("block[type=spritesondestroyed]")))
.concat(pxt.U.toArray(dom.querySelectorAll("block[type=spritesollisions]")));
allEventNodes.forEach(node => {
// Don't rewrite if already upgraded, i.e. if there are argument_reporter_custom
// shadows already present
if (node.querySelectorAll("shadow[type=argument_reporter_custom]").length > 0) {
return;
}
const paramValues = pxt.U.toArray(node.children).filter(child => {
return child.tagName == "value" && child.getAttribute("name").indexOf("HANDLER_DRAG_PARAM_") !== -1;
});
const statementsRoot = node.querySelector("statement[name=HANDLER]");
const usedVariables = pxt.U.toArray(statementsRoot.querySelectorAll("block[type=variables_get]"));
paramValues.forEach(value => {
let oldVariableName = "";
const handlerVarShadow = getShadow(value, "variables_get_reporter");
if (!handlerVarShadow) {
return;
}
const connectedVarBlock = getChildBlock(value, "variables_get");
if (connectedVarBlock) {
// A variable is connected to the shadow variable reporter; use the name for
// the argument reporter and delete the variable
const connectedVarField = getField(connectedVarBlock, "VAR");
oldVariableName = connectedVarField.textContent;
value.removeChild(connectedVarBlock);
}
const handlerVarField = getField(handlerVarShadow, "VAR");
const argReporterName = handlerVarField.textContent;
oldVariableName = oldVariableName || argReporterName;
changeVariableToSpriteReporter(handlerVarShadow, argReporterName);
// Change all references to this variable inside the handler to argument reporters
usedVariables.forEach(usedVarBlock => {
const usedVarField = getField(usedVarBlock, "VAR");
if (usedVarField && usedVarField.textContent === oldVariableName) {
// This variable is a reference to a handler parameter; change it to an
// argument reporter
changeVariableToSpriteReporter(usedVarBlock, argReporterName);
}
});
});
});
/**
* Upgrade for scene.setTile() which went from being expandable to not
*/
pxt.U.toArray(dom.querySelectorAll("block[type=gamesettile]")).forEach(block => {
const mutation = getMutation(block);
if (!mutation)
return; // Already upgraded
const expanded = mutation.getAttribute("_expanded") !== "0";
block.removeChild(mutation);
if (expanded) {
// The value input must already be in the XML, so no changes needed
return;
}
else {
// There might be a value input present, but we should remove it
// and replace it with the default to replicate the unexpanded behavior
const value = getChildNode(block, "value", "name", "wall");
if (value) {
block.removeChild(value);
}
const newValue = replaceToggle("wall", "toggleOnOff", "on", "false");
block.appendChild(newValue);
}
});
/**
* Upgrade for game.over() which went from being expandable twice to being expandable once
*/
if (pxt.semver.strcmp(pkgTargetVersion || "0.0.0", "0.10.0") < 0) {
pxt.U.toArray(dom.querySelectorAll("block[type=gameOver]")).forEach(block => {
const mutation = getMutation(block);
const value = getChildNode(block, "value", "name", "win");
const expansion = mutation.getAttribute("_expanded");
if (expansion !== "0") {
// Decrement expansion level, as win is now required
mutation.setAttribute("_expanded", (Number(expansion) - 1) + "");
}
else {
// Remove old value to replace it default to maintain current behavior
if (value) {
block.removeChild(value);
}
const newValue = replaceToggle("win", "toggleWinLose", "win", "false");
block.appendChild(newValue);
}
});
}
/**
* Upgrade for enum SpriteKind -> SpriteKindLegacy
*/
if (pxt.semver.strcmp(pkgTargetVersion || "0.0.0", "0.11.20") < 0) {
/**
* Sometimes the getters for these omit the enum member's # improperly,
* so we need to map those numbers to the new values.
* e.g.
* bad:
<value name="kind">
<shadow type="spritekind">
<field name="MEMBER">Cow</field>
</shadow>
<block type="spritetype">
<field name="MEMBER">Player</field>
</block>
</value>
*
* good:
<value name="kind">
<shadow type="spritekind">
<field name="MEMBER">7Cow</field>
</shadow>
<block type="spritetype">
<field name="MEMBER">1Player</field>
</block>
</value>
*/
const legacyKindConversions = {};
pxt.U.toArray(dom.querySelectorAll("variable[type=SpriteKind]")).forEach(block => {
block.setAttribute("type", "SpriteKindLegacy");
const kindValue = (block.textContent || "").trim();
const withoutNum = /[0-9]*([^0-9].*)/.exec(kindValue);
if (withoutNum) {
legacyKindConversions[withoutNum[1]] = kindValue;
}
});
pxt.U.toArray(dom.querySelectorAll("shadow[type=spritetype], block[type=spritetype]")).forEach(block => {
const memberField = getField(block, "MEMBER");
const cont = ((memberField === null || memberField === void 0 ? void 0 : memberField.textContent) || "").trim();
if (legacyKindConversions[cont]) {
memberField.textContent = legacyKindConversions[cont];
}
});
}
if (pxt.semver.strcmp(pkgTargetVersion || "0.0.0", "0.18.9") < 0) {
/**
* Add draggable param for tile that was hit as child of sprite hit wall block
<value name="HANDLER_DRAG_PARAM_location">
<shadow type="argument_reporter_custom">
<mutation typename="tiles.Location"/>
<field name="VALUE">location</field>
</shadow>
</value>
*/
pxt.U.toArray(dom.querySelectorAll("block[type=spriteshitwall]")).forEach(block => {
const doc = block.ownerDocument;
const tileHitParam = doc.createElement("value");
tileHitParam.setAttribute("name", "HANDLER_DRAG_PARAM_location");
const shadow = doc.createElement("shadow");
shadow.setAttribute("type", "argument_reporter_custom");
const mut = doc.createElement("mutation");
mut.setAttribute("typename", "tiles.Location");
const field = doc.createElement("field");
field.setAttribute("name", "VALUE");
field.textContent = "location";
shadow.appendChild(mut);
shadow.appendChild(field);
tileHitParam.appendChild(shadow);
block.appendChild(tileHitParam);
});
/**
* move from tilemap namespace to tiles namespace
* <block type="tilemap_locationXY">
<field name="xy">tilemap.XY.column</field>
<value name="location">
<block type="variables_get">
<field name="VAR" id="L%xa3_Yy]Kq+]Q|yE{Fv">location</field>
</block>
</value>
</block>
*/
pxt.U.toArray(dom.querySelectorAll("block[type=tilemap_locationXY]")).forEach(block => {
const xyField = getField(block, "xy");
xyField.textContent = (xyField.textContent || "").replace(/^tilemap./, "tiles.");
});
}
if (pxt.semver.strcmp(pkgTargetVersion || "0.0.0", "1.12.34") < 0) {
const lang = pxt.BrowserUtils.getCookieLang();
if (lang == "es-MX") {
// on player 2 a button pressed
pxt.U.toArray(dom.querySelectorAll("block[type=ctrlonbuttonevent]"))
.forEach(eventRoot => {
swapFieldIfNotMatching(eventRoot, "button", "controller", "ControllerButton.");
});
}
else if (lang === "es-ES") {
pxt.U.toArray(dom.querySelectorAll("[type=music_sounds]>field[name=note]"))
.forEach(node => node.setAttribute("name", "name"));
// on a button pressed
pxt.U.toArray(dom.querySelectorAll("block[type=keyonevent]"))
.forEach(eventRoot => {
swapFieldIfNotMatching(eventRoot, "event", "button", "ControllerButtonEvent.");
});
// on player 2 a button pressed
pxt.U.toArray(dom.querySelectorAll("block[type=ctrlonbuttonevent]"))
.forEach(eventRoot => {
swapFieldIfNotMatching(eventRoot, "button", "controller", "ControllerButton.");
});
}
else if (lang === "de") {
pxt.U.toArray(dom.querySelectorAll("block[type=image_create]>value[name=heNacht]"))
.forEach(node => node.setAttribute("name", "height"));
}
}
if (pxt.semver.strcmp(pkgTargetVersion || "0.0.0", "1.0.0") < 0) {
// At some point Sprite.z switched from being a getter/setter to a property
pxt.U.toArray(dom.querySelectorAll("block[type=Sprite_blockCombine_set]>field[name=property]"))
.forEach(node => {
if (node.textContent.trim() === "Sprite.z@set") {
node.textContent = "Sprite.z";
}
});
const performOldEnumShimUpgrade = (variableType, blockType) => {
const variableKinds = pxt.U.toArray(dom.querySelectorAll(`variable[type=${variableType}]`));
if (variableKinds.length) {
pxt.U.toArray(dom.querySelectorAll(`block[type=${blockType}]>field[name=MEMBER]`))
.concat(pxt.U.toArray(dom.querySelectorAll(`shadow[type=${blockType}]>field[name=MEMBER]`)))
.forEach(node => {
const value = node.textContent;
if (!/^\d/.test(value)) {
// The correct numerical value will be in the variables
for (const kind of variableKinds) {
const match = /^\d+(.*)/.exec(kind.textContent);
if ((match === null || match === void 0 ? void 0 : match[1]) === value) {
node.textContent = kind.textContent;
break;
}
}
}
});
}
};
// The kind field used by the legacy animation editor switched to including the numerical
// enum value in the field (e.g. Walking -> 0Walking)
performOldEnumShimUpgrade("ActionKind", "action_enum_shim");
// same for SpriteKindLegacy
performOldEnumShimUpgrade("SpriteKindLegacy", "spritetype");
}
// Added the "use on-screen keyboard" options to the ask for number and ask for string blocks
if (pxt.semver.strcmp(pkgTargetVersion || "0.0.0", "2.0.35") < 0) {
const allPromptBlocks = pxt.U.toArray(dom.querySelectorAll("block[type=gameaskforstring]"))
.concat(pxt.U.toArray(dom.querySelectorAll("shadow[type=gameaskforstring]")))
.concat(pxt.U.toArray(dom.querySelectorAll("block[type=gameaskfornumber]")))
.concat(pxt.U.toArray(dom.querySelectorAll("shadow[type=gameaskfornumber]")));
for (const block of allPromptBlocks) {
if (getChildNode(block, "value", "name", "useOnScreenKeyboard")) {
continue;
}
else {
// preserve the old behavior by setting the value to true if not present
const value = document.createElement("value");
value.setAttribute("name", "useOnScreenKeyboard");
const shadow = document.createElement("shadow");
shadow.setAttribute("type", "logic_boolean");
const field = document.createElement("field");
field.setAttribute("name", "BOOL");
field.textContent = "FALSE";
shadow.appendChild(field);
value.appendChild(shadow);
block.appendChild(value);
}
// since we are going to expand the block, we also need to make sure that
// the other expandable parameter answerLength is present. the default values
// and min/max were taken from the source in pxt-common-packages prior to this change
const isStringBlock = block.getAttribute("type") === "gameaskforstring";
if (!getChildNode(block, "value", "name", "answerLength")) {
const value = document.createElement("value");
value.setAttribute("name", "answerLength");
const shadow = document.createElement("shadow");
shadow.setAttribute("type", "math_number_minmax");
const mutation = document.createElement("mutation");
mutation.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
mutation.setAttribute("min", "1");
mutation.setAttribute("max", isStringBlock ? "24" : "10");
mutation.setAttribute("label", "AnswerLength");
mutation.setAttribute("prevision", "0");
const field = document.createElement("field");
field.setAttribute("name", "SLIDER");
field.textContent = isStringBlock ? "12" : "6";
shadow.appendChild(field);
shadow.appendChild(mutation);
value.appendChild(shadow);
block.appendChild(value);
}
}
}
// fixed a typing issue where the browserEvents_mouseButton_pauseUntil was using the wrong enum
if (pxt.semver.strcmp(pkgTargetVersion || "0.0.0", "2.0.41") < 0) {
for (const block of pxt.U.toArray(dom.querySelectorAll("block[type=browserEvents_mouseButton_pauseUntil]"))) {
const field = getChildNode(block, "field", "name", "event");
if (!field)
continue;
if (field.textContent.startsWith("browserEvents.KeyEvent")) {
field.textContent = field.textContent.replace("KeyEvent", "MouseButtonEvent");
}
}
}
}
function swapFieldIfNotMatching(eventRoot, fieldAName, fieldBName, fieldAShouldStartWith) {
const fieldA = eventRoot.querySelector(`field[name=${fieldAName}]`);
const fieldB = eventRoot.querySelector(`field[name=${fieldBName}]`);
if (!fieldB || !fieldA)
return;
if (!fieldA.innerHTML.startsWith(fieldAShouldStartWith)
&& fieldB.innerHTML.startsWith(fieldAShouldStartWith)) {
// swapped by invalid translation we now catch; swap back
fieldA.setAttribute("name", fieldBName);
fieldB.setAttribute("name", fieldAName);
}
}
function changeVariableToSpriteReporter(varBlockOrShadow, reporterName) {
const varField = getField(varBlockOrShadow, "VAR");
varBlockOrShadow.setAttribute("type", "argument_reporter_custom");
varField.setAttribute("name", "VALUE");
varField.textContent = reporterName;
varField.removeAttribute("variabletype");
varField.removeAttribute("id");
const mutation = varBlockOrShadow.ownerDocument.createElement("mutation");
mutation.setAttribute("typename", "Sprite");
varBlockOrShadow.insertBefore(mutation, varBlockOrShadow.firstChild);
}
function getField(parent, name) {
return getChildNode(parent, "field", "name", name);
}
function getShadow(parent, type) {
return getChildNode(parent, "shadow", "type", type);
}
function getChildBlock(parent, type) {
return getChildNode(parent, "block", "type", type);
}
function getChildNode(parent, nodeType, idAttribute, idValue) {
for (let i = 0; i < parent.children.length; i++) {
const child = parent.children.item(i);
if (child.tagName === nodeType && child.getAttribute(idAttribute) === idValue) {
return child;
}
}
return undefined;
}
function getMutation(parent) {
for (let i = 0; i < parent.children.length; i++) {
const child = parent.children.item(i);
if (child.tagName === "mutation") {
return child;
}
}
return undefined;
}
editor.initExtensionsAsync = function (opts) {
pxt.debug('loading arcade target extensions...');
const res = {
blocklyPatch: patchBlocks
};
return Promise.resolve(res);
};
function replaceToggle(valueName, shadowType, fieldName, fieldValue) {
const newValue = document.createElement("value");
newValue.setAttribute("name", valueName);
const shadow = document.createElement("shadow");
shadow.setAttribute("type", shadowType);
const field = document.createElement("field");
field.setAttribute("name", fieldName);
field.textContent = fieldValue;
shadow.appendChild(field);
newValue.appendChild(shadow);
return newValue;
}
})(editor = pxt.editor || (pxt.editor = {}));
})(pxt || (pxt = {}));