Skip to content

Commit

Permalink
Improve admin UI around roles (#1702)
Browse files Browse the repository at this point in the history
All role-related logic is moved into the "Roles" admin tab, and the
search bar is now sticky-scrolling above searchable submenus. Role
layering description improved, and algorithm overview of role
distribution added.

I added the algorithm overview as HTML in the language file; is there a
better way to have large translatable strings?
  • Loading branch information
nike4613 authored Jan 19, 2025
1 parent c9e0a24 commit 7e45997
Show file tree
Hide file tree
Showing 28 changed files with 951 additions and 398 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ All notable changes to TTT2 will be documented here. Inspired by [keep a changel
- With this, the KeyHelp feature also hides the PropSpec bind if PropSpec is disabled on the server
- Renamed `ttt_session_limits_enabled` to `ttt_session_limits_mode`, introducing a four-mode control (0-3) for managing how TTT2 ends a session. (by @NickCloudAT)
- Modes: 0 = No session limit, 1 = Default TTT, 2 = Only time limit, 3 = Only round limit
- Moved all role-related admin options into the "Roles" menu (by @nike4613)
- Improved description of role layering (by @nike4613)

## [v0.14.0b](https://github.com/TTT-2/TTT2/tree/v0.14.0b) (2024-09-20)

Expand Down
6 changes: 4 additions & 2 deletions gamemodes/terrortown/gamemode/client/cl_help.lua
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ function HELPSCRN:ShowSubmenu(menuClass)
local navArea = vgui.Create("DNavPanelTTT2", frame)
navArea:SetWide(widthNav)
navArea:SetPos(0, 0)
navArea:DockPadding(0, self.padding, 1, self.padding)
navArea:DockPadding(0, 0, 1, 0)
navArea:Dock(LEFT)

local contentArea = vgui.Create("DContentPanelTTT2", frame)
Expand All @@ -381,7 +381,9 @@ function HELPSCRN:ShowSubmenu(menuClass)
submenuList:Dock(FILL)
submenuList:SetPadding(self.padding)
submenuList:SetBasemenuClass(menuClass, contentArea)
submenuList:EnableSearchBar(menuClass:HasSearchbar())
if menuClass.searchBarPlaceholderText then
submenuList:SetSearchBarPlaceholderText(menuClass.searchBarPlaceholderText)
end

-- REFRESH SIZE OF SUBMENULIST FOR CORRECT SUBMENU DEPENDENT SIZE
submenuList:InvalidateLayout(true)
Expand Down
179 changes: 143 additions & 36 deletions gamemodes/terrortown/gamemode/client/cl_vskin/vgui/dsubmenulist_ttt2.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@

local PANEL = {}

---
-- @accessor function
-- @realm client
AccessorFunc(PANEL, "searchFunction", "SearchFunction")

-- Define sizes
local heightNavHeader = 10
local heightNavButton = 50

---
Expand All @@ -22,11 +16,23 @@ function PANEL:Init()
navAreaScroll:Dock(BOTTOM)
self.navAreaScroll = navAreaScroll

local origScrollOnVScroll = navAreaScroll.OnVScroll
navAreaScroll.OnVScroll = function(pnl, scrollOffset)
origScrollOnVScroll(pnl, scrollOffset)
if self.scrollTracker then
local x, y = self.scrollTracker:GetPos()
y = math.max(y + scrollOffset, 0)
self.searchBar:SetPos(x, y)
end
end

-- Split nav area into a grid layout
local navAreaScrollGrid = vgui.Create("DIconLayout", self.navAreaScroll)
navAreaScrollGrid:Dock(FILL)
self.navAreaScrollGrid = navAreaScrollGrid

self.scrollTracker = nil

-- Get the frame to be able to enable keyboardinput on searchbar focus
self.frame = util.getHighestPanelParent(self)

Expand All @@ -47,6 +53,24 @@ function PANEL:SetSearchBarSize(widthBar, heightBar)
end

self.searchBar:SetSize(widthBar, heightBar)
if self.scrollTracker then
self.scrollTracker:SetSize(widthBar, heightBar)
end
end

---
-- Sets the search bar's placeholder text.
-- @param string placeholder The placeholder text.
-- @realm client
function PANEL:SetSearchBarPlaceholderText(placeholder)
if not self.searchBar then
return
end

self.searchBar:SetPlaceholderText(placeholder)
if self.searchBar:GetValue() == "" then
self.searchBar:SetCurrentPlaceholderText(placeholder)
end
end

---
Expand All @@ -57,6 +81,7 @@ function PANEL:EnableSearchBar(active)
if not active then
if self.searchBar then
self.searchBar:Clear()
self.searchBar = nil
end

return
Expand All @@ -65,11 +90,17 @@ function PANEL:EnableSearchBar(active)
-- Add searchbar on top
local searchBar = vgui.Create("DSearchBarTTT2", self)
searchBar:SetUpdateOnType(true)
searchBar:SetPos(0, heightNavHeader)
searchBar:SetHeightMult(1)
searchBar:SetPos(0, heightNavHeader)

searchBar.OnValueChange = function(slf, searchText)
self:GenerateSubmenuList(self.basemenuClass:GetMatchingSubmenus(searchText))
self:ResetSubmenuList()
self:ExtendSubmenuList(self.basemenuClass:GetVisibleNonSearchedSubmenus())
self:AddSearchTracker()
local index = self.navAreaScrollGrid:ChildCount()
self:ExtendSubmenuList(self.basemenuClass:GetMatchingSubmenus(searchText))
self:SelectFirst(index)
self:InvalidateLayout(true)
end

searchBar.OnGetFocus = function(slf)
Expand Down Expand Up @@ -117,40 +148,109 @@ function PANEL:AddSubmenuButton(submenuClass)
return settingsButton
end

---
-- Resets the submenu list, clearing both the nav buttons and associated content area.
-- @realm client
function PANEL:ResetSubmenuList()
self.navAreaScrollGrid:Clear()
self.contentArea:Clear()
self.scrollTracker = nil
end

---
-- This function generates the list of the submenus which are shown in the given contentArea.
-- @param menuClasses submenuClasses
-- @realm client
function PANEL:GenerateSubmenuList(submenuClasses)
self.navAreaScrollGrid:Clear()
self.contentArea:Clear()
self:ResetSubmenuList()
self:ExtendSubmenuList(submenuClasses)
self:SelectFirst()

-- Last refresh sizes depending on number of submenus added
self:InvalidateLayout(true)
end

---
-- Acts like PANEL:GenerateSubmenuList, but does not clear content area or scroll grid.
-- @note This function does NOT invalidate layout.
-- @param menuClasses submenuClasses
-- @realm client
function PANEL:ExtendSubmenuList(submenuClasses)
for i = 1, #submenuClasses do
local submenuClass = submenuClasses[i]
self:AddSubmenuButton(submenuClass)
end

--self:InvalidateLayout(true)
end

---
-- Selects the first submenu added to this list.
-- @param index The index to select the first item at or after. This index is 0-based.
-- @realm client
function PANEL:SelectFirst(index)
if not index then
index = 0
end

if #submenuClasses == 0 then
local labelNoContent = vgui.Create("DLabelTTT2", self.contentArea)
local widthContent = self.contentArea:GetSize()

labelNoContent:SetText("label_menu_not_populated")
labelNoContent:SetSize(widthContent - 40, 50)
labelNoContent:SetFont("DermaTTT2Title")
labelNoContent:SetPos(20, 0)
else
for i = 1, #submenuClasses do
local submenuClass = submenuClasses[i]
local settingsButton = self:AddSubmenuButton(submenuClass)

-- Handle the set of active buttons for the draw process
if i == 1 then
settingsButton:SetActive()
self.lastActive = settingsButton
end
for i = index, self.navAreaScrollGrid:ChildCount() do
local child = self.navAreaScrollGrid:GetChild(i)
if child and child.DoClick then
child:DoClick()
return
end
end

HELPSCRN:SetupContentArea(self.contentArea, submenuClasses[1])
HELPSCRN:BuildContentArea()
-- If a non-zero index was specified, we don't want to present the unpopulated message, because we're doing a search.
-- In that case, we want to display a message that there were no results.
if index ~= 0 then
local msgLabel = vgui.Create("DLabelTTT2", self.contentArea)
msgLabel:SetText("label_menu_search_no_items")
msgLabel:SetFont("DermaTTT2Title")
msgLabel:Dock(FILL)

local dummyPnl = vgui.Create("DPanel", self.contentArea)
dummyPnl:Dock(LEFT)

return
end

-- Last refresh sizes depending on number of submenus added
self:InvalidateLayout(true)
-- make sure the last active gets cleared in this case
if self.lastActive and self.lastActive.SetActive then
self.lastActive:SetActive(false)
end

-- no content, fill the content area appropriately
local labelNoContent = vgui.Create("DLabelTTT2", self.contentArea)
local widthContent = self.contentArea:GetSize()

labelNoContent:SetText("label_menu_not_populated")
labelNoContent:SetSize(widthContent - 40, 50)
labelNoContent:SetFont("DermaTTT2Title")
labelNoContent:SetPos(20, 0)
end

---
-- Adds the search tracker element to the scroll view. Elements added after the search tracker will be searched for.
-- @realm client
function PANEL:AddSearchTracker()
if self.scrollTracker then
ErrorNoHaltWithStack(
"ERROR: DSubMenuListTTT2:AddSearchTracker() called multiple times without resetting!"
)
return
end

if not self.searchBar then
self:EnableSearchBar(true)
end
local tracker = self.navAreaScrollGrid:Add("Panel")

tracker.PerformLayout = function(panel)
panel:SetSize(self.searchBar:GetSize())
end

self.scrollTracker = tracker
end

---
Expand All @@ -162,7 +262,14 @@ function PANEL:SetBasemenuClass(basemenuClass, contentArea)
self.basemenuClass = basemenuClass
self.contentArea = contentArea

self:GenerateSubmenuList(basemenuClass:GetVisibleSubmenus())
self:ResetSubmenuList()
self:ExtendSubmenuList(self.basemenuClass:GetVisibleNonSearchedSubmenus())
if self.basemenuClass:HasSearchbar() then
self:AddSearchTracker()
end
self:ExtendSubmenuList(self.basemenuClass:GetVisibleSubmenus())
self:SelectFirst()
self:InvalidateLayout(true)
end

---
Expand All @@ -172,6 +279,7 @@ function PANEL:SetPadding(padding)
self.padding = padding

self.navAreaScrollGrid:SetSpaceY(padding)
self.navAreaScrollGrid:DockPadding(0, padding, 0, padding)
end

---
Expand All @@ -188,10 +296,9 @@ function PANEL:PerformLayout()
self:InvalidateParent(true)

local widthNavContent, heightNavContent = self:GetSize()
local heightShift = heightNavHeader + (self.searchBar and heightNavButton + self.padding or 0)

self:SetSearchBarSize(widthNavContent, heightNavButton)
self.navAreaScroll:SetSize(widthNavContent, heightNavContent - heightShift)
self.navAreaScroll:SetSize(widthNavContent, heightNavContent)
self:SetSearchBarSize(self.navAreaScroll:InnerWidth(), heightNavButton)

-- Last invalidate all buttons and then the scrolllist for correct size to contents
self.navAreaScrollGrid:InvalidateChildren(true)
Expand Down
9 changes: 7 additions & 2 deletions gamemodes/terrortown/gamemode/server/sv_roleselection.lua
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,9 @@ function roleselection.GetSelectableRolesList(maxPlys, rolesAmountList)
local curRoles = 2 -- amount of roles, start with 2 because INNOCENT and TRAITOR are all the time available
local curBaseroles = 2 -- amount of base roles, ...

local layeredBaseRolesTbl = table.Copy(roleselection.baseroleLayers) -- layered roles list, the order defines the pick order. Just one role per layer is picked. Before a role is picked, the given layer is cleared (checked if the given roles are still selectable). Insert a table as a "or" list
-- layered roles list, the order defines the pick order. Just one role per layer is picked.
-- Before a role is picked, the given layer is cleared (checked if the given roles are still selectable). Insert a table as a "or" list
local layeredBaseRolesTbl = table.Copy(roleselection.baseroleLayers)

---
-- @realm server
Expand Down Expand Up @@ -531,8 +533,11 @@ function roleselection.GetSelectableRolesList(maxPlys, rolesAmountList)
-- if there are still defined layer
if #layeredBaseRolesTbl >= i then
for j = i, #layeredBaseRolesTbl do
-- clean the currently indexed layer (so that it just includes selectable roles),
-- because we working with predefined layers that probably includes roles that aren't
-- selectable with the current amount of players, etc.
local cleanedLayerTbl =
CleanupAvailableRolesLayerTbl(availableBaseRolesTbl, layeredBaseRolesTbl[i]) -- clean the currently indexed layer (so that it just includes selectable roles), because we working with predefined layers that probably includes roles that aren't selectable with the current amount of players, etc.
CleanupAvailableRolesLayerTbl(availableBaseRolesTbl, layeredBaseRolesTbl[i])

-- if there is no selectable role left in the current layer
if #cleanedLayerTbl < 1 then
Expand Down
4 changes: 2 additions & 2 deletions lua/terrortown/lang/de.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,7 @@ L.layering_not_layered = "Ohne Ebene"
L.layering_layer = "Ebene {layer}"
L.header_rolelayering_role = "{role}-Ebene"
L.header_rolelayering_baserole = "Basisrollenebenen"
L.submenu_administration_rolelayering_title = "Rollenebenen"
L.submenu_roles_rolelayering_title = "Rollenebenen"
L.header_rolelayering_info = "Rollenebeneninformationen"
L.help_rolelayering_roleselection = "Der Rollenverteilungsprozess ist in zwei Phasen unterteilt. In der ersten Phase werden Basisrollen verteilt, zu denen Unschuldige, Verräter und diejenigen gehören, welche in der 'Basisrollenebene' unten aufgeführt sind. Die zweite Phase dient dazu, diese Basisrollen zu Unterrollen aufzuwerten."
L.help_rolelayering_layers = "Aus jeder Ebene wird nur eine Rolle ausgewählt. Zuerst werden die Rollen aus den benutzerdefinierten Ebenen verteilt, beginnend mit der ersten Ebene, bis die letzte erreicht ist oder keine Rollen mehr aufgewertet werden können. Was auch immer zuerst passiert, wenn noch aufwertbare Slots verfügbar sind, werden auch die nicht geschichteten Rollen verteilt."
Expand Down Expand Up @@ -1461,7 +1461,7 @@ Der kleine Indikator oben links zeigt an, ob das Spielermodell eine Kopftrefferb
L.menu_roles_title = "Rollen Einstellungen"
L.menu_roles_description = "Richte Rollenspawning, Ausrüsungspunkte und mehr ein."

L.submenu_administration_roles_general_title = "Allgemeine Rolleneinstellungen"
L.submenu_roles_roles_general_title = "Allgemeine Rolleneinstellungen"

L.header_roles_info = "Roleninformationen"
L.header_roles_selection = "Rollenauswahlparameter"
Expand Down
Loading

0 comments on commit 7e45997

Please sign in to comment.