PcoWSkbVqDnWTu_dm2ix
We use cookies on this site to enhance your user experience

In-Game Currency

In-Game Currency

Jul 03 2018, 10:16 AM PST 20 min

In-game currencies can be a great way to add retention and monetization to a game. There are many ways to implement virtual economies, but this tutorial will cover how to implement a single currency economy. A player can gain currency in this economy through various in-game actions, by logging in daily, or through purchases with Robux.

This tutorial will make use of a ModuleScript library that will handle all of the data store calls. Scripts that use this library will have to require it with its asset id like so:

local CurrencyManager = require(389753980)

If you are interested to see how this ModuleScript works, the full source is available at the end of this article.

Game Set-up

This tutorial has several different components. If you want to use the code from the tutorial verbatim, it is important your game is set up the way the code expects.

InGameCurrency_Image0.png

  • ReplicatedStorage
    • BuyableAssets - ModuleScript containing the shirts a player can buy in the game
    • CurrencyEvents - Folder containing all of the RemoteEvents used by the script
  • ServerScriptService
    • PurhcaseScript - Script that handles the purchases of the player
  • StarterGui
    • ShopGui - LocalScript that handles the input from the player
    • ShopScreen - ScreenGui that contains all of the GUI elements for purchasing

InGameCurrency_Image1.png

Player Set-up

When the player first joins the game, you will want to make sure that they have been initialized in the DataStore. In PurchaseScript you can call the Currency Module’s InitializePlayer function using Players/PlayerAdded.

local CurrencyManager = require(389753980)

local currentGold = {}
local updateCurrencyDisplayEvent = game.ReplicatedStorage.CurrencyEvents.UpdateCurrencyDisplay
CurrencyManager:SetInitialFunds(100)

local function onPlayerJoin(player)
	CurrencyManager:InitializePlayer(player.UserId) 
	local balance = CurrencyManager:GetBalance(player.UserId)
	currentGold[player.UserId] = balance
	updateCurrencyDisplayEvent:FireClient(player, balance)
end

game.Players.PlayerAdded:connect(onPlayerJoin)
for _, player in ipairs(game.Players:GetPlayers()) do
	onPlayerJoin(player)
end

The above code creates the function onPlayerJoin which takes a Player as an argument. The function first calls InitializePlayer which adds the player to the DataStore if they are not already there. If the player is added in this way, they will be given an initial amount of currency specified by SetInitialFunds.

Next, the function checks how much currency the player has with the GetBalance function. It then stores this value in a table (so it doesn’t have to keep checking in with the DataStore) and fires a remote event to update the currency display on the client.

The code then connects onPlayerJoin to the PlayerAdded event and calls the function on all of the players currently in the game in case they joined while this script was running.

The LocalScript ShopGui handles the RemoteEvent fired by the above code by updating the contents of CurrentCurrencyDisplay:

local player = game.Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local shopScreen = playerGui:WaitForChild("ShopScreen")
local buyCurrencyFrame = shopScreen:WaitForChild("BuyCurrencyFrame")
local currentCurrencyDisplay = buyCurrencyFrame:WaitForChild("CurrentCurrencyDisplay")

local updateCurrencyDisplayEvent = game.ReplicatedStorage.CurrencyEvents.UpdateCurrencyDisplay

updateCurrencyDisplayEvent.OnClientEvent:connect(function(amount)
	if amount then
		currentCurrencyDisplay.Text = amount
	else
		currentCurrencyDisplay.Text = "error"
	end
end)

Daily Login Bonus

A very common retention mechanic is to reward players every day they log into the game. This can be done very simply with the CurrencyManager’s function CheckDailyLogin. This function calculates what the current day is and compares that to the last day the player joined the game. If the player is joining on a new day, then they are awarded some currency. onPlayerJoin can be modified to include this function as it is being called when the player joins anyway.

CurrencyManager:SetDailyLoginBonus(10)
CurrencyManager:SetTimeZoneOffsetHours(-8)

local function onPlayerJoin(player)
	CurrencyManager:InitializePlayer(player.UserId)
	CurrencyManager:CheckDailyLogin(player.UserId)
	local balance = CurrencyManager:GetBalance(player.UserId)
	currentGold[player.UserId] = balance
	updateCurrencyDisplayEvent:FireClient(player, balance)
end

