I’m currently working on my first matchmaking system, and I’ve run into a persistent issue that I haven’t been able to resolve, despite trying several approaches. I’d really appreciate some guidance or clarification on what I might be doing wrong.
Here’s the situation:
When the intermission ends and the minimum number of players is met (for example, just me and my friend, since the minimum is 2), everything seems to work fine—unless one player leaves right at the millisecond before the typewriter dialogue effect begins. In this case, the server correctly detects the player left, sends a GameReset signal, the UI shows “NOT ENOUGH PLAYERS”, and the game state returns to “waiting” as expected.
However, the dialogue sequence continues playing in the background—messages keep appearing and animations progress all the way to the final blue button animation. The timeout never starts because the match was canceled, but the UI elements aren’t being properly reset or stopped.
Now, when I test it by waiting until the first message appears and the typewriter effect starts, and then a player leaves, everything works perfectly: the message disappears, the skip dialogue frame vanishes, and the UI resets completely.
What I want to achieve is this:
Any time the server sends a GameReset signal, I need the dialogue typing effect and any other running UI sequences or animations to be fully interrupted and stopped immediately.
Since this is my first time building a matchmaking system, I might be missing something fundamental, so if there’s a more reliable or standard way to handle these cases and avoid bugs like this, I’d be very grateful for your suggestions.
local GameService = {}
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local NotificationService = require(ReplicatedStorage.Modules.NotificationService)
local SoundService = require(ReplicatedStorage.Modules.SoundService)
local Constants = require(ReplicatedStorage.Modules.GameService.Constants)
local UpdateState = ReplicatedStorage.Remotes.GameService.UpdateState
local Sync = ReplicatedStorage.Remotes.GameService.Sync
local TeamSelect = ReplicatedStorage.Remotes.GameService.TeamSelect
local player = Players.LocalPlayer
local currentVotedButton = nil
local teamSelectionCountdown = nil
local notificationTimer = nil
local currentTeamSelectionData = nil
local teamButtonConnections = {red = nil, blue = nil}
local messageUpdateConnections = {}
local hasVotedSkip = false
local skipButtonConnection = nil
function GameService.init()
local player = Players.LocalPlayer
local status = player:WaitForChild("Status")
local playerGui = player:WaitForChild("PlayerGui")
local gameUI = playerGui:WaitForChild("Game")
local notEnoughPlayersLabel = gameUI:WaitForChild("NotEnoughPlayers")
local intermissionFrame = gameUI:WaitForChild("Intermission")
local teamSelectorFrame = gameUI:WaitForChild("TeamSelector")
local intermissionScale = intermissionFrame:WaitForChild("UIScale")
local titleLabel = intermissionFrame:WaitForChild("Title")
local themesFrame = intermissionFrame:WaitForChild("Themes")
notEnoughPlayersLabel.Visible = true
intermissionFrame.Visible = false
teamSelectorFrame.Visible = false
intermissionScale.Scale = -1
GameService.setupThemeButtons(themesFrame)
UpdateState.OnClientEvent:Connect(function(state)
if state.type == "GameState" then
notEnoughPlayersLabel.Visible = not state.hasEnoughPlayers
if state.state == Constants.GAME_STATES.TEAM_SELECTION or
state.state == Constants.GAME_STATES.IN_PROGRESS then
notEnoughPlayersLabel.Visible = false
end
if state.state == "INTERMISSION" then
if not intermissionFrame.Visible then
intermissionFrame.Visible = true
intermissionScale.Scale = -1
local scaleTween = TweenService:Create(
intermissionScale,
TweenInfo.new(1, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
{Scale = 1}
)
scaleTween:Play()
end
titleLabel.Text = string.format("INTERMISSION %d", state.timeRemaining)
else
intermissionFrame.Visible = false
intermissionScale.Scale = -1
end
elseif state.type == "VoteUpdate" then
GameService.updateVoteCount(themesFrame, state.votes)
elseif state.type == "TeamSelectionTime" then
notEnoughPlayersLabel.Visible = false
local timeout = teamSelectorFrame.Timeout
local selected = teamSelectorFrame.Selected
if not selected.Visible then
timeout.Visible = true
timeout.Text = string.format("%d", state.timeRemaining)
if state.timeRemaining <= 10 and state.timeRemaining > 0 then
SoundService.Play("UI.GameService.Countdown")
end
end
if selected.Visible then
local fillBar = selected.TimeLeft.Fill
fillBar.Size = UDim2.new(state.progress, 0, 1, 0)
end
elseif state.type == "TeamSelected" then
local teamSelectorFrame = player.PlayerGui.Game.TeamSelector
local redButton = teamSelectorFrame.Red
local blueButton = teamSelectorFrame.Blue
local timeout = teamSelectorFrame.Timeout
local selected = teamSelectorFrame.Selected
redButton.Visible = false
blueButton.Visible = false
timeout.Visible = false
if not state.autoAssigned then
selected.Visible = true
local teamColor = state.team
local teamText = selected.TextLabel
local colorCode = teamColor == "RED" and '<font color="rgb(255,0,0)">' or '<font color="rgb(0,0,255)">'
teamText.Text = string.format("YOU'RE ON THE %s%s</font> TEAM", colorCode, teamColor)
if currentTeamSelectionData and currentTeamSelectionData.options then
local optionName = teamColor == "RED" and currentTeamSelectionData.options.red.name or currentTeamSelectionData.options.blue.name
local coloredOption = string.format('<font color="rgb(%d,%d,%d)">%s</font>',
teamColor == "RED" and 255 or 0,
0,
teamColor == "BLUE" and 255 or 0,
optionName)
NotificationService.ShowNotification(string.format('<font color="rgb(0,255,0)">Selected %s successfully</font>', coloredOption), "Grant", 1.5)
end
else
local teamColor = state.team
local colorCode = teamColor == "RED" and '<font color="rgb(255,0,0)">' or '<font color="rgb(0,0,255)">'
local message = string.format("You have been automatically assigned to %s%s</font> TEAM", colorCode, teamColor)
NotificationService.ShowNotification(message, "Info", 2)
end
elseif state.type == "TeamSelectError" then
NotificationService.ShowNotification(state.message, "Error", 1.5)
elseif state.type == "StartGameMessage" then
GameService.handleStartGameMessage(state)
elseif state.type == "StartGameCountdown" then
GameService.handleGameCountdown(state)
elseif state.type == "MatchUpdate" then
local scoreboard = player.PlayerGui.HUD.Bottom.Scoreboard
local timeLeft = scoreboard.MATCH_DURATION.TextLabel
local minutes = math.floor(state.timeLeft / 60)
local seconds = state.timeLeft % 60
local newTimeText = string.format("%02d:%02d", minutes, seconds)
if timeLeft.Text ~= newTimeText then
local originalSize = timeLeft.Size
local expandedSize = UDim2.new(originalSize.X.Scale * 1.15, 0, originalSize.Y.Scale * 1.15, 0)
local expandTween = TweenService:Create(
timeLeft,
TweenInfo.new(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
{Size = expandedSize}
)
local contractTween = TweenService:Create(
timeLeft,
TweenInfo.new(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.In),
{Size = originalSize}
)
timeLeft.Text = newTimeText
expandTween:Play()
expandTween.Completed:Connect(function()
contractTween:Play()
end)
end
end
end)
Sync.OnClientEvent:Connect(function(data)
if data then
if data.type == "SkipVotesUpdate" then
local skipMessagesFrame = player.PlayerGui.HUD.Bottom.SkipMessages
local skipButton = skipMessagesFrame.Frame.TextButton
skipButton.Text = string.format("%d/%d", data.votes, data.requiredVotes or data.totalPlayers)
elseif data.type == "VoteSkipConfirmation" then
NotificationService.ShowNotification("Your vote to skip dialog was counted successfully", "Grant", 1.5)
elseif data.type == "DialogSkippedNotification" then
NotificationService.ShowNotification("Dialog has been skipped by team vote", "Info", 2.5)
elseif data.type == "SkipDialog" then
for _, connection in ipairs(messageUpdateConnections) do
if connection then
connection:Disconnect()
end
end
messageUpdateConnections = {}
local teamSelectorFrame = player.PlayerGui.Game.TeamSelector
local messages = teamSelectorFrame.Messages
local skipMessagesFrame = player.PlayerGui.HUD.Bottom.SkipMessages
if messages then
messages.Visible = false
messages.Text = ""
end
skipMessagesFrame.Visible = false
local redButton = teamSelectorFrame.Red
local blueButton = teamSelectorFrame.Blue
local redTween = TweenService:Create(
redButton,
TweenInfo.new(1, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out),
{Position = UDim2.new(0.264, 0, 0.499, 0)}
)
redTween:Play()
task.delay(1.5, function()
local blueTween = TweenService:Create(
blueButton,
TweenInfo.new(1, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out),
{Position = UDim2.new(0.736, 0, 0.499, 0)}
)
blueTween:Play()
end)
elseif data.type == "CancelGameStart" then
GameService.resetUI(data.resetComplete)
elseif data.type == "SpecialNotification" then
GameService.showSpecialNotification(data.notificationType, data.data)
elseif data.type == "GameReset" then
GameService.resetUI(data.resetComplete)
local themesFrame = player.PlayerGui.Game.Intermission.Themes
GameService.updateVoteCount(themesFrame, data.votes)
elseif data.type == "ErrorNotification" and data.errorType == "GameStateError" then
NotificationService.ShowNotification(data.message, "Error", 2)
end
end
end)
TeamSelect.OnClientEvent:Connect(function(data)
if data.type == "StartTeamSelection" then
notEnoughPlayersLabel.Visible = false
local intermissionTween = TweenService:Create(
intermissionScale,
TweenInfo.new(1, Enum.EasingStyle.Quad),
{Scale = -1}
)
intermissionTween:Play()
intermissionTween.Completed:Wait()
task.wait(1)
intermissionFrame.Visible = false
teamSelectorFrame.Visible = true
GameService.handleTeamSelection(teamSelectorFrame, data)
end
end)
local TextChatRemote = ReplicatedStorage.Remotes.TextChatService
local TextChatService = game:GetService("TextChatService")
TextChatRemote.OnClientEvent:Connect(function(data)
if TextChatService and TextChatService:FindFirstChild("TextChannels") and
TextChatService.TextChannels:FindFirstChild("RBXSystem") then
local systemChannel = TextChatService.TextChannels.RBXSystem
local formattedMessage = string.format(
'<font color="rgb(%d,%d,%d)">%s</font>',
data.color.R * 255,
data.color.G * 255,
data.color.B * 255,
data.message
)
systemChannel:DisplaySystemMessage(formattedMessage)
end
end)
end
function GameService.handleStartGameMessage(state)
local playerGui = player.PlayerGui
local inGameUI = playerGui.Game.InGame
local messagesLabel = inGameUI:FindFirstChild("Messages")
local teamSelectorFrame = playerGui.Game.TeamSelector
local selected = teamSelectorFrame.Selected
selected.Visible = false
if not messagesLabel then
return
end
messagesLabel.Visible = true
messagesLabel.Text = ""
local message = state.message
local playerOption = state.playerOptions[tostring(player.UserId)]
if not playerOption then
return
end
local serverStartTime = state.startTimestamp
local typingSpeed = state.typingSpeed or 0.05
local pauseDuration = state.pauseDuration or 2
local totalDuration = state.totalDuration
local clientStartTime = os.time()
local timeOffset = clientStartTime - serverStartTime
local function syncedTypewriterWithPause(text, option, teamColor)
local pausePosition = string.find(text, "???")
local totalChars = #text
local effectiveLength = totalChars
if pausePosition then
local beforePause = text:sub(1, pausePosition - 1)
local afterPause = text:sub(pausePosition + 3)
local formattedOption = string.format('<font color="rgb(%d,%d,%d)">%s</font>',
teamColor.R * 255, teamColor.G * 255, teamColor.B * 255, option)
effectiveLength = #beforePause + #formattedOption + #afterPause
end
local showCursor = true
local cursorBlinkSpeed = 0.5
local lastCursorBlink = os.clock()
local currentText = ""
local SoundService = require(ReplicatedStorage.Modules.SoundService)
local expectedTotalTime = totalChars * typingSpeed
if pausePosition then
expectedTotalTime = expectedTotalTime + pauseDuration
end
local startTime = os.clock() - timeOffset
local pauseTriggered = false
local updateConnection
updateConnection = RunService.RenderStepped:Connect(function()
if not messagesLabel.Visible then
if updateConnection then
updateConnection:Disconnect()
updateConnection = nil
end
return
end
local now = os.clock()
local elapsed = now - startTime
if now - lastCursorBlink >= cursorBlinkSpeed then
showCursor = not showCursor
lastCursorBlink = now
end
local targetIndex = math.floor(elapsed / typingSpeed)
if pausePosition and targetIndex >= pausePosition and not pauseTriggered then
pauseTriggered = true
local beforePause = text:sub(1, pausePosition - 1)
local afterPause = text:sub(pausePosition + 3)
local formattedOption = string.format('<font color="rgb(%d,%d,%d)">%s</font>',
teamColor.R * 255, teamColor.G * 255, teamColor.B * 255, option)
text = beforePause .. formattedOption .. afterPause
currentText = beforePause .. formattedOption
targetIndex = #beforePause + #formattedOption
end
if not pauseTriggered or (pauseTriggered and elapsed > (pausePosition * typingSpeed + pauseDuration)) then
if targetIndex > #currentText and targetIndex <= #text then
SoundService.Play("UI.GameService.Dialogue")
currentText = text:sub(1, targetIndex)
end
end
messagesLabel.Text = currentText .. (showCursor and "|" or "")
if elapsed >= expectedTotalTime + 2.0 or targetIndex >= #text + 10 then
if updateConnection then
updateConnection:Disconnect()
updateConnection = nil
end
messagesLabel.Visible = false
end
end)
task.delay(expectedTotalTime + 4.0, function()
if updateConnection then
updateConnection:Disconnect()
updateConnection = nil
messagesLabel.Visible = false
end
end)
end
syncedTypewriterWithPause(message, playerOption.option, playerOption.teamColor)
end
function GameService.handleGameCountdown(state)
local inGameUI = player.PlayerGui.Game.InGame
local countdown = inGameUI.Countdown
countdown.Visible = true
local SoundService = require(ReplicatedStorage.Modules.SoundService)
local serverStartTime = state.startTimestamp
local clientStartTime = os.time()
local timeOffset = clientStartTime - serverStartTime
local countdownSequence = {
{text = "3", color = Color3.fromRGB(4, 255, 0), duration = 1},
{text = "2", color = Color3.fromRGB(255, 247, 0), duration = 1},
{text = "1", color = Color3.fromRGB(255, 147, 52), duration = 1},
{text = "GO!", color = Color3.fromRGB(255, 0, 0), duration = 1}
}
local totalDuration = 0
task.delay(0.1, function()
if countdown.Visible then
SoundService.Play("UI.GameService.GameCountdown")
end
end)
for i, step in ipairs(countdownSequence) do
task.delay(totalDuration, function()
if not countdown.Visible then
return
end
countdown.Text = step.text
countdown.TextColor3 = step.color
if i == #countdownSequence then
task.delay(step.duration, function()
if not countdown.Visible then
return
end
countdown.Visible = false
local scoreboard = player.PlayerGui.HUD.Bottom.Scoreboard
scoreboard.Visible = true
GameService.setupScoreboard(state.redTeam, state.blueTeam)
end)
end
end)
totalDuration = totalDuration + step.duration
end
end
function GameService.resetUI(resetComplete)
for _, connection in ipairs(messageUpdateConnections) do
if connection then
connection:Disconnect()
end
end
messageUpdateConnections = {}
local playerGui = player.PlayerGui
local gameUI = playerGui:WaitForChild("Game")
local notEnoughPlayersLabel = gameUI:WaitForChild("NotEnoughPlayers")
local intermissionFrame = gameUI:WaitForChild("Intermission")
local teamSelectorFrame = gameUI:WaitForChild("TeamSelector")
local inGameUI = gameUI:WaitForChild("InGame")
local skipMessagesFrame = playerGui.HUD.Bottom.SkipMessages
hasVotedSkip = false
skipMessagesFrame.Visible = false
local skipButton = skipMessagesFrame.Frame.TextButton
skipButton.BackgroundColor3 = Color3.fromRGB(229, 0, 0)
skipButton.Text = "0/0"
if skipButtonConnection then
skipButtonConnection:Disconnect()
skipButtonConnection = nil
end
notEnoughPlayersLabel.Visible = true
intermissionFrame.Visible = false
teamSelectorFrame.Visible = false
if currentVotedButton then
currentVotedButton.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
currentVotedButton.BackgroundTransparency = 0.55
currentVotedButton = nil
end
if teamSelectionCountdown then
teamSelectionCountdown:Disconnect()
teamSelectionCountdown = nil
end
for _, connection in pairs(teamButtonConnections) do
if connection then
connection:Disconnect()
end
end
teamButtonConnections = {red = nil, blue = nil}
if notificationTimer then
task.cancel(notificationTimer)
notificationTimer = nil
end
local notificationFrame = inGameUI:WaitForChild("SpecialNotification")
local notificationText = notificationFrame:WaitForChild("TextLabel")
notificationFrame.Visible = false
notificationText.TextTransparency = 0
notificationFrame.BackgroundTransparency = 0
notificationFrame.Position = UDim2.new(0.5, 0, 0.148, 0)
local scoreboard = playerGui.HUD.Bottom.Scoreboard
scoreboard.Visible = false
for _, child in pairs(scoreboard.Red:GetChildren()) do
if child.Name ~= "_Template" and not child:IsA("UIListLayout") then
child:Destroy()
end
end
for _, child in pairs(scoreboard.Blue:GetChildren()) do
if child.Name ~= "_Template" and not child:IsA("UIListLayout") then
child:Destroy()
end
end
local messagesLabel = inGameUI:FindFirstChild("Messages")
if messagesLabel then
messagesLabel.Visible = false
messagesLabel.Text = ""
end
local countdown = inGameUI.Countdown
countdown.Visible = false
local selected = teamSelectorFrame.Selected
selected.Visible = false
teamSelectorFrame.Red.Visible = true
teamSelectorFrame.Blue.Visible = true
teamSelectorFrame.Red.Position = UDim2.new(-0.264, 0, 0.499, 0)
teamSelectorFrame.Blue.Position = UDim2.new(1.736, 0, 0.499, 0)
teamSelectorFrame.Timeout.Visible = false
currentTeamSelectionData = nil
if resetComplete then
notEnoughPlayersLabel.Visible = true
NotificationService.ShowNotification("Game reset complete. Ready for a new match!", "Info", 2)
end
end
function GameService.setupThemeButtons(themesFrame)
for _, button in themesFrame:GetChildren() do
if button:IsA("TextButton") then
button.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
button.BackgroundTransparency = 0.55
button.Votes.Visible = false
button.MouseButton1Click:Connect(function()
if currentVotedButton then
currentVotedButton.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
currentVotedButton.BackgroundTransparency = 0.55
end
button.BackgroundColor3 = Color3.fromRGB(17, 255, 0)
button.BackgroundTransparency = 0
currentVotedButton = button
UpdateState:FireServer({
type = "Vote",
theme = button.Name
})
NotificationService.ShowNotification(string.format("Voted for %s theme successfully", button.Name), "Grant", 1.5)
end)
end
end
end
function GameService.updateVoteCount(themesFrame, votes)
for _, button in themesFrame:GetChildren() do
if button:IsA("TextButton") then
local votesFrame = button.Votes
local votesCount = votesFrame.VotesCount
local count = votes[button.Name] or 0
votesFrame.Visible = count > 0
votesCount.Text = tostring(count)
end
end
end
function GameService.setupScoreboard(redTeam, blueTeam)
local scoreboard = player.PlayerGui.HUD.Bottom.Scoreboard
scoreboard.Red.TextLabel.Text = tostring(#redTeam)
scoreboard.Blue.TextLabel.Text = tostring(#blueTeam)
local timeLeft = scoreboard.MATCH_DURATION.TextLabel
local minutes = math.floor(Constants.MATCH_DURATION / 60)
local seconds = Constants.MATCH_DURATION % 60
timeLeft.Text = string.format("%02d:%02d", minutes, seconds)
end
function GameService.handleTeamSelection(teamSelectionFrame, data)
for _, connection in ipairs(messageUpdateConnections) do
if connection then
connection:Disconnect()
end
end
messageUpdateConnections = {}
local messages = teamSelectionFrame.Messages
local redButton = teamSelectionFrame.Red
local blueButton = teamSelectionFrame.Blue
local timeout = teamSelectionFrame.Timeout
local selected = teamSelectionFrame.Selected
local SoundService = require(ReplicatedStorage.Modules.SoundService)
local skipMessagesFrame = player.PlayerGui.HUD.Bottom.SkipMessages
local skipFrame = skipMessagesFrame.Frame
local skipButton = skipFrame.TextButton
hasVotedSkip = false
skipButton.BackgroundColor3 = Color3.fromRGB(229, 0, 0)
local activePlayers = 0
for _, p in ipairs(Players:GetPlayers()) do
if not p:FindFirstChild("Status") or not p.Status:FindFirstChild("AFK") or not p.Status.AFK.Value then
activePlayers = activePlayers + 1
end
end
local requiredVotes
if activePlayers == 2 then
requiredVotes = 2
else
requiredVotes = math.ceil(activePlayers / 2)
end
skipButton.Text = string.format("0/%d", requiredVotes)
skipMessagesFrame.Visible = true
skipFrame.Position = UDim2.new(0.5, 0, 2, 0)
local skipFrameTween = TweenService:Create(
skipFrame,
TweenInfo.new(0.5, Enum.EasingStyle.Linear, Enum.EasingDirection.Out),
{Position = UDim2.new(0.5, 0, 0.5, 0)}
)
skipFrameTween:Play()
if skipButtonConnection then
skipButtonConnection:Disconnect()
end
skipButtonConnection = skipButton.MouseButton1Click:Connect(function()
if not hasVotedSkip then
hasVotedSkip = true
skipButton.BackgroundColor3 = Color3.fromRGB(11, 229, 0)
UpdateState:FireServer({
type = "SkipVote"
})
SoundService.Play("UI.GameService.ButtonClick")
end
end)
currentTeamSelectionData = data
messages.Visible = true
timeout.Visible = false
selected.Visible = false
redButton.Position = UDim2.new(-0.264, 0, 0.499, 0)
blueButton.Position = UDim2.new(1.736, 0, 0.499, 0)
redButton.Preview.Image = "rbxassetid://" .. data.options.red.imageId
redButton:WaitForChild("Name").Text = data.options.red.name
blueButton.Preview.Image = "rbxassetid://" .. data.options.blue.imageId
blueButton:WaitForChild("Name").Text = data.options.blue.name
local function setupTeamButton(button, teamColor)
if teamButtonConnections[string.lower(teamColor)] then
teamButtonConnections[string.lower(teamColor)]:Disconnect()
end
local background = button.Background
local gradient = background.UIGradient
local currentConnection
button.MouseEnter:Connect(function()
if currentConnection then
currentConnection:Disconnect()
end
local startTime = os.clock()
local duration = 0.3
currentConnection = RunService.RenderStepped:Connect(function()
local elapsed = os.clock() - startTime
local alpha = math.clamp(elapsed/duration, 0, 1)
gradient.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1 - alpha),
NumberSequenceKeypoint.new(1, 1)
})
if alpha >= 1 then
currentConnection:Disconnect()
currentConnection = nil
end
end)
end)
button.MouseLeave:Connect(function()
if currentConnection then
currentConnection:Disconnect()
end
local startTime = os.clock()
local duration = 0.3
currentConnection = RunService.RenderStepped:Connect(function()
local elapsed = os.clock() - startTime
local alpha = math.clamp(elapsed/duration, 0, 1)
gradient.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, alpha),
NumberSequenceKeypoint.new(1, 1)
})
if alpha >= 1 then
currentConnection:Disconnect()
currentConnection = nil
end
end)
end)
teamButtonConnections[string.lower(teamColor)] = button.MouseButton1Click:Connect(function()
UpdateState:FireServer({
type = "TeamSelect",
team = teamColor
})
end)
end
setupTeamButton(redButton, "RED")
setupTeamButton(blueButton, "BLUE")
local function syncedTypewriterEffect(text, isWarning, startDelay, duration)
local delayTask = task.delay(startDelay, function()
messages.Text = ""
local startTime = os.clock()
local typingSpeed = 0.05
local totalCharacters = #text
local expectedDuration = totalCharacters * typingSpeed
if duration and duration > 0 then
typingSpeed = duration / totalCharacters
end
local showCursor = true
local cursorBlinkSpeed = 0.5
local lastCursorBlink = startTime
local currentText = ""
local isTyping = true
local targetIndex = 0
local updateConnection
updateConnection = RunService.RenderStepped:Connect(function()
local now = os.clock()
local elapsed = now - startTime
targetIndex = math.floor(elapsed / typingSpeed)
if targetIndex > totalCharacters then
targetIndex = totalCharacters
end
if targetIndex > #currentText then
if targetIndex > 0 and targetIndex <= totalCharacters then
SoundService.Play("UI.GameService.Dialogue")
end
currentText = text:sub(1, targetIndex)
end
if now - lastCursorBlink >= cursorBlinkSpeed then
showCursor = not showCursor
lastCursorBlink = now
end
messages.Text = currentText .. (showCursor and "|" or "")
if targetIndex >= totalCharacters and elapsed >= expectedDuration then
isTyping = false
if elapsed >= expectedDuration + (isWarning and 2.0 or 2.0) then
if updateConnection then
updateConnection:Disconnect()
updateConnection = nil
end
messages.Text = ""
end
end
end)
table.insert(messageUpdateConnections, updateConnection)
local maxTime = expectedDuration + (isWarning and 2.0 or 2.0) + 0.5
task.delay(maxTime, function()
if updateConnection then
updateConnection:Disconnect()
updateConnection = nil
end
end)
end)
table.insert(messageUpdateConnections, {
Disconnect = function()
task.cancel(delayTask)
end
})
end
local totalMessageTime = 0
local currentDelay = 0
for i, message in ipairs(data.messages) do
local messageLength = #message
local typingDuration = messageLength * 0.05
local totalDuration = typingDuration + 2.0
syncedTypewriterEffect(message, false, currentDelay, typingDuration)
currentDelay = currentDelay + totalDuration
end
local warningLength = #data.warning
local warningTypingDuration = warningLength * 0.05
syncedTypewriterEffect(data.warning, true, currentDelay, warningTypingDuration)
local animationStartTime = data.animationStartTime or (currentDelay + warningTypingDuration + 2.0 + 1.5)
local buttonAnimTask = task.delay(animationStartTime, function()
messages.Visible = false
skipMessagesFrame.Visible = false
local redTween = TweenService:Create(
redButton,
TweenInfo.new(1, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out),
{Position = UDim2.new(0.264, 0, 0.499, 0)}
)
redTween:Play()
task.delay(1.5, function()
local blueTween = TweenService:Create(
blueButton,
TweenInfo.new(1, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out),
{Position = UDim2.new(0.736, 0, 0.499, 0)}
)
blueTween:Play()
end)
end)
table.insert(messageUpdateConnections, {
Disconnect = function()
task.cancel(buttonAnimTask)
end
})
end
function GameService.showSpecialNotification(notificationType, data)
local playerGui = player.PlayerGui
local inGameUI = playerGui:WaitForChild("Game"):WaitForChild("InGame")
local notificationFrame = inGameUI:WaitForChild("SpecialNotification")
local notificationText = notificationFrame:WaitForChild("TextLabel")
local gradient = notificationFrame:WaitForChild("UIGradient")
if notificationTimer then
task.cancel(notificationTimer)
notificationTimer = nil
end
notificationFrame.Visible = true
notificationText.TextTransparency = 0
notificationFrame.BackgroundTransparency = 0
if notificationType == "Kill" then
notificationText.Text = string.format("%s KILLED BY %s", data.killed, data.killer)
gradient.Color = ColorSequence.new(Color3.fromRGB(255, 0, 0))
SoundService.Play("UI.GameService.SpecialNotification")
notificationTimer = task.delay(4, function()
local textFadeTween = TweenService:Create(
notificationText,
TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
{TextTransparency = 1}
)
local frameFadeTween = TweenService:Create(
notificationFrame,
TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
{BackgroundTransparency = 1}
)
textFadeTween:Play()
frameFadeTween:Play()
frameFadeTween.Completed:Connect(function()
notificationFrame.Visible = false
end)
end)
elseif notificationType == "TeamVictory" then
notificationText.Text = string.format("TEAM %s WINS THE MATCH!", data.team)
gradient.Color = ColorSequence.new(Color3.fromRGB(13, 255, 0))
local scoreboard = player.PlayerGui.HUD.Bottom.Scoreboard
if scoreboard then
scoreboard.Visible = false
end
notificationTimer = task.delay(6, function()
local textFadeTween = TweenService:Create(
notificationText,
TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
{TextTransparency = 1}
)
local frameFadeTween = TweenService:Create(
notificationFrame,
TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
{BackgroundTransparency = 1}
)
textFadeTween:Play()
frameFadeTween:Play()
frameFadeTween.Completed:Connect(function()
notificationFrame.Visible = false
end)
end)
end
notificationFrame.Position = UDim2.new(0.5, 0, 0.148, 0)
end
return GameService