Scripting a Cash Door

Hello!

I am trying to create a purchasable door similar to those found in simulator games.

When the player touches the door, they should receive a GUI prompt asking if they want to purchase the door or not. The currency is a leaderstat value called “Tokens”, and the user should only be prompted if they have 3000 Tokens or more.

If the player chooses yes, they should have 3000 Tokens removed from their leaderstat, and the door should get destroyed. If they choose no, the GUI should just go away.

If the player were to leave and rejoin the game, the door should stay unlocked. I am pretty sure that I would need to use Datastores for this part, but I am not exactly sure how to do so. The elements and code that I currently have implemented is below:

There is a Part in Workplace named “Zone1Door”.

In ReplicatedStorage, there are two RemoteEvents named “OpenDoor1Gui” and “RemoveDoor1”.

In ServerScriptService, there is a Script that contains the following code:

local player = game.Players.LocalPlayer
local tokens = player.leaderstats.Tokens.Value
local openGui = game.ReplicatedStorage:WaitForChild("OpenDoor1Gui")
local door = game.Workspace:WaitForChild("Zone1Door")
local purchaseGui = game.StarterGui.Door1PurchaseGUI.Frame


game.Players.PlayerAdded:Connect(function(player)
--Function that checks if the player already bought the door
end)

door.Touched:Connect(function(player)
	if tokens >= 3000 then
		openGui:FireClient()
	end
end)

Lastly, in StarterGui, there is a local script with the following code:

local event = game.ReplicatedStorage:WaitForChild("RemoveDoor1")

event.OnClientEvent:Connect(function()
	local door = workspace.Zone1Door
	door:Destroy()
end)

and a ScreenGui containing a frame with a “Yes” TextButton, a “No” TextButton, and a LocalScript that contains the following:

local player = game.Players.LocalPlayer
local tokens = player.leaderstats.Tokens.Value
local openGui = game.ReplicatedStorage:WaitForChild("OpenDoor1Gui")
local removeDoor = game.ReplicatedStorage:WaitForChild("RemoveDoor1")
local purchaseGui = script.Parent
local yesButton = script.Parent.Yes
local noButton = script.Parent.No

openGui.OnClientEvent:Connect(function()
	purchaseGui.Visible = true
end)

yesButton.Activated:Connect(function()
	tokens -= 3000
	removeDoor:FireClient()
	purchaseGui.Visible = false
end)

yesButton.Activated:Connect(function()
	purchaseGui.Visible = false
end)

I am not too experienced in Roblox programming just yet, so anything helps, thank you! :slight_smile:

3 Likes

You didn’t give us anything you wanted help with, but if it isn’t removing the door, might help if you wait a little before firing the event as it may be firing before it replicates to the client

2 Likes

The code I provided was incomplete and I was wondering how to complete it.

You know when firing the client with a remote event the server doesn’t know which player to send the event in, so you’ll need the player parameter to pass in.
Also, the Touched event doesn’t directly give you the player, you need a method for that:

door.Touched:Connect(function(hit)
	local player = game.Players:GetPlayerFromCharacter(hit.Parent) -- Get the player from the character which should be the hit's parent
	if not player or not player:IsA("Player") then return end -- Skip if the hit's parent is not a player character

	if tokens >= 3000 then
		openGui:FireClient(player)
	end
end)

In order to save whether the player purchased the Door or not, could I use a Datastore that stores either a 0 or a 1, or is there a better way to do it?

Also, if you wanna make a variable that points to a Value object, you don’t wanna put .Value in it since you aren’t able to modify it, and the variable won’t ever update again. However, you can reference the object itself, and putting .Value when using it:

local tokens = player.leaderstats.Tokens

So, in my latest reply, I have made the mistake to not be aware of that, so here’s a fix:

if tokens.Value >= 3000 then
	openGui:FireClient(player)
end

If I use removeDoor:FireClient(player), would that help reference the player to destroy the door for?

I won’t make a full script for that, but a good thing would be to store every single door purchased in an array, a variable with multiple values, containing either true or false.

Also, you should give a unique identifier to each door (Some name that has to be unique for every door) to then use as index in the array.

If you don’t quite understand, you can check this: Tables | Documentation - Roblox Creator Hub

Random comment but you really don’t need this check because the previous method returns only a player or not. Your comments good though :+1:

Oh I was going to talk about that part too.

I see you’re using that in a Local Script, meaning you already have direct access to the client, so you shouldn’t really use any remote events. Also, removing the door should be in the Local Script. So you can just directly destroy the door in the same code where you did Fire that event.

Also, you shouldn’t change leaderstats values from a Local Script, this is when you can use the remote event to fire to the server and change the leaderstats value from the server:

-- Local Script
yesButton.Activated:Connect(function()
	local door = workspace.Zone1Door
	door:Destroy()
	purchaseGui.Visible = false
	removeDoor:FireServer()
end)
-- Server Script

