aether-framework/casino/index.html
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- 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>
2026-07-27 18:55:32 +02:00

367 lines
33 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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">
</head>
<body>
<title>Aether — Casino</title>
<style>
:root{
--bg:#070a08;--fg:#f0ecdf;--muted:#9a9683;--surface:rgba(24,30,24,0.72);--surface2:rgba(36,44,36,0.85);
--slot:#101610;--border:rgba(255,255,255,0.09);--accent:#e0b64a;--ink:#231a04;--accent-soft:rgba(224,182,74,0.16);
--green:#34d39a;--red:#f0506e;--felt:#12321f;--gold:#e0b64a;
}
*{box-sizing:border-box;} html,body{margin:0;height:100%;}
body{min-height:100%;background:radial-gradient(1100px 700px at 50% -10%,rgba(224,182,74,0.1),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;overflow-x:hidden;}
.app{position:relative;width:min(1200px,98vw);aspect-ratio:16/10;border-radius:20px;overflow:hidden;
background:linear-gradient(160deg,rgba(18,24,18,0.94),rgba(8,10,8,0.97));border:1px solid rgba(224,182,74,0.25);
box-shadow:0 50px 100px -30px rgba(0,0,0,0.85);display:grid;grid-template-columns:206px 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(--gold),#a8801f);color:#231a04;}
.brand .bn{font-size:16px;font-weight:800;letter-spacing:-0.02em;} .brand .bn span{color:var(--gold);} .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:14px;padding:0 18px;}
.top .chip{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:7px 13px;text-align:right;} .top .chip .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;font-weight:800;} .top .chip .v{font-size:15px;font-weight:800;font-variant-numeric:tabular-nums;}
.top .guthaben .v{color:var(--gold);} .top .netto.plus .v{color:var(--green);} .top .netto.minus .v{color:var(--red);}
.top .acts{margin-left:auto;display:flex;gap:8px;}
.nav{grid-area:nav;border-right:1px solid var(--border);padding:12px 9px;display:flex;flex-direction:column;gap:2px;overflow-y:auto;}
.nav button{display:flex;align-items:center;gap:10px;padding:9px 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(--gold);}
.main{grid-area:main;overflow-y:auto;padding:20px 24px;} .main::-webkit-scrollbar,.nav::-webkit-scrollbar{width:7px;} .main::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.09);border-radius:4px;}
.phead{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-bottom:16px;} .phead h2{font-size:21px;margin:0;font-weight:800;letter-spacing:-0.02em;} .phead p{margin:2px 0 0;color:var(--muted);font-size:12.5px;}
.btn{border:none;border-radius:11px;padding:11px 16px;font-size:14px;font-weight:800;cursor:pointer;font-family:inherit;transition:.12s;} .btn.primary{background:var(--gold);color:var(--ink);} .btn.primary:hover{filter:brightness(1.08);} .btn.ghost{background:var(--surface2);color:var(--fg);border:1px solid var(--border);} .btn.green{background:var(--green);color:#06231a;} .btn.red{background:var(--red);color:#2a0910;} .btn.sm{padding:7px 12px;font-size:12.5px;} .btn:disabled{opacity:.4;cursor:not-allowed;} .btn:active{transform:scale(0.98);}
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:18px;}
.felt{background:radial-gradient(120% 90% at 50% 0%,#184026,var(--felt));border:1px solid rgba(224,182,74,0.25);border-radius:18px;padding:22px;}
.list-h{font-size:10.5px;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted);font-weight:800;margin:0 0 10px;}
.input{width:120px;background:var(--slot);border:1px solid var(--border);border-radius:9px;padding:10px 11px;color:var(--fg);font-size:15px;font-family:inherit;outline:none;font-weight:700;text-align:center;font-variant-numeric:tabular-nums;} .input:focus{border-color:var(--gold);}
.flabel{font-size:10.5px;color:var(--muted);font-weight:800;text-transform:uppercase;letter-spacing:0.04em;margin:0 2px 6px;display:block;}
.quickchips{display:flex;gap:6px;} .quickchips button{border:none;background:var(--slot);border:1px solid var(--border);color:var(--fg);border-radius:8px;padding:8px 11px;font-family:inherit;font-weight:700;font-size:12.5px;cursor:pointer;} .quickchips button:hover{border-color:var(--gold);}
.pill{font-size:10px;font-weight:800;padding:3px 9px;border-radius:999px;display:inline-block;} .pill.g{background:rgba(52,211,153,0.16);color:var(--green);} .pill.r{background:rgba(240,80,110,0.16);color:var(--red);} .pill.gold{background:var(--accent-soft);color:var(--gold);}
.result{text-align:center;font-size:18px;font-weight:800;margin-top:12px;min-height:24px;} .result.win{color:var(--green);} .result.lose{color:var(--red);} .result.jackpot{color:var(--gold);}
/* Lobby */
.lobbygrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px;}
.gamecard{border-radius:16px;padding:18px;cursor:pointer;border:1px solid var(--border);background:var(--surface);transition:.12s;position:relative;overflow:hidden;}
.gamecard:hover{transform:translateY(-3px);border-color:var(--gold);} .gamecard .gi{font-size:36px;} .gamecard .gn{font-size:17px;font-weight:800;margin-top:8px;} .gamecard .gd{font-size:12px;color:var(--muted);margin-top:3px;} .gamecard .go{position:absolute;top:12px;right:14px;color:var(--gold);font-size:11px;font-weight:800;}
/* Slots */
.reels{display:flex;gap:12px;justify-content:center;margin:10px 0 6px;}
.reel{width:92px;height:110px;border-radius:14px;background:linear-gradient(180deg,#0c120c,#0a0e0a);border:2px solid rgba(224,182,74,0.4);display:grid;place-items:center;font-size:52px;overflow:hidden;box-shadow:0 0 0 4px rgba(0,0,0,0.3) inset;}
.reel.spin{animation:shake .1s infinite;} @keyframes shake{50%{transform:translateY(-2px);}}
.paytable{display:grid;grid-template-columns:1fr 1fr;gap:4px 18px;font-size:12px;color:var(--muted);} .paytable b{color:var(--fg);}
/* Karten */
.hand{display:flex;gap:8px;flex-wrap:wrap;min-height:96px;align-items:center;}
.pkarte{width:64px;height:90px;border-radius:9px;background:#f6f3ea;color:#1a1a1a;display:flex;flex-direction:column;justify-content:space-between;padding:6px 8px;font-weight:800;box-shadow:0 4px 10px rgba(0,0,0,0.4);animation:deal .3s ease;} @keyframes deal{from{transform:translateY(-14px) scale(.9);opacity:0;}to{transform:none;opacity:1;}}
.pkarte.rot{color:#c0392b;} .pkarte .r{font-size:16px;} .pkarte .s{font-size:22px;text-align:center;} .pkarte .r.bot{align-self:flex-end;transform:rotate(180deg);}
.pkarte.back{background:linear-gradient(135deg,#7c2f8f,#3a1450);color:transparent;} .pkarte.back::after{content:"♠";color:rgba(255,255,255,0.3);font-size:30px;margin:auto;}
.handval{font-size:13px;color:var(--muted);font-weight:700;margin-top:6px;}
/* Roulette */
.rnum{width:110px;height:110px;border-radius:50%;display:grid;place-items:center;font-size:44px;font-weight:800;margin:0 auto;border:3px solid rgba(224,182,74,0.5);}
.rnum.rot{background:#8a1f1f;} .rnum.schwarz{background:#1a1a1a;} .rnum.gruen{background:#14713f;}
.betbtns{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;} .betbtns button{border:none;background:var(--slot);border:1px solid var(--border);color:var(--fg);border-radius:9px;padding:10px;font-family:inherit;font-weight:700;font-size:12.5px;cursor:pointer;} .betbtns button:hover{border-color:var(--gold);}
.betlist{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;} .betlist .b{background:var(--surface2);border:1px solid var(--border);border-radius:999px;padding:4px 10px;font-size:11.5px;font-weight:700;}
/* Glücksrad */
.wheelwrap{position:relative;width:230px;height:230px;margin:0 auto;}
.wheel{width:100%;height:100%;border-radius:50%;border:6px solid var(--gold);transition:transform 4.4s cubic-bezier(.15,.9,.25,1);box-shadow:0 0 0 4px rgba(0,0,0,0.4);}
.wheel .lbl{position:absolute;top:50%;left:50%;font-size:12px;font-weight:800;color:#fff;text-shadow:0 1px 2px #000;white-space:nowrap;}
.wpointer{position:absolute;top:-6px;left:50%;transform:translateX(-50%);border-left:11px solid transparent;border-right:11px solid transparent;border-top:20px solid var(--gold);z-index:2;}
.whub{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:40px;height:40px;border-radius:50%;background:var(--gold);border:3px solid #231a04;z-index:2;display:grid;place-items:center;font-size:16px;}
/* Coin */
.coin{width:120px;height:120px;border-radius:50%;margin:6px auto 0;display:grid;place-items:center;font-size:50px;font-weight:800;color:#231a04;background:radial-gradient(circle at 35% 30%,#f4d97a,#c69a2e);border:4px solid #a8801f;transition:transform .1s;}
.coin.flip{animation:coinflip 1.2s ease;} @keyframes coinflip{0%{transform:rotateY(0);}100%{transform:rotateY(1800deg);}}
.toast{position:absolute;bottom:16px;left:50%;transform:translateX(-50%) translateY(16px);background:rgba(10,14,10,0.97);border:1px solid var(--border);color:var(--fg);padding:11px 20px;border-radius:12px;font-size:13.5px;font-weight:600;z-index:50;opacity:0;pointer-events:none;transition:.3s;} .toast.show{opacity:1;transform:translateX(-50%) translateY(0);} .toast.good{border-color:var(--green);} .toast.warn{border-color:var(--red);} .toast.gold{border-color:var(--gold);}
</style>
<div class="app">
<div class="brand"><span class="bl">🎰</span><div class="bn">Aether <span>Casino</span><small>Los Santos</small></div></div>
<div class="top" id="top"></div>
<div class="nav" id="nav"></div>
<div class="main" id="main"></div>
<div class="toast" id="toast"></div>
</div>
<script>
"use strict";
const $=id=>document.getElementById(id);
function sicher(t){const d=document.createElement("div");d.textContent=t==null?"":t;return d.innerHTML;}
const euro=n=>Math.round(n).toLocaleString("de-DE")+" €";
const rnd=(a,b)=>a+Math.floor(Math.random()*(b-a+1));
const pick=a=>a[Math.floor(Math.random()*a.length)];
let toastTimer=null; function toast(t,typ){const e=$("toast");e.textContent=t;e.className="toast show"+(typ?" "+typ:"");clearTimeout(toastTimer);toastTimer=setTimeout(()=>e.classList.remove("show"),2600);}
/* =====================================================================
GUTHABEN / SESSION
===================================================================== */
const spieler={bargeld:20000};
let guthaben=5000;
const session={ein:0,aus:0};
let sektion="lobby", busy=false;
function setzen(betrag){ if(!(betrag>0)){toast("Einsatz wählen","warn");return false;} if(betrag>guthaben){toast("Nicht genug Guthaben","warn");return false;} guthaben-=betrag; session.ein+=betrag; renderTop(); return true; }
function gewinne(total){ if(total>0){ guthaben+=total; session.aus+=total; } renderTop(); }
function renderTop(){
const netto=session.aus-session.ein;
$("top").innerHTML=`<div class="chip guthaben"><div class="l">Guthaben</div><div class="v">${euro(guthaben)}</div></div>
<div class="chip"><div class="l">Bargeld</div><div class="v">${euro(spieler.bargeld)}</div></div>
<div class="chip netto ${netto>=0?'plus':'minus'}"><div class="l">Session</div><div class="v">${netto>=0?'+':''}${euro(netto)}</div></div>
<div class="acts"><button class="btn ghost sm" id="t-load">+ Aufladen</button><button class="btn ghost sm" id="t-cash">Auszahlen</button></div>`;
$("t-load").addEventListener("click",()=>geldModal('load'));
$("t-cash").addEventListener("click",()=>geldModal('cash'));
}
function geldModal(dir){ const rein=dir==='load';
const b=prompt(rein?"Guthaben aufladen (Bargeld: "+euro(spieler.bargeld)+")\nBetrag in €:":"Guthaben auszahlen (Guthaben: "+euro(guthaben)+")\nBetrag in €:");
const n=parseInt(b,10); if(!n||n<=0)return;
if(rein){ if(n>spieler.bargeld){toast("Nicht genug Bargeld","warn");return;} spieler.bargeld-=n; guthaben+=n; toast(euro(n)+" aufgeladen","good"); }
else { if(n>guthaben){toast("Nicht genug Guthaben","warn");return;} guthaben-=n; spieler.bargeld+=n; toast(euro(n)+" ausgezahlt","good"); }
renderTop();
}
/* =====================================================================
SHELL
===================================================================== */
const nav=[{id:"lobby",label:"Lobby",icon:"🏛️"},{id:"slots",label:"Spielautomat",icon:"🎰"},{id:"blackjack",label:"Blackjack",icon:"🃏"},{id:"roulette",label:"Roulette",icon:"🎡"},{id:"wheel",label:"Glücksrad",icon:"🎯"},{id:"coin",label:"Münzwurf",icon:"🪙"}];
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",()=>{if(busy)return;sektion=b.dataset.s;renderNav();renderSektion();}));
}
function renderSektion(){(sektionen[sektion]||sektionen.lobby)();}
const sektionen={};
/* Einsatz-Steuerung (wiederverwendbar) */
function einsatzHtml(id,val){return `<div><label class="flabel">Einsatz</label><div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"><input class="input" id="${id}" type="number" value="${val}" min="1"><div class="quickchips">${[50,100,500,1000].map(c=>`<button data-chip="${c}">${c}</button>`).join('')}<button data-chip="max">Max</button></div></div></div>`;}
function bindEinsatz(id){document.querySelectorAll("[data-chip]").forEach(b=>b.addEventListener("click",()=>{const el=$(id);el.value=b.dataset.chip==='max'?guthaben:b.dataset.chip;}));}
const leseEinsatz=id=>parseInt($(id).value,10)||0;
/* =====================================================================
LOBBY
===================================================================== */
sektionen.lobby=()=>{
const netto=session.aus-session.ein;
$("main").innerHTML=`<div class="phead"><div><h2>Willkommen im Aether Casino</h2><p>Wähle ein Spiel · viel Glück!</p></div></div>
<div class="felt" style="margin-bottom:16px;display:flex;align-items:center;justify-content:space-between"><div><div style="font-size:12px;color:#bfe0c8;text-transform:uppercase;letter-spacing:0.05em;font-weight:800">Dein Guthaben</div><div style="font-size:38px;font-weight:800;color:var(--gold)">${euro(guthaben)}</div></div>
<div style="text-align:right"><div style="font-size:12px;color:#bfe0c8">Session-Bilanz</div><div style="font-size:22px;font-weight:800;color:${netto>=0?'var(--green)':'var(--red)'}">${netto>=0?'+':''}${euro(netto)}</div></div></div>
<div class="lobbygrid">${nav.slice(1).map(g=>{const info={slots:"3 Walzen · Jackpot bis ×180",blackjack:"Schlage den Dealer auf 21",roulette:"Farben, Zahlen & Dutzende",wheel:"Dreh am Rad · bis ×5",coin:"Kopf oder Zahl · ×2"};
return `<div class="gamecard" data-g="${g.id}"><div class="gi">${g.icon}</div><div class="gn">${g.label}</div><div class="gd">${info[g.id]}</div><div class="go">Spielen ›</div></div>`;}).join('')}</div>
<div style="text-align:center;color:var(--muted);font-size:11.5px;margin-top:14px">🎲 Spiel mit Verantwortung — auf lange Sicht gewinnt die Bank.</div>`;
$("main").querySelectorAll("[data-g]").forEach(c=>c.addEventListener("click",()=>{sektion=c.dataset.g;renderNav();renderSektion();}));
};
/* =====================================================================
SLOTS
===================================================================== */
/* Gewichtete Walzen: seltene Symbole = hohe Auszahlung. RTP ~90% (Hausvorteil ~10%). */
const slotReel=[{s:"🍒",w:8},{s:"🍋",w:7},{s:"🔔",w:6},{s:"⭐",w:4},{s:"💎",w:3},{s:"7️⃣",w:2}];
const slotPay={"🍒":10,"🍋":12,"🔔":18,"⭐":35,"💎":70,"7️⃣":180};
const slotSyms=slotReel.map(x=>x.s), slotGewicht=slotReel.reduce((s,x)=>s+x.w,0);
function slotPick(){let r=Math.random()*slotGewicht;for(const x of slotReel){if((r-=x.w)<0)return x.s;}return slotReel[0].s;}
sektionen.slots=()=>{
setSpiel("slots");
$("main").innerHTML=`<div class="phead"><div><h2>Spielautomat</h2><p>3× gleiches Symbol gewinnt · seltene Symbole zahlen mehr · 7️⃣7️⃣7️⃣ = Jackpot ×180</p></div></div>
<div class="felt" style="text-align:center"><div class="reels"><div class="reel" id="r0">🍒</div><div class="reel" id="r1">🔔</div><div class="reel" id="r2">7️⃣</div></div>
<div class="result" id="slot-res">Setze deinen Einsatz und drehe!</div></div>
<div class="card" style="margin-top:14px;display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap">${einsatzHtml('slot-ein',100)}<button class="btn primary" id="slot-spin" style="min-width:160px">🎰 Drehen</button></div>
<div class="card" style="margin-top:14px"><div class="list-h">Auszahlungstabelle (×Einsatz)</div><div class="paytable">${slotReel.map(x=>`<div>${x.s} ${x.s} ${x.s} <b>×${slotPay[x.s]}</b></div>`).join('')}<div>🍒 🍒 <span style="color:var(--muted)">(zwei)</span> <b>×1.5</b></div></div></div>`;
bindEinsatz('slot-ein');
$("slot-spin").addEventListener("click",slotSpin);
};
function slotSpin(){
if(busy)return; const ein=leseEinsatz('slot-ein'); if(!setzen(ein))return;
busy=true; setSpiel("slots"); $("slot-spin").disabled=true; $("slot-res").textContent="…"; $("slot-res").className="result";
const finals=[slotPick(),slotPick(),slotPick()];
const ivs=[]; [0,1,2].forEach(i=>{$("r"+i).classList.add("spin");ivs[i]=setInterval(()=>{$("r"+i).textContent=pick(slotSyms);},70);});
[0,1,2].forEach(i=>setTimeout(()=>{clearInterval(ivs[i]);$("r"+i).classList.remove("spin");$("r"+i).textContent=finals[i];if(i===2)slotErgebnis(finals,ein);},700+i*450));
}
function slotErgebnis(f,ein){
let gewinn=0,txt="",cls="lose";
if(f[0]===f[1]&&f[1]===f[2]){ gewinn=ein*slotPay[f[0]]; cls=f[0]==="7️⃣"?"jackpot":"win"; txt=(f[0]==="7️⃣"?"🎉 JACKPOT! ":"Drei Gleiche! ")+f.join("")+" · +"+euro(gewinn); }
else if(f.filter(x=>x==="🍒").length===2){ gewinn=Math.round(ein*1.5); cls="win"; txt="Zwei 🍒 · +"+euro(gewinn); }
else { txt="Kein Gewinn · "+euro(ein)+" verloren"; }
if(gewinn>0)gewinne(gewinn);
const r=$("slot-res"); r.textContent=txt; r.className="result "+cls;
if(gewinn>0)toast((cls==="jackpot"?"🎉 JACKPOT ":"Gewonnen ")+euro(gewinn),cls==="jackpot"?"gold":"good");
busy=false; $("slot-spin").disabled=false;
}
/* =====================================================================
BLACKJACK
===================================================================== */
const suits=[["♠",0],["♥",1],["♦",1],["♣",0]], ranks=["A","2","3","4","5","6","7","8","9","10","J","Q","K"];
function neuesDeck(){const d=[];for(const [s,rot] of suits)for(const r of ranks)d.push({r,s,rot});for(let i=d.length-1;i>0;i--){const j=rnd(0,i);[d[i],d[j]]=[d[j],d[i]];}return d;}
function kWert(c){return c.r==="A"?11:["J","Q","K","10"].includes(c.r)?10:+c.r;}
function handWert(h){let sum=h.reduce((s,c)=>s+kWert(c),0),as=h.filter(c=>c.r==="A").length;while(sum>21&&as>0){sum-=10;as--;}return sum;}
function karteHtml(c,back){if(back)return `<div class="pkarte back"></div>`;return `<div class="pkarte ${c.rot?'rot':''}"><div class="r">${c.r}<span style="font-size:13px"> ${c.s}</span></div><div class="s">${c.s}</div><div class="r bot">${c.r}<span style="font-size:13px"> ${c.s}</span></div></div>`;}
const bj={phase:"bet"};
sektionen.blackjack=()=>{
setSpiel("blackjack");
const spielt=bj.phase==="spielen", ende=bj.phase==="ende";
const dVal=bj.dealer?(spielt?kWert(bj.dealer[0]):handWert(bj.dealer)):0;
$("main").innerHTML=`<div class="phead"><div><h2>Blackjack</h2><p>Näher an 21 als der Dealer · überkaufst du zuerst, verlierst du</p></div></div>
<div class="felt">
<div class="list-h" style="color:#bfe0c8">Dealer ${bj.dealer?'· '+(spielt?dVal+' +?':handWert(bj.dealer)):''}</div>
<div class="hand" id="bj-dealer">${bj.dealer?bj.dealer.map((c,i)=>karteHtml(c,spielt&&i===1)).join(''):'<span style="color:var(--muted)">—</span>'}</div>
<div class="list-h" style="color:#bfe0c8;margin-top:14px">Deine Hand ${bj.player?'· '+handWert(bj.player):''}</div>
<div class="hand" id="bj-player">${bj.player?bj.player.map(c=>karteHtml(c)).join(''):'<span style="color:var(--muted)">—</span>'}</div>
<div class="result ${bj.resCls||''}" id="bj-res">${bj.resTxt||''}</div>
</div>
<div class="card" style="margin-top:14px;display:flex;align-items:flex-end;gap:14px;flex-wrap:wrap">
${bj.phase==="bet"||ende?einsatzHtml('bj-ein',bj.ein||100):`<div><div class="flabel">Einsatz</div><div style="font-size:18px;font-weight:800">${euro(bj.ein)}</div></div>`}
${bj.phase==="bet"||ende?`<button class="btn primary" id="bj-deal" style="min-width:150px">🃏 Austeilen</button>`:
`<button class="btn ghost" id="bj-hit">Ziehen</button><button class="btn green" id="bj-stand">Stehen</button><button class="btn ghost" id="bj-double" ${bj.player.length===2&&guthaben>=bj.ein?'':'disabled'}>Verdoppeln</button>`}
</div>`;
if(bj.phase==="bet"||ende){bindEinsatz('bj-ein');$("bj-deal").addEventListener("click",bjDeal);}
else{$("bj-hit").addEventListener("click",bjHit);$("bj-stand").addEventListener("click",bjStand);$("bj-double").addEventListener("click",bjDouble);}
};
function bjDeal(){const ein=leseEinsatz('bj-ein');if(!setzen(ein))return;bj.ein=ein;bj.deck=neuesDeck();bj.player=[bj.deck.pop(),bj.deck.pop()];bj.dealer=[bj.deck.pop(),bj.deck.pop()];bj.phase="spielen";bj.resTxt="";bj.resCls="";
if(handWert(bj.player)===21){bjStand(true);}else sektionen.blackjack();}
function bjHit(){bj.player.push(bj.deck.pop());if(handWert(bj.player)>21){bjEnde("bust");}else sektionen.blackjack();}
function bjDouble(){if(!setzen(bj.ein))return;bj.ein*=2;bj.player.push(bj.deck.pop());if(handWert(bj.player)>21)bjEnde("bust");else bjStand();}
function bjStand(auto){ while(handWert(bj.dealer)<17)bj.dealer.push(bj.deck.pop()); bjEnde(auto===true?"bjcheck":"stand"); }
function bjEnde(){const pv=handWert(bj.player),dv=handWert(bj.dealer);let total=0,txt,cls;
const blackjack=pv===21&&bj.player.length===2;
if(pv>21){txt="Überkauft! −"+euro(bj.ein);cls="lose";}
else if(dv>21){total=bj.ein*2;txt="Dealer überkauft · +"+euro(bj.ein);cls="win";}
else if(blackjack&&!(dv===21&&bj.dealer.length===2)){total=bj.ein*2;txt="🎉 Blackjack! +"+euro(bj.ein);cls="jackpot";}
else if(pv>dv){total=bj.ein*2;txt="Du gewinnst · +"+euro(bj.ein);cls="win";}
else if(pv<dv){txt="Dealer gewinnt · −"+euro(bj.ein);cls="lose";}
else {total=bj.ein;txt="Unentschieden · Einsatz zurück";cls="";}
gewinne(total); bj.phase="ende"; bj.resTxt=txt; bj.resCls=cls;
if(total>bj.ein)toast(txt,cls==="jackpot"?"gold":"good");
sektionen.blackjack();
}
/* =====================================================================
ROULETTE
===================================================================== */
const rotZahlen=[1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32,34,36];
const rFarbe=n=>n===0?"gruen":rotZahlen.includes(n)?"rot":"schwarz";
const rlt={bets:[]};
sektionen.roulette=()=>{
setSpiel("roulette");
const gesamt=rlt.bets.reduce((s,b)=>s+b.betrag,0);
$("main").innerHTML=`<div class="phead"><div><h2>Roulette</h2><p>Setze auf Farben, Zahlen oder Dutzende</p></div></div>
<div class="felt" style="text-align:center"><div class="rnum ${rlt.last!=null?rFarbe(rlt.last):'schwarz'}" id="rnum">${rlt.last!=null?rlt.last:'—'}</div><div class="result ${rlt.resCls||''}" id="rlt-res" style="margin-top:14px">${rlt.resTxt||'Platziere Einsätze und drehe'}</div></div>
<div class="card" style="margin-top:14px"><div style="display:flex;align-items:flex-end;gap:14px;flex-wrap:wrap;margin-bottom:12px">${einsatzHtml('rlt-ein',100)}<div style="flex:1"></div><button class="btn primary" id="rlt-spin" ${gesamt>0?'':'disabled'} style="min-width:150px">🎡 Drehen (${euro(gesamt)})</button><button class="btn ghost" id="rlt-clear" ${gesamt>0?'':'disabled'}>Löschen</button></div>
<div class="list-h">Einfache Chancen (×2)</div><div class="betbtns" style="grid-template-columns:repeat(6,1fr);margin-bottom:8px">
<button data-bet="rot|Rot">🔴 Rot</button><button data-bet="schwarz|Schwarz">⚫ Schwarz</button><button data-bet="gerade|Gerade">Gerade</button><button data-bet="ungerade|Ungerade">Ungerade</button><button data-bet="tief|1–18">1–18</button><button data-bet="hoch|19–36">19–36</button></div>
<div class="list-h">Dutzend (×3)</div><div class="betbtns" style="margin-bottom:8px"><button data-bet="d1|1. Dutzend">1–12</button><button data-bet="d2|2. Dutzend">13–24</button><button data-bet="d3|3. Dutzend">25–36</button></div>
<div class="list-h">Einzelne Zahl (×36)</div><div style="display:flex;gap:8px;align-items:center"><input class="input" id="rlt-zahl" type="number" min="0" max="36" placeholder="0–36"><button class="btn ghost sm" id="rlt-setzahl">Zahl setzen</button></div>
${rlt.bets.length?`<div class="betlist">${rlt.bets.map((b,i)=>`<span class="b">${sicher(b.label)}: ${euro(b.betrag)} <span style="cursor:pointer;color:var(--red)" data-delbet="${i}">✕</span></span>`).join('')}</div>`:''}</div>`;
bindEinsatz('rlt-ein');
$("main").querySelectorAll("[data-bet]").forEach(b=>b.addEventListener("click",()=>rltAddBet(b.dataset.bet)));
$("rlt-setzahl").addEventListener("click",()=>{const z=parseInt($("rlt-zahl").value,10);if(isNaN(z)||z<0||z>36){toast("Zahl 0–36","warn");return;}rltAddBet("z"+z+"|Zahl "+z);});
$("rlt-spin").addEventListener("click",rltSpin);
$("rlt-clear").addEventListener("click",()=>{rlt.bets.forEach(b=>{guthaben+=b.betrag;session.ein-=b.betrag;});rlt.bets=[];renderTop();sektionen.roulette();});
$("main").querySelectorAll("[data-delbet]").forEach(b=>b.addEventListener("click",()=>{const i=+b.dataset.delbet,x=rlt.bets.splice(i,1)[0];guthaben+=x.betrag;session.ein-=x.betrag;renderTop();sektionen.roulette();}));
};
function rltAddBet(spec){const ein=leseEinsatz('rlt-ein');if(!setzen(ein))return;const [typ,label]=spec.split('|');rlt.bets.push({typ,label,betrag:ein});sektionen.roulette();}
function rltSpin(){
if(busy||!rlt.bets.length)return; busy=true; setSpiel("roulette");
const num=rnd(0,36); const el=$("rnum"); let t=0;
const iv=setInterval(()=>{const r=rnd(0,36);el.textContent=r;el.className="rnum "+rFarbe(r);t++;if(t>18){clearInterval(iv);el.textContent=num;el.className="rnum "+rFarbe(num);rltErgebnis(num);}},90);
$("rlt-res").textContent="Die Kugel rollt…"; $("rlt-res").className="result";
}
function rltErgebnis(num){
let gewinn=0; const gesamt=rlt.bets.reduce((s,b)=>s+b.betrag,0);
for(const b of rlt.bets){ let g=0;
if(b.typ==="rot"&&rFarbe(num)==="rot")g=b.betrag*2;
else if(b.typ==="schwarz"&&rFarbe(num)==="schwarz")g=b.betrag*2;
else if(b.typ==="gerade"&&num!==0&&num%2===0)g=b.betrag*2;
else if(b.typ==="ungerade"&&num%2===1)g=b.betrag*2;
else if(b.typ==="tief"&&num>=1&&num<=18)g=b.betrag*2;
else if(b.typ==="hoch"&&num>=19&&num<=36)g=b.betrag*2;
else if(b.typ==="d1"&&num>=1&&num<=12)g=b.betrag*3;
else if(b.typ==="d2"&&num>=13&&num<=24)g=b.betrag*3;
else if(b.typ==="d3"&&num>=25&&num<=36)g=b.betrag*3;
else if(b.typ.startsWith("z")&&+b.typ.slice(1)===num)g=b.betrag*36;
gewinn+=g;
}
gewinne(gewinn);
rlt.last=num; rlt.bets=[];
if(gewinn>0){rlt.resTxt="Zahl "+num+" ("+({rot:"Rot",schwarz:"Schwarz",gruen:"Grün"}[rFarbe(num)])+") · +"+euro(gewinn);rlt.resCls="win";toast("Gewonnen "+euro(gewinn),"good");}
else {rlt.resTxt="Zahl "+num+" · "+euro(gesamt)+" verloren";rlt.resCls="lose";}
busy=false; sektionen.roulette();
}
/* =====================================================================
GLÜCKSRAD
===================================================================== */
/* 12 Felder, die meisten verlieren (×0/×0.5). EV ≈ 0.79 → Hausvorteil ~21%. */
const wSegs=[{m:0,c:"#4a2530"},{m:0.5,c:"#3d4a2a"},{m:0,c:"#2a3340"},{m:1.5,c:"#2f6b4a"},{m:0,c:"#4a2530"},{m:0,c:"#2a3340"},{m:2,c:"#3563a8"},{m:0,c:"#4a2530"},{m:0.5,c:"#3d4a2a"},{m:0,c:"#2a3340"},{m:5,c:"#e0b64a"},{m:0,c:"#4a2530"}];
let wRot=0;
sektionen.wheel=()=>{
setSpiel("wheel");
const seg=360/wSegs.length;
const grad=wSegs.map((s,i)=>`${s.c} ${i*seg}deg ${(i+1)*seg}deg`).join(",");
$("main").innerHTML=`<div class="phead"><div><h2>Glücksrad</h2><p>Setze und dreh am Rad · Multiplikator bis ×10</p></div></div>
<div class="felt" style="text-align:center"><div class="wheelwrap"><div class="wpointer"></div><div class="wheel" id="wheel" style="background:conic-gradient(${grad});transform:rotate(${wRot}deg)">${wSegs.map((s,i)=>`<div class="lbl" style="transform:translate(-50%,-50%) rotate(${i*seg+seg/2}deg) translateY(-84px)">×${s.m}</div>`).join('')}</div><div class="whub">🎯</div></div>
<div class="result ${wheelResCls||''}" id="wheel-res" style="margin-top:14px">${wheelResTxt||'Setze deinen Einsatz und drehe'}</div></div>
<div class="card" style="margin-top:14px;display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap">${einsatzHtml('wheel-ein',100)}<button class="btn primary" id="wheel-spin" style="min-width:160px">🎯 Drehen</button></div>`;
bindEinsatz('wheel-ein');
$("wheel-spin").addEventListener("click",wheelSpin);
};
let wheelResTxt="",wheelResCls="";
function wheelSpin(){
if(busy)return; const ein=leseEinsatz('wheel-ein'); if(!setzen(ein))return;
busy=true; const seg=360/wSegs.length; const idx=rnd(0,wSegs.length-1);
// Feld idx mittig unter den Zeiger (oben) bringen: Zielwinkel = -Feldmitte
const zielMod=((360-(idx*seg+seg/2))%360+360)%360;
const aktuellMod=((wRot%360)+360)%360;
const delta=((zielMod-aktuellMod)%360+360)%360;
wRot+=360*5+delta; // mind. 5 volle Umdrehungen, endet exakt auf idx
const w=$("wheel"); w.style.transform="rotate("+wRot+"deg)";
$("wheel-spin").disabled=true; $("wheel-res").textContent="Das Rad dreht…"; $("wheel-res").className="result";
setTimeout(()=>{const m=wSegs[idx].m,gewinn=Math.round(ein*m);gewinne(gewinn);
if(m>1){wheelResTxt="×"+m+" · +"+euro(gewinn-ein);wheelResCls=m>=5?"jackpot":"win";toast("×"+m+" · "+euro(gewinn),m>=5?"gold":"good");}
else if(m===1){wheelResTxt="×1 · Einsatz zurück";wheelResCls="";}
else if(m>0){wheelResTxt="×"+m+" · nur "+euro(gewinn)+" zurück ("+euro(ein-gewinn)+" Verlust)";wheelResCls="lose";}
else {wheelResTxt="Leider verloren · −"+euro(ein);wheelResCls="lose";}
busy=false; sektionen.wheel();},4500);
}
/* =====================================================================
MÜNZWURF
===================================================================== */
let coinRes="🪙",coinResTxt="",coinResCls="";
sektionen.coin=()=>{
setSpiel("coin");
$("main").innerHTML=`<div class="phead"><div><h2>Münzwurf</h2><p>Kopf oder Zahl · richtig = ×2</p></div></div>
<div class="felt" style="text-align:center"><div class="coin" id="coin">${coinRes}</div><div class="result ${coinResCls}" id="coin-res" style="margin-top:14px">${coinResTxt||'Wähle Kopf oder Zahl'}</div></div>
<div class="card" style="margin-top:14px;display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap">${einsatzHtml('coin-ein',100)}<div style="display:flex;gap:8px"><button class="btn ghost" id="coin-kopf" style="min-width:110px">👑 Kopf</button><button class="btn ghost" id="coin-zahl" style="min-width:110px">🔢 Zahl</button></div></div>`;
bindEinsatz('coin-ein');
$("coin-kopf").addEventListener("click",()=>coinFlip("kopf"));
$("coin-zahl").addEventListener("click",()=>coinFlip("zahl"));
};
function coinFlip(wahl){
if(busy)return; const ein=leseEinsatz('coin-ein'); if(!setzen(ein))return;
busy=true; const c=$("coin"); c.classList.remove("flip"); void c.offsetWidth; c.classList.add("flip");
$("coin-res").textContent="…"; $("coin-res").className="result";
const gewonnen=Math.random()<0.475; const erg=gewonnen?wahl:(wahl==="kopf"?"zahl":"kopf");
setTimeout(()=>{coinRes=erg==="kopf"?"👑":"🔢";c.textContent=coinRes;
if(gewonnen){const g=ein*2;gewinne(g);coinResTxt=(erg==="kopf"?"Kopf":"Zahl")+" · +"+euro(ein);coinResCls="win";toast("Gewonnen "+euro(g),"good");}
else {coinResTxt=(erg==="kopf"?"Kopf":"Zahl")+" · −"+euro(ein);coinResCls="lose";}
busy=false; sektionen.coin();},1200);
}
/* Spielwechsel-Reset (offene Einsätze erstatten) */
function setSpiel(id){
if(id!=="blackjack"){bj.phase="bet";bj.player=null;bj.dealer=null;bj.resTxt="";bj.resCls="";}
if(id!=="roulette"&&rlt.bets&&rlt.bets.length){rlt.bets.forEach(b=>{guthaben+=b.betrag;session.ein-=b.betrag;});rlt.bets=[];renderTop();}
}
/* START */
renderTop(); renderNav(); renderSektion();
</script>
</body>
</html>