---@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 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)