-- PROJECT GHOST FULL
-- Safe runtime build: no bypasses, no executor hooks, no bytecode dumping.
-- Green/Black Runtime Panel + Scanner Tool + Script Viewer + Action Menu + Neon Ghost Background
-- Put this LocalScript in:
-- StarterPlayer > StarterPlayerScripts > ProjectGhostClient.lua
--
-- Notes:
-- Pressing 1 only equips the Ghost Scanner tool.
-- The UI opens only when you click/use the scanner on an object, or when clicking the ghost icon.
-- Roblox gameplay LocalScripts cannot read real Script.Source.
-- To show code in the Script Viewer, use CODE_REGISTRY or add Attributes:
--   GhostSource = "your code text"
--   GhostInfo = "your scan info"
-- For ModuleScript reflection, set Attribute:
--   GhostInspectable = true
local Players = game:GetService("Players")
local UIS = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local Stats = game:GetService("Stats")
local RS = game:GetService("ReplicatedStorage")
local Lighting = game:GetService("Lighting")
local TweenService = game:GetService("TweenService")
local player = Players.LocalPlayer
local mouse = player:GetMouse()
local playerGui = player:WaitForChild("PlayerGui")
local backpack = player:WaitForChild("Backpack")
local GREEN = Color3.fromRGB(0, 255, 120)
local GREEN_DIM = Color3.fromRGB(0, 150, 70)
local GREEN_DARK = Color3.fromRGB(0, 55, 28)
local BLACK = Color3.fromRGB(0, 0, 0)
local BLACK_2 = Color3.fromRGB(3, 7, 5)
local BLACK_3 = Color3.fromRGB(7, 14, 10)
local WHITE_GREEN = Color3.fromRGB(170, 255, 205)
local PANEL_LINE = Color3.fromRGB(0, 95, 45)
local CYAN = Color3.fromRGB(80, 210, 255)
local WARNING = Color3.fromRGB(255, 210, 90)
local BAD = Color3.fromRGB(255, 80, 80)
local ALLOW_ALL_FOR_TESTING = true
local OWNER_USER_IDS = {
	[player.UserId] = true,
	-- [123456789] = true,
}
if not ALLOW_ALL_FOR_TESTING and not OWNER_USER_IDS[player.UserId] then
	return
end
local PANEL_SCALE_X = 0.82
local PANEL_SCALE_Y = 0.78
local PANEL_POS_X = 0.09
local PANEL_POS_Y = 0.10
local MAX_CLONE_PARTS = 80
local CLONE_OFFSET = Vector3.new(5, 1, 0)
local TOGGLE_KEY = Enum.KeyCode.RightShift
local PROJECT_GHOST_VERSION = "v3.5"
local SpawnCarName = "SpawnCar"
local SpawnCar = RS:FindFirstChild(SpawnCarName)
local ghostConnections = {}
local refreshCurrent = nil
local refreshBusy = false
local refreshQueued = false
local scanToken = 0
actionMenuFollowPart = nil
local undoStack = {}
local redoStack = {}
local MAX_HISTORY = 40
--=====================================================
-- CODE REGISTRY
--=====================================================
local CODE_REGISTRY = {
	{
		Name = "SpawnCar Remote",
		MatchNames = {"SpawnCar"},
		Description = "Vehicle spawning RemoteEvent.",
		AccessLevel = "GHOST_ROOT",
		Source = [[
local RS = game:GetService("ReplicatedStorage")
local SpawnCar = RS:WaitForChild("SpawnCar")
SpawnCar:FireServer(0, "SuperCar")
]],
	},
	{
		Name = "Vehicle Spawn Example",
		MatchNames = {"SuperCar", "Jeep", "PickUp", "Van", "MegaJeep", "Helicopter"},
		Description = "Registered vehicle spawn code.",
		AccessLevel = "VEHICLE_ADMIN",
		Source = [[
local RS = game:GetService("ReplicatedStorage")
local SpawnCar = RS:WaitForChild("SpawnCar")
local vehicle = "SuperCar"
SpawnCar:FireServer(0, vehicle)
]],
	},
	{
		Name = "Ghost Scanner Tool",
		MatchNames = {"Ghost Scanner"},
		Description = "Tool used to inspect and duplicate selected client-visible objects.",
		AccessLevel = "SCANNER",
		Source = [[
-- Hold the Ghost Scanner.
-- Press 1 only equips the tool.
-- Click an object to scan it and open Project Ghost.
-- Use the popup target menu or the Project Ghost panel.
]],
	},
	{
		Name = "Lighting Blackout",
		MatchNames = {"Lighting"},
		Description = "Dark hacking-game atmosphere script.",
		AccessLevel = "WORLD_EDIT",
		Source = [[
local Lighting = game:GetService("Lighting")
Lighting.ClockTime = 0
Lighting.Brightness = 0.5
]],
	},
	{
		Name = "Generic Model Profile",
		MatchClasses = {"Model"},
		Description = "Generic model profile. Add GhostSource attribute for custom source.",
		AccessLevel = "PARTIAL",
		Source = [[
-- No exact registered source found.
-- Add Attribute GhostSource to this object to display custom code.
-- Add Attribute GhostInfo to display custom scan notes.
]],
	},
}
--=====================================================
-- STATE
--=====================================================
local selectedObject = nil
local selectedFromTool = false
local selectedScriptObject = nil
local ghostLogs = {}
local remoteLogs = {}
local remoteStats = {}
local selectedRemoteObject = nil
local selectedStatObject = nil
local statScanResults = {}
local setSelectedStatValue = nil
local refreshStatsScanner = nil
local scanStatus = "IDLE"
local fps = 0
local lastFrame = os.clock()
local selectedVehicle = "SuperCar"
local vehicles = {"SuperCar", "Jeep", "PickUp", "Van", "MegaJeep", "Helicopter"}
local lastSpawn = 0
local spawnCooldown = 1.0
local trackedObjects = {}
local trackedObjectLog = {}
local scanHistory = {}
local originalLighting = {
	ClockTime = Lighting.ClockTime,
	Brightness = Lighting.Brightness,
	FogEnd = Lighting.FogEnd,
	Ambient = Lighting.Ambient,
	OutdoorAmbient = Lighting.OutdoorAmbient,
}
--=====================================================
-- UTILS
--=====================================================
local function safeFullName(obj)
	local ok, res = pcall(function()
		return obj:GetFullName()
	end)
	return ok and res or tostring(obj)
end
local function pushLog(text)
	table.insert(ghostLogs, 1, "[" .. os.date("%H:%M:%S") .. "] " .. tostring(text))
	if #ghostLogs > 140 then
		table.remove(ghostLogs)
	end
end
local function pushRemoteLog(text)
	table.insert(remoteLogs, 1, "[" .. os.date("%H:%M:%S") .. "] " .. tostring(text))
	if #remoteLogs > 140 then
		table.remove(remoteLogs)
	end
end
_G.ProjectGhost_Log = pushLog
_G.ProjectGhost_LogRemote = function(remoteName, direction, ...)
	local args = {...}
	local out = {}
	for i, v in ipairs(args) do
		out[i] = tostring(v)
	end
	local argsText = table.concat(out, ", ")
	pushRemoteLog(tostring(direction) .. " | " .. tostring(remoteName) .. " | " .. argsText)
	logRemoteCall(remoteName, direction, argsText)
end
local function getRegisteredCodeForObject(obj)
	if not obj then return nil end
	local objName = obj.Name
	local fullName = safeFullName(obj)
	local className = obj.ClassName
	for _, entry in ipairs(CODE_REGISTRY) do
		if entry.MatchFullName and entry.MatchFullName == fullName then
			return entry
		end
		if entry.MatchNames then
			for _, name in ipairs(entry.MatchNames) do
				if name == objName then
					return entry
				end
			end
		end
		if entry.MatchClasses then
			for _, c in ipairs(entry.MatchClasses) do
				if c == className then
					return entry
				end
			end
		end
	end
	return nil
end
local function countDescendants(obj)
	local ok, desc = pcall(function()
		return obj:GetDescendants()
	end)
	if not ok or not desc then return 0 end
	return #desc
end
local function canCloneObject(obj)
	if not obj then
		return false, "No object selected."
	end
	if obj:IsDescendantOf(playerGui) then
		return false, "Cannot clone PlayerGui UI."
	end
	if obj == workspace or obj == game then
		return false, "Cannot clone root services."
	end
	local total = countDescendants(obj)
	if total > MAX_CLONE_PARTS then
		return false, "Object too large. Descendants: " .. tostring(total)
	end
	if obj:IsA("Terrain") then
		return false, "Terrain cloning blocked."
	end
	return true, "OK"
end
local function getPivotOrCFrame(obj)
	if obj:IsA("BasePart") then
		return obj.CFrame
	end
	if obj:IsA("Model") then
		local ok, cf = pcall(function()
			return obj:GetPivot()
		end)
		if ok then return cf end
	end
	return nil
end
local function moveCloneNearOriginal(clone, original)
	local cf = getPivotOrCFrame(original)
	if not cf then return end
	local newCf = cf + CLONE_OFFSET
	if clone:IsA("BasePart") then
		clone.CFrame = newCf
	elseif clone:IsA("Model") then
		pcall(function()
			clone:PivotTo(newCf)
		end)
	end
end
local addScanHistory = nil
local diffLatestScans = nil

local function cloneSelectedObject()
	local obj = selectedObject
	local allowed, reason = canCloneObject(obj)
	if not allowed then
		pushLog("Clone blocked: " .. tostring(reason))
		return false, reason
	end
	local ok, cloneOrErr = pcall(function()
		local clone = obj:Clone()
		clone.Name = obj.Name .. "_GhostClone"
		clone.Parent = obj.Parent
		moveCloneNearOriginal(clone, obj)
		return clone
	end)
	if ok then
		pushUndo({Type = "Clone", Clone = cloneOrErr, Parent = cloneOrErr.Parent})
		pushLog("Cloned: " .. safeFullName(cloneOrErr))
		return true, "Cloned: " .. cloneOrErr.Name
	end
	pushLog("Clone failed: " .. tostring(cloneOrErr))
	return false, tostring(cloneOrErr)
end
local function selectObject(obj, fromTool)
	if not obj then return end
	selectedObject = obj
	selectedScriptObject = nil
	selectedFromTool = fromTool == true
	scanStatus = "TARGET_LOCKED"
	if typeof(addScanHistory) == "function" then
		addScanHistory(obj)
	end
	pushLog("Selected: " .. safeFullName(obj))
end
local function scanForNearbyScripts(obj)
	local found = {}
	if not obj then return found end
	local current = obj
	for _ = 1, 6 do
		if not current then break end
		for _, child in ipairs(current:GetChildren()) do
			if child:IsA("LocalScript") or child:IsA("ModuleScript") or child:IsA("Script") then
				table.insert(found, child)
			end
		end
		current = current.Parent
	end
	for _, desc in ipairs(obj:GetDescendants()) do
		if desc:IsA("LocalScript") or desc:IsA("ModuleScript") or desc:IsA("Script") then
			table.insert(found, desc)
		end
	end
	return found
end
local function getGhostSource(obj)
	if not obj then return nil, "No object." end
	local attrSource = obj:GetAttribute("GhostSource")
	if typeof(attrSource) == "string" and attrSource ~= "" then
		return attrSource, "GhostSource Attribute"
	end
	local registered = getRegisteredCodeForObject(obj)
	if registered then
		return registered.Source, "CODE_REGISTRY: " .. registered.Name
	end
	return nil, "No registered source."
end
local function getGhostInfo(obj)
	if not obj then return nil end
	local info = obj:GetAttribute("GhostInfo")
	if typeof(info) == "string" and info ~= "" then
		return info
	end
	local registered = getRegisteredCodeForObject(obj)
	if registered then
		return registered.Description
	end
	return nil
