This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,10 @@
{
"lint": {
"SameLineStatement": "fatal",
"LocalShadow": "fatal",
"LocalUnused": "fatal",
"FunctionUnused": "fatal",
"ImportUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(emotes)
return {
emotes = emotes,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(equippedEmotes)
return {
equippedEmotes = equippedEmotes,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(segmentIndex)
return {
segmentIndex = segmentIndex,
}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(numberEmotesLoaded)
return {
numberEmotesLoaded = numberEmotesLoaded,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(guiInset)
return {
guiInset = guiInset,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(layout)
return {
layout = layout,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(locale)
return {
locale = locale,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(errorText)
return {
errorText = errorText,
}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,59 @@
local Players = game:GetService("Players")
local Stats = game:GetService("Stats")
local EmotesAnalytics = {}
EmotesAnalytics.__index = EmotesAnalytics
EmotesAnalytics.EventName = {
MenuOpened = "emoteMenuOpened",
EmotePlayed = "emoteTriggered",
}
function EmotesAnalytics.new()
local self = {
eventStreamImpl = nil,
}
setmetatable(self, EmotesAnalytics)
return self
end
function EmotesAnalytics:withEventStream(eventStreamImpl)
self.eventStreamImpl = eventStreamImpl
return self
end
function EmotesAnalytics:onMenuOpened()
local eventContext = "emoteMenu"
local eventName = self.EventName.MenuOpened
local additionalArgs = {
pid = tostring(game.PlaceId),
uid = tostring(Players.LocalPlayer.UserId),
btid = Stats:GetBrowserTrackerId(),
}
self:_getEventStream():setRBXEventStream(eventContext, eventName, additionalArgs)
end
function EmotesAnalytics:onEmotePlayed(slotNumber, assetId)
local eventContext = "emoteMenu"
local eventName = self.EventName.EmotePlayed
local additionalArgs = {
pid = tostring(game.PlaceId),
uid = tostring(Players.LocalPlayer.UserId),
btid = Stats:GetBrowserTrackerId(),
slotNumber = slotNumber,
assetID = assetId,
}
self:_getEventStream():setRBXEventStream(eventContext, eventName, additionalArgs)
end
function EmotesAnalytics:_getEventStream()
assert(self.eventStreamImpl, "EventStream implementation not found. Did you forget to construct withEventStream?")
return self.eventStreamImpl
end
return EmotesAnalytics
@@ -0,0 +1,192 @@
local ContentProvider = game:GetService("ContentProvider")
local CorePackages = game:GetService("CorePackages")
local GetFFlagUseThumbnailUrl = require(game:GetService("CoreGui").RobloxGui.Modules.Common.Flags.GetFFlagUseThumbnailUrl)
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent
local EmotesMenu = Components.Parent
local Actions = EmotesMenu.Actions
local Thunks = EmotesMenu.Thunks
local Utility = EmotesMenu.Utility
local Constants = require(EmotesMenu.Constants)
local GetSegmentFromPosition = require(Utility.GetSegmentFromPosition)
local FocusSegment = require(Actions.FocusSegment)
local HideMenu = require(Actions.HideMenu)
local PlayEmote = require(Thunks.PlayEmote)
local EmotesButtons = Roact.PureComponent:extend("EmotesButtons")
local function positionInsideOuterCircle(frame, centerRelativePos)
local outerCircleRadius = frame.AbsoluteSize.X / 2
local innerCircleRadius = outerCircleRadius * Constants.InnerCircleSizeRatio
return centerRelativePos.Magnitude > innerCircleRadius and centerRelativePos.Magnitude < outerCircleRadius
end
local function getSegmentFromInput(frame, input)
local position = Vector2.new(input.Position.X, input.Position.Y)
position = position - frame.AbsolutePosition
position = position - frame.AbsoluteSize / 2
-- Invert Y position
-- If the Y value is negative then the position is above the center of the emotes wheel
position = Vector2.new(position.X, -position.Y)
if not positionInsideOuterCircle(frame, position) then
return 0
end
return GetSegmentFromPosition(position)
end
local function getRandomAssetId(emotesAssetIds)
if #emotesAssetIds == 0 then
return
end
local rand = math.random(1, #emotesAssetIds)
return emotesAssetIds[rand]
end
local function getEmoteImage(assetId)
if GetFFlagUseThumbnailUrl() then
return Constants.EmotesImage:format(assetId)
else
return ContentProvider.BaseUrl.. Constants.EmotesImageApi:format(assetId)
end
end
function EmotesButtons:render()
local emotesPage = self.props.emotesPage.currentEmotes
local emotesInfoTable = self.props.emotesPage.emotesInfo
local emoteButtons = {}
for segmentIndex, emoteName in pairs(emotesPage) do
local emoteAssetIds = emotesInfoTable[emoteName]
if segmentIndex > Constants.EmotesPerPage then
warn("EmotesMenu: Number of emotes in page exceeds max emotes per page")
break
end
local angle = (360 / Constants.EmotesPerPage) * (segmentIndex - 1) + Constants.SegmentsStartRotation
local radius = Constants.InnerCircleSizeRatio / 2
local spaceAvailable = (1 - Constants.InnerCircleSizeRatio) / 2
local imageSize = spaceAvailable - Constants.SlotNumberSize - Constants.ImageOutsidePadding
local imagePadding = imageSize / 2 + Constants.SlotNumberSize - Constants.ImageOutsidePadding / 2
local cos = math.cos(math.rad(angle))
local xRadiusPos = radius * cos
local xPadding = imagePadding * cos
local xPos = 0.5 + xRadiusPos + xPadding
local sin = math.sin(math.rad(angle))
local yRadiusPos = radius * sin
local yPadding = imagePadding * sin
local yPos = 0.5 + yRadiusPos + yPadding
local assetId = getRandomAssetId(emoteAssetIds)
local emoteImage = getEmoteImage(assetId)
emoteButtons[segmentIndex] = Roact.createElement("ImageLabel", {
Image = emoteImage,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(xPos, 0, yPos, 0),
Size = UDim2.new(imageSize, 0, imageSize, 0),
BackgroundTransparency = 1,
ZIndex = 2, -- TODO: Remove with sibling Zindex
}, {
AsspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
AspectRatio = 1,
}),
})
end
return Roact.createElement("TextButton", {
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
BackgroundTransparency = 1,
Text = "",
[Roact.Event.InputChanged] = function(frame, input)
local inputType = input.UserInputType
if inputType == Enum.UserInputType.MouseMovement or inputType == Enum.UserInputType.Touch then
local segmentIndex = getSegmentFromInput(frame, input)
if segmentIndex == self.props.emotesWheel.focusedSegmentIndex then
return
end
if self.props.emotesPage.currentEmotes[segmentIndex] then
self.props.focusSegment(segmentIndex)
else
self.props.focusSegment(0)
end
end
end,
[Roact.Event.Activated] = function(frame, input)
local segmentIndex = getSegmentFromInput(frame, input)
if segmentIndex == 0 then
self.props.hideMenu()
return
end
local emoteName = self.props.emotesPage.currentEmotes[segmentIndex]
if not emoteName then
return
end
local emoteAssetIds = self.props.emotesPage.emotesInfo[emoteName]
if not emoteAssetIds then
return
end
local assetId = getRandomAssetId(emoteAssetIds)
self.props.playEmote(emoteName, segmentIndex, assetId)
end,
[Roact.Event.MouseLeave] = function()
self.props.focusSegment(0)
end,
}, emoteButtons)
end
local function mapStateToProps(state)
return {
displayOptions = state.displayOptions,
emotesPage = state.emotesPage,
emotesWheel = state.emotesWheel,
}
end
local function mapDispatchToProps(dispatch)
return {
playEmote = function(emoteName, slotNumber, assetId)
return dispatch(PlayEmote(emoteName, slotNumber, assetId))
end,
focusSegment = function(segmentIndex)
return dispatch(FocusSegment(segmentIndex))
end,
hideMenu = function()
return dispatch(HideMenu())
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(EmotesButtons)
@@ -0,0 +1,236 @@
local ContextActionService = game:GetService("ContextActionService")
local CorePackages = game:GetService("CorePackages")
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
local Workspace = game:GetService("Workspace")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local FFlagDisableAutoTranslateForKeyTranslatedContent
= require(RobloxGui.Modules.Flags.FFlagDisableAutoTranslateForKeyTranslatedContent)
local FFlagEmotesMenuFixSelectedObject = game:DefineFastFlag("EmotesMenuFixSelectedObject", false)
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent
local EmotesModules = Components.Parent
local Actions = EmotesModules.Actions
local Thunks = EmotesModules.Thunks
local HideMenu = require(Actions.HideMenu)
local SetLayout = require(Actions.SetLayout)
local OpenMenu = require(Thunks.OpenMenu)
local EmotesWheel = require(Components.EmotesWheel)
local ErrorMessage = require(Components.ErrorMessage)
local Constants = require(EmotesModules.Constants)
local EmotesMenu = Roact.PureComponent:extend("EmotesMenu")
function EmotesMenu:init()
self.savedSelectedCoreObject = nil
self.savedSelectedObject = nil
end
function EmotesMenu:bindActions()
local function toggleMenuFunc(actionName, inputState, inputObj)
if GuiService.MenuIsOpen then
return Enum.ContextActionResult.Pass
end
if inputState == Enum.UserInputState.Begin then
if self.props.displayOptions.menuVisible then
self.props.hideMenu()
else
self.props.openMenu()
end
end
return nil
end
ContextActionService:BindAction(Constants.ToggleMenuAction, toggleMenuFunc, --[[createTouchButton = ]] false,
Constants.EmoteMenuOpenKey)
end
function EmotesMenu:unbindActions()
ContextActionService:UnbindAction(Constants.ToggleMenuAction)
end
function EmotesMenu:saveSelectedObject()
self.savedSelectedCoreObject = GuiService.SelectedCoreObject
GuiService.SelectedCoreObject = nil
self.savedSelectedObject = GuiService.SelectedObject
GuiService.SelectedObject = nil
end
function EmotesMenu:resetSelectedObject()
if GuiService.SelectedCoreObject == nil then
GuiService.SelectedCoreObject = self.savedSelectedCoreObject
end
if GuiService.SelectedObject == nil then
GuiService.SelectedObject = self.savedSelectedObject
end
end
function EmotesMenu:viewPortSizeChanged()
if self.props.layout == Constants.Layout.TenFoot then
return
end
local camera = Workspace.CurrentCamera
if not camera then
return
end
local viewportSize = camera.ViewportSize
if viewportSize == Vector2.new(1, 1) then
-- Viewport is not initialized yet
return
end
local layout = Constants.Layout.Large
local smallScreenX = viewportSize.X < Constants.SmallScreenMaxSize.X
local smallScreenY = viewportSize.Y < Constants.SmallScreenMaxSize.Y
if smallScreenX or smallScreenY then
layout = Constants.Layout.Small
end
self.props.setLayout(layout)
end
function EmotesMenu:currentCameraChanged()
if self.viewportSizeChangedConn then
self.viewportSizeChangedConn:Disconnect()
end
local camera = Workspace.CurrentCamera
if camera then
self.viewportSizeChangedConn = camera:GetPropertyChangedSignal("ViewportSize"):Connect(function()
self:viewPortSizeChanged()
end)
self:viewPortSizeChanged()
end
end
function EmotesMenu:didMount()
self.currentCameraChangedConn = Workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function()
self:currentCameraChanged()
end)
self:currentCameraChanged()
self.menuOpenedConn = GuiService.MenuOpened:Connect(function()
if self.props.displayOptions.menuVisible then
self.props.hideMenu()
end
end)
self.inputOutsideMenuConn = UserInputService.InputBegan:Connect(function(input, gameProcessedEvent)
if gameProcessedEvent then
return
end
if not self.props.displayOptions.menuVisible then
return
end
local inputType = input.UserInputType
if inputType == Enum.UserInputType.MouseButton1 or inputType == Enum.UserInputType.Touch then
self.props.hideMenu()
end
end)
self:bindActions()
end
function EmotesMenu:willUnmount()
self.currentCameraChangedConn:Disconnect()
self.viewportSizeChangedConn:Disconnect()
self.menuOpenedConn:Disconnect()
self.inputOutsideMenuConn:Disconnect()
self.currentCameraChangedConn = nil
self.viewportSizeChangedConn = nil
self.menuOpenedConn = nil
self.inputOutsideMenuConn = nil
self:unbindActions()
end
if FFlagEmotesMenuFixSelectedObject then
function EmotesMenu:didUpdate(prevProps, prevState)
if self.props.displayOptions.menuVisible ~= prevProps.displayOptions.menuVisible then
if self.props.displayOptions.menuVisible then
self:saveSelectedObject()
else
self:resetSelectedObject()
end
end
end
end
function EmotesMenu:render()
local LayoutConstants = Constants.Layouts[self.props.layout]
local guiInset = self.props.displayOptions.guiInset
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Position = UDim2.new(0, 0, 0, -guiInset),
Size = UDim2.new(1, 0, 1, guiInset),
ZIndex = Constants.EmotesMenuZIndex,
AutoLocalize = not FFlagDisableAutoTranslateForKeyTranslatedContent,
}, {
Main = Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = Constants.ScreenAvailable,
BackgroundTransparency = 1,
}, {
AsspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
AspectRatio = 1,
}),
SizeConstraint = Roact.createElement("UISizeConstraint", {
MinSize = LayoutConstants.EmotesWheelMinSize,
MaxSize = LayoutConstants.EmotesWheelMaxSize,
}),
EmotesWheel = Roact.createElement(EmotesWheel),
}),
ErrorMessage = Roact.createElement(ErrorMessage),
})
end
local function mapStateToProps(state)
return {
displayOptions = state.displayOptions,
layout = state.layout,
}
end
local function mapDispatchToProps(dispatch)
return {
openMenu = function()
return dispatch(OpenMenu())
end,
hideMenu = function()
return dispatch(HideMenu())
end,
setLayout = function(layout)
return dispatch(SetLayout(layout))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(EmotesMenu)
@@ -0,0 +1,288 @@
local ContextActionService = game:GetService("ContextActionService")
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local MouseIconOverrideService = require(CorePackages.InGameServices.MouseIconOverrideService)
local Components = script.Parent
local EmotesMenu = Components.Parent
local Actions = EmotesMenu.Actions
local Thunks = EmotesMenu.Thunks
local Utility = EmotesMenu.Utility
local Constants = require(EmotesMenu.Constants)
local GetSegmentFromPosition = require(Utility.GetSegmentFromPosition)
local EmotesButtons = require(Components.EmotesButtons)
local SlotNumbers = require(Components.SlotNumbers)
local WheelBackground = require(Components.WheelBackground)
local FocusSegment = require(Actions.FocusSegment)
local HideMenu = require(Actions.HideMenu)
local PlayEmote = require(Thunks.PlayEmote)
local KEYBINDS_PRIORITY = Enum.ContextActionPriority.High.Value
local EmotesWheel = Roact.PureComponent:extend("EmotesWheel")
local function getRandomAssetId(emotesAssetIds)
if #emotesAssetIds == 0 then
return
end
local rand = math.random(1, #emotesAssetIds)
return emotesAssetIds[rand]
end
function EmotesWheel:bindActions()
if self.actionsBound then
return
end
local function selectEmote(actionName, inputState, inputObj)
local inputVector = Vector2.new(inputObj.Position.X, inputObj.Position.Y)
if inputVector.Magnitude < Constants.ThumbstickThreshold then
return
end
local segmentIndex = GetSegmentFromPosition(inputVector)
if segmentIndex == self.props.emotesWheel.focusedSegmentIndex then
return
end
if self.props.emotesPage.currentEmotes[segmentIndex] then
self.props.focusSegment(segmentIndex)
else
self.props.focusSegment(0)
end
end
ContextActionService:BindActionAtPriority(Constants.EmoteSelectionAction, selectEmote,
--[[createTouchButton = ]] false, KEYBINDS_PRIORITY, Constants.SelectionThumbstick)
local function playSelected(actionName, inputState, inputObj)
if inputState == Enum.UserInputState.Begin then
local focusedSegment = self.props.emotesWheel.focusedSegmentIndex
local emoteName = self.props.emotesPage.currentEmotes[focusedSegment]
if not emoteName then
return
end
local emoteAssetIds = self.props.emotesPage.emotesInfo[emoteName]
if not emoteAssetIds then
return
end
local assetId = getRandomAssetId(emoteAssetIds)
self.props.playEmote(emoteName, focusedSegment, assetId)
end
end
ContextActionService:BindActionAtPriority(Constants.PlaySelectedAction, playSelected,
--[[createTouchButton = ]] false, KEYBINDS_PRIORITY, Constants.PlayEmoteButton)
local function closeMenu(actionName, inputState, inputObj)
if inputState == Enum.UserInputState.Begin then
self.props.hideMenu()
end
end
local closeButtons = {
Constants.EmoteMenuCloseKey,
Constants.EmoteMenuCloseButton,
Constants.EmoteMenuCloseButtonSecondary
}
ContextActionService:BindActionAtPriority(Constants.CloseMenuAction, closeMenu, --[[createTouchButton = ]] false,
KEYBINDS_PRIORITY, unpack(closeButtons))
local function closeMenuNoSink(actionName, inputState, inputObj)
closeMenu(actionName, inputState, inputObj)
return Enum.ContextActionResult.Pass
end
ContextActionService:BindActionAtPriority(Constants.LeaveMenuDontSinkInputAction, closeMenuNoSink,
--[[createTouchButton = ]] false, KEYBINDS_PRIORITY, unpack(Constants.LeaveMenuNoSinkInputs))
local function activateEmoteByNumber(actionName, inputState, inputObj)
if inputState ~= Enum.UserInputState.Begin then
return
end
local pressedSlot
for slot, key in ipairs(Constants.EmoteSlotKeys) do
if key == inputObj.KeyCode then
pressedSlot = slot
break
end
end
if not pressedSlot then
return
end
local emoteName = self.props.emotesPage.currentEmotes[pressedSlot]
if not emoteName then
return
end
local emoteAssetIds = self.props.emotesPage.emotesInfo[emoteName]
if not emoteAssetIds then
return
end
local assetId = getRandomAssetId(emoteAssetIds)
self.props.playEmote(emoteName, pressedSlot, assetId)
end
ContextActionService:BindActionAtPriority(Constants.ActivateEmoteSlotAction, activateEmoteByNumber,
--[[ createTouchButton = ]] false, KEYBINDS_PRIORITY, unpack(Constants.EmoteSlotKeys))
self.actionsBound = true
end
function EmotesWheel:unbindActions()
if self.actionsBound then
ContextActionService:UnbindAction(Constants.CloseMenuAction)
ContextActionService:UnbindAction(Constants.EmoteSelectionAction)
ContextActionService:UnbindAction(Constants.PlaySelectedAction)
ContextActionService:UnbindAction(Constants.LeaveMenuDontSinkInputAction)
ContextActionService:UnbindAction(Constants.ActivateEmoteSlotAction)
self.actionsBound = false
end
end
function EmotesWheel:addCursorOverride()
if self.isUsingGamepad and not self.isCursorHidden then
MouseIconOverrideService.push(Constants.CursorOverrideName, Enum.OverrideMouseIconBehavior.ForceHide)
self.isCursorHidden = true
end
end
function EmotesWheel:removeCursorOverride()
if self.isCursorHidden then
MouseIconOverrideService.pop(Constants.CursorOverrideName)
self.isCursorHidden = false
end
end
function EmotesWheel:connectListeners()
self.inputTypeChangedListener = UserInputService.LastInputTypeChanged:Connect(function(lastInputType)
if Constants.GamepadInputTypes[lastInputType] then
self.isUsingGamepad = true
if self.props.displayOptions.menuVisible then
self:addCursorOverride()
end
else
self.isUsingGamepad = false
self:removeCursorOverride()
end
end)
end
function EmotesWheel:disconnectListeners()
self.inputTypeChangedListener:Disconnect()
end
function EmotesWheel:didUpdate()
if self.props.displayOptions.menuVisible then
self:bindActions()
self:addCursorOverride()
else
self:unbindActions()
self:removeCursorOverride()
end
end
function EmotesWheel:didMount()
if self.props.displayOptions.menuVisible then
self:bindActions()
end
self:connectListeners()
end
function EmotesWheel:willUnmount()
self:unbindActions()
self:disconnectListeners()
end
function EmotesWheel:render()
return Roact.createElement("Frame", {
Active = true,
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
Visible = self.props.displayOptions.menuVisible,
}, {
MouseUnlock = Roact.createElement("TextButton", {
Modal = true,
Size = UDim2.new(0, 0, 0, 0),
Text = "",
Transparency = 1,
}),
Back = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
ZIndex = 1,
}, {
Background = Roact.createElement(WheelBackground),
}),
Front = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
ZIndex = 2,
}, {
EmotesButtons = Roact.createElement(EmotesButtons),
SlotNumbers = Roact.createElement(SlotNumbers),
}),
})
end
local function mapStateToProps(state)
return {
displayOptions = state.displayOptions,
emotesWheel = state.emotesWheel,
emotesPage = state.emotesPage,
}
end
local function mapDispatchToProps(dispatch)
return {
playEmote = function(emoteName, slotNumber, assetId)
return dispatch(PlayEmote(emoteName, slotNumber, assetId))
end,
focusSegment = function(segmentIndex)
return dispatch(FocusSegment(segmentIndex))
end,
hideMenu = function()
return dispatch(HideMenu())
end
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(EmotesWheel)
@@ -0,0 +1,98 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent
local EmotesMenu = Components.Parent
local Actions = EmotesMenu.Actions
local HideError = require(Actions.HideError)
local Constants = require(EmotesMenu.Constants)
local ErrorMessage = Roact.PureComponent:extend("ErrorMessage")
function ErrorMessage:render()
local LayoutConstants = Constants.Layouts[self.props.layout]
return Roact.createElement("Frame", {
Size = LayoutConstants.ErrorFrameSize,
Position = LayoutConstants.ErrorFramePosition,
BackgroundColor3 = Constants.Colors.Black,
BackgroundTransparency = Constants.ErrorFrameBackgroundTransparency,
BorderSizePixel = Constants.ErrorFrameBorderSize,
Selectable = false,
Visible = self.props.displayOptions.errorVisible,
[Roact.Event.TouchTap] = function()
self.props.close()
end,
[Roact.Event.InputBegan] = function(_, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
self.props.close()
end
end,
}, {
AsspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
AspectRatio = LayoutConstants.ErrorFrameAspectRatio,
}),
SizeConstraint = Roact.createElement("UISizeConstraint", {
MinSize = LayoutConstants.ErrorFrameMinSize,
MaxSize = LayoutConstants.ErrorFrameMaxSize,
}),
ErrorIcon = Roact.createElement("ImageLabel", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, LayoutConstants.ErrorIconOffset, 0.5, 0),
Size = UDim2.new(0.2, -LayoutConstants.ErrorIconOffset * 2, 1, -LayoutConstants.ErrorIconOffset * 2),
Image = LayoutConstants.ErrorIcon,
BackgroundTransparency = 1,
}, {
AsspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
AspectRatio = 1,
}),
}),
ErrorText = Roact.createElement("TextLabel", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0.2, -LayoutConstants.ErrorIconOffset, 0.5, 0),
Size = UDim2.new(0.8, 0, 1, 0),
Text = self.props.displayOptions.errorText,
TextScaled = true,
TextSize = LayoutConstants.ErrorTextSize,
TextColor3 = Constants.Colors.White,
TextXAlignment = Enum.TextXAlignment.Left,
Font = LayoutConstants.ErrorTextFont,
BackgroundTransparency = 1,
}, {
SizeConstraint = Roact.createElement("UISizeConstraint", {
MaxSize = Vector2.new(LayoutConstants.ErrorFrameMaxSize.X,
LayoutConstants.ErrorTextSize + Constants.TextPadding),
}),
TextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
MaxTextSize = LayoutConstants.ErrorTextSize,
}),
}),
})
end
local function mapStateToProps(state)
return {
displayOptions = state.displayOptions,
layout = state.layout,
}
end
local function mapDispatchToProps(dispatch)
return {
close = function()
return dispatch(HideError())
end
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(ErrorMessage)
@@ -0,0 +1,53 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent
local EmotesMenu = Components.Parent
local Constants = require(EmotesMenu.Constants)
local SelectionEffect = Roact.PureComponent:extend("SelectionEffect")
function SelectionEffect:render()
local LayoutConstants = Constants.Layouts[self.props.layout]
local focusedIndex = self.props.emotesWheel.focusedSegmentIndex
local angle = (360 / Constants.EmotesPerPage) * (focusedIndex - 1) + Constants.SegmentsStartRotation
local radius = Constants.InnerCircleSizeRatio / 2
local segmentAngle = 360 / Constants.EmotesPerPage
local chordLength = Constants.InnerCircleSizeRatio * math.sin(math.rad(segmentAngle / 2))
return Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
Rotation = angle,
Visible = focusedIndex ~= 0,
}, {
SelectedLine = Roact.createElement("ImageLabel", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(0.5 + radius, -LayoutConstants.SelectedLinePadding, 0.5, 0),
Size = UDim2.new(1, 0, chordLength, -LayoutConstants.SelectedLinePadding),
BackgroundTransparency = 1,
Image = LayoutConstants.SelectedLine,
ZIndex = 4, -- TODO: Remove when CoreGui uses Sibling ZIndex
}, {
AsspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
AspectRatio = LayoutConstants.SelectedLineImageSize.X/LayoutConstants.SelectedLineImageSize.Y,
}),
}),
})
end
local function mapStateToProps(state)
return {
emotesWheel = state.emotesWheel,
layout = state.layout,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, nil)(SelectionEffect)
@@ -0,0 +1,54 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent
local EmotesMenu = Components.Parent
local Constants = require(EmotesMenu.Constants)
local SelectionGradient = Roact.PureComponent:extend("SelectionGradient")
function SelectionGradient:render()
local LayoutConstants = Constants.Layouts[self.props.layout]
local focusedIndex = self.props.emotesWheel.focusedSegmentIndex
local angle = (360 / Constants.EmotesPerPage) * (focusedIndex - 1) + Constants.SegmentsStartRotation
return Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
Rotation = angle,
Visible = focusedIndex ~= 0,
}, {
SelectedGradient = Roact.createElement("ImageLabel", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(0.5, 0, 1, 0),
BackgroundTransparency = 1,
ImageTransparency = Constants.GradientTransparency,
Image = LayoutConstants.SelectedGradient,
ZIndex = 2, -- TODO: Remove when RobloxGui uses sibling ZIndex behavior
}, {
AsspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
AspectRatio = LayoutConstants.SelectedGradientImageSize.X / LayoutConstants.SelectedGradientImageSize.Y,
}),
SizeConstraint = Roact.createElement("UISizeConstraint", {
MaxSize = LayoutConstants.SelectedGradientImageSize,
}),
}),
})
end
local function mapStateToProps(state)
return {
emotesWheel = state.emotesWheel,
layout = state.layout,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, nil)(SelectionGradient)
@@ -0,0 +1,67 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent
local EmotesMenu = Components.Parent
local Constants = require(EmotesMenu.Constants)
local SlotNumbers = Roact.PureComponent:extend("SlotNumbers")
function SlotNumbers:render()
local LayoutConstants = Constants.Layouts[self.props.layout]
local slotNumbers = {}
for slotIndex = 1, Constants.EmotesPerPage do
local angle = (360 / Constants.EmotesPerPage) * (slotIndex - 1) + Constants.SegmentsStartRotation
local radius = Constants.InnerCircleSizeRatio / 2
local numberSize = Constants.SlotNumberSize
local numberPadding = numberSize / 2
local cos = math.cos(math.rad(angle))
local xRadiusPos = radius * cos
local xPadding = numberPadding * cos
local xPos = 0.5 + xRadiusPos + xPadding
local sin = math.sin(math.rad(angle))
local yRadiusPos = radius * sin
local yPadding = numberPadding * sin
local yPos = 0.5 + yRadiusPos + yPadding
slotNumbers[slotIndex] = Roact.createElement("TextLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(xPos, 0, yPos, 0),
Size = UDim2.new(numberSize, 0, numberSize, 0),
BackgroundTransparency = 1,
TextScaled = true,
TextSize = LayoutConstants.SlotNumberTextSize,
TextColor3 = Constants.Colors.White,
Text = slotIndex,
Font = LayoutConstants.SlotNumberFont,
ZIndex = 2, -- TODO: Remove with Sibling ZIndex
}, {
TextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
MaxTextSize = LayoutConstants.SlotNumberTextSize,
}),
})
end
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, slotNumbers)
end
local function mapStateToProps(state)
return {
layout = state.layout,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, nil)(SlotNumbers)
@@ -0,0 +1,70 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent
local EmotesMenu = Components.Parent
local Constants = require(EmotesMenu.Constants)
local SelectionGradient = require(Components.SelectionGradient)
local SelectionEffect = require(Components.SelectionEffect)
local WheelText = require(Components.WheelText)
local WheelBackground = Roact.PureComponent:extend("WheelBackground")
function WheelBackground:render()
local LayoutConstants = Constants.Layouts[self.props.layout]
return Roact.createElement("Folder", {}, {
BackgroundCircleOverlay = Roact.createElement("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
BackgroundTransparency = 1,
Image = LayoutConstants.CircleBackground,
ZIndex = 1,
}),
BackgroundGradient = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 2,
}, {
SelectionGradient = Roact.createElement(SelectionGradient)
}),
BackgroundImage = Roact.createElement("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
BackgroundTransparency = 1,
Image = LayoutConstants.SegmentedCircle,
ZIndex = 3,
}),
Selection = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 4,
}, {
SelectionEffect = Roact.createElement(SelectionEffect),
}),
BackgroundText = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 5,
}, {
MiddleText = Roact.createElement(WheelText),
}),
})
end
local function mapStateToProps(state)
return {
layout = state.layout,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, nil)(WheelBackground)
@@ -0,0 +1,71 @@
local CorePackages = game:GetService("CorePackages")
local StarterPlayer = game:GetService("StarterPlayer")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent
local EmotesMenu = Components.Parent
local CoreScriptsModules = EmotesMenu.Parent
local Constants = require(EmotesMenu.Constants)
local RobloxTranslator = require(CoreScriptsModules.RobloxTranslator)
local WheelText = Roact.PureComponent:extend("WheelText")
function WheelText:getWheelText()
local currentEmotes = self.props.emotesPage.currentEmotes
local locale = self.props.locale
local numberEmotesLoaded = self.props.emotesPage.numberEmotesLoaded
if next(currentEmotes) == nil then
if StarterPlayer.UserEmotesEnabled and numberEmotesLoaded == 0 then
return RobloxTranslator:FormatByKeyForLocale(Constants.LocalizationKeys.NoEmotesEquipped, locale)
else
return RobloxTranslator:FormatByKeyForLocale(Constants.LocalizationKeys.EmotesDisabled, locale)
end
end
local focusedSlot = self.props.emotesWheel.focusedSegmentIndex
local emoteName = currentEmotes[focusedSlot]
if emoteName then
return emoteName
else
return RobloxTranslator:FormatByKeyForLocale(Constants.LocalizationKeys.SelectAnEmote, locale)
end
end
function WheelText:render()
local LayoutConstants = Constants.Layouts[self.props.layout]
local wheelText = self:getWheelText()
return Roact.createElement("TextLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.new(Constants.InnerCircleSizeRatio, -Constants.TextPadding,
Constants.InnerCircleSizeRatio, -Constants.TextPadding),
Position = UDim2.new(0.5, 0, 0.5, 0),
BackgroundTransparency = 1,
Text = wheelText,
TextScaled = true,
TextSize = LayoutConstants.MiddleTextSize,
TextColor3 = Constants.Colors.White,
Font = LayoutConstants.MiddleTextFont,
ZIndex = 5, -- TODO: Remove when CoreGui uses Sibling ZIndex
}, {
TextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
MaxTextSize = LayoutConstants.MiddleTextSize,
}),
})
end
local function mapStateToProps(state)
return {
emotesWheel = state.emotesWheel,
emotesPage = state.emotesPage,
layout = state.layout,
locale = state.locale,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, nil)(WheelText)
@@ -0,0 +1,168 @@
local EmotesMenu = script.Parent
local Layouts = EmotesMenu.Layouts
local SmallLayout = require(Layouts.Small)
local LargeLayout = require(Layouts.Large)
local TenFootLayout = require(Layouts.TenFoot)
local Constants = {
EmotesPerPage = 8,
ErrorDisplayTimeSeconds = 5,
FallbackLocale = "en-us",
LocalizationKeys = {
SelectAnEmote = "InGame.EmotesMenu.SelectAnEmote",
NoEmotesEquipped = "InGame.EmotesMenu.NoEmotesEquipped",
EmotesDisabled = "InGame.EmotesMenu.EmotesDisabled",
ErrorMessages = {
NotSupported = "InGame.EmotesMenu.ErrorMessageNotSupported",
R15Only = "InGame.EmotesMenu.ErrorMessageR15Only",
NoMatchingEmote = "InGame.EmotesMenu.ErrorMessageNoMatchingEmote",
TemporarilyUnavailable = "InGame.EmotesMenu.ErrorMessageTemporarilyUnavailable",
},
},
EmotesImage = "rbxthumb://type=Asset&id=%d&w=420&h=420",
--todo: remove with GetFFlagUseThumbnailUrl
EmotesImageApi = "asset-thumbnail/image?assetId=%d&width=420&height=420&format=png",
EmotesMenuZIndex = 50,
ErrorFrameBorderSize = 0,
ErrorFrameBackgroundTransparency = 0.3,
TextPadding = 10,
SegmentsStartRotation = -90,
-- Ratio of diameter of inner circle to emote wheel image width
InnerCircleSizeRatio = 0.45,
-- Size of slot numbers relative to emote wheel size
SlotNumberSize = 0.1,
ImageOutsidePadding = 0.025,
GradientTransparency = 0.5,
CursorOverrideName = "EmotesMenuCursorOverride",
GamepadInputTypes = {
[Enum.UserInputType.Gamepad1] = true,
[Enum.UserInputType.Gamepad2] = true,
[Enum.UserInputType.Gamepad3] = true,
[Enum.UserInputType.Gamepad4] = true,
[Enum.UserInputType.Gamepad5] = true,
[Enum.UserInputType.Gamepad6] = true,
[Enum.UserInputType.Gamepad7] = true,
[Enum.UserInputType.Gamepad8] = true,
},
-- User will leave the menu if it's open with any of these inputs but the input won't be sunk
LeaveMenuNoSinkInputs = {
Enum.KeyCode.W,
Enum.KeyCode.A,
Enum.KeyCode.S,
Enum.KeyCode.D,
Enum.KeyCode.Up,
Enum.KeyCode.Left,
Enum.KeyCode.Down,
Enum.KeyCode.Right,
Enum.KeyCode.Space,
Enum.KeyCode.Tab,
Enum.KeyCode.Slash,
Enum.KeyCode.Backquote,
},
EmoteSlotKeys = {
Enum.KeyCode.One,
Enum.KeyCode.Two,
Enum.KeyCode.Three,
Enum.KeyCode.Four,
Enum.KeyCode.Five,
Enum.KeyCode.Six,
Enum.KeyCode.Seven,
Enum.KeyCode.Eight,
},
EmoteMenuOpenKey = Enum.KeyCode.Period,
EmoteMenuCloseKey = Enum.KeyCode.Escape,
EmoteMenuCloseButton = Enum.KeyCode.ButtonB,
EmoteMenuCloseButtonSecondary = Enum.KeyCode.ButtonStart,
SelectionThumbstick = Enum.KeyCode.Thumbstick1,
ThumbstickThreshold = 0.8,
PlayEmoteButton = Enum.KeyCode.ButtonA,
ToggleMenuAction = "EmotesMenuToggleAction",
CloseMenuAction = "EmotesMenuCloseAction",
EmoteSelectionAction = "EmotesMenuSelectionAction",
ActivateEmoteSlotAction = "EmotesMenuActivateEmoteSlotAction",
PlaySelectedAction = "EmotesMenuPlaySelectedAction",
LeaveMenuDontSinkInputAction = "EmotesMenuLeaveMenuDontSinkInputAction",
-- Emotes Menu can use up to 90% of the screen horizontally and 75% vertically
ScreenAvailable = UDim2.new(0.9, 0, 0.75, 0),
-- Use the Large layout if the screen is larger than this size
-- Values taken from isSmallTouchScreen in Utility module
SmallScreenMaxSize = Vector2.new(700, 500),
Layout = {
Small = 0,
Large = 1,
TenFoot = 2,
},
Colors = {
White = Color3.new(1, 1, 1),
Black = Color3.new(0, 0, 0),
}
}
Constants.Layouts = {
[Constants.Layout.Small] = SmallLayout,
[Constants.Layout.Large] = LargeLayout,
[Constants.Layout.TenFoot] = TenFootLayout,
}
local function makeTableConstant(name, tbl)
setmetatable(tbl, {
__newindex = function()
end,
__index = function(t, index)
error(name.. " table has no index: " ..tostring(index), 2)
end
})
end
local constantTables = {
["Constants"] = Constants,
["Constants.Colors"] = Constants.Colors,
["Constants.Layout"] = Constants.Layout,
["Constants.LeaveMenuNoSinkInputs"] = Constants.LeaveMenuNoSinkInputs,
["Constants.LocalizationKeys"] = Constants.LocalizationKeys,
["Constants.LocalizationKeys.ErrorMessages"] = Constants.LocalizationKeys.ErrorMessages,
["Constants.Layouts"] = Constants.Layouts,
["Constants.Layouts.Small"] = Constants.Layouts[Constants.Layout.Small],
["Constants.Layouts.Large"] = Constants.Layouts[Constants.Layout.Large],
["Constants.Layouts.TenFoot"] = Constants.Layouts[Constants.Layout.TenFoot],
}
for name, tbl in pairs(constantTables) do
makeTableConstant(name, tbl)
end
return Constants
@@ -0,0 +1,392 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local GuiService = game:GetService("GuiService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local LocalizationService = game:GetService("LocalizationService")
local StarterGui = game:GetService("StarterGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local FFlagAllowEmotesMenuWhenTopbarIsDisabled = game:DefineFastFlag("AllowEmotesMenuWhenTopbarIsDisabled", false)
local FFlagCoreScriptLoadEmotesFromWeb = settings():GetFFlag("CoreScriptLoadEmotesFromWeb")
-- Wait for LocalPlayer to exist
local LocalPlayer = Players.LocalPlayer
if not LocalPlayer then
Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
LocalPlayer = Players.LocalPlayer
end
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local RoactRodux = require(CorePackages.RoactRodux)
local EmotesModules = script.Parent
local CoreScriptModules = EmotesModules.Parent
local Actions = EmotesModules.Actions
local Components = EmotesModules.Components
local Reducers = EmotesModules.Reducers
local Thunks = EmotesModules.Thunks
local Utility = EmotesModules.Utility
local Backpack = require(CoreScriptModules.BackpackScript)
local Chat = require(CoreScriptModules.ChatSelector)
local TenFootInterface = require(CoreScriptModules.TenFootInterface)
local CanPlayEmotes = require(Utility.CanPlayEmotes)
local Constants = require(EmotesModules.Constants)
local EmotesMenu = require(Components.EmotesMenu)
local EmotesMenuReducer = require(Reducers.EmotesMenuReducer)
local LoadEmotesFromWeb = require(Thunks.LoadEmotesFromWeb)
local OpenMenu = require(Thunks.OpenMenu)
local HideMenu = require(Actions.HideMenu)
local EmotesChanged = require(Actions.EmotesChanged)
local EquippedEmotesChanged = require(Actions.EquippedEmotesChanged)
local NumberEmotesLoadedChanged = require(Actions.NumberEmotesLoadedChanged)
local SetGuiInset = require(Actions.SetGuiInset)
local SetLayout = require(Actions.SetLayout)
local SetLocale = require(Actions.SetLocale)
local EmotesMenuMaster = {}
EmotesMenuMaster.__index = EmotesMenuMaster
local function isClient()
return RunService:IsClient() and
not RunService:IsEdit() and
not RunService:IsRunMode()
end
function EmotesMenuMaster:isOpen()
return self.store:getState().displayOptions.menuVisible
end
function EmotesMenuMaster:open()
self.store:dispatch(OpenMenu())
end
function EmotesMenuMaster:close()
self.store:dispatch(HideMenu())
end
function EmotesMenuMaster:setTopBarEnabled(isEnabled)
self.topBarEnabled = isEnabled
if FFlagAllowEmotesMenuWhenTopbarIsDisabled then
if not isEnabled then
-- Don't allow user to get stuck in a state where the Emotes Menu is stuck open
if self:isOpen() then
self:close()
end
end
else
local isEmotesMenuEnabled = StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.EmotesMenu)
if isEnabled and isEmotesMenuEnabled and self.canPlayEmotes then
self:_mount()
else
self:_unmount()
end
end
end
function EmotesMenuMaster:_connectCoreGuiListeners()
Backpack.StateChanged.Event:Connect(function(isBackpackOpen)
if not isBackpackOpen then
return
end
if self:isOpen() then
self:close()
end
end)
Chat.VisibilityStateChanged:connect(function(isChatVisible)
if not isChatVisible then
return
end
if self:isOpen() then
self:close()
end
end)
end
function EmotesMenuMaster:_connectApiListeners()
StarterGui.CoreGuiChangedSignal:connect(function(coreGuiType, enabled)
if not FFlagAllowEmotesMenuWhenTopbarIsDisabled and not self.topBarEnabled then
return
end
if not self.canPlayEmotes then
return
end
if coreGuiType == Enum.CoreGuiType.EmotesMenu or coreGuiType == Enum.CoreGuiType.All then
if enabled then
self:_mount()
else
self:_unmount()
end
end
end)
GuiService.EmotesMenuOpenChanged:Connect(function(isOpen)
if isOpen then
if not self:isOpen() then
self:open()
end
else
if self:isOpen() then
self:close()
end
end
end)
end
function EmotesMenuMaster:_loadEmotesFromWeb()
self.store:dispatch(LoadEmotesFromWeb())
end
function EmotesMenuMaster:_onEquippedEmotesChanged(newEquippedEmotes)
self.store:dispatch(EquippedEmotesChanged(newEquippedEmotes))
end
function EmotesMenuMaster:_onEmotesChanged(newEmotes)
self.store:dispatch(EmotesChanged(newEmotes))
end
function EmotesMenuMaster:_onNumberEmotesLoadedChanged(newNumberEmotesLoaded)
self.store:dispatch(NumberEmotesLoadedChanged(newNumberEmotesLoaded))
end
function EmotesMenuMaster:_onHumanoidDescriptionChanged(humanoidDescription)
if self.equippedEmotesChangedConn then
self.equippedEmotesChangedConn:Disconnect()
self.equippedEmotesChangedConn = nil
end
if self.emotesChangedConn then
self.emotesChangedConn:Disconnect()
self.emotesChangedConn = nil
end
if self.numberEmotesLoadedChangedConn then
self.numberEmotesLoadedChangedConn:Disconnect()
self.numberEmotesLoadedChangedConn = nil
end
if self.humanoidDescriptionAncestryConn then
self.humanoidDescriptionAncestryConn:Disconnect()
self.humanoidDescriptionAncestryConn = nil
end
if humanoidDescription then
self.equippedEmotesChangedConn = humanoidDescription.EquippedEmotesChanged:Connect(function(newEquippedEmotes)
self:_onEquippedEmotesChanged(newEquippedEmotes)
end)
self:_onEquippedEmotesChanged(humanoidDescription:GetEquippedEmotes())
self.emotesChangedConn = humanoidDescription.EmotesChanged:Connect(function(newEmotes)
self:_onEmotesChanged(newEmotes)
end)
if FFlagCoreScriptLoadEmotesFromWeb and next(humanoidDescription:GetEmotes()) == nil
and humanoidDescription.NumberEmotesLoaded == -1 then
self:_loadEmotesFromWeb()
else
self:_onEmotesChanged(humanoidDescription:GetEmotes())
end
local numberEmotesChangedSignal = humanoidDescription:GetPropertyChangedSignal("NumberEmotesLoaded")
self.numberEmotesLoadedChangedConn = numberEmotesChangedSignal:Connect(function(newNumberEmotesLoaded)
self:_onNumberEmotesLoadedChanged(newNumberEmotesLoaded)
end)
self:_onNumberEmotesLoadedChanged(humanoidDescription.NumberEmotesLoaded)
self.humanoidDescriptionAncestryConn = humanoidDescription.AncestryChanged:Connect(function(child, parent)
if child == humanoidDescription and parent == nil then
self:_onHumanoidDescriptionChanged(nil)
end
end)
else
self:_onEquippedEmotesChanged({})
self:_onEmotesChanged({})
self:_onNumberEmotesLoadedChanged(-1)
end
end
function EmotesMenuMaster:_onHumanoidChanged(humanoid)
if self.humanoidChildAddedConn then
self.humanoidChildAddedConn:Disconnect()
self.humanoidChildAddedConn = nil
end
if self.humanoidAncestryChangedConn then
self.humanoidAncestryChangedConn:Disconnect()
self.humanoidAncestryChangedConn = nil
end
if humanoid then
self.humanoidChildAddedConn = humanoid.ChildAdded:Connect(function(child)
if child:IsA("HumanoidDescription") then
self:_onHumanoidDescriptionChanged(child)
end
end)
local humanoidDescription = humanoid:FindFirstChildOfClass("HumanoidDescription")
if humanoidDescription then
self:_onHumanoidDescriptionChanged(humanoidDescription)
elseif FFlagCoreScriptLoadEmotesFromWeb then
self:_loadEmotesFromWeb()
end
self.humanoidAncestryChangedConn = humanoid.AncestryChanged:Connect(function(child, parent)
if child == humanoid and parent == nil then
self:_onHumanoidChanged(nil)
end
end)
else
self:_onHumanoidDescriptionChanged(nil)
end
end
function EmotesMenuMaster:_onCharacterAdded(character)
if self.characterChildAddedConn then
self.characterChildAddedConn:Disconnect()
self.characterChildAddedConn = nil
end
self.characterChildAddedConn = character.ChildAdded:Connect(function(child)
if child:IsA("Humanoid") then
self:_onHumanoidChanged(child)
end
end)
local humanoid = character:FindFirstChildWhichIsA("Humanoid")
if humanoid then
self:_onHumanoidChanged(humanoid)
end
end
function EmotesMenuMaster:_connectListeners()
self:_connectCoreGuiListeners()
self:_connectApiListeners()
LocalPlayer.CharacterAdded:Connect(function(character) self:_onCharacterAdded(character) end)
if LocalPlayer.Character then
self:_onCharacterAdded(LocalPlayer.Character)
end
LocalizationService:GetPropertyChangedSignal("RobloxLocaleId"):Connect(function()
self.store:dispatch(SetLocale(LocalizationService.RobloxLocaleId))
end)
CanPlayEmotes.ChangedEvent.Event:connect(function(canPlay)
self.canPlayEmotes = canPlay
local isEmotesMenuEnabled = StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.EmotesMenu)
if FFlagAllowEmotesMenuWhenTopbarIsDisabled then
if canPlay and isEmotesMenuEnabled then
self:_mount()
else
self:_unmount()
end
else
if canPlay and self.topBarEnabled and isEmotesMenuEnabled then
self:_mount()
else
self:_unmount()
end
end
end)
end
function EmotesMenuMaster:_onStateChanged(newState, oldState)
if oldState.displayOptions.menuVisible ~= newState.displayOptions.menuVisible then
self.EmotesMenuToggled:Fire(newState.displayOptions.menuVisible)
GuiService:SetEmotesMenuOpen(newState.displayOptions.menuVisible)
end
end
function EmotesMenuMaster:_mount()
if not self.instance then
local app = Roact.createElement(RoactRodux.StoreProvider, {
store = self.store,
}, {
EmotesMenu = Roact.createElement(EmotesMenu)
})
self.instance = Roact.mount(app, RobloxGui, "EmotesMenu")
self.MenuIsVisible = true
self.MenuVisibilityChanged:Fire(true)
end
end
function EmotesMenuMaster:_unmount()
if self.instance then
Roact.unmount(self.instance)
self.instance = nil
self.MenuIsVisible = false
self.MenuVisibilityChanged:Fire(false)
end
end
function EmotesMenuMaster.new()
local self = setmetatable({}, EmotesMenuMaster)
self.canPlayEmotes = CanPlayEmotes.Value
self.topBarEnabled = true
-- Bindable that can be used by other modules to see when the Emotes Menu is opened or closed.
-- Fired with a bool indicating whether the EmotesMenu is currently open
self.EmotesMenuToggled = Instance.new("BindableEvent")
-- Indicates whether the menu is currently mounted
self.MenuIsVisible = false
-- Bindable that can be used by other modules to see when the Emotes Menu should
-- not be visible to users or should be visible to users.
self.MenuVisibilityChanged = Instance.new("BindableEvent")
self.store = Rodux.Store.new(EmotesMenuReducer, {}, {
Rodux.thunkMiddleware,
})
-- If not running in the correct context return early and don't mount the Gui
-- This is necessary for non-networked DataModels where CoreScripts can still run
if not isClient() then
return self
end
self.store.changed:connect(function(newState, oldState)
self:_onStateChanged(newState, oldState)
end)
self.store:dispatch(SetLocale(LocalizationService.RobloxLocaleId))
if TenFootInterface:IsEnabled() then
self.store:dispatch(SetLayout(Constants.Layout.TenFoot))
end
local inGameGlobalGuiInset = settings():GetFVariable("InGameGlobalGuiInset")
if not RobloxGui.IgnoreGuiInset then
self.store:dispatch(SetGuiInset(inGameGlobalGuiInset))
end
self:_mount()
self:_connectListeners()
return self
end
return EmotesMenuMaster.new()
@@ -0,0 +1,34 @@
return {
EmotesWheelMinSize = Vector2.new(200, 200),
EmotesWheelMaxSize = Vector2.new(400, 400),
ErrorFrameSize = UDim2.new(0.6, 0, 0.2, 0),
ErrorFramePosition = UDim2.new(0, 0, 0.1, 0),
ErrorFrameAspectRatio = 6,
ErrorFrameMinSize = Vector2.new(336, 56),
ErrorFrameMaxSize = Vector2.new(432, 72),
ErrorTextFont = Enum.Font.Gotham,
ErrorTextSize = 36,
ErrorIcon = "rbxasset://textures/ui/Emotes/ErrorIcon.png",
ErrorIconOffset = 10,
MiddleTextSize = 28,
MiddleTextFont = Enum.Font.Gotham,
SlotNumberTextSize = 24,
SlotNumberFont = Enum.Font.Gotham,
CircleBackground = "rbxasset://textures/ui/Emotes/Large/CircleBackground.png",
SegmentedCircle = "rbxasset://textures/ui/Emotes/Large/SegmentedCircle.png",
SelectedLine = "rbxasset://textures/ui/Emotes/Large/SelectedLine.png",
SelectedLineImageSize = Vector2.new(11, 67),
SelectedLinePadding = 4,
SelectedGradient = "rbxasset://textures/ui/Emotes/Large/SelectedGradient.png",
SelectedGradientImageSize = Vector2.new(183, 141),
}
@@ -0,0 +1,34 @@
return {
EmotesWheelMinSize = Vector2.new(150, 150),
EmotesWheelMaxSize = Vector2.new(300, 300),
ErrorFrameSize = UDim2.new(0.6, 0, 0.2, 0),
ErrorFramePosition = UDim2.new(0, 0, 0.1, 0),
ErrorFrameAspectRatio = 6,
ErrorFrameMinSize = Vector2.new(336, 56),
ErrorFrameMaxSize = Vector2.new(432, 72),
ErrorTextFont = Enum.Font.Gotham,
ErrorTextSize = 24,
ErrorIcon = "rbxasset://textures/ui/Emotes/ErrorIcon.png",
ErrorIconOffset = 10,
MiddleTextSize = 18,
MiddleTextFont = Enum.Font.Gotham,
SlotNumberTextSize = 16,
SlotNumberFont = Enum.Font.Gotham,
CircleBackground = "rbxasset://textures/ui/Emotes/Small/CircleBackground.png",
SegmentedCircle = "rbxasset://textures/ui/Emotes/Small/SegmentedCircle.png",
SelectedLine = "rbxasset://textures/ui/Emotes/Small/SelectedLine.png",
SelectedLineImageSize = Vector2.new(8, 50),
SelectedLinePadding = 2,
SelectedGradient = "rbxasset://textures/ui/Emotes/Small/SelectedGradient.png",
SelectedGradientImageSize = Vector2.new(138, 106),
}
@@ -0,0 +1,34 @@
return {
EmotesWheelMinSize = Vector2.new(320, 320),
EmotesWheelMaxSize = Vector2.new(640, 640),
ErrorFrameSize = UDim2.new(0.6, 0, 0.2, 0),
ErrorFramePosition = UDim2.new(0, 0, 0.1, 0),
ErrorFrameAspectRatio = 6,
ErrorFrameMinSize = Vector2.new(336, 56),
ErrorFrameMaxSize = Vector2.new(432, 72),
ErrorTextFont = Enum.Font.Gotham,
ErrorTextSize = 48,
ErrorIcon = "rbxasset://textures/ui/Emotes/ErrorIcon.png",
ErrorIconOffset = 10,
MiddleTextSize = 48,
MiddleTextFont = Enum.Font.Gotham,
SlotNumberTextSize = 48,
SlotNumberFont = Enum.Font.Gotham,
CircleBackground = "rbxasset://textures/ui/Emotes/TenFoot/CircleBackground.png",
SegmentedCircle = "rbxasset://textures/ui/Emotes/TenFoot/SegmentedCircle.png",
SelectedLine = "rbxasset://textures/ui/Emotes/TenFoot/SelectedLine.png",
SelectedLineImageSize = Vector2.new(18, 107),
SelectedLinePadding = 5,
SelectedGradient = "rbxasset://textures/ui/Emotes/TenFoot/SelectedGradient.png",
SelectedGradientImageSize = Vector2.new(292, 225),
}
@@ -0,0 +1,59 @@
local CorePackages = game:GetService("CorePackages")
local Cryo = require(CorePackages.Cryo)
local Rodux = require(CorePackages.Rodux)
local Reducers = script.Parent
local EmotesMenu = Reducers.Parent
local Actions = EmotesMenu.Actions
local ShowMenu = require(Actions.ShowMenu)
local HideMenu = require(Actions.HideMenu)
local ShowError = require(Actions.ShowError)
local HideError = require(Actions.HideError)
local SetGuiInset = require(Actions.SetGuiInset)
local default = {
menuVisible = false,
guiInset = 0,
errorVisible = false,
errorText = "",
}
return Rodux.createReducer(default, {
[ShowMenu.name] = function(state, action)
return Cryo.Dictionary.join(state, {
menuVisible = true,
})
end,
[HideMenu.name] = function(state, action)
return Cryo.Dictionary.join(state, {
menuVisible = false,
})
end,
[ShowError.name] = function(state, action)
return Cryo.Dictionary.join(state, {
errorVisible = true,
errorText = action.errorText,
})
end,
[HideError.name] = function(state, action)
return Cryo.Dictionary.join(state, {
errorVisible = false,
})
end,
[SetGuiInset.name] = function(state, action)
return Cryo.Dictionary.join(state, {
guiInset = action.guiInset,
})
end,
})
@@ -0,0 +1,21 @@
local CorePackages = game:GetService("CorePackages")
local Rodux = require(CorePackages.Rodux)
local Reducers = script.Parent
local DisplayOptions = require(Reducers.DisplayOptions)
local EmotesPage = require(Reducers.EmotesPage)
local EmotesWheel = require(Reducers.EmotesWheel)
local Layout = require(Reducers.Layout)
local Locale = require(Reducers.Locale)
local EmotesMenuReducer = Rodux.combineReducers({
displayOptions = DisplayOptions,
emotesPage = EmotesPage,
emotesWheel = EmotesWheel,
layout = Layout,
locale = Locale,
})
return EmotesMenuReducer
@@ -0,0 +1,60 @@
local CorePackages = game:GetService("CorePackages")
local Cryo = require(CorePackages.Cryo)
local Rodux = require(CorePackages.Rodux)
local Reducers = script.Parent
local EmotesMenu = Reducers.Parent
local Actions = EmotesMenu.Actions
local EmotesChanged = require(Actions.EmotesChanged)
local EquippedEmotesChanged = require(Actions.EquippedEmotesChanged)
local NumberEmotesLoadedChanged = require(Actions.NumberEmotesLoadedChanged)
local default = {
emotesInfo = {},
equippedEmotes = {},
numberEmotesLoaded = -1,
currentEmotes = {},
}
local function createCurrentEmotes(emotesInfoTable, equippedEmotesTable)
local currentEmotes = {}
for _, emoteInfo in ipairs(equippedEmotesTable) do
local slot = emoteInfo.Slot
local emoteName = emoteInfo.Name
if slot and emoteName then
if emotesInfoTable[emoteName] then
currentEmotes[slot] = emoteName
end
end
end
return currentEmotes
end
return Rodux.createReducer(default, {
[EmotesChanged.name] = function(state, action)
return Cryo.Dictionary.join(state, {
emotesInfo = action.emotes,
currentEmotes = createCurrentEmotes(action.emotes, state.equippedEmotes),
})
end,
[EquippedEmotesChanged.name] = function(state, action)
return Cryo.Dictionary.join(state, {
equippedEmotes = action.equippedEmotes,
currentEmotes = createCurrentEmotes(state.emotesInfo, action.equippedEmotes),
})
end,
[NumberEmotesLoadedChanged.name] = function(state, action)
return Cryo.Dictionary.join(state, {
numberEmotesLoaded = action.numberEmotesLoaded,
})
end,
})
@@ -0,0 +1,28 @@
local CorePackages = game:GetService("CorePackages")
local Cryo = require(CorePackages.Cryo)
local Rodux = require(CorePackages.Rodux)
local Reducers = script.Parent
local EmotesMenu = Reducers.Parent
local Actions = EmotesMenu.Actions
local FocusSegment = require(Actions.FocusSegment)
local HideMenu = require(Actions.HideMenu)
local default = {
focusedSegmentIndex = 0,
}
return Rodux.createReducer(default, {
[FocusSegment.name] = function(state, action)
return Cryo.Dictionary.join(state, {
focusedSegmentIndex = action.segmentIndex,
})
end,
-- Reset EmotesWheel state when the menu is hidden
[HideMenu.name] = function(state, action)
return Cryo.Dictionary.join(state, default)
end,
})
@@ -0,0 +1,17 @@
local CorePackages = game:GetService("CorePackages")
local Rodux = require(CorePackages.Rodux)
local Reducers = script.Parent
local EmotesMenu = Reducers.Parent
local Actions = EmotesMenu.Actions
local SetLayout = require(Actions.SetLayout)
local Constants = require(EmotesMenu.Constants)
return Rodux.createReducer(Constants.Layout.Large, {
[SetLayout.name] = function(state, action)
return action.layout
end,
})
@@ -0,0 +1,17 @@
local CorePackages = game:GetService("CorePackages")
local Rodux = require(CorePackages.Rodux)
local Reducers = script.Parent
local EmotesMenu = Reducers.Parent
local Actions = EmotesMenu.Actions
local SetLocale = require(Actions.SetLocale)
local Constants = require(EmotesMenu.Constants)
return Rodux.createReducer(Constants.FallbackLocale, {
[SetLocale.name] = function(state, action)
return action.locale
end,
})
@@ -0,0 +1,66 @@
local CorePackages = game:GetService("CorePackages")
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local HttpService = game:GetService("HttpService")
local StarterPlayer = game:GetService("StarterPlayer")
local Thunks = script.Parent
local EmotesMenu = Thunks.Parent
local Actions = EmotesMenu.Actions
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
local EmotesChanged = require(Actions.EmotesChanged)
local EquippedEmotesChanged = require(Actions.EquippedEmotesChanged)
local cachedEmotes = nil
local function getEmotesInfo()
if cachedEmotes then
return cachedEmotes
end
local getEmotesUrl = Url.AVATAR_URL .. "v1/emotes"
local success, result = pcall(HttpRbxApiService.GetAsyncFullUrl, HttpRbxApiService, getEmotesUrl)
if not success then
return {}
end
success, result = pcall(HttpService.JSONDecode, HttpService, result)
if not success then
return {}
end
cachedEmotes = result
return result
end
local function loadEmotes(store)
local equippedEmotesInfo = getEmotesInfo()
local emotesTable = {}
local equippedEmotesTable = {}
for index, emoteInfo in ipairs(equippedEmotesInfo) do
emotesTable[emoteInfo.assetName] = { emoteInfo.assetId }
equippedEmotesTable[index] = {
Slot = emoteInfo.position,
Name = emoteInfo.assetName,
}
end
store:dispatch(EmotesChanged(emotesTable))
store:dispatch(EquippedEmotesChanged(equippedEmotesTable))
end
local function LoadEmotesFromWeb()
return function(store)
if not StarterPlayer.UserEmotesEnabled then
return
end
coroutine.wrap(loadEmotes)(store)
end
end
return LoadEmotesFromWeb
@@ -0,0 +1,40 @@
local CorePackages = game:GetService("CorePackages")
local GuiService = game:GetService("GuiService")
local Thunks = script.Parent
local EmotesMenu = Thunks.Parent
local Actions = EmotesMenu.Actions
local CoreScriptModules = EmotesMenu.Parent
local EventStream = require(CorePackages.AppTempCommon.Temp.EventStream)
local Analytics = require(EmotesMenu.Analytics)
local Backpack = require(CoreScriptModules.BackpackScript)
local ShowMenu = require(Actions.ShowMenu)
local EmotesAnalytics = Analytics.new():withEventStream(EventStream.new())
local function OpenMenu(emoteName)
return function(store)
if GuiService.MenuIsOpen then
return
end
if Backpack.IsOpen then
Backpack.OpenClose()
end
-- If user is interacting with the backpack it can stay open
if Backpack.IsOpen then
return
end
EmotesAnalytics:onMenuOpened()
-- Backpack was closed, show the emotes menu
store:dispatch(ShowMenu())
end
end
return OpenMenu
@@ -0,0 +1,107 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local FFlagCoreScriptLoadEmotesFromWeb = settings():GetFFlag("CoreScriptLoadEmotesFromWeb")
local LocalPlayer = Players.LocalPlayer
local Thunks = script.Parent
local EmotesMenu = Thunks.Parent
local CoreScriptModules = EmotesMenu.Parent
local Actions = EmotesMenu.Actions
local Analytics = require(EmotesMenu.Analytics)
local Constants = require(EmotesMenu.Constants)
local EventStream = require(CorePackages.AppTempCommon.Temp.EventStream)
local RobloxTranslator = require(CoreScriptModules.RobloxTranslator)
local HideMenu = require(Actions.HideMenu)
local HideError = require(Actions.HideError)
local ShowError = require(Actions.ShowError)
local EmotesAnalytics = Analytics.new():withEventStream(EventStream.new())
local function emoteInHumanoidDescription(humanoidDescription, emoteName)
if not humanoidDescription then
return false
end
local emotes = humanoidDescription:GetEmotes()
if emotes and emotes[emoteName] then
return true
end
return false
end
local function handlePlayFailure(store, reasonLocalizationKey)
if reasonLocalizationKey then
local locale = store:getState().locale
local reason = RobloxTranslator:FormatByKeyForLocale(reasonLocalizationKey, locale)
store:dispatch(ShowError(reason))
delay(Constants.ErrorDisplayTimeSeconds, function()
store:dispatch(HideError())
end)
end
store:dispatch(HideMenu())
end
local function PlayEmote(emoteName, slotNumber, emoteAssetId)
return function(store)
local character = LocalPlayer.Character
if not character then
handlePlayFailure(store, Constants.LocalizationKeys.ErrorMessages.TemporarilyUnavailable)
return
end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then
handlePlayFailure(store, Constants.LocalizationKeys.ErrorMessages.TemporarilyUnavailable)
return
end
if humanoid.RigType ~= Enum.HumanoidRigType.R15 then
handlePlayFailure(store, Constants.LocalizationKeys.ErrorMessages.R15Only)
return
end
local animate = character:FindFirstChild("Animate")
if not animate then
handlePlayFailure(store, Constants.LocalizationKeys.ErrorMessages.NotSupported)
return
end
local humanoidDescription = humanoid:FindFirstChildOfClass("HumanoidDescription")
if not FFlagCoreScriptLoadEmotesFromWeb and not humanoidDescription then
handlePlayFailure(store, Constants.LocalizationKeys.ErrorMessages.NotSupported)
return
end
local playEmoteBindable = animate:FindFirstChild("PlayEmote")
if playEmoteBindable and playEmoteBindable:IsA("BindableFunction") then
store:dispatch(HideMenu())
local playEmoteFunction = function() return humanoid:PlayEmote(emoteName) end
if FFlagCoreScriptLoadEmotesFromWeb and not emoteInHumanoidDescription(humanoidDescription, emoteName) then
playEmoteFunction = function() return humanoid:PlayEmoteById(emoteName, emoteAssetId) end
end
local success, didPlay = pcall(playEmoteFunction)
if success and didPlay then
EmotesAnalytics:onEmotePlayed(slotNumber, emoteAssetId)
else
handlePlayFailure(store, Constants.LocalizationKeys.ErrorMessages.TemporarilyUnavailable)
return
end
else
handlePlayFailure(store, Constants.LocalizationKeys.ErrorMessages.NotSupported)
return
end
end
end
return PlayEmote
@@ -0,0 +1,150 @@
local Players = game:GetService("Players")
local LocalPlayer = Players.LocalPlayer
if not LocalPlayer then
Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
LocalPlayer = Players.LocalPlayer
end
local CanPlayEmotes = {
Value = false,
ChangedEvent = Instance.new("BindableEvent")
}
local function getAnimateScript(character)
if not character then
return nil
end
local animate = character:FindFirstChild("Animate")
if animate and animate:IsA("LocalScript") then
return animate
end
return nil
end
local function getPlayEmoteBindable(character)
local animate = getAnimateScript(character)
if not animate then
return nil
end
local playEmoteBindable = animate:FindFirstChild("PlayEmote")
if playEmoteBindable and playEmoteBindable:IsA("BindableFunction") then
return playEmoteBindable
end
return nil
end
local function getCanPlayEmotes(character)
local playEmoteBindable = getPlayEmoteBindable(character)
return playEmoteBindable ~= nil
end
local animateAddedListener, playEmoteAddedListener, playEmoteRemovedListener
local function disconnectListeners()
if animateAddedListener then
animateAddedListener:Disconnect()
animateAddedListener = nil
end
if playEmoteAddedListener then
playEmoteAddedListener:Disconnect()
playEmoteAddedListener = nil
end
if playEmoteRemovedListener then
playEmoteRemovedListener:Disconnect()
playEmoteRemovedListener = nil
end
end
local checkConnect
local function checkUpdate(character)
local newCanPlayEmotes = getCanPlayEmotes(character)
if CanPlayEmotes.Value ~= newCanPlayEmotes then
CanPlayEmotes.Value = newCanPlayEmotes
CanPlayEmotes.ChangedEvent:Fire(newCanPlayEmotes)
end
checkConnect(newCanPlayEmotes, character)
end
local function connectAnimateAddedListener(character)
local animate = getAnimateScript(character)
if animate then
checkUpdate(character)
return
end
animateAddedListener = character.ChildAdded:Connect(function(child)
if child:IsA("LocalScript") and child.Name == "Animate" then
checkUpdate(character)
end
end)
end
local function connectPlayEmoteAddedListener(character, animateScript)
local playEmoteBindable = getPlayEmoteBindable(character)
if playEmoteBindable then
checkUpdate(character)
return
end
playEmoteAddedListener = animateScript.ChildAdded:Connect(function(child)
if child:IsA("BindableFunction") and child.Name == "PlayEmote" then
checkUpdate(character)
end
end)
end
local function connectPlayEmoteRemovedListener(character)
local playEmoteBindable = getPlayEmoteBindable(character)
if not playEmoteBindable then
checkUpdate(character)
return
end
playEmoteRemovedListener = playEmoteBindable.AncestryChanged:Connect(function(_, parent)
if not parent or not playEmoteBindable:IsDescendantOf(character) then
checkUpdate(character)
end
end)
end
checkConnect = function(canPlay, character)
disconnectListeners()
if not character then
return
end
if canPlay then
connectPlayEmoteRemovedListener(character)
else
local animateScript = getAnimateScript(character)
if animateScript then
connectPlayEmoteAddedListener(character, animateScript)
else
connectAnimateAddedListener(character)
end
end
end
local function onCharacterChanged(character)
disconnectListeners()
checkUpdate(character)
end
LocalPlayer:GetPropertyChangedSignal("Character"):Connect(function()
onCharacterChanged(LocalPlayer.Character)
end)
onCharacterChanged(LocalPlayer.Character)
return CanPlayEmotes
@@ -0,0 +1,33 @@
local Utility = script.Parent
local EmotesMenu = Utility.Parent
local Constants = require(EmotesMenu.Constants)
local function GetSegmentIndexFromPosition(position)
local angle = math.deg(math.atan2(position.X, position.Y))
if angle < 0 then
angle = angle + 360
end
local segmentAngle = 360 / Constants.EmotesPerPage
angle = angle + math.ceil(segmentAngle / 2)
angle = math.fmod(angle, 360)
local segmentIndex = math.ceil(angle / segmentAngle)
local offsetAngle = Constants.SegmentsStartRotation + 90
local segmentOffset = offsetAngle / segmentAngle
segmentIndex = segmentIndex + segmentOffset
segmentIndex = math.ceil(segmentIndex)
if segmentIndex > Constants.EmotesPerPage then
segmentIndex = segmentIndex - Constants.EmotesPerPage
elseif segmentIndex < 1 then
segmentIndex = segmentIndex + Constants.EmotesPerPage
end
return segmentIndex
end
return GetSegmentIndexFromPosition