Confusing gacha system error

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
    im making a gacha system

  2. What is the issue? Include screenshots / videos if possible!
    this error

  14:22:24.225  Stack Begin  -  Studio
  14:22:24.225  Script 'ServerScriptService.Leaderstats', Line 21  -  Studio - Leaderstats:21
  14:22:24.226  Stack End  -  Studio
  14:22:24.226  superman978705 joined with 0 Tokens and 70 Stardust  -  Server - Leaderstats:40
  14:22:25.877  Animation Spoofer cannot run while game is running  -  Server
  14:22:26.559  Animation Spoofer cannot run while game is running  -  Client
  14:22:32.969  0_1410575  -  Server
  14:22:33.321  Players.superman978705.PlayerScripts.GachaClient:297: attempt to concatenate nil with string  -  Client - GachaClient:297
  14:22:33.321  Stack Begin  -  Studio
  14:22:33.321  Script 'Players.superman978705.PlayerScripts.GachaClient', Line 297 - function updateCurrencyDisplays  -  Studio - GachaClient:297
  14:22:33.321  Script 'Players.superman978705.PlayerScripts.GachaClient', Line 304 - function RefreshPlayerData  -  Studio - GachaClient:304
  14:22:33.322  Script 'Players.superman978705.PlayerScripts.GachaClient', Line 500  -  Studio - GachaClient:500
  14:22:33.322  Stack End  -  Studio
  14:22:33.411  0_1447505  -  Client
  1. What solutions have you tried so far? Did you look for solutions on the Creator Hub?
    ive tried ai, devforum, documentation, etc.

After that, you should include more details if you have any. Try to make your topic as descriptive as possible, so that it’s easier for people to help you!

leaderstats script


local Players = game:GetService("Players")
local PlayerDataManager = require(script.Parent.PlayerDataManager)

Players.PlayerAdded:Connect(function(player)
	-- Load player data first
	local data = PlayerDataManager.LoadData(player)

	-- Create leaderstats folder
	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats"
	leaderstats.Parent = player

	-- Create Tokens value (loaded from data)
	local tokens = Instance.new("IntValue")
	tokens.Name = "Tokens"
	tokens.Value = data.Tokens
	tokens.Parent = leaderstats

	-- Create Stardust value (loaded from data)
	local stardust = Instance.new("IntValue")
	stardust.Name = "Stardust"
	stardust.Value = data.Stardust
	stardust.Parent = leaderstats

	-- Sync Tokens changes to DataStore
	tokens.Changed:Connect(function(newValue)
		PlayerDataManager.SetTokens(player, newValue)
	end)

	-- Sync Stardust changes to DataStore
	stardust.Changed:Connect(function(newValue)
		PlayerDataManager.SetStardust(player, newValue)
	end)

	print(player.Name.." joined with "..tokens.Value.." Tokens and "..stardust.Value.." Stardust")
end)

Players.PlayerRemoving:Connect(function(player)
	-- Save data when player leaves
	PlayerDataManager.SaveData(player)
end)

GachaClient

-- GachaClient.lua
-- Place in StarterPlayer > StarterPlayerScripts
-- Handles UI interactions

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")

-- Wait for modules to load
local Modules = ReplicatedStorage:WaitForChild("Modules")
local GachaModule = require(Modules:WaitForChild("GachaModule"))
local BannerConfig = require(Modules:WaitForChild("BannerConfig"))

local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local gachaGui = playerGui:WaitForChild("GachaGui")

-- UI Elements (with error checking)
local mainFrame = gachaGui:WaitForChild("MainFrame", 10)
if not mainFrame then
	warn("MainFrame not found in GachaGui! Please create the UI first.")
	return
end

local bannerDisplay = mainFrame:WaitForChild("BannerDisplay", 5)
local pullButton1 = mainFrame:WaitForChild("Pull1Button", 5)
local pullButton10 = mainFrame:WaitForChild("Pull10Button", 5)
local stardustLabel = mainFrame:WaitForChild("StardustLabel", 5)
local tokensLabel = mainFrame:WaitForChild("TokensLabel", 5)
local bannerButtonsFrame = mainFrame:WaitForChild("BannerButtons", 5)
local shopButton = mainFrame:WaitForChild("ShopButton", 5)
local closeButton = mainFrame:WaitForChild("CloseButton", 5)

