
KeylessUniversal
![Action recorder V1.0 [RECORD ACTIONS]](https://cdn.kocakz.me/images/9ccc60dbb202297c68d1665bfa8a4cae28765742a1bcff2303c1f9e8a41655cf.webp)
-- Action Recorder V1.3
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local GuiService = game:GetService("GuiService")
local player = Players.LocalPlayer
local camera = workspace.CurrentCamera
--// CONFIG
local RED = Color3.fromRGB(255, 40, 40)
local BLACK = Color3.fromRGB(8, 8, 8)
local DARK = Color3.fromRGB(16, 16, 16)
local OUTLINE = 1.5
local FONT = Enum.Font.Code
local TEXT_SIZE = 14
local MOVE_KEYS = {
W = Vector3.new(0, 0, -1),
A = Vector3.new(-1, 0, 0),
S = Vector3.new(0, 0, 1),
D = Vector3.new(1, 0, 0),
}
local NUMBER_KEYS = {
One = true, Two = true, Three = true, Four = true, Five = true,
Six = true, Seven = true, Eight = true, Nine = true, Zero = true
}
--// STATE
local recordings = {}
local nextId = 1
local currentRecordingId = nil
local isRecording = false
local isPlaying = false
local isPlayingSchedule = false
local recordStartTime = 0
local lastActionTime = 0
local rawActions = {}
local schedule = {}
local inScheduler = false
local isMobile = false -- set by first-time popup
local hasChosenDevice = false
local draggingFrame, dragStart, frameStart = nil, nil, nil
----------------------------------------------------------------
-- HELPERS
----------------------------------------------------------------
local function create(class, props)
local obj = Instance.new(class)
for k, v in pairs(props) do obj[k] = v end
return obj
end
local function addOutline(parent, thickness)
local stroke = Instance.new("UIStroke")
stroke.Color = RED
stroke.Thickness = thickness or OUTLINE
stroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
stroke.Parent = parent
return stroke
end
local function makeButton(text, parent, size, pos)
local btn = create("TextButton", {
Text = text, Font = FONT, TextSize = TEXT_SIZE, TextColor3 = RED,
BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = size, Position = pos,
AutoButtonColor = false, Parent = parent
})
addOutline(btn)
return btn
end
local function makeTopBarDraggable(topBar, targetFrame)
topBar.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
draggingFrame = targetFrame
dragStart = input.Position
frameStart = targetFrame.Position
end
end)
end
UserInputService.InputChanged:Connect(function(input)
if draggingFrame and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
local delta = input.Position - dragStart
draggingFrame.Position = UDim2.new(
frameStart.X.Scale, frameStart.X.Offset + delta.X,
frameStart.Y.Scale, frameStart.Y.Offset + delta.Y
)
end
end)
UserInputService.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
draggingFrame = nil
end
end)
local function getEquippedToolName()
local char = player.Character
if not char then return nil end
for _, t in ipairs(char:GetChildren()) do
if t:IsA("Tool") then return t.Name end
end
return nil
end
local function equipToolByName(toolName)
if not toolName then return end
local char = player.Character
local backpack = player:FindFirstChild("Backpack")
if not char or not backpack then return end
local hum = char:FindFirstChildOfClass("Humanoid")
if not hum then return end
for _, t in ipairs(char:GetChildren()) do
if t:IsA("Tool") and t.Name == toolName then return end
end
local tool = backpack:FindFirstChild(toolName)
if tool and tool:IsA("Tool") then
hum:UnequipTools()
task.wait()
hum:EquipTool(tool)
end
end
----------------------------------------------------------------
-- DEVICE POPUP (first time only)
----------------------------------------------------------------
local function showDevicePopup(onChosen)
-- Create a temporary ScreenGui for the popup
local popupGui = create("ScreenGui", {
Name = "DevicePopup",
ResetOnSpawn = false,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
DisplayOrder = 100,
Parent = player:WaitForChild("PlayerGui")
})
local popup = create("Frame", {
BackgroundColor3 = BLACK,
BorderSizePixel = 0,
Size = UDim2.new(0, 320, 0, 160),
Position = UDim2.new(0.5, -160, 0.5, -80),
Parent = popupGui
})
addOutline(popup, 2)
create("TextLabel", {
Text = "Are you on Mobile or Computer?",
Font = FONT,
TextSize = 16,
TextColor3 = RED,
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 50),
Parent = popup
})
local mobileBtn = makeButton("Mobile", popup, UDim2.new(0, 120, 0, 40), UDim2.new(0.5, -130, 1, -60))
local computerBtn = makeButton("Computer", popup, UDim2.new(0, 120, 0, 40), UDim2.new(0.5, 10, 1, -60))
local function choose(isMob)
isMobile = isMob
hasChosenDevice = true
popupGui:Destroy()
onChosen()
end
mobileBtn.MouseButton1Click:Connect(function()
choose(true)
end)
computerBtn.MouseButton1Click:Connect(function()
choose(false)
end)
end
----------------------------------------------------------------
-- BUILD UI
----------------------------------------------------------------
local screenGui = create("ScreenGui", {
Name = "ActionRecorder", ResetOnSpawn = false,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
Parent = player:WaitForChild("PlayerGui")
})
local mainFrame = create("Frame", {
Name = "Main", BackgroundColor3 = BLACK, BorderSizePixel = 0,
Size = UDim2.new(0, 560, 0, 400),
Position = UDim2.new(0.5, -280, 0.5, -200),
Visible = false, -- hidden until device is chosen
Parent = screenGui
})
addOutline(mainFrame, 2)
local topBar = create("Frame", {
BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(1, 0, 0, 34), Parent = mainFrame
})
addOutline(topBar)
makeTopBarDraggable(topBar, mainFrame)
local newBtn = makeButton(" + NEW", topBar, UDim2.new(0, 90, 1, 0), UDim2.new(0, 0, 0, 0))
newBtn.TextXAlignment = Enum.TextXAlignment.Left
local schedulerBtn = makeButton("Scheduler", topBar, UDim2.new(0, 90, 1, 0), UDim2.new(0, 95, 0, 0))
local title = create("TextLabel", {
Text = "V1.3 Action Recorder", Font = FONT, TextSize = 15, TextColor3 = RED,
BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(0, 210, 1, 0),
Position = UDim2.new(0, 195, 0, 0), Parent = topBar
})
addOutline(title)
local killBtn = makeButton("Kill", topBar, UDim2.new(0, 55, 1, 0), UDim2.new(1, -115, 0, 0))
local minBtn = makeButton("Min", topBar, UDim2.new(0, 50, 1, 0), UDim2.new(1, -50, 0, 0))
local sideBar = create("Frame", {
BackgroundColor3 = BLACK, BorderSizePixel = 0,
Size = UDim2.new(0, 155, 1, -34), Position = UDim2.new(0, 0, 0, 34), Parent = mainFrame
})
addOutline(sideBar)
local sideScroll = create("ScrollingFrame", {
BackgroundTransparency = 1, BorderSizePixel = 0,
Size = UDim2.new(1, -6, 1, -6), Position = UDim2.new(0, 3, 0, 3),
CanvasSize = UDim2.new(0, 0, 0, 0), ScrollBarThickness = 3, ScrollBarImageColor3 = RED,
Parent = sideBar
})
create("UIListLayout", {Padding = UDim.new(0, 5), SortOrder = Enum.SortOrder.LayoutOrder, Parent = sideScroll})
local mainPage = create("Frame", {
BackgroundColor3 = BLACK, BorderSizePixel = 0,
Size = UDim2.new(1, -155, 1, -34), Position = UDim2.new(0, 155, 0, 34), Parent = mainFrame
})
addOutline(mainPage)
local emptyLabel = create("TextLabel", {
Text = "No recording open", Font = FONT, TextSize = 16, TextColor3 = RED,
BackgroundTransparency = 1, Size = UDim2.new(1, 0, 1, 0), Parent = mainPage
})
local recordingBar = create("Frame", {
BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(1, 0, 0, 32),
Visible = false, Parent = mainPage
})
addOutline(recordingBar)
local closeBtn = makeButton("X", recordingBar, UDim2.new(0, 32, 1, 0), UDim2.new(0, 0, 0, 0))
local nameBox = create("TextBox", {
Text = "Recording 1", Font = FONT, TextSize = TEXT_SIZE, TextColor3 = RED,
BackgroundColor3 = DARK, BorderSizePixel = 0, ClearTextOnFocus = false,
Size = UDim2.new(1, -150, 1, 0), Position = UDim2.new(0, 38, 0, 0), Parent = recordingBar
})
addOutline(nameBox)
create("Frame", {BackgroundColor3 = RED, BorderSizePixel = 0, Size = UDim2.new(1, 0, 0, 1), Position = UDim2.new(0, 0, 1, -1), Parent = nameBox})
local recordBtn = create("TextButton", {
Text = "●", Font = FONT, TextSize = 22, TextColor3 = RED, BackgroundColor3 = DARK, BorderSizePixel = 0,
Size = UDim2.new(0, 36, 1, 0), Position = UDim2.new(1, -80, 0, 0), Parent = recordingBar
})
addOutline(recordBtn)
local playBtn = create("TextButton", {
Text = "▶", Font = FONT, TextSize = 16, TextColor3 = RED, BackgroundColor3 = DARK, BorderSizePixel = 0,
Size = UDim2.new(0, 36, 1, 0), Position = UDim2.new(1, -38, 0, 0), Parent = recordingBar
})
addOutline(playBtn)
local actionListFrame = create("ScrollingFrame", {
BackgroundColor3 = BLACK, BorderSizePixel = 0,
Size = UDim2.new(1, -8, 1, -40), Position = UDim2.new(0, 4, 0, 36),
CanvasSize = UDim2.new(0, 0, 0, 0), ScrollBarThickness = 3, ScrollBarImageColor3 = RED,
Visible = false, Parent = mainPage
})
addOutline(actionListFrame)
local actionLayout = create("UIListLayout", {Padding = UDim.new(0, 2), SortOrder = Enum.SortOrder.LayoutOrder, Parent = actionListFrame})
----------------------------------------------------------------
-- SCHEDULER UI
----------------------------------------------------------------
local schedulerFrame = create("Frame", {
BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = UDim2.new(1, 0, 1, 0),
Visible = false, Parent = mainPage
})
local recBar = create("ScrollingFrame", {
BackgroundColor3 = DARK, BorderSizePixel = 0,
Size = UDim2.new(1, -8, 0, 34), Position = UDim2.new(0, 4, 0, 6),
CanvasSize = UDim2.new(0, 0, 0, 0), ScrollBarThickness = 3, ScrollBarImageColor3 = RED,
ScrollingDirection = Enum.ScrollingDirection.X, Parent = schedulerFrame
})
addOutline(recBar)
create("UIListLayout", {FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder, Parent = recBar})
local addDelayBtn = makeButton("+ Delay", schedulerFrame, UDim2.new(0, 80, 0, 26), UDim2.new(0, 8, 0, 46))
local addRepeatBtn = makeButton("+ Repeat", schedulerFrame, UDim2.new(0, 90, 0, 26), UDim2.new(0, 96, 0, 46))
create("TextLabel", {
Text = " Sequence (blocks)", Font = FONT, TextSize = 13, TextColor3 = RED, BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 20), Position = UDim2.new(0, 0, 0, 78),
TextXAlignment = Enum.TextXAlignment.Left, Parent = schedulerFrame
})
local seqScroll = create("ScrollingFrame", {
BackgroundColor3 = BLACK, BorderSizePixel = 0,
Size = UDim2.new(1, -8, 1, -150), Position = UDim2.new(0, 4, 0, 100),
CanvasSize = UDim2.new(0, 0, 0, 0), ScrollBarThickness = 3, ScrollBarImageColor3 = RED,
Parent = schedulerFrame
})
addOutline(seqScroll)
create("UIListLayout", {Padding = UDim.new(0, 5), SortOrder = Enum.SortOrder.LayoutOrder, Parent = seqScroll})
local playSchedBtn = makeButton("Play Schedule", schedulerFrame, UDim2.new(0, 140, 0, 28), UDim2.new(0, 8, 1, -36))
local clearSchedBtn = makeButton("Clear", schedulerFrame, UDim2.new(0, 70, 0, 28), UDim2.new(0, 156, 1, -36))
local backBtn = makeButton("Back", schedulerFrame, UDim2.new(0, 70, 0, 28), UDim2.new(1, -80, 1, -36))
local minButton = create("TextButton", {
Text = "AR", Font = FONT, TextSize = 24, TextColor3 = BLACK, BackgroundColor3 = RED, BorderSizePixel = 0,
Size = UDim2.new(0, 68, 0, 68), Position = UDim2.new(1, -100, 1, -100), Visible = false, Parent = screenGui
})
Instance.new("UICorner", minButton).CornerRadius = UDim.new(1, 0)
local minStroke = Instance.new("UIStroke")
minStroke.Color = BLACK
minStroke.Thickness = 3
minStroke.Parent = minButton
minButton.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
draggingFrame = minButton
dragStart = input.Position
frameStart = minButton.Position
end
end)
local confirmFrame = create("Frame", {
BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = UDim2.new(0, 270, 0, 110),
Position = UDim2.new(0.5, -135, 0.5, -55), Visible = false, ZIndex = 20, Parent = screenGui
})
addOutline(confirmFrame, 2)
create("TextLabel", {
Text = "Delete this recording?", Font = FONT, TextSize = 15, TextColor3 = RED, BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 45), Parent = confirmFrame
})
local yesBtn = makeButton("Yes", confirmFrame, UDim2.new(0, 90, 0, 32), UDim2.new(0.5, -100, 1, -45))
local noBtn = makeButton("No", confirmFrame, UDim2.new(0, 90, 0, 32), UDim2.new(0.5, 10, 1, -45))
----------------------------------------------------------------
-- APPLY MOBILE / COMPUTER SCALING
----------------------------------------------------------------
local function applyDeviceScaling()
if isMobile then
mainFrame.Size = UDim2.new(0.95, 0, 0.7, 0)
mainFrame.Position = UDim2.new(0.025, 0, 0.15, 0)
-- slightly larger text / buttons for touch
TEXT_SIZE = 16
else
mainFrame.Size = UDim2.new(0, 560, 0, 400)
mainFrame.Position = UDim2.new(0.5, -280, 0.5, -200)
end
mainFrame.Visible = true
end
----------------------------------------------------------------
-- SIDEBAR + ACTION LIST (copyable)
----------------------------------------------------------------
local function refreshSidebar()
for _, child in ipairs(sideScroll:GetChildren()) do
if child:IsA("Frame") then child:Destroy() end
end
for i, rec in ipairs(recordings) do
local item = create("Frame", {
BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(1, 0, 0, 52),
LayoutOrder = i, Parent = sideScroll
})
addOutline(item)
create("TextLabel", {
Text = rec.name, Font = FONT, TextSize = 13, TextColor3 = RED, BackgroundTransparency = 1,
Size = UDim2.new(1, -6, 0, 20), Position = UDim2.new(0, 4, 0, 3),
TextXAlignment = Enum.TextXAlignment.Left, Parent = item
})
local openBtn = makeButton("Open", item, UDim2.new(0.48, 0, 0, 22), UDim2.new(0.02, 0, 0, 26))
openBtn.TextSize = 12
local dupBtn = makeButton("Duplicate", item, UDim2.new(0.48, 0, 0, 22), UDim2.new(0.5, 0, 0, 26))
dupBtn.TextSize = 11
openBtn.MouseButton1Click:Connect(function() openRecording(rec.id) end)
dupBtn.MouseButton1Click:Connect(function() duplicateRecording(rec.id) end)
end
sideScroll.CanvasSize = UDim2.new(0, 0, 0, #recordings * 57)
end
local function clearActionList()
for _, c in ipairs(actionListFrame:GetChildren()) do
if c:IsA("Frame") or c:IsA("TextBox") or c:IsA("TextLabel") then c:Destroy() end
end
actionListFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
end
-- Copyable line (TextBox that is not editable)
local function addSimpleLine(text)
local box = create("TextBox", {
Text = " " .. text,
Font = FONT,
TextSize = 13,
TextColor3 = RED,
BackgroundTransparency = 1,
BorderSizePixel = 0,
ClearTextOnFocus = false,
TextEditable = false, -- important: user can still select & copy
Size = UDim2.new(1, -4, 0, 18),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = actionListFrame
})
end
local function addMouseGroup(group)
local container = create("Frame", {
BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(1, -4, 0, 20), Parent = actionListFrame
})
addOutline(container)
local expanded = false
local arrow = create("TextButton", {
Text = "▶", Font = FONT, TextSize = 12, TextColor3 = RED, BackgroundTransparency = 1,
Size = UDim2.new(0, 20, 1, 0), Parent = container
})
create("TextLabel", {
Text = string.format(" Mouse Sequence (%d moves)", #group), Font = FONT, TextSize = 13, TextColor3 = RED,
BackgroundTransparency = 1, Size = UDim2.new(1, -24, 1, 0), Position = UDim2.new(0, 20, 0, 0),
TextXAlignment = Enum.TextXAlignment.Left, Parent = container
})
local children = {}
arrow.MouseButton1Click:Connect(function()
expanded = not expanded
arrow.Text = expanded and "▼" or "▶"
for _, child in ipairs(children) do child.Visible = expanded end
actionListFrame.CanvasSize = UDim2.new(0, 0, 0, actionLayout.AbsoluteContentSize.Y + 8)
end)
for _, move in ipairs(group) do
local box = create("TextBox", {
Text = " " .. move.desc,
Font = FONT, TextSize = 12, TextColor3 = Color3.fromRGB(200, 80, 80),
BackgroundTransparency = 1, BorderSizePixel = 0,
ClearTextOnFocus = false, TextEditable = false,
Size = UDim2.new(1, -4, 0, 16), TextXAlignment = Enum.TextXAlignment.Left,
Visible = false, Parent = actionListFrame
})
table.insert(children, box)
end
end
local function rebuildActionList(actions)
clearActionList()
addSimpleLine("Start")
local i = 1
while i <= #actions do
local act = actions[i]
if act.data.type == "look" then
local group = {act}
local j = i + 1
while j <= #actions and actions[j].data.type == "look" do
table.insert(group, actions[j])
j += 1
end
addMouseGroup(group)
i = j
else
addSimpleLine(act.desc)
i += 1
end
end
addSimpleLine("Stop")
actionListFrame.CanvasSize = UDim2.new(0, 0, 0, actionLayout.AbsoluteContentSize.Y + 10)
end
----------------------------------------------------------------
-- RECORDING
----------------------------------------------------------------
local function showRecordingUI(rec)
emptyLabel.Visible = false
recordingBar.Visible = true
actionListFrame.Visible = true
schedulerFrame.Visible = false
inScheduler = false
nameBox.Text = rec.name
rebuildActionList(rec.actions)
end
function openRecording(id)
currentRecordingId = id
for _, r in ipairs(recordings) do
if r.id == id then showRecordingUI(r) break end
end
end
function duplicateRecording(id)
local original
for _, r in ipairs(recordings) do if r.id == id then original = r break end end
if not original then return end
local copy = {id = nextId, name = original.name .. " (copy)", actions = table.clone(original.actions)}
nextId += 1
table.insert(recordings, copy)
refreshSidebar()
refreshSchedulerBar()
end
local function deleteRecording(id)
for i, r in ipairs(recordings) do
if r.id == id then table.remove(recordings, i) break end
end
if currentRecordingId == id then
currentRecordingId = nil
recordingBar.Visible = false
actionListFrame.Visible = false
emptyLabel.Visible = true
end
refreshSidebar()
refreshSchedulerBar()
end
local heldKeys = {}
local lastMousePos = Vector2.zero
local lastCamLook = Vector3.zero
local lastSample = 0
local function startRecording()
if isRecording or isPlaying or isPlayingSchedule or not currentRecordingId then return end
isRecording = true
rawActions = {}
recordStartTime = tick()
lastActionTime = recordStartTime
lastSample = recordStartTime
heldKeys = {}
lastMousePos = UserInputService:GetMouseLocation()
if camera then lastCamLook = camera.CFrame.LookVector end
recordBtn.TextColor3 = Color3.fromRGB(255, 90, 90)
clearActionList()
addSimpleLine("Start")
end
local function stopRecording()
if not isRecording then return end
isRecording = false
recordBtn.TextColor3 = RED
local now = tick()
if now - lastActionTime > 0.15 then
table.insert(rawActions, {
desc = string.format("Idle (%.2f seconds)", now - lastActionTime),
data = {type = "idle", duration = now - lastActionTime, time = now - recordStartTime}
})
end
local final = {}
for _, a in ipairs(rawActions) do table.insert(final, a) end
for _, r in ipairs(recordings) do
if r.id == currentRecordingId then r.actions = final break end
end
openRecording(currentRecordingId)
end
UserInputService.InputBegan:Connect(function(input, gp)
if gp or not isRecording then return end
local now = tick()
local rel = now - recordStartTime
if now - lastActionTime > 0.12 then
table.insert(rawActions, {
desc = string.format("Idle (%.2f seconds)", now - lastActionTime),
data = {type = "idle", duration = now - lastActionTime, time = rel}
})
end
if input.UserInputType == Enum.UserInputType.Keyboard then
local key = input.KeyCode.Name
heldKeys[key] = true
local data = {type = "keyDown", key = key, time = rel}
if NUMBER_KEYS[key] then
task.delay(0.05, function()
local toolName = getEquippedToolName()
if toolName then data.toolName = toolName end
end)
end
table.insert(rawActions, {desc = key .. " Down", data = data})
elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
local pos = UserInputService:GetMouseLocation()
table.insert(rawActions, {
desc = string.format("LMB Click (%d, %d)", math.floor(pos.X), math.floor(pos.Y)),
data = {type = "click", button = "LMB", x = pos.X, y = pos.Y, time = rel}
})
elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
local pos = UserInputService:GetMouseLocation()
table.insert(rawActions, {
desc = string.format("RMB Click (%d, %d)", math.floor(pos.X), math.floor(pos.Y)),
data = {type = "click", button = "RMB", x = pos.X, y = pos.Y, time = rel}
})
end
lastActionTime = now
end)
UserInputService.InputEnded:Connect(function(input, gp)
if gp or not isRecording then return end
if input.UserInputType == Enum.UserInputType.Keyboard then
local key = input.KeyCode.Name
if heldKeys[key] then
heldKeys[key] = nil
local now = tick()
table.insert(rawActions, {
desc = key .. " Up",
data = {type = "keyUp", key = key, time = now - recordStartTime}
})
lastActionTime = now
end
end
end)
UserInputService.InputChanged:Connect(function(input, gp)
if gp or not isRecording then return end
if input.UserInputType == Enum.UserInputType.MouseWheel then
local now = tick()
local rel = now - recordStartTime
local delta = input.Position.Z
table.insert(rawActions, {
desc = string.format("Scroll %s", delta > 0 and "Up" or "Down"),
data = {type = "scroll", delta = delta, time = rel}
})
lastActionTime = now
end
end)
RunService.RenderStepped:Connect(function()
if not isRecording then return end
local now = tick()
if now - lastSample < 0.033 then return end
lastSample = now
local pos = UserInputService:GetMouseLocation()
local camLook = camera and camera.CFrame.LookVector or Vector3.zero
local moved = (pos - lastMousePos).Magnitude > 3
local looked = (camLook - lastCamLook).Magnitude > 0.02
if moved or looked then
local rel = now - recordStartTime
if now - lastActionTime > 0.08 then
table.insert(rawActions, {
desc = string.format("Idle (%.2f seconds)", now - lastActionTime),
data = {type = "idle", duration = now - lastActionTime, time = rel}
})
end
table.insert(rawActions, {
desc = string.format("Cursor (%d, %d)", math.floor(pos.X), math.floor(pos.Y)),
data = {
type = "look",
x = pos.X, y = pos.Y,
look = {camLook.X, camLook.Y, camLook.Z},
time = rel
}
})
lastMousePos = pos
lastCamLook = camLook
lastActionTime = now
end
end)
----------------------------------------------------------------
-- PLAYBACK (camera now follows the character)
----------------------------------------------------------------
local playConnection = nil
local playStartTime = 0
local playIndex = 1
local activeKeys = {}
local currentLook = nil
local originalCameraType = nil
local originalFOV = nil
local function stopPlayback()
isPlaying = false
playBtn.Text = "▶"
if playConnection then
playConnection:Disconnect()
playConnection = nil
end
activeKeys = {}
currentLook = nil
if originalCameraType and camera then
camera.CameraType = originalCameraType
originalCameraType = nil
end
if originalFOV and camera then
camera.FieldOfView = originalFOV
originalFOV = nil
end
local char = player.Character
if char then
local hum = char:FindFirstChildOfClass("Humanoid")
if hum then hum:Move(Vector3.zero, true) end
end
end
local function playRecordingById(id, onFinished)
local rec
for _, r in ipairs(recordings) do
if r.id == id then rec = r break end
end
if not rec or #rec.actions == 0 then
if onFinished then onFinished() end
return
end
isPlaying = true
playStartTime = tick()
playIndex = 1
activeKeys = {}
currentLook = nil
local char = player.Character
local humanoid = char and char:FindFirstChildOfClass("Humanoid")
local root = char and char:FindFirstChild("HumanoidRootPart")
if not humanoid or not root then
if onFinished then onFinished() end
return
end
if camera then
originalCameraType = camera.CameraType
originalFOV = camera.FieldOfView
camera.CameraType = Enum.CameraType.Scriptable
end
playConnection = RunService.RenderStepped:Connect(function()
if not isPlaying then return end
local elapsed = tick() - playStartTime
while playIndex <= #rec.actions do
local act = rec.actions[playIndex]
local t = act.data.time or 0
if t > elapsed then break end
local d = act.data
if d.type == "keyDown" then
activeKeys[d.key] = true
if d.toolName then equipToolByName(d.toolName) end
elseif d.type == "keyUp" then
activeKeys[d.key] = nil
elseif d.type == "look" and d.look then
currentLook = Vector3.new(d.look[1], d.look[2], d.look[3])
elseif d.type == "scroll" then
if camera then
local fov = camera.FieldOfView
if d.delta > 0 then
camera.FieldOfView = math.clamp(fov - 5, 20, 120)
else
camera.FieldOfView = math.clamp(fov + 5, 20, 120)
end
end
end
playIndex += 1
end
-- Movement
local dir = Vector3.zero
for key, vec in pairs(MOVE_KEYS) do
if activeKeys[key] then dir += vec end
end
if dir.Magnitude > 0 then
humanoid:Move(dir.Unit, true)
else
humanoid:Move(Vector3.zero, true)
end
if activeKeys["Space"] then humanoid.Jump = true end
-- CAMERA FOLLOW FIX
-- Camera stays with the character while applying recorded look
if camera and root then
local head = char:FindFirstChild("Head")
local camPos = (head and head.Position or root.Position) + Vector3.new(0, 1.5, 0)
local look = currentLook and currentLook.Magnitude > 0.05 and currentLook or root.CFrame.LookVector
camera.CFrame = CFrame.lookAt(camPos, camPos + look)
end
if playIndex > #rec.actions then
local lastTime = rec.actions[#rec.actions].data.time or 0
if elapsed > lastTime + 0.25 then
stopPlayback()
if onFinished then onFinished() end
end
end
end)
end
local function playRecording()
if isPlaying or isRecording or isPlayingSchedule or not currentRecordingId then return end
playBtn.Text = "■"
playRecordingById(currentRecordingId, function() playBtn.Text = "▶" end)
end
----------------------------------------------------------------
-- SCHEDULER
----------------------------------------------------------------
function refreshSchedulerBar()
for _, c in ipairs(recBar:GetChildren()) do
if c:IsA("Frame") then c:Destroy() end
end
local totalWidth = 0
for i, rec in ipairs(recordings) do
local chip = create("Frame", {
BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = UDim2.new(0, 120, 0, 26),
LayoutOrder = i, Parent = recBar
})
addOutline(chip)
create("TextLabel", {
Text = rec.name, Font = FONT, TextSize = 12, TextColor3 = RED, BackgroundTransparency = 1,
Size = UDim2.new(1, -28, 1, 0), Position = UDim2.new(0, 4, 0, 0),
TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd, Parent = chip
})
local plus = makeButton("+", chip, UDim2.new(0, 24, 0, 22), UDim2.new(1, -26, 0.5, -11))
plus.TextSize = 16
plus.MouseButton1Click:Connect(function()
table.insert(schedule, {type = "recording", id = rec.id, name = rec.name})
refreshSequenceList()
end)
totalWidth += 126
end
recBar.CanvasSize = UDim2.new(0, totalWidth + 10, 0, 0)
end
function refreshSequenceList()
for _, c in ipairs(seqScroll:GetChildren()) do
if c:IsA("Frame") then c:Destroy() end
end
for i, block in ipairs(schedule) do
local row = create("Frame", {
BackgroundColor3 = DARK, BorderSizePixel = 0, Size = UDim2.new(1, -4, 0, 34),
LayoutOrder = i, Parent = seqScroll
})
addOutline(row)
if block.type == "recording" then
create("TextLabel", {
Text = i .. ". [Recording] " .. block.name, Font = FONT, TextSize = 13, TextColor3 = RED,
BackgroundTransparency = 1, Size = UDim2.new(1, -40, 1, 0), Position = UDim2.new(0, 8, 0, 0),
TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd, Parent = row
})
elseif block.type == "delay" then
create("TextLabel", {
Text = i .. ". [Delay]", Font = FONT, TextSize = 13, TextColor3 = RED, BackgroundTransparency = 1,
Size = UDim2.new(0, 90, 1, 0), Position = UDim2.new(0, 8, 0, 0), Parent = row
})
local box = create("TextBox", {
Text = tostring(block.seconds or 1), Font = FONT, TextSize = 13, TextColor3 = RED,
BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = UDim2.new(0, 50, 0, 22),
Position = UDim2.new(0, 100, 0.5, -11), ClearTextOnFocus = false, Parent = row
})
addOutline(box)
create("TextLabel", {
Text = "seconds", Font = FONT, TextSize = 12, TextColor3 = RED, BackgroundTransparency = 1,
Size = UDim2.new(0, 60, 1, 0), Position = UDim2.new(0, 155, 0, 0), Parent = row
})
box.FocusLost:Connect(function()
local n = tonumber(box.Text)
if n and n >= 0 then block.seconds = n else box.Text = tostring(block.seconds or 1) end
end)
elseif block.type == "repeat" then
create("TextLabel", {
Text = i .. ". [Repeat]", Font = FONT, TextSize = 13, TextColor3 = RED, BackgroundTransparency = 1,
Size = UDim2.new(0, 90, 1, 0), Position = UDim2.new(0, 8, 0, 0), Parent = row
})
local box = create("TextBox", {
Text = tostring(block.times or 2), Font = FONT, TextSize = 13, TextColor3 = RED,
BackgroundColor3 = BLACK, BorderSizePixel = 0, Size = UDim2.new(0, 40, 0, 22),
Position = UDim2.new(0, 100, 0.5, -11), ClearTextOnFocus = false, Parent = row
})
addOutline(box)
create("TextLabel", {
Text = "times (next block)", Font = FONT, TextSize = 12, TextColor3 = RED, BackgroundTransparency = 1,
Size = UDim2.new(0, 120, 1, 0), Position = UDim2.new(0, 145, 0, 0), Parent = row
})
box.FocusLost:Connect(function()
local n = tonumber(box.Text)
if n and n >= 1 then block.times = math.floor(n) else box.Text = tostring(block.times or 2) end
end)
end
local removeBtn = makeButton("X", row, UDim2.new(0, 26, 0, 22), UDim2.new(1, -32, 0.5, -11))
removeBtn.TextSize = 14
removeBtn.MouseButton1Click:Connect(function()
table.remove(schedule, i)
refreshSequenceList()
end)
end
seqScroll.CanvasSize = UDim2.new(0, 0, 0, #schedule * 39)
end
local function playSchedule()
if isPlayingSchedule or isPlaying or isRecording or #schedule == 0 then return end
isPlayingSchedule = true
task.spawn(function()
local i = 1
while i <= #schedule and isPlayingSchedule do
playSchedBtn.Text = "Playing... (" .. i .. ")"
local block = schedule[i]
if block.type == "recording" then
local finished = false
playRecordingById(block.id, function() finished = true end)
while not finished and isPlayingSchedule do task.wait(0.05) end
elseif block.type == "delay" then
local waitTime = block.seconds or 1
local start = tick()
while tick() - start < waitTime and isPlayingSchedule do task.wait(0.05) end
elseif block.type == "repeat" then
local times = block.times or 2
local nextIdx = i + 1
if nextIdx <= #schedule then
local nextBlock = schedule[nextIdx]
for rep = 1, times do
if not isPlayingSchedule then break end
playSchedBtn.Text = "Playing... (" .. nextIdx .. ") x" .. rep
if nextBlock.type == "recording" then
local finished = false
playRecordingById(nextBlock.id, function() finished = true end)
while not finished and isPlayingSchedule do task.wait(0.05) end
elseif nextBlock.type == "delay" then
local waitTime = nextBlock.seconds or 1
local start = tick()
while tick() - start < waitTime and isPlayingSchedule do task.wait(0.05) end
end
end
i = nextIdx
end
end
i += 1
end
isPlayingSchedule = false
playSchedBtn.Text = "Play Schedule"
stopPlayback()
end)
end
local function showScheduler()
inScheduler = true
emptyLabel.Visible = false
recordingBar.Visible = false
actionListFrame.Visible = false
schedulerFrame.Visible = true
refreshSchedulerBar()
refreshSequenceList()
end
local function hideScheduler()
inScheduler = false
schedulerFrame.Visible = false
if currentRecordingId then openRecording(currentRecordingId) else emptyLabel.Visible = true end
end
----------------------------------------------------------------
-- BUTTONS
----------------------------------------------------------------
newBtn.MouseButton1Click:Connect(function()
local rec = {id = nextId, name = "Recording " .. nextId, actions = {}}
nextId += 1
table.insert(recordings, rec)
refreshSidebar()
refreshSchedulerBar()
end)
schedulerBtn.MouseButton1Click:Connect(function()
if inScheduler then hideScheduler() else showScheduler() end
end)
killBtn.MouseButton1Click:Connect(function() screenGui:Destroy() end)
minBtn.MouseButton1Click:Connect(function()
mainFrame.Visible = false
minButton.Visible = true
end)
minButton.MouseButton1Click:Connect(function()
mainFrame.Visible = true
minButton.Visible = false
end)
closeBtn.MouseButton1Click:Connect(function() confirmFrame.Visible = true end)
yesBtn.MouseButton1Click:Connect(function()
if currentRecordingId then deleteRecording(currentRecordingId) end
confirmFrame.Visible = false
end)
noBtn.MouseButton1Click:Connect(function() confirmFrame.Visible = false end)
nameBox.FocusLost:Connect(function()
if currentRecordingId then
for _, r in ipairs(recordings) do
if r.id == currentRecordingId then r.name = nameBox.Text break end
end
refreshSidebar()
refreshSchedulerBar()
end
end)
recordBtn.MouseButton1Click:Connect(function()
if isRecording then stopRecording() else startRecording() end
end)
playBtn.MouseButton1Click:Connect(function()
if isPlaying then stopPlayback() else playRecording() end
end)
addDelayBtn.MouseButton1Click:Connect(function()
table.insert(schedule, {type = "delay", seconds = 1})
refreshSequenceList()
end)
addRepeatBtn.MouseButton1Click:Connect(function()
table.insert(schedule, {type = "repeat", times = 2})
refreshSequenceList()
end)
playSchedBtn.MouseButton1Click:Connect(function()
if isPlayingSchedule then
isPlayingSchedule = false
stopPlayback()
playSchedBtn.Text = "Play Schedule"
else
playSchedule()
end
end)
clearSchedBtn.MouseButton1Click:Connect(function()
schedule = {}
refreshSequenceList()
end)
backBtn.MouseButton1Click:Connect(function() hideScheduler() end)
----------------------------------------------------------------
-- STARTUP
----------------------------------------------------------------
-- First-time device choice
showDevicePopup(function()
applyDeviceScaling()
refreshSidebar()
print("[Action Recorder] V1.3 ready")
end)No comments yet for this script.



