CoreScriptsRoot/Modules/BackpackScript3D.lua
--BackpackScript3D: VR port of backpack interface using a 3D panel
--written by 0xBAADF00D
local ICON_SIZE = 48
local ICON_SPACING = 52
local PIXELS_PER_STUD = 64

local SLOT_BORDER_SIZE = 0
local SLOT_BORDER_SELECTED_SIZE = 4
local SLOT_BORDER_COLOR = Color3.new(90/255, 142/255, 233/255)
local SLOT_BACKGROUND_COLOR = Color3.new(0.2, 0.2, 0.2)
local SLOT_HOVER_BACKGROUND_COLOR = Color3.new(90/255, 90/255, 90/255)

local HOPPERBIN_ANGLE = math.rad(-45)
local HOPPERBIN_ROTATION = CFrame.Angles(HOPPERBIN_ANGLE, 0, 0)
local HOPPERBIN_OFFSET = Vector3.new(0, 0, -5)

local HEALTHBAR_SPACE = 12
local HEALTHBAR_WIDTH = 82
local HEALTHBAR_HEIGHT = 5

local NAME_SPACE = 14

local Tools = {}
local ToolsList = {}
local slotIcons = {}

local BackpackScript = {}
local topbarEnabled = false

local player = game:GetService("Players").LocalPlayer
local currentHumanoid = nil
local CoreGui = game:GetService('CoreGui')
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Panel3D = require(RobloxGui.Modules.VR.Panel3D)
local Util = require(RobloxGui.Modules.Settings.Utility)

local ContextActionService = game:GetService("ContextActionService")

local BackpackPanel = Panel3D.Get("Backpack")
BackpackPanel:ResizeStuds(5, 2)
BackpackPanel:SetType(Panel3D.Type.Fixed, { CFrame = CFrame.new(0, 0, -5) })
BackpackPanel:SetVisible(true)

local toolsFrame = Instance.new("TextButton", BackpackPanel:GetGUI()) --prevent clicks falling through in case you have a rocket launcher and blow yourself up
toolsFrame.Text = ""
toolsFrame.Size = UDim2.new(1, 0, 0, ICON_SIZE)
toolsFrame.BackgroundTransparency = 1
toolsFrame.Selectable = false
local insetAdjustY = toolsFrame.AbsolutePosition.Y
toolsFrame.Position = UDim2.new(0, 0, 0, HEALTHBAR_SPACE + NAME_SPACE)

--Healthbar color function stolen from Topbar.lua
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 healthbarBack = Instance.new("ImageLabel", BackpackPanel:GetGUI())
healthbarBack.ImageColor3 = HEALTH_BACKGROUND_COLOR
healthbarBack.BackgroundTransparency = 1
healthbarBack.ScaleType = Enum.ScaleType.Slice
healthbarBack.SliceCenter = Rect.new(10, 10, 10, 10)
healthbarBack.Name = "HealthbarContainer"
healthbarBack.Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png"
local healthbarFront = Instance.new("ImageLabel", healthbarBack)
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"

local playerName = Instance.new("TextLabel", BackpackPanel:GetGUI())
playerName.Name = "PlayerName"
playerName.BackgroundTransparency = 1
playerName.TextColor3 = Color3.new(1, 1, 1)
playerName.Text = player.Name
playerName.Font = Enum.Font.SourceSansBold
playerName.FontSize = Enum.FontSize.Size12
playerName.TextXAlignment = Enum.TextXAlignment.Left
playerName.Size = UDim2.new(1, 0, 0, NAME_SPACE)


BackpackScript.ToolAddedEvent = Instance.new("BindableEvent")


local healthColorToPosition = {
	[Vector3.new(HEALTH_RED_COLOR.r, HEALTH_RED_COLOR.g, HEALTH_RED_COLOR.b)] = 0.1;
	[Vector3.new(HEALTH_YELLOW_COLOR.r, HEALTH_YELLOW_COLOR.g, HEALTH_YELLOW_COLOR.b)] = 0.5;
	[Vector3.new(HEALTH_GREEN_COLOR.r, HEALTH_GREEN_COLOR.g, HEALTH_GREEN_COLOR.b)] = 0.8;
}
local min = 0.1
local minColor = HEALTH_RED_COLOR
local max = 0.8
local maxColor = HEALTH_GREEN_COLOR

