--[[ PORTAL (primera persona) - MÓVIL Colócalo como LocalScript en: StarterPlayer > StarterPlayerScripts - Fuerza la primera persona. - Muestra la pistola de portales y la vida (HEALTH) como en el videojuego. - Los portales se crean en la superficie a la que apuntas (pared, suelo o techo). Si no apuntas a nada, usa la superficie más cercana a ti. - Conserva el impulso: si caes por un portal del suelo, sales con esa velocidad. - Los botones están junto al botón de saltar. - Solo tú los ves y usas (LocalScript). ]] local Players = game:GetService("Players") local RunService = game:GetService("RunService") local TweenService = game:GetService("TweenService") local Debris = game:GetService("Debris") local player = Players.LocalPlayer local camera = workspace.CurrentCamera local playerGui = player:WaitForChild("PlayerGui") local A = CFrame.Angles local char, humanoid, root player.CameraMode = Enum.CameraMode.LockFirstPerson local fx = Instance.new("Folder") fx.Name = "LocalPortals" fx.Parent = workspace local H, W = 7.5, 5 -- alto y ancho del portal local COLORS = { A = Color3.fromRGB(70, 160, 255), B = Color3.fromRGB(255, 140, 40), } local portals = {} -- portals.A / portals.B = { model, cf } --------------------------------------------------------------------- -- UTILIDADES --------------------------------------------------------------------- local function castParams() local p = RaycastParams.new() p.FilterType = Enum.RaycastFilterType.Exclude p.FilterDescendantsInstances = { char, fx } return p end local function camFlat() local l = camera.CFrame.LookVector local f = Vector3.new(l.X, 0, l.Z) return f.Magnitude > 0.01 and f.Unit or Vector3.new(0, 0, -1) end --------------------------------------------------------------------- -- INTERFAZ (botones junto al botón de saltar) --------------------------------------------------------------------- local slot = playerGui:GetAttribute("HeroUISlots") or 0 -- evita solapar con mis otros scripts playerGui:SetAttribute("HeroUISlots", slot + 1) local BTN, GAP = 58, 8 local old = playerGui:FindFirstChild("PortalGui") if old then old:Destroy() end local gui = Instance.new("ScreenGui") gui.Name = "PortalGui" gui.ResetOnSpawn = false gui.IgnoreGuiInset = true gui.DisplayOrder = 100 gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling gui.Parent = playerGui local function dockNextToJump(frame) local function update() local vs = camera.ViewportSize local x, y = vs.X - 110, vs.Y - 16 local tg = playerGui:FindFirstChild("TouchGui") local tcf = tg and tg:FindFirstChild("TouchControlFrame") local jb = tcf and tcf:FindFirstChild("JumpButton") if jb and jb.AbsoluteSize.X > 0 then x = jb.AbsolutePosition.X - 12 y = jb.AbsolutePosition.Y + jb.AbsoluteSize.Y end frame.Position = UDim2.fromOffset(x - slot * (BTN + GAP + 4), y) end update() camera:GetPropertyChangedSignal("ViewportSize"):Connect(update) task.spawn(function() local tg = playerGui:WaitForChild("TouchGui", 15) if not tg then return end task.wait(1) update() local tcf = tg:WaitForChild("TouchControlFrame", 5) local jb = tcf and tcf:WaitForChild("JumpButton", 5) if jb then jb:GetPropertyChangedSignal("AbsolutePosition"):Connect(update) end update() end) end local column = Instance.new("Frame") column.BackgroundTransparency = 1 column.AnchorPoint = Vector2.new(1, 1) column.Size = UDim2.fromOffset(BTN, BTN * 3 + GAP * 2) column.Parent = gui local layout = Instance.new("UIListLayout") layout.Padding = UDim.new(0, GAP) layout.VerticalAlignment = Enum.VerticalAlignment.Bottom layout.Parent = column dockNextToJump(column) local function makeButton(text, color) local b = Instance.new("TextButton") b.Size = UDim2.fromOffset(BTN, BTN) b.BackgroundColor3 = color b.BackgroundTransparency = 0.1 b.TextColor3 = Color3.new(1, 1, 1) b.Font = Enum.Font.GothamBold b.TextSize = 13 b.TextWrapped = true b.Text = text Instance.new("UICorner", b).CornerRadius = UDim.new(1, 0) b.Parent = column return b end local btnClear = makeButton(":wastebasket:\nBorrar", Color3.fromRGB(60, 60, 75)) local btnB = makeButton(":orange_circle:\nPortal B", Color3.fromRGB(210, 100, 20)) local btnA = makeButton(":blue_circle:\nPortal A", Color3.fromRGB(30, 100, 200)) -- (el orden de creación + alineación inferior deja A arriba, B en medio, Borrar abajo) -- Mira estilo Portal: aro central + indicadores azul / naranja local cross = Instance.new("Frame") cross.AnchorPoint = Vector2.new(0.5, 0.5) cross.Position = UDim2.fromScale(0.5, 0.5) cross.Size = UDim2.fromOffset(26, 26) cross.BackgroundTransparency = 1 Instance.new("UICorner", cross).CornerRadius = UDim.new(1, 0) local cs = Instance.new("UIStroke", cross) cs.Color = Color3.new(1, 1, 1) cs.Thickness = 2 cross.Parent = gui local function indicator(x, color) local f = Instance.new("Frame") f.AnchorPoint = Vector2.new(0.5, 0.5) f.Position = UDim2.new(0.5, x, 0.5, 0) f.Size = UDim2.fromOffset(12, 12) f.BackgroundColor3 = color f.BackgroundTransparency = 1 Instance.new("UICorner", f).CornerRadius = UDim.new(1, 0) local s = Instance.new("UIStroke", f) s.Color = color s.Thickness = 2 f.Parent = gui return f end local indA, indB = indicator(-26, COLORS.A), indicator(26, COLORS.B) local function refreshCross() indA.BackgroundTransparency = portals.A and 0.05 or 1 indB.BackgroundTransparency = portals.B and 0.05 or 1 end -- Destello al teletransportarse local flashFrame = Instance.new("Frame") flashFrame.Size = UDim2.fromScale(1, 1) flashFrame.BackgroundColor3 = Color3.new(1, 1, 1) flashFrame.BackgroundTransparency = 1 flashFrame.BorderSizePixel = 0 flashFrame.ZIndex = 10 flashFrame.Parent = gui local function flash() flashFrame.BackgroundTransparency = 0.35 TweenService:Create(flashFrame, TweenInfo.new(0.4), { BackgroundTransparency = 1 }):Play() end -- HUD de vida estilo Portal / Half-Life local ORANGE = Color3.fromRGB(255, 176, 0) local hud = Instance.new("Frame") hud.AnchorPoint = Vector2.new(0.5, 1) hud.Position = UDim2.new(0.5, 0, 1, -14) hud.Size = UDim2.fromOffset(170, 52) hud.BackgroundColor3 = Color3.new(0, 0, 0) hud.BackgroundTransparency = 0.45 Instance.new("UICorner", hud).CornerRadius = UDim.new(0, 8) local hs = Instance.new("UIStroke", hud) hs.Color = ORANGE hs.Thickness = 1.5 hs.Transparency = 0.3 hud.Parent = gui local hudLabel = Instance.new("TextLabel") hudLabel.BackgroundTransparency = 1 hudLabel.Position = UDim2.fromOffset(10, 28) hudLabel.Size = UDim2.fromOffset(80, 16) hudLabel.Font = Enum.Font.GothamBold hudLabel.TextSize = 13 hudLabel.TextColor3 = ORANGE hudLabel.TextXAlignment = Enum.TextXAlignment.Left hudLabel.Text = "HEALTH" hudLabel.Parent = hud local hudNum = Instance.new("TextLabel") hudNum.BackgroundTransparency = 1 hudNum.Position = UDim2.fromOffset(70, 2) hudNum.Size = UDim2.fromOffset(92, 48) hudNum.Font = Enum.Font.GothamBold hudNum.TextSize = 38 hudNum.TextColor3 = ORANGE hudNum.TextXAlignment = Enum.TextXAlignment.Right hudNum.Text = "100" hudNum.Parent = hud local hint = Instance.new("TextLabel") hint.AnchorPoint = Vector2.new(0.5, 0) hint.Position = UDim2.new(0.5, 0, 0, 50) hint.Size = UDim2.fromOffset(380, 50) hint.BackgroundTransparency = 1 hint.TextColor3 = Color3.new(1, 1, 1) hint.TextStrokeTransparency = 0.4 hint.TextWrapped = true hint.Font = Enum.Font.GothamMedium hint.TextSize = 14 hint.Text = "Apunta con la mira y toca :blue_circle: o :orange_circle: para disparar un portal. Entra en uno para salir por el otro." hint.Parent = gui task.delay(12, function() hint.Visible = false end) local function say(text) hint.Text = text hint.Visible = true task.delay(3, function() if hint.Text == text then hint.Visible = false end end) end --------------------------------------------------------------------- -- PISTOLA DE PORTALES (viewmodel en primera persona) --------------------------------------------------------------------- local gunFolder = Instance.new("Folder") gunFolder.Name = "PortalGun" gunFolder.Parent = fx local gunParts = {} local function addGunPart(size, rel, color, material, shape) local p = Instance.new("Part") p.Size = size p.Color = color p.Material = material or Enum.Material.SmoothPlastic if shape then p.Shape = shape end p.Anchored, p.CanCollide, p.CanTouch, p.CanQuery, p.CastShadow = true, false, false, false, false p.Parent = gunFolder table.insert(gunParts, { part = p, rel = rel }) return p end local white = Color3.fromRGB(240, 240, 242) local grey = Color3.fromRGB(80, 80, 88) local silver = Color3.fromRGB(190, 190, 198) addGunPart(Vector3.new(0.34, 0.34, 1.3), CFrame.new(0, 0, 0), white) addGunPart(Vector3.new(0.46, 0.5, 0.55), CFrame.new(0, -0.03, 0.8), grey) addGunPart(Vector3.new(0.1, 0.52, 0.52), CFrame.new(0, 0, -0.35) * A(0, math.pi / 2, 0), silver, Enum.Material.Metal, Enum.PartType.Cylinder) addGunPart(Vector3.new(0.1, 0.46, 0.46), CFrame.new(0, 0, -0.7) * A(0, math.pi / 2, 0), silver, Enum.Material.Metal, Enum.PartType.Cylinder) for _, o in { Vector3.new(0.19, 0.1, 0), Vector3.new(-0.19, 0.1, 0), Vector3.new(0, -0.2, 0) } do addGunPart(Vector3.new(0.05, 0.05, 0.75), CFrame.new(o.X, o.Y, -1.0), silver, Enum.Material.Metal) end local tip = addGunPart(Vector3.new(0.24, 0.24, 0.24), CFrame.new(0, 0, -1.0), COLORS.A, Enum.Material.Neon, Enum.PartType.Ball) local strip = addGunPart(Vector3.new(0.07, 0.07, 0.9), CFrame.new(0, 0.19, -0.05), COLORS.A, Enum.Material.Neon) local recoil, tipColor = 0, COLORS.A RunService:BindToRenderStep("PortalGunViewmodel", Enum.RenderPriority.Camera.Value + 1, function(dt) recoil = math.max(0, recoil - dt * 5) local speed = 0 if root then local v = root.AssemblyLinearVelocity speed = Vector3.new(v.X, 0, v.Z).Magnitude end local bob = math.min(speed / 16, 1) local t = os.clock() local off = CFrame.new(0.8 + math.sin(t * 6) * 0.03 * bob, -0.75 + math.abs(math.cos(t * 6)) * 0.04 * bob, -1.4 + recoil * 0.35) * A(recoil * 0.25, -0.05, 0) local base = camera.CFrame * off for _, g in gunParts do g.part.CFrame = base * g.rel end tip.Color, strip.Color = tipColor, tipColor -- HUD de vida if humanoid then local hp = humanoid.Health hudNum.Text = (hp >= 1e6) and "∞" or tostring(math.max(0, math.ceil(hp))) local low = hp < 25 hudNum.TextColor3 = low and Color3.fromRGB(255, 70, 40) or ORANGE hudLabel.TextColor3 = hudNum.TextColor3 end end) --------------------------------------------------------------------- -- CREAR PORTALES --------------------------------------------------------------------- local function buildPortal(key, cf) if portals[key] then portals[key].model:Destroy() end local model = Instance.new("Model") model.Name = "Portal" .. key model.Parent = fx local color = COLORS[key] local function makeCylinder(fullSize, c, transparency) local p = Instance.new("Part") p.Shape = Enum.PartType.Cylinder p.Size = Vector3.new(fullSize.X, 0.1, 0.1) p.Material = Enum.Material.Neon p.Color = c p.Transparency = transparency p.Anchored, p.CanCollide, p.CanTouch, p.CanQuery, p.CastShadow = true, false, false, false, false p.CFrame = cf * A(0, math.pi / 2, 0) -- el eje del cilindro mira hacia la normal de la superficie p.Parent = model TweenService:Create(p, TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { Size = fullSize }):Play() return p end makeCylinder(Vector3.new(0.2, H + 0.8, W + 0.8), color, 0.05) -- aro brillante local disc = makeCylinder(Vector3.new(0.35, H, W), color:Lerp(Color3.new(0, 0, 0), 0.55), 0.35) local e = Instance.new("ParticleEmitter") e.Color = ColorSequence.new(color, Color3.new(1, 1, 1)) e.LightEmission = 1 e.Size = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.6), NumberSequenceKeypoint.new(1, 0) }) e.Transparency = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.2), NumberSequenceKeypoint.new(1, 1) }) e.Lifetime = NumberRange.new(0.6, 1) e.Rate = 45 e.Speed = NumberRange.new(0.5, 2) e.SpreadAngle = Vector2.new(180, 180) e.RotSpeed = NumberRange.new(-120, 120) e.Parent = disc local light = Instance.new("PointLight") light.Color = color light.Brightness = 2 light.Range = 14 light.Parent = disc portals[key] = { model = model, cf = cf } refreshCross() end -- Marco del portal sobre una superficie (pared, suelo o techo) local function surfaceFrame(pos, normal) pos = pos + normal * 0.06 local up = Vector3.yAxis if math.abs(normal.Y) > 0.7 then up = camFlat() -- en suelo/techo el eje largo del portal sigue tu mirada, como en Portal else local g = workspace:Raycast(pos + Vector3.new(0, 1, 0), Vector3.new(0, -100, 0), castParams()) if g and pos.Y - H / 2 < g.Position.Y then pos = Vector3.new(pos.X, g.Position.Y + H / 2 + 0.1, pos.Z) end end return CFrame.lookAt(pos, pos + normal, up) end -- Superficie a la que apuntas; si no hay, la más cercana a ti local function findSurface() local params = castParams() local vp = camera.ViewportSize / 2 local ray = camera:ViewportPointToRay(vp.X, vp.Y) local hit = workspace:Raycast(ray.Origin, ray.Direction * 250, params) if hit then return hit.Position, hit.Normal end local best for _, d in { Vector3.xAxis, -Vector3.xAxis, Vector3.yAxis, -Vector3.yAxis, Vector3.zAxis, -Vector3.zAxis } do local h = workspace:Raycast(root.Position, d * 60, params) if h and (not best or h.Distance < best.Distance) then best = h end end if best then return best.Position, best.Normal end end local function shootBall(from, to, color, onHit) local b = Instance.new("Part") b.Shape = Enum.PartType.Ball b.Size = Vector3.new(0.6, 0.6, 0.6) b.Material = Enum.Material.Neon b.Color = color b.Anchored, b.CanCollide, b.CanTouch, b.CanQuery = true, false, false, false b.Position = from b.Parent = fx local dur = math.clamp((to - from).Magnitude / 250, 0.05, 0.4) local tw = TweenService:Create(b, TweenInfo.new(dur, Enum.EasingStyle.Linear), { Position = to }) tw.Completed:Connect(function() b:Destroy() onHit() end) tw:Play() end local function fire(key) if not root or not root.Parent then return end local pos, normal = findSurface() if not pos then say("No hay ninguna superficie cerca") return end tipColor = COLORS[key] recoil = 1 local cf = surfaceFrame(pos, normal) shootBall(tip.Position, pos, COLORS[key], function() buildPortal(key, cf) end) end local function clearPortals() for key, data in portals do data.model:Destroy() portals[key] = nil end refreshCross() say("Portales borrados") end btnA.MouseButton1Click:Connect(function() fire("A") end) btnB.MouseButton1Click:Connect(function() fire("B") end) btnClear.MouseButton1Click:Connect(clearPortals) --------------------------------------------------------------------- -- TELETRANSPORTE (detección local: solo tú puedes entrar) --------------------------------------------------------------------- local lastTp, armed = 0, true -- ¿Está tu personaje dentro de la zona de entrada del portal? local function inside(data) local rel = data.cf:PointToObjectSpace(root.Position) if (rel.X / (W / 2)) ^ 2 + (rel.Y / (H / 2)) ^ 2 > 1 then return false end local d = -rel.Z -- distancia hacia fuera de la superficie local n = data.cf.LookVector if math.abs(n.Y) > 0.7 then if n.Y > 0 then return d < 4.3 and d > -1.5 end -- suelo: pisarlo return d < 3.3 and d > -1.5 -- techo: saltar hacia él end return math.abs(d) < 1.3 -- pared end local function teleport(from, to) lastTp = os.clock() local nOut = to.cf.LookVector -- Velocidad: del portal de entrada al de salida (giro de 180°) local vl = from.cf:VectorToObjectSpace(root.AssemblyLinearVelocity) vl = Vector3.new(-vl.X, vl.Y, -vl.Z) local vOut = to.cf:VectorToWorldSpace(vl) local minOut = (math.abs(nOut.Y) > 0.7) and 22 or 14 local outSpeed = vOut:Dot(nOut) if outSpeed < minOut then vOut += nOut * (minOut - outSpeed) end -- Posición de salida local pos if math.abs(nOut.Y) > 0.7 then pos = to.cf.Position + nOut * 3.4 else local flatOut = Vector3.new(nOut.X, 0, nOut.Z).Unit pos = to.cf.Position + flatOut * 2.4 pos = Vector3.new(pos.X, to.cf.Position.Y - H / 2 + 3.2, pos.Z) end -- Giro de la cámara (para que mires hacia fuera del portal de salida) local rotMap = to.cf.Rotation * A(0, math.pi, 0) * from.cf.Rotation:Inverse() local newLook = rotMap * camera.CFrame.LookVector local camHeight = camera.CFrame.Position.Y - root.Position.Y local flat = Vector3.new(newLook.X, 0, newLook.Z) if flat.Magnitude < 0.1 then flat = camFlat() end root.CFrame = CFrame.lookAt(pos, pos + flat.Unit) root.AssemblyLinearVelocity = vOut root.AssemblyAngularVelocity = Vector3.zero local camPos = pos + Vector3.new(0, camHeight, 0) pcall(function() camera.CFrame = CFrame.lookAt(camPos, camPos + newLook) end) flash() end RunService.Heartbeat:Connect(function() if not root or not root.Parent then return end local hitKey for key, data in portals do if inside(data) then hitKey = key break end end if not hitKey then armed = true -- te has alejado: vuelve a activarse return end if not armed or os.clock() - lastTp < 0.4 then return end local other = portals[hitKey == "A" and "B" or "A"] if other then armed = false teleport(portals[hitKey], other) end end) --------------------------------------------------------------------- -- PERSONAJE / REAPARICIÓN --------------------------------------------------------------------- local function setChar(c) char = c humanoid = c:WaitForChild("Humanoid") root = c:WaitForChild("HumanoidRootPart") end player.CharacterAdded:Connect(setChar) setChar(player.Character or player.CharacterAdded:Wait())