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

271 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 35 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 (~23 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 ~4050 m, max. 34 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 | 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](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
```