[ESX]Custom ESX_Tattoos - camera is stick

Hello all,

First of all, sorry if it’s the wrong place to ask my question, but I didn’t know where to ask somewhere else.

I began scripting in Lua a few days before. I have a lot of experience with other languages (Java, Javascript, Angular, Ruby, Python and others) so developping in Lua isn’t quite a problem for me.
Some friends wanted helps on some of their scripts, so I took the challenge.

I began with esx_tatoos because this plug-in doesn’t have all DLC and they wanted to have some more useful tricks.
For exemple, when we see the selected tattos with a closer look, we would like to rotate the camera. Unfortunatly, every time I try, the camera is still stucked.
There may be something I still don’t understand. Thus, I seek some help :slight_smile:

Here’s my code to help you see the problem

ESX = nil
local currentTattoos, cam, CurrentActionData = {}, -1, {}
local HasAlreadyEnteredMarker, CurrentAction, CurrentActionMsg
local isCamFixed = false
local playerHasBeenUndressed = false
local playerSex = -1
local zoomOffset, camOffset, heading = 0.4, -0.6, 90.0
local selectedX, selectedY, selectedZ, selectedRotZ = 0.0, 0.0, 0.0, 0.0
local selectedCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)

--[[ ________________________________
	All threads
_____________________________________ ]]--

--[[ share object. What does it do precisely ? ]]--
Citizen.CreateThread(function()
	while ESX == nil do
		TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
		Citizen.Wait(0)
	end
end)

--[[ Key controls ]]--
Citizen.CreateThread(function()
	while true do
		Citizen.Wait(0)

		if CurrentAction then
			ESX.ShowHelpNotification(CurrentActionMsg)

			if IsControlJustReleased(0, 38) then
				if CurrentAction == 'tattoo_shop' then
					OpenTattooShopMenu(false)
				end
				CurrentAction = nil
			end
		else
			Citizen.Wait(500)
		end
	end
end)

--[[ Fix camera ]]--
Citizen.CreateThread(function()
	while true do
		Citizen.Wait(0)

		if isCamFixed then
			DisableAllControlActions(0)
            EnableControlAction(0, 109, true) -- 6 NUMPAD
			EnableControlAction(0, 108, true) -- 4 NUMPAD
			EnableControlAction(0, 172, true) -- UP ARROW
			EnableControlAction(0, 173, true) -- DOWN ARROW
			EnableControlAction(0, 174, true) -- LEFT ARROW
			EnableControlAction(0, 175, true) -- RIGHT ARROW
			EnableControlAction(0, 191, true) -- ENTER
			--EnableControlAction(0, 215, true) -- ENTER
			--EnableControlAction(0, 18, true) -- ENTER
            EnableControlAction(0, 177, true) --BackSpace, Esc, Right MB

			local angle = heading * math.pi / 180.0
			local coords    = GetEntityCoords(playerPed)

			local theta = {
                x = math.cos(angle),
                y = math.sin(angle)
            }

            local pos = {
                x = coords.x + (zoomOffset * theta.x),
                y = coords.y + (zoomOffset * theta.y)
            }

            local angleToLook = heading - 140.0
            if angleToLook > 360 then
                angleToLook = angleToLook - 360
            elseif angleToLook < 0 then
                angleToLook = angleToLook + 360
            end

            angleToLook = angleToLook * math.pi / 180.0
            local thetaToLook = {
                x = math.cos(angleToLook),
                y = math.sin(angleToLook)
            }

            SetCamCoord(cam, x + selectedX, y + selectedY, selectedZ)
			SetCamRot(cam, 0.0, 0.0, selectedRotZ)

			ESX.ShowHelpNotification(_U('use_rotate_view'))
		else
			Citizen.Wait(200)
		end
	end
end)

--[[ Being able to rotate camera in isCamFixed ]]--
Citizen.CreateThread(function()
    local angle = 250

    while true do
        Citizen.Wait(0)

        if isCamFixed then
            if IsControlPressed(0, 108) then
				print('je presse 4')
                angle = angle - 1
            elseif IsControlPressed(0, 109) then
                angle = angle + 1
            end

            if angle > 360 then
                angle = angle - 360
            elseif angle < 0 then
                angle = angle + 360
            end

            heading = angle + 0.0
        else
            Citizen.Wait(500)
        end
    end
end)

--[[ Sound and text ]]--
Citizen.CreateThread(function()
	for k,v in pairs(Config.Zones) do
		local blip = AddBlipForCoord(v)
		SetBlipSprite(blip, 75)
		SetBlipColour(blip, 1)
		SetBlipAsShortRange(blip, true)

		BeginTextCommandSetBlipName('STRING')
		AddTextComponentString(_U('tattoo_shop'))
		EndTextCommandSetBlipName(blip)
	end
end)

--[[ Display markers ]]--
Citizen.CreateThread(function()
	while true do
		Citizen.Wait(0)
		local coords, letSleep = GetEntityCoords(PlayerPedId()), true

		for k,v in pairs(Config.Zones) do
			if (Config.Type ~= -1 and GetDistanceBetweenCoords(coords, v, true) < Config.DrawDistance) then
				DrawMarker(Config.Type, v, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Config.Size.x, Config.Size.y, Config.Size.z, Config.Color.r, Config.Color.g, Config.Color.b, 100, false, true, 2, false, false, false, false)
				letSleep = false
			end
		end

		if letSleep then
			Citizen.Wait(500)
		end
	end
end)

--[[ Enter / Exit marker events ]]-- 
Citizen.CreateThread(function()
	while true do
		Citizen.Wait(100)

		local coords = GetEntityCoords(PlayerPedId())
		local isInMarker = false
		local currentZone, LastZone

		for k,v in pairs(Config.Zones) do
			if GetDistanceBetweenCoords(coords, v, true) < Config.Size.x then
				isInMarker  = true
				currentZone = 'TattooShop'
				LastZone    = 'TattooShop'
			end
		end

		if isInMarker and not HasAlreadyEnteredMarker then
			HasAlreadyEnteredMarker = true
			TriggerEvent('ev_tattooshops:hasEnteredMarker', currentZone)
		end

		if not isInMarker and HasAlreadyEnteredMarker then
			HasAlreadyEnteredMarker = false
			TriggerEvent('ev_tattooshops:hasExitedMarker', LastZone)
		end
	end
end)


--[[ ________________________________
	All EventHandler
_____________________________________ ]]--

--[[ While loading player skin ]]-- 
AddEventHandler('skinchanger:modelLoaded', function()
	ESX.TriggerServerCallback('ev_tattooshops:requestPlayerTattoos', function(tattooList)

		-- get and set playerSex
		TriggerEvent('skinchanger:getSkin', function(skin)
			playerSex = skin.sex
		end)

		if tattooList then
			for k,v in pairs(tattooList) do
				if playerSex == 0 then
					AddPedDecorationFromHashes(PlayerPedId(), GetHashKey(Config.TattooList[v.collection][v.texture].collection), GetHashKey(Config.TattooList[v.collection][v.texture].nameHashM))
				else
					AddPedDecorationFromHashes(PlayerPedId(), GetHashKey(Config.TattooList[v.collection][v.texture].collection), GetHashKey(Config.TattooList[v.collection][v.texture].nameHashF))
				end
			end

			currentTattoos = tattooList
		end
	end)
end)

