375 lines
15 KiB
Lua
375 lines
15 KiB
Lua
local animationDict = "pickup_object"
|
|
local animation = "pickup_low"
|
|
local isInPlaceItemMode = false
|
|
|
|
-- Keeps track of the models that have already been set with target options, ensuring we don't create duplicate options for the same model
|
|
-- In some frameworks like OX, if you create targetOptions on a model that already has it, it will append the options, whereas
|
|
-- in QB it will override and only the last set of options will be used. We just need to add one option to the target model and then
|
|
-- the pickup event will use the statebag to determine the correct item to give back to the player
|
|
local targetModels = {}
|
|
|
|
local function LoadPropDict(model)
|
|
while not HasModelLoaded(GetHashKey(model)) do
|
|
RequestModel(GetHashKey(model))
|
|
Wait(10)
|
|
end
|
|
end
|
|
|
|
-- Gets the direction the camera is looking for the raycast function
|
|
local function RotationToDirection(rotation)
|
|
local adjustedRotation = {
|
|
x = (math.pi / 180) * rotation.x,
|
|
y = (math.pi / 180) * rotation.y,
|
|
z = (math.pi / 180) * rotation.z,
|
|
}
|
|
local direction = {
|
|
x = -math.sin(adjustedRotation.z) * math.abs(math.cos(adjustedRotation.x)),
|
|
y = math.cos(adjustedRotation.z) * math.abs(math.cos(adjustedRotation.x)),
|
|
z = math.sin(adjustedRotation.x),
|
|
}
|
|
return direction
|
|
end
|
|
|
|
-- Uses a RayCast to get the entity, coords, and whether we "hit" something with the raycast
|
|
-- Object passed in, is the current object that we want the raycast to ignore
|
|
local function RayCastGamePlayCamera(distance, object, raycastDetectWorldOnly)
|
|
local cameraRotation = GetGameplayCamRot()
|
|
local cameraCoord = GetGameplayCamCoord()
|
|
local direction = RotationToDirection(cameraRotation)
|
|
local destination = {
|
|
x = cameraCoord.x + direction.x * distance,
|
|
y = cameraCoord.y + direction.y * distance,
|
|
z = cameraCoord.z + direction.z * distance,
|
|
}
|
|
|
|
-- Trace flag 4294967295 means the raycast will intersect with everything (including vehicles)
|
|
-- Trace flag 1 means the raycast will only intersect with the world (ignoring other entities like peds, cars, etc)
|
|
local traceFlag = 4294967295
|
|
if raycastDetectWorldOnly then
|
|
traceFlag = 1
|
|
end
|
|
|
|
local a, hit, coords, d, entity = GetShapeTestResult(StartShapeTestRay(cameraCoord.x, cameraCoord.y, cameraCoord.z, destination.x, destination.y, destination.z, traceFlag, object, 0))
|
|
return hit, coords, entity
|
|
end
|
|
|
|
-- Used to Draw the text on the screen
|
|
local function Draw2DText(content, font, colour, scale, x, y)
|
|
SetTextFont(font)
|
|
SetTextScale(scale, scale)
|
|
SetTextColour(colour[1], colour[2], colour[3], 255)
|
|
SetTextEntry("STRING")
|
|
SetTextDropShadow(0, 0, 0, 0, 255)
|
|
SetTextDropShadow()
|
|
SetTextEdge(4, 0, 0, 0, 255)
|
|
SetTextOutline()
|
|
AddTextComponentString(content)
|
|
DrawText(x, y)
|
|
end
|
|
|
|
-- This handles placing the actual item that is network synced
|
|
local function placeItem(item, coords, heading, shouldSnapToGround)
|
|
local ped = PlayerPedId()
|
|
local itemName = item.item
|
|
local itemModel = item.model
|
|
local shouldFreezeItem = item.isFrozen
|
|
|
|
-- Cancel any active animation
|
|
ClearPedTasks(ped)
|
|
|
|
Progressbar("place_item", "Placing " .. item.label, 750, false, true, {
|
|
disableMovement = false,
|
|
disableCarMovement = false,
|
|
disableMouse = false,
|
|
disableCombat = true,
|
|
}, {
|
|
animDict = animationDict,
|
|
anim = animation,
|
|
flags = 0,
|
|
}, nil, nil, function() -- Done
|
|
-- Stop playing the animation
|
|
StopAnimTask(ped, animationDict, animation, 1.0)
|
|
|
|
-- Remove the item from the inventory
|
|
TriggerServerEvent("wp-placeables:server:RemoveItem", itemName)
|
|
|
|
LoadPropDict(itemModel)
|
|
|
|
-- Spawn prop on ground at the provided coords and heading
|
|
local obj = CreateObject(itemModel, GetEntityCoords(ped), true)
|
|
if obj ~= 0 then
|
|
SetEntityRotation(obj, 0.0, 0.0, heading, false, false)
|
|
SetEntityCoords(obj, coords)
|
|
|
|
if shouldFreezeItem then
|
|
FreezeEntityPosition(obj, true)
|
|
end
|
|
|
|
-- Some items dont go to the ground properly with this, and it actually makes them hover
|
|
if shouldSnapToGround then
|
|
PlaceObjectOnGroundProperly(obj)
|
|
end
|
|
|
|
-- Use statebag property itemName to set the itemName on the entity.
|
|
-- This value is used to grant the correct item back to the player when they pick it up.
|
|
-- It also solves the issue of the same model being used for multiple items
|
|
Entity(obj).state:set("itemName", itemName, true)
|
|
|
|
CreateLog(itemName, true)
|
|
end
|
|
|
|
SetModelAsNoLongerNeeded(itemModel)
|
|
end, function() -- Cancel
|
|
StopAnimTask(ped, animationDict, animation, 1.0)
|
|
Notify("Canceled..", "error")
|
|
end)
|
|
end
|
|
|
|
-- Starts a thread that puts the player into item placement mode
|
|
-- This will spawn a local object that only the player can see and move around to position it
|
|
-- Once the player places the object it will delete the local one and then create a new network synced object
|
|
local function startItemPlacementMode(item)
|
|
-- This is to prevent entering place mode multiple times if its already active
|
|
if isInPlaceItemMode then
|
|
Notify("Already placing an item", "error", 5000)
|
|
return
|
|
end
|
|
|
|
isInPlaceItemMode = true
|
|
local ped = PlayerPedId()
|
|
local itemModel = item.model
|
|
|
|
-- Create a local object for only this client (not synced to network) and make it transparent
|
|
local obj = CreateObject(itemModel, GetEntityCoords(ped), false, false)
|
|
SetEntityAlpha(obj, 150, false)
|
|
SetEntityCollision(obj, false, false)
|
|
|
|
local zOffset = 0
|
|
|
|
-- This is used to determine if the raycast should only detect the world or if it should detect everything (including vehicles)
|
|
local raycastDetectWorldOnly = true
|
|
|
|
CreateThread(function()
|
|
while isInPlaceItemMode do
|
|
-- Use raycast based on where the camera is pointed
|
|
local hit, coords, entity = RayCastGamePlayCamera(Config.ItemPlacementModeRadius, obj, raycastDetectWorldOnly)
|
|
|
|
-- Move the object to the coords from the raycast
|
|
SetEntityCoords(obj, coords.x, coords.y, coords.z + zOffset)
|
|
|
|
-- Display the controls
|
|
Draw2DText("[E] Place\n[Shift+E] Place on ground\n[Scroll Up/Down] Rotate\n[Shift+Scroll Up/Down] Raise/lower", 4, { 255, 255, 255, }, 0.4, 0.85, 0.85)
|
|
Draw2DText("[Scroll Click] Change mode\n[Right Click / Backspace] Exit place mode", 4, { 255, 255, 255, }, 0.4, 0.85, 0.945)
|
|
|
|
-- Handle various key presses and actions
|
|
|
|
-- Controls for placing item
|
|
|
|
-- Pressed Shift + E - Place object on ground
|
|
if IsControlJustReleased(0, 38) and IsControlPressed(0, 21) then
|
|
isInPlaceItemMode = false
|
|
|
|
local objHeading = GetEntityHeading(obj)
|
|
local snapToGround = true
|
|
|
|
DeleteEntity(obj)
|
|
placeItem(item, vector3(coords.x, coords.y, coords.z + zOffset), objHeading, snapToGround)
|
|
|
|
-- Pressed E - Place object at current position
|
|
elseif IsControlJustReleased(0, 38) then
|
|
isInPlaceItemMode = false
|
|
|
|
local objHeading = GetEntityHeading(obj)
|
|
local snapToGround = false
|
|
|
|
DeleteEntity(obj)
|
|
placeItem(item, vector3(coords.x, coords.y, coords.z + zOffset), objHeading, snapToGround)
|
|
end
|
|
|
|
-- Controls for rotating item
|
|
|
|
-- Mouse Wheel Up (and Shift not pressed), rotate by +10 degrees
|
|
if IsControlJustReleased(0, 241) and not IsControlPressed(0, 21) then
|
|
local objHeading = GetEntityHeading(obj)
|
|
SetEntityRotation(obj, 0.0, 0.0, objHeading + 10, false, false)
|
|
end
|
|
|
|
-- Mouse Wheel Down (and shift not pressed), rotate by -10 degrees
|
|
if IsControlJustReleased(0, 242) and not IsControlPressed(0, 21) then
|
|
local objHeading = GetEntityHeading(obj)
|
|
SetEntityRotation(obj, 0.0, 0.0, objHeading - 10, false, false)
|
|
end
|
|
|
|
-- Controls for raising/lowering item
|
|
|
|
-- Shift + Mouse Wheel Up, move item up
|
|
if IsControlPressed(0, 21) and IsControlJustReleased(0, 241) then
|
|
zOffset = zOffset + 0.1
|
|
if zOffset > Config.maxZOffset then
|
|
zOffset = Config.maxZOffset
|
|
end
|
|
end
|
|
|
|
-- Shift + Mouse Wheel Down, move item down
|
|
if IsControlPressed(0, 21) and IsControlJustReleased(0, 242) then
|
|
zOffset = zOffset - 0.1
|
|
if zOffset < Config.minZOffset then
|
|
zOffset = Config.minZOffset
|
|
end
|
|
end
|
|
|
|
-- Mouse Wheel Click, change placement mode
|
|
if IsControlJustReleased(0, 348) then
|
|
raycastDetectWorldOnly = not raycastDetectWorldOnly
|
|
end
|
|
|
|
-- Right click or Backspace to exit out of placement mode and delete the local object
|
|
if IsControlJustReleased(0, 177) then
|
|
isInPlaceItemMode = false
|
|
DeleteEntity(obj)
|
|
end
|
|
|
|
Wait(1)
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Handles picking up the prop, deleting it from the world and adding it to the players inventory
|
|
local function pickUpItem(itemData)
|
|
local ped = PlayerPedId()
|
|
local itemEntity = itemData.entity
|
|
local itemModel = itemData.itemModel
|
|
|
|
-- When picking up the item, try to get the itemName from the statebag property first, else fallback to the itemName from the itemData provided by the target script
|
|
-- Using the statebag property ensures we get the correct item name if the prop model is shared by multiple items..
|
|
local itemName = Entity(itemEntity).state.itemName or itemData.itemName
|
|
|
|
if itemName then
|
|
-- Cancel any active animation
|
|
ClearPedTasks(ped)
|
|
|
|
Progressbar("pickup_item", "Picking up item", 200, false, true, {
|
|
disableMovement = false,
|
|
disableCarMovement = false,
|
|
disableMouse = false,
|
|
disableCombat = true,
|
|
}, {
|
|
animDict = animationDict,
|
|
anim = animation,
|
|
flags = 0,
|
|
}, nil, nil, function() -- Done
|
|
-- Stop playing the animation
|
|
StopAnimTask(ped, animationDict, animation, 1.0)
|
|
|
|
-- Add the item to the inventory
|
|
TriggerServerEvent("wp-placeables:server:AddItem", itemName)
|
|
|
|
-- First request control of networkId and wait until have control of netId before deleting it
|
|
-- Item will not properly delete if the client doesn't have control of the networkId
|
|
local coords = GetEntityCoords(itemEntity)
|
|
local netId = NetworkGetNetworkIdFromEntity(itemEntity)
|
|
RequestNetworkControlOfObject(netId, itemEntity)
|
|
SetEntityAsMissionEntity(itemEntity, true, true)
|
|
DeleteEntity(itemEntity)
|
|
|
|
local object = { coords = coords, model = itemModel, }
|
|
TriggerServerEvent("wp-placeables:server:deleteWorldObject", object)
|
|
|
|
CreateLog(itemName, false)
|
|
end, function() -- Cancel
|
|
StopAnimTask(ped, animationDict, animation, 1.0)
|
|
Notify("Canceled..", "error")
|
|
end)
|
|
end
|
|
end
|
|
|
|
RegisterNetEvent("wp-placeables:client:placeItem", function(item)
|
|
if not IsPedInAnyVehicle(PlayerPedId(), true) then
|
|
startItemPlacementMode(item)
|
|
else
|
|
Notify("You cannot place items while in a vehicle", "error", 5000)
|
|
end
|
|
end)
|
|
|
|
RegisterNetEvent("wp-placeables:client:pickUpItem", function(data)
|
|
pickUpItem(data)
|
|
end)
|
|
|
|
-- Setup each placeable prop to use QB target
|
|
-- Itemname is in the options so we know which item to give back when picked up
|
|
for _, prop in pairs(Config.PlaceableProps) do
|
|
local pickUpEvent = "wp-placeables:client:pickUpItem"
|
|
if prop.customPickupEvent then
|
|
pickUpEvent = prop.customPickupEvent
|
|
end
|
|
local targetOptions = {
|
|
{
|
|
event = pickUpEvent,
|
|
icon = "fas fa-hand-holding",
|
|
label = "Pick up",
|
|
itemName = prop.item,
|
|
itemModel = prop.model,
|
|
},
|
|
}
|
|
|
|
-- Add custom target options to the target options for this item prop
|
|
if prop.customTargetOptions then
|
|
for _, customOption in pairs(prop.customTargetOptions) do
|
|
-- Stamp the itemName and itemModel onto the data so the custom events have access to this info
|
|
customOption.itemName = prop.item
|
|
customOption.itemModel = prop.model
|
|
|
|
targetOptions[#targetOptions + 1] = customOption
|
|
end
|
|
end
|
|
|
|
-- Make sure we only define the target options once for each model
|
|
-- If you define the same model twice:
|
|
-- In qb-target, it will override the options, and the last one defined is used
|
|
-- In ox_target, it will append the options, resulting in N duplicate options
|
|
if not targetModels[prop.model] then
|
|
AddTargetModel(prop.model, {
|
|
options = targetOptions,
|
|
distance = 1.5,
|
|
})
|
|
targetModels[prop.model] = true
|
|
end
|
|
end
|
|
|
|
-- Delete the world object
|
|
-- object = {coords = coords, model = itemModel}
|
|
RegisterNetEvent("wp-placeables:client:deleteWorldObject", function(object)
|
|
local entity = GetClosestObjectOfType(object.coords.x, object.coords.y, object.coords.z, 0.1, object.model, false, false, false)
|
|
if DoesEntityExist(entity) then
|
|
SetEntityAsMissionEntity(entity, 1, 1)
|
|
DeleteObject(entity)
|
|
SetEntityAsNoLongerNeeded(entity)
|
|
end
|
|
end)
|
|
|
|
-- Runs a thread to loop through the deleted world objects table and removes the item if it exists
|
|
-- This is to handle cases if the item were to have respawned
|
|
-- Disabling this for now since we dont need to care if the item respawns
|
|
-- There is currently an issue where this was being used for both player placed objects as well as world props
|
|
-- If you placed an item, picked it up and placed the same item down again, the cleanup thread would delete it since its at the same coords.
|
|
-- If we want to re-enable this, we need to find a way to only use this cleanup thread on world spawned props
|
|
-- AddEventHandler('QBCore:Client:OnPlayerLoaded', function()
|
|
-- QBCore.Functions.TriggerCallback('wp-placeables:server:GetDeletedWorldObjects', function(deletedObjects)
|
|
-- objects = deletedObjects
|
|
-- end)
|
|
-- end)
|
|
-- CreateThread(function()
|
|
-- while true do
|
|
-- for k = 1, #objects, 1 do
|
|
-- v = objects[k]
|
|
-- local entity = GetClosestObjectOfType(v.coords.x, v.coords.y, v.coords.z, 0.1, v.model, false, false, false)
|
|
-- if DoesEntityExist(entity) then
|
|
-- SetEntityAsMissionEntity(entity, 1, 1)
|
|
-- DeleteObject(entity)
|
|
-- SetEntityAsNoLongerNeeded(entity)
|
|
-- end
|
|
-- end
|
|
-- Wait(10000)
|
|
-- end
|
|
-- end)
|