Camara espiaV3
Key RequiredUniversal

Camara espiaV3

Universal ScriptCleoooCleooo 5.6K views 33.2K executesUpdated Oct 20, 2025
5.0/ 5(1 votes)
10100% Positive
Lua Script•1363 lines (52259 chars)
--[[
	VERSIÓN MEJORADA Y CORREGIDA
]]
local Players = game:GetService("Players")
local CoreGui = game:GetService("CoreGui")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local Workspace = workspace
local StarterGui = game:GetService("StarterGui")

local LocalPlayer = Players.LocalPlayer
local Camera = Workspace.CurrentCamera
local Mouse = LocalPlayer:GetMouse()

if not Camera or not LocalPlayer or not Mouse then
    return
end

local TWEEN_INFO_FAST = TweenInfo.new(0.20, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local TWEEN_INFO_MED  = TweenInfo.new(0.30, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
local TWEEN_INFO_SLOW = TweenInfo.new(0.75, Enum.EasingStyle.Quart, Enum.EasingDirection.Out)

local THEME = {
    bg = Color3.fromRGB(28, 28, 30),
    panel = Color3.fromRGB(35, 35, 38),
    miniPanel = Color3.fromRGB(15, 15, 17),
    accent = Color3.fromRGB(0, 160, 255),
    subtleAccent = Color3.fromRGB(16, 120, 200),
    danger = Color3.fromRGB(255, 80, 80),
    text = Color3.fromRGB(240, 240, 240),
    muted = Color3.fromRGB(170, 170, 180)
}

local isMobile = UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled
local scale = isMobile and 0.75 or 1.0

local SIZES = {
    MainW = 320 * scale,
    MainH = 460 * scale,
    MainX = 60 * scale,
    MainY = 80 * scale,
    TitleH = 44 * scale,
    BtnSize = 36 * scale,
    BtnPadding = 10 * scale,
    CornerS = 8 * scale,
    CornerM = 10 * scale,
    CornerL = 14 * scale,
    SearchH = 36 * scale,
    Padding = 14 * scale,
    ListTop = 104 * scale,
    ListBottom = 170 * scale,
    BottomBtnH = 44 * scale,
    BottomBtnY = 56 * scale,
    EntryH = 56 * scale,
    EntryPad = 12 * scale,
    AvatarSize = 44 * scale,
    AvatarPad = 10 * scale,
    AvatarY = 6 * scale,
    MiniH = 64 * scale,
    MiniIconSize = 32 * scale,
    MiniSideMargin = 12 * scale,
    MiniBetweenPadding = 12 * scale,
    MiniMinWidth = 120 * scale,
    MiniMaxWidth = 420 * scale,
}

local FONTS = {
    Title = math.floor(17 * scale),
    Btn = math.floor(18 * scale),
    MiniIcon = math.floor(18 * scale),
    Search = math.floor(15 * scale),
    State = math.floor(13 * scale),
    Name = math.floor(15 * scale),
    Sub = math.floor(13 * scale),
    ViewBtn = math.floor(14 * scale),
    BottomBtn = math.floor(16 * scale),
}

local existing = CoreGui:FindFirstChild("CamaraEspiaUI")
if existing then 
    existing:Destroy() 
end

local isMinimized = false
local dragging, dragStart, startPos
local searchDebounce = false
local spyConnection = nil
local spyTargetPart = nil
local spiedPlayer = nil
local hiddenGuisBySpy = {}
local isSpying = false
local cameraCanClip = false

local touchId = nil

local orbit = {
    enabled = false,
    distance = 12,
    minDistance = 1.2,
    maxDistance = 60,
    smoothing = 6,
}
local DEFAULT_ORBIT_MAX_DISTANCE = orbit.maxDistance

local isManualCameraActive = false
local manualJustActivated = false
local isTransitioning = false

local cameraAngle = Vector2.new(0, 0)
local orbitRadius = orbit.distance
local zoomDelta = 0
local isMouseDown = false
local CAMERA_ROTATION_SENSITIVITY = 0.015
local CAMERA_ZOOM_SPEED = 3

local lastTouchScale = nil
local PINCH_ZOOM_SENSITIVITY = 10

local isPlaceholderActive = false
local ctrlLockActive = false

local prevCameraSaved = false
local prevCameraOffset = nil
local prevCameraType = nil
local prevCameraSubject = nil

local initialCameraAngle = Vector2.new(0, 0)
local hasInitialAngle = false

local npcCache = {}
local lastNPCRefresh = 0
local NPC_CACHE_TIMEOUT = 2

local function getRootPart(character)
    if not character then return nil end
    return character:FindFirstChild("HumanoidRootPart") or
           character:FindFirstChild("Torso") or
           character:FindFirstChild("UpperTorso") or
           character:FindFirstChild("Head")
end

local function safeName(player)
    if not player then return "Jugador" end
    local displayName = player.DisplayName and player.DisplayName ~= "" and player.DisplayName or player.Name
    return displayName or "Jugador"
end

local function isPartPassable(part)
    if not part or not part:IsA("BasePart") then return true end
    if part.Anchored == false then return true end
    if part.CanCollide == false then return true end
    return false
end

local function getCameraPositionWithCollision(targetPos, desiredPos, ignoreInstances)
    if cameraCanClip then return desiredPos, false end
    
    local rayParams = RaycastParams.new()
    rayParams.FilterType = Enum.RaycastFilterType.Blacklist
    rayParams.FilterDescendantsInstances = ignoreInstances or {}
    
    local direction = desiredPos - targetPos
    local ignored = {}
    local attempts = 0
    
    while true do
        attempts = attempts + 1
        local filterList = {}
        
        if ignoreInstances then 
            for _, v in ipairs(ignoreInstances) do 
                table.insert(filterList, v) 
            end 
        end
        
        for _, v in ipairs(ignored) do 
            table.insert(filterList, v) 
        end
        
        rayParams.FilterDescendantsInstances = filterList
        local result = Workspace:Raycast(targetPos, direction, rayParams)
        
        if not result then 
            return desiredPos, false 
        end
        
        local part = result.Instance
        if isPartPassable(part) then
            table.insert(ignored, part)
            if attempts > 60 then
                local offset = result.Normal * 0.5
                return result.Position + offset, true
            end
        else
            local offset = result.Normal * 0.5
            return result.Position + offset, true
        end
    end
end

local function tweenGui(instance, props, info)
    if instance and instance.Parent then
        pcall(function()
            TweenService:Create(instance, info or TWEEN_INFO_FAST, props):Play()
        end)
    end
end

local ScreenGui = Instance.new("ScreenGui")
ScreenGui.Name = "CamaraEspiaUI"
ScreenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
ScreenGui.Parent = CoreGui
ScreenGui.ResetOnSpawn = false
ScreenGui.Enabled = true

local SpyFadeOverlay = Instance.new("Frame", ScreenGui)
SpyFadeOverlay.Name = "SpyFadeOverlay"
SpyFadeOverlay.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
SpyFadeOverlay.BackgroundTransparency = 1
SpyFadeOverlay.BorderSizePixel = 0
SpyFadeOverlay.Size = UDim2.new(1, 0, 1, 0)
SpyFadeOverlay.ZIndex = 3
SpyFadeOverlay.Visible = false

local MainFrame = Instance.new("Frame", ScreenGui)
MainFrame.Name = "MainFrame"
MainFrame.Size = UDim2.new(0, SIZES.MainW, 0, SIZES.MainH)
MainFrame.Position = UDim2.new(0, SIZES.MainX, 0, SIZES.MainY)
MainFrame.BackgroundColor3 = THEME.bg
MainFrame.BorderSizePixel = 0
MainFrame.ClipsDescendants = true
MainFrame.Active = true
MainFrame.ZIndex = 2
Instance.new("UICorner", MainFrame).CornerRadius = UDim.new(0, SIZES.CornerL)
local mainStroke = Instance.new("UIStroke", MainFrame)
mainStroke.Color = Color3.fromRGB(40, 40, 45)
mainStroke.Transparency = 0.6

local MiniCover = Instance.new("Frame", MainFrame)
MiniCover.Name = "MiniCover"
MiniCover.Size = UDim2.new(1, 0, 1, 0)
MiniCover.Position = UDim2.new(0, 0, 0, 0)
MiniCover.BackgroundColor3 = THEME.miniPanel
MiniCover.BorderSizePixel = 0
MiniCover.Visible = false
Instance.new("UICorner", MiniCover).CornerRadius = UDim.new(0, SIZES.CornerL)
MiniCover.ZIndex = 1

local TitleBar = Instance.new("Frame", MainFrame)
TitleBar.Name = "TitleBar"
TitleBar.Size = UDim2.new(1, 0, 0, SIZES.TitleH)
TitleBar.Position = UDim2.new(0, 0, 0, 0)
TitleBar.BackgroundColor3 = THEME.panel
TitleBar.Active = true
Instance.new("UICorner", TitleBar).CornerRadius = UDim.new(0, SIZES.CornerL)

local TitleContainer = Instance.new("Frame", TitleBar)
TitleContainer.Size = UDim2.new(1, -(SIZES.Padding), 1, 0)
TitleContainer.Position = UDim2.new(0, SIZES.Padding / 2, 0, 0)
TitleContainer.BackgroundTransparency = 1

local UIListLayout_Title = Instance.new("UIListLayout", TitleContainer)
UIListLayout_Title.FillDirection = Enum.FillDirection.Horizontal
UIListLayout_Title.VerticalAlignment = Enum.VerticalAlignment.Center
UIListLayout_Title.HorizontalAlignment = Enum.HorizontalAlignment.Right
UIListLayout_Title.SortOrder = Enum.SortOrder.LayoutOrder
UIListLayout_Title.Padding = UDim.new(0, SIZES.BtnPadding)

local TitleLabel = Instance.new("TextLabel", TitleContainer)
TitleLabel.BackgroundTransparency = 1
TitleLabel.Size = UDim2.new(1, -(SIZES.BtnSize * 2 + SIZES.BtnPadding * 2), 1, 0)
TitleLabel.Text = "Cámara Espía"
TitleLabel.TextColor3 = THEME.text
TitleLabel.Font = Enum.Font.GothamBold
TitleLabel.TextSize = FONTS.Title
TitleLabel.TextXAlignment = Enum.TextXAlignment.Left
TitleLabel.LayoutOrder = 1

local ToggleCameraButton = Instance.new("TextButton", TitleContainer)
ToggleCameraButton.Name = "ToggleCameraButton"
ToggleCameraButton.Size = UDim2.new(0, SIZES.BtnSize, 0, SIZES.BtnSize)
ToggleCameraButton.BackgroundColor3 = THEME.subtleAccent
ToggleCameraButton.Text = "🔒"
ToggleCameraButton.Font = Enum.Font.GothamBold
ToggleCameraButton.TextSize = FONTS.Btn * 1.2
ToggleCameraButton.BorderSizePixel = 0
ToggleCameraButton.Visible = false
ToggleCameraButton.LayoutOrder = 2
Instance.new("UICorner", ToggleCameraButton).CornerRadius = UDim.new(0, SIZES.CornerS)

local MaximizeButton = Instance.new("TextButton", TitleContainer)
MaximizeButton.Size = UDim2.new(0, SIZES.BtnSize, 0, SIZES.BtnSize)
MaximizeButton.BackgroundColor3 = THEME.accent
MaximizeButton.Text = "—"
MaximizeButton.Font = Enum.Font.GothamBold
MaximizeButton.TextSize = FONTS.Btn
MaximizeButton.BorderSizePixel = 0
MaximizeButton.LayoutOrder = 3
Instance.new("UICorner", MaximizeButton).CornerRadius = UDim.new(0, SIZES.CornerS)

local MiniControls = Instance.new("Frame", MainFrame)
MiniControls.Name = "MiniControls"
MiniControls.BackgroundTransparency = 1
MiniControls.Visible = false
MiniControls.ClipsDescendants = false
MiniControls.ZIndex = 4

local MiniLayout = Instance.new("UIListLayout", MiniControls)
MiniLayout.FillDirection = Enum.FillDirection.Horizontal
MiniLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
MiniLayout.VerticalAlignment = Enum.VerticalAlignment.Center
MiniLayout.SortOrder = Enum.SortOrder.LayoutOrder
MiniLayout.Padding = UDim.new(0, SIZES.MiniBetweenPadding)

local MaximizeMiniButton = Instance.new("TextButton", MiniControls)
MaximizeMiniButton.Size = UDim2.new(0, SIZES.MiniIconSize, 0, SIZES.MiniIconSize)
MaximizeMiniButton.BackgroundColor3 = THEME.accent
MaximizeMiniButton.Text = "+"
MaximizeMiniButton.Font = Enum.Font.GothamBold
MaximizeMiniButton.TextSize = FONTS.MiniIcon
MaximizeMiniButton.TextColor3 = Color3.fromRGB(255, 255, 255)
MaximizeMiniButton.LayoutOrder = 1
Instance.new("UICorner", MaximizeMiniButton).CornerRadius = UDim.new(0, SIZES.CornerM)
MaximizeMiniButton.ZIndex = 5

local MiniManualButton = Instance.new("TextButton", MiniControls)
MiniManualButton.Size = UDim2.new(0, SIZES.MiniIconSize, 0, SIZES.MiniIconSize)
MiniManualButton.BackgroundColor3 = THEME.subtleAccent
MiniManualButton.Text = "🔒"
MiniManualButton.Font = Enum.Font.GothamBold
MiniManualButton.TextSize = FONTS.MiniIcon
MiniManualButton.TextColor3 = Color3.fromRGB(255, 255, 255)
MiniManualButton.LayoutOrder = 2
MiniManualButton.BorderSizePixel = 0
Instance.new("UICorner", MiniManualButton).CornerRadius = UDim.new(0, SIZES.CornerM)
MiniManualButton.ZIndex = 5

local MiniBackButton = Instance.new("TextButton", MiniControls)
MiniBackButton.Size = UDim2.new(0, SIZES.MiniIconSize, 0, SIZES.MiniIconSize)
MiniBackButton.BackgroundColor3 = THEME.danger
MiniBackButton.Text = ""
MiniBackButton.Font = Enum.Font.GothamBold
MiniBackButton.TextSize = FONTS.MiniIcon
MiniBackButton.TextColor3 = Color3.fromRGB(255, 255, 255)
MiniBackButton.LayoutOrder = 3
MiniBackButton.BorderSizePixel = 0
Instance.new("UICorner", MiniBackButton).CornerRadius = UDim.new(0, SIZES.CornerM)
MiniBackButton.ZIndex = 5

local MiniBackSquare = Instance.new("Frame", MiniBackButton)
local sqSize = SIZES.MiniIconSize - 26
MiniBackSquare.Size = UDim2.new(0, sqSize, 0, sqSize)
MiniBackSquare.Position = UDim2.new(0.5, -sqSize/2, 0.5, -sqSize/2)
MiniBackSquare.BackgroundTransparency = 1
MiniBackSquare.BorderSizePixel = 0
MiniBackSquare.ZIndex = 6
local MiniBackStroke = Instance.new("UIStroke", MiniBackSquare)
MiniBackStroke.Color = Color3.fromRGB(255, 255, 255)
MiniBackStroke.Thickness = 2.5
MiniBackStroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border

local SearchBox = Instance.new("TextBox", MainFrame)
SearchBox.Size = UDim2.new(1, -(SIZES.Padding * 2), 0, SIZES.SearchH)
SearchBox.Position = UDim2.new(0, SIZES.Padding, 0, SIZES.TitleH + 12 * scale)
SearchBox.PlaceholderText = "Buscar jugador o NPCs..."
SearchBox.PlaceholderColor3 = THEME.muted
SearchBox.BackgroundColor3 = Color3.fromRGB(38, 38, 40)
SearchBox.TextColor3 = THEME.text
SearchBox.TextSize = FONTS.Search
SearchBox.Text = ""
SearchBox.ClearTextOnFocus = false
Instance.new("UICorner", SearchBox).CornerRadius = UDim.new(0, SIZES.CornerS)

local PlayerFrame = Instance.new("ScrollingFrame", MainFrame)
PlayerFrame.Size = UDim2.new(1, -(SIZES.Padding * 2), 1, -SIZES.ListBottom)
PlayerFrame.Position = UDim2.new(0, SIZES.Padding, 0, SIZES.ListTop)
PlayerFrame.BackgroundColor3 = Color3.fromRGB(28, 28, 30)
PlayerFrame.ScrollBarThickness = 6 * scale
PlayerFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
PlayerFrame.ScrollBarImageColor3 = THEME.accent
PlayerFrame.ClipsDescendants = true
PlayerFrame.ScrollingDirection = Enum.ScrollingDirection.Y
Instance.new("UICorner", PlayerFrame).CornerRadius = UDim.new(0, 6 * scale)

local PlayerLayout = Instance.new("UIListLayout", PlayerFrame)
PlayerLayout.Padding = UDim.new(0, SIZES.CornerS)
PlayerLayout.SortOrder = Enum.SortOrder.LayoutOrder
PlayerLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center

local BackToPlayerButton = Instance.new("TextButton", MainFrame)
BackToPlayerButton.Size = UDim2.new(1, -(SIZES.Padding * 2), 0, SIZES.BottomBtnH)
BackToPlayerButton.Position = UDim2.new(0, SIZES.Padding, 1, -SIZES.BottomBtnY)
BackToPlayerButton.BackgroundColor3 = THEME.danger
BackToPlayerButton.Text = "Volver a mi cámara"
BackToPlayerButton.Font = Enum.Font.GothamBold
BackToPlayerButton.TextSize = FONTS.BottomBtn
BackToPlayerButton.BorderSizePixel = 0
Instance.new("UICorner", BackToPlayerButton).CornerRadius = UDim.new(0, SIZES.CornerM)

local StateLabel = Instance.new("TextLabel", MainFrame)
StateLabel.Size = UDim2.new(1, -(SIZES.Padding * 2), 0, 20 * scale)
StateLabel.Position = UDim2.new(0, SIZES.Padding, 0, SIZES.TitleH + 12 * scale + SIZES.SearchH + 4 * scale)
StateLabel.BackgroundTransparency = 1
StateLabel.TextColor3 = THEME.muted
StateLabel.Font = Enum.Font.Gotham
StateLabel.TextSize = FONTS.State
StateLabel.Visible = false

local function cleanupSpyMode()
    for gui, _ in pairs(hiddenGuisBySpy) do
        if gui and gui.Parent then 
            pcall(function() gui.Enabled = true end)
        end
    end
    hiddenGuisBySpy = {}
end

local function setSpyMode(enabled)
    if enabled == isSpying then return end
    isSpying = enabled
    
    if enabled then
        hiddenGuisBySpy = {}
        cameraCanClip = false
        pcall(function()
            StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.All, false)
            StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.PlayerList, false)
        end)
        for _, gui in ipairs(LocalPlayer.PlayerGui:GetChildren()) do
            if gui:IsA("ScreenGui") and gui.Name ~= ScreenGui.Name and gui.Enabled == true then
                hiddenGuisBySpy[gui] = true
                gui.Enabled = false
            end
        end
    else
        cameraCanClip = false
        pcall(function()
            StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.All, true)
            StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.PlayerList, true)
        end)
        cleanupSpyMode()
    end
    
    if not isMinimized then
        ToggleCameraButton.Visible = isSpying
    end