local shopFrame = gachaGui:WaitForChild("ShopFrame", 5)
local shopCloseButton = shopFrame and shopFrame:WaitForChild("CloseButton", 5)
local shopItemsFrame = shopFrame and shopFrame:WaitForChild("ItemsFrame", 5)
local shopStardustLabel = shopFrame and shopFrame:WaitForChild("StardustLabel", 5)

local resultsFrame = gachaGui:WaitForChild("ResultsFrame", 5)
local resultsContainer = resultsFrame and resultsFrame:WaitForChild("ResultsContainer", 5)
local resultsCloseButton = resultsFrame and resultsFrame:WaitForChild("CloseButton", 5)

-- Check if critical UI elements exist
if not pullButton1 or not pullButton10 then
	warn("Critical UI elements missing! Please check the UI structure.")
	return
end

-- Set button text with costs
if pullButton1 then
	pullButton1.Text = "Pull x1\n(100 Tokens)"
end
if pullButton10 then
	pullButton10.Text = "Pull x10\n(900 Tokens)"
end

-- Remotes
local GachaRemotes = ReplicatedStorage:WaitForChild("GachaRemotes", 10)
if not GachaRemotes then
	warn("GachaRemotes folder not found! Please run SetupRemotes.lua")
	return
end

local PullRemote = GachaRemotes:WaitForChild("PullRemote", 5)
local PurchaseFromShopRemote = GachaRemotes:WaitForChild("PurchaseFromShopRemote", 5)
local GetPlayerDataRemote = GachaRemotes:WaitForChild("GetPlayerDataRemote", 5)

if not PullRemote or not PurchaseFromShopRemote or not GetPlayerDataRemote then
	warn("One or more remotes are missing! Please run SetupRemotes.lua")
	return
end

-- State
local currentBanner = "Standard"
local playerData = nil
local isPulling = false
local currentBackgroundClone = nil
local originalCameraCFrame = nil
local originalCameraType = nil
local originalSkybox = {}
local backgroundsFolder = nil
local isGachaOpen = false

-- Wait for backgrounds folder
local function GetBackgroundsFolder()
	if not backgroundsFolder then
		backgroundsFolder = ReplicatedStorage:WaitForChild("GachaBackgrounds", 5)
		if not backgroundsFolder then
			warn("GachaBackgrounds folder not found in ReplicatedStorage!")
		end
	end
	return backgroundsFolder
end

-- Save original skybox
local function SaveOriginalSkybox()
	local lighting = game:GetService("Lighting")
	originalSkybox = {
		SkyboxBk = lighting:FindFirstChild("SkyboxBk") and lighting.SkyboxBk:Clone(),
		SkyboxDn = lighting:FindFirstChild("SkyboxDn") and lighting.SkyboxDn:Clone(),
		SkyboxFt = lighting:FindFirstChild("SkyboxFt") and lighting.SkyboxFt:Clone(),
		SkyboxLf = lighting:FindFirstChild("SkyboxLf") and lighting.SkyboxLf:Clone(),
		SkyboxRt = lighting:FindFirstChild("SkyboxRt") and lighting.SkyboxRt:Clone(),
		SkyboxUp = lighting:FindFirstChild("SkyboxUp") and lighting.SkyboxUp:Clone()
	}
end

-- Apply skybox from background model
local function ApplySkybox(backgroundModel)
	local lighting = game:GetService("Lighting")

	-- Remove existing skybox
	for _, child in ipairs(lighting:GetChildren()) do
		if child:IsA("Sky") or child.Name:match("^Skybox") then
			child:Destroy()
		end
	end

	-- Check if background has a Sky object
	local skyObject = backgroundModel:FindFirstChildOfClass("Sky")
	if skyObject then
		local newSky = skyObject:Clone()
		newSky.Parent = lighting
		return
	end

	-- Check for individual skybox faces
	local skyboxFaces = {"SkyboxBk", "SkyboxDn", "SkyboxFt", "SkyboxLf", "SkyboxRt", "SkyboxUp"}
	local hasSkybox = false

	for _, faceName in ipairs(skyboxFaces) do
		local face = backgroundModel:FindFirstChild(faceName)
		if face then
			hasSkybox = true
			local newFace = face:Clone()
			newFace.Parent = lighting
		end
	end

	if not hasSkybox then
		-- No custom skybox, could set a default dark sky
		local sky = Instance.new("Sky")
		sky.SkyboxBk = "rbxasset://textures/sky/night.jpg"
		sky.SkyboxDn = "rbxasset://textures/sky/night.jpg"
		sky.SkyboxFt = "rbxasset://textures/sky/night.jpg"
		sky.SkyboxLf = "rbxasset://textures/sky/night.jpg"
		sky.SkyboxRt = "rbxasset://textures/sky/night.jpg"
		sky.SkyboxUp = "rbxasset://textures/sky/night.jpg"
		sky.Parent = lighting
	end
