first commit
This commit is contained in:
@@ -0,0 +1,989 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user