diff --git a/extensions/Lily/MoreEvents.js b/extensions/Lily/MoreEvents.js new file mode 100644 index 0000000000..525d93e3de --- /dev/null +++ b/extensions/Lily/MoreEvents.js @@ -0,0 +1,594 @@ +// Name: More Events +// ID: lmsMoreEvents +// Description: Start your scripts in new ways. + +(function (Scratch) { + "use strict"; + + const vm = Scratch.vm; + const runtime = vm.runtime; + + const stopIcon = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAQlBMVEUAAAC/UFC8Q0OzTU24SEi4SEi3SEi4R0e4SEi4SEi4SEi4SEi7SUm8SUnMTk7MT0/OT0/PT0/gVVXiVVXsWVn///+CoOd2AAAAC3RSTlMAEBMUu7zLz9D8/dIXnJwAAAABYktHRBXl2PmjAAAAxklEQVRIx+3WwRKDIBAD0JWqVEOtWv7/W3twOqKwELzW3N9wYhORMMYiztgZUZMUAKxqmh5Kno/MG256nzI59Z2mB+BWH+XzUt5RhWoyQjFZkTQFkTBFERlCnAwlDoYUgaHFblpaeL86AK0MvNjMIABmT2cGIAAWniw3ucm/k9ovduEjXzgXtUfJmtrTt9VZzYH9FSB/xvfKZMsiLFmuko61zBTfucjL9RpXf6nEU2MhPxXS86J+kORmjz6V6seViOnG8oT7ApMcjsYZwhXCAAAAAElFTkSuQmCC"; + + // Source: + // https://github.com/TurboWarp/scratch-vm/blob/develop/src/io/keyboard.js + // https://github.com/TurboWarp/scratch-blocks/blob/develop/blocks_vertical/event.js + const validKeyboardInputs = [ + // Special Inputs + { text: "space", value: "space" }, + { text: "up arrow", value: "up arrow" }, + { text: "down arrow", value: "down arrow" }, + { text: "right arrow", value: "right arrow" }, + { text: "left arrow", value: "left arrow" }, + { text: "enter", value: "enter" }, + // TW: Extra Special Inputs + { text: "backspace", value: "backspace" }, + { text: "delete", value: "delete" }, + { text: "shift", value: "shift" }, + { text: "caps lock", value: "caps lock" }, + { text: "scroll lock", value: "scroll lock" }, + { text: "control", value: "control" }, + { text: "escape", value: "escape" }, + { text: "insert", value: "insert" }, + { text: "home", value: "home" }, + { text: "end", value: "end" }, + { text: "page up", value: "page up" }, + { text: "page down", value: "page down" }, + // Letter Keyboard Inputs + { text: "a", value: "a" }, + { text: "b", value: "b" }, + { text: "c", value: "c" }, + { text: "d", value: "d" }, + { text: "e", value: "e" }, + { text: "f", value: "f" }, + { text: "g", value: "g" }, + { text: "h", value: "h" }, + { text: "i", value: "i" }, + { text: "j", value: "j" }, + { text: "k", value: "k" }, + { text: "l", value: "l" }, + { text: "m", value: "m" }, + { text: "n", value: "n" }, + { text: "o", value: "o" }, + { text: "p", value: "p" }, + { text: "q", value: "q" }, + { text: "r", value: "r" }, + { text: "s", value: "s" }, + { text: "t", value: "t" }, + { text: "u", value: "u" }, + { text: "v", value: "v" }, + { text: "w", value: "w" }, + { text: "x", value: "x" }, + { text: "y", value: "y" }, + { text: "z", value: "z" }, + // Number Keyboard Inputs + { text: "0", value: "0" }, + { text: "1", value: "1" }, + { text: "2", value: "2" }, + { text: "3", value: "3" }, + { text: "4", value: "4" }, + { text: "5", value: "5" }, + { text: "6", value: "6" }, + { text: "7", value: "7" }, + { text: "8", value: "8" }, + { text: "9", value: "9" }, + ]; + + var lastValues = {}; + var runTimer = 0; + + class MoreEvents { + constructor() { + // Stop Sign Clicked contributed by @CST1229 + runtime.shouldExecuteStopClicked = true; + runtime.on("BEFORE_EXECUTE", () => { + runTimer++; + runtime.shouldExecuteStopClicked = false; + + runtime.startHats("lmsMoreEvents_forever"); + runtime.startHats("lmsMoreEvents_whileTrueFalse"); + runtime.startHats("lmsMoreEvents_whenValueChanged"); + runtime.startHats("lmsMoreEvents_everyDuration"); + runtime.startHats("lmsMoreEvents_whileKeyPressed"); + }); + runtime.on("PROJECT_START", () => { + runTimer = 0; + }); + runtime.on("PROJECT_STOP_ALL", () => { + runTimer = 0; + if (runtime.shouldExecuteStopClicked) + queueMicrotask(() => + runtime.startHats("lmsMoreEvents_whenStopClicked") + ); + }); + runtime.on("AFTER_EXECUTE", () => { + runtime.shouldExecuteStopClicked = true; + }); + const originalGreenFlag = vm.greenFlag; + vm.greenFlag = function () { + runtime.shouldExecuteStopClicked = false; + originalGreenFlag.call(this); + }; + } + + getInfo() { + return { + id: "lmsMoreEvents", + name: "More Events", + color1: "#FFBF00", + color2: "#E6AC00", + color3: "#CC9900", + blocks: [ + { + opcode: "whenStopClicked", + blockType: Scratch.BlockType.EVENT, + text: "when [STOP] clicked", + isEdgeActivated: false, + arguments: { + STOP: { + type: Scratch.ArgumentType.IMAGE, + dataURI: stopIcon, + }, + }, + }, + { + opcode: "forever", + blockType: Scratch.BlockType.EVENT, + text: "forever", + isEdgeActivated: false, + }, + + "---", + + { + opcode: "whenTrueFalse", + blockType: Scratch.BlockType.HAT, + text: "when [CONDITION] becomes [STATE]", + isEdgeActivated: true, + arguments: { + CONDITION: { + type: Scratch.ArgumentType.BOOLEAN, + }, + STATE: { + type: Scratch.ArgumentType.STRING, + menu: "boolean", + }, + }, + }, + { + opcode: "whileTrueFalse", + blockType: Scratch.BlockType.HAT, + text: "while [CONDITION] is [STATE]", + isEdgeActivated: false, + arguments: { + CONDITION: { + type: Scratch.ArgumentType.BOOLEAN, + }, + STATE: { + type: Scratch.ArgumentType.STRING, + menu: "boolean", + }, + }, + }, + + "---", + + { + opcode: "whenValueChanged", + blockType: Scratch.BlockType.HAT, + text: "when [INPUT] is changed", + isEdgeActivated: false, + arguments: { + INPUT: { + // Intentional: + // Encourages people to place a block + // (as opposed to typing a value) + type: null, + }, + }, + }, + { + opcode: "everyDuration", + blockType: Scratch.BlockType.HAT, + text: "every [DURATION] frames", + isEdgeActivated: false, + arguments: { + DURATION: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 3, + }, + }, + }, + + "---", + + { + opcode: "whenKeyAction", + blockType: Scratch.BlockType.HAT, + text: "when [KEY_OPTION] key [ACTION]", + isEdgeActivated: true, + arguments: { + KEY_OPTION: { + type: Scratch.ArgumentType.STRING, + defaultValue: "space", + menu: "keyboardButtons", + }, + ACTION: { + type: Scratch.ArgumentType.STRING, + menu: "action", + }, + }, + }, + { + opcode: "whileKeyPressed", + blockType: Scratch.BlockType.HAT, + text: "while [KEY_OPTION] key pressed", + isEdgeActivated: false, + arguments: { + KEY_OPTION: { + type: Scratch.ArgumentType.STRING, + defaultValue: "space", + menu: "keyboardButtons", + }, + }, + }, + + "---", + + { + opcode: "broadcastToTarget", + blockType: Scratch.BlockType.COMMAND, + text: "broadcast [BROADCAST_OPTION] to [TARGET]", + arguments: { + BROADCAST_OPTION: { + type: null, + }, + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: "targetMenu", + }, + }, + hideFromPalette: true, + }, + { + opcode: "broadcastToTargetAndWait", + blockType: Scratch.BlockType.COMMAND, + text: "broadcast [BROADCAST_OPTION] to [TARGET] and wait", + arguments: { + BROADCAST_OPTION: { + type: null, + }, + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: "targetMenu", + }, + }, + hideFromPalette: true, + }, + + "---", + + { + opcode: "broadcastData", + blockType: Scratch.BlockType.COMMAND, + text: "broadcast [BROADCAST_OPTION] with data [DATA]", + arguments: { + BROADCAST_OPTION: { + type: null, + }, + DATA: { + type: Scratch.ArgumentType.STRING, + }, + }, + hideFromPalette: true, + }, + { + opcode: "broadcastDataAndWait", + blockType: Scratch.BlockType.COMMAND, + text: "broadcast [BROADCAST_OPTION] with data [DATA] and wait", + arguments: { + BROADCAST_OPTION: { + type: null, + }, + DATA: { + type: Scratch.ArgumentType.STRING, + }, + }, + hideFromPalette: true, + }, + { + blockType: Scratch.BlockType.XML, + xml: '', + }, + { + opcode: "receivedData", + blockType: Scratch.BlockType.REPORTER, + text: "received data", + disableMonitor: true, + allowDropAnywhere: true, + }, + + "---", + + { + opcode: "broadcastDataToTarget", + blockType: Scratch.BlockType.COMMAND, + text: "broadcast [BROADCAST_OPTION] to [TARGET] with data [DATA]", + func: "broadcastToTarget", + arguments: { + BROADCAST_OPTION: { + type: null, + }, + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: "targetMenu", + }, + DATA: { + type: Scratch.ArgumentType.STRING, + }, + }, + hideFromPalette: true, + }, + { + opcode: "broadcastDataToTargetAndWait", + blockType: Scratch.BlockType.COMMAND, + text: "broadcast [BROADCAST_OPTION] to [TARGET] with data [DATA] and wait", + func: "broadcastToTargetAndWait", + arguments: { + BROADCAST_OPTION: { + type: null, + }, + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: "targetMenu", + }, + DATA: { + type: Scratch.ArgumentType.STRING, + }, + }, + hideFromPalette: true, + }, + { + blockType: Scratch.BlockType.XML, + xml: '', + }, + ], + menus: { + // Targets have acceptReporters: true + targetMenu: { + acceptReporters: true, + items: "_getTargets", + }, + keyboardButtons: { + acceptReporters: true, + items: validKeyboardInputs, + }, + // Attributes have acceptReporters: false + action: { + acceptReporters: false, + items: ["hit", "released"], + }, + boolean: { + acceptReporters: false, + items: ["true", "false"], + }, + state: { + acceptReporters: false, + items: ["enabled", "disabled"], + }, + }, + }; + } + + whenTrueFalse(args) { + return args.STATE === "true" ? args.CONDITION : !args.CONDITION; + } + + whileTrueFalse(args) { + return args.STATE === "true" ? args.CONDITION : !args.CONDITION; + } + + whenValueChanged(args, util) { + const blockId = util.thread.peekStack(); + if (!lastValues[blockId]) + lastValues[blockId] = Scratch.Cast.toString(args.INPUT); + if (lastValues[blockId] !== Scratch.Cast.toString(args.INPUT)) { + lastValues[blockId] = Scratch.Cast.toString(args.INPUT); + return true; + } + return false; + } + + everyDuration(args, util) { + const duration = Math.max( + Math.round(Scratch.Cast.toNumber(args.DURATION)), + 0 + ); + return !!(runTimer % duration === 0); + } + + whenKeyAction(args, util) { + const key = Scratch.Cast.toString(args.KEY_OPTION).toLowerCase(); + const pressed = util.ioQuery("keyboard", "getKeyIsDown", [key]); + return args.ACTION === "released" ? !pressed : pressed; + } + + whileKeyPressed(args, util) { + const key = Scratch.Cast.toString(args.KEY_OPTION).toLowerCase(); + return util.ioQuery("keyboard", "getKeyIsDown", [key]); + } + + broadcastToTarget(args, util) { + const broadcastOption = Scratch.Cast.toString(args.BROADCAST_OPTION); + if (!broadcastOption) return; + + const data = Scratch.Cast.toString(args.DATA); + console.log(data); + + const cloneTargets = this._getTargetFromMenu(args.TARGET).sprite.clones; + let startedThreads = []; + + for (const clone of cloneTargets) { + startedThreads = [ + ...startedThreads, + ...util.startHats( + "event_whenbroadcastreceived", + { + BROADCAST_OPTION: broadcastOption, + }, + clone + ), + ]; + if (data) { + startedThreads.forEach((thread) => (thread.receivedData = args.DATA)); + } + } + } + + broadcastToTargetAndWait(args, util) { + if (!util.stackFrame.broadcastVar) { + util.stackFrame.broadcastVar = Scratch.Cast.toString( + args.BROADCAST_OPTION + ); + } + + const spriteTarget = this._getTargetFromMenu(args.TARGET); + if (!spriteTarget) return; + const cloneTargets = spriteTarget.sprite.clones; + + const data = Scratch.Cast.toString(args.DATA); + + if (util.stackFrame.broadcastVar) { + const broadcastOption = util.stackFrame.broadcastVar; + if (!util.stackFrame.startedThreads) { + util.stackFrame.startedThreads = []; + for (const clone of cloneTargets) { + util.stackFrame.startedThreads = [ + ...util.stackFrame.startedThreads, + ...util.startHats( + "event_whenbroadcastreceived", + { + BROADCAST_OPTION: broadcastOption, + }, + clone + ), + ]; + if (data) { + util.stackFrame.startedThreads.forEach( + (thread) => (thread.receivedData = args.DATA) + ); + } + } + if (util.stackFrame.startedThreads.length === 0) { + return; + } + } + + const waiting = util.stackFrame.startedThreads.some( + (thread) => runtime.threads.indexOf(thread) !== -1 + ); + if (waiting) { + if ( + util.stackFrame.startedThreads.every((thread) => + runtime.isWaitingThread(thread) + ) + ) { + util.yieldTick(); + } else { + util.yield(); + } + } + } + } + + broadcastData(args, util) { + const broadcast = Scratch.Cast.toString(args.BROADCAST_OPTION); + if (!broadcast) return; + + const data = Scratch.Cast.toString(args.DATA); + + let threads = util.startHats("event_whenbroadcastreceived", { + BROADCAST_OPTION: broadcast, + }); + threads.forEach((thread) => (thread.receivedData = data)); + } + + broadcastDataAndWait(args, util) { + const data = Scratch.Cast.toString(args.DATA); + + if (!util.stackFrame.broadcastVar) { + util.stackFrame.broadcastVar = Scratch.Cast.toString( + args.BROADCAST_OPTION + ); + } + + if (util.stackFrame.broadcastVar) { + const broadcastOption = util.stackFrame.broadcastVar; + if (!util.stackFrame.startedThreads) { + util.stackFrame.startedThreads = util.startHats( + "event_whenbroadcastreceived", + { + BROADCAST_OPTION: broadcastOption, + } + ); + if (util.stackFrame.startedThreads.length === 0) { + return; + } else { + util.stackFrame.startedThreads.forEach( + (thread) => (thread.receivedData = data) + ); + } + } + + const waiting = util.stackFrame.startedThreads.some( + (thread) => runtime.threads.indexOf(thread) !== -1 + ); + if (waiting) { + if ( + util.stackFrame.startedThreads.every((thread) => + runtime.isWaitingThread(thread) + ) + ) { + util.yieldTick(); + } else { + util.yield(); + } + } + } + } + + receivedData(args, util) { + const received = util.thread.receivedData; + return received ? received : ""; + } + + _getTargetFromMenu(targetName) { + let target = Scratch.vm.runtime.getSpriteTargetByName(targetName); + if (targetName === "_stage_") target = runtime.getTargetForStage(); + return target; + } + + _getTargets() { + const spriteNames = [{ text: "Stage", value: "_stage_" }]; + const targets = Scratch.vm.runtime.targets; + for (let index = 1; index < targets.length; index++) { + const target = targets[index]; + if (target.isOriginal) { + const targetName = target.getName(); + spriteNames.push({ + text: targetName, + value: targetName, + }); + } + } + if (spriteNames.length > 0) { + return spriteNames; + } else { + return [{ text: "", value: 0 }]; //this should never happen but it's a failsafe + } + } + } + + Scratch.extensions.register(new MoreEvents()); +})(Scratch); diff --git a/extensions/extensions.json b/extensions/extensions.json index 73ce264655..6418f9577d 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -22,6 +22,7 @@ "obviousAlexC/SensingPlus", "Lily/ClonesPlus", "Lily/LooksPlus", + "Lily/MoreEvents", "NexusKitten/moremotion", "navigator", "battery", diff --git a/images/Lily/MoreEvents.svg b/images/Lily/MoreEvents.svg new file mode 100644 index 0000000000..82ec5703a2 --- /dev/null +++ b/images/Lily/MoreEvents.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/README.md b/images/README.md index 23ce9387e3..c2af2ec5c8 100644 --- a/images/README.md +++ b/images/README.md @@ -264,3 +264,7 @@ All images in this folder are licensed under the [GNU General Public License ver ## Lily/AllMenus.svg - Created by [YogaindoCR](https://github.com/YogaindoCR) in https://github.com/TurboWarp/extensions/issues/90#issuecomment-1681839774 + +## Lily/MoreEvents.svg + - Created by [@LilyMakesThings](https://github.com/LilyMakesThings). + - Background "blobs" by Scratch. \ No newline at end of file