[Release] [Free] Elevator Script for ESX & QBCore

First of all, good work and congrats on your first release! :clap:

Second, might I suggest some improvements to make this a bit better and different to alternatives (because this is basically just a teleporter system, similar to every other script like it).

  1. Fade in after the teleport, so you don’t see the player drop into position.
  2. Extend the wait time between fade out and fade in, to simulate travel time in the elevator. This seeks to make it more realistically while also preventing people from going up and down and up and down, power gaming to get away from someone pursuing them (seen it happen).
  3. Add music while you wait (easy with InteractSound, xsound, etc).

Also, in the image provided AND in the code on GitHub you made a mistake and listed jobrestriction3 twice, instead of the last being jobrestriction4. This means that the 4th being blank is overwriting anything listed in the 3rd. Additionally, consider using a table of jobs to remove a limit of the jobs. I am actually looking through the code at the moment and will be rewriting it and will post my version here with some improvements for scaling it, such as removing redundant functions, etc. Feel free to use it or ignore it. :slight_smile:

In fact, as I go through it and rewrite it, there are a lot of errors or bad optimisations.
Your job check is going to void for jobs that aren’t in the first slot, you get their ESX player data every second for no reason, etc.
I highly suggest you push my version when it’s done. Not long now.

Took a while because I started rewriting it at work but had a lot of customers, so came home and finished it. Here you go…

Please note it is untested, but the changes, other than making it actually work with multiple jobs, includes;

  • ESX and QBCore support
  • Easier config by only changing config file and not client file
  • Unlimited job support instead of only 3 or 4
  • Optional notification system for ESX, QBCore or MythicNotify
  • Smoother teleportation without ESX dependency
  • Combined all elevators into a main config that loops, etc.
  • Removed the need to manually number each floor (numbers were wrong anyway)
  • Removed redundant need to have duplicate events with different names, for each elevator
  • You no longer see your current floor in the list of floors to go to

Now when people want to add elevators, they add a table with the key being the elevator name and the value being a table with a list of floors and their data. The comments have been adjusted to make it easier too. Hope you don’t mind that I took the liberty of improving it. Feel free to update your GitHub with this, once you test it and confirm it works. Any issues, let me know.

config.lua:

Config = {}

Config.UseESX = true			-- Use ESX Framework
Config.UseQBCore = false		-- Use QBCore Framework (Ignored if Config.UseESX = true)

Config.UseMythicNotify = true	-- Use mythic_notify (Requires mythic_notify script)

--[[
	USAGE

	Dependencies (can be swapped out):
		ESX or QBCore
		qtarget
		nh-context
		nh-keyboard

	To add an elevator, copy the table below and configure as needed:
		coords = Enter vector3 coords of center of elevator
		heading = Direction facing out of the elevator
		level = What floor are they going to
		label = What is on that floor
		jobs = Table of job keys that are allowed to access that floor and value of minimum grade of each job
]]

--[[
	ExampleElevator = {	
		{
			coords = vector3(xxx, yyy, zzz), heading = 0.0, level = "Floor 2", label = "Roof",
			jobs = {
				["police"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(xxx, yyy, zzz), heading = 0.0, level = "Floor 1", label = "Ground",
			jobs = {
				["police"] = 0,
				["ambulance"] = 0,
			}
		},
	},
]]

Config.Elevators = {

	VPDMainElevator = {	
		{
			coords = vector3(-1096.22, -850.763, 38.20), heading = 36.8, level = "Floor 6", label = "Roof Access",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1096.22, -850.763, 34.40), heading = 36.8, level = "Floor 5", label = "Detective Bureau",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1096.22, -850.763, 30.80), heading = 36.8, level = "Floor 4", label = "Operations Center",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1096.22, -850.763, 27.00), heading = 36.8, level = "Floor 3", label = "Division Offices & Briefing Room",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1096.22, -850.763, 23.00), heading = 36.8, level = "Floor 2", label = "Cafe",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1096.22, -850.763, 19.00), heading = 36.8, level = "Floor 1", label = "Main Hall",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1096.22, -850.763, 4.80), heading = 36.8, level = "Floor -1", label = "Detention Cells & Interrogation",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1096.22, -850.763, 10.20), heading = 36.8, level = "Floor -2", label = "Crime Lab & Evidence Rooms",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1096.22, -850.763, 13.70), heading = 36.8, level = "Floor -3", label = "Garage & Armory",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
	},

	VPDPublicElevator = {
		{
			coords = vector3(-1066.05, -833.71, 26.82318), heading = 36.1, level = "Floor 3", label = "Division Offices",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1066.05, -833.71, 23.03471), heading = 36.1, level = "Floor 2", label = "UNDER RENOVATIONS",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1066.05, -833.713, 18.9964), heading = 36.1, level = "Floor 1", label = "Main Hall",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1066.05, -833.71, 4.88), heading = 36.1, level = "Floor -1", label = "Detention Cells & Interrogation",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
				["hclaw"] = 0,
			}
		},
		{
			coords = vector3(-1066.05, -833.71, 10.27282), heading = 36.1, level = "Floor -2", label = "Crime Lab & Evidence Rooms",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
		{
			coords = vector3(-1066.05, -833.71, 13.69069), heading = 36.1, level = "Floor -3", label = "Garage & Armory",
			jobs = {
				["police"] = 0,
				["sheriff"] = 0,
				["ambulance"] = 0,
			}
		},
	},

	SkybarElevatorSouth = {
		{
			coords = vector3(315.49, -929.32, 52.81), heading = 176.67, level = "Skybar 5th Floor", label = "Bar Level for Skybar",
			jobs = {}
		},
		{
			coords = vector3(315.49, -929.32, 29.47), heading = 176.67, level = "Skybar Ground", label = "Street Level for Skybar",
			jobs = {}
		},
	},
	
	SkybarElevatorNorth = {
		{
			coords = vector3(309.81, -929.05, 52.81), heading = 176.67, level = "Skybar 5th Floor", label = "Bar Level for Skybar",
			jobs = {}
		},
		{
			coords = vector3(309.81, -929.05, 29.47), heading = 176.67, level = "Skybar Ground", label = "Street Level for Skybar",
			jobs = {}
		},
	},

}

