feat(hud): Manometer in bar und ausblendbares HUD
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>
This commit is contained in:
2026-08-11 00:06:58 +02:00
co-authored by Claude Opus 5
parent e5af47da66
commit 5a59fc513f
12 changed files with 151 additions and 24 deletions
+13 -1
View File
@@ -121,6 +121,15 @@ Tiefenanzeige und O2-Anzeige zusammen als ein NUI-Overlay — `qbx.drawText2d`
- [x] Updates gedrosselt per `SendNUIMessage` (~4/s statt jeden Frame) - [x] Updates gedrosselt per `SendNUIMessage` (~4/s statt jeden Frame)
- [x] Kein `backdrop-filter` (bekannter FiveM-Bug → schwarzer Hintergrund) - [x] Kein `backdrop-filter` (bekannter FiveM-Bug → schwarzer Hintergrund)
- [x] `hudEnabled`, `hudUpdateIntervalMs` und `deepWarningDepth` in `config/client.lua` - [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` - [ ] **Offen:** Position und Größe des HUD sind bisher nur in `nui/style.css`
einstellbar, nicht in der Config einstellbar, nicht in der Config
@@ -234,7 +243,10 @@ Locale-JSONs und das HUD im Browser.
tauschen oder das Vorzeichen drehen. tauschen oder das Vorzeichen drehen.
3. **Auto-Anziehen nach Relog** (siehe Phase 2) und **HUD-Position in der Config** 3. **Auto-Anziehen nach Relog** (siehe Phase 2) und **HUD-Position in der Config**
(Phase 4) sind nicht umgesetzt. (Phase 4) sind nicht umgesetzt.
4. **Balancing:** `maxDrainPerSecond = 5.0` muss über `maxDecayFactor = 4.0` liegen, 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. sonst bekommen ehrliche Spieler in großer Tiefe vom Server Luft zurückgeschrieben.
Wer den Tiefenfaktor hochdreht, muss beides nachziehen. Wer den Tiefenfaktor hochdreht, muss beides nachziehen.
+19 -2
View File
@@ -11,8 +11,9 @@ Animationen nicht verschwindet.
Füllstand ist als Balken direkt im Inventar-Slot sichtbar. Füllstand ist als Balken direkt im Inventar-Slot sichtbar.
- **Gesyncte Tauchlampe** — an der Ausrüstung, An/Aus per Taste (Standard `L`). - **Gesyncte Tauchlampe** — an der Ausrüstung, An/Aus per Taste (Standard `L`).
Andere Spieler sehen den Lichtkegel. Andere Spieler sehen den Lichtkegel.
- **NUI-HUD** — Restluft als Ring mit `mm:ss` und Tauchtiefe in Metern, mit - **NUI-HUD** — Restluft als Ring mit `mm:ss`, Tauchtiefe in Metern und
Warnstufen. Nur sichtbar mit angelegter Ausrüstung unter Wasser. Manometer in bar, mit Warnstufen. Nur sichtbar mit angelegter Ausrüstung
unter Wasser, und ausblendbar für Unterwasser-Screenshots.
- **Tiefenabhängiger Verbrauch** — tiefer tauchen kostet mehr Luft. Abschaltbar. - **Tiefenabhängiger Verbrauch** — tiefer tauchen kostet mehr Luft. Abschaltbar.
- **Ausrüstung bleibt** — bei Emotes, Ragdoll, Kleiderwechsel und aufräumenden - **Ausrüstung bleibt** — bei Emotes, Ragdoll, Kleiderwechsel und aufräumenden
Fremd-Scripts. Fremd-Scripts.
@@ -39,6 +40,22 @@ Animationen nicht verschwindet.
| Flasche wählen | Menü beim Anlegen (entfällt bei nur einer Flasche) | | Flasche wählen | Menü beim Anlegen (entfällt bei nur einer Flasche) |
| Flasche auffüllen | `diving_fill` benutzen, nicht unter Wasser | | Flasche auffüllen | `diving_fill` benutzen, nicht unter Wasser |
| Lampe an/aus | `L` (in den FiveM-Tastenbelegungen umlegbar) | | Lampe an/aus | `L` (in den FiveM-Tastenbelegungen umlegbar) |
| HUD ausblenden | Eigene Taste, standardmäßig unbelegt — in den FiveM-Tastenbelegungen unter "FiveM" setzen |
### HUD ausblenden für Screenshots
Drei Wege, damit es unabhängig von der eingesetzten HUD-Resource funktioniert:
1. **Automatisch** — das Tauch-HUD verschwindet mit dem Spiel-HUD. Greift bei
allem, was `DisplayHud(false)` setzt (Screenshot-Scripts, HUD-Toggles,
Cinematic-Modes). Abschaltbar über `hudFollowGameHud`.
2. **Eigene Taste**`hudToggleKey` in der Config, oder vom Spieler selbst belegt.
3. **Export** — für andere Resources:
```lua
exports.d4rk_divegear:setHudVisible(false)
local shown = exports.d4rk_divegear:isHudVisible()
```
## Konfiguration ## Konfiguration
+48 -1
View File
@@ -289,10 +289,20 @@ local function takeOffSuit()
end end
end end
--- Manometer-Anzeige. Bei konstantem Volumen ist der Druck proportional zur
--- verbliebenen Gasmenge, deshalb reicht der lineare Anteil an der Füllung.
--- @return number bar
local function getPressure()
if not activeTank or activeTank.capacity <= 0 then return 0 end
return (activeTank.pressure or 0) * (activeTank.oxygen / activeTank.capacity)
end
--- @param visible boolean --- @param visible boolean
local function sendHudState(visible) local function sendHudState(visible)
local oxygen = activeTank and activeTank.oxygen or 0.0 local oxygen = activeTank and activeTank.oxygen or 0.0
local capacity = activeTank and activeTank.capacity or 1 local capacity = activeTank and activeTank.capacity or 1
local pressure = getPressure()
local level = 'ok' local level = 'ok'
if oxygen <= dangerThreshold then if oxygen <= dangerThreshold then
@@ -307,6 +317,8 @@ local function sendHudState(visible)
oxygen = oxygen, oxygen = oxygen,
capacity = capacity, capacity = capacity,
depth = currentDepth, depth = currentDepth,
pressure = pressure,
reserve = pressure > 0 and pressure <= config.reservePressure,
level = level, level = level,
deep = currentDepth >= config.deepWarningDepth, deep = currentDepth >= config.deepWarningDepth,
hasLamp = true, hasLamp = true,
@@ -314,6 +326,25 @@ local function sendHudState(visible)
}) })
end end
--- Manuell ausgeblendet (eigene Taste oder Export)? Unabhängig davon, ob das
--- Spiel-HUD gerade sichtbar ist.
local hudHidden = false
--- Blendet das Tauch-HUD aus oder wieder ein, z.B. für Unterwasser-Screenshots.
--- Als Export nutzbar, damit HUD- und Screenshot-Resources es mitnehmen können:
--- exports.d4rk_divegear:setHudVisible(false)
--- @param visible boolean
local function setHudVisible(visible)
hudHidden = not visible
if hudHidden then
SendNUIMessage({ action = 'state', visible = false })
end
end
exports('setHudVisible', setHudVisible)
exports('isHudVisible', function() return not hudHidden end)
--- Pflegt die geglättete Tiefe und schiebt den Zustand ins NUI. --- Pflegt die geglättete Tiefe und schiebt den Zustand ins NUI.
--- Läuft auch über Wasser weiter, weil getDecayFactor die Tiefe braucht - nur die --- Läuft auch über Wasser weiter, weil getDecayFactor die Tiefe braucht - nur die
--- Anzeige wird ausgeblendet. --- Anzeige wird ausgeblendet.
@@ -325,13 +356,20 @@ local function startHudThread()
action = 'labels', action = 'labels',
air = locale('hud.air'), air = locale('hud.air'),
depth = locale('hud.depth'), depth = locale('hud.depth'),
pressure = locale('hud.pressure'),
lamp = locale('hud.lamp') lamp = locale('hud.lamp')
}) })
while currentGear.enabled do while currentGear.enabled do
currentDepth = currentDepth + (getDepth() - currentDepth) * 0.25 currentDepth = currentDepth + (getDepth() - currentDepth) * 0.25
local visible = config.hudEnabled and activeTank ~= nil and IsPedSwimmingUnderWater(cache.ped) local visible = config.hudEnabled
and not hudHidden
and activeTank ~= nil
and IsPedSwimmingUnderWater(cache.ped)
-- Folgt dem Spiel-HUD: alles was DisplayHud(false) setzt, blendet
-- damit auch das Tauch-HUD aus.
and not (config.hudFollowGameHud and IsHudHidden())
if visible or wasVisible then if visible or wasVisible then
sendHudState(visible) sendHudState(visible)
@@ -478,6 +516,15 @@ RegisterNetEvent('d4rk_divegear:client:useGear', function()
end end
end) end)
lib.addKeybind({
name = 'divegear_hud',
description = locale('keybind.toggle_hud'),
defaultKey = config.hudToggleKey,
onPressed = function()
setHudVisible(hudHidden)
end
})
lib.addKeybind({ lib.addKeybind({
name = 'divegear_lamp', name = 'divegear_lamp',
description = locale('keybind.toggle_lamp'), description = locale('keybind.toggle_lamp'),
+14
View File
@@ -36,9 +36,23 @@ return {
--- ruhige Anzeige ohne unnötigen NUI-Traffic. --- ruhige Anzeige ohne unnötigen NUI-Traffic.
hudUpdateIntervalMs = 250, hudUpdateIntervalMs = 250,
--- Tauch-HUD mit ausblenden, sobald das Spiel-HUD ausgeblendet ist.
--- Greift bei allem, was DisplayHud(false) setzt - Screenshot-Scripts,
--- HUD-Toggles, Cinematic-Modes -, ohne dass wir die Resource kennen müssen.
hudFollowGameHud = true,
--- Eigene Taste zum Aus-/Einblenden des Tauch-HUD. Leer = keine Vorbelegung,
--- der Spieler kann sie in den FiveM-Tastenbelegungen unter "FiveM" setzen.
--- Zusätzlich gibt es den Export setHudVisible(bool) für andere Resources.
hudToggleKey = '',
--- Ab dieser Tiefe wird die Tiefenanzeige eingefärbt (Meter). --- Ab dieser Tiefe wird die Tiefenanzeige eingefärbt (Meter).
deepWarningDepth = 25.0, deepWarningDepth = 25.0,
--- Reservedruck in bar. Darunter wird die Druckanzeige eingefärbt - in der
--- Tauchpraxis die Schwelle, ab der man den Aufstieg einleitet.
reservePressure = 50,
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
-- Taschenlampe -- Taschenlampe
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
+11 -5
View File
@@ -2,13 +2,19 @@
-- damit lib.require die Datei auf beiden Seiten laden kann. -- damit lib.require die Datei auf beiden Seiten laden kann.
local config = { local config = {
--- Tauchflaschen. capacity = Laufzeit in Sekunden bei normalem Verbrauch. --- Tauchflaschen.
--- capacity = Laufzeit in Sekunden bei normalem Verbrauch.
--- pressure = Fülldruck in bar. Reine Anzeige: der angezeigte Druck ist
--- pressure * (Restluft / capacity). Bei konstantem Volumen ist der
--- Druck proportional zur verbliebenen Gasmenge, das passt also.
--- Dass alle Flaschen denselben Fülldruck haben ist gewollt - in echt
--- macht das Volumen die Laufzeit, nicht der Druck.
--- Reihenfolge = Reihenfolge im Auswahlmenü. --- Reihenfolge = Reihenfolge im Auswahlmenü.
tanks = { tanks = {
{ name = 'diving_tank_small', capacity = 300 }, -- 5 Minuten { name = 'diving_tank_small', capacity = 300, pressure = 300 }, -- 5 Minuten
{ name = 'diving_tank_medium', capacity = 600 }, -- 10 Minuten { name = 'diving_tank_medium', capacity = 600, pressure = 300 }, -- 10 Minuten
{ name = 'diving_tank_large', capacity = 900 }, -- 15 Minuten { name = 'diving_tank_large', capacity = 900, pressure = 300 }, -- 15 Minuten
{ name = 'diving_tank_xl', capacity = 1200 }, -- 20 Minuten { name = 'diving_tank_xl', capacity = 1200, pressure = 300 }, -- 20 Minuten
}, },
--- Wie oft der Client die Restluft an den Server meldet (Sekunden). --- Wie oft der Client die Restluft an den Server meldet (Sekunden).
+7 -6
View File
@@ -5,8 +5,9 @@
-- (modules/items/server.lua) - die leere Flasche würde also verschwinden statt -- (modules/items/server.lua) - die leere Flasche würde also verschwinden statt
-- auffüllbar zu bleiben. -- auffüllbar zu bleiben.
-- --
-- Die Laufzeiten stehen in config/shared.lua (capacity in Sekunden). Wer die dort -- Laufzeit (capacity, Sekunden) und Fülldruck (pressure, bar) stehen in
-- ändert, sollte die Labels hier mitziehen. -- config/shared.lua. Wer die dort ändert, sollte Labels und Beschreibungen hier
-- mitziehen.
['diving_gear'] = { ['diving_gear'] = {
label = 'Tauchausrüstung', label = 'Tauchausrüstung',
@@ -34,7 +35,7 @@
stack = false, stack = false,
close = true, close = true,
consume = 0, consume = 0,
description = 'Kleine Tauchflasche. Reicht für etwa 5 Minuten.', description = 'Kleine Tauchflasche, 300 bar. Reicht für etwa 5 Minuten.',
client = { image = 'diving_tank_small.png' } client = { image = 'diving_tank_small.png' }
}, },
@@ -44,7 +45,7 @@
stack = false, stack = false,
close = true, close = true,
consume = 0, consume = 0,
description = 'Mittlere Tauchflasche. Reicht für etwa 10 Minuten.', description = 'Mittlere Tauchflasche, 300 bar. Reicht für etwa 10 Minuten.',
client = { image = 'diving_tank_medium.png' } client = { image = 'diving_tank_medium.png' }
}, },
@@ -54,7 +55,7 @@
stack = false, stack = false,
close = true, close = true,
consume = 0, consume = 0,
description = 'Große Tauchflasche. Reicht für etwa 15 Minuten.', description = 'Große Tauchflasche, 300 bar. Reicht für etwa 15 Minuten.',
client = { image = 'diving_tank_large.png' } client = { image = 'diving_tank_large.png' }
}, },
@@ -64,6 +65,6 @@
stack = false, stack = false,
close = true, close = true,
consume = 0, consume = 0,
description = 'Doppelflasche für lange Tauchgänge. Reicht für etwa 20 Minuten.', description = 'Doppelflasche, 300 bar. Reicht für etwa 20 Minuten.',
client = { image = 'diving_tank_xl.png' } client = { image = 'diving_tank_xl.png' }
}, },
+3 -1
View File
@@ -25,9 +25,11 @@
"hud": { "hud": {
"air": "Luft", "air": "Luft",
"depth": "Tiefe", "depth": "Tiefe",
"pressure": "Druck",
"lamp": "Lampe" "lamp": "Lampe"
}, },
"keybind": { "keybind": {
"toggle_lamp": "Tauchlampe an/aus" "toggle_lamp": "Tauchlampe an/aus",
"toggle_hud": "Tauch-HUD ein-/ausblenden"
} }
} }
+3 -1
View File
@@ -25,9 +25,11 @@
"hud": { "hud": {
"air": "Air", "air": "Air",
"depth": "Depth", "depth": "Depth",
"pressure": "Pressure",
"lamp": "Lamp" "lamp": "Lamp"
}, },
"keybind": { "keybind": {
"toggle_lamp": "Toggle dive lamp" "toggle_lamp": "Toggle dive lamp",
"toggle_hud": "Toggle dive HUD"
} }
} }
+7
View File
@@ -18,11 +18,18 @@
</div> </div>
<div class="readouts"> <div class="readouts">
<div class="readout-row">
<div class="readout"> <div class="readout">
<span id="depth-label" class="caption">TIEFE</span> <span id="depth-label" class="caption">TIEFE</span>
<div class="value"><span id="depth-value">0.0</span><span class="unit">m</span></div> <div class="value"><span id="depth-value">0.0</span><span class="unit">m</span></div>
</div> </div>
<div class="readout" id="pressure">
<span id="pressure-label" class="caption">DRUCK</span>
<div class="value"><span id="pressure-value">0</span><span class="unit">bar</span></div>
</div>
</div>
<div id="lamp" class="lamp"> <div id="lamp" class="lamp">
<svg viewBox="0 0 24 24" aria-hidden="true"> <svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M9 21h6v-1H9v1zm3-19a7 7 0 0 0-4 12.7V17h8v-2.3A7 7 0 0 0 12 2z"></path> <path d="M9 21h6v-1H9v1zm3-19a7 7 0 0 0-4 12.7V17h8v-2.3A7 7 0 0 0 12 2z"></path>
+10 -1
View File
@@ -8,11 +8,14 @@ const hud = document.getElementById('hud');
const gaugeFill = document.getElementById('gauge-fill'); const gaugeFill = document.getElementById('gauge-fill');
const airValue = document.getElementById('air-value'); const airValue = document.getElementById('air-value');
const depthValue = document.getElementById('depth-value'); const depthValue = document.getElementById('depth-value');
const pressure = document.getElementById('pressure');
const pressureValue = document.getElementById('pressure-value');
const lamp = document.getElementById('lamp'); const lamp = document.getElementById('lamp');
const labels = { const labels = {
air: document.getElementById('air-label'), air: document.getElementById('air-label'),
depth: document.getElementById('depth-label'), depth: document.getElementById('depth-label'),
pressure: document.getElementById('pressure-label'),
lamp: document.getElementById('lamp-label') lamp: document.getElementById('lamp-label')
}; };
@@ -34,6 +37,8 @@ function render(state) {
gaugeFill.style.strokeDashoffset = String(RING * (1 - ratio)); gaugeFill.style.strokeDashoffset = String(RING * (1 - ratio));
airValue.textContent = formatTime(state.oxygen); airValue.textContent = formatTime(state.oxygen);
depthValue.textContent = state.depth.toFixed(1); depthValue.textContent = state.depth.toFixed(1);
pressureValue.textContent = String(Math.round(state.pressure ?? 0));
pressure.classList.toggle('reserve', !!state.reserve);
// Warnstufen kommen als Restsekunden vom Client, damit sie zu // Warnstufen kommen als Restsekunden vom Client, damit sie zu
// config.warnAtSeconds passen statt hier nochmal definiert zu werden. // config.warnAtSeconds passen statt hier nochmal definiert zu werden.
@@ -53,6 +58,7 @@ window.addEventListener('message', (event) => {
if (data.action === 'labels') { if (data.action === 'labels') {
labels.air.textContent = data.air; labels.air.textContent = data.air;
labels.depth.textContent = data.depth; labels.depth.textContent = data.depth;
labels.pressure.textContent = data.pressure;
labels.lamp.textContent = data.lamp; labels.lamp.textContent = data.lamp;
return; return;
} }
@@ -62,5 +68,8 @@ window.addEventListener('message', (event) => {
// Damit sich das HUD im Browser ohne FiveM anschauen laesst. // Damit sich das HUD im Browser ohne FiveM anschauen laesst.
if (typeof GetParentResourceName !== 'function') { if (typeof GetParentResourceName !== 'function') {
render({ visible: true, oxygen: 372, capacity: 600, depth: 12.4, level: 'ok', hasLamp: true, lamp: true }); render({
visible: true, oxygen: 372, capacity: 600, depth: 12.4, pressure: 186,
level: 'ok', hasLamp: true, lamp: true
});
} }
+11 -2
View File
@@ -111,13 +111,18 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
min-width: 92px; }
.readout-row {
display: flex;
gap: 18px;
} }
.readout { .readout {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 3px; gap: 3px;
min-width: 76px;
} }
.value { .value {
@@ -140,6 +145,7 @@ body {
.lamp { .lamp {
display: flex; display: flex;
align-items: center; align-items: center;
align-self: flex-start; /* sonst zieht der Flex-Container die Leiste auf volle Breite */
gap: 6px; gap: 6px;
padding: 5px 9px; padding: 5px 9px;
border-radius: 8px; border-radius: 8px;
@@ -176,7 +182,10 @@ body {
#hud.danger #air-value { color: var(--danger); } #hud.danger #air-value { color: var(--danger); }
#hud.danger .gauge { animation: pulse 1s ease-in-out infinite; } #hud.danger .gauge { animation: pulse 1s ease-in-out infinite; }
#hud.deep .value { color: var(--warn); } #hud.deep #depth-value { color: var(--warn); }
/* Reservedruck: in der Tauchpraxis die Schwelle zum Aufstieg. */
#pressure.reserve .value { color: var(--danger); }
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; } 0%, 100% { opacity: 1; }
+2 -1
View File
@@ -67,7 +67,8 @@ local function getTankList(source)
name = name, name = name,
label = (exports.ox_inventory:Items(name) or {}).label or name, label = (exports.ox_inventory:Items(name) or {}).label or name,
oxygen = math.max(0, math.min(oxygen, tankConfig.capacity)), oxygen = math.max(0, math.min(oxygen, tankConfig.capacity)),
capacity = tankConfig.capacity capacity = tankConfig.capacity,
pressure = tankConfig.pressure
} }
end end
end end