end
local function tryReflectModule(moduleScript)
	if not moduleScript or not moduleScript:IsA("ModuleScript") then
		return nil, "Not a ModuleScript."
	end
	if moduleScript:GetAttribute("GhostInspectable") ~= true then
		return nil, "Set GhostInspectable = true."
	end
	local finished = false
	local okResult = false
	local resultValue = nil
	task.spawn(function()
		local ok, result = pcall(function()
			return require(moduleScript)
		end)
		okResult = ok
		resultValue = result
		finished = true
	end)
	local started = os.clock()
	while not finished and os.clock() - started < 1.25 do
		task.wait()
	end
	if not finished then
		return nil, "Module timeout."
	end
	if not okResult then
		return nil, "Require failed: " .. tostring(resultValue)
	end
	local lines = {}
	table.insert(lines, "Module: " .. safeFullName(moduleScript))
	table.insert(lines, "Return type: " .. typeof(resultValue))
	if typeof(resultValue) == "table" then
		for k, v in pairs(resultValue) do
			table.insert(lines, tostring(k) .. " = " .. tostring(v) .. " [" .. typeof(v) .. "]")
		end
	else
		table.insert(lines, "Value: " .. tostring(resultValue))
	end
	return table.concat(lines, "\n"), "OK"
end
local function findTargetFromMouse()
	local target = mouse.Target
	if not target then return nil end
	local selected = target
	if target.Parent and target.Parent:IsA("Model") and target.Parent ~= workspace then
		selected = target.Parent
	end
	return selected
end
local function classifyRemotePower(remote)
	local name = remote.Name:lower()
	local path = safeFullName(remote):lower()
	local text = name .. " " .. path
	-- Locked/server-only names are hidden from the usable monitor.
	local lockedWords = {
		"kick", "ban", "admin", "shutdown", "servercontrol", "server_control",
		"givecash", "givemoney", "setcash", "money", "cash", "currency",
		"damage", "kill", "forceadmin", "moderator", "modcommand",
		"getserverversion", "getservertype", "getserverchannel",
		"serverversion", "servertype", "serverchannel"
	}
	for _, word in ipairs(lockedWords) do
		if text:find(word, 1, true) then
			return "LOCKED", "Server Only / Not Shown", 0
		end
	end
	local score = 10
	local category = "Unknown"
	local level = "UNKNOWN"
	local usableRules = {
		{words = {"spawncar", "spawnvehicle", "vehicle", "car", "heli", "helicopter", "plane", "boat"}, score = 90, category = "Vehicle / Spawn", level = "USABLE"},
		{words = {"door", "open", "unlock", "interact", "use", "button"}, score = 80, category = "Interact / Door", level = "USABLE"},
		{words = {"mission", "quest", "startmission", "startquest", "objective"}, score = 75, category = "Mission / Quest", level = "TESTABLE"},
		{words = {"effect", "visual", "sound", "camera", "shake", "particle", "light"}, score = 65, category = "Visual / Effect", level = "CLIENT"},
		{words = {"equip", "tool", "item", "inventory"}, score = 60, category = "Tool / Inventory", level = "TESTABLE"},
		{words = {"weather", "time", "lighting", "fog"}, score = 55, category = "World Visual", level = "TESTABLE"},
		{words = {"debug", "test", "dev", "ghost"}, score = 50, category = "Debug / Test", level = "TESTABLE"},
	}
	for _, rule in ipairs(usableRules) do
		for _, word in ipairs(rule.words) do
			if text:find(word, 1, true) then
				if rule.score > score then
					score = rule.score
					category = rule.category
					level = rule.level
				end
			end
		end
	end
	return level, category, score
end
local function getRemoteStats(remote)
	if not remote then return nil end
	local key = safeFullName(remote)
	if not remoteStats[key] then
		local level, category, score = classifyRemotePower(remote)
		remoteStats[key] = {
			path = key,
			name = remote.Name,
			className = remote.ClassName,
			level = level,
			category = category,
			score = score,
			callCount = 0,
			lastArgs = "No calls logged",
			lastTime = "Never",
			firstSeen = os.clock(),
			lastClock = 0,
		}
	end
	return remoteStats[key]
end
local function registerRemoteSeen(remote)
	if remote and (remote:IsA("RemoteEvent") or remote:IsA("RemoteFunction")) then
		getRemoteStats(remote)
	end
end
local function logRemoteCall(remoteName, direction, argsText)
	for _, remote in ipairs(game:GetDescendants()) do
		if (remote:IsA("RemoteEvent") or remote:IsA("RemoteFunction")) and remote.Name == tostring(remoteName) then
			local stat = getRemoteStats(remote)
			stat.callCount += 1
			stat.lastArgs = argsText or ""
			stat.lastTime = os.date("%H:%M:%S")
			stat.lastClock = os.clock()
			break
		end
	end
end
local function scanAllRemotes()
	for _, obj in ipairs(game:GetDescendants()) do
		if obj:IsA("RemoteEvent") or obj:IsA("RemoteFunction") then
			registerRemoteSeen(obj)
		end
	end
end
local function pushObjectTrack(text)
	table.insert(trackedObjectLog, 1, "[" .. os.date("%H:%M:%S") .. "] " .. tostring(text))
	if #trackedObjectLog > 120 then
		table.remove(trackedObjectLog)
	end
end
local function startObjectTracker()
	if trackedObjects.started then
		return
	end
	trackedObjects.started = true
	workspace.DescendantAdded:Connect(function(obj)
		if obj:IsA("Model") or obj:IsA("BasePart") or obj:IsA("Tool") then
			pushObjectTrack("+ " .. obj.ClassName .. ": " .. safeFullName(obj))
		end
	end)
	workspace.DescendantRemoving:Connect(function(obj)
		if obj:IsA("Model") or obj:IsA("BasePart") or obj:IsA("Tool") then
			pushObjectTrack("- " .. obj.ClassName .. ": " .. obj.Name)
		end
	end)
	pushObjectTrack("Object tracker started.")
end
local function setGhostVision(state)
	removedVisionFlag = state
	if state then
		Lighting.ClockTime = 0
		Lighting.Brightness = 0.45
		Lighting.FogEnd = 175
		Lighting.Ambient = Color3.fromRGB(0, 35, 15)
		Lighting.OutdoorAmbient = Color3.fromRGB(0, 20, 10)
		pushLog("Removed enabled.")
	else
		Lighting.ClockTime = originalLighting.ClockTime
		Lighting.Brightness = originalLighting.Brightness
		Lighting.FogEnd = originalLighting.FogEnd
		Lighting.Ambient = originalLighting.Ambient
		Lighting.OutdoorAmbient = originalLighting.OutdoorAmbient
		pushLog("Removed disabled.")
	end
end
local function editablePropertyRows(obj)
	if not obj then return {} end
	local rows = {}
	if obj:IsA("BasePart") then
		table.insert(rows, {"Anchored", "bool"})
		table.insert(rows, {"CanCollide", "bool"})
		table.insert(rows, {"Transparency", "number"})
		table.insert(rows, {"Reflectance", "number"})
	end
	if obj:IsA("Humanoid") then
		table.insert(rows, {"WalkSpeed", "number"})
		table.insert(rows, {"JumpPower", "number"})
		table.insert(rows, {"Health", "number"})
	end
	return rows
end
local function toggleBoolProperty(obj, prop)
	local oldValue = obj[prop]
	local ok, err = pcall(function()
		obj[prop] = not obj[prop]
	end)
	if ok then
		pushUndo({Type = "Property", Object = obj, Property = prop, OldValue = oldValue, NewValue = obj[prop]})
		pushLog(prop .. " -> " .. tostring(obj[prop]))
	else
		pushLog("Failed changing " .. prop .. ": " .. tostring(err))
	end
end
local function stepNumberProperty(obj, prop, amount)
	local oldValue = obj[prop]
	local ok, err = pcall(function()
		local current = tonumber(obj[prop]) or 0
		obj[prop] = current + amount
	end)
	if ok then
		pushUndo({Type = "Property", Object = obj, Property = prop, OldValue = oldValue, NewValue = obj[prop]})
		pushLog(prop .. " -> " .. tostring(obj[prop]))
	else
		pushLog("Failed changing " .. prop .. ": " .. tostring(err))
	end
end
local function getSelectedHumanoid()
	if not selectedObject then return nil end
	if selectedObject:IsA("Humanoid") then
		return selectedObject
	end
	if selectedObject:IsA("Model") then
		return selectedObject:FindFirstChildOfClass("Humanoid")
	end
	if selectedObject.Parent and selectedObject.Parent:IsA("Model") then
		return selectedObject.Parent:FindFirstChildOfClass("Humanoid")
	end
	return nil
end
local function connectGhost(signal, callback)
	local connection = signal:Connect(callback)
	table.insert(ghostConnections, connection)
	return connection
end
local function shutdownGhost()
	for _, connection in ipairs(ghostConnections) do
		pcall(function()
			connection:Disconnect()
		end)
	end
	table.clear(ghostConnections)
	if gui then
		pcall(function()
			gui:Destroy()
		end)
	end
end
_G.ProjectGhost_Shutdown = shutdownGhost
local function getSpawnCarRemote(timeout)
	local remote = RS:FindFirstChild(SpawnCarName)
	if remote then
		SpawnCar = remote
		return remote
	end
	local ok, result = pcall(function()
		return RS:WaitForChild(SpawnCarName, timeout or 1)
	end)
	if ok and result then
		SpawnCar = result
		return result
	end
	return nil
end
local function pushUndo(action)
	table.insert(undoStack, action)
	if #undoStack > MAX_HISTORY then
		table.remove(undoStack, 1)
	end
	table.clear(redoStack)
end
local function undoLast()
	local action = table.remove(undoStack)
	if not action then
		pushLog("Nothing to undo.")
		return
	end
	if action.Type == "Delete" and action.Clone and action.Parent then
		action.Clone.Parent = action.Parent
		if action.CFrame and action.Clone:IsA("BasePart") then
			action.Clone.CFrame = action.CFrame
		elseif action.CFrame and action.Clone:IsA("Model") then
			pcall(function()
				action.Clone:PivotTo(action.CFrame)
			end)
		end
		selectedObject = action.Clone
		pushLog("Undo delete: " .. action.Clone.Name)
		table.insert(redoStack, action)
		return
	end
	if action.Type == "Clone" and action.Clone and action.Clone.Parent then
		action.Clone.Parent = nil
		pushLog("Undo clone.")
		table.insert(redoStack, action)
		return
	end
	if action.Type == "Property" and action.Object then
		pcall(function()
			action.Object[action.Property] = action.OldValue
		end)
		pushLog("Undo property: " .. action.Property)
		table.insert(redoStack, action)
		return
	end
	pushLog("Undo failed.")
end
local function redoLast()
	local action = table.remove(redoStack)
	if not action then
		pushLog("Nothing to redo.")
		return
	end
	if action.Type == "Delete" and action.Clone then
		action.Clone.Parent = nil
		pushLog("Redo delete.")
		table.insert(undoStack, action)
		return
	end
	if action.Type == "Clone" and action.Clone and action.Parent then
		action.Clone.Parent = action.Parent
		pushLog("Redo clone.")
		table.insert(undoStack, action)
		return
	end
	if action.Type == "Property" and action.Object then
		pcall(function()
			action.Object[action.Property] = action.NewValue
		end)
		pushLog("Redo property: " .. action.Property)
		table.insert(undoStack, action)
		return
	end
	pushLog("Redo failed.")
end
local function clampPanelToScreen()
	if not main then return end
	camera = workspace.CurrentCamera
	if not camera then return end
	local viewport = camera.ViewportSize
	local size = main.AbsoluteSize
	local pos = main.AbsolutePosition
	local x = math.clamp(pos.X, 4, math.max(4, viewport.X - size.X - 4))
	local y = math.clamp(pos.Y, 4, math.max(4, viewport.Y - size.Y - 4))
	main.Position = UDim2.new(0, x, 0, y)
end
local function resetPanelPosition()
	main.Size = UDim2.new(PANEL_SCALE_X, 0, PANEL_SCALE_Y, 0)
	main.Position = UDim2.new(PANEL_POS_X, 0, PANEL_POS_Y, 0)
	pushLog("Panel position reset.")
end
local function smartRefresh()
	if refreshBusy then
		refreshQueued = true
		return
	end
	refreshBusy = true
	refreshCurrent()
	refreshBusy = false
	if refreshQueued then
		refreshQueued = false
		task.defer(smartRefresh)
	end
