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 shared = require 'config.shared'
|
||||
|
||||
local currentGear = {
|
||||
mask = 0,
|
||||
@@ -6,13 +7,42 @@ local currentGear = {
|
||||
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 TANK_MODEL = `p_s_scuba_tank_s`
|
||||
local MASK_BONE = 12844
|
||||
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
|
||||
local function enableScuba(ped)
|
||||
ped = ped or cache.ped
|
||||
@@ -80,22 +110,87 @@ 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)
|
||||
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.
|
||||
--- @param outOfAir boolean?
|
||||
local function removeGear(outOfAir)
|
||||
--- @param immediateSync boolean?
|
||||
local function removeGear(outOfAir, immediateSync)
|
||||
syncTank(immediateSync)
|
||||
|
||||
currentGear.enabled = false
|
||||
activeTank = nil
|
||||
warned = {}
|
||||
|
||||
deleteGear()
|
||||
disableScuba(outOfAir)
|
||||
-- Stop breathing suit audio
|
||||
end
|
||||
|
||||
lib.callback.register('qbx_divegear:client:fillTank', function()
|
||||
if IsPedSwimmingUnderWater(cache.ped) then
|
||||
exports.qbx_core:Notify(locale('error.underwater', {oxygenlevel = oxygenLevel}), 'error')
|
||||
return false
|
||||
--- Lässt den Spieler eine Flasche wählen. Bei nur einer Flasche entfällt das Menü.
|
||||
--- @param tanks table[]
|
||||
--- @return table? tank
|
||||
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
|
||||
|
||||
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,
|
||||
label = locale('info.filling_air'),
|
||||
useWhileDead = false,
|
||||
@@ -106,13 +201,22 @@ lib.callback.register('qbx_divegear:client:fillTank', function()
|
||||
blendIn = 8.0
|
||||
}
|
||||
}) then
|
||||
oxygenLevel = config.startingOxygenLevel
|
||||
exports.qbx_core:Notify(locale('success.tube_filled'), 'success')
|
||||
if currentGear.enabled then
|
||||
enableScuba()
|
||||
end
|
||||
return true
|
||||
return
|
||||
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)
|
||||
|
||||
local function takeOffSuit()
|
||||
@@ -135,9 +239,9 @@ end
|
||||
local function startOxygenLevelDrawTextThread()
|
||||
CreateThread(function()
|
||||
while currentGear.enabled do
|
||||
if IsPedSwimmingUnderWater(cache.ped) then
|
||||
if IsPedSwimmingUnderWater(cache.ped) and activeTank then
|
||||
qbx.drawText2d({
|
||||
text = oxygenLevel..'⏱',
|
||||
text = formatTime(activeTank.oxygen)..'⏱',
|
||||
coords = vec2(1.0, 1.42),
|
||||
scale = 0.45
|
||||
})
|
||||
@@ -147,20 +251,53 @@ local function startOxygenLevelDrawTextThread()
|
||||
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()
|
||||
CreateThread(function()
|
||||
local lastTick = GetGameTimer()
|
||||
local sinceSync = 0.0
|
||||
|
||||
while currentGear.enabled do
|
||||
if IsPedSwimmingUnderWater(cache.ped) and oxygenLevel > 0 then
|
||||
oxygenLevel -= config.decayRate
|
||||
if oxygenLevel % 10 == 0 and oxygenLevel ~= config.startingOxygenLevel then
|
||||
-- Initiate breathing suit audio
|
||||
Wait(250)
|
||||
|
||||
local now = GetGameTimer()
|
||||
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
|
||||
if oxygenLevel == 0 then
|
||||
|
||||
if activeTank.oxygen <= 0 then
|
||||
disableScuba(true)
|
||||
exports.qbx_core:Notify(locale('error.out_of_air'), 'error')
|
||||
syncTank()
|
||||
sinceSync = 0.0
|
||||
-- Stop breathing suit audio
|
||||
goto continue
|
||||
end
|
||||
|
||||
sinceSync = sinceSync + elapsed
|
||||
|
||||
if sinceSync >= shared.syncIntervalSeconds then
|
||||
sinceSync = 0.0
|
||||
syncTank()
|
||||
end
|
||||
end
|
||||
Wait(1000)
|
||||
|
||||
::continue::
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -173,7 +310,7 @@ local function startGearWatchdogThread()
|
||||
CreateThread(function()
|
||||
while currentGear.enabled do
|
||||
if config.removeGearOnDeath and IsPedDeadOrDying(cache.ped, true) then
|
||||
removeGear()
|
||||
removeGear(false, true)
|
||||
break
|
||||
end
|
||||
|
||||
@@ -182,7 +319,7 @@ local function startGearWatchdogThread()
|
||||
attachGear()
|
||||
end
|
||||
|
||||
if oxygenLevel > 0 then
|
||||
if activeTank and activeTank.oxygen > 0 then
|
||||
enableScuba()
|
||||
end
|
||||
|
||||
@@ -192,13 +329,24 @@ local function startGearWatchdogThread()
|
||||
end
|
||||
|
||||
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')
|
||||
return
|
||||
end
|
||||
|
||||
if IsPedSwimming(cache.ped) or cache.vehicle then
|
||||
exports.qbx_core:Notify(locale('error.not_standing_up'), 'error')
|
||||
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
|
||||
|
||||
@@ -213,6 +361,9 @@ local function putOnSuit()
|
||||
blendIn = 8.0
|
||||
}
|
||||
}) then
|
||||
activeTank = tank
|
||||
warned = {}
|
||||
|
||||
deleteGear()
|
||||
attachGear()
|
||||
enableScuba()
|
||||
@@ -224,7 +375,7 @@ local function putOnSuit()
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent('qbx_divegear:client:useGear', function()
|
||||
RegisterNetEvent('d4rk_divegear:client:useGear', function()
|
||||
if currentGear.enabled then
|
||||
takeOffSuit()
|
||||
else
|
||||
@@ -245,7 +396,7 @@ lib.onCache('ped', function(ped)
|
||||
deleteGear()
|
||||
attachGear(ped)
|
||||
|
||||
if oxygenLevel > 0 then
|
||||
if activeTank and activeTank.oxygen > 0 then
|
||||
enableScuba(ped)
|
||||
end
|
||||
end)
|
||||
@@ -253,6 +404,7 @@ end)
|
||||
AddEventHandler('onResourceStop', function(resource)
|
||||
if resource ~= GetCurrentResourceName() then return end
|
||||
|
||||
syncTank(true)
|
||||
deleteGear()
|
||||
SetEnableScuba(cache.ped, false)
|
||||
SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater)
|
||||
|
||||
Reference in New Issue
Block a user