robloxexploits.lol
PatchedKeylessUniversal

TuboCAM v13 - Retro CCTV & NVR System [Claude IA]

pmiguel10766 23 hours ago 368 views 1

Run it

Copy the script and paste it into your executor.

TuboCAM v13 - Retro CCTV NVR System Claude IA.lua · 1,973 lines
-- // Delta Executor - TuboCAM v13
-- v12 + botão ON/OFF da grade na TV (OFF = tela velha com logo quicando).

local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local RunService = game:GetService("RunService")
local Lighting = game:GetService("Lighting")
local GuiService = game:GetService("GuiService")

local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local rootPart = character:WaitForChild("HumanoidRootPart")
local camera = Workspace.CurrentCamera

-- Limpar execuções anteriores
if Workspace:FindFirstChild("CCTVRetroFolder") then Workspace.CCTVRetroFolder:Destroy() end
if player.PlayerGui:FindFirstChild("RetroTVGui") then player.PlayerGui.RetroTVGui:Destroy() end
if Lighting:FindFirstChild("CCTVNightVision") then Lighting.CCTVNightVision:Destroy() end

local folder = Instance.new("Folder")
folder.Name = "CCTVRetroFolder"
folder.Parent = Workspace

local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = {folder, character}

local ALONG_Z = CFrame.Angles(0, math.rad(90), 0)

-- ===================== CONFIG =====================
local CFG = {
    WALL_PENALTY = 15,
    SIGNAL_LOSS = 0.25,
    BOOT_TIME = 6,
    HEIGHT_STEP = 0.5,

    MOTION_RANGE = 45,
    MOTION_HALF_FOV = 35,
    MOTION_MIN_SPEED = 2,
    DETECT_PLAYERS = true,
    DETECT_NPCS = true,
    DETECT_SELF = true,
    ALERT_HOLD = 3,
    ALARM_SOUND_ID = "",
    ICON_IMAGE_ID = "",

    GRID_MAX_CAMS = 16,
    FEED_RADIUS = 60,
    FEED_PART_BUDGET = 800,
    FEED_REFRESH = 10,
    FEED_FOV = 70,
    GRID_SHOW_NPCS = false,
    CHAR_INTERVAL = 0.25,
    MAX_CHARS = 6,
    DEBUG = true,

    TV_W = 480, TV_H = 380,
    LOGO_W = 150, LOGO_H = 44,

    RED = Color3.fromRGB(255, 30, 30),
    BLACK = Color3.fromRGB(0, 0, 0),
}

-- ===================== ESTADO =====================
local ST = {
    camIndex = 1, viewing = false, currentCamData = nil,
    activeTV = nil, activeTVScreen = nil, tvBase = nil, rcaModel = nil,
    nvrOn = true, nvrBootStart = tick(),
    nvrModel = nil, nvrBase = nil, nvrBody = nil, nvrButton = nil,
    nvrLedPower = nil, nvrLedRec = nil, ledCache = {},
    nightVision = false, motionOn = false, alertUntil = 0, lastAlarm = 0,
    camHeight = 3, panelsOpen = true, cableTarget = nil,
    tvNoiseUpdate = nil, tvLogo = nil, tvUI = nil, lastTVNoise = 0,
    logoX = 20, logoY = 20, logoVX = 110, logoVY = 90,
    lastPos = setmetatable({}, {__mode = "k"}),
    npcCache = {}, lastNpcRefresh = 0,
    lastIR = 0, lastScan = 0,
    gridMode = true,
}

local UI = {}
local FN = {}
local GRID = {cells = {}, count = 0, lastSnap = 0, lastChars = 0, lastNpc = 0, charModels = {}, lastWarn = 0}
local createdCams = {}

-- Filtro de visão noturna
do
    local e = Instance.new("ColorCorrectionEffect")
    e.Name = "CCTVNightVision"
    e.TintColor = Color3.fromRGB(110, 255, 110)
    e.Brightness = 0.12
    e.Contrast = 0.25
    e.Saturation = -0.85
    e.Enabled = false
    e.Parent = Lighting
    UI.nightEffect = e
end

local function new(class, props, parent)
    local o = Instance.new(class)
    for k, v in pairs(props) do o[k] = v end
    o.Parent = parent
    return o
end

-- ===================== AJUDANTES =====================
local function floorYAt(pos)
    local hit = Workspace:Raycast(pos, Vector3.new(0, -300, 0), rayParams)
    return hit and hit.Position.Y + 0.05 or pos.Y
end

local function cableSeg(parent, a, b, color, thick)
    local d = (a - b).Magnitude
    if d < 0.05 then return end
    local t = thick or 0.06
    new("Part", {
        Size = Vector3.new(t, t, d),
        CFrame = CFrame.lookAt(a, b) * CFrame.new(0, 0, -d / 2),
        Color = color or Color3.fromRGB(20, 20, 20),
        Material = Enum.Material.Rubber,
        Anchored = true, CanCollide = false, CanQuery = false,
    }, parent)
end

local function nvrExists()
    return ST.nvrBody ~= nil and ST.nvrBody.Parent ~= nil
end

local function nvrReady()
    return nvrExists() and ST.nvrOn and (tick() - ST.nvrBootStart) >= CFG.BOOT_TIME
end

local function tvLinked()
    return nvrExists() and ST.rcaModel ~= nil and ST.rcaModel.Parent ~= nil
end

local function toggleNVR()
    ST.nvrOn = not ST.nvrOn
    if ST.nvrOn then ST.nvrBootStart = tick() end
    if ST.nvrButton then
        ST.nvrButton.Color = ST.nvrOn and Color3.fromRGB(170, 170, 175) or Color3.fromRGB(80, 80, 85)
    end
    if FN.toast then
        FN.toast(ST.nvrOn and "📼 NVR ligado — iniciando..." or "📼 NVR desligado")
    end
end

local function isDark()
    local t = Lighting.ClockTime
    return t < 5.5 or t > 18.5
end

-- Cor do LED de status (cinza quando a câmera está OFF)
local function baseLed(c)
    return c.enabled and c.ledColor or Color3.fromRGB(60, 60, 60)
end

-- Próxima câmera ligada a partir do índice "from" (inclusive)
local function nextEnabled(from)
    local n = #createdCams
    if n == 0 then return nil end
    for step = 0, n - 1 do
        local idx = (from - 1 + step) % n + 1
        if createdCams[idx].enabled then return idx end
    end
    return nil
end

-- Clona mesmo se o objeto estiver marcado como Archivable = false
local function safeClone(inst)
    local was = inst.Archivable
    if not was then pcall(function() inst.Archivable = true end) end
    local ok, c = pcall(function() return inst:Clone() end)
    if not was then pcall(function() inst.Archivable = false end) end
    if ok then return c end
    return nil
end

-- ===================== 1. TV ANTIGA =====================
local function createRetroTV(cframe)
    if ST.activeTV then ST.activeTV:Destroy() end

    local origin = cframe.Position + Vector3.new(0, 3, 0)
    local hit = Workspace:Raycast(origin, Vector3.new(0, -30, 0), rayParams)
    local groundY = hit and hit.Position.Y or (cframe.Position.Y - 3)
    local _, yaw, _ = cframe:ToOrientation()
    local base = CFrame.new(cframe.Position.X, groundY, cframe.Position.Z) * CFrame.Angles(0, yaw + math.pi, 0)
    ST.tvBase = base

    local tvModel = new("Model", {Name = "TV_Antiga_CCTV"}, folder)
    local woodDark = Color3.fromRGB(40, 28, 20)
    local black = Color3.fromRGB(20, 20, 20)

    for _, sx in ipairs({-1.5, 1.5}) do
        for _, sz in ipairs({-0.6, 0.6}) do
            new("Part", {
                Size = Vector3.new(0.2, 0.9, 0.2), CFrame = base * CFrame.new(sx, 0.45, sz),
                Color = woodDark, Material = Enum.Material.Wood, Anchored = true, CanCollide = false,
            }, tvModel)
        end
    end

    local body = new("Part", {
        Name = "TVBody", Size = Vector3.new(3.8, 2.6, 1.8), CFrame = base * CFrame.new(0, 2.2, 0),
        Color = Color3.fromRGB(60, 45, 35), Material = Enum.Material.Wood, Anchored = true,
    }, tvModel)

    local screen = new("Part", {
        Name = "TVScreen", Size = Vector3.new(2.4, 1.9, 0.1),
        CFrame = body.CFrame * CFrame.new(0.4, 0, -0.86),
        Color = Color3.fromRGB(20, 30, 25), Material = Enum.Material.Glass, Anchored = true,
    }, tvModel)

    for _, y in ipairs({0.5, -0.1}) do
        new("Part", {
            Shape = Enum.PartType.Ball, Size = Vector3.new(0.3, 0.3, 0.3),
            CFrame = body.CFrame * CFrame.new(-1.35, y, -0.92),
            Color = Color3.fromRGB(170, 170, 170), Material = Enum.Material.Metal,
            Anchored = true, CanCollide = false,
        }, tvModel)
    end

    for _, side in ipairs({-1, 1}) do
        new("Part", {
            Size = Vector3.new(0.06, 1.4, 0.06),
            CFrame = body.CFrame * CFrame.new(0, 1.3, 0)
                * CFrame.Angles(0, 0, math.rad(side * 25)) * CFrame.new(0, 0.7, 0),
            Color = black, Material = Enum.Material.Metal, Anchored = true, CanCollide = false,
        }, tvModel)
    end

    for _, j in ipairs({{0.4, Color3.fromRGB(255, 205, 0)}, {0.85, Color3.fromRGB(245, 245, 245)}}) do
        new("Part", {
            Shape = Enum.PartType.Cylinder, Size = Vector3.new(0.12, 0.26, 0.26),
            CFrame = base * CFrame.new(j[1], 1.7, 0.93) * ALONG_Z,
            Color = j[2], Material = Enum.Material.Metal, Anchored = true, CanCollide = false,
        }, tvModel)
    end

    new("ClickDetector", {MaxActivationDistance = 15}, screen)

    tvModel.PrimaryPart = screen
    ST.activeTV = tvModel
    ST.activeTVScreen = screen
    return tvModel
end