end

-- Restore original skybox
local function RestoreSkybox()
	local lighting = game:GetService("Lighting")

	-- Remove gacha skybox
	for _, child in ipairs(lighting:GetChildren()) do
		if child:IsA("Sky") or child.Name:match("^Skybox") then
			child:Destroy()
		end
	end

	-- Restore original
	for name, skyboxPart in pairs(originalSkybox) do
		if skyboxPart then
			local restored = skyboxPart:Clone()
			restored.Parent = lighting
		end
	end
end

-- Load and position background model (CLIENT-SIDE)
local function LoadBackgroundModel(bannerName)
	local bannerData = BannerConfig.Banners[bannerName]
	if not bannerData or not bannerData.BackgroundModel then
		warn("No background model specified for banner: "..bannerName)
		return
	end

	-- Remove old background if it exists
	if currentBackgroundClone then
		currentBackgroundClone:Destroy()
		currentBackgroundClone = nil
	end

	-- Get backgrounds folder
	local bgFolder = GetBackgroundsFolder()
	if not bgFolder then return end

	-- Find the background model template
	local backgroundTemplate = bgFolder:FindFirstChild(bannerData.BackgroundModel)
	if not backgroundTemplate then
		warn("Background model not found: "..bannerData.BackgroundModel.." in ReplicatedStorage.GachaBackgrounds")
		return
	end

	-- Clone the background model to the camera
	-- This keeps it client-side only
	currentBackgroundClone = backgroundTemplate:Clone()

	-- Parent to CurrentCamera so it's only visible to this client
	currentBackgroundClone.Parent = workspace.CurrentCamera

	-- Apply skybox if gacha is open
	if isGachaOpen then
		ApplySkybox(currentBackgroundClone)
	end

	-- Find the camera part in the model
	local cameraPart = currentBackgroundClone:FindFirstChild("Camera")
	if not cameraPart then
		warn("Camera part not found in background model: "..bannerData.BackgroundModel)
		currentBackgroundClone:Destroy()
		currentBackgroundClone = nil
		return
	end

	-- Set camera to the background camera position (only if gacha is open)
	if isGachaOpen then
		local camera = workspace.CurrentCamera

		-- Save original camera settings (only first time)
		if not originalCameraCFrame then
			originalCameraCFrame = camera.CFrame
			originalCameraType = camera.CameraType
		end

		-- Switch to scriptable camera at the background position
		camera.CameraType = Enum.CameraType.Scriptable
		camera.CFrame = cameraPart.CFrame
	end

	-- Make the Camera part invisible (just in case)
	if cameraPart:IsA("BasePart") then
		cameraPart.Transparency = 1
		cameraPart.CanCollide = false
	end
end

-- Restore original camera (CLIENT-SIDE)
local function RestoreCamera()
	local camera = workspace.CurrentCamera

	if originalCameraType then
		camera.CameraType = originalCameraType
	end

	-- Reset values
	originalCameraCFrame = nil
	originalCameraType = nil

	-- Remove background model clone
	if currentBackgroundClone then
		currentBackgroundClone:Destroy()
		currentBackgroundClone = nil
	end

	-- Restore skybox
	RestoreSkybox()
end

-- Open gacha (with camera and skybox)
local function OpenGachaUI()
	if isGachaOpen then return end
	isGachaOpen = true

	-- Save current skybox before opening
	SaveOriginalSkybox()

	-- Show UI
	mainFrame.Visible = true
	gachaGui.Enabled = true

	-- Load background with camera and skybox
	LoadBackgroundModel(currentBanner)
end

