Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- ╔══════════════════════════════════════════════════════════════╗
- ║ Save Locate (NickZ) — V5.0 MEGA UPDATE ║
- ║ ║
- ║ • Arquitetura modular profissional ║
- ║ • Teleporte robusto (anti-void, anti-seat, retry) ║
- ║ • UI componentizada e otimizada ║
- ║ • Persistência com validação e integridade ║
- ║ • Sistema de paginação avançado ║
- ║ • Atalhos de teclado ║
- ║ • Performance: pooling, cleanup, throttle ║
- ║ • Compatível: Delta, Fluxus, KRNL, Arceus, Solara ║
- ╚══════════════════════════════════════════════════════════════╝
- --]]
- ----------------------------------------------------------------
- -- SEÇÃO 0 · BOOTSTRAP SEGURO
- ----------------------------------------------------------------
- if not game then return end
- if not game:IsLoaded() then game.Loaded:Wait() end
- -- Anti-duplicação
- local SCRIPT_ID = "SaveLocate_NickZ_V5"
- if getgenv and type(getgenv) == "function" then
- if getgenv()[SCRIPT_ID] then
- -- Destrói instância anterior
- local oldGui = game:GetService("CoreGui"):FindFirstChild("SaveLocateNickZV5")
- if oldGui then oldGui:Destroy() end
- end
- getgenv()[SCRIPT_ID] = true
- end
- ----------------------------------------------------------------
- -- SEÇÃO 1 · SERVIÇOS (cache único)
- ----------------------------------------------------------------
- local Services = setmetatable({}, {
- __index = function(self, name)
- local service = game:GetService(name)
- rawset(self, name, service)
- return service
- end
- })
- local Players = Services.Players
- local HttpService = Services.HttpService
- local TweenService = Services.TweenService
- local UserInputService = Services.UserInputService
- local RunService = Services.RunService
- local CoreGui = Services.CoreGui
- ----------------------------------------------------------------
- -- SEÇÃO 2 · REFERÊNCIAS DO JOGADOR
- ----------------------------------------------------------------
- local Player = Players.LocalPlayer
- local CharacterManager = {
- character = nil,
- rootPart = nil,
- humanoid = nil,
- _connections = {},
- }
- function CharacterManager:Refresh()
- local char = Player.Character
- if not char then return false end
- self.character = char
- self.rootPart = char:FindFirstChild("HumanoidRootPart")
- self.humanoid = char:FindFirstChildWhichIsA("Humanoid")
- return self.rootPart ~= nil
- end
- function CharacterManager:WaitForCharacter()
- local char = Player.Character or Player.CharacterAdded:Wait()
- self.character = char
- self.rootPart = char:WaitForChild("HumanoidRootPart", 15)
- self.humanoid = char:WaitForChild("Humanoid", 15)
- return self.rootPart ~= nil
- end
- function CharacterManager:IsAlive()
- if not self.character or not self.character.Parent then return false end
- if not self.rootPart or not self.rootPart.Parent then return false end
- if not self.humanoid or self.humanoid.Health <= 0 then return false end
- return true
- end
- function CharacterManager:Init()
- self:WaitForCharacter()
- -- Reconectar quando personagem renascer
- table.insert(self._connections, Player.CharacterAdded:Connect(function(char)
- self.character = char
- self.rootPart = char:WaitForChild("HumanoidRootPart", 15)
- self.humanoid = char:WaitForChild("Humanoid", 15)
- end))
- end
- function CharacterManager:Destroy()
- for _, conn in ipairs(self._connections) do
- if conn.Connected then conn:Disconnect() end
- end
- self._connections = {}
- end
- ----------------------------------------------------------------
- -- SEÇÃO 3 · DETECÇÃO DE AMBIENTE (UNC / Executor)
- ----------------------------------------------------------------
- local Environment = {}
- function Environment:Detect()
- self.hasWriteFile = type(writefile) == "function"
- self.hasReadFile = type(readfile) == "function"
- self.hasFileSystem = self.hasWriteFile and self.hasReadFile
- self.hasIsFolder = type(isfolder) == "function"
- self.hasMakeFolder = type(makefolder) == "function"
- self.hasFolderOps = self.hasIsFolder and self.hasMakeFolder
- self.hasIsFile = type(isfile) == "function"
- self.hasDelFile = type(delfile) == "function"
- self.hasListFiles = type(listfiles) == "function"
- -- Identificar executor
- self.executorName = "Unknown"
- if type(identifyexecutor) == "function" then
- local ok, name = pcall(identifyexecutor)
- if ok and name then self.executorName = tostring(name) end
- end
- -- Clipboard
- self.hasClipboard = type(setclipboard) == "function"
- return self
- end
- Environment:Detect()
- ----------------------------------------------------------------
- -- SEÇÃO 4 · CODIFICAÇÃO / DECODIFICAÇÃO DE CFRAME
- ----------------------------------------------------------------
- local CFrameCodec = {}
- function CFrameCodec.Encode(cf)
- if typeof(cf) ~= "CFrame" then return nil end
- local components = {cf:GetComponents()}
- return {
- x = components[1], y = components[2], z = components[3],
- R00 = components[4], R01 = components[5], R02 = components[6],
- R10 = components[7], R11 = components[8], R12 = components[9],
- R20 = components[10], R21 = components[11], R22 = components[12],
- }
- end
- function CFrameCodec.Decode(t)
- if type(t) ~= "table" then return nil end
- if not (t.x and t.y and t.z and t.R00) then return nil end
- local ok, cf = pcall(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
- )
- return ok and cf or nil
- end
- function CFrameCodec.IsValid(cf)
- if typeof(cf) ~= "CFrame" then return false end
- local pos = cf.Position
- -- Proteção anti-void: rejeita coordenadas absurdas
- local MAX = 1e7
- if math.abs(pos.X) > MAX or math.abs(pos.Y) > MAX or math.abs(pos.Z) > MAX then
- return false
- end
- -- Verifica NaN
- if pos.X ~= pos.X or pos.Y ~= pos.Y or pos.Z ~= pos.Z then
- return false
- end
- return true
- end
- ----------------------------------------------------------------
- -- SEÇÃO 5 · SISTEMA DE PERSISTÊNCIA
- ----------------------------------------------------------------
- local Persistence = {
- SAVE_DIR = "SaveLocate_NickZ",
- VERSION = 5,
- MAX_SAVES = 50,
- _gameDir = nil,
- _saveFile = nil,
- _mainFile = nil,
- _saveBak = nil,
- _mainBak = nil,
- }
- function Persistence:Init()
- local gameId = tostring(game.PlaceId)
- local userId = tostring(Player.UserId)
- if Environment.hasFolderOps then
- -- Cria diretórios
- if not isfolder(self.SAVE_DIR) then
- pcall(makefolder, self.SAVE_DIR)
- end
- self._gameDir = self.SAVE_DIR .. "/" .. gameId
- if not isfolder(self._gameDir) then
- pcall(makefolder, self._gameDir)
- end
- self._saveFile = self._gameDir .. "/Saves_" .. userId .. ".json"
- self._mainFile = self._gameDir .. "/MainPos_" .. userId .. ".json"
- self._saveBak = self._gameDir .. "/Saves_" .. userId .. ".bak"
- self._mainBak = self._gameDir .. "/MainPos_" .. userId .. ".bak"
- else
- -- Fallback sem pastas
- local prefix = string.format("SL_%s_%s", gameId, userId)
- self._saveFile = prefix .. "_saves.json"
- self._mainFile = prefix .. "_main.json"
- self._saveBak = prefix .. "_saves.bak"
- self._mainBak = prefix .. "_main.bak"
- end
- end
- function Persistence:_SafeRead(path)
- if not Environment.hasFileSystem or type(path) ~= "string" then return nil end
- -- Verifica se arquivo existe antes de ler
- if Environment.hasIsFile then
- local exists = pcall(function() return isfile(path) end)
- if not exists then return nil end
- local ok, result = pcall(isfile, path)
- if not ok or not result then return nil end
- end
- local ok, content = pcall(readfile, path)
- if ok and type(content) == "string" and #content > 2 then
- return content
- end
- return nil
- end
- function Persistence:_SafeWrite(path, content)
- if not Environment.hasWriteFile or type(path) ~= "string" then return false end
- local ok = pcall(writefile, path, content)
- return ok
- end
- function Persistence:_SafeDelete(path)
- if not Environment.hasDelFile or type(path) ~= "string" then return false end
- local ok = pcall(delfile, path)
- return ok
- end
- function Persistence:_ReadWithFallback(primary, backup)
- local content = self:_SafeRead(primary)
- if content then return content, primary end
- content = self:_SafeRead(backup)
- if content then return content, backup end
- -- Tenta caminhos legados (V4)
- local legacyPaths = {
- string.format("SaveLocate_NickZ/%s/Saves_%s.json", game.PlaceId, Player.UserId),
- string.format("SkyTP/Saves_%d_%d.json", game.PlaceId, Player.UserId),
- string.format("Saves_%d_%d.json", game.PlaceId, Player.UserId),
- string.format("SaveLocate_%d_%s.json", game.PlaceId, Player.UserId),
- }
- for _, path in ipairs(legacyPaths) do
- content = self:_SafeRead(path)
- if content then return content, path end
- end
- return nil, nil
- end
- function Persistence:_Validate(data)
- if type(data) ~= "table" then return false end
- -- Aceita dados sem gameId (migração) ou com gameId correto
- if data.gameId and data.gameId ~= game.PlaceId then return false end
- return true
- end
- function Persistence:SavePositions(positions)
- if not Environment.hasFileSystem then return false end
- local payload = {
- version = self.VERSION,
- gameId = game.PlaceId,
- gameName = game.Name,
- userId = Player.UserId,
- userName = Player.Name,
- saveDate = os.date("%Y-%m-%d %H:%M:%S"),
- positions = {},
- }
- for slot, data in pairs(positions) do
- if type(slot) == "number" and slot >= 1 and slot <= self.MAX_SAVES then
- if typeof(data.position) == "CFrame" and CFrameCodec.IsValid(data.position) then
- payload.positions[tostring(slot)] = {
- name = data.name or ("Slot " .. slot),
- timestamp = data.timestamp or os.date("%Y-%m-%d %H:%M:%S"),
- position = CFrameCodec.Encode(data.position),
- }
- end
- end
- end
- local ok, encoded = pcall(function()
- return HttpService:JSONEncode(payload)
- end)
- if not ok or not encoded then return false end
- -- Escreve backup primeiro, depois principal (mais seguro)
- self:_SafeWrite(self._saveBak, encoded)
- return self:_SafeWrite(self._saveFile, encoded)
- end
- function Persistence:LoadPositions()
- local positions = {}
- if not Environment.hasFileSystem then return positions end
- local content, usedPath = self:_ReadWithFallback(self._saveFile, self._saveBak)
- if not content then return positions end
- local ok, data = pcall(function()
- return HttpService:JSONDecode(content)
- end)
- if not ok or not self:_Validate(data) then return positions end
- local rawPositions = data.positions or data
- for k, v in pairs(rawPositions) do
- local slot = tonumber(k)
- if slot and slot >= 1 and slot <= self.MAX_SAVES and type(v) == "table" then
- local cf = CFrameCodec.Decode(v.position)
- if cf and CFrameCodec.IsValid(cf) then
- positions[slot] = {
- position = cf,
- name = tostring(v.name or ("Slot " .. slot)),
- timestamp = tostring(v.timestamp or ""),
- }
- end
- end
- end
- -- Se leu de caminho legado, salva no caminho novo
- if usedPath and usedPath ~= self._saveFile then
- -- Migração silenciosa
- task.defer(function()
- local tempPositions = positions
- -- Salva no formato novo
- local p = {
- version = self.VERSION,
- gameId = game.PlaceId,
- gameName = game.Name,
- userId = Player.UserId,
- userName = Player.Name,
- saveDate = os.date("%Y-%m-%d %H:%M:%S"),
- positions = {},
- }
- for s, d in pairs(tempPositions) do
- p.positions[tostring(s)] = {
- name = d.name,
- timestamp = d.timestamp,
- position = CFrameCodec.Encode(d.position),
- }
- end
- local okE, enc = pcall(function() return HttpService:JSONEncode(p) end)
- if okE and enc then
- self:_SafeWrite(self._saveFile, enc)
- self:_SafeWrite(self._saveBak, enc)
- end
- end)
- end
- return positions
- end
- function Persistence:SaveMainPosition(mainPos)
- if not Environment.hasFileSystem or not mainPos then return false end
- if not CFrameCodec.IsValid(mainPos.position) then return false end
- local payload = {
- version = self.VERSION,
- gameId = game.PlaceId,
- position = CFrameCodec.Encode(mainPos.position),
- name = mainPos.name or "Principal",
- timestamp = mainPos.timestamp or os.date("%Y-%m-%d %H:%M:%S"),
- }
- local ok, encoded = pcall(function()
- return HttpService:JSONEncode(payload)
- end)
- if not ok or not encoded then return false end
- self:_SafeWrite(self._mainBak, encoded)
- return self:_SafeWrite(self._mainFile, encoded)
- end
- function Persistence:LoadMainPosition()
- if not Environment.hasFileSystem then return nil end
- local content = self:_SafeRead(self._mainFile) or self:_SafeRead(self._mainBak)
- if not content then return nil end
- local ok, data = pcall(function()
- return HttpService:JSONDecode(content)
- end)
- if not ok or type(data) ~= "table" then return nil end
- if data.gameId and data.gameId ~= game.PlaceId then return nil end
- local cf = CFrameCodec.Decode(data.position)
- if not cf or not CFrameCodec.IsValid(cf) then return nil end
- return {
- position = cf,
- name = data.name or "Principal",
- timestamp = data.timestamp or "",
- }
- end
- function Persistence:DeleteMainPosition()
- self:_SafeDelete(self._mainFile)
- self:_SafeDelete(self._mainBak)
- end
- ----------------------------------------------------------------
- -- SEÇÃO 6 · SISTEMA DE TELEPORTE ROBUSTO
- ----------------------------------------------------------------
- local Teleporter = {
- MAX_RETRIES = 3,
- RETRY_DELAY = 0.1,
- STABILIZE_FRAMES = 2,
- }
- function Teleporter:Execute(targetCFrame)
- -- Validações
- if not CFrameCodec.IsValid(targetCFrame) then
- return false, "CFrame inválido"
- end
- if not CharacterManager:Refresh() then
- return false, "Personagem não encontrado"
- end
- if not CharacterManager:IsAlive() then
- return false, "Personagem morto"
- end
- local root = CharacterManager.rootPart
- local hum = CharacterManager.humanoid
- -- 1. Dessentar se estiver sentado
- if hum and hum.Sit then
- hum.Sit = false
- hum:ChangeState(Enum.HumanoidStateType.GettingUp)
- task.wait(0.15)
- end
- -- 2. Salva estados originais
- local originalAnchored = root.Anchored
- -- 3. Ancora para evitar interferência da física
- root.Anchored = true
- -- 4. Zera velocidades
- root.Velocity = Vector3.zero
- root.RotVelocity = Vector3.zero
- root.AssemblyLinearVelocity = Vector3.zero
- root.AssemblyAngularVelocity = Vector3.zero
- -- 5. Aplica CFrame com retry
- local success = false
- for attempt = 1, self.MAX_RETRIES do
- root.CFrame = targetCFrame
- -- Espera frames de física para estabilizar
- for _ = 1, self.STABILIZE_FRAMES do
- RunService.Heartbeat:Wait()
- end
- -- Verifica se a posição foi aplicada (tolerância de 5 studs)
- local distance = (root.Position - targetCFrame.Position).Magnitude
- if distance < 5 then
- success = true
- break
- end
- -- Retry: limpa velocidade novamente
- root.Velocity = Vector3.zero
- root.RotVelocity = Vector3.zero
- root.AssemblyLinearVelocity = Vector3.zero
- root.AssemblyAngularVelocity = Vector3.zero
- if attempt < self.MAX_RETRIES then
- task.wait(self.RETRY_DELAY)
- end
- end
- -- 6. Restaura ancoragem
- root.Anchored = originalAnchored
- -- 7. Zera velocidade final
- root.Velocity = Vector3.zero
- root.RotVelocity = Vector3.zero
- root.AssemblyLinearVelocity = Vector3.zero
- root.AssemblyAngularVelocity = Vector3.zero
- return success, success and "Teleportado" or "Falha após tentativas"
- end
- ----------------------------------------------------------------
- -- SEÇÃO 7 · STATE MANAGER (Estado centralizado)
- ----------------------------------------------------------------
- local State = {
- positions = {}, -- {[slot] = {position, name, timestamp}}
- mainPosition = nil, -- {position, name, timestamp}
- currentPage = 1,
- currentSlot = 1,
- maxSaves = 50,
- slotsPerPage = 10,
- maxPages = 5,
- _listeners = {},
- }
- function State:GetPageRange()
- local startSlot = (self.currentPage - 1) * self.slotsPerPage + 1
- local endSlot = self.currentPage * self.slotsPerPage
- return startSlot, endSlot
- end
- function State:FindNextEmptySlot()
- for i = 1, self.maxSaves do
- if not self.positions[i] then return i end
- end
- return (self.currentSlot % self.maxSaves) + 1
- end
- function State:GetSlotCount()
- local count = 0
- for _ in pairs(self.positions) do count = count + 1 end
- return count
- end
- function State:GetPageSlotCount(page)
- local count = 0
- local s = (page - 1) * self.slotsPerPage + 1
- local e = page * self.slotsPerPage
- for i = s, e do
- if self.positions[i] then count = count + 1 end
- end
- return count
- end
- function State:SavePosition(slot, cframe, name)
- if not CFrameCodec.IsValid(cframe) then return false end
- if slot < 1 or slot > self.maxSaves then return false end
- self.positions[slot] = {
- position = cframe,
- name = (#(name or "") > 0) and name or ("Slot " .. slot),
- timestamp = os.date("%Y-%m-%d %H:%M:%S"),
- }
- self:_PersistAll()
- self:_Notify("positions_changed")
- return true
- end
- function State:DeletePosition(slot)
- if not self.positions[slot] then return false end
- -- Se era a posição principal, remove também
- if self.mainPosition and self.positions[slot] then
- local mainCF = self.mainPosition.position
- local slotCF = self.positions[slot].position
- if mainCF and slotCF and mainCF == slotCF then
- self.mainPosition = nil
- Persistence:DeleteMainPosition()
- self:_Notify("main_changed")
- end
- end
- self.positions[slot] = nil
- self:_PersistAll()
- self:_Notify("positions_changed")
- return true
- end
- function State:RenamePosition(slot, newName)
- if not self.positions[slot] or #newName == 0 then return false end
- self.positions[slot].name = newName
- -- Atualiza nome na posição principal se for a mesma
- if self.mainPosition then
- local mainCF = self.mainPosition.position
- local slotCF = self.positions[slot].position
- if mainCF and slotCF and mainCF == slotCF then
- self.mainPosition.name = newName
- Persistence:SaveMainPosition(self.mainPosition)
- end
- end
- self:_PersistAll()
- self:_Notify("positions_changed")
- return true
- end
- function State:SwapPositions(fromSlot, toSlot)
- if fromSlot == toSlot then return false end
- if fromSlot < 1 or fromSlot > self.maxSaves then return false end
- if toSlot < 1 or toSlot > self.maxSaves then return false end
- local temp = self.positions[fromSlot]
- self.positions[fromSlot] = self.positions[toSlot]
- self.positions[toSlot] = temp
- self:_PersistAll()
- self:_Notify("positions_changed")
- return true
- end
- function State:SetMainPosition(slot)
- if not self.positions[slot] then return false end
- self.mainPosition = {
- position = self.positions[slot].position,
- name = self.positions[slot].name,
- timestamp = self.positions[slot].timestamp,
- }
- Persistence:SaveMainPosition(self.mainPosition)
- self:_Notify("main_changed")
- return true
- end
- function State:ClearMainPosition()
- self.mainPosition = nil
- Persistence:DeleteMainPosition()
- self:_Notify("main_changed")
- end
- function State:IsMainPosition(slot)
- if not self.mainPosition or not self.positions[slot] then return false end
- return self.mainPosition.position == self.positions[slot].position
- end
- function State:SetPage(page)
- page = math.clamp(page, 1, self.maxPages)
- if page ~= self.currentPage then
- self.currentPage = page
- self:_Notify("page_changed")
- end
- end
- function State:NextPage()
- local newPage = self.currentPage + 1
- if newPage > self.maxPages then newPage = 1 end
- self:SetPage(newPage)
- end
- function State:PrevPage()
- local newPage = self.currentPage - 1
- if newPage < 1 then newPage = self.maxPages end
- self:SetPage(newPage)
- end
- function State:UpdateSlot()
- self.currentSlot = self:FindNextEmptySlot()
- self:_Notify("slot_changed")
- end
- function State:NextSlot()
- self.currentSlot = (self.currentSlot % self.maxSaves) + 1
- self:_Notify("slot_changed")
- end
- function State:PrevSlot()
- self.currentSlot = ((self.currentSlot - 2 + self.maxSaves) % self.maxSaves) + 1
- self:_Notify("slot_changed")
- end
- function State:_PersistAll()
- task.defer(function()
- Persistence:SavePositions(self.positions)
- end)
- end
- function State:_Notify(event)
- for _, callback in ipairs(self._listeners) do
- task.defer(callback, event)
- end
- end
- function State:OnChange(callback)
- table.insert(self._listeners, callback)
- end
- function State:Load()
- self.positions = Persistence:LoadPositions()
- self.mainPosition = Persistence:LoadMainPosition()
- self:UpdateSlot()
- end
- ----------------------------------------------------------------
- -- SEÇÃO 8 · UI FACTORY (Componentes reutilizáveis)
- ----------------------------------------------------------------
- local Theme = {
- bg_primary = Color3.fromRGB(18, 16, 26),
- bg_secondary = Color3.fromRGB(28, 25, 40),
- bg_tertiary = Color3.fromRGB(38, 35, 52),
- bg_input = Color3.fromRGB(25, 22, 36),
- accent = Color3.fromRGB(110, 85, 255),
- accent_hover = Color3.fromRGB(135, 110, 255),
- accent_light = Color3.fromRGB(150, 130, 255),
- success = Color3.fromRGB(55, 180, 110),
- success_hover = Color3.fromRGB(75, 210, 135),
- danger = Color3.fromRGB(220, 75, 75),
- danger_hover = Color3.fromRGB(240, 100, 100),
- warning = Color3.fromRGB(255, 200, 60),
- text_primary = Color3.fromRGB(240, 240, 255),
- text_secondary = Color3.fromRGB(170, 170, 195),
- text_muted = Color3.fromRGB(110, 108, 135),
- stroke = Color3.fromRGB(100, 80, 200),
- stroke_dim = Color3.fromRGB(60, 55, 85),
- star_gold = Color3.fromRGB(255, 210, 50),
- corner_radius = UDim.new(0, 7),
- corner_round = UDim.new(0.5, 0),
- }
- local UI = {}
- function UI.Corner(parent, radius)
- local c = Instance.new("UICorner")
- c.CornerRadius = radius or Theme.corner_radius
- c.Parent = parent
- return c
- end
- function UI.Stroke(parent, color, thickness)
- local s = Instance.new("UIStroke")
- s.Color = color or Theme.stroke
- s.Thickness = thickness or 1.2
- s.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
- s.Parent = parent
- return s
- end
- function UI.Padding(parent, top, right, bottom, left)
- local p = Instance.new("UIPadding")
- p.PaddingTop = UDim.new(0, top or 0)
- p.PaddingRight = UDim.new(0, right or 0)
- p.PaddingBottom = UDim.new(0, bottom or 0)
- p.PaddingLeft = UDim.new(0, left or 0)
- p.Parent = parent
- return p
- end
- function UI.Tween(instance, props, duration, style, direction)
- return TweenService:Create(
- instance,
- TweenInfo.new(
- duration or 0.18,
- style or Enum.EasingStyle.Quad,
- direction or Enum.EasingDirection.Out
- ),
- props
- )
- end
- function UI.HoverEffect(button, hoverColor, normalColor, props)
- props = props or {}
- button.MouseEnter:Connect(function()
- UI.Tween(button, {BackgroundColor3 = hoverColor}, 0.15):Play()
- if props.scale then
- UI.Tween(button, {Size = props.scaleUp}, 0.15):Play()
- end
- end)
- button.MouseLeave:Connect(function()
- UI.Tween(button, {BackgroundColor3 = normalColor}, 0.15):Play()
- if props.scale then
- UI.Tween(button, {Size = props.scaleDown}, 0.15):Play()
- end
- end)
- end
- function UI.CreateButton(config)
- local btn = Instance.new("TextButton")
- btn.Name = config.name or "Button"
- btn.Text = config.text or ""
- btn.Size = config.size or UDim2.new(0, 100, 0, 36)
- btn.Position = config.position or UDim2.new(0, 0, 0, 0)
- btn.BackgroundColor3 = config.color or Theme.accent
- btn.Font = config.font or Enum.Font.GothamSemibold
- btn.TextSize = config.textSize or 14
- btn.TextColor3 = config.textColor or Theme.text_primary
- btn.AutoButtonColor = false
- btn.BorderSizePixel = 0
- btn.Parent = config.parent
- UI.Corner(btn, config.cornerRadius)
- if config.hoverColor then
- UI.HoverEffect(btn, config.hoverColor, config.color or Theme.accent)
- end
- if config.stroke then
- UI.Stroke(btn, config.strokeColor, config.strokeThickness)
- end
- return btn
- end
- function UI.CreateLabel(config)
- local label = Instance.new("TextLabel")
- label.Name = config.name or "Label"
- label.Text = config.text or ""
- label.Size = config.size or UDim2.new(1, 0, 0, 20)
- label.Position = config.position or UDim2.new(0, 0, 0, 0)
- label.BackgroundTransparency = 1
- label.Font = config.font or Enum.Font.Gotham
- label.TextSize = config.textSize or 14
- label.TextColor3 = config.textColor or Theme.text_primary
- label.TextXAlignment = config.alignX or Enum.TextXAlignment.Left
- label.TextTruncate = config.truncate or Enum.TextTruncate.None
- label.Parent = config.parent
- return label
- end
- function UI.CreateInput(config)
- local input = Instance.new("TextBox")
- input.Name = config.name or "Input"
- input.Text = config.text or ""
- input.PlaceholderText = config.placeholder or ""
- input.Size = config.size or UDim2.new(1, 0, 0, 38)
- input.Position = config.position or UDim2.new(0, 0, 0, 0)
- input.BackgroundColor3 = config.color or Theme.bg_input
- input.TextColor3 = config.textColor or Theme.text_primary
- input.PlaceholderColor3 = config.placeholderColor or Theme.text_muted
- input.Font = config.font or Enum.Font.Gotham
- input.TextSize = config.textSize or 14
- input.ClearTextOnFocus = config.clearOnFocus or false
- input.BorderSizePixel = 0
- input.Parent = config.parent
- UI.Corner(input, config.cornerRadius)
- UI.Stroke(input, Theme.stroke_dim, 1)
- -- Focus effect
- input.Focused:Connect(function()
- local s = input:FindFirstChildWhichIsA("UIStroke")
- if s then UI.Tween(s, {Color = Theme.accent}, 0.2):Play() end
- end)
- input.FocusLost:Connect(function()
- local s = input:FindFirstChildWhichIsA("UIStroke")
- if s then UI.Tween(s, {Color = Theme.stroke_dim}, 0.2):Play() end
- end)
- return input
- end
- function UI.CreateFrame(config)
- local frame = Instance.new("Frame")
- frame.Name = config.name or "Frame"
- frame.Size = config.size or UDim2.new(1, 0, 0, 50)
- frame.Position = config.position or UDim2.new(0, 0, 0, 0)
- frame.BackgroundColor3 = config.color or Theme.bg_secondary
- frame.BackgroundTransparency = config.transparency or 0
- frame.BorderSizePixel = 0
- frame.ClipsDescendants = config.clip ~= false
- frame.Parent = config.parent
- if config.corner ~= false then
- UI.Corner(frame, config.cornerRadius)
- end
- return frame
- end
- ----------------------------------------------------------------
- -- SEÇÃO 9 · OVERLAY SYSTEM (Modais reutilizáveis)
- ----------------------------------------------------------------
- local OverlaySystem = {
- _activeOverlay = nil,
- }
- function OverlaySystem:CreateOverlay(parent, config)
- local overlay = UI.CreateFrame({
- name = config.name or "Overlay",
- size = UDim2.new(1, 0, 1, 0),
- color = Color3.new(0, 0, 0),
- transparency = 0.55,
- parent = parent,
- corner = false,
- })
- overlay.ZIndex = config.zIndex or 10
- overlay.Visible = false
- local dialog = UI.CreateFrame({
- name = "Dialog",
- size = config.dialogSize or UDim2.new(0, 320, 0, 140),
- position = UDim2.new(0.5, 0, 0.5, 0),
- color = Theme.bg_secondary,
- parent = overlay,
- })
- dialog.AnchorPoint = Vector2.new(0.5, 0.5)
- dialog.ZIndex = (config.zIndex or 10) + 1
- UI.Stroke(dialog, Theme.accent, 1.5)
- return overlay, dialog
- end
- function OverlaySystem:Show(overlay)
- if self._activeOverlay and self._activeOverlay ~= overlay then
- self._activeOverlay.Visible = false
- end
- overlay.Visible = true
- self._activeOverlay = overlay
- end
- function OverlaySystem:Hide(overlay)
- overlay.Visible = false
- if self._activeOverlay == overlay then
- self._activeOverlay = nil
- end
- end
- ----------------------------------------------------------------
- -- SEÇÃO 10 · CONSTRUÇÃO PRINCIPAL DA UI
- ----------------------------------------------------------------
- local Connections = {} -- Para cleanup
- -- ScreenGui
- local ScreenGui = Instance.new("ScreenGui")
- ScreenGui.Name = "SaveLocateNickZV5"
- ScreenGui.ResetOnSpawn = false
- ScreenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
- ScreenGui.DisplayOrder = 100
- ScreenGui.Parent = CoreGui
- -- ===== FRAME PRINCIPAL =====
- local MainFrame = UI.CreateFrame({
- name = "MainFrame",
- size = UDim2.new(0, 620, 0, 480),
- position = UDim2.new(0.5, 0, 0.5, 0),
- color = Theme.bg_primary,
- transparency = 0.05,
- parent = ScreenGui,
- })
- MainFrame.AnchorPoint = Vector2.new(0.5, 0.5)
- MainFrame.Active = true
- MainFrame.Draggable = true
- UI.Stroke(MainFrame, Theme.accent, 1.5)
- -- Gradiente de fundo
- local bgGradient = Instance.new("UIGradient", MainFrame)
- bgGradient.Color = ColorSequence.new({
- ColorSequenceKeypoint.new(0, Color3.fromRGB(22, 19, 34)),
- ColorSequenceKeypoint.new(1, Color3.fromRGB(38, 34, 58)),
- })
- bgGradient.Rotation = 135
- -- ===== TOP BAR =====
- local TopBar = UI.CreateFrame({
- name = "TopBar",
- size = UDim2.new(1, 0, 0, 42),
- color = Theme.bg_primary,
- transparency = 0.8,
- parent = MainFrame,
- corner = false,
- })
- local TitleLabel = UI.CreateLabel({
- name = "Title",
- text = "✦ Save Locate (NickZ) · V5",
- size = UDim2.new(1, -50, 1, 0),
- position = UDim2.new(0, 16, 0, 0),
- font = Enum.Font.GothamBold,
- textSize = 17,
- textColor = Theme.text_primary,
- parent = TopBar,
- })
- -- Badge de versão
- local VersionBadge = UI.CreateLabel({
- name = "VersionBadge",
- text = Environment.executorName,
- size = UDim2.new(0, 80, 0, 18),
- position = UDim2.new(1, -130, 0.5, -9),
- font = Enum.Font.GothamBold,
- textSize = 9,
- textColor = Theme.accent_light,
- alignX = Enum.TextXAlignment.Center,
- parent = TopBar,
- })
- local vBadgeBg = UI.CreateFrame({
- name = "BadgeBG",
- size = UDim2.new(0, 80, 0, 18),
- position = UDim2.new(1, -130, 0.5, -9),
- color = Theme.accent,
- transparency = 0.85,
- parent = TopBar,
- })
- vBadgeBg.ZIndex = 0
- UI.Corner(vBadgeBg, UDim.new(0, 4))
- -- Botão Fechar
- local CloseBtn = UI.CreateButton({
- name = "Close",
- text = "✕",
- size = UDim2.new(0, 32, 0, 32),
- position = UDim2.new(1, -40, 0.5, -16),
- color = Theme.bg_tertiary,
- hoverColor = Theme.danger,
- textColor = Theme.danger,
- textSize = 15,
- font = Enum.Font.GothamBold,
- parent = TopBar,
- })
- -- Linha separadora
- local Separator1 = UI.CreateFrame({
- name = "Sep1",
- size = UDim2.new(1, -24, 0, 1),
- position = UDim2.new(0, 12, 0, 42),
- color = Theme.stroke_dim,
- transparency = 0.5,
- parent = MainFrame,
- corner = false,
- })
- -- ===== CONTROLES (Salvar + Slot + Nome) =====
- local ControlsFrame = UI.CreateFrame({
- name = "Controls",
- size = UDim2.new(1, -24, 0, 88),
- position = UDim2.new(0, 12, 0, 48),
- transparency = 1,
- parent = MainFrame,
- })
- -- Linha superior: Botão Salvar + Seletor de Slot
- local SaveButton = UI.CreateButton({
- name = "SaveBtn",
- text = "💾 Salvar Posição",
- size = UDim2.new(0.52, -4, 0, 40),
- position = UDim2.new(0, 0, 0, 0),
- color = Theme.success,
- hoverColor = Theme.success_hover,
- font = Enum.Font.GothamBold,
- textSize = 15,
- parent = ControlsFrame,
- })
- -- Slot selector
- local SlotFrame = UI.CreateFrame({
- name = "SlotFrame",
- size = UDim2.new(0.48, -4, 0, 40),
- position = UDim2.new(0.52, 4, 0, 0),
- color = Theme.bg_secondary,
- parent = ControlsFrame,
- })
- UI.Stroke(SlotFrame, Theme.stroke_dim, 1)
- local SlotLabel = UI.CreateLabel({
- name = "SlotLabel",
- text = "Slot: 1",
- size = UDim2.new(1, -80, 1, 0),
- position = UDim2.new(0, 12, 0, 0),
- font = Enum.Font.GothamSemibold,
- textSize = 14,
- textColor = Theme.text_primary,
- parent = SlotFrame,
- })
- local SlotPrev = UI.CreateButton({
- name = "SlotPrev",
- text = "◂",
- size = UDim2.new(0, 34, 0, 30),
- position = UDim2.new(1, -74, 0.5, -15),
- color = Theme.bg_tertiary,
- hoverColor = Theme.accent,
- font = Enum.Font.GothamBold,
- textSize = 16,
- parent = SlotFrame,
- })
- local SlotNext = UI.CreateButton({
- name = "SlotNext",
- text = "▸",
- size = UDim2.new(0, 34, 0, 30),
- position = UDim2.new(1, -38, 0.5, -15),
- color = Theme.bg_tertiary,
- hoverColor = Theme.accent,
- font = Enum.Font.GothamBold,
- textSize = 16,
- parent = SlotFrame,
- })
- -- Nome da posição
- local NameField = UI.CreateInput({
- name = "NameField",
- placeholder = "Nome da posição (opcional)...",
- size = UDim2.new(1, 0, 0, 38),
- position = UDim2.new(0, 0, 0, 48),
- parent = ControlsFrame,
- })
- -- Linha separadora 2
- local Separator2 = UI.CreateFrame({
- name = "Sep2",
- size = UDim2.new(1, -24, 0, 1),
- position = UDim2.new(0, 12, 0, 140),
- color = Theme.stroke_dim,
- transparency = 0.5,
- parent = MainFrame,
- corner = false,
- })
- -- ===== PAGINAÇÃO =====
- local PageFrame = UI.CreateFrame({
- name = "PageFrame",
- size = UDim2.new(1, -24, 0, 32),
- position = UDim2.new(0, 12, 0, 146),
- transparency = 1,
- parent = MainFrame,
- })
- local PagePrev = UI.CreateButton({
- name = "PagePrev",
- text = "◄ Anterior",
- size = UDim2.new(0, 100, 0, 28),
- position = UDim2.new(0, 0, 0, 2),
- color = Theme.bg_tertiary,
- hoverColor = Theme.accent,
- font = Enum.Font.GothamSemibold,
- textSize = 11,
- parent = PageFrame,
- })
- local PageLabel = UI.CreateLabel({
- name = "PageLabel",
- text = "Página 1 / 5",
- size = UDim2.new(1, -220, 1, 0),
- position = UDim2.new(0, 110, 0, 0),
- font = Enum.Font.GothamBold,
- textSize = 15,
- textColor = Theme.text_primary,
- alignX = Enum.TextXAlignment.Center,
- parent = PageFrame,
- })
- local PageCounter = UI.CreateLabel({
- name = "PageCounter",
- text = "0 / 10 slots",
- size = UDim2.new(0, 80, 1, 0),
- position = UDim2.new(1, -190, 0, 0),
- font = Enum.Font.Gotham,
- textSize = 11,
- textColor = Theme.text_muted,
- alignX = Enum.TextXAlignment.Center,
- parent = PageFrame,
- })
- local PageNext = UI.CreateButton({
- name = "PageNext",
- text = "Próxima ►",
- size = UDim2.new(0, 100, 0, 28),
- position = UDim2.new(1, -100, 0, 2),
- color = Theme.bg_tertiary,
- hoverColor = Theme.accent,
- font = Enum.Font.GothamSemibold,
- textSize = 11,
- parent = PageFrame,
- })
- -- Indicadores de página (dots)
- local DotsFrame = UI.CreateFrame({
- name = "Dots",
- size = UDim2.new(0, 80, 0, 8),
- position = UDim2.new(0.5, -40, 1, -3),
- transparency = 1,
- parent = PageFrame,
- corner = false,
- })
- local dotInstances = {}
- for i = 1, State.maxPages do
- local dot = UI.CreateFrame({
- name = "Dot" .. i,
- size = UDim2.new(0, 8, 0, 8),
- position = UDim2.new(0, (i - 1) * 16, 0, 0),
- color = i == 1 and Theme.accent or Theme.stroke_dim,
- parent = DotsFrame,
- cornerRadius = Theme.corner_round,
- })
- dotInstances[i] = dot
- end
- -- ===== CONTAINER DE SLOTS =====
- local SlotsContainer = UI.CreateFrame({
- name = "SlotsContainer",
- size = UDim2.new(1, -24, 1, -195),
- position = UDim2.new(0, 12, 0, 185),
- transparency = 1,
- parent = MainFrame,
- corner = false,
- })
- local GridLayout = Instance.new("UIGridLayout", SlotsContainer)
- GridLayout.CellPadding = UDim2.new(0, 8, 0, 6)
- GridLayout.CellSize = UDim2.new(0.5, -4, 0, 50)
- GridLayout.FillDirection = Enum.FillDirection.Vertical
- GridLayout.SortOrder = Enum.SortOrder.LayoutOrder
- GridLayout.StartCorner = Enum.StartCorner.TopLeft
- -- ===== BOTÃO ABRIR (quando minimizado) =====
- local OpenBtn = UI.CreateButton({
- name = "OpenBtn",
- text = "✦",
- size = UDim2.new(0, 48, 0, 48),
- position = UDim2.new(0, 15, 0.5, -24),
- color = Theme.bg_primary,
- hoverColor = Theme.accent,
- font = Enum.Font.GothamBold,
- textSize = 24,
- textColor = Theme.accent,
- stroke = true,
- strokeColor = Theme.accent,
- parent = ScreenGui,
- })
- OpenBtn.Visible = false
- -- ===== BOTÃO POSIÇÃO PRINCIPAL (Flutuante) =====
- local MainPosContainer = UI.CreateFrame({
- name = "MainPosContainer",
- size = UDim2.new(0, 52, 0, 52),
- position = UDim2.new(1, -62, 0, 12),
- transparency = 1,
- parent = ScreenGui,
- corner = false,
- })
- MainPosContainer.Visible = false
- MainPosContainer.Active = true
- MainPosContainer.Draggable = true
- local MainPosBtn = UI.CreateButton({
- name = "MainPosBtn",
- text = "🚀",
- size = UDim2.new(1, 0, 1, 0),
- color = Theme.bg_primary,
- font = Enum.Font.GothamBold,
- textSize = 22,
- parent = MainPosContainer,
- cornerRadius = Theme.corner_round,
- stroke = true,
- strokeColor = Theme.accent,
- strokeThickness = 2,
- })
- -- Tooltip
- local MainPosTooltip = UI.CreateLabel({
- name = "Tooltip",
- text = "",
- size = UDim2.new(0, 160, 0, 28),
- position = UDim2.new(-3.4, 0, 0.5, -14),
- font = Enum.Font.GothamSemibold,
- textSize = 11,
- textColor = Theme.text_primary,
- alignX = Enum.TextXAlignment.Center,
- parent = MainPosBtn,
- })
- MainPosTooltip.BackgroundColor3 = Theme.bg_primary
- MainPosTooltip.BackgroundTransparency = 0.08
- MainPosTooltip.Visible = false
- MainPosTooltip.ZIndex = 5
- UI.Corner(MainPosTooltip, UDim.new(0, 5))
- UI.Stroke(MainPosTooltip, Theme.accent, 1)
- -- Botão remover posição principal
- local MainPosRemove = UI.CreateButton({
- name = "RemoveMainPos",
- text = "×",
- size = UDim2.new(0, 20, 0, 20),
- position = UDim2.new(1, -3, 0, -3),
- color = Theme.danger,
- hoverColor = Theme.danger_hover,
- font = Enum.Font.GothamBold,
- textSize = 14,
- parent = MainPosBtn,
- cornerRadius = Theme.corner_round,
- })
- MainPosRemove.Visible = false
- MainPosRemove.ZIndex = 6
- ----------------------------------------------------------------
- -- SEÇÃO 11 · OVERLAYS (Confirmação, Renomear, Mover)
- ----------------------------------------------------------------
- -- ===== OVERLAY DE CONFIRMAÇÃO =====
- local ConfirmOverlay, ConfirmDialog = OverlaySystem:CreateOverlay(MainFrame, {
- name = "ConfirmOverlay",
- zIndex = 20,
- dialogSize = UDim2.new(0, 320, 0, 135),
- })
- local ConfirmLabel = UI.CreateLabel({
- name = "Message",
- text = "",
- size = UDim2.new(1, -24, 0, 60),
- position = UDim2.new(0, 12, 0, 8),
- font = Enum.Font.GothamSemibold,
- textSize = 15,
- textColor = Theme.text_primary,
- parent = ConfirmDialog,
- })
- ConfirmLabel.TextWrapped = true
- ConfirmLabel.ZIndex = 21
- local ConfirmYes = UI.CreateButton({
- name = "Yes",
- text = "Sim",
- size = UDim2.new(0.5, -16, 0, 38),
- position = UDim2.new(0, 10, 1, -48),
- color = Theme.danger,
- hoverColor = Theme.danger_hover,
- font = Enum.Font.GothamBold,
- parent = ConfirmDialog,
- })
- ConfirmYes.ZIndex = 21
- local ConfirmNo = UI.CreateButton({
- name = "No",
- text = "Não",
- size = UDim2.new(0.5, -16, 0, 38),
- position = UDim2.new(0.5, 6, 1, -48),
- color = Theme.bg_tertiary,
- hoverColor = Theme.accent,
- font = Enum.Font.GothamBold,
- parent = ConfirmDialog,
- })
- ConfirmNo.ZIndex = 21
- -- ===== OVERLAY DE RENOMEAR =====
- local RenameOverlay, RenameDialog = OverlaySystem:CreateOverlay(MainFrame, {
- name = "RenameOverlay",
- zIndex = 25,
- dialogSize = UDim2.new(0, 360, 0, 170),
- })
- local RenameTitle = UI.CreateLabel({
- name = "Title",
- text = "Renomear Posição",
- size = UDim2.new(1, -24, 0, 36),
- position = UDim2.new(0, 12, 0, 5),
- font = Enum.Font.GothamBold,
- textSize = 16,
- textColor = Theme.text_primary,
- parent = RenameDialog,
- })
- RenameTitle.ZIndex = 26
- local RenameField = UI.CreateInput({
- name = "RenameInput",
- placeholder = "Novo nome...",
- size = UDim2.new(1, -24, 0, 40),
- position = UDim2.new(0, 12, 0, 44),
- parent = RenameDialog,
- })
- RenameField.ZIndex = 26
- local RenameConfirm = UI.CreateButton({
- name = "Confirm",
- text = "✓ Confirmar",
- size = UDim2.new(0.5, -16, 0, 38),
- position = UDim2.new(0, 10, 1, -48),
- color = Theme.accent,
- hoverColor = Theme.accent_hover,
- font = Enum.Font.GothamBold,
- parent = RenameDialog,
- })
- RenameConfirm.ZIndex = 26
- local RenameCancel = UI.CreateButton({
- name = "Cancel",
- text = "Cancelar",
- size = UDim2.new(0.5, -16, 0, 38),
- position = UDim2.new(0.5, 6, 1, -48),
- color = Theme.bg_tertiary,
- hoverColor = Theme.accent,
- font = Enum.Font.GothamBold,
- parent = RenameDialog,
- })
- RenameCancel.ZIndex = 26
- -- ===== OVERLAY DE MOVER =====
- local MoveOverlay, MoveDialog = OverlaySystem:CreateOverlay(MainFrame, {
- name = "MoveOverlay",
- zIndex = 30,
- dialogSize = UDim2.new(0, 320, 0, 190),
- })
- local MoveTitle = UI.CreateLabel({
- name = "Title",
- text = "Mover Posição",
- size = UDim2.new(1, -24, 0, 36),
- position = UDim2.new(0, 12, 0, 5),
- font = Enum.Font.GothamBold,
- textSize = 16,
- textColor = Theme.text_primary,
- parent = MoveDialog,
- })
- MoveTitle.ZIndex = 31
- local MoveInfo = UI.CreateLabel({
- name = "Info",
- text = "Slot de destino (1-50):",
- size = UDim2.new(1, -24, 0, 22),
- position = UDim2.new(0, 12, 0, 42),
- font = Enum.Font.Gotham,
- textSize = 13,
- textColor = Theme.text_secondary,
- parent = MoveDialog,
- })
- MoveInfo.ZIndex = 31
- local MoveInput = UI.CreateInput({
- name = "MoveInput",
- placeholder = "1-50",
- size = UDim2.new(0, 100, 0, 42),
- position = UDim2.new(0.5, -50, 0, 68),
- parent = MoveDialog,
- textSize = 20,
- font = Enum.Font.GothamBold,
- })
- MoveInput.ZIndex = 31
- MoveInput.TextXAlignment = Enum.TextXAlignment.Center
- local MoveConfirm = UI.CreateButton({
- name = "Confirm",
- text = "✓ Mover",
- size = UDim2.new(0.5, -16, 0, 38),
- position = UDim2.new(0, 10, 1, -48),
- color = Theme.accent,
- hoverColor = Theme.accent_hover,
- font = Enum.Font.GothamBold,
- parent = MoveDialog,
- })
- MoveConfirm.ZIndex = 31
- local MoveCancel = UI.CreateButton({
- name = "Cancel",
- text = "Cancelar",
- size = UDim2.new(0.5, -16, 0, 38),
- position = UDim2.new(0.5, 6, 1, -48),
- color = Theme.bg_tertiary,
- hoverColor = Theme.accent,
- font = Enum.Font.GothamBold,
- parent = MoveDialog,
- })
- MoveCancel.ZIndex = 31
- ----------------------------------------------------------------
- -- SEÇÃO 12 · RENDERIZAÇÃO DOS SLOTS
- ----------------------------------------------------------------
- local activeMenuFrame = nil -- Referência ao menu de contexto aberto
- local activeMenuConnection = nil
- local function CloseActiveMenu()
- if activeMenuFrame then
- activeMenuFrame.Visible = false
- activeMenuFrame = nil
- end
- if activeMenuConnection then
- activeMenuConnection:Disconnect()
- activeMenuConnection = nil
- end
- end
- local function RenderSlots()
- -- Fecha menu de contexto aberto
- CloseActiveMenu()
- -- Limpa slots existentes (apenas Frames do container)
- for _, child in ipairs(SlotsContainer:GetChildren()) do
- if child:IsA("Frame") then child:Destroy() end
- end
- -- Remove menus de contexto órfãos
- for _, child in ipairs(ScreenGui:GetChildren()) do
- if child.Name:match("^CtxMenu_") then child:Destroy() end
- end
- local startSlot, endSlot = State:GetPageRange()
- -- Atualiza contador
- local pageCount = State:GetPageSlotCount(State.currentPage)
- PageCounter.Text = pageCount .. " / " .. State.slotsPerPage .. " slots"
- for slot = startSlot, endSlot do
- local data = State.positions[slot]
- local slotFrame = UI.CreateFrame({
- name = "Slot_" .. slot,
- size = UDim2.new(0.5, -4, 0, 50),
- color = Theme.bg_tertiary,
- transparency = data and 0.15 or 0.55,
- parent = SlotsContainer,
- })
- slotFrame.LayoutOrder = slot
- if data then
- -- === SLOT COM DADOS ===
- local isMain = State:IsMainPosition(slot)
- -- Coluna esquerda: ações rápidas
- local actionsCol = UI.CreateFrame({
- name = "Actions",
- size = UDim2.new(0, 32, 1, -6),
- position = UDim2.new(0, 4, 0, 3),
- transparency = 1,
- parent = slotFrame,
- corner = false,
- })
- -- Estrela (definir como principal)
- local starBtn = UI.CreateButton({
- name = "Star",
- text = isMain and "★" or "☆",
- size = UDim2.new(1, 0, 0, 20),
- position = UDim2.new(0, 0, 0, 0),
- color = Theme.bg_tertiary,
- textColor = isMain and Theme.star_gold or Theme.text_muted,
- font = Enum.Font.GothamBold,
- textSize = 14,
- parent = actionsCol,
- cornerRadius = UDim.new(0, 4),
- })
- -- Menu (⋮)
- local menuBtn = UI.CreateButton({
- name = "Menu",
- text = "⋮",
- size = UDim2.new(1, 0, 0, 20),
- position = UDim2.new(0, 0, 0, 23),
- color = Theme.bg_tertiary,
- textColor = Theme.text_muted,
- font = Enum.Font.GothamBold,
- textSize = 18,
- parent = actionsCol,
- cornerRadius = UDim.new(0, 4),
- })
- -- Nome
- local nameLabel = UI.CreateLabel({
- name = "Name",
- text = data.name,
- size = UDim2.new(1, -85, 0, 24),
- position = UDim2.new(0, 40, 0, 4),
- font = Enum.Font.GothamSemibold,
- textSize = 13,
- textColor = Theme.text_primary,
- truncate = Enum.TextTruncate.AtEnd,
- parent = slotFrame,
- })
- -- Timestamp
- local timeLabel = UI.CreateLabel({
- name = "Time",
- text = "📅 " .. (data.timestamp or ""),
- size = UDim2.new(1, -85, 0, 14),
- position = UDim2.new(0, 40, 0, 30),
- font = Enum.Font.Gotham,
- textSize = 10,
- textColor = Theme.text_muted,
- parent = slotFrame,
- })
- -- Botão TP
- local tpBtn = UI.CreateButton({
- name = "TP",
- text = "TP",
- size = UDim2.new(0, 38, 0, 38),
- position = UDim2.new(1, -44, 0.5, -19),
- color = Theme.accent,
- hoverColor = Theme.accent_hover,
- font = Enum.Font.GothamBold,
- textSize = 13,
- parent = slotFrame,
- })
- -- Indicador de posição principal
- if isMain then
- local mainIndicator = UI.CreateFrame({
- name = "MainIndicator",
- size = UDim2.new(0, 3, 1, -8),
- position = UDim2.new(0, 0, 0, 4),
- color = Theme.star_gold,
- parent = slotFrame,
- cornerRadius = UDim.new(0, 2),
- })
- end
- -- === EVENTOS DO SLOT ===
- -- TP
- tpBtn.MouseButton1Click:Connect(function()
- local success, msg = Teleporter:Execute(data.position)
- if success then
- tpBtn.Text = "✓"
- UI.Tween(tpBtn, {BackgroundColor3 = Theme.success}, 0.15):Play()
- task.delay(1.2, function()
- if tpBtn.Parent then
- tpBtn.Text = "TP"
- UI.Tween(tpBtn, {BackgroundColor3 = Theme.accent}, 0.15):Play()
- end
- end)
- else
- tpBtn.Text = "✕"
- UI.Tween(tpBtn, {BackgroundColor3 = Theme.danger}, 0.15):Play()
- task.delay(1.2, function()
- if tpBtn.Parent then
- tpBtn.Text = "TP"
- UI.Tween(tpBtn, {BackgroundColor3 = Theme.accent}, 0.15):Play()
- end
- end)
- end
- end)
- -- Estrela
- starBtn.MouseButton1Click:Connect(function()
- if State:IsMainPosition(slot) then
- ConfirmLabel.Text = "Remover como posição principal?"
- ConfirmYes.Text = "Remover"
- local _onConfirm
- _onConfirm = ConfirmYes.MouseButton1Click:Once(function()
- State:ClearMainPosition()
- RenderSlots()
- OverlaySystem:Hide(ConfirmOverlay)
- end)
- local _onCancel
- _onCancel = ConfirmNo.MouseButton1Click:Once(function()
- OverlaySystem:Hide(ConfirmOverlay)
- end)
- OverlaySystem:Show(ConfirmOverlay)
- else
- local msg = State.mainPosition
- and "Substituir posição principal atual?"
- or "Definir como posição principal?"
- ConfirmLabel.Text = msg
- ConfirmYes.Text = "Sim"
- ConfirmYes.MouseButton1Click:Once(function()
- State:SetMainPosition(slot)
- RenderSlots()
- OverlaySystem:Hide(ConfirmOverlay)
- end)
- ConfirmNo.MouseButton1Click:Once(function()
- OverlaySystem:Hide(ConfirmOverlay)
- end)
- OverlaySystem:Show(ConfirmOverlay)
- end
- end)
- -- Menu de contexto
- menuBtn.MouseButton1Click:Connect(function()
- CloseActiveMenu()
- -- Cria menu de contexto no ScreenGui
- local ctxMenu = UI.CreateFrame({
- name = "CtxMenu_" .. slot,
- size = UDim2.new(0, 130, 0, 100),
- color = Theme.bg_secondary,
- parent = ScreenGui,
- })
- ctxMenu.ZIndex = 50
- UI.Stroke(ctxMenu, Theme.accent, 1.2)
- -- Posiciona perto do botão
- local btnPos = menuBtn.AbsolutePosition
- ctxMenu.Position = UDim2.new(0, btnPos.X + 35, 0, btnPos.Y - 5)
- -- Botões do menu
- local ctxBtns = {
- {text = "✏️ Renomear", action = "rename", color = Theme.text_primary},
- {text = "↔️ Mover", action = "move", color = Theme.text_primary},
- {text = "🗑️ Excluir", action = "delete", color = Theme.danger},
- }
- for i, info in ipairs(ctxBtns) do
- local ctxBtn = UI.CreateButton({
- name = info.action,
- text = info.text,
- size = UDim2.new(1, -8, 0, 27),
- position = UDim2.new(0, 4, 0, 4 + (i - 1) * 31),
- color = Theme.bg_tertiary,
- hoverColor = Theme.accent,
- font = Enum.Font.Gotham,
- textSize = 12,
- textColor = info.color,
- parent = ctxMenu,
- })
- ctxBtn.ZIndex = 51
- ctxBtn.TextXAlignment = Enum.TextXAlignment.Left
- UI.Padding(ctxBtn, 0, 0, 0, 8)
- ctxBtn.MouseButton1Click:Connect(function()
- CloseActiveMenu()
- if info.action == "rename" then
- RenameTitle.Text = string.format("Renomear Slot %d", slot)
- RenameField.Text = data.name
- RenameConfirm.MouseButton1Click:Once(function()
- if #RenameField.Text > 0 then
- State:RenamePosition(slot, RenameField.Text)
- RenderSlots()
- end
- OverlaySystem:Hide(RenameOverlay)
- end)
- RenameCancel.MouseButton1Click:Once(function()
- OverlaySystem:Hide(RenameOverlay)
- end)
- OverlaySystem:Show(RenameOverlay)
- elseif info.action == "move" then
- MoveTitle.Text = string.format("Mover: %s (Slot %d)", data.name, slot)
- MoveInput.Text = ""
- MoveConfirm.MouseButton1Click:Once(function()
- local target = tonumber(MoveInput.Text)
- if target and target >= 1 and target <= State.maxSaves then
- State:SwapPositions(slot, target)
- -- Navega para a página do destino
- local targetPage = math.ceil(target / State.slotsPerPage)
- State:SetPage(targetPage)
- RenderSlots()
- else
- MoveInput.Text = ""
- MoveInput.PlaceholderText = "Inválido!"
- task.delay(1, function()
- if MoveInput.Parent then
- MoveInput.PlaceholderText = "1-50"
- end
- end)
- return
- end
- OverlaySystem:Hide(MoveOverlay)
- end)
- MoveCancel.MouseButton1Click:Once(function()
- OverlaySystem:Hide(MoveOverlay)
- end)
- OverlaySystem:Show(MoveOverlay)
- elseif info.action == "delete" then
- ConfirmLabel.Text = string.format(
- "Excluir \"%s\" (Slot %d)?",
- data.name, slot
- )
- ConfirmYes.Text = "Sim, Excluir"
- ConfirmYes.MouseButton1Click:Once(function()
- State:DeletePosition(slot)
- State:UpdateSlot()
- RenderSlots()
- OverlaySystem:Hide(ConfirmOverlay)
- end)
- ConfirmNo.MouseButton1Click:Once(function()
- OverlaySystem:Hide(ConfirmOverlay)
- end)
- OverlaySystem:Show(ConfirmOverlay)
- end
- end)
- end
- activeMenuFrame = ctxMenu
- -- Fecha ao clicar fora
- activeMenuConnection = UserInputService.InputBegan:Connect(function(input)
- if input.UserInputType == Enum.UserInputType.MouseButton1
- or input.UserInputType == Enum.UserInputType.Touch then
- task.defer(function()
- if not ctxMenu.Parent then return end
- local mouse = UserInputService:GetMouseLocation()
- local pos = ctxMenu.AbsolutePosition
- local size = ctxMenu.AbsoluteSize
- if mouse.X < pos.X or mouse.X > pos.X + size.X
- or mouse.Y < pos.Y or mouse.Y > pos.Y + size.Y then
- CloseActiveMenu()
- end
- end)
- end
- end)
- end)
- -- Hover no slot
- slotFrame.MouseEnter:Connect(function()
- UI.Tween(slotFrame, {BackgroundTransparency = 0.05}, 0.12):Play()
- end)
- slotFrame.MouseLeave:Connect(function()
- UI.Tween(slotFrame, {BackgroundTransparency = 0.15}, 0.12):Play()
- end)
- else
- -- === SLOT VAZIO ===
- local emptyLabel = UI.CreateLabel({
- name = "Empty",
- text = string.format(" Slot %d · Vazio", slot),
- size = UDim2.new(1, -12, 1, 0),
- position = UDim2.new(0, 6, 0, 0),
- font = Enum.Font.Gotham,
- textSize = 13,
- textColor = Theme.text_muted,
- parent = slotFrame,
- })
- end
- end
- end
- ----------------------------------------------------------------
- -- SEÇÃO 13 · ATUALIZAÇÃO DA UI DE POSIÇÃO PRINCIPAL
- ----------------------------------------------------------------
- local function UpdateMainPositionUI()
- if State.mainPosition then
- MainPosContainer.Visible = true
- MainPosTooltip.Text = State.mainPosition.name or "Principal"
- else
- MainPosContainer.Visible = false
- end
- end
- ----------------------------------------------------------------
- -- SEÇÃO 14 · ATUALIZAÇÃO DE PAGINAÇÃO
- ----------------------------------------------------------------
- local function UpdatePageUI()
- PageLabel.Text = string.format("Página %d / %d", State.currentPage, State.maxPages)
- -- Atualiza dots
- for i, dot in ipairs(dotInstances) do
- local targetColor = (i == State.currentPage) and Theme.accent or Theme.stroke_dim
- UI.Tween(dot, {BackgroundColor3 = targetColor}, 0.2):Play()
- end
- RenderSlots()
- end
- local function UpdateSlotUI()
- SlotLabel.Text = "Slot: " .. State.currentSlot
- end
- ----------------------------------------------------------------
- -- SEÇÃO 15 · CONEXÕES DE EVENTOS
- ----------------------------------------------------------------
- -- Fechar / Abrir
- CloseBtn.MouseButton1Click:Connect(function()
- UI.Tween(MainFrame, {
- Position = UDim2.new(0.5, 0, 0.5, 20),
- BackgroundTransparency = 1,
- }, 0.25):Play()
- task.delay(0.25, function()
- MainFrame.Visible = false
- MainFrame.Position = UDim2.new(0.5, 0, 0.5, 0)
- MainFrame.BackgroundTransparency = 0.05
- end)
- end)
- OpenBtn.MouseButton1Click:Connect(function()
- MainFrame.Visible = true
- MainFrame.BackgroundTransparency = 1
- MainFrame.Position = UDim2.new(0.5, 0, 0.5, -20)
- UI.Tween(MainFrame, {
- Position = UDim2.new(0.5, 0, 0.5, 0),
- BackgroundTransparency = 0.05,
- }, 0.25):Play()
- end)
- MainFrame:GetPropertyChangedSignal("Visible"):Connect(function()
- OpenBtn.Visible = not MainFrame.Visible
- end)
- -- Paginação
- PagePrev.MouseButton1Click:Connect(function() State:PrevPage() end)
- PageNext.MouseButton1Click:Connect(function() State:NextPage() end)
- -- Slot selector
- SlotPrev.MouseButton1Click:Connect(function() State:PrevSlot() end)
- SlotNext.MouseButton1Click:Connect(function() State:NextSlot() end)
- -- Salvar
- SaveButton.MouseButton1Click:Connect(function()
- local function doSave()
- if not CharacterManager:Refresh() then
- SaveButton.Text = "❌ Sem personagem"
- UI.Tween(SaveButton, {BackgroundColor3 = Theme.danger}, 0.15):Play()
- task.delay(1.5, function()
- if SaveButton.Parent then
- SaveButton.Text = "💾 Salvar Posição"
- UI.Tween(SaveButton, {BackgroundColor3 = Theme.success}, 0.15):Play()
- end
- end)
- return
- end
- local name = NameField.Text
- local success = State:SavePosition(
- State.currentSlot,
- CharacterManager.rootPart.CFrame,
- name
- )
- if success then
- -- Navega para a página do slot salvo
- local savedPage = math.ceil(State.currentSlot / State.slotsPerPage)
- State:SetPage(savedPage)
- State:UpdateSlot()
- SaveButton.Text = "✅ Salvo!"
- UI.Tween(SaveButton, {BackgroundColor3 = Color3.fromRGB(50, 200, 100)}):Play()
- NameField.Text = ""
- else
- SaveButton.Text = "❌ Erro!"
- UI.Tween(SaveButton, {BackgroundColor3 = Theme.danger}):Play()
- end
- task.delay(1.5, function()
- if SaveButton.Parent then
- SaveButton.Text = "💾 Salvar Posição"
- UI.Tween(SaveButton, {BackgroundColor3 = Theme.success}):Play()
- end
- end)
- end
- -- Se o slot já tem dados, pede confirmação
- if State.positions[State.currentSlot] then
- ConfirmLabel.Text = string.format(
- "O Slot %d já contém \"%s\".\nSubstituir?",
- State.currentSlot,
- State.positions[State.currentSlot].name
- )
- ConfirmYes.Text = "Substituir"
- ConfirmYes.MouseButton1Click:Once(function()
- OverlaySystem:Hide(ConfirmOverlay)
- doSave()
- end)
- ConfirmNo.MouseButton1Click:Once(function()
- OverlaySystem:Hide(ConfirmOverlay)
- end)
- OverlaySystem:Show(ConfirmOverlay)
- else
- doSave()
- end
- end)
- -- Posição Principal
- MainPosBtn.MouseButton1Click:Connect(function()
- if not State.mainPosition then return end
- local success = Teleporter:Execute(State.mainPosition.position)
- if success then
- MainPosBtn.Text = "✅"
- UI.Tween(MainPosBtn, {BackgroundColor3 = Theme.success}, 0.15):Play()
- else
- MainPosBtn.Text = "❌"
- UI.Tween(MainPosBtn, {BackgroundColor3 = Theme.danger}, 0.15):Play()
- end
- task.delay(1.2, function()
- if MainPosBtn.Parent then
- MainPosBtn.Text = "🚀"
- UI.Tween(MainPosBtn, {BackgroundColor3 = Theme.bg_primary}, 0.15):Play()
- end
- end)
- end)
- MainPosBtn.MouseEnter:Connect(function()
- if State.mainPosition then
- MainPosTooltip.Visible = true
- MainPosRemove.Visible = true
- end
- local stroke = MainPosBtn:FindFirstChildWhichIsA("UIStroke")
- if stroke then UI.Tween(stroke, {Thickness = 3}, 0.15):Play() end
- end)
- MainPosBtn.MouseLeave:Connect(function()
- MainPosTooltip.Visible = false
- MainPosRemove.Visible = false
- local stroke = MainPosBtn:FindFirstChildWhichIsA("UIStroke")
- if stroke then UI.Tween(stroke, {Thickness = 2}, 0.15):Play() end
- end)
- MainPosRemove.MouseButton1Click:Connect(function()
- ConfirmLabel.Text = "Remover posição principal?"
- ConfirmYes.Text = "Remover"
- ConfirmYes.MouseButton1Click:Once(function()
- State:ClearMainPosition()
- UpdateMainPositionUI()
- RenderSlots()
- OverlaySystem:Hide(ConfirmOverlay)
- end)
- ConfirmNo.MouseButton1Click:Once(function()
- OverlaySystem:Hide(ConfirmOverlay)
- end)
- OverlaySystem:Show(ConfirmOverlay)
- end)
- -- Listener do State
- State:OnChange(function(event)
- if event == "positions_changed" then
- RenderSlots()
- elseif event == "main_changed" then
- UpdateMainPositionUI()
- RenderSlots()
- elseif event == "page_changed" then
- UpdatePageUI()
- elseif event == "slot_changed" then
- UpdateSlotUI()
- end
- end)
- ----------------------------------------------------------------
- -- SEÇÃO 16 · ATALHOS DE TECLADO
- ----------------------------------------------------------------
- table.insert(Connections, UserInputService.InputBegan:Connect(function(input, processed)
- if processed then return end
- -- F9 = Toggle UI
- if input.KeyCode == Enum.KeyCode.F9 then
- MainFrame.Visible = not MainFrame.Visible
- end
- -- Ctrl+S = Salvar rápido no slot atual
- if input.KeyCode == Enum.KeyCode.S and UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) then
- if CharacterManager:Refresh() then
- local success = State:SavePosition(
- State.currentSlot,
- CharacterManager.rootPart.CFrame,
- "Quick Save " .. State.currentSlot
- )
- if success then
- State:UpdateSlot()
- end
- end
- end
- -- Ctrl+Shift+M = Teleportar para posição principal
- if input.KeyCode == Enum.KeyCode.M
- and UserInputService:IsKeyDown(Enum.KeyCode.LeftControl)
- and UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) then
- if State.mainPosition then
- Teleporter:Execute(State.mainPosition.position)
- end
- end
- end))
- ----------------------------------------------------------------
- -- SEÇÃO 17 · INICIALIZAÇÃO
- ----------------------------------------------------------------
- Persistence:Init()
- CharacterManager:Init()
- State:Load()
- -- Renderização inicial
- UpdatePageUI()
- UpdateSlotUI()
- UpdateMainPositionUI()
- -- Auto-save periódico (a cada 60s)
- task.spawn(function()
- while task.wait(60) do
- if ScreenGui.Parent then
- Persistence:SavePositions(State.positions)
- if State.mainPosition then
- Persistence:SaveMainPosition(State.mainPosition)
- end
- else
- break
- end
- end
- end)
- -- Cleanup ao fechar o jogo
- if type(game.BindToClose) == "function" then
- game:BindToClose(function()
- Persistence:SavePositions(State.positions)
- if State.mainPosition then
- Persistence:SaveMainPosition(State.mainPosition)
- end
- end)
- end
- ----------------------------------------------------------------
- -- SEÇÃO 18 · LOG DE INICIALIZAÇÃO
- ----------------------------------------------------------------
- local totalSaved = State:GetSlotCount()
- print("═══════════════════════════════════════════════")
- print(" ✦ Save Locate (NickZ) — V5.0 MEGA UPDATE")
- print("═══════════════════════════════════════════════")
- print(string.format(" 📍 Slots: %d/%d ocupados", totalSaved, State.maxSaves))
- print(string.format(" 📄 Páginas: %d", State.maxPages))
- print(string.format(" 🎮 Executor: %s", Environment.executorName))
- print(string.format(" 💾 FileSystem: %s", Environment.hasFileSystem and "✓" or "✕"))
- print(string.format(" ⭐ Pos. Principal: %s", State.mainPosition and State.mainPosition.name or "Nenhuma"))
- print(" ─────────────────────────────────────────────")
- print(" ⌨️ Atalhos:")
- print(" F9 ─────────── Toggle UI")
- print(" Ctrl+S ──────── Quick Save")
- print(" Ctrl+Shift+M ── TP Principal")
- print("═══════════════════════════════════════════════")
Add Comment
Please, Sign In to add comment