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)