Files
d4rk_divegear/PLAN.md
T
D4rkst3randClaude Opus 5 7c4004a378 docs: Testliste durch Issues ersetzt
TESTING.md war nicht filterbar, nicht zuweisbar und haette neben den Issues als
zweite Quelle gedriftet. Jetzt ein Issue pro Testbereich mit Labels (test,
prio:kritisch/mittel/niedrig, ungeprueft), plus bug und doku fuer spaeter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:20:27 +02:00

265 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)
**Nachträglich komplett ersetzt.** Der ganze Abschnitt unten beschreibt den
DrawSpotLight-Eigenbau — der ist raus. GTA hat seit v1493 eine eigene Lampe an der
Tauchausrüstung (`SetEnableScubaGearLight`, `0xEE2476B9EE4A094F`), gefunden über
[wobozkyng/esx_scuba](https://github.com/wobozkyng/esx_scuba). Die sitzt richtig,
leuchtet in Blickrichtung und macht Prop, Offsets, Kegelwerte, Distanzfilter und
Draw-Thread überflüssig. Geblieben ist nur der Statebag, weil der Flag ein lokaler
Ped-Zustand ist. Einschränkung: die Lampe hängt an der Scuba-Kleidung.
Der ursprüngliche Plan zum Vergleich:
- [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
Ingame getestet (2026-08-11): Anlegen/Ablegen, Flaschenauswahl, Luftverbrauch,
HUD mit Tiefe und Druck, Lampe. Zwei Bugs kamen dabei raus und sind gefixt —
`consume = 0` im Item-Snippet (der qbx-Callback feuerte nie) und leere
Metadata bei frisch gekauften Flaschen.
1. **Scuba-Lampe braucht die Scuba-Kleidung** (Component 8). Auf Wunsch des Users
fasst die Resource das Outfit nicht an — wer keine trägt, bekommt eine Meldung.
Soll die Ausrüstung die Kleidung selbst setzen, muss das inklusive Sichern und
Wiederherstellen gebaut werden (wie in wobozkyng/esx_scuba).
2. **`IsHudHidden()`** — ungetestet, ob die Native auf `DisplayHud(false)`
anspringt. Falls nicht, greifen Taste und Export weiterhin.
3. **Auto-Anziehen nach Relog** und **HUD-Position in der Config** sind nicht
umgesetzt.
4. **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. Die Laufzeiten selbst
sind noch nicht am Spielgefühl geprüft.
---
## Testplan
Als Issues im Repo, ein Issue pro Testbereich:
[git.d4rkst3r.de/D4rkst3r/d4rk_divegear/issues](https://git.d4rkst3r.de/D4rkst3r/d4rk_divegear/issues?labels=test)