diff --git a/00_aMeta/toolz/CYOA/Dockerfile.echobox b/00_aMeta/toolz/CYOA/Dockerfile.echobox
index bcc31e2..adf0c5b 100644
--- a/00_aMeta/toolz/CYOA/Dockerfile.echobox
+++ b/00_aMeta/toolz/CYOA/Dockerfile.echobox
@@ -9,9 +9,9 @@ RUN groupadd --gid 1000 flammenderberg && \
useradd --uid 1000 --gid flammenderberg --shell /bin/bash --create-home hexenmeister
WORKDIR /weltraum
-RUN mkdir stories && mkdir export
+RUN mkdir stories && mkdir export && mkdir frontend
COPY ./cyoa-engine ./
-ADD ./templates/template.html ./
+COPY ./frontend ./frontend/
RUN chown -R hexenmeister:flammenderberg /weltraum
diff --git a/00_aMeta/toolz/CYOA/frontend/dom_rendering.js b/00_aMeta/toolz/CYOA/frontend/dom_rendering.js
deleted file mode 100644
index 684a537..0000000
--- a/00_aMeta/toolz/CYOA/frontend/dom_rendering.js
+++ /dev/null
@@ -1,173 +0,0 @@
-// engine_ui.js - Reines DOM-Handling und visuelle Logik
-const EngineUI = {
- audioCtx: null,
-
- init: function() {
- this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
-
- const rawData = document.getElementById("story-data").textContent.trim();
- EngineLogic.init(rawData);
-
- this.attachEvents();
- this.renderNode(EngineLogic.state.node);
- },
-
- playBeep: function(freq = 440, type = "square", duration = 0.05) {
- try {
- if(!this.audioCtx) return;
- const osc = this.audioCtx.createOscillator();
- const gain = this.audioCtx.createGain();
- osc.type = type;
- osc.frequency.setValueAtTime(freq, this.audioCtx.currentTime);
- gain.gain.setValueAtTime(0.05, this.audioCtx.currentTime);
- gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + duration);
- osc.connect(gain); gain.connect(this.audioCtx.destination);
- osc.start(); osc.stop(this.audioCtx.currentTime + duration);
- } catch(e) {}
- },
-
- renderHUD: function() {
- const hud = document.getElementById("hud-dynamic");
- hud.innerHTML = "";
-
- hud.innerHTML += `
REBOOTS: #${String(EngineLogic.state.reboots).padStart(3, "0")}
`;
- hud.innerHTML += `KNOTEN: ${EngineLogic.state.node}
`;
-
- Object.keys(EngineLogic.state).forEach(key => {
- if(key !== "reboots" && key !== "node") {
- hud.innerHTML += `${key.toUpperCase()}: ${EngineLogic.state[key]}
`;
- }
- });
- },
-
- renderNode: function(id) {
- const node = EngineLogic.processNodeEntry(id);
- if (!node) return;
-
- this.playBeep(600, "sine", 0.04);
- if (node.isReboot) this.playBeep(200, "sawtooth", 0.3);
-
- this.renderHUD();
-
- document.getElementById("node-title").innerText = "=== " + node.title + " ===";
- document.getElementById("node-body").innerHTML = node.content;
-
- const choicesBox = document.getElementById("choices-box");
- choicesBox.innerHTML = "";
-
- if (node.choices && node.choices.length > 0) {
- let visibleIndex = 1;
- node.choices.forEach((choice) => {
- const parsed = EngineLogic.parseRequirement(choice.text);
- const btn = document.createElement("button");
- btn.className = "choice-btn";
-
- if (parsed.meetsCondition) {
- btn.innerHTML = `[${visibleIndex}] ${parsed.displayText}`;
- btn.dataset.target = choice.targetId;
- btn.dataset.index = visibleIndex;
- btn.onclick = () => this.renderNode(choice.targetId);
- visibleIndex++;
- } else {
- btn.innerHTML = `[LOCKED] ${parsed.displayText} ${parsed.missingReq}`;
- btn.disabled = true;
- }
- choicesBox.appendChild(btn);
- });
- } else {
- const btn = document.createElement("button");
- btn.className = "choice-btn";
- btn.style.borderColor = "var(--term-amber)";
- btn.style.color = "var(--term-amber)";
- btn.innerHTML = "[R] Speicherabbild sichern und rebooten...";
- btn.onclick = () => this.triggerReboot();
- choicesBox.appendChild(btn);
- }
- document.getElementById("terminal-viewport").scrollTop = 0;
- },
-
- triggerReboot: function() {
- const startId = EngineLogic.triggerReboot();
- if (startId) this.renderNode(startId);
- },
-
- toggleGraph: function() {
- const modal = document.getElementById("graph-modal");
- if (modal.style.display === "flex") {
- modal.style.display = "none";
- } else {
- modal.style.display = "flex";
- this.drawGraphCanvas();
- }
- },
-
- drawGraphCanvas: function() {
- const canvas = document.getElementById("graph-canvas");
- canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight;
- const ctx = canvas.getContext("2d");
- ctx.fillStyle = "#000"; ctx.fillRect(0, 0, canvas.width, canvas.height);
-
- const keys = Object.keys(EngineLogic.storyData.nodes || {});
- if (keys.length === 0) return;
-
- const total = keys.length; const radius = Math.min(canvas.width, canvas.height) * 0.38;
- const centerX = canvas.width / 2; const centerY = canvas.height / 2;
- const nodeRadius = 8; const positions = {};
-
- keys.forEach((key, idx) => {
- const angle = (idx / total) * 2 * Math.PI;
- positions[key] = { x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle) };
- });
-
- ctx.lineWidth = 1;
- keys.forEach(key => {
- const node = EngineLogic.storyData.nodes[key];
- if (node && node.choices) {
- node.choices.forEach(c => {
- if (positions[c.targetId]) {
- const start = positions[key], end = positions[c.targetId];
- ctx.beginPath(); ctx.strokeStyle = "rgba(51, 255, 102, 0.4)";
- ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); ctx.stroke();
- const dx = end.x - start.x, dy = end.y - start.y, angle = Math.atan2(dy, dx), headlen = 12;
- const tipX = end.x - Math.cos(angle) * (nodeRadius + 2), tipY = end.y - Math.sin(angle) * (nodeRadius + 2);
- ctx.beginPath(); ctx.moveTo(tipX, tipY);
- ctx.lineTo(tipX - headlen * Math.cos(angle - Math.PI / 7), tipY - headlen * Math.sin(angle - Math.PI / 7));
- ctx.lineTo(tipX - headlen * Math.cos(angle + Math.PI / 7), tipY - headlen * Math.sin(angle + Math.PI / 7));
- ctx.closePath(); ctx.fillStyle = "rgba(51, 255, 102, 0.9)"; ctx.fill();
- }
- });
- }
- });
-
- keys.forEach(key => {
- const pos = positions[key];
- ctx.beginPath(); ctx.arc(pos.x, pos.y, nodeRadius, 0, 2 * Math.PI);
- if (key === EngineLogic.state.node) ctx.fillStyle = "#00ffff";
- else if (key === EngineLogic.storyData.startNodeId) ctx.fillStyle = "#ffb000";
- else ctx.fillStyle = "#116633";
- ctx.fill(); ctx.strokeStyle = "#33ff66"; ctx.stroke();
- ctx.fillStyle = "#fff"; ctx.font = "11px monospace";
- const textAngle = Math.atan2(pos.y - centerY, pos.x - centerX);
- ctx.fillText(key, pos.x + Math.cos(textAngle) * 15 - 10, pos.y + Math.sin(textAngle) * 15 + 4);
- });
- },
-
- attachEvents: function() {
- window.addEventListener("keydown", (e) => {
- if (document.getElementById("graph-modal").style.display === "flex") {
- if (e.key.toLowerCase() === "g" || e.key === "Escape") this.toggleGraph();
- return;
- }
- if (e.key >= "1" && e.key <= "9") {
- const btn = document.querySelector(`.choice-btn[data-index="${e.key}"]`);
- if (btn && !btn.disabled) this.renderNode(btn.dataset.target);
- } else if (e.key.toLowerCase() === "r") {
- this.triggerReboot();
- } else if (e.key.toLowerCase() === "g") {
- this.toggleGraph();
- }
- });
- }
-};
-
-window.onload = () => EngineUI.init();
\ No newline at end of file
diff --git a/00_aMeta/toolz/CYOA/frontend/engine_logic.js b/00_aMeta/toolz/CYOA/frontend/engine_logic.js
new file mode 100644
index 0000000..3c7427f
--- /dev/null
+++ b/00_aMeta/toolz/CYOA/frontend/engine_logic.js
@@ -0,0 +1,69 @@
+// engine_logic.js - Reine State- und Datenverarbeitung (Kein UI, Kein Fetch)
+const EngineLogic = {
+ storyData: { nodes: {} },
+ state: { reboots: 1, node: "" },
+
+ init: function(dataObj) {
+ if (!dataObj || !dataObj.nodes || Object.keys(dataObj.nodes).length === 0) {
+ console.warn("[System] Ungültige Graph-Daten. Fallback geladen.");
+ this.storyData = {
+ startNodeId: "err",
+ nodes: {
+ "err": {
+ id: "err", title: "SYSTEM OFFLINE",
+ content: "Konnte graph.json nicht laden.
",
+ choices: [],
+ metadata: { status: "offline" }
+ }
+ }
+ };
+ } else {
+ this.storyData = dataObj;
+ }
+
+ this.state.node = this.storyData.startNodeId || Object.keys(this.storyData.nodes)[0] || "";
+ },
+
+ getNode: function(id) { return this.storyData.nodes[id]; },
+
+ processNodeEntry: function(id) {
+ const node = this.getNode(id);
+ if (!node) return null;
+
+ this.state.node = id;
+
+ if (node.metadata) {
+ Object.keys(node.metadata).forEach(key => {
+ this.state[key] = node.metadata[key];
+ });
+ }
+
+ if (node.isReboot) this.state.reboots++;
+ return node;
+ },
+
+ parseRequirement: function(choiceText) {
+ let conditionMatch = choiceText.match(/\[req:\s*([a-zA-Z0-9_]+)\s*=\s*([^\]]+)\]/i);
+ let displayText = choiceText;
+ let meetsCondition = true;
+ let missingReq = "";
+
+ if (conditionMatch) {
+ let reqKey = conditionMatch[1].toLowerCase();
+ let reqVal = conditionMatch[2];
+ if (String(this.state[reqKey]) !== String(reqVal)) {
+ meetsCondition = false;
+ missingReq = `(Benötigt: ${reqKey}=${reqVal})`;
+ }
+ displayText = displayText.replace(conditionMatch[0], "").trim();
+ }
+
+ return { displayText, meetsCondition, missingReq };
+ },
+
+ triggerReboot: function() {
+ const currentReboots = this.state.reboots + 1;
+ this.state = { reboots: currentReboots };
+ return this.storyData.startNodeId;
+ }
+};
\ No newline at end of file
diff --git a/00_aMeta/toolz/CYOA/frontend/engine_ui.js b/00_aMeta/toolz/CYOA/frontend/engine_ui.js
index 0ac9751..90245b2 100644
--- a/00_aMeta/toolz/CYOA/frontend/engine_ui.js
+++ b/00_aMeta/toolz/CYOA/frontend/engine_ui.js
@@ -1,75 +1,185 @@
-// engine_logic.js - Reine State- und Datenverarbeitung
-const EngineLogic = {
- storyData: { nodes: {} },
- state: { reboots: 1, node: "" },
+// engine_ui.js - Statisches Rendering & asynchrones Nachladen der graph.json
+const EngineUI = {
+ audioCtx: null,
- init: function(rawJsonData) {
+ init: async function() {
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
+ if(AudioContext) this.audioCtx = new AudioContext();
+
+ // 1. Asynchroner Fetch der graph.json
+ // (Im Dev-Modus liefert Go dies on-the-fly, im Export-Modus ist es eine Datei)
try {
- if (!rawJsonData || rawJsonData.trim() === "") throw "Leer";
- this.storyData = JSON.parse(rawJsonData);
- } catch(e) {
- console.warn("[System] Go-Engine Offline. Fallback Modus geladen.");
- this.storyData = {
- startNodeId: "err",
- nodes: {
- "err": {
- id: "err", title: "SYSTEM OFFLINE",
- content: "Konnte [story-data] nicht parsen. Go-Server läuft nicht.
",
- choices: []
- }
- }
- };
- }
- this.state.node = this.storyData.startNodeId || Object.keys(this.storyData.nodes)[0] || "";
- },
-
- getNode: function(id) {
- return this.storyData.nodes[id];
- },
-
- processNodeEntry: function(id) {
- const node = this.getNode(id);
- if (!node) return null;
-
- this.state.node = id;
-
- // Metadaten anwenden (z.B. [meta: schild=80])
- if (node.metadata) {
- Object.keys(node.metadata).forEach(key => {
- this.state[key] = node.metadata[key];
- });
+ const response = await fetch('graph.json', { cache: "no-store" });
+ if (!response.ok) throw new Error("HTTP Fehler " + response.status);
+ const graphData = await response.json();
+
+ // 2. Initialisiere die gekapselte Logik mit den frisch geladenen Daten
+ EngineLogic.init(graphData);
+ } catch (err) {
+ console.error("Fehler beim Abrufen der graph.json:", err);
+ EngineLogic.init(null);
}
- // Reboot Erkennung
- if (node.isReboot) {
- this.state.reboots++;
- }
-
- return node;
+ this.attachEvents();
+ this.renderNode(EngineLogic.state.node);
},
- parseRequirement: function(choiceText) {
- let conditionMatch = choiceText.match(/\[req:\s*([a-zA-Z0-9_]+)\s*=\s*([^\]]+)\]/i);
- let displayText = choiceText;
- let meetsCondition = true;
- let missingReq = "";
+ playBeep: function(freq = 440, type = "square", duration = 0.05) {
+ try {
+ if(!this.audioCtx) return;
+ const osc = this.audioCtx.createOscillator();
+ const gain = this.audioCtx.createGain();
+ osc.type = type;
+ osc.frequency.setValueAtTime(freq, this.audioCtx.currentTime);
+ gain.gain.setValueAtTime(0.05, this.audioCtx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + duration);
+ osc.connect(gain); gain.connect(this.audioCtx.destination);
+ osc.start(); osc.stop(this.audioCtx.currentTime + duration);
+ } catch(e) {}
+ },
- if (conditionMatch) {
- let reqKey = conditionMatch[1].toLowerCase();
- let reqVal = conditionMatch[2];
- if (String(this.state[reqKey]) !== String(reqVal)) {
- meetsCondition = false;
- missingReq = `(Benötigt: ${reqKey}=${reqVal})`;
+ renderHUD: function() {
+ const hud = document.getElementById("hud-dynamic");
+ hud.innerHTML = "";
+
+ hud.innerHTML += `REBOOTS: #${String(EngineLogic.state.reboots).padStart(3, "0")}
`;
+ hud.innerHTML += `KNOTEN: ${EngineLogic.state.node}
`;
+
+ Object.keys(EngineLogic.state).forEach(key => {
+ if(key !== "reboots" && key !== "node") {
+ hud.innerHTML += `${key.toUpperCase()}: ${EngineLogic.state[key]}
`;
}
- displayText = displayText.replace(conditionMatch[0], "").trim();
- }
+ });
+ },
- return { displayText, meetsCondition, missingReq };
+ renderNode: function(id) {
+ const node = EngineLogic.processNodeEntry(id);
+ if (!node) return;
+
+ this.playBeep(600, "sine", 0.04);
+ if (node.isReboot) this.playBeep(200, "sawtooth", 0.3);
+
+ this.renderHUD();
+
+ document.getElementById("node-title").innerText = "=== " + node.title + " ===";
+ document.getElementById("node-body").innerHTML = node.content;
+
+ const choicesBox = document.getElementById("choices-box");
+ choicesBox.innerHTML = "";
+
+ if (node.choices && node.choices.length > 0) {
+ let visibleIndex = 1;
+ node.choices.forEach((choice) => {
+ const parsed = EngineLogic.parseRequirement(choice.text);
+ const btn = document.createElement("button");
+ btn.className = "choice-btn";
+
+ if (parsed.meetsCondition) {
+ btn.innerHTML = `[${visibleIndex}] ${parsed.displayText}`;
+ btn.dataset.target = choice.targetId;
+ btn.dataset.index = visibleIndex;
+ btn.onclick = () => this.renderNode(choice.targetId);
+ visibleIndex++;
+ } else {
+ btn.innerHTML = `[LOCKED] ${parsed.displayText} ${parsed.missingReq}`;
+ btn.disabled = true;
+ }
+ choicesBox.appendChild(btn);
+ });
+ } else {
+ const btn = document.createElement("button");
+ btn.className = "choice-btn";
+ btn.style.borderColor = "var(--term-amber)";
+ btn.style.color = "var(--term-amber)";
+ btn.innerHTML = "[R] Speicherabbild sichern und rebooten...";
+ btn.onclick = () => this.triggerReboot();
+ choicesBox.appendChild(btn);
+ }
+ document.getElementById("terminal-viewport").scrollTop = 0;
},
triggerReboot: function() {
- const currentReboots = this.state.reboots + 1;
- this.state = { reboots: currentReboots }; // Resets inventory/state
- return this.storyData.startNodeId;
+ const startId = EngineLogic.triggerReboot();
+ if (startId) this.renderNode(startId);
+ },
+
+ toggleGraph: function() {
+ const modal = document.getElementById("graph-modal");
+ if (modal.style.display === "flex") {
+ modal.style.display = "none";
+ } else {
+ modal.style.display = "flex";
+ this.drawGraphCanvas();
+ }
+ },
+
+ drawGraphCanvas: function() {
+ const canvas = document.getElementById("graph-canvas");
+ canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight;
+ const ctx = canvas.getContext("2d");
+ ctx.fillStyle = "#000"; ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ const keys = Object.keys(EngineLogic.storyData.nodes || {});
+ if (keys.length === 0) return;
+
+ const total = keys.length; const radius = Math.min(canvas.width, canvas.height) * 0.38;
+ const centerX = canvas.width / 2; const centerY = canvas.height / 2;
+ const nodeRadius = 8; const positions = {};
+
+ keys.forEach((key, idx) => {
+ const angle = (idx / total) * 2 * Math.PI;
+ positions[key] = { x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle) };
+ });
+
+ ctx.lineWidth = 1;
+ keys.forEach(key => {
+ const node = EngineLogic.storyData.nodes[key];
+ if (node && node.choices) {
+ node.choices.forEach(c => {
+ if (positions[c.targetId]) {
+ const start = positions[key], end = positions[c.targetId];
+ ctx.beginPath(); ctx.strokeStyle = "rgba(51, 255, 102, 0.4)";
+ ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); ctx.stroke();
+ const dx = end.x - start.x, dy = end.y - start.y, angle = Math.atan2(dy, dx), headlen = 12;
+ const tipX = end.x - Math.cos(angle) * (nodeRadius + 2), tipY = end.y - Math.sin(angle) * (nodeRadius + 2);
+ ctx.beginPath(); ctx.moveTo(tipX, tipY);
+ ctx.lineTo(tipX - headlen * Math.cos(angle - Math.PI / 7), tipY - headlen * Math.sin(angle - Math.PI / 7));
+ ctx.lineTo(tipX - headlen * Math.cos(angle + Math.PI / 7), tipY - headlen * Math.sin(angle + Math.PI / 7));
+ ctx.closePath(); ctx.fillStyle = "rgba(51, 255, 102, 0.9)"; ctx.fill();
+ }
+ });
+ }
+ });
+
+ keys.forEach(key => {
+ const pos = positions[key];
+ ctx.beginPath(); ctx.arc(pos.x, pos.y, nodeRadius, 0, 2 * Math.PI);
+ if (key === EngineLogic.state.node) ctx.fillStyle = "#00ffff";
+ else if (key === EngineLogic.storyData.startNodeId) ctx.fillStyle = "#ffb000";
+ else ctx.fillStyle = "#116633";
+ ctx.fill(); ctx.strokeStyle = "#33ff66"; ctx.stroke();
+ ctx.fillStyle = "#fff"; ctx.font = "11px monospace";
+ const textAngle = Math.atan2(pos.y - centerY, pos.x - centerX);
+ ctx.fillText(key, pos.x + Math.cos(textAngle) * 15 - 10, pos.y + Math.sin(textAngle) * 15 + 4);
+ });
+ },
+
+ attachEvents: function() {
+ window.addEventListener("keydown", (e) => {
+ if (document.getElementById("graph-modal").style.display === "flex") {
+ if (e.key.toLowerCase() === "g" || e.key === "Escape") this.toggleGraph();
+ return;
+ }
+ if (e.key >= "1" && e.key <= "9") {
+ const btn = document.querySelector(`.choice-btn[data-index="${e.key}"]`);
+ if (btn && !btn.disabled) this.renderNode(btn.dataset.target);
+ } else if (e.key.toLowerCase() === "r") {
+ this.triggerReboot();
+ } else if (e.key.toLowerCase() === "g") {
+ this.toggleGraph();
+ }
+ });
}
-};
\ No newline at end of file
+};
+
+window.onload = () => EngineUI.init();
\ No newline at end of file
diff --git a/00_aMeta/toolz/CYOA/frontend/index.html b/00_aMeta/toolz/CYOA/frontend/index.html
new file mode 100644
index 0000000..03d70d7
--- /dev/null
+++ b/00_aMeta/toolz/CYOA/frontend/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+ Computer Historyz :: Interactive Terminal Engine
+
+
+
+
+
+
+
+
System Initialisierung...
+
Verbinde mit Datenstrom...
+
+
+
+
+
+
+
+
+ === INTERAKTIVER VORHERSAGE-GRAPH ===
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/00_aMeta/toolz/CYOA/frontend/template.html b/00_aMeta/toolz/CYOA/frontend/template.html
deleted file mode 100644
index 66ebe6e..0000000
--- a/00_aMeta/toolz/CYOA/frontend/template.html
+++ /dev/null
@@ -1,373 +0,0 @@
-
-
-
-
-
- Computer Historyz :: Interactive Terminal Engine
-
-
-
-
-
-
-
-
-
System Initialisierung...
-
Verbinde mit Datenstrom...
-
-
-
-
-
-
-
-
- === INTERAKTIVER VORHERSAGE-GRAPH ===
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/00_aMeta/toolz/CYOA/main.go b/00_aMeta/toolz/CYOA/main.go
index 54727ca..f1af3ad 100644
--- a/00_aMeta/toolz/CYOA/main.go
+++ b/00_aMeta/toolz/CYOA/main.go
@@ -21,7 +21,7 @@ type Choice struct {
type StoryNode struct {
ID string `json:"id"`
Title string `json:"title"`
- Content string `json:"content"` // Reiner String, JSON kümmert sich um das Escaping des HTMLs
+ Content string `json:"content"`
Choices []Choice `json:"choices"`
IsReboot bool `json:"isReboot"`
IsEnd bool `json:"isEnd"`
@@ -48,13 +48,13 @@ var (
func main() {
dirPtr := flag.String("dir", "./stories", "Verzeichnis mit AsciiDoc/Markdown Dateien")
frontendPtr := flag.String("frontend", "./frontend", "Pfad zum Verzeichnis der statischen Frontend-Dateien")
- outPtr := flag.String("out", "export/story.html", "Zielpfad für den Dateiexport (Single-File HTML)")
- servePtr := flag.Bool("serve", false, "Startet den API- & Preview-Server, statt zu exportieren")
+ outPtr := flag.String("out", "export", "Ziel-Ordner für den Dateiexport (SSG Modus)")
+ servePtr := flag.Bool("serve", false, "Startet den Live-Preview-Server (API Modus)")
portPtr := flag.Int("port", 3344, "Port für den Server")
flag.Parse()
- fmt.Println("\033[1;36m=== Computer Historyz :: CYOA Engine ===\033[0m")
-
+ fmt.Println("\033[1;36m=== Computer Historyz :: CYOA SSG & API ===\033[0m")
+
if info, err := os.Stat(*frontendPtr); os.IsNotExist(err) || !info.IsDir() {
fmt.Printf("\033[1;33m[WARNUNG]\033[0m Frontend-Verzeichnis '%s' nicht gefunden!\n", *frontendPtr)
}
@@ -64,104 +64,79 @@ func main() {
return
}
- // --- EXPORT MODUS ---
- fmt.Printf("\033[1;34m[EXPORT]\033[0m Kompiliere Story-Graph via Asciidoctor aus '%s'...\n", *dirPtr)
+ // --- EXPORT MODUS (Static Site Generation) ---
+ fmt.Printf("\033[1;34m[EXPORT]\033[0m Kompiliere Story-Graph via Asciidoctor...\n")
graph := loadStoryGraphFromDir(*dirPtr)
validateGraph(graph)
- outDir := filepath.Dir(*outPtr)
- os.MkdirAll(outDir, 0755)
+ // Export Ordner anlegen
+ os.MkdirAll(*outPtr, 0755)
- err := buildSingleFileExport(graph, *frontendPtr, *outPtr)
+ // 1. Statische Daten als JSON schreiben
+ graphPath := filepath.Join(*outPtr, "graph.json")
+ graphJson, _ := json.MarshalIndent(graph, "", " ")
+ os.WriteFile(graphPath, graphJson, 0644)
+
+ // 2. Frontend-Dateien 1:1 kopieren
+ copyFile(filepath.Join(*frontendPtr, "index.html"), filepath.Join(*outPtr, "index.html"))
+ copyFile(filepath.Join(*frontendPtr, "style.css"), filepath.Join(*outPtr, "style.css"))
+ copyFile(filepath.Join(*frontendPtr, "engine_logic.js"), filepath.Join(*outPtr, "engine_logic.js"))
+ copyFile(filepath.Join(*frontendPtr, "engine_ui.js"), filepath.Join(*outPtr, "engine_ui.js"))
+
+ fmt.Printf("\033[1;32m[ERFOLG]\033[0m Web-App erfolgreich exportiert nach -> \033[1;33m%s/\033[0m\n", *outPtr)
+}
+
+func copyFile(src, dst string) {
+ input, err := os.ReadFile(src)
if err != nil {
- fmt.Printf("\033[1;31m[FEHLER]\033[0m Build fehlgeschlagen: %v\n", err)
- os.Exit(1)
+ fmt.Printf("\033[1;33m[WARNUNG]\033[0m Frontend-Datei %s fehlt.\n", src)
+ return
}
- fmt.Printf("\033[1;32m[ERFOLG]\033[0m Single-File Story exportiert -> \033[1;33m%s\033[0m\n", *outPtr)
+ os.WriteFile(dst, input, 0644)
}
func startServer(port int, watchDir string, frontendDir string) {
addr := fmt.Sprintf(":%d", port)
+ fmt.Printf("\n\033[1;32m[SERVER LÄUFT]\033[0m http://localhost:%d\n", port)
- http.HandleFunc("/api/graph", func(w http.ResponseWriter, r *http.Request) {
+ // Dynamischer API-Endpunkt für graph.json
+ http.HandleFunc("/graph.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
+ var graph *StoryGraph
if r.Method == http.MethodPost {
- bodyBytes, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, `{"error": "Daten defekt"}`, http.StatusBadRequest)
- return
- }
-
- graph := &StoryGraph{Nodes: make(map[string]*StoryNode)}
+ bodyBytes, _ := io.ReadAll(r.Body)
+ graph = &StoryGraph{Nodes: make(map[string]*StoryNode)}
parseContent(string(bodyBytes), "API_POST_Stream", graph)
- validateGraph(graph)
-
- json.NewEncoder(w).Encode(graph)
- fmt.Println("\033[1;34m[POST-API]\033[0m Story-Graph verarbeitet.")
- return
- }
-
- if r.Method == http.MethodGet {
- graph := loadStoryGraphFromDir(watchDir)
- validateGraph(graph)
- json.NewEncoder(w).Encode(graph)
+ } else {
+ graph = loadStoryGraphFromDir(watchDir)
}
+ validateGraph(graph)
+ json.NewEncoder(w).Encode(graph)
})
+ // Liefert alles andere aus dem /frontend Ordner
fs := http.FileServer(http.Dir(frontendDir))
http.Handle("/", fs)
- fmt.Printf("\n\033[1;32m[SERVER LÄUFT]\033[0m http://localhost:%d\n", port)
if err := http.ListenAndServe(addr, nil); err != nil {
fmt.Printf("\033[1;31m[FEHLER]\033[0m %v\n", err)
}
}
-func buildSingleFileExport(graph *StoryGraph, frontendDir string, outPath string) error {
- indexPath := filepath.Join(frontendDir, "index.html")
- htmlBytes, err := os.ReadFile(indexPath)
- if err != nil {
- return fmt.Errorf("konnte %s nicht lesen: %v", indexPath, err)
- }
- htmlStr := string(htmlBytes)
-
- cssBytes, _ := os.ReadFile(filepath.Join(frontendDir, "style.css"))
- logicBytes, _ := os.ReadFile(filepath.Join(frontendDir, "engine_logic.js"))
- uiBytes, _ := os.ReadFile(filepath.Join(frontendDir, "engine_ui.js"))
-
- graphJson, _ := json.Marshal(graph)
-
- // CSS ersetzen
- htmlStr = strings.Replace(htmlStr, ``, fmt.Sprintf("", string(cssBytes)), 1)
-
- // JS ersetzen
- htmlStr = strings.Replace(htmlStr, ``, fmt.Sprintf("", string(logicBytes)), 1)
- htmlStr = strings.Replace(htmlStr, ``, fmt.Sprintf("", string(uiBytes)), 1)
-
- // JSON State in den Data-Block pressen
- re := regexp.MustCompile(`(?s)`)
- htmlStr = re.ReplaceAllString(htmlStr, fmt.Sprintf("", string(graphJson)))
-
- return os.WriteFile(outPath, []byte(htmlStr), 0644)
-}
-
// renderAsciidoctor piped rohen AsciiDoc Text in den lokalen Container-Prozess
func renderAsciidoctor(text string) string {
if strings.TrimSpace(text) == "" {
return ""
}
-
cmd := exec.Command("asciidoctor", "--embedded", "-")
cmd.Stdin = strings.NewReader(text)
-
out, err := cmd.Output()
if err != nil {
fmt.Printf("\033[1;33m[WARNUNG]\033[0m Asciidoctor Rendering fehlgeschlagen: %v\n", err)
return strings.ReplaceAll(text, "\n", "
")
}
-
return string(out)
}
@@ -184,38 +159,18 @@ func parseContent(rawContent string, sourceName string, graph *StoryGraph) {
currentBody.Reset()
}
nodeID := matches[1]
- currentNode = &StoryNode{
- ID: nodeID,
- Title: nodeID,
- Choices: make([]Choice, 0),
- FileSource: sourceName,
- Metadata: make(map[string]string),
- }
- if graph.StartNodeID == "" || nodeID == "start" || nodeID == "node_start" {
- graph.StartNodeID = nodeID
- }
+ currentNode = &StoryNode{ ID: nodeID, Title: nodeID, Choices: make([]Choice, 0), FileSource: sourceName, Metadata: make(map[string]string) }
+ if graph.StartNodeID == "" || nodeID == "start" || nodeID == "node_start" { graph.StartNodeID = nodeID }
continue
}
if currentNode == nil {
if strings.TrimSpace(line) != "" {
baseID := "start"
- if sourceName != "API_POST_Stream" {
- baseID = strings.TrimSuffix(filepath.Base(sourceName), filepath.Ext(sourceName))
- }
- currentNode = &StoryNode{
- ID: baseID,
- Title: baseID,
- Choices: make([]Choice, 0),
- FileSource: sourceName,
- Metadata: make(map[string]string),
- }
- if graph.StartNodeID == "" {
- graph.StartNodeID = baseID
- }
- } else {
- continue
- }
+ if sourceName != "API_POST_Stream" { baseID = strings.TrimSuffix(filepath.Base(sourceName), filepath.Ext(sourceName)) }
+ currentNode = &StoryNode{ ID: baseID, Title: baseID, Choices: make([]Choice, 0), FileSource: sourceName, Metadata: make(map[string]string) }
+ if graph.StartNodeID == "" { graph.StartNodeID = baseID }
+ } else { continue }
}
if mMatches := metaRegex.FindStringSubmatch(line); len(mMatches) > 2 {
@@ -228,24 +183,16 @@ func parseContent(rawContent string, sourceName string, graph *StoryGraph) {
}
if cMatches := adocChoiceRegex.FindAllStringSubmatch(line, -1); len(cMatches) > 0 {
- for _, match := range cMatches {
- currentNode.Choices = append(currentNode.Choices, Choice{TargetID: match[1], Text: strings.TrimSpace(match[2])})
- }
+ for _, match := range cMatches { currentNode.Choices = append(currentNode.Choices, Choice{TargetID: match[1], Text: strings.TrimSpace(match[2])}) }
}
if mMatches := mdChoiceRegex.FindAllStringSubmatch(line, -1); len(mMatches) > 0 {
- for _, match := range mMatches {
- currentNode.Choices = append(currentNode.Choices, Choice{TargetID: match[2], Text: strings.TrimSpace(match[1])})
- }
+ for _, match := range mMatches { currentNode.Choices = append(currentNode.Choices, Choice{TargetID: match[2], Text: strings.TrimSpace(match[1])}) }
}
lowerLine := strings.ToLower(line)
- if strings.Contains(lowerLine, "[reboot]") || strings.Contains(lowerLine, "speicherabbild") {
- currentNode.IsReboot = true
- }
- if strings.Contains(lowerLine, "[ende]") || strings.Contains(lowerLine, "[end]") {
- currentNode.IsEnd = true
- }
+ if strings.Contains(lowerLine, "[reboot]") || strings.Contains(lowerLine, "speicherabbild") { currentNode.IsReboot = true }
+ if strings.Contains(lowerLine, "[ende]") || strings.Contains(lowerLine, "[end]") { currentNode.IsEnd = true }
currentBody.WriteString(line + "\n")
}
diff --git a/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_sensoren.adoc b/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_sensoren.adoc
index ca09fef..46ffcc9 100644
--- a/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_sensoren.adoc
+++ b/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_sensoren.adoc
@@ -5,7 +5,7 @@
```
: sensor daten angeschlossen
```
-
+[meta: sensor=online]
## Kontext
diff --git a/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_steuerungUnten.adoc b/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_steuerungUnten.adoc
index 2d98ae2..0ff6c29 100644
--- a/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_steuerungUnten.adoc
+++ b/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_steuerungUnten.adoc
@@ -9,7 +9,7 @@
## Kontext
-Mitte
+Unten
### Was werden wir ausführen?