Files
Elite-Gaming-FiveM/resources/radio/animations.lua
T
2026-03-28 17:31:40 -07:00

227 lines
9.2 KiB
Lua

-- ┌──────────────────────────────────────────────────────────────┐
-- │ Tommy's Radio - Animation Configuration │
-- │ Loaded after config.lua — assigns Config.animations │
-- │ Documentation: https://docs.timmygstudios.com/docs/tommys-radio │
-- └──────────────────────────────────────────────────────────────┘
-- ══════════════════════════════════════════════════════════════════
-- ANIMATION HELPERS (used internally by the animation definitions)
-- ══════════════════════════════════════════════════════════════════
-- Shared animation state tracker (global so it persists across calls)
_radioAnimState = _radioAnimState or {
isPlaying = false,
pendingStart = false,
dictLoaded = false,
radioProp = nil,
}
--- Loads an anim dictionary without blocking the main thread.
--- Calls onLoaded() once ready, only if shouldContinue() still returns true.
--- @param dict string Animation dictionary name
--- @param shouldContinue fun():boolean Check before starting (e.g. user hasn't released PTT)
--- @param onLoaded fun() Callback when dictionary is loaded and shouldContinue is true
local function LoadAnimDictAsync(dict, shouldContinue, onLoaded)
RequestAnimDict(dict)
Citizen.CreateThread(function()
local attempts = 0
while not HasAnimDictLoaded(dict) and attempts < 50 do
Citizen.Wait(10)
attempts = attempts + 1
end
if shouldContinue() and HasAnimDictLoaded(dict) then
onLoaded()
end
end)
end
--- Returns true if radio animations should be suppressed (in vehicle or holding a weapon).
--- @param playerPed number Ped handle
--- @return boolean
local function ShouldSuppressAnim(playerPed)
local vehicle = GetVehiclePedIsIn(playerPed, false)
if vehicle ~= 0 and GetPedInVehicleSeat(vehicle, -1) == playerPed then return true end
local _, weaponHash = GetCurrentPedWeapon(playerPed, true)
if weaponHash ~= GetHashKey("WEAPON_UNARMED") then return true end
return false
end
--- Starts a ped animation and optionally attaches a prop.
--- @param playerPed number Ped handle
--- @param dict string Animation dictionary
--- @param anim string Animation name
--- @param prop string|nil Prop model name (nil = no prop)
local function StartRadioAnim(playerPed, dict, anim, prop)
if IsEntityPlayingAnim(playerPed, dict, anim, 3) then return end
TaskPlayAnim(playerPed, dict, anim, 8.0, 2.0, -1, 50, 2.0, false, false, false)
_radioAnimState.isPlaying = true
if prop then
_radioAnimState.radioProp = CreateObject(GetHashKey(prop), 0, 0, 0, true, true, true)
AttachEntityToEntity(
_radioAnimState.radioProp, playerPed,
GetPedBoneIndex(playerPed, 28422),
0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
true, true, false, true, 1, true
)
SetEntityAsMissionEntity(_radioAnimState.radioProp, true, true)
end
end
--- Stops an animation and cleans up any attached prop.
--- @param playerPed number Ped handle
--- @param dict string Animation dictionary
--- @param anim string Animation name
local function StopRadioAnim(playerPed, dict, anim)
if _radioAnimState.isPlaying or IsEntityPlayingAnim(playerPed, dict, anim, 3) then
StopAnimTask(playerPed, dict, anim, -4.0)
end
if _radioAnimState.radioProp then
DeleteObject(_radioAnimState.radioProp)
_radioAnimState.radioProp = nil
end
_radioAnimState.isPlaying = false
end
--- Creates a standard onKeyState handler (plays anim on PTT press, stops on release).
--- @param dict string Animation dictionary
--- @param anim string Animation name
--- @param prop string|nil Prop model name (nil = no prop)
--- @return fun(isKeyDown: boolean)
local function MakePTTHandler(dict, anim, prop)
return function(isKeyDown)
local playerPed = PlayerPedId()
if not playerPed or playerPed == 0 then return end
if isKeyDown then
if ShouldSuppressAnim(playerPed) then return end
_radioAnimState.pendingStart = true
LoadAnimDictAsync(dict,
function() return _radioAnimState.pendingStart end,
function()
_radioAnimState.dictLoaded = true
StartRadioAnim(playerPed, dict, anim, prop)
end
)
else
_radioAnimState.pendingStart = false
StopRadioAnim(playerPed, dict, anim)
end
end
end
--- Creates a standard onRadioFocus handler (plays anim when radio UI is focused).
--- @param dict string Animation dictionary
--- @param anim string Animation name
--- @param prop string|nil Prop model name (nil = no prop)
--- @return fun(focused: boolean)
local function MakeFocusHandler(dict, anim, prop)
return function(focused)
local playerPed = PlayerPedId()
if not playerPed or playerPed == 0 then return end
if focused then
if ShouldSuppressAnim(playerPed) then return end
if not _radioAnimState.isPlaying then
LoadAnimDictAsync(dict,
function() return true end,
function() StartRadioAnim(playerPed, dict, anim, prop) end
)
end
else
StopRadioAnim(playerPed, dict, anim)
end
end
end
-- ══════════════════════════════════════════════════════════════════
-- ANIMATION DEFINITIONS
-- ══════════════════════════════════════════════════════════════════
-- Users can select which animation to use through the radio settings menu.
-- Each entry needs:
-- name - Display name shown in the settings menu
-- onKeyState - Called when PTT key is pressed (true) or released (false)
-- onRadioFocus - Called when the radio UI is focused (true) or unfocused (false)
--
-- You can add, remove, or reorder entries. The index [1] is used as the
-- fallback if a player's saved animation is no longer available.
Config.animations = {
-- [1] No animation at all
[1] = {
name = "None",
onKeyState = function(isKeyDown) end,
onRadioFocus = function(focused) end,
},
-- [2] Shoulder mic — PTT plays a shoulder-radio gesture, focus shows handheld + prop
[2] = {
name = "Shoulder",
onKeyState = MakePTTHandler("random@arrests", "generic_radio_enter", nil),
onRadioFocus = MakeFocusHandler("cellphone@", "cellphone_call_to_text", "prop_cs_hand_radio"),
},
-- [3] Handheld radio — Both PTT and focus use the same handheld + prop animation
[3] = {
name = "Handheld",
onKeyState = MakePTTHandler("cellphone@", "cellphone_call_to_text", "prop_cs_hand_radio"),
onRadioFocus = MakeFocusHandler("cellphone@", "cellphone_call_to_text", "prop_cs_hand_radio"),
},
-- [4] Earpiece — PTT plays an ear-touch gesture, no animation on focus
[4] = {
name = "Earpiece",
onKeyState = MakePTTHandler("cellphone@", "cellphone_call_listen_base", nil),
onRadioFocus = function(focused)
-- Earpiece has no focus animation — PTT only
end,
},
--[[ ── RP Emotes examples (requires rpemotes resource) ──────────────
-- Uncomment any of these and adjust the index numbers as needed.
-- See docs for more details: https://tommys-scripts.gitbook.io/fivem/
-- paid-scripts/tommys-radio/setup-and-configuration
[5] = {
name = "Chest",
onKeyState = function(isKeyDown)
if isKeyDown then
exports["rpemotes"]:EmoteCommandStart("radiochest", 0)
else
exports["rpemotes"]:EmoteCancel(true)
end
end,
onRadioFocus = function(focused)
if focused then
exports["rpemotes"]:EmoteCommandStart("wt", 0)
else
exports["rpemotes"]:EmoteCancel(true)
end
end,
},
[6] = {
name = "Handheld2",
onKeyState = function(isKeyDown)
if isKeyDown then
exports["rpemotes"]:EmoteCommandStart("wt4", 0)
else
exports["rpemotes"]:EmoteCancel(true)
end
end,
onRadioFocus = function(focused)
if focused then
exports["rpemotes"]:EmoteCommandStart("wt", 0)
else
exports["rpemotes"]:EmoteCancel(true)
end
end,
},
--]]
}