end

local function stopSpy()
    if spyConnection then
        spyConnection:Disconnect()
        spyConnection = nil
    end
    isTransitioning = false
    spyTargetPart = nil
    spiedPlayer = nil
    orbit.enabled = false
    isMouseDown = false
    lastTouchScale = nil
    touchId = nil
    ctrlLockActive = false
    
    pcall(function()
        UserInputService.MouseBehavior = Enum.MouseBehavior.Default
        pcall(function() UserInputService.MouseIconEnabled = true end)
    end)
end

local function syncManualIcons()
    local icon = isManualCameraActive and "🎮" or "🔒"
    if ToggleCameraButton and ToggleCameraButton.Parent then 
        ToggleCameraButton.Text = icon 
    end
    if MiniManualButton and MiniManualButton.Parent then 
        MiniManualButton.Text = icon 
    end
    
    local color = isManualCameraActive and THEME.accent or THEME.subtleAccent
    if ToggleCameraButton and ToggleCameraButton.Parent then 
        ToggleCameraButton.BackgroundColor3 = color 
    end
    if MiniManualButton and MiniManualButton.Parent then 
        MiniManualButton.BackgroundColor3 = color 
    end
end

local function initManualAnglesFromCurrentCamera(targetPos)
    local camPos = Camera.CFrame.Position
    local toTarget = (camPos - targetPos)
    local dir = toTarget.Unit
    local rotY = math.atan2(dir.X, -dir.Z)
    local rotX = math.asin(math.clamp(dir.Y, -0.999, 0.999))
    cameraAngle = Vector2.new(rotY, rotX)

    local rotation = CFrame.Angles(0, rotY, 0) * CFrame.Angles(rotX, 0, 0)
    local correctPos = targetPos + (rotation.LookVector * orbitRadius * -1)
    Camera.CFrame = CFrame.lookAt(correctPos, targetPos)
