aether-framework/resources/[aether]/aether-death/html/index.html
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark,
  Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI)
- aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource,
  an aether-core gekoppelt (Rechte über aether_users.admin_group), mit
  WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU)
- Prototypen admin/ vervollständigt
- SQL-Schema aether_admin.sql
- Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:55:32 +02:00

406 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Aether — Tod / Respawn</title>
<style>
:root{
--fg:#f2eef0; /* Standard-Textfarbe */
--muted:#a7a0ac; /* gedämpfter Text */
--panel:rgba(20,10,14,0.72); /* halbtransparente Fläche */
--panel2:rgba(30,16,20,0.9); /* dichtere Fläche */
--border:rgba(255,255,255,0.1);/* dezente Rahmen */
--danger:#f0506e; /* Warn-/Blutrot (Leitfarbe des Screens) */
--danger2:#ff2e57; /* kräftigeres Rot für Akzente */
--accent:#7c5cf6; /* Aether-Akzent (Violett) für Sekundäres */
--good:#34d39a; /* Erfolg (behandelt) */
--ems:#f5a742; /* Rettungsdienst-Orange */
}
*{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 liegt die echte Spielwelt dahinter: Kulisse und Demo entfallen. */
body.ingame .scene{display:none;}
body.ingame .demo{display:none;}
/* ============================================================
ENTSÄTTIGTE HINTERGRUND-KULISSE (Boden-Perspektive)
============================================================ */
.scene{position:fixed;inset:0;z-index:0;overflow:hidden;filter:grayscale(0.85) brightness(0.45);
background:linear-gradient(180deg,#14121a 0%,#1e1a26 40%,#2a2430 60%,#171320 60%);}
.scene .city{position:absolute;left:0;right:0;bottom:32%;height:26%;
background:linear-gradient(90deg,#1c1826 0 9%,transparent 9% 14%,#241f30 14% 22%,transparent 22% 27%,
#1a1624 27% 35%,transparent 35% 41%,#241f30 41% 48%,transparent 48% 55%,#1c1826 55% 63%,transparent 63% 69%,
#201a2c 69% 78%,transparent 78% 84%,#241f30 84% 92%,transparent 92%);}
.scene .ground{position:absolute;left:0;right:0;bottom:0;height:32%;background:linear-gradient(180deg,#0f0c14,#000);}
/* Pulsierende blutrote Vignette über allem */
.vignette{position:fixed;inset:0;z-index:1;pointer-events:none;
background:radial-gradient(120% 100% at 50% 60%,transparent 20%,rgba(90,0,15,0.55) 75%,rgba(50,0,8,0.85) 100%);
animation:blut 3.2s ease-in-out infinite;}
@keyframes blut{50%{opacity:0.72;}}
.vignette.kritisch{animation-duration:1.1s;background:radial-gradient(120% 100% at 50% 60%,transparent 8%,rgba(140,0,20,0.6) 65%,rgba(70,0,10,0.92) 100%);}
/* ============================================================
ZENTRALER INHALTSBLOCK (wechselt je nach Phase)
============================================================ */
.wrap{position:relative;z-index:2;height:100%;display:grid;place-items:center;padding:24px;}
.center{width:min(560px,94vw);text-align:center;animation:auf .5s ease;}
@keyframes auf{from{opacity:0;transform:translateY(14px);}to{opacity:1;transform:none;}}
.status-label{font-size:13px;font-weight:800;letter-spacing:0.32em;text-transform:uppercase;color:var(--danger);
margin-bottom:10px;animation:flackern 2.4s ease-in-out infinite;}
@keyframes flackern{45%{opacity:1;}50%{opacity:0.45;}55%{opacity:1;}}
.title{font-size:clamp(44px,9vw,74px);font-weight:900;letter-spacing:-0.02em;line-height:0.95;margin:0;
color:#fff;text-shadow:0 0 30px rgba(240,80,110,0.45);}
.subtitle{color:var(--muted);font-size:15.5px;margin:14px auto 0;max-width:400px;line-height:1.55;}
/* EKG-Streifen */
.ecg{margin:26px auto 6px;width:min(440px,86vw);height:80px;border-radius:14px;overflow:hidden;position:relative;
background:rgba(0,0,0,0.4);border:1px solid var(--border);}
.ecg .track{display:flex;width:200%;height:100%;animation:ecgScroll var(--beat,1s) linear infinite;}
.ecg .track svg{width:50%;height:100%;flex:none;}
.ecg .line{fill:none;stroke:var(--danger2);stroke-width:2.4;filter:drop-shadow(0 0 6px rgba(255,46,87,0.8));}
@keyframes ecgScroll{to{transform:translateX(-50%);}}
.ecg.flat .track{animation:none;transform:none;}
.ecg.flat .line{stroke:#8a2740;filter:none;}
/* Countdown-Anzeige */
.timer{margin-top:8px;}
.timer .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;font-weight:800;}
.timer .v{font-size:40px;font-weight:900;font-variant-numeric:tabular-nums;letter-spacing:0.02em;}
.timer.warnung .v{color:var(--danger);}
/* Statuszeile zum Rettungsdienst */
.emsline{margin:16px auto 0;display:inline-flex;align-items:center;gap:9px;background:var(--panel);
border:1px solid var(--border);border-radius:999px;padding:8px 16px;font-size:13px;font-weight:700;backdrop-filter:blur(8px);}
.emsline .dot{width:8px;height:8px;border-radius:50%;background:var(--muted);flex:none;}
.emsline.aktiv .dot{background:var(--ems);box-shadow:0 0 8px var(--ems);animation:blink 1s steps(2) infinite;}
.emsline.da .dot{background:var(--good);box-shadow:0 0 8px var(--good);animation:none;}
@keyframes blink{50%{opacity:0.3;}}
/* Aktions-Buttons */
.actions{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:24px;}
.btn{border:1px solid var(--border);background:var(--panel2);color:var(--fg);border-radius:12px;padding:13px 20px;
font-family:inherit;font-size:14px;font-weight:800;cursor:pointer;transition:.12s;display:inline-flex;align-items:center;gap:8px;backdrop-filter:blur(8px);}
.btn:hover:not(:disabled){transform:translateY(-2px);border-color:rgba(255,255,255,0.25);}
.btn:disabled{opacity:0.4;cursor:not-allowed;}
.btn.ems{background:linear-gradient(150deg,var(--ems),#d97b1a);color:#231603;border-color:transparent;}
.btn.give{background:var(--panel2);}
.btn.good{background:linear-gradient(150deg,var(--good),#1f9e77);color:#052018;border-color:transparent;}
.btn:active:not(:disabled){transform:scale(0.98);}
.hint{font-size:11.5px;color:var(--muted);margin-top:14px;}
/* Große Ikone für Ergebnis-Screens (behandelt / verstorben) */
.bigicon{font-size:64px;margin-bottom:6px;}
.infocard{margin:20px auto 0;background:var(--panel);border:1px solid var(--border);border-radius:14px;
padding:14px 18px;max-width:340px;text-align:left;backdrop-filter:blur(8px);}
.infocard .r{display:flex;justify-content:space-between;gap:14px;font-size:13px;padding:5px 0;}
.infocard .r .k{color:var(--muted);} .infocard .r .val{font-weight:800;font-variant-numeric:tabular-nums;}
.infocard .r .val.kosten{color:var(--ems);}
/* Demo-Hinweisleiste unten (nur Prototyp) */
.demo{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:3;display:flex;gap:8px;align-items:center;
background:var(--panel2);border:1px solid var(--border);border-radius:12px;padding:7px 10px;backdrop-filter:blur(10px);}
.demo span{font-size:10.5px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:0.06em;margin:0 4px;}
.demo button{border:1px solid var(--border);background:rgba(255,255,255,0.05);color:var(--fg);border-radius:8px;
padding:6px 11px;font-family:inherit;font-size:11.5px;font-weight:700;cursor:pointer;transition:.12s;}
.demo button:hover{border-color:var(--accent);color:#fff;}
</style>
</head>
<body>
<!-- Kulisse + Vignette -->
<div class="scene"><div class="city"></div><div class="ground"></div></div>
<div class="vignette" id="vignette"></div>
<!-- Wechselnder Inhaltsbereich -->
<div class="wrap"><div class="center" id="center"></div></div>
<!-- Demo-Steuerung (nur Prototyp — in FiveM entfällt sie) -->
<div class="demo">
<span>Demo</span>
<button id="dRevive">🚑 Sanitäter behandelt sofort</button>
<button id="dReset">↺ Zurücksetzen</button>
</div>
<script>
"use strict";
/* Kurzer Zugriffshelfer auf Elemente per ID */
const $=id=>document.getElementById(id);
/* Sekunden als M:SS formatieren */
const mmss=s=>Math.floor(s/60)+":"+String(Math.max(0,s)%60).padStart(2,"0");
/* Betrag als Euro formatieren */
const euro=n=>Math.round(n).toLocaleString("de-DE")+" €";
/* =====================================================================
KONSTANTEN
===================================================================== */
let BLEEDOUT_START=300; // Verblutungs-Countdown in Sekunden (5:00)
let RESPAWN_SPERRE=45; // Mindestwartezeit, bevor „Aufgeben" möglich ist
const BEHANDLUNG_DAUER=6; // Dauer der Behandlung durch den Sanitäter
let KRANKENHAUS_KOSTEN=1500; // Behandlungspauschale beim Aufwachen im Krankenhaus
/* Ein Heartbeat-Segment (PQRST) als SVG-Pfad für den EKG-Streifen */
const EKG_PFAD="M0,40 L70,40 L82,40 L90,16 L98,60 L106,26 L114,40 L150,40 L200,40";
/* =====================================================================
ZUSTAND (im echten Betrieb per NUI vom Client gesetzt / gesteuert)
Bewusst vom UI getrennt gehalten.
===================================================================== */
const tod={
phase:"down", // down | behandelt | krankenhaus
restBleed:BLEEDOUT_START, // verbleibende Zeit bis zum Verbluten
respawnSperre:RESPAWN_SPERRE, // Restsperre für „Aufgeben"
emsGerufen:false, // wurde der Rettungsdienst gerufen?
emsEta:0, // verbleibende Zeit bis der Sanitäter eintrifft
sanitaeterDa:false, // ist der Sanitäter am Einsatzort?
behandlungRest:0, // Restzeit der laufenden Behandlung
};
/* =====================================================================
RENDERING JE PHASE
===================================================================== */
/* Baut den „Verwundet"-Screen einmalig auf (EKG-Animation soll durchlaufen) */
function renderDown(){
$("center").innerHTML=`
<div class="status-label">● Schwer verletzt</div>
<h1 class="title">VERWUNDET</h1>
<p class="subtitle">Du liegst am Boden und verlierst Blut. Ruf den Rettungsdienst — oder warte auf Hilfe.</p>
<div class="ecg" id="ecg"><div class="track">
<svg viewBox="0 0 200 80" preserveAspectRatio="none"><path class="line" d="${EKG_PFAD}"></path></svg>
<svg viewBox="0 0 200 80" preserveAspectRatio="none"><path class="line" d="${EKG_PFAD}"></path></svg>
</div></div>
<div class="timer" id="timer"><div class="l">Verbleibende Zeit</div><div class="v" id="timerV">${mmss(tod.restBleed)}</div></div>
<div class="emsline" id="emsline"><span class="dot"></span><span id="emsText">Kein Notruf abgesetzt</span></div>
<div class="actions">
<button class="btn ems" id="btnEms">🚑 Rettungsdienst rufen</button>
<button class="btn" id="btnHelp">🆘 Um Hilfe rufen</button>
<button class="btn give" id="btnGive" disabled>🏳️ Aufgeben</button>
</div>
<div class="hint" id="hint"></div>`;
// Button-Ereignisse verknüpfen
$("btnEms").addEventListener("click",rufeEms);
$("btnHelp").addEventListener("click",rufeHilfe);
$("btnGive").addEventListener("click",aufgeben);
updateDown();
}
/* Aktualisiert die dynamischen Teile des „Verwundet"-Screens (jede Sekunde) */
function updateDown(){
// Countdown + Warnfärbung bei wenig Restzeit
$("timerV").textContent=mmss(tod.restBleed);
$("timer").classList.toggle("warnung",tod.restBleed<=60);
$("vignette").classList.toggle("kritisch",tod.restBleed<=60);
// EKG-Takt an die Restzeit koppeln (weniger Zeit = langsamerer Herzschlag)
const anteil=Math.max(0,tod.restBleed/BLEEDOUT_START);
$("ecg").style.setProperty("--beat",(2.2-1.3*anteil).toFixed(2)+"s");
// Statuszeile zum Rettungsdienst
const emsline=$("emsline");
if(!tod.emsGerufen){emsline.className="emsline";$("emsText").textContent="Kein Notruf abgesetzt";}
else if(!tod.sanitaeterDa){emsline.className="emsline aktiv";$("emsText").textContent="🚑 Rettungsdienst unterwegs · Eintreffen in "+mmss(tod.emsEta);}
else {emsline.className="emsline da";$("emsText").textContent="🩹 Sanitäter behandelt dich … ("+tod.behandlungRest+"s)";}
// „Aufgeben" erst nach Ablauf der Respawn-Sperre freigeben
const btnGive=$("btnGive");
btnGive.disabled=tod.respawnSperre>0||tod.sanitaeterDa;
btnGive.innerHTML=tod.respawnSperre>0?`🔒 Aufgeben in ${tod.respawnSperre}s`:"🏳️ Aufgeben";
}
/* Zeigt den „Behandelt"-Screen nach erfolgreicher Wiederbelebung */
function renderBehandelt(){
$("vignette").className="vignette";
$("center").innerHTML=`
<div class="bigicon">🩹</div>
<div class="status-label" style="color:var(--good)">● Stabilisiert</div>
<h1 class="title" style="text-shadow:0 0 30px rgba(52,211,153,0.4)">BEHANDELT</h1>
<p class="subtitle">Ein Sanitäter hat dich rechtzeitig erreicht und wiederbelebt. Du bist wieder auf den Beinen.</p>
<div class="infocard">
<div class="r"><span class="k">Leben</span><span class="val" style="color:var(--good)">100 %</span></div>
<div class="r"><span class="k">Behandelt von</span><span class="val">EMS · Los Santos</span></div>
</div>
<div class="actions"><button class="btn good" id="btnWeiter">✔ Weiterspielen</button></div>
<div class="hint">Tipp: Trag Verletzungen aus, statt sie zu ignorieren — gutes Rollenspiel zahlt sich aus.</div>`;
$("btnWeiter").addEventListener("click",neustart);
}
/* Zeigt den „Im Krankenhaus aufgewacht"-Screen (nach Aufgeben oder Verbluten) */
function renderKrankenhaus(grund){
$("vignette").className="vignette";
$("center").innerHTML=`
<div class="bigicon">🏥</div>
<div class="status-label" style="color:var(--accent)">● Notaufnahme</div>
<h1 class="title" style="text-shadow:0 0 30px rgba(124,92,246,0.4)">KRANKENHAUS</h1>
<p class="subtitle">${grund} Das Pflegepersonal hat dich versorgt — die Behandlung ist allerdings nicht umsonst.</p>
<div class="infocard">
<div class="r"><span class="k">Grund</span><span class="val">${tod.emsGerufen?'Kein Sanitäter rechtzeitig':'Aufgegeben'}</span></div>
<div class="r"><span class="k">Behandlungskosten</span><span class="val kosten">− ${euro(KRANKENHAUS_KOSTEN)}</span></div>
<div class="r"><span class="k">Leben</span><span class="val" style="color:var(--good)">100 %</span></div>
</div>
<div class="actions"><button class="btn" id="btnWeiter">➜ Krankenhaus verlassen</button></div>
<div class="hint">Die Kosten gehen als Rechnung an dich (EMS-Finanzen im Fraktions-CAD).</div>`;
$("btnWeiter").addEventListener("click",neustart);
}
/* =====================================================================
AKTIONEN
===================================================================== */
/* Rettungsdienst rufen: Notruf absetzen und Eintreff-Zeit starten */
function rufeEms(){
if(tod.emsGerufen)return;
if(IST_FIVEM){ nui("emsRufen",{}); $("btnEms").disabled=true; return; }
tod.emsGerufen=true;
tod.emsEta=rndInt(30,60); // zufällige Eintreffzeit 0:30–1:00
$("btnEms").disabled=true;
$("hint").textContent="Notruf abgesetzt — ein Sanitäter wurde alarmiert.";
updateDown();
}
/* Um Hilfe rufen: rein kosmetischer Ruf an Umstehende */
function rufeHilfe(){
if(IST_FIVEM){ nui("hilfeRufen",{}); return; }
$("hint").textContent="Du schreist um Hilfe … vielleicht hört dich jemand in der Nähe.";
}
/* Aufgeben: im Krankenhaus aufwachen */
function aufgeben(){
if(tod.respawnSperre>0||tod.sanitaeterDa)return;
if(IST_FIVEM){ nui("aufgeben",{}); return; }
tod.phase="krankenhaus";
renderKrankenhaus("Du hast aufgegeben und wachst in der Notaufnahme wieder auf.");
}
/* Sanitäter beginnt die Behandlung (durch Eintreffen oder Demo-Button) */
function starteBehandlung(){
if(tod.sanitaeterDa||tod.phase!=="down")return;
tod.sanitaeterDa=true;
tod.emsGerufen=true; // Demo-Sofortbehandlung setzt „gerufen" mit
tod.behandlungRest=BEHANDLUNG_DAUER;
updateDown();
}
/* Behandlung erfolgreich abgeschlossen → wiederbelebt */
function wiederbelebt(){
tod.phase="behandelt";
renderBehandelt();
}
/* Verbluten ohne rechtzeitige Hilfe → Krankenhaus */
function verblutet(){
tod.phase="krankenhaus";
renderKrankenhaus("Niemand hat dich rechtzeitig erreicht — du bist verblutet.");
}
/* Alles auf Anfang zurücksetzen (für erneutes Durchspielen) */
function neustart(){
Object.assign(tod,{phase:"down",restBleed:BLEEDOUT_START,respawnSperre:RESPAWN_SPERRE,
emsGerufen:false,emsEta:0,sanitaeterDa:false,behandlungRest:0});
renderDown();
}
/* Ganzzahl-Zufall zwischen a und b (inklusive) */
function rndInt(a,b){return a+Math.floor(Math.random()*(b-a+1));}
/* =====================================================================
SEKUNDEN-TAKT (treibt alle Countdowns; in FiveM von echten Events ersetzt)
===================================================================== */
setInterval(()=>{
if(IST_FIVEM)return; // In FiveM führt der Server den Countdown
if(tod.phase!=="down")return; // in den Ergebnis-Screens läuft nichts weiter
// Respawn-Sperre herunterzählen
if(tod.respawnSperre>0)tod.respawnSperre--;
if(tod.sanitaeterDa){
// Laufende Behandlung: bei 0 wiederbeleben
tod.behandlungRest--;
if(tod.behandlungRest<=0){wiederbelebt();return;}
} else {
// Verblutungs-Countdown
tod.restBleed--;
// Sanitäter unterwegs: ETA herunterzählen, bei 0 Behandlung starten
if(tod.emsGerufen&&tod.emsEta>0){tod.emsEta--; if(tod.emsEta<=0)starteBehandlung();}
// Zeit abgelaufen ohne Rettung → verblutet
if(tod.restBleed<=0){verblutet();return;}
}
updateDown();
},1000);
/* Demo-Buttons */
$("dRevive").addEventListener("click",()=>{if(tod.phase==="down")starteBehandlung();});
$("dReset").addEventListener("click",neustart);
/* =====================================================================
NUI-BRÜCKE — Verbindung zum FiveM-Client
===================================================================== */
const IST_FIVEM = (typeof GetParentResourceName === "function");
const RESSOURCE = IST_FIVEM ? GetParentResourceName() : "aether-death";
function nui(aktion, daten){
if(!IST_FIVEM) return Promise.resolve();
return fetch(`https://${RESSOURCE}/${aktion}`, {
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: JSON.stringify(daten || {}),
}).catch(() => {});
}
window.addEventListener("message", (ereignis) => {
const n = ereignis.data || {};
if(n.aktion === "oeffnen"){
document.body.style.display = "";
if(n.bleedoutMax) BLEEDOUT_START = n.bleedoutMax;
Object.assign(tod, {
phase: "down",
restBleed: n.restBleed,
respawnSperre: n.respawnSperre,
emsGerufen: false,
sanitaeterDa: false,
});
renderDown();
}
else if(n.aktion === "zustand"){
/* Der Server ist die Wahrheit: Countdown und Sperre kommen von dort. */
if(tod.phase !== "down") return;
tod.restBleed = n.restBleed;
tod.respawnSperre = n.respawnSperre;
tod.emsGerufen = n.emsGerufen;
updateDown();
}
else if(n.aktion === "hinweis"){
const el = document.getElementById("hint");
if(el) el.textContent = n.text || "";
}
else if(n.aktion === "behandelt"){
tod.phase = "behandelt";
renderBehandelt();
}
else if(n.aktion === "krankenhaus"){
tod.phase = "krankenhaus";
if(n.kosten) KRANKENHAUS_KOSTEN = n.kosten;
const grund = (n.grund === "verblutet")
? "Niemand hat dich rechtzeitig erreicht — du bist verblutet."
: "Du hast aufgegeben und wachst in der Notaufnahme wieder auf.";
renderKrankenhaus(grund + (n.klinik ? " (" + n.klinik + ")" : ""));
// In FiveM übernimmt der Client das Weiterspielen — Knopf ausblenden
const weiter = document.getElementById("btnWeiter");
if(weiter) weiter.style.display = "none";
}
else if(n.aktion === "schliessen"){
document.body.style.display = "none";
}
});
/* In FiveM zunächst verstecken — die Seite wartet auf "oeffnen" */
if(IST_FIVEM){ document.body.classList.add("ingame"); document.body.style.display = "none"; }
/* =====================================================================
START
===================================================================== */
if(!IST_FIVEM) renderDown();
</script>
</body>
</html>