local config = require 'config.client' if not config.enableClient then return end local VEHICLES = exports.qbx_core:GetVehiclesByName() ---@enum ProgressColor local ProgressColor = { GREEN = 'green.5', YELLOW = 'yellow.5', RED = 'red.5' } ---@param percent number ---@return string local function getProgressColor(percent) if percent >= 75 then return ProgressColor.GREEN elseif percent > 25 then return ProgressColor.YELLOW else return ProgressColor.RED end end local VehicleCategory = { all = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22}, car = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 17, 18, 19, 20, 22}, air = {15, 16}, sea = {14}, } ---@param category VehicleType ---@param vehicle number ---@return boolean local function isOfType(category, vehicle) local classSet = {} for _, class in pairs(VehicleCategory[category]) do classSet[class] = true end return classSet[GetVehicleClass(vehicle)] == true end ---@param vehicle number local function kickOutPeds(vehicle) for i = -1, 5, 1 do local seat = GetPedInVehicleSeat(vehicle, i) if seat then TaskLeaveVehicle(seat, vehicle, 0) end end end local spawnLock = false ---@param vehicleId number ---@param garageName string ---@param accessPoint integer local function takeOutOfGarage(vehicleId, garageName, accessPoint) if spawnLock then exports.qbx_core:Notify(locale('error.spawn_in_progress')) end spawnLock = true local success, result = pcall(function() if cache.vehicle then exports.qbx_core:Notify(locale('error.in_vehicle')) return end local netId = lib.callback.await('qbx_garages:server:spawnVehicle', false, vehicleId, garageName, accessPoint) if not netId then return end local veh = lib.waitFor(function() if NetworkDoesEntityExistWithNetworkId(netId) then return NetToVeh(netId) end end) if veh == 0 then exports.qbx_core:Notify('Something went wrong spawning the vehicle', 'error') return end if config.engineOn then SetVehicleEngineOn(veh, true, true, false) end end) spawnLock = false assert(success, result) end ---@param data {vehicle: PlayerVehicle, garageName: string, accessPoint: integer} local function takeOutDepot(data) if data.vehicle.depotPrice ~= 0 then local success = lib.callback.await('qbx_garages:server:payDepotPrice', false, data.vehicle.id) if not success then exports.qbx_core:Notify(locale('error.not_enough'), 'error') return end end takeOutOfGarage(data.vehicle.id, data.garageName, data.accessPoint) end ---@param vehicle PlayerVehicle ---@param garageName string ---@param garageInfo GarageConfig ---@param accessPoint integer local function displayVehicleInfo(vehicle, garageName, garageInfo, accessPoint) local engine = qbx.math.round(vehicle.props.engineHealth / 10) local body = qbx.math.round(vehicle.props.bodyHealth / 10) local engineColor = getProgressColor(engine) local bodyColor = getProgressColor(body) local fuelColor = getProgressColor(vehicle.props.fuelLevel) local vehicleLabel = ('%s %s'):format(VEHICLES[vehicle.modelName].brand, VEHICLES[vehicle.modelName].name) local options = { { title = locale('menu.information'), icon = 'circle-info', description = locale('menu.description', vehicleLabel, vehicle.props.plate, lib.math.groupdigits(vehicle.depotPrice)), readOnly = true, }, { title = locale('menu.body'), icon = 'car-side', readOnly = true, progress = body, colorScheme = bodyColor, }, { title = locale('menu.engine'), icon = 'oil-can', readOnly = true, progress = engine, colorScheme = engineColor, }, { title = locale('menu.fuel'), icon = 'gas-pump', readOnly = true, progress = vehicle.props.fuelLevel, colorScheme = fuelColor, } } if vehicle.state == VehicleState.OUT then if garageInfo.type == GarageType.DEPOT then options[#options + 1] = { title = 'Take out', icon = 'fa-truck-ramp-box', description = ('$%s'):format(lib.math.groupdigits(vehicle.depotPrice)), arrow = true, onSelect = function() takeOutDepot({ vehicle = vehicle, garageName = garageName, accessPoint = accessPoint, }) end, } else options[#options + 1] = { title = 'Your vehicle is already out...', icon = VehicleType.CAR, readOnly = true, } end elseif vehicle.state == VehicleState.GARAGED then options[#options + 1] = { title = locale('menu.take_out'), icon = 'car-rear', arrow = true, onSelect = function() takeOutOfGarage(vehicle.id, garageName, accessPoint) end, } elseif vehicle.state == VehicleState.IMPOUNDED then options[#options + 1] = { title = locale('menu.veh_impounded'), icon = 'building-shield', readOnly = true, } end lib.registerContext({ id = 'vehicleList', title = garageInfo.label, menu = 'garageMenu', options = options, }) lib.showContext('vehicleList') end ---@param garageName string ---@param garageInfo GarageConfig ---@param accessPoint integer local function openGarageMenu(garageName, garageInfo, accessPoint) ---@type PlayerVehicle[]? local vehicleEntities = lib.callback.await('qbx_garages:server:getGarageVehicles', false, garageName) if not vehicleEntities then exports.qbx_core:Notify(locale('error.no_vehicles'), 'error') return end table.sort(vehicleEntities, function(a, b) return a.modelName < b.modelName end) local options = {} for i = 1, #vehicleEntities do local vehicleEntity = vehicleEntities[i] local vehicleLabel = ('%s %s'):format(VEHICLES[vehicleEntity.modelName].brand, VEHICLES[vehicleEntity.modelName].name) options[#options + 1] = { title = vehicleLabel, description = vehicleEntity.props.plate, arrow = true, onSelect = function() displayVehicleInfo(vehicleEntity, garageName, garageInfo, accessPoint) end, } end lib.registerContext({ id = 'garageMenu', title = garageInfo.label, options = options, }) lib.showContext('garageMenu') end ---@param vehicle number ---@param garageName string local function parkVehicle(vehicle, garageName) if GetVehicleNumberOfPassengers(vehicle) ~= 1 then local isParkable = lib.callback.await('qbx_garages:server:isParkable', false, garageName, NetworkGetNetworkIdFromEntity(vehicle)) if not isParkable then exports.qbx_core:Notify(locale('error.not_owned'), 'error', 5000) return end kickOutPeds(vehicle) SetVehicleDoorsLocked(vehicle, 2) Wait(1500) lib.callback.await('qbx_garages:server:parkVehicle', false, NetworkGetNetworkIdFromEntity(vehicle), lib.getVehicleProperties(vehicle), garageName) exports.qbx_core:Notify(locale('success.vehicle_parked'), 'primary', 4500) else exports.qbx_core:Notify(locale('error.vehicle_occupied'), 'error', 3500) end end ---@param garage GarageConfig ---@return boolean local function checkCanAccess(garage) if garage.groups and not exports.qbx_core:HasPrimaryGroup(garage.groups, QBX.PlayerData) then exports.qbx_core:Notify(locale('error.no_access'), 'error') return false end if cache.vehicle and not isOfType(garage.vehicleType, cache.vehicle) then exports.qbx_core:Notify(locale('error.not_correct_type'), 'error') return false end return true end ---@param garageName string ---@param garage GarageConfig ---@param accessPoint AccessPoint ---@param accessPointIndex integer local function createZones(garageName, garage, accessPoint, accessPointIndex) CreateThread(function() accessPoint.dropPoint = accessPoint.dropPoint or accessPoint.spawn local dropZone, coordsZone lib.zones.sphere({ coords = accessPoint.coords, radius = 15, onEnter = function() if accessPoint.dropPoint and garage.type ~= GarageType.DEPOT then dropZone = lib.zones.sphere({ coords = accessPoint.dropPoint, radius = 1.5, onEnter = function() if not cache.vehicle then return end lib.showTextUI(locale('info.park_e')) end, onExit = function() lib.hideTextUI() end, inside = function() if not cache.vehicle then return end if IsControlJustReleased(0, 38) then if not checkCanAccess(garage) then return end parkVehicle(cache.vehicle, garageName) end end, debug = config.debugPoly }) end coordsZone = lib.zones.sphere({ coords = accessPoint.coords, radius = 1, onEnter = function() if accessPoint.dropPoint and cache.vehicle then return end lib.showTextUI((garage.type == GarageType.DEPOT and locale('info.impound_e')) or (cache.vehicle and locale('info.park_e')) or locale('info.car_e')) end, onExit = function() lib.hideTextUI() end, inside = function() if accessPoint.dropPoint and cache.vehicle then return end if IsControlJustReleased(0, 38) then if not checkCanAccess(garage) then return end if cache.vehicle and garage.type ~= GarageType.DEPOT then parkVehicle(cache.vehicle, garageName) else openGarageMenu(garageName, garage, accessPointIndex) end end end, debug = config.debugPoly }) end, onExit = function() if dropZone then dropZone:remove() end if coordsZone then coordsZone:remove() end end, inside = function() if accessPoint.dropPoint then config.drawDropOffMarker(accessPoint.dropPoint) end config.drawGarageMarker(accessPoint.coords.xyz) end, debug = config.debugPoly, }) end) end ---@param garageInfo GarageConfig ---@param accessPoint AccessPoint local function createBlips(garageInfo, accessPoint) local blip = AddBlipForCoord(accessPoint.coords.x, accessPoint.coords.y, accessPoint.coords.z) SetBlipSprite(blip, accessPoint.blip.sprite or 357) SetBlipDisplay(blip, 4) SetBlipScale(blip, 0.60) SetBlipAsShortRange(blip, true) SetBlipColour(blip, accessPoint.blip.color or 3) BeginTextCommandSetBlipName('STRING') AddTextComponentSubstringPlayerName(accessPoint.blip.name or garageInfo.label) EndTextCommandSetBlipName(blip) end local function createGarage(name, garage) local accessPoints = garage.accessPoints for i = 1, #accessPoints do local accessPoint = accessPoints[i] if accessPoint.blip then createBlips(garage, accessPoint) end createZones(name, garage, accessPoint, i) end end local function createGarages() local garages = lib.callback.await('qbx_garages:server:getGarages') for name, garage in pairs(garages) do createGarage(name, garage) end end RegisterNetEvent('qbx_garages:client:garageRegistered', function(name, garage) createGarage(name, garage) end) CreateThread(function() createGarages() end)