From 0a43a96744e90f9a54438faf03576a280ae86506 Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" Date: Sun, 16 Aug 2026 17:16:01 +0200 Subject: [PATCH] Camera: front-camera transform and focus changes Compute a more accurate front (selfie) camera transform and adjust front-camera params (FOV, distance, height). Replace apply_rear_camera_view with a unified apply_camera_view that selects the correct view_mode for ped/vehicle. Add block-look support to SkyPhoneFocus (Resolve now returns block_look) and make ApplyGameInputControls accept a block_look flag; main.lua tracks phone_block_look and passes it when disabling controls. Update related tests to reflect new camera math and input behavior. --- .../src/views/apps/CameraApp.contract.test.ts | 22 ++++--- sky_phone/source/client/camera.lua | 61 ++++++++----------- sky_phone/source/client/focus.lua | 24 ++++++-- sky_phone/source/client/main.lua | 14 ++++- tests/client_camera.lua | 6 +- tests/client_focus.lua | 48 ++++++++++++--- 6 files changed, 111 insertions(+), 64 deletions(-) diff --git a/frontend/src/views/apps/CameraApp.contract.test.ts b/frontend/src/views/apps/CameraApp.contract.test.ts index 8380ea3..8b1d617 100644 --- a/frontend/src/views/apps/CameraApp.contract.test.ts +++ b/frontend/src/views/apps/CameraApp.contract.test.ts @@ -71,12 +71,9 @@ describe('Camera app controls', () => { expect(cameraClient).toContain( 'SetFollowPedCamViewMode(first_person_view_mode)', ) - expect(cameraClient).toContain( - 'SetFollowVehicleCamViewMode(first_person_view_mode)', - ) - expect(cameraClient).toMatch( - /while camera_state\.active do[\s\S]*else\s+apply_rear_camera_view\(\)/, - ) + expect(cameraClient).toContain('SetFollowVehicleCamViewMode(view_mode)') + expect(cameraClient).toContain('SetFollowPedCamViewMode(view_mode)') + expect(cameraClient).toMatch(/while camera_state\.active do[\s\S]*apply_camera_view\(\)/) expect(cameraClient).not.toContain('next_view_apply') expect(cameraClient).not.toContain('ultrawide_camera_handle') expect(cameraClient).not.toContain('ensure_ultrawide_camera') @@ -89,9 +86,16 @@ describe('Camera app controls', () => { expect(cameraClient).toContain( 'if camera_state.locked or camera_state.front_camera then', ) - expect(cameraClient).toContain('capture_front_camera_transform') - expect(cameraClient).toContain('forward_vector * front_camera_distance') - expect(cameraClient).toContain('front_camera_target_offset') + expect(cameraClient).toContain('get_front_camera_transform') + expect(cameraClient).toContain('local front_camera_view_mode = 0') + expect(cameraClient).toContain('local front_camera_fov = 32.0') + expect(cameraClient).toContain('local front_camera_distance = 1.05') + expect(cameraClient).toContain('local head_position = GetPedBoneCoords') + expect(cameraClient).toContain('local dot = (to_camera.x * forward_vector.x)') + expect(cameraClient).toContain('front_camera_target_height') + expect(cameraClient).toContain( + 'return { block_game = false, block_look = false, cursor = false, focused = true, game_input = true, keep_input = true }', + ) expect(cameraClient).toContain('SetCamCoord(') expect(cameraClient).toContain('PointCamAtCoord(') expect(cameraClient).not.toContain('SetCamRot(') diff --git a/sky_phone/source/client/camera.lua b/sky_phone/source/client/camera.lua index 24903d9..89a56ea 100644 --- a/sky_phone/source/client/camera.lua +++ b/sky_phone/source/client/camera.lua @@ -2,11 +2,11 @@ local minimum_zoom = 0.5 local maximum_zoom = 3.0 local mouse_wheel_zoom_step = 0.08 local first_person_view_mode = 4 -local front_camera_fov = 48.0 -local front_camera_distance = 1.45 -local front_camera_side_offset = 0.0 -local front_camera_height = 0.06 -local front_camera_target_height = -0.1 +local front_camera_view_mode = 0 +local front_camera_fov = 32.0 +local front_camera_distance = 1.05 +local front_camera_height = 0.05 +local front_camera_target_height = 0.03 local blocked_camera_controls = { 0, -- INPUT_NEXT_CAMERA 22, -- INPUT_JUMP @@ -43,8 +43,6 @@ local camera_state = { focus_watcher = false, front_camera = false, front_camera_handle = nil, - front_camera_offset = nil, - front_camera_target_offset = nil, game_input = false, landscape = false, locked = false, @@ -85,34 +83,32 @@ local function set_flash_enabled(enabled) end) end -local function capture_front_camera_transform(ped) - local ped_position = GetEntityCoords(ped) +local function get_front_camera_transform(ped) local head_position = GetPedBoneCoords(ped, 31086, 0.0, 0.0, 0.0) - local head_height = head_position.z - ped_position.z local forward = GetEntityForwardVector(ped) - local forward_vector = vector3(forward.x, forward.y, 0.0) - local right_vector = vector3(forward_vector.y, -forward_vector.x, 0.0) - local camera_offset = (forward_vector * front_camera_distance) - + (right_vector * front_camera_side_offset) - + vector3(0.0, 0.0, head_height + front_camera_height) - local target_offset = (right_vector * (front_camera_side_offset * 0.25)) - + vector3(0.0, 0.0, head_height + front_camera_target_height) - return camera_offset, target_offset + local forward_vector = vector3(forward.x, forward.y, forward.z) + local camera_offset = forward_vector * front_camera_distance + local camera_position = head_position + camera_offset + vector3(0.0, 0.0, front_camera_height) + local to_camera = camera_position - head_position + local dot = (to_camera.x * forward_vector.x) + + (to_camera.y * forward_vector.y) + + (to_camera.z * forward_vector.z) + if dot < 0.0 then + camera_position = head_position - camera_offset + vector3(0.0, 0.0, front_camera_height) + end + local target_position = head_position + vector3(0.0, 0.0, front_camera_target_height) + return camera_position, target_position end local function apply_front_camera(ped) if not camera_state.front_camera_handle or not DoesCamExist(camera_state.front_camera_handle) then - camera_state.front_camera_offset, camera_state.front_camera_target_offset = - capture_front_camera_transform(ped) camera_state.front_camera_handle = CreateCam("DEFAULT_SCRIPTED_CAMERA", true) SetCamFov(camera_state.front_camera_handle, front_camera_fov) SetCamActive(camera_state.front_camera_handle, true) RenderScriptCams(true, false, 0, true, true) end - local ped_position = GetEntityCoords(ped) - local camera_position = ped_position + camera_state.front_camera_offset - local target_position = ped_position + camera_state.front_camera_target_offset + local camera_position, target_position = get_front_camera_transform(ped) SetCamCoord( camera_state.front_camera_handle, camera_position.x, @@ -133,20 +129,16 @@ local function clear_front_camera() DestroyCam(camera_state.front_camera_handle, false) end camera_state.front_camera_handle = nil - camera_state.front_camera_offset = nil - camera_state.front_camera_target_offset = nil end -local function apply_rear_camera_view() - if camera_state.front_camera then - return - end +local function apply_camera_view() local ped = PlayerPedId() + local view_mode = camera_state.front_camera and front_camera_view_mode or first_person_view_mode if IsPedInAnyVehicle(ped, false) then - SetFollowVehicleCamViewMode(first_person_view_mode) + SetFollowVehicleCamViewMode(view_mode) return end - SetFollowPedCamViewMode(first_person_view_mode) + SetFollowPedCamViewMode(view_mode) end local function restore_camera_view() @@ -269,7 +261,7 @@ local function set_camera_active(active) camera_state.previous_vehicle_view = GetFollowVehicleCamViewMode() DisplayRadar(false) set_camera_focus(true) - apply_rear_camera_view() + apply_camera_view() TriggerEvent("sky_phone:animation:camera", { active = true, front = camera_state.front_camera, @@ -284,9 +276,8 @@ local function set_camera_active(active) HideHudAndRadarThisFrame() if camera_state.front_camera then apply_front_camera(PlayerPedId()) - else - apply_rear_camera_view() end + apply_camera_view() Wait(0) end camera_state.enforcing = false @@ -321,8 +312,8 @@ local function set_front_camera(active) apply_front_camera(PlayerPedId()) else clear_front_camera() - apply_rear_camera_view() end + apply_camera_view() TriggerEvent("sky_phone:animation:camera", { active = true, front = camera_state.front_camera, diff --git a/sky_phone/source/client/focus.lua b/sky_phone/source/client/focus.lua index b8a8a3c..979af41 100644 --- a/sky_phone/source/client/focus.lua +++ b/sky_phone/source/client/focus.lua @@ -1,23 +1,34 @@ SkyPhoneFocus = {} local blocked_phone_controls = { 19, 24, 140, 141, 142, 257, 263, 264 } +local blocked_phone_look_controls = { 1, 2, 3, 4, 5, 6 } -function SkyPhoneFocus.ApplyGameInputControls() +function SkyPhoneFocus.ApplyFocusedControls() + DisableAllControlActions(0) + DisablePlayerFiring(PlayerId(), true) +end + +function SkyPhoneFocus.ApplyGameInputControls(block_look) for _, control in ipairs(blocked_phone_controls) do DisableControlAction(0, control, true) end + if block_look then + for _, control in ipairs(blocked_phone_look_controls) do + DisableControlAction(0, control, true) + end + end DisablePlayerFiring(PlayerId(), true) end function SkyPhoneFocus.Resolve(state) if state.activity_suspended then - return { cursor = false, focused = false, game_input = false, keep_input = false } + return { block_game = false, cursor = false, focused = false, game_input = false, keep_input = false } end if state.call_focus then - return { cursor = true, focused = true, game_input = false, keep_input = false } + return { block_game = true, cursor = true, focused = true, game_input = false, keep_input = false } end if state.camera_active and not state.camera_nui_focused then - return { cursor = false, focused = true, game_input = false, keep_input = true } + return { block_game = false, block_look = false, cursor = false, focused = true, game_input = true, keep_input = true } end local game_input = state.is_open and state.allow_movement and not state.camera_active local focused = state.is_open @@ -25,8 +36,11 @@ function SkyPhoneFocus.Resolve(state) or state.payphone_focus or state.sim_picker_open or (state.camera_active and state.camera_nui_focused) + local cursor = focused and not (game_input and state.cursor_disabled) return { - cursor = focused and not (game_input and state.cursor_disabled), + block_game = cursor, + block_look = game_input and not state.cursor_disabled, + cursor = cursor, focused = focused, game_input = game_input, keep_input = game_input, diff --git a/sky_phone/source/client/main.lua b/sky_phone/source/client/main.lua index d5267a7..efd9370 100644 --- a/sky_phone/source/client/main.lua +++ b/sky_phone/source/client/main.lua @@ -12,6 +12,8 @@ local active_call_payload = nil local call_channel = 0 local nui_generation = 0 local activity_suspended = false +local phone_block_game = false +local phone_block_look = false local phone_game_input = false local phone_cursor_disabled = false @@ -306,8 +308,10 @@ local function update_nui_focus() }) SetNuiFocus(focus.focused, focus.cursor) SetNuiFocusKeepInput(focus.keep_input) + phone_block_game = focus.block_game == true + phone_block_look = focus.block_look == true phone_game_input = focus.game_input - if not phone_game_input then + if not phone_game_input and not phone_block_game then phone_cursor_disabled = false end TriggerEvent("sky_phone:client:cameraFocusApplied", { @@ -320,8 +324,12 @@ end CreateThread(function() while true do - if phone_game_input then - SkyPhoneFocus.ApplyGameInputControls() + if phone_game_input or phone_block_game then + if phone_block_game then + SkyPhoneFocus.ApplyFocusedControls() + else + SkyPhoneFocus.ApplyGameInputControls(phone_block_look) + end if IsDisabledControlJustPressed(0, 19) then phone_cursor_disabled = not phone_cursor_disabled update_nui_focus() diff --git a/tests/client_camera.lua b/tests/client_camera.lua index 5e7a5b1..6f85e7e 100644 --- a/tests/client_camera.lua +++ b/tests/client_camera.lua @@ -113,11 +113,11 @@ assert(response_from("camera:setFacing", { front = true }).success) assert(camera_created and scripted_camera_rendering, "selfie mode must render a scripted camera") assert(camera_coord and camera_target, "selfie mode must position and aim the camera") assert(close_enough(camera_coord.x, 10.0)) -assert(close_enough(camera_coord.y, 21.45)) -assert(close_enough(camera_coord.z, 2.76)) +assert(close_enough(camera_coord.y, 21.05)) +assert(close_enough(camera_coord.z, 2.75)) assert(close_enough(camera_target.x, 10.0)) assert(close_enough(camera_target.y, 20.0)) -assert(close_enough(camera_target.z, 2.6)) +assert(close_enough(camera_target.z, 2.73)) assert(response_from("camera:setFacing", { front = false }).success) assert(camera_destroyed and not scripted_camera_rendering, "rear mode must release the selfie camera") diff --git a/tests/client_focus.lua b/tests/client_focus.lua index 21bd95a..be0b11c 100644 --- a/tests/client_focus.lua +++ b/tests/client_focus.lua @@ -1,4 +1,5 @@ local disabled_controls = {} +local all_controls_disabled = false local firing_disabled = false function DisableControlAction(group, control, disabled) @@ -6,6 +7,11 @@ function DisableControlAction(group, control, disabled) disabled_controls[control] = true end +function DisableAllControlActions(group) + assert(group == 0, "focused phone input must block the primary input group") + all_controls_disabled = true +end + function PlayerId() return 7 end @@ -53,7 +59,10 @@ assert( local stationary_phone = resolve({ is_open = true }) assert( - stationary_phone.cursor and stationary_phone.focused and not stationary_phone.keep_input, + stationary_phone.cursor + and stationary_phone.focused + and stationary_phone.block_game + and not stationary_phone.keep_input, "an open phone must block game input when movement is disabled" ) @@ -62,8 +71,9 @@ assert( movable_phone.cursor and movable_phone.focused and movable_phone.keep_input - and movable_phone.game_input, - "an open phone must keep GTA input when movement is enabled" + and movable_phone.game_input + and movable_phone.block_game, + "an open phone with the cursor active must block GTA hotkeys while keeping NUI input" ) local cursor_disabled_phone = resolve({ @@ -74,19 +84,35 @@ local cursor_disabled_phone = resolve({ assert( not cursor_disabled_phone.cursor and cursor_disabled_phone.focused - and cursor_disabled_phone.keep_input, + and cursor_disabled_phone.keep_input + and cursor_disabled_phone.game_input + and not cursor_disabled_phone.block_game, "toggling Alt must release the NUI cursor while preserving phone and GTA input" ) -SkyPhoneFocus.ApplyGameInputControls() +SkyPhoneFocus.ApplyFocusedControls() +assert(all_controls_disabled, "focused phone cursor must block every GTA control while typing") +assert(firing_disabled, "focused phone cursor must block attacks while typing") + +all_controls_disabled = false +firing_disabled = false +SkyPhoneFocus.ApplyGameInputControls(true) for _, control in ipairs({ 19, 24, 140, 141, 142, 257, 263, 264 }) do assert(disabled_controls[control], ("phone control %d must remain disabled"):format(control)) end -assert(not disabled_controls[1] and not disabled_controls[2], "look controls must stay enabled") +for _, control in ipairs({ 1, 2, 3, 4, 5, 6 }) do + assert(disabled_controls[control], ("look control %d must be disabled while the phone cursor is active"):format(control)) +end assert(not disabled_controls[21] and not disabled_controls[22], "sprint and jump must stay enabled") assert(not disabled_controls[30] and not disabled_controls[31], "movement axes must stay enabled") assert(firing_disabled, "player attacks must remain disabled while the phone is open") +disabled_controls = {} +firing_disabled = false +SkyPhoneFocus.ApplyGameInputControls(false) +assert(not disabled_controls[1] and not disabled_controls[2], "Alt cursor toggle must restore camera look") +assert(firing_disabled, "player attacks must remain disabled after the cursor is toggled off") + local movable_notification = resolve({ allow_movement = true, notification_focus = true }) assert( movable_notification.focused and not movable_notification.keep_input, @@ -102,8 +128,9 @@ assert( not camera_game_input.cursor and camera_game_input.focused and camera_game_input.keep_input - and not camera_game_input.game_input, - "camera movement must keep keyboard focus without retaining the NUI cursor" + and camera_game_input.game_input + and not camera_game_input.block_look, + "camera movement must keep keyboard focus and GTA movement without retaining the NUI cursor" ) local focused_camera = resolve({ @@ -113,7 +140,10 @@ local focused_camera = resolve({ is_open = true, }) assert( - focused_camera.cursor and focused_camera.focused and not focused_camera.keep_input, + focused_camera.cursor + and focused_camera.focused + and not focused_camera.keep_input + and not focused_camera.game_input, "focused camera must override movement configuration until Space enables passthrough" )