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:
1 parent
930ff6381f
commit
e64a1bd3d0
9 files changed
+401
-214
No files matched your search
@@ -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
@@ -1,12 +1,17 @@
|
||||
local config = require 'config.client'
|
||||
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 = {
|
||||
mask = 0,
|
||||
tank = 0,
|
||||
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,
|
||||
--- gesteuert über den replizierten Statebag.
|
||||
local lampOn = false
|
||||
@@ -32,10 +37,6 @@ for i = 1, #config.warnAtSeconds do
|
||||
if dangerThreshold == 0 or threshold < dangerThreshold then dangerThreshold = threshold 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
|
||||
--- @return string mm:ss
|
||||
@@ -127,50 +128,6 @@ local function disableScuba(outOfAir, ped)
|
||||
SetPedMaxTimeUnderwater(ped, outOfAir and config.maxTimeUnderwaterOutOfAir or config.maxTimeUnderwater)
|
||||
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 notifyIfUnavailable boolean? Rückmeldung, wenn das Outfit keine Lampe hat
|
||||
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
|
||||
-- besser als ein scheinbar kaputter Keybind.
|
||||
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
|
||||
|
||||
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
|
||||
--- und gibt den akzeptierten Stand zurück, damit Client und Inventar nicht auseinanderlaufen.
|
||||
--- @param immediate boolean? true = Fire-and-Forget (Tod, Resource-Stop)
|
||||
@@ -220,8 +169,10 @@ end
|
||||
--- Gemeinsamer Teardown für Ablegen, leere Flasche und Tod.
|
||||
--- @param outOfAir boolean?
|
||||
--- @param immediateSync boolean?
|
||||
local function removeGear(outOfAir, immediateSync)
|
||||
syncTank(immediateSync)
|
||||
--- @param ohneSync boolean? nach einem Patronentausch: der Client-Stand gehoert
|
||||
--- 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
|
||||
setLamp(false)
|
||||
@@ -232,7 +183,6 @@ local function removeGear(outOfAir, immediateSync)
|
||||
narcosisDamageBuffer = 0.0
|
||||
engineDepthWarned = false
|
||||
|
||||
deleteGear()
|
||||
disableScuba(outOfAir)
|
||||
-- Stop breathing suit audio
|
||||
end
|
||||
@@ -272,7 +222,7 @@ end
|
||||
|
||||
lib.callback.register('d4rk_divegear:client:fillTank', function(tanks)
|
||||
if IsPedSwimmingUnderWater(cache.ped) then
|
||||
exports.qbx_core:Notify(locale('error.underwater'), 'error')
|
||||
lib.notify({ description = locale('error.underwater'), type = 'error' })
|
||||
return
|
||||
end
|
||||
|
||||
@@ -281,7 +231,7 @@ lib.callback.register('d4rk_divegear:client:fillTank', function(tanks)
|
||||
if not tank then return end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@@ -314,23 +264,6 @@ RegisterNetEvent('d4rk_divegear:client:tankRefilled', function(slot, capacity)
|
||||
enableScuba()
|
||||
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
|
||||
--- verbliebenen Gasmenge, deshalb reicht der lineare Anteil an der Füllung.
|
||||
--- @return number bar
|
||||
@@ -411,7 +344,9 @@ local function startHudThread()
|
||||
lamp = locale('hud.lamp')
|
||||
})
|
||||
|
||||
while currentGear.enabled do
|
||||
local meinLauf = gearLauf
|
||||
|
||||
while currentGear.enabled and meinLauf == gearLauf do
|
||||
currentDepth = currentDepth + (getDepth() - currentDepth) * 0.25
|
||||
|
||||
local visible = config.hudEnabled
|
||||
@@ -432,6 +367,9 @@ local function startHudThread()
|
||||
Wait(config.hudUpdateIntervalMs)
|
||||
end
|
||||
|
||||
-- Abgeloest durch einen neuen Faden (Patronentausch): der zeigt weiter.
|
||||
if meinLauf ~= gearLauf and currentGear.enabled then return end
|
||||
|
||||
currentDepth = 0.0
|
||||
sendHudState(false)
|
||||
end)
|
||||
@@ -451,7 +389,7 @@ local function updateNarcosis(elapsed)
|
||||
if currentDepth >= engineWarnAt then
|
||||
if not engineDepthWarned then
|
||||
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
|
||||
else
|
||||
engineDepthWarned = false
|
||||
@@ -471,7 +409,7 @@ local function updateNarcosis(elapsed)
|
||||
if activeTank and over >= -config.narcosis.warnBeforeMeters then
|
||||
if not narcosisWarned then
|
||||
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
|
||||
else
|
||||
narcosisWarned = false
|
||||
@@ -502,8 +440,9 @@ local function startOxygenLevelDecrementerThread()
|
||||
CreateThread(function()
|
||||
local lastTick = GetGameTimer()
|
||||
local sinceSync = 0.0
|
||||
local meinLauf = gearLauf
|
||||
|
||||
while currentGear.enabled do
|
||||
while currentGear.enabled and meinLauf == gearLauf do
|
||||
Wait(250)
|
||||
|
||||
local now = GetGameTimer()
|
||||
@@ -522,13 +461,13 @@ local function startOxygenLevelDecrementerThread()
|
||||
|
||||
if activeTank.oxygen <= threshold and not warned[threshold] then
|
||||
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
|
||||
|
||||
if activeTank.oxygen <= 0 then
|
||||
disableScuba(true)
|
||||
exports.qbx_core:Notify(locale('error.out_of_air'), 'error')
|
||||
lib.notify({ description = locale('error.out_of_air'), type = 'error' })
|
||||
syncTank()
|
||||
sinceSync = 0.0
|
||||
-- Stop breathing suit audio
|
||||
@@ -554,17 +493,14 @@ end
|
||||
--- und Fahrzeugausstieg vom Spiel zurückgesetzt werden.
|
||||
local function startGearWatchdogThread()
|
||||
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
|
||||
removeGear(false, true)
|
||||
break
|
||||
end
|
||||
|
||||
if not isGearIntact() then
|
||||
deleteGear()
|
||||
attachGear()
|
||||
end
|
||||
|
||||
if activeTank and activeTank.oxygen > 0 then
|
||||
enableScuba()
|
||||
end
|
||||
@@ -574,31 +510,94 @@ local function startGearWatchdogThread()
|
||||
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
|
||||
exports.qbx_core:Notify(locale('error.not_standing_up'), 'error')
|
||||
lib.notify({ description = locale('error.not_standing_up'), type = 'error' })
|
||||
return
|
||||
end
|
||||
|
||||
local tanks = lib.callback.await('d4rk_divegear:server:getTanks', false)
|
||||
|
||||
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({
|
||||
if not lib.progressBar({
|
||||
duration = config.putOnSuitTimeMs,
|
||||
label = locale('info.put_suit'),
|
||||
label = locale('info.patrone_einsetzen'),
|
||||
useWhileDead = false,
|
||||
canCancel = true,
|
||||
anim = {
|
||||
@@ -607,26 +606,22 @@ local function putOnSuit()
|
||||
blendIn = 8.0
|
||||
}
|
||||
}) then
|
||||
activeTank = tank
|
||||
warned = {}
|
||||
|
||||
deleteGear()
|
||||
attachGear()
|
||||
enableScuba()
|
||||
currentGear.enabled = true
|
||||
-- Initiate breathing suit audio
|
||||
startOxygenLevelDecrementerThread()
|
||||
startHudThread()
|
||||
startGearWatchdogThread()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent('d4rk_divegear:client:useGear', function()
|
||||
if currentGear.enabled then
|
||||
takeOffSuit()
|
||||
else
|
||||
putOnSuit()
|
||||
-- Die laufende Luft zuerst sichern - danach gehoert der Client-Stand zur
|
||||
-- alten Patrone und wird nicht mehr geschrieben (pruefen -> removeGear ohneSync).
|
||||
if currentGear.enabled then syncTank() end
|
||||
|
||||
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
|
||||
|
||||
lib.notify({ description = locale('success.patrone_getauscht'), type = 'success' })
|
||||
pruefen()
|
||||
end)
|
||||
|
||||
lib.addKeybind({
|
||||
@@ -659,60 +654,17 @@ lib.addKeybind({
|
||||
lib.onCache('ped', function(ped)
|
||||
if not currentGear.enabled then return end
|
||||
|
||||
deleteGear()
|
||||
attachGear(ped)
|
||||
|
||||
if activeTank and activeTank.oxygen > 0 then
|
||||
enableScuba(ped)
|
||||
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)
|
||||
if resource ~= GetCurrentResourceName() then return end
|
||||
|
||||
syncTank(true)
|
||||
setLamp(false)
|
||||
applyNarcosisStage(0)
|
||||
deleteGear()
|
||||
SendNUIMessage({ action = 'state', visible = false })
|
||||
SetEnableScuba(cache.ped, false)
|
||||
SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater)
|
||||
|
||||
+25
-3
@@ -26,9 +26,31 @@ local config = {
|
||||
{ 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,
|
||||
--- weil sowohl der Useable-Item-Handler als auch die Besitzprüfung sie brauchen.
|
||||
gearItem = 'diving_gear',
|
||||
--- SEIT 28.09.2026 SIND FLASCHE UND MASKE KLEIDUNG (plan-divegear.md).
|
||||
--- Betreiber: "die Prop flasche zu einer richtigen taucherfalsche + maske
|
||||
--- 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',
|
||||
|
||||
--- Flaschen ohne Metadata als voll behandeln. Betrifft alles, was frisch aus
|
||||
|
||||
@@ -16,7 +16,11 @@ shared_scripts {
|
||||
server_script 'server/main.lua'
|
||||
|
||||
client_scripts {
|
||||
-- lib.notify im Hausdesign (d4rk_lib) - VOR allen, die melden.
|
||||
'@d4rk_lib/client/meldung_shared.lua',
|
||||
'client/main.lua',
|
||||
-- Die Konsole beim HUD anmelden (d4rk_hud, BauteilAnmelden).
|
||||
'client/hud.lua',
|
||||
'client/light.lua',
|
||||
-- Messung fuer die Umstellung auf Kleidung (plan-divegear.md Schritt 0).
|
||||
-- Wird nach der Messung wieder entfernt.
|
||||
|
||||
+15
-4
@@ -10,11 +10,19 @@
|
||||
"no_lamp_on_outfit": "Deine Tauchausrüstung hat keine Lampe.",
|
||||
"crush_depth": "Lebensgefahr! Ab %d m zerdrückt dich der Wasserdruck.",
|
||||
"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": {
|
||||
"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": {
|
||||
"put_suit": "Tauchanzug wird angezogen...",
|
||||
@@ -22,7 +30,9 @@
|
||||
"filling_air": "Sauerstoff wird aufgefüllt...",
|
||||
"oxygen_low": "Nur noch %s Luft!",
|
||||
"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": {
|
||||
"choose_tank": "Tauchflasche wählen"
|
||||
@@ -31,7 +41,8 @@
|
||||
"air": "Luft",
|
||||
"depth": "Tiefe",
|
||||
"pressure": "Druck",
|
||||
"lamp": "Lampe"
|
||||
"lamp": "Lampe",
|
||||
"konsole": "Tauchkonsole"
|
||||
},
|
||||
"keybind": {
|
||||
"toggle_lamp": "Tauchlampe an/aus",
|
||||
|
||||
+15
-4
@@ -10,11 +10,19 @@
|
||||
"no_lamp_on_outfit": "Your diving suit doesn't have a lamp.",
|
||||
"crush_depth": "Danger! Below %d m the water pressure will crush you.",
|
||||
"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": {
|
||||
"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": {
|
||||
"put_suit": "Putting on your diving suit...",
|
||||
@@ -22,7 +30,9 @@
|
||||
"filling_air": "Filling air...",
|
||||
"oxygen_low": "Only %s of air left!",
|
||||
"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": {
|
||||
"choose_tank": "Choose an air tank"
|
||||
@@ -31,7 +41,8 @@
|
||||
"air": "Air",
|
||||
"depth": "Depth",
|
||||
"pressure": "Pressure",
|
||||
"lamp": "Lamp"
|
||||
"lamp": "Lamp",
|
||||
"konsole": "Dive console"
|
||||
},
|
||||
"keybind": {
|
||||
"toggle_lamp": "Toggle dive lamp",
|
||||
|
||||
+34
-3
@@ -118,10 +118,35 @@ function formatTime(seconds) {
|
||||
return `${mm}:${ss}`;
|
||||
}
|
||||
|
||||
function render(state) {
|
||||
hud.classList.toggle('hidden', !state.visible);
|
||||
// Lage vom HUD (d4rk_hud, BauteilAnmelden - client/hud.lua). null = keine,
|
||||
// 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 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 === 'lage') {
|
||||
lage = data.lage || null;
|
||||
lageAnwenden();
|
||||
if (letzterStand) render(letzterStand);
|
||||
}
|
||||
});
|
||||
|
||||
// Damit sich das HUD im Browser ohne FiveM anschauen laesst.
|
||||
|
||||
@@ -60,6 +60,18 @@ body {
|
||||
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 {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
|
||||
+121
-27
@@ -26,7 +26,7 @@ end
|
||||
local function getTankSlot(source, slotId)
|
||||
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
|
||||
|
||||
@@ -37,6 +37,19 @@ local function getTankSlot(source, slotId)
|
||||
return slot, tankConfig
|
||||
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.
|
||||
--- SetMetadata *ersetzt* die Metadata-Tabelle komplett (modules/inventory/server.lua),
|
||||
--- 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).
|
||||
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
|
||||
end
|
||||
@@ -64,7 +77,7 @@ end
|
||||
--- @param source number
|
||||
--- @return table[]
|
||||
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 = {}
|
||||
|
||||
for i = 1, #shared.tankNames do
|
||||
@@ -79,7 +92,7 @@ local function getTankList(source)
|
||||
tanks[#tanks + 1] = {
|
||||
slot = slot.slot,
|
||||
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),
|
||||
capacity = tankConfig.capacity,
|
||||
pressure = tankConfig.pressure,
|
||||
@@ -99,9 +112,11 @@ local function syncTank(source, slotId, oxygen)
|
||||
|
||||
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 now = os.time()
|
||||
@@ -118,28 +133,102 @@ end
|
||||
|
||||
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 -
|
||||
--- 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)
|
||||
--[[
|
||||
WAS TRAEGT ER? - 28.09.2026, ersetzt verifyGear.
|
||||
|
||||
if slot and slot.name == name then
|
||||
return { gear = (gearCount or 0) > 0, tank = slotId }
|
||||
end
|
||||
Flasche (rigSlot) UND Maske (maskSlot) angezogen, und in der Flasche eine
|
||||
Patrone. Die Antwort nennt den Grund, wenn etwas fehlt - der Client fragt
|
||||
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,
|
||||
-- sonst kostet jedes Verschieben im Inventar die Ausrüstung.
|
||||
local found = shared.tankByName[name] and exports.ox_inventory:Search(source, 'slots', name)
|
||||
if not rig then return { ok = false, grund = 'keine_flasche' } end
|
||||
|
||||
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 {
|
||||
gear = (gearCount or 0) > 0,
|
||||
tank = found and found[1] and found[1].slot or nil
|
||||
ok = true,
|
||||
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)
|
||||
|
||||
--[[
|
||||
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)
|
||||
return syncTank(source, slotId, oxygen)
|
||||
end)
|
||||
@@ -154,15 +243,20 @@ AddEventHandler('playerDropped', function()
|
||||
lastSync[source] = nil
|
||||
end)
|
||||
|
||||
exports.qbx_core:CreateUseableItem(shared.gearItem, function(source)
|
||||
TriggerClientEvent('d4rk_divegear:client:useGear', source)
|
||||
end)
|
||||
-- Eine Patrone benutzen = sie in die getragene Flasche einsetzen (28.09.2026).
|
||||
-- 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
|
||||
|
||||
exports.qbx_core:CreateUseableItem(shared.fillItem, function(source)
|
||||
local tanks = getTankList(source)
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@@ -175,12 +269,12 @@ exports.qbx_core:CreateUseableItem(shared.fillItem, function(source)
|
||||
if not slot then return end
|
||||
|
||||
-- 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)
|
||||
lastSync[source] = os.time()
|
||||
|
||||
-- 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)
|
||||
end)
|
||||
Reference in new issue
Block a user