-
-
Save RoyallyFlushed/e032269a57ed12944f2736df40ee6818 to your computer and use it in GitHub Desktop.
The moonball code for Man City Blue Moon
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
TITLE: Moonball Service Server Module | |
DESC: This module script is responsible for implementing | |
the entire server-side functionality for a game of | |
moonball. | |
Specifically, facilitating the starting of matches, | |
their direction and validation of actions, as well | |
as replication to clients. | |
AUTHOR: -- | |
CONTRIBS: | |
- Reid @RoyallyFlushed: 13/02/2024 Major Revamp for Tournament System | |
CREATED: --/--/---- | |
MODIFIED: 25/03/2024 | |
--]] | |
--// Constants | |
local MOONBALL_SCOREBOARD_GUI_PATH = "$(Assets)Moonball/MoonballMatchScoreboardGui" | |
--// Service Constants (All constants defined in ReplicatedStorage/Game/Miscellaneous/MoonballTypes) | |
local MOONBALL_STATE | |
local MAX_PLAYERS_PER_TEAM | |
local TOTAL_MATCH_TIME_S | |
local POINTS_PER_GOAL | |
local POINTS_PER_WIN | |
local BALL_OWNERSHIP_TIME_S | |
local GOAL_SCORED_DELAY_TIME_S | |
local PRE_MATCH_CUTSCENE_TIME_S | |
local BALL_TIME_POLL_INTERVAL_S | |
local BLUE_TEAM | |
local YELLOW_TEAM | |
--// Services | |
local ReplicatedStorage = game:GetService("ReplicatedStorage") | |
local Players = game:GetService("Players") | |
local RunService = game:GetService("RunService") | |
--// Modules | |
local Utility = require(ReplicatedStorage.Game.Miscellaneous.Utility) | |
local debounce = Utility.debounce | |
local Path = Utility.Path | |
local MoonballTypes = require(ReplicatedStorage.Game.Miscellaneous.MoonballTypes) | |
local GameDirectorTypes = require(ReplicatedStorage.Game.Miscellaneous.GameDirectorTypes) | |
--// Types | |
type Array<T> = Utility.Array<T> | |
type MoonballMatch = MoonballTypes.MoonballMatch | |
type Team = MoonballTypes.Team | |
type GameObject = GameDirectorTypes.GameObject | |
--// Knit | |
local Knit = require(ReplicatedStorage.Packages.Knit) | |
local Signal = require(Knit.Util.Signal) | |
local DataService | |
local QuestService | |
local LeaderboardService | |
local AnalyticsService | |
local MilestoneTrackerService | |
local MoonballService = Knit.CreateService{ | |
Name = "MoonballService", | |
Client = { | |
UpdateMatchState = Knit.CreateSignal(), | |
MatchStarted = Knit.CreateSignal(), | |
MatchEnded = Knit.CreateSignal(), | |
StartOvertime = Knit.CreateSignal(), | |
StartCountdown = Knit.CreateSignal(), | |
PlayerScored = Knit.CreateSignal(), | |
UpdateTimer = Knit.CreateSignal(), | |
}, | |
Connections = setmetatable({}, { __mode = "k" }) :: Array<RBXScriptConnection>, | |
Matches = {} :: Array<MoonballMatch>, | |
BlueGoals = 0, | |
YellowGoals = 0, | |
} | |
--// Set Constants | |
MOONBALL_STATE = MoonballTypes.MOONBALL_STATE | |
MAX_PLAYERS_PER_TEAM = MoonballTypes.MAX_PLAYERS_PER_TEAM | |
TOTAL_MATCH_TIME_S = MoonballTypes.TOTAL_MATCH_TIME_S | |
POINTS_PER_GOAL = MoonballTypes.POINTS_PER_GOAL | |
POINTS_PER_WIN = MoonballTypes.POINTS_PER_WIN | |
BALL_OWNERSHIP_TIME_S = MoonballTypes.BALL_OWNERSHIP_TIME_S | |
GOAL_SCORED_DELAY_TIME_S = MoonballTypes.GOAL_SCORED_DELAY_TIME_S | |
PRE_MATCH_CUTSCENE_TIME_S = MoonballTypes.PRE_MATCH_CUTSCENE_TIME_S | |
BALL_TIME_POLL_INTERVAL_S = MoonballTypes.BALL_TIME_POLL_INTERVAL_S | |
BLUE_TEAM = MoonballTypes.BLUE_TEAM | |
YELLOW_TEAM = MoonballTypes.YELLOW_TEAM | |
--// Variables | |
local moonballWorkspaceFolder = workspace.Scriptables.Moonball | |
local moonballAssetsFolder = ReplicatedStorage.Assets.Moonball | |
local moonballScoreboardGui = Path(MOONBALL_SCOREBOARD_GUI_PATH, nil, { PanicOnFailure = true }) | |
--[[ | |
//////////////////////////////////////// | |
|| INTERNAL API || | |
//////////////////////////////////////// | |
--]] | |
local function getPlayersInMatch(match: MoonballMatch): Array<Player>? | |
if not match or typeof(match) ~= "table" then | |
return | |
end | |
local result: Array<Player> = {} | |
for _, team in { BLUE_TEAM, YELLOW_TEAM } do | |
for _, player in match.Teams[team] do | |
table.insert(result, player) | |
end | |
end | |
return result | |
end | |
local function getMatchFromPitchIndex(self, pitchIndex: number): MoonballMatch? | |
for _, match: MoonballMatch in self.Matches do | |
if match.PitchIndex == pitchIndex then | |
return match | |
end | |
end | |
end | |
local function getMatchFromPlayer(self, targetPlayer: Player): MoonballMatch? | |
for _, match: MoonballMatch in self.Matches do | |
for _, team in { BLUE_TEAM, YELLOW_TEAM } do | |
for _, player in match.Teams[team] do | |
if player == targetPlayer then | |
return match | |
end | |
end | |
end | |
end | |
end | |
local function isPitchEmpty(self, pitchIndex: number): boolean | |
return getMatchFromPitchIndex(self, pitchIndex) == nil | |
end | |
local function teleportPlayerToPitch(match: MoonballMatch, player: Player, playerIndex: number, teamName: string) | |
local teamSpawnsFolder: Folder? = match.PitchFolder.Spawns[teamName] | |
local character = player.Character or player.CharacterAdded:Wait() | |
local humanoid = character:WaitForChild("Humanoid") | |
local spawnLocation: SpawnLocation? = teamSpawnsFolder[playerIndex] or teamSpawnsFolder:GetChildren()[1] | |
local newCFrame = CFrame.lookAt(spawnLocation.Position, match.PitchFolder.CentrePiece.Position) | |
character:PivotTo(newCFrame) | |
end | |
function setupTeamAndTeleportToPitch(self, match: MoonballMatch) | |
for _, teamName: string in { BLUE_TEAM, YELLOW_TEAM } do | |
local teamAccessory: Accessory? = moonballAssetsFolder:FindFirstChild(teamName .. "Accessory") | |
for i, player: Player in match.Teams[teamName] do | |
task.spawn(function() | |
if not player or not player.Parent then | |
return | |
end | |
self.Client.UpdateMatchState:Fire(player, match) | |
self.Client.MatchStarted:Fire(player, match) | |
AnalyticsService:MoonballMatchStarted(player) | |
task.wait(1) | |
if not player or not player.Parent then | |
return | |
end | |
local character = player.Character or player.CharacterAdded:Wait() | |
local humanoid = character:WaitForChild("Humanoid") | |
teleportPlayerToPitch(match, player, i, teamName) | |
if teamAccessory then | |
humanoid:AddAccessory(teamAccessory:Clone()) | |
end | |
end) | |
end | |
end | |
end | |
local function updateMatchScoreboard(self, match: MoonballMatch) | |
if not match or not match.PitchFolder or not match.PitchFolder:FindFirstChild("Scoreboard") then | |
return | |
end | |
local scoreboardPart: BasePart = match.PitchFolder.Scoreboard | |
local scoreboardGui: SurfaceGui? = scoreboardPart:FindFirstChild(moonballScoreboardGui.Name) | |
if not scoreboardGui then | |
scoreboardGui = moonballScoreboardGui:Clone() | |
scoreboardGui.Parent = scoreboardPart | |
end | |
local blueScoreLabel: TextLabel? = Path("$/Main/Body/Blue/Score", scoreboardGui) | |
local yellowScoreLabel: TextLabel? = Path("$/Main/Body/Yellow/Score", scoreboardGui) | |
if blueScoreLabel then | |
blueScoreLabel.Text = match.Goals.Blue.Total | |
end | |
if yellowScoreLabel then | |
yellowScoreLabel.Text = match.Goals.Yellow.Total | |
end | |
end | |
local function replicateMatchState(self, match: MoonballMatch) | |
for _, team in { BLUE_TEAM, YELLOW_TEAM } do | |
for _, player in match.Teams[team] do | |
self.Client.UpdateMatchState:Fire(player, match) | |
end | |
end | |
end | |
local function matchDestroyEvents(self, match: MoonballMatch) | |
if not match or not self.Connections[match] then | |
return | |
end | |
for _, connection in self.Connections[match] do | |
if typeof(connection) == "RBXScriptConnection" then | |
connection:Disconnect() | |
end | |
end | |
end | |
local function matchDestroyBalls(match: MoonballMatch) | |
if not match then | |
return | |
end | |
match.PitchFolder.Balls:ClearAllChildren() | |
end | |
local function matchAddGoal(self, match: MoonballMatch, teamThatScored: string, scoringPlayer: Player): boolean | |
if not match or not teamThatScored or not scoringPlayer then | |
return | |
end | |
local scoringPlayerTeam: string | |
for _, team in { BLUE_TEAM, YELLOW_TEAM } do | |
for _, player in match.Teams[team] do | |
if player == scoringPlayer then | |
scoringPlayerTeam = team | |
break | |
end | |
end | |
end | |
-- Increment the total goals for the team that scored | |
match.Goals[teamThatScored].Total += 1 | |
-- If the scoring player is on the same team that scored, then increment their goals too | |
if teamThatScored == scoringPlayerTeam then | |
match.Goals[scoringPlayerTeam].Players[scoringPlayer] += 1 | |
end | |
-- Give each player the new game state | |
replicateMatchState(self, match) | |
-- Display match scores on the scoreboard | |
updateMatchScoreboard(self, match) | |
end | |
local function matchSetState(self, match: MoonballMatch, newState: number) | |
if not match or not newState then | |
return | |
end | |
match.State = newState | |
replicateMatchState(self, match) | |
end | |
local function matchStartBallFailSafe(self, match: MoonballMatch) | |
-- Create a listener for the ball being destroyed, to spawn one in if the game isn't paused or over | |
table.insert(self.Connections[match], match.PitchFolder.Balls.ChildRemoved:Connect(function() | |
if not match | |
or match.State ~= MOONBALL_STATE.IN_PROGRESS -- We use in-progress rather than paused to account for win states | |
then | |
return | |
end | |
if #match.PitchFolder.Balls:GetChildren() == 0 then | |
self:CreateBall(match) | |
end | |
end)) | |
end | |
local function matchStartBallTimeRecording(self, match: MoonballMatch) | |
local blueAdvantageBox: BasePart = match.PitchFolder.BlueAdvantageBox | |
local yellowAdvantageBox: BasePart = match.PitchFolder.YellowAdvantageBox | |
local ballTimeRecordingOverlapParams = OverlapParams.new() | |
ballTimeRecordingOverlapParams.FilterDescendantsInstances = match.PitchFolder.Balls:GetChildren() | |
ballTimeRecordingOverlapParams.FilterType = Enum.RaycastFilterType.Include | |
local currentAdvantage | |
table.insert(self.Connections[match], RunService.Heartbeat:Connect(debounce(function(dt: number) | |
if match.State ~= MOONBALL_STATE.IN_PROGRESS then | |
return | |
end | |
local ballsInBlueBox: Array<BasePart> = workspace:GetPartBoundsInBox(blueAdvantageBox.CFrame, blueAdvantageBox.Size, ballTimeRecordingOverlapParams) | |
local ballsInYellowBox: Array<BasePart> = workspace:GetPartBoundsInBox(yellowAdvantageBox.CFrame, yellowAdvantageBox.Size, ballTimeRecordingOverlapParams) | |
-- If the ball is in either the blue or yellow, then add time based on if it was there in the previous iteration | |
if #ballsInBlueBox > 0 then | |
if currentAdvantage == BLUE_TEAM then | |
match.BallTimes.Blue += BALL_TIME_POLL_INTERVAL_S | |
end | |
currentAdvantage = BLUE_TEAM | |
elseif #ballsInYellowBox > 0 then | |
if currentAdvantage == YELLOW_TEAM then | |
match.BallTimes.Yellow += BALL_TIME_POLL_INTERVAL_S | |
end | |
currentAdvantage = YELLOW_TEAM | |
end | |
task.wait(BALL_TIME_POLL_INTERVAL_S) | |
end))) | |
end | |
local function createMatch(self, gameObject: GameObject, pitchIndex: number, teamBluePlayers: Array<Player>, teamYellowPlayers: Array<Player>): MoonballMatch? | |
if not pitchIndex or typeof(pitchIndex) ~= "number" then | |
return | |
end | |
local pitchFolder: Folder? = moonballWorkspaceFolder:FindFirstChild(tostring(pitchIndex)) | |
if not pitchFolder then | |
return | |
end | |
if not isPitchEmpty(self, pitchIndex) then | |
return | |
end | |
local newMatch: MoonballMatch = { | |
GameObject = gameObject, | |
PitchIndex = pitchIndex, | |
PitchFolder = pitchFolder, | |
Teams = { | |
Blue = {}, | |
Yellow = {}, | |
}, | |
Goals = { | |
Blue = { | |
Total = 0, | |
Players = {}, | |
}, | |
Yellow = { | |
Total = 0, | |
Players = {}, | |
}, | |
}, | |
BallTimes = { | |
Blue = 0, | |
Yellow = 0 | |
}, | |
Phase = 1, | |
State = MOONBALL_STATE.NOT_STARTED, | |
TimeStarted = -1, | |
TimeFinished = -1 | |
} | |
-- Populate the players into the match | |
for _, player in teamBluePlayers do | |
table.insert(newMatch.Teams.Blue, player) | |
newMatch.Goals.Blue.Players[player] = 0 | |
end | |
for _, player in teamYellowPlayers do | |
table.insert(newMatch.Teams.Yellow, player) | |
newMatch.Goals.Yellow.Players[player] = 0 | |
end | |
table.insert(self.Matches, newMatch) | |
-- Reset the scoreboard | |
updateMatchScoreboard(self, newMatch) | |
return newMatch | |
end | |
local function startMatch(self, match: MoonballMatch): MoonballMatch | |
self.Connections[match] = {} | |
-- Teleport the teams to the pitch | |
self:TeleportTeamsToPitch(match) | |
-- Wait for the cutscene to finish running | |
task.wait(PRE_MATCH_CUTSCENE_TIME_S) | |
-- Tell all the clients to start the countdown | |
for _, player: Player in getPlayersInMatch(match) do | |
self.Client.StartCountdown:Fire(player) | |
end | |
-- Remove everything from the balls folder | |
matchDestroyBalls(match) | |
-- Create a new ball | |
self:CreateBall(match) | |
matchStartBallFailSafe(self, match) | |
-- Wait for the match to finish | |
self:WaitToFinish(match) | |
-- End the match | |
local winners = self:EndMatch(match) | |
-- Disconnect any events | |
matchDestroyEvents(self, match) | |
-- Reset the connections table just in case Roblox doesn't | |
self.Connections[match] = nil | |
return match | |
end | |
local function onPlayerRemoving(self, targetPlayer: Player) | |
local match: MoonballMatch? = getMatchFromPlayer(self, targetPlayer) | |
if not match then | |
return | |
end | |
local shouldBreak = false | |
for _, team in { BLUE_TEAM, YELLOW_TEAM } do | |
for i, player in match.Teams[team] do | |
if player == targetPlayer then | |
table.remove(match.Teams[team], i) | |
shouldBreak = true | |
break | |
end | |
end | |
if shouldBreak then | |
break | |
end | |
end | |
end | |
--[[ | |
//////////////////////////////////////// | |
|| SERVER API || | |
//////////////////////////////////////// | |
--]] | |
function MoonballService:KnitStart() | |
DataService = Knit.GetService("DataService") | |
QuestService = Knit.GetService("QuestService") | |
LeaderboardService = Knit.GetService("LeaderboardService") | |
AnalyticsService = Knit.GetService("AnalyticsService") | |
CurrencyService = Knit.GetService("CurrencyService") | |
MilestoneTrackerService = Knit.GetService("MilestoneTrackerService") | |
Players.PlayerRemoving:Connect(function(...) | |
onPlayerRemoving(self, ...) | |
end) | |
end | |
function MoonballService:CreateBall(match: MoonballMatch) | |
local newBall = moonballAssetsFolder.Ball:Clone() | |
newBall.Anchored = true | |
newBall.CFrame = match.PitchFolder.CentrePiece.CFrame + Vector3.new(0, 4.75, 0) -- Offset the ball so it sits on top of the pitch | |
newBall.Parent = match.PitchFolder.Balls | |
newBall.Anchored = false | |
local _GOAL_NAMES = { | |
[match.PitchFolder.BlueGoal] = YELLOW_TEAM, | |
[match.PitchFolder.YellowGoal] = BLUE_TEAM | |
} | |
-- Create a .Touched connection to check if the ball has entered the goal, if so, handle appropriately | |
local newBallConnection | |
newBallConnection = newBall.Touched:Connect(function(part: BasePart) | |
local team: string? = _GOAL_NAMES[part] | |
if not team then | |
return | |
end | |
newBallConnection:Disconnect() | |
local lastTouchedPlayerName: string? = newBall:GetAttribute("LastTouched") | |
self:GoalScored(match, lastTouchedPlayerName, team) | |
end) | |
table.insert(self.Connections[match], newBallConnection) | |
end | |
function MoonballService:TeleportTeamsToPitch(match: MoonballMatch) | |
setupTeamAndTeleportToPitch(self, match) | |
end | |
function MoonballService:TeleportTeamsAfterScore(match: MoonballMatch) | |
for _, teamName: string in { BLUE_TEAM, YELLOW_TEAM } do | |
for i: number, player: Player in match.Teams[teamName] do | |
task.spawn(function() | |
if not player or not player.Parent then | |
return | |
end | |
teleportPlayerToPitch(match, player, i, teamName) | |
end) | |
end | |
end | |
end | |
function MoonballService:ChangeCameras(match: MoonballMatch, playerNameWhoScored: string, shouldWatchScorer: boolean) | |
-- Loop through all players of the match, and tell each client that a player scored a goal | |
for _, player: Player in getPlayersInMatch(match) do | |
if not player or not player.Parent then | |
continue | |
end | |
task.spawn(function() | |
self.Client.PlayerScored:Fire(player, playerNameWhoScored, shouldWatchScorer) | |
end) | |
end | |
end | |
function MoonballService:CreateMatch(gameObject: GameObject, pitchIndex: number, teamBluePlayers: Array<Player>, teamYellowPlayers: Array<Player>): MoonballMatch? | |
local teamBluePlayersTrimmed = {} | |
local teamYellowPlayersTrimmed = {} | |
-- Truncate the players to be the maximum number of players per team | |
for i = 1, math.min(#teamBluePlayers, MAX_PLAYERS_PER_TEAM) do | |
table.insert(teamBluePlayersTrimmed, teamBluePlayers[i]) | |
end | |
for i = 1, math.min(#teamYellowPlayers, MAX_PLAYERS_PER_TEAM) do | |
table.insert(teamYellowPlayersTrimmed, teamYellowPlayers[i]) | |
end | |
return createMatch(self, gameObject, pitchIndex, teamBluePlayersTrimmed, teamYellowPlayersTrimmed) | |
end | |
function MoonballService:StartMatch(pitchIndex: number): MoonballMatch | |
local match: MoonballMatch? = getMatchFromPitchIndex(self, pitchIndex) | |
if not match then | |
return | |
end | |
match.TimeStarted = DateTime.now() | |
return startMatch(self, match) | |
end | |
function MoonballService:EndMatch(match: MoonballMatch) | |
-- Destroy any events for the match | |
matchDestroyEvents(self, match) | |
-- Destroy any balls | |
matchDestroyBalls(match) | |
match.TimeFinished = DateTime.now() | |
local matchResult: number = matchEvaluateState(match) | |
local winningTeamName: string | |
matchSetState(self, match, matchResult) | |
if matchResult == MOONBALL_STATE.WIN_TEAM_BLUE then | |
winningTeamName = BLUE_TEAM | |
elseif matchResult == MOONBALL_STATE.WIN_TEAM_YELLOW then | |
winningTeamName = YELLOW_TEAM | |
end | |
local highestScore: number = -1 | |
local highestScoringPlayer: Player | |
-- Calculate the highest scoring player for the entire match | |
for _, team in { BLUE_TEAM, YELLOW_TEAM } do | |
for _, player in match.Teams[team] do | |
local playerGoals: number = match.Goals[team].Players[player] | |
if playerGoals > 0 then | |
CurrencyService:IncrementCoins(player, playerGoals * POINTS_PER_GOAL) | |
end | |
if team == winningTeamName then | |
CurrencyService:IncrementCoins(player, POINTS_PER_WIN) | |
end | |
if playerGoals > highestScore then | |
highestScore = playerGoals | |
highestScoringPlayer = player | |
end | |
end | |
end | |
-- Loop through each player in each team, fire analytics, and reset the player | |
for _, team: string in { BLUE_TEAM, YELLOW_TEAM } do | |
for _, player in match.Teams[team] do | |
self.Client.MatchEnded:Fire(player, winningTeamName, highestScoringPlayer.Name) | |
QuestService:AddResource(player, "Moonball") | |
AnalyticsService:MoonballMatchFinished(player) | |
-- Do character stuff in pcall in case character isn't available | |
pcall(function() | |
if match.PitchFolder:FindFirstChild("ExitLocation") then | |
player.Character:PivotTo(match.PitchFolder.ExitLocation.CFrame) | |
else | |
player.Character:PivotTo(workspace.Spawns:GetChildren()[1].CFrame) | |
end | |
local teamAccessory: Accessory? = player.Character:FindFirstChild(team .. "Accessory") | |
if teamAccessory then | |
teamAccessory:Destroy() | |
end | |
end) | |
end | |
end | |
self:ResetPitch(match) | |
end | |
function matchEvaluateState(match: MoonballMatch) | |
if match.Goals.Blue.Total == match.Goals.Yellow.Total then | |
-- If the match is in the 2nd phase or more (overtime phase), then we need to find a definitive winner | |
if match.Phase >= 2 then | |
if match.BallTimes.Blue > match.BallTimes.Yellow then | |
return MOONBALL_STATE.WIN_TEAM_BLUE | |
elseif match.BallTimes.Yellow > match.BallTimes.Blue then | |
return MOONBALL_STATE.WIN_TEAM_YELLOW | |
else | |
return math.random(MOONBALL_STATE.WIN_TEAM_BLUE, MOONBALL_STATE.WIN_TEAM_YELLOW) -- Fallback to random winner (unlikely to ever happen) | |
end | |
end | |
-- At this point, there are no recorded values, so return a draw (Will only ever be used for 1st part of the game) | |
return MOONBALL_STATE.DRAW | |
elseif match.Goals.Blue.Total > match.Goals.Yellow.Total then | |
return MOONBALL_STATE.WIN_TEAM_BLUE | |
else | |
return MOONBALL_STATE.WIN_TEAM_YELLOW | |
end | |
end | |
function shouldFinishWaiting(match: MoonballMatch) | |
return match.State ~= MOONBALL_STATE.PAUSED | |
or #match.Teams.Blue == 0 | |
or #match.Teams.Yellow == 0 | |
end | |
function MoonballService:WaitToFinish(match: MoonballMatch) | |
local blueTeam = match.Teams.Blue | |
local yellowTeam = match.Teams.Yellow | |
for i = TOTAL_MATCH_TIME_S, 0, -1 do | |
-- If one of the teams has no players, then skip countdown and end the match | |
if #blueTeam == 0 or #yellowTeam == 0 then | |
break | |
end | |
-- If the game is paused, then wait for the game to be unpaused, or players on a team to leave | |
if match.State == MOONBALL_STATE.PAUSED then | |
repeat | |
task.wait() | |
until shouldFinishWaiting(match) | |
end | |
match.GameObject.Time = i | |
for _, team in { BLUE_TEAM, YELLOW_TEAM } do | |
for _, player in match.Teams[team] do | |
self.Client.UpdateTimer:Fire(player, i) | |
end | |
end | |
task.wait(1) | |
end | |
-- Disconnect any events associated with the match | |
matchDestroyEvents(self, match) | |
-- Allow a buffer of 1 second to account for players scoring a goal right at the very end of the match | |
task.wait(1) | |
-- If the game is paused, then wait for the game to be unpaused, or players on a team to leave | |
if match.State == MOONBALL_STATE.PAUSED then | |
repeat | |
task.wait() | |
until shouldFinishWaiting(match) | |
end | |
-- If there are no players, then exit | |
if #blueTeam == 0 or #yellowTeam == 0 then | |
return | |
end | |
local tentativeResult: number = matchEvaluateState(match) | |
-- If the result of the game isn't a draw, then exit | |
if tentativeResult ~= MOONBALL_STATE.DRAW then | |
return | |
end | |
-- At this point, the game will be a draw, so start the overtime phase of the match, and start recording the ball time | |
match.Phase += 1 | |
self:TeleportTeamsAfterScore(match) | |
for _, player: Player in getPlayersInMatch(match) do | |
self.Client.StartOvertime:Fire(player) | |
end | |
matchDestroyBalls(match) | |
matchSetState(self, match, MOONBALL_STATE.IN_PROGRESS) | |
self:CreateBall(match) | |
matchStartBallFailSafe(self, match) | |
matchStartBallTimeRecording(self, match) | |
for i = TOTAL_MATCH_TIME_S, 0, -1 do | |
-- If one of the teams has no players, then skip countdown and end the match | |
if #blueTeam == 0 or #yellowTeam == 0 then | |
break | |
end | |
-- If the game is paused, then wait for the game to be unpaused, or players on a team to leave | |
if match.State == MOONBALL_STATE.PAUSED then | |
repeat | |
task.wait() | |
until shouldFinishWaiting(match) | |
end | |
match.GameObject.Time = i | |
for _, team in { BLUE_TEAM, YELLOW_TEAM } do | |
for _, player in match.Teams[team] do | |
self.Client.UpdateTimer:Fire(player, i) | |
end | |
end | |
task.wait(1) | |
end | |
matchDestroyEvents(self, match) | |
-- Allow a buffer of 1 second to account for players scoring a goal right at the very end of the match | |
task.wait(1) | |
-- If the game is paused, then wait for the game to be unpaused, or players on a team to leave | |
if match.State == MOONBALL_STATE.PAUSED then | |
repeat | |
task.wait() | |
until shouldFinishWaiting(match) | |
end | |
local tentativeResult: number = matchEvaluateState(match) | |
-- The match should never end in a draw! | |
if tentativeResult == MOONBALL_STATE.DRAW then | |
task.spawn(error, "MoonballService -> Match Ended in a Draw! Impossible!") | |
end | |
end | |
function MoonballService:GoalScored(match: MoonballMatch, playerNameWhoScored: string, teamThatScored: string) | |
matchSetState(self, match, MOONBALL_STATE.PAUSED) | |
-- Pcall function to register goal scored for player in all services | |
local success, err = pcall(function() | |
local player: Player? = Players:FindFirstChild(playerNameWhoScored) | |
if not player then | |
return | |
end | |
matchAddGoal(self, match, teamThatScored, player) | |
DataService:GetProfile(player).Data.GoalsScored["allTime"] += 1 | |
QuestService:AddResource(player, "Goals") | |
LeaderboardService:RegisterGoals(player, 1) | |
AnalyticsService:AddGoal(player) | |
MilestoneTrackerService:ContributeToMilestone(player, "CommunityGoals", 1) | |
match.PitchFolder.Balls:FindFirstChild("Ball").Main.ParticleEmitter:Emit(200) | |
end) | |
if not success then | |
warn(err) | |
end | |
-- Heave each player look at the player who scored | |
self:ChangeCameras(match, playerNameWhoScored, true) | |
-- Wait a little bit after scoring the goal before resetting the pitch | |
task.wait(GOAL_SCORED_DELAY_TIME_S) | |
-- Reset players (Teleport back to team spawns, and reset camera) | |
self:TeleportTeamsAfterScore(match) | |
self:ChangeCameras(match, nil, false) | |
-- Remove the ball from the pitch, and create a new one | |
matchDestroyBalls(match) | |
-- Wait for 0.1 seconds to allow the previous balls to be destroyed before we set the match state | |
task.wait(0.1) | |
-- Unpause the game | |
matchSetState(self, match, MOONBALL_STATE.IN_PROGRESS) | |
-- Spawn a ball in | |
self:CreateBall(match) | |
end | |
function MoonballService:ResetPitch(match: MoonballMatch) | |
matchDestroyBalls(match) | |
for matchIndex, activeMatch: MoonballMatch in self.Matches do | |
if activeMatch.PitchIndex == match.PitchIndex then | |
table.remove(self.Matches, matchIndex) | |
break | |
end | |
end | |
end | |
function MoonballService:IsPlayerInMatch(player: Player): boolean | |
return getMatchFromPlayer(self, player) ~= nil | |
end | |
function MoonballService:GetMatchFromPlayer(player: Player): MoonballMatch? | |
return getMatchFromPlayer(self, player) | |
end | |
function MoonballService:GetPlayersInMatch(match: MoonballMatch): Array<Player>? | |
return getPlayersInMatch(match) | |
end | |
--[[ | |
//////////////////////////////////////// | |
|| CLIENT API || | |
//////////////////////////////////////// | |
--]] | |
function MoonballService.Client:GetPlayersInMatch(player: Player, match: MoonballMatch): Array<Player>? | |
return self.Server:GetPlayersInMatch(match) | |
end | |
function MoonballService.Client:RequestOwnership(player: Player, ball: BasePart): boolean | |
if not ball then | |
return false | |
end | |
local match: Match? = getMatchFromPlayer(self.Server, player) | |
-- If the player isn't in a match, then exit | |
if not match then | |
return false | |
end | |
-- If the ball the player is requesting isn't part of the player's match, or is in cooldown, then exit | |
if not ball:IsDescendantOf(match.PitchFolder) or ball:GetAttribute("Cooldown") then | |
return false | |
end | |
-- Set network ownership of the ball to the player | |
ball:SetNetworkOwner(player) | |
ball:SetAttribute("Cooldown", true) | |
ball:SetAttribute("LastTouched", player.Name) | |
task.delay(BALL_OWNERSHIP_TIME_S, function() | |
ball:SetAttribute("Cooldown", false) | |
end) | |
return true | |
end | |
function MoonballService.Client:IsPlayerInMatch(client: Player, targetPlayer: Player) | |
return self.Server:IsPlayerInMatch(targetPlayer) | |
end | |
return MoonballService |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment