546 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			546 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| ---@alias tire { index: integer, distance: number, coords: vector3, name: string, vehicle: integer }
 | |
| 
 | |
| -- Variables --
 | |
| local isSlashing = false
 | |
| 
 | |
| 
 | |
| -- Functions --
 | |
| 
 | |
| ---Requests and waits for a animation dictionary to be loaded
 | |
| ---@param dict string
 | |
| local function loadAnimDict(dict)
 | |
|     RequestAnimDict(dict)
 | |
|     while not HasAnimDictLoaded(dict) do
 | |
|         Wait(0)
 | |
|     end
 | |
| end
 | |
| 
 | |
| ---Async function that waits until anim is played or timeout is passed
 | |
| ---@param ped integer
 | |
| ---@param animDict string
 | |
| ---@param animName string
 | |
| ---@param timeout integer|nil Deciseconds, defualts to 30 (3000ms)
 | |
| ---@return boolean playedAnim
 | |
| local function waitUntilPedIsPlayingAnim(ped, animDict, animName, timeout)
 | |
|     if not timeout then
 | |
|         timeout = 30
 | |
|     end
 | |
| 
 | |
|     while not IsEntityPlayingAnim(ped, animDict, animName, 3) do
 | |
|         timeout = timeout - 1
 | |
|         if timeout <= 0 then
 | |
|             return false
 | |
|         end
 | |
| 
 | |
|         Wait(100)
 | |
|     end
 | |
| 
 | |
|     return true
 | |
| end
 | |
| 
 | |
| ---Returns if the vehicle's model is blacklisted from getting slashed
 | |
| ---@param vehicle integer
 | |
| ---@return boolean isBlacklisted
 | |
| ---@return string|nil blacklistReason
 | |
| local function isVehicleModelBlacklisted(vehicle)
 | |
|     local blacklistReason = Config.VehicleBlacklist[GetEntityModel(vehicle)]
 | |
|     if blacklistReason == nil then
 | |
|         if not Config.CanSlashEmergencyVehicles and GetVehicleClass(vehicle) == 18 then
 | |
|             blacklistReason = 3
 | |
|         end
 | |
|     end
 | |
| 
 | |
|     return blacklistReason ~= nil, VEHICLE_BLACKLIST_REASONS[blacklistReason]
 | |
| end
 | |
| 
 | |
| ---Returns if the weapon can slash a tire
 | |
| ---@return boolean canSlash
 | |
| local function canWeaponSlashTires(weaponHash)
 | |
|     return Config.AllowedWeapons[weaponHash] ~= nil
 | |
| end
 | |
| 
 | |
| ---Returns if the current player weapon can slash a tire
 | |
| ---@return boolean canSlash
 | |
| local function canCurrentWeaponSlashTires()
 | |
|     local weaponHash = GetSelectedPedWeapon(PlayerPedId())
 | |
|     return canWeaponSlashTires(weaponHash)
 | |
| end
 | |
| 
 | |
| ---Gets the heading from coords A to coords B
 | |
| ---@param initialCoords vector2
 | |
| ---@param targetCoords vector2
 | |
| ---@return number heading
 | |
| local function getHeadingBetweenCoords(initialCoords, targetCoords)
 | |
|     local x = targetCoords.x - initialCoords.x
 | |
|     local y = targetCoords.y - initialCoords.y
 | |
| 
 | |
|     local heading = GetHeadingFromVector_2d(x, y)
 | |
|     return heading
 | |
| end
 | |
| 
 | |
| ---Burst a vehicles tire
 | |
| ---@param vehicle integer
 | |
| ---@param tireIndex integer
 | |
| local function burstVehicleTire(vehicle, tireIndex)
 | |
|     local bulletproof = not GetVehicleTyresCanBurst(vehicle)
 | |
|     if bulletproof then
 | |
|         SetVehicleTyresCanBurst(vehicle, true)
 | |
|     end
 | |
| 
 | |
