Lint / Lint Resource (push) Has been cancelled
GTA toetet selbst ab einer bestimmten Tiefe ueber den Wasserdruck - beim U-Boot bei rund 150 m mit Implosion ab ~168 m, zu Fuss verliert man ebenfalls Leben. Dagegen hilft kein Atemgas, das ist die Engine. Deshalb dafuer nur eine Warnung statt eigener Effekte, konservativ auf 130 m mit 20 m Vorwarnung, beides in config.narcosis. Trimix hoert mit 100 m bewusst darueber auf; dazwischen liegt die Zone, in der keine Ausruestung mehr hilft. Der genaue Wert fuer Taucher zu Fuss ist nicht belegt und steht als Testpunkt in TESTING.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
747 lines
24 KiB
Lua
747 lines
24 KiB
Lua
local config = require 'config.client'
|
|
local shared = require 'config.shared'
|
|
|
|
local currentGear = {
|
|
mask = 0,
|
|
tank = 0,
|
|
lamp = 0,
|
|
enabled = false
|
|
}
|
|
|
|
--- Lampe an? Der eigentliche Lichtkegel wird in client/light.lua gezeichnet,
|
|
--- gesteuert über den replizierten Statebag.
|
|
local lampOn = false
|
|
|
|
--- 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 = {}
|
|
|
|
--- Geglättete Tauchtiefe in Metern. Wird vom HUD-Thread gepflegt und sowohl für
|
|
--- die Anzeige als auch für den tiefenabhängigen Verbrauch benutzt.
|
|
local currentDepth = 0.0
|
|
|
|
--- Grenzen für die Farbstufen im HUD, aus config.warnAtSeconds abgeleitet.
|
|
local warnThreshold, dangerThreshold = 0, 0
|
|
|
|
for i = 1, #config.warnAtSeconds do
|
|
local threshold = config.warnAtSeconds[i]
|
|
|
|
if threshold > warnThreshold then warnThreshold = threshold end
|
|
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
|
|
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
|
|
|
|
--- Aktuell aktive Tiefenrausch-Stufe (Index in config.narcosis.stages), 0 = keine.
|
|
local narcosisStage = 0
|
|
local narcosisWarned = false
|
|
local narcosisDamageBuffer = 0.0
|
|
local engineDepthWarned = false
|
|
|
|
--- Wie weit die aktuelle Tiefe unter der Grenze der getragenen Flasche liegt.
|
|
--- @return number Meter unterhalb der Grenze, negativ solange alles gut ist
|
|
local function getDepthOverLimit()
|
|
if not activeTank or not activeTank.maxDepth then return -math.huge end
|
|
|
|
return currentDepth - activeTank.maxDepth
|
|
end
|
|
|
|
--- Setzt Bildschirmeffekt und Kamerawackeln auf die angegebene Stufe.
|
|
--- @param stage number 0 = alles aus
|
|
local function applyNarcosisStage(stage)
|
|
if stage == narcosisStage then return end
|
|
|
|
narcosisStage = stage
|
|
|
|
ClearTimecycleModifier()
|
|
StopGameplayCamShaking(true)
|
|
|
|
local data = stage > 0 and config.narcosis.stages[stage]
|
|
|
|
if not data then return end
|
|
|
|
if data.timecycle then
|
|
SetTimecycleModifier(data.timecycle)
|
|
SetTimecycleModifierStrength(data.strength or 1.0)
|
|
end
|
|
|
|
if data.shake then
|
|
ShakeGameplayCam(data.shake, data.shakeIntensity or 0.5)
|
|
end
|
|
end
|
|
|
|
--- Verbrauchsfaktor. Tiefer tauchen kostet mehr Luft.
|
|
--- Nutzt die geglättete Tiefe aus dem HUD-Thread, damit Wellengang den Verbrauch
|
|
--- nicht im Sekundentakt springen lässt.
|
|
local function getDecayFactor()
|
|
local factor = 1.0
|
|
|
|
if config.depthDecayEnabled then
|
|
factor = math.min(config.maxDecayFactor, 1.0 + currentDepth / config.depthDecayReference)
|
|
end
|
|
|
|
-- Tiefenrausch: unter Stress atmet man mehr.
|
|
local stage = narcosisStage > 0 and config.narcosis.stages[narcosisStage]
|
|
|
|
if stage then
|
|
factor = factor * (stage.drainFactor or 1.0)
|
|
end
|
|
|
|
return factor
|
|
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
|
|
SetEnableScuba(ped, true)
|
|
SetPedMaxTimeUnderwater(ped, 2000.00)
|
|
end
|
|
|
|
--- @param outOfAir boolean? true wenn die Flasche leer ist statt die Ausrüstung abgelegt wurde
|
|
--- @param ped number? default cache.ped
|
|
local function disableScuba(outOfAir, ped)
|
|
ped = ped or cache.ped
|
|
SetEnableScuba(ped, false)
|
|
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
|
|
|
|
if currentGear.lamp ~= 0 then
|
|
DetachEntity(currentGear.lamp, false, true)
|
|
DeleteEntity(currentGear.lamp)
|
|
currentGear.lamp = 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
|
|
|
|
--- Lampen-Model einmalig prüfen. lib.requestModel wirft bei ungültigen Models einen
|
|
--- Error - ohne diese Prüfung würde ein falsch geschriebener Prop-Name die komplette
|
|
--- Ausrüstung unbenutzbar machen, statt nur die Optik der Lampe zu kosten.
|
|
local lampModel = config.lampProp ~= '' and joaat(config.lampProp) or nil
|
|
local hasLampProp = lampModel ~= nil and (IsModelValid(lampModel) or IsModelInCdimage(lampModel))
|
|
|
|
if lampModel and not hasLampProp then
|
|
lib.print.warn(('lampProp "%s" ist kein gültiges Model - die Lampe leuchtet, das Prop fehlt.'):format(config.lampProp))
|
|
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))
|
|
|
|
if hasLampProp then
|
|
currentGear.lamp = attachProp(ped, lampModel, config.lampBone, config.lampPropOffset, config.lampPropRotation)
|
|
end
|
|
end
|
|
|
|
--- @param state boolean
|
|
--- @param notifyIfUnavailable boolean? Rückmeldung, wenn das Outfit keine Lampe hat
|
|
local function setLamp(state, notifyIfUnavailable)
|
|
if not currentGear.enabled then state = false end
|
|
|
|
lampOn = state
|
|
-- Repliziert: client/light.lua setzt daraus den Lampen-Flag bei allen Tauchern.
|
|
LocalPlayer.state:set('divelight', state, true)
|
|
|
|
-- Lokal sofort setzen statt auf den Abgleich-Thread zu warten.
|
|
SetEnableScubaGearLight(cache.ped, state)
|
|
|
|
-- Die Spiel-Lampe hängt an der Scuba-Kleidung (Component 8). Fehlt die, tut
|
|
-- die Native nichts - ohne Hinweis sieht das nach einem kaputten Keybind aus.
|
|
if state and notifyIfUnavailable and not IsScubaGearLightEnabled(cache.ped) then
|
|
exports.qbx_core:Notify(locale('error.no_lamp_on_outfit'), 'error')
|
|
end
|
|
end
|
|
|
|
local function isPropIntact(prop)
|
|
return prop ~= 0 and DoesEntityExist(prop) and GetEntityAttachedTo(prop) == cache.ped
|
|
end
|
|
|
|
local function isGearIntact()
|
|
if hasLampProp and not isPropIntact(currentGear.lamp) then return false end
|
|
|
|
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?
|
|
--- @param immediateSync boolean?
|
|
local function removeGear(outOfAir, immediateSync)
|
|
syncTank(immediateSync)
|
|
|
|
currentGear.enabled = false
|
|
setLamp(false)
|
|
applyNarcosisStage(0)
|
|
activeTank = nil
|
|
warned = {}
|
|
narcosisWarned = false
|
|
narcosisDamageBuffer = 0.0
|
|
engineDepthWarned = false
|
|
|
|
deleteGear()
|
|
disableScuba(outOfAir)
|
|
-- Stop breathing suit audio
|
|
end
|
|
|
|
--- 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),
|
|
tank.gas or '?', math.floor(tank.maxDepth or 0)),
|
|
icon = 'fa-solid fa-bottle-water',
|
|
onSelect = function() selection:resolve(tank) end
|
|
}
|
|
end
|
|
|
|
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,
|
|
canCancel = true,
|
|
anim = {
|
|
dict = 'clothingshirt',
|
|
clip = 'try_shirt_positive_d',
|
|
blendIn = 8.0
|
|
}
|
|
}) then
|
|
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()
|
|
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
|
|
local function getPressure()
|
|
if not activeTank or activeTank.capacity <= 0 then return 0 end
|
|
|
|
return (activeTank.pressure or 0) * (activeTank.oxygen / activeTank.capacity)
|
|
end
|
|
|
|
--- @param visible boolean
|
|
local function sendHudState(visible)
|
|
local oxygen = activeTank and activeTank.oxygen or 0.0
|
|
local capacity = activeTank and activeTank.capacity or 1
|
|
local pressure = getPressure()
|
|
local level = 'ok'
|
|
|
|
if oxygen <= dangerThreshold then
|
|
level = 'danger'
|
|
elseif oxygen <= warnThreshold then
|
|
level = 'warn'
|
|
end
|
|
|
|
SendNUIMessage({
|
|
action = 'state',
|
|
visible = visible,
|
|
oxygen = oxygen,
|
|
capacity = capacity,
|
|
depth = currentDepth,
|
|
pressure = pressure,
|
|
reserve = pressure > 0 and pressure <= config.reservePressure,
|
|
level = level,
|
|
deep = currentDepth >= config.deepWarningDepth,
|
|
-- Grenze der getragenen Flasche überschritten: Tiefenrausch läuft.
|
|
overLimit = config.narcosis.enabled and getDepthOverLimit() >= 0,
|
|
hasLamp = true,
|
|
lamp = lampOn
|
|
})
|
|
end
|
|
|
|
--- Manuell ausgeblendet (eigene Taste oder Export)? Unabhängig davon, ob das
|
|
--- Spiel-HUD gerade sichtbar ist.
|
|
local hudHidden = false
|
|
|
|
--- Blendet das Tauch-HUD aus oder wieder ein, z.B. für Unterwasser-Screenshots.
|
|
--- Als Export nutzbar, damit HUD- und Screenshot-Resources es mitnehmen können:
|
|
--- exports.d4rk_divegear:setHudVisible(false)
|
|
--- @param visible boolean
|
|
local function setHudVisible(visible)
|
|
hudHidden = not visible
|
|
|
|
if hudHidden then
|
|
SendNUIMessage({ action = 'state', visible = false })
|
|
end
|
|
end
|
|
|
|
exports('setHudVisible', setHudVisible)
|
|
exports('isHudVisible', function() return not hudHidden end)
|
|
|
|
--- Pflegt die geglättete Tiefe und schiebt den Zustand ins NUI.
|
|
--- Läuft auch über Wasser weiter, weil getDecayFactor die Tiefe braucht - nur die
|
|
--- Anzeige wird ausgeblendet.
|
|
local function startHudThread()
|
|
CreateThread(function()
|
|
local wasVisible = false
|
|
|
|
SendNUIMessage({
|
|
action = 'labels',
|
|
air = locale('hud.air'),
|
|
depth = locale('hud.depth'),
|
|
pressure = locale('hud.pressure'),
|
|
lamp = locale('hud.lamp')
|
|
})
|
|
|
|
while currentGear.enabled do
|
|
currentDepth = currentDepth + (getDepth() - currentDepth) * 0.25
|
|
|
|
local visible = config.hudEnabled
|
|
and not hudHidden
|
|
and activeTank ~= nil
|
|
-- Standardmäßig schon an Land sichtbar, damit sich der
|
|
-- Flaschendruck ohne Inventar ablesen lässt.
|
|
and (not config.hudOnlyUnderwater or IsPedSwimmingUnderWater(cache.ped))
|
|
-- Folgt dem Spiel-HUD: alles was DisplayHud(false) setzt, blendet
|
|
-- damit auch das Tauch-HUD aus.
|
|
and not (config.hudFollowGameHud and IsHudHidden())
|
|
|
|
if visible or wasVisible then
|
|
sendHudState(visible)
|
|
wasVisible = visible
|
|
end
|
|
|
|
Wait(config.hudUpdateIntervalMs)
|
|
end
|
|
|
|
currentDepth = 0.0
|
|
sendHudState(false)
|
|
end)
|
|
end
|
|
|
|
--- Wertet die Tiefe gegen die Grenze der getragenen Flasche aus und setzt Effekte,
|
|
--- Verbrauchsaufschlag und Schaden. Läuft auch über Wasser weiter, damit die
|
|
--- Effekte beim Auftauchen wieder verschwinden.
|
|
--- @param elapsed number Sekunden seit dem letzten Aufruf
|
|
local function updateNarcosis(elapsed)
|
|
if not config.narcosis.enabled then return end
|
|
|
|
-- Die Engine tötet ab einer bestimmten Tiefe über den Wasserdruck. Dagegen hilft
|
|
-- kein Atemgas, deshalb warnt das hier unabhängig von der Flasche.
|
|
local engineWarnAt = config.narcosis.engineDepthLimit - config.narcosis.engineWarnBeforeMeters
|
|
|
|
if currentDepth >= engineWarnAt then
|
|
if not engineDepthWarned then
|
|
engineDepthWarned = true
|
|
exports.qbx_core:Notify(locale('error.crush_depth', math.floor(config.narcosis.engineDepthLimit)), 'error')
|
|
end
|
|
else
|
|
engineDepthWarned = false
|
|
end
|
|
|
|
local over = getDepthOverLimit()
|
|
local stages = config.narcosis.stages
|
|
local stage = 0
|
|
|
|
for i = 1, #stages do
|
|
if over >= stages[i].over then stage = i end
|
|
end
|
|
|
|
applyNarcosisStage(stage)
|
|
|
|
-- Vorwarnung, solange noch Zeit zum Umkehren ist.
|
|
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')
|
|
end
|
|
else
|
|
narcosisWarned = false
|
|
end
|
|
|
|
local data = stage > 0 and stages[stage]
|
|
|
|
if not data or (data.damagePerTick or 0) <= 0 then
|
|
narcosisDamageBuffer = 0.0
|
|
return
|
|
end
|
|
|
|
-- Schaden sammeln statt jeden Tick zu runden, sonst geht bei 250 ms Takt
|
|
-- alles unter 4 Schaden pro Sekunde verloren.
|
|
narcosisDamageBuffer = narcosisDamageBuffer + data.damagePerTick * elapsed
|
|
|
|
if narcosisDamageBuffer >= 1.0 then
|
|
local damage = math.floor(narcosisDamageBuffer)
|
|
narcosisDamageBuffer -= damage
|
|
ApplyDamageToPed(cache.ped, damage, false)
|
|
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
|
|
Wait(250)
|
|
|
|
local now = GetGameTimer()
|
|
local elapsed = (now - lastTick) / 1000
|
|
lastTick = now
|
|
|
|
updateNarcosis(elapsed)
|
|
|
|
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 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
|
|
|
|
::continue::
|
|
end
|
|
end)
|
|
end
|
|
|
|
--- Hängt die Ausrüstung nach, wenn sie verloren gegangen ist, und stellt die
|
|
--- Scuba-Flags wieder her. Nötig weil Fremd-Scripts (Emotes, Handcuffs, Death-Handler)
|
|
--- attached Props aufräumen und SetEnableScuba/SetPedMaxTimeUnderwater bei Respawn
|
|
--- und Fahrzeugausstieg vom Spiel zurückgesetzt werden.
|
|
local function startGearWatchdogThread()
|
|
CreateThread(function()
|
|
while currentGear.enabled 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
|
|
|
|
Wait(config.gearWatchdogIntervalMs)
|
|
end
|
|
end)
|
|
end
|
|
|
|
local function putOnSuit()
|
|
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
|
|
|
|
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,
|
|
label = locale('info.put_suit'),
|
|
useWhileDead = false,
|
|
canCancel = true,
|
|
anim = {
|
|
dict = 'clothingshirt',
|
|
clip = 'try_shirt_positive_d',
|
|
blendIn = 8.0
|
|
}
|
|
}) then
|
|
activeTank = tank
|
|
warned = {}
|
|
|
|
deleteGear()
|
|
attachGear()
|
|
enableScuba()
|
|
currentGear.enabled = true
|
|
-- Initiate breathing suit audio
|
|
startOxygenLevelDecrementerThread()
|
|
startHudThread()
|
|
startGearWatchdogThread()
|
|
end
|
|
end
|
|
|
|
RegisterNetEvent('d4rk_divegear:client:useGear', function()
|
|
if currentGear.enabled then
|
|
takeOffSuit()
|
|
else
|
|
putOnSuit()
|
|
end
|
|
end)
|
|
|
|
lib.addKeybind({
|
|
name = 'divegear_hud',
|
|
description = locale('keybind.toggle_hud'),
|
|
defaultKey = config.hudToggleKey,
|
|
onPressed = function()
|
|
setHudVisible(hudHidden)
|
|
end
|
|
})
|
|
|
|
lib.addKeybind({
|
|
name = 'divegear_lamp',
|
|
description = locale('keybind.toggle_lamp'),
|
|
defaultKey = config.lampKey,
|
|
onPressed = function()
|
|
if not currentGear.enabled then return end
|
|
|
|
setLamp(not lampOn, true)
|
|
end
|
|
})
|
|
|
|
--- Wo Prop und Lichtkegel am Bone sitzen, lässt sich nicht am Schreibtisch
|
|
--- erraten - das hängt daran, wie das Model selbst ausgerichtet ist. Diese Befehle
|
|
--- ändern die Werte im laufenden Spiel und geben die Zeile zum Übernehmen aus,
|
|
--- damit nicht pro Versuch die Resource neu gestartet werden muss.
|
|
---
|
|
--- lib.require cached Module, main.lua und light.lua teilen sich also dieselbe
|
|
--- config-Tabelle - das Licht zieht sofort mit.
|
|
if config.debug then
|
|
--- @return vector3?
|
|
local function readVec(args, from)
|
|
local x, y, z = tonumber(args[from]), tonumber(args[from + 1]), tonumber(args[from + 2])
|
|
|
|
if not x or not y or not z then return end
|
|
|
|
return vec3(x, y, z)
|
|
end
|
|
|
|
local function fmtVec(v)
|
|
return ('vec3(%.2f, %.2f, %.2f)'):format(v.x, v.y, v.z)
|
|
end
|
|
|
|
RegisterCommand('divelamp', function(_, args)
|
|
local offset, rotation = readVec(args, 1), readVec(args, 4)
|
|
|
|
if not offset or not rotation then
|
|
lib.print.info('/divelamp <x> <y> <z> <rx> <ry> <rz> - Sitz des Lampen-Props')
|
|
return
|
|
end
|
|
|
|
config.lampPropOffset, config.lampPropRotation = offset, rotation
|
|
|
|
if currentGear.enabled then
|
|
deleteGear()
|
|
attachGear()
|
|
end
|
|
|
|
lib.print.info(('lampPropOffset = %s,\nlampPropRotation = %s,'):format(fmtVec(offset), fmtVec(rotation)))
|
|
end, false)
|
|
|
|
--- Die Spiel-Lampe braucht die Scuba-Kleidung am Ped. Dieser Befehl sagt, ob
|
|
--- das aktuelle Outfit sie überhaupt hat - sonst rätselt man, warum nichts
|
|
--- leuchtet.
|
|
RegisterCommand('divelampcheck', function()
|
|
SetEnableScubaGearLight(cache.ped, true)
|
|
local works = IsScubaGearLightEnabled(cache.ped)
|
|
SetEnableScubaGearLight(cache.ped, lampOn)
|
|
|
|
lib.print.info(('Scuba-Lampe am aktuellen Outfit: %s'):format(works and 'ja' or 'nein'))
|
|
end, false)
|
|
end
|
|
|
|
--- Beim Model-Wechsel (Kleiderladen, Skin-Change) ist der Ped ein anderer -
|
|
--- alle attached Props sind weg und die Scuba-Flags gelten für den alten Ped.
|
|
---
|
|
--- Der neue Ped kommt zwingend aus dem Callback-Parameter: ox_lib feuert die
|
|
--- onCache-Callbacks über Citizen.CreateThreadNow, *bevor* der neue Wert in den
|
|
--- Cache geschrieben wird (ox_lib/init.lua). cache.ped zeigt hier also noch auf
|
|
--- den alten, bereits verschwundenen Ped.
|
|
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)
|
|
|
|
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)
|
|
end)
|