Main/resources/[carscripts]/slashtires/client/client.lua
2025-06-07 08:51:21 +02:00

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)