-- Written by Kip Turner, Copyright ROBLOX 2015 local TextService = game:GetService('TextService') local CoreGui = game:GetService("CoreGui") local GuiService = game:GetService('GuiService') local RunService = game:GetService('RunService') local ContextActionService = game:GetService("ContextActionService") local GuiRoot = CoreGui:FindFirstChild("RobloxGui") local Modules = GuiRoot:FindFirstChild("Modules") local PackageData = require(Modules:FindFirstChild('PackageData')) local OutfitData = require(Modules:FindFirstChild('OutfitData')) local ScrollingGridModule = require(Modules:FindFirstChild('ScrollingGrid')) local AvatarTile = require(Modules:FindFirstChild('AvatarTile')) local OutfitTile = require(Modules:FindFirstChild('OutfitTile')) local Utility = require(Modules:FindFirstChild('Utility')) local Http = require(Modules:FindFirstChild('Http')) local ScreenManager = require(Modules:FindFirstChild('ScreenManager')) local GlobalSettings = require(Modules:FindFirstChild('GlobalSettings')) local LoadingWidget = require(Modules:FindFirstChild('LoadingWidget')) local ThumbnailLoader = require(Modules:FindFirstChild('ThumbnailLoader')) local UserData = require(Modules:FindFirstChild('UserData')) local EventHub = require(Modules:FindFirstChild('EventHub')) local Strings = require(Modules:FindFirstChild('LocalizedStrings')) local AssetManager = require(Modules:FindFirstChild('AssetManager')) local SoundManager = require(Modules:FindFirstChild('SoundManager')) local GLOW_BASE_RPM = 2 local GLOW_TOP_RPM = -0.5 local GLOW_TRANSPARENCY = 0.2 local CATALOG_BELOW_POSITION = 320 local function CreateAvatarPane(parent) local this = {} local inFocus = false local isShown = false local AvatarObjects = {} local OnGuiServiceChangedConn = nil local lastParent = parent local LastWearingPackageAssetId = nil local LastWearingOutfitId = nil local MainContainer = Utility.Create'Frame' { Name = 'AvatarPane'; Size = UDim2.new(1, 0, 1, 0); BackgroundTransparency = 1; Visible = false; } local MyAvatarContainer = Utility.Create'Frame' { Name = 'MyAvatarContainer'; Size = UDim2.new(0.38,0,1,0); Position = UDim2.new(0,0,0,0); BackgroundTransparency = 1; ClipsDescendants = true; Parent = MainContainer; } local MyNameLabel = Utility.Create'TextLabel' { Name = 'MyNameLabel'; Text = ''; Size = UDim2.new(1,0,0,25); Position = UDim2.new(0,12,0,0); TextXAlignment = 'Left'; TextColor3 = GlobalSettings.WhiteTextColor; Font = GlobalSettings.RegularFont; FontSize = GlobalSettings.SubHeaderSize; BackgroundTransparency = 1; Parent = MyAvatarContainer; }; local ProfileImageContainer = Utility.Create'Frame' { Name = 'ProfileImageContainer'; Size = UDim2.new(0.68,0,0.9,-MyNameLabel.Size.Y.Offset); Position = UDim2.new(0.16, 0, 0.05, MyNameLabel.Size.Y.Offset); BackgroundTransparency = 1; Parent = MyAvatarContainer; } local ProfileImage = Utility.Create'ImageLabel' { Name = 'ProfileImage'; Size = UDim2.new(0,780,0,780); Position = UDim2.new(0.5, 0, 0.5, 0); BackgroundTransparency = 1; ZIndex = 3; Parent = ProfileImageContainer; }; local CrossfadeProfileImage = Utility.Create'ImageLabel' { Name = 'CrossfadeProfileImage'; Size = UDim2.new(1,0,1,0); Position = UDim2.new(0, 0, 0, 0); BackgroundTransparency = 1; ImageTransparency = 1; ZIndex = 3; Parent = ProfileImage; }; local CharacterGlowBase = Utility.Create'ImageLabel' { Name = 'CharacterGlowBase'; Size = UDim2.new(0,1015,0,1002); Position = UDim2.new(0, 0, 0, 0); BackgroundTransparency = 1; Image = 'rbxasset://textures/ui/Shell/Images/CharacterGlow/CharacterGlowBase.png'; ImageTransparency = GLOW_TRANSPARENCY; Parent = CrossfadeProfileImage; }; Utility.CalculateAnchor(CharacterGlowBase, UDim2.new(0.5, 0, 0.5, 0), Utility.Enum.Anchor.Center) local CharacterGlowTop = Utility.Create'ImageLabel' { Name = 'CharacterGlowTop'; Size = UDim2.new(0,1026,0,1009); Position = UDim2.new(0, 0, 0, 0); BackgroundTransparency = 1; Image = 'rbxasset://textures/ui/Shell/Images/CharacterGlow/CharacterGlowTop.png'; ImageTransparency = GLOW_TRANSPARENCY; ZIndex = 2; Parent = CrossfadeProfileImage; }; Utility.CalculateAnchor(CharacterGlowTop, UDim2.new(0.5, 0, 0.5, 0), Utility.Enum.Anchor.Center) local function onSizeChanged() ProfileImage.Size = Utility.CalculateFill(ProfileImage, Vector2.new(576, 324)) Utility.CalculateAnchor(ProfileImage, UDim2.new(0.5, 0, 0.5, 0), Utility.Enum.Anchor.Center) end local EquipButtonImage = Utility.Create'ImageLabel' { Name = 'EquipButtonImage'; Size = UDim2.new(0,70,0,70); Position = UDim2.new(0.5, 25, 0.75, 0); Image = 'rbxasset://textures/ui/Shell/ButtonIcons/XButton.png'; ImageTransparency = 1; BackgroundTransparency = 1; Parent = MainContainer; }; local EquipHint = Utility.Create'TextLabel' { Name = 'EquipHint'; Text = Strings:LocalizedString('EquipWord'):upper(); Size = UDim2.new(0,0,1,0); Position = UDim2.new(1, 5, 0, -3); TextXAlignment = 'Left'; TextColor3 = GlobalSettings.WhiteTextColor; TextTransparency = 1; Font = GlobalSettings.RegularFont; FontSize = GlobalSettings.TitleSize; BackgroundTransparency = 1; Parent = EquipButtonImage; }; do local equipHintTextSize = TextService:GetTextSize(EquipHint.Text, Utility.ConvertFontSizeEnumToInt(EquipHint.FontSize), EquipHint.Font, Vector2.new()) Utility.CalculateAnchor(EquipButtonImage, UDim2.new(0.95, -equipHintTextSize.X,1,0), Utility.Enum.Anchor.BottomRight) end local function TweenEquipButton(newValue, duration) duration = duration or 0.25 Utility.PropertyTweener(EquipButtonImage, 'ImageTransparency', EquipButtonImage.ImageTransparency, newValue, duration, Utility.EaseOutQuad, true) Utility.PropertyTweener(EquipHint, 'TextTransparency', EquipHint.TextTransparency, newValue, duration, Utility.EaseOutQuad, true) end local function UpdateEquipButton(IsOwned, IsWearing) TweenEquipButton((IsOwned and not IsWearing) and 0 or 1) end local SelectableAvatarsContainer = Utility.Create'Frame' { Name = 'SelectableAvatarsContainer'; Size = UDim2.new(0.6,0,1,0); Position = UDim2.new(0.4,0,0,0); BackgroundTransparency = 1; Parent = MainContainer; } local NoCatalogStatusMessage = Utility.Create'TextLabel' { Name = 'NoCatalogStatusMessage'; Text = Strings:LocalizedString('DefaultErrorPhrase'); Size = UDim2.new(0.9,0,1,-125); Position = UDim2.new(0.05, 0, 0, 0); TextColor3 = GlobalSettings.GreyTextColor; TextWrapped = true; TextTransparency = GlobalSettings.FriendStatusTextTransparency; Font = GlobalSettings.BoldFont; FontSize = GlobalSettings.DescriptionSize; BackgroundTransparency = 1; Visible = false; Parent = SelectableAvatarsContainer; }; local OutfitsTitle = Utility.Create'TextLabel' { Name = 'OutfitsTitle'; Text = Strings:LocalizedString('AvatarOutfitsTitle'):upper(); Size = UDim2.new(1,0,0,40); TextXAlignment = 'Left'; TextColor3 = GlobalSettings.WhiteTextColor; Font = GlobalSettings.RegularFont; FontSize = GlobalSettings.SubHeaderSize; BackgroundTransparency = 1; Visible = false; Parent = SelectableAvatarsContainer; }; local OutfitsScroller = ScrollingGridModule() OutfitsScroller:SetSize(UDim2.new(1,0,1,-OutfitsTitle.Size.Y.Offset - 40)) OutfitsScroller:SetScrollDirection(OutfitsScroller.Enum.ScrollDirection.Horizontal) OutfitsScroller:SetCellSize(Vector2.new(220, 220)) OutfitsScroller:SetSpacing(Vector2.new(25,25)) OutfitsScroller:SetPosition(UDim2.new(0,0,0,OutfitsTitle.Size.Y.Offset)) OutfitsScroller:SetRowColumnConstraint(1) -- OutfitsScroller:SetParent(SelectableAvatarsContainer) local CatalogTitle = Utility.Create'TextLabel' { Name = 'CatalogTitle'; Text = Strings:LocalizedString('AvatarCatalogTitle'):upper(); Size = UDim2.new(1,0,0,40); Position = UDim2.new(0,0,0,0); TextXAlignment = 'Left'; TextColor3 = GlobalSettings.WhiteTextColor; Font = GlobalSettings.RegularFont; FontSize = GlobalSettings.SubHeaderSize; BackgroundTransparency = 1; Parent = SelectableAvatarsContainer; }; local AvatarScroller = ScrollingGridModule() AvatarScroller:SetSize(UDim2.new(1,0,1,-CatalogTitle.Size.Y.Offset - 40)) AvatarScroller:SetScrollDirection(AvatarScroller.Enum.ScrollDirection.Horizontal) AvatarScroller:SetCellSize(Vector2.new(220, 220)) AvatarScroller:SetPosition(UDim2.new(0,0,0,40)) AvatarScroller:SetSpacing(Vector2.new(25,25)) AvatarScroller:SetRowColumnConstraint(2) AvatarScroller:SetParent(SelectableAvatarsContainer) local function SortOutfitsScroller() OutfitsScroller:SortItems( function(a, b) local aObject = a and AvatarObjects[a] and AvatarObjects[a]:GetPackageInfo() local bObject = b and AvatarObjects[b] and AvatarObjects[b]:GetPackageInfo() local aIsEquipped = aObject and aObject:IsWearing() local bIsEquipped = bObject and bObject:IsWearing() local aName = aObject and aObject:GetName() local bName = bObject and bObject:GetName() if aIsEquipped then return true elseif bIsEquipped then return false end if aName and bName then return aName < bName end return aObject ~= nil end) end local function OnAddToOutfitsScroller() CatalogTitle.Position = UDim2.new(0,0,0, CATALOG_BELOW_POSITION) AvatarScroller:SetPosition(UDim2.new(0,0,0,CATALOG_BELOW_POSITION + 40)) AvatarScroller:SetRowColumnConstraint(1) OutfitsTitle.Visible = true OutfitsScroller:SetParent(SelectableAvatarsContainer) SortOutfitsScroller() end local LoaderSpinner = nil local ProfileImageThumbnailLoader = nil local GlobalFadeCount = 1 local function CrossfadeAvatarImage(frontImage, fadeImage, newImageUrl, duration) duration = duration or 0.75 GlobalFadeCount = GlobalFadeCount + 1 local thisFadeCount = GlobalFadeCount local fadeoutDuration = duration spawn(function() local dummyImage = {} local function waitForWearReady() PackageData:AwaitWearAssetRequest() if thisFadeCount == GlobalFadeCount then if ProfileImageThumbnailLoader then ProfileImageThumbnailLoader:Cancel() end ProfileImageThumbnailLoader = ThumbnailLoader:Create(dummyImage, newImageUrl, ThumbnailLoader.Sizes.Large, ThumbnailLoader.AssetType.Avatar, true) local loadResult = ProfileImageThumbnailLoader:LoadAsync(false, false) ProfileImageThumbnailLoader = nil end end if LoaderSpinner then LoaderSpinner:Cleanup() end local newLoaderSpinner = LoadingWidget({Parent = frontImage, ZIndex = 3}, {waitForWearReady}) LoaderSpinner = newLoaderSpinner newLoaderSpinner:AwaitFinished() newLoaderSpinner:Cleanup() if thisFadeCount == GlobalFadeCount then local noPreviousImage = (frontImage.Image == "" and fadeImage.Image == "") if noPreviousImage then fadeoutDuration = 0 else fadeImage.Image = frontImage.Image end Utility.PropertyTweener(fadeImage, 'ImageTransparency', noPreviousImage and 0 or frontImage.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true) Utility.PropertyTweener(CharacterGlowBase, 'ImageTransparency', CharacterGlowBase.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true) Utility.PropertyTweener(CharacterGlowTop, 'ImageTransparency', CharacterGlowTop.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true) frontImage.ImageTransparency = 1 frontImage.Image = dummyImage.Image Utility.PropertyTweener(frontImage, 'ImageTransparency', frontImage.ImageTransparency, 0, duration, Utility.EaseInOutQuad, true) Utility.PropertyTweener(CharacterGlowBase, 'ImageTransparency', CharacterGlowBase.ImageTransparency, GLOW_TRANSPARENCY, duration, Utility.EaseInOutQuad, true) Utility.PropertyTweener(CharacterGlowTop, 'ImageTransparency', CharacterGlowTop.ImageTransparency, GLOW_TRANSPARENCY, duration, Utility.EaseInOutQuad, true) end end) end local function UpdateProfileImage(forceRefresh) MyNameLabel.Text = UserData:GetDisplayName() if forceRefresh or LastWearingPackageAssetId ~= PackageData:GetCachedWearingPackage() or LastWearingOutfitId ~= OutfitData:GetCachedWearingOutfitId() then CrossfadeAvatarImage(ProfileImage, CrossfadeProfileImage, UserData.GetLocalUserIdAsync()) LastWearingPackageAssetId = PackageData:GetCachedWearingPackage() LastWearingOutfitId = OutfitData:GetCachedWearingOutfitId() end end UpdateProfileImage(true) local function onOwnershipChanged(tile, nowOwns) -- Move purchased packages into my outfits if nowOwns then local guiObject = tile and tile:GetGuiObject() if guiObject and AvatarScroller:ContainsItem(guiObject) then AvatarScroller:RemoveItem(guiObject) OutfitsScroller:AddItem(guiObject) OnAddToOutfitsScroller() if inFocus then GuiService.SelectedCoreObject = guiObject end end end end local ownershipChangedCns = {} local function listenToOwnershipChanged(tile) local packageInfo = tile and tile:GetPackageInfo() if packageInfo and not packageInfo:IsOwned() and (packageInfo.OwnershipChanged ~= nil) then if not ownershipChangedCns[tile] then ownershipChangedCns[tile] = packageInfo.OwnershipChanged:connect(function(nowOwns) onOwnershipChanged(tile, nowOwns) end) end end end local function removeListenToOwnershipChanged(tile) if ownershipChangedCns[tile] then ownershipChangedCns[tile]:disconnect() ownershipChangedCns[tile] = nil end end local packagesLoaded = false local outfitsLoaded = true -- NOTE: we don't load outfits atm local LoadAvatarWebDataLoader = nil local function LoadAvatarWebData() local function loadCatalogPackages() local packages = PackageData:GetXboxCatalogPackagesAsync() if packages and not packagesLoaded then packagesLoaded = true for _, packageInfo in pairs(packages) do local avatarItemContainer = AvatarTile(packageInfo) AvatarObjects[avatarItemContainer:GetGuiObject()] = avatarItemContainer if packageInfo:IsOwned() then OutfitsScroller:AddItem(avatarItemContainer:GetGuiObject()) OnAddToOutfitsScroller() else AvatarScroller:AddItem(avatarItemContainer:GetGuiObject()) listenToOwnershipChanged(avatarItemContainer) end if isShown then avatarItemContainer:Show() end end end end local function loadOutfits() local outfits = OutfitData:GetMyOutfitsAsync() if outfits and not outfitsLoaded then outfitsLoaded = true for _, outfitInfo in pairs(outfits) do local outfitItemContainer = OutfitTile(outfitInfo) AvatarObjects[outfitItemContainer:GetGuiObject()] = outfitItemContainer OutfitsScroller:AddItem(outfitItemContainer:GetGuiObject()) OnAddToOutfitsScroller() if isShown then outfitItemContainer:Show() end end end end if LoadAvatarWebDataLoader then return end SelectableAvatarsContainer.Visible = false local containerSize = SelectableAvatarsContainer.Size LoadAvatarWebDataLoader = LoadingWidget( {Parent = MainContainer, Position = SelectableAvatarsContainer.Position + UDim2.new(containerSize.X.Scale / 2, containerSize.X.Offset / 2, containerSize.Y.Scale / 2, containerSize.Y.Offset / 2)}, -- {loadCatalogPackages, loadOutfits}) {loadCatalogPackages}) spawn(function() NoCatalogStatusMessage.Visible = false LoadAvatarWebDataLoader:AwaitFinished() LoadAvatarWebDataLoader:Cleanup() LoadAvatarWebDataLoader = nil SelectableAvatarsContainer.Visible = true -- SortOutfitsScroller() if not (packagesLoaded and outfitsLoaded) then NoCatalogStatusMessage.Visible = true end if inFocus and isShown and GuiService.SelectedCoreObject == nil then GuiService.SelectedCoreObject = this:GetDefaultSelectableObject() end if this.TransitionTweens == nil or #this.TransitionTweens == 0 then this.TransitionTweens = ScreenManager:FadeInSitu(SelectableAvatarsContainer) end end) end LoadAvatarWebData() local function onEquipChanged() local selectedObject = GuiService.SelectedCoreObject if AvatarObjects[selectedObject] then local packageInfo = AvatarObjects[selectedObject]:GetPackageInfo() if packageInfo then UpdateEquipButton(packageInfo:IsOwned(), packageInfo:IsWearing()) end end SortOutfitsScroller() end local lastSelectedObject = GuiService.SelectedCoreObject local function OnSelectedCoreObjectChanged() local selectedObject = GuiService.SelectedCoreObject if AvatarObjects[lastSelectedObject] then AvatarObjects[lastSelectedObject]:RemoveFocus() end if AvatarObjects[selectedObject] then AvatarObjects[selectedObject]:Focus() onEquipChanged() end lastSelectedObject = selectedObject end function this:GetDefaultSelectableObject() if OutfitsScroller.GridItems[1] then return OutfitsScroller.GridItems[1] end if AvatarScroller.GridItems[1] then return AvatarScroller.GridItems[1] end end --[[ Public API ]]-- function this:GetName() return Strings:LocalizedString('AvatarWord') end function this:IsFocused() return inFocus end local debounceSelect = false function this:OnSelectAction() if debounceSelect then return end debounceSelect = true local selectedObject = GuiService.SelectedCoreObject if selectedObject and AvatarObjects[selectedObject] then if AvatarObjects[selectedObject]:Select() then SoundManager:Play('ButtonPress') end end debounceSelect = false end function this:OpenEquippedPackage() if isShown and inFocus then for _, avatarItemContainer in pairs(AvatarObjects) do local packageInfo = avatarItemContainer:GetPackageInfo() if packageInfo then if packageInfo:IsWearing() then avatarItemContainer:OnClick() end end end end end --[[ End Public API ]]-- local profileImageChangeCn = nil function this:Show() isShown = true Utility.DisconnectEvent(profileImageChangeCn) profileImageChangeCn = ProfileImageContainer.Changed:connect(function(prop) if prop == 'AbsoluteSize' then onSizeChanged() end end) onSizeChanged() local lastUpdate = tick() RunService:BindToRenderStep("UpdateAvatarGlow", Enum.RenderPriority.Camera.Value, function() local now = tick() local delta = now - lastUpdate CharacterGlowBase.Rotation = CharacterGlowBase.Rotation + delta * GLOW_BASE_RPM * 6 -- 6 = 360 / 60 CharacterGlowTop.Rotation = CharacterGlowTop.Rotation + delta * GLOW_TOP_RPM * 6 lastUpdate = now end) if not (packagesLoaded and outfitsLoaded) then LoadAvatarWebData() end for _, avatarItemContainer in pairs(AvatarObjects) do avatarItemContainer:Show() listenToOwnershipChanged(avatarItemContainer) local packageInfo = tile and tile:GetPackageInfo() if packageInfo then onOwnershipChanged(avatarItemContainer, packageInfo:IsOwned()) end end UpdateProfileImage() EventHub:addEventListener(EventHub.Notifications["AvatarEquipBegin"], "AvatarPane", function(assetId) UpdateProfileImage(true) spawn(function() PackageData:AwaitWearAssetRequest() LastWearingPackageAssetId = PackageData:GetCachedWearingPackage() end) end) EventHub:addEventListener(EventHub.Notifications["DonnedDifferentPackage"], "AvatarPane", function() onEquipChanged() end) EventHub:addEventListener(EventHub.Notifications["DonnedDifferentOutfit"], "AvatarPane", function() onEquipChanged() UpdateProfileImage() end) local seenXButtonPressed = false local function onSelectAvatar(actionName, inputState, inputObject) if inputState == Enum.UserInputState.Begin then seenXButtonPressed = true elseif inputState == Enum.UserInputState.End and seenXButtonPressed then self:OnSelectAction() end end ContextActionService:BindCoreAction("AvatarPaneSelectAction", onSelectAvatar, false, Enum.KeyCode.ButtonX) self.TransitionTweens = ScreenManager:DefaultFadeIn(MainContainer) SortOutfitsScroller() MainContainer.Parent = lastParent MainContainer.Visible = true end function this:Hide() isShown = false MainContainer.Visible = false profileImageChangeCn = Utility.DisconnectEvent(profileImageChangeCn) RunService:UnbindFromRenderStep("UpdateAvatarGlow") ContextActionService:UnbindCoreAction("AvatarPaneSelectAction") for _, avatarItemContainer in pairs(AvatarObjects) do avatarItemContainer:Hide() removeListenToOwnershipChanged(avatarItemContainer) end EventHub:removeEventListener(EventHub.Notifications["AvatarEquipBegin"], "AvatarPane") EventHub:removeEventListener(EventHub.Notifications["DonnedDifferentPackage"], "AvatarPane") EventHub:removeEventListener(EventHub.Notifications["DonnedDifferentOutfit"], "AvatarPane") ScreenManager:DefaultCancelFade(self.TransitionTweens) self.TransitionTweens = nil -- Clean out saved selected object so when we tab back -- we will start with the default selection self.SavedSelectObject = nil end function this:Focus() inFocus = true Utility.DisconnectEvent(OnGuiServiceChangedConn) OnGuiServiceChangedConn = GuiService.Changed:connect(function(prop) if prop == 'SelectedCoreObject' then OnSelectedCoreObjectChanged() end end) OnSelectedCoreObjectChanged() if self.SavedSelectObject and self.SavedSelectObject:IsDescendantOf(MainContainer) then GuiService.SelectedCoreObject = self.SavedSelectObject else local defaultSelection = self:GetDefaultSelectableObject() if defaultSelection then GuiService.SelectedCoreObject = defaultSelection end end end function this:RemoveFocus() inFocus = false OnGuiServiceChangedConn = Utility.DisconnectEvent(OnGuiServiceChangedConn) local selectedObject = GuiService.SelectedCoreObject if isShown and selectedObject and selectedObject:IsDescendantOf(MainContainer) then self.SavedSelectObject = GuiService.SelectedCoreObject GuiService.SelectedCoreObject = nil end end function this:SetPosition(newPosition) MainContainer.Position = newPosition end function this:SetParent(newParent) lastParent = newParent MainContainer.Parent = newParent end return this end return CreateAvatarPane