feat: Flasche und Maske als Kleidung, Patronen tauschen, HUD-Anmeldung

- Tauchgeraet bereit = Taucherflasche (rigItem, Slot 31) mit Patrone UND
  Maske (maskItem, Slot 29) angezogen; Server getWorn statt verifyGear,
  geprueft bei jeder Inventaraenderung und nach jedem Umziehen.
  Props, diving_gear und der Anzieh-Balken entfallen.
- Die fuenf Flaschen sind Patronen: benutzen = in die getragene Flasche
  einsetzen, die alte kommt mit Restluft zurueck (patroneTauschen).
- Faeden mit Laufnummer, damit ein Tausch sie nicht verdoppelt.
- exports.ox_inventory -> exports.d4rk_inventory, Meldungen ueber
  lib.notify (meldung_shared) und d4rk_bridge:Notify.
- Konsole beim HUD angemeldet (client/hud.lua), 366 x 165 bei 1080p
  gemessen; ohne HUD bleibt die CSS-Position.
- Nachfuellen (diving_fill) stillgelegt: kommt spaeter ueber Shop/Crafting.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
D4rkst3randClaude Opus 5.5 committed 2026-09-28 23:48:26 +02:00
1 parent 930ff6381f
commit e64a1bd3d0
9 files changed
+400 -213

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
--[[
d4rk_divegear - die Tauchkonsole beim HUD anmelden (28.09.2026).
d4rk_hud verwaltet Lage, Groesse und Sichtbarkeit fremder Bauteile im
Editor (/hudbearbeiten) und speichert sie je Charakter; die Konsole
zeichnet sich weiter selbst (d4rk_hud/client/anmeldung.lua, Vertrag dort).
Laeuft d4rk_hud nicht, bleibt die Konsole, wo nui/style.css sie hinsetzt.
breite x hoehe: die Konsole bei 1080p, GEMESSEN am 28.09.2026 mit
d4rk-rp-planung/scripts/nui-pruefen/divegear-konsole.mjs (offsetWidth/
offsetHeight von #hud, --hud-scale 1.000): 366 x 165. Das Beispiel in
d4rk_hud/client/anmeldung.lua nennt 364 x 162 - das war nicht gemessen.
]]
local KEY = 'tauchkonsole'
local BREITE, HOEHE = 366, 165
local function anmelden()
if GetResourceState('d4rk_hud') ~= 'started' then return end
pcall(function()
exports.d4rk_hud:BauteilAnmelden(KEY, {
name = locale('hud.konsole'),
breite = BREITE,
hoehe = HOEHE,
vorgabe = { anker = 'unten-mitte', x = 0, y = 76, skala = 1.0, an = true },
})
end)
end
AddEventHandler('d4rk_hud:bauteilLage', function(key, lage)
if key ~= KEY then return end
SendNUIMessage({ action = 'lage', lage = lage })
end)
AddEventHandler('d4rk_hud:bauteileAnfragen', anmelden)
AddEventHandler('onClientResourceStart', function(resource)
if resource == 'd4rk_hud' then SetTimeout(1000, function() anmelden() end) end
end)
AddEventHandler('onClientResourceStop', function(resource)
-- Ohne HUD wieder die eigene Position.
if resource == 'd4rk_hud' then SendNUIMessage({ action = 'lage', lage = false }) end
end)
CreateThread(function()
Wait(1000)
anmelden()
end)
+125 -173
View File
@@ -1,12 +1,17 @@
local config = require 'config.client' local config = require 'config.client'
local shared = require 'config.shared' local shared = require 'config.shared'
--- Tauchgeraet bereit: Flasche mit Patrone UND Maske angezogen (28.09.2026 -
--- vorher hingen hier zwei Props, mask und tank).
local currentGear = { local currentGear = {
mask = 0,
tank = 0,
enabled = false enabled = false
} }
--- Laufnummer der Faeden (Verbrauch, HUD, Waechter). Wird beim Tauschen einer
--- Patrone sofort neu angelegt - ohne Nummer liefen die alten Faeden nach
--- ihrem Wait einfach weiter, und es gaebe sie doppelt.
local gearLauf = 0
--- Lampe an? Der eigentliche Lichtkegel wird in client/light.lua gezeichnet, --- Lampe an? Der eigentliche Lichtkegel wird in client/light.lua gezeichnet,
--- gesteuert über den replizierten Statebag. --- gesteuert über den replizierten Statebag.
local lampOn = false local lampOn = false
@@ -32,10 +37,6 @@ for i = 1, #config.warnAtSeconds do
if dangerThreshold == 0 or threshold < dangerThreshold then dangerThreshold = threshold end if dangerThreshold == 0 or threshold < dangerThreshold then dangerThreshold = threshold end
end end
local MASK_MODEL = `p_d_scuba_mask_s`
local TANK_MODEL = `p_s_scuba_tank_s`
local MASK_BONE = 12844
local TANK_BONE = 24818
--- @param seconds number --- @param seconds number
--- @return string mm:ss --- @return string mm:ss
@@ -127,50 +128,6 @@ local function disableScuba(outOfAir, ped)
SetPedMaxTimeUnderwater(ped, outOfAir and config.maxTimeUnderwaterOutOfAir or config.maxTimeUnderwater) SetPedMaxTimeUnderwater(ped, outOfAir and config.maxTimeUnderwaterOutOfAir or config.maxTimeUnderwater)
end end
local function deleteGear()
if currentGear.mask ~= 0 then
DetachEntity(currentGear.mask, false, true)
DeleteEntity(currentGear.mask)
currentGear.mask = 0
end
if currentGear.tank ~= 0 then
DetachEntity(currentGear.tank, false, true)
DeleteEntity(currentGear.tank)
currentGear.tank = 0
end
end
--- Hängt ein Prop hart an einen Ped-Bone.
--- useSoftPinning bleibt bewusst false: mit true darf die Physik-Engine die
--- Attachment bei Ragdoll und harten Anim-Übergängen lösen, dann fliegt das Prop weg.
local function attachProp(ped, model, bone, offset, rotation)
lib.requestModel(model)
local prop = CreateObject(model, 1.0, 1.0, 1.0, true, true, false)
SetEntityCollision(prop, false, false) --- Disable collision so it doesn't block bullets
AttachEntityToEntity(prop, ped, GetPedBoneIndex(ped, bone),
offset.x, offset.y, offset.z,
rotation.x, rotation.y, rotation.z,
false, -- p9
false, -- useSoftPinning ("if set to false attached entity will not detach when fixed")
false, -- collision
false, -- isPed
2, -- rotationOrder
true) -- syncRot
SetModelAsNoLongerNeeded(model)
return prop
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))
end
--- @param state boolean --- @param state boolean
--- @param notifyIfUnavailable boolean? Rückmeldung, wenn das Outfit keine Lampe hat --- @param notifyIfUnavailable boolean? Rückmeldung, wenn das Outfit keine Lampe hat
local function setLamp(state, notifyIfUnavailable) local function setLamp(state, notifyIfUnavailable)
@@ -187,18 +144,10 @@ local function setLamp(state, notifyIfUnavailable)
-- aber falls sie bei einem Ped-Model doch nicht anschlägt, ist eine Meldung -- aber falls sie bei einem Ped-Model doch nicht anschlägt, ist eine Meldung
-- besser als ein scheinbar kaputter Keybind. -- besser als ein scheinbar kaputter Keybind.
if state and notifyIfUnavailable and not IsScubaGearLightEnabled(cache.ped) then if state and notifyIfUnavailable and not IsScubaGearLightEnabled(cache.ped) then
exports.qbx_core:Notify(locale('error.no_lamp_on_outfit'), 'error') lib.notify({ description = locale('error.no_lamp_on_outfit'), type = 'error' })
end end
end end
local function isPropIntact(prop)
return prop ~= 0 and DoesEntityExist(prop) and GetEntityAttachedTo(prop) == cache.ped
end
local function isGearIntact()
return isPropIntact(currentGear.mask) and isPropIntact(currentGear.tank)
end
--- Schreibt die Restluft in die Metadata der Flasche. Der Server clampt den Wert --- Schreibt die Restluft in die Metadata der Flasche. Der Server clampt den Wert
--- und gibt den akzeptierten Stand zurück, damit Client und Inventar nicht auseinanderlaufen. --- und gibt den akzeptierten Stand zurück, damit Client und Inventar nicht auseinanderlaufen.
--- @param immediate boolean? true = Fire-and-Forget (Tod, Resource-Stop) --- @param immediate boolean? true = Fire-and-Forget (Tod, Resource-Stop)
@@ -220,8 +169,10 @@ end
--- Gemeinsamer Teardown für Ablegen, leere Flasche und Tod. --- Gemeinsamer Teardown für Ablegen, leere Flasche und Tod.
--- @param outOfAir boolean? --- @param outOfAir boolean?
--- @param immediateSync boolean? --- @param immediateSync boolean?
local function removeGear(outOfAir, immediateSync) --- @param ohneSync boolean? nach einem Patronentausch: der Client-Stand gehoert
syncTank(immediateSync) --- zur ALTEN Patrone und darf die neue nicht ueberschreiben
local function removeGear(outOfAir, immediateSync, ohneSync)
if not ohneSync then syncTank(immediateSync) end
currentGear.enabled = false currentGear.enabled = false
setLamp(false) setLamp(false)
@@ -232,7 +183,6 @@ local function removeGear(outOfAir, immediateSync)
narcosisDamageBuffer = 0.0 narcosisDamageBuffer = 0.0
engineDepthWarned = false engineDepthWarned = false
deleteGear()
disableScuba(outOfAir) disableScuba(outOfAir)
-- Stop breathing suit audio -- Stop breathing suit audio
end end
@@ -272,7 +222,7 @@ end
lib.callback.register('d4rk_divegear:client:fillTank', function(tanks) lib.callback.register('d4rk_divegear:client:fillTank', function(tanks)
if IsPedSwimmingUnderWater(cache.ped) then if IsPedSwimmingUnderWater(cache.ped) then
exports.qbx_core:Notify(locale('error.underwater'), 'error') lib.notify({ description = locale('error.underwater'), type = 'error' })
return return
end end
@@ -281,7 +231,7 @@ lib.callback.register('d4rk_divegear:client:fillTank', function(tanks)
if not tank then return end if not tank then return end
if tank.oxygen >= tank.capacity then if tank.oxygen >= tank.capacity then
exports.qbx_core:Notify(locale('error.tank_already_full'), 'error') lib.notify({ description = locale('error.tank_already_full'), type = 'error' })
return return
end end
@@ -314,23 +264,6 @@ RegisterNetEvent('d4rk_divegear:client:tankRefilled', function(slot, capacity)
enableScuba() enableScuba()
end) end)
local function takeOffSuit()
if lib.progressBar({
duration = config.takeOffSuitTimeMs,
label = locale('info.pullout_suit'),
useWhileDead = false,
canCancel = true,
anim = {
dict = 'clothingshirt',
clip = 'try_shirt_positive_d',
blendIn = 8.0
}
}) then
removeGear()
exports.qbx_core:Notify(locale('success.took_out'))
end
end
--- Manometer-Anzeige. Bei konstantem Volumen ist der Druck proportional zur --- Manometer-Anzeige. Bei konstantem Volumen ist der Druck proportional zur
--- verbliebenen Gasmenge, deshalb reicht der lineare Anteil an der Füllung. --- verbliebenen Gasmenge, deshalb reicht der lineare Anteil an der Füllung.
--- @return number bar --- @return number bar
@@ -411,7 +344,9 @@ local function startHudThread()
lamp = locale('hud.lamp') lamp = locale('hud.lamp')
}) })
while currentGear.enabled do local meinLauf = gearLauf
while currentGear.enabled and meinLauf == gearLauf do
currentDepth = currentDepth + (getDepth() - currentDepth) * 0.25 currentDepth = currentDepth + (getDepth() - currentDepth) * 0.25
local visible = config.hudEnabled local visible = config.hudEnabled
@@ -432,6 +367,9 @@ local function startHudThread()
Wait(config.hudUpdateIntervalMs) Wait(config.hudUpdateIntervalMs)
end end
-- Abgeloest durch einen neuen Faden (Patronentausch): der zeigt weiter.
if meinLauf ~= gearLauf and currentGear.enabled then return end
currentDepth = 0.0 currentDepth = 0.0
sendHudState(false) sendHudState(false)
end) end)
@@ -451,7 +389,7 @@ local function updateNarcosis(elapsed)
if currentDepth >= engineWarnAt then if currentDepth >= engineWarnAt then
if not engineDepthWarned then if not engineDepthWarned then
engineDepthWarned = true engineDepthWarned = true
exports.qbx_core:Notify(locale('error.crush_depth', math.floor(config.narcosis.engineDepthLimit)), 'error') lib.notify({ description = locale('error.crush_depth', math.floor(config.narcosis.engineDepthLimit)), type = 'error' })
end end
else else
engineDepthWarned = false engineDepthWarned = false
@@ -471,7 +409,7 @@ local function updateNarcosis(elapsed)
if activeTank and over >= -config.narcosis.warnBeforeMeters then if activeTank and over >= -config.narcosis.warnBeforeMeters then
if not narcosisWarned then if not narcosisWarned then
narcosisWarned = true narcosisWarned = true
exports.qbx_core:Notify(locale('info.depth_limit', math.floor(activeTank.maxDepth or 0)), 'warning') lib.notify({ description = locale('info.depth_limit', math.floor(activeTank.maxDepth or 0)), type = 'warning' })
end end
else else
narcosisWarned = false narcosisWarned = false
@@ -502,8 +440,9 @@ local function startOxygenLevelDecrementerThread()
CreateThread(function() CreateThread(function()
local lastTick = GetGameTimer() local lastTick = GetGameTimer()
local sinceSync = 0.0 local sinceSync = 0.0
local meinLauf = gearLauf
while currentGear.enabled do while currentGear.enabled and meinLauf == gearLauf do
Wait(250) Wait(250)
local now = GetGameTimer() local now = GetGameTimer()
@@ -522,13 +461,13 @@ local function startOxygenLevelDecrementerThread()
if activeTank.oxygen <= threshold and not warned[threshold] then if activeTank.oxygen <= threshold and not warned[threshold] then
warned[threshold] = true warned[threshold] = true
exports.qbx_core:Notify(locale('info.oxygen_low', formatTime(activeTank.oxygen)), 'warning') lib.notify({ description = locale('info.oxygen_low', formatTime(activeTank.oxygen)), type = 'warning' })
end end
end end
if activeTank.oxygen <= 0 then if activeTank.oxygen <= 0 then
disableScuba(true) disableScuba(true)
exports.qbx_core:Notify(locale('error.out_of_air'), 'error') lib.notify({ description = locale('error.out_of_air'), type = 'error' })
syncTank() syncTank()
sinceSync = 0.0 sinceSync = 0.0
-- Stop breathing suit audio -- Stop breathing suit audio
@@ -554,17 +493,14 @@ end
--- und Fahrzeugausstieg vom Spiel zurückgesetzt werden. --- und Fahrzeugausstieg vom Spiel zurückgesetzt werden.
local function startGearWatchdogThread() local function startGearWatchdogThread()
CreateThread(function() CreateThread(function()
while currentGear.enabled do local meinLauf = gearLauf
while currentGear.enabled and meinLauf == gearLauf do
if config.removeGearOnDeath and IsPedDeadOrDying(cache.ped, true) then if config.removeGearOnDeath and IsPedDeadOrDying(cache.ped, true) then
removeGear(false, true) removeGear(false, true)
break break
end end
if not isGearIntact() then
deleteGear()
attachGear()
end
if activeTank and activeTank.oxygen > 0 then if activeTank and activeTank.oxygen > 0 then
enableScuba() enableScuba()
end end
@@ -574,31 +510,94 @@ local function startGearWatchdogThread()
end) end)
end end
local function putOnSuit() --[[
ANLEGEN - 28.09.2026. Kein Balken und keine Props mehr: angezogen wird im
Inventar (Flasche und Maske sind Kleidung). Hier wird nur das Tauchgeraet
scharf geschaltet, wenn der Server sagt, dass beides getragen wird und
Luft in der Patrone ist.
]]
--- @param tank table aus d4rk_divegear:server:getWorn
--- @return boolean
local function anlegen(tank)
if not tank or (tank.oxygen or 0) <= 0 then return false end
activeTank = tank
warned = {}
gearLauf = gearLauf + 1
enableScuba()
currentGear.enabled = true
startOxygenLevelDecrementerThread()
startHudThread()
startGearWatchdogThread()
return true
end
--[[
NACHSEHEN, WAS ER TRAEGT - bei jeder Inventaraenderung und nach jedem
Umziehen. Mehrere Anstoesse in kurzer Folge fuehren zu EINER Frage mehr,
nicht zu vielen parallelen.
]]
local pruefenLaeuft, pruefenNochmal = false, false
local function pruefen()
if pruefenLaeuft then
pruefenNochmal = true
return
end
pruefenLaeuft = true
CreateThread(function()
repeat
pruefenNochmal = false
local erg = lib.callback.await('d4rk_divegear:server:getWorn', false)
if type(erg) == 'table' then
if erg.ok then
if not currentGear.enabled then
if anlegen(erg.tank) then
lib.notify({ description = locale('info.bereit', formatTime(erg.tank.oxygen)), type = 'info' })
end
elseif activeTank and activeTank.name ~= erg.tank.name then
-- Patrone getauscht: der Client-Stand gehoert zur alten.
removeGear(false, false, true)
anlegen(erg.tank)
end
elseif currentGear.enabled then
lib.notify({ description = locale('error.' .. (erg.grund or 'gear_gone')), type = 'error' })
removeGear(false)
end
end
until not pruefenNochmal
pruefenLaeuft = false
end)
end
AddEventHandler('d4rk:kleidung:angezogen', pruefen)
AddEventHandler('ox_inventory:updateInventory', pruefen)
CreateThread(function()
Wait(2000)
pruefen()
end)
--[[
EINE PATRONE BENUTZEN = SIE EINSETZEN (Server: CreateUseableItem je
Patrone). Nur an Land, mit Balken - wie vorher das Anlegen.
]]
RegisterNetEvent('d4rk_divegear:client:patroneEinsetzen', function(slot)
if IsPedSwimming(cache.ped) or cache.vehicle then if IsPedSwimming(cache.ped) or cache.vehicle then
exports.qbx_core:Notify(locale('error.not_standing_up'), 'error') lib.notify({ description = locale('error.not_standing_up'), type = 'error' })
return return
end end
local tanks = lib.callback.await('d4rk_divegear:server:getTanks', false) if not lib.progressBar({
if not tanks or #tanks == 0 then
exports.qbx_core:Notify(locale('error.need_otube'), 'error')
return
end
local tank = chooseTank(tanks)
if not tank then return end
if tank.oxygen <= 0 then
exports.qbx_core:Notify(locale('error.tank_empty'), 'error')
return
end
if lib.progressBar({
duration = config.putOnSuitTimeMs, duration = config.putOnSuitTimeMs,
label = locale('info.put_suit'), label = locale('info.patrone_einsetzen'),
useWhileDead = false, useWhileDead = false,
canCancel = true, canCancel = true,
anim = { anim = {
@@ -607,26 +606,22 @@ local function putOnSuit()
blendIn = 8.0 blendIn = 8.0
} }
}) then }) then
activeTank = tank return
warned = {}
deleteGear()
attachGear()
enableScuba()
currentGear.enabled = true
-- Initiate breathing suit audio
startOxygenLevelDecrementerThread()
startHudThread()
startGearWatchdogThread()
end
end end
RegisterNetEvent('d4rk_divegear:client:useGear', function() -- Die laufende Luft zuerst sichern - danach gehoert der Client-Stand zur
if currentGear.enabled then -- alten Patrone und wird nicht mehr geschrieben (pruefen -> removeGear ohneSync).
takeOffSuit() if currentGear.enabled then syncTank() end
else
putOnSuit() local ok, grund = lib.callback.await('d4rk_divegear:server:patroneTauschen', false, slot)
if not ok then
lib.notify({ description = locale('error.' .. tostring(grund or 'fehler')), type = 'error' })
return
end end
lib.notify({ description = locale('success.patrone_getauscht'), type = 'success' })
pruefen()
end) end)
lib.addKeybind({ lib.addKeybind({
@@ -659,60 +654,17 @@ lib.addKeybind({
lib.onCache('ped', function(ped) lib.onCache('ped', function(ped)
if not currentGear.enabled then return end if not currentGear.enabled then return end
deleteGear()
attachGear(ped)
if activeTank and activeTank.oxygen > 0 then if activeTank and activeTank.oxygen > 0 then
enableScuba(ped) enableScuba(ped)
end end
end) end)
--- Ausrüstung und Flasche können während des Tauchgangs das Inventar verlassen -
--- weggegeben, abgelegt, vom Admin entfernt. Ohne diese Prüfung hielt der Client
--- nur eine Slot-Nummer, die niemand mehr gegenprüfte, und man konnte unbegrenzt
--- weitertauchen. ox_inventory feuert bei jeder Änderung, also einmal nachfragen.
AddEventHandler('ox_inventory:updateInventory', function()
if not currentGear.enabled or not activeTank then return end
CreateThread(function()
local expected = activeTank
-- Der Thread startet erst im nächsten Tick, die Prüfung oben ist also
-- schon veraltet. Beim Ablegen passiert genau das: removeGear sichert die
-- Restluft, das ist selbst eine Inventar-Änderung, und bis hierher ist
-- activeTank längst nil.
if not expected or not currentGear.enabled then return end
local result = lib.callback.await('d4rk_divegear:server:verifyGear', false, expected.slot, expected.name)
-- Während des Callbacks kann die Ausrüstung schon abgelegt worden sein.
if not currentGear.enabled or activeTank ~= expected then return end
if not result then return end
if not result.gear then
exports.qbx_core:Notify(locale('error.gear_gone'), 'error')
removeGear(true)
return
end
if not result.tank then
exports.qbx_core:Notify(locale('error.tank_gone'), 'error')
removeGear(true)
return
end
activeTank.slot = result.tank
end)
end)
AddEventHandler('onResourceStop', function(resource) AddEventHandler('onResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end if resource ~= GetCurrentResourceName() then return end
syncTank(true) syncTank(true)
setLamp(false) setLamp(false)
applyNarcosisStage(0) applyNarcosisStage(0)
deleteGear()
SendNUIMessage({ action = 'state', visible = false }) SendNUIMessage({ action = 'state', visible = false })
SetEnableScuba(cache.ped, false) SetEnableScuba(cache.ped, false)
SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater) SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater)
+25 -3
View File
@@ -26,9 +26,31 @@ local config = {
{ name = 'diving_tank_trimix', capacity = 900, pressure = 300, maxDepth = 100.0, gas = 'Trimix' }, -- 15 Minuten, tieftauglich { name = 'diving_tank_trimix', capacity = 900, pressure = 300, maxDepth = 100.0, gas = 'Trimix' }, -- 15 Minuten, tieftauglich
}, },
--- Item, das die Ausrüstung an- und auszieht, und das Füll-Item. Stehen hier, --- SEIT 28.09.2026 SIND FLASCHE UND MASKE KLEIDUNG (plan-divegear.md).
--- weil sowohl der Useable-Item-Handler als auch die Besitzprüfung sie brauchen. --- Betreiber: "die Prop flasche zu einer richtigen taucherfalsche + maske
gearItem = 'diving_gear', --- welche wir im kleider laden kaufen koennen", und "die flaschen groessen
--- soll man an der normalen taucherfalsche austauschen koennen".
---
--- rigItem die Taucherflasche aus dem Kleiderladen (d4rk_garment -> item).
--- Sie traegt die eingesetzte Patrone in der Metadata:
--- patrone = Name aus tanks (unten), oxygen = Restsekunden.
--- maskItem die Tauchmaske aus dem Kleiderladen.
--- rigSlot / maskSlot wo sie getragen werden - d4rk_inventory
--- shared/d4rk_slots.lua:71-96: 31 Unterhemd (Komponente 8),
--- 29 Maske (Komponente 1). Welche Komponente die GTA-Taucherflasche
--- wirklich ist, steht erst nach der Messung (Schritt 0) fest.
---
--- Die fuenf `tanks` sind jetzt PATRONEN: Items im Inventar, die man an der
--- getragenen Flasche tauscht (Patrone benutzen). Die alten Props und das
--- Item diving_gear gibt es nicht mehr.
rigItem = 'diving_tank',
maskItem = 'diving_mask',
rigSlot = 31,
maskSlot = 29,
--- Das Fuell-Item. NICHT ANGELEGT: Nachfuellen kommt spaeter ueber einen
--- Shop oder Crafting (Betreiber, 28.09.2026). Der Weg bleibt stehen, damit
--- er sich dort anhaengen laesst - ohne das Item ist er nie erreichbar.
fillItem = 'diving_fill', fillItem = 'diving_fill',
--- Flaschen ohne Metadata als voll behandeln. Betrifft alles, was frisch aus --- Flaschen ohne Metadata als voll behandeln. Betrifft alles, was frisch aus
+4
View File
@@ -16,7 +16,11 @@ shared_scripts {
server_script 'server/main.lua' server_script 'server/main.lua'
client_scripts { client_scripts {
-- lib.notify im Hausdesign (d4rk_lib) - VOR allen, die melden.
'@d4rk_lib/client/meldung_shared.lua',
'client/main.lua', 'client/main.lua',
-- Die Konsole beim HUD anmelden (d4rk_hud, BauteilAnmelden).
'client/hud.lua',
'client/light.lua', 'client/light.lua',
-- Messung fuer die Umstellung auf Kleidung (plan-divegear.md Schritt 0). -- Messung fuer die Umstellung auf Kleidung (plan-divegear.md Schritt 0).
-- Wird nach der Messung wieder entfernt. -- Wird nach der Messung wieder entfernt.
+15 -4
View File
@@ -10,11 +10,19 @@
"no_lamp_on_outfit": "Deine Tauchausrüstung hat keine Lampe.", "no_lamp_on_outfit": "Deine Tauchausrüstung hat keine Lampe.",
"crush_depth": "Lebensgefahr! Ab %d m zerdrückt dich der Wasserdruck.", "crush_depth": "Lebensgefahr! Ab %d m zerdrückt dich der Wasserdruck.",
"gear_gone": "Deine Tauchausrüstung ist weg!", "gear_gone": "Deine Tauchausrüstung ist weg!",
"tank_gone": "Deine Tauchflasche ist weg!" "tank_gone": "Deine Tauchflasche ist weg!",
"keine_flasche": "Du trägst keine Taucherflasche.",
"keine_maske": "Ohne Tauchmaske kein Tauchgerät.",
"keine_patrone": "In deiner Taucherflasche steckt keine Patrone.",
"keine_patrone_im_slot": "Diese Patrone hast du nicht mehr.",
"kein_platz": "Kein Platz für die alte Patrone.",
"ungueltig": "Das geht nicht.",
"fehler": "Das hat nicht geklappt."
}, },
"success": { "success": {
"took_out": "Du hast deine Ausrüstung abgelegt!", "took_out": "Du hast deine Ausrüstung abgelegt!",
"tube_filled": "Du hast deine Tauchflasche erfolgreich aufgefüllt!" "tube_filled": "Du hast deine Tauchflasche erfolgreich aufgefüllt!",
"patrone_getauscht": "Patrone eingesetzt."
}, },
"info": { "info": {
"put_suit": "Tauchanzug wird angezogen...", "put_suit": "Tauchanzug wird angezogen...",
@@ -22,7 +30,9 @@
"filling_air": "Sauerstoff wird aufgefüllt...", "filling_air": "Sauerstoff wird aufgefüllt...",
"oxygen_low": "Nur noch %s Luft!", "oxygen_low": "Nur noch %s Luft!",
"tank_remaining": "%s verbleibend (%d%%) · %s bis %d m", "tank_remaining": "%s verbleibend (%d%%) · %s bis %d m",
"depth_limit": "Du näherst dich der Grenze deines Atemgases (%d m)!" "depth_limit": "Du näherst dich der Grenze deines Atemgases (%d m)!",
"bereit": "Tauchgerät bereit - %s Luft.",
"patrone_einsetzen": "Patrone wird eingesetzt..."
}, },
"menu": { "menu": {
"choose_tank": "Tauchflasche wählen" "choose_tank": "Tauchflasche wählen"
@@ -31,7 +41,8 @@
"air": "Luft", "air": "Luft",
"depth": "Tiefe", "depth": "Tiefe",
"pressure": "Druck", "pressure": "Druck",
"lamp": "Lampe" "lamp": "Lampe",
"konsole": "Tauchkonsole"
}, },
"keybind": { "keybind": {
"toggle_lamp": "Tauchlampe an/aus", "toggle_lamp": "Tauchlampe an/aus",
+15 -4
View File
@@ -10,11 +10,19 @@
"no_lamp_on_outfit": "Your diving suit doesn't have a lamp.", "no_lamp_on_outfit": "Your diving suit doesn't have a lamp.",
"crush_depth": "Danger! Below %d m the water pressure will crush you.", "crush_depth": "Danger! Below %d m the water pressure will crush you.",
"gear_gone": "Your diving gear is gone!", "gear_gone": "Your diving gear is gone!",
"tank_gone": "Your air tank is gone!" "tank_gone": "Your air tank is gone!",
"keine_flasche": "You are not wearing a scuba tank.",
"keine_maske": "No diving mask, no breathing apparatus.",
"keine_patrone": "There is no cartridge in your scuba tank.",
"keine_patrone_im_slot": "You no longer have that cartridge.",
"kein_platz": "No room for the old cartridge.",
"ungueltig": "That does not work.",
"fehler": "That did not work."
}, },
"success": { "success": {
"took_out": "You've taken your gear off!", "took_out": "You've taken your gear off!",
"tube_filled": "You've successfully refilled your air tank!" "tube_filled": "You've successfully refilled your air tank!",
"patrone_getauscht": "Cartridge inserted."
}, },
"info": { "info": {
"put_suit": "Putting on your diving suit...", "put_suit": "Putting on your diving suit...",
@@ -22,7 +30,9 @@
"filling_air": "Filling air...", "filling_air": "Filling air...",
"oxygen_low": "Only %s of air left!", "oxygen_low": "Only %s of air left!",
"tank_remaining": "%s remaining (%d%%) · %s down to %d m", "tank_remaining": "%s remaining (%d%%) · %s down to %d m",
"depth_limit": "You're approaching the limit of your breathing gas (%d m)!" "depth_limit": "You're approaching the limit of your breathing gas (%d m)!",
"bereit": "Breathing apparatus ready - %s of air.",
"patrone_einsetzen": "Inserting cartridge..."
}, },
"menu": { "menu": {
"choose_tank": "Choose an air tank" "choose_tank": "Choose an air tank"
@@ -31,7 +41,8 @@
"air": "Air", "air": "Air",
"depth": "Depth", "depth": "Depth",
"pressure": "Pressure", "pressure": "Pressure",
"lamp": "Lamp" "lamp": "Lamp",
"konsole": "Dive console"
}, },
"keybind": { "keybind": {
"toggle_lamp": "Toggle dive lamp", "toggle_lamp": "Toggle dive lamp",
+34 -3
View File
@@ -118,10 +118,35 @@ function formatTime(seconds) {
return `${mm}:${ss}`; return `${mm}:${ss}`;
} }
function render(state) { // Lage vom HUD (d4rk_hud, BauteilAnmelden - client/hud.lua). null = keine,
hud.classList.toggle('hidden', !state.visible); // dann gilt die Position aus style.css. 28.09.2026.
let lage = null;
let letzterStand = null;
if (!state.visible) return; function lageAnwenden() {
if (!lage) {
hud.classList.remove('gelagert');
hud.style.left = '';
hud.style.top = '';
hud.style.removeProperty('--lage-scale');
return;
}
hud.classList.add('gelagert');
hud.style.left = `${lage.links}px`;
hud.style.top = `${lage.oben}px`;
hud.style.setProperty('--lage-scale', String(lage.skalierung ?? 1));
}
function render(state) {
letzterStand = state;
// Im HUD abgeschaltet, Fotomodus, Auswahl: das HUD sagt "nicht sichtbar".
const zeigen = state.visible && (!lage || lage.sichtbar !== false);
hud.classList.toggle('hidden', !zeigen);
if (!zeigen) return;
const capacity = state.capacity > 0 ? state.capacity : 1; const capacity = state.capacity > 0 ? state.capacity : 1;
const ratio = Math.min(1, Math.max(0, state.oxygen / capacity)); const ratio = Math.min(1, Math.max(0, state.oxygen / capacity));
@@ -171,6 +196,12 @@ window.addEventListener('message', (event) => {
} }
if (data.action === 'state') render(data); if (data.action === 'state') render(data);
if (data.action === 'lage') {
lage = data.lage || null;
lageAnwenden();
if (letzterStand) render(letzterStand);
}
}); });
// Damit sich das HUD im Browser ohne FiveM anschauen laesst. // Damit sich das HUD im Browser ohne FiveM anschauen laesst.
+12
View File
@@ -60,6 +60,18 @@ body {
pointer-events: none; pointer-events: none;
} }
/* Lage vom HUD (28.09.2026): linke obere Ecke in Bildschirmpixeln, Skalierung
fuer den bei 1080p gebauten Inhalt - rechnet d4rk_hud, hier nur anwenden. */
#hud.gelagert {
bottom: auto;
transform: scale(var(--lage-scale, 1));
transform-origin: 0 0;
}
#hud.gelagert.hidden {
transform: scale(var(--lage-scale, 1)) translateY(14px);
}
.caption { .caption {
font-size: 9px; font-size: 9px;
font-weight: 600; font-weight: 600;
+120 -26
View File
@@ -26,7 +26,7 @@ end
local function getTankSlot(source, slotId) local function getTankSlot(source, slotId)
if type(slotId) ~= 'number' then return end if type(slotId) ~= 'number' then return end
local slot = exports.ox_inventory:GetSlot(source, slotId) local slot = exports.d4rk_inventory:GetSlot(source, slotId)
if not slot then return end if not slot then return end
@@ -37,6 +37,19 @@ local function getTankSlot(source, slotId)
return slot, tankConfig return slot, tankConfig
end end
--- Die getragene Taucherflasche (Kleidung, shared.rigSlot) und die Patrone darin.
--- @return table? slot nil = keine Flasche angezogen
--- @return table? tankConfig nil = keine (gueltige) Patrone eingesetzt
local function getRig(source)
local slot = exports.d4rk_inventory:GetSlot(source, shared.rigSlot)
if not slot or slot.name ~= shared.rigItem then return nil end
local patrone = slot.metadata and slot.metadata.patrone
return slot, patrone and shared.tankByName[patrone] or nil
end
--- Schreibt die Restluft in die Metadata. --- Schreibt die Restluft in die Metadata.
--- SetMetadata *ersetzt* die Metadata-Tabelle komplett (modules/inventory/server.lua), --- SetMetadata *ersetzt* die Metadata-Tabelle komplett (modules/inventory/server.lua),
--- deshalb wird der bestehende Inhalt kopiert statt nur oxygen gesetzt. --- deshalb wird der bestehende Inhalt kopiert statt nur oxygen gesetzt.
@@ -55,7 +68,7 @@ local function writeTank(source, slot, tankConfig, oxygen)
-- Items, sonst würde ox_inventory die leere Flasche löschen (items/server.lua). -- Items, sonst würde ox_inventory die leere Flasche löschen (items/server.lua).
metadata.durability = math.floor(oxygen / tankConfig.capacity * 100) metadata.durability = math.floor(oxygen / tankConfig.capacity * 100)
exports.ox_inventory:SetMetadata(source, slot.slot, metadata) exports.d4rk_inventory:SetMetadata(source, slot.slot, metadata)
return oxygen return oxygen
end end
@@ -64,7 +77,7 @@ end
--- @param source number --- @param source number
--- @return table[] --- @return table[]
local function getTankList(source) local function getTankList(source)
local found = exports.ox_inventory:Search(source, 'slots', shared.tankNames) local found = exports.d4rk_inventory:Search(source, 'slots', shared.tankNames)
local tanks = {} local tanks = {}
for i = 1, #shared.tankNames do for i = 1, #shared.tankNames do
@@ -79,7 +92,7 @@ local function getTankList(source)
tanks[#tanks + 1] = { tanks[#tanks + 1] = {
slot = slot.slot, slot = slot.slot,
name = name, name = name,
label = (exports.ox_inventory:Items(name) or {}).label or name, label = (exports.d4rk_inventory:Items(name) or {}).label or name,
oxygen = readOxygen(slot, tankConfig), oxygen = readOxygen(slot, tankConfig),
capacity = tankConfig.capacity, capacity = tankConfig.capacity,
pressure = tankConfig.pressure, pressure = tankConfig.pressure,
@@ -99,9 +112,11 @@ local function syncTank(source, slotId, oxygen)
if not oxygen then return end if not oxygen then return end
local slot, tankConfig = getTankSlot(source, slotId) -- Seit 28.09.2026 steckt die Luft in der GETRAGENEN Flasche (Kleidung);
-- slotId bleibt im Aufruf, zaehlt aber nicht mehr.
local slot, tankConfig = getRig(source)
if not slot then return end if not slot or not tankConfig then return end
local current = readOxygen(slot, tankConfig) local current = readOxygen(slot, tankConfig)
local now = os.time() local now = os.time()
@@ -118,28 +133,102 @@ end
lib.callback.register('d4rk_divegear:server:getTanks', getTankList) lib.callback.register('d4rk_divegear:server:getTanks', getTankList)
--- Prüft, ob der Spieler Ausrüstung und Flasche überhaupt noch besitzt. --[[
--- Ohne das konnte man beides im Tauchgang weggeben und trotzdem weiteratmen - WAS TRAEGT ER? - 28.09.2026, ersetzt verifyGear.
--- der Client hielt nur eine Slot-Nummer, die niemand mehr gegenprüfte.
--- @return { gear: boolean, tank: number? }
lib.callback.register('d4rk_divegear:server:verifyGear', function(source, slotId, name)
local gearCount = exports.ox_inventory:Search(source, 'count', shared.gearItem)
local slot = getTankSlot(source, slotId)
if slot and slot.name == name then Flasche (rigSlot) UND Maske (maskSlot) angezogen, und in der Flasche eine
return { gear = (gearCount or 0) > 0, tank = slotId } Patrone. Die Antwort nennt den Grund, wenn etwas fehlt - der Client fragt
end bei jeder Inventaraenderung, und "keine Maske" ist etwas anderes als "keine
Patrone".
]]
lib.callback.register('d4rk_divegear:server:getWorn', function(source)
local rig, tankConfig = getRig(source)
-- Nur umsortiert? Dann dieselbe Flaschensorte woanders im Inventar suchen, if not rig then return { ok = false, grund = 'keine_flasche' } end
-- sonst kostet jedes Verschieben im Inventar die Ausrüstung.
local found = shared.tankByName[name] and exports.ox_inventory:Search(source, 'slots', name) local maske = exports.d4rk_inventory:GetSlot(source, shared.maskSlot)
if not maske or maske.name ~= shared.maskItem then return { ok = false, grund = 'keine_maske' } end
if not tankConfig then return { ok = false, grund = 'keine_patrone' } end
local patrone = rig.metadata.patrone
return { return {
gear = (gearCount or 0) > 0, ok = true,
tank = found and found[1] and found[1].slot or nil tank = {
slot = rig.slot,
name = patrone,
label = (exports.d4rk_inventory:Items(patrone) or {}).label or patrone,
oxygen = readOxygen(rig, tankConfig),
capacity = tankConfig.capacity,
pressure = tankConfig.pressure,
maxDepth = tankConfig.maxDepth,
gas = tankConfig.gas,
},
} }
end) end)
--[[
PATRONE TAUSCHEN - 28.09.2026. Die benutzte Patrone wandert in die
getragene Flasche, die alte kommt mit ihrer Restluft als Item zurueck.
REIHENFOLGE: erst pruefen, ob die alte Patrone ueberhaupt Platz hat, dann
die neue nehmen, dann die alte geben - scheitert das Geben, bekommt der
Spieler die neue zurueck. Zuletzt die Flasche beschreiben (Metadata
KOPIEREN, SetMetadata ersetzt die ganze Tabelle - ENTSCHEIDUNGEN.md).
]]
lib.callback.register('d4rk_divegear:server:patroneTauschen', function(source, cartSlot)
cartSlot = math.tointeger(tonumber(cartSlot))
if not cartSlot or cartSlot == shared.rigSlot then return false, 'ungueltig' end
local cart, cartConfig = getTankSlot(source, cartSlot)
if not cart then return false, 'keine_patrone_im_slot' end
local rig, altConfig = getRig(source)
if not rig then return false, 'keine_flasche' end
local altName = rig.metadata and rig.metadata.patrone
local altLuft = altConfig and readOxygen(rig, altConfig) or nil
if altConfig and not exports.d4rk_inventory:CanCarryItem(source, altName, 1) then
return false, 'kein_platz'
end
local neuLuft = readOxygen(cart, cartConfig)
if not exports.d4rk_inventory:RemoveItem(source, cart.name, 1, nil, cartSlot) then
return false, 'fehler'
end
if altConfig then
local meta = {
oxygen = math.floor(altLuft),
durability = math.floor(altLuft / altConfig.capacity * 100),
}
if not exports.d4rk_inventory:AddItem(source, altName, 1, meta) then
exports.d4rk_inventory:AddItem(source, cart.name, 1, cart.metadata)
return false, 'kein_platz'
end
end
local meta = {}
for key, value in pairs(rig.metadata or {}) do meta[key] = value end
meta.patrone = cart.name
meta.oxygen = math.floor(neuLuft)
meta.durability = math.floor(neuLuft / cartConfig.capacity * 100)
exports.d4rk_inventory:SetMetadata(source, rig.slot, meta)
lastSync[source] = os.time()
return true
end)
lib.callback.register('d4rk_divegear:server:syncTank', function(source, slotId, oxygen) lib.callback.register('d4rk_divegear:server:syncTank', function(source, slotId, oxygen)
return syncTank(source, slotId, oxygen) return syncTank(source, slotId, oxygen)
end) end)
@@ -154,15 +243,20 @@ AddEventHandler('playerDropped', function()
lastSync[source] = nil lastSync[source] = nil
end) end)
exports.qbx_core:CreateUseableItem(shared.gearItem, function(source) -- Eine Patrone benutzen = sie in die getragene Flasche einsetzen (28.09.2026).
TriggerClientEvent('d4rk_divegear:client:useGear', source) -- d4rk_inventory ruft den Rueckruf mit den Itemdaten samt Slot
-- (modules/bridge/qbx/server.lua: cb(source, data)).
for i = 1, #shared.tankNames do
exports.qbx_core:CreateUseableItem(shared.tankNames[i], function(source, data)
TriggerClientEvent('d4rk_divegear:client:patroneEinsetzen', source, data and data.slot)
end) end)
end
exports.qbx_core:CreateUseableItem(shared.fillItem, function(source) exports.qbx_core:CreateUseableItem(shared.fillItem, function(source)
local tanks = getTankList(source) local tanks = getTankList(source)
if #tanks == 0 then if #tanks == 0 then
exports.qbx_core:Notify(source, locale('error.no_tank'), 'error') exports.d4rk_bridge:Notify(source, locale('error.no_tank'), 'error')
return return
end end
@@ -175,12 +269,12 @@ exports.qbx_core:CreateUseableItem(shared.fillItem, function(source)
if not slot then return end if not slot then return end
-- Nach dem Progressbar nochmal prüfen: das Item kann in der Zwischenzeit weg sein. -- Nach dem Progressbar nochmal prüfen: das Item kann in der Zwischenzeit weg sein.
if not exports.ox_inventory:RemoveItem(source, shared.fillItem, 1) then return end if not exports.d4rk_inventory:RemoveItem(source, shared.fillItem, 1) then return end
writeTank(source, slot, tankConfig, tankConfig.capacity) writeTank(source, slot, tankConfig, tankConfig.capacity)
lastSync[source] = os.time() lastSync[source] = os.time()
-- Erst melden wenn das Item tatsächlich weg und die Flasche voll ist. -- Erst melden wenn das Item tatsächlich weg und die Flasche voll ist.
exports.qbx_core:Notify(source, locale('success.tube_filled'), 'success') exports.d4rk_bridge:Notify(source, locale('success.tube_filled'), 'success')
TriggerClientEvent('d4rk_divegear:client:tankRefilled', source, slot.slot, tankConfig.capacity) TriggerClientEvent('d4rk_divegear:client:tankRefilled', source, slot.slot, tankConfig.capacity)
end) end)