Files
2026-08-05 01:29:39 +02:00

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)