- 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>
367 lines
33 KiB
HTML
367 lines
33 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">
|
||
</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>
|