mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 00:01:29 +00:00
FIX - stabilize camera movement and selfie view
This commit is contained in:
@@ -38,7 +38,16 @@ describe('Camera app controls', () => {
|
||||
it('keeps continuous wheel zoom without an extra slider bar', () => {
|
||||
expect(cameraView).not.toContain('camera-zoom-slider')
|
||||
expect(cameraView).not.toContain('type="range"')
|
||||
expect(cameraView).toContain('event.deltaY * 0.0025')
|
||||
expect(cameraView).toContain('@wheel.prevent.stop="zoomWithWheel"')
|
||||
expect(cameraView).toContain('Math.min(120, event.deltaY * deltaMultiplier)')
|
||||
expect(cameraView).toContain('wheelDelta * 0.00075')
|
||||
expect(cameraView).toContain("message.type === 'camera:zoom'")
|
||||
expect(cameraClient).toContain('mouse_wheel_zoom_step = 0.08')
|
||||
expect(cameraClient).toContain('INPUT_CURSOR_SCROLL_UP')
|
||||
expect(cameraClient).toContain('INPUT_CURSOR_SCROLL_DOWN')
|
||||
expect(cameraClient).toContain('IsDisabledControlJustPressed(0, 241)')
|
||||
expect(cameraClient).toContain('IsDisabledControlJustPressed(0, 242)')
|
||||
expect(cameraClient).toContain('type = "camera:zoom"')
|
||||
expect(mediaCapture).toContain('nextZoom < 0.5 || nextZoom > 3')
|
||||
expect(mediaCapture).not.toContain('[0.5, 1, 2, 3].includes(nextZoom)')
|
||||
})
|
||||
@@ -65,10 +74,36 @@ describe('Camera app controls', () => {
|
||||
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).not.toContain('next_view_apply')
|
||||
expect(cameraClient).not.toContain('ultrawide_camera_handle')
|
||||
expect(cameraClient).not.toContain('ensure_ultrawide_camera')
|
||||
})
|
||||
|
||||
it('keeps the selfie camera stable while Space still allows movement', () => {
|
||||
expect(cameraView).toMatch(
|
||||
/event\.code !== 'Space'[\s\S]*cameraLocked\.value/,
|
||||
)
|
||||
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('SetCamCoord(')
|
||||
expect(cameraClient).toContain('SetCamRot(')
|
||||
expect(cameraClient).not.toContain('SetEntityHeading(')
|
||||
expect(cameraClient).not.toContain('front_camera_position')
|
||||
expect(cameraClient).not.toContain('AttachCamToEntity(')
|
||||
expect(cameraClient).not.toContain('PointCamAtEntity(')
|
||||
expect(cameraClient).not.toContain('PointCamAtCoord(')
|
||||
expect(cameraView).toContain("window.addEventListener('keyup', onKeyup)")
|
||||
expect(cameraView).toContain(
|
||||
"nuiCall('camera:setFocus', { focused: true })",
|
||||
)
|
||||
})
|
||||
|
||||
it('uses a looping camera-hold pose instead of the old selfie dance', () => {
|
||||
expect(cameraConfig).toContain('Camera = "cellphone@self"')
|
||||
expect(cameraConfig).toContain('Camera = "selfie"')
|
||||
|
||||
@@ -50,8 +50,8 @@ const flashEnabled = ref(false)
|
||||
const microphoneEnabled = ref(true)
|
||||
const frontCamera = ref(false)
|
||||
const shutterActive = ref(false)
|
||||
const focused = ref(true)
|
||||
const cameraLocked = ref(false)
|
||||
const movementEnabled = ref(false)
|
||||
const recording = ref(false)
|
||||
const savingVideo = ref(false)
|
||||
const recordingStartedAt = ref(0)
|
||||
@@ -231,6 +231,10 @@ async function toggleFacing(): Promise<void> {
|
||||
|
||||
async function toggleCameraLock(): Promise<void> {
|
||||
cameraLocked.value = !cameraLocked.value
|
||||
if (cameraLocked.value && movementEnabled.value) {
|
||||
movementEnabled.value = false
|
||||
await nuiCall('camera:setFocus', { focused: true })
|
||||
}
|
||||
await nuiCall('camera:setLocked', { locked: cameraLocked.value })
|
||||
}
|
||||
|
||||
@@ -265,7 +269,17 @@ function setZoom(zoom: number): void {
|
||||
}
|
||||
|
||||
function zoomWithWheel(event: WheelEvent): void {
|
||||
setZoom(selectedZoom.value - event.deltaY * 0.0025)
|
||||
const deltaMultiplier =
|
||||
event.deltaMode === WheelEvent.DOM_DELTA_LINE
|
||||
? 16
|
||||
: event.deltaMode === WheelEvent.DOM_DELTA_PAGE
|
||||
? window.innerHeight
|
||||
: 1
|
||||
const wheelDelta = Math.max(
|
||||
-120,
|
||||
Math.min(120, event.deltaY * deltaMultiplier),
|
||||
)
|
||||
setZoom(selectedZoom.value - wheelDelta * 0.00075)
|
||||
}
|
||||
|
||||
function resizeGameView(entry?: ResizeObserverEntry): void {
|
||||
@@ -323,23 +337,34 @@ function onKeydown(event: KeyboardEvent): void {
|
||||
if (
|
||||
event.code !== 'Space' ||
|
||||
event.repeat ||
|
||||
!focused.value ||
|
||||
cameraLocked.value
|
||||
cameraLocked.value ||
|
||||
movementEnabled.value
|
||||
)
|
||||
return
|
||||
event.preventDefault()
|
||||
focused.value = false
|
||||
movementEnabled.value = true
|
||||
void nuiCall('camera:setFocus', { focused: false })
|
||||
}
|
||||
|
||||
function onKeyup(event: KeyboardEvent): void {
|
||||
if (event.code !== 'Space' || !movementEnabled.value) return
|
||||
event.preventDefault()
|
||||
movementEnabled.value = false
|
||||
void nuiCall('camera:setFocus', { focused: true })
|
||||
}
|
||||
|
||||
function onMessage(event: MessageEvent): void {
|
||||
if (!isTrustedRootMessageSource(event.source, window)) return
|
||||
const message = event.data as {
|
||||
data?: Record<string, unknown>
|
||||
type?: string
|
||||
}
|
||||
if (message.type === 'camera:focus') {
|
||||
focused.value = message.data?.focused === true
|
||||
if (message.type === 'camera:zoom') {
|
||||
const zoom = Number(message.data?.zoom)
|
||||
if (Number.isFinite(zoom) && zoom >= minimumZoom && zoom <= maximumZoom) {
|
||||
selectedZoom.value = zoom
|
||||
resizeGameView()
|
||||
}
|
||||
} else if (message.type === 'camera:recordState') {
|
||||
const active = message.data?.active === true
|
||||
savingVideo.value = message.data?.saving === true
|
||||
@@ -406,6 +431,7 @@ onMounted(() => {
|
||||
'*',
|
||||
)
|
||||
window.addEventListener('keydown', onKeydown)
|
||||
window.addEventListener('keyup', onKeyup)
|
||||
window.addEventListener('message', onMessage)
|
||||
void nuiCall('camera:setActive', { active: true })
|
||||
void nuiCall<{ videoBitrateKbps?: number }>('media:config').then(
|
||||
@@ -424,7 +450,12 @@ onBeforeUnmount(() => {
|
||||
if (noticeTimer !== undefined) window.clearTimeout(noticeTimer)
|
||||
if (recordingTimer !== undefined) window.clearInterval(recordingTimer)
|
||||
window.removeEventListener('keydown', onKeydown)
|
||||
window.removeEventListener('keyup', onKeyup)
|
||||
window.removeEventListener('message', onMessage)
|
||||
if (movementEnabled.value) {
|
||||
movementEnabled.value = false
|
||||
void nuiCall('camera:setFocus', { focused: true })
|
||||
}
|
||||
if (renderFrameId !== undefined) window.cancelAnimationFrame(renderFrameId)
|
||||
resizeObserver?.disconnect()
|
||||
gameView?.dispose()
|
||||
@@ -446,7 +477,7 @@ onBeforeUnmount(() => {
|
||||
:class="{ 'camera-page--landscape': phone.cameraLandscape }"
|
||||
:aria-label="phone.t('Apps.camera.name')"
|
||||
>
|
||||
<div class="camera-viewport" @wheel.prevent="zoomWithWheel">
|
||||
<div class="camera-viewport" @wheel.prevent.stop="zoomWithWheel">
|
||||
<canvas
|
||||
v-if="!isDevelopment && !gameViewUnavailable"
|
||||
ref="gameCanvas"
|
||||
|
||||
@@ -1,10 +1,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 = 40.0
|
||||
local front_camera_distance = 1.05
|
||||
local front_camera_side_offset = 0.08
|
||||
local front_camera_height = 0.12
|
||||
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 blocked_camera_controls = {
|
||||
0, -- INPUT_NEXT_CAMERA
|
||||
@@ -22,7 +23,11 @@ local blocked_camera_controls = {
|
||||
140, -- INPUT_MELEE_ATTACK_LIGHT
|
||||
141, -- INPUT_MELEE_ATTACK_HEAVY
|
||||
142, -- INPUT_MELEE_ATTACK_ALTERNATE
|
||||
241, -- INPUT_CURSOR_SCROLL_UP
|
||||
242, -- INPUT_CURSOR_SCROLL_DOWN
|
||||
257, -- INPUT_ATTACK2
|
||||
261, -- INPUT_PREV_WEAPON
|
||||
262, -- INPUT_NEXT_WEAPON
|
||||
263, -- INPUT_MELEE_ATTACK1
|
||||
264, -- INPUT_MELEE_ATTACK2
|
||||
}
|
||||
@@ -38,6 +43,9 @@ local camera_state = {
|
||||
focus_watcher = false,
|
||||
front_camera = false,
|
||||
front_camera_handle = nil,
|
||||
front_camera_offset = nil,
|
||||
front_camera_rotation = nil,
|
||||
game_input = false,
|
||||
landscape = false,
|
||||
locked = false,
|
||||
applied_nui_focus = true,
|
||||
@@ -77,29 +85,54 @@ local function set_flash_enabled(enabled)
|
||||
end)
|
||||
end
|
||||
|
||||
local function front_camera_position(ped)
|
||||
local head = GetPedBoneCoords(ped, 31086, 0.0, 0.0, 0.0)
|
||||
local function capture_front_camera_transform(ped)
|
||||
local ped_position = GetEntityCoords(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, forward.z)
|
||||
local forward_vector = vector3(forward.x, forward.y, 0.0)
|
||||
local right_vector = vector3(forward_vector.y, -forward_vector.x, 0.0)
|
||||
local camera_position = head
|
||||
+ (forward_vector * front_camera_distance)
|
||||
local camera_offset = (forward_vector * front_camera_distance)
|
||||
+ (right_vector * front_camera_side_offset)
|
||||
+ vector3(0.0, 0.0, front_camera_height)
|
||||
local target = head
|
||||
+ (right_vector * (front_camera_side_offset * 0.25))
|
||||
+ vector3(0.0, 0.0, front_camera_target_height)
|
||||
return camera_position, target
|
||||
+ 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)
|
||||
local direction = target_offset - camera_offset
|
||||
local horizontal_length = math.sqrt((direction.x * direction.x) + (direction.y * direction.y))
|
||||
local rotation = vector3(
|
||||
math.deg(math.atan(direction.z, horizontal_length)),
|
||||
0.0,
|
||||
math.deg(math.atan(-direction.x, direction.y))
|
||||
)
|
||||
return camera_offset, rotation
|
||||
end
|
||||
|
||||
local function ensure_front_camera()
|
||||
if camera_state.front_camera_handle and DoesCamExist(camera_state.front_camera_handle) then
|
||||
return
|
||||
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_rotation =
|
||||
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
|
||||
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)
|
||||
|
||||
local ped_position = GetEntityCoords(ped)
|
||||
local camera_position = ped_position + camera_state.front_camera_offset
|
||||
local rotation = camera_state.front_camera_rotation
|
||||
SetCamCoord(
|
||||
camera_state.front_camera_handle,
|
||||
camera_position.x,
|
||||
camera_position.y,
|
||||
camera_position.z
|
||||
)
|
||||
SetCamRot(
|
||||
camera_state.front_camera_handle,
|
||||
rotation.x,
|
||||
rotation.y,
|
||||
rotation.z,
|
||||
2
|
||||
)
|
||||
end
|
||||
|
||||
local function clear_front_camera()
|
||||
@@ -108,6 +141,8 @@ 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_rotation = nil
|
||||
end
|
||||
|
||||
local function apply_rear_camera_view()
|
||||
@@ -138,7 +173,7 @@ local function apply_camera_controls()
|
||||
for _, control in ipairs(blocked_camera_controls) do
|
||||
DisableControlAction(0, control, true)
|
||||
end
|
||||
if camera_state.locked then
|
||||
if camera_state.locked or camera_state.front_camera then
|
||||
for _, control in ipairs(camera_look_controls) do
|
||||
DisableControlAction(0, control, true)
|
||||
end
|
||||
@@ -155,6 +190,19 @@ end
|
||||
|
||||
local set_camera_focus
|
||||
|
||||
local function set_camera_zoom(zoom)
|
||||
if not zoom or zoom < minimum_zoom or zoom > maximum_zoom then
|
||||
return false
|
||||
end
|
||||
zoom = math.floor((zoom * 100.0) + 0.5) / 100.0
|
||||
if camera_state.zoom == zoom then
|
||||
return true
|
||||
end
|
||||
camera_state.zoom = zoom
|
||||
SendNUIMessage({ type = "camera:zoom", data = { zoom = zoom } })
|
||||
return true
|
||||
end
|
||||
|
||||
local function watch_camera_controls()
|
||||
if camera_state.focus_watcher then
|
||||
return
|
||||
@@ -163,8 +211,25 @@ local function watch_camera_controls()
|
||||
CreateThread(function()
|
||||
while camera_state.active do
|
||||
apply_camera_controls()
|
||||
if not camera_state.applied_nui_focus and IsDisabledControlJustReleased(0, 22) then
|
||||
set_camera_focus(true)
|
||||
if camera_state.game_input then
|
||||
if
|
||||
IsDisabledControlJustPressed(0, 241)
|
||||
or IsDisabledControlJustPressed(0, 261)
|
||||
then
|
||||
set_camera_zoom(
|
||||
math.min(maximum_zoom, camera_state.zoom + mouse_wheel_zoom_step)
|
||||
)
|
||||
elseif
|
||||
IsDisabledControlJustPressed(0, 242)
|
||||
or IsDisabledControlJustPressed(0, 262)
|
||||
then
|
||||
set_camera_zoom(
|
||||
math.max(minimum_zoom, camera_state.zoom - mouse_wheel_zoom_step)
|
||||
)
|
||||
end
|
||||
if IsDisabledControlJustReleased(0, 22) then
|
||||
set_camera_focus(true)
|
||||
end
|
||||
end
|
||||
Wait(0)
|
||||
end
|
||||
@@ -180,11 +245,13 @@ end
|
||||
AddEventHandler("sky_phone:client:cameraFocusApplied", function(data)
|
||||
if type(data) ~= "table"
|
||||
or type(data.active) ~= "boolean"
|
||||
or type(data.cursor) ~= "boolean"
|
||||
or type(data.focused) ~= "boolean"
|
||||
or type(data.gameInput) ~= "boolean"
|
||||
then
|
||||
return
|
||||
end
|
||||
camera_state.game_input = data.gameInput
|
||||
if camera_state.applied_nui_focus ~= data.focused then
|
||||
camera_state.applied_nui_focus = data.focused
|
||||
SendNUIMessage({ type = "camera:focus", data = { focused = data.focused } })
|
||||
@@ -221,26 +288,12 @@ local function set_camera_active(active)
|
||||
end
|
||||
camera_state.enforcing = true
|
||||
CreateThread(function()
|
||||
local next_view_apply = 0
|
||||
while camera_state.active do
|
||||
HideHudAndRadarThisFrame()
|
||||
if camera_state.front_camera then
|
||||
local ped = PlayerPedId()
|
||||
ensure_front_camera()
|
||||
local camera_position, target = front_camera_position(ped)
|
||||
SetCamCoord(
|
||||
camera_state.front_camera_handle,
|
||||
camera_position.x,
|
||||
camera_position.y,
|
||||
camera_position.z
|
||||
)
|
||||
PointCamAtCoord(camera_state.front_camera_handle, target.x, target.y, target.z)
|
||||
apply_front_camera(PlayerPedId())
|
||||
else
|
||||
local now = GetGameTimer()
|
||||
if now >= next_view_apply then
|
||||
apply_rear_camera_view()
|
||||
next_view_apply = now + 250
|
||||
end
|
||||
apply_rear_camera_view()
|
||||
end
|
||||
Wait(0)
|
||||
end
|
||||
@@ -250,6 +303,7 @@ local function set_camera_active(active)
|
||||
end
|
||||
set_flash_enabled(false)
|
||||
camera_state.front_camera = false
|
||||
camera_state.game_input = false
|
||||
camera_state.landscape = false
|
||||
camera_state.locked = false
|
||||
clear_front_camera()
|
||||
@@ -272,7 +326,7 @@ local function set_front_camera(active)
|
||||
return
|
||||
end
|
||||
if active then
|
||||
ensure_front_camera()
|
||||
apply_front_camera(PlayerPedId())
|
||||
else
|
||||
clear_front_camera()
|
||||
apply_rear_camera_view()
|
||||
@@ -298,15 +352,6 @@ local function set_camera_landscape(active)
|
||||
end
|
||||
end
|
||||
|
||||
local function set_camera_zoom(zoom)
|
||||
if not zoom or zoom < minimum_zoom or zoom > maximum_zoom then
|
||||
return false
|
||||
end
|
||||
zoom = math.floor((zoom * 100.0) + 0.5) / 100.0
|
||||
camera_state.zoom = zoom
|
||||
return true
|
||||
end
|
||||
|
||||
RegisterNUICallback("camera:setActive", function(data, cb)
|
||||
if type(data) ~= "table" then
|
||||
cb({ success = false, error = "invalid_request" })
|
||||
|
||||
@@ -2,20 +2,24 @@ SkyPhoneFocus = {}
|
||||
|
||||
function SkyPhoneFocus.Resolve(state)
|
||||
if state.activity_suspended then
|
||||
return { focused = false, keep_input = false }
|
||||
return { cursor = false, focused = false, keep_input = false, movement_only = false }
|
||||
end
|
||||
if state.call_focus then
|
||||
return { focused = true, keep_input = false }
|
||||
return { cursor = true, focused = true, keep_input = false, movement_only = false }
|
||||
end
|
||||
if state.camera_active and not state.camera_nui_focused then
|
||||
return { focused = false, keep_input = true }
|
||||
return { cursor = false, focused = true, keep_input = true, movement_only = false }
|
||||
end
|
||||
local movement_only = state.is_open and state.allow_movement and not state.camera_active
|
||||
local focused = state.is_open
|
||||
or state.notification_focus
|
||||
or state.payphone_focus
|
||||
or state.sim_picker_open
|
||||
or (state.camera_active and state.camera_nui_focused)
|
||||
return {
|
||||
focused = state.is_open
|
||||
or state.notification_focus
|
||||
or state.payphone_focus
|
||||
or state.sim_picker_open
|
||||
or (state.camera_active and state.camera_nui_focused),
|
||||
keep_input = state.is_open and state.allow_movement and not state.camera_active,
|
||||
cursor = focused,
|
||||
focused = focused,
|
||||
keep_input = movement_only,
|
||||
movement_only = movement_only,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -12,6 +12,7 @@ local active_call_payload = nil
|
||||
local call_channel = 0
|
||||
local nui_generation = 0
|
||||
local activity_suspended = false
|
||||
local movement_only_input = false
|
||||
|
||||
Bridge.Debug("debug", "[sky_phone] Client script initialized.", { always = true })
|
||||
|
||||
@@ -301,15 +302,28 @@ local function update_nui_focus()
|
||||
payphone_focus = payphone_focus,
|
||||
sim_picker_open = sim_picker_open,
|
||||
})
|
||||
SetNuiFocus(focus.focused, focus.focused)
|
||||
SetNuiFocus(focus.focused, focus.cursor)
|
||||
SetNuiFocusKeepInput(focus.keep_input)
|
||||
movement_only_input = focus.movement_only
|
||||
TriggerEvent("sky_phone:client:cameraFocusApplied", {
|
||||
active = camera_active,
|
||||
cursor = focus.cursor,
|
||||
focused = focus.focused,
|
||||
gameInput = focus.keep_input,
|
||||
})
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
if movement_only_input then
|
||||
SkyPhoneFocus.ApplyMovementOnlyControls()
|
||||
Wait(0)
|
||||
else
|
||||
Wait(250)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler("sky_phone:client:setSuspended", function(suspended)
|
||||
activity_suspended = suspended == true
|
||||
update_nui_focus()
|
||||
|
||||
+61
-13
@@ -1,3 +1,25 @@
|
||||
local disabled_control_group = nil
|
||||
local enabled_controls = {}
|
||||
local firing_disabled = false
|
||||
|
||||
function DisableAllControlActions(group)
|
||||
disabled_control_group = group
|
||||
end
|
||||
|
||||
function EnableControlAction(group, control, enabled)
|
||||
assert(group == 0 and enabled, "movement controls must be enabled in the primary input group")
|
||||
enabled_controls[control] = true
|
||||
end
|
||||
|
||||
function PlayerId()
|
||||
return 7
|
||||
end
|
||||
|
||||
function DisablePlayerFiring(player, disabled)
|
||||
assert(player == 7, "movement filtering must target the local player")
|
||||
firing_disabled = disabled
|
||||
end
|
||||
|
||||
dofile("sky_phone/source/client/focus.lua")
|
||||
|
||||
local function resolve(overrides)
|
||||
@@ -19,26 +41,45 @@ local function resolve(overrides)
|
||||
end
|
||||
|
||||
local idle = resolve()
|
||||
assert(not idle.focused and not idle.keep_input, "idle NUI must release focus and game input override")
|
||||
assert(
|
||||
not idle.cursor and not idle.focused and not idle.keep_input,
|
||||
"idle NUI must release focus and game input override"
|
||||
)
|
||||
|
||||
local minimized_call = resolve()
|
||||
assert(not minimized_call.focused, "a replayed call without an attention claim must stay unfocused")
|
||||
|
||||
local incoming_call = resolve({ call_focus = true })
|
||||
assert(incoming_call.focused and not incoming_call.keep_input, "incoming call attention must focus the NUI")
|
||||
assert(
|
||||
incoming_call.cursor and incoming_call.focused and not incoming_call.keep_input,
|
||||
"incoming call attention must focus the NUI"
|
||||
)
|
||||
|
||||
local stationary_phone = resolve({ is_open = true })
|
||||
assert(
|
||||
stationary_phone.focused and not stationary_phone.keep_input,
|
||||
stationary_phone.cursor and stationary_phone.focused and not stationary_phone.keep_input,
|
||||
"an open phone must block game input when movement is disabled"
|
||||
)
|
||||
|
||||
local movable_phone = resolve({ allow_movement = true, is_open = true })
|
||||
assert(
|
||||
movable_phone.focused and movable_phone.keep_input,
|
||||
"an open phone must keep game input when movement is enabled"
|
||||
movable_phone.cursor
|
||||
and movable_phone.focused
|
||||
and movable_phone.keep_input
|
||||
and movable_phone.movement_only,
|
||||
"an open phone must keep only movement input when movement is enabled"
|
||||
)
|
||||
|
||||
SkyPhoneFocus.ApplyMovementOnlyControls()
|
||||
assert(disabled_control_group == 0, "movement filtering must disable the primary input group")
|
||||
for _, control in ipairs({ 21, 30, 31, 32, 33, 34, 35 }) do
|
||||
assert(enabled_controls[control], ("movement control %d must stay enabled"):format(control))
|
||||
end
|
||||
assert(not enabled_controls[1] and not enabled_controls[2], "look controls must stay disabled")
|
||||
assert(not enabled_controls[24] and not enabled_controls[25], "combat controls must stay disabled")
|
||||
assert(not enabled_controls[22], "jump must stay disabled")
|
||||
assert(firing_disabled, "player firing must remain disabled while the phone is open")
|
||||
|
||||
local movable_notification = resolve({ allow_movement = true, notification_focus = true })
|
||||
assert(
|
||||
movable_notification.focused and not movable_notification.keep_input,
|
||||
@@ -51,8 +92,11 @@ local camera_game_input = resolve({
|
||||
is_open = true,
|
||||
})
|
||||
assert(
|
||||
not camera_game_input.focused and camera_game_input.keep_input,
|
||||
"unfocused camera must own game input over the open phone"
|
||||
not camera_game_input.cursor
|
||||
and camera_game_input.focused
|
||||
and camera_game_input.keep_input
|
||||
and not camera_game_input.movement_only,
|
||||
"camera movement must keep keyboard focus without retaining the NUI cursor"
|
||||
)
|
||||
|
||||
local focused_camera = resolve({
|
||||
@@ -62,8 +106,8 @@ local focused_camera = resolve({
|
||||
is_open = true,
|
||||
})
|
||||
assert(
|
||||
focused_camera.focused and not focused_camera.keep_input,
|
||||
"focused camera must override movement configuration until Space releases NUI focus"
|
||||
focused_camera.cursor and focused_camera.focused and not focused_camera.keep_input,
|
||||
"focused camera must override movement configuration until Space enables passthrough"
|
||||
)
|
||||
|
||||
local camera_interrupted_by_call = resolve({
|
||||
@@ -73,8 +117,10 @@ local camera_interrupted_by_call = resolve({
|
||||
is_open = true,
|
||||
})
|
||||
assert(
|
||||
camera_interrupted_by_call.focused and not camera_interrupted_by_call.keep_input,
|
||||
"incoming call attention must override unfocused camera input"
|
||||
camera_interrupted_by_call.cursor
|
||||
and camera_interrupted_by_call.focused
|
||||
and not camera_interrupted_by_call.keep_input,
|
||||
"incoming call attention must override camera passthrough input"
|
||||
)
|
||||
|
||||
local camera_after_connected_call = resolve({
|
||||
@@ -84,8 +130,10 @@ local camera_after_connected_call = resolve({
|
||||
is_open = true,
|
||||
})
|
||||
assert(
|
||||
not camera_after_connected_call.focused and camera_after_connected_call.keep_input,
|
||||
"connected call without an attention claim must restore unfocused camera input"
|
||||
not camera_after_connected_call.cursor
|
||||
and camera_after_connected_call.focused
|
||||
and camera_after_connected_call.keep_input,
|
||||
"connected call without an attention claim must restore camera movement input"
|
||||
)
|
||||
|
||||
local payphone_closed_behind_phone = resolve({ is_open = true, payphone_focus = false })
|
||||
|
||||
Reference in New Issue
Block a user