- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark, Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI) - aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource, an aether-core gekoppelt (Rechte über aether_users.admin_group), mit WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU) - Prototypen admin/ vervollständigt - SQL-Schema aether_admin.sql - Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
405 lines
24 KiB
HTML
405 lines
24 KiB
HTML
<!doctype html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="color-scheme" content="dark">
|
|
<title>Aether — Minispiele</title>
|
|
<style>
|
|
:root{
|
|
--bg:#08070d;--fg:#f0eef7;--muted:#918bad; /* Grundfarben */
|
|
--surface:rgba(22,19,36,0.72);--surface2:rgba(34,29,54,0.9); /* Panel-Flächen */
|
|
--slot:#100e1a;--border:rgba(255,255,255,0.09); /* Eingaben/Rahmen */
|
|
--accent:#7c5cf6;--accent2:#5b8cff;--ink:#160c2e;/* Aether-Akzent */
|
|
--accent-soft:rgba(124,92,246,0.16);
|
|
--green:#34d39a;--red:#f0506e;--amber:#f5a742; /* Zustände */
|
|
}
|
|
*{box-sizing:border-box;} html,body{margin:0;height:100%;}
|
|
body{min-height:100%;background:radial-gradient(1000px 640px at 50% -8%,rgba(124,92,246,0.12),transparent 55%),var(--bg);
|
|
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--fg);
|
|
display:flex;align-items:center;justify-content:center;padding:22px 14px;}
|
|
.app{position:relative;width:min(1000px,98vw);aspect-ratio:16/10;border-radius:20px;overflow:hidden;
|
|
background:linear-gradient(160deg,rgba(20,17,32,0.95),rgba(8,7,12,0.98));border:1px solid rgba(124,92,246,0.22);
|
|
box-shadow:0 50px 100px -30px rgba(0,0,0,0.85);display:grid;grid-template-columns:210px 1fr;grid-template-rows:auto 1fr;
|
|
grid-template-areas:"brand top" "nav main";}
|
|
.brand{grid-area:brand;padding:14px 16px;border-right:1px solid var(--border);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;}
|
|
.brand .bl{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;font-size:19px;background:linear-gradient(150deg,var(--accent),var(--accent2));color:#fff;}
|
|
.brand .bn{font-size:16px;font-weight:800;letter-spacing:-0.02em;} .brand .bn span{color:var(--accent);} .brand .bn small{display:block;font-size:10px;color:var(--muted);font-weight:500;}
|
|
.top{grid-area:top;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;padding:0 20px;font-size:12.5px;color:var(--muted);}
|
|
.top b{color:var(--fg);}
|
|
.nav{grid-area:nav;border-right:1px solid var(--border);padding:12px 9px;display:flex;flex-direction:column;gap:2px;}
|
|
.nav button{display:flex;align-items:center;gap:10px;padding:10px 11px;border-radius:9px;border:none;background:none;color:var(--muted);font-family:inherit;font-size:13.5px;font-weight:700;cursor:pointer;text-align:left;transition:.12s;}
|
|
.nav button .ni{font-size:15px;width:18px;text-align:center;} .nav button:hover{background:var(--surface);color:var(--fg);} .nav button.active{background:var(--accent-soft);color:var(--accent);}
|
|
.main{grid-area:main;overflow-y:auto;padding:22px 26px;display:flex;flex-direction:column;}
|
|
|
|
.phead{margin-bottom:16px;} .phead h2{font-size:22px;margin:0;font-weight:800;letter-spacing:-0.02em;} .phead p{margin:3px 0 0;color:var(--muted);font-size:13px;}
|
|
.stage{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;text-align:center;}
|
|
|
|
.btn{border:none;border-radius:11px;padding:12px 20px;font-size:14px;font-weight:800;cursor:pointer;font-family:inherit;transition:.12s;}
|
|
.btn.primary{background:var(--accent);color:#fff;} .btn.primary:hover{filter:brightness(1.1);}
|
|
.btn.ghost{background:var(--surface2);color:var(--fg);border:1px solid var(--border);}
|
|
.btn.green{background:var(--green);color:#052018;} .btn:active{transform:scale(0.98);}
|
|
.kbd{display:inline-block;background:rgba(255,255,255,0.1);border:1px solid var(--border);border-bottom-width:2px;border-radius:6px;padding:1px 8px;font-weight:800;font-size:12px;}
|
|
|
|
/* Übersicht */
|
|
.lobbygrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px;width:100%;}
|
|
.gamecard{border-radius:16px;padding:18px;cursor:pointer;border:1px solid var(--border);background:var(--surface);transition:.12s;text-align:left;}
|
|
.gamecard:hover{transform:translateY(-3px);border-color:var(--accent);}
|
|
.gamecard .gi{font-size:32px;} .gamecard .gn{font-size:16px;font-weight:800;margin-top:8px;} .gamecard .gd{font-size:12px;color:var(--muted);margin-top:3px;line-height:1.4;}
|
|
|
|
.roundinfo{font-size:13px;color:var(--muted);font-weight:700;} .roundinfo b{color:var(--accent);}
|
|
|
|
/* Skill-Check: rotierender Zeiger */
|
|
.scwrap{position:relative;width:230px;height:230px;}
|
|
.scwrap svg{width:100%;height:100%;}
|
|
.sc-track{stroke:rgba(255,255,255,0.1);}
|
|
.sc-zone{stroke:var(--green);transition:stroke .2s;}
|
|
.sc-pointer{stroke:var(--accent);stroke-width:5;stroke-linecap:round;}
|
|
.sc-hub{position:absolute;inset:0;display:grid;place-items:center;font-size:15px;font-weight:800;pointer-events:none;}
|
|
|
|
/* Lockpick: Timing-Bar + Pins */
|
|
.pins{display:flex;gap:8px;} .pins i{width:26px;height:8px;border-radius:99px;background:var(--slot);border:1px solid var(--border);}
|
|
.pins i.set{background:var(--green);border-color:transparent;} .pins i.fail{background:var(--red);}
|
|
.lives{font-size:13px;color:var(--muted);font-weight:700;} .lives b{color:var(--red);}
|
|
.lpbar{position:relative;width:min(420px,80vw);height:52px;border-radius:12px;background:var(--slot);border:1px solid var(--border);overflow:hidden;}
|
|
.lp-zone{position:absolute;top:0;bottom:0;background:linear-gradient(180deg,rgba(52,211,153,0.4),rgba(52,211,153,0.18));border-left:2px solid var(--green);border-right:2px solid var(--green);}
|
|
.lp-marker{position:absolute;top:-3px;bottom:-3px;width:5px;background:var(--accent);border-radius:3px;box-shadow:0 0 12px var(--accent);}
|
|
|
|
/* Memory: 4 Pads */
|
|
.pads{display:grid;grid-template-columns:1fr 1fr;gap:12px;width:270px;height:270px;}
|
|
.pad{border-radius:16px;border:2px solid rgba(255,255,255,0.08);cursor:pointer;transition:.1s;opacity:0.5;}
|
|
.pad.lit{opacity:1;transform:scale(0.96);box-shadow:0 0 26px currentColor;}
|
|
.pad[data-p="0"]{background:#7c5cf6;color:#7c5cf6;} .pad[data-p="1"]{background:#5b8cff;color:#5b8cff;}
|
|
.pad[data-p="2"]{background:#34d39a;color:#34d39a;} .pad[data-p="3"]{background:#f5a742;color:#f5a742;}
|
|
.pads.locked .pad{cursor:not-allowed;}
|
|
|
|
/* Keypad-Crack */
|
|
.kpwrap{display:flex;gap:26px;align-items:flex-start;flex-wrap:wrap;justify-content:center;}
|
|
.kpdisplay{display:flex;gap:8px;} .kpdisplay .d{width:46px;height:58px;border-radius:10px;background:var(--slot);border:1px solid var(--border);
|
|
display:grid;place-items:center;font-size:26px;font-weight:800;font-variant-numeric:tabular-nums;}
|
|
.kpdisplay .d.fill{border-color:var(--accent);}
|
|
.keypad{display:grid;grid-template-columns:repeat(3,58px);gap:8px;}
|
|
.keypad button{height:52px;border-radius:10px;border:1px solid var(--border);background:var(--surface2);color:var(--fg);font-family:inherit;font-size:19px;font-weight:800;cursor:pointer;transition:.1s;}
|
|
.keypad button:hover{border-color:var(--accent);} .keypad button.wide{grid-column:span 1;font-size:15px;}
|
|
.kphist{min-width:180px;max-height:260px;overflow-y:auto;display:flex;flex-direction:column;gap:6px;}
|
|
.kphist .row{background:var(--surface);border:1px solid var(--border);border-radius:9px;padding:7px 10px;}
|
|
/* Jede Ziffer bekommt ihren Marker direkt darunter (positionsbezogen) */
|
|
.kphist .zeile{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;text-align:center;}
|
|
.kphist .zeile .z{font-size:17px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1.1;}
|
|
.kphist .zeile .m{font-size:12px;margin-top:3px;line-height:1;}
|
|
.kpmeta{display:flex;gap:16px;font-size:13px;color:var(--muted);font-weight:700;} .kpmeta b{color:var(--fg);}
|
|
.kpmeta .t.low b{color:var(--red);}
|
|
|
|
/* Ergebnis-Banner */
|
|
.result{display:flex;flex-direction:column;align-items:center;gap:14px;animation:auf .3s ease;}
|
|
@keyframes auf{from{opacity:0;transform:translateY(12px);}to{opacity:1;transform:none;}}
|
|
.result .ri{font-size:60px;} .result h3{margin:0;font-size:26px;font-weight:900;}
|
|
.result.win h3{color:var(--green);} .result.lose h3{color:var(--red);}
|
|
.result p{margin:0;color:var(--muted);font-size:13.5px;} .result .row{display:flex;gap:10px;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="app">
|
|
<div class="brand"><span class="bl">🕹️</span><div class="bn">Aether <span>Minigames</span><small>Skill-Checks</small></div></div>
|
|
<div class="top" id="top">Sammlung von Minispielen · in FiveM per NUI ausgelöst, geben <b>Erfolg / Fehlschlag</b> zurück.</div>
|
|
<div class="nav" id="nav"></div>
|
|
<div class="main" id="main"></div>
|
|
</div>
|
|
|
|
<script>
|
|
"use strict";
|
|
/* Kurzer Zugriffshelfer auf Elemente per ID */
|
|
const $=id=>document.getElementById(id);
|
|
/* Ganzzahl-Zufall a..b inklusive */
|
|
const rnd=(a,b)=>a+Math.floor(Math.random()*(b-a+1));
|
|
|
|
/* =====================================================================
|
|
TIMER-VERWALTUNG (damit beim Wechsel nichts weiterläuft)
|
|
===================================================================== */
|
|
let _intervals=[], _timeouts=[];
|
|
let letzteAktion=0; // Zeitstempel der letzten Aktion (verhindert Doppelauslösung)
|
|
/* Intervall registrieren */
|
|
function I(fn,ms){const id=setInterval(fn,ms);_intervals.push(id);return id;}
|
|
/* Timeout registrieren */
|
|
function T(fn,ms){const id=setTimeout(fn,ms);_timeouts.push(id);return id;}
|
|
/* Alle laufenden Timer stoppen */
|
|
function stopAlle(){_intervals.forEach(clearInterval);_timeouts.forEach(clearTimeout);_intervals=[];_timeouts=[];}
|
|
|
|
/* =====================================================================
|
|
SHELL / NAVIGATION
|
|
===================================================================== */
|
|
const spiele=[
|
|
{id:"skillcheck", icon:"🎯", label:"Skill-Check", desc:"Stoppe den Zeiger in der grünen Zone — mehrere Runden, wird schneller."},
|
|
{id:"lockpick", icon:"🔓", label:"Lockpick", desc:"Setze jeden Pin, indem du den Marker in der Zone triffst. 3 Versuche."},
|
|
{id:"memory", icon:"🧠", label:"Memory", desc:"Merke dir die Sequenz der Felder und wiederhole sie fehlerfrei."},
|
|
{id:"keypad", icon:"🔢", label:"Code knacken", desc:"Errate den 4-stelligen Code aus den Hinweisen — gegen die Zeit."},
|
|
];
|
|
const nav=[{id:"lobby",icon:"🏠",label:"Übersicht"},...spiele.map(s=>({id:s.id,icon:s.icon,label:s.label}))];
|
|
let sektion="lobby";
|
|
|
|
/* Navigationsleiste rendern */
|
|
function renderNav(){
|
|
$("nav").innerHTML=nav.map(n=>`<button data-s="${n.id}" class="${n.id===sektion?'active':''}"><span class="ni">${n.icon}</span>${n.label}</button>`).join('');
|
|
$("nav").querySelectorAll("[data-s]").forEach(b=>b.addEventListener("click",()=>wechsle(b.dataset.s)));
|
|
}
|
|
/* Zu einer Sektion wechseln (stoppt vorher alle Timer) */
|
|
function wechsle(id){stopAlle();sektion=id;renderNav();(sektionen[id]||sektionen.lobby)();}
|
|
const sektionen={};
|
|
|
|
/* Gemeinsames Ergebnis-Banner (Erfolg/Fehlschlag) mit Aktionen */
|
|
function spielEnde(gewonnen,text){
|
|
stopAlle();
|
|
const spiel=spiele.find(s=>s.id===sektion);
|
|
$("main").innerHTML=`<div class="phead"><h2>${spiel.label}</h2></div>
|
|
<div class="stage"><div class="result ${gewonnen?'win':'lose'}">
|
|
<div class="ri">${gewonnen?'✅':'❌'}</div>
|
|
<h3>${gewonnen?'Erfolg!':'Fehlgeschlagen'}</h3>
|
|
<p>${text}</p>
|
|
<div class="row"><button class="btn primary" id="reNochmal">↻ Nochmal</button>
|
|
<button class="btn ghost" id="reBack">Zur Übersicht</button></div>
|
|
</div></div>`;
|
|
$("reNochmal").addEventListener("click",()=>wechsle(sektion));
|
|
$("reBack").addEventListener("click",()=>wechsle("lobby"));
|
|
}
|
|
|
|
/* =====================================================================
|
|
ÜBERSICHT
|
|
===================================================================== */
|
|
sektionen.lobby=()=>{
|
|
$("main").innerHTML=`<div class="phead"><h2>Minispiele</h2><p>Wähle einen Skill-Check zum Ausprobieren.</p></div>
|
|
<div class="lobbygrid">${spiele.map(s=>
|
|
`<div class="gamecard" data-g="${s.id}"><div class="gi">${s.icon}</div><div class="gn">${s.label}</div><div class="gd">${s.desc}</div></div>`).join('')}</div>`;
|
|
$("main").querySelectorAll("[data-g]").forEach(c=>c.addEventListener("click",()=>wechsle(c.dataset.g)));
|
|
};
|
|
|
|
/* =====================================================================
|
|
1) SKILL-CHECK (rotierender Zeiger in die grüne Zone stoppen)
|
|
===================================================================== */
|
|
const SC_RUNDEN=3; // Anzahl zu bestehender Runden
|
|
const SC_UMFANG=2*Math.PI*92; // Umfang des Rings (Radius 92)
|
|
const sc={runde:1, winkel:0, spd:3, zoneStart:0, zoneWinkel:60, aktiv:false};
|
|
sektionen.skillcheck=()=>{
|
|
Object.assign(sc,{runde:1,winkel:0,spd:3,zoneWinkel:60,aktiv:true});
|
|
$("main").innerHTML=`<div class="phead"><h2>Skill-Check</h2><p>Drücke <span class="kbd">Leertaste</span> oder klicke, wenn der Zeiger in der grünen Zone steht.</p></div>
|
|
<div class="stage">
|
|
<div class="roundinfo">Runde <b id="sc-runde">1</b> / ${SC_RUNDEN}</div>
|
|
<div class="scwrap" id="sc-click">
|
|
<svg viewBox="0 0 230 230">
|
|
<circle class="sc-track" cx="115" cy="115" r="92" fill="none" stroke-width="14"></circle>
|
|
<circle class="sc-zone" id="sc-zone" cx="115" cy="115" r="92" fill="none" stroke-width="14" stroke-linecap="butt"></circle>
|
|
<line class="sc-pointer" id="sc-pointer" x1="115" y1="115" x2="115" y2="18"></line>
|
|
</svg>
|
|
<div class="sc-hub">STOP</div>
|
|
</div>
|
|
<button class="btn primary" id="sc-btn">Stoppen</button>
|
|
</div>`;
|
|
scNeueRunde();
|
|
I(()=>{ // Zeiger drehen
|
|
sc.winkel=(sc.winkel+sc.spd)%360;
|
|
$("sc-pointer").setAttribute("transform",`rotate(${sc.winkel} 115 115)`);
|
|
},16);
|
|
$("sc-btn").addEventListener("click",scAktion);
|
|
$("sc-click").addEventListener("click",scAktion);
|
|
};
|
|
/* Neue Runde vorbereiten: Zone zufällig setzen, Tempo/Größe je Runde */
|
|
function scNeueRunde(){
|
|
sc.zoneWinkel=[60,46,36][sc.runde-1]||34; // Zone schrumpft je Runde
|
|
sc.spd=[3,3.9,4.8][sc.runde-1]||5; // Tempo steigt je Runde
|
|
sc.zoneStart=rnd(0,359);
|
|
const arc=sc.zoneWinkel/360*SC_UMFANG;
|
|
const z=$("sc-zone");
|
|
z.setAttribute("stroke-dasharray",`${arc} ${SC_UMFANG}`);
|
|
z.setAttribute("transform",`rotate(${sc.zoneStart-90} 115 115)`); // -90: Start oben
|
|
$("sc-runde").textContent=sc.runde;
|
|
}
|
|
/* Aktion: prüfen, ob der Zeiger in der Zone steht */
|
|
function scAktion(){
|
|
if(!sc.aktiv)return;
|
|
// Doppelauslösung (Leertaste + fokussierter Button) verhindern
|
|
const jetzt=performance.now(); if(jetzt-letzteAktion<140)return; letzteAktion=jetzt;
|
|
const rel=((sc.winkel-sc.zoneStart)%360+360)%360; // Abstand zum Zonenstart
|
|
if(rel<=sc.zoneWinkel){ // Treffer
|
|
if(sc.runde>=SC_RUNDEN){sc.aktiv=false;spielEnde(true,"Alle "+SC_RUNDEN+" Runden getroffen — sauber!");}
|
|
else {sc.runde++;scNeueRunde();}
|
|
} else { // Verfehlt
|
|
sc.aktiv=false;spielEnde(false,"Zeiger verfehlt die Zone. Timing üben!");
|
|
}
|
|
}
|
|
|
|
/* =====================================================================
|
|
2) LOCKPICK (Marker in der Zone stoppen, mehrere Pins)
|
|
===================================================================== */
|
|
const LP_PINS=4;
|
|
const lp={pin:0, leben:3, pos:0, dir:1, spd:1.6, zoneStart:0, zoneBreite:26, aktiv:false};
|
|
sektionen.lockpick=()=>{
|
|
Object.assign(lp,{pin:0,leben:3,pos:5,dir:1,aktiv:true});
|
|
$("main").innerHTML=`<div class="phead"><h2>Lockpick</h2><p>Drücke <span class="kbd">Leertaste</span> oder klicke, wenn der Marker in der grünen Zone ist.</p></div>
|
|
<div class="stage">
|
|
<div class="roundinfo">Pins <span class="pins" id="lp-pins" style="display:inline-flex;vertical-align:middle;margin:0 6px"></span> · <span class="lives" id="lp-leben"></span></div>
|
|
<div class="lpbar" id="lp-bar"><div class="lp-zone" id="lp-zone"></div><div class="lp-marker" id="lp-marker"></div></div>
|
|
<button class="btn primary" id="lp-btn">Pin setzen</button>
|
|
</div>`;
|
|
lpRenderPins(); lpRenderLeben(); lpNeuerPin();
|
|
I(()=>{ // Marker hin und her bewegen
|
|
lp.pos+=lp.dir*lp.spd;
|
|
if(lp.pos>=100){lp.pos=100;lp.dir=-1;} if(lp.pos<=0){lp.pos=0;lp.dir=1;}
|
|
$("lp-marker").style.left="calc("+lp.pos+"% - 2.5px)";
|
|
},16);
|
|
$("lp-btn").addEventListener("click",lpAktion);
|
|
$("lp-bar").addEventListener("click",lpAktion);
|
|
};
|
|
/* Pin-Anzeige (gesetzt/offen) rendern */
|
|
function lpRenderPins(){$("lp-pins").innerHTML=Array.from({length:LP_PINS},(_,i)=>`<i class="${i<lp.pin?'set':''}" id="lp-pin-${i}"></i>`).join('');}
|
|
/* Verbleibende Versuche rendern */
|
|
function lpRenderLeben(){$("lp-leben").innerHTML="Versuche <b>"+lp.leben+"</b>";}
|
|
/* Neuen Pin vorbereiten: Zone zufällig, wird enger/schneller */
|
|
function lpNeuerPin(){
|
|
lp.zoneBreite=Math.max(12,26-lp.pin*3);
|
|
lp.spd=1.6+lp.pin*0.35;
|
|
lp.zoneStart=rnd(6,94-lp.zoneBreite);
|
|
$("lp-zone").style.left=lp.zoneStart+"%";
|
|
$("lp-zone").style.width=lp.zoneBreite+"%";
|
|
}
|
|
/* Aktion: liegt der Marker in der Zone? */
|
|
function lpAktion(){
|
|
if(!lp.aktiv)return;
|
|
// Doppelauslösung (Leertaste + fokussierter Button) verhindern
|
|
const jetzt=performance.now(); if(jetzt-letzteAktion<140)return; letzteAktion=jetzt;
|
|
if(lp.pos>=lp.zoneStart&&lp.pos<=lp.zoneStart+lp.zoneBreite){ // Pin gesetzt
|
|
lp.pin++;
|
|
if(lp.pin>=LP_PINS){lp.aktiv=false;lpRenderPins();spielEnde(true,"Alle Pins gesetzt — das Schloss ist offen!");return;}
|
|
lpRenderPins(); lpNeuerPin();
|
|
} else { // Fehlversuch
|
|
lp.leben--; lpRenderLeben();
|
|
if(lp.leben<=0){lp.aktiv=false;spielEnde(false,"Dietrich abgebrochen — kein Versuch mehr übrig.");return;}
|
|
lpNeuerPin(); // neuer Zonenort als „Ruck"
|
|
}
|
|
}
|
|
|
|
/* =====================================================================
|
|
3) MEMORY (wachsende Sequenz merken und wiederholen)
|
|
===================================================================== */
|
|
const MEM_ZIEL=5; // benötigte Sequenzlänge zum Gewinnen
|
|
const mem={seq:[], eingabe:0, sperre:true};
|
|
sektionen.memory=()=>{
|
|
Object.assign(mem,{seq:[],eingabe:0,sperre:true});
|
|
$("main").innerHTML=`<div class="phead"><h2>Memory</h2><p>Merke dir die aufleuchtende Reihenfolge und tippe sie nach.</p></div>
|
|
<div class="stage">
|
|
<div class="roundinfo" id="mem-info">Achtung …</div>
|
|
<div class="pads locked" id="mem-pads">${[0,1,2,3].map(p=>`<div class="pad" data-p="${p}"></div>`).join('')}</div>
|
|
</div>`;
|
|
$("mem-pads").querySelectorAll(".pad").forEach(p=>p.addEventListener("click",()=>memEingabe(+p.dataset.p)));
|
|
T(memNaechsteRunde,700);
|
|
};
|
|
/* Ein Feld kurz aufleuchten lassen */
|
|
function memBlink(p,dauer){const el=$("mem-pads").querySelector(`[data-p="${p}"]`);if(!el)return;el.classList.add("lit");T(()=>el.classList.remove("lit"),dauer);}
|
|
/* Sequenz verlängern und dem Spieler vorspielen */
|
|
function memNaechsteRunde(){
|
|
mem.seq.push(rnd(0,3)); mem.eingabe=0; mem.sperre=true;
|
|
$("mem-pads").classList.add("locked");
|
|
$("mem-info").textContent="Merken … ("+mem.seq.length+"/"+MEM_ZIEL+")";
|
|
mem.seq.forEach((p,i)=>{T(()=>memBlink(p,380),700+i*620);});
|
|
// Nach dem Vorspielen Eingabe freigeben
|
|
T(()=>{mem.sperre=false;$("mem-pads").classList.remove("locked");$("mem-info").textContent="Jetzt wiederholen …";},700+mem.seq.length*620+150);
|
|
}
|
|
/* Spieler-Eingabe prüfen */
|
|
function memEingabe(p){
|
|
if(mem.sperre)return;
|
|
memBlink(p,220);
|
|
if(p!==mem.seq[mem.eingabe]){mem.sperre=true;spielEnde(false,"Falsche Reihenfolge bei Schritt "+(mem.eingabe+1)+".");return;}
|
|
mem.eingabe++;
|
|
if(mem.eingabe===mem.seq.length){ // Sequenz korrekt wiederholt
|
|
if(mem.seq.length>=MEM_ZIEL){mem.sperre=true;spielEnde(true,"Sequenz aus "+MEM_ZIEL+" Feldern gemeistert!");return;}
|
|
mem.sperre=true; $("mem-pads").classList.add("locked"); $("mem-info").textContent="Richtig! Weiter …";
|
|
T(memNaechsteRunde,700);
|
|
}
|
|
}
|
|
|
|
/* =====================================================================
|
|
4) KEYPAD / CODE KNACKEN (Deduktion gegen die Zeit)
|
|
===================================================================== */
|
|
const KP_STELLEN=4, KP_VERSUCHE=6, KP_ZEIT=60;
|
|
const kp={code:[], eingabe:"", versuche:KP_VERSUCHE, zeit:KP_ZEIT, aktiv:false};
|
|
sektionen.keypad=()=>{
|
|
// Geheimen Code aus vier verschiedenen Ziffern erzeugen
|
|
const ziffern=[0,1,2,3,4,5,6,7,8,9]; kp.code=[];
|
|
for(let i=0;i<KP_STELLEN;i++)kp.code.push(ziffern.splice(rnd(0,ziffern.length-1),1)[0]);
|
|
Object.assign(kp,{eingabe:"",versuche:KP_VERSUCHE,zeit:KP_ZEIT,aktiv:true});
|
|
$("main").innerHTML=`<div class="phead"><h2>Code knacken</h2><p>Vier verschiedene Ziffern. Der Marker steht unter seiner Ziffer: 🟩 hier richtig · 🟨 kommt vor, aber woanders · ⬛ kommt nicht vor.</p></div>
|
|
<div class="stage"><div class="kpmeta"><div>Versuche <b id="kp-v">${KP_VERSUCHE}</b></div><div class="t" id="kp-tw">Zeit <b id="kp-t">${KP_ZEIT}s</b></div></div>
|
|
<div class="kpwrap">
|
|
<div>
|
|
<div class="kpdisplay" id="kp-disp"></div>
|
|
<div class="keypad" id="kp-pad" style="margin-top:14px">
|
|
${[1,2,3,4,5,6,7,8,9].map(n=>`<button data-k="${n}">${n}</button>`).join('')}
|
|
<button class="wide" data-k="del">⌫</button><button data-k="0">0</button><button class="wide" data-k="ok">✓</button>
|
|
</div>
|
|
</div>
|
|
<div class="kphist" id="kp-hist"></div>
|
|
</div>
|
|
</div>`;
|
|
kpRenderDisplay();
|
|
$("kp-pad").querySelectorAll("[data-k]").forEach(b=>b.addEventListener("click",()=>kpTaste(b.dataset.k)));
|
|
// Countdown starten
|
|
I(()=>{ if(!kp.aktiv)return; kp.zeit--; $("kp-t").textContent=kp.zeit+"s";
|
|
$("kp-tw").classList.toggle("low",kp.zeit<=10);
|
|
if(kp.zeit<=0){kp.aktiv=false;spielEnde(false,"Zeit abgelaufen — Code nicht geknackt. (Er war "+kp.code.join("")+")");}
|
|
},1000);
|
|
};
|
|
/* Aktuelle Eingabe in den vier Feldern anzeigen */
|
|
function kpRenderDisplay(){
|
|
$("kp-disp").innerHTML=Array.from({length:KP_STELLEN},(_,i)=>
|
|
`<div class="d ${i<kp.eingabe.length?'fill':''}">${kp.eingabe[i]??''}</div>`).join('');
|
|
}
|
|
/* Tastendruck auf dem Keypad verarbeiten */
|
|
function kpTaste(k){
|
|
if(!kp.aktiv)return;
|
|
if(k==="del")kp.eingabe=kp.eingabe.slice(0,-1);
|
|
else if(k==="ok"){ if(kp.eingabe.length===KP_STELLEN)kpPruefe(); return; }
|
|
else if(kp.eingabe.length<KP_STELLEN)kp.eingabe+=k;
|
|
kpRenderDisplay();
|
|
}
|
|
/* Eingabe mit dem Code vergleichen und Rückmeldung geben */
|
|
function kpPruefe(){
|
|
const g=kp.eingabe.split("").map(Number);
|
|
// Rückmeldung je Stelle: 🟩 richtige Ziffer hier · 🟨 Ziffer kommt vor, aber woanders · ⬛ kommt nicht vor
|
|
const marker=g.map((z,i)=> z===kp.code[i] ? "🟩" : (kp.code.includes(z) ? "🟨" : "⬛"));
|
|
const exakt=marker.filter(m=>m==="🟩").length;
|
|
// Ergebniszeile in die Historie schreiben (Marker steht unter seiner Ziffer)
|
|
const zeile=document.createElement("div"); zeile.className="row";
|
|
zeile.innerHTML=`<div class="zeile">${g.map((z,i)=>
|
|
`<div><div class="z">${z}</div><div class="m">${marker[i]}</div></div>`).join('')}</div>`;
|
|
$("kp-hist").prepend(zeile);
|
|
if(exakt===KP_STELLEN){kp.aktiv=false;spielEnde(true,"Code "+kp.code.join("")+" geknackt!");return;}
|
|
kp.versuche--; $("kp-v").textContent=kp.versuche;
|
|
if(kp.versuche<=0){kp.aktiv=false;spielEnde(false,"Keine Versuche mehr — der Code war "+kp.code.join("")+".");return;}
|
|
kp.eingabe=""; kpRenderDisplay();
|
|
}
|
|
|
|
/* =====================================================================
|
|
GLOBALE TASTATUR (Leertaste für Skill-Check/Lockpick, Ziffern fürs Keypad)
|
|
===================================================================== */
|
|
addEventListener("keydown",e=>{
|
|
if(e.repeat)return;
|
|
if(e.code==="Space"){ // Leertaste als Aktions-Taste
|
|
// Fokus vom Button nehmen, damit die Leertaste ihn nicht zusätzlich auslöst
|
|
if(document.activeElement&&document.activeElement.blur)document.activeElement.blur();
|
|
if(sektion==="skillcheck"){e.preventDefault();scAktion();}
|
|
else if(sektion==="lockpick"){e.preventDefault();lpAktion();}
|
|
} else if(sektion==="keypad"){
|
|
if(/^[0-9]$/.test(e.key))kpTaste(e.key);
|
|
else if(e.key==="Enter")kpTaste("ok");
|
|
else if(e.key==="Backspace")kpTaste("del");
|
|
}
|
|
});
|
|
|
|
/* =====================================================================
|
|
START
|
|
===================================================================== */
|
|
renderNav(); sektionen.lobby();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|