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",
"LocalUnused": "fatal",
"FunctionUnused": "fatal",
"ImportUnused": "fatal",
"LocalShadow": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("CANCEL_LEAVING_GAME", function()
return {}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("CLOSE_NATIVE_PROMPT", function()
return {}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("CLOSE_REPORT_DIALOG", function()
return {}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("CLOSE_REPORT_SENT_DIALOG", function()
return {}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("OPEN_NATIVE_CLOSE_PROMPT", function()
return {}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("OPEN_REPORT_DIALOG", function(userId, userName)
return {
userId = userId,
userName = userName,
}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("OPEN_REPORT_SENT_DIALOG", function()
return {}
end)
@@ -0,0 +1,9 @@
local Modules = game:GetService("CorePackages").AppTempCommon
local Action = require(Modules.Common.Action)
return Action("RECEIVED_USER_INVITE_STATUS", function(userId, inviteStatus)
return {
userId = tostring(userId),
inviteStatus = inviteStatus,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_CONTROL_LAYOUT", function(controlLayout)
return {
controlLayout = controlLayout
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_CURRENT_PAGE", function(pageKey)
return {
newPage = pageKey,
}
end)
@@ -0,0 +1,11 @@
--Remove with isNewGamepadMenuEnabled
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_GAME_DESCRIPTIONS", function(newDescriptions)
return {
newDescriptions = newDescriptions,
}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_FRIEND_STATUS", function(userId, friendStatus)
return {
userId = userId,
friendStatus = friendStatus,
}
end)
@@ -0,0 +1,11 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_GAME_NAME_AND_DESCRIPTIONS", function(gameNameLocaleMap, gameDescriptionsLocaleMap, sourceLocale)
return {
gameNames = gameNameLocaleMap,
gameDescriptions = gameDescriptionsLocaleMap,
sourceLocale = sourceLocale,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_INPUT_TYPE", function(inputType)
return {
inputType = inputType,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_INSPECT_MENU_ENABLED", function(enabled)
return {
enabled = enabled,
}
end)
@@ -0,0 +1,11 @@
--Remove with isNewGamepadMenuEnabled
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_LANGUAGE_CODE_MAP", function(languageCodeMap)
return {
languageCodeMap = languageCodeMap,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_LOCALE_ID", function(localeId)
return {
newLocaleId = localeId,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_MENU_ICON_TOOLTIP_OPEN", function(status)
return {
menuIconTooltipOpen = status,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_MENU_OPEN", function(isMenuOpen)
return {
isMenuOpen = isMenuOpen,
}
end)
@@ -0,0 +1,11 @@
--Remove with isNewGamepadMenuEnabled
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_GAME_NAMES", function(newNames)
return {
newNames = newNames,
}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_PLACE_INFO", function(placeName, placeDescription)
return {
placeName = placeName,
placeDescription = placeDescription,
}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_RESPAWN_BEHAVIOR", function(respawnEnabled, respawnCallback)
return {
respawnEnabled = respawnEnabled,
customCallback = respawnCallback,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_RESPAWNING", function(isRespawning)
return {
respawning = isRespawning,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_SCREEN_SIZE", function(screenSize)
return {
newScreenSize = screenSize,
}
end)
@@ -0,0 +1,11 @@
--Remove with isNewGamepadMenuEnabled
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_SOURCE_LANGUAGE_CODE", function(languageCode)
return {
languageCode = languageCode,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("SET_VIDEO_RECORDING", function (recording)
return {
recording = recording,
}
end)
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action("START_LEAVING_GAME", function()
return {}
end)
@@ -0,0 +1,90 @@
--[[
The topbar button that opens the in-game menu.
]]
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local InGameMenuPolicy = require(RobloxGui.Modules.InGameMenu.InGameMenuPolicy)
local ViewportOverlay = require(script.Parent.ViewportOverlay)
local SideNavigation = require(script.Parent.SideNavigation)
local LeaveGameDialog = require(script.Parent.LeaveGameDialog)
local PageContainer = require(script.Parent.PageContainer)
local RespawnDialog = require(script.Parent.RespawnDialog)
local ReportDialog = require(script.Parent.ReportPage.ReportDialog)
local ReportSentDialog = require(script.Parent.ReportPage.ReportSentDialog)
local ControlLayoutSetter = require(script.Parent.ControlsPage.ControlLayoutSetter)
local Connection = require(script.Parent.Connection)
local EducationalPopup = require(script.Parent.EducationalPopup)
local MenuIconTooltip = require(script.Parent.MenuIconTooltip)
local FullscreenTitleBar = require(script.Parent.FullscreenTitleBar)
local GetFFlagEducationalPopupOnNativeClose = require(RobloxGui.Modules.Flags.GetFFlagEducationalPopupOnNativeClose)
local FFlagLuaMenuPerfImprovements = require(script.Parent.Parent.Flags.FFlagLuaMenuPerfImprovements)
local GetFFlagInGameMenuIconTooltip = require(RobloxGui.Modules.Flags.GetFFlagInGameMenuIconTooltip)
local GetFFlagUseRoactPolicyProvider = require(RobloxGui.Modules.Flags.GetFFlagUseRoactPolicyProvider)
local Constants = require(script.Parent.Parent.Resources.Constants)
local function App(props)
local fullscreenTitleBar = nil
if props.enableFullscreenTitleBar then
fullscreenTitleBar = Roact.createElement(FullscreenTitleBar, {
displayOrder = Constants.DisplayOrder.FullscreenTitleBar,
})
end
if FFlagLuaMenuPerfImprovements then
return Roact.createFragment({
Content = props.visible and Roact.createFragment({
Overlay = Roact.createElement(ViewportOverlay),
SideNavigation = Roact.createElement(SideNavigation),
LeaveGameDialog = Roact.createElement(LeaveGameDialog),
PageContainer = Roact.createElement(PageContainer),
RespawnDialog = Roact.createElement(RespawnDialog),
ReportDialog = Roact.createElement(ReportDialog),
ReportSentDialog = Roact.createElement(ReportSentDialog),
ControlLayoutSetter = Roact.createElement(ControlLayoutSetter),
}) or nil,
Connection = Roact.createElement(Connection),
EducationalPopup = GetFFlagEducationalPopupOnNativeClose() and Roact.createElement(EducationalPopup) or nil,
MenuIconTooltip = GetFFlagInGameMenuIconTooltip() and Roact.createElement(MenuIconTooltip) or nil,
FullscreenTitleBar = fullscreenTitleBar,
})
else
return Roact.createFragment({
Overlay = Roact.createElement(ViewportOverlay),
SideNavigation = Roact.createElement(SideNavigation),
LeaveGameDialog = Roact.createElement(LeaveGameDialog),
PageContainer = Roact.createElement(PageContainer),
RespawnDialog = Roact.createElement(RespawnDialog),
ReportDialog = Roact.createElement(ReportDialog),
ReportSentDialog = Roact.createElement(ReportSentDialog),
ControlLayoutSetter = Roact.createElement(ControlLayoutSetter),
Connection = Roact.createElement(Connection),
EducationalPopup = GetFFlagEducationalPopupOnNativeClose() and Roact.createElement(EducationalPopup) or nil,
MenuIconTooltip = GetFFlagInGameMenuIconTooltip() and Roact.createElement(MenuIconTooltip) or nil,
FullscreenTitleBar = fullscreenTitleBar,
})
end
end
if GetFFlagUseRoactPolicyProvider() then
App = InGameMenuPolicy.connect(function(appPolicy, props)
return {
enableFullscreenTitleBar = appPolicy.enableFullscreenTitleBar(),
}
end)(App)
end
local function mapStateToProps(state, props)
return { visible = state.isMenuOpen }
end
return RoactRodux.connect(mapStateToProps, nil)(App)
@@ -0,0 +1,48 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local InGameMenuPolicy = require(InGameMenu.InGameMenuPolicy)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local App = require(script.Parent.App)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
InGameMenuPolicy = Roact.createElement(InGameMenuPolicy.Provider, {
policy = { InGameMenuPolicy.Mapper },
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
App = Roact.createElement(App),
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,47 @@
--[[
Displays an image from the asset dictionary, slicing it if the asset
requires that.
]]
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Cryo = InGameMenuDependencies.Cryo
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local ImageSetButton = UIBlox.Core.ImageSet.Button
local Assets = require(InGameMenu.Resources.Assets)
local function makeAssetImageComponent(component)
return function(props)
local imageKey = props.imageKey
local imageData = Assets.Images[imageKey]
local imageProps
if typeof(imageData) == "string" then
imageProps = {
Image = imageData,
}
else
imageProps = imageData
end
local mergedProps = Cryo.Dictionary.join(props, {
imageKey = Cryo.None,
}, imageProps, {
BackgroundTransparency = 1,
})
return Roact.createElement(component, mergedProps)
end
end
return {
Label = makeAssetImageComponent(ImageSetLabel),
Button = makeAssetImageComponent(ImageSetButton),
}
@@ -0,0 +1,70 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local BarOnTopScrollingFrame = Roact.PureComponent:extend("BarOnTopScrollingFrame")
BarOnTopScrollingFrame.validateProps = t.strictInterface({
Size = t.UDim2,
Position = t.optional(t.UDim2),
CanvasSize = t.UDim2,
scrollBarOffset = t.integer,
ScrollingEnabled = t.optional(t.boolean),
[Roact.Children] = t.table,
})
BarOnTopScrollingFrame.defaultProps = {
ScrollingEnabled = true,
}
function BarOnTopScrollingFrame:init()
self.state = {
canvasPosition = Vector2.new(0, 0),
}
end
function BarOnTopScrollingFrame:render()
return Roact.createElement("Frame", {
Size = self.props.Size,
Position = self.props.Position,
BackgroundTransparency = 1,
ClipsDescendants = true,
}, {
DisplayScrollingFrame = Roact.createElement("ScrollingFrame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, -self.props.scrollBarOffset, 1, 0),
CanvasSize = self.props.CanvasSize - UDim2.new(0, self.props.scrollBarOffset, 0, 0),
ScrollBarThickness = 8,
BorderSizePixel = 0,
ZIndex = 2,
ScrollingEnabled = self.props.ScrollingEnabled,
[Roact.Change.CanvasPosition] = function(rbx)
self:setState({
CanvasPosition = rbx.CanvasPosition
})
end,
}),
ContentsScrollingFrame = Roact.createElement("ScrollingFrame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, -self.props.scrollBarOffset, 1, 0),
CanvasSize = self.props.CanvasSize - UDim2.new(0, self.props.scrollBarOffset, 0, 0),
CanvasPosition = self.state.CanvasPosition,
ScrollBarThickness = 0,
BorderSizePixel = 0,
ClipsDescendants = false,
ScrollingEnabled = false,
}, {
Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, self.props.scrollBarOffset, 1, 0),
}, self.props[Roact.Children]),
}),
})
end
return BarOnTopScrollingFrame
@@ -0,0 +1,37 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local RunService = game:GetService("RunService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local BlurredModalPortal = Roact.PureComponent:extend("BlurredModalPortal")
BlurredModalPortal.validateProps = t.strictInterface({
Enabled = t.boolean,
[Roact.Children] = t.optional(t.table)
})
function BlurredModalPortal:render()
return Roact.createElement(Roact.Portal, {
target = CoreGui
}, {
InGameMenuModalBlur = Roact.createElement("ScreenGui", {
OnTopOfCoreBlur = true,
Enabled = self.props.Enabled,
IgnoreGuiInset = true,
}, self.props[Roact.Children])
})
end
function BlurredModalPortal:didUpdate(prevProps)
local wasEnabled = prevProps.Enabled
local enabled = self.props.Enabled
if wasEnabled ~= enabled then
RunService:SetRobloxGuiFocused(enabled)
end
end
return BlurredModalPortal
@@ -0,0 +1,227 @@
local CoreGui = game:GetService("CoreGui")
local RunService = game:GetService("RunService")
local TextService = game:GetService("TextService")
local CorePackages = game:GetService("CorePackages")
local ContextActionService = game:GetService("ContextActionService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent
local Assets = require(InGameMenu.Resources.Assets)
local ThemedTextLabel = require(script.Parent.ThemedTextLabel)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local MODAL_CONFIRM_ACTION = "InGameMenuModalConfirm"
local MODAL_CANCEL_ACTION = "InGameMenuModalCancel"
local ConfirmationDialog = Roact.PureComponent:extend("ConfirmationDialog")
ConfirmationDialog.validateProps = t.strictInterface({
bodyText = t.string,
cancelText = t.string,
confirmText = t.string,
titleText = t.string,
bindReturnToConfirm = t.boolean,
onCancel = t.callback,
onConfirm = t.callback,
blurBackground = t.boolean,
visible = t.boolean,
})
ConfirmationDialog.defaultProps = {
bodyText = "This is some body text. It will wrap when needed.",
cancelText = "Cancel",
confirmText = "Confirm",
titleText = "Dialog Title",
blurBackground = false,
}
function ConfirmationDialog:render()
local props = self.props
return withStyle(function(style)
local bodyFont = style.Font.Body.Font
local bodyFontSize = style.Font.Body.RelativeSize * style.Font.BaseSize
local bodyText = props.bodyText
local textHeight = TextService:GetTextSize(
bodyText,
bodyFontSize,
bodyFont,
-- 335 (width) - 20px padding on left and right
Vector2.new(335 - 20 - 20, math.huge)
).Y
-- 20px padding top, 20px padding bottom
-- Minimum height of two lines of text.
-- TODO: Line height!
local bodyTextContainerHeight = 20 + math.max(textHeight, bodyFontSize * 2) + 20
return Roact.createElement(Roact.Portal, {
target = CoreGui,
}, {
InGameMenuConfirmationDialog = Roact.createElement("ScreenGui", {
DisplayOrder = 8,
IgnoreGuiInset = true,
OnTopOfCoreBlur = props.blurBackground,
Enabled = props.visible,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
}, {
Overlay = Roact.createElement("TextButton", {
AutoButtonColor = false,
BackgroundColor3 = style.Theme.Overlay.Color,
BackgroundTransparency = style.Theme.Overlay.Transparency,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0),
Text = "",
}),
DialogMainFrame = Roact.createElement(ImageSetLabel, {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = Assets.Images.RoundedRect.Image,
ImageColor3 = style.Theme.BackgroundUIDefault.Color,
ImageTransparency = style.Theme.BackgroundUIDefault.Transparency,
Position = UDim2.new(0.5, 0, 0.5, 0),
ScaleType = Assets.Images.RoundedRect.ScaleType,
-- button container (36px), bottom padding (20px)
-- height: title bar (48px), divider (1px), body text height,
Size = UDim2.new(0, 335, 0, 48 + 1 + bodyTextContainerHeight + 36 + 20),
SliceCenter = Assets.Images.RoundedRect.SliceCenter,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingBottom = UDim.new(0, 20),
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Title = Roact.createElement(ThemedTextLabel, {
fontKey = "Header1",
themeKey = "TextEmphasis",
LayoutOrder = 1,
Size = UDim2.new(1, 0, 0, 48),
Text = props.titleText,
}),
Divider = Roact.createElement("Frame", {
BackgroundColor3 = style.Theme.Divider.Color,
BackgroundTransparency = style.Theme.Divider.Transparency,
BorderSizePixel = 0,
LayoutOrder = 3,
Size = UDim2.new(0.8, 0, 0, 1),
}),
BodyTextContainer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 4,
Size = UDim2.new(1, 0, 0, bodyTextContainerHeight),
}, {
BodyText = Roact.createElement(ThemedTextLabel, {
Size = UDim2.new(1, 0, 1, 0),
Text = props.bodyText,
TextWrapped = true,
})
}),
ButtonContainer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 5,
Size = UDim2.new(1, 0, 0, 36),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
Padding = UDim.new(0, 10),
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
CancelButton = Roact.createElement(UIBlox.App.Button.SecondaryButton, {
layoutOrder = 1,
size = UDim2.new(0.5, -5, 1, 0),
text = props.cancelText,
onActivated = props.onCancel,
}),
ConfirmButton = Roact.createElement(UIBlox.App.Button.PrimarySystemButton, {
layoutOrder = 2,
size = UDim2.new(0.5, -5, 1, 0),
text = props.confirmText,
onActivated = props.onConfirm,
}),
}),
}),
}),
})
end)
end
function ConfirmationDialog:updateBlur()
local shouldBlur = self.props.blurBackground and self.props.visible
RunService:SetRobloxGuiFocused(shouldBlur)
end
-- TODO: consider if we want to have a button selection model like the old menu rather than enter always confirming
function ConfirmationDialog:bindActions()
local function confirmFunc(actionName, inputState, input)
if inputState == Enum.UserInputState.Begin then
self.props.onConfirm()
end
end
if self.props.bindReturnToConfirm then
ContextActionService:BindCoreAction(
MODAL_CONFIRM_ACTION, confirmFunc, false, Enum.KeyCode.Return)
end
local function cancelFunc(actionName, inputState, input)
if inputState == Enum.UserInputState.Begin then
self.props.onCancel()
end
end
ContextActionService:BindCoreAction(
MODAL_CANCEL_ACTION, cancelFunc, false, Enum.KeyCode.Escape)
end
function ConfirmationDialog:unbindActions()
ContextActionService:UnbindCoreAction(MODAL_CONFIRM_ACTION)
ContextActionService:UnbindCoreAction(MODAL_CANCEL_ACTION)
end
function ConfirmationDialog:didMount()
self:updateBlur()
if self.props.visible then
self:bindActions()
end
end
function ConfirmationDialog:didUpdate()
self:updateBlur()
if self.props.visible then
self:bindActions()
else
self:unbindActions()
end
end
function ConfirmationDialog:willUnmount()
if self.props.blurBackground then
RunService:SetRobloxGuiFocused(false)
end
self:unbindActions()
end
return ConfirmationDialog
@@ -0,0 +1,59 @@
return function()
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local ConfirmationDialog = require(script.Parent.ConfirmationDialog)
local dummyDialogProps = {
bodyText = "Hello world!",
cancelText = "Cancel",
confirmText = "Confirm",
titleText = "Title",
bindReturnToConfirm = false,
onCancel = function()
print("cancel")
end,
onConfirm = function()
print("confirm")
end,
blurBackground = false,
visible = true,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
ConfirmationDialog = Roact.createElement(ConfirmationDialog, dummyDialogProps),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should be portaled into CoreGui", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
ConfirmationDialog = Roact.createElement(ConfirmationDialog, dummyDialogProps),
})
local instance = Roact.mount(element)
expect(CoreGui:FindFirstChild("InGameMenuConfirmationDialog")).to.never.equal(nil)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,63 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local InGameMenu = script.Parent.Parent.Parent
local SetFriendStatus = require(InGameMenu.Actions.SetFriendStatus)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local FriendStatusConnector = Roact.PureComponent:extend("FriendStatusConnector")
function FriendStatusConnector:init()
self.state = {
localPlayer = Players.LocalPlayer,
}
end
function FriendStatusConnector:render()
local connections = {
PlayerAdded = Roact.createElement(ExternalEventConnection, {
event = Players.PlayerAdded,
callback = function(player)
if player == Players.LocalPlayer then
self:setState({
localPlayer = player,
})
else
self.props.setFriendStatus(player.UserId, Enum.FriendStatus.Unknown)
end
end,
}),
PlayerRemoving = Roact.createElement(ExternalEventConnection, {
event = Players.PlayerRemoving,
callback = function(player)
self.props.setFriendStatus(player.UserId, nil)
end,
}),
}
if self.state.localPlayer then
connections.FriendStatusChanged = Roact.createElement(ExternalEventConnection, {
event = Players.LocalPlayer.FriendStatusChanged,
callback = function(player, friendStatus)
self.props.setFriendStatus(player.UserId, friendStatus)
end,
})
end
return Roact.createFragment(connections)
end
return RoactRodux.UNSTABLE_connect2(nil, function(dispatch)
return {
setFriendStatus = function(player, friendStatus)
return dispatch(SetFriendStatus(player, friendStatus))
end,
}
end)(FriendStatusConnector)
@@ -0,0 +1,63 @@
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local InGameMenu = script.Parent.Parent.Parent
local SetInputType = require(InGameMenu.Actions.SetInputType)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local Constants = require(InGameMenu.Resources.Constants)
local InputType = Constants.InputType
local LastInputTypeConnector = Roact.PureComponent:extend("LastInputTypeConnector")
local inputTypeMap = {
[Enum.UserInputType.MouseButton2] = InputType.MouseAndKeyBoard,
[Enum.UserInputType.MouseButton3] = InputType.MouseAndKeyBoard,
[Enum.UserInputType.MouseWheel] = InputType.MouseAndKeyBoard,
[Enum.UserInputType.MouseMovement] = InputType.MouseAndKeyBoard,
[Enum.UserInputType.Keyboard] = InputType.MouseAndKeyBoard,
[Enum.UserInputType.Gamepad1] = InputType.Gamepad,
[Enum.UserInputType.Gamepad2] = InputType.Gamepad,
[Enum.UserInputType.Gamepad3] = InputType.Gamepad,
[Enum.UserInputType.Gamepad4] = InputType.Gamepad,
[Enum.UserInputType.Gamepad5] = InputType.Gamepad,
[Enum.UserInputType.Gamepad6] = InputType.Gamepad,
[Enum.UserInputType.Gamepad7] = InputType.Gamepad,
[Enum.UserInputType.Gamepad8] = InputType.Gamepad,
[Enum.UserInputType.Touch] = InputType.Touch,
}
function LastInputTypeConnector:init()
local initalInputType = inputTypeMap[UserInputService:GetLastInputType()]
if initalInputType then
self.props.setInputType(initalInputType)
end
end
function LastInputTypeConnector:render()
return Roact.createElement(ExternalEventConnection, {
event = UserInputService.LastInputTypeChanged,
callback = function(lastInputType)
local inputType = inputTypeMap[lastInputType]
if inputType then
self.props.setInputType(inputType)
end
end,
})
end
return RoactRodux.UNSTABLE_connect2(nil, function(dispatch)
return {
setInputType = function(inputType)
return dispatch(SetInputType(inputType))
end,
}
end)(LastInputTypeConnector)
@@ -0,0 +1,89 @@
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local VRService = game:GetService("VRService")
local Symbol = require(CorePackages.Symbol)
local MouseIconOverrideService = require(CorePackages.InGameServices.MouseIconOverrideService)
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local Constants = require(InGameMenu.Resources.Constants)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local INGAME_MENU_CUSOR_OVERRIDE_KEY = Symbol.named("InGameMenuCursorOverride")
local FFlagNewInGameMenuUseMouseOverrideService2 = game:DefineFastFlag("NewInGameMenuUseMouseOverrideService2", false)
local InputType = Constants.InputType
local MouseOverrideManager = Roact.PureComponent:extend("MouseOverrideManager")
MouseOverrideManager.validateProps = t.strictInterface({
menuOpen = t.boolean,
inputType = t.string,
})
function MouseOverrideManager:render()
return Roact.createElement(ExternalEventConnection, {
event = UserInputService:GetPropertyChangedSignal("OverrideMouseIconBehavior"),
callback = function()
-- Another menu that overrides the mouse icon behaviour has closed.
-- But we are still open so we need to keep re-override the icon behaviour.
if self.props.menuOpen and UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.None then
self:updateMouseIconOverride()
end
end,
})
end
function MouseOverrideManager:init()
self.didOverrideMouse = false
end
if FFlagNewInGameMenuUseMouseOverrideService2 then
function MouseOverrideManager:updateMouseIconOverride()
if self.props.menuOpen then
self.didOverrideMouse = true
if self.props.inputType == InputType.Gamepad or VRService.VREnabled then
MouseIconOverrideService.push(INGAME_MENU_CUSOR_OVERRIDE_KEY, Enum.OverrideMouseIconBehavior.ForceHide)
else
MouseIconOverrideService.push(INGAME_MENU_CUSOR_OVERRIDE_KEY, Enum.OverrideMouseIconBehavior.ForceShow)
end
elseif self.didOverrideMouse then
self.didOverrideMouse = false
MouseIconOverrideService.pop(INGAME_MENU_CUSOR_OVERRIDE_KEY)
end
end
else
function MouseOverrideManager:updateMouseIconOverride()
if self.props.menuOpen then
if self.props.inputType == InputType.Gamepad or VRService.VREnabled then
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
else
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceShow
end
else
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None
end
end
end
function MouseOverrideManager:didMount()
self:updateMouseIconOverride()
end
function MouseOverrideManager:didUpdate()
self:updateMouseIconOverride()
end
return RoactRodux.UNSTABLE_connect2(function(state)
return {
menuOpen = state.isMenuOpen,
inputType = state.displayOptions.inputType,
}
end, nil)(MouseOverrideManager)
@@ -0,0 +1,94 @@
local GuiService = game:GetService("GuiService")
local AppStorageService = game:GetService("AppStorageService")
local RbxAnalyticsService = game:GetService("RbxAnalyticsService")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local OpenNativeClosePrompt = require(InGameMenu.Actions.OpenNativeClosePrompt)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local InGameMenuPolicy = require(InGameMenu.InGameMenuPolicy)
local UserLocalStore = require(InGameMenu.Utility.UserLocalStore)
local GetFFlagPlayerSpecificPopupCounter = require(InGameMenu.Flags.GetFFlagPlayerSpecificPopupCounter)
local LOCAL_STORAGE_KEY_NATIVE_CLOSE = "NativeCloseLuaPromptDisplayCount"
local notificationTypes = GuiService:GetNotificationTypeList()
local NativeCloseEventConnector = Roact.PureComponent:extend("NativeCloseEventConnector")
NativeCloseEventConnector.validateProps = t.strictInterface({
openNativeClosePrompt = t.callback,
isEducationalPopupEnabled = t.boolean,
maxDisplayCount = t.integer,
})
function NativeCloseEventConnector:init()
local function getDisplayCount()
local localStore = AppStorageService
if GetFFlagPlayerSpecificPopupCounter() then
localStore = UserLocalStore.new()
end
return tonumber(localStore:GetItem(LOCAL_STORAGE_KEY_NATIVE_CLOSE)) or 0
end
local function setDisplayCount(number)
local localStore = AppStorageService
if GetFFlagPlayerSpecificPopupCounter() then
localStore = UserLocalStore.new()
end
localStore:SetItem(LOCAL_STORAGE_KEY_NATIVE_CLOSE, tostring(number))
localStore:flush()
end
self.onNativeCloseConnect = function()
if self.props.isEducationalPopupEnabled then
RbxAnalyticsService:ReportCounter("LeaveGame_NativeClose", 1)
if self.props.isClosingApp then
GuiService:BroadcastNotification("", notificationTypes.NATIVE_EXIT)
return
end
local displayCount = getDisplayCount()
local maxDisplayCount = self.props.maxDisplayCount
if displayCount >= maxDisplayCount then
GuiService:BroadcastNotification("", notificationTypes.NATIVE_EXIT)
else
self.props.openNativeClosePrompt()
setDisplayCount(displayCount + 1)
end
end
end
end
function NativeCloseEventConnector:render()
return Roact.createElement(ExternalEventConnection, {
event = GuiService.NativeClose,
callback = self.onNativeCloseConnect,
})
end
NativeCloseEventConnector = InGameMenuPolicy.connect(function(appPolicy, props)
return {
isEducationalPopupEnabled = appPolicy.enableEducationalPopup(),
maxDisplayCount = appPolicy.educationalPopupMaxDisplayCount(),
}
end)(NativeCloseEventConnector)
return RoactRodux.UNSTABLE_connect2(function(state, props)
return {
isClosingApp = state.nativeClosePrompt.closingApp,
}
end, function(dispatch)
return {
openNativeClosePrompt = function()
return dispatch(OpenNativeClosePrompt())
end,
}
end)(NativeCloseEventConnector)
@@ -0,0 +1,52 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local InGameMenu = script.Parent.Parent.Parent
local reducer = require(InGameMenu.reducer)
local InGameMenuPolicy = require(InGameMenu.InGameMenuPolicy)
local mockEducationalPolicyMapper = function(policy)
return {
enableEducationalPopup = function()
return false
end,
educationalPopupMaxDisplayCount = function()
return 0
end,
}
end
local NativeCloseEventConnector = require(script.Parent.NativeCloseEventConnector)
it("should create and destroy without errors on engine feature enabled", function()
local isNativeCloseIntercept = game:GetEngineFeature("NativeCloseIntercept")
--[[
NativeCloseEventConnector relies on GuiService.NativeClose which guarded
by an engine feature NativeCloseIntercept
Skip unit test for older engine version which NativeCloseIntercept is disabled
]]
if not isNativeCloseIntercept then
SKIP()
return
end
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
PolicyProvider = Roact.createElement(InGameMenuPolicy.Provider, {
policy = { mockEducationalPolicyMapper },
}, {
TestComponent = Roact.createElement(NativeCloseEventConnector, {
openNativeClosePrompt = function() end,
}),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,35 @@
--!nocheck
local CorePackages = game:GetService("CorePackages")
local GameSettings = settings():WaitForChild("Game Options") -- TODO: we should migrate the APIs from here to CoreGui
local InGameMenu = script.Parent.Parent.Parent
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local SetVideoRecording = require(InGameMenu.Actions.SetVideoRecording)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local VideoRecordConnector = Roact.PureComponent:extend("VideoRecordConnector")
function VideoRecordConnector:init()
self.props.setVideoRecording(GameSettings.VideoRecording)
end
function VideoRecordConnector:render()
return Roact.createElement(ExternalEventConnection, {
event = GameSettings:GetPropertyChangedSignal("VideoRecording"),
callback = function ()
self.props.setVideoRecording(GameSettings.VideoRecording)
end,
})
end
return RoactRodux.UNSTABLE_connect2(nil, function (dispatch)
return {
setVideoRecording = function (state)
return dispatch(SetVideoRecording(state))
end,
}
end)(VideoRecordConnector)
@@ -0,0 +1,41 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local FriendStatusConnector = require(script.FriendStatusConnector)
local LastInputTypeConnector = require(script.LastInputTypeConnector)
local MouseOverrideManager = require(script.MouseOverrideManager)
local VideoRecordConnector = require(script.VideoRecordConnector)
local NativeCloseEventConnector = require(script.NativeCloseEventConnector)
local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui")
local InGameMenuPolicy = require(RobloxGui.Modules.InGameMenu.InGameMenuPolicy)
local GetFFlagUseRoactPolicyProvider = require(RobloxGui.Modules.Flags.GetFFlagUseRoactPolicyProvider)
local validateProps = t.strictInterface({
isEducationalPopupEnabled = t.optional(t.boolean),
})
local function Connection(props)
assert(validateProps)
return Roact.createFragment({
FriendStatusConnector = Roact.createElement(FriendStatusConnector),
LastInputTypeConnector = Roact.createElement(LastInputTypeConnector),
MouseOverrideManager = Roact.createElement(MouseOverrideManager),
VideoRecordConnector = Roact.createElement(VideoRecordConnector),
NativeCloseEventConnector = props.isEducationalPopupEnabled and Roact.createElement(NativeCloseEventConnector)
or nil,
})
end
if GetFFlagUseRoactPolicyProvider() then
Connection = InGameMenuPolicy.connect(function(appPolicy, props)
return {
isEducationalPopupEnabled = appPolicy.enableEducationalPopup(),
}
end)(Connection)
end
return Connection
@@ -0,0 +1,139 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local KeyLabel = require(InGameMenu.Components.KeyLabel)
local PointerLabel = require(InGameMenu.Components.PointerLabel)
local Divider = require(InGameMenu.Components.Divider)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local CONTROL_ENTRY_HEIGHT = 50
local validateProps = t.strictInterface({
actionDescription = t.string,
inputs = t.table,
isList = t.optional(t.boolean),
hasDivider = t.boolean,
LayoutOrder = t.integer,
})
local function renderHorizontalSpacer(symbol, layoutOrder, isElipses)
local size = isElipses and UDim2.new(0, 19, 0, 24) or UDim2.new(0, 19, 0, 16)
local yAlignment = isElipses and Enum.TextYAlignment.Bottom
return Roact.createElement(ThemedTextLabel, {
themeKey = "TextEmphasis",
fontKey = "Header1",
Text = symbol,
Size = size,
TextYAlignment = yAlignment,
LayoutOrder = layoutOrder,
})
end
local function renderKey(input, layoutOrder)
local inputComponent = input.EnumType == Enum.KeyCode and KeyLabel or PointerLabel
return Roact.createElement(inputComponent, {
input = input,
LayoutOrder = layoutOrder,
})
end
local function renderInputIconContainer(inputs, isList)
local children = {}
-- If we're rendering inputs as a "list", we render the keys next to
-- eachother, with a "..." at the end, so we want 0 padding.
local paddingWidth = not isList and 7 or 0
children.ListLayout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, paddingWidth),
})
local layoutOrder = 1
for i, input in ipairs(inputs) do
-- If input is a group of inputs, we iterate over that table and render
-- with "+"" as spacers. Otherwise, we render the single input with "/"
-- as the spacer.
if typeof(input) ~= "table" then
children["input"..i] = renderKey(input, layoutOrder)
layoutOrder = layoutOrder + 1
-- if "list", append elipses to the end of inputs
if isList and i == #inputs then
children.Elipses = renderHorizontalSpacer("...", layoutOrder, true)
end
else
for i2, groupedInput in ipairs(input) do
local order = string.format("%d_%d", i, i2)
children["groupedInput"..order] = renderKey(groupedInput, layoutOrder)
layoutOrder = layoutOrder + 1
if i2 < #input then
children["groupedDivider"..order] = renderHorizontalSpacer("+", layoutOrder)
layoutOrder = layoutOrder + 1
end
end
end
-- If "list", don't render dividers
if not isList and i < #inputs then
children["divider"..i] = renderHorizontalSpacer("/", layoutOrder)
layoutOrder = layoutOrder + 1
end
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 1, 0),
Position = UDim2.new(1, 0, 0, 0),
}, children)
end
local function ControlEntry(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withLocalization({
actionDescription = props.actionDescription,
})(function(localized)
return Roact.createElement("Frame", {
LayoutOrder = props.LayoutOrder,
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, CONTROL_ENTRY_HEIGHT)
}, {
ActionLabel = Roact.createElement(ThemedTextLabel, {
fontKey = "Header2",
themeKey = "TextEmphasis",
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(0, 200, 0, 22),
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
Text = localized.actionDescription,
}),
Divider = props.hasDivider and Roact.createElement(Divider, {
Size = UDim2.new(1, 0, 0, 1),
Position = UDim2.new(0, 0, 1, -1),
}),
Inputs = renderInputIconContainer(props.inputs, props.isList),
})
end)
end
return ControlEntry
@@ -0,0 +1,101 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local ControlEntry = require(script.Parent.ControlEntry)
local key = Enum.KeyCode
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
ControlEntry = Roact.createElement(ControlEntry, {
actionDescription = "CoreScripts.InGameMenu.Controls.MoveForward",
inputs = { key.A },
isList = false,
hasDivider = false,
LayoutOrder = 5,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors when displaying a list", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
ControlEntry = Roact.createElement(ControlEntry, {
actionDescription = "CoreScripts.InGameMenu.Controls.MoveForward",
inputs = { key.A, key.B, key.C, key.D },
isList = true,
hasDivider = true,
LayoutOrder = 1,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors when displaying multiple key inputs", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
ControlEntry = Roact.createElement(ControlEntry, {
actionDescription = "CoreScripts.InGameMenu.Controls.MoveForward",
inputs = { {key.LeftSuper, key.LeftShift, key.Three} },
isList = false,
hasDivider = false,
LayoutOrder = 1,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,52 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local HEADER_PADDING_TOP = 20
local HEADER_TEXT_HEIGHT = 17
local HEADER_PADDING_BOTTOM = 8
local HEADER_HEIGHT = HEADER_PADDING_TOP + HEADER_TEXT_HEIGHT + HEADER_PADDING_BOTTOM
local validateProps = t.strictInterface({
title = t.string,
LayoutOrder = t.integer,
})
local function ControlEntry(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withLocalization({
title = props.title,
})(function(localized)
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
LayoutOrder = props.LayoutOrder,
}, {
HeaderText = Roact.createElement(ThemedTextLabel, {
fontKey = "CaptionHeader",
themeKey = "TextMuted",
Text = localized.title,
TextXAlignment = Enum.TextXAlignment.Left,
Position = UDim2.new(0, 0, 0, HEADER_PADDING_TOP),
Size = UDim2.new(1, 0, 0, HEADER_TEXT_HEIGHT),
})
})
end)
end
return ControlEntry
@@ -0,0 +1,46 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local ControlEntryHeader = require(script.Parent.ControlEntryHeader)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
ControlEntryHeader = Roact.createElement(ControlEntryHeader, {
title = "CoreScripts.InGameMenu.Controls.CharacterMovementTitle",
LayoutOrder = 1,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,136 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(script.Parent.Parent.ThemedTextLabel)
local Divider = require(script.Parent.Parent.Divider)
-- local KeyLabel = require(script.Parent.KeyLabel)
local SetCurrentPage = require(InGameMenu.Actions.SetCurrentPage)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Assets = require(InGameMenu.Resources.Assets)
local Constants = require(InGameMenu.Resources.Constants)
local ImageSetButton = UIBlox.Core.ImageSet.Button
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local HEADER_HEIGHT = 132
local HEADER_SIDE_PADDING = 95
local HEADER_CONTENT_Y_CENTER = 100
local validateProps = t.strictInterface({
titleText = t.string,
[Roact.Children] = t.optional(t.table),
onClosed = t.callback,
})
local function ControlLayoutContainer(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withLocalization({
titleText = props.titleText,
})(function(localized)
return withStyle(function(style)
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = style.Theme.Overlay.Color,
BackgroundTransparency = style.Theme.Overlay.Transparency,
Active = true,
}, {
PageContent = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT),
Position = UDim2.new(0, 0, 0, HEADER_HEIGHT),
ClipsDescendants = true
}, props[Roact.Children]),
HeaderContainer = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, HEADER_SIDE_PADDING),
PaddingRight = UDim.new(0, HEADER_SIDE_PADDING),
}),
CloseButton = Roact.createElement(ImageSetButton, {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0, HEADER_CONTENT_Y_CENTER),
Size = UDim2.new(0, 36, 0, 36),
Image = Assets.Images.CloseModal,
BackgroundTransparency = 1,
[Roact.Event.Activated] = props.onClosed,
}),
HeaderTextLabel = Roact.createElement(ThemedTextLabel, {
fontKey = "Header1",
themeKey = "TextEmphasis",
Size = UDim2.new(0, 250, 0, 28),
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0, HEADER_CONTENT_Y_CENTER),
Text = localized.titleText,
}),
HeaderDivider = Roact.createElement(Divider, {
Size = UDim2.new(1, 0, 0, 2),
AnchorPoint = Vector2.new(0.5, 1),
Position = UDim2.new(0.5, 0, 1, 0),
}),
-- Yale wanted to keep this around, unsure if we're going to use it or not
-- CloseButtonLabel = Roact.createElement(KeyLabel, {
-- Position = UDim2.new(0, 36, 0, 0),
-- input = Enum.KeyCode.Escape,
-- }),
}),
RadialGlowBig = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.RadialGlow,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
-- Renders as a square based off of screen width
SizeConstraint = Enum.SizeConstraint.RelativeXX,
Size = UDim2.new(1, 0, 1, 0),
}),
RadialGlowSquashed = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.RadialGlow,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 1, 0),
-- Renders with height always at 60% of width
SizeConstraint = Enum.SizeConstraint.RelativeXX,
Size = UDim2.new(1, 0, 0.6, 0),
})
})
end)
end)
end
return RoactRodux.UNSTABLE_connect2(
nil,
function(dispatch)
return {
onClosed = function()
dispatch(SetCurrentPage(Constants.defaultPageKey))
end
}
end
)(ControlLayoutContainer)
@@ -0,0 +1,45 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local ControlLayoutContainer = require(script.Parent.ControlLayoutContainer)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
ControlEntryHeader = Roact.createElement(ControlLayoutContainer, {
titleText = "CoreScripts.InGameMenu.Controls.GamepadTitle",
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,48 @@
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local InGameMenu = script.Parent.Parent.Parent
local Controls = require(InGameMenu.Resources.Controls)
local SetControlLayout = require(InGameMenu.Actions.SetControlLayout)
local ControlLayoutSetter = Roact.PureComponent:extend("ControlLayoutSetter")
function ControlLayoutSetter:render()
return nil
end
function ControlLayoutSetter:didMount()
self.inputChangedConnection = UserInputService.LastInputTypeChanged:Connect(function(lastInputType)
local controlLayout = Controls.inputToControlTypeMap[lastInputType]
if controlLayout then
self.props.dispatchSetControlLayout(controlLayout)
end
end)
self.props.dispatchSetControlLayout(Controls.ControlLayouts.TOUCH)
end
function ControlLayoutSetter:willUnmount()
if self.inputChangedConnection then
self.inputChangedConnection:Disconnect()
end
end
return RoactRodux.UNSTABLE_connect2(
nil,
function(dispatch)
return {
dispatchSetControlLayout = function(controlLayout)
dispatch(SetControlLayout(controlLayout))
end
}
end
)(ControlLayoutSetter)
@@ -0,0 +1,102 @@
local UserInputService = game:GetService("UserInputService")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent.Parent
local ControlLayoutContainer = require(script.Parent.Parent.ControlLayoutContainer)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local Assets = require(InGameMenu.Resources.Assets)
local Controls = require(InGameMenu.Resources.Controls)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local GAMEPAD_IMAGE_HEIGHT = 465
local GAMEPAD_IMAGE_WIDTH = 473
local GAMEPAD_WIDTH = 440
local GAMEPAD_ICONS_WIDTH = GAMEPAD_IMAGE_WIDTH - GAMEPAD_WIDTH
-- Height and padding for gamepad button labels
local TEXT_HEIGHT = 22
local LIST_PADDING = 21
-- Offsets for labels
local TEXT_Y_OFFSET = 62
local TEXT_X_PADDING = 26
-- Text for camera controls needs be offset based on the other text on the right
-- side of the controller. We mutliply the number of control items by the total
-- size (height + padding) of each control label element.
local RIGHT_LABELS_HEIGHT = (#Controls.gamepadLabels.right * (TEXT_HEIGHT + LIST_PADDING))
local CONTROL_CAMERA_TEXT_Y_OFFSET = RIGHT_LABELS_HEIGHT + TEXT_Y_OFFSET
local function verticalControlsList(props)
return withLocalization(props.controls)(function(localized)
local listChildren = {}
listChildren.ListLayout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, LIST_PADDING),
})
for i, control in ipairs(localized) do
listChildren["controlLabel_"..i] = Roact.createElement(ThemedTextLabel, {
fontKey = "Header2",
themeKey = "TextEmphasis",
TextXAlignment = props.TextXAlignment,
Size = UDim2.new(0, 1, 0, TEXT_HEIGHT),
Text = control,
LayoutOrder = i,
})
end
return Roact.createElement("Frame", {
Position = props.Position,
Size = UDim2.new(0, 0, 1, 0),
BackgroundTransparency = 1,
}, listChildren)
end)
end
local function GamepadControls()
local platform = UserInputService:GetPlatform()
local isXbox = platform == Enum.Platform.XBox360 or platform == Enum.Platform.XBoxOne
return Roact.createElement(ControlLayoutContainer, {
titleText = "CoreScripts.InGameMenu.Controls.GamepadTitle"
}, {
GamepadImage = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = isXbox and Assets.Images.GamepadXbox or Assets.Images.Gamepad,
Size = UDim2.new(0, GAMEPAD_IMAGE_WIDTH, 0, GAMEPAD_IMAGE_HEIGHT),
Position = UDim2.new(0.5, GAMEPAD_ICONS_WIDTH/2, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
}, {
LeftControlsList = Roact.createElement(verticalControlsList, {
TextXAlignment = Enum.TextXAlignment.Right,
Position = UDim2.new(0, -TEXT_X_PADDING, 0, TEXT_Y_OFFSET),
controls = Controls.gamepadLabels.left
}),
RightControlsList = Roact.createElement(verticalControlsList, {
TextXAlignment = Enum.TextXAlignment.Left,
Position = UDim2.new(1, TEXT_X_PADDING-GAMEPAD_ICONS_WIDTH, 0, TEXT_Y_OFFSET),
controls = Controls.gamepadLabels.right
}),
RightIconsControlslIst = Roact.createElement(verticalControlsList, {
TextXAlignment = Enum.TextXAlignment.Left,
Position = UDim2.new(1, TEXT_X_PADDING, 0, CONTROL_CAMERA_TEXT_Y_OFFSET),
controls = Controls.gamepadLabels.camera
})
})
})
end
return GamepadControls
@@ -0,0 +1,43 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local GamepadControls = require(script.Parent.GamepadControls)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GamepadControls = Roact.createElement(GamepadControls),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,89 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local InGameMenu = script.Parent.Parent.Parent.Parent
local Controls = require(InGameMenu.Resources.Controls)
local ControlLayoutContainer = require(script.Parent.Parent.ControlLayoutContainer)
local PaddedScrollFrame = require(InGameMenu.Components.PaddedScrollFrame)
local ControlEntry = require(script.Parent.Parent.ControlEntry)
local ControlEntryHeader = require(script.Parent.Parent.ControlEntryHeader)
local CONTENT_MAX_WIDTH = 900
local CONTENT_MIN_WIDTH = 150
local MIN_SIDE_PADDING = 95
local HEADER_HEIGHT = 45
local ENTRY_HEIGHT = 50
local BOTTOM_PADDING = 130
local function KeyboardControls(props)
local controlsChildren = {}
controlsChildren.ListLayout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
})
controlsChildren.Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, MIN_SIDE_PADDING),
PaddingRight = UDim.new(0, MIN_SIDE_PADDING)
})
controlsChildren.SizeConstraint = Roact.createElement("UISizeConstraint", {
MaxSize = Vector2.new(CONTENT_MAX_WIDTH + (MIN_SIDE_PADDING * 2), math.huge),
MinSize = Vector2.new(CONTENT_MIN_WIDTH, 0),
})
local numHeaders = 0
local numEntries = 0
local layoutOrder = 1
for _, controlsGroup in ipairs(Controls.keybinds) do
local groupTitle = controlsGroup.title
numHeaders = numHeaders + 1
controlsChildren[groupTitle.."_header"] = Roact.createElement(ControlEntryHeader, {
title = groupTitle,
LayoutOrder = layoutOrder,
})
layoutOrder = layoutOrder + 1
for controlIndex, controlAction in ipairs(controlsGroup.controls) do
numEntries = numEntries + 1
controlsChildren[groupTitle.."_"..controlIndex] = Roact.createElement(ControlEntry, {
LayoutOrder = layoutOrder,
actionDescription = controlAction.desc,
inputs = controlAction.inputs,
isList = controlAction.list,
hasDivider = controlIndex ~= #controlsGroup.controls,
})
layoutOrder = layoutOrder + 1
end
end
local canvasHeight = (HEADER_HEIGHT * numHeaders) + (ENTRY_HEIGHT * numEntries)
return Roact.createElement(ControlLayoutContainer, {
titleText = "CoreScripts.InGameMenu.Controls.KeyboardAndMouseTitle",
}, {
ScrollContent = Roact.createElement(PaddedScrollFrame, {
CanvasSize = UDim2.new(0, 0, 0, canvasHeight),
ScrollingDirection = Enum.ScrollingDirection.Y,
Size = UDim2.new(1, 0, 1, -BOTTOM_PADDING),
}, {
ListContainer = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.new(0.5, 0, 0, 0)
}, controlsChildren)
})
})
end
return KeyboardControls
@@ -0,0 +1,43 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local KeyboardControls = require(script.Parent.KeyboardControls)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
KeyboardControls = Roact.createElement(KeyboardControls),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,12 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local KeyboardControls = require(script.Parent.KeyboardControls)
local function TouchControls(props)
return Roact.createElement(KeyboardControls, props)
end
return TouchControls
@@ -0,0 +1,43 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local TouchControls = require(script.Parent.TouchControls)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
TouchControls = Roact.createElement(TouchControls),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,37 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local InGameMenu = script.Parent.Parent
local Controls = require(InGameMenu.Resources.Controls)
local KeyboardControls = require(script.ControlLayouts.KeyboardControls)
local GamepadControls = require(script.ControlLayouts.GamepadControls)
-- local TouchControls = require(InGameMenu.Components.ControlLayouts.TouchControls)
local ControlsPage = Roact.PureComponent:extend("ControlsPage")
function ControlsPage:render()
local controlLayout = self.props.controlLayout
if controlLayout == Controls.ControlLayouts.KEYBOARD then
return Roact.createElement(KeyboardControls)
elseif controlLayout == Controls.ControlLayouts.GAMEPAD then
return Roact.createElement(GamepadControls)
-- elseif controlLayout == Controls.ControlLayouts.TOUCH then
-- return Roact.createElement(TouchControls)
else
return nil
end
end
return RoactRodux.UNSTABLE_connect2(function(state)
local controlLayout = state.controlLayout
return {
controlLayout = controlLayout
}
end)(ControlsPage)
@@ -0,0 +1,40 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local Cryo = InGameMenuDependencies.Cryo
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local validateProps = t.strictInterface({
Size = t.optional(t.UDim2),
Position = t.optional(t.UDim2),
AnchorPoint = t.optional(t.Vector2),
LayoutOrder = t.optional(t.integer),
Visible = t.optional(t.boolean),
})
local function Divider(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withStyle(function(style)
-- Divider relies on being right-aligned in order to be left-indented.
return Roact.createElement("Frame", Cryo.Dictionary.join({
Size = UDim2.new(1, 0, 0, 1),
}, props, {
BorderSizePixel = 0,
BackgroundColor3 = style.Theme.Divider.Color,
BackgroundTransparency = style.Theme.Divider.Transparency,
}))
end)
end
return Divider
@@ -0,0 +1,307 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent
local Assets = require(InGameMenu.Resources.Assets)
local divideTransparency = require(InGameMenu.Utility.divideTransparency)
local ThemedButton = require(script.Parent.ThemedButton)
local ThemedTextLabel = require(script.Parent.ThemedTextLabel)
local Divider = require(script.Parent.Divider)
local BarOnTopScrollingFrame = require(script.Parent.BarOnTopScrollingFrame)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local DropDownSelection = Roact.PureComponent:extend("DropDownSelection")
local DROP_DOWN_BORDER_SIZE = 4
local MAX_ITEMS_DISPLAYED = 7.5
local ITEM_HEIGHT = 44
DropDownSelection.validateProps = t.intersection(t.strictInterface({
Size = t.UDim2,
Position = t.optional(t.UDim2),
LayoutOrder = t.optional(t.integer),
placeHolderText = t.optional(t.string),
selectedIndex = t.integer, --0 if placeholder is selected
selections = t.array(t.string),
localize = t.boolean,
enabled = t.boolean,
selectionChanged = t.callback,
}), function(props)
if props.selectedIndex < 0 then
return false, "selectedIndex must be greater than 0"
end
if props.selectedIndex > #props.selections then
return false, "selectedIndex must not be greater than the number of selections"
end
return true
end)
function DropDownSelection:init()
self.rootRef = Roact.createRef()
self.state = {
isOpen = false,
dropDownItemHeight = 10,
absolutePositionX = 0,
absolutePositionY = 0,
screenSizeX = 0,
screenSizeY = 0,
}
self.ancestryConnection = nil
self.sizeConnection = nil
end
function DropDownSelection:renderDropDownList(style, localized)
local dropDownList = {}
dropDownList.Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
})
for index, dropDownOption in ipairs(self.props.selections) do
local buttonText = dropDownOption
if self.props.localize then
buttonText = localized[index]
end
dropDownList["button" .. index] = Roact.createElement(ThemedButton, {
Size = UDim2.new(1, 0, 0, self.state.dropDownItemHeight - 1),
LayoutOrder = index * 2,
onActivated = function()
self:setState({
isOpen = false,
})
self.props.selectionChanged(index)
end,
normalThemeKey = "BackgroundUIDefault",
hoverThemeKey = "BackgroundOnHover",
imageProps = Assets.Images.WhiteSquare,
renderChildren = function(transparency, isHovered, isPressed)
local iconColor = isHovered and style.Theme.IconOnHover or style.Theme.IconDefault
local iconColor3 = iconColor.Color
local iconTransparency = divideTransparency(iconColor.Transparency, isPressed and 2 or 1)
return {
SelectedIcon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(1, 0.5),
Size = UDim2.new(0, 15, 0, 12),
Position = UDim2.new(1, -25, 0.5, 0),
Image = Assets.Images.SelectedCheck,
ImageColor3 = iconColor3,
ImageTransparency = iconTransparency,
Visible = self.props.selectedIndex == index
}),
TextLabel = Roact.createElement(ThemedTextLabel, {
fontKey = "Header2",
themeKey = "TextEmphasis",
Position = UDim2.new(0, 25, 0, 0),
Size = UDim2.new(1, -25, 1, 0),
Text = buttonText,
TextXAlignment = Enum.TextXAlignment.Left,
}),
}
end,
})
if index < #self.props.selections then
dropDownList["divider" .. index] = Roact.createElement(Divider, {
LayoutOrder = index * 2 + 1
})
end
end
return dropDownList
end
function DropDownSelection:render()
return withStyle(function(style)
return withLocalization(
self.props.localize and self.props.selections or {}
)(function(localized)
local dropDownOpenText = self.props.placeHolderText
if self.props.selectedIndex > 0 then
if self.props.localize then
dropDownOpenText = localized[self.props.selectedIndex]
else
dropDownOpenText = self.props.selections[self.props.selectedIndex]
end
end
local shadowSize = Assets.Images.Shadow.Size
local itemSize = math.min(#self.props.selections, MAX_ITEMS_DISPLAYED) * ITEM_HEIGHT
local dropDownList = self:renderDropDownList(style, localized)
return Roact.createElement("Frame", {
Size = self.props.Size,
Position = self.props.Position,
LayoutOrder = self.props.LayoutOrder,
BackgroundTransparency = 1,
[Roact.Ref] = self.rootRef,
[Roact.Change.AbsoluteSize] = function(rbx)
self:setState({
dropDownItemHeight = rbx.AbsoluteSize.Y,
})
end,
[Roact.Change.AbsolutePosition] = function(rbx)
self:setState({
absolutePositionX = rbx.AbsolutePosition.X,
absolutePositionY = rbx.AbsolutePosition.Y,
})
end,
}, {
OpenDropDownButton = Roact.createElement(ThemedButton, {
Size = UDim2.new(1, 0, 1, 0),
Visible = not self.state.isOpen,
enabled = self.props.enabled,
onActivated = function()
self:setState({
isOpen = true,
})
end,
normalThemeKey = "SecondaryDefault",
hoverThemeKey = "SecondaryOnHover",
imageProps = Assets.Images.RoundedRectBorder,
renderChildren = function(transparency, isHovered, isPressed)
local iconColor = isHovered and style.Theme.IconOnHover or style.Theme.IconDefault
local iconColor3 = iconColor.Color
local iconTransparency = divideTransparency(iconColor.Transparency, isPressed and 2 or 1)
return {
ButtonIcon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Size = UDim2.new(0, 36, 0, 36),
Position = UDim2.new(1, -16, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
Image = Assets.Images.ExpandArrow,
ImageColor3 = iconColor3,
ImageTransparency = iconTransparency,
}),
ButtonText = Roact.createElement(ThemedTextLabel, {
Text = dropDownOpenText,
themeKey = "SecondaryContent",
fontKey = "Header2",
Position = UDim2.new(0, 12, 0, 0),
Size = UDim2.new(1, -12, 1, 0),
TextTransparency = transparency,
TextXAlignment = Enum.TextXAlignment.Left,
}),
}
end,
}),
CloseDropDownArea = Roact.createElement("TextButton", {
BackgroundTransparency = 1,
Text = "",
Position = UDim2.new(0, -self.state.absolutePositionX, 0, -self.state.absolutePositionY),
Size = UDim2.new(0, self.state.screenSizeX, 0, self.state.screenSizeY),
Visible = self.state.isOpen,
[Roact.Event.Activated] = function()
self:setState({
isOpen = false,
})
end,
ZIndex = 2,
}),
DropDownShadow = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.Shadow.Image,
Position = UDim2.new(0, -shadowSize, 0, -shadowSize),
Size = UDim2.new(1, shadowSize * 2, 0, shadowSize * 2 + DROP_DOWN_BORDER_SIZE * 2 + itemSize),
ScaleType = Assets.Images.Shadow.ScaleType,
SliceCenter = Assets.Images.Shadow.SliceCenter,
Visible = self.state.isOpen,
ZIndex = 3,
}, {
DropDownBackground = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.RoundedRect.Image,
ImageColor3 = style.Theme.BackgroundUIDefault.Color,
ImageTransparency = style.Theme.BackgroundUIDefault.Transparency,
ScaleType = Assets.Images.RoundedRect.ScaleType,
Size = UDim2.new(1, -(shadowSize * 2), 1, -(shadowSize * 2)),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
SliceCenter = Assets.Images.RoundedRect.SliceCenter,
SliceScale = 0.5,
}, {
DropDownList = Roact.createElement(BarOnTopScrollingFrame, {
Position = UDim2.new(0, 0, 0, DROP_DOWN_BORDER_SIZE),
Size = UDim2.new(1, 0, 1, -DROP_DOWN_BORDER_SIZE * 2),
CanvasSize = UDim2.new(1, 0, 0, #self.props.selections * self.state.dropDownItemHeight),
scrollBarOffset = 4,
}, dropDownList),
}),
}),
})
end)
end)
end
function DropDownSelection:watchLayerCollector(layerCollector)
self:setState({
screenSizeX = layerCollector.AbsoluteSize.X,
screenSizeY = layerCollector.AbsoluteSize.Y,
})
self.sizeConnection = layerCollector:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
self:setState({
screenSizeX = layerCollector.AbsoluteSize.X,
screenSizeY = layerCollector.AbsoluteSize.Y,
})
end)
end
function DropDownSelection:didMount()
local root = self.rootRef.current
local layerCollector = root:FindFirstAncestorWhichIsA("LayerCollector")
if layerCollector == nil then
self.ancestryConnection = root.AncestryChanged:Connect(function()
layerCollector = root:FindFirstAncestorWhichIsA("LayerCollector")
if layerCollector ~= nil then
self.ancestryConnection:Disconnect()
self.ancestryConnection = nil
self:watchLayerCollector(layerCollector)
end
end)
else
self:watchLayerCollector(layerCollector)
end
end
function DropDownSelection:willUnmount()
if self.ancestryConnection then
self.ancestryConnection:Disconnect()
end
if self.sizeConnection then
self.sizeConnection:Disconnect()
end
end
return DropDownSelection
@@ -0,0 +1,97 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Cryo = InGameMenuDependencies.Cryo
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local DropDownSelection = require(script.Parent.DropDownSelection)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local dummyDropDownProps = {
Size = UDim2.new(0, 200, 0, 44),
Position = UDim2.new(0.5, 0, 0.5, 0),
LayoutOrder = 0,
placeHolderText = "Placeholder",
selectedIndex = 0,
selections = {
"Option1",
"Option2",
"Option3",
},
localize = false,
enabled = true,
selectionChanged = function()
print("selection changed")
end,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
DropDownSelection = Roact.createElement(DropDownSelection, dummyDropDownProps),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with a selected item", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
DropDownSelection = Roact.createElement(DropDownSelection, Cryo.Dictionary.join(dummyDropDownProps, {
selectedIndex = 1,
})),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should throw when passed a selectedIndex out of bounds when propvalidation is turned on", function()
Roact.setGlobalConfig({
propValidation = true,
})
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
DropDownSelection = Roact.createElement(DropDownSelection, Cryo.Dictionary.join(dummyDropDownProps, {
selectedIndex = 100,
})),
}),
})
expect(function()
local instance = Roact.mount(element)
Roact.unmount(instance)
end).to.throw()
end)
end
@@ -0,0 +1,123 @@
local GuiService = game:GetService("GuiService")
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local RbxAnalyticsService = game:GetService("RbxAnalyticsService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local Images = UIBlox.App.ImageSet.Images
local InGameMenu = script.Parent.Parent
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Constants = require(InGameMenu.Resources.Constants)
local CloseNativeClosePrompt = require(InGameMenu.Actions.CloseNativeClosePrompt)
local SetMenuIconTooltipOpen = require(InGameMenu.Actions.SetMenuIconTooltipOpen)
local OpenMenu = require(InGameMenu.Thunks.OpenMenu)
local EducationalPopupDialog = require(script.Parent.EducationalPopupDialog)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local GetFFlagInGameMenuIconTooltip = require(RobloxGui.Modules.Flags.GetFFlagInGameMenuIconTooltip)
local NotificationType = GuiService:GetNotificationTypeList()
local EducationalAnalytics = {
EventContext = "educational_popup",
ConfirmName = "educational_confirmed",
CancelName = "educational_close_app",
DismissName = "educational_dismiss_prompt",
}
local EducationalPopup = Roact.PureComponent:extend("EducationalPopup")
EducationalPopup.validateProps = t.strictInterface({
screenSize = t.Vector2,
onDismiss = t.callback,
onConfirm = t.callback,
isClosingApp = t.boolean,
})
function EducationalPopup:init()
self.onCancel = function()
SendAnalytics(EducationalAnalytics.EventContext, EducationalAnalytics.CancelName, {})
GuiService:BroadcastNotification("", NotificationType.NATIVE_EXIT)
end
end
function EducationalPopup:render()
return withLocalization({
bodyTextLn1 = "CoreScripts.InGameMenu.EducationalPopup.BodyTextOpenSystemMenu",
bodyTextLn2 = "CoreScripts.InGameMenu.EducationalPopup.BodyTextClickHome",
bodyTextLn3 = "CoreScripts.InGameMenu.EducationalPopup.BodyTextPlayMore",
titleText = "CoreScripts.InGameMenu.EducationalPopup.Title",
confirmText = "CoreScripts.InGameMenu.Ok",
cancelText = "CoreScripts.InGameMenu.EducationalPopup.LeaveRoblox",
})(function(localized)
return Roact.createElement(EducationalPopupDialog, {
bodyContents = {
{
icon = Images["icons/logo/block"],
text = localized.bodyTextLn1,
layoutOrder = 1,
isSystemMenuIcon = true,
},
{
icon = Images["icons/menu/home_off"],
text = localized.bodyTextLn2,
layoutOrder = 2,
},
{
icon = Images["icons/menu/games_off"],
text = localized.bodyTextLn3,
layoutOrder = 3,
},
},
cancelText = localized.cancelText,
confirmText = localized.confirmText,
titleText = localized.titleText,
titleBackgroundImageProps = {
image = "rbxasset://textures/ui/LuaApp/graphic/EducationalBackground.png",
imageHeight = 261,
},
screenSize = self.props.screenSize,
onDismiss = self.props.onDismiss,
onCancel = self.onCancel,
onConfirm = self.props.onConfirm,
blurBackground = true,
visible = self.props.isClosingApp,
})
end)
end
return RoactRodux.UNSTABLE_connect2(function(state, props)
return {
isClosingApp = state.nativeClosePrompt.closingApp,
screenSize = state.screenSize,
}
end, function(dispatch)
return {
onDismiss = function()
dispatch(CloseNativeClosePrompt())
RbxAnalyticsService:ReportCounter("EducationalPopup_Dismiss", 1)
SendAnalytics(EducationalAnalytics.EventContext, EducationalAnalytics.DismissName, {})
end,
onConfirm = function()
dispatch(CloseNativeClosePrompt())
if GetFFlagInGameMenuIconTooltip() then
dispatch(SetMenuIconTooltipOpen(true))
else
dispatch(OpenMenu(Constants.AnalyticsMenuOpenTypes.EducationalMenuTriggered))
end
RbxAnalyticsService:ReportCounter("EducationalPopup_Confirm", 1)
SendAnalytics(EducationalAnalytics.EventContext, EducationalAnalytics.ConfirmName, {})
end,
}
end)(EducationalPopup)
@@ -0,0 +1,47 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local EducationalPopup = require(script.Parent.EducationalPopup)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
EducationalPopup = Roact.createElement(EducationalPopup, {
screenSize = Vector2.new(1920, 1080),
onDismiss = function()end,
onConfirm = function()end,
isClosingApp = false,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,97 @@
local CoreGui = game:GetService("CoreGui")
local RunService = game:GetService("RunService")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local EducationalModal = UIBlox.App.Dialog.Modal.EducationalModal
local withStyle = UIBlox.Core.Style.withStyle
local EducationalPopupDialog = Roact.PureComponent:extend("EducationalPopupDialog")
EducationalPopupDialog.validateProps = t.strictInterface({
bodyContents = t.array(t.strictInterface({
icon = t.union(t.string, t.table),
text = t.string,
layoutOrder = t.integer,
isSystemMenuIcon = t.optional(t.boolean),
})),
cancelText = t.string,
confirmText = t.string,
titleText = t.string,
titleBackgroundImageProps = t.strictInterface({
image = t.string,
imageHeight = t.number,
}),
screenSize = t.Vector2,
onDismiss = t.callback,
onCancel = t.callback,
onConfirm = t.callback,
blurBackground = t.boolean,
visible = t.boolean,
})
EducationalPopupDialog.defaultProps = {
blurBackground = false,
}
function EducationalPopupDialog:render()
return withStyle(function(style)
return Roact.createElement(Roact.Portal, {
target = CoreGui,
}, {
InGameMenuEducationalPopupDialog = Roact.createElement("ScreenGui", {
IgnoreGuiInset = true,
OnTopOfCoreBlur = self.props.blurBackground,
Enabled = self.props.visible,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
}, {
Overlay = Roact.createElement("TextButton", {
AutoButtonColor = false,
BackgroundColor3 = style.Theme.Overlay.Color,
BackgroundTransparency = style.Theme.Overlay.Transparency,
BorderSizePixel = 0,
Size = UDim2.fromScale(1, 1),
Text = "",
}),
Popup = Roact.createElement(EducationalModal, {
bodyContents = self.props.bodyContents,
cancelText = self.props.cancelText,
confirmText = self.props.confirmText,
titleText = self.props.titleText,
titleBackgroundImageProps = self.props.titleBackgroundImageProps,
screenSize = self.props.screenSize,
onDismiss = self.props.onDismiss,
onCancel = self.props.onCancel,
onConfirm = self.props.onConfirm,
})
})
})
end)
end
function EducationalPopupDialog:updateBlur()
local shouldBlur = self.props.blurBackground and self.props.visible
RunService:SetRobloxGuiFocused(shouldBlur)
end
function EducationalPopupDialog:didMount()
self:updateBlur()
end
function EducationalPopupDialog:didUpdate()
self:updateBlur()
end
function EducationalPopupDialog:willUnmount()
if self.props.blurBackground then
RunService:SetRobloxGuiFocused(false)
end
end
return EducationalPopupDialog
@@ -0,0 +1,72 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local Images = UIBlox.App.ImageSet.Images
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local EducationalPopupDialog = require(script.Parent.EducationalPopupDialog)
local mockProps = {
bodyContents = {
{
icon = Images["icons/logo/block"],
text = "Body 1",
layoutOrder = 1
},
{
icon = Images["icons/menu/home_on"],
text = "Body 2",
layoutOrder = 2
},
{
icon = Images["icons/menu/games_on"],
text = "Body 3",
layoutOrder = 3
},
},
cancelText = "Cancel",
confirmText = "Confirm",
titleText = "Title",
titleBackgroundImageProps = {
image = "rbxassetid://2610133241",
imageHeight = 200,
},
screenSize = Vector2.new(1920, 1080),
onDismiss = function()end,
onCancel = function()end,
onConfirm = function()end,
blurBackground = true,
visible = true,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
EducationalPopupDialog = Roact.createElement(EducationalPopupDialog, mockProps),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,158 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local GuiService = game:GetService("GuiService")
local AppStorageService = game:GetService("AppStorageService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local FullscreenTitleBarComponent = UIBlox.App.Bar.FullscreenTitleBar
local InGameMenu = script.Parent.Parent
local InGameMenuPolicy = require(InGameMenu.InGameMenuPolicy)
local OpenEducationalPopup = require(InGameMenu.Thunks.OpenEducationalPopup)
local StartLeavingGame = require(InGameMenu.Actions.StartLeavingGame)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local UserLocalStore = require(InGameMenu.Utility.UserLocalStore)
local GetFFlagPlayerSpecificPopupCounter = require(InGameMenu.Flags.GetFFlagPlayerSpecificPopupCounter)
local DISAPPEAR_DELAY = 0.5
local FullscreenTitleBar = Roact.PureComponent:extend("FullscreenTitleBar")
FullscreenTitleBar.validateProps = t.strictInterface({
titleText = t.optional(t.string),
displayOrder = t.integer,
isMenuOpen = t.boolean,
isEducationalPopupEnabled = t.boolean,
maxDisplayCount = t.integer,
openEducationalPopup = t.callback,
startLeavingGame = t.callback,
})
FullscreenTitleBar.defaultProps = {
titleText = "Roblox",
}
function FullscreenTitleBar:init()
self:setState({
isTriggered = false,
fullscreenEnabled = UserGameSettings:InFullScreen(),
})
self.onFullscreenChanged = function(newFullscreenStatus)
self:setState({
fullscreenEnabled = newFullscreenStatus,
isTriggered = false,
})
end
self.triggerTitleBar = function()
if not self.state.isTriggered then
self:setState({
isTriggered = true,
})
end
end
self.hideTitleBar = function()
if self.state.isTriggered then
self:setState({
isTriggered = false,
})
end
end
self.onMouseLeave = function()
delay(DISAPPEAR_DELAY, self.hideTitleBar)
end
self.exitFullscreen = function()
self.hideTitleBar()
GuiService:ToggleFullscreen()
end
self.closeRoblox = function()
self.hideTitleBar()
if self.props.isEducationalPopupEnabled then
local localStore = AppStorageService
if GetFFlagPlayerSpecificPopupCounter() then
localStore = UserLocalStore.new()
end
self.props.openEducationalPopup(GuiService, localStore, self.props.maxDisplayCount)
else
self.props.startLeavingGame()
end
end
end
function FullscreenTitleBar:render()
return Roact.createFragment({
FullscreenChangedEvent = Roact.createElement(ExternalEventConnection, {
event = UserGameSettings.FullscreenChanged,
callback = self.onFullscreenChanged,
}),
FullscreenTitleBar = Roact.createElement(Roact.Portal, {
target = CoreGui,
}, {
InGameFullscreenTitleBarScreen = Roact.createElement("ScreenGui", {
Enabled = self.state.fullscreenEnabled,
IgnoreGuiInset = true,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
DisplayOrder = self.props.displayOrder,
}, {
TriggerArea = Roact.createElement("Frame", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 1),
[Roact.Event.MouseEnter] = self.triggerTitleBar,
}),
Bar = Roact.createElement(FullscreenTitleBarComponent, {
title = self.props.titleText,
isTriggered = self.state.isTriggered,
onDisappear = self.onMouseLeave,
exitFullscreen = self.exitFullscreen,
closeRoblox = self.closeRoblox,
}),
}),
})
})
end
function FullscreenTitleBar:didUpdate(prevProps)
if not prevProps.isMenuOpen and self.props.isMenuOpen then
self.hideTitleBar()
end
end
FullscreenTitleBar = InGameMenuPolicy.connect(function(appPolicy, props)
return {
isEducationalPopupEnabled = appPolicy.enableEducationalPopup(),
maxDisplayCount = appPolicy.educationalPopupMaxDisplayCount(),
}
end)(FullscreenTitleBar)
return RoactRodux.UNSTABLE_connect2(
function(state, props)
return {
isMenuOpen = state.isMenuOpen,
}
end,
function(dispatch)
return {
openEducationalPopup = function(guiService, appStorageService, maxCount)
return dispatch(OpenEducationalPopup(guiService, appStorageService, maxCount))
end,
startLeavingGame = function()
dispatch(StartLeavingGame())
end,
}
end
)(FullscreenTitleBar)
@@ -0,0 +1,85 @@
return function()
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local InGameMenuPolicy = require(InGameMenu.InGameMenuPolicy)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local mockPolicyMapper = function(policy)
return {
enableEducationalPopup = function()
return false
end,
educationalPopupMaxDisplayCount = function()
return 0
end,
}
end
local FullscreenTitleBar = require(script.Parent.FullscreenTitleBar)
local mockProps = {
titleText = "Roblox",
displayOrder = 1,
isMenuOpen = true,
isEducationalPopupEnabled = true,
maxDisplayCount = 1,
openEducationalPopup = function() end,
startLeavingGame = function() end,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
PolicyProvider = Roact.createElement(InGameMenuPolicy.Provider, {
policy = { mockPolicyMapper },
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
FullscreenTitleBar = Roact.createElement(FullscreenTitleBar, mockProps),
}),
}),
})
local handle = Roact.mount(element)
Roact.unmount(handle)
end)
it("should be a child ScreenGui of CoreGui", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
PolicyProvider = Roact.createElement(InGameMenuPolicy.Provider, {
policy = { mockPolicyMapper },
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
FullscreenTitleBar = Roact.createElement(FullscreenTitleBar, mockProps),
}),
}),
})
local handle = Roact.mount(element)
local instance = CoreGui:FindFirstChild("InGameFullscreenTitleBarScreen")
expect(instance).to.never.equal(nil)
expect(instance.ClassName).to.equal("ScreenGui")
Roact.unmount(handle)
end)
end
@@ -0,0 +1,78 @@
--[[
A square game icon
]]
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent
local withStyle = UIBlox.Core.Style.withStyle
local Images = UIBlox.App.ImageSet.Images
local AssetImage = require(InGameMenu.Components.AssetImage)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local getGameIconRequestSize = require(InGameMenu.Components.getGameIconRequestSize)
local MAX_PLACEHOLDER_SIZE = 44
local GameIcon = Roact.PureComponent:extend("GameIcon")
GameIcon.validateProps = t.strictInterface({
gameId = t.number,
iconSize = t.numberMin(0),
layoutOrder = t.optional(t.number),
})
function GameIcon:render()
local iconSize = self.props.iconSize
local requestIconSize = getGameIconRequestSize(iconSize)
local gameId = self.props.gameId
local iconUrl = ("rbxthumb://type=GameIcon&id=%d&w=%d&h=%d"):format(
gameId,
requestIconSize,
requestIconSize
)
--Special case for when the gameId is not available. For example developing in studio with a un-published game.
local isPlaceholder = gameId == 0
if isPlaceholder then
local placeholderIconSize = MAX_PLACEHOLDER_SIZE
if iconSize <= MAX_PLACEHOLDER_SIZE then
placeholderIconSize = iconSize
end
return withStyle(function(style)
return Roact.createElement(AssetImage.Label, {
imageKey = "RoundedRect",
Size = UDim2.fromOffset(iconSize, iconSize),
ImageColor3 = style.Theme.PlaceHolder.Color,
ImageTransparency = style.Theme.PlaceHolder.Transparency,
LayoutOrder = self.props.layoutOrder,
}, {
PlaceholderIcon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(placeholderIconSize, placeholderIconSize),
Position = UDim2.fromScale(0.5, 0.5),
Image = Images["icons/imageUnavailable"],
ImageColor3 = style.Theme.UIDefault.Color,
ImageTransparency = style.Theme.UIDefault.Transparency,
AnchorPoint = Vector2.new(0.5, 0.5),
}),
})
end)
else
return Roact.createElement("ImageLabel", {
Image = iconUrl,
Size = UDim2.fromOffset(iconSize, iconSize),
BackgroundTransparency = 1,
LayoutOrder = self.props.layoutOrder,
})
end
end
return GameIcon
@@ -0,0 +1,70 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local GameIcon = require(script.Parent.GameIcon)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GameIcon = Roact.createElement(GameIcon, {
gameId = game.gameId,
iconSize = 100,
layoutOrder = 1,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy placeholder game icon without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GameIcon = Roact.createElement(GameIcon, {
gameId = 0,
iconSize = 100,
layoutOrder = 1,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,51 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local InGameMenu = script.Parent.Parent
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local GameIcon = require(InGameMenu.Components.GameIcon)
local GAME_ICON_RENDER_SIZE = 100
local EXTERIOR_PADDING = 24
local TEXT_ICON_PADDING = 12
local function GameIconHeader(props)
return Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, GAME_ICON_RENDER_SIZE + EXTERIOR_PADDING * 2),
}, {
Padding = Roact.createElement("UIPadding", {
PaddingBottom = UDim.new(0, EXTERIOR_PADDING),
PaddingLeft = UDim.new(0, EXTERIOR_PADDING),
PaddingRight = UDim.new(0, EXTERIOR_PADDING),
PaddingTop = UDim.new(0, EXTERIOR_PADDING),
}),
GameIcon = Roact.createElement(GameIcon, {
gameId = game.GameId,
iconSize = 100,
}),
GameName = Roact.createElement(ThemedTextLabel, {
fontKey = "Title",
themeKey = "TextEmphasis",
Position = UDim2.new(0, GAME_ICON_RENDER_SIZE + TEXT_ICON_PADDING, 0, 0),
Size = UDim2.new(1, -(GAME_ICON_RENDER_SIZE + TEXT_ICON_PADDING), 1, 0),
Text = props.gameName,
TextTruncate = Enum.TextTruncate.AtEnd,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
}),
})
end
return RoactRodux.UNSTABLE_connect2(function(state)
return {
gameName = state.localization.currentGameName,
}
end)(GameIconHeader)
@@ -0,0 +1,43 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local GameIconHeader = require(script.Parent.GameIconHeader)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GameIconHeader = Roact.createElement(GameIconHeader),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,138 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local MicroProfilerChanged = UserGameSettings:GetPropertyChangedSignal("OnScreenProfilerEnabled")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local withStyle = UIBlox.Core.Style.withStyle
local DevConsoleMaster = require(CoreGui.RobloxGui.Modules.DevConsoleMaster)
local InGameMenu = script.Parent.Parent.Parent
local Divider = require(InGameMenu.Components.Divider)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local Page = require(script.Parent.Parent.Page)
local CategoryHeader = require(script.Parent.CategoryHeader)
local ToggleEntry = require(script.Parent.ToggleEntry)
local VersionReporter = require(script.Parent.VersionReporter)
local CloseMenu = require(InGameMenu.Thunks.CloseMenu)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local AdvancedPage = Roact.PureComponent:extend("AdvancedPage")
AdvancedPage.validateProps = t.strictInterface({
closeMenu = t.callback,
pageTitle = t.string,
})
function AdvancedPage:init()
self:setState({
microProfilerEnabled = UserGameSettings.OnScreenProfilerEnabled,
performanceStatsEnabled = UserGameSettings.PerformanceStatsVisible,
})
end
function AdvancedPage:render()
return withStyle(function(style)
return Roact.createElement(Page, {
pageTitle = self.props.pageTitle,
zIndex = 2,
position = self.props.position,
}, {
Layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Top,
}),
AdvancedHeader = Roact.createElement(CategoryHeader, {
LayoutOrder = 1,
localizationKey = "CoreScripts.InGameMenu.GameSettings.AdvancedSettingsTitle",
}),
PerformanceStats = Roact.createElement(ToggleEntry, {
LayoutOrder = 2,
labelKey = "CoreScripts.InGameMenu.GameSettings.ShowPerfStats",
checked = self.state.performanceStatsEnabled,
onToggled = function()
UserGameSettings.PerformanceStatsVisible = not UserGameSettings.PerformanceStatsVisible
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
MicroProfiler = Roact.createElement(ToggleEntry, {
LayoutOrder = 3,
labelKey = "CoreScripts.InGameMenu.GameSettings.ShowMicroProfiler",
checked = self.state.microProfilerEnabled,
onToggled = function()
UserGameSettings.OnScreenProfilerEnabled = not UserGameSettings.OnScreenProfilerEnabled
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
DeveloperConsole = withLocalization({
text = "CoreScripts.InGameMenu.GameSettings.DeveloperConsole"
})(function(localized)
return Roact.createElement("TextButton", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 54),
Text = localized.text,
TextColor3 = style.Theme.TextEmphasis.Color,
Font = style.Font.Header2.Font,
TextSize = style.Font.Header2.RelativeSize * style.Font.BaseSize,
TextXAlignment = Enum.TextXAlignment.Left,
LayoutOrder = 4,
[Roact.Event.Activated] = function()
DevConsoleMaster:SetVisibility(true)
self.props.closeMenu()
end,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 24),
}),
})
end),
Divider = Roact.createElement(Divider, {
Size = UDim2.new(1, -24, 0, 1),
LayoutOrder = 5,
}),
VersionReporter = Roact.createElement(VersionReporter, {
LayoutOrder = 6,
}),
MicroProfilerVisibilityListener = Roact.createElement(ExternalEventConnection, {
event = MicroProfilerChanged,
callback = function()
self:setState({
microProfilerEnabled = UserGameSettings.OnScreenProfilerEnabled,
})
end,
}),
PerformanceStatsVisibilityListener = Roact.createElement(ExternalEventConnection, {
event = UserGameSettings.PerformanceStatsVisibleChanged,
callback = function()
self:setState({
performanceStatsEnabled = UserGameSettings.PerformanceStatsVisible,
})
end,
}),
})
end)
end
return RoactRodux.UNSTABLE_connect2(nil, function(dispatch)
return {
closeMenu = function()
dispatch(CloseMenu)
end
}
end)(AdvancedPage)
@@ -0,0 +1,90 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local ToggleEntry = require(script.Parent.ToggleEntry)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local AutoPropertyToggleEntry = Roact.PureComponent:extend("AutoPropertyToggleEntry")
AutoPropertyToggleEntry.validateProps = t.strictInterface({
instance = t.Instance,
key = t.string,
lockedToOff = t.optional(t.boolean),
onValue = t.optional(t.any),
offValue = t.optional(t.any),
labelKey = t.string,
LayoutOrder = t.integer,
subtextEnabled = t.optional(t.boolean),
subtextKey = t.optional(t.string),
})
AutoPropertyToggleEntry.defaultProps = {
onValue = true,
offValue = false,
lockedToOff = false,
subtextEnabled = false,
subtextKey = "",
}
function AutoPropertyToggleEntry:init(props)
local currentValue = props.instance[props.key]
self.changeEvent = props.instance:GetPropertyChangedSignal(props.key)
self:setState({
checked = not props.lockedToOff and currentValue == props.onValue,
})
self.onExternalChange = function()
local newValue = props.instance[props.key]
self:setState({
checked = newValue == props.onValue
})
end
self.onToggled = function()
if self.props.lockedToOff then
return
end
local isOn = props.instance[props.key] == props.onValue
if isOn then
props.instance[props.key] = props.offValue
else
props.instance[props.key] = props.onValue
end
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end
end
function AutoPropertyToggleEntry:render()
local props = self.props
return Roact.createFragment({
Toggle = Roact.createElement(ToggleEntry, {
checked = self.state.checked,
lockedToOff = props.lockedToOff,
labelKey = props.labelKey,
LayoutOrder = props.LayoutOrder,
onToggled = self.onToggled,
subtextEnabled = props.subtextEnabled,
subtextKey = props.subtextKey,
}),
Listener = Roact.createElement(ExternalEventConnection, {
event = self.changeEvent,
callback = self.onExternalChange,
}),
})
end
return AutoPropertyToggleEntry
@@ -0,0 +1,57 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local AutoPropertyToggleEntry = require(script.Parent.AutoPropertyToggleEntry)
it("should create and destroy without errors", function()
local dummyValue = Instance.new("BoolValue")
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
AutoPropertyToggleEntry = Roact.createElement(AutoPropertyToggleEntry, {
instance = dummyValue,
key = "Value",
lockedToOff = false,
onValue = true,
offValue = false,
labelKey = "CoreScripts.InGameMenu.GameSettings.ShowPerfStats",
LayoutOrder = 2,
}),
}),
}),
})
local instance = Roact.mount(element)
dummyValue.Value = false
dummyValue.Value = true
Roact.unmount(instance)
end)
end
@@ -0,0 +1,258 @@
local Players = game:GetService("Players")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local GuiService = game:GetService("GuiService")
local CorePackages = game:GetService("CorePackages")
local VRService = game:GetService("VRService")
local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local localPlayer = Players.LocalPlayer
local DevEnableMouseLockChanged = localPlayer:GetPropertyChangedSignal("DevEnableMouseLock")
local IsUsingCameraYInvertedChanged = UserGameSettings:GetPropertyChangedSignal("IsUsingCameraYInverted")
local InGameMenu = script.Parent.Parent.Parent
local Divider = require(InGameMenu.Components.Divider)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local Page = require(InGameMenu.Components.Page)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Assets = require(InGameMenu.Resources.Assets)
local SetCurrentPage = require(InGameMenu.Actions.SetCurrentPage)
local AutoPropertyToggleEntry = require(script.Parent.AutoPropertyToggleEntry)
local CameraModeEntry = require(script.Parent.CameraModeEntry)
local CameraSensitivityEntry = require(script.Parent.CameraSensitivityEntry)
local CategoryHeader = require(script.Parent.CategoryHeader)
local GraphicsQualityEntry = require(script.Parent.GraphicsQualityEntry)
local MovementModeEntry = require(script.Parent.MovementModeEntry)
local ToggleEntry = require(script.Parent.ToggleEntry)
local VolumeEntry = require(script.Parent.VolumeEntry)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local FFlagInGameMenuVRToggle = game:DefineFastFlag("InGameMenuVRToggle", false)
local GetFFlagFullscreenAnalytics = require(RobloxGui.Modules.Flags.GetFFlagFullscreenAnalytics)
local VRAvailableChanged = VRService:GetPropertyChangedSignal("VREnabled")
local VREnabledChanged = UserGameSettings:GetPropertyChangedSignal("VREnabled")
local BasicPage = Roact.PureComponent:extend("BasicPage")
BasicPage.validateProps = t.strictInterface({
switchToAdvancedPage = t.callback,
pageTitle = t.string,
})
local vrEnabledAtModuleLoad = UserGameSettings.VREnabled
function BasicPage:init()
self:setState({
shiftLockEnabled = localPlayer.DevEnableMouseLock,
fullScreenEnabled = UserGameSettings:InFullScreen(),
invertedCameraEnabled = UserGameSettings.IsUsingCameraYInverted,
vrAvailable = VRService.VREnabled,
vrEnabled = UserGameSettings.VREnabled,
})
self.pageSize, self.setPageSize = Roact.createBinding(UDim2.new(0, 0, 0, 0))
end
function BasicPage:render()
local shouldSettingsDisabledInVRBeShown = not (FFlagInGameMenuVRToggle and self.state.vrEnabled and self.state.vrAvailable)
return Roact.createElement(Page, {
pageTitle = self.props.pageTitle,
position = self.props.position,
}, {
PageContents = Roact.createElement("ScrollingFrame", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
CanvasSize = self.pageSize,
Position = self.props.position,
Size = UDim2.new(1, 0, 1, 0),
}, {
Layout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Right,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
[Roact.Change.AbsoluteContentSize] = function(rbx)
self.setPageSize(UDim2.new(0, 0, 0, rbx.AbsoluteContentSize.Y))
end,
}),
CameraHeader = Roact.createElement(CategoryHeader, {
LayoutOrder = 1,
localizationKey = "CoreScripts.InGameMenu.GameSettings.CameraTitle",
}),
CameraMode = shouldSettingsDisabledInVRBeShown and Roact.createElement(CameraModeEntry, {
LayoutOrder = 2,
}),
InvertedCamera = shouldSettingsDisabledInVRBeShown and self.state.invertedCameraEnabled and Roact.createElement(AutoPropertyToggleEntry, {
LayoutOrder = 3,
labelKey = "CoreScripts.InGameMenu.GameSettings.InvertedCamera",
instance = UserGameSettings,
key = "CameraYInverted",
}),
CameraSensitivity = shouldSettingsDisabledInVRBeShown and Roact.createElement(CameraSensitivityEntry, {
LayoutOrder = 4,
}),
CameraDivider = Roact.createElement(Divider, {
LayoutOrder = 5,
Size = UDim2.new(1, -24, 0, 1),
}),
ControlsHeader = Roact.createElement(CategoryHeader, {
LayoutOrder = 6,
localizationKey = "CoreScripts.InGameMenu.GameSettings.ControlsAudio",
}),
ShiftLock = shouldSettingsDisabledInVRBeShown and Roact.createElement(AutoPropertyToggleEntry, {
LayoutOrder = 7,
labelKey = "CoreScripts.InGameMenu.GameSettings.ShiftLock",
instance = UserGameSettings,
key = "ControlMode",
onValue = Enum.ControlMode.MouseLockSwitch,
offValue = Enum.ControlMode.Classic,
lockedToOff = not self.state.shiftLockEnabled,
}),
MovementMode = shouldSettingsDisabledInVRBeShown and Roact.createElement(MovementModeEntry, {
LayoutOrder = 8,
}),
VolumeEntry = Roact.createElement(VolumeEntry, {
LayoutOrder = 9,
}),
ControlsDivider = Roact.createElement(Divider, {
LayoutOrder = 10,
Size = UDim2.new(1, -24, 0, 1),
}),
GraphicsHeader = Roact.createElement(CategoryHeader, {
LayoutOrder = 11,
localizationKey = "CoreScripts.InGameMenu.GameSettings.GraphicsTitle",
}),
GraphicsQualityEntry = Roact.createElement(GraphicsQualityEntry, {
LayoutOrder = 12,
}),
FullScreen = shouldSettingsDisabledInVRBeShown and Roact.createElement(ToggleEntry, {
LayoutOrder = 13,
labelKey = "CoreScripts.InGameMenu.GameSettings.FullScreen",
checked = self.state.fullScreenEnabled,
onToggled = function()
GuiService:ToggleFullscreen()
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
VRMode = FFlagInGameMenuVRToggle and self.state.vrAvailable and Roact.createElement(AutoPropertyToggleEntry, {
LayoutOrder = 14,
labelKey = "CoreScripts.InGameMenu.GameSettings.VREnabled",
instance = UserGameSettings,
key = "VREnabled",
subtextEnabled = self.state.vrEnabled ~= vrEnabledAtModuleLoad,
subtextKey = "CoreScripts.InGameMenu.GameSettings.RestartPending",
}),
GraphicsDivider = Roact.createElement(Divider, {
LayoutOrder = 15,
Size = UDim2.new(1, -24, 0, 1),
}),
AdvancedSettings = Roact.createElement("TextButton", {
LayoutOrder = 16,
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 54),
Text = "",
[Roact.Event.Activated] = self.props.switchToAdvancedPage,
}, {
Text = withLocalization({
text = "CoreScripts.InGameMenu.GameSettings.SwitchToAdvancedSettings",
})(function(localization)
return Roact.createElement(ThemedTextLabel, {
Text = localization.text,
TextXAlignment = Enum.TextXAlignment.Left,
fontKey = "Body",
themeKey = "TextDefault",
Size = UDim2.new(1, -96, 1, 0),
Position = UDim2.new(0, 24, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
})
end),
Icon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.NavigateForward,
Size = UDim2.new(0, 36, 0, 36),
Position = UDim2.new(1, -36, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
}),
}),
AdvancedDivider = Roact.createElement(Divider, {
LayoutOrder = 17,
Size = UDim2.new(1, -24, 0, 1),
}),
ShiftLockEnabledListener = Roact.createElement(ExternalEventConnection, {
event = DevEnableMouseLockChanged,
callback = function()
self:setState({
shiftLockEnabled = localPlayer.DevEnableMouseLock,
})
end,
}),
FullScreenListener = Roact.createElement(ExternalEventConnection, {
event = UserGameSettings.FullscreenChanged,
callback = function(newFullscreenStatus)
self:setState({
fullScreenEnabled = newFullscreenStatus,
})
if GetFFlagFullscreenAnalytics() then
SendAnalytics(Constants.AnalyticsInGameMenuName, Constants.AnalyticsFullscreenModeName, {
enabled = newFullscreenStatus,
})
end
end,
}),
CameraYInvertedListener = Roact.createElement(ExternalEventConnection, {
event = IsUsingCameraYInvertedChanged,
callback = function()
self:setState({
invertedCameraEnabled = UserGameSettings.IsUsingCameraYInverted,
})
end,
}),
VRAvailableListener = FFlagInGameMenuVRToggle and Roact.createElement(ExternalEventConnection, {
event = VRAvailableChanged,
callback = function()
self:setState({
vrAvailable = VRService.VREnabled
})
end,
}),
VREnabledListener = FFlagInGameMenuVRToggle and Roact.createElement(ExternalEventConnection, {
event = VREnabledChanged,
callback = function()
self:setState({
vrEnabled = UserGameSettings.VREnabled
})
end,
}),
})
})
end
return RoactRodux.UNSTABLE_connect2(nil, function(dispatch)
return {
switchToAdvancedPage = function()
dispatch(SetCurrentPage(Constants.advancedSettingsPageKey))
end,
}
end)(BasicPage)
@@ -0,0 +1,140 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local ComputerCameraMovementModeChanged = UserGameSettings:GetPropertyChangedSignal("ComputerCameraMovementMode")
local localPlayer = Players.LocalPlayer
local playerScripts = localPlayer:WaitForChild("PlayerScripts")
local DevComputerMovementModeChanged = localPlayer:GetPropertyChangedSignal("DevComputerMovementMode")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local DropDownSelection = require(InGameMenu.Components.DropDownSelection)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local InputLabel = require(script.Parent.InputLabel)
local DeveloperLockLabel = require(script.Parent.DeveloperLockLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local CAMERA_MODE_LOCALIZATION_KEYS = {
[Enum.ComputerCameraMovementMode.Classic] = "CoreScripts.InGameMenu.GameSettings.CameraModeClassic",
[Enum.ComputerCameraMovementMode.Follow] = "CoreScripts.InGameMenu.GameSettings.CameraModeFollow",
[Enum.ComputerCameraMovementMode.Default] = "CoreScripts.InGameMenu.GameSettings.Default",
[Enum.ComputerCameraMovementMode.CameraToggle] = "CoreScripts.InGameMenu.GameSettings.CameraModeCameraToggle",
}
local CameraModeEntry = Roact.PureComponent:extend("CameraSensitivityEntry")
CameraModeEntry.validateProps = t.strictInterface({
LayoutOrder = t.integer,
})
function CameraModeEntry:init()
self:setState({
selectedComputerMode = UserGameSettings.ComputerCameraMovementMode,
computerOptions = playerScripts:GetRegisteredComputerCameraMovementModes(),
developerComputerMode = localPlayer.DevComputerCameraMode,
})
end
function CameraModeEntry:render()
-- TODO: Support gamepad and touch controls.
local result = {
ComputerCameraMovementModeListener = Roact.createElement(ExternalEventConnection, {
event = ComputerCameraMovementModeChanged,
callback = function()
self:setState({
selectedComputerMode = UserGameSettings.ComputerCameraMovementMode,
})
end,
}),
ComputerCameraMovementModeRegisteredListener = Roact.createElement(ExternalEventConnection, {
event = playerScripts.ComputerCameraMovementModeRegistered,
callback = function()
self:setState({
computerOptions = playerScripts:GetRegisteredComputerCameraMovementModes(),
})
end,
}),
LocalPlayerComputerMovementMode = Roact.createElement(ExternalEventConnection, {
event = DevComputerMovementModeChanged,
callback = function()
self:setState({
developerComputerMode = localPlayer.DevComputerCameraMode,
})
end,
}),
}
if #self.state.computerOptions == 0 then
return Roact.createFragment(result)
end
local disabled = self.state.developerComputerMode ~= Enum.DevComputerCameraMovementMode.UserChoice
local optionTexts = {}
local selectedIndex = 0
if not disabled then
for index, enum in ipairs(self.state.computerOptions) do
optionTexts[index] = CAMERA_MODE_LOCALIZATION_KEYS[enum]
assert(optionTexts[index] ~= nil, "Camera movement mode " .. enum.Name .. " has no localization key")
if self.state.selectedComputerMode.Value == enum.Value then
selectedIndex = index
end
end
end
result.CameraModeEntrySelector = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 44 + 56 + 20),
BackgroundTransparency = 1,
LayoutOrder = self.props.LayoutOrder,
ZIndex = 2,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
}),
InputLabel = Roact.createElement(InputLabel, {
localizationKey = "CoreScripts.InGameMenu.GameSettings.CameraMode",
Size = UDim2.new(1, 0, 0, 56),
Position = UDim2.new(0, 0, 0, 0),
AnchorPoint = Vector2.new(0, 0),
}),
Dropdown = withLocalization({
placeholder = disabled and CAMERA_MODE_LOCALIZATION_KEYS[self.state.developerComputerMode] or nil
})(function(localized)
return Roact.createElement(DropDownSelection, {
Size = UDim2.new(1, 0, 0, 44),
Position = UDim2.new(0, 0, 0, 56),
selections = optionTexts,
selectedIndex = selectedIndex,
placeHolderText = self.state.developerComputerMode.Name,
enabled = not disabled,
localize = true,
selectionChanged = function(newSelection)
UserGameSettings.ComputerCameraMovementMode = self.state.computerOptions[newSelection]
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
})
end),
LockedLabel = disabled and Roact.createElement(DeveloperLockLabel, {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 0, 1, 0),
Size = UDim2.new(1, -72, 0.25, 0),
}),
})
return Roact.createFragment(result)
end
return CameraModeEntry
@@ -0,0 +1,147 @@
local MIN_ENGINE_MOUSE_SENSITIVITY = 0
local MAX_ENGINE_MOUSE_SENSITIVITY = 10
local MIN_GUI_MOUSE_SENSITIVITY = 0
local MAX_GUI_MOUSE_SENSITIVITY = 10
local GUI_MOUSE_SENSITIVITY_GRANULARITY = 0.01
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local sensitivityEvents = {
UserGameSettings:GetPropertyChangedSignal("GamepadCameraSensitivity"),
UserGameSettings:GetPropertyChangedSignal("MouseSensitivity"),
UserGameSettings:GetPropertyChangedSignal("MouseSensitivityFirstPerson"),
UserGameSettings:GetPropertyChangedSignal("MouseSensitivityThirdPerson"),
}
local InGameMenu = script.Parent.Parent.Parent
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local SliderEntry = require(script.Parent.SliderEntry)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local FFlagCameraSensitivityAllowGranularKeyboardInput = game:DefineFastFlag("CameraSensitivityAllowGranularKeyboardInput", false)
-- remove with FFlagCameraSensitivityAllowGranularKeyboardInput
local function guiToEngineSensitivity_DEPRECATED(guiSensitivity)
local engineSensitivity
if guiSensitivity <= 5 then
engineSensitivity = 0.16 * guiSensitivity + 0.2
else
engineSensitivity = 0.6 * guiSensitivity - 2
end
return math.clamp(
engineSensitivity,
MIN_ENGINE_MOUSE_SENSITIVITY,
MAX_ENGINE_MOUSE_SENSITIVITY
)
end
local function guiToEngineSensitivity(guiSensitivity)
local engineSensitivity
if guiSensitivity <= 5 then
engineSensitivity = 0.198 * guiSensitivity + 0.01
else
engineSensitivity = 0.6 * guiSensitivity - 2
end
return math.clamp(
engineSensitivity,
MIN_ENGINE_MOUSE_SENSITIVITY,
MAX_ENGINE_MOUSE_SENSITIVITY
)
end
-- remove with FFlagCameraSensitivityAllowGranularKeyboardInput
local function engineToGuiSensitivity_DEPRECATED(engineSensitivity)
local guiSensitivity
if engineSensitivity <= 1 then
guiSensitivity = math.floor((engineSensitivity - 0.2) / 0.16 + 0.5)
else
guiSensitivity = math.floor((engineSensitivity + 2) / 0.6 + 0.5)
end
return math.clamp(
guiSensitivity,
MIN_GUI_MOUSE_SENSITIVITY,
MAX_GUI_MOUSE_SENSITIVITY
)
end
local function engineToGuiSensitivity(engineSensitivity)
local guiSensitivity
if engineSensitivity <= 1 then
guiSensitivity = (engineSensitivity - 0.01) / 0.198
else
guiSensitivity = (engineSensitivity + 2) / 0.6
end
guiSensitivity = math.floor(guiSensitivity / GUI_MOUSE_SENSITIVITY_GRANULARITY + 0.5) * GUI_MOUSE_SENSITIVITY_GRANULARITY
return math.clamp(
guiSensitivity,
MIN_GUI_MOUSE_SENSITIVITY,
MAX_GUI_MOUSE_SENSITIVITY
)
end
local CameraSensitivityEntry = Roact.PureComponent:extend("CameraSensitivityEntry")
CameraSensitivityEntry.validateProps = t.strictInterface({
LayoutOrder = t.integer,
})
function CameraSensitivityEntry:init()
self.onSensitivityChanged = function()
self:setState({
-- TODO: Implement gamepad sensitivity.
engineSensitivity = UserGameSettings.MouseSensitivityFirstPerson.X,
})
end
self.onSensitivityChanged()
end
function CameraSensitivityEntry:render()
local children = {
-- TODO: Switch to gamepad sensitivity when appropriate
CameraSensitivityMouseSlider = Roact.createElement(SliderEntry, {
LayoutOrder = self.props.LayoutOrder,
labelKey = "CoreScripts.InGameMenu.GameSettings.CameraSensitivity",
value = FFlagCameraSensitivityAllowGranularKeyboardInput
and engineToGuiSensitivity(self.state.engineSensitivity)
or engineToGuiSensitivity_DEPRECATED(self.state.engineSensitivity),
min = MIN_GUI_MOUSE_SENSITIVITY,
max = MAX_GUI_MOUSE_SENSITIVITY,
stepInterval = 1,
keyboardInputStepInterval = FFlagCameraSensitivityAllowGranularKeyboardInput and GUI_MOUSE_SENSITIVITY_GRANULARITY or nil,
valueChanged = function(value)
local newEngineSensitivity = FFlagCameraSensitivityAllowGranularKeyboardInput
and guiToEngineSensitivity(value)
or guiToEngineSensitivity_DEPRECATED(value)
local engineSensitivityVector = Vector2.new(newEngineSensitivity, newEngineSensitivity)
UserGameSettings.MouseSensitivityFirstPerson = engineSensitivityVector
UserGameSettings.MouseSensitivityThirdPerson = engineSensitivityVector
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
})
}
for _, event in ipairs(sensitivityEvents) do
children[tostring(event) .. "CameraSensitivityControlConnection"] = Roact.createElement(ExternalEventConnection, {
event = event,
callback = self.onSensitivityChanged,
})
end
return Roact.createFragment(children)
end
return CameraSensitivityEntry
@@ -0,0 +1,46 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(script.Parent.Parent.ThemedTextLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local validateProps = t.strictInterface({
LayoutOrder = t.integer,
localizationKey = t.string,
})
return function(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = props.LayoutOrder,
Size = UDim2.new(1, 0, 0, 36),
}, {
Label = withLocalization({
text = props.localizationKey
})(function(localization)
return Roact.createElement(ThemedTextLabel, {
fontKey = "Footer",
themeKey = "TextEmphasis",
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(1, -48, 0, 14),
Text = localization.text,
TextTruncate = Enum.TextTruncate.AtEnd,
TextXAlignment = Enum.TextXAlignment.Left,
})
end)
})
end
@@ -0,0 +1,40 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local validateProps = t.strictInterface({
Position = t.UDim2,
Size = t.UDim2,
AnchorPoint = t.Vector2,
})
return function(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withLocalization({
lockedText = "CoreScripts.InGameMenu.GameSettings.LockedByDeveloper",
})(function(localized)
return Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "CaptionBody",
TextTransparency = 0.5,
Text = localized.lockedText,
Size = UDim2.new(1, -72, 0.25, 0),
Position = UDim2.new(0, 0, 1, 0),
AnchorPoint = Vector2.new(0, 1),
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
})
end)
end
@@ -0,0 +1,229 @@
--[[
Encompasses both the automatic graphics quality toggle _and_ the graphics
quality slider.
]]
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local RenderSettings = settings().Rendering
local SavedQualityLevelChanged = UserGameSettings:GetPropertyChangedSignal("SavedQualityLevel") -- <- DEPRECATED: remove with FixGraphicsQuality
local InGameMenu = script.Parent.Parent.Parent
local ToggleEntry = require(script.Parent.ToggleEntry)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local SliderEntry = require(script.Parent.SliderEntry)
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local GetFixGraphicsQuality = require(RobloxGui.Modules.Flags.GetFixGraphicsQuality)
local SendNotification
local RobloxTranslator
local GraphicsQualityLevelChanged
if GetFixGraphicsQuality() then
RobloxTranslator = require(RobloxGui:WaitForChild("Modules"):WaitForChild("RobloxTranslator"))
GraphicsQualityLevelChanged = UserGameSettings:GetPropertyChangedSignal("GraphicsQualityLevel")
end
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local SAVED_QUALITY_LEVELS = #Enum.SavedQualitySetting:GetEnumItems() - 1 -- <- DEPRECATED: remove with FixGraphicsQuality
local GRAPHICS_QUALITY_LEVELS = 10
if GetFixGraphicsQuality() then
GRAPHICS_QUALITY_LEVELS = RenderSettings:GetMaxQualityLevel() - 1
-- Don't be fooled by the word "max". It's not the maximum level, it's a strict upper bound
-- so if GetMaxQualityLevel() returns 22, that means the biggest the level can be is 21
end
local function mapInteger(x, xMin, xMax, yMin, yMax) -- <- DEPRECATED: remove with FixGraphicsQuality
return math.clamp(
math.floor(yMin + (x - xMin)*(yMax - yMin)/(xMax - xMin)),
yMin,
yMax)
end
--[[
Generates the popup in the lower-right corner when the user changes the
graphics quality level with F-keys.
]]
local function sendNotificationForGraphicsQualityLevelChange(message, level)
if SendNotification == nil then
SendNotification = RobloxGui:WaitForChild("SendNotificationInfo")
end
SendNotification:Fire({
GroupName = "Graphics",
Title = "Graphics Quality",
Text = message,
DetailText = message,
Image = "",
Duration = 2
})
end
--[[
Takes two arguments: newValue, delta. If delta is non-zero, constructs a message for the pop-up
notification (based on whether delta is positive or negative) to inform the user that the level
incrememnted / decremented to the new value.
]]
local function notifyForLevelChange(newValue, delta)
if delta > 0 then
sendNotificationForGraphicsQualityLevelChange(
RobloxTranslator:FormatByKey("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Increased", {RBX_NUMBER = tostring(newValue)}),
newValue)
elseif delta < 0 then
sendNotificationForGraphicsQualityLevelChange(
RobloxTranslator:FormatByKey("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Decreased", {RBX_NUMBER = tostring(newValue)}),
newValue)
end
end
local GraphicsQualityEntry = Roact.PureComponent:extend("GraphicsQualityEntry")
GraphicsQualityEntry.validateProps = t.strictInterface({
LayoutOrder = t.integer,
})
function GraphicsQualityEntry:init()
local isAutomatic
if GetFixGraphicsQuality() then
local quality = UserGameSettings.GraphicsQualityLevel
isAutomatic = quality == 0
self:setState({
quality = isAutomatic and 5 or UserGameSettings.GraphicsQualityLevel,
automatic = isAutomatic,
})
self.onQualityChanged = function()
local quality = UserGameSettings.GraphicsQualityLevel
self:setState({
quality = quality ~= 0 and quality or nil,
automatic = quality == 0,
})
end
else
isAutomatic = UserGameSettings.SavedQualityLevel == Enum.SavedQualitySetting.Automatic
self:setState({
quality = isAutomatic and 5 or UserGameSettings.SavedQualityLevel.Value,
automatic = isAutomatic,
})
self.onQualityChanged = function()
local newSavedQuality = UserGameSettings.SavedQualityLevel
local changedToAutomatic = newSavedQuality == Enum.SavedQualitySetting.Automatic
self:setState({
quality = not changedToAutomatic and newSavedQuality.Value or nil,
automatic = newSavedQuality == Enum.SavedQualitySetting.Automatic,
})
end
end
if isAutomatic then
self:setAutomaticQualityLevel()
else
self:setManualQualityLevel(self.state.quality)
end
if GetFixGraphicsQuality() then
--[[
Gets called when the user hits F10 / Shift-F10 to adjust the graphcs quality level.
Calls setManualQualityLevel to set the new level. Note: setManualQualityLevel checks whether the new value
it's given is in bounds. The returned newValue and delta indicate the change that actually takes place
We use newValue and delta to determine what notification to show.
]]
game.GraphicsQualityChangeRequest:connect(
function(isIncrease)
local current = UserGameSettings.GraphicsQualityLevel
if current ~= 0 then
local newValue, delta = self:setManualQualityLevel(current + (isIncrease and 1 or -1))
notifyForLevelChange(newValue, delta)
end
end
)
end
end
function GraphicsQualityEntry:setAutomaticQualityLevel()
if GetFixGraphicsQuality() then
UserGameSettings.GraphicsQualityLevel = 0
RenderSettings.QualityLevel = 0
else
UserGameSettings.SavedQualityLevel = Enum.SavedQualitySetting.Automatic
RenderSettings.QualityLevel = 0
end
end
function GraphicsQualityEntry:setManualQualityLevel(manualQualityLevel)
if GetFixGraphicsQuality() then
local newValue = math.clamp(manualQualityLevel, 1, GRAPHICS_QUALITY_LEVELS)
local oldValue = UserGameSettings.GraphicsQualityLevel
UserGameSettings.GraphicsQualityLevel = newValue
RenderSettings.QualityLevel = newValue
return newValue, newValue - oldValue
else
local renderQualityLevel = mapInteger(
manualQualityLevel,
1, SAVED_QUALITY_LEVELS,
-- Quality levels are zero-based; GetMaxQualityLevel reports 22, even
-- though 21 is the maximum.
1, RenderSettings:GetMaxQualityLevel() - 1)
UserGameSettings.SavedQualityLevel = manualQualityLevel
RenderSettings.QualityLevel = renderQualityLevel
return nil
end
end
function GraphicsQualityEntry:render()
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 56 + 100),
LayoutOrder = self.props.LayoutOrder,
}, {
Layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
}),
AutoToggle = Roact.createElement(ToggleEntry, {
LayoutOrder = 1,
labelKey = "CoreScripts.InGameMenu.GameSettings.AutoGraphicsQuality",
checked = self.state.automatic,
onToggled = function()
if self.state.automatic then
self:setManualQualityLevel(self.state.quality)
else
self:setAutomaticQualityLevel()
end
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
Slider = Roact.createElement(SliderEntry, {
LayoutOrder = 2,
labelKey = "CoreScripts.InGameMenu.GameSettings.ManualGraphicsQuality",
min = 1,
max = GRAPHICS_QUALITY_LEVELS,
stepInterval = 1,
value = self.state.quality,
disabled = self.state.automatic,
valueChanged = function(newValue)
self:setManualQualityLevel(newValue)
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
QualityListener = Roact.createElement(ExternalEventConnection, {
event = GetFixGraphicsQuality() and GraphicsQualityLevelChanged or SavedQualityLevelChanged,
callback = self.onQualityChanged,
})
})
end
return GraphicsQualityEntry
@@ -0,0 +1,104 @@
return function()
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local GetFixGraphicsQuality = require(RobloxGui.Modules.Flags.GetFixGraphicsQuality)
local SendNotificationInfo = Instance.new("BindableEvent")
SendNotificationInfo.Name = "SendNotificationInfo"
SendNotificationInfo.Parent = RobloxGui
local GraphicsQualityEntry = require(script.Parent.GraphicsQualityEntry)
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local RenderSettings = settings().Rendering
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GraphicsQualityEntry = Roact.createElement(GraphicsQualityEntry, {
LayoutOrder = 2,
}),
}),
}),
})
local instance = Roact.mount(element)
UserGameSettings.SavedQualityLevel = 10
RenderSettings.QualityLevel = 5
Roact.unmount(instance)
end)
it("should set the rendering setting to the saved quality level when initalized", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
GraphicsQualityEntry = Roact.createElement(GraphicsQualityEntry, {
LayoutOrder = 2,
}),
}),
}),
})
if GetFixGraphicsQuality() then
UserGameSettings.GraphicsQualityLevel = 11
local instance = Roact.mount(element)
expect(RenderSettings.QualityLevel).to.equal(Enum.QualityLevel.Level11)
Roact.unmount(instance)
UserGameSettings.GraphicsQualityLevel = 21
instance = Roact.mount(element)
expect(RenderSettings.QualityLevel).to.equal(Enum.QualityLevel.Level21)
Roact.unmount(instance)
else
UserGameSettings.SavedQualityLevel = Enum.SavedQualitySetting.QualityLevel2
local instance = Roact.mount(element)
expect(RenderSettings.QualityLevel).to.equal(Enum.QualityLevel.Level03)
Roact.unmount(instance)
UserGameSettings.SavedQualityLevel = Enum.SavedQualitySetting.QualityLevel10
instance = Roact.mount(element)
expect(RenderSettings.QualityLevel).to.equal(Enum.QualityLevel.Level21)
Roact.unmount(instance)
end
end)
end
@@ -0,0 +1,42 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local validateProps = t.strictInterface({
AnchorPoint = t.optional(t.Vector2),
Position = t.optional(t.UDim2),
Size = t.optional(t.UDim2),
localizationKey = t.string,
})
return function(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withLocalization({
text = props.localizationKey,
})(function(localized)
return Roact.createElement(ThemedTextLabel, {
fontKey = "Body",
themeKey = "TextDefault",
AnchorPoint = props.AnchorPoint or Vector2.new(0, 0.5),
Position = props.Position or UDim2.new(0, 0, 0.5, 0),
Size = props.Size or UDim2.new(1, -72, 0.5, 0),
Text = localized.text,
TextTruncate = Enum.TextTruncate.AtEnd,
TextXAlignment = Enum.TextXAlignment.Left,
})
end)
end
@@ -0,0 +1,144 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local ComputerMovementModeChanged = UserGameSettings:GetPropertyChangedSignal("ComputerMovementMode")
local localPlayer = Players.LocalPlayer
local playerScripts = localPlayer:WaitForChild("PlayerScripts")
local DevComputerMovementModeChanged = localPlayer:GetPropertyChangedSignal("DevComputerMovementMode")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local DropDownSelection = require(InGameMenu.Components.DropDownSelection)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local InputLabel = require(script.Parent.InputLabel)
local DeveloperLockLabel = require(script.Parent.DeveloperLockLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
local MOVEMENT_MODE_LOCALIZATION_KEYS = {
[Enum.ComputerMovementMode.KeyboardMouse] = "CoreScripts.InGameMenu.GameSettings.ComputerMoveModeKeyboardMouse",
[Enum.ComputerMovementMode.ClickToMove] = "CoreScripts.InGameMenu.GameSettings.ComputerMoveModeClickToMove",
[Enum.ComputerMovementMode.Default] = "CoreScripts.InGameMenu.GameSettings.Default",
}
local DEV_MOVEMENT_MODE_LOCALIZATION_KEYS = {
[Enum.DevComputerMovementMode.KeyboardMouse] = "CoreScripts.InGameMenu.GameSettings.ComputerMoveModeKeyboardMouse",
[Enum.DevComputerMovementMode.ClickToMove] = "CoreScripts.InGameMenu.GameSettings.ComputerMoveModeClickToMove",
}
local MovementModeEntry = Roact.PureComponent:extend("MovementModeEntry")
MovementModeEntry.validateProps = t.strictInterface({
LayoutOrder = t.integer,
})
function MovementModeEntry:init()
self:setState({
selectedComputerMode = UserGameSettings.ComputerMovementMode,
computerOptions = playerScripts:GetRegisteredComputerMovementModes(),
developerComputerMode = localPlayer.DevComputerMovementMode,
})
end
function MovementModeEntry:render()
-- TODO: Support gamepad and touch controls.
local result = {
ComputerMovementModeListener = Roact.createElement(ExternalEventConnection, {
event = ComputerMovementModeChanged,
callback = function()
self:setState({
selectedComputerMode = UserGameSettings.ComputerMovementMode,
})
end,
}),
ComputerMovementModeRegisteredListener = Roact.createElement(ExternalEventConnection, {
event = playerScripts.ComputerMovementModeRegistered,
callback = function()
self:setState({
computerOptions = playerScripts:GetRegisteredComputerMovementModes(),
})
end,
}),
DevComputerMovementMode = Roact.createElement(ExternalEventConnection, {
event = DevComputerMovementModeChanged,
callback = function()
self:setState({
developerComputerMode = localPlayer.DevComputerMovementMode,
})
end,
}),
}
if #self.state.computerOptions == 0 then
return Roact.createFragment(result)
end
local disabled = self.state.developerComputerMode ~= Enum.DevComputerMovementMode.UserChoice
local optionTexts = {}
local selectedIndex = 0
if not disabled then
for index, enum in ipairs(self.state.computerOptions) do
optionTexts[index] = MOVEMENT_MODE_LOCALIZATION_KEYS[enum]
assert(optionTexts[index] ~= nil, "Movement mode " .. enum.Name .. " has no localization key")
if self.state.selectedComputerMode.Value == enum.Value then
selectedIndex = index
end
end
end
result.MovementModeEntrySelector = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 44 + 56 + 20),
BackgroundTransparency = 1,
LayoutOrder = self.props.LayoutOrder,
ZIndex = 2,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
}),
InputLabel = Roact.createElement(InputLabel, {
localizationKey = "CoreScripts.InGameMenu.GameSettings.MovementMode",
Size = UDim2.new(1, 0, 0, 56),
Position = UDim2.new(0, 0, 0, 0),
AnchorPoint = Vector2.new(0, 0),
}),
Dropdown = withLocalization({
placeholder = disabled and DEV_MOVEMENT_MODE_LOCALIZATION_KEYS[self.state.developerComputerMode] or nil
})(function(localized)
return Roact.createElement(DropDownSelection, {
Size = UDim2.new(1, 0, 0, 44),
Position = UDim2.new(0, 0, 0, 56),
selections = optionTexts,
selectedIndex = selectedIndex,
placeHolderText = localized.placeholder,
enabled = not disabled,
localize = true,
selectionChanged = function(newSelection)
UserGameSettings.ComputerMovementMode = self.state.computerOptions[newSelection]
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
})
end),
LockedLabel = disabled and Roact.createElement(DeveloperLockLabel, {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 0, 1, 0),
Size = UDim2.new(1, -72, 0.25, 0),
}),
})
return Roact.createFragment(result)
end
return MovementModeEntry
@@ -0,0 +1,54 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local MovementModeEntry = require(script.Parent.MovementModeEntry)
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local localPlayer = Players.LocalPlayer
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
MovementModeEntry = Roact.createElement(MovementModeEntry, {
LayoutOrder = 2,
}),
}),
}),
})
local instance = Roact.mount(element)
UserGameSettings.ComputerMovementMode = Enum.ComputerMovementMode.ClickToMove
localPlayer.DevComputerMovementMode = Enum.DevComputerMovementMode.ClickToMove
Roact.unmount(instance)
end)
end
@@ -0,0 +1,61 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local InputLabel = require(script.Parent.InputLabel)
local SliderWithInput = require(InGameMenu.Components.SliderWithInput)
local validateProps = t.strictInterface({
LayoutOrder = t.integer,
labelKey = t.string,
min = t.number,
max = t.number,
stepInterval = t.number,
keyboardInputStepInterval = t.optional(t.numberPositive),
value = t.number,
disabled = t.optional(t.boolean),
valueChanged = t.callback,
})
local function SliderEntry(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 92),
LayoutOrder = props.LayoutOrder,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, 24),
PaddingTop = UDim.new(0, 12),
PaddingRight = UDim.new(0, 30),
PaddingBottom = UDim.new(0, 24),
}),
Label = Roact.createElement(InputLabel, {
Size = UDim2.new(1, 0, 0, 24),
localizationKey = props.labelKey,
Position = UDim2.new(0, 0, 0, 0),
AnchorPoint = Vector2.new(0, 0),
}),
Slider = Roact.createElement(SliderWithInput, {
min = props.min,
max = props.max,
stepInterval = props.stepInterval,
keyboardInputStepInterval = props.keyboardInputStepInterval,
value = props.value,
Position = UDim2.new(0, 0, 0, 32),
disabled = props.disabled,
valueChanged = props.valueChanged,
}),
})
end
return SliderEntry
@@ -0,0 +1,55 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local SliderEntry = require(script.Parent.SliderEntry)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
SliderEntry = Roact.createElement(SliderEntry, {
LayoutOrder = 2,
labelKey = "CoreScripts.InGameMenu.GameSettings.CameraSensitivity",
min = 1,
max = 10,
stepInterval = 1,
value = 5,
disabled = false,
valueChanged = function()
print("Value changed")
end,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,75 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ToggleSwitch = require(InGameMenu.Components.ToggleSwitch)
local InputLabel = require(script.Parent.InputLabel)
local DeveloperLockLabel = require(script.Parent.DeveloperLockLabel)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local validateProps = t.strictInterface({
LayoutOrder = t.integer,
labelKey = t.string,
lockedToOff = t.optional(t.boolean),
subtextEnabled = t.optional(t.boolean),
subtextKey = t.optional(t.string),
checked = t.boolean,
onToggled = t.callback,
})
local function ToggleEntry(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = props.LayoutOrder,
Size = UDim2.new(1, 0, 0, 56),
}, {
Padding = Roact.createElement("UIPadding", {
PaddingRight = UDim.new(0, 30),
PaddingLeft = UDim.new(0, 24),
}),
ControlLabel = Roact.createElement(InputLabel, {
localizationKey = props.labelKey,
}),
LockedLabel = props.lockedToOff and Roact.createElement(DeveloperLockLabel, {
Size = UDim2.new(1, -72, 0.25, 0),
Position = UDim2.new(0, 0, 1, 0),
AnchorPoint = Vector2.new(0, 1),
}),
SubtextLabel = props.subtextEnabled and withLocalization({
text = props.subtextKey,
})(function(localization)
return Roact.createElement(ThemedTextLabel, {
Text = localization.text,
themeKey = "TextDefault",
fontKey = "CaptionBody",
TextTransparency = 0.5,
Size = UDim2.new(1, -72, 0.25, 0),
Position = UDim2.new(0, 0, 1, 0),
AnchorPoint = Vector2.new(0, 1),
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
})
end),
Toggle = Roact.createElement(ToggleSwitch, {
Position = UDim2.new(1, 0, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
checked = props.checked and not props.lockedToOff,
onToggled = props.onToggled,
disabled = props.lockedToOff,
}),
})
end
return ToggleEntry
@@ -0,0 +1,239 @@
local LATEST_PLACE_VERSION = "CoreScripts.InGameMenu.GameSettings.PlaceVersionWithLatest"
local PLACE_VERSION = "CoreScripts.InGameMenu.GameSettings.PlaceVersion"
local ContentProvider = game:GetService("ContentProvider")
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local RobloxReplicatedStorage = game:GetService("RobloxReplicatedStorage")
local RunService = game:GetService("RunService")
local StarterPlayer = game:GetService("StarterPlayer")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local PlayerPermissions = require(CoreGui.RobloxGui.Modules.PlayerPermissionsModule)
local InGameMenu = script.Parent.Parent.Parent
local Divider = require(InGameMenu.Components.Divider)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local canGetCoreScriptVersion = game:getEngineFeature("CoreScriptVersionEnabled")
local VersionReporter = Roact.PureComponent:extend("VersionReporter")
VersionReporter.validateProps = t.strictInterface({
LayoutOrder = t.integer,
})
local function inferPlayerScriptStatus(starterPlayerScripts)
local loader = starterPlayerScripts:FindFirstChild("PlayerScriptsLoader")
local playerModule = starterPlayerScripts:FindFirstChild("PlayerModule")
if loader ~= nil and playerModule ~= nil then
if not playerModule.Archivable and not loader.Archivable then
return "PlayerScript Status: Default"
else
return "PlayerScript Status: Possibly Custom"
end
end
local cameraScript = starterPlayerScripts:FindFirstChild("CameraScript")
local controlScript = starterPlayerScripts:FindFirstChild("ControlScript")
if cameraScript ~= nil or controlScript ~= nil then
return "PlayerScript Status: Custom Old"
else
return "PlayerScript Status: Custom"
end
end
function VersionReporter:init()
local initalPlayerscriptStatus = "PlayerScript Status: Unknown"
local starterPlayerScripts = StarterPlayer:FindFirstChild("StarterPlayerScripts")
if starterPlayerScripts then
initalPlayerscriptStatus = inferPlayerScriptStatus(starterPlayerScripts)
else
self.playerScriptsAddedConnection = StarterPlayer.ChildAdded:Connect(function(child)
if child.Name == "StarterPlayerScripts" then
self.playerScriptsAddedConnection:Disconnect()
self.playerScriptsAddedConnection = nil
self:setState({
playerScriptStatus = inferPlayerScriptStatus(child),
})
end
end)
end
self:setState({
baseUrl = ContentProvider.BaseUrl,
clientVersion = RunService:GetRobloxVersion(),
isAdmin = false,
latestPlaceVersion = nil,
placeVersion = game.PlaceVersion,
serverVersion = nil,
playerScriptStatus = initalPlayerscriptStatus,
coreScriptVersion = canGetCoreScriptVersion and RunService:GetCoreScriptVersion() or nil,
})
end
function VersionReporter:render()
local haveServerVersion = self.state.serverVersion ~= nil
local haveLatestPlaceVersion = self.state.latestPlaceVersion ~= nil
local isTestEnvironment = not self.state.baseUrl:find("www.roblox.com")
local showEnvironment = isTestEnvironment
local showPlayerScriptStatus = (isTestEnvironment or self.state.isAdmin)
return withLocalization({
clientVersion = {
"CoreScripts.InGameMenu.GameSettings.ClientVersion",
CLIENT_VERSION = self.state.clientVersion,
},
serverVersion = {
"CoreScripts.InGameMenu.GameSettings.ServerVersion",
SERVER_VERSION = self.state.serverVersion,
},
placeVersion = {
haveLatestPlaceVersion and LATEST_PLACE_VERSION or PLACE_VERSION,
PLACE_VERSION = self.state.placeVersion,
LATEST_PLACE_VERSION = self.state.latestPlaceVersion,
},
coreScriptVersion = canGetCoreScriptVersion and {
"CoreScripts.InGameMenu.GameSettings.ClientCoreScriptVersion",
CLIENT_CORESCRIPT_VERSION = self.state.coreScriptVersion,
} or nil
})(function(localized)
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 80),
LayoutOrder = self.props.LayoutOrder,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingTop = UDim.new(0, 14),
PaddingLeft = UDim.new(0, 24),
PaddingBottom = UDim.new(0, 14),
}),
Layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
}),
ClientVersion = Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = localized.clientVersion,
LayoutOrder = 1,
Size = UDim2.new(1, -24, 0, 14),
}),
ServerVersion = Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = haveServerVersion and localized.serverVersion or "",
LayoutOrder = 2,
Size = UDim2.new(1, -24, 0, 14),
}, {
not haveServerVersion and Roact.createElement(UIBlox.Loading.ShimmerPanel, {
Size = UDim2.new(1, 0, 1, 0),
shimmerSpeed = 2,
}),
}),
PlaceVersion = Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = localized.placeVersion,
LayoutOrder = 3,
Size = UDim2.new(1, -24, 0, 14),
}),
Divider = (showEnvironment or showPlayerScriptStatus) and Roact.createElement(Divider, {
Size = UDim2.new(1, 0, 0, 1),
LayoutOrder = 4,
}),
PlayerScriptStatus = showPlayerScriptStatus and Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = self.state.playerScriptStatus,
LayoutOrder = 5,
Size = UDim2.new(1, -24, 0, 14),
}),
Environment = showEnvironment and Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = ("Base URL: %s"):format(self.state.baseUrl),
LayoutOrder = 6,
Size = UDim2.new(1, -24, 0, 14),
}),
Divider2 = canGetCoreScriptVersion and Roact.createElement(Divider, {
Size = UDim2.new(1, 0, 0, 1),
LayoutOrder = 7,
}),
CoreScriptVersion = canGetCoreScriptVersion and Roact.createElement(ThemedTextLabel, {
themeKey = "TextDefault",
fontKey = "Footer",
Text = localized.coreScriptVersion,
LayoutOrder = 8,
Size = UDim2.new(1, -24, 0, 14),
}),
VersionWatcher = Roact.createElement(ExternalEventConnection, {
event = game:GetPropertyChangedSignal("PlaceVersion"),
callback = function()
self:setState({
placeVersion = game.PlaceVersion,
})
end,
})
})
end)
end
function VersionReporter:didMount()
self.mounted = true
coroutine.wrap(function()
local serverVersionRemote = RobloxReplicatedStorage:WaitForChild("GetServerVersion", math.huge)
local version = serverVersionRemote:InvokeServer()
if self.mounted then
self:setState({
serverVersion = version,
})
end
end)()
coroutine.wrap(function()
pcall(function()
local json = HttpRbxApiService:GetAsync(string.format("assets/%d/versions", game.PlaceId))
local versionData = HttpService:JSONDecode(json)
local latestVersion = versionData[1].VersionNumber
if self.mounted then
self:setState({
latestPlaceVersion = latestVersion,
})
end
end)
end)()
coroutine.wrap(function()
local isAdmin = PlayerPermissions.IsPlayerAdminAsync(Players.LocalPlayer)
if self.mounted then
self:setState({
isAdmin = isAdmin,
})
end
end)()
end
function VersionReporter:willUnmount()
self.mounted = false
if self.playerScriptsAddedConnection then
self.playerScriptsAddedConnection:Disconnect()
end
end
return VersionReporter
@@ -0,0 +1,67 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UserGameSettings = UserSettings():GetService("UserGameSettings")
local MasterVolumeChanged = UserGameSettings:GetPropertyChangedSignal("MasterVolume")
local InGameMenu = script.Parent.Parent.Parent
local ExternalEventConnection = require(InGameMenu.Utility.ExternalEventConnection)
local SliderEntry = require(script.Parent.SliderEntry)
local SendAnalytics = require(InGameMenu.Utility.SendAnalytics)
local Constants = require(InGameMenu.Resources.Constants)
if CoreGui.RobloxGui:FindFirstChild("Sounds") == nil then
local folder = Instance.new("Folder")
folder.Name = "Sounds"
folder.Parent = CoreGui.RobloxGui
end
local volumeTest = Instance.new("Sound", CoreGui.RobloxGui.Sounds)
volumeTest.Name = "VolumeChangeSound"
volumeTest.Volume = 1
volumeTest.SoundId = "rbxasset://sounds/uuhhh.mp3"
local VolumeEntry = Roact.PureComponent:extend("VolumeEntry")
VolumeEntry.validateProps = t.strictInterface({
LayoutOrder = t.integer,
})
function VolumeEntry:init()
self.onVolumeChanged = function()
self:setState({
volume = math.floor(UserGameSettings.MasterVolume * 10 + 0.5)
})
end
self.onVolumeChanged()
end
function VolumeEntry:render()
return Roact.createFragment({
VolumeSlider = Roact.createElement(SliderEntry, {
labelKey = "CoreScripts.InGameMenu.GameSettings.Volume",
LayoutOrder = self.props.LayoutOrder,
min = 0,
max = 10,
stepInterval = 1,
value = self.state.volume,
valueChanged = function(newValue)
UserGameSettings.MasterVolume = newValue / 10
volumeTest:Play()
SendAnalytics(Constants.AnalyticsSettingsChangeName, nil, {}, true)
end,
}),
VolumeListener = Roact.createElement(ExternalEventConnection, {
event = MasterVolumeChanged,
callback = self.onVolumeChanged,
}),
})
end
return VolumeEntry
@@ -0,0 +1,167 @@
local TextService = game:GetService("TextService")
local CorePackages = game:GetService("CorePackages")
local ContextActionService = game:GetService("ContextActionService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent
local Assets = require(InGameMenu.Resources.Assets)
local ThemedTextLabel = require(script.Parent.ThemedTextLabel)
local Divider = require(script.Parent.Divider)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local DIALOG_WIDTH = 312
local DIALOG_PADDING = 24
local MODAL_DISMISS_ACTION = "InGameMenuModalDismiss"
local InfoDialog = Roact.PureComponent:extend("InfoDialog")
InfoDialog.validateProps = t.strictInterface({
bodyText = t.string,
dismissText = t.string,
titleText = t.string,
iconImage = t.union(t.string, t.table),
onDismiss = t.callback,
visible = t.boolean,
})
function InfoDialog:render()
local props = self.props
return withStyle(function(style)
local bodyFont = style.Font.Body.Font
local bodyFontSize = style.Font.Body.RelativeSize * style.Font.BaseSize
local bodyText = props.bodyText
local textHeight = TextService:GetTextSize(
bodyText,
bodyFontSize,
bodyFont,
Vector2.new(DIALOG_WIDTH - DIALOG_PADDING * 2, math.huge)
).Y
-- 24px padding top, 24px padding bottom
-- Minimum height of two lines of text.
-- TODO: Line height!
local bodyTextContainerHeight = DIALOG_PADDING * 2 + math.max(textHeight, bodyFontSize * 2)
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
ZIndex = 8,
Visible = props.visible,
}, {
Overlay = Roact.createElement("TextButton", {
AutoButtonColor = false,
BackgroundColor3 = style.Theme.Overlay.Color,
BackgroundTransparency = style.Theme.Overlay.Transparency,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0),
Text = "",
}),
DialogMainFrame = Roact.createElement(ImageSetLabel, {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = Assets.Images.RoundedRect.Image,
ImageColor3 = style.Theme.BackgroundUIDefault.Color,
ImageTransparency = style.Theme.BackgroundUIDefault.Transparency,
Position = UDim2.new(0.5, 0, 0.5, 0),
ScaleType = Assets.Images.RoundedRect.ScaleType,
-- button (36px), bottom padding (24px), top padding (10px)
-- height: title bar (48px), divider (1px), body text height, icon(36px)
Size = UDim2.new(0, DIALOG_WIDTH, 0, 48 + 1 + bodyTextContainerHeight + 36 + 36 + DIALOG_PADDING + 10),
SliceCenter = Assets.Images.RoundedRect.SliceCenter,
}, {
Padding = Roact.createElement("UIPadding", {
PaddingTop = UDim.new(0, 10),
PaddingBottom = UDim.new(0, DIALOG_PADDING),
PaddingLeft = UDim.new(0, DIALOG_PADDING),
PaddingRight = UDim.new(0, DIALOG_PADDING),
}),
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Icon = Roact.createElement(ImageSetLabel, {
Size = UDim2.new(0, 36, 0, 36),
BackgroundTransparency = 1,
Image = self.props.iconImage,
LayoutOrder = 1,
}),
Title = Roact.createElement(ThemedTextLabel, {
fontKey = "Header1",
themeKey = "TextEmphasis",
LayoutOrder = 2,
Size = UDim2.new(1, 0, 0, 48),
Text = props.titleText,
}),
Divider = Roact.createElement(Divider, {
LayoutOrder = 3,
}),
BodyTextContainer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 4,
Size = UDim2.new(1, 0, 0, bodyTextContainerHeight),
}, {
BodyText = Roact.createElement(ThemedTextLabel, {
Size = UDim2.new(1, 0, 1, 0),
Text = props.bodyText,
TextWrapped = true,
})
}),
ConfirmButton = Roact.createElement(UIBlox.App.Button.PrimarySystemButton, {
layoutOrder = 5,
size = UDim2.new(1, 0, 0, 36),
onActivated = props.onDismiss,
text = props.dismissText,
}),
}),
})
end)
end
function InfoDialog:bindActions()
local function dismissFunc(actionName, inputState, input)
if inputState == Enum.UserInputState.Begin then
self.props.onDismiss()
end
end
ContextActionService:BindCoreAction(
MODAL_DISMISS_ACTION, dismissFunc, false, Enum.KeyCode.Escape)
end
function InfoDialog:unbindActions()
ContextActionService:UnbindCoreAction(MODAL_DISMISS_ACTION)
end
function InfoDialog:didMount()
if self.props.visible then
self:bindActions()
end
end
function InfoDialog:didUpdate()
if self.props.visible then
self:bindActions()
else
self:unbindActions()
end
end
function InfoDialog:willUnmount()
self:unbindActions()
end
return InfoDialog
@@ -0,0 +1,47 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local Rodux = InGameMenuDependencies.Rodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local InfoDialog = require(script.Parent.InfoDialog)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
GameIconHeader = Roact.createElement(InfoDialog, {
bodyText = "Hello world!",
dismissText = "Okay",
titleText = "Title",
iconImage = "",
onDismiss = function()
print("on dismiss")
end,
visible = true,
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,111 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Assets = require(InGameMenu.Resources.Assets)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local SetCurrentPage = require(InGameMenu.Actions.SetCurrentPage)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local CONTAINER_WIDTH = 304
local TEXT_PADDING_TOP = 4
local TEXT_PADDING_BOTTOM = 16
local validateProps = t.strictInterface({
switchToPlayers = t.callback,
})
local function AddFriendsNow(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withStyle(function(style)
return withLocalization({
noFriendsText = "CoreScripts.InGameMenu.InviteFriends.NoFriends",
makeFriendsNow = "CoreScripts.InGameMenu.InviteFriends.MakeFriendsNow",
})(function(localized)
local bodyFont = style.Font.Body.Font
local bodyFontSize = style.Font.Body.RelativeSize * style.Font.BaseSize
local textHeight = TextService:GetTextSize(
localized.noFriendsText,
bodyFontSize,
bodyFont,
Vector2.new(CONTAINER_WIDTH, math.huge)
).Y
local totalTextPadding = TEXT_PADDING_TOP + TEXT_PADDING_BOTTOM
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, CONTAINER_WIDTH, 1, 0),
Position = UDim2.new(0.5, 0, 0, 0),
AnchorPoint = Vector2.new(0.5, 0),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Icon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.AddFriend,
Size = UDim2.new(0, 64, 0, 64),
ImageTransparency = 0.5,
LayoutOrder = 1,
}),
TextContainer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, textHeight + totalTextPadding),
LayoutOrder = 2,
}, {
Text = Roact.createElement(ThemedTextLabel, {
fontKey = "Body",
themeKey = "TextEmphasis",
Text = localized.noFriendsText,
Position = UDim2.new(0, 0, 0, TEXT_PADDING_TOP),
Size = UDim2.new(1, 0, 1, -totalTextPadding),
TextXAlignment = Enum.TextXAlignment.Center,
TextWrapped = true,
})
}),
MakeFriendsButton = Roact.createElement(UIBlox.App.Button.SecondaryButton, {
layoutOrder = 3,
size = UDim2.new(1, 0, 0, 48),
text = localized.makeFriendsNow,
onActivated = props.switchToPlayers,
}),
})
end)
end)
end
return RoactRodux.UNSTABLE_connect2(
nil,
function(dispatch)
return {
switchToPlayers = function()
dispatch(SetCurrentPage("Players"))
end
}
end
)(AddFriendsNow)
@@ -0,0 +1,44 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local AddFriendsNow = require(script.Parent.AddFriendsNow)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
AddFriendsNow = Roact.createElement(AddFriendsNow),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,191 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local RoactRodux = InGameMenuDependencies.RoactRodux
local t = InGameMenuDependencies.t
local Cryo = InGameMenuDependencies.Cryo
local InGameMenu = script.Parent.Parent.Parent
local FFlagInGameMenuInviteFriendsDisplayNameSearch =
require(InGameMenu.Flags.FFlagInGameMenuInviteFriendsDisplayNameSearch)
local getFFlagUseNewPlayerLabelDesign = require(InGameMenu.Flags.GetFFlagUseNewPlayerLabelDesign)
local fflagUseNewPlayerLabelDesign = getFFlagUseNewPlayerLabelDesign()
local PlayerLabel = fflagUseNewPlayerLabelDesign and require(InGameMenu.Components.PlayerLabelV2)
or require(InGameMenu.Components.PlayerLabel)
local Divider = require(InGameMenu.Components.Divider)
local SendInviteButton = require(script.Parent.SendInviteButton)
local SearchBox = require(script.Parent.SearchBox)
local BarOnTopScrollingFrame = require(InGameMenu.Components.BarOnTopScrollingFrame)
local PlayerSearchPredicate = require(InGameMenu.Utility.PlayerSearchPredicate)
local Constants = require(InGameMenu.Resources.Constants)
local InviteStatus = Constants.InviteStatus
local InviteUserToPlaceId = require(InGameMenu.Thunks.InviteUserToPlaceId)
local DIVIDER_INDENT = fflagUseNewPlayerLabelDesign and 104 or 70
local PLAYER_LABEL_HEIGHT = fflagUseNewPlayerLabelDesign and 71 or 70
local SEARCHBOX_HEIGHT = 36
local SEARCHBOX_TOP_PADDING = 8
local SEARCHBOX_BOTTOM_PADDING = 20
local PLACE_ID = tostring(game.PlaceId)
local InviteFriendsList = Roact.PureComponent:extend("InviteFriendsList")
InviteFriendsList.validateProps = t.strictInterface({
players = t.array(t.strictInterface({
IsOnline = t.boolean,
Id = t.integer,
Username = t.string,
DisplayName = t.string,
})),
invitesState = t.table,
dispatchInviteUserToPlaceId = t.callback,
})
local function sortPlayers(p1, p2)
if p1.IsOnline == p2.IsOnline then
return p1.Username:lower() < p2.Username:lower()
else
return p1.IsOnline and not p2.IsOnline
end
end
local function searchFilter(searchText, playerName, displayName)
if searchText == "" then
return true
end
return string.find(playerName:lower(), searchText:lower(), 1, true) ~= nil
end
function InviteFriendsList:init()
self.state = {
searchText = "",
}
self.inviteAnimationInProgress = {}
end
function InviteFriendsList:renderListEntries()
local sortedPlayers = Cryo.List.sort(self.props.players, sortPlayers)
local playersCount = #sortedPlayers
local layoutOrder = 1
local listComponents = {}
local visibleEntryCount = 0
listComponents.ListLayout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
})
for index, playerInfo in pairs(sortedPlayers) do
local userId = tostring(playerInfo.Id)
local userInviteStatus = self.props.invitesState[userId]
local isEntryVisible
if FFlagInGameMenuInviteFriendsDisplayNameSearch then
isEntryVisible = PlayerSearchPredicate(self.state.searchText, playerInfo.Username, playerInfo.DisplayName)
else
isEntryVisible = searchFilter(self.state.searchText, playerInfo.Username, playerInfo.DisplayName)
end
if isEntryVisible then
visibleEntryCount = visibleEntryCount + 1
end
listComponents["friend_"..index] = Roact.createElement(PlayerLabel, {
username = playerInfo.Username,
displayName = fflagUseNewPlayerLabelDesign and playerInfo.DisplayName or nil,
userId = playerInfo.Id,
isOnline = playerInfo.IsOnline,
isSelected = false,
LayoutOrder = layoutOrder,
Visible = isEntryVisible,
onActivated = function()
local isPending = userInviteStatus == InviteStatus.Pending
local isSuccess = userInviteStatus == InviteStatus.Success
local inviteAnimationInProgress = self.inviteAnimationInProgress[playerInfo.Id]
if not (isPending or isSuccess or inviteAnimationInProgress) then
self.props.dispatchInviteUserToPlaceId(userId, PLACE_ID)
end
end
}, {
InviteButton = Roact.createElement(SendInviteButton, {
userInviteStatus = userInviteStatus,
onActivated = function()
self.props.dispatchInviteUserToPlaceId(userId, PLACE_ID)
end,
animationBegun = function()
self.inviteAnimationInProgress[playerInfo.Id] = true
end,
animationEnded = function()
self.inviteAnimationInProgress[playerInfo.Id] = nil
end,
})
})
layoutOrder = layoutOrder + 1
if index < playersCount then
listComponents["divider_" .. layoutOrder] = Roact.createElement(Divider, {
LayoutOrder = layoutOrder,
Size = UDim2.new(1, -DIVIDER_INDENT, 0, 1),
Visible = isEntryVisible,
})
layoutOrder = layoutOrder + 1
end
end
return listComponents, visibleEntryCount
end
function InviteFriendsList:render()
local listEntries, visibleEntryCount = self:renderListEntries()
local listOffset = SEARCHBOX_HEIGHT + SEARCHBOX_TOP_PADDING + SEARCHBOX_BOTTOM_PADDING
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
}, {
SearchBox = Roact.createElement(SearchBox, {
Size = UDim2.new(1, -48, 0, SEARCHBOX_HEIGHT),
Position = UDim2.new(0.5, 0, 0, SEARCHBOX_TOP_PADDING),
AnchorPoint = Vector2.new(0.5, 0),
searchUpdated = function(newSearch)
self:setState({
searchText = newSearch,
})
end,
}),
List = Roact.createElement(BarOnTopScrollingFrame, {
Position = UDim2.new(0, 0, 0, listOffset),
Size = UDim2.new(1, 0, 1, -listOffset),
CanvasSize = UDim2.new(1, 0, 0, visibleEntryCount * (PLAYER_LABEL_HEIGHT + 1)),
scrollBarOffset = 4,
}, listEntries)
})
end
return RoactRodux.UNSTABLE_connect2(
function(state, props)
return {
invitesState = state.invites
}
end,
function(dispatch)
return {
dispatchInviteUserToPlaceId = function(userId, placeId)
dispatch(InviteUserToPlaceId(userId, placeId))
end
}
end
)(InviteFriendsList)
@@ -0,0 +1,103 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local ReceivedUserInviteStatus = require(InGameMenu.Actions.ReceivedUserInviteStatus)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local Constants = require(InGameMenu.Resources.Constants)
local InviteStatus = Constants.InviteStatus
local InviteFriendsList = require(script.Parent.InviteFriendsList)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
InviteFriendsList = Roact.createElement(InviteFriendsList, {
players = {
{
Id = 2231221,
IsOnline = true,
Username = "TheGamer101",
DisplayName = "TestDisplayName",
},
{
Id = 261,
IsOnline = false,
Username = "Shedletsky",
DisplayName = "TestDisplayName",
},
}
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should not throw erros when invite state is updated", function()
local store = Rodux.Store.new(reducer)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
InviteFriendsList = Roact.createElement(InviteFriendsList, {
players = {
{
Id = 2231221,
IsOnline = true,
Username = "TheGamer101",
DisplayName = "TestDisplayName",
},
{
Id = 261,
IsOnline = false,
Username = "Shedletsky",
DisplayName = "TestDisplayName",
},
}
}),
}),
}),
})
local instance = Roact.mount(element)
store:dispatch(ReceivedUserInviteStatus(261, InviteStatus.Failed))
store:flush()
Roact.unmount(instance)
end)
end
@@ -0,0 +1,98 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent.Parent
local GlobalConfig = require(InGameMenu.GlobalConfig)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Assets = require(InGameMenu.Resources.Assets)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local CONTAINER_WIDTH = 304
local TEXT_PADDING_TOP = 10
local TEXT_PADDING_BOTTOM = 26
local validateProps = t.strictInterface({
onRetry = t.callback,
})
local function LoadingFriendsError(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withStyle(function(style)
return withLocalization({
errorOccured = "CoreScripts.InGameMenu.InviteFriends.ErrorOccured",
})(function(localized)
local bodyFont = style.Font.Body.Font
local bodyFontSize = style.Font.Body.RelativeSize * style.Font.BaseSize
local textHeight = TextService:GetTextSize(
localized.errorOccured,
bodyFontSize,
bodyFont,
Vector2.new(CONTAINER_WIDTH, math.huge)
).Y
local totalTextPadding = TEXT_PADDING_TOP + TEXT_PADDING_BOTTOM
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, CONTAINER_WIDTH, 1, 0),
Position = UDim2.new(0.5, 0, 0, 0),
AnchorPoint = Vector2.new(0.5, 0),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Icon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = Assets.Images.Friends,
Size = UDim2.new(0, 64, 0, 64),
ImageTransparency = 0.5,
LayoutOrder = 1,
}),
TextContainer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, textHeight + totalTextPadding),
LayoutOrder = 2,
}, {
Text = Roact.createElement(ThemedTextLabel, {
fontKey = "Body",
themeKey = "TextEmphasis",
Text = localized.errorOccured,
Position = UDim2.new(0, 0, 0, TEXT_PADDING_TOP),
Size = UDim2.new(1, 0, 1, -totalTextPadding),
TextXAlignment = Enum.TextXAlignment.Center,
TextWrapped = true,
})
}),
MakeFriendsButton = Roact.createElement(UIBlox.App.Button.SecondaryButton, {
layoutOrder = 3,
size = UDim2.new(1, 0, 0, 48),
icon = Assets.Images.RetryIcon,
onActivated = props.onRetry,
}),
})
end)
end)
end
return LoadingFriendsError
@@ -0,0 +1,48 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local Rodux = InGameMenuDependencies.Rodux
local RoactRodux = InGameMenuDependencies.RoactRodux
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local Localization = require(InGameMenu.Localization.Localization)
local LocalizationProvider = require(InGameMenu.Localization.LocalizationProvider)
local reducer = require(InGameMenu.reducer)
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local LoadingFriendsError = require(script.Parent.LoadingFriendsError)
it("should create and destroy without errors", function()
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = Rodux.Store.new(reducer)
}, {
ThemeProvider = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
LocalizationProvider = Roact.createElement(LocalizationProvider, {
localization = Localization.new("en-us"),
}, {
LoadingFriendsError = Roact.createElement(LoadingFriendsError, {
onRetry = function()
print("On retry")
end,
}),
}),
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,135 @@
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local UIBlox = InGameMenuDependencies.UIBlox
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent.Parent
local TEXT_BOX_OFFSET = 44
local Assets = require(InGameMenu.Resources.Assets)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local ImageSetButton = UIBlox.Core.ImageSet.Button
local SearchBox = Roact.PureComponent:extend("SearchBox")
SearchBox.validateProps = t.strictInterface({
Size = t.UDim2,
Position = t.UDim2,
AnchorPoint = t.Vector2,
searchUpdated = t.callback,
})
function SearchBox:init()
self.state = {
searchText = "",
}
end
function SearchBox:render()
return withLocalization({
searchPlaceholder = "CoreScripts.InGameMenu.InviteFriends.Search",
})(function(localized)
return withStyle(function(style)
local textTheme = style.Theme.TextEmphasis
local textFont = style.Font.Body
return Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Size = self.props.Size,
Position = self.props.Position,
AnchorPoint = self.props.AnchorPoint,
Image = Assets.Images.RoundedRect.Image,
ImageColor3 = style.Theme.BackgroundUIContrast.Color,
ImageTransparency = style.Theme.BackgroundUIContrast.Transparency,
ScaleType = Assets.Images.RoundedRect.ScaleType,
SliceCenter = Assets.Images.RoundedRect.SliceCenter,
}, {
Border = Roact.createElement(ImageSetLabel, {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
Image = Assets.Images.RoundedRectBorder.Image,
SliceCenter = Assets.Images.RoundedRectBorder.SliceCenter,
ScaleType = Assets.Images.RoundedRectBorder.ScaleType,
ImageColor3 = style.Theme.Divider.Color,
ImageTransparency = style.Theme.Divider.Transparency,
}),
SearchIcon = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Size = UDim2.new(0, 16, 0, 16),
Position = UDim2.new(0, 22, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Image = Assets.Images.SearchIcon,
ImageColor3 = style.Theme.IconEmphasis.Color,
}),
TextBox = Roact.createElement("TextBox", {
Size = UDim2.new(1, -TEXT_BOX_OFFSET, 1, 0),
Position = UDim2.new(0, TEXT_BOX_OFFSET, 0, 0),
PlaceholderText = "",
Text = self.state.searchText,
ClearTextOnFocus = false,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
TextColor3 = textTheme.Color,
TextTransparency = textTheme.Transparency,
Font = textFont.Font,
TextSize = textFont.RelativeSize * style.Font.BaseSize,
TextWrapped = false,
[Roact.Change.Text] = function(rbx)
if self.state.searchText ~= rbx.Text then
self:setState({
searchText = rbx.Text,
})
self.props.searchUpdated(self.state.searchText)
end
end,
}),
--TextBox.PlaceholderText can't be used as it's text transparency can't be changed.
PlaceholderText = Roact.createElement(ThemedTextLabel, {
themeKey = "TextEmphasis",
fontKey = "Body",
Text = localized.searchPlaceholder,
Size = UDim2.new(1, -TEXT_BOX_OFFSET, 1, 0),
Position = UDim2.new(0, TEXT_BOX_OFFSET, 0, 0),
TextTransparency = 0.5,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 2,
Visible = self.state.searchText == "",
}),
ClearButton = Roact.createElement(ImageSetButton, {
Size = UDim2.new(0, 18, 0, 18),
Position = UDim2.new(1, -14, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
BackgroundTransparency = 1,
Image = Assets.Images.ClearIcon,
ImageColor3 = style.Theme.IconEmphasis.Color,
Visible = self.state.searchText ~= "",
[Roact.Event.Activated] = function()
self:setState({
searchText = "",
})
self.props.searchUpdated("")
end,
}),
})
end)
end)
end
return SearchBox
@@ -0,0 +1,262 @@
local RunService = game:GetService("RunService")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent.Parent
local Constants = require(InGameMenu.Resources.Constants)
local InviteStatus = Constants.InviteStatus
local Assets = require(InGameMenu.Resources.Assets)
local Images = UIBlox.App.ImageSet.Images
local ImageSetLabel = UIBlox.Core.ImageSet.Label
local FFlagFixMenuIcons = require(InGameMenu.Flags.FFlagFixMenuIcons)
local SendInviteButton = Roact.PureComponent:extend("SendInviteButton")
SendInviteButton.validateProps = t.strictInterface({
onActivated = t.callback,
animationBegun = t.callback,
animationEnded = t.callback,
userInviteStatus = t.optional(t.string)
})
local FPS = 30
-- Returns 0-1 based on the value of time betweenStartFrame and endFrame
local function linearTween(dTime, startFrame, endFrame)
local range = (endFrame - startFrame)/FPS
return (dTime - (startFrame/FPS)) / range
end
local function isBetweenFrames(dTime, startFrame, endFrame)
return dTime >= (startFrame/FPS) and dTime <= (endFrame/FPS)
end
local STATUS_TO_ANIMATION_MAP = {
[InviteStatus.Pending] = function(timeElapsed, updateBindings)
local maxTransparency = 0.75
local minTransparency = 0
local range = (maxTransparency - minTransparency) / 2
-- Tween transparency on a sin wave for a transparency "pulse" effect
local transparency = range + (range * math.sin(2 * math.pi * timeElapsed))
updateBindings.sendTransparency(transparency)
end,
[InviteStatus.Success] = function(timeElapsed, updateBindings, stopCallback)
-- Fade out send icon, fade-in and scale-down success icon
if isBetweenFrames(timeElapsed, 0, 4) then
local scaledElapse = linearTween(timeElapsed, 0, 4)
updateBindings.sendTransparency(scaledElapse)
updateBindings.successTransparency(1-scaledElapse)
local successSizeMax = 1.5
local successSizeMin = 0.75
local range = successSizeMax-successSizeMin
local size = successSizeMin + (range * (1 - scaledElapse))
updateBindings.successSize(size)
-- Complete fade-outs, scale success icon to normal size
elseif isBetweenFrames(timeElapsed, 4, 9) then
updateBindings.sendTransparency(1)
updateBindings.successTransparency(0)
local scaledElapse = linearTween(timeElapsed, 4, 9)
local successSizeMax = 1
local successSizeMin = 0.75
local range = successSizeMax-successSizeMin
local size = successSizeMin + (range * (scaledElapse))
updateBindings.successSize(size)
-- Complete success icon size tween,
else
updateBindings.successSize(1)
stopCallback()
end
end,
[InviteStatus.Failed] = function(timeElapsed, updateBindings, stopCallback)
-- Fade out send icon
if isBetweenFrames(timeElapsed, 0, 2) then
local scaledElapse = linearTween(timeElapsed, 0, 4)
updateBindings.sendTransparency(scaledElapse)
-- Hold fadeout to frame 52
elseif isBetweenFrames(timeElapsed, 2, 52) then
updateBindings.sendTransparency(1)
-- Fade in send icon
elseif isBetweenFrames(timeElapsed, 52, 57) then
local scaledElapse = linearTween(timeElapsed, 52, 57)
updateBindings.sendTransparency(1-scaledElapse)
-- Finish send fade-in and stop animation
else
updateBindings.sendTransparency(0)
stopCallback()
end
-- Fade in fail icon
if isBetweenFrames(timeElapsed, 0, 4) then
local scaledElapse = linearTween(timeElapsed, 0, 4)
updateBindings.failTransparency(1-scaledElapse)
-- Shake fail icon
elseif isBetweenFrames(timeElapsed, 4, 42) then
updateBindings.failTransparency(0)
local e = 2.71828182846
-- Shake amplitude
local a = -0.4
-- Shake frequency
local f = 40
-- Shake damping
local k = 8
-- Damped oscillation https://www.desmos.com/calculator/ajx32emroq
local et = timeElapsed - 4/FPS
local pos = a * e^(-et * k) * math.sin(et * f)
updateBindings.failPos(pos)
-- Fadeout fail icon
elseif isBetweenFrames(timeElapsed, 42, 47) then
updateBindings.failPos(0)
local scaledElapse = linearTween(timeElapsed, 42, 47)
updateBindings.failTransparency(scaledElapse)
-- Complete fadeout
else
updateBindings.failTransparency(1)
end
end,
}
function SendInviteButton:init(initialProps)
local sendTransparency, updateSendTransparency = Roact.createBinding(0)
local successTransparency, updateSuccessTransparency = Roact.createBinding(1)
local failTransparency, updateFailTransparency = Roact.createBinding(1)
local successSize, updateSuccessSize = Roact.createBinding(1)
local failPos, updateFailPos = Roact.createBinding(0)
self.bindings = {
sendTransparency = sendTransparency,
successTransparency = successTransparency,
failTransparency = failTransparency,
successSize = successSize:map(function(size)
return UDim2.new(size, 0, size, 0)
end),
failPos = failPos:map(function(pos)
return UDim2.new(pos, 0, 0, 0)
end),
}
self.updateBindings = {
sendTransparency = updateSendTransparency,
successTransparency = updateSuccessTransparency,
failTransparency = updateFailTransparency,
successSize = updateSuccessSize,
failPos = updateFailPos,
}
if initialProps.userInviteStatus == InviteStatus.Success then
updateSuccessTransparency(0)
updateSendTransparency(1)
end
end
function SendInviteButton:render()
local props = self.props
return withStyle(function(style)
return Roact.createElement("TextButton", {
Size = UDim2.new(0, 36, 0, 36),
BackgroundTransparency = 1,
Text = "",
[Roact.Event.Activated] = function()
local isPending = props.userInviteStatus == InviteStatus.Pending
local isSuccess = props.userInviteStatus == InviteStatus.Success
if not self.renderSteppedConnection and not (isPending or isSuccess) then
props.onActivated()
end
end
}, {
SendLabel = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = FFlagFixMenuIcons and Images["icons/actions/friends/friendInvite"] or Assets.Images.SendInvite,
Size = UDim2.new(1, 0, 1, 0),
ImageColor3 = style.Theme.IconEmphasis.Color,
ImageTransparency = self.bindings.sendTransparency,
}),
SuccessLabel = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = FFlagFixMenuIcons and Images["icons/status/success"] or Assets.Images.InviteSuccess,
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
ImageColor3 = style.Theme.IconEmphasis.Color,
ImageTransparency = self.bindings.successTransparency,
Size = self.bindings.successSize
}),
FailLabel = Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
Image = FFlagFixMenuIcons and Images["icons/status/alert"] or Assets.Images.InviteFail,
Size = UDim2.new(1, 0, 1, 0),
ImageColor3 = style.Theme.IconEmphasis.Color,
ImageTransparency = self.bindings.failTransparency,
Position = self.bindings.failPos
})
})
end)
end
function SendInviteButton:didUpdate(previousProps)
if self.props.userInviteStatus and self.props.userInviteStatus ~= previousProps.userInviteStatus then
self:playAnimation(STATUS_TO_ANIMATION_MAP[self.props.userInviteStatus])
end
end
function SendInviteButton:didMount()
if self.props.userInviteStatus then
self:playAnimation(STATUS_TO_ANIMATION_MAP[self.props.userInviteStatus])
end
end
function SendInviteButton:playAnimation(animFunc)
self.animationStartTime = tick()
self.animationFunction = animFunc
if not self.renderSteppedConnection then
self.renderSteppedConnection = RunService.RenderStepped:Connect(function()
self.animationFunction(tick() - self.animationStartTime, self.updateBindings, function()
self:stopAnimation()
end)
end)
self.props.animationBegun()
end
end
function SendInviteButton:stopAnimation()
if self.renderSteppedConnection then
self.renderSteppedConnection:Disconnect()
self.renderSteppedConnection = nil
self.props.animationEnded()
end
end
function SendInviteButton:willUnmount()
self:stopAnimation()
end
return SendInviteButton
@@ -0,0 +1,75 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local InGameMenu = script.Parent.Parent.Parent
local AppDarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local AppFont = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
local appStyle = {
Theme = AppDarkTheme,
Font = AppFont,
}
local SendInviteButton = require(script.Parent.SendInviteButton)
local Constants = require(InGameMenu.Resources.Constants)
local InviteStatus = Constants.InviteStatus
it("should create and destroy without errors", function()
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
SendInviteButton = Roact.createElement(SendInviteButton, {
onActivated = function()
print("onActivated")
end,
animationBegun = function()
print("animationBegun")
end,
animationEnded = function()
print("animationEnded")
end,
userInviteStatus = nil,
}),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
describe("animations", function()
it("should call animation began and ended once", function()
local animationBegunCalled = 0
local animationEndedCalled = 0
local element = Roact.createElement(UIBlox.Core.Style.Provider, {
style = appStyle,
}, {
SendInviteButton = Roact.createElement(SendInviteButton, {
onActivated = function()
print("onActivated")
end,
animationBegun = function()
animationBegunCalled = animationBegunCalled + 1
end,
animationEnded = function()
animationEndedCalled = animationEndedCalled + 1
end,
userInviteStatus = InviteStatus.Failed,
}),
})
local instance = Roact.mount(element)
wait(1)
Roact.unmount(instance)
expect(animationBegunCalled).to.equal(1)
expect(animationEndedCalled).to.equal(1)
end)
end)
end
@@ -0,0 +1,161 @@
local Players = game:GetService("Players")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local t = InGameMenuDependencies.t
local InGameMenu = script.Parent.Parent
local Promise = require(InGameMenu.Utility.Promise)
local withLocalization = require(InGameMenu.Localization.withLocalization)
local Page = require(InGameMenu.Components.Page)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local PageNavigationWatcher = require(InGameMenu.Components.PageNavigationWatcher)
local InviteFriendsList = require(script.InviteFriendsList)
local AddFriendsNow = require(script.AddFriendsNow)
local LoadingFriendsError = require(script.LoadingFriendsError)
local InviteFriendsPage = Roact.PureComponent:extend("InviteFriendsPage")
InviteFriendsPage.validateProps = t.strictInterface({
pageTitle = t.string,
})
function InviteFriendsPage:init()
self.state = {
loadingFriends = true,
loadingFriendsError = false,
friends = {},
}
end
function InviteFriendsPage:renderLoadingPage()
return withLocalization({
loading = "CoreScripts.InGameMenu.InviteFriends.Loading",
})(function(localized)
return Roact.createElement(ThemedTextLabel, {
Size = UDim2.new(1, 0, 1, 0),
Text = localized.loading,
fontKey = "Header1",
themeKey = "TextEmphasis",
})
end)
end
function InviteFriendsPage:renderError()
return Roact.createElement(LoadingFriendsError, {
onRetry = function()
self:setState({
loadingFriends = true,
loadingFriendsError = false,
})
self:loadFriends()
end
})
end
function InviteFriendsPage:renderNoFriends()
return Roact.createElement(AddFriendsNow)
end
function InviteFriendsPage:renderFriends()
return Roact.createElement(InviteFriendsList, {
players = self.state.friends,
})
end
function InviteFriendsPage:render()
local state = self.state
local children = {
Watcher = Roact.createElement(PageNavigationWatcher, {
desiredPage = "InviteFriends",
onNavigateTo = function()
self:loadFriends()
end,
})
}
if state.loadingFriends then
children.Loading = self:renderLoadingPage()
elseif state.loadingFriendsError then
children.ErrorLoading = self:renderError()
elseif #state.friends == 0 then
children.NoFriends = self:renderNoFriends()
else
children.FriendsList = self:renderFriends()
end
return Roact.createElement(Page, {
pageTitle = self.props.pageTitle,
}, children)
end
function InviteFriendsPage:didMount()
self.mounted = true
self:loadFriends()
end
function InviteFriendsPage:loadFriends()
Promise.new(function(resolve, reject)
coroutine.wrap(function()
local localPlayer = Players.LocalPlayer
local success, friendsPages = pcall(function()
return Players:GetFriendsAsync(localPlayer.UserId)
end)
if not success then
reject("Error loading friends")
return
end
local friends = {}
while true do
for _, item in ipairs(friendsPages:GetCurrentPage()) do
friends[#friends+1] = {
IsOnline = item.IsOnline,
Id = item.Id,
Username = item.Username,
DisplayName = item.DisplayName,
}
end
if not friendsPages.IsFinished then
success = pcall(function()
friendsPages:AdvanceToNextPageAsync()
end)
if not success then
reject("Error loading friends")
return
end
else
break
end
end
resolve(friends)
end)()
end):andThen(function(friends)
if self.mounted then
self:setState({
loadingFriends = false,
friends = friends
})
end
end):catch(function()
if self.mounted then
self:setState({
loadingFriends = false,
loadingFriendsError = true,
})
end
end)
end
function InviteFriendsPage:willUnmount()
self.mounted = false
end
return InviteFriendsPage
@@ -0,0 +1,190 @@
local TextService = game:GetService("TextService")
local UserInputService = game:GetService("UserInputService")
local CorePackages = game:GetService("CorePackages")
local InGameMenuDependencies = require(CorePackages.InGameMenuDependencies)
local Roact = InGameMenuDependencies.Roact
local UIBlox = InGameMenuDependencies.UIBlox
local t = InGameMenuDependencies.t
local withStyle = UIBlox.Core.Style.withStyle
local InGameMenu = script.Parent.Parent
local Assets = require(InGameMenu.Resources.Assets)
local GlobalConfig = require(InGameMenu.GlobalConfig)
local ThemedTextLabel = require(InGameMenu.Components.ThemedTextLabel)
local ImageSetLabel = UIBlox.Core.ImageSet.Label
-- Additional width for keys with centered text like [Backspace], [Enter], etc
local CENTERED_EXTRA_WIDTH = 14 * 2
-- Side padding for off-centered keys [Shift ]
local OFF_CENTER_PADDING = 9
-- Big and small font sizes for key text. (Var names are succinct to make
-- populating override more easy)
local BIG = "CaptionHeader"
local SMALL = "Footer"
local DEFAULT_BORDER_THEME = "UIEmphasis"
local DEFAULT_TEXT_THEME = "TextEmphasis"
local CONTENT_OVERRIDE_MAP = {
[Enum.KeyCode.Escape] = {text = "ESC", fontKey = SMALL, width = 36},
[Enum.KeyCode.Space] = {text = "Space", width = 92},
[Enum.KeyCode.LeftShift] = {text = "Shift", width = 66, alignment = Enum.TextXAlignment.Left},
[Enum.KeyCode.LeftControl] = {text = "Ctrl", width = 66, alignment = Enum.TextXAlignment.Left},
[Enum.KeyCode.LeftSuper] = {text = "Command"},
[Enum.KeyCode.LeftMeta] = {text = "fn"},
[Enum.KeyCode.LeftAlt] = {text = "Opt"},
[Enum.KeyCode.Tilde] = {text = "~", fontKey = BIG},
[Enum.KeyCode.F10] = {fontKey = BIG, width = 36},
[Enum.KeyCode.F11] = {fontKey = BIG, width = 36},
[Enum.KeyCode.Up] = {image = Assets.Images.UpArrow},
[Enum.KeyCode.Down] = {image = Assets.Images.DownArrow},
[Enum.KeyCode.Left] = {image = Assets.Images.LeftArrow},
[Enum.KeyCode.Right] = {image = Assets.Images.RightArrow},
}
local validateProps = t.strictInterface({
input = t.enum(Enum.KeyCode),
--Defaults to UIEmphasis
borderThemeKey = t.optional(t.string),
--Defaults to TextEmphasis
textThemeKey = t.optional(t.string),
AnchorPoint = t.optional(t.Vector2),
Position = t.optional(t.UDim2),
LayoutOrder = t.optional(t.integer),
ZIndex = t.optional(t.integer),
})
local function getLabelWidthAndContent(input, style, textThemeKey)
local override = CONTENT_OVERRIDE_MAP[input]
if override and override.image then
local width = 36
local content = Roact.createElement(ImageSetLabel, {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = override.image,
ImageColor3 = style.Theme.IconEmphasis.Color,
ImageTransparency = style.Theme.IconEmphasis.Transparency,
Position = UDim2.new(0.5, 0, 0.5, -1),
Size = UDim2.new(0, 16, 0, 16),
})
return width, content
else
local text, fontKey, width, alignment do
local keyString = UserInputService:GetStringForKeyCode(input)
if override and override.text then
text = override.text
elseif keyString and keyString ~= "" then
text = keyString
else
text = input.Name
end
local textIsShort = text:len() < 3
if override and override.fontKey then
fontKey = override.fontKey
else
fontKey = textIsShort and BIG or SMALL
end
if override and override.width then
width = override.width
elseif textIsShort then
width = 36
else
local fontStyle = style.Font[fontKey]
local textSize = fontStyle.RelativeSize * style.Font.BaseSize
local fontType = fontStyle.Font
local textWidth = TextService:GetTextSize(
text,
textSize,
fontType,
Vector2.new(math.huge, 36)
).X
width = textWidth + CENTERED_EXTRA_WIDTH
end
if override and override.alignment then
alignment = override.alignment
end
end
local content = Roact.createElement(ThemedTextLabel, {
fontKey = fontKey,
themeKey = textThemeKey or DEFAULT_TEXT_THEME,
Text = text,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, -1),
TextXAlignment = alignment,
})
return width, content, alignment
end
end
local function KeyLabel(props)
if GlobalConfig.propValidation then
assert(validateProps(props))
end
return withStyle(function(style)
local borderTheme = style.Theme[props.borderThemeKey or DEFAULT_BORDER_THEME]
local width, content, alignment = getLabelWidthAndContent(
props.input,
style,
props.textThemeKey
)
local padding
if alignment then
padding = OFF_CENTER_PADDING
end
return Roact.createElement(ImageSetLabel, {
BackgroundTransparency = 1,
ImageTransparency = borderTheme.Transparency,
ImageColor3 = borderTheme.Color,
Image = Assets.Images.KeyBorder,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(Vector2.new(9, 7), Vector2.new(26, 26)),
Size = UDim2.new(0, width, 0, 36),
Position = props.Position,
AnchorPoint = props.AnchorPoint,
LayoutOrder = props.LayoutOrder,
ZIndex = props.ZIndex,
}, {
Padding = padding and Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, padding),
PaddingRight = UDim.new(0, padding)
}),
LabelContent = content
})
end)
end
return KeyLabel

Some files were not shown because too many files have changed in this diff Show More