From 1e426fba800e78d32312478b706c73ad120555a8 Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 10 Aug 2026 20:03:29 +0200 Subject: [PATCH] FIX - improve payphone call flow --- frontend/src/components/PayphoneOverlay.vue | 6 +- sky_phone/config/locales/en.lua | 4 +- sky_phone/source/bridge/client/framework.lua | 7 + sky_phone/source/client/payphones.lua | 157 ++++++++++++++++--- 4 files changed, 145 insertions(+), 29 deletions(-) diff --git a/frontend/src/components/PayphoneOverlay.vue b/frontend/src/components/PayphoneOverlay.vue index c3894e2..8dd459e 100644 --- a/frontend/src/components/PayphoneOverlay.vue +++ b/frontend/src/components/PayphoneOverlay.vue @@ -648,8 +648,8 @@ onBeforeUnmount(() => { width: 50.9%; height: 5.9%; display: grid; - grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr); - gap: 3%; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 3.2%; } .payphone-action { @@ -676,10 +676,12 @@ onBeforeUnmount(() => { } .payphone-action--call { + grid-column: span 2; background: linear-gradient(#3dbd74, #187c43); } .payphone-action--hangup { + grid-column: span 2; background: linear-gradient(#e95c55, #a12420); } diff --git a/sky_phone/config/locales/en.lua b/sky_phone/config/locales/en.lua index 8f7c793..72d4ad4 100644 --- a/sky_phone/config/locales/en.lua +++ b/sky_phone/config/locales/en.lua @@ -30,7 +30,9 @@ Locales["en"] = { default = "The phone could not be opened.", }, Payphone = { - Interact = "Press ~INPUT_CONTEXT~ to use the payphone.", + Interact = "Use the payphone.", + RingingHelp = "Calling {number}... | Hang up", + ConnectedHelp = "Connected to {number} | {duration} | {currency}{cost} | Hang up", }, Nui = { Payphone = { diff --git a/sky_phone/source/bridge/client/framework.lua b/sky_phone/source/bridge/client/framework.lua index 32fd73d..77f498b 100644 --- a/sky_phone/source/bridge/client/framework.lua +++ b/sky_phone/source/bridge/client/framework.lua @@ -50,3 +50,10 @@ function Bridge.Framework.Notify(title, message, notification_type, duration) Bridge.Debug("error", "[sky_phone] Notification requested for unsupported framework '%s'.", tostring(framework_name)) end + +function Bridge.Framework.ShowHelpNotification(message, key) + local control = key == "E" and "~INPUT_CONTEXT~" or ("[%s]"):format(tostring(key or "E")) + BeginTextCommandDisplayHelp("STRING") + AddTextComponentSubstringPlayerName(("%s %s"):format(control, tostring(message or ""))) + EndTextCommandDisplayHelp(0, false, true, -1) +end diff --git a/sky_phone/source/client/payphones.lua b/sky_phone/source/client/payphones.lua index 7c187d9..5c7173c 100644 --- a/sky_phone/source/client/payphones.lua +++ b/sky_phone/source/client/payphones.lua @@ -2,11 +2,16 @@ local payphone_open = false local nearest_payphone = nil local active_booth = nil local active_call_id = nil +local active_call_state = nil +local active_call_number = nil +local active_call_elapsed_seconds = 0 +local active_call_elapsed_updated_at = 0 local call_channel = 0 local replacement_prop = nil local hidden_prop = nil local animation_scene = nil -local distance_hangup_requested = false +local visuals_starting = false +local hangup_requested = false local configured_models = {} for _, model_name in ipairs(Config.Payphones.Props or {}) do @@ -64,16 +69,24 @@ end local function stop_call_visuals() local animation = Config.Payphones.Animation local ped = PlayerPedId() + local visuals_active = animation_scene ~= nil or replacement_prop ~= nil + visuals_starting = false + + if visuals_active and DoesEntityExist(ped) then + StopAnimTask(ped, animation.Dictionary, animation.PedClip, 0.0) + ClearPedTasksImmediately(ped) + end + if animation_scene then - ClearPedTasks(ped) + SetSynchronizedSceneHoldLastFrame(animation_scene, false) DisposeSynchronizedScene(animation_scene) animation_scene = nil - else - StopAnimTask(ped, animation.Dictionary, animation.PedClip, 2.0) end if replacement_prop and DoesEntityExist(replacement_prop) then - StopEntityAnim(replacement_prop, animation.PropClip, animation.Dictionary, -2.0) + StopEntityAnim(replacement_prop, animation.PropClip, animation.Dictionary, 0.0) + SetEntityVisible(replacement_prop, false, false) + SetEntityAsMissionEntity(replacement_prop, true, true) DeleteEntity(replacement_prop) end replacement_prop = nil @@ -86,19 +99,32 @@ local function stop_call_visuals() end local function start_call_visuals() - if replacement_prop or not active_booth or not DoesEntityExist(active_booth.entity) then + if visuals_starting or replacement_prop or not active_call_id + or not active_booth or not DoesEntityExist(active_booth.entity) + then return end + visuals_starting = true + local expected_call_id = active_call_id + local booth = active_booth local replacement_hash = joaat(Config.Payphones.ReplacementProp) local animation = Config.Payphones.Animation if not load_model(replacement_hash) or not load_animation(animation.Dictionary) then Bridge.Debug("error", "[sky_phone] The payphone animation assets could not be loaded.") + visuals_starting = false SetModelAsNoLongerNeeded(replacement_hash) + RemoveAnimDict(animation.Dictionary) + return + end + if active_call_id ~= expected_call_id or active_booth ~= booth or not DoesEntityExist(booth.entity) then + visuals_starting = false + SetModelAsNoLongerNeeded(replacement_hash) + RemoveAnimDict(animation.Dictionary) return end - local original = active_booth.entity + local original = booth.entity local coords = GetEntityCoords(original) local rotation = GetEntityRotation(original, 2) local replacement = CreateObjectNoOffset( @@ -112,7 +138,9 @@ local function start_call_visuals() ) if replacement == 0 or not DoesEntityExist(replacement) then Bridge.Debug("error", "[sky_phone] The animated payphone replacement prop could not be created.") + visuals_starting = false SetModelAsNoLongerNeeded(replacement_hash) + RemoveAnimDict(animation.Dictionary) return end @@ -133,6 +161,12 @@ local function start_call_visuals() rotation.z, 2 ) + if not animation_scene or animation_scene == -1 then + Bridge.Debug("error", "[sky_phone] The payphone synchronized scene could not be created.") + animation_scene = nil + stop_call_visuals() + return + end SetSynchronizedSceneHoldLastFrame(animation_scene, true) TaskSynchronizedScene( ped, @@ -147,6 +181,7 @@ local function start_call_visuals() 0 ) PlayEntityAnim(replacement, animation.PropClip, animation.Dictionary, 8.0, false, true, false, 0.0, 0) + visuals_starting = false SetModelAsNoLongerNeeded(replacement_hash) end @@ -159,14 +194,74 @@ local function booth_payload(booth) end local function close_payphone() + local was_open = payphone_open payphone_open = false - SetNuiFocus(false, false) - SendNUIMessage({ type = "payphone:close" }) + if was_open then + SetNuiFocus(false, false) + SendNUIMessage({ type = "payphone:close" }) + end if not active_call_id then active_booth = nil end end +local function format_duration(seconds) + local duration = math.max(0, math.floor(tonumber(seconds) or 0)) + return ("%02d:%02d"):format(math.floor(duration / 60), duration % 60) +end + +local function replace_placeholder(value, placeholder, replacement) + return value:gsub("{" .. placeholder .. "}", tostring(replacement)) +end + +local function current_call_elapsed_seconds() + if active_call_state ~= "connected" then + return 0 + end + return active_call_elapsed_seconds + + math.max(0, math.floor((GetGameTimer() - active_call_elapsed_updated_at) / 1000)) +end + +local function call_help_message() + local locale = get_locale().Payphone + local message + if active_call_state == "connected" then + local elapsed_seconds = current_call_elapsed_seconds() + message = locale.ConnectedHelp + message = replace_placeholder(message, "duration", format_duration(elapsed_seconds)) + message = replace_placeholder(message, "currency", Config.Payphones.Currency) + message = replace_placeholder(message, "cost", elapsed_seconds * (tonumber(Config.Payphones.PricePerSecond) or 0)) + else + message = locale.RingingHelp + end + return replace_placeholder(message, "number", active_call_number or "") +end + +local function apply_active_call_state(data) + if active_call_id ~= data.id then + hangup_requested = false + end + active_call_id = data.id + active_call_state = data.state + active_call_number = data.otherNumber or active_call_number + if data.state == "connected" then + active_call_elapsed_seconds = math.max(0, math.floor(tonumber(data.elapsedSeconds) or 0)) + active_call_elapsed_updated_at = GetGameTimer() + else + active_call_elapsed_seconds = 0 + active_call_elapsed_updated_at = 0 + end +end + +local function clear_active_call_state() + active_call_id = nil + active_call_state = nil + active_call_number = nil + active_call_elapsed_seconds = 0 + active_call_elapsed_updated_at = 0 + hangup_requested = false +end + local function open_payphone(booth) if payphone_open or active_call_id or IsNuiFocused() then return @@ -193,12 +288,16 @@ RegisterNUICallback("payphone:dial", function(data, cb) local payload = booth_payload(active_booth) payload.phoneNumber = type(data) == "table" and data.phoneNumber or nil local result = Bridge.Callbacks.Trigger("sky_phone:payphone:dial", payload) - if result and result.success and result.data and (result.data.state == "ringing" or result.data.state == "connected") then - active_call_id = result.data.id - distance_hangup_requested = false - start_call_visuals() + local call_started = result and result.success and result.data + and (result.data.state == "ringing" or result.data.state == "connected") + if call_started then + apply_active_call_state(result.data) end cb(result or { success = false, error = "request_failed" }) + if call_started then + close_payphone() + start_call_visuals() + end end) RegisterNUICallback("payphone:hangup", function(_, cb) @@ -223,19 +322,20 @@ RegisterNetEvent("sky_phone:payphone:state", function(data) return end if data.state == "ringing" or data.state == "connected" then - active_call_id = data.id - distance_hangup_requested = false + apply_active_call_state(data) + close_payphone() start_call_visuals() if data.state == "connected" and data.channel and call_channel ~= tonumber(data.channel) and not join_call_voice(data.channel) then + hangup_requested = true Bridge.Callbacks.Trigger("sky_phone:payphone:hangup", { id = active_call_id }) end else - active_call_id = nil - distance_hangup_requested = false + clear_active_call_state() leave_call_voice() stop_call_visuals() + active_booth = nil end SendNUIMessage({ type = "payphone:state", data = data }) end) @@ -278,9 +378,7 @@ end) CreateThread(function() while true do if nearest_payphone and nearest_payphone.distance <= Config.Payphones.InteractionDistance and not IsNuiFocused() then - BeginTextCommandDisplayHelp("STRING") - AddTextComponentSubstringPlayerName(get_locale().Payphone.Interact) - EndTextCommandDisplayHelp(0, false, true, -1) + Bridge.Framework.ShowHelpNotification(get_locale().Payphone.Interact, "E") if IsControlJustReleased(0, 38) then open_payphone(nearest_payphone) end @@ -293,15 +391,21 @@ end) CreateThread(function() while true do - if active_call_id and active_booth and not distance_hangup_requested then - local distance = #(GetEntityCoords(PlayerPedId()) - active_booth.coords) - if distance > Config.Payphones.MaximumCallDistance then - distance_hangup_requested = true + if active_call_id and active_booth then + Bridge.Framework.ShowHelpNotification(call_help_message(), "E") + if IsControlJustReleased(0, 38) and not hangup_requested then + hangup_requested = true Bridge.Callbacks.Trigger("sky_phone:payphone:hangup", { id = active_call_id }) end - Wait(500) + + local distance = #(GetEntityCoords(PlayerPedId()) - active_booth.coords) + if distance > Config.Payphones.MaximumCallDistance and not hangup_requested then + hangup_requested = true + Bridge.Callbacks.Trigger("sky_phone:payphone:hangup", { id = active_call_id }) + end + Wait(0) else - Wait(1000) + Wait(250) end end end) @@ -315,4 +419,5 @@ AddEventHandler("onResourceStop", function(resource_name) end leave_call_voice() stop_call_visuals() + clear_active_call_state() end)