end
local function captureObjectSnapshot(obj)
	if not obj then return nil end
	local data = {
		Name = obj.Name,
		ClassName = obj.ClassName,
		Path = safeFullName(obj),
		Time = os.date("%H:%M:%S"),
		Descendants = countDescendants(obj),
		Props = {},
	}
	if obj:IsA("BasePart") then
		data.Props.Anchored = tostring(obj.Anchored)
		data.Props.CanCollide = tostring(obj.CanCollide)
		data.Props.Transparency = tostring(obj.Transparency)
		data.Props.Position = tostring(obj.Position)
	end
	local hum = nil
	if obj:IsA("Model") then
		hum = obj:FindFirstChildOfClass("Humanoid")
	elseif obj:IsA("Humanoid") then
		hum = obj
	end
	if hum then
		data.Props.Health = tostring(math.floor(hum.Health))
		data.Props.WalkSpeed = tostring(hum.WalkSpeed)
		data.Props.JumpPower = tostring(hum.JumpPower)
	end
	return data
end
addScanHistory = function(obj)
	local snap = captureObjectSnapshot(obj)
	if not snap then return end
	table.insert(scanHistory, 1, snap)
	if #scanHistory > 80 then
		table.remove(scanHistory)
	end
end
diffLatestScans = function()
	if #scanHistory < 2 then
		return "Need two scans."
	end
	local now = scanHistory[1]
	local before = nil
	for i = 2, #scanHistory do
		if scanHistory[i].Path == now.Path then
			before = scanHistory[i]
			break
		end
	end
	if not before then
		return "No previous scan for target."
	end
	local lines = {"Diff: " .. now.Name}
	for k, v in pairs(now.Props) do
		local old = before.Props[k]
		if old == nil then
			table.insert(lines, "+ " .. k .. " = " .. v)
		elseif old ~= v then
			table.insert(lines, "~ " .. k .. ": " .. old .. " -> " .. v)
		end
	end
	for k, old in pairs(before.Props) do
		if now.Props[k] == nil then
			table.insert(lines, "- " .. k .. " = " .. old)
		end
	end
	if #lines == 1 then
		table.insert(lines, "No changes.")
	end
	return table.concat(lines, "\n")
end
--=====================================================
-- RUNTIME API STATUS
-- Safe compatibility check only.
-- No bypassing, no executor hooks, no bytecode dumping.
--=====================================================
local RuntimeAPIStatus = {
	HookMetamethod = typeof(hookmetamethod) == "function",
	GetNamecallMethod = typeof(getnamecallmethod) == "function",
	Decompile = typeof(decompile) == "function",
	GetGC = typeof(getgc) == "function",
	GetGenv = typeof(getgenv) == "function",
}
local function getRuntimeAPIReport()
	local lines = {
		"RUNTIME API STATUS",
		"hookmetamethod: " .. tostring(RuntimeAPIStatus.HookMetamethod),
		"getnamecallmethod: " .. tostring(RuntimeAPIStatus.GetNamecallMethod),
		"decompile: " .. tostring(RuntimeAPIStatus.Decompile),
		"getgc: " .. tostring(RuntimeAPIStatus.GetGC),
		"getgenv: " .. tostring(RuntimeAPIStatus.GetGenv),
		"",
		"Mode: Safe fallback",
		"Bypass: not loaded",
		"Bytecode dump: not loaded",
	}
	return table.concat(lines, "\n")
end

local function isStatLikeName(name)
	local n = tostring(name):lower()
	local keys = {"cash","money","coin","coins","currency","point","points","time","timer","minutes","seconds","level","xp","exp","score","kills","wins","gems","tokens","credits"}

	for _, key in ipairs(keys) do
		if n:find(key, 1, true) then
			return true
		end
	end

	return false
end

local function isValueObject(obj)
	return obj:IsA("IntValue") or obj:IsA("NumberValue") or obj:IsA("StringValue") or obj:IsA("BoolValue")
end

local function addStatResult(obj, source)
	if not obj or not isValueObject(obj) then return end
	if not isStatLikeName(obj.Name) then return end

	table.insert(statScanResults, {
		Object = obj,
		Name = obj.Name,
		Value = tostring(obj.Value),
		ClassName = obj.ClassName,
		Path = safeFullName(obj),
		Source = source or "Unknown",
	})
end

local function scanStatsAndCurrency()
	table.clear(statScanResults)

	local localPlayer = Players.LocalPlayer
	if not localPlayer then return statScanResults end

	local leaderstats = localPlayer:FindFirstChild("leaderstats")
	if leaderstats then
		for _, obj in ipairs(leaderstats:GetDescendants()) do
			addStatResult(obj, "leaderstats")
		end
	end

	for _, obj in ipairs(localPlayer:GetDescendants()) do
		addStatResult(obj, "Player")
	end

	local playerGui = localPlayer:FindFirstChild("PlayerGui")
	if playerGui then
		for _, obj in ipairs(playerGui:GetDescendants()) do
			if obj:IsA("TextLabel") or obj:IsA("TextButton") or obj:IsA("TextBox") then
				local shownText = tostring(obj.Text)
				local lowerText = shownText:lower()
				if isStatLikeName(obj.Name) or lowerText:find("cash", 1, true) or lowerText:find("money", 1, true) or lowerText:find("points", 1, true) then
					table.insert(statScanResults, {
						Object = obj,
						Name = obj.Name,
						Value = shownText,
						ClassName = obj.ClassName,
						Path = safeFullName(obj),
						Source = "PlayerGui",
					})
				end
			else
				addStatResult(obj, "PlayerGui")
			end
		end
	end

	for _, obj in ipairs(RS:GetDescendants()) do
		addStatResult(obj, "ReplicatedStorage")
	end

	for key, value in pairs(localPlayer:GetAttributes()) do
		if isStatLikeName(key) then
			table.insert(statScanResults, {
				Object = localPlayer,
				Name = key,
				Value = tostring(value),
				ClassName = "Attribute",
				Path = safeFullName(localPlayer) .. ".Attribute." .. key,
				Source = "Player Attribute",
				AttributeName = key,
			})
		end
	end

	table.sort(statScanResults, function(a, b)
		return a.Name < b.Name
	end)

	return statScanResults
end

setSelectedStatValue = function(newValue)
	if not selectedStatObject then
		pushLog("No stat selected.")
		return
	end

	local target = selectedStatObject.Object
	if not target then
		pushLog("Stat target missing.")
		return
	end

	local ok, err = pcall(function()
		if selectedStatObject.AttributeName then
			local castValue = tonumber(newValue) or newValue
			target:SetAttribute(selectedStatObject.AttributeName, castValue)
			pushLog(selectedStatObject.Name .. " -> " .. tostring(castValue))
			return
		end

		if target:IsA("IntValue") then
			target.Value = math.floor(tonumber(newValue) or target.Value)
		elseif target:IsA("NumberValue") then
			target.Value = tonumber(newValue) or target.Value
		elseif target:IsA("StringValue") then
			target.Value = tostring(newValue)
		elseif target:IsA("BoolValue") then
			local n = tostring(newValue):lower()
			target.Value = (n == "true" or n == "1" or n == "yes")
		elseif target:IsA("TextLabel") or target:IsA("TextButton") or target:IsA("TextBox") then
			target.Text = tostring(newValue)
		end

		pushLog(selectedStatObject.Name .. " -> " .. tostring(newValue))
	end)

	if not ok then
		pushLog("Stat edit failed: " .. tostring(err))
	end
end

--=====================================================
-- UI ROOT
--=====================================================
if _G.ProjectGhost_Shutdown then
	pcall(_G.ProjectGhost_Shutdown)
end
local oldGui = playerGui:FindFirstChild("ProjectGhost")
if oldGui then oldGui:Destroy() end
local gui = Instance.new("ScreenGui")
gui.Name = "ProjectGhost"
gui.IgnoreGuiInset = true
gui.ResetOnSpawn = false
gui.Parent = playerGui
local ghostIcon = Instance.new("TextButton")
ghostIcon.Name = "GhostIcon"
ghostIcon.Size = UDim2.new(0, 56, 0, 56)
ghostIcon.Position = UDim2.new(0, 18, 0.5, -28)
ghostIcon.BackgroundColor3 = BLACK
ghostIcon.Text = "👻"
ghostIcon.TextSize = 31
ghostIcon.TextColor3 = GREEN
ghostIcon.Font = Enum.Font.GothamBold
ghostIcon.AutoButtonColor = false
ghostIcon.Parent = gui
do
	local c = Instance.new("UICorner")
	c.CornerRadius = UDim.new(1, 0)
	c.Parent = ghostIcon
	local s = Instance.new("UIStroke")
	s.Color = GREEN
	s.Thickness = 2
	s.Parent = ghostIcon
end
-- Draggable ghost button
local draggingGhostIcon = false
local ghostIconDragStart = nil
local ghostIconStartPos = nil
ghostIcon.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
		draggingGhostIcon = true
		ghostIconDragStart = input.Position
		ghostIconStartPos = ghostIcon.Position
		input.Changed:Connect(function()
			if input.UserInputState == Enum.UserInputState.End then
				draggingGhostIcon = false
			end
		end)
	end
end)
UIS.InputChanged:Connect(function(input)
	if draggingGhostIcon and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
		local delta = input.Position - ghostIconDragStart
		ghostIcon.Position = UDim2.new(
			ghostIconStartPos.X.Scale,
			ghostIconStartPos.X.Offset + delta.X,
			ghostIconStartPos.Y.Scale,
			ghostIconStartPos.Y.Offset + delta.Y
		)
	end
end)
local main = Instance.new("Frame")
main.Name = "Main"
main.Size = UDim2.new(PANEL_SCALE_X, 0, PANEL_SCALE_Y, 0)
main.Position = UDim2.new(PANEL_POS_X, 0, PANEL_POS_Y, 0)
main.BackgroundColor3 = BLACK
main.BackgroundTransparency = 0.18
main.BorderSizePixel = 0
main.Visible = false
main.Parent = gui
local mainScale = Instance.new("UIScale")
mainScale.Name = "ResponsiveScale"
mainScale.Scale = UIS.TouchEnabled and 0.82 or 1
mainScale.Parent = main
do
	local s = Instance.new("UIStroke")
	s.Color = GREEN
	s.Thickness = 2
	s.Parent = main
end
local top = Instance.new("Frame")
top.Name = "TopBar"
top.Size = UDim2.new(1, 0, 0, 58)
top.BackgroundColor3 = BLACK_2
top.BorderSizePixel = 0
top.Parent = main
local title = Instance.new("TextLabel")
title.Size = UDim2.new(0, 360, 1, 0)
title.Position = UDim2.new(0, 12, 0, 0)
title.BackgroundTransparency = 1
title.Text = "PROJECT GHOST // v3.4 // Scanner"
title.TextColor3 = GREEN
title.Font = Enum.Font.Code
title.TextSize = 18
title.TextXAlignment = Enum.TextXAlignment.Left
title.Parent = top
local statusText = Instance.new("TextLabel")
statusText.Size = UDim2.new(0, 160, 1, 0)
statusText.Position = UDim2.new(0, 390, 0, 0)
statusText.BackgroundTransparency = 1
statusText.Text = "STATUS: OPERATIONAL"
statusText.TextColor3 = GREEN
statusText.Font = Enum.Font.Code
statusText.TextSize = 13
statusText.TextXAlignment = Enum.TextXAlignment.Left
statusText.Parent = top
local targetText = Instance.new("TextLabel")
targetText.Size = UDim2.new(0, 260, 1, 0)
targetText.Position = UDim2.new(0, 570, 0, 0)
targetText.BackgroundTransparency = 1
targetText.Text = "TARGET: none"
targetText.TextColor3 = WHITE_GREEN
targetText.Font = Enum.Font.Code
targetText.TextSize = 13
targetText.TextXAlignment = Enum.TextXAlignment.Left
targetText.Parent = top
local uptimeText = Instance.new("TextLabel")
uptimeText.Size = UDim2.new(0, 170, 1, 0)
uptimeText.Position = UDim2.new(1, -390, 0, 0)
uptimeText.BackgroundTransparency = 1
uptimeText.Text = "UPTIME: 00:00"
uptimeText.TextColor3 = WHITE_GREEN
uptimeText.Font = Enum.Font.Code
uptimeText.TextSize = 13
uptimeText.TextXAlignment = Enum.TextXAlignment.Left
uptimeText.Parent = top
local refreshBtn = Instance.new("TextButton")
refreshBtn.Size = UDim2.new(0, 90, 0, 30)
refreshBtn.Position = UDim2.new(1, -210, 0, 13)
refreshBtn.BackgroundColor3 = BLACK
refreshBtn.Text = "REFRESH"
refreshBtn.TextColor3 = GREEN
refreshBtn.Font = Enum.Font.Code
refreshBtn.TextSize = 14
refreshBtn.AutoButtonColor = false
refreshBtn.Parent = top
local minimizeBtn = Instance.new("TextButton")
minimizeBtn.Size = UDim2.new(0, 36, 0, 30)
minimizeBtn.Position = UDim2.new(1, -110, 0, 13)
minimizeBtn.BackgroundColor3 = BLACK
minimizeBtn.Text = "_"
minimizeBtn.TextColor3 = GREEN
minimizeBtn.Font = Enum.Font.Code
minimizeBtn.TextSize = 20
minimizeBtn.AutoButtonColor = false
minimizeBtn.Parent = top
local closeBtn = Instance.new("TextButton")
closeBtn.Size = UDim2.new(0, 36, 0, 30)
closeBtn.Position = UDim2.new(1, -60, 0, 13)
closeBtn.BackgroundColor3 = BLACK
closeBtn.Text = "X"
closeBtn.TextColor3 = BAD
closeBtn.Font = Enum.Font.Code
closeBtn.TextSize = 16
closeBtn.AutoButtonColor = false
closeBtn.Parent = top
for _, btn in ipairs({refreshBtn, minimizeBtn, closeBtn}) do
	local s = Instance.new("UIStroke")
	s.Color = GREEN
	s.Thickness = 1
	s.Parent = btn