local function HealthbarColorTransferFunction(healthPercent)
	if healthPercent < min then
		return minColor
	elseif healthPercent > max then
		return maxColor
	end

	-- Shepard's Interpolation
	local numeratorSum = Vector3.new(0,0,0)
	local denominatorSum = 0
	for colorSampleValue, samplePoint in pairs(healthColorToPosition) 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 backpackEnabled = true
local healthbarEnabled = true

local function UpdateLayout()
	local width, height = 100, 100
	local borderSize = (ICON_SPACING - ICON_SIZE) / 2

	local x = borderSize
	local y = 0
	for _, tool in ipairs(ToolsList) do
		local slot = Tools[tool]
		if slot then
			slot.icon.Position = UDim2.new(0, x, 0, y)
			x = x + ICON_SPACING
		end
	end

	if #ToolsList == 0 then
		width = HEALTHBAR_WIDTH
		height = HEALTHBAR_SPACE + NAME_SPACE
		BackpackPanel.showCursor = false
	else
		width = #ToolsList * ICON_SPACING
		height = ICON_SIZE + HEALTHBAR_SPACE + NAME_SPACE
		BackpackPanel.showCursor = true
	end

	BackpackPanel:ResizePixels(width, height)

	playerName.Position = UDim2.new(0, borderSize, 0, 0)

	healthbarBack.Position = UDim2.new(0, borderSize, 0, NAME_SPACE + (HEALTHBAR_SPACE - HEALTHBAR_HEIGHT) / 2)
	healthbarBack.Size = UDim2.new(0, HEALTHBAR_WIDTH, 0, HEALTHBAR_HEIGHT)
end

local function UpdateHealth(humanoid)
	local percentHealth = humanoid.Health / humanoid.MaxHealth
	if percentHealth ~= percentHealth then
		percentHealth = 1
	end
	healthbarFront.BackgroundColor3 = HealthbarColorTransferFunction(percentHealth)
	healthbarFront.Size = UDim2.new(percentHealth, 0, 1, 0)
end

local function SetTransparency(transparency)
	for i, v in pairs(Tools) do
		v.bg.ImageTransparency = transparency
		v.image.ImageTransparency = transparency
		v.text.TextTransparency = transparency
	end

	playerName.TextTransparency = transparency
	healthbarBack.ImageTransparency = transparency
	healthbarFront.ImageTransparency = transparency
end

local function OnHotbarEquipPrimary(actionName, state, obj)
	if state ~= Enum.UserInputState.Begin then
		return
	end
	for tool, slot in pairs(Tools) do
		if slot.hovered then
			slot.OnClick()
			return
		end
	end
end

local function EnableHotbarInput(enable)
	if not backpackEnabled then
		enable = false
	end
	if not currentHumanoid then
		return
	end
	if enable then
		ContextActionService:BindCoreAction("HotbarEquipPrimary", OnHotbarEquipPrimary, false, Enum.KeyCode.ButtonA, Enum.KeyCode.ButtonR2, Enum.UserInputType.MouseButton1)
	else
		ContextActionService:UnbindCoreAction("HotbarEquipPrimary")
	end
end

local function AddTool(tool)
	if Tools[tool] then
		return
	end

	local slot = {}
	Tools[tool] = slot
	table.insert(ToolsList, tool)

	slot.hovered = false
	slot.tool = tool

	slot.icon = Instance.new("TextButton", toolsFrame)
	slot.icon.Text = ""
	slot.icon.Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE)
	slot.icon.BackgroundColor3 = Color3.new(0, 0, 0)
	slot.icon.Selectable = true
	slot.icon.BackgroundTransparency = 1
	slotIcons[tool] = slot.icon

	slot.bg = Instance.new("ImageLabel", slot.icon)
	slot.bg.Position = UDim2.new(0, -1, 0, -1)
	slot.bg.Size = UDim2.new(1, 2, 1, 2)
	slot.bg.Image = "rbxasset://textures/ui/VR/rectBackground.png"
	slot.bg.ScaleType = Enum.ScaleType.Slice
	slot.bg.SliceCenter = Rect.new(10, 10, 10, 10)
	slot.bg.BackgroundTransparency = 1

	slot.image = Instance.new("ImageLabel", slot.icon)
	slot.image.Position = UDim2.new(0, 1, 0, 1)
	slot.image.Size = UDim2.new(1, -2, 1, -2)
	slot.image.BackgroundTransparency = 1
	slot.image.Selectable = false

	slot.text = Instance.new("TextLabel", slot.icon)
	slot.text.Position = UDim2.new(0, 1, 0, 1)
	slot.text.Size = UDim2.new(1, -2, 1, -2)
	slot.text.BackgroundTransparency = 1
	slot.text.TextColor3 = Color3.new(1, 1, 1)
	slot.text.Font = Enum.Font.SourceSans
	slot.text.FontSize = Enum.FontSize.Size12
	slot.text.ClipsDescendants = true
	slot.text.Selectable = false

	local selectionObject = Util:Create'ImageLabel'
	{
		Name = 'SelectionObject';
		Size = UDim2.new(1,0,1,0);
		BackgroundTransparency = 1;
		Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png";
		ImageTransparency = 0;
		ScaleType = Enum.ScaleType.Slice;
		SliceCenter = Rect.new(12,12,52,52);
		BorderSizePixel = 0;
	}
	slot.icon.SelectionImageObject = selectionObject

	local function updateToolData()
		slot.image.Image = tool.TextureId
		slot.text.Text = tool.TextureId == "" and tool.Name or ""
	end
	updateToolData()

	slot.OnClick = function()
		if not player.Character then return end
		local humanoid = player.Character:FindFirstChild("Humanoid")
		if not humanoid then return end

		local inBackpack = tool.Parent == player.Backpack
		humanoid:UnequipTools()
		if inBackpack then
			humanoid:EquipTool(tool)
		end
	end

	slot.icon.MouseButton1Click:connect(slot.OnClick)
	slot.OnEnter = function()
		slot.hovered = true
	end
	slot.OnLeave = function()
		slot.hovered = false
	end
