- 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>
773 lines
41 KiB
HTML
773 lines
41 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 — Inventar</title>
|
||
<style>
|
||
/* ===================== Design-Tokens ===================== */
|
||
:root {
|
||
--page-bg: #0a0b10;
|
||
--fg: #e9ecf4;
|
||
--muted: #838aa0;
|
||
--surface: rgba(24,26,36,0.72);
|
||
--surface2: rgba(36,39,53,0.8);
|
||
--slot: #14161f;
|
||
--slot-line: rgba(255,255,255,0.07);
|
||
--border: rgba(255,255,255,0.09);
|
||
--accent: #f5a623; /* warmer "Loot"-Akzent */
|
||
--accent-soft: rgba(245,166,35,0.16);
|
||
--red: #f0506e;
|
||
--green: #34d39a;
|
||
--amber: #f5b23d;
|
||
/* Seltenheits-Farben */
|
||
--r-common: #8b93a1;
|
||
--r-uncommon: #34d39a;
|
||
--r-rare: #4aa8ff;
|
||
--r-epic: #a56bff;
|
||
--r-legendary: #f5b23d;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; height: 100%; }
|
||
body {
|
||
min-height: 100%;
|
||
background:
|
||
radial-gradient(1100px 700px at 12% 8%, rgba(245,166,35,0.08), transparent 55%),
|
||
radial-gradient(1000px 800px at 88% 92%, rgba(74,168,255,0.07), transparent 55%),
|
||
var(--page-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: 32px 18px; overflow-x: hidden;
|
||
}
|
||
|
||
/* ===================== Fenster ===================== */
|
||
.inv {
|
||
position: relative;
|
||
width: min(1200px, 96vw);
|
||
aspect-ratio: 16 / 10;
|
||
background:
|
||
radial-gradient(120% 80% at 0% 0%, rgba(245,166,35,0.05), transparent 45%),
|
||
linear-gradient(160deg, rgba(18,20,28,0.92), rgba(10,11,16,0.95));
|
||
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
||
border: 1px solid var(--border); border-radius: 22px;
|
||
box-shadow: 0 50px 100px -30px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,255,255,0.03) inset;
|
||
display: flex; flex-direction: column; overflow: hidden;
|
||
}
|
||
|
||
.inv-head { display: flex; align-items: center; gap: 18px; padding: 16px 22px; border-bottom: 1px solid var(--border); }
|
||
.inv-head .logo { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center;
|
||
background: linear-gradient(150deg, #f5a623, #c9791a); color: #1a1206; font-weight: 800; }
|
||
.inv-head .title { font-size: 19px; font-weight: 800; letter-spacing: -0.02em; }
|
||
.inv-head .title small { display: block; font-size: 11.5px; font-weight: 500; color: var(--muted); letter-spacing: 0; }
|
||
.weight { margin-left: auto; display: flex; align-items: center; gap: 12px; }
|
||
.weight-bar { width: 200px; height: 9px; border-radius: 5px; background: var(--slot); overflow: hidden; border: 1px solid var(--slot-line); }
|
||
.weight-fill { height: 100%; border-radius: 5px; background: var(--accent); transition: width .3s ease, background .3s ease; }
|
||
.weight-txt { font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; min-width: 96px; text-align: right; }
|
||
|
||
.inv-body { flex: 1; display: grid; grid-template-columns: 232px 1fr 300px; gap: 0; min-height: 0; }
|
||
.col { padding: 16px 18px; min-height: 0; display: flex; flex-direction: column; }
|
||
.col.umgebung { border-right: 1px solid var(--border); }
|
||
.col.avatar-col { border-left: 1px solid var(--border); }
|
||
.panel-h { font-size: 11.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.07em;
|
||
color: var(--muted); margin: 4px 2px 10px; display: flex; align-items: center; gap: 8px; }
|
||
.panel-h:not(:first-child) { margin-top: 18px; }
|
||
|
||
/* ===================== Slots & Items ===================== */
|
||
.slots { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; overflow-y: auto; padding-right: 4px; align-content: start; }
|
||
.slots::-webkit-scrollbar { width: 7px; } .slots::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
|
||
.slot {
|
||
aspect-ratio: 1; border-radius: 12px; background: var(--slot); border: 1px solid var(--slot-line);
|
||
position: relative; display: grid; place-items: center; transition: border-color .12s, background .12s;
|
||
}
|
||
.slot.over { border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 2px var(--accent-soft); }
|
||
.slot .num { position: absolute; top: 5px; left: 7px; font-size: 11px; font-weight: 800; color: var(--muted); }
|
||
|
||
.item {
|
||
width: 100%; height: 100%; border-radius: 12px; position: relative; cursor: grab;
|
||
display: grid; place-items: center; background: linear-gradient(160deg, rgba(255,255,255,0.05), rgba(255,255,255,0.01));
|
||
border: 1.5px solid var(--rar, var(--r-common));
|
||
box-shadow: 0 0 14px -6px var(--rar, transparent) inset;
|
||
transition: transform .1s ease, box-shadow .12s;
|
||
}
|
||
.item:hover { box-shadow: 0 0 0 1px var(--rar), 0 8px 20px -8px rgba(0,0,0,0.6); }
|
||
.item.dragging { opacity: 0.35; }
|
||
.item .ic { font-size: 30px; line-height: 1; filter: drop-shadow(0 3px 5px rgba(0,0,0,0.5)); }
|
||
.item .qty { position: absolute; right: 5px; bottom: 4px; font-size: 12px; font-weight: 800;
|
||
background: rgba(8,9,13,0.85); border-radius: 6px; padding: 1px 6px; font-variant-numeric: tabular-nums; }
|
||
.item .wt { position: absolute; left: 5px; bottom: 4px; font-size: 9.5px; color: var(--muted); font-weight: 600; font-variant-numeric: tabular-nums; }
|
||
.item .asset { position: absolute; top: 5px; right: 6px; width: 8px; height: 8px; border-radius: 50%;
|
||
background: var(--accent); box-shadow: 0 0 8px var(--accent); }
|
||
.rar-common { --rar: var(--r-common); } .rar-uncommon { --rar: var(--r-uncommon); }
|
||
.rar-rare { --rar: var(--r-rare); } .rar-epic { --rar: var(--r-epic); } .rar-legendary { --rar: var(--r-legendary); }
|
||
|
||
/* ===================== Umgebung (links) ===================== */
|
||
.player { display: flex; align-items: center; gap: 10px; padding: 9px 11px; border-radius: 12px;
|
||
background: var(--surface); border: 1px solid var(--border); margin-bottom: 8px; transition: .12s; }
|
||
.player.over { border-color: var(--green); background: rgba(52,211,154,0.12); }
|
||
.player .pa { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; color: #fff; flex-shrink: 0; }
|
||
.player .pn { font-size: 13.5px; font-weight: 700; }
|
||
.player .pr { font-size: 11.5px; color: var(--muted); }
|
||
.player .give { margin-left: auto; font-size: 15px; opacity: 0.5; }
|
||
|
||
.boden { flex: 1; border: 1.5px dashed var(--border); border-radius: 14px; padding: 10px; overflow-y: auto;
|
||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; align-content: start; transition: .12s; min-height: 90px; }
|
||
.boden.over { border-color: var(--accent); background: var(--accent-soft); }
|
||
.boden .hint { grid-column: 1 / -1; color: var(--muted); font-size: 12px; text-align: center; align-self: center; padding: 18px 8px; line-height: 1.5; }
|
||
|
||
/* ===================== Avatar (rechts) ===================== */
|
||
.avatar-stage { position: relative; flex: 1; min-height: 0; display: grid; place-items: center; perspective: 900px; }
|
||
.mannequin { width: 130px; height: 240px; animation: turn 7s ease-in-out infinite; transform-style: preserve-3d; }
|
||
@keyframes turn { 0%,100% { transform: rotateY(-13deg); } 50% { transform: rotateY(13deg); } }
|
||
@keyframes floaty { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
|
||
@media (prefers-reduced-motion: reduce) { .mannequin { animation: none; } }
|
||
.platform { position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%); width: 150px; height: 30px;
|
||
border-radius: 50%; background: radial-gradient(ellipse, rgba(245,166,35,0.22), transparent 70%); }
|
||
|
||
.eq-slot { position: absolute; width: 58px; height: 58px; border-radius: 12px; background: var(--slot);
|
||
border: 1px solid var(--slot-line); display: grid; place-items: center; transition: .12s; }
|
||
.eq-slot.over { border-color: var(--accent); background: var(--accent-soft); }
|
||
.eq-slot .hintico { font-size: 22px; opacity: 0.28; }
|
||
.eq-slot .eqlabel { position: absolute; bottom: -16px; left: 0; right: 0; text-align: center; font-size: 9.5px;
|
||
color: var(--muted); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; }
|
||
.eq-slot .item { border-radius: 11px; }
|
||
.eq-helm { top: 4px; left: 50%; transform: translateX(-50%); }
|
||
.eq-schmuck{ top: 40px; right: 6px; }
|
||
.eq-brust { top: 40px; left: 6px; }
|
||
.eq-hose { top: 116px; left: 6px; }
|
||
.eq-schuhe { bottom: 30px; left: 50%; transform: translateX(-50%); }
|
||
|
||
.hotbar { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; }
|
||
.hot { aspect-ratio: 1; border-radius: 11px; background: var(--slot); border: 1px solid var(--slot-line);
|
||
position: relative; display: grid; place-items: center; transition: .12s; }
|
||
.hot.over { border-color: var(--accent); background: var(--accent-soft); }
|
||
.hot .num { position: absolute; top: 4px; left: 6px; font-size: 10px; font-weight: 800; color: var(--accent); }
|
||
|
||
/* ===================== Tooltip ===================== */
|
||
.tooltip { position: fixed; z-index: 200; pointer-events: none; display: none; max-width: 268px;
|
||
background: rgba(12,14,20,0.97); border: 1px solid var(--border); border-radius: 14px; padding: 13px 15px;
|
||
box-shadow: 0 20px 50px -12px rgba(0,0,0,0.8); }
|
||
.tt-name { font-size: 15px; font-weight: 800; letter-spacing: -0.01em; }
|
||
.tt-kat { font-size: 11.5px; color: var(--muted); font-weight: 600; margin-top: 1px; }
|
||
.tt-desc { font-size: 12.5px; color: #b9c0cf; line-height: 1.45; margin: 9px 0; }
|
||
.tt-row { display: flex; justify-content: space-between; gap: 12px; font-size: 12.5px; padding: 3px 0; }
|
||
.tt-row span:first-child { color: var(--muted); }
|
||
.tt-row .mono { font-family: ui-monospace, monospace; font-size: 11.5px; letter-spacing: 0.03em; }
|
||
.tt-assets { margin-top: 9px; border-top: 1px solid var(--border); padding-top: 9px; }
|
||
.tt-assets-h { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 800; margin-bottom: 6px; }
|
||
.chip { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600;
|
||
background: var(--surface2); border: 1px solid var(--border); border-radius: 999px; padding: 3px 9px; margin: 0 5px 5px 0; }
|
||
.chip::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
|
||
.tt-fg-common { color: var(--r-common); } .tt-fg-uncommon { color: var(--r-uncommon); }
|
||
.tt-fg-rare { color: var(--r-rare); } .tt-fg-epic { color: var(--r-epic); } .tt-fg-legendary { color: var(--r-legendary); }
|
||
|
||
/* ===================== Rechtsklick-Menü ===================== */
|
||
.ctx { position: absolute; z-index: 210; min-width: 168px; background: rgba(14,16,22,0.98);
|
||
border: 1px solid var(--border); border-radius: 12px; padding: 5px; display: none;
|
||
box-shadow: 0 20px 50px -12px rgba(0,0,0,0.8); }
|
||
.ctx.show { display: block; animation: ctxin .12s ease; }
|
||
@keyframes ctxin { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }
|
||
.ctx-head { padding: 8px 10px 6px; font-size: 12.5px; font-weight: 800; border-bottom: 1px solid var(--border); margin-bottom: 4px; }
|
||
.ctx button { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; border: none;
|
||
background: none; color: var(--fg); font-family: inherit; font-size: 13.5px; font-weight: 600; padding: 9px 10px;
|
||
border-radius: 8px; cursor: pointer; }
|
||
.ctx button:hover { background: var(--surface2); }
|
||
.ctx button.danger { color: var(--red); }
|
||
.ctx button .k { margin-left: auto; font-size: 16px; opacity: 0.7; }
|
||
|
||
/* ===================== Split-Modal ===================== */
|
||
.overlay { position: absolute; inset: 0; z-index: 220; background: rgba(6,7,11,0.62); backdrop-filter: blur(4px);
|
||
display: none; align-items: center; justify-content: center; }
|
||
.overlay.show { display: flex; }
|
||
.modal { width: 360px; max-width: 90%; background: rgba(18,20,28,0.98); border: 1px solid var(--border);
|
||
border-radius: 18px; padding: 22px; box-shadow: 0 30px 70px -18px rgba(0,0,0,0.8); }
|
||
.modal h3 { margin: 0 0 4px; font-size: 18px; }
|
||
.modal p { margin: 0 0 18px; color: var(--muted); font-size: 13px; }
|
||
.split-val { text-align: center; font-size: 34px; font-weight: 800; font-variant-numeric: tabular-nums; margin-bottom: 4px; }
|
||
.split-sub { text-align: center; color: var(--muted); font-size: 12.5px; margin-bottom: 16px; }
|
||
input[type=range] { width: 100%; accent-color: var(--accent); height: 26px; }
|
||
.modal-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 18px; }
|
||
.btn { border: none; border-radius: 12px; padding: 12px; font-size: 14.5px; font-weight: 700; cursor: pointer; font-family: inherit; }
|
||
.btn.primary { background: var(--accent); color: #1a1206; }
|
||
.btn.ghost { background: var(--surface2); color: var(--fg); border: 1px solid var(--border); }
|
||
|
||
.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: 13px; font-size: 13.5px; font-weight: 600; z-index: 230; opacity: 0; pointer-events: none;
|
||
transition: .3s; box-shadow: 0 14px 34px -10px rgba(0,0,0,0.7); }
|
||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||
|
||
.hint-bar { position: fixed; bottom: 14px; left: 50%; transform: translateX(-50%); z-index: 5;
|
||
font-size: 12px; color: #6f7789; text-align: center; }
|
||
.hint-bar b { color: #9aa2b4; }
|
||
</style>
|
||
|
||
<div class="inv" id="inv">
|
||
<div class="inv-head">
|
||
<span class="logo">Æ</span>
|
||
<div class="title">Inventar<small>Max Weber · Rucksack</small></div>
|
||
<div class="weight">
|
||
<div class="weight-bar"><div class="weight-fill" id="weight-fill"></div></div>
|
||
<div class="weight-txt" id="weight-txt">0 / 30 kg</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="inv-body">
|
||
<!-- Links: Umgebung -->
|
||
<div class="col umgebung">
|
||
<div class="panel-h">Spieler in der Nähe</div>
|
||
<div id="players"></div>
|
||
<div class="panel-h">Boden</div>
|
||
<div class="boden" id="boden"></div>
|
||
</div>
|
||
|
||
<!-- Mitte: Inventar-Grid -->
|
||
<div class="col grid-col">
|
||
<div class="panel-h">Tasche <span style="margin-left:auto;font-weight:600;text-transform:none;letter-spacing:0" id="slots-count"></span></div>
|
||
<div class="slots" id="slots"></div>
|
||
</div>
|
||
|
||
<!-- Rechts: Avatar + Ausrüstung + Hotbar -->
|
||
<div class="col avatar-col">
|
||
<div class="panel-h">Charakter & Ausrüstung</div>
|
||
<div class="avatar-stage">
|
||
<div class="platform"></div>
|
||
<svg class="mannequin" viewBox="0 0 130 240" fill="none">
|
||
<defs>
|
||
<linearGradient id="body" x1="0" y1="0" x2="1" y2="1">
|
||
<stop offset="0%" stop-color="#3a4152"/><stop offset="55%" stop-color="#262b38"/><stop offset="100%" stop-color="#171a24"/>
|
||
</linearGradient>
|
||
</defs>
|
||
<ellipse cx="65" cy="34" rx="19" ry="21" fill="url(#body)" stroke="rgba(255,255,255,0.08)"/>
|
||
<path d="M46 66 q19 -10 38 0 l6 60 q-25 12 -50 0 Z" fill="url(#body)" stroke="rgba(255,255,255,0.08)"/>
|
||
<path d="M46 68 q-14 6 -18 46 l12 5 q6 -34 12 -44 Z" fill="url(#body)" stroke="rgba(255,255,255,0.06)"/>
|
||
<path d="M84 68 q14 6 18 46 l-12 5 q-6 -34 -12 -44 Z" fill="url(#body)" stroke="rgba(255,255,255,0.06)"/>
|
||
<path d="M50 128 q15 8 30 0 l-2 82 l-11 0 l-3 -60 l-3 60 l-11 0 Z" fill="url(#body)" stroke="rgba(255,255,255,0.07)"/>
|
||
</svg>
|
||
<div class="eq-slot eq-helm" data-eq="helm"><span class="eqlabel">Helm</span></div>
|
||
<div class="eq-slot eq-schmuck" data-eq="schmuck"><span class="eqlabel">Schmuck</span></div>
|
||
<div class="eq-slot eq-brust" data-eq="brust"><span class="eqlabel">Brust</span></div>
|
||
<div class="eq-slot eq-hose" data-eq="hose"><span class="eqlabel">Hose</span></div>
|
||
<div class="eq-slot eq-schuhe" data-eq="schuhe"><span class="eqlabel">Schuhe</span></div>
|
||
</div>
|
||
<div class="panel-h">Schnellzugriff (Hotbar)</div>
|
||
<div class="hotbar" id="hotbar"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Overlays -->
|
||
<div class="ctx" id="ctx"></div>
|
||
<div class="overlay" id="overlay"></div>
|
||
<div class="toast" id="toast"></div>
|
||
</div>
|
||
|
||
<div class="hint-bar">
|
||
<b>Ziehen</b> zum Verschieben · auf <b>Boden</b> = fallenlassen · auf <b>Spieler</b> = geben · <b>Rechtsklick</b> für Menü & Splitten · <b>Hover</b> für Details
|
||
</div>
|
||
|
||
<div class="tooltip" id="tooltip"></div>
|
||
|
||
<script>
|
||
"use strict";
|
||
|
||
/* =====================================================================
|
||
ITEM-DEFINITIONEN (Registry) — Name, Icon, Kategorie, Gewicht, Seltenheit …
|
||
komponenten = fest verbaute Assets (z. B. Handy → SIM + Akku)
|
||
waffe = besitzt Seriennummer & Aufsätze (Attachments)
|
||
slot = ausrüstbar in Ausrüstungs-Slot; verbrauchbar = wird bei Nutzung -1
|
||
===================================================================== */
|
||
/* Item-Registry. In FiveM wird sie vom Server überschrieben, damit Server und
|
||
Oberfläche garantiert dieselben Werte verwenden. */
|
||
let itemDefs = {
|
||
pistole: { name: "Pistole P99", icon: "🔫", kat: "Waffe", gewicht: 1.2, rar: "epic", waffe: true,
|
||
beschreibung: "Halbautomatische 9mm-Pistole. Registrierte Feuerwaffe." },
|
||
munition9: { name: "9mm Munition", icon: "🧨", kat: "Munition", gewicht: 0.01, rar: "common", stapelbar: true,
|
||
beschreibung: "Standard-Patronen für 9mm-Waffen." },
|
||
handy: { name: "Smartphone", icon: "📱", kat: "Elektronik", gewicht: 0.3, rar: "rare",
|
||
komponenten: ["SIM-Karte", "Akku"], nutzbar: true,
|
||
beschreibung: "Modernes Smartphone. Enthält verbaute Komponenten." },
|
||
wasser: { name: "Wasserflasche", icon: "🥤", kat: "Nahrung", gewicht: 0.5, rar: "common", stapelbar: true, verbrauchbar: true, nutzbar: true,
|
||
beschreibung: "Löscht den Durst. +25 Durst." },
|
||
sandwich: { name: "Sandwich", icon: "🥪", kat: "Nahrung", gewicht: 0.2, rar: "common", stapelbar: true, verbrauchbar: true, nutzbar: true,
|
||
beschreibung: "Stillt den Hunger. +30 Hunger." },
|
||
verband: { name: "Verband", icon: "🩹", kat: "Medizin", gewicht: 0.1, rar: "uncommon", stapelbar: true, verbrauchbar: true, nutzbar: true,
|
||
beschreibung: "Stoppt Blutungen und heilt leichte Wunden." },
|
||
tablette: { name: "Schmerztabletten", icon: "💊", kat: "Medizin", gewicht: 0.02, rar: "common", stapelbar: true, verbrauchbar: true, nutzbar: true,
|
||
beschreibung: "Lindert Schmerzen kurzfristig." },
|
||
funk: { name: "Funkgerät", icon: "📻", kat: "Elektronik", gewicht: 0.4, rar: "uncommon", nutzbar: true,
|
||
beschreibung: "Kurzstrecken-Funk für die Kommunikation." },
|
||
dietrich: { name: "Dietrich", icon: "🗝️", kat: "Werkzeug", gewicht: 0.05, rar: "uncommon", stapelbar: true,
|
||
beschreibung: "Zum Öffnen einfacher Schlösser. Kann brechen." },
|
||
reparaturkit:{ name: "Reparaturkit", icon: "🧰", kat: "Werkzeug", gewicht: 2.0, rar: "uncommon", nutzbar: true,
|
||
beschreibung: "Repariert Fahrzeuge notdürftig." },
|
||
bargeld: { name: "Bargeld", icon: "💵", kat: "Wertsache", gewicht: 0, rar: "legendary", stapelbar: true,
|
||
beschreibung: "Sauberes Bargeld." },
|
||
goldkette: { name: "Goldkette", icon: "📿", kat: "Schmuck", gewicht: 0.15, rar: "epic", slot: "schmuck",
|
||
beschreibung: "Massive Goldkette. Ein echter Hingucker." },
|
||
helm: { name: "Ballistikhelm", icon: "⛑️", kat: "Ausrüstung", gewicht: 1.0, rar: "uncommon", slot: "helm",
|
||
beschreibung: "Schützt den Kopf vor Treffern." },
|
||
weste: { name: "Schutzweste", icon: "🦺", kat: "Ausrüstung", gewicht: 3.5, rar: "rare", slot: "brust",
|
||
beschreibung: "Kugelsichere Weste. Reduziert Schaden am Oberkörper." },
|
||
jeans: { name: "Jeans", icon: "👖", kat: "Kleidung", gewicht: 0.6, rar: "common", slot: "hose",
|
||
beschreibung: "Robuste Jeans." },
|
||
sneaker: { name: "Sneaker", icon: "👟", kat: "Kleidung", gewicht: 0.8, rar: "common", slot: "schuhe",
|
||
beschreibung: "Bequeme Sneaker." }
|
||
};
|
||
|
||
// Icon-Hinweise für leere Ausrüstungs-Slots
|
||
const slotHinweis = { helm: "⛑️", brust: "🦺", hose: "👖", schuhe: "👟", schmuck: "💍" };
|
||
const slotNamen = { helm: "Helm", brust: "Brust", hose: "Hose", schuhe: "Schuhe", schmuck: "Schmuck" };
|
||
const rarNamen = { common: "Gewöhnlich", uncommon: "Ungewöhnlich", rare: "Selten", epic: "Episch", legendary: "Legendär" };
|
||
|
||
let MAX_GEWICHT = 30; // kg — kommt vom Server
|
||
const SLOTS = 25; // Größe der Tasche
|
||
|
||
/* =====================================================================
|
||
ZUSTAND — die verschiedenen "Orte", an denen Items liegen können
|
||
===================================================================== */
|
||
/* Alle Behälter werden vom Server befüllt — die Oberfläche hält nur eine
|
||
Anzeigekopie und entscheidet selbst nichts. */
|
||
let inventar = new Array(SLOTS).fill(null);
|
||
let ausruestung = { helm: null, brust: null, hose: null, schuhe: null, schmuck: null };
|
||
let hotbar = new Array(5).fill(null);
|
||
let boden = []; // [{ bodenId, def, menge, meta }]
|
||
let spielerNah = []; // [{ id, bekannt }]
|
||
let gewichtVomServer = 0;
|
||
|
||
// Slotnummern der Ausrüstungsplätze (kommen vom Server)
|
||
let EQUIP_SLOTS = { helm: 200, brust: 201, hose: 202, schuhe: 203, schmuck: 204 };
|
||
const HOTBAR_BASIS = 100;
|
||
|
||
/* Anzeige-Daten eines Spielers in der Nähe.
|
||
WICHTIG (RP): Der Server schickt bewusst keine Namen mit — im Rollenspiel
|
||
kennt man eine Person erst nach Ausweis oder Visitenkarte. */
|
||
function spielerAnzeige(sp) {
|
||
return sp.bekannt
|
||
? { label: sp.name, sub: sp.rolle || "", av: (sp.name || "?").substring(0, 1), farbe: "#4aa8ff" }
|
||
: { label: "Unbekannt", sub: "ID #" + sp.id, av: "?", farbe: "#3a3f4d" };
|
||
}
|
||
|
||
/* =====================================================================
|
||
HILFSFUNKTIONEN
|
||
===================================================================== */
|
||
const $ = id => document.getElementById(id);
|
||
function sicher(t) { const d = document.createElement("div"); d.textContent = t; return d.innerHTML; }
|
||
// Gewicht deutsch als kg
|
||
function kg(x) { return x.toLocaleString("de-DE", { minimumFractionDigits: x < 10 ? 1 : 0, maximumFractionDigits: 2 }) + " kg"; }
|
||
|
||
let toastTimer = null;
|
||
function toast(text) {
|
||
const t = $("toast"); t.textContent = text; t.classList.add("show");
|
||
clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.remove("show"), 2000);
|
||
}
|
||
|
||
/* =====================================================================
|
||
ORTE LESEN / SCHREIBEN (inv | equip | hotbar | boden)
|
||
===================================================================== */
|
||
function leseOrt(o) {
|
||
if (o.typ === "inv") return inventar[o.i];
|
||
if (o.typ === "equip") return ausruestung[o.key];
|
||
if (o.typ === "hotbar") return hotbar[o.i];
|
||
if (o.typ === "boden") return boden[o.i];
|
||
return null;
|
||
}
|
||
/* Schreibende Helfer gibt es bewusst nicht mehr: Der Zustand wird
|
||
ausschließlich vom Server gesetzt (siehe setzeZustand). */
|
||
function gleicherOrt(a, b) { return a.typ === b.typ && a.i === b.i && a.key === b.key; }
|
||
|
||
// erster freier Tasche-Slot (für Splitten)
|
||
function freierSlot() { return inventar.findIndex(s => s === null); }
|
||
|
||
/* =====================================================================
|
||
VERSCHIEBEN (Drag & Drop-Kernlogik)
|
||
===================================================================== */
|
||
let zieht = null; // aktuell gezogener Ort-Deskriptor
|
||
|
||
/* Rechnet einen Ort-Deskriptor in die Slotnummer des Servers um.
|
||
Tasche 0–24, Hotbar 100–104, Ausrüstung 200–204. */
|
||
function ortZuSlot(o) {
|
||
if (o.typ === "inv") return o.i;
|
||
if (o.typ === "hotbar") return HOTBAR_BASIS + o.i;
|
||
if (o.typ === "equip") return EQUIP_SLOTS[o.key];
|
||
return null;
|
||
}
|
||
|
||
/* Verschieben: Die Oberfläche entscheidet nichts selbst, sondern schickt den
|
||
Wunsch an den Server. Dieser prüft und antwortet mit dem neuen Zustand. */
|
||
function verschiebe(quelle, ziel) {
|
||
if (gleicherOrt(quelle, ziel)) return;
|
||
const a = leseOrt(quelle);
|
||
if (!a) return;
|
||
|
||
// Vom Boden in die Tasche ziehen = aufheben
|
||
if (quelle.typ === "boden") {
|
||
nui("aufheben", { bodenId: a.bodenId });
|
||
return;
|
||
}
|
||
// Auf den Boden ziehen = fallenlassen
|
||
if (ziel.typ === "boden") {
|
||
nui("fallenlassen", { slot: ortZuSlot(quelle), menge: a.menge });
|
||
return;
|
||
}
|
||
// Auf einen Spieler ziehen = geben
|
||
if (ziel.typ === "spieler") {
|
||
nui("geben", { slot: ortZuSlot(quelle), menge: a.menge, zielId: ziel.id });
|
||
return;
|
||
}
|
||
|
||
const vonSlot = ortZuSlot(quelle), nachSlot = ortZuSlot(ziel);
|
||
if (vonSlot === null || nachSlot === null) return;
|
||
nui("verschieben", { von: vonSlot, nach: nachSlot });
|
||
}
|
||
|
||
/* =====================================================================
|
||
RENDERN
|
||
===================================================================== */
|
||
function render() {
|
||
// Gewicht: in FiveM rechnet der Server, im Browser rechnen wir selbst
|
||
let gewicht = gewichtVomServer;
|
||
if (!IST_FIVEM) {
|
||
gewicht = 0;
|
||
const zaehle = it => { if (it) gewicht += it.def.gewicht * it.menge; };
|
||
inventar.forEach(zaehle); Object.values(ausruestung).forEach(zaehle); hotbar.forEach(zaehle);
|
||
}
|
||
const proz = Math.min(100, gewicht / MAX_GEWICHT * 100);
|
||
const fill = $("weight-fill");
|
||
fill.style.width = proz + "%";
|
||
fill.style.background = proz > 90 ? "var(--red)" : (proz > 70 ? "var(--amber)" : "var(--accent)");
|
||
$("weight-txt").textContent = gewicht.toLocaleString("de-DE", { maximumFractionDigits: 1 }) + " / " + MAX_GEWICHT + " kg";
|
||
$("slots-count").textContent = inventar.filter(Boolean).length + " / " + SLOTS;
|
||
|
||
// Tasche
|
||
const slots = $("slots"); slots.innerHTML = "";
|
||
for (let i = 0; i < SLOTS; i++) {
|
||
const slot = document.createElement("div");
|
||
slot.className = "slot";
|
||
dropZiel(slot, { typ: "inv", i });
|
||
if (inventar[i]) slot.appendChild(baueItem(inventar[i], { typ: "inv", i }));
|
||
slots.appendChild(slot);
|
||
}
|
||
|
||
// Ausrüstung (Slots sind persistent -> Drop wird einmalig in initDrop() verdrahtet)
|
||
document.querySelectorAll(".eq-slot").forEach(el => {
|
||
const key = el.dataset.eq;
|
||
el.querySelectorAll(".item").forEach(n => n.remove());
|
||
const alt = el.querySelector(".hintico"); if (alt) alt.remove();
|
||
if (ausruestung[key]) el.insertBefore(baueItem(ausruestung[key], { typ: "equip", key }), el.firstChild);
|
||
else { const h = document.createElement("span"); h.className = "hintico"; h.textContent = slotHinweis[key]; el.insertBefore(h, el.firstChild); }
|
||
});
|
||
|
||
// Hotbar
|
||
const hb = $("hotbar"); hb.innerHTML = "";
|
||
for (let i = 0; i < 5; i++) {
|
||
const h = document.createElement("div"); h.className = "hot";
|
||
h.innerHTML = '<span class="num">' + (i + 1) + "</span>";
|
||
dropZiel(h, { typ: "hotbar", i });
|
||
if (hotbar[i]) h.appendChild(baueItem(hotbar[i], { typ: "hotbar", i }));
|
||
hb.appendChild(h);
|
||
}
|
||
|
||
// Boden (Container ist persistent -> Drop wird einmalig in initDrop() verdrahtet)
|
||
const bd = $("boden"); bd.innerHTML = "";
|
||
if (boden.length === 0) bd.innerHTML = '<div class="hint">Items hierher ziehen,<br>um sie fallenzulassen</div>';
|
||
else boden.forEach((it, i) => bd.appendChild(baueItem(it, { typ: "boden", i })));
|
||
|
||
// Spieler (anonymisiert, solange unbekannt)
|
||
const pl = $("players"); pl.innerHTML = "";
|
||
spielerNah.forEach(sp => {
|
||
const a = spielerAnzeige(sp);
|
||
const row = document.createElement("div"); row.className = "player";
|
||
row.innerHTML = '<span class="pa" style="background:' + a.farbe + '">' + a.av + '</span>' +
|
||
'<div><div class="pn">' + sicher(a.label) + '</div><div class="pr">' + sicher(a.sub) + '</div></div>' +
|
||
'<span class="give">🤝</span>';
|
||
dropZiel(row, { typ: "spieler", id: sp.id });
|
||
pl.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// Drop-Ziel verdrahten
|
||
function dropZiel(el, ziel) {
|
||
el.addEventListener("dragover", e => { e.preventDefault(); el.classList.add("over"); });
|
||
el.addEventListener("dragleave", () => el.classList.remove("over"));
|
||
el.addEventListener("drop", e => {
|
||
e.preventDefault(); el.classList.remove("over");
|
||
if (zieht) { verschiebe(zieht, ziel); zieht = null; }
|
||
});
|
||
}
|
||
|
||
// Ein Item-Element (draggable, mit Hover-Tooltip & Rechtsklick) bauen
|
||
function baueItem(item, ort) {
|
||
const d = item.def;
|
||
const el = document.createElement("div");
|
||
el.className = "item rar-" + (d.rar || "common");
|
||
el.draggable = true;
|
||
const hatAssets = (d.komponenten && d.komponenten.length) || (item.meta && item.meta.attachments && item.meta.attachments.length);
|
||
el.innerHTML =
|
||
'<span class="ic">' + d.icon + "</span>" +
|
||
(item.menge > 1 ? '<span class="qty">' + item.menge + "</span>" : "") +
|
||
'<span class="wt">' + kg(d.gewicht * item.menge) + "</span>" +
|
||
(hatAssets ? '<span class="asset" title="Enthält Komponenten"></span>' : "");
|
||
el.addEventListener("dragstart", e => { zieht = ort; el.classList.add("dragging"); versteckeTooltip(); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", d.name); });
|
||
el.addEventListener("dragend", () => el.classList.remove("dragging"));
|
||
// Doppelklick: benutzen (bzw. Ausrüstung anlegen)
|
||
el.addEventListener("dblclick", () => {
|
||
versteckeTooltip();
|
||
if (d.nutzbar) benutze(item, ort);
|
||
else if (d.slot && ort.typ !== "equip") verschiebe(ort, { typ: "equip", key: d.slot });
|
||
else toast(d.name + " lässt sich nicht benutzen");
|
||
});
|
||
el.addEventListener("mouseenter", e => zeigeTooltip(item, e));
|
||
el.addEventListener("mousemove", bewegeTooltip);
|
||
el.addEventListener("mouseleave", versteckeTooltip);
|
||
el.addEventListener("contextmenu", e => { e.preventDefault(); versteckeTooltip(); zeigeKontext(item, ort, e); });
|
||
return el;
|
||
}
|
||
|
||
/* =====================================================================
|
||
TOOLTIP (alle Details + Gewicht + Asset-Komponenten / Waffen-Infos)
|
||
===================================================================== */
|
||
const tooltip = $("tooltip");
|
||
function zeigeTooltip(item, e) {
|
||
const d = item.def;
|
||
let h = '<div class="tt-name tt-fg-' + (d.rar || "common") + '">' + sicher(d.name) + "</div>";
|
||
h += '<div class="tt-kat">' + sicher(d.kat) + " · " + (rarNamen[d.rar] || "Gewöhnlich") + "</div>";
|
||
h += '<div class="tt-desc">' + sicher(d.beschreibung) + "</div>";
|
||
h += '<div class="tt-row"><span>Gewicht</span><span>' + kg(d.gewicht) + " / Stück</span></div>";
|
||
if (item.menge > 1) h += '<div class="tt-row"><span>Gesamt (' + item.menge + "×)</span><span>" + kg(d.gewicht * item.menge) + "</span></div>";
|
||
// Assets / verbaute Komponenten (nur was da ist, kein Status)
|
||
if (d.komponenten && d.komponenten.length)
|
||
h += '<div class="tt-assets"><div class="tt-assets-h">Verbaute Komponenten</div>' + d.komponenten.map(k => '<span class="chip">' + sicher(k) + "</span>").join("") + "</div>";
|
||
// Waffen-Infos: Seriennummer + Aufsätze
|
||
if (d.waffe) {
|
||
h += '<div class="tt-row"><span>Seriennummer</span><span class="mono">' + sicher(item.meta && item.meta.serie ? item.meta.serie : "unbekannt") + "</span></div>";
|
||
if (item.meta && item.meta.attachments && item.meta.attachments.length)
|
||
h += '<div class="tt-assets"><div class="tt-assets-h">Aufsätze</div>' + item.meta.attachments.map(a => '<span class="chip">' + sicher(a) + "</span>").join("") + "</div>";
|
||
}
|
||
tooltip.innerHTML = h; tooltip.style.display = "block"; bewegeTooltip(e);
|
||
}
|
||
function bewegeTooltip(e) {
|
||
const pad = 16, w = tooltip.offsetWidth, hgt = tooltip.offsetHeight;
|
||
let x = e.clientX + pad, y = e.clientY + pad;
|
||
if (x + w > window.innerWidth - 8) x = e.clientX - w - pad;
|
||
if (y + hgt > window.innerHeight - 8) y = e.clientY - hgt - pad;
|
||
tooltip.style.left = x + "px"; tooltip.style.top = y + "px";
|
||
}
|
||
function versteckeTooltip() { tooltip.style.display = "none"; }
|
||
|
||
/* =====================================================================
|
||
RECHTSKLICK-MENÜ (Benutzen, Ausrüsten, Splitten, Geben, Fallenlassen)
|
||
===================================================================== */
|
||
const ctx = $("ctx");
|
||
function zeigeKontext(item, ort, e) {
|
||
const d = item.def;
|
||
const aktionen = [];
|
||
if (d.nutzbar) aktionen.push({ label: "Benutzen", icon: "▶", fn: () => benutze(item, ort) });
|
||
if (d.slot) aktionen.push({ label: "Ausrüsten", icon: "🛡", fn: () => verschiebe(ort, { typ: "equip", key: d.slot }) });
|
||
if (ort.typ === "equip") aktionen.push({ label: "Ablegen", icon: "↩", fn: () => ablegen(ort) });
|
||
if (item.menge > 1) aktionen.push({ label: "Splitten…", icon: "⇔", fn: () => oeffneSplit(item, ort) });
|
||
aktionen.push({ label: "Geben…", icon: "🤝", fn: () => oeffneGeben(item, ort) });
|
||
aktionen.push({ label: "Fallenlassen", icon: "🗑", danger: true, fn: () => verschiebe(ort, { typ: "boden" }) });
|
||
|
||
ctx.innerHTML = '<div class="ctx-head">' + sicher(d.name) + "</div>" +
|
||
aktionen.map((a, i) => '<button data-i="' + i + '"' + (a.danger ? ' class="danger"' : "") + ">" +
|
||
sicher(a.label) + '<span class="k">' + a.icon + "</span></button>").join("");
|
||
// Position relativ zum Inventar-Fenster
|
||
const box = $("inv").getBoundingClientRect();
|
||
let x = e.clientX - box.left, y = e.clientY - box.top;
|
||
ctx.classList.add("show");
|
||
x = Math.min(x, box.width - ctx.offsetWidth - 8);
|
||
y = Math.min(y, box.height - ctx.offsetHeight - 8);
|
||
ctx.style.left = x + "px"; ctx.style.top = y + "px";
|
||
ctx.querySelectorAll("button").forEach(b => b.addEventListener("click", () => {
|
||
schliesseKontext(); aktionen[+b.dataset.i].fn();
|
||
}));
|
||
}
|
||
function schliesseKontext() { ctx.classList.remove("show"); }
|
||
document.addEventListener("click", schliesseKontext);
|
||
document.addEventListener("scroll", schliesseKontext, true);
|
||
|
||
/* =====================================================================
|
||
AKTIONEN
|
||
===================================================================== */
|
||
// Item benutzen — Wirkung entscheidet der Server
|
||
function benutze(item, ort) {
|
||
nui("benutzen", { slot: ortZuSlot(ort) });
|
||
}
|
||
// Ausrüstung ablegen -> in ersten freien Tasche-Slot
|
||
function ablegen(ort) {
|
||
const frei = freierSlot();
|
||
if (frei < 0) { toast("Kein Platz in der Tasche"); return; }
|
||
verschiebe(ort, { typ: "inv", i: frei });
|
||
}
|
||
|
||
// Splitten-Modal
|
||
function oeffneSplit(item, ort) {
|
||
if (item.menge < 2) return;
|
||
const mitte = Math.floor(item.menge / 2);
|
||
$("overlay").innerHTML =
|
||
'<div class="modal"><h3>Stapel teilen</h3><p>' + sicher(item.def.name) + " · " + item.menge + " Stück</p>" +
|
||
'<div class="split-val" id="sv">' + mitte + "</div>" +
|
||
'<div class="split-sub" id="ss">' + mitte + " abtrennen · " + (item.menge - mitte) + " bleibt</div>" +
|
||
'<input type="range" id="sr" min="1" max="' + (item.menge - 1) + '" value="' + mitte + '">' +
|
||
'<div class="modal-actions"><button class="btn ghost" id="sc">Abbrechen</button>' +
|
||
'<button class="btn primary" id="sok">Teilen</button></div></div>';
|
||
$("overlay").classList.add("show");
|
||
const sr = $("sr");
|
||
sr.addEventListener("input", () => {
|
||
$("sv").textContent = sr.value;
|
||
$("ss").textContent = sr.value + " abtrennen · " + (item.menge - sr.value) + " bleibt";
|
||
});
|
||
$("sc").addEventListener("click", schliesseOverlay);
|
||
$("sok").addEventListener("click", () => {
|
||
// Teilen übernimmt der Server (prüft freien Platz selbst)
|
||
nui("splitten", { slot: ortZuSlot(ort), menge: parseInt(sr.value, 10) });
|
||
schliesseOverlay();
|
||
});
|
||
}
|
||
|
||
// Geben-Modal (Spieler in der Nähe auswählen)
|
||
function oeffneGeben(item, ort) {
|
||
$("overlay").innerHTML =
|
||
'<div class="modal"><h3>Item geben</h3><p>' + sicher(item.def.name) + (item.menge > 1 ? " ×" + item.menge : "") + " an wen?</p>" +
|
||
'<div id="glist">' + spielerNah.map((sp, i) => { const a = spielerAnzeige(sp); return
|
||
'<button class="btn ghost" data-g="' + i + '" style="width:100%;display:flex;align-items:center;gap:10px;justify-content:flex-start;margin-bottom:8px">' +
|
||
'<span class="pa" style="width:28px;height:28px;background:' + a.farbe + '">' + a.av + "</span>" +
|
||
sicher(a.label) + " · " + sicher(a.sub) + "</button>"; }).join("") + "</div>" +
|
||
'<button class="btn ghost" id="gc" style="width:100%">Abbrechen</button></div>';
|
||
$("overlay").classList.add("show");
|
||
$("gc").addEventListener("click", schliesseOverlay);
|
||
$("glist").querySelectorAll("[data-g]").forEach(b => b.addEventListener("click", () => {
|
||
const sp = spielerNah[+b.dataset.g];
|
||
schliesseOverlay();
|
||
verschiebe(ort, { typ: "spieler", id: sp.id });
|
||
}));
|
||
}
|
||
function schliesseOverlay() { $("overlay").classList.remove("show"); $("overlay").innerHTML = ""; }
|
||
$("overlay").addEventListener("click", e => { if (e.target === $("overlay")) schliesseOverlay(); });
|
||
|
||
/* =====================================================================
|
||
START
|
||
===================================================================== */
|
||
// Drop-Ziele der persistenten Elemente (Ausrüstung + Boden) einmalig verdrahten
|
||
function initDrop() {
|
||
document.querySelectorAll(".eq-slot").forEach(el => dropZiel(el, { typ: "equip", key: el.dataset.eq }));
|
||
dropZiel($("boden"), { typ: "boden" });
|
||
}
|
||
|
||
/* =====================================================================
|
||
NUI-BRÜCKE — Verbindung zum FiveM-Client
|
||
===================================================================== */
|
||
const IST_FIVEM = (typeof GetParentResourceName === "function");
|
||
const RESSOURCE = IST_FIVEM ? GetParentResourceName() : "aether-inventory";
|
||
|
||
/* Schickt eine Aktion an den Client; der reicht sie an den Server weiter. */
|
||
function nui(aktion, daten) {
|
||
if (!IST_FIVEM) { toast("(Browser-Ansicht — keine Serververbindung)"); return Promise.resolve(); }
|
||
return fetch(`https://${RESSOURCE}/${aktion}`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json; charset=UTF-8" },
|
||
body: JSON.stringify(daten || {}),
|
||
}).catch(() => {});
|
||
}
|
||
|
||
/* Wandelt einen Servereintrag in ein Anzeige-Item um */
|
||
function baueEintrag(eintrag) {
|
||
const def = itemDefs[eintrag.item];
|
||
if (!def) return null; // unbekanntes Item überspringen
|
||
return { def, menge: eintrag.menge, meta: eintrag.meta || null, kennung: eintrag.item };
|
||
}
|
||
|
||
/* Übernimmt den kompletten Zustand vom Server */
|
||
function setzeZustand(daten) {
|
||
if (daten.items) itemDefs = daten.items;
|
||
if (daten.equipSlots) EQUIP_SLOTS = daten.equipSlots;
|
||
if (daten.maxGewicht) MAX_GEWICHT = daten.maxGewicht;
|
||
gewichtVomServer = daten.gewicht || 0;
|
||
|
||
// Behälter leeren und aus der flachen Slot-Tabelle neu aufbauen
|
||
inventar = new Array(SLOTS).fill(null);
|
||
hotbar = new Array(5).fill(null);
|
||
ausruestung = { helm: null, brust: null, hose: null, schuhe: null, schmuck: null };
|
||
|
||
const roh = daten.inventar || {};
|
||
for (const schluessel of Object.keys(roh)) {
|
||
const slot = parseInt(schluessel, 10);
|
||
const item = baueEintrag(roh[schluessel]);
|
||
if (!item) continue;
|
||
|
||
if (slot >= 200) {
|
||
// Ausrüstung: Slotnummer zurück auf den Schlüssel abbilden
|
||
const key = Object.keys(EQUIP_SLOTS).find(k => EQUIP_SLOTS[k] === slot);
|
||
if (key) ausruestung[key] = item;
|
||
} else if (slot >= HOTBAR_BASIS) {
|
||
hotbar[slot - HOTBAR_BASIS] = item;
|
||
} else if (slot >= 0 && slot < SLOTS) {
|
||
inventar[slot] = item;
|
||
}
|
||
}
|
||
|
||
// Boden-Items (jedes mit seiner Server-ID zum Aufheben)
|
||
boden = (daten.boden || []).map(b => {
|
||
const item = baueEintrag(b);
|
||
if (item) item.bodenId = b.id;
|
||
return item;
|
||
}).filter(Boolean);
|
||
|
||
spielerNah = daten.spieler || [];
|
||
render();
|
||
if (daten.hinweis) toast(daten.hinweis);
|
||
}
|
||
|
||
/* Nachrichten vom Client */
|
||
window.addEventListener("message", (ereignis) => {
|
||
const n = ereignis.data || {};
|
||
if (n.aktion === "oeffnen") {
|
||
document.body.style.display = "";
|
||
setzeZustand(n);
|
||
} else if (n.aktion === "zustand") {
|
||
setzeZustand(n);
|
||
} else if (n.aktion === "hinweis") {
|
||
toast(n.text || "");
|
||
} else if (n.aktion === "schliessen") {
|
||
document.body.style.display = "none";
|
||
schliesseOverlay(); schliesseKontext();
|
||
}
|
||
});
|
||
|
||
/* ESC schließt das Inventar */
|
||
document.addEventListener("keydown", (e) => {
|
||
if (e.key === "Escape" && IST_FIVEM) nui("schliessen", {});
|
||
});
|
||
|
||
/* =====================================================================
|
||
START
|
||
===================================================================== */
|
||
initDrop();
|
||
|
||
if (IST_FIVEM) {
|
||
document.body.style.display = "none"; // wartet auf "oeffnen"
|
||
render();
|
||
} else {
|
||
// Eigenständige Browser-Ansicht: Beispieldaten zum Prüfen des Layouts
|
||
const bsp = (id, menge, meta) => ({ def: itemDefs[id], menge, meta: meta || null, kennung: id });
|
||
inventar[0] = bsp("pistole", 1, { serie: "P99-8842-LS", attachments: ["Schalldämpfer", "Laservisier"] });
|
||
inventar[1] = bsp("munition9", 64);
|
||
inventar[2] = bsp("handy", 1);
|
||
inventar[3] = bsp("wasser", 3);
|
||
inventar[4] = bsp("sandwich", 2);
|
||
inventar[5] = bsp("verband", 5);
|
||
inventar[10] = bsp("bargeld", 1450);
|
||
ausruestung.brust = bsp("weste", 1);
|
||
ausruestung.hose = bsp("jeans", 1);
|
||
ausruestung.schuhe = bsp("sneaker", 1);
|
||
spielerNah = [{ id: 4821, bekannt: false }, { id: 1093, bekannt: false }];
|
||
render();
|
||
}
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|