forked from Simnation/Main
547 lines
16 KiB
Lua
547 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)
|