--	slot.icon.MouseEnter:connect(slot.OnEnter)
--	slot.icon.MouseLeave:connect(slot.OnLeave)

	tool.Changed:connect(function(prop)
		if prop == "Parent" then
			if tool.Parent == player:FindFirstChild("Backpack") then
				slot.bg.Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE) --temporary hold-over until new backpack design comes along (can't use border with this antialiased frame stand-in)
				slot.bg.Position = UDim2.new(0, 0, 0, 0)
			elseif tool.Parent == player.Character then
				slot.bg.Size = UDim2.new(0, ICON_SIZE + 8, 0, ICON_SIZE + 8)
				slot.bg.Position = UDim2.new(0, -4, 0, -4)
			end
		elseif prop == "TextureId" or prop == "Name" then
			updateToolData()
		end
	end)

	UpdateLayout()

	BackpackScript.ToolAddedEvent:Fire(tool)
end

local humanoidChangedEvent = nil
local humanoidAncestryChangedEvent = nil
local function RegisterHumanoid(humanoid)
	currentHumanoid = humanoid
	if humanoidChangedEvent then
		humanoidChangedEvent:disconnect()
		humanoidChangedEvent = nil
	end
	if humanoidAncestryChangedEvent then
		humanoidAncestryChangedEvent:disconnect()
		humanoidAncestryChangedEvent = nil
	end
	if humanoid then
		humanoidChangedEvent = humanoid.HealthChanged:connect(function() UpdateHealth(humanoid) end)
		humanoidAncestryChangedEvent = humanoid.AncestryChanged:connect(function(child, parent)
			if child == humanoid and parent ~= player.Character then
				RegisterHumanoid(nil)
			end
		end)
		UpdateHealth(humanoid)
	end
end

local function OnChildAdded(child)
	if child:IsA("Tool") or child:IsA("HopperBin") then
		AddTool(child)
	end
	if child:IsA("Humanoid") and child.Parent == player.Character then
		RegisterHumanoid(child)
	end
end

local function RemoveTool(tool)
	if not Tools[tool] then
		return
	end
	Tools[tool].icon:Destroy()
	for i, v in ipairs(ToolsList) do
		if v == tool then
			table.remove(ToolsList, i)
			break
		end
	end
	Tools[tool] = nil
	slotIcons[tool] = nil
	UpdateLayout()
end

local function OnChildRemoved(child)
	if child:IsA("Tool") or child:IsA("HopperBin") then
		if Tools[child] then
			if child.Parent ~= player:FindFirstChild("Backpack") and child.Parent ~= player.Character then
				RemoveTool(child)
			end
		end
	end
end

local function OnCharacterAdded(character)
	local backpack = player:WaitForChild("Backpack")

	for i, v in ipairs(character:GetChildren()) do
		if v:IsA("Humanoid") then
			RegisterHumanoid(v)
			break
		end
	end

	for tool, v in pairs(Tools) do
		RemoveTool(tool)
	end
	Tools = {}
	ToolsList = {}

	character.ChildAdded:connect(OnChildAdded)
	character.ChildRemoved:connect(OnChildRemoved)

	for i, v in ipairs(backpack:GetChildren()) do
		OnChildAdded(v)
	end

	backpack.ChildAdded:connect(OnChildAdded)
	backpack.ChildRemoved:connect(OnChildRemoved)