end

local function toggleManualCamera(enabled)
    isManualCameraActive = enabled
    syncManualIcons()

    if enabled then
        if spyTargetPart and spyTargetPart.Parent then
            local tPos = spyTargetPart.Position + Vector3.new(0, 1.5, 0)
            local realDist = (Camera.CFrame.Position - tPos).Magnitude
            orbitRadius = math.clamp(realDist, orbit.minDistance, orbit.maxDistance)
            initManualAnglesFromCurrentCamera(tPos)
        else
            orbitRadius = orbit.distance
        end
        orbit.maxDistance = math.max(DEFAULT_ORBIT_MAX_DISTANCE, 1000000)
        lastTouchScale = nil
        manualJustActivated = true
    else
        orbit.maxDistance = DEFAULT_ORBIT_MAX_DISTANCE
        orbit.distance = orbitRadius
        manualJustActivated = false
        pcall(function()
            UserInputService.MouseBehavior = Enum.MouseBehavior.Default
            pcall(function() UserInputService.MouseIconEnabled = true end)
        end)
    end
end

local function captureInitialCameraAngle()
    if not hasInitialAngle then
        local camPos = Camera.CFrame.Position
        local forward = Camera.CFrame.LookVector
        
        local rotY = math.atan2(forward.X, -forward.Z)
        local rotX = math.asin(math.clamp(forward.Y, -0.999, 0.999))
        
        initialCameraAngle = Vector2.new(rotY, rotX)
        hasInitialAngle = true
    end