local event = game.ReplicatedStorage:WaitForChild("RemoveDoor1")

event.OnServerEvent:Connect(function(player)
	local tokens = player.leaderstats.Tokens
	tokens.Value -= 3000
end)

There are obviously better ways to do that, for handling for example multiple doors with different prices. But this is just a quick one. Not bad for a beginner

For the saving part, I have:

local DataStoreService = game:GetService("DataStoreService")
local DoorsUnlocked = DataStoreService:GetDataStore("DoorsUnlocked")

local DoorData = DoorsUnlocked:GetAsync(player.UserId)
game.Players.PlayerAdded:Connect(function(player)

	if not DoorData then
		DoorData = {false}
		DoorData:SetAsync(player.UserId, DoorData)
	end

end)

game.Players.PlayerRemoving:Connect(function(player)
	DoorData:SetAsync(player.UserId, DoorData)
end)

When saving “DoorData” upon leave, would I need to redeclare it?

Oh yeah right it was just a silly double check

We can store the data in another array

local DataStoreService = game:GetService("DataStoreService")
local DoorsUnlocked = DataStoreService:GetDataStore("DoorsUnlocked")

local DoorData = {} -- Table/Array that stores the doors purchased for every player

game.Players.PlayerAdded:Connect(function(player)

	DoorData[player] = DoorsUnlocked:GetAsync(player.UserId)
	if not DoorData[player] then
		DoorData[player] = {}
		DoorsUnlocked:SetAsync(player.UserId, DoorData[player])
	end

end)

game.Players.PlayerRemoving:Connect(function(player)
	DoorsUnlocked:SetAsync(player.UserId, DoorData[player])
	DoorData[player] = nil -- We don't need the door data anymore since it's already been stored
end)

Just as a recap, in my ServerScript, I have:

local player = game.Players.LocalPlayer
local tokens = player.leaderstats.Tokens
local openGui = game.ReplicatedStorage:WaitForChild("OpenDoor1Gui")
local door = game.Workspace:WaitForChild("Zone1Door")
local purchaseGui = game.StarterGui.Door1PurchaseGUI.Frame
local event = game.ReplicatedStorage:WaitForChild("RemoveDoor1")

local DataStoreService = game:GetService("DataStoreService")
local DoorsUnlocked = DataStoreService:GetDataStore("DoorsUnlocked")
local DoorData = {}

game.Players.PlayerAdded:Connect(function(player)

	DoorData[player] = DoorsUnlocked:GetAsync(player.UserId)
	if not DoorData[player] then
		DoorData[player] = {}
		DoorsUnlocked:SetAsync(player.UserId, DoorData[player])
	end

end)

game.Players.PlayerRemoving:Connect(function(player)
	DoorsUnlocked:SetAsync(player.UserId, DoorData[player])
	DoorData[player] = nil
end)
door.Touched:Connect(function(hit)
	local player = game.Players:GetPlayerFromCharacter(hit.Parent)
	if not player or not player:IsA("Player") then return end

	if tokens.Value >= 3000 then
		openGui:FireClient(player)
	end
end)

event.OnServerEvent:Connect(function(player)
	local tokens = player.leaderstats.Tokens
	tokens.Value -= 3000
end)

and in my LocalScript in the ScreenGui, I have:

local player = game.Players.LocalPlayer
local tokens = player.leaderstats.Tokens.Value
local openGui = game.ReplicatedStorage:WaitForChild("OpenDoor1Gui")
local removeDoor = game.ReplicatedStorage:WaitForChild("RemoveDoor1")
local purchaseGui = script.Parent
local yesButton = script.Parent.Yes
local noButton = script.Parent.No

openGui.OnClientEvent:Connect(function()
	purchaseGui.Visible = true
end)

yesButton.Activated:Connect(function()
	local door = workspace.Zone1Door
	door:Destroy()
	purchaseGui.Visible = false
	removeDoor:FireServer()
end)

noButton.Activated:Connect(function()
	purchaseGui.Visible = false
end)

So the problem has not been resolved?

Not yet, we’re taking it step by step.

I’m getting a [ServerScriptService.BuyDoor1:2: attempt to index nil with ‘leaderstats’] error. Should I put back .Value again at the end of [local tokens = player.leaderstats.Tokens]?

You can remove the 2 first lines. Since you’re trying to access LocalPlayer from the server, it doesn’t give you anything.

Now when I touch the door, I get a [ServerScriptService.BuyDoor1:29: attempt to index nil with ‘Value’] error.

Okay, right in line 29 you also need to access the tokens value:

door.Touched:Connect(function(hit)
	local player = game.Players:GetPlayerFromCharacter(hit.Parent)
	if not player or not player:IsA("Player") then return end

	local tokens = player.leaderstats.Tokens
	if tokens.Value >= 3000 then
		openGui:FireClient(player)
	end
end)