Skip to content

Instantly share code, notes, and snippets.

@ThatBobo
Created December 29, 2025 11:24
Show Gist options
  • Select an option

  • Save ThatBobo/18ed0fdffae75d598d2ad9aed9987c40 to your computer and use it in GitHub Desktop.

Select an option

Save ThatBobo/18ed0fdffae75d598d2ad9aed9987c40 to your computer and use it in GitHub Desktop.
/* 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