diff --git a/PLAN.md b/PLAN.md index 4e9823a..f3bbff7 100644 --- a/PLAN.md +++ b/PLAN.md @@ -34,21 +34,26 @@ Reihenfolge ist bewusst gewählt: Phase 1 und 2 sind Fundament, Phase 3–5 baue Zuerst, weil kleinster Aufwand, echter Bug, und die Taschenlampe dieselbe Attach-Logik nutzt. -- [ ] `attachGear()` auf harte Attachment umstellen: `useSoftPinning = false`, - `p9 = false`, `fixedRot = true` -- [ ] `isGearIntact()` Helper: `DoesEntityExist` + `GetEntityAttachedTo(...) == cache.ped` +- [x] `attachGear()` auf harte Attachment umstellen: `useSoftPinning = false`, + `p9 = false`, `fixedRot = true` — ausgelagert in einen `attachProp()`-Helper, + den Phase 5 für die Lampe mitbenutzt +- [x] `isGearIntact()` Helper: `DoesEntityExist` + `GetEntityAttachedTo(...) == cache.ped` für beide Props -- [ ] Watchdog-Thread (1000 ms) solange `currentGear.enabled`: bei Defekt - `deleteGear()` + `attachGear()`, und `enableScuba()` unbedingt neu setzen - (`SetEnableScuba`/`SetPedMaxTimeUnderwater` werden bei Respawn/Fahrzeugausstieg - vom Spiel zurückgesetzt) -- [ ] `lib.onCache('ped', ...)` → bei Model-Wechsel (Kleiderladen, Skin-Change) +- [x] Watchdog-Thread (`gearWatchdogIntervalMs`, default 1000 ms) solange + `currentGear.enabled`: bei Defekt `deleteGear()` + `attachGear()`, und + `enableScuba()` neu setzen (`SetEnableScuba`/`SetPedMaxTimeUnderwater` werden bei + Respawn/Fahrzeugausstieg vom Spiel zurückgesetzt) +- [x] `lib.onCache('ped', ...)` → bei Model-Wechsel (Kleiderladen, Skin-Change) Props neu anhängen, Scuba-State neu setzen -- [ ] Death-Handler: beim Tod Gear sofort ablegen (ohne Progressbar), Restluft - vorher sichern — Flasche bleibt im Inventar -- [ ] Inkonsistenz aufräumen: `takeOffSuit()` setzt `SetPedMaxTimeUnderwater(ped, 50.0)`, - `disableScuba()` setzt `1.0` — einen Wert festlegen -- [ ] `onResourceStop` Cleanup-Handler für Props +- [x] Death-Handler: `IsPedDeadOrDying`-Check im Watchdog statt eigenem Event — + keine Abhängigkeit zu qbx_medical o.ä. Sichern der Restluft kommt in Phase 2 dazu +- [x] Gemeinsamer `removeGear(outOfAir)`-Teardown für Ablegen, leere Flasche und Tod +- [x] `onResourceStop` Cleanup-Handler für Props + +**Zu `SetPedMaxTimeUnderwater`:** war doch kein Bug. Die `50.0` beim Ablegen und die +`1.0` bei leerer Flasche sind zwei verschiedene Absichten (normal weiterschwimmen vs. +ohne Luft absaufen). Beide Werte sind jetzt als `maxTimeUnderwater` / +`maxTimeUnderwaterOutOfAir` in der Config statt als Magic Number im Code. ### Phase 2 — Sauerstoff-Logik neu (~1 h) diff --git a/client/main.lua b/client/main.lua index c4129ed..df36b0b 100644 --- a/client/main.lua +++ b/client/main.lua @@ -8,14 +8,79 @@ local currentGear = { 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 -local function disableScuba() +--- @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, 1.00) + 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() @@ -44,39 +109,6 @@ lib.callback.register('qbx_divegear:client:fillTank', function() end 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 - -local function attachGear() - local maskModel = `p_d_scuba_mask_s` - local tankModel = `p_s_scuba_tank_s` - lib.requestModel(maskModel) - lib.requestModel(tankModel) - - currentGear.tank = CreateObject(tankModel, 1.0, 1.0, 1.0, true, true, false) - SetEntityCollision(currentGear.tank, false, false) --- Disable collision for the tank so it doesn't block bullets - local bone1 = GetPedBoneIndex(cache.ped, 24818) - AttachEntityToEntity(currentGear.tank, cache.ped, bone1, -0.25, -0.25, 0.0, 180.0, 90.0, 0.0, true, true, false, false, 2, true) - - currentGear.mask = CreateObject(maskModel, 1.0, 1.0, 1.0, true, true, false) - SetEntityCollision(currentGear.mask, false, false) --- Disable collision for the mask so it doesn't block bullets - local bone2 = GetPedBoneIndex(cache.ped, 12844) - AttachEntityToEntity(currentGear.mask, cache.ped, bone2, 0.0, 0.0, 0.0, 180.0, 90.0, 0.0, true, true, false, false, 2, true) - SetModelAsNoLongerNeeded(maskModel) - SetModelAsNoLongerNeeded(tankModel) -end - local function takeOffSuit() if lib.progressBar({ duration = config.takeOffSuitTimeMs, @@ -89,12 +121,8 @@ local function takeOffSuit() blendIn = 8.0 } }) then - SetEnableScuba(cache.ped, false) - SetPedMaxTimeUnderwater(cache.ped, 50.00) - currentGear.enabled = false - deleteGear() + removeGear() exports.qbx_core:Notify(locale('success.took_out')) - -- Stop breathing suit audio end end @@ -122,7 +150,7 @@ local function startOxygenLevelDecrementerThread() -- Initiate breathing suit audio end if oxygenLevel == 0 then - disableScuba() + disableScuba(true) -- Stop breathing suit audio end end @@ -131,6 +159,32 @@ local function startOxygenLevelDecrementerThread() 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') @@ -160,6 +214,7 @@ local function putOnSuit() -- Initiate breathing suit audio startOxygenLevelDecrementerThread() startOxygenLevelDrawTextThread() + startGearWatchdogThread() end end @@ -170,3 +225,24 @@ RegisterNetEvent('qbx_divegear:client:useGear', function() 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) diff --git a/config/client.lua b/config/client.lua index 1665c0a..b06f7e1 100644 --- a/config/client.lua +++ b/config/client.lua @@ -4,4 +4,16 @@ return { takeOffSuitTimeMs = 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). + gearWatchdogIntervalMs = 1000, + + -- Sekunden, die der Spieler ohne Ausrüstung unter Wasser überlebt (Vanilla-Verhalten). + maxTimeUnderwater = 50.0, + + -- Sekunden, sobald die Flasche leer ist. Bewusst kurz: leer tauchen soll wehtun. + maxTimeUnderwaterOutOfAir = 1.0, + + -- Ausrüstung beim Tod abnehmen. Die Flasche bleibt mit ihrer Restluft im Inventar. + removeGearOnDeath = true, }