Files

8259 lines
347 KiB
Lua

-- ============================================================================
--
-- ██╗ ██╗██╗████████╗████████╗██╗ ██╗ ██╗ ██╗██╗ ██╗██████╗
-- ██║ ██╔╝██║╚══██╔══╝╚══██╔══╝╚██╗ ██╔╝ ██║ ██║██║ ██║██╔══██╗
-- █████╔╝ ██║ ██║ ██║ ╚████╔╝ ███████║██║ ██║██████╔╝
-- ██╔═██╗ ██║ ██║ ██║ ╚██╔╝ ██╔══██║██║ ██║██╔══██╗
-- ██║ ██╗██║ ██║ ██║ ██║ ██║ ██║╚██████╔╝██████╔╝
-- ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝
--
-- Murder Mystery 2 Script
-- build 3.1.0+91464df1 · 2026-09-04 05:15 UTC
--
-- GENERATED FILE — do not edit directly.
-- Sources live in src/mm2/ ; rebuild with `python build.py`.
--
-- ============================================================================
-- ─── src/_shared/00_prelude.lua ────────────────────────────────────
-- ============================================================================
-- PRELUDE — services, shared namespace, lifecycle helpers
-- ============================================================================
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local Lighting = game:GetService("Lighting")
local HttpService = game:GetService("HttpService")
local StarterGui = game:GetService("StarterGui")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TeleportService = game:GetService("TeleportService")
local Stats = game:GetService("Stats")
local VirtualUser = game:GetService("VirtualUser")
local LocalPlayer = Players.LocalPlayer
local Camera = workspace.CurrentCamera
local env = (type(getgenv) == "function" and getgenv()) or _G
-- Re-running the script? Tear the previous instance down first, otherwise we
-- stack a second render loop and a duplicate GUI on top of the old one.
if type(env.KittyHubCleanup) == "function" then
pcall(env.KittyHubCleanup)
end
-- Every module hangs its exports off KH. One table instead of hundreds of
-- file-level locals: the build concatenates every source into a single chunk,
-- and a chunk's main body is capped at 200 active locals.
local KH = {
Version = "3.1.0",
Conn = {}, -- RBXScriptConnections -> disconnected on unload
Inst = {}, -- Instances we created -> destroyed on unload
Thread = {}, -- task threads -> cancelled on unload
Undo = {}, -- restore closures -> called on unload (hooks, lighting, ...)
Frame = {}, -- ordered per-frame jobs
Alive = true,
}
env.KittyHub = KH
-- ---------------------------------------------------------------- lifecycle
function KH.track(conn)
if conn then KH.Conn[#KH.Conn + 1] = conn end
return conn
end
function KH.own(inst)
if inst then KH.Inst[#KH.Inst + 1] = inst end
return inst
end
function KH.undo(fn)
if fn then KH.Undo[#KH.Undo + 1] = fn end
end
-- Long-running loops: they check KH.Alive so unload actually stops them.
function KH.spawn(fn)
local thread = task.spawn(function()
local ok, err = pcall(fn)
if not ok and KH.Alive then
warn("[KittyHub] thread error: " .. tostring(err))
end
end)
KH.Thread[#KH.Thread + 1] = thread
return thread
end
-- Fire-and-forget: short work that needs no cancellation. Deliberately not
-- tracked — the aimbot spawns one per shot and the table would grow forever.
function KH.detach(fn)
return task.spawn(function()
local ok, err = pcall(fn)
if not ok and KH.Alive then
warn("[KittyHub] task error: " .. tostring(err))
end
end)
end
-- A loop that ends itself on unload. `interval` may be 0 for per-heartbeat.
function KH.loop(interval, fn)
return KH.spawn(function()
while KH.Alive do
local ok, err = pcall(fn)
if not ok then
warn("[KittyHub] loop error: " .. tostring(err))
task.wait(1)
end
task.wait(interval)
end
end)
end
-- ------------------------------------------------------------- error damping
-- A feature that throws every frame would spam the console into uselessness.
-- Report the first few failures per site, then go quiet.
local errSeen = {}
function KH.safe(name, fn, ...)
local ok, err = pcall(fn, ...)
if not ok then
local n = (errSeen[name] or 0) + 1
errSeen[name] = n
if n <= 3 then
warn(("[KittyHub] %s failed (%d): %s"):format(name, n, tostring(err)))
end
return false
end
return true
end
-- ------------------------------------------------------- per-frame scheduler
-- One RenderStepped connection drives everything, so ordering is explicit and
-- unload is a single disconnect.
function KH.onFrame(name, fn, order)
KH.Frame[#KH.Frame + 1] = {name = name, fn = fn, order = order or 50}
table.sort(KH.Frame, function(a, b) return a.order < b.order end)
end
-- ------------------------------------------------------------------ executor
-- Feature-detect once; several modules degrade gracefully without these.
local X = {}
X.getgenv = type(getgenv) == "function"
X.gethui = type(gethui) == "function"
X.writefile = type(writefile) == "function" and type(readfile) == "function"
X.listfiles = type(listfiles) == "function"
X.makefolder = type(makefolder) == "function" and type(isfolder) == "function"
X.firetouch = type(firetouchinterest) == "function"
X.hookmetamethod = type(hookmetamethod) == "function" and type(getnamecallmethod) == "function"
X.rawmeta = type(getrawmetatable) == "function" and type(setreadonly) == "function"
X.newcclosure = type(newcclosure) == "function"
X.checkcaller = type(checkcaller) == "function"
-- Synthetic input. Every executor names these the same way, and an executor
-- that ships none of them cannot drive the mouse aimbot.
X.mousemove = type(mousemoverel) == "function" or type(mousemoveabs) == "function"
X.mouseclick = type(mouse1click) == "function"
or (type(mouse1press) == "function" and type(mouse1release) == "function")
X.setclipboard = type(setclipboard) == "function"
X.queueteleport = type(queue_on_teleport) == "function"
or (type(syn) == "table" and type(syn.queue_on_teleport) == "function")
X.identifyexec = type(identifyexecutor) == "function"
X.name = X.identifyexec and select(1, identifyexecutor()) or "Unknown"
KH.X = X
KH.Services = {
Players = Players, RunService = RunService, UserInputService = UserInputService,
TweenService = TweenService, Lighting = Lighting, HttpService = HttpService,
StarterGui = StarterGui, ReplicatedStorage = ReplicatedStorage,
TeleportService = TeleportService, Stats = Stats, VirtualUser = VirtualUser,
}
KH.LocalPlayer = LocalPlayer
function KH.camera()
Camera = workspace.CurrentCamera or Camera
return Camera
end
-- ─── src/mm2/10_defaults.lua ───────────────────────────────────────
-- ============================================================================
-- DEFAULTS — the MM2 settings schema
-- ============================================================================
KH.GameTag = "MM2"
KH.GameName = "Murder Mystery 2"
do
-- Defaults double as the schema: a saved profile is backfilled from here
-- and keys that no longer exist are dropped, so upgrades never half-migrate.
local DEFAULTS = {
-- No line-of-sight or range gate on purpose: the server resolves the
-- position, so the murderer can be hit through a wall across the map.
Aim = {
Enabled = true,
Target = "Murderer", -- Murderer | Nearest | Crosshair
Mode = "Press", -- Press | Hold | Toggle | Always
Key = "C",
Method = "Mouse", -- Remote | Mouse
CameraSnap = true, -- Mouse method: face the target first
MouseSpeed = 0.6, -- camera turn only; the cursor always snaps
Prediction = 2.8, -- studs of lead per unit of velocity
PingComp = true, -- scale lead by measured ping
FireRate = 0.10, -- seconds between shots
KeepEquipped = true, -- re-draw the gun if it gets stowed
SilentAim = false, -- redirect your own manual shots
SilentMode = "Auto", -- Auto | Hook | Takeover | Click
DryRun = false, -- aim and report, never shoot
AimAtHead = false, -- Head instead of HumanoidRootPart
NotifyShot = false,
},
Knife = {
AutoThrow = false, -- master switch, like Aim.Enabled
ThrowMode = "Press", -- Press | Always
ThrowDelay = 1.2, -- Always mode only
ThrowRange = 70, -- past this the knife is just thrown away
ThrowKey = "G", -- one press, one throw
ThrowAnim = true, -- play MM2's own wind-up with the throw
ThrowRelease = 55, -- percent of it before the knife leaves the hand
Aura = false,
AuraRadius = 18,
AuraDelay = 0.15,
TpStab = false,
TpRange = 150, -- how far a blink is worth making
SkipSheriff = false,
TargetSheriff = false,
},
ESP = {
Enabled = true,
Boxes = true,
BoxStyle = "Corner", -- Corner | Full
Names = true,
Roles = true,
Distance = true,
Health = false,
Tracers = false,
TracerOrigin = "Bottom", -- Bottom | Center
Chams = false,
ChamsFill = 0.55,
OffScreen = false, -- edge arrows for off-screen players
MaxDistance = 2000,
OnlyRoles = false, -- hide plain innocents
TextSize = 13,
CoinESP = false,
GunESP = true,
TrapESP = true,
ColorMurderer = Color3.fromRGB(255, 64, 64),
ColorSheriff = Color3.fromRGB(64, 148, 255),
ColorInnocent = Color3.fromRGB(112, 232, 128),
ColorHero = Color3.fromRGB(255, 196, 64),
},
Farm = {
CoinFarm = false,
CoinMode = "Teleport", -- Teleport | Smooth | Walk
CoinSpeed = 90,
CoinDelay = 0.08,
Magnet = false,
MagnetRadius = 30,
AutoGrabGun = true,
GrabReturn = true, -- snap back after grabbing
AntiAFK = true,
LobbyFarm = true,
},
Safety = {
ProximityAlert = true,
AlertDistance = 45,
AlertSound = true,
AlertFlash = true,
AutoDodge = false,
DodgeDistance = 14,
DodgeHeight = 55,
RoleNotify = true,
},
Move = {
SpeedEnabled = false,
Speed = 32,
SpeedMode = "Humanoid", -- Humanoid | CFrame
JumpEnabled = false,
Jump = 70,
InfJump = false,
Bhop = false,
Noclip = false,
NoclipKey = "N",
Fly = false,
FlyKey = "F",
FlySpeed = 70,
Spinbot = false,
SafeLand = true, -- never arrive at the floor fast enough to hurt
},
Visual = {
Fullbright = false,
Brightness = 2,
NoFog = false,
FovEnabled = false,
Fov = 90,
Xray = false,
XrayTransp = 0.72,
LowDetail = false,
},
UI = {
MenuKey = "X",
Accent = Color3.fromRGB(178, 118, 255),
Watermark = true,
KeybindList = true,
Notifications = true,
AutoSave = true,
Profile = "default",
},
}
KH.Defaults = DEFAULTS
end
-- ─── src/_shared/20_config.lua ─────────────────────────────────────
-- ============================================================================
-- CONFIG — deep-merge over the game's defaults, and on-disk profiles
-- ============================================================================
local S -- the settings table every other module reads
do
local HttpService = KH.Services.HttpService
local X = KH.X
local DEFAULTS = KH.Defaults
-- Switches that are for right now, not for next time. Restoring fly or
-- noclip on join is never what anyone wanted, and anything that drives the
-- character coming back by itself is worse than losing the setting.
KH.Volatile = KH.Volatile or {"Move.Noclip", "Move.Fly"}
-- ------------------------------------------------------------ deep merge
local function deepCopy(t)
local out = {}
for k, v in pairs(t) do
out[k] = (typeof(v) == "table") and deepCopy(v) or v
end
return out
end
-- Fill gaps, drop strays, reject values whose type no longer matches — a
-- toggle that became a slider would crash the control reading it.
local function reconcile(dst, schema)
for k, v in pairs(schema) do
if typeof(v) == "table" then
if typeof(dst[k]) ~= "table" then dst[k] = {} end
reconcile(dst[k], v)
elseif dst[k] == nil or typeof(dst[k]) ~= typeof(v) then
dst[k] = v
end
end
for k in pairs(dst) do
if schema[k] == nil then dst[k] = nil end
end
end
S = deepCopy(DEFAULTS)
KH.S = S
-- ------------------------------------------------------- (de)serialising
-- Color3 has no JSON form, so it round-trips through a tagged table.
local function encode(v)
if typeof(v) == "Color3" then
return {__c3 = {
math.floor(v.R * 255 + 0.5),
math.floor(v.G * 255 + 0.5),
math.floor(v.B * 255 + 0.5),
}}
elseif typeof(v) == "table" then
local out = {}
for k, sub in pairs(v) do out[k] = encode(sub) end
return out
end
return v
end
local function decode(v)
if typeof(v) == "table" then
if typeof(v.__c3) == "table" then
return Color3.fromRGB(v.__c3[1] or 0, v.__c3[2] or 0, v.__c3[3] or 0)
end
local out = {}
for k, sub in pairs(v) do out[k] = decode(sub) end
return out
end
return v
end
-- ------------------------------------------------------------ disk layer
local ROOT, DIR = "KittyHub", "KittyHub/configs"
local Config = {}
KH.Config = Config
local function ensureDir()
if not (X.writefile and X.makefolder) then return false end
local ok = pcall(function()
if not isfolder(ROOT) then makefolder(ROOT) end
if not isfolder(DIR) then makefolder(DIR) end
end)
return ok
end
Config.available = ensureDir()
local function pathFor(name)
local safeName = tostring(name):gsub("[^%w_%-]", "_")
return DIR .. "/" .. safeName .. ".json"
end
function Config.list()
local names = {}
if not (Config.available and X.listfiles) then return names end
pcall(function()
for _, file in ipairs(listfiles(DIR)) do
local name = tostring(file):match("([^/\\]+)%.json$")
if name then names[#names + 1] = name end
end
end)
table.sort(names)
return names
end
function Config.save(name)
if not Config.available then return false, "executor has no file API" end
name = name or S.UI.Profile
local ok, err = pcall(function()
writefile(pathFor(name), HttpService:JSONEncode(encode(S)))
end)
return ok, err
end
function Config.load(name)
if not Config.available then return false, "executor has no file API" end
name = name or S.UI.Profile
local path = pathFor(name)
local ok, data = pcall(function()
if not isfile(path) then return nil end
return HttpService:JSONDecode(readfile(path))
end)
if not ok or typeof(data) ~= "table" then return false, "no such profile" end
local loaded = decode(data)
reconcile(loaded, DEFAULTS)
-- In place: every control captured a reference to its own sub-table,
-- so swapping S wholesale would orphan the lot.
for group, values in pairs(loaded) do
if typeof(S[group]) == "table" and typeof(values) == "table" then
for k, v in pairs(values) do S[group][k] = v end
end
end
for _, path in ipairs(KH.Volatile) do
local group, key = path:match("^(%w+)%.(%w+)$")
local schema = group and DEFAULTS[group]
if schema and schema[key] ~= nil then S[group][key] = schema[key] end
end
S.UI.Profile = name
return true
end
function Config.delete(name)
if not Config.available then return false end
return pcall(function() delfile(pathFor(name)) end)
end
function Config.reset()
local fresh = deepCopy(DEFAULTS)
for group, values in pairs(fresh) do
for k, v in pairs(values) do S[group][k] = v end
end
end
-- Autosave is debounced: sliders fire their callback on every mouse-move,
-- and writing a file per frame is a good way to stutter the game.
local pending = false
function Config.touch()
if not (S.UI.AutoSave and Config.available) or pending then return end
pending = true
KH.detach(function()
task.wait(1.5)
pending = false
Config.save(S.UI.Profile)
end)
end
end
-- ─── src/_shared/30_util.lua ───────────────────────────────────────
-- ============================================================================
-- UTIL — small helpers shared by every feature module
-- ============================================================================
local U = {}
KH.U = U
do
local Players = KH.Services.Players
local Stats = KH.Services.Stats
local LocalPlayer = KH.LocalPlayer
-- ------------------------------------------------------------------ math
function U.round(n, places)
local m = 10 ^ (places or 0)
return math.floor(n * m + 0.5) / m
end
function U.lerp(a, b, t) return a + (b - a) * t end
function U.clamp(n, lo, hi) return math.max(lo, math.min(hi, n)) end
-- ------------------------------------------------------------ characters
function U.charOf(player)
local char = player and player.Character
-- A character that has left the DataModel (respawn in flight) still
-- reads as a valid Instance, so check it is actually parented.
if char and char.Parent then return char end
return nil
end
function U.rootOf(player)
local char = U.charOf(player)
return char and (char:FindFirstChild("HumanoidRootPart") or char.PrimaryPart)
end
function U.humOf(player)
local char = U.charOf(player)
return char and char:FindFirstChildOfClass("Humanoid")
end
function U.isAliveChar(player)
local hum = U.humOf(player)
return hum ~= nil and hum.Health > 0
end
function U.myRoot() return U.rootOf(LocalPlayer) end
function U.myHum() return U.humOf(LocalPlayer) end
-- R15 names the torso differently from R6. Aim and prediction both want
-- the mass centre rather than the head.
function U.torsoOf(char)
return char:FindFirstChild("HumanoidRootPart")
or char:FindFirstChild("UpperTorso")
or char:FindFirstChild("Torso")
or char:FindFirstChild("Head")
end
function U.distanceTo(position)
local root = U.myRoot()
if not root then return math.huge end
return (root.Position - position).Magnitude
end
-- --------------------------------------------------------------- network
function U.ping()
local ok, ms = pcall(function()
return Stats.Network.ServerStatsItem["Data Ping"]:GetValue()
end)
if ok and typeof(ms) == "number" then return ms end
return 0
end
-- ---------------------------------------------------------------- screen
-- Screen position, on-screen flag, depth. Depth goes negative behind the
-- camera, which is what flips off-screen arrows to the right side.
function U.toScreen(worldPos)
local cam = KH.camera()
local v, onScreen = cam:WorldToViewportPoint(worldPos)
return Vector2.new(v.X, v.Y), onScreen, v.Z
end
-- ------------------------------------------------------------ prediction
-- The server checks the position against where the target is when the
-- packet lands, so the lead covers their motion and our round trip.
function U.predict(char, strength, usePing, partOverride)
local part = partOverride or U.torsoOf(char)
if not part then return nil end
local pos = part.Position
if strength <= 0 then return pos end
local hum = char:FindFirstChildOfClass("Humanoid")
local velocity = part.AssemblyLinearVelocity or Vector3.zero
local moveDir = hum and hum.MoveDirection or Vector3.zero
-- Y is damped hard: a jumping target's vertical velocity swings far
-- more than its hitbox does, and over-leading up is the usual miss.
local flattened = Vector3.new(velocity.X * 0.75, velocity.Y * 0.35, velocity.Z * 0.75)
local lead = flattened * (strength / 15) + moveDir * strength
if usePing then
local pingSec = U.clamp(U.ping() / 1000, 0, 0.5)
lead = lead + velocity * pingSec
end
return pos + lead
end
-- ------------------------------------------------------------- teleport
-- Preserves look direction so a farm teleport does not spin the camera.
function U.moveTo(position, keepLook)
local root = U.myRoot()
if not root then return false end
if keepLook then
root.CFrame = CFrame.new(position, position + root.CFrame.LookVector)
else
root.CFrame = CFrame.new(position)
end
return true
end
-- ----------------------------------------------------------------- sound
-- Asset id or full URI. `rbxasset://sounds/...` ships with the client, so
-- it always resolves — worth preferring for alerts that must be audible.
local soundCache = {}
function U.playSound(assetId, volume)
local sound = soundCache[assetId]
if not sound or not sound.Parent then
sound = Instance.new("Sound")
sound.SoundId = tostring(assetId):match("^rbx")
and tostring(assetId)
or ("rbxassetid://" .. tostring(assetId))
sound.Parent = KH.camera()
soundCache[assetId] = sound
KH.own(sound)
end
sound.Volume = volume or 0.5
pcall(function() sound:Play() end)
end
-- ------------------------------------------------------------- keycodes
function U.keyCode(name)
if typeof(name) ~= "string" then return nil end
local ok, key = pcall(function() return Enum.KeyCode[name] end)
return ok and key or nil
end
-- Roblox's chat box and our own text fields are both TextBoxes. A key
-- held while one of them has focus belongs to whoever is typing, not to a
-- feature: without this, saying "we" in chat flies you across the map.
function U.typing()
local ok, box = pcall(function()
return KH.Services.UserInputService:GetFocusedTextBox()
end)
return ok and box ~= nil
end
function U.keyHeld(name)
if U.typing() then return false end
local key = U.keyCode(name)
if not key then return false end
return KH.Services.UserInputService:IsKeyDown(key)
end
-- ------------------------------------------------------------- iteration
function U.otherPlayers()
local out = {}
for _, player in ipairs(Players:GetPlayers()) do
if player ~= LocalPlayer then out[#out + 1] = player end
end
return out
end
end
-- ─── src/mm2/35_game.lua ───────────────────────────────────────────
-- ============================================================================
-- GAME — everything that knows what Murder Mystery 2 actually looks like
--
-- Verified against the live game's structure:
-- roles ReplicatedStorage.GetPlayerData:InvokeServer() -> {[name] = {Role=...}}
-- ReplicatedStorage.Remotes.Gameplay.PlayerDataChanged (push)
-- shoot Character.Gun.KnifeLocal.CreateBeam.RemoteFunction:InvokeServer(1, pos, "AH2")
-- stab Character.Knife.Stab:FireServer("Down")
-- throw Character.Knife.Events.KnifeThrown:FireServer(fromCF, toCF)
-- map the workspace child owning both `CoinContainer` and `Spawns`
-- gun a BasePart named `GunDrop`, parented into the map when a sheriff dies
-- ============================================================================
local Game = {}
KH.Game = Game
do
local RS = KH.Services.ReplicatedStorage
local Players = KH.Services.Players
local U = KH.U
local LocalPlayer = KH.LocalPlayer
Game.Data = {} -- [playerName] = {Role = ..., Killed = ..., Dead = ...}
Game.Murderer = nil -- player name, or nil before roles are dealt
Game.Sheriff = nil
Game.Hero = nil
-- ------------------------------------------------------------- signals
local listeners = {}
function Game.on(event, fn)
listeners[event] = listeners[event] or {}
table.insert(listeners[event], fn)
end
local function emit(event, ...)
for _, fn in ipairs(listeners[event] or {}) do
KH.safe("event:" .. event, fn, ...)
end
end
-- ------------------------------------------------------------- remotes
-- MM2 moves these between updates, so resolve by recursive name lookup
-- and re-resolve if the cached instance gets reparented.
local remoteCache = {}
local function findRemote(name)
local cached = remoteCache[name]
if cached and cached.Parent then return cached end
local ok, found = pcall(function() return RS:FindFirstChild(name, true) end)
if ok and found then
remoteCache[name] = found
return found
end
return nil
end
-- --------------------------------------------------------- role tracking
-- Our own role is latched for the round. MM2 empties our hands the moment
-- the knife is thrown and the role table can arrive late, in pieces, or
-- still holding last round's answer — one unlucky read must never turn the
-- murderer back into an innocent.
local myLatch = nil
local function latch(role)
if role == "Murderer" or role == "Sheriff" or role == "Hero" then
myLatch = role
end
end
local function clearRoles()
Game.Data = {}
Game.Murderer, Game.Sheriff, Game.Hero = nil, nil, nil
myLatch = nil
end
Game.clearRoles = clearRoles
local function recompute()
local murderer, sheriff, hero
for name, entry in pairs(Game.Data) do
if typeof(entry) == "table" then
local role = entry.Role
if role == "Murderer" then murderer = name
elseif role == "Sheriff" then sheriff = name
elseif role == "Hero" then hero = name end
end
end
local changed = (murderer ~= Game.Murderer)
or (sheriff ~= Game.Sheriff)
or (hero ~= Game.Hero)
Game.Murderer, Game.Sheriff, Game.Hero = murderer, sheriff, hero
local me = LocalPlayer.Name
if murderer == me then latch("Murderer")
elseif sheriff == me then latch("Sheriff")
elseif hero == me then latch("Hero") end
if changed then emit("RoleChange", murderer, sheriff, hero) end
end
-- Only a payload that actually carries roles may replace what we have; a
-- partial push used to wipe the table and leave us knowing nothing.
local function applyData(data)
if typeof(data) ~= "table" then return end
local fresh, roles = {}, 0
for name, entry in pairs(data) do
if typeof(name) == "string" and typeof(entry) == "table" then
fresh[name] = entry
if typeof(entry.Role) == "string" then roles = roles + 1 end
end
end
if roles == 0 then return end
Game.Data = fresh
recompute()
end
Game.applyData = applyData
-- Some builds push one player's entry instead of the whole table.
local function applyOne(who, entry)
local name = who
if typeof(who) == "Instance" and who:IsA("Player") then name = who.Name end
if typeof(name) ~= "string" or typeof(entry) ~= "table" then return end
if typeof(entry.Role) ~= "string" then return end
Game.Data[name] = entry
recompute()
end
function Game.refreshRoles()
local remote = findRemote("GetPlayerData")
if not remote then return false end
local ok, data = pcall(function() return remote:InvokeServer() end)
if ok and typeof(data) == "table" then
applyData(data)
return true
end
return false
end
-- The server pushes the whole table on any state change, which is how the
-- murderer is known before their knife is visible to anyone.
KH.spawn(function()
local gameplay = findRemote("PlayerDataChanged")
if gameplay and gameplay:IsA("RemoteEvent") then
KH.track(gameplay.OnClientEvent:Connect(function(first, second)
if typeof(first) == "table" then applyData(first) else applyOne(first, second) end
end))
end
Game.refreshRoles()
end)
-- ------------------------------------------------- tool-based fallback
-- No remote needed, and the only way to spot a Hero the moment they pick
-- the gun up. Cached briefly: ESP asks for every role every frame.
local toolCache, toolCacheAt = {}, 0
local function toolRole(player)
local char = U.charOf(player)
local backpack = player:FindFirstChildOfClass("Backpack")
if (char and char:FindFirstChild("Knife"))
or (backpack and backpack:FindFirstChild("Knife")) then
return "Murderer"
end
if (char and char:FindFirstChild("Gun"))
or (backpack and backpack:FindFirstChild("Gun")) then
return "Sheriff"
end
return nil
end
local function cachedToolRole(player)
local now = os.clock()
if now - toolCacheAt > 0.25 then
toolCache, toolCacheAt = {}, now
end
local hit = toolCache[player]
if hit == nil then
hit = toolRole(player) or false
toolCache[player] = hit
end
return hit or nil
end
function Game.roleOf(player)
if not player then return "Innocent" end
local entry = Game.Data[player.Name]
local role = entry and entry.Role or nil
local held = cachedToolRole(player)
-- Only the murderer ever holds a knife, so that outranks the table —
-- including a table left standing from the round before.
if held == "Murderer" then return "Murderer" end
if role == nil or role == "Innocent" then
if held == "Sheriff" then
-- An innocent holding a gun picked it up off a dead sheriff.
role = (role == "Innocent") and "Hero" or "Sheriff"
end
end
return role or "Innocent"
end
function Game.colorOf(player)
local role = Game.roleOf(player)
local S = KH.S
if role == "Murderer" then return S.ESP.ColorMurderer, role end
if role == "Sheriff" then return S.ESP.ColorSheriff, role end
if role == "Hero" then return S.ESP.ColorHero, role end
return S.ESP.ColorInnocent, role
end
function Game.isAlive(player)
local entry = Game.Data[player.Name]
if entry and (entry.Killed or entry.Dead) then return false end
return U.isAliveChar(player)
end
function Game.myRole()
local role = Game.roleOf(LocalPlayer)
latch(role)
if role == "Innocent" and myLatch then return myLatch end
return role
end
function Game.amSheriff()
local role = Game.myRole()
return role == "Sheriff" or role == "Hero"
end
-- Asked from every angle: a wrong "no" walks the murderer onto the gun.
function Game.amMurderer()
if myLatch == "Murderer" then return true end
if Game.Murderer == LocalPlayer.Name then return true end
if Game.knifeTool() then return true end
return Game.myRole() == "Murderer"
end
-- Lets callers tell "innocent" apart from "no idea yet".
function Game.selfKnown()
if myLatch then return true end
local entry = Game.Data[LocalPlayer.Name]
return typeof(entry) == "table" and typeof(entry.Role) == "string"
end
-- Latch our role even when nothing asks: the knife is only ours briefly.
KH.loop(0.5, function() Game.myRole() end)
-- Poll until we know who the murderer is *and* what we are, then keep
-- re-asking slowly: a missed round boundary would otherwise leave last
-- round's roles standing for the whole of this one.
local polledAt = 0
KH.loop(2, function()
if Game.Murderer and Game.selfKnown() and os.clock() - polledAt < 10 then return end
polledAt = os.clock()
Game.refreshRoles()
end)
-- A fresh body means a fresh round; stale roles are worse than none.
KH.track(LocalPlayer.CharacterAdded:Connect(function()
clearRoles()
task.wait(1)
Game.refreshRoles()
end))
-- Resolve names to live Player objects, skipping the dead.
local function playerByName(name, requireAlive)
if not name then return nil end
local player = Players:FindFirstChild(name)
if not player then return nil end
if requireAlive and not Game.isAlive(player) then return nil end
return player
end
function Game.murdererPlayer()
local player = playerByName(Game.Murderer, true)
if player then return player end
-- Fallback for the window before role data arrives.
for _, other in ipairs(U.otherPlayers()) do
if cachedToolRole(other) == "Murderer" and Game.isAlive(other) then return other end
end
return nil
end
function Game.sheriffPlayer()
return playerByName(Game.Sheriff, true) or playerByName(Game.Hero, true)
end
-- ------------------------------------------------------------------ map
-- Whichever workspace child owns a CoinContainer. The lobby has one too,
-- so it is matched separately.
local function isMapModel(obj)
return obj:FindFirstChild("CoinContainer") ~= nil
end
function Game.map()
for _, obj in ipairs(workspace:GetChildren()) do
if obj.Name ~= "Lobby" and isMapModel(obj) then return obj end
end
return nil
end
function Game.lobby()
local lobby = workspace:FindFirstChild("Lobby")
if lobby and isMapModel(lobby) then return lobby end
return nil
end
function Game.inRound() return Game.map() ~= nil end
function Game.roundTime()
local part = workspace:FindFirstChild("RoundTimerPart")
if not part then return nil end
local ok, value = pcall(function() return part:GetAttribute("Time") end)
if ok and typeof(value) == "number" then return value end
return nil
end
-- ---------------------------------------------------------------- coins
-- Coin entries come in two shapes depending on map age: a bare BasePart, or
-- a `Coin_Server` model wrapping `CoinVisual.MainCoin`.
local function coinPart(obj)
if obj:IsA("BasePart") then return obj end
local visual = obj:FindFirstChild("CoinVisual")
if visual then
local main = visual:FindFirstChild("MainCoin")
if main and main:IsA("BasePart") then return main end
end
return obj:FindFirstChildWhichIsA("BasePart", true)
end
Game.coinPart = coinPart
function Game.coinContainers()
local out = {}
local map = Game.map()
if map then
local container = map:FindFirstChild("CoinContainer")
if container then out[#out + 1] = container end
end
if KH.S.Farm.LobbyFarm then
local lobby = Game.lobby()
local container = lobby and lobby:FindFirstChild("CoinContainer")
if container then out[#out + 1] = container end
end
return out
end
-- Returns {model = Instance, part = BasePart} pairs.
function Game.coins()
local out = {}
for _, container in ipairs(Game.coinContainers()) do
for _, obj in ipairs(container:GetChildren()) do
local part = coinPart(obj)
if part then out[#out + 1] = {model = obj, part = part} end
end
end
return out
end
function Game.nearestCoin(skip)
local root = U.myRoot()
if not root then return nil end
local best, bestDist = nil, math.huge
for _, coin in ipairs(Game.coins()) do
if not (skip and skip[coin.model]) then
local dist = (coin.part.Position - root.Position).Magnitude
if dist < bestDist then best, bestDist = coin, dist end
end
end
return best, bestDist
end
-- ------------------------------------------------ dropped gun and traps
-- By event, not by scanning: `GunDrop` appears at most once a round and a
-- per-frame sweep of an MM2 map is expensive.
Game.GunDrop = nil
Game.Traps = {}
local function noteDescendant(obj)
if obj.Name == "GunDrop" and obj:IsA("BasePart") then
Game.GunDrop = obj
emit("GunDropped", obj)
elseif obj.Name == "Trap" and obj:IsA("BasePart") then
Game.Traps[obj] = true
emit("TrapPlaced", obj)
end
end
local function forgetDescendant(obj)
if obj == Game.GunDrop then
Game.GunDrop = nil
emit("GunTaken", obj)
elseif Game.Traps[obj] then
Game.Traps[obj] = nil
end
end
KH.track(workspace.DescendantAdded:Connect(noteDescendant))
KH.track(workspace.DescendantRemoving:Connect(forgetDescendant))
-- Catch a gun already lying on the ground when the script is executed.
KH.spawn(function()
local map = Game.map()
if map then
local existing = map:FindFirstChild("GunDrop", true)
if existing and existing:IsA("BasePart") then Game.GunDrop = existing end
end
end)
-- --------------------------------------------------------- round change
KH.track(workspace.ChildAdded:Connect(function(obj)
task.wait(0.5) -- CoinContainer is parented in a frame or two after the model
if obj.Parent == workspace and obj.Name ~= "Lobby" and isMapModel(obj) then
Game.Traps = {}
Game.GunDrop = nil
clearRoles()
emit("RoundStart", obj)
KH.detach(Game.refreshRoles)
end
end))
KH.track(workspace.ChildRemoved:Connect(function(obj)
if obj.Name ~= "Lobby" and obj:FindFirstChild("CoinContainer") then
clearRoles()
Game.Traps = {}
Game.GunDrop = nil
emit("RoundEnd")
end
end))
-- ---------------------------------------------------------------- tools
local function findTool(name)
local char = U.charOf(LocalPlayer)
local held = char and char:FindFirstChild(name)
if held then return held, true end
local backpack = LocalPlayer:FindFirstChildOfClass("Backpack")
local stowed = backpack and backpack:FindFirstChild(name)
if stowed then return stowed, false end
return nil, false
end
function Game.gunTool() return findTool("Gun") end
function Game.knifeTool() return findTool("Knife") end
function Game.equip(tool)
if not tool then return false end
local char = U.charOf(LocalPlayer)
if not char then return false end
if tool.Parent == char then return true end
local hum = char:FindFirstChildOfClass("Humanoid")
if not hum then return false end
return pcall(function() hum:EquipTool(tool) end)
end
-- ---------------------------------------------------------------- shoot
-- A RemoteFunction taking a world position; the server does the hit test.
-- Aim means sending the right coordinates, not moving the camera.
local beamCache
local function beamRemote(gun)
if beamCache and beamCache.Parent and beamCache:IsDescendantOf(gun) then
return beamCache
end
local remote
local knifeLocal = gun:FindFirstChild("KnifeLocal")
local createBeam = knifeLocal and knifeLocal:FindFirstChild("CreateBeam")
if createBeam then
remote = createBeam:FindFirstChild("RemoteFunction")
or (createBeam:IsA("RemoteFunction") and createBeam)
end
-- Loose fallback in case the tool's internals get renamed again.
if not remote then
remote = gun:FindFirstChildWhichIsA("RemoteFunction", true)
end
beamCache = remote
return remote
end
Game.beamRemote = beamRemote
-- Older builds of the gun expect the tag "AH"; current ones want "AH2".
-- Start on the current one and latch onto whichever the server accepts.
local SHOT_TAGS = {"AH2", "AH"}
local tagIndex = 1
local equipPending = false
Game.ShotsFired = 0
function Game.shoot(position)
local gun, equipped = Game.gunTool()
if not gun then return false, "no gun" end
if not equipped then
-- One waiter at a time, or a held aim key stacks a fresh one every
-- frame and they all fire at once when the gun lands.
if equipPending then return false, "equipping" end
-- MM2 rejects a shot from a gun that is not actually held, so draw
-- it first. It stays out afterwards — nothing here ever stows it.
if not Game.equip(gun) then return false, "could not equip" end
end
local remote = beamRemote(gun)
if not remote then return false, "no shot remote" end
-- InvokeServer blocks until the server replies. Off the render thread it
-- goes, or a laggy round would freeze the whole menu.
KH.detach(function()
-- EquipTool is not instant, and the server drops a shot from a gun
-- it does not yet think we hold — the first shot after a pickup.
if not equipped then
equipPending = true
local char = U.charOf(LocalPlayer)
local began = os.clock()
while gun.Parent ~= char and os.clock() - began < 0.35 do
task.wait()
char = U.charOf(LocalPlayer)
end
equipPending = false
end
local ok = pcall(function()
remote:InvokeServer(1, position, SHOT_TAGS[tagIndex])
end)
if not ok then
tagIndex = (tagIndex % #SHOT_TAGS) + 1
pcall(function()
remote:InvokeServer(1, position, SHOT_TAGS[tagIndex])
end)
end
end)
Game.ShotsFired = Game.ShotsFired + 1
return true
end
-- ---------------------------------------------------------------- knife
function Game.stab()
local knife, equipped = Game.knifeTool()
if not knife then return false, "no knife" end
if not equipped and not Game.equip(knife) then return false, "could not equip" end
local stab = knife:FindFirstChild("Stab")
if not stab or not stab:IsA("RemoteEvent") then return false, "no stab remote" end
pcall(function() stab:FireServer("Down") end)
-- The swing is a down/up pair; without the release the tool can stick.
KH.detach(function()
task.wait(0.06)
pcall(function() stab:FireServer("Up") end)
end)
return true
end
-- ------------------------------------------------------ throw animation
-- The throw remote makes the knife fly; it does nothing to your character.
-- MM2 plays the wind-up from its own client script, off an input we never
-- make, so a scripted throw looks like a knife leaving a statue's hand.
--
-- The asset id is never hardcoded — it is read off whatever animation MM2
-- ships on the tool, so a game update that renames or replaces it is
-- picked up rather than silently played wrong. Resolved once per knife and
-- loaded once per character: a throw pays for none of this twice.
local anim = {tool = nil, source = nil, animator = nil, track = nil}
local function findThrowAnimation(knife)
local only, count
for _, inst in ipairs(knife:GetDescendants()) do
if inst:IsA("Animation") and inst.AnimationId ~= "" then
if inst.Name:lower():find("throw") or inst.Name:lower():find("toss") then
return inst
end
count, only = (count or 0) + 1, inst
end
end
-- One animation on a throwing knife is the throw. Two or more and
-- guessing means playing a stab or an idle instead, which looks worse
-- than playing nothing.
return count == 1 and only or nil
end
local function animatorOf(char)
local hum = char and char:FindFirstChildOfClass("Humanoid")
if not hum then return nil end
return hum:FindFirstChildOfClass("Animator") or hum
end
-- Resolve and load without playing. The track's length is what the release
-- delay is measured from and it reads zero until Roblox has actually
-- fetched the asset, so doing this ahead of time is the difference between
-- the first throw of a round looking right and looking like the rest.
function Game.primeThrowAnimation(knife)
knife = knife or Game.knifeTool()
if not knife then return nil end
if anim.tool ~= knife then
anim.tool, anim.source, anim.track = knife, findThrowAnimation(knife), nil
end
if not anim.source then return nil end
local animator = animatorOf(U.charOf(LocalPlayer))
if not animator then return nil end
if anim.animator ~= animator then
anim.animator, anim.track = animator, nil
end
if not anim.track then
local ok, track = pcall(function() return animator:LoadAnimation(anim.source) end)
if not ok or not track then return nil end
anim.track = track
end
return anim.track
end
-- Nobody throws a knife on the first frame of the wind-up. Play it, then
-- say how long the knife should stay in the hand — the arm has to come back
-- before anything leaves it, and firing the remote immediately is what made
-- the throw read as a script no matter how good the animation looked.
--
-- A hard ceiling regardless of the slider: a knife still in your hand most
-- of a second after you decided to throw it is a bug, not a style.
local RELEASE_CAP = 0.75
local function playThrowAnimation()
if not KH.S.Knife.ThrowAnim then return 0 end
local track = Game.primeThrowAnimation()
if not track then return 0 end
-- Restart rather than stack: two throws in quick succession should look
-- like two throws, not one blended into itself.
local ok = pcall(function()
if track.IsPlaying then track:Stop(0) end
track:Play(0.05)
end)
if not ok then return 0 end
-- Still no length means the asset has not landed yet. That throw goes
-- out immediately rather than waiting on a delay nothing can measure —
-- exactly what it did before the animation existed.
local length = track.Length
if typeof(length) ~= "number" or length <= 0 then return 0 end
local point = U.clamp((KH.S.Knife.ThrowRelease or 55) / 100, 0, 1)
return math.min(length * point, RELEASE_CAP)
end
-- Leaving a wind-up frozen on the character is the one thing unloading
-- must not do.
KH.undo(function()
if anim.track then pcall(function() anim.track:Stop(0) end) end
end)
-- One throw in the air at a time. The hold below yields, and a second call
-- landing inside it would restart the animation under a knife that has
-- already been committed.
local holding = false
-- `refresh`, when given, is asked for the aim again at the moment the knife
-- actually leaves the hand. Without it a held throw would fly at wherever
-- the target stood when the arm started moving.
function Game.throwKnife(targetPos, refresh)
if holding then return false, "mid-throw" end
local knife, equipped = Game.knifeTool()
if not knife then return false, "no knife" end
if not equipped and not Game.equip(knife) then return false, "could not equip" end
local char = U.charOf(LocalPlayer)
local hand = char and (char:FindFirstChild("RightHand")
or char:FindFirstChild("Right Arm")
or char:FindFirstChild("HumanoidRootPart"))
if not hand then return false, "no hand" end
local events = knife:FindFirstChild("Events")
local thrown = events and events:FindFirstChild("KnifeThrown")
if not thrown then thrown = knife:FindFirstChild("Throw") end
if not thrown or not thrown:IsA("RemoteEvent") then return false, "no throw remote" end
-- Built at the moment of release, not before it: both the hand and the
-- target have moved by then.
--
-- Orientation, not just position: MM2 flies the knife along the CFrame
-- it is handed, and a bare CFrame.new(position) faces down the world
-- axis rather than at the target. Degenerate when the two points
-- coincide, so that case keeps the old bare frame.
local function send(point)
local from = CFrame.new(hand.Position)
if (point - hand.Position).Magnitude > 0.5 then
from = CFrame.new(hand.Position, point)
end
pcall(function()
thrown:FireServer(from, CFrame.new(point) * (from - from.Position))
end)
end
local hold = playThrowAnimation()
if hold <= 0 then
send(targetPos)
return true
end
holding = true
KH.detach(function()
-- pcall, or one error leaves the flag stuck on forever.
pcall(function()
task.wait(hold)
-- Dying mid-wind-up, stowing the knife, or unloading the script
-- cancels the throw rather than firing a remote from a hand
-- that is not there any more.
if not KH.Alive then return end
if not (hand.Parent and knife.Parent == U.charOf(LocalPlayer)) then return end
send(refresh and refresh() or targetPos)
end)
holding = false
end)
return true
end
end
-- ─── src/_shared/40_ui_core.lua ────────────────────────────────────
-- ============================================================================
-- UI CORE — window shell, tabs, notifications, watermark, theming
-- ============================================================================
local UI = {}
KH.UI = UI
do
local TweenService = KH.Services.TweenService
local UserInputService = KH.Services.UserInputService
local Lighting = KH.Services.Lighting
local LocalPlayer = KH.LocalPlayer
local S = KH.S
-- ------------------------------------------------------------- palette
local C = {
Bg = Color3.fromRGB(13, 13, 18),
Panel = Color3.fromRGB(20, 20, 28),
Card = Color3.fromRGB(25, 25, 34),
Row = Color3.fromRGB(31, 31, 42),
RowHover = Color3.fromRGB(40, 40, 54),
Stroke = Color3.fromRGB(42, 42, 56),
Text = Color3.fromRGB(237, 237, 242),
TextDim = Color3.fromRGB(138, 138, 160),
TextFaint = Color3.fromRGB(90, 90, 112),
Off = Color3.fromRGB(51, 51, 74),
Good = Color3.fromRGB(74, 222, 128),
Bad = Color3.fromRGB(248, 113, 113),
Warn = Color3.fromRGB(251, 191, 36),
}
C.Accent = S.UI.Accent
UI.C = C
-- ------------------------------------------------------- instance sugar
function UI.make(class, props, children)
local inst = Instance.new(class)
for k, v in pairs(props or {}) do
if k ~= "Parent" then inst[k] = v end
end
for _, child in ipairs(children or {}) do child.Parent = inst end
if props and props.Parent then inst.Parent = props.Parent end
return inst
end
local make = UI.make
function UI.corner(parent, radius)
return make("UICorner", {CornerRadius = UDim.new(0, radius or 8), Parent = parent})
end
function UI.stroke(parent, color, thickness, transparency)
return make("UIStroke", {
Color = color or C.Stroke,
Thickness = thickness or 1,
Transparency = transparency or 0,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
Parent = parent,
})
end
function UI.pad(parent, all, top, bottom, left, right)
return make("UIPadding", {
PaddingTop = UDim.new(0, top or all or 0),
PaddingBottom = UDim.new(0, bottom or all or 0),
PaddingLeft = UDim.new(0, left or all or 0),
PaddingRight = UDim.new(0, right or all or 0),
Parent = parent,
})
end
function UI.list(parent, padding, align)
return make("UIListLayout", {
Padding = UDim.new(0, padding or 6),
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = align or Enum.HorizontalAlignment.Center,
Parent = parent,
})
end
function UI.gradient(parent, from, to, rotation)
return make("UIGradient", {
Color = ColorSequence.new(from, to),
Rotation = rotation or 0,
Parent = parent,
})
end
local QUAD = TweenInfo.new(0.16, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
function UI.tween(obj, duration, props, style)
local info = duration and TweenInfo.new(duration,
style or Enum.EasingStyle.Quad, Enum.EasingDirection.Out) or QUAD
local t = TweenService:Create(obj, info, props)
t:Play()
return t
end
local tween = UI.tween
-- --------------------------------------------------------- accent theme
-- Anything painted with the accent registers here so the colour picker in
-- Settings can repaint the whole menu live.
local accented = {}
function UI.accented(obj, property, shade)
accented[#accented + 1] = {obj = obj, prop = property or "BackgroundColor3", shade = shade}
return obj
end
local function shadeOf(color, shade)
if not shade then return color end
local h, s, v = color:ToHSV()
return Color3.fromHSV(h, math.clamp(s * (shade.s or 1), 0, 1), math.clamp(v * (shade.v or 1), 0, 1))
end
function UI.applyAccent(color)
C.Accent = color
S.UI.Accent = color
for i = #accented, 1, -1 do
local entry = accented[i]
if entry.obj and entry.obj.Parent then
local target = shadeOf(color, entry.shade)
if entry.prop == "Gradient" then
entry.obj.Color = ColorSequence.new(shadeOf(color, {v = 1.25, s = 0.75}), target)
else
pcall(function() entry.obj[entry.prop] = target end)
end
else
table.remove(accented, i)
end
end
end
-- ------------------------------------------------------------- host gui
local function guiParent()
if KH.X.gethui then
local ok, hui = pcall(gethui)
if ok and hui then return hui end
end
local ok, coreGui = pcall(function() return game:GetService("CoreGui") end)
if ok and coreGui then return coreGui end
return LocalPlayer:WaitForChild("PlayerGui")
end
local function newScreen(name, order)
local gui = make("ScreenGui", {
Name = name,
ResetOnSpawn = false,
IgnoreGuiInset = true,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
DisplayOrder = order,
})
pcall(function() gui.Parent = guiParent() end)
return KH.own(gui)
end
-- Randomised names so a second copy of the menu never collides with ours.
local suffix = tostring(math.random(100000, 999999))
UI.Screen = newScreen("kh_" .. suffix, 10000)
UI.World = newScreen("kw_" .. suffix, 9998) -- ESP layer, below the menu
UI.Overlay = newScreen("ko_" .. suffix, 10001) -- notifications, watermark
-- ---------------------------------------------------------- drop shadow
-- Standard 9-slice shadow image; if the asset ever fails to resolve it just
-- renders as nothing rather than erroring.
function UI.shadow(parent, spread, transparency)
spread = spread or 22
return make("ImageLabel", {
Name = "Shadow",
BackgroundTransparency = 1,
Image = "rbxassetid://6014261993",
ImageColor3 = Color3.new(0, 0, 0),
ImageTransparency = transparency or 0.45,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(49, 49, 450, 450),
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(1, spread * 2, 1, spread * 2),
ZIndex = 0,
Parent = parent,
})
end
-- ============================================================== WINDOW
local WIN_W, WIN_H = 660, 452
local SIDEBAR_W, TOPBAR_H = 152, 46
local Root = make("Frame", {
Name = "Root",
Size = UDim2.fromOffset(WIN_W, WIN_H),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Visible = false,
Parent = UI.Screen,
})
UI.Root = Root
UI.shadow(Root, 26, 0.4)
local Window = make("Frame", {
Name = "Window",
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = C.Bg,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = Root,
})
UI.corner(Window, 12)
UI.stroke(Window, C.Stroke, 1)
UI.Window = Window
-- A soft accent wash across the top edge, so the window reads as themed
-- rather than as a flat grey box.
local wash = make("Frame", {
Size = UDim2.new(1, 0, 0, 150),
BackgroundColor3 = C.Accent,
BackgroundTransparency = 0.93,
BorderSizePixel = 0,
Parent = Window,
})
UI.accented(wash, "BackgroundColor3")
make("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(1, 1),
}),
Rotation = 90,
Parent = wash,
})
-- ------------------------------------------------------------- top bar
local TopBar = make("Frame", {
Name = "TopBar",
Size = UDim2.new(1, 0, 0, TOPBAR_H),
BackgroundTransparency = 1,
Parent = Window,
})
local titleHolder = make("Frame", {
BackgroundTransparency = 1,
Position = UDim2.fromOffset(18, 0),
Size = UDim2.new(0, 300, 1, 0),
Parent = TopBar,
})
local title = make("TextLabel", {
Text = "Kitty Hub",
Font = Enum.Font.GothamBold,
TextSize = 19,
TextColor3 = C.Text,
BackgroundTransparency = 1,
Size = UDim2.new(0, 92, 1, 0),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = titleHolder,
})
UI.accented(UI.gradient(title, C.Accent, C.Accent, 25), "Gradient")
local badge = make("Frame", {
BackgroundColor3 = C.Accent,
BackgroundTransparency = 0.82,
Position = UDim2.fromOffset(96, 13),
Size = UDim2.fromOffset(46, 20),
BorderSizePixel = 0,
Parent = titleHolder,
})
UI.corner(badge, 6)
UI.accented(badge, "BackgroundColor3")
local badgeText = make("TextLabel", {
Text = KH.GameTag or "HUB",
Font = Enum.Font.GothamBold,
TextSize = 11,
TextColor3 = C.Accent,
BackgroundTransparency = 1,
Size = UDim2.fromScale(1, 1),
Parent = badge,
})
UI.accented(badgeText, "TextColor3", {v = 1.35, s = 0.5})
make("TextLabel", {
Text = "v" .. KH.Version,
Font = Enum.Font.Gotham,
TextSize = 11,
TextColor3 = C.TextFaint,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(150, 0),
Size = UDim2.new(0, 60, 1, 0),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = titleHolder,
})
local function topButton(text, offsetX, hoverColor)
local btn = make("TextButton", {
Text = text,
Font = Enum.Font.GothamBold,
TextSize = 15,
TextColor3 = C.TextDim,
BackgroundColor3 = C.Row,
BackgroundTransparency = 1,
AutoButtonColor = false,
BorderSizePixel = 0,
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, offsetX, 0.5, 0),
Size = UDim2.fromOffset(28, 28),
Parent = TopBar,
})
UI.corner(btn, 7)
btn.MouseEnter:Connect(function()
tween(btn, 0.12, {BackgroundTransparency = 0, TextColor3 = hoverColor or C.Text})
end)
btn.MouseLeave:Connect(function()
tween(btn, 0.12, {BackgroundTransparency = 1, TextColor3 = C.TextDim})
end)
return btn
end
local closeBtn = topButton("✕", -12, C.Bad)
local minBtn = topButton("—", -46)
-- ------------------------------------------------------------- sidebar
local Sidebar = make("Frame", {
Name = "Sidebar",
Size = UDim2.new(0, SIDEBAR_W, 1, -TOPBAR_H),
Position = UDim2.fromOffset(0, TOPBAR_H),
BackgroundColor3 = C.Panel,
BackgroundTransparency = 0.35,
BorderSizePixel = 0,
Parent = Window,
})
make("Frame", { -- hairline divider
Size = UDim2.new(0, 1, 1, 0),
Position = UDim2.new(1, -1, 0, 0),
BackgroundColor3 = C.Stroke,
BorderSizePixel = 0,
Parent = Sidebar,
})
local tabHolder = make("Frame", {
Size = UDim2.new(1, 0, 1, -58),
BackgroundTransparency = 1,
Parent = Sidebar,
})
UI.list(tabHolder, 3)
UI.pad(tabHolder, 0, 12, 0, 10, 10)
-- The indicator glides between tabs instead of hard-cutting.
local indicator = make("Frame", {
Size = UDim2.fromOffset(3, 18),
Position = UDim2.fromOffset(0, 16),
AnchorPoint = Vector2.new(0, 0.5),
BackgroundColor3 = C.Accent,
BorderSizePixel = 0,
Visible = false,
ZIndex = 3,
Parent = Sidebar,
})
UI.corner(indicator, 2)
UI.accented(indicator, "BackgroundColor3")
-- Status strip pinned to the bottom of the sidebar.
local statusBox = make("Frame", {
Size = UDim2.new(1, -20, 0, 44),
Position = UDim2.new(0, 10, 1, -52),
BackgroundColor3 = C.Card,
BackgroundTransparency = 0.3,
BorderSizePixel = 0,
Parent = Sidebar,
})
UI.corner(statusBox, 8)
local roleLabel = make("TextLabel", {
Text = "Waiting…",
Font = Enum.Font.GothamBold,
TextSize = 13,
TextColor3 = C.TextDim,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(10, 5),
Size = UDim2.new(1, -20, 0, 16),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = statusBox,
})
local statLabel = make("TextLabel", {
Text = "—",
Font = Enum.Font.Gotham,
TextSize = 11,
TextColor3 = C.TextFaint,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(10, 22),
Size = UDim2.new(1, -20, 0, 14),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = statusBox,
})
UI.RoleLabel, UI.StatLabel = roleLabel, statLabel
-- ------------------------------------------------------------- content
local Content = make("Frame", {
Name = "Content",
Size = UDim2.new(1, -SIDEBAR_W, 1, -TOPBAR_H),
Position = UDim2.fromOffset(SIDEBAR_W, TOPBAR_H),
BackgroundTransparency = 1,
Parent = Window,
})
-- Search box: filters every control by label across the active tab.
local searchWrap = make("Frame", {
Size = UDim2.new(1, -28, 0, 30),
Position = UDim2.fromOffset(14, 8),
BackgroundColor3 = C.Row,
BackgroundTransparency = 0.25,
BorderSizePixel = 0,
Parent = Content,
})
UI.corner(searchWrap, 8)
make("TextLabel", {
Text = "⌕",
Font = Enum.Font.GothamBold,
TextSize = 15,
TextColor3 = C.TextFaint,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(10, 0),
Size = UDim2.fromOffset(16, 30),
Parent = searchWrap,
})
local searchBox = make("TextBox", {
Text = "",
PlaceholderText = "Search settings…",
PlaceholderColor3 = C.TextFaint,
Font = Enum.Font.Gotham,
TextSize = 13,
TextColor3 = C.Text,
BackgroundTransparency = 1,
ClearTextOnFocus = false,
Position = UDim2.fromOffset(30, 0),
Size = UDim2.new(1, -40, 1, 0),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = searchWrap,
})
local Pages = make("Frame", {
Size = UDim2.new(1, 0, 1, -44),
Position = UDim2.fromOffset(0, 44),
BackgroundTransparency = 1,
Parent = Content,
})
-- ============================================================== TABS
local tabs = {}
UI.Tabs = tabs
local activeTab
local tabOrder = 0
function UI.selectTab(name)
local tab = tabs[name]
if not tab then return end
activeTab = name
UI.ActiveTab = name
for tabName, entry in pairs(tabs) do
local on = (tabName == name)
entry.page.Visible = on
tween(entry.button, 0.14, {BackgroundTransparency = on and 0.86 or 1})
tween(entry.label, 0.14, {TextColor3 = on and C.Text or C.TextDim})
end
indicator.Visible = true
tween(indicator, 0.2, {
Position = UDim2.fromOffset(0, tab.button.AbsolutePosition.Y
- Sidebar.AbsolutePosition.Y + tab.button.AbsoluteSize.Y / 2),
}, Enum.EasingStyle.Back)
end
function UI.addTab(name)
tabOrder = tabOrder + 1
local button = make("TextButton", {
Name = name,
Text = "",
AutoButtonColor = false,
BackgroundColor3 = C.Accent,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 34),
LayoutOrder = tabOrder,
Parent = tabHolder,
})
UI.corner(button, 7)
UI.accented(button, "BackgroundColor3")
local textLabel = make("TextLabel", {
Text = name,
Font = Enum.Font.GothamMedium,
TextSize = 13,
TextColor3 = C.TextDim,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(15, 0),
Size = UDim2.new(1, -21, 1, 0),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = button,
})
local page = make("ScrollingFrame", {
Name = name,
Visible = false,
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = 3,
ScrollBarImageColor3 = C.Accent,
ScrollBarImageTransparency = 0.3,
CanvasSize = UDim2.new(),
AutomaticCanvasSize = Enum.AutomaticSize.Y,
Parent = Pages,
})
UI.accented(page, "ScrollBarImageColor3")
UI.list(page, 10)
UI.pad(page, 0, 2, 16, 14, 14)
local tab = {name = name, button = button, page = page, label = textLabel, sections = {}}
tabs[name] = tab
button.MouseEnter:Connect(function()
if activeTab ~= name then tween(button, 0.1, {BackgroundTransparency = 0.94}) end
end)
button.MouseLeave:Connect(function()
if activeTab ~= name then tween(button, 0.1, {BackgroundTransparency = 1}) end
end)
button.MouseButton1Click:Connect(function() UI.selectTab(name) end)
return tab
end
-- ---------------------------------------------------------- sections
-- Every control lives inside a card. Cards auto-size to their contents so
-- nothing has to declare a pixel height.
function UI.section(tab, titleText)
tab.order = (tab.order or 0) + 1
local card = make("Frame", {
BackgroundColor3 = C.Card,
BackgroundTransparency = 0.25,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
LayoutOrder = tab.order,
Parent = tab.page,
})
UI.corner(card, 10)
UI.stroke(card, C.Stroke, 1, 0.35)
make("TextLabel", {
Text = titleText:upper(),
Font = Enum.Font.GothamBold,
TextSize = 11,
TextColor3 = C.TextDim,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(14, 11),
Size = UDim2.new(1, -28, 0, 14),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = card,
})
local body = make("Frame", {
BackgroundTransparency = 1,
Position = UDim2.fromOffset(0, 30),
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Parent = card,
})
UI.list(body, 4)
UI.pad(body, 0, 0, 10, 10, 10)
local section = {card = card, body = body, rows = {}, tab = tab, order = 0}
table.insert(tab.sections, section)
return section
end
-- Controls call this so each row lands below the previous one. Without an
-- explicit LayoutOrder every row ties at 0 and the order is undefined.
function UI.nextOrder(section)
section.order = (section.order or 0) + 1
return section.order
end
-- ------------------------------------------------------------- search
-- Rows register their searchable text; sections with no visible row hide
-- themselves so the results do not leave empty cards floating around.
function UI.registerSearch(section, frame, text)
table.insert(section.rows, {frame = frame, text = text:lower()})
end
local function applySearch(query)
query = query:lower():gsub("^%s+", ""):gsub("%s+$", "")
for _, tab in pairs(tabs) do
for _, section in ipairs(tab.sections) do
local anyVisible = false
for _, row in ipairs(section.rows) do
local visible = (query == "") or row.text:find(query, 1, true) ~= nil
row.frame.Visible = visible
anyVisible = anyVisible or visible
end
section.card.Visible = anyVisible or #section.rows == 0
end
end
end
searchBox:GetPropertyChangedSignal("Text"):Connect(function()
applySearch(searchBox.Text)
end)
-- ==================================================== OPEN / CLOSE / DRAG
local blur = make("BlurEffect", {Name = "kh_blur", Size = 0, Enabled = false, Parent = Lighting})
KH.own(blur)
local isOpen = false
UI.IsOpen = false
function UI.setOpen(open, instant)
if open == isOpen then return end
isOpen = open
UI.IsOpen = open
if open then
Root.Visible = true
UI.OpenButton.Visible = false
blur.Enabled = true
if instant then
Root.Size = UDim2.fromOffset(WIN_W, WIN_H)
blur.Size = 12
else
Root.Size = UDim2.fromOffset(WIN_W * 0.94, WIN_H * 0.94)
tween(Root, 0.22, {Size = UDim2.fromOffset(WIN_W, WIN_H)}, Enum.EasingStyle.Back)
tween(blur, 0.22, {Size = 12})
end
else
tween(Root, 0.16, {Size = UDim2.fromOffset(WIN_W * 0.94, WIN_H * 0.94)})
tween(blur, 0.16, {Size = 0})
task.delay(0.17, function()
if not isOpen then
Root.Visible = false
blur.Enabled = false
if UI.OpenButton then UI.OpenButton.Visible = true end
end
end)
end
end
function UI.toggleOpen() UI.setOpen(not isOpen) end
closeBtn.MouseButton1Click:Connect(function() UI.setOpen(false) end)
minBtn.MouseButton1Click:Connect(function() UI.setOpen(false) end)
-- Floating re-open button.
local openBtn = make("TextButton", {
Name = "Open",
Text = " Kitty Hub",
Font = Enum.Font.GothamBold,
TextSize = 14,
TextColor3 = C.Text,
BackgroundColor3 = C.Accent,
AutoButtonColor = false,
BorderSizePixel = 0,
Size = UDim2.fromOffset(116, 38),
Position = UDim2.new(0, 20, 1, -62),
Parent = UI.Screen,
})
UI.corner(openBtn, 10)
UI.accented(openBtn, "BackgroundColor3")
UI.shadow(openBtn, 14, 0.55)
UI.OpenButton = openBtn
openBtn.MouseButton1Click:Connect(function() UI.setOpen(true) end)
openBtn.MouseEnter:Connect(function() tween(openBtn, 0.12, {Size = UDim2.fromOffset(122, 40)}) end)
openBtn.MouseLeave:Connect(function() tween(openBtn, 0.12, {Size = UDim2.fromOffset(116, 38)}) end)
-- Dragging, from the top bar only.
do
local dragging, dragStart, startPos
KH.track(TopBar.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragging, dragStart, startPos = true, input.Position, Root.Position
input.Changed:Connect(function()
if input.UserInputState == Enum.UserInputState.End then dragging = false end
end)
end
end))
KH.track(UserInputService.InputChanged:Connect(function(input)
if not dragging then return end
if input.UserInputType == Enum.UserInputType.MouseMovement
or input.UserInputType == Enum.UserInputType.Touch then
local delta = input.Position - dragStart
Root.Position = UDim2.new(
startPos.X.Scale, startPos.X.Offset + delta.X,
startPos.Y.Scale, startPos.Y.Offset + delta.Y)
end
end))
end
-- =========================================================== NOTIFICATIONS
local notifyHolder = make("Frame", {
Name = "Notifications",
AnchorPoint = Vector2.new(1, 1),
Position = UDim2.new(1, -18, 1, -18),
Size = UDim2.fromOffset(280, 400),
BackgroundTransparency = 1,
Parent = UI.Overlay,
})
make("UIListLayout", {
Padding = UDim.new(0, 8),
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Bottom,
Parent = notifyHolder,
})
local KIND_COLOR = {info = nil, good = C.Good, bad = C.Bad, warn = C.Warn}
local notifySeq = 0
-- A card inside a layout-managed slot: the list stacks the slot, the card
-- slides within it. A list layout overwrites its children's Position.
function UI.notify(opts)
if not S.UI.Notifications then return end
opts = opts or {}
local duration = opts.duration or 3.5
local accent = KIND_COLOR[opts.kind or "info"] or C.Accent
notifySeq = notifySeq + 1
local slot = make("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = notifySeq,
Parent = notifyHolder,
})
local card = make("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Position = UDim2.fromOffset(320, 0),
BackgroundColor3 = C.Card,
BorderSizePixel = 0,
Parent = slot,
})
UI.corner(card, 9)
UI.stroke(card, C.Stroke, 1, 0.2)
UI.shadow(card, 12, 0.6)
UI.pad(card, 0, 10, 12, 0, 0)
make("Frame", {
Size = UDim2.new(0, 3, 1, -20),
Position = UDim2.fromOffset(0, 10),
BackgroundColor3 = accent,
BorderSizePixel = 0,
Parent = card,
})
make("TextLabel", {
Text = opts.title or "Kitty Hub",
Font = Enum.Font.GothamBold,
TextSize = 13,
TextColor3 = C.Text,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(14, 0),
Size = UDim2.new(1, -26, 0, 16),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = card,
})
local text = opts.text or ""
if text ~= "" then
make("TextLabel", {
Text = text,
Font = Enum.Font.Gotham,
TextSize = 12,
TextColor3 = C.TextDim,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(14, 18),
Size = UDim2.new(1, -26, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
Parent = card,
})
end
tween(card, 0.26, {Position = UDim2.fromOffset(0, 0)}, Enum.EasingStyle.Quint)
task.delay(duration, function()
if not slot.Parent then return end
tween(card, 0.2, {Position = UDim2.fromOffset(340, 0)})
task.delay(0.22, function() pcall(function() slot:Destroy() end) end)
end)
return card
end
-- ============================================================= WATERMARK
local watermark = make("Frame", {
Name = "Watermark",
Position = UDim2.fromOffset(20, 18),
Size = UDim2.fromOffset(340, 28),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = C.Bg,
BackgroundTransparency = 0.12,
BorderSizePixel = 0,
Visible = S.UI.Watermark,
Parent = UI.Overlay,
})
UI.corner(watermark, 8)
UI.stroke(watermark, C.Stroke, 1, 0.3)
local watermarkBar = make("Frame", {
Size = UDim2.new(0, 3, 1, -10),
Position = UDim2.fromOffset(6, 5),
BackgroundColor3 = C.Accent,
BorderSizePixel = 0,
Parent = watermark,
})
UI.corner(watermarkBar, 2)
UI.accented(watermarkBar, "BackgroundColor3")
local watermarkText = make("TextLabel", {
Text = "Kitty Hub",
Font = Enum.Font.GothamMedium,
TextSize = 12,
TextColor3 = C.Text,
BackgroundTransparency = 1,
RichText = true,
Position = UDim2.fromOffset(16, 0),
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
TextXAlignment = Enum.TextXAlignment.Left,
Parent = watermark,
})
UI.Watermark, UI.WatermarkText = watermark, watermarkText
-- ========================================================== KEYBIND LIST
local keybindPanel = make("Frame", {
Name = "Keybinds",
Position = UDim2.fromOffset(20, 58),
Size = UDim2.fromOffset(168, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundColor3 = C.Bg,
BackgroundTransparency = 0.15,
BorderSizePixel = 0,
Visible = S.UI.KeybindList,
Parent = UI.Overlay,
})
UI.corner(keybindPanel, 8)
UI.stroke(keybindPanel, C.Stroke, 1, 0.3)
make("TextLabel", {
Text = "KEYBINDS",
Font = Enum.Font.GothamBold,
TextSize = 10,
TextColor3 = C.TextFaint,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(10, 8),
Size = UDim2.new(1, -20, 0, 12),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = keybindPanel,
})
local keybindList = make("Frame", {
BackgroundTransparency = 1,
Position = UDim2.fromOffset(0, 24),
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Parent = keybindPanel,
})
UI.list(keybindList, 2, Enum.HorizontalAlignment.Left)
UI.pad(keybindList, 0, 0, 9, 10, 10)
UI.KeybindPanel, UI.KeybindList = keybindPanel, keybindList
-- Entries are re-rendered whenever a bind changes, so the panel always
-- shows the current key rather than whatever it was at load.
local keybindEntries = {}
function UI.registerKeybind(label, getKey, isActive)
keybindEntries[#keybindEntries + 1] = {label = label, get = getKey, active = isActive}
end
function UI.refreshKeybinds()
for _, child in ipairs(keybindList:GetChildren()) do
if child:IsA("TextLabel") then child:Destroy() end
end
for _, entry in ipairs(keybindEntries) do
local on = entry.active and entry.active() or false
make("TextLabel", {
Text = string.format('<font color="#%s">[%s]</font> %s',
(on and C.Good or C.TextFaint):ToHex(), entry.get(), entry.label),
Font = Enum.Font.Gotham,
TextSize = 11,
RichText = true,
TextColor3 = C.TextDim,
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 14),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = keybindList,
})
end
end
-- Some controls (keybind pickers, text boxes) need to swallow global
-- hotkeys while they are focused.
UI.Capturing = false
end
-- ─── src/_shared/50_ui_controls.lua ────────────────────────────────
-- ============================================================================
-- UI CONTROLS — toggle, slider, dropdown, keybind, button, input, colour
-- ============================================================================
do
local UI = KH.UI
local C = UI.C
local make = UI.make
local tween = UI.tween
local UserInputService = KH.Services.UserInputService
local Config = KH.Config
local ROW_H, ROW_DESC_H = 36, 50
-- Every control that can redraw itself registers here, so loading a config
-- profile can repaint the entire menu in one pass.
UI.Controls = {}
local function register(control)
if control and control.refresh then UI.Controls[#UI.Controls + 1] = control end
return control
end
function UI.refreshAll()
for _, control in ipairs(UI.Controls) do
KH.safe("refresh", control.refresh)
end
for _, readout in ipairs(UI.Readouts or {}) do
KH.safe("readout", readout.refresh)
end
UI.refreshKeybinds()
end
-- Every control writes through here so autosave and any dependent UI stay
-- in step with the settings table.
local function commit(opts, value)
if opts.set then KH.safe("control:" .. tostring(opts.text), opts.set, value) end
Config.touch()
end
-- ---------------------------------------------------------------- row
local function baseRow(section, opts, height)
local hasDesc = opts.desc ~= nil
local row = make("Frame", {
Size = UDim2.new(1, 0, 0, height or (hasDesc and ROW_DESC_H or ROW_H)),
BackgroundColor3 = C.Row,
BackgroundTransparency = 1,
BorderSizePixel = 0,
LayoutOrder = UI.nextOrder(section),
Parent = section.body,
})
UI.corner(row, 7)
row.MouseEnter:Connect(function() tween(row, 0.1, {BackgroundTransparency = 0.55}) end)
row.MouseLeave:Connect(function() tween(row, 0.1, {BackgroundTransparency = 1}) end)
local label = make("TextLabel", {
Text = opts.text or "",
Font = Enum.Font.GothamMedium,
TextSize = 13,
TextColor3 = C.Text,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(12, hasDesc and 8 or 0),
Size = UDim2.new(1, -110, 0, hasDesc and 15 or height or ROW_H),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = row,
})
if hasDesc then
make("TextLabel", {
Text = opts.desc,
Font = Enum.Font.Gotham,
TextSize = 11,
TextColor3 = C.TextFaint,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(12, 25),
Size = UDim2.new(1, -110, 0, 14),
TextXAlignment = Enum.TextXAlignment.Left,
TextTruncate = Enum.TextTruncate.AtEnd,
Parent = row,
})
end
UI.registerSearch(section, row, (opts.text or "") .. " " .. (opts.desc or ""))
return row, label
end
-- =============================================================== TOGGLE
function UI.toggle(section, opts)
local row = baseRow(section, opts)
local track = make("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -12, 0.5, 0),
Size = UDim2.fromOffset(40, 21),
BackgroundColor3 = C.Off,
BorderSizePixel = 0,
Parent = row,
})
UI.corner(track, 11)
local fill = make("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = C.Accent,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Parent = track,
})
UI.corner(fill, 11)
UI.accented(fill, "BackgroundColor3")
local knob = make("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Size = UDim2.fromOffset(15, 15),
BackgroundColor3 = Color3.fromRGB(235, 235, 242),
BorderSizePixel = 0,
ZIndex = 2,
Parent = track,
})
UI.corner(knob, 8)
local control = {}
function control.refresh(animate)
local on = opts.get and opts.get() or false
local pos = on and UDim2.new(1, -18, 0.5, 0) or UDim2.new(0, 3, 0.5, 0)
local transparency = on and 0 or 1
if animate then
tween(knob, 0.15, {Position = pos}, Enum.EasingStyle.Back)
tween(fill, 0.15, {BackgroundTransparency = transparency})
else
knob.Position = pos
fill.BackgroundTransparency = transparency
end
end
control.refresh(false)
local hit = make("TextButton", {
Text = "", BackgroundTransparency = 1,
Size = UDim2.fromScale(1, 1), Parent = row,
})
hit.MouseButton1Click:Connect(function()
local value = not (opts.get and opts.get())
commit(opts, value)
control.refresh(true)
UI.refreshKeybinds()
end)
control.row = row
if opts.flag then UI[opts.flag] = control end
return register(control)
end
-- =============================================================== SLIDER
function UI.slider(section, opts)
local height = opts.desc and 60 or 48
local row = baseRow(section, opts, height)
local minV, maxV = opts.min or 0, opts.max or 100
local step = opts.step or 1
local decimals = (step < 1) and 2 or 0
local value = make("TextLabel", {
Text = "0",
Font = Enum.Font.GothamBold,
TextSize = 12,
TextColor3 = C.Accent,
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -12, 0, 8),
Size = UDim2.fromOffset(70, 15),
TextXAlignment = Enum.TextXAlignment.Right,
Parent = row,
})
UI.accented(value, "TextColor3", {v = 1.3, s = 0.6})
local track = make("Frame", {
Position = UDim2.new(0, 12, 0, height - 16),
Size = UDim2.new(1, -24, 0, 5),
BackgroundColor3 = C.Off,
BorderSizePixel = 0,
Parent = row,
})
UI.corner(track, 3)
local fill = make("Frame", {
Size = UDim2.fromScale(0, 1),
BackgroundColor3 = C.Accent,
BorderSizePixel = 0,
Parent = track,
})
UI.corner(fill, 3)
UI.accented(UI.gradient(fill, C.Accent, C.Accent, 0), "Gradient")
local knob = make("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(1, 0.5),
Size = UDim2.fromOffset(12, 12),
BackgroundColor3 = Color3.fromRGB(245, 245, 250),
BorderSizePixel = 0,
ZIndex = 2,
Parent = fill,
})
UI.corner(knob, 6)
local function format(n)
local text = (decimals > 0) and string.format("%." .. decimals .. "f", n) or tostring(math.floor(n))
return text .. (opts.suffix or "")
end
local control = {}
local function paint(n)
local scale = (maxV > minV) and ((n - minV) / (maxV - minV)) or 0
fill.Size = UDim2.fromScale(math.clamp(scale, 0, 1), 1)
value.Text = format(n)
end
function control.refresh()
paint(opts.get and opts.get() or minV)
end
control.refresh()
local function setFromX(x)
local scale = math.clamp((x - track.AbsolutePosition.X) / math.max(track.AbsoluteSize.X, 1), 0, 1)
local raw = minV + (maxV - minV) * scale
local snapped = math.clamp(math.floor(raw / step + 0.5) * step, minV, maxV)
paint(snapped)
commit(opts, snapped)
end
-- The hit area is taller than the 5px track so the slider is actually
-- grabbable, and covers the row's lower half.
local hit = make("TextButton", {
Text = "", BackgroundTransparency = 1,
Position = UDim2.new(0, 0, 0, height - 26),
Size = UDim2.new(1, 0, 0, 26),
Parent = row,
})
local dragging = false
hit.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragging = true
tween(knob, 0.1, {Size = UDim2.fromOffset(15, 15)})
setFromX(input.Position.X)
end
end)
KH.track(UserInputService.InputChanged:Connect(function(input)
if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement
or input.UserInputType == Enum.UserInputType.Touch) then
setFromX(input.Position.X)
end
end))
KH.track(UserInputService.InputEnded:Connect(function(input)
if dragging and (input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch) then
dragging = false
tween(knob, 0.1, {Size = UDim2.fromOffset(12, 12)})
end
end))
control.row = row
if opts.flag then UI[opts.flag] = control end
return register(control)
end
-- ============================================================= DROPDOWN
-- Expands inline instead of floating a popup: the page auto-sizes, so
-- pushing rows down avoids every z-order problem a popup would bring.
function UI.dropdown(section, opts)
local row = baseRow(section, opts)
local options = opts.options or {}
local button = make("TextButton", {
Text = "",
AutoButtonColor = false,
BackgroundColor3 = C.Card,
BorderSizePixel = 0,
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -12, 0.5, 0),
Size = UDim2.fromOffset(112, 26),
Parent = row,
})
UI.corner(button, 6)
UI.stroke(button, C.Stroke, 1, 0.3)
local current = make("TextLabel", {
Text = "—",
Font = Enum.Font.GothamMedium,
TextSize = 12,
TextColor3 = C.Text,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(9, 0),
Size = UDim2.new(1, -26, 1, 0),
TextXAlignment = Enum.TextXAlignment.Left,
TextTruncate = Enum.TextTruncate.AtEnd,
Parent = button,
})
local chevron = make("TextLabel", {
Text = "▾",
Font = Enum.Font.GothamBold,
TextSize = 11,
TextColor3 = C.TextDim,
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -8, 0.5, 0),
Size = UDim2.fromOffset(12, 26),
Parent = button,
})
-- The expanded list is a sibling row inside the same section body, so
-- the list layout handles all the spacing for us.
local panel = make("Frame", {
Size = UDim2.new(1, 0, 0, 0),
BackgroundColor3 = C.Bg,
BackgroundTransparency = 0.2,
BorderSizePixel = 0,
ClipsDescendants = true,
Visible = false,
LayoutOrder = UI.nextOrder(section),
Parent = section.body,
})
UI.corner(panel, 7)
UI.stroke(panel, C.Stroke, 1, 0.4)
local panelList = make("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
Parent = panel,
})
UI.list(panelList, 2)
UI.pad(panelList, 5)
local expanded = false
local entryHeight = 26
local control = {}
function control.refresh()
current.Text = tostring(opts.get and opts.get() or "—")
for _, child in ipairs(panelList:GetChildren()) do
if child:IsA("TextButton") then
local selected = child.Name == current.Text
child.BackgroundTransparency = selected and 0.85 or 1
child.TextColor3 = selected and C.Text or C.TextDim
end
end
end
local function setExpanded(open)
expanded = open
local target = open and (#options * (entryHeight + 2) + 10) or 0
panel.Visible = true
tween(chevron, 0.15, {Rotation = open and 180 or 0})
tween(panel, 0.16, {Size = UDim2.new(1, 0, 0, target)})
if not open then
task.delay(0.17, function()
if not expanded then panel.Visible = false end
end)
end
end
local function buildEntry(option)
local entry = make("TextButton", {
Name = tostring(option),
Text = tostring(option),
Font = Enum.Font.Gotham,
TextSize = 12,
TextColor3 = C.TextDim,
BackgroundColor3 = C.Accent,
BackgroundTransparency = 1,
AutoButtonColor = false,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, entryHeight),
Parent = panelList,
})
UI.corner(entry, 5)
UI.accented(entry, "BackgroundColor3")
entry.MouseButton1Click:Connect(function()
commit(opts, option)
control.refresh()
setExpanded(false)
end)
end
for _, option in ipairs(options) do buildEntry(option) end
-- Lists that are discovered at runtime (saved waypoints, config
-- profiles) rebuild themselves through this.
function control.setOptions(newOptions)
options = newOptions or {}
for _, child in ipairs(panelList:GetChildren()) do
if child:IsA("TextButton") then child:Destroy() end
end
for _, option in ipairs(options) do buildEntry(option) end
if expanded then
panel.Size = UDim2.new(1, 0, 0, #options * (entryHeight + 2) + 10)
end
control.refresh()
end
button.MouseButton1Click:Connect(function() setExpanded(not expanded) end)
control.refresh()
UI.registerSearch(section, panel, (opts.text or ""))
control.row = row
if opts.flag then UI[opts.flag] = control end
return register(control)
end
-- ============================================================== KEYBIND
function UI.keybind(section, opts)
local row = baseRow(section, opts)
local button = make("TextButton", {
Text = "—",
Font = Enum.Font.GothamBold,
TextSize = 12,
TextColor3 = C.Text,
BackgroundColor3 = C.Card,
AutoButtonColor = false,
BorderSizePixel = 0,
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -12, 0.5, 0),
Size = UDim2.fromOffset(78, 26),
Parent = row,
})
UI.corner(button, 6)
UI.stroke(button, C.Stroke, 1, 0.3)
local control = {}
function control.refresh()
button.Text = tostring(opts.get and opts.get() or "None")
button.TextColor3 = C.Text
end
control.refresh()
local listening = false
button.MouseButton1Click:Connect(function()
listening = true
UI.Capturing = true
button.Text = "press…"
button.TextColor3 = C.Accent
end)
KH.track(UserInputService.InputBegan:Connect(function(input)
if not listening then return end
if input.UserInputType ~= Enum.UserInputType.Keyboard then return end
listening = false
UI.Capturing = false
-- Escape clears the binding rather than binding Escape itself.
if input.KeyCode == Enum.KeyCode.Escape then
commit(opts, "None")
else
commit(opts, input.KeyCode.Name)
end
control.refresh()
UI.refreshKeybinds()
end))
control.row = row
if opts.flag then UI[opts.flag] = control end
return register(control)
end
-- =============================================================== BUTTON
function UI.button(section, opts)
local row = make("Frame", {
Size = UDim2.new(1, 0, 0, opts.desc and 46 or 32),
BackgroundTransparency = 1,
LayoutOrder = UI.nextOrder(section),
Parent = section.body,
})
local button = make("TextButton", {
Text = opts.text or "Button",
Font = Enum.Font.GothamMedium,
TextSize = 13,
TextColor3 = opts.kind == "danger" and C.Bad or C.Text,
BackgroundColor3 = opts.kind == "primary" and C.Accent or C.Row,
BackgroundTransparency = opts.kind == "primary" and 0 or 0.35,
AutoButtonColor = false,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 30),
Parent = row,
})
UI.corner(button, 7)
if opts.kind == "primary" then UI.accented(button, "BackgroundColor3") end
UI.stroke(button, opts.kind == "danger" and C.Bad or C.Stroke, 1, 0.45)
if opts.desc then
make("TextLabel", {
Text = opts.desc,
Font = Enum.Font.Gotham,
TextSize = 11,
TextColor3 = C.TextFaint,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(2, 31),
Size = UDim2.new(1, -4, 0, 14),
TextXAlignment = Enum.TextXAlignment.Left,
Parent = row,
})
end
local baseTransparency = button.BackgroundTransparency
button.MouseEnter:Connect(function()
tween(button, 0.1, {BackgroundTransparency = math.max(baseTransparency - 0.25, 0)})
end)
button.MouseLeave:Connect(function()
tween(button, 0.1, {BackgroundTransparency = baseTransparency})
end)
button.MouseButton1Click:Connect(function()
-- A click that opens a teleport or a server hop can yield; never
-- let it block the input thread.
KH.detach(function()
if opts.callback then opts.callback() end
end)
end)
UI.registerSearch(section, row, (opts.text or "") .. " " .. (opts.desc or ""))
return {row = row, button = button}
end
-- ================================================================ INPUT
function UI.input(section, opts)
local row = baseRow(section, opts)
local box = make("TextBox", {
Text = tostring(opts.get and opts.get() or ""),
PlaceholderText = opts.placeholder or "",
PlaceholderColor3 = C.TextFaint,
Font = Enum.Font.Gotham,
TextSize = 12,
TextColor3 = C.Text,
BackgroundColor3 = C.Card,
ClearTextOnFocus = false,
BorderSizePixel = 0,
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -12, 0.5, 0),
Size = UDim2.fromOffset(opts.width or 130, 26),
Parent = row,
})
UI.corner(box, 6)
UI.stroke(box, C.Stroke, 1, 0.3)
UI.pad(box, 0, 0, 0, 8, 8)
-- Hotkeys must not fire while the user is typing into the field.
box.Focused:Connect(function() UI.Capturing = true end)
box.FocusLost:Connect(function(enter)
UI.Capturing = false
if enter or opts.commitOnBlur ~= false then
commit(opts, box.Text)
if opts.clearOnSubmit then box.Text = "" end
end
end)
local control = {box = box, row = row}
function control.refresh()
if opts.get then box.Text = tostring(opts.get()) end
end
if opts.flag then UI[opts.flag] = control end
return register(control)
end
-- ========================================================= COLOUR PICKER
-- Three gradient strips rather than a 2D swatch image, so it needs no
-- uploaded assets and renders identically on every executor.
function UI.colorpicker(section, opts)
local row = baseRow(section, opts)
local swatch = make("TextButton", {
Text = "",
AutoButtonColor = false,
BackgroundColor3 = opts.get and opts.get() or C.Accent,
BorderSizePixel = 0,
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -12, 0.5, 0),
Size = UDim2.fromOffset(42, 22),
Parent = row,
})
UI.corner(swatch, 6)
UI.stroke(swatch, C.Stroke, 1, 0.2)
local panel = make("Frame", {
Size = UDim2.new(1, 0, 0, 0),
BackgroundColor3 = C.Bg,
BackgroundTransparency = 0.2,
BorderSizePixel = 0,
ClipsDescendants = true,
Visible = false,
LayoutOrder = UI.nextOrder(section),
Parent = section.body,
})
UI.corner(panel, 7)
UI.stroke(panel, C.Stroke, 1, 0.4)
local h, s, v = (opts.get and opts.get() or C.Accent):ToHSV()
local function currentColor() return Color3.fromHSV(h, s, v) end
local strips = {}
local function strip(name, y, buildGradient)
local holder = make("Frame", {
Position = UDim2.fromOffset(10, y),
Size = UDim2.new(1, -20, 0, 14),
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
Parent = panel,
})
UI.corner(holder, 4)
local gradient = make("UIGradient", {Parent = holder})
buildGradient(gradient)
local marker = make("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0, 0.5),
Size = UDim2.fromOffset(4, 20),
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
ZIndex = 3,
Parent = holder,
})
UI.corner(marker, 2)
UI.stroke(marker, Color3.new(0, 0, 0), 1, 0.5)
strips[name] = {holder = holder, gradient = gradient, marker = marker}
return strips[name]
end
strip("hue", 10, function(gradient)
local keys = {}
for i = 0, 6 do
keys[#keys + 1] = ColorSequenceKeypoint.new(i / 6, Color3.fromHSV(i / 6, 1, 1))
end
gradient.Color = ColorSequence.new(keys)
end)
strip("sat", 32, function(gradient)
gradient.Color = ColorSequence.new(Color3.new(1, 1, 1), Color3.fromHSV(h, 1, 1))
end)
strip("val", 54, function(gradient)
gradient.Color = ColorSequence.new(Color3.new(0, 0, 0), Color3.fromHSV(h, s, 1))
end)
local preview = make("Frame", {
Position = UDim2.fromOffset(10, 76),
Size = UDim2.new(1, -20, 0, 18),
BackgroundColor3 = currentColor(),
BorderSizePixel = 0,
Parent = panel,
})
UI.corner(preview, 4)
local function repaint(pushValue)
local color = currentColor()
swatch.BackgroundColor3 = color
preview.BackgroundColor3 = color
strips.sat.gradient.Color = ColorSequence.new(Color3.new(1, 1, 1), Color3.fromHSV(h, 1, 1))
strips.val.gradient.Color = ColorSequence.new(Color3.new(0, 0, 0), Color3.fromHSV(h, s, 1))
strips.hue.marker.Position = UDim2.fromScale(h, 0.5)
strips.sat.marker.Position = UDim2.fromScale(s, 0.5)
strips.val.marker.Position = UDim2.fromScale(v, 0.5)
if pushValue then commit(opts, color) end
end
repaint(false)
local dragTarget = nil
local function stripScale(entry, x)
return math.clamp((x - entry.holder.AbsolutePosition.X)
/ math.max(entry.holder.AbsoluteSize.X, 1), 0, 1)
end
for name, entry in pairs(strips) do
entry.holder.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragTarget = name
local scale = stripScale(entry, input.Position.X)
if name == "hue" then h = scale elseif name == "sat" then s = scale else v = scale end
repaint(true)
end
end)
end
KH.track(UserInputService.InputChanged:Connect(function(input)
if not dragTarget then return end
if input.UserInputType ~= Enum.UserInputType.MouseMovement
and input.UserInputType ~= Enum.UserInputType.Touch then return end
local entry = strips[dragTarget]
local scale = stripScale(entry, input.Position.X)
if dragTarget == "hue" then h = scale elseif dragTarget == "sat" then s = scale else v = scale end
repaint(true)
end))
KH.track(UserInputService.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragTarget = nil
end
end))
local expanded = false
swatch.MouseButton1Click:Connect(function()
expanded = not expanded
panel.Visible = true
tween(panel, 0.16, {Size = UDim2.new(1, 0, 0, expanded and 104 or 0)})
if not expanded then
task.delay(0.17, function()
if not expanded then panel.Visible = false end
end)
end
end)
UI.registerSearch(section, panel, opts.text or "")
local control = {row = row}
function control.refresh()
local color = opts.get and opts.get() or C.Accent
h, s, v = color:ToHSV()
repaint(false)
end
if opts.flag then UI[opts.flag] = control end
return register(control)
end
-- ================================================================ LABEL
function UI.label(section, text)
local label = make("TextLabel", {
Text = text,
Font = Enum.Font.Gotham,
TextSize = 11,
TextColor3 = C.TextFaint,
BackgroundTransparency = 1,
Size = UDim2.new(1, -8, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
LayoutOrder = UI.nextOrder(section),
Parent = section.body,
})
UI.registerSearch(section, label, text)
return label
end
-- ============================================================ READOUT
-- A live value display: same shape as a row, but the right-hand side is
-- driven by a getter polled from the main loop.
function UI.readout(section, opts)
local row = baseRow(section, opts)
local value = make("TextLabel", {
Text = "—",
Font = Enum.Font.GothamBold,
TextSize = 12,
TextColor3 = C.TextDim,
BackgroundTransparency = 1,
RichText = true,
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -12, 0.5, 0),
Size = UDim2.fromOffset(180, 20),
TextXAlignment = Enum.TextXAlignment.Right,
Parent = row,
})
local control = {row = row, label = value}
function control.refresh()
if opts.get then
local ok, text = pcall(opts.get)
value.Text = ok and tostring(text) or "—"
end
end
control.refresh()
UI.Readouts = UI.Readouts or {}
table.insert(UI.Readouts, control)
return control
end
end
-- ─── src/_shared/55_session.lua ────────────────────────────────────
-- ============================================================================
-- SESSION — the one render loop, rejoin, server hop, and unload
-- ============================================================================
do
local UI = KH.UI
local RunService = KH.Services.RunService
local TeleportService = KH.Services.TeleportService
local HttpService = KH.Services.HttpService
local LocalPlayer = KH.LocalPlayer
-- ============================================================ RENDER LOOP
-- One connection drives every per-frame job. Each is pcall-wrapped, so a
-- feature that breaks cannot take the rest of the menu down with it.
KH.track(RunService.RenderStepped:Connect(function(delta)
if not KH.Alive then return end
KH.camera()
local jobs = KH.Frame
for i = 1, #jobs do
local job = jobs[i]
KH.safe(job.name, job.fn, delta)
end
end))
-- ============================================================== SESSION
-- The places this hub has a module for. Switching between them from inside
-- the menu queues the loader first, so an executor that refuses to attach
-- to the destination still ends up running there — it is already in the
-- process, and the queue survives the teleport.
KH.Games = {
{name = "Murder Mystery 2", place = 142823291},
{name = "Jailbreak", place = 606849621},
}
function KH.goToGame(placeId)
if typeof(placeId) ~= "number" or placeId == game.PlaceId then return end
local env = (type(getgenv) == "function" and getgenv()) or _G
local queued = type(env.KittyHubQueue) == "function" and env.KittyHubQueue() == true
UI.notify({
title = "Switching Game",
text = queued and "Queued — it will load itself when you land."
or "This executor cannot queue; run the loadstring again on arrival.",
kind = queued and "good" or "warn",
duration = 6,
})
task.wait(1)
pcall(function() TeleportService:Teleport(placeId, LocalPlayer) end)
end
function KH.rejoin()
UI.notify({title = "Rejoin", text = "Teleporting…"})
pcall(function()
TeleportService:Teleport(game.PlaceId, LocalPlayer)
end)
end
function KH.serverHop()
KH.detach(function()
UI.notify({title = "Server Hop", text = "Looking for a server…"})
local url = ("https://games.roblox.com/v1/games/%d/servers/Public?sortOrder=Desc&limit=100")
:format(game.PlaceId)
local ok, body = pcall(function() return game:HttpGet(url) end)
if not ok then
UI.notify({title = "Server Hop", text = "Could not reach the server list.", kind = "bad"})
return
end
local decoded, data = pcall(function() return HttpService:JSONDecode(body) end)
if not decoded or typeof(data) ~= "table" or typeof(data.data) ~= "table" then
UI.notify({title = "Server Hop", text = "Server list was unreadable.", kind = "bad"})
return
end
local candidates = {}
for _, server in ipairs(data.data) do
if typeof(server) == "table"
and server.id ~= game.JobId
and typeof(server.playing) == "number"
and typeof(server.maxPlayers) == "number"
and server.playing < server.maxPlayers then
candidates[#candidates + 1] = server.id
end
end
if #candidates == 0 then
UI.notify({title = "Server Hop", text = "No other servers with room.", kind = "warn"})
return
end
local pick = candidates[math.random(1, #candidates)]
pcall(function()
TeleportService:TeleportToPlaceInstance(game.PlaceId, pick, LocalPlayer)
end)
end)
end
-- =============================================================== UNLOAD
function KH.unload()
if not KH.Alive then return end
KH.Alive = false
-- Undo world changes first, while our instances still exist.
for _, restore in ipairs(KH.Undo) do pcall(restore) end
for _, conn in ipairs(KH.Conn) do pcall(function() conn:Disconnect() end) end
local current = coroutine.running()
for _, thread in ipairs(KH.Thread) do
if thread ~= current then pcall(task.cancel, thread) end
end
for _, inst in ipairs(KH.Inst) do pcall(function() inst:Destroy() end) end
local env = (type(getgenv) == "function" and getgenv()) or _G
env.KittyHubCleanup = nil
env.KittyHub = nil
print("[Kitty Hub] Unloaded.")
end
do
local env = (type(getgenv) == "function" and getgenv()) or _G
env.KittyHubCleanup = KH.unload
end
end
-- ─── src/_shared/60_movement.lua ───────────────────────────────────
-- ============================================================================
-- MOVEMENT — speed, jump, noclip, fly, spinbot, teleports, waypoints
--
-- Game-agnostic: anything that needs to know what it is teleporting to
-- lives in the game module and builds on Move.tpTo / Move.tpToPlayer.
-- ============================================================================
do
local UI = KH.UI
local U = KH.U
local S = KH.S
local X = KH.X
local UserInputService = KH.Services.UserInputService
local RunService = KH.Services.RunService
local HttpService = KH.Services.HttpService
local LocalPlayer = KH.LocalPlayer
local Move = {}
KH.Move = Move
-- ======================================================== SPEED / JUMP
-- Re-applied continuously because MM2 resets WalkSpeed on respawn and when
-- rounds change; a one-shot assignment silently stops working.
function Move.applyHumanoid()
local hum = U.myHum()
if not hum then return end
if S.Move.SpeedEnabled and S.Move.SpeedMode == "Humanoid" then
if hum.WalkSpeed ~= S.Move.Speed then hum.WalkSpeed = S.Move.Speed end
elseif hum.WalkSpeed ~= 16 and not S.Move.SpeedEnabled then
hum.WalkSpeed = 16
end
-- R15 humanoids may be driven by JumpHeight rather than JumpPower;
-- writing only JumpPower would silently do nothing on those.
if S.Move.JumpEnabled then
if hum.UseJumpPower then
if hum.JumpPower ~= S.Move.Jump then hum.JumpPower = S.Move.Jump end
else
local height = S.Move.Jump / 7.5
if math.abs(hum.JumpHeight - height) > 0.01 then hum.JumpHeight = height end
end
else
if hum.UseJumpPower then
if hum.JumpPower ~= 50 then hum.JumpPower = 50 end
elseif math.abs(hum.JumpHeight - 7.2) > 0.01 then
hum.JumpHeight = 7.2
end
end
end
-- CFrame mode drives the character directly, which ignores any server-side
-- WalkSpeed clamp — at the cost of looking less natural.
KH.onFrame("speed-cframe", function(delta)
if not (S.Move.SpeedEnabled and S.Move.SpeedMode == "CFrame") then return end
if S.Move.Fly then return end
local hum, root = U.myHum(), U.myRoot()
if not hum or not root then return end
local direction = hum.MoveDirection
if direction.Magnitude < 0.05 then return end
-- delta comes from the shared render loop; never yield in here.
root.CFrame = root.CFrame + direction * (S.Move.Speed - 16) * (delta or 0)
end, 60)
KH.loop(0.4, function() Move.applyHumanoid() end)
-- ============================================================== NOCLIP
-- One owner for the character's collisions, refcounted. The toggle is one
-- holder and a rob routine is another: without that, a job finishing put
-- the collisions back underneath a noclip the player had switched on by
-- hand, and the toggle looked broken for the rest of the session.
--
-- Weak keys: a respawn leaves the old limbs in here, and nothing should be
-- keeping a destroyed character alive just to remember it was solid.
local clipOriginal = setmetatable({}, {__mode = "k"})
local clipHolders = 0
local clipManual = false
local clipConn
-- Descendants every frame, not once: a respawn brings a whole new set of
-- solid limbs that a one-off pass would never see.
local function clipStep()
local char = U.charOf(LocalPlayer)
if not char then return end
for _, part in ipairs(char:GetDescendants()) do
if part:IsA("BasePart") and part.CanCollide then
if clipOriginal[part] == nil then clipOriginal[part] = true end
part.CanCollide = false
end
end
end
local function clipRestore()
for part, wasCollidable in pairs(clipOriginal) do
if part.Parent and wasCollidable then
pcall(function() part.CanCollide = true end)
end
end
table.clear(clipOriginal)
end
-- Stepped, not the render loop: this has to be the last write before the
-- physics step. On the render loop the humanoid controller puts the
-- collisions back first, and the character bounces off the wall it should
-- be passing through.
function Move.pushNoclip()
clipHolders = clipHolders + 1
if clipHolders == 1 then
if not clipConn then
-- Not KH.track: this is disconnected below and on unload, and
-- tracking it would add a dead entry on every single toggle.
clipConn = RunService.Stepped:Connect(clipStep)
end
clipStep() -- this frame, rather than the next one
end
end
function Move.popNoclip()
clipHolders = math.max(clipHolders - 1, 0)
if clipHolders == 0 then
if clipConn then
clipConn:Disconnect()
clipConn = nil
end
clipRestore()
end
end
function Move.noclipHeld() return clipHolders > 0 end
-- Seated, it is the vehicle that collides with the world and not the
-- character, so switching the character's collisions off does nothing at
-- all. Say that outright instead of leaving a toggle that looks on and
-- changes nothing.
function Move.noclipBlocked()
local hum = U.myHum()
if hum and hum.SeatPart then
return "You are in a vehicle — noclip only moves your character."
end
return nil
end
function Move.setNoclip(on)
on = on and true or false
if on ~= clipManual then
clipManual = on
if on then Move.pushNoclip() else Move.popNoclip() end
end
S.Move.Noclip = on
if on then
local why = Move.noclipBlocked()
if why then UI.notify({title = "Noclip", text = why, kind = "warn"}) end
end
UI.refreshKeybinds()
end
-- Unload drops every holder, not just the manual one: a job cut short by
-- an unload would otherwise leave the character permanently intangible.
KH.undo(function()
clipManual, clipHolders = false, 0
S.Move.Noclip = false
if clipConn then
clipConn:Disconnect()
clipConn = nil
end
clipRestore()
end)
-- ================================================== INFINITE JUMP / BHOP
KH.track(UserInputService.JumpRequest:Connect(function()
if not S.Move.InfJump then return end
local hum = U.myHum()
if hum then pcall(function() hum:ChangeState(Enum.HumanoidStateType.Jumping) end) end
end))
KH.onFrame("bhop", function()
if not S.Move.Bhop then return end
if U.typing() then return end
if not UserInputService:IsKeyDown(Enum.KeyCode.Space) then return end
local hum = U.myHum()
if not hum then return end
local state = hum:GetState()
if state == Enum.HumanoidStateType.Running
or state == Enum.HumanoidStateType.Landed
or state == Enum.HumanoidStateType.RunningNoPhysics then
pcall(function() hum:ChangeState(Enum.HumanoidStateType.Jumping) end)
end
end, 62)
-- ======================================================== SAFE LANDING
-- Roblox itself has no fall damage; Jailbreak adds its own and reads it
-- off how fast you arrive. Flying is therefore safe right up until the
-- moment it stops, which is why "fly works but going down kills you".
-- Everything below exists to make sure the character never reaches the
-- floor faster than a walk off a kerb.
local groundParams = RaycastParams.new()
groundParams.FilterType = Enum.RaycastFilterType.Exclude
-- Only real floors count. Without this the probe stops on glass canopies
-- and decorative volumes, and the descent brakes for scenery it would have
-- passed straight through.
pcall(function() groundParams.RespectCanCollide = true end)
pcall(function() groundParams.IgnoreWater = true end)
-- Studs to whatever is under the character, or nil for nothing in reach.
-- The character is excluded so noclip does not read its own legs as floor.
local function groundDrop(root, reach)
groundParams.FilterDescendantsInstances = {LocalPlayer.Character}
local hit = workspace:Raycast(root.Position, Vector3.new(0, -(reach or 600), 0), groundParams)
return hit and (root.Position.Y - hit.Position.Y) or nil
end
-- The fastest descent that still lands softly from here: a ramp, so a long
-- drop stays quick at the top and only bleeds off near the ground. Capped,
-- because the ramp alone asks for two thousand studs a second from the top
-- of a skyscraper, and that is a speed check of its own.
local function descentSpeed(drop)
return math.min(math.max(drop - 4, 0) * 2.2 + 8, 200)
end
-- ================================================================= FLY
local flyVelocity, flyGyro
local flying = false
local landing = false -- flying the character down after fly was cut
local landingUntil = 0
local function stopFly()
flying, landing = false, false
if flyVelocity then flyVelocity:Destroy(); flyVelocity = nil end
if flyGyro then flyGyro:Destroy(); flyGyro = nil end
local hum = U.myHum()
if hum then hum.PlatformStand = false end
end
local function startFly()
local root, hum = U.myRoot(), U.myHum()
if not root or not hum then return false end
stopFly()
flyVelocity = Instance.new("BodyVelocity")
flyVelocity.MaxForce = Vector3.new(9e9, 9e9, 9e9)
flyVelocity.Velocity = Vector3.zero
flyVelocity.Parent = root
flyGyro = Instance.new("BodyGyro")
flyGyro.MaxTorque = Vector3.new(9e9, 9e9, 9e9)
flyGyro.P = 9e4
flyGyro.CFrame = root.CFrame
flyGyro.Parent = root
hum.PlatformStand = true
flying = true
return true
end
-- Letting go three hundred studs up is not a landing, it is a death, so
-- switching fly off in mid-air keeps the flight parts and flies the
-- character down instead. PlatformStand stays on the whole way, so a game
-- reading fall damage off the humanoid freefall state never sees one.
local function beginLanding()
local root = U.myRoot()
if not root or not flyVelocity or not flyVelocity.Parent then return false end
local drop = groundDrop(root, 900)
if not drop or drop <= 6 then return false end
landing = true
landingUntil = os.clock() + 15
return true
end
local function stepLanding()
local root = U.myRoot()
if not root or not flyVelocity or not flyVelocity.Parent then
stopFly()
return
end
local drop = groundDrop(root, 900)
-- Nothing underneath at all means the void, and hanging there forever
-- helps nobody; the timeout hands the character back either way.
if not drop or drop <= 4 or os.clock() > landingUntil then
stopFly()
root.AssemblyLinearVelocity = Vector3.zero
return
end
flyVelocity.Velocity = Vector3.new(0, -descentSpeed(drop), 0)
end
function Move.landing() return landing end
function Move.setFly(on)
S.Move.Fly = on
if on then
landing = false
-- Already holding the flight parts? Resume rather than rebuild:
-- this is the path taken when fly is switched back on mid-landing.
if not (flying and flyVelocity and flyVelocity.Parent) then
if not startFly() then
S.Move.Fly = false
UI.notify({title = "Fly", text = "No character to attach to.", kind = "bad"})
end
end
elseif not (S.Move.SafeLand and beginLanding()) then
stopFly()
end
UI.refreshKeybinds()
end
KH.undo(function() stopFly() end)
KH.onFrame("fly", function()
if landing then
stepLanding()
return
end
if not S.Move.Fly then
if flying then stopFly() end
return
end
if not flying or not flyVelocity or not flyVelocity.Parent then
if not startFly() then return end
end
local cam = KH.camera()
local direction = Vector3.zero
-- Hold still while typing rather than reading the chat as flight input.
if U.typing() then
flyVelocity.Velocity = Vector3.zero
flyGyro.CFrame = cam.CFrame
return
end
if UserInputService:IsKeyDown(Enum.KeyCode.W) then direction = direction + cam.CFrame.LookVector end
if UserInputService:IsKeyDown(Enum.KeyCode.S) then direction = direction - cam.CFrame.LookVector end
if UserInputService:IsKeyDown(Enum.KeyCode.A) then direction = direction - cam.CFrame.RightVector end
if UserInputService:IsKeyDown(Enum.KeyCode.D) then direction = direction + cam.CFrame.RightVector end
if UserInputService:IsKeyDown(Enum.KeyCode.Space) then direction = direction + Vector3.new(0, 1, 0) end
if UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) then direction = direction - Vector3.new(0, 1, 0) end
if direction.Magnitude > 0 then direction = direction.Unit end
local velocity = direction * S.Move.FlySpeed
-- Diving into the floor at flight speed is the other way flight kills
-- you: the fall damage does not care that a BodyVelocity put you there.
if S.Move.SafeLand and velocity.Y < 0 then
local root = U.myRoot()
local drop = root and groundDrop(root, 600)
if drop then
local allowed = descentSpeed(drop)
if -velocity.Y > allowed then
velocity = Vector3.new(velocity.X, -allowed, velocity.Z)
end
end
end
flyVelocity.Velocity = velocity
flyGyro.CFrame = cam.CFrame
end, 61)
-- Falls that did not start as flight: walking off a roof, a jump gone
-- wrong, a teleport that dropped short. Once this latches on it stays on
-- until the character is back on the ground, because releasing at a speed
-- threshold only lets gravity build the speed straight back up.
local braking = false
KH.onFrame("safe-land", function()
if not S.Move.SafeLand or flying or landing or Move.noclipHeld() then
braking = false
return
end
local root, hum = U.myRoot(), U.myHum()
if not root or not hum then
braking = false
return
end
local velocity = root.AssemblyLinearVelocity
if not braking then
-- About twelve studs of fall. Below that nothing hurts, and
-- clamping it would make every ordinary jump feel like a landing.
if velocity.Y > -70 then return end
braking = true
end
if velocity.Y >= 0 or hum.FloorMaterial ~= Enum.Material.Air then
braking = false
return
end
local drop = groundDrop(root, 700)
if not drop then return end
local allowed = descentSpeed(drop)
if -velocity.Y > allowed then
root.AssemblyLinearVelocity = Vector3.new(velocity.X, -allowed, velocity.Z)
end
end, 59)
-- ============================================================= SPINBOT
KH.onFrame("spinbot", function()
if not S.Move.Spinbot then return end
local root = U.myRoot()
if not root then return end
root.CFrame = root.CFrame * CFrame.Angles(0, math.rad(28), 0)
end, 63)
-- Respawns wipe all of this, so reapply once the new character settles.
KH.track(LocalPlayer.CharacterAdded:Connect(function()
task.wait(0.6)
Move.applyHumanoid()
if S.Move.Noclip then Move.setNoclip(true) end
if S.Move.Fly then Move.setFly(true) end
end))
-- =========================================================== TELEPORTS
local function tpTo(position, label)
if not U.myRoot() then
UI.notify({title = "Teleport", text = "No character.", kind = "bad"})
return false
end
U.moveTo(position + Vector3.new(0, 3, 0), true)
if label then UI.notify({title = "Teleport", text = label, duration = 2}) end
return true
end
Move.tpTo = tpTo
local function tpToPlayer(player, label)
if not player then
UI.notify({title = "Teleport", text = "Nobody to teleport to.", kind = "warn"})
return false
end
local part = U.rootOf(player)
if not part then
UI.notify({title = "Teleport", text = player.DisplayName .. " has no character.", kind = "warn"})
return false
end
return tpTo(part.Position, label or ("Moved to " .. player.DisplayName))
end
Move.tpToPlayer = tpToPlayer
-- ============================================================ WAYPOINTS
-- Their own file, not the settings profile: the reconciler drops keys it
-- does not recognise, and waypoint names are never in the schema.
local WAYPOINT_FILE = "KittyHub/waypoints.json"
Move.Waypoints = {}
local function loadWaypoints()
if not (X.writefile and KH.Config.available) then return end
pcall(function()
if not isfile(WAYPOINT_FILE) then return end
local data = HttpService:JSONDecode(readfile(WAYPOINT_FILE))
if typeof(data) == "table" then Move.Waypoints = data end
end)
end
local function saveWaypoints()
if not (X.writefile and KH.Config.available) then return end
pcall(function()
writefile(WAYPOINT_FILE, HttpService:JSONEncode(Move.Waypoints))
end)
end
loadWaypoints()
function Move.saveWaypoint(name)
name = tostring(name or ""):gsub("^%s+", ""):gsub("%s+$", "")
if name == "" then
UI.notify({title = "Waypoint", text = "Give it a name first.", kind = "warn"})
return false
end
local root = U.myRoot()
if not root then return false end
local p = root.Position
Move.Waypoints[name] = {p.X, p.Y, p.Z}
saveWaypoints()
UI.notify({title = "Waypoint", text = 'Saved "' .. name .. '".', kind = "good"})
return true
end
function Move.gotoWaypoint(name)
local entry = Move.Waypoints[name]
if not entry then
UI.notify({title = "Waypoint", text = "No waypoint called " .. tostring(name) .. ".", kind = "warn"})
return false
end
return tpTo(Vector3.new(entry[1], entry[2], entry[3]), 'Moved to "' .. name .. '".')
end
function Move.deleteWaypoint(name)
if Move.Waypoints[name] == nil then return false end
Move.Waypoints[name] = nil
saveWaypoints()
UI.notify({title = "Waypoint", text = 'Deleted "' .. name .. '".'})
return true
end
function Move.waypointNames()
local names = {}
for name in pairs(Move.Waypoints) do names[#names + 1] = name end
table.sort(names)
return names
end
end
-- ─── src/_shared/62_visuals.lua ────────────────────────────────────
-- ============================================================================
-- VISUALS — fullbright, fog, field of view, x-ray walls, detail stripping
--
-- Every effect here records what it changed and restores it on unload, so
-- unloading the script leaves the game looking exactly as it did before.
-- ============================================================================
do
local S = KH.S
local Lighting = KH.Services.Lighting
local Game = KH.Game or {}
local Players = KH.Services.Players
local Visual = {}
KH.Visual = Visual
-- ========================================================== FULLBRIGHT
local lightingSaved = nil
local function saveLighting()
if lightingSaved then return end
lightingSaved = {
Brightness = Lighting.Brightness,
ClockTime = Lighting.ClockTime,
Ambient = Lighting.Ambient,
OutdoorAmbient = Lighting.OutdoorAmbient,
GlobalShadows = Lighting.GlobalShadows,
FogEnd = Lighting.FogEnd,
FogStart = Lighting.FogStart,
ExposureCompensation = Lighting.ExposureCompensation,
}
end
local function restoreLighting()
if not lightingSaved then return end
for property, value in pairs(lightingSaved) do
pcall(function() Lighting[property] = value end)
end
lightingSaved = nil
end
KH.undo(restoreLighting)
-- Atmosphere density is not a Lighting property, so it needs recording
-- separately or turning fog off again leaves the map permanently clear.
local atmosphereSaved = {}
local function restoreAtmosphere()
for effect, density in pairs(atmosphereSaved) do
if effect.Parent then pcall(function() effect.Density = density end) end
end
atmosphereSaved = {}
end
KH.undo(restoreAtmosphere)
-- Reapplied on a timer: MM2 drives its own day/night cycle per round and
-- will happily reset Brightness out from under us.
KH.loop(0.5, function()
if S.Visual.Fullbright then
saveLighting()
local shade = 128 * math.clamp(S.Visual.Brightness / 2, 0.25, 1.5)
Lighting.Brightness = S.Visual.Brightness
Lighting.ClockTime = 14
Lighting.GlobalShadows = false
Lighting.Ambient = Color3.fromRGB(shade, shade, shade)
Lighting.OutdoorAmbient = Color3.fromRGB(shade, shade, shade)
elseif lightingSaved and not S.Visual.NoFog and not S.Visual.LowDetail then
restoreLighting()
end
if S.Visual.NoFog then
saveLighting()
Lighting.FogEnd = 1e6
Lighting.FogStart = 1e6
for _, effect in ipairs(Lighting:GetChildren()) do
if effect:IsA("Atmosphere") then
if atmosphereSaved[effect] == nil then
atmosphereSaved[effect] = effect.Density
end
pcall(function() effect.Density = 0 end)
end
end
elseif next(atmosphereSaved) then
restoreAtmosphere()
end
end)
-- ================================================================= FOV
KH.onFrame("fov", function()
if not S.Visual.FovEnabled then return end
local cam = KH.camera()
if math.abs(cam.FieldOfView - S.Visual.Fov) > 0.1 then
cam.FieldOfView = S.Visual.Fov
end
end, 70)
KH.undo(function()
pcall(function() KH.camera().FieldOfView = 70 end)
end)
-- =============================================================== X-RAY
-- Only the map. Characters, coins and the dropped gun keep their real
-- transparency, or x-ray would hide what you turned it on to see.
local xraySaved = {}
local xrayOn = false
local xrayValue = nil -- the transparency actually painted on
local function isProtected(part)
-- Anything belonging to a character.
local model = part:FindFirstAncestorOfClass("Model")
while model do
if Players:GetPlayerFromCharacter(model) then return true end
model = model:FindFirstAncestorOfClass("Model")
end
local name = part.Name
return name == "GunDrop" or name == "MainCoin" or name == "Trap"
end
local function applyXray()
local map = Game.map and Game.map() or workspace
if not map then return end
for _, part in ipairs(map:GetDescendants()) do
if part:IsA("BasePart") and part.Transparency < 0.9 and not isProtected(part) then
if xraySaved[part] == nil then xraySaved[part] = part.Transparency end
part.Transparency = S.Visual.XrayTransp
end
end
xrayOn = true
xrayValue = S.Visual.XrayTransp
end
local function clearXray()
for part, transparency in pairs(xraySaved) do
if part.Parent then
pcall(function() part.Transparency = transparency end)
end
end
xraySaved = {}
xrayOn = false
end
KH.undo(clearXray)
-- A new round means a new map model, so re-run rather than tracking parts.
if Game.on then
Game.on("RoundStart", function()
if S.Visual.Xray then
task.wait(1)
xraySaved = {}
applyXray()
end
end)
end
KH.loop(1, function()
-- Also when the slider has moved: the walls are already painted, so
-- nothing else would ever notice a new value.
if S.Visual.Xray and (not xrayOn or xrayValue ~= S.Visual.XrayTransp) then
applyXray()
elseif not S.Visual.Xray and xrayOn then
clearXray()
end
end)
-- ============================================================ LOW DETAIL
-- Turns effects off rather than destroying them, so it is fully reversible.
local detailSaved = {}
local lowDetailOn = false
local waterSaved = nil
local EFFECT_CLASSES = {
ParticleEmitter = true, Trail = true, Smoke = true,
Fire = true, Sparkles = true, Beam = true,
}
local function applyLowDetail()
for _, obj in ipairs(workspace:GetDescendants()) do
if EFFECT_CLASSES[obj.ClassName] and obj.Enabled then
detailSaved[obj] = true
obj.Enabled = false
end
end
saveLighting()
Lighting.GlobalShadows = false
pcall(function()
local terrain = workspace.Terrain
if not waterSaved then
waterSaved = {
size = terrain.WaterWaveSize,
reflectance = terrain.WaterReflectance,
}
end
terrain.WaterWaveSize = 0
terrain.WaterReflectance = 0
end)
lowDetailOn = true
end
local function clearLowDetail()
for obj in pairs(detailSaved) do
if obj.Parent then pcall(function() obj.Enabled = true end) end
end
detailSaved = {}
-- The water was flattened by hand, so it has to be put back by hand.
if waterSaved then
pcall(function()
workspace.Terrain.WaterWaveSize = waterSaved.size
workspace.Terrain.WaterReflectance = waterSaved.reflectance
end)
waterSaved = nil
end
lowDetailOn = false
end
KH.undo(clearLowDetail)
KH.loop(2, function()
if S.Visual.LowDetail and not lowDetailOn then
applyLowDetail()
elseif S.Visual.LowDetail and lowDetailOn then
applyLowDetail() -- catch effects added by the new round
elseif not S.Visual.LowDetail and lowDetailOn then
clearLowDetail()
end
end)
end
-- ─── src/mm2/70_esp.lua ────────────────────────────────────────────
-- ============================================================================
-- ESP — player boxes/names/tracers/chams plus coin, gun-drop and trap markers
-- ============================================================================
do
local UI = KH.UI
local C = UI.C
local make = UI.make
local U = KH.U
local Game = KH.Game
local S = KH.S
local Players = KH.Services.Players
local LocalPlayer = KH.LocalPlayer
local Layer = UI.World
-- ------------------------------------------------------ player objects
local objects = {} -- player -> drawing set
local chams = {} -- player -> Highlight
local function corner(parent)
return make("Frame", {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
Parent = parent,
})
end
local function build(player)
if player == LocalPlayer or objects[player] then return end
local box = make("Frame", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Visible = false,
Parent = Layer,
})
local boxStroke = make("UIStroke", {
Thickness = 1.4,
Color = Color3.new(1, 1, 1),
ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
Enabled = false,
Parent = box,
})
-- Eight segments make the four L-shaped corners of a bracket box.
local corners = {}
for i = 1, 8 do corners[i] = corner(box) end
local name = make("TextLabel", {
Font = Enum.Font.GothamBold,
TextSize = 13,
BackgroundTransparency = 1,
TextStrokeTransparency = 0.35,
RichText = true,
AnchorPoint = Vector2.new(0.5, 1),
Size = UDim2.fromOffset(260, 16),
Visible = false,
Parent = Layer,
})
local role = make("TextLabel", {
Font = Enum.Font.GothamMedium,
TextSize = 12,
BackgroundTransparency = 1,
TextStrokeTransparency = 0.35,
RichText = true,
AnchorPoint = Vector2.new(0.5, 0),
Size = UDim2.fromOffset(260, 15),
Visible = false,
Parent = Layer,
})
local tracer = make("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
BorderSizePixel = 0,
Size = UDim2.fromOffset(0, 1),
Visible = false,
Parent = Layer,
})
local healthBg = make("Frame", {
BackgroundColor3 = Color3.fromRGB(15, 15, 20),
BorderSizePixel = 0,
Visible = false,
Parent = Layer,
})
local healthFill = make("Frame", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.fromScale(0, 1),
BackgroundColor3 = C.Good,
BorderSizePixel = 0,
Parent = healthBg,
})
local arrow = make("TextLabel", {
Text = "▲",
Font = Enum.Font.GothamBold,
TextSize = 20,
BackgroundTransparency = 1,
TextStrokeTransparency = 0.4,
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.fromOffset(28, 28),
Visible = false,
Parent = Layer,
})
objects[player] = {
box = box, boxStroke = boxStroke, corners = corners,
name = name, role = role, tracer = tracer,
healthBg = healthBg, healthFill = healthFill, arrow = arrow,
}
end
local function destroy(player)
local set = objects[player]
if set then
for _, key in ipairs({"box", "name", "role", "tracer", "healthBg", "arrow"}) do
pcall(function() set[key]:Destroy() end)
end
objects[player] = nil
end
if chams[player] then
pcall(function() chams[player]:Destroy() end)
chams[player] = nil
end
end
local function hide(set)
set.box.Visible = false
set.name.Visible = false
set.role.Visible = false
set.tracer.Visible = false
set.healthBg.Visible = false
set.arrow.Visible = false
end
-- ------------------------------------------------------------ box paint
local function paintCorners(set, width, height, color, show)
local segs = set.corners
if not show then
for i = 1, 8 do segs[i].Visible = false end
return
end
-- Arm length scales with the box but stays sane at extreme distances.
local armX = math.clamp(width * 0.28, 3, 14)
local armY = math.clamp(height * 0.22, 3, 18)
local t = 1.6
local layout = {
{UDim2.fromOffset(0, 0), UDim2.fromOffset(armX, t)},
{UDim2.fromOffset(0, 0), UDim2.fromOffset(t, armY)},
{UDim2.fromOffset(width - armX, 0), UDim2.fromOffset(armX, t)},
{UDim2.fromOffset(width - t, 0), UDim2.fromOffset(t, armY)},
{UDim2.fromOffset(0, height - t), UDim2.fromOffset(armX, t)},
{UDim2.fromOffset(0, height - armY), UDim2.fromOffset(t, armY)},
{UDim2.fromOffset(width - armX, height - t), UDim2.fromOffset(armX, t)},
{UDim2.fromOffset(width - t, height - armY), UDim2.fromOffset(t, armY)},
}
for i = 1, 8 do
local seg = segs[i]
seg.Visible = true
seg.Position = layout[i][1]
seg.Size = layout[i][2]
seg.BackgroundColor3 = color
end
end
-- ----------------------------------------------------------- chams pool
local function ensureCham(player)
if chams[player] then return chams[player] end
local highlight = make("Highlight", {
FillTransparency = S.ESP.ChamsFill,
OutlineTransparency = 0,
DepthMode = Enum.HighlightDepthMode.AlwaysOnTop,
Enabled = false,
Parent = Layer,
})
chams[player] = highlight
return highlight
end
-- =============================================================== UPDATE
local function updatePlayers()
local espOn = S.ESP.Enabled
if not espOn then
for _, set in pairs(objects) do hide(set) end
for _, highlight in pairs(chams) do highlight.Enabled = false end
return
end
local cam = KH.camera()
local viewport = cam.ViewportSize
local centre = Vector2.new(viewport.X / 2, viewport.Y / 2)
local myRoot = U.myRoot()
for player, set in pairs(objects) do
local char = U.charOf(player)
local root = char and (char:FindFirstChild("HumanoidRootPart") or char.PrimaryPart)
local alive = root ~= nil and Game.isAlive(player)
if not alive then
hide(set)
if chams[player] then chams[player].Enabled = false end
else
local color, role = Game.colorOf(player)
local hideInnocent = S.ESP.OnlyRoles and (role == "Innocent")
local distance = myRoot and (myRoot.Position - root.Position).Magnitude
or (cam.CFrame.Position - root.Position).Magnitude
local inRange = distance <= S.ESP.MaxDistance
if hideInnocent or not inRange then
hide(set)
if chams[player] then chams[player].Enabled = false end
else
local head = char:FindFirstChild("Head")
local topWorld = (head and head.Position or root.Position) + Vector3.new(0, 0.75, 0)
local botWorld = root.Position - Vector3.new(0, 3.2, 0)
local topPos, onScreen, depth = U.toScreen(topWorld)
local botPos = U.toScreen(botWorld)
if onScreen then
set.arrow.Visible = false
local height = math.abs(topPos.Y - botPos.Y)
local width = height * 0.52
local cx = (topPos.X + botPos.X) / 2
local left = cx - width / 2
-- Box
if S.ESP.Boxes then
set.box.Visible = true
set.box.Position = UDim2.fromOffset(left, topPos.Y)
set.box.Size = UDim2.fromOffset(width, height)
local isCorner = S.ESP.BoxStyle == "Corner"
set.boxStroke.Enabled = not isCorner
set.boxStroke.Color = color
paintCorners(set, width, height, color, isCorner)
else
set.box.Visible = false
end
-- Name (+ optional distance)
if S.ESP.Names then
set.name.Visible = true
set.name.TextSize = S.ESP.TextSize
set.name.TextColor3 = color
set.name.Text = S.ESP.Distance
and string.format("%s <font color=\"#%s\">%dm</font>",
player.DisplayName, C.TextDim:ToHex(), math.floor(distance))
or player.DisplayName
set.name.Position = UDim2.fromOffset(cx, topPos.Y - 3)
else
set.name.Visible = false
end
-- Role
if S.ESP.Roles then
set.role.Visible = true
set.role.TextSize = math.max(S.ESP.TextSize - 1, 8)
set.role.TextColor3 = color
set.role.Text = role:upper()
set.role.Position = UDim2.fromOffset(cx, botPos.Y + 3)
else
set.role.Visible = false
end
-- Health bar, pinned to the left edge of the box
local hum = char:FindFirstChildOfClass("Humanoid")
if S.ESP.Health and hum and hum.MaxHealth > 0 then
local ratio = math.clamp(hum.Health / hum.MaxHealth, 0, 1)
set.healthBg.Visible = true
set.healthBg.Position = UDim2.fromOffset(left - 6, topPos.Y)
set.healthBg.Size = UDim2.fromOffset(3, height)
set.healthFill.Size = UDim2.fromScale(1, ratio)
set.healthFill.BackgroundColor3 =
Color3.fromRGB(255, 80, 80):Lerp(C.Good, ratio)
else
set.healthBg.Visible = false
end
-- Tracer
if S.ESP.Tracers then
local origin = (S.ESP.TracerOrigin == "Center") and centre
or Vector2.new(viewport.X / 2, viewport.Y)
local target = Vector2.new(cx, botPos.Y)
local delta = target - origin
set.tracer.Visible = true
set.tracer.BackgroundColor3 = color
set.tracer.Size = UDim2.fromOffset(delta.Magnitude, 1)
set.tracer.Position = UDim2.fromOffset(
(origin.X + target.X) / 2, (origin.Y + target.Y) / 2)
set.tracer.Rotation = math.deg(math.atan2(delta.Y, delta.X))
else
set.tracer.Visible = false
end
else
-- Off-screen: everything hides except the edge arrow.
set.box.Visible = false
set.name.Visible = false
set.role.Visible = false
set.tracer.Visible = false
set.healthBg.Visible = false
if S.ESP.OffScreen then
-- Behind the camera the projection mirrors, so flip
-- it back before working out a bearing.
local point = topPos
if depth < 0 then
point = Vector2.new(viewport.X - point.X, viewport.Y - point.Y)
end
local direction = (point - centre)
if direction.Magnitude < 1 then direction = Vector2.new(0, -1) end
direction = direction.Unit
local radius = math.min(viewport.X, viewport.Y) * 0.34
local at = centre + direction * radius
set.arrow.Visible = true
set.arrow.TextColor3 = color
set.arrow.Position = UDim2.fromOffset(at.X, at.Y)
set.arrow.Rotation = math.deg(math.atan2(direction.Y, direction.X)) + 90
else
set.arrow.Visible = false
end
end
-- Chams
if S.ESP.Chams then
local highlight = ensureCham(player)
highlight.Enabled = true
highlight.FillTransparency = S.ESP.ChamsFill
if highlight.Adornee ~= char then highlight.Adornee = char end
highlight.FillColor = color
highlight.OutlineColor = color
elseif chams[player] then
chams[player].Enabled = false
end
end
end
end
end
-- ====================================================== WORLD MARKERS
-- Anchored in 3D, so BillboardGuis carry them — no per-frame projection
-- on our side — and they rebuild on a timer rather than every frame.
local markers = {} -- instance -> BillboardGui
local function marker(part, text, color, size, maxDistance)
local billboard = make("BillboardGui", {
Adornee = part,
Size = UDim2.fromOffset(size or 70, 20),
StudsOffset = Vector3.new(0, 1.8, 0),
AlwaysOnTop = true,
MaxDistance = maxDistance or 500,
Parent = Layer,
})
make("TextLabel", {
Text = text,
Font = Enum.Font.GothamBold,
TextSize = 13,
TextColor3 = color,
BackgroundTransparency = 1,
TextStrokeTransparency = 0.35,
Size = UDim2.fromScale(1, 1),
Parent = billboard,
})
return billboard
end
local COIN_COLOR = Color3.fromRGB(255, 214, 64)
local GUN_COLOR = Color3.fromRGB(255, 236, 64)
local TRAP_COLOR = Color3.fromRGB(255, 122, 26)
local gunHighlight
local function updateWorld()
local seen = {}
-- Coins
if S.ESP.CoinESP then
for _, coin in ipairs(Game.coins()) do
local part = coin.part
seen[part] = true
if not markers[part] then
markers[part] = marker(part, "◆", COIN_COLOR, 26, 320)
end
end
end
-- Dropped gun
local drop = Game.GunDrop
if S.ESP.GunESP and drop and drop.Parent then
seen[drop] = true
if not markers[drop] then
markers[drop] = marker(drop, "GUN", GUN_COLOR, 70, 2000)
end
if not gunHighlight or not gunHighlight.Parent then
gunHighlight = make("Highlight", {
FillColor = GUN_COLOR, FillTransparency = 0.45,
OutlineColor = GUN_COLOR, OutlineTransparency = 0,
DepthMode = Enum.HighlightDepthMode.AlwaysOnTop,
Parent = Layer,
})
end
gunHighlight.Adornee = drop
gunHighlight.Enabled = true
elseif gunHighlight then
gunHighlight.Enabled = false
end
-- Traps
if S.ESP.TrapESP then
for trap in pairs(Game.Traps) do
if trap.Parent then
seen[trap] = true
if not markers[trap] then
-- Traps are invisible in game; make ours actually show.
pcall(function() trap.Transparency = 0.4 end)
markers[trap] = marker(trap, "TRAP", TRAP_COLOR, 70, 900)
end
end
end
end
for instance, billboard in pairs(markers) do
if not seen[instance] or not instance.Parent then
pcall(function() billboard:Destroy() end)
markers[instance] = nil
end
end
end
-- ------------------------------------------------------------ lifecycle
for _, player in ipairs(Players:GetPlayers()) do build(player) end
KH.track(Players.PlayerAdded:Connect(build))
KH.track(Players.PlayerRemoving:Connect(destroy))
KH.onFrame("esp", function() updatePlayers() end, 20)
-- 8 Hz is plenty: coins spawn in batches and the gun drops once a round.
KH.loop(0.125, function()
if S.ESP.Enabled and (S.ESP.CoinESP or S.ESP.GunESP or S.ESP.TrapESP) then
updateWorld()
elseif next(markers) then
for instance, billboard in pairs(markers) do
pcall(function() billboard:Destroy() end)
markers[instance] = nil
end
if gunHighlight then gunHighlight.Enabled = false end
end
end)
end
-- ─── src/mm2/71_combat.lua ─────────────────────────────────────────
-- ============================================================================
-- COMBAT — aimbot, silent aim, knife. MM2's gun is server-authoritative: the
-- shot is a RemoteFunction carrying a world position, so aiming means sending
-- good coordinates, not pointing a camera.
-- ============================================================================
do
local UI = KH.UI
local U = KH.U
local Game = KH.Game
local S = KH.S
local LocalPlayer = KH.LocalPlayer
local Combat = {}
KH.Combat = Combat
-- ===================================================== TARGET SELECTION
local function shootableParts(player)
local char = U.charOf(player)
if not char then return nil end
local part = S.Aim.AimAtHead and char:FindFirstChild("Head") or U.torsoOf(char)
return char, part
end
-- char, part, distance when the player can be shot right now. No line of
-- sight or range test on purpose: the server resolves the position, so
-- walls and distance do not stop the shot.
local function validate(player)
if not player or player == LocalPlayer then return nil end
if not Game.isAlive(player) then return nil end
local char, part = shootableParts(player)
if not char or not part then return nil end
local myRoot = U.myRoot()
local distance = myRoot and (myRoot.Position - part.Position).Magnitude or 0
return char, part, distance
end
-- Angular distance from the crosshair, in pixels, or nil when off-screen.
local function screenOffset(part)
local cam = KH.camera()
local pos, onScreen = U.toScreen(part.Position)
if not onScreen then return nil end
local centre = Vector2.new(cam.ViewportSize.X / 2, cam.ViewportSize.Y / 2)
return (pos - centre).Magnitude
end
function Combat.pickTarget(mode)
mode = mode or S.Aim.Target
if mode == "Murderer" then
local murderer = Game.murdererPlayer()
if not murderer then return nil end
local char, part, distance = validate(murderer)
if not char then return nil end
return murderer, char, part, distance
end
local bestPlayer, bestChar, bestPart, bestDistance
local bestScore = math.huge
for _, player in ipairs(U.otherPlayers()) do
local char, part, distance = validate(player)
if char then
local score
if mode == "Crosshair" then
score = screenOffset(part)
else -- Nearest
score = distance
end
if score and score < bestScore then
bestScore = score
bestPlayer, bestChar, bestPart, bestDistance = player, char, part, distance
end
end
end
return bestPlayer, bestChar, bestPart, bestDistance
end
function Combat.aimPoint(char, part)
return U.predict(char, S.Aim.Prediction, S.Aim.PingComp, part) or part.Position
end
-- ============================================================== AIMBOT
local lastShot = 0
local toggleArmed = false
Combat.LastTarget = nil
function Combat.isEngaged()
if not S.Aim.Enabled then return false end
local mode = S.Aim.Mode
if mode == "Always" then return true end
if mode == "Toggle" then return toggleArmed end
-- Press is one shot per key press, fired from the key handler. Never
-- engaged, so the render loop leaves it alone.
if mode == "Press" then return false end
return U.keyHeld(S.Aim.Key)
end
function Combat.setToggle(on)
toggleArmed = on
if on then
UI.notify({title = "Aimbot", text = "Locked on — auto-firing.", kind = "good", duration = 2})
end
end
function Combat.toggleArmedState() return toggleArmed end
-- Disarm at the end of a round: otherwise the lock grabs the camera again
-- the moment the next murderer is known, before you have touched the key.
Game.on("RoundEnd", function()
if toggleArmed then
toggleArmed = false
UI.refreshKeybinds()
end
end)
-- One cooldown for every shot the script sends: MM2 drops shots that land
-- too close together, and the one it drops could be either feature's.
function Combat.fireAt(position, force)
local now = os.clock()
if not force and now - lastShot < S.Aim.FireRate then return false, "cooldown" end
lastShot = now
return Game.shoot(position)
end
-- ========================================================== MOUSE AIM
-- For the executors where the remote shot above never lands. Builds no shot
-- at all: turns the camera onto the target, drives the real cursor onto
-- them and clicks, so MM2's own gun fires it. Needs no hook, only synthetic
-- mouse input. The cost is subtlety — the camera visibly snaps.
local Mouse = {Firing = false, Turning = false}
Combat.Mouse = Mouse
do
local GuiService = game:GetService("GuiService")
local Players = KH.Services.Players
local UIS = KH.Services.UserInputService
-- Plain globals; one the executor lacks reads back nil.
local moveRel = type(mousemoverel) == "function" and mousemoverel or nil
local moveAbs = type(mousemoveabs) == "function" and mousemoveabs or nil
local press = type(mouse1press) == "function" and mouse1press or nil
local release = type(mouse1release) == "function" and mouse1release or nil
local click = type(mouse1click) == "function" and mouse1click or nil
-- The engine's own input injector, open to the elevated context an
-- executor runs as. Touched now so a stripped service fails here
-- rather than mid-shot.
local VIM = (function()
local ok, service = pcall(function() return game:GetService("VirtualInputManager") end)
if not ok or typeof(service) ~= "Instance" then return nil end
local fine = pcall(function() return service.SendMouseButtonEvent end)
return fine and service or nil
end)()
Mouse.CanMove = (moveRel ~= nil) or (moveAbs ~= nil) or (VIM ~= nil)
-- What a viewport pixel is worth to the cursor, and how long a frame of
-- the walk really takes. Both are measured rather than assumed, and both
-- are reported in the menu — a wrong one shows up there first.
local gain, samples = 1, 0
local stepTime = 1 / 60
function Mouse.support()
if not Mouse.CanMove then return "none — this executor cannot move the cursor" end
local mover = moveRel and "mousemoverel"
or moveAbs and "mousemoveabs"
or "VirtualInputManager"
local clicker = (press and release) and "mouse1press"
or click and "mouse1click"
or VIM and "VirtualInputManager"
or "Tool:Activate"
return ("%s + %s · gain %.2f · %d ms"):format(mover, clicker, gain, stepTime * 1000)
end
local mouseObj
local function project(pos)
local point, onScreen = KH.camera():WorldToViewportPoint(pos)
-- Behind the camera the X/Y come back mirrored: chasing them walks
-- the cursor the wrong way, so depth has to agree.
return Vector2.new(point.X, point.Y), onScreen and point.Z > 0
end
-- Where the cursor is, in the same space the target is projected into.
--
-- mouse.X/Y are plain screen coordinates and never lag, but they and
-- WorldToViewportPoint disagree about the topbar on some clients, and a
-- constant offset between the two is a constant aim error. Projecting
-- the world point under the cursor lands in the right space but lags a
-- frame behind the camera, because that point is computed before the
-- lock turns the view. So take the difference between the two on the
-- frames where the camera is still — the frames where both readings are
-- good — and carry it. Right space, no lag, nothing assumed.
local offset = Vector2.zero
local function cursor()
mouseObj = mouseObj or LocalPlayer:GetMouse()
local raw = Vector2.new(mouseObj.X, mouseObj.Y)
if not Mouse.Turning then
local ok, hit = pcall(function() return mouseObj.Hit.Position end)
if ok and typeof(hit) == "Vector3" then
local point, onScreen = project(hit)
if onScreen then offset = point - raw end
end
end
return raw + offset, mouseObj
end
-- A relative move is in OS pixels and the projection is in viewport
-- pixels, and on a scaled display those are not the same unit — so watch
-- what the last move actually achieved and correct the next one.
local function learn(want, got)
if not moveRel then return end
if want.Magnitude < 8 or got.Magnitude < 2 then return end
-- A real display scale lives near 1. Anything wilder is a dropped
-- frame or a cursor that hit the edge of the window, and folding
-- that into the gain is how the aim runs away.
local ratio = want.Magnitude / got.Magnitude
if ratio < 0.4 or ratio > 2.5 then return end
-- Take the first couple of samples whole, since the first press of
-- a session has nothing better to go on, then ease: one noisy frame
-- swinging the next move is the cursor wandering instead of walking.
samples = samples + 1
gain = U.clamp(gain * (1 + (ratio - 1) * (samples <= 2 and 1 or 0.25)), 0.5, 2)
end
local function recalibrate()
gain, samples = 1, 0
end
-- The cursor's reported position only stops responding once it has left
-- the Roblox window, and it can only leave through an edge.
local function atBorder(point)
local view = KH.camera().ViewportSize
return point.X <= 8 or point.Y <= 8
or point.X >= view.X - 8 or point.Y >= view.Y - 8
end
-- Drag the cursor back inside the window. Once it is out there — put
-- there by an earlier bad move, or by the player in windowed mode —
-- mouse.X/Y stick to the border, so every reading is the same pixel and
-- the walk cannot tell where it is or how far it moved. A big move
-- inwards is the only way back. Gain is deliberately not applied: in
-- this state it is the value least worth trusting.
local function sweepHome(here)
if not moveRel then return false end -- absolute movers cannot lose it
local view = KH.camera().ViewportSize
local push = Vector2.new(view.X * 0.5, view.Y * 0.5) - here
if push.Magnitude < 1 then return false end
local far = push.Unit * (view.Magnitude * 0.75)
return (pcall(moveRel, math.floor(far.X + 0.5), math.floor(far.Y + 0.5)))
end
-- The cursor has to stay somewhere Roblox can still see it. Outside the
-- window mouse.X/Y stop moving, so every further move reads as achieving
-- nothing at all.
local function clampToView(point)
local view = KH.camera().ViewportSize
return Vector2.new(
U.clamp(point.X, 6, math.max(view.X - 6, 6)),
U.clamp(point.Y, 6, math.max(view.Y - 6, 6)))
end
local function moveTo(here, point)
if moveRel then
local delta = (point - here) * gain
return (pcall(moveRel, math.floor(delta.X + 0.5), math.floor(delta.Y + 0.5)))
end
local inset = GuiService:GetGuiInset()
local x, y = point.X + inset.X, point.Y + inset.Y
if moveAbs then
return (pcall(moveAbs, math.floor(x + 0.5), math.floor(y + 0.5)))
end
if VIM then
return (pcall(function() VIM:SendMouseMoveEvent(x, y, game) end))
end
return false
end
-- Down, then up: MM2 shoots on the press, but a button left held means
-- the next press never arrives.
local function shootHere()
if press and release then
if pcall(press) then
-- MM2 shoots on the press, so the release only has to
-- happen; waiting here would just delay the next shot.
KH.detach(function()
task.wait(0.04)
pcall(release)
end)
return true
end
pcall(release)
end
if click and pcall(click) then return true end
if VIM then
local point = cursor()
local inset = GuiService:GetGuiInset()
local x, y = point.X + inset.X, point.Y + inset.Y
local ok = pcall(function()
VIM:SendMouseButtonEvent(x, y, 0, true, game, 0)
task.wait(0.04)
VIM:SendMouseButtonEvent(x, y, 0, false, game, 0)
end)
if ok then return true end
end
-- Last resort. MM2 reads the cursor either way, so this still lands
-- where we pointed — unless the gun listens for the raw button.
local gun, equipped = Game.gunTool()
if gun and equipped then
return (pcall(function() gun:Activate() end))
end
return false
end
local TOLERANCE = 4 -- pixel floor for "close enough"
local NEAR_STUDS = 4.5 -- how far off them a landed ray may stop
-- A ceiling on the loop, not the real limit — the clock below is that.
-- Forty steps is under a third of a second at 144 fps, which quietly
-- undid the budget for anyone running a high refresh rate.
local MAX_STEPS = 90
-- Every lead below is counted in frames of the walk, so a 30 fps client
-- leads twice as far as a 144 fps one.
local function noteStep(dt)
if dt > 0 and dt < 0.2 then stepTime = stepTime + (dt - stepTime) * 0.2 end
end
-- Where the target will be in `lead` seconds. Nothing here has a
-- projectile to lead, but the cursor lags a frame behind the move that
-- put it there, the click lags a frame behind that, and the server
-- resolves the shot a ping later still — a running target crosses most
-- of their own body in that. Straight-line is enough: over a couple of
-- frames gravity moves a jumping target a fraction of a stud.
local function livePoint(part, lead)
if not lead or lead <= 0 then return part.Position end
local ok, velocity = pcall(function() return part.AssemblyLinearVelocity end)
if not ok or typeof(velocity) ~= "Vector3" then return part.Position end
return part.Position + velocity * lead
end
Mouse.livePoint = livePoint
-- Where the camera should be pointing for the shot to land. The lock
-- reads this too, so the turn and the shot never disagree about where
-- the target is going to be.
Mouse.Lead = 0
function Mouse.aimPoint(part)
return livePoint(part, Mouse.Lead)
end
-- Where the target sits on screen right now, for the camera lock.
function Mouse.screenOf(part)
return project(livePoint(part))
end
-- How big they look from here, in pixels. The cursor has to end up on
-- their body, not on a point: four pixels is nothing to a target
-- crossing twenty a frame, and the walk would never call itself close
-- enough.
--
-- Measured to the top of their head, not to the top of the part being
-- aimed at. HumanoidRootPart is a two-stud brick at the middle of a
-- six-stud body, so reading its half-height gave a figure about a third
-- of the truth — and every tolerance below is a fraction of this one.
-- Too small, and the walk keeps correcting a cursor already on them
-- while the lead is capped somewhere inside their own hitbox: exactly
-- the case of a target who will not hold still.
local function bodyRadius(char, part, point)
local ok, edge = pcall(function()
local head = char and char:FindFirstChild("Head")
local top = head and (head.Position + Vector3.new(0, head.Size.Y * 0.5, 0))
or (part.Position + Vector3.new(0, part.Size.Y * 0.5, 0))
return project(top)
end)
if not ok then return TOLERANCE end
return U.clamp((edge - point).Magnitude, TOLERANCE, 150)
end
-- A press projects before the camera lock has had a frame to run, so a
-- target who is off screen this instant may simply not have been turned
-- to yet. Give the lock time to bring them in, and no more than that:
-- the walk below is happy to chase a view that is still moving.
local function waitVisible(part)
local speed = U.clamp(S.Aim.MouseSpeed or 1, 0.1, 1)
local deadline = os.clock() + U.clamp(0.2 / speed, 0.2, 0.7)
repeat
local _, onScreen = project(part.Position)
if onScreen then return true end
if not S.Aim.CameraSnap then return false end
task.wait()
until os.clock() >= deadline
return false
end
-- First person and shift lock pin the cursor to the middle of the
-- screen, so there is no cursor to walk: the crosshair *is* the camera
-- and aiming is turning. This is the more exact of the two paths — no
-- display scale to learn, no readback to wait for, one rotation — but it
-- is only available when the lock is allowed to take the camera.
local function pointCamera(char, part)
if not S.Aim.CameraSnap then
return false, "cursor is locked — switch on Turn Camera to aim in first person"
end
Mouse.Lead = stepTime + (S.Aim.PingComp and U.clamp(U.ping() / 3000, 0, 0.06) or 0)
Mouse.Precise = true
local budget = os.clock() + 0.3
local reached, why = false, "could not turn onto them"
while true do
if not (char.Parent and part.Parent) then
why = "target gone"
break
end
-- The lock does the turning, on its own bind after Roblox's
-- camera module. Writing the camera from here as well would put
-- two authors on it in the same frame.
task.wait()
local point, onScreen = project(Mouse.aimPoint(part))
if onScreen then
local view = KH.camera().ViewportSize
local centre = Vector2.new(view.X / 2, view.Y / 2)
local hit = select(2, cursor()).Target
local onBody = hit and hit:IsDescendantOf(char)
-- The cursor sits dead centre, so the distance from the
-- centre to them is the whole aiming error.
if onBody or (point - centre).Magnitude <= bodyRadius(char, part, point) * 0.75 then
local blocker = not onBody and hit and hit:FindFirstAncestorOfClass("Model")
local owner = blocker and Players:GetPlayerFromCharacter(blocker)
if owner and owner ~= LocalPlayer then
why = "holding fire — " .. owner.DisplayName .. " is in the line"
else
reached, why = true, hit and ("through " .. hit.Name) or "on target"
end
break
end
end
if os.clock() > budget then break end
end
Mouse.Precise = false
Mouse.Lead = 0
return reached, why
end
-- Puts the cursor on the target and says whether it got there.
--
-- Nobody else can see your cursor — the camera turn is the only part of
-- this anyone else witnesses — so there is nothing to be gained by
-- easing it across the screen. It goes the whole way in one move, then
-- spends the frames after that correcting for what the move actually
-- achieved. That is both the fastest way there and the most exact.
local function pointAt(char, part)
-- No cursor to place: turn instead.
if UIS.MouseBehavior ~= Enum.MouseBehavior.Default then
return pointCamera(char, part)
end
if not Mouse.CanMove then return false, "no mouse control" end
if not waitVisible(part) then return false, "off screen" end
-- A target standing still is reached in two or three frames and
-- never sees this. It is the one jumping and strafing that needs
-- the frames, and a fifth of a second was not enough of them to
-- catch someone mid-jump: the walk ran out of budget on the way up
-- and reported that it could not reach them.
local budget = os.clock() + 0.45
-- The click lands a frame after the cursor does, and MM2 casts its
-- own ray then rather than now.
local pingLead = S.Aim.PingComp and U.clamp(U.ping() / 3000, 0, 0.06) or 0
local lastFrom, lastWant, stuck = nil, nil, 0
local tick = os.clock()
local grazed = false
for _ = 1, MAX_STEPS do
if not (char.Parent and part.Parent) then return false, "target gone" end
local mark = os.clock()
if lastWant then noteStep(mark - tick) end
tick = mark
local here, m = cursor()
-- The last move has not shown up yet. Issuing another on top of
-- it is how the cursor overshoots and has to come back — wait
-- for it to land instead. Against an edge it means something
-- else: the cursor is outside the window, where its reported
-- position stops changing at all, and only a sweep inwards gets
-- it back.
if lastWant and lastWant.Magnitude >= 4 and (here - lastFrom).Magnitude < 1 then
if atBorder(here) then
stuck = stuck + 1
if stuck >= 2 then
stuck = 0
recalibrate()
lastFrom, lastWant = nil, nil
if not sweepHome(here) then return false, "cursor is off the window" end
end
end
task.wait()
else
stuck = 0
local now, onScreen = project(part.Position)
if not onScreen then return false, "off screen" end
if lastWant then learn(lastWant, here - lastFrom) end
-- Two questions, and for anyone moving they have different
-- answers. Where must the ray land? Where they will be when
-- the click is processed, a frame from now. Where must the
-- cursor be sent? Where they will be once *this* move has
-- landed, a frame later again. Never past their body edge
-- either way: MM2 shoots wherever the cursor's ray stops, so
-- a lead that overshoots them sends the wall behind instead.
local radius = bodyRadius(char, part, now)
local function ahead(seconds)
local point, ok = project(livePoint(part, seconds))
if not ok then return now end
local drift = point - now
local cap = radius * 0.8
if drift.Magnitude > cap then drift = drift.Unit * cap end
return now + drift
end
local hitPoint = ahead(stepTime + pingLead)
local sendTo = ahead(stepTime * 2 + pingLead)
-- Against where the shot has to land, not where the next
-- move is going: the cursor is already a frame ahead, and
-- measuring it against the frame after that left a running
-- target permanently one step out of reach.
local gap = (hitPoint - here).Magnitude
local spent = os.clock() >= budget
-- The ray hitting them proves where they were a frame ago,
-- which is the whole answer for someone standing still and
-- worth nothing against someone crossing.
local hit = m.Target
local onBody = hit and hit:IsDescendantOf(char)
local settled = (hitPoint - now).Magnitude <= radius * 0.3
local close = gap <= math.max(TOLERANCE, radius * 0.7)
or (spent and gap <= radius)
-- Pixels lie. A cursor within a body width of their centre
-- can still be a ray that slips past them into the scenery
-- twenty studs behind, and MM2 sends wherever the ray
-- stops, so that is what has to be near them -- in studs.
local lands = onBody and settled
if not lands and close then
local reach, at = pcall(function() return m.Hit.Position end)
lands = reach
and (at - livePoint(part, stepTime)).Magnitude <= NEAR_STUDS
if not lands then grazed = true end
end
if lands then
-- MM2 shoots where the cursor's ray stops. A wall costs
-- a wasted shot, but another player standing in the line
-- costs the round: a sheriff who shoots an innocent dies
-- for it. Hold fire and let the next press try again.
local blocker = not onBody and hit and hit:FindFirstAncestorOfClass("Model")
local owner = blocker and Players:GetPlayerFromCharacter(blocker)
if owner and owner ~= LocalPlayer then
return false, "holding fire — " .. owner.DisplayName .. " is in the line"
end
return true, hit and ("through " .. hit.Name) or "on target"
end
if spent then break end
-- The whole way, every time. What the move does not achieve
-- is measured next frame and taken off the one after.
local dest = clampToView(sendTo)
lastFrom, lastWant = here, dest - here
if not moveTo(here, dest) then
return false, "no mouse control"
end
task.wait()
end
end
-- On them by the pixels every time and never by the ray: they are
-- behind something, and the shot would go into whatever that is.
if grazed then return false, "the shot would land behind them" end
-- Never got there: the scale it is moving by is the thing most
-- likely wrong, so the next press starts from a clean one.
recalibrate()
return false, "could not reach"
end
local busy = false
local activePart
-- The part the in-flight sequence is aiming at, or nil between shots.
function Mouse.active() return activePart end
-- One at a time: the walk yields across frames, and the render loop
-- would otherwise have a dozen of them fighting over the cursor.
function Mouse.fire(player, char, part, force)
if busy then return false, "aiming" end
-- A locked cursor is aimed by turning, which needs no mouse mover
-- at all — only something to click with.
if UIS.MouseBehavior == Enum.MouseBehavior.Default and not Mouse.CanMove then
return false, "no mouse control"
end
local now = os.clock()
if not force and now - lastShot < S.Aim.FireRate then return false, "cooldown" end
lastShot = now
busy = true
activePart = part
local function sequence()
local dry = S.Aim.DryRun
-- Draw the gun and walk at the same time rather than one after
-- the other: EquipTool takes a frame or two, and so does the
-- walk, so waiting for the first before starting the second
-- doubled the time from the key press to the shot.
local gun, equipped = Game.gunTool()
if gun and not equipped and not dry then Game.equip(gun) end
local reached, why = pointAt(char, part)
if not reached then
Combat.LastResult = (dry and "aim test — " or "mouse aim — ") .. why
-- No shot went out; do not charge the next one a cooldown.
lastShot = 0
-- A press that quietly does nothing looks like a dead key.
if force then
UI.notify({title = dry and "Aim Test" or "Aimbot", text = why, kind = "warn", duration = 2})
end
return
end
-- Aim test: say where the cursor actually ended up. The point
-- under it is the one MM2 would have sent, so its distance to
-- them is the whole truth about whether the shot would land.
if dry then
local _, m = cursor()
local ok, at = pcall(function() return m.Hit.Position end)
local speed = 0
pcall(function() speed = part.AssemblyLinearVelocity.Magnitude end)
local text = ok
and ("%.1f studs off %s, who was doing %.0f studs/s (%s)"):format(
(at - part.Position).Magnitude, player.DisplayName, speed, why)
or "could not read where the cursor landed"
Combat.LastResult = "aim test — " .. text
if force then
UI.notify({title = "Aim Test", text = text, duration = 4})
end
return
end
-- The click still needs the gun actually out.
local began = os.clock()
while not select(2, Game.gunTool()) and os.clock() - began < 0.25 do
task.wait()
end
Mouse.Firing = true
local sent = shootHere()
Combat.LastResult = sent
and ("mouse shot at " .. player.DisplayName .. " — " .. why)
or "cursor on target but the click went nowhere"
if force and not sent then
UI.notify({title = "Aimbot", text = "the click went nowhere", kind = "bad", duration = 2})
end
end
KH.detach(function()
-- Both flags have to clear even if the walk throws. A stuck
-- busy refuses every later shot, and a stuck Firing makes the
-- silent-aim handler ignore every real click.
local ok, err = pcall(sequence)
Mouse.Firing = false
Mouse.Precise = false
Mouse.Lead = 0
activePart = nil
busy = false
if not ok then Combat.LastResult = "mouse aim error: " .. tostring(err) end
end)
return true
end
end
-- Fires once at the current best target. Returns true when a shot went out.
function Combat.fireOnce(force)
-- A key press that does nothing at all is the most confusing thing this
-- can do, so say why. Only for a real press: the render loop calls this
-- every frame and would notify every frame with it.
local function refuse(reason)
Combat.LastResult = reason
if force then
UI.notify({title = "Aimbot", text = reason, kind = "warn", duration = 2})
end
return false, reason
end
-- An aim test needs no gun: it is checking where the cursor lands, and
-- waiting to roll sheriff to find that out is most of a round each try.
local gun = Game.gunTool()
if not gun and not S.Aim.DryRun then
return refuse("no gun — you are not holding one")
end
local player, char, part = Combat.pickTarget()
if not player and S.Aim.DryRun then
-- Anyone will do when nothing is going to be fired at them.
player, char, part = Combat.pickTarget("Nearest")
end
if not player then
return refuse(S.Aim.Target == "Murderer"
and "no target — the murderer is not known yet"
or "no target")
end
Combat.LastTarget = player
local ok, err
if S.Aim.Method == "Mouse" then
-- Mouse.fire reports its own result, several frames from now.
ok, err = Mouse.fire(player, char, part, force)
else
ok, err = Combat.fireAt(Combat.aimPoint(char, part), force)
if ok then
Combat.LastResult = "remote shot at " .. player.DisplayName
elseif err ~= "cooldown" then
-- A cooldown is the fire rate doing its job, not a failure.
Combat.LastResult = tostring(err)
end
end
-- A cooldown is the fire rate doing its job, not a failure worth saying
-- out loud; everything else here means the press went nowhere.
if not ok then
if err == "cooldown" then return false, err end
return refuse(tostring(err))
end
if S.Aim.NotifyShot then
UI.notify({title = "Shot", text = "Fired at " .. player.DisplayName, duration = 1.5})
end
return ok, err
end
-- Why the aimbot is or is not shooting. A dead key, no gun and a shot the
-- server drops all look identical, and only the last one means anything.
function Combat.aimStatus()
if not S.Aim.Enabled then return "off" end
if not Combat.isEngaged() then
local key = tostring(S.Aim.Key)
if S.Aim.Mode == "Press" then
-- The last press is the only thing worth reporting here.
return "ready — press " .. key .. (Combat.LastResult and (" · last: " .. Combat.LastResult) or "")
end
if S.Aim.Mode == "Toggle" then return "not armed — press " .. key end
if S.Aim.Mode == "Hold" then return "waiting for " .. key end
return "idle"
end
return "engaged — " .. (Combat.LastResult or "...")
end
-- Bound after Roblox's own camera step rather than onto KH's render job.
-- The job runs before the camera module, so every lock we wrote there was
-- overwritten in the same frame — the aim fighting itself, once per frame.
do
local RunService = KH.Services.RunService
local BIND = "KittyHubAimCam"
-- Start turning at the edge of the view, stop well inside it. The gap
-- between the two is what stops a target hovering near the border from
-- switching the lock on and off every frame.
local TURN_IN, TURN_OUT = 0.10, 0.28
local turning = false
local held -- the rotation we are driving, nil when hands off
-- How far into the view the target is: 0 at the border, 0.5 dead
-- centre, negative off screen.
local function inset(part)
local point, onScreen = Combat.Mouse.screenOf(part)
if not onScreen then return -1 end
local view = KH.camera().ViewportSize
return math.min(
math.min(point.X, view.X - point.X) / math.max(view.X, 1),
math.min(point.Y, view.Y - point.Y) / math.max(view.Y, 1))
end
-- What the lock should be aiming at, or nil for hands off. Ordered
-- cheapest test first: this runs every rendered frame of the session,
-- and in Press mode it answers nil on the second line nearly always.
local function lockTarget()
if S.Aim.Method ~= "Mouse" or not S.Aim.CameraSnap then return nil end
-- While a shot is in flight, hold that shot's target. Between
-- shots only the sustained triggers keep the camera, which is what
-- lets Press lock on, fire, and hand it straight back.
local part = Combat.Mouse.active()
if not part then
if not Combat.isEngaged() then return nil end
local _, _, picked = Combat.pickTarget()
part = picked
end
-- Last, because it is the only test that walks the character.
if part and (Game.gunTool() or S.Aim.DryRun) then return part end
return nil
end
local function lockCamera(dt)
local part = lockTarget()
-- A shot with a locked cursor is aimed entirely by this: it has to
-- land exactly on the target rather than ease towards it, and it
-- cannot stop early for being comfortably inside the view.
local precise = part ~= nil and Combat.Mouse.Precise
if precise then
turning = true
elseif part then
-- Turn only when there is something to turn for: a target
-- already well inside the view needs no camera work at all,
-- and a view that stays still is both smoother and far less
-- obvious than one that snaps on every shot.
local depth = inset(part)
if depth < TURN_IN then turning = true
elseif depth > TURN_OUT then turning = false end
else
turning = false
end
-- The walk needs to know: while this is true its projected cursor
-- reading is a frame behind the camera and must not be trusted.
Combat.Mouse.Turning = turning
if not (turning or held) then return end
local cam = KH.camera()
local speed = U.clamp(S.Aim.MouseSpeed or 1, 0.1, 1)
-- The same share of the gap per second rather than per frame, so
-- the turn lasts the same at 30 fps as at 144.
local alpha = speed >= 1 and 1
or U.clamp(1 - (1 - speed) ^ ((dt or 1 / 60) * 60), 0, 1)
-- Position belongs to the camera module, which has already written
-- it this frame; we only ever replace the rotation, carried forward
-- from wherever the module has just put the camera.
held = CFrame.new(cam.CFrame.Position) * (held or cam.CFrame).Rotation
if turning then
local want = CFrame.new(cam.CFrame.Position, Combat.Mouse.aimPoint(part))
held = precise and want or held:Lerp(want, alpha)
cam.CFrame = held
return
end
-- Turned far enough, but the shot is still in the air: hold this
-- rotation rather than starting to give it back. Drifting towards
-- the player's angles now would walk the target across the screen
-- while the cursor is still closing on it, and the two would spend
-- the rest of the shot chasing each other.
if Combat.Mouse.active() then
cam.CFrame = held
return
end
-- Hand it back over a few frames rather than dropping it. The
-- module still holds the angles the player left it at, so letting
-- go in one frame snaps the view — easing into its CFrame, which is
-- what we just read, turns that snap into a turn.
held = held:Lerp(cam.CFrame, alpha)
if held.LookVector:Dot(cam.CFrame.LookVector) > 0.9995 then
held = nil
return
end
cam.CFrame = held
end
-- A bind left over from a session that failed to unload would make
-- this one error out and leave the camera unlocked.
pcall(function() RunService:UnbindFromRenderStep(BIND) end)
pcall(function()
RunService:BindToRenderStep(BIND, Enum.RenderPriority.Camera.Value + 1, function(dt)
if KH.Alive then KH.safe("aimcam", lockCamera, dt) end
end)
end)
KH.undo(function() pcall(function() RunService:UnbindFromRenderStep(BIND) end) end)
end
KH.onFrame("aimbot", function()
if not Combat.isEngaged() then return end
Combat.fireOnce(false)
end, 30)
-- MM2 stows your tool on respawn and when a round flips, which would
-- leave the next round's first shot doing nothing but re-equipping.
KH.loop(0.5, function()
if not (S.Aim.Enabled and S.Aim.KeepEquipped) then return end
local gun, equipped = Game.gunTool()
if gun and not equipped then Game.equip(gun) end
end)
-- =========================================================== SILENT AIM
-- The shot *you* fire by hand lands on the target instead of under your
-- cursor. Three routes, best available wins:
-- hook rewrite the position argument in flight. Invisible, but needs
-- a real hookmetamethod or a writable game metatable.
-- takeover no hook: switch MM2's gun script off so your click stops
-- producing a shot of its own, and fire the aimed one from that
-- same click. One shot, out of a plain property write.
-- click neither: an aimed shot beside your real one, two beams.
do
local X = KH.X
local UIS = KH.Services.UserInputService
-- --------------------------------------------------------- route probe
-- Takeover needs one thing: switching a client script off. A throwaway
-- instance settles that without going near the gun.
local canDisableScripts = (function()
local ok, disabled = pcall(function()
local probe = Instance.new("LocalScript")
probe.Disabled = true
local value = probe.Disabled
probe:Destroy()
return value
end)
return ok and disabled == true
end)()
local hookReady = false -- set by the install self-test below
local suppressed = {} -- [BaseScript] = true, ones we turned off
-- Which route is live. The manual settings exist because "installed"
-- and "works" are not the same claim on these executors.
local function effectiveMode()
local want = S.Aim.SilentMode or "Auto"
if want == "Click" then return "click" end
-- Takeover switches MM2's gun script off; the mouse aimbot fires
-- that same script. They cannot both be on, and the aimbot wins.
local canTakeover = canDisableScripts and S.Aim.Method ~= "Mouse"
if want == "Takeover" then return canTakeover and "takeover" or "click" end
if hookReady then return "hook" end
-- Auto and Hook both prefer the hook, and both fall back the same
-- way when there isn't one.
return canTakeover and "takeover" or "click"
end
Combat.effectiveMode = effectiveMode
-- ------------------------------------------------------ route 1: hook
-- Must never throw — an error here breaks the call it wrapped, leaving
-- the gun unable to shoot — and must ignore our own calls, or Kill All
-- would have every shot rewritten onto one target. No reliable unhook
-- exists, so it stays installed and passes through once KH.Alive drops.
local oldNamecall -- the original, set by whichever route installs
local verifying = true -- flipped off once the install self-test is done
local sawCall = false
local function onNamecall(self, ...)
if verifying then sawCall = true end
if not oldNamecall then return end
if not KH.Alive or not S.Aim.SilentAim then
return oldNamecall(self, ...)
end
-- Forced onto another route: leave the shot exactly as it was sent.
if not verifying and effectiveMode() ~= "hook" then
return oldNamecall(self, ...)
end
if X.checkcaller and checkcaller() then
-- Our own Game.shoot() call; already aimed.
return oldNamecall(self, ...)
end
local ok, redirected = pcall(function(...)
if getnamecallmethod() ~= "InvokeServer" then return nil end
-- Only the gun's own beam remote is of interest.
local parent = typeof(self) == "Instance" and self.Parent or nil
if not parent or parent.Name ~= "CreateBeam" then return nil end
local args = table.pack(...)
if typeof(args[2]) ~= "Vector3" then return nil end
local player, char, part = Combat.pickTarget()
if not (player and char and part) then return nil end
args[2] = Combat.aimPoint(char, part)
Combat.LastTarget = player
return args
end, ...)
if ok and redirected then
return oldNamecall(self, table.unpack(redirected, 1, redirected.n))
end
return oldNamecall(self, ...)
end
-- Two ways in: the convenience wrapper, then the raw metatable for
-- executors that only expose the primitives.
local function installHook()
local cb = onNamecall
if X.newcclosure then
-- Some executors reject a plain Lua closure as a metamethod.
local ok, wrapped = pcall(newcclosure, cb)
if ok and type(wrapped) == "function" then cb = wrapped end
end
if X.hookmetamethod then
local ok, old = pcall(hookmetamethod, game, "__namecall", cb)
if ok and type(old) == "function" then
oldNamecall = old
return "hookmetamethod"
end
end
if X.rawmeta then
local ok = pcall(function()
local mt = getrawmetatable(game)
local old = rawget(mt, "__namecall")
if type(old) ~= "function" then error("__namecall is not a function") end
setreadonly(mt, false)
oldNamecall = old
mt.__namecall = cb
setreadonly(mt, true)
end)
if ok then return "getrawmetatable" end
oldNamecall = nil
end
end
local route = installHook()
if route then
-- Reporting success and never firing is the normal failure here,
-- so prove it runs on a harmless call before trusting it.
pcall(function() return game:GetService("Players") end)
verifying = false
hookReady = sawCall
Combat.SilentRoute = route
if not sawCall then
Combat.SilentReason = "the " .. route .. " hook installed but never fires"
end
else
verifying = false
Combat.SilentReason = (X.hookmetamethod or X.rawmeta)
and "this executor's metamethod hook could not be installed"
or "this executor exposes no metamethod hook"
end
Combat.HookAvailable = hookReady
-- -------------------------------------------------- route 2: takeover
-- Client-side property writes do not replicate, so the server's view of
-- the tool is untouched — it just stops hearing from the script that
-- spoke for your mouse. You lose the local shot sound and muzzle flash;
-- the beam and the kill are the server's work and look normal.
local function isClientScript(inst)
if inst:IsA("LocalScript") then return true end
if not inst:IsA("Script") then return false end
-- A re-upload could use a client-context Script instead.
local ok, ctx = pcall(function() return inst.RunContext end)
return ok and ctx == Enum.RunContext.Client
end
-- Only record scripts *we* turned off, so restoring never switches on
-- something MM2 disabled for its own reasons.
local function suppress(gun)
-- Every round destroys the old Gun, so drop the dead entries or the
-- table grows for the whole session.
for inst in pairs(suppressed) do
if not inst.Parent then suppressed[inst] = nil end
end
for _, inst in ipairs(gun:GetDescendants()) do
local ok, isClient = pcall(isClientScript, inst)
if ok and isClient and not inst.Disabled then
if pcall(function() inst.Disabled = true end) and inst.Disabled then
suppressed[inst] = true
end
end
end
end
local function restore()
if not next(suppressed) then return end
for inst in pairs(suppressed) do
pcall(function() inst.Disabled = false end)
end
suppressed = {}
-- Re-enabling restarts the script from the top. One that only
-- connects from Equipped would sit dead until the gun is next
-- drawn, so hand it that event rather than guess which it is.
local gun, equipped = Game.gunTool()
if gun and equipped then
KH.detach(function()
local char = U.charOf(LocalPlayer)
local hum = char and char:FindFirstChildOfClass("Humanoid")
if not hum then return end
pcall(function() hum:UnequipTools() end)
task.wait(0.1)
Game.equip(gun)
end)
end
end
KH.undo(restore)
-- On a timer, not once: the Gun is a fresh instance every round, and
-- the toggle and unload have to hand it back at any moment.
KH.loop(0.3, function()
if KH.Alive and S.Aim.SilentAim and effectiveMode() == "takeover" then
local gun = Game.gunTool()
if gun then suppress(gun) end
elseif next(suppressed) then
restore()
end
end)
-- --------------------------------------------------------- your click
-- Under takeover this *is* the gun — MM2's script is off, so if this
-- does not fire, nothing does. Under the hook it stays out of the way.
local mouse
local function mousePoint()
local ok, point = pcall(function()
mouse = mouse or LocalPlayer:GetMouse()
return mouse.Hit.Position
end)
if ok and typeof(point) == "Vector3" then return point end
return nil
end
KH.track(UIS.InputBegan:Connect(function(input, processed)
-- Touch counts: no-hook executors are also the phone ones.
local kind = input.UserInputType
if kind ~= Enum.UserInputType.MouseButton1
and kind ~= Enum.UserInputType.Touch then return end
if processed then return end -- the input landed on the menu
if Combat.Mouse.Firing then return end -- our own synthetic click
if not (KH.Alive and S.Aim.SilentAim) then return end
local mode = effectiveMode()
if mode == "hook" then return end -- the hook already redirected it
-- Only with the gun genuinely in hand. Drawing it here would yank
-- a murderer's knife away mid-round.
local gun, equipped = Game.gunTool()
if not (gun and equipped) then return end
local ok, err = Combat.fireOnce(false)
if not ok and err == "no target" and mode == "takeover" then
-- Nothing to redirect onto and MM2's script is off, so put the
-- round where you actually pointed.
local point = mousePoint()
if point then Combat.fireAt(point, false) end
end
end))
-- ------------------------------------------------------------- status
-- Both real routes give one shot on target; only "click" has to apologise.
Combat.SilentDesc = hookReady
and "Redirects the shot you fire by hand onto the target. One shot, and nothing on screen moves."
or (canDisableScripts
and "No working hook on this executor, so Kitty Hub switches MM2's gun script off and fires the aimed shot on your click itself. Still one shot; you lose the local shot sound."
or "No route on this executor — your click fires an aimed shot beside your real one, so two beams go out.")
function Combat.silentStatus()
local mode = effectiveMode()
if mode == "hook" then
return "hook (" .. tostring(Combat.SilentRoute) .. ")"
elseif mode == "takeover" then
return "takeover" .. (next(suppressed) and " — gun script off" or " — waiting for gun")
end
-- Only say *why* when it was forced on us.
if S.Aim.SilentMode == "Click" then return "click aim, two shots — your choice" end
if canDisableScripts and S.Aim.Method == "Mouse" then
return "click aim, two shots — takeover is off while Fire Method is Mouse"
end
return "click aim, two shots — " .. (Combat.SilentReason or "no route")
end
end
-- ============================================================= KILL ALL
-- Sequential, not a burst: MM2 rate-limits the shot remote.
local killAllRunning = false
function Combat.killAll()
if killAllRunning then return end
if not Game.gunTool() then
UI.notify({title = "Kill All", text = "You are not holding a gun.", kind = "bad"})
return
end
killAllRunning = true
KH.detach(function()
local count = 0
for _, player in ipairs(U.otherPlayers()) do
if not KH.Alive then break end
if Game.isAlive(player) then
local char, part = shootableParts(player)
if char and part then
Game.shoot(Combat.aimPoint(char, part))
count = count + 1
task.wait(0.12)
end
end
end
killAllRunning = false
UI.notify({title = "Kill All", text = ("Fired at %d players."):format(count), kind = "good"})
end)
end
-- =============================================================== KNIFE
-- `sheriffFirst` is the bound-key path asking for the gun holder whatever
-- the Target Filter toggles say: pressing the throw key is a deliberate
-- call, and the one target worth spending the knife on is the one who can
-- shoot back.
local function knifeTargets(sheriffFirst)
local out = {}
local myRoot = U.myRoot()
if not myRoot then return out end
local skipSheriff = S.Knife.SkipSheriff and not sheriffFirst
for _, player in ipairs(U.otherPlayers()) do
if Game.isAlive(player) then
local role = Game.roleOf(player)
local isSheriff = (role == "Sheriff" or role == "Hero")
-- Prioritising the sheriff is the sort's job, below. Filtering
-- here as well left an empty list whenever no sheriff was alive.
if not (skipSheriff and isSheriff) then
local char = U.charOf(player)
local part = char and U.torsoOf(char)
if part then
out[#out + 1] = {
player = player, char = char, part = part,
distance = (myRoot.Position - part.Position).Magnitude,
sheriff = isSheriff,
}
end
end
end
end
-- Sheriff first when asked for, then by proximity.
local preferSheriff = sheriffFirst or S.Knife.TargetSheriff
table.sort(out, function(a, b)
if preferSheriff and a.sheriff ~= b.sheriff then return a.sheriff end
return a.distance < b.distance
end)
return out
end
Combat.knifeTargets = knifeTargets
-- A thrown knife is gone until it comes back, so it is only worth throwing
-- at a range the throw can actually cover.
local function inThrowRange(target)
return target ~= nil and target.distance <= (S.Knife.ThrowRange or 70)
end
-- Asked once now, for the throw that goes straight out, and again at the
-- release when the animation holds the knife back. Same aim either way.
local function throwAt(target)
local function point()
return U.predict(target.char, S.Aim.Prediction + 1, S.Aim.PingComp)
or target.part.Position
end
return Game.throwKnife(point(), point)
end
function Combat.throwAtNearest()
if not Game.knifeTool() then return false end
local target = knifeTargets()[1]
if not inThrowRange(target) then return false end
return throwAt(target)
end
-- The sheriff when the throw can reach them, otherwise the closest target
-- it can: the list already runs sheriff-then-distance, so the first entry
-- in range is the right one. Refusing outright because the only sheriff is
-- across the map would make the key feel dead with someone stood in front
-- of you.
local function pickThrowTarget()
for _, target in ipairs(knifeTargets(true)) do
if inThrowRange(target) then return target end
end
return nil
end
-- ------------------------------------------------------ bound-key throw
-- One press, one throw — no loop, no repeat, no cooldown beyond the draw
-- it may have to wait for. The flag only covers that draw: while the knife
-- is already out the whole thing runs inline on the pressing frame, which
-- is as fast as a throw can be sent.
local drawing = false
function Combat.throwOnce(announce)
local function refuse(text, kind)
if announce then
UI.notify({title = "Knife", text = text, kind = kind or "warn"})
end
return false
end
-- A second press during the draw would put two knives in the air off
-- one keystroke.
if drawing then return false end
local knife, equipped = Game.knifeTool()
if not knife then return refuse("You have no knife.", "bad") end
local target = pickThrowTarget()
if not target then return refuse("Nobody alive within throwing range.") end
-- Already drawn: send it now rather than a frame later.
if equipped then return throwAt(target) end
-- Not drawn. EquipTool is not instant and the server drops a throw from
-- a knife it does not yet think we hold, so wait for the tool to land
-- in the character first. It stays out afterwards — nothing here stows
-- it again, and the next press skips this branch entirely.
drawing = true
KH.detach(function()
-- pcall, or one error leaves the flag stuck on forever.
pcall(function()
if not Game.equip(knife) then return end
local char = U.charOf(LocalPlayer)
local began = os.clock()
while knife.Parent ~= char and os.clock() - began < 0.35 do
task.wait()
char = U.charOf(LocalPlayer)
end
if knife.Parent ~= char then return end
-- Re-pick: the wait is short, but a target can die or walk out
-- of range inside it and a stale point throws the knife away.
local fresh = pickThrowTarget()
if fresh then throwAt(fresh) end
end)
drawing = false
end)
return true
end
-- Auto throw, Always mode only — in Press mode the bound key is the
-- trigger and a timer running underneath it would throw knives the user
-- never asked for.
KH.loop(0.1, function()
if not (S.Knife.AutoThrow and S.Knife.ThrowMode == "Always") then return end
if not Game.knifeTool() then return end
Combat.throwAtNearest()
task.wait(math.max(S.Knife.ThrowDelay, 0.2))
end)
-- Load the throw animation before it is first needed. Its length is what
-- the release is measured from, and that reads zero until Roblox has the
-- asset — which would leave the first throw of every round the one that
-- goes out on frame one.
KH.loop(1, function()
if not S.Knife.ThrowAnim then return end
local knife = Game.knifeTool()
if knife then Game.primeThrowAnimation(knife) end
end)
-- Knife aura: stab anything that wanders into range.
KH.loop(0.05, function()
if not S.Knife.Aura then return end
if not Game.knifeTool() then return end
local targets = knifeTargets()
local nearest = targets[1]
if nearest and nearest.distance <= S.Knife.AuraRadius then
Game.stab()
task.wait(math.max(S.Knife.AuraDelay, 0.05))
end
end)
-- Blink to the target, swing, blink back. The return hop is what keeps it
-- from reading as a teleport to everyone else.
local tpStabRunning = false
function Combat.tpStab()
if tpStabRunning then return end
if not Game.knifeTool() then
UI.notify({title = "Knife", text = "You are not holding a knife.", kind = "bad"})
return
end
local target = knifeTargets()[1]
if not target then return end
-- Blinking the length of the map to reach someone is not a stab, it is
-- a teleport everyone can see.
if target.distance > (S.Knife.TpRange or 150) then return end
tpStabRunning = true
KH.detach(function()
local root = U.myRoot()
if root then
local origin = root.CFrame
-- Already within swinging distance: no reason to blink at all.
if target.distance > 6 then
root.CFrame = target.part.CFrame * CFrame.new(0, 0, 2.5)
task.wait(0.08)
end
Game.stab()
task.wait(0.12)
-- Same body only. Dying mid-blink would otherwise drag the
-- fresh spawn back to where the old one was standing.
if U.myRoot() == root then root.CFrame = origin end
end
tpStabRunning = false
end)
end
KH.loop(0.1, function()
if S.Knife.TpStab and Game.knifeTool() then
Combat.tpStab()
task.wait(0.35)
end
end)
end
-- ─── src/mm2/72_farm.lua ───────────────────────────────────────────
-- ============================================================================
-- FARM — coin collection, the coin magnet, gun-drop grabbing, anti-AFK
-- ============================================================================
do
local UI = KH.UI
local U = KH.U
local Game = KH.Game
local S = KH.S
local X = KH.X
local VirtualUser = KH.Services.VirtualUser
local LocalPlayer = KH.LocalPlayer
local Farm = {}
KH.Farm = Farm
Farm.Collected = 0
-- Coins that were touched but whose model has not despawned yet. Without
-- this the farm re-targets the same coin forever.
local claimed = {}
-- The drop we already reacted to, so we never run at it twice.
local triedDrop = nil
Game.on("RoundStart", function() claimed = {} triedDrop = nil end)
Game.on("RoundEnd", function() claimed = {} triedDrop = nil end)
-- ------------------------------------------------------------- collect
local function touch(part)
if not X.firetouch then return false end
local root = U.myRoot()
if not root or not part or not part.Parent then return false end
local ok = pcall(function()
firetouchinterest(root, part, 0)
firetouchinterest(root, part, 1)
end)
return ok
end
Farm.touch = touch
local function markCollected(coin)
if claimed[coin.model] then return end
claimed[coin.model] = os.clock()
Farm.Collected = Farm.Collected + 1
end
-- Stale claims are released after a few seconds so a coin we failed to pick
-- up does not get ignored for the rest of the round.
KH.loop(2, function()
local now = os.clock()
for model, at in pairs(claimed) do
if not model.Parent or now - at > 6 then claimed[model] = nil end
end
end)
-- ============================================================ COIN FARM
local function moveTeleport(coin)
U.moveTo(coin.part.Position + Vector3.new(0, 2.5, 0), true)
task.wait(math.max(S.Farm.CoinDelay, 0))
end
-- Interpolated hop: still a teleport, but spread over several frames so it
-- reads as fast movement rather than a snap.
local function moveSmooth(coin)
local root = U.myRoot()
if not root then return end
local startPos = root.Position
local endPos = coin.part.Position + Vector3.new(0, 2.5, 0)
local distance = (endPos - startPos).Magnitude
local duration = distance / math.max(S.Farm.CoinSpeed, 1)
local began = os.clock()
while S.Farm.CoinFarm and KH.Alive do
local elapsed = os.clock() - began
if elapsed >= duration then break end
if not coin.part.Parent then break end
local current = U.myRoot()
if not current then break end
current.CFrame = CFrame.new(startPos:Lerp(endPos, elapsed / duration))
task.wait()
end
end
local function moveWalk(coin)
local hum = U.myHum()
if not hum then return end
hum:MoveTo(coin.part.Position)
local began = os.clock()
while S.Farm.CoinFarm and KH.Alive and os.clock() - began < 6 do
local root = U.myRoot()
if not root or not coin.part.Parent then break end
if (root.Position - coin.part.Position).Magnitude < 4 then break end
task.wait(0.15)
end
end
KH.loop(0.05, function()
if not S.Farm.CoinFarm then return end
if not U.myRoot() then return end
local coin = Game.nearestCoin(claimed)
if not coin then
task.wait(1)
return
end
local mode = S.Farm.CoinMode
if mode == "Smooth" then moveSmooth(coin)
elseif mode == "Walk" then moveWalk(coin)
else moveTeleport(coin) end
touch(coin.part)
markCollected(coin)
end)
-- =========================================================== COIN MAGNET
-- Fires touch events on every coin inside the radius without moving. Much
-- less conspicuous than teleporting, and it stacks with normal play.
KH.loop(0.2, function()
if not S.Farm.Magnet then return end
local root = U.myRoot()
if not root then return end
local radius = S.Farm.MagnetRadius
for _, coin in ipairs(Game.coins()) do
if (coin.part.Position - root.Position).Magnitude <= radius then
if touch(coin.part) then markCollected(coin) end
end
end
end)
-- ========================================================= GUN GRABBING
local grabbing = false
-- The murderer cannot pick the gun up, so anything short of a confident
-- "not the murderer" counts as a no.
local function blockedReason(strict)
if Game.amMurderer() then return "You are the murderer — that gun is not yours to take." end
if Game.gunTool() then return "You already have a gun." end
if not U.isAliveChar(LocalPlayer) then return "You are not alive." end
-- Automatic grabs only: a button press is the user's call, but nothing
-- moves on its own while we cannot prove what we are.
if strict and not Game.selfKnown() then return "Roles are not known yet — staying put." end
return nil
end
function Farm.grabGun(announce, strict)
local drop = Game.GunDrop
if not drop or not drop.Parent then
if announce then
UI.notify({title = "Gun Drop", text = "No dropped gun right now.", kind = "warn"})
end
return false
end
if grabbing then return false end
local blocked = blockedReason(strict)
if blocked then
if announce then
UI.notify({title = "Gun Drop", text = blocked, kind = "warn"})
end
return false
end
grabbing = true
KH.detach(function()
-- pcall, or one error leaves the flag stuck on forever.
pcall(function()
local root = U.myRoot()
if not root then return end
local origin = root.CFrame
root.CFrame = CFrame.new(drop.Position + Vector3.new(0, 2.5, 0))
-- Wait for the pickup, rechecking we may stand here at all.
local began, bailed = os.clock(), false
while true do
task.wait(0.08)
if Game.gunTool() or not drop.Parent then break end
if os.clock() - began > 2.5 then break end
if U.myRoot() ~= root or blockedReason(false) then
bailed = true
break
end
end
-- Same body only: never drag a fresh spawn to the old spot.
if U.myRoot() == root and (bailed or S.Farm.GrabReturn) then
root.CFrame = origin
end
if not bailed and Game.gunTool() then
UI.notify({title = "Gun Drop", text = "Picked up the dropped gun.", kind = "good"})
end
end)
grabbing = false
end)
return true
end
Game.on("GunDropped", function(drop)
UI.notify({title = "Gun Dropped", text = "The sheriff went down — gun is on the floor.", kind = "warn", duration = 5})
if not S.Farm.AutoGrabGun then return end
if drop == triedDrop then return end
triedDrop = drop
-- Off the signal thread: a listener that yields stalls the rest.
KH.detach(function()
task.wait(0.6) -- let the part settle where it lands
-- The murderer's thrown knife is in flight right now, so empty
-- hands are not proof of innocence — wait for the real roles.
local deadline = os.clock() + 4
while not Game.selfKnown() and os.clock() < deadline do
if Game.amMurderer() then return end
task.wait(0.2)
end
if not S.Farm.AutoGrabGun then return end
if not drop.Parent or Game.GunDrop ~= drop then return end
Farm.grabGun(false, true)
end)
end)
Game.on("GunTaken", function()
local hero = Game.Hero and Game.Hero or nil
UI.notify({
title = "Gun Taken",
text = hero and (hero .. " picked up the gun.") or "Someone picked up the gun.",
duration = 4,
})
end)
-- ============================================================= ANTI-AFK
KH.track(LocalPlayer.Idled:Connect(function()
if not S.Farm.AntiAFK then return end
pcall(function()
VirtualUser:CaptureController()
VirtualUser:ClickButton2(Vector2.new())
end)
end))
end
-- ─── src/mm2/73_safety.lua ─────────────────────────────────────────
-- ============================================================================
-- SAFETY — murderer proximity warning, auto-dodge, round/role announcements
--
-- The half of an MM2 script that actually keeps you alive as an innocent,
-- which most feature lists skip entirely.
-- ============================================================================
do
local UI = KH.UI
local C = UI.C
local make = UI.make
local U = KH.U
local Game = KH.Game
local S = KH.S
local Safety = {}
KH.Safety = Safety
Safety.MurdererDistance = math.huge
-- ------------------------------------------------------------ vignette
-- Four edge bars, each fading inward, make a red pulse around the screen
-- border. No image assets, and it never obscures the middle of the view.
local vignette = make("Frame", {
Name = "Alert",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Visible = false,
ZIndex = 0,
Parent = UI.Overlay,
})
local bars = {}
local EDGES = {
{size = UDim2.new(1, 0, 0, 90), pos = UDim2.fromScale(0, 0), rot = 90},
{size = UDim2.new(1, 0, 0, 90), pos = UDim2.new(0, 0, 1, -90), rot = 270},
{size = UDim2.new(0, 120, 1, 0), pos = UDim2.fromScale(0, 0), rot = 0},
{size = UDim2.new(0, 120, 1, 0), pos = UDim2.new(1, -120, 0, 0), rot = 180},
}
for _, edge in ipairs(EDGES) do
local bar = make("Frame", {
Size = edge.size,
Position = edge.pos,
BackgroundColor3 = Color3.fromRGB(255, 40, 40),
BackgroundTransparency = 1,
BorderSizePixel = 0,
Parent = vignette,
})
make("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(1, 1),
}),
Rotation = edge.rot,
Parent = bar,
})
bars[#bars + 1] = bar
end
local function setAlertLevel(intensity)
local visible = intensity > 0.01
vignette.Visible = visible
if not visible then return end
for _, bar in ipairs(bars) do
bar.BackgroundTransparency = 1 - (0.55 * intensity)
end
end
-- ==================================================== PROXIMITY WARNING
local ALERT_SOUND = "rbxasset://sounds/electronicpingshort.wav"
local lastBeep, lastNotify, lastDodge = 0, 0, 0
local wasClose = false
KH.onFrame("proximity", function()
local murderer = Game.murdererPlayer()
local myRoot = U.myRoot()
if not murderer or not myRoot or murderer == KH.LocalPlayer then
Safety.MurdererDistance = math.huge
setAlertLevel(0)
wasClose = false
return
end
local part = U.rootOf(murderer)
if not part then
Safety.MurdererDistance = math.huge
setAlertLevel(0)
return
end
local distance = (myRoot.Position - part.Position).Magnitude
Safety.MurdererDistance = distance
-- --------------------------------------------------------- warning
if S.Safety.ProximityAlert then
local threshold = S.Safety.AlertDistance
if distance <= threshold then
-- Intensity ramps as they close in, so the screen tells you how
-- much trouble you are in without reading a number.
local intensity = 1 - (distance / threshold)
setAlertLevel(S.Safety.AlertFlash and intensity or 0)
local now = os.clock()
if not wasClose and now - lastNotify > 4 then
lastNotify = now
UI.notify({
title = "Murderer Nearby",
text = ("%s is %dm away."):format(murderer.DisplayName, math.floor(distance)),
kind = "bad",
duration = 3,
})
end
-- Beep faster the closer they get.
if S.Safety.AlertSound then
local interval = 0.18 + (distance / threshold) * 0.9
if now - lastBeep > interval then
lastBeep = now
U.playSound(ALERT_SOUND, 0.35)
end
end
wasClose = true
else
setAlertLevel(0)
wasClose = false
end
else
setAlertLevel(0)
end
-- ------------------------------------------------------ auto dodge
-- Only bails when we are not the one hunting: as murderer or an armed
-- sheriff, launching yourself into the air is worse than useless.
if S.Safety.AutoDodge
and distance <= S.Safety.DodgeDistance
and os.clock() - lastDodge > 1.5
and not Game.amMurderer()
and not Game.gunTool() then
local root = U.myRoot()
if root then
lastDodge = os.clock()
-- Straight up, away from the knife, keeping our facing intact.
root.CFrame = root.CFrame + Vector3.new(0, S.Safety.DodgeHeight, 0)
UI.notify({title = "Dodged", text = "Bailed out of knife range.", kind = "warn", duration = 2})
end
end
end, 40)
-- ================================================ ROLE ANNOUNCEMENTS
Game.on("RoleChange", function(murderer, sheriff)
if not S.Safety.RoleNotify then return end
if not murderer and not sheriff then return end
local me = KH.LocalPlayer.Name
if murderer == me then
UI.notify({title = "You are the MURDERER", text = "Knife tools are on the Knife tab.", kind = "bad", duration = 6})
elseif sheriff == me then
UI.notify({title = "You are the SHERIFF", text = "Aimbot is armed — check the Aimbot tab.", kind = "good", duration = 6})
end
local lines = {}
if murderer then lines[#lines + 1] = "Murderer: " .. murderer end
if sheriff then lines[#lines + 1] = "Sheriff: " .. sheriff end
if #lines > 0 then
UI.notify({
title = "Roles Revealed",
text = table.concat(lines, "\n"),
kind = "warn",
duration = 7,
})
end
end)
Game.on("RoundStart", function()
if S.Safety.RoleNotify then
UI.notify({title = "Round Started", text = "Waiting for roles…", duration = 3})
end
end)
Game.on("TrapPlaced", function()
if S.ESP.TrapESP then
UI.notify({title = "Trap Placed", text = "The murderer set a trap.", kind = "warn", duration = 4})
end
end)
end
-- ─── src/mm2/74_teleports.lua ──────────────────────────────────────
-- ============================================================================
-- TELEPORTS — the MM2-specific destinations, on top of shared movement
-- ============================================================================
do
local UI = KH.UI
local Game = KH.Game
local Move = KH.Move
local tpTo, tpToPlayer = Move.tpTo, Move.tpToPlayer
function Move.tpMurderer() return tpToPlayer(Game.murdererPlayer(), "Moved to the murderer.") end
function Move.tpSheriff() return tpToPlayer(Game.sheriffPlayer(), "Moved to the sheriff.") end
function Move.tpGunDrop()
local drop = Game.GunDrop
if not drop or not drop.Parent then
UI.notify({title = "Teleport", text = "No dropped gun right now.", kind = "warn"})
return false
end
return tpTo(drop.Position, "Moved to the dropped gun.")
end
function Move.tpCoin()
local coin = Game.nearestCoin()
if not coin then
UI.notify({title = "Teleport", text = "No coins found.", kind = "warn"})
return false
end
return tpTo(coin.part.Position, "Moved to the nearest coin.")
end
local function anySpawn(container)
local spawns = container and container:FindFirstChild("Spawns")
if not spawns then return nil end
local options = spawns:GetChildren()
if #options == 0 then return nil end
local pick = options[math.random(1, #options)]
return pick:IsA("BasePart") and pick.Position or nil
end
function Move.tpLobby()
local position = anySpawn(Game.lobby())
if not position then
UI.notify({title = "Teleport", text = "Could not find the lobby.", kind = "warn"})
return false
end
return tpTo(position, "Moved to the lobby.")
end
function Move.tpRandomSpawn()
local position = anySpawn(Game.map())
if not position then
UI.notify({title = "Teleport", text = "No map spawns available.", kind = "warn"})
return false
end
return tpTo(position, "Moved to a random spawn.")
end
end
-- ─── src/mm2/90_menu.lua ───────────────────────────────────────────
-- ============================================================================
-- MENU — every tab, section and control
-- ============================================================================
do
local UI = KH.UI
local C = UI.C
local make = UI.make
local U = KH.U
local S = KH.S
local Game = KH.Game
local Combat = KH.Combat
local Farm = KH.Farm
local Move = KH.Move
local Safety = KH.Safety
local Players = KH.Services.Players
local LocalPlayer = KH.LocalPlayer
-- Binds a control straight to a settings field, with an optional side
-- effect to run after the value lands.
local function opt(group, key, extra)
local t = extra or {}
local after = t.onSet
t.onSet = nil
t.get = function() return S[group][key] end
t.set = function(value)
S[group][key] = value
if after then after(value) end
end
return t
end
-- ========================================================== AIMBOT TAB
do
local tab = UI.addTab("Aimbot")
local targeting = UI.section(tab, "Targeting")
UI.label(targeting, "Draws your gun, then shoots the murderer wherever they are — through walls, across the map, no mouse movement. MM2's shot is a world position the server resolves, so nothing needs to be on screen.")
UI.toggle(targeting, opt("Aim", "Enabled", {
text = "Aimbot Enabled",
desc = "Master switch for automatic firing.",
}))
UI.dropdown(targeting, opt("Aim", "Target", {
text = "Target",
desc = "Nearest and Crosshair will shoot innocents too.",
options = {"Murderer", "Nearest", "Crosshair"},
}))
UI.dropdown(targeting, opt("Aim", "Mode", {
text = "Trigger",
desc = "Press: one lock-and-shot per key press — it aims, fires once, and gives the camera straight back. Hold: keeps firing while the key is down. Toggle: press once to stay locked on, again to stop. Always: never stops.",
options = {"Press", "Hold", "Toggle", "Always"},
}))
UI.keybind(targeting, opt("Aim", "Key", {text = "Aim Key"}))
UI.dropdown(targeting, opt("Aim", "Method", {
text = "Fire Method",
desc = "Mouse (default) turns the camera onto the target, puts the cursor on them and clicks, so MM2's own gun fires the shot — the one that works without a hook. Remote hands the server the position instead: silent, shoots through walls, and does nothing at all on an executor the game does not accept built shots from.",
options = {"Remote", "Mouse"},
}))
UI.toggle(targeting, opt("Aim", "KeepEquipped", {
text = "Keep Gun Equipped",
desc = "Re-draw the gun if a respawn or round change stows it.",
}))
local accuracy = UI.section(tab, "Accuracy")
UI.slider(accuracy, opt("Aim", "Prediction", {
text = "Prediction",
desc = "Studs of lead, Remote method only. Mouse aim points at where the target actually is — leading a client raycast just walks it off their body.",
min = 0, max = 8, step = 0.1,
}))
UI.toggle(accuracy, opt("Aim", "PingComp", {
text = "Ping Compensation",
desc = "Scale the lead by your measured ping.",
}))
UI.toggle(accuracy, opt("Aim", "AimAtHead", {
text = "Aim At Head",
desc = "Target the head instead of the torso.",
}))
UI.slider(accuracy, opt("Aim", "FireRate", {
text = "Fire Rate", min = 0.05, max = 1, step = 0.05, suffix = "s",
}))
local mouseAim = UI.section(tab, "Mouse Aim")
UI.label(mouseAim, "Only used when Fire Method is Mouse. Your camera snaps onto the target and the cursor is driven there, so this one is visible — to you and to anyone watching you.")
UI.toggle(mouseAim, opt("Aim", "CameraSnap", {
text = "Turn Camera",
desc = "Turn towards the target when they are near the edge of your view or behind you, so they do not have to be on screen already. Anyone already well inside the view is left to the cursor alone and the camera stays still. Off, the aimbot can only shoot what you can see.",
}))
UI.slider(mouseAim, opt("Aim", "MouseSpeed", {
text = "Aim Speed",
desc = "How fast the camera turns onto the target. 1 snaps in one frame; lower eases the turn, which looks far smoother and is the only part of this anyone else can see. The cursor itself always goes straight there.",
min = 0.1, max = 1, step = 0.05,
}))
UI.readout(mouseAim, {
text = "Mouse Control",
get = function() return Combat.Mouse.support() end,
})
UI.toggle(mouseAim, opt("Aim", "DryRun", {
text = "Aim Test (no shooting)",
desc = "Aims at the nearest player and reports how many studs the cursor landed from them, without firing and without needing a gun. For checking the aim without waiting to roll sheriff.",
}))
UI.readout(mouseAim, {
text = "Last Result",
get = function() return Combat.LastResult or "nothing yet" end,
})
local extras = UI.section(tab, "Extras")
UI.toggle(extras, opt("Aim", "SilentAim", {
text = "Silent Aim",
desc = Combat.SilentDesc,
}))
UI.dropdown(extras, opt("Aim", "SilentMode", {
text = "Silent Aim Route",
desc = "Auto picks the best one your executor can do. Takeover needs no hook at all — switch to it by hand if Auto's hook reports success but your shots still miss.",
options = {"Auto", "Hook", "Takeover", "Click"},
}))
UI.readout(extras, {
text = "Route In Use",
get = function() return Combat.silentStatus() end,
})
UI.toggle(extras, opt("Aim", "NotifyShot", {text = "Notify On Shot"}))
UI.readout(extras, {
text = "Aimbot Status",
get = function() return Combat.aimStatus() end,
})
UI.readout(extras, {
text = "Current Target",
get = function()
local target = Combat.LastTarget
if target and target.Parent then return target.DisplayName end
return "none"
end,
})
UI.button(extras, {
text = "Kill All",
desc = "Fire at every living player in sequence.",
kind = "danger",
callback = function() Combat.killAll() end,
})
end
-- =========================================================== KNIFE TAB
do
local tab = UI.addTab("Knife")
UI.label(UI.section(tab, "Murderer Only"),
"These do nothing unless you are holding the knife.")
local throwing = UI.section(tab, "Throwing")
UI.toggle(throwing, opt("Knife", "AutoThrow", {
text = "Auto Throw Enabled",
desc = "Master switch for automatic throwing. The throw key does nothing while this is off.",
}))
UI.dropdown(throwing, opt("Knife", "ThrowMode", {
text = "Trigger",
desc = "Press: one throw per key press, and nothing at all in between. Always: throws on the delay timer below and ignores the key.",
options = {"Press", "Always"},
onSet = function() UI.refreshKeybinds() end,
}))
UI.keybind(throwing, opt("Knife", "ThrowKey", {
text = "Throw Key",
desc = "Press mode only. One press, one throw, at the sheriff if one is in range. Draws the knife first when it is not already out and then leaves it out — after that the tool is yours again.",
}))
UI.slider(throwing, opt("Knife", "ThrowDelay", {
text = "Throw Delay",
desc = "Seconds between throws in Always mode. Press mode does not use it.",
min = 0.2, max = 5, step = 0.1, suffix = "s",
}))
UI.slider(throwing, opt("Knife", "ThrowRange", {
text = "Throw Range",
desc = "Do not throw at anyone further away than this. The knife is gone until it comes back, so a throw that cannot reach costs you the weapon for nothing.",
min = 20, max = 200, step = 5, suffix = " studs",
}))
UI.toggle(throwing, opt("Knife", "ThrowAnim", {
text = "Throw Animation",
desc = "Play MM2's own wind-up, and hold the knife until the arm has actually swung. Without it the knife leaves a character that never moved, which is what a scripted throw looks like to everyone watching.",
}))
UI.slider(throwing, opt("Knife", "ThrowRelease", {
text = "Release Point",
desc = "How far into the wind-up the knife leaves your hand, as a share of the animation. Lower throws sooner and looks more scripted; higher looks right but costs you that long before the knife is in the air. Capped at three quarters of a second whatever this says.",
min = 0, max = 100, step = 5, suffix = "%",
}))
UI.button(throwing, {
text = "Throw Now",
desc = "Throws once whatever the switch above is set to.",
callback = function() Combat.throwOnce(true) end,
})
local melee = UI.section(tab, "Melee")
UI.toggle(melee, opt("Knife", "Aura", {
text = "Knife Aura",
desc = "Stab anyone who walks into range.",
}))
UI.slider(melee, opt("Knife", "AuraRadius", {
text = "Aura Radius", min = 5, max = 40, step = 1, suffix = " studs",
}))
UI.slider(melee, opt("Knife", "AuraDelay", {
text = "Aura Delay", min = 0.05, max = 1, step = 0.05, suffix = "s",
}))
UI.toggle(melee, opt("Knife", "TpStab", {
text = "Teleport Stab",
desc = "Blink to the target, swing, blink back. Anyone already within swinging distance is stabbed where they stand.",
}))
UI.slider(melee, opt("Knife", "TpRange", {
text = "Teleport Range",
desc = "How far a blink is worth making. Crossing the map to reach someone is not a stab, it is a teleport everyone sees.",
min = 20, max = 400, step = 10, suffix = " studs",
}))
local targets = UI.section(tab, "Target Filter")
UI.toggle(targets, opt("Knife", "TargetSheriff", {
text = "Prioritise Sheriff",
desc = "Go for whoever is holding the gun first, then everyone else by distance.",
}))
UI.toggle(targets, opt("Knife", "SkipSheriff", {
text = "Never Target Sheriff",
desc = "Avoid the gun holder entirely.",
}))
end
-- ============================================================= ESP TAB
do
local tab = UI.addTab("ESP")
local players = UI.section(tab, "Players")
UI.toggle(players, opt("ESP", "Enabled", {text = "ESP Enabled"}))
UI.toggle(players, opt("ESP", "Boxes", {text = "Boxes"}))
UI.dropdown(players, opt("ESP", "BoxStyle", {
text = "Box Style", options = {"Corner", "Full"},
}))
UI.toggle(players, opt("ESP", "Names", {text = "Names"}))
UI.toggle(players, opt("ESP", "Roles", {text = "Role Labels"}))
UI.toggle(players, opt("ESP", "Distance", {text = "Show Distance"}))
UI.toggle(players, opt("ESP", "Health", {text = "Health Bars"}))
UI.toggle(players, opt("ESP", "Tracers", {text = "Tracers"}))
UI.dropdown(players, opt("ESP", "TracerOrigin", {
text = "Tracer Origin", options = {"Bottom", "Center"},
}))
UI.toggle(players, opt("ESP", "Chams", {
text = "Chams", desc = "Fill characters with their role colour.",
}))
UI.slider(players, opt("ESP", "ChamsFill", {
text = "Cham Opacity", min = 0, max = 0.95, step = 0.05,
}))
UI.toggle(players, opt("ESP", "OffScreen", {
text = "Off-Screen Arrows",
desc = "Edge markers pointing at players behind you.",
}))
UI.toggle(players, opt("ESP", "OnlyRoles", {
text = "Hide Innocents",
desc = "Only draw the murderer, sheriff and hero.",
}))
UI.slider(players, opt("ESP", "MaxDistance", {
text = "Max Distance", min = 100, max = 5000, step = 50, suffix = " studs",
}))
UI.slider(players, opt("ESP", "TextSize", {
text = "Text Size", min = 8, max = 22, step = 1,
}))
local world = UI.section(tab, "World")
UI.toggle(world, opt("ESP", "CoinESP", {text = "Coin ESP"}))
UI.toggle(world, opt("ESP", "GunESP", {
text = "Gun Drop ESP", desc = "Highlight the gun when a sheriff dies.",
}))
UI.toggle(world, opt("ESP", "TrapESP", {
text = "Trap ESP", desc = "Reveal the murderer's traps.",
}))
local colours = UI.section(tab, "Colours")
UI.colorpicker(colours, opt("ESP", "ColorMurderer", {text = "Murderer"}))
UI.colorpicker(colours, opt("ESP", "ColorSheriff", {text = "Sheriff"}))
UI.colorpicker(colours, opt("ESP", "ColorHero", {text = "Hero"}))
UI.colorpicker(colours, opt("ESP", "ColorInnocent", {text = "Innocent"}))
end
-- ============================================================ FARM TAB
do
local tab = UI.addTab("Farm")
local coins = UI.section(tab, "Coins")
UI.toggle(coins, opt("Farm", "CoinFarm", {
text = "Auto Coin Farm",
desc = "Travel to every coin on the map and collect it.",
}))
UI.dropdown(coins, opt("Farm", "CoinMode", {
text = "Travel Mode",
desc = "Teleport is fastest; Walk is the least obvious.",
options = {"Teleport", "Smooth", "Walk"},
}))
UI.slider(coins, opt("Farm", "CoinSpeed", {
text = "Smooth Speed", min = 20, max = 300, step = 10, suffix = " s/s",
}))
UI.slider(coins, opt("Farm", "CoinDelay", {
text = "Teleport Delay", min = 0, max = 1, step = 0.02, suffix = "s",
}))
UI.toggle(coins, opt("Farm", "LobbyFarm", {
text = "Include Lobby",
desc = "Also collect the coins in the lobby between rounds.",
}))
local magnet = UI.section(tab, "Coin Magnet")
UI.label(magnet, "Collects nearby coins without moving you. Slower, but it stacks with playing normally.")
UI.toggle(magnet, opt("Farm", "Magnet", {text = "Coin Magnet"}))
UI.slider(magnet, opt("Farm", "MagnetRadius", {
text = "Magnet Radius", min = 5, max = 120, step = 5, suffix = " studs",
}))
local gun = UI.section(tab, "Dropped Gun")
UI.toggle(gun, opt("Farm", "AutoGrabGun", {
text = "Auto Grab Gun",
desc = "Rush the gun the moment a sheriff dies.",
}))
UI.toggle(gun, opt("Farm", "GrabReturn", {
text = "Return After Grab",
desc = "Snap back to where you were standing.",
}))
UI.button(gun, {text = "Grab Gun Now", callback = function() Farm.grabGun(true) end})
local session = UI.section(tab, "Session")
UI.toggle(session, opt("Farm", "AntiAFK", {
text = "Anti-AFK", desc = "Block the twenty-minute idle kick.",
}))
UI.readout(session, {text = "Coins Collected", get = function() return Farm.Collected end})
UI.readout(session, {text = "Shots Fired", get = function() return Game.ShotsFired end})
end
-- ========================================================== SAFETY TAB
do
local tab = UI.addTab("Safety")
local warning = UI.section(tab, "Murderer Warning")
UI.label(warning, "The half of an MM2 script that keeps you alive when you are just an innocent.")
UI.toggle(warning, opt("Safety", "ProximityAlert", {
text = "Proximity Alert",
desc = "Warn when the murderer closes in.",
}))
UI.slider(warning, opt("Safety", "AlertDistance", {
text = "Alert Distance", min = 10, max = 150, step = 5, suffix = " studs",
}))
UI.toggle(warning, opt("Safety", "AlertSound", {
text = "Alert Beep", desc = "Beeps faster the closer they get.",
}))
UI.toggle(warning, opt("Safety", "AlertFlash", {
text = "Screen Edge Flash",
desc = "Red vignette that intensifies with proximity.",
}))
UI.readout(warning, {
text = "Murderer Distance",
get = function()
local distance = Safety.MurdererDistance
if distance == math.huge then return "unknown" end
return ("%dm"):format(math.floor(distance))
end,
})
local dodge = UI.section(tab, "Auto Dodge")
UI.toggle(dodge, opt("Safety", "AutoDodge", {
text = "Auto Dodge",
desc = "Launch upward if the knife gets too close. Off while armed.",
}))
UI.slider(dodge, opt("Safety", "DodgeDistance", {
text = "Trigger Distance", min = 5, max = 40, step = 1, suffix = " studs",
}))
UI.slider(dodge, opt("Safety", "DodgeHeight", {
text = "Dodge Height", min = 10, max = 200, step = 5, suffix = " studs",
}))
local announce = UI.section(tab, "Announcements")
UI.toggle(announce, opt("Safety", "RoleNotify", {
text = "Role Reveal",
desc = "Name the murderer and sheriff the moment roles are dealt.",
}))
end
-- ======================================================== MOVEMENT TAB
do
local tab = UI.addTab("Movement")
local speed = UI.section(tab, "Speed & Jump")
UI.toggle(speed, opt("Move", "SpeedEnabled", {
text = "Speed Hack", onSet = function() Move.applyHumanoid() end,
}))
UI.slider(speed, opt("Move", "Speed", {
text = "Walk Speed", min = 16, max = 250, step = 1,
onSet = function() Move.applyHumanoid() end,
}))
UI.dropdown(speed, opt("Move", "SpeedMode", {
text = "Speed Mode",
desc = "CFrame ignores server speed clamps but looks less natural.",
options = {"Humanoid", "CFrame"},
onSet = function() Move.applyHumanoid() end,
}))
UI.toggle(speed, opt("Move", "JumpEnabled", {
text = "Jump Hack", onSet = function() Move.applyHumanoid() end,
}))
UI.slider(speed, opt("Move", "Jump", {
text = "Jump Power", min = 50, max = 400, step = 5,
onSet = function() Move.applyHumanoid() end,
}))
UI.toggle(speed, opt("Move", "InfJump", {text = "Infinite Jump"}))
UI.toggle(speed, opt("Move", "Bhop", {
text = "Bunny Hop", desc = "Auto-jump while holding space.",
}))
local clip = UI.section(tab, "Noclip & Fly")
UI.toggle(clip, opt("Move", "Noclip", {
text = "Noclip", onSet = function(v) Move.setNoclip(v) end,
}))
UI.keybind(clip, opt("Move", "NoclipKey", {text = "Noclip Key"}))
UI.toggle(clip, opt("Move", "Fly", {
text = "Fly",
desc = "WASD to move, Space up, Left Shift down.",
onSet = function(v) Move.setFly(v) end,
}))
UI.keybind(clip, opt("Move", "FlyKey", {text = "Fly Key"}))
UI.slider(clip, opt("Move", "FlySpeed", {
text = "Fly Speed", min = 10, max = 300, step = 5,
}))
UI.toggle(clip, opt("Move", "Spinbot", {
text = "Spinbot", desc = "Constantly rotate your character.",
}))
UI.toggle(clip, opt("Move", "SafeLand", {
text = "Safe Landing",
desc = "Fly down instead of dropping when fly is switched off in the air, and slow any long fall before it lands.",
}))
local teleport = UI.section(tab, "Teleport")
UI.button(teleport, {text = "To Murderer", callback = function() Move.tpMurderer() end})
UI.button(teleport, {text = "To Sheriff", callback = function() Move.tpSheriff() end})
UI.button(teleport, {text = "To Dropped Gun", callback = function() Move.tpGunDrop() end})
UI.button(teleport, {text = "To Nearest Coin", callback = function() Move.tpCoin() end})
UI.button(teleport, {text = "To Lobby", callback = function() Move.tpLobby() end})
UI.button(teleport, {text = "To Random Spawn", callback = function() Move.tpRandomSpawn() end})
local waypoints = UI.section(tab, "Waypoints")
local pendingName = {value = ""}
local selected = {value = ""}
local waypointDropdown
local function refreshWaypoints()
local names = Move.waypointNames()
if #names == 0 then names = {"(none saved)"} end
if waypointDropdown then waypointDropdown.setOptions(names) end
end
UI.input(waypoints, {
text = "Waypoint Name",
placeholder = "e.g. roof camp",
get = function() return pendingName.value end,
set = function(v) pendingName.value = v end,
})
UI.button(waypoints, {
text = "Save Current Position",
callback = function()
if Move.saveWaypoint(pendingName.value) then refreshWaypoints() end
end,
})
waypointDropdown = UI.dropdown(waypoints, {
text = "Saved Waypoints",
options = {"(none saved)"},
get = function() return selected.value ~= "" and selected.value or "(none saved)" end,
set = function(v) selected.value = v end,
})
UI.button(waypoints, {
text = "Teleport To Waypoint",
callback = function() Move.gotoWaypoint(selected.value) end,
})
UI.button(waypoints, {
text = "Delete Waypoint",
kind = "danger",
callback = function()
if Move.deleteWaypoint(selected.value) then
selected.value = ""
refreshWaypoints()
end
end,
})
refreshWaypoints()
end
-- ========================================================= VISUALS TAB
do
local tab = UI.addTab("Visuals")
local lighting = UI.section(tab, "Lighting")
UI.toggle(lighting, opt("Visual", "Fullbright", {
text = "Fullbright", desc = "Flatten the lighting so nowhere is dark.",
}))
UI.slider(lighting, opt("Visual", "Brightness", {
text = "Brightness", min = 0.5, max = 6, step = 0.1,
}))
UI.toggle(lighting, opt("Visual", "NoFog", {text = "Remove Fog"}))
local camera = UI.section(tab, "Camera")
UI.toggle(camera, opt("Visual", "FovEnabled", {text = "Custom Field Of View"}))
UI.slider(camera, opt("Visual", "Fov", {
text = "Field Of View", min = 40, max = 120, step = 1, suffix = "°",
}))
local world = UI.section(tab, "World")
UI.toggle(world, opt("Visual", "Xray", {
text = "X-Ray Walls",
desc = "Make the map semi-transparent. Players stay solid.",
}))
UI.slider(world, opt("Visual", "XrayTransp", {
text = "Wall Transparency", min = 0.2, max = 0.95, step = 0.05,
}))
UI.toggle(world, opt("Visual", "LowDetail", {
text = "Low Detail Mode",
desc = "Disable particles, trails and shadows for more frames.",
}))
end
-- ========================================================= PLAYERS TAB
do
local tab = UI.addTab("Players")
local section = UI.section(tab, "In This Server")
UI.label(section, "Live roster with roles. Click a name to spectate, or use the arrow to teleport.")
local listHolder = make("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
Parent = section.body,
})
UI.list(listHolder, 4)
local spectating = nil
local function setSpectate(player)
local cam = KH.camera()
if spectating == player or player == nil then
spectating = nil
local hum = U.myHum()
if hum then cam.CameraSubject = hum end
UI.notify({title = "Spectate", text = "Back to your own view.", duration = 2})
else
local hum = U.humOf(player)
if not hum then
UI.notify({title = "Spectate", text = "No character to watch.", kind = "warn"})
return
end
spectating = player
cam.CameraSubject = hum
UI.notify({title = "Spectate", text = "Watching " .. player.DisplayName .. ".", duration = 2})
end
end
KH.undo(function() setSpectate(nil) end)
-- Roblox hands the camera back on any respawn, theirs or yours, which
-- used to end the spectate silently and leave the toggle out of step.
KH.loop(0.4, function()
if not spectating then return end
if not spectating.Parent then
spectating = nil
return
end
local hum = U.humOf(spectating)
if not hum then return end -- mid-respawn; wait for the new one
local cam = KH.camera()
if cam.CameraSubject ~= hum then cam.CameraSubject = hum end
end)
local rows = {}
local function buildRow(player)
local row = make("Frame", {
Size = UDim2.new(1, 0, 0, 34),
BackgroundColor3 = C.Row,
BackgroundTransparency = 0.55,
BorderSizePixel = 0,
Parent = listHolder,
})
UI.corner(row, 7)
local dot = make("Frame", {
Position = UDim2.fromOffset(10, 13),
Size = UDim2.fromOffset(8, 8),
BackgroundColor3 = C.TextDim,
BorderSizePixel = 0,
Parent = row,
})
UI.corner(dot, 4)
local name = make("TextButton", {
Text = player.DisplayName,
Font = Enum.Font.GothamMedium,
TextSize = 12,
TextColor3 = C.Text,
BackgroundTransparency = 1,
AutoButtonColor = false,
Position = UDim2.fromOffset(26, 0),
Size = UDim2.new(1, -140, 1, 0),
TextXAlignment = Enum.TextXAlignment.Left,
TextTruncate = Enum.TextTruncate.AtEnd,
Parent = row,
})
name.MouseButton1Click:Connect(function() setSpectate(player) end)
local info = make("TextLabel", {
Text = "",
Font = Enum.Font.Gotham,
TextSize = 11,
TextColor3 = C.TextDim,
BackgroundTransparency = 1,
RichText = true,
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -40, 0.5, 0),
Size = UDim2.fromOffset(120, 20),
TextXAlignment = Enum.TextXAlignment.Right,
Parent = row,
})
local tp = make("TextButton", {
Text = "→",
Font = Enum.Font.GothamBold,
TextSize = 15,
TextColor3 = C.TextDim,
BackgroundColor3 = C.Card,
AutoButtonColor = false,
BorderSizePixel = 0,
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -8, 0.5, 0),
Size = UDim2.fromOffset(26, 22),
Parent = row,
})
UI.corner(tp, 6)
tp.MouseButton1Click:Connect(function() Move.tpToPlayer(player) end)
tp.MouseEnter:Connect(function() tp.TextColor3 = C.Text end)
tp.MouseLeave:Connect(function() tp.TextColor3 = C.TextDim end)
return {row = row, dot = dot, name = name, info = info, player = player}
end
local function refreshList()
local present = {}
for _, player in ipairs(Players:GetPlayers()) do
present[player] = true
if not rows[player] then rows[player] = buildRow(player) end
end
for player, entry in pairs(rows) do
if not present[player] then
pcall(function() entry.row:Destroy() end)
rows[player] = nil
end
end
local myRoot = U.myRoot()
for player, entry in pairs(rows) do
local color, role = Game.colorOf(player)
entry.dot.BackgroundColor3 = color
local bits = {}
if player == LocalPlayer then
bits[#bits + 1] = "you"
elseif myRoot then
local part = U.rootOf(player)
if part then
bits[#bits + 1] = ("%dm"):format(
math.floor((myRoot.Position - part.Position).Magnitude))
end
end
if not Game.isAlive(player) then bits[#bits + 1] = "dead" end
entry.info.Text = ('<font color="#%s">%s</font> %s')
:format(color:ToHex(), role, table.concat(bits, " · "))
entry.name.TextColor3 = (spectating == player) and color or C.Text
end
end
KH.loop(0.6, function()
if UI.IsOpen and UI.ActiveTab == "Players" then refreshList() end
end)
refreshList()
end
-- Extra rows for the shared Settings tab, which is built after this file.
KH.SessionInfo = {
{text = "Silent Aim Support", get = function() return Combat.silentStatus() end},
{text = "Mouse Control", get = function() return Combat.Mouse.support() end},
{text = "Metamethod Hook", get = function()
if Combat.HookAvailable then return tostring(Combat.SilentRoute) end
return "none — " .. (Combat.SilentReason or "not exposed")
end},
}
KH.FirstTab = "Aimbot"
end
-- ─── src/_shared/91_settings.lua ───────────────────────────────────
-- ============================================================================
-- SETTINGS — the tab every build has: interface, profiles, session
--
-- Added after the game module's own tabs so it is always last, and it picks up
-- whatever extra rows that module left in KH.SessionInfo.
-- ============================================================================
do
local UI = KH.UI
local S = KH.S
local Config = KH.Config
local function opt(group, key, extra)
local t = extra or {}
local after = t.onSet
t.onSet = nil
t.get = function() return S[group][key] end
t.set = function(value)
S[group][key] = value
if after then after(value) end
end
return t
end
local tab = UI.addTab("Settings")
local interface = UI.section(tab, "Interface")
UI.keybind(interface, opt("UI", "MenuKey", {text = "Menu Key"}))
UI.colorpicker(interface, opt("UI", "Accent", {
text = "Accent Colour",
onSet = function(color) UI.applyAccent(color) end,
}))
UI.toggle(interface, opt("UI", "Watermark", {
text = "Watermark",
onSet = function(v) UI.Watermark.Visible = v end,
}))
UI.toggle(interface, opt("UI", "KeybindList", {
text = "Keybind List",
onSet = function(v) UI.KeybindPanel.Visible = v end,
}))
UI.toggle(interface, opt("UI", "Notifications", {text = "Notifications"}))
local configs = UI.section(tab, "Configuration")
if not Config.available then
UI.label(configs, "This executor exposes no file API, so settings only persist until you close Roblox.")
else
UI.label(configs, "Profiles are stored in the KittyHub/configs folder next to your executor.")
end
UI.toggle(configs, opt("UI", "AutoSave", {
text = "Auto Save",
desc = "Write the active profile whenever something changes.",
}))
local profileName = {value = S.UI.Profile}
local profileDropdown
local function refreshProfiles()
local names = Config.list()
if #names == 0 then names = {"default"} end
if profileDropdown then profileDropdown.setOptions(names) end
end
UI.input(configs, {
text = "Profile Name",
placeholder = "default",
get = function() return profileName.value end,
set = function(v) profileName.value = v end,
})
UI.button(configs, {
text = "Save Profile",
kind = "primary",
callback = function()
local ok, err = Config.save(profileName.value)
if ok then
S.UI.Profile = profileName.value
refreshProfiles()
UI.notify({title = "Config", text = 'Saved "' .. profileName.value .. '".', kind = "good"})
else
UI.notify({title = "Config", text = tostring(err), kind = "bad"})
end
end,
})
profileDropdown = UI.dropdown(configs, {
text = "Saved Profiles",
options = {"default"},
get = function() return profileName.value end,
set = function(v) profileName.value = v end,
})
UI.button(configs, {
text = "Load Profile",
callback = function()
local ok, err = Config.load(profileName.value)
if ok then
UI.refreshAll()
UI.applyAccent(S.UI.Accent)
UI.notify({title = "Config", text = 'Loaded "' .. profileName.value .. '".', kind = "good"})
else
UI.notify({title = "Config", text = tostring(err), kind = "bad"})
end
end,
})
UI.button(configs, {
text = "Delete Profile",
kind = "danger",
callback = function()
Config.delete(profileName.value)
refreshProfiles()
UI.notify({title = "Config", text = "Deleted."})
end,
})
UI.button(configs, {
text = "Reset To Defaults",
kind = "danger",
callback = function()
Config.reset()
UI.refreshAll()
UI.applyAccent(S.UI.Accent)
UI.notify({title = "Config", text = "Settings reset.", kind = "warn"})
end,
})
refreshProfiles()
local session = UI.section(tab, "Session")
UI.readout(session, {text = "Executor", get = function() return KH.X.name end})
for _, row in ipairs(KH.SessionInfo or {}) do
UI.readout(session, row)
end
UI.readout(session, {
text = "Teleport Queue",
get = function() return KH.X.queueteleport and "supported" or "not supported" end,
})
-- Only the games this build is not already in.
local elsewhere = {}
for _, entry in ipairs(KH.Games) do
if entry.place ~= game.PlaceId then elsewhere[#elsewhere + 1] = entry end
end
if #elsewhere > 0 then
local going = {name = elsewhere[1].name}
local names = {}
for _, entry in ipairs(elsewhere) do names[#names + 1] = entry.name end
UI.dropdown(session, {
text = "Switch Game",
options = names,
get = function() return going.name end,
set = function(v) going.name = v end,
})
UI.button(session, {
text = "Go There",
desc = "Queues the hub before teleporting, so it loads itself on arrival — for when your executor will not attach to that game.",
callback = function()
for _, entry in ipairs(elsewhere) do
if entry.name == going.name then KH.goToGame(entry.place) end
end
end,
})
end
UI.button(session, {
text = "Rejoin Server",
callback = function() KH.rejoin() end,
})
UI.button(session, {
text = "Server Hop",
desc = "Find a different public server for this place.",
callback = function() KH.serverHop() end,
})
UI.button(session, {
text = "Unload Kitty Hub",
kind = "danger",
desc = "Remove the menu and undo every change.",
callback = function() KH.unload() end,
})
UI.selectTab(KH.FirstTab or (next(UI.Tabs)))
end
-- ─── src/mm2/92_main.lua ───────────────────────────────────────────
-- ============================================================================
-- MAIN — hotkeys, HUD readouts, and boot
-- ============================================================================
do
local UI = KH.UI
local C = UI.C
local U = KH.U
local S = KH.S
local Game = KH.Game
local Combat = KH.Combat
local Move = KH.Move
local Safety = KH.Safety
local Config = KH.Config
local UserInputService = KH.Services.UserInputService
-- =============================================================== HOTKEYS
UI.registerKeybind("Menu", function() return S.UI.MenuKey end, function() return UI.IsOpen end)
-- Press mode is never "engaged", so light the chip while its shot is in
-- flight instead — otherwise the key would look dead every time it works.
UI.registerKeybind("Aimbot", function() return S.Aim.Key end, function()
return Combat.isEngaged() or Combat.Mouse.active() ~= nil
end)
UI.registerKeybind("Noclip", function() return S.Move.NoclipKey end, function() return S.Move.Noclip end)
UI.registerKeybind("Fly", function() return S.Move.FlyKey end, function() return S.Move.Fly end)
-- Lit when the key is actually armed: the throw itself is over before the
-- panel would be redrawn, so what the chip is worth showing is whether the
-- press will do anything at all.
UI.registerKeybind("Throw Knife", function() return S.Knife.ThrowKey end, function()
return S.Knife.AutoThrow and S.Knife.ThrowMode == "Press"
end)
UI.refreshKeybinds()
KH.track(UserInputService.InputBegan:Connect(function(input, processed)
if input.UserInputType ~= Enum.UserInputType.Keyboard then return end
-- A keybind picker or a focused text box owns this press.
if UI.Capturing then return end
if processed then return end
local key = input.KeyCode
if key == U.keyCode(S.UI.MenuKey) then
UI.toggleOpen()
return
end
if key == U.keyCode(S.Move.NoclipKey) then
Move.setNoclip(not S.Move.Noclip)
UI.refreshAll()
return
end
if key == U.keyCode(S.Move.FlyKey) then
Move.setFly(not S.Move.Fly)
UI.refreshAll()
return
end
-- Armed by the Auto Throw switch, the same way the aim key is armed by
-- Aimbot Enabled. One throw per press: InputBegan does not auto-repeat,
-- so holding the key throws once and then nothing until it is released
-- and pressed again. Always mode leaves the key alone — the loop has it.
if key == U.keyCode(S.Knife.ThrowKey) and S.Knife.AutoThrow then
if S.Knife.ThrowMode == "Press" then Combat.throwOnce(true) end
return
end
-- In Toggle mode the aim key arms the aimbot rather than firing once.
if key == U.keyCode(S.Aim.Key) and S.Aim.Enabled then
if S.Aim.Mode == "Toggle" then
Combat.setToggle(not Combat.toggleArmedState())
UI.refreshKeybinds()
elseif S.Aim.Mode == "Press" or S.Aim.Mode == "Hold" then
-- Both shoot on the press. Hold then keeps going from the
-- render loop while the key is down; Press is done here.
Combat.fireOnce(true)
end
end
end))
-- ================================================================= HUD
local frames, fpsAccum, fps = 0, 0, 0
KH.onFrame("fps", function(delta)
frames = frames + 1
fpsAccum = fpsAccum + delta
if fpsAccum >= 0.5 then
fps = math.floor(frames / fpsAccum + 0.5)
frames, fpsAccum = 0, 0
end
end, 90)
local function roleColorHex()
local role = Game.myRole()
if role == "Murderer" then return S.ESP.ColorMurderer:ToHex(), role end
if role == "Sheriff" then return S.ESP.ColorSheriff:ToHex(), role end
if role == "Hero" then return S.ESP.ColorHero:ToHex(), role end
return S.ESP.ColorInnocent:ToHex(), role
end
local function clockText()
local seconds = Game.roundTime()
if not seconds then return nil end
seconds = math.max(math.floor(seconds), 0)
return ("%d:%02d"):format(math.floor(seconds / 60), seconds % 60)
end
KH.loop(0.25, function()
local hex, role = roleColorHex()
local dim = C.TextDim:ToHex()
-- Watermark
if S.UI.Watermark then
local parts = {
('<font color="#%s">Kitty Hub</font>'):format(C.Accent:ToHex()),
('<font color="#%s">MM2</font>'):format(dim),
("%d fps"):format(fps),
("%d ms"):format(math.floor(U.ping())),
('<font color="#%s">%s</font>'):format(hex, role),
}
local clock = clockText()
if clock then parts[#parts + 1] = clock end
UI.WatermarkText.Text = table.concat(parts, (' <font color="#%s">·</font> '):format(dim))
end
-- Sidebar status strip
UI.RoleLabel.Text = role
UI.RoleLabel.TextColor3 = Color3.fromHex(hex)
local bits = {}
local distance = Safety.MurdererDistance
if distance and distance ~= math.huge then
bits[#bits + 1] = ("murderer %dm"):format(math.floor(distance))
elseif Game.inRound() then
bits[#bits + 1] = "in round"
else
bits[#bits + 1] = "lobby"
end
local clock = clockText()
if clock then bits[#bits + 1] = clock end
UI.StatLabel.Text = table.concat(bits, " · ")
-- Live readouts, only while their tab is actually on screen.
if UI.IsOpen then
for _, readout in ipairs(UI.Readouts or {}) do
KH.safe("readout", readout.refresh)
end
end
end)
-- ================================================================ BOOT
if Config.available then
local ok = Config.load(S.UI.Profile)
if ok then
UI.refreshAll()
UI.applyAccent(S.UI.Accent)
end
end
-- Bring the world into line with whatever the loaded profile says.
Move.applyHumanoid()
if S.Move.Noclip then Move.setNoclip(true) end
UI.refreshKeybinds()
UI.setOpen(true)
-- The sidebar indicator is placed from AbsolutePosition, which is still
-- zero until the window has been laid out for a frame.
task.delay(0.35, function()
if KH.Alive then UI.selectTab(UI.ActiveTab or "Aimbot") end
end)
if game.PlaceId ~= 142823291 then
UI.notify({
title = "Not Murder Mystery 2",
text = "This build targets MM2. Most features will do nothing here.",
kind = "warn",
duration = 8,
})
end
UI.notify({
title = "Kitty Hub v" .. KH.Version,
text = ("Loaded on %s. Press %s for the menu."):format(KH.X.name, S.UI.MenuKey),
kind = "good",
duration = 5,
})
print(("[Kitty Hub] v%s loaded — executor: %s"):format(KH.Version, KH.X.name))
print(("[Kitty Hub] [%s] menu · [%s] aim · [%s] noclip · [%s] fly · [%s] throw knife")
:format(S.UI.MenuKey, S.Aim.Key, S.Move.NoclipKey, S.Move.FlyKey, S.Knife.ThrowKey))
end
-- ─── src/mm2/94_probe.lua ──────────────────────────────────────────
-- ============================================================================
-- PROBE — the diagnostics tab. Everything the mouse aim rests on but cannot
-- see for itself: whether the cursor answers a synthetic move, what a pixel is
-- worth to it, how fast a target really crosses the screen, and where the last
-- few shots actually landed.
-- ============================================================================
do
local UI = KH.UI
local U = KH.U
local Combat = KH.Combat
local UIS = KH.Services.UserInputService
local LocalPlayer = KH.LocalPlayer
local Probe = {}
KH.Probe = Probe
Probe.Cursor = "not run yet"
local live = {} -- the target's numbers, while the tab is open
local history = {} -- the last few aim results, newest first
local lines = {} -- the labels those are printed into
local lastResult
-- --------------------------------------------------------- cursor test
-- Ask for a move of a known size and watch what mouse.X/Y makes of it. Two
-- answers matter: whether it answers at all, which is what the whole aim
-- loop is built on, and by how much, which is the display scale it has to
-- learn rather than assume.
local testing = false
function Probe.testCursor()
if testing then return end
if type(mousemoverel) ~= "function" then
Probe.Cursor = "no mousemoverel on this executor"
return
end
testing = true
Probe.Cursor = "running…"
KH.detach(function()
pcall(function()
local mouse = LocalPlayer:GetMouse()
local home = Vector2.new(mouse.X, mouse.Y)
local lag, scale, lost = {}, {}, 0
-- Start from the middle of the screen. A move that runs into
-- the edge of the window is clamped, and a clamped sample looks
-- exactly like a wildly wrong display scale.
local view = KH.camera().ViewportSize
pcall(mousemoverel,
math.floor(view.X / 2 - mouse.X + 0.5),
math.floor(view.Y / 2 - mouse.Y + 0.5))
task.wait()
task.wait()
local moves = {
Vector2.new(120, 0), Vector2.new(-120, 0),
Vector2.new(0, 90), Vector2.new(0, -90),
}
for _, delta in ipairs(moves) do
local from = Vector2.new(mouse.X, mouse.Y)
pcall(mousemoverel, delta.X, delta.Y)
local landed, waited = nil, 0
for frame = 1, 10 do
task.wait()
waited = frame
if (Vector2.new(mouse.X, mouse.Y) - from).Magnitude >= 1 then
task.wait() -- one more, in case it arrives in pieces
landed = Vector2.new(mouse.X, mouse.Y)
break
end
end
if landed then
lag[#lag + 1] = waited
scale[#scale + 1] = delta.Magnitude
/ math.max((landed - from).Magnitude, 0.001)
else
lost = lost + 1
end
end
-- Put it back roughly where it was found.
pcall(mousemoverel,
math.floor(home.X - mouse.X + 0.5),
math.floor(home.Y - mouse.Y + 0.5))
if lost >= 3 then
Probe.Cursor = "never moved — mouse aim cannot work on this setup"
elseif #scale == 0 then
Probe.Cursor = "no usable samples"
else
local frames = 0
for i = 1, #lag do frames = frames + lag[i] end
-- Median, and every sample printed beside it. One clamped
-- or half-landed move used to drag the mean into nonsense
-- and make a healthy setup look broken.
local sorted, each = {}, {}
for i = 1, #scale do
sorted[i] = scale[i]
each[i] = ("%.2f"):format(scale[i])
end
table.sort(sorted)
Probe.Cursor = ("%d frame lag · scale %.2f (%s)%s"):format(
math.floor(frames / #lag + 0.5),
sorted[math.ceil(#sorted / 2)],
table.concat(each, " "),
lost > 0 and (" · " .. lost .. " lost") or "")
end
end)
testing = false
end)
end
-- ----------------------------------------------------------------- tab
local tab = UI.addTab("Probe")
local env = UI.section(tab, "This Executor")
UI.label(env, "What the mouse aim has to work with. If something here is wrong, no amount of tuning on the Aimbot tab will land a shot.")
UI.readout(env, {text = "Mouse Control", get = function() return Combat.Mouse.support() end})
UI.readout(env, {
text = "Mouse Behaviour",
get = function()
local mode = tostring(UIS.MouseBehavior):gsub("Enum%.MouseBehavior%.", "")
if mode == "Default" then return mode end
return mode .. " — cursor is locked"
end,
})
UI.button(env, {text = "Run Cursor Test", callback = function() Probe.testCursor() end})
UI.readout(env, {text = "Cursor Test", get = function() return Probe.Cursor end})
local target = UI.section(tab, "Target Right Now")
UI.label(target, "Live while this tab is open. The last line is the one that matters: a shot takes roughly 30 ms from the key press to the server, so anything near that is a target the aim has to lead rather than follow.")
UI.readout(target, {text = "Target", get = function() return live.name or "none" end})
UI.readout(target, {
text = "Distance",
get = function() return live.distance and ("%.0f studs"):format(live.distance) or "—" end,
})
UI.readout(target, {
text = "Speed",
get = function() return live.speed and ("%.0f studs/s"):format(live.speed) or "—" end,
})
UI.readout(target, {
text = "On Screen",
get = function()
if not live.name then return "—" end
return live.onScreen and "yes" or "no"
end,
})
UI.readout(target, {
text = "Body Width",
get = function() return live.radius and ("%.0f px"):format(live.radius * 2) or "—" end,
})
UI.readout(target, {
text = "Screen Speed",
get = function() return live.pxPerSec and ("%.0f px/s"):format(live.pxPerSec) or "—" end,
})
UI.readout(target, {
text = "Crosses Own Width",
get = function() return live.crossMs and ("%.0f ms"):format(live.crossMs) or "—" end,
})
local results = UI.section(tab, "Last Aim Results")
UI.label(results, "Switch on Aim Test on the Aimbot tab, then press your aim key at someone who is moving. Under a stud means the cursor was on them; twenty means the ray went past them into the scenery.")
for i = 1, 6 do
lines[i] = UI.label(results, "—")
end
-- -------------------------------------------------------- the report
-- One button, one paste. Reading numbers off a screen and retyping them is
-- how the important digit gets lost.
local function report()
local out = {
"kitty hub probe · v" .. tostring(KH.Version),
"executor: " .. tostring(KH.X.name),
"mouse control: " .. tostring(Combat.Mouse.support()),
"mouse behaviour: " .. tostring(UIS.MouseBehavior),
"cursor test: " .. tostring(Probe.Cursor),
}
if live.name then
out[#out + 1] = ("target: %s · %.0f studs · %.0f studs/s · body %.0f px · %.0f px/s · own width in %s")
:format(live.name, live.distance or 0, live.speed or 0,
(live.radius or 0) * 2, live.pxPerSec or 0,
live.crossMs and ("%.0f ms"):format(live.crossMs) or "n/a")
else
out[#out + 1] = "target: none in view"
end
out[#out + 1] = "aim results:"
for i = 1, #history do out[#out + 1] = " " .. tostring(history[i]) end
if #history == 0 then out[#out + 1] = " (none yet)" end
return table.concat(out, "\n")
end
local share = UI.section(tab, "Report")
UI.label(share, "Copies everything on this tab as text. Open the tab, take a few shots at someone moving with Aim Test on, then copy.")
UI.button(share, {
text = "Copy Report",
callback = function()
local ok, text = pcall(report)
if not ok then
UI.notify({title = "Probe", text = "Could not build the report.", kind = "bad", duration = 3})
return
end
if KH.X.setclipboard and pcall(setclipboard, text) then
UI.notify({title = "Probe", text = "Copied. Paste it wherever you need it.", kind = "good", duration = 3})
else
print(text)
UI.notify({
title = "Probe",
text = "No clipboard here — printed to the console (F9) instead.",
kind = "warn",
duration = 5,
})
end
end,
})
-- ------------------------------------------------------- aim history
-- Polled rather than pushed: the aim already writes its own outcome, and
-- nothing here has to reach into it to find out.
KH.loop(0.2, function()
local result = Combat.LastResult
if not result or result == lastResult then return end
lastResult = result
table.insert(history, 1, result)
for i = #history, 7, -1 do history[i] = nil end
for i = 1, #lines do lines[i].Text = history[i] or "—" end
end)
-- ------------------------------------------------------- live target
local lastPoint, lastAt
KH.loop(0.1, function()
if not (UI.IsOpen and UI.ActiveTab == "Probe") then
lastPoint = nil
return
end
local player, _, part = Combat.pickTarget()
if not player then player, _, part = Combat.pickTarget("Nearest") end
if not part then
live, lastPoint = {}, nil
return
end
local cam = KH.camera()
local point, onScreen = cam:WorldToViewportPoint(part.Position)
local here = Vector2.new(point.X, point.Y)
local now = os.clock()
local pxPerSec = 0
if lastPoint and now > lastAt then
pxPerSec = (here - lastPoint).Magnitude / (now - lastAt)
end
lastPoint, lastAt = here, now
local edge = cam:WorldToViewportPoint(part.Position + Vector3.new(0, part.Size.Y * 0.5, 0))
local radius = (Vector2.new(edge.X, edge.Y) - here).Magnitude
local speed = 0
pcall(function() speed = part.AssemblyLinearVelocity.Magnitude end)
live = {
name = player.DisplayName,
onScreen = onScreen,
distance = U.distanceTo(part.Position),
speed = speed,
pxPerSec = pxPerSec,
radius = radius,
crossMs = pxPerSec > 1 and (radius * 2 / pxPerSec) * 1000 or nil,
}
end)
end
-- ─── src/_shared/96_logo.lua ───────────────────────────────────────
-- ============================================================================
-- LOGO — the Kitty Hub mark, carried in the script rather than fetched.
--
-- Roblox only draws an image it can resolve as an asset, and this one was
-- never uploaded to Roblox. getcustomasset turns a file in the executor's own
-- folder into an asset, so the mark travels here as base64, is written out
-- once on the first run, and is read straight off disk every run after.
--
-- The name carries a version because the file is cached: change the image and
-- the old one has to stop being found, or it is the one that keeps drawing.
--
-- An executor with no file API gets nothing back. Nothing may assume this
-- returns an asset — the splash draws the wordmark as text when it does not.
-- ============================================================================
do
local FILE = "kittyhub_logo_v2.png"
local STALE = {"kittyhub_logo.png"}
local ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local VALUE = {}
for i = 1, #ALPHABET do VALUE[ALPHABET:byte(i)] = i - 1 end
-- Whitespace and padding read back as nil and are skipped, so the blob
-- below can be wrapped to a sane width.
local function decode(text)
local out, bits, held = {}, 0, 0
for i = 1, #text do
local value = VALUE[text:byte(i)]
if value then
bits = bits * 64 + value
held = held + 6
if held >= 8 then
held = held - 8
local scale = 2 ^ held
out[#out + 1] = string.char(math.floor(bits / scale) % 256)
bits = bits % scale
end
end
end
return table.concat(out)
end
local DATA = [[
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]]
local asset, resolved = nil, false
function KH.logoAsset()
if resolved then return asset end
resolved = true
if type(getcustomasset) ~= "function" then return nil end
local ok, result = pcall(function()
-- A truncated file from a half-finished write would resolve to an
-- asset that draws nothing, so check it looks like the image before
-- trusting it.
if type(isfile) == "function" and isfile(FILE) then
local read, existing = pcall(readfile, FILE)
if read and type(existing) == "string" and #existing > 1000 then
return getcustomasset(FILE)
end
end
if not KH.X.writefile then return nil end
writefile(FILE, decode(DATA))
-- Best effort: an earlier version of the mark is dead weight now.
if type(delfile) == "function" and type(isfile) == "function" then
for _, old in ipairs(STALE) do
if isfile(old) then pcall(delfile, old) end
end
end
return getcustomasset(FILE)
end)
asset = ok and result or nil
return asset
end
end
-- ─── src/_shared/98_splash.lua ─────────────────────────────────────
-- ============================================================================
-- SPLASH — the load-in screen.
--
-- Cosmetic, and honest about it: the script is a single chunk and has already
-- finished running by the time this draws, so nothing here is waiting on
-- anything. It sits over the menu for a moment and fades off.
--
-- Everything it makes is owned by KH and has a hard timer behind it, so there
-- is no path where it stays on screen — including the blur, which would be the
-- worst thing to leave behind.
-- ============================================================================
do
local UI = KH.UI
local C = UI.C
local make = UI.make
local Lighting = KH.Services.Lighting
if UI.Overlay then
local fades = {}
local function fading(obj, prop)
fades[#fades + 1] = {obj = obj, prop = prop or "BackgroundTransparency"}
return obj
end
-- Blurring what is behind it is what makes a transparent cover read as
-- glass rather than as a screen that failed to draw.
local blur = make("BlurEffect", {Name = "khSplash", Size = 0, Parent = Lighting})
KH.own(blur)
-- Active = false: even in the worst case this can never swallow a click.
local root = fading(make("Frame", {
Name = "Splash",
BackgroundColor3 = C.Bg,
BorderSizePixel = 0,
Size = UDim2.fromScale(1, 1),
Active = false,
ZIndex = 500,
Parent = UI.Overlay,
}))
KH.own(root)
-- Darkest at the edges, thinnest across the middle, so the game stays
-- readable behind the card instead of being painted out.
make("UIGradient", {
Rotation = 90,
Color = ColorSequence.new(C.Bg, C.Panel),
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.12),
NumberSequenceKeypoint.new(0.5, 0.46),
NumberSequenceKeypoint.new(1, 0.12),
}),
Parent = root,
})
local card = fading(make("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(330, 224),
BackgroundColor3 = C.Panel,
BackgroundTransparency = 0.1,
BorderSizePixel = 0,
ZIndex = 502,
Parent = root,
}))
UI.corner(card, 16)
UI.gradient(card, C.Card, C.Panel, 90)
fading(UI.stroke(card, C.Accent, 1, 0.55), "Transparency")
local shadow = UI.shadow(card, 30, 0.45)
shadow.ZIndex = 501
fading(shadow, "ImageTransparency")
-- A hairline of accent along the top edge, faded out at both ends.
local edge = fading(make("Frame", {
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.new(0.5, 0, 0, 0),
Size = UDim2.new(1, -70, 0, 1),
BackgroundColor3 = C.Accent,
BorderSizePixel = 0,
ZIndex = 503,
Parent = card,
}))
make("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.5, 0.05),
NumberSequenceKeypoint.new(1, 1),
}),
Parent = edge,
})
-- The mark carries the name, so there is no title line to draw with it.
-- Without it, the name has to be set instead.
local asset = KH.logoAsset()
if asset then
fading(make("ImageLabel", {
Image = asset,
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.new(0.5, 0, 0, 24),
Size = UDim2.fromOffset(96, 96),
ZIndex = 503,
Parent = card,
}), "ImageTransparency")
else
fading(make("TextLabel", {
Text = "KITTY HUB",
Font = Enum.Font.GothamBold,
TextSize = 24,
TextColor3 = C.Text,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(0, 56),
Size = UDim2.new(1, 0, 0, 30),
ZIndex = 503,
Parent = card,
}), "TextTransparency")
end
fading(make("TextLabel", {
Text = KH.GameName or "Roblox",
Font = Enum.Font.Gotham,
TextSize = 12,
TextColor3 = C.TextDim,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(0, 128),
Size = UDim2.new(1, 0, 0, 16),
ZIndex = 503,
Parent = card,
}), "TextTransparency")
local track = fading(make("Frame", {
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.new(0.5, 0, 0, 156),
Size = UDim2.fromOffset(236, 3),
BackgroundColor3 = C.Row,
BackgroundTransparency = 0.35,
BorderSizePixel = 0,
ZIndex = 503,
Parent = card,
}))
UI.corner(track, 2)
local fill = fading(make("Frame", {
Size = UDim2.fromScale(0, 1),
BackgroundColor3 = C.Accent,
BorderSizePixel = 0,
ZIndex = 504,
Parent = track,
}))
UI.corner(fill, 2)
local status = fading(make("TextLabel", {
Text = "starting up",
Font = Enum.Font.Gotham,
TextSize = 11,
TextColor3 = C.TextDim,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(0, 170),
Size = UDim2.new(1, 0, 0, 14),
ZIndex = 503,
Parent = card,
}), "TextTransparency")
fading(make("TextLabel", {
Text = ("v%s · %s"):format(KH.Version, KH.X.name),
Font = Enum.Font.Gotham,
TextSize = 10,
TextColor3 = C.TextFaint,
BackgroundTransparency = 1,
Position = UDim2.fromOffset(0, 194),
Size = UDim2.new(1, 0, 0, 14),
ZIndex = 503,
Parent = card,
}), "TextTransparency")
-- Nothing is loading, so the bar walks a fixed path rather than
-- pretending to measure one. Each step glides for as long as it is held
-- so the bar keeps moving the whole way rather than jumping and waiting.
local STAGE = 0.72
local stages = {
{0.15, "checking the executor"},
{0.33, "building the interface"},
{0.52, "loading features"},
{0.7, "arming the aimbot"},
{0.88, "reading the map"},
{1, "ready"},
}
KH.spawn(function()
UI.tween(blur, 0.5, {Size = 14})
for _, stage in ipairs(stages) do
if not (KH.Alive and root.Parent) then return end
status.Text = stage[2]
UI.tween(fill, STAGE, {Size = UDim2.fromScale(stage[1], 1)})
task.wait(STAGE)
end
task.wait(0.15)
if not (KH.Alive and root.Parent) then return end
for _, item in ipairs(fades) do
pcall(function() UI.tween(item.obj, 0.35, {[item.prop] = 1}) end)
end
UI.tween(blur, 0.35, {Size = 0})
task.wait(0.4)
root:Destroy()
blur:Destroy()
end)
-- Whatever happens above — an error, an unload mid-fade — both come
-- off. Reading Parent on a destroyed instance is safe.
task.delay(9, function()
if root.Parent then root:Destroy() end
if blur.Parent then blur:Destroy() end
end)
end
end