One time purchase tool

I’m trying to make a shop system that uses in-game currency to purchase tools and, I need to add a one-time purchase feature.

And no, this is not the thing where if the player already has the tool in their backpack they can’t buy it again, I already implemented that feature, I want it so once the player purchases the tool, the tool will be purchased for them forever, even upon losing the tool they can get it back without paying for it again.

For example:

I have a rubber duck tool added in my shop and the player is able to buy the tool for 25 cash. The player purchases the duck and leaves the game, losing the tool because it has not saved. When player joins back in the game, they can go back to where they purchased the duck from and just grab it again, without paying for it again.

The problem is that I do not have any idea on how to make it. I know that I need datastores for it but I don’t know how those really work either.

If anybody has any idea on how I could make this in my game, I’d really appreciate some help.

5 Likes

I would create a folder called “OwnedTools” and parent it to the player whenever he joins. Whenever the player buys something, it adds an IntValue with the name of the tool (here for example, the intValue’s name would be “Rubber Duck”). When the player leaves, the values in that folder would save and when he joins again the values would load back in the folder.

In the shop script, just detect if the item’s name the player is trying to purchase is one of the IntValues’ name in the “OwnerTools” folder. If it is then just give the player the tool and if it isn’t then make it so that they can buy it.

Hierarchy:
Capture d’écran 2023-08-27 à 14.38.17

Capture d’écran 2023-08-27 à 14.50.06

Owned Tools Data Script (ServerScriptService):

local DataStore = game:GetService("DataStoreService"):GetDataStore("MainDatastore")

function formatOwnedToolsData(player)

	local data = {}

	local YourFolder = player:WaitForChild("OwnedTools")
	local steps = {}

	for i,piece in pairs(YourFolder:GetChildren()) do
		table.insert(data, piece.Name)
	end

	return data
end

function saveData(player)
	local Key = "OwnedTools_"..player.UserId
	local OwnedToolsData = formatOwnedToolsData(player)

	local succes, err = pcall(function()
		if OwnedToolsData then
			DataStore:SetAsync(Key, OwnedToolsData)
		end

	end)

	if err then
		warn('WARNING: COULD NOT SAVE PLAYER DATA: '..err)
	end
end

function loadData(player)
	local Key = "OwnedTools_"..player.UserId

	local OwnedToolsFolder = script:FindFirstChild("OwnedTools"):Clone()
	OwnedToolsFolder.Parent = player

	local sucess, err = pcall(function()
		local data = DataStore:GetAsync(Key)

		if data then
			for i, value in pairs(data) do
				if not OwnedToolsFolder:FindFirstChild(value) then
					local newTool = Instance.new("IntValue", OwnedToolsFolder)
					newTool.Name = value
				end
			end
		end
	end)

	if err then
		warn('WARNING: COULD NOT LOAD PLAYER DATA: '..err)
	end
end

function onServerShutdown()
	if game:GetService("RunService"):IsStudio() then
		wait(2)
	else
		for _, player in pairs(game.Players:GetPlayers()) do
			local finished = Instance.new("BindableEvent")

			saveData(player)

			finished.Event:Wait()
		end		
	end
end

game.ReplicatedStorage.BuyTool.OnServerEvent:Connect(function(player, ToolName)
	if not player:FindFirstChild("OwnedTools"):FindFirstChild(ToolName) then
		local newInstance = Instance.new("IntValue", player:FindFirstChild("OwnedTools"))
		newInstance.Name = ToolName
	end
end)

game.Players.PlayerAdded:Connect(loadData)
game.Players.PlayerRemoving:Connect(saveData)

Shop Script:

item.Button.MouseButton1Click:Connect(function()
	if not game.Players.LocalPlayer:FindFirstChild("OwnedTools"):FindFirstChild(item.Name) then
		game.ReplicatedStorage.BuyTool:FireServer(item.Name)
	end
end

Don’t just copy and paste the shop script as it will probably error. it’s just a general thought of how you would check if the tool is already owned.

EDIT: forgot to make it so that when you buy a tool it adds a value in the OwnedTools folder.

2 Likes

Approximately like Youf_Developer said, i would do a classic datastore system:

  • Add a new folder in Player named “Tools”.
  • Add a new BoolValue in this folder for each permanent tools of your game, so each BoolValue name are a tool name.
  • Then save/load these BoolValues when players Join/Leave the game.
  • If a BoolValue is true then it mean the player already bought it, so put the right tool in player backpack, if a BoolValue is false then it mean the player haven’t bought it so do nothing.
2 Likes

Replies above already appeals the OP question, u can use an table to manipulate tool one time purchasing, an e.g structure would be

local PlrsData = {
    ["Player1"] = {
        {"OwnedTools"] = {"SomeTool"}
}
}

local function PlayerOwnsTool(plr, ToolName)
    local data = PlrsData[plr.Name].Tools

    for _, Name in data do
          if Name == ToolName then true end
     end
end
1 Like

The concept behind DataStores is easy. There are two basic functions: GetAsync() and SetAsync().

GetAsync() basically gets data, while SetAsync() saves data.

How do I use them?
Use GetAsync() when the player joins, and SetAsync() when the player is about to leave.

Here is an example of a simple DataStore that saves player’s points.

local Players = game:GetService("Players")

local DataStoreService = game:GetService("DataStoreService")
local DataStore = DataStoreService:GetDataStore("Points")

-- When player joins
function GetData(Player)
   -- Create leaderstats
   local leaderstats = Instance.new("Folder", Player)
   leaderstats.Name = "leaderstats"
   
   local Points = Instance.new("IntValue", leaderstats)
   Points.Name = "Points"
   
   -- Get player data
   local Data = DataStore:GetAsync(Player.UserId)
   
   if Data then -- if the player has saved data
      Points.Value = Data
   else
      Points.Value = 0
   end
end

-- When player is leaving
function SaveData(Player)
   -- Get leaderstats
   local leaderstats = Player:FindFirstChild("leaderstats")
   local Points = leaderstats:FindFirstChild("Points")
   
   if leaderstats and Points then
      -- Save player data
      DataStore:SetAsync(Player.UserId, Points.Value)
   end
end

Players.PlayerAdded:Connect(GetData)
Players.PlayerRemoving:Connect(SaveData)

If you want to know more about DataStores, check these

If you want to get advanced to DataStores, check this

3 Likes

I’ve implemented your scripts into my game but now I’m trying to get the server event to fire but I can’t seem to make it fire for some reason.

  • My shop script runs on a server script so no way the remote event would fire
  • I tried to switch it to a local script but that does not seem to work either
  • I also tried to place the local script in server script service and in the proximity prompt itself, none of them worked (I did mention the proximityprompt correctly when I switched it to the server script service btw)
    Here’s the local script:
local prox = script.Parent
prox.Triggered:Connect(function()
	local item = game.ServerStorage.PawnShopTool["Rubber Duck"]
	if not game.Players.LocalPlayer:FindFirstChild("OwnedTools"):FindFirstChild(item.Name) then
		print("fired")
		game.ReplicatedStorage.Remotes.BuyTool:FireServer(item.Name)
	else
		print("didn't fire")
	end
end)

What’s even weirder is that it did not fire or print any statements nor any errors.

First of all, the Triggered event used on ProximityPrompt can only be used in a Server Script, not a LocalScript. I would personally put the script inside of the proximityPrompt.

Secondly, RemoteEvents can only fire from Server to Client and from Client to Server.
That’s when BindableEvents come in place! Bindable Events, on the other hand, are used to fire an event from Server to Server and from Client to Client.

So here is the current hierarchy of the ReplicatedStorage. Remember, it’s a BindableEvent, not a RemoteEvent. (I noticed you had a “Remotes” Folder in the Replicated Storage I added that to the scripts as well)
Capture d’écran 2023-08-27 à 15.45.08

Lastly, we’ll need to make a couple changed to the scripts. When using BindableEvents, we’re not going to write :FireServer() and .OnServerEvent, but just :Fire() and .Event. Simple right? Now let’s implement that in our scripts.

ToolSaver Script (ServerScriptService):
I’m only gonna change the last couple lines of the codes where the Remote is received. No need to change anything above that.

game.ReplicatedStorage.Remotes.BuyTool.Event:Connect(function(player, ToolName)
	if not player:FindFirstChild("OwnedTools"):FindFirstChild(ToolName) then
		local newInstance = Instance.new("IntValue", player:FindFirstChild("OwnedTools"))
		newInstance.Name = ToolName
	end
end)

Proximity Prompt Server Script (Inside of the ProximityPrompt): not a localscript!!

local prox = script.Parent
prox.Triggered:Connect(function(player)
	local item = game.ServerStorage.PawnShopTool["Rubber Duck"]
	if not game.Players.LocalPlayer:FindFirstChild("OwnedTools"):FindFirstChild(item.Name) then
		print("fired")
		game.ReplicatedStorage.Remotes.BuyTool:Fire(player, item.Name)
	else
		print("didn't fire")
	end
end)

This should do the trick! If you have any more question, feel free to ask me and if i helped, mark it as a solution! :wink:

1 Like

I have changed the scripts but now there’s 2 new errors

This is on the datastore script

  • Event is not a valid member of RemoteEvent “ReplicatedStorage.Remotes.BuyTool”

This is in the proximityprompt script (only upon being triggered)

  • Workspace.PawnShopItems.Duck.ProximityPrompt.Script:4: attempt to index nil with ‘FindFirstChild’

This is because you used a RemoteEvent instead of a Bindable Event. Remove the current “BuyTool” RemoteEvent and replace it with a BindableEvent. This is what it looks like:
Capture d’écran 2023-08-27 à 16.08.20
Make sure you rename the BindableEvent to “BuyTool”.

If you still don’t understand, here is the Difference between a RemoteEvent and a BindableEvent. They really look alike so make sure you take the correct one.
Capture d’écran 2023-08-27 à 16.08.59

This is because we can’t use “game.Players.LocalPlayer” on a ServerScript. We can only access the “LocalPlayer” on a localScript. Luckily, when someone triggers a ProximityPrompt, we can get the player that triggered it.
Capture d’écran 2023-08-27 à 16.14.07

Now that we know that, let’s change the Proximity Prompt Server Script (Inside of the ProximityPrompt):

local prox = script.Parent
prox.Triggered:Connect(function(player) -- this is the player that triggered the ProximityPrompt
	local item = game.ServerStorage.PawnShopTool["Rubber Duck"]
	if not player:FindFirstChild("OwnedTools"):FindFirstChild(item.Name) then
		print("fired")
		game.ReplicatedStorage.Remotes.BuyTool:Fire(player, item.Name)
	else
		print("didn't fire")
	end
end)
3 Likes

Worked flawlessly, thank you very much, tweaked it to my liking and it works fine :smiley:

1 Like

Then set his answer as solution.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.