Lint / Lint Resource (push) Has been cancelled
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>
271 lines
13 KiB
Markdown
271 lines
13 KiB
Markdown
# d4rk_divegear — Umbauplan
|
||
|
||
Fork von [Qbox-project/qbx_divegear](https://github.com/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 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.
|
||
|
||
- [x] `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
|
||
- [x] `isGearIntact()` Helper: `DoesEntityExist` + `GetEntityAttachedTo(...) == cache.ped`
|
||
für beide Props
|
||
- [x] 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)
|
||
- [x] `lib.onCache('ped', ...)` → bei Model-Wechsel (Kleiderladen, Skin-Change)
|
||
Props neu anhängen, Scuba-State neu setzen
|
||
- [x] Death-Handler: `IsPedDeadOrDying`-Check im Watchdog statt eigenem Event —
|
||
keine Abhängigkeit zu qbx_medical o.ä. Sichern der Restluft kommt in Phase 2 dazu
|
||
- [x] Gemeinsamer `removeGear(outOfAir)`-Teardown für Ablegen, leere Flasche und Tod
|
||
- [x] `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)
|
||
|
||
- [x] Umstellung von "Punkte" auf **Restsekunden** (`oxygenSeconds`)
|
||
- [x] Decrementer-Thread auf `GetGameTimer()`-Delta statt gezählter `Wait(1000)`
|
||
(driftfrei, überlebt Frame-Drops)
|
||
- [x] Float-Vergleiche raus, Warnschwellen explizit als Config-Liste
|
||
(z.B. `warnAtSeconds = { 60, 30, 10 }`), jede Schwelle einmal auslösen
|
||
- [x] Anzeige als `mm:ss` statt roher Zahl
|
||
- [x] 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`)
|
||
- [x] 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 |
|
||
|
||
- [x] `capacity` in `config/shared.lua` (Client *und* Server brauchen sie), Label
|
||
und Gewicht im Item-Snippet. Kein Prop-Scale — siehe Optik-Hinweis unten
|
||
- [x] **`items_for_ox_inventory.lua`** als Snippet-Datei zum manuellen Reinkopieren
|
||
in `ox_inventory/data/items.lua` — kein `RegisterItem`-Export
|
||
- [x] `metadata.oxygen` (Restsekunden) + `metadata.durability` (Prozent, damit der
|
||
Füllstand direkt im Inventar-Slot sichtbar ist)
|
||
- [x] `diving_fill` füllt bis `capacity` des jeweiligen Tanks statt auf Fixwert
|
||
- [x] 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):**
|
||
- [x] Tiefe = Wasseroberfläche − Ped-Z, via `GetWaterHeight` (Fallback
|
||
`GetWaterHeightNoWaves`)
|
||
- [x] Wert geglättet (Lerp) damit die Zahl nicht zappelt
|
||
- [x] Liefert gleichzeitig den Faktor für den tiefenabhängigen Verbrauch aus Phase 3
|
||
|
||
**NUI (~2 h):**
|
||
- [x] O2 als Balken/Ring + `mm:ss` Restzeit, Farbwechsel an den Warnschwellen aus
|
||
Phase 2, Pulsieren im kritischen Bereich
|
||
- [x] Tiefe in Metern, Warnfarbe ab Config-Tiefe
|
||
- [x] Slot für den Lampen-Status (wird in Phase 5 befüllt)
|
||
- [x] Sichtbar nur wenn Gear angelegt **und** unter Wasser, mit Fade-In/Out
|
||
- [x] Updates gedrosselt per `SendNUIMessage` (~4/s statt jeden Frame)
|
||
- [x] Kein `backdrop-filter` (bekannter FiveM-Bug → schwarzer Hintergrund)
|
||
- [x] `hudEnabled`, `hudUpdateIntervalMs` und `deepWarningDepth` in `config/client.lua`
|
||
- [x] **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
|
||
- [x] **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 (~2–3 h)
|
||
|
||
Licht ist in GTA nie synchronisiert — jeder Client zeichnet das Licht für jeden
|
||
Taucher in seiner Nähe selbst.
|
||
|
||
- [x] Torch-Prop mit derselben harten Attach-Methode aus Phase 1 anhängen
|
||
(Kopf-Bone oder Hand, Config)
|
||
- [x] Toggle per `RegisterKeyMapping`/`lib.addKeybind`
|
||
- [x] State replizieren: `LocalPlayer.state:set('divelight', true, true)`
|
||
— Statebag statt Event-Spam
|
||
- [x] Draw-Thread: über `GetActivePlayers()`, `Player(serverId).state.divelight`
|
||
prüfen, `DrawSpotLightWithShadow` zeichnen
|
||
- [x] **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
|
||
- [x] 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
|
||
- [x] Performance: Distanzfilter ~40–50 m, max. 3–4 gleichzeitige Lichter
|
||
(`DrawSpotLightWithShadow` ist teuer)
|
||
- [x] 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:
|
||
|
||
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](https://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
|
||
```
|