--> Copyright (c) 2026 Mastedore. --> SPDX-License-Identifier: Unlicense --[[ Este código lo he dedicado al dominio publico, Usalo como se te pegue, si? ]] -- Estado de un slot (solo spin + zoom) type SlotVP = { cam : Camera, model : Model, center : Vector3, yaw : number, distance : number, baseSize : number, spinSpeed : number, } -- Estado del InfoFrame (full interactivo) type InfoVP = { cam : Camera, model : Model, center : Vector3, yaw : number, pitch : number, distance : number, panOffset : Vector3, baseSize : number, autoSpin : boolean, } -- Zoom local ZOOM_PERCENT = 0.15 -- fraccion de la distancia recorrida por cada paso de desplazamiento local MIN_ZOOM_MUL = 0.8 -- distancia min = baseSize * this local MAX_ZOOM_MUL = 6 -- distancia max = baseSize * this -- orbita del InfoFrame (drag de clik izquierdo) local ORBIT_SENS = 0.005 -- radianes por pixel local MIN_PITCH = math.rad(-80) local MAX_PITCH = math.rad(80) -- pan 🥖🥐🍞🥯 del InfoFrame (drag pero con click derecho) local PAN_SENS = 0.003 -- unidades por pixel, escalado por la distancia -- radianes por segundo local SLOT_SPIN_MIN = 0.35 local SLOT_SPIN_MAX = 0.65 local INFO_SPIN_SPD = 0.50 local function StartLoop() if LoopConn then return end LoopConn = RunService.RenderStepped:Connect(function(dt: number) -- viewports de slots for vp, data in SlotViewports do if not vp.Parent then SlotViewports[vp] = nil continue end data.yaw += dt * data.spinSpeed local camPos = data.center + Vector3.new( math.sin(data.yaw) * data.distance, data.distance * 0.25, math.cos(data.yaw) * data.distance ) data.cam.CFrame = CFrame.lookAt(camPos, data.center) end -- viewporet del infoframe local iv = CurrentInfoVP if iv and InfoViewport and InfoViewport.Parent then if iv.autoSpin then iv.yaw += dt * INFO_SPIN_SPD end local camPos = iv.center + iv.panOffset + Vector3.new( math.sin(iv.yaw) * math.cos(iv.pitch) * iv.distance, math.sin(iv.pitch) * iv.distance, math.cos(iv.yaw) * math.cos(iv.pitch) * iv.distance ) iv.cam.CFrame = CFrame.lookAt(camPos, iv.center + iv.panOffset) end end) end -------------------------------------------------------------------------------- -- VIEWPORT HELPERS - -------------------------------------------------------------------------------- -- prepara un viewportframe y le penetra un modelo (sin worldmodels). -- retorna el modelo y la camara local function CloneIntoVP(vp: viewportframe, model: Model): (Model, Camera) for _, child in vp:GetChildren() do if child:IsA("UIStroke") or child:IsA("UIGradient") then continue end child:Destroy() end local clone = model:Clone() for _, desc in clone:GetDescendants() do -- Beams/Trails need Attachments that don't render properly in ViewportFrames if desc:IsA("Beam") or desc:IsA("Trail") then desc:Destroy() end end clone.Parent = vp local cam = Instance.new("Camera") cam.FieldOfView = 40 vp.CurrentCamera = cam cam.Parent = vp return clone, cam end -- giro + scroll pa zoom. cada slot tiene una velocidad random para darle un mejor look local function SetupSlotViewport( vp : ViewportFrame, model : Model, index : number, janitor : typeof(Janitor.new()) ) local clone, cam = CloneIntoVP(vp, model) local cf, size = clone:GetBoundingBox() local baseSize = math.max(size.X, size.Y, size.Z) local startDist = baseSize * 2.2 local data: SlotVP = { cam = cam, model = clone, center = cf.Position, yaw = index * 0.7, -- lo q ya mencione distance = startDist, baseSize = baseSize, spinSpeed = SLOT_SPIN_MIN + math.random() * (SLOT_SPIN_MAX - SLOT_SPIN_MIN), } SlotViewports[vp] = data -- scroll zoom (y solo pasa cuando el cursor esta dentro del viewport!!11!1!!!1!!1!1111!!!!!!) janitor:Add(vp.InputChanged:Connect(function(input: InputObject) if input.UserInputType ~= Enum.UserInputType.MouseWheel then return end local delta = -input.Position.Z * (data.distance * ZOOM_PERCENT) data.distance = math.clamp( data.distance + delta, baseSize * MIN_ZOOM_MUL, baseSize * MAX_ZOOM_MUL ) end)) end --setupiar el infoframe lol ya explique eso local function SetupInfoViewport(model: Model) if not InfoViewport then return end InfoInputJanitor:Cleanup() SlotViewports[InfoViewport] = nil local clone, cam = CloneIntoVP(InfoViewport, model) local cf, size = clone:GetBoundingBox() local baseSize = math.max(size.X, size.Y, size.Z) local iv: InfoVP = { cam = cam, model = clone, center = cf.Position, yaw = 0, pitch = math.rad(15), -- para darle una pequeña sacudida la primera vez que renderiza distance = baseSize * 2.5, panOffset = Vector3.zero, baseSize = baseSize, autoSpin = true, } CurrentInfoVP = iv local orbitActive = false local panActive = false local lastMousePos = Vector2.zero -- scroll zoom InfoInputJanitor:Add(InfoViewport.InputChanged:Connect(function(input: InputObject) if input.UserInputType ~= Enum.UserInputType.MouseWheel then return end local delta = -input.Position.Z * (iv.distance * ZOOM_PERCENT) iv.distance = math.clamp( iv.distance + delta, baseSize * MIN_ZOOM_MUL, baseSize * MAX_ZOOM_MUL ) end)) -- drag InfoInputJanitor:Add(InfoViewport.InputBegan:Connect(function(input: InputObject) local t = input.UserInputType if t == Enum.UserInputType.MouseButton1 then orbitActive = true panActive = false lastMousePos = Vector2.new(input.Position.X, input.Position.Y) iv.autoSpin = false elseif t == Enum.UserInputType.MouseButton2 then panActive = true orbitActive = false lastMousePos = Vector2.new(input.Position.X, input.Position.Y) iv.autoSpin = false end end)) -- drag delta InfoInputJanitor:Add(UserInputService.InputChanged:Connect(function(input: InputObject) if input.UserInputType ~= Enum.UserInputType.MouseMovement then return end if not orbitActive and not panActive then return end local curPos = Vector2.new(input.Position.X, input.Position.Y) local delta = curPos - lastMousePos lastMousePos = curPos if orbitActive then iv.yaw -= delta.X * ORBIT_SENS iv.pitch = math.clamp(iv.pitch + delta.Y * ORBIT_SENS, MIN_PITCH, MAX_PITCH) elseif panActive then local right = cam.CFrame.RightVector local up = cam.CFrame.UpVector iv.panOffset += right * (-delta.X * PAN_SENS * iv.distance) iv.panOffset += up * ( delta.Y * PAN_SENS * iv.distance) end end)) -- liberar InfoInputJanitor:Add(UserInputService.InputEnded:Connect(function(input: InputObject) local t = input.UserInputType if t == Enum.UserInputType.MouseButton1 then orbitActive = false elseif t == Enum.UserInputType.MouseButton2 then panActive = false end end)) end