// engine_ui.js - Statisches Rendering & asynchrones Nachladen der graph.json const EngineUI = { audioCtx: null, 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 { 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); } 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();