789 lines
26 KiB
Lua
Raw Normal View History

2025-04-05 22:25:06 +00:00
local QBCore = exports['qb-core']:GetCoreObject()
local UseTarget = GetConvar('UseTarget', 'false') == 'true'
local InApartment = false
local ClosestHouse = nil
local CurrentApartment = nil
local IsOwned = false
local CurrentDoorBell = 0
local CurrentOffset = 0
local HouseObj = {}
local POIOffsets = nil
local RangDoorbell = nil
-- target variables
local InApartmentTargets = {}
-- polyzone variables
local IsInsideEntranceZone = false
local IsInsideExitZone = false
local IsInsideStashZone = false
local IsInsideOutfitsZone = false
local IsInsideLogoutZone = false
-- polyzone integration
local function OpenEntranceMenu()
local headerMenu = {}
if IsOwned then
headerMenu[#headerMenu + 1] = {
header = Lang:t('text.enter'),
params = {
event = 'apartments:client:EnterApartment',
args = {}
}
}
elseif not IsOwned then
headerMenu[#headerMenu + 1] = {
header = Lang:t('text.move_here'),
params = {
event = 'apartments:client:UpdateApartment',
args = {}
}
}
end
headerMenu[#headerMenu + 1] = {
header = Lang:t('text.ring_doorbell'),
params = {
event = 'apartments:client:DoorbellMenu',
args = {}
}
}
headerMenu[#headerMenu + 1] = {
header = Lang:t('text.close_menu'),
txt = '',
params = {
event = 'qb-menu:client:closeMenu'
}
}
exports['qb-menu']:openMenu(headerMenu)
end
local function OpenExitMenu()
local headerMenu = {}
headerMenu[#headerMenu + 1] = {
header = Lang:t('text.open_door'),
params = {
event = 'apartments:client:OpenDoor',
args = {}
}
}
headerMenu[#headerMenu + 1] = {
header = Lang:t('text.leave'),
params = {
event = 'apartments:client:LeaveApartment',
args = {}
}
}
headerMenu[#headerMenu + 1] = {
header = Lang:t('text.close_menu'),
txt = '',
params = {
event = 'qb-menu:client:closeMenu'
}
}
exports['qb-menu']:openMenu(headerMenu)
end
-- exterior entrance (polyzone)
local function RegisterApartmentEntranceZone(apartmentID, apartmentData)
local coords = apartmentData.coords['enter']
local boxName = 'apartmentEntrance_' .. apartmentID
local boxData = apartmentData.polyzoneBoxData
if boxData.created then
return
end
local zone = BoxZone:Create(coords, boxData.length, boxData.width, {
name = boxName,
heading = 340.0,
minZ = coords.z - 1.0,
maxZ = coords.z + 5.0,
debugPoly = false
})
zone:onPlayerInOut(function(isPointInside)
if isPointInside and not InApartment then
exports['qb-core']:DrawText(Lang:t('text.options'), 'left')
else
exports['qb-core']:HideText()
end
IsInsideEntranceZone = isPointInside
end)
boxData.created = true
boxData.zone = zone
end
-- exterior entrance (target)
local function RegisterApartmentEntranceTarget(apartmentID, apartmentData)
local coords = apartmentData.coords['enter']
local boxName = 'apartmentEntrance_' .. apartmentID
local boxData = apartmentData.polyzoneBoxData
if boxData.created then
return
end
local options = {}
if apartmentID == ClosestHouse and IsOwned then
options = {
{
type = 'client',
event = 'apartments:client:EnterApartment',
icon = 'fas fa-door-open',
label = Lang:t('text.enter'),
},
}
else
options = {
{
type = 'client',
event = 'apartments:client:UpdateApartment',
icon = 'fas fa-hotel',
label = Lang:t('text.move_here'),
}
}
end
options[#options + 1] = {
type = 'client',
event = 'apartments:client:DoorbellMenu',
icon = 'fas fa-concierge-bell',
label = Lang:t('text.ring_doorbell'),
}
exports['qb-target']:AddBoxZone(boxName, coords, boxData.length, boxData.width, {
name = boxName,
heading = boxData.heading,
debugPoly = boxData.debug,
minZ = boxData.minZ,
maxZ = boxData.maxZ,
}, {
options = options,
distance = boxData.distance
})
boxData.created = true
end
-- interior interactable points (polyzone)
local function RegisterInApartmentZone(targetKey, coords, heading, text)
if not InApartment then
return
end
if InApartmentTargets[targetKey] and InApartmentTargets[targetKey].created then
return
end
Wait(1500)
local boxName = 'inApartmentTarget_' .. targetKey
local zone = BoxZone:Create(coords, 1.5, 1.5, {
name = boxName,
heading = heading,
minZ = coords.z - 1.0,
maxZ = coords.z + 5.0,
debugPoly = false
})
zone:onPlayerInOut(function(isPointInside)
if isPointInside and text then
exports['qb-core']:DrawText(text, 'left')
else
exports['qb-core']:HideText()
end
if targetKey == 'entrancePos' then
IsInsideExitZone = isPointInside
end
if targetKey == 'stashPos' then
IsInsideStashZone = isPointInside
end
if targetKey == 'outfitsPos' then
IsInsideOutfitsZone = isPointInside
end
if targetKey == 'logoutPos' then
IsInsideLogoutZone = isPointInside
end
end)
InApartmentTargets[targetKey] = InApartmentTargets[targetKey] or {}
InApartmentTargets[targetKey].created = true
InApartmentTargets[targetKey].zone = zone
end
-- interior interactable points (target)
local function RegisterInApartmentTarget(targetKey, coords, heading, options)
if not InApartment then
return
end
if InApartmentTargets[targetKey] and InApartmentTargets[targetKey].created then
return
end
local boxName = 'inApartmentTarget_' .. targetKey
exports['qb-target']:AddBoxZone(boxName, coords, 1.5, 1.5, {
name = boxName,
heading = heading,
minZ = coords.z - 1.0,
maxZ = coords.z + 5.0,
debugPoly = false,
}, {
options = options,
distance = 1
})
InApartmentTargets[targetKey] = InApartmentTargets[targetKey] or {}
InApartmentTargets[targetKey].created = true
end
-- shared
local function SetApartmentsEntranceTargets()
if Apartments.Locations and next(Apartments.Locations) then
for id, apartment in pairs(Apartments.Locations) do
if apartment and apartment.coords and apartment.coords['enter'] then
if UseTarget then
RegisterApartmentEntranceTarget(id, apartment)
else
RegisterApartmentEntranceZone(id, apartment)
end
end
end
end
end
local function SetInApartmentTargets()
if not POIOffsets then
-- do nothing
return
end
local entrancePos = vector3(Apartments.Locations[ClosestHouse].coords.enter.x + POIOffsets.exit.x, Apartments.Locations[ClosestHouse].coords.enter.y + POIOffsets.exit.y, Apartments.Locations[ClosestHouse].coords.enter.z - CurrentOffset + POIOffsets.exit.z)
local stashPos = vector3(Apartments.Locations[ClosestHouse].coords.enter.x - POIOffsets.stash.x, Apartments.Locations[ClosestHouse].coords.enter.y - POIOffsets.stash.y, Apartments.Locations[ClosestHouse].coords.enter.z - CurrentOffset + POIOffsets.stash.z)
local outfitsPos = vector3(Apartments.Locations[ClosestHouse].coords.enter.x - POIOffsets.clothes.x, Apartments.Locations[ClosestHouse].coords.enter.y - POIOffsets.clothes.y, Apartments.Locations[ClosestHouse].coords.enter.z - CurrentOffset + POIOffsets.clothes.z)
local logoutPos = vector3(Apartments.Locations[ClosestHouse].coords.enter.x - POIOffsets.logout.x, Apartments.Locations[ClosestHouse].coords.enter.y + POIOffsets.logout.y, Apartments.Locations[ClosestHouse].coords.enter.z - CurrentOffset + POIOffsets.logout.z)
if UseTarget then
RegisterInApartmentTarget('entrancePos', entrancePos, 0, {
{
type = 'client',
event = 'apartments:client:OpenDoor',
icon = 'fas fa-door-open',
label = Lang:t('text.open_door'),
},
{
type = 'client',
event = 'apartments:client:LeaveApartment',
icon = 'fas fa-door-open',
label = Lang:t('text.leave'),
},
})
RegisterInApartmentTarget('stashPos', stashPos, 0, {
{
type = 'client',
event = 'apartments:client:OpenStash',
icon = 'fas fa-box-open',
label = Lang:t('text.open_stash'),
},
})
RegisterInApartmentTarget('outfitsPos', outfitsPos, 0, {
{
type = 'client',
event = 'apartments:client:ChangeOutfit',
icon = 'fas fa-tshirt',
label = Lang:t('text.change_outfit'),
},
})
RegisterInApartmentTarget('logoutPos', logoutPos, 0, {
{
type = 'client',
event = 'apartments:client:Logout',
icon = 'fas fa-sign-out-alt',
label = Lang:t('text.logout'),
},
})
else
RegisterInApartmentZone('stashPos', stashPos, 0, '[E] ' .. Lang:t('text.open_stash'))
RegisterInApartmentZone('outfitsPos', outfitsPos, 0, '[E] ' .. Lang:t('text.change_outfit'))
RegisterInApartmentZone('logoutPos', logoutPos, 0, '[E] ' .. Lang:t('text.logout'))
RegisterInApartmentZone('entrancePos', entrancePos, 0, Lang:t('text.options'))
end
end
local function DeleteApartmentsEntranceTargets()
if Apartments.Locations and next(Apartments.Locations) then
for id, apartment in pairs(Apartments.Locations) do
if UseTarget then
exports['qb-target']:RemoveZone('apartmentEntrance_' .. id)
else
if apartment.polyzoneBoxData.zone then
apartment.polyzoneBoxData.zone:destroy()
apartment.polyzoneBoxData.zone = nil
end
end
apartment.polyzoneBoxData.created = false
end
end
end
local function DeleteInApartmentTargets()
IsInsideExitZone = false
IsInsideStashZone = false
IsInsideOutfitsZone = false
IsInsideLogoutZone = false
if InApartmentTargets and next(InApartmentTargets) then
for id, apartmentTarget in pairs(InApartmentTargets) do
if UseTarget then
exports['qb-target']:RemoveZone('inApartmentTarget_' .. id)
else
if apartmentTarget.zone then
apartmentTarget.zone:destroy()
apartmentTarget.zone = nil
end
end
end
end
InApartmentTargets = {}
end
-- utility functions
local function loadAnimDict(dict)
while (not HasAnimDictLoaded(dict)) do
RequestAnimDict(dict)
Wait(5)
end
end
local function openHouseAnim()
loadAnimDict('anim@heists@keycard@')
TaskPlayAnim(PlayerPedId(), 'anim@heists@keycard@', 'exit', 5.0, 1.0, -1, 16, 0, 0, 0, 0)
Wait(400)
ClearPedTasks(PlayerPedId())
end
local function EnterApartment(house, apartmentId, new)
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_open', 0.1)
openHouseAnim()
Wait(250)
QBCore.Functions.TriggerCallback('apartments:GetApartmentOffset', function(offset)
if offset == nil or offset == 0 then
QBCore.Functions.TriggerCallback('apartments:GetApartmentOffsetNewOffset', function(newoffset)
if newoffset > 230 then
newoffset = 210
end
CurrentOffset = newoffset
TriggerServerEvent('apartments:server:AddObject', apartmentId, house, CurrentOffset)
local coords = { x = Apartments.Locations[house].coords.enter.x, y = Apartments.Locations[house].coords.enter.y, z = Apartments.Locations[house].coords.enter.z - CurrentOffset }
local data = exports['qb-interior']:CreateApartmentFurnished(coords)
Wait(100)
HouseObj = data[1]
POIOffsets = data[2]
InApartment = true
CurrentApartment = apartmentId
ClosestHouse = house
RangDoorbell = nil
Wait(500)
TriggerEvent('qb-weathersync:client:DisableSync')
Wait(100)
TriggerServerEvent('qb-apartments:server:SetInsideMeta', house, apartmentId, true, false)
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_close', 0.1)
TriggerServerEvent('apartments:server:setCurrentApartment', CurrentApartment)
end, house)
else
if offset > 230 then
offset = 210
end
CurrentOffset = offset
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_open', 0.1)
TriggerServerEvent('apartments:server:AddObject', apartmentId, house, CurrentOffset)
local coords = { x = Apartments.Locations[ClosestHouse].coords.enter.x, y = Apartments.Locations[ClosestHouse].coords.enter.y, z = Apartments.Locations[ClosestHouse].coords.enter.z - CurrentOffset }
local data = exports['qb-interior']:CreateApartmentFurnished(coords)
Wait(100)
HouseObj = data[1]
POIOffsets = data[2]
InApartment = true
CurrentApartment = apartmentId
Wait(500)
TriggerEvent('qb-weathersync:client:DisableSync')
Wait(100)
TriggerServerEvent('qb-apartments:server:SetInsideMeta', house, apartmentId, true, true)
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_close', 0.1)
TriggerServerEvent('apartments:server:setCurrentApartment', CurrentApartment)
end
if new ~= nil then
if new then
TriggerEvent('qb-interior:client:SetNewState', true)
else
TriggerEvent('qb-interior:client:SetNewState', false)
end
else
TriggerEvent('qb-interior:client:SetNewState', false)
end
end, apartmentId)
end
local function LeaveApartment(house)
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_open', 0.1)
openHouseAnim()
TriggerServerEvent('qb-apartments:returnBucket')
DoScreenFadeOut(500)
while not IsScreenFadedOut() do Wait(10) end
exports['qb-interior']:DespawnInterior(HouseObj, function()
TriggerEvent('qb-weathersync:client:EnableSync')
SetEntityCoords(PlayerPedId(), Apartments.Locations[house].coords.enter.x, Apartments.Locations[house].coords.enter.y, Apartments.Locations[house].coords.enter.z)
SetEntityHeading(PlayerPedId(), Apartments.Locations[house].coords.enter.w)
Wait(1000)
TriggerServerEvent('apartments:server:RemoveObject', CurrentApartment, house)
TriggerServerEvent('qb-apartments:server:SetInsideMeta', CurrentApartment, false)
CurrentApartment = nil
InApartment = false
CurrentOffset = 0
DoScreenFadeIn(1000)
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'houses_door_close', 0.1)
TriggerServerEvent('apartments:server:setCurrentApartment', nil)
DeleteInApartmentTargets()
DeleteApartmentsEntranceTargets()
end)
end
local function SetClosestApartment()
local pos = GetEntityCoords(PlayerPedId())
local current = nil
local dist = 100
for id, _ in pairs(Apartments.Locations) do
local distcheck = #(pos - vector3(Apartments.Locations[id].coords.enter.x, Apartments.Locations[id].coords.enter.y, Apartments.Locations[id].coords.enter.z))
if distcheck < dist then
current = id
end
end
if current ~= ClosestHouse and LocalPlayer.state.isLoggedIn and not InApartment then
ClosestHouse = current
QBCore.Functions.TriggerCallback('apartments:IsOwner', function(result)
IsOwned = result
DeleteApartmentsEntranceTargets()
DeleteInApartmentTargets()
end, ClosestHouse)
end
end
function MenuOwners()
QBCore.Functions.TriggerCallback('apartments:GetAvailableApartments', function(apartments)
if next(apartments) == nil then
QBCore.Functions.Notify(Lang:t('error.nobody_home'), 'error', 3500)
CloseMenuFull()
else
local apartmentMenu = {
{
header = Lang:t('text.tennants'),
isMenuHeader = true
}
}
for k, v in pairs(apartments) do
apartmentMenu[#apartmentMenu + 1] = {
header = v,
txt = '',
params = {
event = 'apartments:client:RingMenu',
args = {
apartmentId = k
}
}
}
end
apartmentMenu[#apartmentMenu + 1] = {
header = Lang:t('text.close_menu'),
txt = '',
params = {
event = 'qb-menu:client:closeMenu'
}
}
exports['qb-menu']:openMenu(apartmentMenu)
end
end, ClosestHouse)
end
function CloseMenuFull()
exports['qb-menu']:closeMenu()
end
-- Event Handlers
AddEventHandler('onResourceStop', function(resource)
if resource == GetCurrentResourceName() then
if HouseObj ~= nil then
exports['qb-interior']:DespawnInterior(HouseObj, function()
CurrentApartment = nil
TriggerEvent('qb-weathersync:client:EnableSync')
DoScreenFadeIn(500)
while not IsScreenFadedOut() do
Wait(10)
end
SetEntityCoords(PlayerPedId(), Apartments.Locations[ClosestHouse].coords.enter.x, Apartments.Locations[ClosestHouse].coords.enter.y, Apartments.Locations[ClosestHouse].coords.enter.z)
SetEntityHeading(PlayerPedId(), Apartments.Locations[ClosestHouse].coords.enter.w)
Wait(1000)
InApartment = false
DoScreenFadeIn(1000)
end)
end
DeleteApartmentsEntranceTargets()
DeleteInApartmentTargets()
end
end)
-- Events
RegisterNetEvent('QBCore:Client:OnPlayerUnload', function()
CurrentApartment = nil
InApartment = false
CurrentOffset = 0
DeleteApartmentsEntranceTargets()
DeleteInApartmentTargets()
end)
RegisterNetEvent('apartments:client:setupSpawnUI', function(cData)
QBCore.Functions.TriggerCallback('apartments:GetOwnedApartment', function(result)
if result then
TriggerEvent('qb-spawn:client:setupSpawns', cData, false, nil)
TriggerEvent('qb-spawn:client:openUI', true)
TriggerEvent('apartments:client:SetHomeBlip', result.type)
else
if Apartments.Starting then
TriggerEvent('qb-spawn:client:setupSpawns', cData, true, Apartments.Locations)
TriggerEvent('qb-spawn:client:openUI', true)
else
TriggerEvent('qb-spawn:client:setupSpawns', cData, false, nil)
TriggerEvent('qb-spawn:client:openUI', true)
TriggerEvent('apartments:client:SetHomeBlip', nil)
end
end
end, cData.citizenid)
end)
RegisterNetEvent('apartments:client:SpawnInApartment', function(apartmentId, apartment)
local pos = GetEntityCoords(PlayerPedId())
if RangDoorbell ~= nil then
local doorbelldist = #(pos - vector3(Apartments.Locations[RangDoorbell].coords.enter.x, Apartments.Locations[RangDoorbell].coords.enter.y, Apartments.Locations[RangDoorbell].coords.enter.z))
if doorbelldist > 5 then
QBCore.Functions.Notify(Lang:t('error.to_far_from_door'))
return
end
end
ClosestHouse = apartment
EnterApartment(apartment, apartmentId, true)
IsOwned = true
end)
RegisterNetEvent('qb-apartments:client:LastLocationHouse', function(apartmentType, apartmentId)
ClosestHouse = apartmentType
EnterApartment(apartmentType, apartmentId, false)
end)
RegisterNetEvent('apartments:client:SetHomeBlip', function(home)
CreateThread(function()
SetClosestApartment()
for name, _ in pairs(Apartments.Locations) do
RemoveBlip(Apartments.Locations[name].blip)
Apartments.Locations[name].blip = AddBlipForCoord(Apartments.Locations[name].coords.enter.x, Apartments.Locations[name].coords.enter.y, Apartments.Locations[name].coords.enter.z)
if (name == home) then
SetBlipSprite(Apartments.Locations[name].blip, 475)
SetBlipCategory(Apartments.Locations[name].blip, 11)
else
SetBlipSprite(Apartments.Locations[name].blip, 476)
SetBlipCategory(Apartments.Locations[name].blip, 10)
end
SetBlipDisplay(Apartments.Locations[name].blip, 4)
SetBlipScale(Apartments.Locations[name].blip, 0.65)
SetBlipAsShortRange(Apartments.Locations[name].blip, true)
SetBlipColour(Apartments.Locations[name].blip, 3)
AddTextEntry(Apartments.Locations[name].label, Apartments.Locations[name].label)
BeginTextCommandSetBlipName(Apartments.Locations[name].label)
EndTextCommandSetBlipName(Apartments.Locations[name].blip)
end
end)
end)
RegisterNetEvent('apartments:client:RingMenu', function(data)
RangDoorbell = ClosestHouse
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'doorbell', 0.1)
TriggerServerEvent('apartments:server:RingDoor', data.apartmentId, ClosestHouse)
end)
RegisterNetEvent('apartments:client:RingDoor', function(player, _)
CurrentDoorBell = player
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'doorbell', 0.1)
QBCore.Functions.Notify(Lang:t('info.at_the_door'))
end)
RegisterNetEvent('apartments:client:DoorbellMenu', function()
MenuOwners()
end)
RegisterNetEvent('apartments:client:EnterApartment', function()
QBCore.Functions.TriggerCallback('apartments:GetOwnedApartment', function(result)
if result ~= nil then
EnterApartment(ClosestHouse, result.name)
end
end)
end)
RegisterNetEvent('apartments:client:UpdateApartment', function()
local apartmentType = ClosestHouse
local apartmentLabel = Apartments.Locations[ClosestHouse].label
QBCore.Functions.TriggerCallback('apartments:GetOwnedApartment', function(result)
if result == nil then
TriggerServerEvent("apartments:server:CreateApartment", apartmentType, apartmentLabel, false)
else
TriggerServerEvent('apartments:server:UpdateApartment', apartmentType, apartmentLabel)
end
end)
IsOwned = true
DeleteApartmentsEntranceTargets()
DeleteInApartmentTargets()
end)
RegisterNetEvent('apartments:client:OpenDoor', function()
if CurrentDoorBell == 0 then
QBCore.Functions.Notify(Lang:t('error.nobody_at_door'))
return
end
TriggerServerEvent('apartments:server:OpenDoor', CurrentDoorBell, CurrentApartment, ClosestHouse)
CurrentDoorBell = 0
end)
RegisterNetEvent('apartments:client:LeaveApartment', function()
LeaveApartment(ClosestHouse)
end)
RegisterNetEvent('apartments:client:OpenStash', function()
if CurrentApartment then
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'StashOpen', 0.4)
TriggerServerEvent('apartments:server:openStash', CurrentApartment)
end
end)
RegisterNetEvent('apartments:client:ChangeOutfit', function()
TriggerServerEvent('InteractSound_SV:PlayOnSource', 'Clothes1', 0.4)
TriggerEvent('qb-clothing:client:openOutfitMenu')
end)
RegisterNetEvent('apartments:client:Logout', function()
TriggerServerEvent('qb-houses:server:LogoutLocation')
end)
-- Threads
if UseTarget then
CreateThread(function()
local sleep = 5000
while not LocalPlayer.state.isLoggedIn do
-- do nothing
Wait(sleep)
end
while true do
sleep = 1000
if not InApartment then
SetClosestApartment()
SetApartmentsEntranceTargets()
elseif InApartment then
SetInApartmentTargets()
end
Wait(sleep)
end
end)
else
CreateThread(function()
local sleep = 5000
while not LocalPlayer.state.isLoggedIn do
-- do nothing
Wait(sleep)
end
while true do
sleep = 1000
if not InApartment then
SetClosestApartment()
SetApartmentsEntranceTargets()
if IsInsideEntranceZone then
sleep = 0
if IsControlJustPressed(0, 38) then
OpenEntranceMenu()
exports['qb-core']:HideText()
end
end
elseif InApartment then
sleep = 0
SetInApartmentTargets()
if IsInsideExitZone then
if IsControlJustPressed(0, 38) then
OpenExitMenu()
exports['qb-core']:HideText()
end
end
if IsInsideStashZone then
if IsControlJustPressed(0, 38) then
TriggerEvent('apartments:client:OpenStash')
exports['qb-core']:HideText()
end
end
if IsInsideOutfitsZone then
if IsControlJustPressed(0, 38) then
TriggerEvent('apartments:client:ChangeOutfit')
exports['qb-core']:HideText()
end
end
if IsInsideLogoutZone then
if IsControlJustPressed(0, 38) then
TriggerEvent('apartments:client:Logout')
exports['qb-core']:HideText()
end
end
end
Wait(sleep)
end
end)
end