end

local function startSpyOnCharacter(charModel)
    if not charModel or not charModel.Parent then return end

    if not isSpying then
        captureInitialCameraAngle()
    end

    if not prevCameraSaved then
        local localRoot = getRootPart(LocalPlayer.Character)
        if localRoot then
            prevCameraType = Camera.CameraType
            prevCameraSubject = Camera.CameraSubject
            prevCameraOffset = localRoot.CFrame:ToObjectSpace(Camera.CFrame)
            prevCameraSaved = true
        end
    end

    local savedRadius
    if isManualCameraActive then
        savedRadius = orbitRadius
    elseif spyTargetPart and spyTargetPart.Parent then
        local tPos = spyTargetPart.Position + Vector3.new(0, 1.5, 0)
        savedRadius = (Camera.CFrame.Position - tPos).Magnitude
    else
        local localRoot = getRootPart(LocalPlayer.Character)
        savedRadius = localRoot and (Camera.CFrame.Position - localRoot.Position).Magnitude or orbit.distance
    end
    savedRadius = math.clamp(savedRadius, orbit.minDistance, DEFAULT_ORBIT_MAX_DISTANCE)

    stopSpy()

    local root = getRootPart(charModel)
    if not root then return end

    spyTargetPart = root
    spiedPlayer = Players:GetPlayerFromCharacter(charModel)

    orbit.distance = savedRadius
    orbitRadius = savedRadius
    orbit.maxDistance = isManualCameraActive
        and math.max(DEFAULT_ORBIT_MAX_DISTANCE, 1000000)
        or DEFAULT_ORBIT_MAX_DISTANCE

    orbit.enabled = true
    Camera.CameraType = Enum.CameraType.Scriptable
    setSpyMode(true)

    if isManualCameraActive then
        local tPos = root.Position + Vector3.new(0, 1.5, 0)
        
        local startCF = Camera.CFrame
        local duration = 1
        local elapsed = 0
        isTransitioning = true
        
        local animConn
        animConn = RunService.RenderStepped:Connect(function(dt)
            pcall(function()
                elapsed = elapsed + dt
                local t = math.clamp(elapsed / duration, 0, 1)
                local e = 1 - (1 - t)^3
                
                local behindOffset = -root.CFrame.LookVector * orbitRadius + Vector3.new(0, 3, 0)
                local desiredPos = tPos + behindOffset
                local goalCF = CFrame.lookAt(desiredPos, tPos)
                
                Camera.CFrame = startCF:Lerp(goalCF, e)
                
                if elapsed >= duration then
                    if animConn then
                        animConn:Disconnect()
                        animConn = nil
                    end
                    isTransitioning = false
                    local finalDir = (Camera.CFrame.Position - tPos).Unit
                    cameraAngle = Vector2.new(
                        math.atan2(finalDir.X, -finalDir.Z),
                        math.asin(math.clamp(finalDir.Y, -0.999, 0.999))
                    )
                    initialCameraAngle = cameraAngle
                end
            end)
        end)
    end

    spyConnection = RunService.RenderStepped:Connect(function(dt)
        pcall(function()
            if isTransitioning then return end
            if not spyTargetPart or not spyTargetPart.Parent then
                if spiedPlayer and spiedPlayer.Character then
                    local newRoot = getRootPart(spiedPlayer.Character)
                    if newRoot then
                        spyTargetPart = newRoot
                    else
                        resetCamera()
                        return
                    end
                else
                    resetCamera()
                    return
                end
            end

            local tPos = spyTargetPart.Position + Vector3.new(0, 1.5, 0)
            local ignoreList = {}
            if spyTargetPart.Parent then table.insert(ignoreList, spyTargetPart.Parent) end
            if LocalPlayer and LocalPlayer.Character then table.insert(ignoreList, LocalPlayer.Character) end

            local targetCFrame

            if isManualCameraActive then
                orbitRadius = math.max(orbit.minDistance, orbitRadius - zoomDelta)
                zoomDelta = 0
                local rotation = CFrame.Angles(0, cameraAngle.X, 0) * CFrame.Angles(cameraAngle.Y, 0, 0)
                local desiredPos = tPos + (rotation.LookVector * orbitRadius * -1)
                local camPosCollided, _ = getCameraPositionWithCollision(tPos, desiredPos, ignoreList)
                targetCFrame = CFrame.lookAt(camPosCollided, tPos)
            else
                orbitRadius = orbit.distance
                local desiredPos = tPos - spyTargetPart.CFrame.LookVector * orbit.distance + Vector3.new(0, 3, 0)
                local minHeight = math.max(2.5, tPos.Y + 1)
                if desiredPos.Y < minHeight then desiredPos = Vector3.new(desiredPos.X, minHeight, desiredPos.Z) end
                local camPosCollided, _ = getCameraPositionWithCollision(tPos, desiredPos, ignoreList)
                targetCFrame = CFrame.lookAt(camPosCollided, tPos)
            end

            if manualJustActivated then
                Camera.CFrame = targetCFrame
                manualJustActivated = false
            else
                local smooth = isManualCameraActive and 10 or orbit.smoothing
                local alpha = math.clamp(1 - math.exp(-dt * smooth), 0, 1)
                Camera.CFrame = Camera.CFrame:Lerp(targetCFrame, alpha)
            end
        end)
    end)
end

