const Cast = require('../util/cast'); const SandboxRunner = require('../util/sandboxed-javascript-runner.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' }); }); this.runtime.on('KEY_HIT', key => { this.runtime.startHats('event_whenkeyhit', { KEY_OPTION: key }); this.runtime.startHats('event_whenkeyhit', { KEY_OPTION: 'any' }); }); this.isStarting = false; this.runtime.on('PROJECT_START_BEFORE_RESET', () => { // we need to remember that the project is starting // otherwise the stop block will run when flag is clicked this.isStarting = true; }); this.runtime.on('PROJECT_STOP_ALL', () => { // if green flag is clicked, dont bother starting the hat if (this.isStarting) { this.isStarting = false; return; } // we need to wait for runtime to step once // otherwise the hat will be stopped as soon as it starts this.runtime.once('RUNTIME_STEP_START', () => { this.runtime.startHats('event_whenstopclicked'); }) this.isStarting = false; }); this.runtime.on('RUNTIME_STEP_START', () => { this.runtime.startHats('event_always'); }); this.runtime.on("AFTER_EXECUTE", () => { // Use a timeout as regular Block Threads and Events Blocks dont run at the Same Speed setTimeout(() => { const stage = this.runtime.getTargetForStage(); if (!stage) return; // happens when project is loading const stageVars = stage.variables; for (const key in stageVars) { if (stageVars[key].isSent !== undefined) stageVars[key].isSent = false; } }, 10); }); } /** * Retrieve the block primitives implemented by this package. * @return {object.} Mapping of opcode to Function. */ getPrimitives () { return { event_whenanything: this.whenanything, event_whenjavascript: this.whenjavascript, event_whentouchingobject: this.touchingObject, event_broadcast: this.broadcast, event_broadcastandwait: this.broadcastAndWait, event_whengreaterthan: this.hatGreaterThanPredicate }; } whenanything (args) { return Cast.toBoolean(args.ANYTHING); } whenjavascript (args) { return new Promise((resolve) => { const js = Cast.toString(args.JS); SandboxRunner.execute(js).then(result => { resolve(result.value === true); }) }) } getHats () { return { event_whenflagclicked: { restartExistingThreads: true }, event_whenstopclicked: { restartExistingThreads: true }, event_always: { restartExistingThreads: false }, event_whenkeypressed: { restartExistingThreads: false }, event_whenkeyhit: { restartExistingThreads: false }, event_whenmousescrolled: { restartExistingThreads: false }, event_whenanything: { restartExistingThreads: false, edgeActivated: true }, event_whenjavascript: { restartExistingThreads: false, edgeActivated: true }, 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; broadcastVar.isSent = true; 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) { broadcastVar.isSent = true; // 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;