Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- ╔══════════════════════════════════════════════════════════════════════╗
- ║ Save Locate (NickZ) - V5.0 MEGA UPDATE ║
- ║ ║
- ║ • Arquitetura modular com sistemas desacoplados ║
- ║ • Teleporte robusto anti-bug / anti-animação ║
- ║ • Paginação: 5 páginas × 10 slots = 50 saves ║
- ║ • Persistência dupla (JSON + BAK) com fallback inteligente ║
- ║ • UI totalmente refatorada: mais limpa, leve e organizada ║
- ║ • Gestão de memória otimizada (sem leaks de conexão) ║
- ║ • Sistema de notificações integrado ║
- ║ • Feedback visual aprimorado em todos os elementos ║
- ╚══════════════════════════════════════════════════════════════════════╝
- ]]
- -- ============================================================
- -- SEÇÃO 1 ► AGUARDAR CARREGAMENTO DO JOGO
- -- ============================================================
- if not game:IsLoaded() then
- game.Loaded:Wait()
- end
- -- ============================================================
- -- SEÇÃO 2 ► SERVIÇOS (cache local para evitar chamadas repetidas)
- -- ============================================================
- local Players = game:GetService("Players")
- local HttpService = game:GetService("HttpService")
- local TweenService = game:GetService("TweenService")
- local UserInputService = game:GetService("UserInputService")
- local RunService = game:GetService("RunService")
- local CoreGui = game:GetService("CoreGui")
- -- ============================================================
- -- SEÇÃO 3 ► REFERÊNCIAS DO JOGADOR
- -- ============================================================
- local plr = Players.LocalPlayer
- local chr = plr.Character or plr.CharacterAdded:Wait()
- local root = chr:WaitForChild("HumanoidRootPart")
- -- ============================================================
- -- SEÇÃO 4 ► CONSTANTES DE CONFIGURAÇÃO
- -- ============================================================
- local CFG = {
- MAX_SAVES = 50,
- SLOTS_PER_PAGE = 10,
- MAX_PAGES = 5,
- SAVE_DIR = "SaveLocate_NickZ",
- VERSION = "5.0",
- TWEEN_FAST = TweenInfo.new(0.18, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
- TWEEN_MED = TweenInfo.new(0.28, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
- TWEEN_SLOW = TweenInfo.new(0.45, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
- }
- -- Paleta de cores centralizada (single source of truth)
- local CLR = {
- BG_DARK = Color3.fromRGB(14, 12, 22),
- BG_MID = Color3.fromRGB(22, 19, 35),
- BG_LIGHT = Color3.fromRGB(32, 28, 50),
- BG_CARD = Color3.fromRGB(28, 25, 42),
- BG_CARD_HOV = Color3.fromRGB(38, 34, 58),
- ACCENT = Color3.fromRGB(110, 80, 255),
- ACCENT_HOV = Color3.fromRGB(135, 105, 255),
- ACCENT_DIM = Color3.fromRGB(70, 55, 160),
- SUCCESS = Color3.fromRGB(60, 200, 110),
- SUCCESS_HOV = Color3.fromRGB(80, 225, 130),
- DANGER = Color3.fromRGB(220, 70, 70),
- DANGER_HOV = Color3.fromRGB(240, 95, 95),
- WARNING = Color3.fromRGB(220, 160, 40),
- INFO = Color3.fromRGB(70, 150, 230),
- INFO_HOV = Color3.fromRGB(90, 170, 250),
- TEXT_PRI = Color3.fromRGB(240, 238, 255),
- TEXT_SEC = Color3.fromRGB(170, 165, 200),
- TEXT_DIM = Color3.fromRGB(110, 105, 140),
- TEXT_MUTED = Color3.fromRGB(75, 70, 100),
- GOLD = Color3.fromRGB(255, 210, 60),
- STROKE = Color3.fromRGB(80, 65, 160),
- STROKE_BRIGHT= Color3.fromRGB(120, 95, 255),
- OVERLAY = Color3.fromRGB(5, 4, 10),
- WHITE = Color3.new(1, 1, 1),
- }
- -- ============================================================
- -- SEÇÃO 5 ► SISTEMA DE FILESYSTEM (detecção de capacidades)
- -- ============================================================
- local FS = {}
- do
- FS.canWrite = type(writefile) == "function"
- FS.canRead = type(readfile) == "function"
- FS.canFolder = type(isfolder) == "function" and type(makefolder) == "function"
- FS.canIsFile = type(isfile) == "function"
- FS.canDelete = type(delfile) == "function"
- FS.active = FS.canWrite and FS.canRead
- local GAME_ID = tostring(game.PlaceId)
- local USER_ID = tostring(plr.UserId)
- local BASE_DIR = CFG.SAVE_DIR .. "/" .. GAME_ID
- -- Cria diretórios se suportado
- if FS.canFolder then
- if not isfolder(CFG.SAVE_DIR) then
- pcall(makefolder, CFG.SAVE_DIR)
- end
- if not isfolder(BASE_DIR) then
- pcall(makefolder, BASE_DIR)
- end
- end
- -- Resolve caminhos principais e de backup
- local function buildPath(dir, name)
- return dir .. "/" .. name
- end
- if FS.canFolder then
- FS.SAVE_FILE = buildPath(BASE_DIR, "Saves_" .. USER_ID .. ".json")
- FS.SAVE_BAK = buildPath(BASE_DIR, "Saves_" .. USER_ID .. ".bak")
- FS.MAIN_FILE = buildPath(BASE_DIR, "MainPos_" .. USER_ID .. ".json")
- FS.MAIN_BAK = buildPath(BASE_DIR, "MainPos_" .. USER_ID .. ".bak")
- else
- -- Fallback flat (sem suporte a pastas)
- local prefix = string.format("%s_%s_%s", CFG.SAVE_DIR, GAME_ID, USER_ID)
- FS.SAVE_FILE = prefix .. "_Saves.json"
- FS.SAVE_BAK = prefix .. "_Saves.bak"
- FS.MAIN_FILE = prefix .. "_Main.json"
- FS.MAIN_BAK = prefix .. "_Main.bak"
- end
- -- Caminhos legados para migração automática
- FS.LEGACY_PATHS_SAVES = {
- string.format("SkyTP/Saves_%s_%s.json", game.PlaceId, plr.UserId),
- string.format("SkyTP/Saves_%s_%s.bak", game.PlaceId, plr.UserId),
- string.format("Saves_%s_%s.json", game.PlaceId, plr.UserId),
- string.format("Saves_%s_%s.bak", game.PlaceId, plr.UserId),
- }
- FS.LEGACY_PATHS_MAIN = {
- string.format("SkyTP/MainPos_%s_%s.json", game.PlaceId, plr.UserId),
- string.format("SkyTP/MainPos_%s_%s.bak", game.PlaceId, plr.UserId),
- string.format("MainPos_%s_%s.json", game.PlaceId, plr.UserId),
- string.format("MainPos_%s_%s.bak", game.PlaceId, plr.UserId),
- }
- -- Lê arquivo com proteção
- function FS.read(path)
- if not FS.active or type(path) ~= "string" then return nil end
- local ok, content = pcall(readfile, path)
- if ok and type(content) == "string" and #content > 2 then
- return content
- end
- return nil
- end
- -- Lê o primeiro arquivo válido de uma lista de candidatos
- function FS.readFirst(paths)
- for _, p in ipairs(paths) do
- local content = FS.read(p)
- if content then return content, p end
- end
- return nil, nil
- end
- -- Escreve em arquivo + backup atomicamente
- function FS.writeSafe(primary, backup, content)
- if not FS.active then return false end
- local ok1 = pcall(writefile, primary, content)
- local ok2 = pcall(writefile, backup, content)
- return ok1 or ok2
- end
- -- Remove arquivo com segurança
- function FS.delete(path)
- if FS.active and FS.canDelete and type(path) == "string" then
- pcall(delfile, path)
- end
- end
- end
- -- ============================================================
- -- SEÇÃO 6 ► CODIFICADOR DE CFRAME (compacto e seguro)
- -- ============================================================
- local Codec = {}
- do
- function Codec.encode(cf)
- if typeof(cf) ~= "CFrame" then return nil end
- local x,y,z,r00,r01,r02,r10,r11,r12,r20,r21,r22 = cf:GetComponents()
- return {
- x=x, y=y, z=z,
- m={r00, r01, r02, r10, r11, r12, r20, r21, r22}
- }
- end
- function Codec.decode(t)
- if type(t) ~= "table" then return nil end
- -- Suporte ao formato legado {x,y,z, R00..R22}
- if t.R00 ~= nil then
- return CFrame.new(
- t.x,t.y,t.z,
- t.R00,t.R01,t.R02,
- t.R10,t.R11,t.R12,
- t.R20,t.R21,t.R22
- )
- end
- -- Formato novo compacto
- if t.x and t.m and #t.m == 9 then
- local m = t.m
- return CFrame.new(t.x,t.y,t.z, m[1],m[2],m[3],m[4],m[5],m[6],m[7],m[8],m[9])
- end
- return nil
- end
- function Codec.jsonEncode(data)
- local ok, result = pcall(function() return HttpService:JSONEncode(data) end)
- return ok and result or nil
- end
- function Codec.jsonDecode(str)
- if type(str) ~= "string" or #str < 2 then return nil end
- local ok, result = pcall(function() return HttpService:JSONDecode(str) end)
- return ok and result or nil
- end
- end
- -- ============================================================
- -- SEÇÃO 7 ► ESTADO GLOBAL DO SCRIPT
- -- ============================================================
- local State = {
- saves = {}, -- [slot: number] = {position, name, timestamp}
- mainPos = nil, -- {position, name, timestamp}
- currentPage = 1,
- saveSlot = 1, -- slot de destino atual
- isDirty = false, -- controle de escrita pendente
- connections = {}, -- pool de conexões para cleanup
- optionFrames = {}, -- frames de opções flutuantes ativos
- }
- -- ============================================================
- -- SEÇÃO 8 ► SISTEMA DE PERSISTÊNCIA
- -- ============================================================
- local Persist = {}
- do
- -- Serializa saves para JSON
- function Persist.buildSavePayload()
- local positions = {}
- for slot, data in pairs(State.saves) do
- if type(slot) == "number" and slot >= 1 and slot <= CFG.MAX_SAVES
- and typeof(data.position) == "CFrame" then
- positions[tostring(slot)] = {
- name = data.name or ("Slot " .. slot),
- timestamp = data.timestamp or os.date("%Y-%m-%d %H:%M:%S"),
- position = Codec.encode(data.position),
- }
- end
- end
- return {
- version = CFG.VERSION,
- gameId = game.PlaceId,
- gameName = game.Name,
- userId = plr.UserId,
- userName = plr.Name,
- savedAt = os.date("%Y-%m-%d %H:%M:%S"),
- positions = positions,
- }
- end
- -- Salva tudo no disco
- function Persist.save()
- if not FS.active then return end
- -- Saves normais
- local payload = Codec.jsonEncode(Persist.buildSavePayload())
- if payload then
- FS.writeSafe(FS.SAVE_FILE, FS.SAVE_BAK, payload)
- end
- -- Posição principal
- if State.mainPos then
- local mainPayload = Codec.jsonEncode({
- version = CFG.VERSION,
- gameId = game.PlaceId,
- gameName = game.Name,
- name = State.mainPos.name,
- timestamp = State.mainPos.timestamp,
- position = Codec.encode(State.mainPos.position),
- })
- if mainPayload then
- FS.writeSafe(FS.MAIN_FILE, FS.MAIN_BAK, mainPayload)
- end
- end
- State.isDirty = false
- end
- -- Carrega saves do disco com suporte a formatos legados
- function Persist.load()
- if not FS.active then return end
- -- Tenta carregar saves
- local candidates = {FS.SAVE_FILE, FS.SAVE_BAK}
- for _, p in ipairs(FS.LEGACY_PATHS_SAVES) do
- table.insert(candidates, p)
- end
- local content, usedPath = FS.readFirst(candidates)
- if content then
- local data = Codec.jsonDecode(content)
- if data and type(data) == "table" then
- -- Valida gameId para não misturar saves de jogos diferentes
- if not data.gameId or data.gameId == game.PlaceId then
- local positions = data.positions or data
- for k, v in pairs(positions) do
- local slot = tonumber(k)
- if slot and slot >= 1 and slot <= CFG.MAX_SAVES and type(v) == "table" then
- local cf = Codec.decode(v.position)
- if cf then
- State.saves[slot] = {
- position = cf,
- name = v.name or ("Slot " .. slot),
- timestamp = v.timestamp or os.date("%Y-%m-%d %H:%M:%S"),
- }
- end
- end
- end
- -- Migra legado para novo caminho
- if usedPath ~= FS.SAVE_FILE then
- Persist.save()
- end
- end
- end
- end
- -- Tenta carregar posição principal
- local mainCandidates = {FS.MAIN_FILE, FS.MAIN_BAK}
- for _, p in ipairs(FS.LEGACY_PATHS_MAIN) do
- table.insert(mainCandidates, p)
- end
- local mainContent, mainUsedPath = FS.readFirst(mainCandidates)
- if mainContent then
- local data = Codec.jsonDecode(mainContent)
- if data and type(data) == "table" then
- if not data.gameId or data.gameId == game.PlaceId then
- local cf = Codec.decode(data.position)
- if cf then
- State.mainPos = {
- position = cf,
- name = data.name or "Posição Principal",
- timestamp = data.timestamp or os.date("%Y-%m-%d %H:%M:%S"),
- }
- if mainUsedPath ~= FS.MAIN_FILE then
- Persist.save()
- end
- end
- end
- end
- end
- end
- -- Salva de forma adiada (throttle para evitar spam de escrita)
- local saveDebounce = false
- function Persist.scheduleSave()
- State.isDirty = true
- if saveDebounce then return end
- saveDebounce = true
- task.delay(0.5, function()
- if State.isDirty then Persist.save() end
- saveDebounce = false
- end)
- end
- -- Remove posição principal do disco
- function Persist.deleteMain()
- FS.delete(FS.MAIN_FILE)
- FS.delete(FS.MAIN_BAK)
- end
- end
- -- ============================================================
- -- SEÇÃO 9 ► SISTEMA DE TELEPORTE ANTI-BUG
- -- ============================================================
- local Teleport = {}
- do
- -- Atualiza referências do personagem (chamado ao respawn)
- function Teleport.refreshCharacter()
- chr = plr.Character
- root = chr and chr:FindFirstChild("HumanoidRootPart")
- end
- -- Teleporte robusto: âncora → zera velocidade → aplica CFrame → desancora
- function Teleport.to(targetCFrame)
- if not targetCFrame then return false end
- -- Garante referências atualizadas
- if not chr or not chr.Parent then
- Teleport.refreshCharacter()
- end
- if not root then return false end
- local hum = chr:FindFirstChildWhichIsA("Humanoid")
- -- Sai de assentos se necessário
- if hum and hum.SeatPart then
- hum.Sit = false
- task.wait(0.05)
- end
- -- Desativa efeitos de física temporariamente
- local wasAnchored = root.Anchored
- root.Anchored = true
- -- Zera momentum para evitar "lançamentos"
- if root:IsA("BasePart") then
- root.AssemblyLinearVelocity = Vector3.zero
- root.AssemblyAngularVelocity = Vector3.zero
- end
- -- Aplica posição
- root.CFrame = targetCFrame
- -- Aguarda confirmação de física e aplica novamente
- RunService.Heartbeat:Wait()
- root.CFrame = targetCFrame
- -- Restaura estado original
- root.Anchored = wasAnchored
- return true
- end
- end
- -- ============================================================
- -- SEÇÃO 10 ► UTILITÁRIOS DE UI
- -- ============================================================
- local UI = {}
- do
- -- Cria UICorner de forma fluida
- function UI.corner(parent, radius)
- local c = Instance.new("UICorner")
- c.CornerRadius = UDim.new(0, radius or 8)
- c.Parent = parent
- return c
- end
- -- Cria UIStroke de forma fluida
- function UI.stroke(parent, color, thickness)
- local s = Instance.new("UIStroke")
- s.Color = color or CLR.STROKE
- s.Thickness = thickness or 1.5
- s.Parent = parent
- return s
- end
- -- Gradiente simples de cima para baixo
- function UI.gradient(parent, colorA, colorB, rotation)
- local g = Instance.new("UIGradient")
- g.Color = ColorSequence.new({
- ColorSequenceKeypoint.new(0, colorA),
- ColorSequenceKeypoint.new(1, colorB),
- })
- g.Rotation = rotation or 90
- g.Parent = parent
- return g
- end
- -- Hover animado para botão (cor de fundo)
- function UI.hoverBG(button, hoverColor, baseColor)
- button.MouseEnter:Connect(function()
- TweenService:Create(button, CFG.TWEEN_FAST, {BackgroundColor3 = hoverColor}):Play()
- end)
- button.MouseLeave:Connect(function()
- TweenService:Create(button, CFG.TWEEN_FAST, {BackgroundColor3 = baseColor}):Play()
- end)
- end
- -- Hover animado para botão (escala via Size)
- function UI.hoverScale(button, baseSize, scaleUp)
- local upSize = UDim2.new(baseSize.X.Scale * scaleUp, baseSize.X.Offset,
- baseSize.Y.Scale * scaleUp, baseSize.Y.Offset)
- button.MouseEnter:Connect(function()
- TweenService:Create(button, CFG.TWEEN_FAST, {Size = upSize}):Play()
- end)
- button.MouseLeave:Connect(function()
- TweenService:Create(button, CFG.TWEEN_FAST, {Size = baseSize}):Play()
- end)
- end
- -- Fecha todos os menus de opções flutuantes abertos
- function UI.closeAllOptionMenus(except)
- for slot, frame in pairs(State.optionFrames) do
- if frame ~= except and frame and frame.Parent then
- frame.Visible = false
- end
- end
- end
- -- Fábrica de Label padrão
- function UI.label(parent, props)
- local l = Instance.new("TextLabel")
- l.BackgroundTransparency = 1
- l.Font = props.font or Enum.Font.Gotham
- l.TextSize = props.size or 14
- l.TextColor3 = props.color or CLR.TEXT_PRI
- l.Text = props.text or ""
- l.TextXAlignment = props.align or Enum.TextXAlignment.Left
- l.TextWrapped = props.wrap or false
- l.TextTruncate = props.truncate or Enum.TextTruncate.None
- l.Size = props.sz or UDim2.new(1, 0, 1, 0)
- l.Position = props.pos or UDim2.new(0, 0, 0, 0)
- l.Parent = parent
- return l
- end
- -- Fábrica de Button padrão
- function UI.button(parent, props)
- local b = Instance.new("TextButton")
- b.Font = props.font or Enum.Font.GothamSemibold
- b.TextSize = props.size or 14
- b.TextColor3 = props.textColor or CLR.WHITE
- b.Text = props.text or ""
- b.BackgroundColor3 = props.bgColor or CLR.ACCENT
- b.Size = props.sz or UDim2.new(0, 80, 0, 30)
- b.Position = props.pos or UDim2.new(0, 0, 0, 0)
- b.AutoButtonColor = false
- b.BorderSizePixel = 0
- b.Parent = parent
- UI.corner(b, props.radius or 7)
- if props.hoverColor then
- UI.hoverBG(b, props.hoverColor, props.bgColor)
- end
- return b
- end
- end
- -- ============================================================
- -- SEÇÃO 11 ► CONSTRUÇÃO DA GUI
- -- ============================================================
- -- Remove instâncias antigas para evitar duplicatas ao re-executar
- do
- local old = CoreGui:FindFirstChild("SaveLocate_NickZ_V5")
- if old then old:Destroy() end
- end
- local ScreenGui = Instance.new("ScreenGui")
- ScreenGui.Name = "SaveLocate_NickZ_V5"
- ScreenGui.DisplayOrder = 999
- ScreenGui.ResetOnSpawn = false
- ScreenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
- ScreenGui.IgnoreGuiInset = true
- ScreenGui.Parent = CoreGui
- -- ────────────────────────────────────────────────
- -- 11.1 ► JANELA PRINCIPAL
- -- ────────────────────────────────────────────────
- local MainFrame = Instance.new("Frame")
- MainFrame.Name = "MainFrame"
- MainFrame.Size = UDim2.new(0, 590, 0, 470)
- MainFrame.Position = UDim2.new(0.5, -295, 0.5, -235)
- MainFrame.BackgroundColor3 = CLR.BG_DARK
- MainFrame.BorderSizePixel = 0
- MainFrame.Active = true
- MainFrame.Draggable = true
- MainFrame.ClipsDescendants = true
- MainFrame.Parent = ScreenGui
- UI.corner(MainFrame, 10)
- UI.stroke(MainFrame, CLR.STROKE_BRIGHT, 1.5)
- UI.gradient(MainFrame, CLR.BG_MID, CLR.BG_DARK, 135)
- -- ────────────────────────────────────────────────
- -- 11.2 ► BARRA DE TÍTULO
- -- ────────────────────────────────────────────────
- local TitleBar = Instance.new("Frame")
- TitleBar.Name = "TitleBar"
- TitleBar.Size = UDim2.new(1, 0, 0, 44)
- TitleBar.BackgroundColor3 = CLR.BG_MID
- TitleBar.BorderSizePixel = 0
- TitleBar.Parent = MainFrame
- UI.corner(TitleBar, 10)
- -- Borda inferior da titlebar para separar visualmente
- local TitleDivider = Instance.new("Frame")
- TitleDivider.Name = "Divider"
- TitleDivider.Size = UDim2.new(1, 0, 0, 1)
- TitleDivider.Position = UDim2.new(0, 0, 1, -1)
- TitleDivider.BackgroundColor3 = CLR.STROKE
- TitleDivider.BorderSizePixel = 0
- TitleDivider.Parent = TitleBar
- -- Ícone decorativo
- local TitleIcon = UI.label(TitleBar, {
- text = "✦",
- sz = UDim2.new(0, 30, 1, 0),
- pos = UDim2.new(0, 12, 0, 0),
- size = 18,
- color = CLR.ACCENT,
- font = Enum.Font.GothamBold,
- align = Enum.TextXAlignment.Center,
- })
- local TitleLabel = UI.label(TitleBar, {
- text = "Save Locate · NickZ",
- sz = UDim2.new(1, -120, 1, 0),
- pos = UDim2.new(0, 46, 0, 0),
- size = 16,
- color = CLR.TEXT_PRI,
- font = Enum.Font.GothamBold,
- align = Enum.TextXAlignment.Left,
- })
- local VersionBadge = Instance.new("Frame")
- VersionBadge.Size = UDim2.new(0, 42, 0, 20)
- VersionBadge.Position = UDim2.new(1, -90, 0.5, -10)
- VersionBadge.BackgroundColor3 = CLR.ACCENT_DIM
- VersionBadge.Parent = TitleBar
- UI.corner(VersionBadge, 5)
- local VersionLabel = UI.label(VersionBadge, {
- text = "V 5.0",
- size = 10,
- color = CLR.TEXT_PRI,
- font = Enum.Font.GothamBold,
- align = Enum.TextXAlignment.Center,
- sz = UDim2.new(1, 0, 1, 0),
- })
- local CloseBtn = UI.button(TitleBar, {
- text = "✕",
- sz = UDim2.new(0, 28, 0, 28),
- pos = UDim2.new(1, -36, 0.5, -14),
- bgColor = CLR.BG_LIGHT,
- hoverColor= CLR.DANGER,
- textColor = CLR.TEXT_SEC,
- font = Enum.Font.GothamBold,
- size = 14,
- radius = 7,
- })
- -- ────────────────────────────────────────────────
- -- 11.3 ► ÁREA DE CONTROLES (Salvar / Nome / Slot)
- -- ────────────────────────────────────────────────
- local CtrlFrame = Instance.new("Frame")
- CtrlFrame.Name = "Controls"
- CtrlFrame.Size = UDim2.new(1, -20, 0, 96)
- CtrlFrame.Position = UDim2.new(0, 10, 0, 50)
- CtrlFrame.BackgroundColor3 = CLR.BG_MID
- CtrlFrame.BorderSizePixel = 0
- CtrlFrame.Parent = MainFrame
- UI.corner(CtrlFrame, 8)
- UI.stroke(CtrlFrame, CLR.STROKE, 1)
- -- Botão Salvar
- local SaveBtn = UI.button(CtrlFrame, {
- text = "+ Salvar Posição",
- sz = UDim2.new(0, 175, 0, 36),
- pos = UDim2.new(0, 10, 0, 10),
- bgColor = CLR.SUCCESS,
- hoverColor = CLR.SUCCESS_HOV,
- textColor = CLR.WHITE,
- font = Enum.Font.GothamBold,
- size = 14,
- radius = 7,
- })
- -- Campo de nome
- local NameBox = Instance.new("TextBox")
- NameBox.Name = "NameBox"
- NameBox.PlaceholderText = " Nome da posição..."
- NameBox.Size = UDim2.new(1, -200, 0, 36)
- NameBox.Position = UDim2.new(0, 10, 0, 50)
- NameBox.BackgroundColor3 = CLR.BG_DARK
- NameBox.TextColor3 = CLR.TEXT_PRI
- NameBox.PlaceholderColor3 = CLR.TEXT_MUTED
- NameBox.Font = Enum.Font.Gotham
- NameBox.TextSize = 13
- NameBox.ClearTextOnFocus = false
- NameBox.TextXAlignment = Enum.TextXAlignment.Left
- NameBox.Parent = CtrlFrame
- UI.corner(NameBox, 7)
- UI.stroke(NameBox, CLR.STROKE, 1)
- -- Painel de seleção de slot (lado direito dos controles)
- local SlotPanel = Instance.new("Frame")
- SlotPanel.Name = "SlotPanel"
- SlotPanel.Size = UDim2.new(0, 175, 0, 76)
- SlotPanel.Position = UDim2.new(1, -185, 0, 10)
- SlotPanel.BackgroundColor3 = CLR.BG_DARK
- SlotPanel.BorderSizePixel = 0
- SlotPanel.Parent = CtrlFrame
- UI.corner(SlotPanel, 7)
- UI.stroke(SlotPanel, CLR.STROKE, 1)
- local SlotTitleLbl = UI.label(SlotPanel, {
- text = "SLOT DE DESTINO",
- sz = UDim2.new(1, 0, 0, 20),
- pos = UDim2.new(0, 0, 0, 4),
- size = 9,
- color = CLR.TEXT_DIM,
- font = Enum.Font.GothamBold,
- align = Enum.TextXAlignment.Center,
- })
- local SlotValueLbl = UI.label(SlotPanel, {
- text = "01",
- sz = UDim2.new(0, 50, 0, 30),
- pos = UDim2.new(0.5, -25, 0, 22),
- size = 22,
- color = CLR.ACCENT,
- font = Enum.Font.GothamBold,
- align = Enum.TextXAlignment.Center,
- })
- local SlotPrevBtn = UI.button(SlotPanel, {
- text = "‹",
- sz = UDim2.new(0, 32, 0, 28),
- pos = UDim2.new(0, 6, 0, 24),
- bgColor = CLR.ACCENT_DIM,
- hoverColor = CLR.ACCENT,
- size = 18,
- radius = 6,
- })
- local SlotNextBtn = UI.button(SlotPanel, {
- text = "›",
- sz = UDim2.new(0, 32, 0, 28),
- pos = UDim2.new(1, -38, 0, 24),
- bgColor = CLR.ACCENT_DIM,
- hoverColor = CLR.ACCENT,
- size = 18,
- radius = 6,
- })
- local SlotStatusLbl = UI.label(SlotPanel, {
- text = "livre",
- sz = UDim2.new(1, 0, 0, 14),
- pos = UDim2.new(0, 0, 1, -16),
- size = 9,
- color = CLR.SUCCESS,
- font = Enum.Font.GothamSemibold,
- align = Enum.TextXAlignment.Center,
- })
- -- ────────────────────────────────────────────────
- -- 11.4 ► BARRA DE PAGINAÇÃO
- -- ────────────────────────────────────────────────
- local PageBar = Instance.new("Frame")
- PageBar.Name = "PageBar"
- PageBar.Size = UDim2.new(1, -20, 0, 34)
- PageBar.Position = UDim2.new(0, 10, 0, 152)
- PageBar.BackgroundColor3 = CLR.BG_MID
- PageBar.BorderSizePixel = 0
- PageBar.Parent = MainFrame
- UI.corner(PageBar, 7)
- UI.stroke(PageBar, CLR.STROKE, 1)
- local PagePrevBtn = UI.button(PageBar, {
- text = "◄",
- sz = UDim2.new(0, 70, 0, 26),
- pos = UDim2.new(0, 4, 0.5, -13),
- bgColor = CLR.BG_LIGHT,
- hoverColor = CLR.ACCENT_DIM,
- size = 12,
- radius = 6,
- })
- local PageLabel = UI.label(PageBar, {
- text = "PÁGINA 1 / 5",
- sz = UDim2.new(1, -160, 1, 0),
- pos = UDim2.new(0, 80, 0, 0),
- size = 12,
- color = CLR.TEXT_SEC,
- font = Enum.Font.GothamBold,
- align = Enum.TextXAlignment.Center,
- })
- -- Indicadores de paginação (bolinhas)
- local PageDotsFrame = Instance.new("Frame")
- PageDotsFrame.Size = UDim2.new(0, 70, 0, 10)
- PageDotsFrame.Position = UDim2.new(0.5, -35, 1, -13)
- PageDotsFrame.BackgroundTransparency = 1
- PageDotsFrame.Parent = PageBar
- local PageDotLayout = Instance.new("UIListLayout")
- PageDotLayout.FillDirection = Enum.FillDirection.Horizontal
- PageDotLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
- PageDotLayout.Padding = UDim.new(0, 6)
- PageDotLayout.Parent = PageDotsFrame
- local PageDots = {}
- for i = 1, CFG.MAX_PAGES do
- local dot = Instance.new("Frame")
- dot.Size = UDim2.new(0, 7, 0, 7)
- dot.BackgroundColor3 = (i == 1) and CLR.ACCENT or CLR.TEXT_MUTED
- dot.Parent = PageDotsFrame
- UI.corner(dot, 4)
- PageDots[i] = dot
- end
- local PageNextBtn = UI.button(PageBar, {
- text = "►",
- sz = UDim2.new(0, 70, 0, 26),
- pos = UDim2.new(1, -74, 0.5, -13),
- bgColor = CLR.BG_LIGHT,
- hoverColor = CLR.ACCENT_DIM,
- size = 12,
- radius = 6,
- })
- -- ────────────────────────────────────────────────
- -- 11.5 ► GRID DE SLOTS
- -- ────────────────────────────────────────────────
- local SlotsFrame = Instance.new("Frame")
- SlotsFrame.Name = "Slots"
- SlotsFrame.Size = UDim2.new(1, -20, 1, -198)
- SlotsFrame.Position = UDim2.new(0, 10, 0, 194)
- SlotsFrame.BackgroundTransparency = 1
- SlotsFrame.Parent = MainFrame
- local GridLayout = Instance.new("UIGridLayout")
- GridLayout.CellPadding = UDim2.new(0, 7, 0, 7)
- GridLayout.CellSize = UDim2.new(0.5, -4, 0, 48)
- GridLayout.FillDirection = Enum.FillDirection.Horizontal
- GridLayout.SortOrder = Enum.SortOrder.LayoutOrder
- GridLayout.Parent = SlotsFrame
- -- ────────────────────────────────────────────────
- -- 11.6 ► BOTÃO FLUTUANTE (quando janela fechada)
- -- ────────────────────────────────────────────────
- local FloatContainer = Instance.new("Frame")
- FloatContainer.Name = "FloatBtn"
- FloatContainer.Size = UDim2.new(0, 46, 0, 46)
- FloatContainer.Position = UDim2.new(0, 16, 0.5, -23)
- FloatContainer.BackgroundTransparency = 1
- FloatContainer.Active = true
- FloatContainer.Draggable = true
- FloatContainer.Visible = false
- FloatContainer.Parent = ScreenGui
- local FloatBtn = UI.button(FloatContainer, {
- text = "✦",
- sz = UDim2.new(1, 0, 1, 0),
- bgColor = CLR.BG_MID,
- hoverColor = CLR.BG_LIGHT,
- textColor = CLR.ACCENT,
- font = Enum.Font.GothamBold,
- size = 22,
- radius = 10,
- })
- UI.stroke(FloatBtn, CLR.STROKE_BRIGHT, 1.5)
- -- ────────────────────────────────────────────────
- -- 11.7 ► BOTÃO FLUTUANTE DE POSIÇÃO PRINCIPAL
- -- ────────────────────────────────────────────────
- local MainPosContainer = Instance.new("Frame")
- MainPosContainer.Name = "MainPosBtn"
- MainPosContainer.Size = UDim2.new(0, 50, 0, 50)
- MainPosContainer.Position = UDim2.new(1, -64, 0, 14)
- MainPosContainer.BackgroundTransparency = 1
- MainPosContainer.Active = true
- MainPosContainer.Draggable = true
- MainPosContainer.Visible = false
- MainPosContainer.Parent = ScreenGui
- local MainPosBtn = UI.button(MainPosContainer, {
- text = "🚀",
- sz = UDim2.new(1, 0, 1, 0),
- bgColor = CLR.BG_MID,
- hoverColor = CLR.BG_LIGHT,
- size = 22,
- radius = 25,
- })
- local MainPosStroke = UI.stroke(MainPosBtn, CLR.ACCENT, 2)
- -- Tooltip da posição principal
- local MainPosTooltip = Instance.new("Frame")
- MainPosTooltip.Name = "Tooltip"
- MainPosTooltip.Size = UDim2.new(0, 160, 0, 32)
- MainPosTooltip.Position = UDim2.new(-3.4, 0, 0.5, -16)
- MainPosTooltip.BackgroundColor3 = CLR.BG_MID
- MainPosTooltip.BackgroundTransparency = 0.05
- MainPosTooltip.Visible = false
- MainPosTooltip.ZIndex = 5
- MainPosTooltip.Parent = MainPosBtn
- UI.corner(MainPosTooltip, 7)
- UI.stroke(MainPosTooltip, CLR.ACCENT, 1)
- local TooltipLabel = UI.label(MainPosTooltip, {
- text = "",
- sz = UDim2.new(1, -8, 1, 0),
- pos = UDim2.new(0, 4, 0, 0),
- size = 11,
- color = CLR.TEXT_PRI,
- font = Enum.Font.GothamSemibold,
- align = Enum.TextXAlignment.Center,
- wrap = true,
- })
- TooltipLabel.ZIndex = 6
- -- Botão X (remover posição principal)
- local MainPosRemoveBtn = UI.button(MainPosBtn, {
- text = "×",
- sz = UDim2.new(0, 18, 0, 18),
- pos = UDim2.new(1, -4, 0, -4),
- bgColor = CLR.DANGER,
- hoverColor = CLR.DANGER_HOV,
- size = 12,
- radius = 9,
- })
- MainPosRemoveBtn.Visible = false
- MainPosRemoveBtn.ZIndex = 6
- -- ────────────────────────────────────────────────
- -- 11.8 ► SISTEMA DE NOTIFICAÇÃO (Toast)
- -- ────────────────────────────────────────────────
- local ToastFrame = Instance.new("Frame")
- ToastFrame.Name = "Toast"
- ToastFrame.Size = UDim2.new(0, 260, 0, 38)
- ToastFrame.Position = UDim2.new(0.5, -130, 0, -50)
- ToastFrame.BackgroundColor3 = CLR.BG_MID
- ToastFrame.BackgroundTransparency = 0.05
- ToastFrame.Visible = false
- ToastFrame.ZIndex = 99
- ToastFrame.Parent = ScreenGui
- UI.corner(ToastFrame, 8)
- local ToastStroke = UI.stroke(ToastFrame, CLR.SUCCESS, 1.5)
- local ToastLabel = UI.label(ToastFrame, {
- text = "",
- sz = UDim2.new(1, -16, 1, 0),
- pos = UDim2.new(0, 8, 0, 0),
- size = 13,
- color = CLR.TEXT_PRI,
- font = Enum.Font.GothamSemibold,
- align = Enum.TextXAlignment.Center,
- wrap = false,
- })
- ToastLabel.ZIndex = 100
- local toastActive = false
- local function showToast(message, toastType)
- local colorMap = {
- success = CLR.SUCCESS,
- error = CLR.DANGER,
- info = CLR.INFO,
- warning = CLR.WARNING,
- }
- local color = colorMap[toastType] or CLR.SUCCESS
- ToastStroke.Color = color
- ToastLabel.Text = message
- ToastLabel.TextColor3 = color
- ToastFrame.Visible = true
- toastActive = true
- TweenService:Create(ToastFrame, CFG.TWEEN_MED, {
- Position = UDim2.new(0.5, -130, 0, 12)
- }):Play()
- task.delay(2.2, function()
- if toastActive then
- TweenService:Create(ToastFrame, CFG.TWEEN_MED, {
- Position = UDim2.new(0.5, -130, 0, -50)
- }):Play()
- task.wait(0.35)
- ToastFrame.Visible = false
- toastActive = false
- end
- end)
- end
- -- ────────────────────────────────────────────────
- -- 11.9 ► OVERLAYS MODAIS (Confirmação / Renomear / Mover)
- -- ────────────────────────────────────────────────
- -- Helper para criar overlay base
- local function createOverlay(parent, zIndex)
- local overlay = Instance.new("Frame")
- overlay.Size = UDim2.new(1, 0, 1, 0)
- overlay.BackgroundColor3 = CLR.OVERLAY
- overlay.BackgroundTransparency = 0.5
- overlay.Visible = false
- overlay.ZIndex = zIndex
- overlay.Parent = parent
- return overlay
- end
- local function createModalFrame(parent, w, h, zIndex)
- local frame = Instance.new("Frame")
- frame.Size = UDim2.new(0, w, 0, h)
- frame.Position = UDim2.new(0.5, -w/2, 0.5, -h/2)
- frame.BackgroundColor3 = CLR.BG_MID
- frame.BorderSizePixel = 0
- frame.ZIndex = zIndex
- frame.Parent = parent
- UI.corner(frame, 10)
- UI.stroke(frame, CLR.STROKE_BRIGHT, 1.5)
- return frame
- end
- -- ── Modal de Confirmação ──────────────────────
- local ConfirmOverlay = createOverlay(MainFrame, 20)
- local ConfirmFrame = createModalFrame(ConfirmOverlay, 310, 130, 21)
- local ConfirmTitleBar = Instance.new("Frame")
- ConfirmTitleBar.Size = UDim2.new(1, 0, 0, 32)
- ConfirmTitleBar.BackgroundColor3 = CLR.BG_DARK
- ConfirmTitleBar.BorderSizePixel = 0
- ConfirmTitleBar.ZIndex = 22
- ConfirmTitleBar.Parent = ConfirmFrame
- UI.corner(ConfirmTitleBar, 10)
- local ConfirmIcon = UI.label(ConfirmTitleBar, {
- text = "⚠",
- sz = UDim2.new(0, 30, 1, 0),
- pos = UDim2.new(0, 8, 0, 0),
- size = 14,
- color = CLR.WARNING,
- font = Enum.Font.GothamBold,
- align = Enum.TextXAlignment.Center,
- })
- ConfirmIcon.ZIndex = 23
- local ConfirmTitleLbl = UI.label(ConfirmTitleBar, {
- text = "Confirmar Ação",
- sz = UDim2.new(1, -40, 1, 0),
- pos = UDim2.new(0, 38, 0, 0),
- size = 13,
- color = CLR.TEXT_PRI,
- font = Enum.Font.GothamBold,
- align = Enum.TextXAlignment.Left,
- })
- ConfirmTitleLbl.ZIndex = 23
- local ConfirmLabel = UI.label(ConfirmFrame, {
- text = "",
- sz = UDim2.new(1, -20, 0, 40),
- pos = UDim2.new(0, 10, 0, 36),
- size = 13,
- color = CLR.TEXT_SEC,
- font = Enum.Font.Gotham,
- align = Enum.TextXAlignment.Center,
- wrap = true,
- })
- ConfirmLabel.ZIndex = 22
- local ConfirmYesBtn = UI.button(ConfirmFrame, {
- text = "Confirmar",
- sz = UDim2.new(0.5, -12, 0, 32),
- pos = UDim2.new(0, 8, 1, -40),
- bgColor = CLR.DANGER,
- hoverColor = CLR.DANGER_HOV,
- size = 13,
- radius = 7,
- })
- ConfirmYesBtn.ZIndex = 22
- local ConfirmNoBtn = UI.button(ConfirmFrame, {
- text = "Cancelar",
- sz = UDim2.new(0.5, -12, 0, 32),
- pos = UDim2.new(0.5, 4, 1, -40),
- bgColor = CLR.ACCENT_DIM,
- hoverColor = CLR.ACCENT,
- size = 13,
- radius = 7,
- })
- ConfirmNoBtn.ZIndex = 22
- -- ── Modal de Renomear ─────────────────────────
- local RenameOverlay = createOverlay(MainFrame, 25)
- local RenameFrame = createModalFrame(RenameOverlay, 340, 155, 26)
- local RenameTitleBar = Instance.new("Frame")
- RenameTitleBar.Size = UDim2.new(1, 0, 0, 32)
- RenameTitleBar.BackgroundColor3 = CLR.BG_DARK
- RenameTitleBar.BorderSizePixel = 0
- RenameTitleBar.ZIndex = 27
- RenameTitleBar.Parent = RenameFrame
- UI.corner(RenameTitleBar, 10)
- local RenameTitleLbl = UI.label(RenameTitleBar, {
- text = "✏ Renomear Posição",
- sz = UDim2.new(1, -16, 1, 0),
- pos = UDim2.new(0, 12, 0, 0),
- size = 13,
- color = CLR.TEXT_PRI,
- font = Enum.Font.GothamBold,
- align = Enum.TextXAlignment.Left,
- })
- RenameTitleLbl.ZIndex = 28
- local RenameField = Instance.new("TextBox")
- RenameField.Size = UDim2.new(1, -20, 0, 38)
- RenameField.Position = UDim2.new(0, 10, 0, 40)
- RenameField.BackgroundColor3 = CLR.BG_DARK
- RenameField.TextColor3 = CLR.TEXT_PRI
- RenameField.PlaceholderColor3 = CLR.TEXT_MUTED
- RenameField.PlaceholderText = " Digite o novo nome..."
- RenameField.Font = Enum.Font.Gotham
- RenameField.TextSize = 14
- RenameField.ClearTextOnFocus = false
- RenameField.ZIndex = 27
- RenameField.Parent = RenameFrame
- UI.corner(RenameField, 7)
- UI.stroke(RenameField, CLR.STROKE, 1)
- local RenameConfirmBtn = UI.button(RenameFrame, {
- text = "Confirmar",
- sz = UDim2.new(0.5, -12, 0, 34),
- pos = UDim2.new(0, 8, 1, -42),
- bgColor = CLR.INFO,
- hoverColor = CLR.INFO_HOV,
- size = 13,
- radius = 7,
- })
- RenameConfirmBtn.ZIndex = 27
- local RenameCancelBtn = UI.button(RenameFrame, {
- text = "Cancelar",
- sz = UDim2.new(0.5, -12, 0, 34),
- pos = UDim2.new(0.5, 4, 1, -42),
- bgColor = CLR.ACCENT_DIM,
- hoverColor = CLR.ACCENT,
- size = 13,
- radius = 7,
- })
- RenameCancelBtn.ZIndex = 27
- -- ── Modal de Mover ────────────────────────────
- local MoveOverlay = createOverlay(MainFrame, 30)
- local MoveFrame = createModalFrame(MoveOverlay, 300, 170, 31)
- local MoveTitleBar = Instance.new("Frame")
- MoveTitleBar.Size = UDim2.new(1, 0, 0, 32)
- MoveTitleBar.BackgroundColor3 = CLR.BG_DARK
- MoveTitleBar.BorderSizePixel = 0
- MoveTitleBar.ZIndex = 32
- MoveTitleBar.Parent = MoveFrame
- UI.corner(MoveTitleBar, 10)
- local MoveTitleLbl = UI.label(MoveTitleBar, {
- text = "↔ Mover Posição",
- sz = UDim2.new(1, -16, 1, 0),
- pos = UDim2.new(0, 12, 0, 0),
- size = 13,
- color = CLR.TEXT_PRI,
- font = Enum.Font.GothamBold,
- align = Enum.TextXAlignment.Left,
- })
- MoveTitleLbl.ZIndex = 33
- local MoveSubLbl = UI.label(MoveFrame, {
- text = "Slot de destino (1 – 50)",
- sz = UDim2.new(1, -20, 0, 22),
- pos = UDim2.new(0, 10, 0, 38),
- size = 12,
- color = CLR.TEXT_SEC,
- font = Enum.Font.Gotham,
- align = Enum.TextXAlignment.Center,
- })
- MoveSubLbl.ZIndex = 32
- local MoveInput = Instance.new("TextBox")
- MoveInput.Size = UDim2.new(0, 90, 0, 42)
- MoveInput.Position = UDim2.new(0.5, -45, 0, 64)
- MoveInput.BackgroundColor3 = CLR.BG_DARK
- MoveInput.TextColor3 = CLR.TEXT_PRI
- MoveInput.PlaceholderColor3 = CLR.TEXT_MUTED
- MoveInput.PlaceholderText = "1 – 50"
- MoveInput.Font = Enum.Font.GothamBold
- MoveInput.TextSize = 20
- MoveInput.ClearTextOnFocus = true
- MoveInput.ZIndex = 32
- MoveInput.Parent = MoveFrame
- UI.corner(MoveInput, 8)
- UI.stroke(MoveInput, CLR.STROKE, 1)
- local MoveConfirmBtn = UI.button(MoveFrame, {
- text = "Confirmar",
- sz = UDim2.new(0.5, -12, 0, 34),
- pos = UDim2.new(0, 8, 1, -42),
- bgColor = CLR.INFO,
- hoverColor = CLR.INFO_HOV,
- size = 13,
- radius = 7,
- })
- MoveConfirmBtn.ZIndex = 32
- local MoveCancelBtn = UI.button(MoveFrame, {
- text = "Cancelar",
- sz = UDim2.new(0.5, -12, 0, 34),
- pos = UDim2.new(0.5, 4, 1, -42),
- bgColor = CLR.ACCENT_DIM,
- hoverColor = CLR.ACCENT,
- size = 13,
- radius = 7,
- })
- MoveCancelBtn.ZIndex = 32
- -- ============================================================
- -- SEÇÃO 12 ► LÓGICA DE ESTADO E ATUALIZAÇÃO DA UI
- -- ============================================================
- -- Callbacks dos modais
- local onConfirmYes = nil
- local slotRenaming = nil
- local slotMoving = nil
- -- Atualiza o display do slot de destino
- local function refreshSlotUI()
- local n = State.saveSlot
- local data = State.saves[n]
- local disp = string.format("%02d", n)
- SlotValueLbl.Text = disp
- if data then
- SlotStatusLbl.Text = "ocupado"
- SlotStatusLbl.TextColor3 = CLR.WARNING
- else
- SlotStatusLbl.Text = "livre"
- SlotStatusLbl.TextColor3 = CLR.SUCCESS
- end
- end
- -- Atualiza os dots de paginação
- local function refreshPageDots()
- for i, dot in ipairs(PageDots) do
- TweenService:Create(dot, CFG.TWEEN_FAST, {
- BackgroundColor3 = (i == State.currentPage) and CLR.ACCENT or CLR.TEXT_MUTED,
- Size = (i == State.currentPage)
- and UDim2.new(0, 9, 0, 9)
- or UDim2.new(0, 7, 0, 7),
- }):Play()
- end
- end
- -- Atualiza o label de página
- local function refreshPageLabel()
- PageLabel.Text = string.format("PÁGINA %d / %d", State.currentPage, CFG.MAX_PAGES)
- end
- -- Atualiza visibilidade do botão de posição principal
- local function refreshMainPosUI()
- MainPosContainer.Visible = (State.mainPos ~= nil)
- end
- -- Encontra o próximo slot vazio
- local function findNextEmpty()
- for i = 1, CFG.MAX_SAVES do
- if not State.saves[i] then return i end
- end
- -- Se todos cheios, retorna o próximo após o atual
- return (State.saveSlot % CFG.MAX_SAVES) + 1
- end
- local function advanceSlot()
- State.saveSlot = findNextEmpty()
- refreshSlotUI()
- end
- -- Mostra overlay de confirmação
- local function showConfirm(message, yesText, callback)
- ConfirmLabel.Text = message
- ConfirmYesBtn.Text = yesText or "Confirmar"
- onConfirmYes = callback
- ConfirmOverlay.Visible = true
- end
- -- ────────────────────────────────────────────────
- -- 12.1 ► RENDERIZAÇÃO DOS SLOTS (página atual)
- -- ────────────────────────────────────────────────
- local function buildSlot(slot)
- local data = State.saves[slot]
- local card = Instance.new("Frame")
- card.Name = "Slot_" .. slot
- card.LayoutOrder = slot
- card.BackgroundColor3 = CLR.BG_CARD
- card.BorderSizePixel = 0
- card.ClipsDescendants = false
- UI.corner(card, 7)
- local cardStroke = UI.stroke(card, CLR.STROKE, 1)
- if data then
- -- ── Slot preenchido ───────────────────────────
- local isMain = State.mainPos and typeof(State.mainPos.position) == "CFrame"
- and State.mainPos.position == data.position
- -- Indicador lateral colorido
- local Accent = Instance.new("Frame")
- Accent.Size = UDim2.new(0, 3, 1, -10)
- Accent.Position = UDim2.new(0, 0, 0, 5)
- Accent.BackgroundColor3 = isMain and CLR.GOLD or CLR.ACCENT
- Accent.BorderSizePixel = 0
- Accent.Parent = card
- UI.corner(Accent, 2)
- -- Número do slot (badge)
- local SlotNumBg = Instance.new("Frame")
- SlotNumBg.Size = UDim2.new(0, 24, 0, 18)
- SlotNumBg.Position = UDim2.new(0, 7, 0.5, -9)
- SlotNumBg.BackgroundColor3 = isMain and CLR.GOLD or CLR.ACCENT_DIM
- SlotNumBg.BorderSizePixel = 0
- SlotNumBg.Parent = card
- UI.corner(SlotNumBg, 4)
- local SlotNumLbl = UI.label(SlotNumBg, {
- text = tostring(slot),
- size = 9,
- color = CLR.WHITE,
- font = Enum.Font.GothamBold,
- align = Enum.TextXAlignment.Center,
- })
- -- Nome da posição
- local NameLbl = UI.label(card, {
- text = data.name,
- sz = UDim2.new(1, -105, 0, 22),
- pos = UDim2.new(0, 38, 0, 4),
- size = 12,
- color = CLR.TEXT_PRI,
- font = Enum.Font.GothamSemibold,
- align = Enum.TextXAlignment.Left,
- truncate = Enum.TextTruncate.AtEnd,
- })
- -- Timestamp
- local TimeLbl = UI.label(card, {
- text = data.timestamp,
- sz = UDim2.new(1, -105, 0, 14),
- pos = UDim2.new(0, 38, 0, 26),
- size = 9,
- color = CLR.TEXT_MUTED,
- font = Enum.Font.Gotham,
- align = Enum.TextXAlignment.Left,
- })
- -- Botão Teleportar
- local TpBtn = UI.button(card, {
- text = "TP",
- sz = UDim2.new(0, 34, 0, 34),
- pos = UDim2.new(1, -78, 0.5, -17),
- bgColor = CLR.ACCENT,
- hoverColor = CLR.ACCENT_HOV,
- font = Enum.Font.GothamBold,
- size = 13,
- radius = 7,
- })
- -- Botão Favorito (estrela)
- local StarBtn = UI.button(card, {
- text = isMain and "★" or "☆",
- sz = UDim2.new(0, 18, 0, 20),
- pos = UDim2.new(1, -40, 0, 4),
- bgColor = Color3.fromRGB(0,0,0),
- textColor = isMain and CLR.GOLD or CLR.TEXT_DIM,
- size = 14,
- radius = 4,
- })
- StarBtn.BackgroundTransparency = 1
- StarBtn.AutoButtonColor = false
- -- Botão Menu de opções
- local MenuBtn = UI.button(card, {
- text = "⋮",
- sz = UDim2.new(0, 18, 0, 20),
- pos = UDim2.new(1, -40, 1, -24),
- bgColor = Color3.fromRGB(0,0,0),
- textColor = CLR.TEXT_DIM,
- size = 15,
- radius = 4,
- })
- MenuBtn.BackgroundTransparency = 1
- MenuBtn.AutoButtonColor = false
- -- Menu de opções flutuante (no ScreenGui para não ser cortado)
- local OptionsMenu = Instance.new("Frame")
- OptionsMenu.Name = "OptionsMenu_" .. slot
- OptionsMenu.Size = UDim2.new(0, 130, 0, 102)
- OptionsMenu.BackgroundColor3 = CLR.BG_MID
- OptionsMenu.BackgroundTransparency = 0.04
- OptionsMenu.Visible = false
- OptionsMenu.ZIndex = 60
- OptionsMenu.BorderSizePixel = 0
- OptionsMenu.Parent = ScreenGui
- UI.corner(OptionsMenu, 8)
- UI.stroke(OptionsMenu, CLR.STROKE_BRIGHT, 1.5)
- State.optionFrames[slot] = OptionsMenu
- local function makeMenuOption(text, color, yPos)
- local btn = UI.button(OptionsMenu, {
- text = text,
- sz = UDim2.new(1, -12, 0, 26),
- pos = UDim2.new(0, 6, 0, yPos),
- bgColor = CLR.BG_CARD,
- hoverColor = CLR.BG_CARD_HOV,
- textColor = color or CLR.TEXT_PRI,
- font = Enum.Font.Gotham,
- size = 12,
- radius = 5,
- })
- btn.TextXAlignment = Enum.TextXAlignment.Left
- btn.ZIndex = 61
- -- Adiciona padding com label à esquerda
- return btn
- end
- local RenameOpt = makeMenuOption(" ✏ Renomear", CLR.TEXT_PRI, 8)
- local MoveOpt = makeMenuOption(" ↔ Mover", CLR.TEXT_PRI, 38)
- local DeleteOpt = makeMenuOption(" 🗑 Excluir", CLR.DANGER, 68)
- local menuOpen = false
- local function closeMenu()
- if not OptionsMenu.Parent then return end
- OptionsMenu.Visible = false
- menuOpen = false
- end
- local function openMenu()
- UI.closeAllOptionMenus(OptionsMenu)
- local absPos = MenuBtn.AbsolutePosition
- local absSize = MenuBtn.AbsoluteSize
- OptionsMenu.Position = UDim2.new(0,
- absPos.X - 130 + absSize.X,
- 0,
- absPos.Y - 5
- )
- OptionsMenu.Visible = true
- menuOpen = true
- end
- MenuBtn.MouseButton1Click:Connect(function()
- if menuOpen then closeMenu() else openMenu() end
- end)
- -- Fecha menu ao clicar fora
- UserInputService.InputBegan:Connect(function(input)
- if not menuOpen then return end
- if input.UserInputType == Enum.UserInputType.MouseButton1 then
- task.wait(0.05)
- if not OptionsMenu.Visible then return end
- local mp = UserInputService:GetMouseLocation()
- local fp = OptionsMenu.AbsolutePosition
- local fs = OptionsMenu.AbsoluteSize
- if mp.X < fp.X or mp.X > fp.X + fs.X
- or mp.Y < fp.Y or mp.Y > fp.Y + fs.Y then
- closeMenu()
- end
- end
- end)
- -- Hover visual no card
- card.MouseEnter:Connect(function()
- TweenService:Create(card, CFG.TWEEN_FAST,
- {BackgroundColor3 = CLR.BG_CARD_HOV}):Play()
- TweenService:Create(cardStroke, CFG.TWEEN_FAST,
- {Color = CLR.STROKE_BRIGHT}):Play()
- end)
- card.MouseLeave:Connect(function()
- TweenService:Create(card, CFG.TWEEN_FAST,
- {BackgroundColor3 = CLR.BG_CARD}):Play()
- TweenService:Create(cardStroke, CFG.TWEEN_FAST,
- {Color = CLR.STROKE}):Play()
- end)
- -- Ações dos botões
- TpBtn.MouseButton1Click:Connect(function()
- if Teleport.to(data.position) then
- TpBtn.Text = "✓"
- TweenService:Create(TpBtn, CFG.TWEEN_FAST,
- {BackgroundColor3 = CLR.SUCCESS}):Play()
- task.wait(1.2)
- if TpBtn.Parent then
- TpBtn.Text = "TP"
- TweenService:Create(TpBtn, CFG.TWEEN_FAST,
- {BackgroundColor3 = CLR.ACCENT}):Play()
- end
- end
- end)
- StarBtn.MouseButton1Click:Connect(function()
- if isMain then
- showConfirm(
- "Remover esta posição principal?",
- "Remover",
- function()
- State.mainPos = nil
- Persist.deleteMain()
- refreshMainPosUI()
- -- Força re-render da lista
- task.defer(function()
- local rebuilder = ScreenGui:FindFirstChild("__rebuild")
- if rebuilder then rebuilder:Fire() end
- end)
- end
- )
- else
- local hasMain = State.mainPos ~= nil
- showConfirm(
- hasMain and "Substituir a posição principal atual?"
- or "Definir esta posição como principal?",
- "Sim",
- function()
- State.mainPos = {
- position = data.position,
- name = data.name,
- timestamp = data.timestamp,
- }
- Persist.scheduleSave()
- refreshMainPosUI()
- task.defer(function()
- local rebuilder = ScreenGui:FindFirstChild("__rebuild")
- if rebuilder then rebuilder:Fire() end
- end)
- end
- )
- end
- end)
- RenameOpt.MouseButton1Click:Connect(function()
- closeMenu()
- slotRenaming = slot
- RenameField.Text = data.name
- RenameOverlay.Visible = true
- end)
- MoveOpt.MouseButton1Click:Connect(function()
- closeMenu()
- slotMoving = slot
- MoveInput.Text = ""
- MoveOverlay.Visible = true
- end)
- DeleteOpt.MouseButton1Click:Connect(function()
- closeMenu()
- showConfirm(
- string.format("Excluir o Slot %d \"%s\" ?", slot, data.name),
- "Excluir",
- function()
- State.saves[slot] = nil
- Persist.scheduleSave()
- showToast("Slot " .. slot .. " excluído", "info")
- advanceSlot()
- -- Re-render
- local rebuilder = ScreenGui:FindFirstChild("__rebuild")
- if rebuilder then rebuilder:Fire() end
- end
- )
- end)
- else
- -- ── Slot vazio ────────────────────────────────
- local EmptyLbl = UI.label(card, {
- text = string.format("Slot %02d — vazio", slot),
- sz = UDim2.new(1, -10, 1, 0),
- pos = UDim2.new(0, 10, 0, 0),
- size = 12,
- color = CLR.TEXT_MUTED,
- font = Enum.Font.Gotham,
- align = Enum.TextXAlignment.Left,
- })
- -- Destaca slot de destino atual
- if slot == State.saveSlot then
- cardStroke.Color = CLR.ACCENT
- cardStroke.Thickness = 1.5
- EmptyLbl.TextColor3 = CLR.ACCENT
- EmptyLbl.Text = string.format("▶ Slot %02d — selecionado", slot)
- end
- end
- return card
- end
- -- Renderiza a página atual
- local function renderPage()
- -- Limpa slots anteriores
- for _, child in ipairs(SlotsFrame:GetChildren()) do
- if child:IsA("Frame") then
- child:Destroy()
- end
- end
- -- Limpa menus de opções órfãos
- for slot, frame in pairs(State.optionFrames) do
- if frame and frame.Parent then
- frame:Destroy()
- end
- end
- State.optionFrames = {}
- local startSlot = (State.currentPage - 1) * CFG.SLOTS_PER_PAGE + 1
- local endSlot = State.currentPage * CFG.SLOTS_PER_PAGE
- for slot = startSlot, endSlot do
- local card = buildSlot(slot)
- card.Parent = SlotsFrame
- end
- refreshPageDots()
- refreshPageLabel()
- end
- -- Ponto de re-render global (via BindableEvent)
- local RebuildEvent = Instance.new("BindableEvent")
- RebuildEvent.Name = "__rebuild"
- RebuildEvent.Parent = ScreenGui
- RebuildEvent.Event:Connect(renderPage)
- -- ============================================================
- -- SEÇÃO 13 ► CONEXÕES DE CONTROLES
- -- ============================================================
- -- ── Fechar / Abrir janela ─────────────────────
- CloseBtn.MouseButton1Click:Connect(function()
- TweenService:Create(MainFrame, CFG.TWEEN_MED, {
- Size = UDim2.new(0, 590, 0, 0),
- Position = UDim2.new(0.5, -295, 0.5, 0),
- }):Play()
- task.wait(0.3)
- MainFrame.Visible = false
- MainFrame.Size = UDim2.new(0, 590, 0, 470)
- MainFrame.Position = UDim2.new(0.5, -295, 0.5, -235)
- end)
- FloatBtn.MouseButton1Click:Connect(function()
- MainFrame.Size = UDim2.new(0, 590, 0, 0)
- MainFrame.Position = UDim2.new(0.5, -295, 0.5, 0)
- MainFrame.Visible = true
- TweenService:Create(MainFrame, CFG.TWEEN_MED, {
- Size = UDim2.new(0, 590, 0, 470),
- Position = UDim2.new(0.5, -295, 0.5, -235),
- }):Play()
- end)
- MainFrame:GetPropertyChangedSignal("Visible"):Connect(function()
- FloatContainer.Visible = not MainFrame.Visible
- end)
- -- ── Paginação ────────────────────────────────
- local function changePage(delta)
- local newPage = State.currentPage + delta
- if newPage < 1 then newPage = CFG.MAX_PAGES end
- if newPage > CFG.MAX_PAGES then newPage = 1 end
- State.currentPage = newPage
- renderPage()
- end
- PagePrevBtn.MouseButton1Click:Connect(function() changePage(-1) end)
- PageNextBtn.MouseButton1Click:Connect(function() changePage( 1) end)
- -- ── Seletor de Slot ───────────────────────────
- SlotPrevBtn.MouseButton1Click:Connect(function()
- State.saveSlot = ((State.saveSlot - 2 + CFG.MAX_SAVES) % CFG.MAX_SAVES) + 1
- refreshSlotUI()
- renderPage()
- end)
- SlotNextBtn.MouseButton1Click:Connect(function()
- State.saveSlot = (State.saveSlot % CFG.MAX_SAVES) + 1
- refreshSlotUI()
- renderPage()
- end)
- -- ── Botão Salvar ──────────────────────────────
- local function doSave()
- if not root then return false end
- local name = (NameBox.Text ~= "" and NameBox.Text) or ("Slot " .. State.saveSlot)
- State.saves[State.saveSlot] = {
- position = root.CFrame,
- name = name,
- timestamp = os.date("%Y-%m-%d %H:%M:%S"),
- }
- Persist.scheduleSave()
- -- Navega para a página do slot salvo
- local targetPage = math.ceil(State.saveSlot / CFG.SLOTS_PER_PAGE)
- if targetPage ~= State.currentPage then
- State.currentPage = targetPage
- end
- renderPage()
- showToast(string.format("✓ Salvo no Slot %d", State.saveSlot), "success")
- advanceSlot()
- return true
- end
- SaveBtn.MouseButton1Click:Connect(function()
- if State.saves[State.saveSlot] then
- showConfirm(
- string.format("O Slot %d já tem uma posição.\nDeseja substituir?", State.saveSlot),
- "Substituir",
- function()
- doSave()
- end
- )
- else
- doSave()
- end
- end)
- -- ── Botão Posição Principal (TP) ──────────────
- MainPosBtn.MouseButton1Click:Connect(function()
- if State.mainPos and Teleport.to(State.mainPos.position) then
- MainPosBtn.Text = "✓"
- TweenService:Create(MainPosBtn, CFG.TWEEN_FAST,
- {BackgroundColor3 = CLR.SUCCESS}):Play()
- task.wait(1)
- if MainPosBtn.Parent then
- MainPosBtn.Text = "🚀"
- TweenService:Create(MainPosBtn, CFG.TWEEN_FAST,
- {BackgroundColor3 = CLR.BG_MID}):Play()
- end
- end
- end)
- MainPosBtn.MouseEnter:Connect(function()
- if State.mainPos then
- TooltipLabel.Text = State.mainPos.name or "Principal"
- MainPosTooltip.Visible = true
- MainPosRemoveBtn.Visible = true
- end
- TweenService:Create(MainPosStroke, CFG.TWEEN_FAST, {Thickness = 3}):Play()
- TweenService:Create(MainPosBtn, CFG.TWEEN_FAST,
- {Size = UDim2.new(1.1, 0, 1.1, 0)}):Play()
- end)
- MainPosBtn.MouseLeave:Connect(function()
- MainPosTooltip.Visible = false
- MainPosRemoveBtn.Visible = false
- TweenService:Create(MainPosStroke, CFG.TWEEN_FAST, {Thickness = 2}):Play()
- TweenService:Create(MainPosBtn, CFG.TWEEN_FAST,
- {Size = UDim2.new(1, 0, 1, 0)}):Play()
- end)
- MainPosRemoveBtn.MouseButton1Click:Connect(function()
- showConfirm(
- "Remover a posição principal?",
- "Remover",
- function()
- State.mainPos = nil
- Persist.deleteMain()
- refreshMainPosUI()
- renderPage()
- end
- )
- end)
- -- ── Confirmação ───────────────────────────────
- ConfirmYesBtn.MouseButton1Click:Connect(function()
- if onConfirmYes then onConfirmYes() end
- onConfirmYes = nil
- ConfirmOverlay.Visible = false
- end)
- ConfirmNoBtn.MouseButton1Click:Connect(function()
- onConfirmYes = nil
- ConfirmOverlay.Visible = false
- end)
- -- ── Renomear ──────────────────────────────────
- RenameConfirmBtn.MouseButton1Click:Connect(function()
- local newName = RenameField.Text
- if not slotRenaming or newName == "" then return end
- local data = State.saves[slotRenaming]
- if data then
- data.name = newName
- -- Atualiza posição principal se for o mesmo
- if State.mainPos and State.mainPos.position == data.position then
- State.mainPos.name = newName
- end
- Persist.scheduleSave()
- renderPage()
- showToast("✏ Slot " .. slotRenaming .. " renomeado", "info")
- end
- RenameOverlay.Visible = false
- slotRenaming = nil
- end)
- RenameCancelBtn.MouseButton1Click:Connect(function()
- RenameOverlay.Visible = false
- slotRenaming = nil
- end)
- -- ── Mover ─────────────────────────────────────
- MoveConfirmBtn.MouseButton1Click:Connect(function()
- local target = tonumber(MoveInput.Text)
- if not slotMoving or not target then return end
- if target < 1 or target > CFG.MAX_SAVES or target ~= math.floor(target) then
- MoveInput.Text = ""
- MoveInput.PlaceholderText = "Inválido! (1-50)"
- task.delay(1.5, function()
- MoveInput.PlaceholderText = "1 – 50"
- end)
- return
- end
- local from = slotMoving
- local to = target
- if from == to then
- MoveOverlay.Visible = false
- slotMoving = nil
- return
- end
- -- Swap
- if State.saves[to] then
- local temp = State.saves[from]
- State.saves[from] = State.saves[to]
- State.saves[to] = temp
- else
- State.saves[to] = State.saves[from]
- State.saves[from] = nil
- end
- Persist.scheduleSave()
- -- Navega para a página do slot de destino
- local targetPage = math.ceil(to / CFG.SLOTS_PER_PAGE)
- State.currentPage = targetPage
- renderPage()
- showToast(string.format("↔ Slot %d → Slot %d", from, to), "info")
- MoveOverlay.Visible = false
- slotMoving = nil
- end)
- MoveCancelBtn.MouseButton1Click:Connect(function()
- MoveOverlay.Visible = false
- slotMoving = nil
- end)
- -- ── Teclado: Enter confirma campos ────────────
- RenameField.FocusLost:Connect(function(enterPressed)
- if enterPressed then RenameConfirmBtn.MouseButton1Click:Fire() end
- end)
- MoveInput.FocusLost:Connect(function(enterPressed)
- if enterPressed then MoveConfirmBtn.MouseButton1Click:Fire() end
- end)
- -- ============================================================
- -- SEÇÃO 14 ► EVENTOS DE CICLO DE VIDA
- -- ============================================================
- -- Respawn: atualiza referências do personagem
- plr.CharacterAdded:Connect(function(character)
- chr = character
- root = character:WaitForChild("HumanoidRootPart")
- end)
- -- Salva ao fechar executor
- game:BindToClose(Persist.save)
- -- Salva periodicamente (a cada 60s) como segurança extra
- task.spawn(function()
- while task.wait(60) do
- if State.isDirty then
- Persist.save()
- end
- end
- end)
- -- ============================================================
- -- SEÇÃO 15 ► INICIALIZAÇÃO
- -- ============================================================
- Persist.load()
- refreshSlotUI()
- refreshMainPosUI()
- renderPage()
- -- Log de inicialização
- print(string.format([[
- ╔══════════════════════════════════════════════════════╗
- ║ ✦ Save Locate (NickZ) V%s — MEGA UPDATE ║
- ║ ┌─────────────────────────────────────────────────┐ ║
- ║ │ Slots : %d (5 páginas × 10) │ ║
- ║ │ Saves : %d carregados do disco │ ║
- ║ │ FS : %s │ ║
- ║ │ Teleport: Hard-TP Anti-Bug ativo │ ║
- ║ └─────────────────────────────────────────────────┘ ║
- ╚══════════════════════════════════════════════════════╝
- ]], CFG.VERSION,
- CFG.MAX_SAVES,
- (function() local n=0; for _ in pairs(State.saves) do n=n+1 end; return n end)(),
- FS.active and "Ativo (leitura+escrita)" or "Inativo (sem suporte)"
- ))
Add Comment
Please, Sign In to add comment