CCCPaste

Trading System

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local UnitConfig = require(ReplicatedStorage.Databases.UnitConfig)
local BadgeData = require(ReplicatedStorage.Databases.BadgeData)
local GeneralData = require(ReplicatedStorage.Databases.GeneralData)
local Common = require(ReplicatedStorage.Common)
local Sound = require(ReplicatedStorage.Shared.Sound)
local Server = require(ReplicatedStorage.Blink.Server)

local TRADE_TIMEOUT = 120 
local MAX_TRADE_UNITS = 8
local TRADE_COOLDOWN = 5
local TRADE_EXECUTION_DELAY = 5

local function generateTradeId()
	return HttpService:GenerateGUID()
end

local function isValidUnit(unitId, playerData)
	return playerData.Inventory.Units[unitId] ~= nil
end

local function getPlayerInventoryCount(playerData)
	local count = 0
	for _ in pairs(playerData.Inventory.Units) do
		count = count + 1
	end
	return count
end

local function getMaxUnitsForPlayer(playerData)
	local maxUnits = GeneralData.Settings.Default_MaxUnits or 100
	local owned = (playerData and playerData.Gamepasses) or {}
	
	local storagePassBonus = {
		[1462584282] = 50,
		[1462564275] = 50,
		[1298364212] = 50, 
	}
	for passId, bonus in pairs(storagePassBonus) do
		if table.find(owned, passId) then
			maxUnits += bonus
		end
	end
	return maxUnits
end

local function validateInventorySpaceAfterTrade(playerData, unitsToAdd, unitsToGive)
	local addCount, giveCount = 0, 0
	for _ in pairs(unitsToAdd or {}) do addCount += 1 end
	for _ in pairs(unitsToGive or {}) do giveCount += 1 end

	local currentCount = getPlayerInventoryCount(playerData)
	local maxUnits = getMaxUnitsForPlayer(playerData)
	local finalCount = currentCount - giveCount + addCount
	return finalCount <= maxUnits
end

local function validateInventorySpace(playerData, unitsToAdd)
	local currentCount = getPlayerInventoryCount(playerData)
	local maxUnits = GeneralData.Settings.Default_MaxUnits
	if playerData and playerData.Gamepasses then
		if table.find(playerData.Gamepasses, 1298364212) then
			maxUnits = maxUnits + 50
		end
	end
	local unitsToAddCount = 0
	for _ in pairs(unitsToAdd) do
		unitsToAddCount = unitsToAddCount + 1
	end
	return currentCount + unitsToAddCount <= maxUnits
end

local TradingManager = {}
TradingManager.activeTrades = {}
TradingManager.playerCooldowns = {}
TradingManager.tradeRequests = {}

