mmbvn
🧩 Syntax:
--[[
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.")