local ox_target = exports.ox_target local ox_inventory = exports.ox_inventory local spawnedSafes = {} -- Debug helper local function dbg(...) if Config and Config.Debug then print('[0ixb-stashes CLIENT]', ...) end end ------------------------------------------------------- -- CHECAR COLISÃO ------------------------------------------------------- local function isPlaceable(coords, radius) local ped = PlayerPedId() for _, obj in ipairs(GetGamePool('CObject')) do if DoesEntityExist(obj) and #(GetEntityCoords(obj) - coords) < radius then return false end end for _, veh in ipairs(GetGamePool('CVehicle')) do if DoesEntityExist(veh) and #(GetEntityCoords(veh) - coords) < radius then return false end end for _, p in ipairs(GetGamePool('CPed')) do if p ~= ped and DoesEntityExist(p) and #(GetEntityCoords(p) - coords) < radius then return false end end return true end ------------------------------------------------------- -- SPAWN SAFE ------------------------------------------------------- RegisterNetEvent('safe:spawn', function(safeData) if not safeData or not safeData.type or not Config.SafeTypes[safeData.type] then dbg('safe:spawn invalid safeData', safeData) return end local model = Config.SafeTypes[safeData.type].prop RequestModel(model) while not HasModelLoaded(model) do Wait(0) end local obj = CreateObject( model, safeData.coords.x, safeData.coords.y, safeData.coords.z, false, false, false ) -------------------------------------------------------------------- -- APLICAR ROTAÇÃO -- PRIORIDADE: -- 1) rx, ry, rz (se existir) -- 2) heading antigo (fallback) -------------------------------------------------------------------- if safeData.coords.rx and safeData.coords.ry and safeData.coords.rz then -- rotação completa (verdadeira 3D) SetEntityRotation(obj, safeData.coords.rx, safeData.coords.ry, safeData.coords.rz, 2, true) else -- fallback (como antes) SetEntityHeading(obj, safeData.coords.h or 0.0) end FreezeEntityPosition(obj, true) spawnedSafes[safeData.id] = obj dbg('Spawning safe id', safeData.id, 'type', safeData.type, 'entity', obj) -------------------------------------------------------------------- -- TARGET (mantido exatamente como estava) -------------------------------------------------------------------- ox_target:addLocalEntity(obj, { { label = "» Abrir SafeBox", icon = "fa-solid fa-lock", onSelect = function() dbg('Player clicou Abrir Safe', safeData.id) local pin = lib.inputDialog('PIN do Safe', { 'Digite o PIN' }) if pin and pin[1] then dbg('Player digitou PIN', pin[1]) TriggerServerEvent('safe:open', safeData.id, tostring(pin[1])) else dbg('Player cancelou input do PIN') end end }, { label = "» Eliminar SafeBox", icon = "fa-solid fa-triangle-exclamation", onSelect = function() dbg('Player pediu Mostrar Alerta', safeData.id) local result = lib.alertDialog({ header = 'CONFIRMAR REMOÇÃO DO SAFE', content = 'Antes de prosseguir, certifique-se de que o safe está totalmente vazio.', centered = true, cancel = true, size = 'lg' }) if result == 'confirm' then TriggerServerEvent('safe:pickup', safeData.id) else lib.notify({ type = 'inform', description = 'Operação cancelada.' }) end end } }) end) ------------------------------------------------------- -- DESPAWN SAFE ------------------------------------------------------- RegisterNetEvent('safe:despawn', function(id) local ent = spawnedSafes[id] if ent and DoesEntityExist(ent) then pcall(function() ox_target:removeLocalEntity(ent) end) FreezeEntityPosition(ent, false) DeleteObject(ent) end spawnedSafes[id] = nil end) ------------------------------------------------------- -- ABRIR INVENTÁRIO ------------------------------------------------------- RegisterNetEvent('safe:openInventory', function(data) if not data or not data.id then return end ox_inventory:openInventory('stash', { id = "safe_" .. tostring(data.id), slots = data.slots, weight = data.weight }) end) ------------------------------------------------------- -- FUNÇÕES DE CAMERA ------------------------------------------------------- local function RotToDir(rot) local z = math.rad(rot.z) local x = math.rad(rot.x) local num = math.abs(math.cos(x)) return vector3(-math.sin(z) * num, math.cos(z) * num, math.sin(x)) end local function RayCastFromCam(distance) local camRot = GetGameplayCamRot(2) local camCoord = GetGameplayCamCoord() local dir = RotToDir(camRot) local dest = camCoord + dir * distance local ray = StartShapeTestRay(camCoord.x, camCoord.y, camCoord.z, dest.x, dest.y, dest.z, -1, -1, 1) local _, hit, endCoords, surfaceNormal, entity = GetShapeTestResult(ray) return hit, endCoords, surfaceNormal, entity, camCoord, dir end ------------------------------------------------------- -- PLACEMENT / GIZMO ------------------------------------------------------- RegisterNetEvent('safe:use', function(type) if not Config.SafeTypes[type] then exports.ox_lib:notify({ type='error', description = 'Tipo de safe inválido.' }) return end local ped = PlayerPedId() local model = Config.SafeTypes[type].prop RequestModel(model) while not HasModelLoaded(model) do Wait(0) end local startPos = GetOffsetFromEntityInWorldCoords(ped, 0.0, 1.5, 0.0) local startHeading = GetEntityHeading(ped) local obj = CreateObject(model, startPos.x, startPos.y, startPos.z, false, false, false) SetEntityHeading(obj, startHeading) FreezeEntityPosition(obj, true) SetNuiFocus(false, false) Wait(50) -- abrir gizmo local ok, result = pcall(function() return exports.object_gizmo:useGizmo(obj) end) if not ok or not result then exports.ox_lib:notify({ type='error', description = 'Erro ao abrir gizmo.' }) if DoesEntityExist(obj) then DeleteObject(obj) end return end -- posição final local pos = result.position or GetEntityCoords(obj) -- rotação TOTAL local rot = result.rotation or GetEntityRotation(obj, 2) local heading = result.heading or rot.z ------------------------------------------------------- -- PEDIR PIN ------------------------------------------------------- SetNuiFocus(false, false) local pin = exports.ox_lib:inputDialog('PIN do Safe', {'Digite um PIN'}) SetNuiFocus(false, false) if not pin or not pin[1] then SetNuiFocus(false, false) SetNuiFocusKeepInput(false) SetCursorLocation(0.5, 0.5) exports.ox_lib:notify({ type='error', description = 'Você cancelou a operação.' }) if DoesEntityExist(obj) then DeleteObject(obj) end return end -- ENVIAR POSIÇÃO + ROTAÇÃO COMPLETA PARA O SERVIDOR TriggerServerEvent('safe:place', type, { x = pos.x, y = pos.y, z = pos.z, h = heading, rx = rot.x, ry = rot.y, rz = rot.z }, tostring(pin[1])) if DoesEntityExist(obj) then DeleteObject(obj) end end) ------------------------------------------------------- -- COMANDO DEBUG ------------------------------------------------------- RegisterCommand('colocarsafe', function(_, args) local typ = args[1] if not typ or not Config.SafeTypes[typ] then exports.ox_lib:notify({ type='error', description = 'Uso: /colocarsafe [small|medium|large]' }) return end TriggerEvent('safe:use', typ) end) ------------------------------------------------------- -- ITEM USO ------------------------------------------------------- for type, data in pairs(Config.SafeTypes) do if data.item then pcall(function() ox_inventory:useItem(data.item, function() dbg('Player usou item', data.item) TriggerEvent('safe:use', type) end) end) end end ------------------------------------------------------- -- CLEANUP ------------------------------------------------------- Citizen.CreateThread(function() while true do Wait(1000) for id, ent in pairs(spawnedSafes) do if ent and not DoesEntityExist(ent) then spawnedSafes[id] = nil end end end end) AddEventHandler('onClientResourceStart', function(resourceName) if resourceName ~= GetCurrentResourceName() then return end TriggerServerEvent('safe:requestSync') end)