Skip to content

Commit

Permalink
Role distribution inspection (#1714)
Browse files Browse the repository at this point in the history
This PR adds an admin UI to inspect how and why role distribution
selects certain roles, and why it distributes those roles to players.

The goal here is to assist admins in understanding the role distribution
process, so they can more effectively tweak the parameters involved, and
better see their effects.

This also includes a graphic showing player weights during role
distribution (if derandomization is enabled) which can assist debugging
that as well as help intuition when tweaking it.

Some of the graphics are a touch jank, and in particular I wish there
was a good way to get an icon in the label of a collapsible form, but
just role names probably works fine anyway.

This PR is based on (and soft depends-on) #1702. That dependency can be
removed if necessary.

Screenshots:

![Screenshot_20250111_221601](https://github.com/user-attachments/assets/3238a7ae-68ad-4e32-89f2-3c88ae47faea)

![Screenshot_20250111_222215](https://github.com/user-attachments/assets/623eac41-db13-45d0-a8aa-46815bc72603)

![Screenshot_20250111_222252](https://github.com/user-attachments/assets/cae1e417-d63b-44fe-9bc0-73be97007d0c)

![Screenshot_20250111_222312](https://github.com/user-attachments/assets/1f4ca740-f9c7-4ded-be63-da09b73d98b3)

![Screenshot_20250111_222324](https://github.com/user-attachments/assets/b34f0688-5423-4bf9-8bba-8fd312631056)

![Screenshot_20250111_222328](https://github.com/user-attachments/assets/2e9c9ba4-ddfa-43ea-b492-c5a59f0b8bcc)

![Screenshot_20250111_221616](https://github.com/user-attachments/assets/e63513b4-5852-4a6e-a578-abbbf9b95ddd)

![Screenshot_20250111_221629](https://github.com/user-attachments/assets/980540be-54e9-4268-93cd-687ea3be8611)

![Screenshot_20250111_221656](https://github.com/user-attachments/assets/e1f1798f-4e68-4ad2-b54c-61751d64d714)

![Screenshot_20250111_221731](https://github.com/user-attachments/assets/7884d52a-c323-41ed-8d9b-137627c8574c)

![Screenshot_20250111_221747](https://github.com/user-attachments/assets/90405fac-f055-4d97-93ea-da771c6bbfbc)

---------

Co-authored-by: Tim Goll <[email protected]>
Co-authored-by: Histalek <[email protected]>
  • Loading branch information
3 people authored Jan 26, 2025
1 parent 215ad5a commit e32af20
Show file tree
Hide file tree
Showing 13 changed files with 1,943 additions and 51 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to TTT2 will be documented here. Inspired by [keep a changel
- Added `GM:TTT2PlayDeathScream` hook to cancel or overwrite/change the deathscream sound that plays, when you die (by @NickCloudAT)
- Added support for "toggle_zoom" binds to trigger the radio commands menu (by @TW1STaL1CKY)
- Added option to use right click to enable/disable roles in the role layering menu (by @TimGoll)
- Added a menu to allow admins to inspect, in detail, how and why roles are distributed as they are (by @nike4613)
- Added option to enable team name next to role name on the HUD (by @milkwxter)
- Added score event for winning with configurable role parameter (by @MrXonte)

Expand Down
2 changes: 2 additions & 0 deletions gamemodes/terrortown/gamemode/client/cl_main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ ttt_include("cl_vskin__vgui__dprofilepanel")
ttt_include("cl_vskin__vgui__dinfoitem")
ttt_include("cl_vskin__vgui__dsubmenulist")
ttt_include("cl_vskin__vgui__dweaponpreview")
ttt_include("cl_vskin__vgui__dpippanel")
ttt_include("cl_vskin__vgui__dplayergraph")

ttt_include("cl_changes")
ttt_include("cl_network_sync")
Expand Down
106 changes: 97 additions & 9 deletions gamemodes/terrortown/gamemode/client/cl_vskin/default_skin.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1345,15 +1345,24 @@ function SKIN:PaintTooltipTTT2(panel, w, h)
)

if panel:HasText() then
drawSimpleText(
TryT(panel:GetText()),
panel:GetFont(),
0.5 * w,
0.5 * (h + sizeArrow),
utilGetDefaultColor(colors.background),
TEXT_ALIGN_CENTER,
TEXT_ALIGN_CENTER
)
local text = TryT(panel:GetText())
local textColor = utilGetDefaultColor(colors.background)

local wrapped = drawGetWrappedText(text, ScrW() - 20, panel:GetFont())
local _, lineHeight = drawGetTextSize("", panel:GetFont())
local y = 4 + sizeArrow
for i = 1, #wrapped do
drawSimpleText(
wrapped[i],
panel:GetFont(),
10,
y,
textColor,
TEXT_ALIGN_LEFT,
TEXT_ALIGN_TOP
)
y = y + lineHeight
end
end
end

Expand Down Expand Up @@ -2293,5 +2302,84 @@ function SKIN:PaintWeaponPreviewTTT2(panel, w, h)
end
end

---
-- @param Panel panel
-- @param number w
-- @param number h
-- @realm client
function SKIN:PaintPlayerGraphTTT2(panel, w, h)
local renderData = panel.renderData
local padding = panel:GetPadding()

if panel.title ~= "" then
-- title text
drawSimpleText(
panel.title,
panel:GetFont(),
renderData.titleX,
renderData.titleY,
colors.helpText,
TEXT_ALIGN_LEFT,
TEXT_ALIGN_TOP
)
end

local barColor = utilGetChangedColor(colors.background, 30)
local valueInsideColor = utilGetDefaultColor(barColor)
local valueOutsideColor = utilGetDefaultColor(colors.background)

if renderData.sepY then
-- title separator
drawBox(0, renderData.sepY, w, 1, barColor)
end

local hBarColor = colors.accent
local hValueInsideColor = colors.accentText

-- then the items
for i = 1, #renderData.order do
local item = renderData.order[i]
-- first, draw the bar
local thisBarColor
if item.data.highlight then
thisBarColor = hBarColor
else
thisBarColor = barColor
end
drawBox(item.x, item.y, item.w, item.h, thisBarColor)
-- then the value text
if item.valueWidth > w - item.x - item.w - padding then
-- the value would take up too much space outside, put it inside
local thisTextCol
if item.data.highlight then
thisTextCol = hValueInsideColor
else
thisTextCol = valueInsideColor
end
local x = item.x + item.w - item.valueWidth - padding
drawSimpleText(
tostring(item.data.value),
panel:GetFont(),
x,
item.y,
thisTextCol,
TEXT_ALIGN_LEFT,
TEXT_ALIGN_TOP
)
else
-- the value will fit outside the bar, draw it there
drawSimpleText(
tostring(item.data.value),
panel:GetFont(),
item.x + item.w + padding,
item.y,
valueOutsideColor,
TEXT_ALIGN_LEFT,
TEXT_ALIGN_TOP
)
end
end
end

-- REGISTER DERMA SKIN
derma.DefineSkin(SKIN.Name, "TTT2 default skin for all vgui elements", SKIN)
241 changes: 241 additions & 0 deletions gamemodes/terrortown/gamemode/client/cl_vskin/vgui/dpippanel_ttt2.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
---
-- @class PANEL
-- @section DPiPPanelTTT2

local PANEL = {}

---
-- @accessor string
-- @realm client
AccessorFunc(PANEL, "Padding", "Padding", FORCE_NUMBER)
---
-- @accessor number
-- @realm client
AccessorFunc(PANEL, "innerPadding", "InnerPadding", FORCE_NUMBER)
---
-- @accessor number
-- @realm client
AccessorFunc(PANEL, "pipOuterOffset", "OuterOffset", FORCE_NUMBER)

---
-- @ignore
function PANEL:Init()
self:SetPadding(0)
self:SetInnerPadding(2)
self:SetOuterOffset(0)

self.mainPanel = nil
self.subPanels = {}
end

---
-- Adds a child panel. The first such panel is the "main" panel, and appears larger than the others. All others are positioned over the main, according to their alignment.
-- @param Panel|string|table pnl The panel to add. If it is an actual panel, added directly. Otherwise, the name/table of a panel to create.
-- @param DOCK ... Alignment. Only LEFT, RIGHT, TOP, and BOTTOM are valid. The first specified direction specifies the preferred axis (the axis along which it will be moved to prevent overlap).
-- @return The added panel.
-- @realm client
function PANEL:Add(pnl, ...)
local realPnl
if ispanel(pnl) then
realPnl = pnl
pnl:SetParent(self)
elseif istable(pnl) then
realPnl = vgui.CreateFromTable(pnl, self)
else
realPnl = vgui.Create(pnl, self)
end

if self.mainPanel == nil then
-- the first panel added becomes the main panel
self.mainPanel = realPnl
self:InvalidateLayout()
return realPnl
end

-- later panels become sub-panels
self.subPanels[#self.subPanels + 1] = {
pnl = realPnl,
align = { ... },
}

self:InvalidateLayout()
self:InvalidateChildren()

return realPnl
end

---
-- Clears this panel.
-- @realm client
function PANEL:Clear()
if self.mainPanel then
self.mainPanel:Remove()
self.mainPanel = nil
end
for _, pnl in pairs(self.subPanels) do
pnl.pnl:Remove()
end
self.subPanels = {}
end

local function RectsOverlap(r1, r2)
local x11, y11 = r1.x, r1.y
local x12, y12 = r1.x + r1.w, r1.y + r1.h
local x21, y21 = r2.x, r2.y
local x22, y22 = r2.x + r2.w, r2.y + r2.h

-- take the max/min coords, if width or height is negative there is no overlap
local xn1 = math.max(x11, x21)
local yn1 = math.max(y11, y21)
local xn2 = math.min(x12, x22)
local yn2 = math.min(y12, y22)

-- note that exact edge meets are considered overlaps for this
return xn2 >= xn1 and yn2 >= yn1
end

local axisX = 1
local axisY = 2
local axisCoordTbl = {
[axisX] = "x",
[axisY] = "y",
}
local axisSizeTbl = {
[axisX] = "w",
[axisY] = "h",
}

---
-- @ignore
function PANEL:PerformLayout()
local mainPanel = self.mainPanel
if not IsValid(mainPanel) then
-- no main panel, nothing to actually layout
return
end

local mw, mh = mainPanel:GetSize()
local padding = self:GetPadding()
local innerPadding = self:GetInnerPadding()
local outerOffset = self:GetOuterOffset()

-- we can't immediately set our own size or the main panel's position because we
-- need to look at the sub-panels first
local leftBound = -padding
local rightBound = mw + padding
local topBound = -padding
local bottomBound = mh + padding

-- subpanel positions relative to the TL corner of the main panel
-- stores panel->hitbox index
-- need this because we need to go through the subpanels to determine where the
-- main panel will go
local subPnlRelPos = {}
local subPnlHitboxes = {}

for i, pnl in ipairs(self.subPanels) do
-- align center is 0, 0; -1 = left/top, +1 = right/bottom
local alignx, aligny = 0, 0

local preferAxis = 0 -- the preferred direction; the first one provided

if istable(pnl.align) then
-- loop through the alignments to identify it
for _, align in ipairs(pnl.align) do
local axis = 0
if align == TOP then
axis = axisY
aligny = -1
elseif align == BOTTOM then
axis = axisY
aligny = 1
elseif align == LEFT then
axis = axisX
alignx = -1
elseif align == RIGHT then
axis = axisX
alignx = 1
end

if axis ~= 0 and preferAxis == 0 then
preferAxis = axis
end
end
end

-- if no alignment axis was specified, use the X axis
if preferAxis == 0 then
preferAxis = axisX
end

-- we now have a usable understanding of the alignment request

-- time to figure out where to put the subpanel
local pw, ph = pnl.pnl:GetSize()

-- start with the preferred position
-- outermost corner/edge
local outerx = (mw / 2) + alignx * (mw / 2)
local outery = (mh / 2) + aligny * (mh / 2)
-- then adjust because we need the top-left corner always
local x = outerx - ((alignx + 1) * (pw / 2))
local y = outery - ((aligny + 1) * (ph / 2))
-- then adjust according to the outerOffset
x = x + alignx * outerOffset
y = y + aligny * outerOffset

local rect = { x = x, y = y, w = pw, h = ph }

-- now that we've used the alignment properly, make sure the axes are nonzero
-- so that our preferred axis move actually moves it
if alignx == 0 then
alignx = -1
end
if aligny == 0 then
aligny = -1
end

local paxisCoord = axisCoordTbl[preferAxis]
local paxisSize = axisSizeTbl[preferAxis]
local paxisAlign = preferAxis == axisX and alignx or aligny

-- now that we have our target rectangle, check for intersections against all
-- existing rectangles and adjust along the preferred axis to not overlap
for j = 1, #subPnlHitboxes do
local box = subPnlHitboxes[j]

if RectsOverlap(rect, box) then
-- overlap, move rect to be non-overlapping along the target axis

-- to do so, we first set the relevant axis to the box
rect[paxisCoord] = box[paxisCoord]

-- then, we adjust position by the width accodring to the align value
rect[paxisCoord] = rect[paxisCoord]
+ -paxisAlign * ((paxisAlign < 0 and box or rect)[paxisSize] + innerPadding)
end
end

-- we now have a good position for this item, update the bounds
leftBound = math.min(leftBound, rect.x)
rightBound = math.max(rightBound, rect.x + rect.w)
topBound = math.min(topBound, rect.y)
bottomBound = math.max(bottomBound, rect.y + rect.h)

-- and add the box and panel entry
subPnlHitboxes[#subPnlHitboxes + 1] = rect
subPnlRelPos[pnl.pnl] = #subPnlHitboxes
end

-- we now know how to position and size everything, so do that
local mainx, mainy = -leftBound, -topBound
self:SetSize(rightBound - leftBound, bottomBound - topBound)
mainPanel:SetPos(mainx, mainy)

for pnl, recti in pairs(subPnlRelPos) do
local rect = subPnlHitboxes[recti]
pnl:SetPos(mainx + rect.x, mainy + rect.y)
end
end

derma.DefineControl("DPiPPanelTTT2", "A panel-in-panel panel.", PANEL, "DPanelTTT2")
Loading

0 comments on commit e32af20

Please sign in to comment.