2026-07-27 20:09:31 +02:00
<!DOCTYPE html>
< html lang = "de" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Computer Historyz :: Interactive Terminal Engine</ title >
< style >
: root {
--bg-color : #050b08 ;
--term-green : #33ff66 ;
--term-dim : #116633 ;
--term-cyan : #00ffff ;
--term-warn : #ff3333 ;
--term-amber : #ffb000 ;
--font-stack : 'Courier New' , Courier , monospace ;
}
* { box-sizing : border-box ; margin : 0 ; padding : 0 ; }
body {
background-color : var ( -- bg - color ); color : var ( -- term - green );
font-family : var ( -- font - stack ); height : 100 vh ; display : flex ;
flex-direction : column ; overflow : hidden ; text-shadow : 0 0 4 px rgba ( 51 , 255 , 102 , 0.6 );
}
body :: before {
content : " " ; display : block ; position : absolute ;
top : 0 ; left : 0 ; bottom : 0 ; right : 0 ;
background : linear-gradient ( rgba ( 18 , 16 , 16 , 0 ) 50 % , rgba ( 0 , 0 , 0 , 0.25 ) 50 % ), linear-gradient ( 90 deg , rgba ( 255 , 0 , 0 , 0.06 ), rgba ( 0 , 255 , 0 , 0.02 ), rgba ( 0 , 0 , 255 , 0.06 ));
z-index : 10 ; background-size : 100 % 3 px , 3 px 100 % ; pointer-events : none ;
}
header { background : #111 ; border-bottom : 2 px solid var ( -- term - green ); padding : 8 px 16 px ; display : flex ; justify-content : space-between ; align-items : center ; font-size : 0.9 rem ; }
. sys-title { color : var ( -- term - cyan ); font-weight : bold ; letter-spacing : 1 px ; }
. hud-stats { display : flex ; gap : 15 px ; font-size : 0.85 rem ; flex-wrap : wrap ; }
. stat-item { border : 1 px solid var ( -- term - dim ); padding : 2 px 8 px ; background : rgba ( 0 , 20 , 10 , 0.5 ); }
main { flex : 1 ; display : flex ; flex-direction : column ; padding : 20 px ; max-width : 900 px ; margin : 0 auto ; width : 100 % ; overflow-y : auto ; position : relative ; z-index : 1 ; }
. node-card { border : 1 px solid var ( -- term - dim ); background : rgba ( 5 , 15 , 10 , 0.85 ); padding : 20 px ; margin-bottom : 20 px ; box-shadow : 0 0 15 px rgba ( 0 , 255 , 102 , 0.1 ); }
. node-title { font-size : 1.3 rem ; color : var ( -- term - cyan ); border-bottom : 1 px dashed var ( -- term - dim ); padding-bottom : 8 px ; margin-bottom : 15 px ; text-transform : uppercase ; }
/* * ASCIIDOCTOR HTML STYLING
* Da das Go-Backend nun volles HTML liefert, muss das CSS dieses abfangen.
*/
. node-body { line-height : 1.6 ; font-size : 1 rem ; margin-bottom : 20 px ; }
. node-body p { margin-bottom : 15 px ; }
. node-body strong , . node-body b { color : var ( -- term - cyan ); font-weight : normal ; text-shadow : 0 0 5 px rgba ( 0 , 255 , 255 , 0.4 ); }
. node-body em , . node-body i { color : var ( -- term - amber ); font-style : italic ; }
. node-body ul , . node-body ol { margin-left : 25 px ; margin-bottom : 15 px ; list-style-type : square ; }
. node-body li { margin-bottom : 5 px ; }
. node-body h1 , . node-body h2 , . node-body h3 , . node-body h4 { color : var ( -- term - cyan ); margin : 20 px 0 10 px 0 ; border-bottom : 1 px solid var ( -- term - dim ); padding-bottom : 5 px ; }
. node-body code { background : rgba ( 0 , 255 , 102 , 0.1 ); padding : 2 px 5 px ; border-radius : 3 px ; font-family : var ( -- font - stack ); }
. node-body pre { background : rgba ( 0 , 20 , 10 , 0.8 ); border-left : 3 px solid var ( -- term - dim ); padding : 10 px ; overflow-x : auto ; margin-bottom : 15 px ; }
. node-body table { width : 100 % ; border-collapse : collapse ; margin-bottom : 15 px ; }
. node-body th , . node-body td { border : 1 px solid var ( -- term - dim ); padding : 8 px ; text-align : left ; }
. node-body th { background : rgba ( 0 , 255 , 102 , 0.1 ); color : var ( -- term - cyan ); }
. choices-container { display : flex ; flex-direction : column ; gap : 10 px ; margin-top : 15 px ; }
. choice-btn {
background : rgba ( 0 , 30 , 15 , 0.6 ); border : 1 px solid var ( -- term - green );
color : var ( -- term - green ); padding : 12 px 16 px ; font-family : var ( -- font - stack );
font-size : 0.95 rem ; text-align : left ; cursor : pointer ;
transition : all 0.15 s ease ; display : flex ; align-items : center ; gap : 10 px ;
}
. choice-btn : hover : not ( : disabled ) { background : var ( -- term - green ); color : var ( -- bg - color ); box-shadow : 0 0 10 px var ( -- term - green ); outline : none ; }
. choice-btn : disabled { border-color : var ( -- term - warn ); color : var ( -- term - warn ); cursor : not-allowed ; opacity : 0.6 ; }
. choice-key { background : var ( -- term - dim ); color : var ( -- term - green ); padding : 2 px 6 px ; font-size : 0.8 rem ; border-radius : 2 px ; }
. choice-btn : hover : not ( : disabled ) . choice-key { background : var ( -- bg - color ); color : var ( -- term - green ); }
. choice-btn : disabled . choice-key { background : rgba ( 255 , 51 , 51 , 0.2 ); color : var ( -- term - warn ); }
footer { border-top : 1 px solid var ( -- term - dim ); padding : 10 px 20 px ; display : flex ; justify-content : space-between ; background : #080f0c ; font-size : 0.8 rem ; z-index : 2 ; }
. btn-small { background : transparent ; border : 1 px solid var ( -- term - dim ); color : var ( -- term - green ); padding : 4 px 10 px ; cursor : pointer ; font-family : var ( -- font - stack ); }
. btn-small : hover { border-color : var ( -- term - cyan ); color : var ( -- term - cyan ); }
# graph-modal { display : none ; position : fixed ; top : 0 ; left : 0 ; width : 100 vw ; height : 100 vh ; background : rgba ( 5 , 11 , 8 , 0.95 ); z-index : 20 ; padding : 20 px ; flex-direction : column ; }
# graph-canvas { flex : 1 ; border : 1 px solid var ( -- term - green ); background : #000 ; }
</ style >
</ head >
< body >
< header >
< div class = "sys-title" > [COMPUTER HISTORYZ :: INSTANZ_TERMINAL]</ div >
< div class = "hud-stats" id = "hud-dynamic" >
<!-- Wird per JS gefüllt -->
</ div >
</ header >
< main id = "terminal-viewport" >
< div class = "node-card" >
< div id = "node-title" class = "node-title" > System Initialisierung...</ div >
< div id = "node-body" class = "node-body" > Verbinde mit Datenstrom...</ div >
< div id = "choices-box" class = "choices-container" ></ div >
</ div >
</ main >
< footer >
< div > [HOTKEYS: 1-9 Auswahl | R Reboot | G Graph]</ div >
< div >
< button class = "btn-small" onclick = "toggleGraphView()" > [G] KNOTEN-GRAPH</ button >
< button class = "btn-small" onclick = "restartStory()" > [R] REBOOT / RESET</ button >
</ div >
</ footer >
< div id = "graph-modal" >
< div style = "display:flex; justify-content:space-between; margin-bottom:15px;" >
< span style = "color:var(--term-cyan)" > === INTERAKTIVER VORHERSAGE-GRAPH ===</ span >
< button class = "btn-small" onclick = "toggleGraphView()" > [X] SCHLIESSEN</ button >
</ div >
< canvas id = "graph-canvas" ></ canvas >
</ div >
<!-- JSON Injection -->
< script id = "story-data" type = "application/json" >
{{ . | json }}
</ script >
< script >
let storyData = { nodes : {} };
try {
const rawData = document . getElementById ( 'story-data' ). textContent . trim ();
2026-07-27 22:21:18 +02:00
if ( ! rawData . startsWith ( "\{\{" )) {
2026-07-27 20:09:31 +02:00
storyData = JSON . parse ( rawData );
} else {
console . warn ( "[WARNUNG] Go-Engine nicht verbunden. Lade Offline-Preview." );
storyData = {
startNodeId : "preview_start" ,
nodes : {
"preview_start" : {
id : "preview_start" ,
title : "OFFLINE PREVIEW" ,
content : "<p>Verbindung zur Go-Engine unterbrochen.</p><p><strong>Dies ist der Testmodus.</strong> Die Node-Inhalte unterstützen nun <em>echtes HTML</em>, das durch Asciidoctor generiert wird.</p><ul><li>Listen funktionieren</li><li>Tabellen ebenso</li></ul>" ,
choices : [{ targetId : "preview_start" , text : "Endlosschleife starten" }],
metadata : { schild : "100%" , status : "offline" }
}
}
};
}
} catch ( e ) {
console . error ( "Fehler beim Parsen der Injektion:" , e );
}
let currentNodeID = storyData . startNodeId || ( Object . keys ( storyData . nodes ). length > 0 ? Object . keys ( storyData . nodes )[ 0 ] : '' );
let history = [];
let gameState = {
reboots : 1 ,
node : currentNodeID
};
const audioCtx = new ( window . AudioContext || window . webkitAudioContext )();
function playBeep ( freq = 440 , type = 'square' , duration = 0.05 ) {
try {
const osc = audioCtx . createOscillator ();
const gain = audioCtx . createGain ();
osc . type = type ;
osc . frequency . setValueAtTime ( freq , audioCtx . currentTime );
gain . gain . setValueAtTime ( 0.05 , audioCtx . currentTime );
gain . gain . exponentialRampToValueAtTime ( 0.001 , audioCtx . currentTime + duration );
osc . connect ( gain ); gain . connect ( audioCtx . destination );
osc . start (); osc . stop ( audioCtx . currentTime + duration );
} catch ( e ) {}
}
function renderHUD () {
const hud = document . getElementById ( 'hud-dynamic' );
hud . innerHTML = '' ;
hud . innerHTML += `<div class="stat-item">REBOOTS: <span style="color:var(--term-cyan)"># ${ String ( gameState . reboots ). padStart ( 3 , '0' ) } </span></div>` ;
hud . innerHTML += `<div class="stat-item">KNOTEN: <span style="color:var(--term-amber)"> ${ gameState . node } </span></div>` ;
Object . keys ( gameState ). forEach ( key => {
if ( key !== 'reboots' && key !== 'node' ) {
hud . innerHTML += `<div class="stat-item"> ${ key . toUpperCase () } : <span style="color:var(--term-green)"> ${ gameState [ key ] } </span></div>` ;
}
});
}
function renderNode ( id ) {
const node = storyData . nodes [ id ];
if ( ! node ) return ;
currentNodeID = id ;
gameState . node = id ;
history . push ( id );
playBeep ( 600 , 'sine' , 0.04 );
if ( node . metadata ) {
Object . keys ( node . metadata ). forEach ( key => {
gameState [ key ] = node . metadata [ key ];
});
}
if ( node . isReboot ) {
gameState . reboots ++ ;
playBeep ( 200 , 'sawtooth' , 0.3 );
}
renderHUD ();
document . getElementById ( 'node-title' ). innerText = "=== " + node . title + " ===" ;
// WICHTIG: Das Go-Backend liefert nun echtes HTML von Asciidoctor!
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 ) => {
let conditionMatch = choice . text . match ( /\[req:\s*([a-zA-Z0-9_]+)\s*=\s*([^\]]+)\]/i );
let displayText = choice . text ;
let meetsCondition = true ;
let missingReq = "" ;
if ( conditionMatch ) {
let reqKey = conditionMatch [ 1 ]. toLowerCase ();
let reqVal = conditionMatch [ 2 ];
if ( String ( gameState [ reqKey ]) !== String ( reqVal )) {
meetsCondition = false ;
missingReq = `(Benötigt: ${ reqKey } = ${ reqVal } )` ;
}
displayText = displayText . replace ( conditionMatch [ 0 ], '' ). trim ();
}
const btn = document . createElement ( 'button' );
btn . className = 'choice-btn' ;
if ( meetsCondition ) {
btn . innerHTML = `<span class="choice-key">[ ${ visibleIndex } ]</span> ${ displayText } ` ;
btn . dataset . target = choice . targetId ;
btn . dataset . index = visibleIndex ;
btn . onclick = () => renderNode ( choice . targetId );
visibleIndex ++ ;
} else {
btn . innerHTML = `<span class="choice-key">[LOCKED]</span> ${ displayText } <span style="color:var(--term-warn); font-size:0.8rem; margin-left:auto;"> ${ 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 = () => restartStory ();
choicesBox . appendChild ( btn );
}
document . getElementById ( 'terminal-viewport' ). scrollTop = 0 ;
}
function restartStory () {
const currentReboots = gameState . reboots + 1 ;
gameState = { reboots : currentReboots };
if ( storyData . startNodeId ) renderNode ( storyData . startNodeId );
}
window . addEventListener ( 'keydown' , ( e ) => {
if ( document . getElementById ( 'graph-modal' ). style . display === 'flex' ) {
if ( e . key . toLowerCase () === 'g' || e . key === 'Escape' ) toggleGraphView ();
return ;
}
if ( e . key >= '1' && e . key <= '9' ) {
const btn = document . querySelector ( `.choice-btn[data-index=" ${ e . key } "]` );
if ( btn && ! btn . disabled ) renderNode ( btn . dataset . target );
} else if ( e . key . toLowerCase () === 'r' ) restartStory ();
else if ( e . key . toLowerCase () === 'g' ) toggleGraphView ();
});
function toggleGraphView () {
const modal = document . getElementById ( 'graph-modal' );
if ( modal . style . display === 'flex' ) modal . style . display = 'none' ;
else { modal . style . display = 'flex' ; drawGraphCanvas (); }
}
function drawGraphCanvas () {
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 ( 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 = storyData . nodes [ key ];
if ( 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 === currentNodeID ) ctx . fillStyle = '#00ffff' ;
else if ( key === 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 );
});
}
window . onload = () => { if ( currentNodeID ) renderNode ( currentNodeID ); };
</ script >
</ body >
2026-07-27 22:21:18 +02:00
</ html >