end

player.CharacterAdded:connect(OnCharacterAdded)
if player.Character then
	spawn(function() OnCharacterAdded(player.Character) end)
end

local function OnHotbarEquip(actionName, state, obj)
	if not backpackEnabled then
		return
	end
	local character = player.Character
	if not character then
		return
	end
	if not currentHumanoid then
		return
	end
	if state ~= Enum.UserInputState.Begin then
		return
	end
	if #ToolsList == 0 then
		return
	end
	local current = 0
	for i, v in pairs(ToolsList) do
		if v.Parent == character then
			current = i
		end
	end
	currentHumanoid:UnequipTools()
	if obj.KeyCode == Enum.KeyCode.ButtonR1 then
		current = current + 1
		if current > #ToolsList then
			current = 1
		end
	else
		current = current - 1
		if current < 1 then
			current = #ToolsList
		end
	end
	currentHumanoid:EquipTool(ToolsList[current])
end

local function OnCoreGuiChanged(coreGuiType, enabled)
	-- Check for enabling/disabling the whole thing
	if coreGuiType == Enum.CoreGuiType.Backpack or coreGuiType == Enum.CoreGuiType.All then
		backpackEnabled = enabled
		UpdateLayout()
		if enabled then
			ContextActionService:BindCoreAction("HotbarEquip2", OnHotbarEquip, false, Enum.KeyCode.ButtonL1, Enum.KeyCode.ButtonR1)
			toolsFrame.Parent = BackpackPanel:GetGUI()
		else
			ContextActionService:UnbindCoreAction("HotbarEquip2")
			toolsFrame.Parent = nil
		end
	end

	if coreGuiType == Enum.CoreGuiType.Health or coreGuiType == Enum.CoreGuiType.All then
		healthbarEnabled = enabled
		UpdateLayout()
		if enabled then
			healthbarBack.Parent = BackpackPanel:GetGUI()
		else
			healthbarBack.Parent = nil
		end
	end
end

local StarterGui = game:GetService("StarterGui")
StarterGui.CoreGuiChangedSignal:connect(OnCoreGuiChanged)
OnCoreGuiChanged(Enum.CoreGuiType.Backpack, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Backpack))
OnCoreGuiChanged(Enum.CoreGuiType.Backpack, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.All))

OnCoreGuiChanged(Enum.CoreGuiType.Health, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Health))
OnCoreGuiChanged(Enum.CoreGuiType.Health, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.All))

local panelLocalCF = CFrame.Angles(math.rad(-5), 0, 0) * CFrame.new(0, 1.75, 0) * CFrame.Angles(math.rad(-5), 0, 0)

function BackpackPanel:PreUpdate(cameraCF, cameraRenderCF, userHeadCF, lookRay)
	--the backpack panel needs to go in front of the user when they look at it.
	--if they aren't looking, we should be updating self.localCF

	local topbarPanel = Panel3D.Get("Topbar3D")
	local panelOriginCF = topbarPanel.localCF or CFrame.new()
	self.localCF = panelOriginCF * panelLocalCF
end

function BackpackPanel:OnUpdate()
	SetTransparency(self.transparency)

	local hovered, tool = BackpackPanel:FindHoveredGuiElement(slotIcons)
	if hovered and tool then
		local slot = Tools[tool]
		if not slot.hovered then
			slot.OnEnter()
		end
		for i, v in pairs(Tools) do
			if v.hovered and v ~= slot then
				v.OnLeave()
			end
		end
	end
end

function BackpackPanel:OnMouseEnter(x, y)
	EnableHotbarInput(true)
end
function BackpackPanel:OnMouseLeave(x, y)
	EnableHotbarInput(false)
end

local VRHub = require(RobloxGui.Modules.VR.VRHub)
VRHub.ModuleOpened.Event:connect(function(moduleName)
	local module = VRHub:GetModule(moduleName)
	if module.VRIsExclusive then
		BackpackPanel:SetVisible(false)
	end
end)
VRHub.ModuleClosed.Event:connect(function(moduleName)
	BackpackPanel:SetVisible(true)
end)


BackpackPanel:LinkTo("Topbar3D")

return BackpackScript