|     -- This is to give it a sound effect
 | |
|     SetVehicleTyreBurst(vehicle, tireIndex, true, 1000.0)
 | |
|     SetVehicleTyreFixed(vehicle, tireIndex)
 | |
| 
 | |
|     -- Actuall tire deflation
 | |
|     SetVehicleTyreBurst(vehicle, tireIndex, false, 100.0)
 | |
| 
 | |
|     if bulletproof then
 | |
|         SetVehicleTyresCanBurst(vehicle, false)
 | |
|     end
 | |
| end
 | |
| 
 | |
| ---Gets data for the vehicle tire
 | |
| ---@param vehicle integer
 | |
| ---@param boneName string
 | |
| ---@param coords vector3|nil
 | |
| ---@return tire tire
 | |
| local function getVehicleTireByBone(vehicle, boneName, coords)
 | |
|     local boneIndex = GetEntityBoneIndexByName(vehicle, boneName)
 | |
|     if boneIndex == -1 then
 | |
|         return {}
 | |
|     end
 | |
| 
 | |
|     local boneCoords = GetWorldPositionOfEntityBone(vehicle, boneIndex)
 | |
|     if not coords then
 | |
|         coords = GetEntityCoords(PlayerPedId())
 | |
|     end
 | |
| 
 | |
|     return {
 | |
|         index = WHEEL_BONES[boneName],
 | |
|         distance = #(coords - boneCoords),
 | |
|         coords = boneCoords,
 | |
|         name = boneName,
 | |
|         vehicle = vehicle
 | |
|     }
 | |
| end
 | |
| 
 | |
| ---Returns the closest tire of a vehicle
 | |
| ---@param vehicle integer
 | |
| ---@param coords vector3|nil|false
 | |
| ---@return table closestVehicle
 | |
| local function getClosestVehicleTire(vehicle, coords)
 | |
|     local closest = {
 | |
|         distance = Config.MaxTireDetectionDist,
 | |
|     }
 | |
| 
 | |
|     if not coords then
 | |
|         coords = GetEntityCoords(PlayerPedId())
 | |
|     end
 | |
| 
 | |
|     for boneName, _index in pairs(WHEEL_BONES) do
 | |
|         local tire = getVehicleTireByBone(vehicle, boneName, coords)
 | |
| 
 | |
|         if tire.index ~= nil and tire.distance < closest.distance then
 | |
|             closest = tire
 | |
|         end
 | |
|     end
 | |
| 
 | |
|     return closest
 | |
| end
 | |
| 
 | |
| ---Makes the ped face the spesifed coords
 | |
| ---@param ped integer
 | |
| ---@param coords vector3
 | |
| local function makePedFaceCoords(ped, coords)
 | |
|     local headingDifference = math.abs(GetEntityHeading(ped) - getHeadingBetweenCoords(GetEntityCoords(ped), coords))
 | |
|     if headingDifference < 40.0 then
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     local duration = math.min(math.floor(headingDifference*6), 1000)
 | |
|     TaskTurnPedToFaceCoord(ped, coords.x, coords.y, coords.z, duration)
 | |
|     Wait(duration)
 | |
| end
 | |
| 
 | |
| ---Checks if the ped should be given the flee task from the spesifed coords
 | |
| ---@param ped integer
 | |
| ---@param coords vector3
 | |
| ---@param vehicle integer
 | |
| ---@param playerPed integer
 | |
| ---@return boolean canFleePed
 | |
| local function canGivePedFleeTask(ped, coords, vehicle, playerPed)
 | |
|     local dist = #(coords - GetEntityCoords(ped))
 | |
|     if dist > Config.AIReactionDistance then
 | |
|         return false
 | |
|     end
 | |
| 
 | |
|     if IsPedAPlayer(ped) then
 | |
|         return false
 | |
|     end
 | |
| 
 | |
|     -- Frozen peds can't flee anyway, and they are most likley a script handled ped (for stores and robberies for example)
 | |
