Druckanzeige: - pressure * (Restluft / capacity). Bei konstantem Volumen ist der Druck proportional zur verbliebenen Gasmenge, die lineare Abbildung stimmt also - Fuelldruck pro Flasche in config/shared.lua, alle bei 300 bar. Dass die grosse Flasche denselben Druck zeigt wie die kleine ist gewollt - in echt macht das Volumen die Laufzeit, nicht der Druck - Unter config.reservePressure (50 bar) rot: in der Tauchpraxis die Schwelle, ab der man den Aufstieg einleitet HUD ausblenden fuer Unterwasser-Screenshots. qbx_core hat selbst keinen HUD-Toggle, es feuert nur hud:client:* an eine separate HUD-Resource - deshalb drei voneinander unabhaengige Wege: - IsHudHidden() folgen, greift bei allem was DisplayHud(false) setzt - eigene Taste, standardmaessig unbelegt - Export setHudVisible(bool) / isHudVisible() fuer andere Resources Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 KiB
d4rk_divegear — Umbauplan
Fork von Qbox-project/qbx_divegear (v1.0.1).
Die Upstream-Historie ist vollständig erhalten und liegt auf dem Remote upstream,
damit spätere Upstream-Änderungen mergebar bleiben.
Ziel: Gesyncte Taschenlampe, verschiedene Flaschengrößen mit echter Laufzeit, Tiefenanzeige, und Ausrüstung die bei Animationen nicht mehr verschwindet.
Ausgangslage (Analyse des Originals)
client/main.lua hat 173 Zeilen, server/main.lua 10. Drei strukturelle Probleme,
die die neuen Features blockieren:
- Sauerstoff ist eine lokale Client-Variable (
local oxygenLevel = 0) — nicht persistent, nicht am Item hängend, weg bei Relog/Restart. Blockiert Flaschengrößen. - Laufzeit-Rechenfehler:
startingOxygenLevel = 100beidecayRate = 1pro Sekunde ergibt 100 Sekunden, nicht Minuten. Die VergleicheoxygenLevel % 10 == 0undoxygenLevel == 0(client/main.lua:121, :124) brechen, sobalddecayRatefraktional wird. - Attachment ist soft-pinned (client/main.lua:70, :75) — die Physik-Engine darf die Props bei Ragdoll/Anims lösen. Kein Ped-Swap-Handling, kein Watchdog.
Phasen
Reihenfolge ist bewusst gewählt: Phase 1 und 2 sind Fundament, Phase 3–5 bauen darauf auf.
Phase 1 — Gear-Persistenz (Bugfix, ~1 h)
Zuerst, weil kleinster Aufwand, echter Bug, und die Taschenlampe dieselbe Attach-Logik nutzt.
attachGear()auf harte Attachment umstellen:useSoftPinning = false,p9 = false,syncRot = true— ausgelagert in einenattachProp()-Helper, den Phase 5 für die Lampe mitbenutzt. Laut Native-Doku heißtuseSoftPinningfalse: "attached entity will not detach when fixed" — genau das wollen wirisGearIntact()Helper:DoesEntityExist+GetEntityAttachedTo(...) == cache.pedfür beide Props- Watchdog-Thread (
gearWatchdogIntervalMs, default 1000 ms) solangecurrentGear.enabled: bei DefektdeleteGear()+attachGear(), undenableScuba()neu setzen (SetEnableScuba/SetPedMaxTimeUnderwaterwerden bei Respawn/Fahrzeugausstieg vom Spiel zurückgesetzt) lib.onCache('ped', ...)→ bei Model-Wechsel (Kleiderladen, Skin-Change) Props neu anhängen, Scuba-State neu setzen- Death-Handler:
IsPedDeadOrDying-Check im Watchdog statt eigenem Event — keine Abhängigkeit zu qbx_medical o.ä. Sichern der Restluft kommt in Phase 2 dazu - Gemeinsamer
removeGear(outOfAir)-Teardown für Ablegen, leere Flasche und Tod onResourceStopCleanup-Handler für Props
Zu SetPedMaxTimeUnderwater: war doch kein Bug. Die 50.0 beim Ablegen und die
1.0 bei leerer Flasche sind zwei verschiedene Absichten (normal weiterschwimmen vs.
ohne Luft absaufen). Beide Werte sind jetzt als maxTimeUnderwater /
maxTimeUnderwaterOutOfAir in der Config statt als Magic Number im Code.
Phase 2 — Sauerstoff-Logik neu (~1 h)
- Umstellung von "Punkte" auf Restsekunden (
oxygenSeconds) - Decrementer-Thread auf
GetGameTimer()-Delta statt gezählterWait(1000)(driftfrei, überlebt Frame-Drops) - Float-Vergleiche raus, Warnschwellen explizit als Config-Liste
(z.B.
warnAtSeconds = { 60, 30, 10 }), jede Schwelle einmal auslösen - Anzeige als
mm:ssstatt roher Zahl - Persistenz: Restluft alle ~15 s und beim Ablegen per Server-Event in die
ox_inventory-Metadata des Tank-Items zurückschreiben
(
exports.ox_inventory:SetMetadata) - Beim Anlegen Metadata lesen → Restluft übernehmen
- Offen: Beim Spawn/Resource-Start die angelegte Ausrüstung automatisch wieder anziehen. Die Restluft überlebt bereits (liegt auf der Flasche), aber nach einem Relog steht man ohne Maske da und muss sie neu anlegen
Serverseitig validieren: Der Client schickt den O2-Wert. Server prüft Plausibilität (nur Abnahme, nicht mehr als seit letztem Update vergehen konnte), sonst ist das ein trivialer Cheat-Vektor.
Phase 3 — Flaschengrößen (~1 h, nach Phase 2 trivial)
Vier Items mit unterschiedlicher capacity in Sekunden:
| Item | Laufzeit | capacity |
|---|---|---|
diving_tank_small |
5 min | 300 |
diving_tank_medium |
10 min | 600 |
diving_tank_large |
15 min | 900 |
diving_tank_xl |
20 min | 1200 |
capacityinconfig/shared.lua(Client und Server brauchen sie), Label und Gewicht im Item-Snippet. Kein Prop-Scale — siehe Optik-Hinweis untenitems_for_ox_inventory.luaals Snippet-Datei zum manuellen Reinkopieren inox_inventory/data/items.lua— keinRegisterItem-Exportmetadata.oxygen(Restsekunden) +metadata.durability(Prozent, damit der Füllstand direkt im Inventar-Slot sichtbar ist)diving_fillfüllt biscapacitydes jeweiligen Tanks statt auf Fixwert- Verbrauch skaliert mit Tiefe:
decayRate * (1 + depth / 30)— physikalisch korrekt und macht große Flaschen erst sinnvoll (per Config abschaltbar)
Optik: p_s_scuba_tank_s ist das einzige Basegame-Tank-Prop. Größenunterschied
über SetEntityScale ist bei attached Objects unzuverlässig — entweder akzeptieren,
dass alle vier gleich aussehen, oder Custom-Model. Für v1: gleich aussehen.
Phase 4 — NUI-HUD: Tiefe + Sauerstoff (~2,5 h)
Tiefenanzeige und O2-Anzeige zusammen als ein NUI-Overlay — qbx.drawText2d
(aktuell oxygenLevel..'⏱' in der Bildschirmecke) fliegt raus.
Datenseite (~30 min):
- Tiefe = Wasseroberfläche − Ped-Z, via
GetWaterHeight(FallbackGetWaterHeightNoWaves) - Wert geglättet (Lerp) damit die Zahl nicht zappelt
- Liefert gleichzeitig den Faktor für den tiefenabhängigen Verbrauch aus Phase 3
NUI (~2 h):
- O2 als Balken/Ring +
mm:ssRestzeit, Farbwechsel an den Warnschwellen aus Phase 2, Pulsieren im kritischen Bereich - Tiefe in Metern, Warnfarbe ab Config-Tiefe
- Slot für den Lampen-Status (wird in Phase 5 befüllt)
- Sichtbar nur wenn Gear angelegt und unter Wasser, mit Fade-In/Out
- Updates gedrosselt per
SendNUIMessage(~4/s statt jeden Frame) - Kein
backdrop-filter(bekannter FiveM-Bug → schwarzer Hintergrund) hudEnabled,hudUpdateIntervalMsunddeepWarningDepthinconfig/client.lua- Nachgereicht: Manometer in bar.
pressure * (Restluft / capacity)— bei konstantem Volumen ist der Druck proportional zur verbliebenen Gasmenge, die lineare Abbildung ist also korrekt. Alle Flaschen starten bei 300 bar; dass die große Flasche denselben Druck zeigt wie die kleine, ist gewollt (in echt macht das Volumen die Laufzeit). UnterreservePressurewird rot eingefärbt - Nachgereicht: HUD ausblendbar für Unterwasser-Screenshots. Drei Wege,
weil
qbx_coreselbst keinen HUD-Toggle hat (es feuert nurhud:client:*an eine separate HUD-Resource):IsHudHidden()folgen, eigene Taste, und der ExportsetHudVisible(bool) - Offen: Position und Größe des HUD sind bisher nur in
nui/style.csseinstellbar, nicht in der Config
Phase 5 — Gesyncte Taschenlampe (~2–3 h)
Licht ist in GTA nie synchronisiert — jeder Client zeichnet das Licht für jeden Taucher in seiner Nähe selbst.
- Torch-Prop mit derselben harten Attach-Methode aus Phase 1 anhängen (Kopf-Bone oder Hand, Config)
- Toggle per
RegisterKeyMapping/lib.addKeybind - State replizieren:
LocalPlayer.state:set('divelight', true, true)— Statebag statt Event-Spam - Draw-Thread: über
GetActivePlayers(),Player(serverId).state.divelightprüfen,DrawSpotLightWithShadowzeichnen - Richtung anders gelöst als geplant: statt Prop-Matrix und Objekt-Pool
werden zwei bone-relative Punkte am Kopf-Bone benutzt. Die Offsets von
GetPedBoneCoordssind laut Native-Doku "relative to the bone's rotation", die Differenz zweier solcher Punkte ergibt also die Blickrichtung inklusive Pitch. Das löst das Problem (GetEntityForwardVectorwürde beim waagerecht im Wasser liegenden Ped stur horizontal leuchten), ohne fremde Props überGetGamePool('CObject')suchen und cachen zu müssen — der Renderer braucht dadurch überhaupt keinen Prop-Zugriff und kommt mit dem Statebag allein aus - Prop-Model wird über
IsModelValid/IsModelInCdimagegeprüft, weillib.requestModelbei ungültigen Models einen Error wirft. Ein falscher Prop-Name kostet so nur die Optik, nicht die ganze Ausrüstung - Performance: Distanzfilter ~40–50 m, max. 3–4 gleichzeitige Lichter
(
DrawSpotLightWithShadowist teuer) - Lampen-Status (An/Aus) in das HUD aus Phase 4 einhängen — keine Batterie, die Lampe läuft solange Sauerstoff da ist
Zielstruktur
Die Config bleibt beim Upstream-Pattern (config/client.lua + require) statt beim
shared_scripts-Template, damit spätere Upstream-Änderungen mergebar bleiben.
Tatsächlich gebaut (die geplante Aufteilung in oxygen.lua/hud.lua ist entfallen —
die teilen sich alle denselben Zustand mit main.lua, eine Trennung hätte nur
Plumbing erzeugt. light.lua hat dagegen eine echte Naht: es kommt mit dem
Statebag allein aus und kennt weder Ausrüstung noch Flasche):
d4rk_divegear/
├── fxmanifest.lua
├── config/
│ ├── client.lua # Timings, Tiefe, Licht, HUD
│ └── shared.lua # Tank-Definitionen (Client + Server brauchen capacity)
├── client/
│ ├── main.lua # Gear an/aus, Watchdog, Sauerstoff, HUD-Bridge
│ └── light.lua # Statebag + Draw-Thread, ohne gemeinsamen Zustand
├── nui/
│ ├── index.html
│ ├── style.css
│ └── script.js
├── server/
│ └── main.lua # Useable Items, Metadata schreiben + validieren
├── locales/
│ ├── de.json
│ └── en.json
└── items_for_ox_inventory.lua # Snippet zum Reinkopieren
Locales: DE + EN werden gepflegt, die 12 übrigen Upstream-Sprachen bleiben liegen (neue Keys fehlen dort → ox_lib fällt auf den Key-Namen zurück, kein Crash).
Aufwand
| Phase | Aufwand |
|---|---|
| 1 — Gear-Persistenz | ~1 h |
| 2 — Sauerstoff-Logik | ~1 h |
| 3 — Flaschengrößen | ~1 h |
| 4 — NUI-HUD (Tiefe + O2) | ~2,5 h |
| 5 — Taschenlampe | 2–3 h |
| Summe | ~8 h |
Entscheidungen
Am 2026-08-10 festgelegt:
- Tod: Ausrüstung wird beim Respawn abgenommen, die Flasche bleibt mit ihrer
Restluft im Inventar. → Phase 1: Death-Handler ruft
takeOffSuit()ohne Progressbar, Phase 2 schreibt vorher die Restluft in die Metadata. - Taschenlampe: fest an der Ausrüstung, kein eigenes Item. An/Aus per Keybind. → Phase 5: Torch-Prop wird zusammen mit Maske und Flasche angehängt.
- Batterie: keine. Lampe läuft, solange Sauerstoff da ist.
→
metadata.batteryentfällt komplett, HUD zeigt nur An/Aus. - Tiefenabhängiger Verbrauch: an per Default, in der Config abschaltbar.
→ Phase 3:
decayRate * (1 + depth / 30), braucht den Tiefenwert aus Phase 4. Deshalb Phase 4 vor dem Feintuning der Verbrauchskurve fertigstellen.
Offen
Alles unten ist ungetestet im Spiel — es gibt hier keinen FiveM-Server. Geprüft
sind nur Lua-Syntax und Globals (node ../.tools/lint.mjs, 0 Warnungen), die
Locale-JSONs und das HUD im Browser.
- Lampen-Prop
prop_cs_police_torch— Name ungeprüft. Ist er falsch, meldet das Script eine Warnung und leuchtet ohne sichtbares Prop weiter. Ersatz suchen: forge.plebmasters.de/objects lampDirection— welche lokale Achse des Kopf-Bones nach vorne zeigt, hängt am Skelett. Leuchtet der Kegel falsch herum, inconfig/client.luadie Achse tauschen oder das Vorzeichen drehen.- Auto-Anziehen nach Relog (siehe Phase 2) und HUD-Position in der Config (Phase 4) sind nicht umgesetzt.
IsHudHidden()— ob die Native wirklich aufDisplayHud(false)anspringt, ist ungetestet. Falls nicht, greifen weiterhin die eigene Taste und der Export;hudFollowGameHudlässt sich dann abschalten.- Balancing:
maxDrainPerSecond = 5.0muss übermaxDecayFactor = 4.0liegen, sonst bekommen ehrliche Spieler in großer Tiefe vom Server Luft zurückgeschrieben. Wer den Tiefenfaktor hochdreht, muss beides nachziehen.
Testplan
- Anlegen/Ablegen an Land, im flachen Wasser, unter Wasser
- Emote starten (rpemotes o.ä.) → Props bleiben
- Ragdoll (aus Höhe fallen, angefahren werden) → Props bleiben
- Kleiderladen / Skin-Change → Props hängen am neuen Ped
- Relog mit halbvoller Flasche → Restluft stimmt
- Resource-Restart während des Tauchens → Gear wird wiederhergestellt
- Zwei Spieler, beide mit Lampe → jeder sieht das Licht des anderen
- Lampe an, senkrecht nach unten tauchen → Kegel folgt der Blickrichtung nach unten
(das ist der Fall, an dem
GetEntityForwardVectorgescheitert wäre) - Tank leer unter Wasser → Scuba aus, Ertrinken setzt ein
- Mehrere Flaschen im Inventar → Auswahlmenü mit Füllstand pro Flasche
- Volle Flasche auffüllen → Meldung, Kartusche wird nicht verbraucht
- Flasche im Inventar ansehen → durability-Balken zeigt den Füllstand