soiz1's picture
Upload 811 files
30c32c8 verified
raw
history blame
53.1 kB
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;