Easy Tool Shop with Tool Saving

Easy Tool Shop with Tool Saving

About

This module is really easy to set up and it’s very useful for making Tool Shop with tool saving!
Tools are loaded by ToolID NumberValue that can be stored in (almost) every datastore.
If you get the Model or Install Place you will get simple DataStore with it. You can simply delete the datastore script if you already have one and just add ToolID NumberValue with Folder to your own!

Benefits

  1. Built in AdminGui for giving tools
  2. Very easy to set up!
  3. Can be used with (almost) every datastore only thing you need to do is create Folder for saving ToolD NumberValue and create NumberValue with name ToolID inside your DataStore
  4. Support for ClickDetectors!

Documentation/How to set up

  1. Get Model/Uncopylocked place/Install place below
  2. If you get model then ungroup models to Services by their model names
  3. Set up ToolModule whatever you like by notes that are literally everywhere to help you with setting up everything
  4. You are done! (Maybe make your own gui for Shop/Admin Gui because mine looks horrible)
ModuleScript (ToolModule)/ Settings
local module = {
	Settings = { --Table with information about value and folder names
		["ToolStorageFolder"] = {Name = "SkinStorage", Service = "ServerStorage"}, --First parameter 'Name' is Name of the Folder where we are storing tools, second parameter is Service where is the Folder for storing tool located Warning: Folder needs to be direct child of the Service (Recommended service to store tools is ServerStorage becase Exploiter can't access it)
		["AdminGui"] = {Name = "AdminGui", Service = "ServerStorage"}, --First parameter is 'Name' of the gui for Admin's and second parameter is Service if you don't want to exploiter's clone Admin Gui for themselves then don't change the location!
		["CloneInformation"] = true, --Optional if we want to access to tables with Client (set to false if you dont want this ModuleScript (ToolModule) to be cloned inside ReplicatedStorage)
		["CanBuyPrevious"] = true, --If its on true then Player can buy previous tools or whatever tools Player wants, if its on false then Player can't buy anything else instead of Current ToolID + 1 (Next Tool)
		["ClickDetectorMethod"] = true, --Optional if you want to be able to buy Tools with ClickDetectors (set to false if you dont want to use ClickDetectors)
		Currencies = {
			["Currency1"] = {Currency = "Cash", Folder = "leaderstats"}, --First Parameter is Currency Name that you want to buy tools for, second parameter is Folder Name where is the Currency located (parented) leaderstats etc.
			["Currency2"] = {Currency = "Gems", Folder = "leaderstats"},
			-->>					ˇ Tool Data Values ˇ					<<--
			["ToolData"] = {Currency = "ToolSkin", ToolFolder = "StatFolder"}, --First Parameter is Name of Tool Storage Number Value and second parameter is Folder Name where is Tool Storage Number located (parented) StatFolder etc.
			["ToolData2"] = {Currency = "BackpackTool", ToolFolder = "StatFolder"}, 
			-->>					^ Tool Data Values ^					<<--
		},
		AdminCommands = { --Here you can customize if you want certain commands for Admins to work!
			["GiveTools"] = true, --If it's on true it will work if it's on false it won't work, this command allows Admins to give Tools to anybody!
			["RemoveData"] = true, --Note: Action made by this command can not be undone! - This will work only with normal DataStores at the moment!!! This command will remove all specified Player's Data (Not Data from all Players but only specified player!)
		},
		Admins = { --Table with information about Admins, be careful when you put here someone, Admins are able to change their ToolID value to whatever they want
			[994223339] = {Name = "caviarbro"}, --Copy UserId of the player that should be Admin here, Name is optional you can leave it blank "", but it helps to indicate who is who (the name parameter is not used in any scripts so it can be the admin nickname or whatever)
		},
		DataStoreAdmin = { --This will work only with normal DataStores (at the moment)
			["PlayerKey"] = "Id_", --Unique key for storing player's data this needs to be changed if you want certain admin function(s) to work, Note: Put there only string not whole Unique key with Player UserId (bad example: "Id_"..player.UserId) <<-- this is wrong you only want the string so it will be only "Id_" !!! (If you are using my Datastore then You can find this Key inside script called 'DataStore' on line 23)
			["DataStoreName"] = "SkinStore", --Name of your DataStore (If you are using my version then you can find this information in script called 'DataStore' on line 1)
		},
	},
	ClickParts = { --Table with ClickParts, remove everything from this table if you dont want to use ClickDetector buying method
		["ClickPart"] = {ToolID = 3, Callback = true, DefaultText = "Buy Tool -With Callback", WaitTime = 2.5, TextLabelName = "TextLabel"}, --If Callback is true then the message will change to the return message of the Buy function, if you want Callback then set it to true if not then set it to false, if we have callback set to true then we will define DefaultText as the original Text that you want on the part, third parameter WaitTime if we have callback to true then we want to set time before it changes back to the DefaultText that we defined before WaitTime and the last parameter is the name of the TextLabel if we have callback set to true
		["ClickPart2"] = {ToolID = 1, Callback = false, DefaultText = "", WaitTime = 0, TextLabelName = ""}, --Example of ClickPart without Callback Message
		["ClickPart3"] = {ToolID = 2, Callback = true, DefaultText = "Buy Tool 3", WaitTime = 1, TextLabelName = "LabelText"}, --Example of ClickPart with Callback Message
	},
	TableWithTools = { --Table with informations about tools
		["BasicTool"] = {ToolID = 0,Cost = 0, CurrencyName = "Cash", Type = "ToolSkin"}, --Default tool
		["AdvancedTool"] = {ToolID = 1,Cost = 5, CurrencyName = "Gems", Type = "BackpackTool"}, --First argument ["AdvancedTool"] is a name of the tool, second the ToolID is the ID that will be indicating what to clone to player Backpack and the third parameter is Cost of the Tool
		["AmazingTool"] = {ToolID = 2,Cost = 100, CurrencyName = "Cash", Type = "ToolSkin"},
		["UltraTool"] = {ToolID = 3,Cost = 500, CurrencyName = "Cash", Type = "BackpackTool"},
	}
}






















--[[




⚠️ Code below is explaining how everything works so feel free to read it 
but if you dont know what you are doing don't touch it!!!




]]










local TableWithTools = module["TableWithTools"] --Table with information about tools
local ToolModuleSettings = module["Settings"] --Table with Settings of whole Module
local ToolModuleCurrencies = ToolModuleSettings["Currencies"] --Table with information about Currencies that are used to buy tools for

function module:GetToolType(ToolID) --Getting Tool Type by ToolID this is useful for indicating what type is the tool
	for i,v in pairs(TableWithTools) do --Loops through table
		if v["ToolID"] == ToolID then --If we find a match then we will run the code below
			return v["Type"] --If match was found then we return Type of the tool
		end
	end
	return warn("Tool with Tool ID "..tostring(ToolID).." does not found!") --If match was not found then we return warning!
end

function module:StringConverter(args)
	return string.gsub(args, '^%s*(.-)%s*$', '%1') --return string without unwanted spaces before and after args
end

function module:GetCurrencyName(player, ToolID) --function that will give us the currency
	for i,v in pairs(TableWithTools) do --loop through table with tools
		if module:StringConverter(v["ToolID"]) == module:StringConverter(ToolID) then --if find a match then it will run the code below
			for index,toolcurrency in pairs(ToolModuleCurrencies) do --loops through table with currencies
				if toolcurrency["Currency"] == v["CurrencyName"] then --if find a match then it will return currency
					return player:FindFirstChild(toolcurrency["Folder"]):FindFirstChild(toolcurrency["Currency"]) --This line will return currency, line below this won't run
				end
			end 
		end
	end
	return warn("Tool ID does not exist or Currency is not defined/found") --Uh Oh, something went wrong (Probably ToolID Does not exist or Currency does not exist inside ToolModule (ModuleScript))
end

function module:GetToolDataName(player, ToolID) --This is useful for getting ToolData folder where we have our Tool Stored
	for i,v in pairs(TableWithTools) do --loop through table with tools
		if module:StringConverter(v["ToolID"]) == module:StringConverter(ToolID) then --If we find a match in ToolIds then we will continue with the code below
			for index,ToolData in pairs(ToolModuleCurrencies) do --loops through table with currencies (also with tool data folders)
				if ToolData["Currency"] == v["Type"] then --If ToolData (Type) Value is the same as the Type Value then we will run the code below
					return player:FindFirstChild(ToolData["ToolFolder"]):FindFirstChild(ToolData["Currency"]) --If we found a match then we will return ToolData Type
				end
			end
		end
	end
end

function module:GetAllToolFolders(player) --This is useful for getting all folders that is used for Storing Tools!
	local ToolFolderTable = {} --Blank table where we going to store our ToolData folders
	for i,v in pairs(ToolModuleCurrencies) do --loop through table with Currencies
		if v["ToolFolder"] then --If we find ToolFolder then we going to run the code belowe
			table.insert(ToolFolderTable,player:FindFirstChild(v["ToolFolder"]):FindFirstChild(v["Currency"])) --We are inserting the ToolData Type to the blank Table now with this ToolData Type
		end
	end
	return ToolFolderTable --Loop done we are returning ToolFolderTable
end

return module


ToolHandler/Shop Handler Script
--[[

-->> Easy Tool Shop with Tool Saving <<--

	Creator: caviarbro
	Discord: Caviarbro#1511
	DevForum Post: https://devforum.roblox.com/t/easy-tool-shop-with-tool-saving/1160796
	Last Update: 13/04/v1b


-->> Easy Tool Shop with Tool Saving <<--


⚠️ Code below is explaining how everything works so feel free to read it 
	but if you dont know what you are doing don't touch it!!! 
	
	 Changing Tools only works with Admin Function AddNewTool 
	 if tool is added manually then most likely expect unwanted behaviours! 
	 
	 Removing Player's data is currently in Beta Testing and this function can not be undone! ⚠️


Note(s): Everything you need is inside "ToolModule" (ModuleScript) with notes how to change it properly!

]]
























local ReplicatedStorage = game:GetService("ReplicatedStorage") --Getting Service called ReplicatedStorage where we have RemoteFunction
local ServerStorage = game:GetService("ServerStorage") --Getting Service called ServerStorage where we have Stored our tools
local RunService = game:GetService("RunService") --Getting Service called RunService for loops
local Players = game:GetService("Players") --Getting Player Service for things such as getting UserId's etc.
local Workspace = game:GetService("Workspace") --Getting Service called Workspace
local RemoteFunction = ReplicatedStorage:WaitForChild("RemoteFunction") --Remote Function to run when player wants to buy Tool
local ToolModule = require(script.Parent) --Module with table that contains informations about tools

local TableWithTools = ToolModule["TableWithTools"] --Table with information about tools
local ToolModuleSettings = ToolModule["Settings"] --Table with Settings of whole Module
local ToolModuleCurrencies = ToolModuleSettings["Currencies"] --Table with information about Currencies that are used to buy tools for
local ToolModuleAdmins = ToolModuleSettings["Admins"] --Table with information about Admin(s) UserId(s)
local ToolModuleDataStore = ToolModuleSettings["DataStoreAdmin"] --Table with information about your DataStore
local ToolModuleCommands = ToolModuleSettings["AdminCommands"] --Table where you can customize your commands to be able to use or not!

local ToolStorage = game:GetService(ToolModuleSettings["ToolStorageFolder"]["Service"]):WaitForChild(ToolModuleSettings["ToolStorageFolder"]["Name"]) --Getting place where we have stored our Tools

-->> PlayerAdded function <<--

game.Players.PlayerAdded:Connect(function(player) --Player Added function
	local TimeCounter = os.clock() + 2 --This variable is there to prevent this function to run before player loads, os.clock() is time now in seconds and the second parameter is number in seconds
	local co = coroutine.create(function() --there we are creating new thread that we will be resuming after TimeCounter will be done
		local ToolFolders = ToolModule:GetAllToolFolders(player) --Calling function to get All Folders with Tools in table
		if ToolModuleAdmins[player.UserId] then --if we find player userid inside of Admins in ToolModule (ModuleScript) then we will clone the Gui to player's PlayerGui
			local AdminGuiClone = game:GetService(ToolModuleSettings["AdminGui"]["Service"]):FindFirstChild(ToolModuleSettings["AdminGui"]["Name"]):Clone() --Getting clone of the gui
			if AdminGuiClone then --if gui exist then we will run the code below
				AdminGuiClone.Parent = player.PlayerGui --We are parenting the Admin Guí clone to admin's Player Gui
			end
		end
		for index,ToolIndex in pairs(ToolFolders) do
			for i,v in pairs(TableWithTools) do --looping through table to find the ToolSkin ID that player has
				if ToolIndex.Value == v["ToolID"] then --if ToolID from the table is same as the ToolSkin Value then we will be cloning the Tool
					local Tool =  ToolStorage:FindFirstChild(i) --Variable of tool to index if the tool exists or not
					if Tool then --Checking if the tool exist in Folder with tools
						local CloneTool = Tool:Clone() --Variable that will be cloning the tool, the i is index in the table so basically name of the tool for example ["BasicTool"], ["AdvancedTool"] etc.
						CloneTool.Parent = player.StarterGear --Setting the parent to StarterGear so it will appear also in player's Backpack
					else --If Tool does not exist then we will warn the server that Tool does not exist
						return warn("Tool with Name "..tostring(i).." does not exist in place where should be stored!") --Warning that can help us in debugging code if something unexpected happen!
					end
				end
			end
		end
		return print("Tool(s) were loaded!") --If tools were loaded remotely then we get return print with this message.
	end)
	
	if ToolModuleSettings["CloneInformation"] == true then  --Optional if we want to access to Information from ToolModule (ModuleScript) with Client
		local ModuleClone = script.Parent:Clone() --Creating Clone of ToolModule (ModuleScript)
		ModuleClone:FindFirstChildOfClass("Script"):Destroy() --Destroying the ToolHandler before parenting the Module to ReplicatedStorage to prevent unwanted things
		ModuleClone.Parent = ReplicatedStorage --Parenting Clone of the ModuleScript to ReplicatedStorage
	end
	
	local Connection --Connection to disconnect Heartbeat even so it won't cause future problems (lags,etc.)
	Connection = RunService.Heartbeat:Connect(function() --loop to indicate when the TimeCounter will be done
		if os.clock() >= TimeCounter then --if os.clock() (current time) is higher than TimeCounter then we will be resuming the thread
			coroutine.resume(co) --We are resuming (coroutine) thread with name "co" so the function inside will run now
			Connection:Disconnect() --Disconnecting Connection so the Heartbeat event won't run again
		end
	end)
end)

-->> PlayerAdded function <<--

-->> Normal function(s) <<--

local function DestroyTool(player,ToolID) --function to destroy tool
	for index,tool in pairs(TableWithTools) do --looping through table to find the ToolSkin ID that we want to destroy
		if tool["ToolID"] == ToolID then --If the tool inside the table has same ID that we want to destroy then it will run the code below
			if player.Backpack:FindFirstChild(index) then --We are finding through options where the tool can be located and if the tool is found then it will be destroyed!
				player.Backpack:FindFirstChild(index):Destroy() --Destroying the tool
			end
			if player.StarterGear:FindFirstChild(index) then --We are finding through options where the tool can be located and if the tool is found then it will be destroyed!
				player.StarterGear:FindFirstChild(index):Destroy() --Destroying the tool
			end
			if player.Character:FindFirstChild(index) then --We are finding through options where the tool can be located and if the tool is found then it will be destroyed!
				player.Character:FindFirstChild(index):Destroy() --Destroying the tool
			end
			return "Destroyed" --Return message (callback) if we want to indicate if the function ended
		end
	end
	return warn("Tool with ID "..tostring(ToolID).." does not found!") --If the tool is not found that we will warn the player
end

local function GetNewTool(player,ToolID) --Function that we will be calling when we want to buy new Tool first parameter is automatic player Instance and the second argument is ToolID that we want to buy
	local Currency = ToolModule:GetCurrencyName(player, ToolID) --We are calling function to find Currency Name that we defined inside of the tool
	local ToolVal = ToolModule:GetToolDataName(player,tonumber(ToolID)) --Number value that is storing ToolID
	for i,v in pairs(TableWithTools) do --loop through table to find the ToolSkin ID that player want to buy
		if ToolModule:StringConverter(v["ToolID"]) == ToolModule:StringConverter(ToolID) then --If the tool inside the table has same ID as the Tool we want to buy then it will run the code below if the Tool with this ToolID does not exist it will return warn, the v["ToolID"] and ToolID is wrapped to function that will remove unwanted spaces before and after the ToolID and in same time keep spaces between the ToolID so for example if we invoke server with ToolID '2 2' (22) it will be only 2 not 22 but if we invoke server with ToolID '    22   ' it will still be 22
			if ToolStorage:FindFirstChild(i) then --If the Tool exist inside place where we are storing our Tools then it will run the code below if the Tool does not exist it will return warn
				if ToolVal.Value ~= tonumber(ToolID) and ToolModuleSettings["CanBuyPrevious"] == true or ToolVal.Value ~= tonumber(ToolID) and ToolModuleSettings["CanBuyPrevious"] == false and ToolVal.Value == (tonumber(ToolID) - 1) and ToolVal.Value < tonumber(ToolID) then --We are checking if player does not already owns the tool or if the CanBuyPrevious is false then player can buy only the next tool! You can change this option inside of the ToolModule (ModuleScript) in settings
					if Currency.Value >= v["Cost"] then --Checking if the player Currency Value is higher than the cost of the tool that we have defined inside the table
						if ToolModule:GetToolType(ToolVal.Value) == ToolModule:GetToolType(ToolID) then --If Tool Value is the same type as the ToolID then it will run the code below with tool destroy
							local DestroyMessage = DestroyTool(player,ToolVal.Value) --Function that will destroy the current Tool
							if DestroyMessage == "Destroyed" then --If the function return this message then we will run the code below this is to prevent "tool duplicating"
								Currency.Value -= v["Cost"] --Removing Cost of the tool from player balance (Note: You can use Currency.Value = Currency.Value - v["Cost"] or just Currency.Value -= v["Cost"], the effect will be same)
								local ToolClone = ToolStorage:FindFirstChild(i):Clone() --Variable that will be cloning the tool, the i is index in the table so basically name of the tool for example ["BasicTool"], ["AdvancedTool"] etc.
								ToolVal.Value = v["ToolID"] --SettingValue of the ToolSkin to ID of the new tool
								ToolClone.Parent = player.Backpack --Setting parent to StarterGear
								return print("Successfully bought Tool with name "..tostring(i).."!"), "Successfully bought Tool with name "..tostring(i).." for "..v["Cost"].." "..tostring(Currency.Name).."!" --If player receive the tool then we will return print and this message (Note: Code below this won't run if the player receive this if not then the player will receive warn that the Tool does not exist!)
							end
						else --If Tool Value is not the same type as the ToolID then it will run the code below without destroying the tool
							Currency.Value -= v["Cost"] --Removing Cost of the tool from player balance (Note: You can use Currency.Value = Currency.Value - v["Cost"] or just Currency.Value -= v["Cost"], the effect will be same)
							local ToolClone = ToolStorage:FindFirstChild(i):Clone() --Variable that will be cloning the tool, the i is index in the table so basically name of the tool for example ["BasicTool"], ["AdvancedTool"] etc.
							ToolVal.Value = v["ToolID"] --SettingValue of the ToolSkin to ID of the new tool
							ToolClone.Parent = player.Backpack --Setting parent to StarterGear
							return print("Successfully bought Tool with name "..tostring(i).."!"), "Successfully bought Tool with name "..tostring(i).." for "..v["Cost"].." "..tostring(Currency.Name).."!" --If player receive the tool then we will return print and this message (Note: Code below this won't run if the player receive this if not then the player will receive warn that the Tool does not exist!)
						end
					else
						return warn(player.Name.." does not have enough "..tostring(Currency.Name).." to buy "..tostring(i).." for "..tostring(v["Cost"]).." "..tostring(Currency.Name).."!"),player.Name.." does not have enough "..tostring(Currency.Name).." to buy "..tostring(i).." for "..tostring(v["Cost"]).." "..tostring(Currency.Name).."!" --If player does not have enough Cash then it will return warn
					end
				else
					return warn(player.Name.." already owns "..tostring(i).." or Tool with ID "..tostring(ToolID).." has higher or lower ID than "..player.Name.." has!"), player.Name.." already owns "..tostring(i).." or Tool with ID "..tostring(ToolID).." has higher or lower ID than "..player.Name.." has!" --If player already has the tool it will return this message so the player won't be able to buy the same tool again
				end
			else
				return warn("Tool with name "..tostring(i).." does not exist in the place where the tool should be stored!"), "Tool with name "..tostring(i).." does not exist in the place where the tool should be stored!" --Return warn if the tool does not exist in the folder with Tools
			end
		end
	end
	return warn("Tool with ID "..tostring(ToolID).." does not found!"), "Tool with ID "..tostring(ToolID).." does not found!"  --If the tool is not found that we will warn the player (this will help us with debugging the code if something like this happen)
end

-->> Normal function(s) <<--

-->> Admin Function(s) <<--

local function AddNewTool(player,ToolID,ToPlayer)
	if ToolModuleCommands["GiveTools"] == true then --If Command is enabled in ToolModule (ModuleScript) then we will run the code below
		if ToolID ~= nil and ToolID ~= "" then --We are checking if the ToolID that the function is getting is existing
			local PlayerToAdd --Variable for player that should receive new Tool.
			if ToPlayer ~= nil and game.Players:FindFirstChild(tostring(ToPlayer)) then --Check if the ToPlayer is not nil and if Player Exist, if ToPlayer is nil then it will set PlayerToAdd variable to player argument that is automatically sent from Remote Function invoke as a first argument
				PlayerToAdd = game.Players:FindFirstChild(tostring(ToPlayer)) --If ToPlayer is not nil then we will set the variable of the PlayerToAdd that should receive new tool to ToPlayer argument from Remote Function Invoke
			else
				PlayerToAdd = game.Players:FindFirstChild(tostring(player)) --If ToPlayer is nil then we will set the variable of the PlayerToAdd that should receive new tool to player argument from Remote Function Invoke
			end
			local ToolVal = ToolModule:GetToolDataName(PlayerToAdd,tonumber(ToolID)) --Number value that is storing ToolID
			if ToolModuleAdmins[player.UserId] then --We are checking if the UserId is inside of Admins table in ToolModule (ModuleScript)
				for i,v in pairs(TableWithTools) do --loop through table to find the ToolSkin ID that player want to buy
					if ToolModule:StringConverter(v["ToolID"]) == ToolModule:StringConverter(ToolID) then --If the tool inside the table has same ID as the Tool we want to buy then it will run the code below if the Tool with this ToolID does not exist it will return warn, the v["ToolID"] and ToolID is wrapped to function that will remove unwanted spaces before and after the ToolID and in same time keep spaces between the ToolID so for example if we invoke server with ToolID '2 2' (22) it will be only 2 not 22 but if we invoke server with ToolID '    22   ' it will still be 22
						if ToolStorage:FindFirstChild(i) then --If the Tool exist inside place where we are storing our Tools then it will run the code below if the Tool does not exist it will return warn
							if ToolVal.Value ~= tonumber(ToolID) then --We are checking if player does not already owns the tool or if the CanBuyPrevious is false then player can buy only the next tool! You can change this option inside of the ToolModule (ModuleScript) in settings
								if ToolModule:GetToolType(ToolVal.Value) == ToolModule:GetToolType(tonumber(ToolID)) then --If Tool Value is the same type as the ToolID then it will run the code below with tool destroy
									local DestroyMessage = DestroyTool(PlayerToAdd,ToolVal.Value) --Function that will destroy the current Tool
									if DestroyMessage == "Destroyed" then --If the function return this message then we will run the code below this is to prevent "tool duplicating"
										local ToolClone = ToolStorage:FindFirstChild(i):Clone() --Variable that will be cloning the tool, the i is index in the table so basically name of the tool for example ["BasicTool"], ["AdvancedTool"] etc.
										ToolVal.Value = v["ToolID"] --Setting Value of the ToolSkin to ID of the new tool
										ToolClone.Parent = PlayerToAdd.Backpack --Setting parent to StarterGear
										return print("Successfully changed Tool of Player "..tostring(PlayerToAdd.Name).." to Tool with name "..tostring(i).."!"), "Successfully changed Tool of Player "..tostring(PlayerToAdd.Name).." to Tool with name "..tostring(i).."!" --If player receive the tool then we will return print and this message (Note: Code below this won't run if the player receive this if not then the player will receive warn that the Tool does not exist!)
									end
								else --If Tool Value is not the same type as the ToolID then it will run the code below without destroying the tool
									local ToolClone = ToolStorage:FindFirstChild(i):Clone() --Variable that will be cloning the tool, the i is index in the table so basically name of the tool for example ["BasicTool"], ["AdvancedTool"] etc.
									ToolVal.Value = v["ToolID"] --Setting Value of the ToolSkin to ID of the new tool
									ToolClone.Parent = PlayerToAdd.Backpack --Setting parent to StarterGear
									return print("Successfully changed Tool of Player "..tostring(PlayerToAdd.Name).." to Tool with name "..tostring(i).."!"), "Successfully changed Tool of Player "..tostring(PlayerToAdd.Name).." to Tool with name "..tostring(i).."!" --If player receive the tool then we will return print and this message (Note: Code below this won't run if the player receive this if not then the player will receive warn that the Tool does not exist!)
								end
							else
								return warn(PlayerToAdd.Name.." already owns "..tostring(i).."!"), PlayerToAdd.Name.." already owns "..tostring(i).."!" --If player already has the tool it will return this message so the player won't be able to get the same tool again
							end
						else
							return warn("Tool with name "..tostring(i).." does not exist in the place where the tool should be stored!"), "Tool with name "..tostring(i).." does not exist in the place where the tool should be stored!" --Return warn if the tool does not exist in the folder with Tools
						end
					end
				end
				return warn("Tool with ID "..tostring(ToolID).." does not found!"), "Tool with ID "..tostring(ToolID).." does not found!"  --If the tool is not found that we will warn the player (this will help us with debugging the code if something like this happen)
			else
				return warn("Player with name "..player.Name.." is not admin!"),"Player with name "..player.Name.." is not admin!" --If the player is not admin then it will return warn.
			end
		else
			return warn("Invalid arguments Tool ID does not exist (nil)!)"), "Invalid arguments Tool ID does not exist (nil)!" --If ToolID does not exist then we will return warn.
		end
	else
		return warn("This command is disabled if you want this command to work consider change it inside ToolModule (ModuleScript)"), "This command is disabled if you want this command to work consider change it inside ToolModule (ModuleScript)" --Return warn if command is disabled inside of ToolModule.
	end
end

local function RemoveData(player, PlayerToRemove) --Remove Player's data very sensitive function if is this command executed changes can not be undone!
	if ToolModuleCommands["RemoveData"] == true then --If command is enabled then it will run the code below
		if ToolModuleAdmins[player.UserId] then --If player who executed this command is admin then it will run the code below
			local Key
			local suc, err = pcall(function() --We are wrapping this function to pcall to prevent possible unwanted behaviours!
				local PlayerUserId = Players:GetUserIdFromNameAsync(tostring(PlayerToRemove)) --Getting UserId from PlayerToRemove Name variable, so we can add it into unique PlayerKey
				if PlayerUserId ~= nil then --If Player UserId/Name exist then we will run the code below
					Key = tostring(ToolModuleDataStore["PlayerKey"]..PlayerUserId) --Unique Player Key for Player that we want to Remove Data From
					local DS = game:GetService("DataStoreService"):GetDataStore(tostring(ToolModuleDataStore["DataStoreName"])) --Getting DataStore
					if DS ~= nil then --If DataStore exist then we will Remove Player's Data
						DS:RemoveAsync(Key) --Now we run RemoveAsync to remove player's data
						local ToFind = Players:FindFirstChild(tostring(PlayerToRemove)) --If player is in (your) game then it will run the code below
						if ToFind then --If player Exist then
							local Character = ToFind.Character --Character of the player that we want to remove Data from
							local Humanoid = Character:FindFirstChildOfClass("Humanoid") --If Character finds Children with class Humanoid then we will run the code below
							if Humanoid then --If Humanoid exists then we will run the code below
								Humanoid:UnequipTools() --If Humanoid was found then we will unequip all tools
							else --If Humanoid does not exist then we will return warn
								return warn("Humanoid does not exist anymore!") --This line returns warning determining that player's character does not exist anymore.
							end
							for i,v in pairs(player.Backpack:GetChildren() or player.StarterGear:GetChildren() or player.Character:GetChildren()) do --Loop through all possible places where tool should be located
								if v:IsA("Tool") then --We are checking if the tool has Class with name Tool
									local Tool = ToolStorage:FindFirstChild(v.Name) --If Tool exist in ToolStorage folder where we are storing our tools then we will run the code below
									if Tool then --If Tool Exist then we will destroy the tool
										v:Destroy() --This line will destroy the tool
									end
								end
							end
						end
					else
						return warn("DataStore for Name "..tostring(ToolModuleDataStore["DataStoreName"]).." Does not exist!"), "DataStore for Name "..tostring(ToolModuleDataStore["DataStoreName"]).." Does not exist!" --DataStore does not exist
					end
				else
					return warn("Player UserId or Name does not exist for Name "..tostring(PlayerToRemove).."!"), "Player UserId or Name does not exist for Name "..tostring(PlayerToRemove).."!" --Player UserId/Name does not exist.
				end
			end)
			if suc then --If everything was successfull then we will return warn with success
				return warn("All data for "..tostring(PlayerToRemove).."/"..Key.." were removed! This action can not be undone!"), "All data for "..tostring(PlayerToRemove).."/"..Key.." were removed! This action can not be undone!" --If everything works then we will return this warn.
			else --If something went wrong (Uh, Oh) then we will return warn with error message this can be very useful for future code debugging
				return warn("Error Message: "..err), "Error Message: "..err --Return warning with error message
			end
		else
			return warn(player.Name.." is not admin!"), player.Name.." is not admin!" --If player is not admin!
		end
	else
		return warn("This command is disabled if you want this command to work consider change it inside ToolModule (ModuleScript)"), "This command is disabled if you want this command to work consider change it inside ToolModule (ModuleScript)" --Return warn if command is disabled inside of ToolModule.
	end
end

-->> Admin Function(s) <<--

-->> Remote Function (Invoke Server) Request(s) <<--

local function ToolRequest(player,args,ToolID,add) --Tool Request function first parameter is automatically sent from Remote Function Invoke, second parameter is type of argument (function) that we want to call, third parameter is ToolID, and fourth parameter is Optional (usable when you want to give someone new tool if you dont want to give someone new tool then leave it as nil when you are Invoking server)
	if args == "GetTool" then --Normal function to buy tool
		return GetNewTool(player,ToolID) --calling function with player parameter and ToolID parameter
	elseif args == "AddTool" then --Admin function to add tool
		return AddNewTool(player,ToolID,add) --calling function with player parameter, ToolID parameter and add parameter that should be player that you want to add tool for (if you don't Invoke server with this it will automatically set to your User)
	elseif args == "RemoveData" then --Admin function to remove player data even if player is offline!
		return RemoveData(player, add) --we are calling remove data function with player variable that is automatically done by Remote Function Server Invoke and with add - add is Username of the Player that we want to remove data! 
	else
		return tostring(args).." is not valid argument!", tostring(args).." is not valid argument!" --if args does not exist, there's no warn because exploiters can abuse it and try to lag the server
	end
end

RemoteFunction.OnServerInvoke = ToolRequest --If the RemoteFunction is invoked then it will run the function to get New Tool

-->> Remote Function (Invoke Server) Request(s) <<--

-->> ClickDetector Method (Optional) <<--

if ToolModuleSettings["ClickDetectorMethod"] == true then --Optional if you want to be able to buy Tools with ClickDetectors (set to false if you dont want to use ClickDetectors)
	for i,v in pairs(ToolModule["ClickParts"]) do --loop through inside of ToolModule (ModuleScript) table to find Click Parts that we defined inside of ClickParts table in ToolModule (ModuleScript)
		local ClickPart = Workspace:FindFirstChild(i) --Variable to define ClickPart inside Workspace
		if ClickPart then --If the ClickPart is inside of workspace then it will run code below
			local ClickDetector = ClickPart:FindFirstChildOfClass("ClickDetector") --Variable for ClickDetector
			if ClickDetector then --If ClickDetector exist then we will run code below
				ClickDetector.MouseClick:Connect(function(player) --If Click Part is clicked then it's connected to buy event in the code below
					local Click, Message = GetNewTool(player,v["ToolID"]) --Buy function, we are getting the ToolID that we want to buy from ToolModule (ModuleScript)
					if v["Callback"] == true then --If callback inside of ClickParts table in ToolModule is true then it will run code with "response"
						for index,ClickTxt in pairs(ClickPart:GetDescendants()) do --Loop through Descendants of the part
							if ClickTxt:IsA("TextLabel") and ClickTxt.Name == v["TextLabelName"] or ClickTxt:IsA("TextButton") and ClickTxt.Name == v["TextLabelName"] then --If we find TextLabel or TextButton inside the ClickPart with name that we defined inside of ClickParts table in ToolModule then it will run the code
								ClickTxt.Text = tostring(Message) --Set Text to callback if the callback for the part is on true inside of ClickParts table in ToolModule (ModuleScript)
								wait(v["WaitTime"]) --Wait time before the callback message on the TextLabel changes back to DefaultMessage
								ClickTxt.Text = tostring(v["DefaultText"]) --Default Text inside inside of ClickParts table in ToolModule
								return true --return true so code below won't run
							end
						end
						return warn("TextLabel or TextButton for ClickPart does not found!") --If ClickText does not exist then it will return this warning.
					else --If callback inside of ClickParts table in ToolModule is false then it will run code without "response"
						return Click --Return buy function without (Text) response
					end
				end)
			else
				return warn("ClickDetector for Part "..tostring(i).." does not exist!") --If ClickDetector for part does not exist then we will return warning
			end
		else
			return warn("ClickDetector Part does not exist in workspace!") --If ClickDetector part does not exist in workspace then we will return warning
		end
	end
end

-->> ClickDetector Method (Optional) <<--

Admin Gui Client Code
local Frame = script.Parent:WaitForChild("Frame")
local ConfirmButton = Frame:WaitForChild("ConfirmButton")
local ToolIDBox = Frame:WaitForChild("ToolIDBox")
local ToolPlayerNameBox = Frame:WaitForChild("ToolPlayerNameBox")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RemoteFunction = ReplicatedStorage:WaitForChild("RemoteFunction")
local OpenCloseButton = script.Parent:WaitForChild("OpenButton")
local CloseButton = Frame:WaitForChild("CloseButton")
local ArrowNext = Frame:WaitForChild("ArrowButtonNext")
local ArrowBack = Frame:WaitForChild("ArrowButtonBack")

local function GetAttributeFunc(AttID) --Function to get current attribute
	if AttID == "Give" then
		ToolIDBox.Visible = true
		ConfirmButton.BackgroundColor3 = Color3.fromRGB(49, 147, 72)
		ConfirmButton.Text = "Confirm give"
	elseif AttID == "Remove" then
		ToolIDBox.Visible = false
		ConfirmButton.BackgroundColor3 = Color3.fromRGB(255, 0, 0)
		ConfirmButton.Text = "Confirm Data Removal for Player  ["..tostring(ToolPlayerNameBox.Text).."]!"
	end
end

ToolPlayerNameBox:GetPropertyChangedSignal("Text"):Connect(function()
	local Attribute
	Attribute = Frame:GetAttribute("Mode")
	GetAttributeFunc(Attribute)
end)

OpenCloseButton.MouseButton1Click:Connect(function()
	if Frame.Visible == false then
		Frame.Visible = true
	else
		Frame.Visible = false
	end
end)

CloseButton.MouseButton1Click:Connect(function()
	if Frame.Visible == true then
		Frame.Visible = false
	end
end)
	
ArrowNext.MouseButton1Click:Connect(function() --Arrow to click to change Functionalities
	local Attribute
	Attribute = Frame:GetAttribute("Mode")
	if Attribute == "Give" then
		Frame:SetAttribute("Mode","Remove")
		GetAttributeFunc("Remove")
	elseif Attribute == "Remove" then
		Frame:SetAttribute("Mode","Give")
		GetAttributeFunc("Give")
	end
end)

ArrowBack.MouseButton1Click:Connect(function() --Arrow to click to change Functionalities
	local Attribute
	Attribute = Frame:GetAttribute("Mode")
	if Attribute == "Give" then
		Frame:SetAttribute("Mode","Remove")
		GetAttributeFunc("Remove")
	elseif Attribute == "Remove" then
		Frame:SetAttribute("Mode","Give")
		GetAttributeFunc("Give")
	end
end)

ConfirmButton.MouseButton1Click:Connect(function() --If we click to the Confirm button it will run the code below
	local Attribute
	Attribute = Frame:GetAttribute("Mode")
	if ToolIDBox.Text ~= "" and Attribute == "Give" then --we are checking if the text is not nothing
		local Remote,Message = RemoteFunction:InvokeServer("AddTool", tonumber(ToolIDBox.Text), ToolPlayerNameBox.Text) --Invoking the server with ToolID from ToolIDBox text and ToolPlayerNameBox text to determine to who we want to add the tool
		ConfirmButton.Text = tostring(Message) --Message that we get from the RemoteFunction (callback)
		wait(2)
		GetAttributeFunc(Attribute)
	elseif ToolPlayerNameBox.Text ~= "" and Attribute == "Remove" then
		local Remote,Message = RemoteFunction:InvokeServer("RemoveData", nil, ToolPlayerNameBox.Text) --Invoking the server with ToolPlayerNameBox text to determine to who we want to remove data
		ConfirmButton.Text = tostring(Message) --Message that we get from the RemoteFunction (callback)
		wait(2)
		GetAttributeFunc(Attribute)
	end
end)



Tool Shop Gui Client Code
local Frame = script.Parent:WaitForChild("Frame")
local FrameScrolling = Frame.ScrollingFrame
local ToolExample = FrameScrolling.ToolExample
local OpenShop = script.Parent:WaitForChild("OpenButton")
local CloseShop = Frame.CloseButton
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ToolShopModule = require(ReplicatedStorage:FindFirstChild("ToolModule"))
local RemoteFunction = ReplicatedStorage.RemoteFunction
local debounce = false
local player = game:GetService("Players").LocalPlayer

local TableWithTools = ToolShopModule["TableWithTools"] --Table with information about tools
local ToolModuleSettings = ToolShopModule["Settings"] --Table with Settings of whole Module
local ToolModuleCurrencies = ToolModuleSettings["Currencies"] --Table with information about Currencies that are used to buy tools for

OpenShop.MouseButton1Click:Connect(function() --Opening/Closing Shop
	if Frame.Visible == false then
		Frame.Visible = true
	else
		Frame.Visible = false
	end
end)

CloseShop.MouseButton1Click:Connect(function() --Opening Shop
	if Frame.Visible == true then
		Frame.Visible = false
	end
end)

for i,v in pairs(TableWithTools) do --loops through table and clone every member of the Tool Table inside ToolModule and create clone with its name and cost
	if v["ToolID"] ~= 0 then
		local FrameClone = ToolExample:Clone()
		local FrameCloneToolName = FrameClone:FindFirstChild("ToolNameLabel")
		local FrameCloneToolCost = FrameClone:FindFirstChild("CostLabel")
		local NumberVal = Instance.new("NumberValue", FrameClone)
		FrameClone.Name = i
		NumberVal.Name = "ToolID"
		NumberVal.Value = v["ToolID"]
		FrameCloneToolName.Text = i
		FrameCloneToolCost.Text = "Cost: "..v["Cost"].." "..v["CurrencyName"]
		FrameClone.Visible = true
		FrameClone.Parent = FrameScrolling
	end
end

local function GetCurrencyName(player, ToolID) --this function helps to get currency that the Tool cost
	for i,v in pairs(TableWithTools) do
		if v["ToolID"] == ToolID then
			for index,toolcurrency in pairs(ToolModuleCurrencies) do
				if toolcurrency["Currency"] == v["CurrencyName"] then
					return player:FindFirstChild(toolcurrency["Folder"]):FindFirstChild(toolcurrency["Currency"])
				end
			end 
		end
	end
end

for i,v in pairs(FrameScrolling:GetDescendants()) do --Loops through all descendants if find a match then create MouseButton1Click event if MouseButton1Click event is fired then it will Invoke Server if player has enough Currency or if player doesn't have debounce to prevent spamming Server Invoke
	if v:IsA("TextButton") then
		if v.Name == "BuyButton" then
			v.MouseButton1Click:Connect(function()
				for index,tools in pairs(TableWithTools) do
					local ToolIDNumberValue = v.Parent:FindFirstChild("ToolID")
					if tools["ToolID"] == ToolIDNumberValue.Value then
						if GetCurrencyName(player,tools["ToolID"]).Value >= tools["Cost"] and not debounce then
							debounce = true
							local BuyButton = v.Parent:FindFirstChild("BuyButton")
							local Remote,Message = RemoteFunction:InvokeServer("GetTool", ToolIDNumberValue.Value)
							BuyButton.Text = tostring(Message)
							wait(2)
							BuyButton.Text = "Buy"
							debounce = false
						elseif not debounce then
							debounce = true
							local BuyButton = v.Parent:FindFirstChild("BuyButton")
							BuyButton.Text = "You don't have enough "..tools["CurrencyName"].."!"
							wait(2)
							BuyButton.Text = "Buy"
							debounce = false
						end
					end
				end
			end)
		end
	end
end

UI(s)

Admin Gui

Functionality:
robloxapp-20210413-2155555

Image(s):
UIPictureA1

Shop Gui

Image(s):
UIPictureB1

Installation/Model/Uncopylocked place

Model: EasyToolShop - Roblox
Uncopylocked place: Easy Tool Shop with Tool Saving - Roblox
Install place here: Easy Tool Shop with Tool Saving.rbxl (64.5 KB)

Note(s)

Thanks for using Easy Tool Shop!
If you found bug or way to improve something let me know down below or in my DMs!

That’s all I hope you will enjoy this Tool Shop :smiley: and please if I did something wrong let me know this is my first Community Resource so any feedback is appreciated!

23 Likes

Thanks for this! Keep up the good work!

2 Likes

Seems pretty good, I’d use it if I didn’t already make my own shop.
Also adding pictures of the GUI would be a good idea (Inside of a dropdown, the hide details.) in case people want to see the GUI without opening the place.

2 Likes

Oh… I mean I really just make something and didn’t put a lot of effort to it :D. But if I should show GUIs here I will do it after I am gonna add ViewPortFrame support because I think it looks so cool and many starting developers don’t know how to do it so once I update Easy Tool Shop to next version I will try to get there some examples with ViewPortFrames, thanks for pointing that out!

Update v1b is here

  1. Added Multi Type support (This feature will 100% be more configured in the future)
  2. Added Data Remove function to Admin System [Deleting data is done by Username]
  3. Fixed some bugs from the previous version
  4. Updated Style of Admin Gui a bit
  5. Moved some functions from ToolHandler Script to ToolModule ModuleScript so code is now looking cleaner
  6. You can now disable Admin Commands!

Please switch to this version as soon as possible if you experience any bug let me know asap!

1 Like