|     if IsEntityPositionFrozen(ped) then
 | |
|         return false
 | |
|     end
 | |
| 
 | |
|     -- If the ped has the CPED_CONFIG_FLAG_DisableShockingEvents flag
 | |
|     if GetPedConfigFlag(ped, 294, true) then
 | |
|         return false
 | |
|     end
 | |
| 
 | |
|     -- Ignore dead peds
 | |
|     if IsPedDeadOrDying(ped, true) then
 | |
|         return false
 | |
|     end
 | |
| 
 | |
|     if not IsEntityVisible(ped) then
 | |
|         return false
 | |
|     end
 | |
| 
 | |
|     if not IsPedHuman(ped) then
 | |
|         return false
 | |
|     end
 | |
| 
 | |
|     -- This is cpu demanding, so it should be left as the last check
 | |
|     if GetVehiclePedIsIn(ped, false) ~= vehicle and not HasEntityClearLosToEntityInFront(ped, playerPed) then
 | |
|         return false
 | |
|     end
 | |
| 
 | |
|     return true
 | |
| end
 | |
| 
 | |
| ---Gets the peds that we want to react and flee after we have slashed a tire
 | |
| ---@param coords vector3
 | |
| ---@param vehicle integer The vehicle of the tire getting slashed
 | |
| ---@return table <integer, integer>
 | |
| local function getPedsToFlee(coords, vehicle, playerPed)
 | |
|     local peds = {}
 | |
|     local pedPool = GetGamePool('CPed')
 | |
| 
 | |
|     for i = 1, #pedPool do
 | |
|         local ped = pedPool[i]
 | |
|         if canGivePedFleeTask(ped, coords, vehicle, playerPed) then
 | |
