Skip to content

Commit 1494d28

Browse files
nike4613TimGollHistalek
committed
Role distribution inspection (TTT-2#1714)
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) TTT-2#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 <github@timgoll.de> Co-authored-by: Histalek <16392835+Histalek@users.noreply.github.com>
1 parent c466acd commit 1494d28

File tree

13 files changed

+1943
-51
lines changed

13 files changed

+1943
-51
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ All notable changes to TTT2 will be documented here. Inspired by [keep a changel
1212
- Added `GM:TTT2PlayDeathScream` hook to cancel or overwrite/change the deathscream sound that plays, when you die (by @NickCloudAT)
1313
- Added support for "toggle_zoom" binds to trigger the radio commands menu (by @TW1STaL1CKY)
1414
- Added option to use right click to enable/disable roles in the role layering menu (by @TimGoll)
15+
- Added a menu to allow admins to inspect, in detail, how and why roles are distributed as they are (by @nike4613)
1516
- Added option to enable team name next to role name on the HUD (by @milkwxter)
1617
- Added score event for winning with configurable role parameter (by @MrXonte)
1718

gamemodes/terrortown/gamemode/client/cl_main.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ ttt_include("cl_vskin__vgui__dprofilepanel")
8585
ttt_include("cl_vskin__vgui__dinfoitem")
8686
ttt_include("cl_vskin__vgui__dsubmenulist")
8787
ttt_include("cl_vskin__vgui__dweaponpreview")
88+
ttt_include("cl_vskin__vgui__dpippanel")
89+
ttt_include("cl_vskin__vgui__dplayergraph")
8890

8991
ttt_include("cl_changes")
9092
ttt_include("cl_network_sync")

gamemodes/terrortown/gamemode/client/cl_vskin/default_skin.lua

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,15 +1345,24 @@ function SKIN:PaintTooltipTTT2(panel, w, h)
13451345
)
13461346

13471347
if panel:HasText() then
1348-
drawSimpleText(
1349-
TryT(panel:GetText()),
1350-
panel:GetFont(),
1351-
0.5 * w,
1352-
0.5 * (h + sizeArrow),
1353-
utilGetDefaultColor(colors.background),
1354-
TEXT_ALIGN_CENTER,
1355-
TEXT_ALIGN_CENTER
1356-
)
1348+
local text = TryT(panel:GetText())
1349+
local textColor = utilGetDefaultColor(colors.background)
1350+
1351+
local wrapped = drawGetWrappedText(text, ScrW() - 20, panel:GetFont())
1352+
local _, lineHeight = drawGetTextSize("", panel:GetFont())
1353+
local y = 4 + sizeArrow
1354+
for i = 1, #wrapped do
1355+
drawSimpleText(
1356+
wrapped[i],
1357+
panel:GetFont(),
1358+
10,
1359+
y,
1360+
textColor,
1361+
TEXT_ALIGN_LEFT,
1362+
TEXT_ALIGN_TOP
1363+
)
1364+
y = y + lineHeight
1365+
end
13571366
end
13581367
end
13591368

@@ -2293,5 +2302,84 @@ function SKIN:PaintWeaponPreviewTTT2(panel, w, h)
22932302
end
22942303
end
22952304

