Files
wnsrc/gamemodes/terrortown/gamemode/cl_scoring.lua
lifestorm 94063e4369 Upload
2024-08-04 22:55:00 +03:00

631 lines
17 KiB
Lua

--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
-- Game report
include("cl_awards.lua")
local table = table
local string = string
local vgui = vgui
local pairs = pairs
CLSCORE = {}
CLSCORE.Events = {}
CLSCORE.Scores = {}
CLSCORE.TraitorIDs = {}
CLSCORE.DetectiveIDs = {}
CLSCORE.Players = {}
CLSCORE.StartTime = 0
CLSCORE.Panel = nil
CLSCORE.EventDisplay = {}
include("scoring_shd.lua")
local skull_icon = Material("HUD/killicons/default")
surface.CreateFont("WinHuge", {
font = "Trebuchet24",
size = 72,
weight = 1000,
shadow = true,
extended = true
})
-- so much text here I'm using shorter names than usual
local T = LANG.GetTranslation
local PT = LANG.GetParamTranslation
function CLSCORE:GetDisplay(key, event)
local displayfns = self.EventDisplay[event.id]
if not displayfns then return end
local keyfn = displayfns[key]
if not keyfn then return end
return keyfn(event)
end
function CLSCORE:TextForEvent(e)
return self:GetDisplay("text", e)
end
function CLSCORE:IconForEvent(e)
return self:GetDisplay("icon", e)
end
function CLSCORE:TimeForEvent(e)
local t = e.t - self.StartTime
if t >= 0 then
return util.SimpleTime(t, "%02i:%02i")
else
return " "
end
end
-- Tell CLSCORE how to display an event. See cl_scoring_events for examples.
-- Pass an empty table to keep an event from showing up.
function CLSCORE.DeclareEventDisplay(event_id, event_fns)
-- basic input vetting, can't check returned value types because the
-- functions may be impure
if not tonumber(event_id) then
error("Event ??? display: invalid event id", 2)
end
if not istable(event_fns) then
error(string.format("Event %d display: no display functions found.", event_id), 2)
end
if not event_fns.text then
error(string.format("Event %d display: no text display function found.", event_id), 2)
end
if not event_fns.icon then
error(string.format("Event %d display: no icon and tooltip display function found.", event_id), 2)
end
CLSCORE.EventDisplay[event_id] = event_fns
end
function CLSCORE:FillDList(dlst)
local events = self.Events
for i = 1, #events do
local e = events[i]
local etxt = self:TextForEvent(e)
local eicon, ttip = self:IconForEvent(e)
local etime = self:TimeForEvent(e)
if etxt then
if eicon then
local mat = eicon
eicon = vgui.Create("DImage")
eicon:SetMaterial(mat)
eicon:SetTooltip(ttip)
eicon:SetKeepAspect(true)
eicon:SizeToContents()
end
dlst:AddLine(etime, eicon, " " .. etxt)
end
end
end
function CLSCORE:BuildEventLogPanel(dpanel)
local margin = 10
local w, h = dpanel:GetSize()
local dlist = vgui.Create("DListView", dpanel)
dlist:SetPos(0, 0)
dlist:SetSize(w, h - margin*2)
dlist:SetSortable(true)
dlist:SetMultiSelect(false)
local timecol = dlist:AddColumn(T("col_time"))
local iconcol = dlist:AddColumn("")
local eventcol = dlist:AddColumn(T("col_event"))
iconcol:SetFixedWidth(16)
timecol:SetFixedWidth(40)
-- If sortable is off, no background is drawn for the headers which looks
-- terrible. So enable it, but disable the actual use of sorting.
iconcol.Header:SetDisabled(true)
timecol.Header:SetDisabled(true)
eventcol.Header:SetDisabled(true)
self:FillDList(dlist)
end
CLSCORE.ScorePanelNames = {
"",
"col_player",
"col_role",
"col_kills1",
"col_kills2",
"col_points",
"col_team",
"col_total"
}
CLSCORE.ScorePanelColor = Color(150, 50, 50)
function CLSCORE:BuildScorePanel(dpanel)
local margin = 10
local w, h = dpanel:GetSize()
local dlist = vgui.Create("DListView", dpanel)
dlist:SetPos(0, 0)
dlist:SetSize(w, h)
dlist:SetSortable(true)
dlist:SetMultiSelect(false)
local scorenames = self.ScorePanelNames
for i = 1, #scorenames do
local name = scorenames[i]
if isstring(name) then
if name == "" then
-- skull icon column
local c = dlist:AddColumn("")
c:SetFixedWidth(18)
else
dlist:AddColumn(T(name))
end
end
end
-- the type of win condition triggered is relevant for team bonus
local wintype = WIN_NONE
local events = self.Events
for i = #events, 1, -1 do
local e = self.Events[i]
if e.id == EVENT_FINISH then
wintype = e.win
break
end
end
local scores = self.Scores
local nicks = self.Players
local bonus = ScoreTeamBonus(scores, wintype)
for id, s in pairs(scores) do
if id != -1 then
local was_traitor = s.was_traitor
local role = was_traitor and T("traitor") or (s.was_detective and T("detective") or "")
local surv = ""
if s.deaths > 0 then
surv = vgui.Create("ColoredBox", dlist)
surv:SetColor(self.ScorePanelColor)
surv:SetBorder(false)
surv:SetSize(18,18)
local skull = vgui.Create("DImage", surv)
skull:SetMaterial(skull_icon)
skull:SetTooltip("Dead")
skull:SetKeepAspect(true)
skull:SetSize(18,18)
end
local points_own = KillsToPoints(s, was_traitor)
local points_team = (was_traitor and bonus.traitors or bonus.innos)
local points_total = points_own + points_team
local l = dlist:AddLine(surv, nicks[id], role, s.innos, s.traitors, points_own, points_team, points_total)
-- center align
for k, col in pairs(l.Columns) do
col:SetContentAlignment(5)
end
-- when sorting on the column showing survival, we would get an error
-- because images can't be sorted, so instead hack in a dummy value
local surv_col = l.Columns[1]
if surv_col then
surv_col.Value = TypeID(surv_col.Value) == TYPE_PANEL and "1" or "0"
end
end
end
dlist:SortByColumn(6)
end
function CLSCORE:AddAward(y, pw, award, dpanel)
local nick = award.nick
local text = award.text
local title = string.upper(award.title)
local titlelbl = vgui.Create("DLabel", dpanel)
titlelbl:SetText(title)
titlelbl:SetFont("TabLarge")
titlelbl:SizeToContents()
local tiw, tih = titlelbl:GetSize()
local nicklbl = vgui.Create("DLabel", dpanel)
nicklbl:SetText(nick)
nicklbl:SetFont("DermaDefaultBold")
nicklbl:SizeToContents()
local nw, nh = nicklbl:GetSize()
local txtlbl = vgui.Create("DLabel", dpanel)
txtlbl:SetText(text)
txtlbl:SetFont("DermaDefault")
txtlbl:SizeToContents()
local tw, th = txtlbl:GetSize()
titlelbl:SetPos((pw - tiw) / 2, y)
y = y + tih + 2
local fw = nw + tw + 5
local fx = ((pw - fw) / 2)
nicklbl:SetPos(fx, y)
txtlbl:SetPos(fx + nw + 5, y)
y = y + nh
return y
end
-- double check that we have no nils
local function ValidAward(a)
return istable(a) and isstring(a.nick) and isstring(a.text) and isstring(a.title) and isnumber(a.priority)
end
CLSCORE.WinTypes = {
[WIN_INNOCENT] = {
Text = "hilite_win_innocent",
BoxColor = Color(5, 190, 5, 255),
TextColor = COLOR_WHITE,
BackgroundColor = Color(50, 50, 50, 255)
},
[WIN_TRAITOR] = {
Text = "hilite_win_traitors",
BoxColor = Color(190, 5, 5, 255),
TextColor = COLOR_WHITE,
BackgroundColor = Color(50, 50, 50, 255)
}
}
-- when win is due to timeout, innocents win
CLSCORE.WinTypes[WIN_TIMELIMIT] = CLSCORE.WinTypes[WIN_INNOCENT]
-- The default wintype if no EVENT_FINISH is specified
CLSCORE.WinTypes.Default = CLSCORE.WinTypes[WIN_INNOCENT]
function CLSCORE:BuildHilitePanel(dpanel, title, starttime, endtime)
local w, h = dpanel:GetSize()
local numply = table.Count(self.Players)
local numtr = table.Count(self.TraitorIDs)
local bg = vgui.Create("ColoredBox", dpanel)
bg:SetColor(title.BackgroundColor or self.WinTypes.Default.BackgroundColor)
bg:SetSize(w,h)
bg:SetPos(0,0)
local winlbl = vgui.Create("DLabel", dpanel)
winlbl:SetFont("WinHuge")
winlbl:SetText( T(title.Text or self.WinTypes.Default.Text) )
winlbl:SetTextColor(title.TextColor or self.WinTypes.Default.TextColor)
winlbl:SizeToContents()
local xwin = (w - winlbl:GetWide())/2
local ywin = 30
winlbl:SetPos(xwin, ywin)
bg.PaintOver = function()
draw.RoundedBox(8, xwin - 15, ywin - 5, winlbl:GetWide() + 30, winlbl:GetTall() + 10, title.BoxColor or self.WinTypes.Default.BoxColor)
end
local ysubwin = ywin + winlbl:GetTall()
local partlbl = vgui.Create("DLabel", dpanel)
local plytxt = PT(numtr == 1 and "hilite_players2" or "hilite_players1",
{numplayers = numply, numtraitors = numtr})
partlbl:SetText(plytxt)
partlbl:SizeToContents()
partlbl:SetPos(xwin, ysubwin + 8)
local timelbl = vgui.Create("DLabel", dpanel)
timelbl:SetText(PT("hilite_duration", {time= util.SimpleTime(endtime - starttime, "%02i:%02i")}))
timelbl:SizeToContents()
timelbl:SetPos(xwin + winlbl:GetWide() - timelbl:GetWide(), ysubwin + 8)
-- Awards
local wa = math.Round(w * 0.9)
local ha = h - ysubwin - 40
local xa = (w - wa) / 2
local ya = h - ha
local awardp = vgui.Create("DPanel", dpanel)
awardp:SetSize(wa, ha)
awardp:SetPos(xa, ya)
awardp:SetPaintBackground(false)
-- Before we pick awards, seed the rng in a way that is the same on all
-- clients. We can do this using the round start time. To make it a bit more
-- random, involve the round's duration too.
math.randomseed(starttime + endtime)
-- Attempt to generate every award, then sort the succeeded ones based on
-- priority/interestingness
local award_choices = {}
for k, afn in pairs(AWARDS) do
local a = afn(self.Events, self.Scores, self.Players, self.TraitorIDs, self.DetectiveIDs)
if ValidAward(a) then
table.insert(award_choices, a)
end
end
local num_choices = table.Count(award_choices)
local max_awards = 5
-- sort descending by priority
table.SortByMember(award_choices, "priority")
-- put the N most interesting awards in the menu
for i=1,max_awards do
local a = award_choices[i]
if a then
self:AddAward((i - 1) * 42, wa, a, awardp)
end
end
end
function CLSCORE:ShowPanel()
if IsValid(self.Panel) then
self:ClearPanel()
end
local margin = 15
local dpanel = vgui.Create("DFrame")
local title = self.WinTypes.Default
local starttime = self.StartTime
local endtime = starttime
local events = self.Events
for i = #events, 1, -1 do
local e = events[i]
if e.id == EVENT_FINISH then
endtime = e.t
title = self.WinTypes[e.win]
break
end
end
-- size the panel based on the win text w/ 88px horizontal padding and 44px veritcal padding
surface.SetFont("WinHuge")
local w, h = surface.GetTextSize( T(title.Text or self.WinTypes.Default.Text) )
-- w + DPropertySheet padding (8) + winlbl padding (30) + offset margin (margin * 2) + size margin (margin)
w, h = math.max(700, w + 38 + margin * 3), 500
dpanel:SetSize(w, h)
dpanel:Center()
dpanel:SetTitle(T("report_title"))
dpanel:SetVisible(true)
dpanel:ShowCloseButton(true)
dpanel:SetMouseInputEnabled(true)
dpanel:SetKeyboardInputEnabled(true)
dpanel.OnKeyCodePressed = util.BasicKeyHandler
-- keep it around so we can reopen easily
dpanel:SetDeleteOnClose(false)
self.Panel = dpanel
local dbut = vgui.Create("DButton", dpanel)
local bw, bh = 100, 25
dbut:SetSize(bw, bh)
dbut:SetPos(w - bw - margin, h - bh - margin/2)
dbut:SetText(T("close"))
dbut.DoClick = function() dpanel:Close() end
local dsave = vgui.Create("DButton", dpanel)
dsave:SetSize(bw,bh)
dsave:SetPos(margin, h - bh - margin/2)
dsave:SetText(T("report_save"))
dsave:SetTooltip(T("report_save_tip"))
dsave:SetConsoleCommand("ttt_save_events")
local dtabsheet = vgui.Create("DPropertySheet", dpanel)
dtabsheet:SetPos(margin, margin + 15)
dtabsheet:SetSize(w - margin*2, h - margin*3 - bh)
local padding = dtabsheet:GetPadding()
-- Highlight tab
local dtabhilite = vgui.Create("DPanel", dtabsheet)
dtabhilite:SetPaintBackground(false)
dtabhilite:StretchToParent(padding,padding,padding,padding)
self:BuildHilitePanel(dtabhilite, title, starttime, endtime)
dtabsheet:AddSheet(T("report_tab_hilite"), dtabhilite, "icon16/star.png", false, false, T("report_tab_hilite_tip"))
-- Event log tab
local dtabevents = vgui.Create("DPanel", dtabsheet)
-- dtab1:SetSize(650, 450)
dtabevents:StretchToParent(padding, padding, padding, padding)
self:BuildEventLogPanel(dtabevents)
dtabsheet:AddSheet(T("report_tab_events"), dtabevents, "icon16/application_view_detail.png", false, false, T("report_tab_events_tip"))
-- Score tab
local dtabscores = vgui.Create("DPanel", dtabsheet)
dtabscores:SetPaintBackground(false)
dtabscores:StretchToParent(padding, padding, padding, padding)
self:BuildScorePanel(dtabscores)
dtabsheet:AddSheet(T("report_tab_scores"), dtabscores, "icon16/user.png", false, false, T("report_tab_scores_tip"))
dpanel:MakePopup()
-- makepopup grabs keyboard, whereas we only need mouse
dpanel:SetKeyboardInputEnabled(false)
end
function CLSCORE:ClearPanel()
if IsValid(self.Panel) then
-- move the mouse off any tooltips and then remove the panel next tick
-- we need this hack as opposed to just calling Remove because gmod does
-- not offer a means of killing the tooltip, and doesn't clean it up
-- properly on Remove
input.SetCursorPos( ScrW()/2, ScrH()/2 )
local pnl = self.Panel
timer.Simple(0, function() if IsValid(pnl) then pnl:Remove() end end)
end
end
function CLSCORE:SaveLog()
local events = self.Events
if events == nil or #events == 0 then
chat.AddText(COLOR_WHITE, T("report_save_error"))
return
end
local logdir = "ttt/logs"
if not file.IsDir(logdir, "DATA") then
file.CreateDir(logdir)
end
local logname = logdir .. "/ttt_events_" .. os.time() .. ".txt"
local log = "Trouble in Terrorist Town - Round Events Log\n".. string.rep("-", 50) .."\n"
log = log .. string.format("%s | %-25s | %s\n", " TIME", "TYPE", "WHAT HAPPENED") .. string.rep("-", 50) .."\n"
for i = 1, #events do
local e = events[i]
local etxt = self:TextForEvent(e)
local etime = self:TimeForEvent(e)
local _, etype = self:IconForEvent(e)
if etxt then
log = log .. string.format("%s | %-25s | %s\n", etime, etype, etxt)
end
end
file.Write(logname, log)
chat.AddText(COLOR_WHITE, T("report_save_result"), COLOR_GREEN, " /garrysmod/data/" .. logname)
end
function CLSCORE:Reset()
self.Events = {}
self.TraitorIDs = {}
self.DetectiveIDs = {}
self.Scores = {}
self.Players = {}
self.RoundStarted = 0
self:ClearPanel()
end
function CLSCORE:Init(events)
-- Get start time, traitors, detectives, scores, and nicks
local starttime = 0
local traitors, detectives
local scores, nicks = {}, {}
local game, selected, spawn = false, false, false
for i = 1, #events do
local e = events[i]
if e.id == EVENT_GAME then
if e.state == ROUND_ACTIVE then
starttime = e.t
if selected and spawn then
break
end
game = true
end
elseif e.id == EVENT_SELECTED then
traitors = e.traitor_ids
detectives = e.detective_ids
if game and spawn then
break
end
selected = true
elseif e.id == EVENT_SPAWN then
scores[e.sid64] = ScoreInit()
nicks[e.sid64] = e.ni
if game and selected then
break
end
spawn = true
end
end
if traitors == nil then traitors = {} end
if detectives == nil then detectives = {} end
scores = ScoreEventLog(events, scores, traitors, detectives)
self.Players = nicks
self.Scores = scores
self.TraitorIDs = traitors
self.DetectiveIDs = detectives
self.StartTime = starttime
self.Events = events
end
function CLSCORE:ReportEvents(events)
self:Reset()
self:Init(events)
self:ShowPanel()
end
function CLSCORE:Toggle()
if IsValid(self.Panel) then
self.Panel:ToggleVisible()
end
end
local function SortEvents(a, b)
return a.t < b.t
end
local buff = ""
net.Receive("TTT_ReportStream_Part", function()
buff = buff .. net.ReadData(CLSCORE.MaxStreamLength)
end)
net.Receive("TTT_ReportStream", function()
local events = util.Decompress(buff .. net.ReadData(net.ReadUInt(16)))
buff = ""
if events == "" then
ErrorNoHalt("Round report decompression failed!\n")
end
events = util.JSONToTable(events)
if events == nil then
ErrorNoHalt("Round report decoding failed!\n")
end
table.sort(events, SortEvents)
CLSCORE:ReportEvents(events)
end)
concommand.Add("ttt_save_events", function()
CLSCORE:SaveLog()
end)