function TradingManager:Init(gameModule)
	self.game = gameModule
	
	Server.SendTradeRequest.On(function(sender, payload)

		local targetUserId
		
		if typeof(payload) == "table" and payload.UserId then
			targetUserId = tonumber(payload.UserId)
		elseif typeof(payload) == "number" then
			local function toSignedI32(n: number): number
				local r = n % 4294967296
				if r >= 2147483648 then
					r -= 4294967296
				end
				return r
			end

			local receivedI32 = payload
			for _, p in ipairs(Players:GetPlayers()) do
				if toSignedI32(p.UserId) == receivedI32 then
					targetUserId = p.UserId
					break
				end
			end

			if not targetUserId then
				for _, p in ipairs(Players:GetPlayers()) do
				end
				Server.UI.Fire(sender, {UI = "HUD", Function = "Notify", Arguments = {"Player not found!", 3, "Error"}})
				return
			end
		else
			Server.UI.Fire(sender, {UI = "HUD", Function = "Notify", Arguments = {"Player not found!", 3, "Error"}})
			return
		end

		return self:SendTradeRequest(sender, targetUserId)
	end)

	Server.AcceptTrade.On(function(player, struct)
		return self:RespondToTradeRequest(player, struct)
	end)

	Server.AddUnitToTrade.On(function(player, struct)
		return self:AddUnitsToTrade(player, struct)
	end)

	Server.RemoveUnitFromTrade.On(function(player, struct)
		return self:RemoveUnitFromTrade(player, struct)
	end)

	Server.ConfirmTrade.On(function(player, struct)
		return self:ConfirmTrade(player, struct)
	end)

	Server.CancelTrade.On(function(player, struct)
		return self:CancelTrade(player, struct)
	end)

	Players.PlayerRemoving:Connect(function(player)
		self:HandlePlayerLeaving(player)
	end)

	local debounce = {}

	if workspace:FindFirstChild("TradePortal") then
		workspace.TradePortal.PrimaryPart.Touched:Connect(function(hit)
			local player = game.Players:GetPlayerFromCharacter(hit.Parent)
			if not player or debounce[player] then return end

			debounce[player] = true
			task.delay(1, function()
				debounce[player] = nil
			end)

			Server.UI.Fire(player, {UI = "HUD", Function = "Transition", Arguments = {}})
			game["Teleport Service"]:Teleport(98293630968682, player)
		end)
	end

	if workspace:FindFirstChild("LobbyPortal") then
		workspace.LobbyPortal.PrimaryPart.Touched:Connect(function(hit)
			local player = game.Players:GetPlayerFromCharacter(hit.Parent)
			if not player or debounce[player] then return end

			debounce[player] = true
			Server.UI.Fire(player, {UI = "HUD", Function = "Transition", Arguments = {}})
			game["Teleport Service"]:Teleport(134765553595771, player)
			task.delay(1, function()
				debounce[player] = nil
			end)
		end)
	end

	task.spawn(function()
		while true do
			task.wait(10)
			self:CleanupExpiredTrades()
		end
	end)
end

function TradingManager:HandlePlayerLeaving(player)
	local trade = self.activeTrades[player.UserId]
	if trade then
		if trade.executionTimer then
			task.cancel(trade.executionTimer)
			trade.executionTimer = nil
		end

		local otherPlayer = trade.player1 == player and trade.player2 or trade.player1
		if otherPlayer then
			Server.UI.Fire(otherPlayer, {
				UI = "Trade", 
				Function = "CancelTrade", 
				Arguments = {CancelledBy = player.Name .. " (Disconnected)"}
			})
		end
		self.activeTrades[trade.player1.UserId] = nil
		self.activeTrades[trade.player2.UserId] = nil
	end

	for dequestKey, request in pairs(self.tradeRequests) do
		if request.sender == player or request.receiver == player then
			self.tradeRequests[dequestKey] = nil
		end
	end

	self.playerCooldowns[player.UserId] = nil
end

function TradingManager:IsPlayerOnCooldown(player)
	local cooldownEnd = self.playerCooldowns[player.UserId]
	return cooldownEnd and os.time() < cooldownEnd
end

function TradingManager:SetPlayerCooldown(player)
	self.playerCooldowns[player.UserId] = os.time() + TRADE_COOLDOWN
end

function TradingManager:SendTradeRequest(player, playerId)

	local targetPlayer = Players:GetPlayerByUserId(playerId)
	if not targetPlayer then
		for _, p in ipairs(Players:GetPlayers()) do
		end
		Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Player not found!", 3, "Error"}})
		return 
	end

	if targetPlayer == player then 
		Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Cannot trade with yourself!", 3, "Error"}})
		return 
	end

	if self:IsPlayerOnCooldown(player) then 
		Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"You are on trade cooldown!", 3, "Error"}})
		return 
	end

	local senderData = self.game.dataManager:GetPlayerData(player, true)
	if not senderData then 
		Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Failed to load your data!", 3, "Error"}})
		return 
	end

	local targetPlayerData = self.game.dataManager:GetPlayerData(targetPlayer, true)
	if not targetPlayerData then 
		Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Failed to load player data!", 3, "Error"}})
		return 
	end

	if self.activeTrades[player.UserId] then 
		Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"You are already in a trade!", 3, "Error"}})
		return 
	end

	if self.activeTrades[targetPlayer.UserId] then 
		Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {targetPlayer.Name.." is already in a trade!", 3, "Error"}})
		return 
	end
	
	if targetPlayerData.Settings then
	end

	if targetPlayerData.Settings and targetPlayerData.Settings.TradeInvites == false then
		Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"This player has trade invites disabled!", 3, "Error"}})
		return
	end
	
	Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Sent trade invite to "..targetPlayer.Name.."!", 3, "Success"}})

	local dequestKey = player.UserId .. "_" .. targetPlayer.UserId
	local reverseKey = targetPlayer.UserId .. "_" .. player.UserId

	if self.tradeRequests[dequestKey] or self.tradeRequests[reverseKey] then 
		return 
	end


	self.tradeRequests[dequestKey] = {
		sender = player,
		receiver = targetPlayer,
		timestamp = os.time()
	}

	Server.UI.Fire(targetPlayer, {
		UI = "Trade",
		Function = "DisplayTradeRequest",
		Arguments = {
			SenderName = player.Name,
			SenderId = player.UserId,
			DequestKey = dequestKey
		}
	})

	task.delay(15, function()
		self.tradeRequests[dequestKey] = nil
	end)