SetDailyLoginBonus simply sets how much currency to award to the player on their daily login. SetTimeZoneOffsetHours sets the time zone offset you want to use for the server. It is impossible to get the current time of any client as that can be changed on a client’s machine. Instead, you can keep track of the current day with the server. This gives a consistent experience for all of your players that can’t be exploited. By default CurrencyManager uses -8 hours (PST) for its timezone offset, but you can change this by providing a different value to SetTimeZoneOffsetHours.

Buying Shirts

Awarding virtual currency is not very useful unless the player has some way to spend it. A common way to get players to spend money is to provide a character appearance shop. This tutorial covers how to make a shop that unlocks shirts for the player’s character, but keep in mind this code can be modified to sell whatever is appropriate for your game.

When selling products, it is useful to have a list of the products and their prices that everything in the game can reference so you don’t have to hardcode the values in your scripts. A ModuleScript in ReplicatedStorage is very useful for this, as any script in the game, either server or local, can access the contents of ReplicatedStorage.

local BuyableAssets = {}

BuyableAssets.Shirts = {}
BuyableAssets.Shirts[323171019] = {Cost = 10}
BuyableAssets.Shirts[176086705] = {Cost = 10}
BuyableAssets.Shirts[323170554] = {Cost = 10}

return BuyableAssets

The above ModuleScript simply returns a table with all of the shirts a player can buy. This table is indexed by the assetid of the shirt, and the values simply contain the cost in in-game currency.

Once you have a table of products, you can use it to display the available products to the player. You can have any kind of interface you want to show players this information, but for the sake of example a simple list on the side of the screen is set up in the ShopGui LocalScript:

local player = game.Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local shopScreen = playerGui:WaitForChild("ShopScreen")
local shirtFrame = shopScreen:WaitForChild("ShirtFrame")
local buyFrame = shopScreen:WaitForChild("BuyFrame")
local buyAsset = game.ReplicatedStorage.CurrencyEvents.BuyAsset

local BuyableAssets = require(game.ReplicatedStorage.BuyableAssets)

local currentProduct = nil

local index = 0
for id, cost in pairs(BuyableAssets.Shirts) do
	local shirtButton = Instance.new("ImageButton")
	shirtButton.Image = "http://www.roblox.com/Thumbs/Asset.ashx?width=110&height=110&assetId=" .. id
	shirtButton.Size = UDim2.new(0, 100, 0, 100)
	shirtButton.Position = UDim2.new(0, 5, 0, 5 + index * 105)
	shirtButton.Parent = shirtFrame
	shirtButton.MouseButton1Click:connect(function()
		buyFrame.Cost.Text = BuyableAssets.Shirts[id].Cost
		buyFrame.Visible = true
		currentProduct = id
	end)
	index = index + 1
end

buyFrame.Yes.MouseButton1Click:connect(function()
	buyFrame.Visible = false
	buyAsset:FireServer(currentProduct)
	currentProduct = nil
end)

buyFrame.No.MouseButton1Click:connect(function()
	buyFrame.Visible = false
	currentProduct = nil
end)

After declaring variables for the various UI elements, the script requires the BuyableAssets ModuleScript which contains all of the information of sellable items in the game. The script goes through all of these items in a loop and creates an ImageButton for each. The GuiButton/MouseButton1Click event for each of these buttons is bound to a simple inline function. This function updates the Cost frame in the BuyFrame to the price of the product, makes the BuyFrame visible, and stores the current product id in currentProduct.

The MouseButton1Click events for both the Yes and No buttons are bound to very similar functions. Both make the BuyFrame invisible and clear the current product. The function for the Yes frame also fires the BuyAsset RemoteEvent.

In the PurchaseScript on the server, a function has to be set up for when BuyAsset fires. This function has to attempt to buy the asset and give it to the player if successful.

local BuyableAssets = require(game.ReplicatedStorage.BuyableAssets)
local buyAssetEvent = game.ReplicatedStorage.CurrencyEvents.BuyAsset

local wearableAssetLimit = {}
wearableAssetLimit["Hat"] = 3
wearableAssetLimit["Shirt"] = 1
wearableAssetLimit["Pants"] = 1

local function cleanCharacter(character, assetType)
	local limit = wearableAssetLimit[assetType]
	local objectsFound = 0
	for _, object in ipairs(character:GetChildren()) do
		if object:IsA(assetType) then
			objectsFound = objectsFound + 1
			if objectsFound >= limit then
				object:Destroy()
				objectsFound = objectsFound - 1
			end
		end
	end