-- ===================== 1B. NVR =====================
local function createNVR(cframe)
    if ST.nvrModel then ST.nvrModel:Destroy() end

    local origin = cframe.Position + Vector3.new(0, 3, 0)
    local hit = Workspace:Raycast(origin, Vector3.new(0, -30, 0), rayParams)
    local groundY = hit and hit.Position.Y or (cframe.Position.Y - 3)
    local _, yaw, _ = cframe:ToOrientation()
    local base = CFrame.new(cframe.Position.X, groundY, cframe.Position.Z) * CFrame.Angles(0, yaw + math.pi, 0)

    local model = new("Model", {Name = "NVR_TuboCAM"}, folder)

    local function mkPart(size, cf, color, material, shape, collide)
        local p = Instance.new("Part")
        if shape then p.Shape = shape end
        p.Size = size
        p.CFrame = cf
        p.Color = color
        p.Material = material or Enum.Material.SmoothPlastic
        p.Anchored = true
        p.CanCollide = collide or false
        p.Parent = model
        return p
    end

    local metal = Color3.fromRGB(45, 45, 50)
    mkPart(Vector3.new(5, 0.2, 3), base * CFrame.new(0, 2.1, 0), metal, Enum.Material.Metal, nil, true)
    for _, sx in ipairs({-2.3, 2.3}) do
        for _, sz in ipairs({-1.3, 1.3}) do
            mkPart(Vector3.new(0.2, 2, 0.2), base * CFrame.new(sx, 1, sz), metal, Enum.Material.Metal)
        end
    end

    local body = mkPart(Vector3.new(4.4, 0.9, 2.6), base * CFrame.new(0, 2.65, 0), Color3.fromRGB(22, 22, 25), nil, nil, true)
    local front = mkPart(Vector3.new(4.4, 0.9, 0.05), base * CFrame.new(0, 2.65, -1.325), Color3.fromRGB(14, 14, 17))

    local sg = new("SurfaceGui", {Face = Enum.NormalId.Front, CanvasSize = Vector2.new(440, 90), LightInfluence = 0}, front)

    local function label(text, x, y, w, h, size, color)
        return new("TextLabel", {
            BackgroundTransparency = 1, Position = UDim2.fromOffset(x, y), Size = UDim2.fromOffset(w, h),
            Text = text, TextSize = size, TextColor3 = color, Font = Enum.Font.GothamBold,
        }, sg)
    end

    local logo = new("Frame", {BackgroundTransparency = 1, Position = UDim2.fromOffset(24, 22), Size = UDim2.fromOffset(210, 40)}, sg)
    new("UIListLayout", {
        FillDirection = Enum.FillDirection.Horizontal, VerticalAlignment = Enum.VerticalAlignment.Center,
        SortOrder = Enum.SortOrder.LayoutOrder, Padding = UDim.new(0, 5),
    }, logo)

    local function logoText(text, order)
        new("TextLabel", {
            LayoutOrder = order, BackgroundTransparency = 1, AutomaticSize = Enum.AutomaticSize.X,
            Size = UDim2.new(0, 0, 1, 0), Text = text, TextSize = 30,
            TextColor3 = Color3.fromRGB(255, 255, 255), Font = Enum.Font.GothamBlack,
        }, logo)
    end
    local function logoSquare(color, order)
        new("Frame", {LayoutOrder = order, Size = UDim2.fromOffset(18, 18), BackgroundColor3 = color, BorderSizePixel = 0}, logo)
    end
    logoText("Tubo", 1)
    logoSquare(Color3.fromRGB(225, 30, 30), 2)
    logoText("CAM", 3)
    logoSquare(Color3.fromRGB(245, 245, 245), 4)

    label("NETWORK VIDEO RECORDER", 24, 68, 220, 12, 9, Color3.fromRGB(140, 140, 145))
    label("POWER", 270, 44, 60, 14, 10, Color3.fromRGB(150, 150, 155))
    label("REC", 330, 44, 60, 14, 10, Color3.fromRGB(150, 150, 155))
    label("ON/OFF", 385, 72, 50, 14, 9, Color3.fromRGB(150, 150, 155))

    local ledPower = mkPart(Vector3.new(0.1, 0.1, 0.1), base * CFrame.new(-0.8, 2.8, -1.35),
        Color3.fromRGB(0, 60, 25), nil, Enum.PartType.Ball)
    local ledRec = mkPart(Vector3.new(0.1, 0.1, 0.1), base * CFrame.new(-1.4, 2.8, -1.35),
        Color3.fromRGB(70, 10, 10), nil, Enum.PartType.Ball)

    local btn = mkPart(Vector3.new(0.1, 0.45, 0.45), base * CFrame.new(-1.9, 2.65, -1.38) * ALONG_Z,
        Color3.fromRGB(170, 170, 175), Enum.Material.Metal, Enum.PartType.Cylinder)
    local cd = new("ClickDetector", {MaxActivationDistance = 15}, btn)
    cd.MouseClick:Connect(toggleNVR)

    mkPart(Vector3.new(4.2, 0.7, 0.05), base * CFrame.new(0, 2.65, 1.325), Color3.fromRGB(35, 35, 40))
    mkPart(Vector3.new(0.12, 0.26, 0.26), base * CFrame.new(1.0, 2.65, 1.36) * ALONG_Z,
        Color3.fromRGB(255, 205, 0), Enum.Material.Metal, Enum.PartType.Cylinder)
    mkPart(Vector3.new(0.12, 0.26, 0.26), base * CFrame.new(1.5, 2.65, 1.36) * ALONG_Z,
        Color3.fromRGB(245, 245, 245), Enum.Material.Metal, Enum.PartType.Cylinder)
    for i = 0, 7 do
        mkPart(Vector3.new(0.18, 0.14, 0.05), base * CFrame.new(-1.9 + i * 0.22, 2.55, 1.36), Color3.fromRGB(90, 90, 95))
    end

    ST.nvrModel, ST.nvrBase, ST.nvrBody, ST.nvrButton = model, base, body, btn
    ST.nvrLedPower, ST.nvrLedRec = ledPower, ledRec
    ST.ledCache = {}
    ST.nvrOn = true
    ST.nvrBootStart = tick()
    return model
end

local function hubPoints()
    local inPos = (ST.nvrBase * CFrame.new(-1.1, 2.55, 1.36)).Position
    local outPos = inPos - ST.nvrBase.LookVector * 0.4
    return inPos, outPos
end

-- ===================== 1C. CABOS RCA =====================
local function updateRCA()
    if ST.rcaModel then ST.rcaModel:Destroy() ST.rcaModel = nil end
    if not nvrExists() or not ST.tvBase then return end

    local nvrBase, tvBase = ST.nvrBase, ST.tvBase
    local rca = new("Model", {Name = "CaboRCA"}, folder)
    ST.rcaModel = rca

    local specs = {
        {Color3.fromRGB(255, 205, 0), 1.0, 0.4, 0.5},
        {Color3.fromRGB(245, 245, 245), 1.5, 0.85, 0.75},
    }

    for _, s in ipairs(specs) do
        local color, nx, tx, off = s[1], s[2], s[3], s[4]
        local nPort = (nvrBase * CFrame.new(nx, 2.65, 1.36)).Position
        local tPort = (tvBase * CFrame.new(tx, 1.7, 0.93)).Position
        local nOut = nPort - nvrBase.LookVector * off
        local tOut = tPort - tvBase.LookVector * off
        local nFloor = Vector3.new(nOut.X, floorYAt(nOut), nOut.Z)
        local tFloor = Vector3.new(tOut.X, floorYAt(tOut), tOut.Z)

        cableSeg(rca, nPort, nOut, color, 0.07)
        cableSeg(rca, nOut, nFloor, color, 0.07)
        cableSeg(rca, nFloor, tFloor, color, 0.07)
        cableSeg(rca, tFloor, tOut, color, 0.07)
        cableSeg(rca, tOut, tPort, color, 0.07)

        for _, plug in ipairs({
            nvrBase * CFrame.new(nx, 2.65, 1.5) * ALONG_Z,
            tvBase * CFrame.new(tx, 1.7, 1.08) * ALONG_Z,
        }) do
            new("Part", {
                Shape = Enum.PartType.Cylinder, Size = Vector3.new(0.25, 0.22, 0.22), CFrame = plug,
                Color = color, Material = Enum.Material.Metal,
                Anchored = true, CanCollide = false, CanQuery = false,
            }, rca)
        end
    end
end

-- ===================== 2. CABO (câmera com fio -> NVR) =====================
local function updateCable(camData)
    if camData.cable then camData.cable:Destroy() camData.cable = nil end
    if not camData.isWired or not nvrExists() then return end

    local cable = new("Model", {Name = "Cabo"}, camData.model)
    local from = (camData.lens.CFrame * CFrame.new(0, -0.55, 1.79)).Position
    local inPos, outPos = hubPoints()
    local mode = camData.cableMode

    if mode == "cima" then
        local up = Workspace:Raycast(from, Vector3.new(0, 300, 0), rayParams)
        local y = up and up.Position.Y - 0.05 or from.Y + 4
        local a = Vector3.new(from.X, y, from.Z)
        local b = Vector3.new(outPos.X, y, outPos.Z)
        cableSeg(cable, from, a)
        cableSeg(cable, a, b)
        cableSeg(cable, b, outPos)
        cableSeg(cable, outPos, inPos)
    elseif mode == "parede" then
        local b = Vector3.new(outPos.X, from.Y, outPos.Z)
        cableSeg(cable, from, b)
        cableSeg(cable, b, outPos)
        cableSeg(cable, outPos, inPos)
    else
        local a = Vector3.new(from.X, floorYAt(from), from.Z)
        local b = Vector3.new(outPos.X, floorYAt(outPos), outPos.Z)
        cableSeg(cable, from, a)
        cableSeg(cable, a, b)
        cableSeg(cable, b, outPos)
        cableSeg(cable, outPos, inPos)
    end

    camData.cable = cable
end

-- ===================== 3. CÂMERAS =====================
local function setIR(camData, on)
    if camData.irOn == on then return end
    camData.irOn = on
    for _, led in ipairs(camData.irLeds) do
        if on then
            led.Material = Enum.Material.Neon
            led.Color = Color3.fromRGB(255, 60, 60)
        else
            led.Material = Enum.Material.SmoothPlastic
            led.Color = Color3.fromRGB(90, 20, 20)
        end
    end
    camData.irLight.Enabled = on
end

local function buildPole(camData)
    if camData.pole then camData.pole:Destroy() camData.pole = nil end
    if not camData.hasPole then return end

    local base = (camData.lens.CFrame * CFrame.new(0, -0.55, 1.79)).Position
    local hit = Workspace:Raycast(base, Vector3.new(0, -300, 0), rayParams)
    local gy = hit and hit.Position.Y or (base.Y - 3)
    local h = math.max(base.Y - gy, 0.2)
    local gray = Color3.fromRGB(70, 70, 75)

    local pole = new("Model", {Name = "Poste"}, camData.model)

    local function cyl(size, cf)
        new("Part", {
            Shape = Enum.PartType.Cylinder, Size = size, CFrame = cf, Color = gray,
            Material = Enum.Material.Metal, Anchored = true, CanCollide = false,
        }, pole)
    end

    local upright = CFrame.Angles(0, 0, math.rad(90))
    cyl(Vector3.new(h, 0.3, 0.3), CFrame.new(base.X, gy + h / 2, base.Z) * upright)
    cyl(Vector3.new(0.15, 1.6, 1.6), CFrame.new(base.X, gy + 0.075, base.Z) * upright)

    camData.pole = pole
end

