Files
computerhistory/00_aMeta/toolz/CYOA/frontend/engine_ui.js
T

185 lines
8.3 KiB
JavaScript
Raw Normal View History

2026-07-28 18:31:05 +02:00
// engine_ui.js - Statisches Rendering & asynchrones Nachladen der graph.json
const EngineUI = {
audioCtx: null,
2026-07-28 16:19:17 +02:00
2026-07-28 18:31:05 +02:00
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)
2026-07-28 16:19:17 +02:00
try {
2026-07-28 18:31:05 +02:00
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);
2026-07-28 16:19:17 +02:00
}
2026-07-28 18:31:05 +02:00
this.attachEvents();
this.renderNode(EngineLogic.state.node);
2026-07-28 16:19:17 +02:00
},
2026-07-28 18:31:05 +02:00
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) {}
2026-07-28 16:19:17 +02:00
},
2026-07-28 18:31:05 +02:00
renderHUD: function() {
const hud = document.getElementById("hud-dynamic");
hud.innerHTML = "";
hud.innerHTML += `<div class="stat-item">REBOOTS: <span style="color:var(--term-cyan)">#${String(EngineLogic.state.reboots).padStart(3, "0")}</span></div>`;
hud.innerHTML += `<div class="stat-item">KNOTEN: <span style="color:var(--term-amber)">${EngineLogic.state.node}</span></div>`;
Object.keys(EngineLogic.state).forEach(key => {
if(key !== "reboots" && key !== "node") {
hud.innerHTML += `<div class="stat-item">${key.toUpperCase()}: <span style="color:var(--term-green)">${EngineLogic.state[key]}</span></div>`;
}
});
},
2026-07-28 16:19:17 +02:00
2026-07-28 18:31:05 +02:00
renderNode: function(id) {
const node = EngineLogic.processNodeEntry(id);
if (!node) return;
2026-07-28 16:19:17 +02:00
2026-07-28 18:31:05 +02:00
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 = `<span class="choice-key">[${visibleIndex}]</span> ${parsed.displayText}`;
btn.dataset.target = choice.targetId;
btn.dataset.index = visibleIndex;
btn.onclick = () => this.renderNode(choice.targetId);
visibleIndex++;
} else {
btn.innerHTML = `<span class="choice-key">[LOCKED]</span> ${parsed.displayText} <span style="color:var(--term-warn); font-size:0.8rem; margin-left:auto;">${parsed.missingReq}</span>`;
btn.disabled = true;
}
choicesBox.appendChild(btn);
2026-07-28 16:19:17 +02:00
});
2026-07-28 18:31:05 +02:00
} else {
const btn = document.createElement("button");
btn.className = "choice-btn";
btn.style.borderColor = "var(--term-amber)";
btn.style.color = "var(--term-amber)";
btn.innerHTML = "<span class=\"choice-key\">[R]</span> Speicherabbild sichern und rebooten...";
btn.onclick = () => this.triggerReboot();
choicesBox.appendChild(btn);
2026-07-28 16:19:17 +02:00
}
2026-07-28 18:31:05 +02:00
document.getElementById("terminal-viewport").scrollTop = 0;
},
2026-07-28 16:19:17 +02:00
2026-07-28 18:31:05 +02:00
triggerReboot: function() {
const startId = EngineLogic.triggerReboot();
if (startId) this.renderNode(startId);
},
2026-07-28 16:19:17 +02:00
2026-07-28 18:31:05 +02:00
toggleGraph: function() {
const modal = document.getElementById("graph-modal");
if (modal.style.display === "flex") {
modal.style.display = "none";
} else {
modal.style.display = "flex";
this.drawGraphCanvas();
}
2026-07-28 16:19:17 +02:00
},
2026-07-28 18:31:05 +02:00
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();
}
});
2026-07-28 16:19:17 +02:00
}
2026-07-28 18:31:05 +02:00
});
2026-07-28 16:19:17 +02:00
2026-07-28 18:31:05 +02:00
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);
});
2026-07-28 16:19:17 +02:00
},
2026-07-28 18:31:05 +02:00
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();
}
});
2026-07-28 16:19:17 +02:00
}
2026-07-28 18:31:05 +02:00
};
window.onload = () => EngineUI.init();