end

local function onBuyAsset(player, assetId)
	local purchased = CurrencyManager:HasAsset(player.UserId, assetId)
	if not purchased then
		
		if currentGold[player.UserId] >= BuyableAssets.Shirts[assetId].Cost then
			local success = CurrencyManager:GiveAsset(player, assetId)
			if success then
				local success, balance, enoughFunds = CurrencyManager:Withdraw(player.UserId, BuyableAssets.Shirts[assetId].Cost)
				if success then
					purchased = true
					currentGold[player.UserId] = balance
					updateCurrencyDisplayEvent:FireClient(player, balance)
				end
			end
		end
	end	
	
	local character = player.Character
	if character and purchased then
		local asset = game.InsertService:LoadAsset(assetId):GetChildren()[1]
		cleanCharacter(character, asset.ClassName)
		asset.Parent = character
	end
end

buyAssetEvent.OnServerEvent:connect(onBuyAsset)

The onBuyAsset function is defined and bound to the BuyAsset RemoteEvent. This function first checks if the player has already purchased this asset by calling the HasAsset function of CurrencyManager. If not, and the player has enough money, the function then attempts to give the asset to the player and withdraw the funds from the player’s DataStore. Once the purchase goes through, UpdateCurrencyDisplay is fired to show the client their updated balance.

After the transaction has been handled, the function then gives the shirt to the player’s character. It first uses InsertService/LoadAsset to get the shirt instance from the Roblox website. It then calls cleanCharacter to make sure there is space in the character for the shirt (the same function can be used for pants and hats). It then parents the shirt to the player’s character model.

Buying Currency

You can also monetize with an in-game currency system by giving the player the option to purchase the currency with Robux.

To integrate this feature with the currency manager, you will need to create a Articles/Developer Products In Game Purchases and make note of its id. Then, in the ShopGui LocalScript call MarketplaceService/PromptProductPurchase:

local MarketplaceService = game:GetService("MarketplaceService")
local buyCurrencyProductId = 32909239

local buyCurrencyFrame = shopScreen:WaitForChild("BuyCurrencyFrame")
local buyCurrencyButton = buyCurrencyFrame:WaitForChild("BuyCurrencyButton")

buyCurrencyButton.MouseButton1Click:connect(function()
	MarketplaceService:PromptProductPurchase(player, buyCurrencyProductId)
end)

Nothing else is needed in the LocalScript as Roblox handles all of the interface for purchasing.

Next, in the PurchaseScript Script you will have to create the callback for MarketplaceService/ProcessReceipt. Remember, you can only have one of these callbacks in your game, so if have already defined it to handle other developer products you will have to merge the code below with your existing code.

local MarketPlaceService = game:GetService("MarketplaceService")

local goldDeveloperProducts = {}
goldDeveloperProducts[32909239] = 100

local otherDeveloperProducts = {}
MarketPlaceService.ProcessReceipt = function(receiptInfo)
	local productId = receiptInfo.ProductId	
	
	if goldDeveloperProducts[productId] then
		local success, purchased = CurrencyManager:PurchaseHandled(receiptInfo.PlayerId, receiptInfo.PurchaseId)
		
		if success and not purchased then
			local success, balance = CurrencyManager:HandleRobuxPurchase(goldDeveloperProducts[productId], receiptInfo)
			if success then
				local player = game.Players:GetPlayerByUserId(receiptInfo.PlayerId)
				if player then
					currentGold[receiptInfo.PlayerId] = balance
					updateCurrencyDisplayEvent:FireClient(player, balance)
				end
				return Enum.ProductPurchaseDecision.PurchaseGranted
			end
		end		
		
	end
	
	if otherDeveloperProducts[productId] then
		-- Handle other dev product purchases
	end
	
	return Enum.ProductPurchaseDecision.NotProcessedYet
end

The ProcessReceipt callback function is written to accommodate other developer products, so it first checks if the product that was purchased was in the goldDeveloperProducts table. If it was, then the function calls PurchaseHandled. This function takes a playerId and a PurchaseId and checks if the purchase has already been recorded. If not, then the function calls HandleRobuxPurchase which gives currency to the player and then records the purchase. If all of this succeeds then the current currency balance of the player is updated and the function returns PurchaseGranted.

Source Code

PurchaseScript

This code goes in a Script inside of ServerScriptService.

