990 lines
38 KiB
Lua
990 lines
38 KiB
Lua
local propertyState = {}
|
|
local ownedPropertyKey = nil
|
|
local ownedPropertyLabel = nil
|
|
local locationMode = 'world'
|
|
local currentBuilding = nil
|
|
local currentFloor = nil
|
|
local currentProperty = nil
|
|
local currentInteriorStyle = nil
|
|
local previewOpen = false
|
|
local previewUnits = {}
|
|
local previewIndex = 1
|
|
local previewCamera = nil
|
|
local previewShotIndex = 1
|
|
local nextPreviewShotAt = 0
|
|
local pendingDoorbell = nil
|
|
local pendingInvite = nil
|
|
local activeInteraction = false
|
|
local hallwayObjects = {}
|
|
local lastPropertyRefresh = 0
|
|
|
|
local function notify(message)
|
|
exports.devx_core:Notify(message)
|
|
end
|
|
|
|
local function help(text)
|
|
BeginTextCommandDisplayHelp('STRING')
|
|
AddTextComponentSubstringPlayerName(text)
|
|
EndTextCommandDisplayHelp(0, false, true, -1)
|
|
end
|
|
|
|
local function drawText3D(coords, lineOne, lineTwo)
|
|
local onScreen, screenX, screenY = World3dToScreen2d(coords.x, coords.y, coords.z)
|
|
if not onScreen then return end
|
|
|
|
SetTextScale(0.0, 0.30)
|
|
SetTextFont(4)
|
|
SetTextProportional(true)
|
|
SetTextColour(255, 255, 255, 235)
|
|
SetTextCentre(true)
|
|
SetTextOutline()
|
|
BeginTextCommandDisplayText('STRING')
|
|
AddTextComponentSubstringPlayerName(lineOne or '')
|
|
EndTextCommandDisplayText(screenX, screenY)
|
|
|
|
if lineTwo and lineTwo ~= '' then
|
|
SetTextScale(0.0, 0.25)
|
|
SetTextFont(4)
|
|
SetTextProportional(true)
|
|
SetTextColour(155, 190, 255, 230)
|
|
SetTextCentre(true)
|
|
SetTextOutline()
|
|
BeginTextCommandDisplayText('STRING')
|
|
AddTextComponentSubstringPlayerName(lineTwo)
|
|
EndTextCommandDisplayText(screenX, screenY + 0.018)
|
|
end
|
|
end
|
|
|
|
local function findBuilding(key)
|
|
for _, building in ipairs(DevXHousing.Buildings) do
|
|
if building.key == key then return building end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function findUnit(key)
|
|
for _, building in ipairs(DevXHousing.Buildings) do
|
|
for _, unit in ipairs(building.units) do
|
|
if unit.key == key then return unit, building end
|
|
end
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
local function requestAnim(dict)
|
|
RequestAnimDict(dict)
|
|
local deadline = GetGameTimer() + 5000
|
|
while not HasAnimDictLoaded(dict) and GetGameTimer() < deadline do Wait(0) end
|
|
return HasAnimDictLoaded(dict)
|
|
end
|
|
|
|
local function requestModel(name)
|
|
local hash = GetHashKey(name)
|
|
if not IsModelInCdimage(hash) or not IsModelValid(hash) then return nil end
|
|
RequestModel(hash)
|
|
local deadline = GetGameTimer() + 5000
|
|
while not HasModelLoaded(hash) and GetGameTimer() < deadline do Wait(0) end
|
|
return HasModelLoaded(hash) and hash or nil
|
|
end
|
|
|
|
local function clearHallwayObjects()
|
|
for _, entity in ipairs(hallwayObjects) do
|
|
if entity and DoesEntityExist(entity) then DeleteEntity(entity) end
|
|
end
|
|
hallwayObjects = {}
|
|
end
|
|
|
|
local function createHallwayObject(modelName, coords, collision)
|
|
local model = requestModel(modelName)
|
|
if not model then
|
|
print(('[devx_housing] Dekorationsmodell fehlt: %s'):format(modelName))
|
|
return nil
|
|
end
|
|
|
|
local object = CreateObjectNoOffset(model, coords.x, coords.y, coords.z, false, false, false)
|
|
if object and object ~= 0 then
|
|
SetEntityHeading(object, coords.heading or coords.w or 0.0)
|
|
FreezeEntityPosition(object, true)
|
|
SetEntityCollision(object, collision ~= false, collision ~= false)
|
|
SetEntityAsMissionEntity(object, true, true)
|
|
hallwayObjects[#hallwayObjects + 1] = object
|
|
end
|
|
SetModelAsNoLongerNeeded(model)
|
|
return object
|
|
end
|
|
|
|
local function spawnHallwayObjects(showDoors)
|
|
clearHallwayObjects()
|
|
local shell = DevXHousing.FloorShell
|
|
for _, entry in ipairs(shell.setDressing or {}) do
|
|
createHallwayObject(entry.model, entry.coords, true)
|
|
end
|
|
if showDoors then
|
|
for _, door in ipairs(shell.doors or {}) do
|
|
createHallwayObject(shell.doorModel, door, true)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function restoreWorldStreaming(reason)
|
|
clearHallwayObjects()
|
|
TriggerEvent('devx_ipl:client:restoreWorld', reason or 'housing')
|
|
end
|
|
|
|
local function prepareInterior(definition)
|
|
local position = definition and (definition.loadAt or definition.spawn) or nil
|
|
if not position then return true end
|
|
|
|
-- v0.4.3 never removes or swaps global apartment IPLs. It only streams the
|
|
-- already existing interior around the target coordinate.
|
|
NewLoadSceneStop()
|
|
ClearFocus()
|
|
SetFocusPosAndVel(position.x, position.y, position.z, 0.0, 0.0, 0.0)
|
|
RequestCollisionAtCoord(position.x, position.y, position.z)
|
|
RequestAdditionalCollisionAtCoord(position.x, position.y, position.z)
|
|
NewLoadSceneStartSphere(position.x, position.y, position.z, 120.0, 0)
|
|
|
|
local interiorId = GetInteriorAtCoords(position.x, position.y, position.z)
|
|
if interiorId == 0 then interiorId = GetInteriorAtCoords(position.x, position.y, position.z + 1.0) end
|
|
if interiorId ~= 0 then
|
|
PinInteriorInMemory(interiorId)
|
|
RefreshInterior(interiorId)
|
|
end
|
|
|
|
local deadline = GetGameTimer() + 15000
|
|
local ready = false
|
|
while GetGameTimer() < deadline do
|
|
local sceneReady = IsNewLoadSceneLoaded()
|
|
local interiorReady = interiorId == 0 or IsInteriorReady(interiorId)
|
|
if sceneReady and interiorReady then
|
|
ready = true
|
|
break
|
|
end
|
|
RequestCollisionAtCoord(position.x, position.y, position.z)
|
|
RequestAdditionalCollisionAtCoord(position.x, position.y, position.z)
|
|
Wait(0)
|
|
end
|
|
|
|
NewLoadSceneStop()
|
|
if not ready then
|
|
print(('[devx_housing] Streaming-Timeout bei %.4f %.4f %.4f (interior=%s)'):format(
|
|
position.x, position.y, position.z, interiorId
|
|
))
|
|
end
|
|
return ready
|
|
end
|
|
|
|
local function teleport(position, interiorDefinition)
|
|
DoScreenFadeOut(280)
|
|
local fadeDeadline = GetGameTimer() + 1500
|
|
while not IsScreenFadedOut() and GetGameTimer() < fadeDeadline do Wait(0) end
|
|
|
|
if interiorDefinition and not prepareInterior(interiorDefinition) then
|
|
ClearFocus()
|
|
DoScreenFadeIn(300)
|
|
notify('Der Innenraum konnte nicht gestreamt werden. Du wurdest nicht teleportiert.')
|
|
return false
|
|
end
|
|
|
|
local ped = PlayerPedId()
|
|
FreezeEntityPosition(ped, true)
|
|
SetEntityCollision(ped, true, true)
|
|
RequestCollisionAtCoord(position.x, position.y, position.z)
|
|
RequestAdditionalCollisionAtCoord(position.x, position.y, position.z)
|
|
SetEntityCoordsNoOffset(ped, position.x, position.y, position.z, false, false, false)
|
|
SetEntityHeading(ped, position.heading or position.w or 0.0)
|
|
ClearPedTasksImmediately(ped)
|
|
|
|
local collisionDeadline = GetGameTimer() + 10000
|
|
while not HasCollisionLoadedAroundEntity(ped) and GetGameTimer() < collisionDeadline do
|
|
RequestCollisionAtCoord(position.x, position.y, position.z)
|
|
RequestAdditionalCollisionAtCoord(position.x, position.y, position.z)
|
|
Wait(0)
|
|
end
|
|
|
|
FreezeEntityPosition(ped, false)
|
|
Wait(180)
|
|
ClearFocus()
|
|
DoScreenFadeIn(380)
|
|
return true
|
|
end
|
|
|
|
local function refreshProperties(callback)
|
|
DevXTriggerCallback('housing:list', {}, function(result)
|
|
if result.ok then
|
|
propertyState = {}
|
|
ownedPropertyKey = result.ownedPropertyKey
|
|
ownedPropertyLabel = result.ownedPropertyLabel
|
|
for _, property in ipairs(result.properties or {}) do
|
|
propertyState[property.propertyKey] = {
|
|
id = property.id,
|
|
ownerCharacterId = property.ownerCharacterId,
|
|
ownerName = property.ownerName,
|
|
characterId = result.characterId,
|
|
price = tonumber(property.price) or 0,
|
|
label = property.label
|
|
}
|
|
end
|
|
lastPropertyRefresh = GetGameTimer()
|
|
end
|
|
if callback then callback(result) end
|
|
end)
|
|
end
|
|
|
|
local function stateFor(unit)
|
|
local player = exports.devx_core:GetPlayerData()
|
|
local state = propertyState[unit.key] or {
|
|
characterId = player and player.characterId,
|
|
price = unit.price
|
|
}
|
|
local ownerName = type(state.ownerName) == 'string' and state.ownerName:gsub('^%s+', ''):gsub('%s+$', '') or ''
|
|
state.ownerName = ownerName ~= '' and ownerName or nil
|
|
state.ownedByMe = state.ownerCharacterId
|
|
and tonumber(state.ownerCharacterId) == tonumber(state.characterId)
|
|
state.sold = state.ownerCharacterId ~= nil and state.ownerName ~= nil
|
|
return state
|
|
end
|
|
|
|
local function sendUi(action, data)
|
|
data = data or {}
|
|
data.action = action
|
|
SendNUIMessage(data)
|
|
end
|
|
|
|
local function destroyPreviewCamera()
|
|
if previewCamera and DoesCamExist(previewCamera) then
|
|
RenderScriptCams(false, true, 450, true, true)
|
|
DestroyCam(previewCamera, false)
|
|
end
|
|
previewCamera = nil
|
|
end
|
|
|
|
local function destroyPreview()
|
|
previewOpen = false
|
|
destroyPreviewCamera()
|
|
SetNuiFocus(false, false)
|
|
local ped = PlayerPedId()
|
|
SetEntityVisible(ped, true, false)
|
|
SetEntityCollision(ped, true, true)
|
|
FreezeEntityPosition(ped, false)
|
|
ClearFocus()
|
|
end
|
|
|
|
local function currentPreviewUnit()
|
|
return previewUnits[previewIndex]
|
|
end
|
|
|
|
local function updatePreviewUi()
|
|
local unit = currentPreviewUnit()
|
|
if not unit then return end
|
|
local state = stateFor(unit)
|
|
sendUi('previewData', {
|
|
label = unit.label,
|
|
building = unit.buildingLabel,
|
|
floor = unit.floor,
|
|
unitNumber = unit.unitNumber,
|
|
style = DevXHousing.Interiors[unit.style].label,
|
|
price = state.price or unit.price,
|
|
sold = state.sold,
|
|
ownedByMe = state.ownedByMe,
|
|
ownerName = state.ownerName,
|
|
index = previewIndex,
|
|
total = #previewUnits
|
|
})
|
|
end
|
|
|
|
local function usePreviewShot(instant)
|
|
local unit = currentPreviewUnit()
|
|
if not unit then return end
|
|
local interior = DevXHousing.Interiors[unit.style]
|
|
local shots = interior.cameraShots or {}
|
|
if #shots == 0 then return end
|
|
if previewShotIndex > #shots then previewShotIndex = 1 end
|
|
local shot = shots[previewShotIndex]
|
|
|
|
local newCamera = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
|
|
SetCamCoord(newCamera, shot.cam.x, shot.cam.y, shot.cam.z)
|
|
PointCamAtCoord(newCamera, shot.target.x, shot.target.y, shot.target.z)
|
|
SetCamFov(newCamera, 48.0)
|
|
SetCamActive(newCamera, true)
|
|
|
|
if previewCamera and DoesCamExist(previewCamera) and not instant then
|
|
SetCamActiveWithInterp(newCamera, previewCamera, 1450, true, true)
|
|
local oldCamera = previewCamera
|
|
CreateThread(function()
|
|
Wait(1550)
|
|
if DoesCamExist(oldCamera) then DestroyCam(oldCamera, false) end
|
|
end)
|
|
else
|
|
RenderScriptCams(true, true, 450, true, true)
|
|
if previewCamera and DoesCamExist(previewCamera) then DestroyCam(previewCamera, false) end
|
|
end
|
|
|
|
previewCamera = newCamera
|
|
nextPreviewShotAt = GetGameTimer() + 6000
|
|
end
|
|
|
|
local function showPreviewUnit()
|
|
local unit = currentPreviewUnit()
|
|
if not unit then return end
|
|
local interior = DevXHousing.Interiors[unit.style]
|
|
local ped = PlayerPedId()
|
|
|
|
DoScreenFadeOut(180)
|
|
while not IsScreenFadedOut() do Wait(0) end
|
|
if not prepareInterior(interior) then
|
|
DoScreenFadeIn(250)
|
|
notify('Diese Apartmentansicht konnte nicht geladen werden.')
|
|
return
|
|
end
|
|
|
|
SetEntityCoordsNoOffset(ped, interior.spawn.x, interior.spawn.y, interior.spawn.z, false, false, false)
|
|
SetEntityHeading(ped, interior.spawn.heading or 0.0)
|
|
SetEntityVisible(ped, false, false)
|
|
SetEntityCollision(ped, false, false)
|
|
FreezeEntityPosition(ped, true)
|
|
|
|
destroyPreviewCamera()
|
|
previewShotIndex = 1
|
|
usePreviewShot(true)
|
|
updatePreviewUi()
|
|
Wait(120)
|
|
DoScreenFadeIn(260)
|
|
end
|
|
|
|
local function closePreview(returnToWorld)
|
|
local building = currentBuilding
|
|
destroyPreview()
|
|
sendUi('close')
|
|
DevXTriggerCallback('housing:previewEnd', {}, function() end)
|
|
locationMode = 'world'
|
|
if returnToWorld ~= false and building then
|
|
teleport(building.exterior)
|
|
restoreWorldStreaming('housing-preview-end')
|
|
end
|
|
end
|
|
|
|
local function startPreview(building)
|
|
if previewOpen then return end
|
|
clearHallwayObjects()
|
|
refreshProperties(function(result)
|
|
if not result.ok then notify(result.error or 'Wohnungen konnten nicht geladen werden.'); return end
|
|
if ownedPropertyKey then
|
|
notify(('Du besitzt bereits %s. Pro Charakter ist nur ein Apartment möglich.'):format(ownedPropertyLabel or 'ein Apartment'))
|
|
return
|
|
end
|
|
|
|
previewUnits = {}
|
|
for _, unit in ipairs(building.units) do
|
|
if not stateFor(unit).sold then previewUnits[#previewUnits + 1] = unit end
|
|
end
|
|
table.sort(previewUnits, function(a, b) return a.price < b.price end)
|
|
if #previewUnits == 0 then notify('In diesem Gebäude sind derzeit keine Apartments frei.'); return end
|
|
|
|
previewIndex = 1
|
|
currentBuilding = building
|
|
DevXTriggerCallback('housing:previewStart', { buildingKey = building.key }, function(startResult)
|
|
if not startResult.ok then notify(startResult.error or 'Vorschau konnte nicht gestartet werden.'); return end
|
|
previewOpen = true
|
|
locationMode = 'preview'
|
|
SetNuiFocus(true, true)
|
|
sendUi('previewOpen')
|
|
showPreviewUnit()
|
|
end)
|
|
end)
|
|
end
|
|
|
|
local function applyUnitResult(result, unit, building)
|
|
clearHallwayObjects()
|
|
locationMode = 'unit'
|
|
currentProperty = unit.key
|
|
currentBuilding = building
|
|
currentFloor = unit.floor
|
|
currentInteriorStyle = unit.style
|
|
if teleport(result.interior.spawn, result.interior) then
|
|
notify(('Du bist in %s.'):format(result.label or unit.label))
|
|
else
|
|
locationMode = 'world'
|
|
currentProperty = nil
|
|
currentInteriorStyle = nil
|
|
TriggerServerEvent('devx_core:server:returnToWorld')
|
|
teleport(building.exterior)
|
|
restoreWorldStreaming('housing-unit-failed')
|
|
end
|
|
end
|
|
|
|
local function enterUnit(unit)
|
|
DevXTriggerCallback('housing:enterUnit', { propertyKey = unit.key }, function(result)
|
|
if not result.ok then notify(result.error or 'Wohnung konnte nicht betreten werden.'); return end
|
|
local _, building = findUnit(unit.key)
|
|
applyUnitResult(result, unit, building)
|
|
end)
|
|
end
|
|
|
|
local function enterOwned(building)
|
|
DevXTriggerCallback('housing:enterOwned', { buildingKey = building.key }, function(result)
|
|
if not result.ok then notify(result.error or 'Dein Apartment konnte nicht betreten werden.'); return end
|
|
local unit, unitBuilding = findUnit(result.propertyKey)
|
|
if unit then applyUnitResult(result, unit, unitBuilding) end
|
|
end)
|
|
end
|
|
|
|
local function enterLobby(building)
|
|
DevXTriggerCallback('housing:enterLobby', { buildingKey = building.key }, function(result)
|
|
if not result.ok then notify(result.error); return end
|
|
currentBuilding = building
|
|
currentFloor = 0
|
|
currentProperty = nil
|
|
currentInteriorStyle = nil
|
|
locationMode = 'lobby'
|
|
if teleport(result.position, DevXHousing.FloorShell) then
|
|
spawnHallwayObjects(false)
|
|
notify(('Lobby von %s betreten. Am Aufzug kannst du eine Etage wählen.'):format(building.label))
|
|
else
|
|
TriggerServerEvent('devx_core:server:returnToWorld')
|
|
locationMode = 'world'
|
|
teleport(building.exterior)
|
|
restoreWorldStreaming('housing-lobby-failed')
|
|
end
|
|
end)
|
|
end
|
|
|
|
local function openDoorMenu(unit)
|
|
local state = stateFor(unit)
|
|
if not state.sold then return end
|
|
SetNuiFocus(true, true)
|
|
sendUi('doorOpen', {
|
|
propertyKey = unit.key,
|
|
label = ('Wohnung %s'):format(unit.doorNumber),
|
|
ownerName = state.ownerName,
|
|
sold = true,
|
|
ownedByMe = state.ownedByMe
|
|
})
|
|
end
|
|
|
|
local function openInviteMenu()
|
|
DevXTriggerCallback('housing:inviteCandidates', {}, function(result)
|
|
if not result.ok then notify(result.error or 'Spielerliste konnte nicht geladen werden.'); return end
|
|
SetNuiFocus(true, true)
|
|
sendUi('inviteOpen', {
|
|
title = 'Spieler einladen',
|
|
players = result.players or {}
|
|
})
|
|
end)
|
|
end
|
|
|
|
local function playDrink(kind)
|
|
if activeInteraction then return end
|
|
activeInteraction = true
|
|
local ped = PlayerPedId()
|
|
local modelName = kind == 'whiskey' and 'prop_drink_whisky' or 'prop_amb_beer_bottle'
|
|
local model = requestModel(modelName)
|
|
local prop = nil
|
|
if model then
|
|
prop = CreateObject(model, 0.0, 0.0, 0.0, false, false, false)
|
|
AttachEntityToEntity(prop, ped, GetPedBoneIndex(ped, 28422), 0.0, -0.01, -0.03, 0.0, 0.0, 0.0, true, true, false, true, 1, true)
|
|
end
|
|
if requestAnim('amb@world_human_drinking@beer@male@idle_a') then
|
|
TaskPlayAnim(ped, 'amb@world_human_drinking@beer@male@idle_a', 'idle_c', 3.0, -3.0, 6500, 49, 0.0, false, false, false)
|
|
end
|
|
Wait(6500)
|
|
ClearPedSecondaryTask(ped)
|
|
if prop and DoesEntityExist(prop) then DeleteEntity(prop) end
|
|
if model then SetModelAsNoLongerNeeded(model) end
|
|
notify(kind == 'whiskey' and 'Du hast einen Whiskey getrunken.' or 'Du hast ein Bier getrunken.')
|
|
activeInteraction = false
|
|
end
|
|
|
|
local function playSleep(point)
|
|
if activeInteraction then return end
|
|
activeInteraction = true
|
|
local ped = PlayerPedId()
|
|
SetEntityCoordsNoOffset(ped, point.coords.x, point.coords.y, point.coords.z + 0.15, false, false, false)
|
|
SetEntityHeading(ped, point.heading or 0.0)
|
|
if requestAnim('timetable@tracy@sleep@') then
|
|
TaskPlayAnim(ped, 'timetable@tracy@sleep@', 'idle_c', 2.0, -2.0, 10000, 1, 0.0, false, false, false)
|
|
end
|
|
notify('Du schläfst kurz …')
|
|
Wait(10000)
|
|
ClearPedTasksImmediately(ped)
|
|
SetEntityHealth(ped, math.min(GetEntityMaxHealth(ped), GetEntityHealth(ped) + 15))
|
|
activeInteraction = false
|
|
end
|
|
|
|
local function playBong()
|
|
if activeInteraction then return end
|
|
activeInteraction = true
|
|
local ped = PlayerPedId()
|
|
TaskStartScenarioInPlace(ped, 'WORLD_HUMAN_SMOKING_POT', 0, true)
|
|
StartScreenEffect('DrugsMichaelAliensFight', 0, false)
|
|
Wait(9000)
|
|
StopScreenEffect('DrugsMichaelAliensFight')
|
|
ClearPedTasksImmediately(ped)
|
|
activeInteraction = false
|
|
end
|
|
|
|
local function useApartmentInteraction(point)
|
|
if point.type == 'beer' or point.type == 'whiskey' then
|
|
CreateThread(function() playDrink(point.type) end)
|
|
elseif point.type == 'sleep' then
|
|
CreateThread(function() playSleep(point) end)
|
|
elseif point.type == 'bong' then
|
|
CreateThread(playBong)
|
|
end
|
|
end
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
if previewOpen and GetGameTimer() >= nextPreviewShotAt then
|
|
local unit = currentPreviewUnit()
|
|
if unit then
|
|
local shots = DevXHousing.Interiors[unit.style].cameraShots or {}
|
|
previewShotIndex = previewShotIndex + 1
|
|
if previewShotIndex > #shots then previewShotIndex = 1 end
|
|
usePreviewShot(false)
|
|
end
|
|
end
|
|
Wait(250)
|
|
end
|
|
end)
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
if previewOpen then
|
|
DisableAllControlActions(0)
|
|
EnableControlAction(0, 200, true)
|
|
Wait(0)
|
|
else
|
|
Wait(500)
|
|
end
|
|
end
|
|
end)
|
|
|
|
RegisterNUICallback('previewPrevious', function(_, cb)
|
|
if #previewUnits > 0 then
|
|
previewIndex = previewIndex - 1
|
|
if previewIndex < 1 then previewIndex = #previewUnits end
|
|
showPreviewUnit()
|
|
end
|
|
cb({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('previewNext', function(_, cb)
|
|
if #previewUnits > 0 then
|
|
previewIndex = previewIndex + 1
|
|
if previewIndex > #previewUnits then previewIndex = 1 end
|
|
showPreviewUnit()
|
|
end
|
|
cb({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('previewBuy', function(_, cb)
|
|
local unit = currentPreviewUnit()
|
|
if not unit then cb({ ok = false, error = 'Keine Wohnung ausgewählt.' }); return end
|
|
DevXTriggerCallback('housing:buy', { propertyKey = unit.key }, function(result)
|
|
if result.ok then
|
|
refreshProperties(function()
|
|
notify('Wohnung erfolgreich gekauft. Am Gebäudeeingang kannst du sie jetzt direkt betreten.')
|
|
closePreview(true)
|
|
end)
|
|
end
|
|
cb(result)
|
|
end)
|
|
end)
|
|
|
|
RegisterNUICallback('previewClose', function(_, cb)
|
|
closePreview(true)
|
|
cb({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('elevatorFloor', function(data, cb)
|
|
local building = currentBuilding
|
|
local floor = tonumber(data.floor)
|
|
if not building or not floor then cb({ ok = false }); return end
|
|
|
|
DevXTriggerCallback('housing:goFloor', { buildingKey = building.key, floor = floor }, function(result)
|
|
if result.ok then
|
|
SetNuiFocus(false, false)
|
|
sendUi('close')
|
|
locationMode = 'floor'
|
|
currentFloor = floor
|
|
refreshProperties()
|
|
if teleport(result.position, DevXHousing.FloorShell) then
|
|
spawnHallwayObjects(true)
|
|
else
|
|
locationMode = 'world'
|
|
TriggerServerEvent('devx_core:server:returnToWorld')
|
|
teleport(building.exterior)
|
|
restoreWorldStreaming('housing-floor-failed')
|
|
end
|
|
else
|
|
notify(result.error)
|
|
end
|
|
cb(result)
|
|
end)
|
|
end)
|
|
|
|
RegisterNUICallback('uiClose', function(_, cb)
|
|
SetNuiFocus(false, false)
|
|
sendUi('close')
|
|
cb({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('doorRing', function(data, cb)
|
|
DevXTriggerCallback('housing:ring', { propertyKey = data.propertyKey }, function(result)
|
|
notify(result.message or result.error or 'Klingelanfrage beendet.')
|
|
cb(result)
|
|
end)
|
|
end)
|
|
|
|
RegisterNUICallback('doorKnock', function(data, cb)
|
|
DevXTriggerCallback('housing:knock', { propertyKey = data.propertyKey }, function(result)
|
|
notify(result.message or result.error or 'Klopfen beendet.')
|
|
cb(result)
|
|
end)
|
|
end)
|
|
|
|
RegisterNUICallback('doorEnter', function(data, cb)
|
|
local unit = findUnit(data.propertyKey)
|
|
SetNuiFocus(false, false)
|
|
sendUi('close')
|
|
if unit then enterUnit(unit) end
|
|
cb({ ok = true })
|
|
end)
|
|
|
|
RegisterNUICallback('invitePlayer', function(data, cb)
|
|
DevXTriggerCallback('housing:invite', { target = tonumber(data.target) }, function(result)
|
|
if result.ok then
|
|
notify('Apartment-Einladung wurde gesendet. Der Spieler muss sie zuerst annehmen.')
|
|
SetNuiFocus(false, false)
|
|
sendUi('close')
|
|
else
|
|
notify(result.error or 'Einladung konnte nicht gesendet werden.')
|
|
end
|
|
cb(result)
|
|
end)
|
|
end)
|
|
|
|
RegisterNetEvent('devx_housing:client:notify', notify)
|
|
|
|
RegisterNetEvent('devx_housing:client:invite', function(data)
|
|
pendingInvite = {
|
|
propertyKey = data.propertyKey,
|
|
label = data.label or 'Apartment',
|
|
invitedBy = data.invitedBy or 'Jemand',
|
|
expiresAt = GetGameTimer() + ((tonumber(data.expiresIn) or 120) * 1000)
|
|
}
|
|
PlaySoundFrontend(-1, 'Menu_Accept', 'Phone_SoundSet_Default', true)
|
|
notify(('%s lädt dich in %s ein. Drücke E zum Annehmen oder X zum Ablehnen.'):format(
|
|
pendingInvite.invitedBy, pendingInvite.label
|
|
))
|
|
end)
|
|
|
|
RegisterNetEvent('devx_housing:client:enterGranted', function(result)
|
|
local unit, building = findUnit(result.propertyKey)
|
|
if not unit then return end
|
|
pendingInvite = nil
|
|
applyUnitResult(result, unit, building)
|
|
notify(('Zutritt zu %s erhalten.'):format(result.label or unit.label))
|
|
end)
|
|
|
|
RegisterNetEvent('devx_housing:client:doorbell', function(data)
|
|
pendingDoorbell = data
|
|
pendingDoorbell.expiresAt = GetGameTimer() + 30000
|
|
PlaySoundFrontend(-1, 'Door_Open', 'DOCKS_HEIST_FINALE_2B_SOUNDS', true)
|
|
notify(('%s klingelt an deiner Wohnung. Drücke E zum Öffnen oder X zum Ablehnen.'):format(data.visitorName))
|
|
end)
|
|
|
|
RegisterNetEvent('devx_housing:client:knock', function(visitorName)
|
|
PlaySoundFrontend(-1, 'Knuckle_Crack_Hard_Cel', 'MP_SNACKS_SOUNDSET', true)
|
|
notify(('%s klopft an der Wohnungstür.'):format(visitorName or 'Jemand'))
|
|
end)
|
|
|
|
RegisterNetEvent('devx_housing:client:visitorAccepted', function(visitorName)
|
|
notify(('%s wurde in deine Wohnung gelassen.'):format(visitorName or 'Der Besucher'))
|
|
end)
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
if pendingInvite then
|
|
if GetGameTimer() > pendingInvite.expiresAt then
|
|
pendingInvite = nil
|
|
notify('Die Apartment-Einladung ist abgelaufen.')
|
|
else
|
|
help(('%s lädt dich ein: ~INPUT_CONTEXT~ annehmen | ~INPUT_VEH_DUCK~ ablehnen'):format(pendingInvite.invitedBy))
|
|
if IsControlJustReleased(0, 38) then
|
|
DevXTriggerCallback('housing:acceptInvite', {}, function(result)
|
|
if result.ok then
|
|
local unit, building = findUnit(result.propertyKey)
|
|
if unit then applyUnitResult(result, unit, building) end
|
|
else
|
|
notify(result.error or 'Einladung ist nicht mehr gültig.')
|
|
end
|
|
end)
|
|
pendingInvite = nil
|
|
elseif IsControlJustReleased(0, 73) then
|
|
DevXTriggerCallback('housing:declineInvite', {}, function() end)
|
|
pendingInvite = nil
|
|
notify('Apartment-Einladung abgelehnt.')
|
|
end
|
|
end
|
|
Wait(0)
|
|
else
|
|
Wait(400)
|
|
end
|
|
end
|
|
end)
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
if pendingDoorbell then
|
|
if pendingDoorbell.expiresAt and GetGameTimer() > pendingDoorbell.expiresAt then
|
|
pendingDoorbell = nil
|
|
else
|
|
help(('%s klingelt: ~INPUT_CONTEXT~ öffnen | ~INPUT_VEH_DUCK~ ablehnen'):format(
|
|
pendingDoorbell.visitorName or 'Besucher'
|
|
))
|
|
if IsControlJustReleased(0, 38) then
|
|
DevXTriggerCallback('housing:acceptVisitor', {}, function(result)
|
|
if not result.ok then notify(result.error) end
|
|
end)
|
|
pendingDoorbell = nil
|
|
elseif IsControlJustReleased(0, 73) then
|
|
DevXTriggerCallback('housing:declineVisitor', {}, function() end)
|
|
pendingDoorbell = nil
|
|
notify('Klingelanfrage abgelehnt.')
|
|
end
|
|
end
|
|
Wait(0)
|
|
else
|
|
Wait(400)
|
|
end
|
|
end
|
|
end)
|
|
|
|
AddEventHandler('devx_core:client:playerLoaded', function()
|
|
locationMode = 'world'
|
|
currentBuilding = nil
|
|
currentFloor = nil
|
|
currentProperty = nil
|
|
currentInteriorStyle = nil
|
|
clearHallwayObjects()
|
|
refreshProperties()
|
|
end)
|
|
|
|
CreateThread(function()
|
|
while not exports.devx_core:IsPlayerLoaded() do Wait(500) end
|
|
refreshProperties()
|
|
|
|
for _, building in ipairs(DevXHousing.Buildings) do
|
|
local blip = AddBlipForCoord(building.exterior.x, building.exterior.y, building.exterior.z)
|
|
SetBlipSprite(blip, 40)
|
|
SetBlipScale(blip, 0.72)
|
|
SetBlipColour(blip, 3)
|
|
SetBlipAsShortRange(blip, true)
|
|
BeginTextCommandSetBlipName('STRING')
|
|
AddTextComponentString(building.label)
|
|
EndTextCommandSetBlipName(blip)
|
|
end
|
|
end)
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
if locationMode == 'floor' and GetGameTimer() - lastPropertyRefresh >= (DevXHousing.PropertyRefreshMs or 8000) then
|
|
refreshProperties()
|
|
end
|
|
Wait(1000)
|
|
end
|
|
end)
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
local sleep = 900
|
|
if exports.devx_core:IsPlayerLoaded() and not previewOpen then
|
|
local ped = PlayerPedId()
|
|
local coords = GetEntityCoords(ped)
|
|
|
|
if locationMode == 'world' then
|
|
local _, ownedBuilding = nil, nil
|
|
if ownedPropertyKey then _, ownedBuilding = findUnit(ownedPropertyKey) end
|
|
|
|
for _, building in ipairs(DevXHousing.Buildings) do
|
|
local entry = building.exterior
|
|
local distance = #(coords - vector3(entry.x, entry.y, entry.z))
|
|
if distance < DevXHousing.MarkerDistance then
|
|
sleep = 0
|
|
DrawMarker(1, entry.x, entry.y, entry.z - 1.0, 0.0,0.0,0.0,0.0,0.0,0.0,
|
|
1.0,1.0,0.25,65,110,245,125,false,false,2,false,nil,nil,false)
|
|
if distance < DevXHousing.InteractionDistance then
|
|
if ownedPropertyKey and ownedBuilding and ownedBuilding.key == building.key then
|
|
help(('~INPUT_CONTEXT~ Apartment betreten | ~INPUT_DETONATE~ Lobby von %s'):format(building.label))
|
|
if IsControlJustReleased(0, 38) then enterOwned(building)
|
|
elseif IsControlJustReleased(0, 47) then enterLobby(building) end
|
|
elseif ownedPropertyKey then
|
|
help(('Du besitzt bereits %s | ~INPUT_DETONATE~ Lobby betreten'):format(ownedPropertyLabel or 'ein Apartment'))
|
|
if IsControlJustReleased(0, 47) then enterLobby(building) end
|
|
else
|
|
help(('~INPUT_CONTEXT~ Apartments ansehen | ~INPUT_DETONATE~ Lobby von %s betreten'):format(building.label))
|
|
if IsControlJustReleased(0, 38) then startPreview(building)
|
|
elseif IsControlJustReleased(0, 47) then enterLobby(building) end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
elseif locationMode == 'lobby' or locationMode == 'floor' then
|
|
local shell = DevXHousing.FloorShell
|
|
local elevatorDistance = #(coords - vector3(shell.elevator.x, shell.elevator.y, shell.elevator.z))
|
|
local exitDistance = #(coords - vector3(shell.exit.x, shell.exit.y, shell.exit.z))
|
|
|
|
if elevatorDistance < 10.0 then
|
|
sleep = 0
|
|
DrawMarker(1, shell.elevator.x, shell.elevator.y, shell.elevator.z - 1.0,
|
|
0.0,0.0,0.0,0.0,0.0,0.0,0.75,0.75,0.22,80,145,255,130,false,false,2,false,nil,nil,false)
|
|
if elevatorDistance < 1.5 then
|
|
help('Drücke ~INPUT_CONTEXT~, um den Aufzug zu benutzen.')
|
|
if IsControlJustReleased(0, 38) then
|
|
SetNuiFocus(true, true)
|
|
sendUi('elevatorOpen', {
|
|
building = currentBuilding.label,
|
|
floors = currentBuilding.floors,
|
|
currentFloor = currentFloor or 0
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
if exitDistance < 8.0 then
|
|
sleep = 0
|
|
DrawMarker(1, shell.exit.x, shell.exit.y, shell.exit.z - 1.0,
|
|
0.0,0.0,0.0,0.0,0.0,0.0,0.75,0.75,0.22,255,255,255,100,false,false,2,false,nil,nil,false)
|
|
if exitDistance < 1.5 then
|
|
help('Drücke ~INPUT_CONTEXT~, um das Gebäude zu verlassen.')
|
|
if IsControlJustReleased(0, 38) then
|
|
DevXTriggerCallback('housing:leaveBuilding', {}, function(result)
|
|
if result.ok then
|
|
locationMode = 'world'
|
|
currentFloor = nil
|
|
clearHallwayObjects()
|
|
teleport(result.position)
|
|
restoreWorldStreaming('housing-leave-building')
|
|
else notify(result.error) end
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
|
|
if locationMode == 'floor' and currentBuilding and currentFloor then
|
|
for _, unit in ipairs(currentBuilding.units) do
|
|
if unit.floor == currentFloor then
|
|
local state = stateFor(unit)
|
|
-- Free units have no resident sign and no door action.
|
|
if state.sold then
|
|
local door = unit.door
|
|
local distance = #(coords - vector3(door.x, door.y, door.z))
|
|
if distance < 12.0 then
|
|
sleep = 0
|
|
drawText3D(door.sign or { x = door.x + 0.75, y = door.y, z = door.z + 1.2 },
|
|
('Wohnung %s'):format(unit.doorNumber),
|
|
state.ownedByMe and ('~b~%s (Deine Wohnung)'):format(state.ownerName) or state.ownerName)
|
|
if distance < 1.55 then
|
|
if state.ownedByMe then
|
|
help(('~INPUT_CONTEXT~ Wohnung %s betreten'):format(unit.doorNumber))
|
|
if IsControlJustReleased(0, 38) then enterUnit(unit) end
|
|
else
|
|
help(('~INPUT_CONTEXT~ bei %s klingeln oder klopfen'):format(state.ownerName))
|
|
if IsControlJustReleased(0, 38) then openDoorMenu(unit) end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
elseif locationMode == 'unit' and currentProperty then
|
|
local unit = findUnit(currentProperty)
|
|
if unit then
|
|
local interior = DevXHousing.Interiors[unit.style]
|
|
local exit = interior.exit
|
|
local distance = #(coords - vector3(exit.x, exit.y, exit.z))
|
|
if distance < 10.0 then
|
|
sleep = 0
|
|
DrawMarker(1, exit.x, exit.y, exit.z - 1.0, 0.0,0.0,0.0,0.0,0.0,0.0,
|
|
0.85,0.85,0.22,255,255,255,105,false,false,2,false,nil,nil,false)
|
|
if distance < 1.55 then
|
|
local state = stateFor(unit)
|
|
if state.ownedByMe then
|
|
help('~INPUT_CONTEXT~ Apartment verlassen | ~INPUT_DETONATE~ Spieler einladen')
|
|
else
|
|
help('~INPUT_CONTEXT~ Apartment verlassen')
|
|
end
|
|
|
|
if IsControlJustReleased(0, 38) then
|
|
DevXTriggerCallback('housing:exitUnit', {}, function(result)
|
|
if result.ok then
|
|
TriggerEvent('devx_housing:client:leftApartment')
|
|
locationMode = 'floor'
|
|
currentProperty = nil
|
|
currentInteriorStyle = nil
|
|
currentFloor = result.floor
|
|
refreshProperties()
|
|
if teleport(result.position, DevXHousing.FloorShell) then
|
|
spawnHallwayObjects(true)
|
|
end
|
|
else notify(result.error) end
|
|
end)
|
|
elseif state.ownedByMe and IsControlJustReleased(0, 47) then
|
|
openInviteMenu()
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, point in ipairs(interior.interactions or {}) do
|
|
local pointDistance = #(coords - vector3(point.coords.x, point.coords.y, point.coords.z))
|
|
if pointDistance < 7.0 then
|
|
sleep = 0
|
|
DrawMarker(2, point.coords.x, point.coords.y, point.coords.z + 0.15,
|
|
0.0,0.0,0.0,0.0,180.0,0.0,0.22,0.22,0.22,95,160,255,155,false,false,2,false,nil,nil,false)
|
|
if pointDistance < 1.35 then
|
|
help(('~INPUT_CONTEXT~ %s'):format(point.label))
|
|
if IsControlJustReleased(0, 38) then useApartmentInteraction(point) end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
Wait(sleep)
|
|
end
|
|
end)
|
|
|
|
RegisterCommand('inviteapartment', function(_, args)
|
|
local target = tonumber(args[1])
|
|
if not target then
|
|
openInviteMenu()
|
|
return
|
|
end
|
|
DevXTriggerCallback('housing:invite', { target = target }, function(result)
|
|
notify(result.ok and 'Apartment-Einladung gesendet.' or (result.error or 'Einladung fehlgeschlagen.'))
|
|
end)
|
|
end, false)
|
|
|
|
exports('GetHousingState', function()
|
|
return {
|
|
mode = locationMode,
|
|
propertyKey = currentProperty,
|
|
buildingKey = currentBuilding and currentBuilding.key or nil,
|
|
floor = currentFloor,
|
|
ownedPropertyKey = ownedPropertyKey,
|
|
interiorStyle = currentInteriorStyle,
|
|
interior = currentInteriorStyle and DevXHousing.Interiors[currentInteriorStyle] or nil
|
|
}
|
|
end)
|
|
|
|
AddEventHandler('onResourceStop', function(resourceName)
|
|
if resourceName ~= GetCurrentResourceName() then return end
|
|
destroyPreview()
|
|
clearHallwayObjects()
|
|
TriggerServerEvent('devx_core:server:returnToWorld')
|
|
TriggerEvent('devx_ipl:client:restoreWorld', 'housing-stop')
|
|
end)
|