end

function TradingManager:RespondToTradeRequest(player, struct)
	local request = self.tradeRequests[struct.DequestKey]
	if not request or request.receiver ~= player then 
		return 
	end

	local sender = request.sender
	self.tradeRequests[struct.DequestKey] = nil

	if not struct.Accepted then 
		Server.UI.Fire(sender, {UI = "HUD", Function = "Notify", Arguments = {player.Name.." declined your trade request!", 3, "Error"}})
		return 
	end

	local senderData = self.game.dataManager:GetPlayerData(sender, true)
	local receiverData = self.game.dataManager:GetPlayerData(player, true)
	if not senderData or not receiverData then 
		return 
	end

	if self.activeTrades[sender.UserId] or self.activeTrades[player.UserId] then 
		return 
	end

	local tradeId = generateTradeId()
	local tradeData = {
		id = tradeId,
		player1 = sender,
		player2 = player,
		player1_units = {},
		player2_units = {},
		player1_confirmed = false,
		player2_confirmed = false,
		timestamp = os.time(),
		status = "active",
		executionTimer = nil,
		executionStartTime = nil
	}

	self.activeTrades[sender.UserId] = tradeData
	self.activeTrades[player.UserId] = tradeData

	Server.UI.Fire(sender, {
		UI = "Trade",
		Function = "StartTrade",
		Arguments = {
			TradeId = tradeId,
			PartnerName = player.Name,
			PartnerId = player.UserId
		}
	})

	Server.UI.Fire(player, {
		UI = "Trade",
		Function = "StartTrade",
		Arguments = {
			TradeId = tradeId,
			PartnerName = sender.Name,
			PartnerId = sender.UserId
		}
	})
end

function TradingManager:AddUnitsToTrade(player, struct)
	local trade = self.activeTrades[player.UserId]
	if not trade or trade.status ~= "active" then 
		return 
	end

	local playerData = self.game.dataManager:GetPlayerData(player, true)
	if not playerData then 
		return 
	end

	local isPlayer1 = trade.player1 == player
	local currentUnits = isPlayer1 and trade.player1_units or trade.player2_units

	local currentUnitCount = 0
	for _ in pairs(currentUnits) do
		currentUnitCount = currentUnitCount + 1
	end

	if currentUnitCount + #struct.UnitIds > MAX_TRADE_UNITS then 
		Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Cannot add more than "..MAX_TRADE_UNITS.." units to trade!", 3, "Error"}})
		return 
	end

	for _, unitId in ipairs(struct.UnitIds) do
		if not isValidUnit(unitId, playerData) then
			Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Invalid unit!", 3, "Error"}})
			return
		end
		currentUnits[unitId] = playerData.Inventory.Units[unitId]
	end

	trade.player1_confirmed = false
	trade.player2_confirmed = false

	if trade.executionTimer then
		task.cancel(trade.executionTimer)
		trade.executionTimer = nil
		trade.executionStartTime = nil
	end

	self:UpdateTradeDisplay(trade)
end