-- Close gacha (restore camera and skybox)
local function CloseGachaUI()
	if not isGachaOpen then return end
	isGachaOpen = false

	-- Hide UI
	mainFrame.Visible = false
	gachaGui.Enabled = false

	-- Restore everything
	RestoreCamera()
end

-- Update stardust display
local function updateCurrencyDisplays()
	if playerData then
		stardustLabel.Text = "⭐ "..playerData.Stardust
		shopStardustLabel.Text = "⭐ "..playerData.Stardust
		tokensLabel.Text = playerData.Tokens.." Tokens."
	end
end

-- Refresh player data
local function RefreshPlayerData()
	playerData = GetPlayerDataRemote:InvokeServer()
	updateCurrencyDisplays()
end

-- Update banner display
local function UpdateBannerDisplay()
	local bannerData = BannerConfig.Banners[currentBanner]
	if bannerDisplay then
		bannerDisplay.Image = bannerData.imageId
		if bannerDisplay:FindFirstChild("Title") then
			bannerDisplay.Title.Text = bannerData.name
		end
	end

	-- Load the background model and switch camera (only if gacha is open)
	if isGachaOpen then
		LoadBackgroundModel(currentBanner)
	end
end

-- Create banner selection buttons
local function CreateBannerButtons()
	for bannerName, bannerData in pairs(BannerConfig.Banners) do
		local button = Instance.new("TextButton")
		button.Size = UDim2.new(0, 150, 0, 40)
		button.Text = bannerData.name
		button.Parent = bannerButtonsFrame

		button.MouseButton1Click:Connect(function()
			currentBanner = bannerName
			UpdateBannerDisplay()
		end)
	end
end

-- Show pull results
local function ShowResults(results)
	-- Clear previous results
	for _, child in ipairs(resultsContainer:GetChildren()) do
		if child:IsA("Frame") then
			child:Destroy()
		end
	end

	-- Create result cards
	for i, result in ipairs(results) do
		local card = Instance.new("Frame")
		card.Size = UDim2.new(0, 150, 0, 200)
		card.BackgroundColor3 = GachaModule.RarityConfig[result.rarity].color
		card.Parent = resultsContainer

		local nameLabel = Instance.new("TextLabel")
		nameLabel.Size = UDim2.new(1, 0, 0, 30)
		nameLabel.Position = UDim2.new(0, 0, 0, 0)
		nameLabel.Text = result.name
		nameLabel.TextScaled = true
		nameLabel.Parent = card

		local rarityLabel = Instance.new("TextLabel")
		rarityLabel.Size = UDim2.new(1, 0, 0, 25)
		rarityLabel.Position = UDim2.new(0, 0, 0, 35)
		rarityLabel.Text = result.rarity
		rarityLabel.TextScaled = true
		rarityLabel.Parent = card

		if result.isDuplicate then
			local dupLabel = Instance.new("TextLabel")
			dupLabel.Size = UDim2.new(1, 0, 0, 30)
			dupLabel.Position = UDim2.new(0, 0, 1, -30)
			dupLabel.Text = "DUPLICATE\n+"..result.stardustGained.." ⭐"
			dupLabel.TextScaled = true
			dupLabel.TextColor3 = Color3.new(1, 1, 0)
			dupLabel.Parent = card
		else
			local newLabel = Instance.new("TextLabel")
			newLabel.Size = UDim2.new(1, 0, 0, 30)
			newLabel.Position = UDim2.new(0, 0, 1, -30)
			newLabel.Text = "NEW!"
			newLabel.TextScaled = true
			newLabel.TextColor3 = Color3.new(0, 1, 0)
			newLabel.Parent = card
		end

		-- Animate card appearance
		card.BackgroundTransparency = 1
		TweenService:Create(card, TweenInfo.new(0.3), {BackgroundTransparency = 0}):Play()
	end

	resultsFrame.Visible = true
end

