185 lines
8.3 KiB
JavaScript
185 lines
8.3 KiB
JavaScript
// 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 += `<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>`;
|
|
}
|
|
});
|
|
},
|
|
|
|
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 = `<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);
|
|
});
|
|
} 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);
|
|
}
|
|
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(); |