How to make a simple tool shop system

The structure and requirements for this system

Inside of ServerScriptService
A server Script named Leaderstats
A server Script named ProcessToolOrders

Inside of ReplicatedStorage
A ModuleScript named ToolData
A RemoteFunction named PurchaseToolRequest

Inside of ServerStorage
A Folder named Tools

Inside of StarterGui
A ScreenGui named ToolShopGui

Inside of ToolShopGui
A LocalScript (I also named it ToolShopGui, but you can safely change its name if you prefer). Inside add a TextButton named TemplateButton, which as the name suggests will be used as the reference by the script

A ScrollingFrame with its name left unchanged. Inside add a UIListLayout (I set its SortOrder property to Name in my example file)


The code

For the Leaderstats script I’m creating an IntValue named Money as the currency:

--!strict
local Players = game:GetService("Players")

local function onPlayerAdded(player: Player)
	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats"
	leaderstats.Parent = player

	local money = Instance.new("IntValue")
	money.Name = "Money"
	money.Value = 100
	money.Parent = leaderstats
end

Players.PlayerAdded:Connect(onPlayerAdded)

This will be the ToolData module:

--!strict
local ToolData: {[string]: {Price: number}} = {}

ToolData.ExampleTool1 = {
	Price = 50
}

ToolData.ExampleTool2 = {
	Price = 100
}

ToolData.ExampleTool3 = {
	Price = 150
}

return ToolData

As the name suggests, the module is essentially a dictionary that contains information about the tools you’d like to sell. Currently the only data tools will have is their price, but feel free to experiment with adding more info like for example a description that will be shown in your gui that explains the tool’s functionality


The code for the LocalScript will be as follows:

--!strict
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local ToolData = require(ReplicatedStorage:WaitForChild("ToolData"))

local templateButton = script:WaitForChild("TemplateButton")

local toolShopGui = script.Parent
local scrollingFrame = toolShopGui:WaitForChild("ScrollingFrame")

local money = Players.LocalPlayer:WaitForChild("leaderstats"):WaitForChild("Money"):: IntValue

local purchaseToolRequest = ReplicatedStorage:WaitForChild("PurchaseToolRequest")

for name, data in ToolData do
	local defaultText = `Buy {name} (Price: {data.Price})`

	local textButton = templateButton:Clone()
	textButton.Name = name
	textButton.Text = defaultText
	textButton.Parent = scrollingFrame

	local debounce: boolean?

	textButton.Activated:Connect(function()
		if debounce then return end
		debounce = true

		if money.Value >= data.Price and purchaseToolRequest:InvokeServer(name) then
			textButton.BackgroundColor3 = Color3.new(0, 1, 0)
			textButton.Text = "Success!"
		else
			textButton.BackgroundColor3 = Color3.new(1, 0, 0)
			textButton.Text = "Fail!"
		end

		task.wait(1)

		textButton.BackgroundColor3 = templateButton.BackgroundColor3
		textButton.Text = defaultText

		debounce = nil
	end)
end

Now I know it might seem strange that I’m checking if the player has enough money inside of a LocalScript, but don’t worry as the ProcessToolOrders script will verify it as well on the server-side

Although this now creates a new question: Why check the money in a client script if you’re going to check it again on the server? There are 2 benefits for checking the value using this method, which are:

  1. Reducing the number of invocations for the RemoteFunction, which has the advantage of eliminating unnecessary processes for the server which will overall improve the player’s experience by reducing the amount of time it takes before the result of the invocation is send back
  2. Provides instant feedback for the player in the case that they don’t have enough currency to purchase the tool even if they’re experiencing poor network conditions

Before moving on to the ProcessToolOrders script, I’d also like to mention that it’s safe to change the value of the defaultText variable found in line 17 or the Color3 values found in lines 31 and 34 if you prefer


Concluding the scripts of this tutorial, this will be the code for the ProcessToolOrders script:

--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")

local ToolData = require(ReplicatedStorage.ToolData)

local purchaseToolRequest = ReplicatedStorage.PurchaseToolRequest

local toolsFolder = ServerStorage.Tools

local function onPurchaseToolRequest(player, toolName: string)
	if typeof(toolName) == "string" and ToolData[toolName] then
		local money: IntValue = player.leaderstats.Money
		local price: number = ToolData[toolName].Price

		if money.Value >= price then
			toolsFolder[toolName]:Clone().Parent = player.Backpack

			money.Value -= price

			return true
		end
	end
end

purchaseToolRequest.OnServerInvoke = onPurchaseToolRequest

Now you might be thinking: Why are you checking the type of toolName if it’s guaranteed to be a string? The truth is it’s not actually guaranteed to be a string since an exploiter can modify the data being sent to the server by the RemoteFunction

Admittedly since we’re also checking to see if the value is inside of the ToolData folder before executing the rest of the script the type check can be left out in this case, although the reason why I included it anyway is because it’s good practice if you aren’t already to get used to checking the type of values being sent to the server by a client


Conclusion

I hope you have found this tutorial helpful, and as previously mentioned, here's a place file that contains a simple but functional example of this tool shop system:

ToolBuyButtonsExample.rbxl (65.0 KB)

6 Likes