-
Notifications
You must be signed in to change notification settings - Fork 44
/
AvatarContextMenu.lua
461 lines (384 loc) · 14.9 KB
/
AvatarContextMenu.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
--[[
// FileName: AvatarContextMenu.lua
// Written by: TheGamer101
// Description: A context menu to allow users to click on avatars and then interact with that user.
]]
-- OPTIONS
local DEBUG_MODE = game:GetService("RunService"):IsStudio() -- use this to run as a guest/use in games that don't have AvatarContextMenu. FOR TESTING ONLY!
local isAvatarContextMenuEnabled = false
local FFlagUseRoactPlayerList = settings():GetFFlag("UseRoactPlayerList")
-- CONSTANTS
local MAX_CONTEXT_MENU_DISTANCE = 100
local OPEN_MENU_TIME = 0.2
local OPEN_MENU_TWEEN = TweenInfo.new(OPEN_MENU_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local CLOSE_MENU_TIME = 0.2
local CLOSE_MENU_TWEEN = TweenInfo.new(CLOSE_MENU_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.In)
local LEAVE_MENU_ACTION_NAME = "EscapeAvatarContextMenu"
local GAMEPAD_OPEN_MENU_ACTION = "GamepadOpenAvatarContextMenu"
local SWITCH_PAGE_ACTION_NAME = "SwitchPageAvatarContextMenu"
local MAX_MOVEMENT_THRESHOLD = 20
-- SERVICES
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local PlayersService = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local CoreGuiService = game:GetService("CoreGui")
local StarterGui = game:GetService("StarterGui")
local GuiService = game:GetService("GuiService")
local AnalyticsService = game:GetService("RbxAnalyticsService")
local hasTrackedAvatarContextMenu = false
function enableAvatarContextMenu(enabled)
isAvatarContextMenuEnabled = not not enabled
if isAvatarContextMenuEnabled and not hasTrackedAvatarContextMenu then
hasTrackedAvatarContextMenu = true
AnalyticsService:TrackEvent("Game", "AvatarContextMenuEnabled", "placeId: " .. tostring(game.PlaceId))
end
end
StarterGui:RegisterSetCore("SetAvatarContextMenuEnabled", enableAvatarContextMenu)
StarterGui:RegisterSetCore("AvatarContextMenuEnabled", enableAvatarContextMenu)
StarterGui:RegisterGetCore("AvatarContextMenuEnabled", function()
return isAvatarContextMenuEnabled
end)
--- MODULES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu")
local ContextMenuGui = require(AvatarMenuModules:WaitForChild("ContextMenuGui"))
local ContextMenuItemsModule = require(AvatarMenuModules:WaitForChild("ContextMenuItems"))
local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil"))
local SelectedCharacterIndicator = require(AvatarMenuModules:WaitForChild("SelectedCharacterIndicator"))
local ThemeHandler = require(AvatarMenuModules.ThemeHandler)
local BlockingUtility
if FFlagUseRoactPlayerList then
BlockingUtility = require(CoreGuiModules.BlockingUtility)
else
local PlayerDropDownModule = require(CoreGuiModules:WaitForChild("PlayerDropDown"))
BlockingUtility = PlayerDropDownModule:CreateBlockingUtility()
end
--- VARIABLES
local LocalPlayer = PlayersService.LocalPlayer
while not LocalPlayer do
PlayersService.PlayerAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
-- no avatar context menu for guests
if LocalPlayer.UserId <= 0 and not DEBUG_MODE then return end
local ContextMenuItems = nil
local ContextMenuFrame = nil
local ContextMenuOpening = false
local ContextMenuOpen = false
local SelectedPlayer = nil
local lastInputObject = nil
local initialScreenPoint = nil
local hasTouchSwipeInput = nil
local contextMenuPlayerChangedConn = nil
ContextMenuFrame = ContextMenuGui:CreateMenuFrame(ThemeHandler:GetTheme())
ContextMenuItems = ContextMenuItemsModule.new(ContextMenuFrame.Content.ContextActionList)
function SetSelectedPlayer(player, dontTween)
if SelectedPlayer == player then return end
SelectedPlayer = player
SelectedCharacterIndicator:ChangeSelectedPlayer(SelectedPlayer, ThemeHandler:GetTheme())
ContextMenuItems:BuildContextMenuItems(SelectedPlayer)
ContextMenuGui:SwitchToPlayerEntry(SelectedPlayer, dontTween)
end
function OpenMenu(theme)
ContextMenuOpening = true
ContextMenuFrame.Visible = true
ContextMenuFrame.Content.ContextActionList.CanvasPosition = Vector2.new(0,0)
ContextMenuFrame.Position = theme.OffScreenPosition
contextMenuPlayerChangedConn = ContextMenuGui.SelectedPlayerChanged:connect(function()
SetSelectedPlayer(ContextMenuGui:GetSelectedPlayer())
end)
ContextMenuFrame.Position = theme.OffScreenPosition
local positionTween = TweenService:Create(ContextMenuFrame, OPEN_MENU_TWEEN, {Position = theme.OnScreenPosition})
positionTween:Play()
positionTween.Completed:wait()
ContextMenuOpening = false
end
function BindMenuActions()
-- Close Menu actions
local closeMenuFunc = function(actionName, inputState, input)
if inputState ~= Enum.UserInputState.Begin then
return
end
CloseContextMenu()
end
ContextActionService:BindCoreAction(LEAVE_MENU_ACTION_NAME, closeMenuFunc, false, Enum.KeyCode.Escape,
Enum.KeyCode.ButtonB)
local gamepadSwitchPage = function(actionName, inputState, input)
if inputState == Enum.UserInputState.Begin then
if input.KeyCode == Enum.KeyCode.ButtonR1 then
ContextMenuGui:OffsetPlayerEntry(1)
elseif input.KeyCode == Enum.KeyCode.ButtonL1 then
ContextMenuGui:OffsetPlayerEntry(-1)
end
end
end
ContextActionService:BindCoreAction(SWITCH_PAGE_ACTION_NAME, gamepadSwitchPage, false, Enum.KeyCode.ButtonR1,
Enum.KeyCode.ButtonL1)
local menuOpenedCon = nil
menuOpenedCon = GuiService.MenuOpened:connect(function()
menuOpenedCon:disconnect()
closeMenuFunc(nil, Enum.UserInputState.Begin, nil)
end)
end
function BuildPlayerCarousel(selectedPlayer, worldPoint)
local playersByProximity = {}
local players = PlayersService:GetPlayers()
for i = 1, #players do
if players[i].UserId > 0 or DEBUG_MODE then
if players[i] ~= LocalPlayer then
local playerPosition = ContextMenuUtil:GetPlayerPosition(players[i])
if playerPosition then
local distanceFromClicked = (worldPoint - playerPosition).magnitude
table.insert(playersByProximity, {players[i], distanceFromClicked})
end
end
end
end
local function closestPlayerComp(playerA, playerB)
return playerA[2] > playerB[2]
end
table.sort(playersByProximity, closestPlayerComp)
ContextMenuGui:BuildPlayerCarousel(playersByProximity, ThemeHandler:GetTheme())
end
PlayersService.PlayerRemoving:connect(function(player)
if ContextMenuOpen and player ~= SelectedPlayer then
ContextMenuGui:RemovePlayerEntry(player)
end
end)
function OpenContextMenu(player, worldPoint)
if ContextMenuOpening or ContextMenuOpen or not isAvatarContextMenuEnabled then
return
end
ContextMenuOpen = true
BuildPlayerCarousel(player, worldPoint)
ContextMenuUtil:DisablePlayerMovement()
BindMenuActions()
SetSelectedPlayer(player, true)
ContextMenuGui:UpdateGuiTheme(ThemeHandler:GetTheme())
OpenMenu(ThemeHandler:GetTheme())
end
function CloseContextMenu()
GuiService.SelectedCoreObject = nil
ContextActionService:UnbindCoreAction(LEAVE_MENU_ACTION_NAME)
ContextActionService:UnbindCoreAction(SWITCH_PAGE_ACTION_NAME)
ContextMenuUtil:EnablePlayerMovement()
if contextMenuPlayerChangedConn then
contextMenuPlayerChangedConn:disconnect()
end
local positionTween = TweenService:Create(ContextMenuFrame, CLOSE_MENU_TWEEN, {Position = UDim2.new(0.5, 0, 1, ContextMenuFrame.AbsoluteSize.Y)})
positionTween:Play()
positionTween.Completed:wait()
ContextMenuFrame.Visible = false
SetSelectedPlayer(nil)
ContextMenuOpen = false
end
ContextMenuGui:SetCloseMenuFunc(CloseContextMenu)
ContextMenuItems:SetCloseMenuFunc(CloseContextMenu)
local function isPointInside(point, topLeft, bottomRight)
return (point.X >= topLeft.X and
point.X <= bottomRight.X and
point.Y >= topLeft.Y and
point.Y <= bottomRight.Y)
end
function PointInSwipeArea(screenPoint)
local topLeft = ContextMenuFrame.AbsolutePosition
local nameTag = ContextMenuFrame.Content:FindFirstChild("NameTag")
local bottomRight = Vector2.new(topLeft.x + ContextMenuFrame.AbsoluteSize.x, nameTag.AbsolutePosition.y + nameTag.AbsoluteSize.y)
return isPointInside(screenPoint, topLeft, bottomRight)
end
function LocalPlayerHasToolEquipped()
if not LocalPlayer.Character then return false end
for _, child in ipairs(LocalPlayer.Character:GetChildren()) do
if child:IsA("BackpackItem") then
return true
end
end
return false
end
function shouldIgnoreLocalCharacter()
if LocalPlayer.Character then
local head = LocalPlayer.Character:FindFirstChild("Head")
if head then
-- This will be true if the player is in first person.
return head.LocalTransparencyModifier >= 0.95
end
end
end
function clickedOnPoint(screenPoint)
local camera = workspace.CurrentCamera
if not camera then return end
if LocalPlayerHasToolEquipped() then return end
local ray = camera:ScreenPointToRay(screenPoint.X, screenPoint.Y)
ray = Ray.new(ray.Origin, ray.Direction * MAX_CONTEXT_MENU_DISTANCE)
local hitPart, hitPoint
if shouldIgnoreLocalCharacter() then
hitPart, hitPoint = workspace:FindPartOnRay(ray, LocalPlayer.Character, false, true)
else
hitPart, hitPoint = workspace:FindPartOnRay(ray, nil, false, true)
end
local player = ContextMenuUtil:FindPlayerFromPart(hitPart)
if player and ((DEBUG_MODE and player ~= LocalPlayer) or (player ~= LocalPlayer and player.UserId > 0)) then
if ContextMenuOpen then
SetSelectedPlayer(player)
else
OpenContextMenu(player, hitPoint)
end
elseif not player and ContextMenuOpen then
CloseContextMenu()
end
end
function OnUserInput(screenPoint, inputObject)
if inputObject.UserInputState == Enum.UserInputState.Begin and lastInputObject == nil then
lastInputObject = inputObject
initialScreenPoint = screenPoint
elseif lastInputObject == inputObject and inputObject.UserInputState == Enum.UserInputState.Change then
if (screenPoint - initialScreenPoint).magnitude > 5 then
lastInputObject = nil
initialScreenPoint = nil
end
elseif inputObject.UserInputState == Enum.UserInputState.End and lastInputObject == inputObject then
lastInputObject = nil
initialScreenPoint = nil
clickedOnPoint(screenPoint)
end
end
function OnMouseMoved(screenPoint)
if not ContextMenuOpen and lastInputObject and (screenPoint - initialScreenPoint).magnitude > MAX_MOVEMENT_THRESHOLD then
lastInputObject = nil
initialScreenPoint = nil
end
end
function trackTouchSwipeInput(inputObject)
if inputObject.UserInputType == Enum.UserInputType.Touch then
if not hasTouchSwipeInput and inputObject.UserInputState == Enum.UserInputState.Begin then
if PointInSwipeArea(inputObject.Position) then
hasTouchSwipeInput = inputObject
end
elseif hasTouchSwipeInput == inputObject and inputObject.UserInputState == Enum.UserInputState.End then
spawn(function()
hasTouchSwipeInput = nil
end)
end
end
end
local function functionProcessInput(inputObject, gameProcessedEvent)
trackTouchSwipeInput(inputObject)
if gameProcessedEvent then
if inputObject == lastInputObject then
lastInputObject = nil
end
return
end
if inputObject.UserInputType == Enum.UserInputType.MouseButton1 or
inputObject.UserInputType == Enum.UserInputType.Touch then
OnUserInput(Vector2.new(inputObject.Position.X, inputObject.Position.Y), inputObject)
elseif inputObject.UserInputType == Enum.UserInputType.MouseMovement then
OnMouseMoved(Vector2.new(inputObject.Position.X, inputObject.Position.Y))
end
end
UserInputService.InputBegan:Connect(functionProcessInput)
UserInputService.InputChanged:Connect(functionProcessInput)
UserInputService.InputEnded:Connect(functionProcessInput)
UserInputService.TouchSwipe:Connect(function(swipeDir, numOfTouches, gameProcessedEvent)
if not gameProcessedEvent then return end
if not ContextMenuOpen then return end
if not hasTouchSwipeInput then return end
local offset = 0
if swipeDir == Enum.SwipeDirection.Left then
offset = 1
elseif swipeDir == Enum.SwipeDirection.Right then
offset = -1
end
if offset ~= 0 then
ContextMenuGui:OffsetPlayerEntry(offset)
SetSelectedPlayer(ContextMenuGui:GetSelectedPlayer())
end
end)
LocalPlayer.FriendStatusChanged:Connect(function(player, friendStatus)
if player and player == SelectedPlayer then
local isBlocked = BlockingUtility:IsPlayerBlockedByUserId(player.UserId)
ContextMenuItems:UpdateFriendButton(friendStatus, isBlocked)
end
end)
function GetWorldPoint(player)
if player.Character then
local rootPart = player.Character:FindFirstChild("HumanoidRootPart")
if rootPart then
return rootPart.Position
end
end
if player ~= LocalPlayer then
return GetWorldPoint(LocalPlayer)
end
return Vector3.new(0, 0, 0)
end
StarterGui:RegisterGetCore("AvatarContextMenuTarget",
function()
return SelectedPlayer
end
)
StarterGui:RegisterSetCore("AvatarContextMenuTarget",
function(player)
local isPlayer = typeof(player) == "Instance" and player:IsA("Player")
if isPlayer then
if player.Parent ~= nil then
if ContextMenuOpen or ContextMenuOpening then
SetSelectedPlayer(player, true)
else
OpenContextMenu(player, GetWorldPoint(player))
end
else
error("AvatarContextMenuTarget Player must be in the game")
end
elseif player == nil then
CloseContextMenu()
else
error("AvatarContextMenuTarget argument must be a Player or nil")
end
end
)
local function getClosestPlayer()
if not LocalPlayer.Character then
return
end
local rootPart = LocalPlayer.Character:FindFirstChild("HumanoidRootPart")
if not rootPart then
return
end
local localPosition = rootPart.Position
local closestPlayer = nil
local closestPlayerDistance = math.huge
local closestPlayerPoint = nil
local players = PlayersService:GetPlayers()
for _, player in ipairs(players) do
if player ~= LocalPlayer and player.Character then
local rootPart = player.Character:FindFirstChild("HumanoidRootPart")
if rootPart and (rootPart.Position - localPosition).magnitude < closestPlayerDistance then
closestPlayer = player
closestPlayerDistance = (rootPart.Position - localPosition).magnitude
closestPlayerPoint = rootPart.Position
end
end
end
return closestPlayer, closestPlayerPoint
end
local function gamepadOpenMenu(actionName, inputState, input)
if not isAvatarContextMenuEnabled then
return Enum.ContextActionResult.Pass
end
if inputState ~= Enum.UserInputState.Begin then
return Enum.ContextActionResult.Sink
end
if ContextMenuOpen then
CloseContextMenu()
else
local closestPlayer, closestPlayerPoint = getClosestPlayer()
if closestPlayer then
OpenContextMenu(closestPlayer, closestPlayerPoint)
end
end
return Enum.ContextActionResult.Sink
end
ContextActionService:BindCoreAction(GAMEPAD_OPEN_MENU_ACTION, gamepadOpenMenu, false, Enum.KeyCode.DPadUp)