2013/terrain plugins/09 - materialpaint.luau

872 lines
19 KiB
Plaintext

--!strict
while not game do
wait()
end
local ChangeHistoryService = game:GetService "ChangeHistoryService"
local CoreGui = game:GetService "CoreGui"
local News = require "../Modules/New"
local New = News.New
local Hydrate = News.Hydrate
---------------
--PLUGIN SETUP-
---------------
local loaded = false
local on = false
local On, Off
local mouseDown, mouseUp, mouseMove
local this = PluginManager():CreatePlugin() :: Plugin
local mouse = this:GetMouse()
mouse.Button1Down:connect(function()
mouseDown(mouse)
end)
mouse.Button1Up:connect(function()
mouseUp(mouse)
end)
mouse.Move:connect(function()
mouseMove(mouse)
end)
local toolbar = this:CreateToolbar "Terrain" :: Toolbar
local toolbarbutton = toolbar:CreateButton(
"Material Brush",
"Material Brush",
"materialBrush.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 Terrain = workspace.Terrain
local SetCell = Terrain.SetCell
local SetWaterCell = Terrain.SetWaterCell
-- local GetWaterCell = c.GetWaterCell
local GetCell = Terrain.GetCell
local WorldToCellPreferSolid = Terrain.WorldToCellPreferSolid
local CellCenterToWorld = Terrain.CellCenterToWorld
local waterMaterial = 17
local brushTypes = { "Circular", "Square" }
-- local waterForceDirections = { "NegX", "X", "NegY", "Y", "NegZ", "Z" }
-- local waterForces = { "None", "Small", "Medium", "Strong", "Max" }
local mediumWaterForce = Enum.WaterForce.Medium
-----------------
--DEFAULT VALUES-
-----------------
local terrainSelectorGui, terrainSelected, radiusLabel, dragBar, closeEvent, helpFrame, currSelectionUpdate, currSelectionDestroy, lastCell, lastLastCell --, waterPanel
local dragging = false
-- local painting = false
--- exposed values to user via gui
local currentMaterial = Enum.CellMaterial.Grass
local radius = 3
local brushType = "Square"
-- local currWaterForceDirection = "NegX"
-- local currWaterForce = "None"
-- lua library load
local RbxGui = LoadLibrary "RbxGui"
local RbxUtil = LoadLibrary "RbxUtility"
-----------------------
--FUNCTION DEFINITIONS-
-----------------------
function paintWaterfall(setCells)
if not setCells then
return
end
for i = 1, #setCells do
SetWaterCell(
Terrain,
setCells[i].xPos,
setCells[i].yPos,
setCells[i].zPos,
mediumWaterForce,
Enum.WaterDirection.NegY
)
end
end
-- Factored out this stuff because I didn't like the mutability of the setCells arrays. - Heliodex
local function getSquareCell(x: number, y: number, z: number)
-- local tempCellPos = Vector3.new(x, y, z)
local oldMaterial, oldType, oldOrientation = GetCell(Terrain, x, y, z)
if oldMaterial.Value <= 0 then
return nil
end
return {
xPos = x,
yPos = y,
zPos = z,
theType = oldType,
orientation = oldOrientation,
}
end
local function getSquare(cellPos: Vector3)
local setCells = {}
local finalX = cellPos.X + radius - 1
local finalY = cellPos.Y + radius - 1
local finalZ = cellPos.Z + radius - 1
for x = cellPos.X - radius + 1, finalX do
for y = cellPos.Y - radius + 1, finalY do
for z = cellPos.Z - radius + 1, finalZ do
table.insert(setCells, getSquareCell(x, y, z))
end
end
end
return setCells
end
local function getCircularCell(
x: number,
y: number,
z: number,
cellPos,
radiusSquared
)
local tempCellPos = Vector3.new(x, y, z)
local holdDist = tempCellPos - cellPos
local distSq = (holdDist):Dot(holdDist)
if distSq >= radiusSquared then
return nil
end
local oldMaterial, oldType, oldOrientation = GetCell(Terrain, x, y, z)
if oldMaterial.Value <= 0 then
return nil
end
return {
xPos = x,
yPos = y,
zPos = z,
theType = oldType,
orientation = oldOrientation,
}
end
local function getCircular(cellPos: Vector3)
local setCells = {}
-- whoever originally wrote these ordered (X, Z, Y) is a psychopath - Heliodex
local finalX = cellPos.X + radius
local finalY = cellPos.Y + radius
local finalZ = cellPos.Z + radius
for x = cellPos.X - radius, finalX do
for y = cellPos.Y - radius, finalY do
for z = cellPos.Z - radius, finalZ do
table.insert(
setCells,
getCircularCell(x, y, z, cellPos, radius * radius)
)
end
end
end
return setCells
end
local function getAffectedCells(startPos: Vector3)
if startPos and Terrain then
if brushType == "Circular" then
return getCircular(startPos)
elseif brushType == "Square" then
return getSquare(startPos)
end
end
return {}
end
local function directionIsDown(fromCell: Vector3, toCell: Vector3)
if not toCell then
return false
end
if toCell and fromCell then
local direction = (toCell - fromCell).Unit
local absX, absY, absZ =
math.abs(direction.X), math.abs(direction.Y), math.abs(direction.Z)
if absY > absX and absY > absZ then
return true
end
end
local viableCells = getAffectedCells(toCell)
if not viableCells or #viableCells < 2 then
return false
end
local lowX, lowY, lowZ =
viableCells[1].xPos, viableCells[1].yPos, viableCells[1].zPos
local highX, highY, highZ = lowX, lowY, lowZ
for i = 2, #viableCells do
if viableCells[i].xPos < lowX then
lowX = viableCells[i].xPos
end
if viableCells[i].xPos > highX then
highX = viableCells[i].xPos
end
if viableCells[i].yPos < lowY then
lowY = viableCells[i].yPos
end
if viableCells[i].yPos > highY then
highY = viableCells[i].yPos
end
if viableCells[i].zPos < lowZ then
lowZ = viableCells[i].zPos
end
if viableCells[i].zPos > highZ then
highZ = viableCells[i].zPos
end
end
local xRange, yRange, zRange =
math.abs(highX - lowX), math.abs(highY - lowY), math.abs(highZ - lowZ)
local xzPlaneArea = xRange * zRange
local xyPlaneArea = xRange * yRange
local yzPlaneArea = yRange * zRange
return xyPlaneArea > xzPlaneArea or yzPlaneArea > xzPlaneArea
end
local function setWaterDirection(mouseCellPos, setCells)
if not setCells or #setCells <= 0 then
return
elseif directionIsDown(lastCell, mouseCellPos) then
paintWaterfall(setCells)
return
end
local initX = setCells[1].xPos
local initZ = setCells[1].zPos
local endX = setCells[#setCells].xPos
local endZ = setCells[#setCells].zPos
local zWidth = math.abs(endZ - initZ)
local zMiddle = math.ceil(zWidth / 2 + initZ)
local xMiddle = math.ceil(zWidth / 2 + initX)
local down = endX - initX
local up, left, right = nil
if down < 0 then
down = Enum.WaterDirection.NegX
up = Enum.WaterDirection.X
left = Enum.WaterDirection.Z
right = Enum.WaterDirection.NegZ
else
down = Enum.WaterDirection.X
up = Enum.WaterDirection.NegX
left = Enum.WaterDirection.NegZ
right = Enum.WaterDirection.Z
end
if #setCells == 1 then
if not mouseCellPos or not lastCell then
return
end
local overallDirection = (mouseCellPos - lastCell).unit
if
math.abs(overallDirection.x) > math.abs(overallDirection.y)
and math.abs(overallDirection.x) > math.abs(overallDirection.z)
then
if overallDirection.x > 0 then
SetWaterCell(
Terrain,
setCells[1].xPos,
setCells[1].yPos,
setCells[1].zPos,
mediumWaterForce,
Enum.WaterDirection.X
)
else
SetWaterCell(
Terrain,
setCells[1].xPos,
setCells[1].yPos,
setCells[1].zPos,
mediumWaterForce,
Enum.WaterDirection.NegX
)
end
elseif
math.abs(overallDirection.z) > math.abs(overallDirection.y)
and math.abs(overallDirection.z) > math.abs(overallDirection.x)
then
if overallDirection.z > 0 then
SetWaterCell(
Terrain,
setCells[1].xPos,
setCells[1].yPos,
setCells[1].zPos,
mediumWaterForce,
Enum.WaterDirection.Z
)
else
SetWaterCell(
Terrain,
setCells[1].xPos,
setCells[1].yPos,
setCells[1].zPos,
mediumWaterForce,
Enum.WaterDirection.NegZ
)
end
elseif
math.abs(overallDirection.y) > math.abs(overallDirection.z)
and math.abs(overallDirection.y) > math.abs(overallDirection.x)
then
if overallDirection.y > 0 then
SetWaterCell(
Terrain,
setCells[1].xPos,
setCells[1].yPos,
setCells[1].zPos,
mediumWaterForce,
Enum.WaterDirection.Y
)
else
SetWaterCell(
Terrain,
setCells[1].xPos,
setCells[1].yPos,
setCells[1].zPos,
mediumWaterForce,
Enum.WaterDirection.NegY
)
end
end
return
end
for i = 1, #setCells do
if setCells[i].xPos == initX then
SetWaterCell(
Terrain,
setCells[i].xPos,
setCells[i].yPos,
setCells[i].zPos,
mediumWaterForce,
down
)
elseif setCells[i].xPos == endX then
SetWaterCell(
Terrain,
setCells[i].xPos,
setCells[i].yPos,
setCells[i].zPos,
mediumWaterForce,
up
)
else
if setCells[i].zPos < zMiddle then
SetWaterCell(
Terrain,
setCells[i].xPos,
setCells[i].yPos,
setCells[i].zPos,
mediumWaterForce,
right
)
elseif setCells[i].zPos > zMiddle then
SetWaterCell(
Terrain,
setCells[i].xPos,
setCells[i].yPos,
setCells[i].zPos,
mediumWaterForce,
left
)
else
if setCells[i].xPos < xMiddle then
SetWaterCell(
Terrain,
setCells[i].xPos,
setCells[i].yPos,
setCells[i].zPos,
mediumWaterForce,
down
)
elseif setCells[i].xPos > xMiddle then
SetWaterCell(
Terrain,
setCells[i].xPos,
setCells[i].yPos,
setCells[i].zPos,
mediumWaterForce,
up
)
end
end
end
end
return setCells
end
local function paintWith(
fn: (
Vector3
) -> { xPos: number, yPos: number, zPos: number },
cellPos: Vector3
)
local setCells = fn(cellPos)
if currentMaterial == waterMaterial then
return
end
for i = 1, #setCells do
SetCell(
Terrain,
setCells[i].xPos,
setCells[i].yPos,
setCells[i].zPos,
currentMaterial,
setCells[i].theType,
setCells[i].orientation
)
end
return setCells
end
local function paint(startPos)
if not (startPos and Terrain) then
return
end
local cellPos = startPos
local setCells
if brushType == "Circular" then
setCells = paintWith(getCircular, cellPos)
elseif brushType == "Square" then
setCells = paintWith(getSquare, cellPos)
end
if currentMaterial == waterMaterial then
return setWaterDirection(cellPos, setCells)
end
return setCells
end
local function calculateRegion(mouseR)
local cellPos = WorldToCellPreferSolid(Terrain, mouseR.Hit.p)
local lowVec =
Vector3.new(cellPos.x - radius, cellPos.y - radius, cellPos.z - radius)
local highVec =
Vector3.new(cellPos.x + radius, cellPos.y + radius, cellPos.z + radius)
lowVec = CellCenterToWorld(Terrain, lowVec.x, lowVec.y, lowVec.z)
highVec = CellCenterToWorld(Terrain, highVec.x, highVec.y, highVec.z)
return Region3.new(
lowVec + Vector3.new(2, 2, 2),
highVec - Vector3.new(2, 2, 2)
)
end
local function createSelection(mouseS, massSelection)
currSelectionUpdate, currSelectionDestroy = RbxUtil.SelectTerrainRegion(
calculateRegion(mouseS),
BrickColor.new "Lime green",
massSelection,
CoreGui
)
end
local function updateSelection(mouseS)
if not currSelectionUpdate then
createSelection(mouseS, radius > 4)
return
end
currSelectionUpdate(calculateRegion(mouseS), BrickColor.new "Lime green")
end
local function setPositionDirectionality()
if nil == lastCell then
return
elseif lastCell and not lastLastCell then
-- no dragging occured, lets set our water to be stagnant or be a waterfall
local cellsToSet = paint(lastCell)
if directionIsDown(nil, lastCell) then
paintWaterfall(cellsToSet)
else
for i = 1, #cellsToSet do
SetWaterCell(
Terrain,
cellsToSet[i].xPos,
cellsToSet[i].yPos,
cellsToSet[i].zPos,
Enum.WaterForce.None,
Enum.WaterDirection.NegX
)
end
end
return
end
if directionIsDown(lastLastCell, lastCell) then
local cellsToSet = paint(lastCell)
paintWaterfall(cellsToSet)
return
end
local overallDirection = (lastCell - lastLastCell).unit
local cellsToSet = paint(lastCell)
local absX, absY, absZ =
math.abs(overallDirection.X),
math.abs(overallDirection.Y),
math.abs(overallDirection.Z)
local direction
if absX > absY and absX > absZ then
direction = overallDirection.X > 0 and Enum.WaterDirection.X
or Enum.WaterDirection.NegX
elseif absY > absX and absY > absZ then
direction = overallDirection.Y > 0 and Enum.WaterDirection.Y
or Enum.WaterDirection.NegY
elseif absZ > absX and absZ > absY then
direction = overallDirection.Z > 0 and Enum.WaterDirection.Z
or Enum.WaterDirection.NegZ
end
if not direction then -- this should never be hit, but just in case
direction = Enum.WaterDirection.NegX
end
for i = 1, #cellsToSet do
SetWaterCell(
Terrain,
cellsToSet[i].xPos,
cellsToSet[i].yPos,
cellsToSet[i].zPos,
mediumWaterForce,
direction
)
end
end
function mouseDown(mouseD)
if not (on and mouseD.Target == workspace.Terrain) then
return
end
dragging = true
if not (mouseD and mouseD.Hit and mouseD.Target == workspace.Terrain) then
return
end
local newCell = WorldToCellPreferSolid(Terrain, mouseD.Hit.p)
if not newCell then
return
elseif
currentMaterial == waterMaterial
and directionIsDown(lastCell, newCell)
then
paintWaterfall(paint(newCell))
end
lastCell = newCell
end
function mouseUp(_)
dragging = false
-- we need to fix directionality on last cell set (if water)
if currentMaterial == waterMaterial then
setPositionDirectionality()
end
ChangeHistoryService:SetWaypoint "MaterialPaint"
lastLastCell = nil
lastCell = nil
end
local function moveTowardsGoal(
direction: string,
currPos: number,
goalPos: number,
currCell: Vector3
)
if currPos == goalPos then
return currCell
end
if currPos < goalPos then
if direction == "X" then
currCell = Vector3.new(currCell.X + 1, currCell.Y, currCell.Z)
elseif direction == "Y" then
currCell = Vector3.new(currCell.X, currCell.Y + 1, currCell.Z)
elseif direction == "Z" then
currCell = Vector3.new(currCell.X, currCell.Y, currCell.Z + 1)
end
elseif currPos > goalPos then
if direction == "X" then
currCell = Vector3.new(currCell.X - 1, currCell.Y, currCell.Z)
elseif direction == "Y" then
currCell = Vector3.new(currCell.X, currCell.Y - 1, currCell.Z)
elseif direction == "Z" then
currCell = Vector3.new(currCell.X, currCell.Y, currCell.Z - 1)
end
end
return currCell
end
local function interpolateOneDim(direction, currPos, goalPos, currCell)
if currPos ~= goalPos then
currCell = moveTowardsGoal(direction, currPos, goalPos, currCell)
paint(currCell)
end
return currCell
end
local function paintBetweenPoints(lastCellP, newCell)
local currCell = lastCellP
while
currCell.X ~= newCell.X
or currCell.Z ~= newCell.Z
or currCell.Y ~= newCell.Y
do
currCell = interpolateOneDim("X", currCell.X, newCell.X, currCell)
currCell = interpolateOneDim("Z", currCell.Z, newCell.Z, currCell)
currCell = interpolateOneDim("Y", currCell.Y, newCell.Y, currCell)
end
end
local function destroySelection()
if currSelectionUpdate then
currSelectionUpdate = nil
end
if currSelectionDestroy then
currSelectionDestroy()
currSelectionDestroy = nil
end
end
function mouseMove(mouseM)
if not on then
return
elseif mouseM.Target == workspace.Terrain then
if lastCell == WorldToCellPreferSolid(Terrain, mouseM.Hit.p) then
return
end
updateSelection(mouseM)
local newCell = WorldToCellPreferSolid(Terrain, mouseM.Hit.p)
if not dragging then
return
end
-- local painting = true
paint(newCell)
if lastCell and newCell and (lastCell - newCell).Magnitude > 1 then
paintBetweenPoints(lastCell, newCell)
end
lastLastCell = lastCell
lastCell = newCell
-- painting = false
else
destroySelection()
end
end
function On()
if not Terrain then
return
end
this:Activate(true)
toolbarbutton:SetActive(true)
dragBar.Visible = true
on = true
end
function Off()
toolbarbutton:SetActive(false)
destroySelection()
dragBar.Visible = false
on = false
end
------
--GUI-
------
--screengui
local g = New "ScreenGui" {
Name = "MaterialPainterGui",
Parent = CoreGui,
}
local containerFrame
dragBar, containerFrame, helpFrame, closeEvent = RbxGui.CreatePluginFrame(
"Material Brush",
UDim2.new(0, 163, 0, 285),
UDim2.new(0, 0, 0, 0),
false,
g
)
dragBar.Visible = false
-- End dragging if it goes over the gui frame.
containerFrame.MouseEnter:connect(function()
dragging = false
end)
dragBar.MouseEnter:connect(function()
dragging = false
end)
helpFrame.Size = UDim2.new(0, 200, 0, 250)
helpFrame.ZIndex = 3
local helpText =
[[Changes existing terrain blocks to the specified material. Simply hold the mouse down and drag to 'paint' the terrain (only cells inside the selection box will be affected).
Size:
The size of the brush we paint terrain with.
Brush Type:
The shape we paint terrain with inside of our selection box.
Material Selection:
The terrain material we will paint.]]
New "TextLabel" {
Name = "TextHelp",
Font = Enum.Font.ArialBold,
FontSize = Enum.FontSize.Size12,
TextColor3 = Color3.new(1, 1, 1),
Size = UDim2.new(1, -6, 1, -6),
-- Position = UDim2.new(0, 3, 0, 3), -- lellel new supremacy - Heliodex
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
Position = UDim2.new(0, 4, 0, 4),
BackgroundTransparency = 1,
TextWrapped = true,
ZIndex = 4,
Text = helpText,
Parent = helpFrame,
}
closeEvent.Event:connect(function()
Off()
end)
terrainSelectorGui, terrainSelected = RbxGui.CreateTerrainMaterialSelector(
UDim2.new(1, -10, 0, 184),
UDim2.new(0, 5, 1, -190)
)
Hydrate(terrainSelectorGui) {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Parent = containerFrame,
}
terrainSelected.Event:connect(function(newMaterial)
currentMaterial = newMaterial
end)
-- Purpose:
-- Retrive the size text to display for a given radius, where 1 == 1 block and 2 == 3 blocks, etc.
local function SizeText(radiusT: number)
return "Size: " .. (((radiusT - 1) * 2) + 1)
end
local function makeRadiusLabel()
return New "TextLabel" {
Size = UDim2.new(1, -3, 0, 14),
TextColor3 = Color3.new(0.95, 0.95, 0.95),
Font = Enum.Font.ArialBold,
FontSize = Enum.FontSize.Size14,
BorderColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
}
end
--current radius display label
radiusLabel = Hydrate(makeRadiusLabel()) {
Name = "RadiusLabel",
Text = SizeText(radius),
Position = UDim2.new(0, 10, 0, 5),
Parent = containerFrame,
}
--radius slider
local radSliderGui, radSliderPosition =
RbxGui.CreateSlider(6, 0, UDim2.new(0, 0, 0, 18))
Hydrate(radSliderGui) {
Size = UDim2.new(1, -2, 0, 20),
Position = UDim2.new(0, 0, 0, 24),
Parent = containerFrame,
}
Hydrate(radSliderGui.Bar) {
Size = UDim2.new(1, -20, 0, 5),
Position = UDim2.new(0, 10, 0.5, -3),
}
radSliderPosition.Value = radius
radSliderPosition.Changed:connect(function()
radius = radSliderPosition.Value
radiusLabel.Text = SizeText(radius)
destroySelection()
end)
local brushTypeChanged = function(newBrush)
brushType = newBrush
end
-- brush type drop-down
local brushDropDown, forceSelection =
RbxGui.CreateDropDownMenu(brushTypes, brushTypeChanged)
forceSelection "Square"
Hydrate(brushDropDown) {
Size = UDim2.new(1, -10, 0.01, 25),
Position = UDim2.new(0, 5, 0, 65),
Parent = containerFrame,
}
Hydrate(makeRadiusLabel()) {
Name = "BrushLabel",
Text = "Brush Type",
Position = UDim2.new(0, 10, 0, 50),
Parent = containerFrame,
}
this.Deactivation:connect(function()
Off()
end)
--------------------------
--SUCCESSFUL LOAD MESSAGE-
--------------------------
loaded = true