-- Services
local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
local TextService = game:GetService("TextService")
local RunService = game:GetService("RunService")
local SoundService = game:GetService("SoundService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- References
local player = Players.LocalPlayer
local gui = player:WaitForChild("PlayerGui"):WaitForChild("MiniGames"):WaitForChild("TypingMinigame")
local seat = workspace:WaitForChild("House"):WaitForChild("ArcadeGames"):WaitForChild("TypingMinigame"):WaitForChild("Seat")
local background = gui:WaitForChild("Background")

local promptDisplayer = background:WaitForChild("PromptDisplayer")
local exitButton = background.ExitBackground:WaitForChild("ExitButton")
local moneyDisplayer = background:WaitForChild("MoneyDisplayer")
local streakDisplayer = background:WaitForChild("StreakDisplayer")
local multiplierDisplayer = background:WaitForChild("MultiplierDisplayer")
local wpmDisplayer = background:WaitForChild("WPMDisplayer")

local arcadeMusic = SoundService.Arcades.Typing:WaitForChild("ArcadeMusic")
local sounds = SoundService.Arcades.Typing.Keys
local exitSound = SoundService.Arcades.Typing.Exit

local mainTheme = SoundService:FindFirstChild("MainMusic")

local leaderstats = player:WaitForChild("leaderstats")
local dollars = leaderstats:WaitForChild("Dollars")
local wordBank = require(ReplicatedStorage:WaitForChild("Modules"):WaitForChild("WordBank"))

-- State
local sentence = {}
local maxWordsVisible = 10
local wordIndex = 1
local totalTypedWords = 0
local startTime = 0
local streak = 0
local typingMultiplier = 1
local arcadeActive = false
local originalWalkSpeed = 16
local originalJumpPower = 50

-- Format money
local function formatMoney(n)
	local suffixes = {"", "K", "M", "B", "T", "Qa", "Qi"}
	local i = 1
	while n >= 1000 and i < #suffixes do
		n /= 1000
		i += 1
	end
	return ("$%.2f%s"):format(n, suffixes[i])
end

-- Reset sentence
local function resetSentence()
	sentence = {}
	for _ = 1, maxWordsVisible do
		table.insert(sentence, {
			text = wordBank[math.random(1, #wordBank)],
			typed = "",
			status = "pending",
			cursorPos = 1,
		})
	end
	wordIndex = 1
	sentence[wordIndex].status = "active"
	totalTypedWords = 0
	startTime = tick()
end

-- Render sentence
local function renderSentence()
	local renderText = ""

	for i, wordObj in ipairs(sentence) do
		local word = wordObj.text
		local typed = wordObj.typed
		local cursorPos = wordObj.cursorPos
		local status = wordObj.status
		local wordRender = ""

		for j = 1, #word do
			local actualChar = word:sub(j, j):lower()
			local typedChar = typed:sub(j, j):lower()
			local color = "rgb(0,0,0)"

			if status == "correct" then
				color = "rgb(36,255,20)"
			elseif status == "incorrect" then
				color = "rgb(255,100,100)"
			elseif status == "active" then
				if j <= #typed then
					color = (typedChar == actualChar) and "rgb(36,255,20)" or "rgb(255,100,100)"
				end
				if j == cursorPos then
					color = "rgb(50,150,255)"
				end
			end

			wordRender ..= ("<font color='%s'>%s</font>"):format(color, word:sub(j, j))
		end

		if status == "active" and cursorPos > #word then
			wordRender ..= "<font color='rgb(50,150,255)'>_</font>"
		end

		renderText ..= wordRender
		if i < #sentence then renderText ..= " " end
	end

	promptDisplayer.Text = renderText
end

-- Animate UI pop
local function animatePop(ui)
	local originalSize = ui.Size
	local big = TweenService:Create(ui, TweenInfo.new(0.15), {
		Size = originalSize + UDim2.new(0.05, 0, 0.05, 0)
	})
	local reset = TweenService:Create(ui, TweenInfo.new(0.15), {
		Size = originalSize
	})
	big:Play()
	big.Completed:Wait(.5)
	reset:Play()
end

-- Apply money reward
local function applyReward()
	local bonus = player:GetAttribute("MoneyBonus") or 0
	local upgradeMult = player:GetAttribute("MoneyMultiplier") or 1
	local gain = math.floor((1 + bonus) * upgradeMult * typingMultiplier)

	-- Secure server update
	ReplicatedStorage.Remotes:WaitForChild("AddMoneyFromTyping"):FireServer(gain)

	-- Show +$ gain visibly
	moneyDisplayer.Text = "+$" .. string.format("%.0f", gain or 0)
	animatePop(moneyDisplayer)

	task.delay(4, function()
		moneyDisplayer.Text = formatMoney(dollars.Value)
	end)

	local s = sounds.Correct:Clone()
	s.Parent = workspace
	s:Play()
	game.Debris:AddItem(s, 2)
end

-- Update UI stats
local function updateStats()
	moneyDisplayer.Text = formatMoney(dollars.Value)
	streakDisplayer.Text = streak .. " Streak"
	multiplierDisplayer.Text = typingMultiplier .. "x"

	local timeElapsed = math.max(1, tick() - startTime)
	local wpm = ((totalTypedWords / timeElapsed) * 60)
	wpmDisplayer.Text = ("%.1f WPM"):format(wpm)
end

-- Update streak
local function updateStreak()
	streak += 1
	if streak == 10 then typingMultiplier = 2 animatePop(multiplierDisplayer)
	elseif streak == 20 then typingMultiplier = 3 animatePop(multiplierDisplayer)
	elseif streak == 30 then typingMultiplier = 4 animatePop(multiplierDisplayer)
	end
end

-- Lock movement
local function lockMovement(lock)
	local char = player.Character
	if not char then return end
	local hum = char:FindFirstChildWhichIsA("Humanoid")
	if not hum then return end

	if lock then
		originalWalkSpeed = hum.WalkSpeed
		originalJumpPower = hum.JumpPower
		hum.WalkSpeed = 0
		hum.JumpPower = 0
	else
		hum.WalkSpeed = originalWalkSpeed
		hum.JumpPower = originalJumpPower
	end
end

-- Start arcade
local function startArcade()
	gui.Enabled = true
	arcadeActive = true
	background.Size = UDim2.new(0.01, 0, 0.01, 0)
	TweenService:Create(background, TweenInfo.new(0.4), {
		Size = UDim2.new(1, 0, 1, 0)
	}):Play()

	if mainTheme then mainTheme:Stop() end

	arcadeMusic.Looped = true
	arcadeMusic:Play()
	UserInputService.MouseIconEnabled = true

	lockMovement(true)
	resetSentence()
	renderSentence()
	updateStats()
end

-- Stop arcade
local function stopArcade()
	local char = player.Character
	local hum = char and char:FindFirstChildWhichIsA("Humanoid")

	if hum then
		hum.Sit = false
		wait(0.1)
	end

	lockMovement(false)

	local camera = workspace.CurrentCamera
	if camera and hum then
		camera.CameraSubject = hum
	end

	wait(0.1)

	gui.Enabled = false
	arcadeActive = false
	arcadeMusic:Stop()

	if mainTheme then mainTheme:Play() end

	UserInputService.MouseIconEnabled = true
	exitSound:Play()
end

-- Exit button
exitButton.MouseButton1Click:Connect(stopArcade)

-- Arcade trigger
RunService.RenderStepped:Connect(function()
	local char = player.Character
	local hum = char and char:FindFirstChildWhichIsA("Humanoid")
	if hum and hum.SeatPart == seat and not arcadeActive then
		hum.Jump = false
		hum.AutoJumpEnabled = false
		startArcade()
	end
end)

-- Typing input
UserInputService.InputBegan:Connect(function(input, gameProcessed)
	if not arcadeActive or gameProcessed then return end
	if input.UserInputType ~= Enum.UserInputType.Keyboard then return end

	local key = input.KeyCode
	local currentWord = sentence[wordIndex]
	if not currentWord then return end

	if key == Enum.KeyCode.Left then
		currentWord.cursorPos = math.max(1, currentWord.cursorPos - 1)
		renderSentence()
		return
	elseif key == Enum.KeyCode.Right then
		currentWord.cursorPos = math.min(#(currentWord.typed or "") + 1, currentWord.cursorPos + 1)
		renderSentence()
		return
	elseif key == Enum.KeyCode.Backspace then
		local pos = currentWord.cursorPos
		if pos > 1 then
			currentWord.typed = currentWord.typed:sub(1, pos - 2) .. currentWord.typed:sub(pos)
			currentWord.cursorPos -= 1
		end
		sounds.Key:Clone():Play()
		renderSentence()
		updateStats()
		return
	elseif key == Enum.KeyCode.Space then
		if #(currentWord.typed or "") > 0 then
			local typed = currentWord.typed:lower()
			local target = currentWord.text:lower()

			if typed == target then
				currentWord.status = "correct"
				applyReward()
				updateStreak()
			else
				currentWord.status = "incorrect"
				streak = 0
				typingMultiplier = 1
				local s = sounds.Incorrect:Clone()
				s.Parent = workspace
				s:Play()
				game.Debris:AddItem(s, 2)
			end

			wordIndex += 1
			totalTypedWords += 1

			if wordIndex > #sentence then
				resetSentence()
			else
				sentence[wordIndex].status = "active"
			end

			renderSentence()
			updateStats()
		end
		return
	end

	local letter = tostring(key):match("Enum.KeyCode.(%w)")
	if letter and #letter == 1 and letter:match("[a-zA-Z]") then
		letter = letter:lower()
		local pos = currentWord.cursorPos
		currentWord.typed = currentWord.typed:sub(1, pos - 1) .. letter .. currentWord.typed:sub(pos)
		currentWord.cursorPos += 1

		sounds.Key:Clone():Play()
		renderSentence()
		updateStats()
	end
end)


This works really well, however I would love a lot of improvements and better feedback on it. Only use the existing ui and dont add notifications or any of that sort. Make it look smoother, and since im using keybinds for my game make it so that any other existing keybind doesnt work and the keybinds work again after you exit the typing game. Make everything look smoother and add like particles and effects for everything, For example if I do a word wrong then an explosion sound plays and theres an explosion on the word.