function TradingManager:RemoveUnitFromTrade(player, struct)
	local trade = self.activeTrades[player.UserId]
	if not trade or trade.status ~= "active" then return end

	local playerData = self.game.dataManager:GetPlayerData(player, true)
	if not playerData then return end

	local isPlayer1 = trade.player1 == player
	local currentUnits = isPlayer1 and trade.player1_units or trade.player2_units

	local unitId = struct.UnitId
	if not unitId or not currentUnits[unitId] then return end

	currentUnits[unitId] = nil
	trade.player1_confirmed = false
	trade.player2_confirmed = false

	if trade.executionTimer then
		task.cancel(trade.executionTimer)
		trade.executionTimer = nil
		trade.executionStartTime = nil
	end

	self:UpdateTradeDisplay(trade)
end

function TradingManager:ConfirmTrade(player, struct)
	local trade = self.activeTrades[player.UserId]
	if not trade or trade.status ~= "active" then return end

	local player1Data = self.game.dataManager:GetPlayerData(trade.player1, true)
	local player2Data = self.game.dataManager:GetPlayerData(trade.player2, true)
	if not player1Data or not player2Data then return end

	if not validateInventorySpaceAfterTrade(player1Data, trade.player2_units, trade.player1_units)
		or not validateInventorySpaceAfterTrade(player2Data, trade.player1_units, trade.player2_units) then
		Server.UI.Fire(trade.player1, {UI = "HUD", Function = "Notify", Arguments = {"Not enough inventory space to complete trade!", 3, "Error"}})
		Server.UI.Fire(trade.player2, {UI = "HUD", Function = "Notify", Arguments = {"Not enough inventory space to complete trade!", 3, "Error"}})
		return
	end

	local isPlayer1 = trade.player1 == player
	if isPlayer1 then
		trade.player1_confirmed = not trade.player1_confirmed
	else
		trade.player2_confirmed = not trade.player2_confirmed
	end

	if trade.executionTimer and ((isPlayer1 and not trade.player1_confirmed) or (not isPlayer1 and not trade.player2_confirmed)) then
		task.cancel(trade.executionTimer)
		trade.executionTimer = nil
		trade.executionStartTime = nil
	end

	if trade.player1_confirmed and trade.player2_confirmed and not trade.executionTimer then
		return self:StartTradeExecution(trade)
	end

	self:UpdateTradeDisplay(trade)
end

function TradingManager:StartTradeExecution(trade)
	trade.executionStartTime = os.time()
	trade.executionTimer = task.delay(TRADE_EXECUTION_DELAY, function()
		if trade.player1_confirmed and trade.player2_confirmed and trade.status == "active" then
			self:ExecuteTrade(trade)
		else
			trade.executionTimer = nil
			trade.executionStartTime = nil
		end
	end)
	self:UpdateTradeDisplay(trade)
end

function TradingManager:UpdateTradeDisplay(trade)
	local executionTimeRemaining = nil
	if trade.executionStartTime then
		executionTimeRemaining = TRADE_EXECUTION_DELAY - (os.time() - trade.executionStartTime)
	end

	Server.UI.Fire(trade.player1, {
		UI = "Trade",
		Function = "UpdateTrade",
		Arguments = {
			Player1 = trade.player1,
			Player2 = trade.player2,
			Player1Units = trade.player1_units,
			Player2Units = trade.player2_units,
			Player1Confirmed = trade.player1_confirmed,
			Player2Confirmed = trade.player2_confirmed,
			IsExecuting = trade.executionTimer ~= nil,
			ExecutionTimeRemaining = executionTimeRemaining
		}
	})

	Server.UI.Fire(trade.player2, {
		UI = "Trade",
		Function = "UpdateTrade",
		Arguments = {
			Player1 = trade.player1,
			Player2 = trade.player2,
			Player1Units = trade.player1_units,
			Player2Units = trade.player2_units,
			Player1Confirmed = trade.player1_confirmed,
			Player2Confirmed = trade.player2_confirmed,
			IsExecuting = trade.executionTimer ~= nil,
			ExecutionTimeRemaining = executionTimeRemaining
		}
	})
end

function TradingManager:ExecuteTrade(trade)
	local player1Data = self.game.dataManager:GetPlayerData(trade.player1, true)
	local player2Data = self.game.dataManager:GetPlayerData(trade.player2, true)
	if not player1Data or not player2Data then
		self:CancelTrade(trade.player1, {})
		return
	end

	for unitId in pairs(trade.player1_units) do
		if not isValidUnit(unitId, player1Data) then
			self:CancelTrade(trade.player1, {})
			return
		end
	end
	for unitId in pairs(trade.player2_units) do
		if not isValidUnit(unitId, player2Data) then
			self:CancelTrade(trade.player1, {})
			return
		end
	end

	for unitId in pairs(trade.player1_units) do
		player1Data.Inventory.Units[unitId] = nil
	end
	for unitId in pairs(trade.player2_units) do
		player2Data.Inventory.Units[unitId] = nil
	end

	for unitId in pairs(trade.player1_units) do
		for slot, equippedUnitId in pairs(player1Data.Equipped.Units) do
			if equippedUnitId == unitId then
				player1Data.Equipped.Units[slot] = nil
			end
		end
	end
	for unitId in pairs(trade.player2_units) do
		for slot, equippedUnitId in pairs(player2Data.Equipped.Units) do
			if equippedUnitId == unitId then
				player2Data.Equipped.Units[slot] = nil
			end
		end
	end

	for _, unitData in pairs(trade.player1_units) do
		player2Data.Inventory.Units[HttpService:GenerateGUID()] = unitData
	end
	for _, unitData in pairs(trade.player2_units) do
		player1Data.Inventory.Units[HttpService:GenerateGUID()] = unitData
	end

	self.game.dataManager:UpdateData(trade.player1, "Inventory.Units", player1Data.Inventory.Units)
	self.game.dataManager:UpdateData(trade.player2, "Inventory.Units", player2Data.Inventory.Units)

	self.game.dataManager:UpdateData(trade.player1, "Equipped.Units", player1Data.Equipped.Units)
	self.game.dataManager:UpdateData(trade.player2, "Equipped.Units", player2Data.Equipped.Units)

	self.game.dataManager:AddToData(trade.player1, "Trades", 1)
	self.game.dataManager:AddToData(trade.player2, "Trades", 1)

	self:SetPlayerCooldown(trade.player1)
	self:SetPlayerCooldown(trade.player2)
	Server.UI.Fire(trade.player1, {UI = "Trade", Function = "CompleteTrade", Arguments = {Success = true, PartnerName = trade.player2.Name}})
	Server.UI.Fire(trade.player2, {UI = "Trade", Function = "CompleteTrade", Arguments = {Success = true, PartnerName = trade.player1.Name}})
	if trade.executionTimer then
		task.cancel(trade.executionTimer)
		trade.executionTimer = nil
	end
	self.activeTrades[trade.player1.UserId] = nil
	self.activeTrades[trade.player2.UserId] = nil
end

function TradingManager:CancelTrade(player, struct)
	local trade = self.activeTrades[player.UserId]
	if not trade then return end

	if trade.executionTimer then
		task.cancel(trade.executionTimer)
		trade.executionTimer = nil
	end

	Server.UI.Fire(trade.player1, {UI = "Trade", Function = "CancelTrade", Arguments = {CancelledBy = player.Name}})
	Server.UI.Fire(trade.player2, {UI = "Trade", Function = "CancelTrade", Arguments = {CancelledBy = player.Name}})

	self.activeTrades[trade.player1.UserId] = nil
	self.activeTrades[trade.player2.UserId] = nil
end

function TradingManager:CleanupExpiredTrades()
	local currentTime = os.time()
	for playerId, trade in pairs(self.activeTrades) do
		if currentTime - trade.timestamp > TRADE_TIMEOUT then
			if trade.executionTimer then
				task.cancel(trade.executionTimer)
				trade.executionTimer = nil
			end
			Server.UI.Fire(trade.player1, {UI = "Trade", Function = "CancelTrade", Arguments = {CancelledBy = "System (Timeout)"}})
			Server.UI.Fire(trade.player2, {UI = "Trade", Function = "CancelTrade", Arguments = {CancelledBy = "System (Timeout)"}})
			self.activeTrades[trade.player1.UserId] = nil
			self.activeTrades[trade.player2.UserId] = nil
		end
	end
	for dequestKey, request in pairs(self.tradeRequests) do
		if currentTime - request.timestamp > 30 then
			self.tradeRequests[dequestKey] = nil
		end
	end
end

return TradingManager