function resetCamera()
    if not isSpying then return end

    local hadSpy = isSpying

    if spyConnection then
        spyConnection:Disconnect()
        spyConnection = nil
    end

    local root = getRootPart(LocalPlayer.Character)

    if prevCameraSaved and root and prevCameraOffset then
        Camera.CameraType = Enum.CameraType.Scriptable
        local startCFrame = Camera.CFrame
        local duration = 1.1
        local elapsed = 0
        
        local resetConn
        resetConn = RunService.RenderStepped:Connect(function(dt)
            pcall(function()
                elapsed = elapsed + dt
                local t = math.clamp(elapsed / duration, 0, 1)
                local eased
                
                if t < 0.5 then
                    eased = 4 * t * t * t
                else
                    local f = (2 * t) - 2
                    eased = 0.5 * f * f * f + 1
                end
                
                local currentRoot = getRootPart(LocalPlayer.Character)
                if currentRoot then
                    local endCFrame = currentRoot.CFrame * prevCameraOffset
                    Camera.CFrame = startCFrame:Lerp(endCFrame, eased)
                end
                
                if elapsed >= duration then
                    if resetConn then
                        resetConn:Disconnect()
                        resetConn = nil
                    end
                    
                    if prevCameraType then
                        Camera.CameraType = prevCameraType
                    else
                        Camera.CameraType = Enum.CameraType.Custom
                    end

                    if prevCameraType == Enum.CameraType.Custom and prevCameraSubject then
                        pcall(function() Camera.CameraSubject = prevCameraSubject end)
                    elseif prevCameraType == Enum.CameraType.Custom then
                        local humanoid = LocalPlayer.Character and LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
                        if humanoid then pcall(function() Camera.CameraSubject = humanoid end) end
                    end

                    prevCameraSaved = false
                    prevCameraOffset = nil
                    prevCameraType = nil
                    prevCameraSubject = nil
                end
            end)
        end)

    elseif root then
        Camera.CameraType = Enum.CameraType.Scriptable
        local startCFrame = Camera.CFrame
        local duration = 1.1
        local elapsed = 0

        local resetConn
        resetConn = RunService.RenderStepped:Connect(function(dt)
            pcall(function()
                elapsed = elapsed + dt
                local t = math.clamp(elapsed / duration, 0, 1)
                local eased
                
                if t < 0.5 then
                    eased = 4 * t * t * t
                else
                    local f = (2 * t) - 2
                    eased = 0.5 * f * f * f + 1
                end
                
                local currentRoot = getRootPart(LocalPlayer.Character)
                if currentRoot then
                    local desiredPos = currentRoot.Position + currentRoot.CFrame.LookVector * -6 + Vector3.new(0, 3, 0)
                    local endCFrame = CFrame.lookAt(desiredPos, currentRoot.Position)
                    Camera.CFrame = startCFrame:Lerp(endCFrame, eased)
                end
                
                if elapsed >= duration then
                    if resetConn then
                        resetConn:Disconnect()
                        resetConn = nil
                    end
                    
                    Camera.CameraType = Enum.CameraType.Custom
                    local humanoid = LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
                    if humanoid then Camera.CameraSubject = humanoid end
                end
            end)
        end)
    else
        Camera.CameraType = Enum.CameraType.Custom
    end

    if hadSpy then
        setSpyMode(false)
    end
    stopSpy()
    hasInitialAngle = false
    initialCameraAngle = Vector2.new(0, 0)
    orbit.maxDistance = DEFAULT_ORBIT_MAX_DISTANCE
    syncManualIcons()
end

BackToPlayerButton.MouseButton1Click:Connect(function()
    if not isSpying then return end
    tweenGui(BackToPlayerButton, {BackgroundColor3 = THEME.subtleAccent}, TWEEN_INFO_FAST)
    task.delay(0.12, function() 
        tweenGui(BackToPlayerButton, {BackgroundColor3 = THEME.danger}, TWEEN_INFO_FAST) 
    end)
    resetCamera()
end)

MiniBackButton.MouseButton1Click:Connect(function()
    if not isSpying then return end
    tweenGui(MiniBackButton, {BackgroundTransparency = 0.5}, TWEEN_INFO_FAST)
    task.delay(0.12, function() 
        tweenGui(MiniBackButton, {BackgroundTransparency = 0}, TWEEN_INFO_FAST) 
    end)
    resetCamera()
end)

MiniManualButton.MouseButton1Click:Connect(function()
    if not isSpying then return end
    toggleManualCamera(not isManualCameraActive)
end)

ToggleCameraButton.MouseButton1Click:Connect(function()
    if not isSpying then return end
    toggleManualCamera(not isManualCameraActive)
end)

local function createNPCEntry(model)
    local entry = Instance.new("TextButton")
    entry.Name = "NpcBtn_" .. model:GetFullName()
    entry.Parent = PlayerFrame
    entry.Size = UDim2.new(1, -SIZES.EntryPad, 0, SIZES.EntryH)
    entry.BackgroundColor3 = Color3.fromRGB(38, 38, 40)
    entry.BorderSizePixel = 0
    entry.AutoButtonColor = true
    entry.Text = ""
    entry.LayoutOrder = #PlayerFrame:GetChildren()
    Instance.new("UICorner", entry).CornerRadius = UDim.new(0, SIZES.CornerM)
    
    local left = Instance.new("Frame", entry)
    left.BackgroundTransparency = 1
    left.Position = UDim2.new(0, SIZES.AvatarPad, 0, SIZES.AvatarY)
    left.Size = UDim2.new(0, SIZES.AvatarSize, 1, -(SIZES.AvatarY * 2))
    
    local avatar = Instance.new("ImageLabel", left)
    avatar.Size = UDim2.new(1, 0, 1, 0)
    avatar.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
    avatar.BorderSizePixel = 0
    avatar.Image = "rbxassetid://1322223555"
    avatar.ScaleType = Enum.ScaleType.Crop
    Instance.new("UICorner", avatar).CornerRadius = UDim.new(0, SIZES.CornerS)
    
    local nameX = SIZES.AvatarPad + SIZES.AvatarSize + 10 * scale
    local btnW = 90 * scale
    local btnPadding = SIZES.AvatarPad
    local btnTotalSpace = btnW + btnPadding
    
    local nameLabel = Instance.new("TextLabel", entry)
    nameLabel.BackgroundTransparency = 1
    nameLabel.Position = UDim2.new(0, nameX, 0, 8 * scale)
    nameLabel.Size = UDim2.new(1, -(nameX + btnTotalSpace + 5 * scale), 0, FONTS.Name + 5 * scale)
    nameLabel.TextXAlignment = Enum.TextXAlignment.Left
    nameLabel.Text = model.Name
    nameLabel.TextColor3 = THEME.text
    nameLabel.Font = Enum.Font.GothamSemibold
    nameLabel.TextSize = FONTS.Name
    nameLabel.TextTruncate = Enum.TextTruncate.AtEnd
    
    local subLabel = Instance.new("TextLabel", entry)
    subLabel.BackgroundTransparency = 1
    subLabel.Position = UDim2.new(0, nameX, 0, (8 * scale) + FONTS.Name + (2 * scale))
    subLabel.Size = UDim2.new(1, -(nameX + btnTotalSpace + 5 * scale), 0, FONTS.Sub + 5 * scale)
    subLabel.TextXAlignment = Enum.TextXAlignment.Left
    subLabel.Text = "NPC"
    subLabel.TextColor3 = THEME.muted
    subLabel.Font = Enum.Font.Gotham
    subLabel.TextSize = FONTS.Sub
    subLabel.TextTruncate = Enum.TextTruncate.AtEnd
    
    local viewBtn = Instance.new("TextButton", entry)
    viewBtn.Size = UDim2.new(0, btnW, 0, SIZES.BtnSize)
    viewBtn.Position = UDim2.new(1, -(btnW + btnPadding), 0.5, -(SIZES.BtnSize / 2))
    viewBtn.BackgroundColor3 = THEME.accent
    viewBtn.Text = "Ver"
    viewBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
    viewBtn.Font = Enum.Font.GothamBold
    viewBtn.TextSize = FONTS.ViewBtn
    viewBtn.BorderSizePixel = 0
    Instance.new("UICorner", viewBtn).CornerRadius = UDim.new(0, SIZES.CornerS)
    
    entry.MouseEnter:Connect(function() 
        tweenGui(entry, {BackgroundColor3 = Color3.fromRGB(45, 45, 47)}, TWEEN_INFO_FAST) 
    end)
    entry.MouseLeave:Connect(function() 
        tweenGui(entry, {BackgroundColor3 = Color3.fromRGB(38, 38, 40)}, TWEEN_INFO_FAST) 
    end)
    
    viewBtn.MouseButton1Click:Connect(function()
        tweenGui(viewBtn, {BackgroundTransparency = 0.5}, TWEEN_INFO_FAST)
        task.delay(0.12, function() 
            tweenGui(viewBtn, {BackgroundTransparency = 0}, TWEEN_INFO_FAST) 
        end)
        startSpyOnCharacter(model)
    end)
    
    return entry
