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:
- 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
- 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)