2305+
---
2306+
-- @param Panel panel
2307+
-- @param number w
2308+
-- @param number h
2309+
-- @realm client
2310+
function SKIN:PaintPlayerGraphTTT2(panel, w, h)
2311+
local renderData = panel.renderData
2312+
local padding = panel:GetPadding()
2313+
2314+
if panel.title ~= "" then
2315+
-- title text
2316+
drawSimpleText(
2317+
panel.title,
2318+
panel:GetFont(),
2319+
renderData.titleX,
2320+
renderData.titleY,
2321+
colors.helpText,
2322+
TEXT_ALIGN_LEFT,
2323+
TEXT_ALIGN_TOP
2324+
)
2325+
end
2326+
2327+
local barColor = utilGetChangedColor(colors.background, 30)
2328+
local valueInsideColor = utilGetDefaultColor(barColor)
2329+
local valueOutsideColor = utilGetDefaultColor(colors.background)
2330+
2331+
if renderData.sepY then
2332+
-- title separator
2333+
drawBox(0, renderData.sepY, w, 1, barColor)
2334+
end
2335+
2336+
local hBarColor = colors.accent
2337+
local hValueInsideColor = colors.accentText
2338+
2339+
-- then the items
2340+
for i = 1, #renderData.order do
2341+
local item = renderData.order[i]
2342+
-- first, draw the bar
2343+
local thisBarColor
2344+
if item.data.highlight then
2345+
thisBarColor = hBarColor
2346+
else
2347+
thisBarColor = barColor
2348+
end
2349+
drawBox(item.x, item.y, item.w, item.h, thisBarColor)
2350+
-- then the value text
2351+
if item.valueWidth > w - item.x - item.w - padding then
2352+
-- the value would take up too much space outside, put it inside
2353+
local thisTextCol
2354+
if item.data.highlight then
2355+
thisTextCol = hValueInsideColor
2356+
else
2357+
thisTextCol = valueInsideColor
2358+
end
2359+
local x = item.x + item.w - item.valueWidth - padding
2360+
drawSimpleText(
2361+
tostring(item.data.value),
2362+
panel:GetFont(),
2363+
x,
2364+
item.y,
2365+
thisTextCol,
2366+
TEXT_ALIGN_LEFT,
2367+
TEXT_ALIGN_TOP
2368+
)
2369+
else
2370+
-- the value will fit outside the bar, draw it there
2371+
drawSimpleText(
2372+
tostring(item.data.value),
2373+
panel:GetFont(),
2374+
item.x + item.w + padding,
2375+
item.y,
2376+
valueOutsideColor,
2377+
TEXT_ALIGN_LEFT,
2378+
TEXT_ALIGN_TOP
2379+
)
2380+
end
2381+
end
2382+
end
2383+
22962384
-- REGISTER DERMA SKIN
22972385
derma.DefineSkin(SKIN.Name, "TTT2 default skin for all vgui elements", SKIN)
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
---
2+
-- @class PANEL
3+
-- @section DPiPPanelTTT2
4+
5+
local PANEL = {}
6+
7+
---
8+
-- @accessor string
9+
-- @realm client
10+
AccessorFunc(PANEL, "Padding", "Padding", FORCE_NUMBER)
11+
---
12+
-- @accessor number
13+
-- @realm client
14+
AccessorFunc(PANEL, "innerPadding", "InnerPadding", FORCE_NUMBER)
15+
---
16+
-- @accessor number
17+
-- @realm client
18+
AccessorFunc(PANEL, "pipOuterOffset", "OuterOffset", FORCE_NUMBER)
19+
20+
---
21+
-- @ignore
22+
function PANEL:Init()
23+
self:SetPadding(0)
24+
self:SetInnerPadding(2)
25+
self:SetOuterOffset(0)
26+
27+
self.mainPanel = nil
28+
self.subPanels = {}
29+
end
30+
31+
---
32+
-- 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.
33+
-- @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.
34+
-- @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).
35+
-- @return The added panel.
36+
-- @realm client
37+
function PANEL:Add(pnl, ...)
38+
local realPnl
39+
if ispanel(pnl) then
40+
realPnl = pnl
41+
pnl:SetParent(self)
42+
elseif istable(pnl) then
43+
realPnl = vgui.CreateFromTable(pnl, self)
44+
else
45+
realPnl = vgui.Create(pnl, self)
46+
end
47+
48+
if self.mainPanel == nil then
49+
-- the first panel added becomes the main panel
50+
self.mainPanel = realPnl
51+
self:InvalidateLayout()
52+
return realPnl
53+
end
54+
55+
-- later panels become sub-panels
56+
self.subPanels[#self.subPanels + 1] = {
57+
pnl = realPnl,
58+
align = { ... },
59+
}
60+
61+
self:InvalidateLayout()
62+
self:InvalidateChildren()
63+
64+
return realPnl
65+
end
66+
67+
---
68+
-- Clears this panel.
69+
-- @realm client
70+
function PANEL:Clear()
71+
if self.mainPanel then
72+
self.mainPanel:Remove()
73+
self.mainPanel = nil
74+
end
75+
for _, pnl in pairs(self.subPanels) do
76+
pnl.pnl:Remove()
77+
end
78+
self.subPanels = {}
79+
end
80+
81+
local function RectsOverlap(r1, r2)
82+
local x11, y11 = r1.x, r1.y
83+
local x12, y12 = r1.x + r1.w, r1.y + r1.h
84+
local x21, y21 = r2.x, r2.y
85+
local x22, y22 = r2.x + r2.w, r2.y + r2.h
86+
87+
-- take the max/min coords, if width or height is negative there is no overlap
88+
local xn1 = math.max(x11, x21)
89+
local yn1 = math.max(y11, y21)
90+
local xn2 = math.min(x12, x22)
91+
local yn2 = math.min(y12, y22)
92+
93+
-- note that exact edge meets are considered overlaps for this
94+
return xn2 >= xn1 and yn2 >= yn1
95+
end
96+
97+
local axisX = 1
98+
local axisY = 2
99+
local axisCoordTbl = {
100+
[axisX] = "x",
101+
[axisY] = "y",
102+
}
103+
local axisSizeTbl = {
104+
[axisX] = "w",
105+
[axisY] = "h",
106+
}
107+
108+
---
109+
-- @ignore
110+
function PANEL:PerformLayout()
111+
local mainPanel = self.mainPanel
112+
if not IsValid(mainPanel) then
113+
-- no main panel, nothing to actually layout
114+
return
115+
end
116+
117+
local mw, mh = mainPanel:GetSize()
118+
local padding = self:GetPadding()
119+
local innerPadding = self:GetInnerPadding()
120+
local outerOffset = self:GetOuterOffset()
121+
122+
-- we can't immediately set our own size or the main panel's position because we
123+
-- need to look at the sub-panels first
124+
local leftBound = -padding
125+
local rightBound = mw + padding
126+
local topBound = -padding
127+
local bottomBound = mh + padding
128+
129+
-- subpanel positions relative to the TL corner of the main panel
130+
-- stores panel->hitbox index
131+
-- need this because we need to go through the subpanels to determine where the
132+
-- main panel will go
133+
local subPnlRelPos = {}
134+
local subPnlHitboxes = {}
135+
136+
for i, pnl in ipairs(self.subPanels) do
137+
-- align center is 0, 0; -1 = left/top, +1 = right/bottom
138+
local alignx, aligny = 0, 0
139+
140+
local preferAxis = 0 -- the preferred direction; the first one provided
141+
142+
if istable(pnl.align) then
143+
-- loop through the alignments to identify it
144+
for _, align in ipairs(pnl.align) do
145+
local axis = 0
146+
if align == TOP then
147+
axis = axisY
148+
aligny = -1
149+
elseif align == BOTTOM then
150+
axis = axisY
151+
aligny = 1
152+
elseif align == LEFT then
153+
axis = axisX
154+
alignx = -1
155+
elseif align == RIGHT then
156+
axis = axisX
157+
alignx = 1
158+
end
159+
160+
if axis ~= 0 and preferAxis == 0 then
161+
preferAxis = axis
162+
end
163+
end
164+
end
165+
166+
-- if no alignment axis was specified, use the X axis
167+
if preferAxis == 0 then
168+
preferAxis = axisX
169+
end
170+
171+
-- we now have a usable understanding of the alignment request
172+
173+
-- time to figure out where to put the subpanel
174+
local pw, ph = pnl.pnl:GetSize()
175+
176+
-- start with the preferred position
177+
-- outermost corner/edge
178+
local outerx = (mw / 2) + alignx * (mw / 2)
179+
local outery = (mh / 2) + aligny * (mh / 2)
180+
-- then adjust because we need the top-left corner always
181+
local x = outerx - ((alignx + 1) * (pw / 2))
182+
local y = outery - ((aligny + 1) * (ph / 2))
183+
-- then adjust according to the outerOffset
184+
x = x + alignx * outerOffset
185+
y = y + aligny * outerOffset
186+
187+
local rect = { x = x, y = y, w = pw, h = ph }
188+
189+
-- now that we've used the alignment properly, make sure the axes are nonzero
190+
-- so that our preferred axis move actually moves it
191+
if alignx == 0 then
192+
alignx = -1
193+
end
194+
if aligny == 0 then
195+
aligny = -1
196+
end
197+
198+
local paxisCoord = axisCoordTbl[preferAxis]
199+
local paxisSize = axisSizeTbl[preferAxis]
200+
local paxisAlign = preferAxis == axisX and alignx or aligny
201+
202+
-- now that we have our target rectangle, check for intersections against all
203+
-- existing rectangles and adjust along the preferred axis to not overlap
204+
for j = 1, #subPnlHitboxes do
205+
local box = subPnlHitboxes[j]
206+
207+
if RectsOverlap(rect, box) then
208+
-- overlap, move rect to be non-overlapping along the target axis
209+
210+
-- to do so, we first set the relevant axis to the box
211+
rect[paxisCoord] = box[paxisCoord]
212+
213+
-- then, we adjust position by the width accodring to the align value
214+
rect[paxisCoord] = rect[paxisCoord]
215+
+ -paxisAlign * ((paxisAlign < 0 and box or rect)[paxisSize] + innerPadding)
216+
end
217+
end
218+
219+
-- we now have a good position for this item, update the bounds
220+
leftBound = math.min(leftBound, rect.x)
221+
rightBound = math.max(rightBound, rect.x + rect.w)
222+
topBound = math.min(topBound, rect.y)
223+
bottomBound = math.max(bottomBound, rect.y + rect.h)
224+
225+
-- and add the box and panel entry
226+
subPnlHitboxes[#subPnlHitboxes + 1] = rect
227+
subPnlRelPos[pnl.pnl] = #subPnlHitboxes
228+
end
229+
230+
-- we now know how to position and size everything, so do that
231+
local mainx, mainy = -leftBound, -topBound
232+
self:SetSize(rightBound - leftBound, bottomBound - topBound)
233+
mainPanel:SetPos(mainx, mainy)
234+
235+
for pnl, recti in pairs(subPnlRelPos) do
236+
local rect = subPnlHitboxes[recti]
237+
pnl:SetPos(mainx + rect.x, mainy + rect.y)
238+
end
239+
end
240+
241+
derma.DefineControl("DPiPPanelTTT2", "A panel-in-panel panel.", PANEL, "DPanelTTT2")

0 commit comments

Comments
 (0)