-- TowerIdeas

--Mayhem, Rage, Violence, Malice, Malevolence -- 0 Stars (Starter Tower) 

--Chaos, Destruction, Havoc, Depredation, Despoliation -- 2000 Stars

-- Purity, Holy, Angelic, Heavenly, Godlike -- 1500 Stars
-- Corruption, Vanta-X, Dark, Black, VOID -- 1500 Stars

-- MobIdeas

-- ORB (7500 HP)

-- UnitsIdeas



-- ShopServer

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local DataStoreService = game:GetService("DataStoreService")
local MarketplaceService = game:GetService("MarketplaceService")

local DataStore = DataStoreService:GetDataStore("fanum")

local towers = require(ReplicatedStorage:WaitForChild("TowerShop"))

local UpdateInventoryEvent = ReplicatedStorage:WaitForChild("UpdateInventory")

local maxSelectedTowers = 5

local data = {}

local function LoadData(player)
	local success = nil
	local playerData = nil
	local attempt = 1
	local leaderstats = Instance.new("Folder", player)
	leaderstats.Name = "leaderstats"

	repeat
		success, playerData = pcall(function()
			return DataStore:GetAsync(player.UserId)
		end)

		attempt += 1

		if not success then
			warn(playerData)
			task.wait()
		end
	until success or attempt == 5

	if success then
		if not playerData then
			playerData = {
				["Stars"] = 0,
				["Level"] = 1,
				["Experience"] = 0,
				["RequiredExperience"] = 10,
				["SelectedTowers"] = {"Mayhem"},
				["OwnedTowers"] = {"Mayhem", "Sword Novice"},
				["OwnedSkins"] = {},
				["EquippedSkins"] = {},
				["SkinsHave"] = {},
				["KilledEnemies"] = {},
			}
		end

        local function GamepassCheck(GamepassID, TowerName)
            local UserID = player.UserId

            if MarketplaceService:UserOwnsGamePassAsync(UserID, GamepassID) then
                if not table.find(playerData.OwnedTowers, TowerName) then
                    table.insert(playerData.OwnedTowers, TowerName)
                else
                    return print(TowerName)
                end
            end
        end
        
		if not playerData.KilledEnemies then
			playerData.KilledEnemies = {}
		end
		
		data[player.UserId] = playerData
		
		local Stars = Instance.new("NumberValue", leaderstats)
		Stars.Name = "Stars"
		Stars.Value = playerData.Stars
		
		local Level = Instance.new("NumberValue", leaderstats)
		Level.Name = "Level"
		Level.Value = playerData.Level
		
		print(playerData.OwnedTowers)

		while wait() do
			if playerData.Level and playerData.Experience and playerData.RequiredExperience then
				if playerData.Experience >= playerData.RequiredExperience then
					playerData.Experience -= playerData.RequiredExperience
					playerData.Level += 1
					playerData.RequiredExperience = playerData.Level * 10
				end
			else
				wait(playerData.Level and playerData.Experience and playerData.RequiredExperience)
				
				if playerData.Experience >= playerData.RequiredExperience then
					playerData.Experience -= playerData.RequiredExperience
					playerData.Level += 1
					playerData.RequiredExperience = playerData.Level * 10
				end
			end
		end
		
		data[player.UserId].Stars:Changed(function()
			player.leaderstats.Stars.Value = data[player.UserId].Stars
		end)
		
		data[player.UserId].Level:Changed(function()
			player.leaderstats.Level.Value = data[player.UserId].Level
		end)
	else
		player:Kick("There was a problem getting your data, try again.")
	end
end

Players.PlayerAdded:Connect(LoadData)

local function SaveData(player)
	if data[player.UserId] then
		local success = nil
		local playerData = nil
		local attempt = 1

		repeat
			success, playerData = pcall(function()
				return DataStore:UpdateAsync(player.UserId, function()
					return data[player.UserId]
				end)
			end)

			attempt += 1

			if not success then
				warn(playerData)
				task.wait()
			end
		until success or attempt == 5

		if success then
			print("Data saved successfully!")
		else
			warn("Unable to save data for", player.UserId)
		end
	else
		warn("No session data for", player.UserId)
	end
end

Players.PlayerRemoving:Connect(function(player)
	SaveData(player)
	data[player.UserId] = nil
end)

game:BindToClose(function()
	if not RunService:IsStudio() then
		for index, player in pairs(Players:GetPlayers()) do
			task.spawn(function()
				SaveData(player)
			end)
		end
	else
		print("Shutting down inside studio.")
	end

end)

local function getItemStatus(player, itemName)
	local playerData = data[player.UserId]

	if table.find(playerData.SelectedTowers, itemName) then
		return "Equipped"
	elseif table.find(playerData.OwnedTowers, itemName) then
		return "Owned"
	else
		return "Locked"
	end
end

ReplicatedStorage.InteractItem.OnServerInvoke = function(player, itemName)
	
	ReplicatedStorage.UpdateInventory:FireClient(player)
	
	local shopItem = towers[itemName]
	local playerData = data[player.UserId]

	if shopItem and playerData then
		local status = getItemStatus(player, itemName)

		if status == "Locked" and shopItem.Price <= playerData.Stars then

			playerData.Stars -= shopItem.Price 
			table.insert(playerData.OwnedTowers, shopItem.Name)

		elseif status == "Owned" then

			table.insert(playerData.SelectedTowers, shopItem.Name)
			if #playerData.SelectedTowers > maxSelectedTowers then
				table.remove(playerData.SelectedTowers, 1)
			end

		elseif status == "Equipped" then

			if #playerData.SelectedTowers > 0 then
				local towerToRemove = table.find(playerData.SelectedTowers, itemName)
				table.remove(playerData.SelectedTowers, towerToRemove)
			end

		end

		return playerData

	else
		warn("Tower/player data does not exist")
	end

	return false
end

ReplicatedStorage.InteractItemInventory.OnServerInvoke = function(player, itemName)
	
	ReplicatedStorage.UpdateInventory:FireClient(player)
	
	local shopItem = towers[itemName]
	local playerData = data[player.UserId]

	if shopItem and playerData then
		local status = getItemStatus(player, itemName)

		if status == "Locked" and shopItem.Price <= playerData.Stars then

			playerData.Stars -= shopItem.Price 
			table.insert(playerData.OwnedTowers, shopItem.Name)

		elseif status == "Owned" then

			table.insert(playerData.SelectedTowers, shopItem.Name)
			if #playerData.SelectedTowers > maxSelectedTowers then
				table.remove(playerData.SelectedTowers, 1)
			end
			
		elseif status == "Equipped" then
			
			if #playerData.SelectedTowers > 0 then
				local towerToRemove = table.find(playerData.SelectedTowers, itemName)
				table.remove(playerData.SelectedTowers, towerToRemove)
			end
		end

		return playerData

	else
		warn("Tower/player data does not exist")
	end

	return false
end

ReplicatedStorage.GetData.OnServerEvent = function(player)
	return data[player.UserId]
end

ReplicatedStorage.LevelData.GetLevelData.OnInvoke = function(player)
	return data[player.UserId]
end

--ShopClient

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local towers = require(ReplicatedStorage:WaitForChild("TowerShop"))

local getDataFunction = ReplicatedStorage:WaitForChild("GetData")
local interactItemFunction = ReplicatedStorage:WaitForChild("InteractItem")

local gui = script.Parent

local Container = gui.Container

local exit = Container.Exit
local stars = Container.Stars
local limit = Container.Limit
local itemsFrame = Container.ItemsFrame
local itemBuyTFrame = Container.ItemBuyTower

local playerData = {}

local function getItemStatus(itemName)
	if table.find(playerData.SelectedTowers, itemName) then
		return "Equipped"
	elseif table.find(playerData.OwnedTowers, itemName) then
		return "Owned"
	else
		return "Locked"
	end
end

local function interactItem(itemName)
	local data = interactItemFunction:InvokeServer(itemName)
	if data then
		playerData = data
		updateItems()
		print(playerData.SelectedTowers)
	end
end

