Advanced SugarCube debug panel with UI, configuration, and real-time variable inspection
// ==UserScript==
// @name SugarCube Game Engine Debugger
// @version 1.0.5
// @description Advanced SugarCube debug panel with UI, configuration, and real-time variable inspection
// @author X Death
// @match file:///*.html
// @match file:///*.htm
// @icon https://f95zone.to/data/avatars/l/1963/1963870.jpg
// @run-at document-idle
// @license GPL-3.0-or-later
// @namespace https://github.com/Zenix-Al/SugarCube-Game-Engine-Debugger
// @homepage https://github.com/Zenix-Al/SugarCube-Game-Engine-Debugger
// @supportURL https://github.com/Zenix-Al/SugarCube-Game-Engine-Debugger
// ==/UserScript==
// ------------------------------------------------------------
// Built on 2026-05-31 14:54:17 UTC -- AUTO-GENERATED, edit /src then rebuild
// Mode: build
// ------------------------------------------------------------
(() => {
// src/logger.js
var debugLoggingEnabled = false;
function safeCall(method, args) {
try {
method.apply(console, args);
} catch {
}
}
function setDebugLoggingEnabled(enabled) {
debugLoggingEnabled = Boolean(enabled);
}
function debugLog(...args) {
if (!debugLoggingEnabled) return;
safeCall(console.log, args);
}
function debugInfo(...args) {
if (!debugLoggingEnabled) return;
safeCall(console.info || console.log, args);
}
function debugWarn(...args) {
if (!debugLoggingEnabled) return;
safeCall(console.warn || console.log, args);
}
function debugError(...args) {
if (!debugLoggingEnabled) return;
safeCall(console.error || console.log, args);
}
// src/config.js
var DebuggerConfig = class {
constructor() {
this.enabled = true;
this.settings = {
showUnderlines: true,
showConditions: true,
showVariables: true,
showLinks: true,
showNavControls: false,
debugLogging: true,
forceHistory: false,
historyMaxStates: 120,
lastTab: "debug",
underlineColor: "#00ff00",
underlineStyle: "dashed"
};
this.load();
}
toggle() {
this.enabled = !this.enabled;
return this.enabled;
}
getSetting(key) {
return this.settings[key] ?? null;
}
setSetting(key, value) {
this.settings[key] = value;
this.save();
}
updateSettings(obj) {
Object.assign(this.settings, obj);
this.save();
}
getSettings() {
return { ...this.settings };
}
save() {
try {
localStorage.setItem("sugarcubeDebugger", JSON.stringify(this.settings));
} catch (e) {
debugWarn("Could not save debugger settings:", e);
}
}
load() {
try {
const saved = localStorage.getItem("sugarcubeDebugger");
if (saved) {
this.settings = Object.assign(this.settings, JSON.parse(saved));
}
} catch (e) {
debugWarn("Could not load debugger settings:", e);
}
}
};
var config_default = new DebuggerConfig();
// src/modal.js
var DebuggerModal = class {
constructor(mountRoot = document.body) {
this.isOpen = false;
this.modalEl = null;
this.backdropEl = null;
this.onClose = null;
this.onSettingsChange = null;
this.mountRoot = mountRoot;
this.suppressSettingSync = false;
}
create() {
const backdrop = document.createElement("div");
backdrop.id = "sc-debugger-backdrop";
backdrop.className = "sc-debugger-backdrop";
backdrop.addEventListener("click", () => this.close());
const modal = document.createElement("div");
modal.id = "sc-debugger-modal";
modal.className = "sc-debugger-modal";
modal.innerHTML = `
<div class="sc-debugger-header">
<h2>SugarCube Debugger</h2>
<button class="sc-debugger-close" id="sc-debugger-close-btn" type="button" aria-label="Close debugger">×</button>
</div>
<div class="sc-debugger-tabs">
<button class="sc-debugger-tab-btn active" data-tab="debug" type="button">Debug Info</button>
<button class="sc-debugger-tab-btn" data-tab="editor" type="button">Editor</button>
<button class="sc-debugger-tab-btn" data-tab="links" type="button">Links</button>
<button class="sc-debugger-tab-btn" data-tab="settings" type="button">Settings</button>
</div>
<div class="sc-debugger-content">
<div id="sc-debugger-debug-tab" class="sc-debugger-tab-pane active">
<div id="sc-debugger-output"></div>
</div>
<div id="sc-debugger-editor-tab" class="sc-debugger-tab-pane">
<div id="sc-debugger-editor-output"></div>
</div>
<div id="sc-debugger-links-tab" class="sc-debugger-tab-pane">
<div class="sc-debugger-warning" id="sc-debugger-links-warning"></div>
<div id="sc-debugger-links-explorer"></div>
</div>
<div id="sc-debugger-settings-tab" class="sc-debugger-tab-pane">
<div class="sc-debugger-settings-group">
<label class="sc-debugger-switch">
<input type="checkbox" id="sc-show-underlines" />
<span class="sc-debugger-slider"></span>
Show Link Underlines
</label>
<label class="sc-debugger-switch">
<input type="checkbox" id="sc-show-conditions" />
<span class="sc-debugger-slider"></span>
Show Conditions
</label>
<label class="sc-debugger-switch">
<input type="checkbox" id="sc-show-variables" />
<span class="sc-debugger-slider"></span>
Show Variables
</label>
<label class="sc-debugger-switch">
<input type="checkbox" id="sc-show-links" />
<span class="sc-debugger-slider"></span>
Show Links In Debug Tab
</label>
<label class="sc-debugger-switch">
<input type="checkbox" id="sc-show-nav-controls" />
<span class="sc-debugger-slider"></span>
Show Back/Forward Buttons
</label>
<label class="sc-debugger-switch">
<input type="checkbox" id="sc-debug-logging" />
<span class="sc-debugger-slider"></span>
Debug Logging (Console)
</label>
<label class="sc-debugger-switch">
<input type="checkbox" id="sc-force-history" />
<span class="sc-debugger-slider"></span>
Force Enable History Navigation
</label>
</div>
<div class="sc-debugger-number-group">
<label for="sc-history-max-states">Forced History Max States:</label>
<input
type="number"
id="sc-history-max-states"
class="sc-debugger-number-input"
min="2"
max="2000"
step="1"
value="120"
/>
<p class="sc-debugger-setting-note">
Applies to future turns. Use at least 2 states for usable back/forward history.
</p>
</div>
<div class="sc-debugger-color-group">
<label for="sc-underline-color">Underline Color:</label>
<input type="color" id="sc-underline-color" value="#00ff00" />
</div>
</div>
</div>
`;
this.mountRoot.append(backdrop, modal);
this.backdropEl = backdrop;
this.modalEl = modal;
this.attachEventListeners();
}
attachEventListeners() {
const modal = this.modalEl;
if (!modal) return;
const closeBtn = modal.querySelector("#sc-debugger-close-btn");
closeBtn?.addEventListener("click", () => this.close());
modal.querySelectorAll(".sc-debugger-tab-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
const tab = e.currentTarget?.getAttribute("data-tab");
if (tab) this.switchTab(tab);
});
});
const bindCheckbox = (id, setting) => {
const el = modal.querySelector(id);
el?.addEventListener("change", (e) => {
const checked = e.target?.checked ?? false;
this.handleSettingChange(setting, checked);
});
};
bindCheckbox("#sc-show-underlines", "showUnderlines");
bindCheckbox("#sc-show-conditions", "showConditions");
bindCheckbox("#sc-show-variables", "showVariables");
bindCheckbox("#sc-show-links", "showLinks");
bindCheckbox("#sc-show-nav-controls", "showNavControls");
bindCheckbox("#sc-debug-logging", "debugLogging");
bindCheckbox("#sc-force-history", "forceHistory");
const historyMaxInput = modal.querySelector("#sc-history-max-states");
historyMaxInput?.addEventListener("change", (e) => {
const raw = Number.parseInt(String(e.target?.value ?? ""), 10);
const nextValue = Number.isFinite(raw) ? Math.min(Math.max(raw, 2), 2e3) : 120;
e.target.value = String(nextValue);
this.handleSettingChange("historyMaxStates", nextValue);
});
const color = modal.querySelector("#sc-underline-color");
color?.addEventListener("change", (e) => {
this.handleSettingChange("underlineColor", e.target?.value);
});
}
handleSettingChange(setting, value) {
if (this.onSettingsChange) {
this.onSettingsChange(setting, value);
}
}
switchTab(tab) {
if (!this.modalEl) return;
const normalized = ["debug", "editor", "links", "settings"].includes(tab) ? tab : "debug";
this.modalEl.querySelectorAll(".sc-debugger-tab-pane").forEach((el) => el.classList.remove("active"));
this.modalEl.querySelectorAll(".sc-debugger-tab-btn").forEach((el) => el.classList.remove("active"));
this.modalEl.querySelector(`#sc-debugger-${normalized}-tab`)?.classList.add("active");
this.modalEl.querySelector(`[data-tab="${normalized}"]`)?.classList.add("active");
if (!this.suppressSettingSync) {
this.handleSettingChange("lastTab", normalized);
}
}
updateSettings(settings) {
if (!this.modalEl) return;
const setChecked = (id, val) => {
const el = this.modalEl.querySelector(id);
if (el) el.checked = Boolean(val);
};
setChecked("#sc-show-underlines", settings.showUnderlines);
setChecked("#sc-show-conditions", settings.showConditions);
setChecked("#sc-show-variables", settings.showVariables);
setChecked("#sc-show-links", settings.showLinks);
setChecked("#sc-show-nav-controls", settings.showNavControls);
setChecked("#sc-debug-logging", settings.debugLogging);
setChecked("#sc-force-history", settings.forceHistory);
const historyMaxInput = this.modalEl.querySelector("#sc-history-max-states");
if (historyMaxInput) {
const raw = Number.parseInt(String(settings.historyMaxStates ?? 120), 10);
const clamped = Number.isFinite(raw) ? Math.min(Math.max(raw, 2), 2e3) : 120;
historyMaxInput.value = String(clamped);
}
const color = this.modalEl.querySelector("#sc-underline-color");
if (color) color.value = settings.underlineColor;
this.suppressSettingSync = true;
this.switchTab(settings.lastTab || "debug");
this.suppressSettingSync = false;
}
setContent(html) {
if (!this.modalEl) return;
const out = this.modalEl.querySelector("#sc-debugger-output");
if (out) out.innerHTML = html;
}
setEditorContent(html) {
if (!this.modalEl) return;
const out = this.modalEl.querySelector("#sc-debugger-editor-output");
if (out) out.innerHTML = html;
}
setLinksExplorerContent(html, warningText) {
if (!this.modalEl) return;
const out = this.modalEl.querySelector("#sc-debugger-links-explorer");
if (out) out.innerHTML = html;
const warn = this.modalEl.querySelector("#sc-debugger-links-warning");
if (warn) warn.textContent = warningText || "";
}
open() {
if (!this.modalEl) {
this.create();
}
if (this.backdropEl) this.backdropEl.style.display = "block";
if (this.modalEl) this.modalEl.style.display = "flex";
this.isOpen = true;
}
close() {
if (this.backdropEl) {
this.backdropEl.remove();
this.backdropEl = null;
}
if (this.modalEl) {
this.modalEl.remove();
this.modalEl = null;
}
this.isOpen = false;
if (this.onClose) {
this.onClose();
}
}
destroy() {
if (this.backdropEl) {
this.backdropEl.remove();
this.backdropEl = null;
}
if (this.modalEl) {
this.modalEl.remove();
this.modalEl = null;
}
this.isOpen = false;
}
};
var modal_default = DebuggerModal;
// src/passageAnalyzer.js
var PassageAnalyzer = class {
static analyzeConditions(sourceText) {
const conditionRegex = /<<(if|elseif|else|\/if)[\s\S]*?>>/gi;
const matches = sourceText.match(conditionRegex) || [];
return {
found: matches.length > 0,
conditions: matches.map((match) => ({
raw: match,
type: this.getConditionType(match)
}))
};
}
static analyzeConditionBlocks(sourceText) {
const source = String(sourceText || "");
const tokenRegex = /<<\s*(if|elseif|else|\/if)\b([\s\S]*?)>>/gi;
const stack = [];
const blocks = [];
let match;
while ((match = tokenRegex.exec(source)) !== null) {
const tokenType = String(match[1] || "").toLowerCase();
const tokenArgs = String(match[2] || "").trim();
const tokenRaw = String(match[0] || "");
const tokenStart = match.index;
const tokenEnd = tokenRegex.lastIndex;
if (tokenType === "if") {
const block = {
depth: stack.length,
start: tokenStart,
end: null,
branches: [],
openingToken: tokenRaw
};
const branch = {
type: "if",
condition: tokenArgs,
tokenRaw,
contentStart: tokenEnd,
contentEnd: source.length,
summary: null
};
block.branches.push(branch);
block.currentBranch = branch;
stack.push(block);
continue;
}
const current = stack[stack.length - 1];
if (!current) continue;
if (tokenType === "elseif" || tokenType === "else") {
if (current.currentBranch) {
current.currentBranch.contentEnd = tokenStart;
}
const branch = {
type: tokenType,
condition: tokenArgs,
tokenRaw,
contentStart: tokenEnd,
contentEnd: source.length,
summary: null
};
current.branches.push(branch);
current.currentBranch = branch;
continue;
}
if (tokenType === "/if") {
if (current.currentBranch) {
current.currentBranch.contentEnd = tokenStart;
}
current.end = tokenEnd;
stack.pop();
blocks.push(current);
}
}
while (stack.length > 0) {
const dangling = stack.pop();
dangling.end = source.length;
if (dangling.currentBranch) {
dangling.currentBranch.contentEnd = source.length;
}
blocks.push(dangling);
}
const enriched = blocks.sort((a, b) => a.start - b.start).map((block, idx) => ({
id: idx + 1,
depth: block.depth,
start: block.start,
end: block.end,
openingToken: block.openingToken,
branches: block.branches.map((branch) => {
const body = source.slice(branch.contentStart, branch.contentEnd);
return {
type: branch.type,
condition: branch.condition,
tokenRaw: branch.tokenRaw,
summary: this.summarizeBranchBody(body)
};
})
}));
return {
found: enriched.length > 0,
blocks: enriched
};
}
static getConditionType(text) {
if (text.startsWith("<<if")) return "if";
if (text.startsWith("<<elseif")) return "elseif";
if (text.startsWith("<<else")) return "else";
if (text.startsWith("<</if")) return "endif";
return "unknown";
}
static getPassageVariables() {
const vars = SugarCube?.State?.variables || {};
const cleaned = {};
for (const key in vars) {
if (!Object.prototype.hasOwnProperty.call(vars, key)) continue;
if (typeof vars[key] === "function" || key === "widget" || key.toLowerCase().includes("widget")) {
continue;
}
cleaned[key] = vars[key];
}
return cleaned;
}
static getTemporaryVariables() {
return { ...SugarCube?.State?.temporary || {} };
}
static getReferencedVariableNamesFromSource(sourceText) {
const story = /* @__PURE__ */ new Set();
const temp = /* @__PURE__ */ new Set();
const storyVarRegex = /\$([a-zA-Z0-9_]+)/g;
const tempVarRegex = /_([a-zA-Z0-9_]+)/g;
let match;
while ((match = storyVarRegex.exec(sourceText)) !== null) {
const name = match[1];
if (!name || name === "widget") continue;
story.add(name);
}
while ((match = tempVarRegex.exec(sourceText)) !== null) {
const name = match[1];
if (!name) continue;
temp.add(name);
}
return {
story: Array.from(story).sort(),
temp: Array.from(temp).sort()
};
}
static getReferencedVariables() {
const source = this.getCurrentPassageSource();
const names = this.getReferencedVariableNamesFromSource(source);
const storyValues = {};
const tempValues = {};
for (const name of names.story) {
storyValues[name] = SugarCube?.State?.variables?.[name];
}
for (const name of names.temp) {
tempValues[name] = SugarCube?.State?.temporary?.[name];
}
return { storyValues, tempValues, names };
}
static getPassageMetadata() {
const title = this.getCurrentPassageTitle();
const passage = SugarCube?.Story?.get?.(title) || null;
const tags = Array.isArray(passage?.tags) ? passage.tags : [];
const state = SugarCube?.State || {};
return {
title,
tags,
turns: Number.isFinite(state.turns) ? state.turns : null,
historyLength: Number.isFinite(state.length) ? state.length : null,
historySize: Number.isFinite(state.size) ? state.size : null,
sourceChars: String(passage?.text || "").length
};
}
static analyzeMacroUsage(sourceText) {
const usage = /* @__PURE__ */ new Map();
const macroRegex = /<<\s*(\/?[A-Za-z][\w-]*)\b/g;
let match;
while ((match = macroRegex.exec(String(sourceText || ""))) !== null) {
const rawName = String(match[1] || "").trim();
if (!rawName) continue;
const isClosing = rawName.startsWith("/");
const name = isClosing ? rawName.slice(1) : rawName;
if (!name) continue;
if (!usage.has(name)) {
usage.set(name, {
name,
count: 0,
openingCount: 0,
closingCount: 0
});
}
const entry = usage.get(name);
entry.count += 1;
if (isClosing) {
entry.closingCount += 1;
} else {
entry.openingCount += 1;
}
}
return Array.from(usage.values()).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
}
static getMacroDetails(sourceText, macroName, { maxSnippets = 6, snippetRadius = 170 } = {}) {
const source = String(sourceText || "");
const targetName = String(macroName || "").trim();
if (!targetName) {
return {
name: "",
total: 0,
opening: 0,
closing: 0,
variables: { story: [], temp: [] },
snippets: []
};
}
const regex = new RegExp(`<<\\s*(\\/?)${this.escapeRegex(targetName)}\\b[\\s\\S]*?>>`, "gi");
const snippets = [];
let total = 0;
let opening = 0;
let closing = 0;
let match;
while ((match = regex.exec(source)) !== null) {
total += 1;
if (match[1] === "/") {
closing += 1;
} else {
opening += 1;
}
if (snippets.length < maxSnippets) {
const at = match.index;
const from = Math.max(0, at - snippetRadius);
const to = Math.min(source.length, regex.lastIndex + snippetRadius);
const rawSnippet = source.slice(from, to).replace(/\s+/g, " ").trim();
snippets.push({
index: at,
text: rawSnippet
});
}
}
const variableRefs = this.extractVariableRefs(source);
return {
name: targetName,
total,
opening,
closing,
variables: variableRefs,
snippets
};
}
static summarizeBranchBody(contentText) {
const content = String(contentText || "");
const normalized = content.replace(/\s+/g, " ").trim();
const plainText = content.replace(/<<[\s\S]*?>>/g, " ").replace(/\[\[[\s\S]*?\]\]/g, " ").replace(/<[^>]+>/g, " ").replace(/&[a-z]+;/gi, " ").replace(/\s+/g, " ").trim();
const words = plainText ? plainText.split(/\s+/).length : 0;
const macroMatches = content.match(/<<\s*\/?[A-Za-z][\w-]*\b/g) || [];
const linkMatches = content.match(/\[\[[^[\]]+?\]\]/g) || [];
const macroLinkMatches = content.match(/<<\s*(link|button|linkgoto|goto)\b/gi) || [];
const vars = this.extractVariableRefs(content);
const preview = normalized ? normalized.length > 220 ? `${normalized.slice(0, 220).trimEnd()}...` : normalized : "(No body content.)";
return {
rawChars: content.length,
textChars: plainText.length,
words,
macroCount: macroMatches.length,
linkCount: linkMatches.length + macroLinkMatches.length,
storyVars: vars.story,
tempVars: vars.temp,
preview
};
}
static extractVariableRefs(text) {
const source = String(text || "");
const story = /* @__PURE__ */ new Set();
const temp = /* @__PURE__ */ new Set();
const storyVarRegex = /\$([a-zA-Z0-9_]+)/g;
const tempVarRegex = /_([a-zA-Z0-9_]+)/g;
let match;
while ((match = storyVarRegex.exec(source)) !== null) {
if (match[1]) story.add(match[1]);
}
while ((match = tempVarRegex.exec(source)) !== null) {
if (match[1]) temp.add(match[1]);
}
return {
story: Array.from(story).sort(),
temp: Array.from(temp).sort()
};
}
static escapeRegex(value) {
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
static analyzeLinksFromDom() {
const links = [];
const root = document.getElementById("passages") || document;
const elements = root.querySelectorAll("a, button, .link-internal");
elements.forEach((el) => {
const domId = this.ensureDomLinkId(el);
const destination = this.resolveElementDestination(el);
const text = this.resolveElementText(el);
const mergeKey = this.getLinkMergeKey(text, destination);
links.push({
text,
destination,
isMacro: this.isMacroElement(el),
id: el.getAttribute("id") || null,
origin: "dom",
isVisible: true,
domId,
actionType: "dom-click",
mergeKey
});
});
return links;
}
static analyzeLinksFromSource(sourceText) {
const links = [];
const pushSourceLink = (text, destination, { isMacro = false, actionType = "navigate" } = {}) => {
const safeText = String(text || "").trim() || "(Untitled Link)";
const safeDestination = String(destination || "").trim() || "Executes Code/Macro";
links.push({
text: safeText,
destination: safeDestination,
isMacro,
id: null,
origin: "source",
isVisible: false,
actionType,
mergeKey: this.getLinkMergeKey(safeText, safeDestination)
});
};
const bracketLinkRegex = /\[\[([^[\]]+?)\]\]/g;
let match;
while ((match = bracketLinkRegex.exec(sourceText)) !== null) {
const body = (match[1] || "").trim();
if (!body) continue;
let text = body;
let destination = body;
if (body.includes("->")) {
const [left, right] = body.split("->");
text = (left || "").trim();
destination = (right || "").trim();
} else if (body.includes("<-")) {
const [left, right] = body.split("<-");
destination = (left || "").trim();
text = (right || "").trim();
} else if (body.includes("|")) {
const [left, right] = body.split("|");
text = (left || "").trim();
destination = (right || "").trim();
}
pushSourceLink(text || destination, destination, { isMacro: false, actionType: "navigate" });
}
const linkGotoRegex = /<<\s*linkgoto\s+"([^"]+)"\s+"([^"]+)"\s*>>/gi;
while ((match = linkGotoRegex.exec(sourceText)) !== null) {
pushSourceLink((match[1] || "").trim() || "(linkgoto)", (match[2] || "").trim(), {
isMacro: true,
actionType: "navigate"
});
}
const gotoRegex = /<<\s*goto\s+"([^"]+)"\s*>>/gi;
while ((match = gotoRegex.exec(sourceText)) !== null) {
const destination = (match[1] || "").trim();
if (!destination) continue;
pushSourceLink(destination, destination, { isMacro: true, actionType: "navigate" });
}
const linkButtonMacroRegex = /<<\s*(link|button)\s+([\s\S]*?)>>/gi;
while ((match = linkButtonMacroRegex.exec(sourceText)) !== null) {
const rawArgs = String(match[2] || "").trim();
const tokens = this.tokenizeMacroArgs(rawArgs);
if (!tokens.length) continue;
const textToken = this.normalizeMacroToken(tokens[0]);
const destinationToken = tokens.length > 1 ? this.normalizeMacroToken(tokens[1]) : "";
const destination = destinationToken || "Executes Code/Macro";
const actionType = this.isLikelyPassageTarget(destinationToken) ? "navigate" : "macro";
pushSourceLink(textToken || destination || "(Untitled Link)", destination, {
isMacro: true,
actionType
});
}
return links;
}
static analyzeAllLinks() {
const sourceText = this.getCurrentPassageSource();
const fromSource = this.analyzeLinksFromSource(sourceText);
const fromDom = this.analyzeLinksFromDom();
const merged = /* @__PURE__ */ new Map();
for (const link of fromSource) {
merged.set(link.mergeKey, link);
}
for (const link of fromDom) {
const key = link.mergeKey;
if (merged.has(key)) {
const existing = merged.get(key);
merged.set(key, {
...existing,
...link,
isVisible: true,
origin: "dom",
actionType: "dom-click"
});
} else {
const uniqueDomKey = `${key}::dom:${link.domId}`;
merged.set(uniqueDomKey, link);
}
}
return Array.from(merged.values());
}
static analyzeLinks() {
return this.analyzeLinksFromDom();
}
static getCurrentPassageTitle() {
try {
const activeTitle = SugarCube?.State?.active?.title;
if (typeof activeTitle === "string" && activeTitle.trim()) {
return activeTitle;
}
} catch {
}
const fromDom = document.querySelector("#passages .passage")?.getAttribute?.("data-passage");
if (typeof fromDom === "string" && fromDom.trim()) {
return fromDom;
}
return "Story Loading...";
}
static getCurrentPassageSource() {
try {
const title = this.getCurrentPassageTitle();
return SugarCube?.Story?.get?.(title)?.text || "";
} catch (error) {
debugError("Could not get passage source:", error);
return "";
}
}
static resolveElementDestination(el) {
const dataPassage = (el.getAttribute("data-passage") || "").trim();
if (dataPassage) return dataPassage;
const dataSetter = (el.getAttribute("data-setter") || "").trim();
if (dataSetter) return dataSetter;
if (el.tagName === "A") {
const href = (el.getAttribute("href") || "").trim();
if (href && href !== "#") return href;
}
return "Executes Code/Macro";
}
static resolveElementText(el) {
const fromText = (el.textContent || "").trim();
if (fromText) return fromText;
const aria = (el.getAttribute("aria-label") || "").trim();
if (aria) return aria;
const title = (el.getAttribute("title") || "").trim();
if (title) return title;
const html = String(el.innerHTML || "").replace(/\s+/g, " ").trim();
if (html) {
const cut = html.length > 120 ? `${html.slice(0, 120)}...` : html;
return `[Markup] ${cut}`;
}
return "(Unnamed Link)";
}
static isMacroElement(el) {
const classes = el.classList;
if (!classes) return false;
return classes.contains("macro-link") || classes.contains("macro-button") || classes.contains("link-internal");
}
static ensureDomLinkId(el) {
const existing = el.getAttribute("data-sc-debugger-link-id");
if (existing) return existing;
this._domLinkCounter = (this._domLinkCounter || 0) + 1;
const domId = `scdbg-link-${this._domLinkCounter}`;
el.setAttribute("data-sc-debugger-link-id", domId);
return domId;
}
static tokenizeMacroArgs(rawArgs) {
const tokens = [];
const tokenRegex = /"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|`([^`]+)`|\[\[([^\]]+)\]\]|(\S+)/g;
let match;
while ((match = tokenRegex.exec(rawArgs)) !== null) {
const token = match[1] != null ? match[1] : match[2] != null ? match[2] : match[3] != null ? `\`${match[3]}\`` : match[4] != null ? match[4] : match[5] != null ? match[5] : "";
if (token) tokens.push(token);
}
return tokens;
}
static normalizeMacroToken(token) {
const value = String(token || "").trim();
if (!value) return "";
if (value.startsWith("`") && value.endsWith("`")) {
return value;
}
return value;
}
static isLikelyPassageTarget(destination) {
const target = String(destination || "").trim();
if (!target) return false;
if (target.startsWith("`") && target.endsWith("`")) return false;
if (target.includes("$(") || target.includes(";")) return false;
if (target.toLowerCase().startsWith("http://") || target.toLowerCase().startsWith("https://")) {
return false;
}
return true;
}
static getLinkMergeKey(text, destination) {
return `${String(text || "").trim()}||${String(destination || "").trim()}`;
}
};
var passageAnalyzer_default = PassageAnalyzer;
// src/renderer.js
var DebugRenderer = class {
static render(config) {
const sections = [this.renderPassageOverview(), this.renderPassageMetadata(), this.renderMacroUsage()];
if (config.getSetting("showConditions")) {
sections.push(this.renderConditions());
}
if (config.getSetting("showVariables")) {
sections.push(this.renderVariables());
}
return sections.join("");
}
static renderPassageOverview() {
const title = passageAnalyzer_default.getCurrentPassageTitle();
return `
<section class="sc-debugger-section sc-debugger-section-passage">
<h3 class="sc-debugger-passage-heading">Current Passage</h3>
<div class="sc-debugger-passage-name">${this.escapeHtml(String(title || "(Unknown Passage)"))}</div>
</section>
`;
}
static renderLinksExplorer() {
const links = passageAnalyzer_default.analyzeAllLinks();
const sorted = [...links].sort((a, b) => {
const av = a.isVisible ? 0 : 1;
const bv = b.isVisible ? 0 : 1;
if (av !== bv) return av - bv;
return String(a.destination || "").localeCompare(String(b.destination || ""));
});
if (!sorted.length) {
return `
<section class="sc-debugger-section">
<h4 class="sc-debugger-section-title">Detected Links</h4>
<p class="sc-debugger-empty">No links detected in this passage.</p>
</section>
`;
}
const rows = sorted.map((link) => {
const visibility = link.isVisible ? "LIVE" : "HIDDEN";
const origin = link.origin === "source" ? "SOURCE" : "DOM";
const kind = link.isMacro ? "MACRO" : "LINK";
const destination = String(link.destination || "").trim();
const linkText = String(link.text || "").trim() || destination;
const interaction = this.getLinkInteraction(link, destination);
const searchText = `${linkText} ${destination} ${visibility} ${origin} ${kind}`.toLowerCase();
const kindKey = kind.toLowerCase();
const visibilityKey = visibility.toLowerCase();
return `
<article
class="sc-debugger-link ${interaction.clickable ? "sc-debugger-link-clickable" : ""}"
${this.getLinkInteractionAttributes(interaction)}
data-sc-search="${this.escapeHtml(searchText)}"
data-sc-kind="${this.escapeHtml(kindKey)}"
data-sc-visibility="${this.escapeHtml(visibilityKey)}"
>
<header class="sc-link-header">
<span class="sc-link-label">${kind}</span>
<span class="sc-link-label sc-link-visibility">${visibility}</span>
<span class="sc-link-label sc-link-origin">${origin}</span>
</header>
<div class="sc-link-text">${this.escapeHtml(linkText)}</div>
<div class="sc-link-dest">${this.escapeHtml(interaction.description)}</div>
</article>
`;
}).join("");
return `
<section class="sc-debugger-section">
<h4 class="sc-debugger-section-title">Detected Links</h4>
<div class="sc-var-editor-search">
<label class="sc-var-editor-label" for="sc-links-search">Filter Links</label>
<input
type="search"
id="sc-links-search"
autocomplete="off"
spellcheck="false"
placeholder="Filter by text, destination, type, or visibility..."
/>
</div>
<div class="sc-links-filter-bar" id="sc-links-filter-bar">
<button type="button" class="sc-var-editor-btn sc-links-filter-btn is-active" data-sc-link-filter="all">All</button>
<button type="button" class="sc-var-editor-btn sc-links-filter-btn" data-sc-link-filter="live">Live</button>
<button type="button" class="sc-var-editor-btn sc-links-filter-btn" data-sc-link-filter="hidden">Hidden</button>
<button type="button" class="sc-var-editor-btn sc-links-filter-btn" data-sc-link-filter="macro">Macro</button>
<button type="button" class="sc-var-editor-btn sc-links-filter-btn" data-sc-link-filter="link">Link</button>
</div>
<p class="sc-debugger-empty" id="sc-links-search-count">Showing ${sorted.length} of ${sorted.length} links.</p>
<p class="sc-debugger-empty" id="sc-links-search-empty" hidden>No links match this filter.</p>
<div class="sc-debugger-links-list" id="sc-links-search-results">${rows}</div>
</section>
`;
}
static renderConditions() {
const source = passageAnalyzer_default.getCurrentPassageSource();
const analysis = passageAnalyzer_default.analyzeConditionBlocks(source);
if (!analysis.found) {
return `
<section class="sc-debugger-section">
<h4 class="sc-debugger-section-title">Logic Conditions</h4>
<p class="sc-debugger-empty">No if/else conditions found in the current passage.</p>
</section>
`;
}
const blocks = analysis.blocks.map((block) => {
const branches = block.branches.map((branch) => {
const typeClass = `sc-cond-${branch.type}`;
const readableType = String(branch.type || "unknown").toUpperCase();
const conditionText = branch.type === "else" ? "Fallback branch" : branch.condition ? `Condition: ${branch.condition}` : "Condition: (not provided)";
const summary = branch.summary || {};
const metrics = [
`${summary.textChars || 0} text chars`,
`${summary.words || 0} words`,
`${summary.macroCount || 0} macros`,
`${summary.linkCount || 0} links`,
`Vars: ${this.formatVariableChipText(summary.storyVars, summary.tempVars)}`
].join(" | ");
return `
<div class="sc-debugger-condition ${typeClass}">
<span class="sc-cond-type">${this.escapeHtml(readableType)}</span>
<div>
<div class="sc-cond-expression">${this.escapeHtml(conditionText)}</div>
<div class="sc-cond-meta">${this.escapeHtml(metrics)}</div>
<code>${this.escapeHtml(summary.preview || "(No branch body content.)")}</code>
</div>
</div>
`;
}).join("");
return `
<div class="sc-debugger-var-group">
<h5>IF Block #${block.id}${block.depth > 0 ? ` (nested depth ${block.depth})` : ""}</h5>
<div class="sc-debugger-conditions-list">${branches}</div>
</div>
`;
}).join("");
return `
<section class="sc-debugger-section">
<h4 class="sc-debugger-section-title">Logic Conditions</h4>
${blocks}
</section>
`;
}
static renderVariables() {
const { storyValues, tempValues } = passageAnalyzer_default.getReferencedVariables();
const hasStory = Object.keys(storyValues).length > 0;
const hasTemp = Object.keys(tempValues).length > 0;
if (!hasStory && !hasTemp) {
return `
<section class="sc-debugger-section">
<h4 class="sc-debugger-section-title">Referenced Variables</h4>
<p class="sc-debugger-empty">No variables referenced in this passage.</p>
</section>
`;
}
let html = `
<section class="sc-debugger-section">
<h4 class="sc-debugger-section-title">Referenced Variables</h4>
`;
if (hasStory) {
html += `
<div class="sc-debugger-var-group">
<h5>Story Variables ($)</h5>
<div class="sc-debugger-variables-list">
${Object.entries(storyValues).map(([key, value]) => this.renderVariable(key, value)).join("")}
</div>
</div>
`;
}
if (hasTemp) {
html += `
<div class="sc-debugger-var-group">
<h5>Temporary Variables (_)</h5>
<div class="sc-debugger-variables-list">
${Object.entries(tempValues).map(([key, value]) => this.renderVariable(key, value)).join("")}
</div>
</div>
`;
}
html += "</section>";
return html;
}
static renderVariable(key, value) {
const type = this.getValueType(value);
const preview = this.formatValue(value, { maxChars: 400 });
return `
<article class="sc-debugger-variable">
<span class="sc-var-name">${this.escapeHtml(String(key || ""))}</span>
<span class="sc-var-type sc-var-${this.escapeHtml(type)}">${this.escapeHtml(type)}</span>
<code class="sc-var-value">${this.escapeHtml(preview)}</code>
</article>
`;
}
static renderPassageMetadata() {
const meta = passageAnalyzer_default.getPassageMetadata();
const tagsText = meta.tags.length ? meta.tags.join(", ") : "(none)";
const turnsText = meta.turns == null ? "n/a" : String(meta.turns);
const histLenText = meta.historyLength == null ? "n/a" : String(meta.historyLength);
const histSizeText = meta.historySize == null ? "n/a" : String(meta.historySize);
return `
<section class="sc-debugger-section">
<h4 class="sc-debugger-section-title">Passage Metadata</h4>
<div class="sc-debugger-variables-list">
<article class="sc-debugger-variable">
<span class="sc-var-name">Tags</span>
<span class="sc-var-type">meta</span>
<code class="sc-var-value">${this.escapeHtml(tagsText)}</code>
</article>
<article class="sc-debugger-variable">
<span class="sc-var-name">Turns</span>
<span class="sc-var-type">meta</span>
<code class="sc-var-value">${this.escapeHtml(turnsText)}</code>
</article>
<article class="sc-debugger-variable">
<span class="sc-var-name">History</span>
<span class="sc-var-type">meta</span>
<code class="sc-var-value">${this.escapeHtml(`${histLenText} / ${histSizeText}`)}</code>
</article>
<article class="sc-debugger-variable">
<span class="sc-var-name">Source Size</span>
<span class="sc-var-type">meta</span>
<code class="sc-var-value">${this.escapeHtml(`${meta.sourceChars} chars`)}</code>
</article>
</div>
</section>
`;
}
static renderMacroUsage() {
const source = passageAnalyzer_default.getCurrentPassageSource();
const usage = passageAnalyzer_default.analyzeMacroUsage(source);
if (!usage.length) {
return `
<section class="sc-debugger-section">
<h4 class="sc-debugger-section-title">Macro Usage</h4>
<p class="sc-debugger-empty">No macros detected in this passage source.</p>
</section>
`;
}
const items = usage.slice(0, 20).map((item) => {
const classification = this.classifyMacro(item.name);
const usageBits = [`${item.count} total`];
if (item.openingCount > 0) usageBits.push(`open ${item.openingCount}`);
if (item.closingCount > 0) usageBits.push(`close ${item.closingCount}`);
usageBits.push(classification.detail);
return `
<article class="sc-macro-usage-row" data-sc-macro-name="${this.escapeHtml(item.name)}">
<button type="button" class="sc-macro-usage-row-btn" aria-expanded="false">
<span class="sc-var-name">${this.escapeHtml(`<<${item.name}>>`)}</span>
<span class="sc-var-type">${this.escapeHtml(classification.label)}</span>
<code class="sc-var-value">${this.escapeHtml(usageBits.join(" | "))}</code>
</button>
<div class="sc-macro-usage-details" hidden></div>
</article>
`;
}).join("");
return `
<section class="sc-debugger-section">
<h4 class="sc-debugger-section-title">Macro Usage</h4>
<p class="sc-debugger-empty">Top ${Math.min(usage.length, 20)} macros in this passage source.</p>
<div class="sc-debugger-variables-list">${items}</div>
</section>
`;
}
static renderMacroUsageDetails(macroName) {
const source = passageAnalyzer_default.getCurrentPassageSource();
const details = passageAnalyzer_default.getMacroDetails(source, macroName, {
maxSnippets: 6,
snippetRadius: 190
});
if (!details.total) {
return `<p class="sc-debugger-empty">No occurrences found for <<${this.escapeHtml(macroName)}>>.</p>`;
}
const storyVars = details.variables.story.map((name) => `$${name}`);
const tempVars = details.variables.temp.map((name) => `_${name}`);
const varText = storyVars.length || tempVars.length ? [...storyVars, ...tempVars].join(", ") : "(none)";
const snippets = details.snippets.map(
(entry, idx) => `
<article class="sc-macro-usage-snippet">
<div class="sc-macro-usage-snippet-title">Occurrence ${idx + 1} at char ${entry.index}</div>
<code>${this.escapeHtml(entry.text)}</code>
</article>
`
).join("");
return `
<div class="sc-macro-usage-detail-meta">
<span>Total: ${details.total}</span>
<span>Open: ${details.opening}</span>
<span>Close: ${details.closing}</span>
</div>
<div class="sc-macro-usage-detail-vars">Variables referenced in passage: ${this.escapeHtml(varText)}</div>
<div class="sc-macro-usage-snippets">${snippets || `<p class="sc-debugger-empty">No snippets available.</p>`}</div>
`;
}
static classifyMacro(name) {
const key = String(name || "").toLowerCase();
const table = {
if: { label: "logic", detail: "Conditional branch start." },
elseif: { label: "logic", detail: "Conditional branch continuation." },
else: { label: "logic", detail: "Conditional fallback branch." },
switch: { label: "logic", detail: "Multi-branch selection." },
case: { label: "logic", detail: "Switch case branch." },
default: { label: "logic", detail: "Switch fallback branch." },
set: { label: "state", detail: "Writes state variables." },
unset: { label: "state", detail: "Removes variable values." },
remember: { label: "state", detail: "Stores persistent value." },
forget: { label: "state", detail: "Clears persistent value." },
link: { label: "ui", detail: "Interactive text link block." },
button: { label: "ui", detail: "Interactive button block." },
linkappend: { label: "ui", detail: "Link that appends content." },
linkreplace: { label: "ui", detail: "Link that replaces content." },
linkrepeat: { label: "ui", detail: "Link that can run repeatedly." },
goto: { label: "nav", detail: "Navigates to another passage." },
linkgoto: { label: "nav", detail: "Link with explicit destination passage." },
back: { label: "nav", detail: "Moves backward in history." },
return: { label: "nav", detail: "Returns to prior passage context." },
include: { label: "content", detail: "Injects another passage content." },
display: { label: "content", detail: "Displays another passage." },
print: { label: "output", detail: "Renders expression output." },
script: { label: "code", detail: "Runs JavaScript block." },
widget: { label: "code", detail: "Defines reusable macro widget." },
timed: { label: "timing", detail: "Schedules delayed content." },
repeat: { label: "timing", detail: "Repeats timed content block." },
stop: { label: "timing", detail: "Stops repeat/timed behavior." }
};
return table[key] || { label: "macro", detail: "Custom or less-common macro." };
}
static formatVariableChipText(storyVars, tempVars) {
const story = Array.isArray(storyVars) ? storyVars.map((name) => `$${name}`) : [];
const temp = Array.isArray(tempVars) ? tempVars.map((name) => `_${name}`) : [];
const all = [...story, ...temp];
if (!all.length) return "(none)";
if (all.length <= 6) return all.join(", ");
return `${all.slice(0, 6).join(", ")}, +${all.length - 6} more`;
}
static renderVariableEditor() {
return `
<section class="sc-debugger-section sc-var-editor-shell">
<h4 class="sc-debugger-section-title">Search And Edit Variables</h4>
<p class="sc-var-editor-hint">Select a variable entry to edit it. Deep object and array paths are supported.</p>
<div class="sc-var-editor-search">
<label class="sc-var-editor-label" for="sc-var-editor-search">Search</label>
<input
type="search"
id="sc-var-editor-search"
autocomplete="off"
spellcheck="false"
placeholder="Try stamina, inventory[0], or nested key names..."
/>
</div>
<div class="sc-var-editor-loading" id="sc-var-editor-loading" hidden></div>
<div class="sc-var-editor-results" id="sc-var-editor-results">
<p class="sc-debugger-empty">Start typing to search variables.</p>
</div>
<div class="sc-var-editor-selected" id="sc-var-editor-selected" hidden>
<h5 class="sc-var-editor-selected-title">Selected Variable</h5>
<div class="sc-var-editor-path" id="sc-var-editor-path-display"></div>
<div class="sc-var-editor-current" id="sc-var-editor-current-display"></div>
<div class="sc-var-editor-input-group">
<label for="sc-var-editor-input">New Value</label>
<textarea
id="sc-var-editor-input"
rows="6"
spellcheck="false"
placeholder="Enter plain text, number, true/false, null, or JSON."
></textarea>
</div>
<div class="sc-var-editor-buttons">
<button id="sc-var-editor-save" class="sc-var-editor-btn sc-var-editor-btn-save" type="button">Save</button>
<button id="sc-var-editor-reset" class="sc-var-editor-btn sc-var-editor-btn-reset" type="button">Reset</button>
<button id="sc-var-editor-pin" class="sc-var-editor-btn" type="button" title="Pin selected variable for quick access">Pin</button>
<button id="sc-var-editor-clear" class="sc-var-editor-btn sc-var-editor-btn-clear" type="button">Clear</button>
</div>
<div id="sc-var-editor-status" role="status" aria-live="polite"></div>
</div>
<div class="sc-var-editor-pins" id="sc-var-editor-pins">
<h5 class="sc-var-editor-selected-title">Pinned Variables</h5>
<div class="sc-var-editor-results" id="sc-var-editor-pins-list">
<p class="sc-debugger-empty">No pinned variables yet.</p>
</div>
</div>
</section>
`;
}
static getValueType(value) {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
return typeof value;
}
static getLinkInteraction(link, destination) {
const isExpressionDestination = destination.startsWith("`") && destination.endsWith("`");
const isMacroPlaceholder = !destination || destination === "Executes Code/Macro";
if (link?.domId) {
const isMacroAction = isMacroPlaceholder || isExpressionDestination;
return {
clickable: true,
actionType: "dom-click",
domId: String(link.domId),
destination: destination || "",
description: isMacroAction ? "Action: DOM click (runs live macro/script)" : `Target: ${destination}`
};
}
const isNavigableDestination = destination.length > 0 && destination !== "Executes Code/Macro" && !destination.includes("$(") && !destination.includes(";") && !(destination.startsWith("`") && destination.endsWith("`"));
if (isNavigableDestination) {
return {
clickable: true,
actionType: "navigate",
domId: "",
destination,
description: `Target: ${destination}`
};
}
return {
clickable: false,
actionType: "none",
domId: "",
destination: "",
description: isExpressionDestination ? `Dynamic target expression: ${destination}` : isMacroPlaceholder ? "Action: Macro or script (no direct passage target)" : `Target: ${destination}`
};
}
static getLinkInteractionAttributes(interaction) {
if (!interaction?.clickable) return "";
const attrs = [`data-sc-action="${this.escapeHtml(interaction.actionType)}"`];
if (interaction.destination) {
attrs.push(`data-sc-destination="${this.escapeHtml(interaction.destination)}"`);
}
if (interaction.domId) {
attrs.push(`data-sc-dom-id="${this.escapeHtml(interaction.domId)}"`);
}
return attrs.join(" ");
}
static formatValue(value, { maxChars = 2e3 } = {}) {
let formatted;
if (typeof value === "string") {
formatted = value;
} else if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
formatted = String(value);
} else if (value === null) {
formatted = "null";
} else if (typeof value === "undefined") {
formatted = "undefined";
} else if (typeof value === "function") {
formatted = "[Function]";
} else {
formatted = this.safeStringify(value, 2);
}
if (formatted.length > maxChars) {
return `${formatted.slice(0, maxChars)}...`;
}
return formatted;
}
static safeStringify(value, spacing = 0) {
const seen = /* @__PURE__ */ new WeakSet();
try {
return JSON.stringify(
value,
(key, current) => {
if (typeof current === "function") {
return "[Function]";
}
if (typeof current === "object" && current !== null) {
if (seen.has(current)) return "[Circular]";
seen.add(current);
}
return current;
},
spacing
);
} catch (error) {
return `[Unserializable: ${error.message}]`;
}
}
static escapeHtml(text) {
const map = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
};
return String(text).replace(/[&<>"']/g, (char) => map[char]);
}
};
var renderer_default = DebugRenderer;
// src/variableEditor.js
var CACHE_TTL_MS = 1e3;
var MAX_VALUE_CHARS = 1800;
var MAX_SEARCH_CHARS = 4e3;
var MAX_DEPTH = 14;
var MAX_INDEX_SIZE = 12e3;
var VariableEditor = class {
constructor() {
this.selectedVar = null;
this.searchCache = null;
this.cacheTimestamp = 0;
this._buildToken = 0;
}
/**
* Build searchable index of variables and nested values.
*/
buildSearchIndex() {
if (this.isCacheFresh()) {
return this.searchCache;
}
const root = this.getStoryVariablesRoot();
const state = this.createTraversalState(root);
while (state.cursor < state.queue.length && state.index.length < MAX_INDEX_SIZE) {
this.consumeQueueItem(state);
}
this.commitCache(state.index);
return state.index;
}
/**
* Build index incrementally to avoid UI freezes.
*/
async buildSearchIndexAsync({ frameBudgetMs = 8, onProgress } = {}) {
if (this.isCacheFresh()) {
return this.searchCache;
}
const root = this.getStoryVariablesRoot();
const state = this.createTraversalState(root);
while (state.cursor < state.queue.length && state.index.length < MAX_INDEX_SIZE) {
const frameStart = this.now();
while (state.cursor < state.queue.length && state.index.length < MAX_INDEX_SIZE && this.now() - frameStart < frameBudgetMs) {
this.consumeQueueItem(state);
}
if (typeof onProgress === "function") {
onProgress({
processed: state.processed,
indexed: state.index.length,
queued: state.queue.length - state.cursor
});
}
await this._yieldToBrowser();
}
this.commitCache(state.index);
return state.index;
}
search(query) {
const index = this.buildSearchIndex();
const q = String(query || "").toLowerCase().trim();
if (!q) {
return index;
}
return index.filter((item) => item.searchText.includes(q));
}
async searchAsync(query, { frameBudgetMs = 8, onProgress } = {}) {
const index = await this.buildSearchIndexAsync({ frameBudgetMs, onProgress });
const q = String(query || "").toLowerCase().trim();
if (!q) {
return index;
}
return index.filter((item) => item.searchText.includes(q));
}
/**
* Get variable value by path (supports array/object paths).
*/
getVariable(path) {
const tokens = this.parsePath(path);
let current = this.getStoryVariablesRoot();
for (const token of tokens) {
if (current == null) {
return void 0;
}
current = current[token];
}
return current;
}
/**
* Parse text input into typed value.
*/
parseValue(input) {
if (typeof input !== "string") {
return input;
}
const trimmed = input.trim();
if (trimmed === "") return "";
if (trimmed === "undefined") return void 0;
if (trimmed === "null") return null;
if (trimmed === "true") return true;
if (trimmed === "false") return false;
if (/^-?(?:\d+|\d+\.\d+|\.\d+)(?:[eE][+-]?\d+)?$/.test(trimmed)) {
return Number(trimmed);
}
if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
return trimmed.slice(1, -1);
}
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith('"') || trimmed.startsWith("'")) {
try {
return JSON.parse(trimmed);
} catch {
}
}
return input;
}
/**
* Set variable value by path. Creates missing containers when needed.
*/
setValue(path, value) {
const tokens = this.parsePath(path);
if (!tokens.length) {
throw new Error("Invalid variable path.");
}
let current = this.getStoryVariablesRoot();
for (let i = 0; i < tokens.length - 1; i++) {
const token = tokens[i];
const nextToken = tokens[i + 1];
if (!this.isObjectLike(current)) {
throw new Error(`Cannot navigate into non-object segment "${String(token)}".`);
}
if (!this.isObjectLike(current[token])) {
current[token] = typeof nextToken === "number" ? [] : {};
}
current = current[token];
}
const lastToken = tokens[tokens.length - 1];
if (!this.isObjectLike(current)) {
throw new Error("Cannot assign into non-object target.");
}
current[lastToken] = value;
this.invalidateCache();
return true;
}
/**
* Format value for display with circular-reference safety.
*/
formatValue(value, { maxChars = 8e3 } = {}) {
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
return String(value);
}
if (value === null) return "null";
if (typeof value === "undefined") return "undefined";
if (typeof value === "function") return "[Function]";
const serialized = this.safeStringify(value, 2);
if (serialized.length > maxChars) {
return `${serialized.slice(0, maxChars)}...`;
}
return serialized;
}
invalidateCache() {
this.searchCache = null;
this.cacheTimestamp = 0;
this._buildToken++;
}
isCacheFresh() {
if (!Array.isArray(this.searchCache)) return false;
return Date.now() - this.cacheTimestamp < CACHE_TTL_MS;
}
getStoryVariablesRoot() {
return SugarCube?.State?.variables || {};
}
createTraversalState(root) {
const queue = [];
const index = [];
const visited = /* @__PURE__ */ new WeakSet();
const rootKeys = Object.keys(root);
for (const key of rootKeys) {
if (this.shouldSkipKey(key)) continue;
const value = root[key];
const path = this.composePath("", key, false);
const item = this.buildIndexItem(path, value, key);
index.push(item);
queue.push({ value, path, depth: 0 });
}
return {
queue,
cursor: 0,
index,
visited,
processed: 0
};
}
consumeQueueItem(state) {
const node = state.queue[state.cursor++];
state.processed += 1;
if (!this.isTraversable(node?.value)) {
return;
}
if (state.visited.has(node.value)) {
return;
}
state.visited.add(node.value);
if (node.depth >= MAX_DEPTH) {
return;
}
if (Array.isArray(node.value)) {
for (let i = 0; i < node.value.length && state.index.length < MAX_INDEX_SIZE; i++) {
const value = node.value[i];
const path = this.composePath(node.path, String(i), true);
const item = this.buildIndexItem(path, value, String(i));
state.index.push(item);
if (this.isTraversable(value)) {
state.queue.push({ value, path, depth: node.depth + 1 });
}
}
return;
}
for (const key of Object.keys(node.value)) {
if (state.index.length >= MAX_INDEX_SIZE) break;
if (this.shouldSkipKey(key)) continue;
const value = node.value[key];
const path = this.composePath(node.path, key, false);
const item = this.buildIndexItem(path, value, key);
state.index.push(item);
if (this.isTraversable(value)) {
state.queue.push({ value, path, depth: node.depth + 1 });
}
}
}
buildIndexItem(path, value, key) {
const displayPath = `$${path}`;
const valueStr = this.previewValue(value);
const type = this.getValueType(value);
const searchText = `${displayPath} ${key} ${type} ${valueStr}`.toLowerCase().slice(
0,
MAX_SEARCH_CHARS
);
return {
path,
displayPath,
value,
valueStr,
key,
type,
searchText
};
}
previewValue(value) {
if (typeof value === "string") {
return value.length > MAX_VALUE_CHARS ? `${value.slice(0, MAX_VALUE_CHARS)}...` : value;
}
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
return String(value);
}
if (value === null) return "null";
if (typeof value === "undefined") return "undefined";
if (typeof value === "function") return "[Function]";
if (Array.isArray(value)) return `[Array(${value.length})]`;
if (this.isObjectLike(value)) {
const keys = Object.keys(value);
const preview = keys.slice(0, 6).join(", ");
return `[Object(${keys.length})${preview ? `: ${preview}` : ""}${keys.length > 6 ? ", ..." : ""}]`;
}
return String(value);
}
getValueType(value) {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
return typeof value;
}
composePath(parentPath, key, isArrayIndex) {
if (isArrayIndex) {
return parentPath ? `${parentPath}[${key}]` : `[${key}]`;
}
const isIdentifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key);
if (!parentPath) {
return isIdentifier ? key : `[${JSON.stringify(key)}]`;
}
return isIdentifier ? `${parentPath}.${key}` : `${parentPath}[${JSON.stringify(key)}]`;
}
parsePath(path) {
const source = String(path || "").trim().replace(/^\$/, "");
if (!source) return [];
const tokens = [];
let i = 0;
while (i < source.length) {
const char = source[i];
if (char === ".") {
i += 1;
continue;
}
if (char === "[") {
i += 1;
while (i < source.length && /\s/.test(source[i])) i += 1;
if (i >= source.length) break;
if (source[i] === "'" || source[i] === '"') {
const quote = source[i];
i += 1;
let token2 = "";
while (i < source.length) {
const current = source[i];
if (current === "\\" && i + 1 < source.length) {
token2 += source[i + 1];
i += 2;
continue;
}
if (current === quote) {
i += 1;
break;
}
token2 += current;
i += 1;
}
while (i < source.length && /\s/.test(source[i])) i += 1;
if (source[i] === "]") i += 1;
tokens.push(token2);
continue;
}
let raw = "";
while (i < source.length && source[i] !== "]") {
raw += source[i];
i += 1;
}
if (source[i] === "]") i += 1;
const trimmed = raw.trim();
if (trimmed === "") continue;
if (/^\d+$/.test(trimmed)) {
tokens.push(Number(trimmed));
} else {
tokens.push(trimmed);
}
continue;
}
let identifier = "";
while (i < source.length && source[i] !== "." && source[i] !== "[") {
identifier += source[i];
i += 1;
}
const token = identifier.trim();
if (token) tokens.push(token);
}
return tokens;
}
shouldSkipKey(key) {
if (typeof key !== "string") return true;
if (key === "widget") return true;
if (key.toLowerCase().includes("widget")) return true;
if (key.startsWith("__scdbg_")) return true;
return false;
}
isObjectLike(value) {
return value !== null && (typeof value === "object" || typeof value === "function");
}
isTraversable(value) {
return value !== null && typeof value === "object";
}
safeStringify(value, spacing = 0) {
const seen = /* @__PURE__ */ new WeakSet();
try {
return JSON.stringify(
value,
(key, current) => {
if (typeof current === "function") return "[Function]";
if (typeof current === "bigint") return `${String(current)}n`;
if (typeof current === "symbol") return String(current);
if (typeof current === "object" && current !== null) {
if (seen.has(current)) return "[Circular]";
seen.add(current);
}
return current;
},
spacing
);
} catch (error) {
return `[Unserializable: ${error.message}]`;
}
}
now() {
if (typeof performance !== "undefined" && typeof performance.now === "function") {
return performance.now();
}
return Date.now();
}
_yieldToBrowser() {
return new Promise((resolve) => {
if (typeof requestIdleCallback === "function") {
requestIdleCallback(() => resolve(), { timeout: 50 });
return;
}
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => resolve());
return;
}
setTimeout(resolve, 0);
});
}
commitCache(index) {
this.searchCache = index;
this.cacheTimestamp = Date.now();
}
};
var variableEditor_default = new VariableEditor();
// src/utils.js
function ensureDebuggerStylesInjected(targetRoot = document.head) {
if (targetRoot?.querySelector?.("#sc-debugger-inline-styles")) return;
const css = `
.sc-debugger-backdrop{position:fixed;inset:0;background:radial-gradient(circle at 18% 18%,rgba(77,196,255,.18),transparent 55%),rgba(2,8,20,.72);backdrop-filter:blur(4px);z-index:9998;display:none;pointer-events:auto}
.sc-debugger-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:min(1100px,95vw);height:min(86vh,760px);border:1px solid #2f435f;border-radius:16px;background:linear-gradient(160deg,#111926 0%,#101827 45%,#0d1523 100%);box-shadow:0 24px 56px rgba(2,8,20,.64);z-index:9999;display:none;flex-direction:column;color:#e6edf7;font-family:Segoe UI,Inter,Arial,sans-serif;overflow:hidden;pointer-events:auto}
.sc-debugger-header{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;border-bottom:1px solid #25344b;background:linear-gradient(180deg,rgba(18,32,52,.8),rgba(12,21,35,.75))}
.sc-debugger-header h2{margin:0;font-size:20px;color:#92f8bf;font-weight:700}
.sc-debugger-close{width:32px;height:32px;border:1px solid transparent;border-radius:8px;background:transparent;color:#9caec7;font-size:22px;cursor:pointer}
.sc-debugger-close:hover{color:#ff9e9e;background:rgba(255,158,158,.12);border-color:rgba(255,158,158,.24)}
.sc-debugger-tabs{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:10px 12px;border-bottom:1px solid #25344b;background:rgba(10,17,29,.55)}
.sc-debugger-tab-btn{min-height:38px;padding:9px 12px;border:1px solid transparent;border-radius:10px;background:transparent;color:#9caec7;cursor:pointer;font-size:14px;font-weight:600}
.sc-debugger-tab-btn:hover{color:#e6edf7;border-color:rgba(77,196,255,.28);background:rgba(77,196,255,.08)}
.sc-debugger-tab-btn.active{color:#dff6ff;background:linear-gradient(180deg,rgba(44,165,255,.24),rgba(44,165,255,.09));border-color:rgba(44,165,255,.42)}
.sc-debugger-content{flex:1;overflow:auto;padding:16px}
.sc-debugger-tab-pane{display:none}
.sc-debugger-tab-pane.active{display:block}
.sc-debugger-section{margin-bottom:16px;padding:14px;border:1px solid #25344b;border-radius:12px;background:linear-gradient(180deg,rgba(24,37,56,.45),rgba(15,24,38,.55))}
.sc-debugger-section:last-child{margin-bottom:0}
.sc-debugger-section-title{margin:0 0 10px;color:#4dc4ff;font-size:12px;letter-spacing:.8px;text-transform:uppercase;font-weight:700}
.sc-debugger-empty{margin:8px 0 0;color:#9caec7;font-size:13px}
.sc-debugger-warning{margin:0 0 12px;padding:11px 12px;border-radius:10px;border:1px solid rgba(255,210,128,.35);background:rgba(255,210,128,.09);color:#ffd280;font-size:12px;line-height:1.45}
.sc-debugger-section-passage{border-color:rgba(146,248,191,.28);background:linear-gradient(140deg,rgba(25,54,44,.3),rgba(18,31,47,.6))}
.sc-debugger-passage-heading{margin:0;font-size:12px;letter-spacing:.8px;text-transform:uppercase;color:#9caec7}
.sc-debugger-passage-name{margin-top:8px;font-size:18px;font-weight:700;color:#92f8bf;word-break:break-word}
.sc-debugger-conditions-list{display:grid;gap:8px}
.sc-debugger-condition{display:grid;grid-template-columns:78px 1fr;gap:8px;align-items:start;padding:9px 10px;border-radius:8px;border:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.03)}
.sc-cond-type{color:#dfe9ff;font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase}
.sc-debugger-condition code{margin:0;font-family:Consolas,SFMono-Regular,Monaco,monospace;font-size:12px;color:#d4deec;line-height:1.4;word-break:break-word;white-space:pre-wrap}
.sc-cond-expression{color:#dbe8fa;font-size:12px;line-height:1.4;margin-bottom:4px}
.sc-cond-meta{color:#9fb3cf;font-size:11px;line-height:1.35;margin-bottom:6px}
.sc-debugger-condition.sc-cond-if{border-color:rgba(125,213,255,.35);background:rgba(77,196,255,.08)}
.sc-debugger-condition.sc-cond-elseif{border-color:rgba(255,210,128,.35);background:rgba(255,210,128,.08)}
.sc-debugger-condition.sc-cond-else{border-color:rgba(179,194,215,.34);background:rgba(179,194,215,.08)}
.sc-debugger-condition.sc-cond-endif{border-color:rgba(111,232,159,.35);background:rgba(111,232,159,.07)}
.sc-debugger-var-group{margin-bottom:12px}
.sc-debugger-var-group:last-child{margin-bottom:0}
.sc-debugger-var-group h5{margin:0 0 8px;color:#9caec7;font-size:11px;font-weight:700;letter-spacing:.75px;text-transform:uppercase}
.sc-debugger-variables-list{display:grid;gap:7px}
.sc-debugger-variable{display:grid;grid-template-columns:minmax(100px,170px) minmax(68px,92px) 1fr;gap:10px;align-items:start;padding:9px 10px;border-radius:9px;border:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.025)}
.sc-var-name{color:#f6f0ab;font-size:12px;font-weight:700;line-height:1.3;word-break:break-word}
.sc-var-type{display:inline-flex;align-items:center;justify-content:center;min-height:20px;padding:0 7px;border-radius:999px;border:1px solid rgba(200,213,236,.25);font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:#c8d5ec;background:rgba(200,213,236,.08)}
.sc-var-type.sc-var-string{color:#8efeb0;border-color:rgba(142,254,176,.36);background:rgba(142,254,176,.14)}
.sc-var-type.sc-var-number{color:#8cd9ff;border-color:rgba(140,217,255,.4);background:rgba(140,217,255,.12)}
.sc-var-type.sc-var-boolean{color:#ffdca6;border-color:rgba(255,220,166,.42);background:rgba(255,220,166,.12)}
.sc-var-type.sc-var-object,.sc-var-type.sc-var-array{color:#c9bbff;border-color:rgba(201,187,255,.35);background:rgba(201,187,255,.1)}
.sc-var-type.sc-var-null,.sc-var-type.sc-var-undefined{color:#f1b4cc;border-color:rgba(241,180,204,.3);background:rgba(241,180,204,.08)}
.sc-var-value{margin:0;color:#d4deed;font-family:Consolas,SFMono-Regular,Monaco,monospace;font-size:12px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
.sc-macro-usage-row{border:1px solid rgba(255,255,255,.08);border-radius:9px;background:rgba(255,255,255,.02);padding:8px 9px;width:100%}
.sc-macro-usage-row-btn{width:100%;border:0;background:transparent;color:inherit;text-align:left;display:grid;grid-template-columns:minmax(100px,170px) minmax(68px,92px) 1fr;gap:10px;align-items:start;padding:0;cursor:pointer}
.sc-macro-usage-row-btn .sc-var-type{align-self:start}
.sc-macro-usage-row-btn .sc-var-value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sc-macro-usage-row-btn:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(77,196,255,.18);border-radius:8px}
.sc-macro-usage-row.is-expanded{border-color:rgba(77,196,255,.42);background:rgba(77,196,255,.08)}
.sc-macro-usage-details{margin-top:8px;padding-top:8px;border-top:1px dashed rgba(77,196,255,.28);width:100%}
.sc-macro-usage-detail-meta{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:6px;color:#bee7ff;font-size:11px}
.sc-macro-usage-detail-vars{color:#aecaeb;font-size:11px;line-height:1.35;margin-bottom:8px}
.sc-macro-usage-snippets{display:grid;gap:7px}
.sc-macro-usage-snippet{border:1px solid rgba(255,255,255,.1);border-radius:8px;background:rgba(5,12,22,.5);padding:7px 8px}
.sc-macro-usage-snippet-title{color:#9ec4e4;font-size:10px;font-weight:700;margin-bottom:4px;letter-spacing:.35px;text-transform:uppercase}
.sc-macro-usage-snippet code{margin:0;color:#d7e2f3;font-family:Consolas,SFMono-Regular,Monaco,monospace;font-size:11px;line-height:1.35;white-space:pre-wrap;word-break:break-word}
.sc-debugger-links-list{display:grid;gap:9px}
.sc-debugger-link{padding:11px 12px;border-radius:10px;border:1px solid rgba(77,196,255,.2);background:rgba(77,196,255,.07)}
.sc-debugger-link-clickable{cursor:pointer}
.sc-debugger-link-clickable:hover{border-color:rgba(77,196,255,.52);background:rgba(77,196,255,.14)}
.sc-link-header{display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap}
.sc-link-label{display:inline-flex;align-items:center;justify-content:center;min-height:18px;padding:0 7px;border-radius:999px;border:1px solid rgba(255,210,128,.4);background:rgba(255,210,128,.15);color:#ffe5ba;font-size:10px;font-weight:700;letter-spacing:.45px;text-transform:uppercase}
.sc-link-visibility{border-color:rgba(140,217,255,.4);background:rgba(140,217,255,.12);color:#bee7ff}
.sc-link-origin{border-color:rgba(184,194,216,.36);background:rgba(184,194,216,.1);color:#d4dbeb}
.sc-link-text{color:#e3efff;font-weight:600;font-size:13px;line-height:1.35;word-break:break-word}
.sc-link-dest{margin-top:6px;color:#9caec7;font-size:11px;font-family:Consolas,SFMono-Regular,Monaco,monospace;word-break:break-word}
.sc-link-id{margin-top:6px;color:#b5c5dc;font-size:11px}
.sc-debugger-settings-group{display:grid;gap:10px}
.sc-debugger-switch{display:grid;grid-template-columns:46px 1fr;align-items:center;gap:10px;cursor:pointer;color:#d8e4f8;font-size:13px;user-select:none}
.sc-debugger-switch input{position:absolute;opacity:0;pointer-events:none}
.sc-debugger-slider{width:44px;height:24px;border-radius:999px;border:1px solid rgba(255,255,255,.2);background:rgba(188,201,222,.25);position:relative}
.sc-debugger-slider::after{content:"";position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#f2f6ff;transition:transform .2s ease}
.sc-debugger-switch input:checked + .sc-debugger-slider{border-color:rgba(111,232,159,.7);background:rgba(111,232,159,.45)}
.sc-debugger-switch input:checked + .sc-debugger-slider::after{transform:translateX(19px);background:#f0fff6}
.sc-debugger-color-group{margin-top:14px;padding-top:14px;border-top:1px solid #25344b;display:grid;gap:8px}
.sc-debugger-number-group{margin-top:4px;display:grid;gap:8px}
.sc-debugger-number-group label{color:#9caec7;font-size:13px}
.sc-debugger-number-input{width:110px;min-height:34px;border-radius:8px;border:1px solid rgba(77,196,255,.34);background:rgba(8,15,27,.78);color:#e6edf7;font-family:Consolas,SFMono-Regular,Monaco,monospace;font-size:13px;padding:6px 8px}
.sc-debugger-number-input:focus{outline:none;border-color:rgba(111,232,159,.7);box-shadow:0 0 0 3px rgba(111,232,159,.16)}
.sc-debugger-setting-note{margin:0;color:#9caec7;font-size:11px;line-height:1.4}
.sc-debugger-color-group label{color:#9caec7;font-size:13px}
#sc-underline-color{width:68px;height:36px;border-radius:8px;border:1px solid rgba(255,255,255,.18);background:transparent;cursor:pointer}
.sc-debugger-controls{position:fixed;right:20px;bottom:20px;display:flex;gap:8px;align-items:center;pointer-events:none;z-index:9997}
#sc-debugger-toggle-btn,.sc-debugger-nav-btn{pointer-events:auto}
#sc-debugger-toggle-btn{width:50px;height:50px;border:1px solid rgba(146,248,191,.42);border-radius:14px;background:linear-gradient(160deg,#8af5bb,#63e0a1);color:#0b2a1d;font-size:22px;font-weight:800;cursor:grab;touch-action:none;user-select:none;box-shadow:0 12px 26px rgba(11,42,29,.34)}
#sc-debugger-toggle-btn.active{border-color:rgba(255,158,158,.5);background:linear-gradient(170deg,#ffa9a9,#ff7c7c);color:#3e1010}
#sc-debugger-toggle-btn.sc-debugger-dragging{cursor:grabbing}
.sc-debugger-nav-btn{width:42px;height:42px;border-radius:10px;border:1px solid rgba(77,196,255,.35);background:rgba(77,196,255,.13);color:#d6efff;font-size:18px;cursor:pointer}
.sc-debugger-nav-btn:hover{background:rgba(77,196,255,.2)}
#passages a.sc-debugger-underlined,#passages button.sc-debugger-underlined,#passages .link-internal.sc-debugger-underlined{border-bottom-width:2px!important;border-bottom-style:dashed!important;border-bottom-color:#4dc4ff!important}
.sc-var-editor-hint{margin:0 0 12px;color:#9caec7;font-size:12px;line-height:1.45}
.sc-var-editor-search{margin-bottom:10px}
.sc-var-editor-label{display:block;margin-bottom:7px;color:#d7e7ff;font-size:12px;font-weight:700;letter-spacing:.45px;text-transform:uppercase}
.sc-var-editor-search input{width:100%;min-height:40px;padding:9px 12px;border-radius:9px;border:1px solid rgba(77,196,255,.3);background:rgba(8,15,27,.72);color:#e6edf7;font-size:14px;font-family:Consolas,SFMono-Regular,Monaco,monospace}
.sc-var-editor-search input:focus{outline:none;border-color:rgba(111,232,159,.72);box-shadow:0 0 0 3px rgba(111,232,159,.18)}
.sc-var-editor-loading{margin-bottom:10px;color:#b9d8ef;font-size:12px;display:flex;align-items:center;gap:8px}
.sc-var-editor-loading::before{content:"";width:11px;height:11px;border-radius:50%;border:2px solid rgba(77,196,255,.4);border-top-color:#92f8bf;animation:scdbgspin .8s linear infinite}
.sc-var-editor-loading[hidden]{display:none}
.sc-var-editor-results{max-height:280px;overflow-y:auto;border-radius:10px;border:1px solid rgba(77,196,255,.22);background:rgba(9,16,28,.56);padding:4px;margin-bottom:12px}
.sc-var-editor-result-item{width:100%;display:grid;gap:4px;text-align:left;border:1px solid transparent;border-radius:8px;padding:9px 10px;background:transparent;color:#e6edf7;cursor:pointer}
.sc-var-editor-result-item:hover{border-color:rgba(111,232,159,.36);background:rgba(111,232,159,.12)}
.sc-var-editor-result-item:focus-visible{outline:none;border-color:rgba(111,232,159,.82);box-shadow:0 0 0 3px rgba(111,232,159,.18)}
.sc-var-editor-result-item.is-selected{border-color:rgba(77,196,255,.65);background:rgba(77,196,255,.17)}
.sc-var-editor-result-name{color:#f7efab;font-size:12px;font-weight:700;line-height:1.35;word-break:break-word}
.sc-var-editor-result-meta{display:flex;flex-wrap:wrap;gap:6px}
.sc-var-editor-result-type{display:inline-flex;align-items:center;justify-content:center;min-height:18px;padding:0 7px;border-radius:999px;border:1px solid rgba(140,217,255,.46);background:rgba(140,217,255,.13);color:#cbecff;font-size:10px;font-weight:700;letter-spacing:.45px;text-transform:uppercase}
.sc-var-editor-result-value{color:#c0cce0;font-family:Consolas,SFMono-Regular,Monaco,monospace;font-size:11px;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sc-var-editor-selected{border-radius:10px;border:1px solid rgba(111,232,159,.28);background:rgba(111,232,159,.08);padding:12px}
.sc-var-editor-selected[hidden]{display:none}
.sc-var-editor-selected-title{margin:0 0 10px;color:#d6ffe8;font-size:13px;font-weight:700;letter-spacing:.4px}
.sc-var-editor-path{padding:8px 9px;border-radius:8px;border:1px solid rgba(77,196,255,.35);background:rgba(8,15,27,.7);color:#f7efab;font-family:Consolas,SFMono-Regular,Monaco,monospace;font-size:12px;margin-bottom:9px;word-break:break-word}
.sc-var-editor-current{padding:8px 9px;border-radius:8px;border:1px solid rgba(220,229,245,.2);background:rgba(10,17,30,.82);color:#d7e3f8;font-size:12px;font-family:Consolas,SFMono-Regular,Monaco,monospace;white-space:pre-wrap;word-break:break-word;max-height:180px;overflow-y:auto;margin-bottom:10px}
.sc-var-editor-input-group{margin-bottom:10px}
.sc-var-editor-input-group label{display:block;margin-bottom:6px;color:#dce8fb;font-size:12px;font-weight:700;letter-spacing:.35px}
.sc-var-editor-input-group textarea,.sc-var-editor-input-group input{width:100%;border-radius:9px;border:1px solid rgba(77,196,255,.33);background:rgba(8,15,27,.82);color:#e6edf7;font-family:Consolas,SFMono-Regular,Monaco,monospace;font-size:12px;line-height:1.45;padding:9px 11px;resize:vertical}
.sc-var-editor-input-group textarea:focus,.sc-var-editor-input-group input:focus{outline:none;border-color:rgba(111,232,159,.78);box-shadow:0 0 0 3px rgba(111,232,159,.18)}
.sc-var-editor-buttons{display:grid;grid-template-columns:repeat(3,minmax(90px,1fr));gap:8px;margin-bottom:8px}
.sc-var-editor-btn{min-height:36px;border-radius:9px;border:1px solid rgba(255,255,255,.18);background:rgba(8,15,27,.75);color:#dbe8ff;font-size:12px;font-weight:700;cursor:pointer}
.sc-var-editor-btn-save{border-color:rgba(111,232,159,.48);color:#b8ffd4;background:rgba(111,232,159,.14)}
.sc-var-editor-btn-reset{border-color:rgba(255,210,128,.45);color:#ffe8bc;background:rgba(255,210,128,.14)}
.sc-var-editor-btn-clear{border-color:rgba(255,158,158,.5);color:#ffd2d2;background:rgba(255,158,158,.15)}
#sc-var-editor-status{min-height:34px;display:none;align-items:center;justify-content:center;text-align:center;border-radius:8px;padding:0 10px;font-size:12px;font-weight:700}
#sc-var-editor-status.success{display:flex;border:1px solid rgba(111,232,159,.45);background:rgba(111,232,159,.14);color:#bfffd8}
#sc-var-editor-status.error{display:flex;border:1px solid rgba(255,158,158,.45);background:rgba(255,158,158,.14);color:#ffd4d4}
.sc-links-filter-bar{display:flex;flex-wrap:wrap;gap:8px;margin:4px 0 10px}
.sc-links-filter-btn{min-width:66px}
.sc-links-filter-btn.is-active{border-color:rgba(111,232,159,.52);background:rgba(111,232,159,.2);color:#d7ffe8}
.sc-var-editor-result-pin{display:inline-flex;align-items:center;justify-content:center;min-height:18px;padding:0 7px;border-radius:999px;border:1px solid rgba(111,232,159,.42);background:rgba(111,232,159,.16);color:#c6ffda;font-size:10px;font-weight:700;letter-spacing:.42px;text-transform:uppercase}
.sc-var-editor-buttons{grid-template-columns:repeat(4,minmax(90px,1fr))}
.sc-var-editor-btn.is-active{border-color:rgba(111,232,159,.48);background:rgba(111,232,159,.22);color:#c7ffe0}
.sc-var-editor-pins{margin-top:12px;padding-top:12px;border-top:1px dashed rgba(111,232,159,.32)}
.sc-var-editor-pin-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:stretch;margin-bottom:8px}
.sc-var-editor-pin-row:last-child{margin-bottom:0}
.sc-var-editor-pin-remove{min-width:74px;border-radius:8px;border:1px solid rgba(255,158,158,.48);background:rgba(255,158,158,.16);color:#ffd3d3;font-size:11px;font-weight:700;cursor:pointer;transition:background .18s ease,transform .16s ease}
.sc-var-editor-pin-remove:hover{background:rgba(255,158,158,.26);transform:translateY(-1px)}
.sc-var-editor-pin-remove:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(255,158,158,.2)}
@media (max-width:780px){.sc-debugger-modal{width:98vw;height:92vh;border-radius:12px}.sc-debugger-tabs{grid-template-columns:repeat(2,1fr);gap:6px}.sc-debugger-variable{grid-template-columns:1fr;gap:6px}.sc-macro-usage-row-btn{grid-template-columns:1fr;gap:6px}.sc-var-editor-buttons{grid-template-columns:1fr}.sc-debugger-controls{right:12px;bottom:12px}}
@keyframes scdbgspin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
`;
const styleEl = document.createElement("style");
styleEl.id = "sc-debugger-inline-styles";
styleEl.textContent = css;
targetRoot.appendChild(styleEl);
}
function notify(message, duration = 2e3, mountRoot = document.body, styleRoot = document.head) {
ensureDebuggerStylesInjected(styleRoot);
let box = mountRoot?.querySelector?.("#sc-debugger-notify") || document.getElementById("sc-debugger-notify");
if (!box) {
box = document.createElement("div");
box.id = "sc-debugger-notify";
Object.assign(box.style, {
position: "fixed",
bottom: "84px",
right: "20px",
background: "#18253a",
color: "#e6edf7",
padding: "10px 14px",
border: "1px solid #4dc4ff",
borderRadius: "8px",
boxShadow: "0 8px 24px rgba(2, 8, 20, 0.45)",
zIndex: 9996,
fontFamily: "Consolas, monospace",
fontSize: "12px",
display: "none",
transition: "opacity 0.2s ease"
});
mountRoot.appendChild(box);
}
box.textContent = message;
box.style.display = "block";
box.style.opacity = "1";
window.clearTimeout(box._scHideTimer);
box._scHideTimer = window.setTimeout(() => {
box.style.opacity = "0";
window.setTimeout(() => {
box.style.display = "none";
}, 200);
}, duration);
}
function applyLinkUnderlines(config) {
const shouldUnderline = Boolean(config.getSetting("showUnderlines"));
const color = config.getSetting("underlineColor") || "#4dc4ff";
const style = config.getSetting("underlineStyle") || "dashed";
const root = document.getElementById("passages") || document;
const elements = root.querySelectorAll("a, button, .link-internal");
elements.forEach((el) => {
const dest = el.getAttribute("data-passage") || el.getAttribute("data-setter") || "Executes Code/Macro";
if (shouldUnderline) {
el.classList.add("sc-debugger-underlined");
el.style.borderBottomColor = color;
el.style.borderBottomStyle = style;
el.setAttribute("title", `-> Target: ${dest}`);
} else {
el.classList.remove("sc-debugger-underlined");
el.style.borderBottom = "none";
}
});
}
var clickCaptureBound = false;
function captureClickEvents() {
if (clickCaptureBound) return;
clickCaptureBound = true;
document.addEventListener(
"click",
(event) => {
const root = document.getElementById("passages");
if (!root) return;
const target = event.target?.closest?.("a, button, .link-internal");
if (!target || !root.contains(target)) return;
const dest = target.getAttribute("data-passage") || target.getAttribute("data-setter") || "Executes Code/Macro";
debugLog("%c[SC Debugger - Link Clicked]", "color: #ffcc88; font-weight: bold;", {
text: (target.textContent || "").trim(),
destination: dest,
element: target
});
},
true
);
}
var modalNavBoundRoots = /* @__PURE__ */ new WeakSet();
function wireModalLinkNavigation(root = document) {
if (root && modalNavBoundRoots.has(root)) return;
if (root) modalNavBoundRoots.add(root);
root.addEventListener("click", (event) => {
const modal = root.querySelector?.("#sc-debugger-modal") || document.getElementById("sc-debugger-modal");
if (!modal) return;
const row = event.target?.closest?.(".sc-debugger-link-clickable");
if (!row || !modal.contains(row)) return;
event.preventDefault();
const action = row.getAttribute("data-sc-action") || "navigate";
if (action === "dom-click") {
const domId = row.getAttribute("data-sc-dom-id");
if (domId) {
const selectorId = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(domId) : domId;
const liveElement = document.querySelector(
`#passages [data-sc-debugger-link-id="${selectorId}"]`
);
if (liveElement && typeof liveElement.click === "function") {
liveElement.click();
return;
}
}
}
const destination = row.getAttribute("data-sc-destination");
if (!destination) return;
const engine = typeof Engine !== "undefined" && Engine || typeof SugarCube !== "undefined" && SugarCube.Engine || null;
if (engine && typeof engine.play === "function") {
engine.play(destination);
} else {
debugWarn("SugarCube Engine not found; cannot navigate to:", destination);
}
});
}
function makeToggleButtonDraggable(btnEl, storageKey = "sugarcubeDebuggerBtnPos") {
if (!btnEl) return;
const applyPosition = (left, top) => {
const maxLeft = Math.max(0, window.innerWidth - btnEl.offsetWidth);
const maxTop = Math.max(0, window.innerHeight - btnEl.offsetHeight);
const clampedLeft = Math.min(Math.max(0, left), maxLeft);
const clampedTop = Math.min(Math.max(0, top), maxTop);
btnEl.style.left = `${clampedLeft}px`;
btnEl.style.top = `${clampedTop}px`;
btnEl.style.right = "auto";
btnEl.style.bottom = "auto";
try {
localStorage.setItem(storageKey, JSON.stringify({ left: clampedLeft, top: clampedTop }));
} catch {
}
};
try {
const raw = localStorage.getItem(storageKey);
if (raw) {
const parsed = JSON.parse(raw);
if (typeof parsed?.left === "number" && typeof parsed?.top === "number") {
applyPosition(parsed.left, parsed.top);
}
}
} catch {
}
let isDragging = false;
let startLeft = 0;
let startTop = 0;
let startX = 0;
let startY = 0;
let moved = false;
let pressedButton = null;
let activePointerId = null;
const onPointerMove = (event) => {
if (!isDragging || event.pointerId !== activePointerId) return;
const dx = event.clientX - startX;
const dy = event.clientY - startY;
if (!moved && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) {
moved = true;
}
if (moved) {
applyPosition(startLeft + dx, startTop + dy);
}
};
const detachDragListeners = () => {
window.removeEventListener("pointermove", onPointerMove, true);
window.removeEventListener("pointerup", onPointerEnd, true);
window.removeEventListener("pointercancel", onPointerEnd, true);
};
const endDrag = () => {
if (!isDragging) return;
isDragging = false;
btnEl.classList.remove("sc-debugger-dragging");
if (moved && pressedButton) {
const targetButton = pressedButton;
targetButton._scJustDragged = true;
window.setTimeout(() => {
targetButton._scJustDragged = false;
}, 120);
}
moved = false;
pressedButton = null;
activePointerId = null;
detachDragListeners();
};
const onPointerEnd = (event) => {
if (event.pointerId !== activePointerId) return;
endDrag();
};
const ensureLeftTop = () => {
const left = parseFloat(btnEl.style.left);
const top = parseFloat(btnEl.style.top);
if (!Number.isFinite(left) || !Number.isFinite(top)) {
const rect = btnEl.getBoundingClientRect();
applyPosition(rect.left, rect.top);
return { left: rect.left, top: rect.top };
}
return { left, top };
};
btnEl.addEventListener("pointerdown", (event) => {
if (event.button != null && event.button !== 0) return;
const eventTarget = event.target;
const targetElement = eventTarget instanceof Element ? eventTarget : eventTarget?.parentElement || null;
pressedButton = targetElement?.closest?.("button") || null;
isDragging = true;
moved = false;
activePointerId = event.pointerId;
startX = event.clientX;
startY = event.clientY;
const pos = ensureLeftTop();
startLeft = pos.left;
startTop = pos.top;
btnEl.classList.add("sc-debugger-dragging");
window.addEventListener("pointermove", onPointerMove, true);
window.addEventListener("pointerup", onPointerEnd, true);
window.addEventListener("pointercancel", onPointerEnd, true);
});
window.addEventListener("resize", () => {
const rect = btnEl.getBoundingClientRect();
applyPosition(rect.left, rect.top);
});
}
// src/main.js
var SugarCubeDebugger = class {
constructor() {
this.uiHost = null;
this.shadowRoot = null;
this.modal = null;
this.isInitialized = false;
this.toggleBtn = null;
this.editorUi = null;
this.debuggerMetaVariableKey = "__scdbg_meta_b3f4a81d9427";
this.debuggerMetaPersistentKey = "__scdbg_meta_pins_b3f4a81d9427";
this.originalHistoryConfig = null;
this.engineHistoryPatch = null;
}
init() {
if (this.isInitialized) return;
setDebugLoggingEnabled(Boolean(config_default.getSetting("debugLogging")));
this.ensureUiRoot();
ensureDebuggerStylesInjected(this.shadowRoot);
this.modal = new modal_default(this.shadowRoot);
this.createToggleButton();
this.attachKeyboardShortcuts();
this.attachPassageChangeListener();
this.modal.onSettingsChange = (setting, value) => {
config_default.setSetting(setting, value);
this.applySettings();
this.refreshDebugInfo();
};
this.modal.onClose = () => {
this.cleanup();
};
this.isInitialized = true;
notify(
"SugarCube Debugger Ready (Press Ctrl+D to toggle)",
2e3,
this.shadowRoot,
this.shadowRoot
);
this.applySettings();
}
ensureUiRoot() {
let host = document.getElementById("sc-debugger-host");
if (!host) {
host = document.createElement("div");
host.id = "sc-debugger-host";
host.style.position = "fixed";
host.style.right = "0";
host.style.bottom = "0";
host.style.width = "220px";
host.style.height = "130px";
host.style.overflow = "visible";
host.style.background = "transparent";
host.style.zIndex = "2147483647";
host.style.pointerEvents = "none";
document.body.appendChild(host);
}
if (!host.shadowRoot) {
host.attachShadow({ mode: "open" });
}
this.uiHost = host;
this.shadowRoot = host.shadowRoot;
}
createToggleButton() {
if (!this.shadowRoot) return;
if (this.shadowRoot.getElementById("sc-debugger-controls")) return;
const controls = document.createElement("div");
controls.id = "sc-debugger-controls";
controls.className = "sc-debugger-controls";
controls.style.pointerEvents = "auto";
controls.innerHTML = `
<button id="sc-debugger-back-btn" class="sc-debugger-nav-btn" type="button" title="Back (Engine.backward)" hidden>
←
</button>
<button id="sc-debugger-toggle-btn" type="button" title="Toggle Debugger (Ctrl+D)">D</button>
<button id="sc-debugger-forward-btn" class="sc-debugger-nav-btn" type="button" title="Forward (Engine.forward)" hidden>
→
</button>
`;
controls.addEventListener("click", (event) => {
const eventTarget = event.target;
const targetElement = eventTarget instanceof Element ? eventTarget : eventTarget?.parentElement || null;
const button = targetElement?.closest?.("button");
if (!button) return;
if (button._scJustDragged) return;
if (button.id === "sc-debugger-toggle-btn") {
this.toggle();
return;
}
if (button.id === "sc-debugger-back-btn") {
this.navigateHistory("backward");
return;
}
if (button.id === "sc-debugger-forward-btn") {
this.navigateHistory("forward");
}
});
this.shadowRoot.appendChild(controls);
makeToggleButtonDraggable(controls);
this.toggleBtn = controls.querySelector("#sc-debugger-toggle-btn");
this.updateNavControls();
}
attachKeyboardShortcuts() {
const isToggleShortcut = (event) => (event.ctrlKey || event.metaKey) && (event.key === "d" || event.key === "D");
const stopPropagation = (event) => {
if (typeof event.stopImmediatePropagation === "function") {
event.stopImmediatePropagation();
}
event.stopPropagation();
};
document.addEventListener(
"keydown",
(event) => {
if (isToggleShortcut(event)) {
event.preventDefault();
stopPropagation(event);
this.toggle();
return;
}
if (event.key === "Escape" && this.modal?.isOpen) {
event.preventDefault();
stopPropagation(event);
this.modal.close();
}
},
true
);
const trapModalKeyEvent = (event) => {
if (!this.modal?.isOpen) return;
if (isToggleShortcut(event)) return;
if (event.key === "Escape") return;
stopPropagation(event);
};
this.shadowRoot?.addEventListener("keydown", trapModalKeyEvent);
this.shadowRoot?.addEventListener("keypress", trapModalKeyEvent);
this.shadowRoot?.addEventListener("keyup", trapModalKeyEvent);
}
attachPassageChangeListener() {
document.addEventListener(":passageend", () => this.onPassageEnd());
document.addEventListener(":passagerender", () => this.onPassageEnd());
if (typeof window.jQuery !== "undefined") {
window.jQuery(document).on(":passageend.sc-debugger", () => this.onPassageEnd());
}
}
onPassageEnd() {
this.applySettings();
if (this.modal?.isOpen) {
this.refreshDebugInfo();
}
}
toggle() {
if (this.modal?.isOpen) {
this.modal.close();
} else {
this.open();
}
}
open() {
if (!this.modal) return;
this.modal.open();
this.modal.updateSettings(config_default.getSettings());
this.refreshDebugInfo();
this.applySettings();
this.shadowRoot?.getElementById("sc-debugger-toggle-btn")?.classList.add("active");
}
refreshDebugInfo() {
if (!this.modal) return;
this.modal.setContent(renderer_default.render(config_default));
this.modal.setEditorContent(renderer_default.renderVariableEditor());
const linksWarning = "Warning: Link actions try to click the live passage element first. If unavailable, debugger falls back to Engine.play(destination), which may skip original side-effects.";
this.modal.setLinksExplorerContent(renderer_default.renderLinksExplorer(), linksWarning);
this.bindDebugInfoEvents();
this.bindEditorEvents();
this.bindLinksExplorerEvents();
}
bindDebugInfoEvents() {
if (!this.shadowRoot) return;
const debugOutput = this.shadowRoot.getElementById("sc-debugger-output");
if (!debugOutput || debugOutput._scDebugInfoBound) return;
debugOutput._scDebugInfoBound = true;
debugOutput.addEventListener("click", (event) => {
const target = event.target;
const targetElement = target instanceof Element ? target : target?.parentElement || null;
const rowButton = targetElement?.closest?.(".sc-macro-usage-row-btn");
if (!rowButton || !debugOutput.contains(rowButton)) return;
const row = rowButton.closest(".sc-macro-usage-row");
const detailBox = row?.querySelector(".sc-macro-usage-details");
if (!row || !detailBox) return;
const isExpanding = !row.classList.contains("is-expanded");
row.classList.toggle("is-expanded", isExpanding);
rowButton.setAttribute("aria-expanded", isExpanding ? "true" : "false");
if (!isExpanding) {
detailBox.hidden = true;
return;
}
if (!row.dataset.scMacroLoaded) {
const macroName = row.getAttribute("data-sc-macro-name") || "";
detailBox.innerHTML = renderer_default.renderMacroUsageDetails(macroName);
row.dataset.scMacroLoaded = "1";
}
detailBox.hidden = false;
});
}
bindEditorEvents() {
if (!this.shadowRoot) return;
const searchInput = this.shadowRoot.getElementById("sc-var-editor-search");
const resultsContainer = this.shadowRoot.getElementById("sc-var-editor-results");
const selectedDiv = this.shadowRoot.getElementById("sc-var-editor-selected");
const valueInput = this.shadowRoot.getElementById("sc-var-editor-input");
const saveBtn = this.shadowRoot.getElementById("sc-var-editor-save");
const resetBtn = this.shadowRoot.getElementById("sc-var-editor-reset");
const pinBtn = this.shadowRoot.getElementById("sc-var-editor-pin");
const clearBtn = this.shadowRoot.getElementById("sc-var-editor-clear");
const statusDiv = this.shadowRoot.getElementById("sc-var-editor-status");
const loadingDiv = this.shadowRoot.getElementById("sc-var-editor-loading");
const pinsList = this.shadowRoot.getElementById("sc-var-editor-pins-list");
if (!searchInput || !resultsContainer || !valueInput) return;
let searchSeq = 0;
let searchTimer = null;
let statusTimer = null;
const escapeHtml = (text) => renderer_default.escapeHtml(String(text || ""));
const setLoading = (text = "") => {
if (!loadingDiv) return;
loadingDiv.textContent = text;
loadingDiv.hidden = !text;
};
const setStatus = (message = "", kind = "") => {
if (!statusDiv) return;
statusDiv.classList.remove("success", "error");
statusDiv.textContent = message;
if (!message) return;
if (kind) statusDiv.classList.add(kind);
if (statusTimer) window.clearTimeout(statusTimer);
statusTimer = window.setTimeout(() => {
statusDiv.classList.remove("success", "error");
statusDiv.textContent = "";
}, 2200);
};
const renderResults = (results, maxResults = 200) => {
if (!results.length) {
resultsContainer.innerHTML = `<p class="sc-debugger-empty">No variables found.</p>`;
return;
}
const pinnedSet = new Set(this.getPinnedPaths());
const shown = results.slice(0, maxResults);
let html = "";
if (results.length > maxResults) {
html += `<p class="sc-debugger-empty">Showing first ${maxResults} results. Refine your search for more precision.</p>`;
}
html += shown.map((item) => {
const preview = item.valueStr.length > 95 ? `${item.valueStr.slice(0, 95).trimEnd()}...` : item.valueStr;
const selectedClass = item.path === variableEditor_default.selectedVar ? " is-selected" : "";
const pinnedBadge = pinnedSet.has(item.path) ? `<span class="sc-var-editor-result-pin">Pinned</span>` : "";
return `
<button
type="button"
class="sc-var-editor-result-item${selectedClass}"
data-path="${escapeHtml(item.path)}"
title="${escapeHtml(item.displayPath)}"
>
<div class="sc-var-editor-result-name">${escapeHtml(item.displayPath)}</div>
<div class="sc-var-editor-result-meta">
<span class="sc-var-editor-result-type">${escapeHtml(item.type)}</span>
${pinnedBadge}
</div>
<div class="sc-var-editor-result-value">${escapeHtml(preview)}</div>
</button>
`;
}).join("");
resultsContainer.innerHTML = html;
};
const updateVisibleResultPinBadges = () => {
const pinnedSet = new Set(this.getPinnedPaths());
const items = resultsContainer.querySelectorAll(".sc-var-editor-result-item");
items.forEach((item) => {
const path = item.getAttribute("data-path");
if (!path) return;
const meta = item.querySelector(".sc-var-editor-result-meta");
if (!meta) return;
const badge = meta.querySelector(".sc-var-editor-result-pin");
const pinned = pinnedSet.has(path);
if (pinned && !badge) {
const newBadge = document.createElement("span");
newBadge.className = "sc-var-editor-result-pin";
newBadge.textContent = "Pinned";
meta.appendChild(newBadge);
}
if (!pinned && badge) {
badge.remove();
}
});
};
const updatePinButton = () => {
if (!pinBtn) return;
const selectedPath = this.normalizeVariablePath(variableEditor_default.selectedVar);
if (!selectedPath) {
pinBtn.disabled = true;
pinBtn.textContent = "Pin";
pinBtn.classList.remove("is-active");
pinBtn.setAttribute("aria-pressed", "false");
return;
}
const pinned = this.isPathPinned(selectedPath);
pinBtn.disabled = false;
pinBtn.textContent = pinned ? "Unpin" : "Pin";
pinBtn.classList.toggle("is-active", pinned);
pinBtn.setAttribute("aria-pressed", pinned ? "true" : "false");
};
const renderPinnedList = () => {
if (!pinsList) return;
const pinnedPaths = this.getPinnedPaths();
if (!pinnedPaths.length) {
pinsList.innerHTML = `<p class="sc-debugger-empty">No pinned variables yet.</p>`;
return;
}
const rows = pinnedPaths.map((path) => {
const value = variableEditor_default.getVariable(path);
const type = variableEditor_default.getValueType(value);
const selectedClass = path === variableEditor_default.selectedVar ? " is-selected" : "";
const displayPath = `$${path}`;
const preview = variableEditor_default.formatValue(value, { maxChars: 260 }).replace(/\s+/g, " ").trim();
const compactPreview = preview.length > 110 ? `${preview.slice(0, 110).trimEnd()}...` : preview;
return `
<div class="sc-var-editor-pin-row${selectedClass}">
<button
type="button"
class="sc-var-editor-result-item sc-var-editor-pin-select${selectedClass}"
data-path="${escapeHtml(path)}"
title="${escapeHtml(displayPath)}"
>
<div class="sc-var-editor-result-name">${escapeHtml(displayPath)}</div>
<div class="sc-var-editor-result-meta">
<span class="sc-var-editor-result-type">${escapeHtml(type)}</span>
<span class="sc-var-editor-result-pin">Pinned</span>
</div>
<div class="sc-var-editor-result-value">${escapeHtml(compactPreview || "undefined")}</div>
</button>
<button
type="button"
class="sc-var-editor-pin-remove"
data-path="${escapeHtml(path)}"
title="Remove pin for ${escapeHtml(displayPath)}"
>
Unpin
</button>
</div>
`;
}).join("");
pinsList.innerHTML = rows;
};
const handleSearch = () => {
const query = searchInput.value;
if (searchTimer) window.clearTimeout(searchTimer);
const seq = ++searchSeq;
searchTimer = window.setTimeout(async () => {
setLoading("Indexing variables...");
try {
const results = await variableEditor_default.searchAsync(query, {
frameBudgetMs: 6,
onProgress: ({ processed }) => {
if (seq !== searchSeq) return;
setLoading(`Indexing variables... (${processed} scanned)`);
}
});
if (seq !== searchSeq) return;
setLoading("");
renderResults(results);
} catch (error) {
if (seq !== searchSeq) return;
setLoading("");
setStatus(`Search failed: ${error.message}`, "error");
resultsContainer.innerHTML = `<p class="sc-debugger-empty">Search failed. Check console for details.</p>`;
}
}, 150);
};
searchInput.addEventListener("input", handleSearch);
resultsContainer.addEventListener("click", (event) => {
const target = event.target?.closest?.(".sc-var-editor-result-item");
if (!target || !resultsContainer.contains(target)) return;
const path = target.getAttribute("data-path");
if (!path) return;
this.selectVariable(path);
setStatus("");
});
saveBtn?.addEventListener("click", () => {
if (!variableEditor_default.selectedVar) return;
try {
const newValue = variableEditor_default.parseValue(valueInput.value);
variableEditor_default.setValue(variableEditor_default.selectedVar, newValue);
this.selectVariable(variableEditor_default.selectedVar);
setStatus(`Saved ${variableEditor_default.selectedVar}`, "success");
if (searchInput.value.trim()) {
handleSearch();
}
} catch (error) {
setStatus(`Error: ${error.message}`, "error");
}
});
resetBtn?.addEventListener("click", () => {
if (!variableEditor_default.selectedVar) return;
const currentVal = variableEditor_default.getVariable(variableEditor_default.selectedVar);
valueInput.value = variableEditor_default.formatValue(currentVal);
});
pinBtn?.addEventListener("click", () => {
const selectedPath = this.normalizeVariablePath(variableEditor_default.selectedVar);
if (!selectedPath) return;
const { pinned, success } = this.togglePinnedPath(selectedPath);
if (!success) {
setStatus("Pin storage is not ready yet. Open after SugarCube finishes loading.", "error");
return;
}
renderPinnedList();
updatePinButton();
updateVisibleResultPinBadges();
setStatus(`${pinned ? "Pinned" : "Unpinned"} $${selectedPath}`, "success");
});
clearBtn?.addEventListener("click", () => {
variableEditor_default.selectedVar = null;
selectedDiv?.setAttribute("hidden", "");
searchInput.value = "";
setLoading("");
setStatus("");
updatePinButton();
resultsContainer.innerHTML = `<p class="sc-debugger-empty">Start typing to search variables.</p>`;
});
pinsList?.addEventListener("click", (event) => {
const removeButton = event.target?.closest?.(".sc-var-editor-pin-remove");
if (removeButton && pinsList.contains(removeButton)) {
const path2 = this.normalizeVariablePath(removeButton.getAttribute("data-path"));
if (!path2) return;
const success = this.removePinnedPath(path2);
if (!success) {
setStatus("Could not remove pin from storage.", "error");
return;
}
renderPinnedList();
updatePinButton();
updateVisibleResultPinBadges();
setStatus(`Unpinned $${path2}`, "success");
return;
}
const selectButton = event.target?.closest?.(".sc-var-editor-pin-select");
if (!selectButton || !pinsList.contains(selectButton)) return;
const path = this.normalizeVariablePath(selectButton.getAttribute("data-path"));
if (!path) return;
this.selectVariable(path);
setStatus("");
});
this.editorUi = {
setStatus,
updatePinButton,
renderPinnedList,
updateVisibleResultPinBadges
};
renderPinnedList();
updatePinButton();
if (variableEditor_default.selectedVar) {
this.selectVariable(variableEditor_default.selectedVar);
}
}
selectVariable(path) {
if (!this.shadowRoot) return;
const normalizedPath = this.normalizeVariablePath(path);
if (!normalizedPath) return;
variableEditor_default.selectedVar = normalizedPath;
const pathDisplay = this.shadowRoot.getElementById("sc-var-editor-path-display");
const currentDisplay = this.shadowRoot.getElementById("sc-var-editor-current-display");
const valueInput = this.shadowRoot.getElementById("sc-var-editor-input");
const selectedDiv = this.shadowRoot.getElementById("sc-var-editor-selected");
const resultsContainer = this.shadowRoot.getElementById("sc-var-editor-results");
const fullPath = `$${normalizedPath}`;
const currentValue = variableEditor_default.getVariable(normalizedPath);
const formattedValue = variableEditor_default.formatValue(currentValue);
if (pathDisplay) {
pathDisplay.textContent = `Path: ${fullPath}`;
}
if (currentDisplay) {
currentDisplay.textContent = `Current Value:
${formattedValue}`;
}
if (valueInput) {
valueInput.value = formattedValue;
valueInput.focus();
}
if (selectedDiv) {
selectedDiv.removeAttribute("hidden");
}
if (resultsContainer) {
resultsContainer.querySelectorAll(".sc-var-editor-result-item").forEach((item) => {
const isCurrent = item.getAttribute("data-path") === normalizedPath;
item.classList.toggle("is-selected", isCurrent);
});
}
this.editorUi?.renderPinnedList?.();
this.editorUi?.updatePinButton?.();
this.editorUi?.updateVisibleResultPinBadges?.();
}
normalizeVariablePath(path) {
const normalized = String(path || "").trim().replace(/^\$/, "");
if (!normalized) return "";
if (normalized === this.debuggerMetaVariableKey) return "";
if (normalized.startsWith(`${this.debuggerMetaVariableKey}.`)) return "";
if (normalized.startsWith(`${this.debuggerMetaVariableKey}[`)) return "";
return normalized;
}
getStateVariablesRoot() {
const root = SugarCube?.State?.variables;
if (!root || typeof root !== "object") return null;
return root;
}
getDebuggerMetaStore({ create = true } = {}) {
const root = this.getStateVariablesRoot();
if (!root) return null;
let meta = root[this.debuggerMetaVariableKey];
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
if (!create) return null;
meta = {
version: 1,
pins: [],
updatedAt: 0
};
root[this.debuggerMetaVariableKey] = meta;
}
if (!Array.isArray(meta.pins)) {
meta.pins = [];
}
if (!Number.isFinite(meta.updatedAt)) {
meta.updatedAt = 0;
}
meta.pins = this.sanitizePinnedPaths(meta.pins);
return meta;
}
getMetadataStoreApi() {
const state = this.resolveStateObject();
const metadata = state?.metadata;
if (!metadata) return null;
if (typeof metadata.get !== "function" || typeof metadata.set !== "function") {
return null;
}
return metadata;
}
readPinnedPathsFromMetadata() {
const metadata = this.getMetadataStoreApi();
if (!metadata) return null;
try {
const raw = metadata.get(this.debuggerMetaPersistentKey);
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
const pins = this.sanitizePinnedPaths(raw.pins);
const updatedAt = Number.isFinite(raw.updatedAt) ? raw.updatedAt : 0;
return { pins, updatedAt };
} catch {
return null;
}
}
writePinnedPathsToMetadata(paths, updatedAt) {
const metadata = this.getMetadataStoreApi();
if (!metadata) return false;
try {
metadata.set(this.debuggerMetaPersistentKey, {
version: 1,
pins: this.sanitizePinnedPaths(paths),
updatedAt: Number.isFinite(updatedAt) ? updatedAt : Date.now()
});
return true;
} catch {
return false;
}
}
sanitizePinnedPaths(paths) {
if (!Array.isArray(paths)) return [];
const seen = /* @__PURE__ */ new Set();
const cleaned = [];
for (const rawPath of paths) {
const path = this.normalizeVariablePath(rawPath);
if (!path) continue;
if (seen.has(path)) continue;
seen.add(path);
cleaned.push(path);
if (cleaned.length >= 250) break;
}
return cleaned;
}
getPinnedPaths() {
const meta = this.getDebuggerMetaStore({ create: false });
const currentPins = Array.isArray(meta?.pins) ? this.sanitizePinnedPaths(meta.pins) : [];
const currentUpdatedAt = Number.isFinite(meta?.updatedAt) ? meta.updatedAt : 0;
const persistent = this.readPinnedPathsFromMetadata();
if (persistent && persistent.updatedAt > currentUpdatedAt) {
const synced = this.setPinnedPaths(persistent.pins, {
updatedAt: persistent.updatedAt,
persistMetadata: false
});
if (Array.isArray(synced)) return synced;
}
if (persistent && currentUpdatedAt > persistent.updatedAt) {
this.writePinnedPathsToMetadata(currentPins, currentUpdatedAt);
}
return [...currentPins];
}
setPinnedPaths(paths, { updatedAt = Date.now(), persistMetadata = true } = {}) {
const meta = this.getDebuggerMetaStore({ create: true });
if (!meta) return null;
meta.pins = this.sanitizePinnedPaths(paths);
meta.updatedAt = Number.isFinite(updatedAt) ? updatedAt : Date.now();
if (persistMetadata) {
this.writePinnedPathsToMetadata(meta.pins, meta.updatedAt);
}
variableEditor_default.invalidateCache();
return [...meta.pins];
}
isPathPinned(path) {
const normalizedPath = this.normalizeVariablePath(path);
if (!normalizedPath) return false;
return this.getPinnedPaths().includes(normalizedPath);
}
addPinnedPath(path) {
const normalizedPath = this.normalizeVariablePath(path);
if (!normalizedPath) return false;
const pinned = this.getPinnedPaths();
if (pinned.includes(normalizedPath)) return true;
pinned.push(normalizedPath);
const next = this.setPinnedPaths(pinned);
return Array.isArray(next) && next.includes(normalizedPath);
}
removePinnedPath(path) {
const normalizedPath = this.normalizeVariablePath(path);
if (!normalizedPath) return false;
const pinned = this.getPinnedPaths();
const next = pinned.filter((item) => item !== normalizedPath);
if (next.length === pinned.length) return false;
const updated = this.setPinnedPaths(next);
return Array.isArray(updated);
}
togglePinnedPath(path) {
const normalizedPath = this.normalizeVariablePath(path);
if (!normalizedPath) {
return { pinned: false, success: false };
}
if (this.isPathPinned(normalizedPath)) {
const success2 = this.removePinnedPath(normalizedPath);
return { pinned: false, success: success2 };
}
const success = this.addPinnedPath(normalizedPath);
return { pinned: true, success };
}
bindLinksExplorerEvents() {
if (!this.shadowRoot) return;
const searchInput = this.shadowRoot.getElementById("sc-links-search");
const rowsContainer = this.shadowRoot.getElementById("sc-links-search-results");
const countLabel = this.shadowRoot.getElementById("sc-links-search-count");
const emptyLabel = this.shadowRoot.getElementById("sc-links-search-empty");
const filterButtons = Array.from(
this.shadowRoot.querySelectorAll("[data-sc-link-filter]")
);
if (!searchInput || !rowsContainer) return;
const rows = Array.from(rowsContainer.querySelectorAll(".sc-debugger-link"));
const total = rows.length;
let activeFilter = "all";
const updateCount = (shown, query) => {
if (!countLabel) return;
const filterLabel = activeFilter === "all" ? "all" : activeFilter;
const queryLabel = query ? ` and search "${query}"` : "";
countLabel.textContent = `Showing ${shown} of ${total} links (filter: ${filterLabel}${queryLabel}).`;
};
const matchesActiveFilter = (row) => {
if (activeFilter === "all") return true;
if (activeFilter === "live" || activeFilter === "hidden") {
return row.getAttribute("data-sc-visibility") === activeFilter;
}
if (activeFilter === "macro" || activeFilter === "link") {
return row.getAttribute("data-sc-kind") === activeFilter;
}
return true;
};
const applyFilter = () => {
const query = String(searchInput.value || "").trim().toLowerCase();
let shown = 0;
rows.forEach((row) => {
const haystack = (row.getAttribute("data-sc-search") || row.textContent || "").toLowerCase();
const queryMatch = !query || haystack.includes(query);
const chipMatch = matchesActiveFilter(row);
const visible = queryMatch && chipMatch;
row.hidden = !visible;
if (visible) shown += 1;
});
if (emptyLabel) {
emptyLabel.hidden = shown !== 0;
}
updateCount(shown, query);
};
searchInput.addEventListener("input", applyFilter);
searchInput.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
if (!searchInput.value) return;
searchInput.value = "";
applyFilter();
});
filterButtons.forEach((button) => {
button.addEventListener("click", () => {
const nextFilter = String(button.getAttribute("data-sc-link-filter") || "all").trim().toLowerCase();
activeFilter = nextFilter || "all";
filterButtons.forEach((candidate) => {
candidate.classList.toggle("is-active", candidate === button);
});
applyFilter();
});
});
applyFilter();
}
resolveEngineObject() {
return typeof Engine !== "undefined" && Engine || typeof SugarCube !== "undefined" && SugarCube.Engine || null;
}
resolveStateObject() {
return typeof State !== "undefined" && State || typeof SugarCube !== "undefined" && SugarCube.State || null;
}
resolveHistoryConfigObject() {
const root = typeof Config !== "undefined" && Config || typeof SugarCube !== "undefined" && SugarCube.Config || null;
return root?.history || null;
}
sanitizeHistoryMaxStates(value) {
const parsed = Number.parseInt(String(value ?? ""), 10);
if (!Number.isFinite(parsed)) return 120;
return Math.min(Math.max(parsed, 2), 2e3);
}
captureOriginalHistoryConfig(historyConfig) {
if (!historyConfig || this.originalHistoryConfig) return;
this.originalHistoryConfig = {
controls: historyConfig.controls,
maxStates: historyConfig.maxStates
};
}
restoreOriginalHistoryConfig(historyConfig) {
if (!historyConfig || !this.originalHistoryConfig) return;
try {
historyConfig.controls = this.originalHistoryConfig.controls;
} catch {
}
try {
historyConfig.maxStates = this.originalHistoryConfig.maxStates;
} catch {
}
}
unpatchEngineHistoryMethods() {
const patch = this.engineHistoryPatch;
if (!patch?.engine) {
this.engineHistoryPatch = null;
return;
}
const {
engine,
originalBackward,
originalForward,
patchedBackward,
patchedForward,
hadBackward,
hadForward
} = patch;
if (engine.backward === patchedBackward) {
try {
if (hadBackward) {
engine.backward = originalBackward;
} else {
delete engine.backward;
}
} catch {
}
}
if (engine.forward === patchedForward) {
try {
if (hadForward) {
engine.forward = originalForward;
} else {
delete engine.forward;
}
} catch {
}
}
this.engineHistoryPatch = null;
}
patchEngineHistoryMethods(engine) {
if (!engine) return;
if (this.engineHistoryPatch?.engine === engine) return;
this.unpatchEngineHistoryMethods();
const hadBackward = typeof engine.backward === "function";
const hadForward = typeof engine.forward === "function";
const originalBackward = hadBackward ? engine.backward : null;
const originalForward = hadForward ? engine.forward : null;
const tryOffset = (offset) => {
if (typeof engine.go === "function") {
try {
const goResult = engine.go(offset);
if (goResult !== false) return true;
} catch {
}
}
const state = this.resolveStateObject();
if (state && typeof state.go === "function") {
try {
const goResult = state.go(offset);
if (goResult !== false) return true;
} catch {
}
}
return false;
};
const patchedBackward = (...args) => {
if (typeof originalBackward === "function") {
try {
const result = originalBackward.apply(engine, args);
if (result !== false) return result;
} catch {
}
}
return tryOffset(-1);
};
const patchedForward = (...args) => {
if (typeof originalForward === "function") {
try {
const result = originalForward.apply(engine, args);
if (result !== false) return result;
} catch {
}
}
return tryOffset(1);
};
try {
engine.backward = patchedBackward;
engine.forward = patchedForward;
this.engineHistoryPatch = {
engine,
originalBackward,
originalForward,
patchedBackward,
patchedForward,
hadBackward,
hadForward
};
} catch (error) {
debugWarn("Could not patch engine history methods:", error);
this.engineHistoryPatch = null;
}
}
applyHistoryOverrides() {
const historyConfig = this.resolveHistoryConfigObject();
const forceHistory = Boolean(config_default.getSetting("forceHistory"));
const maxStates = this.sanitizeHistoryMaxStates(config_default.getSetting("historyMaxStates"));
if (historyConfig) {
this.captureOriginalHistoryConfig(historyConfig);
if (forceHistory) {
try {
historyConfig.controls = true;
} catch {
}
try {
historyConfig.maxStates = maxStates;
} catch (error) {
debugWarn("Could not force Config.history.maxStates:", error);
}
} else {
this.restoreOriginalHistoryConfig(historyConfig);
}
}
const engine = this.resolveEngineObject();
if (forceHistory && engine) {
this.patchEngineHistoryMethods(engine);
} else {
this.unpatchEngineHistoryMethods();
}
}
applySettings() {
setDebugLoggingEnabled(Boolean(config_default.getSetting("debugLogging")));
this.applyHistoryOverrides();
applyLinkUnderlines(config_default);
captureClickEvents();
wireModalLinkNavigation(this.shadowRoot);
this.updateNavControls();
}
updateNavControls() {
if (!this.shadowRoot) return;
const showNavControls = Boolean(config_default.getSetting("showNavControls"));
const forceHistory = Boolean(config_default.getSetting("forceHistory"));
const backBtn = this.shadowRoot.getElementById("sc-debugger-back-btn");
const forwardBtn = this.shadowRoot.getElementById("sc-debugger-forward-btn");
const state = this.resolveStateObject();
const canGoBackward = typeof state?.length === "number" ? state.length >= 1 : true;
const canGoForward = typeof state?.length === "number" && typeof state?.size === "number" ? state.length < state.size : true;
if (backBtn) {
backBtn.hidden = !showNavControls;
backBtn.disabled = !forceHistory && !canGoBackward;
}
if (forwardBtn) {
forwardBtn.hidden = !showNavControls;
forwardBtn.disabled = !forceHistory && !canGoForward;
}
}
navigateHistory(direction) {
this.applyHistoryOverrides();
const engine = this.resolveEngineObject();
if (!engine) {
debugWarn("SugarCube engine not found for history navigation.");
return;
}
const directionOffset = direction === "backward" ? -1 : 1;
if (direction === "backward" && typeof engine.backward === "function") {
const result = engine.backward();
if (result !== false) return;
}
if (direction === "forward" && typeof engine.forward === "function") {
const result = engine.forward();
if (result !== false) return;
}
if (typeof engine.go === "function") {
const result = engine.go(directionOffset);
if (result !== false) return;
}
const state = this.resolveStateObject();
if (state && typeof state.go === "function") {
const result = state.go(directionOffset);
if (result !== false) return;
}
debugWarn(`History ${direction} navigation failed (no available moment or story blocked it).`);
}
cleanup() {
this.shadowRoot?.getElementById("sc-debugger-toggle-btn")?.classList.remove("active");
}
destroy() {
this.cleanup();
this.modal?.destroy();
this.shadowRoot?.getElementById("sc-debugger-controls")?.remove();
this.isInitialized = false;
}
};
function boot() {
const scDebugger = new SugarCubeDebugger();
setDebugLoggingEnabled(Boolean(config_default.getSetting("debugLogging")));
const pageWindow = typeof unsafeWindow !== "undefined" && unsafeWindow ? unsafeWindow : globalThis;
const getSugarCubeRef = () => globalThis?.SugarCube || pageWindow?.SugarCube || null;
const hasStateVariables = () => Boolean(getSugarCubeRef()?.State?.variables);
let observer = null;
const bootTrace = [];
const pushBootTrace = (stage, data = {}) => {
const entry = {
stage,
at: (/* @__PURE__ */ new Date()).toISOString(),
...data
};
bootTrace.push(entry);
try {
globalThis.__SCDBG_BOOT_TRACE = bootTrace;
} catch {
}
try {
console.log("[SCDBG][boot]", stage, data);
} catch {
}
debugInfo("[SCDBG][boot]", stage, data);
};
const pushBootWarn = (stage, data = {}) => {
const entry = {
stage: `WARN:${stage}`,
at: (/* @__PURE__ */ new Date()).toISOString(),
...data
};
bootTrace.push(entry);
try {
globalThis.__SCDBG_BOOT_TRACE = bootTrace;
} catch {
}
try {
console.warn("[SCDBG][boot]", stage, data);
} catch {
}
debugWarn("[SCDBG][boot]", stage, data);
};
const bridgeSugarCubeGlobals = () => {
const sugar = getSugarCubeRef();
if (!sugar) return false;
try {
if (!globalThis.SugarCube) {
globalThis.SugarCube = sugar;
}
} catch {
}
try {
if (typeof globalThis.State === "undefined" && sugar.State) {
globalThis.State = sugar.State;
}
} catch {
}
try {
if (typeof globalThis.Engine === "undefined" && sugar.Engine) {
globalThis.Engine = sugar.Engine;
}
} catch {
}
try {
if (typeof globalThis.Config === "undefined" && sugar.Config) {
globalThis.Config = sugar.Config;
}
} catch {
}
return true;
};
pushBootTrace("boot-start", {
readyState: document.readyState,
hasJquery: Boolean(window.jQuery),
hasSugarCube: Boolean(getSugarCubeRef()),
hasState: Boolean(getSugarCubeRef()?.State),
hasVariables: hasStateVariables(),
debugLoggingSetting: Boolean(config_default.getSetting("debugLogging")),
usingUnsafeWindow: pageWindow !== globalThis
});
const cleanupObserver = () => {
if (!observer) return;
observer.disconnect();
observer = null;
};
const waitForCondition = (check, { intervalMs = 50, timeoutMs = 45e3 } = {}) => new Promise((resolve, reject) => {
if (check()) {
resolve();
return;
}
const startedAt = Date.now();
const timer = window.setInterval(() => {
if (check()) {
window.clearInterval(timer);
resolve();
return;
}
if (Date.now() - startedAt > timeoutMs) {
window.clearInterval(timer);
reject(new Error("SugarCube.State.variables wait timed out."));
}
}, intervalMs);
});
const initDebugger = () => {
if (scDebugger.isInitialized) {
pushBootTrace("init-skipped-already-initialized");
return true;
}
if (!hasStateVariables()) {
return false;
}
try {
bridgeSugarCubeGlobals();
pushBootTrace("init-attempt");
scDebugger.init();
scDebugger.applyHistoryOverrides();
pushBootTrace("init-success");
return true;
} catch (error) {
pushBootWarn("init-failed", { message: error?.message || String(error) });
debugWarn("SugarCubeDebugger initialization failed:", error);
return false;
}
};
const ensureStarted = async () => {
pushBootTrace("ensure-started-begin");
if (initDebugger()) return;
try {
pushBootTrace("wait-start", { target: "SugarCube.State.variables" });
await waitForCondition(hasStateVariables, { intervalMs: 60, timeoutMs: 45e3 });
pushBootTrace("wait-complete", { target: "SugarCube.State.variables" });
initDebugger();
} catch (error) {
pushBootWarn("wait-timeout", {
message: error?.message || "SugarCube debugger prerequisites timed out.",
hasSugarCube: Boolean(getSugarCubeRef()),
hasState: Boolean(getSugarCubeRef()?.State),
hasVariables: hasStateVariables()
});
debugWarn(error?.message || "SugarCube debugger prerequisites timed out.");
} finally {
cleanupObserver();
}
};
if (!hasStateVariables()) {
pushBootTrace("observer-attach");
observer = new MutationObserver(() => {
if (!hasStateVariables()) return;
pushBootTrace("observer-detected-variables");
initDebugger();
cleanupObserver();
});
observer.observe(document.documentElement, { childList: true, subtree: true });
window.setTimeout(cleanupObserver, 45e3);
}
if (typeof window.jQuery !== "undefined") {
window.jQuery(document).one(":storyready", () => {
pushBootTrace("storyready-fired");
initDebugger();
cleanupObserver();
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", ensureStarted, { once: true });
} else {
ensureStarted();
}
window.sugarcubeDebugger = scDebugger;
}
boot();
})();