add gs
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"lint": {
|
||||
"ImportUnused": "fatal"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"lint": {
|
||||
"SameLineStatement": "fatal",
|
||||
"LocalShadow": "fatal",
|
||||
"FunctionUnused": "fatal",
|
||||
"ImplicitReturn": "fatal"
|
||||
}
|
||||
}
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
local VRService = game:GetService("VRService")
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local RobloxGui = CoreGui.RobloxGui
|
||||
local CommonUtil = require(RobloxGui.Modules.Common.CommonUtil)
|
||||
|
||||
local PARTS_INFO = {
|
||||
Body = {
|
||||
size = Vector3.new(0.391, 0.277, 0.731),
|
||||
meshId = "rbxassetid://433286977",
|
||||
textureId = "rbxassetid://433287000",
|
||||
offset = CFrame.new(0, -0.115, 0.213)
|
||||
},
|
||||
RightGrip = {
|
||||
meshId = "rbxassetid://433290663",
|
||||
textureId = "rbxassetid://433290688",
|
||||
offset = CFrame.new(0.057, -0.047, 0.29),
|
||||
moveOffset = CFrame.new(-0.005, 0, 0)
|
||||
},
|
||||
LeftGrip = {
|
||||
meshId = "rbxassetid://433289821",
|
||||
textureId = "rbxassetid://433289832",
|
||||
offset = CFrame.new(-0.057, -0.047, 0.29),
|
||||
moveOffset = CFrame.new(0.005, 0, 0)
|
||||
},
|
||||
SystemButton = {
|
||||
meshId = "rbxassetid://433291265",
|
||||
textureId = "rbxassetid://433291278",
|
||||
offset = CFrame.new(0, 0.005, 0.294),
|
||||
moveOffset = CFrame.new(Vector3.new(0, 0.997420907, 0.071774438) * -0.001)
|
||||
},
|
||||
MenuButton = {
|
||||
meshId = "rbxassetid://433288124",
|
||||
textureId = "rbxassetid://433288134",
|
||||
offset = CFrame.new(0, 0.016, 0.068),
|
||||
moveOffset = CFrame.new(Vector3.new(0, 0.999877751, -0.0156304892) * -0.001)
|
||||
},
|
||||
Trackpad = {
|
||||
meshId = "rbxassetid://433288821",
|
||||
textureId = "rbxassetid://433288836",
|
||||
offset = CFrame.new(0, 0.000, 0.163),
|
||||
moveOffset = CFrame.new(Vector3.new(0, 0.993506849, 0.113772281) * -0.001)
|
||||
},
|
||||
Trigger = {
|
||||
meshId = "rbxassetid://433288767",
|
||||
textureId = "rbxassetid://433288775",
|
||||
offset = CFrame.new(0, -0.093, 0.163),
|
||||
},
|
||||
LED = {
|
||||
meshId = "rbxassetid://433293218",
|
||||
textureId = "rbxassetid://433293231",
|
||||
offset = CFrame.new(0, 0.007, 0.32)
|
||||
},
|
||||
ButtonA = {
|
||||
meshId = "rbxassetid://922444331",
|
||||
textureId = "rbxasset://textures/ui/VR/ButtonA.png",
|
||||
offset = CFrame.new(-0, 0.0115499999, 0.197307006) * CFrame.Angles(0, math.pi, 0),
|
||||
moveOffset = CFrame.new(0, -0.005, 0)
|
||||
},
|
||||
ButtonB = {
|
||||
meshId = "rbxassetid://922454202",
|
||||
textureId = "rbxasset://textures/ui/VR/ButtonB.png",
|
||||
offset = CFrame.new(0.0391042456, 0.0154935224, 0.161779419) * CFrame.Angles(0, math.pi, 0),
|
||||
moveOffset = CFrame.new(0, -0.005, 0)
|
||||
},
|
||||
ButtonX = {
|
||||
meshId = "rbxassetid://922453111",
|
||||
textureId = "rbxasset://textures/ui/VR/ButtonX.png",
|
||||
offset = CFrame.new(-0.0390719995, 0.0154770007, 0.161733001) * CFrame.Angles(0, math.pi, 0),
|
||||
moveOffset = CFrame.new(0, -0.005, 0)
|
||||
},
|
||||
ButtonY = {
|
||||
meshId = "rbxassetid://922455502",
|
||||
textureId = "rbxasset://textures/ui/VR/ButtonY.png",
|
||||
offset = CFrame.new(0, 0.0197229274, 0.126810834) * CFrame.Angles(0, math.pi, 0),
|
||||
moveOffset = CFrame.new(0, -0.005, 0)
|
||||
}
|
||||
}
|
||||
local MATERIAL = Enum.Material.Granite
|
||||
local SCALE = Vector3.new(0.033, 0.033, 0.033)
|
||||
|
||||
local ViveController = {}
|
||||
ViveController.__index = ViveController
|
||||
|
||||
local touchpadForUserCFrame = {
|
||||
[Enum.UserCFrame.LeftHand] = Enum.VRTouchpad.Left,
|
||||
[Enum.UserCFrame.RightHand] = Enum.VRTouchpad.Right
|
||||
}
|
||||
|
||||
function ViveController.new(userCFrame)
|
||||
local self = setmetatable({}, ViveController)
|
||||
self.userCFrame = userCFrame
|
||||
self.touchpad = touchpadForUserCFrame[self.userCFrame]
|
||||
self.touchpadMode = VRService:GetTouchpadMode(self.touchpad)
|
||||
self.onTouchpadModeChangedConn = VRService.TouchpadModeChanged:connect(function(...)
|
||||
self:onTouchpadModeChanged(...)
|
||||
end)
|
||||
|
||||
self.model = CommonUtil.Create("Model") {
|
||||
Name = "ViveController",
|
||||
Archivable = false
|
||||
}
|
||||
|
||||
self.origin = CommonUtil.Create("Part") {
|
||||
Parent = self.model,
|
||||
Name = "Origin",
|
||||
Anchored = false,
|
||||
Transparency = 1,
|
||||
Size = Vector3.new(0.05, 0.05, 0.05),
|
||||
CanCollide = false
|
||||
}
|
||||
|
||||
self.parts = {}
|
||||
|
||||
for partName, partInfo in pairs(PARTS_INFO) do
|
||||
local partScale = SCALE
|
||||
if partInfo.scale then
|
||||
partScale = partScale * partInfo.scale
|
||||
end
|
||||
|
||||
local part = CommonUtil.Create("Part") {
|
||||
Parent = self.model,
|
||||
Name = partName,
|
||||
Anchored = false,
|
||||
CanCollide = false,
|
||||
Material = MATERIAL,
|
||||
Size = partInfo.size or Vector3.new(0.05, 0.05, 0.05),
|
||||
CFrame = self.origin.CFrame * partInfo.offset
|
||||
}
|
||||
local mesh = CommonUtil.Create("SpecialMesh") {
|
||||
Parent = part,
|
||||
Name = "Mesh",
|
||||
MeshId = partInfo.meshId,
|
||||
TextureId = partInfo.textureId,
|
||||
Scale = partScale
|
||||
}
|
||||
local weld = CommonUtil.Create("Weld") {
|
||||
Parent = part,
|
||||
Name = "Weld",
|
||||
Part0 = self.origin,
|
||||
Part1 = part,
|
||||
C0 = partInfo.offset
|
||||
}
|
||||
|
||||
self.parts[partName] = part
|
||||
end
|
||||
|
||||
local trackpadIndicator = CommonUtil.Create("Part") {
|
||||
Parent = self.model,
|
||||
Name = "TrackpadIndicator",
|
||||
Material = Enum.Material.Neon,
|
||||
BrickColor = BrickColor.new("Institutional white"),
|
||||
Shape = Enum.PartType.Ball,
|
||||
Anchored = false,
|
||||
Transparency = 1,
|
||||
Size = Vector3.new(0.05, 0.05, 0.05),
|
||||
CanCollide = false
|
||||
}
|
||||
CommonUtil.Create("Weld") {
|
||||
Parent = trackpadIndicator,
|
||||
Name = "Weld",
|
||||
Part0 = self.origin,
|
||||
Part1 = trackpadIndicator,
|
||||
C0 = PARTS_INFO.Trackpad.offset
|
||||
}
|
||||
self.parts.TrackpadIndicator = trackpadIndicator
|
||||
self.model.PrimaryPart = self.origin
|
||||
|
||||
self:onTouchpadModeChanged(self.touchpad, VRService:GetTouchpadMode(self.touchpad))
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ViveController:setPartVisible(partName, visible)
|
||||
local part = self.parts[partName]
|
||||
if part then
|
||||
part.Transparency = visible and 0 or 1
|
||||
end
|
||||
end
|
||||
|
||||
function ViveController:setABXYEnabled(enabled)
|
||||
self:setPartVisible("ButtonA", enabled)
|
||||
self:setPartVisible("ButtonB", enabled)
|
||||
self:setPartVisible("ButtonX", enabled)
|
||||
self:setPartVisible("ButtonY", enabled)
|
||||
end
|
||||
|
||||
function ViveController:setCFrame(cframe)
|
||||
self.model:SetPrimaryPartCFrame(cframe)
|
||||
end
|
||||
|
||||
function ViveController:setButtonState(partName, depressed)
|
||||
local partInfo = PARTS_INFO[partName]
|
||||
if not partInfo then return end
|
||||
|
||||
local offset = partInfo.offset
|
||||
local moveOffset = partInfo.moveOffset
|
||||
|
||||
if offset and moveOffset then
|
||||
local part = self.parts[partName]
|
||||
if part then
|
||||
local mesh = part:FindFirstChild("Mesh")
|
||||
local weld = part:FindFirstChild("Weld")
|
||||
if weld then
|
||||
if depressed then
|
||||
part.CFrame = self.origin.CFrame * offset * moveOffset
|
||||
weld.C0 = offset * moveOffset
|
||||
else
|
||||
part.CFrame = self.origin.CFrame * offset
|
||||
weld.C0 = offset
|
||||
end
|
||||
end
|
||||
if mesh then
|
||||
if depressed then
|
||||
mesh.VertexColor = Vector3.new(0.5, 0.5, 0.5)
|
||||
else
|
||||
mesh.VertexColor = Vector3.new(1, 1, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ViveController:setTriggerState(state)
|
||||
local partInfo = PARTS_INFO.Trigger
|
||||
local offset = partInfo.offset
|
||||
|
||||
local part = self.parts.Trigger
|
||||
local weld = part:FindFirstChild("Weld")
|
||||
if weld then
|
||||
local angleMin, angleMax = math.rad(0), math.rad(-20)
|
||||
local angleRange = angleMax - angleMin
|
||||
local rotCenter = Vector3.new(0, 0.05, -0.025)
|
||||
local angle = (state * angleRange) + angleMin
|
||||
local newOffset = offset * CFrame.new(rotCenter) * CFrame.Angles(angle, 0, 0) * CFrame.new(-rotCenter)
|
||||
part.CFrame = self.origin.CFrame * newOffset
|
||||
weld.C0 = newOffset
|
||||
end
|
||||
end
|
||||
|
||||
function ViveController:setTrackpadState(pos)
|
||||
local part = self.parts.TrackpadIndicator
|
||||
local weld = part:FindFirstChild("Weld")
|
||||
if weld then
|
||||
local pos3d = Vector3.new(pos.X, 0, -pos.Y) * 0.055
|
||||
local trackpadSpace = CFrame.Angles(math.rad(6.5), 0, 0) * CFrame.new(0, 0.002 * (pos.magnitude ^ 3), 0)
|
||||
local newOffset = PARTS_INFO.Trackpad.offset * CFrame.new(0, 0.01, 0) * trackpadSpace:toWorldSpace(CFrame.new(pos3d))
|
||||
part.CFrame = self.origin.CFrame * newOffset
|
||||
weld.C0 = newOffset
|
||||
end
|
||||
end
|
||||
|
||||
function ViveController:onButtonInputChanged(inputObject, depressed)
|
||||
--Trackpad is bound to L3 or R3 depending on controller
|
||||
if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.ButtonR3) or
|
||||
(self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.ButtonL3) then
|
||||
self:setButtonState("Trackpad", depressed)
|
||||
return
|
||||
end
|
||||
|
||||
--Grips are bound to L2 or R2 depending on controller
|
||||
if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.ButtonR1) or
|
||||
(self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.ButtonL1) then
|
||||
self:setButtonState("LeftGrip", depressed)
|
||||
self:setButtonState("RightGrip", depressed)
|
||||
return
|
||||
end
|
||||
|
||||
--ButtonStart on the right, ButtonSelect on the left
|
||||
if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.ButtonStart) or
|
||||
(self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.ButtonSelect) then
|
||||
self:setButtonState("MenuButton", depressed)
|
||||
return
|
||||
end
|
||||
|
||||
--If the touchpad mode is ABXY, then we have four buttons to deal with
|
||||
if self.touchpadMode == Enum.VRTouchpadMode.ABXY then
|
||||
if inputObject.KeyCode == Enum.KeyCode.ButtonA then
|
||||
self:setButtonState("ButtonA", depressed)
|
||||
return
|
||||
end
|
||||
if inputObject.KeyCode == Enum.KeyCode.ButtonB then
|
||||
self:setButtonState("ButtonB", depressed)
|
||||
return
|
||||
end
|
||||
if inputObject.KeyCode == Enum.KeyCode.ButtonX then
|
||||
self:setButtonState("ButtonX", depressed)
|
||||
return
|
||||
end
|
||||
if inputObject.KeyCode == Enum.KeyCode.ButtonY then
|
||||
self:setButtonState("ButtonY", depressed)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ViveController:onInputBegan(inputObject)
|
||||
self:onButtonInputChanged(inputObject, true)
|
||||
if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick2) or
|
||||
(self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick1) then
|
||||
self:setPartVisible("TrackpadIndicator", true)
|
||||
end
|
||||
end
|
||||
|
||||
function ViveController:onInputChanged(inputObject)
|
||||
if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.ButtonR2) or
|
||||
(self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.ButtonL2) then
|
||||
self:setTriggerState(inputObject.Position.Z)
|
||||
end
|
||||
if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick2) or
|
||||
(self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick1) then
|
||||
self:setTrackpadState(inputObject.Position)
|
||||
end
|
||||
end
|
||||
|
||||
function ViveController:onInputEnded(inputObject)
|
||||
self:onButtonInputChanged(inputObject, false)
|
||||
if (self.userCFrame == Enum.UserCFrame.RightHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick2) or
|
||||
(self.userCFrame == Enum.UserCFrame.LeftHand and inputObject.KeyCode == Enum.KeyCode.Thumbstick1) then
|
||||
self:setPartVisible("TrackpadIndicator", false)
|
||||
end
|
||||
end
|
||||
|
||||
function ViveController:onTouchpadModeChanged(touchpad, touchpadMode)
|
||||
if touchpad ~= self.touchpad then
|
||||
return
|
||||
end
|
||||
|
||||
self.touchpadMode = touchpadMode
|
||||
|
||||
if touchpadMode == Enum.VRTouchpadMode.ABXY then
|
||||
self:setABXYEnabled(true)
|
||||
else
|
||||
self:setABXYEnabled(false)
|
||||
end
|
||||
end
|
||||
|
||||
function ViveController:destroy()
|
||||
if self.onTouchpadModeChangedConn then
|
||||
self.onTouchpadModeChangedConn:disconnect()
|
||||
self.onTouchpadModeChangedConn = nil
|
||||
end
|
||||
self.model:Destroy()
|
||||
end
|
||||
|
||||
return ViveController
|
||||
@@ -0,0 +1,199 @@
|
||||
--Dialog: 3D dialogs for ROBLOX in VR
|
||||
--written by 0xBAADF00D
|
||||
--6/30/2016
|
||||
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
|
||||
local InputService = game:GetService("UserInputService")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Utility = require(RobloxGui.Modules.Settings.Utility)
|
||||
local Panel3D = require(RobloxGui.Modules.VR.Panel3D)
|
||||
|
||||
local DIALOG_BG_COLOR = Color3.new(0.2, 0.2, 0.2)
|
||||
local DIALOG_BG_TRANSPARENCY = 0.3
|
||||
local DIALOG_TITLE_HEIGHT = 66
|
||||
local DIALOG_COLOR_HEIGHT = 8
|
||||
local DIALOG_TITLE_TEXT_SIZE = Enum.FontSize.Size36
|
||||
local DIALOG_CONTENT_PADDING = 48
|
||||
local TITLE_COLOR = Color3.new(1, 1, 1)
|
||||
|
||||
local PANEL_OFFSET_CF = CFrame.new(0, 0, -7) * CFrame.Angles(0, math.pi, 0)
|
||||
|
||||
local emptySelectionImage = Utility:Create "ImageLabel" {
|
||||
Name = "EmptySelectionImage",
|
||||
Image = "",
|
||||
BackgroundTransparency = 1,
|
||||
ImageTransparency = 1
|
||||
}
|
||||
|
||||
local DialogPanel = Panel3D.Get("Dialog")
|
||||
DialogPanel:SetType(Panel3D.Type.Fixed)
|
||||
DialogPanel.localCF = PANEL_OFFSET_CF
|
||||
DialogPanel:SetCanFade(true)
|
||||
|
||||
local dialogPanelAngle = 0
|
||||
local opacityLookup = {}
|
||||
local resetBaseAngle = false
|
||||
local baseAngle = 0
|
||||
|
||||
|
||||
local PANEL_FADE_ANGLE_0, PANEL_FADE_ANGLE_1 = math.rad(37.5), math.rad(44)
|
||||
local PANEL_FADE_RANGE = PANEL_FADE_ANGLE_1 - PANEL_FADE_ANGLE_0
|
||||
local PANEL_REAPPEAR_ANGLE = math.rad(90)
|
||||
|
||||
local function positionDialogPanel(desiredAngle)
|
||||
dialogPanelAngle = desiredAngle
|
||||
local headCF = InputService:GetUserCFrame(Enum.UserCFrame.Head)
|
||||
local headPos = headCF.p
|
||||
DialogPanel.localCF = CFrame.new(headPos) * CFrame.Angles(0, desiredAngle, 0) * PANEL_OFFSET_CF
|
||||
end
|
||||
|
||||
function DialogPanel:CalculateTransparency()
|
||||
local headCF = InputService:GetUserCFrame(Enum.UserCFrame.Head)
|
||||
local headLook = headCF.lookVector * Vector3.new(1, 0, 1)
|
||||
local vertAngle = math.asin(headCF.lookVector.Y)
|
||||
local vectorToPanel = Vector3.new(math.cos(baseAngle + dialogPanelAngle + math.rad(90)), 0, -math.sin(baseAngle + dialogPanelAngle + math.rad(90)))
|
||||
|
||||
if math.abs(vertAngle) > PANEL_FADE_ANGLE_1 then
|
||||
resetBaseAngle = true
|
||||
end
|
||||
|
||||
local angleToPanel = math.acos(headLook:Dot(vectorToPanel))
|
||||
|
||||
return math.min(math.max(0, (angleToPanel - PANEL_FADE_ANGLE_0) / PANEL_FADE_RANGE), 1)
|
||||
end
|
||||
|
||||
|
||||
local function updatePanelPosition()
|
||||
local headCF = InputService:GetUserCFrame(Enum.UserCFrame.Head)
|
||||
local headLook = headCF.lookVector * Vector3.new(1, 0, 1)
|
||||
local headAngle = (math.atan2(-headLook.Z, headLook.X) - math.rad(90)) % math.rad(360)
|
||||
local newPanelAngle = baseAngle + math.floor((headAngle / PANEL_REAPPEAR_ANGLE) + 0.5) * PANEL_REAPPEAR_ANGLE
|
||||
|
||||
if resetBaseAngle then
|
||||
resetBaseAngle = false
|
||||
baseAngle = headAngle
|
||||
end
|
||||
|
||||
positionDialogPanel(newPanelAngle)
|
||||
end
|
||||
|
||||
-- RenderStep update function for the DialogPanel
|
||||
function dialogPanelRenderUpdate()
|
||||
if DialogPanel.transparency == 1 or resetBaseAngle then
|
||||
updatePanelPosition()
|
||||
end
|
||||
|
||||
--update the transparency of gui elements
|
||||
local opacityMult = 1 - DialogPanel.transparency
|
||||
for guiElement, baseOpacity in pairs(opacityLookup) do
|
||||
local transparency = 1 - (baseOpacity * opacityMult)
|
||||
if guiElement:IsA("TextLabel") or guiElement:IsA("TextButton") then
|
||||
guiElement.TextTransparency = transparency
|
||||
elseif guiElement:IsA("ImageLabel") or guiElement:IsA("ImageButton") then
|
||||
guiElement.ImageTransparency = transparency
|
||||
elseif guiElement:IsA("Frame") then
|
||||
guiElement.BackgroundTransparency = transparency
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local DialogQueue = {}
|
||||
|
||||
local currentDescendantConn = nil
|
||||
local lastDialogShown = nil
|
||||
local function updatePanel()
|
||||
local currentDialog = DialogQueue[1]
|
||||
if lastDialogShown and lastDialogShown.content then
|
||||
lastDialogShown.content.Parent = nil
|
||||
end
|
||||
if not currentDialog or not currentDialog.content then
|
||||
DialogPanel:SetVisible(false)
|
||||
opacityLookup = {}
|
||||
if currentDescendantConn then
|
||||
currentDescendantConn:disconnect()
|
||||
currentDescendantConn = nil
|
||||
end
|
||||
else
|
||||
currentDialog.content.Parent = DialogPanel:GetGUI()
|
||||
lastDialogShown = currentDialog
|
||||
|
||||
local contentSize = currentDialog.content.AbsoluteSize
|
||||
DialogPanel:ResizePixels(contentSize.X, contentSize.Y, 100)
|
||||
|
||||
resetBaseAngle = true
|
||||
updatePanelPosition()
|
||||
DialogPanel:SetVisible(true)
|
||||
|
||||
opacityLookup = {}
|
||||
local function search(parent)
|
||||
if parent:IsA("ImageLabel") or parent:IsA("ImageButton") then
|
||||
opacityLookup[parent] = 1 - parent.ImageTransparency
|
||||
elseif parent:IsA("TextLabel") or parent:IsA("TextButton") then
|
||||
opacityLookup[parent] = 1 - parent.TextTransparency
|
||||
elseif parent:IsA("Frame") then
|
||||
opacityLookup[parent] = 1 - parent.BackgroundTransparency
|
||||
end
|
||||
for i, v in pairs(parent:GetChildren()) do
|
||||
search(v)
|
||||
end
|
||||
end
|
||||
search(DialogPanel:GetGUI())
|
||||
if currentDescendantConn then
|
||||
currentDescendantConn:disconnect()
|
||||
currentDescendantConn = nil
|
||||
end
|
||||
currentDescendantConn = DialogPanel:GetGUI().DescendantAdded:connect(function(descendant)
|
||||
search(descendant)
|
||||
end)
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
local Dialog = {}
|
||||
Dialog.__index = Dialog
|
||||
function Dialog.new(content)
|
||||
local self = setmetatable({}, Dialog)
|
||||
self.content = content
|
||||
return self
|
||||
end
|
||||
|
||||
function Dialog:SetContent(guiElement)
|
||||
if not guiElement and self.content then
|
||||
self.content.Parent = nil
|
||||
end
|
||||
self.content = guiElement
|
||||
end
|
||||
|
||||
local renderFuncBound = false
|
||||
function Dialog:Show(shouldTakeover)
|
||||
if not renderFuncBound then
|
||||
renderFuncBound = true
|
||||
RunService:BindToRenderStep("DialogPanel", Enum.RenderPriority.First.Value, dialogPanelRenderUpdate)
|
||||
end
|
||||
if shouldTakeover then
|
||||
table.insert(DialogQueue, 1, self)
|
||||
else
|
||||
table.insert(DialogQueue, self)
|
||||
end
|
||||
updatePanel()
|
||||
end
|
||||
|
||||
function Dialog:Close()
|
||||
for idx, dialog in pairs(DialogQueue) do
|
||||
if dialog == self then
|
||||
table.remove(DialogQueue, idx)
|
||||
break
|
||||
end
|
||||
end
|
||||
if renderFuncBound and #DialogQueue == 0 then
|
||||
renderFuncBound = false
|
||||
RunService:UnbindFromRenderStep("DialogPanel")
|
||||
end
|
||||
updatePanel()
|
||||
end
|
||||
|
||||
return Dialog
|
||||
@@ -0,0 +1,291 @@
|
||||
|
||||
-- Written by SolarCrane
|
||||
|
||||
local HEALTH_BACKGROUND_COLOR = Color3.new(228/255, 236/255, 246/255)
|
||||
local HEALTH_RED_COLOR = Color3.new(255/255, 28/255, 0/255)
|
||||
local HEALTH_YELLOW_COLOR = Color3.new(250/255, 235/255, 0)
|
||||
local HEALTH_GREEN_COLOR = Color3.new(27/255, 252/255, 107/255)
|
||||
|
||||
local MIN_COLOR_POSITION = 0.1
|
||||
local MIN_COLOR = HEALTH_RED_COLOR
|
||||
local MID_COLOR_POSITION = 0.5
|
||||
local MAX_COLOR_POSITION = 0.8
|
||||
local MAX_COLOR = HEALTH_GREEN_COLOR
|
||||
|
||||
local NAME_SPACE = 14
|
||||
|
||||
local PlayersService = game:GetService('Players')
|
||||
local CoreGui = game:GetService('CoreGui')
|
||||
local StarterGui = game:GetService('StarterGui')
|
||||
local UserInputService = game:GetService('UserInputService')
|
||||
|
||||
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
|
||||
|
||||
|
||||
|
||||
local healthbarContainer = Instance.new('Frame')
|
||||
healthbarContainer.Name = 'HealthbarContainer'
|
||||
healthbarContainer.BackgroundTransparency = 1
|
||||
healthbarContainer.Size = UDim2.new(1,0,1,0)
|
||||
|
||||
local healthbarBack = Instance.new('ImageLabel')
|
||||
healthbarBack.ImageColor3 = HEALTH_BACKGROUND_COLOR
|
||||
healthbarBack.BackgroundTransparency = 1
|
||||
healthbarBack.ScaleType = Enum.ScaleType.Slice
|
||||
healthbarBack.SliceCenter = Rect.new(10, 10, 10, 10)
|
||||
healthbarBack.Name = 'HealthbarBack'
|
||||
healthbarBack.Image = 'rbxasset://textures/ui/VR/rectBackgroundWhite.png'
|
||||
healthbarBack.Size = UDim2.new(1,0,0.3,0)
|
||||
healthbarBack.Position = UDim2.new(0,0,0.7,0)
|
||||
healthbarBack.Parent = healthbarContainer
|
||||
|
||||
local healthbarFront = Instance.new('ImageLabel')
|
||||
healthbarFront.ImageColor3 = HEALTH_GREEN_COLOR
|
||||
healthbarFront.BackgroundTransparency = 1
|
||||
healthbarFront.ScaleType = Enum.ScaleType.Slice
|
||||
healthbarFront.SliceCenter = Rect.new(10, 10, 10, 10)
|
||||
healthbarFront.Size = UDim2.new(1, 0, 1, 0)
|
||||
healthbarFront.Position = UDim2.new(0, 0, 0, 0)
|
||||
healthbarFront.Name = 'HealthbarFill'
|
||||
healthbarFront.Image = 'rbxasset://textures/ui/VR/rectBackgroundWhite.png'
|
||||
healthbarFront.Parent = healthbarBack
|
||||
|
||||
local playerName = Instance.new('TextLabel')
|
||||
playerName.Name = 'PlayerName'
|
||||
playerName.BackgroundTransparency = 1
|
||||
playerName.TextColor3 = Color3.new(1, 1, 1)
|
||||
playerName.Text = ''
|
||||
playerName.Font = Enum.Font.SourceSansBold
|
||||
playerName.FontSize = Enum.FontSize.Size24
|
||||
playerName.TextXAlignment = Enum.TextXAlignment.Left
|
||||
playerName.Size = UDim2.new(1, 0, 0, NAME_SPACE)
|
||||
playerName.Parent = healthbarContainer
|
||||
|
||||
|
||||
local function Color3ToVec3(color)
|
||||
return Vector3.new(color.r, color.g, color.b)
|
||||
end
|
||||
|
||||
local function FindChildOfType(object, className)
|
||||
for _, child in pairs(object:GetChildren()) do
|
||||
if child:IsA(className) then
|
||||
return child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local HEALTH_COLOR_TO_POSITION = {
|
||||
[Color3ToVec3(MIN_COLOR)] = MIN_COLOR_POSITION;
|
||||
[Color3ToVec3(HEALTH_YELLOW_COLOR)] = MID_COLOR_POSITION;
|
||||
[Color3ToVec3(MAX_COLOR)] = MAX_COLOR_POSITION;
|
||||
}
|
||||
|
||||
|
||||
local function HealthbarColorTransferFunction(healthPercent)
|
||||
if healthPercent <= MIN_COLOR_POSITION then
|
||||
return MIN_COLOR
|
||||
elseif healthPercent >= MAX_COLOR_POSITION then
|
||||
return MAX_COLOR
|
||||
end
|
||||
|
||||
-- Shepard's Interpolation
|
||||
local numeratorSum = Vector3.new(0,0,0)
|
||||
local denominatorSum = 0
|
||||
for colorSampleValue, samplePoint in pairs(HEALTH_COLOR_TO_POSITION) do
|
||||
local distance = healthPercent - samplePoint
|
||||
if distance == 0 then
|
||||
-- If we are exactly on an existing sample value then we don't need to interpolate
|
||||
return Color3.new(colorSampleValue.x, colorSampleValue.y, colorSampleValue.z)
|
||||
else
|
||||
local wi = 1 / (distance*distance)
|
||||
numeratorSum = numeratorSum + wi * colorSampleValue
|
||||
denominatorSum = denominatorSum + wi
|
||||
end
|
||||
end
|
||||
|
||||
local result = numeratorSum / denominatorSum
|
||||
return Color3.new(result.x, result.y, result.z)
|
||||
end
|
||||
|
||||
local function UpdateHealth(humanoid)
|
||||
local percentHealth = humanoid.Health / humanoid.MaxHealth
|
||||
if percentHealth ~= percentHealth then
|
||||
percentHealth = 1
|
||||
end
|
||||
healthbarFront.ImageColor3 = HealthbarColorTransferFunction(percentHealth)
|
||||
healthbarFront.Size = UDim2.new(percentHealth, 0, 1, 0)
|
||||
end
|
||||
|
||||
local HumanoidChangedConn = nil
|
||||
local HumanoidAncestryChangedConn = nil
|
||||
local function RegisterHumanoid(humanoid)
|
||||
if HumanoidAncestryChangedConn then
|
||||
HumanoidAncestryChangedConn:disconnect()
|
||||
HumanoidAncestryChangedConn = nil
|
||||
end
|
||||
if HumanoidChangedConn then
|
||||
HumanoidChangedConn:disconnect()
|
||||
HumanoidChangedConn = nil
|
||||
end
|
||||
|
||||
if humanoid then
|
||||
HumanoidAncestryChangedConn = humanoid.AncestryChanged:connect(function(child, parent)
|
||||
local player = PlayersService.LocalPlayer
|
||||
if child == humanoid and (not player or parent ~= player.Character) then
|
||||
RegisterHumanoid(nil)
|
||||
end
|
||||
end)
|
||||
HumanoidChangedConn = humanoid.HealthChanged:connect(function() UpdateHealth(humanoid) end)
|
||||
UpdateHealth(humanoid)
|
||||
end
|
||||
end
|
||||
|
||||
local function OnCharacterChildAdded(child)
|
||||
local player = PlayersService.LocalPlayer
|
||||
if player and child.Parent == player.Character and child:IsA('Humanoid') then
|
||||
RegisterHumanoid(child)
|
||||
end
|
||||
end
|
||||
|
||||
local CharacterChildAddedConn = nil
|
||||
local function OnCharacterAdded(character)
|
||||
local humanoid = FindChildOfType(character, 'Humanoid')
|
||||
if humanoid then
|
||||
RegisterHumanoid(humanoid)
|
||||
end
|
||||
|
||||
if CharacterChildAddedConn then
|
||||
CharacterChildAddedConn:disconnect()
|
||||
CharacterChildAddedConn = nil
|
||||
end
|
||||
CharacterChildAddedConn = character.ChildAdded:connect(OnCharacterChildAdded)
|
||||
end
|
||||
|
||||
local function OnPlayerAdded(player)
|
||||
playerName.Text = player.Name
|
||||
|
||||
player.CharacterAdded:connect(OnCharacterAdded)
|
||||
if player.Character then
|
||||
OnCharacterAdded(player.Character)
|
||||
end
|
||||
end
|
||||
|
||||
if PlayersService.LocalPlayer then
|
||||
OnPlayerAdded(PlayersService.LocalPlayer)
|
||||
else
|
||||
spawn(function()
|
||||
while not PlayersService.LocalPlayer do
|
||||
PlayersService.ChildAdded:wait()
|
||||
end
|
||||
OnPlayerAdded(PlayersService.LocalPlayer)
|
||||
end)
|
||||
end
|
||||
|
||||
local Healthbar = {}
|
||||
|
||||
Healthbar.ModuleName = "Healthbar"
|
||||
Healthbar.KeepVRTopbarOpen = false
|
||||
Healthbar.VRIsExclusive = false
|
||||
Healthbar.VRClosesNonExclusive = false
|
||||
|
||||
local CoreGuiChangedConn, VRModuleOpenedConn, VRModuleClosedConn;
|
||||
local function OnVREnabled(prop)
|
||||
if prop == "VREnabled" then
|
||||
if UserInputService.VREnabled then
|
||||
local VRHub = require(RobloxGui.Modules.VR.VRHub)
|
||||
local Panel3D = require(RobloxGui.Modules.VR.Panel3D)
|
||||
|
||||
local HealthbarPanel = Panel3D.Get("Healthbar")
|
||||
HealthbarPanel:ResizeStuds(1.5, 0.25, 128)
|
||||
HealthbarPanel:SetType(Panel3D.Type.Fixed, { CFrame = CFrame.new(0, 0, -5) })
|
||||
HealthbarPanel:SetVisible(true)
|
||||
|
||||
|
||||
function HealthbarPanel:PreUpdate(cameraCF, cameraRenderCF, userHeadCF, lookRay)
|
||||
local relativePanel = Panel3D.Get("Backpack") or Panel3D.Get("Topbar3D")
|
||||
local topbarPanel = Panel3D.Get("Topbar3D")
|
||||
if relativePanel and topbarPanel then
|
||||
local panelOriginCF = relativePanel.localCF or CFrame.new()
|
||||
-- Line up the Healthbar with the backpack icons, which are set 0.11 inwards
|
||||
self.localCF = panelOriginCF * CFrame.new(math.max(topbarPanel.width, relativePanel.width)/2 - HealthbarPanel.width/2 - 0.11, 0.25, 0.1)
|
||||
end
|
||||
end
|
||||
|
||||
function HealthbarPanel:CalculateTransparency()
|
||||
local backpackPanel = Panel3D.Get("Backpack")
|
||||
local topbarPanel = Panel3D.Get("Topbar3D")
|
||||
local transparency = math.min(
|
||||
backpackPanel and backpackPanel:IsVisible() and backpackPanel:CalculateTransparency() or 1,
|
||||
topbarPanel and topbarPanel:IsVisible() and topbarPanel:CalculateTransparency() or 1)
|
||||
|
||||
healthbarBack.ImageTransparency = transparency
|
||||
healthbarFront.ImageTransparency = transparency
|
||||
playerName.TextTransparency = transparency
|
||||
|
||||
return transparency
|
||||
end
|
||||
|
||||
local OtherPanelOpen = false
|
||||
local function UpdateExclusivePanelOpen()
|
||||
for _, openModule in pairs(VRHub:GetOpenedModules()) do
|
||||
if openModule.VRIsExclusive then
|
||||
OtherPanelOpen = true
|
||||
return
|
||||
end
|
||||
end
|
||||
OtherPanelOpen = false
|
||||
end
|
||||
|
||||
local function CalculateVisibility()
|
||||
return StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Health) and
|
||||
not OtherPanelOpen
|
||||
end
|
||||
|
||||
|
||||
CoreGuiChangedConn = StarterGui.CoreGuiChangedSignal:connect(function()
|
||||
HealthbarPanel:SetVisible(CalculateVisibility())
|
||||
end)
|
||||
|
||||
VRModuleOpenedConn = VRHub.ModuleOpened.Event:connect(function(moduleName)
|
||||
UpdateExclusivePanelOpen()
|
||||
HealthbarPanel:SetVisible(CalculateVisibility())
|
||||
end)
|
||||
VRModuleClosedConn = VRHub.ModuleClosed.Event:connect(function(moduleName)
|
||||
UpdateExclusivePanelOpen()
|
||||
HealthbarPanel:SetVisible(CalculateVisibility())
|
||||
end)
|
||||
|
||||
-- Initialize OtherPanelOpen variable
|
||||
UpdateExclusivePanelOpen()
|
||||
HealthbarPanel:SetVisible(CalculateVisibility())
|
||||
healthbarContainer.Parent = HealthbarPanel:GetGUI()
|
||||
|
||||
VRHub:RegisterModule(Healthbar)
|
||||
else
|
||||
if CoreGuiChangedConn then
|
||||
CoreGuiChangedConn:disconnect()
|
||||
CoreGuiChangedConn = nil
|
||||
end
|
||||
if VRModuleOpenedConn then
|
||||
VRModuleOpenedConn:disconnect()
|
||||
VRModuleOpenedConn = nil
|
||||
end
|
||||
if VRModuleClosedConn then
|
||||
VRModuleClosedConn:disconnect()
|
||||
VRModuleClosedConn = nil
|
||||
end
|
||||
healthbarContainer.Parent = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
UserInputService.Changed:connect(OnVREnabled)
|
||||
if UserInputService.VREnabled then
|
||||
OnVREnabled("VREnabled")
|
||||
end
|
||||
|
||||
return Healthbar
|
||||
@@ -0,0 +1,670 @@
|
||||
--!nocheck
|
||||
|
||||
--LaserPointer.lua
|
||||
--Implements the visual part of the VR laser pointer
|
||||
--Written by Kyle, September 2016
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
local Players = game:GetService("Players")
|
||||
local GuiService = game:GetService("GuiService")
|
||||
local VRService = game:GetService("VRService")
|
||||
local Utility = require(RobloxGui.Modules.Settings.Utility)
|
||||
|
||||
local LocalPlayer = Players.LocalPlayer
|
||||
while not LocalPlayer do
|
||||
Players.Changed:wait()
|
||||
LocalPlayer = Players.LocalPlayer
|
||||
end
|
||||
|
||||
local gamma, invGamma = 2.2, 1/2.2
|
||||
local function fromLinearRGB(color)
|
||||
return Color3.new(color.r ^ gamma, color.g ^ gamma, color.b ^ gamma)
|
||||
end
|
||||
local function toLinearRGB(color)
|
||||
return Color3.new(color.r ^ invGamma, color.g ^ invGamma, color.b ^ invGamma)
|
||||
end
|
||||
|
||||
local function addPartsToGame(...)
|
||||
local parts = {...}
|
||||
local container = GuiService.CoreEffectFolder
|
||||
|
||||
--The container might not be ready yet.
|
||||
if not container then
|
||||
coroutine.wrap(function()
|
||||
--Wait until the container is ready, then add the parts to it.
|
||||
while GuiService.Changed:wait() ~= "CoreEffectFolder" and GuiService.CoreEffectFolder == nil do end
|
||||
for _, part in pairs(parts) do
|
||||
part.Parent = container
|
||||
end
|
||||
end)()
|
||||
else
|
||||
--The container is ready, no waiting necessary.
|
||||
for _, part in pairs(parts) do
|
||||
part.Parent = container
|
||||
end
|
||||
end
|
||||
end
|
||||
local function removePartsFromGame(...)
|
||||
local parts = {...}
|
||||
for _, part in pairs(parts) do
|
||||
part.Parent = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function getLocalHumanoid()
|
||||
local character = LocalPlayer.Character
|
||||
if not character then
|
||||
return
|
||||
end
|
||||
|
||||
for _, child in pairs(character:GetChildren()) do
|
||||
if child:IsA("Humanoid") then
|
||||
return child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function applyExpCurve(x, exp)
|
||||
local y = x ^ exp
|
||||
if y ~= y then
|
||||
y = math.abs(x) ^ exp
|
||||
end
|
||||
return y
|
||||
end
|
||||
|
||||
local HEAD_MOUNT_OFFSET = Vector3.new(0.5, 0.5, 0)
|
||||
local HEAD_MOUNT_THICKNESS_MULTIPLIER = 0.25
|
||||
|
||||
local BUTTON_LONG_PRESS_TIME = 0.75
|
||||
|
||||
local LaserPointerMode = {
|
||||
Disabled = 0,
|
||||
Pointer = 1,
|
||||
Navigation = 2,
|
||||
Hidden = 3
|
||||
}
|
||||
|
||||
--Teleport visual configuration
|
||||
local TELEPORT = {
|
||||
MODE_ENABLED = true,
|
||||
|
||||
ARC_COLOR_GOOD = fromLinearRGB(Color3.fromRGB(0, 162, 255)),
|
||||
ARC_COLOR_BAD = fromLinearRGB(Color3.fromRGB(253, 68, 72)),
|
||||
ARC_THICKNESS = 0.025,
|
||||
|
||||
PLOP_GOOD = "rbxasset://textures/ui/VR/VRPointerDiscBlue.png",
|
||||
PLOP_BAD = "rbxasset://textures/ui/VR/VRPointerDiscRed.png",
|
||||
PLOP_BALL_COLOR_GOOD = BrickColor.new("Bright green"),
|
||||
PLOP_BALL_COLOR_BAD = BrickColor.new("Bright red"),
|
||||
PLOP_BALL_SIZE = 0.5,
|
||||
PLOP_SIZE = 2,
|
||||
PLOP_PULSE_MIN_SIZE = 0,
|
||||
PLOP_PULSE_MAX_SIZE = 2,
|
||||
|
||||
MAX_VALID_DISTANCE = 100,
|
||||
|
||||
BUTTON_DOWN_THRESHOLD = 0.95,
|
||||
BUTTON_UP_THRESHOLD = 0.5,
|
||||
|
||||
MIN_VELOCITY = 10,
|
||||
RANGE_T_EXP = 2,
|
||||
G = 10, -- Gravity constant for parabola
|
||||
|
||||
PULSE_DURATION = 0.8,
|
||||
PULSE_PERIOD = 1,
|
||||
PULSE_EXP = 2,
|
||||
PULSE_SIZE_0 = 0.25,
|
||||
PULSE_SIZE_1 = 2,
|
||||
|
||||
BALL_WAVE_PERIOD = 2,
|
||||
BALL_WAVE_AMPLITUDE = 0.5,
|
||||
BALL_WAVE_START = 0.25,
|
||||
BALL_WAVE_EXP = 0.8,
|
||||
|
||||
FLOOR_OFFSET = 4.5,
|
||||
|
||||
FADE_OUT_DURATION = 0.125,
|
||||
FADE_IN_DURATION = 0.125,
|
||||
|
||||
TRANSITION_DURATION = 0.25,
|
||||
TRANSITION_FUNC = Utility:GetEaseInOutQuad(),
|
||||
}
|
||||
|
||||
local LASER = {
|
||||
MODE_ENABLED = true,
|
||||
|
||||
ARC_COLOR_GOOD = TELEPORT.ARC_COLOR_GOOD,
|
||||
ARC_COLOR_BAD = TELEPORT.ARC_COLOR_BAD,
|
||||
ARC_THICKNESS = 0.02,
|
||||
|
||||
MAX_DISTANCE = 500,
|
||||
|
||||
G = 0, -- Gravity constant for parabola; in this case we want a laser/straight line
|
||||
|
||||
--Couldn't figure out a good name for this. This is the maximum angle that the parabola's hit point
|
||||
--can be from the laser's hit point when switching to laser pointer mode solely from the parabola hitting
|
||||
--a gui part.
|
||||
SWITCH_AIM_THRESHOLD = math.rad(15),
|
||||
|
||||
TRANSITION_DURATION = 0.075,
|
||||
TRANSITION_FUNC = Utility:GetEaseInOutQuad()
|
||||
}
|
||||
|
||||
local zeroVector2, identityVector2 = Vector2.new(0, 0), Vector2.new(1, 1)
|
||||
local zeroVector3, identityVector3 = Vector3.new(0, 0, 0), Vector3.new(1, 1, 1)
|
||||
local flattenMask = Vector3.new(1, 0, 1) --flattens a direction vector when multiplied by removing the vertical component
|
||||
local minimumPartSize = Vector3.new(0.2, 0.2, 0.2)
|
||||
local identity = CFrame.new()
|
||||
|
||||
local aimableStates = {
|
||||
[Enum.HumanoidStateType.Running] = true,
|
||||
[Enum.HumanoidStateType.RunningNoPhysics] = true,
|
||||
[Enum.HumanoidStateType.None] = true
|
||||
}
|
||||
|
||||
local LaserPointer = {}
|
||||
LaserPointer.__index = LaserPointer
|
||||
LaserPointer.Mode = LaserPointerMode
|
||||
|
||||
function LaserPointer.new()
|
||||
local self = setmetatable({}, LaserPointer)
|
||||
|
||||
self.mode = LaserPointerMode.Disabled
|
||||
self.lastMode = self.mode
|
||||
|
||||
self.inputUserCFrame = Enum.UserCFrame.RightHand
|
||||
self.equippedTool = false
|
||||
|
||||
self.lastLaserModeHit = tick()
|
||||
|
||||
self.guiMenuIsOpen = false
|
||||
self.externalForcePointer = false
|
||||
|
||||
self.navHitPoint = zeroVector3
|
||||
self.navHitNormal = Vector3.new(0, 1, 0)
|
||||
self.navHitPart = nil
|
||||
|
||||
self.navigationIsValid = false
|
||||
self.lastNavigationValidityChangeTime = tick()
|
||||
self.plopBallBounceStart = tick()
|
||||
|
||||
self.buttonPressStart = 0
|
||||
|
||||
do --Create the instances that make up the Laser Pointer
|
||||
self.parabola = Utility:Create("ParabolaAdornment") {
|
||||
Name = "LaserPointerParabola",
|
||||
Parent = CoreGui,
|
||||
A = -1,
|
||||
B = 2,
|
||||
C = 0,
|
||||
Color3 = TELEPORT.COLOR_GOOD,
|
||||
Thickness = TELEPORT.ARC_THICKNESS,
|
||||
Visible = false
|
||||
}
|
||||
|
||||
self.originPart = Utility:Create("Part") {
|
||||
Name = "LaserPointerOrigin",
|
||||
Anchored = true,
|
||||
CanCollide = false,
|
||||
TopSurface = Enum.SurfaceType.SmoothNoOutlines,
|
||||
BottomSurface = Enum.SurfaceType.SmoothNoOutlines,
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
Size = minimumPartSize,
|
||||
Transparency = 1
|
||||
}
|
||||
self.parabola.Adornee = self.originPart
|
||||
|
||||
self.plopPart = Utility:Create("Part") {
|
||||
Name = "LaserPointerTeleportPlop",
|
||||
Anchored = true,
|
||||
CanCollide = false,
|
||||
Size = minimumPartSize,
|
||||
Transparency = 1
|
||||
}
|
||||
self.plopBall = Utility:Create("Part") {
|
||||
Name = "LaserPointerTeleportPlopBall",
|
||||
Anchored = true,
|
||||
CanCollide = false,
|
||||
TopSurface = Enum.SurfaceType.SmoothNoOutlines,
|
||||
BottomSurface = Enum.SurfaceType.SmoothNoOutlines,
|
||||
Material = Enum.Material.Neon,
|
||||
BrickColor = TELEPORT.PLOP_BALL_COLOR_GOOD,
|
||||
Shape = Enum.PartType.Ball,
|
||||
Size = identityVector3 * TELEPORT.PLOP_BALL_SIZE
|
||||
}
|
||||
self.plopAdorn = Utility:Create("ImageHandleAdornment") {
|
||||
Name = "LaserPointerTeleportPlopAdorn",
|
||||
Parent = self.plopPart,
|
||||
Adornee = self.plopPart,
|
||||
Size = identityVector2 * TELEPORT.PLOP_SIZE,
|
||||
Image = TELEPORT.PLOP_GOOD
|
||||
}
|
||||
self.plopAdornPulse = Utility:Create("ImageHandleAdornment") {
|
||||
Name = "LaserPointerTeleportPlopAdornPulse",
|
||||
Parent = self.plopPart,
|
||||
Adornee = self.plopPart,
|
||||
Size = zeroVector2,
|
||||
Image = TELEPORT.PLOP_GOOD,
|
||||
Transparency = 0.5
|
||||
}
|
||||
end
|
||||
|
||||
do --Event connections and final setup
|
||||
GuiService.MenuOpened:connect(function()
|
||||
self.guiMenuIsOpen = true
|
||||
end)
|
||||
GuiService.MenuClosed:connect(function()
|
||||
self.guiMenuIsOpen = false
|
||||
end)
|
||||
|
||||
self.inputUserCFrame = VRService.GuiInputUserCFrame
|
||||
VRService.Changed:connect(function(prop)
|
||||
if prop == "GuiInputUserCFrame" then
|
||||
self.inputUserCFrame = VRService.GuiInputUserCFrame
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
self:onModeChanged(self.mode)
|
||||
|
||||
self:updateInputUserCFrame()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function LaserPointer.hasAnyHandController()
|
||||
return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) or
|
||||
VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)
|
||||
end
|
||||
|
||||
function LaserPointer.getModeName(mode)
|
||||
for name, value in pairs(LaserPointerMode) do
|
||||
if mode == value then
|
||||
return name
|
||||
end
|
||||
end
|
||||
|
||||
return "unknown"
|
||||
end
|
||||
|
||||
function LaserPointer:updateInputUserCFrame()
|
||||
if VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) then
|
||||
VRService.GuiInputUserCFrame = Enum.UserCFrame.RightHand
|
||||
elseif VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand) then
|
||||
VRService.GuiInputUserCFrame = Enum.UserCFrame.LeftHand
|
||||
else
|
||||
VRService.GuiInputUserCFrame = Enum.UserCFrame.Head
|
||||
end
|
||||
end
|
||||
|
||||
function LaserPointer:onModeChanged(newMode)
|
||||
self:updateInputUserCFrame()
|
||||
|
||||
--Disabled mode
|
||||
if newMode == LaserPointerMode.Disabled or newMode == LaserPointerMode.Hidden then
|
||||
removePartsFromGame(self.originPart, self.plopPart, self.plopBall)
|
||||
self.parabola.Visible = false
|
||||
self:setNavigationActionEnabled(false)
|
||||
--Pointer mode
|
||||
elseif newMode == LaserPointerMode.Pointer then
|
||||
addPartsToGame(self.originPart)
|
||||
removePartsFromGame(self.plopPart, self.plopBall)
|
||||
self.parabola.Visible = true
|
||||
self:setNavigationActionEnabled(false)
|
||||
--Navigation mode
|
||||
elseif newMode == LaserPointerMode.Navigation then
|
||||
addPartsToGame(self.originPart, self.plopPart, self.plopBall)
|
||||
self.parabola.Visible = true
|
||||
self:setNavigationActionEnabled(true)
|
||||
end
|
||||
end
|
||||
|
||||
function LaserPointer:setMode(mode)
|
||||
if mode == self.mode then
|
||||
return
|
||||
end
|
||||
|
||||
local oldMode = self.mode
|
||||
self.mode = mode
|
||||
self.lastMode = oldMode
|
||||
self:onModeChanged(mode)
|
||||
end
|
||||
|
||||
function LaserPointer:getMode()
|
||||
return self.mode
|
||||
end
|
||||
|
||||
local cosMax = math.cos(math.pi/4)
|
||||
local sinMax = math.sin(math.pi/4)
|
||||
function LaserPointer:calculateLaunchVelocity(gravity, desiredRange, height)
|
||||
--return math.sqrt(desiredRange * gravity) / math.sqrt(math.sin(2*math.pi/4))
|
||||
--This calculates a launch velocity for an imaginary projectile that will start at y=height, travel desiredRange
|
||||
--in the x direction until it hits y=0. This is only reached when the projectile is launched at the optimal
|
||||
--launch angle, 45 degrees elevation. Anything above or below that will fall short, which is desired.
|
||||
--See https://en.wikipedia.org/wiki/Range_of_a_projectile for a breakdown of this function.
|
||||
return -(math.sqrt(gravity / cosMax)*desiredRange) / (math.sqrt(2*height*cosMax+2*desiredRange*sinMax))
|
||||
end
|
||||
|
||||
function LaserPointer:isHeadMounted()
|
||||
return self.inputUserCFrame == Enum.UserCFrame.Head
|
||||
end
|
||||
|
||||
function LaserPointer:shouldForcePointer()
|
||||
return self.externalForcePointer or self.guiMenuIsOpen
|
||||
end
|
||||
|
||||
function LaserPointer:setForcePointer(force)
|
||||
self.externalForcePointer = force
|
||||
end
|
||||
|
||||
function LaserPointer:getNavigationOrigin()
|
||||
local humanoid = getLocalHumanoid()
|
||||
if not humanoid then return end
|
||||
|
||||
local rootPart = humanoid.Torso
|
||||
if not rootPart then return end
|
||||
|
||||
local hipHeight = humanoid.HipHeight
|
||||
if humanoid.RigType == Enum.HumanoidRigType.R6 then
|
||||
hipHeight = 2
|
||||
end
|
||||
local rootPartOffset = rootPart.Size.Y / 2
|
||||
return rootPart.Position + Vector3.new(0, -rootPartOffset - hipHeight, 0)
|
||||
end
|
||||
|
||||
function LaserPointer:horzDistanceFromCharacter(point)
|
||||
local character = LocalPlayer.Character
|
||||
if not character then
|
||||
return math.huge
|
||||
end
|
||||
local rootPart = character:FindFirstChild("HumanoidRootPart")
|
||||
if not rootPart then
|
||||
return math.huge
|
||||
end
|
||||
|
||||
return ((rootPart:GetRenderCFrame().p - point) * flattenMask).magnitude
|
||||
end
|
||||
|
||||
|
||||
function LaserPointer:onNavigateAction(actionName, inputState, inputObj)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
VRService:RequestNavigation(CFrame.new(self.navHitPoint, self.navHitPoint + self.navHitNormal) * CFrame.Angles(math.pi/2, 0, 0), self.inputUserCFrame)
|
||||
end
|
||||
end
|
||||
|
||||
function LaserPointer:setNavigationActionEnabled(enabled)
|
||||
if enabled then
|
||||
ContextActionService:BindCoreAction("LaserPointerNavigate", function(...) self:onNavigateAction(...) end, false, Enum.KeyCode.ButtonA)
|
||||
else
|
||||
ContextActionService:UnbindCoreAction("LaserPointerNavigate")
|
||||
end
|
||||
end
|
||||
|
||||
function LaserPointer:setArcLaunchParams(launchAngle, launchVelocity, gravity, desiredRange)
|
||||
local velocityX = math.cos(launchAngle) * launchVelocity
|
||||
local velocityY = math.sin(launchAngle) * launchVelocity
|
||||
|
||||
--don't let velocityX = 0 or we get a divide-by-zero and bad things happen
|
||||
if velocityX == 0 then
|
||||
velocityX = 1e-6
|
||||
end
|
||||
|
||||
self.parabola.A = -(0.5 * gravity) * (1 / (velocityX ^ 2))
|
||||
self.parabola.B = velocityY / velocityX
|
||||
self.parabola.C = 0
|
||||
self.parabola.Range = desiredRange * 1.5
|
||||
end
|
||||
|
||||
function LaserPointer:renderAsParabola(origin, lookDir)
|
||||
local lookFlat = lookDir * flattenMask
|
||||
self.originPart.CFrame = CFrame.new(origin, origin + lookFlat) * CFrame.Angles(0, math.pi / 2, 0)
|
||||
end
|
||||
|
||||
function LaserPointer:renderAsLaser(laserOriginPos, laserEndpoint)
|
||||
self.originPart.CFrame = CFrame.new(laserOriginPos, laserEndpoint) * CFrame.Angles(0, math.pi / 2, 0)
|
||||
self.parabola.A = 0
|
||||
self.parabola.B = 1e-6
|
||||
self.parabola.C = 0
|
||||
self.parabola.Range = (laserEndpoint - laserOriginPos).magnitude
|
||||
end
|
||||
|
||||
function LaserPointer:getArcHit(pos, look, ignore)
|
||||
if self.parabola.A == 0 then
|
||||
--Just skip the parabola since this is effectively a line without the x^2 term
|
||||
return self:getLaserHit(pos, look, ignore)
|
||||
end
|
||||
|
||||
self:renderAsParabola(pos, look)
|
||||
|
||||
local parabHitPart, parabHitPoint, parabHitNormal, _, t = self.parabola:FindPartOnParabola(ignore)
|
||||
return parabHitPart, parabHitPoint, parabHitNormal, t
|
||||
end
|
||||
|
||||
function LaserPointer:getLaserHit(pos, look, ignore)
|
||||
local ray = Ray.new(pos, look * LASER.MAX_DISTANCE)
|
||||
local laserHitPart, laserHitPoint, laserHitNormal, laserHitMaterial = workspace:FindPartOnRayWithIgnoreList(ray, ignore)
|
||||
local t = (laserHitPoint - pos).magnitude / LASER.MAX_DISTANCE
|
||||
return laserHitPart, laserHitPoint, laserHitNormal, t
|
||||
end
|
||||
|
||||
function LaserPointer:canNavigateTo(part, point, normal)
|
||||
local character = LocalPlayer.Character
|
||||
if not character then
|
||||
return false
|
||||
end
|
||||
local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
|
||||
if not humanoidRootPart then
|
||||
return false
|
||||
end
|
||||
|
||||
--Check if a part was hit
|
||||
if not part then
|
||||
return false
|
||||
end
|
||||
|
||||
--Check if the surface hit is upside down or not
|
||||
if normal.Y < -1e-6 then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function LaserPointer:checkHeadMountedMode(laserHitPart)
|
||||
if self:shouldForcePointer() then
|
||||
self:setMode(LaserPointerMode.Pointer)
|
||||
return
|
||||
end
|
||||
|
||||
local coreGuiPartContainer = GuiService.CoreGuiFolder
|
||||
if laserHitPart and laserHitPart:IsDescendantOf(coreGuiPartContainer) then
|
||||
self:setMode(LaserPointerMode.Pointer)
|
||||
else
|
||||
self:setMode(LaserPointerMode.Navigation)
|
||||
end
|
||||
end
|
||||
|
||||
function LaserPointer:checkMode(originPos, parabHitPart, parabHitPoint, laserHitPart, laserHitPoint)
|
||||
if self:shouldForcePointer() then
|
||||
self:setMode(LaserPointerMode.Pointer)
|
||||
return
|
||||
end
|
||||
|
||||
local angleBetween = 0
|
||||
|
||||
--only check the angle between these two if the hit points aren't exactly equal
|
||||
if parabHitPoint ~= laserHitPoint then
|
||||
local toParabHit = (parabHitPoint - originPos).unit
|
||||
local toLaserHit = (laserHitPoint - originPos).unit
|
||||
angleBetween = math.acos(toParabHit:Dot(toLaserHit))
|
||||
end
|
||||
|
||||
--todo: update this when we move the parts; also update it so that it can work with user surfaceguis
|
||||
--we may need to be more creative about that since we can't easily tell if a part has a surfacegui from Lua
|
||||
local coreGuiPartContainer = GuiService.CoreGuiFolder
|
||||
local laserHitGui = laserHitPart and laserHitPart:IsDescendantOf(coreGuiPartContainer)
|
||||
local parabHitGui = laserHitGui
|
||||
--only check parab hit part if it's not the same as the laser hit part
|
||||
if parabHitPart ~= laserHitPart then
|
||||
parabHitGui = parabHitPart and parabHitPart:IsDescendantOf(coreGuiPartContainer)
|
||||
end
|
||||
|
||||
local newMode = self.mode
|
||||
|
||||
if laserHitGui then
|
||||
self.lastLaserModeHit = tick()
|
||||
end
|
||||
|
||||
--If we are navigating and the parabola hits a gui part, we switch to laser pointer if the laser pointer is close enough
|
||||
--If we are navigating and the laser hits a gui part, we switch to laser pointer regardless of where the parabola is
|
||||
if self.mode ~= LaserPointerMode.Pointer and laserHitGui then
|
||||
if self:isHeadMounted() or ((parabHitGui and angleBetween < LASER.SWITCH_AIM_THRESHOLD) or laserHitGui) then
|
||||
newMode = LaserPointerMode.Pointer
|
||||
end
|
||||
end
|
||||
|
||||
--If we are in laser pointer mode but neither the parabola nor the laser hit any gui parts, we switch back to navigation or hidden mode.
|
||||
if self.mode == LaserPointerMode.Pointer and not laserHitGui and not parabHitGui and tick() - self.lastLaserModeHit > 0.2 then
|
||||
if self.lastMode == LaserPointerMode.Navigation or self.lastMode == LaserPointerMode.Hidden then
|
||||
newMode = self.lastMode
|
||||
end
|
||||
end
|
||||
|
||||
self:setMode(newMode)
|
||||
end
|
||||
|
||||
function LaserPointer:setNavigationValidState(valid)
|
||||
if valid == self.navigationIsValid then return end
|
||||
self.navigationIsValid = valid
|
||||
self.lastNavigationValidityChangeTime = tick()
|
||||
|
||||
if valid then
|
||||
self.plopBallBounceStart = tick()
|
||||
self.parabola.Color3 = TELEPORT.ARC_COLOR_GOOD
|
||||
self.plopAdorn.Visible = true
|
||||
self.plopAdorn.Image = TELEPORT.PLOP_GOOD
|
||||
self.plopAdornPulse.Visible = true
|
||||
self.plopAdornPulse.Image = TELEPORT.PLOP_GOOD
|
||||
self.plopBall.BrickColor = TELEPORT.PLOP_BALL_COLOR_GOOD
|
||||
else
|
||||
self.parabola.Color3 = TELEPORT.ARC_COLOR_BAD
|
||||
self.plopAdorn.Visible = false
|
||||
self.plopAdorn.Image = TELEPORT.PLOP_BAD
|
||||
self.plopAdornPulse.Visible = false
|
||||
self.plopAdornPulse.Image = TELEPORT.PLOP_BAD
|
||||
self.plopBall.BrickColor = TELEPORT.PLOP_BALL_COLOR_BAD
|
||||
end
|
||||
end
|
||||
|
||||
function LaserPointer:updateNavPlop(parabHitPoint, parabHitNormal)
|
||||
local now = tick() - self.plopBallBounceStart
|
||||
|
||||
local plopCF = CFrame.new(parabHitPoint, parabHitPoint + parabHitNormal)
|
||||
|
||||
local ballHeight = 0
|
||||
if self.navigationIsValid then
|
||||
local ballWave = applyExpCurve(math.sin((now * 2 * math.pi) / TELEPORT.BALL_WAVE_PERIOD), TELEPORT.BALL_WAVE_EXP)
|
||||
ballHeight = TELEPORT.BALL_WAVE_START + (ballWave * TELEPORT.BALL_WAVE_AMPLITUDE)
|
||||
end
|
||||
|
||||
self.plopPart.CFrame = plopCF
|
||||
self.plopBall.CFrame = plopCF * CFrame.new(0, 0, -ballHeight)
|
||||
|
||||
--Handle the pulse animation
|
||||
--We're scheduling it to begin every TELEPORT.PULSE_PERIOD seconds
|
||||
--and the animation runs for TELEPORT.PULSE_DURATION seconds. TELEPORT.PULSE_EXP
|
||||
--affects the growth rate of the pulse size; ^2 is a good look, starts slow and accelerates.
|
||||
local timeSincePulseStart = now % TELEPORT.PULSE_PERIOD
|
||||
if timeSincePulseStart > 0 then
|
||||
local pulseSize = timeSincePulseStart / TELEPORT.PULSE_DURATION
|
||||
if pulseSize < 1 then
|
||||
self.plopAdornPulse.Visible = true
|
||||
self.plopAdornPulse.Size = identityVector2 * (TELEPORT.PULSE_SIZE_0 + applyExpCurve(pulseSize, TELEPORT.PULSE_EXP) * (TELEPORT.PULSE_SIZE_1 - TELEPORT.PULSE_SIZE_0))
|
||||
self.plopAdornPulse.Transparency = 0.5 + (pulseSize * 0.5)
|
||||
else
|
||||
self.plopAdornPulse.Visible = false
|
||||
self.plopAdornPulse.Size = zeroVector2
|
||||
self.pulseStartTime = tick() + TELEPORT.PULSE_PERIOD
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function LaserPointer:updateNavigationMode(hitPoint, hitNormal, hitPart)
|
||||
self.navHitPoint = hitPoint
|
||||
self.navHitNormal = hitNormal
|
||||
self.navHitPart = hitPart
|
||||
|
||||
self:updateNavPlop(hitPoint, hitNormal)
|
||||
self:setNavigationValidState(self:canNavigateTo(self.navHitPart, self.navHitPoint, self.navHitNormal))
|
||||
end
|
||||
|
||||
function LaserPointer:update(dt)
|
||||
if self.mode == LaserPointerMode.Disabled then
|
||||
return
|
||||
end
|
||||
|
||||
local humanoid = getLocalHumanoid()
|
||||
|
||||
local ignore = { LocalPlayer.Character or LocalPlayer, self.originPart, self.plopPart, self.plopBall, GuiService.CoreEffectFolder }
|
||||
local cameraSpace = workspace.CurrentCamera.CFrame
|
||||
local thickness0, thickness1 = LASER.ARC_THICKNESS, TELEPORT.ARC_THICKNESS
|
||||
local gravity0, gravity1 = LASER.G, TELEPORT.G
|
||||
|
||||
if self:isHeadMounted() then
|
||||
self.parabola.Thickness = LASER.ARC_THICKNESS * HEAD_MOUNT_THICKNESS_MULTIPLIER
|
||||
|
||||
--cast ray from center of camera, then render laser going from offset point to hit point
|
||||
local originCFrame = cameraSpace * VRService:GetUserCFrame(Enum.UserCFrame.Head)
|
||||
local originPos, originLook = originCFrame.p, originCFrame.lookVector
|
||||
|
||||
local laserHitPart, laserHitPoint, laserHitNormal, laserHitT = self:getLaserHit(originPos, originLook, ignore)
|
||||
self:checkHeadMountedMode(laserHitPart)
|
||||
|
||||
--we actually want to render the laser from an offset from the head though
|
||||
local offsetPosition = originCFrame:pointToWorldSpace(HEAD_MOUNT_OFFSET * workspace.CurrentCamera.HeadScale)
|
||||
self:renderAsLaser(offsetPosition, laserHitPoint)
|
||||
|
||||
if self.mode == LaserPointerMode.Navigation then
|
||||
self:updateNavigationMode(laserHitPoint, laserHitNormal, laserHitPart, laserHitPoint)
|
||||
else
|
||||
self.parabola.Color3 = LASER.ARC_COLOR_GOOD
|
||||
end
|
||||
else
|
||||
local originCFrame = cameraSpace * VRService:GetUserCFrame(self.inputUserCFrame)
|
||||
local originPos, originLook = originCFrame.p, originCFrame.lookVector
|
||||
|
||||
local gravity = TELEPORT.G
|
||||
|
||||
local launchAngle = math.asin(originLook.Y)
|
||||
local offsetHeight = humanoid and originPos.Y - self:getNavigationOrigin().Y or 0
|
||||
|
||||
local desiredRange = TELEPORT.MAX_VALID_DISTANCE
|
||||
local launchVelocity = self:calculateLaunchVelocity(gravity, desiredRange, offsetHeight)
|
||||
|
||||
self:setArcLaunchParams(launchAngle, launchVelocity, gravity, desiredRange)
|
||||
|
||||
--Always check for both parabola and laser hits so we can use it to judge when to transition
|
||||
ignore[6] = GuiService.CoreGuiFolder
|
||||
local parabHitPart, parabHitPoint, parabHitNormal, parabHitT = self:getArcHit(originPos, originLook, ignore)
|
||||
|
||||
--Clear the gui folder out of our ignore table and cast so we might hit SurfaceGuis with the laser
|
||||
ignore[6] = nil
|
||||
local laserHitPart, laserHitPoint, laserHitNormal, laserHitT = self:getLaserHit(originPos, originLook, ignore)
|
||||
|
||||
self:checkMode(originPos, parabHitPart, parabHitPoint, laserHitPart, laserHitPoint)
|
||||
|
||||
if self.mode == LaserPointerMode.Navigation then
|
||||
self.parabola.Range = self.parabola.Range * parabHitT
|
||||
self.parabola.Thickness = TELEPORT.ARC_THICKNESS
|
||||
self:updateNavigationMode(parabHitPoint, parabHitNormal, parabHitPart)
|
||||
else
|
||||
self.parabola.Color3 = LASER.ARC_COLOR_GOOD
|
||||
self.parabola.Thickness = LASER.ARC_THICKNESS
|
||||
self:renderAsLaser(originPos, laserHitPoint)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return LaserPointer
|
||||
@@ -0,0 +1,791 @@
|
||||
--!nocheck
|
||||
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
|
||||
local Util = require(RobloxGui.Modules.Settings.Utility)
|
||||
local Panel3D = require(RobloxGui.Modules.VR.Panel3D)
|
||||
local VRHub = require(RobloxGui.Modules.VR.VRHub)
|
||||
|
||||
local PANEL_OFFSET_CFRAME = CFrame.Angles(math.rad(-5), 0, 0) * CFrame.new(0, 4, 0) * CFrame.Angles(math.rad(-15), 0, 0)
|
||||
|
||||
local NO_TRANSITION_ANIMATIONS = false
|
||||
local ANIMATE_OUT_DISTANCE = -100
|
||||
local ANIMATE_OUT_DURATION = 0.25
|
||||
local PIXELS_PER_STUD = 150
|
||||
local WINDOW_TITLEBAR_HEIGHT = 72
|
||||
local BLURRED_TITLEBAR_COLOR = Color3.new(78 / 255, 84 / 255, 96 / 255)
|
||||
local FOCUSED_TITLEBAR_COLOR = Color3.new(82 / 255, 101 / 255, 141 / 255)
|
||||
local WINDOW_BG_COLOR = Color3.new(20/255, 20/255, 20/255)
|
||||
local WINDOW_BG_TRANSPARENCY = 0.5
|
||||
local POPOUT_DISTANCE = 0.25
|
||||
local POPOUT_DURATION = 0.25
|
||||
|
||||
local NOTIFICATION_WIDTH_SCALE = 0.85
|
||||
local NOTIFICATION_HEIGHT_OFFSET = 80
|
||||
local NOTIFICATION_PADDING_Y = 20
|
||||
local NOTIFICATION_PADDING_X_SCALE = (1 - NOTIFICATION_WIDTH_SCALE) / 2
|
||||
local NOTIFICATION_DEPTH_OFFSET = 0.25
|
||||
local NOTIFICATION_BG_COLOR = Color3.new(0.2, 0.2, 0.2)
|
||||
local NOTIFICATION_BG_TRANSPARENCY = 0.1
|
||||
local MAX_NOTIFICATIONS_SHOWN = 3
|
||||
local MAX_DETAILS_SHOWN = 2
|
||||
local DETAILS_PADDING = 20
|
||||
|
||||
local BUTTON_1_POS = 0.07
|
||||
local BUTTON_2_POS = 0.511
|
||||
local BUTTON_SINGLE_SIZE = 0.86
|
||||
local BUTTON_DOUBLE_SIZE = 0.415
|
||||
|
||||
local BUTTON_Y_POS = 0.55
|
||||
local BUTTON_Y_SIZE = 0.29
|
||||
|
||||
local BUTTON_NORMAL_IMG = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButton.png"
|
||||
local BUTTON_SELECTED_IMG = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png"
|
||||
|
||||
local CLOSE_BUTTON_IMG = "rbxasset://textures/ui/Keyboard/close_button_icon.png"
|
||||
local CLOSE_BUTTON_HOVER = "rbxasset://textures/ui/Keyboard/close_button_selection.png"
|
||||
local CLOSE_BUTTON_SIZE = 32
|
||||
local CLOSE_BUTTON_OFFSET = 22
|
||||
local CLOSE_BUTTON_HOVER_OFFSET = 22
|
||||
|
||||
local emptySelectionImage = Util:Create "ImageLabel" {
|
||||
BackgroundTransparency = 1,
|
||||
Image = ""
|
||||
}
|
||||
|
||||
|
||||
local AVATAR_IMAGE_URL = 'http://www.roblox.com/thumbs/avatar.ashx?userId=%d&x=%d&y=%d'
|
||||
|
||||
local TEXT_COLOR = Color3.new(1, 1, 1)
|
||||
|
||||
local aspectRatio = 1.62666666
|
||||
local totalHeight = 3.5
|
||||
local totalWidth = totalHeight * aspectRatio
|
||||
local leftPanelWidth = totalWidth * 0.4
|
||||
local rightPanelWidth = totalWidth * 0.6
|
||||
|
||||
local panelOffset = -totalWidth / 2
|
||||
local leftOffset = (panelOffset + (leftPanelWidth * 0.5))
|
||||
local rightOffset = (leftOffset + (leftPanelWidth * 0.5) + (rightPanelWidth * 0.5))
|
||||
|
||||
local NotificationHubModule = {}
|
||||
NotificationHubModule.ModuleName = "Notifications"
|
||||
NotificationHubModule.KeepVRTopbarOpen = true
|
||||
NotificationHubModule.VRIsExclusive = true
|
||||
NotificationHubModule.VRClosesNonExclusive = true
|
||||
NotificationHubModule.UnreadCountChanged = function() end
|
||||
VRHub:RegisterModule(NotificationHubModule)
|
||||
|
||||
local notificationsPanel = Panel3D.Get("Notifications")
|
||||
local notificationsWindow = nil
|
||||
local detailsPanel = Panel3D.Get("NotificationDetails")
|
||||
local detailsWindow = nil
|
||||
|
||||
local function IsDeveloperGroupEnabled()
|
||||
return false
|
||||
end
|
||||
|
||||
local WindowFrame = {}
|
||||
do
|
||||
local windows = {}
|
||||
local WindowFrame_mt = { __index = WindowFrame }
|
||||
function WindowFrame.new(panel, parent, title)
|
||||
local instance = {}
|
||||
table.insert(windows, instance)
|
||||
instance.zeroCF = panel.localCF
|
||||
instance.zOffset = 0
|
||||
instance.isPopping = false
|
||||
instance.isAnimating = false
|
||||
instance.tweener = nil
|
||||
instance.panel = panel
|
||||
instance.panel.OnMouseEnter = function()
|
||||
for i, v in pairs(windows) do
|
||||
if v ~= instance then
|
||||
v:SetPopOut(false)
|
||||
end
|
||||
end
|
||||
instance:SetPopOut(true)
|
||||
end
|
||||
|
||||
instance.titlebar = Util:Create "ImageLabel" {
|
||||
Parent = parent,
|
||||
Name = "TitlebarBackground",
|
||||
|
||||
Position = UDim2.new(0, -1, 0, -1),
|
||||
Size = UDim2.new(1, 2, 0, WINDOW_TITLEBAR_HEIGHT + 2),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png",
|
||||
ImageColor3 = BLURRED_TITLEBAR_COLOR,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(10, 10, 10, 10)
|
||||
}
|
||||
instance.titleText = Util:Create "TextLabel" {
|
||||
Parent = instance.titlebar,
|
||||
Name = "TitleText",
|
||||
|
||||
Position = UDim2.new(0, 1, 0, 1),
|
||||
Size = UDim2.new(1, -2, 1, -2),
|
||||
|
||||
Text = title,
|
||||
TextColor3 = TEXT_COLOR,
|
||||
Font = Enum.Font.SourceSans,
|
||||
FontSize = Enum.FontSize.Size36,
|
||||
|
||||
BackgroundTransparency = 1
|
||||
}
|
||||
|
||||
instance.content = Util:Create "ImageLabel" {
|
||||
Parent = parent,
|
||||
Name = "ContentFrame",
|
||||
|
||||
Position = UDim2.new(0, -1, 0, WINDOW_TITLEBAR_HEIGHT + 2),
|
||||
Size = UDim2.new(1, 2, 1, -WINDOW_TITLEBAR_HEIGHT - 4),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png",
|
||||
ImageColor3 = WINDOW_BG_COLOR,
|
||||
ImageTransparency = WINDOW_BG_TRANSPARENCY,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(10, 10, 10, 10)
|
||||
}
|
||||
|
||||
return setmetatable(instance, WindowFrame_mt)
|
||||
end
|
||||
|
||||
function WindowFrame:SetTitle(title)
|
||||
self.titleText.Text = title
|
||||
end
|
||||
|
||||
function WindowFrame:AddCloseButton(callback)
|
||||
self.closeButton = Util:Create "ImageButton" {
|
||||
Parent = self.titlebar,
|
||||
Name = "CloseButton",
|
||||
|
||||
Position = UDim2.new(0, CLOSE_BUTTON_OFFSET, 0, CLOSE_BUTTON_OFFSET),
|
||||
Size = UDim2.new(0, CLOSE_BUTTON_SIZE, 0, CLOSE_BUTTON_SIZE),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Image = CLOSE_BUTTON_IMG,
|
||||
SelectionImageObject = Util:Create "ImageButton" {
|
||||
Name = "CloseButtonHover",
|
||||
Position = UDim2.new(0, CLOSE_BUTTON_HOVER_OFFSET / -2, 0, CLOSE_BUTTON_HOVER_OFFSET / -2),
|
||||
Size = UDim2.new(1, CLOSE_BUTTON_HOVER_OFFSET, 1, CLOSE_BUTTON_HOVER_OFFSET),
|
||||
BackgroundTransparency = 1,
|
||||
Image = CLOSE_BUTTON_HOVER
|
||||
}
|
||||
}
|
||||
self.closeButton.MouseButton1Click:connect(callback)
|
||||
end
|
||||
|
||||
function WindowFrame:TweenZOffsetTo(zOffset, duration, easingFunc, callback)
|
||||
if self.tweener and not self.tweener:IsFinished() then
|
||||
self.tweener:Cancel()
|
||||
end
|
||||
self.tweener = Util:TweenProperty(self, "zOffset", self.zOffset, zOffset, duration, easingFunc, callback)
|
||||
end
|
||||
|
||||
function WindowFrame:AnimateOut(callback)
|
||||
self.isAnimating = true
|
||||
self:TweenZOffsetTo(ANIMATE_OUT_DISTANCE, ANIMATE_OUT_DURATION, Util:GetEaseInOutQuad(), function()
|
||||
if callback then callback() end
|
||||
self.isAnimating = false
|
||||
end)
|
||||
end
|
||||
|
||||
function WindowFrame:AnimateIn(callback)
|
||||
self.zOffset = ANIMATE_OUT_DISTANCE
|
||||
self:OnUpdate(0)
|
||||
self.isAnimating = true
|
||||
self:TweenZOffsetTo(0, ANIMATE_OUT_DURATION, Util:GetEaseInOutQuad(), function()
|
||||
if callback then callback() end
|
||||
self.isAnimating = false
|
||||
end)
|
||||
end
|
||||
|
||||
function WindowFrame:SetPopOut(popOut)
|
||||
if self.isAnimating then
|
||||
return
|
||||
end
|
||||
|
||||
if popOut then
|
||||
self.isPopping = true
|
||||
self:TweenZOffsetTo(POPOUT_DISTANCE, POPOUT_DURATION, Util:GetEaseInOutQuad(), function()
|
||||
self.isPopping = false
|
||||
end)
|
||||
else
|
||||
self.isPopping = true
|
||||
self:TweenZOffsetTo(0, POPOUT_DURATION, Util:GetEaseInOutQuad(), function()
|
||||
self.isPopping = false
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function WindowFrame:OnUpdate(dt)
|
||||
self.panel.localCF = self.zeroCF * CFrame.new(0, 0, self.zOffset)
|
||||
self.panel.needsLocalPositionUpdate = self.isAnimating or self.isPopping
|
||||
|
||||
if self.isPopping then
|
||||
local alpha = math.max(0, math.min(1, self.zOffset / POPOUT_DISTANCE))
|
||||
self.titlebar.ImageColor3 = BLURRED_TITLEBAR_COLOR:lerp(FOCUSED_TITLEBAR_COLOR, alpha)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--Notifications panel setup
|
||||
do
|
||||
notificationsPanel:SetType(Panel3D.Type.Standard)
|
||||
notificationsPanel:SetVisible(false)
|
||||
notificationsPanel:SetCanFade(false)
|
||||
notificationsPanel:ResizeStuds(leftPanelWidth, totalHeight, PIXELS_PER_STUD)
|
||||
local notificationsFrame = Util:Create "TextButton" {
|
||||
Parent = notificationsPanel:GetGUI(),
|
||||
Name = "NotificationsListFrame",
|
||||
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = UDim2.new(1, -4, 1, 0),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
Text = "",
|
||||
|
||||
Selectable = true,
|
||||
SelectionImageObject = emptySelectionImage
|
||||
}
|
||||
notificationsWindow = WindowFrame.new(notificationsPanel, notificationsFrame, "Notifications")
|
||||
notificationsWindow:AddCloseButton(function()
|
||||
NotificationHubModule:SetVisible(false)
|
||||
end)
|
||||
function notificationsPanel:OnUpdate(dt)
|
||||
notificationsWindow:OnUpdate(dt)
|
||||
end
|
||||
end
|
||||
|
||||
--Details panel setup
|
||||
do
|
||||
detailsPanel:SetType(Panel3D.Type.Standard)
|
||||
detailsPanel:SetVisible(false)
|
||||
detailsPanel:SetCanFade(false)
|
||||
detailsPanel:ResizeStuds(rightPanelWidth, totalHeight, PIXELS_PER_STUD)
|
||||
local detailsFrame = Util:Create "TextButton" {
|
||||
Parent = detailsPanel:GetGUI(),
|
||||
Name = "NotificationsDetailFrame",
|
||||
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
Text = "",
|
||||
|
||||
Selectable = true,
|
||||
SelectionImageObject = emptySelectionImage
|
||||
}
|
||||
detailsWindow = WindowFrame.new(detailsPanel, detailsFrame, "Friend Requests")
|
||||
function detailsPanel:OnUpdate(dt)
|
||||
detailsWindow:OnUpdate(dt)
|
||||
end
|
||||
end
|
||||
|
||||
local notificationsGroups = {}
|
||||
local notificationsGroupsList = {}
|
||||
local function groupSort(a, b)
|
||||
return a.order < b.order
|
||||
end
|
||||
local activeGroup = nil
|
||||
|
||||
local function layoutNotificationsGroups()
|
||||
local y = NOTIFICATION_PADDING_Y
|
||||
for _, group in ipairs(notificationsGroupsList) do
|
||||
if #group.notifications > 0 then
|
||||
local height = NOTIFICATION_HEIGHT_OFFSET + (NOTIFICATION_PADDING_Y * (math.min(MAX_NOTIFICATIONS_SHOWN, #group.notifications) - 1))
|
||||
local widthOffset = -((MAX_NOTIFICATIONS_SHOWN - 1) * NOTIFICATION_PADDING_Y)
|
||||
group.frame.Position = UDim2.new(NOTIFICATION_PADDING_X_SCALE, 0, 0, y)
|
||||
group.frame.Size = UDim2.new(NOTIFICATION_WIDTH_SCALE, widthOffset, 0, height)
|
||||
y = y + height + NOTIFICATION_PADDING_Y
|
||||
|
||||
if group.notificationsDirty then
|
||||
group.notificationsDirty = false
|
||||
|
||||
local notificationOffset = 0
|
||||
local notificationDepth = 0
|
||||
|
||||
local notificationsEnd = #group.notifications
|
||||
if notificationsEnd > 0 then
|
||||
local notificationsStart = math.max(1, notificationsEnd - MAX_NOTIFICATIONS_SHOWN + 1)
|
||||
for i = 1, notificationsEnd do
|
||||
local notification = group.notifications[i]
|
||||
if i >= notificationsStart then
|
||||
notification.frame.Visible = true
|
||||
notification.frame.Position = UDim2.new(0, notificationOffset, 0, notificationOffset)
|
||||
notificationOffset = notificationOffset + NOTIFICATION_PADDING_Y
|
||||
|
||||
local subpanel = notificationsPanel:SetSubpanelDepth(notification.frame, notificationDepth)
|
||||
notificationDepth = notificationDepth + NOTIFICATION_DEPTH_OFFSET
|
||||
else
|
||||
notification.frame.Visible = false
|
||||
end
|
||||
end
|
||||
|
||||
if activeGroup == group then
|
||||
notificationsStart = math.max(1, notificationsEnd - MAX_DETAILS_SHOWN + 1)
|
||||
local detailY = 0
|
||||
local fraction = 1 / MAX_DETAILS_SHOWN
|
||||
for i = 1, notificationsEnd do
|
||||
local notification = group.notifications[i]
|
||||
if i >= notificationsStart then
|
||||
notification.detailsFrame.Visible = true
|
||||
notification.detailsFrame.Position = UDim2.new(0, DETAILS_PADDING, detailY, DETAILS_PADDING)
|
||||
notification.detailsFrame.Size = UDim2.new(1, -DETAILS_PADDING * 2, fraction, -DETAILS_PADDING * 2)
|
||||
|
||||
detailY = detailY + fraction
|
||||
else
|
||||
notification.detailsFrame.Visible = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local NotificationGroup = {}
|
||||
do
|
||||
local NotificationGroup_mt = { __index = NotificationGroup }
|
||||
function NotificationGroup.new(key, title, order)
|
||||
local self = setmetatable({}, NotificationGroup_mt)
|
||||
|
||||
self.key = key
|
||||
self.title = title
|
||||
self.order = order
|
||||
self.notifications = {}
|
||||
self.notificationsDirty = false
|
||||
self.frame = Util:Create "Frame" {
|
||||
Parent = notificationsWindow.content,
|
||||
Name = "NotificationGroup",
|
||||
BackgroundTransparency = 1
|
||||
}
|
||||
self.detailsFrame = Util:Create "Frame" {
|
||||
Parent = nil,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = UDim2.new(1, 0, 1, 0)
|
||||
}
|
||||
|
||||
notificationsGroups[key] = self
|
||||
table.insert(notificationsGroupsList, self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function NotificationGroup:Deactivate()
|
||||
self.detailsFrame.Parent = nil
|
||||
for i, v in pairs(self.detailsFrame:GetChildren()) do
|
||||
detailsPanel:RemoveSubpanel(v)
|
||||
end
|
||||
end
|
||||
|
||||
function NotificationGroup:SwitchTo()
|
||||
detailsWindow:SetTitle(self.title)
|
||||
for i, v in pairs(notificationsGroups) do
|
||||
if v ~= self then
|
||||
v:Deactivate()
|
||||
end
|
||||
end
|
||||
self.detailsFrame.Parent = detailsWindow.content
|
||||
|
||||
activeGroup = self
|
||||
self.notificationsDirty = true
|
||||
end
|
||||
|
||||
function NotificationGroup:BringNotificationToFront(notification)
|
||||
if activeGroup ~= self then
|
||||
self:SwitchTo()
|
||||
end
|
||||
|
||||
if #self.notifications ~= 0 and notification == self.notifications[#self.notifications] then
|
||||
layoutNotificationsGroups()
|
||||
return --already on top, no point
|
||||
end
|
||||
|
||||
for i, v in ipairs(self.notifications) do
|
||||
if v == notification then
|
||||
--take it out
|
||||
table.remove(self.notifications, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
--put it back on top
|
||||
table.insert(self.notifications, notification)
|
||||
self.notificationsDirty = true
|
||||
layoutNotificationsGroups()
|
||||
end
|
||||
|
||||
function NotificationGroup:RemoveNotification(notification)
|
||||
for i, v in ipairs(self.notifications) do
|
||||
if v == notification then
|
||||
table.remove(self.notifications, i)
|
||||
notificationsPanel:RemoveSubpanel(notification.frame)
|
||||
detailsPanel:RemoveSubpanel(notification.detailsFrame)
|
||||
notification.detailsFrame:Destroy()
|
||||
notification.frame:Destroy()
|
||||
|
||||
self.notificationsDirty = true
|
||||
layoutNotificationsGroups()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function NotificationGroup:GetTopNotification()
|
||||
local numNotifications = #self.notifications
|
||||
if numNotifications <= 0 then
|
||||
return nil
|
||||
end
|
||||
return self.notifications[numNotifications]
|
||||
end
|
||||
end
|
||||
|
||||
NotificationGroup.new("Friends", "Friends", 1)
|
||||
NotificationGroup.new("BadgeAwards", "Badges", 2)
|
||||
NotificationGroup.new("PlayerPoints", "Points", 3)
|
||||
if IsDeveloperGroupEnabled() then
|
||||
NotificationGroup.new("Developer", "Other", 4)
|
||||
end
|
||||
table.sort(notificationsGroupsList, groupSort)
|
||||
|
||||
local function doCallback(callback, ...)
|
||||
if not callback then
|
||||
return
|
||||
end
|
||||
|
||||
if type(callback) == "function" then
|
||||
callback(...)
|
||||
return
|
||||
end
|
||||
if callback:IsA("BindableEvent") then
|
||||
callback:Fire(...)
|
||||
return
|
||||
end
|
||||
if callback:IsA("BindableFunction") then
|
||||
callback:Invoke(...)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local Notification = {}
|
||||
do
|
||||
local Notification_mt = { __index = Notification }
|
||||
function Notification.new(group, notificationInfo)
|
||||
local self = setmetatable({}, Notification_mt)
|
||||
self.group = group
|
||||
self.frame = Util:Create "ImageButton" {
|
||||
Parent = group.frame,
|
||||
Size = UDim2.new(1, 0, 0, NOTIFICATION_HEIGHT_OFFSET),
|
||||
|
||||
SelectionImageObject = emptySelectionImage,
|
||||
|
||||
BackgroundTransparency = 1 --when we have proper frame rendering with AA, we can change this and remove the stand-in background
|
||||
}
|
||||
self.frame.MouseButton1Click:connect(function()
|
||||
self:OnClicked()
|
||||
end)
|
||||
self.background = Util:Create "ImageLabel" { --this is the stand-in background for that smoooooooth edge rendering
|
||||
Parent = self.frame,
|
||||
Position = UDim2.new(0, -1, 0, -1),
|
||||
Size = UDim2.new(1, 2, 1, 2),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
Image = "rbxasset://textures/ui/vr/rectBackgroundWhite.png",
|
||||
ImageColor3 = NOTIFICATION_BG_COLOR,
|
||||
ImageTransparency = NOTIFICATION_BG_TRANSPARENCY,
|
||||
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(10, 10, 10, 10)
|
||||
}
|
||||
self.imageBackground = Util:Create "ImageLabel" {
|
||||
Parent = self.frame,
|
||||
Position = UDim2.new(0, 5, 0, 5),
|
||||
Size = UDim2.new(0, 70, 0, 70),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Image = "rbxasset://textures/ui/VR/circleWhite.png",
|
||||
ImageColor3 = notificationInfo.imgBackgroundColor or Color3.new(1, 1, 1)
|
||||
}
|
||||
self.image = Util:Create "ImageLabel" {
|
||||
Parent = self.imageBackground,
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Image = notificationInfo.Image
|
||||
}
|
||||
|
||||
local text = notificationInfo.Text
|
||||
if notificationInfo.Title and notificationInfo.Text then
|
||||
text = ("%s\n%s"):format(notificationInfo.Title, notificationInfo.Text)
|
||||
end
|
||||
self.text = Util:Create "TextLabel" {
|
||||
Parent = self.frame,
|
||||
|
||||
Position = UDim2.new(0, NOTIFICATION_HEIGHT_OFFSET, 0, 0),
|
||||
Size = UDim2.new(1, -NOTIFICATION_HEIGHT_OFFSET, 1, 0),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Text = text,
|
||||
TextWrapped = true,
|
||||
Font = Enum.Font.SourceSans,
|
||||
FontSize = Enum.FontSize.Size18,
|
||||
TextColor3 = TEXT_COLOR
|
||||
}
|
||||
|
||||
self.detailsFrame = Util:Create "Frame" {
|
||||
Parent = group.detailsFrame,
|
||||
BackgroundTransparency = 1
|
||||
}
|
||||
self.detailsFrame.MouseEnter:connect(function()
|
||||
detailsPanel:SetSubpanelDepth(self.detailsFrame, 0.25)
|
||||
end)
|
||||
self.detailsFrame.MouseLeave:connect(function()
|
||||
detailsPanel:SetSubpanelDepth(self.detailsFrame, 0)
|
||||
end)
|
||||
self.detailsBackground = Util:Create "ImageLabel" {
|
||||
Parent = self.detailsFrame,
|
||||
Position = UDim2.new(0, -1, 0, -1),
|
||||
Size = UDim2.new(1, 2, 1, 2),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png",
|
||||
ImageColor3 = Color3.new(0.2, 0.2, 0.2),
|
||||
ImageTransparency = 0.1,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(10,10,10,10)
|
||||
}
|
||||
|
||||
self.detailsIconBackground = Util:Create "ImageLabel" {
|
||||
Parent = self.detailsFrame,
|
||||
Position = UDim2.new(0, 20, 0, 10),
|
||||
Size = UDim2.new(0, 80, 0, 80),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Image = "rbxasset://textures/ui/VR/circleWhite.png",
|
||||
ImageColor3 = notificationInfo.imgBackgroundColor or Color3.new(1, 1, 1)
|
||||
}
|
||||
self.detailsIcon = Util:Create "ImageLabel" {
|
||||
Parent = self.detailsIconBackground,
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Image = notificationInfo.Image
|
||||
}
|
||||
|
||||
local detailText = notificationInfo.DetailText or notificationInfo.Title
|
||||
self.detailsText = Util:Create "TextLabel" {
|
||||
Parent = self.detailsFrame,
|
||||
Position = UDim2.new(0, 110, 0, 10),
|
||||
Size = UDim2.new(1, -120, 0, 90),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Text = detailText,
|
||||
TextWrapped = true,
|
||||
TextColor3 = TEXT_COLOR,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Font = Enum.Font.SourceSansBold,
|
||||
FontSize = Enum.FontSize.Size36
|
||||
}
|
||||
|
||||
local function createButton(xPosScale, xSizeScale, text)
|
||||
local button, text = Util:Create "ImageButton" {
|
||||
Parent = self.detailsFrame,
|
||||
Position = UDim2.new(xPosScale, 0, BUTTON_Y_POS, 0),
|
||||
Size = UDim2.new(xSizeScale, 0, BUTTON_Y_SIZE, 0),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Image = BUTTON_NORMAL_IMG,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(8,6,46,44),
|
||||
|
||||
SelectionImageObject = emptySelectionImage
|
||||
}, Util:Create "TextLabel" {
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = UDim2.new(1, 0, 1, -6),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Text = text,
|
||||
TextColor3 = TEXT_COLOR,
|
||||
Font = Enum.Font.SourceSansBold,
|
||||
FontSize = Enum.FontSize.Size24
|
||||
}
|
||||
text.Parent = button
|
||||
|
||||
button.SelectionGained:connect(function()
|
||||
button.Image = BUTTON_SELECTED_IMG
|
||||
end)
|
||||
button.SelectionLost:connect(function()
|
||||
button.Image = BUTTON_NORMAL_IMG
|
||||
end)
|
||||
return button, text
|
||||
end
|
||||
|
||||
if notificationInfo.Button1Text and notificationInfo.Button2Text then
|
||||
self.detailsButton1, self.detailsButton1Text = createButton(BUTTON_1_POS, BUTTON_DOUBLE_SIZE, notificationInfo.Button1Text)
|
||||
self.detailsButton2, self.detailsButton2Text = createButton(BUTTON_2_POS, BUTTON_DOUBLE_SIZE, notificationInfo.Button2Text)
|
||||
|
||||
self.detailsButton1.MouseButton1Click:connect(function()
|
||||
doCallback(notificationInfo.Callback, notificationInfo.Button1Text)
|
||||
self:Dismiss()
|
||||
end)
|
||||
self.detailsButton2.MouseButton1Click:connect(function()
|
||||
doCallback(notificationInfo.Callback, notificationInfo.Button2Text)
|
||||
self:Dismiss()
|
||||
end)
|
||||
elseif not notificationInfo.button2Text then
|
||||
local text = notificationInfo.Button1Text or "Dismiss"
|
||||
self.detailsButton1, self.detailsButton1Text = createButton(BUTTON_1_POS, BUTTON_SINGLE_SIZE, text)
|
||||
self.detailsButton1.MouseButton1Click:connect(function()
|
||||
doCallback(notificationInfo.Callback, notificationInfo.Button1Text)
|
||||
self:Dismiss()
|
||||
end)
|
||||
end
|
||||
|
||||
table.insert(group.notifications, self)
|
||||
group.notificationsDirty = true
|
||||
layoutNotificationsGroups()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Notification:OnClicked()
|
||||
--We don't want this functionality anymore, but I'd like to keep this commented
|
||||
--out for now since this design is still in a state of flux
|
||||
--self.group:BringNotificationToFront(self)
|
||||
self.group:SwitchTo()
|
||||
layoutNotificationsGroups()
|
||||
end
|
||||
|
||||
function Notification:Dismiss()
|
||||
self.group:RemoveNotification(self)
|
||||
end
|
||||
end
|
||||
|
||||
--NotificationHubModule API and state management
|
||||
do
|
||||
local pendingNotifications = {}
|
||||
local isVisible = false
|
||||
local unreadCount = 0
|
||||
|
||||
local SendNotificationInfoEvent = RobloxGui:WaitForChild("SendNotificationInfo")
|
||||
SendNotificationInfoEvent.Event:connect(function(notificationInfo)
|
||||
local group = notificationInfo.GroupName and notificationsGroups[notificationInfo.GroupName] --avoid error by nil index
|
||||
if not group then
|
||||
if IsDeveloperGroupEnabled() then
|
||||
group = notificationsGroups.Developer
|
||||
else
|
||||
return --ignore it, invalid group
|
||||
end
|
||||
end
|
||||
|
||||
Notification.new(group, notificationInfo)
|
||||
|
||||
if not isVisible then
|
||||
unreadCount = unreadCount + 1
|
||||
NotificationHubModule.UnreadCountChanged(unreadCount)
|
||||
end
|
||||
end)
|
||||
|
||||
local menuCloseShortcutBindName = "NotificationsMenuCloseShortcut"
|
||||
local function onMenuCloseShortcut(actionName, inputState, inputObj)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
NotificationHubModule:SetVisible(false)
|
||||
end
|
||||
end
|
||||
|
||||
NotificationHubModule.VisibilityStateChanged = Util:Create "BindableEvent" {
|
||||
Name = "VisibilityStateChanged"
|
||||
}
|
||||
|
||||
function NotificationHubModule:GetNumberOfPendingNotifications()
|
||||
return #pendingNotifications
|
||||
end
|
||||
|
||||
function NotificationHubModule:IsVisible()
|
||||
return isVisible
|
||||
end
|
||||
|
||||
function NotificationHubModule:SetVisible(visible)
|
||||
if isVisible == visible then
|
||||
return
|
||||
end
|
||||
isVisible = visible
|
||||
|
||||
local topbarPanel = Panel3D.Get("Topbar3D")
|
||||
topbarPanel:SetCanFade(not visible)
|
||||
if visible then
|
||||
unreadCount = 0
|
||||
NotificationHubModule.UnreadCountChanged(unreadCount)
|
||||
|
||||
notificationsPanel.localCF = CFrame.new(leftOffset, 0, 0)
|
||||
notificationsWindow.zeroCF = notificationsPanel.localCF
|
||||
if not NO_TRANSITION_ANIMATIONS then
|
||||
notificationsWindow:AnimateIn(nil)
|
||||
end
|
||||
|
||||
detailsPanel.localCF = CFrame.new(rightOffset, 0, 0)
|
||||
detailsWindow.zeroCF = detailsPanel.localCF
|
||||
if not NO_TRANSITION_ANIMATIONS then
|
||||
detailsWindow:AnimateIn(nil)
|
||||
end
|
||||
|
||||
notificationsPanel:SetVisible(true)
|
||||
detailsPanel:SetVisible(true)
|
||||
|
||||
ContextActionService:BindCoreAction(menuCloseShortcutBindName, onMenuCloseShortcut, false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonStart)
|
||||
|
||||
VRHub:FireModuleOpened(NotificationHubModule.ModuleName)
|
||||
else
|
||||
if not NO_TRANSITION_ANIMATIONS then
|
||||
spawn(function()
|
||||
notificationsWindow:AnimateOut(function()
|
||||
notificationsPanel:SetVisible(false)
|
||||
end)
|
||||
detailsWindow:AnimateOut(function()
|
||||
detailsPanel:SetVisible(false)
|
||||
end)
|
||||
end)
|
||||
else
|
||||
notificationsPanel:SetVisible(false)
|
||||
detailsPanel:SetVisible(false)
|
||||
end
|
||||
|
||||
ContextActionService:UnbindCoreAction(menuCloseShortcutBindName)
|
||||
|
||||
VRHub:FireModuleClosed(NotificationHubModule.ModuleName)
|
||||
end
|
||||
|
||||
NotificationHubModule.VisibilityStateChanged:Fire(visible)
|
||||
end
|
||||
|
||||
|
||||
VRHub.ModuleOpened.Event:connect(function(moduleName, isExclusive, shouldCloseNonExclusive, shouldKeepTopbarOpen)
|
||||
if moduleName ~= NotificationHubModule.ModuleName then
|
||||
NotificationHubModule:SetVisible(false)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return NotificationHubModule
|
||||
@@ -0,0 +1,85 @@
|
||||
-- NotifierHint3D.lua --
|
||||
-- Written by Kip Turner, copyright ROBLOX 2016 --
|
||||
|
||||
local NotifierHint = {}
|
||||
|
||||
local CoreGui = game:GetService('CoreGui')
|
||||
local RunService = game:GetService('RunService')
|
||||
|
||||
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
|
||||
|
||||
local Util = require(RobloxGui.Modules.Settings.Utility)
|
||||
|
||||
|
||||
local NotificationObject = Util:Create'ImageLabel'
|
||||
{
|
||||
Name = 'NotificationObject';
|
||||
-- These numbers are a bit funny to fit the screen of ROBLOX VR
|
||||
Position = UDim2.new(0.5, -860, 1, -1 - 300);
|
||||
Size = UDim2.new(0, 1700,0, 700 + 300);
|
||||
BackgroundTransparency = 1;
|
||||
Image = "rbxasset://textures/ui/VR/notifier_glow.png";
|
||||
ImageTransparency = 0;
|
||||
BorderSizePixel = 0;
|
||||
}
|
||||
|
||||
NotifierHint.DEFAULT_DURATION = 5
|
||||
|
||||
local function CreateNotificationEffect()
|
||||
local this = {}
|
||||
|
||||
local speed = 2.5
|
||||
local MAX_OPACITY = 0.5
|
||||
local WAVE_START_SHIFT = math.pi/2
|
||||
|
||||
local renderConn = nil
|
||||
|
||||
function this:Init(duration)
|
||||
local start = tick()
|
||||
local endTime = start + duration
|
||||
|
||||
if renderConn then
|
||||
renderConn:disconnect()
|
||||
renderConn = nil
|
||||
end
|
||||
renderConn = RunService.RenderStepped:connect(function()
|
||||
local now = tick()
|
||||
if now >= endTime then
|
||||
self:Cancel()
|
||||
return
|
||||
end
|
||||
NotificationObject.Parent = RobloxGui
|
||||
local tweenPositionOnSineWave = math.sin((tick() - start) * speed + WAVE_START_SHIFT)
|
||||
-- Restrict the sine wave to only positive values
|
||||
tweenPositionOnSineWave = (tweenPositionOnSineWave + 1) / 2
|
||||
-- Keep the transparency in the range
|
||||
NotificationObject.ImageTransparency = tweenPositionOnSineWave * (1 - MAX_OPACITY) + MAX_OPACITY
|
||||
end)
|
||||
end
|
||||
|
||||
function this:Cancel()
|
||||
if renderConn then
|
||||
renderConn:disconnect()
|
||||
renderConn = nil
|
||||
end
|
||||
NotificationObject.Parent = nil
|
||||
end
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
local NotifierEffect = CreateNotificationEffect()
|
||||
|
||||
function NotifierHint:BeginNotification(duration)
|
||||
self:CancelNotification()
|
||||
NotifierEffect:Init(duration or self.DEFAULT_DURATION)
|
||||
end
|
||||
|
||||
function NotifierHint:CancelNotification()
|
||||
NotifierEffect:Cancel()
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
return NotifierHint
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
||||
local InputService = game:GetService("UserInputService")
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
|
||||
local Panel3D = require(RobloxGui.Modules.VR.Panel3D)
|
||||
local VRHub = require(RobloxGui.Modules.VR.VRHub)
|
||||
local Util = require(RobloxGui.Modules.Settings.Utility)
|
||||
|
||||
local cancelShortcutName = "CancelRecenterShortcut"
|
||||
local visible = false
|
||||
|
||||
local RecenterModule = {}
|
||||
RecenterModule.ModuleName = "Recenter"
|
||||
RecenterModule.KeepVRTopbarOpen = true
|
||||
RecenterModule.VRIsExclusive = true
|
||||
RecenterModule.VRClosesNonExclusive = false
|
||||
VRHub:RegisterModule(RecenterModule)
|
||||
|
||||
local countdownPanel = Panel3D.Get("RecenterCountdown")
|
||||
countdownPanel:SetType(Panel3D.Type.HorizontalFollow)
|
||||
countdownPanel:ResizeStuds(5, 3, 128)
|
||||
countdownPanel:SetCanFade(false)
|
||||
|
||||
local countdown = Util:Create "TextLabel" {
|
||||
Parent = countdownPanel:GetGUI(),
|
||||
|
||||
Position = UDim2.new(0.5, -64, 0.5, -64),
|
||||
Size = UDim2.new(0, 128, 0, 128),
|
||||
|
||||
BackgroundTransparency = 0.9,
|
||||
BackgroundColor3 = Color3.new(0.2, 0.2, 0.2),
|
||||
|
||||
TextColor3 = Color3.new(1, 1, 1),
|
||||
Text = "",
|
||||
TextScaled = true,
|
||||
Font = Enum.Font.SourceSansBold,
|
||||
|
||||
Visible = true
|
||||
}
|
||||
local recenterFrame = Util:Create "ImageLabel" {
|
||||
Parent = countdownPanel:GetGUI(),
|
||||
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Image = "rbxasset://textures/ui/VR/recenterFrame.png"
|
||||
}
|
||||
|
||||
countdownPanel:SetVisible(false)
|
||||
|
||||
local isCountingDown = false
|
||||
local function cancelCountdown()
|
||||
isCountingDown = false
|
||||
countdownPanel:SetVisible(false)
|
||||
end
|
||||
|
||||
VRHub.ModuleOpened.Event:connect(function(moduleName)
|
||||
if moduleName ~= RecenterModule.ModuleName then
|
||||
local module = VRHub:GetModule(moduleName)
|
||||
if module.VRIsExclusive then
|
||||
cancelCountdown()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function RecenterModule:SetVisible(value)
|
||||
visible = value
|
||||
|
||||
if visible then
|
||||
if isCountingDown then
|
||||
cancelCountdown()
|
||||
VRHub:FireModuleClosed(RecenterModule.ModuleName)
|
||||
return
|
||||
else
|
||||
VRHub:FireModuleOpened(RecenterModule.ModuleName)
|
||||
end
|
||||
|
||||
spawn(function()
|
||||
isCountingDown = true
|
||||
countdownPanel:SetVisible(true)
|
||||
|
||||
ContextActionService:BindCoreAction(cancelShortcutName, function(actionName, inputState, inputObj)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
cancelCountdown()
|
||||
end
|
||||
end, false, Enum.KeyCode.ButtonB)
|
||||
|
||||
for i = 3, 1, -1 do
|
||||
if isCountingDown then
|
||||
countdown.Text = tostring(i)
|
||||
wait(1)
|
||||
end
|
||||
end
|
||||
|
||||
if isCountingDown then
|
||||
InputService:RecenterUserHeadCFrame()
|
||||
end
|
||||
|
||||
countdownPanel:SetVisible(false)
|
||||
isCountingDown = false
|
||||
|
||||
ContextActionService:UnbindCoreAction(cancelShortcutName)
|
||||
|
||||
VRHub:FireModuleClosed(RecenterModule.ModuleName)
|
||||
end)
|
||||
else
|
||||
cancelCountdown()
|
||||
VRHub:FireModuleClosed(RecenterModule.ModuleName)
|
||||
end
|
||||
end
|
||||
|
||||
function RecenterModule:IsVisible()
|
||||
return visible
|
||||
end
|
||||
|
||||
return RecenterModule
|
||||
@@ -0,0 +1,77 @@
|
||||
--!nocheck
|
||||
|
||||
local VRService = game:GetService("VRService")
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local RobloxGui = CoreGui.RobloxGui
|
||||
local Panel3D = require(RobloxGui.Modules.VR.Panel3D)
|
||||
local VRHub = require(RobloxGui.Modules.VR.VRHub)
|
||||
local VRKeyboard = require(RobloxGui.Modules.VR.VirtualKeyboard)
|
||||
|
||||
local UserGuiModule = {}
|
||||
UserGuiModule.ModuleName = "UserGui"
|
||||
UserGuiModule.KeepVRTopbarOpen = false
|
||||
UserGuiModule.VRIsExclusive = false
|
||||
UserGuiModule.VRClosesNonExclusive = false
|
||||
VRHub:RegisterModule(UserGuiModule)
|
||||
|
||||
local userGuiPanel = Panel3D.Get(UserGuiModule.ModuleName)
|
||||
userGuiPanel:SetType(Panel3D.Type.Standard)
|
||||
userGuiPanel:ResizeStuds(4, 4, 128)
|
||||
userGuiPanel:SetVisible(false)
|
||||
|
||||
VRHub.ModuleOpened.Event:connect(function(moduleName)
|
||||
if moduleName ~= UserGuiModule.ModuleName then
|
||||
local module = VRHub:GetModule(moduleName)
|
||||
if module.VRClosesNonExclusive and userGuiPanel:IsVisible() then
|
||||
UserGuiModule:SetVisible(false)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local KeyboardOpen = false
|
||||
local GuiVisible = false
|
||||
|
||||
function UserGuiModule:SetVisible(visible)
|
||||
GuiVisible = visible
|
||||
userGuiPanel:SetVisible(GuiVisible)
|
||||
if GuiVisible then
|
||||
VRHub:FireModuleOpened(UserGuiModule.ModuleName)
|
||||
else
|
||||
VRHub:FireModuleClosed(UserGuiModule.ModuleName)
|
||||
end
|
||||
|
||||
-- We need to hide the UserGui when typing on the keyboard so that the textbox doesn't sink events from the keyboard
|
||||
local showGui = GuiVisible and not KeyboardOpen
|
||||
CoreGui:SetUserGuiRendering(true, showGui and userGuiPanel:GetPart() or nil, Enum.NormalId.Front)
|
||||
end
|
||||
|
||||
function UserGuiModule:IsVisible()
|
||||
return GuiVisible
|
||||
end
|
||||
|
||||
function UserGuiModule:Update()
|
||||
self:SetVisible(GuiVisible)
|
||||
end
|
||||
|
||||
local function OnVREnabledChanged()
|
||||
if not VRService.VREnabled then
|
||||
userGuiPanel:SetVisible(false)
|
||||
CoreGui:SetUserGuiRendering(false, nil, Enum.NormalId.Front)
|
||||
end
|
||||
end
|
||||
VRService:GetPropertyChangedSignal("VREnabled"):connect(OnVREnabledChanged)
|
||||
OnVREnabledChanged()
|
||||
|
||||
VRKeyboard.OpenedEvent:connect(function()
|
||||
KeyboardOpen = true
|
||||
UserGuiModule:Update()
|
||||
end)
|
||||
|
||||
VRKeyboard.ClosedEvent:connect(function()
|
||||
KeyboardOpen = false
|
||||
UserGuiModule:Update()
|
||||
end)
|
||||
|
||||
UserGuiModule:SetVisible(true)
|
||||
|
||||
return UserGuiModule
|
||||
@@ -0,0 +1,159 @@
|
||||
local VRService = game:GetService("VRService")
|
||||
local Players = game:GetService("Players")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local RobloxGui = CoreGui.RobloxGui
|
||||
local ViveController = require(RobloxGui.Modules.VR.Controllers.ViveController)
|
||||
|
||||
local LocalPlayer = Players.LocalPlayer
|
||||
while not LocalPlayer do
|
||||
Players.Changed:wait()
|
||||
LocalPlayer = Players.LocalPlayer
|
||||
end
|
||||
|
||||
local VRControllerModel = {}
|
||||
VRControllerModel.__index = VRControllerModel
|
||||
|
||||
function VRControllerModel.new(userCFrame)
|
||||
local self = setmetatable({}, VRControllerModel)
|
||||
self.userCFrame = userCFrame
|
||||
self.enabled = false
|
||||
self.currentModel = nil
|
||||
self.currentVRDeviceName = nil
|
||||
self.modelIsInWorkspace = false
|
||||
|
||||
self.onVRDeviceChangedConn = nil
|
||||
self.onCurrentCameraChangedConn = nil
|
||||
self.onUserCFrameEnabledChangedConn = nil
|
||||
self.onInputBeganConn = nil
|
||||
self.onInputChangedConn = nil
|
||||
self.onInputEndedConn = nil
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function VRControllerModel:setModelType(vrDeviceName)
|
||||
if vrDeviceName ~= self.currentVRDeviceName then
|
||||
self.currentVRDeviceName = vrDeviceName
|
||||
|
||||
if self.currentModel then
|
||||
self.currentModel:destroy()
|
||||
self.currentModel = nil
|
||||
end
|
||||
if self.currentVRDeviceName:match("Vive") then
|
||||
self.currentModel = ViveController.new(self.userCFrame)
|
||||
elseif self.currentVRDeviceName:match("Oculus") then
|
||||
--todo: add an Oculus touch controller model
|
||||
--self.currentModel = OculusTouchController.new(self.userCFrame)
|
||||
end
|
||||
|
||||
--If the controller is enabled, put the model into the workspace
|
||||
if self.enabled then
|
||||
self:setModelInWorkspace(VRService:GetUserCFrameEnabled(self.userCFrame))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function VRControllerModel:setModelInWorkspace(inWorkspace)
|
||||
if not self.currentModel then
|
||||
return
|
||||
end
|
||||
if inWorkspace ~= self.modelIsInWorkspace then
|
||||
self.modelIsInWorkspace = inWorkspace
|
||||
if self.modelIsInWorkspace then
|
||||
local camera = workspace.CurrentCamera
|
||||
local folder = camera:FindFirstChild("VRCoreEffectParts")
|
||||
if folder then
|
||||
self.currentModel.model.Parent = folder
|
||||
end
|
||||
else
|
||||
self.currentModel.model.Parent = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function VRControllerModel:setEnabled(enabled)
|
||||
if enabled ~= self.enabled then
|
||||
self.enabled = enabled
|
||||
|
||||
if self.enabled then
|
||||
--Connect events
|
||||
self.onVRDeviceChangedConn = VRService:GetPropertyChangedSignal("VRDeviceName"):connect(function()
|
||||
self:setModelType(VRService.VRDeviceName)
|
||||
end)
|
||||
self:setModelType(VRService.VRDeviceName)
|
||||
|
||||
self.onCurrentCameraChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):connect(function()
|
||||
self:setModelInWorkspace(VRService:GetUserCFrameEnabled(self.userCFrame))
|
||||
end)
|
||||
|
||||
self.onUserCFrameEnabledChangedConn = VRService.UserCFrameEnabled:connect(function(userCFrame, enabled)
|
||||
if userCFrame == self.userCFrame then
|
||||
self:setModelInWorkspace(enabled)
|
||||
end
|
||||
end)
|
||||
|
||||
self.onInputBeganConn = UserInputService.InputBegan:connect(function(...) self:onInputBegan(...) end)
|
||||
self.onInputChangedConn = UserInputService.InputChanged:connect(function(...) self:onInputChanged(...) end)
|
||||
self.onInputEndedConn = UserInputService.InputEnded:connect(function(...) self:onInputEnded(...) end)
|
||||
|
||||
--Put the model in the workspace
|
||||
self:setModelInWorkspace(VRService:GetUserCFrameEnabled(self.userCFrame))
|
||||
else
|
||||
--Disconnect events
|
||||
if self.onVRDeviceChangedConn then self.onVRDeviceChangedConn:disconnect() self.onVRDeviceChangedConn = nil end
|
||||
if self.onCurrentCameraChangedConn then self.onCurrentCameraChangedConn:disconnect() self.onCurrentCameraChangedConn = nil end
|
||||
if self.onUserCFrameEnabledChangedConn then self.onUserCFrameEnabledChangedConn:disconnect() self.onUserCFrameEnabledChangedConn = nil end
|
||||
if self.onInputBeganConn then self.onInputBeganConn:disconnect() self.onInputBeganConn = nil end
|
||||
if self.onInputChangedConn then self.onInputChangedConn:disconnect() self.onInputChangedConn = nil end
|
||||
if self.onInputEndedConn then self.onInputEndedConn:disconnect() self.onInputEndedConn = nil end
|
||||
|
||||
--Remove the model from the workspace
|
||||
if self.currentModel then
|
||||
self:setModelInWorkspace(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function VRControllerModel:update(dt)
|
||||
if not self.enabled then
|
||||
return
|
||||
end
|
||||
|
||||
if self.currentModel then
|
||||
local camera = workspace.CurrentCamera
|
||||
|
||||
local cameraCFrame = camera.CFrame
|
||||
local controllerCFrame = VRService:GetUserCFrame(self.userCFrame)
|
||||
local cframe = cameraCFrame * controllerCFrame
|
||||
self.currentModel:setCFrame(cframe)
|
||||
end
|
||||
end
|
||||
|
||||
function VRControllerModel:onInputBegan(inputObject, wasProcessed)
|
||||
if not self.enabled or not self.modelIsInWorkspace then
|
||||
return
|
||||
end
|
||||
if self.currentModel then
|
||||
self.currentModel:onInputBegan(inputObject)
|
||||
end
|
||||
end
|
||||
|
||||
function VRControllerModel:onInputChanged(inputObject, wasProcessed)
|
||||
if not self.enabled or not self.modelIsInWorkspace then
|
||||
return
|
||||
end
|
||||
if self.currentModel then
|
||||
self.currentModel:onInputChanged(inputObject)
|
||||
end
|
||||
end
|
||||
|
||||
function VRControllerModel:onInputEnded(inputObject, wasProcessed)
|
||||
if self.currentModel then
|
||||
self.currentModel:onInputEnded(inputObject)
|
||||
end
|
||||
end
|
||||
|
||||
return VRControllerModel
|
||||
@@ -0,0 +1,181 @@
|
||||
--Modules/VR/VRHub.lua
|
||||
--Handles all global VR state that isn't built into a specific module.
|
||||
--Written by 0xBAADF00D (Kyle) on 6/10/16
|
||||
local StarterGui = game:GetService("StarterGui")
|
||||
local VRService = game:GetService("VRService")
|
||||
local RunService = game:GetService("RunService")
|
||||
local HttpService = game:GetService("HttpService")
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local RobloxGui = CoreGui.RobloxGui
|
||||
local Util = require(RobloxGui.Modules.Settings.Utility)
|
||||
|
||||
local LaserPointer = require(RobloxGui.Modules.VR.LaserPointer)
|
||||
local VRControllerModel = require(RobloxGui.Modules.VR.VRControllerModel)
|
||||
|
||||
local VRHub = {}
|
||||
local RegisteredModules = {}
|
||||
local OpenModules = {}
|
||||
|
||||
--VR Setup
|
||||
local vrUpdateRenderstepName = HttpService:GenerateGUID(true)
|
||||
|
||||
VRHub.LaserPointer = nil
|
||||
|
||||
VRHub.ControllerModelsEnabled = false
|
||||
VRHub.LeftControllerModel = nil
|
||||
VRHub.RightControllerModel = nil
|
||||
|
||||
StarterGui:RegisterSetCore("VRLaserPointerMode", function(mode)
|
||||
if not VRHub.LaserPointer then
|
||||
return
|
||||
end
|
||||
if not mode or not tostring(mode) then
|
||||
return
|
||||
end
|
||||
VRHub.LaserPointer:setMode(LaserPointer.Mode[tostring(mode)] or LaserPointer.Mode.Disabled)
|
||||
end)
|
||||
|
||||
local function enableControllerModels(enabled)
|
||||
if enabled ~= VRHub.ControllerModelsEnabled then
|
||||
VRHub.ControllerModelsEnabled = enabled
|
||||
|
||||
if enabled then
|
||||
if not VRHub.LeftControllerModel then
|
||||
VRHub.LeftControllerModel = VRControllerModel.new(Enum.UserCFrame.LeftHand)
|
||||
end
|
||||
VRHub.LeftControllerModel:setEnabled(true)
|
||||
|
||||
if not VRHub.RightControllerModel then
|
||||
VRHub.RightControllerModel = VRControllerModel.new(Enum.UserCFrame.RightHand)
|
||||
end
|
||||
VRHub.RightControllerModel:setEnabled(true)
|
||||
else
|
||||
if VRHub.LeftControllerModel then
|
||||
VRHub.LeftControllerModel:setEnabled(false)
|
||||
end
|
||||
if VRHub.RightControllerModel then
|
||||
VRHub.RightControllerModel:setEnabled(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local enableControllerModelsSetByDeveloper = false
|
||||
StarterGui:RegisterSetCore("VREnableControllerModels", function(enabled)
|
||||
enableControllerModelsSetByDeveloper = true
|
||||
enableControllerModels(enabled)
|
||||
end)
|
||||
|
||||
local start = tick()
|
||||
local function onRenderSteppedLast()
|
||||
local now = tick()
|
||||
local dt = now - start
|
||||
start = now
|
||||
|
||||
if VRHub.LaserPointer then
|
||||
VRHub.LaserPointer:update(dt)
|
||||
end
|
||||
|
||||
if VRHub.LeftControllerModel then
|
||||
VRHub.LeftControllerModel:update(dt)
|
||||
end
|
||||
if VRHub.RightControllerModel then
|
||||
VRHub.RightControllerModel:update(dt)
|
||||
end
|
||||
end
|
||||
|
||||
local function onVREnabled(property)
|
||||
if property ~= "VREnabled" then
|
||||
return
|
||||
end
|
||||
|
||||
if VRService.VREnabled then
|
||||
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
|
||||
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
|
||||
|
||||
if not VRHub.LaserPointer then
|
||||
VRHub.LaserPointer = LaserPointer.new()
|
||||
end
|
||||
|
||||
--Check again in case creating the laser pointer gracefully failed
|
||||
if VRHub.LaserPointer then
|
||||
VRHub.LaserPointer:setMode(LaserPointer.Mode.Navigation)
|
||||
end
|
||||
if not enableControllerModelsSetByDeveloper then
|
||||
enableControllerModels(true)
|
||||
end
|
||||
RunService:BindToRenderStep(vrUpdateRenderstepName, Enum.RenderPriority.Last.Value, onRenderSteppedLast)
|
||||
else
|
||||
if VRHub.LaserPointer then
|
||||
VRHub.LaserPointer:setMode(LaserPointer.Mode.Disabled)
|
||||
end
|
||||
RunService:UnbindFromRenderStep(vrUpdateRenderstepName)
|
||||
end
|
||||
end
|
||||
onVREnabled("VREnabled")
|
||||
VRService.Changed:connect(onVREnabled)
|
||||
|
||||
--VRHub API
|
||||
function VRHub:RegisterModule(module)
|
||||
RegisteredModules[module.ModuleName] = module
|
||||
end
|
||||
|
||||
function VRHub:GetModule(moduleName)
|
||||
return RegisteredModules[moduleName]
|
||||
end
|
||||
|
||||
function VRHub:IsModuleOpened(moduleName)
|
||||
return OpenModules[moduleName] ~= nil
|
||||
end
|
||||
|
||||
function VRHub:GetOpenedModules()
|
||||
local result = {}
|
||||
|
||||
for _, openModule in pairs(OpenModules) do
|
||||
table.insert(result, openModule)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
VRHub.ModuleOpened = Util:Create "BindableEvent" {
|
||||
Name = "VRModuleOpened"
|
||||
}
|
||||
--Wrapper function to document the arguments to the event
|
||||
function VRHub:FireModuleOpened(moduleName)
|
||||
if not RegisteredModules[moduleName] then
|
||||
error("Tried to open module that is not registered: " .. moduleName)
|
||||
end
|
||||
|
||||
if OpenModules[moduleName] ~= RegisteredModules[moduleName] then
|
||||
OpenModules[moduleName] = RegisteredModules[moduleName]
|
||||
VRHub.ModuleOpened:Fire(moduleName)
|
||||
end
|
||||
end
|
||||
|
||||
VRHub.ModuleClosed = Util:Create "BindableEvent" {
|
||||
Name = "VRModuleClosed"
|
||||
}
|
||||
--Wrapper function to document the arguments to the event
|
||||
function VRHub:FireModuleClosed(moduleName)
|
||||
if not RegisteredModules[moduleName] then
|
||||
error("Tried to close module that is not registered: " .. moduleName)
|
||||
end
|
||||
|
||||
if OpenModules[moduleName] ~= nil then
|
||||
OpenModules[moduleName] = nil
|
||||
VRHub.ModuleClosed:Fire(moduleName)
|
||||
end
|
||||
end
|
||||
|
||||
function VRHub:KeepVRTopbarOpen()
|
||||
for moduleName, openModule in pairs(OpenModules) do
|
||||
if openModule.KeepVRTopbarOpen then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return VRHub
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user