--July/5/2025 local GuiService = game:GetService("GuiService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") local SoundService = game:GetService("SoundService") local StarterGui = game:GetService("StarterGui") local TweenService = game:GetService("TweenService") local UserInputService = game:GetService("UserInputService") local AABB = require(ReplicatedStorage.ClientUtils.AABB) local ScreenFlash = require(ReplicatedStorage.ClientUtils.ScreenFlash) local SlotManager = require(script.Parent.SlotManager) local ItemInfo = require(ReplicatedStorage.Information.ItemInfo) local PageInfo = require(ReplicatedStorage.Information.PageInfo) local UIColors = require(StarterGui.UIColors) local DEFAULT_BUTTON_IMAGE = "HIDDEN" local DEFAULT_HOVER_IMAGE = "HIDDEN" local HIGHLIGHT_BUTTON_IMAGE = "HIDDEN" local HIGHLIGHT_HOVER_IMAGE = "HIDDEN" local MENU_SIZE = StarterGui.BuildGui.Holder.Menu.Size local COLOR_PICKER_SIZE = StarterGui.BuildGui.Holder.ColorPicker.Size local EDIT_GUI_SIZE = StarterGui.BuildGui.Holder.EditGui.Size local PREVIEW_HIDE = CFrame.new(0, -50, 0) local TOUCH_ENABLED_OFFSET = Vector3.new(0, 4, 0) local PLOT_HEIGHT = 35 local ROTATION_TWEEN_INFO = TweenInfo.new(0.2, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut) local player = Players.LocalPlayer local playerGui = player.PlayerGui local mouse = player:GetMouse() local camera = workspace.CurrentCamera local function round(number) local precision = math.pow(10, -2) number = number + (precision / 2) return string.sub(tostring(math.floor(number / precision) * precision), 1, 4) end local function setupButton(button, pressed) button.MouseEnter:Connect(function() SoundService.UIHover:Play() end) button.MouseButton1Click:Connect(function() SoundService.UIClick:Play() pressed() end) end local function clearGrid(frame) for _, v in frame:GetChildren() do if v:IsA("UIGridLayout") then continue end v:Destroy() end end local function formatAmpersand(str) if not str then return end str = str:gsub("(%l)(%u)", "%1 %2") str = str:gsub(" And ", " & ") return str end local BuildManager = {} function BuildManager.setItems(self) self.buildGui = playerGui:WaitForChild("BuildGui") local holder = self.buildGui.Holder local menu = holder.Menu clearGrid(menu.ItemFrame) for _, item in self.categoryInfo.items do local info = ItemInfo[item] local button = menu.Templates.Item:Clone() button.Name = item button.Icon.Image = info.icon button.Visible = true local hovering = false button.MouseButton1Down:Connect(function() button.Hover.BackgroundColor3 = Color3.new(0.705882, 0.705882, 0.705882) button.Hover.Visible = true end) button.MouseButton1Up:Connect(function() SoundService.UIClick:Play() if hovering then button.Hover.BackgroundColor3 = Color3.new(0, 0, 0) else button.Hover.Visible = false end if UserInputService.KeyboardEnabled and UserInputService.MouseEnabled then holder.Placement.MouseKeyboard.Visible = true else holder.Placement.Touch.Visible = true end self:startPlacement(button.Name) menu.Visible = false holder.Hotbar.Visible = false end) button.MouseEnter:Connect(function() SoundService.UIHover:Play() hovering = true button.Hover.BackgroundColor3 = Color3.new(0, 0, 0) button.Hover.Visible = true end) button.MouseLeave:Connect(function() hovering = false button.Hover.Visible = false end) button.Parent = menu.ItemFrame end end function BuildManager.setMenuPage(self) self.selectedCategory = PageInfo[self.selectedPage].categoryOrder[1] self.categoryInfo = PageInfo[self.selectedPage].categories[self.selectedCategory] self.buildGui = playerGui:WaitForChild("BuildGui") local holder = self.buildGui.Holder local menu = holder.Menu local top = menu.Top top.Title.Text = formatAmpersand(self.selectedCategory) or "Category Title" clearGrid(menu.Categories) clearGrid(menu.ItemFrame) if not self.categoryInfo then return end self:setItems() for _, category in PageInfo[self.selectedPage].categoryOrder do local info = PageInfo[self.selectedPage].categories[category] local button = menu.Templates.Category:Clone() button.Name = category button.Icon.Image = info.icon button.Visible = true if category ~= self.selectedCategory then button.BackgroundColor3 = UIColors.primary end setupButton(button, function() if self.selectedCategory == category then return end self.selectedCategory = category self.categoryInfo = PageInfo[self.selectedPage].categories[self.selectedCategory] button.BackgroundColor3 = UIColors.accent for _, otherButton in menu.Categories:GetChildren() do if otherButton == button then continue end if otherButton:IsA("UIGridLayout") then continue end otherButton.BackgroundColor3 = UIColors.primary end top.Title.Text = formatAmpersand(self.selectedCategory) self:setItems() end) button.Parent = menu.Categories end end function BuildManager.setupSearch(self) self.buildGui = playerGui:WaitForChild("BuildGui") local holder = self.buildGui.Holder local menu = holder.Menu local searchBar = menu.Top.SearchBar searchBar.TextBox.Changed:Connect(function(property) if property ~= "ContentText" then return end local input = searchBar.TextBox.ContentText input = input:gsub("%s+", ""):lower() for _, v in menu.ItemFrame:GetChildren() do if v:IsA("UIGridLayout") then continue end local normalizedName = v.Name:lower() if normalizedName:find(input, 1, true) then v.Visible = true else v.Visible = false end end end) end function BuildManager.setupMobilePlacement(self) self.buildGui = playerGui:WaitForChild("BuildGui") local holder = self.buildGui.Holder local menu = holder.Menu local frame = holder.Placement.Touch setupButton(frame.Cancel, function() self:stopPlacement() menu.Visible = true holder.Hotbar.Visible = true if UserInputService.KeyboardEnabled and UserInputService.MouseEnabled then holder.Placement.MouseKeyboard.Visible = false else holder.Placement.Touch.Visible = false end end) setupButton(frame.Place, function() self:place() end) setupButton(frame.RotateLeft, function() self:rotate(-90) end) setupButton(frame.RotateRight, function() self:rotate(90) end) end function BuildManager.setupEditGui(self) self.buildGui = playerGui:WaitForChild("BuildGui") local holder = self.buildGui.Holder local editGui = holder:WaitForChild("EditGui") local cg = editGui.CanvasGroup setupButton(cg.Cancel, function() for _, highlight in self.selectedItems do highlight:Destroy() end for item, _ in self.selectedItems do local pos = item:GetPivot().Position local hue, saturation, lightness = item.Recolorable:GetChildren()[1].Color:ToHSV() SlotManager:color(pos.X, pos.Y, pos.Z, hue, saturation, lightness) end table.clear(self.selectedItems) self.lastEditGuiPos = nil TweenService:Create( editGui, TweenInfo.new(0.1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), { Size = UDim2.fromScale(0, 0) } ):Play() TweenService:Create( holder.ColorPicker, TweenInfo.new(0.1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), { Size = UDim2.fromScale(0, 0) } ):Play() end) setupButton(cg.Delete, function() for item, _ in self.selectedItems do local pos = item:GetPivot().Position SlotManager:delete(pos.X, pos.Y, pos.Z) item:Destroy() end table.clear(self.selectedItems) self.lastEditGuiPos = nil TweenService:Create( editGui, TweenInfo.new(0.1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), { Size = UDim2.fromScale(0, 0) } ):Play() TweenService:Create( holder.ColorPicker, TweenInfo.new(0.1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), { Size = UDim2.fromScale(0, 0) } ):Play() end) setupButton(cg.Color, function() holder.ColorPicker.Visible = true holder.ColorPicker.Size = UDim2.fromScale(0, 0) TweenService:Create( holder.ColorPicker, TweenInfo.new(0.3, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { Size = COLOR_PICKER_SIZE } ):Play() end) end function BuildManager.setHue(self, value) local picker = self.buildGui.Holder.ColorPicker local wheel = picker.Wheel self.hue = value local color = Color3.fromHSV(self.hue, self.saturation, self.lightness) for item in self.selectedItems do for _, rc in item.Recolorable:GetChildren() do rc.Color = color end end local angle = self.hue * math.pi * 2 local radius = self.saturation * 0.5 wheel.Pointer.Position = UDim2.fromScale(0.5 - math.cos(angle) * radius, 0.5 + math.sin(angle) * radius) end function BuildManager.setSaturation(self, value) local picker = self.buildGui.Holder.ColorPicker local wheel = picker.Wheel self.saturation = value local color = Color3.fromHSV(self.hue, self.saturation, self.lightness) for item in self.selectedItems do for _, rc in item.Recolorable:GetChildren() do rc.Color = color end end local angle = self.hue * math.pi * 2 local radius = self.saturation * 0.5 wheel.Pointer.Position = UDim2.fromScale(0.5 - math.cos(angle) * radius, 0.5 + math.sin(angle) * radius) end function BuildManager.setLightness(self, value) self.lightness = value local color = Color3.fromHSV(self.hue, self.saturation, self.lightness) for item in self.selectedItems do for _, rc in item.Recolorable:GetChildren() do rc.Color = color end end local picker = self.buildGui.Holder.ColorPicker local slider = picker.LightnessSlider local y = math.min(1 - self.lightness, 1 - slider.Pointer.Size.Y.Scale) slider.Pointer.Position = UDim2.fromScale(0.5, y) end function BuildManager.startMovingLightness(self) local picker = self.buildGui.Holder.ColorPicker local slider = picker.LightnessSlider local function move() local mousePos = UserInputService:GetMouseLocation() local inset = GuiService:GetGuiInset() local pureY = math.clamp((mousePos.Y - inset.Y - slider.AbsolutePosition.Y) / slider.AbsoluteSize.Y, 0, 1) local y = math.min(pureY, 1 - slider.Pointer.Size.Y.Scale) slider.Pointer.Position = UDim2.fromScale(0.5, y) self.lightness = 1 - pureY local color = Color3.fromHSV(self.hue, self.saturation, self.lightness) picker.Hue.TextBox.Text = round(self.hue) picker.Saturation.TextBox.Text = round(self.saturation) picker.Lightness.TextBox.Text = round(self.lightness) picker.Hue.TextBox.PlaceholderText = round(self.hue) picker.Saturation.TextBox.PlaceholderText = round(self.saturation) picker.Lightness.TextBox.PlaceholderText = round(self.lightness) for item in self.selectedItems do for _, rc in item.Recolorable:GetChildren() do rc.Color = color end end end self.lightnessConnection = mouse.Move:Connect(move) move() end function BuildManager.stopMovingLightness(self) if not self.lightnessConnection then return end self.lightnessConnection:Disconnect() self.lightnessConnection = nil end function BuildManager.setupColorPicker(self) self.buildGui = playerGui:WaitForChild("BuildGui") local holder = self.buildGui.Holder setupButton(holder.ColorPicker.Cancel, function() TweenService:Create( holder.ColorPicker, TweenInfo.new(0.1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), { Size = UDim2.fromScale(0, 0) } ):Play() end) holder.ColorPicker.Wheel.MouseButton1Down:Connect(function() self:startMovingColor() end) holder.ColorPicker.LightnessSlider.MouseButton1Down:Connect(function() self:startMovingLightness() end) holder.ColorPicker.Hue.TextBox.FocusLost:Connect(function() local num = tonumber(holder.ColorPicker.Hue.TextBox.Text) if not num then return end self:setHue(num) end) holder.ColorPicker.Saturation.TextBox.FocusLost:Connect(function() local num = tonumber(holder.ColorPicker.Saturation.TextBox.Text) if not num then return end self:setSaturation(num) end) holder.ColorPicker.Lightness.TextBox.FocusLost:Connect(function() local num = tonumber(holder.ColorPicker.Lightness.TextBox.Text) if not num then return end self:setLightness(num) end) end function BuildManager.startMovingColor(self) local picker = self.buildGui.Holder.ColorPicker local wheel = picker.Wheel local function move() local mousePos = UserInputService:GetMouseLocation() local inset = GuiService:GetGuiInset() local ucPos = (mousePos - inset - wheel.AbsolutePosition) / wheel.AbsoluteSize local p = Vector2.new(0.5 - ucPos.X, 0.5 - ucPos.Y) local mag = math.clamp(p.Magnitude * 2, 0, 1) local angle = -math.atan2(p.Y, p.X) if angle < 0 then angle += math.pi * 2 end wheel.Pointer.Position = UDim2.fromScale(0.5 - math.cos(angle) * mag * 0.5, 0.5 + math.sin(angle) * mag * 0.5) self.hue = angle / (math.pi * 2) self.saturation = mag local color = Color3.fromHSV(self.hue, self.saturation, self.lightness) picker.Hue.TextBox.Text = round(self.hue) picker.Saturation.TextBox.Text = round(self.saturation) picker.Lightness.TextBox.Text = round(self.lightness) picker.Hue.TextBox.PlaceholderText = round(self.hue) picker.Saturation.TextBox.PlaceholderText = round(self.saturation) picker.Lightness.TextBox.PlaceholderText = round(self.lightness) for item in self.selectedItems do for _, rc in item.Recolorable:GetChildren() do rc.Color = color end end end self.moveConnection = mouse.Move:Connect(move) move() end function BuildManager.stopMovingColor(self) if not self.moveConnection then return end self.moveConnection:Disconnect() self.moveConnection = nil end function BuildManager.startSelection(self) table.clear(self.selectionSelectedItems) if self.selectConnection then self.selectConnection:Disconnect() end local origin = UserInputService:GetMouseLocation() local frame = playerGui:WaitForChild("Selection").Frame local placed = SlotManager:getIsland().Placed:GetChildren() self.selectConnection = mouse.Move:Connect(function() local mousePos = UserInputService:GetMouseLocation() local diff = mousePos - origin local x, y = math.abs(diff.X), math.abs(diff.Y) frame.Size = UDim2.fromOffset(x, y) frame.Position = UDim2.fromOffset(diff.X > 0 and origin.X or origin.X - x, diff.Y > 0 and origin.Y or origin.Y - y) frame.Visible = diff.Magnitude > 0 local minX = math.min(origin.X, mousePos.X) local maxX = math.max(origin.X, mousePos.X) local minY = math.min(origin.Y, mousePos.Y) local maxY = math.max(origin.Y, mousePos.Y) for _, item in placed do local hitbox = item.Hitbox local pos, visible = camera:WorldToViewportPoint(hitbox.Position) local inside = visible and pos.X >= minX and pos.X <= maxX and pos.Y >= minY and pos.Y <= maxY if inside then if not item:FindFirstChild("Highlight") then local highlight = Instance.new("Highlight") highlight.DepthMode = Enum.HighlightDepthMode.Occluded highlight.OutlineColor = Color3.new(1, 1, 1) highlight.FillTransparency = 1 highlight.Parent = item end self.selectionSelectedItems[item] = item.Highlight elseif self.selectionSelectedItems[item] then local highlight = self.selectionSelectedItems[item] if highlight then highlight:Destroy() self.selectionSelectedItems[item] = nil end end end end) end function BuildManager.stopSelection(self) self.selectConnection:Disconnect() self.selectConnection = nil local gui = playerGui:FindFirstChild("Selection") if gui then gui.Frame.Visible = false end return self.selectionSelectedItems end function BuildManager.setupUI(self) self.buildGui = playerGui:WaitForChild("BuildGui") local holder = self.buildGui.Holder local hotbar = holder.Hotbar local menu = holder.Menu self:setupSearch() self:setupMobilePlacement() self:setupEditGui() self:setupColorPicker() for _, button in hotbar:GetChildren() do if button:IsA("UILayout") then continue end setupButton(button, function() local deselect = button.Name == self.selectedPage if deselect then self.selectedPage = nil self.selectedCategory = nil button.Image = DEFAULT_BUTTON_IMAGE button.HoverImage = DEFAULT_HOVER_IMAGE TweenService:Create( menu, TweenInfo.new(0.1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), { Size = UDim2.fromScale(0, 0) } ):Play() else if self.selectedPage == nil then menu.Visible = true menu.Size = UDim2.fromScale(0, 0) TweenService:Create( menu, TweenInfo.new(0.3, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { Size = MENU_SIZE } ):Play() end self.selectedPage = button.Name button.Image = HIGHLIGHT_BUTTON_IMAGE button.HoverImage = HIGHLIGHT_HOVER_IMAGE self:setMenuPage() for _, otherButton in hotbar:GetChildren() do if otherButton == button then continue end if otherButton:IsA("UILayout") then continue end otherButton.Image = DEFAULT_BUTTON_IMAGE otherButton.HoverImage = DEFAULT_HOVER_IMAGE end end end) end end function BuildManager.isWithinBounds(self) local base = SlotManager:getIsland().Base local cf = self.preview.Hitbox.CFrame local size = self.preview.Hitbox.Size return ( AABB.IsWithinBounds( base.CFrame * CFrame.new(0, PLOT_HEIGHT / 2, 0), base.Size + Vector3.new(0, PLOT_HEIGHT, 0), cf, size ) ) end function BuildManager.isIntersecting(self) local placed = SlotManager:getIsland().Placed local params = OverlapParams.new() params.FilterType = Enum.RaycastFilterType.Include params.FilterDescendantsInstances = { player.Character, placed } local parts = workspace:GetPartsInPart(self.preview.Hitbox, params) for _, part in parts do if part.Name ~= "Hitbox" and part.Parent ~= player.Character then continue end return true end return false end function BuildManager.startPlacement(self, item) self.template = ReplicatedStorage.Assets.Models[self.selectedPage][self.selectedCategory][item] self.preview = self.template:Clone() for _, v in self.preview:GetDescendants() do if v:IsA("BasePart") then v.CanCollide = false end end local highlight = Instance.new("Highlight") highlight.FillColor = Color3.fromRGB(0, 255, 0) highlight.OutlineColor = Color3.fromRGB(0, 255, 0) highlight.DepthMode = Enum.HighlightDepthMode.Occluded highlight.Parent = self.preview self.preview.Name = item self.preview:PivotTo(PREVIEW_HIDE) self.preview.Parent = workspace end function BuildManager.stopPlacement(self) if self.rotLerpConnection then self.rotLerpConnection:Disconnect() self.rotLerpConnection = nil end self.rotationTweenPart.CFrame = CFrame.new() self.lastRotation = CFrame.new() self.preview:Destroy() self.preview = nil end function BuildManager.rotate(self, angle) local goal = self.lastRotation * CFrame.Angles(0, math.rad(angle), 0) TweenService:Create(self.rotationTweenPart, ROTATION_TWEEN_INFO, { CFrame = goal }):Play() self.lastRotation = goal end function BuildManager.place(self) if not self.preview then return end if not self.canPlace then SoundService.Error:Play() ScreenFlash.Activate(Color3.new(1, 0, 0)) return end SlotManager:place(self.template, CFrame.new(self.preview:GetPivot().Position) * self.lastRotation) end function BuildManager.updatePlacement(self) if not self.preview then return end local island = SlotManager:getIsland() if not island then return end local raycastParams = RaycastParams.new() raycastParams.FilterType = Enum.RaycastFilterType.Include raycastParams.FilterDescendantsInstances = { island.Base, island.Placed } local raycast if UserInputService.TouchEnabled then raycast = workspace:Raycast( camera.CFrame.Position + TOUCH_ENABLED_OFFSET, camera.CFrame.LookVector * 200, raycastParams ) else local mousePos = UserInputService:GetMouseLocation() local ray = camera:ViewportPointToRay(mousePos.X, mousePos.Y) raycast = workspace:Raycast(ray.Origin, ray.Direction * 200, raycastParams) end if not raycast then self.preview:PivotTo(PREVIEW_HIDE) return end local snapped = Vector3.new(math.round(raycast.Position.X), math.round(raycast.Position.Y), math.round(raycast.Position.Z)) local position = snapped + raycast.Normal * (self.preview.PrimaryPart.Size / 2) local cf = CFrame.new(position) * self.rotationTweenPart.CFrame self.preview:PivotTo(cf) if self:isWithinBounds() and not self:isIntersecting() then self.preview.Highlight.FillColor = Color3.fromRGB(0, 255, 0) self.preview.Highlight.OutlineColor = Color3.fromRGB(0, 255, 0) self.canPlace = true else self.preview.Highlight.FillColor = Color3.fromRGB(255, 0, 0) self.preview.Highlight.OutlineColor = Color3.fromRGB(255, 0, 0) self.canPlace = false end end function BuildManager.setupInput(self) self.buildGui = playerGui:WaitForChild("BuildGui") local holder = self.buildGui.Holder local menu = holder.Menu local editGui = holder:WaitForChild("EditGui") UserInputService.InputBegan:Connect(function(input, processed) if processed then return end if input.KeyCode == Enum.KeyCode.Q then if not self.preview then return end self:stopPlacement() menu.Visible = true holder.Hotbar.Visible = true if UserInputService.KeyboardEnabled and UserInputService.MouseEnabled then holder.Placement.MouseKeyboard.Visible = false else holder.Placement.Touch.Visible = false end elseif input.KeyCode == Enum.KeyCode.R then if not self.preview then return end self:rotate(90) elseif input.UserInputType == Enum.UserInputType.MouseButton1 then if self.preview then self:place() else local mousePos = UserInputService:GetMouseLocation() - GuiService:GetGuiInset() for _, v in playerGui:GetGuiObjectsAtPosition(mousePos.X, mousePos.Y) do if not v.Visible then continue end if v.BackgroundTransparency == 1 then continue end return end self:startSelection() end end end) UserInputService.InputEnded:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then self:stopMovingColor() self:stopMovingLightness() end end) UserInputService.InputEnded:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then if self.selectConnection ~= nil then local s = self:stopSelection() for k, v in s do self.selectedItems[k] = v end end if mouse.Target and os.clock() - self.lastPressedM1 < 0.5 and self.lastPressedPart == mouse.Target:FindFirstAncestorOfClass("Model") and self.lastPressedPart.Parent == SlotManager:getIsland().Placed then if not self.lastPressedPart:FindFirstChild("Highlight") then local highlight = Instance.new("Highlight") highlight.DepthMode = Enum.HighlightDepthMode.Occluded highlight.OutlineColor = Color3.new(1, 1, 1) highlight.FillTransparency = 1 highlight.Parent = self.lastPressedPart end self.selectedItems[self.lastPressedPart] = self.lastPressedPart.Highlight end self.lastPressedM1 = os.clock() if mouse.Target then self.lastPressedPart = mouse.Target:FindFirstAncestorOfClass("Model") end local posSum = Vector3.zero local posCount = 0 for item, _ in self.selectedItems do posCount += 1 posSum += item:GetPivot().Position end local averagePos = posSum / posCount self.editGuiPart.Position = averagePos editGui.Adornee = self.editGuiPart editGui.Enabled = true if self.lastEditGuiPos ~= averagePos then editGui.Size = UDim2.fromScale(0, 0) TweenService:Create( editGui, TweenInfo.new(0.3, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { Size = EDIT_GUI_SIZE } ):Play() end self.lastEditGuiPos = averagePos end end) end function BuildManager.init(self) self.selectedItems = {} self.selectionSelectedItems = {} self.lightness = 1 self.hue = 0 self.saturation = 0 self.rotationTweenPart = Instance.new("Part") self.lastRotation = CFrame.new() self.canPlace = false self:setupUI() local editGuiPart = Instance.new("Part") editGuiPart.Size = Vector3.one editGuiPart.CanQuery = false editGuiPart.CanCollide = false editGuiPart.CanTouch = false editGuiPart.CastShadow = false editGuiPart.Transparency = 1 editGuiPart.Anchored = true editGuiPart.Parent = workspace self.editGuiPart = editGuiPart self:setupInput() RunService.PreRender:Connect(function() self:updatePlacement() end) end return BuildManager