chore: aufgeraeumt nach der Abnahme

Lampen-Prop entfernt. Seit der Umstellung auf die spieleigene Scuba-Lampe war es
funktionslos: standardmaessig aus, nie verifiziert, und eine Polizei-Taschenlampe
am Kopf haette neben dem nativen Licht nur Deko gespielt. Damit fallen auch die
Debug-Befehle /divelamp und /divelampcheck weg, samt config.debug und vier
Config-Werten.

Zwoelf Upstream-Locales geloescht. Sie hatten 8 von 26 Keys - im Spiel waeren
dort rohe Key-Namen erschienen. Gepflegt werden DE und EN.

PLAN.md war ein Vorab-Plan mit Phasen und Aufwandsschaetzungen und damit
erledigt. Ersetzt durch ENTSCHEIDUNGEN.md: warum die Dinge so gebaut sind und
welche Fallstricke dahinterstecken - onCache-Timing, consume, decay,
SetMetadata, die Atemgas-Grenze, das transform-origin am Zeiger. Also das, was
beim naechsten Anfassen wirklich hilft.

README von den Abschnitten befreit, die es nicht mehr gibt. Bilder und Icons neu
erzeugt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 10:16:04 +02:00
co-authored by Claude Opus 5
parent a49e6d0f6a
commit 1ed6a924f8
24 changed files with 147 additions and 607 deletions
+140
View File
@@ -0,0 +1,140 @@
# Entscheidungen und Fallstricke
Warum die Dinge so gebaut sind, wie sie gebaut sind. Wer hier etwas ändern will,
sollte den jeweiligen Absatz vorher gelesen haben — das meiste davon hat genau
einmal wehgetan.
Fork von [qbx_divegear](https://github.com/Qbox-project/qbx_divegear) v1.0.1. Die
Upstream-Historie liegt vollständig auf dem Remote `upstream`.
---
## Was am Original nicht funktionierte
1. **Der Sauerstoff war eine lokale Client-Variable**, die an keinem Item hing.
Nicht persistent, nicht übertragbar, weg bei Relog. Damit war weder eine
Flaschengröße noch eine ehrliche Restluft-Anzeige möglich.
2. **Die Laufzeit war verrechnet:** `startingOxygenLevel = 100` bei `decayRate = 1`
pro Sekunde ergab 100 Sekunden, nicht Minuten.
3. **Die Props waren soft-pinned angehängt.** Mit `useSoftPinning = true` darf die
Physik-Engine die Verbindung bei Ragdoll und harten Anim-Übergängen lösen —
genau deshalb verschwand die Ausrüstung ständig.
---
## Ausrüstung
**Attachment hart, nicht soft.** `useSoftPinning = false`; laut Native-Doku heißt
das *"attached entity will not detach when fixed"*. Zusätzlich prüft ein Watchdog
jede Sekunde, ob beide Props noch am **aktuellen** Ped hängen, und hängt sie sonst
neu an. Er setzt auch `SetEnableScuba` und `SetPedMaxTimeUnderwater` neu, weil das
Spiel beides bei Respawn und Fahrzeugausstieg zurücksetzt.
**`lib.onCache('ped')` gibt den neuen Ped als Parameter — `cache.ped` nicht.**
ox_lib ruft die Callbacks über `Citizen.CreateThreadNow` auf, *bevor* der neue Wert
in den Cache geschrieben wird. Wer im Handler `cache.ped` benutzt, hängt die Props
an den gerade verschwundenen Ped. Das ist der Kleiderladen-Fall, für den der
Handler überhaupt existiert.
**`SetPedMaxTimeUnderwater` hat zwei Werte, das ist Absicht.** `maxTimeUnderwater`
(50 s) beim Ablegen = normal weiterschwimmen. `maxTimeUnderwaterOutOfAir` (1 s)
bei leerer Flasche = absaufen. Sieht nach Inkonsistenz aus, ist keine.
**Besitz wird laufend geprüft.** Ausrüstung und Flasche können das Inventar mitten
im Tauchgang verlassen — weggegeben, abgelegt, vom Admin entfernt. Der Client hält
nur eine Slot-Nummer; ohne Gegenprüfung taucht man unbegrenzt weiter.
`ox_inventory:updateInventory` feuert bei jeder Änderung, dort wird beim Server
nachgefragt. Wurde nur umsortiert, bindet der Client auf den neuen Slot um.
## Items
**Kein `consume` bei Items mit `CreateUseableItem`.** ox_inventory verzweigt so:
```lua
elseif consume then -- interner Pfad
elseif not item.weapon and server.UseItem then -- hier erst CreateUseableItem
```
In Lua ist `0` wahr. `consume = 0` landet im ersten Zweig, der qbx-Callback feuert
nie, und beim Benutzen passiert **gar nichts** — ohne Fehlermeldung. Steht so nicht
in der ox-Doku, nur im Quellcode.
**Kein `decay` auf den Flaschen.** `metadata.durability` trägt hier den Füllstand.
ox_inventory löscht Items mit `decay`, sobald `durability` 0 erreicht — die leere
Flasche würde also verschwinden statt auffüllbar zu bleiben.
**`SetMetadata` ersetzt die Metadata-Tabelle komplett**, es merged nicht. Deshalb
wird der bestehende Inhalt kopiert, bevor `oxygen` und `durability` gesetzt werden.
**Flaschen ohne Metadata gelten als voll** (`newTanksFull`). Eine frisch gekaufte
Flasche hat kein `oxygen` — ohne diese Regel wäre sie leer und man müsste erst eine
Kartusche verbrauchen, um überhaupt tauchen zu können.
## Sauerstoff
**Restsekunden statt Punkte**, und der Verbrauch läuft über `GetGameTimer()`-Deltas
statt gezählter `Wait(1000)`. Ein Tick dauert unter Last länger als eine Sekunde;
über einen 20-Minuten-Tauchgang summiert sich das sichtbar.
**Der Server clampt.** Der Client meldet die Restluft, der Server akzeptiert nur
Abnahme und höchstens `maxDrainPerSecond` pro echter Sekunde. Wer den Tiefenfaktor
oder den Narkose-`drainFactor` hochdreht, muss diesen Wert mitziehen — sonst
bekommen ehrliche Spieler in großer Tiefe Luft zurückgeschrieben.
## Tiefe
**Die Grenze hängt am Atemgas, nicht an einer unsichtbaren Wand.** Real begrenzt
nicht der Druck, sondern der Stickstoff in der Pressluft: tiefer wirkt er
narkotisch, ab etwa 56 m wird auch der Sauerstoff giftig. Wer tiefer will, atmet
Trimix. Genau so umgesetzt — `maxDepth` pro Flasche, darunter Tiefenrausch in
Stufen.
**Bei 160 m ist Schluss, das macht die Engine.** Dagegen hilft kein Atemgas,
deshalb dort nur eine Warnung und kein eigener Effekt. Trimix hört mit 100 m
bewusst darüber auf.
## Lampe
**Die spieleigene Scuba-Lampe, kein Eigenbau.** GTA hat seit v1493
`SetEnableScubaGearLight` (`0xEE2476B9EE4A094F`). Die sitzt richtig, leuchtet in
Blickrichtung und macht Prop, Offsets, Kegelwerte und einen Draw-Thread
überflüssig. Gefunden über [esx_scuba](https://github.com/wobozkyng/esx_scuba).
Der dortige Hinweis, das Licht hänge am Kleidungs-Component 8, **trifft nicht zu**
ingame geprüft, sie funktioniert auch ohne Scuba-Kleidung. Das Outfit wird nicht
angefasst.
**Der Flag ist lokal und repliziert sich nicht von selbst.** Deshalb der Umweg über
einen Statebag: jeder Client setzt den Flag für die Taucher in seiner Nähe. Der
Abgleich-Thread ist nötig, weil Peds beim Streaming, Respawn und Model-Wechsel neu
erzeugt werden und den Flag dabei verlieren.
## HUD
**Zeigerinstrumente ohne Skalenzahlen.** Bei 96 px kollidieren Zahlen mit Strichen,
Zeiger und rotem Bereich, und gelesen werden sie ohnehin nicht. Der Zeiger zeigt
die Tendenz, die Zahl darunter den Wert.
**Kein `transform-origin` am Zeiger.** Das `rotate(winkel 50 50)` im
transform-Attribut bringt den Drehpunkt schon mit; CSS rechnet eine zusätzliche
Origin obendrauf und der Zeiger landet außerhalb der Uhr.
**Das HUD skaliert mit der Bildschirmhöhe.** Entworfen auf 1080p; FiveM rendert das
NUI in der echten Auflösung, ohne Nachskalieren wäre es auf 4K halb so groß.
**Kein `backdrop-filter`** — bekannter FiveM-Bug, wird nach Updates zu einem
schwarzen Kasten.
---
## Offen
- Nach einem Relog muss die Ausrüstung neu angelegt werden. Die Restluft überlebt,
die liegt auf der Flasche.
- Die HUD-Position ist nur in `nui/style.css` einstellbar, nicht in der Config.
- `IsHudHidden()` — ob die Native auf `DisplayHud(false)` anspringt, ist ungeprüft.
Falls nicht, greifen Taste und Export weiterhin.
- Die `timecycle`-Namen des Tiefenrauschs sind nicht verifiziert. Kommt kein Effekt,
in `config/client.lua` unter `narcosis.stages` einen anderen eintragen.
- Balancing: Laufzeiten und Narkose-Stufen sind gesetzt, aber nicht über längere
Zeit am Spielgefühl geprüft.
-264
View File
@@ -1,264 +0,0 @@
# 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)
+4 -54
View File
@@ -201,64 +201,13 @@ auf z.B. `2`.
| Option | Default | Bedeutung |
|---|---|---|
| `lampKey` | `'l'` | Vorbelegung |
| `lampKey` | `'l'` | Vorbelegung der Taste |
| `lampReconcileIntervalMs` | `500` | Wie oft der Lampen-Zustand auf die Peds in der Nähe nachgezogen wird |
| `lampProp` | `''` | Optionales Prop am Kopf, rein optisch. Die Lampe leuchtet unabhängig davon |
| `lampBone` / `lampPropOffset` / `lampPropRotation` | `12844` / `vec3(0,0,0)` | Sitz des optionalen Props — mit `/divelamp` einstellen |
> **Beim Balancing beachten:** `maxDrainPerSecond` (shared) muss über
> `maxDecayFactor` (client) liegen. Sonst hält der Server den ehrlichen Verbrauch
> in großer Tiefe für Cheat und schreibt Luft zurück.
## Debug-Befehle
Mit `debug = true` in `config/client.lua`:
```
/divelampcheck Hat das aktuelle Outfit eine Scuba-Lampe?
/divelamp <x> <y> <z> <rx> <ry> <rz> Sitz des optionalen Lampen-Props
```
`/divelamp` hängt das Prop sofort neu an und schreibt die fertige Config-Zeile ins
Log — kein Resource-Restart pro Versuch. Alternativ mit einem Prop-Attach-Editor
wie [noted_propattacher](https://github.com/NotedDevelopment/noted_propattacher)
ausmessen.
> Ein Tauchlampen-Prop gibt es im Basegame nicht. Was es gibt, sind Varianten
> derselben Polizei-Taschenlampe: `prop_cs_police_torch`, `prop_police_torch`,
> `prop_scn_police_torch`. Für die Beleuchtung braucht es das Prop ohnehin nicht.
## Tiefengrenze und Tiefenrausch
Warum 4045 m real das Limit sind, liegt nicht am Druck, sondern am **Stickstoff
in der Pressluft**: tiefer wirkt er narkotisch, und ab etwa 56 m wird auch der
Sauerstoff giftig. Wer tiefer will, atmet **Trimix** — dort ersetzt Helium einen
Teil des Stickstoffs.
Genau so ist es hier umgesetzt: jede Flasche hat in `config/shared.lua` eine
`maxDepth`. Darunter setzt Tiefenrausch ein, in drei Stufen, je tiefer desto
heftiger — Bildschirmeffekt, Kamerawackeln, höherer Luftverbrauch, in der letzten
Stufe Schaden. Fünf Meter vorher kommt eine Warnung, und die Tiefenanzeige im HUD
wird rot und pulsiert, sobald die Grenze überschritten ist.
Damit ist ein 45-m-Limit keine Absprache mehr, sondern spürbar — und die tiefen
Spots bleiben erreichbar, kosten aber die richtige Flasche.
Alles daran ist in `config/client.lua` unter `narcosis` einstellbar: Schwellen,
Effekte, Stärke, Verbrauchsaufschlag, Schaden. `enabled = false` schaltet es ganz aus.
> Die `timecycle`-Namen sind Spiel-Effekte, keine Natives. Existiert einer nicht,
> passiert einfach nichts — dann einen anderen eintragen.
### Die Grenze der Engine
Unabhängig von allem hier ist bei **160 m** Schluss — das macht GTA selbst über den
Wasserdruck, dagegen hilft kein Atemgas. Ingame gemessen.
Deshalb gibt es dafür nur eine Warnung (`engineDepthLimit`, 20 m vorher). Trimix
hört mit 100 m bewusst darüber auf; dazwischen liegt die Zone, in der es zwar
ungemütlich wird, aber noch nichts tödlich ist.
## Exports
```lua
@@ -287,7 +236,7 @@ Sie funktioniert **auch ohne Scuba-Kleidung** am Ped — ingame bestätigt. Der
Hinweis in [esx_scuba](https://github.com/wobozkyng/esx_scuba), das Licht hänge am
Kleidungs-Component, trifft hier also nicht zu. Das Outfit wird nicht angefasst.
Sollte die Native bei einem Ped-Model doch nicht anschlagen, gibt es beim
Einschalten eine Meldung, und `/divelampcheck` (mit `debug = true`) sagt es direkt.
Einschalten eine Meldung statt stiller Wirkungslosigkeit.
Der Flag ist ein **lokaler** Ped-Zustand und wird nicht von selbst repliziert.
Deshalb läuft der Zustand über einen Statebag
@@ -321,7 +270,8 @@ Schachbrett, in Slot-Größe und klein, zum Gegenschauen nach Änderungen.
> passiert bei jedem `<text>`-Element, auch im minimalsten Testfall — die Zahlen
> kommen korrekt raus.
Der Umbau ist in [PLAN.md](PLAN.md) dokumentiert, inklusive der offenen Punkte.
Warum die Dinge so gebaut sind — und welche Fallstricke dahinterstecken — steht
in [ENTSCHEIDUNGEN.md](ENTSCHEIDUNGEN.md), zusammen mit den offenen Punkten.
Die Abnahme läuft über die
[Issues](https://git.d4rkst3r.de/D4rkst3r/d4rk_divegear/issues?labels=test) —
ein Issue pro Testbereich, mit `prio:`-Labels und `ungeprueft` für alles, was
-74
View File
@@ -4,7 +4,6 @@ local shared = require 'config.shared'
local currentGear = {
mask = 0,
tank = 0,
lamp = 0,
enabled = false
}
@@ -140,12 +139,6 @@ local function deleteGear()
DeleteEntity(currentGear.tank)
currentGear.tank = 0
end
if currentGear.lamp ~= 0 then
DetachEntity(currentGear.lamp, false, true)
DeleteEntity(currentGear.lamp)
currentGear.lamp = 0
end
end
--- Hängt ein Prop hart an einen Ped-Bone.
@@ -171,25 +164,11 @@ local function attachProp(ped, model, bone, offset, rotation)
return prop
end
--- Lampen-Model einmalig prüfen. lib.requestModel wirft bei ungültigen Models einen
--- Error - ohne diese Prüfung würde ein falsch geschriebener Prop-Name die komplette
--- Ausrüstung unbenutzbar machen, statt nur die Optik der Lampe zu kosten.
local lampModel = config.lampProp ~= '' and joaat(config.lampProp) or nil
local hasLampProp = lampModel ~= nil and (IsModelValid(lampModel) or IsModelInCdimage(lampModel))
if lampModel and not hasLampProp then
lib.print.warn(('lampProp "%s" ist kein gültiges Model - die Lampe leuchtet, das Prop fehlt.'):format(config.lampProp))
end
--- @param ped number? default cache.ped
local function attachGear(ped)
ped = ped or cache.ped
currentGear.tank = attachProp(ped, TANK_MODEL, TANK_BONE, vec3(-0.25, -0.25, 0.0), vec3(180.0, 90.0, 0.0))
currentGear.mask = attachProp(ped, MASK_MODEL, MASK_BONE, vec3(0.0, 0.0, 0.0), vec3(180.0, 90.0, 0.0))
if hasLampProp then
currentGear.lamp = attachProp(ped, lampModel, config.lampBone, config.lampPropOffset, config.lampPropRotation)
end
end
--- @param state boolean
@@ -217,8 +196,6 @@ local function isPropIntact(prop)
end
local function isGearIntact()
if hasLampProp and not isPropIntact(currentGear.lamp) then return false end
return isPropIntact(currentGear.mask) and isPropIntact(currentGear.tank)
end
@@ -672,57 +649,6 @@ lib.addKeybind({
end
})
--- Wo Prop und Lichtkegel am Bone sitzen, lässt sich nicht am Schreibtisch
--- erraten - das hängt daran, wie das Model selbst ausgerichtet ist. Diese Befehle
--- ändern die Werte im laufenden Spiel und geben die Zeile zum Übernehmen aus,
--- damit nicht pro Versuch die Resource neu gestartet werden muss.
---
--- lib.require cached Module, main.lua und light.lua teilen sich also dieselbe
--- config-Tabelle - das Licht zieht sofort mit.
if config.debug then
--- @return vector3?
local function readVec(args, from)
local x, y, z = tonumber(args[from]), tonumber(args[from + 1]), tonumber(args[from + 2])
if not x or not y or not z then return end
return vec3(x, y, z)
end
local function fmtVec(v)
return ('vec3(%.2f, %.2f, %.2f)'):format(v.x, v.y, v.z)
end
RegisterCommand('divelamp', function(_, args)
local offset, rotation = readVec(args, 1), readVec(args, 4)
if not offset or not rotation then
lib.print.info('/divelamp <x> <y> <z> <rx> <ry> <rz> - Sitz des Lampen-Props')
return
end
config.lampPropOffset, config.lampPropRotation = offset, rotation
if currentGear.enabled then
deleteGear()
attachGear()
end
lib.print.info(('lampPropOffset = %s,\nlampPropRotation = %s,'):format(fmtVec(offset), fmtVec(rotation)))
end, false)
--- Die Spiel-Lampe braucht die Scuba-Kleidung am Ped. Dieser Befehl sagt, ob
--- das aktuelle Outfit sie überhaupt hat - sonst rätselt man, warum nichts
--- leuchtet.
RegisterCommand('divelampcheck', function()
SetEnableScubaGearLight(cache.ped, true)
local works = IsScubaGearLightEnabled(cache.ped)
SetEnableScubaGearLight(cache.ped, lampOn)
lib.print.info(('Scuba-Lampe am aktuellen Outfit: %s'):format(works and 'ja' or 'nein'))
end, false)
end
--- Beim Model-Wechsel (Kleiderladen, Skin-Change) ist der Ped ein anderer -
--- alle attached Props sind weg und die Scuba-Flags gelten für den alten Ped.
---
+3 -23
View File
@@ -1,9 +1,4 @@
return {
--- Schaltet /divelamp und /divelight frei, mit denen sich Sitz und
--- Leuchtrichtung der Lampe im laufenden Spiel einstellen lassen - ohne
--- Resource-Restart pro Versuch. Auf dem Live-Server ausschalten.
debug = false,
putOnSuitTimeMs = 5000,
takeOffSuitTimeMs = 5000,
refillTankTimeMs = 5000,
@@ -142,27 +137,12 @@ return {
lampKey = 'l',
--- Die Lampe ist die spieleigene Scuba-Lampe (SetEnableScubaGearLight, seit
--- v1493). Sie sitzt an der richtigen Stelle und leuchtet in Blickrichtung -
--- deshalb gibt es hier weder Offsets noch Farb- und Kegelwerte.
---
--- Sie funktioniert auch ohne Scuba-Kleidung am Ped (ingame bestätigt), das
--- Outfit wird also nicht angefasst.
--- v1493). Sie sitzt an der richtigen Stelle, leuchtet in Blickrichtung und
--- funktioniert auch ohne Scuba-Kleidung am Ped - deshalb gibt es hier weder
--- Prop noch Offsets noch Farb- und Kegelwerte.
--- Wie oft der Lampen-Zustand auf die Peds in der Nähe nachgezogen wird (ms).
--- Nötig, weil Peds beim Streaming, Respawn und Model-Wechsel neu erzeugt
--- werden und den Flag dabei verlieren.
lampReconcileIntervalMs = 500,
--- Optionales Prop am Kopf, rein optisch. Die Lampe leuchtet unabhängig davon.
--- Standardmäßig aus, weil die Spiel-Lampe schon an der Ausrüstung sitzt.
--- Ein Basegame-Tauchlampen-Prop gibt es nicht; die Polizei-Taschenlampe
--- ('prop_cs_police_torch', 'prop_police_torch', 'prop_scn_police_torch')
--- passt optisch nur bedingt.
lampProp = '',
--- Bone und Sitz des optionalen Props. 12844 = Kopf (wie die Maske).
--- Mit /divelamp im Spiel einstellen statt hier zu raten.
lampBone = 12844,
lampPropOffset = vec3(0.0, 0.0, 0.0),
lampPropRotation = vec3(0.0, 0.0, 0.0),
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "/divingsuit لنزع بدلة الغوص الخاصة بك",
"need_otube": "أنت لا ترتدي معدات الغوص",
"underwater": "الوقوف لاستخدام خزان الأكسجين"
},
"success": {
"took_out": "جاري اخد لباس الغوض",
"tube_filled": "You've successfully refilled your air tank!"
},
"info": {
"put_suit": "وضعت بدلة الغوص",
"pullout_suit": "ترك بدلة الغوص",
"filling_air": "Filling air..."
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "Du skal stå op for at tage dykkerudstyret på",
"need_otube": "You need to refill your oxygen! Get a replacement air supply!",
"underwater": "You cannot do this underwater..."
},
"success": {
"took_out": "Du tog din våddragt af",
"tube_filled": "You've successfully refilled your air tank!"
},
"info": {
"put_suit": "Tag en dykkerdragt på",
"pullout_suit": "Træk en dykker dragt ud..",
"filling_air": "Filling air..."
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "Tienes que estar de pie para ponerte el equipo de buceo",
"need_otube": "You need to refill your oxygen! Get a replacement air supply!",
"underwater": "You cannot do this underwater..."
},
"success": {
"took_out": "Te quitaste tu traje de neopreno",
"tube_filled": "You've successfully refilled your air tank!"
},
"info": {
"put_suit": "Ponerse equipo de buceo",
"pullout_suit": "Saca un equipo de buceo..",
"filling_air": "Filling air..."
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "Sukeldumisvarustuse selga panemiseks peate püsti seisma",
"need_otube": "You need to refill your oxygen! Get a replacement air supply!",
"underwater": "You cannot do this underwater..."
},
"success": {
"took_out": "Sa võtsid sukeldumisülikonna seljast",
"tube_filled": "You've successfully refilled your air tank!"
},
"info": {
"put_suit": "Pane selga sukeldumisvarustust",
"pullout_suit": "Võta välja sukeldumisvarustus..",
"filling_air": "Filling air..."
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "Dalış teçhizatını takmak için ayağa kalkman gerekiyor",
"need_otube": "You need to refill your oxygen! Get a replacement air supply!",
"underwater": "You cannot do this underwater..."
},
"success": {
"took_out": "Dalgıç giysini çıkardın",
"tube_filled": "You've successfully refilled your air tank!"
},
"info": {
"put_suit": "Dalış kıyafeti giy",
"pullout_suit": "Dalgıç kıyafeti çıkarılıyor..",
"filling_air": "Filling air..."
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "Þú þarft að standa upp til að setja á þig köfunarbúnaðinn",
"need_otube": "You need to refill your oxygen! Get a replacement air supply!",
"underwater": "You cannot do this underwater..."
},
"success": {
"took_out": "Þú tókst blautbúninginn þinn af þér",
"tube_filled": "You've successfully refilled your air tank!"
},
"info": {
"put_suit": "Farðu í köfunargalla",
"pullout_suit": "Dragðu fram köfunargalla..",
"filling_air": "Filling air..."
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "Devi essere in piedi per indossare l'attrezzatura subacquea",
"need_otube": "You need to refill your oxygen! Get a replacement air supply!",
"underwater": "You cannot do this underwater..."
},
"success": {
"took_out": "Ti sei tolto la muta da sub",
"tube_filled": "You've successfully refilled your air tank!"
},
"info": {
"put_suit": "Indossando muta da sub..",
"pullout_suit": "Rimuovendo muta da sub..",
"filling_air": "Filling air..."
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "これを着用するには地面に立っている必要があります...",
"need_otube": "酸素を補充する必要があります!予備の空気源を入手してください!",
"underwater": "水中ではこの操作はできません..."
},
"success": {
"took_out": "装備を脱ぎました!",
"tube_filled": "エアタンクの補充に成功しました!"
},
"info": {
"put_suit": "ダイビングスーツを着用中...",
"pullout_suit": "ダイビングスーツを脱いでいます...",
"filling_air": "空気を充填中..."
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "Je moet rechtop staan om de duikuitrusting aan te trekken",
"need_otube": "You need to refill your oxygen! Get a replacement air supply!",
"underwater": "You cannot do this underwater..."
},
"success": {
"took_out": "Je hebt je wetsuit uitgedaan",
"tube_filled": "You've successfully refilled your air tank!"
},
"info": {
"put_suit": "Doe een duikpak aan",
"pullout_suit": "Trek een duikpak uit..",
"filling_air": "Filling air..."
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "Você precisa estar em terra firme para colocar isso...",
"need_otube": "Você precisa reabastecer seu oxigênio! Consiga um suprimento de ar de reposição!",
"underwater": "Você não pode fazer isso debaixo d'água..."
},
"success": {
"took_out": "Você tirou seu equipamento!",
"tube_filled": "Você reabasteceu com sucesso seu tanque de ar!"
},
"info": {
"put_suit": "Colocando seu traje de mergulho...",
"pullout_suit": "Tirando seu traje de mergulho...",
"filling_air": "Reabastecendo ar..."
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "Precisas de estar num terreno sólido para colocar isto...",
"need_otube": "Precisas de reabastecer o teu oxigénio! Obtêm um abastecimento de ar de substituição!",
"underwater": "Não podes fazer isto debaixo de água..."
},
"success": {
"took_out": "Tiraste o teu equipamento!",
"tube_filled": "Reabasteceste com sucesso o teu tanque de ar!"
},
"info": {
"put_suit": "A vestir o teu fato de mergulho...",
"pullout_suit": "A tirar o teu fato de mergulho...",
"filling_air": "A encher o ar..."
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"error": {
"not_standing_up": "Bunu giymek için sağlam zeminde olmalısınız...",
"need_otube": "Oksijeninizi doldurmanız gerekiyor! Yedek hava tankı alın!",
"underwater": "Bunu su altında yapamazsınız..."
},
"success": {
"took_out": "Ekipmanınızı çıkardınız!",
"tube_filled": "Hava tankınızı başarıyla doldurdunuz!"
},
"info": {
"put_suit": "Dalış kıyafetinizi giyiyorsunuz...",
"pullout_suit": "Dalış kıyafetinizi çıkarıyorsunuz...",
"filling_air": "Hava tankı dolduruluyor..."
}
}