client.lua:

ESX = nil
QBCore = nil
PlayerJob = nil
PlayerGrade = nil

if Config.UseESX then

	Citizen.CreateThread(function()
		while ESX == nil do
			TriggerEvent("esx:getSharedObject", function(obj) ESX = obj end)
			Wait(0)
		end
	
		while not ESX.IsPlayerLoaded() do
			Wait(100)
		end
	
		local playerData = ESX.GetPlayerData()
		PlayerJob = playerData.job.name
		PlayerGrade = playerData.job.grade
	end)

	RegisterNetEvent("esx:setJob", function(job)
		PlayerJob = job.name
		PlayerGrade = job.grade
	end)

elseif Config.UseQBCore then

	QBCore = exports["qb-core"]:GetCoreObject()

	local playerData = QBCore.Functions.GetPlayerData()
	PlayerJob = playerData.job.name
	PlayerGrade = playerData.job.grade.level

	RegisterNetEvent("QBCore:Client:OnJobUpdate", function(job)
		PlayerJob = job.name
		PlayerGrade = job.grade.level
	end)

end

CreateThread(function()
	for elevatorName, elevatorFloors in pairs(Config.Elevators) do
		for index, floor in pairs(elevatorFloors) do
			exports["qtarget"]:AddBoxZone(elevatorName .. index, floor.coords, 5, 4, {
				name = elevatorName,
				heading = floor.heading,
				debugPoly = false,
				minZ = floor.coords.z - 1.5,
				maxZ = floor.coords.z + 1.5
			},
			{
				options = {
					{
						event = "angelicxs_elevator:showFloors",
						icon = "fas fa-hand-point-up",
						label = "Use Elevator From " .. floor.level,
						elevator = elevatorName,
						level = index
					},
				},
				distance = 1.5 
			})
		end
	end
end)

RegisterNetEvent("angelicxs_elevator:showFloors", function(data)
	local elevator = {}
	for index, floor in pairs(Config.Elevators[data.elevator]) do
		table.insert(elevator, {
			header = floor.level,
			context = floor.label,
			disabled = (index == data.level or not hasRequiredJob(floor.jobs)),
			event = "angelicxs_elevator:movement",
			args = { floor }
		})
	end
	TriggerEvent("nh-context:createMenu", elevator)
end)

RegisterNetEvent("angelicxs_elevator:movement", function(floor)
	if hasRequiredJob(floor.jobs) then
		local ped = PlayerPedId()
		DoScreenFadeOut(1500)
		while not IsScreenFadedOut() do
			Wait(10)
		end
		RequestCollisionAtCoord(floor.coords.x, floor.coords.y, floor.coords.z)
		while not HasCollisionLoadedAroundEntity(ped) do
			Citizen.Wait(0)
		end
		SetEntityCoords(ped, floor.coords.x, floor.coords.y, floor.coords.z, false, false, false, false)
		SetEntityHeading(ped, floor.heading and floor.heading or 0.0)
		Wait(3000)
		DoScreenFadeIn(1500)
	else
		TriggerEvent("angelicxs_elevator:notify", "You don't have clearance for this floor!", "error")
	end
end)