local CurrencyManager = require(389753980)
local MarketPlaceService = game:GetService("MarketplaceService")
 
local BuyableAssets = require(game.ReplicatedStorage.BuyableAssets)
local buyAssetEvent = game.ReplicatedStorage.CurrencyEvents.BuyAsset
 
local goldDeveloperProducts = {}
goldDeveloperProducts[32909239] = 100
 
local otherDeveloperProducts = {}
 
local wearableAssetLimit = {}
wearableAssetLimit["Hat"] = 3
wearableAssetLimit["Shirt"] = 1
wearableAssetLimit["Pants"] = 1
 
local updateCurrencyDisplayEvent = game.ReplicatedStorage.CurrencyEvents.UpdateCurrencyDisplay
 
local currentGold = {}
 
MarketPlaceService.ProcessReceipt = function(receiptInfo)
	local productId = receiptInfo.ProductId	
 
	if goldDeveloperProducts[productId] then
		local success, purchased = CurrencyManager:PurchaseHandled(receiptInfo.PlayerId, receiptInfo.PurchaseId)
 
		if success and not purchased then
			local success, balance = CurrencyManager:HandleRobuxPurchase(goldDeveloperProducts[productId], receiptInfo)
			if success then
				local player = game.Players:GetPlayerByUserId(receiptInfo.PlayerId)
				if player then
					currentGold[receiptInfo.PlayerId] = balance
					updateCurrencyDisplayEvent:FireClient(player, balance)
				end
				return Enum.ProductPurchaseDecision.PurchaseGranted
			end
		end		
 
	end
 
	if otherDeveloperProducts[productId] then
		-- Handle other dev product purchases
	end
 
	return Enum.ProductPurchaseDecision.NotProcessedYet
end
 
local function onPlayerJoin(player)
	CurrencyManager:InitializePlayer(player.UserId)
	CurrencyManager:CheckDailyLogin(player.UserId)
	local balance = CurrencyManager:GetBalance(player.UserId)
	currentGold[player.UserId] = balance
	updateCurrencyDisplayEvent:FireClient(player, balance)
end
 
local function cleanCharacter(character, assetType)
	local limit = wearableAssetLimit[assetType]
	local objectsFound = 0
	for _, object in ipairs(character:GetChildren()) do
		if object:IsA(assetType) then
			objectsFound = objectsFound + 1
			if objectsFound >= limit then
				object:Destroy()
				objectsFound = objectsFound - 1
			end
		end
	end
end
 
local function onBuyAsset(player, assetId)
	local purchased = CurrencyManager:HasAsset(player.UserId, assetId)
	if not purchased then
 
		if currentGold[player.UserId] >= BuyableAssets.Shirts[assetId].Cost then
			local success = CurrencyManager:GiveAsset(player, assetId)
			if success then
				local success, balance, enoughFunds = CurrencyManager:Withdraw(player.UserId, BuyableAssets.Shirts[assetId].Cost)
				if success then
					purchased = true
					currentGold[player.UserId] = balance
					updateCurrencyDisplayEvent:FireClient(player, balance)
				end
			end
		end
	end	
 
	local character = player.Character
	if character and purchased then
		local asset = game.InsertService:LoadAsset(assetId):GetChildren()[1]
		cleanCharacter(character, asset.ClassName)
		asset.Parent = character
	end
end
 
buyAssetEvent.OnServerEvent:Connect(onBuyAsset)
 
game.Players.PlayerAdded:Connect(onPlayerJoin)
for _, player in ipairs(game.Players:GetPlayers()) do
	onPlayerJoin(player)
end

ShopGUI

This code goes inside a LocalScript inside of StarterGui

local MarketplaceService = game:GetService("MarketplaceService")
 
local player = game.Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local shopScreen = playerGui:WaitForChild("ShopScreen")
local shirtFrame = shopScreen:WaitForChild("ShirtFrame")
local buyFrame = shopScreen:WaitForChild("BuyFrame")
local buyAsset = game.ReplicatedStorage.CurrencyEvents.BuyAsset
 
local buyCurrencyFrame = shopScreen:WaitForChild("BuyCurrencyFrame")
local buyCurrencyButton = buyCurrencyFrame:WaitForChild("BuyCurrencyButton")
local currentCurrencyDisplay = buyCurrencyFrame:WaitForChild("CurrentCurrencyDisplay")
local updateCurrencyDisplayEvent = game.ReplicatedStorage.CurrencyEvents.UpdateCurrencyDisplay
 