function updateItems()
	stars.Text = "★" .. playerData.Stars
	limit.Text = #playerData.SelectedTowers .. "/5"
	
	for i, tower in pairs(towers) do
		
		local oldButton = itemsFrame:FindFirstChild(tower.Name)
		if oldButton then
			oldButton:Destroy()
		end
		
		local oldPldButton = itemBuyTFrame:GetChildren()
		
		for x, o in pairs(oldPldButton) do
			if not o:IsA("UICorner") or not o:IsA("UIAspectRatioConstraint") then
				if o:IsA("TextButton") and o.Visible == true then
					o:Destroy()
				end
			end
		end
		
		local newButton = itemsFrame.Template:Clone()
		newButton.Name = tower.Name
		newButton.TowerName.Text = tower.Name
		newButton.Visible = true
		newButton.Parent = itemsFrame

        if tower.Level > playerData.Level then
            newButton.Locked.Visible = true
            newButton.Locked.Level.Text = "Lvl. " .. tower.Level
        else
            newButton.Locked.Visible = false
        end
		
		local RSTower = ReplicatedStorage:WaitForChild("Towers"):FindFirstChild(tower.Name):Clone()
		
		RSTower.Parent = newButton.ViewportFrame.WorldModel
		RSTower.PrimaryPart.CFrame = CFrame.new(0, 0, -3) * CFrame.Angles(0 , math.rad(-180), 0)
		
		local Animations = RSTower:WaitForChild("Animations")
		local Animator = RSTower:FindFirstChild("Humanoid"):FindFirstChildOfClass("Animator") or Instance.new("Animator", RSTower:FindFirstChild("Humanoid"))
		local IdleAnimationTrack = Animator:LoadAnimation(Animations.Idle)
		local WingAnimationTrack = Animator:LoadAnimation(Animations.WingAnim)

		if IdleAnimationTrack then
			IdleAnimationTrack:Play()
		end

		if WingAnimationTrack then
			WingAnimationTrack:Play()
		end
		
		newButton.Activated:Connect(function()
            if tower.Level > playerData.Level then
                return
            end
            
			itemBuyTFrame.Visible = true
			
			local oldPlButton = itemBuyTFrame:FindFirstChild(tower.Name)
			if oldPlButton then
				oldPlButton:Destroy()
			end

			local oldPldButton = itemBuyTFrame:GetChildren()

			for x, o in pairs(oldPldButton) do
				if not o:IsA("UICorner") or not o:IsA("UIAspectRatioConstraint") then
					if o:IsA("TextButton") and o.Visible == true then
						o:Destroy()
					end
				end
			end
			
			local PldButton = itemBuyTFrame.Template:Clone()
			PldButton.Name = tower.Name
			PldButton.TowerName.Text = tower.Name
			PldButton.Desc.Text = tower.Description
			PldButton.LayoutOrder = tower.Price
			PldButton.Visible = true
			PldButton.Parent = itemBuyTFrame
			
			local RSTower2 = ReplicatedStorage:WaitForChild("Towers"):FindFirstChild(tower.Name):Clone()

			RSTower2.Parent = PldButton.ViewportFrame.WorldModel
			RSTower2.PrimaryPart.CFrame = CFrame.new(0, 0, -3) * CFrame.Angles(0 , math.rad(-180), 0)
			
			local Animations = RSTower2:WaitForChild("Animations")
			local Animator = RSTower2:FindFirstChild("Humanoid"):FindFirstChildOfClass("Animator") or Instance.new("Animator", RSTower2:FindFirstChild("Humanoid"))
			local IdleAnimationTrack = Animator:LoadAnimation(Animations.Idle)
			local WingAnimationTrack = Animator:LoadAnimation(Animations.WingAnim)

			if IdleAnimationTrack then
				IdleAnimationTrack:Play()
			end

			if WingAnimationTrack then
				WingAnimationTrack:Play()
			end
			
			local status = getItemStatus(tower.Name)
			
			print(status)

			if status == "Locked" then
				PldButton.Status.Cost.Text = "Stars ★: " .. tower.Price
				PldButton.Status.BackgroundColor3 = Color3.new(0.9, 0, 0)
			elseif status == "Equipped" then
				PldButton.Status.Cost.Text = "Unequip"
				PldButton.Status.BackgroundColor3 = Color3.new(0, 0.5, 0)
			elseif status == "Owned" then
				PldButton.Status.Cost.Text = "Equip"
				PldButton.Status.BackgroundColor3 = Color3.new(0, 0.6666667, 1)
			end

			PldButton.Status.Activated:Connect(function()
				interactItem(tower.Name)
			end)
		end)
	end
end

local function TweenShopOut()
	Container:TweenSizeAndPosition(UDim2.new(0.8, 0, 0.8, 0), UDim2.new(0.5, 0, 0.5, 0), Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 1)
end

local function TweenShopIn()
	Container:TweenSizeAndPosition(UDim2.new(0, 0, 0, 0), UDim2.new(0, 0, 0, 0), Enum.EasingDirection.In, Enum.EasingStyle.Quad, 1)
end

local function toggleShop()
	Container.Visible = not Container.Visible
	
	if Container.Visible then
		itemBuyTFrame.Visible = false
		playerData = getDataFunction:InvokeServer()
		updateItems()
	end
end

local function setupShop()
	local prompty = Instance.new("ProximityPrompt", workspace.Map.Shop:WaitForChild("ShopPart"))
	prompty.RequiresLineOfSight = false
	prompty.ActionText = "Shop"
	
	prompty.Triggered:Connect(toggleShop)
	exit.Activated:Connect(toggleShop)
	gui.Buttons.Shop.Activated:Connect(toggleShop)
end

setupShop()

--LoadoutClient

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local getDataFunction = ReplicatedStorage:WaitForChild("GetData")

local UpdateInventoryEvent = ReplicatedStorage:WaitForChild("UpdateInventory")

local gui = script.Parent

local function updateLoadout()
	
	for i, button in ipairs(gui.Towers:GetDescendants()) do
		if button:IsA("ImageButton") and button.Name ~= "Template" then
			button:Destroy()
		end
	end
	
	local playerData
	
	repeat 
		playerData = getDataFunction:InvokeServer()
	until playerData ~= nil
	
	gui.Towers.Limit.Text = "Towers: " .. #playerData.SelectedTowers .. " / 5"
	
	for i, t in pairs(playerData.SelectedTowers) do
		
		local function Comma(Amount)
			while true do
				local digit
				Amount, digit = string.gsub(Amount, "(-?%d+)(%d%d%d)", "%1,%2")
				if (digit == 0) then
					break
				end
			end
			return Amount
		end
		
		local Towers = ReplicatedStorage:WaitForChild("Towers")
		local Tower = Towers:FindFirstChild(t)
		
		if Tower then
			local button = gui.Towers.Template:Clone()
			local config = Tower:WaitForChild("Config")
			
			local ClonedTower = Tower:Clone()
			
			button.Name = Tower.Name
			button.TowerName.Text = Tower.Name
			button.TowerName.TextColor3 = ClonedTower.Ring.Color
			button.Visible = true
			button.LayoutOrder = i
			button.KeybindText.Text = i
			button.Parent = gui.Towers
			button.Cost.Text = "$" .. Comma(config.Price.Value)
			
			ClonedTower.Parent = button.ViewportFrame.WorldModel
			ClonedTower.PrimaryPart.CFrame = CFrame.new(0, 0, -3) * CFrame.Angles(0 , math.rad(-180), 0)
			
			local Animations = ClonedTower:WaitForChild("Animations")
			local Animator = ClonedTower:FindFirstChild("Humanoid"):FindFirstChildOfClass("Animator") or Instance.new("Animator", ClonedTower:FindFirstChild("Humanoid"))
			local IdleAnimationTrack = Animator:LoadAnimation(Animations.Idle)
			local WingAnimationTrack = Animator:LoadAnimation(Animations.WingAnim)
			
			if IdleAnimationTrack then
				IdleAnimationTrack:Play()
			end
			
			if WingAnimationTrack then
				WingAnimationTrack:Play()
			end
		end
	end
end

UpdateInventoryEvent.OnClientEvent:Connect(function()
	updateLoadout()
end)

local playerData = getDataFunction:InvokeServer()

if playerData then
	updateLoadout()
else
	warn("No player data yet.")
	repeat
		task.wait()
		local playerData = getDataFunction:InvokeServer()
	until playerData
	
	print("Successfully retrieved player data.")
	updateLoadout()
end

--ButtonClient

local gui = script.Parent

local buttons = gui.Buttons
local InventoryFrame = gui.Inventory

local TopFrame = InventoryFrame.Top

local InventoryButton = buttons.Inventory

TopFrame.Exit.Activated:Connect(function()
	InventoryFrame.Visible = false
end)


--InventoryClient

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local getDataFunction = ReplicatedStorage:WaitForChild("GetData")
local interactItemFunction = ReplicatedStorage:WaitForChild("InteractItem")

local gui = script.Parent

local InventoryFrame = gui.Inventory
local buttons = gui.Buttons

local stars = InventoryFrame.Stars
local Level = InventoryFrame.Level
local ExperienceBar = InventoryFrame.ExperienceBar

local ReqExp = ExperienceBar.RequiredExperience
local CurrExp = ExperienceBar.CurrentExperience

local InventoryButton = buttons.Inventory

local Player = game.Players.LocalPlayer

local playerData = getDataFunction:InvokeServer()

local function getItemStatus(itemName)
	if table.find(playerData.SelectedTowers, itemName) then
		return "Equipped"
	elseif table.find(playerData.OwnedTowers, itemName) then
		return "Owned"
	else
		return "Locked"
	end
end

local function interactItem(itemName)
	local data = interactItemFunction:InvokeServer(itemName)
	if data then
		playerData = data
		updateInventory()
		print(playerData.SelectedTowers)
	end
end

function updateInventory()
	if playerData ~= nil then
		stars.Text = "★" .. playerData.Stars
		Level.Text = playerData.Level
		CurrExp.Text = playerData.Experience
		ReqExp.Text = playerData.RequiredExperience
		
		local percentage = playerData.Experience / playerData.RequiredExperience
		
		ExperienceBar.Experience.Size = UDim2.new(percentage, 0, 1, 0)

		for i, towerName in pairs(playerData.OwnedTowers) do
			local tower = ReplicatedStorage.Towers:FindFirstChild(towerName)

			if tower then
				local status = getItemStatus(tower.Name)

				local oldPlButton = InventoryFrame.Towers:FindFirstChild(tower.Name)
				if oldPlButton then
					oldPlButton:Destroy()
				end

			--[[local oldPldButton = InventoryFrame.Towers:GetChildren()

			for x, o in pairs(oldPldButton) do
				if not o:IsA("UICorner") or not o:IsA("UIAspectRatioConstraint") then
					if o:IsA("TextButton") and o.Visible == true then
						o:Destroy()
					end
				end
			end]]

				local button = InventoryFrame.Towers.Template:Clone()
				
				local ClonedTower = tower:Clone()
				
				ClonedTower.Parent = button.ViewportFrame.WorldModel
				ClonedTower.PrimaryPart.CFrame = CFrame.new(0, 0, -3) * CFrame.Angles(0 , math.rad(-180), 0)

				button.Visible = true
				button.Name = tower.Name
				button.TowerName.Text = tower.Name
				button.LayoutOrder = i
				button.Parent = InventoryFrame.Towers

				if status == "Equipped" then
					button.Status.Text = "Unequip"
					button.BackgroundColor3 = Color3.new(0, 0.5, 0)
				elseif status == "Owned" then
					button.Status.Text = "Equip"
					button.BackgroundColor3 = Color3.new(0, 0.6666667, 1)
				end
				
				local Animations = ClonedTower:WaitForChild("Animations")
				local Animator = ClonedTower:FindFirstChild("Humanoid"):FindFirstChildOfClass("Animator") or Instance.new("Animator", ClonedTower:FindFirstChild("Humanoid"))
				local IdleAnimationTrack = Animator:LoadAnimation(Animations.Idle)
				local WingAnimationTrack = Animator:LoadAnimation(Animations.WingAnim)

				if IdleAnimationTrack then
					IdleAnimationTrack:Play()
				end

				if WingAnimationTrack then
					WingAnimationTrack:Play()
				end

				button.Activated:Connect(function()
					interactItem(towerName)
				end)
			end
		end
	else
		repeat
			task.wait(1)
			playerData = getDataFunction:InvokeServer()
		until playerData ~= nil

		updateInventory()
	end
