mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
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.
This commit is contained in:
@@ -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(')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
+39
-9
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user