Advertisement
BADABATS

Placement Handler

May 20th, 2025 (edited)
154
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 10.30 KB | Gaming | 0 0
  1. -- Placement handler for my candy cooking game sweet empire
  2.  
  3. -- Youtube Link [[https://youtu.be/WHLzknKAk6w]]
  4.  
  5. -- Bools
  6. local interpolation = true
  7. local moveByGrid = true
  8. local buildModePlacement = true
  9.  
  10. -- Ints
  11. local rotationStep = 90
  12. local maxHeight = 90
  13.  
  14. -- Numbers/Floats
  15. local lerpLevel = 0.8 -- 0 = instand snapping, 1 = no movement at all/high interpolation
  16.  
  17. -- Other
  18. local gridTexture = "rbxassetid://7956721784" -- texture player sees when placing a object
  19.  
  20. local placement = {}
  21. placement.__index = placement
  22.  
  23. local players = game:GetService("Players")
  24. local runService = game:GetService("RunService")
  25. local contextActionService = game:GetService("ContextActionService")
  26. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  27.  
  28. local player = players.LocalPlayer
  29. local character = player.Character or player.CharacterAdded:Wait()
  30. local mouse = player:GetMouse()
  31.  
  32. -- Constructor Variables
  33. local GRID_SIZE
  34. local ITEM_LOCATION
  35. local ROTATE_KEY
  36. local TERMINATE_KEY
  37.  
  38. -- Activation Variables
  39. local object
  40. local placedObjects
  41. local plot
  42. local plotModel
  43. local stackable
  44.  
  45. local primary
  46.  
  47.  
  48. -- Variables used in calculations
  49. local posX
  50. local posY
  51. local posZ
  52. local speed = 1
  53. local rotation = 0
  54. local rotationVal = false
  55.  
  56. -- Other
  57. local DataEvent = ReplicatedStorage.Events.RemoteFunctions.GetData
  58. local collided = nil
  59.  
  60. -- Quick type variables
  61. local cframe = CFrame.new
  62. local angles = CFrame.fromEulerAnglesXYZ
  63. local clamp = math.clamp
  64.  
  65. local function renderGrid()
  66.     -- So that multiple textures dont get added
  67.     if plot:FindFirstChildOfClass("Texture") then
  68.         return
  69.     end
  70.     local texture = Instance.new("Texture")
  71.     texture.StudsPerTileU = GRID_SIZE*2
  72.     texture.StudsPerTileV = GRID_SIZE*2
  73.     texture.Texture = gridTexture
  74.     texture.Face = Enum.NormalId.Top
  75.     texture.Parent = plot
  76. end
  77.  
  78. -- Handles collisions on the hitbox
  79. local function changeHitBoxColor()
  80.     if primary then
  81.         if collided then
  82.             primary.Transparency = 0.7
  83.             primary.Color = Color3.fromRGB(255, 0, 4)
  84.         else
  85.             primary.Transparency = 0.7
  86.             primary.Color = Color3.fromRGB(4, 255, 0)
  87.         end
  88.     end
  89. end
  90.  
  91. -- Handles collisions on the hitbox
  92. local function handleCollisions()
  93.     if object then
  94.         collided = false
  95.        
  96.         local collisionPoint = primary.Touched:Connect(function() end)
  97.         local collisionPoints = primary:GetTouchingParts()
  98.        
  99.         for i = 1, #collisionPoints, 1 do
  100.             if not collisionPoints[i]:IsDescendantOf(object) and not collisionPoints[i]:IsDescendantOf(plotModel) then
  101.                 collided = true            
  102.                 break
  103.             end
  104.         end
  105.        
  106.         collisionPoint:Disconnect()
  107.        
  108.         return
  109.     end
  110. end
  111.  
  112. local function bounds(cf, offsetX, offsetZ)
  113.     local LOWER_X_BOUND
  114.     local LOWER_Z_BOUND
  115.     local UPPER_X_BOUND
  116.     local UPPER_Z_BOUND
  117.    
  118.     -- Calculates the bounds at each corner including model scale
  119.     LOWER_X_BOUND = plot.Position.X - (plot.Size.X*0.5) + offsetX
  120.     UPPER_X_BOUND = plot.Position.X + (plot.Size.X*0.5) - offsetX
  121.    
  122.     LOWER_Z_BOUND = plot.Position.Z - (plot.Size.Z*0.5) + offsetZ
  123.     UPPER_Z_BOUND = plot.Position.Z + (plot.Size.Z*0.5) - offsetZ
  124.    
  125.     local newX = clamp(cf.X, LOWER_X_BOUND, UPPER_X_BOUND)
  126.     local newZ = clamp(cf.Z, LOWER_Z_BOUND, UPPER_Z_BOUND)
  127.    
  128.     return cframe(newX, posY, newZ)
  129. end
  130.  
  131. -- Calculates the initial y position above an object
  132. local function calculateYPosition(toP, toS, oS)
  133.     return (toP + toS*0.5) + oS*0.5
  134. end
  135.  
  136. function placement.rotate(actionName, inputState, inputObj, scriptcall)
  137.     if inputState == Enum.UserInputState.Begin or scriptcall then
  138.         rotation += rotationStep
  139.         rotationVal = not rotationVal
  140.     end
  141. end
  142.  
  143. local function unbindInputs()
  144.     contextActionService:UnbindAction("Rotate")
  145.     contextActionService:UnbindAction("Cancel")
  146. end
  147.  
  148. function placement.cancel(actionName, inputState, inputObj, scriptcall)
  149.     if inputState == Enum.UserInputState.Begin or scriptcall then
  150.         -- Stop the rendering and movement logic immediately
  151.         runService:UnbindFromRenderStep("Input")
  152.  
  153.         -- Destroy the object
  154.         if object then
  155.             object:Destroy()
  156.             object = nil  -- Ensure the reference is cleared
  157.         end
  158.  
  159.         -- Destroy the texture from the plot
  160.         if plot and plot:FindFirstChild("Texture") then
  161.             plot:FindFirstChild("Texture"):Destroy()
  162.         end
  163.  
  164.         -- Clear the mouse target filter
  165.         mouse.TargetFilter = nil
  166.  
  167.         -- Unbind inputs to prevent further interactions
  168.         unbindInputs()
  169.  
  170.         -- Reset placement-related variables to ensure proper reinitialization on retry
  171.         placedObjects = nil
  172.         plot = nil
  173.         stackable = nil
  174.         primary = nil
  175.         rotation = 0
  176.         rotationVal = false
  177.         collided = nil  -- Reset collision state
  178.         speed = 1       -- Reset speed for consistency
  179.     end
  180. end
  181.  
  182. local function snap(c)
  183.     local newX = math.round(c.X/GRID_SIZE)*GRID_SIZE
  184.     local newZ = math.round(c.Z/GRID_SIZE)*GRID_SIZE
  185.    
  186.     return cframe(newX, 0 , newZ)
  187. end
  188.  
  189. -- Calculates the models position based on grid
  190. local function calculateItemPosition()
  191.     local finalCFrame = cframe(0,0,0)
  192.     local x, z
  193.     local offsetX, offsetZ
  194.    
  195.     -- Calculates offsets and swaps them on rotation to keep object snapped within the grid units
  196.     if rotationVal then
  197.         offsetX, offsetZ = primary.Size.X*0.5, primary.Size.Z*0.5
  198.         x, z = mouse.Hit.X - offsetX, mouse.Hit.Z - offsetZ
  199.     else
  200.         offsetX, offsetZ = primary.Size.Z*0.5, primary.Size.X*0.5
  201.         x, z = mouse.Hit.X - offsetX, mouse.Hit.Z - offsetZ    
  202.     end
  203.    
  204.     -- Calculates the y position
  205.     if stackable and mouse.Target and mouse.Target:IsDescendantOf(plot) then
  206.         posY = calculateYPosition(mouse.Target.Position.Y, mouse.Target.Size.Y, primary.Size.Y)
  207.     else
  208.         posY = calculateYPosition(plot.Position.Y, plot.Size.Y, primary.Size.Y)
  209.     end
  210.    
  211.     -- Calculates either snapped position or non snapped position
  212.     if moveByGrid then
  213.         local pltCFrame = cframe(plot.CFrame.X, plot.CFrame.Y, plot.CFrame.Z)
  214.         local pos = cframe(x, 0, z)
  215.         pos = snap(pos*pltCFrame:Inverse())
  216.         finalCFrame = pos*pltCFrame*cframe(offsetX, 0, offsetZ)
  217.     else
  218.         finalCFrame = cframe(mouse.Hit.X, posY, mouse.Hit.Z)
  219.     end
  220.    
  221.     finalCFrame = bounds(cframe(finalCFrame.X, posY, finalCFrame.Z), offsetX, offsetZ)
  222.    
  223.     return finalCFrame*angles(0, math.rad(rotation), 0)
  224. end
  225.  
  226.  
  227.  
  228. local function bindInputs()
  229.     contextActionService:BindAction("Rotate", placement.rotate, false, ROTATE_KEY) -- only for pc right now
  230.     contextActionService:BindAction("Cancel", placement.cancel, false, TERMINATE_KEY)
  231. end
  232.  
  233. -- Sets models position based on pivot
  234. local function translateObj()
  235.     if placedObjects and object.Parent == placedObjects then
  236.         handleCollisions()
  237.         changeHitBoxColor()    
  238.         object:PivotTo(object.PrimaryPart.CFrame:Lerp(calculateItemPosition(), speed))
  239.     end
  240. end
  241.  
  242. -- Returns the instant/non interpolated postition of the object at any given moment (non interpolated = not moving with the mouse but just tp'ing)
  243. local function getInstantCFrame()
  244.     return calculateItemPosition()
  245. end
  246.  
  247. function placement:place(remote)
  248.     if not collided and object then
  249.         -- Calculate the relative CFrame of the object to the plot
  250.         local cf = plot.CFrame:Inverse() * calculateItemPosition() -- assuming `calculateItemPosition` provides world CFrame       
  251.         if not remote:InvokeServer(object.Name, placedObjects, cf, plot, plotModel) then
  252.             placement.cancel(nil, nil, nil, true)
  253.             return true        
  254.         else
  255.             placement.cancel(nil, nil, nil, true)
  256.             return true
  257.         end
  258.     else
  259.         return false
  260.     end
  261. end
  262.  
  263. -- Check if model the will snap evenly on the plot, handling floating-point precision
  264. local function verifyPlane()
  265.     local plotX = math.round(plot.Size.X)   local plotZ = math.round(plot.Size.Z)   return plotX % GRID_SIZE == 0 and plotZ % GRID_SIZE == 0
  266. end
  267.  
  268. -- Confirms that the settings are valid for placement
  269. local function approvePlacement(id)
  270.     local playerData = DataEvent:InvokeServer()
  271.     if playerData.Inventory[id] == nil then
  272.         warn("player has no model in inventory he can place")
  273.         return false
  274.     end
  275.    
  276.     if not verifyPlane() then
  277.         warn("The model cannot snap on the plot. Please change the plot size to fix this")
  278.         return false
  279.     end
  280.    
  281.     if GRID_SIZE >= math.min(plot.Size.X, plot.Size.Z) then
  282.         warn("Grid size is too close to the size of the plot on at least one axis")
  283.         return false
  284.     end
  285.    
  286.     return true
  287. end
  288.  
  289. -- Constructor function
  290. function placement.new(g, objs, r, t)
  291.     local data = {}
  292.     local metaData = setmetatable(data, placement)
  293.    
  294.     GRID_SIZE = g
  295.     ITEM_LOCATION = objs
  296.     ROTATE_KEY = r
  297.     TERMINATE_KEY = t
  298.    
  299.     data.grid = GRID_SIZE
  300.     data.itemlocation = ITEM_LOCATION
  301.     data.rotatekey = ROTATE_KEY or Enum.KeyCode.R
  302.     data.terminatekey = TERMINATE_KEY or Enum.KeyCode.X
  303.    
  304.     return data
  305. end
  306.  
  307. -- activates placement (id = name, pobjs = placedOblects, plt = plotm, pltmdl = plot model, stk = stackable)
  308. function placement:activate(id, pobjs, plt, pltmdl, stk, player)
  309.     -- Ensure that the object is destroyed if it already exists
  310.     if object then
  311.         object:Destroy()
  312.         object = nil
  313.     end
  314.  
  315.     -- Attempt to clone the new object from ITEM_LOCATION
  316.     object = ITEM_LOCATION:FindFirstChild(id)
  317.     if not object then
  318.         warn("Item not found in ITEM_LOCATION: " .. id)
  319.         return "Item not available for placement"
  320.     end
  321.  
  322.     -- Clone and prepare the object for placement
  323.     object = object:Clone()
  324.     primary = object.PrimaryPart
  325.     if not primary then
  326.         warn("Primary part not found on object")
  327.         return "Primary part missing"
  328.     end
  329.  
  330.     placedObjects = pobjs
  331.     plot = plt
  332.     plotModel = pltmdl
  333.     stackable = stk
  334.     rotation = 0
  335.     rotationVal = true
  336.     collided = nil  -- Reset collision state
  337.  
  338.     -- Ensure `CanCollide` is false for placement so it does not interfere with player
  339.     for _, v in pairs(object:GetDescendants()) do
  340.         if v:IsA("BasePart") then
  341.             v.CanCollide = false
  342.         end
  343.     end
  344.  
  345.     -- Check if placement can activate
  346.     if not approvePlacement(id) then
  347.         warn("Placement could not activate")
  348.         placement.cancel(nil, nil, nil, true)  -- Call cancel to reset variables if placement cannot proceed
  349.         return "Placement could not activate"
  350.     end
  351.  
  352.     -- Set mouse target filter based on stackability
  353.     if not stackable then
  354.         mouse.TargetFilter = placedObjects
  355.     else
  356.         mouse.TargetFilter = object
  357.     end
  358.  
  359.     -- Set interpolation speed
  360.     local tempSpeed = interpolation and math.clamp(1 - lerpLevel, 0, 0.8) or 1
  361.     speed = tempSpeed
  362.  
  363.     -- Render the grid and bind inputs
  364.     renderGrid()
  365.     object.Parent = placedObjects
  366.     task.wait()
  367.     bindInputs()
  368.  
  369.     runService:BindToRenderStep("Input", Enum.RenderPriority.Input.Value, translateObj)
  370. end
  371.  
  372. return placement
  373.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement