Files
d4rk_divegear/client/main.lua
T
D4rkst3randClaude Opus 5 921d9c89c8
Lint / Lint Resource (push) Has been cancelled
fix(client): Ausruestung verschwindet nicht mehr bei Animationen
Die Props waren soft-pinned angehaengt (useSoftPinning = true), wodurch die
Physik-Engine die Attachment bei Ragdoll und harten Anim-Uebergaengen loesen
darf. Dazu kam, dass ein Ped-Model-Wechsel und aufraeumende Fremd-Scripts
(Emotes, Handcuffs) gar nicht abgefangen wurden.

- attachProp() Helper mit harter Attachment (useSoftPinning = false)
- Watchdog-Thread prueft jede Sekunde, ob beide Props noch am aktuellen Ped
  haengen, und haengt sie sonst neu an
- Scuba-Flags werden im Watchdog neu gesetzt, weil das Spiel sie bei Respawn
  und Fahrzeugausstieg zuruecksetzt
- lib.onCache('ped') haengt die Props nach einem Model-Wechsel neu an
- Ausruestung wird beim Tod abgenommen (removeGearOnDeath, default true)
- removeGear() als gemeinsamer Teardown fuer Ablegen, leere Flasche und Tod
- onResourceStop raeumt Props und Scuba-Flags auf
- maxTimeUnderwater / maxTimeUnderwaterOutOfAir statt Magic Numbers

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 23:24:39 +02:00

249 lines
7.1 KiB
Lua

local config = require 'config.client'
local currentGear = {
mask = 0,
tank = 0,
enabled = false
}
local oxygenLevel = 0
local MASK_MODEL = `p_d_scuba_mask_s`
local TANK_MODEL = `p_s_scuba_tank_s`
local MASK_BONE = 12844
local TANK_BONE = 24818
local function enableScuba()
SetEnableScuba(cache.ped, true)
SetPedMaxTimeUnderwater(cache.ped, 2000.00)
end
--- @param outOfAir boolean? true wenn die Flasche leer ist statt die Ausrüstung abgelegt wurde
local function disableScuba(outOfAir)
SetEnableScuba(cache.ped, false)
SetPedMaxTimeUnderwater(cache.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(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, cache.ped, GetPedBoneIndex(cache.ped, bone),
offset.x, offset.y, offset.z,
rotation.x, rotation.y, rotation.z,
false, -- p9
false, -- useSoftPinning
false, -- collision
false, -- isPed
2, -- rotationOrder
true) -- fixedRot
SetModelAsNoLongerNeeded(model)
return prop
end
local function attachGear()
currentGear.tank = attachProp(TANK_MODEL, TANK_BONE, vec3(-0.25, -0.25, 0.0), vec3(180.0, 90.0, 0.0))
currentGear.mask = attachProp(MASK_MODEL, MASK_BONE, vec3(0.0, 0.0, 0.0), vec3(180.0, 90.0, 0.0))
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
--- Gemeinsamer Teardown für Ablegen, leere Flasche und Tod.
--- @param outOfAir boolean?
local function removeGear(outOfAir)
currentGear.enabled = false
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
end
if 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
oxygenLevel = config.startingOxygenLevel
exports.qbx_core:Notify(locale('success.tube_filled'), 'success')
if currentGear.enabled then
enableScuba()
end
return true
end
end)
local function takeOffSuit()
if lib.progressBar({
duration = config.takeOffSuitTimeMs,
label = locale('info.pullout_suit'),
useWhileDead = false,
canCancel = true,
anim = {
dict = 'clothingshirt',
clip = 'try_shirt_positive_d',
blendIn = 8.0
}
}) then
removeGear()
exports.qbx_core:Notify(locale('success.took_out'))
end
end
local function startOxygenLevelDrawTextThread()
CreateThread(function()
while currentGear.enabled do
if IsPedSwimmingUnderWater(cache.ped) then
qbx.drawText2d({
text = oxygenLevel..'⏱',
coords = vec2(1.0, 1.42),
scale = 0.45
})
end
Wait(0)
end
end)
end
local function startOxygenLevelDecrementerThread()
CreateThread(function()
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
end
if oxygenLevel == 0 then
disableScuba(true)
-- Stop breathing suit audio
end
end
Wait(1000)
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()
break
end
if not isGearIntact() then
deleteGear()
attachGear()
end
if oxygenLevel > 0 then
enableScuba()
end
Wait(config.gearWatchdogIntervalMs)
end
end)
end
local function putOnSuit()
if oxygenLevel <= 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')
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
deleteGear()
attachGear()
enableScuba()
currentGear.enabled = true
-- Initiate breathing suit audio
startOxygenLevelDecrementerThread()
startOxygenLevelDrawTextThread()
startGearWatchdogThread()
end
end
RegisterNetEvent('qbx_divegear:client:useGear', function()
if currentGear.enabled then
takeOffSuit()
else
putOnSuit()
end
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.
lib.onCache('ped', function()
if not currentGear.enabled then return end
deleteGear()
attachGear()
if oxygenLevel > 0 then
enableScuba()
end
end)
AddEventHandler('onResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end
deleteGear()
SetEnableScuba(cache.ped, false)
SetPedMaxTimeUnderwater(cache.ped, config.maxTimeUnderwater)
end)