Spaces:
Build error
Build error
const formatMessage = require('format-message'); | |
const BlockType = require('../../extension-support/block-type'); | |
const ArgumentType = require('../../extension-support/argument-type'); | |
const BufferUtil = new (require('../../util/array buffer')); | |
const Cast = require('../../util/cast'); | |
const Color = require('../../util/color'); | |
// ShovelUtils | |
let fps = 0; | |
/** | |
* Class for Runtime blocks | |
* @constructor | |
*/ | |
class JgRuntimeBlocks { | |
constructor(runtime) { | |
/** | |
* The runtime instantiating this block package. | |
* @type {Runtime} | |
*/ | |
this.runtime = runtime; | |
// SharkPool | |
this.pausedScripts = Object.create(null); | |
// ShovelUtils | |
// Based on from https://www.growingwiththeweb.com/2017/12/fast-simple-js-fps-counter.html | |
const times = []; | |
fps = this.runtime.frameLoop.framerate; | |
this.runtime.on('RUNTIME_STEP_START', () => { | |
const now = performance.now(); | |
while (times.length > 0 && times[0] <= now - 1000) { times.shift() } | |
times.push(now); | |
fps = times.length; | |
}); | |
this.runtime.on('PROJECT_STOP_ALL', () => { this.pausedScripts = Object.create(null) }); | |
} | |
_typeIsBitmap(type) { | |
return ( | |
type === 'image/png' || type === 'image/bmp' || type === 'image/jpg' || type === 'image/jpeg' || | |
type === 'image/jfif' || type === 'image/webp' || type === 'image/gif' | |
); | |
} | |
/** | |
* @returns {object} metadata for this extension and its blocks. | |
*/ | |
getInfo() { | |
return { | |
id: 'jgRuntime', | |
name: 'Runtime', | |
color1: '#777777', | |
color2: '#6a6a6a', | |
blocks: [ | |
{ | |
opcode: 'addSpriteUrl', | |
text: 'add sprite from [URL]', | |
blockType: BlockType.COMMAND, | |
arguments: { | |
URL: { | |
type: ArgumentType.STRING, | |
defaultValue: `https://corsproxy.io/?${encodeURIComponent('https://penguinmod.com/Sprite1.pms')}` | |
} | |
} | |
}, | |
{ | |
opcode: 'addCostumeUrl', | |
text: 'add costume [name] from [URL]', | |
blockType: BlockType.COMMAND, | |
arguments: { | |
URL: { | |
type: ArgumentType.STRING, | |
defaultValue: `https://corsproxy.io/?${encodeURIComponent('https://penguinmod.com/navicon.png')}` | |
}, | |
name: { | |
type: ArgumentType.STRING, | |
defaultValue: 'penguinmod' | |
} | |
} | |
}, | |
{ | |
opcode: 'addCostumeUrlForceMime', | |
text: 'add [costtype] costume [name] from [URL]', | |
blockType: BlockType.COMMAND, | |
arguments: { | |
costtype: { | |
type: ArgumentType.STRING, | |
menu: "costumeMimeType" | |
}, | |
URL: { | |
type: ArgumentType.STRING, | |
defaultValue: `https://corsproxy.io/?${encodeURIComponent('https://penguinmod.com/navicon.png')}` | |
}, | |
name: { | |
type: ArgumentType.STRING, | |
defaultValue: 'penguinmod' | |
} | |
} | |
}, | |
{ | |
opcode: 'addSoundUrl', | |
text: 'add sound [NAME] from [URL]', | |
blockType: BlockType.COMMAND, | |
arguments: { | |
URL: { | |
type: ArgumentType.STRING, | |
defaultValue: 'https://extensions.turbowarp.org/meow.mp3' | |
}, | |
NAME: { | |
type: ArgumentType.STRING, | |
defaultValue: 'Meow' | |
} | |
} | |
}, | |
{ | |
opcode: 'loadProjectDataUrl', | |
text: 'load project from [URL]', | |
blockType: BlockType.COMMAND, | |
arguments: { | |
URL: { | |
type: ArgumentType.STRING, | |
defaultValue: '' | |
} | |
} | |
}, | |
{ | |
opcode: 'getIndexOfCostume', | |
text: 'get costume index of [costume]', | |
blockType: BlockType.REPORTER, | |
arguments: { | |
costume: { | |
type: ArgumentType.STRING, | |
defaultValue: "costume1" | |
} | |
} | |
}, | |
{ | |
opcode: 'getIndexOfSound', | |
text: 'get sound index of [NAME]', | |
blockType: BlockType.REPORTER, | |
arguments: { | |
NAME: { | |
type: ArgumentType.STRING, | |
defaultValue: "Pop" | |
} | |
} | |
}, | |
{ | |
opcode: 'getProjectDataUrl', | |
text: 'get data url of project', | |
blockType: BlockType.REPORTER, | |
disableMonitor: true | |
}, | |
'---', | |
{ | |
opcode: 'setStageSize', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.setStageSize', | |
default: 'set stage width: [WIDTH] height: [HEIGHT]', | |
description: 'Sets the width and height of the stage.' | |
}), | |
blockType: BlockType.COMMAND, | |
arguments: { | |
WIDTH: { | |
type: ArgumentType.NUMBER, | |
defaultValue: 480 | |
}, | |
HEIGHT: { | |
type: ArgumentType.NUMBER, | |
defaultValue: 360 | |
} | |
} | |
}, | |
{ | |
opcode: 'getStageWidth', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.getStageWidth', | |
default: 'stage width', | |
description: 'Block that returns the width of the stage.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.REPORTER | |
}, | |
{ | |
opcode: 'getStageHeight', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.getStageHeight', | |
default: 'stage height', | |
description: 'Block that returns the height of the stage.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.REPORTER | |
}, | |
'---', | |
{ | |
opcode: 'updateRuntimeConfig', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.updateRuntimeConfig', | |
default: 'set [OPTION] to [ENABLED]', | |
description: 'Block that enables or disables configuration on the runtime like high quality pen or turbo mode.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.COMMAND, | |
arguments: { | |
OPTION: { | |
menu: 'runtimeConfig' | |
}, | |
ENABLED: { | |
menu: 'onoff' | |
} | |
} | |
}, | |
{ | |
opcode: 'changeRenderingCapping', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.changeRenderingCapping', | |
default: 'change render setting [OPTION] to [CAPPED]', | |
description: 'Block that updates configuration on the renderer like resolution for certain content.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.COMMAND, | |
arguments: { | |
OPTION: { | |
menu: 'renderConfigCappable' | |
}, | |
CAPPED: { | |
menu: 'cappableSettings' | |
} | |
} | |
}, | |
{ | |
opcode: 'setRenderingNumber', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.setRenderingNumber', | |
default: 'set render setting [OPTION] to [NUM]', | |
description: 'Block that sets configuration on the renderer like resolution for certain content.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.COMMAND, | |
arguments: { | |
OPTION: { | |
menu: 'renderConfigNumber' | |
}, | |
NUM: { | |
type: ArgumentType.NUMBER, | |
defaultValue: 0 | |
} | |
} | |
}, | |
{ | |
opcode: 'runtimeConfigEnabled', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.runtimeConfigEnabled', | |
default: '[OPTION] enabled?', | |
description: 'Block that returns whether a runtime option like Turbo Mode is enabled on the project or not.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.BOOLEAN, | |
arguments: { | |
OPTION: { | |
menu: 'runtimeConfig' | |
} | |
} | |
}, | |
{ | |
opcode: 'turboModeEnabled', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.turboModeEnabled', | |
default: 'turbo mode enabled?', | |
description: 'Block that returns whether Turbo Mode is enabled on the project or not.' | |
}), | |
disableMonitor: false, | |
hideFromPalette: true, | |
blockType: BlockType.BOOLEAN | |
}, | |
'---', | |
{ | |
opcode: 'setMaxClones', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.setMaxClones', | |
default: 'set max clones to [MAX]', | |
description: 'Block that enables or disables configuration on the runtime like high quality pen or turbo mode.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.COMMAND, | |
arguments: { | |
MAX: { | |
menu: 'cloneLimit', | |
defaultValue: 300 | |
} | |
} | |
}, | |
{ | |
opcode: 'maxAmountOfClones', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.maxAmountOfClones', | |
default: 'max clone count', | |
description: 'Block that returns the maximum amount of clones that may exist.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.REPORTER | |
}, | |
{ | |
opcode: 'amountOfClones', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.amountOfClones', | |
default: 'clone count', | |
description: 'Block that returns the amount of clones that currently exist.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.REPORTER | |
}, | |
{ | |
opcode: 'getIsClone', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.getIsClone', | |
default: 'is clone?', | |
description: 'Block that returns whether the sprite is a clone or not.' | |
}), | |
disableMonitor: true, | |
blockType: BlockType.BOOLEAN | |
}, | |
'---', | |
{ | |
opcode: 'setMaxFrameRate', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.setMaxFrameRate', | |
default: 'set max framerate to: [FRAMERATE]', | |
description: 'Sets the max allowed framerate.' | |
}), | |
blockType: BlockType.COMMAND, | |
arguments: { | |
FRAMERATE: { | |
type: ArgumentType.NUMBER, | |
defaultValue: 30 | |
} | |
} | |
}, | |
{ | |
opcode: 'getMaxFrameRate', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.getMaxFrameRate', | |
default: 'max framerate', | |
description: 'Block that returns the amount of FPS allowed.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.REPORTER | |
}, | |
{ | |
opcode: 'getFrameRate', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.getFrameRate', | |
default: 'framerate', | |
description: 'Block that returns the amount of FPS.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.REPORTER | |
}, | |
'---', | |
{ | |
opcode: 'setBackgroundColor', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.setBackgroundColor', | |
default: 'set stage background color to [COLOR]', | |
description: 'Sets the background color of the stage.' | |
}), | |
blockType: BlockType.COMMAND, | |
arguments: { | |
COLOR: { | |
type: ArgumentType.COLOR | |
} | |
} | |
}, | |
{ | |
opcode: 'getBackgroundColor', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.getBackgroundColor', | |
default: 'stage background color', | |
description: 'Block that returns the stage background color in HEX.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.REPORTER | |
}, | |
"---", | |
{ | |
opcode: "pauseScript", | |
blockType: BlockType.COMMAND, | |
text: "pause this script using name: [NAME]", | |
arguments: { | |
NAME: { | |
type: ArgumentType.STRING, | |
defaultValue: "my script", | |
}, | |
} | |
}, | |
{ | |
opcode: "unpauseScript", | |
blockType: BlockType.COMMAND, | |
text: "unpause script named: [NAME]", | |
arguments: { | |
NAME: { | |
type: ArgumentType.STRING, | |
defaultValue: "my script", | |
}, | |
} | |
}, | |
{ | |
opcode: "isScriptPaused", | |
blockType: BlockType.BOOLEAN, | |
text: "is script named [NAME] paused?", | |
arguments: { | |
NAME: { | |
type: ArgumentType.STRING, | |
defaultValue: "my script", | |
}, | |
} | |
}, | |
"---", | |
{ | |
opcode: 'variables_createVariable', | |
text: 'create variable named [NAME] for [SCOPE]', | |
blockType: BlockType.COMMAND, | |
arguments: { | |
NAME: { type: ArgumentType.STRING, defaultValue: "my variable" }, | |
SCOPE: { type: ArgumentType.STRING, menu: "variableScope" } | |
} | |
}, | |
{ | |
opcode: 'variables_createCloudVariable', | |
text: 'create cloud variable named [NAME]', | |
blockType: BlockType.COMMAND, | |
arguments: { | |
NAME: { type: ArgumentType.STRING, defaultValue: "cloud variable" }, | |
} | |
}, | |
{ | |
opcode: 'variables_createList', | |
text: 'create list named [NAME] for [SCOPE]', | |
blockType: BlockType.COMMAND, | |
arguments: { | |
NAME: { type: ArgumentType.STRING, defaultValue: "list" }, | |
SCOPE: { type: ArgumentType.STRING, menu: "variableScope" } | |
} | |
}, | |
{ | |
opcode: 'variables_getVariable', | |
text: 'get value of variable named [NAME] in [SCOPE]', | |
disableMonitor: true, | |
blockType: BlockType.REPORTER, | |
arguments: { | |
NAME: { type: ArgumentType.STRING, defaultValue: "my variable" }, | |
SCOPE: { type: ArgumentType.STRING, menu: "variableTypes" } | |
} | |
}, | |
{ | |
opcode: 'variables_getList', | |
text: 'get array of list named [NAME] in [SCOPE]', | |
disableMonitor: true, | |
blockType: BlockType.REPORTER, | |
arguments: { | |
NAME: { type: ArgumentType.STRING, defaultValue: "list" }, | |
SCOPE: { type: ArgumentType.STRING, menu: "variableScope" } | |
} | |
}, | |
{ | |
opcode: 'variables_existsVariable', | |
text: 'variable named [NAME] exists in [SCOPE]?', | |
disableMonitor: true, | |
blockType: BlockType.BOOLEAN, | |
arguments: { | |
NAME: { type: ArgumentType.STRING, defaultValue: "my variable" }, | |
SCOPE: { type: ArgumentType.STRING, menu: "variableTypes" } | |
} | |
}, | |
{ | |
opcode: 'variables_existsList', | |
text: 'list named [NAME] exists in [SCOPE]?', | |
disableMonitor: true, | |
blockType: BlockType.BOOLEAN, | |
arguments: { | |
NAME: { type: ArgumentType.STRING, defaultValue: "list" }, | |
SCOPE: { type: ArgumentType.STRING, menu: "variableScope" } | |
} | |
}, | |
"---", | |
{ | |
opcode: 'getDataOption', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.getDataOption', | |
default: 'get binary data of [OPTION] named [NAME]', | |
description: 'Block that returns the binary data of a sprite, sound or costume.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.REPORTER, | |
arguments: { | |
OPTION: { | |
type: ArgumentType.STRING, | |
menu: "objectType" | |
}, | |
NAME: { | |
type: ArgumentType.STRING, | |
defaultValue: "Sprite1" | |
} | |
} | |
}, | |
{ | |
opcode: 'getDataUriOption', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.getDataUriOption', | |
default: 'get data uri of [OPTION] named [NAME]', | |
description: 'Block that returns the data URI of a sprite, sound or costume.' | |
}), | |
disableMonitor: false, | |
blockType: BlockType.REPORTER, | |
arguments: { | |
OPTION: { | |
type: ArgumentType.STRING, | |
menu: "objectType" | |
}, | |
NAME: { | |
type: ArgumentType.STRING, | |
defaultValue: "Sprite1" | |
} | |
} | |
}, | |
"---", | |
{ | |
opcode: 'getAllSprites', | |
text: 'get all sprites', | |
disableMonitor: false, | |
blockType: BlockType.REPORTER | |
}, | |
{ | |
opcode: 'getAllCostumes', | |
text: 'get all costumes', | |
disableMonitor: false, | |
blockType: BlockType.REPORTER | |
}, | |
{ | |
opcode: 'getAllSounds', | |
text: 'get all sounds', | |
disableMonitor: false, | |
blockType: BlockType.REPORTER | |
}, | |
{ | |
opcode: 'getAllFonts', | |
text: 'get all fonts', | |
disableMonitor: false, | |
blockType: BlockType.REPORTER | |
}, | |
"---", | |
{ | |
opcode: 'getAllVariables', | |
text: 'get all variables [ALLSCOPE]', | |
disableMonitor: false, | |
blockType: BlockType.REPORTER, | |
arguments: { | |
ALLSCOPE: { | |
type: ArgumentType.STRING, | |
menu: "allVariableType" | |
} | |
} | |
}, | |
{ | |
opcode: 'getAllLists', | |
text: 'get all lists [ALLSCOPE]', | |
disableMonitor: false, | |
blockType: BlockType.REPORTER, | |
arguments: { | |
ALLSCOPE: { | |
type: ArgumentType.STRING, | |
menu: "allVariableScope" | |
} | |
} | |
}, | |
"---", | |
{ | |
blockType: BlockType.LABEL, | |
text: "Potentially Dangerous" | |
}, | |
{ | |
opcode: 'deleteCostume', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.deleteCostume', | |
default: 'delete costume at index [COSTUME]', | |
description: 'Deletes a costume at the specified index.' | |
}), | |
blockType: BlockType.COMMAND, | |
arguments: { | |
COSTUME: { | |
type: ArgumentType.NUMBER, | |
defaultValue: 1 | |
} | |
} | |
}, | |
{ | |
opcode: 'deleteSound', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.deleteSound', | |
default: 'delete sound at index [SOUND]', | |
description: 'Deletes a sound at the specified index.' | |
}), | |
blockType: BlockType.COMMAND, | |
arguments: { | |
SOUND: { | |
type: ArgumentType.NUMBER, | |
defaultValue: 1 | |
} | |
} | |
}, | |
"---", | |
{ | |
opcode: 'variables_deleteVariable', | |
text: 'delete variable named [NAME] in [SCOPE]', | |
blockType: BlockType.COMMAND, | |
arguments: { | |
NAME: { type: ArgumentType.STRING, defaultValue: "my variable" }, | |
SCOPE: { type: ArgumentType.STRING, menu: "variableTypes" } | |
} | |
}, | |
{ | |
opcode: 'variables_deleteList', | |
text: 'delete list named [NAME] in [SCOPE]', | |
blockType: BlockType.COMMAND, | |
arguments: { | |
NAME: { type: ArgumentType.STRING, defaultValue: "list" }, | |
SCOPE: { type: ArgumentType.STRING, menu: "variableScope" } | |
} | |
}, | |
"---", | |
{ | |
opcode: 'deleteSprite', | |
text: formatMessage({ | |
id: 'jgRuntime.blocks.deleteSprite', | |
default: 'delete sprite named [NAME]', | |
description: 'Deletes a sprite with the specified name.' | |
}), | |
blockType: BlockType.COMMAND, | |
arguments: { | |
NAME: { | |
type: ArgumentType.STRING, | |
defaultValue: "Sprite1" | |
} | |
} | |
}, | |
], | |
menus: { | |
objectType: { | |
acceptReporters: true, | |
items: [ | |
"sprite", "costume", "sound" | |
].map(item => ({ text: item, value: item })) | |
}, | |
variableScope: { | |
acceptReporters: true, | |
items: [ | |
"all sprites", "this sprite" | |
].map(item => ({ text: item, value: item })) | |
}, | |
allVariableScope: { | |
acceptReporters: true, | |
items: [ | |
"for all sprites", "in every sprite", "in this sprite" | |
].map(item => ({ text: item, value: item })) | |
}, | |
allVariableType: { | |
acceptReporters: true, | |
items: [ | |
"for all sprites", "in every sprite", | |
"in this sprite", "in the cloud" | |
].map(item => ({ text: item, value: item })) | |
}, | |
variableTypes: { | |
acceptReporters: true, | |
items: [ | |
"all sprites", "this sprite", "cloud" | |
].map(item => ({ text: item, value: item })) | |
}, | |
cloneLimit: { | |
items: [ | |
'100', '128', '300', '500', | |
'1000', '1024', '5000', | |
'10000', '16384', 'Infinity' | |
], | |
isTypeable: true, | |
isNumeric: true | |
}, | |
runtimeConfig: { | |
acceptReporters: true, | |
items: [ | |
"turbo mode", | |
"high quality pen", | |
"offscreen sprites", | |
"remove miscellaneous limits", | |
"disable offscreen rendering", | |
"interpolation", | |
"warp timer" | |
] | |
}, | |
renderConfigCappable: { | |
acceptReporters: true, | |
items: ["animated text resolution"] | |
}, | |
renderConfigNumber: { | |
acceptReporters: true, | |
items: ["animated text resolution"] | |
}, | |
onoff: ["on", "off"], | |
costumeMimeType: ["png", "bmp", "jpg", "jpeg", "jfif", "webp", "gif", "vector"], | |
cappableSettings: ["uncapped", "capped", "fixed"] | |
} | |
}; | |
} | |
// utils | |
_generateScratchId() { | |
const soup = "!#%()*+,-./:;=?@[]^_`{|}~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | |
const id = []; | |
for (let i = 0; i < 20; i++) { id[i] = soup.charAt(Math.random() * soup.length) } | |
return id.join(""); | |
} | |
// blocks | |
addCostumeUrl(args, util) { | |
const targetId = util.target.id; | |
return new Promise(resolve => { | |
fetch(args.URL, { method: 'GET' }).then(x => x.blob().then(blob => { | |
const costumeHasForcedMime = !!args.costtype; | |
const costumeForcedMimeBitmap = args.costtype !== "vector"; | |
if (!( | |
(this._typeIsBitmap(blob.type)) || (blob.type === 'image/svg+xml') | |
) && !costumeHasForcedMime) { | |
resolve(); | |
throw new Error(`Invalid mime type: "${blob.type}"`); | |
} | |
const assetType = (costumeHasForcedMime ? costumeForcedMimeBitmap : this._typeIsBitmap(blob.type)) ? this.runtime.storage.AssetType.ImageBitmap : this.runtime.storage.AssetType.ImageVector; | |
const dataType = costumeHasForcedMime ? (costumeForcedMimeBitmap ? args.costtype : 'svg') : (blob.type === 'image/svg+xml' ? 'svg' : blob.type.split('/')[1]); | |
blob.arrayBuffer().then(buffer => { | |
const data = costumeHasForcedMime ? (!costumeForcedMimeBitmap ? buffer : new Uint8Array(buffer)) : (dataType === 'image/svg+xml' | |
? buffer : new Uint8Array(buffer)); | |
const asset = this.runtime.storage.createAsset(assetType, dataType, data, null, true); | |
const name = `${asset.assetId}.${asset.dataFormat}`; | |
const spriteJson = { asset: asset, md5ext: name, name: args.name }; | |
const request = vm.addCostume(name, spriteJson, targetId); | |
if (request.then) request.then(resolve); | |
else resolve(); | |
}) | |
.catch(err => { | |
console.error(`Failed to Load Costume: ${err}`); | |
resolve(); | |
}); | |
})); | |
}); | |
} | |
addCostumeUrlForceMime(args, util) { | |
this.addCostumeUrl(args, util); | |
} | |
deleteCostume(args, util) { | |
const index = Math.round(Cast.toNumber(args.COSTUME)) - 1; | |
if (index < 0) return; | |
util.target.deleteCostume(index); | |
} | |
deleteSound(args, util) { | |
const index = Math.round(Cast.toNumber(args.SOUND)) - 1; | |
if (index < 0) return; | |
util.target.deleteSound(index); | |
} | |
getIndexOfCostume(args, util) { return util.target.getCostumeIndexByName(args.costume) + 1 } | |
getIndexOfSound(args, util) { | |
let index = 0; | |
const sounds = util.target.getSounds(); | |
for (let i = 0; i < sounds.length; i++) { | |
if (sounds[i].name === args.NAME) index = i + 1; | |
} | |
return index; | |
} | |
setStageSize(args) { | |
if (vm) vm.setStageSize( | |
Math.max(1, Cast.toNumber(args.WIDTH)), Math.max(1, Cast.toNumber(args.HEIGHT)) | |
); | |
} | |
turboModeEnabled() { return this.runtime.turboMode } | |
amountOfClones() { return this.runtime._cloneCounter } | |
getStageWidth() { return this.runtime.stageWidth } | |
getStageHeight() { return this.runtime.stageHeight } | |
getMaxFrameRate() { return this.runtime.frameLoop.framerate } | |
getIsClone(_, util) { return !(util.target.isOriginal) } | |
changeRenderingCapping(args) { | |
const option = Cast.toString(args.OPTION).toLowerCase(); | |
const capping = Cast.toString(args.CAPPED).toLowerCase(); | |
switch (option) { | |
case "animated text resolution": { | |
this.runtime.renderer.customRenderConfig.textCostumeResolution.fixed = false; | |
this.runtime.renderer.customRenderConfig.textCostumeResolution.capped = false; | |
if (capping === "fixed") this.runtime.renderer.customRenderConfig.textCostumeResolution.fixed = true; | |
else if (capping === "capped") this.runtime.renderer.customRenderConfig.textCostumeResolution.capped = true; | |
break; | |
} | |
} | |
this.runtime.renderer.dirty = true; | |
this.runtime.requestRedraw(); | |
} | |
setRenderingNumber(args) { | |
const option = Cast.toString(args.OPTION).toLowerCase(); | |
const number = Cast.toNumber(args.NUM); | |
switch (option) { | |
case "animated text resolution": { | |
this.runtime.renderer.customRenderConfig.textCostumeResolution.value = number; | |
break; | |
} | |
case "max texture scale for new svg images": { | |
this.runtime.renderer.setMaxTextureDimension(number); | |
break; | |
} | |
} | |
this.runtime.renderer.dirty = true; | |
this.runtime.requestRedraw(); | |
} | |
updateRuntimeConfig(args) { | |
const enabled = Cast.toString(args.ENABLED).toLowerCase() === 'on'; | |
switch (Cast.toString(args.OPTION).toLowerCase()) { | |
case 'turbo mode': return vm.setTurboMode(enabled); | |
case "high quality pen": return this.runtime.renderer.setUseHighQualityRender(enabled); | |
case "offscreen sprites": return this.runtime.setRuntimeOptions({ fencing: !enabled }); | |
case "remove miscellaneous limits": return this.runtime.setRuntimeOptions({ miscLimits: !enabled }); | |
case "disable offscreen rendering": return this.runtime.setRuntimeOptions({ disableOffscreenRendering: enabled }); | |
case "interpolation": return vm.setInterpolation(enabled); | |
case "warp timer": return this.runtime.setCompilerOptions({ warpTimer: enabled }); | |
} | |
} | |
runtimeConfigEnabled(args) { | |
switch (Cast.toString(args.OPTION).toLowerCase()) { | |
case 'turbo mode': return this.runtime.turboMode; | |
case "high quality pen": return this.runtime.renderer.useHighQualityRender; | |
case "offscreen sprites": return !this.runtime.runtimeOptions.fencing; | |
case "remove miscellaneous limits": return !this.runtime.runtimeOptions.miscLimits; | |
case "disable offscreen rendering": return this.runtime.runtimeOptions.disableOffscreenRendering; | |
case "interpolation": return this.runtime.interpolationEnabled; | |
case "warp timer": return this.runtime.compilerOptions.warpTimer; | |
default: return false; | |
} | |
} | |
setMaxClones(args) { | |
const limit = Math.round(Cast.toNumber(args.MAX)); | |
this.runtime.vm.setRuntimeOptions({ maxClones: limit }); | |
} | |
maxAmountOfClones() { return this.runtime.runtimeOptions.maxClones } | |
setBackgroundColor(args) { | |
let RGB; | |
if (typeof args.COLOR === "number") { | |
RGB = Cast.toRgbColorObject(args.COLOR); | |
this.runtime.renderer.setBackgroundColor(RGB.r / 255, RGB.g / 255, RGB.b / 255); | |
} else { | |
RGB = Cast.toString(args.COLOR); | |
RGB = RGB.startsWith("#") ? RGB.slice(1) : RGB; | |
this.runtime.renderer.setBackgroundColor( | |
parseInt(RGB.slice(0, 2), 16) / 255, | |
parseInt(RGB.slice(2, 4), 16) / 255, | |
parseInt(RGB.slice(4, 6), 16) / 255, | |
RGB.length === 8 ? parseInt(RGB.slice(6, 8), 16) / 255 : 1 | |
) | |
} | |
} | |
getBackgroundColor() { | |
const colorArray = this.runtime.renderer._backgroundColor3b; | |
const colorObject = { | |
r: Math.round(Cast.toNumber(colorArray[0])), | |
g: Math.round(Cast.toNumber(colorArray[1])), | |
b: Math.round(Cast.toNumber(colorArray[2])) | |
}; | |
const hex = Color.rgbToHex(colorObject); | |
return hex; | |
} | |
// SharkPool, edited by JeremyGamer13 | |
pauseScript(args, util) { | |
const scriptName = Cast.toString(args.NAME); | |
const state = util.stackFrame.pausedScript; | |
if (!state) { | |
this.pausedScripts[scriptName] = true; | |
util.stackFrame.pausedScript = scriptName; | |
util.yield(); | |
} else if (state in this.pausedScripts) { | |
util.yield(); | |
} | |
} | |
unpauseScript(args) { | |
const scriptName = Cast.toString(args.NAME); | |
if (scriptName in this.pausedScripts) { | |
delete this.pausedScripts[scriptName]; | |
} | |
} | |
isScriptPaused(args) { | |
const scriptName = Cast.toString(args.NAME); | |
return scriptName in this.pausedScripts; | |
} | |
setMaxFrameRate(args) { | |
let frameRate = Cast.toNumber(args.FRAMERATE); | |
this.runtime.frameLoop.setFramerate(frameRate); | |
} | |
deleteSprite(args) { | |
const target = this.runtime.getSpriteTargetByName(args.NAME); | |
if (!target) return; | |
vm.deleteSpriteInternal(target.id); | |
} | |
getDataOption(args, util) { | |
switch (args.OPTION) { | |
case "sprite": { | |
const sprites = this.runtime.targets.filter(target => target.isOriginal); | |
const sprite = sprites.filter(sprite => sprite.sprite.name === args.NAME)[0]; | |
if (!sprite) return "[]"; | |
return new Promise(resolve => { | |
vm.exportSprite(sprite.id).then(blob => { | |
blob.arrayBuffer().then(arrayBuffer => { | |
const array = BufferUtil.bufferToArray(arrayBuffer); | |
const stringified = JSON.stringify(array); | |
resolve(stringified); | |
}).catch(() => resolve("[]")); | |
}).catch(() => resolve("[]")); | |
}); | |
} | |
case "costume": { | |
const costumes = util.target.getCostumes(); | |
const index = util.target.getCostumeIndexByName(args.NAME); | |
if (!costumes[index]) return "[]"; | |
const costume = costumes[index]; | |
const data = costume.asset.data; | |
const array = BufferUtil.bufferToArray(data.buffer); | |
return JSON.stringify(array); | |
} | |
case "sound": { | |
const sounds = util.target.getSounds(); | |
const index = this.getIndexOfSound(args, util) - 1; | |
if (!sounds[index]) return "[]"; | |
const sound = sounds[index]; | |
const data = sound.asset.data; | |
const array = BufferUtil.bufferToArray(data.buffer); | |
return JSON.stringify(array); | |
} | |
default: return "[]"; | |
} | |
} | |
getDataUriOption(args, util) { | |
switch (args.OPTION) { | |
case "sprite": { | |
const sprites = this.runtime.targets.filter(target => target.isOriginal); | |
const sprite = sprites.filter(sprite => sprite.sprite.name === args.NAME)[0]; | |
if (!sprite) return ""; | |
return new Promise(resolve => { | |
vm.exportSprite(sprite.id).then(blob => { | |
const reader = new FileReader(); | |
reader.onload = () => resolve(reader.result); | |
reader.onerror = () => resolve(""); | |
reader.onabort = () => resolve(""); | |
reader.readAsDataURL(blob); | |
}).catch(() => resolve("")); | |
}); | |
} | |
case "costume": { | |
const costumes = util.target.getCostumes(); | |
const index = util.target.getCostumeIndexByName(args.NAME); | |
if (!costumes[index]) return ""; | |
const costume = costumes[index]; | |
return costume.asset.encodeDataURI(); | |
} | |
case "sound": { | |
const sounds = util.target.getSounds(); | |
const index = this.getIndexOfSound(args, util) - 1; | |
if (!sounds[index]) return ""; | |
const sound = sounds[index]; | |
return sound.asset.encodeDataURI(); | |
} | |
default: return ""; | |
} | |
} | |
getAllSprites() { | |
return JSON.stringify(this.runtime.targets.filter(target => target.isOriginal && !target.isStage).map(target => target.sprite.name)); | |
} | |
getAllCostumes(_, util) { | |
const costumes = util.target.getCostumes(); | |
return JSON.stringify(costumes.map(costume => costume.name)); | |
} | |
getAllSounds(_, util) { | |
const sounds = util.target.getSounds(); | |
return JSON.stringify(sounds.map(sound => sound.name)); | |
} | |
getAllFonts() { | |
const fonts = this.runtime.fontManager.getFonts(); | |
return JSON.stringify(fonts.map(font => font.name)); | |
} | |
loadProjectDataUrl(args) { | |
const url = Cast.toString(args.URL); | |
if (typeof ScratchBlocks !== "undefined") { | |
// We are in the editor. Ask before loading a new project to avoid unrecoverable data loss. | |
if (!confirm(`Runtime Extension - Editor: Are you sure you want to load a new project?\nEverything in the current project will be permanently deleted.`)) { | |
return; | |
} | |
} | |
console.log("Loading project from custom source..."); | |
fetch(url) | |
.then((r) => r.arrayBuffer()) | |
.then((buffer) => vm.loadProject(buffer)) | |
.then(() => { | |
console.log("Loaded project!"); | |
vm.greenFlag(); | |
}) | |
.catch((error) => { | |
console.log("Error loading custom project;", error); | |
}); | |
} | |
getProjectDataUrl() { | |
return new Promise((resolve) => { | |
const failingUrl = 'data:application/octet-stream;base64,'; | |
vm.saveProjectSb3().then(blob => { | |
const fileReader = new FileReader(); | |
fileReader.onload = () => { resolve(fileReader.result); }; | |
fileReader.onerror = () => { resolve(failingUrl) } | |
fileReader.readAsDataURL(blob); | |
}).catch(() => { resolve(failingUrl) }); | |
}); | |
} | |
getAllVariables(args, util) { | |
switch (args.ALLSCOPE) { | |
case "for all sprites": { | |
const stage = this.runtime.getTargetForStage(); | |
if (!stage) return "[]"; | |
const variables = stage.variables; | |
if (!variables) return "[]"; | |
return JSON.stringify(Object.values(variables).filter(v => v.type !== "list").map(v => v.name)); | |
} | |
case "in every sprite": { | |
const targets = this.runtime.targets; | |
if (!targets) return "[]"; | |
const variables = targets.filter(t => t.isOriginal).map(t => t.variables); | |
if (!variables) return "[]"; | |
return JSON.stringify(variables.map(v => Object.values(v)).map(v => v.filter(v => v.type !== "list").map(v => v.name)).flat(1)); | |
} | |
case "in this sprite": { | |
const target = util.target; | |
if (!target) return "[]"; | |
const variables = target.variables; | |
if (!variables) return "[]"; | |
return JSON.stringify(Object.values(variables).filter(v => v.type !== "list").map(v => v.name)); | |
} | |
case "in the cloud": { | |
const stage = this.runtime.getTargetForStage(); | |
if (!stage) return "[]"; | |
const variables = stage.variables; | |
if (!variables) return "[]"; | |
return JSON.stringify(Object.values(variables).filter(v => v.type !== "list").filter(v => v.isCloud === true).map(v => v.name)); | |
} | |
default: return "[]"; | |
} | |
} | |
getAllLists(args, util) { | |
switch (args.ALLSCOPE) { | |
case "for all sprites": { | |
const stage = this.runtime.getTargetForStage(); | |
if (!stage) return "[]"; | |
const variables = stage.variables; | |
if (!variables) return "[]"; | |
return JSON.stringify(Object.values(variables).filter(v => v.type === "list").map(v => v.name)); | |
} | |
case "in every sprite": { | |
const targets = this.runtime.targets; | |
if (!targets) return "[]"; | |
const variables = targets.filter(t => t.isOriginal).map(t => t.variables); | |
if (!variables) return "[]"; | |
return JSON.stringify(variables.map(v => Object.values(v)).map(v => v.filter(v => v.type === "list").map(v => v.name)).flat(1)); | |
} | |
case "in this sprite": { | |
const target = util.target; | |
if (!target) return "[]"; | |
const variables = target.variables; | |
if (!variables) return "[]"; | |
return JSON.stringify(Object.values(variables).filter(v => v.type === "list").map(v => v.name)); | |
} | |
default: return "[]"; | |
} | |
} | |
// ShovelUtils | |
getFrameRate() { return fps } | |
addSoundUrl(args, util) { | |
const targetId = util.target.id; | |
return new Promise((resolve) => { | |
fetch(args.URL) | |
.then((r) => r.arrayBuffer()) | |
.then((arrayBuffer) => { | |
const storage = this.runtime.storage; | |
const asset = new storage.Asset( | |
storage.AssetType.Sound, null, storage.DataFormat.MP3, | |
new Uint8Array(arrayBuffer), true | |
); | |
resolve(vm.addSound({ | |
md5: asset.assetId + '.' + asset.dataFormat, | |
asset: asset, name: args.NAME | |
}, targetId)); | |
}).catch(resolve); | |
}) | |
} | |
// GameUtils | |
addSpriteUrl(args) { | |
return new Promise((resolve) => { | |
fetch(args.URL).then(response => { | |
response.arrayBuffer().then(arrayBuffer => { | |
vm.addSprite(arrayBuffer).finally(resolve); | |
}).catch(resolve); | |
}).catch(resolve); | |
}); | |
} | |
// variables | |
variables_createVariable(args, util) { | |
const variableName = args.NAME; | |
switch (args.SCOPE) { | |
case "all sprites": return this.runtime.createNewGlobalVariable(variableName); | |
case "this sprite": return util.target.createVariable(this._generateScratchId(), variableName, ""); | |
} | |
} | |
variables_createCloudVariable(args) { | |
const variableName = `☁ ${args.NAME}`; | |
const stage = this.runtime.getTargetForStage(); | |
if (!stage) return; | |
const id = this._generateScratchId(); | |
stage.createVariable(id, variableName, "", true); | |
} | |
variables_createList(args, util) { | |
const variableName = args.NAME; | |
switch (args.SCOPE) { | |
case "all sprites": return this.runtime.createNewGlobalVariable(variableName, null, "list"); | |
case "this sprite": return util.target.createVariable(this._generateScratchId(), variableName, "list"); | |
} | |
} | |
variables_getVariable(args, util) { | |
const variableName = args.NAME; | |
let target; | |
let isCloud = false; | |
if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); | |
else if (args.SCOPE === "this sprite") target = util.target; | |
else if (args.SCOPE === "cloud") { | |
target = this.runtime.getTargetForStage(); | |
isCloud = true; | |
} else return ""; | |
const variables = Object.values(target.variables).filter(variable => variable.type !== "list").filter(variable => { | |
if (variable.isCloud) return String(variable.name).replace("☁ ", "") === variableName; | |
if (isCloud) return false; // above check should have already told us its a cloud variable | |
return variable.name === variableName; | |
}); | |
if (!variables) return ""; | |
const variable = variables[0]; | |
if (!variable) return ""; | |
return variable.value; | |
} | |
variables_getList(args, util) { | |
const variableName = args.NAME; | |
let target; | |
if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); | |
else if (args.SCOPE === "this sprite") target = util.target; | |
else return "[]"; | |
const variables = Object.values(target.variables).filter(v => v.type === "list").filter(v => v.name === variableName); | |
if (!variables) return "[]"; | |
const variable = variables[0]; | |
if (!variable) return "[]"; | |
return JSON.stringify(variable.value); | |
} | |
variables_deleteVariable(args, util) { | |
const variableName = args.NAME; | |
let target, isCloud = false; | |
if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); | |
else if (args.SCOPE === "this sprite") target = util.target; | |
else if (args.SCOPE === "cloud") { | |
target = this.runtime.getTargetForStage(); | |
isCloud = true; | |
} else return; | |
const variables = Object.values(target.variables).filter(v => v.type !== "list").filter(variable => { | |
if (variable.isCloud) return String(variable.name).replace("☁ ", "") === variableName; | |
if (isCloud) return false; // above check should have already told us its a cloud variable | |
return variable.name === variableName; | |
}); | |
if (!variables) return; | |
const variable = variables[0]; | |
if (!variable) return; | |
return target.deleteVariable(variable.id); | |
} | |
variables_deleteList(args, util) { | |
const variableName = args.NAME; | |
let target; | |
if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); | |
else if (args.SCOPE === "this sprite") target = util.target; | |
else return; | |
const variables = Object.values(target.variables).filter(v => v.type === "list").filter(v => v.name === variableName); | |
if (!variables) return; | |
const variable = variables[0]; | |
if (!variable) return; | |
return target.deleteVariable(variable.id); | |
} | |
variables_existsVariable(args, util) { | |
const variableName = args.NAME; | |
let target, isCloud = false; | |
if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); | |
else if (args.SCOPE === "this sprite") target = util.target; | |
else if (args.SCOPE === "cloud") { | |
target = this.runtime.getTargetForStage(); | |
isCloud = true; | |
} else return false; | |
const variables = Object.values(target.variables).filter(v => v.type !== "list").filter(variable => { | |
if (variable.isCloud) return String(variable.name).replace("☁ ", "") === variableName; | |
if (isCloud) return false; // above check should have already told us its a cloud variable | |
return variable.name === variableName; | |
}); | |
if (!variables) return false; | |
const variable = variables[0]; | |
if (!variable) return false; | |
return true; | |
} | |
variables_existsList(args, util) { | |
const variableName = args.NAME; | |
let target; | |
if (args.SCOPE === "all sprites") target = this.runtime.getTargetForStage(); | |
else if (args.SCOPE === "this sprite") target = util.target; | |
else return false; | |
const variables = Object.values(target.variables).filter(v => v.type === "list").filter(v => v.name === variableName); | |
if (!variables) return false; | |
const variable = variables[0]; | |
if (!variable) return false; | |
return true; | |
} | |
} | |
module.exports = JgRuntimeBlocks; | |