local function createCameraModel(name, cframe, isWired, hasPole)
    local model = new("Model", {Name = name}, folder)

    local BLOCK, BALL, CYL = Enum.PartType.Block, Enum.PartType.Ball, Enum.PartType.Cylinder
    local bodyColor = isWired and Color3.fromRGB(55, 55, 60) or Color3.fromRGB(238, 238, 242)
    local ledColor = isWired and Color3.fromRGB(255, 170, 0) or Color3.fromRGB(0, 255, 150)
    local black = Color3.fromRGB(25, 25, 28)

    local function mk(shape, size, cf, color, material, transparency)
        return new("Part", {
            Shape = shape, Size = size, CFrame = cf, Color = color,
            Material = material or Enum.Material.SmoothPlastic,
            Transparency = transparency or 0, Anchored = true, CanCollide = false,
        }, model)
    end

    local body = mk(CYL, Vector3.new(1.8, 0.8, 0.8), cframe * ALONG_Z, bodyColor)
    mk(CYL, Vector3.new(0.08, 0.82, 0.82), cframe * CFrame.new(0, 0, -0.65) * ALONG_Z, Color3.fromRGB(30, 70, 160))
    mk(CYL, Vector3.new(0.12, 0.82, 0.82), cframe * CFrame.new(0, 0, -0.9) * ALONG_Z, black)
    mk(CYL, Vector3.new(0.08, 0.38, 0.38), cframe * CFrame.new(0, 0, -0.95) * ALONG_Z, Color3.fromRGB(10, 20, 45), Enum.Material.Glass)
    mk(BLOCK, Vector3.new(0.9, 0.08, 1.6), cframe * CFrame.new(0, 0.46, -0.2), bodyColor)

    local irLeds = {}
    for i = 0, 7 do
        local cf = cframe * CFrame.Angles(0, 0, math.rad(i * 45)) * CFrame.new(0.27, 0, -0.97)
        table.insert(irLeds, mk(BALL, Vector3.new(0.07, 0.07, 0.07), cf, Color3.fromRGB(90, 20, 20)))
    end

    local statusLed = mk(BALL, Vector3.new(0.08, 0.08, 0.08), cframe * CFrame.new(0.4, 0.1, 0.5), ledColor, Enum.Material.Neon)

    if not isWired then
        for _, side in ipairs({-1, 1}) do
            local cf = cframe * CFrame.new(side * 0.25, 0.3, 0.85)
                * CFrame.Angles(0, 0, math.rad(-side * 12))
                * CFrame.new(0, 0.5, 0)
                * CFrame.Angles(0, 0, math.rad(90))
            mk(CYL, Vector3.new(1, 0.07, 0.07), cf, black)
        end
    end

    mk(BALL, Vector3.new(0.25, 0.25, 0.25), cframe * CFrame.new(0, -0.5, 0), bodyColor)
    mk(BLOCK, Vector3.new(0.12, 0.12, 0.8), cframe * CFrame.new(0, -0.55, 0.4), bodyColor)
    mk(BLOCK, Vector3.new(0.5, 0.5, 0.06), cframe * CFrame.new(0, -0.55, 0.8), bodyColor)

    local lens = mk(BLOCK, Vector3.new(0.2, 0.2, 0.05), cframe * CFrame.new(0, 0, -0.99), black, nil, 1)

    local irLight = new("PointLight", {Color = Color3.fromRGB(255, 70, 70), Range = 16, Brightness = 1.5, Enabled = false}, lens)
    local camLight = new("SpotLight", {
        Color = Color3.fromRGB(255, 244, 214), Range = 40, Angle = 70, Brightness = 3,
        Face = Enum.NormalId.Front, Enabled = false,
    }, lens)

    model.PrimaryPart = lens
    local camData = {
        lens = lens, isWired = isWired, name = name, model = model, cable = nil,
        irLeds = irLeds, irLight = irLight, irOn = nil,
        statusLed = statusLed, ledColor = ledColor,
        motionUntil = nil, motionName = nil, motionHRP = nil,
        hasPole = hasPole, pole = nil, cableMode = "baixo",
        camLight = camLight, lightOn = false,
        enabled = true,
    }
    setIR(camData, isDark())
    buildPole(camData)
    updateCable(camData)

    if isWired then
        local cd = new("ClickDetector", {MaxActivationDistance = 20}, body)
        cd.MouseClick:Connect(function()
            if FN.openCableMenu then FN.openCableMenu(camData) end
        end)
    end

    return camData
end

-- ===================== 4. INTERFACE =====================
UI.gui = new("ScreenGui", {Name = "RetroTVGui", ResetOnSpawn = false}, player.PlayerGui)

-- ----- Sistema de chuvisco -----
local function makeNoise(parent, count, zIndex)
    local pool = {}
    for i = 1, count do
        pool[i] = new("Frame", {BorderSizePixel = 0, Visible = false, ZIndex = zIndex or 1}, parent)
    end
    local lastActive = 0

    return function(intensity, areaW, areaH, sMin, sMax, alphaMin)
        areaW, areaH = math.floor(areaW), math.floor(areaH)
        local active = math.floor(count * math.clamp(intensity, 0, 1))
        for i = 1, active do
            local f = pool[i]
            local g = math.random(80, 255)
            f.BackgroundColor3 = Color3.fromRGB(g, g, g)
            f.BackgroundTransparency = alphaMin + math.random() * (0.9 - alphaMin)
            f.Size = UDim2.fromOffset(math.random(sMin, sMax) * math.random(1, 3), math.random(sMin, sMax))
            f.Position = UDim2.fromOffset(math.random(0, areaW), math.random(0, areaH))
            f.Visible = true
        end
        for i = active + 1, lastActive do
            pool[i].Visible = false
        end
        lastActive = active
    end
end

-- Chuvisco em tela cheia + listras + SEM SINAL
do
    UI.static = new("Frame", {
        Size = UDim2.new(1, 0, 1, 0), BackgroundColor3 = Color3.fromRGB(255, 255, 255),
        BackgroundTransparency = 0.95, BorderSizePixel = 0, ClipsDescendants = true, Visible = false,
    }, UI.gui)
    UI.screenNoise = makeNoise(UI.static, 170)

    UI.bars = {}
    for i = 1, 6 do
        UI.bars[i] = new("Frame", {BackgroundColor3 = Color3.fromRGB(255, 255, 255), BorderSizePixel = 0, Visible = false}, UI.static)
    end

    UI.noSignal = new("TextLabel", {
        AnchorPoint = Vector2.new(0.5, 0.5), Size = UDim2.new(0, 420, 0, 80), Position = UDim2.new(0.5, 0, 0.5, 0),
        BackgroundTransparency = 1, Text = "SEM SINAL", TextColor3 = Color3.fromRGB(255, 255, 255),
        TextStrokeTransparency = 0, TextSize = 54, Font = Enum.Font.Code, Visible = false,
    }, UI.static)
end

-- ----- Tela da TV no mundo -----
local function attachTVNoise(screenPart)
    local sg = new("SurfaceGui", {
        Face = Enum.NormalId.Front, CanvasSize = Vector2.new(CFG.TV_W, CFG.TV_H),
        LightInfluence = 0, ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
    }, screenPart)

    local bg = new("Frame", {
        Size = UDim2.new(1, 0, 1, 0), BackgroundColor3 = Color3.fromRGB(35, 40, 38),
        BorderSizePixel = 0, ClipsDescendants = true,
    }, sg)

    local grid = new("Frame", {
        Size = UDim2.new(1, 0, 1, 0), BackgroundColor3 = Color3.fromRGB(0, 0, 0),
        BorderSizePixel = 0, ZIndex = 2, Visible = false,
    }, bg)

    ST.tvNoiseUpdate = makeNoise(bg, 140, 3)

    ST.logoX, ST.logoY = 20, 20
    ST.logoVX, ST.logoVY = math.abs(ST.logoVX), math.abs(ST.logoVY)
    ST.tvLogo = new("TextLabel", {
        Size = UDim2.fromOffset(CFG.LOGO_W, CFG.LOGO_H), Position = UDim2.fromOffset(ST.logoX, ST.logoY),
        BackgroundColor3 = Color3.fromRGB(0, 0, 0), BackgroundTransparency = 0.25, BorderSizePixel = 0,
        Text = "📺 CCTV", TextColor3 = Color3.fromRGB(0, 255, 150), TextSize = 28,
        Font = Enum.Font.GothamBold, ZIndex = 4, Visible = false,
    }, bg)

    local roll = new("Frame", {
        Size = UDim2.new(1, 0, 0, 44), BackgroundColor3 = Color3.fromRGB(255, 255, 255),
        BackgroundTransparency = 0.8, BorderSizePixel = 0, ZIndex = 8, Visible = false,
    }, bg)

    local live = new("TextLabel", {
        Position = UDim2.new(0, 16, 0, 12), Size = UDim2.fromOffset(320, 28), BackgroundTransparency = 1,
        TextColor3 = Color3.fromRGB(0, 255, 120), TextSize = 24, Font = Enum.Font.Code,
        TextXAlignment = Enum.TextXAlignment.Left, Text = "● SEM CÂMERAS", ZIndex = 7, Visible = false,
    }, bg)

    local blue = new("Frame", {
        Size = UDim2.new(1, 0, 1, 0), BackgroundColor3 = Color3.fromRGB(10, 40, 200),
        BorderSizePixel = 0, ZIndex = 10,
    }, bg)

    new("TextLabel", {
        AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -20, 0, 16), Size = UDim2.fromOffset(220, 52),
        BackgroundTransparency = 1, Text = "AV1 ↗", TextColor3 = Color3.fromRGB(255, 255, 255),
        TextSize = 44, Font = Enum.Font.Code, TextXAlignment = Enum.TextXAlignment.Right, ZIndex = 11,
    }, blue)

    ST.tvUI = {bg = bg, blue = blue, roll = roll, live = live, grid = grid}
end

local function updateTVScreen()
    local tvUI = ST.tvUI
    if not tvUI or not ST.tvNoiseUpdate or not ST.activeTVScreen or not ST.activeTVScreen.Parent then return end
    local now = tick()
    local state, p = "blue", 0
    if tvLinked() and ST.nvrOn then
        p = (now - ST.nvrBootStart) / CFG.BOOT_TIME
        state = (p < 1) and "boot" or "live"
    end

    -- hasCams = tem câmeras E a grade está ligada (OFF = tela velha)
    local hasCams = #createdCams > 0 and ST.gridMode
    local showGrid = hasCams and (state == "live" or (state == "boot" and p >= 0.85))

    tvUI.blue.Visible = (state == "blue")
    tvUI.grid.Visible = showGrid
    tvUI.live.Visible = (state == "live" and not hasCams)
    tvUI.live.Text = (#createdCams > 0) and "● CCTV" or "● SEM CÂMERAS"
    ST.tvLogo.Visible = (state == "live" and not hasCams)
    tvUI.bg.BackgroundColor3 = (state == "boot" and p < 0.2)
        and Color3.fromRGB(0, 0, 0) or Color3.fromRGB(35, 40, 38)

    if state == "blue" or not hasCams then
        if FN.gridClear then FN.gridClear() end
    elseif FN.gridUpdate and not ST.viewing then
        FN.gridUpdate(now, showGrid)
    end

    if state == "boot" and p >= 0.2 then
        tvUI.roll.Visible = true
        tvUI.roll.Position = UDim2.new(0, 0, ((now * 0.5) % 1.2) - 0.1, 0)
        tvUI.roll.BackgroundTransparency = 0.8 + (p - 0.2) * 0.2
    else
        tvUI.roll.Visible = false
    end

    if now - ST.lastTVNoise > 0.05 then
        ST.lastTVNoise = now
        local intensity = 0
        if state == "boot" then
            if p < 0.2 then
                intensity = 0
            elseif p < 0.5 then
                intensity = 0.6
            else
                intensity = 0.6 - ((p - 0.5) / 0.5) * 0.48
            end
        elseif state == "live" then
            intensity = hasCams and 0.04 or 0.06
        end
        ST.tvNoiseUpdate(intensity, CFG.TV_W, CFG.TV_H, 5, 16, 0.1)
    end
end

-- LEDs do NVR
local function setLed(led, key, on, onColor, offColor)
    if ST.ledCache[key] == on then return end
    ST.ledCache[key] = on
    led.Material = on and Enum.Material.Neon or Enum.Material.SmoothPlastic
    led.Color = on and onColor or offColor
end

local function updateNVRLeds()
    if not nvrExists() then return end
    local now = tick()
    local booting = ST.nvrOn and (now - ST.nvrBootStart) < CFG.BOOT_TIME
    local powerOn = ST.nvrOn and ((not booting) or (math.floor(now * 3) % 2 == 0))
    setLed(ST.nvrLedPower, "p", powerOn, Color3.fromRGB(0, 255, 90), Color3.fromRGB(0, 60, 25))
    local recOn = ST.nvrOn and (not booting) and #createdCams > 0 and (math.floor(now * 2) % 2 == 0)
    setLed(ST.nvrLedRec, "r", recOn, Color3.fromRGB(255, 30, 30), Color3.fromRGB(70, 10, 10))
end

-- REC + data/hora
UI.rec = new("TextLabel", {
    Size = UDim2.new(0, 120, 0, 28), Position = UDim2.new(1, -140, 0, 15), BackgroundTransparency = 1,
    TextColor3 = Color3.fromRGB(255, 40, 40), Text = "● REC", TextSize = 22, Font = Enum.Font.Code,
    TextXAlignment = Enum.TextXAlignment.Right, Visible = false,
}, UI.gui)

UI.time = new("TextLabel", {
    Size = UDim2.new(0, 320, 0, 44), Position = UDim2.new(0, 15, 0, 15), BackgroundTransparency = 1,
    TextColor3 = Color3.fromRGB(230, 230, 230), Text = "", TextSize = 16, Font = Enum.Font.Code,
    TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top, Visible = false,
}, UI.gui)

-- HUD da TV
do
    UI.hud = new("Frame", {
        Size = UDim2.new(0, 270, 0, 150), Position = UDim2.new(0.02, 0, 0.72, 0),
        BackgroundColor3 = Color3.fromRGB(25, 20, 18), BorderSizePixel = 0, Visible = false,
    }, UI.gui)
    new("UICorner", {CornerRadius = UDim.new(0, 8)}, UI.hud)

    new("TextLabel", {
        Size = UDim2.new(1, 0, 0, 28), BackgroundColor3 = Color3.fromRGB(45, 35, 30),
        TextColor3 = Color3.fromRGB(255, 180, 50), Text = "📺 TV TUBOCAM - CANAL CCTV",
        TextSize = 11, Font = Enum.Font.Code,
    }, UI.hud)

    UI.signal = new("TextLabel", {
        Size = UDim2.new(0.9, 0, 0, 24), Position = UDim2.new(0.05, 0, 0.22, 0),
        BackgroundColor3 = Color3.fromRGB(15, 15, 15), TextColor3 = Color3.fromRGB(0, 255, 100),
        Text = "📶 SINAL WI-FI: 100% (EXCELENTE)", TextSize = 10, Font = Enum.Font.Code,
    }, UI.hud)

    UI.btnNext = new("TextButton", {
        Size = UDim2.new(0.9, 0, 0, 28), Position = UDim2.new(0.05, 0, 0.42, 0),
        BackgroundColor3 = Color3.fromRGB(180, 90, 20), TextColor3 = Color3.fromRGB(255, 255, 255),
        Text = "🔄 Mudar Canal (Câmera)", TextSize = 11, Font = Enum.Font.GothamBold,
    }, UI.hud)

    UI.btnExit = new("TextButton", {
        Size = UDim2.new(0.9, 0, 0, 26), Position = UDim2.new(0.05, 0, 0.78, 0),
        BackgroundColor3 = Color3.fromRGB(150, 40, 40), TextColor3 = Color3.fromRGB(255, 255, 255),
        Text = "🔌 Desligar TV", TextSize = 11, Font = Enum.Font.GothamBold,
    }, UI.hud)
end

-- Botões acima do HUD (só enquanto assiste)
local function hudButton(text, yOffset, color)
    return new("TextButton", {
        Size = UDim2.new(0, 270, 0, 28), Position = UDim2.new(0.02, 0, 0.72, yOffset),
        BackgroundColor3 = color, TextColor3 = Color3.fromRGB(255, 255, 255),
        Text = text, TextSize = 12, Font = Enum.Font.GothamBold, Visible = false,
    }, UI.gui)
end

UI.btnNight = hudButton("🌙 Visão Noturna: OFF", -34, Color3.fromRGB(35, 50, 35))
UI.btnMotion = hudButton("🚶 Detecção de Movimento: OFF", -68, Color3.fromRGB(60, 50, 50))
UI.btnLight = hudButton("💡 Luz da Câmera: OFF", -102, Color3.fromRGB(70, 65, 30))

local function refreshLightButton()
    local c = ST.currentCamData
    local on = c and c.lightOn
    UI.btnLight.Text = on and "💡 Luz da Câmera: ON" or "💡 Luz da Câmera: OFF"
    UI.btnLight.BackgroundColor3 = on and Color3.fromRGB(190, 150, 20) or Color3.fromRGB(70, 65, 30)
end

UI.btnLight.MouseButton1Click:Connect(function()
    local c = ST.currentCamData
    if not c then return end
    c.lightOn = not c.lightOn
    c.camLight.Enabled = c.lightOn
    refreshLightButton()
end)

-- Aviso rápido (toast)
do
    UI.toast = new("TextLabel", {
        AnchorPoint = Vector2.new(0.5, 0), Size = UDim2.new(0, 330, 0, 34), Position = UDim2.new(0.5, 0, 0.12, 0),
        BackgroundColor3 = Color3.fromRGB(20, 20, 25), TextColor3 = Color3.fromRGB(255, 255, 255),
        TextSize = 14, Font = Enum.Font.GothamBold, Visible = false, ZIndex = 20,
    }, UI.gui)
    new("UICorner", {CornerRadius = UDim.new(0, 8)}, UI.toast)

    local toastId = 0
    FN.toast = function(text)
        toastId += 1
        local my = toastId
        UI.toast.Text = text
        UI.toast.Visible = true
        task.delay(2.5, function()
            if toastId == my then UI.toast.Visible = false end
        end)
    end
end

-- Painel de instalação
UI.control = new("Frame", {
    Size = UDim2.new(0, 210, 0, 158), Position = UDim2.new(0.02, 0, 0.05, 0),
    BackgroundColor3 = Color3.fromRGB(20, 20, 25),
}, UI.gui)

local function makeButton(text, y, color)
    return new("TextButton", {
        Size = UDim2.new(0.9, 0, 0, 28), Position = UDim2.new(0.05, 0, 0, y),
        BackgroundColor3 = color, TextColor3 = Color3.fromRGB(255, 255, 255),
        Text = text, Font = Enum.Font.GothamBold,
    }, UI.control)
end

UI.btnTV = makeButton("📺 Colocar TV Antiga", 10, Color3.fromRGB(120, 70, 30))
UI.btnNVR = makeButton("🖥 Colocar NVR", 46, Color3.fromRGB(70, 70, 80))
UI.btnWifi = makeButton("📶 Instalar Cam Wi-Fi", 82, Color3.fromRGB(0, 140, 90))
UI.btnWired = makeButton("🔌 Instalar Cam com Fio", 118, Color3.fromRGB(60, 80, 110))

-- ----- Altura + posição das câmeras -----
local function getCameraSpawnCFrame()
    local feetY = rootPart.Position.Y - 3
    local y = feetY + ST.camHeight
    local look = rootPart.CFrame.LookVector
    local flat = Vector3.new(look.X, 0, look.Z)
    if flat.Magnitude < 0.01 then flat = Vector3.new(0, 0, -1) end
    flat = flat.Unit

    local origin = Vector3.new(rootPart.Position.X, y, rootPart.Position.Z)
    local hit = Workspace:Raycast(origin, flat * 60, rayParams)

    if hit and math.abs(hit.Normal.Y) < 0.5 then
        local pos = hit.Position + hit.Normal * 0.84
        return CFrame.lookAt(pos, pos + hit.Normal), true
    end

    -- Terreno aberto: a câmera nasce virada PARA o avatar
    local pos = origin + flat * 6
    return CFrame.lookAt(pos, pos - flat), false
end

do
    UI.height = new("Frame", {
        Size = UDim2.new(0, 210, 0, 70), Position = UDim2.new(0.02, 0, 0.05, 168),
        BackgroundColor3 = Color3.fromRGB(20, 20, 25),
    }, UI.gui)

    UI.heightLabel = new("TextLabel", {
        Size = UDim2.new(0.9, 0, 0, 24), Position = UDim2.new(0.05, 0, 0, 6), BackgroundTransparency = 1,
        TextColor3 = Color3.fromRGB(255, 255, 255), Font = Enum.Font.GothamBold, TextSize = 12,
        Text = "📏 Altura: 3.0",
    }, UI.height)

    local function hbtn(text, x, color)
        return new("TextButton", {
            Size = UDim2.new(0.43, 0, 0, 28), Position = UDim2.new(x, 0, 0, 34), BackgroundColor3 = color,
            TextColor3 = Color3.fromRGB(255, 255, 255), Text = text, TextSize = 14, Font = Enum.Font.GothamBold,
        }, UI.height)
    end
    UI.btnHDown = hbtn("▼ Baixar", 0.05, Color3.fromRGB(90, 60, 60))
    UI.btnHUp = hbtn("▲ Subir", 0.52, Color3.fromRGB(60, 90, 60))
end

local function changeHeight(delta)
    local newH = math.clamp(ST.camHeight + delta, 1, 15)
    local real = newH - ST.camHeight
    if real == 0 then return end
    ST.camHeight = newH
    UI.heightLabel.Text = string.format("📏 Altura: %.1f", ST.camHeight)

    local last = createdCams[#createdCams]
    if last and last.model.Parent then
        last.model:PivotTo(last.model:GetPivot() + Vector3.new(0, real, 0))
        buildPole(last)
        updateCable(last)
    end
end

UI.btnHDown.MouseButton1Click:Connect(function() changeHeight(-CFG.HEIGHT_STEP) end)
UI.btnHUp.MouseButton1Click:Connect(function() changeHeight(CFG.HEIGHT_STEP) end)

-- ----- Lista de câmeras: ON/OFF + excluir -----
do
    UI.delete = new("Frame", {
        Size = UDim2.new(0, 210, 0, 150), Position = UDim2.new(0.02, 0, 0.05, 248),
        BackgroundColor3 = Color3.fromRGB(20, 20, 25),
    }, UI.gui)

    new("TextLabel", {
        Size = UDim2.new(0.9, 0, 0, 22), Position = UDim2.new(0.05, 0, 0, 4), BackgroundTransparency = 1,
        TextColor3 = Color3.fromRGB(255, 200, 120), Font = Enum.Font.GothamBold, TextSize = 11,
        Text = "📹 Câmeras: ON/OFF  •  🗑 excluir",
    }, UI.delete)

    UI.deleteList = new("ScrollingFrame", {
        Size = UDim2.new(0.9, 0, 0, 114), Position = UDim2.new(0.05, 0, 0, 28), BackgroundTransparency = 1,
        BorderSizePixel = 0, ScrollBarThickness = 4, CanvasSize = UDim2.new(0, 0, 0, 0),
        AutomaticCanvasSize = Enum.AutomaticSize.Y,
    }, UI.delete)

    new("UIListLayout", {Padding = UDim.new(0, 4), SortOrder = Enum.SortOrder.LayoutOrder}, UI.deleteList)
end

local function refreshDeleteList()
    for _, child in ipairs(UI.deleteList:GetChildren()) do
        if child:IsA("GuiObject") then child:Destroy() end
    end

    if #createdCams == 0 then
        new("TextLabel", {
            Size = UDim2.new(1, 0, 0, 24), BackgroundTransparency = 1, TextColor3 = Color3.fromRGB(150, 150, 150),
            Font = Enum.Font.Gotham, TextSize = 11, Text = "Nenhuma câmera instalada",
        }, UI.deleteList)
        return
    end

    for i, c in ipairs(createdCams) do
        c.name = (c.isWired and "CAM-Fio-" or "CAM-WiFi-") .. i
        c.model.Name = c.name

        local row = new("Frame", {
            LayoutOrder = i, Size = UDim2.new(1, -6, 0, 26), BackgroundTransparency = 1,
        }, UI.deleteList)

        local tog = new("TextButton", {
            Size = UDim2.new(0, 50, 1, 0),
            BackgroundColor3 = c.enabled and Color3.fromRGB(30, 130, 50) or Color3.fromRGB(120, 35, 35),
            TextColor3 = Color3.fromRGB(255, 255, 255), Font = Enum.Font.GothamBold, TextSize = 11,
            Text = c.enabled and "ON" or "OFF",
        }, row)
        tog.MouseButton1Click:Connect(function()
            if FN.setCamEnabled then FN.setCamEnabled(c, not c.enabled) end
        end)

        local del = new("TextButton", {
            Position = UDim2.new(0, 54, 0, 0), Size = UDim2.new(1, -54, 1, 0),
            BackgroundColor3 = c.isWired and Color3.fromRGB(60, 80, 110) or Color3.fromRGB(0, 110, 75),
            TextColor3 = Color3.fromRGB(255, 255, 255), Font = Enum.Font.GothamBold, TextSize = 11,
            Text = "🗑 " .. i .. " - " .. (c.isWired and "Fio" or "Wi-Fi"),
        }, row)
        del.MouseButton1Click:Connect(function() FN.deleteCamera(i) end)
    end
end

-- ===================== DETECÇÃO DE MOVIMENTO =====================
do
    UI.alarm = new("Sound", {Volume = 0.8}, UI.gui)

    UI.sign = new("Frame", {
        AnchorPoint = Vector2.new(0.5, 0), Size = UDim2.new(0, 300, 0, 120), Position = UDim2.new(0.5, 0, 0, 10),
        BackgroundColor3 = Color3.fromRGB(255, 255, 255), BorderSizePixel = 0, Visible = false,
    }, UI.gui)
    new("UICorner", {CornerRadius = UDim.new(0, 14)}, UI.sign)
    UI.signStroke = new("UIStroke", {Thickness = 4, Color = CFG.BLACK}, UI.sign)

    local icon = new("TextLabel", {
        Size = UDim2.new(1, 0, 0, 38), Position = UDim2.new(0, 0, 0, 6), BackgroundTransparency = 1,
        TextColor3 = CFG.BLACK, Text = "📹 )))  ❗", TextSize = 28, Font = Enum.Font.GothamBlack,
    }, UI.sign)

    if CFG.ICON_IMAGE_ID ~= "" then
        icon.Text = ""
        new("ImageLabel", {
            AnchorPoint = Vector2.new(0.5, 0), Size = UDim2.fromOffset(42, 42), Position = UDim2.new(0.5, 0, 0, 4),
            BackgroundTransparency = 1, Image = CFG.ICON_IMAGE_ID,
        }, UI.sign)
    end

    new("TextLabel", {
        Size = UDim2.new(1, 0, 0, 40), Position = UDim2.new(0, 0, 0, 46), BackgroundTransparency = 1,
        TextColor3 = CFG.BLACK, Text = "MOTION DETECTION", TextSize = 25, Font = Enum.Font.GothamBlack,
    }, UI.sign)

    UI.signInfo = new("TextLabel", {
        Size = UDim2.new(1, 0, 0, 22), Position = UDim2.new(0, 0, 0, 90), BackgroundTransparency = 1,
        TextColor3 = Color3.fromRGB(60, 60, 60), Text = "", TextSize = 13, Font = Enum.Font.Code,
    }, UI.sign)

    UI.alertBorder = new("Frame", {Size = UDim2.new(1, 0, 1, 0), BackgroundTransparency = 1, Visible = false}, UI.static)
    UI.borderStroke = new("UIStroke", {Thickness = 14, Color = CFG.RED}, UI.alertBorder)

    UI.trackBox = new("Frame", {AnchorPoint = Vector2.new(0.5, 0.5), BackgroundTransparency = 1, Visible = false}, UI.static)
    new("UIStroke", {Thickness = 2, Color = CFG.RED}, UI.trackBox)
    UI.trackLabel = new("TextLabel", {
        Size = UDim2.new(0, 200, 0, 16), Position = UDim2.new(0.5, -100, 0, -18), BackgroundTransparency = 1,
        TextColor3 = CFG.RED, TextStrokeTransparency = 0, TextSize = 12, Font = Enum.Font.Code, Text = "",
    }, UI.trackBox)

    UI.motionMenu = new("Frame", {
        Size = UDim2.new(0, 210, 0, 44), Position = UDim2.new(0.02, 0, 0.05, 408),
        BackgroundColor3 = Color3.fromRGB(20, 20, 25),
    }, UI.gui)
    UI.btnMotionPanel = new("TextButton", {
        Size = UDim2.new(0.9, 0, 0, 28), Position = UDim2.new(0.05, 0, 0, 8),
        BackgroundColor3 = Color3.fromRGB(60, 50, 50), TextColor3 = Color3.fromRGB(255, 255, 255),
        Font = Enum.Font.GothamBold, TextSize = 10, Text = "🚶 Detecção de Movimento: OFF",
    }, UI.motionMenu)
end

local function playAlarm()
    if CFG.ALARM_SOUND_ID == "" then return end
    if tick() - ST.lastAlarm < CFG.ALERT_HOLD then return end
    ST.lastAlarm = tick()
    UI.alarm.SoundId = CFG.ALARM_SOUND_ID
    UI.alarm:Play()
end

local function refreshMotionButtons()
    local txt = ST.motionOn and "🚶 Detecção de Movimento: ON" or "🚶 Detecção de Movimento: OFF"
    local col = ST.motionOn and Color3.fromRGB(150, 40, 40) or Color3.fromRGB(60, 50, 50)
    UI.btnMotion.Text = txt
    UI.btnMotion.BackgroundColor3 = col
    UI.btnMotionPanel.Text = txt
    UI.btnMotionPanel.BackgroundColor3 = col
end

local function resetMotion()
    ST.alertUntil = 0
    UI.sign.Visible = false
    UI.alertBorder.Visible = false
    UI.trackBox.Visible = false
    for _, c in ipairs(createdCams) do
        c.motionUntil = nil
        c.motionName = nil
        c.motionHRP = nil
        c.statusLed.Color = baseLed(c)
    end
end

local function setMotion(on)
    ST.motionOn = on
    if not on then resetMotion() end
    refreshMotionButtons()
end

UI.btnMotion.MouseButton1Click:Connect(function() setMotion(not ST.motionOn) end)
UI.btnMotionPanel.MouseButton1Click:Connect(function() setMotion(not ST.motionOn) end)

-- ----- Botão: Grade na TV ON/OFF (NOVO v13) -----
do
    UI.gridMenu = new("Frame", {
        Size = UDim2.new(0, 210, 0, 44), Position = UDim2.new(0.02, 0, 0.05, 462),
        BackgroundColor3 = Color3.fromRGB(20, 20, 25),
    }, UI.gui)
    UI.btnGridMode = new("TextButton", {
        Size = UDim2.new(0.9, 0, 0, 28), Position = UDim2.new(0.05, 0, 0, 8),
        BackgroundColor3 = Color3.fromRGB(30, 130, 50), TextColor3 = Color3.fromRGB(255, 255, 255),
        Font = Enum.Font.GothamBold, TextSize = 11, Text = "🔲 Grade na TV: ON",
    }, UI.gridMenu)
    UI.btnGridMode.MouseButton1Click:Connect(function()
        ST.gridMode = not ST.gridMode
        UI.btnGridMode.Text = ST.gridMode and "🔲 Grade na TV: ON" or "📺 Tela velha (grade OFF)"
        UI.btnGridMode.BackgroundColor3 = ST.gridMode and Color3.fromRGB(30, 130, 50) or Color3.fromRGB(120, 70, 30)
        FN.toast(ST.gridMode and "🔲 Grade ligada" or "📺 Tela velha")
    end)
end

local function refreshNpcs()
    ST.npcCache = {}
    for _, d in ipairs(Workspace:GetDescendants()) do
        if d:IsA("Humanoid") and d.Health > 0 then
            local model = d.Parent
            if model and model:IsA("Model")
                and not Players:GetPlayerFromCharacter(model)
                and not model:IsDescendantOf(folder) then
                local hrp = model:FindFirstChild("HumanoidRootPart") or model.PrimaryPart
                if hrp then
                    table.insert(ST.npcCache, {model = model, hrp = hrp, name = model.Name})
                end
            end
        end
    end
end

local function getSpeed(hrp, now)
    local pos = hrp.Position
    local rec = ST.lastPos[hrp]
    ST.lastPos[hrp] = {pos, now}
    if not rec then return 0 end
    local dt = now - rec[2]
    if dt <= 0 then return 0 end
    return (pos - rec[1]).Magnitude / dt
end

local function scanMotion()
    local now = tick()
    if CFG.DETECT_NPCS and now - ST.lastNpcRefresh > 3 then
        ST.lastNpcRefresh = now
        refreshNpcs()
    end

    local targets = {}
    if CFG.DETECT_PLAYERS then
        for _, plr in ipairs(Players:GetPlayers()) do
            if plr ~= player or CFG.DETECT_SELF then
                local char = plr.Character
                local hrp = char and char:FindFirstChild("HumanoidRootPart")
                if hrp then
                    table.insert(targets, {char = char, hrp = hrp, name = plr.DisplayName, speed = getSpeed(hrp, now)})
                end
            end
        end
    end
    if CFG.DETECT_NPCS then
        for _, n in ipairs(ST.npcCache) do
            if n.model.Parent and n.hrp.Parent then
                table.insert(targets, {char = n.model, hrp = n.hrp, name = n.name, speed = getSpeed(n.hrp, now)})
            end
        end
    end

    local cosLimit = math.cos(math.rad(CFG.MOTION_HALF_FOV))

    for _, c in ipairs(createdCams) do
        if not c.enabled then continue end

        local lensCF = c.lens.CFrame
        local look = lensCF.LookVector
        local found = nil

        for _, t in ipairs(targets) do
            if t.speed > CFG.MOTION_MIN_SPEED then
                local toTarget = t.hrp.Position - lensCF.Position
                local dist = toTarget.Magnitude
                if dist > 0.5 and dist <= CFG.MOTION_RANGE and look:Dot(toTarget.Unit) >= cosLimit then
                    local hit = Workspace:Raycast(lensCF.Position, toTarget, rayParams)
                    if hit == nil or hit.Instance:IsDescendantOf(t.char) then
                        found = t
                        break
                    end
                end
            end
        end

        if found then
            local isNew = (not c.motionUntil) or c.motionUntil < now
            c.motionUntil = now + CFG.ALERT_HOLD
            c.motionName = found.name
            c.motionHRP = found.hrp
            ST.alertUntil = now + CFG.ALERT_HOLD
            UI.signInfo.Text = c.name .. "  •  " .. found.name
            if isNew then playAlarm() end
        end

        if c.motionUntil and now < c.motionUntil then
            c.statusLed.Color = (math.floor(now * 4) % 2 == 0) and CFG.RED or c.ledColor
        else
            c.statusLed.Color = c.ledColor
        end
    end
end

-- ===================== GRADE MULTI-VIEW =====================
local function layoutFor(n)
    if n <= 1 then return 1, 1 end
    if n <= 4 then return 2, 2 end
    if n <= 9 then return 3, 3 end
    return 4, 4
end

local function clearGrid()
    for _, cell in ipairs(GRID.cells) do
        if cell.frame then cell.frame:Destroy() end
    end
    GRID.cells = {}
    GRID.count = 0
end

FN.gridClear = function()
    if GRID.count == 0 and #GRID.cells == 0 then return end
    clearGrid()
end

-- Libera o mundo e os avatares clonados de um quadrado
FN.releaseCell = function(cell)
    if cell.world then
        cell.world:Destroy()
        cell.world = nil
    end
    for m, e in pairs(cell.chars) do
        e.model:Destroy()
        cell.chars[m] = nil
    end
    cell.styleKey = nil
    cell.lastRefresh = 0
    cell.dbgMsg = "aguardando..."
end

local function buildGrid()
    clearGrid()
    local frame = ST.tvUI and ST.tvUI.grid
    if not frame then return end
    local n = math.min(#createdCams, CFG.GRID_MAX_CAMS)
    if n == 0 then return end

    local cols, rows = layoutFor(n)
    local gap = 4
    local cellW = (CFG.TV_W - gap * (cols + 1)) / cols
    local cellH = (CFG.TV_H - gap * (rows + 1)) / rows
    local labelH = math.clamp(math.floor(cellH * 0.13), 12, 28)

    for idx = 1, cols * rows do
        local r = math.floor((idx - 1) / cols)
        local c = (idx - 1) % cols

        local f = new("Frame", {
            Position = UDim2.fromOffset(gap + c * (cellW + gap), gap + r * (cellH + gap)),
            Size = UDim2.fromOffset(cellW, cellH), BackgroundColor3 = Color3.fromRGB(0, 0, 0),
            BorderSizePixel = 0, ClipsDescendants = true,
        }, frame)

        local stroke = new("UIStroke", {Thickness = 1.5, Color = Color3.fromRGB(70, 75, 80)}, f)

        local cell = {
            frame = f, stroke = stroke, cam = nil, vp = nil, vcam = nil, rec = nil,
            off = nil, dbg = nil, dbgMsg = "aguardando...",
            world = nil, chars = {}, lastRefresh = 0, lastChars = 0, styleKey = nil,
        }

        local cam = createdCams[idx]
        if idx <= n and cam then
            cell.cam = cam

            local vp = new("ViewportFrame", {
                Size = UDim2.new(1, 0, 1, 0), BackgroundColor3 = Color3.fromRGB(0, 0, 0), BorderSizePixel = 0,
                Ambient = Color3.fromRGB(150, 150, 155), LightColor = Color3.fromRGB(255, 255, 255),
                LightDirection = Vector3.new(-1, -2, -1),
            }, f)

            local vcam = new("Camera", {FieldOfView = CFG.FEED_FOV, CFrame = cam.lens.CFrame}, vp)
            vp.CurrentCamera = vcam

            new("TextLabel", {
                BackgroundTransparency = 1, Position = UDim2.new(0, 5, 1, -(labelH + 3)),
                Size = UDim2.fromOffset(cellW - 10, labelH), Text = "Canal " .. idx,
                TextColor3 = Color3.fromRGB(255, 255, 255), TextStrokeTransparency = 0.2, TextSize = labelH,
                Font = Enum.Font.Code, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 5,
            }, f)

            local rec = new("TextLabel", {
                BackgroundTransparency = 1, AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -5, 0, 2),
                Size = UDim2.fromOffset(labelH + 4, labelH + 2), Text = "●",
                TextColor3 = Color3.fromRGB(255, 40, 40), TextSize = labelH, Font = Enum.Font.Code, ZIndex = 5,
            }, f)

            local off = new("TextLabel", {
                BackgroundTransparency = 1, Size = UDim2.new(1, 0, 1, 0), Text = "⏻ OFF",
                TextColor3 = Color3.fromRGB(120, 120, 120), TextSize = math.clamp(math.floor(cellH * 0.25), 14, 40),
                Font = Enum.Font.Code, ZIndex = 6, Visible = false,
            }, f)

            local dbg = new("TextLabel", {
                BackgroundTransparency = 1, Position = UDim2.new(0, 5, 0, 2),
                Size = UDim2.fromOffset(cellW - labelH - 14, labelH * 3), Text = "",
                TextColor3 = Color3.fromRGB(255, 255, 0), TextStrokeTransparency = 0.4,
                TextSize = math.max(labelH - 2, 10), Font = Enum.Font.Code, TextWrapped = true,
                TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top,
                ZIndex = 7, Visible = CFG.DEBUG,
            }, f)

            cell.vp, cell.vcam, cell.rec, cell.off, cell.dbg = vp, vcam, rec, off, dbg
        end
        GRID.cells[idx] = cell
    end
    GRID.count = n
end

local function gridMatches()
    local n = math.min(#createdCams, CFG.GRID_MAX_CAMS)
    if GRID.count ~= n then return false end
    for i = 1, n do
        local cell = GRID.cells[i]
        if not cell or cell.cam ~= createdCams[i] then return false end
    end
    return true
end

-- Tira uma "foto" do mundo ao redor da câmera (peças mais próximas primeiro)
local function snapshotCell(cell, now)
    local cam = cell.cam
    if not cam or not cam.model.Parent then return end

    local n = math.max(GRID.count, 1)
    local budget = math.clamp(math.floor(CFG.FEED_PART_BUDGET / n), 150, 600)
    local camPos = cam.lens.Position

    local ignore = {folder}
    for _, m in ipairs(GRID.charModels) do table.insert(ignore, m) end

    local op = OverlapParams.new()
    op.FilterType = Enum.RaycastFilterType.Exclude
    op.FilterDescendantsInstances = ignore
    op.MaxParts = 4000

    local found = Workspace:GetPartBoundsInRadius(camPos, CFG.FEED_RADIUS, op)

    local list = {}
    for _, part in ipairs(found) do
        if part:IsA("BasePart") and not part:IsA("Terrain") and part.Transparency < 1
            and part.Size.Magnitude >= 0.35 then
            local score = (part.Position - camPos).Magnitude - part.Size.Magnitude * 0.5
            table.insert(list, {part, score})
        end
    end

    -- Busca alternativa (mais lenta) se a consulta espacial não achou nada
    if #list == 0 then
        for _, d in ipairs(Workspace:GetDescendants()) do
            if d:IsA("BasePart") and not d:IsA("Terrain") and d.Transparency < 1
                and not d:IsDescendantOf(folder) then
                local dist = (d.Position - camPos).Magnitude
                if dist <= CFG.FEED_RADIUS then
                    local isChar = false
                    for _, m in ipairs(GRID.charModels) do
                        if d:IsDescendantOf(m) then isChar = true break end
                    end
                    if not isChar then table.insert(list, {d, dist}) end
                end
            end
        end
    end

    table.sort(list, function(a, b) return a[2] < b[2] end)

    local newWorld = Instance.new("Folder")
    newWorld.Name = "Mundo"
    local count = 0
    for _, item in ipairs(list) do
        if count >= budget then break end
        local c = safeClone(item[1])
        if c then
            for _, ch in ipairs(c:GetDescendants()) do
                if ch:IsA("LuaSourceContainer") or ch:IsA("Sound") then ch:Destroy() end
            end
            c.Anchored = true
            c.Parent = newWorld
            count += 1
        end
    end

    cell.dbgMsg = "peças: " .. count .. "/" .. #list

    if count == 0 and now - GRID.lastWarn > 3 then
        GRID.lastWarn = now
        warn("[TuboCAM] nenhuma peça clonada (encontradas: " .. #found .. ")")
    end

    newWorld.Parent = cell.vp
    if cell.world then cell.world:Destroy() end
    cell.world = newWorld
    cell.lastRefresh = now
    cell.styleKey = nil
end

-- Clone de personagem SEM física: só visual
local function makeCharClone(model)
    local c = safeClone(model)
    if not c then return nil end

    local links = {}
    local od, cd = model:GetDescendants(), c:GetDescendants()
    if #od == #cd then
        for i, o in ipairs(od) do
            local k = cd[i]
            if o:IsA("BasePart") and k:IsA("BasePart") and o.Name == k.Name then
                table.insert(links, {o, k})
            end
        end
    end

    for _, d in ipairs(c:GetDescendants()) do
        if d:IsA("LuaSourceContainer") or d:IsA("Sound")
            or d:IsA("BodyMover") or d:IsA("Constraint")
            or d:IsA("Animator") or d:IsA("Humanoid")
            or d:IsA("ForceField") or d:IsA("BillboardGui") then
            d:Destroy()
        elseif d:IsA("BasePart") then
            d.Anchored = true
            d.CanCollide = false
            d.CanTouch = false
            d.CanQuery = false
            d.Massless = true
            d.AssemblyLinearVelocity = Vector3.zero
            d.AssemblyAngularVelocity = Vector3.zero
        end
    end

    return c, links
end

local function syncChars(cell)
    local lens = cell.cam.lens
    local pos = lens.Position
    local look = lens.CFrame.LookVector
    local seen = {}
    local count = 0

    for _, model in ipairs(GRID.charModels) do
        if count >= CFG.MAX_CHARS then break end
        if model.Parent then
            local pivot = model:GetPivot()
            local d = pivot.Position - pos
            local dist = d.Magnitude
            if dist > 0.5 and dist <= CFG.FEED_RADIUS and look:Dot(d.Unit) > 0.2 then
                seen[model] = true
                count += 1
                local entry = cell.chars[model]
                if not entry then
                    local c, links = makeCharClone(model)
                    if c then
                        entry = {model = c, links = links}
                        cell.chars[model] = entry
                        c.Parent = cell.vp
                    end
                end
                if entry then
                    if #entry.links > 0 then
                        for _, pr in ipairs(entry.links) do
                            if pr[1].Parent and pr[2].Parent then pr[2].CFrame = pr[1].CFrame end
                        end
                    else
                        entry.model:PivotTo(pivot)
                    end
                end
            end
        end
    end

    for model, entry in pairs(cell.chars) do
        if not seen[model] then
            entry.model:Destroy()
            cell.chars[model] = nil
        end
    end
end

local function styleCell(cell)
    local dark = isDark() and not cell.cam.lightOn
    local key = (cell.world ~= nil and "w" or "n") .. (dark and "d" or "l")
    if cell.styleKey == key then return end
    cell.styleKey = key
    if not cell.world then
        cell.vp.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
    elseif dark then
        cell.vp.BackgroundColor3 = Color3.fromRGB(20, 40, 28)
        cell.vp.Ambient = Color3.fromRGB(140, 160, 140)
        cell.vp.ImageColor3 = Color3.fromRGB(175, 255, 175)
    else
        cell.vp.BackgroundColor3 = Color3.fromRGB(135, 180, 230)
        cell.vp.Ambient = Color3.fromRGB(150, 150, 155)
        cell.vp.ImageColor3 = Color3.fromRGB(255, 255, 255)
    end
end

FN.gridUpdate = function(now, visible)
    if not ST.tvUI or not ST.tvUI.grid then return end
    if not gridMatches() then buildGrid() end
    if GRID.count == 0 then return end

    if CFG.GRID_SHOW_NPCS and now - GRID.lastNpc > 3 then
        GRID.lastNpc = now
        refreshNpcs()
    end
    if now - GRID.lastChars > 0.2 then
        GRID.lastChars = now
        local list = {}
        for _, plr in ipairs(Players:GetPlayers()) do
            if plr.Character then table.insert(list, plr.Character) end
        end
        if CFG.GRID_SHOW_NPCS then
            for _, n in ipairs(ST.npcCache) do
                if n.model.Parent then table.insert(list, n.model) end
            end
        end
        GRID.charModels = list
    end

    local pick, filling = nil, false
    for _, cell in ipairs(GRID.cells) do
        if cell.cam and cell.cam.enabled and not cell.world then
            pick = cell
            filling = true
            break
        end
    end
    if not pick then
        local oldest = now - CFG.FEED_REFRESH
        for _, cell in ipairs(GRID.cells) do
            if cell.cam and cell.cam.enabled and cell.lastRefresh < oldest then
                pick = cell
                oldest = cell.lastRefresh
            end
        end
    end
    if pick and now - GRID.lastSnap > (filling and 0.12 or 0.4) then
        GRID.lastSnap = now
        local ok, err = pcall(snapshotCell, pick, now)
        if not ok then
            pick.lastRefresh = now
            pick.dbgMsg = "ERRO: " .. string.sub(tostring(err), 1, 60)
            if now - GRID.lastWarn > 3 then
                GRID.lastWarn = now
                warn("[TuboCAM] erro no feed: " .. tostring(err))
            end
        end
    end

    local blink = (math.floor(now * 2) % 2 == 0)
    for _, cell in ipairs(GRID.cells) do
        local cam = cell.cam
        if cam and cam.model.Parent then
            if not cam.enabled then
                if cell.world or next(cell.chars) ~= nil then FN.releaseCell(cell) end
                cell.vp.Visible = false
                cell.off.Visible = true
                cell.rec.TextTransparency = 1
                cell.stroke.Color = Color3.fromRGB(70, 75, 80)
                cell.stroke.Thickness = 1.5
                if cell.dbg.Text ~= "OFF" then cell.dbg.Text = "OFF" end
            else
                cell.vp.Visible = true
                cell.off.Visible = false
                cell.vcam.CFrame = cam.lens.CFrame
                styleCell(cell)
                if cam.motionUntil and now < cam.motionUntil then
                    cell.stroke.Color = blink and CFG.RED or Color3.fromRGB(70, 75, 80)
                    cell.stroke.Thickness = 3
                else
                    cell.stroke.Color = Color3.fromRGB(70, 75, 80)
                    cell.stroke.Thickness = 1.5
                end
                cell.rec.TextTransparency = blink and 0 or 1
                if visible and now - cell.lastChars > CFG.CHAR_INTERVAL then
                    cell.lastChars = now
                    local ok, err = pcall(syncChars, cell)
                    if not ok and now - GRID.lastWarn > 3 then
                        GRID.lastWarn = now
                        warn("[TuboCAM] erro avatares: " .. tostring(err))
                    end
                end
                if cell.dbg.Text ~= cell.dbgMsg then cell.dbg.Text = cell.dbgMsg end
            end
        end
    end
end

-- ===================== FECHAR / ABRIR GUI =====================
UI.panels = {UI.control, UI.height, UI.delete, UI.motionMenu, UI.gridMenu}

UI.btnToggle = new("TextButton", {
    Size = UDim2.new(0, 100, 0, 28), Position = UDim2.new(0.02, 220, 0.05, 0),
    BackgroundColor3 = Color3.fromRGB(70, 70, 80), TextColor3 = Color3.fromRGB(255, 255, 255),
    Font = Enum.Font.GothamBold, TextSize = 11, Text = "✖ Fechar GUI",
}, UI.gui)

local function applyPanels()
    for _, p in ipairs(UI.panels) do
        p.Visible = ST.panelsOpen and not ST.viewing
    end
    UI.btnToggle.Visible = not ST.viewing
    UI.btnToggle.Text = ST.panelsOpen and "✖ Fechar GUI" or "☰ Abrir GUI"
end

UI.btnToggle.MouseButton1Click:Connect(function()
    ST.panelsOpen = not ST.panelsOpen
    applyPanels()
end)

-- ===================== JANELA DO FIO =====================
do
    UI.cableMenu = new("Frame", {
        Size = UDim2.new(0, 210, 0, 175), Position = UDim2.new(0.5, -105, 0.35, 0),
        BackgroundColor3 = Color3.fromRGB(20, 20, 25), Visible = false,
    }, UI.gui)
    new("UICorner", {CornerRadius = UDim.new(0, 8)}, UI.cableMenu)

    UI.cableTitle = new("TextLabel", {
        Size = UDim2.new(0.9, 0, 0, 24), Position = UDim2.new(0.05, 0, 0, 6), BackgroundTransparency = 1,
        TextColor3 = Color3.fromRGB(255, 255, 255), Font = Enum.Font.GothamBold, TextSize = 12,
        Text = "🔌 Qual fio?",
    }, UI.cableMenu)

    local function cableBtn(text, y, mode, color)
        local b = new("TextButton", {
            Size = UDim2.new(0.9, 0, 0, 28), Position = UDim2.new(0.05, 0, 0, y), BackgroundColor3 = color,
            TextColor3 = Color3.fromRGB(255, 255, 255), Font = Enum.Font.GothamBold, TextSize = 12, Text = text,
        }, UI.cableMenu)
        b.MouseButton1Click:Connect(function()
            if mode and ST.cableTarget and ST.cableTarget.model.Parent then
                ST.cableTarget.cableMode = mode
                updateCable(ST.cableTarget)
            end
            UI.cableMenu.Visible = false
        end)
    end

    cableBtn("⬆ Cima (teto)", 34, "cima", Color3.fromRGB(60, 80, 110))
    cableBtn("⬇ Baixo (chão)", 68, "baixo", Color3.fromRGB(60, 80, 110))
    cableBtn("🧱 Parede", 102, "parede", Color3.fromRGB(60, 80, 110))
    cableBtn("✖ Fechar", 136, nil, Color3.fromRGB(90, 60, 60))
end

FN.openCableMenu = function(c)
    ST.cableTarget = c
    UI.cableTitle.Text = "🔌 Fio da " .. c.name
    UI.cableMenu.Visible = true
end

-- ===================== LEDs IR =====================
local function updateIR()
    local dark = isDark()
    for _, c in ipairs(createdCams) do
        local forced = ST.viewing and ST.nightVision and c == ST.currentCamData
        setIR(c, c.enabled and (dark or forced))
    end
end

-- ===================== 5. VISUALIZAÇÃO =====================
local function setViewUI(on)
    UI.hud.Visible = on
    UI.btnNight.Visible = on
    UI.btnMotion.Visible = on
    UI.btnLight.Visible = on
    UI.static.Visible = on
    UI.rec.Visible = on
    UI.time.Visible = on
    if on then UI.cableMenu.Visible = false end
    applyPanels()
end

local function startViewing()
    if not ST.activeTVScreen then return end
    if not tvLinked() or not ST.nvrOn then
        FN.toast("📼 Sem sinal (AV1) — ligue o NVR e os cabos")
        return
    end
    if not nvrReady() then
        FN.toast("📼 NVR iniciando... aguarde")
        return
    end
    if #createdCams == 0 then
        FN.toast("📹 Nenhuma câmera instalada")
        return
    end
    local idx = nextEnabled(ST.camIndex)
    if not idx then
        FN.toast("📹 Todas as câmeras estão OFF")
        return
    end
    ST.camIndex = idx
    ST.viewing = true
    ST.currentCamData = createdCams[idx]
    refreshLightButton()
    camera.CameraType = Enum.CameraType.Scriptable
    UI.nightEffect.Enabled = ST.nightVision
    setViewUI(true)
    updateIR()
end

local function stopViewing()
    ST.viewing = false
    camera.CameraType = Enum.CameraType.Custom
    local hum = character:FindFirstChildOfClass("Humanoid")
    if hum then camera.CameraSubject = hum end
    UI.nightEffect.Enabled = false
    setViewUI(false)
    updateIR()
end

-- Liga/desliga uma câmera
FN.setCamEnabled = function(c, on)
    if c.enabled == on then return end
    c.enabled = on
    c.statusLed.Material = on and Enum.Material.Neon or Enum.Material.SmoothPlastic
    c.statusLed.Color = baseLed(c)

    if not on then
        c.motionUntil, c.motionName, c.motionHRP = nil, nil, nil
        if c.lightOn then
            c.lightOn = false
            c.camLight.Enabled = false
        end
        for _, cell in ipairs(GRID.cells) do
            if cell.cam == c then FN.releaseCell(cell) end
        end
        if ST.viewing and ST.currentCamData == c then
            local idx = nextEnabled(ST.camIndex)
            if idx then
                ST.camIndex = idx
                ST.currentCamData = createdCams[idx]
                refreshLightButton()
            else
                stopViewing()
                FN.toast("📹 Todas as câmeras estão OFF")
            end
        end
    end

    updateIR()
    refreshDeleteList()
end

FN.deleteCamera = function(i)
    local c = createdCams[i]
    if not c then return end

    local wasViewingThis = ST.viewing and ST.currentCamData == c
    if ST.cableTarget == c then
        ST.cableTarget = nil
        UI.cableMenu.Visible = false
    end
    c.model:Destroy()
    table.remove(createdCams, i)

    if ST.camIndex > i then ST.camIndex = ST.camIndex - 1 end
    if ST.camIndex > #createdCams then ST.camIndex = 1 end
    if ST.camIndex < 1 then ST.camIndex = 1 end

    if ST.viewing then
        if #createdCams == 0 then
            stopViewing()
        elseif wasViewingThis then
            local idx = nextEnabled(ST.camIndex)
            if idx then
                ST.camIndex = idx
                ST.currentCamData = createdCams[idx]
                refreshLightButton()
            else
                stopViewing()
                FN.toast("📹 Todas as câmeras estão OFF")
            end
        end
    end

    refreshDeleteList()
end

-- ===================== 6. EVENTOS =====================
UI.btnTV.MouseButton1Click:Connect(function()
    if ST.viewing then stopViewing() end
    local tv = createRetroTV(rootPart.CFrame * CFrame.new(0, -1, -5))
    attachTVNoise(tv.TVScreen)
    tv.TVScreen.ClickDetector.MouseClick:Connect(startViewing)
    updateRCA()
end)

UI.btnNVR.MouseButton1Click:Connect(function()
    if ST.viewing then stopViewing() end
    createNVR(rootPart.CFrame * CFrame.new(-6, -1, -5))
    updateRCA()
    for _, c in ipairs(createdCams) do updateCable(c) end
end)

UI.btnWifi.MouseButton1Click:Connect(function()
    local num = #createdCams + 1
    local cf, onWall = getCameraSpawnCFrame()
    table.insert(createdCams, createCameraModel("CAM-WiFi-" .. num, cf, false, not onWall))
    refreshDeleteList()
end)

UI.btnWired.MouseButton1Click:Connect(function()
    local num = #createdCams + 1
    local cf, onWall = getCameraSpawnCFrame()
    table.insert(createdCams, createCameraModel("CAM-Fio-" .. num, cf, true, not onWall))
    refreshDeleteList()
end)

UI.btnNext.MouseButton1Click:Connect(function()
    local n = #createdCams
    if n > 0 then
        local idx = nextEnabled(ST.camIndex % n + 1)
        if idx then
            ST.camIndex = idx
            ST.currentCamData = createdCams[idx]
            refreshLightButton()
            updateIR()
        end
    end
end)

UI.btnExit.MouseButton1Click:Connect(stopViewing)

UI.btnNight.MouseButton1Click:Connect(function()
    ST.nightVision = not ST.nightVision
    UI.nightEffect.Enabled = ST.viewing and ST.nightVision
    UI.btnNight.Text = ST.nightVision and "🌙 Visão Noturna: ON" or "🌙 Visão Noturna: OFF"
    UI.btnNight.BackgroundColor3 = ST.nightVision and Color3.fromRGB(30, 120, 40) or Color3.fromRGB(35, 50, 35)
    updateIR()
end)

refreshDeleteList()
refreshMotionButtons()
applyPanels()

-- ===================== 7. LOOP PRINCIPAL =====================
RunService.RenderStepped:Connect(function(dt)
    updateTVScreen()
    updateNVRLeds()

    -- Logo quicando na TV (só na tela velha)
    local logo = ST.tvLogo
    if logo and logo.Parent and logo.Visible then
        ST.logoX = ST.logoX + ST.logoVX * dt
        ST.logoY = ST.logoY + ST.logoVY * dt
        local bounced = false

        if ST.logoX <= 0 then
            ST.logoX = 0
            ST.logoVX = math.abs(ST.logoVX)
            bounced = true
        elseif ST.logoX + CFG.LOGO_W >= CFG.TV_W then
            ST.logoX = CFG.TV_W - CFG.LOGO_W
            ST.logoVX = -math.abs(ST.logoVX)
            bounced = true
        end

        if ST.logoY <= 0 then
            ST.logoY = 0
            ST.logoVY = math.abs(ST.logoVY)
            bounced = true
        elseif ST.logoY + CFG.LOGO_H >= CFG.TV_H then
            ST.logoY = CFG.TV_H - CFG.LOGO_H
            ST.logoVY = -math.abs(ST.logoVY)
            bounced = true
        end

        if bounced then logo.TextColor3 = Color3.fromHSV(math.random(), 0.8, 1) end
        logo.Position = UDim2.fromOffset(ST.logoX, ST.logoY)
    end

    if tick() - ST.lastIR > 0.25 then
        ST.lastIR = tick()
        updateIR()
    end

    if ST.motionOn and tick() - ST.lastScan > 0.2 then
        ST.lastScan = tick()
        scanMotion()
    end

    local showSign = ST.motionOn and tick() < ST.alertUntil
    UI.sign.Visible = showSign
    if showSign then
        UI.signStroke.Color = (math.floor(tick() * 3) % 2 == 0) and CFG.RED or CFG.BLACK
    end

    local cd = ST.currentCamData
    if not ST.viewing or not cd then return end

    if not ST.activeTVScreen or not ST.activeTVScreen.Parent then
        stopViewing()
        return
    end

    if not nvrReady() then
        stopViewing()
        FN.toast("📼 NVR desligado — sem sinal (AV1)")
        return
    end

    camera.CFrame = cd.lens.CFrame

    -- Borda vermelha + caixa seguindo quem se mexe
    local tracking = ST.motionOn and cd.motionUntil and tick() < cd.motionUntil
        and cd.motionHRP and cd.motionHRP.Parent
    UI.alertBorder.Visible = tracking and true or false
    UI.trackBox.Visible = false
    if tracking then
        UI.borderStroke.Transparency = (math.floor(tick() * 3) % 2 == 0) and 0 or 0.6
        local hrpPos = cd.motionHRP.Position
        local screenPos, onScreen = camera:WorldToViewportPoint(hrpPos)
        if onScreen then
            local inset = GuiService:GetGuiInset()
            local vpSize = camera.ViewportSize
            local dist = (hrpPos - camera.CFrame.Position).Magnitude
            local h = 5.5 / (2 * dist * math.tan(math.rad(camera.FieldOfView / 2))) * vpSize.Y
            h = math.clamp(h, 40, 500)
            UI.trackBox.Size = UDim2.fromOffset(h * 0.5, h)
            UI.trackBox.Position = UDim2.fromOffset(screenPos.X - inset.X, screenPos.Y - inset.Y)
            UI.trackLabel.Text = "⚠ MOVIMENTO • " .. (cd.motionName or "?")
            UI.trackBox.Visible = true
        end
    end

    UI.time.Text = cd.name .. "\n" .. os.date("%d/%m/%Y  %H:%M:%S")
    UI.time.TextColor3 = ST.nightVision and Color3.fromRGB(120, 255, 120) or Color3.fromRGB(230, 230, 230)
    UI.rec.TextTransparency = (math.floor(tick() * 2) % 2 == 0) and 0 or 1

    local vp = camera.ViewportSize
    local intensity, bars, noSignal = 0, 0, false
    local sMin, sMax, alphaMin = 2, 6, 0.5

    if cd.isWired then
        UI.signal.Text = "🔌 CABO DE VÍDEO: 100% (ESTÁVEL)"
        UI.signal.TextColor3 = Color3.fromRGB(0, 255, 150)
        UI.static.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
        UI.static.BackgroundTransparency = 0.98
    else
        local origin = cd.lens.Position
        local target = ST.nvrBody.Position
        local dist = (target - origin).Magnitude
        local signal = 100 - (dist * CFG.SIGNAL_LOSS)

        local blocked = Workspace:Raycast(origin, target - origin, rayParams) ~= nil
        if blocked then signal = signal - CFG.WALL_PENALTY end
        signal = math.clamp(signal, 0, 100)
        local wallTag = blocked and " 🧱" or ""

        if signal > 60 then
            UI.signal.Text = "📶 WI-FI: " .. math.floor(signal) .. "% (BOM)" .. wallTag
            UI.signal.TextColor3 = Color3.fromRGB(0, 255, 100)
            UI.static.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
            UI.static.BackgroundTransparency = 0.97
            intensity = 0.04
        elseif signal > 25 then
            local weak = 1 - (signal - 25) / 35
            UI.signal.Text = "⚠️ WI-FI: " .. math.floor(signal) .. "% (FRACO)" .. wallTag
            UI.signal.TextColor3 = Color3.fromRGB(255, 180, 0)
            UI.static.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
            UI.static.BackgroundTransparency = 0.88 - weak * 0.15
            intensity = 0.15 + weak * 0.45
            bars = math.floor(1 + weak * 2)
            sMin, sMax, alphaMin = 3, 10, 0.35
        else
            UI.signal.Text = "🚫 WI-FI: " .. math.floor(signal) .. "% (SEM SINAL)" .. wallTag
            UI.signal.TextColor3 = Color3.fromRGB(255, 50, 50)
            UI.static.BackgroundColor3 = Color3.fromRGB(30, 30, 30)
            UI.static.BackgroundTransparency = 0.1
            intensity = 1
            bars = 6
            sMin, sMax, alphaMin = 6, 22, 0.05
            noSignal = true
        end
    end

    UI.screenNoise(intensity, vp.X, vp.Y, sMin, sMax, alphaMin)

    for i, bar in ipairs(UI.bars) do
        if i <= bars then
            bar.Position = UDim2.new(0, 0, math.random(), 0)
            bar.Size = UDim2.new(1, 0, 0, math.random(2, 14))
            bar.BackgroundTransparency = 0.5 + math.random() * 0.4
            bar.Visible = true
        else
            bar.Visible = false
        end
    end

    UI.noSignal.Visible = noSignal
    if noSignal then
        UI.noSignal.Position = UDim2.new(0.5, math.random(-4, 4), 0.5, math.random(-3, 3))
        UI.noSignal.TextTransparency = (math.floor(tick() * 3) % 2 == 0) and 0 or 0.35
    end
end)

print("TuboCAM v13 ativado!")

About this script

Sistema completo de CFTV analógico e NVR para Roblox! - Logótipo personalizado TuboCAM com painel físico realista. - Conexão por cabos analógicos (RCA amarelo e branco) entre o NVR e a TV de tubo. - Visualização ao vivo em grade de múltiplas câmeras diretamente na tela da TV antiga. - Opções de câmeras com e sem fios, ajuste de altura e detecção de movimento. - Script totalmente universal.

More universal scripts