end

local function createPlayerEntry(player)
    local entry = Instance.new("TextButton")
    entry.Name = "PlayerBtn_" .. player.UserId
    entry.Parent = PlayerFrame
    entry.Size = UDim2.new(1, -SIZES.EntryPad, 0, SIZES.EntryH)
    entry.BackgroundColor3 = Color3.fromRGB(38, 38, 40)
    entry.BorderSizePixel = 0
    entry.AutoButtonColor = true
    entry.Text = ""
    entry.LayoutOrder = #PlayerFrame:GetChildren()
    Instance.new("UICorner", entry).CornerRadius = UDim.new(0, SIZES.CornerM)
    
    local left = Instance.new("Frame", entry)
    left.BackgroundTransparency = 1
    left.Position = UDim2.new(0, SIZES.AvatarPad, 0, SIZES.AvatarY)
    left.Size = UDim2.new(0, SIZES.AvatarSize, 1, -(SIZES.AvatarY * 2))
    
    local avatar = Instance.new("ImageLabel", left)
    avatar.Size = UDim2.new(1, 0, 1, 0)
    avatar.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
    avatar.BorderSizePixel = 0
    avatar.Image = "rbxthumb://type=AvatarHeadShot&id=" .. tostring(player.UserId) .. "&w=150&h=150"
    avatar.ScaleType = Enum.ScaleType.Crop
    Instance.new("UICorner", avatar).CornerRadius = UDim.new(0, SIZES.CornerS)
    
    local nameX = SIZES.AvatarPad + SIZES.AvatarSize + 10 * scale
    local btnW = 90 * scale
    local btnPadding = SIZES.AvatarPad
    local btnTotalSpace = btnW + btnPadding
    
    local nameLabel = Instance.new("TextLabel", entry)
    nameLabel.BackgroundTransparency = 1
    nameLabel.Position = UDim2.new(0, nameX, 0, 8 * scale)
    nameLabel.Size = UDim2.new(1, -(nameX + btnTotalSpace + 5 * scale), 0, FONTS.Name + 5 * scale)
    nameLabel.TextXAlignment = Enum.TextXAlignment.Left
    nameLabel.Text = safeName(player)
    nameLabel.TextColor3 = THEME.text
    nameLabel.Font = Enum.Font.GothamSemibold
    nameLabel.TextSize = FONTS.Name
    nameLabel.TextTruncate = Enum.TextTruncate.AtEnd
    
    local subLabel = Instance.new("TextLabel", entry)
    subLabel.BackgroundTransparency = 1
    subLabel.Position = UDim2.new(0, nameX, 0, (8 * scale) + FONTS.Name + (2 * scale))
    subLabel.Size = UDim2.new(1, -(nameX + btnTotalSpace + 5 * scale), 0, FONTS.Sub + 5 * scale)
    subLabel.TextXAlignment = Enum.TextXAlignment.Left
    subLabel.Text = "@" .. player.Name
    subLabel.TextColor3 = THEME.muted
    subLabel.Font = Enum.Font.Gotham
    subLabel.TextSize = FONTS.Sub
    subLabel.TextTruncate = Enum.TextTruncate.AtEnd
    
    local viewBtn = Instance.new("TextButton", entry)
    viewBtn.Size = UDim2.new(0, btnW, 0, SIZES.BtnSize)
    viewBtn.Position = UDim2.new(1, -(btnW + btnPadding), 0.5, -(SIZES.BtnSize / 2))
    viewBtn.BackgroundColor3 = THEME.accent
    viewBtn.Text = "Ver"
    viewBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
    viewBtn.Font = Enum.Font.GothamBold
    viewBtn.TextSize = FONTS.ViewBtn
    viewBtn.BorderSizePixel = 0
    Instance.new("UICorner", viewBtn).CornerRadius = UDim.new(0, SIZES.CornerS)
    
    entry.MouseEnter:Connect(function() 
        tweenGui(entry, {BackgroundColor3 = Color3.fromRGB(45, 45, 47)}, TWEEN_INFO_FAST) 
    end)
    entry.MouseLeave:Connect(function() 
        tweenGui(entry, {BackgroundColor3 = Color3.fromRGB(38, 38, 40)}, TWEEN_INFO_FAST) 
    end)
    
    viewBtn.MouseButton1Click:Connect(function()
        tweenGui(viewBtn, {BackgroundTransparency = 0.5}, TWEEN_INFO_FAST)
        task.delay(0.12, function() 
            tweenGui(viewBtn, {BackgroundTransparency = 0}, TWEEN_INFO_FAST) 
        end)
        startSpyOnCharacter(player.Character)
    end)
    
    return entry
end

local function refreshPlayers(filter)
    filter = (filter or ""):lower()
    
    for _, child in ipairs(PlayerFrame:GetChildren()) do
        if child:IsA("TextButton") and (child.Name:match("^PlayerBtn_") or child.Name:match("^NpcBtn_")) then
            child:Destroy()
        end
    end
    
    local visibleCount = 0
    
    for _, player in ipairs(Players:GetPlayers()) do
        if player == LocalPlayer then continue end
        local display = (player.DisplayName or player.Name):lower()
        local username = (player.Name or ""):lower()
        if filter == "" or display:find(filter, 1, true) or username:find(filter, 1, true) then
            visibleCount = visibleCount + 1
            local entry = createPlayerEntry(player)
            entry.LayoutOrder = visibleCount
            entry.BackgroundTransparency = 1
            tweenGui(entry, {BackgroundTransparency = 0}, TWEEN_INFO_MED)
        end
    end
    
    local npcCount = 0
    local processedNPCs = {}
    local currentTime = tick()
    
    if currentTime - lastNPCRefresh > NPC_CACHE_TIMEOUT then
        npcCache = {}
        lastNPCRefresh = currentTime
    end
    
    for _, descendant in ipairs(Workspace:GetDescendants()) do
        if descendant:IsA("Humanoid") then
            local model = descendant.Parent
            if not processedNPCs[model] then
                local rootPart = model:FindFirstChild("HumanoidRootPart")
                if rootPart and descendant.Health > 0 and not Players:GetPlayerFromCharacter(model) then
                    local npcName = model.Name:lower()
                    if filter == "" or npcName:find(filter, 1, true) then
                        processedNPCs[model] = true
                        npcCount = npcCount + 1
                        local entry = createNPCEntry(model)
                        entry.LayoutOrder = visibleCount + npcCount + 1000
                        entry.BackgroundTransparency = 1
                        tweenGui(entry, {BackgroundTransparency = 0}, TWEEN_INFO_MED)
                    end
                end
            end
        end
    end
    
    if visibleCount == 0 and npcCount == 0 then
        StateLabel.Text = "No se encontraron resultados"
        StateLabel.Visible = true
        tweenGui(StateLabel, {TextTransparency = 0}, TWEEN_INFO_FAST)
    else
        tweenGui(StateLabel, {TextTransparency = 1}, TWEEN_INFO_FAST)
        task.delay(0.25, function() StateLabel.Visible = false end)
    end
