Created
December 29, 2025 11:24
-
-
Save ThatBobo/18ed0fdffae75d598d2ad9aed9987c40 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* story-weaver-extension.js | |
| Story Weaver extension (for PenguinMod extra extensions) | |
| - Adds localStorage persistence | |
| - Safer removal behavior (removes all occurrences) | |
| - Optional alerts (showAlerts = false by default) | |
| - Preserves original block opcodes/IDs | |
| */ | |
| (async function (Scratch) { | |
| // Configuration | |
| const STORAGE_KEY = "story-weaver:v1"; | |
| const showAlerts = false; // set true to keep alert() behavior, false uses console.log | |
| if (!Scratch.extensions || !Scratch.extensions.unsandboxed) { | |
| const msg = "This extension needs to be unsandboxed to run (or set showAlerts=false)."; | |
| if (showAlerts) alert(msg); else console.warn(msg); | |
| } | |
| const ExtForge = { | |
| Broadcasts: new function () { | |
| this.raw = {}; | |
| this.register = (name, blocks) => { this.raw[name] = blocks; }; | |
| this.execute = async (name) => { if (this.raw[name]) { await this.raw[name](); } }; | |
| }, | |
| Variables: new function () { | |
| this.raw = {}; | |
| this.set = (name, value) => { this.raw[name] = value; }; | |
| this.get = (name) => { return this.raw.hasOwnProperty(name) ? this.raw[name] : null; }; | |
| }, | |
| Vector: class { | |
| constructor(x = 0, y = 0) { this.x = x; this.y = y; } | |
| static from(v) { | |
| if (v instanceof ExtForge.Vector) return v; | |
| if (Array.isArray(v)) return new ExtForge.Vector(Number(v[0]) || 0, Number(v[1]) || 0); | |
| if (v && typeof v === "object") return new ExtForge.Vector(Number(v.x) || 0, Number(v.y) || 0); | |
| return new ExtForge.Vector(); | |
| } | |
| add(v) { v = ExtForge.Vector.from(v); return new ExtForge.Vector(this.x + v.x, this.y + v.y); } | |
| set(x, y) { return new ExtForge.Vector(x ?? this.x, y ?? this.y); } | |
| }, | |
| Utils: { | |
| setList: (list, index, value) => { list[index] = value; return list; }, | |
| lists_foreach: { index: [0], value: [null], depth: 0 }, | |
| countString: (x, y) => { return y.length === 0 ? 0 : x.split(y).length - 1; } | |
| } | |
| }; | |
| function loadState() { | |
| try { | |
| const raw = localStorage.getItem(STORAGE_KEY); | |
| if (!raw) return {}; | |
| return JSON.parse(raw) || {}; | |
| } catch (e) { | |
| console.warn("Failed to load Story Weaver state:", e); | |
| return {}; | |
| } | |
| } | |
| function saveState(state) { | |
| try { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); | |
| } catch (e) { | |
| console.warn("Failed to save Story Weaver state:", e); | |
| } | |
| } | |
| const persisted = loadState(); | |
| ExtForge.Variables.set("Stories", persisted.stories || []); | |
| ExtForge.Variables.set("Story title", persisted.storyTitle || ""); | |
| ExtForge.Variables.set("Text to write", persisted.textToWrite || ""); | |
| ExtForge.Variables.set("Current story", persisted.currentStory || null); | |
| function persistAll() { | |
| saveState({ | |
| stories: ExtForge.Variables.get("Stories") || [], | |
| storyTitle: ExtForge.Variables.get("Story title") || "", | |
| textToWrite: ExtForge.Variables.get("Text to write") || "", | |
| currentStory: ExtForge.Variables.get("Current story") || null | |
| }); | |
| } | |
| function infoLog(msg) { | |
| if (showAlerts) alert(msg); else console.log("[Story Weaver] " + msg); | |
| } | |
| class Extension { | |
| getInfo() { | |
| return { | |
| id: "e65ne8edk37", | |
| name: "Story Weaver", | |
| color1: "#00ffff", | |
| blocks: [ | |
| { | |
| opcode: "block_dbe7cc6f9af127a7", | |
| text: "Write: [6a0063cc118c5c60]", | |
| blockType: "command", | |
| arguments: { "6a0063cc118c5c60": { type: "string", defaultValue: "" } } | |
| }, | |
| { | |
| opcode: "block_70c0e1c5a32b88ec", | |
| text: "Remove: [25955655b5ed7326]", | |
| blockType: "command", | |
| arguments: { "25955655b5ed7326": { type: "string" } } | |
| }, | |
| { | |
| opcode: "block_6e3ff48a02165de6", | |
| text: "Start new story", | |
| blockType: "command", | |
| arguments: {} | |
| }, | |
| { | |
| opcode: "block_1b61abc26d5e64f0", | |
| text: "Set story title to: [dae557575f4fb031]", | |
| blockType: "command", | |
| arguments: { "dae557575f4fb031": { type: "string" } } | |
| }, | |
| { | |
| opcode: "block_43543f481c03f3b2", | |
| text: "Story done", | |
| blockType: "command", | |
| arguments: {} | |
| }, | |
| { | |
| opcode: "block_showStory", | |
| text: "Show story", | |
| blockType: "command", | |
| arguments: {} | |
| }, | |
| { | |
| opcode: "block_getStories", | |
| text: "Stories", | |
| blockType: "reporter", | |
| arguments: {} | |
| }, | |
| { | |
| opcode: "block_switchStory", | |
| text: "Switch to story [storyTitle]", | |
| blockType: "command", | |
| arguments: { storyTitle: { type: "string", defaultValue: "" } } | |
| }, | |
| { | |
| opcode: "block_getCurrentStory", | |
| text: "Current story", | |
| blockType: "reporter", | |
| arguments: {} | |
| } | |
| ], | |
| menus: {} | |
| }; | |
| } | |
| async block_dbe7cc6f9af127a7(args) { | |
| const toWrite = String(args["6a0063cc118c5c60"] || ""); | |
| let story = ExtForge.Variables.get("Current story"); | |
| if (story) { | |
| story.text = String(story.text || "") + toWrite; | |
| ExtForge.Variables.set("Current story", story); | |
| ExtForge.Variables.set("Text to write", story.text); | |
| } else { | |
| const prev = String(ExtForge.Variables.get("Text to write") || ""); | |
| ExtForge.Variables.set("Text to write", prev + toWrite); | |
| } | |
| persistAll(); | |
| infoLog('Wrote "' + toWrite + '" to story'); | |
| } | |
| async block_70c0e1c5a32b88ec(args) { | |
| const remove = String(args["25955655b5ed7326"] || ""); | |
| if (remove.length === 0) { | |
| infoLog("Remove string is empty — nothing removed."); | |
| return; | |
| } | |
| let text = String(ExtForge.Variables.get("Text to write") || ""); | |
| const newText = text.split(remove).join(""); | |
| ExtForge.Variables.set("Text to write", newText); | |
| let story = ExtForge.Variables.get("Current story"); | |
| if (story) { | |
| story.text = newText; | |
| ExtForge.Variables.set("Current story", story); | |
| } | |
| persistAll(); | |
| infoLog('Removed "' + remove + '" from story'); | |
| } | |
| async block_6e3ff48a02165de6() { | |
| ExtForge.Variables.set("Story title", ""); | |
| ExtForge.Variables.set("Text to write", ""); | |
| ExtForge.Variables.set("Current story", null); | |
| persistAll(); | |
| infoLog("Started new story (cleared draft)"); | |
| } | |
| async block_1b61abc26d5e64f0(args) { | |
| const title = String(args["dae557575f4fb031"] || ""); | |
| ExtForge.Variables.set("Story title", title); | |
| persistAll(); | |
| infoLog('Story title set to "' + title + '"'); | |
| } | |
| async block_43543f481c03f3b2() { | |
| const stories = ExtForge.Variables.get("Stories") || []; | |
| const title = ExtForge.Variables.get("Story title") || "(no title)"; | |
| const text = ExtForge.Variables.get("Text to write") || "(no text yet)"; | |
| stories.push({ title: String(title), text: String(text) }); | |
| ExtForge.Variables.set("Stories", stories); | |
| ExtForge.Variables.set("Story title", ""); | |
| ExtForge.Variables.set("Text to write", ""); | |
| ExtForge.Variables.set("Current story", null); | |
| persistAll(); | |
| infoLog("Saved story \"" + title + "\""); | |
| } | |
| async block_showStory() { | |
| const story = ExtForge.Variables.get("Current story"); | |
| if (story) { | |
| infoLog((story.title || "(no title)") + "\n" + (story.text || "")); | |
| if (showAlerts) alert((story.title || "(no title)") + "\n" + (story.text || "")); | |
| } else { | |
| const title = ExtForge.Variables.get("Story title") || "(no title)"; | |
| const text = ExtForge.Variables.get("Text to write") || "(no text yet)"; | |
| infoLog(title + "\n" + text); | |
| if (showAlerts) alert(title + "\n" + text); | |
| } | |
| } | |
| async block_getStories() { | |
| const stories = ExtForge.Variables.get("Stories") || []; | |
| return stories.map(s => s.title || "(no title)"); | |
| } | |
| async block_switchStory(args) { | |
| const storyTitle = String(args["storyTitle"] || ""); | |
| const stories = ExtForge.Variables.get("Stories") || []; | |
| const found = stories.find(s => s.title === storyTitle); | |
| if (found) { | |
| ExtForge.Variables.set("Story title", found.title); | |
| ExtForge.Variables.set("Text to write", found.text); | |
| ExtForge.Variables.set("Current story", { title: found.title, text: found.text }); | |
| persistAll(); | |
| infoLog("Switched to: " + found.title + "\n" + found.text); | |
| if (showAlerts) alert("Switched to: " + found.title + "\n" + found.text); | |
| } else { | |
| infoLog("Story not found: " + storyTitle); | |
| if (showAlerts) alert("Story not found"); | |
| } | |
| } | |
| async block_getCurrentStory() { | |
| const story = ExtForge.Variables.get("Current story"); | |
| if (story) { | |
| return (story.title || "(no title)") + "\n" + (story.text || ""); | |
| } else { | |
| const title = ExtForge.Variables.get("Story title") || "(no title)"; | |
| const text = ExtForge.Variables.get("Text to write") || "(no text yet)"; | |
| return title + "\n" + text; | |
| } | |
| } | |
| } | |
| Scratch.extensions.register(new Extension()); | |
| })(Scratch); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment