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