local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local Player = Players.LocalPlayer
local PlayerGui = Player:WaitForChild("PlayerGui")
local function new(class, props)
local obj = Instance.new(class)
if props then
for k,v in pairs(props) do
obj[k] = v
end
end
return obj
end
local math_random = math.random
math.randomseed(tick() % 2147483647)
local screenGui = new("ScreenGui", {Name="WPMTesterGUI", ResetOnSpawn=false, Parent=PlayerGui, ZIndexBehavior=Enum.ZIndexBehavior.Sibling})
local mainFrame = new("Frame", {
Parent = screenGui,
Name = "MainFrame",
Size = UDim2.new(0,540,0,420),
Position = UDim2.new(0.5,-270,0.5,-210),
BackgroundColor3 = Color3.fromRGB(18,18,18),
BorderSizePixel = 0,
})
mainFrame.Active = true
mainFrame.Draggable = true
new("UICorner", {Parent = mainFrame, CornerRadius = UDim.new(0,10)})
local header = new("Frame", {
Parent = mainFrame,
Size = UDim2.new(1,0,0,48),
Position = UDim2.new(0,0,0,0),
BackgroundTransparency = 1,
})
local headerLabel = new("TextLabel", {
Parent = header,
Size = UDim2.new(1,-72,1,0),
Position = UDim2.new(0,12,0,0),
BackgroundTransparency = 1,
Text = "WPM Tester - By Stummer",
TextColor3 = Color3.fromRGB(240,240,240),
Font = Enum.Font.GothamBold,
TextSize = 20,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center
})
local headerClose = new("TextButton", {
Parent = header,
Size = UDim2.new(0,44,0,30),
Position = UDim2.new(1,-56,0,9),
BackgroundColor3 = Color3.fromRGB(40,40,40),
BorderSizePixel = 0,
Text = "✕",
TextColor3 = Color3.fromRGB(255,255,255),
Font = Enum.Font.GothamBold,
TextSize = 18
})
new("UICorner", {Parent = headerClose, CornerRadius = UDim.new(0,6)})
headerClose.MouseButton1Click:Connect(function() screenGui:Destroy() end)
local content = new("Frame", {
Parent = mainFrame,
Size = UDim2.new(1,-24,1,-140),
Position = UDim2.new(0,12,0,56),
BackgroundTransparency = 1,
})
local scroll = new("ScrollingFrame", {
Parent = content,
Size = UDim2.new(1,0,1,0),
Position = UDim2.new(0,0,0,0),
CanvasSize = UDim2.new(0,0,0,0),
ScrollingDirection = Enum.ScrollingDirection.Y,
ScrollBarThickness = 8,
BackgroundTransparency = 1,
BorderSizePixel = 0
})
scroll.AutomaticCanvasSize = Enum.AutomaticSize.Y
local bigLabel = new("TextLabel", {
Parent = scroll,
Size = UDim2.new(1, -10, 0, 48),
Position = UDim2.new(0, 5, 0, 6),
BackgroundTransparency = 1,
Text = "",
Font = Enum.Font.GothamBold,
TextSize = 26,
TextWrapped = true,
TextColor3 = Color3.fromRGB(220,220,220),
RichText = true,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
})
bigLabel:GetPropertyChangedSignal("TextBounds"):Connect(function()
local h = math.max(48, bigLabel.TextBounds.Y + 12)
bigLabel.Size = UDim2.new(1, -10, 0, h)
-- pastikan inputBox selalu sama ukuran/posisi dengan bigLabel
if inputBox and inputBox.Parent == scroll then
inputBox.Size = bigLabel.Size
inputBox.Position = bigLabel.Position
end
scroll.CanvasSize = UDim2.new(0, 0, 0, h + 18)
end)
local bottomBar = new("Frame", {
Parent = mainFrame,
Size = UDim2.new(1,-24,0,72),
Position = UDim2.new(0,12,1,-84),
BackgroundTransparency = 1
})
-- inputBox sekarang di-overlay di atas bigLabel (Parent = scroll),
-- ukuran/posisi akan selalu disamakan dengan bigLabel, dan background transparan (1).
local inputBox = new("TextBox", {
Parent = scroll, -- dipindah ke scroll agar menimpa bigLabel
Size = bigLabel.Size,
Position = bigLabel.Position,
BackgroundColor3 = Color3.fromRGB(30,30,30),
Text = "",
Font = Enum.Font.Gotham,
TextSize = 20,
TextColor3 = Color3.fromRGB(255,255,255),
PlaceholderText = "Type here...",
ClearTextOnFocus = false,
BorderSizePixel = 0,
})
inputBox.BackgroundTransparency = 1 -- transparan seperti permintaan
inputBox.PlaceholderColor3 = Color3.fromRGB(150,150,150)
inputBox.TextEditable = true
inputBox.ZIndex = bigLabel.ZIndex + 1 -- pastikan textbox berada di atas label
local controls = new("Frame", {
Parent = bottomBar,
Size = UDim2.new(0.26,0,1,0),
Position = UDim2.new(0.74,8,0,0),
BackgroundTransparency = 1
})
local modeButton = new("TextButton", {
Parent = controls,
Size = UDim2.new(1,0,0,34),
Position = UDim2.new(0,0,0,0),
BackgroundColor3 = Color3.fromRGB(44,44,44),
Text = "Select Mode",
TextColor3 = Color3.fromRGB(255,255,255),
Font = Enum.Font.GothamBold,
TextSize = 14,
BorderSizePixel = 0
})
new("UICorner", {Parent = modeButton, CornerRadius = UDim.new(0,6)})
local resetBtn = new("TextButton", {
Parent = controls,
Size = UDim2.new(1,0,0,34),
Position = UDim2.new(0,0,0,38),
BackgroundColor3 = Color3.fromRGB(44,44,44),
Text = "Reset",
TextColor3 = Color3.fromRGB(255,255,255),
Font = Enum.Font.Gotham,
TextSize = 14,
BorderSizePixel = 0
})
new("UICorner", {Parent = resetBtn, CornerRadius = UDim.new(0,6)})
local dropdown = new("Frame", {
Parent = mainFrame,
Size = UDim2.new(0,220,0,0),
Position = UDim2.new(1,-240,0,56),
BackgroundColor3 = Color3.fromRGB(28,28,28),
BorderSizePixel = 0,
ClipsDescendants = true
})
new("UICorner", {Parent = dropdown, CornerRadius = UDim.new(0,8)})
local list = new("UIListLayout", {Parent = dropdown, Padding = UDim.new(0,6), FillDirection = Enum.FillDirection.Vertical})
list.HorizontalAlignment = Enum.HorizontalAlignment.Center
list.SortOrder = Enum.SortOrder.LayoutOrder
local infoBar = new("Frame", {
Parent = mainFrame,
Size = UDim2.new(1,-24,0,36),
Position = UDim2.new(0,12,1,-120),
BackgroundTransparency = 1
})
local timerLabel = new("TextLabel", {
Parent = infoBar,
Size = UDim2.new(0.6,0,1,0),
Position = UDim2.new(0,0,0,0),
BackgroundTransparency = 1,
Text = "Time: 00:00.000",
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = Color3.fromRGB(200,200,200),
Font = Enum.Font.Gotham,
TextSize = 16
})
local wpmLabel = new("TextLabel", {
Parent = infoBar,
Size = UDim2.new(0.4,0,1,0),
Position = UDim2.new(0.6,4,0,0),
BackgroundTransparency = 1,
Text = "WPM: 0.00 | Acc: 0.00%",
TextXAlignment = Enum.TextXAlignment.Right,
TextColor3 = Color3.fromRGB(200,200,200),
Font = Enum.Font.GothamBold,
TextSize = 16
})
local resultLabel = new("TextLabel", {
Parent = mainFrame,
Size = UDim2.new(1,-24,0,24),
Position = UDim2.new(0,12,1,-36),
BackgroundTransparency = 1,
Text = "",
TextColor3 = Color3.fromRGB(200,200,200),
Font = Enum.Font.GothamBold,
TextSize = 14,
TextXAlignment = Enum.TextXAlignment.Left
})
local modes = {
Easy = {name = "Easy - Casual Typer", count = 15, pool = {}},
Medium = {name = "Medium - More Tryhard", count = 20, pool = {}},
Hard = {name = "Hard - Fast Hand", count = 25, pool = {}},
Extreme = {name = "Extreme - Profesional", count = 30, pool = {}},
Stenographer = {name = "Stenographer - Impossible using normal keyboard", count = 35, pool = {}},
}
local easyPools = {
"Mother","Father","Sister","Brother","Child","River","Mountain","Forest","Flower","Animal","Teacher","Student","Book","Pen","Class",
"Sun","Moon","Star","Sky","Cloud","Rain","Wind","Earth","Water","Fire","Tree","Grass","Bird","Fish","Dog","Cat","Home",
"Run","Jump","Walk","Talk","See","Hear","Eat","Drink","Sleep","Wake","Think","Feel","Happy","Sad","Love","Friend",
"Door","Window","Chair","Table","Bed","Lamp","Clock","Phone","Key","Bag","Shoe","Hat","Coat","Hand","Foot","Face","Eye",
"Red","Blue","Green","Yellow","Black","White","Big","Small","Fast","Slow","Hot","Cold","High","Low","New","Old",
"Car","Bus","Bike","Road","Street","City","Town","House","School","Park","Shop","Food","Drink","Time","Day","Night",
"One","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Hundred","Thousand","First","Second","Third",
"Read","Write","Draw","Paint","Sing","Dance","Play","Game","Sport","Ball","Music","Song","Film","TV","Book","Page",
"Apple","Bread","Rice","Milk","Egg","Meat","Cake","Coffee","Tea","Salt","Sugar","Water","Juice","Fruit","Vegetable",
"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday","January","February","March","April",
"Doctor","Nurse","Hospital","Medicine","Health","Sick","Pain","Fever","Cough","Cold","Headache","Tooth","Heart",
"King","Queen","Prince","Princess","Knight","Castle","Sword","Shield","Horse","Dragon","Treasure","Magic","Gold",
"Computer","Mouse","Keyboard","Screen","Phone","Internet","Email","Website","Data","File","Program","Code",
"Winter","Spring","Summer","Autumn","Snow","Ice","Frost","Sun","Rain","Leaf","Flower","Bloom","Seed","Grow",
"Sea","Beach","Sand","Wave","Boat","Ship","Fish","Shell","Swim","Tide","Coral","Island","Ocean","Coast",
"North","South","East","West","Map","World","Country","Flag","Language","Travel","Plane","Ticket","Hotel",
"Morning","Afternoon","Evening","Midnight","Dawn","Dusk","Noon","Today","Tomorrow","Yesterday","Week","Month",
"Pen","Pencil","Paper","Eraser","Ruler","Sharpener","Bag","Book","Notebook","Test","Grade","Lesson","Homework",
"Baby","Girl","Boy","Man","Woman","Family","Grandfather","Grandmother","Aunt","Uncle","Cousin","Parents","Home"
}
local midPools = {
"creative","deadline","developer","algorithm","framework","variable","function","database","interface","network",
"package","module","compile","execute","template","optimize","iterate","synchronize","parallel","concurrent",
"migration","authentication","authorization","encryption","compression","serialization","deserialization",
"encapsulation","polymorphism","inheritance","abstraction","architecture","deployment","containerization",
"virtualization","benchmark","profiling","integration","automation","orchestration","scalability","redundancy"
}
local hardPools = {
"synchronization","characterization","infrastructure","specification","microarchitecture","misconfiguration",
"decentralization","cryptanalysis","transformation","authentication","reconciliation","multithreading",
"decomposition","internationalization","modularization","backpropagation","differentiation","optimization",
"regularization","vectorization","serialization","containerization","parallelization","virtualization",
"interoperability","instrumentation","suboptimization","refactoring","consolidation","heterogeneous"
}
local extremePools = {
"characteristically","electroencephalogram","counterintuitively","uncharacteristically",
"photosynthesizing","thermodynamically","hyperconnectivity","neurotransmission",
"incompatibilities","misinterpretations","reconfigurations","overengineering",
"transcendentalism","bioluminescence","microarchitectural","heterogeneousness",
"telecommunication","decentralizationist","institutionalization","miscommunication",
"counterproductive","reconstructional","interdisciplinary","infrastructural"
}
local stenoPools = {
"floccinaucinihilipilification","pseudopseudohypoparathyroidism","antidisestablishmentarianism",
"psychoneuroendocrinological","thyroparathyroidectomized","hepaticocholangiocholecystenterostomies",
"radioimmunoelectrophoresis","spectrophotofluorometrically","incomprehensibilities",
"psychophysicotherapeutic","electroencephalographically","otorhinolaryngological",
"psychoneuroendocrinology","ultramicroscopically","immunohistochemically",
"dermatoglyphically","counterrevolutionaries","electrocardiographically"
}
modes.Easy.pool = easyPools
modes.Medium.pool = {}
for _,v in pairs(easyPools) do table.insert(modes.Medium.pool, v) end
for _,v in pairs(midPools) do table.insert(modes.Medium.pool, v) end
modes.Hard.pool = {}
for _,v in pairs(hardPools) do table.insert(modes.Hard.pool, v) end
for _,v in pairs(midPools) do table.insert(modes.Hard.pool, v) end
modes.Extreme.pool = {}
for _,v in pairs(extremePools) do table.insert(modes.Extreme.pool, v) end
for _,v in pairs(hardPools) do table.insert(modes.Extreme.pool, v) end
modes.Stenographer.pool = {}
for _,v in pairs(stenoPools) do table.insert(modes.Stenographer.pool, v) end
for _,v in pairs(extremePools) do table.insert(modes.Stenographer.pool, v) end
local currentModeKey = "Easy"
modeButton.Text = modes.Easy.name
local function shuffle(t)
for i = #t, 2, -1 do
local j = math_random(i)
t[i], t[j] = t[j], t[i]
end
end
local function pickWords(pool, count)
local tmp = {}
for _,w in pairs(pool) do tmp[#tmp+1] = w end
shuffle(tmp)
local out = {}
for i=1, math.min(count, #tmp) do out[#out+1] = tmp[i] end
return out
end
local function safeEscape(ch)
if ch == "&" then return "&" end
if ch == "<" then return "<" end
if ch == ">" then return ">" end
return ch
end
local function colorizeTargetAndTyped(fullText, typed)
typed = typed or ""
local result = {}
local fullLen = #fullText
local typedLen = #typed
for i = 1, fullLen do
local ch = string.sub(fullText, i, i)
local safe = safeEscape(ch)
if i <= typedLen then
local tch = string.sub(typed, i, i)
if tch == ch then
result[#result+1] = ''..safe..''
else
result[#result+1] = ''..safe..''
end
else
result[#result+1] = ''..safe..''
end
end
return table.concat(result)
end
local sequence = {}
local sequenceText = ""
local startTime = nil
local finished = false
local lastTyped = ""
local correctCharsAtEnd = 0
local function buildSequence(modeKey)
local cfg = modes[modeKey]
local picked = pickWords(cfg.pool, cfg.count)
sequence = picked
sequenceText = table.concat(picked, " ")
end
local function resetTest(keepMode)
startTime = nil
finished = false
lastTyped = ""
correctCharsAtEnd = 0
inputBox.Text = ""
timerLabel.Text = "Time: 00:00.000"
wpmLabel.Text = "WPM: 0.00 | Acc: 0.00%"
resultLabel.Text = ""
if not keepMode then
buildSequence(currentModeKey)
end
bigLabel.RichText = true
bigLabel.Text = colorizeTargetAndTyped(sequenceText, "")
-- pastikan ukuran inputBox mengikuti bigLabel setelah reset juga
inputBox.Size = bigLabel.Size
inputBox.Position = bigLabel.Position
end
local function computeStats(full, typed)
local total = #full
local typedLen = #typed
local correct = 0
for i=1, math.min(total, typedLen) do
if string.sub(full, i, i) == string.sub(typed, i, i) then
correct = correct + 1
end
end
return correct, typedLen - correct, total
end
local function formatTime(secs)
local ms = math.floor((secs - math.floor(secs)) * 1000)
local s = math.floor(secs % 60)
local m = math.floor(secs / 60)
return string.format("%02d:%02d.%03d", m, s, ms)
end
local function updateRealtime()
if not startTime or finished then return end
local now = tick() - startTime
timerLabel.Text = "Time: " .. formatTime(now)
local correct, incorrect, total = computeStats(sequenceText, inputBox.Text)
local minutes = math.max(now / 60, 1/60000)
local wpm = (correct / 5) / minutes
local acc = (correct / math.max(#inputBox.Text,1)) * 100
wpmLabel.Text = string.format("WPM: %.2f | Acc: %.2f%%", wpm, acc)
end
local function finishTest()
if finished then return end
finished = true
local totalTime = tick() - (startTime or tick())
local correct, incorrect, total = computeStats(sequenceText, inputBox.Text)
local finalWPM = (correct / 5) / math.max((totalTime / 60), 1/60000)
local finalAcc = (correct / math.max(#inputBox.Text,1)) * 100
timerLabel.Text = "Time: " .. formatTime(totalTime)
wpmLabel.Text = string.format("WPM: %.2f | Acc: %.2f%%", finalWPM, finalAcc)
resultLabel.Text = string.format("Finished • Time: %s • WPM: %.2f • Accuracy: %.2f%% • CorrectChars: %d/%d", formatTime(totalTime), finalWPM, finalAcc, correct, total)
correctCharsAtEnd = correct
end
inputBox:GetPropertyChangedSignal("Text"):Connect(function()
if finished then return end
if not sequenceText or sequenceText == "" then return end
if startTime == nil and #inputBox.Text > 0 then
startTime = tick()
end
if #inputBox.Text > #sequenceText then
inputBox.Text = string.sub(inputBox.Text, 1, #sequenceText)
end
bigLabel.Text = colorizeTargetAndTyped(sequenceText, inputBox.Text)
local correct, incorrect, total = computeStats(sequenceText, inputBox.Text)
if #inputBox.Text >= #sequenceText then
finishTest()
end
end)
RunService.Heartbeat:Connect(function()
updateRealtime()
end)
modeButton.MouseButton1Click:Connect(function()
if dropdown.Size.Y.Offset == 0 then
local count = 0
for _ in pairs(modes) do count = count + 1 end
local h = math.clamp(count * 38 + 12, 0, 240)
dropdown:TweenSize(UDim2.new(0,220,0,h), Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.18, true)
else
dropdown:TweenSize(UDim2.new(0,220,0,0), Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.12, true)
end
end)
local function createModeButton(key, def)
local b = new("TextButton", {
Parent = dropdown,
Size = UDim2.new(1,-12,0,32),
BackgroundColor3 = Color3.fromRGB(36,36,36),
BorderSizePixel = 0,
Text = def.name,
TextColor3 = Color3.fromRGB(255,255,255),
Font = Enum.Font.Gotham,
TextSize = 14
})
new("UICorner", {Parent = b, CornerRadius = UDim.new(0,6)})
b.MouseButton1Click:Connect(function()
currentModeKey = key
modeButton.Text = def.name
dropdown:TweenSize(UDim2.new(0,220,0,0), Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.12, true)
buildSequence(key)
resetTest(true)
end)
end
for k,v in pairs(modes) do
createModeButton(k,v)
end
resetBtn.MouseButton1Click:Connect(function()
resetTest(true)
end)
local function startNext()
buildSequence(currentModeKey)
resetTest(true)
end
local function initialSetup()
buildSequence(currentModeKey)
resetTest(true)
end
initialSetup()
headerLabel.Text = "WPM Tester - By Stummer"
inputBox.Focused:Connect(function()
local h = mainFrame.AbsoluteSize.Y
local screenH = workspace.CurrentCamera and workspace.CurrentCamera.ViewportSize.Y or 720
if UserInputService.TouchEnabled then
local newY = math.clamp(0.12 * screenH, 0, screenH/2)
mainFrame.Position = UDim2.new(0.5, -mainFrame.Size.X.Offset/2, 0, 36)
wait(0.08)
scroll.CanvasPosition = Vector2.new(0, math.max(0, bigLabel.AbsoluteSize.Y - (mainFrame.AbsoluteSize.Y * 0.45)))
end
end)
inputBox.FocusLost:Connect(function(enter)
if enter then
if not finished and #inputBox.Text >= #sequenceText then
finishTest()
end
end
if UserInputService.TouchEnabled then
mainFrame.Position = UDim2.new(0.5,-mainFrame.Size.X.Offset/2,0.5,-210)
end
end)
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if gameProcessed then return end
if input.KeyCode == Enum.KeyCode.Return then
if finished then
startNext()
else
if #inputBox.Text >= #sequenceText then
finishTest()
end
end
end
end)
headerClose.MouseButton1Click:Connect(function()
screenGui:Destroy()
end)