local buyCurrencyProductId = 32909239
local BuyableAssets = require(game.ReplicatedStorage.BuyableAssets)
 
local currentProduct = nil
 
local index = 0
for id, cost in pairs(BuyableAssets.Shirts) do
	local shirtButton = Instance.new("ImageButton")
	shirtButton.Image = "http://www.roblox.com/Thumbs/Asset.ashx?width=110&height=110&assetId=" .. id
	shirtButton.Size = UDim2.new(0, 100, 0, 100)
	shirtButton.Position = UDim2.new(0, 5, 0, 5 + index * 105)
	shirtButton.Parent = shirtFrame
	shirtButton.MouseButton1Click:connect(function()
		buyFrame.Cost.Text = BuyableAssets.Shirts[id].Cost
		buyFrame.Visible = true
		currentProduct = id
	end)
	index = index + 1
end
 
buyFrame.Yes.MouseButton1Click:connect(function()
	buyFrame.Visible = false
	buyAsset:FireServer(currentProduct)
	currentProduct = nil
end)
 
buyFrame.No.MouseButton1Click:connect(function()
	buyFrame.Visible = false
	currentProduct = nil
end)
 
buyCurrencyButton.MouseButton1Click:connect(function()
	MarketplaceService:PromptProductPurchase(player, buyCurrencyProductId)
end)
 
updateCurrencyDisplayEvent.OnClientEvent:connect(function(amount)
	if amount then
		currentCurrencyDisplay.Text = amount
	else
		currentCurrencyDisplay.Text = "error"
	end
end)

BuyableAssets

This code goes inside a ModuleScript inside of ReplicatedStorage

local BuyableAssets = {}
 
BuyableAssets.Shirts = {}
BuyableAssets.Shirts[323171019] = {Cost = 10}
BuyableAssets.Shirts[176086705] = {Cost = 10}
BuyableAssets.Shirts[323170554] = {Cost = 10}
 
return BuyableAssets

CurrencyManager

This code is inside of a ModuleScript that can be required with the assetid 389753980. It is not necessary to copy this code into your game, but it is provided here so you can see how it works.

local CurrencyManager = {}
 
-- Services
local DataStoreService = game:GetService("DataStoreService")
 
-- Setable variables
local timeZoneOffset = -8 * 60 * 60
local dailyLoginBonus = 10
local initialFunds = 100
 
-- Constants
local DATASTORE_RETRIES = 3
 
-- Private variables
local playerDataStore = DataStoreService:GetDataStore("PlayerData")
 
-- Private functions
 
-- Function to retry the passed in function several times. If the passed in function
-- is unable to be run then this function returns false and creates an error.
local function dataStoreRetry(dataStoreFunction)
	local tries = 0	
	local success = true
	local data = nil
	repeat
		tries = tries + 1
		success = pcall(function() data = dataStoreFunction() end)
		if not success then wait(1) end
	until tries == DATASTORE_RETRIES or success
	if not success then
		error('Could not access DataStore! Warn players that their data might not get saved!')
	end
	return success, data
end
 
-- Attempts to process a list of transactions (either all deposits or withdrawls). Set deposit
-- to true if depositing currency, set to false if withdrawing
local function batchTransactions(playerId, transactions, deposit)
	local balance = 0
 
	-- Get sum of all transactions
	local total = 0
	for i = 1, #transactions do
		if deposit then
			total = total + transactions[i]
		else
			total = total - transactions[i]
		end
	end
 
	local enoughMoney = true
	local success = dataStoreRetry(function()
		playerDataStore:UpdateAsync(playerId, function(oldData)
			-- Check if player has enough funds to cover transaction
			if oldData.Currency + total >= 0 then
				oldData.Currency = oldData.Currency + total
			else
				enoughMoney = false
			end
			balance = oldData.Currency
			return oldData
		end)
		return true
	end)
 
	return success, balance, enoughMoney
end
 
-- Setters
-- Sets the time zone offset for the server. Mainly used to calculate daily login bonus
function CurrencyManager:SetTimeZoneOffsetHours(offset)
	timeZoneOffset = offset * 60 * 60
end
 
-- Sets the amount to award players when they rejoin the game each day
function CurrencyManager:SetDailyLoginBonus(bonus)
	dailyLoginBonus = bonus
end
 