--[[ Event to open tattoos' shop ]]-- 
RegisterNetEvent('ev_tattooshops:openTattooMenu')
AddEventHandler('ev_tattooshops:openTattooMenu', function(isRemovingByEMS, target)
	ESX.TriggerServerCallback('ev_tattooshops:requestTattoosOfPlayer', function(tattooList)
		if tattooList then
			OpenTattooShopMenu(isRemovingByEMS, tattooList, target)
		end
	end, target)
end)

--[[ Event when player enters to the marker of tattoos' shop ]]-- 
AddEventHandler('ev_tattooshops:hasEnteredMarker', function(zone)
	if zone == 'TattooShop' then
		CurrentAction     = 'tattoo_shop'
		CurrentActionMsg  = _U('tattoo_shop_prompt')
		CurrentActionData = {zone = zone}
	end
end)

--[[ Event when player exits to the marker of tattoos' shop ]]-- 
AddEventHandler('ev_tattooshops:hasExitedMarker', function(zone)
	CurrentAction = nil
	ESX.UI.Menu.CloseAll()
	if playerHasBeenUndressed then
		cleanPlayer()
		setPedSkin()
		playerHasBeenUndressed = false
	end
end)

--[[ Event when player remove a tattoo ]]-- 
RegisterNetEvent('ev_tattooshops:removeTattoo')
AddEventHandler('ev_tattooshops:removeTattoo', function(tattoo)
	for i, aTattoo in ipairs(currentTattoos) do
		if aTattoo.collection == tattoo.collection and aTattoo.texture == tattoo.texture then
			table.remove(currentTattoos, i)
		end
	end
	cleanPlayer()
end)


--[[ ________________________________
	All menu function
_____________________________________ ]]--

-- [[ Function to open tattoos' shop and set player naked]]--
function OpenTattooShopMenu(isRemovingByEMS, listTattoos, target)
	local elements = {}

	if isRemovingByEMS then
		currentTattoos = listTattoos
	end

	-- create all categories setted in tatooList --
	for k,v in pairs(Config.TattooCategories) do
		local label = _U('category', v.name)
		if v.new then
			label = _U('category_new', v.name)
		end

		table.insert(elements, {label= label, value = v.value})
	end

	-- see if delete it will do something --
	if DoesCamExist(cam) then
		RenderScriptCams(false, false, 0, 1, 0)
		DestroyCam(cam, false)
	end

	-- put player naked
	TriggerEvent('skinchanger:getSkin', function(skin)
		-- I do prefer to confirm sex here, even if we got it previously
		playerSex = skin.sex
		if skin.sex == 0 then
			TriggerEvent('skinchanger:loadClothes', skin, {
				['torso_1'] = 15, 
				['torso_2'] = 0,
				['tshirt_1'] = 15,
				['tshirt_2'] = 0,
				['arms'] = 15,
				['arms_2'] = 0,
				['decals_1'] = 0,
				['decals_2'] = 0,
				['bags_1'] = 0,
				['bags_2'] = 0,
				['pants_1'] = 61,
				['pants_2'] = 1,
				['bags_1'] = 0,
				['bags_2'] = 0,
				['shoes_1'] = 34,
				['shoes_2'] = 0,
			})
		else
			TriggerEvent('skinchanger:loadClothes', skin, {
				['torso_1'] = 15, 
				['torso_2'] = 0,
				['tshirt_1'] = 15,
				['tshirt_2'] = 0,
				['arms'] = 15,
				['arms_2'] = 0,
				['decals_1'] = 0,
				['decals_2'] = 0,
				['bags_1'] = 0,
				['bags_2'] = 0,
				['pants_1'] = 15,
				['pants_2'] = 0,
				['bags_1'] = 0,
				['bags_2'] = 0,
				['shoes_1'] = 35,
				['shoes_2'] = 0,
			})
		end
	end)

	tattooCategoriesMenu(isRemovingByEMS, listTattoos, target, elements, currentTattoos)

end

-- [[ Function to manage all actions in TattooCategoryMenu (where you have all listed tattoos' categories)]]--
function tattooCategoriesMenu(isRemovingByEMS, listTattoos, target, elements, currentTattoos)

	-- open window's menu with tattoos' categories
	ESX.UI.Menu.Open('default', GetCurrentResourceName(), 'tattoo_shop', {
		title    = _U('tattoos'),
		align    = 'top-left',
		elements = elements
	}, function(category, categoryMenu)

		local currentLabel, currentValue = category.current.label, category.current.value

		if category.current.value then
			elements = {{label = _U('go_back_to_menu'), value = nil}}

			for k,v in pairs(Config.TattooList[category.current.value]) do
				local hasAlreadyTattoo = false
				local putTattooInList = false
				if playerSex == 0 and v.nameHashM ~= "" then
					putTattooInList = true
				elseif playerSex == 1 and v.nameHashF ~= "" then
					putTattooInList = true
				end


				for i, aTattoo in pairs(currentTattoos) do
					if aTattoo.collection == currentValue and aTattoo.texture == k then
						hasAlreadyTattoo = true
					end
				end

				if putTattooInList then
					if not isRemovingByEMS then

						if hasAlreadyTattoo then
							local label = _U('already_have_tattoo_item', v.name)
							if v.new then
								label = _U('already_have_tattoo_item_new', v.name)
							end

							table.insert(elements, {
								--label = _U('erase_tattoo_item', k, _U('money_amount', ESX.Math.GroupDigits(v.price))),
								label = _U('already_have_tattoo_item', v.name),
								value = k,
								price = v.price,
								toRemove = true
							})
						else
							local label = _U('tattoo_item', v.name, _U('money_amount', ESX.Math.GroupDigits(v.price)))
							if v.new then
								label = _U('tattoo_item_new', v.name, _U('money_amount', ESX.Math.GroupDigits(v.price)))
							end

							table.insert(elements, {
								label = label,
								value = k,
								price = v.price,
								toRemove = false
							})
						end
					else
						if hasAlreadyTattoo then
							table.insert(elements, {
								label = _U('ems_remove_tattoo_item', v.name),
								value = k
							})
						end
					end
				end
			end

			tattooListMenu(isRemovingByEMS, listTattoos, target, elements, currentLabel, currentValue, currentTattoos)

		end
	end, function(category, categoryMenu)
		categoryMenu.close()
		if not isRemovingByEMS then
			setPedSkin()
		end
	end)

end

--[[ Function to display tattoos' list according to selected category ]]--
function tattooListMenu(isRemovingByEMS, listTattoos, target, elements, currentLabel, currentValue, currentTattoos)

	-- open window's menu with tattoos from the selected categorie
	ESX.UI.Menu.Open('default', GetCurrentResourceName(), 'tattoo_shop_categories', {
		title    = _U('tattoos') .. ' | '..currentLabel,
		align    = 'top-left',
		elements = elements
	}, function(data2, tattooListCategoryMenu)
		if data2.current.value ~= nil then
			local tattoo = {collection = currentValue, texture = data2.current.value}

			if not isRemovingByEMS then
				local price = data2.current.price
				if data2.current.toRemove then
					--[[ESX.TriggerServerCallback('ev_tattooshops:eraseTattoo', function(success)
						if success then
							for i, aTattoo in ipairs(currentTattoos) do
								if aTattoo.collection == tattoo.collection and aTattoo.texture == tattoo.texture then
									table.remove(currentTattoos,i)
								end
							end
							cleanPlayer()
						end
					end, currentTattoos, price, tattoo)]]--
					TriggerEvent('esx:showNotification', _U('already_have_tattoo'))
				else
					-- voir si c'est encore utile de le garder ou pas --
					RenderScriptCams(false, false, 0, 1, 0)
					DestroyCam(cam, false)

					confirmSelectedTattooMenu(currentTattoos, tattoo, price)					
				end
			else
				ESX.TriggerServerCallback('ev_tattooshops:eraseTattooFromEMS', function()
					for i, aTattoo in ipairs(currentTattoos) do
						if aTattoo.collection == tattoo.collection and aTattoo.texture == tattoo.texture then
							table.remove(currentTattoos,i)
						end
					end
					tattooListCategoryMenu.close()
				end, currentTattoos, tattoo, target)
			end
		else
			-- we go on return. So we must delete temporary tattoo. Can be used if player move outside tattoo's shop marker or if pressed escap button
			cleanPlayer()
			tattooListCategoryMenu.close()
		end

	end, function(data2, tattooListCategoryMenu)
		if not isRemovingByEMS then
			isCamFixed = false
			FreezeEntityPosition(GetPlayerPed(-1), false)
			cleanPlayer()
			tattooListCategoryMenu.close()
			RenderScriptCams(false, false, 0, 1, 0)
			DestroyCam(cam, false)
			--setPedSkin()
		else
			cleanPlayer()
			tattooListCategoryMenu.close()
		end
	end, function(data2, tattooListCategoryMenu) -- when highlighted
		if not isRemovingByEMS then
			if data2.current.value ~= nil then
				FreezeEntityPosition(GetPlayerPed(-1), false)
				RenderScriptCams(false, false, 0, 1, 0)
				drawTattoo(data2.current.value, currentValue)
				playerHasBeenUndressed = true
			else
				isCamFixed = false
				-- no need to display last highlighted tattoo. Can be confusing for players
				cleanPlayer()
				FreezeEntityPosition(GetPlayerPed(-1), false)
				RenderScriptCams(false, false, 0, 1, 0)
				DestroyCam(cam, false)
			end
		end
	end)

end

--[[ Function to display tattoos' list according to selected category ]]--
function confirmSelectedTattooMenu(currentTattoos, tattoo, price)

	-- confirm tattoo
	ESX.UI.Menu.Open('default', GetCurrentResourceName(), 'tattoo_confirm', {
		title    = _U('tattoo_confirm'),
		align    = 'top-left',
		elements = {
			{label = _U('yes'), value = 'yes'},
			{label = _U('no'),  value = 'no'}
		}
	}, function(data3, confirmTattooMenu)
		if data3.current.value == 'yes' then
			ESX.TriggerServerCallback('ev_tattooshops:purchaseTattoo', function(success)
				if success then
					table.insert(currentTattoos, tattoo)
					cleanPlayer()
				end
				confirmTattooMenu.close()
				FreezeEntityPosition(GetPlayerPed(-1), false)
				cleanPlayer()
			end, currentTattoos, price, tattoo)
		elseif data3.current.value == 'no' then
			confirmTattooMenu.close()
		end
	end, function(data3, confirmTattooMenu)
		confirmTattooMenu.close()
		cleanPlayer()
	end)

end

--[[ todo : function to add more opacity (put more than one same tattoo)]]--



--[[ ________________________________
	All functions
_____________________________________ ]]--

--[[ draw tattoo on player for display purposes ]]--
function drawTattoo(current, collection)
	SetEntityHeading(PlayerPedId(), 297.7296)
	ClearPedDecorations(PlayerPedId())

	-- tattoos already on player
	for k,v in pairs(currentTattoos) do
		if playerSex == 0 then
			AddPedDecorationFromHashes(PlayerPedId(), GetHashKey(Config.TattooList[v.collection][v.texture].collection), GetHashKey(Config.TattooList[v.collection][v.texture].nameHashM))
		else
			AddPedDecorationFromHashes(PlayerPedId(), GetHashKey(Config.TattooList[v.collection][v.texture].collection), GetHashKey(Config.TattooList[v.collection][v.texture].nameHashF))
		end
	end

	-- new tattoo to display (not already bought)
	if playerSex == 0 then
		AddPedDecorationFromHashes(PlayerPedId(), GetHashKey(Config.TattooList[collection][current].collection), GetHashKey(Config.TattooList[collection][current].nameHashM))
	else
		AddPedDecorationFromHashes(PlayerPedId(), GetHashKey(Config.TattooList[collection][current].collection), GetHashKey(Config.TattooList[collection][current].nameHashF))
	end

	if not DoesCamExist(cam) then
		cam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)

		SetCamCoord(cam, GetEntityCoords(PlayerPedId()))
		SetCamRot(cam, 0.0, 0.0, 0.0)
		SetCamActive(cam, true)
		RenderScriptCams(true, false, 0, true, true)
		SetCamCoord(cam, GetEntityCoords(PlayerPedId()))
	end

	local x,y,z = table.unpack(GetEntityCoords(PlayerPedId()))
	selectedX, selectedY, selectedZ = x + Config.TattooList[collection][current].addedX, y + Config.TattooList[collection][current].addedY, z + Config.TattooList[collection][current].addedZ
	selectedRotZ = Config.TattooList[collection][current].rotZ
	
	selectedCam = cam
	isCamFixed = true


	--[[
	SetCamCoord(cam, x + Config.TattooList[collection][current].addedX, y + Config.TattooList[collection][current].addedY, z + Config.TattooList[collection][current].addedZ)
	SetCamRot(cam, 0.0, 0.0, Config.TattooList[collection][current].rotZ)
	]]--
end

--[[ clean player from all and draws its tattoos ]]--
function cleanPlayer()
	ClearPedDecorations(PlayerPedId())
	for k,v in pairs(currentTattoos) do
		if playerSex == 0 then
			AddPedDecorationFromHashes(PlayerPedId(), GetHashKey(Config.TattooList[v.collection][v.texture].collection), GetHashKey(Config.TattooList[v.collection][v.texture].nameHashM))
		else
			AddPedDecorationFromHashes(PlayerPedId(), GetHashKey(Config.TattooList[v.collection][v.texture].collection), GetHashKey(Config.TattooList[v.collection][v.texture].nameHashF))
		end
	end
end

--[[ set player skin ]]--
function setPedSkin()
	ESX.TriggerServerCallback('esx_skin:getPlayerSkin', function(skin)
		TriggerEvent('skinchanger:loadSkin', skin)
	end)

	Citizen.Wait(1000)

	for k,v in pairs(currentTattoos) do
		if playerSex == 0 then
			AddPedDecorationFromHashes(PlayerPedId(), GetHashKey(Config.TattooList[v.collection][v.texture].collection), GetHashKey(Config.TattooList[v.collection][v.texture].nameHashM))
		else
			AddPedDecorationFromHashes(PlayerPedId(), GetHashKey(Config.TattooList[v.collection][v.texture].collection), GetHashKey(Config.TattooList[v.collection][v.texture].nameHashF))
		end
	end
end

And here an example of data used in tattoos.json

Config.TattooList = {
test_overlays = {
		{
			-- mettre la cam au centre du perso x était à 2.0
			name = 'Grim Rider', 
			nameHashM = 'MP_MP_Biker_Tat_042_M', 
			nameHashF = 'MP_MP_Biker_Tat_042_F', 
			addedX = 0.4, 
			addedY=-0.7,
			addedZ=0.1,
			rotZ = 24.3, 
			price = 1350,
			new = false,
			collection = "mpbiker_overlays"
		},
		
		
		{
			-- test
			name = 'Grim Rider', 
			nameHashM = 'MP_MP_Biker_Tat_042_M', 
			nameHashF = 'MP_MP_Biker_Tat_042_F', 
			addedX = 0.4, 
			addedY=0.0,
			addedZ=0.1,
			rotZ = 24.3, 
			price = 1350,
			new = false,
			collection = "mpbiker_overlays"
		},		
		
		
		{
			-- tourner la camera
			name = 'Scorched Soul', 
			nameHashM = 'MP_MP_Biker_Tat_037_M', 
			nameHashF = 'MP_MP_Biker_Tat_037_F', 
			addedX = 0.1, 
			addedY=1.0,
			addedZ=-0.2,
			rotZ = 156.7, 
			price = 1200,
			new = false,
			collection = "mpbiker_overlays"
		},		
		{
			-- pouvoir tourner la cam vers la droite
			name = 'Ride Free', 
			nameHashM = 'MP_MP_Biker_Tat_044_M', 
			nameHashF = 'MP_MP_Biker_Tat_044_F', 
			addedX = 0.1, 
			addedY=0.9,
			addedZ=-0.2,
			rotZ = 156.7, 
			price = 1200,
			new = false,
			collection = "mpbiker_overlays"
		},		
		
	},
}
Config.TattooCategories = {
{name = _U('test'), 	  			value = 'test_overlays', new = false},
}

I know my problem is about this line because if I don’t use it, the camera is not glued. The camera will not be at the place I want it to be :

SetCamCoord(cam, x + selectedX, y + selectedY, selectedZ)

And when I press 4 or 6 (there is a client trace when I press 4 for testing purpose), the camera does not rotate.
Someone can please tell me why I do have this issue and how to resolve it ?

Thank you by advance :slight_smile: