-- Services local Players = game:GetService("Players") local UserInputService = game:GetService("UserInputService") local ContentProvider = game:GetService("ContentProvider") local player = Players.LocalPlayer -- Configuration local MESH_ID = "rbxassetid://1066606415" local TEXTURE_ID = "" -- optional: rbxassetid://... if the mesh has a matching texture local MESH_SCALE = Vector3.new(0.016, 0.016, 0.016) -- adjust if the mesh looks too big/small local BASE_OFFSET = CFrame.new(0, 0, 1.0) -- forward/back + left/right placement, Y height is adjusted separately below local SPEED_STEP = 0.1 local SPEED_MIN, SPEED_MAX = 0.5, 2 local PITCH_STEP = 0.1 local PITCH_MIN, PITCH_MAX = 0.5, 2 local COLOR_SWATCHES = { Color3.fromRGB(40, 40, 40), Color3.fromRGB(200, 200, 200), Color3.fromRGB(180, 40, 40), Color3.fromRGB(40, 90, 180), Color3.fromRGB(60, 150, 70), Color3.fromRGB(230, 200, 40), } -- State (shared between GUI and boombox) local boomboxPart = nil local boomboxWeld = nil local currentUpperTorso = nil local sound = nil local pitchEffect = nil local currentSoundId = "" local soundLoadedConn = nil local statusLabelRef = nil -- set once the GUI exists, used for error feedback local yOffsetLabelRef = nil local speedLabelRef = nil local pitchLabelRef = nil local yOffset = 0 -- studs, adjustable from the GUI, persists across respawns/recreation local currentSpeed = 1 local currentPitch = 1 local currentColor = COLOR_SWATCHES[1] -- Re-welds the boombox at its current upperTorso + yOffset. WeldConstraint -- locks in the relative CFrame at creation time, so to move it we have to -- set the CFrame first, then rebuild the weld against the new position. local function repositionBoombox() if not boomboxPart or not currentUpperTorso then return end if boomboxWeld then boomboxWeld:Destroy() boomboxWeld = nil end boomboxPart.CFrame = currentUpperTorso.CFrame * BASE_OFFSET * CFrame.new(0, yOffset, 0) local weld = Instance.new("WeldConstraint") weld.Part0 = currentUpperTorso weld.Part1 = boomboxPart weld.Parent = boomboxPart boomboxWeld = weld end local function applySpeed() if sound then sound.PlaybackSpeed = currentSpeed end end local function applyPitch() if pitchEffect then pitchEffect.Octave = currentPitch end end local function applyColor() if boomboxPart then boomboxPart.Color = currentColor end end -- Removes the boombox and stops the sound. It comes back next respawn -- (createBoombox runs again on CharacterAdded). local function deleteBoombox() if boomboxPart then boomboxPart:Destroy() end boomboxPart = nil boomboxWeld = nil sound = nil pitchEffect = nil end -- Pull a numeric asset id out of either a bare number or a pasted -- roblox.com/library/123456/Song-Name style link local function parseSoundId(text) local digits = text:match("(%d+)") return digits and tonumber(digits) or nil end -- Plays soundId on the given Sound instance, handling both the "already -- cached, fires before we can connect" case and genuine load failures -- (e.g. the asset isn't permitted for use in this experience). local function playSoundId(soundInstance, soundId, onResult) if soundLoadedConn then soundLoadedConn:Disconnect() soundLoadedConn = nil end soundInstance:Stop() soundInstance.SoundId = soundId local function attemptPlay() local ok, err = pcall(function() soundInstance:Play() end) if onResult then onResult(ok, err) end end if soundInstance.IsLoaded then -- Already loaded (common with cached/short sounds) - the Loaded -- event will never fire, so play immediately instead of waiting. attemptPlay() return end local loaded = false soundLoadedConn = soundInstance.Loaded:Connect(function() loaded = true attemptPlay() if soundLoadedConn then soundLoadedConn:Disconnect() soundLoadedConn = nil end end) -- Fallback in case Loaded never fires (bad id, moderation block, etc.) task.delay(2, function() if not loaded and soundInstance and soundInstance.Parent then attemptPlay() end end) end -- Small helper to build a plain grey button, matching the rough flat style local function makeGreyButton(name, size, position, text, parent) local btn = Instance.new("TextButton") btn.Name = name btn.Size = size btn.Position = position btn.BackgroundColor3 = Color3.fromRGB(200, 200, 200) btn.BorderSizePixel = 1 btn.BorderColor3 = Color3.fromRGB(0, 0, 0) btn.Text = text btn.TextColor3 = Color3.fromRGB(0, 0, 0) btn.Font = Enum.Font.SourceSans btn.TextSize = 14 btn.Parent = parent return btn end -- Small helper to build a +/- adjust row: label on the left, minus/plus on the right local function makeAdjustRow(y, labelText, parent) local label = Instance.new("TextLabel") label.Size = UDim2.new(0, 80, 0, 26) label.Position = UDim2.new(0, 0, 0, y) label.BackgroundColor3 = Color3.fromRGB(200, 200, 200) label.BorderSizePixel = 1 label.BorderColor3 = Color3.fromRGB(0, 0, 0) label.Text = labelText label.TextColor3 = Color3.fromRGB(0, 0, 0) label.Font = Enum.Font.SourceSans label.TextSize = 14 label.Parent = parent local minusBtn = makeGreyButton("Minus", UDim2.new(0, 50, 0, 26), UDim2.new(0, 85, 0, y), "-", parent) local plusBtn = makeGreyButton("Plus", UDim2.new(0, 50, 0, 26), UDim2.new(0, 140, 0, y), "+", parent) return label, minusBtn, plusBtn end -- Create the draggable GUI local function createGUI() local playerGui = player:FindFirstChild("PlayerGui") if not playerGui then return end local existing = playerGui:FindFirstChild("BoomboxGui") if existing then existing:Destroy() end local screenGui = Instance.new("ScreenGui") screenGui.Name = "BoomboxGui" screenGui.ResetOnSpawn = false screenGui.Parent = playerGui local frame = Instance.new("Frame") frame.Name = "MainFrame" frame.Size = UDim2.new(0, 200, 0, 250) frame.Position = UDim2.new(0, 20, 0.5, -125) frame.BackgroundColor3 = Color3.fromRGB(150, 150, 150) frame.BorderSizePixel = 1 frame.BorderColor3 = Color3.fromRGB(0, 0, 0) frame.Parent = screenGui local title = Instance.new("TextLabel") title.Name = "Title" title.Size = UDim2.new(1, 0, 0, 20) title.BackgroundColor3 = Color3.fromRGB(150, 150, 150) title.BorderSizePixel = 1 title.BorderColor3 = Color3.fromRGB(0, 0, 0) title.Text = "Boombox" title.TextColor3 = Color3.fromRGB(0, 0, 0) title.Font = Enum.Font.SourceSans title.TextSize = 14 title.Active = true title.Parent = frame -- Tabs local mainTabButton = makeGreyButton("MainTabButton", UDim2.new(0, 95, 0, 22), UDim2.new(0, 5, 0, 22), "Main", frame) local colorTabButton = makeGreyButton("ColorTabButton", UDim2.new(0, 90, 0, 22), UDim2.new(0, 100, 0, 22), "Color", frame) local mainPage = Instance.new("Frame") mainPage.Name = "MainPage" mainPage.Size = UDim2.new(1, -10, 0, 172) mainPage.Position = UDim2.new(0, 5, 0, 46) mainPage.BackgroundTransparency = 1 mainPage.Visible = true mainPage.Parent = frame local colorPage = Instance.new("Frame") colorPage.Name = "ColorPage" colorPage.Size = UDim2.new(1, -10, 0, 172) colorPage.Position = UDim2.new(0, 5, 0, 46) colorPage.BackgroundTransparency = 1 colorPage.Visible = false colorPage.Parent = frame local function showTab(tab) mainPage.Visible = (tab == "main") colorPage.Visible = (tab == "color") end mainTabButton.MouseButton1Click:Connect(function() showTab("main") end) colorTabButton.MouseButton1Click:Connect(function() showTab("color") end) -- ===== Main page ===== local textBox = Instance.new("TextBox") textBox.Name = "SongIdInput" textBox.Size = UDim2.new(1, 0, 0, 26) textBox.Position = UDim2.new(0, 0, 0, 0) textBox.BackgroundColor3 = Color3.fromRGB(200, 200, 200) textBox.BorderSizePixel = 1 textBox.BorderColor3 = Color3.fromRGB(0, 0, 0) textBox.PlaceholderText = "Enter Sound ID or link..." textBox.Text = "" textBox.TextColor3 = Color3.fromRGB(0, 0, 0) textBox.Font = Enum.Font.SourceSans textBox.TextSize = 14 textBox.ClearTextOnFocus = true textBox.Parent = mainPage local button = makeGreyButton("EnterButton", UDim2.new(1, 0, 0, 26), UDim2.new(0, 0, 0, 28), "Enter", mainPage) local yLabel, yDownButton, yUpButton = makeAdjustRow(58, "Y: 0.0", mainPage) yOffsetLabelRef = yLabel local speedLabel, speedDownButton, speedUpButton = makeAdjustRow(88, "Speed: 1.0", mainPage) speedLabelRef = speedLabel local pitchLabel, pitchDownButton, pitchUpButton = makeAdjustRow(118, "Pitch: 1.0", mainPage) pitchLabelRef = pitchLabel local deleteButton = makeGreyButton("DeleteButton", UDim2.new(1, 0, 0, 26), UDim2.new(0, 0, 0, 148), "Delete Boombox", mainPage) -- Small status line so load/permission errors are visible, not silent local status = Instance.new("TextLabel") status.Name = "Status" status.Size = UDim2.new(1, 0, 0, 16) status.Position = UDim2.new(0, 0, 1, -16) status.BackgroundTransparency = 1 status.Text = "" status.TextColor3 = Color3.fromRGB(30, 30, 30) status.Font = Enum.Font.SourceSans status.TextSize = 12 status.TextXAlignment = Enum.TextXAlignment.Left status.Parent = frame statusLabelRef = status local Y_STEP = 0.1 local function nudgeY(delta) yOffset = yOffset + delta yLabel.Text = "Y: " .. string.format("%.1f", yOffset) repositionBoombox() end yDownButton.MouseButton1Click:Connect(function() nudgeY(-Y_STEP) end) yUpButton.MouseButton1Click:Connect(function() nudgeY(Y_STEP) end) local function nudgeSpeed(delta) currentSpeed = math.clamp(currentSpeed + delta, SPEED_MIN, SPEED_MAX) speedLabel.Text = "Speed: " .. string.format("%.1f", currentSpeed) applySpeed() end speedDownButton.MouseButton1Click:Connect(function() nudgeSpeed(-SPEED_STEP) end) speedUpButton.MouseButton1Click:Connect(function() nudgeSpeed(SPEED_STEP) end) local function nudgePitch(delta) currentPitch = math.clamp(currentPitch + delta, PITCH_MIN, PITCH_MAX) pitchLabel.Text = "Pitch: " .. string.format("%.1f", currentPitch) applyPitch() end pitchDownButton.MouseButton1Click:Connect(function() nudgePitch(-PITCH_STEP) end) pitchUpButton.MouseButton1Click:Connect(function() nudgePitch(PITCH_STEP) end) deleteButton.MouseButton1Click:Connect(function() deleteBoombox() status.Text = "Boombox deleted" status.TextColor3 = Color3.fromRGB(30, 30, 30) end) -- ===== Color page (very simple - just swatch buttons) ===== local swatchSize = 40 local perRow = 4 local gap = 5 for i, color in ipairs(COLOR_SWATCHES) do local row = math.floor((i - 1) / perRow) local col = (i - 1) % perRow local swatch = Instance.new("TextButton") swatch.Name = "Swatch" .. i swatch.Size = UDim2.new(0, swatchSize, 0, swatchSize) swatch.Position = UDim2.new(0, col * (swatchSize + gap), 0, row * (swatchSize + gap)) swatch.BackgroundColor3 = color swatch.BorderSizePixel = 1 swatch.BorderColor3 = Color3.fromRGB(0, 0, 0) swatch.Text = "" swatch.Parent = colorPage swatch.MouseButton1Click:Connect(function() currentColor = color applyColor() end) end -- Custom dragging via the title bar local dragging = false local dragStart = nil local startPos = nil title.InputBegan:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then dragging = true dragStart = input.Position startPos = frame.Position end end) title.InputEnded:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then dragging = false end end) UserInputService.InputChanged:Connect(function(input) if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then local delta = input.Position - dragStart frame.Position = UDim2.new(startPos.X.Scale, startPos.X.Offset + delta.X, startPos.Y.Scale, startPos.Y.Offset + delta.Y) end end) local submitting = false local function submitSong() if submitting then return end submitting = true local soundIdNum = parseSoundId(textBox.Text) if soundIdNum then currentSoundId = "rbxassetid://" .. tostring(soundIdNum) -- If the boombox exists, update+play immediately. If it doesn't -- exist yet (character not spawned, or deleted), currentSoundId -- is still saved and will be picked up next time it's created. if sound then playSoundId(sound, currentSoundId, function(ok, err) if ok then status.Text = "Playing " .. tostring(soundIdNum) status.TextColor3 = Color3.fromRGB(20, 120, 40) else status.Text = "Can't play this id (check it's a valid, permitted audio asset)" status.TextColor3 = Color3.fromRGB(150, 30, 30) warn("[Boombox] Play failed for", currentSoundId, err) end end) end button.Text = "Enter" else status.Text = "Enter a numeric Sound ID" status.TextColor3 = Color3.fromRGB(150, 30, 30) button.Text = "Invalid ID!" end task.delay(1.5, function() button.Text = "Enter" submitting = false end) end button.MouseButton1Click:Connect(submitSong) textBox.FocusLost:Connect(function(enterPressed) if enterPressed then submitSong() end end) end -- Create the boombox on the character local function createBoombox(character) if boomboxPart then boomboxPart:Destroy() boomboxPart = nil end local upperTorso = character:FindFirstChild("UpperTorso") or character:FindFirstChild("Torso") if not upperTorso then warn("[Boombox] Could not find Torso/UpperTorso on the player's character.") return end -- Plain Part + SpecialMesh (FileMesh) - this is the combo you confirmed -- actually renders for this mesh id. MeshPart.MeshId is pickier about -- what it'll accept and can silently show nothing for the same asset. local part = Instance.new("Part") part.Name = "ClientBoombox" part.Size = Vector3.new(1, 1, 1) part.Transparency = 0 part.CanCollide = false part.CanQuery = false part.Massless = true part.Color = currentColor local mesh = Instance.new("SpecialMesh") mesh.MeshType = Enum.MeshType.FileMesh mesh.MeshId = MESH_ID if TEXTURE_ID ~= "" then mesh.TextureId = TEXTURE_ID end mesh.Scale = MESH_SCALE mesh.Parent = part part.Parent = character -- Kick off loading the mesh/texture as soon as it's placed pcall(function() ContentProvider:PreloadAsync({ part }) end) boomboxPart = part currentUpperTorso = upperTorso repositionBoombox() -- sets CFrame + weld using the current yOffset sound = Instance.new("Sound") sound.Volume = 1 sound.Looped = true sound.RollOffMaxDistance = 60 sound.PlaybackSpeed = currentSpeed sound.Parent = part pitchEffect = Instance.new("PitchShiftSoundEffect") pitchEffect.Octave = currentPitch pitchEffect.Parent = sound if currentSoundId ~= "" then playSoundId(sound, currentSoundId, function(ok, err) if statusLabelRef then if ok then statusLabelRef.Text = "Playing" statusLabelRef.TextColor3 = Color3.fromRGB(20, 120, 40) else statusLabelRef.Text = "Can't play this id (check it's a valid, permitted audio asset)" statusLabelRef.TextColor3 = Color3.fromRGB(150, 30, 30) warn("[Boombox] Play failed for", currentSoundId, err) end end end) end end -- Handle character spawns local function onCharacterAdded(character) character:WaitForChild("HumanoidRootPart", 5) createBoombox(character) end -- Create the GUI (persists across respawns) createGUI() -- Handle current character and future respawns if player.Character then onCharacterAdded(player.Character) end player.CharacterAdded:Connect(onCharacterAdded)