end

PlayerLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
    local y = PlayerLayout.AbsoluteContentSize.Y
    PlayerFrame.CanvasSize = UDim2.new(0, 0, 0, y + 10 * scale)
end)

Players.PlayerAdded:Connect(function(player)
    local filterText = isPlaceholderActive and "" or SearchBox.Text
    refreshPlayers(filterText)
end)

Players.PlayerRemoving:Connect(function(player)
    if spiedPlayer and player == spiedPlayer then resetCamera() end
    local filterText = isPlaceholderActive and "" or SearchBox.Text
    refreshPlayers(filterText)
end)

SearchBox:GetPropertyChangedSignal("Text"):Connect(function()
    if SearchBox.Text ~= SearchBox.PlaceholderText and SearchBox.Text ~= "" then 
        isPlaceholderActive = false 
    end
    if searchDebounce then return end
    searchDebounce = true
    task.delay(0.12, function()
        local filterText = (SearchBox.Text == SearchBox.PlaceholderText or SearchBox.Text == "") and "" or SearchBox.Text
        refreshPlayers(filterText)
        searchDebounce = false
    end)
end)

SearchBox.Focused:Connect(function() 
    if isPlaceholderActive then 
        SearchBox.Text = "" 
    end 
end)

SearchBox.FocusLost:Connect(function()
    if SearchBox.Text == "" then
        isPlaceholderActive = true
        SearchBox.Text = SearchBox.PlaceholderText
        refreshPlayers("")
    end
end)

refreshPlayers("")

local function computeMiniWidth()
    local children = MiniControls:GetChildren()
    local btnCount = 0
    for _, c in ipairs(children) do
        if c:IsA("GuiObject") and c.Visible then 
            btnCount = btnCount + 1 
        end
    end
    if btnCount == 0 then return SIZES.MiniMinWidth end
    local totalIconsWidth = btnCount * SIZES.MiniIconSize
    local totalPadding = math.max(0, btnCount - 1) * SIZES.MiniBetweenPadding
    local width = SIZES.MiniSideMargin * 2 + totalIconsWidth + totalPadding
    width = math.clamp(width, SIZES.MiniMinWidth, SIZES.MiniMaxWidth)
    return width
end

local function updateMiniButtons()
    if isMinimized then
        MiniManualButton.Visible = isSpying
    else
        MiniManualButton.Visible = false
    end
    MaximizeMiniButton.Visible = true
    MiniBackButton.Visible = true
    syncManualIcons()
    if isMinimized then
        local newW = computeMiniWidth()
        local miniInnerW = newW - (SIZES.MiniSideMargin * 2)
        tweenGui(MiniControls, {Size = UDim2.new(0, miniInnerW, 0, SIZES.MiniH)}, TWEEN_INFO_MED)
        tweenGui(MiniControls, {Position = UDim2.new(0.5, -miniInnerW / 2, 0.5, -(SIZES.MiniH / 2))}, TWEEN_INFO_MED)
        tweenGui(MainFrame, {Size = UDim2.new(0, newW, 0, SIZES.MiniH)}, TWEEN_INFO_MED)
    end
end

spawn(function()
    while true do
        updateMiniButtons()
        task.wait(0.35)
    end
end)

local function hideContentNow()
    SearchBox.Visible = false
    PlayerFrame.Visible = false
    BackToPlayerButton.Visible = false
    StateLabel.Visible = false
    TitleLabel.Visible = false
end

local function showContentNow()
    SearchBox.Visible = true
    PlayerFrame.Visible = true
    BackToPlayerButton.Visible = true
    TitleLabel.Visible = true
end

local function minimizeUI()
    if isMinimized then return end
    isMinimized = true
    hideContentNow()
    MiniCover.Visible = true
    MiniCover.BackgroundTransparency = 0
    MiniCover.ZIndex = 2
    tweenGui(TitleBar, {BackgroundColor3 = THEME.miniPanel}, TWEEN_INFO_FAST)
    tweenGui(TitleLabel, {TextTransparency = 1}, TWEEN_INFO_FAST)
    tweenGui(SearchBox, {BackgroundTransparency = 1, TextTransparency = 1}, TWEEN_INFO_FAST)
    tweenGui(PlayerFrame, {BackgroundTransparency = 1}, TWEEN_INFO_FAST)
    tweenGui(BackToPlayerButton, {BackgroundTransparency = 1, TextTransparency = 1}, TWEEN_INFO_FAST)
    updateMiniButtons()
    task.delay(0.22, function()
        MaximizeButton.Visible = false
        ToggleCameraButton.Visible = false
        MiniControls.Visible = true
        MiniControls.ZIndex = 4
        MiniCover.ZIndex = 1
    end)
end

local function maximizeUI()
    if not isMinimized then return end
    isMinimized = false
    showContentNow()
    MiniCover.Visible = false
    TitleLabel.TextTransparency = 1
    SearchBox.BackgroundTransparency = 1
    SearchBox.TextTransparency = 1
    PlayerFrame.BackgroundTransparency = 1
    BackToPlayerButton.BackgroundTransparency = 1
    BackToPlayerButton.TextTransparency = 1
    tweenGui(MainFrame, {Size = UDim2.new(0, SIZES.MainW, 0, SIZES.MainH)}, TWEEN_INFO_MED)
    MaximizeButton.Visible = true
    ToggleCameraButton.Visible = isSpying
    MiniControls.Visible = false
    tweenGui(TitleBar, {BackgroundColor3 = THEME.panel}, TWEEN_INFO_FAST)
    tweenGui(TitleLabel, {TextTransparency = 0}, TWEEN_INFO_FAST)
    tweenGui(SearchBox, {BackgroundTransparency = 0, TextTransparency = 0}, TWEEN_INFO_FAST)
    tweenGui(PlayerFrame, {BackgroundTransparency = 0}, TWEEN_INFO_FAST)
    tweenGui(BackToPlayerButton, {BackgroundTransparency = 0, TextTransparency = 0}, TWEEN_INFO_FAST)
end

MaximizeButton.MouseButton1Click:Connect(function()
    if not isMinimized then 
        minimizeUI()
        MaximizeButton.Text = "+" 
    else 
        maximizeUI()
        MaximizeButton.Text = "—" 
    end
end)

MaximizeMiniButton.MouseButton1Click:Connect(function()
    maximizeUI()
    MaximizeButton.Text = "—"
end)

local function beginDrag(input)
    if input.UserInputType ~= Enum.UserInputType.MouseButton1 and input.UserInputType ~= Enum.UserInputType.Touch then 
        return 
    end
    dragging = true
    dragStart = input.Position
    startPos = MainFrame.Position
    input.Changed:Connect(function()
        if input.UserInputState == Enum.UserInputState.End then 
            dragging = false 
        end
    end)
end

