503 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			503 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| --[[ 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)
 | 
