Files
d4rk_divegear/PLAN.md
T
D4rkst3randClaude Opus 5 5a59fc513f
Lint / Lint Resource (push) Has been cancelled
feat(hud): Manometer in bar und ausblendbares HUD
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>
2026-08-11 00:06:58 +02:00

13 KiB
Raw Blame History

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:

  1. Sauerstoff ist eine lokale Client-Variable (local oxygenLevel = 0) — nicht persistent, nicht am Item hängend, weg bei Relog/Restart. Blockiert Flaschengrößen.
  2. Laufzeit-Rechenfehler: startingOxygenLevel = 100 bei decayRate = 1 pro Sekunde ergibt 100 Sekunden, nicht Minuten. Die Vergleiche oxygenLevel % 10 == 0 und oxygenLevel == 0 (client/main.lua:121, :124) brechen, sobald decayRate fraktional wird.
  3. 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 35 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 einen attachProp()-Helper, den Phase 5 für die Lampe mitbenutzt. Laut Native-Doku heißt useSoftPinning false: "attached entity will not detach when fixed" — genau das wollen wir
  • isGearIntact() Helper: DoesEntityExist + GetEntityAttachedTo(...) == cache.ped für beide Props
  • Watchdog-Thread (gearWatchdogIntervalMs, default 1000 ms) solange currentGear.enabled: bei Defekt deleteGear() + attachGear(), und enableScuba() neu setzen (SetEnableScuba/SetPedMaxTimeUnderwater werden 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
  • onResourceStop Cleanup-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ählter Wait(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:ss statt 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
  • capacity in config/shared.lua (Client und Server brauchen sie), Label und Gewicht im Item-Snippet. Kein Prop-Scale — siehe Optik-Hinweis unten
  • items_for_ox_inventory.lua als Snippet-Datei zum manuellen Reinkopieren in ox_inventory/data/items.lua — kein RegisterItem-Export
  • metadata.oxygen (Restsekunden) + metadata.durability (Prozent, damit der Füllstand direkt im Inventar-Slot sichtbar ist)
  • diving_fill füllt bis capacity des 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 (Fallback GetWaterHeightNoWaves)
  • 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:ss Restzeit, 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, hudUpdateIntervalMs und deepWarningDepth in config/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). Unter reservePressure wird rot eingefärbt
  • Nachgereicht: HUD ausblendbar für Unterwasser-Screenshots. Drei Wege, weil qbx_core selbst keinen HUD-Toggle hat (es feuert nur hud:client:* an eine separate HUD-Resource): IsHudHidden() folgen, eigene Taste, und der Export setHudVisible(bool)
  • Offen: Position und Größe des HUD sind bisher nur in nui/style.css einstellbar, nicht in der Config

Phase 5 — Gesyncte Taschenlampe (~23 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.divelight prüfen, DrawSpotLightWithShadow zeichnen
  • Richtung anders gelöst als geplant: statt Prop-Matrix und Objekt-Pool werden zwei bone-relative Punkte am Kopf-Bone benutzt. Die Offsets von GetPedBoneCoords sind 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 (GetEntityForwardVector würde beim waagerecht im Wasser liegenden Ped stur horizontal leuchten), ohne fremde Props über GetGamePool('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/IsModelInCdimage geprüft, weil lib.requestModel bei ungültigen Models einen Error wirft. Ein falscher Prop-Name kostet so nur die Optik, nicht die ganze Ausrüstung
  • Performance: Distanzfilter ~4050 m, max. 34 gleichzeitige Lichter (DrawSpotLightWithShadow ist 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 23 h
Summe ~8 h

Entscheidungen

Am 2026-08-10 festgelegt:

  1. 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.
  2. 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.
  3. Batterie: keine. Lampe läuft, solange Sauerstoff da ist. → metadata.battery entfällt komplett, HUD zeigt nur An/Aus.
  4. 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.

  1. 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
  2. lampDirection — welche lokale Achse des Kopf-Bones nach vorne zeigt, hängt am Skelett. Leuchtet der Kegel falsch herum, in config/client.lua die Achse tauschen oder das Vorzeichen drehen.
  3. Auto-Anziehen nach Relog (siehe Phase 2) und HUD-Position in der Config (Phase 4) sind nicht umgesetzt.
  4. IsHudHidden() — ob die Native wirklich auf DisplayHud(false) anspringt, ist ungetestet. Falls nicht, greifen weiterhin die eigene Taste und der Export; hudFollowGameHud lässt sich dann abschalten.
  5. Balancing: maxDrainPerSecond = 5.0 muss über maxDecayFactor = 4.0 liegen, 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 GetEntityForwardVector gescheitert 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