--[[ require ]] local Utils = require 'modules.utils.client' local Target = require 'modules.target.client' --[[ state ]] Hack = { camId = nil, } local onAtmHack = false local currentAtmCameraKey = 1 local isAimBusy = false local aimOnHackable = false local foundEntityByShapeTest = nil local atmParticles = {} local ownedCashPiles = {} local selectedAtmEntity = nil --[[ functions ]] local function isAtmModel(model) for _, atmModel in pairs(Config.AtmModels) do if model == atmModel then return true end end return false end local function deleteOwnedCashPile(entity) for key, value in pairs(ownedCashPiles) do if value == entity then if DoesEntityExist(value) then SetEntityAsMissionEntity(value, true, true) DeleteEntity(value) end table.remove(ownedCashPiles, key) return end end end local function createMoneySprayEffect(entity) local coords = GetOffsetFromEntityInWorldCoords(entity, 0.0, 0.0, 1.0) local ptFxName = 'scr_xs_celebration' local rotX, rotY, rotZ = table.unpack(GetEntityRotation(entity, 2)) local atmParticles = {} for i = 1, 10 do lib.requestNamedPtfxAsset(ptFxName) UseParticleFxAssetNextCall(ptFxName) local randomRotX = 60.0 local randomRotY = rotY local randomRotZ = rotZ local particle = StartParticleFxLoopedAtCoord( 'scr_xs_money_rain', coords.x, coords.y, coords.z, randomRotX, randomRotY, randomRotZ, 1.0, false, false, false, false ) table.insert(atmParticles, particle) RemoveNamedPtfxAsset(ptFxName) end local particleKey = #atmParticles + 1 atmParticles[particleKey] = atmParticles Citizen.SetTimeout(10000, function() for _, particle in ipairs(atmParticles[particleKey]) do StopParticleFxLooped(particle, 0) end atmParticles[particleKey] = nil end) end local function createMoneyObject(entity) local coords = GetOffsetFromEntityInWorldCoords(entity, 0.0, -.5, 1.0) local rotation = GetEntityRotation(entity) local model = 'bkr_prop_bkr_cashpile_05' local objectCashPile = Utils.CreateObject(model, coords, rotation, true, true, false) table.insert(ownedCashPiles, objectCashPile) lib.waitFor(function() return PlaceObjectOnGroundProperly(objectCashPile) end, nil, 1000) local zoneKey = string.format('atmrobbery_hack_pickup_money_%d', #ownedCashPiles) Target.addBoxZone(zoneKey, { coords = GetEntityCoords(objectCashPile), size = vector3(1.75, 1.75, 1.75), debug = false, options = { { icon = 'fa-solid fa-hand-fist', label = locale('collect'), distance = 1.5, pile = objectCashPile, onSelect = function(data) local playerPedId = cache.ped Target.removeZone(zoneKey) lib.playAnim(playerPedId, 'pickup_object', 'pickup_low', nil, nil, 1000) Citizen.Wait(1000) ClearPedTasks(playerPedId) deleteOwnedCashPile(objectCashPile) lib.callback.await(_e('server:hacking:collectAtmCashPile'), false) end } } }) end local function RotationToDirection(rotation) local radZ = math.rad(rotation.z) local radX = math.rad(rotation.x) local num = math.abs(math.cos(radX)) return vector3( -math.sin(radZ) * num, math.cos(radZ) * num, math.sin(radX) ) end local function IsPointInFrontOfCamera(camPos, forwardVec, point) local pointDirection = (point - camPos) local dotProduct = forwardVec.x * pointDirection.x + forwardVec.y * pointDirection.y + forwardVec.z * pointDirection.z return dotProduct > 0 end local function FindHackableObjectFromCamera() local hit, entityHit = lib.raycast.fromCamera(16, 4, 15.0) if not hit or not DoesEntityExist(entityHit) then return nil end local entityModel = GetEntityModel(entityHit) local entityPos = GetEntityCoords(entityHit) if not isAtmModel(entityModel) then return nil end return entityHit end local function DisableDisplayControlActions() DisableControlAction(0, 263, true) -- disable melee DisableControlAction(0, 264, true) -- disable melee DisableControlAction(0, 257, true) -- disable melee DisableControlAction(0, 140, true) -- disable melee DisableControlAction(0, 141, true) -- disable melee DisableControlAction(0, 142, true) -- disable melee DisableControlAction(0, 143, true) -- disable melee DisableControlAction(0, 177, true) -- disable escape DisableControlAction(0, 200, true) -- disable escape DisableControlAction(0, 202, true) -- disable escape DisableControlAction(0, 322, true) -- disable escape DisableControlAction(0, 245, true) -- disable chat if isAimBusy then DisableAllControlActions(0) end end function deleteHackCam() if DoesCamExist(Hack.camId) then DestroyCam(Hack.camId) ClearFocus() RenderScriptCams(false, false, 0, false, false) ClearTimecycleModifier() ClearExtraTimecycleModifier() end client.setBusy(false, 'hack.clear') Hack.camId = nil end function CleanUp() deleteHackCam() client.SendReactMessage('ui:setVisible', false) if aimOnHackable then client.SendReactMessage('ui:setAimOnHackable', false) SetEntityDrawOutline(aimOnHackable, false) end Utils.ToggleHud(true) client.removeTabletFromPlayer() aimOnHackable = false onAtmHack = false selectedAtmEntity = nil end function CreateAtmCamera(cameraData) local cam = CreateCamWithParams('DEFAULT_SCRIPTED_CAMERA', cameraData.coords.x, cameraData.coords.y, cameraData.coords.z, cameraData.rot.x, cameraData.rot.y, cameraData.rot.z, 70.0) return cam end function ChangeCamera(cam, id) ClearFocus() local camCoords = Config.AtmCameras[id].coords local camRot = Config.AtmCameras[id].rot SetCamCoord(cam, camCoords.x, camCoords.y, camCoords.z) SetCamRot(cam, camRot.x, camRot.y, camRot.z, 2) SetFocusPosAndVel(camCoords.x, camCoords.y, camCoords.z, 0.0, 0.0, 0.0) end function CheckCamRotationInput(cam) local rightAxisX = GetDisabledControlNormal(0, 220) local rightAxisY = GetDisabledControlNormal(0, 221) if rightAxisX ~= 0.0 or rightAxisY ~= 0.0 then local rotation = GetCamRot(cam, 2) local new_z = rotation.z + rightAxisX * -1.0 * 2.0 * (4.0 + 0.1) local new_x = math.max(-60.0, math.min(60.0, rotation.x + rightAxisY * -1.0 * 2.0 * (4.0 + 0.1))) SetCamRot(cam, new_x, 0.0, new_z, 2) end end function HandleControls(cam) DisableAllControlActions(0) HideHudAndRadarThisFrame() CheckCamRotationInput(cam) if IsDisabledControlJustPressed(0, 34) then currentAtmCameraKey = (currentAtmCameraKey - 2) % #Config.AtmCameras + 1 ChangeCamera(cam, currentAtmCameraKey) elseif IsDisabledControlJustPressed(0, 35) then currentAtmCameraKey = currentAtmCameraKey % #Config.AtmCameras + 1 ChangeCamera(cam, currentAtmCameraKey) end if IsDisabledControlPressed(0, 97) and GetCamFov(cam) < 100 then SetCamFov(cam, GetCamFov(cam) + 2.0) elseif IsDisabledControlPressed(0, 96) and GetCamFov(cam) > 10 then SetCamFov(cam, GetCamFov(cam) - 2.0) end if IsDisabledControlJustPressed(0, 194) then CleanUp() end end function UpdateHackableEntityData() local entity = FindHackableObjectFromCamera() foundEntityByShapeTest = entity end function GetLocationLabel(entity) local entityCoords = GetEntityCoords(entity) local streetHash, crossingRoadHash = GetStreetNameAtCoord(entityCoords.x, entityCoords.y, entityCoords.z) local st1 = GetStreetNameFromHashKey(streetHash) local st2 = GetStreetNameFromHashKey(crossingRoadHash) return st1 .. ' ' .. st2 end function HandleHackableObject() if foundEntityByShapeTest then if not aimOnHackable then aimOnHackable = foundEntityByShapeTest local targetCoords = GetOffsetFromEntityInWorldCoords(aimOnHackable, 0.2, -0.2, 1.5) local _, screenX, screenY = GetScreenCoordFromWorldCoord(targetCoords.x, targetCoords.y, targetCoords.z) client.SendReactMessage('ui:setAimOnHackable', true) client.SendReactMessage('ui:setAtmInfo', { entity = aimOnHackable, screenX = screenX, screenY = screenY, minMoney = Entity(aimOnHackable).state.robbed and 0 or Config.Hack.reward.min, maxMoney = Entity(aimOnHackable).state.robbed and 0 or Config.Hack.reward.max, location = GetLocationLabel(aimOnHackable) }) SetEntityDrawOutlineColor(255, 0, 0) SetEntityDrawOutline(foundEntityByShapeTest, true) elseif aimOnHackable ~= foundEntityByShapeTest then SetEntityDrawOutline(aimOnHackable, false) aimOnHackable = false end if not isAimBusy and IsDisabledControlJustPressed(0, 229) then local model = GetEntityModel(aimOnHackable) local coords = GetEntityCoords(aimOnHackable) if lib.callback.await(_e('server:IsAtmHacked'), false, model, coords) then Utils.Notify(locale('atm_can_not_be_hack'), 'error') Citizen.Wait(500) else Utils.TriggerPoliceAlert(coords) isAimBusy = true selectedAtmEntity = aimOnHackable client.SendReactMessage('ui:setMiniGameOpen', true) SetNuiFocus(true, false) end end elseif aimOnHackable then client.SendReactMessage('ui:setAimOnHackable', false) SetEntityDrawOutline(aimOnHackable, false) aimOnHackable = false end end function Hack.Clear() deleteHackCam() for _, v in ipairs(atmParticles) do for _, particle in pairs(v) do StopParticleFxLooped(particle, 0) end end for key, value in pairs(ownedCashPiles) do if DoesEntityExist(value) then SetEntityAsMissionEntity(value, true, true) DeleteEntity(value) end local zoneKey = string.format('atmrobbery_explode_pickup_money_%d', key) Target.removeZone(zoneKey) end if onAtmHack then CleanUp() end atmParticles = {} ownedCashPiles = {} end function Hack.OpenTablet() if onAtmHack then return end onAtmHack = true client.SendReactMessage('ui:setPage', 'tablet') client.SendReactMessage('ui:setVisible', true) client.setBusy(true, 'hack.OpenTablet') client.giveTabletToPlayer() local cameras = Config.AtmCameras Hack.camId = CreateAtmCamera(cameras[currentAtmCameraKey]) SetCamActive(Hack.camId, true) RenderScriptCams(true, false, 1, false, true) ChangeCamera(Hack.camId, currentAtmCameraKey) SetTimecycleModifier('scanline_cam_cheap') SetTimecycleModifierStrength(2.0) Utils.ToggleHud(false) Citizen.CreateThread(function() while onAtmHack do local wait = isAimBusy and 1000 or 1 HandleControls(Hack.camId) HandleHackableObject() Citizen.Wait(wait) end CleanUp() end) Citizen.CreateThread(function() while onAtmHack do local wait = isAimBusy and 1000 or 500 UpdateHackableEntityData() Citizen.Wait(wait) end end) end function Hack.DeletePhone() if onAtmHack then CleanUp() end end function Hack.OpenHackPhone() if onAtmHack then return end onAtmHack = true client.SendReactMessage('ui:setPage', 'phone') client.SendReactMessage('ui:setVisible', true) client.setBusy(true, 'hack.OpenHackPhone') client.givePhoneToPlayer() Utils.ToggleHud(false) Citizen.CreateThread(function() while onAtmHack do local wait = 0 HandleHackableObject() DisableDisplayControlActions() if IsDisabledControlJustPressed(0, 194) then CleanUp() end if aimOnHackable then if DoesEntityExist(client.tabletInHand) then local phoneCoords = GetEntityCoords(client.tabletInHand) local camCoords = GetGameplayCamCoord() local camRot = GetGameplayCamRot(2) local camDirection = RotationToDirection(camRot) local distance = 1.0 local targetCoords = vector3( camCoords.x + camDirection.x * distance, camCoords.y + camDirection.y * distance, camCoords.z + camDirection.z * distance ) DrawLine( phoneCoords.x, phoneCoords.y, phoneCoords.z, targetCoords.x, targetCoords.y, targetCoords.z, 255, 255, 255, 125 ) end end Citizen.Wait(wait) end CleanUp() end) Citizen.CreateThread(function() local holdPhoneAnim = { dict = 'cellphone@', name = 'cellphone_text_in' } while onAtmHack do local wait = 500 UpdateHackableEntityData() local ped = cache.ped if not IsEntityPlayingAnim(ped, holdPhoneAnim.dict, holdPhoneAnim.name, 3) then lib.playAnim(playerPedId, holdPhoneAnim.dict, holdPhoneAnim.name, 3.0, 3.0, -1, 50) end Citizen.Wait(wait) end end) end --[[ events ]] RegisterNUICallback('nui:hacking:hackCurrentAtm', function(_, resultCallback) if not selectedAtmEntity or not DoesEntityExist(selectedAtmEntity) then return Utils.Notify(locale('atm_can_not_be_hack'), 'error') end local model = GetEntityModel(selectedAtmEntity) local coords = GetEntityCoords(selectedAtmEntity) TriggerServerEvent(_e('server:hacking:onAtmHackCompleted'), model, coords) Utils.Notify(locale('atm_hacked'), 'success') Entity(selectedAtmEntity).state.robbed = true resultCallback(true) end) RegisterNetEvent(_e('client:hacking:onAtmTabletUsed'), function() Hack.OpenTablet() end) RegisterNetEvent(_e('client:hacking:onHackPhoneUsed'), function() Hack.OpenHackPhone() end) RegisterNetEvent(_e('client:hacking:onAtmHacked'), function(model, coords) local atmEntity = GetClosestObjectOfType(coords.x, coords.y, coords.z, 0.3, model, true) if not DoesEntityExist(atmEntity) then return end createMoneySprayEffect(atmEntity) end) RegisterNetEvent(_e('client:hacking:onHackCompleted'), function(model, coords) isAimBusy = false SetNuiFocus(false, false) Citizen.SetTimeout(7000, function() local atmEntity = GetClosestObjectOfType(coords.x, coords.y, coords.z, 0.3, model, true) if not DoesEntityExist(atmEntity) then return end createMoneyObject(atmEntity) end) end)