RegisterNetEvent("angelicxs_elevator:notify", function(message, type)
	if Config.UseMythicNotify then
		exports.mythic_notify:SendAlert(type, message, 4000)
	elseif Config.UseESX then
		ESX.ShowNotification(message)
	elseif Config.UseQBCore then
		QBCore.Functions.Notify(message, type)
	end
end)

function hasRequiredJob(jobs)
	if next(jobs) then
		for jobName, gradeLevel in pairs(jobs) do
			if PlayerJob == jobName and PlayerGrade >= gradeLevel then
				return true
			end
		end
		return false
	end
	return true
end

Adding new elevators:
Inside Config.Elevators, add the following structure (as provided in the config file comments)…

ExampleElevator = {	
	{
		coords = vector3(xxx, yyy, zzz), heading = 0.0, level = "Floor 2", label = "Roof",
		jobs = {
			["police"] = 0,
			["ambulance"] = 0,
		}
	},
	{
		coords = vector3(xxx, yyy, zzz), heading = 0.0, level = "Floor 1", label = "Ground",
		jobs = {}
	},
},

That’s all. Now you have a new elevator. No messing with events for the context menu or teleportation. Enjoy! :spades:

Good rewrite but 2 issues.

Name isnt unique for each floor so qtarget doesnt work correctly and

data.floor.coords

needs to be

data.floor.coords.x, data.floor.coords.y, data.floor.coords.z
1 Like
CreateThread(function()
	for elevatorName, elevatorFloors in pairs(Config.Elevators) do
        local idCount = 1
		for index, floor in pairs(elevatorFloors) do
            idCount = idCount + 1
            local Name = elevatorName .. '_' .. idCount
			exports["qtarget"]:AddCircleZone(Name, floor.coords, 1.75, {
				name = Name,
				debugPoly = false,
                useZ = true
			},
			{
				options = {
					{
						event = angelicxs_elevator:showFloors",
						icon = "fas fa-hand-point-up",
						label = "Use Elevator",
						elevator = elevatorName,
						level = index
					},
				},
				distance = 2.5 
			})
		end
	end
end)

RegisterNetEvent("angelicxs_elevator:showFloors", function(data)
	local elevator = {}
	for index, floor in pairs(Config.Elevators[data.elevator]) do
		if index ~= data.index then
			table.insert(elevator, {
				id = index,
				header = floor.level,
				txt = floor.label,
				params = {
					event = "angelicxs_elevator:movement",
					arg1 = {
						floor = floor
					}
				}
			})
		end
	end
	TriggerEvent("nh-context:sendMenu", elevator)
end)

RegisterNetEvent("angelicxs_elevator:movement", function(data)
	if hasRequiredJob(data.floor.jobs) then
		local ped = PlayerPedId()
		DoScreenFadeOut(1500)
		while not IsScreenFadedOut() do
			Wait(10)
		end
		RequestCollisionAtCoord(data.floor.coords.x, data.floor.coords.y, data.floor.coords.z)
		while not HasCollisionLoadedAroundEntity(ped) do
			Citizen.Wait(0)
		end
		SetEntityCoords(ped, data.floor.coords.x, data.floor.coords.y, data.floor.coords.z, false, false, false, false)
		SetEntityHeading(ped, data.floor.heading and data.floor.heading or 0.0)
		Wait(3000)
		DoScreenFadeIn(1500)
	else
        TriggerEvent("angelicxs_elevator:notify", "You don't have clearance for this floor!", "error")
	end
end)

This worked for me

1 Like

Thanks for the feedback mate.
I went and updated it. I have never used qtarget or nh-context, so honestly I just hoped for the best after reading the documentation for both while at work earlier today haha
Didn’t know it had to be unique, but I see now why it does. Your tweak will work fine, but I made it even simpler. Instead of 2 new variables to act as a counter and a zone name, I used the preexisting “index” key from the for loop.

