--[[ GameServer (Server) Integrates with Matchmaker module, handles player connections and basic events. Board/base creation is now handled by Matchmaker.StartMatch. MODIFIED: Added print statements BEFORE and AFTER require calls to debug which one is failing. MODIFIED: Added print statements to confirm RemoteEvent creation. ]] local ReplicatedStorage = game:GetService("ReplicatedStorage") local ServerScriptService = game:GetService("ServerScriptService") local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local CollectionService = game:GetService("CollectionService") -- Load Modules needed on the server print("GameServer: Attempting to require BoardManager...") -- Debug print BEFORE require local BoardManager = require(ReplicatedStorage:WaitForChild("BoardManager")) print("GameServer: Successfully required BoardManager.") -- Debug print AFTER require print("GameServer: Attempting to require TroopManager...") -- Debug print BEFORE require local TroopManager = require(ReplicatedStorage:WaitForChild("TroopManager")) print("GameServer: Successfully required TroopManager.") -- Debug print AFTER require print("GameServer: Attempting to require Matchmaker...") -- Debug print BEFORE require local Matchmaker = require(ServerScriptService:WaitForChild("Matchmaker")) -- Require the new module print("GameServer: Successfully required Matchmaker.") -- Debug print AFTER require print("GameServer: Starting initialization...") -- This print should now appear if all requires succeed -- Create RemoteEvents if they don't exist local RequestTroopMoveEvent = ReplicatedStorage:FindFirstChild("RequestTroopMove") or Instance.new("RemoteEvent", ReplicatedStorage) RequestTroopMoveEvent.Name = "RequestTroopMove" print("GameServer: Created/Verified RemoteEvent: RequestTroopMove") -- Debug print local BuyTroopEvent = ReplicatedStorage:FindFirstChild("BuyTroop") or Instance.new("RemoteEvent", ReplicatedStorage) BuyTroopEvent.Name = "BuyTroop" print("GameServer: Created/Verified RemoteEvent: BuyTroop") -- Debug print local UpdateMoneyEvent = ReplicatedStorage:FindFirstChild("UpdateMoney") or Instance.new("RemoteEvent", ReplicatedStorage) UpdateMoneyEvent.Name = "UpdateMoney" print("GameServer: Created/Verified RemoteEvent: UpdateMoney") -- Debug print local SendFeedbackMessage = ReplicatedStorage:FindFirstChild("SendFeedbackMessage") or Instance.new("RemoteEvent", ReplicatedStorage) SendFeedbackMessage.Name = "SendFeedbackMessage" print("GameServer: Created/Verified RemoteEvent: SendFeedbackMessage") -- Debug print -- **NEW Matchmaking RemoteEvents** local RequestJoinQueueEvent = ReplicatedStorage:FindFirstChild("RequestJoinQueue") or Instance.new("RemoteEvent", ReplicatedStorage) RequestJoinQueueEvent.Name = "RequestJoinQueue" print("GameServer: Created/Verified RemoteEvent: RequestJoinQueue") -- Debug print local RequestLeaveQueueEvent = ReplicatedStorage:FindFirstChild("RequestLeaveQueue") or Instance.new("RemoteEvent", ReplicatedStorage) RequestLeaveQueueEvent.Name = "RequestLeaveQueue" print("GameServer: Created/Verified RemoteEvent: RequestLeaveQueue") -- Debug print local UpdateClientStateEvent = ReplicatedStorage:FindFirstChild("UpdateClientState") or Instance.new("RemoteEvent", ReplicatedStorage) UpdateClientStateEvent.Name = "UpdateClientState" print("GameServer: Created/Verified RemoteEvent: UpdateClientState") -- Debug print print("GameServer: All RemoteEvents creation/verification steps completed.") -- Confirmation print -- == REMOVED Board and Base creation from server start == -- BoardManager:CreateBoard() -- Now called by Matchmaker -- Base creation/tagging -- Now called by Matchmaker -- == Connect Matchmaking Event Handlers == RequestJoinQueueEvent.OnServerEvent:Connect(function(player) print("GameServer: Received RequestJoinQueue from", player.Name) Matchmaker.AddPlayerToQueue(player) end) RequestLeaveQueueEvent.OnServerEvent:Connect(function(player) print("GameServer: Received RequestLeaveQueue from", player.Name) Matchmaker.RemovePlayerFromQueue(player) end) -- == Player Connection Handlers == Players.PlayerAdded:Connect(function(player) print("GameServer:", player.Name, "joined.") -- Initialize player state via Matchmaker (sends "Lobby" state to client) Matchmaker.InitializePlayer(player) -- Set initial money attribute (consider DataStore loading here later) if not player:GetAttribute("Money") then player:SetAttribute("Money", 500) -- Starting money print("GameServer: Set initial money for", player.Name, "to", player:GetAttribute("Money")) -- No need to fire UpdateMoneyEvent here, client will get it when needed or GUI loads end end) Players.PlayerRemoving:Connect(function(player) print("GameServer:", player.Name, "leaving.") -- Let Matchmaker handle cleanup (remove from queue, end match etc.) Matchmaker.HandlePlayerLeaving(player) end) -- == Troop Movement Handling (Remains the same) == RequestTroopMoveEvent.OnServerEvent:Connect(function(player, troopToMove, targetTile) -- Check if player is actually in a game state before processing move local playerState = Matchmaker.GetPlayerState(player) if playerState ~= "InGame" then warn("GameServer: Player", player.Name, "attempted move while not InGame (State:", playerState, ")") -- Optionally send feedback here if needed, but usually moves are only possible in InGame return -- Ignore move if not in game end print("GameServer: Received RequestTroopMove from", player.Name) -- 1. --- VALIDATION --- local boardModel = nil local matchId = Matchmaker.playerMatchMap[player.UserId] -- Get matchId from map if matchId then local matchInfo = Matchmaker.activeMatches[matchId] if matchInfo then boardModel = matchInfo.boardModel -- Get the correct board for this match end end if not boardModel then warn("GameServer: Could not find board model for player", player.Name, "'s match.") SendFeedbackMessage:FireClient(player, "Match board not found.") -- Feedback return end -- Basic checks: Ensure instances exist and are the correct type if not (troopToMove and troopToMove:IsA("BasePart") and troopToMove.Parent == Workspace) then warn("GameServer: Invalid troop instance received from", player.Name) SendFeedbackMessage:FireClient(player, "Invalid troop selected.") -- **Feedback** return -- Ignore invalid request end -- Check if targetTile is a valid board tile part *within the correct match board* if not (targetTile and targetTile:IsA("BasePart") and targetTile.Parent and targetTile.Parent == boardModel) then warn("GameServer: Invalid target tile instance received from", player.Name, "or not part of correct board.") SendFeedbackMessage:FireClient(player, "Invalid target tile.") -- **Feedback** return -- Ignore invalid request end -- Check Ownership if not TroopManager:DoesPlayerOwnTroop(troopToMove, player) then warn("GameServer: Player", player.Name, "attempted to move troop they don't own:", troopToMove.Name) SendFeedbackMessage:FireClient(player, "You don't own this troop.") -- **Feedback** return -- Ignore request end -- Check if troop is anchored if not troopToMove.Anchored then warn("GameServer: Troop", troopToMove.Name, "is not anchored, cannot move via script.") SendFeedbackMessage:FireClient(player, "Troop is not ready to move.") -- **Feedback** return end -- Check Distance (Server-side calculation) -- Use BoardManager:GetTileAtWorldPos, assumes it works with boards not at origin local currentTile = BoardManager:GetTileAtWorldPos(troopToMove.Position) -- Also check if the tile found is actually part of the correct match board if not currentTile or not currentTile.Parent or currentTile.Parent ~= boardModel then warn("GameServer: Could not determine current tile for troop:", troopToMove.Name, "on correct board.") SendFeedbackMessage:FireClient(player, "Could not determine troop's current location.") -- **Feedback** return -- Cannot validate distance end local currentTileX = currentTile:GetAttribute("TileX") local currentTileZ = currentTile:GetAttribute("TileZ") local targetTileX = targetTile:GetAttribute("TileX") local targetTileZ = targetTile:GetAttribute("TileZ") if currentTileX == nil or currentTileZ == nil or targetTileX == nil or targetTileZ == nil then warn("GameServer: Tile coordinates missing attributes for distance check.") SendFeedbackMessage:FireClient(player, "Error checking move distance.") -- **Feedback** return -- Cannot validate distance end local distance = math.abs(currentTileX - targetTileX) + math.abs(currentTileZ - targetTileZ) print("GameServer: Calculated move distance:", distance) if distance <= 0 or distance > MAX_MOVE_DISTANCE then warn("GameServer: Invalid move distance for", troopToMove.Name, "- Distance:", distance) SendFeedbackMessage:FireClient(player, "Target tile is too far away.") -- **Feedback** return -- Ignore request end -- Check if Target Tile is Occupied by ANOTHER TROOP or a BUILDING (within the match context) local overlapParams = OverlapParams.new() overlapParams.FilterDescendantsInstances = {troopToMove, boardModel} -- Ignore self and the board model overlapParams.FilterType = Enum.RaycastFilterType.Exclude -- Expand the check slightly vertically to catch things resting on the tile local checkRegion = Region3.new(targetTile.Position - Vector3.new(targetTile.Size.X/2, 0.5, targetTile.Size.Z/2), targetTile.Position + Vector3.new(targetTile.Size.X/2, troopToMove.Size.Y + 0.5, targetTile.Size.Z/2)) local occupants = Workspace:FindPartsInRegion3WithIgnoreList(checkRegion, overlapParams.FilterDescendantsInstances, 100) local isOccupied = false for _, part in ipairs(occupants) do -- Check if the overlapping part is another troop OR a building IN THIS MATCH -- Check for troop owner attribute OR base owner attribute local ownerAttribute = part:GetAttribute("OwnerUserId") -- Check for base owner attribute (set by Matchmaker) local troopOwnerAttribute = part:GetAttribute("Owner") -- Check for troop owner attribute (set by TroopManager) if (part:IsA("BasePart") and troopOwnerAttribute) or (ownerAttribute and (ownerAttribute == matchInfo.player1.UserId or ownerAttribute == matchInfo.player2.UserId)) then print("GameServer: Target tile", targetTile.Name, "is occupied by", part.Name) isOccupied = true break end end if isOccupied then warn("GameServer: Target tile", targetTile.Name, "is occupied. Move denied.") SendFeedbackMessage:FireClient(player, "Target tile is occupied.") -- **Feedback** return -- Ignore request end -- 2. --- EXECUTION --- print("GameServer: Move validated for", troopToMove.Name, "to", targetTile.Name) local targetPosition = targetTile.Position + Vector3.new(0, troopToMove.Size.Y / 2, 0) troopToMove.Position = targetPosition print("GameServer:", troopToMove.Name, "moved successfully.") end) -- == Buy Troop Handling (Remains mostly the same, but check player state) == BuyTroopEvent.OnServerEvent:Connect(function(player, troopType, placementPosition, clickedBuilding) -- Check if player is actually in a game state before processing purchase local playerState = Matchmaker.GetPlayerState(player) if playerState ~= "InGame" then warn("GameServer: Player", player.Name, "attempted purchase while not InGame (State:", playerState, ")") -- Optionally send feedback here if needed, but usually purchases are only possible in InGame via shop GUI return -- Ignore purchase if not in game end print("GameServer: Received BuyTroop request from", player.Name, "for", troopType, "at", placementPosition, "via building", clickedBuilding and clickedBuilding.Name or "nil") -- 1. --- VALIDATION --- local boardModel = nil local matchId = Matchmaker.playerMatchMap[player.UserId] -- Get matchId from map if matchId then local matchInfo = Matchmaker.activeMatches[matchId] if matchInfo then boardModel = matchInfo.boardModel -- Get the correct board for this match end end if not boardModel then warn("GameServer: Could not find board model for player", player.Name, "'s match (BuyTroop).") SendFeedbackMessage:FireClient(player, "Match board not found.") -- Feedback return end -- Validate the clicked building instance received from the client if not clickedBuilding or not clickedBuilding:IsA("BasePart") or not clickedBuilding.Parent or not CollectionService:HasTag(clickedBuilding, PRODUCTION_BUILDING_TAG) then warn("GameServer: Invalid or untagged building instance received from", player.Name) SendFeedbackMessage:FireClient(player, "Invalid building selected for placement.") -- **Feedback** return -- Ignore invalid request end -- **NEW: Check if the clicked building belongs to the player requesting the purchase** local buildingOwnerId = clickedBuilding:GetAttribute("OwnerUserId") if buildingOwnerId ~= player.UserId then warn("GameServer: Player", player.Name, "tried to buy from building owned by", buildingOwnerId) SendFeedbackMessage:FireClient(player, "You can only buy from your own buildings.") -- **Feedback** return end -- Check if the troop type is valid local troopStats = TroopManager.TROOP_TYPES[troopType] if not troopStats then warn("GameServer: Player", player.Name, "requested invalid troop type:", troopType) SendFeedbackMessage:FireClient(player, "Invalid troop type selected.") -- **Feedback** return -- Ignore invalid request end -- Check if player has enough money local playerMoney = player:GetAttribute("Money") or 0 local troopCost = troopStats.Cost or math.huge if playerMoney < troopCost then warn("GameServer: Player", player.Name, "does not have enough money to buy", troopType) SendFeedbackMessage:FireClient(player, "Not enough money to buy this troop.") -- **Feedback** return -- Ignore request end -- Check if the placement position is on a valid board tile *on the correct board* local targetTile = BoardManager:GetTileAtWorldPos(placementPosition) if not targetTile or not targetTile.Parent or targetTile.Parent ~= boardModel then warn("GameServer: Invalid placement position: Not on a tile or wrong board.") SendFeedbackMessage:FireClient(player, "Placement position is not on the board.") -- **Feedback** return -- Ignore invalid request end -- Check if the target tile is within the spawn radius of the clicked building local buildingTile = BoardManager:GetTileAtWorldPos(clickedBuilding.Position) -- Also check if the tile found for the building is actually part of the correct match board if not buildingTile or not buildingTile.Parent or buildingTile.Parent ~= boardModel then warn("GameServer: Could not determine tile for clicked building:", clickedBuilding.Name, "on correct board.") SendFeedbackMessage:FireClient(player, "Error determining building location.") -- **Feedback** return -- Cannot validate proximity end local buildingTileX = buildingTile:GetAttribute("TileX") local buildingTileZ = buildingTile:GetAttribute("TileZ") local targetTileX = targetTile:GetAttribute("TileX") local targetTileZ = targetTile:GetAttribute("TileZ") if buildingTileX == nil or buildingTileZ == nil or targetTileX == nil or targetTileZ == nil then warn("GameServer: Tile coordinates missing attributes for proximity check.") SendFeedbackMessage:FireClient(player, "Error checking placement proximity.") -- **Feedback** return -- Cannot validate proximity end local distance = math.abs(buildingTileX - targetTileX) + math.abs(buildingTileZ - targetTileZ) print("GameServer: Calculated placement distance from building:", distance) if distance <= 0 or distance > SPAWN_RADIUS then warn("GameServer: Invalid placement distance from building for", troopType, "- Distance:", distance) SendFeedbackMessage:FireClient(player, "Placement must be near the building.") -- **Feedback** return -- Ignore request end -- Check if Target Tile is Occupied by ANOTHER TROOP (within the match context) local overlapParams = OverlapParams.new() overlapParams.FilterDescendantsInstances = {boardModel, clickedBuilding} -- Ignore board and clicked building overlapParams.FilterType = Enum.RaycastFilterType.Exclude -- Expand the check slightly vertically to catch things resting on the tile local checkRegion = Region3.new(targetTile.Position - Vector3.new(targetTile.Size.X/2, 0.5, targetTile.Size.Z/2), targetTile.Position + Vector3.new(targetTile.Size.X/2, troopStats.Size.Y + 0.5, targetTile.Size.Z/2)) local occupants = Workspace:FindPartsInRegion3WithIgnoreList(checkRegion, overlapParams.FilterDescendantsInstances, 100) local isOccupiedByTroop = false for _, part in ipairs(occupants) do if part:IsA("BasePart") and part:GetAttribute("Owner") then -- Check for troop owner attribute print("GameServer: Target tile", targetTile.Name, "is occupied by another troop:", part.Name) isOccupiedByTroop = true break end end if isOccupiedByTroop then warn("GameServer: Target tile", targetTile.Name, "is occupied by a troop. Placement denied.") SendFeedbackMessage:FireClient(player, "Target tile is already occupied by a troop.") -- **Feedback** return -- Ignore request end -- 2. --- EXECUTION --- print("GameServer: Buy and placement validated for", troopType, "at", placementPosition, "near", clickedBuilding.Name) -- Deduct Money player:SetAttribute("Money", playerMoney - troopCost) print("GameServer: Deducted $", troopCost, "from", player.Name, ". New balance:", player:GetAttribute("Money")) -- Fire event to update client GUI UpdateMoneyEvent:FireClient(player, player:GetAttribute("Money")) -- Create the troop instance local newTroop = TroopManager:CreateTroop(troopType, placementPosition, player) if newTroop then print("GameServer:", troopType, "created successfully for", player.Name, "at", placementPosition) -- TroopManager should parent the troop to Workspace. Consider parenting to matchModel? -- newTroop.Parent = matchModel -- Optional: Parent troop to match model for easier cleanup else warn("GameServer: Failed to create troop instance for", troopType) SendFeedbackMessage:FireClient(player, "Failed to create troop.") -- **Feedback** -- Optional: Refund money if creation failed? end end) print("GameServer: Initialization complete. Matchmaking system integrated.")