local function updateDrag(input)
    if not dragging then return end
    local delta = input.Position - dragStart
    local newX = startPos.X.Offset + delta.X
    local newY = startPos.Y.Offset + delta.Y
    local screenW, screenH = Workspace.CurrentCamera.ViewportSize.X, Workspace.CurrentCamera.ViewportSize.Y
    local fw, fh = MainFrame.AbsoluteSize.X, MainFrame.AbsoluteSize.Y
    newX = math.clamp(newX, -fw + 40, screenW - 40)
    newY = math.clamp(newY, -fh + 24, screenH - 24)
    MainFrame.Position = UDim2.new(0, newX, 0, newY)
end

TitleBar.InputBegan:Connect(beginDrag)
TitleBar.InputChanged:Connect(function(input) 
    if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then 
        updateDrag(input) 
    end 
end)
MiniControls.InputBegan:Connect(beginDrag)
MiniControls.InputChanged:Connect(function(input) 
    if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then 
        updateDrag(input) 
    end 
end)
UserInputService.InputChanged:Connect(function(input) 
    if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then 
        updateDrag(input) 
    end 
end)

UserInputService.InputBegan:Connect(function(input, gameProcessed)
    pcall(function()
        if gameProcessed then return end
        if (input.KeyCode == Enum.KeyCode.LeftControl or input.KeyCode == Enum.KeyCode.RightControl) and isSpying and isManualCameraActive then
            ctrlLockActive = not ctrlLockActive
            if ctrlLockActive then
                isMouseDown = true
                pcall(function() UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter end)
                pcall(function() UserInputService.MouseIconEnabled = false end)
            else
                isMouseDown = false
                pcall(function() UserInputService.MouseBehavior = Enum.MouseBehavior.Default end)
                pcall(function() UserInputService.MouseIconEnabled = true end)
            end
            return
        end
        if not isSpying or not isManualCameraActive then return end
        local isControlInput = input.UserInputType == Enum.UserInputType.MouseButton2 or input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.Gamepad1
        if isControlInput and not isMouseDown then
            if input.UserInputType == Enum.UserInputType.MouseButton2 then
                isMouseDown = true
                UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
            elseif input.UserInputType == Enum.UserInputType.Touch then
                isMouseDown = true
                touchId = input.UserInputType
            elseif input.UserInputType == Enum.UserInputType.Gamepad1 and input.KeyCode == Enum.KeyCode.Thumbstick2 then
                isMouseDown = true
            end
        end
    end)
end)

UserInputService.InputEnded:Connect(function(input, gameProcessed)
    pcall(function()
        if not isSpying or not isManualCameraActive then return end
        local isControlInput = input.UserInputType == Enum.UserInputType.MouseButton2 or input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.Gamepad1
        if isControlInput and isMouseDown and not ctrlLockActive then
            if input.UserInputType == Enum.UserInputType.MouseButton2 then
                isMouseDown = false
                UserInputService.MouseBehavior = Enum.MouseBehavior.Default
            elseif input.UserInputType == Enum.UserInputType.Touch then
                isMouseDown = false
                touchId = nil
            elseif input.UserInputType == Enum.UserInputType.Gamepad1 and input.KeyCode == Enum.KeyCode.Thumbstick2 then
                isMouseDown = false
            end
        end
    end)
end)

UserInputService.InputChanged:Connect(function(input, gameProcessed)
    pcall(function()
        if not isSpying or not isManualCameraActive then return end
        if isMouseDown then
            if input.UserInputType == Enum.UserInputType.MouseMovement then
                local delta = input.Delta * CAMERA_ROTATION_SENSITIVITY
                cameraAngle = cameraAngle + Vector2.new(-delta.X, -delta.Y)
            elseif input.UserInputType == Enum.UserInputType.Touch and touchId then
                local delta = input.Delta * CAMERA_ROTATION_SENSITIVITY * (isMobile and 1 or 2)
                cameraAngle = cameraAngle + Vector2.new(-delta.X, -delta.Y)
            elseif input.UserInputType == Enum.UserInputType.Gamepad1 and input.KeyCode == Enum.KeyCode.Thumbstick2 then
                local delta = Vector2.new(-input.Position.X, input.Position.Y) * CAMERA_ROTATION_SENSITIVITY * 10
                cameraAngle = cameraAngle + delta
            end
            cameraAngle = Vector2.new(cameraAngle.X, math.clamp(cameraAngle.Y, -math.pi/2 + 0.01, math.pi/2 - 0.01))
        end
        if input.UserInputType == Enum.UserInputType.MouseWheel then
            local mousePos = UserInputService:GetMouseLocation()
            local absPos = MainFrame.AbsolutePosition
            local absSize = MainFrame.AbsoluteSize
            local overUI = mousePos.X >= absPos.X and mousePos.X <= absPos.X + absSize.X
                        and mousePos.Y >= absPos.Y and mousePos.Y <= absPos.Y + absSize.Y
            if not overUI then
                zoomDelta = zoomDelta + input.Position.Z * CAMERA_ZOOM_SPEED
            end
        end
        if input.UserInputType == Enum.UserInputType.Gamepad1 then
            if input.KeyCode == Enum.KeyCode.ButtonL1 or input.KeyCode == Enum.KeyCode.ButtonL2 then
                if input.UserInputState == Enum.UserInputState.Begin then zoomDelta = zoomDelta - CAMERA_ZOOM_SPEED * 2 end
            elseif input.KeyCode == Enum.KeyCode.ButtonR1 or input.KeyCode == Enum.KeyCode.ButtonR2 then
                if input.UserInputState == Enum.UserInputState.Begin then zoomDelta = zoomDelta + CAMERA_ZOOM_SPEED * 2 end
            end
        end
    end)
end)

UserInputService.TouchPinch:Connect(function(touchPositions, scale, velocity, state)
    pcall(function()
        if not isSpying or not isManualCameraActive then return end
        if state == Enum.UserInputState.Begin then lastTouchScale = scale
        elseif state == Enum.UserInputState.Change then
            if lastTouchScale then
                local difference = scale - lastTouchScale
                zoomDelta = zoomDelta + difference * PINCH_ZOOM_SENSITIVITY
                lastTouchScale = scale
            end
        elseif state == Enum.UserInputState.End then lastTouchScale = nil end
    end)
end)

UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if gameProcessed then return end
    if input.KeyCode == Enum.KeyCode.Escape then
        if isMinimized then maximizeUI() else minimizeUI() end
    end
end)

MainFrame.Position = UDim2.new(0, SIZES.MainX, 0, -500)
tweenGui(MainFrame, {Position = UDim2.new(0, SIZES.MainX, 0, SIZES.MainY)}, TWEEN_INFO_SLOW)
tweenGui(MainFrame, {Size = UDim2.new(0, SIZES.MainW, 0, SIZES.MainH)}, TWEEN_INFO_SLOW)

Features & Information

ofrece una interfaz moderna que permite espiar jugadores o NPCs con controles de cámara avanzados. Incluye: UI adaptable con búsqueda, lista de jugadores/NPCs y botones de control. Modo espía que oculta la interfaz del jugador y sigue al objetivo. Cámara manual y automática con zoom, rotación orbital y colisión inteligente. Animaciones suaves para transiciones y restauración de la cámara original. Compatibilidad móvil/PC con escalado automático y controles táctiles o de teclado/ratón.

Community Comments (0)

No comments yet for this script.

More Scripts for Universal Scripts