exports["qtarget"]:AddBoxZone(elevatorName .. index, floor.coords, 5, 4, {

And I updated the SetEntityCoords function. So my rewrite should work a treat now.
Only thing missing are sound effects and that’s just a simple xsound or InteractSound function call. Enjoy! :spades:

1 Like

If you play with the new nh-context you can disable the floor you currently are on to prevent TP to same location OR have it disabled if you dont have the right job.

Here are a pick on all disable

I dont have the knowledge to do it by my self

Continuing the discussion from [Release] [Free] Elevator Script for ESX:

Did change so it shows the floor you are on with qtarget

CreateThread(function()
	for elevatorName, elevatorFloors in pairs(Config.Elevators) do
		for index, floor in pairs(elevatorFloors) do
			exports["qtarget"]:AddBoxZone(elevatorName .. index, floor.coords, 5, 4, {
				name = elevatorName,
				heading = floor.heading,
				debugPoly = false,
				minZ = floor.coords.z - 1.5,
				maxZ = floor.coords.z + 1.5
			},
			{
				options = {
					{
						event = "angelicxs_elevator:showFloors",
						icon = "fas fa-hand-point-up",
						label = "Use Elevator From " .. floor.level,
						elevator = elevatorName,
						level = index
					},
				},
				distance = 1.5 
			})
		end
	end
end)

Thank you all for the optimizations, I have compiled them tested and adjusted in a live environment and pushed the current collaborative version.

However, during testing both with this updated version and when I was first writing the script I noted that calling to a table to check for jobs results in a false value being provided.
This is why I had separated them out in the original script. Any ideas on how to solve this?

1 Like

Possibly due to your hasRequiredJob function only returning if the player has the job name and exact grade. I believe the desired outcome was any grade above the required grade(i.e. With current function, if you set job name ‘police’ and grade ‘0’, it will only allow your lowest ranked police officers to use that floor.

Check if this works for the hasRequiredJob function to achieve the outcome you are looking for:

function hasRequiredJob(jobs)
	if next(jobs) then
		for jobName, gradeLevel in pairs(jobs) do
			if PlayerJob == jobName and PlayerGrade >= gradeLevel then
				return true
			end
		end
		return false
	end
	return true
end
1 Like

I thought I had done this, but apparently I made a mistake…
In angelicxs_elevator:showFloors I wrote;

if index ~= data.index then

But it should be;

if index ~= data.level then

@angelicxs Please update the GitHub with this small change that will prevent your current floor from showing up for now, and perhaps look into this alternative mentioned by @Smokiiee that allows you to display the button but have it disabled. Smokiiee, could you please provide a link to this new thing you speak of?

EDIT: I am also looking into the issue with detecting multiple jobs when passing a table. Not sure why it wouldn’t work… Code looks fine to me, but again, I wrote most of this at work between customers. I haven’t tested a single thing haha

Yes :slight_smile:
its in the readme file

disabled = "pass "true" if you want to disable this button from being pressed, and will change to a disabled color",

I believe the OP was getting the player data within a loop because some servers utilize clocking in/out and multiple jobs on one character.

Although the loop for getting player data could’ve been delayed to every 30 seconds or so rather than every frame(Every frame was adding about 0.05ms or so to resmon), with doing the job check only once on player load will result in players having to relog when clocking in to be able to access said floors which could be an inconvenience to said player.

I am aware of this and I support multiple jobs on my server, however that is why we have “esx:setJob”. You don’t need to get the PlayerData every second, or even every 30 seconds.

Have a read of the rewritten version I made. They pushed it to GitHub already, so you can see how I went about it with ESX and QBCore support, still updating on job changes, but massively optimised. There is no relogging required, as you suggested may be an issue. It’s easy to update on job changes without loops. Just listen for events.

Thanks anyway though!

I see now:

        RegisterNetEvent("esx:setJob", function(job)
            PlayerJob = job.name
            PlayerGrade = job.grade
        end)

:face_with_open_eyes_and_hand_over_mouth:
my bad.

1 Like

Brilliant!
Here is another push then (updated my original post again).

RegisterNetEvent("angelicxs_elevator:showFloors", function(data)
	local elevator = {}
	for index, floor in pairs(Config.Elevators[data.elevator]) do
		table.insert(elevator, {
			id = index,
			header = floor.level,
			txt = floor.label,
			disabled = index == data.level,
			params = {
				event = "angelicxs_elevator:movement",
				args = {
					floor = floor
				}
			}
		})
	end
	TriggerEvent("nh-context:sendMenu", elevator)
end)

I have removed the table insert from the if statement checking the current floor index, which causes it to not show up, and replaced it with a conditional assignment to the new “disabled” parameter of nh-context meaning that it will still show, but it will be disabled if that button is for the current floor. Thanks Smokiiee!

Another push to GitHub please, @angelicxs :spades:

You nailed it, mate. Thanks! I updated the original post again. It should be matching all the desired changes now and can be pushed to GitHub. Again, when you’re at work and rushing between customers, mistakes happen. Appreciate everyone helping out. I am sure the OP is pleased with their new and improved release :slight_smile:

Teamwork!

The new updates have been pushed and live tests show that the jobs are correctly being read, thank everyone!

Unfortunately, that cool feature suggested by Smokiiee did not work on live.

Did you test it with the new nh-context?

Ahh, didn’t catch that it was for the newest version. Will update, test and come back.