UNPKG

scratch-vm

Version:
1,314 lines (1,242 loc) • 3.19 MB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["VirtualMachine"] = factory(); else root["VirtualMachine"] = factory(); })(global, () => { return /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ /***/ "./node_modules/arraybuffer-loader/lib/to-array-buffer.js": /*!****************************************************************!*\ !*** ./node_modules/arraybuffer-loader/lib/to-array-buffer.js ***! \****************************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { "use strict"; /* provided dependency */ var Buffer = __webpack_require__(/*! buffer */ "buffer")["Buffer"]; module.exports = function (base64Data) { var isBrowser = typeof window !== 'undefined' && typeof window.atob === 'function' var binary = isBrowser ? window.atob(base64Data) : Buffer.from(base64Data, 'base64').toString('binary') var bytes = new Uint8Array(binary.length) for (var i = 0; i < binary.length; ++i) { bytes[i] = binary.charCodeAt(i) } return bytes.buffer } /***/ }), /***/ "./package.json": /*!**********************!*\ !*** ./package.json ***! \**********************/ /***/ ((module) => { "use strict"; module.exports = /*#__PURE__*/JSON.parse('{"name":"scratch-vm","version":"5.0.299","description":"Virtual Machine for Scratch 3.0","author":"Massachusetts Institute of Technology","license":"AGPL-3.0-only","homepage":"https://github.com/scratchfoundation/scratch-vm#readme","repository":{"type":"git","url":"https://github.com/scratchfoundation/scratch-vm.git","sha":"af179d8114073ee02ee3315ac5ebebdb58151b47"},"main":"./dist/node/scratch-vm.js","browser":"./dist/web/scratch-vm.js","exports":{"webpack":"./src/index.js","browser":"./dist/web/scratch-vm.js","node":"./dist/node/scratch-vm.js","default":"./src/index.js"},"scripts":{"build":"npm run docs && webpack --progress","coverage":"tap ./test/{unit,integration}/*.js --coverage --coverage-report=lcov","docs":"jsdoc -c .jsdoc.json","i18n:src":"mkdirp translations/core && format-message extract --out-file translations/core/en.json src/extensions/**/index.js","i18n:push":"tx-push-src scratch-editor extensions translations/core/en.json","lint":"eslint . && format-message lint src/**/*.js","prepare":"husky install","prepublish":"in-publish && npm run build || not-in-publish","start":"webpack serve","tap":"tap ./test/{unit,integration}/*.js","tap:unit":"tap ./test/unit/*.js","tap:integration":"tap ./test/integration/*.js","test":"npm run lint && npm run tap","watch":"webpack --progress --watch","version":"json -f package.json -I -e \\"this.repository.sha = \'$(git log -n1 --pretty=format:%H)\'\\""},"config":{"commitizen":{"path":"cz-conventional-changelog"}},"browserslist":["Chrome >= 63","Edge >= 15","Firefox >= 57","Safari >= 11"],"tap":{"branches":60,"functions":70,"lines":70,"statements":70},"dependencies":{"@vernier/godirect":"^1.5.0","arraybuffer-loader":"^1.0.6","atob":"^2.1.2","btoa":"^1.2.1","buffer":"^6.0.3","canvas-toBlob":"^1.0.0","decode-html":"^2.0.0","diff-match-patch":"^1.0.4","format-message":"^6.2.1","htmlparser2":"^3.10.0","immutable":"^3.8.1","jszip":"^3.1.5","minilog":"^3.1.0","scratch-audio":"^2.0.0","scratch-parser":"^6.0.0","scratch-render":"^2.0.0","scratch-sb1-converter":"^2.0.0","scratch-storage":"^4.0.0","scratch-svg-renderer":"3.0.114","scratch-translate-extension-languages":"^1.0.0","text-encoding":"^0.7.0","uuid":"^8.3.2","web-worker":"^1.3.0"},"devDependencies":{"@babel/core":"7.27.1","@babel/eslint-parser":"7.27.1","@babel/preset-env":"7.27.1","@commitlint/cli":"17.8.1","@commitlint/config-conventional":"17.8.1","adm-zip":"0.4.11","babel-loader":"9.2.1","callsite":"1.0.0","copy-webpack-plugin":"4.6.0","docdash":"1.2.0","eslint":"8.57.1","eslint-config-scratch":"9.0.9","expose-loader":"1.0.3","file-loader":"6.2.0","format-message-cli":"6.2.4","husky":"8.0.3","in-publish":"2.0.1","js-md5":"0.7.3","jsdoc":"3.6.11","json":"^9.0.4","pngjs":"3.4.0","scratch-blocks":"1.1.210","scratch-l10n":"5.0.230","scratch-render-fonts":"1.0.190","scratch-semantic-release-config":"3.0.0","scratch-webpack-configuration":"3.0.0","script-loader":"0.7.2","semantic-release":"19.0.5","stats.js":"0.17.0","tap":"16.3.10","webpack":"5.99.7","webpack-cli":"4.10.0","webpack-dev-server":"3.11.3"}}'); /***/ }), /***/ "./src/blocks/scratch3_control.js": /*!****************************************!*\ !*** ./src/blocks/scratch3_control.js ***! \****************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { const Cast = __webpack_require__(/*! ../util/cast */ "./src/util/cast.js"); class Scratch3ControlBlocks { constructor(runtime) { /** * The runtime instantiating this block package. * @type {Runtime} */ this.runtime = runtime; /** * The "counter" block value. For compatibility with 2.0. * @type {number} */ this._counter = 0; this.runtime.on('RUNTIME_DISPOSED', this.clearCounter.bind(this)); } /** * Retrieve the block primitives implemented by this package. * @return {object.<string, Function>} Mapping of opcode to Function. */ getPrimitives() { return { control_repeat: this.repeat, control_repeat_until: this.repeatUntil, control_while: this.repeatWhile, control_for_each: this.forEach, control_forever: this.forever, control_wait: this.wait, control_wait_until: this.waitUntil, control_if: this.if, control_if_else: this.ifElse, control_stop: this.stop, control_create_clone_of: this.createClone, control_delete_this_clone: this.deleteClone, control_get_counter: this.getCounter, control_incr_counter: this.incrCounter, control_clear_counter: this.clearCounter, control_all_at_once: this.allAtOnce }; } getHats() { return { control_start_as_clone: { restartExistingThreads: false } }; } repeat(args, util) { const times = Math.round(Cast.toNumber(args.TIMES)); // Initialize loop if (typeof util.stackFrame.loopCounter === 'undefined') { util.stackFrame.loopCounter = times; } // Only execute once per frame. // When the branch finishes, `repeat` will be executed again and // the second branch will be taken, yielding for the rest of the frame. // Decrease counter util.stackFrame.loopCounter--; // If we still have some left, start the branch. if (util.stackFrame.loopCounter >= 0) { util.startBranch(1, true); } } repeatUntil(args, util) { const condition = Cast.toBoolean(args.CONDITION); // If the condition is false (repeat UNTIL), start the branch. if (!condition) { util.startBranch(1, true); } } repeatWhile(args, util) { const condition = Cast.toBoolean(args.CONDITION); // If the condition is true (repeat WHILE), start the branch. if (condition) { util.startBranch(1, true); } } forEach(args, util) { const variable = util.target.lookupOrCreateVariable(args.VARIABLE.id, args.VARIABLE.name); if (typeof util.stackFrame.index === 'undefined') { util.stackFrame.index = 0; } if (util.stackFrame.index < Number(args.VALUE)) { util.stackFrame.index++; variable.value = util.stackFrame.index; util.startBranch(1, true); } } waitUntil(args, util) { const condition = Cast.toBoolean(args.CONDITION); if (!condition) { util.yield(); } } forever(args, util) { util.startBranch(1, true); } wait(args, util) { if (util.stackTimerNeedsInit()) { const duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION)); util.startStackTimer(duration); this.runtime.requestRedraw(); util.yield(); } else if (!util.stackTimerFinished()) { util.yield(); } } if(args, util) { const condition = Cast.toBoolean(args.CONDITION); if (condition) { util.startBranch(1, false); } } ifElse(args, util) { const condition = Cast.toBoolean(args.CONDITION); if (condition) { util.startBranch(1, false); } else { util.startBranch(2, false); } } stop(args, util) { const option = args.STOP_OPTION; if (option === 'all') { util.stopAll(); } else if (option === 'other scripts in sprite' || option === 'other scripts in stage') { util.stopOtherTargetThreads(); } else if (option === 'this script') { util.stopThisScript(); } } createClone(args, util) { // Cast argument to string args.CLONE_OPTION = Cast.toString(args.CLONE_OPTION); // Set clone target let cloneTarget; if (args.CLONE_OPTION === '_myself_') { cloneTarget = util.target; } else { cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION); } // If clone target is not found, return if (!cloneTarget) return; // Create clone const newClone = cloneTarget.makeClone(); if (newClone) { this.runtime.addTarget(newClone); // Place behind the original target. newClone.goBehindOther(cloneTarget); } } deleteClone(args, util) { if (util.target.isOriginal) return; this.runtime.disposeTarget(util.target); this.runtime.stopForTarget(util.target); } getCounter() { return this._counter; } clearCounter() { this._counter = 0; } incrCounter() { this._counter++; } allAtOnce(args, util) { // Since the "all at once" block is implemented for compatiblity with // Scratch 2.0 projects, it behaves the same way it did in 2.0, which // is to simply run the contained script (like "if 1 = 1"). // (In early versions of Scratch 2.0, it would work the same way as // "run without screen refresh" custom blocks do now, but this was // removed before the release of 2.0.) util.startBranch(1, false); } } module.exports = Scratch3ControlBlocks; /***/ }), /***/ "./src/blocks/scratch3_core_example.js": /*!*********************************************!*\ !*** ./src/blocks/scratch3_core_example.js ***! \*********************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { const BlockType = __webpack_require__(/*! ../extension-support/block-type */ "./src/extension-support/block-type.js"); const ArgumentType = __webpack_require__(/*! ../extension-support/argument-type */ "./src/extension-support/argument-type.js"); /* eslint-disable-next-line max-len */ const blockIconURI = 'data:image/svg+xml,%3Csvg id="rotate-counter-clockwise" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%233d79cc;%7D.cls-2%7Bfill:%23fff;%7D%3C/style%3E%3C/defs%3E%3Ctitle%3Erotate-counter-clockwise%3C/title%3E%3Cpath class="cls-1" d="M22.68,12.2a1.6,1.6,0,0,1-1.27.63H13.72a1.59,1.59,0,0,1-1.16-2.58l1.12-1.41a4.82,4.82,0,0,0-3.14-.77,4.31,4.31,0,0,0-2,.8,4.25,4.25,0,0,0-1.34,1.73,5.06,5.06,0,0,0,.54,4.62A5.58,5.58,0,0,0,12,17.74h0a2.26,2.26,0,0,1-.16,4.52A10.25,10.25,0,0,1,3.74,18,10.14,10.14,0,0,1,2.25,8.78,9.7,9.7,0,0,1,5.08,4.64,9.92,9.92,0,0,1,9.66,2.5a10.66,10.66,0,0,1,7.72,1.68l1.08-1.35a1.57,1.57,0,0,1,1.24-.6,1.6,1.6,0,0,1,1.54,1.21l1.7,7.37A1.57,1.57,0,0,1,22.68,12.2Z"/%3E%3Cpath class="cls-2" d="M21.38,11.83H13.77a.59.59,0,0,1-.43-1l1.75-2.19a5.9,5.9,0,0,0-4.7-1.58,5.07,5.07,0,0,0-4.11,3.17A6,6,0,0,0,7,15.77a6.51,6.51,0,0,0,5,2.92,1.31,1.31,0,0,1-.08,2.62,9.3,9.3,0,0,1-7.35-3.82A9.16,9.16,0,0,1,3.17,9.12,8.51,8.51,0,0,1,5.71,5.4,8.76,8.76,0,0,1,9.82,3.48a9.71,9.71,0,0,1,7.75,2.07l1.67-2.1a.59.59,0,0,1,1,.21L22,11.08A.59.59,0,0,1,21.38,11.83Z"/%3E%3C/svg%3E'; /** * An example core block implemented using the extension spec. * This is not loaded as part of the core blocks in the VM but it is provided * and used as part of tests. */ class Scratch3CoreExample { constructor(runtime) { /** * The runtime instantiating this block package. * @type {Runtime} */ this.runtime = runtime; } /** * @returns {object} metadata for this extension and its blocks. */ getInfo() { return { id: 'coreExample', name: 'CoreEx', // This string does not need to be translated as this extension is only used as an example. blocks: [{ func: 'MAKE_A_VARIABLE', blockType: BlockType.BUTTON, text: 'make a variable (CoreEx)' }, { opcode: 'exampleOpcode', blockType: BlockType.REPORTER, text: 'example block' }, { opcode: 'exampleWithInlineImage', blockType: BlockType.COMMAND, text: 'block with image [CLOCKWISE] inline', arguments: { CLOCKWISE: { type: ArgumentType.IMAGE, dataURI: blockIconURI } } }] }; } /** * Example opcode just returns the name of the stage target. * @returns {string} The name of the first target in the project. */ exampleOpcode() { const stage = this.runtime.getTargetForStage(); return stage ? stage.getName() : 'no stage yet'; } exampleWithInlineImage() { return; } } module.exports = Scratch3CoreExample; /***/ }), /***/ "./src/blocks/scratch3_data.js": /*!*************************************!*\ !*** ./src/blocks/scratch3_data.js ***! \*************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { const Cast = __webpack_require__(/*! ../util/cast */ "./src/util/cast.js"); class Scratch3DataBlocks { constructor(runtime) { /** * The runtime instantiating this block package. * @type {Runtime} */ this.runtime = runtime; } /** * Retrieve the block primitives implemented by this package. * @return {object.<string, Function>} Mapping of opcode to Function. */ getPrimitives() { return { data_variable: this.getVariable, data_setvariableto: this.setVariableTo, data_changevariableby: this.changeVariableBy, data_hidevariable: this.hideVariable, data_showvariable: this.showVariable, data_listcontents: this.getListContents, data_addtolist: this.addToList, data_deleteoflist: this.deleteOfList, data_deletealloflist: this.deleteAllOfList, data_insertatlist: this.insertAtList, data_replaceitemoflist: this.replaceItemOfList, data_itemoflist: this.getItemOfList, data_itemnumoflist: this.getItemNumOfList, data_lengthoflist: this.lengthOfList, data_listcontainsitem: this.listContainsItem, data_hidelist: this.hideList, data_showlist: this.showList }; } getVariable(args, util) { const variable = util.target.lookupOrCreateVariable(args.VARIABLE.id, args.VARIABLE.name); return variable.value; } setVariableTo(args, util) { const variable = util.target.lookupOrCreateVariable(args.VARIABLE.id, args.VARIABLE.name); variable.value = args.VALUE; if (variable.isCloud) { util.ioQuery('cloud', 'requestUpdateVariable', [variable.name, args.VALUE]); } } changeVariableBy(args, util) { const variable = util.target.lookupOrCreateVariable(args.VARIABLE.id, args.VARIABLE.name); const castedValue = Cast.toNumber(variable.value); const dValue = Cast.toNumber(args.VALUE); const newValue = castedValue + dValue; variable.value = newValue; if (variable.isCloud) { util.ioQuery('cloud', 'requestUpdateVariable', [variable.name, newValue]); } } changeMonitorVisibility(id, visible) { // Send the monitor blocks an event like the flyout checkbox event. // This both updates the monitor state and changes the isMonitored block flag. this.runtime.monitorBlocks.changeBlock({ id: id, // Monitor blocks for variables are the variable ID. element: 'checkbox', // Mimic checkbox event from flyout. value: visible }, this.runtime); } showVariable(args) { this.changeMonitorVisibility(args.VARIABLE.id, true); } hideVariable(args) { this.changeMonitorVisibility(args.VARIABLE.id, false); } showList(args) { this.changeMonitorVisibility(args.LIST.id, true); } hideList(args) { this.changeMonitorVisibility(args.LIST.id, false); } getListContents(args, util) { const list = util.target.lookupOrCreateList(args.LIST.id, args.LIST.name); // If block is running for monitors, return copy of list as an array if changed. if (util.thread.updateMonitor) { // Return original list value if up-to-date, which doesn't trigger monitor update. if (list._monitorUpToDate) return list.value; // If value changed, reset the flag and return a copy to trigger monitor update. // Because monitors use Immutable data structures, only new objects trigger updates. list._monitorUpToDate = true; return list.value.slice(); } // Determine if the list is all single letters. // If it is, report contents joined together with no separator. // If it's not, report contents joined together with a space. let allSingleLetters = true; for (let i = 0; i < list.value.length; i++) { const listItem = list.value[i]; if (!(typeof listItem === 'string' && listItem.length === 1)) { allSingleLetters = false; break; } } if (allSingleLetters) { return list.value.join(''); } return list.value.join(' '); } addToList(args, util) { const list = util.target.lookupOrCreateList(args.LIST.id, args.LIST.name); if (list.value.length < Scratch3DataBlocks.LIST_ITEM_LIMIT) { list.value.push(args.ITEM); list._monitorUpToDate = false; } } deleteOfList(args, util) { const list = util.target.lookupOrCreateList(args.LIST.id, args.LIST.name); const index = Cast.toListIndex(args.INDEX, list.value.length, true); if (index === Cast.LIST_INVALID) { return; } else if (index === Cast.LIST_ALL) { list.value = []; return; } list.value.splice(index - 1, 1); list._monitorUpToDate = false; } deleteAllOfList(args, util) { const list = util.target.lookupOrCreateList(args.LIST.id, args.LIST.name); list.value = []; return; } insertAtList(args, util) { const item = args.ITEM; const list = util.target.lookupOrCreateList(args.LIST.id, args.LIST.name); const index = Cast.toListIndex(args.INDEX, list.value.length + 1, false); if (index === Cast.LIST_INVALID) { return; } const listLimit = Scratch3DataBlocks.LIST_ITEM_LIMIT; if (index > listLimit) return; list.value.splice(index - 1, 0, item); if (list.value.length > listLimit) { // If inserting caused the list to grow larger than the limit, // remove the last element in the list list.value.pop(); } list._monitorUpToDate = false; } replaceItemOfList(args, util) { const item = args.ITEM; const list = util.target.lookupOrCreateList(args.LIST.id, args.LIST.name); const index = Cast.toListIndex(args.INDEX, list.value.length, false); if (index === Cast.LIST_INVALID) { return; } list.value[index - 1] = item; list._monitorUpToDate = false; } getItemOfList(args, util) { const list = util.target.lookupOrCreateList(args.LIST.id, args.LIST.name); const index = Cast.toListIndex(args.INDEX, list.value.length, false); if (index === Cast.LIST_INVALID) { return ''; } return list.value[index - 1]; } getItemNumOfList(args, util) { const item = args.ITEM; const list = util.target.lookupOrCreateList(args.LIST.id, args.LIST.name); // Go through the list items one-by-one using Cast.compare. This is for // cases like checking if 123 is contained in a list [4, 7, '123'] -- // Scratch considers 123 and '123' to be equal. for (let i = 0; i < list.value.length; i++) { if (Cast.compare(list.value[i], item) === 0) { return i + 1; } } // We don't bother using .indexOf() at all, because it would end up with // edge cases such as the index of '123' in [4, 7, 123, '123', 9]. // If we use indexOf(), this block would return 4 instead of 3, because // indexOf() sees the first occurence of the string 123 as the fourth // item in the list. With Scratch, this would be confusing -- after all, // '123' and 123 look the same, so one would expect the block to say // that the first occurrence of '123' (or 123) to be the third item. // Default to 0 if there's no match. Since Scratch lists are 1-indexed, // we don't have to worry about this conflicting with the "this item is // the first value" number (in JS that is 0, but in Scratch it's 1). return 0; } lengthOfList(args, util) { const list = util.target.lookupOrCreateList(args.LIST.id, args.LIST.name); return list.value.length; } listContainsItem(args, util) { const item = args.ITEM; const list = util.target.lookupOrCreateList(args.LIST.id, args.LIST.name); if (list.value.indexOf(item) >= 0) { return true; } // Try using Scratch comparison operator on each item. // (Scratch considers the string '123' equal to the number 123). for (let i = 0; i < list.value.length; i++) { if (Cast.compare(list.value[i], item) === 0) { return true; } } return false; } /** * Type representation for list variables. * @const {number} */ static get LIST_ITEM_LIMIT() { return 200000; } } module.exports = Scratch3DataBlocks; /***/ }), /***/ "./src/blocks/scratch3_event.js": /*!**************************************!*\ !*** ./src/blocks/scratch3_event.js ***! \**************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { const Cast = __webpack_require__(/*! ../util/cast */ "./src/util/cast.js"); class Scratch3EventBlocks { constructor(runtime) { /** * The runtime instantiating this block package. * @type {Runtime} */ this.runtime = runtime; this.runtime.on('KEY_PRESSED', key => { this.runtime.startHats('event_whenkeypressed', { KEY_OPTION: key }); this.runtime.startHats('event_whenkeypressed', { KEY_OPTION: 'any' }); }); } /** * Retrieve the block primitives implemented by this package. * @return {object.<string, Function>} Mapping of opcode to Function. */ getPrimitives() { return { event_whentouchingobject: this.touchingObject, event_broadcast: this.broadcast, event_broadcastandwait: this.broadcastAndWait, event_whengreaterthan: this.hatGreaterThanPredicate }; } getHats() { return { event_whenflagclicked: { restartExistingThreads: true }, event_whenkeypressed: { restartExistingThreads: false }, event_whenthisspriteclicked: { restartExistingThreads: true }, event_whentouchingobject: { restartExistingThreads: false, edgeActivated: true }, event_whenstageclicked: { restartExistingThreads: true }, event_whenbackdropswitchesto: { restartExistingThreads: true }, event_whengreaterthan: { restartExistingThreads: false, edgeActivated: true }, event_whenbroadcastreceived: { restartExistingThreads: true } }; } touchingObject(args, util) { return util.target.isTouchingObject(args.TOUCHINGOBJECTMENU); } hatGreaterThanPredicate(args, util) { const option = Cast.toString(args.WHENGREATERTHANMENU).toLowerCase(); const value = Cast.toNumber(args.VALUE); switch (option) { case 'timer': return util.ioQuery('clock', 'projectTimer') > value; case 'loudness': return this.runtime.audioEngine && this.runtime.audioEngine.getLoudness() > value; } return false; } broadcast(args, util) { const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg(args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); if (broadcastVar) { const broadcastOption = broadcastVar.name; util.startHats('event_whenbroadcastreceived', { BROADCAST_OPTION: broadcastOption }); } } broadcastAndWait(args, util) { if (!util.stackFrame.broadcastVar) { util.stackFrame.broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg(args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); } if (util.stackFrame.broadcastVar) { const broadcastOption = util.stackFrame.broadcastVar.name; // Have we run before, starting threads? if (!util.stackFrame.startedThreads) { // No - start hats for this broadcast. util.stackFrame.startedThreads = util.startHats('event_whenbroadcastreceived', { BROADCAST_OPTION: broadcastOption }); if (util.stackFrame.startedThreads.length === 0) { // Nothing was started. return; } } // We've run before; check if the wait is still going on. const instance = this; // Scratch 2 considers threads to be waiting if they are still in // runtime.threads. Threads that have run all their blocks, or are // marked done but still in runtime.threads are still considered to // be waiting. const waiting = util.stackFrame.startedThreads.some(thread => instance.runtime.threads.indexOf(thread) !== -1); if (waiting) { // If all threads are waiting for the next tick or later yield // for a tick as well. Otherwise yield until the next loop of // the threads. if (util.stackFrame.startedThreads.every(thread => instance.runtime.isWaitingThread(thread))) { util.yieldTick(); } else { util.yield(); } } } } } module.exports = Scratch3EventBlocks; /***/ }), /***/ "./src/blocks/scratch3_looks.js": /*!**************************************!*\ !*** ./src/blocks/scratch3_looks.js ***! \**************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { const Cast = __webpack_require__(/*! ../util/cast */ "./src/util/cast.js"); const Clone = __webpack_require__(/*! ../util/clone */ "./src/util/clone.js"); const RenderedTarget = __webpack_require__(/*! ../sprites/rendered-target */ "./src/sprites/rendered-target.js"); const uid = __webpack_require__(/*! ../util/uid */ "./src/util/uid.js"); const StageLayering = __webpack_require__(/*! ../engine/stage-layering */ "./src/engine/stage-layering.js"); const getMonitorIdForBlockWithArgs = __webpack_require__(/*! ../util/get-monitor-id */ "./src/util/get-monitor-id.js"); const MathUtil = __webpack_require__(/*! ../util/math-util */ "./src/util/math-util.js"); /** * @typedef {object} BubbleState - the bubble state associated with a particular target. * @property {Boolean} onSpriteRight - tracks whether the bubble is right or left of the sprite. * @property {?int} drawableId - the ID of the associated bubble Drawable, null if none. * @property {string} text - the text of the bubble. * @property {string} type - the type of the bubble, "say" or "think" * @property {?string} usageId - ID indicating the most recent usage of the say/think bubble. * Used for comparison when determining whether to clear a say/think bubble. */ class Scratch3LooksBlocks { constructor(runtime) { /** * The runtime instantiating this block package. * @type {Runtime} */ this.runtime = runtime; this._onTargetChanged = this._onTargetChanged.bind(this); this._onResetBubbles = this._onResetBubbles.bind(this); this._onTargetWillExit = this._onTargetWillExit.bind(this); this._updateBubble = this._updateBubble.bind(this); // Reset all bubbles on start/stop this.runtime.on('PROJECT_STOP_ALL', this._onResetBubbles); this.runtime.on('targetWasRemoved', this._onTargetWillExit); // Enable other blocks to use bubbles like ask/answer this.runtime.on(Scratch3LooksBlocks.SAY_OR_THINK, this._updateBubble); } /** * The default bubble state, to be used when a target has no existing bubble state. * @type {BubbleState} */ static get DEFAULT_BUBBLE_STATE() { return { drawableId: null, onSpriteRight: true, skinId: null, text: '', type: 'say', usageId: null }; } /** * The key to load & store a target's bubble-related state. * @type {string} */ static get STATE_KEY() { return 'Scratch.looks'; } /** * Event name for a text bubble being created or updated. * @const {string} */ static get SAY_OR_THINK() { // There are currently many places in the codebase which explicitly refer to this event by the string 'SAY', // so keep this as the string 'SAY' for now rather than changing it to 'SAY_OR_THINK' and breaking things. return 'SAY'; } /** * Limit for say bubble string. * @const {string} */ static get SAY_BUBBLE_LIMIT() { return 330; } /** * Limit for ghost effect * @const {object} */ static get EFFECT_GHOST_LIMIT() { return { min: 0, max: 100 }; } /** * Limit for brightness effect * @const {object} */ static get EFFECT_BRIGHTNESS_LIMIT() { return { min: -100, max: 100 }; } /** * @param {Target} target - collect bubble state for this target. Probably, but not necessarily, a RenderedTarget. * @returns {BubbleState} the mutable bubble state associated with that target. This will be created if necessary. * @private */ _getBubbleState(target) { let bubbleState = target.getCustomState(Scratch3LooksBlocks.STATE_KEY); if (!bubbleState) { bubbleState = Clone.simple(Scratch3LooksBlocks.DEFAULT_BUBBLE_STATE); target.setCustomState(Scratch3LooksBlocks.STATE_KEY, bubbleState); } return bubbleState; } /** * Handle a target which has moved. * @param {RenderedTarget} target - the target which has moved. * @private */ _onTargetChanged(target) { const bubbleState = this._getBubbleState(target); if (bubbleState.drawableId) { this._positionBubble(target); } } /** * Handle a target which is exiting. * @param {RenderedTarget} target - the target. * @private */ _onTargetWillExit(target) { const bubbleState = this._getBubbleState(target); if (bubbleState.drawableId && bubbleState.skinId) { this.runtime.renderer.destroyDrawable(bubbleState.drawableId, StageLayering.SPRITE_LAYER); this.runtime.renderer.destroySkin(bubbleState.skinId); bubbleState.drawableId = null; bubbleState.skinId = null; this.runtime.requestRedraw(); } target.removeListener(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this._onTargetChanged); } /** * Handle project start/stop by clearing all visible bubbles. * @private */ _onResetBubbles() { for (let n = 0; n < this.runtime.targets.length; n++) { const bubbleState = this._getBubbleState(this.runtime.targets[n]); bubbleState.text = ''; this._onTargetWillExit(this.runtime.targets[n]); } clearTimeout(this._bubbleTimeout); } /** * Position the bubble of a target. If it doesn't fit on the specified side, flip and rerender. * @param {!Target} target Target whose bubble needs positioning. * @private */ _positionBubble(target) { if (!target.visible) return; const bubbleState = this._getBubbleState(target); const [bubbleWidth, bubbleHeight] = this.runtime.renderer.getCurrentSkinSize(bubbleState.drawableId); let targetBounds; try { targetBounds = target.getBoundsForBubble(); } catch (error_) { // Bounds calculation could fail (e.g. on empty costumes), in that case // use the x/y position of the target. targetBounds = { left: target.x, right: target.x, top: target.y, bottom: target.y }; } const stageSize = this.runtime.renderer.getNativeSize(); const stageBounds = { left: -stageSize[0] / 2, right: stageSize[0] / 2, top: stageSize[1] / 2, bottom: -stageSize[1] / 2 }; if (bubbleState.onSpriteRight && bubbleWidth + targetBounds.right > stageBounds.right && targetBounds.left - bubbleWidth > stageBounds.left) { // Only flip if it would fit bubbleState.onSpriteRight = false; this._renderBubble(target); } else if (!bubbleState.onSpriteRight && targetBounds.left - bubbleWidth < stageBounds.left && bubbleWidth + targetBounds.right < stageBounds.right) { // Only flip if it would fit bubbleState.onSpriteRight = true; this._renderBubble(target); } else { this.runtime.renderer.updateDrawablePosition(bubbleState.drawableId, [bubbleState.onSpriteRight ? Math.max(stageBounds.left, // Bubble should not extend past left edge of stage Math.min(stageBounds.right - bubbleWidth, targetBounds.right)) : Math.min(stageBounds.right - bubbleWidth, // Bubble should not extend past right edge of stage Math.max(stageBounds.left, targetBounds.left - bubbleWidth)), // Bubble should not extend past the top of the stage Math.min(stageBounds.top, targetBounds.bottom + bubbleHeight)]); this.runtime.requestRedraw(); } } /** * Create a visible bubble for a target. If a bubble exists for the target, * just set it to visible and update the type/text. Otherwise create a new * bubble and update the relevant custom state. * @param {!Target} target Target who needs a bubble. * @return {undefined} Early return if text is empty string. * @private */ _renderBubble(target) { if (!this.runtime.renderer) return; const bubbleState = this._getBubbleState(target); const { type, text, onSpriteRight } = bubbleState; // Remove the bubble if target is not visible, or text is being set to blank. if (!target.visible || text === '') { this._onTargetWillExit(target); return; } if (bubbleState.skinId) { this.runtime.renderer.updateTextSkin(bubbleState.skinId, type, text, onSpriteRight, [0, 0]); } else { target.addListener(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this._onTargetChanged); bubbleState.drawableId = this.runtime.renderer.createDrawable(StageLayering.SPRITE_LAYER); bubbleState.skinId = this.runtime.renderer.createTextSkin(type, text, bubbleState.onSpriteRight, [0, 0]); this.runtime.renderer.updateDrawableSkinId(bubbleState.drawableId, bubbleState.skinId); } this._positionBubble(target); } /** * Properly format text for a text bubble. * @param {string} text The text to be formatted * @return {string} The formatted text * @private */ _formatBubbleText(text) { if (text === '') return text; // Non-integers should be rounded to 2 decimal places (no more, no less), unless they're small enough that // rounding would display them as 0.00. This matches 2.0's behavior: // https://github.com/scratchfoundation/scratch-flash/blob/2e4a402ceb205a042887f54b26eebe1c2e6da6c0/src/scratch/ScratchSprite.as#L579-L585 if (typeof text === 'number' && Math.abs(text) >= 0.01 && text % 1 !== 0) { text = text.toFixed(2); } // Limit the length of the string. text = String(text).substr(0, Scratch3LooksBlocks.SAY_BUBBLE_LIMIT); return text; } /** * The entry point for say/think blocks. Clears existing bubble if the text is empty. * Set the bubble custom state and then call _renderBubble. * @param {!Target} target Target that say/think blocks are being called on. * @param {!string} type Either "say" or "think" * @param {!string} text The text for the bubble, empty string clears the bubble. * @private */ _updateBubble(target, type, text) { const bubbleState = this._getBubbleState(target); bubbleState.type = type; bubbleState.text = this._formatBubbleText(text); bubbleState.usageId = uid(); this._renderBubble(target); } /** * Retrieve the block primitives implemented by this package. * @return {object.<string, Function>} Mapping of opcode to Function. */ getPrimitives() { return { looks_say: this.say, looks_sayforsecs: this.sayforsecs, looks_think: this.think, looks_thinkforsecs: this.thinkforsecs, looks_show: this.show, looks_hide: this.hide, looks_hideallsprites: () => {}, // legacy no-op block looks_switchcostumeto: this.switchCostume, looks_switchbackdropto: this.switchBackdrop, looks_switchbackdroptoandwait: this.switchBackdropAndWait, looks_nextcostume: this.nextCostume, looks_nextbackdrop: this.nextBackdrop, looks_changeeffectby: this.changeEffect, looks_seteffectto: this.setEffect, looks_cleargraphiceffects: this.clearEffects, looks_changesizeby: this.changeSize, looks_setsizeto: this.setSize, looks_changestretchby: () => {}, // legacy no-op blocks looks_setstretchto: () => {}, looks_gotofrontback: this.goToFrontBack, looks_goforwardbackwardlayers: this.goForwardBackwardLayers, looks_size: this.getSize, looks_costumenumbername: this.getCostumeNumberName, looks_backdropnumbername: this.getBackdropNumberName }; } getMonitored() { return { looks_size: { isSpriteSpecific: true, getId: targetId => "".concat(targetId, "_size") }, looks_costumenumbername: { isSpriteSpecific: true, getId: (targetId, fields) => getMonitorIdForBlockWithArgs("".concat(targetId, "_costumenumbername"), fields) }, looks_backdropnumbername: { getId: (_, fields) => getMonitorIdForBlockWithArgs('backdropnumbername', fields) } }; } say(args, util) { // @TODO in 2.0 calling say/think resets the right/left bias of the bubble this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, 'say', args.MESSAGE); } sayforsecs(args, util) { this.say(args, util); const target = util.target; const usageId = this._getBubbleState(target).usageId; return new Promise(resolve => { this._bubbleTimeout = setTimeout(() => { this._bubbleTimeout = null; // Clear say bubble if it hasn't been changed and proceed. if (this._getBubbleState(target).usageId === usageId) { this._updateBubble(target, 'say', ''); } resolve(); }, 1000 * args.SECS); }); } think(args, util) { this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, 'think', args.MESSAGE); } thinkforsecs(args, util) { this.think(args, util); const target = util.target; const usageId = this._getBubbleState(target).usageId; return new Promise(resolve => { this._bubbleTimeout = setTimeout(() => { this._bubbleTimeout = null; // Clear think bubble if it hasn't been changed and proceed. if (this._getBubbleState(target).usageId === usageId) { this._updateBubble(target, 'think', ''); } resolve(); }, 1000 * args.SECS); }); } show(args, util) { util.target.setVisible(true); this._renderBubble(util.target); } hide(args, util) { util.target.setVisible(false); this._renderBubble(util.target); } /** * Utility function to set the costume of a target. * Matches the behavior of Scratch 2.0 for different types of arguments. * @param {!Target} target Target to set costume to. * @param {Any} requestedCostume Costume requested, e.g., 0, 'name', etc. * @param {boolean=} optZeroIndex Set to zero-index the requestedCostume. * @return {Array.<!Thread>} Any threads started by this switch. */ _setCostume(target, requestedCostume, optZeroIndex) { if (typeof requestedCostume === 'number') { // Numbers should be treated as costume indices, always target.setCostume(optZeroIndex ? requestedCostume : requestedCostume - 1); } else { // Strings should be treated as costume names, where possible const costumeIndex = target.getCostumeIndexByName(requestedCostume.toString()); if (costumeIndex !== -1) { target.setCostume(costumeIndex); } else if (requestedCostume === 'next costume') { target.setCostume(target.currentCostume + 1); } else if (requestedCostume === 'previous costume') { target.setCostume(target.currentCostume - 1); // Try to cast the string to a number (and treat it as a costume index) // Pure whitespace should not be treated as a number // Note: isNaN will cast the string to a number before checking if it's NaN } else if (!(isNaN(requestedCostume) || Cast.isWhiteSpace(requestedCostume))) { target.setCostume(optZeroIndex ? Number(requestedCostume) : Number(requestedCostume) - 1); } } // Per 2.0, 'switch costume' can't start threads even in the Stage. return []; } /** * Utility function to set the backdrop of a target. * Matches the behavior of Scratch 2.0 for different types of arguments. * @param {!Target} stage Target to set backdrop to. * @param {Any} requestedBackdrop Backdrop requested, e.g., 0, 'name', etc. * @param {boolean=} optZeroIndex Set to zero-index the requestedBackdrop. * @return {Array.<!Thread>} Any threads started by this switch. */ _setBackdrop(stage, requestedBackdrop, optZeroIndex) { if (typeof requestedBackdrop === 'number') { // Numbers should be treated as backdrop indices, always stage.setCostume(optZeroIndex ? requestedBackdrop : requestedBackdrop - 1); } else { // Strings should be treated as backdrop names where possible const costumeIndex = stage.getCostumeIndexByName(requestedBackdrop.toString()); if (costumeIndex !== -1) { stage.setCostume(costumeIndex); } else if (requestedBackdrop === 'next backdrop') { stage.setCostume(stage.currentCostume + 1); } else if (requestedBackdrop === 'previous backdrop') { stage.setCostume(stage.currentCostume - 1); } else if (requestedBackdrop === 'random backdrop') { const numCostumes = stage.getCostumes().length; if (numCostumes > 1) { // Don't pick the current backdrop, so that the block // will always have an observable effect. const lowerBound = 0; const upperBound = numCostumes - 1; const costumeToExclude = stage.currentCostume; const nextCostume = MathUtil.inclusiveRandIntWithout(lowerBound, upperBound, costumeToExclude); stage.setCostume(nextCostume); } // Try to cast the string to a number (and treat it as a costume index) // Pure whitespace should not be treated as a number // Note: isNaN will cast the string to a number before checking if it's NaN } else if (!(isNaN(requestedBackdrop) || Cast.isWhiteSpace(requestedBackdrop))) { stage.setCostume(optZeroIndex ? Number(requestedBackdrop) : Number(requestedBackdrop) - 1); } } const newName = stage.getCostumes()[stage.currentCostume].name; return this.runtime.startHats('event_whenbackdropswitchesto', { BACKDROP: newName }); } switchCostume(args, util) { this._setCostume(util.target, args.COSTUME); } nextCostume(args, util) { this._setCostume(util.target, util.target.currentCostume + 1, true); } switchBackdrop(args) { this._setBackdrop(this.runtime.getTargetForStage(), args.BACKDROP); } switchBackdropAndWait(args, util) { // Have we run before, starting threads? if (!util.stackFrame.startedThreads) { // No - switch the backdrop. util.stackFrame.startedThreads = this._setBackdrop(this.runtime.getTargetForStage(), args.BACKDROP); if (util.stackFrame.startedThreads.length === 0) { // Nothing was started. return; } } // We've run before; check if the wait is still going on. const instance = this; // Scratch 2 considers threads to be waiting if they are still in // runtime.threads. Threads that have run all their blocks, or are // marked done but still in runtime.threads are still considered to // be waiting. const waiting = util.stackFrame.startedThreads.some(thread => instance.runtime.threads.indexOf(thread) !== -1); if (waiting) { // If all threads are waiting for the next tick or later yield // for a tick as well. Otherwise yield until the next loop of // the threads. if (util.stackFrame.startedThreads.every(thread => instance.runtime.isWaitingThread(thread))) { util.yieldTick(); } else { util.yield(); } } } nextBackdrop() { const stage = this.runtime.getTargetForStage(); this._setBackdrop(stage, stage.currentCostume + 1, true); } clampEffect(effect, value) { let clampedValue = value; switch (effect) { case 'ghost': clampedValue = MathUtil.clamp(value, Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.min, Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.max); break; case 'brightness': clampedValue = MathUtil.clamp(value, Scratch3LooksBlocks.EFFECT_BRIGHTNESS_LIMIT.min, Scratch3LooksBlocks.EFFECT_BRIGHTNESS_LIMIT.max); break; } return clampedValue; } changeEffect(args, util) { const effect = Cast.toString(args.EFFECT).toLowerCase(); const change = Cast.toNumber(args.CHANGE); if (!Object.prototype.hasOwnProperty.call(util.target.effects, effect)) return; let newValue = change + util.target.effects[effect]; newValue = this.clampEffect(effect, newValue); util.target.setEffect(effect, newValue); } setEffect(args, util) { const effect = Cast.toString(args.EFFECT).toLowerCase(); let value = Cast.toNumber(args.VALUE); value = this.clampEffect(effect, value); util.target.setEffect(effect, value); } clearEffects(args, util) { util.target.clearEffects(); } changeSize(args, util) { const change = Cast.toNumber(args.CHANGE); util.target.setSize(util.target.size + change); } setSize(args, util) { const size = Cast.toNumber(args.SIZE); util.target.setSize(size); } goToFrontBack(args, util) { if (!util.target.isStage) { if (args.FRONT_BACK === 'front') { util.target.goToFront(); } else { util.target.goToBack(); } } } goForwardBackwardLayers(args, util) { if (!util.target.isStage) { if (args.FORWARD_BACKWARD === 'forward') { util.target.goForwardLayers(Cast.toNumber(args.NUM)); } else { util.target.goBackwardLayers(Cast.toNumber(args.NUM)); } } } getSize(args, util) { return Math.round(util.target.size); } getBackdropNumberName(args) { const stage = this.runtime.getTargetForStage(); if (args.NUMBER_NAME === 'number') { return stage.currentCostume + 1; } // Else return name return stage.getCostumes()[stage.currentCostume].name; } getCostumeNumberName(args, util) { if (args.NUMBER_NAME === 'number') { return util.target.currentCostume + 1; } // Else return name return util.target.getCostumes()[util.target.currentCostume].name; } } module.exports = Scratch3LooksBlocks; /***/ }), /***/ "./src/blocks/scratch3_motion.js": /*!***************************************!*\ !*** ./src/blocks/scratch3_motion.js ***! \***************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { const Cast = __webpack_require__(/*! ../util/cast */ "./src/util/cast.js"); const MathUtil = __webpack_require__(/*! ../util/math-util */ "./src/util/math-util.js"); const Timer = __webpack_require__(/*! ../util/timer */ "./src/util/timer.js"); class Scratch3MotionBlocks { constructor(runtime) { /** * The runtime instantiating this block package. * @type {Runtime} */ this.runtime = runtime; } /** * Retrieve the block primitives implemented by this package. * @return {object.<string, Function>} Mapping of opcode to Function. */ getPrimitives() { return { motion_movesteps: this.moveSteps, motion_gotoxy: this.goToXY, motion_goto: this.goTo, motion_turnright: this.turnRight, motion_turnleft: this.turnLeft, motion_pointindirection: this.pointInDirection, motion_pointtowards: this.pointTowards, motion_glidesecstoxy: this.glide, motion_glideto: this.glideTo, motion_ifonedgebounce: this.ifOnEdgeBounce, motion_setrotationstyle: this.setRotationStyle, motion_changexby: this.changeX, motion_setx: this.setX, motion_changeyby: this.changeY, motion_sety: this.setY, motion_xposition: this.getX, motion_yposition: this.getY, motion_direction: this.getDirection, // Legacy no-op blocks: motion_scroll_right: () => {}, motion_scroll_up: () => {}, motion_align_scene: () => {}, motion_xscroll: () => {}, motion_yscroll: () => {} }; } getMonitored() { return { motion_xposition: { isSpriteSpecific: true, getId: targetId => "".concat(targetId, "_xposition") }, motion_yposition: { isSpriteSpecific: true, getId: targetId => "".concat(targetId, "_yposition") }, motion_direction: { isSpriteSpecific: true, getId: targetId => "".concat(targetId, "_direction") } }; } moveSteps(args, util) { const steps = Cast.toNumber(args.STEPS); const radians = MathUtil.degToRad(90 - util.target