|             peds[#peds+1] = PedToNet(ped)
 | |
|         end
 | |
|     end
 | |
| 
 | |
|     return peds
 | |
| end
 | |
| 
 | |
| ---Displays a message and sets isSlashing to false
 | |
| ---@param notifMessage string
 | |
| local function slashTireEnded(notifMessage)
 | |
|     DisplayNotification(notifMessage)
 | |
|     isSlashing = false
 | |
| end
 | |
| 
 | |
| ---Slashes a vehicles tire
 | |
| ---@param tire tire
 | |
| local function slashTire(tire)
 | |
|     isSlashing = true
 | |
|     local playerPed = PlayerPedId()
 | |
|     local vehicle = tire.vehicle
 | |
| 
 | |
|     if Config.DoFaceCoordTask then
 | |
|         makePedFaceCoords(playerPed, tire.coords)
 | |
|     end
 | |
| 
 | |
|     loadAnimDict(Config.SlashTireAnimation.Dict)
 | |
|     TaskPlayAnim(playerPed, Config.SlashTireAnimation.Dict, Config.SlashTireAnimation.Name, 2.0, 1.0, 700, 0, 0, false, false, false)
 | |
|     RemoveAnimDict(Config.SlashTireAnimation.Dict)
 | |
| 
 | |
|     if Config.DoAnimationCheckLoop then
 | |
|         local playedAnim = waitUntilPedIsPlayingAnim(playerPed, Config.SlashTireAnimation.Dict, Config.SlashTireAnimation.Name, 30)
 | |
|         if not playedAnim then
 | |
|             slashTireEnded(GetLocalization('slash_timeout'))
 | |
|             return
 | |
|         end
 | |
|     end
 | |
| 
 | |
|     Wait(450)
 | |
| 
 | |
|     local weaponHash = GetSelectedPedWeapon(playerPed)
 | |
|     local canSlash = canWeaponSlashTires(weaponHash)
 | |
|     if not canSlash then
 | |
|         slashTireEnded(GetLocalization('invalid_weapon'))
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     -- Get the data for the same tire again and check the new distance in case it has changed
 | |
|     local updatedTire = getVehicleTireByBone(vehicle, tire.name)
 | |
|     if updatedTire.distance > Config.MaxTireDetectionDist then
 | |
|         slashTireEnded(GetLocalization('slash_timeout'))
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     local isBulletproof = not GetVehicleTyresCanBurst(vehicle)
 | |
|     if isBulletproof and (Config.BulletproofSetting == 'proof' or Config.BulletproofSetting == 'limit' and not Config.AllowedWeapons[weaponHash]?.canSlashBulletProof) then
 | |
|         slashTireEnded(GetLocalization('tire_is_bulletproof'))
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     -- If we have network control over the vehicle then just burst the tire, if not we send the event to the server
 | |
|     local hasNetworkControlOverVehicle = NetworkHasControlOfEntity(vehicle)
 | |
|     if hasNetworkControlOverVehicle then
 | |
|         burstVehicleTire(vehicle, tire.index)
 | |
|     end
 | |
| 
 | |
|     local peds = getPedsToFlee(tire.coords, tire.vehicle, playerPed)
 | |
| 
 | |
|     -- Send event to server (for logging + tire burst if we did not have network control)
 | |
|     TriggerServerEvent('slashtires:slashTire', NetworkGetNetworkIdFromEntity(vehicle), tire.index, peds, hasNetworkControlOverVehicle)
 | |
| 
 | |
|     -- Event for other scripts to listen to
 | |
|     TriggerEvent('slashtires:slashedTire', vehicle, tire.index)
 | |
| 
 | |
|     Wait(1000)
 | |
|     isSlashing = false
 | |
| end
 | |
| 
 | |
| ---Does a raycast check to get the current target vehicle
 | |
| ---@return integer|nil
 | |
| local function getTargetVehicle()
 | |
|     local playerPed = PlayerPedId()
 | |
|     local coords = GetEntityCoords(playerPed)
 | |
|     local coordTo = GetOffsetFromEntityInWorldCoords(playerPed, 0.0, 2.0, 0.0)
 | |
| 
 | |
|     local rayHandle = StartShapeTestCapsule(coords.x, coords.y, coords.z, coordTo.x, coordTo.y, coordTo.z, 1.0, 6, playerPed, 7)
 | |
|     local _retval, hit, _endCoords, _surfaceNormal, entityHit = GetShapeTestResult(rayHandle)
 | |
| 
 | |
|     if hit and IsEntityAVehicle(entityHit) then
 | |
|         return entityHit
 | |
|     else
 | |
|         return nil
 | |
|     end
 | |
| end
 | |
| 
 | |
| ---Returns if the player can slash the vehicle tire
 | |
| ---@param tire tire
 | |
| ---@return boolean canSlash
 | |
| ---@return string|nil reason
 | |
| local function canSlashVehicleTire(tire)
 | |
|     if isSlashing then
 | |
|         return false, 'is_slashing'
 | |
|     end
 | |
| 
 | |
|     local canSlash, reason = CanPlayerSlashTires()
 | |
|     if not canSlash then
 | |
|         return false, reason
 | |
|     end
 | |
| 
 | |
|     local playerPed = PlayerPedId()
 | |
|     if IsPedRagdoll(playerPed) then
 | |
|         return false, 'in_ragdoll'
 | |
|     end
 | |
| 
 | |
|     if not canCurrentWeaponSlashTires() then
 | |
|         return false, 'invalid_weapon'
 | |
|     end
 | |
| 
 | |
|     local isBlacklisted, blacklistReason = isVehicleModelBlacklisted(tire.vehicle)
 | |
|     if isBlacklisted then
 | |
|         return false, blacklistReason
 | |
|     end
 | |
| 
 | |
|     if tire.distance > Config.MaxTireInteractionDist then
 | |
|         return false, 'too_far_away'
 | |
|     end
 | |
| 
 | |
|     if IsVehicleTyreBurst(tire.vehicle, tire.index, false) then
 | |
|         return false, 'tire_is_punctured'
 | |
|     end
 | |
| 
 | |
|     return true
 | |
| end
 | |
| 
 | |
| ---Attempts to slash the vehicles tire
 | |
| ---@param tire tire
 | |
| local function attemptToSlashTire(tire)
 | |
|     local canSlash, reason = canSlashVehicleTire(tire)
 | |
|     if not canSlash and reason then
 | |
|         DisplayNotification(GetLocalization(reason))
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     slashTire(tire)
 | |
| end
 | |
| 
 | |
| ---Returns if we can interact with the tires of a vehicle
 | |
| ---@param vehicle integer
 | |
| ---@return boolean canInteract
 | |
| local function canInteractWithVehicleTires(vehicle)
 | |
|     local isBlacklisted, blacklistReason = isVehicleModelBlacklisted(vehicle)
 | |
|     if not isBlacklisted then
 | |
|         return true
 | |
|     end
 | |
| 
 | |
|     if blacklistReason == 'vehicle_is_blacklisted' or blacklistReason == 'tire_is_indestructible' or blacklistReason == 'vehicle_is_emergency' then
 | |
|         return true
 | |
|     end
 | |
| 
 | |
|     return false
 | |
| end
 | |
| 
 | |
| ---Gets the closest vehicle and tire and calls the attemptToSlashTire function
 | |
| local function slashTireCommand()
 | |
|     if IsPedInAnyVehicle(PlayerPedId(), false) then
 | |
|         DisplayNotification(GetLocalization('in_a_vehicle'))
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     local vehicle = getTargetVehicle()
 | |
|     if vehicle == nil then
 | |
|         DisplayNotification(GetLocalization('no_vehicle_nearby'))
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     local tire = getClosestVehicleTire(vehicle)
 | |
|     if tire.index == nil then
 | |
|         DisplayNotification(GetLocalization('no_tire_nearby'))
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     attemptToSlashTire(tire)
 | |
| end
 | |
| 
 | |
| 
 | |
| -- Targeting / 3rd Eye --
 | |
| ---Returns if a target script should show the slash tire option
 | |
| ---@param vehicle integer
 | |
| ---@return boolean canInteract
 | |
| function TargetCanInteract(vehicle)
 | |
|     if not canCurrentWeaponSlashTires() then
 | |
|         return false
 | |
|     end
 | |
| 
 | |
|     if not canInteractWithVehicleTires(vehicle) then
 | |
|         return false
 | |
|     end
 | |
| 
 | |
|     return true
 | |
| end
 | |
| 
 | |
| 
 | |
| -- Commands, Key Mapping & Threads --
 | |
| if Config.HelpText then
 | |
|     local isShowingHelpText = false
 | |
| 
 | |
|     ---If the slashtire key mapping press should be blocked 
 | |
|     ---@return boolean block
 | |
|     local function shouldSlashKeyPressBeBlocked()
 | |
|         return not isShowingHelpText or IsPauseMenuActive()
 | |
|     end
 | |
| 
 | |
|     RegisterKeyMapping('+slashtire', GetLocalization('keymapping_desc_keyboard'), 'keyboard', Config.DefaultKey)
 | |
|     RegisterCommand('+slashtire', function()
 | |
|         if not shouldSlashKeyPressBeBlocked() then
 | |
|             slashTireCommand()
 | |
|         end
 | |
|     end, false)
 | |
|     RegisterCommand('-slashtire', function()
 | |
|         -- Do nothing
 | |
|     end, false)
 | |
| 
 | |
|     -- For controller users
 | |
|     RegisterKeyMapping('stc', GetLocalization('keymapping_desc_controller'), 'PAD_ANALOGBUTTON', Config.DefaultPadAnalogButton)
 | |
|     RegisterCommand('stc', function()
 | |
|         if not shouldSlashKeyPressBeBlocked() then
 | |
|             slashTireCommand()
 | |
|         end
 | |
|     end, false)
 | |
| 
 | |
|     ---Does all the prompt checks and shows the help text if successful, returns the time that the script should wait until next check
 | |
|     ---@return integer waitMs
 | |
|     ---@return boolean showHelpText
 | |
|     local function promptTick()
 | |
|         if isSlashing or IsPedInAnyVehicle(PlayerPedId(), false) then
 | |
|             return 1000, false
 | |
|         end
 | |
| 
 | |
|         local vehicle = getTargetVehicle()
 | |
|         if vehicle == nil or not canInteractWithVehicleTires(vehicle) then
 | |
|             return 500, false
 | |
|         end
 | |
| 
 | |
|         local tire = getClosestVehicleTire(vehicle)
 | |
|         if tire.index == nil or tire.distance > Config.MaxTireInteractionDist or IsVehicleTyreBurst(vehicle, tire.index, false) then
 | |
|             return 250, false
 | |
|         end
 | |
| 
 | |
|         return 100, true
 | |
|     end
 | |
| 
 | |
|     local threadIsActive = false
 | |
|     local slashWeaponEquipped = false
 | |
| 
 | |
|     local function weaponEquippedThread()
 | |
|         threadIsActive = true
 | |
| 
 | |
|         while slashWeaponEquipped do
 | |
|             local wait, showHelpText = promptTick()
 | |
|             if showHelpText then
 | |
|                 if not isShowingHelpText then
 | |
|                     StartHelpText()
 | |
|                     isShowingHelpText = true
 | |
|                 end
 | |
|             elseif isShowingHelpText then
 | |
|                 StopHelpText()
 | |
|                 isShowingHelpText = false
 | |
|             end
 | |
| 
 | |
|             Wait(wait)
 | |
|         end
 | |
| 
 | |
|         -- Hide the help text if the weapon was de-equipped
 | |
|         if isShowingHelpText then
 | |
|             StopHelpText()
 | |
|             isShowingHelpText = false
 | |
|         end
 | |
| 
 | |
|         threadIsActive = false
 | |
|     end
 | |
| 
 | |
|     AddEventHandler('slashtires:slashWeaponEquipped', function(state)
 | |
|         slashWeaponEquipped = state
 | |
| 
 | |
|         if not state or threadIsActive then
 | |
|             return
 | |
|         end
 | |
| 
 | |
|         CreateThread(weaponEquippedThread)
 | |
|     end)
 | |
| end
 | |
| 
 | |
| RegisterCommand('slashtire', function()
 | |
|     slashTireCommand()
 | |
| end, false)
 | |
| 
 | |
| if Config.AddChatSuggestion then
 | |
|     TriggerEvent('chat:addSuggestion', '/slashtire', GetLocalization('chat_suggestion'))
 | |
| end
 | |
| 
 | |
| 
 | |
| -- Events --
 | |
| RegisterNetEvent('slashtires:burstTire', function(netId, tireIndex)
 | |
|     local vehicle = NetworkGetEntityFromNetworkId(netId)
 | |
|     burstVehicleTire(vehicle, tireIndex)
 | |
| end)
 | |
| 
 | |
| RegisterNetEvent('slashtires:displayNotification', function(message)
 | |
|     DisplayNotification(message)
 | |
| end)
 | |
| 
 | |
| 
 | |
| -- Exports --
 | |
| local function getIsSlashing()
 | |
|     return isSlashing
 | |
| end
 | |
| 
 | |
| exports('isSlashing', getIsSlashing)
 | |
| exports('canCurrentWeaponSlashTires', canCurrentWeaponSlashTires)
 | |
| exports('burstVehicleTire', burstVehicleTire)
 | |
| exports('getVehicleTireByBone', getVehicleTireByBone)
 | |
| exports('getClosestVehicleTire', getClosestVehicleTire)
 | |
| exports('slashTire', slashTire)
 | |
| exports('canSlashVehicleTire', canSlashVehicleTire)
 | |
| exports('attemptToSlashTire', attemptToSlashTire)
 | 