-- Sets the initial amount of money to grant to players the first time they join
function CurrencyManager:SetInitialFunds(initial)
	initialFunds = initial
end
 
-- Public Functions
-- Stores that the player owns an asset in the game
function CurrencyManager:GiveAsset(playerId, assetId)
	local success = dataStoreRetry(function()
		local assetPurchaseHistory = DataStoreService:GetDataStore(playerId, "AssetPurchaseHistory")
		assetPurchaseHistory:SetAsync(assetId, true)
	end)
	return success
end
 
-- Checks that the player owns an asset in the game
function CurrencyManager:HasAsset(playerId, assetId)
	local found = false
	local success = dataStoreRetry(function()
		local assetPurchaseHistory = DataStoreService:GetDataStore(playerId, "AssetPurchaseHistory")
		found = assetPurchaseHistory:GetAsync(assetId)
	end)
	return found
end
 
-- Deposits a table of transactions for the given player
function CurrencyManager:DepositBatch(playerId, deposits)
	return batchTransactions(playerId, deposits, true)
end
 
-- Deposits the given amount of currency for the given player
function CurrencyManager:Deposit(playerId, amount)
	return batchTransactions(playerId, {amount}, true)
end
 
-- Deposits a table of transactions for the given player
function CurrencyManager:WithdrawBatch(playerId, withrawals)
	return batchTransactions(playerId, withrawals, false)
end
 
-- Withdraws the given amount of currency from the given player
function CurrencyManager:Withdraw(playerId, amount)
	return batchTransactions(playerId, {amount}, false)
end
 
-- Sets up the datastore for a player when that player first joins the game
function CurrencyManager:InitializePlayer(playerId)
	local success = dataStoreRetry(function()
		playerDataStore:UpdateAsync(playerId, function(oldData)
			if not oldData or oldData == {} then
				oldData = {}
				oldData.Currency = initialFunds
				oldData.LastLoginReward = 0
				return oldData
			end
		end)
	end)
	return success
end
 
-- Checks the current balance of currency a player has
function CurrencyManager:GetBalance(playerId)
	local balance = 0
	local success = dataStoreRetry(function()
		local playerData = playerDataStore:GetAsync(playerId)
		balance = playerData.Currency
		return true
	end)
	return balance
end
 
-- Check if a developer product purchase has already been recorded
function CurrencyManager:PurchaseHandled(playerId, purchaseId)
	local purchased = false
	local success = dataStoreRetry(function()
		local playerPurchaseHistory = DataStoreService:GetDataStore(playerId, "PurchaseHistory")
		purchased = playerPurchaseHistory:GetAsync(purchaseId)
	end)
	return success, purchased
end
 
-- Handle developer product purchase to buy currency with Robux
function CurrencyManager:HandleRobuxPurchase(amount, receiptInfo)
	local balance = 0
	local success = dataStoreRetry(function()
		local playerPurchaseHistory = DataStoreService:GetDataStore(receiptInfo.PlayerId, "PurchaseHistory")
 
		-- First attempt to grant currency to the player
		local batchSuccess = false
		batchSuccess, balance = batchTransactions(receiptInfo.PlayerId, {amount}, true)
		if not batchSuccess then
			return false
		end
 
		-- Currency has been awarded successfully, record the transaction
		playerPurchaseHistory:SetAsync(receiptInfo.PurchaseId, tick())
		return true
	end)
 
	return success, balance
end
 
-- Check if the player has logged in yet today
function CurrencyManager:CheckDailyLogin(playerId)
	local todayThreshold = math.floor((tick() + timeZoneOffset) / (24 * 60 * 60))
 
	local success = dataStoreRetry(function()
		playerDataStore:UpdateAsync(playerId, function(oldData)
			if oldData.LastLoginReward < todayThreshold then
				oldData.LastLoginReward = todayThreshold
				oldData.Currency = oldData.Currency + dailyLoginBonus
				return oldData
			end
		end)
		return true
	end)
 
	return success	
end
	playerDataStore:SetAsync(playerId, false)
	for id, asset in pairs(require(game.ReplicatedStorage.BuyableAssets).Shirts) do
		local assetPurchaseHistory = DataStoreService:GetDataStore(playerId, "AssetPurchaseHistory")
		assetPurchaseHistory:SetAsync(id, false)
		return true
	end
end
 
return CurrencyManager
Tags:
  • monetization
  • coding