feat: Flaschengroessen mit echter Laufzeit und persistenter Restluft
Lint / Lint Resource (push) Has been cancelled
Lint / Lint Resource (push) Has been cancelled
Der Sauerstoff war eine lokale Client-Variable: nicht persistent, an kein Item gebunden, weg bei Relog. Dazu ergab startingOxygenLevel=100 bei decayRate=1/s genau 100 Sekunden Tauchzeit statt Minuten. Sauerstoff-Logik: - Restsekunden statt Punkte, Verbrauch ueber GetGameTimer-Delta statt gezaehlter Wait(1000) - ein Tick laeuft unter Last laenger als eine Sekunde, was sich ueber einen langen Tauchgang aufsummiert - Float-Vergleiche (oxygenLevel % 10 == 0, == 0) raus, Warnschwellen als config.warnAtSeconds, jede Schwelle einmal pro Tauchgang - Verbrauch steigt mit der Tiefe (1 + tiefe/30, gedeckelt), abschaltbar - Anzeige als mm:ss Flaschen: - Vier Items mit capacity in Sekunden (300/600/900/1200) in config/shared.lua - Restluft in metadata.oxygen, Fuellstand zusaetzlich in metadata.durability, damit der Balken direkt im Inventar-Slot sichtbar ist - Kein decay auf den Items: ox_inventory loescht sonst die leere Flasche - Auswahlmenue beim Anlegen und Auffuellen (entfaellt bei nur einer Flasche) - diving_fill fuellt bis capacity statt auf einen Fixwert, verweigert volle Flaschen und verbraucht sich nur bei Erfolg - items_for_ox_inventory.lua zum Reinkopieren Server: - getTanks/syncTank als Callbacks, Restluft kann nur sinken und nicht schneller als maxDrainPerSecond pro echter Sekunde - SetMetadata ersetzt die Metadata-Tabelle komplett, daher wird der bestehende Inhalt kopiert statt nur oxygen gesetzt Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+180
-28
@@ -1,4 +1,5 @@
|
|||||||
local config = require 'config.client'
|
local config = require 'config.client'
|
||||||
|
local shared = require 'config.shared'
|
||||||
|
|
||||||
local currentGear = {
|
local currentGear = {
|
||||||
mask = 0,
|
mask = 0,
|
||||||
@@ -6,13 +7,42 @@ local currentGear = {
|
|||||||
enabled = false
|
enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
local oxygenLevel = 0
|
--- Aktuell getragene Flasche. { slot, name, label, oxygen, capacity }
|
||||||
|
--- oxygen sind Restsekunden, kein Prozentwert.
|
||||||
|
local activeTank = nil
|
||||||
|
|
||||||
|
--- Bereits ausgelöste Warnschwellen des laufenden Tauchgangs.
|
||||||
|
local warned = {}
|
||||||
|
|
||||||
local MASK_MODEL = `p_d_scuba_mask_s`
|
local MASK_MODEL = `p_d_scuba_mask_s`
|
||||||
local TANK_MODEL = `p_s_scuba_tank_s`
|
local TANK_MODEL = `p_s_scuba_tank_s`
|
||||||
local MASK_BONE = 12844
|
local MASK_BONE = 12844
|
||||||
local TANK_BONE = 24818
|
local TANK_BONE = 24818
|
||||||
|
|
||||||
|
--- @param seconds number
|
||||||
|
--- @return string mm:ss
|
||||||
|
local function formatTime(seconds)
|
||||||
|
seconds = math.max(0, math.floor(seconds))
|
||||||
|
return ('%02d:%02d'):format(seconds // 60, seconds % 60)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Aktuelle Tauchtiefe in Metern. 0 wenn kein Wasser über dem Ped ist.
|
||||||
|
local function getDepth()
|
||||||
|
local coords = GetEntityCoords(cache.ped)
|
||||||
|
local hasWater, waterZ = GetWaterHeight(coords.x, coords.y, coords.z)
|
||||||
|
|
||||||
|
if not hasWater then return 0.0 end
|
||||||
|
|
||||||
|
return math.max(0.0, waterZ - coords.z)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Verbrauchsfaktor. Tiefer tauchen kostet mehr Luft.
|
||||||
|
local function getDecayFactor()
|
||||||
|
if not config.depthDecayEnabled then return 1.0 end
|
||||||
|
|
||||||
|
return math.min(config.maxDecayFactor, 1.0 + getDepth() / config.depthDecayReference)
|
||||||
|
end
|
||||||
|
|
||||||
--- @param ped number? default cache.ped - beim Model-Wechsel muss der neue Ped explizit rein
|
--- @param ped number? default cache.ped - beim Model-Wechsel muss der neue Ped explizit rein
|
||||||
local function enableScuba(ped)
|
local function enableScuba(ped)
|
||||||
ped = ped or cache.ped
|
ped = ped or cache.ped
|
||||||
@@ -80,22 +110,87 @@ local function isGearIntact()
|
|||||||
return isPropIntact(currentGear.mask) and isPropIntact(currentGear.tank)
|
return isPropIntact(currentGear.mask) and isPropIntact(currentGear.tank)
|
||||||
end
|
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)
|
||||||
|
local function syncTank(immediate)
|
||||||
|
if not activeTank then return end
|
||||||
|
|
||||||
|
if immediate then
|
||||||
|
TriggerServerEvent('d4rk_divegear:server:syncTankNow', activeTank.slot, activeTank.oxygen)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local accepted = lib.callback.await('d4rk_divegear:server:syncTank', false, activeTank.slot, activeTank.oxygen)
|
||||||
|
|
||||||
|
if accepted and activeTank then
|
||||||
|
activeTank.oxygen = accepted
|
||||||
|
end
|
||||||
|
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?
|
||||||
local function removeGear(outOfAir)
|
--- @param immediateSync boolean?
|
||||||
|
local function removeGear(outOfAir, immediateSync)
|
||||||
|
syncTank(immediateSync)
|
||||||
|
|
||||||
currentGear.enabled = false
|
currentGear.enabled = false
|
||||||
|
activeTank = nil
|
||||||
|
warned = {}
|
||||||
|
|
||||||
deleteGear()
|
deleteGear()
|
||||||
disableScuba(outOfAir)
|
disableScuba(outOfAir)
|
||||||
-- Stop breathing suit audio
|
-- Stop breathing suit audio
|
||||||
end
|
end
|
||||||
|
|
||||||
lib.callback.register('qbx_divegear:client:fillTank', function()
|
--- Lässt den Spieler eine Flasche wählen. Bei nur einer Flasche entfällt das Menü.
|
||||||
if IsPedSwimmingUnderWater(cache.ped) then
|
--- @param tanks table[]
|
||||||
exports.qbx_core:Notify(locale('error.underwater', {oxygenlevel = oxygenLevel}), 'error')
|
--- @return table? tank
|
||||||
return false
|
local function chooseTank(tanks)
|
||||||
|
if #tanks == 1 then return tanks[1] end
|
||||||
|
|
||||||
|
local selection = promise.new()
|
||||||
|
local options = {}
|
||||||
|
|
||||||
|
for i = 1, #tanks do
|
||||||
|
local tank = tanks[i]
|
||||||
|
options[i] = {
|
||||||
|
title = tank.label,
|
||||||
|
description = locale('info.tank_remaining', formatTime(tank.oxygen),
|
||||||
|
math.floor(tank.oxygen / tank.capacity * 100)),
|
||||||
|
icon = 'fa-solid fa-bottle-water',
|
||||||
|
onSelect = function() selection:resolve(tank) end
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
if lib.progressBar({
|
lib.registerContext({
|
||||||
|
id = 'd4rk_divegear_tanks',
|
||||||
|
title = locale('menu.choose_tank'),
|
||||||
|
options = options,
|
||||||
|
onExit = function() selection:resolve(nil) end
|
||||||
|
})
|
||||||
|
|
||||||
|
lib.showContext('d4rk_divegear_tanks')
|
||||||
|
|
||||||
|
return Citizen.Await(selection)
|
||||||
|
end
|
||||||
|
|
||||||
|
lib.callback.register('d4rk_divegear:client:fillTank', function(tanks)
|
||||||
|
if IsPedSwimmingUnderWater(cache.ped) then
|
||||||
|
exports.qbx_core:Notify(locale('error.underwater'), 'error')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local tank = chooseTank(tanks)
|
||||||
|
|
||||||
|
if not tank then return end
|
||||||
|
|
||||||
|
if tank.oxygen >= tank.capacity then
|
||||||
|
exports.qbx_core:Notify(locale('error.tank_already_full'), 'error')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not lib.progressBar({
|
||||||
duration = config.refillTankTimeMs,
|
duration = config.refillTankTimeMs,
|
||||||
label = locale('info.filling_air'),
|
label = locale('info.filling_air'),
|
||||||
useWhileDead = false,
|
useWhileDead = false,
|
||||||
@@ -106,13 +201,22 @@ lib.callback.register('qbx_divegear:client:fillTank', function()
|
|||||||
blendIn = 8.0
|
blendIn = 8.0
|
||||||
}
|
}
|
||||||
}) then
|
}) then
|
||||||
oxygenLevel = config.startingOxygenLevel
|
return
|
||||||
exports.qbx_core:Notify(locale('success.tube_filled'), 'success')
|
|
||||||
if currentGear.enabled then
|
|
||||||
enableScuba()
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Erfolgsmeldung und das Nachziehen der getragenen Flasche macht der Server,
|
||||||
|
-- sobald das Füll-Item tatsächlich entfernt wurde.
|
||||||
|
return tank.slot
|
||||||
|
end)
|
||||||
|
|
||||||
|
--- Getragene Flasche nach dem Auffüllen mitziehen, sonst überschreibt der
|
||||||
|
--- nächste Sync die frische Füllung wieder mit dem alten Client-Stand.
|
||||||
|
RegisterNetEvent('d4rk_divegear:client:tankRefilled', function(slot, capacity)
|
||||||
|
if not activeTank or activeTank.slot ~= slot then return end
|
||||||
|
|
||||||
|
activeTank.oxygen = capacity
|
||||||
|
warned = {}
|
||||||
|
enableScuba()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local function takeOffSuit()
|
local function takeOffSuit()
|
||||||
@@ -135,9 +239,9 @@ end
|
|||||||
local function startOxygenLevelDrawTextThread()
|
local function startOxygenLevelDrawTextThread()
|
||||||
CreateThread(function()
|
CreateThread(function()
|
||||||
while currentGear.enabled do
|
while currentGear.enabled do
|
||||||
if IsPedSwimmingUnderWater(cache.ped) then
|
if IsPedSwimmingUnderWater(cache.ped) and activeTank then
|
||||||
qbx.drawText2d({
|
qbx.drawText2d({
|
||||||
text = oxygenLevel..'⏱',
|
text = formatTime(activeTank.oxygen)..'⏱',
|
||||||
coords = vec2(1.0, 1.42),
|
coords = vec2(1.0, 1.42),
|
||||||
scale = 0.45
|
scale = 0.45
|
||||||
})
|
})
|
||||||
@@ -147,20 +251,53 @@ local function startOxygenLevelDrawTextThread()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Zieht Luft über die tatsächlich vergangene Zeit ab statt über gezählte Ticks.
|
||||||
|
--- Ein Wait(1000) läuft bei Last spürbar länger als eine Sekunde - über einen
|
||||||
|
--- 20-Minuten-Tauchgang summiert sich das sichtbar auf.
|
||||||
local function startOxygenLevelDecrementerThread()
|
local function startOxygenLevelDecrementerThread()
|
||||||
CreateThread(function()
|
CreateThread(function()
|
||||||
|
local lastTick = GetGameTimer()
|
||||||
|
local sinceSync = 0.0
|
||||||
|
|
||||||
while currentGear.enabled do
|
while currentGear.enabled do
|
||||||
if IsPedSwimmingUnderWater(cache.ped) and oxygenLevel > 0 then
|
Wait(250)
|
||||||
oxygenLevel -= config.decayRate
|
|
||||||
if oxygenLevel % 10 == 0 and oxygenLevel ~= config.startingOxygenLevel then
|
local now = GetGameTimer()
|
||||||
-- Initiate breathing suit audio
|
local elapsed = (now - lastTick) / 1000
|
||||||
|
lastTick = now
|
||||||
|
|
||||||
|
if not activeTank then goto continue end
|
||||||
|
|
||||||
|
if IsPedSwimmingUnderWater(cache.ped) and activeTank.oxygen > 0 then
|
||||||
|
activeTank.oxygen = math.max(0.0, activeTank.oxygen - elapsed * config.decayRate * getDecayFactor())
|
||||||
|
|
||||||
|
for i = 1, #config.warnAtSeconds do
|
||||||
|
local threshold = config.warnAtSeconds[i]
|
||||||
|
|
||||||
|
if activeTank.oxygen <= threshold and not warned[threshold] then
|
||||||
|
warned[threshold] = true
|
||||||
|
exports.qbx_core:Notify(locale('info.oxygen_low', formatTime(activeTank.oxygen)), 'warning')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if oxygenLevel == 0 then
|
|
||||||
|
if activeTank.oxygen <= 0 then
|
||||||
disableScuba(true)
|
disableScuba(true)
|
||||||
|
exports.qbx_core:Notify(locale('error.out_of_air'), 'error')
|
||||||
|
syncTank()
|
||||||
|
sinceSync = 0.0
|
||||||
-- Stop breathing suit audio
|
-- Stop breathing suit audio
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
sinceSync = sinceSync + elapsed
|
||||||
|
|
||||||
|
if sinceSync >= shared.syncIntervalSeconds then
|
||||||
|
sinceSync = 0.0
|
||||||
|
syncTank()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Wait(1000)
|
|
||||||
|
::continue::
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -173,7 +310,7 @@ local function startGearWatchdogThread()
|
|||||||
CreateThread(function()
|
CreateThread(function()
|
||||||
while currentGear.enabled do
|
while currentGear.enabled do
|
||||||
if config.removeGearOnDeath and IsPedDeadOrDying(cache.ped, true) then
|
if config.removeGearOnDeath and IsPedDeadOrDying(cache.ped, true) then
|
||||||
removeGear()
|
removeGear(false, true)
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -182,7 +319,7 @@ local function startGearWatchdogThread()
|
|||||||
attachGear()
|
attachGear()
|
||||||
end
|
end
|
||||||
|
|
||||||
if oxygenLevel > 0 then
|
if activeTank and activeTank.oxygen > 0 then
|
||||||
enableScuba()
|
enableScuba()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -192,13 +329,24 @@ local function startGearWatchdogThread()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function putOnSuit()
|
local function putOnSuit()
|
||||||
if oxygenLevel <= 0 then
|
if IsPedSwimming(cache.ped) or cache.vehicle then
|
||||||
|
exports.qbx_core:Notify(locale('error.not_standing_up'), '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')
|
exports.qbx_core:Notify(locale('error.need_otube'), 'error')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if IsPedSwimming(cache.ped) or cache.vehicle then
|
local tank = chooseTank(tanks)
|
||||||
exports.qbx_core:Notify(locale('error.not_standing_up'), 'error')
|
|
||||||
|
if not tank then return end
|
||||||
|
|
||||||
|
if tank.oxygen <= 0 then
|
||||||
|
exports.qbx_core:Notify(locale('error.tank_empty'), 'error')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -213,6 +361,9 @@ local function putOnSuit()
|
|||||||
blendIn = 8.0
|
blendIn = 8.0
|
||||||
}
|
}
|
||||||
}) then
|
}) then
|
||||||
|
activeTank = tank
|
||||||
|
warned = {}
|
||||||
|
|
||||||
deleteGear()
|
deleteGear()
|
||||||
attachGear()
|
attachGear()
|
||||||
enableScuba()
|
enableScuba()
|
||||||
@@ -224,7 +375,7 @@ local function putOnSuit()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
RegisterNetEvent('qbx_divegear:client:useGear', function()
|
RegisterNetEvent('d4rk_divegear:client:useGear', function()
|
||||||
if currentGear.enabled then
|
if currentGear.enabled then
|
||||||
takeOffSuit()
|
takeOffSuit()
|
||||||
else
|
else
|
||||||
@@ -245,7 +396,7 @@ lib.onCache('ped', function(ped)
|
|||||||
deleteGear()
|
deleteGear()
|
||||||
attachGear(ped)
|
attachGear(ped)
|
||||||
|
|
||||||
if oxygenLevel > 0 then
|
if activeTank and activeTank.oxygen > 0 then
|
||||||
enableScuba(ped)
|
enableScuba(ped)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -253,6 +404,7 @@ end)
|
|||||||
AddEventHandler('onResourceStop', function(resource)
|
AddEventHandler('onResourceStop', function(resource)
|
||||||
if resource ~= GetCurrentResourceName() then return end
|
if resource ~= GetCurrentResourceName() then return end
|
||||||
|
|
||||||
|
syncTank(true)
|
||||||
deleteGear()
|
deleteGear()
|
||||||
SetEnableScuba(cache.ped, false)
|
SetEnableScuba(cache.ped, false)
|
||||||
SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater)
|
SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater)
|
||||||
|
|||||||
+17
-6
@@ -1,19 +1,30 @@
|
|||||||
return {
|
return {
|
||||||
startingOxygenLevel = 100,
|
|
||||||
putOnSuitTimeMs = 5000,
|
putOnSuitTimeMs = 5000,
|
||||||
takeOffSuitTimeMs = 5000,
|
takeOffSuitTimeMs = 5000,
|
||||||
refillTankTimeMs = 5000,
|
refillTankTimeMs = 5000,
|
||||||
decayRate = 1, -- The rate at which the oxygen level decays. Defaults to 1.
|
|
||||||
|
|
||||||
-- Intervall des Watchdogs, der prüft ob die Ausrüstung noch am Ped hängt (ms).
|
--- Sekunden Luft, die pro echter Sekunde verbraucht werden (vor dem Tiefenfaktor).
|
||||||
|
decayRate = 1.0,
|
||||||
|
|
||||||
|
--- Intervall des Watchdogs, der prüft ob die Ausrüstung noch am Ped hängt (ms).
|
||||||
gearWatchdogIntervalMs = 1000,
|
gearWatchdogIntervalMs = 1000,
|
||||||
|
|
||||||
-- Sekunden, die der Spieler ohne Ausrüstung unter Wasser überlebt (Vanilla-Verhalten).
|
--- Sekunden, die der Spieler ohne Ausrüstung unter Wasser überlebt (Vanilla-Verhalten).
|
||||||
maxTimeUnderwater = 50.0,
|
maxTimeUnderwater = 50.0,
|
||||||
|
|
||||||
-- Sekunden, sobald die Flasche leer ist. Bewusst kurz: leer tauchen soll wehtun.
|
--- Sekunden, sobald die Flasche leer ist. Bewusst kurz: leer tauchen soll wehtun.
|
||||||
maxTimeUnderwaterOutOfAir = 1.0,
|
maxTimeUnderwaterOutOfAir = 1.0,
|
||||||
|
|
||||||
-- Ausrüstung beim Tod abnehmen. Die Flasche bleibt mit ihrer Restluft im Inventar.
|
--- Ausrüstung beim Tod abnehmen. Die Flasche bleibt mit ihrer Restluft im Inventar.
|
||||||
removeGearOnDeath = true,
|
removeGearOnDeath = true,
|
||||||
|
|
||||||
|
--- Restsekunden, bei denen gewarnt wird. Jede Schwelle löst einmal pro Tauchgang aus.
|
||||||
|
warnAtSeconds = { 60, 30, 10 },
|
||||||
|
|
||||||
|
--- Verbrauch steigt mit der Tiefe: faktor = 1 + tiefe / depthDecayReference.
|
||||||
|
--- Macht die großen Flaschen erst sinnvoll. Auf false = konstanter Verbrauch,
|
||||||
|
--- dann stimmen die 5/10/15/20 Minuten exakt.
|
||||||
|
depthDecayEnabled = true,
|
||||||
|
depthDecayReference = 30.0,
|
||||||
|
maxDecayFactor = 4.0,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
-- Von Client und Server benutzt. Muss in fxmanifest unter files{} stehen,
|
||||||
|
-- damit lib.require die Datei auf beiden Seiten laden kann.
|
||||||
|
|
||||||
|
local config = {
|
||||||
|
--- Tauchflaschen. capacity = Laufzeit in Sekunden bei normalem Verbrauch.
|
||||||
|
--- Reihenfolge = Reihenfolge im Auswahlmenü.
|
||||||
|
tanks = {
|
||||||
|
{ name = 'diving_tank_small', capacity = 300 }, -- 5 Minuten
|
||||||
|
{ name = 'diving_tank_medium', capacity = 600 }, -- 10 Minuten
|
||||||
|
{ name = 'diving_tank_large', capacity = 900 }, -- 15 Minuten
|
||||||
|
{ name = 'diving_tank_xl', capacity = 1200 }, -- 20 Minuten
|
||||||
|
},
|
||||||
|
|
||||||
|
--- Wie oft der Client die Restluft an den Server meldet (Sekunden).
|
||||||
|
--- Bei einem Crash gehen maximal so viele Sekunden Luft verloren.
|
||||||
|
syncIntervalSeconds = 15,
|
||||||
|
|
||||||
|
--- Obergrenze für die serverseitige Plausibilitätsprüfung: so viele Sekunden
|
||||||
|
--- Luft kann ein Spieler höchstens pro echter Sekunde verbrauchen. Muss über
|
||||||
|
--- maxDecayFactor aus config/client.lua liegen, sonst wird ehrlichen Spielern
|
||||||
|
--- in großer Tiefe fälschlich Luft zurückgegeben.
|
||||||
|
maxDrainPerSecond = 5.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
config.tankByName = {}
|
||||||
|
config.tankNames = {}
|
||||||
|
|
||||||
|
for i = 1, #config.tanks do
|
||||||
|
local tank = config.tanks[i]
|
||||||
|
config.tankByName[tank.name] = tank
|
||||||
|
config.tankNames[i] = tank.name
|
||||||
|
end
|
||||||
|
|
||||||
|
return config
|
||||||
@@ -19,6 +19,7 @@ client_script 'client/main.lua'
|
|||||||
|
|
||||||
files {
|
files {
|
||||||
'config/client.lua',
|
'config/client.lua',
|
||||||
|
'config/shared.lua',
|
||||||
'locales/*.json',
|
'locales/*.json',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
-- Zum Reinkopieren in ox_inventory/data/items.lua
|
||||||
|
--
|
||||||
|
-- WICHTIG: kein `decay = true` auf den Flaschen. Die durability trägt hier den
|
||||||
|
-- Füllstand, und ox_inventory löscht Items mit decay, sobald durability 0 erreicht
|
||||||
|
-- (modules/items/server.lua) - die leere Flasche würde also verschwinden statt
|
||||||
|
-- auffüllbar zu bleiben.
|
||||||
|
--
|
||||||
|
-- Die Laufzeiten stehen in config/shared.lua (capacity in Sekunden). Wer die dort
|
||||||
|
-- ändert, sollte die Labels hier mitziehen.
|
||||||
|
|
||||||
|
['diving_gear'] = {
|
||||||
|
label = 'Tauchausrüstung',
|
||||||
|
weight = 5000,
|
||||||
|
stack = false,
|
||||||
|
close = true,
|
||||||
|
consume = 0,
|
||||||
|
description = 'Maske und Tauchanzug. Benutzen zum An- und Ausziehen.',
|
||||||
|
client = { image = 'diving_gear.png' }
|
||||||
|
},
|
||||||
|
|
||||||
|
['diving_fill'] = {
|
||||||
|
label = 'Pressluft-Kartusche',
|
||||||
|
weight = 1000,
|
||||||
|
stack = true,
|
||||||
|
close = true,
|
||||||
|
consume = 0,
|
||||||
|
description = 'Füllt eine Tauchflasche wieder auf.',
|
||||||
|
client = { image = 'diving_fill.png' }
|
||||||
|
},
|
||||||
|
|
||||||
|
['diving_tank_small'] = {
|
||||||
|
label = 'Tauchflasche (5 Min)',
|
||||||
|
weight = 4000,
|
||||||
|
stack = false,
|
||||||
|
close = true,
|
||||||
|
consume = 0,
|
||||||
|
description = 'Kleine Tauchflasche. Reicht für etwa 5 Minuten.',
|
||||||
|
client = { image = 'diving_tank_small.png' }
|
||||||
|
},
|
||||||
|
|
||||||
|
['diving_tank_medium'] = {
|
||||||
|
label = 'Tauchflasche (10 Min)',
|
||||||
|
weight = 7000,
|
||||||
|
stack = false,
|
||||||
|
close = true,
|
||||||
|
consume = 0,
|
||||||
|
description = 'Mittlere Tauchflasche. Reicht für etwa 10 Minuten.',
|
||||||
|
client = { image = 'diving_tank_medium.png' }
|
||||||
|
},
|
||||||
|
|
||||||
|
['diving_tank_large'] = {
|
||||||
|
label = 'Tauchflasche (15 Min)',
|
||||||
|
weight = 10000,
|
||||||
|
stack = false,
|
||||||
|
close = true,
|
||||||
|
consume = 0,
|
||||||
|
description = 'Große Tauchflasche. Reicht für etwa 15 Minuten.',
|
||||||
|
client = { image = 'diving_tank_large.png' }
|
||||||
|
},
|
||||||
|
|
||||||
|
['diving_tank_xl'] = {
|
||||||
|
label = 'Tauchflasche (20 Min)',
|
||||||
|
weight = 13000,
|
||||||
|
stack = false,
|
||||||
|
close = true,
|
||||||
|
consume = 0,
|
||||||
|
description = 'Doppelflasche für lange Tauchgänge. Reicht für etwa 20 Minuten.',
|
||||||
|
client = { image = 'diving_tank_xl.png' }
|
||||||
|
},
|
||||||
+13
-4
@@ -1,16 +1,25 @@
|
|||||||
{
|
{
|
||||||
"error": {
|
"error": {
|
||||||
"not_standing_up": "Du musst auf festem Boden stehen, um das anzulegen...",
|
"not_standing_up": "Du musst auf festem Boden stehen, um das anzulegen...",
|
||||||
"need_otube": "Du musst deinen Sauerstoff auffüllen! Hol dir eine neue Sauerstoffflasche!",
|
"need_otube": "Du hast keine Tauchflasche dabei!",
|
||||||
"underwater": "Du kannst das nicht unter Wasser tun..."
|
"underwater": "Du kannst das nicht unter Wasser tun...",
|
||||||
|
"no_tank": "Du hast keine Tauchflasche zum Auffüllen dabei!",
|
||||||
|
"tank_empty": "Diese Tauchflasche ist leer!",
|
||||||
|
"tank_already_full": "Diese Tauchflasche ist bereits voll!",
|
||||||
|
"out_of_air": "Deine Luft ist alle! Sofort auftauchen!"
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"took_out": "Du hast deine Ausrüstung abgelegt!",
|
"took_out": "Du hast deine Ausrüstung abgelegt!",
|
||||||
"tube_filled": "Du hast deinen Sauerstofftank erfolgreich aufgefüllt!"
|
"tube_filled": "Du hast deine Tauchflasche erfolgreich aufgefüllt!"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"put_suit": "Tauchanzug wird angezogen...",
|
"put_suit": "Tauchanzug wird angezogen...",
|
||||||
"pullout_suit": "Tauchanzug wird ausgezogen...",
|
"pullout_suit": "Tauchanzug wird ausgezogen...",
|
||||||
"filling_air": "Sauerstoff wird aufgefüllt..."
|
"filling_air": "Sauerstoff wird aufgefüllt...",
|
||||||
|
"oxygen_low": "Nur noch %s Luft!",
|
||||||
|
"tank_remaining": "%s verbleibend (%d%%)"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"choose_tank": "Tauchflasche wählen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-4
@@ -1,8 +1,12 @@
|
|||||||
{
|
{
|
||||||
"error": {
|
"error": {
|
||||||
"not_standing_up": "You need to be on solid ground to put this on...",
|
"not_standing_up": "You need to be on solid ground to put this on...",
|
||||||
"need_otube": "You need to refill your oxygen! Get a replacement air supply!",
|
"need_otube": "You don't have an air tank with you!",
|
||||||
"underwater": "You cannot do this underwater..."
|
"underwater": "You cannot do this underwater...",
|
||||||
|
"no_tank": "You don't have an air tank to refill!",
|
||||||
|
"tank_empty": "This air tank is empty!",
|
||||||
|
"tank_already_full": "This air tank is already full!",
|
||||||
|
"out_of_air": "You're out of air! Surface now!"
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"took_out": "You've taken your gear off!",
|
"took_out": "You've taken your gear off!",
|
||||||
@@ -11,6 +15,11 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"put_suit": "Putting on your diving suit...",
|
"put_suit": "Putting on your diving suit...",
|
||||||
"pullout_suit": "Taking off your diving suit...",
|
"pullout_suit": "Taking off your diving suit...",
|
||||||
"filling_air": "Filling air..."
|
"filling_air": "Filling air...",
|
||||||
|
"oxygen_low": "Only %s of air left!",
|
||||||
|
"tank_remaining": "%s remaining (%d%%)"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"choose_tank": "Choose an air tank"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-5
@@ -1,10 +1,147 @@
|
|||||||
|
local shared = require 'config.shared'
|
||||||
|
|
||||||
|
--- Zeitpunkt des letzten akzeptierten Syncs pro Spieler. Basis für die
|
||||||
|
--- Plausibilitätsprüfung: mehr als maxDrainPerSecond pro echter Sekunde kann
|
||||||
|
--- niemand verbrauchen.
|
||||||
|
local lastSync = {}
|
||||||
|
|
||||||
|
--- @param source number
|
||||||
|
--- @param slotId number
|
||||||
|
--- @return table? slot, table? tankConfig
|
||||||
|
local function getTankSlot(source, slotId)
|
||||||
|
if type(slotId) ~= 'number' then return end
|
||||||
|
|
||||||
|
local slot = exports.ox_inventory:GetSlot(source, slotId)
|
||||||
|
|
||||||
|
if not slot then return end
|
||||||
|
|
||||||
|
local tankConfig = shared.tankByName[slot.name]
|
||||||
|
|
||||||
|
if not tankConfig then return end
|
||||||
|
|
||||||
|
return slot, tankConfig
|
||||||
|
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.
|
||||||
|
--- @return number oxygen akzeptierte Restsekunden
|
||||||
|
local function writeTank(source, slot, tankConfig, oxygen)
|
||||||
|
oxygen = math.floor(math.max(0, math.min(oxygen, tankConfig.capacity)))
|
||||||
|
|
||||||
|
local metadata = {}
|
||||||
|
|
||||||
|
for key, value in pairs(slot.metadata or {}) do
|
||||||
|
metadata[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
metadata.oxygen = oxygen
|
||||||
|
-- durability zeigt den Füllstand direkt im Inventar-Slot an. Kein decay auf den
|
||||||
|
-- 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)
|
||||||
|
|
||||||
|
return oxygen
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Alle Tauchflaschen im Inventar mit Füllstand.
|
||||||
|
--- @param source number
|
||||||
|
--- @return table[]
|
||||||
|
local function getTankList(source)
|
||||||
|
local found = exports.ox_inventory:Search(source, 'slots', shared.tankNames)
|
||||||
|
local tanks = {}
|
||||||
|
|
||||||
|
for i = 1, #shared.tankNames do
|
||||||
|
local name = shared.tankNames[i]
|
||||||
|
local slots = found and found[name]
|
||||||
|
local tankConfig = shared.tankByName[name]
|
||||||
|
|
||||||
|
if slots then
|
||||||
|
for j = 1, #slots do
|
||||||
|
local slot = slots[j]
|
||||||
|
local oxygen = tonumber(slot.metadata and slot.metadata.oxygen) or 0
|
||||||
|
|
||||||
|
tanks[#tanks + 1] = {
|
||||||
|
slot = slot.slot,
|
||||||
|
name = name,
|
||||||
|
label = (exports.ox_inventory:Items(name) or {}).label or name,
|
||||||
|
oxygen = math.max(0, math.min(oxygen, tankConfig.capacity)),
|
||||||
|
capacity = tankConfig.capacity
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return tanks
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @return number? accepted
|
||||||
|
local function syncTank(source, slotId, oxygen)
|
||||||
|
oxygen = tonumber(oxygen)
|
||||||
|
|
||||||
|
if not oxygen then return end
|
||||||
|
|
||||||
|
local slot, tankConfig = getTankSlot(source, slotId)
|
||||||
|
|
||||||
|
if not slot then return end
|
||||||
|
|
||||||
|
local current = tonumber(slot.metadata and slot.metadata.oxygen) or tankConfig.capacity
|
||||||
|
local now = os.time()
|
||||||
|
local elapsed = lastSync[source] and (now - lastSync[source]) or shared.syncIntervalSeconds
|
||||||
|
|
||||||
|
lastSync[source] = now
|
||||||
|
|
||||||
|
-- Luft kann nur weniger werden, und nicht schneller als physikalisch möglich.
|
||||||
|
local floorValue = math.max(0, current - math.max(1, elapsed) * shared.maxDrainPerSecond)
|
||||||
|
local accepted = math.max(floorValue, math.min(oxygen, current))
|
||||||
|
|
||||||
|
return writeTank(source, slot, tankConfig, accepted)
|
||||||
|
end
|
||||||
|
|
||||||
|
lib.callback.register('d4rk_divegear:server:getTanks', getTankList)
|
||||||
|
|
||||||
|
lib.callback.register('d4rk_divegear:server:syncTank', function(source, slotId, oxygen)
|
||||||
|
return syncTank(source, slotId, oxygen)
|
||||||
|
end)
|
||||||
|
|
||||||
|
--- Fire-and-Forget-Variante für Tod und Resource-Stop, wo auf keine Antwort
|
||||||
|
--- mehr gewartet werden kann.
|
||||||
|
RegisterNetEvent('d4rk_divegear:server:syncTankNow', function(slotId, oxygen)
|
||||||
|
syncTank(source, slotId, oxygen)
|
||||||
|
end)
|
||||||
|
|
||||||
|
AddEventHandler('playerDropped', function()
|
||||||
|
lastSync[source] = nil
|
||||||
|
end)
|
||||||
|
|
||||||
exports.qbx_core:CreateUseableItem('diving_gear', function(source)
|
exports.qbx_core:CreateUseableItem('diving_gear', function(source)
|
||||||
TriggerClientEvent('qbx_divegear:client:useGear', source)
|
TriggerClientEvent('d4rk_divegear:client:useGear', source)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
exports.qbx_core:CreateUseableItem('diving_fill', function(source)
|
exports.qbx_core:CreateUseableItem('diving_fill', function(source)
|
||||||
local success = lib.callback.await('qbx_divegear:client:fillTank', source)
|
local tanks = getTankList(source)
|
||||||
if success then
|
|
||||||
exports.ox_inventory:RemoveItem(source, 'diving_fill', 1)
|
if #tanks == 0 then
|
||||||
|
exports.qbx_core:Notify(source, locale('error.no_tank'), 'error')
|
||||||
|
return
|
||||||
end
|
end
|
||||||
end)
|
|
||||||
|
local slotId = lib.callback.await('d4rk_divegear:client:fillTank', source, tanks)
|
||||||
|
|
||||||
|
if not slotId then return end
|
||||||
|
|
||||||
|
local slot, tankConfig = getTankSlot(source, slotId)
|
||||||
|
|
||||||
|
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, 'diving_fill', 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')
|
||||||
|
TriggerClientEvent('d4rk_divegear:client:tankRefilled', source, slot.slot, tankConfig.capacity)
|
||||||
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user