while not game do wait() end local ChangeHistoryService = game:GetService "ChangeHistoryService" local CoreGui = game:GetService "CoreGui" local New = require("../Modules/New").New -- local CreateStandardLabel = require "../Modules/Terrain/CreateStandardLabel" --------------- --PLUGIN SETUP- --------------- local loaded = false local on = false local On, Off local this = PluginManager():CreatePlugin() :: Plugin local toolbar = this:CreateToolbar "Terrain" :: Toolbar local toolbarbutton = toolbar:CreateButton("Brush", "Brush", "brush.png") :: Button toolbarbutton.Click:connect(function() if on then Off() elseif loaded then On() end end) game:WaitForChild "Workspace" workspace:WaitForChild "Terrain" -- Local function definitions local c = workspace.Terrain local GetCell = c.GetCell local SetCells = c.SetCells local AutowedgeCells = c.AutowedgeCells local WorldToCellPreferSolid = c.WorldToCellPreferSolid local CellCenterToWorld = c.CellCenterToWorld local buildTerrainMode = "Add" local removeTerrainMode = "Remove" ----------------- --DEFAULT VALUES- ----------------- local radius = 5 local depth = 0 local mousedown = false local mousemoving = false local brushheight local material = 1 local lastMousePos = Vector2.new(-1, -1) local lastCellFillTime = 0 local maxYExtents = math.min(c.MaxExtents.Max.Y, 512) -- Which mode (build/remove) it is. local mode = buildTerrainMode -- Height and depth to use for the different modes. local buildTerrainHeight = 5 local removeTerrainDepth = -5 local mouse = this:GetMouse() mouse.Button1Down:connect(function() buttonDown() end) mouse.Button1Up:connect(function() mousedown = false brushheight = nil enablePreview() updatePreviewSelection(mouse.Hit) ChangeHistoryService:SetWaypoint "Brush" end) mouse.Move:connect(function() mouseMoved() end) local selectionPart = New "Part" { Name = "SelectionPart", Archivable = false, Transparency = 1, Anchored = true, Locked = true, CanCollide = false, FormFactor = Enum.FormFactor.Custom, } local selectionBox = New "SelectionBox" { Archivable = false, Color = BrickColor.new "Lime green", Adornee = selectionPart, } mouse.TargetFilter = selectionPart function disablePreview() selectionBox.Parent = nil end function enablePreview() selectionBox.Parent = workspace end ----------------------- --FUNCTION DEFINITIONS- ----------------------- -- searches the y depth of a particular cell position to find the lowest y that is empty function findLowestEmptyCell(x, y, z) local cellMat = GetCell(c, x, y, z) local lowestY = y while cellMat == Enum.CellMaterial.Empty do lowestY = y if y > 0 then y = y - 1 cellMat = GetCell(c, x, y, z) else lowestY = 0 cellMat = nil end end return lowestY end -- finds the lowest cell that is not filled in the radius that is currently specified local function findLowPoint(x, y, z) local lowestPoint = maxYExtents + 1 for i = -radius, radius do local zPos = z + i for _ = -radius, radius do local xPos = x + i local cellMat = GetCell(c, xPos, y, zPos) if cellMat == Enum.CellMaterial.Empty then local emptyDepth = findLowestEmptyCell(xPos, y, zPos) if emptyDepth < lowestPoint then lowestPoint = emptyDepth end end end end return lowestPoint end -- Do a line/plane intersection. The line starts at the camera. The plane is at y == 0, normal(0, 1, 0) -- -- vectorPos - End point of the line. -- -- Return: -- success - Value is true if there was a plane intersection, false if not. -- intersection - Value is the terrain cell intersection point if there is one, vectorPos if there isn't. local function PlaneIntersection(vectorPos) local currCamera = workspace.CurrentCamera local startPos = Vector3.new( currCamera.CoordinateFrame.p.X, currCamera.CoordinateFrame.p.Y, currCamera.CoordinateFrame.p.Z ) local endPos = Vector3.new(vectorPos.X, vectorPos.Y, vectorPos.Z) local normal = Vector3.new(0, 1, 0) local p3 = Vector3.new(0, 0, 0) local startEndDot = normal:Dot(endPos - startPos) local cellPos = vectorPos local success = false if startEndDot ~= 0 then local t = normal:Dot(p3 - startPos) / startEndDot if t >= 0 and t <= 1 then local intersection = ((endPos - startPos) * t) + startPos cellPos = c:WorldToCell(intersection) success = true end end return success, cellPos end -- brushes terrain at point (x, y, z) in cluster c local function brush(x, y, z) if depth == 0 then return elseif depth > 0 then local findY = findLowPoint(x, y + depth, z) local yWithDepth = y + depth local lowY if findY < yWithDepth then lowY = findY else lowY = yWithDepth end local lowVec = Vector3int16.new(x - radius, lowY, z - radius) local highVec = Vector3int16.new(x + radius, yWithDepth, z + radius) local regionToFill = Region3int16.new(lowVec, highVec) SetCells(c, regionToFill, material, 0, 0) AutowedgeCells(c, regionToFill) else local lowVec = Vector3int16.new(x - radius, y + depth + 1, z - radius) local highVec = Vector3int16.new(x + radius, maxYExtents, z + radius) local regionToEmpty = Region3int16.new(lowVec, highVec) SetCells(c, regionToEmpty, Enum.CellMaterial.Empty, 0, 0) end end function updatePreviewSelection(position) if not position then return elseif depth == 0 then -- or not mouse.Target disablePreview() return end local vectorPos = Vector3.new(position.x, position.y, position.z) local cellPos = WorldToCellPreferSolid(c, vectorPos) local solidCell = WorldToCellPreferSolid(c, vectorPos) -- If nothing was hit, do the plane intersection. if 0 == GetCell(c, solidCell.X, solidCell.Y, solidCell.Z).Value then local success = false success, cellPos = PlaneIntersection(vectorPos) if not success then if mouse.Target then cellPos = solidCell else return end end end local regionToSelect if depth > 0 then local yWithDepth if brushheight then yWithDepth = brushheight + depth else yWithDepth = cellPos.y + depth end local lowY if brushheight then lowY = brushheight + 1 else local findY = findLowPoint(cellPos.x, yWithDepth, cellPos.z) if findY < yWithDepth then lowY = findY else lowY = yWithDepth end end local lowVec = CellCenterToWorld( c, cellPos.x - radius, lowY - 1, cellPos.z - radius ) local highVec = CellCenterToWorld( c, cellPos.x + radius, yWithDepth + 1, cellPos.z + radius ) selectionBox.Color = BrickColor.new "Lime green" regionToSelect = Region3.new(lowVec, highVec) else local yPos = cellPos.y + depth if brushheight then yPos = brushheight + depth end local lowVec = CellCenterToWorld(c, cellPos.x - radius, yPos, cellPos.z - radius) local highVec = CellCenterToWorld( c, cellPos.x + radius, maxYExtents, cellPos.z + radius ) selectionBox.Color = BrickColor.new "Really red" regionToSelect = Region3.new(lowVec, highVec) end selectionPart.Size = regionToSelect.Size - Vector3.new(-4, 4, -4) selectionPart.CFrame = regionToSelect.CFrame enablePreview() end function doFillCells(position, mouseDrag, needsCellPos) if mouseDrag then local timeBetweenFills = tick() - lastCellFillTime local totalDragPixels = math.abs(mouseDrag.x) + math.abs(mouseDrag.y) local editDistance = ( (workspace.CurrentCamera :: Camera).CoordinateFrame.p - Vector3.new(position.x, position.y, position.z) ).Magnitude if timeBetweenFills <= 0.05 then if editDistance * totalDragPixels < 450 then lastCellFillTime = tick() return end end end local x = position.x local y = position.y local z = position.z if needsCellPos then local cellPos = WorldToCellPreferSolid(c, Vector3.new(x, y, z)) local solidCell = WorldToCellPreferSolid(c, Vector3.new(x, y, z)) -- If nothing was hit, do the plane intersection. if 0 == GetCell(c, solidCell.X, solidCell.Y, solidCell.Z).Value then local success = false success, cellPos = PlaneIntersection(Vector3.new(x, y, z)) if not success then if mouse.Target then cellPos = solidCell else return end end end x = cellPos.x y = cellPos.y z = cellPos.z end if brushheight == nil then brushheight = y end brush(x, brushheight, z) lastCellFillTime = tick() end function mouseMoved() if not on then return elseif mousedown == true then if mousemoving then return end mousemoving = true local currMousePos = Vector2.new(mouse.X, mouse.Y) local mouseDrag = currMousePos - lastMousePos doFillCells(mouse.Hit, mouseDrag, true) lastMousePos = currMousePos mousemoving = false end updatePreviewSelection(mouse.Hit) end function buttonDown() if on then local firstCellPos = WorldToCellPreferSolid( c, Vector3.new(mouse.Hit.x, mouse.Hit.y, mouse.Hit.z) ) local solidCell = WorldToCellPreferSolid( c, Vector3.new(mouse.Hit.x, mouse.Hit.y, mouse.Hit.z) ) -- If nothing was hit, do the plane intersection. if 0 == GetCell(c, solidCell.X, solidCell.Y, solidCell.Z).Value then local success = false success, firstCellPos = PlaneIntersection( Vector3.new(mouse.Hit.x, mouse.Hit.y, mouse.Hit.z) ) if not success then if not mouse.Target then return end firstCellPos = solidCell end end local celMat = GetCell(c, firstCellPos.x, firstCellPos.y, firstCellPos.z) if celMat.Value > 0 then material = celMat.Value else if 0 == material then -- It was nothing, give it a default type and the plane intersection. material = 1 end end brushheight = firstCellPos.y lastMousePos = Vector2.new(mouse.X, mouse.Y) doFillCells(firstCellPos) mousedown = true end end local brushDragBar function On() if not c then return elseif this then this:Activate(true) end if toolbarbutton then toolbarbutton:SetActive(true) end if enablePreview then enablePreview() end if brushDragBar then brushDragBar.Visible = true end on = true end function Off() if toolbarbutton then toolbarbutton:SetActive(false) end if disablePreview then disablePreview() end if brushDragBar then brushDragBar.Visible = false end on = false end ------ --GUI- ------ --load library for with sliders local RbxGui = LoadLibrary "RbxGui" -- Create a standard dropdown. Use this for all dropdowns in the popup so it is easy to standardize. -- name - What to set the text label name as. -- pos - Where to position the label. Should be of type UDim2. -- values - A table of the values that will be in the dropbox, in the order they are to be shown. -- initValue - Initial value the dropdown should be set to. -- funcOnChange - Function to run when a dropdown selection is made. -- parent - What to set the parent as. -- Return: -- dropdown - The dropdown gui. -- updateSelection - Object to use to change the current dropdown. function CreateStandardDropdown( name, pos, values, initValue, funcOnChange, parent ) -- Create a dropdown selection for the modes to fill in a river local dropdown, updateSelection = RbxGui.CreateDropDownMenu(values, funcOnChange) dropdown.Name = name dropdown.Position = pos dropdown.Active = true dropdown.Size = UDim2.new(0, 150, 0, 32) dropdown.Parent = parent updateSelection(initValue) return dropdown, updateSelection end -- Create a standardized slider. -- name - Name to use for the slider. -- pos - Position to display the slider at. -- steps - How many steps there are in the slider. -- funcOnChange - Function to call when the slider changes. -- initValue - Initial value to set the slider to. If nil the slider stays at the default. -- parent - What to set as the parent. -- Return: -- sliderGui - Slider gui object. -- sliderPosition - Object that can set the slider value. -- function CreateStandardSlider( -- name, -- pos, -- lengthBarPos, -- steps, -- funcOnChange, -- initValue, -- parent -- ) -- local sliderGui, sliderPosition = -- RbxGui.CreateSlider(steps, 0, UDim2.new(0, 0, 0, 0)) -- sliderGui.Name = name -- sliderGui.Parent = parent -- sliderGui.Position = pos -- sliderGui.Size = UDim2.new(0, 160, 0, 20) -- local lengthBar = sliderGui:FindFirstChild "Bar" -- lengthBar.Size = UDim2.new(1, -20, 0, 5) -- lengthBar.Position = lengthBarPos -- if nil ~= funcOnChange then -- sliderPosition.Changed:connect(function() -- funcOnChange(sliderPosition) -- end) -- end -- if nil ~= initValue then -- sliderPosition.Value = initValue -- end -- return sliderGui, sliderPosition -- end --screengui local g = New "ScreenGui" { Name = "TerrainBrushGui", Parent = CoreGui, } local elevationFrame, elevationHelpFrame, elevationCloseEvent brushDragBar, elevationFrame, elevationHelpFrame, elevationCloseEvent = RbxGui.CreatePluginFrame( "Terrain Brush", UDim2.new(0, 151, 0, 160), UDim2.new(0, 0, 0, 0), false, g ) brushDragBar.Visible = false elevationCloseEvent.Event:connect(function() Off() end) elevationHelpFrame.Size = UDim2.new(0, 200, 0, 210) local helpText = [[Drag the mouse by holding the left mouse button to either create or destroy terrain defined by the selection box. Radius: Half of the width of the selection box to be used. Height: How tall to make terrain from the mouse location. If this value is negative, the brush will remove terrain instead of creating terrain (indicated by the red selection box). ]] New "TextLabel" { Font = Enum.Font.ArialBold, FontSize = Enum.FontSize.Size12, TextColor3 = Color3.new(1, 1, 1), BackgroundTransparency = 1, TextWrapped = true, Size = UDim2.new(1, -10, 1, -10), Position = UDim2.new(0, 5, 0, 5), TextXAlignment = Enum.TextXAlignment.Left, Text = helpText, Parent = elevationHelpFrame, } -- current radius display label local radl = New "TextLabel" { Position = UDim2.new(0, 0, 0, 70), Size = UDim2.new(1, 0, 0, 14), Text = "", BackgroundColor3 = Color3.new(0.4, 0.4, 0.4), TextColor3 = Color3.new(0.95, 0.95, 0.95), Font = Enum.Font.ArialBold, FontSize = Enum.FontSize.Size14, TextXAlignment = Enum.TextXAlignment.Left, BorderColor3 = Color3.new(0, 0, 0), BackgroundTransparency = 1, Parent = elevationFrame, } --radius slider local radSliderGui, radSliderPosition = RbxGui.CreateSlider(5, 0, UDim2.new(0, 10, 0, 92)) radSliderGui.Parent = elevationFrame local radBar = radSliderGui:FindFirstChild "Bar" radBar.Size = UDim2.new(1, -20, 0, 5) radSliderPosition.Changed:connect(function() radius = radSliderPosition.Value + 1 radl.Text = " Radius: " .. radius end) radSliderPosition.Value = radius - 1 --current depth factor display label local dfl = New "TextLabel" { Position = UDim2.new(0, 0, 0, 110), Size = UDim2.new(1, 0, 0, 14), Text = "", BackgroundColor3 = Color3.new(0.4, 0.4, 0.4), TextColor3 = Color3.new(0.95, 0.95, 0.95), Font = Enum.Font.ArialBold, FontSize = Enum.FontSize.Size14, BorderColor3 = Color3.new(0, 0, 0), TextXAlignment = Enum.TextXAlignment.Left, BackgroundTransparency = 1, Parent = elevationFrame, } --depth factor slider local addSliderGui, addSliderPosition = RbxGui.CreateSlider(31, 0, UDim2.new(0, 10, 0, 132)) addSliderGui.Parent = elevationFrame local dfBar = addSliderGui:FindFirstChild "Bar" dfBar.Size = UDim2.new(1, -20, 0, 5) addSliderPosition.Changed:connect(function() depth = addSliderPosition.Value dfl.Text = " Height: " .. depth end) addSliderPosition.Value = buildTerrainHeight --depth factor slider local removeSliderGui, removeSliderPosition = RbxGui.CreateSlider(31, 0, UDim2.new(0, 10, 0, 132)) removeSliderGui.Parent = elevationFrame dfBar = removeSliderGui:FindFirstChild "Bar" dfBar.Size = UDim2.new(1, -20, 0, 5) removeSliderPosition.Changed:connect(function() depth = -removeSliderPosition.Value dfl.Text = " Height: " .. depth end) removeSliderPosition.Value = removeTerrainDepth -- Set which mode is to be used. Show/hide as needed. -- -- mode - Which build mode to run. function SetMode(setmode) if setmode == buildTerrainMode then addSliderGui.Visible = true local hold = addSliderPosition.Value addSliderPosition.Value = 0 addSliderPosition.Value = hold removeSliderGui.Visible = false elseif setmode == removeTerrainMode then addSliderGui.Visible = false removeSliderGui.Visible = true local hold = removeSliderPosition.Value removeSliderPosition.Value = 0 removeSliderPosition.Value = hold end end -- Create/Remove mode -- Create the build mode gui. -- local buildModeLabel = CreateStandardLabel( -- "BuildModeLabel", -- UDim2.new(0, 8, 0, 10), -- UDim2.new(0, 67, 0, 14), -- "Build Mode:", -- elevationFrame -- ) local _, buildModeSet = CreateStandardDropdown( "BuildModeDropdown", UDim2.new(0, 1, 0, 26), { buildTerrainMode, removeTerrainMode }, buildMode, function(value) if "Add" == value then SetMode(buildTerrainMode) elseif "Remove" == value then SetMode(removeTerrainMode) end end, elevationFrame ) --[[ buildModeSet(buildTerrainMode) buildModeSet(removeTerrainMode) --]] buildModeSet(mode) SetMode(mode) this.Deactivation:connect(function() Off() end) -------------------------- --SUCCESSFUL LOAD MESSAGE- -------------------------- loaded = true