diff --git a/.vscode/settings.json b/.vscode/settings.json index f56fbb6..db839f4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,12 +1,11 @@ { - "Lua.runtime.nonstandardSymbol": ["/**/", "`", "+=", "-=", "*=", "/="], - "Lua.runtime.version": "Lua 5.4", - "Lua.diagnostics.globals": [ - "lib", - "cache", - "locale", - "MySQL", - "QBX", - "qbx" - ] + "Lua.runtime.nonstandardSymbol": ["/**/", "`", "+=", "-=", "*=", "/="], + "Lua.runtime.version": "Lua 5.4", + "Lua.diagnostics.globals": [ + "lib", + "cache", + "QBX", + ], + "editor.tabSize": 2, + "files.trimTrailingWhitespace": true } diff --git a/client.lua b/client.lua new file mode 100644 index 0000000..e03d9ef --- /dev/null +++ b/client.lua @@ -0,0 +1,20 @@ +--- @class CLStateManager +local CLStateManager = require 'modules.statemanager.client' + +local stateManager = CLStateManager:new() +stateManager:init() + +local function addToState(stateId, value) + return lib.callback.await('qbx_playerstates:server:addToState', false, stateId, value) +end +exports('AddToState', addToState) + +local function clearState(stateId) + return lib.callback.await('qbx_playerstates:server:clearState', false, stateId) +end +exports('ClearState', clearState) + +local function clearAllStates() + return lib.callback.await('qbx_playerstates:server:clearAllStates', false) +end +exports('ClearAllStates', clearAllStates) diff --git a/client/hunger.lua b/client/hunger.lua new file mode 100644 index 0000000..a6f1ae3 --- /dev/null +++ b/client/hunger.lua @@ -0,0 +1,20 @@ +local hungerConfig = require 'config.states.hunger' +local hungerEffectData = hungerConfig.effectData +if not hungerEffectData then return end + +local statusInterval = hungerEffectData.statusInterval +local decreaseHealthRange = hungerEffectData.decreaseHealthRange + +CreateThread(function() + while true do + Wait(statusInterval) + if LocalPlayer.state.isLoggedIn and not LocalPlayer.state.isDead then + local hunger = LocalPlayer.state.hunger or hungerConfig.value.default + if hunger <= 0 then + local currentHealth = GetEntityHealth(cache.ped) + local decreaseThreshold = math.random(decreaseHealthRange.min, decreaseHealthRange.max) + SetEntityHealth(cache.ped, currentHealth - decreaseThreshold) + end + end + end +end) diff --git a/client/painkiller.lua b/client/painkiller.lua new file mode 100644 index 0000000..e69f24d --- /dev/null +++ b/client/painkiller.lua @@ -0,0 +1,34 @@ +local painDisabled = false + +local function disablePain() + if not painDisabled then + painDisabled = true + exports.qbx_medical:DisableDamageEffects() + lib.notify({description = 'Painkillers effects have started'}) + end +end + +local function enablePain() + if painDisabled then + painDisabled = false + exports.qbx_medical:EnableDamageEffects() + lib.notify({description = 'Painkillers effects have ended'}) + end +end + +CreateThread(function() + while true do + Wait(10 * 1000) + if LocalPlayer.state.isLoggedIn and not LocalPlayer.state.isDead then + if LocalPlayer.state.painkiller then + if LocalPlayer.state.painkiller > 0 then + disablePain() + else + enablePain() + end + else + enablePain() + end + end + end +end) diff --git a/client/stress.lua b/client/stress.lua new file mode 100644 index 0000000..759f889 --- /dev/null +++ b/client/stress.lua @@ -0,0 +1,144 @@ +-- Stress Gain : Speeding +local stressConfig = require 'config.states.stress' +local stressData = stressConfig.effectData or {} + +local stressSpeed = stressData.stressSpeed +local speedStressRange = stressData.speedStressRange +local speedStressInterval = stressData.speedStressInterval + +local function isSpeedStressWhitelistedJob() + local playerJob = QBX.PlayerData.job.name + for _, v in pairs(stressData.speedStressWhitlistedJobs) do + if playerJob == v then + return true + end + end + return false +end + +CreateThread(function() + while true do + if LocalPlayer.state.isLoggedIn and not isSpeedStressWhitelistedJob() and not LocalPlayer.state.isDead then + if cache.vehicle then + local vehClass = GetVehicleClass(cache.vehicle) + local speed = GetEntitySpeed(cache.vehicle) + if vehClass ~= 13 and vehClass ~= 14 and vehClass ~= 15 and vehClass ~= 16 and vehClass ~= 21 then + if speed >= stressSpeed then + exports.qbx_playerstates:AddToState('stress', math.random(speedStressRange.min, speedStressRange.max)) + end + end + end + end + Wait(speedStressInterval) + end +end) + +-- Stress Gain : Shooting +local hasWeapon = false +local whitelistedWeapons = stressData.whitelistedWeapons +local stressChance = stressData.stressChance +local weaponStressRange = stressData.weaponStressRange + +local function isWhitelistedWeaponStress(weapon) + if weapon then + for _, v in pairs(whitelistedWeapons) do + if + type(v) == 'string' and + type(weapon) == 'string' and + weapon:lower() == v:lower() + then + return true + end + end + end + return false +end + +local function startWeaponStressThread(weapon) + if isWhitelistedWeaponStress(weapon) then return end + hasWeapon = true + CreateThread(function() + while hasWeapon and not LocalPlayer.state.isDead do + if IsPedShooting(cache.ped) then + if math.random() <= stressChance then + exports.qbx_playerstates:AddToState('stress', math.random(weaponStressRange.min, weaponStressRange.max)) + end + end + Wait(0) + end + end) +end + +AddEventHandler('ox_inventory:currentWeapon', function(currentWeapon) + hasWeapon = false + Wait(0) + if not currentWeapon then return end + startWeaponStressThread(currentWeapon.name) +end) + +-- Stress Effects -- + +local minForShaking = stressData.minForShaking +local blurIntensity = stressData.blurIntensity +local effectInterval = stressData.effectInterval + +local function getBlurIntensity(stresslevel) + for _, v in pairs(blurIntensity) do + if stresslevel >= v.min and stresslevel <= v.max then + return v.intensity + end + end + return 1500 +end + +local function getEffectInterval(stresslevel) + for _, v in pairs(effectInterval) do + if stresslevel >= v.min and stresslevel <= v.max then + return v.timeout + end + end + return 60000 +end + +CreateThread(function() + while true do + if LocalPlayer.state.isLoggedIn and not LocalPlayer.state.isDead and not LocalPlayer.state.onEffect then + local stress = LocalPlayer.state.stress or stressConfig.value.default + local effectWaitInterval = getEffectInterval(stress) + if stress >= stressConfig.value.max then + LocalPlayer.state.onEffect = true + local blurIntensityInterval = getBlurIntensity(stress) + local fallRepeat = math.random(2, 4) + local ragdollTimeout = fallRepeat * 1750 + TriggerScreenblurFadeIn(1000.0) + Wait(blurIntensityInterval) + TriggerScreenblurFadeOut(1000.0) + if not IsPedRagdoll(cache.ped) and IsPedOnFoot(cache.ped) and not IsPedSwimming(cache.ped) then + local forwardVector = GetEntityForwardVector(cache.ped) + SetPedToRagdollWithFall(cache.ped, ragdollTimeout, ragdollTimeout, 1, forwardVector.x, forwardVector.y, forwardVector.z, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) + end + Wait(1000) + for _ = 1, fallRepeat, 1 do + Wait(750) + DoScreenFadeOut(200) + Wait(1000) + DoScreenFadeIn(200) + TriggerScreenblurFadeIn(1000.0) + Wait(blurIntensityInterval) + TriggerScreenblurFadeOut(1000.0) + end + LocalPlayer.state.onEffect = false + elseif stress >= minForShaking then + LocalPlayer.state.onEffect = true + local blurIntensityInterval = getBlurIntensity(stress) + TriggerScreenblurFadeIn(1000.0) + Wait(blurIntensityInterval) + TriggerScreenblurFadeOut(1000.0) + LocalPlayer.state.onEffect = false + end + Wait(effectWaitInterval) + else + Wait(1000) + end + end +end) diff --git a/client/thirst.lua b/client/thirst.lua new file mode 100644 index 0000000..7aba537 --- /dev/null +++ b/client/thirst.lua @@ -0,0 +1,20 @@ +local thirstConfig = require 'config.states.thirst' +local thirstEffectData = thirstConfig.effectData +if not thirstEffectData then return end + +local statusInterval = thirstEffectData.statusInterval +local decreaseHealthRange = thirstEffectData.decreaseHealthRange + +CreateThread(function() + while true do + Wait(statusInterval) + if LocalPlayer.state.isLoggedIn and not LocalPlayer.state.isDead then + local thirst = LocalPlayer.state.thirst or thirstConfig.value.default + if thirst <= 0 then + local currentHealth = GetEntityHealth(cache.ped) + local decreaseThreshold = math.random(decreaseHealthRange.min, decreaseHealthRange.max) + SetEntityHealth(cache.ped, currentHealth - decreaseThreshold) + end + end + end +end) diff --git a/config/shared.lua b/config/shared.lua new file mode 100644 index 0000000..5c589ca --- /dev/null +++ b/config/shared.lua @@ -0,0 +1,19 @@ +--- @type table +local allStates = {} +-- numeric +allStates.stress = require 'config.states.stress' +allStates.hunger = require 'config.states.hunger' +allStates.thirst = require 'config.states.thirst' +allStates.painkiller = require 'config.states.painkiller' + +-- boolean +allStates.dead = require 'config.states.dead' +allStates.cuff = require 'config.states.cuff' + +-- custom +allStates.escort = require 'config.states.escort' +allStates.escorted = require 'config.states.escorted' +allStates.carry = require 'config.states.carry' +allStates.carried = require 'config.states.carried' + +return allStates diff --git a/config/states/carried.lua b/config/states/carried.lua new file mode 100644 index 0000000..1f0be63 --- /dev/null +++ b/config/states/carried.lua @@ -0,0 +1,62 @@ +--- @type PlayerStateConfig +return { + id = 'carried', + label = 'Carried', + fields = { stateBag = 'carriedBy' }, + permanent = false, + value = { + default = nil, + }, + notification = { + up = { + id = 'state-carried-up-notif', + title = 'Carried', + description = 'You are being carried', + duration = 2000, + icon = 'user-friends', + iconColor = '#4CAF50', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = 1, + }, + down = { + id = 'state-carried-down-notif', + title = 'Carried', + description = 'You are no longer being carried', + duration = 2000, + icon = 'user-large-slash', + iconColor = '#C53030', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = -1, + }, + }, + disable = { + condition = function(value) + return value ~= nil and value + end, + keys = { + 0, 7, 20, 24, 25, 26, 29, 30, 32, 33, 34, 35, 44, + 45, 46, 47, 48, 49, 59, 74, 75, 77, 140, 141, 142, + 144, 145, 185, 199, 244, 251, 246, 303, 323, + }, + emotes = true, + radio = true, + phone = true, + inventory = true, + }, + forcedAnimations = { + { + condition = function(value) + return value ~= nil and value + end, + dict = 'nm', + name = 'firemans_carry', + flag = 1, + } + }, +} diff --git a/config/states/carry.lua b/config/states/carry.lua new file mode 100644 index 0000000..edef69d --- /dev/null +++ b/config/states/carry.lua @@ -0,0 +1,62 @@ +--- @type PlayerStateConfig +return { + id = 'carry', + label = 'Carry', + fields = { stateBag = 'carry' }, + permanent = false, + value = { + default = nil, + }, + notification = { + up = { + id = 'state-carry-up-notif', + title = 'Carry', + description = 'You are carrying someone', + duration = 2000, + icon = 'user-friends', + iconColor = '#4CAF50', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = 1, + }, + down = { + id = 'state-carry-down-notif', + title = 'Carry', + description = 'You are no longer carrying someone', + duration = 2000, + icon = 'user-large-slash', + iconColor = '#C53030', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = -1, + }, + }, + disable = { + condition = function(value) + return value ~= nil and value + end, + keys = { + 0, 7, 20, 24, 25, 26, 29, 30, 32, 33, 34, 35, 44, + 45, 46, 47, 48, 49, 59, 74, 75, 77, 140, 141, 142, + 144, 145, 185, 199, 244, 251, 246, 303, 323, + }, + emotes = true, + radio = true, + phone = true, + inventory = true, + }, + forcedAnimations = { + { + condition = function(value) + return value ~= nil and value + end, + dict = 'missfinale_c2mcs_1', + name = 'fin_c2_mcs_1_camman', + flag = 48, + } + }, +} diff --git a/config/states/cuff.lua b/config/states/cuff.lua new file mode 100644 index 0000000..d549fca --- /dev/null +++ b/config/states/cuff.lua @@ -0,0 +1,104 @@ +--- @type PlayerStateConfig +return { + id = 'cuff', + label = 'Handcuffs', + fields = { stateBag = 'isCuffed', metadata = 'ishandcuffed' }, + permanent = true, + value = { + default = nil, + }, + notification = { + up = { + id = 'state-cuff-up-notif', + title = 'Handcuffs', + description = 'You are handcuffed', + duration = 2000, + icon = 'handcuffs', + iconColor = '#4CAF50', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = 1, + }, + down = { + id = 'state-cuff-down-notif', + title = 'Handcuffs', + description = 'You are no longer handcuffed', + duration = 2000, + icon = 'handcuffs', + iconColor = '#C53030', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = -1, + }, + }, + disable = { + condition = function(value) + return value ~= nil and value + end, + keys = { + 21, 22, 24, 25, 26, 36, 37, 44, 45, 59, 71, + 72, 73, 75, 140, 141, 142, 143, 167, 170, + 199, 200, 257, 264, 288 + }, + target = true, + emotes = true, + radio = true, + phone = true, + inventory = true, + radialMenu = true, + }, + forcedAnimations = { + { + condition = function(value) + if not value then return false end + if not LocalPlayer.state.escortedBy then return true end + local escortPlayer = GetPlayerFromServerId(LocalPlayer.state.escortedBy) + if not escortPlayer then return true end + local escortPed = GetPlayerPed(escortPlayer) + if not escortPed then return true end + if not DoesEntityExist(escortPed) then return true end + return + not IsPedWalking(escortPed) and + not IsPedRunning(escortPed) and + not IsPedSprinting(escortPed) + end, + dict = 'mp_arresting', + name = 'idle', + flag = 49, + prop = { + { + bone = 0x49D9, + model = 'p_cs_cuffs_02_s', + pos = vec3(0.04, 0.06, 0.0), + rot = vec3(-85.24, 4.2, -106.6), + }, + }, + }, + { + condition = function(value) + if not value or not LocalPlayer.state.escortedBy then return false end + local escortPlayer = GetPlayerFromServerId(LocalPlayer.state.escortedBy) + if not escortPlayer then return false end + local escortPed = GetPlayerPed(escortPlayer) + if not escortPed then return false end + if not DoesEntityExist(escortPed) then return false end + return IsPedWalking(escortPed) or IsPedRunning(escortPed) or IsPedSprinting(escortPed) + end, + dict = 'mp_arresting', + name = 'walk', + flag = 1, + prop = { + { + bone = 0x49D9, + model = 'p_cs_cuffs_02_s', + pos = vec3(0.04, 0.06, 0.0), + rot = vec3(-85.24, 4.2, -106.6), + }, + }, + }, + }, +} diff --git a/config/states/dead.lua b/config/states/dead.lua new file mode 100644 index 0000000..57f8d77 --- /dev/null +++ b/config/states/dead.lua @@ -0,0 +1,84 @@ +--- @type PlayerStateConfig +return { + id = 'dead', + label = 'Dead', + fields = { stateBag = 'isDead', metadata = 'isdead' }, + permanent = true, + value = { + default = false, + }, + notification = { + up = { + id = 'state-dead-up-notif', + title = 'Dead', + description = 'You are dead', + duration = 2000, + icon = 'skull-crossbones', + iconColor = '#C53030', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = 1, + }, + down = { + id = 'state-dead-down-notif', + title = 'Dead', + description = 'You are Aliiiiive!!! (not really)', + duration = 2000, + icon = 'skull-crossbones', + iconColor = '#4CAF50', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = -1, + }, + }, + disable = { + condition = function(value) + return value ~= nil and value + end, + keys = { + 0, 1, 2, 7, 20, 24, 25, 26, 29, 30, 32, 33, 34, + 35, 38, 44, 45, 46, 47, 48, 49, 59, 74, 75, 77, + 101, 104, 140, 141, 142, 144, 145, 185, 199, + 213, 244, 245, 246, 249, 251, 288, + 303, 304, 322, 323, + }, + emotes = true, + radio = true, + phone = true, + inventory = true, + radialMenu = true, + target = true, + }, + forcedAnimations = { + { + condition = function(value) + return + value and + not LocalPlayer.state.carriedBy and + not LocalPlayer.state.inStretcher and + not LocalPlayer.state.inBed and + not cache.vehicle + end, + dict = 'dead', + name = 'dead_a', + flag = 3, + }, + { + condition = function(value) + return + value and + not LocalPlayer.state.carriedBy and + not LocalPlayer.state.inStretcher and + not LocalPlayer.state.inBed and + cache.vehicle + end, + dict = 'veh@bus@passenger@common@idle_duck', + name = 'sit', + flag = 2, + } + }, +} diff --git a/config/states/escort.lua b/config/states/escort.lua new file mode 100644 index 0000000..6df7249 --- /dev/null +++ b/config/states/escort.lua @@ -0,0 +1,60 @@ +--- @type PlayerStateConfig +return { + id = 'escort', + label = 'Escort', + fields = { stateBag = 'escorting' }, + permanent = false, + value = { + default = nil, + }, + notification = { + up = { + id = 'state-escort-up-notif', + title = 'Escort', + description = 'You are escorting a player', + duration = 2000, + icon = 'user-friends', + iconColor = '#4CAF50', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = 1, + }, + down = { + id = 'state-escort-down-notif', + title = 'Escort', + description = 'You are no longer escorting a player', + duration = 2000, + icon = 'user-large-slash', + iconColor = '#C53030', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = -1, + }, + }, + disable = { + condition = function(value) + return value ~= nil and value + end, + keys = { + 25, 59, 140, 141, 142 + }, + emotes = true, + radio = true, + phone = true, + inventory = true, + }, + forcedAnimations = { + { + condition = function(value) + return value ~= nil and value + end, + dict = 'amb@code_human_wander_drinking_fat@beer@male@base', + name = 'static', + flag = 49, + } + }, +} diff --git a/config/states/escorted.lua b/config/states/escorted.lua new file mode 100644 index 0000000..00cb144 --- /dev/null +++ b/config/states/escorted.lua @@ -0,0 +1,54 @@ +--- @type PlayerStateConfig +return { + id = 'escorted', + label = 'Escorted', + fields = { stateBag = 'escortedBy' }, + permanent = false, + value = { + default = nil, + }, + notification = { + up = { + id = 'state-escorted-up-notif', + title = 'Escorted', + description = 'You are being escorted', + duration = 2000, + icon = 'user-friends', + iconColor = '#4CAF50', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = 1, + }, + down = { + id = 'state-escorted-down-notif', + title = 'Escorted', + description = 'You are no longer being escorted', + duration = 2000, + icon = 'user-large-slash', + iconColor = '#C53030', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = -1, + }, + }, + disable = { + condition = function(value) + return value + end, + keys = { + 0, 7, 20, 24, 25, 26, 29, 30, 32, 33, 34, 35, 44, + 45, 46, 47, 48, 49, 59, 74, 75, 77, 140, 141, 142, + 144, 145, 185, 199, 244, 251, 246, 303, 323, + }, + emotes = true, + radio = true, + phone = true, + inventory = true, + radialMenu = true, + target = true, + }, +} diff --git a/config/states/hunger.lua b/config/states/hunger.lua new file mode 100644 index 0000000..f2172c9 --- /dev/null +++ b/config/states/hunger.lua @@ -0,0 +1,48 @@ +--- @type PlayerStateConfig +return { + id = 'hunger', + label = 'Hunger', + fields = { stateBag = 'hunger', metadata = 'hunger' }, + permanent = true, + decay = { + interval = 60*1000, + value = -2, + }, + value = { + min = 0, + max = 100, + default = 100, + }, + notification = { + up = { + id = 'state-hunger-up-notif', + title = 'Hunger', + description = 'You relieved your hunger', + duration = 2000, + icon = 'drumstick-bite', + iconColor = '#4CAF50', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = 50, + }, + down = { + id = 'state-hunger-down-notif', + title = 'Hunger', + description = 'You feel hungry', + duration = 1000, + icon = 'bone', + iconColor = '#C53030', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = -10, + }, + }, + effectData = { + statusInterval = 60*1000, + decreaseHealthRange = {min = 5, max = 10}, + }, +} diff --git a/config/states/painkiller.lua b/config/states/painkiller.lua new file mode 100644 index 0000000..3c1adb3 --- /dev/null +++ b/config/states/painkiller.lua @@ -0,0 +1,21 @@ +--- @type PlayerStateConfig +return { + id = 'painkiller', + label = 'Pain Killer Effect', + fields = { stateBag = 'painkiller' }, + permanent = false, + value = { + min = 0, + max = 10, + default = 0, + }, + decay = { + interval = 30*1000, + value = -1, + }, + overdose = { + chance = 100, + checkInterval = 10*1000, + threshold = 8, + }, +} diff --git a/config/states/stress.lua b/config/states/stress.lua new file mode 100644 index 0000000..6898a93 --- /dev/null +++ b/config/states/stress.lua @@ -0,0 +1,69 @@ +--- @type PlayerStateConfig +return { + id = 'stress', + label = 'Stress', + fields = { stateBag = 'stress', metadata = 'stress' }, + permanent = true, + value = { + min = 0, + max = 100, + default = 0, + }, + notification = { + up = { + id = 'state-stress-up-notif', + title = 'Stress', + description = 'You feel stressed', + duration = 1000, + icon = 'brain', + iconColor = '#C53030', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = 2, + }, + down = { + id = 'state-stress-down-notif', + title = 'Stress', + description = 'You feel relaxed', + duration = 1000, + icon = 'brain', + iconColor = '#4CAF50', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = -2, + }, + }, + -- custom effect data + effectData = { + speedStressWhitlistedJobs = {'ambulance', 'civilprotection'}, + stressSpeed = 30, + speedStressRange = {min = 1, max = 3}, + speedStressInterval = 10000, + whitelistedWeapons = { + 'weapon_petrolcan', + 'weapon_hazardcan', + 'weapon_fireextinguisher', + }, + stressChance = 0.1, + weaponStressRange = {min = 1, max = 5}, + minForShaking = 50, + blurIntensity = { + [1] = {min = 50, max = 60, intensity = 1500}, + [2] = {min = 60, max = 70, intensity = 2000}, + [3] = {min = 70, max = 80, intensity = 2500}, + [4] = {min = 80, max = 90, intensity = 2700}, + [5] = {min = 90, max = 100, intensity = 3000}, + }, + effectInterval = { + [1] = {min = 50, max = 60, timeout = math.random(50000, 60000)}, + [2] = {min = 60, max = 70, timeout = math.random(40000, 50000)}, + [3] = {min = 70, max = 80, timeout = math.random(30000, 40000)}, + [4] = {min = 80, max = 90, timeout = math.random(20000, 30000)}, + [5] = {min = 90, max = 100, timeout = math.random(15000, 20000)}, + }, + }, +} diff --git a/config/states/thirst.lua b/config/states/thirst.lua new file mode 100644 index 0000000..13d4fec --- /dev/null +++ b/config/states/thirst.lua @@ -0,0 +1,48 @@ +--- @type PlayerStateConfig +return { + id = 'thirst', + label = 'Thirst', + fields = { stateBag = 'thirst', metadata = 'thirst' }, + permanent = true, + value = { + min = 0, + max = 100, + default = 100, + }, + decay = { + interval = 60*1000, + value = -2, + }, + notification = { + up = { + id = 'state-thirst-up-notif', + title = 'Thirst', + description = 'You relieved your thirst', + duration = 2000, + icon = 'wine-glass', + iconColor = '#4CAF50', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = 50, + }, + down = { + id = 'state-thirst-down-notif', + title = 'Thirst', + description = 'You feel thirsty', + duration = 1000, + icon = 'wine-glass-empty', + iconColor = '#C53030', + style = { + backgroundColor = '#141517', + color = '#FFFFFF', + }, + value = -10, + }, + }, + effectData = { + statusInterval = 5*1000, + decreaseHealthRange = {min = 5, max = 10}, + }, +} diff --git a/definitions.lua b/definitions.lua new file mode 100644 index 0000000..640f0b4 --- /dev/null +++ b/definitions.lua @@ -0,0 +1,133 @@ +------------ +-- config -- +------------ + +--- @class NotificationStyle The style of a notification +--- @field backgroundColor string The background color of the notification +--- @field color string The color of the notification + +--- @class NotificationData The data of a notification +--- @field id string The unique identifier of the notification (e.g. 'stress-up', 'stress-down', 'hunger-up', 'hunger-down', 'thirst-up', 'thirst-down', 'thc-up', 'thc-down', 'alcohol-up', 'alcohol-down') +--- @field title string The title of the notification +--- @field description string The description of the notification +--- @field duration number The duration of the notification in milliseconds +--- @field value number The value of the state change that trigger the notification +--- @field icon string The icon of the notification +--- @field iconColor string The color of the icon +--- @field style NotificationStyle The style of the notification + +--- @class NotificationConfig The configuration for a notification +--- @field up NotificationData The data of the notification when the state increase +--- @field down NotificationData The data of the notification when the state decrease + +--- @class PlayerStateValueConfig The value configuration of a player state +--- @field min? number The minimum value of the state +--- @field max? number The maximum value of the state +--- @field default any The default value of the state + +--- @class PlayerStateDecayConfig The decay configuration of a player state +--- @field interval number The interval of the decay in milliseconds +--- @field value number The value of the decay + +--- @class PlayerStateClearConfig The clear configuration of a player state +--- @field swimming boolean Whether the state is cleared when the player is swimming or not + +--- @class PlayerStateActionConfig The action configuration of a player state +--- @field chance number The chance of the action +--- @field threshold number The threshold of the action + +--- @class AnimationConfig +--- @field condition fun(value: any): boolean +--- @field dict string +--- @field name string +--- @field flag number +--- @field prop? PropConfig[] + +--- @class PropConfig +--- @field bone number +--- @field model string +--- @field pos vector3 +--- @field rot vector3 + +--- @class FieldsConfig +--- @field stateBag string +--- @field metadata? string + +--- @class DisableConfig +--- @field condition fun(value: any): boolean +--- @field keys? number[] +--- @field inventory? boolean +--- @field radialMenu? boolean +--- @field target? boolean +--- @field emotes? boolean +--- @field radio? boolean +--- @field phone? boolean + +--- @class PlayerStateConfig The configuration for a player state +--- @field id string The unique identifier of the state (e.g. 'stress', 'hunger', 'thirst', 'thc', 'alcohol') +--- @field label string The label of the state (e.g. 'Stress', 'Hunger', 'Thirst', 'THC', 'Alcohol') +--- @field fields FieldsConfig The fields to use for the state +--- @field permanent boolean Whether the state is permanent or not +--- @field value? PlayerStateValueConfig The value configuration of the state +--- @field decay? PlayerStateDecayConfig The decay configuration of the state +--- @field clear? PlayerStateClearConfig The clear configuration of the state +--- @field puke? PlayerStateActionConfig The puke configuration of the state +--- @field overdose? PlayerStateActionConfig The overdose configuration of the state +--- @field notification? NotificationConfig The configuration for the notification +--- @field disable? DisableConfig The configuration for the disable +--- @field forcedAnimations? AnimationConfig[] The animations of the state +--- @field effectData? any The custom effect data of the state + +------------- +-- modules -- +------------- + +--- @class AnimationState +--- @field animation AnimationConfig The current animation +--- @field props number[] Current prop handles + +--- @class CLAnimationManager +--- @field states table The states with their animations +--- @field currentAnimation AnimationState|nil The current animation state +--- @field props number[] The current prop handles +--- @field isLoopRunning boolean Whether the animation loop is running + +--- @class CLDisableManager +--- @field stateDisables table +--- @field loopRunning boolean +--- @field activeDisabledKeys table -- key -> count of states disabling it +--- @field totalDisabledKeys number + +--- @class CLPlayerState +--- @field id string +--- @field label string +--- @field value any +--- @field fields FieldsConfig +--- @field valueConfig PlayerStateValueConfig +--- @field isNumeric boolean +--- @field clearConfig? PlayerStateClearConfig +--- @field pukeConfig? PlayerStateActionConfig +--- @field overdoseConfig? PlayerStateActionConfig +--- @field notificationConfig? NotificationConfig +--- @field disableConfig? DisableConfig +--- @field animations? AnimationConfig[] +--- @field disableManager CLDisableManager +--- @field animationManager CLAnimationManager + +--- @class CLStateManager +--- @field disableManager CLDisableManager +--- @field animationManager CLAnimationManager +--- @field states table + +--- @class SVPlayerState +--- @field id string +--- @field label string +--- @field fields FieldsConfig +--- @field permanent boolean +--- @field valueConfig PlayerStateValueConfig +--- @field values table +--- @field isNumeric boolean +--- @field decayConfig? PlayerStateDecayConfig + +--- @class SVStateManager +--- @field states table diff --git a/fxmanifest.lua b/fxmanifest.lua new file mode 100644 index 0000000..49d6556 --- /dev/null +++ b/fxmanifest.lua @@ -0,0 +1,31 @@ +fx_version 'cerulean' +game 'gta5' + +author 'SaharaScripters' +description 'Player States' +version '1.0.0' + +shared_script '@ox_lib/init.lua' + +client_scripts { + '@qbx_core/modules/playerdata.lua', + 'client.lua', + 'client/*.lua', +} + +server_scripts { + 'server.lua' +} + +files { + 'config/states/*.lua', + 'config/shared.lua', + 'modules/**/client.lua', +} + +dependencies { + 'ox_lib', + 'qbx_core', +} + +lua54 'yes' diff --git a/modules/animationmanager/client.lua b/modules/animationmanager/client.lua new file mode 100644 index 0000000..823a3d3 --- /dev/null +++ b/modules/animationmanager/client.lua @@ -0,0 +1,137 @@ +--- @class CLAnimationManager +local CLAnimationManager = {} + +function CLAnimationManager:new() + --- @type CLAnimationManager + local instance = setmetatable({}, { __index = self }) + instance.states = {} + instance.currentAnimation = nil + instance.isLoopRunning = false + instance.props = {} + instance:ensureLoop() + return instance +end + +--- @param stateId string +--- @param value any +--- @param animations AnimationConfig[]|nil +function CLAnimationManager:updateState(stateId, value, animations) + if not animations then + self.states[stateId] = nil + else + self.states[stateId] = { + animations = animations, + value = value + } + end +end + +function CLAnimationManager:updateCurrentAnimation() + local newCurrentAnimation = nil + + for stateId, state in pairs(self.states) do + for _, animation in ipairs(state.animations) do + if animation.condition(state.value) then + newCurrentAnimation = { + stateId = stateId, + animation = animation + } + break + end + end + if newCurrentAnimation then break end + end + + if not self:isSameAnimation(self.currentAnimation, newCurrentAnimation) then + if self.currentAnimation then + self:clearProps() + ClearPedTasks(cache.ped) + end + self.currentAnimation = newCurrentAnimation + if self.currentAnimation then + self:createProps() + end + end +end + +function CLAnimationManager:createProps() + if not self.currentAnimation then return end + local animation = self.currentAnimation.animation + if animation.prop then + for _, propConfig in ipairs(animation.prop) do + local propHandle = self:createProp(propConfig) + if propHandle then + table.insert(self.props, propHandle) + end + end + end +end + +--- @param anim1 AnimationState|nil +--- @param anim2 AnimationState|nil +function CLAnimationManager:isSameAnimation(anim1, anim2) + if not anim1 and not anim2 then return true end + if not anim1 or not anim2 then return false end + local a1 = anim1.animation + local a2 = anim2.animation + return a1.dict == a2.dict and a1.name == a2.name +end + +function CLAnimationManager:ensureLoop() + if not self.isLoopRunning then + self.isLoopRunning = true + CreateThread(function() + while self.isLoopRunning do + self:updateCurrentAnimation() + local animation = self.currentAnimation?.animation + if animation then + if not IsEntityPlayingAnim(cache.ped, animation.dict, animation.name, animation.flag) then + lib.playAnim(cache.ped, animation.dict, animation.name, 8.0, -8.0, -1, animation.flag, 0, false, false, false) + end + end + Wait(1000) + end + self.isLoopRunning = false + end) + end +end + +--- @param propConfig PropConfig +--- @return number|nil propHandle +function CLAnimationManager:createProp(propConfig) + local modelHash = joaat(propConfig.model) + lib.requestModel(modelHash) + local coords = GetEntityCoords(cache.ped) + local prop = CreateObject(modelHash, coords.x, coords.y, coords.z, true, true, false) + local timeout = 1000 + while not DoesEntityExist(prop) and timeout > 0 do + Wait(100) + timeout = timeout - 100 + end + if DoesEntityExist(prop) then + AttachEntityToEntity( + prop, + cache.ped, + GetPedBoneIndex(cache.ped, propConfig.bone), + propConfig.pos.x, + propConfig.pos.y, + propConfig.pos.z, + propConfig.rot.x, + propConfig.rot.y, + propConfig.rot.z, + true, true, false, true, 1, true + ) + return prop + end + return nil +end + +function CLAnimationManager:clearProps() + if not self.props then return end + for _, propHandle in ipairs(self.props) do + DeleteEntity(propHandle) + end + self.props = {} +end + +return CLAnimationManager diff --git a/modules/disablemanager/client.lua b/modules/disablemanager/client.lua new file mode 100644 index 0000000..ee10fa6 --- /dev/null +++ b/modules/disablemanager/client.lua @@ -0,0 +1,166 @@ +--- @alias FeatureKey 'inventory' | 'radialMenu' | 'target' | 'emotes' | 'radio' | 'phone' + +--- @class CLDisableManager +local CLDisableManager = {} + +function CLDisableManager:new() + --- @type CLDisableManager + local instance = setmetatable({}, { __index = self }) + instance.stateDisables = {} + instance.loopRunning = false + instance.activeDisabledKeys = {} + instance.totalDisabledKeys = 0 + return instance +end + +--- @param feature FeatureKey +--- @return boolean +function CLDisableManager:isFeatureDisabled(feature) + for _, stateData in pairs(self.stateDisables) do + if stateData[feature] then + return true + end + end + return false +end + +--- @param key number +--- @return boolean +function CLDisableManager:isKeyDisabled(key) + for _, stateData in pairs(self.stateDisables) do + if stateData.keys and stateData.keys[key] then + return true + end + end + return false +end + +--- @param stateId string +function CLDisableManager:removeDisable(stateId) + local stateData = self.stateDisables[stateId] + if stateData then + if stateData.keys then + for key in pairs(stateData.keys) do + self.activeDisabledKeys[key] = self.activeDisabledKeys[key] - 1 + if self.activeDisabledKeys[key] == 0 then + self.activeDisabledKeys[key] = nil + self.totalDisabledKeys = self.totalDisabledKeys - 1 + end + end + end + + self.stateDisables[stateId] = nil + self:updateDisableStates() + end +end + +--- @param stateId string +--- @param config DisableConfig +function CLDisableManager:applyDisable(stateId, config) + self:removeDisable(stateId) + self.stateDisables[stateId] = {} + + if config.inventory then self.stateDisables[stateId].inventory = true end + if config.radialMenu then self.stateDisables[stateId].radialMenu = true end + if config.target then self.stateDisables[stateId].target = true end + if config.emotes then self.stateDisables[stateId].emotes = true end + if config.radio then self.stateDisables[stateId].radio = true end + if config.phone then self.stateDisables[stateId].phone = true end + + if config.keys then + self.stateDisables[stateId].keys = {} + for _, key in ipairs(config.keys) do + self.stateDisables[stateId].keys[key] = true + self.activeDisabledKeys[key] = (self.activeDisabledKeys[key] or 0) + 1 + if self.activeDisabledKeys[key] == 1 then + self.totalDisabledKeys = self.totalDisabledKeys + 1 + end + end + end + + self:updateDisableStates() + self:ensureKeyLoop() +end + +function CLDisableManager:updateDisableStates() + --- @type FeatureKey[] + local features = {'inventory', 'radialMenu', 'target', 'emotes', 'radio', 'phone'} + for _, feature in ipairs(features) do + local shouldBeDisabled = self:isFeatureDisabled(feature) + self:toggleFeature(feature, shouldBeDisabled) + end +end + +function CLDisableManager:ensureKeyLoop() + if not self.loopRunning and self.totalDisabledKeys > 0 then + self.loopRunning = true + CreateThread(function() + while self.totalDisabledKeys > 0 do + for key in pairs(self.activeDisabledKeys) do + DisableControlAction(0, key, true) + end + Wait(0) + end + self.loopRunning = false + end) + end +end + +--- @param feature FeatureKey +--- @param shouldDisable boolean +function CLDisableManager:toggleFeature(feature, shouldDisable) + if feature == 'inventory' then + self:toggleInventory(shouldDisable) + elseif feature == 'radialMenu' then + self:toggleRadialMenu(shouldDisable) + elseif feature == 'target' then + self:toggleTarget(shouldDisable) + elseif feature == 'emotes' then + self:toggleEmotes(shouldDisable) + elseif feature == 'radio' then + self:toggleRadio(shouldDisable) + elseif feature == 'phone' then + self:togglePhone(shouldDisable) + end +end + +--- @param shouldDisable boolean +function CLDisableManager:toggleInventory(shouldDisable) + if shouldDisable then + if LocalPlayer.state.invOpen then + exports.ox_inventory:closeInventory() + end + end + LocalPlayer.state.invBusy = shouldDisable +end + +--- @param shouldDisable boolean +function CLDisableManager:toggleRadialMenu(shouldDisable) + lib.disableRadial(shouldDisable) +end + +--- @param shouldDisable boolean +function CLDisableManager:toggleTarget(shouldDisable) + exports.ox_target:disableTargeting(shouldDisable) +end + +--- @param shouldDisable boolean +function CLDisableManager:toggleEmotes(shouldDisable) + exports.scully_emotemenu:setLimitation(shouldDisable) +end + +--- @param shouldDisable boolean +function CLDisableManager:toggleRadio(shouldDisable) + -- if shouldDisable then + -- -- leave radio channel + -- -- exports.qbx_radio:leaveRadio() + -- end + LocalPlayer.state.disableRadio = shouldDisable and 1 or 0 +end + +--- @param shouldDisable boolean +function CLDisableManager:togglePhone(shouldDisable) + exports.npwd:setPhoneDisabled(shouldDisable) +end + +return CLDisableManager diff --git a/modules/playerstate/client.lua b/modules/playerstate/client.lua new file mode 100644 index 0000000..02d67fe --- /dev/null +++ b/modules/playerstate/client.lua @@ -0,0 +1,200 @@ +--- @class CLPlayerState +local CLPlayerState = {} + +--- @param options PlayerStateConfig +--- @param disableManager CLDisableManager +--- @param animationManager CLAnimationManager +--- @return CLPlayerState instance +function CLPlayerState:new(options, disableManager, animationManager) + local instance = setmetatable({}, { __index = self }) + instance.id = options.id + instance.label = options.label + instance.value = nil + instance.valueConfig = options.value + instance.isNumeric = options.value.min and options.value.max and options.value.default and true or false + instance.fields = options.fields + instance.clearConfig = options.clear + instance.pukeConfig = options.puke + instance.overdoseConfig = options.overdose + instance.notificationConfig = options.notification + instance.disableConfig = options.disable + instance.animations = options.forcedAnimations + + instance.disableManager = disableManager + instance.animationManager = animationManager + return instance +end + +--- @param value number +--- @return number +function CLPlayerState:adaptValue(value) + if value < self.valueConfig.min then + return self.valueConfig.min + elseif value > self.valueConfig.max then + return self.valueConfig.max + end + return value +end + +function CLPlayerState:addStateBagChangeHandler() + AddStateBagChangeHandler(self.fields.stateBag, ('player:%s'):format(cache.serverId), function(_bagName, _key, value, _reserved, _replicated) + SetTimeout(100, function() + if self.isNumeric then + value = tonumber(value) or self.valueConfig.default + value = self:adaptValue(value) + end + self:playerStateChanged(value) + end) + end) +end + +--- @param newValue any +function CLPlayerState:playerStateChanged(newValue) + local oldValue = self.value + self.value = newValue + if self.isNumeric then + if not newValue or not oldValue then return end + end + + if self.disableConfig then + local shouldDisable = self.disableConfig.condition(newValue) + if shouldDisable then + self.disableManager:applyDisable(self.id, self.disableConfig) + else + self.disableManager:removeDisable(self.id) + end + end + + if self.animations and #self.animations > 0 then + self.animationManager:updateState(self.id, newValue, self.animations) + end + + self:showStateChangedNotification(oldValue, newValue) +end + +--- @param oldValue any +--- @param newValue any +function CLPlayerState:showStateChangedNotification(oldValue, newValue) + if not self.notificationConfig then return end + local notifyData = nil + if self.isNumeric then + local diff = newValue - oldValue + if diff > 0 and self.notificationConfig.up.value and diff >= self.notificationConfig.up.value then + notifyData = self.notificationConfig.up + elseif diff < 0 and self.notificationConfig.down.value and diff <= self.notificationConfig.down.value then + notifyData = self.notificationConfig.down + end + else + if not oldValue and newValue then + notifyData = self.notificationConfig.up + elseif oldValue and not newValue then + notifyData = self.notificationConfig.down + end + end + if notifyData then + lib.notify(notifyData) + end +end + +function CLPlayerState:clearState() + return lib.callback.await('qbx_playerstates:server:clearState', false, self.id) +end + +function CLPlayerState:checkClear() + if not self.clearConfig or not self.clearConfig.swimming then return end + if IsPedSwimming(cache.ped) then + if self.value ~= self.valueConfig.default and self:clearState() then + lib.notify({ + title = self.label, + description = 'Swimming cleared the state ' .. self.label, + duration = 3000, + type = 'success', + }) + end + end +end + +function CLPlayerState:overDose() + lib.notify({ + title = self.label, + description = 'You feel dizzy and fall to the ground', + duration = 3000, + type = 'error', + }) + local forwardVector = GetEntityForwardVector(cache.ped) + SetPedToRagdollWithFall(cache.ped, 1750, 1750, 1, forwardVector.x, forwardVector.y, forwardVector.z, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) + Wait(1000) + DoScreenFadeOut(200) + Wait(1000) + DoScreenFadeIn(200) + Wait(2000) + lib.notify({ + title = self.label, + description = 'You have overdosed and died from the effects of ' .. self.label, + duration = 3000, + type = 'error', + }) + SetEntityHealth(cache.ped, 0) +end + +function CLPlayerState:checkOverDose() + if not self.overdoseConfig then return false end + if not IsPedOnFoot(cache.ped) then return false end + if not self.value or self.value < self.overdoseConfig.threshold then return false end + local playerOverdoseChance = math.random(0, 100) + if playerOverdoseChance > self.overdoseConfig.chance then return false end + self:overDose() + return true +end + +function CLPlayerState:puke() + lib.notify({ + title = self.label, + description = 'You feel sick and will puke', + duration = 3000, + type = 'error', + }) + Wait(1000) + DoScreenFadeOut(200) + Wait(1000) + DoScreenFadeIn(200) + Wait(1000) + lib.progressCircle({ + duration = 10000, + label = 'Puking', + useWhileDead = false, + allowRagdoll = false, + allowSwimming = false, + allowCuffed = false, + allowFalling = false, + canCancel = false, + anim = { + dict = 'anim@scripted@freemode@throw_up_toilet@male@', + clip = 'vomit', + flag = 1, + }, + }) +end + +function CLPlayerState:checkPuke() + if not self.pukeConfig then return false end + if not IsPedOnFoot(cache.ped) then return false end + if not self.value or self.value < self.pukeConfig.threshold then return false end + local playerPukeChance = math.random(0, 100) + if playerPukeChance > self.pukeConfig.chance then return false end + self:puke() + return true +end + +function CLPlayerState:checkState() + self:checkClear() + if self:checkPuke() then return true end + if self:checkOverDose() then return true end + return false +end + +function CLPlayerState:init() + self:addStateBagChangeHandler() +end + +return CLPlayerState diff --git a/modules/playerstate/server.lua b/modules/playerstate/server.lua new file mode 100644 index 0000000..288a6b9 --- /dev/null +++ b/modules/playerstate/server.lua @@ -0,0 +1,112 @@ +--- @class SVPlayerState +local SVPlayerState = {} + +--- @param options PlayerStateConfig +--- @return SVPlayerState instance +function SVPlayerState:new(options) + local instance = setmetatable({}, { __index = self }) + instance.id = options.id + instance.label = options.label + instance.permanent = options.permanent + instance.valueConfig = options.value + instance.fields = options.fields + instance.values = {} + instance.isNumeric = options.value.min and options.value.max and options.value.default and true or false + instance.decayConfig = options.decay + return instance +end + +--- @param value any +--- @return number +function SVPlayerState:validateValue(value) + if not value or type(value) ~= 'number' then return self.valueConfig.default end + if value < self.valueConfig.min then + return self.valueConfig.min + elseif value > self.valueConfig.max then + return self.valueConfig.max + end + return value +end + +--- @param playerSrc number +--- @param value any +--- @return boolean +function SVPlayerState:setPlayerState(playerSrc, value) + local player = exports.qbx_core:GetPlayer(playerSrc) + if not player then return false end + local playerEntity = Player(playerSrc) + if not playerEntity then return false end + local newValue = self.isNumeric and self:validateValue(value) or value + self.values[playerSrc] = newValue + if playerEntity.state[self.fields.stateBag] ~= newValue then + playerEntity.state:set(self.fields.stateBag, newValue, true) + end + if self.fields.metadata and newValue ~= player.Functions.GetMetaData(self.fields.metadata) then + player.Functions.SetMetaData(self.fields.metadata, newValue) + end + return true +end + +--- @param playerSrc number +--- @param value number +--- @return boolean +function SVPlayerState:addToPlayerState(playerSrc, value) + if not self.isNumeric then return false end + local playerEntity = Player(playerSrc) + if not playerEntity then return false end + local currentStateValue = playerEntity.state[self.fields.stateBag] or self.valueConfig.default + local newValue = self:validateValue(currentStateValue + value) + return self:setPlayerState(playerSrc, newValue) +end + +--- @param playerSrc number +--- @return boolean +function SVPlayerState:clearPlayerState(playerSrc) + return self:setPlayerState(playerSrc, self.valueConfig.default) +end + +--- @param playerSrc number +function SVPlayerState:destroyPlayerState(playerSrc) + local playerEntity = Player(playerSrc) + if not playerEntity then return end + self.values[playerSrc] = nil + playerEntity.state:set(self.fields.stateBag, nil, true) +end + +--- @param playerSrc number +function SVPlayerState:initPlayerState(playerSrc) + local player = exports.qbx_core:GetPlayer(playerSrc) + if not player then return end + local playerEntity = Player(playerSrc) + if not playerEntity then return end + local stateValue = self.valueConfig.default + if self.fields.metadata then + if self.permanent then + local metadataValue = player.Functions.GetMetaData(self.fields.metadata) + stateValue = self.isNumeric and self:validateValue(metadataValue) or metadataValue + end + end + self:setPlayerState(playerSrc, stateValue) +end + +--- @param playerSrc number +--- @param newValue number +function SVPlayerState:correctValue(playerSrc, newValue) + if self.values[playerSrc] == newValue then return end + self:setPlayerState(playerSrc, newValue) +end + +function SVPlayerState:startDecayLoop() + if not self.decayConfig then return end + CreateThread(function() + while true do + Wait(self.decayConfig.interval) + local players = exports.qbx_core:GetQBPlayers() + for playerSrc, _ in pairs(players) do + self:addToPlayerState(playerSrc, self.decayConfig.value) + end + end + end) +end + +return SVPlayerState diff --git a/modules/statemanager/client.lua b/modules/statemanager/client.lua new file mode 100644 index 0000000..7e80764 --- /dev/null +++ b/modules/statemanager/client.lua @@ -0,0 +1,61 @@ +--- @class CLPlayerState +local CLPlayerState = require 'modules.playerstate.client' +--- @class CLDisableManager +local CLDisableManager = require 'modules.disablemanager.client' +--- @class CLAnimationManager +local CLAnimationManager = require 'modules.animationmanager.client' + +local sharedConf = require 'config.shared' +local LOOP_INTERVAL = 10000 +local EFFECT_TIMEOUT = 30000 + +--- @class CLStateManager +local CLStateManager = {} + +--- @return CLStateManager instance +function CLStateManager:new() + local instance = setmetatable({}, { __index = self }) + instance.disableManager = CLDisableManager:new() + instance.animationManager = CLAnimationManager:new() + instance.states = {} + return instance +end + +--- @param stateConfig PlayerStateConfig +function CLStateManager:initState(stateConfig) + if self.states[stateConfig.id] then + print("State with id " .. stateConfig.id .. " already exists") + return + end + self.states[stateConfig.id] = CLPlayerState:new(stateConfig, self.disableManager, self.animationManager) + self.states[stateConfig.id]:init() +end + +function CLStateManager:startCheckLoop() + CreateThread(function() + while true do + Wait(LOOP_INTERVAL) + if LocalPlayer.state.isLoggedIn and not LocalPlayer.state.isDead and not LocalPlayer.state.onEffect then + for _, state in pairs(self.states) do + local onEffect = state:checkState() + if onEffect then + LocalPlayer.state.onEffect = true + SetTimeout(EFFECT_TIMEOUT, function() + LocalPlayer.state.onEffect = false + end) + break + end + end + end + end + end) +end + +function CLStateManager:init() + for _, stateConf in pairs(sharedConf) do + self:initState(stateConf) + end + self:startCheckLoop() +end + +return CLStateManager diff --git a/modules/statemanager/server.lua b/modules/statemanager/server.lua new file mode 100644 index 0000000..870d195 --- /dev/null +++ b/modules/statemanager/server.lua @@ -0,0 +1,139 @@ +--- @class SVPlayerState +local SVPlayerState = require 'modules.playerstate.server' + +local sharedConf = require 'config.shared' + +--- @class SVStateManager +local SVStateManager = {} + +--- @return SVStateManager instance +function SVStateManager:new() + local instance = setmetatable({}, { __index = self }) + instance.states = {} + return instance +end + +--- @param stateConfig PlayerStateConfig +function SVStateManager:addState(stateConfig) + if self.states[stateConfig.id] then + print("State with id " .. stateConfig.id .. " already exists") + return + end + self.states[stateConfig.id] = SVPlayerState:new(stateConfig) +end + +function SVStateManager:startDecayLoops() + for _, state in pairs(self.states) do + state:startDecayLoop() + end +end + +function SVStateManager:init() + for _, stateConf in pairs(sharedConf) do + self:addState(stateConf) + end + self:startDecayLoops() +end + +--- @param src any +--- @param state any +--- @param value any +--- @return boolean +function SVStateManager:addToState(src, state, value) + if not src or not state or not value then return false end + if type(src) ~= 'number' or type(state) ~= 'string' or type(value) ~= 'number' then return false end + local stateInstance = self.states[state] + if not stateInstance then return false end + return stateInstance:addToPlayerState(src, value) +end + +--- @param src any +--- @param state any +--- @return boolean +function SVStateManager:clearState(src, state) + if not src or not state then return false end + if type(src) ~= 'number' or type(state) ~= 'string' then return false end + local stateInstance = self.states[state] + if not stateInstance then return false end + return stateInstance:clearPlayerState(src) +end + +--- @param src any +--- @return boolean +function SVStateManager:clearAllStates(src) + if not src then return false end + if type(src) ~= 'number' then return false end + for _, state in pairs(self.states) do + state:clearPlayerState(src) + end + return true +end + +--- @param src any +function SVStateManager:initPlayerStates(src) + if not src then return end + if type(src) ~= 'number' then return end + for _, state in pairs(self.states) do + state:initPlayerState(src) + end +end + +--- @param src any +function SVStateManager:resetPlayerStates(src) + if not src then return end + if type(src) ~= 'number' then return end + for _, state in pairs(self.states) do + state:destroyPlayerState(src) + end +end + +function SVStateManager:initAllPlayersStates() + local players = exports.qbx_core:GetQBPlayers() + for playerSrc, _ in pairs(players) do + self:initPlayerStates(playerSrc) + end +end + +--- @param src any +--- @param state any +--- @param value any +--- @return boolean +function SVStateManager:setState(src, state, value) + if not src or not state then return false end + if type(src) ~= 'number' or type(state) ~= 'string' then return false end + local stateInstance = self.states[state] + if not stateInstance then return false end + return stateInstance:setPlayerState(src, value) +end + +--- @param metadataKey string +--- @return string? +function SVStateManager:getStateIdByMetadataKey(metadataKey) + for stateId, state in pairs(self.states) do + if state.fields.metadata == metadataKey then + return stateId + end + end +end + +--- @param stateBagKey string +--- @return string? +function SVStateManager:getStateIdByStateBagKey(stateBagKey) + for stateId, state in pairs(self.states) do + if state.fields.stateBag == stateBagKey then + return stateId + end + end +end + +--- @param key any +--- @param value any +--- @param src any +function SVStateManager:correctStateValue(key, value, src) + if not key or not src then return end + local stateInstance = self.states[key] + if not stateInstance then return end + stateInstance:correctValue(src, value) +end + +return SVStateManager diff --git a/server.lua b/server.lua new file mode 100644 index 0000000..6950dd7 --- /dev/null +++ b/server.lua @@ -0,0 +1,61 @@ +--- @class SVStateManager +local SVStateManager = require 'modules.statemanager.server' + +local stateManager = SVStateManager:new() +stateManager:init() + +lib.callback.register('qbx_playerstates:server:addToState', function(source, state, value) + local src = source + return stateManager:addToState(src, state, value) +end) + +lib.callback.register('qbx_playerstates:server:clearState', function(source, state) + local src = source + return stateManager:clearState(src, state) +end) + +lib.callback.register('qbx_playerstates:server:clearAllStates', function(source) + local src = source + return stateManager:clearAllStates(src) +end) + +RegisterNetEvent('qbx_core:server:onSetMetaData', function(key, _, newValue, source) + if not key or not source then return end + local playerSrc = source + local stateId = stateManager:getStateIdByMetadataKey(key) + if not stateId then return end + stateManager:correctStateValue(stateId, newValue, playerSrc) +end) + +AddStateBagChangeHandler('', '', function(bagName, key, value) + if not bagName or not key then return end + local playerSrc = GetPlayerFromStateBagName(bagName) + if not playerSrc then return end + local stateId = stateManager:getStateIdByStateBagKey(bagName) + if not stateId then return end + stateManager:correctStateValue(stateId, value, playerSrc) +end) + +RegisterNetEvent("QBCore:Server:OnPlayerLoaded", function () + local src = source + stateManager:initPlayerStates(src) +end) + +AddEventHandler("QBCore:Server:OnPlayerUnload", function () + local src = source + stateManager:resetPlayerStates(src) +end) + +AddEventHandler('onResourceStart', function(resourceName) + if resourceName == GetCurrentResourceName() then + stateManager:initAllPlayersStates() + end +end) + +exports('SetPlayerState', function(src, state, value) + return stateManager:setState(src, state, value) +end) + +exports('AddToPlayerState', function(src, state, value) + return stateManager:addToState(src, state, value) +end)