end
local sidebar = Instance.new("Frame")
sidebar.Size = UDim2.new(0, 210, 1, -58)
sidebar.Position = UDim2.new(0, 0, 0, 58)
sidebar.BackgroundColor3 = BLACK_2
sidebar.BorderSizePixel = 0
sidebar.Parent = main
local sidebarLayout = Instance.new("UIListLayout")
sidebarLayout.Padding = UDim.new(0, 6)
sidebarLayout.SortOrder = Enum.SortOrder.LayoutOrder
sidebarLayout.Parent = sidebar
local sidebarPad = Instance.new("UIPadding")
sidebarPad.PaddingTop = UDim.new(0, 9)
sidebarPad.PaddingLeft = UDim.new(0, 8)
sidebarPad.PaddingRight = UDim.new(0, 8)
sidebarPad.Parent = sidebar
local modulesHeader = Instance.new("TextLabel")
modulesHeader.Name = "ModulesHeader"
modulesHeader.Size = UDim2.new(1, 0, 0, 28)
modulesHeader.BackgroundTransparency = 1
modulesHeader.Text = "MODULES"
modulesHeader.TextColor3 = WHITE_GREEN
modulesHeader.Font = Enum.Font.Code
modulesHeader.TextSize = 13
modulesHeader.TextXAlignment = Enum.TextXAlignment.Left
modulesHeader.LayoutOrder = -100
modulesHeader.Parent = sidebar
local content = Instance.new("Frame")
content.Name = "Content"
content.Size = UDim2.new(1, -210, 1, -118)
content.Position = UDim2.new(0, 210, 0, 58)
content.BackgroundColor3 = BLACK
content.BackgroundTransparency = 0.58
content.BorderSizePixel = 0
content.ClipsDescendants = true
content.Parent = main
local bottomPanel = Instance.new("Frame")
bottomPanel.Name = "BottomPanel"
bottomPanel.Size = UDim2.new(1, -210, 0, 60)
bottomPanel.Position = UDim2.new(0, 210, 1, -60)
bottomPanel.BackgroundColor3 = BLACK_2
bottomPanel.BackgroundTransparency = 0.25
bottomPanel.BorderSizePixel = 0
bottomPanel.Parent = main
local bottomStroke = Instance.new("UIStroke")
bottomStroke.Color = GREEN_DARK
bottomStroke.Thickness = 1
bottomStroke.Parent = bottomPanel
local terminalLine = Instance.new("TextLabel")
terminalLine.Name = "TerminalLine"
terminalLine.Size = UDim2.new(1, -20, 0, 24)
terminalLine.Position = UDim2.new(0, 10, 0, 30)
terminalLine.BackgroundTransparency = 1
terminalLine.Text = "ghost@terminal:~$ ready"
terminalLine.TextColor3 = WHITE_GREEN
terminalLine.Font = Enum.Font.Code
terminalLine.TextSize = 13
terminalLine.TextXAlignment = Enum.TextXAlignment.Left
terminalLine.Parent = bottomPanel
local quickTitle = Instance.new("TextLabel")
quickTitle.Size = UDim2.new(0, 120, 0, 20)
quickTitle.Position = UDim2.new(0, 10, 0, 6)
quickTitle.BackgroundTransparency = 1
quickTitle.Text = "QUICK ACTIONS"
quickTitle.TextColor3 = GREEN
quickTitle.Font = Enum.Font.Code
quickTitle.TextSize = 12
quickTitle.TextXAlignment = Enum.TextXAlignment.Left
quickTitle.Parent = bottomPanel
local function makeQuickButton(text, x, callback)
	local b = Instance.new("TextButton")
	b.Size = UDim2.new(0, 145, 0, 24)
	b.Position = UDim2.new(0, x, 0, 4)
	b.BackgroundColor3 = BLACK
	b.BackgroundTransparency = 0.25
	b.BorderSizePixel = 0
	b.Text = text
	b.TextColor3 = GREEN
	b.Font = Enum.Font.Code
	b.TextSize = 11
	b.AutoButtonColor = false
	b.Parent = bottomPanel
	local bs = Instance.new("UIStroke")
	bs.Color = GREEN_DARK
	bs.Thickness = 1
	bs.Parent = b
	b.MouseEnter:Connect(function()
		b.BackgroundColor3 = Color3.fromRGB(0, 35, 16)
	end)
	b.MouseLeave:Connect(function()
		b.BackgroundColor3 = BLACK
	end)
	b.MouseButton1Click:Connect(callback)
	return b
end
-- callbacks are resolved later after refresh functions exist
local quickActionsReady = false
--=====================================================
-- ANIMATED GHOST BACKGROUND
-- Clean neon outline ghost, based on the reference image.
--=====================================================
local ghostBg = Instance.new("Frame")
ghostBg.Name = "AnimatedGhostBackground"
ghostBg.Size = UDim2.new(0, 390, 0, 390)
ghostBg.Position = UDim2.new(0.5, -195, 0.5, -195)
ghostBg.BackgroundTransparency = 1
ghostBg.ZIndex = 1
ghostBg.Parent = content
local ghostDots = {}
local outlineDots = {}
local function makeDot(parent, xScale, yScale, size, transparency, z)
	local dot = Instance.new("Frame")
	dot.Size = UDim2.new(0, size, 0, size)
	dot.AnchorPoint = Vector2.new(0.5, 0.5)
	dot.Position = UDim2.new(xScale, 0, yScale, 0)
	dot.BackgroundColor3 = GREEN
	dot.BackgroundTransparency = transparency or 0.35
	dot.BorderSizePixel = 0
	dot.ZIndex = z or 1
	dot.Parent = parent
	local corner = Instance.new("UICorner")
	corner.CornerRadius = UDim.new(1, 0)
	corner.Parent = dot
	local glow = Instance.new("UIStroke")
	glow.Color = GREEN
	glow.Thickness = 4
	glow.Transparency = 0.12
	glow.Parent = dot
	return dot
end
local function addOutlineDot(x, y, size, alpha)
	local dot = makeDot(ghostBg, x, y, size or 6, alpha or 0.42, 1)
	table.insert(ghostDots, dot)
	table.insert(outlineDots, dot)
	return dot
end
-- Rounded dome, smoother and less like a full circle.
for i = 0, 46 do
	local theta = math.pi - (i / 46) * math.pi
	local x = 0.5 + math.cos(theta) * 0.31
	local y = 0.34 - math.sin(theta) * 0.265
	addOutlineDot(x, y, 6, 0.48)
end
-- Straight sides.
for i = 1, 30 do
	local y = 0.34 + i * 0.014
	addOutlineDot(0.19, y, 6, 0.48)
	addOutlineDot(0.81, y, 6, 0.48)
end
-- Bottom zig-zag ghost sheet.
local zigzag = {
	{0.19, 0.76}, {0.27, 0.67}, {0.35, 0.76}, {0.43, 0.67},
	{0.51, 0.76}, {0.59, 0.67}, {0.67, 0.76}, {0.75, 0.67}, {0.81, 0.76},
}
for i = 1, #zigzag - 1 do
	local a = zigzag[i]
	local b = zigzag[i + 1]
	for step = 0, 7 do
		local n = step / 7
		local x = a[1] + (b[1] - a[1]) * n
		local y = a[2] + (b[2] - a[2]) * n
		addOutlineDot(x, y, 6, 0.48)
	end
end
-- Very faint inner ghost body, not a big circle.
local bodyFill = Instance.new("Frame")
bodyFill.Name = "SoftGhostBody"
bodyFill.Size = UDim2.new(0.62, 0, 0.60, 0)
bodyFill.Position = UDim2.new(0.19, 0, 0.16, 0)
bodyFill.BackgroundColor3 = GREEN
bodyFill.BackgroundTransparency = 0.993
bodyFill.BorderSizePixel = 0
bodyFill.ZIndex = 0
bodyFill.Parent = ghostBg
local bodyCorner = Instance.new("UICorner")
bodyCorner.CornerRadius = UDim.new(0.42, 0)
bodyCorner.Parent = bodyFill
local bodyStroke = Instance.new("UIStroke")
bodyStroke.Color = GREEN
bodyStroke.Thickness = 1
bodyStroke.Transparency = 0.97
bodyStroke.Parent = bodyFill
-- Eyes: circular neon rings with small top gaps/highlights.
local eyeDots = {}
local function makeEye(cx, cy)
	local ring = {}
	for i = 0, 20 do
		-- leave a small gap at top-left to mimic the reference
		if not (i >= 3 and i <= 6) then
			local theta = (i / 20) * math.pi * 2
			local x = cx + math.cos(theta) * 0.045
			local y = cy + math.sin(theta) * 0.045
			local d = makeDot(ghostBg, x, y, 5, 0.26, 2)
			table.insert(ring, d)
			table.insert(eyeDots, d)
			table.insert(ghostDots, d)
		end
	end
	-- small glowing accent curl on each eye
	local accent1 = makeDot(ghostBg, cx - 0.018, cy - 0.058, 5, 0.12, 2)
	local accent2 = makeDot(ghostBg, cx - 0.002, cy - 0.066, 5, 0.12, 2)
	table.insert(eyeDots, accent1)
	table.insert(eyeDots, accent2)
	table.insert(ghostDots, accent1)
	table.insert(ghostDots, accent2)
end
makeEye(0.39, 0.36)
makeEye(0.61, 0.36)
-- Angry neon brows
local angryBrowLeft = Instance.new("Frame")
angryBrowLeft.Size = UDim2.new(0, 72, 0, 5)
angryBrowLeft.Position = UDim2.new(0.39, 0, 0.285, 0)
angryBrowLeft.AnchorPoint = Vector2.new(0.5, 0.5)
angryBrowLeft.BackgroundColor3 = GREEN
angryBrowLeft.BorderSizePixel = 0
angryBrowLeft.Rotation = 24
angryBrowLeft.ZIndex = 3
angryBrowLeft.Parent = ghostBg
local angryLeftCorner = Instance.new("UICorner")
angryLeftCorner.CornerRadius = UDim.new(1, 0)
angryLeftCorner.Parent = angryBrowLeft
local angryLeftGlow = Instance.new("UIStroke")
angryLeftGlow.Color = GREEN
angryLeftGlow.Thickness = 3
angryLeftGlow.Transparency = 0.18
angryLeftGlow.Parent = angryBrowLeft
local angryBrowRight = Instance.new("Frame")
angryBrowRight.Size = UDim2.new(0, 72, 0, 5)
angryBrowRight.Position = UDim2.new(0.61, 0, 0.285, 0)
angryBrowRight.AnchorPoint = Vector2.new(0.5, 0.5)
angryBrowRight.BackgroundColor3 = GREEN
angryBrowRight.BorderSizePixel = 0
angryBrowRight.Rotation = -24
angryBrowRight.ZIndex = 3
angryBrowRight.Parent = ghostBg
local angryRightCorner = Instance.new("UICorner")
angryRightCorner.CornerRadius = UDim.new(1, 0)
angryRightCorner.Parent = angryBrowRight
local angryRightGlow = Instance.new("UIStroke")
angryRightGlow.Color = GREEN
angryRightGlow.Thickness = 3
angryRightGlow.Transparency = 0.18
angryRightGlow.Parent = angryBrowRight
-- Reference-style moving highlight following the outline.
local highlightArc = {}
for i = 1, 18 do
	local h = makeDot(ghostBg, 0.5, 0.08, 7, 0.0, 3)
	table.insert(highlightArc, h)
end
--=====================================================
--=====================================================
-- PAGE UI
--=====================================================
local pages = {}
local currentPage = "Scanner"
local function makePage(name)
	local page = Instance.new("ScrollingFrame")
	page.Name = name
	page.Size = UDim2.new(1, 0, 1, 0)
	page.BackgroundTransparency = 1
	page.BorderSizePixel = 0
	page.ScrollBarThickness = 6
	page.ScrollBarImageColor3 = GREEN
	page.CanvasSize = UDim2.new(0, 0, 0, 0)
	page.Visible = false
	page.ZIndex = 3
	page.Parent = content
	local layout = Instance.new("UIListLayout")
	layout.Padding = UDim.new(0, 6)
	layout.SortOrder = Enum.SortOrder.LayoutOrder
	layout.Parent = page
	local pad = Instance.new("UIPadding")
	pad.PaddingTop = UDim.new(0, 10)
	pad.PaddingLeft = UDim.new(0, 10)
	pad.PaddingRight = UDim.new(0, 10)
	pad.PaddingBottom = UDim.new(0, 10)
	pad.Parent = page
	layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
		page.CanvasSize = UDim2.new(0, 0, 0, layout.AbsoluteContentSize.Y + 25)
	end)
	pages[name] = page
	return page
end
local function clearPage(page)
	for _, child in ipairs(page:GetChildren()) do
		if not child:IsA("UIListLayout") and not child:IsA("UIPadding") then
			child:Destroy()
		end
	end
end
local function addLine(page, text, color, height)
	local label = Instance.new("TextLabel")
	label.Size = UDim2.new(1, -8, 0, height or 28)
	label.BackgroundColor3 = BLACK_3
	label.BackgroundTransparency = 0.68
	label.BorderSizePixel = 0
	label.Text = tostring(text)
	label.TextColor3 = color or GREEN
	label.Font = Enum.Font.Code
	label.TextSize = 14
	label.TextXAlignment = Enum.TextXAlignment.Left
	label.TextYAlignment = Enum.TextYAlignment.Center
	label.TextWrapped = true
	label.ZIndex = 4
	label.Parent = page
	local pad = Instance.new("UIPadding")
	pad.PaddingLeft = UDim.new(0, 7)
	pad.PaddingRight = UDim.new(0, 7)
	pad.Parent = label
	local s = Instance.new("UIStroke")
	s.Color = GREEN_DARK
	s.Thickness = 1
	s.Parent = label
	return label
end
local function addBlock(page, text)
	local lines = 1
	for _ in tostring(text):gmatch("\n") do
		lines += 1
	end
	local box = Instance.new("TextBox")
	box.Size = UDim2.new(1, -8, 0, math.clamp(lines * 19 + 24, 80, 360))
	box.BackgroundColor3 = BLACK_3
	box.BackgroundTransparency = 0.60
	box.BorderSizePixel = 0
	box.Text = tostring(text)
	box.TextColor3 = WHITE_GREEN
	box.Font = Enum.Font.Code
	box.TextSize = 13
	box.TextXAlignment = Enum.TextXAlignment.Left
	box.TextYAlignment = Enum.TextYAlignment.Top
	box.ClearTextOnFocus = false
	box.MultiLine = true
	box.TextEditable = false
	box.ZIndex = 4
	box.Parent = page
	local pad = Instance.new("UIPadding")
	pad.PaddingLeft = UDim.new(0, 8)
	pad.PaddingTop = UDim.new(0, 8)
	pad.PaddingRight = UDim.new(0, 8)
	pad.PaddingBottom = UDim.new(0, 8)
	pad.Parent = box
	local s = Instance.new("UIStroke")
	s.Color = GREEN_DARK
	s.Thickness = 1
	s.Parent = box
	return box
end
local function addButton(page, text, callback, color)
	local btn = Instance.new("TextButton")
	btn.Size = UDim2.new(1, -8, 0, 34)
	btn.BackgroundColor3 = BLACK
	btn.BackgroundTransparency = 0.52
	btn.BorderSizePixel = 0
	btn.Text = tostring(text)
	btn.TextColor3 = color or GREEN
	btn.Font = Enum.Font.Code
	btn.TextSize = 13
	btn.AutoButtonColor = false
	btn.ZIndex = 4
	btn.Parent = page
	local s = Instance.new("UIStroke")
	s.Color = color or GREEN
	s.Thickness = 1
	s.Parent = btn
	btn.MouseEnter:Connect(function()
		btn.BackgroundColor3 = Color3.fromRGB(0, 35, 16)
	end)
	btn.MouseLeave:Connect(function()
		btn.BackgroundColor3 = BLACK
	end)
	btn.MouseButton1Click:Connect(callback)
	return btn
end
local function switchPage(name)
	for _, page in pairs(pages) do
		page.Visible = false
	end
	currentPage = name
	pages[name].Visible = true
	if title then
		title.Text = "PROJECT GHOST // v3.4 // " .. tostring(name)
	end
end
local function addTab(name)
	local btn = Instance.new("TextButton")
	btn.Size = UDim2.new(1, 0, 0, 36)
	btn.BackgroundColor3 = BLACK
	btn.BorderSizePixel = 0
	btn.Text = "  > " .. name
	btn.TextColor3 = GREEN
	btn.Font = Enum.Font.Code
	btn.TextSize = 13
	btn.TextXAlignment = Enum.TextXAlignment.Left
	btn.AutoButtonColor = false
	btn.Parent = sidebar
	local pad = Instance.new("UIPadding")
	pad.PaddingLeft = UDim.new(0, 8)
	pad.Parent = btn
	local s = Instance.new("UIStroke")
	s.Color = GREEN_DARK
	s.Thickness = 1
	s.Parent = btn
	btn.MouseButton1Click:Connect(function()
		switchPage(name)
	end)
end
local scannerPage = makePage("Scanner")
local scriptViewerPage = makePage("Script Viewer")
local codePage = makePage("Code Registry")
local toolsPage = makePage("Tools")
local propertyPage = makePage("Property Editor")
local playerPage = makePage("Player Analyzer")
local statsScannerPage = makePage("Stats Scanner")
local trackerPage = makePage("Object Tracker")
local runtimePage = makePage("Runtime API")
local remotesPage = makePage("Remotes")
local remoteMonitorPage = makePage("Remote Monitor")
local explorerPage = makePage("Explorer")
local statsPage = makePage("Stats")
local logsPage = makePage("Logs")
for _, name in ipairs({"Scanner", "Script Viewer", "Code Registry", "Tools", "Property Editor", "Player Analyzer", "Stats Scanner", "Object Tracker", "Runtime API", "Remotes", "Remote Monitor", "Explorer", "Stats", "Logs"}) do
	addTab(name)
