KeylessUniversal
Portal
Run it
Copy the script and paste it into your executor.
Portal.lua · 517 lines
--[[
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())About this script
Portal



