- 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>
485 lines
25 KiB
HTML
485 lines
25 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="color-scheme" content="dark">
|
||
<title>Aether — HUD</title>
|
||
<style>
|
||
:root{
|
||
--fg:#f2f0fa; /* Standard-Textfarbe */
|
||
--muted:#a09bbd; /* gedämpfter Text */
|
||
--panel:rgba(14,12,24,0.62); /* halbtransparente HUD-Fläche */
|
||
--panel2:rgba(20,17,34,0.9); /* dichtere Panel-Fläche */
|
||
--border:rgba(255,255,255,0.1);/* dezente Rahmen */
|
||
--accent:#7c5cf6; /* Aether-Akzent (Violett) */
|
||
--accent2:#5b8cff; /* zweiter Akzent (Blau) */
|
||
/* Statusfarben je Anzeige */
|
||
--health:#34d39a; --armor:#5b8cff; --hunger:#f5a742;
|
||
--thirst:#3fc7e0; --stamina:#c3e04a; --voice:#b58cff; --fuel:#f5a742;
|
||
}
|
||
*{box-sizing:border-box;}
|
||
html,body{margin:0;height:100%;overflow:hidden;background:transparent;
|
||
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--fg);}
|
||
|
||
/* In FiveM ist das HUD ein reines Overlay: Kulisse und Demo-Panel entfallen. */
|
||
body.ingame .scene{display:none;}
|
||
body.ingame .demo{display:none;}
|
||
/* Sanftes Ein-/Ausblenden des gesamten HUDs (z. B. bei offenen Menüs) */
|
||
.hud{transition:opacity .2s;}
|
||
body.hud-aus .hud{opacity:0;pointer-events:none;}
|
||
|
||
/* ============================================================
|
||
STILISIERTE SPIELSZENE (nur Kulisse, damit das HUD als Overlay wirkt)
|
||
============================================================ */
|
||
.scene{position:fixed;inset:0;z-index:0;overflow:hidden;
|
||
background:linear-gradient(180deg,#1a1440 0%,#3a2a5e 32%,#7a4a6a 52%,#c98a5a 66%,#e0a86a 74%,#2a2038 74%);}
|
||
/* Sonnen-/Lichtschein am Horizont */
|
||
.scene .sun{position:absolute;left:50%;top:60%;width:340px;height:340px;transform:translate(-50%,-50%);
|
||
border-radius:50%;background:radial-gradient(circle,rgba(255,220,150,0.9),rgba(255,180,120,0.2) 45%,transparent 70%);}
|
||
/* Häuser-Silhouette im Vordergrund */
|
||
.scene .sky{position:absolute;inset:0;background:radial-gradient(1px 1px at 20% 15%,rgba(255,255,255,0.7),transparent),
|
||
radial-gradient(1px 1px at 70% 8%,rgba(255,255,255,0.5),transparent),
|
||
radial-gradient(1px 1px at 45% 22%,rgba(255,255,255,0.6),transparent),
|
||
radial-gradient(1px 1px at 85% 18%,rgba(255,255,255,0.5),transparent);}
|
||
.scene .city{position:absolute;left:0;right:0;bottom:26%;height:34%;
|
||
background:
|
||
linear-gradient(90deg,transparent 4%,#241a38 4% 9%,transparent 9% 13%,#2c2044 13% 20%,transparent 20% 24%,
|
||
#201830 24% 30%,transparent 30% 36%,#2c2044 36% 41%,transparent 41% 47%,#281c40 47% 55%,transparent 55% 60%,
|
||
#241a38 60% 67%,transparent 67% 72%,#2c2044 72% 80%,transparent 80% 85%,#201830 85% 92%,transparent 92%);
|
||
filter:drop-shadow(0 -2px 0 rgba(124,92,246,0.25));}
|
||
.scene .ground{position:absolute;left:0;right:0;bottom:0;height:26%;background:linear-gradient(180deg,#1a1428,#0c0a16);}
|
||
.scene .road{position:absolute;left:50%;bottom:0;width:60%;height:26%;transform:translateX(-50%);
|
||
background:linear-gradient(180deg,#2a2438,#151020);clip-path:polygon(38% 0,62% 0,100% 100%,0 100%);}
|
||
.scene .dash{position:absolute;left:50%;bottom:0;width:4px;height:26%;transform:translateX(-50%);
|
||
background:repeating-linear-gradient(180deg,rgba(245,167,66,0.8) 0 24px,transparent 24px 48px);}
|
||
|
||
/* ============================================================
|
||
HUD-OVERLAY-CONTAINER
|
||
============================================================ */
|
||
.hud{position:fixed;inset:0;z-index:2;pointer-events:none;} /* Klicks gehen durch, außer bei Bedienelementen */
|
||
.hud > *{pointer-events:auto;}
|
||
|
||
/* Voice-Anzeige oben mittig */
|
||
.voice{position:absolute;top:20px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:10px;
|
||
background:var(--panel);border:1px solid var(--border);border-radius:999px;padding:7px 15px;backdrop-filter:blur(10px);}
|
||
.voice .mic{font-size:16px;transition:.15s;}
|
||
.voice.talking .mic{color:var(--voice);text-shadow:0 0 12px var(--voice);}
|
||
.voice .bars{display:flex;align-items:flex-end;gap:3px;height:16px;}
|
||
.voice .bars i{width:4px;background:var(--border);border-radius:2px;transition:.15s;}
|
||
.voice .bars i.on{background:var(--voice);}
|
||
.voice .range{font-size:11.5px;font-weight:800;color:var(--muted);letter-spacing:0.03em;}
|
||
.voice.talking .range{color:var(--fg);}
|
||
|
||
/* Status-Cluster unten links */
|
||
.cluster{position:absolute;left:22px;bottom:22px;display:flex;flex-direction:column;gap:12px;align-items:flex-start;}
|
||
.gauges{display:flex;gap:9px;flex-wrap:wrap;max-width:340px;}
|
||
.gauge{width:44px;height:44px;position:relative;}
|
||
.gauge svg{transform:rotate(-90deg);} /* Startpunkt oben */
|
||
.gauge .track{stroke:rgba(255,255,255,0.12);}
|
||
.gauge .icon{position:absolute;inset:0;display:grid;place-items:center;font-size:16px;}
|
||
.gauge .num{position:absolute;left:50%;bottom:-14px;transform:translateX(-50%);font-size:9.5px;font-weight:800;
|
||
color:var(--muted);font-variant-numeric:tabular-nums;}
|
||
|
||
/* Geld-Anzeige unten rechts */
|
||
.money{position:absolute;right:22px;bottom:22px;display:flex;flex-direction:column;gap:7px;align-items:flex-end;}
|
||
.money .row{display:flex;align-items:center;gap:9px;background:var(--panel);border:1px solid var(--border);
|
||
border-radius:11px;padding:7px 13px;backdrop-filter:blur(10px);min-width:160px;justify-content:flex-end;}
|
||
.money .row .mi{font-size:14px;}
|
||
.money .row .ml{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:0.07em;font-weight:800;margin-right:auto;}
|
||
.money .row .mv{font-size:16px;font-weight:800;font-variant-numeric:tabular-nums;}
|
||
.money .cash .mv{color:var(--health);}
|
||
.money .bank .mv{color:var(--accent2);}
|
||
.money .dirty .mv{color:#f5a742;}
|
||
.money .row.flash{animation:moneyFlash .6s ease;}
|
||
@keyframes moneyFlash{0%{transform:scale(1);}30%{transform:scale(1.06);border-color:var(--accent);}100%{transform:scale(1);}}
|
||
|
||
/* Fahrzeug-HUD unten rechts (über der Geld-Anzeige) */
|
||
.vehicle{position:absolute;right:22px;bottom:130px;display:none;align-items:flex-end;gap:14px;}
|
||
.vehicle.on{display:flex;}
|
||
.speedo{width:150px;height:150px;position:relative;}
|
||
.speedo svg{transform:rotate(135deg);} /* 270°-Skala, unten offen */
|
||
.speedo .big{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;}
|
||
.speedo .big .n{font-size:38px;font-weight:900;line-height:1;font-variant-numeric:tabular-nums;}
|
||
.speedo .big .u{font-size:11px;color:var(--muted);font-weight:800;letter-spacing:0.1em;margin-top:2px;}
|
||
.vinfo{display:flex;flex-direction:column;gap:7px;align-items:flex-end;margin-bottom:8px;}
|
||
.vinfo .gear{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:6px 12px;backdrop-filter:blur(10px);
|
||
font-size:13px;font-weight:800;}
|
||
.vinfo .gear b{color:var(--accent);font-size:16px;}
|
||
.warn{display:flex;gap:7px;}
|
||
.warn .w{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;font-size:15px;
|
||
background:var(--panel);border:1px solid var(--border);backdrop-filter:blur(10px);color:var(--muted);transition:.15s;}
|
||
.warn .w.alert{color:#f0506e;border-color:#f0506e;background:rgba(240,80,110,0.14);animation:blink 1s steps(2) infinite;}
|
||
.warn .w.ok{color:var(--health);}
|
||
@keyframes blink{50%{opacity:0.35;}}
|
||
.fuelbar{width:150px;height:6px;border-radius:99px;background:rgba(255,255,255,0.12);overflow:hidden;margin-top:2px;}
|
||
.fuelbar i{display:block;height:100%;background:linear-gradient(90deg,#f5a742,#f0d24a);border-radius:99px;transition:width .3s;}
|
||
|
||
/* ============================================================
|
||
DEMO-/DEBUG-PANEL (nur Prototyp — in FiveM entfällt es)
|
||
============================================================ */
|
||
.demo{position:absolute;top:20px;right:20px;width:280px;background:var(--panel2);border:1px solid var(--border);
|
||
border-radius:16px;backdrop-filter:blur(14px);box-shadow:0 24px 50px -20px rgba(0,0,0,0.8);overflow:hidden;}
|
||
.demo .dh{display:flex;align-items:center;justify-content:space-between;padding:12px 15px;cursor:pointer;
|
||
border-bottom:1px solid var(--border);}
|
||
.demo .dh .t{font-size:13px;font-weight:800;} .demo .dh .t small{color:var(--muted);font-weight:600;font-size:10.5px;display:block;margin-top:1px;}
|
||
.demo .dh .tog{color:var(--muted);font-size:15px;transition:.2s;}
|
||
.demo.collapsed .dh .tog{transform:rotate(-90deg);}
|
||
.demo .db{padding:13px 15px;display:flex;flex-direction:column;gap:13px;max-height:76vh;overflow-y:auto;}
|
||
.demo.collapsed .db{display:none;}
|
||
.demo .grp{display:flex;flex-direction:column;gap:8px;}
|
||
.demo .grp > .gl{font-size:9.5px;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em;font-weight:800;}
|
||
.demo .slider{display:flex;align-items:center;gap:9px;}
|
||
.demo .slider label{font-size:11.5px;width:64px;color:var(--fg);font-weight:600;}
|
||
.demo .slider input[type=range]{flex:1;accent-color:var(--accent);cursor:pointer;}
|
||
.demo .slider .rv{font-size:11px;color:var(--muted);width:30px;text-align:right;font-variant-numeric:tabular-nums;}
|
||
.demo .btns{display:flex;flex-wrap:wrap;gap:6px;}
|
||
.demo button{border:1px solid var(--border);background:rgba(255,255,255,0.05);color:var(--fg);border-radius:9px;
|
||
padding:7px 10px;font-family:inherit;font-size:11.5px;font-weight:700;cursor:pointer;transition:.12s;}
|
||
.demo button:hover{border-color:var(--accent);color:#fff;}
|
||
.demo button.pri{background:var(--accent);color:#160c2e;border-color:transparent;}
|
||
.demo button.on{background:var(--accent-soft,rgba(124,92,246,0.2));border-color:var(--accent);color:#fff;}
|
||
.demo .divider{height:1px;background:var(--border);margin:1px 0;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Kulisse hinter dem HUD -->
|
||
<div class="scene">
|
||
<div class="sky"></div><div class="sun"></div><div class="city"></div>
|
||
<div class="ground"></div><div class="road"></div><div class="dash"></div>
|
||
</div>
|
||
|
||
<!-- HUD-Overlay -->
|
||
<div class="hud">
|
||
<!-- Voice-Anzeige -->
|
||
<div class="voice" id="voice">
|
||
<span class="mic">🎙️</span>
|
||
<div class="bars"><i style="height:6px"></i><i style="height:10px"></i><i style="height:14px"></i></div>
|
||
<span class="range" id="voiceRange">Normal</span>
|
||
</div>
|
||
|
||
<!-- Status-Cluster -->
|
||
<div class="cluster">
|
||
<div class="gauges" id="gauges"></div>
|
||
</div>
|
||
|
||
<!-- Fahrzeug-HUD -->
|
||
<div class="vehicle" id="vehicle">
|
||
<div class="vinfo">
|
||
<div class="gear">Gang <b id="gear">N</b></div>
|
||
<div class="warn">
|
||
<div class="w" id="wSeat" title="Sicherheitsgurt">🎗️</div>
|
||
<div class="w ok" id="wEngine" title="Motor">⚙️</div>
|
||
</div>
|
||
<div class="fuelbar"><i id="fuelFill"></i></div>
|
||
</div>
|
||
<div class="speedo">
|
||
<svg width="150" height="150" viewBox="0 0 150 150">
|
||
<circle class="track" cx="75" cy="75" r="62" fill="none" stroke-width="10" stroke-linecap="round"
|
||
stroke-dasharray="292 390" pathLength="390"></circle>
|
||
<circle id="speedArc" cx="75" cy="75" r="62" fill="none" stroke="url(#sg)" stroke-width="10" stroke-linecap="round"
|
||
stroke-dasharray="0 390" pathLength="390"></circle>
|
||
<defs><linearGradient id="sg"><stop offset="0" stop-color="#5b8cff"/><stop offset="1" stop-color="#7c5cf6"/></linearGradient></defs>
|
||
</svg>
|
||
<div class="big"><div class="n" id="speedN">0</div><div class="u">KM/H</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Geld-Anzeige -->
|
||
<div class="money" id="money"></div>
|
||
|
||
<!-- Demo-Panel -->
|
||
<div class="demo" id="demo">
|
||
<div class="dh" id="demoHead">
|
||
<div class="t">Demo-Steuerung<small>Nur Prototyp — Werte simulieren</small></div>
|
||
<div class="tog">▾</div>
|
||
</div>
|
||
<div class="db">
|
||
<div class="grp">
|
||
<div class="gl">Status (0–100)</div>
|
||
<div class="slider"><label>❤️ Leben</label><input type="range" id="sLeben" min="0" max="100" value="100"><span class="rv" id="rLeben">100</span></div>
|
||
<div class="slider"><label>🍔 Hunger</label><input type="range" id="sHunger" min="0" max="100" value="82"><span class="rv" id="rHunger">82</span></div>
|
||
<div class="slider"><label>💧 Durst</label><input type="range" id="sDurst" min="0" max="100" value="74"><span class="rv" id="rDurst">74</span></div>
|
||
<div class="slider"><label>🫁 Ausdauer</label><input type="range" id="sAusdauer" min="0" max="100" value="100"><span class="rv" id="rAusdauer">100</span></div>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="grp">
|
||
<div class="gl">Aktionen</div>
|
||
<div class="btns">
|
||
<button id="bDamage">Schaden −20</button>
|
||
<button id="bHeal">Heilen</button>
|
||
<button id="bEat">🍔 Essen</button>
|
||
<button id="bDrink">💧 Trinken</button>
|
||
<button id="bTalk">🎙️ Sprechen</button>
|
||
<button id="bVoice">Voice-Reichweite</button>
|
||
</div>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="grp">
|
||
<div class="gl">Geld</div>
|
||
<div class="btns">
|
||
<button id="bCash">Bargeld +500</button>
|
||
<button id="bBank">Konto −250</button>
|
||
<button id="bDirty">Schwarzgeld an/aus</button>
|
||
</div>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="grp">
|
||
<div class="gl">Fahrzeug</div>
|
||
<div class="btns"><button id="bVehicle" class="pri">🚗 Einsteigen</button></div>
|
||
<div class="slider"><label>Tempo</label><input type="range" id="sSpeed" min="0" max="260" value="0"><span class="rv" id="rSpeed">0</span></div>
|
||
<div class="slider"><label>Tank</label><input type="range" id="sFuel" min="0" max="100" value="68"><span class="rv" id="rFuel">68</span></div>
|
||
<div class="btns">
|
||
<button id="bSeat">🎗️ Gurt an/aus</button>
|
||
<button id="bEngine" class="on">⚙️ Motor an</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
"use strict";
|
||
/* Kurzer Zugriffshelfer auf Elemente per ID */
|
||
const $=id=>document.getElementById(id);
|
||
/* Zahl auf einen Bereich begrenzen */
|
||
const clamp=(n,a,b)=>Math.max(a,Math.min(b,n));
|
||
/* Betrag als Euro formatieren */
|
||
const euro=n=>Math.round(n).toLocaleString("de-DE")+" €";
|
||
|
||
/* =====================================================================
|
||
HUD-ZUSTAND (im echten Betrieb per NUI vom Client gesetzt)
|
||
Bewusst vom UI getrennt gehalten.
|
||
===================================================================== */
|
||
const hud={
|
||
leben:100, hunger:82, durst:74, ausdauer:100, // Status 0–100
|
||
redet:false, // spricht der Spieler gerade?
|
||
voiceStufe:1, // 0=Flüstern,1=Normal,2=Rufen
|
||
bargeld:2450, konto:18900, schwarzgeld:1200, // Geldwerte
|
||
schwarzgeldSichtbar:false, // Schwarzgeld nur bei Bedarf zeigen
|
||
// Fahrzeug-bezogene Werte
|
||
imFahrzeug:false, tempo:0, tank:68, gurt:false, motor:true,
|
||
};
|
||
|
||
/* Die drei Voice-Reichweiten mit Beschriftung und Anzahl aktiver Balken */
|
||
const voiceStufen=[{name:"Flüstern",bars:1},{name:"Normal",bars:2},{name:"Rufen",bars:3}];
|
||
|
||
/* Definition der Status-Anzeigen (Reihenfolge = Anzeige-Reihenfolge) */
|
||
const gaugeDefs=[
|
||
{key:"leben", icon:"❤️", farbe:"var(--health)"},
|
||
{key:"hunger", icon:"🍔", farbe:"var(--hunger)"},
|
||
{key:"durst", icon:"💧", farbe:"var(--thirst)"},
|
||
{key:"ausdauer", icon:"🫁", farbe:"var(--stamina)"},
|
||
];
|
||
const RING_UMFANG=2*Math.PI*17; // Umfang der Status-Ringe (Radius 17)
|
||
|
||
/* =====================================================================
|
||
RENDERING
|
||
===================================================================== */
|
||
/* Baut die Status-Ringe einmalig auf (SVG-Ring + Icon + Zahl) */
|
||
function baueGauges(){
|
||
$("gauges").innerHTML=gaugeDefs.map(g=>
|
||
`<div class="gauge" title="${g.key}">
|
||
<svg width="44" height="44" viewBox="0 0 44 44">
|
||
<circle class="track" cx="22" cy="22" r="17" fill="none" stroke-width="4"></circle>
|
||
<circle id="arc-${g.key}" cx="22" cy="22" r="17" fill="none" stroke="${g.farbe}" stroke-width="4"
|
||
stroke-linecap="round" stroke-dasharray="${RING_UMFANG}" stroke-dashoffset="${RING_UMFANG}"></circle>
|
||
</svg>
|
||
<div class="icon">${g.icon}</div>
|
||
<div class="num" id="num-${g.key}">0</div>
|
||
</div>`).join('');
|
||
}
|
||
/* Aktualisiert die Füllstände aller Status-Ringe aus dem Zustand */
|
||
function renderGauges(){
|
||
for(const g of gaugeDefs){
|
||
const wert=clamp(hud[g.key],0,100);
|
||
const arc=$("arc-"+g.key);
|
||
arc.style.strokeDashoffset=RING_UMFANG*(1-wert/100); // weniger Wert = mehr Lücke
|
||
$("num-"+g.key).textContent=Math.round(wert);
|
||
}
|
||
}
|
||
|
||
/* Aktualisiert die Voice-Anzeige (Reichweite + Sprech-Zustand) */
|
||
function renderVoice(){
|
||
const stufe=voiceStufen[hud.voiceStufe];
|
||
$("voiceRange").textContent=stufe.name;
|
||
$("voice").classList.toggle("talking",hud.redet);
|
||
// Nur so viele Balken hervorheben, wie die Reichweite vorgibt (und nur beim Reden)
|
||
$("voice").querySelectorAll(".bars i").forEach((b,i)=>
|
||
b.classList.toggle("on",hud.redet&&i<stufe.bars));
|
||
}
|
||
|
||
/* Aktualisiert die Geld-Anzeige (Schwarzgeld optional) */
|
||
function renderMoney(){
|
||
let html=
|
||
`<div class="row cash"><span class="mi">💵</span><span class="ml">Bargeld</span><span class="mv">${euro(hud.bargeld)}</span></div>
|
||
<div class="row bank"><span class="mi">🏦</span><span class="ml">Konto</span><span class="mv">${euro(hud.konto)}</span></div>`;
|
||
if(hud.schwarzgeldSichtbar)
|
||
html+=`<div class="row dirty"><span class="mi">🕶️</span><span class="ml">Schwarzgeld</span><span class="mv">${euro(hud.schwarzgeld)}</span></div>`;
|
||
$("money").innerHTML=html;
|
||
}
|
||
/* Lässt eine Geld-Zeile kurz aufblinken, wenn sich der Wert ändert */
|
||
function blinkeGeld(klasse){const el=$("money").querySelector("."+klasse);if(el){el.classList.add("flash");setTimeout(()=>el.classList.remove("flash"),600);}}
|
||
|
||
/* Aktualisiert das Fahrzeug-HUD (Tacho, Gang, Tank, Warnungen) */
|
||
const SPEED_MAX=260; // Skalen-Maximum des Tachos
|
||
const SPEED_PATHLEN=390, SPEED_ARC=292; // Pfad-Länge und sichtbarer 270°-Bogen
|
||
function renderVehicle(){
|
||
$("vehicle").classList.toggle("on",hud.imFahrzeug);
|
||
if(!hud.imFahrzeug)return;
|
||
// Tacho-Bogen proportional zum Tempo füllen
|
||
const anteil=clamp(hud.tempo/SPEED_MAX,0,1);
|
||
$("speedArc").setAttribute("stroke-dasharray",(SPEED_ARC*anteil)+" "+SPEED_PATHLEN);
|
||
$("speedN").textContent=Math.round(hud.tempo);
|
||
// Gang aus dem Tempo ableiten (grobe Simulation)
|
||
$("gear").textContent=!hud.motor?"–":hud.tempo<1?"N":Math.min(6,Math.floor(hud.tempo/38)+1);
|
||
// Tankanzeige
|
||
$("fuelFill").style.width=clamp(hud.tank,0,100)+"%";
|
||
// Warnleuchten: Gurt blinkt bei Fahrt ohne Gurt, Motor zeigt Status
|
||
$("wSeat").className="w"+(!hud.gurt&&hud.tempo>5?" alert":(hud.gurt?" ok":""));
|
||
$("wEngine").className="w"+(hud.motor?" ok":" alert");
|
||
}
|
||
|
||
/* Rendert das komplette HUD neu */
|
||
function renderAll(){renderGauges();renderVoice();renderMoney();renderVehicle();}
|
||
|
||
/* =====================================================================
|
||
DEMO-STEUERUNG (nur im Prototyp; in FiveM ersetzt durch echte Events)
|
||
===================================================================== */
|
||
/* Verknüpft einen Slider mit einem Zustandswert inkl. Zahl-Anzeige */
|
||
function bindeSlider(sliderId,anzeigeId,setter){
|
||
const s=$(sliderId);
|
||
s.addEventListener("input",()=>{$(anzeigeId).textContent=s.value;setter(+s.value);renderAll();});
|
||
}
|
||
bindeSlider("sLeben","rLeben",v=>hud.leben=v);
|
||
bindeSlider("sHunger","rHunger",v=>hud.hunger=v);
|
||
bindeSlider("sDurst","rDurst",v=>hud.durst=v);
|
||
bindeSlider("sAusdauer","rAusdauer",v=>hud.ausdauer=v);
|
||
bindeSlider("sSpeed","rSpeed",v=>hud.tempo=v);
|
||
bindeSlider("sFuel","rFuel",v=>hud.tank=v);
|
||
|
||
/* Synchronisiert die Slider-Positionen mit dem Zustand (nach Aktionen) */
|
||
function syncSlider(){
|
||
$("sLeben").value=hud.leben; $("rLeben").textContent=Math.round(hud.leben);
|
||
$("sHunger").value=hud.hunger; $("rHunger").textContent=Math.round(hud.hunger);
|
||
$("sDurst").value=hud.durst; $("rDurst").textContent=Math.round(hud.durst);
|
||
$("sAusdauer").value=hud.ausdauer; $("rAusdauer").textContent=Math.round(hud.ausdauer);
|
||
$("sSpeed").value=hud.tempo; $("rSpeed").textContent=Math.round(hud.tempo);
|
||
$("sFuel").value=hud.tank; $("rFuel").textContent=Math.round(hud.tank);
|
||
}
|
||
|
||
/* Aktions-Buttons */
|
||
$("bDamage").addEventListener("click",()=>{
|
||
// Schaden geht direkt auf das Leben
|
||
hud.leben=clamp(hud.leben-20,0,100);
|
||
syncSlider(); renderAll();
|
||
});
|
||
$("bHeal").addEventListener("click",()=>{hud.leben=100;syncSlider();renderAll();});
|
||
$("bEat").addEventListener("click",()=>{hud.hunger=clamp(hud.hunger+25,0,100);syncSlider();renderAll();});
|
||
$("bDrink").addEventListener("click",()=>{hud.durst=clamp(hud.durst+25,0,100);syncSlider();renderAll();});
|
||
|
||
/* „Sprechen" simulieren: Voice kurz aktiv schalten */
|
||
let redeTimer=null;
|
||
$("bTalk").addEventListener("click",()=>{
|
||
hud.redet=true; renderVoice(); clearTimeout(redeTimer);
|
||
redeTimer=setTimeout(()=>{hud.redet=false;renderVoice();},1500);
|
||
});
|
||
/* Voice-Reichweite durchschalten */
|
||
$("bVoice").addEventListener("click",()=>{hud.voiceStufe=(hud.voiceStufe+1)%voiceStufen.length;renderVoice();});
|
||
|
||
/* Geld-Buttons */
|
||
$("bCash").addEventListener("click",()=>{hud.bargeld+=500;renderMoney();blinkeGeld("cash");});
|
||
$("bBank").addEventListener("click",()=>{hud.konto=Math.max(0,hud.konto-250);renderMoney();blinkeGeld("bank");});
|
||
$("bDirty").addEventListener("click",()=>{hud.schwarzgeldSichtbar=!hud.schwarzgeldSichtbar;renderMoney();if(hud.schwarzgeldSichtbar)blinkeGeld("dirty");});
|
||
|
||
/* Fahrzeug-Buttons */
|
||
$("bVehicle").addEventListener("click",()=>{
|
||
hud.imFahrzeug=!hud.imFahrzeug;
|
||
$("bVehicle").textContent=hud.imFahrzeug?"🚶 Aussteigen":"🚗 Einsteigen";
|
||
$("bVehicle").classList.toggle("on",hud.imFahrzeug);
|
||
if(!hud.imFahrzeug){hud.tempo=0;syncSlider();} // beim Aussteigen Tempo zurücksetzen
|
||
renderAll();
|
||
});
|
||
$("bSeat").addEventListener("click",()=>{hud.gurt=!hud.gurt;$("bSeat").classList.toggle("on",hud.gurt);renderVehicle();});
|
||
$("bEngine").addEventListener("click",()=>{hud.motor=!hud.motor;$("bEngine").classList.toggle("on",hud.motor);$("bEngine").textContent=hud.motor?"⚙️ Motor an":"⚙️ Motor aus";renderVehicle();});
|
||
|
||
/* Demo-Panel ein-/ausklappen */
|
||
$("demoHead").addEventListener("click",()=>$("demo").classList.toggle("collapsed"));
|
||
|
||
/* =====================================================================
|
||
LEBENDIGE SIMULATION (langsamer Verbrauch)
|
||
===================================================================== */
|
||
/* Hunger/Durst sinken langsam; leerer Magen zieht am Leben; Tank sinkt bei Fahrt */
|
||
function tickVerbrauchsWerte(){
|
||
hud.hunger=clamp(hud.hunger-1,0,100);
|
||
hud.durst=clamp(hud.durst-1.4,0,100);
|
||
if(hud.hunger<=0||hud.durst<=0)hud.leben=clamp(hud.leben-2,0,100); // Verhungern/Verdursten
|
||
if(hud.imFahrzeug&&hud.motor&&hud.tempo>0)hud.tank=clamp(hud.tank-0.6,0,100);
|
||
if(hud.tank<=0)hud.tempo=0; // ohne Sprit kein Vortrieb
|
||
syncSlider(); renderAll();
|
||
}
|
||
/* =====================================================================
|
||
NUI-BRÜCKE — Werte kommen vom FiveM-Client
|
||
===================================================================== */
|
||
/* Läuft das HUD in FiveM oder eigenständig im Browser? */
|
||
const IST_FIVEM = (typeof GetParentResourceName === "function");
|
||
|
||
/* Übernimmt nur die Felder, die tatsächlich mitgeschickt wurden */
|
||
function uebernehme(ziel, quelle, felder){
|
||
for(const feld of felder){
|
||
if(quelle[feld] !== undefined && quelle[feld] !== null) ziel[feld] = quelle[feld];
|
||
}
|
||
}
|
||
|
||
window.addEventListener("message", (ereignis) => {
|
||
const n = ereignis.data || {};
|
||
|
||
if(n.aktion === "status"){ // Leben, Hunger, Durst, Ausdauer
|
||
uebernehme(hud, n, ["leben","hunger","durst","ausdauer"]);
|
||
renderGauges();
|
||
}
|
||
else if(n.aktion === "geld"){ // Bargeld, Konto, Schwarzgeld
|
||
const vorherBar = hud.bargeld, vorherKonto = hud.konto;
|
||
uebernehme(hud, n, ["bargeld","konto","schwarzgeld","schwarzgeldSichtbar"]);
|
||
renderMoney();
|
||
// Geänderte Beträge kurz hervorheben
|
||
if(n.bargeld !== undefined && n.bargeld !== vorherBar) blinkeGeld("cash");
|
||
if(n.konto !== undefined && n.konto !== vorherKonto) blinkeGeld("bank");
|
||
}
|
||
else if(n.aktion === "voice"){ // Sprechen und Reichweite
|
||
uebernehme(hud, n, ["redet","voiceStufe"]);
|
||
renderVoice();
|
||
}
|
||
else if(n.aktion === "fahrzeug"){ // Tacho, Tank, Gurt, Motor
|
||
uebernehme(hud, n, ["imFahrzeug","tempo","tank","gurt","motor"]);
|
||
renderVehicle();
|
||
}
|
||
else if(n.aktion === "sichtbar"){ // HUD ein-/ausblenden
|
||
document.body.classList.toggle("hud-aus", n.an === false);
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
START
|
||
===================================================================== */
|
||
baueGauges();
|
||
|
||
if(IST_FIVEM){
|
||
// Im Spiel: Kulisse und Demo-Panel ausblenden, keine Simulation laufen lassen
|
||
document.body.classList.add("ingame");
|
||
// Startwerte neutral halten, bis der Client die echten Werte schickt
|
||
Object.assign(hud, {leben:100, hunger:100, durst:100, ausdauer:100,
|
||
bargeld:0, konto:0, schwarzgeld:0, imFahrzeug:false, tempo:0});
|
||
renderAll();
|
||
} else {
|
||
// Eigenständige Browser-Ansicht: Demo-Panel und Verbrauchssimulation aktiv
|
||
syncSlider();
|
||
renderAll();
|
||
setInterval(tickVerbrauchsWerte, 4000);
|
||
}
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|