fix(client): Ausruestung verschwindet nicht mehr bei Animationen
Lint / Lint Resource (push) Has been cancelled
Lint / Lint Resource (push) Has been cancelled
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>
This commit is contained in:
@@ -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
|
Zuerst, weil kleinster Aufwand, echter Bug, und die Taschenlampe dieselbe
|
||||||
Attach-Logik nutzt.
|
Attach-Logik nutzt.
|
||||||
|
|
||||||
- [ ] `attachGear()` auf harte Attachment umstellen: `useSoftPinning = false`,
|
- [x] `attachGear()` auf harte Attachment umstellen: `useSoftPinning = false`,
|
||||||
`p9 = false`, `fixedRot = true`
|
`p9 = false`, `fixedRot = true` — ausgelagert in einen `attachProp()`-Helper,
|
||||||
- [ ] `isGearIntact()` Helper: `DoesEntityExist` + `GetEntityAttachedTo(...) == cache.ped`
|
den Phase 5 für die Lampe mitbenutzt
|
||||||
|
- [x] `isGearIntact()` Helper: `DoesEntityExist` + `GetEntityAttachedTo(...) == cache.ped`
|
||||||
für beide Props
|
für beide Props
|
||||||
- [ ] Watchdog-Thread (1000 ms) solange `currentGear.enabled`: bei Defekt
|
- [x] Watchdog-Thread (`gearWatchdogIntervalMs`, default 1000 ms) solange
|
||||||
`deleteGear()` + `attachGear()`, und `enableScuba()` unbedingt neu setzen
|
`currentGear.enabled`: bei Defekt `deleteGear()` + `attachGear()`, und
|
||||||
(`SetEnableScuba`/`SetPedMaxTimeUnderwater` werden bei Respawn/Fahrzeugausstieg
|
`enableScuba()` neu setzen (`SetEnableScuba`/`SetPedMaxTimeUnderwater` werden bei
|
||||||
vom Spiel zurückgesetzt)
|
Respawn/Fahrzeugausstieg vom Spiel zurückgesetzt)
|
||||||
- [ ] `lib.onCache('ped', ...)` → bei Model-Wechsel (Kleiderladen, Skin-Change)
|
- [x] `lib.onCache('ped', ...)` → bei Model-Wechsel (Kleiderladen, Skin-Change)
|
||||||
Props neu anhängen, Scuba-State neu setzen
|
Props neu anhängen, Scuba-State neu setzen
|
||||||
- [ ] Death-Handler: beim Tod Gear sofort ablegen (ohne Progressbar), Restluft
|
- [x] Death-Handler: `IsPedDeadOrDying`-Check im Watchdog statt eigenem Event —
|
||||||
vorher sichern — Flasche bleibt im Inventar
|
keine Abhängigkeit zu qbx_medical o.ä. Sichern der Restluft kommt in Phase 2 dazu
|
||||||
- [ ] Inkonsistenz aufräumen: `takeOffSuit()` setzt `SetPedMaxTimeUnderwater(ped, 50.0)`,
|
- [x] Gemeinsamer `removeGear(outOfAir)`-Teardown für Ablegen, leere Flasche und Tod
|
||||||
`disableScuba()` setzt `1.0` — einen Wert festlegen
|
- [x] `onResourceStop` Cleanup-Handler für Props
|
||||||
- [ ] `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)
|
### Phase 2 — Sauerstoff-Logik neu (~1 h)
|
||||||
|
|
||||||
|
|||||||
+117
-41
@@ -8,14 +8,79 @@ local currentGear = {
|
|||||||
|
|
||||||
local oxygenLevel = 0
|
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()
|
local function enableScuba()
|
||||||
SetEnableScuba(cache.ped, true)
|
SetEnableScuba(cache.ped, true)
|
||||||
SetPedMaxTimeUnderwater(cache.ped, 2000.00)
|
SetPedMaxTimeUnderwater(cache.ped, 2000.00)
|
||||||
end
|
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)
|
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
|
end
|
||||||
|
|
||||||
lib.callback.register('qbx_divegear:client:fillTank', function()
|
lib.callback.register('qbx_divegear:client:fillTank', function()
|
||||||
@@ -44,39 +109,6 @@ lib.callback.register('qbx_divegear:client:fillTank', function()
|
|||||||
end
|
end
|
||||||
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()
|
local function takeOffSuit()
|
||||||
if lib.progressBar({
|
if lib.progressBar({
|
||||||
duration = config.takeOffSuitTimeMs,
|
duration = config.takeOffSuitTimeMs,
|
||||||
@@ -89,12 +121,8 @@ local function takeOffSuit()
|
|||||||
blendIn = 8.0
|
blendIn = 8.0
|
||||||
}
|
}
|
||||||
}) then
|
}) then
|
||||||
SetEnableScuba(cache.ped, false)
|
removeGear()
|
||||||
SetPedMaxTimeUnderwater(cache.ped, 50.00)
|
|
||||||
currentGear.enabled = false
|
|
||||||
deleteGear()
|
|
||||||
exports.qbx_core:Notify(locale('success.took_out'))
|
exports.qbx_core:Notify(locale('success.took_out'))
|
||||||
-- Stop breathing suit audio
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -122,7 +150,7 @@ local function startOxygenLevelDecrementerThread()
|
|||||||
-- Initiate breathing suit audio
|
-- Initiate breathing suit audio
|
||||||
end
|
end
|
||||||
if oxygenLevel == 0 then
|
if oxygenLevel == 0 then
|
||||||
disableScuba()
|
disableScuba(true)
|
||||||
-- Stop breathing suit audio
|
-- Stop breathing suit audio
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -131,6 +159,32 @@ local function startOxygenLevelDecrementerThread()
|
|||||||
end)
|
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()
|
local function putOnSuit()
|
||||||
if oxygenLevel <= 0 then
|
if oxygenLevel <= 0 then
|
||||||
exports.qbx_core:Notify(locale('error.need_otube'), 'error')
|
exports.qbx_core:Notify(locale('error.need_otube'), 'error')
|
||||||
@@ -160,6 +214,7 @@ local function putOnSuit()
|
|||||||
-- Initiate breathing suit audio
|
-- Initiate breathing suit audio
|
||||||
startOxygenLevelDecrementerThread()
|
startOxygenLevelDecrementerThread()
|
||||||
startOxygenLevelDrawTextThread()
|
startOxygenLevelDrawTextThread()
|
||||||
|
startGearWatchdogThread()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -170,3 +225,24 @@ RegisterNetEvent('qbx_divegear:client:useGear', function()
|
|||||||
putOnSuit()
|
putOnSuit()
|
||||||
end
|
end
|
||||||
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)
|
||||||
|
|||||||
@@ -4,4 +4,16 @@ return {
|
|||||||
takeOffSuitTimeMs = 5000,
|
takeOffSuitTimeMs = 5000,
|
||||||
refillTankTimeMs = 5000,
|
refillTankTimeMs = 5000,
|
||||||
decayRate = 1, -- The rate at which the oxygen level decays. Defaults to 1.
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user