-- Handle pulls
local function DoPull(count)
	if isPulling then return end
	isPulling = true

	if pullButton1 then
		pullButton1.Text = "Rolling..."
	end
	if pullButton10 then
		pullButton10.Text = "Rolling..."
	end

	local response = PullRemote:InvokeServer(currentBanner, count)

	if response.success then
		UpdateCurrencyDisplays() -- Update both currencies from leaderstats
		ShowResults(response.results)
	else
		warn("Pull failed: "..response.message)
		-- Show error to player
		if pullButton1 then
			pullButton1.Text = response.message
			wait(2)
		end
	end

	if pullButton1 then
		pullButton1.Text = "Pull x1\n(100 Tokens)"
	end
	if pullButton10 then
		pullButton10.Text = "Pull x10\n(900 Tokens)"
	end
	isPulling = false
end

-- Create shop items
local function PopulateShop()
	for _, child in ipairs(shopItemsFrame:GetChildren()) do
		if child:IsA("Frame") then
			child:Destroy()
		end
	end

	for index, shopItem in ipairs(BannerConfig.StardustShop) do
		local itemFrame = Instance.new("Frame")
		itemFrame.Size = UDim2.new(0, 200, 0, 100)
		itemFrame.BackgroundColor3 = GachaModule.RarityConfig[shopItem.rarity].color
		itemFrame.Parent = shopItemsFrame

		local nameLabel = Instance.new("TextLabel")
		nameLabel.Size = UDim2.new(1, 0, 0.4, 0)
		nameLabel.Text = shopItem.name
		nameLabel.TextScaled = true
		nameLabel.Parent = itemFrame

		local buyButton = Instance.new("TextButton")
		buyButton.Size = UDim2.new(0.8, 0, 0.4, 0)
		buyButton.Position = UDim2.new(0.1, 0, 0.5, 0)
		buyButton.Text = "Buy ("..shopItem.cost.." ⭐)"
		buyButton.Parent = itemFrame

		buyButton.MouseButton1Click:Connect(function()
			local response = PurchaseFromShopRemote:InvokeServer(index)
			if response.success then
				playerData.Stardust = response.newStardustTotal
				UpdateStardustDisplay()
				-- Could show a success message here
			else
				warn(response.message)
			end
		end)
	end
end

-- Button connections
pullButton1.MouseButton1Click:Connect(function()
	DoPull(1)
end)

pullButton10.MouseButton1Click:Connect(function()
	DoPull(10)
end)

shopButton.MouseButton1Click:Connect(function()
	if shopFrame then
		shopFrame.Visible = true
		PopulateShop()
	end
end)

shopCloseButton.MouseButton1Click:Connect(function()
	if shopFrame then
		shopFrame.Visible = false
	end
end)

closeButton.MouseButton1Click:Connect(function()
	CloseGachaUI() -- Use the new close function that restores camera and skybox
end)

resultsCloseButton.MouseButton1Click:Connect(function()
	resultsFrame.Visible = false
end)

-- Initialize
CreateBannerButtons()
RefreshPlayerData()
UpdateBannerDisplay()
if shopFrame then shopFrame.Visible = false end
if resultsFrame then resultsFrame.Visible = false end

-- Keep gacha closed initially
gachaGui.Enabled = false
mainFrame.Visible = false

-- Monitor leaderstats changes for live updates
if player:FindFirstChild("leaderstats") then
	if player.leaderstats:FindFirstChild("Tokens") then
		player.leaderstats.Tokens.Changed:Connect(function()
			UpdateCurrencyDisplays()
		end)
	end
	if player.leaderstats:FindFirstChild("Stardust") then
		player.leaderstats.Stardust.Changed:Connect(function()
			UpdateCurrencyDisplays()
		end)
	end
end

-- Monitor GachaGui.Enabled changes
gachaGui:GetPropertyChangedSignal("Enabled"):Connect(function()
	if gachaGui.Enabled then
		OpenGachaUI()
	else
		CloseGachaUI()
	end
end)

-- Example: Open gacha with G key (optional, remove if you have your own method)
local UserInputService = game:GetService("UserInputService")
UserInputService.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then return end
	if input.KeyCode == Enum.KeyCode.G then
		if gachaGui.Enabled then
			gachaGui.Enabled = false
		else
			gachaGui.Enabled = true
		end
	end
end)

Please do not ask people to write entire scripts or design entire systems for you. If you can’t answer the three questions above, you should probably pick a different category.

Totally exposed.

The error is happening here:
Screenshot 2025-10-24 at 8.36.39 PM
Basically, playerdata.Tokens is nil, and you try to concat it to " Tokens".

This causes an error.