From df33002d3068c28f31d50ecd0c8afc384df9d49c Mon Sep 17 00:00:00 2001 From: dexta Date: Tue, 28 Jul 2026 18:31:05 +0200 Subject: [PATCH] working for, now writing --- 00_aMeta/toolz/CYOA/Dockerfile.echobox | 4 +- 00_aMeta/toolz/CYOA/frontend/dom_rendering.js | 173 -------- 00_aMeta/toolz/CYOA/frontend/engine_logic.js | 69 ++++ 00_aMeta/toolz/CYOA/frontend/engine_ui.js | 234 ++++++++--- 00_aMeta/toolz/CYOA/frontend/index.html | 43 ++ 00_aMeta/toolz/CYOA/frontend/template.html | 373 ------------------ 00_aMeta/toolz/CYOA/main.go | 151 +++---- .../cyoa_juniorMiner/node_sensoren.adoc | 2 +- .../cyoa_juniorMiner/node_steuerungUnten.adoc | 2 +- 9 files changed, 337 insertions(+), 714 deletions(-) delete mode 100644 00_aMeta/toolz/CYOA/frontend/dom_rendering.js create mode 100644 00_aMeta/toolz/CYOA/frontend/engine_logic.js create mode 100644 00_aMeta/toolz/CYOA/frontend/index.html delete mode 100644 00_aMeta/toolz/CYOA/frontend/template.html 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 + + + +
+
[COMPUTER HISTORYZ :: INSTANZ_TERMINAL]
+
+
+ +
+
+
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 - - - - -
-
[COMPUTER HISTORYZ :: INSTANZ_TERMINAL]
-
-
- -
-
-
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?