end
--=====================================================
-- PAGE REFRESHERS
--=====================================================
local function refreshScanner()
	clearPage(scannerPage)
	addLine(scannerPage, "GHOST SCANNER", GREEN)
	addLine(scannerPage, "Status: " .. scanStatus, WHITE_GREEN)
	if not selectedObject then
		addLine(scannerPage, "No object selected.", WARNING)
		addLine(scannerPage, "Equip Ghost Scanner, then click a part/model.")
		return
	end
	addLine(scannerPage, "Selected: " .. selectedObject.Name)
	addLine(scannerPage, "Class: " .. selectedObject.ClassName)
	addLine(scannerPage, "Path: " .. safeFullName(selectedObject), WHITE_GREEN, 48)
	addLine(scannerPage, "Descendants: " .. tostring(countDescendants(selectedObject)))
	addLine(scannerPage, "Selected By Tool: " .. tostring(selectedFromTool))
	if selectedObject:IsA("BasePart") then
		addLine(scannerPage, "Position: " .. tostring(selectedObject.Position), WHITE_GREEN)
		addLine(scannerPage, "Anchored: " .. tostring(selectedObject.Anchored))
		addLine(scannerPage, "CanCollide: " .. tostring(selectedObject.CanCollide))
	end
	local info = getGhostInfo(selectedObject)
	if info then
		addLine(scannerPage, "GhostInfo: " .. info, WHITE_GREEN, 50)
	end
	local registered = getRegisteredCodeForObject(selectedObject)
	if registered then
		addLine(scannerPage, "Registered Code: " .. registered.Name, GREEN)
		addLine(scannerPage, "Access: " .. tostring(registered.AccessLevel or "UNKNOWN"), WHITE_GREEN)
	else
		addLine(scannerPage, "Registered Code: none", WARNING)
	end
	local nearbyScripts = scanForNearbyScripts(selectedObject)
	addLine(scannerPage, "Nearby Scripts/Modules: " .. tostring(#nearbyScripts), GREEN)
	for _, scriptObj in ipairs(nearbyScripts) do
		addButton(scannerPage, "[" .. scriptObj.ClassName .. "] " .. safeFullName(scriptObj), function()
			selectedScriptObject = scriptObj
			switchPage("Script Viewer")
		end, WHITE_GREEN)
	end
end
local function refreshScriptViewer()
	clearPage(scriptViewerPage)
	addLine(scriptViewerPage, "SCRIPT VIEWER", GREEN)
	if not selectedObject then
		addLine(scriptViewerPage, "No object selected.", WARNING)
		return
	end
	addLine(scriptViewerPage, "Target Object: " .. selectedObject.Name)
	addLine(scriptViewerPage, "Target Path: " .. safeFullName(selectedObject), WHITE_GREEN, 45)
	local nearbyScripts = scanForNearbyScripts(selectedObject)
	if #nearbyScripts > 0 then
		addLine(scriptViewerPage, "Detected scripts/modules:", GREEN)
		for _, scriptObj in ipairs(nearbyScripts) do
			addButton(scriptViewerPage, "[" .. scriptObj.ClassName .. "] " .. safeFullName(scriptObj), function()
				selectedScriptObject = scriptObj
				refreshScriptViewer()
			end, WHITE_GREEN)
		end
	else
		addLine(scriptViewerPage, "No nearby scripts/modules found.", WARNING)
	end
	addLine(scriptViewerPage, " ")
	local viewTarget = selectedScriptObject or selectedObject
	addLine(scriptViewerPage, "View Target: " .. viewTarget.Name)
	addLine(scriptViewerPage, "Class: " .. viewTarget.ClassName)
	local source, sourceType = getGhostSource(viewTarget)
	if source then
		addLine(scriptViewerPage, "Source Type: " .. sourceType, GREEN)
		addBlock(scriptViewerPage, source)
	else
		addLine(scriptViewerPage, "No readable source registered.", WARNING)
		addLine(scriptViewerPage, "Add Attribute GhostSource or add entry to CODE_REGISTRY.", WHITE_GREEN, 42)
	end
	if viewTarget:IsA("ModuleScript") then
		addLine(scriptViewerPage, " ")
		addButton(scriptViewerPage, "REFLECT MODULE METADATA", function()
			local reflected, msg = tryReflectModule(viewTarget)
			if reflected then
				viewTarget:SetAttribute("GhostLastReflection", reflected)
				pushLog("Module reflected: " .. viewTarget.Name)
			else
				pushLog("Module reflection failed: " .. msg)
			end
			refreshScriptViewer()
		end)
		local reflected = viewTarget:GetAttribute("GhostLastReflection")
		if typeof(reflected) == "string" and reflected ~= "" then
			addLine(scriptViewerPage, "Module Reflection:", GREEN)
			addBlock(scriptViewerPage, reflected)
		else
			addLine(scriptViewerPage, "Set GhostInspectable = true on ModuleScript to allow reflection.", WARNING, 45)
		end
	end
end
local function refreshCodeRegistry()
	clearPage(codePage)
	addLine(codePage, "CODE REGISTRY", GREEN)
	addLine(codePage, "Viewable hacking-game code entries.", WHITE_GREEN)
	for _, entry in ipairs(CODE_REGISTRY) do
		addLine(codePage, "Entry: " .. entry.Name, GREEN)
		addLine(codePage, "Access: " .. tostring(entry.AccessLevel or "UNKNOWN"))
		addLine(codePage, "Info: " .. tostring(entry.Description), WHITE_GREEN, 42)
		addBlock(codePage, entry.Source)
	end
end
local function refreshTools()
	clearPage(toolsPage)
	addLine(toolsPage, "OBJECT TOOLS", GREEN)
	addButton(toolsPage, "UNDO", function()
		undoLast()
		refreshTools()
	end)
	addButton(toolsPage, "REDO", function()
		redoLast()
		refreshTools()
	end)
	if selectedObject then
		addLine(toolsPage, "Selected: " .. selectedObject.Name)
		addLine(toolsPage, "Class: " .. selectedObject.ClassName)
	else
		addLine(toolsPage, "Selected: none", WARNING)
	end
	addButton(toolsPage, "DUPLICATE SELECTED OBJECT", function()
		local ok, msg = cloneSelectedObject()
		if not ok then warn("[Project Ghost]", msg) end
		refreshTools()
	end)
	addButton(toolsPage, "DELETE SELECTED CLIENT COPY", function()
		if not selectedObject then
			pushLog("Delete blocked: no selected object.")
			return
		end
		local obj = selectedObject
		if obj:IsDescendantOf(workspace) and obj ~= workspace then
			local name = obj.Name
			local backup = nil
			local parent = obj.Parent
			local cf = getPivotOrCFrame(obj)
			pcall(function()
				backup = obj:Clone()
			end)
			pcall(function() obj:Destroy() end)
			if backup then
				pushUndo({Type = "Delete", Clone = backup, Parent = parent, CFrame = cf})
			end
			selectedObject = nil
			selectedScriptObject = nil
			pushLog("Destroyed client-visible object: " .. name)
		else
			pushLog("Delete blocked: unsafe target.")
		end
		refreshTools()
	end, BAD)
	addButton(toolsPage, "OPEN PROPERTY EDITOR", function()
	switchPage("Property Editor")
	refreshPropertyEditor()
end)
addButton(toolsPage, "OPEN PLAYER ANALYZER", function()
	switchPage("Player Analyzer")
	refreshPlayerAnalyzer()
end)

addButton(toolsPage, "OPEN STATS SCANNER", function()
	switchPage("Stats Scanner")
	refreshStatsScanner()
end)
addButton(toolsPage, "BLACKOUT LIGHTING", function()
		Lighting.ClockTime = 0
		Lighting.Brightness = 0.5
		pushLog("Lighting blackout applied.")
	end)
	addButton(toolsPage, "RESTORE LIGHTING", function()
		Lighting.ClockTime = 14
		Lighting.Brightness = 2
		pushLog("Lighting restored.")
	end)
	addLine(toolsPage, " ")
	addLine(toolsPage, "VEHICLE SPAWNER", GREEN)
	addLine(toolsPage, "Selected Vehicle: " .. selectedVehicle)
	addLine(toolsPage, "SpawnCar Remote: " .. (SpawnCar and "FOUND" or "NOT FOUND"), SpawnCar and GREEN or BAD)
	for _, vehicle in ipairs(vehicles) do
		addButton(toolsPage, "Select " .. vehicle, function()
			selectedVehicle = vehicle
			refreshTools()
		end)
	end
	addButton(toolsPage, "SPAWN SELECTED VEHICLE", function()
		SpawnCar = getSpawnCarRemote(1)
		if not SpawnCar then
			pushLog("Spawn remote missing.")
			return
		end
		if os.clock() - lastSpawn < spawnCooldown then
			pushLog("Spawn blocked: cooldown active.")
			return
		end
		lastSpawn = os.clock()
		local ok, err = pcall(function()
			SpawnCar:FireServer(0, selectedVehicle)
		end)
		if ok then
			pushLog("Spawn request sent: " .. selectedVehicle)
			pushRemoteLog("FireServer | SpawnCar | 0, " .. selectedVehicle)
		else
			pushLog("Spawn error: " .. tostring(err))
		end
	end)
end
local function refreshPropertyEditor()
	clearPage(propertyPage)
	addLine(propertyPage, "PROPERTY EDITOR", GREEN)
	if not selectedObject then
		addLine(propertyPage, "No selected object.", WARNING)
		addLine(propertyPage, "Scan something first with Ghost Scanner.")
		return
	end
	addLine(propertyPage, "Selected: " .. selectedObject.Name)
	addLine(propertyPage, "Class: " .. selectedObject.ClassName)
	addLine(propertyPage, "Path: " .. safeFullName(selectedObject), WHITE_GREEN, 46)
	local rows = editablePropertyRows(selectedObject)
	if #rows == 0 then
		addLine(propertyPage, "No editable properties.", WARNING)
		addLine(propertyPage, "Useful targets: Parts, MeshParts, Humanoids.", WHITE_GREEN)
		return
	end
	for _, row in ipairs(rows) do
		local prop = row[1]
		local typ = row[2]
		local value = "?"
		pcall(function()
			value = tostring(selectedObject[prop])
		end)
		addLine(propertyPage, prop .. " = " .. value, WHITE_GREEN)
		if typ == "bool" then
			addButton(propertyPage, "Toggle " .. prop, function()
				toggleBoolProperty(selectedObject, prop)
				refreshPropertyEditor()
			end)
		elseif typ == "number" then
			addButton(propertyPage, prop .. " +1", function()
				stepNumberProperty(selectedObject, prop, 1)
				refreshPropertyEditor()
			end)
			addButton(propertyPage, prop .. " -1", function()
				stepNumberProperty(selectedObject, prop, -1)
				refreshPropertyEditor()
			end)
		end
	end
end
local function refreshPlayerAnalyzer()
	clearPage(playerPage)
	addLine(playerPage, "PLAYER ANALYZER", GREEN)
	local targetModel = nil
	if selectedObject then
		if selectedObject:IsA("Model") then
			targetModel = selectedObject
		elseif selectedObject.Parent and selectedObject.Parent:IsA("Model") then
			targetModel = selectedObject.Parent
		end
	end
	if not targetModel then
		addLine(playerPage, "No player/character selected.", WARNING)
		addLine(playerPage, "Select target.")
	else
		addLine(playerPage, "Selected Model: " .. targetModel.Name)
		local humanoid = targetModel:FindFirstChildOfClass("Humanoid")
		local root = targetModel:FindFirstChild("HumanoidRootPart")
		if humanoid then
			addLine(playerPage, "Humanoid Health: " .. tostring(math.floor(humanoid.Health)) .. "/" .. tostring(math.floor(humanoid.MaxHealth)))
			addLine(playerPage, "WalkSpeed: " .. tostring(humanoid.WalkSpeed))
			addLine(playerPage, "JumpPower: " .. tostring(humanoid.JumpPower))
			addLine(playerPage, "State: " .. tostring(humanoid:GetState()))
		end
		if root then
			addLine(playerPage, "Position: " .. tostring(root.Position), WHITE_GREEN, 42)
		end
		local targetPlayer = Players:GetPlayerFromCharacter(targetModel)
		if targetPlayer then
			addLine(playerPage, "Player: " .. targetPlayer.Name)
			addLine(playerPage, "DisplayName: " .. targetPlayer.DisplayName)
			addLine(playerPage, "UserId: " .. tostring(targetPlayer.UserId))
			addLine(playerPage, "AccountAge: " .. tostring(targetPlayer.AccountAge))
		else
			addLine(playerPage, "Type: NPC / non-player character", WHITE_GREEN)
		end
	end
	addLine(playerPage, " ")
	addLine(playerPage, "ONLINE PLAYERS", GREEN)
	for _, plr in ipairs(Players:GetPlayers()) do
		addButton(playerPage, plr.Name .. " // " .. plr.DisplayName, function()
			if plr.Character then
				selectObject(plr.Character, false)
				refreshPlayerAnalyzer()
			end
		end, WHITE_GREEN)
	end
end



function applySelectedStatAndRefresh(value)
	if typeof(setSelectedStatValue) ~= "function" then
		pushLog("Stat setter missing.")
		return
	end

	setSelectedStatValue(value)
	scanStatsAndCurrency()
	refreshStatsScanner()
end

function applySelectedStatDelta(delta)
	if not selectedStatObject then
		pushLog("No stat selected.")
		return
	end

	local current = tonumber(selectedStatObject.Value) or 0
	applySelectedStatAndRefresh(current + delta)
end


refreshStatsScanner = function()
	clearPage(statsScannerPage)
	addLine(statsScannerPage, "STATS SCANNER", GREEN)
	addLine(statsScannerPage, "Currency, time, points.", WHITE_GREEN)

	addButton(statsScannerPage, "SCAN STATS", function()
		scanStatsAndCurrency()
		refreshStatsScanner()
	end)

	if #statScanResults == 0 then
		scanStatsAndCurrency()
	end

	addLine(statsScannerPage, "Found: " .. tostring(#statScanResults), GREEN)

	for _, stat in ipairs(statScanResults) do
		addButton(statsScannerPage, stat.Name .. " = " .. stat.Value .. " // " .. stat.Source, function()
			selectedStatObject = stat
			refreshStatsScanner()
		end, WHITE_GREEN)
	end

	addLine(statsScannerPage, " ")

	if selectedStatObject then
		addLine(statsScannerPage, "SELECTED STAT", GREEN)
		addLine(statsScannerPage, "Name: " .. selectedStatObject.Name)
		addLine(statsScannerPage, "Value: " .. tostring(selectedStatObject.Value))
		addLine(statsScannerPage, "Type: " .. selectedStatObject.ClassName)
		addLine(statsScannerPage, "Source: " .. selectedStatObject.Source)
		-- path removed for register optimization

		addButton(statsScannerPage, "SET 0", function()
			applySelectedStatAndRefresh(0)
		end)

		addButton(statsScannerPage, "SET 100", function()
			applySelectedStatAndRefresh(100)
		end)

		addButton(statsScannerPage, "SET 1000", function()
			applySelectedStatAndRefresh(1000)
		end)

		addButton(statsScannerPage, "+100", function()
			applySelectedStatDelta(100)
		end)

		addButton(statsScannerPage, "-100", function()
			applySelectedStatDelta(-100)
		end)

		addLine(statsScannerPage, "Local only unless server allows it.", WARNING, 42)
	else
		addLine(statsScannerPage, "Select stat.", WARNING)
	end
end

local function refreshObjectTracker()
	clearPage(trackerPage)
	addLine(trackerPage, "OBJECT TRACKER", GREEN)
	addButton(trackerPage, "START TRACKER", function()
		startObjectTracker()
		refreshObjectTracker()
	end)
	addLine(trackerPage, "Status: " .. (trackedObjects.started and "RUNNING" or "OFF"), trackedObjects.started and GREEN or WARNING)
	addLine(trackerPage, "Tracking objects.", WHITE_GREEN, 42)
	addLine(trackerPage, " ")
	addLine(trackerPage, "SCAN HISTORY", GREEN)
	addButton(trackerPage, "SHOW LATEST DIFF", function()
		pushObjectTrack(diffLatestScans())
		refreshObjectTracker()
	end)
	for i = 1, math.min(10, #scanHistory) do
		local item = scanHistory[i]
		addButton(trackerPage, item.Time .. " // " .. item.Name, function()
			pushObjectTrack("Selected history: " .. item.Path)
			refreshObjectTracker()
		end, WHITE_GREEN)
	end
	addLine(trackerPage, " ")
	addLine(trackerPage, "RECENT OBJECT EVENTS", GREEN)
	if #trackedObjectLog == 0 then
		addLine(trackerPage, "No tracked events yet.", WARNING)
	else
		for i = 1, math.min(35, #trackedObjectLog) do
			addLine(trackerPage, trackedObjectLog[i], WHITE_GREEN, 38)
		end
	end
end
local function refreshRuntimeAPI()
	clearPage(runtimePage)
	addLine(runtimePage, "RUNTIME API", GREEN)
	addLine(runtimePage, "Safe compatibility check.", WHITE_GREEN)
	addBlock(runtimePage, getRuntimeAPIReport())
	addLine(runtimePage, "Executor-only APIs are not part of Roblox Studio.", WARNING, 42)
	addLine(runtimePage, "Project Ghost uses normal LocalScript-safe systems only.", WHITE_GREEN, 42)
end
local function refreshRemotes()
	clearPage(remotesPage)
	addLine(remotesPage, "REMOTE SCANNER", GREEN)
	local count = 0
	for _, obj in ipairs(game:GetDescendants()) do
		if obj:IsA("RemoteEvent") or obj:IsA("RemoteFunction") then
			count += 1
			addLine(remotesPage, "[" .. obj.ClassName .. "] " .. safeFullName(obj), WHITE_GREEN, 40)
		end
	end
	addLine(remotesPage, "Total remotes: " .. tostring(count), GREEN)
	addLine(remotesPage, " ")
	addLine(remotesPage, "REMOTE LOGS", GREEN)
	if #remoteLogs == 0 then
		addLine(remotesPage, "No remote logs yet.", WARNING)
		addLine(remotesPage, "Use _G.ProjectGhost_LogRemote(name, direction, ...) in your own scripts.", WHITE_GREEN, 42)
	else
		for _, line in ipairs(remoteLogs) do
			addLine(remotesPage, line, WHITE_GREEN)
		end
	end
end
local function refreshRemoteMonitor()
	clearPage(remoteMonitorPage)
	scanAllRemotes()
	addLine(remoteMonitorPage, "LIVE REMOTE MONITOR // USABLE ONLY", GREEN)
	addLine(remoteMonitorPage, "Usable remotes only.", WHITE_GREEN, 50)
	local list = {}
	for _, stat in pairs(remoteStats) do
		if stat.level ~= "LOCKED" then
			table.insert(list, stat)
		end
	end
	table.sort(list, function(a, b)
		if a.score == b.score then
			return a.path < b.path
		end
		return a.score > b.score
	end)
	if #list == 0 then
		addLine(remoteMonitorPage, "No remotes found.", WARNING)
		return
	end
	for _, stat in ipairs(list) do
		local color = GREEN
		if stat.level == "USABLE" then
			color = GREEN
		elseif stat.level == "TESTABLE" then
			color = WHITE_GREEN
		elseif stat.level == "CLIENT" then
			color = Color3.fromRGB(80, 210, 255)
		else
			color = WARNING
		end
		addButton(remoteMonitorPage, "[" .. stat.level .. "] " .. stat.name .. " // " .. stat.category .. " // Score " .. tostring(stat.score), function()
			for _, obj in ipairs(game:GetDescendants()) do
				if (obj:IsA("RemoteEvent") or obj:IsA("RemoteFunction")) and safeFullName(obj) == stat.path then
					selectedRemoteObject = obj
					break
				end
			end
			refreshRemoteMonitor()
		end, color)
	end
	addLine(remoteMonitorPage, " ")
	if selectedRemoteObject then
		local stat = getRemoteStats(selectedRemoteObject)
		addLine(remoteMonitorPage, "SELECTED REMOTE DETAILS", GREEN)
		addLine(remoteMonitorPage, "Name: " .. stat.name)
		addLine(remoteMonitorPage, "Class: " .. stat.className)
		addLine(remoteMonitorPage, "Use Status: " .. stat.level, stat.level == "USABLE" and GREEN or stat.level == "CLIENT" and Color3.fromRGB(80, 210, 255) or WHITE_GREEN)
		addLine(remoteMonitorPage, "Category: " .. stat.category)
		addLine(remoteMonitorPage, "Score: " .. tostring(stat.score))
		addLine(remoteMonitorPage, "Path: " .. stat.path, WHITE_GREEN, 48)
		addLine(remoteMonitorPage, "Logged Calls: " .. tostring(stat.callCount))
		addLine(remoteMonitorPage, "Last Args: " .. tostring(stat.lastArgs), WHITE_GREEN, 44)
		addLine(remoteMonitorPage, "Last Time: " .. tostring(stat.lastTime))
		local sourceText = "-- Remote Analysis\\n"
			.. "Remote: " .. stat.name .. "\\n"
			.. "Type: " .. stat.className .. "\\n"
			.. "Use Status: " .. stat.level .. "\\n"
			.. "Category: " .. stat.category .. "\\n\\n"
			.. "-- This is a monitor view. It does not auto-fire unknown remotes.\\n"
			.. "-- Add safe test actions manually inside your game's Tools page."
		addBlock(remoteMonitorPage, sourceText)
		addButton(remoteMonitorPage, "PIN REMOTE TO LOGS", function()
			pushLog("Pinned remote: " .. stat.path .. " [" .. stat.level .. " / " .. stat.category .. "]")
			switchPage("Logs")
			refreshLogs()
		end)
		addButton(remoteMonitorPage, "SELECT REMOTE AS TARGET", function()
			selectObject(selectedRemoteObject, false)
			switchPage("Scanner")
			refreshScanner()
		end)
	else
		addLine(remoteMonitorPage, "Select remote.", WARNING)
	end
	addLine(remoteMonitorPage, " ")
	addLine(remoteMonitorPage, "RECENT REMOTE ACTIVITY", GREEN)
	if #remoteLogs == 0 then
		addLine(remoteMonitorPage, "No remote calls logged yet.", WARNING)
	else
		for i = 1, math.min(12, #remoteLogs) do
			addLine(remoteMonitorPage, remoteLogs[i], WHITE_GREEN)
		end
	end
end
local function refreshExplorer()
	clearPage(explorerPage)
	addLine(explorerPage, "VISIBLE EXPLORER", GREEN)
	local roots = {
		workspace,
		RS,
		Lighting,
		game:GetService("StarterGui"),
		game:GetService("StarterPlayer"),
	}
	for _, root in ipairs(roots) do
		addLine(explorerPage, "▼ " .. root.Name, GREEN)
		for _, child in ipairs(root:GetChildren()) do
			addButton(explorerPage, "[" .. child.ClassName .. "] " .. child.Name, function()
				selectObject(child, false)
				switchPage("Scanner")
				refreshScanner()
			end, WHITE_GREEN)
		end
	end
end
local function refreshStats()
	clearPage(statsPage)
	addLine(statsPage, "RUNTIME STATS", GREEN)
	addLine(statsPage, "FPS: " .. tostring(fps))
	addLine(statsPage, "Memory: " .. string.format("%.2f MB", Stats:GetTotalMemoryUsageMb()))
	addLine(statsPage, "Players: " .. tostring(#Players:GetPlayers()))
	addLine(statsPage, "Objects: " .. tostring(#game:GetDescendants()))
	addLine(statsPage, "Selected: " .. (selectedObject and selectedObject.Name or "none"))
	addLine(statsPage, "Selected Script: " .. (selectedScriptObject and selectedScriptObject.Name or "none"))
	addLine(statsPage, "Logs: " .. tostring(#ghostLogs))
	addLine(statsPage, "Remote Logs: " .. tostring(#remoteLogs))
end
local function refreshLogs()
	clearPage(logsPage)
	addLine(logsPage, "GHOST LOGS", GREEN)
	for _, line in ipairs(ghostLogs) do
		addLine(logsPage, line, WHITE_GREEN)
	end
	if #ghostLogs == 0 then
		addLine(logsPage, "No logs.")
	end
end
refreshCurrent = function()
	if currentPage == "Scanner" then refreshScanner() end
	if currentPage == "Script Viewer" then refreshScriptViewer() end
	if currentPage == "Code Registry" then refreshCodeRegistry() end
	if currentPage == "Tools" then refreshTools() end
	if currentPage == "Property Editor" then refreshPropertyEditor() end
	if currentPage == "Player Analyzer" then refreshPlayerAnalyzer() end
	if currentPage == "Stats Scanner" then refreshStatsScanner() end
	if currentPage == "Object Tracker" then refreshObjectTracker() end
	if currentPage == "Runtime API" then refreshRuntimeAPI() end
	if currentPage == "Remotes" then refreshRemotes() end
	if currentPage == "Remote Monitor" then refreshRemoteMonitor() end
	if currentPage == "Explorer" then refreshExplorer() end
	if currentPage == "Stats" then refreshStats() end
	if currentPage == "Logs" then refreshLogs() end
end
if not quickActionsReady then
	quickActionsReady = true
	makeQuickButton("SPAWN VEHICLE", 120, function()
		switchPage("Tools")
		refreshTools()
	end)
	makeQuickButton("REMOTE MONITOR", 275, function()
		switchPage("Remote Monitor")
		refreshRemoteMonitor()
	end)
	makeQuickButton("PROPERTY EDITOR", 430, function()
		switchPage("Property Editor")
		refreshPropertyEditor()
	end)
	makeQuickButton("PLAYER INFO", 585, function()
		switchPage("Player Analyzer")
		refreshPlayerAnalyzer()
	end)
	makeQuickButton("OBJECT TRACKER", 740, function()
		switchPage("Object Tracker")
		refreshObjectTracker()
	end)
end
--=====================================================
-- DRAGGABLE PANEL
--=====================================================
local draggingGhostPanel = false
local dragStartPosition = nil
local panelStartPosition = nil
top.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		draggingGhostPanel = true
		dragStartPosition = input.Position
		panelStartPosition = main.Position
		input.Changed:Connect(function()
			if input.UserInputState == Enum.UserInputState.End then
				draggingGhostPanel = false
			end
		end)
	end
end)
UIS.InputChanged:Connect(function(input)
	if draggingGhostPanel and input.UserInputType == Enum.UserInputType.MouseMovement then
		local delta = input.Position - dragStartPosition
		main.Position = UDim2.new(
			panelStartPosition.X.Scale,
			panelStartPosition.X.Offset + delta.X,
			panelStartPosition.Y.Scale,
			panelStartPosition.Y.Offset + delta.Y
		)
		clampPanelToScreen()
	end
end)
--=====================================================
-- ACTION MENU
--=====================================================

actionMenu = nil
actionTitle = nil

local function buildActionMenu()
	actionMenu = Instance.new("Frame")
	actionMenu.Name = "GhostTargetActionMenu"
	actionMenu.Size = UDim2.new(0, 155, 0, 245)
	actionMenu.BackgroundColor3 = BLACK
	actionMenu.BorderSizePixel = 0
	actionMenu.Visible = false
	actionMenu.ZIndex = 50
	actionMenu.Parent = gui

	local menuStroke = Instance.new("UIStroke")
	menuStroke.Color = GREEN
	menuStroke.Thickness = 2
	menuStroke.Parent = actionMenu

	local menuCorner = Instance.new("UICorner")
	menuCorner.CornerRadius = UDim.new(0, 6)
	menuCorner.Parent = actionMenu

	local layout = Instance.new("UIListLayout")
	layout.Padding = UDim.new(0, 3)
	layout.SortOrder = Enum.SortOrder.LayoutOrder
	layout.Parent = actionMenu

	local padding = Instance.new("UIPadding")
	padding.PaddingTop = UDim.new(0, 5)
	padding.PaddingLeft = UDim.new(0, 5)
	padding.PaddingRight = UDim.new(0, 5)
	padding.PaddingBottom = UDim.new(0, 5)
	padding.Parent = actionMenu

	actionTitle = Instance.new("TextLabel")
	actionTitle.Size = UDim2.new(1, 0, 0, 21)
	actionTitle.BackgroundColor3 = BLACK
	actionTitle.BorderSizePixel = 0
	actionTitle.Text = "TARGET ACTIONS"
	actionTitle.TextColor3 = WHITE_GREEN
	actionTitle.Font = Enum.Font.Code
	actionTitle.TextSize = 10
	actionTitle.TextXAlignment = Enum.TextXAlignment.Left
	actionTitle.ZIndex = 51
	actionTitle.Parent = actionMenu

	local titlePadding = Instance.new("UIPadding")
	titlePadding.PaddingLeft = UDim.new(0, 5)
	titlePadding.Parent = actionTitle

	local function addMenuButton(text, callback, color)
		local b = Instance.new("TextButton")
		b.Size = UDim2.new(1, 0, 0, 23)
		b.BackgroundColor3 = BLACK_2
		b.BorderSizePixel = 0
		b.Text = text
		b.TextColor3 = color or GREEN
		b.Font = Enum.Font.Code
		b.TextSize = 10
		b.TextXAlignment = Enum.TextXAlignment.Left
		b.AutoButtonColor = false
		b.ZIndex = 51
		b.Parent = actionMenu

		local bp = Instance.new("UIPadding")
		bp.PaddingLeft = UDim.new(0, 8)
		bp.Parent = b

		local bs = Instance.new("UIStroke")
		bs.Color = color or GREEN
		bs.Thickness = 1
		bs.Transparency = 0.35
		bs.Parent = b

		b.MouseEnter:Connect(function()
			b.BackgroundColor3 = Color3.fromRGB(0, 35, 16)
		end)

		b.MouseLeave:Connect(function()
			b.BackgroundColor3 = BLACK_2
		end)

		b.MouseButton1Click:Connect(function()
			actionMenu.Visible = false
			callback()
		end)

		return b
	end

	addMenuButton("VIEW DETAILS", function()
		if selectedObject then
			main.Visible = true
			switchPage("Scanner")
			refreshScanner()
		end
	end)

	addMenuButton("SCRIPT VIEWER", function()
		if selectedObject then
			main.Visible = true
			switchPage("Script Viewer")
			refreshScriptViewer()
		end
	end)

	addMenuButton("PROPERTY EDITOR", function()
		if selectedObject then
			main.Visible = true
			switchPage("Property Editor")
			refreshPropertyEditor()
		end
	end)

	addMenuButton("PLAYER INFO", function()
		if selectedObject then
			main.Visible = true
			switchPage("Player Analyzer")
			refreshPlayerAnalyzer()
		end
	end)

	addMenuButton("STATS SCANNER", function()
		main.Visible = true
		switchPage("Stats Scanner")
		refreshStatsScanner()
	end)

	addMenuButton("REMOTE MONITOR", function()
		main.Visible = true
		switchPage("Remote Monitor")
		refreshRemoteMonitor()
	end)

	addMenuButton("DUPE / CLONE", function()
		if selectedObject then
			local ok, msg = cloneSelectedObject()
			pushLog("Dupe: " .. tostring(msg))
			main.Visible = true
			switchPage("Tools")
			refreshTools()
		end
	end)

	addMenuButton("PIN PATH", function()
		if selectedObject then
			pushLog("Pinned: " .. safeFullName(selectedObject))
			main.Visible = true
			switchPage("Logs")
			refreshLogs()
		end
	end)

	addMenuButton("DELETE COPY", function()
		if selectedObject and selectedObject:IsDescendantOf(workspace) and selectedObject ~= workspace then
			local old = selectedObject
			local name = old.Name
			local backup = nil
			local parent = old.Parent
			local cf = getPivotOrCFrame(old)

			pcall(function()
				backup = old:Clone()
			end)

			pcall(function()
				old:Destroy()
			end)

			if backup then
				pushUndo({Type = "Delete", Clone = backup, Parent = parent, CFrame = cf})
			end

			selectedObject = nil
			pushLog("Deleted: " .. name)
			main.Visible = true
			switchPage("Logs")
			refreshLogs()
		else
			pushLog("Delete blocked.")
		end
	end, BAD)

	addMenuButton("CLOSE", function()
		actionMenu.Visible = false
	end, WHITE_GREEN)
end

buildActionMenu()

function getActionMenuAdornee()
	if not selectedObject then
		return nil
	end

	if selectedObject:IsA("BasePart") then
		return selectedObject
	end

	if selectedObject:IsA("Model") then
		return selectedObject.PrimaryPart or selectedObject:FindFirstChildWhichIsA("BasePart", true)
	end

	if selectedObject.Parent and selectedObject.Parent:IsA("Model") then
		return selectedObject.Parent.PrimaryPart or selectedObject.Parent:FindFirstChildWhichIsA("BasePart", true)
	end

	return nil
end

function updateActionMenuPosition()
	if not actionMenu or not actionMenu.Visible then
		return
	end

	if not selectedObject then
		actionMenu.Visible = false
		return
	end

	camera = workspace.CurrentCamera
	if not camera then
		return
	end

	part = actionMenuFollowPart or getActionMenuAdornee()
	if not part or not part:IsDescendantOf(game) then
		actionMenu.Visible = false
		return
	end

	worldPos = part.Position + Vector3.new(0, math.max(part.Size.Y + 2, 5), 0)
	screenPos, onScreen = camera:WorldToViewportPoint(worldPos)

	if not onScreen then
		actionMenu.Visible = false
		return
	end

	local viewport = camera.ViewportSize
	local x = math.clamp(screenPos.X - 77, 8, viewport.X - 163)
	local y = math.clamp(screenPos.Y - 24, 8, viewport.Y - 253)

	actionMenu.Position = UDim2.new(0, x, 0, y)
end

function showActionMenuForSelected()
	if not selectedObject then
		actionMenu.Visible = false
		return
	end

	actionMenuFollowPart = getActionMenuAdornee()
	actionMenu.Parent = gui
	actionMenu.Size = UDim2.new(0, 155, 0, 245)
	actionTitle.Text = "TARGET: " .. string.sub(selectedObject.Name, 1, 18)
	actionMenu.Visible = true
	updateActionMenuPosition()
end

UIS.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then return end

	if input.UserInputType == Enum.UserInputType.MouseButton1 and actionMenu and actionMenu.Visible then
		local pos = UIS:GetMouseLocation()
		local inside =
			pos.X >= actionMenu.AbsolutePosition.X and
			pos.X <= actionMenu.AbsolutePosition.X + actionMenu.AbsoluteSize.X and
			pos.Y >= actionMenu.AbsolutePosition.Y and
			pos.Y <= actionMenu.AbsolutePosition.Y + actionMenu.AbsoluteSize.Y

		if not inside then
			actionMenu.Visible = false
		end
	end
end)

--=====================================================
-- SCANNER TOOL
--=====================================================
local tool = nil
local equipped = false
local function createGhostScannerTool()
	local oldTool = backpack:FindFirstChild("Ghost Scanner")
	if oldTool then oldTool:Destroy() end
	if player.Character then
		local charTool = player.Character:FindFirstChild("Ghost Scanner")
		if charTool then charTool:Destroy() end
	end
	tool = Instance.new("Tool")
	tool.Name = "Ghost Scanner"
	tool.RequiresHandle = true
	tool.CanBeDropped = false
	tool.ToolTip = "Click target"
	tool.Parent = backpack
	local handle = Instance.new("Part")
	handle.Name = "Handle"
	handle.Size = Vector3.new(0.4, 1.2, 0.4)
	handle.Material = Enum.Material.Neon
	handle.Color = GREEN
	handle.CanCollide = false
	handle.Parent = tool
	local light = Instance.new("PointLight")
	light.Color = GREEN
	light.Brightness = 2
	light.Range = 8
	light.Parent = handle
	tool.Equipped:Connect(function()
		equipped = true
		pushLog("Scanner equipped.")
	end)
	tool.Unequipped:Connect(function()
		equipped = false
		actionMenu.Visible = false
		pushLog("Scanner unequipped.")
	end)
	tool.Activated:Connect(function()
		if not equipped then return end
		scanToken += 1
		local thisToken = scanToken
		scanStatus = "SCANNING"
		local target = findTargetFromMouse()
		if not target then
			pushLog("No target.")
			return
		end
		local fixedTarget = target
		selectObject(fixedTarget, true)
		task.delay(0.05, function()
			if thisToken ~= scanToken then return end
			if selectedObject ~= fixedTarget then return end
			scanStatus = "DONE"
			showActionMenuForSelected()
		end)
	end)
	pushLog("Scanner ready.")
end
createGhostScannerTool()
player.CharacterAdded:Connect(function()
	task.wait(0.6)
	if gui and gui.Parent then
		createGhostScannerTool()
	end
end)
--=====================================================
-- BUTTONS / LOOPS
--=====================================================
refreshBtn.MouseButton1Click:Connect(smartRefresh)
ghostIcon.MouseButton1Click:Connect(function()
	main.Visible = not main.Visible
	if main.Visible then
		refreshCurrent()
	end
end)
minimizeBtn.MouseButton1Click:Connect(function()
	main.Visible = false
end)
closeBtn.MouseButton1Click:Connect(function()
	actionMenu.Visible = false
	main.Visible = false
	if tool then
		pcall(function()
			tool:Destroy()
		end)
	end
	if gui then
		pcall(function()
			gui:Destroy()
		end)
	end
end)
RunService.RenderStepped:Connect(function()
	local now = os.clock()
	fps = math.floor(1 / math.max(now - lastFrame, 0.001))
	lastFrame = now
	if targetText then
		targetText.Text = "TARGET: " .. (selectedObject and string.sub(selectedObject.Name, 1, 28) or "none")
	end
	if uptimeText then
		local alive = math.floor(os.clock())
		local m = math.floor(alive / 60)
		local s = alive % 60
		uptimeText.Text = string.format("UPTIME: %02d:%02d", m, s)
	end
	if terminalLine then
		terminalLine.Text = "ghost@terminal:~$ " .. tostring(currentPage) .. " // " .. tostring(#remoteLogs) .. " remote logs"
	end
	updateActionMenuPosition()
	if main.Visible and #outlineDots > 0 then
		local speed = os.clock() * 0.45
		local count = #outlineDots
		local head = (math.floor(speed * count) % count) + 1
		for i, dot in ipairs(outlineDots) do
			local distance = math.abs(i - head)
			local wrapDistance = count - distance
			local d = math.min(distance, wrapDistance)
			if d < 4 then
				dot.BackgroundTransparency = 0.02
				dot.Size = UDim2.new(0, 9, 0, 9)
			elseif d < 10 then
				dot.BackgroundTransparency = 0.20
				dot.Size = UDim2.new(0, 7, 0, 7)
			else
				dot.BackgroundTransparency = 0.68
				dot.Size = UDim2.new(0, 5, 0, 5)
			end
		end
		-- Smooth bright segment riding along the outline, like a neon stroke.
		for i, h in ipairs(highlightArc) do
			local idx = ((head + i - 2) % count) + 1
			local ref = outlineDots[idx]
			h.Position = ref.Position
			h.BackgroundTransparency = math.clamp((i - 1) / #highlightArc, 0, 0.75)
			h.Size = UDim2.new(0, 10 - math.floor(i / 4), 0, 10 - math.floor(i / 4))
		end
		local pulse = (math.sin(os.clock() * 3) + 1) / 2
		for _, eye in ipairs(eyeDots) do
			eye.BackgroundTransparency = 0.38 - pulse * 0.08
		end
		if bodyFill then
			bodyFill.BackgroundTransparency = 0.975 - pulse * 0.015
			bodyStroke.Transparency = 0.88 - pulse * 0.08
		end
	end
end)
task.spawn(function()
	while gui.Parent do
		if main.Visible then
			smartRefresh()
		end
		task.wait(0.5)
	end
end)
UIS.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then return end
	if input.KeyCode == TOGGLE_KEY then
		main.Visible = not main.Visible
		if main.Visible then
			clampPanelToScreen()
			smartRefresh()
		end
	elseif input.KeyCode == Enum.KeyCode.Home then
		resetPanelPosition()
	end
end)
-- Startup
scanAllRemotes()
pushLog("Ghost online.")
pushLog("Remote monitor active.")
pushLog("Modules loaded.")
pushLog("Stats scanner ready.")
pushLog("Ghost Scanner tool added to Backpack.")
pushLog("Code Registry entries: " .. tostring(#CODE_REGISTRY))
switchPage("Scanner")
refreshScanner()

Embed on website

To embed this project on your website, copy the following code and paste it into your website's HTML: