PatchedKeylessUniversal
TuboCAM v13 - Retro CCTV & NVR System [Claude IA]
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.



