2026-04-14 17:41:39 +02:00

617 lines
20 KiB
Lua

local Utils = require 'client.utils'
local CameraManager = require 'client.minigame.camera_manager'
local AnimationController = require 'client.minigame.animation_controller'
local config = require 'config.config_c'
local ScrewGame = {}
ScrewGame.__index = ScrewGame
ScrewGame.activeInstance = nil
---@param entity number
---@return table
function ScrewGame.new(entity)
local self = setmetatable({}, ScrewGame)
self.entity = entity
self.isRunning = false
self.isInMinigame = false
self.cameraAlternate = false
self.inControlRange = false
self.camera = CameraManager.new(entity)
self.animator = AnimationController.new(cache.ped)
self.screwDriver = nil
self.screwObject = nil
return self
end
function ScrewGame:createObjects()
local coords = GetEntityCoords(self.entity)
lib.requestModel(config.toilet.screwDriverModel)
self.screwDriver = CreateObject(config.toilet.screwDriverModel, coords.x, coords.y, coords.z - 10, config.toilet.useNetworkedObjects, true, false)
if not DoesEntityExist(self.screwDriver) then
lib.print.error('[ScrewGame] Failed to create screwdriver object')
return false
end
SetEntityVisible(self.screwDriver, false, false)
SetModelAsNoLongerNeeded(config.toilet.screwDriverModel)
SetEntityCompletelyDisableCollision(self.screwDriver, false, true)
lib.print.debug('[ScrewGame] Created screwdriver object:', self.screwDriver)
lib.requestModel(config.toilet.screwModel)
self.screwObject = CreateObject(config.toilet.screwModel, coords.x, coords.y, coords.z - 10, config.toilet.useNetworkedObjects, true, false)
if not DoesEntityExist(self.screwObject) then
lib.print.error('[ScrewGame] Failed to create screw object')
return false
end
SetEntityVisible(self.screwObject, false, false)
SetModelAsNoLongerNeeded(config.toilet.screwModel)
SetEntityCompletelyDisableCollision(self.screwObject, false, true)
lib.print.debug('[ScrewGame] Created screw object:', self.screwObject)
return true
end
---@param screwIndex number
function ScrewGame:attachScrewdriver(screwIndex)
if not DoesEntityExist(self.screwDriver) then
lib.print.error('[ScrewGame] Cannot attach screwdriver - entity does not exist')
return
end
SetEntityVisible(self.screwDriver, true, false)
if screwIndex == 1 or screwIndex == 2 then
AttachEntityToEntity(self.screwDriver, cache.ped, GetPedBoneIndex(cache.ped, 18905),
0.13191204624002, 0.082048422891877, -0.00017189368813841,
158.93643577607, -28.807547138067, -8.8622696090118,
true, true, false, true, 1, true)
else
AttachEntityToEntity(self.screwDriver, cache.ped, GetPedBoneIndex(cache.ped, 57005),
0.14447676481041, 0.083777311695066, -0.030584143861719,
-8.9086513739403, 40.146033454762, 160.55496533153,
true, true, false, true, 1, true)
end
lib.print.debug('[ScrewGame] Attached screwdriver for screw #' .. screwIndex)
end
---@param screwIndex number
---@param progress number
---@param rotation number
function ScrewGame:positionScrew(screwIndex, progress, rotation)
if not DoesEntityExist(self.screwObject) then
lib.print.error('[ScrewGame] Cannot position screw - entity does not exist')
return
end
local screwOffset = config.toilet.screwOffsets[screwIndex]
if not screwOffset then
lib.print.error('[ScrewGame] Invalid screw offset index:', screwIndex)
return
end
local isLeftSide = (screwIndex == 1 or screwIndex == 2)
local entityRot = GetEntityRotation(self.entity)
local adjustedOffset = vec3(
screwOffset.screw.x + (isLeftSide and -progress or progress),
screwOffset.screw.y,
screwOffset.screw.z
)
local screwPos = GetOffsetFromEntityInWorldCoords(self.entity, adjustedOffset.x, adjustedOffset.y, adjustedOffset.z)
SetEntityCoords(self.screwObject, screwPos.x, screwPos.y, screwPos.z, false, false, false, false)
SetEntityRotation(self.screwObject, entityRot.x + rotation, entityRot.y + (isLeftSide and -90.0 or 90.0), entityRot.z, 2, true)
SetEntityVisible(self.screwObject, true, false)
end
function ScrewGame:switchToAlternateCamera()
if not self.isInMinigame or self.cameraAlternate then return end
self.cameraAlternate = true
self.camera:setCamera({
vec3(0.0, 0.0, 1.0),
vec3(0.0, 0.0, GetEntityHeading(self.entity) + 180.0)
}, true, true)
self.camera:activate(true, 300)
lib.print.debug('[ScrewGame] Switched to alternate camera')
end
function ScrewGame:switchToScrewCamera()
if not self.isInMinigame or not self.cameraAlternate then return end
self.cameraAlternate = false
local camOffset = config.toilet.screwOffsets[self.currentScrewIndex].cam
self.camera:setCamera(camOffset, true)
self.camera:activate(true, 300)
lib.print.debug('[ScrewGame] Switched back to screw camera')
end
---@param screwIndex number
---@return boolean
function ScrewGame:handleScrewMinigame(screwIndex)
local isLeftSide = (screwIndex == 1 or screwIndex == 2)
self.isInMinigame = true
self.currentScrewIndex = screwIndex
self.cameraAlternate = false
local camOffset = config.toilet.screwOffsets[screwIndex].cam
self.camera:setCamera(camOffset, false)
self.camera:activate(true, 1500)
self:positionScrew(screwIndex, 0, 0)
self:attachScrewdriver(screwIndex)
self.animator:playScrewAnim(screwIndex)
Wait(100)
local animRange = {start = 0.25, finish = 0.75}
self.inControlRange = false
local wasPlaying = false
local screwRotation = 0.0
local screwProgress = 0.0
SetEntityVisible(self.screwObject, true, false)
lib.print.debug('[ScrewGame] Starting minigame loop for screw #' .. screwIndex)
while self.isRunning do
local currentTime = self.animator:getCurrentTime()
local isInRange = self.animator:isInTimeRange(animRange.start, animRange.finish)
if IsControlJustPressed(0, 73) then
lib.print.debug('[ScrewGame] X pressed, cancelling')
SetNuiFocus(false, false)
self:cancelWithAnimation()
self.isRunning = false
return false
end
if isInRange and not self.inControlRange then
self.animator:setSpeed(0.0)
self.inControlRange = true
SetNuiFocus(false, true)
lib.print.debug('[ScrewGame] Entered control range, cursor enabled')
end
if self.inControlRange then
local isAimingAtScrew = Utils.IsCursorOnEntity(self.screwObject, 35.0)
local isHoldingLMB = IsControlPressed(0, 24) or IsDisabledControlPressed(0, 24)
if isAimingAtScrew then
SetEntityDrawOutline(self.screwObject, true)
SetEntityDrawOutlineColor(255, 255, 0, 255)
SetEntityDrawOutlineShader(1)
else
SetEntityDrawOutline(self.screwObject, false)
end
if isAimingAtScrew and isHoldingLMB then
if not wasPlaying then
self.animator:setSpeed(1.0)
SetEntityDrawOutlineColor(0, 255, 0, 255)
wasPlaying = true
lib.print.debug('[ScrewGame] Started unscrewing from progress:', screwProgress)
end
screwProgress = screwProgress + 0.0001
if isLeftSide then
screwRotation = screwRotation - 2.0
else
screwRotation = screwRotation + 2.0
end
self:positionScrew(screwIndex, screwProgress, screwRotation)
if currentTime >= animRange.finish then
lib.print.debug('[ScrewGame] Screw #' .. screwIndex .. ' fully unscrewed at progress:', screwProgress)
SetNuiFocus(false, false)
SetEntityDrawOutline(self.screwObject, false)
SetEntityVisible(self.screwObject, false, false)
if self.camera then
self.camera:destroy()
self.camera = nil
end
return true
end
else
if wasPlaying then
self.animator:setSpeed(0.0)
if isAimingAtScrew then
SetEntityDrawOutlineColor(255, 255, 0, 255)
end
wasPlaying = false
end
end
end
Wait(0)
end
SetNuiFocus(false, false)
SetEntityDrawOutline(self.screwObject, false)
self.isInMinigame = false
if self.camera then
self.camera:destroy()
self.camera = nil
end
return false
end
function ScrewGame:selectScrew(unscrewed)
local availableScrews = {}
for i = 1, 4 do
if not lib.table.contains(unscrewed, i) then
table.insert(availableScrews, i)
end
end
if #availableScrews == 0 then
return nil
end
local currentIndex = 1
local nuiData = {}
for i = 1, 4 do
table.insert(nuiData, {
id = i,
label = locale('select_screw.options.' .. i),
available = lib.table.contains(availableScrews, i)
})
end
SendNUIMessage({
action = 'open',
data = {
screws = nuiData,
location = config.uiLocation,
locales = {
title = locale('select_screw.title'),
select = locale('select_screw.select'),
cancel = locale('select_screw.cancel'),
change_selection = locale('select_screw.change_selection'),
}
}
})
local selecting = true
local selectedScrew = nil
Wait(100)
while selecting do
if IsControlJustPressed(0, 241) or IsControlJustPressed(0, 172) then -- scroll up or arrow up
currentIndex = currentIndex - 1
if currentIndex < 1 then
currentIndex = #availableScrews
end
SendNUIMessage({
action = 'selectScrew',
data = availableScrews[currentIndex]
})
lib.print.debug('[ScrewGame] Scrolled to screw #' .. availableScrews[currentIndex])
end
if IsControlJustPressed(0, 242) or IsControlJustPressed(0, 173) then -- scroll down or arrow down
currentIndex = currentIndex + 1
if currentIndex > #availableScrews then
currentIndex = 1
end
SendNUIMessage({
action = 'selectScrew',
data = availableScrews[currentIndex]
})
lib.print.debug('[ScrewGame] Scrolled to screw #' .. availableScrews[currentIndex])
end
if IsControlJustPressed(0, 38) or IsControlJustPressed(0, 191) then -- E or enter
selectedScrew = availableScrews[currentIndex]
selecting = false
SendNUIMessage({ action = 'close' })
SetNuiFocus(false, false)
lib.print.debug('[ScrewGame] Confirmed screw #' .. selectedScrew)
end
if IsControlJustPressed(0, 73) or IsControlJustPressed(0, 194) then -- X or backspace
selecting = false
SendNUIMessage({ action = 'close' })
SetNuiFocus(false, false)
lib.print.debug('[ScrewGame] Cancelled screw selection')
end
Wait(0)
end
return selectedScrew
end
local keybind = lib.addKeybind({
name = 'prison_cam_switch',
description = 'Switch Camera View',
defaultKey = 'R',
onPressed = function(self)
if ScrewGame.activeInstance then
ScrewGame.activeInstance:switchToAlternateCamera()
end
end,
onReleased = function(self)
if ScrewGame.activeInstance then
ScrewGame.activeInstance:switchToScrewCamera()
end
end
})
function ScrewGame:start()
lib.print.debug('[ScrewGame] Starting screw minigame')
self.isRunning = true
ScrewGame.activeInstance = self
if not self:createObjects() then
lib.print.error('[ScrewGame] Failed to create objects, aborting')
self:cleanup()
return
end
local offset = GetOffsetFromEntityInWorldCoords(self.entity, 0.0, -0.5, 0.55)
SetEntityCoordsNoOffset(cache.ped, offset.x, offset.y, offset.z, false, false, false)
SetEntityHeading(cache.ped, GetEntityHeading(self.entity))
local totalDuration = GetAnimDuration('promt@jail_3', 'jail_enter_ped') * 1000
lib.playAnim(cache.ped, 'promt@jail_3', 'jail_enter_ped', 8.0, -8.0, -1, 2)
Wait(2000)
self:attachScrewdriver(1)
Wait(totalDuration - 2000)
AttachEntityToEntity(cache.ped, self.entity, 0, 0.0, 0.0, 0.55, 0.0, 0.0, 0.0, false, false, true, false, 2, true)
local allCompleted = false
local unscrewed = lib.callback.await('prompt_prison_escape:getUnscrewedState', 500) or {}
lib.print.debug('[ScrewGame] Loaded unscrewed state from server:', json.encode(unscrewed))
while self.isRunning and #unscrewed < 4 do
local screwIndex = self:selectScrew(unscrewed)
if not screwIndex then
lib.print.debug('[ScrewGame] User cancelled screw selection')
self:cancelWithAnimation()
self.isRunning = false
break
end
lib.print.debug('[ScrewGame] Selected screw #' .. screwIndex .. ', already unscrewed:', json.encode(unscrewed))
AttachEntityToEntity(cache.ped, self.entity, 0, 0.0, 0.0, 0.55, 0.0, 0.0, -180.0, false, false, true, false, 2, true)
SendNUIMessage({
action = 'setHint',
data = locale('screw_minigame.hint')
})
local completed = self:handleScrewMinigame(screwIndex)
SendNUIMessage({
action = 'setHint',
})
SetNuiFocus(false, false)
if self.camera then
self.camera:destroy()
self.camera = nil
end
if not completed then
break
end
local saved = lib.callback.await('prompt_prison_escape:unscrewProgress', 500, screwIndex)
if not saved then
lib.print.error('[ScrewGame] Failed to save screw progress on server')
break
end
table.insert(unscrewed, screwIndex)
lib.print.debug('[ScrewGame] Screw #' .. screwIndex .. ' completed. Total:', #unscrewed .. '/4')
if #unscrewed < 4 then
self.camera = CameraManager.new(self.entity)
end
end
if #unscrewed >= 4 then
if self.screwObject and DoesEntityExist(self.screwObject) then
SetEntityDrawOutline(self.screwObject, false)
end
if self.screwDriver and DoesEntityExist(self.screwDriver) then
DeleteEntity(self.screwDriver)
self.screwDriver = nil
end
config.textUI.show(locale('screw_minigame.instructions_escape'))
while self.isRunning do
if IsControlJustPressed(0, 38) then
config.textUI.hide()
lib.print.debug('[ScrewGame] Starting exit sequence')
self:playExitSequence()
allCompleted = true
break
elseif IsControlJustPressed(0, 73) then
config.textUI.hide()
lib.print.warn('[ScrewGame] User cancelled escape')
self:cancelWithAnimation()
self.isRunning = false
break
end
Wait(0)
end
else
if self.screwObject and DoesEntityExist(self.screwObject) then
SetEntityDrawOutline(self.screwObject, false)
end
if self.screwDriver and DoesEntityExist(self.screwDriver) then
DeleteEntity(self.screwDriver)
self.screwDriver = nil
end
end
if allCompleted then
lib.callback.await('prompt_prison_escape:completeEscape', 500)
else
lib.callback.await('prompt_prison_escape:stopSitting', 500)
end
self:cleanup()
lib.print.debug('[ScrewGame] Cleaned up and finished')
end
function ScrewGame:playExitSequence()
SetNuiFocus(false, false)
if self.camera then
self.camera:destroy()
self.camera = nil
end
DetachEntity(cache.ped, true, false)
local entHeading = GetEntityHeading(self.entity)
local entityStartCoords = GetEntityCoords(self.entity)
local animOffset = GetOffsetFromEntityInWorldCoords(self.entity, 0.0, 0.0, 0.55)
SetEntityCoordsNoOffset(cache.ped, animOffset.x, animOffset.y, animOffset.z, false, false, false)
SetEntityHeading(cache.ped, entHeading - 173.0)
lib.playAnim(cache.ped, 'promt@jail_4', 'jail_exit_open_ped')
Wait(4000)
local targetPos = GetOffsetFromEntityInWorldCoords(self.entity, -0.5, 0.0, 0.0)
local totalOffsetX = targetPos.x - entityStartCoords.x
local totalOffsetY = targetPos.y - entityStartCoords.y
local duration = 2000
local stepTime = 25
local steps = duration / stepTime
for i = 1, steps do
local progress = i / steps
SetEntityCoords(self.entity,
entityStartCoords.x + (totalOffsetX * progress),
entityStartCoords.y + (totalOffsetY * progress),
entityStartCoords.z,
false, false, false)
Wait(stepTime)
end
local targetExitPos = GetOffsetFromEntityInWorldCoords(self.entity, 0.5, 1.5, 0.0)
Wait(GetAnimDuration('promt@jail_4', 'jail_exit_open_ped') * 1000 - 7000)
targetPos = GetOffsetFromEntityInWorldCoords(self.entity, 0.5, -1.0, 0.0)
SetEntityCoordsNoOffset(cache.ped, targetPos.x, targetPos.y, targetPos.z, false, false, false)
lib.playAnim(cache.ped, 'promt@jail_5', 'jail_exit_ped')
SetEntityHeading(cache.ped, entHeading)
SetEntityCollision(self.entity, false, false)
Wait(GetAnimDuration('promt@jail_5', 'jail_exit_ped') * 1000)
SetEntityCoordsNoOffset(cache.ped, targetExitPos.x, targetExitPos.y, targetExitPos.z, false, false, false)
SetEntityHeading(cache.ped, entHeading + 180.0)
SetEntityCollision(self.entity, true, true)
local totalCloseDuration = GetAnimDuration('prompt@jail', 'jail_exit_close') * 1000
lib.playAnim(cache.ped, 'prompt@jail', 'jail_exit_close')
Wait(2500)
local currentEntityCoords = GetEntityCoords(self.entity)
local targetPos = GetOffsetFromEntityInWorldCoords(self.entity, 0.5, 0.0, 0.0)
local totalOffsetX = targetPos.x - currentEntityCoords.x
local totalOffsetY = targetPos.y - currentEntityCoords.y
local duration = 2000
local stepTime = 25
local steps = duration / stepTime
for i = 1, steps do
local progress = i / steps
SetEntityCoords(self.entity,
currentEntityCoords.x + (totalOffsetX * progress),
currentEntityCoords.y + (totalOffsetY * progress),
currentEntityCoords.z,
false, false, false)
Wait(stepTime)
end
Wait(totalCloseDuration - 2500 - 2000)
end
function ScrewGame:cancelWithAnimation()
SetNuiFocus(false, false)
if self.camera then
self.camera:destroy()
self.camera = nil
end
DetachEntity(cache.ped, true, false)
local entHeading = GetEntityHeading(self.entity)
local animOffset = GetOffsetFromEntityInWorldCoords(self.entity, 0.0, 0.0, 0.55)
SetEntityCoordsNoOffset(cache.ped, animOffset.x, animOffset.y, animOffset.z, false, false, false)
SetEntityHeading(cache.ped, entHeading - 173.0)
local totalDuration = GetAnimDuration('promt@jail_2', 'jail_enter_off_ped') * 1000
lib.playAnim(cache.ped, 'promt@jail_2', 'jail_enter_off_ped')
Wait(1000)
if self.screwDriver and DoesEntityExist(self.screwDriver) then
DeleteEntity(self.screwDriver)
self.screwDriver = nil
end
Wait(totalDuration - 1000)
end
function ScrewGame:stop()
self.isRunning = false
end
function ScrewGame:cleanup()
self.isRunning = false
self.isInMinigame = false
if ScrewGame.activeInstance == self then
ScrewGame.activeInstance = nil
end
SetNuiFocus(false, false)
if self.screwDriver and DoesEntityExist(self.screwDriver) then
DeleteEntity(self.screwDriver)
self.screwDriver = nil
end
if self.screwObject and DoesEntityExist(self.screwObject) then
SetEntityDrawOutline(self.screwObject, false)
DeleteEntity(self.screwObject)
self.screwObject = nil
end
if self.camera then
self.camera:destroy()
self.camera = nil
end
DetachEntity(cache.ped, true, false)
if self.animator then
self.animator:stop()
self.animator = nil
end
end
return ScrewGame