· 4 years ago · May 30, 2021, 12:22 AM
1--[[
2 Object Placement Randomizer v 2.3
3 Plugin for Roblox Studio
4 Written by BahamutFierce
5
6 This plugin is designed to create a user-friendly way to copy an object
7 and place it over a wide area. Features numerous ways to customize the
8 copies, including:
9 - Customize size and position of placement area
10 - Place objects on terrain or parts
11 - Ability to limit placements to specific materials
12 - Can set starting orientations perpendicular to surface
13 - Clustering into groups of different sizes and densities
14 - Randomization of scale, rotation, orientation and depth
15]]
16
17-- Services
18local WorkspaceService = game:GetService("Workspace")
19local CoreGui = game:GetService("CoreGui")
20local RunService = game:GetService("RunService")
21local ChangeHistoryService = game:GetService("ChangeHistoryService")
22
23-- Toolbar Icon
24local toolbar = plugin:CreateToolbar("Generation")
25local scriptButton = toolbar:CreateButton("Object Placement Randomizer", "Randomly place objects in an area", "rbxassetid://6551643745")
26
27-- Get main plugin folder
28local folder = script.Parent
29
30-- Required modules
31local CollapsibleTitledSection = require(folder.CollapsibleTitledSection)
32local LabeledTextInput = require(folder.LabeledTextInput)
33local GuiUtilities = require(folder.GuiUtilities)
34local CustomTextButton = require(folder.CustomTextButton)
35local ImageButtonWithText = require(folder.ImageButtonWithText)
36local LabeledSlider = require(folder.LabeledSlider)
37local LabeledCheckbox = require(folder.LabeledCheckbox)
38local VerticallyScalingListFrame = require(folder.VerticallyScalingListFrame)
39local VerticalScrollingFrame = require(folder.VerticalScrollingFrame)
40
41local isGuiActive = false
42
43-- Make sure we're in studio
44if RunService:IsEdit() then
45 -- Get theme colors
46 local textColor = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainText)
47 local borderColor = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Border)
48 local backgroundColor = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
49
50 local rng = Random.new(tick())
51
52 -- Constants
53 local GUI_X_SIZE_MIN = 350
54 local GUI_Y_SIZE_MIN = 300
55 local GUI_X_SIZE = 350
56 local GUI_Y_SIZE = 400
57 local DEFAULT_AREA_POSITION = Vector3.new(0,0,0)
58 local DEFAULT_AREA_SIZE = Vector3.new(500,1,500)
59
60 -- Variables
61 local area = nil
62 local areaPosition = DEFAULT_AREA_POSITION
63 local areaSize = DEFAULT_AREA_SIZE
64 local areaConnection = nil
65 local objectName = ""
66 local number = 20
67 local scaleRange = 0
68 local maxRotation = 0
69 local maxInclination = 0
70 local depth = .50
71 local clusterSize = 5
72 local density = 25
73 local perpendicular = false
74 local onParts = false
75
76 -- Create table of enum materials
77 local materials = {}
78 local materialValid = {}
79 for _, i in pairs(Enum.Material:GetEnumItems()) do
80 if i ~= Enum.Material.Air and i ~= Enum.Material.ForceField and i ~= Enum.Material.Water then
81 materials[i.Name] = i
82 materialValid[i.Name]= true
83 end
84 end
85
86 local blacklist = {}
87 local components = {}
88 local instructions = nil
89
90 -- Create Gui window
91 local guiInfo = DockWidgetPluginGuiInfo.new(
92 Enum.InitialDockState.Float,
93 true,
94 false,
95 GUI_X_SIZE,
96 GUI_Y_SIZE,
97 GUI_X_SIZE_MIN,
98 GUI_Y_SIZE_MIN
99 )
100
101 local gui = plugin:CreateDockWidgetPluginGui("ObjectPlacementRandomizer", guiInfo)
102 gui.Title = "Object Placement Randomizer"
103
104 local progressFrame
105 local progressBackground
106 local progressBorder
107 local progressBar
108 local percentBox
109 local cancelButton
110 local cancelled = false
111
112 -- Check if gui already exists
113 local pluginGui = CoreGui:FindFirstChild("RandomizerGui")
114 if not pluginGui then
115 -- Create gui for progress bar
116 pluginGui = Instance.new("ScreenGui")
117 pluginGui.Name = "RandomizerGui"
118 pluginGui.Parent = CoreGui
119
120 progressFrame = Instance.new("Frame")
121 progressFrame.Name = "ProgressFrame"
122 progressFrame.Size = UDim2.new(.5, 0, 0, 25)
123 progressFrame.AnchorPoint = Vector2.new(.5, .5)
124 progressFrame.Position = UDim2.new(.5, 0, 1, -15)
125 progressFrame.BackgroundTransparency = 1
126 progressFrame.BorderSizePixel = 0
127 progressFrame.Parent = pluginGui
128
129 progressBackground = Instance.new("Frame")
130 progressBackground.Name = "ProgressBackground"
131 progressBackground.Size = UDim2.new(1, 0, 1, 0)
132 progressBackground.AnchorPoint = Vector2.new(.5, .5)
133 progressBackground.Position = UDim2.new(.5, 0, .5, 0)
134 progressBackground.BackgroundColor3 = backgroundColor
135 progressBackground.ZIndex = 1
136 progressBackground.Parent = progressFrame
137
138 progressBorder = progressBackground:Clone()
139 progressBorder.Name = "ProgressBorder"
140 progressBorder.BorderColor3 = borderColor
141 progressBorder.BackgroundTransparency = 1
142 progressBorder.ZIndex = 10
143 progressBorder.Parent = progressFrame
144
145 progressBar = Instance.new("Frame")
146 progressBar.Name = "ProgressBar"
147 progressBar.Size = UDim2.new(0, 0, 1, 0)
148 progressBar.AnchorPoint = Vector2.new(0, .5)
149 progressBar.Position = UDim2.new(0, 0, .5, 0)
150 progressBar.BorderColor3 = Color3.new(0.117647, 0.6, 0.109804)
151 progressBar.BackgroundColor3 = Color3.new(0.392157, 1, 0.254902)
152 progressBar.ZIndex = 5
153 progressBar.Parent = progressFrame
154
155 percentBox = Instance.new("TextLabel")
156 percentBox.Name = "PercentBox"
157 percentBox.Size = UDim2.new(0, 50, 0, 20)
158 percentBox.AnchorPoint = Vector2.new(.5, .5)
159 percentBox.Position = UDim2.new(.5, 0, .5, 0)
160 percentBox.BackgroundTransparency = 1
161 percentBox.Text = ""
162 percentBox.TextColor3 = textColor
163 percentBox.ZIndex = 10
164 percentBox.Parent = progressFrame
165
166 cancelButton = Instance.new("TextButton")
167 cancelButton.Name = "Cancel"
168 cancelButton.Size = UDim2.new(0, 70, 0, 25)
169 cancelButton.AnchorPoint = Vector2.new(.5, .5)
170 cancelButton.Position = UDim2.new(.5, 0, 0, -30)
171 cancelButton.TextColor3 = textColor
172 cancelButton.BackgroundColor3 = borderColor
173 cancelButton.Text = "Cancel"
174 cancelButton.Parent = progressFrame
175
176 cancelButton.Activated:Connect(function()
177 warn("Generation cancelled by user.")
178 cancelled = true
179 end)
180
181 local uiCorner = Instance.new("UICorner")
182 uiCorner.Parent = progressBackground
183 uiCorner = uiCorner:Clone()
184 uiCorner.Parent = progressBorder
185 uiCorner = uiCorner:Clone()
186 uiCorner.Parent = progressBar
187 uiCorner = uiCorner:Clone()
188 uiCorner.Parent = cancelButton
189
190 local uiGradient = Instance.new("UIGradient")
191 uiGradient.Color = ColorSequence.new{
192 ColorSequenceKeypoint.new(0, Color3.new(0, 0.333333, 0)),
193 ColorSequenceKeypoint.new(1, Color3.new(0, 1, 0))
194 }
195 uiGradient.Parent = progressBar
196 pluginGui.Enabled = false
197 else
198 progressFrame = pluginGui.ProgressFrame
199 progressBackground = progressFrame.ProgressBackground
200 progressBorder = progressFrame.ProgressBorder
201 progressBar = progressFrame.ProgressBar
202 percentBox = progressFrame.PercentBox
203 end
204
205 -- Create scrolling frames for gui
206 local scrollFrame = VerticalScrollingFrame.new("Main")
207 local listFrame = VerticallyScalingListFrame.new("Main")
208
209 -- Create collapsable section for instructions
210 local instructionsSection = CollapsibleTitledSection.new(
211 "instructions", -- name suffix of the gui object
212 "Instructions", -- the text displayed beside the collapsible arrow
213 true, -- have the content frame auto-update its size?
214 true, -- minimizable?
215 true -- minimized by default?
216 )
217
218 local instructionsFrame = GuiUtilities:MakeFrame("InstructionsFrame")
219 instructionsFrame.Size = UDim2.new(1,0,0,500)
220
221 local instructionsText = [[-- Area to Fill
222 NOTE: Area must be *above* the region you want to fill.
223 The plugin works by raycasting straight down.
224
225 Area Position: The center position in the workspace
226 where you want your objects. Must be ABOVE terrain!
227 Area Size: Size of the area you want covered in studs,
228 on X and Z axes only. Recommended Y of 1.
229
230 -- Object to Copy
231 Name: Name of the object in the workspace to copy.
232 Copies: Number of copies of the object to make.
233 Place on Parts: Check this box to allow placement on
234 parts as well as terrain. Does not have a filter!
235
236 -- Terrain
237 Checking any of the boxes will allow the plugin to place
238 objects on that terrain. It will not place on
239 unchecked terrain types. If the randomizer cannot
240 find a suitable position to place, it will skip and
241 move onto the next copy.
242
243 -- Orientation
244 Perpendicular: Check this box if you want objects
245 aligned perpendicular to the surface.
246 Scale: Maximum amount to scale parts by. Will scale
247 each copy randomly between ± the specified %.
248 Max Rotation: Maximum amount it will rotate each copy.
249 Max Inclination: Maximum amount it will incline the
250 copy from upright.
251 Depth: Amount to sink copies below the surface, as a
252 percent of half the length of the primary part.
253
254 -- Clustering
255 Max Size: Maximum number of copies that will be put in
256 a cluster.
257 Density: Maximum distance each member of the cluster
258 will be from the center in studs.
259
260 Created by TerminusEstKuldin/BahamutFierce]]
261 instructions = GuiUtilities.MakeStandardPropertyLabel(instructionsText)
262 instructions.Parent = instructionsFrame
263 instructionsFrame.Parent = instructionsSection:GetContentsFrame()
264
265 -- Add Instructions section to components
266 components[#components+1] = instructionsSection:GetSectionFrame()
267
268 local function stringToVector(str)
269 local tokens = string.split(str, ',')
270 for i = 1, 3 do
271 tokens[i] = tonumber(tokens[i])
272 if tokens[i] == "0" or tokens[i] == nil then
273 tokens[i] = 0
274 end
275 end
276 return Vector3.new(tokens[1],tokens[2],tokens[3])
277 end
278
279 -- Create Area section
280 local areaSection = CollapsibleTitledSection.new(
281 "area", -- name suffix of the gui object
282 "Area to Fill", -- the text displayed beside the collapsible arrow
283 true, -- have the content frame auto-update its size?
284 true, -- minimizable?
285 false -- minimized by default?
286 )
287
288 -- Create Area position input
289 local inputAreaPosition = LabeledTextInput.new(
290 "areaPosition", -- name suffix of gui object
291 "Area Position", -- title text of the multi choice
292 --string.format("%.2f, %.2f, %.2f" , areaPosition.X, areaPosition.Y, areaPosition.Z)
293 tostring(areaPosition) -- default value
294 )
295 inputAreaPosition:SetMaxGraphemes(35)
296 inputAreaPosition:SetValueChangedFunction(function(newObj)
297 areaPosition = stringToVector(newObj)
298 end)
299 inputAreaPosition:GetFrame().Wrapper.Size = UDim2.new(0, 200, .6, 0)
300 inputAreaPosition:GetFrame().Parent = areaSection:GetContentsFrame()
301
302 -- Create Area size input
303 local inputAreaSize = LabeledTextInput.new(
304 "areaSize", -- name suffix of gui object
305 "Area Size", -- title text of the multi choice
306 --string.format("%.2f, %.2f, %.2f" , areaSize.X, areaSize.Y, areaSize.Z)
307 tostring(areaSize) -- default value
308 )
309 inputAreaSize:SetMaxGraphemes(35)
310 inputAreaSize:SetValueChangedFunction(function(newObj)
311 areaSize = stringToVector(newObj)
312 end)
313 inputAreaSize:GetFrame().Wrapper.Size = UDim2.new(0, 200, .6, 0)
314 inputAreaSize:GetFrame().Parent = areaSection:GetContentsFrame()
315
316 -- Create Area generation button
317 local areaCreateFrame = GuiUtilities:MakeStandardFixedHeightFrame("areaCreateFrame")
318 local areaCreateObject = CustomTextButton.new(
319 "createAreaButton",
320 "Set Area"
321 )
322 local areaRemoveObject = CustomTextButton.new(
323 "removeAreaButton",
324 "Remove Area"
325 )
326 areaCreateButton = areaCreateObject:GetButton()
327 areaCreateButton.AnchorPoint = Vector2.new(.5,.5)
328 areaCreateButton.Size = UDim2.new(0,80,0,25)
329 areaCreateButton.Position = UDim2.new(.33,0,.5,0)
330 areaCreateObject:GetButton().Parent = areaCreateFrame
331 areaRemoveButton = areaRemoveObject:GetButton()
332 areaRemoveButton.AnchorPoint = Vector2.new(.5,.5)
333 areaRemoveButton.Size = UDim2.new(0,80,0,25)
334 areaRemoveButton.Position = UDim2.new(.66,0,.5,0)
335 areaCreateObject:GetButton().Parent = areaCreateFrame
336 areaRemoveObject:GetButton().Parent = areaCreateFrame
337 areaCreateFrame.Parent = areaSection:GetContentsFrame()
338
339 -- Function to update area if it changes
340 local function updateArea()
341 areaPosition = area.Position
342 areaSize = area.Size
343 inputAreaPosition:GetFrame().Wrapper.TextBox.Text = tostring(areaPosition)
344 inputAreaSize:GetFrame().Wrapper.TextBox.Text = tostring(areaSize)
345 end
346
347 -- Check if Area part exists in workspace
348 area = WorkspaceService:FindFirstChild("Area")
349 if area then
350 areaPosition = area.Position
351 areaSize = area.Size
352 inputAreaPosition:GetFrame().Wrapper.TextBox.Text = tostring(areaPosition)
353 inputAreaSize:GetFrame().Wrapper.TextBox.Text = tostring(areaSize)
354 print("Area already exists. Size and position loaded in.")
355
356 if areaConnection then
357 areaConnection:Disconnect()
358 end
359 areaConnection = area.Changed:Connect(updateArea)
360 end
361
362 -- Connect function to Area Create button
363 areaCreateButton.Activated:Connect(function()
364 if not areaPosition then
365 warn("Error! Area Position must be a valid Vector3.")
366 else
367 if not areaSize then
368 warn("Error! Area Size must be a valid Vector2.")
369 else
370 if not area then
371 area = Instance.new("Part")
372 area.Name = "Area"
373 area.Position = areaPosition
374 area.Size = areaSize
375 area.Transparency = .8
376 area.Anchored = true
377 area.CanCollide = false
378 area.Massless = true
379 area.CastShadow = false
380 area.Parent = WorkspaceService
381 print("Area set. You can adjust size and position in the workspace.")
382 else
383 area.Position = areaPosition
384 area.Size = areaSize
385 print("Area already exists. Size and position updated.")
386 end
387 if areaConnection then
388 areaConnection:Disconnect()
389 end
390 areaConnection = area.Changed:Connect(updateArea)
391 end
392 end
393 end)
394
395 -- Connect function to Area Remove Button
396 areaRemoveButton.Activated:Connect(function()
397 area = WorkspaceService:FindFirstChild("Area")
398 if not area then
399 warn("Error! Area not defined yet.")
400 else
401 areaPosition = DEFAULT_AREA_POSITION
402 areaSize = DEFAULT_AREA_SIZE
403
404 inputAreaPosition:SetValue(tostring(areaPosition))
405 inputAreaSize:SetValue(tostring(areaSize))
406
407 if areaConnection then
408 areaConnection:Disconnect()
409 end
410 area:Destroy()
411 area = nil
412 print("Area removed!")
413 end
414 end)
415
416 -- Add Area section to components
417 components[#components+1] = areaSection:GetSectionFrame()
418
419 -- Create Object section
420 local objectSection = CollapsibleTitledSection.new(
421 "object", -- name suffix of the gui object
422 "Object to Copy", -- the text displayed beside the collapsible arrow
423 true, -- have the content frame auto-update its size?
424 true, -- minimizable?
425 false -- minimized by default?
426 )
427
428 -- Create object input box
429 local inputObject = LabeledTextInput.new(
430 "object", -- name suffix of gui object
431 "Name", -- title text of the multi choice
432 objectName -- default value
433 )
434 inputObject:SetMaxGraphemes(30)
435 inputObject:SetValueChangedFunction(function(newObj)
436 objectName = newObj
437 end)
438 inputObject:GetFrame().Parent = objectSection:GetContentsFrame()
439
440 -- Create number input box
441 local inputNumber = LabeledTextInput.new(
442 "number", -- name suffix of gui object
443 "Copies", -- title text of the multi choice
444 number -- default value
445 )
446 inputNumber:SetMaxGraphemes(3)
447 inputNumber:SetValueChangedFunction(function(newObj)
448 number = tonumber(newObj)
449 if number == nil then
450 number = 0
451 end
452 end)
453 inputNumber:GetFrame().Parent = objectSection:GetContentsFrame()
454
455 -- Create perpendicular checkbox
456 local partsCheckBox = LabeledCheckbox.new(
457 "parts", -- name suffix of gui object
458 "Place on Parts", -- text beside the checkbox
459 false, -- initial value
460 false -- initially disabled?
461 )
462 partsCheckBox:SetValueChangedFunction(function(newValue)
463 onParts = newValue
464 end)
465 partsCheckBox:GetFrame().Parent = objectSection:GetContentsFrame()
466
467 -- Add Object section to components
468 components[#components+1] = objectSection:GetSectionFrame()
469
470 -- Create Materials section
471 local materialSection = CollapsibleTitledSection.new(
472 "material", -- name suffix of the gui object
473 "Materials", -- the text displayed beside the collapsible arrow
474 true, -- have the content frame auto-update its size?
475 true, -- minimizable?
476 true -- minimized by default?
477 )
478
479 local checkCount = 0
480 local checkBox = {}
481 for _, i in pairs(materials) do
482 -- Create material checkbox
483 checkCount += 1
484 checkBox[checkCount] = LabeledCheckbox.new(
485 i.Name, -- name suffix of gui object
486 i.Name, -- text beside the checkbox
487 true, -- initial value
488 false -- initially disabled?
489 )
490 checkBox[checkCount]:SetValueChangedFunction(function(newValue)
491 materialValid[i.Name] = newValue
492 end)
493 checkBox[checkCount]:GetFrame().Parent = materialSection:GetContentsFrame()
494 end
495
496 -- Create material buttons
497 local matButtonFrame = GuiUtilities:MakeStandardFixedHeightFrame("areaCreateFrame")
498 local matClear = CustomTextButton.new(
499 "matClearButton",
500 "Clear All"
501 )
502 local matFill = CustomTextButton.new(
503 "matFillButton",
504 "Select All"
505 )
506 matClearButton = matClear:GetButton()
507 matClearButton.AnchorPoint = Vector2.new(.5,.5)
508 matClearButton.Size = UDim2.new(0,80,0,25)
509 matClearButton.Position = UDim2.new(.33,0,.5,0)
510 matClear:GetButton().Parent = matButtonFrame
511 matFillButton = matFill:GetButton()
512 matFillButton.AnchorPoint = Vector2.new(.5,.5)
513 matFillButton.Size = UDim2.new(0,80,0,25)
514 matFillButton.Position = UDim2.new(.66,0,.5,0)
515 matFill:GetButton().Parent = matButtonFrame
516 matButtonFrame.Parent = materialSection:GetContentsFrame()
517
518 matClearButton.Activated:Connect(function()
519 for i = 1, #checkBox do
520 checkBox[i]:SetValue(false)
521 end
522 end)
523
524 matFillButton.Activated:Connect(function()
525 for i = 1, #checkBox do
526 checkBox[i]:SetValue(true)
527 end
528 end)
529
530 -- Add Materials section to components
531 components[#components+1] = materialSection:GetSectionFrame()
532
533 -- Create Orientation section
534 local orientSection = CollapsibleTitledSection.new(
535 "orient", -- name suffix of the gui object
536 "Orientation", -- the text displayed beside the collapsible arrow
537 true, -- have the content frame auto-update its size?
538 true, -- minimizable?
539 false -- minimized by default?
540 )
541
542 -- Create perpendicular checkbox
543 local perpCheckBox = LabeledCheckbox.new(
544 "Perpendicular", -- name suffix of gui object
545 "Perpendicular", -- text beside the checkbox
546 false, -- initial value
547 false -- initially disabled?
548 )
549 perpCheckBox:SetValueChangedFunction(function(newValue)
550 perpendicular = newValue
551 end)
552 perpCheckBox:GetFrame().Parent = orientSection:GetContentsFrame()
553
554 -- Create scale slider
555 local sliderScale = LabeledSlider.new(
556 "scale", -- name suffix of gui object
557 "Scale", -- title text of the multi choice
558 50, -- how many intervals to split the slider into
559 2 -- the starting value of the slider
560 )
561 local sliderScaleDisplay = GuiUtilities.MakeStandardPropertyLabel("± 1 %")
562 sliderScaleDisplay.TextXAlignment = Enum.TextXAlignment.Right
563 sliderScaleDisplay.Position = UDim2.new(0.6, 0, 0.5, -3)
564 sliderScaleDisplay.Parent = sliderScale:GetFrame()
565
566 sliderScale:SetValueChangedFunction(function(newValue)
567 sliderScaleDisplay.Text = "± "..newValue.." %"
568 scaleRange = (newValue / 100)
569 end)
570 sliderScale:GetFrame().Parent = orientSection:GetContentsFrame()
571
572 -- Create max rotation slider
573 local sliderMaxRot = LabeledSlider.new(
574 "maxRotation", -- name suffix of gui object
575 "Max Rotation", -- title text of the multi choice
576 360, -- how many intervals to split the slider into
577 2 -- the starting value of the slider
578 )
579 local sliderMaxRotDisplay = GuiUtilities.MakeStandardPropertyLabel("1 °")
580 sliderMaxRotDisplay.TextXAlignment = Enum.TextXAlignment.Right
581 sliderMaxRotDisplay.Position = UDim2.new(0.6, 0, 0.5, -3)
582 sliderMaxRotDisplay.Parent = sliderMaxRot:GetFrame()
583
584 sliderMaxRot:SetValueChangedFunction(function(newValue)
585 sliderMaxRotDisplay.Text = newValue.." °"
586 maxRotation = math.rad(newValue)
587 end)
588 sliderMaxRot:GetFrame().Parent = orientSection:GetContentsFrame()
589
590 -- Create max inclination slider
591 local sliderMaxInc = LabeledSlider.new(
592 "maxInclination", -- name suffix of gui object
593 "Max Inclination", -- title text of the multi choice
594 90, -- how many intervals to split the slider into
595 2 -- the starting value of the slider
596 )
597 local sliderMaxIncDisplay = GuiUtilities.MakeStandardPropertyLabel("1 °")
598 sliderMaxIncDisplay.TextXAlignment = Enum.TextXAlignment.Right
599 sliderMaxIncDisplay.Position = UDim2.new(0.6, 0, 0.5, -3)
600 sliderMaxIncDisplay.Parent = sliderMaxInc:GetFrame()
601
602 sliderMaxInc:SetValueChangedFunction(function(newValue)
603 sliderMaxIncDisplay.Text = newValue.." °"
604 maxInclination = math.rad(newValue)
605 end)
606 sliderMaxInc:GetFrame().Parent = orientSection:GetContentsFrame()
607
608 -- Create depth slider
609 local sliderDepth = LabeledSlider.new(
610 "depth", -- name suffix of gui object
611 "Depth", -- title text of the multi choice
612 100, -- how many intervals to split the slider into
613 50 -- the starting value of the slider
614 )
615 local sliderDepthDisplay = GuiUtilities.MakeStandardPropertyLabel("50 %")
616 sliderDepthDisplay.TextXAlignment = Enum.TextXAlignment.Right
617 sliderDepthDisplay.Position = UDim2.new(0.6, 0, 0.5, -3)
618 sliderDepthDisplay.Parent = sliderDepth:GetFrame()
619
620 sliderDepth:SetValueChangedFunction(function(newValue)
621 sliderDepthDisplay.Text = newValue.." %"
622 depth = (newValue / 100)
623 end)
624 sliderDepth:GetFrame().Parent = orientSection:GetContentsFrame()
625
626 -- Add Orientation section to components
627 components[#components+1] = orientSection:GetSectionFrame()
628
629 -- Create Cluster section
630 local clusterSection = CollapsibleTitledSection.new(
631 "cluster", -- name suffix of the gui object
632 "Clustering", -- the text displayed beside the collapsible arrow
633 true, -- have the content frame auto-update its size?
634 true, -- minimizable?
635 false -- minimized by default?
636 )
637
638 -- Create clumpSize input box
639 local inputClusterSize = LabeledTextInput.new(
640 "clustersize", -- name suffix of gui object
641 "Max Size", -- title text of the multi choice
642 "5" -- default value
643 )
644 inputClusterSize:SetMaxGraphemes(3)
645 inputClusterSize:SetValueChangedFunction(function(newObj)
646 clusterSize = tonumber(newObj)
647 if clusterSize == nil then
648 clusterSize = 0
649 end
650 end)
651 inputClusterSize:GetFrame().Parent = clusterSection:GetContentsFrame()
652
653 -- Create density slider
654 local sliderDensity = LabeledSlider.new(
655 "density", -- name suffix of gui object
656 "Density", -- title text of the multi choice
657 50, -- how many intervals to split the slider into
658 25 -- the starting value of the slider
659 )
660 local sliderDensityDisplay = GuiUtilities.MakeStandardPropertyLabel("35 studs away")
661 sliderDensityDisplay.TextXAlignment = Enum.TextXAlignment.Right
662 sliderDensityDisplay.Position = UDim2.new(0.7, 0, 0.5, -3)
663 sliderDensityDisplay.Parent = sliderDensity:GetFrame()
664
665 sliderDensity:SetValueChangedFunction(function(newValue)
666 density = 60 - newValue
667 sliderDensityDisplay.Text = density.." studs away"
668 end)
669 sliderDensity:GetFrame().Parent = clusterSection:GetContentsFrame()
670
671 -- Add Clustering section to components
672 components[#components+1] = clusterSection:GetSectionFrame()
673
674 -- Create confirmation button
675 local confirmObject = CustomTextButton.new(
676 "confirmButton",
677 "Generate"
678 )
679 local confirmButton = confirmObject:GetButton()
680 confirmButton.Size = UDim2.new(0,80,0,25)
681 confirmButton.AnchorPoint = Vector2.new(.5,.5)
682 confirmButton.Position = UDim2.new(.33,0,.5,0)
683 local undoObject = CustomTextButton.new(
684 "undoButton",
685 "Undo"
686 )
687 local undoButton = undoObject:GetButton()
688 undoButton.Size = UDim2.new(0,80,0,25)
689 undoButton.AnchorPoint = Vector2.new(.5,.5)
690 undoButton.Position = UDim2.new(.66,0,.5,0)
691 local buttonFrame = GuiUtilities:MakeStandardFixedHeightFrame("areaCreateFrame")
692 confirmButton.Parent = buttonFrame
693 undoButton.Parent = buttonFrame
694 components[#components+1] = buttonFrame
695
696 undoActive = false
697 undoButton.Activated:Connect(function()
698 if not undoActive then
699 undoActive = true
700 if ChangeHistoryService:GetCanUndo() then
701 ChangeHistoryService:Undo()
702 print("Last generation undone!")
703 else
704 warn("Error: Nothing to undo!")
705 end
706 undoActive = false
707 pluginGui.Enabled = false
708 end
709 end)
710
711 -- Add frames to main frame
712 for _, frame in ipairs(components) do
713 listFrame:AddChild(frame)
714 end
715
716 listFrame:AddBottomPadding()
717 listFrame:GetFrame().Parent = scrollFrame:GetContentsFrame()
718
719 scrollFrame:GetSectionFrame().Parent = gui
720
721 game.Selection.SelectionChanged:Connect(function()
722 -- Check if object selected
723 local selection = game.Selection:Get()[1]
724 if selection and selection.Name ~= "Area" then
725 objectName = selection.Name
726 inputObject:GetFrame().Wrapper.TextBox.Text = objectName
727 end
728 end)
729
730 -- Function to set a part's CFrame. Returns the CFrame of the part
731 local function setPartCFrame(part, newCFrame)
732 part.CFrame = newCFrame
733 return part.CFrame
734 end
735
736 -- Function to return an objects CFrame based on type
737 local function getCFrame(object)
738 if object:IsA("Model") then
739 return object.PrimaryPart.CFrame
740 else
741 return object.CFrame
742 end
743 end
744
745 -- Function to get CFrame of object adjusted to normal of surface
746 local function getNewCFrame(object, normal)
747 local cf = getCFrame(object)
748 local rightVec = cf.UpVector:Cross(normal)
749 return CFrame.fromMatrix(object.Position, rightVec, normal)
750 end
751
752 -- Function to set a model's CFrame. Returns the CFrame of the primary part of the model.
753 -- Written by @Thedagz on Discord
754 local ModelOffsetsRegistry = {} -- Part = Offset
755 local function CFrameModel(Model,CF)
756 local Registry = ModelOffsetsRegistry[Model]
757 if Registry then
758 for Part,Offset in pairs(Registry) do
759 Part.CFrame = CF*Offset
760 end
761 else
762 Registry = {}
763
764 local InversePrimaryPartCF = Model.PrimaryPart.CFrame:Inverse()
765 local c = Model:GetDescendants()
766 for i=1,#c do
767 if c[i]:IsA("BasePart") then
768 Registry[c[i]] = InversePrimaryPartCF*c[i].CFrame
769 end
770 end
771
772 local ParentConnection; ParentConnection = Model:GetPropertyChangedSignal("Parent"):Connect(function(NewParent)
773 if NewParent == nil then
774 ModelOffsetsRegistry[Model] = nil
775 ParentConnection:Disconnect()
776 end
777 end)
778
779 ModelOffsetsRegistry[Model] = Registry
780 CFrameModel(Model,CF)
781 end
782 return Model.PrimaryPart.CFrame
783 end
784
785 -- Function to scale a model
786 -- Written with help from Kohl on RCS Discord Server
787 local function scaleModel(model, scale)
788 local primaryCFrame = model.PrimaryPart.CFrame
789 local scaleVector = Vector3.new(scale, scale, scale)
790 for _, v in next, model:GetDescendants() do
791 if v:IsA("BasePart") then
792 if v == model.PrimaryPart then
793 v.Size *= scaleVector
794 v.CFrame = primaryCFrame
795 else
796 local objectCFrame = primaryCFrame:inverse() * v.CFrame
797 objectCFrame += (objectCFrame.Position * scale) - objectCFrame.Position
798 v.Size *= scaleVector
799 v.CFrame = primaryCFrame * objectCFrame
800 end
801 end
802 end
803
804 return model
805 end
806
807 -- Create Objects folder if it's not already there
808 local objectsFolder = WorkspaceService:FindFirstChild("RandomizerObjects")
809 if not objectsFolder then
810 objectsFolder = Instance.new("Folder")
811 objectsFolder.Name = "RandomizerObjects"
812 objectsFolder.Parent = WorkspaceService
813 end
814
815 local clicked = false
816 -- Event to run placement when button pressed
817 confirmButton.Activated:Connect(function()
818 if not clicked then
819 clicked = true
820
821 -- Enable progress bar
822 pluginGui.Enabled = true
823
824 -- Add any parts in the Objects folder to blacklist
825 if onParts then
826 blacklist = {WorkspaceService.Area}
827 for _, part in pairs(WorkspaceService.Objects:GetDescendants()) do
828 if part:IsA("BasePart") then
829 blacklist[#blacklist+1] = part
830 end
831 end
832 end
833
834 -- Set undo history waypoint
835 ChangeHistoryService:SetWaypoint("GuiActive")
836 -- Flag to monitor if the placement can run
837 local canRun = true
838
839 -- Check if object exists
840 local setCFrame
841 local midpoint
842 local object = game.Workspace:FindFirstChild(objectName)
843 if not object then
844 warn("Error! Object not found in Workspace!")
845 canRun = false
846 else
847 local objType = object.ClassName
848 if object:IsA("Model") then
849 setCFrame = CFrameModel
850 if not object.PrimaryPart then
851 warn("Error! Model must have PrimaryPart set!")
852 canRun = false
853 else
854 midpoint = object.PrimaryPart.Size.Y / 2
855
856 end
857 elseif object:IsA("BasePart") then
858 setCFrame = setPartCFrame
859 midpoint = object.Size.Y / 2
860 else
861 warn("Error! "..tostring(object).." is not a valid model or part!")
862 canRun = false
863 end
864 end
865
866 local raycastParams = RaycastParams.new()
867 if onParts then
868 -- Set up raycast to blacklist placed objects so it can place on other parts
869 raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
870 raycastParams.FilterDescendantsInstances = {WorkspaceService.Area, WorkspaceService.Objects:GetDescendants()}
871 else
872 -- Set up raycast to only detect terrain
873 raycastParams.FilterType = Enum.RaycastFilterType.Whitelist
874 raycastParams.FilterDescendantsInstances = {WorkspaceService.Terrain}
875 end
876
877 -- Check if Area exists
878 local area = game.Workspace:FindFirstChild("Area")
879 if not area then
880 warn("Error! Area not found in Workspace!")
881 canRun = false
882 end
883
884 -- Check if cluster size is bigger than number of objects
885 if number < clusterSize then
886 warn("Error! Cluster size cannot be larger than total number!")
887 canRun = false
888 end
889
890 -- Loop to place objects
891 local count = 0
892 local failures = 0
893 if canRun then
894 gui.Enabled = false
895 print("Placing "..number.." objects.")
896 while (count <= number and failures <= number) and not cancelled do
897 local obj = object:Clone()
898 local posOffset
899 local orientOffset
900 local rotatedCF
901 local scale
902 local newCF
903 local rayResults
904 local loop = 0
905
906 -- Update blacklist if placing on parts
907 if onParts then
908 raycastParams.FilterDescendantsInstances = blacklist
909 end
910 -- Get random location on Area part and raycast down
911 repeat
912 posOffset = Vector3.new(rng:NextNumber(-area.Size.X/2,area.Size.X/2), 0, rng:NextNumber(-area.Size.Z/2,area.Size.Z/2))
913 rayResults = WorkspaceService:Raycast(area.Position + posOffset, Vector3.new(0, -200, 0), raycastParams)
914 loop += 1
915 RunService.RenderStepped:Wait()
916 until (rayResults and materialValid[rayResults.Material.Name]==true) or loop >= 100
917 count += 1
918 if not rayResults or materialValid[rayResults.Material.Name]==false then
919 warn("Raycasting didn't get results. Do you have Area positioned correctly?")
920 if loop >= 100 then
921 obj:Destroy()
922 warn("Raycasting timeout reached. Does the specified material exist here?")
923 end
924 failures += 1
925 else
926 -- Scale parts
927 scale = rng:NextNumber(1 - scaleRange, 1 + scaleRange)
928 if obj:IsA("BasePart") then
929 orientOffset = obj.Orientation
930 obj.Size = obj.Size * Vector3.new(scale, scale, scale)
931 -- Add to blacklist if onParts set
932 if onParts then
933 blacklist[#blacklist+1] = obj
934 end
935 elseif obj:IsA("Model") then
936 orientOffset = obj.PrimaryPart.Orientation
937 obj = scaleModel(obj, scale)
938 if onParts then
939 for _, part in pairs(obj:GetDescendants()) do
940 if part:IsA("BasePart") then
941 blacklist[#blacklist+1] = part
942 end
943 end
944 end
945 end
946
947 rotatedCF = CFrame.new(getCFrame(object).Position)
948 rotatedCF = CFrame.fromAxisAngle(rotatedCF.UpVector, math.rad(orientOffset.Y))
949 rotatedCF = CFrame.fromAxisAngle(rotatedCF.RightVector, math.rad(orientOffset.X))
950 rotatedCF = CFrame.fromAxisAngle(-rotatedCF.LookVector, math.rad(orientOffset.Z))
951 rotatedCF = rotatedCF - rotatedCF.Position
952
953 -- Set CFrame if perpendicular is checked
954 if perpendicular and not onParts then
955 local cf = getCFrame(object)
956 local rightVec = cf.UpVector:Cross(rayResults.Normal)
957 newCF = setCFrame(obj, CFrame.fromMatrix(rayResults.Position + Vector3.new(0, midpoint - (depth * midpoint), 0), rightVec, rayResults.Normal))
958 else
959 newCF = setCFrame(obj, CFrame.new(rayResults.Position + Vector3.new(0, midpoint - (depth * midpoint), 0)))
960 end
961 -- Adjust for original orientation
962 newCF = setCFrame(obj, newCF * rotatedCF)
963 newCF = setCFrame(obj, newCF * CFrame.fromAxisAngle(newCF.UpVector, rng:NextNumber(0, maxRotation)))
964 newCF = setCFrame(obj, newCF * CFrame.fromAxisAngle(newCF.RightVector, rng:NextNumber(0, maxInclination)))
965 newCF = setCFrame(obj, newCF * CFrame.fromAxisAngle(-newCF.LookVector, rng:NextNumber(0, maxInclination)))
966
967 -- Delay for parenting
968 RunService.RenderStepped:Wait()
969 obj.Parent = objectsFolder
970 -- Calculate cluster size
971 local cluster = rng:NextInteger(clusterSize / 2, clusterSize)
972 -- Set position based on original part
973 local pos
974 if obj:IsA("BasePart") then
975 pos = obj.Position
976 else
977 pos = obj.PrimaryPart.Position
978 end
979 -- Loop through for cluster
980 for i = 2, cluster do
981 -- Clone new object from original one
982 local obj2 = obj:Clone()
983 loop = 0
984 -- Update blacklist if placing on parts
985 if onParts then
986 raycastParams.FilterDescendantsInstances = blacklist
987 end
988 -- Get random distance from original part and raycast down
989 repeat
990 posOffset = Vector3.new(rng:NextNumber(5, density), 50, rng:NextNumber(5, density))
991 rayResults = WorkspaceService:Raycast(pos + posOffset, Vector3.new(0, -200, 0), raycastParams)
992 loop += 1
993 RunService.RenderStepped:Wait()
994 until (rayResults and materialValid[rayResults.Material.Name]==true) or loop >= 100
995 count += 1
996 if not rayResults then
997 warn("Raycasting didn't get results. Do you have Area positioned correctly?")
998 if loop >= 100 then
999 obj2:Destroy()
1000 warn("Raycasting timeout reached. Does the specified material exist here?")
1001 end
1002 failures += 1
1003 else
1004 scale = rng:NextNumber(1 - scaleRange, 1 + scaleRange)
1005 if obj2:IsA("BasePart") then
1006 obj2.Size = obj2.Size * Vector3.new(scale, scale, scale)
1007 -- Add to blacklist if onParts set
1008 if onParts then
1009 blacklist[#blacklist+1] = obj2
1010 end
1011 elseif obj2:IsA("Model") then
1012 obj2 = scaleModel(obj2, scale)
1013 if onParts then
1014 for _, part in pairs(obj2:GetDescendants()) do
1015 if part:IsA("BasePart") then
1016 blacklist[#blacklist+1] = part
1017 end
1018 end
1019 end
1020 end
1021
1022 -- Set CFrame if perpendicular is checked
1023 if perpendicular and not onParts then
1024 local cf = getCFrame(object)
1025 local rightVec = cf.UpVector:Cross(rayResults.Normal)
1026 newCF = setCFrame(obj2, CFrame.fromMatrix(rayResults.Position + Vector3.new(0, midpoint - (depth * midpoint), 0), rightVec, rayResults.Normal))
1027 else
1028 newCF = setCFrame(obj2, CFrame.new(rayResults.Position + Vector3.new(0, midpoint - (depth * midpoint), 0)))
1029 end
1030 -- Adjust for original orientation
1031 newCF = setCFrame(obj2, newCF * rotatedCF)
1032 newCF = setCFrame(obj2, newCF * CFrame.fromAxisAngle(newCF.UpVector, rng:NextNumber(0, maxRotation)))
1033 newCF = setCFrame(obj2, newCF * CFrame.fromAxisAngle(newCF.RightVector, rng:NextNumber(0, maxInclination)))
1034 newCF = setCFrame(obj2, newCF * CFrame.fromAxisAngle(-newCF.LookVector, rng:NextNumber(0, maxInclination)))
1035
1036 -- Delay for parenting
1037 RunService.RenderStepped:Wait()
1038 obj2.Parent = objectsFolder
1039 -- Update progress bar
1040 progressBar.Size = UDim2.new((count/number), 0, 1, 0)
1041 percentBox.Text = string.format("%.1f %%", (count / number) * 100)
1042 if (count / number) > .5 then
1043 percentBox.TextColor3 = backgroundColor
1044 end
1045 if count > number or cancelled then break end
1046 end
1047 end
1048 end
1049 -- Update progress bar
1050 progressBar.Size = UDim2.new((count/number), 0, 1, 0)
1051 percentBox.Text = string.format("%.1f %%", (count / number) * 100)
1052 if (count / number) > .5 then
1053 percentBox.TextColor3 = backgroundColor
1054 end
1055 end
1056
1057 if failures > number then
1058 warn("Generation was unable to place any copies. Check your area position and materials")
1059 end
1060
1061 -- Reset if generation was cancelled
1062 if cancelled then
1063 cancelled = false
1064 end
1065
1066 -- Set waypoint after generation complete and reset gui
1067 ChangeHistoryService:SetWaypoint("Generation")
1068 progressBar.Size = UDim2.new(0, 0, 1, 0)
1069 percentBox.TextColor3 = textColor
1070 percentBox.Text = ""
1071 pluginGui.Enabled = false
1072 print("Generation complete!")
1073 end
1074
1075 clicked = false
1076 gui.Enabled = true
1077 end
1078 end)
1079 -- Connection for toolbar button
1080 scriptButton.Click:Connect(function()
1081 -- Show/hide gui window
1082 gui.Enabled = not gui.Enabled
1083 end)
1084end