- 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>
520 lines
38 KiB
HTML
520 lines
38 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 — Charaktererstellung</title>
|
||
<style>
|
||
:root{
|
||
--bg:#07080c;--fg:#eef0f6;--muted:#8a8fa3;--surface:rgba(22,24,33,0.72);--surface2:rgba(36,39,52,0.85);
|
||
--slot:#12141c;--border:rgba(255,255,255,0.1);--accent:#c9a24a;--ink:#241a06;--accent-soft:rgba(201,162,74,0.16);
|
||
--red:#f0506e;--green:#34d39a;
|
||
}
|
||
*{box-sizing:border-box;} html,body{margin:0;height:100%;}
|
||
/* In FiveM ist der Hintergrund transparent — dahinter steht der echte 3D-Ped. */
|
||
body{min-height:100%;background:transparent;
|
||
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--fg);
|
||
display:flex;align-items:center;justify-content:center;overflow:hidden;}
|
||
.stage{position:relative;width:100%;height:100%;overflow:hidden;}
|
||
.view{position:absolute;inset:0;display:none;} .view.active{display:flex;}
|
||
.fade{animation:fin .35s ease;} @keyframes fin{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}
|
||
|
||
/* Vorschau-Bühne: transparentes Fenster auf den 3D-Ped */
|
||
.preview{position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden;
|
||
background:transparent;}
|
||
.preview .floor{display:none;} /* Boden kommt aus der Spielwelt */
|
||
.preview svg{position:relative;z-index:1;height:88%;max-width:92%;filter:drop-shadow(0 30px 40px rgba(0,0,0,0.55));}
|
||
|
||
/* Drehsteuerung unter dem Ped */
|
||
.turnbar{position:absolute;bottom:22px;left:50%;transform:translateX(-50%);z-index:3;display:flex;gap:8px;
|
||
background:rgba(0,0,0,0.45);border:1px solid var(--border);border-radius:12px;padding:6px;backdrop-filter:blur(6px);}
|
||
.turnbar button{border:none;background:rgba(255,255,255,0.06);color:var(--fg);border-radius:8px;
|
||
width:44px;height:34px;font-size:15px;cursor:pointer;font-family:inherit;transition:.12s;}
|
||
.turnbar button:hover{background:var(--accent);color:var(--ink);}
|
||
.turnbar .hint{color:var(--muted);font-size:11px;display:flex;align-items:center;padding:0 8px;font-weight:600;}
|
||
|
||
/* ============ Eigenständige Browser-Ansicht (ohne FiveM) ============
|
||
Stellt den früheren Kasten-Look samt SVG-Vorschau wieder her. */
|
||
body.browser{background:radial-gradient(1200px 800px at 50% -10%,rgba(201,162,74,0.1),transparent 55%),var(--bg);
|
||
padding:24px 16px;}
|
||
body.browser .stage{width:min(1280px,97vw);height:auto;aspect-ratio:16/10;border-radius:22px;
|
||
background:radial-gradient(120% 90% at 50% 0%,#171a24,#0a0c11 70%);border:1px solid var(--border);
|
||
box-shadow:0 50px 100px -30px rgba(0,0,0,0.85);}
|
||
body.browser .preview{background:radial-gradient(70% 60% at 50% 40%,rgba(201,162,74,0.08),transparent 70%),linear-gradient(180deg,#12141c,#0a0c11);}
|
||
body.browser .preview .floor{display:block;position:absolute;bottom:6%;left:50%;transform:translateX(-50%);
|
||
width:52%;height:5%;border-radius:50%;background:radial-gradient(ellipse,rgba(201,162,74,0.25),transparent 70%);}
|
||
body.browser .turnbar{display:none;} /* Drehen gibt es nur mit echtem Ped */
|
||
.view-toggle{position:absolute;top:16px;left:50%;transform:translateX(-50%);z-index:3;display:flex;background:rgba(0,0,0,0.4);border:1px solid var(--border);border-radius:11px;padding:4px;gap:2px;}
|
||
.view-toggle button{border:none;background:none;color:var(--muted);font-family:inherit;font-weight:700;font-size:12.5px;padding:7px 14px;border-radius:8px;cursor:pointer;}
|
||
.view-toggle button.active{background:var(--accent);color:var(--ink);}
|
||
.turn{transform-box:fill-box;transform-origin:center;animation:turn 8s ease-in-out infinite;}
|
||
@keyframes turn{0%,100%{transform:rotateY(-16deg);}50%{transform:rotateY(16deg);}}
|
||
@media (prefers-reduced-motion:reduce){.turn{animation:none;}}
|
||
|
||
/* ============ Auswahl ============ */
|
||
.sel-wrap{width:100%;display:grid;grid-template-columns:340px 1fr;}
|
||
.sel-list{padding:26px 22px;border-right:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto;
|
||
background:rgba(10,12,17,0.92);backdrop-filter:blur(10px);}
|
||
.sel-list h1{font-size:24px;margin:0 0 3px;font-weight:800;letter-spacing:-0.02em;} .sel-list .sub{color:var(--muted);font-size:13px;margin-bottom:18px;}
|
||
.char-slot{display:flex;align-items:center;gap:13px;padding:13px;border-radius:15px;background:var(--surface);border:1px solid var(--border);cursor:pointer;margin-bottom:10px;transition:.14s;}
|
||
.char-slot:hover{border-color:var(--accent);transform:translateX(2px);} .char-slot.active{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent) inset;background:var(--accent-soft);}
|
||
.char-slot .thumb{width:52px;height:52px;border-radius:12px;overflow:hidden;flex-shrink:0;background:#0c0e14;display:grid;place-items:center;}
|
||
.char-slot .thumb svg{height:96%;} .char-slot .cn{font-size:15px;font-weight:800;} .char-slot .cm{font-size:12px;color:var(--muted);}
|
||
/* Initialen als Slot-Vorschau (in FiveM statt SVG-Gesicht) */
|
||
.char-slot .thumb .ini{font-size:18px;font-weight:800;color:var(--accent);letter-spacing:0.03em;}
|
||
.char-slot.empty{border-style:dashed;justify-content:center;color:var(--muted);font-weight:700;font-size:14px;} .char-slot.empty:hover{color:var(--accent);}
|
||
.char-slot.empty .plus{font-size:20px;margin-right:6px;}
|
||
|
||
.sel-detail{position:relative;display:grid;grid-template-rows:1fr auto;}
|
||
.sel-info{position:absolute;bottom:0;left:0;right:0;z-index:2;padding:26px 30px;background:linear-gradient(0deg,rgba(8,9,13,0.92),transparent);}
|
||
.sel-info h2{font-size:32px;margin:0;font-weight:800;letter-spacing:-0.02em;} .sel-info .role{color:var(--accent);font-weight:700;font-size:14px;margin-top:2px;}
|
||
.sel-stats{display:flex;gap:24px;margin:14px 0 18px;flex-wrap:wrap;} .sel-stats .st .l{font-size:10.5px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;font-weight:800;} .sel-stats .st .v{font-size:17px;font-weight:800;font-variant-numeric:tabular-nums;}
|
||
.sel-actions{display:flex;gap:10px;}
|
||
|
||
/* ============ Creator ============ */
|
||
.cr-wrap{width:100%;display:grid;grid-template-columns:1fr 400px;}
|
||
.cr-panel{border-left:1px solid var(--border);display:grid;grid-template-rows:auto auto 1fr auto;min-height:0;
|
||
background:rgba(10,12,17,0.94);backdrop-filter:blur(10px);}
|
||
.cr-head{padding:20px 22px 12px;} .cr-head h1{font-size:21px;margin:0;font-weight:800;letter-spacing:-0.02em;} .cr-head p{margin:2px 0 0;color:var(--muted);font-size:12.5px;}
|
||
.cr-tabs{display:flex;gap:3px;padding:0 22px;flex-wrap:wrap;} .cr-tabs button{border:none;background:none;color:var(--muted);font-family:inherit;font-weight:700;font-size:12.5px;padding:9px 12px;border-radius:9px 9px 0 0;cursor:pointer;border-bottom:2px solid transparent;}
|
||
.cr-tabs button.active{color:var(--accent);border-bottom-color:var(--accent);}
|
||
.cr-body{overflow-y:auto;padding:18px 22px;} .cr-body::-webkit-scrollbar{width:7px;} .cr-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.1);border-radius:4px;}
|
||
.cr-foot{padding:14px 22px;border-top:1px solid var(--border);display:flex;gap:10px;}
|
||
|
||
.flabel{font-size:11px;color:var(--muted);font-weight:800;text-transform:uppercase;letter-spacing:0.05em;margin:16px 2px 8px;display:block;} .flabel:first-child{margin-top:0;}
|
||
.input,select.input{width:100%;background:var(--slot);border:1px solid var(--border);border-radius:10px;padding:11px 12px;color:var(--fg);font-size:14px;font-family:inherit;outline:none;} .input:focus{border-color:var(--accent);}
|
||
.swatches{display:flex;flex-wrap:wrap;gap:8px;}
|
||
.sw{width:34px;height:34px;border-radius:9px;cursor:pointer;border:2px solid transparent;position:relative;} .sw.sel{border-color:#fff;box-shadow:0 0 0 2px var(--accent);}
|
||
.sw[title]:hover::after{content:attr(title);position:absolute;bottom:120%;left:50%;transform:translateX(-50%);background:#000;color:#fff;font-size:10px;padding:2px 6px;border-radius:5px;white-space:nowrap;z-index:5;}
|
||
.opts{display:flex;flex-wrap:wrap;gap:7px;} .opt{padding:9px 14px;border-radius:9px;background:var(--slot);border:1px solid var(--border);color:var(--fg);font-family:inherit;font-weight:700;font-size:12.5px;cursor:pointer;} .opt.sel{background:var(--accent);color:var(--ink);border-color:var(--accent);}
|
||
.slider{display:flex;align-items:center;gap:12px;margin-bottom:12px;} .slider .sl-l{width:110px;font-size:12.5px;font-weight:600;} .slider input[type=range]{flex:1;accent-color:var(--accent);}
|
||
.slider .sl-v{width:34px;text-align:right;font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums;}
|
||
|
||
.btn{border:none;border-radius:11px;padding:12px 18px;font-size:14px;font-weight:800;cursor:pointer;font-family:inherit;transition:.12s;}
|
||
.btn.primary{background:var(--accent);color:var(--ink);flex:1;} .btn.primary:hover{filter:brightness(1.08);}
|
||
.btn.ghost{background:var(--surface2);color:var(--fg);border:1px solid var(--border);} .btn.red{background:var(--red);color:#2a0910;} .btn.green{background:var(--green);color:#06231a;}
|
||
.btn:disabled{opacity:.4;cursor:not-allowed;} .btn:active{transform:scale(0.98);}
|
||
|
||
.overlay{position:absolute;inset:0;z-index:40;background:rgba(5,6,10,0.7);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;} .overlay.show{display:flex;}
|
||
.modal{width:360px;max-width:92%;background:rgba(18,20,28,0.98);border:1px solid var(--border);border-radius:17px;padding:22px;text-align:center;box-shadow:0 30px 70px -18px rgba(0,0,0,0.8);}
|
||
.modal .mic{font-size:38px;} .modal h3{margin:8px 0 4px;font-size:19px;} .modal p{color:var(--muted);font-size:13px;margin:0 0 18px;} .modal-actions{display:grid;grid-template-columns:1fr 1fr;gap:9px;}
|
||
.toast{position:absolute;bottom:20px;left:50%;transform:translateX(-50%) translateY(18px);background:rgba(12,14,20,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);}
|
||
</style>
|
||
|
||
<div class="stage">
|
||
<!-- AUSWAHL -->
|
||
<div class="view active" id="v-select"><div class="sel-wrap">
|
||
<div class="sel-list"><h1>Charaktere</h1><div class="sub">Wähle einen Charakter oder erstelle einen neuen.</div><div id="slot-list"></div></div>
|
||
<div class="sel-detail preview" id="sel-preview">
|
||
<div class="floor"></div>
|
||
<div id="sel-avatar"></div>
|
||
<div class="turnbar">
|
||
<button data-drehen="-30" title="Nach links drehen">◀</button>
|
||
<span class="hint">Drehen</span>
|
||
<button data-drehen="30" title="Nach rechts drehen">▶</button>
|
||
</div>
|
||
<div class="sel-info" id="sel-info"></div>
|
||
</div>
|
||
</div></div>
|
||
|
||
<!-- CREATOR -->
|
||
<div class="view" id="v-create"><div class="cr-wrap">
|
||
<div class="preview" id="cr-preview">
|
||
<div class="view-toggle" id="cr-viewtoggle"><button data-m="gesicht" class="active">Gesicht</button><button data-m="koerper">Körper</button></div>
|
||
<div class="floor"></div>
|
||
<div id="cr-avatar"></div>
|
||
<div class="turnbar">
|
||
<button data-drehen="-30" title="Nach links drehen">◀</button>
|
||
<span class="hint">Drehen</span>
|
||
<button data-drehen="30" title="Nach rechts drehen">▶</button>
|
||
</div>
|
||
</div>
|
||
<div class="cr-panel">
|
||
<div class="cr-head"><h1 id="cr-title">Charakter erstellen</h1><p>Gestalte Aussehen und Identität</p></div>
|
||
<div class="cr-tabs" id="cr-tabs"></div>
|
||
<div class="cr-body" id="cr-body"></div>
|
||
<div class="cr-foot"><button class="btn ghost" id="cr-cancel">Abbrechen</button><button class="btn primary" id="cr-save">Charakter erstellen</button></div>
|
||
</div>
|
||
</div></div>
|
||
|
||
<div class="overlay" id="overlay"></div><div class="toast" id="toast"></div>
|
||
</div>
|
||
|
||
<script>
|
||
"use strict";
|
||
let MAX_SLOTS=5; // wird vom Server aus Config.MaxCharaktere gesetzt
|
||
/* =====================================================================
|
||
PALETTEN & OPTIONEN
|
||
===================================================================== */
|
||
const hautToene=[{n:"Hell",c:"#f1cba6"},{n:"Rosé",c:"#e8b48f"},{n:"Gebräunt",c:"#d29b6f"},{n:"Oliv",c:"#b98552"},{n:"Braun",c:"#9a6a42"},{n:"Dunkelbraun",c:"#7a5232"},{n:"Tief",c:"#5c3d26"},{n:"Ebenholz",c:"#43291a"}];
|
||
const haarFarben=[{n:"Schwarz",c:"#1c1a1e"},{n:"Dunkelbraun",c:"#3a2417"},{n:"Braun",c:"#5a3a22"},{n:"Kastanie",c:"#7a3f2a"},{n:"Blond",c:"#c9a24a"},{n:"Hellblond",c:"#e3ca88"},{n:"Rot",c:"#a3441f"},{n:"Grau",c:"#9a9aa0"},{n:"Weiß",c:"#e8e8ec"},{n:"Petrol (gefärbt)",c:"#2f8f9a"}];
|
||
const augenFarben=[{n:"Braun",c:"#6b4423"},{n:"Haselnuss",c:"#8a6b3a"},{n:"Blau",c:"#3f7ac4"},{n:"Grün",c:"#4a8a5a"},{n:"Grau",c:"#7a8a95"},{n:"Bernstein",c:"#b07a2a"}];
|
||
const kleidFarben=[{n:"Schwarz",c:"#20222a"},{n:"Weiß",c:"#e6e8ee"},{n:"Grau",c:"#6b7180"},{n:"Navy",c:"#27324f"},{n:"Rot",c:"#b23a3a"},{n:"Grün",c:"#2f6b4a"},{n:"Blau",c:"#3563a8"},{n:"Beige",c:"#c7b18a"},{n:"Bordeaux",c:"#6b2740"},{n:"Oliv",c:"#5a5a32"}];
|
||
const haarStile=["Kurz","Mittel","Undercut","Lang","Zopf","Glatze"];
|
||
const bartStile=["Rasiert","Stoppeln","Kurzbart","Vollbart"];
|
||
const augenbrauen=["Dünn","Normal","Dicht"];
|
||
const herkuenfte=["Los Santos","Vice City","Liberty City","San Fierro","Europa","Südamerika","Asien","Afrika"];
|
||
|
||
/* Standard-Aussehen für einen neuen Charakter */
|
||
function standardAussehen(g){return {geschlecht:g||"männlich",skin:1,hairColor:1,hairStyle:g==="weiblich"?3:0,augen:0,bart:0,augenbraue:1,gesichtBreite:50,kiefer:g==="weiblich"?40:55,nase:50,lippen:g==="weiblich"?62:45,koerper:g==="weiblich"?42:55,top:3,hose:0,schuhe:0};}
|
||
|
||
/* =====================================================================
|
||
GESPEICHERTE CHARAKTERE (kommen vom Server per NUI)
|
||
===================================================================== */
|
||
let charaktere=[]; // wird beim Öffnen vom Server befüllt
|
||
let auswahlIndex=0; // aktuell markierter Slot in der Auswahl
|
||
let entwurf=null; // Arbeitskopie im Creator
|
||
let editIndex=null; // Index beim Bearbeiten (null = neu)
|
||
let crModus="gesicht"; // Vorschau-Modus im Creator
|
||
let crTab="identitaet"; // aktiver Reiter
|
||
|
||
/* =====================================================================
|
||
HELFER
|
||
===================================================================== */
|
||
const $=id=>document.getElementById(id);
|
||
function sicher(t){const d=document.createElement("div");d.textContent=t==null?"":t;return d.innerHTML;}
|
||
const euro=n=>n.toLocaleString("de-DE")+" €";
|
||
let toastTimer=null; function toast(t){const e=$("toast");e.textContent=t;e.classList.add("show");clearTimeout(toastTimer);toastTimer=setTimeout(()=>e.classList.remove("show"),2400);}
|
||
function modal(html){$("overlay").innerHTML=`<div class="modal">${html}</div>`;$("overlay").classList.add("show");}
|
||
function closeModal(){$("overlay").classList.remove("show");$("overlay").innerHTML="";}
|
||
/* Farbe abdunkeln/aufhellen (f<1 dunkler, >1 heller) */
|
||
function schattiere(hex,f){const n=parseInt(hex.slice(1),16);let r=(n>>16)&255,g=(n>>8)&255,b=n&255;r=Math.max(0,Math.min(255,Math.round(r*f)));g=Math.max(0,Math.min(255,Math.round(g*f)));b=Math.max(0,Math.min(255,Math.round(b*f)));return "#"+((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1);}
|
||
|
||
/* =====================================================================
|
||
AVATAR — parametrische Vektor-Darstellung (Gesicht & Körper)
|
||
===================================================================== */
|
||
function haarPfad(stil,cx,cy,fw,haar,haarD){
|
||
// gibt {hinten, vorne} Haar-SVG zurück
|
||
const oben=cy-66;
|
||
if(stil==="Glatze")return {hinten:"",vorne:`<path d="M ${cx-fw+4} ${cy-14} C ${cx-fw+2} ${cy-52} ${cx+fw-2} ${cy-52} ${cx+fw-4} ${cy-14} C ${cx+fw-16} ${cy-40} ${cx-fw+16} ${cy-40} ${cx-fw+4} ${cy-14} Z" fill="${schattiere(haar,1.05)}" opacity="0.12"/>`};
|
||
if(stil==="Lang"||stil==="Zopf"){
|
||
const hinten=`<path d="M ${cx-fw-6} ${cy-20} C ${cx-fw-14} ${cy+60} ${cx-fw-6} ${cy+110} ${cx-fw+10} ${cy+130} L ${cx+fw-10} ${cy+130} C ${cx+fw+6} ${cy+110} ${cx+fw+14} ${cy+60} ${cx+fw+6} ${cy-20} C ${cx+fw} ${oben} ${cx-fw} ${oben} ${cx-fw-6} ${cy-20} Z" fill="${haarD}"/>`;
|
||
const vorne=`<path d="M ${cx-fw-2} ${cy-6} C ${cx-fw-4} ${cy-58} ${cx+fw+4} ${cy-58} ${cx+fw+2} ${cy-6} C ${cx+fw-6} ${cy-30} ${cx+16} ${cy-40} ${cx+6} ${cy-24} C ${cx} ${cy-40} ${cx-16} ${cy-38} ${cx-fw+6} ${cy-30} Z" fill="${haar}"/>`+(stil==="Zopf"?`<ellipse cx="${cx}" cy="${cy+118}" rx="10" ry="16" fill="${haarD}"/>`:"");
|
||
return {hinten,vorne};
|
||
}
|
||
if(stil==="Undercut"){
|
||
return {hinten:"",vorne:`<path d="M ${cx-fw+6} ${cy-18} C ${cx-fw+2} ${cy-56} ${cx+fw-2} ${cy-56} ${cx+fw-6} ${cy-18} C ${cx+fw-8} ${cy-36} ${cx+8} ${cy-44} ${cx-4} ${cy-30} C ${cx-10} ${cy-40} ${cx-fw+4} ${cy-38} ${cx-fw+6} ${cy-18} Z" fill="${haar}"/>`};
|
||
}
|
||
if(stil==="Mittel"){
|
||
return {hinten:`<path d="M ${cx-fw-4} ${cy-12} C ${cx-fw-6} ${cy+30} ${cx-fw+2} ${cy+44} ${cx-fw+8} ${cy+48} L ${cx+fw-8} ${cy+48} C ${cx+fw-2} ${cy+44} ${cx+fw+6} ${cy+30} ${cx+fw+4} ${cy-12} C ${cx+fw} ${oben} ${cx-fw} ${oben} ${cx-fw-4} ${cy-12} Z" fill="${haarD}"/>`,
|
||
vorne:`<path d="M ${cx-fw} ${cy-8} C ${cx-fw-2} ${cy-56} ${cx+fw+2} ${cy-56} ${cx+fw} ${cy-8} C ${cx+fw-8} ${cy-32} ${cx+12} ${cy-42} ${cx+2} ${cy-26} C ${cx-4} ${cy-42} ${cx-12} ${cy-40} ${cx-fw+8} ${cy-30} Z" fill="${haar}"/>`};
|
||
}
|
||
// Kurz (Standard)
|
||
return {hinten:"",vorne:`<path d="M ${cx-fw+4} ${cy-16} C ${cx-fw+2} ${cy-54} ${cx+fw-2} ${cy-54} ${cx+fw-4} ${cy-16} C ${cx+fw-8} ${cy-36} ${cx+10} ${cy-42} ${cx} ${cy-28} C ${cx-10} ${cy-42} ${cx-fw+8} ${cy-36} ${cx-fw+4} ${cy-16} Z" fill="${haar}"/>`};
|
||
}
|
||
function bartPfad(stil,cx,cy,fw,jawW,chinY,haar){
|
||
if(stil==="Rasiert")return "";
|
||
const bC=schattiere(haar,0.92);
|
||
if(stil==="Stoppeln")return `<path d="M ${cx-jawW+2} ${cy+30} C ${cx-jawW} ${chinY-14} ${cx} ${chinY} ${cx} ${chinY} C ${cx} ${chinY} ${cx+jawW} ${chinY-14} ${cx+jawW-2} ${cy+30} C ${cx+jawW-10} ${cy+52} ${cx-jawW+10} ${cy+52} ${cx-jawW+2} ${cy+30} Z" fill="${bC}" opacity="0.28"/>`;
|
||
if(stil==="Kurzbart")return `<path d="M ${cx-jawW+3} ${cy+26} C ${cx-jawW+1} ${chinY-12} ${cx} ${chinY} ${cx} ${chinY} C ${cx} ${chinY} ${cx+jawW-1} ${chinY-12} ${cx+jawW-3} ${cy+26} C ${cx+jawW-12} ${cy+50} ${cx-jawW+12} ${cy+50} ${cx-jawW+3} ${cy+26} Z" fill="${bC}" opacity="0.7"/><rect x="${cx-11}" y="${cy+14}" width="22" height="6" rx="3" fill="${bC}" opacity="0.7"/>`;
|
||
// Vollbart
|
||
return `<path d="M ${cx-jawW+1} ${cy+20} C ${cx-jawW-2} ${chinY-6} ${cx} ${chinY+6} ${cx} ${chinY+6} C ${cx} ${chinY+6} ${cx+jawW+2} ${chinY-6} ${cx+jawW-1} ${cy+20} C ${cx+jawW-6} ${cy+56} ${cx-jawW+6} ${cy+56} ${cx-jawW+1} ${cy+20} Z" fill="${bC}"/><rect x="${cx-12}" y="${cy+12}" width="24" height="7" rx="3" fill="${bC}"/>`;
|
||
}
|
||
function faceSvg(a){
|
||
const skin=hautToene[a.skin].c, skinD=schattiere(skin,0.86), skinL=schattiere(skin,1.07);
|
||
const haar=haarFarben[a.hairColor].c, haarD=schattiere(haar,0.8);
|
||
const auge=augenFarben[a.augen].c, brow=schattiere(haar,0.85), lip=schattiere(skin,0.7);
|
||
const topC=kleidFarben[a.top].c;
|
||
const cx=110, cy=104, chinY=176;
|
||
const fw=38+a.gesichtBreite/100*16; // Gesichts-Halbbreite
|
||
const jawW=fw*(0.56+a.kiefer/100*0.34); // Kieferbreite
|
||
const noseW=5+a.nase/100*8;
|
||
const lipW=13+a.lippen/100*10, lipY=chinY-30;
|
||
const browT=2.4+a.augenbraue/100*3.2, eyeY=cy+2, eyeDx=18;
|
||
const kopf=`M ${cx-fw} ${cy} C ${cx-fw} ${cy-74} ${cx+fw} ${cy-74} ${cx+fw} ${cy} C ${cx+fw} ${cy+38} ${cx+jawW} ${chinY-18} ${cx} ${chinY} C ${cx-jawW} ${chinY-18} ${cx-fw} ${cy+38} ${cx-fw} ${cy} Z`;
|
||
const haare=haarPfad(a.hairStyle,cx,cy,fw,haar,haarD);
|
||
const bart=bartPfad(a.bart,cx,cy,fw,jawW,chinY,haar);
|
||
return `<svg viewBox="0 0 220 260" class="${crModus==='gesicht'?'':''}"><defs>
|
||
<linearGradient id="skinG" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="${skinL}"/><stop offset="60%" stop-color="${skin}"/><stop offset="100%" stop-color="${skinD}"/></linearGradient></defs>
|
||
<!-- Schultern / Oberteil -->
|
||
<path d="M ${cx-96} 260 C ${cx-96} 214 ${cx-40} 196 ${cx} 196 C ${cx+40} 196 ${cx+96} 214 ${cx+96} 260 Z" fill="${topC}"/>
|
||
<path d="M ${cx-14} 196 h28 v-24 h-28 Z" fill="${skinD}"/>
|
||
${haare.hinten}
|
||
<!-- Ohren -->
|
||
<ellipse cx="${cx-fw+2}" cy="${cy+8}" rx="8" ry="12" fill="${skinD}"/><ellipse cx="${cx+fw-2}" cy="${cy+8}" rx="8" ry="12" fill="${skinD}"/>
|
||
<!-- Kopf -->
|
||
<path d="${kopf}" fill="url(#skinG)"/>
|
||
<!-- Augenbrauen -->
|
||
<path d="M ${cx-eyeDx-9} ${eyeY-13} q 9 -${browT+3} 18 0" stroke="${brow}" stroke-width="${browT}" fill="none" stroke-linecap="round"/>
|
||
<path d="M ${cx+eyeDx-9} ${eyeY-13} q 9 -${browT+3} 18 0" stroke="${brow}" stroke-width="${browT}" fill="none" stroke-linecap="round"/>
|
||
<!-- Augen -->
|
||
<g><ellipse cx="${cx-eyeDx}" cy="${eyeY}" rx="8" ry="5" fill="#fff"/><circle cx="${cx-eyeDx}" cy="${eyeY}" r="3.6" fill="${auge}"/><circle cx="${cx-eyeDx}" cy="${eyeY}" r="1.7" fill="#12100e"/>
|
||
<ellipse cx="${cx+eyeDx}" cy="${eyeY}" rx="8" ry="5" fill="#fff"/><circle cx="${cx+eyeDx}" cy="${eyeY}" r="3.6" fill="${auge}"/><circle cx="${cx+eyeDx}" cy="${eyeY}" r="1.7" fill="#12100e"/>
|
||
<path d="M ${cx-eyeDx-8} ${eyeY-3} q 8 -4 16 0" stroke="${skinD}" stroke-width="1.4" fill="none"/><path d="M ${cx+eyeDx-8} ${eyeY-3} q 8 -4 16 0" stroke="${skinD}" stroke-width="1.4" fill="none"/></g>
|
||
<!-- Nase -->
|
||
<path d="M ${cx} ${eyeY+4} C ${cx-2} ${eyeY+18} ${cx-noseW} ${eyeY+26} ${cx-noseW+1} ${eyeY+30} q ${noseW-1} 5 ${2*(noseW-1)} 0 C ${cx+noseW} ${eyeY+26} ${cx+2} ${eyeY+18} ${cx} ${eyeY+4}" fill="${schattiere(skin,0.93)}"/>
|
||
<!-- Lippen -->
|
||
<path d="M ${cx-lipW/2} ${lipY} q ${lipW/2} -4 ${lipW} 0 q -${lipW/2} 8 -${lipW} 0 Z" fill="${lip}"/><path d="M ${cx-lipW/2} ${lipY} q ${lipW/2} 3 ${lipW} 0" stroke="${schattiere(lip,0.7)}" stroke-width="1" fill="none"/>
|
||
${bart}
|
||
${haare.vorne}
|
||
</svg>`;
|
||
}
|
||
function bodySvg(a){
|
||
const skin=hautToene[a.skin].c, skinD=schattiere(skin,0.86);
|
||
const haar=haarFarben[a.hairColor].c;
|
||
const topC=kleidFarben[a.top].c, hoseC=kleidFarben[a.hose].c, schuhC=kleidFarben[a.schuhe].c;
|
||
const cx=110, torsoW=30+a.koerper/100*20; // Rumpfbreite je Körperbau
|
||
return `<svg viewBox="0 0 220 330"><g class="turn">
|
||
<!-- Kopf + Haare -->
|
||
<ellipse cx="${cx}" cy="42" rx="21" ry="24" fill="${skin}"/>
|
||
<path d="M ${cx-21} 34 C ${cx-22} 8 ${cx+22} 8 ${cx+21} 34 C ${cx+14} 20 ${cx-14} 20 ${cx-21} 34 Z" fill="${haar}"/>
|
||
<rect x="${cx-8}" y="62" width="16" height="14" fill="${skinD}"/>
|
||
<!-- Oberkörper (Oberteil) -->
|
||
<path d="M ${cx-torsoW} 78 Q ${cx} 70 ${cx+torsoW} 78 L ${cx+torsoW-4} 176 Q ${cx} 186 ${cx-torsoW+4} 176 Z" fill="${topC}"/>
|
||
<!-- Arme -->
|
||
<path d="M ${cx-torsoW} 80 q -18 6 -22 54 l 10 4 q 6 -40 16 -50 Z" fill="${topC}"/><path d="M ${cx+torsoW} 80 q 18 6 22 54 l -10 4 q -6 -40 -16 -50 Z" fill="${topC}"/>
|
||
<ellipse cx="${cx-torsoW-14}" cy="140" rx="6" ry="8" fill="${skin}"/><ellipse cx="${cx+torsoW+14}" cy="140" rx="6" ry="8" fill="${skin}"/>
|
||
<!-- Beine (Hose) -->
|
||
<path d="M ${cx-torsoW+6} 174 L ${cx-6} 176 L ${cx-8} 288 L ${cx-24} 288 Z" fill="${hoseC}"/><path d="M ${cx+torsoW-6} 174 L ${cx+6} 176 L ${cx+8} 288 L ${cx+24} 288 Z" fill="${hoseC}"/>
|
||
<!-- Schuhe -->
|
||
<path d="M ${cx-26} 288 h20 l2 14 h-30 Z" fill="${schuhC}"/><path d="M ${cx+26} 288 h-20 l-2 14 h30 Z" fill="${schuhC}"/>
|
||
</g></svg>`;
|
||
}
|
||
function avatarSvg(a,modus){return modus==="koerper"?bodySvg(a):faceSvg(a);}
|
||
|
||
/* =====================================================================
|
||
AUSWAHL-BILDSCHIRM
|
||
===================================================================== */
|
||
function renderAuswahl(){
|
||
$("v-create").classList.remove("active");$("v-select").classList.add("active","fade");setTimeout(()=>$("v-select").classList.remove("fade"),350);
|
||
const list=$("slot-list");
|
||
let html=charaktere.map((c,i)=>`<div class="char-slot ${i===auswahlIndex?'active':''}" data-slot="${i}">
|
||
<div class="thumb">${slotThumb(c)}</div><div class="grow"><div class="cn">${sicher(c.vorname+' '+c.nachname)}</div><div class="cm">${sicher(c.job)} · ${c.stunden} Std</div></div></div>`).join("");
|
||
if(charaktere.length<MAX_SLOTS)html+=`<div class="char-slot empty" data-neu="1"><span class="plus">+</span> Neuen Charakter erstellen</div>`;
|
||
list.innerHTML=html;
|
||
list.querySelectorAll("[data-slot]").forEach(s=>s.addEventListener("click",()=>{auswahlIndex=+s.dataset.slot;renderAuswahl();}));
|
||
const neu=list.querySelector("[data-neu]");if(neu)neu.addEventListener("click",()=>starteCreator(null));
|
||
renderAuswahlDetail();
|
||
}
|
||
/* Kleines Vorschaubild im Slot.
|
||
In FiveM Initialen (passt besser zum realistischen 3D-Ped),
|
||
im Browser die stilisierte SVG-Zeichnung. */
|
||
function slotThumb(c){
|
||
if(!IST_FIVEM) return faceSvg(c.aussehen);
|
||
const initialen=((c.vorname||"?")[0]+(c.nachname||"?")[0]).toUpperCase();
|
||
return `<span class="ini">${sicher(initialen)}</span>`;
|
||
}
|
||
|
||
function renderAuswahlDetail(){
|
||
const c=charaktere[auswahlIndex];
|
||
if(!c){$("sel-avatar").innerHTML="";$("sel-info").innerHTML='<div class="empty">Kein Charakter.</div>';return;}
|
||
// 3D-Ped auf den markierten Charakter umstellen (bzw. SVG im Browser)
|
||
if(IST_FIVEM){ $("sel-avatar").innerHTML=""; nui("vorschau",{aussehen:c.aussehen,modus:"koerper"}); }
|
||
else $("sel-avatar").innerHTML=faceSvg(c.aussehen);
|
||
$("sel-info").innerHTML=`<h2>${sicher(c.vorname+' '+c.nachname)}</h2><div class="role">${sicher(c.job)} · ${sicher(c.herkunft)}</div>
|
||
<div class="sel-stats"><div class="st"><div class="l">Vermögen</div><div class="v">${euro(c.geld)}</div></div>
|
||
<div class="st"><div class="l">Spielzeit</div><div class="v">${c.stunden} Std</div></div>
|
||
<div class="st"><div class="l">Geboren</div><div class="v">${sicher(c.geburt)}</div></div>
|
||
<div class="st"><div class="l">Zuletzt</div><div class="v">${sicher(c.zuletzt)}</div></div></div>
|
||
<div class="sel-actions"><button class="btn primary" id="d-play">▶ Spielen</button><button class="btn red" id="d-del">Löschen</button></div>`;
|
||
$("d-play").addEventListener("click",()=>{
|
||
// Auswahl an den Server melden — dieser spawnt den Charakter
|
||
$("d-play").disabled=true;
|
||
toast("Lade "+c.vorname+" ...");
|
||
nui("spielen",{charakterId:c.id});
|
||
});
|
||
$("d-del").addEventListener("click",()=>loeschenFrage(auswahlIndex));
|
||
}
|
||
function loeschenFrage(i){const c=charaktere[i];
|
||
modal(`<div class="mic">⚠️</div><h3>${sicher(c.vorname+' '+c.nachname)} löschen?</h3><p>Dieser Charakter wird unwiderruflich entfernt.</p>
|
||
<div class="modal-actions"><button class="btn ghost" id="ld-cancel">Abbrechen</button><button class="btn red" id="ld-ok">Löschen</button></div>`);
|
||
$("ld-cancel").addEventListener("click",closeModal);
|
||
$("ld-ok").addEventListener("click",()=>{
|
||
// Löschen übernimmt der Server; die neue Liste kommt per Nachricht zurück
|
||
closeModal();
|
||
nui("loeschen",{charakterId:c.id});
|
||
});
|
||
}
|
||
|
||
/* =====================================================================
|
||
CREATOR
|
||
===================================================================== */
|
||
function starteCreator(index){
|
||
editIndex=index;
|
||
if(index===null){entwurf={vorname:"",nachname:"",geschlecht:"männlich",geburt:"",herkunft:herkuenfte[0],aussehen:standardAussehen("männlich")};}
|
||
else{const c=charaktere[index];entwurf={vorname:c.vorname,nachname:c.nachname,geschlecht:c.geschlecht,geburt:c.geburt,herkunft:c.herkunft,aussehen:{...c.aussehen}};}
|
||
crModus="gesicht";crTab="identitaet";
|
||
$("v-select").classList.remove("active");$("v-create").classList.add("active","fade");setTimeout(()=>$("v-create").classList.remove("fade"),350);
|
||
$("cr-title").textContent=index===null?"Charakter erstellen":"Charakter bearbeiten";
|
||
$("cr-save").textContent=index===null?"Charakter erstellen":"Änderungen speichern";
|
||
document.querySelectorAll("#cr-viewtoggle button").forEach(b=>b.classList.toggle("active",b.dataset.m===crModus));
|
||
renderTabs();renderControls();
|
||
// Vorschau auf den Entwurf umstellen und ins Gesicht zoomen
|
||
if(IST_FIVEM) nui("vorschau",{aussehen:entwurf.aussehen,modus:"gesicht"});
|
||
else updatePreview();
|
||
}
|
||
/* Vorschau aktualisieren.
|
||
In FiveM: Aussehen an den Client schicken, der es live auf den 3D-Ped überträgt.
|
||
Im Browser: die stilisierte SVG-Vorschau als Fallback zeichnen. */
|
||
let vorschauTimer=null;
|
||
function updatePreview(){
|
||
if(!IST_FIVEM){ $("cr-avatar").innerHTML=avatarSvg(entwurf.aussehen,crModus); return; }
|
||
// Slider feuern sehr oft — Aufrufe kurz bündeln
|
||
clearTimeout(vorschauTimer);
|
||
vorschauTimer=setTimeout(()=>nui("vorschau",{aussehen:entwurf.aussehen}),60);
|
||
}
|
||
const crTabsDef=[["identitaet","Identität"],["gesicht","Gesicht"],["merkmale","Merkmale"],["haare","Haare & Bart"],["kleidung","Kleidung"]];
|
||
function renderTabs(){
|
||
$("cr-tabs").innerHTML=crTabsDef.map(t=>`<button data-t="${t[0]}" class="${t[0]===crTab?'active':''}">${t[1]}</button>`).join("");
|
||
$("cr-tabs").querySelectorAll("[data-t]").forEach(b=>b.addEventListener("click",()=>{crTab=b.dataset.t;if(crTab==="kleidung")setModus("koerper");else if(crModus==="koerper"&&crTab!=="merkmale")setModus("gesicht");renderTabs();renderControls();}));
|
||
}
|
||
function setModus(m){
|
||
crModus=m;
|
||
document.querySelectorAll("#cr-viewtoggle button").forEach(b=>b.classList.toggle("active",b.dataset.m===m));
|
||
if(IST_FIVEM) nui("vorschauModus",{modus:m==="koerper"?"koerper":"gesicht"});
|
||
else updatePreview();
|
||
}
|
||
|
||
/* Bausteine für Controls */
|
||
function swGroup(label,pal,curr,cb){const id="g"+Math.random().toString(36).slice(2,7);
|
||
setTimeout(()=>{const el=$(id);if(el)el.querySelectorAll("[data-i]").forEach(s=>s.addEventListener("click",()=>{el.querySelectorAll("[data-i]").forEach(x=>x.classList.remove("sel"));s.classList.add("sel");cb(+s.dataset.i);}));},0);
|
||
return `<label class="flabel">${label}</label><div class="swatches" id="${id}">${pal.map((p,i)=>`<div class="sw ${i===curr?'sel':''}" data-i="${i}" style="background:${p.c}" title="${sicher(p.n)}"></div>`).join("")}</div>`;
|
||
}
|
||
function optGroup(label,opts,curr,cb){const id="o"+Math.random().toString(36).slice(2,7);
|
||
setTimeout(()=>{const el=$(id);if(el)el.querySelectorAll("[data-i]").forEach(s=>s.addEventListener("click",()=>{el.querySelectorAll("[data-i]").forEach(x=>x.classList.remove("sel"));s.classList.add("sel");cb(+s.dataset.i);}));},0);
|
||
return `<label class="flabel">${label}</label><div class="opts" id="${id}">${opts.map((o,i)=>`<button class="opt ${i===curr?'sel':''}" data-i="${i}">${sicher(o)}</button>`).join("")}</div>`;
|
||
}
|
||
function sliderRow(label,val,cb){const id="s"+Math.random().toString(36).slice(2,7);
|
||
setTimeout(()=>{const el=$(id);if(el)el.addEventListener("input",()=>{$(id+"v").textContent=el.value;cb(+el.value);});},0);
|
||
return `<div class="slider"><span class="sl-l">${label}</span><input type="range" id="${id}" min="0" max="100" value="${val}"><span class="sl-v" id="${id}v">${val}</span></div>`;
|
||
}
|
||
|
||
function renderControls(){
|
||
const a=entwurf.aussehen, body=$("cr-body");
|
||
if(crTab==="identitaet"){
|
||
body.innerHTML=`<label class="flabel">Vorname</label><input class="input" id="i-vor" value="${sicher(entwurf.vorname)}" placeholder="Vorname">
|
||
<label class="flabel">Nachname</label><input class="input" id="i-nach" value="${sicher(entwurf.nachname)}" placeholder="Nachname">
|
||
<label class="flabel">Geschlecht</label><div class="opts" id="i-geschlecht">${["männlich","weiblich","divers"].map(g=>`<button class="opt ${entwurf.geschlecht===g?'sel':''}" data-g="${g}">${g}</button>`).join("")}</div>
|
||
<label class="flabel">Geburtsdatum</label><input class="input" id="i-geb" value="${sicher(entwurf.geburt)}" placeholder="TT.MM.JJJJ">
|
||
<label class="flabel">Herkunft</label><select class="input" id="i-herk">${herkuenfte.map(h=>`<option ${entwurf.herkunft===h?'selected':''}>${sicher(h)}</option>`).join("")}</select>`;
|
||
$("i-vor").addEventListener("input",e=>entwurf.vorname=e.target.value);
|
||
$("i-nach").addEventListener("input",e=>entwurf.nachname=e.target.value);
|
||
$("i-geb").addEventListener("input",e=>entwurf.geburt=e.target.value);
|
||
$("i-herk").addEventListener("change",e=>entwurf.herkunft=e.target.value);
|
||
$("i-geschlecht").querySelectorAll("[data-g]").forEach(b=>b.addEventListener("click",()=>{entwurf.geschlecht=b.dataset.g;a.geschlecht=b.dataset.g;$("i-geschlecht").querySelectorAll("[data-g]").forEach(x=>x.classList.remove("sel"));b.classList.add("sel");}));
|
||
}
|
||
else if(crTab==="gesicht"){
|
||
body.innerHTML=swGroup("Hautton",hautToene,a.skin,i=>{a.skin=i;updatePreview();})+swGroup("Augenfarbe",augenFarben,a.augen,i=>{a.augen=i;updatePreview();});
|
||
}
|
||
else if(crTab==="merkmale"){
|
||
body.innerHTML=`<div style="font-size:12px;color:var(--muted);margin-bottom:10px">Feinjustierung der Gesichtszüge & Statur.</div>`
|
||
+sliderRow("Gesichtsbreite",a.gesichtBreite,v=>{a.gesichtBreite=v;updatePreview();})
|
||
+sliderRow("Kiefer",a.kiefer,v=>{a.kiefer=v;updatePreview();})
|
||
+sliderRow("Nase",a.nase,v=>{a.nase=v;updatePreview();})
|
||
+sliderRow("Lippen",a.lippen,v=>{a.lippen=v;updatePreview();})
|
||
+sliderRow("Körperbau",a.koerper,v=>{a.koerper=v;if(crModus!=='koerper')setModus('koerper');else updatePreview();});
|
||
}
|
||
else if(crTab==="haare"){
|
||
body.innerHTML=optGroup("Frisur",haarStile,a.hairStyle,i=>{a.hairStyle=i;updatePreview();})
|
||
+swGroup("Haarfarbe",haarFarben,a.hairColor,i=>{a.hairColor=i;updatePreview();})
|
||
+optGroup("Augenbrauen",augenbrauen,Math.round(a.augenbraue/50),i=>{a.augenbraue=i*50;updatePreview();})
|
||
+optGroup("Bart",bartStile,a.bart,i=>{a.bart=i;updatePreview();});
|
||
}
|
||
else if(crTab==="kleidung"){
|
||
body.innerHTML=swGroup("Oberteil",kleidFarben,a.top,i=>{a.top=i;updatePreview();})
|
||
+swGroup("Hose",kleidFarben,a.hose,i=>{a.hose=i;updatePreview();})
|
||
+swGroup("Schuhe",kleidFarben,a.schuhe,i=>{a.schuhe=i;updatePreview();});
|
||
}
|
||
}
|
||
|
||
/* Speichern */
|
||
function speichereCharakter(){
|
||
if(!entwurf.vorname.trim()||!entwurf.nachname.trim()){crTab="identitaet";renderTabs();renderControls();toast("Bitte Vor- und Nachnamen angeben");return;}
|
||
if(!entwurf.geburt.trim()){crTab="identitaet";renderTabs();renderControls();toast("Bitte Geburtsdatum angeben (TT.MM.JJJJ)");return;}
|
||
entwurf.aussehen.geschlecht=entwurf.geschlecht;
|
||
// Anlegen übernimmt der Server; die neue Liste kommt per Nachricht zurück
|
||
$("cr-save").disabled=true;
|
||
nui("erstellen",{
|
||
vorname:entwurf.vorname.trim(),
|
||
nachname:entwurf.nachname.trim(),
|
||
geschlecht:entwurf.geschlecht,
|
||
geburt:entwurf.geburt.trim(),
|
||
herkunft:entwurf.herkunft,
|
||
aussehen:entwurf.aussehen,
|
||
});
|
||
}
|
||
|
||
/* Events */
|
||
document.querySelectorAll("#cr-viewtoggle button").forEach(b=>b.addEventListener("click",()=>setModus(b.dataset.m)));
|
||
/* Drehknöpfe unter dem Ped (beide Ansichten) */
|
||
document.querySelectorAll("[data-drehen]").forEach(b=>
|
||
b.addEventListener("click",()=>nui("vorschauDrehen",{grad:+b.dataset.drehen})));
|
||
$("cr-cancel").addEventListener("click",renderAuswahl);
|
||
$("cr-save").addEventListener("click",speichereCharakter);
|
||
$("overlay").addEventListener("click",e=>{if(e.target===$("overlay"))closeModal();});
|
||
|
||
/* =====================================================================
|
||
NUI-BRÜCKE — Verbindung zum FiveM-Client
|
||
===================================================================== */
|
||
/* Läuft die Seite in FiveM (3D-Ped) oder eigenständig im Browser (SVG-Fallback)? */
|
||
const IST_FIVEM = (typeof GetParentResourceName === "function");
|
||
|
||
/* Name dieser Ressource (für die Callback-Adresse) */
|
||
const RESSOURCE = IST_FIVEM ? GetParentResourceName() : "aether-character";
|
||
|
||
/* Schickt eine Aktion an den Client (client/main.lua) */
|
||
function nui(aktion, daten){
|
||
return fetch(`https://${RESSOURCE}/${aktion}`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json; charset=UTF-8" },
|
||
body: JSON.stringify(daten || {}),
|
||
}).catch(() => {}); // Im Browser (ohne FiveM) still fehlschlagen lassen
|
||
}
|
||
|
||
/* Übernimmt eine Charakterliste vom Server und zeigt die Auswahl */
|
||
function setzeCharaktere(liste, maxSlots){
|
||
charaktere = Array.isArray(liste) ? liste : [];
|
||
if (maxSlots) MAX_SLOTS = maxSlots;
|
||
auswahlIndex = Math.max(0, Math.min(auswahlIndex, charaktere.length - 1));
|
||
// Noch kein Charakter vorhanden? Dann direkt in den Ersteller springen
|
||
if (charaktere.length === 0) starteCreator(null);
|
||
else renderAuswahl();
|
||
}
|
||
|
||
/* Nachrichten vom Client entgegennehmen */
|
||
window.addEventListener("message", (ereignis) => {
|
||
const nachricht = ereignis.data || {};
|
||
if (nachricht.aktion === "oeffnen") {
|
||
document.body.style.display = "";
|
||
setzeCharaktere(nachricht.charaktere, nachricht.maxSlots);
|
||
}
|
||
else if (nachricht.aktion === "liste") {
|
||
// Aktualisierte Liste nach Erstellen/Löschen
|
||
const saveBtn = $("cr-save"); if (saveBtn) saveBtn.disabled = false;
|
||
setzeCharaktere(nachricht.charaktere, nachricht.maxSlots);
|
||
if (nachricht.hinweis) toast(nachricht.hinweis);
|
||
}
|
||
else if (nachricht.aktion === "schliessen") {
|
||
document.body.style.display = "none";
|
||
}
|
||
else if (nachricht.aktion === "fehler") {
|
||
// Serverseitige Ablehnung: Bedienelemente wieder freigeben
|
||
const saveBtn = $("cr-save"); if (saveBtn) saveBtn.disabled = false;
|
||
const playBtn = $("d-play"); if (playBtn) playBtn.disabled = false;
|
||
toast(nachricht.text || "Aktion fehlgeschlagen");
|
||
}
|
||
});
|
||
|
||
/* START — im Browser direkt anzeigen, in FiveM wartet die Seite auf "oeffnen" */
|
||
if (IST_FIVEM) {
|
||
document.body.style.display = "none"; // In FiveM zunächst verstecken
|
||
} else {
|
||
// Eigenständige Ansicht: Kasten-Layout und SVG-Vorschau aktivieren
|
||
document.body.classList.add("browser");
|
||
// Eigenständige Vorschau im Browser: Beispieldaten zum Ansehen des Layouts
|
||
charaktere = [
|
||
{id:1,vorname:"Max",nachname:"Weber",geschlecht:"männlich",geburt:"14.03.1996",herkunft:"Los Santos",
|
||
job:"Spediteur",geld:48320,stunden:142,zuletzt:"vor 2 Std",
|
||
aussehen:{geschlecht:"männlich",skin:1,hairColor:2,hairStyle:0,augen:2,bart:1,augenbraue:1,
|
||
gesichtBreite:55,kiefer:60,nase:52,lippen:45,koerper:58,top:3,hose:0,schuhe:0}},
|
||
];
|
||
renderAuswahl();
|
||
}
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|