feat(light): gesyncte Tauchlampe
Lint / Lint Resource (push) Has been cancelled

Licht ist in GTA nie synchronisiert - DrawSpotLight zeichnet nur lokal fuer den
aktuellen Frame. "Gesynct" heisst deshalb: der Zustand wird als Statebag
repliziert und jeder Client zeichnet die Lampen aller Taucher in seiner Naehe
selbst. Muster uebernommen aus d4rk_phone (Statebag, adaptives Polling,
Distanzfilter).

client/light.lua ist ein reiner Renderer ohne gemeinsamen Zustand mit main.lua -
er kommt mit dem Statebag allein aus und kennt weder Ausruestung noch Flasche.

- Richtung ueber zwei bone-relative Punkte am Kopf-Bone statt ueber
  GetEntityForwardVector: beim Tauchen liegt der Ped waagerecht im Wasser, der
  Entity-Forward wuerde stur horizontal leuchten. Die Offsets von
  GetPedBoneCoords sind laut Native-Doku "relative to the bone's rotation",
  die Differenz zweier solcher Punkte enthaelt also den Pitch
- Toggle per lib.addKeybind, Standard L, vom Spieler umlegbar
- Distanzfilter und Limit auf die naechsten N Lampen, Schatten abschaltbar
- Prop-Model wird vorher mit IsModelValid geprueft: lib.requestModel wirft bei
  ungueltigen Models einen Error, der sonst die ganze Ausruestung lahmlegt
- Lampen-Status im HUD

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 23:56:40 +02:00
co-authored by Claude Opus 5
parent 28ab8e796c
commit d9643620ff
7 changed files with 288 additions and 44 deletions
+74 -41
View File
@@ -58,17 +58,19 @@ ohne Luft absaufen). Beide Werte sind jetzt als `maxTimeUnderwater` /
### Phase 2 — Sauerstoff-Logik neu (~1 h)
- [ ] Umstellung von "Punkte" auf **Restsekunden** (`oxygenSeconds`)
- [ ] Decrementer-Thread auf `GetGameTimer()`-Delta statt gezählter `Wait(1000)`
- [x] Umstellung von "Punkte" auf **Restsekunden** (`oxygenSeconds`)
- [x] Decrementer-Thread auf `GetGameTimer()`-Delta statt gezählter `Wait(1000)`
(driftfrei, überlebt Frame-Drops)
- [ ] Float-Vergleiche raus, Warnschwellen explizit als Config-Liste
- [x] 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
- [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`)
- [ ] Beim Anlegen Metadata lesen → Restluft übernehmen
- [ ] Beim Spawn/Resource-Start: war die Gear angelegt, direkt wieder anziehen
- [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),
@@ -85,13 +87,14 @@ Vier Items mit unterschiedlicher `capacity` in Sekunden:
| `diving_tank_large` | 15 min | 900 |
| `diving_tank_xl` | 20 min | 1200 |
- [ ] Items in `config.lua` definieren (Label, capacity, Gewicht, Prop-Scale)
- [ ] **`items_for_ox_inventory.lua`** als Snippet-Datei zum manuellen Reinkopieren
- [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
- [ ] `metadata.oxygen` (Restsekunden) + `metadata.durability` (Prozent, damit der
- [x] `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
- [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
@@ -104,42 +107,49 @@ 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
- [x] 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
- [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):**
- [ ] O2 als Balken/Ring + `mm:ss` Restzeit, Farbwechsel an den Warnschwellen aus
- [x] 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)
- [ ] Position/Scale in `config/client.lua` einstellbar
- [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`
- [ ] **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
- [x] 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)`
- [x] Toggle per `RegisterKeyMapping`/`lib.addKeybind`
- [x] State replizieren: `LocalPlayer.state:set('divelight', true, true)`
— Statebag statt Event-Spam
- [ ] Draw-Thread: über `GetActivePlayers()`, `Player(serverId).state.divelight`
- [x] Draw-Thread: über `GetActivePlayers()`, `Player(serverId).state.divelight`
prüfen, `DrawSpotLightWithShadow` zeichnen
- [ ] **Richtung aus dem Prop, nicht aus dem Ped:** der Ped pitcht beim Tauchen,
`GetEntityForwardVector` geht schief. Position + Richtung über
`GetOffsetFromEntityInWorldCoords(torchProp, 0, 1, 0)` aus der Prop-Matrix
- [ ] Fremde Props: einmal pro Sekunde `GetGamePool('CObject')` nach Torch-Model
filtern, per `GetEntityAttachedTo` dem Ped zuordnen, Ergebnis cachen.
Fallback wenn nicht gefunden: Kopf-Bone + Ped-Matrix
- [ ] Performance: Distanzfilter ~4050 m, max. 34 gleichzeitige Lichter
- [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)
- [ ] Lampen-Status (An/Aus) in das HUD aus Phase 4 einhängen — keine Batterie,
- [x] Lampen-Status (An/Aus) in das HUD aus Phase 4 einhängen — keine Batterie,
die Lampe läuft solange Sauerstoff da ist
---
@@ -149,6 +159,11 @@ Taucher in seiner Nähe selbst.
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
@@ -156,10 +171,8 @@ d4rk_divegear/
│ ├── 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
│ ├── main.lua # Gear an/aus, Watchdog, Sauerstoff, HUD-Bridge
── light.lua # Statebag + Draw-Thread, ohne gemeinsamen Zustand
├── nui/
│ ├── index.html
│ ├── style.css
@@ -205,10 +218,25 @@ Am 2026-08-10 festgelegt:
→ Phase 3: `decayRate * (1 + depth / 30)`, braucht den Tiefenwert aus Phase 4.
Deshalb Phase 4 vor dem Feintuning der Verbrauchskurve fertigstellen.
## Offene Fragen
---
1. **Gitea:** Repo unter `git.d4rkst3r.de/D4rkst3r/d4rk_divegear` — soll ich das
Remote vorbereiten? (Push machst du selbst.)
## 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. **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.
---
@@ -221,5 +249,10 @@ Am 2026-08-10 festgelegt:
- 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
```