214 lines
8.6 KiB
Lua
214 lines
8.6 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
|
|
|
|
--- 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
|
|
_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 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,
|
|
},
|
|
--]]
|
|
}
|