Matchmaking Issue – GameReset Signal Not Interrupting Dialogue Sequence Properly

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

maybe seeing this working in a test .rbxl or an open source testing place my help to further understand the issue and possible solutions.

if you want, i can add you to my project so we can hop in and test it

when you exit before the first message appears:

when yout exit after the first message appears:

I would start, but adding print debug statements after every check condition, like the IF statements to see where it gets into and where it does not… and also print out variables so you know what is getting set.

Do you think the method I’m using with “task.delay” is a good one? How do you think big experiences use it for their matchmaking systems?

I do not know, you have 12 task.delays , you would have to look at each one and know why your are doing it that way and what other ways are there to achieve it… if the task delay is queueing it up, and the player leaves, and then it is queued up and plays that could relate to the issue…

but like I said above, add the debugging 101 steps to see where the issue is at…

if the task.delay is part of the issue, then maybe inside of the thing it is going to execute , after the task time has started, you might need to check if there is some condition it should not now start the task, and instead exit…

but if it has started executing, then stopping it , depending on what it is might be harder…

anyhow… more debugging to see where it is at

I also sent you a private dm if you want to look into the issue more