Hey all! I’ve been working on this little item shop system for a couple days now and finally have something worth showcasing.
The scripts for it are still pretty messy, but y’all can have a look at what I’ve written so far, for those of you that’d like to use it yourselves for something. I’m still planning on adding more to it and improving on it, so any tips or feedback would be appreciated! I’ll also be sure to post updated versions whenever I get a chance to work on it more.
My goal with it is mainly to future proof the system I have now and expand upon it enough to be compatible with new items, more shops, etc.
Here are all the components that go into it:
ShopHandler (ServerScript, ServerScriptService)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
local PlayerInfoFolder = ServerStorage:FindFirstChild("PlayerInfo")
Players.PlayerAdded:Connect(function(player)
local playerInfo = Instance.new("Folder")
playerInfo.Name = player.Name
playerInfo.Parent = PlayerInfoFolder
local playerCash = Instance.new("NumberValue")
playerCash.Name = "Money"
playerCash.Value = 160
playerCash.Parent = playerInfo
end)
local menuOpenEvent = ReplicatedStorage.Events.ShopMenuOpen
local buyItemEvent = ReplicatedStorage.Events.BuyItem
menuOpenEvent.OnServerEvent:Connect(function(player, merchant)
print(`Player: {player} sent request to open shop: {merchant}...`)
local shop = workspace:FindFirstChild(`{merchant}Shop`)
if not shop then
error("Shop not found!")
return
end
menuOpenEvent:FireClient(player, shop.DisplayItems)
print("Success!")
end)
local errLowCash = "NOT ENOUGH MONEY"
local errNoItem = "ITEM NOT FOUND"
buyItemEvent.OnServerEvent:Connect(function(player, merchant, itemName)
print(`Server received purchase request for item: {itemName}`)
local playerInfo = PlayerInfoFolder:FindFirstChild(player.Name)
local playerGui = player:FindFirstChildOfClass("PlayerGui")
local shopItem = workspace:FindFirstChild(`{merchant}Shop`):FindFirstChild(`DisplayItems`):FindFirstChild(itemName)
if not shopItem then
warn(errNoItem)
return
end
local plrCash = playerInfo:FindFirstChild("Money")
if plrCash.Value < shopItem.Price.Value then
warn(errLowCash)
buyItemEvent:FireClient(player, merchant, itemName, plrCash.Value, errLowCash)
return
end
plrCash.Value -= shopItem.Price.Value
ServerStorage.ShopItems[merchant][itemName]:Clone().Parent = player.Backpack
buyItemEvent:FireClient(player, merchant, itemName, plrCash.Value)
print("Purchase success!")
end)
ShopClient (Client-Side ModuleScript, ReplicatedStorage)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StarterGui = game:GetService("StarterGui")
local self; self = setmetatable({}, {__index = self})
function self.Awake(playerChar)
self.character = playerChar
self.playerRoot = playerChar:WaitForChild("HumanoidRootPart")
end
function self.Sleep()
self.character = nil
self.playerRoot = nil
self.currentMerchant = nil
end
function self.characterExists() : boolean
return self.character and self.playerRoot
end
function self.playerTooFar(p1 : BasePart, p2 : BasePart) : boolean
return (p1.CFrame.Position - p2.CFrame.Position).Magnitude > 20
end
--[[function self:ShowHideCharacter()
return function()
for _,part in pairs(self.character:GetChildren()) do
if not part:IsA("BasePart") or part.Name == "HumanoidRootPart" then continue end
part.Transparency = part.Transparency == 0 and .8 or 0
end
end
end]]
function self.RequestOpenShopGui(inputObject, processed : boolean | number)
if not self.currentMerchant or not self.characterExists then return end
if type(processed) == "boolean" and inputObject.KeyCode ~= Enum.KeyCode.F then return end
ReplicatedStorage.Events.ShopMenuOpen:FireServer(self.currentMerchant)
end
function self.onBuyFailure(playerGui, cashAmount, merchantName, itemName, plrCash, errorMessage)
if merchantName ~= self.currentMerchant then return end
cashAmount.Value = plrCash
for _,surfaceGui : SurfaceGui in pairs(playerGui[merchantName]:GetChildren()) do
if not surfaceGui:FindFirstChild("ItemName") or surfaceGui:FindFirstChild("ItemName").Value ~= itemName then continue end
local buyButton = surfaceGui:FindFirstChildOfClass("TextButton")
buyButton.Text = errorMessage
local i = 1
repeat
buyButton.BackgroundColor3 = Color3.fromRGB(255, 90, 40)
task.wait(.2)
buyButton.BackgroundColor3 = Color3.fromRGB(180, 30, 10)
task.wait(.2)
i += 1
until i == 5
buyButton.Text = `{itemName}: ${workspace:FindFirstChild(`{merchantName}Shop`).DisplayItems[itemName].Price.Value}`
buyButton.BackgroundColor3 = Color3.fromRGB(255, 40, 0)
end
end
function self.onShopNearby(merchant, merchantRoot, shopPrompt, playerGui)
if self.currentMerchant == merchant.Name then return end
print("Player is in shop vicinity!")
self.currentMerchant = merchant.Name
shopPrompt.Adornee = merchantRoot
shopPrompt.Parent = playerGui
StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Chat, true)
end
return self
ShopScript (LocalScript, StarterPlayerScripts)
local UserInputSVC = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local RunSVC = game:GetService("RunService")
local CollectionSVC = game:GetService("CollectionService")
local TweenSVC = game:GetService("TweenService")
local StarterGui = game:GetService("StarterGui")
local player = Players.LocalPlayer
local ShopClient = require(ReplicatedStorage.Modules.ShopClient)
player.CharacterAdded:Connect(ShopClient.Awake)
player.CharacterRemoving:Connect(ShopClient.Sleep)
local playerGui = player:FindFirstChildOfClass("PlayerGui")
local exitShopGui = playerGui:WaitForChild("ExitShop")
local exitShopButton = exitShopGui:WaitForChild("ExitButton")
local cashGui = playerGui:WaitForChild("CashGui")
local cashDisplay = cashGui:WaitForChild("Display")
local cashAmount = cashGui:WaitForChild("Amount")
cashDisplay.Text = `${cashAmount.Value}`
local function getItemFromGui(itemName)
return workspace:FindFirstChild(`{ShopClient.currentMerchant}Shop`).DisplayItems[itemName]
end
cashAmount.Changed:Connect(function()
cashDisplay.Text = `${cashAmount.Value}`
if not ShopClient.currentMerchant then return end
local shopItem
for _,buyButton in pairs(playerGui[ShopClient.currentMerchant]:GetDescendants()) do
if not buyButton:IsA("TextButton") or buyButton.Name ~= "BuyButton" then continue end
shopItem = getItemFromGui(buyButton.Parent.ItemName.Value)
if not shopItem then continue end
if cashAmount.Value < shopItem.Price.Value then
buyButton.BackgroundColor3 = Color3.fromRGB(255, 40, 0)
else
buyButton.BackgroundColor3 = Color3.fromRGB(181, 255, 125)
end
end
if not shopItem then
warn("Item does not exist on current client!")
end
end)
local eventsFolder = ReplicatedStorage.Events
local shopGuiFolder = ReplicatedStorage.ShopGui
local shopPrompt = shopGuiFolder.ShopPrompt
local promptButton = shopPrompt:FindFirstChildOfClass("TextButton")
for _,shopFolder in ipairs(shopGuiFolder:GetChildren()) do
if not shopFolder:IsA("Folder") then continue end
for __, itemGui in ipairs(shopFolder:GetChildren()) do
itemGui:FindFirstChild("BuyButton").Activated:Connect(function()
print(`Sending purchase to server for item: {itemGui.ItemName.Value}`)
eventsFolder.BuyItem:FireServer(ShopClient.currentMerchant, itemGui.ItemName.Value)
end)
end
end
local function onBuy(...)
local merchantName, itemName, plrCash, errorMessage = ...
if errorMessage then
warn(`Failed to buy item: {itemName} from merchant: {merchantName} | Reason: {errorMessage}`)
ShopClient.onBuyFailure(playerGui, cashAmount, ...)
return
end
cashAmount.Value = plrCash
print("Purchase success!")
end
eventsFolder.BuyItem.OnClientEvent:Connect(onBuy)
promptButton.Activated:Connect(ShopClient.RequestOpenShopGui)
UserInputSVC.InputBegan:Connect(ShopClient.RequestOpenShopGui)
local camera = workspace.CurrentCamera
local camEnterTweenInfo = TweenInfo.new(1.5, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
local camExitTweenInfo = TweenInfo.new(.8, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
local lastCameraCoord
local function onExitShop()
print("Player exiting shop.")
exitShopGui.Enabled = false
shopPrompt.Parent = shopGuiFolder
RunSVC:UnbindFromRenderStep("SpinShopParts")
-- RunSVC.RenderStepped:Once(ShopClient.ShowHideCharacter)
for _,shopItem in pairs(workspace:FindFirstChild(`{ShopClient.currentMerchant}Shop`).DisplayItems:GetChildren()) do
shopItem.DisplayPart.Transparency = 1
end
ShopClient.currentMerchant = nil
if not ShopClient.shopGui then return end
ShopClient.shopGui.Parent = shopGuiFolder
ShopClient.shopGui = nil
local camExitTween = TweenSVC:Create(camera, camExitTweenInfo, {CFrame = lastCameraCoord})
camExitTween.Completed:Connect(function()
camExitTween:Destroy()
end)
camExitTween:Play()
task.wait(.4)
lastCameraCoord = nil
camera.CameraType = Enum.CameraType.Custom
end
exitShopButton.Activated:Connect(onExitShop)
local function OnShopOpenPassCheck(itemFolder)
if not itemFolder then
warn("Shop open failed!")
return
end
print("Shop open success!")
shopPrompt.Parent = shopGuiFolder
ShopClient.shopGui = ReplicatedStorage.ShopGui[ShopClient.currentMerchant]
-- RunSVC.RenderStepped:Once(ShopClient.ShowHideCharacter)
StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Chat, false)
camera.CameraType = Enum.CameraType.Scriptable
local camTween = TweenSVC:Create(camera, camEnterTweenInfo, {
CFrame = workspace:FindFirstChild(`{ShopClient.currentMerchant}Shop`):FindFirstChild("CamFocus").CFrame;
})
lastCameraCoord = camera.CFrame
camTween.Completed:Connect(function()
ShopClient.shopGui.Parent = playerGui
for _,shopItem in ipairs(itemFolder:GetChildren()) do
ShopClient.shopGui[`Item{shopItem.ShelfSpot.Value}`].BuyButton.Text = `{shopItem.Name}: ${shopItem.Price.Value}`
shopItem:FindFirstChild("DisplayPart").Transparency = 0
end
RunSVC:BindToRenderStep("SpinShopParts", 1, function()
for _,shopItem in ipairs(itemFolder:GetChildren()) do
shopItem.PrimaryPart.CFrame *= CFrame.Angles(0, math.rad(-1), 0)
end
end)
camTween:Destroy()
end)
camTween:Play()
exitShopGui.Enabled = true
end
eventsFolder.ShopMenuOpen.OnClientEvent:Connect(OnShopOpenPassCheck)
local function DetectNearbyShop()
if not ShopClient.characterExists() then
if shopPrompt.Parent == shopGuiFolder then return end
shopPrompt.Parent = shopGuiFolder
return
end
for _,merchant in pairs(CollectionSVC:GetTagged("ShopNPC")) do
local merchantRoot = merchant:FindFirstChild("HumanoidRootPart")
if not ShopClient.playerTooFar(ShopClient.playerRoot, merchantRoot) then
ShopClient.onShopNearby(merchant, merchantRoot, shopPrompt, playerGui)
return
end
if ShopClient.currentMerchant ~= merchant.Name then continue end
onExitShop()
continue
end
end
RunSVC.RenderStepped:Connect(DetectNearbyShop)
Here’s the layout for everything in ReplicatedStorage:
The layout of the shop in workspace:
And here’s everything for ServerStorage and ServerScriptService:
(the ShopNPCModule is just for teleporting the merchant NPC back into place in case the weld breaks for some reason lol)