- Umbauplan (PLAN.md) mit 5 Phasen: Gear-Persistenz, Sauerstoff-Logik, Flaschengroessen, NUI-HUD (Tiefe + O2), gesyncte Taschenlampe - fxmanifest auf D4rkst3r/Gitea umgestellt - Qbox-spezifische CI entfernt (Discord-Webhooks, GitHub-Releases, Issue-Projektzuordnung) - laeuft auf Gitea ins Leere bzw. gegen fremde Endpunkte. lint.yml bleibt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.5 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,fixedRot = trueisGearIntact()Helper:DoesEntityExist+GetEntityAttachedTo(...) == cache.pedfür beide Props- Watchdog-Thread (1000 ms) solange
currentGear.enabled: bei DefektdeleteGear()+attachGear(), undenableScuba()unbedingt 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: beim Tod Gear sofort ablegen (ohne Progressbar), Restluft vorher sichern — Flasche bleibt im Inventar
- Inkonsistenz aufräumen:
takeOffSuit()setztSetPedMaxTimeUnderwater(ped, 50.0),disableScuba()setzt1.0— einen Wert festlegen onResourceStopCleanup-Handler für Props
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
- Beim Spawn/Resource-Start: war die Gear angelegt, direkt wieder anziehen
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 |
- Items in
config.luadefinieren (Label, capacity, Gewicht, Prop-Scale) items_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) - Position/Scale in
config/client.luaeinstellbar
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 aus dem Prop, nicht aus dem Ped: der Ped pitcht beim Tauchen,
GetEntityForwardVectorgeht schief. Position + Richtung überGetOffsetFromEntityInWorldCoords(torchProp, 0, 1, 0)aus der Prop-Matrix - Fremde Props: einmal pro Sekunde
GetGamePool('CObject')nach Torch-Model filtern, perGetEntityAttachedTodem Ped zuordnen, Ergebnis cachen. Fallback wenn nicht gefunden: Kopf-Bone + Ped-Matrix - 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.
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
│ ├── oxygen.lua # Decrementer, Persistenz-Sync
│ ├── light.lua # Statebag + Draw-Thread
│ └── hud.lua # Tiefenberechnung + NUI-Bridge
├── 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.
Offene Fragen
- Gitea: Repo unter
git.d4rkst3r.de/D4rkst3r/d4rk_divegear— soll ich das Remote vorbereiten? (Push machst du selbst.)
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
- Tank leer unter Wasser → Scuba aus, Ertrinken setzt ein