Skip to content

Instantly share code, notes, and snippets.

@RoyallyFlushed
Created July 1, 2024 14:25
Show Gist options
  • Save RoyallyFlushed/e032269a57ed12944f2736df40ee6818 to your computer and use it in GitHub Desktop.
Save RoyallyFlushed/e032269a57ed12944f2736df40ee6818 to your computer and use it in GitHub Desktop.
The moonball code for Man City Blue Moon
--[[
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