end

updateInventory()

local function toggleShop()
	InventoryFrame.Visible = not InventoryFrame.Visible

	if InventoryFrame.Visible then
		updateInventory()
	end
end

InventoryButton.Activated:Connect(function()
	toggleShop()
end)

--TowersModule

local PhysicsService = game:GetService("PhysicsService")
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local events = ReplicatedStorage:WaitForChild("Events")
local towers = ReplicatedStorage:WaitForChild("Towers")
local animateTowerEvent = events:WaitForChild("AnimateTower")
local functions = ReplicatedStorage:WaitForChild("Functions")

local spawnTowerFunction = functions:WaitForChild("SpawnTower")
local requestTowerFunction = functions:WaitForChild("RequestTower")
local sellTowerFunction = functions:WaitForChild("SellTower")
local changeTowerModeFunction = functions:WaitForChild("ChangeTowerMode")

local maxTowers = 30

local tower = {}

function tower.FindTarget(newTower, range, mode)
	local bestTarget = nil

	local bestWaypoint = nil
	local bestDistance = nil
	local bestHealth = nil

	local map = workspace.Maps:FindFirstChildOfClass("Folder")

	for i, mob in ipairs(workspace.Mobs:GetChildren()) do
		local distanceToMob = (mob.HumanoidRootPart.Position - newTower.HumanoidRootPart.Position).Magnitude
		local distanceToWaypoint = (mob.HumanoidRootPart.Position - map.Waypoints[mob.MovingTo.Value].Position).Magnitude

		if distanceToMob <= range then
			if mode == "Near" then
				range = distanceToMob
				bestTarget = mob
			elseif mode == "First" then
				if not bestWaypoint or mob.MovingTo.Value >= bestWaypoint then
					bestWaypoint = mob.MovingTo.Value

					if not bestDistance or distanceToWaypoint < bestDistance then
						bestDistance = distanceToWaypoint
						bestTarget = mob
					end
				end
			elseif mode == "Last" then
				if not bestWaypoint or mob.MovingTo.Value <= bestWaypoint then
					bestWaypoint = mob.MovingTo.Value

					if not bestDistance or distanceToWaypoint > bestDistance then
						bestDistance = distanceToWaypoint
						bestTarget = mob
					end
				end
			elseif mode == "Strong" then
				if not bestHealth or mob.Humanoid.Health > bestHealth then
					bestHealth = mob.Humanoid.Health
					bestTarget = mob
				end
			elseif mode == "Weak" then
				if not bestHealth or mob.Humanoid.Health < bestHealth then
					bestHealth = mob.Humanoid.Health
					bestTarget = mob
				end
			end
		end
	end

	return bestTarget
end

function tower.Attack(newTower, player)
	local towerConfig = newTower.Config
	local target = tower.FindTarget(newTower, towerConfig.Range.Value, towerConfig.TargetMode.Value)

	local Damage = towerConfig.Damage
	local Cooldown = towerConfig.Cooldown

	if target and target:FindFirstChild("Humanoid") and target.Humanoid.Health > 0 then

		if target.Humanoid.Health < Damage.Value then
			player.Stardust.Value += target.Humanoid.Health
		else
			if target:FindFirstChild("Armor") then
				player.Stardust.Value = player.Stardust.Value + (Damage.Value - (Damage.Value * target:FindFirstChild("Armor").DamageReduction.Value))
			else
				player.Stardust.Value += Damage.Value
			end
		end

		local targetCFrame = CFrame.lookAt(newTower.HumanoidRootPart.Position, target.HumanoidRootPart.Position)

		newTower.HumanoidRootPart.BodyGyro.CFrame = targetCFrame
		animateTowerEvent:FireAllClients(newTower, "Attack", target)

		if target:FindFirstChild("Armor") then
			target.Humanoid:TakeDamage(Damage.Value - (Damage.Value * target:FindFirstChild("Armor").DamageReduction.Value))
		else
			target.Humanoid:TakeDamage(Damage.Value)
		end


		if target.Humanoid.Health <= 0 then
			player.Kills.Value += 1
		end

		task.wait(Cooldown.Value)
	end

	task.wait(1/60)

	if newTower and newTower.Parent then
		tower.Attack(newTower, player)
	end
end

function tower.ChangeMode(player, model)
	if model and model:FindFirstChild("Config") then
		local targetMode = model.Config.TargetMode
		local modes = {"First", "Last", "Near", "Strong", "Weak"}
		local modeIndex = table.find(modes, targetMode.Value)

		if modeIndex < #modes then
			targetMode.Value = modes[modeIndex + 1]
		else
			targetMode.Value = modes[1]
		end

		return true
	else
		warn("Unable to change tower mode")
		return false
	end
end

changeTowerModeFunction.OnServerInvoke = tower.ChangeMode

function tower.Sell(player, model)
	if model and model:FindFirstChild("Config") then
		if model.Config.Owner.Value == player.Name then
			player.PlacedTowers.Value -= 1
			player.Stardust.Value += model.Config.Price.Value / 2
			model:Destroy()
			return true
		end
	end

	warn("Unable to sell this tower")
	return false
end

sellTowerFunction.OnServerInvoke = tower.Sell

function tower.Spawn(player, name, cframe, previous)
	local allowedToSpawn = tower.CheckSpawn(player, name, previous)

	if allowedToSpawn then

		local newTower
		local oldMode = nil

		if previous then
			oldMode = previous.Config.TargetMode.Value
			previous:Destroy()
			newTower = towers.Upgrades[name]:Clone()

            towers.Upgrades[name]:FindFirstChild("Config"):FindFirstChild("InGame").Value = true
		else
			newTower = towers[name]:Clone()
			player.PlacedTowers.Value += 1
            
            towers[name]:FindFirstChild("Config"):FindFirstChild("InGame").Value = true
		end

        if not workspace.Towers[towers[name]] then
            towers[name]:FindFirstChild("Config"):FindFirstChild("InGame").Value = false
        elseif not workspace.Towers[towers.Upgrades[name]] then
            towers.Upgrades[name]:FindFirstChild("Config"):FindFirstChild("InGame").Value = false
        end

		local ownerValue = Instance.new("StringValue", newTower.Config)
		ownerValue.Name = "Owner"
		ownerValue.Value = player.Name

		local targetMode = Instance.new("StringValue", newTower.Config)
		targetMode.Name = "TargetMode"
		targetMode.Value = oldMode or "First"

		newTower.HumanoidRootPart.CFrame = cframe
		newTower.Parent = workspace.Towers
		newTower.HumanoidRootPart:SetNetworkOwner(nil)

		local bodyGyro = Instance.new("BodyGyro", newTower.HumanoidRootPart)
		bodyGyro.MaxTorque = Vector3.new(math.huge, math.huge, math.huge)
		bodyGyro.D = 0
		bodyGyro.CFrame = newTower.HumanoidRootPart.CFrame

		for i, object in ipairs(newTower:GetDescendants()) do
			if object:IsA("BasePart") then
				PhysicsService:SetPartCollisionGroup(object, "Towers")
			end
		end

		local height = (newTower.PrimaryPart.Size.Y / 2) + newTower["Left Leg"].Size.Y
		local offset = Vector3.new(0, -height, 0)

		if newTower.Config:FindFirstChild("Boundary") then
			local p = Instance.new("Part", newTower)
			p.Name = "Boundary"
			p.Shape = Enum.PartType.Cylinder
			p.Transparency = 0.6
			p.BrickColor = BrickColor.new("Persimmon")
			p.Size = Vector3.new(0.5, newTower.Config.Boundary.Value, newTower.Config.Boundary.Value)
			p.Material = Enum.Material.Neon
			p.TopSurface = Enum.SurfaceType.Smooth
			p.BottomSurface = Enum.SurfaceType.Smooth
			p.Position = newTower.PrimaryPart.Position + offset
			p.Orientation = Vector3.new(0, 90, 90)
			p.CanCollide = false
			p.CanQuery = true
			p.Anchored = true
		end

		player.Stardust.Value -= newTower.Config.Price.Value

		coroutine.wrap(tower.Attack)(newTower, player)

		return newTower
	else
		warn("Requested tower does not exist:", name)

		return false
	end
end

spawnTowerFunction.OnServerInvoke = tower.Spawn

function tower.CheckSpawn(player, name, towerToSpawn, previous)
	local towerExists = ReplicatedStorage.Towers:FindFirstChild(name, true)

	if towerExists then
		if towerExists.Config.Price.Value <= player.Stardust.Value then
			if previous and towerToSpawn then
                if player.PlacedTowers.Value < maxTowers then
                    return true
                else
                    warn("Player has reached max limit")
                end
			else
                warn("No tower selected")
			end
		else
			warn("Player cannot afford")
		end
	else
		warn("That tower doesn't exist")
	end

	return false
end

requestTowerFunction.OnServerInvoke = tower.CheckSpawn

return tower

--MobModule

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")
local PhysicsService = game:GetService("PhysicsService")
local TweenService = game:GetService("TweenService")

local functions = ReplicatedStorage:WaitForChild("Functions")
local modules = ReplicatedStorage:WaitForChild("Modules")

local getDataFunction = functions:WaitForChild("GetData")

local bindables = ServerStorage:WaitForChild("Bindables")
local updateBaseHealthEvent = bindables:WaitForChild("UpdateBaseHealth")

local Interpolation = require(modules.Interpolation)

local mob = {}
local playerData = {}

function mob.Optimize(mobToOptimize)
	local humanoid = mobToOptimize:WaitForChild("Humanoid")

	if mobToOptimize:FindFirstChild("HumanoidRootPart") then
		mobToOptimize.HumanoidRootPart:SetNetworkOwner(nil)
	elseif mobToOptimize.PrimaryPart ~= nil then
		mobToOptimize.PrimaryPart:SetNetworkOwner(nil)	
	end

	if not humanoid then
		return
	end

	humanoid:SetStateEnabled(Enum.HumanoidStateType.Seated, false)
	humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)
	humanoid:SetStateEnabled(Enum.HumanoidStateType.Running, false)
	humanoid:SetStateEnabled(Enum.HumanoidStateType.GettingUp, false)
	humanoid:SetStateEnabled(Enum.HumanoidStateType.Climbing, false)
	humanoid:SetStateEnabled(Enum.HumanoidStateType.Landed, false)
	humanoid:SetStateEnabled(Enum.HumanoidStateType.Ragdoll, false)
	humanoid:SetStateEnabled(Enum.HumanoidStateType.Freefall, false)
end

function mob.LerpTo(newEnemy, target, Offset)
	local alpha = 0
	local startPos = newEnemy.PrimaryPart.Position
	local loop = nil
	local reachedTarget = Instance.new("BindableEvent")
	local distance = (newEnemy.PrimaryPart.Position - target).Magnitude

	loop = RunService.Heartbeat:Connect(function(delta)
		if newEnemy and newEnemy:FindFirstChild("Humanoid") then

			local speed = newEnemy.Humanoid.WalkSpeed
			local relativeSpeed = distance / speed

			local goalPos = startPos:Lerp(target, alpha)
			TweenService:Create(newEnemy.PrimaryPart, TweenInfo.new(0.75, Enum.EasingStyle.Sine), {CFrame = CFrame.lookAt(newEnemy.PrimaryPart.Position, Vector3.new(target.X, target.Y, target.Z))}):Play()
			newEnemy.PrimaryPart.Position = goalPos

			alpha += delta / relativeSpeed

			if alpha >= 1 then
				loop:Disconnect()
				reachedTarget:Fire()
			end
		else
			loop:Disconnect()
		end
	end)

	reachedTarget.Event:Wait()

end

function mob.Move(mobToMove, map, old)
	local humanoid = mobToMove:WaitForChild("Humanoid")
	local waypoints = map.Waypoints
	local waypoint = 1

	if old then
		waypoint = old.MovingTo.Value
	end

	for wp = waypoint, #waypoints:GetChildren() do
		if mobToMove.PrimaryPart == nil or mobToMove == nil then
			return
		end
		
		local offset = Vector3.new(math.random(-100,100)/100, 0, math.random(-100,100)/100)
		
		mobToMove.MovingTo.Value = wp

		local mobPosVector = mobToMove.PrimaryPart.Position * Vector3.new(1, 0, 1)
		local waypointPosVector = waypoints[wp].Position * Vector3.new(1, 0, 1)
		local mobDistance = (mobPosVector - waypointPosVector).Magnitude
		local waypointVector = Vector3.new(waypoints[wp].Position.X, waypoints[wp].Position.Y, waypoints[wp].Position.Z)
		
		local reached = false

		repeat task.wait(0.05)
			if not humanoid or humanoid.Health <= 0 then
				return
			end

			humanoid:MoveTo(waypointPosVector + offset)
			mobPosVector = mobToMove.PrimaryPart.Position * Vector3.new(1, 0, 1)
			waypointPosVector = waypoints[wp].CFrame.Position * Vector3.new(1, 0, 1)
			mobDistance = (mobPosVector - waypointPosVector).Magnitude

			if mobDistance <= 0.7 then
				reached = true
			end
		until humanoid.MoveToFinished:Wait()
	end

	mobToMove:Destroy()

	map.Base.Humanoid:TakeDamage(humanoid.Health)
end

--[[function mob.Move(newMob, map, Offset, RandomPos)
	local Root = newMob.PrimaryPart
	local Waypoints = map.Waypoints
	local newPos
	
	for waypoint = 1, #Waypoints:GetChildren() do
		newMob.MovingTo.Value = waypoint
		if waypoint == 1 then
		else
			if Waypoints[waypoint]:FindFirstChild("A") and Waypoints[waypoint]:FindFirstChild("B") then
				for t = 0, 1, 0.1 do
					local Waypoint1 = Waypoints[waypoint].Position
					local Waypoint2 = Waypoints[waypoint].A.Position
					local Waypoint3 = Waypoints[waypoint].B.Position
					newPos = Interpolation.QuadBezier(Waypoint1, Waypoint2, Waypoint3, t)
					mob.LerpTo(newMob, Vector3.new(newPos.X + RandomPos, newPos.Y + Offset, newPos.Z + RandomPos))
				end
			else
				newPos = Waypoints[waypoint].Position
				mob.LerpTo(newMob, Vector3.new(newPos.X + RandomPos, newPos.Y + Offset, newPos.Z + RandomPos))
			end
		end
	end
	newMob:Destroy()
end]]

