2025-03-30 21:06:58 -05:00
Scaleform = Scaleform or Require ( "lib/scaleform/client/scaleform.lua" )
Utility = Utility or Require ( "lib/utility/client/utility.lua" )
Raycast = Raycast or Require ( "lib/raycast/client/raycast.lua" )
2025-06-04 15:29:21 -05:00
Language = Language or Require ( "modules/locales/shared.lua" )
2025-02-05 17:42:50 -06:00
PlaceableObject = PlaceableObject or {}
2025-02-12 08:58:36 -06:00
2025-06-30 02:07:20 -05:00
-- Register key mappings for placement controls
2025-07-09 08:10:01 -05:00
RegisterKeyMapping ( '+place_object' , locale ( 'placeable_object.object_place' ), 'mouse_button' , 'MOUSE_LEFT' )
RegisterKeyMapping ( '+cancel_placement' , locale ( 'placeable_object.object_cancel' ), 'mouse_button' , 'MOUSE_RIGHT' )
2025-06-30 02:07:20 -05:00
RegisterKeyMapping ( '+rotate_left' , locale ( 'placeable_object.rotate_left' ), 'keyboard' , 'LEFT' )
RegisterKeyMapping ( '+rotate_right' , locale ( 'placeable_object.rotate_right' ), 'keyboard' , 'RIGHT' )
2025-07-09 08:10:01 -05:00
RegisterKeyMapping ( '+scroll_up' , locale ( 'placeable_object.object_scroll_up' ), 'mouse_wheel' , 'IOM_WHEEL_UP' )
RegisterKeyMapping ( '+scroll_down' , locale ( 'placeable_object.object_scroll_down' ), 'mouse_wheel' , 'IOM_WHEEL_DOWN' )
2025-06-30 02:07:20 -05:00
RegisterKeyMapping ( '+depth_modifier' , locale ( 'placeable_object.depth_modifier' ), 'keyboard' , 'LCONTROL' )
local state = {
isPlacing = false ,
currentEntity = nil ,
mode = 'normal' , -- 'normal' or 'movement'
promise = nil ,
scaleform = nil ,
-- Placement settings
depth = 2.0 ,
heading = 0.0 ,
height = 0.0 ,
snapToGround = true ,
paused = false ,
-- Current settings
settings = {},
boundaryCheck = nil ,
-- Key press states
keys = {
placeObject = false ,
cancelPlacement = false ,
rotateLeft = false ,
rotateRight = false ,
scrollUp = false ,
scrollDown = false ,
depthModifier = false
}
}
-- Command handlers for key mappings
RegisterCommand ( '+place_object' , function ()
if state.isPlacing then
state.keys . placeObject = true
end
end , false )
RegisterCommand ( '-place_object' , function ()
state.keys . placeObject = false
end , false )
RegisterCommand ( '+cancel_placement' , function ()
if state.isPlacing then
state.keys . cancelPlacement = true
end
end , false )
RegisterCommand ( '-cancel_placement' , function ()
state.keys . cancelPlacement = false
end , false )
RegisterCommand ( '+rotate_left' , function ()
if state.isPlacing then
state.keys . rotateLeft = true
end
end , false )
RegisterCommand ( '-rotate_left' , function ()
state.keys . rotateLeft = false
end , false )
RegisterCommand ( '+rotate_right' , function ()
if state.isPlacing then
state.keys . rotateRight = true
end
end , false )
RegisterCommand ( '-rotate_right' , function ()
state.keys . rotateRight = false
end , false )
RegisterCommand ( '+scroll_up' , function ()
if state.isPlacing then
state.keys . scrollUp = true
end
end , false )
RegisterCommand ( '-scroll_up' , function ()
state.keys . scrollUp = false
end , false )
RegisterCommand ( '+scroll_down' , function ()
if state.isPlacing then
state.keys . scrollDown = true
end
end , false )
RegisterCommand ( '-scroll_down' , function ()
state.keys . scrollDown = false
end , false )
RegisterCommand ( '+depth_modifier' , function ()
if state.isPlacing then
state.keys . depthModifier = true
end
end , false )
RegisterCommand ( '-depth_modifier' , function ()
state.keys . depthModifier = false
end , false )
2025-02-05 17:42:50 -06:00
2025-06-04 15:29:21 -05:00
-- Utility functions
2025-06-30 02:07:20 -05:00
local function getMouseWorldPos ( depth )
2025-02-05 17:42:50 -06:00
local screenX = GetDisabledControlNormal ( 0 , 239 )
local screenY = GetDisabledControlNormal ( 0 , 240 )
local world , normal = GetWorldCoordFromScreenCoord ( screenX , screenY )
local playerPos = GetEntityCoords ( PlayerPedId ())
2025-06-30 02:07:20 -05:00
return playerPos + normal * depth
2025-02-05 17:42:50 -06:00
end
2025-06-30 15:18:01 -05:00
-- local function isInBoundary(pos, boundary)
-- if not boundary then return true end
2025-06-30 02:07:20 -05:00
2025-06-30 15:18:01 -05:00
-- local x, y, z = table.unpack(pos)
2025-03-24 18:06:52 -04:00
2025-06-30 15:18:01 -05:00
-- -- Handle legacy min/max boundary format for backwards compatibility
-- if boundary.min and boundary.max then
-- local minX, minY, minZ = table.unpack(boundary.min)
-- local maxX, maxY, maxZ = table.unpack(boundary.max)
-- return x >= minX and x <= maxX and y >= minY and y <= maxY and z >= minZ and z <= maxZ
-- end
2025-06-04 15:29:21 -05:00
2025-06-30 15:18:01 -05:00
-- -- Handle list of points (polygon boundary)
-- if boundary.points and #boundary.points > 0 then
-- local points = boundary.points
-- local minZ = boundary.minZ or -math.huge
-- local maxZ = boundary.maxZ or math.huge
2025-06-04 15:29:21 -05:00
2025-06-30 15:18:01 -05:00
-- -- Check Z bounds first
-- if z < minZ or z > maxZ then
-- return false
-- end
2025-06-30 02:07:20 -05:00
2025-06-30 15:18:01 -05:00
-- -- Point-in-polygon test using ray casting algorithm (improved version)
-- local inside = false
-- local n = #points
2025-06-30 02:07:20 -05:00
2025-06-30 15:18:01 -05:00
-- for i = 1, n do
-- local j = i == n and 1 or i + 1 -- Next point (wrap around)
2025-06-30 02:07:20 -05:00
2025-06-30 15:18:01 -05:00
-- local xi, yi = points[i].x or points[i][1], points[i].y or points[i][2]
-- local xj, yj = points[j].x or points[j][1], points[j].y or points[j][2]
2025-06-30 02:07:20 -05:00
2025-06-30 15:18:01 -05:00
-- -- Ensure xi, yi, xj, yj are numbers
-- if not (xi and yi and xj and yj) then
-- goto continue
-- end
2025-06-30 02:07:20 -05:00
2025-06-30 15:18:01 -05:00
-- -- Ray casting test
-- if ((yi > y) ~= (yj > y)) then
-- -- Calculate intersection point
-- local intersect = (xj - xi) * (y - yi) / (yj - yi) + xi
-- if x < intersect then
-- inside = not inside
-- end
-- end
2025-06-30 02:07:20 -05:00
2025-06-30 15:18:01 -05:00
-- ::continue::
-- end
2025-06-30 02:07:20 -05:00
2025-06-30 15:18:01 -05:00
-- return inside
-- end
2025-06-30 02:07:20 -05:00
2025-06-30 15:18:01 -05:00
-- -- Fallback to true if boundary format is not recognized
-- return true
-- end
2025-02-05 17:42:50 -06:00
2025-06-30 02:07:20 -05:00
local function checkMaterialAndBoundary ()
if not state.currentEntity then return true end
2025-02-05 17:42:50 -06:00
2025-06-30 02:07:20 -05:00
local pos = GetEntityCoords ( state.currentEntity )
2025-06-30 15:18:01 -05:00
local inBounds = Bridge.Math . InBoundary ( pos , state.settings . boundary )
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
-- Check built-in boundary first
if state.settings . boundary and not inBounds then return false end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
-- Check custom boundary function if provided
if state.settings . customCheck then
local customResult = state.settings . customCheck ( pos , state.currentEntity , state.settings )
if not customResult then return false end
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
-- Check allowed materials
if state.settings . allowedMats then
local hit , _ , _ , _ , materialHash = GetShapeTestResult ( StartShapeTestRay ( pos.x , pos.y , pos.z + 1.0 , pos.x , pos.y , pos.z - 5.0 , - 1 , 0 , 7 ))
if hit == 1 then
for _ , allowedMat in ipairs ( state.settings . allowedMats ) do
if materialHash == GetHashKey ( allowedMat ) then
return inBounds
end
end
return false
2025-06-04 15:29:21 -05:00
end
end
2025-06-30 02:07:20 -05:00
return inBounds
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
local function checkMaterialAndBoundaryDetailed ()
if not state.currentEntity then return true , true , true end
local pos = GetEntityCoords ( state.currentEntity )
2025-06-30 15:18:01 -05:00
local inBounds = Bridge.Math . InBoundary ( pos , state.settings . boundary )
2025-06-30 02:07:20 -05:00
local customCheckPassed = true
local materialCheckPassed = true
-- Check built-in boundary first
if state.settings . boundary and not inBounds then
return false , false , customCheckPassed
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
-- Check custom boundary function if provided
if state.settings . customCheck then
customCheckPassed = state.settings . customCheck ( pos , state.currentEntity , state.settings )
if not customCheckPassed then
return false , inBounds , false
2025-06-04 15:29:21 -05:00
end
end
2025-06-30 02:07:20 -05:00
-- Check allowed materials
if state.settings . allowedMats then
local hit , _ , _ , _ , materialHash = GetShapeTestResult ( StartShapeTestRay ( pos.x , pos.y , pos.z + 1.0 , pos.x , pos.y , pos.z - 5.0 , - 1 , 0 , 7 ))
if hit == 1 then
for _ , allowedMat in ipairs ( state.settings . allowedMats ) do
if materialHash == GetHashKey ( allowedMat ) then
return inBounds , inBounds , customCheckPassed
end
2025-02-05 17:42:50 -06:00
end
2025-06-30 02:07:20 -05:00
materialCheckPassed = false
return false , inBounds , customCheckPassed
2025-06-04 15:29:21 -05:00
end
end
2025-02-05 17:42:50 -06:00
2025-06-30 02:07:20 -05:00
return inBounds , inBounds , customCheckPassed
end
-- local function setupInstructionalButtons()
-- local buttons = {}
-- -- Common buttons
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = state.settings.config?.place_object?.name or 'Place Object:', keyIndex = state.settings.config?.place_object?.key or {223}, int = 5})
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = state.settings.config?.cancel_placement?.name or 'Cancel:', keyIndex = state.settings.config?.cancel_placement?.key or {25}, int = 4})
-- if state.mode == 'normal' then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Rotate:', keyIndex = {241, 242}, int = 3})
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Depth:', keyIndex = {224}, int = 2})
-- if state.settings.allowVertical then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Height:', keyIndex = {16, 17}, int = 1})
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Toggle Ground Snap:', keyIndex = {19}, int = 0})
-- end
-- if state.settings.allowMovement then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Movement Mode:', keyIndex = {38}, int = 6})
-- end
-- elseif state.mode == 'movement' then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Move:', keyIndex = {32, 33, 34, 35}, int = 3})
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Rotate:', keyIndex = {174, 175}, int = 2})
-- if state.settings.allowVertical then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Up/Down:', keyIndex = {85, 48}, int = 1})
-- end
-- if state.settings.allowNormal then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Normal Mode:', keyIndex = {38}, int = 0})
-- end
-- end
-- table.insert(buttons, {type = "DRAW_INSTRUCTIONAL_BUTTONS"})
-- table.insert(buttons, {type = "SET_BACKGROUND_COLOUR"})
-- -- return Scaleform.SetupInstructionalButtons(buttons)
-- return nil -- Scaleform disabled for now
-- end
local function drawBoundaryBox ( boundary )
if not boundary then return end
-- Handle legacy min/max boundary format for backwards compatibility
if boundary.min and boundary.max then
local min = boundary.min
local max = boundary.max
-- Define the 8 corners of the box
local corners = {
vector3 ( min.x , min.y , min.z ), -- 1
vector3 ( max.x , min.y , min.z ), -- 2
vector3 ( max.x , max.y , min.z ), -- 3
vector3 ( min.x , max.y , min.z ), -- 4
vector3 ( min.x , min.y , max.z ), -- 5
vector3 ( max.x , min.y , max.z ), -- 6
vector3 ( max.x , max.y , max.z ), -- 7
vector3 ( min.x , max.y , max.z ), -- 8
}
-- Draw wireframe box
local r , g , b , a = 0 , 255 , 0 , 100
-- Bottom face
DrawLine ( corners [ 1 ]. x , corners [ 1 ]. y , corners [ 1 ]. z , corners [ 2 ]. x , corners [ 2 ]. y , corners [ 2 ]. z , r , g , b , a )
DrawLine ( corners [ 2 ]. x , corners [ 2 ]. y , corners [ 2 ]. z , corners [ 3 ]. x , corners [ 3 ]. y , corners [ 3 ]. z , r , g , b , a )
DrawLine ( corners [ 3 ]. x , corners [ 3 ]. y , corners [ 3 ]. z , corners [ 4 ]. x , corners [ 4 ]. y , corners [ 4 ]. z , r , g , b , a )
DrawLine ( corners [ 4 ]. x , corners [ 4 ]. y , corners [ 4 ]. z , corners [ 1 ]. x , corners [ 1 ]. y , corners [ 1 ]. z , r , g , b , a )
-- Top face
DrawLine ( corners [ 5 ]. x , corners [ 5 ]. y , corners [ 5 ]. z , corners [ 6 ]. x , corners [ 6 ]. y , corners [ 6 ]. z , r , g , b , a )
DrawLine ( corners [ 6 ]. x , corners [ 6 ]. y , corners [ 6 ]. z , corners [ 7 ]. x , corners [ 7 ]. y , corners [ 7 ]. z , r , g , b , a )
DrawLine ( corners [ 7 ]. x , corners [ 7 ]. y , corners [ 7 ]. z , corners [ 8 ]. x , corners [ 8 ]. y , corners [ 8 ]. z , r , g , b , a )
DrawLine ( corners [ 8 ]. x , corners [ 8 ]. y , corners [ 8 ]. z , corners [ 5 ]. x , corners [ 5 ]. y , corners [ 5 ]. z , r , g , b , a )
-- Vertical edges
DrawLine ( corners [ 1 ]. x , corners [ 1 ]. y , corners [ 1 ]. z , corners [ 5 ]. x , corners [ 5 ]. y , corners [ 5 ]. z , r , g , b , a )
DrawLine ( corners [ 2 ]. x , corners [ 2 ]. y , corners [ 2 ]. z , corners [ 6 ]. x , corners [ 6 ]. y , corners [ 6 ]. z , r , g , b , a )
DrawLine ( corners [ 3 ]. x , corners [ 3 ]. y , corners [ 3 ]. z , corners [ 7 ]. x , corners [ 7 ]. y , corners [ 7 ]. z , r , g , b , a )
DrawLine ( corners [ 4 ]. x , corners [ 4 ]. y , corners [ 4 ]. z , corners [ 8 ]. x , corners [ 8 ]. y , corners [ 8 ]. z , r , g , b , a )
2025-06-04 15:29:21 -05:00
return
end
2025-06-30 02:07:20 -05:00
-- Handle list of points (polygon boundary)
if boundary.points and # boundary.points > 0 then
local points = boundary.points
local minZ = boundary.minZ or 0
local maxZ = boundary.maxZ or 50
local r , g , b , a = 0 , 255 , 0 , 100
-- Draw bottom polygon outline
for i = 1 , # points do
local currentPoint = points [ i ]
local nextPoint = points [ i % # points + 1 ] -- Wrap around to first point
local x1 , y1 = currentPoint.x or currentPoint [ 1 ], currentPoint.y or currentPoint [ 2 ]
local x2 , y2 = nextPoint.x or nextPoint [ 1 ], nextPoint.y or nextPoint [ 2 ]
-- Bottom edge
DrawLine ( x1 , y1 , minZ , x2 , y2 , minZ , r , g , b , a )
-- Top edge
DrawLine ( x1 , y1 , maxZ , x2 , y2 , maxZ , r , g , b , a )
-- Vertical edge
DrawLine ( x1 , y1 , minZ , x1 , y1 , maxZ , r , g , b , a )
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
return
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
local function drawEntityBoundingBox ( entity , inBounds )
if not entity or not DoesEntityExist ( entity ) then return end
-- Enable entity outline
SetEntityDrawOutlineShader ( 1 )
SetEntityDrawOutline ( entity , true )
-- Set color based on boundary status
if inBounds then
-- Green outline for valid placement
SetEntityDrawOutlineColor ( 0 , 255 , 0 , 255 )
else
-- Red outline for invalid placement
SetEntityDrawOutlineColor ( 255 , 0 , 0 , 255 )
end
end
local function handleNormalMode ()
if not state.isPlacing or state.mode ~= 'normal' or state.paused then
2025-06-04 15:29:21 -05:00
return
end
2025-06-30 02:07:20 -05:00
-- Disable conflicting controls
DisableControlAction ( 0 , 24 , true ) -- Attack
DisableControlAction ( 0 , 25 , true ) -- Aim
DisableControlAction ( 0 , 36 , true ) -- Duck
local moveSpeed = state.keys . depthModifier and ( state.settings . depthStep or 0.1 ) or ( state.settings . rotationStep or 0.5 )
-- Scroll wheel controls using key mappings
if state.keys . depthModifier then -- Depth modifier held - depth control
if state.keys . scrollUp then
state.keys . scrollUp = false -- Reset the key state
state.depth = math.min ( state.settings . maxDepth , state.depth + moveSpeed ) -- Use maxDepth setting
elseif state.keys . scrollDown then
state.keys . scrollDown = false -- Reset the key state
state.depth = math.max ( 1.0 , state.depth - moveSpeed ) -- Fixed: scroll down decreases distance
end
else -- Regular scroll - rotation
if state.keys . scrollUp then
state.keys . scrollUp = false -- Reset the key state
state.heading = state.heading - 5.0 -- Fixed: scroll up = counterclockwise
elseif state.keys . scrollDown then
state.keys . scrollDown = false -- Reset the key state
state.heading = state.heading + 5.0 -- Fixed: scroll down = clockwise
2025-06-04 15:29:21 -05:00
end
end
2025-06-30 02:07:20 -05:00
-- -- Arrow key rotation using key mappings
-- if state.keys.rotateLeft then
-- state.heading = state.heading + 2.0
-- elseif state.keys.rotateRight then
-- state.heading = state.heading - 2.0
-- end
-- Height controls (only if vertical movement allowed and not snapped to ground)
if state.settings . allowVertical and not state.snapToGround then
if IsControlPressed ( 0 , 16 ) then -- Q
state.height = state.height + ( state.settings . heightStep or 0.5 )
elseif IsControlPressed ( 0 , 17 ) then -- E
state.height = state.height - ( state.settings . heightStep or 0.5 )
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
end
-- Toggle ground snap
if state.settings . allowVertical and IsControlJustPressed ( 0 , 19 ) then -- Alt
state.snapToGround = not state.snapToGround
if state.snapToGround then
state.height = 0.0
2025-06-04 15:29:21 -05:00
end
end
2025-02-05 17:42:50 -06:00
2025-06-30 02:07:20 -05:00
-- Switch to movement mode
if state.settings . allowMovement and IsControlJustPressed ( 0 , 38 ) then -- E
state.mode = 'movement'
SetEntityCollision ( state.currentEntity , false , false )
end
-- Update entity position
local pos = getMouseWorldPos ( state.depth )
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
if not state.snapToGround and state.settings . allowVertical then
pos = pos + vector3 ( 0 , 0 , state.height )
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
if state.currentEntity then
2025-06-30 15:18:01 -05:00
SetEntityCoords ( state.currentEntity , pos.x , pos.y , pos.z , false , false , false , false )
2025-06-30 02:07:20 -05:00
SetEntityHeading ( state.currentEntity , state.heading )
if state.snapToGround then
local slerp = PlaceObjectOnGroundProperly ( state.currentEntity )
if not slerp then
-- If the object can't be placed on the ground, adjust its Z position
local groundZ , _z = GetGroundZFor_3dCoord ( pos.x , pos.y , pos.z + 50 , false )
if groundZ then
SetEntityCoords ( state.currentEntity , pos.x , pos.y , _z , false , false , false , true )
end
end
2025-06-04 15:29:21 -05:00
end
end
2025-06-30 02:07:20 -05:00
-- Visual feedback
if not state.settings . disableSphere then
DrawSphere ( pos.x , pos.y , pos.z , 0.5 , 255 , 0 , 0 , 50 )
end
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
local function handleMovementMode ()
if not state.isPlacing or state.mode ~= 'movement' or not DoesEntityExist ( state.currentEntity ) then
2025-06-04 15:29:21 -05:00
return
end
2025-06-30 02:07:20 -05:00
-- Disable player movement
DisableControlAction ( 0 , 30 , true ) -- Move Left/Right
DisableControlAction ( 0 , 31 , true ) -- Move Forward/Back
DisableControlAction ( 0 , 36 , true ) -- Duck
DisableControlAction ( 0 , 21 , true ) -- Sprint
DisableControlAction ( 0 , 22 , true ) -- Jump
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
local coords = GetEntityCoords ( state.currentEntity )
local heading = GetEntityHeading ( state.currentEntity )
local moveSpeed = IsControlPressed ( 0 , 21 ) and ( state.settings . movementStepFast or 0.5 ) or ( state.settings . movementStep or 0.1 ) -- Faster with shift
2025-06-04 15:29:21 -05:00
local moved = false
2025-06-30 02:07:20 -05:00
-- Get camera direction for relative movement
2025-06-04 15:29:21 -05:00
local camRot = GetGameplayCamRot ( 2 )
local camHeading = math.rad ( camRot.z )
local camForward = vector3 ( - math.sin ( camHeading ), math.cos ( camHeading ), 0 )
local camRight = vector3 ( math.cos ( camHeading ), math.sin ( camHeading ), 0 )
2025-06-30 02:07:20 -05:00
-- WASD movement
if IsControlPressed ( 0 , 32 ) then -- W
coords = coords + camForward * moveSpeed
2025-06-04 15:29:21 -05:00
moved = true
2025-06-30 02:07:20 -05:00
elseif IsControlPressed ( 0 , 33 ) then -- S
coords = coords - camForward * moveSpeed
2025-06-04 15:29:21 -05:00
moved = true
end
2025-06-30 02:07:20 -05:00
if IsControlPressed ( 0 , 34 ) then -- A
coords = coords - camRight * moveSpeed
2025-06-04 15:29:21 -05:00
moved = true
2025-06-30 02:07:20 -05:00
elseif IsControlPressed ( 0 , 35 ) then -- D
coords = coords + camRight * moveSpeed
2025-06-04 15:29:21 -05:00
moved = true
end
2025-06-30 02:07:20 -05:00
-- Vertical movement
if state.settings . allowVertical then
if IsControlPressed ( 0 , 85 ) then -- Q
coords = coords + vector3 ( 0 , 0 , moveSpeed )
moved = true
elseif IsControlPressed ( 0 , 48 ) then -- Z
coords = coords + vector3 ( 0 , 0 , - moveSpeed )
moved = true
end
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
-- Rotation
if IsControlPressed ( 0 , 174 ) then -- Left arrow
heading = heading + 2.0
2025-06-04 15:29:21 -05:00
moved = true
2025-06-30 02:07:20 -05:00
elseif IsControlPressed ( 0 , 175 ) then -- Right arrow
heading = heading - 2.0
2025-06-04 15:29:21 -05:00
moved = true
end
2025-06-30 02:07:20 -05:00
-- Apply changes
2025-06-04 15:29:21 -05:00
if moved then
2025-06-30 02:07:20 -05:00
SetEntityCoords ( state.currentEntity , coords.x , coords.y , coords.z , false , false , false , true )
SetEntityHeading ( state.currentEntity , heading )
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
-- Switch to normal mode
if state.settings . allowNormal and IsControlJustPressed ( 0 , 38 ) then -- E
state.mode = 'normal'
SetEntityCollision ( state.currentEntity , true , true )
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
-- Snap to ground
if IsControlJustPressed ( 0 , 19 ) then -- Alt
PlaceObjectOnGroundProperly ( state.currentEntity )
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
local function placementLoop ()
CreateThread ( function ()
while state.isPlacing do
Wait ( 0 )
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
-- Handle input based on mode
if state.mode == 'normal' then
handleNormalMode ()
elseif state.mode == 'movement' then
handleMovementMode ()
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
-- Common controls using key mappings
if state.keys . placeObject then
state.keys . placeObject = false -- Reset the key state
local canPlace = checkMaterialAndBoundary ()
if canPlace then
2025-06-30 15:18:01 -05:00
Wait ( 100 )
2025-06-30 02:07:20 -05:00
local coords = GetEntityCoords ( state.currentEntity )
2025-06-30 15:18:01 -05:00
if not state.settings . allowVertical or state.snapToGround then
local groundZ , _z = GetGroundZFor_3dCoord ( coords.x , coords.y , coords.z + 50 , false )
if groundZ then
coords = vector3 ( coords.x , coords.y , _z )
end
end
2025-06-30 02:07:20 -05:00
2025-06-30 15:18:01 -05:00
local rotation = GetEntityRotation ( state.currentEntity )
2025-06-30 02:07:20 -05:00
if state.promise then
state.promise : resolve ({
entity = state.currentEntity ,
coords = coords ,
2025-06-30 15:18:01 -05:00
rotation = rotation ,
2025-06-30 02:07:20 -05:00
placed = true
})
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
PlaceableObject.Stop ()
break
end
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
-- Cancel placement using key mapping
if state.keys . cancelPlacement then
state.keys . cancelPlacement = false -- Reset the key state
if state.promise then
state.promise : resolve ( false )
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
PlaceableObject.Stop ()
break
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
-- Check if entity is outside boundary and cancel if so
if state.settings . boundary and state.currentEntity then
local canPlace = checkMaterialAndBoundary ()
if not canPlace then
if state.promise then
state.promise : resolve ( false )
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
PlaceableObject.Stop ()
break
end
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
-- Draw boundary if exists and enabled
if state.settings . drawBoundary then
drawBoundaryBox ( state.settings . boundary )
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
-- Draw entity bounding box if enabled
if state.settings . drawInBoundary and state.currentEntity then
local overallResult , boundaryResult , customResult = checkMaterialAndBoundaryDetailed ()
-- Show red if any check fails, green if all pass
local inBounds = overallResult
drawEntityBoundingBox ( state.currentEntity , inBounds )
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
-- Show help text for placement controls
local placementText = {
string.format ( locale ( 'placeable_object.place_object_place' ), Bridge.Utility . GetCommandKey ( '+place_object' )),
string.format ( locale ( 'placeable_object.place_object_cancel' ), Bridge.Utility . GetCommandKey ( '+cancel_placement' )),
-- string.format(locale('placeable_object.rotate_left'), Bridge.Utility.GetCommandKey('+rotate_left')),
-- string.format(locale('placeable_object.rotate_right'), Bridge.Utility.GetCommandKey('+rotate_right')),
string.format ( locale ( 'placeable_object.place_object_scroll_up' ), Bridge.Utility . GetCommandKey ( '+scroll_up' )),
string.format ( locale ( 'placeable_object.place_object_scroll_down' ), Bridge.Utility . GetCommandKey ( '+scroll_down' )),
string.format ( locale ( 'placeable_object.depth_modifier' ), Bridge.Utility . GetCommandKey ( '+depth_modifier' ))
}
Bridge.Notify . ShowHelpText ( type ( placementText ) == 'table' and table.concat ( placementText ))
-- -- Draw entity bounding box
-- drawEntityBoundingBox(state.currentEntity, checkMaterialAndBoundary())
-- -- Update instructional buttons
-- if state.scaleform then
-- Scaleform.RenderInstructionalButtons(state.scaleform)
-- end
end
end )
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
-- Main functions
---@param model - Model name or hash
---@param settings - Configuration table:
--[[
depth (3.0): Starting distance from player,
allowVertical (false): Enable height controls,
allowMovement (false): Enable WASD mode,
disableSphere (false): Hide position indicator,
boundary: Area restriction {min = vector3(), max = vector3()},
allowedMats: Surface materials {"concrete", "grass"},
depthStep (0.1): Step size for depth adjustment,
rotationStep (0.5): Step size for rotation,
heightStep (0.5): Step size for height adjustment,
movementStep (0.1): Step size for normal movement,
movementStepFast (0.5): Step size for fast movement (with shift),
maxDepth (50.0): Maximum distance from player,
--]]
---@returns Promise with: {entity, coords, heading, placed, cancelled}
--[[
Example:
local result = Citizen.Await(PlaceableObject.Create("prop_barrier_work05", {
depth = 5.0,
allowVertical = false
}))
--]]
2025-06-04 15:29:21 -05:00
function PlaceableObject . Create ( model , settings )
2025-06-30 02:07:20 -05:00
if state.isPlacing then
PlaceableObject.Stop ()
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
-- Default settings
2025-06-04 15:29:21 -05:00
settings = settings or {}
2025-06-30 02:07:20 -05:00
settings.depth = settings.depth or 3.0 -- Start closer to player
settings.allowVertical = settings.allowVertical or false
settings.allowMovement = settings.allowMovement or false
settings.allowNormal = settings.allowNormal or false
settings.disableSphere = settings.disableSphere or false
settings.drawBoundary = settings.drawBoundary or false
settings.drawInBoundary = settings.drawInBoundary or false
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
-- Movement speed settings
settings.depthStep = settings.depthStep or 0.1 -- Fine control for depth adjustment
settings.rotationStep = settings.rotationStep or 0.5 -- Normal rotation speed
settings.heightStep = settings.heightStep or 0.5 -- Height adjustment speed
settings.movementStep = settings.movementStep or 0.1 -- Normal movement speed
settings.movementStepFast = settings.movementStepFast or 0.5 -- Fast movement speed (with shift)
settings.maxDepth = settings.maxDepth or 5.0 -- Maximum distance from player
state.settings = settings
state.depth = settings.depth -- Use the settings depth
state.heading = - GetEntityHeading ( PlayerPedId ())
state.height = 0.0
state.snapToGround = not settings.allowVertical
state.mode = 'normal'
local p = promise.new ()
state.promise = p
2025-06-30 15:18:01 -05:00
local point = Bridge.ClientEntity . Create ({
2025-06-30 02:07:20 -05:00
id = 'placeable_object' ,
entityType = 'object' ,
model = model ,
coords = GetEntityCoords ( PlayerPedId ()),
rotation = vector3 ( 0.0 , 0.0 , state.heading ),
OnSpawn = function ( data )
state.currentEntity = data.spawned
SetEntityCollision ( state.currentEntity , false , false )
FreezeEntityPosition ( state.currentEntity , false )
-- Set initial position based on depth
local playerPos = GetEntityCoords ( PlayerPedId ())
local forward = GetEntityForwardVector ( PlayerPedId ())
local spawnPos = playerPos + forward * state.depth
SetEntityCoords ( state.currentEntity , spawnPos.x , spawnPos.y , spawnPos.z + state.height , false , false , false , true )
end ,
})
-- Setup instructional buttons
-- state.scaleform = setupInstructionalButtons()
state.scaleform = nil
state.isPlacing = true
-- Show help text for placement controls
local placementText = {
string.format ( locale ( 'placeable_object.place_object_place' ), Bridge.Utility . GetCommandKey ( '+place_object' )),
string.format ( locale ( 'placeable_object.place_object_cancel' ), Bridge.Utility . GetCommandKey ( '+cancel_placement' )),
-- string.format(locale('placeable_object.rotate_left'), Bridge.Utility.GetCommandKey('+rotate_left')),
-- string.format(locale('placeable_object.rotate_right'), Bridge.Utility.GetCommandKey('+rotate_right')),
string.format ( locale ( 'placeable_object.place_object_scroll_up' ), Bridge.Utility . GetCommandKey ( '+scroll_up' )),
string.format ( locale ( 'placeable_object.place_object_scroll_down' ), Bridge.Utility . GetCommandKey ( '+scroll_down' )),
string.format ( locale ( 'placeable_object.depth_modifier' ), Bridge.Utility . GetCommandKey ( '+depth_modifier' ))
2025-06-04 15:29:21 -05:00
}
2025-06-30 02:07:20 -05:00
Bridge.Notify . ShowHelpText ( type ( placementText ) == 'table' and table.concat ( placementText ))
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
placementLoop ()
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
return Citizen.Await ( p )
2025-02-05 17:42:50 -06:00
end
2025-06-04 15:29:21 -05:00
function PlaceableObject . Stop ()
2025-06-30 02:07:20 -05:00
Bridge.Notify . HideHelpText ()
if state.currentEntity and DoesEntityExist ( state.currentEntity ) then
-- Disable entity outline before deleting
SetEntityDrawOutline ( state.currentEntity , false )
DeleteObject ( state.currentEntity )
end
2025-06-04 15:29:21 -05:00
2025-06-30 02:07:20 -05:00
if state.scaleform then
Scaleform.Unload ( state.scaleform )
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
ClientEntity.Unregister ( 'placeable_object' )
-- Reset state
state.isPlacing = false
state.currentEntity = nil
state.mode = 'normal'
state.promise = nil
state.scaleform = nil
state.settings = {}
2025-06-04 15:29:21 -05:00
return true
end
2025-06-30 02:07:20 -05:00
-- Status functions
2025-06-04 15:29:21 -05:00
function PlaceableObject . IsPlacing ()
2025-06-30 02:07:20 -05:00
return state.isPlacing
2025-06-04 15:29:21 -05:00
end
function PlaceableObject . GetCurrentEntity ()
2025-06-30 02:07:20 -05:00
return state.currentEntity
2025-06-04 15:29:21 -05:00
end
function PlaceableObject . GetCurrentMode ()
2025-06-30 02:07:20 -05:00
return state.mode
2025-06-04 15:29:21 -05:00
end
2025-06-30 02:07:20 -05:00
AddEventHandler ( 'onResourceStop' , function ( resource )
if resource ~= GetCurrentResourceName () then return end
if state.isPlacing then
PlaceableObject.Stop ()
end
end )
2025-06-04 15:29:21 -05:00
2025-03-24 18:06:52 -04:00
return PlaceableObject