CodeEscape Spielführung und Schleusenlogik korrigieren
mindflux-ci / build (push) Successful in 22s

This commit is contained in:
AliceDarkest
2026-07-12 22:39:40 +02:00
parent edb5b36408
commit 0b0360be9d
8 changed files with 26 additions and 7 deletions
@@ -0,0 +1,9 @@
UPDATE escape_rooms SET door_code = '4122SYN', door_prompt = 'Setze alle Fragmente von links nach rechts ohne Leerzeichen zusammen.' WHERE title = 'Rack 01';
UPDATE escape_rooms SET door_code = '6253UP', door_prompt = 'Setze alle Fragmente von links nach rechts ohne Leerzeichen zusammen.' WHERE title = 'Patchfeld';
UPDATE escape_rooms SET door_code = 'DHCP67', door_prompt = 'Setze alle Fragmente von links nach rechts ohne Leerzeichen zusammen.' WHERE title = 'Kontrollraum';
UPDATE escape_rooms SET door_code = '3TBUSVR5', door_prompt = 'Setze alle Fragmente von links nach rechts ohne Leerzeichen zusammen.' WHERE title = 'Stromschleuse';
UPDATE escape_rooms SET door_code = 'VOLL321', door_prompt = 'Setze alle Fragmente von links nach rechts ohne Leerzeichen zusammen.' WHERE title = 'Backup-Tresor';
UPDATE escape_rooms SET door_code = 'TYPE1NOSNAPISO', door_prompt = 'Setze alle Fragmente von links nach rechts ohne Leerzeichen zusammen.' WHERE title = 'Hypervisor-Kern';
UPDATE escape_rooms SET door_code = 'SHA256443CIA', door_prompt = 'Setze alle Fragmente von links nach rechts ohne Leerzeichen zusammen.' WHERE title = 'Forensik-Labor';
UPDATE escape_rooms SET door_code = 'PREPAREDACID', door_prompt = 'Setze alle Fragmente von links nach rechts ohne Leerzeichen zusammen.' WHERE title = 'Datenbank-Bunker';
UPDATE escape_rooms SET door_code = 'ISOLATECALEAST', door_prompt = 'Setze alle Fragmente von links nach rechts ohne Leerzeichen zusammen.' WHERE title = 'Incident-Leitstand';
@@ -0,0 +1 @@
.esc-how{display:grid;grid-template-columns:repeat(3,1fr);gap:.65rem;margin:0 0 1rem}.esc-how>div{display:flex;align-items:center;gap:.65rem;padding:.7rem;background:var(--surface);border:1px solid var(--border);border-radius:9px;color:var(--muted)}.esc-how b{display:grid;place-items:center;min-width:1.7rem;height:1.7rem;border:1px solid var(--border);border-radius:50%;color:var(--muted)}.esc-how strong,.esc-how small{display:block}.esc-how strong{color:var(--heading);font-size:.78rem}.esc-how small{margin-top:.15rem;font-size:.65rem}.esc-how .done,.esc-how .active{border-color:var(--esc-accent)}.esc-how .done b{background:var(--success);border-color:var(--success);color:#06120a}.esc-how .active{box-shadow:0 0 18px var(--esc-glow)}.esc-puzzle:not(.done) .status{color:var(--esc-accent)}.esc-puzzle:not(.done):hover{border-color:var(--esc-accent);transform:translateY(-2px)}.esc-fragment{display:flex;align-items:center;gap:.45rem}.esc-fragment small{display:grid;place-items:center;width:1.25rem;height:1.25rem;border-radius:50%;background:var(--esc-accent);color:#160d00;font-size:.6rem}.esc-inventory.complete{border-style:solid;border-color:var(--success)}.esc-ready{margin:.7rem 0 0;color:var(--success);font-size:.75rem}.esc-door{flex-wrap:wrap}.esc-door.ready{border-color:var(--esc-accent);box-shadow:0 0 22px color-mix(in srgb,var(--esc-accent) 14%,transparent)}.esc-door-title{flex-basis:100%;display:flex;justify-content:space-between;align-items:center}.esc-door-title strong{color:var(--heading);font-size:.8rem}.esc-door-feedback{flex-basis:100%;min-height:1.2rem;font-size:.75rem}.esc-door-feedback.ok{color:var(--success)}.esc-door-feedback.bad{color:var(--danger)}.esc-door-feedback.wait{color:var(--esc-accent)}.esc-input.bad{border-color:var(--danger);box-shadow:0 0 0 2px color-mix(in srgb,var(--danger) 28%,transparent)}.esc-btn:disabled{opacity:.45;cursor:not-allowed;box-shadow:none}@media(max-width:800px){.esc-how{grid-template-columns:1fr}.esc-door-title{align-items:flex-start}}
+3 -2
View File
@@ -5,13 +5,14 @@ function startTimer(){clearInterval(tickId);start=Date.now();tickId=setInterval(
function loadCatalog(){api('/scenarios').then(d=>{catalog=d.scenarios;renderLobby();}).catch(error);}
function renderLobby(){timerEl.textContent='--:--';view.innerHTML='<div class="esc-hero"><span class="esc-eyebrow">FACILITY ARCHIVE // 03 SZENARIEN</span><h1>Wähle deinen Fluchtweg</h1><p>Jeder Raum enthält IT-Rätsel. Sichere die Code-Fragmente, öffne die Schleusen und entkomme, bevor der Zeitbonus verfällt.</p></div><div class="esc-grid">'+catalog.map(s=>`<article class="esc-card"><span class="esc-eyebrow">${esc(s.examScope)} · ${esc(s.difficulty)}</span><h2>${esc(s.title)}</h2><p>${esc(s.description)}</p><div class="esc-meta"><span class="esc-chip">${s.roomCount} Räume</span><span class="esc-chip">${Math.round(s.timeLimitSeconds/60)} Minuten</span></div><button class="esc-btn" data-start="${esc(s.slug)}">FACILITY BETRETEN</button></article>`).join('')+'</div>';view.querySelectorAll('[data-start]').forEach(b=>b.onclick=()=>begin(b.dataset.start));}
function begin(slug){api('/session/'+encodeURIComponent(slug)).then(d=>{session=d;roomIndex=0;answers.clear();fragments.clear();startTimer();renderRoom();}).catch(error);}
function renderRoom(){const r=session.rooms[roomIndex];view.innerHTML=`<div class="esc-room-head"><div><span class="esc-eyebrow">RAUM ${roomIndex+1} / ${session.rooms.length} · ${esc(session.examScope)}</span><h1>${esc(r.title)}</h1><p>${esc(r.intro)}</p></div><div class="esc-progress">${'● '.repeat(roomIndex+1)}${'○ '.repeat(session.rooms.length-roomIndex-1)}</div></div><div class="esc-puzzles">${r.puzzles.map((p,i)=>`<button class="esc-puzzle ${answers.has(p.puzzleId)?'done':''}" data-puzzle="${p.puzzleId}"><span class="esc-eyebrow">NODE ${String(i+1).padStart(2,'0')} · ${esc(p.type)}</span><h3>${esc(p.prompt)}</h3><div class="status">${answers.has(p.puzzleId)?'✓ FRAGMENT GESICHERT':'ZUGRIFF AUSSTEHEND'}</div></button>`).join('')}</div><div class="esc-inventory"><span class="esc-eyebrow">FRAGMENT-INVENTAR</span><div class="esc-fragments">${r.puzzles.map(p=>`<span class="esc-fragment">${fragments.has(p.puzzleId)?esc(fragments.get(p.puzzleId)):'•••'}</span>`).join('')}</div></div><div class="esc-door"><label>${esc(r.doorPrompt)}<input class="esc-input" id="doorCode" autocomplete="off" ${r.puzzles.every(p=>answers.has(p.puzzleId))?'':'disabled'}></label><button class="esc-btn primary" id="doorBtn" ${r.puzzles.every(p=>answers.has(p.puzzleId))?'':'disabled'}>SCHLEUSE ÖFFNEN</button><button class="esc-btn danger" id="quitBtn">AUFGEBEN</button></div>`;view.querySelectorAll('[data-puzzle]').forEach(b=>b.onclick=()=>openPuzzle(r.puzzles.find(p=>p.puzzleId==b.dataset.puzzle)));document.getElementById('doorBtn').onclick=()=>openDoor(r);document.getElementById('quitBtn').onclick=()=>finish(false);}
function renderRoom(){const r=session.rooms[roomIndex],complete=r.puzzles.every(p=>answers.has(p.puzzleId));view.innerHTML=`<div class="esc-room-head"><div><span class="esc-eyebrow">RAUM ${roomIndex+1} / ${session.rooms.length} · ${esc(session.examScope)}</span><h1>${esc(r.title)}</h1><p>${esc(r.intro)}</p></div><div class="esc-progress">${'● '.repeat(roomIndex+1)}${'○ '.repeat(session.rooms.length-roomIndex-1)}</div></div><div class="esc-how"><div class="${complete?'done':''}"><b>1</b><span><strong>Rätsel lösen</strong><small>Die Rätsel sind frei wählbar.</small></span></div><div class="${complete?'done':''}"><b>2</b><span><strong>Fragmente sammeln</strong><small>Ihre Reihenfolge ist links → rechts.</small></span></div><div class="${complete?'active':''}"><b>3</b><span><strong>Türcode bilden</strong><small>Ohne Leerzeichen zusammensetzen.</small></span></div></div><div class="esc-puzzles">${r.puzzles.map((p,i)=>`<button class="esc-puzzle ${answers.has(p.puzzleId)?'done':''}" data-puzzle="${p.puzzleId}"><span class="esc-eyebrow">RÄTSEL ${i+1} · ${esc(p.type)}</span><h3>${esc(p.prompt)}</h3><div class="status">${answers.has(p.puzzleId)?'✓ FRAGMENT GESICHERT':'ANKLICKEN UND LÖSEN →'}</div></button>`).join('')}</div><div class="esc-inventory ${complete?'complete':''}"><span class="esc-eyebrow">2 · FRAGMENTE VON LINKS NACH RECHTS</span><div class="esc-fragments">${r.puzzles.map((p,i)=>`<span class="esc-fragment"><small>${i+1}</small>${fragments.has(p.puzzleId)?esc(fragments.get(p.puzzleId)):'•••'}</span>`).join('')}</div>${complete?'<p class="esc-ready">✓ Alle Fragmente gesichert. Bilde jetzt unten den Türcode.</p>':''}</div><div class="esc-door ${complete?'ready':''}"><div class="esc-door-title"><span class="esc-eyebrow">3 · TÜRTERMINAL</span><strong>${complete?'Schleuse bereit':'Noch nicht verfügbar'}</strong></div><label>${complete?esc(r.doorPrompt):'Löse zuerst alle Rätsel, um das Terminal freizuschalten.'}<input class="esc-input" id="doorCode" autocomplete="off" spellcheck="false" ${complete?'':'disabled'}></label>${complete?'<button class="esc-btn" id="assembleBtn">FRAGMENTE EINSETZEN</button>':''}<button class="esc-btn primary" id="doorBtn" ${complete?'':'disabled'}>CODE PRÜFEN &amp; ÖFFNEN</button><button class="esc-btn danger" id="quitBtn">AUFGEBEN</button><div class="esc-door-feedback" id="doorFeedback" role="status" aria-live="polite"></div></div>`;view.querySelectorAll('[data-puzzle]').forEach(b=>b.onclick=()=>openPuzzle(r.puzzles.find(p=>p.puzzleId==b.dataset.puzzle)));if(complete){document.getElementById('assembleBtn').onclick=()=>{document.getElementById('doorCode').value=r.puzzles.map(p=>fragments.get(p.puzzleId)).join('');document.getElementById('doorCode').focus();doorMessage('Fragmente eingesetzt. Prüfe den Code und öffne die Schleuse.','ok');};document.getElementById('doorCode').onkeydown=e=>{if(e.key==='Enter')openDoor(r);};}document.getElementById('doorBtn').onclick=()=>openDoor(r);document.getElementById('quitBtn').onclick=()=>finish(false);}
function control(p){if(p.type==='code')return `<input class="esc-input" id="answer" placeholder="${esc(p.placeholder||'Antwort')}">`;if(p.type==='choice')return `<div class="esc-options">${p.options.map(x=>`<button class="esc-option" data-value="${esc(x)}">${esc(x)}</button>`).join('')}</div>`;if(p.type==='order')return `<div class="esc-options">${p.items.map((x,i)=>`<label>${i+1}.<select class="esc-select order">${p.items.map(y=>`<option>${esc(y)}</option>`).join('')}</select></label>`).join('')}</div>`;if(p.type==='match')return `<div class="esc-pairs">${p.left.map(x=>`<label>${esc(x)}<select class="esc-select pair" data-left="${esc(x)}">${p.right.map(y=>`<option>${esc(y)}</option>`).join('')}</select></label>`).join('')}</div>`;return'';}
function openPuzzle(p){selected=null;document.body.insertAdjacentHTML('beforeend',`<div class="esc-modal"><div class="esc-dialog"><span class="esc-eyebrow">SECURE TERMINAL · ${esc(p.type)}</span><h2>${esc(p.prompt)}</h2><p>${esc(p.narrative||p.instruction||'Löse das Zugriffsrätsel.')}</p>${p.narrative&&p.instruction?`<p>${esc(p.instruction)}</p>`:''}${control(p)}<div class="esc-feedback" id="feedback"></div><div class="esc-actions"><button class="esc-btn primary" id="checkBtn">PRÜFEN</button><button class="esc-btn" id="hintBtn">HINWEIS</button><button class="esc-btn danger" id="revealBtn">AUFLÖSEN</button><button class="esc-btn" id="closeBtn">SCHLIESSEN</button></div></div></div>`);const modal=document.querySelector('.esc-modal');modal.querySelectorAll('.esc-option').forEach(b=>b.onclick=()=>{modal.querySelectorAll('.esc-option').forEach(x=>x.classList.remove('selected'));b.classList.add('selected');selected=b.dataset.value;});document.getElementById('closeBtn').onclick=()=>modal.remove();let hintLevel=0;document.getElementById('hintBtn').onclick=()=>post('/hint',{puzzleId:p.puzzleId,level:Math.min(2,++hintLevel)}).then(x=>feedback(x.text,'ok'));document.getElementById('checkBtn').onclick=()=>checkPuzzle(p,false,modal);document.getElementById('revealBtn').onclick=()=>checkPuzzle(p,true,modal);}
function submission(p,modal){if(p.type==='code')return{answer:modal.querySelector('#answer').value};if(p.type==='choice')return{answer:selected||''};if(p.type==='order')return{items:[...modal.querySelectorAll('.order')].map(x=>x.value)};if(p.type==='match')return{pairs:[...modal.querySelectorAll('.pair')].map(x=>[x.dataset.left,x.value])};return{};}
function checkPuzzle(p,reveal,modal){const sub=submission(p,modal);post('/check',{puzzleId:p.puzzleId,reveal,submission:sub}).then(x=>{if(x.resolved){answers.set(p.puzzleId,{puzzleId:p.puzzleId,submission:sub});fragments.set(p.puzzleId,x.fragment);feedback(reveal?'Auflösung freigegeben. Fragment gesichert.':'Zugriff bestätigt. Fragment gesichert.','ok');setTimeout(()=>{modal.remove();renderRoom();},700);}else feedback('Zugriff verweigert. Prüfe deine Eingabe.','bad');}).catch(e=>feedback(e.message,'bad'));}
function feedback(t,c){const f=document.getElementById('feedback');if(f){f.textContent=t;f.className='esc-feedback '+c;}}
function openDoor(r){post('/door',{roomId:r.roomId,code:document.getElementById('doorCode').value}).then(x=>{if(!x.open){document.getElementById('doorCode').classList.add('bad');return;}if(roomIndex<session.rooms.length-1){roomIndex++;renderRoom();}else finish(true);}).catch(error);}
function doorMessage(text,type){const box=document.getElementById('doorFeedback');if(box){box.textContent=text;box.className='esc-door-feedback '+type;}}
function openDoor(r){const input=document.getElementById('doorCode'),button=document.getElementById('doorBtn'),code=input.value.trim();input.classList.remove('bad');if(!code){input.classList.add('bad');doorMessage('Setze zuerst die Fragmente zu einem Code zusammen.','bad');input.focus();return;}button.disabled=true;button.textContent='CODE WIRD GEPRÜFT …';doorMessage('Türterminal prüft den Code …','wait');post('/door',{roomId:r.roomId,code}).then(x=>{if(!x.open){input.classList.add('bad');doorMessage(x.message||'Code abgelehnt. Prüfe die Reihenfolge der Fragmente.','bad');button.disabled=false;button.textContent='CODE PRÜFEN & ÖFFNEN';input.focus();return;}doorMessage('✓ '+(x.successText||'Schleuse geöffnet.'),'ok');setTimeout(()=>{if(roomIndex<session.rooms.length-1){roomIndex++;renderRoom();}else finish(true);},1100);}).catch(e=>{doorMessage(e.message||'Das Türterminal ist nicht erreichbar.','bad');button.disabled=false;button.textContent='ERNEUT VERSUCHEN';});}
function finish(escaped){clearInterval(tickId);post('/finish',{slug:session.slug,durationSeconds:seconds(),escaped,items:[...answers.values()],doors:[]}).then(renderResult).catch(error);}
function renderResult(r){timerEl.textContent='ESCAPED';view.innerHTML=`<section class="esc-result"><span class="esc-eyebrow">FACILITY RUN ABGESCHLOSSEN</span><h1>${r.escaped?'AUSGANG ERREICHT':'RUN BEENDET'}</h1><div class="score">${r.score}</div><p>${'★'.repeat(r.stars)}${'☆'.repeat(3-r.stars)}</p><div class="esc-stats"><div><b>${r.baseScore}</b>Basis</div><div><b>+${r.timeBonus}</b>Zeitbonus</div><div><b>${r.solvedPuzzles}</b>Rätsel</div><div><b>${r.roomsOpened}</b>Räume</div><div><b>${r.highscore}</b>Highscore</div><div><b>${r.plays}</b>Runs</div></div><button class="esc-btn primary" id="again">NEUER RUN</button></section>`;document.getElementById('again').onclick=loadCatalog;}
function error(e){view.innerHTML=`<div class="esc-result"><h1>SYSTEMFEHLER</h1><p>${esc(e.message||e)}</p><button class="esc-btn" onclick="location.reload()">NEU LADEN</button></div>`;}loadCatalog();})();
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="de" data-theme="dark" xmlns:th="http://www.thymeleaf.org"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CodeEscape · Mindflux</title><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/codeescape.css?v=1"><script src="/js/theme.js"></script></head><body><main class="esc-app"><header class="esc-top"><a href="/dashboard" class="esc-back">← Dashboard</a><div class="esc-brand"><b>CODEESCAPE</b><span>FACILITY ACCESS PROTOCOL</span></div><div class="esc-timer" id="escTimer">--:--</div></header><section id="escView" class="esc-view"><div class="esc-loading">FACILITY WIRD INITIALISIERT …</div></section></main><script src="/js/codeescape.js?v=1"></script></body></html>
<!doctype html><html lang="de" data-theme="dark" xmlns:th="http://www.thymeleaf.org"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CodeEscape · Mindflux</title><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/codeescape.css?v=2"><link rel="stylesheet" href="/css/codeescape-guide.css?v=2"><script src="/js/theme.js"></script></head><body><main class="esc-app"><header class="esc-top"><a href="/dashboard" class="esc-back">← Dashboard</a><div class="esc-brand"><b>CODEESCAPE</b><span>FACILITY ACCESS PROTOCOL</span></div><div class="esc-timer" id="escTimer">--:--</div></header><section id="escView" class="esc-view"><div class="esc-loading">FACILITY WIRD INITIALISIERT …</div></section></main><script src="/js/codeescape.js?v=2"></script></body></html>