function mob.Mystery(model, map)
	local mysteryMobs = nil

	if workspace.Info.Gamemode.Value == "Normal" then
		mysteryMobs = {
			["Mystery"] = "Zombie", "Speedy Zombie", "Slow Zombie"
		}
	end

	if mysteryMobs then
		if mysteryMobs[model.Name] then
			local mysteryMob = mysteryMobs[mysteryMobs]
			local rng = math.random(1, #mysteryMobs)

			mob.Spawn(mysteryMobs[rng], 1, 0, map, model)
		end
	end
end

function mob.Spawn(name, quantity, waittime, map, old, status)
	if not status then
		status = "None"
	end

	local mobExists = ServerStorage.Mobs:FindFirstChild(name)
	local statusExists = ServerStorage.Status:FindFirstChild(status)

	if mobExists and statusExists then
		for i = 1, quantity do
			task.wait(waittime)
			local newMob = mobExists:Clone()
			local newStatusAilment = statusExists:Clone()

			print(newStatusAilment.Name)

			if old then
				newMob.HumanoidRootPart.CFrame = old.HumanoidRootPart.CFrame
			else
				newMob.HumanoidRootPart.CFrame = map.Start.CFrame
			end

			newMob.Parent = workspace.Mobs
			newMob.HumanoidRootPart:SetNetworkOwner(nil)

			local movingTo = Instance.new("IntValue", newMob)
			movingTo.Name = "MovingTo"

			local RandomPosition = Random.new():NextNumber(-0.5, 0.5)

			mob.Optimize(newMob)

			--// STATUS AILMENTS \\--

			if newStatusAilment.Name == "None" then
				-- Do nothing
			end

			if newStatusAilment.Name == "Bloated" then
				newMob.Humanoid.MaxHealth *= 2
				newMob.Humanoid.Health *= 2
			end

			if newStatusAilment.Name == "Nimble" then
				newMob.Humanoid.WalkSpeed *= 2
			end

			local hum = newMob.Humanoid
			local animator = hum:FindFirstChild("Animator") or Instance.new("Animator", hum)
			local Animations = newMob:WaitForChild("Animations")
			local Died = Animations:WaitForChild("Died")

			for i, object in ipairs(newMob:GetDescendants()) do
				if object:IsA("BasePart") then
					PhysicsService:SetPartCollisionGroup(object, "Mobs")
				end
			end

			hum.BreakJointsOnDeath = false

			hum.Died:Connect(function()
				if newMob:FindFirstChild("Mystery") then
					mob.Mystery(newMob, map)
				end

				if hum.Health <= 0 and newMob.Name ~= "Mystery" and newMob.Name ~= "Mystery Boss" then
					hum.WalkSpeed = 0

					local deathAnimation = animator:LoadAnimation(Died)
					deathAnimation:Play()

					wait(deathAnimation.Length)

					newMob:Destroy()
				end

				newMob:Destroy()
			end)

			coroutine.wrap(mob.Move)(newMob, map, old)
		end
	else
		warn("Requested mob does not exist:", name)

	end
end

return mob

--RoundModule

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

local events = ReplicatedStorage:WaitForChild("Events")

local TextEvent = events:WaitForChild("TextEvent")

local mob = require(script.Parent.Mob)
local info = workspace.Info

local round = {}
local votes = {}

function round.StartGame()
	if info.GameRunning.Value then return end

	local waves = nil

	local map = round.LoadMap()
	local mode
	if map then
		mode = round.LoadMode()
	end
	
	if not mode then
		mode = "Normal"
		print(mode)
    end
    
	if not waves then
		waves = 20
		print(waves)
	end

	if mode == "Normal" then
		waves = 20
	elseif mode == "Anomalous" then
		waves = 25
	end

	info.GameRunning.Value = true

	for i = 3, 0, -1 do
		info.Message.Value = "Game starting in..." .. i
		task.wait(1)
	end

	for wave = 1, waves do
		info.Wave.Value = wave
		info.Message.Value = ""
		
		local playersInGame = #game.Players:GetPlayers()

		if mode == "Normal" then
			round.GetNormal(wave, map)
		elseif mode == "Anomalous" then
			round.GetAnomalous(wave, map)
		end

		repeat
			task.wait(1)
		until #workspace.Mobs:GetChildren() == 0 or not info.GameRunning.Value

		if info.GameRunning.Value and wave == waves then
			info.Message.Value = "VICTORY"
		elseif info.GameRunning.Value then
			local reward

			if mode == "Normal" then
				reward = (45 - ((playersInGame - 1) * 5)) + wave * (135 - ((playersInGame - 1) * 15)) -- wave 1 = $180 if 1 player
			elseif mode == "Anomalous" then
				reward = 80 * math.round(wave / 2)

			end
			for i, player in ipairs(Players:GetPlayers()) do
				player.Stardust.Value += reward
				TextEvent:FireClient(player, "Money", "Wave Reward: $" .. math.round(reward))
			end
			
			task.wait(1)

			for i=5, 0, -1 do
				info.Message.Value = "Next wave starting in..." .. i
				task.wait(1)
			end
		else
			break
		end
	end
end

function round.LoadMap()
	local votedMap = round.ToggleVoting()
	local mapFolder = ServerStorage.Maps:FindFirstChild(votedMap)
	if not mapFolder then
		mapFolder = ServerStorage.Maps.Grassland
	end

	local newMap = mapFolder:Clone()
	newMap.Parent = workspace.Maps

	workspace.SpawnBox.Floor:Destroy()

	newMap.Base.Humanoid.HealthChanged:Connect(function(health)
		if health <= 0 then
			info.GameRunning.Value = false
			info.Message.Value = "GAME OVER"
		end
	end)

	return newMap
end

function round.LoadMode()
	local votedMode = round.ToggleModeVoting()
	local modeValue = ServerStorage.Modes:FindFirstChild(votedMode)
	if not modeValue then
		modeValue = ServerStorage.Modes.Normal
	end
	
	info.Gamemode.Value = modeValue.Name
	
	return modeValue.Name
end

function round.ToggleVoting()
	local maps = ServerStorage.Maps:GetChildren()
	votes = {}
	for i, map in ipairs(maps) do
		votes[map.Name] = {}
	end

	info.Voting.Value = true

	for i=5, 1, -1 do
		info.Message.Value = "Map voting (" .. i .. ")"
		task.wait(1)
	end

	local winVote = nil
	local winScore = 0
	for name, map in pairs(votes) do
		if #map > winScore then
			winScore = #map
			winVote = name
		end
	end

	if not winVote then
		local n = math.random(#maps)
		winVote = maps[n].Name
	end

	info.Voting.Value = false

	return winVote
end

function round.ToggleModeVoting()
	local modes = ServerStorage.Modes:GetChildren()
	votes = {}
	for i, mode in ipairs(modes) do
		votes[mode.Name] = {}
	end

	info.GamemodeVoting.Value = true

	for i=5, 1, -1 do
		info.Message.Value = "Mode voting (" .. i .. ")"
		task.wait(1)
	end

	local winVote = nil
	local winScore = 0
	for name, mode in pairs(votes) do
		if #mode > winScore then
			winScore = #mode
			winVote = name
		end
	end

	if not winVote then
		local n = math.random(#modes)
		winVote = modes[n].Name
	end

	info.GamemodeVoting.Value = false

	return winVote
end

function round.ProcessVote(player, vote)

	for name, mapVotes in pairs(votes) do
		local oldVote = table.find(mapVotes, player.UserId)
		if oldVote then
			table.remove(mapVotes, oldVote)
			print("Old vote found", oldVote)
			break
		end
	end

	print("Processed vote for", vote)
	table.insert(votes[vote], player.UserId)

	events:WaitForChild("UpdateVoteCount"):FireAllClients(votes)
end
events:WaitForChild("VoteForMap").OnServerEvent:Connect(round.ProcessVote)

function round.GetNormal(wave, map)
	if wave < 2 then
		mob.Spawn("Mystery", 3 + wave, 0.5, map)
	elseif wave == 2 then
		mob.Spawn("Speedy Zombie", 2, 0.5, map)
		mob.Spawn("Zombie", 3, 0.5, map)
	elseif wave == 3 then
		mob.Spawn("Zombie", 4, 0.5, map)
		mob.Spawn("Speedy Zombie", 3, 0.5, map)
	elseif wave == 4 then
		mob.Spawn("Slow Zombie", 6, 0.5, map)
	elseif wave == 5 then
		mob.Spawn("Speedy Zombie", 3, 0.5, map)
		mob.Spawn("Zombie", 7, 0.5, map)
		mob.Spawn("Slow Zombie", 4, 0.5, map)
		mob.Spawn("Speedy Zombie", 6, 0.5, map)
	elseif wave < 7 and wave > 5 then
		mob.Spawn("Zombie", 3 + wave, 0.5, map)
		mob.Spawn("Speedy Zombie", wave, 0.5, map)
		mob.Spawn("Slow Zombie", math.round(wave / 2), 0.5, map)
		mob.Spawn("Zombie", math.round(wave / 1.5), 0.5, map)
	elseif wave == 7 then
		mob.Spawn("Zombie", 10, 0.5, map)
		mob.Spawn("Speedy Zombie", 8, 0.2, map)
		mob.Spawn("Slow Zombie", wave, 0.5, map)
		mob.Spawn("Boss Zombie", 1, 0.5, map)
	elseif wave == 8 then
		mob.Spawn("Speedy Zombie", 8, 0.2, map)
		mob.Spawn("Zombie", 10, 0.5, map)
		mob.Spawn("Slow Zombie", 6, 1, map)
	elseif wave == 9 then
		mob.Spawn("Boss Zombie", 1, 1, map)
		mob.Spawn("Zombie", 10, 0.5, map)
		mob.Spawn("Speedy Zombie", 10, 0.1, map)
	elseif wave == 10 then
		mob.Spawn("Boss Zombie", 2, 1, map)
		mob.Spawn("Zombie", 10, 0.5, map)
		mob.Spawn("Slow Zombie", 4, 0.5, map)
	elseif wave == 11 then
		mob.Spawn("Mystery", 4, 0.4, map)
		mob.Spawn("Speedy Zombie", 10, 0.1, map)
	end
end

function round.GetAnomalous(wave, map)
	if wave < 4 then
		mob.Spawn("Zombie", 4 + wave, 0.5, map)
	elseif wave < 5 and wave > 3 then
		mob.Spawn("Zombie", 2 * wave, 0.5, map)
	elseif wave == 5 then
		mob.Spawn("Speedy Zombie", 6, 0.35, map)
	elseif wave == 6 then
		mob.Spawn("Zombie", 7, 0.5, map)
		mob.Spawn("Speedy Zombie", 5, 0.5, map)
	end
end

return round

--EnemyModule

return function(registry)
	
	local enemyType = registry.Cmdr.Util.MakeEnumType("enemyName", 
		{
			"Zombie";
			"Speedy Zombie";
			"Slow Zombie";
			"Boss Zombie";
            "Hidden";
			"Mystery";
		}
	)
	registry:RegisterType("enemy", enemyType)
end

--Datastore

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local ServerStorage = game:GetService("ServerStorage")

local bindables = ServerStorage:WaitForChild("Bindables")

local database = DataStoreService:GetDataStore("fanum")
local functions = ReplicatedStorage:WaitForChild("Functions")
local events = ReplicatedStorage:WaitForChild("Events")
local getDataFunc = functions:WaitForChild("GetData")
local exitEvent = events:WaitForChild("ExitGame")

local MAX_SELECTED_TOWERS = 5

local data = {}

-- Load the players data
local function LoadData(player)
	local success = nil
	local playerData = nil
	local attempt = 1
	
	repeat
		success, playerData = pcall(function()
			return database:GetAsync(player.UserId)
		end)
		
		attempt += 1
		if not success then
			warn(playerData)
			task.wait()
		end
		
	until success or attempt == 5
	
	if success then
		print("Connection success")
		if not playerData then
			print("New player, giving default data")
			playerData = {
				["Stars"] = 0,
				["Level"] = 1,
				["Experience"] = 0,
				["RequiredExperience"] = 10,
				["SelectedTowers"] = {"Mayhem"},
				["OwnedTowers"] = {"Mayhem", "Purity", "Sword Novice"},
				["OwnedSkins"] = {},
				["EquippedSkins"] = {},
				["SkinsHave"] = {},
				["KilledEnemies"] = {},
			}
		end
		
		print(playerData.KilledEnemies)
		
		if not playerData.KilledEnemies then
			playerData.KilledEnemies = {}
		end
		
		print(playerData.KilledEnemies)
		
		data[player.UserId] = playerData
	else
		warn("Unable to get data for player", player.UserId)
		player:Kick("There was a problem getting your data")
	end
end
Players.PlayerAdded:Connect(LoadData)

-- Save the players data
local function SaveData(player)
	if data[player.UserId] then
		local success = nil
		local playerData = nil
		local attempt = 1

		local info = workspace.Info
		local stars = math.round(info.Wave.Value * 10 / 2)
		local experience = math.round(info.Wave.Value * 10 / 5)
		if info.Gamemode.Value == "Normal" and info.Message.Value == "VICTORY" then
			stars = 125
			experience = 50
		end
		
		data[player.UserId].Stars += stars
		data[player.UserId].Experience += experience

		repeat
			success, playerData = pcall(function()
				return database:UpdateAsync(player.UserId, function()
					return data[player.UserId]
				end)
			end)

			attempt += 1
			if not success then
				warn(playerData)
				task.wait()
			end

		until success or attempt == 5

		if success then
			print("Data saved successfully")
		else
			warn("Unable to save data for", player.UserId)
		end
	else
		warn("No session data for", player.UserId)
	end
	
end
exitEvent.OnServerEvent:Connect(function(player)
	SaveData(player)
	data[player.UserId] = nil
end)

game:BindToClose(function()
	if not RunService:IsStudio() then
		for index, player in pairs(Players:GetPlayers()) do
			task.spawn(function()
				SaveData(player)
			end)
		end
	else
		print("Shutting down inside studio")
	end
end)


getDataFunc.OnServerInvoke = function(player)
	return data[player.UserId]
end

--GameController(long)

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local Debris = game:GetService("Debris")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local PhysicsService = game:GetService("PhysicsService")

local localPlayer = Players.LocalPlayer
local Stardust = localPlayer:WaitForChild("Stardust")
local map = workspace.Maps:FindFirstChildOfClass("Folder")

local events = ReplicatedStorage:WaitForChild("Events")
local functions = ReplicatedStorage:WaitForChild("Functions")
local modules = ReplicatedStorage:WaitForChild("Modules")
local towers = ReplicatedStorage:WaitForChild("Towers")

local Misc = ReplicatedStorage:WaitForChild("Misc")

local Range = Misc:WaitForChild("Range")
local HighlightHover = Misc:WaitForChild("HighlightHover")

local health = require(modules:WaitForChild("Health"))

local TextEvent = events:WaitForChild("TextEvent")

local requestTowerFunction = functions:WaitForChild("RequestTower")
local spawnTowerFunction = functions:WaitForChild("SpawnTower")
local sellTowerFunction = functions:WaitForChild("SellTower")
local changeTowerModeFunction = functions:WaitForChild("ChangeTowerMode")
local getDataFunction = functions:WaitForChild("GetData")

local gui = script.Parent

local Sounds = gui.Sounds
local Selection = gui.Selection; Selection.Visible = false
local MobHoverGui = gui:WaitForChild("MobHoverGui")
local BossUI = gui:WaitForChild("BossUI")

local Action = Selection.Action
local LevelStat = Selection.LevelStat
local TotalStats = Selection.TotalStats

local NextLevelText = LevelStat.NextLevel
local NextLevelFrame = LevelStat.NextLevelFrame

local DamageFrame = NextLevelFrame.Damage
local RangeFrame = NextLevelFrame.Range
local CooldownFrame = NextLevelFrame.Cooldown

local TitleFrame = Selection.Title

local LevelText = TitleFrame.Level.LevelText

local StatWindow = Selection:WaitForChild("StatWindow")
local StatsFrame = StatWindow.Stats

local Upgrade = Action.Upgrade
local Sell = Action.Sell
local Target = Action.Target

local wrldModel = Selection.Title.ViewportFrame.WorldModel

local HealthFrame = MobHoverGui:WaitForChild("HealthFrame")

local StatusAilments = HealthFrame:WaitForChild("StatusAilments")
local mobCurrentHealth = HealthFrame:WaitForChild("CurrentHealth")

local info = workspace:WaitForChild("Info")
local camera = workspace.CurrentCamera

local hoveredInstance = nil
local selectedTower = nil
local towerToSpawn = nil
local previousHighlight = nil

local canPlace = false
local hoverHandled = false

local rotation = 0
local placedTowers = 0
local maxTowers = 30

local lastTouch = tick()

local mobsAlive
local towersAlive
local unitsAlive

local function Comma(Amount)
	while true do
		local digit
		Amount, digit = string.gsub(Amount, "(-?%d+)(%d%d%d)", "%1,%2")
		if (digit == 0) then
			break
		end
	end
	return Amount
end

local function EnemyIndexFunction(mode)
	local enemies = {}
	
	if mode == "Normal" then
		enemies = {"Zombie", "Speedy Zombie", "Slow Zombie", "Boss Zombie", "Hidden", "Mystery", "Normal Orb"}
        
	end
end

local function MouseRaycast(blacklist)
	local mousePosition = UserInputService:GetMouseLocation()
	local mouseRay = camera:ViewportPointToRay(mousePosition.X, mousePosition.Y)
	local raycastParams = RaycastParams.new()

	if blacklist then
		raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
		raycastParams.FilterDescendantsInstances = blacklist
	end

	local raycastResult = workspace:Raycast(mouseRay.Origin, mouseRay.Direction * 1000, raycastParams)

	return raycastResult, mousePosition
end

local function Texts(typ, text)
	if typ == "Money" then
		Sounds.MoneyAlert:Play()
		local Template = gui.MoneyAlert.Template:Clone()
		Template.Parent = gui.MoneyAlert.MoneyAlerts
		Template.Text = text
		Template.Font = Enum.Font.JosefinSans
		Template.Visible = true

		local tweenInfo1 = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
		local property1 = {
			Size = UDim2.new(1, 0, 1, 0)
		}

		local Tween1 = TweenService:Create(Template, tweenInfo1, property1)
		Tween1:Play()

		task.wait(1)

		local tweenInfo2 = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
		local property2 = {
			Size = UDim2.new(1, 0, 0, 0)
		}

		local Tween2 = TweenService:Create(Template, tweenInfo2, property2)
		Tween2:Play()

		Tween2.Completed:Wait()

		Template:Destroy()
	elseif typ == "Error" then
		Sounds.Error:Play()
		local Template = gui.Error.Template:Clone()
		Template.Parent = gui.Error.Errors
		Template.Text = text
		Template.Font = Enum.Font.JosefinSans
		Template.Visible = true

		local tweenInfo1 = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
		local property1 = {
			Size = UDim2.new(1, 0, 1, 0)
		}

		local Tween1 = TweenService:Create(Template, tweenInfo1, property1)
		Tween1:Play()

		task.wait(1)

		local tweenInfo2 = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
		local property2 = {
			Size = UDim2.new(1, 0, 0, 0)
		}

		local Tween2 = TweenService:Create(Template, tweenInfo2, property2)
		Tween2:Play()

		Tween2.Completed:Wait()

		Template:Destroy()

	end

end

local function CreateRangeCircle(tower, placeholder)

	local range = tower.Config.Range.Value
	local height = (tower.PrimaryPart.Size.Y) + tower.Humanoid.HipHeight
	local offset = CFrame.new(0, -height, 0)

	local p = Instance.new("Part")
	p.Name = "Range"
	p.Shape = Enum.PartType.Cylinder
	p.Material = Enum.Material.Neon
	p.Transparency = 0.9
	p.Size = Vector3.new(0.25, range * 2, range * 2)
	p.TopSurface = Enum.SurfaceType.Smooth
	p.BottomSurface = Enum.SurfaceType.Smooth
	p.CFrame = tower.PrimaryPart.CFrame * offset * CFrame.Angles(0, 0, math.rad(90))
	p.CanCollide = false

	if placeholder then
		p.Anchored = false
		local weldConstraint = Instance.new("WeldConstraint", p)
		weldConstraint.Part0 = p
		weldConstraint.Part1 = tower.PrimaryPart
		p.Parent = tower
	else
		p.Anchored = true
		p.Parent = workspace.CurrentCamera
		p:Destroy()
	end
end

local function HoverOverMob(result, mousePosition)
	local mobHumanoid = result.Instance.Parent:FindFirstChild("Humanoid") or result.Instance.Parent.Parent:FindFirstChild("Humanoid")

	if mobHumanoid then
		local mobChar = mobHumanoid.Parent

        local currentColor = mobCurrentHealth.Color

		local percent = mobHumanoid.Health / mobHumanoid.MaxHealth
		mobCurrentHealth.Size = UDim2.new(percent, 0, 1, 0)
		HealthFrame.Health.Text = math.round(mobHumanoid.Health) .. "/" .. math.round(mobHumanoid.MaxHealth)
		MobHoverGui.Title.Text = mobChar.Name
		MobHoverGui.Position = UDim2.new(0, mousePosition.X, 0, mousePosition.Y)
		MobHoverGui.Visible = true

		--// STATUS AILMENTS \\--

		if mobChar:FindFirstChild("Nimble") then
			StatusAilments.Nimble.Visible = true
		else
			StatusAilments.Nimble.Visible = false
		end

		if mobChar:FindFirstChild("Bloated") then
			StatusAilments.Bloated.Visible = true
		else
			StatusAilments.Bloated.Visible = false
		end

		if mobChar:FindFirstChild("Armor") then
			StatusAilments.Armor.Visible = true
			StatusAilments.Armor.Title.Text = mobChar:FindFirstChild("Armor").DamageReduction.Value * 100 .. "%"
		else
			StatusAilments.Armor.Visible = false
		end

        if mobChar:FindFirstChild("Boss") then
			StatusAilments.Boss.Visible = true
            mobCurrentHealth.Color = Color3.fromRGB(228, 173, 200)
		else
			StatusAilments.Boss.Visible = false
            mobCurrentHealth.Color = currentColor
		end
	end
end

local function BossGui(mob)
    if mob:FindFirstChild("Boss") then
        local hum = mob:FindFirstChild("Humanoid")

        local percent = hum.Health / hum.MaxHealth
        local healthFrame = BossUI.Health

        healthFrame.Size = UDim2.new(percent, 0, 1, 0)
        healthFrame.Title.Text = mob.Name
        healthFrame.Health.Text = Comma(hum.Health) .. "/" .. Comma(hum.MaxHealth)
        BossUI.Visible = true
    end
end

local function RemovePlaceholderTower()
	if towerToSpawn then
		towerToSpawn:Destroy()
		towerToSpawn = nil
		rotation = 0

		if map then
			for i, BoundaryTower in pairs(workspace.Towers:GetChildren()) do
				for i, Part in pairs(BoundaryTower:GetChildren()) do
					if Part.Name == "Boundary" then
						Part.Transparency = 1
					end
				end
			end
		end
	end

	gui.Controls.Visible = false
end

local function AddPlaceholderTower(name)

	local towerExists = towers:FindFirstChild(name)

	if towerExists then
		RemovePlaceholderTower()
		towerToSpawn = towerExists:Clone()
		towerToSpawn.Parent = workspace.Towers

		if map then
			for i, BoundaryTower in pairs(workspace.Towers:GetChildren()) do
				for i, Part in pairs(BoundaryTower:GetChildren()) do
					if Part.Name == "Boundary" then
						Part.Transparency = 0.8
					end
				end
			end
		end

		CreateRangeCircle(towerToSpawn, true)

		for i, object in ipairs(towerToSpawn:GetDescendants()) do
			if object:IsA("BasePart") then
				PhysicsService:SetPartCollisionGroup(object, "Towers")

				if object.Name ~= "Range" then
					object.Material = Enum.Material.ForceField
					object.Transparency = 0.3
				elseif object:IsA("ParticleEmitter") or object:IsA("Trail") then
					object.Enabled = false
				end
			end
		end

		gui.Controls.Visible = true
	end
end

local function ColorPlaceholderTower(color)
	for i, object in ipairs(towerToSpawn:GetDescendants()) do
		if object:IsA("BasePart") then
			object.Color = color
		end
	end
end

local function toggleTowerInfo()
	camera:ClearAllChildren()
	gui.Towers.Limit.Text = "Towers: " .. placedTowers .. "/" .. maxTowers

	if selectedTower then
		CreateRangeCircle(selectedTower)
		local ClonedSelectionTower = selectedTower:Clone()
		Selection.Visible = true

		local configuration = selectedTower.Config

		Range.Position = selectedTower.HumanoidRootPart.Position + Vector3.new(0, -0.8, 0)
		Range.Parent = workspace.Range
		local range = configuration.Range.Value
		local tweenInfo = TweenInfo.new(0.3, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)
		local TweenTransparency = {Transparency = 0}
		local tT2 = {Transparency = 0.7}
		local TweenSize = {Size = Vector3.new(0.25, range * 2, range * 2)}
		local TweenSi = TweenService:Create(Range, tweenInfo, TweenSize)
		local TweenTr1 = TweenService:Create(Range.C, tweenInfo, tT2)
		local TweenTr2 = TweenService:Create(Range.D, tweenInfo, TweenTransparency)
		TweenSi:Play()
		TweenTr1:Play()
		TweenTr2:Play()

		Selection.Stats.Damage.Value.Text = configuration.Damage.Value
		Selection.Stats.Range.Value.Text = configuration.Range.Value
		Selection.Stats.Cooldown.Value.Text = configuration.Cooldown.Value

		Selection.Title.TowerName.Text = selectedTower.Name
		Selection.Title.OwnerName.Text = configuration.Owner.Value .. "'s"

		Target.Title.Text = "Target: " .. configuration.TargetMode.Value

		StatsFrame.Cooldown.Title.Text = configuration.Cooldown.Value
		StatsFrame.Damage.Title.Text = configuration.Damage.Value
		StatsFrame.Range.Title.Text = configuration.Range.Value

		LevelText.Text = configuration.Level.Value

		if ClonedSelectionTower then

			local SampleScript = script.SampleScript:Clone()
			SampleScript.Parent = ClonedSelectionTower

			while #wrldModel:GetChildren() >= 1 do
				for i, v in pairs(wrldModel:GetChildren()) do
					if v:IsA("Model") then
						v:Destroy()
					end
				end
			end

			ClonedSelectionTower.Parent = wrldModel

			ClonedSelectionTower:SetPrimaryPartCFrame(CFrame.new(0.5, 0.125, 0) * CFrame.Angles(0, math.rad(-180), 0))
			ClonedSelectionTower:ScaleTo(0.25)
		end

		if not selectedTower then
			ClonedSelectionTower:Destroy()
		end

		if configuration.Owner.Value == localPlayer.Name then
			Action.Visible = true

			local upgradeTower = configuration:FindFirstChild("Upgrade")

			if upgradeTower then
				NextLevelFrame.Visible = true
				NextLevelText.Visible = true

				local upgradedTower = upgradeTower.Value
				local upgradedTowerConfiguration = upgradedTower.Config
				Upgrade.Visible = true
				Upgrade.Title.Text = "Upgrade: $" .. Comma(upgradedTowerConfiguration.Price.Value)

				if configuration.Level.Value == upgradedTowerConfiguration.Level.Value then
					NextLevelText.Visible = false
				else
					NextLevelText.Visible = true
					NextLevelText.Text = "Lvl. " .. upgradedTowerConfiguration.Level.Value
				end

				if configuration.Damage.Value == upgradedTowerConfiguration.Damage.Value then
					DamageFrame.Visible = false
				else
					DamageFrame.Visible = true
					DamageFrame.Title.Text = configuration.Damage.Value .. " -> " .. upgradedTowerConfiguration.Damage.Value
				end

				if configuration.Cooldown.Value == upgradedTowerConfiguration.Cooldown.Value then
					CooldownFrame.Visible = false
				else
					CooldownFrame.Visible = true
					CooldownFrame.Title.Text = configuration.Cooldown.Value .. " -> " .. upgradedTowerConfiguration.Cooldown.Value
				end

				if configuration.Range.Value == upgradedTowerConfiguration.Range.Value then
					RangeFrame.Visible = false
				else
					RangeFrame.Visible = true
					RangeFrame.Title.Text = configuration.Range.Value .. " -> " .. upgradedTowerConfiguration.Range.Value
				end
			else
				NextLevelFrame.Visible = false
				NextLevelText.Visible = false

				Upgrade.Title.Text = "MAXED"
			end
		else
			Action.Visible = false
		end

	else
		Selection.Visible = false

		local tweenInfo = TweenInfo.new(0.3, Enum.EasingStyle.Exponential, Enum.EasingDirection.In)
		local TweenTransparency = {Transparency = 1}
		local TweenSize = {Size = Vector3.new(0, 0, 0)}
		local TweenSi = TweenService:Create(Range, tweenInfo, TweenSize)
		local TweenTr1 = TweenService:Create(Range.C, tweenInfo, TweenTransparency)
		local TweenTr2 = TweenService:Create(Range.D, tweenInfo, TweenTransparency)
		TweenSi:Play()
		TweenTr1:Play()
		TweenTr2:Play()

		if selectedTower then
			for i, v in pairs(wrldModel:GetChildren()) do
				if v:IsA("Model") then
					v:Destroy()
				end
			end
		end
	end
end

local function ShakeCamera(length, intensity)
    local startTime = tick()

    while true do
        if tick() - startTime >= length then
            break
        end

        local x = math.random(-intensity, intensity) / 10
        local y = math.random(-intensity, intensity) / 10
        local z = math.random(-intensity, intensity) / 10

        for i, player in ipairs(Players:GetPlayers()) do
            player.Character.Humanoid.CameraOffset = Vector3.new(x, y, z)
        end

        workspace.CurrentCamera.CFrame = CFrame.Angles(x / 100, y / 100, z / 100)
        
        wait()
    end

    script.Parent.Humanoid.CameraOffset = Vector3.new(0, 0, 0)
end

local function SpawnNewTower()
	if canPlace then
		local placedTower = spawnTowerFunction:InvokeServer(towerToSpawn.Name, towerToSpawn.PrimaryPart.CFrame)
		if placedTower then
			placedTowers += 1
			RemovePlaceholderTower()
			toggleTowerInfo()
			Sounds.Place:Play()
		end
	else
	end
end

local function UpgradeTower()
	if selectedTower then
		local upgrade = selectedTower.Config:FindFirstChild("Upgrade")
		if upgrade then
			local upgradeTower = upgrade.Value

			if upgradeTower then
				local upgradeSuccess = spawnTowerFunction:InvokeServer(upgradeTower.Name, selectedTower.PrimaryPart.CFrame, selectedTower)

				if upgradeSuccess then
					selectedTower = upgradeSuccess
					toggleTowerInfo()
					Sounds.Upgrade:Play()
				else
					--Error("You can't upgrade this!", Enum.Font.Jura)
				end
			end
		else

		end
	--[[else
		Error("This tower cannot be upgraded anymore!", Enum.Font.Jura)]]
	end
end

local function SetTarget()
	if selectedTower then
		local modeChangeSuccess = changeTowerModeFunction:InvokeServer(selectedTower)

		if modeChangeSuccess then
			toggleTowerInfo()
		end
	end
end

local function SellTower()
	if selectedTower then
		local soldTower = sellTowerFunction:InvokeServer(selectedTower)

		if soldTower then
			selectedTower = nil
			placedTowers -= 1
			toggleTowerInfo()
		end
	end
end

local function KeybindTower(number)
	local playerData = getDataFunction:InvokeServer()

	for i, v in ipairs(playerData.SelectedTowers) do
		if i == number and workspace.Maps:FindFirstChildOfClass("Folder") then
			local tower = towers:FindFirstChild(v)

			if tower then
				local allowedToSpawn = requestTowerFunction:InvokeServer(tower.Name, towerToSpawn)

				if allowedToSpawn then
					AddPlaceholderTower(tower.Name)
				end
			end
		end
	end
end

gui.Controls.Cancel.Activated:Connect(RemovePlaceholderTower)

Target.Activated:Connect(function()
	SetTarget()
end)

Upgrade.Activated:Connect(function()
	UpgradeTower()
end)

Sell.Activated:Connect(function()
	SellTower()
end)

UserInputService.InputBegan:Connect(function(input, processed)
	if processed then
		return
	end

	if towerToSpawn then
		if input.UserInputType == Enum.UserInputType.MouseButton1 then
			SpawnNewTower()
		elseif input.UserInputType == Enum.UserInputType.Touch then
			local timeSinceLastTouch = tick() - lastTouch

			if timeSinceLastTouch <= 0.25 then
				SpawnNewTower()
			end
			lastTouch = tick()
		elseif input.KeyCode == Enum.KeyCode.R then
			rotation += 90
		elseif input.KeyCode == Enum.KeyCode.Q then
			RemovePlaceholderTower()
		end

	elseif hoveredInstance and (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) then
		local model = hoveredInstance:FindFirstAncestorOfClass("Model")

		if model and model.Parent == workspace.Towers then
			selectedTower = model
		else
			selectedTower = nil
		end

		toggleTowerInfo()
	elseif input.KeyCode == Enum.KeyCode.One then
		KeybindTower(1)
	elseif input.KeyCode == Enum.KeyCode.Two then
		KeybindTower(2)
	elseif input.KeyCode == Enum.KeyCode.Three then
		KeybindTower(3)
	elseif input.KeyCode == Enum.KeyCode.Four then
		KeybindTower(4)
	elseif input.KeyCode == Enum.KeyCode.Five then
		KeybindTower(5)
	elseif input.KeyCode == Enum.KeyCode.E then
		UpgradeTower()
	elseif input.KeyCode == Enum.KeyCode.X then
		SellTower()
	end

end)

RunService.RenderStepped:Connect(function()

	if towerToSpawn then
		local result = MouseRaycast({towerToSpawn})
		if result and result.Instance then
			if result.Instance.Parent.Name == "GroundPlacement" and towerToSpawn.Config:FindFirstChild("GroundTower") and not towerToSpawn.Config:FindFirstChild("CliffTower") then
				canPlace = true
				ColorPlaceholderTower(Color3.new(0, 0.666667, 1))
			elseif result.Instance.Parent.Parent.Name == "CliffPlacement" and not towerToSpawn.Config:FindFirstChild("GroundTower") and towerToSpawn.Config:FindFirstChild("CliffTower") then
				canPlace = true
				ColorPlaceholderTower(Color3.new(0, 0.2, 1))
			else
				canPlace = false
				ColorPlaceholderTower(Color3.new(.8, 0, 0))
			end

			local x = result.Position.X
			local y = result.Position.Y + towerToSpawn.Humanoid.HipHeight + (towerToSpawn.PrimaryPart.Size.Y * 1.25)
			local z = result.Position.Z

			local cframe = CFrame.new(x, y, z) * CFrame.Angles(0, math.rad(rotation), 0)
			towerToSpawn:SetPrimaryPartCFrame(cframe)
		end
	else

		local result, mousePos = MouseRaycast()

		if result and result.Instance and mousePos then
			hoveredInstance = result.Instance
			if result.Instance.Parent.Parent and result.Instance.Parent.Parent.Name == "Mobs" then
				HoverOverMob(result, mousePos)
                BossGui(result.Instance.Parent)
			elseif result.Instance.Parent.Parent.Parent and result.Instance.Parent.Parent.Parent.Name == "Mobs" then
				HoverOverMob(result, mousePos)
                BossGui(result.Instance.Parent)
			else
				MobHoverGui.Visible = false
			end
		else
			MobHoverGui.Visible = false
		end
	end
end)

local function DisplayEndScreen(status)
	local screen = gui.EndScreen

	if status == "GAME OVER" then

		screen.Failure:Play()
		screen.Content.Title.TextColor3 = Color3.new(1, 0, 0)
		screen.ImageColor3 = Color3.new(0, 0, 0)
		screen.Content.Subtitle.Text = "Better luck next time"

	elseif status == "VICTORY" then

		screen.Victory:Play()
		screen.Content.Title.TextColor3 = Color3.new(0, 1, 0)
		screen.ImageColor3 = Color3.new(0.6, 1, 0.4)
		screen.Content.Subtitle.Text = "Victory!!!! YAyAAYyayAYY!"

	end

	local info = workspace.Info
	local stars = math.round(info.Wave.Value * 10 / 2)
	local experience = math.round(info.Wave.Value * 10 / 5)

	if info.Wave.Value >= 20 and info.Gamemode.Value == "Normal" and status == "VICTORY" then
		stars = 125
		experience = 50
	end

	screen.Content.Title.Text = status
	screen.Stats.Wave.Text = "Wave: " .. workspace.Info.Wave.Value
	screen.Stats.Stars.Text = "Stars: " .. stars
	screen.Stats.Kills.Text = "Kills: " .. Players.LocalPlayer.Kills.Value

	screen.Size = UDim2.new(0,0,0,0)
	screen.Visible = true

	local tweenStyle = TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out, 0, false, 0)
	local zoomTween = TweenService:Create(screen, tweenStyle, {Size = UDim2.new(1,0,1,0)})
	zoomTween:Play()

	local events = ReplicatedStorage:WaitForChild("Events")
	local exitEvent = events:WaitForChild("ExitGame")
	local clicked
	clicked = screen.Exit.Activated:Connect(function()
		clicked:Disconnect()
		exitEvent:FireServer()
		screen.Visible = false
	end)
end

local function SetupGameGui()
	if not info.GameRunning.Value then
		return
	end

	TextEvent.OnClientEvent:Connect(Texts)

	gui.Voting.Visible = false
	gui.ModeVoting.Visible = false
	gui.Info.Health.Visible = true
	gui.Info.Stats.Visible = true
	gui.Towers.Visible = true

	local map = workspace.Maps:FindFirstChildOfClass("Folder")

	if map then
		health.Setup(map:WaitForChild("Base"), gui.Info.Health)
	else
		workspace.Maps.ChildAdded:Connect(function(newMap)
			health.Setup(newMap:WaitForChild("Base"), gui.Info.Health)
		end)
	end

	workspace.Units.ChildAdded:Connect(function(unit)
		health.Setup(unit)
	end)

	info.Message.Changed:Connect(function(change)
		gui.Info.Message.Text = change

		if change == "" then
			gui.Info.Message.Visible = false
		else
			gui.Info.Message.Visible = true
		end
	end)

	info.Wave.Changed:Connect(function(change)
		gui.Info.Stats.Wave.Text = "Wave: " .. change
	end)

	Stardust.Changed:Connect(function(change)
		gui.Info.Stats.Stardust.Text = Stardust.Value
	end)

	gui.Info.Stats.Stardust.Text = Stardust.Value

	gui.Towers.Limit.Text = "Towers: " .. placedTowers .. "/" .. maxTowers

	local playerData = getDataFunction:InvokeServer()
	for i, tower in pairs(playerData.SelectedTowers) do
		print(playerData)
		local tower = towers:FindFirstChild(tower)
		if tower then
			local button = gui.Towers.TowersFrame.Template:Clone()
			local config = tower:WaitForChild("Config")

			button.Name = tower.Name
			button.TowerName.Text = tower.Name
			button.TowerName.TextColor3 = tower.Ring.Color
			button.Visible = true
			button.Parent = gui.Towers.TowersFrame
			button.LayoutOrder = i
			button.KeybindText.Text = i
			button.Cost.Text = "$" .. Comma(math.round(config.Price.Value))

			local clonedTower = tower:Clone()

			local SampleScript = script.SampleScript:Clone()
			SampleScript.Parent = clonedTower

			clonedTower.Parent = button.ViewportFrame.WorldModel
			clonedTower:SetPrimaryPartCFrame(CFrame.new(0, 0, -3) * CFrame.Angles(0 , math.rad(-180), 0))

			button.Activated:Connect(function()
				local allowedToSpawn = requestTowerFunction:InvokeServer(tower.Name, towerToSpawn)

				if allowedToSpawn then
					AddPlaceholderTower(tower.Name)
				else
				end
			end)
		end
	end
end

local function SetupVoteMapGui()
	if not info.Voting.Value then
		return
	end

	gui.Voting.Visible = true

	local events = ReplicatedStorage:WaitForChild("Events")
	local voteEvent = events:WaitForChild("VoteForMap")
	local voteCountUpdate = events:WaitForChild("UpdateVoteCount")
	local maps = gui.Voting.Maps:GetChildren()

	for i, button in ipairs(maps) do
		if button:IsA("ImageButton") then
			button.Activated:Connect(function()
				voteEvent:FireServer(button.Name)
			end)
		end
	end

	voteCountUpdate.OnClientEvent:Connect(function(mapScores)
		for name, voteInfo in pairs(mapScores) do
			local button = gui.Voting.Maps:FindFirstChild(name)
			if button then
				button.Vote.Text = #voteInfo
			end
		end
	end)
end

local function SetupVoteModeGui()
	if not info.GamemodeVoting.Value then
		return
	end

	gui.Voting.Visible = false
	gui.ModeVoting.Visible = true

	local events = ReplicatedStorage:WaitForChild("Events")
	local voteEvent = events:WaitForChild("VoteForMap")
	local voteCountUpdate = events:WaitForChild("UpdateVoteCount")
	local maps = gui.ModeVoting.Modes:GetChildren()

	for i, button in ipairs(maps) do
		if button:IsA("ImageButton") then
			button.Activated:Connect(function()
				voteEvent:FireServer(button.Name)
			end)
		end
	end

	voteCountUpdate.OnClientEvent:Connect(function(mapScores)
		for name, voteInfo in pairs(mapScores) do
			local button = gui.ModeVoting.Modes:FindFirstChild(name)
			if button then
				button.Vote.Text = #voteInfo
			end
		end
	end)
end

local function ChangeStardustValue()
	gui.Info.Stats.Stardust.Text = Comma(math.round(localPlayer.Stardust.Value))
end

local function LoadGui()
	gui.Info.Message.Text = info.Message.Value

	info.Message.Changed:Connect(function(change)
		gui.Info.Message.Text = change
		if change == "" then
			gui.Info.Message.Visible = false
		else
			gui.Info.Message.Visible = true

			if change == "VICTORY" or change == "GAME OVER" then
				DisplayEndScreen(change)
			end
		end
	end)

	SetupVoteMapGui()
	SetupGameGui()
	ChangeStardustValue()

	info.GameRunning.Changed:Connect(SetupGameGui)
	info.Voting.Changed:Connect(SetupVoteMapGui)
	info.GamemodeVoting.Changed:Connect(SetupVoteModeGui)
	localPlayer.Stardust.Changed:Connect(ChangeStardustValue)
end

LoadGui()

--AnimationClient

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

local events = ReplicatedStorage:WaitForChild("Events")
local animateTowerEvent = events:WaitForChild("AnimateTower")

local function fireProjectile(tower, target)
	local config = tower:WaitForChild("Config")
	
	local projectile = config:FindFirstChild("Projectile")

	if projectile then
		local newProjectile = projectile:Clone()
		
		local distance = (tower.HumanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude
		newProjectile.CFrame = tower["Right Arm"].CFrame
		newProjectile.Anchored = true
		newProjectile.Parent = workspace.CurrentCamera
		newProjectile.Transparency = 0

		local WantedSize = projectile:GetAttribute("WantedSize")
		local ReturningSize = projectile:GetAttribute("ReturningSize")
		local Color = newProjectile:GetAttribute("Color")

		local TweenInformation  = TweenInfo.new(0.25, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)

		local Properties = {
			Position = target.HumanoidRootPart.Position,
			Size = WantedSize
		}
		
		newProjectile.Color = Color

		local Tween = TweenService:Create(newProjectile, TweenInformation, Properties)

		Tween:Play()

		Debris:AddItem(newProjectile, 0.5)

		Tween.Completed:Connect(function()
			local Tween2 = TweenService:Create(newProjectile, TweenInformation, {Size = ReturningSize})
			Tween2:Play()
		end)
	end
end

local function setAnimation(object, animName)
	local humanoid = object:WaitForChild("Humanoid")
	local animationsFolder = object:WaitForChild("Animations")

	if humanoid and animationsFolder then
		local animationObject = animationsFolder:WaitForChild(animName)

		if animationObject then
			local animator = humanoid:FindFirstChild("Animator") or Instance.new("Animator", humanoid)

			local playingTracks = animator:GetPlayingAnimationTracks()

			for i, track in pairs(playingTracks) do
				if track.Name == animName then
					return track
				end
			end

			local animationTrack = animator:LoadAnimation(animationObject)
			return animationTrack
		end
	end
end

local function playAnimation(object, animName)
	local animationTrack = setAnimation(object, animName)

	if animationTrack then
		animationTrack:Play()
	else
		warn("Animation track does not exist")
		return
	end
end

workspace.Mobs.ChildAdded:Connect(function(object)
	playAnimation(object, "Walk")
end)

workspace.Towers.ChildAdded:Connect(function(object)
	playAnimation(object, "Idle")

	if object.Animations:FindFirstChild("WingAnim") then
		playAnimation(object, "WingAnim")
	end

end)

workspace.Units.ChildAdded:Connect(function(object)
	playAnimation(object, "Walk")
end)


animateTowerEvent.OnClientEvent:Connect(function(tower, animName, target)
	playAnimation(tower, animName)

	if target then
		fireProjectile(tower, target)
	end

	if tower.HumanoidRootPart:FindFirstChild("Attack") then
		tower.HumanoidRootPart.Attack:Play()
	end
end)

Embed on website

To embed this project on your website, copy the following code and paste it into your website's HTML: