This commit is contained in:
lifestorm
2024-08-04 22:55:00 +03:00
parent 8064ba84d8
commit 73479cff9e
7338 changed files with 1718883 additions and 14 deletions

View File

@@ -0,0 +1,226 @@
--[[
| 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/
--]]
--- Admin commands
local function GetPrintFn(ply)
if IsValid(ply) then
return function(...)
local t = ""
for _, a in ipairs({...}) do
t = t .. "\t" .. a
end
ply:PrintMessage(HUD_PRINTCONSOLE, t)
end
else
return print
end
end
local function TraitorSort(a,b)
if not IsValid(a) then return true end
if not IsValid(b) then return false end
if a:GetTraitor() and (not b:GetTraitor()) then return true end
if b:GetTraitor() and (not a:GetTraitor()) then return false end
return false
end
function PrintTraitors(ply)
if not IsValid(ply) or ply:IsSuperAdmin() then
ServerLog(Format("%s used ttt_print_traitors\n", IsValid(ply) and ply:Nick() or "console"))
local pr = GetPrintFn(ply)
local ps = player.GetAll()
table.sort(ps, TraitorSort)
for _, p in ipairs(ps) do
if IsValid(p) then
pr(p:GetTraitor() and "TRAITOR" or "Innocent", ":", p:Nick())
end
end
end
end
concommand.Add("ttt_print_traitors", PrintTraitors)
function PrintGroups(ply)
local pr = GetPrintFn(ply)
pr("User", "-", "Group")
for _, p in player.Iterator() do
pr(p:Nick(), "-", p:GetNWString("UserGroup"))
end
end
concommand.Add("ttt_print_usergroups", PrintGroups)
function PrintReport(ply)
local pr = GetPrintFn(ply)
if not IsValid(ply) or ply:IsSuperAdmin() then
ServerLog(Format("%s used ttt_print_adminreport\n", IsValid(ply) and ply:Nick() or "console"))
for k, e in pairs(SCORE.Events) do
if e.id == EVENT_KILL then
if e.att.sid64 == -1 then
pr("<something> killed " .. e.vic.ni .. (e.vic.tr and " [TRAITOR]" or " [inno.]"))
else
pr(e.att.ni .. (e.att.tr and " [TRAITOR]" or " [inno.]") .. " killed " .. e.vic.ni .. (e.vic.tr and " [TRAITOR]" or " [inno.]"))
end
end
end
else
if IsValid(ply) then
pr("You do not appear to be RCON or a superadmin!")
end
end
end
concommand.Add("ttt_print_adminreport", PrintReport)
local function PrintKarma(ply)
local pr = GetPrintFn(ply)
if (not IsValid(ply)) or ply:IsSuperAdmin() then
ServerLog(Format("%s used ttt_print_karma\n", IsValid(ply) and ply:Nick() or "console"))
KARMA.PrintAll(pr)
else
if IsValid(ply) then
pr("You do not appear to be RCON or a superadmin!")
end
end
end
concommand.Add("ttt_print_karma", PrintKarma)
CreateConVar("ttt_highlight_admins", "1")
local function ApplyHighlightAdmins(cv, old, new)
SetGlobalBool("ttt_highlight_admins", tobool(tonumber(new)))
end
cvars.AddChangeCallback("ttt_highlight_admins", ApplyHighlightAdmins)
local dmglog_console = CreateConVar("ttt_log_damage_for_console", "1")
local dmglog_save = CreateConVar("ttt_damagelog_save", "0")
local function PrintDamageLog(ply)
local pr = GetPrintFn(ply)
if (not IsValid(ply)) or ply:IsSuperAdmin() or GetRoundState() != ROUND_ACTIVE then
ServerLog(Format("%s used ttt_print_damagelog\n", IsValid(ply) and ply:Nick() or "console"))
pr("*** Damage log:\n")
if not dmglog_console:GetBool() then
pr("Damage logging for console disabled. Enable with ttt_log_damage_for_console 1.")
end
for k, txt in ipairs(GAMEMODE.DamageLog) do
pr(txt)
end
pr("*** Damage log end.")
else
if IsValid(ply) then
pr("You do not appear to be RCON or a superadmin, nor are we in the post-round phase!")
end
end
end
concommand.Add("ttt_print_damagelog", PrintDamageLog)
local function SaveDamageLog()
if not dmglog_save:GetBool() then return end
local text = ""
if #GAMEMODE.DamageLog == 0 then
text = "Damage log is empty."
else
for k, txt in ipairs(GAMEMODE.DamageLog) do
text = text .. txt .. "\n"
end
end
local fname = Format("ttt/logs/dmglog_%s_%d.txt",
os.date("%d%b%Y_%H%M"),
os.time())
file.Write(fname, text)
end
hook.Add("TTTEndRound", "ttt_damagelog_save_hook", SaveDamageLog)
function DamageLog(txt)
local t = math.max(0, CurTime() - GAMEMODE.RoundStartTime)
txt = util.SimpleTime(t, "%02i:%02i.%02i - ") .. txt
ServerLog(txt .. "\n")
if dmglog_console:GetBool() or dmglog_save:GetBool() then
table.insert(GAMEMODE.DamageLog, txt)
end
end
local ttt_bantype = CreateConVar("ttt_ban_type", "autodetect")
local function DetectServerPlugin()
if ULib and ULib.kickban then
return "ulx"
elseif evolve and evolve.Ban then
return "evolve"
elseif exsto and exsto.GetPlugin('administration') then
return "exsto"
else
return "gmod"
end
end
local function StandardBan(ply, length, reason)
RunConsoleCommand("banid", length, ply:UserID())
ply:Kick(reason)
end
local ban_functions = {
ulx = ULib and ULib.kickban, -- has (ply, length, reason) signature
evolve = function(p, l, r)
evolve:Ban(p:UniqueID(), l * 60, r) -- time in seconds
end,
sm = function(p, l, r)
game.ConsoleCommand(Format("sm_ban \"#%s\" %d \"%s\"\n", p:SteamID(), l, r))
end,
exsto = function(p, l, r)
local adm = exsto.GetPlugin('administration')
if adm and adm.Ban then
adm:Ban(nil, p, l, r)
end
end,
gmod = StandardBan
};
local function BanningFunction()
local bantype = string.lower(ttt_bantype:GetString())
if bantype == "autodetect" then
bantype = DetectServerPlugin()
end
print("Banning using " .. bantype .. " method.")
return ban_functions[bantype] or ban_functions["gmod"]
end
function PerformKickBan(ply, length, reason)
local banfn = BanningFunction()
banfn(ply, length, reason)
end

View File

@@ -0,0 +1,788 @@
--[[
| 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/
--]]
-- Award/highlight generator functions take the events and the scores as
-- produced by SCORING/CLSCORING and return a table if successful, or nil if
-- not and another one should be tried.
-- some globals we'll use a lot
local table = table
local pairs = pairs
local is_dmg = function(dmg_t, bit)
-- deal with large-number workaround for TableToJSON by
-- parsing back to number here
return util.BitSet(tonumber(dmg_t), bit)
end
-- so much text here I'm using shorter names than usual
local T = LANG.GetTranslation
local PT = LANG.GetParamTranslation
-- a common pattern
local function FindHighest(tbl)
local m_num = 0
local m_id = nil
for id, num in pairs(tbl) do
if num > m_num then
m_id = id
m_num = num
end
end
return m_id, m_num
end
local function FirstSuicide(events, scores, players, traitors)
local fs = nil
local fnum = 0
for k, e in pairs(events) do
if e.id == EVENT_KILL and e.att.sid64 == e.vic.sid64 then
fnum = fnum + 1
if fs == nil then
fs = e
end
end
end
if fs then
local award = {nick=fs.att.ni}
if not award.nick then return nil end
if fnum > 1 then
award.title = T("aw_sui1_title")
award.text = T("aw_sui1_text")
else
award.title = T("aw_sui2_title")
award.text = T("aw_sui2_text")
end
-- only high interest if many people died this way
award.priority = fnum
return award
else
return nil
end
end
local function ExplosiveGrant(events, scores, players, traitors)
local bombers = {}
for k, e in pairs(events) do
if e.id == EVENT_KILL and is_dmg(e.dmg.t, DMG_BLAST) then
bombers[e.att.sid64] = (bombers[e.att.sid64] or 0) + 1
end
end
local award = {title= T("aw_exp1_title")}
if not table.IsEmpty(bombers) then
for sid64, num in pairs(bombers) do
-- award goes to whoever reaches this first I guess
if num > 2 then
award.nick = players[sid64]
if not award.nick then return nil end -- if player disconnected or something
award.text = PT("aw_exp1_text", {num = num})
-- rare award, high interest
award.priority = 10 + num
return award
end
end
end
return nil
end
local function ExplodedSelf(events, scores, players, traitors)
for k, e in pairs(events) do
if e.id == EVENT_KILL and is_dmg(e.dmg.t, DMG_BLAST) and e.att.sid64 == e.vic.sid64 then
return {title=T("aw_exp2_title"), text=T("aw_exp2_text"), nick=e.vic.ni, priority=math.random(1, 4)}
end
end
return nil
end
local function FirstBlood(events, scores, players, traitors)
for k, e in pairs(events) do
if e.id == EVENT_KILL and e.att.sid64 != e.vic.sid64 and e.att.sid64 != -1 then
local award = {nick=e.att.ni}
if not award.nick or award.nick == "" then return nil end
if e.att.tr and not e.vic.tr then -- traitor legit k
award.title = T("aw_fst1_title")
award.text = T("aw_fst1_text")
elseif e.att.tr and e.vic.tr then -- traitor tk
award.title = T("aw_fst2_title")
award.text = T("aw_fst2_text")
elseif not e.att.tr and not e.vic.tr then -- inno tk
award.title = T("aw_fst3_title")
award.text = T("aw_fst3_text")
else -- inno legit k
award.title = T("aw_fst4_title")
award.text = T("aw_fst4_text")
end
-- more interesting if there were many players and therefore many kills
award.priority = math.random(-3, math.Round(table.Count(players) / 4))
return award
end
end
end
local function AllKills(events, scores, players, traitors)
-- see if there is one killer responsible for all kills of either team
local tr_killers = {}
local in_killers = {}
for id, s in pairs(scores) do
if s.innos > 0 then
table.insert(in_killers, id)
elseif s.traitors > 0 then
table.insert(tr_killers, id)
end
end
if #tr_killers == 1 then
local id = tr_killers[1]
if not table.HasValue(traitors, id) then
local killer = players[id]
if not killer then return nil end
return {nick=killer, title=T("aw_all1_title"), text=T("aw_all1_text"), priority=math.random(0, table.Count(players))}
end
end
if #in_killers == 1 then
local id = in_killers[1]
if table.HasValue(traitors, id) then
local killer = players[id]
if not killer then return nil end
return {nick=killer, title=T("aw_all2_title"), text=T("aw_all2_text"), priority=math.random(0, table.Count(players))}
end
end
return nil
end
local function NumKills_Traitor(events, scores, players, traitors)
local trs = {}
for id, s in pairs(scores) do
if table.HasValue(traitors, id) then
if s.innos > 0 then
table.insert(trs, id)
end
end
end
local choices = table.Count(trs)
if choices > 0 then
-- award a random killer
local pick = math.random(1, choices)
local sid64 = trs[pick]
local nick = players[sid64]
if not nick then return nil end
local kills = scores[sid64].innos
if kills == 1 then
return {title=T("aw_nkt1_title"), nick=nick, text=T("aw_nkt1_text"), priority=0}
elseif kills == 2 then
return {title=T("aw_nkt2_title"), nick=nick, text=T("aw_nkt2_text"), priority=1}
elseif kills == 3 then
return {title=T("aw_nkt3_title"), nick=nick, text=T("aw_nkt3_text"), priority=kills}
elseif kills >= 4 and kills < 7 then
return {title=T("aw_nkt4_title"), nick=nick, text=PT("aw_nkt4_text", {num = kills}), priority=kills + 2}
elseif kills >= 7 then
return {title=T("aw_nkt5_title"), nick=nick, text=T("aw_nkt5_text"), priority=kills + 5}
end
else
return nil
end
end
local function NumKills_Inno(events, scores, players, traitors)
local ins = {}
for id, s in pairs(scores) do
if not table.HasValue(traitors, id) then
if s.traitors > 0 then
table.insert(ins, id)
end
end
end
local choices = table.Count(ins)
if not table.IsEmpty(ins) then
-- award a random killer
local pick = math.random(1, choices)
local sid64 = ins[pick]
local nick = players[sid64]
if not nick then return nil end
local kills = scores[sid64].traitors
if kills == 1 then
return {title=T("aw_nki1_title"), nick=nick, text=T("aw_nki1_text"), priority = 0}
elseif kills == 2 then
return {title=T("aw_nki2_title"), nick=nick, text=T("aw_nki2_text"), priority = 1}
elseif kills == 3 then
return {title=T("aw_nki3_title"), nick=nick, text=T("aw_nki3_text"), priority= 5}
elseif kills >= 4 then
return {title=T("aw_nki4_title"), nick=nick, text=T("aw_nki4_text"), priority=kills + 10}
end
else
return nil
end
end
local function FallDeath(events, scores, players, traitors)
for k, e in pairs(events) do
if e.id == EVENT_KILL and is_dmg(e.dmg.t, DMG_FALL) then
if e.att.ni != "" then
return {title=T("aw_fal1_title"), nick=e.att.ni, text=T("aw_fal1_text"), priority=math.random(7, 15)}
else
return {title=T("aw_fal2_title"), nick=e.vic.ni, text=T("aw_fal2_text"), priority=math.random(1, 5)}
end
end
end
return nil
end
local function FallKill(events, scores, players, traitors)
for k, e in pairs(events) do
if e.id == EVENT_KILL and is_dmg(e.dmg.t, DMG_CRUSH) and is_dmg(e.dmg.t, DMG_PHYSGUN) then
if e.att.ni != "" then
return {title=T("aw_fal3_title"), nick=e.att.ni, text=T("aw_fal3_text"), priority=math.random(10, 15)}
end
end
end
end
local function Headshots(events, scores, players, traitors)
local hs = {}
for k, e in pairs(events) do
if e.id == EVENT_KILL and e.dmg.h and is_dmg(e.dmg.t, DMG_BULLET) then
hs[e.att.sid64] = (hs[e.att.sid64] or 0) + 1
end
end
if table.IsEmpty(hs) then return nil end
-- find the one with the most shots
local m_id, m_num = FindHighest(hs)
if not m_id then return nil end
local nick = players[m_id]
if not nick then return nil end
local award = {nick=nick, priority=m_num / 2}
if m_num > 1 and m_num < 4 then
award.title = T("aw_hed1_title")
award.text = PT("aw_hed1_text", {num = m_num})
elseif m_num >= 4 and m_num < 6 then
award.title = T("aw_hed2_title")
award.text = PT("aw_hed2_text", {num = m_num})
elseif m_num >= 6 then
award.title = T("aw_hed3_title")
award.text = PT("aw_hed3_text", {num = m_num})
award.priority = m_num + 5
else
return nil
end
return award
end
local function UsedAmmoMost(events, ammotype)
local user = {}
for k, e in pairs(events) do
if e.id == EVENT_KILL and e.dmg.g == ammotype then
user[e.att.sid64] = (user[e.att.sid64] or 0) + 1
end
end
if table.IsEmpty(user) then return nil end
local m_id, m_num = FindHighest(user)
if not m_id then return nil end
return {sid64=m_id, kills=m_num}
end
local function CrowbarUser(events, scores, players, traitors)
local most = UsedAmmoMost(events, AMMO_CROWBAR)
if not most then return nil end
local nick = players[most.sid64]
if not nick then return nil end
local award = {nick=nick, priority=most.kills + math.random(0, 4)}
local kills = most.kills
if kills > 1 and kills < 3 then
award.title = T("aw_cbr1_title")
award.text = PT("aw_cbr1_text", {num = kills})
elseif kills >= 3 then
award.title = T("aw_cbr2_title")
award.text = PT("aw_cbr2_text", {num = kills})
award.priority = kills + math.random(5, 10)
else
return nil
end
return award
end
local function PistolUser(events, scores, players, traitors)
local most = UsedAmmoMost(events, AMMO_PISTOL)
if not most then return nil end
local nick = players[most.sid64]
if not nick then return nil end
local award = {nick=nick, priority=most.kills}
local kills = most.kills
if kills > 1 and kills < 4 then
award.title = T("aw_pst1_title")
award.text = PT("aw_pst1_text", {num = kills})
elseif kills >= 4 then
award.title = T("aw_pst2_title")
award.text = PT("aw_pst2_text", {num = kills})
award.priority = award.priority + math.random(0, 5)
else
return nil
end
return award
end
local function ShotgunUser(events, scores, players, traitors)
local most = UsedAmmoMost(events, AMMO_SHOTGUN)
if not most then return nil end
local nick = players[most.sid64]
if not nick then return nil end
local award = {nick=nick, priority=most.kills}
local kills = most.kills
if kills > 1 and kills < 4 then
award.title = T("aw_sgn1_title")
award.text = PT("aw_sgn1_text", {num = kills})
award.priority = math.Round(kills / 2)
elseif kills >= 4 then
award.title = T("aw_sgn2_title")
award.text = PT("aw_sgn2_text", {num = kills})
else
return nil
end
return award
end
local function RifleUser(events, scores, players, traitors)
local most = UsedAmmoMost(events, AMMO_RIFLE)
if not most then return nil end
local nick = players[most.sid64]
if not nick then return nil end
local award = {nick=nick, priority=most.kills}
local kills = most.kills
if kills > 1 and kills < 4 then
award.title = T("aw_rfl1_title")
award.text = PT("aw_rfl1_text", {num = kills})
award.priority = math.Round(kills / 2)
elseif kills >= 4 then
award.title = T("aw_rfl2_title")
award.text = PT("aw_rfl2_text", {num = kills})
else
return nil
end
return award
end
local function DeagleUser(events, scores, players, traitors)
local most = UsedAmmoMost(events, AMMO_DEAGLE)
if not most then return nil end
local nick = players[most.sid64]
if not nick then return nil end
local award = {nick=nick, priority=most.kills}
local kills = most.kills
if kills > 1 and kills < 4 then
award.title = T("aw_dgl1_title")
award.text = PT("aw_dgl1_text", {num = kills})
award.priority = math.Round(kills / 2)
elseif kills >= 4 then
award.title = T("aw_dgl2_title")
award.text = PT("aw_dgl2_text", {num = kills})
award.priority = kills + math.random(2, 6)
else
return nil
end
return award
end
local function MAC10User(events, scores, players, traitors)
local most = UsedAmmoMost(events, AMMO_MAC10)
if not most then return nil end
local nick = players[most.sid64]
if not nick then return nil end
local award = {nick=nick, priority=most.kills}
local kills = most.kills
if kills > 1 and kills < 4 then
award.title = T("aw_mac1_title")
award.text = PT("aw_mac1_text", {num = kills})
award.priority = math.Round(kills / 2)
elseif kills >= 4 then
award.title = T("aw_mac2_title")
award.text = PT("aw_mac2_text", {num = kills})
else
return nil
end
return award
end
local function SilencedPistolUser(events, scores, players, traitors)
local most = UsedAmmoMost(events, AMMO_SIPISTOL)
if not most then return nil end
local nick = players[most.sid64]
if not nick then return nil end
local award = {nick=nick, priority=most.kills}
local kills = most.kills
if kills > 1 and kills < 3 then
award.title = T("aw_sip1_title")
award.text = PT("aw_sip1_text", {num = kills})
elseif kills >= 3 then
award.title = T("aw_sip2_title")
award.text = PT("aw_sip2_text", {num = kills})
else
return nil
end
return award
end
local function KnifeUser(events, scores, players, traitors)
local most = UsedAmmoMost(events, AMMO_KNIFE)
if not most then return nil end
local nick = players[most.sid64]
if not nick then return nil end
local award = {nick=nick, priority=most.kills}
local kills = most.kills
if kills == 1 then
if table.HasValue(traitors, most.sid64) then
award.title = T("aw_knf1_title")
award.text = PT("aw_knf1_text", {num = kills})
award.priority = 0
else
award.title = T("aw_knf2_title")
award.text = PT("aw_knf2_text", {num = kills})
award.priority = 5
end
elseif kills > 1 and kills < 4 then
award.title = T("aw_knf3_title")
award.text = PT("aw_knf3_text", {num = kills})
elseif kills >= 4 then
award.title = T("aw_knf4_title")
award.text = PT("aw_knf4_text", {num = kills})
else
return nil
end
return award
end
local function FlareUser(events, scores, players, traitors)
local most = UsedAmmoMost(events, AMMO_FLARE)
if not most then return nil end
local nick = players[most.sid64]
if not nick then return nil end
local award = {nick=nick, priority=most.kills}
local kills = most.kills
if kills > 1 and kills < 3 then
award.title = T("aw_flg1_title")
award.text = PT("aw_flg1_text", {num = kills})
elseif kills >= 3 then
award.title = T("aw_flg2_title")
award.text = PT("aw_flg2_text", {num = kills})
else
return nil
end
return award
end
local function M249User(events, scores, players, traitors)
local most = UsedAmmoMost(events, AMMO_M249)
if not most then return nil end
local nick = players[most.sid64]
if not nick then return nil end
local award = {nick=nick, priority=most.kills}
local kills = most.kills
if kills > 1 and kills < 4 then
award.title = T("aw_hug1_title")
award.text = PT("aw_hug1_text", {num = kills})
elseif kills >= 4 then
award.title = T("aw_hug2_title")
award.text = PT("aw_hug2_text", {num = kills})
else
return nil
end
return award
end
local function M16User(events, scores, players, traitors)
local most = UsedAmmoMost(events, AMMO_M16)
if not most then return nil end
local nick = players[most.sid64]
if not nick then return nil end
local award = {nick=nick, priority=most.kills}
local kills = most.kills
if kills > 1 and kills < 4 then
award.title = T("aw_msx1_title")
award.text = PT("aw_msx1_text", {num = kills})
elseif kills >= 4 then
award.title = T("aw_msx2_title")
award.text = PT("aw_msx2_text", {num = kills})
else
return nil
end
return award
end
local function TeamKiller(events, scores, players, traitors)
local num_traitors = table.Count(traitors)
local num_inno = table.Count(players) - num_traitors
-- find biggest tker
local tker = nil
local pct = 0
for id, s in pairs(scores) do
local kills = s.innos
local team = num_inno - 1
if table.HasValue(traitors, id) then
kills = s.traitors
team = num_traitors - 1
end
if kills > 0 and (kills / team) > pct then
pct = kills / team
tker = id
end
end
-- no tks
if pct == 0 or tker == nil then return nil end
local nick = players[tker]
if not nick then return nil end
local was_traitor = table.HasValue(traitors, tker)
local kills = (was_traitor and scores[tker].traitors > 0 and scores[tker].traitors) or (scores[tker].innos > 0 and scores[tker].innos) or 0
local award = {nick=nick, priority=kills}
if kills == 1 then
award.title = T("aw_tkl1_title")
award.text = T("aw_tkl1_text")
award.priority = 0
elseif kills == 2 then
award.title = T("aw_tkl2_title")
award.text = T("aw_tkl2_text")
elseif kills == 3 then
award.title = T("aw_tkl3_title")
award.text = T("aw_tkl3_text")
elseif pct >= 1.0 then
award.title = T("aw_tkl4_title")
award.text = T("aw_tkl4_text")
award.priority = kills + math.random(3, 6)
elseif pct >= 0.75 and not was_traitor then
award.title = T("aw_tkl5_title")
award.text = T("aw_tkl5_text")
award.priority = kills + 10
elseif pct > 0.5 then
award.title = T("aw_tkl6_title")
award.text = T("aw_tkl6_text")
award.priority = kills + math.random(2, 7)
elseif pct >= 0.25 then
award.title = T("aw_tkl7_title")
award.text = T("aw_tkl7_text")
else
return nil
end
return award
end
local function Burner(events, scores, players, traitors)
local brn = {}
for k, e in pairs(events) do
if e.id == EVENT_KILL and is_dmg(e.dmg.t, DMG_BURN) then
brn[e.att.sid64] = (brn[e.att.sid64] or 0) + 1
end
end
if table.IsEmpty(brn) then return nil end
-- find the one with the most burnings
local m_id, m_num = FindHighest(brn)
if not m_id then return nil end
local nick = players[m_id]
if not nick then return nil end
local award = {nick=nick, priority=m_num * 2}
if m_num > 1 and m_num < 4 then
award.title = T("aw_brn1_title")
award.text = T("aw_brn1_text")
elseif m_num >= 4 and m_num < 7 then
award.title = T("aw_brn2_title")
award.text = T("aw_brn2_text")
elseif m_num >= 7 then
award.title = T("aw_brn3_title")
award.text = T("aw_brn3_text")
award.priority = m_num + math.random(0, 4)
else
return nil
end
return award
end
local function Coroner(events, scores, players, traitors)
local finders = {}
for k, e in pairs(events) do
if e.id == EVENT_BODYFOUND then
finders[e.sid64] = (finders[e.sid64] or 0) + 1
end
end
if table.IsEmpty(finders) then return end
local m_id, m_num = FindHighest(finders)
if not m_id then return nil end
local nick = players[m_id]
if not nick then return nil end
local award = {nick=nick, priority=m_num}
if m_num > 2 and m_num < 6 then
award.title = T("aw_fnd1_title")
award.text = PT("aw_fnd1_text", {num = m_num})
elseif m_num >= 6 and m_num < 10 then
award.title = T("aw_fnd2_title")
award.text = PT("aw_fnd2_text", {num = m_num})
elseif m_num >= 10 then
award.title = T("aw_fnd3_title")
award.text = PT("aw_fnd3_text", {num = m_num})
award.priority = m_num + math.random(0, 4)
else
return nil
end
return award
end
local function CreditFound(events, scores, players, traitors)
local finders = {}
for k, e in pairs(events) do
if e.id == EVENT_CREDITFOUND then
finders[e.sid64] = (finders[e.sid64] or 0) + e.cr
end
end
if table.IsEmpty(finders) then return end
local m_id, m_num = FindHighest(finders)
if not m_id then return nil end
local nick = players[m_id]
if not nick then return nil end
local award = {nick=nick}
if m_num > 2 then
award.title = T("aw_crd1_title")
award.text = PT("aw_crd1_text", {num = m_num})
award.priority = m_num + math.random(0, m_num)
else
return nil
end
return award
end
local function TimeOfDeath(events, scores, players, traitors)
local near = 10
local time_near_start = CLSCORE.StartTime + near
local time_near_end = nil
local traitor_win = nil
local e = nil
for i=#events, 1, -1 do
e = events[i]
if e.id == EVENT_FINISH then
time_near_end = e.t - near
traitor_win = (e.win == WIN_TRAITOR)
elseif e.id == EVENT_KILL and e.vic then
if time_near_end and
e.t > time_near_end and e.vic.tr == traitor_win then
return {
nick = e.vic.ni,
title = T("aw_tod1_title"),
text = T("aw_tod1_text"),
priority = (e.t - time_near_end) * 2
};
elseif e.t < time_near_start then
return {
nick = e.vic.ni,
title = T("aw_tod2_title"),
text = T("aw_tod2_text"),
priority = (time_near_start - e.t) * 2
};
end
end
end
end
-- New award functions must be added to this to be used by CLSCORE.
-- Note that AWARDS is global. You can just go: table.insert(AWARDS, myawardfn) in your SWEPs.
AWARDS = { FirstSuicide, ExplosiveGrant, ExplodedSelf, FirstBlood, AllKills, NumKills_Traitor, NumKills_Inno, FallDeath, Headshots, PistolUser, ShotgunUser, RifleUser, DeagleUser, MAC10User, CrowbarUser, TeamKiller, Burner, SilencedPistolUser, KnifeUser, FlareUser, Coroner, M249User, M16User, CreditFound, FallKill, TimeOfDeath }

View File

@@ -0,0 +1,61 @@
--[[
| 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/
--]]
DISGUISE = {}
local trans = LANG.GetTranslation
function DISGUISE.CreateMenu(parent)
local dform = vgui.Create("DForm", parent)
dform:SetName(trans("disg_menutitle"))
dform:StretchToParent(0,0,0,0)
dform:SetAutoSize(false)
local owned = LocalPlayer():HasEquipmentItem(EQUIP_DISGUISE)
if not owned then
dform:Help(trans("disg_not_owned"))
return dform
end
local dcheck = vgui.Create("DCheckBoxLabel", dform)
dcheck:SetText(trans("disg_enable"))
dcheck:SetIndent(5)
dcheck:SetValue(LocalPlayer():GetNWBool("disguised", false))
dcheck.OnChange = function(s, val)
RunConsoleCommand("ttt_set_disguise", val and "1" or "0")
end
dform:AddItem(dcheck)
dform:Help(trans("disg_help1"))
dform:Help(trans("disg_help2"))
dform:SetVisible(true)
return dform
end
function DISGUISE.Draw(client)
if (not client) or (not client:IsActiveTraitor()) then return end
if not client:GetNWBool("disguised", false) then return end
surface.SetFont("TabLarge")
surface.SetTextColor(255, 0, 0, 230)
local text = trans("disg_hud")
local w, h = surface.GetTextSize(text)
surface.SetTextPos(36, ScrH() - 160 - h)
surface.DrawText(text)
end
concommand.Add("ttt_toggle_disguise", WEPS.DisguiseToggle)

View File

@@ -0,0 +1,515 @@
--[[
| 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/
--]]
---- Traitor equipment menu
local GetTranslation = LANG.GetTranslation
local GetPTranslation = LANG.GetParamTranslation
-- Buyable weapons are loaded automatically. Buyable items are defined in
-- equip_items_shd.lua
local Equipment = nil
function GetEquipmentForRole(role)
-- need to build equipment cache?
if not Equipment then
-- start with all the non-weapon goodies
local tbl = table.Copy(EquipmentItems)
-- find buyable weapons to load info from
for k, v in pairs(weapons.GetList()) do
if v and v.CanBuy then
local data = v.EquipMenuData or {}
local base = {
id = WEPS.GetClass(v),
name = v.PrintName or "Unnamed",
limited = v.LimitedStock,
kind = v.Kind or WEAPON_NONE,
slot = (v.Slot or 0) + 1,
material = v.Icon or "vgui/ttt/icon_id",
-- the below should be specified in EquipMenuData, in which case
-- these values are overwritten
type = "Type not specified",
model = "models/weapons/w_bugbait.mdl",
desc = "No description specified."
};
-- Force material to nil so that model key is used when we are
-- explicitly told to do so (ie. material is false rather than nil).
if data.modelicon then
base.material = nil
end
table.Merge(base, data)
-- add this buyable weapon to all relevant equipment tables
for _, r in pairs(v.CanBuy) do
table.insert(tbl[r], base)
end
end
end
-- mark custom items
for r, is in pairs(tbl) do
for _, i in pairs(is) do
if i and i.id then
i.custom = not table.HasValue(DefaultEquipment[r], i.id)
end
end
end
Equipment = tbl
end
return Equipment and Equipment[role] or {}
end
local function ItemIsWeapon(item) return not tonumber(item.id) end
local function CanCarryWeapon(item) return LocalPlayer():CanCarryType(item.kind) end
local color_bad = Color(220, 60, 60, 255)
local color_good = Color(0, 200, 0, 255)
-- Creates tabel of labels showing the status of ordering prerequisites
local function PreqLabels(parent, x, y)
local tbl = {}
tbl.credits = vgui.Create("DLabel", parent)
tbl.credits:SetTooltip(GetTranslation("equip_help_cost"))
tbl.credits:SetPos(x, y)
tbl.credits.Check = function(s, sel)
local credits = LocalPlayer():GetCredits()
return credits > 0, GetPTranslation("equip_cost", {num = credits})
end
tbl.owned = vgui.Create("DLabel", parent)
tbl.owned:SetTooltip(GetTranslation("equip_help_carry"))
tbl.owned:CopyPos(tbl.credits)
tbl.owned:MoveBelow(tbl.credits, y)
tbl.owned.Check = function(s, sel)
if ItemIsWeapon(sel) and (not CanCarryWeapon(sel)) then
return false, GetPTranslation("equip_carry_slot", {slot = sel.slot})
elseif (not ItemIsWeapon(sel)) and LocalPlayer():HasEquipmentItem(sel.id) then
return false, GetTranslation("equip_carry_own")
else
return true, GetTranslation("equip_carry")
end
end
tbl.bought = vgui.Create("DLabel", parent)
tbl.bought:SetTooltip(GetTranslation("equip_help_stock"))
tbl.bought:CopyPos(tbl.owned)
tbl.bought:MoveBelow(tbl.owned, y)
tbl.bought.Check = function(s, sel)
if sel.limited and LocalPlayer():HasBought(tostring(sel.id)) then
return false, GetTranslation("equip_stock_deny")
else
return true, GetTranslation("equip_stock_ok")
end
end
for k, pnl in pairs(tbl) do
pnl:SetFont("TabLarge")
end
return function(selected)
local allow = true
for k, pnl in pairs(tbl) do
local result, text = pnl:Check(selected)
pnl:SetTextColor(result and color_good or color_bad)
pnl:SetText(text)
pnl:SizeToContents()
allow = allow and result
end
return allow
end
end
-- quick, very basic override of DPanelSelect
local PANEL = {}
local function DrawSelectedEquipment(pnl)
surface.SetDrawColor(255, 200, 0, 255)
surface.DrawOutlinedRect(0, 0, pnl:GetWide(), pnl:GetTall())
end
function PANEL:SelectPanel(pnl)
self.BaseClass.SelectPanel(self, pnl)
if pnl then
pnl.PaintOver = DrawSelectedEquipment
end
end
vgui.Register("EquipSelect", PANEL, "DPanelSelect")
local SafeTranslate = LANG.TryTranslation
local color_darkened = Color(255,255,255, 80)
-- TODO: make set of global role colour defs, these are same as wepswitch
local color_slot = {
[ROLE_TRAITOR] = Color(180, 50, 40, 255),
[ROLE_DETECTIVE] = Color(50, 60, 180, 255)
}
local fieldstbl = {"name", "type", "desc"}
local eqframe = nil
local function TraitorMenuPopup()
local ply = LocalPlayer()
if not IsValid(ply) or not ply:IsActiveSpecial() then
return
end
-- Close any existing traitor menu
if eqframe and IsValid(eqframe) then eqframe:Close() end
local credits = ply:GetCredits()
local can_order = credits > 0
local dframe = vgui.Create("DFrame")
local w, h = 570, 412
dframe:SetSize(w, h)
dframe:Center()
dframe:SetTitle(GetTranslation("equip_title"))
dframe:SetVisible(true)
dframe:ShowCloseButton(true)
dframe:SetMouseInputEnabled(true)
dframe:SetDeleteOnClose(true)
local m = 5
local dsheet = vgui.Create("DPropertySheet", dframe)
-- Add a callback when switching tabs
local oldfunc = dsheet.SetActiveTab
dsheet.SetActiveTab = function(self, new)
if self.m_pActiveTab != new and self.OnTabChanged then
self:OnTabChanged(self.m_pActiveTab, new)
end
oldfunc(self, new)
end
dsheet:SetPos(0,0)
dsheet:StretchToParent(m,m + 25,m,m)
local padding = dsheet:GetPadding()
local dequip = vgui.Create("DPanel", dsheet)
dequip:SetPaintBackground(false)
dequip:StretchToParent(padding,padding,padding,padding)
-- Determine if we already have equipment
local owned_ids = {}
for _, wep in ipairs(ply:GetWeapons()) do
if IsValid(wep) and wep:IsEquipment() then
table.insert(owned_ids, wep:GetClass())
end
end
-- Stick to one value for no equipment
if #owned_ids == 0 then
owned_ids = nil
end
--- Construct icon listing
local dlist = vgui.Create("EquipSelect", dequip)
dlist:SetPos(0,0)
dlist:SetSize(216, h - 75)
dlist:EnableVerticalScrollbar()
dlist:EnableHorizontal(true)
dlist:SetPadding(4)
local items = GetEquipmentForRole(ply:GetRole())
local to_select = nil
for k, item in pairs(items) do
local ic = nil
-- Create icon panel
if item.material then
if item.custom then
-- Custom marker icon
ic = vgui.Create("LayeredIcon", dlist)
local marker = vgui.Create("DImage")
marker:SetImage("vgui/ttt/custom_marker")
marker.PerformLayout = function(s)
s:AlignBottom(2)
s:AlignRight(2)
s:SetSize(16, 16)
end
marker:SetTooltip(GetTranslation("equip_custom"))
ic:AddLayer(marker)
ic:EnableMousePassthrough(marker)
elseif not ItemIsWeapon(item) then
ic = vgui.Create("SimpleIcon", dlist)
else
ic = vgui.Create("LayeredIcon", dlist)
end
-- Slot marker icon
if ItemIsWeapon(item) then
local slot = vgui.Create("SimpleIconLabelled")
slot:SetIcon("vgui/ttt/slotcap")
slot:SetIconColor(color_slot[ply:GetRole()] or COLOR_GREY)
slot:SetIconSize(16)
slot:SetIconText(item.slot)
slot:SetIconProperties(COLOR_WHITE,
"DefaultBold",
{opacity=220, offset=1},
{10, 8})
ic:AddLayer(slot)
ic:EnableMousePassthrough(slot)
end
ic:SetIconSize(64)
ic:SetIcon(item.material)
elseif item.model then
ic = vgui.Create("SpawnIcon", dlist)
ic:SetModel(item.model)
else
ErrorNoHalt("Equipment item does not have model or material specified: " .. tostring(item) .. "\n")
end
ic.item = item
local tip = SafeTranslate(item.name) .. " (" .. SafeTranslate(item.type) .. ")"
ic:SetTooltip(tip)
-- If we cannot order this item, darken it
if ((not can_order) or
-- already owned
table.HasValue(owned_ids, item.id) or
(tonumber(item.id) and ply:HasEquipmentItem(tonumber(item.id))) or
-- already carrying a weapon for this slot
(ItemIsWeapon(item) and (not CanCarryWeapon(item))) or
-- already bought the item before
(item.limited and ply:HasBought(tostring(item.id)))) then
ic:SetIconColor(color_darkened)
end
dlist:AddPanel(ic)
end
local dlistw = 216
local bw, bh = 100, 25
local dih = h - bh - m*5
local diw = w - dlistw - m*6 - 2
local dinfobg = vgui.Create("DPanel", dequip)
dinfobg:SetPaintBackground(false)
dinfobg:SetSize(diw, dih)
dinfobg:SetPos(dlistw + m, 0)
local dinfo = vgui.Create("ColoredBox", dinfobg)
dinfo:SetColor(Color(90, 90, 95))
dinfo:SetPos(0,0)
dinfo:StretchToParent(0, 0, 0, dih - 135)
local dfields = {}
for _, k in ipairs(fieldstbl) do
dfields[k] = vgui.Create("DLabel", dinfo)
dfields[k]:SetTooltip(GetTranslation("equip_spec_" .. k))
dfields[k]:SetPos(m*3, m*2)
end
dfields.name:SetFont("TabLarge")
dfields.type:SetFont("DermaDefault")
dfields.type:MoveBelow(dfields.name)
dfields.desc:SetFont("DermaDefaultBold")
dfields.desc:SetContentAlignment(7)
dfields.desc:MoveBelow(dfields.type, 1)
local iw, ih = dinfo:GetSize()
local dhelp = vgui.Create("ColoredBox", dinfobg)
dhelp:SetColor(Color(90, 90, 95))
dhelp:SetSize(diw, dih - 205)
dhelp:MoveBelow(dinfo, m)
local update_preqs = PreqLabels(dhelp, m*3, m*2)
dhelp:SizeToContents()
local dconfirm = vgui.Create("DButton", dinfobg)
dconfirm:SetPos(0, dih - bh*2)
dconfirm:SetSize(bw, bh)
dconfirm:SetDisabled(true)
dconfirm:SetText(GetTranslation("equip_confirm"))
dsheet:AddSheet(GetTranslation("equip_tabtitle"), dequip, "icon16/bomb.png", false, false, GetTranslation("equip_tooltip_main"))
-- Item control
if ply:HasEquipmentItem(EQUIP_RADAR) then
local dradar = RADAR.CreateMenu(dsheet, dframe)
dsheet:AddSheet(GetTranslation("radar_name"), dradar, "icon16/magnifier.png", false, false, GetTranslation("equip_tooltip_radar"))
end
if ply:HasEquipmentItem(EQUIP_DISGUISE) then
local ddisguise = DISGUISE.CreateMenu(dsheet)
dsheet:AddSheet(GetTranslation("disg_name"), ddisguise, "icon16/user.png", false, false, GetTranslation("equip_tooltip_disguise"))
end
-- Weapon/item control
if IsValid(ply.radio) or ply:HasWeapon("weapon_ttt_radio") then
local dradio = TRADIO.CreateMenu(dsheet)
dsheet:AddSheet(GetTranslation("radio_name"), dradio, "icon16/transmit.png", false, false, GetTranslation("equip_tooltip_radio"))
end
-- Credit transferring
if credits > 0 then
local dtransfer = CreateTransferMenu(dsheet)
dsheet:AddSheet(GetTranslation("xfer_name"), dtransfer, "icon16/group_gear.png", false, false, GetTranslation("equip_tooltip_xfer"))
end
hook.Run("TTTEquipmentTabs", dsheet)
-- couple panelselect with info
dlist.OnActivePanelChanged = function(self, _, new)
for k,v in pairs(new.item) do
if dfields[k] then
dfields[k]:SetText(SafeTranslate(v))
dfields[k]:SizeToContents()
end
end
-- Trying to force everything to update to
-- the right size is a giant pain, so just
-- force a good size.
dfields.desc:SetTall(70)
can_order = update_preqs(new.item)
dconfirm:SetDisabled(not can_order)
end
-- select first
dlist:SelectPanel(to_select or dlist:GetItems()[1])
-- prep confirm action
dconfirm.DoClick = function()
local pnl = dlist.SelectedPanel
if not pnl or not pnl.item then return end
local choice = pnl.item
RunConsoleCommand("ttt_order_equipment", choice.id)
dframe:Close()
end
-- update some basic info, may have changed in another tab
-- specifically the number of credits in the preq list
dsheet.OnTabChanged = function(s, old, new)
if not IsValid(new) then return end
if new:GetPanel() == dequip then
can_order = update_preqs(dlist.SelectedPanel.item)
dconfirm:SetDisabled(not can_order)
end
end
local dcancel = vgui.Create("DButton", dframe)
dcancel:SetPos(w - 13 - bw, h - bh - 16)
dcancel:SetSize(bw, bh)
dcancel:SetDisabled(false)
dcancel:SetText(GetTranslation("close"))
dcancel.DoClick = function() dframe:Close() end
dframe:MakePopup()
dframe:SetKeyboardInputEnabled(false)
eqframe = dframe
end
concommand.Add("ttt_cl_traitorpopup", TraitorMenuPopup)
local function ForceCloseTraitorMenu(ply, cmd, args)
if IsValid(eqframe) then
eqframe:Close()
end
end
concommand.Add("ttt_cl_traitorpopup_close", ForceCloseTraitorMenu)
function GM:OnContextMenuOpen()
local r = GetRoundState()
if r == ROUND_ACTIVE and not (LocalPlayer():GetTraitor() or LocalPlayer():GetDetective()) then
return
elseif r == ROUND_POST or r == ROUND_PREP then
CLSCORE:Toggle()
return
end
if IsValid(eqframe) then
eqframe:Close()
else
RunConsoleCommand("ttt_cl_traitorpopup")
end
end
local function ReceiveEquipment()
local ply = LocalPlayer()
if not IsValid(ply) then return end
ply.equipment_items = net.ReadUInt(16)
end
net.Receive("TTT_Equipment", ReceiveEquipment)
local function ReceiveCredits()
local ply = LocalPlayer()
if not IsValid(ply) then return end
ply.equipment_credits = net.ReadUInt(8)
end
net.Receive("TTT_Credits", ReceiveCredits)
local r = 0
local function ReceiveBought()
local ply = LocalPlayer()
if not IsValid(ply) then return end
ply.bought = {}
local num = net.ReadUInt(8)
for i=1,num do
local s = net.ReadString()
if s != "" then
table.insert(ply.bought, s)
end
end
-- This usermessage sometimes fails to contain the last weapon that was
-- bought, even though resending then works perfectly. Possibly a bug in
-- bf_read. Anyway, this hack is a workaround: we just request a new umsg.
if num != #ply.bought and r < 10 then -- r is an infinite loop guard
RunConsoleCommand("ttt_resend_bought")
r = r + 1
else
r = 0
end
end
net.Receive("TTT_Bought", ReceiveBought)
-- Player received the item he has just bought, so run clientside init
local function ReceiveBoughtItem()
local is_item = net.ReadBit() == 1
local id = is_item and net.ReadUInt(16) or net.ReadString()
-- I can imagine custom equipment wanting this, so making a hook
hook.Run("TTTBoughtItem", is_item, id)
end
net.Receive("TTT_BoughtItem", ReceiveBoughtItem)

View File

@@ -0,0 +1,265 @@
--[[
| 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/
--]]
---- Help screen
local GetTranslation = LANG.GetTranslation
local GetPTranslation = LANG.GetParamTranslation
CreateConVar("ttt_spectator_mode", "0", FCVAR_ARCHIVE)
CreateConVar("ttt_mute_team_check", "0")
CreateClientConVar("ttt_avoid_detective", "0", true, true)
HELPSCRN = {}
local dframe
function HELPSCRN:Show()
if IsValid(dframe) then return end
local margin = 15
dframe = vgui.Create("DFrame")
local w, h = 630, 470
dframe:SetSize(w, h)
dframe:Center()
dframe:SetTitle(GetTranslation("help_title"))
dframe:ShowCloseButton(true)
local dbut = vgui.Create("DButton", dframe)
local bw, bh = 50, 25
dbut:SetSize(bw, bh)
dbut:SetPos(w - bw - margin, h - bh - margin/2)
dbut:SetText(GetTranslation("close"))
dbut.DoClick = function() dframe:Close() end
local dtabs = vgui.Create("DPropertySheet", dframe)
dtabs:SetPos(margin, margin * 2)
dtabs:SetSize(w - margin*2, h - margin*3 - bh)
local padding = dtabs:GetPadding()
padding = padding * 2
local tutparent = vgui.Create("DPanel", dtabs)
tutparent:SetPaintBackground(false)
tutparent:StretchToParent(margin, 0, 0, 0)
self:CreateTutorial(tutparent)
dtabs:AddSheet(GetTranslation("help_tut"), tutparent, "icon16/book_open.png", false, false, GetTranslation("help_tut_tip"))
local dsettings = vgui.Create("DPanelList", dtabs)
dsettings:StretchToParent(0,0,padding,0)
dsettings:EnableVerticalScrollbar()
dsettings:SetPadding(10)
dsettings:SetSpacing(10)
--- Interface area
local dgui = vgui.Create("DForm", dsettings)
dgui:SetName(GetTranslation("set_title_gui"))
local cb = nil
dgui:CheckBox(GetTranslation("set_tips"), "ttt_tips_enable")
cb = dgui:NumSlider(GetTranslation("set_startpopup"), "ttt_startpopup_duration", 0, 60, 0)
if cb.Label then
cb.Label:SetWrap(true)
end
cb:SetTooltip(GetTranslation("set_startpopup_tip"))
cb = dgui:NumSlider(GetTranslation("set_cross_opacity"), "ttt_ironsights_crosshair_opacity", 0, 1, 1)
if cb.Label then
cb.Label:SetWrap(true)
end
cb:SetTooltip(GetTranslation("set_cross_opacity"))
cb = dgui:NumSlider(GetTranslation("set_cross_brightness"), "ttt_crosshair_brightness", 0, 1, 1)
if cb.Label then
cb.Label:SetWrap(true)
end
cb = dgui:NumSlider(GetTranslation("set_cross_size"), "ttt_crosshair_size", 0.1, 3, 1)
if cb.Label then
cb.Label:SetWrap(true)
end
dgui:CheckBox(GetTranslation("set_cross_disable"), "ttt_disable_crosshair")
dgui:CheckBox(GetTranslation("set_minimal_id"), "ttt_minimal_targetid")
dgui:CheckBox(GetTranslation("set_healthlabel"), "ttt_health_label")
cb = dgui:CheckBox(GetTranslation("set_lowsights"), "ttt_ironsights_lowered")
cb:SetTooltip(GetTranslation("set_lowsights_tip"))
cb = dgui:CheckBox(GetTranslation("set_fastsw"), "ttt_weaponswitcher_fast")
cb:SetTooltip(GetTranslation("set_fastsw_tip"))
cb = dgui:CheckBox(GetTranslation("set_fastsw_menu"), "ttt_weaponswitcher_displayfast")
cb:SetTooltip(GetTranslation("set_fastswmenu_tip"))
cb = dgui:CheckBox(GetTranslation("set_wswitch"), "ttt_weaponswitcher_stay")
cb:SetTooltip(GetTranslation("set_wswitch_tip"))
cb = dgui:CheckBox(GetTranslation("set_cues"), "ttt_cl_soundcues")
cb = dgui:CheckBox(GetTranslation("set_msg_cue"), "ttt_cl_msg_soundcue")
dsettings:AddItem(dgui)
--- Gameplay area
local dplay = vgui.Create("DForm", dsettings)
dplay:SetName(GetTranslation("set_title_play"))
cb = dplay:CheckBox(GetTranslation("set_avoid_det"), "ttt_avoid_detective")
cb:SetTooltip(GetTranslation("set_avoid_det_tip"))
cb = dplay:CheckBox(GetTranslation("set_specmode"), "ttt_spectator_mode")
cb:SetTooltip(GetTranslation("set_specmode_tip"))
-- For some reason this one defaulted to on, unlike other checkboxes, so
-- force it to the actual value of the cvar (which defaults to off)
local mute = dplay:CheckBox(GetTranslation("set_mute"), "ttt_mute_team_check")
mute:SetValue(GetConVar("ttt_mute_team_check"):GetBool())
mute:SetTooltip(GetTranslation("set_mute_tip"))
dsettings:AddItem(dplay)
--- Language area
local dlanguage = vgui.Create("DForm", dsettings)
dlanguage:SetName(GetTranslation("set_title_lang"))
local dlang = vgui.Create("DComboBox", dlanguage)
dlang:SetConVar("ttt_language")
dlang:AddChoice("Server default", "auto")
for lang, lang_name in pairs(LANG.GetLanguageNames()) do
dlang:AddChoice(lang_name, lang)
end
-- Why is DComboBox not updating the cvar by default?
dlang.OnSelect = function(idx, val, data)
RunConsoleCommand("ttt_language", data)
end
dlang.Think = dlang.ConVarStringThink
dlanguage:Help(GetTranslation("set_lang"))
dlanguage:AddItem(dlang)
dsettings:AddItem(dlanguage)
dtabs:AddSheet(GetTranslation("help_settings"), dsettings, "icon16/wrench.png", false, false, GetTranslation("help_settings_tip"))
hook.Call("TTTSettingsTabs", GAMEMODE, dtabs)
dframe:MakePopup()
end
local function ShowTTTHelp(ply, cmd, args)
HELPSCRN:Show()
end
concommand.Add("ttt_helpscreen", ShowTTTHelp)
-- Some spectator mode bookkeeping
local function SpectateCallback(cv, old, new)
local num = tonumber(new)
if num and (num == 0 or num == 1) then
RunConsoleCommand("ttt_spectate", num)
end
end
cvars.AddChangeCallback("ttt_spectator_mode", SpectateCallback)
local function MuteTeamCallback(cv, old, new)
local num = tonumber(new)
if num and (num == 0 or num == 1) then
RunConsoleCommand("ttt_mute_team", num)
end
end
cvars.AddChangeCallback("ttt_mute_team_check", MuteTeamCallback)
--- Tutorial
local imgpath = "vgui/ttt/help/tut0%d"
local tutorial_pages = 6
function HELPSCRN:CreateTutorial(parent)
local w, h = parent:GetSize()
local m = 5
local bg = vgui.Create("ColoredBox", parent)
bg:StretchToParent(0,0,0,0)
bg:SetTall(330)
bg:SetColor(COLOR_BLACK)
local tut = vgui.Create("DImage", parent)
tut:StretchToParent(0, 0, 0, 0)
tut:SetVerticalScrollbarEnabled(false)
tut:SetImage(Format(imgpath, 1))
tut:SetWide(1024)
tut:SetTall(512)
tut.current = 1
local bw, bh = 100, 30
local bar = vgui.Create("TTTProgressBar", parent)
bar:SetSize(200, bh)
bar:MoveBelow(bg)
bar:CenterHorizontal()
bar:SetMin(1)
bar:SetMax(tutorial_pages)
bar:SetValue(1)
bar:SetColor(Color(0,200,0))
-- fixing your panels...
bar.UpdateText = function(s)
s.Label:SetText(Format("%i / %i", s.m_iValue, s.m_iMax))
end
bar:UpdateText()
local bnext = vgui.Create("DButton", parent)
bnext:SetFont("Trebuchet22")
bnext:SetSize(bw, bh)
bnext:SetText(GetTranslation("next"))
bnext:CopyPos(bar)
bnext:AlignRight(1)
local bprev = vgui.Create("DButton", parent)
bprev:SetFont("Trebuchet22")
bprev:SetSize(bw, bh)
bprev:SetText(GetTranslation("prev"))
bprev:CopyPos(bar)
bprev:AlignLeft()
bnext.DoClick = function()
if tut.current < tutorial_pages then
tut.current = tut.current + 1
tut:SetImage(Format(imgpath, tut.current))
bar:SetValue(tut.current)
end
end
bprev.DoClick = function()
if tut.current > 1 then
tut.current = tut.current - 1
tut:SetImage(Format(imgpath, tut.current))
bar:SetValue(tut.current)
end
end
end

View File

@@ -0,0 +1,384 @@
--[[
| 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/
--]]
-- HUD HUD HUD
local table = table
local surface = surface
local draw = draw
local math = math
local string = string
local GetTranslation = LANG.GetTranslation
local GetPTranslation = LANG.GetParamTranslation
local GetLang = LANG.GetUnsafeLanguageTable
local interp = string.Interp
-- Fonts
surface.CreateFont("TraitorState", {font = "Trebuchet24",
size = 28,
weight = 1000})
surface.CreateFont("TimeLeft", {font = "Trebuchet24",
size = 24,
weight = 800})
surface.CreateFont("HealthAmmo", {font = "Trebuchet24",
size = 24,
weight = 750})
-- Color presets
local bg_colors = {
background_main = Color(0, 0, 10, 200),
noround = Color(100,100,100,200),
traitor = Color(200, 25, 25, 200),
innocent = Color(25, 200, 25, 200),
detective = Color(25, 25, 200, 200)
};
local health_colors = {
border = COLOR_WHITE,
background = Color(100, 25, 25, 222),
fill = Color(200, 50, 50, 250)
};
local ammo_colors = {
border = COLOR_WHITE,
background = Color(20, 20, 5, 222),
fill = Color(205, 155, 0, 255)
};
-- Modified RoundedBox
local Tex_Corner8 = surface.GetTextureID( "gui/corner8" )
local function RoundedMeter( bs, x, y, w, h, color)
surface.SetDrawColor(clr(color))
surface.DrawRect( x+bs, y, w-bs*2, h )
surface.DrawRect( x, y+bs, bs, h-bs*2 )
surface.SetTexture( Tex_Corner8 )
surface.DrawTexturedRectRotated( x + bs/2 , y + bs/2, bs, bs, 0 )
surface.DrawTexturedRectRotated( x + bs/2 , y + h -bs/2, bs, bs, 90 )
if w > 14 then
surface.DrawRect( x+w-bs, y+bs, bs, h-bs*2 )
surface.DrawTexturedRectRotated( x + w - bs/2 , y + bs/2, bs, bs, 270 )
surface.DrawTexturedRectRotated( x + w - bs/2 , y + h - bs/2, bs, bs, 180 )
else
surface.DrawRect( x + math.max(w-bs, bs), y, bs/2, h )
end
end
---- The bar painting is loosely based on:
---- http://wiki.garrysmod.com/?title=Creating_a_HUD
-- Paints a graphical meter bar
local function PaintBar(x, y, w, h, colors, value)
-- Background
-- slightly enlarged to make a subtle border
draw.RoundedBox(8, x-1, y-1, w+2, h+2, colors.background)
-- Fill
local width = w * math.Clamp(value, 0, 1)
if width > 0 then
RoundedMeter(8, x, y, width, h, colors.fill)
end
end
local roundstate_string = {
[ROUND_WAIT] = "round_wait",
[ROUND_PREP] = "round_prep",
[ROUND_ACTIVE] = "round_active",
[ROUND_POST] = "round_post"
};
-- Returns player's ammo information
local function GetAmmo(ply)
local weap = ply:GetActiveWeapon()
if not weap or not ply:Alive() then return -1 end
local ammo_inv = weap:Ammo1() or 0
local ammo_clip = weap:Clip1() or 0
local ammo_max = weap.Primary.ClipSize or 0
return ammo_clip, ammo_max, ammo_inv
end
local function DrawBg(x, y, width, height, client)
-- Traitor area sizes
local th = 30
local tw = 170
-- Adjust for these
y = y - th
height = height + th
-- main bg area, invariant
-- encompasses entire area
draw.RoundedBox(8, x, y, width, height, bg_colors.background_main)
-- main border, traitor based
local col = bg_colors.innocent
if GAMEMODE.round_state != ROUND_ACTIVE then
col = bg_colors.noround
elseif client:GetTraitor() then
col = bg_colors.traitor
elseif client:GetDetective() then
col = bg_colors.detective
end
draw.RoundedBox(8, x, y, tw, th, col)
end
local sf = surface
local dr = draw
local function ShadowedText(text, font, x, y, color, xalign, yalign)
dr.SimpleText(text, font, x+2, y+2, COLOR_BLACK, xalign, yalign)
dr.SimpleText(text, font, x, y, color, xalign, yalign)
end
local margin = 10
-- Paint punch-o-meter
local function PunchPaint(client)
local L = GetLang()
local punch = client:GetNWFloat("specpunches", 0)
local width, height = 200, 25
local x = ScrW() / 2 - width/2
local y = margin/2 + height
PaintBar(x, y, width, height, ammo_colors, punch)
local color = bg_colors.background_main
dr.SimpleText(L.punch_title, "HealthAmmo", ScrW() / 2, y, color, TEXT_ALIGN_CENTER)
dr.SimpleText(L.punch_help, "TabLarge", ScrW() / 2, margin, COLOR_WHITE, TEXT_ALIGN_CENTER)
local bonus = client:GetNWInt("bonuspunches", 0)
if bonus != 0 then
local text
if bonus < 0 then
text = interp(L.punch_bonus, {num = bonus})
else
text = interp(L.punch_malus, {num = bonus})
end
dr.SimpleText(text, "TabLarge", ScrW() / 2, y * 2, COLOR_WHITE, TEXT_ALIGN_CENTER)
end
end
local key_params = { usekey = Key("+use", "USE") }
local function SpecHUDPaint(client)
local L = GetLang() -- for fast direct table lookups
-- Draw round state
local x = margin
local height = 32
local width = 250
local round_y = ScrH() - height - margin
-- move up a little on low resolutions to allow space for spectator hints
if ScrW() < 1000 then round_y = round_y - 15 end
local time_x = x + 170
local time_y = round_y + 4
draw.RoundedBox(8, x, round_y, width, height, bg_colors.background_main)
draw.RoundedBox(8, x, round_y, time_x - x, height, bg_colors.noround)
local text = L[ roundstate_string[GAMEMODE.round_state] ]
ShadowedText(text, "TraitorState", x + margin, round_y, COLOR_WHITE)
-- Draw round/prep/post time remaining
local text = util.SimpleTime(math.max(0, GetGlobalFloat("ttt_round_end", 0) - CurTime()), "%02i:%02i")
ShadowedText(text, "TimeLeft", time_x + margin, time_y, COLOR_WHITE)
local tgt = client:GetObserverTarget()
if IsValid(tgt) and tgt:IsPlayer() then
ShadowedText(tgt:Nick(), "TimeLeft", ScrW() / 2, margin, COLOR_WHITE, TEXT_ALIGN_CENTER)
elseif IsValid(tgt) and tgt:GetNWEntity("spec_owner", nil) == client then
PunchPaint(client)
else
ShadowedText(interp(L.spec_help, key_params), "TabLarge", ScrW() / 2, margin, COLOR_WHITE, TEXT_ALIGN_CENTER)
end
end
local ttt_health_label = CreateClientConVar("ttt_health_label", "0", true)
local function InfoPaint(client)
local L = GetLang()
local width = 250
local height = 90
local x = margin
local y = ScrH() - margin - height
DrawBg(x, y, width, height, client)
local bar_height = 25
local bar_width = width - (margin*2)
-- Draw health
local health = math.max(0, client:Health())
local health_y = y + margin
PaintBar(x + margin, health_y, bar_width, bar_height, health_colors, health/client:GetMaxHealth())
ShadowedText(tostring(health), "HealthAmmo", bar_width, health_y, COLOR_WHITE, TEXT_ALIGN_RIGHT, TEXT_ALIGN_RIGHT)
if ttt_health_label:GetBool() then
local health_status = util.HealthToString(health, client:GetMaxHealth())
draw.SimpleText(L[health_status], "TabLarge", x + margin*2, health_y + bar_height/2, COLOR_WHITE, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
end
-- Draw ammo
if client:GetActiveWeapon().Primary then
local ammo_clip, ammo_max, ammo_inv = GetAmmo(client)
if ammo_clip != -1 then
local ammo_y = health_y + bar_height + margin
PaintBar(x+margin, ammo_y, bar_width, bar_height, ammo_colors, ammo_clip/ammo_max)
local text = string.format("%i + %02i", ammo_clip, ammo_inv)
ShadowedText(text, "HealthAmmo", bar_width, ammo_y, COLOR_WHITE, TEXT_ALIGN_RIGHT, TEXT_ALIGN_RIGHT)
end
end
-- Draw traitor state
local round_state = GAMEMODE.round_state
local traitor_y = y - 30
local text = nil
if round_state == ROUND_ACTIVE then
text = L[ client:GetRoleStringRaw() ]
else
text = L[ roundstate_string[round_state] ]
end
ShadowedText(text, "TraitorState", x + margin + 73, traitor_y, COLOR_WHITE, TEXT_ALIGN_CENTER)
-- Draw round time
local is_haste = HasteMode() and round_state == ROUND_ACTIVE
local is_traitor = client:IsActiveTraitor()
local endtime = GetGlobalFloat("ttt_round_end", 0) - CurTime()
local text
local font = "TimeLeft"
local color = COLOR_WHITE
local rx = x + margin + 170
local ry = traitor_y + 3
-- Time displays differently depending on whether haste mode is on,
-- whether the player is traitor or not, and whether it is overtime.
if is_haste then
local hastetime = GetGlobalFloat("ttt_haste_end", 0) - CurTime()
if hastetime < 0 then
if (not is_traitor) or (math.ceil(CurTime()) % 7 <= 2) then
-- innocent or blinking "overtime"
text = L.overtime
font = "Trebuchet18"
-- need to hack the position a little because of the font switch
ry = ry + 5
rx = rx - 3
else
-- traitor and not blinking "overtime" right now, so standard endtime display
text = util.SimpleTime(math.max(0, endtime), "%02i:%02i")
color = COLOR_RED
end
else
-- still in starting period
local t = hastetime
if is_traitor and math.ceil(CurTime()) % 6 < 2 then
t = endtime
color = COLOR_RED
end
text = util.SimpleTime(math.max(0, t), "%02i:%02i")
end
else
-- bog standard time when haste mode is off (or round not active)
text = util.SimpleTime(math.max(0, endtime), "%02i:%02i")
end
ShadowedText(text, font, rx, ry, color)
if is_haste then
dr.SimpleText(L.hastemode, "TabLarge", x + margin + 165, traitor_y - 8)
end
end
-- Paints player status HUD element in the bottom left
function GM:HUDPaint()
local client = LocalPlayer()
if hook.Call( "HUDShouldDraw", GAMEMODE, "TTTTargetID" ) then
hook.Call( "HUDDrawTargetID", GAMEMODE )
end
if hook.Call( "HUDShouldDraw", GAMEMODE, "TTTMStack" ) then
MSTACK:Draw(client)
end
if (not client:Alive()) or client:Team() == TEAM_SPEC then
if hook.Call( "HUDShouldDraw", GAMEMODE, "TTTSpecHUD" ) then
SpecHUDPaint(client)
end
return
end
if hook.Call( "HUDShouldDraw", GAMEMODE, "TTTRadar" ) then
RADAR:Draw(client)
end
if hook.Call( "HUDShouldDraw", GAMEMODE, "TTTTButton" ) then
TBHUD:Draw(client)
end
if hook.Call( "HUDShouldDraw", GAMEMODE, "TTTWSwitch" ) then
WSWITCH:Draw(client)
end
if hook.Call( "HUDShouldDraw", GAMEMODE, "TTTVoice" ) then
VOICE.Draw(client)
end
if hook.Call( "HUDShouldDraw", GAMEMODE, "TTTDisguise" ) then
DISGUISE.Draw(client)
end
if hook.Call( "HUDShouldDraw", GAMEMODE, "TTTPickupHistory" ) then
hook.Call( "HUDDrawPickupHistory", GAMEMODE )
end
-- Draw bottom left info panel
if hook.Call( "HUDShouldDraw", GAMEMODE, "TTTInfoPanel" ) then
InfoPaint(client)
end
end
-- Hide the standard HUD stuff
local hud = {["CHudHealth"] = true, ["CHudBattery"] = true, ["CHudAmmo"] = true, ["CHudSecondaryAmmo"] = true}
function GM:HUDShouldDraw(name)
if hud[name] then return false end
return self.BaseClass.HUDShouldDraw(self, name)
end

View File

@@ -0,0 +1,203 @@
--[[
| 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/
--]]
local TryTranslation = LANG.TryTranslation
GM.PickupHistory = {}
GM.PickupHistoryLast = 0
GM.PickupHistoryTop = ScrH() / 2
GM.PickupHistoryWide = 300
GM.PickupHistoryCorner = surface.GetTextureID( "gui/corner8" )
local pickupclr = {
[ROLE_INNOCENT] = Color(55, 170, 50, 255),
[ROLE_TRAITOR] = Color(180, 50, 40, 255),
[ROLE_DETECTIVE] = Color(50, 60, 180, 255)
}
function GM:HUDWeaponPickedUp( wep )
if not (IsValid(wep) and IsValid(LocalPlayer())) or (not LocalPlayer():Alive()) then return end
local name = TryTranslation(wep.GetPrintName and wep:GetPrintName() or wep:GetClass() or "Unknown Weapon Name")
local pickup = {}
pickup.time = CurTime()
pickup.name = string.upper(name)
pickup.holdtime = 5
pickup.font = "DefaultBold"
pickup.fadein = 0.04
pickup.fadeout = 0.3
local role = LocalPlayer().GetRole and LocalPlayer():GetRole() or ROLE_INNOCENT
pickup.color = pickupclr[role]
pickup.upper = true
surface.SetFont( pickup.font )
local w, h = surface.GetTextSize( pickup.name )
pickup.height = h
pickup.width = w
if (self.PickupHistoryLast >= pickup.time) then
pickup.time = self.PickupHistoryLast + 0.05
end
table.insert( self.PickupHistory, pickup )
self.PickupHistoryLast = pickup.time
end
function GM:HUDItemPickedUp( itemname )
if not (IsValid(LocalPlayer()) and LocalPlayer():Alive()) then return end
local pickup = {}
pickup.time = CurTime()
-- as far as I'm aware TTT does not use any "items", so better leave this to
-- source's localisation
pickup.name = "#"..itemname
pickup.holdtime = 5
pickup.font = "DefaultBold"
pickup.fadein = 0.04
pickup.fadeout = 0.3
pickup.color = Color( 255, 255, 255, 255 )
pickup.upper = false
surface.SetFont( pickup.font )
local w, h = surface.GetTextSize( pickup.name )
pickup.height = h
pickup.width = w
if self.PickupHistoryLast >= pickup.time then
pickup.time = self.PickupHistoryLast + 0.05
end
table.insert( self.PickupHistory, pickup )
self.PickupHistoryLast = pickup.time
end
function GM:HUDAmmoPickedUp( itemname, amount )
if not (IsValid(LocalPlayer()) and LocalPlayer():Alive()) then return end
local itemname_trans = TryTranslation(string.lower("ammo_" .. itemname))
if self.PickupHistory then
local localized_name = string.upper(itemname_trans)
for k, v in pairs( self.PickupHistory ) do
if v.name == localized_name then
v.amount = tostring( tonumber(v.amount) + amount )
v.time = CurTime() - v.fadein
return
end
end
end
local pickup = {}
pickup.time = CurTime()
pickup.name = string.upper(itemname_trans)
pickup.holdtime = 5
pickup.font = "DefaultBold"
pickup.fadein = 0.04
pickup.fadeout = 0.3
pickup.color = Color(205, 155, 0, 255)
pickup.amount = tostring(amount)
surface.SetFont( pickup.font )
local w, h = surface.GetTextSize( pickup.name )
pickup.height = h
pickup.width = w
local w, h = surface.GetTextSize( pickup.amount )
pickup.xwidth = w
pickup.width = pickup.width + w + 16
if (self.PickupHistoryLast >= pickup.time) then
pickup.time = self.PickupHistoryLast + 0.05
end
table.insert( self.PickupHistory, pickup )
self.PickupHistoryLast = pickup.time
end
function GM:HUDDrawPickupHistory()
if (not self.PickupHistory) then return end
local x, y = ScrW() - self.PickupHistoryWide - 20, self.PickupHistoryTop
local tall = 0
local wide = 0
for k, v in pairs( self.PickupHistory ) do
if v.time < CurTime() then
if (v.y == nil) then v.y = y end
v.y = (v.y*5 + y) / 6
local delta = (v.time + v.holdtime) - CurTime()
delta = delta / v.holdtime
local alpha = 255
local colordelta = math.Clamp( delta, 0.6, 0.7 )
if delta > (1 - v.fadein) then
alpha = math.Clamp( (1.0 - delta) * (255/v.fadein), 0, 255 )
elseif delta < v.fadeout then
alpha = math.Clamp( delta * (255/v.fadeout), 0, 255 )
end
v.x = x + self.PickupHistoryWide - (self.PickupHistoryWide * (alpha/255))
local rx, ry, rw, rh = math.Round(v.x-4), math.Round(v.y-(v.height/2)-4), math.Round(self.PickupHistoryWide+9), math.Round(v.height+8)
local bordersize = 8
surface.SetTexture( self.PickupHistoryCorner )
surface.SetDrawColor( v.color.r, v.color.g, v.color.b, alpha )
surface.DrawTexturedRectRotated( rx + bordersize/2 , ry + bordersize/2, bordersize, bordersize, 0 )
surface.DrawTexturedRectRotated( rx + bordersize/2 , ry + rh -bordersize/2, bordersize, bordersize, 90 )
surface.DrawRect( rx, ry+bordersize, bordersize, rh-bordersize*2 )
surface.DrawRect( rx+bordersize, ry, v.height - 4, rh )
--surface.SetDrawColor( 230*colordelta, 230*colordelta, 230*colordelta, alpha )
surface.SetDrawColor( 20*colordelta, 20*colordelta, 20*colordelta, math.Clamp(alpha, 0, 200) )
surface.DrawRect( rx+bordersize+v.height-4, ry, rw - (v.height - 4) - bordersize*2, rh )
surface.DrawTexturedRectRotated( rx + rw - bordersize/2 , ry + rh - bordersize/2, bordersize, bordersize, 180 )
surface.DrawTexturedRectRotated( rx + rw - bordersize/2 , ry + bordersize/2, bordersize, bordersize, 270 )
surface.DrawRect( rx+rw-bordersize, ry+bordersize, bordersize, rh-bordersize*2 )
draw.SimpleText( v.name, v.font, v.x+2+v.height+8, v.y - (v.height/2)+2, Color( 0, 0, 0, alpha*0.75 ) )
draw.SimpleText( v.name, v.font, v.x+v.height+8, v.y - (v.height/2), Color( 255, 255, 255, alpha ) )
if v.amount then
draw.SimpleText( v.amount, v.font, v.x+self.PickupHistoryWide+2, v.y - (v.height/2)+2, Color( 0, 0, 0, alpha*0.75 ), TEXT_ALIGN_RIGHT )
draw.SimpleText( v.amount, v.font, v.x+self.PickupHistoryWide, v.y - (v.height/2), Color( 255, 255, 255, alpha ), TEXT_ALIGN_RIGHT )
end
y = y + (v.height + 16)
tall = tall + v.height + 18
wide = math.Max( wide, v.width + v.height + 24 )
if alpha == 0 then self.PickupHistory[k] = nil end
end
end
self.PickupHistoryTop = (self.PickupHistoryTop * 5 + ( ScrH() * 0.75 - tall ) / 2 ) / 6
self.PickupHistoryWide = (self.PickupHistoryWide * 5 + wide) / 6
end

View File

@@ -0,0 +1,414 @@
--[[
| 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/
--]]
include("shared.lua")
-- Define GM12 fonts for compatibility
surface.CreateFont("DefaultBold", {font = "Tahoma",
size = 13,
weight = 1000})
surface.CreateFont("TabLarge", {font = "Tahoma",
size = 13,
weight = 700,
shadow = true, antialias = false})
surface.CreateFont("Trebuchet22", {font = "Trebuchet MS",
size = 22,
weight = 900})
include("corpse_shd.lua")
include("player_ext_shd.lua")
include("weaponry_shd.lua")
include("vgui/ColoredBox.lua")
include("vgui/SimpleIcon.lua")
include("vgui/ProgressBar.lua")
include("vgui/ScrollLabel.lua")
include("cl_radio.lua")
include("cl_disguise.lua")
include("cl_transfer.lua")
include("cl_targetid.lua")
include("cl_search.lua")
include("cl_radar.lua")
include("cl_tbuttons.lua")
include("cl_scoreboard.lua")
include("cl_tips.lua")
include("cl_help.lua")
include("cl_hud.lua")
include("cl_msgstack.lua")
include("cl_hudpickup.lua")
include("cl_keys.lua")
include("cl_wepswitch.lua")
include("cl_scoring.lua")
include("cl_scoring_events.lua")
include("cl_popups.lua")
include("cl_equip.lua")
include("cl_voice.lua")
function GM:Initialize()
MsgN("TTT Client initializing...")
GAMEMODE.round_state = ROUND_WAIT
LANG.Init()
self.BaseClass:Initialize()
end
function GM:InitPostEntity()
MsgN("TTT Client post-init...")
net.Start("TTT_Spectate")
net.WriteBool(GetConVar("ttt_spectator_mode"):GetBool())
net.SendToServer()
if not game.SinglePlayer() then
timer.Create("idlecheck", 5, 0, CheckIdle)
end
-- make sure player class extensions are loaded up, and then do some
-- initialization on them
if IsValid(LocalPlayer()) and LocalPlayer().GetTraitor then
GAMEMODE:ClearClientState()
end
timer.Create("cache_ents", 1, 0, GAMEMODE.DoCacheEnts)
RunConsoleCommand("_ttt_request_serverlang")
RunConsoleCommand("_ttt_request_rolelist")
end
function GM:DoCacheEnts()
RADAR:CacheEnts()
TBHUD:CacheEnts()
end
function GM:HUDClear()
RADAR:Clear()
TBHUD:Clear()
end
KARMA = {}
function KARMA.IsEnabled() return GetGlobalBool("ttt_karma", false) end
function GetRoundState() return GAMEMODE.round_state end
local function RoundStateChange(o, n)
if n == ROUND_PREP then
-- prep starts
GAMEMODE:ClearClientState()
GAMEMODE:CleanUpMap()
-- show warning to spec mode players
if GetConVar("ttt_spectator_mode"):GetBool() and IsValid(LocalPlayer())then
LANG.Msg("spec_mode_warning")
end
-- reset cached server language in case it has changed
RunConsoleCommand("_ttt_request_serverlang")
elseif n == ROUND_ACTIVE then
-- round starts
VOICE.CycleMuteState(MUTE_NONE)
CLSCORE:ClearPanel()
-- people may have died and been searched during prep
for _, p in player.Iterator() do
p.search_result = nil
end
-- clear blood decals produced during prep
RunConsoleCommand("r_cleardecals")
GAMEMODE.StartingPlayers = #util.GetAlivePlayers()
elseif n == ROUND_POST then
RunConsoleCommand("ttt_cl_traitorpopup_close")
end
-- stricter checks when we're talking about hooks, because this function may
-- be called with for example o = WAIT and n = POST, for newly connecting
-- players, which hooking code may not expect
if n == ROUND_PREP then
-- can enter PREP from any phase due to ttt_roundrestart
hook.Call("TTTPrepareRound", GAMEMODE)
elseif (o == ROUND_PREP) and (n == ROUND_ACTIVE) then
hook.Call("TTTBeginRound", GAMEMODE)
elseif (o == ROUND_ACTIVE) and (n == ROUND_POST) then
hook.Call("TTTEndRound", GAMEMODE)
end
-- whatever round state we get, clear out the voice flags
for k,v in player.Iterator() do
v.traitor_gvoice = false
end
end
concommand.Add("ttt_print_playercount", function() print(GAMEMODE.StartingPlayers) end)
--- optional sound cues on round start and end
CreateConVar("ttt_cl_soundcues", "0", FCVAR_ARCHIVE)
local cues = {
Sound("ttt/thump01e.mp3"),
Sound("ttt/thump02e.mp3")
};
local function PlaySoundCue()
if GetConVar("ttt_cl_soundcues"):GetBool() then
surface.PlaySound(table.Random(cues))
end
end
GM.TTTBeginRound = PlaySoundCue
GM.TTTEndRound = PlaySoundCue
--- usermessages
local function ReceiveRole()
local client = LocalPlayer()
local role = net.ReadUInt(2)
-- after a mapswitch, server might have sent us this before we are even done
-- loading our code
if not client.SetRole then return end
client:SetRole(role)
Msg("You are: ")
if client:IsTraitor() then MsgN("TRAITOR")
elseif client:IsDetective() then MsgN("DETECTIVE")
else MsgN("INNOCENT") end
end
net.Receive("TTT_Role", ReceiveRole)
local function ReceiveRoleList()
local role = net.ReadUInt(2)
local num_ids = net.ReadUInt(8)
for i=1, num_ids do
local eidx = net.ReadUInt(7) + 1 -- we - 1 worldspawn=0
local ply = player.GetByID(eidx)
if IsValid(ply) and ply.SetRole then
ply:SetRole(role)
if ply:IsTraitor() then
ply.traitor_gvoice = false -- assume traitorchat by default
end
end
end
end
net.Receive("TTT_RoleList", ReceiveRoleList)
-- Round state comm
local function ReceiveRoundState()
local o = GetRoundState()
GAMEMODE.round_state = net.ReadUInt(3)
if o != GAMEMODE.round_state then
RoundStateChange(o, GAMEMODE.round_state)
end
MsgN("Round state: " .. GAMEMODE.round_state)
end
net.Receive("TTT_RoundState", ReceiveRoundState)
-- Cleanup at start of new round
function GM:ClearClientState()
GAMEMODE:HUDClear()
local client = LocalPlayer()
if not client.SetRole then return end -- code not loaded yet
client:SetRole(ROLE_INNOCENT)
client.equipment_items = EQUIP_NONE
client.equipment_credits = 0
client.bought = {}
client.last_id = nil
client.radio = nil
client.called_corpses = {}
VOICE.InitBattery()
for _, p in player.Iterator() do
if IsValid(p) then
p.sb_tag = nil
p:SetRole(ROLE_INNOCENT)
p.search_result = nil
end
end
VOICE.CycleMuteState(MUTE_NONE)
RunConsoleCommand("ttt_mute_team_check", "0")
if GAMEMODE.ForcedMouse then
gui.EnableScreenClicker(false)
end
end
net.Receive("TTT_ClearClientState", GM.ClearClientState)
function GM:CleanUpMap()
-- Ragdolls sometimes stay around on clients. Deleting them can create issues
-- so all we can do is try to hide them.
for _, ent in ipairs(ents.FindByClass("prop_ragdoll")) do
if IsValid(ent) and CORPSE.GetPlayerNick(ent, "") != "" then
ent:SetNoDraw(true)
ent:SetSolid(SOLID_NONE)
ent:SetColor(Color(0,0,0,0))
-- Horrible hack to make targetid ignore this ent, because we can't
-- modify the collision group clientside.
ent.NoTarget = true
end
end
-- This cleans up decals since GMod v100
game.CleanUpMap()
end
-- server tells us to call this when our LocalPlayer has spawned
local function PlayerSpawn()
local as_spec = net.ReadBit() == 1
if as_spec then
TIPS.Show()
else
TIPS.Hide()
end
end
net.Receive("TTT_PlayerSpawned", PlayerSpawn)
local function PlayerDeath()
TIPS.Show()
end
net.Receive("TTT_PlayerDied", PlayerDeath)
function GM:ShouldDrawLocalPlayer(ply) return false end
local view = {origin = vector_origin, angles = angle_zero, fov=0}
function GM:CalcView( ply, origin, angles, fov )
view.origin = origin
view.angles = angles
view.fov = fov
-- first person ragdolling
if ply:Team() == TEAM_SPEC and ply:GetObserverMode() == OBS_MODE_IN_EYE then
local tgt = ply:GetObserverTarget()
if IsValid(tgt) and (not tgt:IsPlayer()) then
-- assume if we are in_eye and not speccing a player, we spec a ragdoll
local eyes = tgt:LookupAttachment("eyes") or 0
eyes = tgt:GetAttachment(eyes)
if eyes then
view.origin = eyes.Pos
view.angles = eyes.Ang
end
end
end
local wep = ply:GetActiveWeapon()
if IsValid(wep) then
local func = wep.CalcView
if func then
view.origin, view.angles, view.fov = func( wep, ply, origin*1, angles*1, fov )
end
end
return view
end
function GM:AddDeathNotice() end
function GM:DrawDeathNotice() end
function GM:Tick()
local client = LocalPlayer()
if IsValid(client) then
if client:Alive() and client:Team() != TEAM_SPEC then
WSWITCH:Think()
RADIO:StoreTarget()
end
VOICE.Tick()
end
end
-- Simple client-based idle checking
local idle = {ang = nil, pos = nil, mx = 0, my = 0, t = 0}
function CheckIdle()
local client = LocalPlayer()
if not IsValid(client) then return end
if not idle.ang or not idle.pos then
-- init things
idle.ang = client:GetAngles()
idle.pos = client:GetPos()
idle.mx = gui.MouseX()
idle.my = gui.MouseY()
idle.t = CurTime()
return
end
if GetRoundState() == ROUND_ACTIVE and client:IsTerror() and client:Alive() then
local idle_limit = GetGlobalInt("ttt_idle_limit", 300) or 300
if idle_limit <= 0 then idle_limit = 300 end -- networking sucks sometimes
if client:GetAngles() != idle.ang then
-- Normal players will move their viewing angles all the time
idle.ang = client:GetAngles()
idle.t = CurTime()
elseif gui.MouseX() != idle.mx or gui.MouseY() != idle.my then
-- Players in eg. the Help will move their mouse occasionally
idle.mx = gui.MouseX()
idle.my = gui.MouseY()
idle.t = CurTime()
elseif client:GetPos():Distance(idle.pos) > 10 then
-- Even if players don't move their mouse, they might still walk
idle.pos = client:GetPos()
idle.t = CurTime()
elseif CurTime() > (idle.t + idle_limit) then
RunConsoleCommand("say", "(AUTOMATED MESSAGE) I have been moved to the Spectator team because I was idle/AFK.")
timer.Simple(0.3, function()
RunConsoleCommand("ttt_spectator_mode", 1)
net.Start("TTT_Spectate")
net.WriteBool(true)
net.SendToServer()
RunConsoleCommand("ttt_cl_idlepopup")
end)
elseif CurTime() > (idle.t + (idle_limit / 2)) then
-- will repeat
LANG.Msg("idle_warning")
end
end
end
function GM:OnEntityCreated(ent)
-- Make ragdolls look like the player that has died
if ent:IsRagdoll() then
local ply = CORPSE.GetPlayer(ent)
if IsValid(ply) then
-- Only copy any decals if this ragdoll was recently created
if ent:GetCreationTime() > CurTime() - 1 then
ent:SnatchModelInstance(ply)
end
-- Copy the color for the PlayerColor matproxy
local playerColor = ply:GetPlayerColor()
ent.GetPlayerColor = function()
return playerColor
end
end
end
return self.BaseClass.OnEntityCreated(self, ent)
end

View File

@@ -0,0 +1,135 @@
--[[
| 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/
--]]
-- Key overrides for TTT specific keyboard functions
local function SendWeaponDrop()
RunConsoleCommand("ttt_dropweapon")
-- Turn off weapon switch display if you had it open while dropping, to avoid
-- inconsistencies.
WSWITCH:Disable()
end
function GM:OnSpawnMenuOpen()
SendWeaponDrop()
end
function GM:PlayerBindPress(ply, bind, pressed)
if not IsValid(ply) then return end
if bind == "invnext" and pressed then
if ply:IsSpec() then
TIPS.Next()
else
WSWITCH:SelectNext()
end
return true
elseif bind == "invprev" and pressed then
if ply:IsSpec() then
TIPS.Prev()
else
WSWITCH:SelectPrev()
end
return true
elseif bind == "+attack" then
if WSWITCH:PreventAttack() then
if not pressed then
WSWITCH:ConfirmSelection()
end
return true
end
elseif bind == "+sprint" then
-- set voice type here just in case shift is no longer down when the
-- PlayerStartVoice hook runs, which might be the case when switching to
-- steam overlay
ply.traitor_gvoice = false
RunConsoleCommand("tvog", "0")
return true
elseif bind == "+use" and pressed then
if ply:IsSpec() then
RunConsoleCommand("ttt_spec_use")
return true
elseif TBHUD:PlayerIsFocused() then
return TBHUD:UseFocused()
end
elseif string.sub(bind, 1, 4) == "slot" and pressed then
local idx = tonumber(string.sub(bind, 5, -1)) or 1
-- if radiomenu is open, override weapon select
if RADIO.Show then
RADIO:SendCommand(idx)
else
WSWITCH:SelectSlot(idx)
end
return true
elseif bind == "+zoom" and pressed then
-- open or close radio
RADIO:ShowRadioCommands(not RADIO.Show)
return true
elseif bind == "+voicerecord" then
if not VOICE.CanSpeak() then
return true
end
elseif bind == "gm_showteam" and pressed and ply:IsSpec() then
local m = VOICE.CycleMuteState()
RunConsoleCommand("ttt_mute_team", m)
return true
elseif bind == "+duck" and pressed and ply:IsSpec() then
if not IsValid(ply:GetObserverTarget()) then
if GAMEMODE.ForcedMouse then
gui.EnableScreenClicker(false)
GAMEMODE.ForcedMouse = false
else
gui.EnableScreenClicker(true)
GAMEMODE.ForcedMouse = true
end
end
elseif bind == "noclip" and pressed then
if not GetConVar("sv_cheats"):GetBool() then
RunConsoleCommand("ttt_equipswitch")
return true
end
elseif (bind == "gmod_undo" or bind == "undo") and pressed then
RunConsoleCommand("ttt_dropammo")
return true
elseif bind == "phys_swap" and pressed then
RunConsoleCommand("ttt_quickslot", "5")
end
end
-- Note that for some reason KeyPress and KeyRelease are called multiple times
-- for the same key event in multiplayer.
function GM:KeyPress(ply, key)
if not IsFirstTimePredicted() then return end
if not IsValid(ply) or ply != LocalPlayer() then return end
if key == IN_SPEED and ply:IsActiveTraitor() then
timer.Simple(0.05, function() permissions.EnableVoiceChat( true ) end)
end
end
function GM:KeyRelease(ply, key)
if not IsFirstTimePredicted() then return end
if not IsValid(ply) or ply != LocalPlayer() then return end
if key == IN_SPEED and ply:IsActiveTraitor() then
timer.Simple(0.05, function() permissions.EnableVoiceChat( false ) end)
end
end
function GM:PlayerButtonUp(ply, btn)
if not IsFirstTimePredicted() then return end
-- Would be nice to clean up this whole "all key handling in massive
-- functions" thing. oh well
if btn == KEY_PAD_ENTER then
WEPS.DisguiseToggle(ply)
end
end

View File

@@ -0,0 +1,378 @@
--[[
| 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/
--]]
---- Clientside language stuff
-- Need to build custom tables of strings. Can't use language.Add as there is no
-- way to access the translated string in Lua. Identifiers only get translated
-- when Source/gmod print them. By using our own table and our own lookup, we
-- have far more control. Maybe it's slower, but maybe not, we aren't scanning
-- strings for "#identifiers" after all.
LANG.Strings = {}
local ttt_language = CreateConVar("ttt_language", "auto", FCVAR_ARCHIVE)
LANG.DefaultLanguage = "english"
LANG.ActiveLanguage = LANG.DefaultLanguage
LANG.ServerLanguage = "english"
local cached_default, cached_active
function LANG.CreateLanguage(raw_lang_name)
if not raw_lang_name then return end
lang_name = string.lower(raw_lang_name)
if not LANG.IsLanguage(lang_name) then
LANG.Strings[lang_name] = {
-- Empty string is very convenient to have, so init with that.
[""] = "",
-- Presentational, not-lowercased language name
language_name = raw_lang_name
}
end
if lang_name == LANG.DefaultLanguage then
cached_default = LANG.Strings[lang_name]
-- when a string is not found in the active or the default language, an
-- error message is shown
setmetatable(LANG.Strings[lang_name],
{
__index = function(tbl, name)
return Format("[ERROR: Translation of %s not found]", name), false
end
})
end
return LANG.Strings[lang_name]
end
-- Add a string to a language. Should not be used in a language file, only for
-- adding strings elsewhere, such as a SWEP script.
function LANG.AddToLanguage(lang_name, string_name, string_text)
lang_name = lang_name and string.lower(lang_name)
if not LANG.IsLanguage(lang_name) then
ErrorNoHalt(Format("Failed to add '%s' to language '%s': language does not exist.\n", tostring(string_name), tostring(lang_name)))
return false
end
LANG.Strings[lang_name][string_name] = string_text
return string_name
end
-- Simple and fastest name->string lookup
function LANG.GetTranslation(name)
return cached_active[name]
end
-- Lookup with no error upon failback, just nil. Slightly slower, but best way
-- to handle lookup of strings that may legitimately fail to exist
-- (eg. SWEP-defined).
function LANG.GetRawTranslation(name)
return rawget(cached_active, name) or rawget(cached_default, name)
end
-- A common idiom
local GetRaw = LANG.GetRawTranslation
function LANG.TryTranslation(name)
return GetRaw(name) or name
end
local interp = string.Interp
-- Parameterised version, performs string interpolation. Slower than
-- GetTranslation.
function LANG.GetParamTranslation(name, params)
return interp(cached_active[name], params)
end
LANG.GetPTranslation = LANG.GetParamTranslation
function LANG.GetTranslationFromLanguage(name, lang_name)
return rawget(LANG.Strings[lang_name], name)
end
-- Ability to perform lookups in the current language table directly is of
-- interest to consumers in draw/think hooks. Grabbing a translation directly
-- from the table is very fast, and much simpler than a local caching solution.
-- Modifying it would typically be a bad idea.
function LANG.GetUnsafeLanguageTable() return cached_active end
function LANG.GetUnsafeNamed(name) return LANG.Strings[name] end
-- Safe and slow access, not sure if it's ever useful.
function LANG.GetLanguageTable(lang_name)
lang_name = lang_name or LANG.ActiveLanguage
local cpy = table.Copy(LANG.Strings[lang_name])
SetFallback(cpy)
return cpy
end
local function SetFallback(tbl)
-- languages may deal with this themselves, or may already have the fallback
local m = getmetatable(tbl)
if m and m.__index then return end
-- Set the __index of the metatable to use the default lang, which makes any
-- keys not found in the table to be looked up in the default. This is faster
-- than using branching ("return lang[x] or default[x] or errormsg") and
-- allows fallback to occur even when consumer code directly accesses the
-- lang table for speed (eg. in a rendering hook).
setmetatable(tbl,
{
__index = cached_default
})
end
function LANG.SetActiveLanguage(lang_name)
lang_name = lang_name and string.lower(lang_name)
if LANG.IsLanguage(lang_name) then
local old_name = LANG.ActiveLanguage
LANG.ActiveLanguage = lang_name
-- cache ref to table to avoid hopping through LANG and Strings every time
cached_active = LANG.Strings[lang_name]
-- set the default lang as fallback, if it hasn't yet
SetFallback(cached_active)
-- some interface elements will want to know so they can update themselves
if old_name != lang_name then
hook.Call("TTTLanguageChanged", GAMEMODE, old_name, lang_name)
end
else
MsgN(Format("The language '%s' does not exist on this server. Falling back to English...", lang_name))
-- fall back to default if possible
if lang_name != LANG.DefaultLanguage then
LANG.SetActiveLanguage(LANG.DefaultLanguage)
end
end
end
function LANG.Init()
local lang_name = ttt_language:GetString()
-- if we want to use the server language, we'll be switching to it as soon as
-- we hear from the server which one it is, for now use default
if LANG.IsServerDefault(lang_name) then
lang_name = LANG.ServerLanguage
end
LANG.SetActiveLanguage(lang_name)
end
function LANG.IsServerDefault(lang_name)
lang_name = string.lower(lang_name)
return lang_name == "server default" or lang_name == "auto"
end
function LANG.IsLanguage(lang_name)
lang_name = lang_name and string.lower(lang_name)
return LANG.Strings[lang_name]
end
local function LanguageChanged(cv, old, new)
if new and new != LANG.ActiveLanguage then
if LANG.IsServerDefault(new) then
new = LANG.ServerLanguage
end
LANG.SetActiveLanguage(new)
end
end
cvars.AddChangeCallback("ttt_language", LanguageChanged)
local function ForceReload()
LANG.SetActiveLanguage("english")
end
concommand.Add("ttt_reloadlang", ForceReload)
-- Get a copy of all available languages (keys in the Strings tbl)
function LANG.GetLanguages()
local langs = {}
for lang, strings in pairs(LANG.Strings) do
table.insert(langs, lang)
end
return langs
end
function LANG.GetLanguageNames()
-- Typically preferable to GetLanguages but separate to avoid API breakage.
local lang_names = {}
for lang, strings in pairs(LANG.Strings) do
lang_names[lang] = strings["language_name"] or string.Capitalize(lang)
end
return lang_names
end
---- Styling
local bgcolor = {
[ROLE_TRAITOR] = Color(150, 0, 0, 200),
[ROLE_DETECTIVE] = Color(0, 0, 150, 200),
[ROLE_INNOCENT] = Color(0, 50, 0, 200)
};
-- Table of styles that can take a string and display it in some position,
-- colour, etc.
LANG.Styles = {
default = function(text)
MSTACK:AddMessage(text)
print("TTT: " .. text)
end,
rolecolour = function(text)
MSTACK:AddColoredBgMessage(text,
bgcolor[ LocalPlayer():GetRole() ])
print("TTT: " .. text)
end,
chat_warn = function(text)
chat.AddText(COLOR_RED, text)
end,
chat_plain = chat.AddText
};
-- Table mapping message name => message style name. If no message style is
-- defined, the default style is used. This is the case for the vast majority of
-- messages at the time of writing.
LANG.MsgStyle = {}
-- Access of message styles
function LANG.GetStyle(name)
return LANG.MsgStyle[name] or LANG.Styles.default
end
-- Set a style by name or directly as style-function
function LANG.SetStyle(name, style)
if isstring(style) then
style = LANG.Styles[style]
end
LANG.MsgStyle[name] = style
end
function LANG.ShowStyledMsg(text, style)
style(text)
end
function LANG.ProcessMsg(name, params)
local raw = LANG.GetTranslation(name)
local text = raw
if params then
-- some of our params may be string names themselves
for k, v in pairs(params) do
if isstring(v) then
local name = LANG.GetNameParam(v)
if not name then continue end
params[k] = LANG.GetTranslation(name)
end
end
text = interp(raw, params)
end
LANG.ShowStyledMsg(text, LANG.GetStyle(name))
end
--- Message style declarations
-- Rather than having a big list of LANG.SetStyle calls, we specify it the other
-- way around here and churn through it in code. This is convenient because
-- we're doing it en-masse for some random interface things spread out over the
-- place.
--
-- Styles of custom SWEP messages and such should use LANG.SetStyle in their
-- script. The SWEP stuff here might be moved out to the SWEPS too.
local styledmessages = {
rolecolour = {
"round_traitors_one",
"round_traitors_more",
"buy_no_stock",
"buy_pending",
"buy_received",
"xfer_no_recip",
"xfer_no_credits",
"xfer_success",
"xfer_received",
"c4_no_disarm",
"tele_failed",
"tele_no_mark",
"tele_marked",
"dna_identify",
"dna_notfound",
"dna_limit",
"dna_decayed",
"dna_killer",
"dna_no_killer",
"dna_armed",
"dna_object",
"dna_gone",
"credit_det_all",
"credit_tr_all",
"credit_kill"
},
chat_plain = {
"body_call",
"disg_turned_on",
"disg_turned_off",
"mute_all",
"mute_off",
"mute_specs",
"mute_living"
},
chat_warn = {
"spec_mode_warning",
"radar_not_owned",
"radar_charging",
"drop_no_room",
"body_burning",
"tele_no_ground",
"tele_no_crouch",
"tele_no_mark_ground",
"tele_no_mark_crouch",
"drop_no_ammo"
}
};
local set_style = LANG.SetStyle
for style, msgs in pairs(styledmessages) do
for _, name in ipairs(msgs) do
set_style(name, style)
end
end

View File

@@ -0,0 +1,246 @@
--[[
| 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/
--]]
---- HUD stuff similar to weapon/ammo pickups but for game status messages
-- This is some of the oldest TTT code, and some of the first Lua code I ever
-- wrote. It's not the greatest.
MSTACK = {}
MSTACK.msgs = {}
MSTACK.last = 0
-- Localise some libs
local table = table
local surface = surface
local draw = draw
local pairs = pairs
-- Constants for configuration
local msgfont = "DefaultBold"
local margin = 6
local msg_width = 400
local text_width = msg_width - (margin * 3) -- three margins for a little more room
local text_height = draw.GetFontHeight(msgfont)
local top_y = margin
local top_x = ScrW() - margin - msg_width
local staytime = 12
local max_items = 8
local fadein = 0.1
local fadeout = 0.6
local movespeed = 2
-- Text colors to render the messages in
local msgcolors = {
traitor_text = COLOR_RED,
generic_text = COLOR_WHITE,
generic_bg = Color(0, 0, 0, 200)
};
-- Total width we take up on screen, for other elements to read
MSTACK.width = msg_width + margin
function MSTACK:AddColoredMessage(text, clr)
local item = {}
item.text = text
item.col = clr
item.bg = msgcolors.generic_bg
self:AddMessageEx(item)
end
function MSTACK:AddColoredBgMessage(text, bg_clr)
local item = {}
item.text = text
item.col = msgcolors.generic_text
item.bg = bg_clr
self:AddMessageEx(item)
end
local ttt_msg_soundcue = CreateClientConVar("ttt_cl_msg_soundcue", "0", true)
-- Internal
function MSTACK:AddMessageEx(item)
item.col = table.Copy(item.col or msgcolors.generic_text)
item.col.a_max = item.col.a
item.bg = table.Copy(item.bg or msgcolors.generic_bg)
item.bg.a_max = item.bg.a
item.text = self:WrapText(item.text, text_width)
-- Height depends on number of lines, which is equal to number of table
-- elements of the wrapped item.text
item.height = (#item.text * text_height) + (margin * (1 + #item.text))
item.time = CurTime()
item.sounded = not ttt_msg_soundcue:GetBool()
item.move_y = -item.height
-- Stagger the fading a bit
if self.last > (item.time - 1) then
item.time = self.last + 1 --item.time + 1
end
-- Insert at the top
table.insert(self.msgs, 1, item)
self.last = item.time
end
-- Add a given message to the stack, will be rendered in a different color if it
-- is a special traitor-only message that traitors should pay attention to.
-- Use the newer AddColoredMessage if you want special colours.
function MSTACK:AddMessage(text, traitor_only)
self:AddColoredBgMessage(text, traitor_only and msgcolors.traitor_bg or msgcolors.generic_bg)
end
-- Oh joy, I get to write my own wrapping function. Thanks Lua!
-- Splits a string into a table of strings that are under the given width.
function MSTACK:WrapText(text, width)
surface.SetFont(msgfont)
-- Any wrapping required?
local w, _ = surface.GetTextSize(text)
if w <= width then
return {text} -- Nope, but wrap in table for uniformity
end
local lines = {""}
for i, wrd in ipairs(string.Explode(" ", text)) do -- No spaces means you're screwed
local l = #lines
local added = lines[l] .. " " .. wrd
w, _ = surface.GetTextSize(added)
if w > text_width then
-- New line needed
table.insert(lines, wrd)
else
-- Safe to tack it on
lines[l] = added
end
end
return lines
end
sound.Add({
name = "TTT.MessageCue",
channel = CHAN_STATIC,
volume = 1.0,
level = SNDLVL_NONE,
pitch = 100,
sound = "ui/hint.wav"
})
local msg_sound = Sound("TTT.MessageCue")
local base_spec = {
font = msgfont,
xalign = TEXT_ALIGN_CENTER,
yalign = TEXT_ALIGN_TOP
};
function MSTACK:Draw(client)
if next(self.msgs) == nil then return end -- fast empty check
local running_y = top_y
for k, item in pairs(self.msgs) do
if item.time < CurTime() then
if item.sounded == false then
client:EmitSound(msg_sound, 80, 250)
item.sounded = true
end
-- Apply move effects to y
local y = running_y + margin + item.move_y
item.move_y = (item.move_y < 0) and item.move_y + movespeed or 0
local delta = (item.time + staytime) - CurTime()
delta = delta / staytime -- pct of staytime left
-- Hurry up if we have too many
if k >= max_items then
delta = delta / 2
end
local alpha = 255
-- These somewhat arcane delta and alpha equations are from gmod's
-- HUDPickup stuff
if delta > 1 - fadein then
alpha = math.Clamp( (1.0 - delta) * (255 / fadein), 0, 255)
elseif delta < fadeout then
alpha = math.Clamp( delta * (255 / fadeout), 0, 255)
end
local height = item.height
-- Background box
item.bg.a = math.Clamp(alpha, 0, item.bg.a_max)
draw.RoundedBox(8, top_x, y, msg_width, height, item.bg)
-- Text
item.col.a = math.Clamp(alpha, 0, item.col.a_max)
local spec = base_spec
spec.color = item.col
for i = 1, #item.text do
spec.text=item.text[i]
local tx = top_x + (msg_width / 2)
local ty = y + margin + (i - 1) * (text_height + margin)
spec.pos={tx, ty}
draw.TextShadow(spec, 1, alpha)
end
if alpha == 0 then
self.msgs[k] = nil
end
running_y = y + height
end
end
end
-- Game state message channel
local function ReceiveGameMsg()
local text = net.ReadString()
local special = net.ReadBit() == 1
print(text)
MSTACK:AddMessage(text, special)
end
net.Receive("TTT_GameMsg", ReceiveGameMsg)
local function ReceiveCustomMsg()
local text = net.ReadString()
local clr = Color(255, 255, 255)
clr.r = net.ReadUInt(8)
clr.g = net.ReadUInt(8)
clr.b = net.ReadUInt(8)
print(text)
MSTACK:AddColoredMessage(text, clr)
end
net.Receive("TTT_GameMsgColor", ReceiveCustomMsg)

View File

@@ -0,0 +1,140 @@
--[[
| 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/
--]]
-- Some popup window stuff
local GetTranslation = LANG.GetTranslation
local GetPTranslation = LANG.GetParamTranslation
---- Round start
local function GetTextForRole(role)
local menukey = Key("+menu_context", "C")
if role == ROLE_INNOCENT then
return GetTranslation("info_popup_innocent")
elseif role == ROLE_DETECTIVE then
return GetPTranslation("info_popup_detective", {menukey = Key("+menu_context", "C")})
else
local traitors = {}
for _, ply in player.Iterator() do
if ply:IsTraitor() then
table.insert(traitors, ply)
end
end
local text
if #traitors > 1 then
local traitorlist = ""
for k, ply in ipairs(traitors) do
if ply != LocalPlayer() then
traitorlist = traitorlist .. string.rep(" ", 42) .. ply:Nick() .. "\n"
end
end
text = GetPTranslation("info_popup_traitor",
{menukey = menukey, traitorlist = traitorlist})
else
text = GetPTranslation("info_popup_traitor_alone", {menukey = menukey})
end
return text
end
end
local startshowtime = CreateConVar("ttt_startpopup_duration", "17", FCVAR_ARCHIVE)
-- shows info about goal and fellow traitors (if any)
local function RoundStartPopup()
-- based on Derma_Message
if startshowtime:GetInt() <= 0 then return end
if not LocalPlayer() then return end
local dframe = vgui.Create( "Panel" )
dframe:SetDrawOnTop( true )
dframe:SetMouseInputEnabled(false)
dframe:SetKeyboardInputEnabled(false)
local color = Color(0,0,0, 200)
dframe.Paint = function(s)
draw.RoundedBox(8, 0, 0, s:GetWide(), s:GetTall(), color)
end
local text = GetTextForRole(LocalPlayer():GetRole())
local dtext = vgui.Create( "DLabel", dframe )
dtext:SetFont("TabLarge")
dtext:SetText(text)
dtext:SizeToContents()
dtext:SetContentAlignment( 5 )
dtext:SetTextColor( color_white )
local w, h = dtext:GetSize()
local m = 10
dtext:SetPos(m,m)
dframe:SetSize( w + m*2, h + m*2 )
dframe:Center()
dframe:AlignBottom( 10 )
timer.Simple(startshowtime:GetInt(), function() dframe:Remove() end)
end
concommand.Add("ttt_cl_startpopup", RoundStartPopup)
--- Idle message
local function IdlePopup()
local w, h = 300, 180
local dframe = vgui.Create("DFrame")
dframe:SetSize(w, h)
dframe:Center()
dframe:SetTitle(GetTranslation("idle_popup_title"))
dframe:SetVisible(true)
dframe:SetMouseInputEnabled(true)
local inner = vgui.Create("DPanel", dframe)
inner:StretchToParent(5, 25, 5, 45)
local idle_limit = GetGlobalInt("ttt_idle_limit", 300) or 300
local text = vgui.Create("DLabel", inner)
text:SetWrap(true)
text:SetText(GetPTranslation("idle_popup", {num = idle_limit, helpkey = Key("gm_showhelp", "F1")}))
text:SetDark(true)
text:StretchToParent(10,5,10,5)
local bw, bh = 75, 25
local cancel = vgui.Create("DButton", dframe)
cancel:SetPos(10, h - 40)
cancel:SetSize(bw, bh)
cancel:SetText(GetTranslation("idle_popup_close"))
cancel.DoClick = function() dframe:Close() end
local disable = vgui.Create("DButton", dframe)
disable:SetPos(w - 185, h - 40)
disable:SetSize(175, bh)
disable:SetText(GetTranslation("idle_popup_off"))
disable.DoClick = function()
RunConsoleCommand("ttt_spectator_mode", "0")
dframe:Close()
end
dframe:MakePopup()
end
concommand.Add("ttt_cl_idlepopup", IdlePopup)

View File

@@ -0,0 +1,322 @@
--[[
| 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/
--]]
-- Traitor radar rendering
local render = render
local surface = surface
local string = string
local player = player
local math = math
RADAR = {}
RADAR.targets = {}
RADAR.enable = false
RADAR.duration = 30
RADAR.endtime = 0
RADAR.bombs = {}
RADAR.bombs_count = 0
RADAR.repeating = true
RADAR.samples = {}
RADAR.samples_count = 0
RADAR.called_corpses = {}
function RADAR:EndScan()
self.enable = false
self.endtime = CurTime()
end
function RADAR:Clear()
self:EndScan()
self.bombs = {}
self.samples = {}
self.bombs_count = 0
self.samples_count = 0
end
function RADAR:Timeout()
self:EndScan()
if self.repeating and LocalPlayer() and LocalPlayer():IsActiveSpecial() and LocalPlayer():HasEquipmentItem(EQUIP_RADAR) then
RunConsoleCommand("ttt_radar_scan")
end
end
-- cache stuff we'll be drawing
function RADAR.CacheEnts()
-- also do some corpse cleanup here
for k, corpse in pairs(RADAR.called_corpses) do
if (corpse.called + 45) < CurTime() then
RADAR.called_corpses[k] = nil -- will make # inaccurate, no big deal
end
end
if RADAR.bombs_count == 0 then return end
-- Update bomb positions for those we know about
for idx, b in pairs(RADAR.bombs) do
local ent = Entity(idx)
if IsValid(ent) then
b.pos = ent:GetPos()
end
end
end
function RADAR.Bought(is_item, id)
if is_item and id == EQUIP_RADAR then
RunConsoleCommand("ttt_radar_scan")
end
end
hook.Add("TTTBoughtItem", "RadarBoughtItem", RADAR.Bought)
local function DrawTarget(tgt, size, offset, no_shrink)
local scrpos = tgt.pos:ToScreen() -- sweet
local sz = (IsOffScreen(scrpos) and (not no_shrink)) and size/2 or size
scrpos.x = math.Clamp(scrpos.x, sz, ScrW() - sz)
scrpos.y = math.Clamp(scrpos.y, sz, ScrH() - sz)
if IsOffScreen(scrpos) then return end
surface.DrawTexturedRect(scrpos.x - sz, scrpos.y - sz, sz * 2, sz * 2)
-- Drawing full size?
if sz == size then
local text = math.ceil(LocalPlayer():GetPos():Distance(tgt.pos))
local w, h = surface.GetTextSize(text)
-- Show range to target
surface.SetTextPos(scrpos.x - w/2, scrpos.y + (offset * sz) - h/2)
surface.DrawText(text)
if tgt.t then
-- Show time
text = util.SimpleTime(tgt.t - CurTime(), "%02i:%02i")
w, h = surface.GetTextSize(text)
surface.SetTextPos(scrpos.x - w / 2, scrpos.y + sz / 2)
surface.DrawText(text)
elseif tgt.nick then
-- Show nickname
text = tgt.nick
w, h = surface.GetTextSize(text)
surface.SetTextPos(scrpos.x - w / 2, scrpos.y + sz / 2)
surface.DrawText(text)
end
end
end
local indicator = surface.GetTextureID("effects/select_ring")
local c4warn = surface.GetTextureID("vgui/ttt/icon_c4warn")
local sample_scan = surface.GetTextureID("vgui/ttt/sample_scan")
local det_beacon = surface.GetTextureID("vgui/ttt/det_beacon")
local GetPTranslation = LANG.GetParamTranslation
local FormatTime = util.SimpleTime
local near_cursor_dist = 180
function RADAR:Draw(client)
if not client then return end
surface.SetFont("HudSelectionText")
-- C4 warnings
if self.bombs_count != 0 and client:IsActiveTraitor() then
surface.SetTexture(c4warn)
surface.SetTextColor(200, 55, 55, 220)
surface.SetDrawColor(255, 255, 255, 200)
for k, bomb in pairs(self.bombs) do
DrawTarget(bomb, 24, 0, true)
end
end
-- Corpse calls
if client:IsActiveDetective() and #self.called_corpses then
surface.SetTexture(det_beacon)
surface.SetTextColor(255, 255, 255, 240)
surface.SetDrawColor(255, 255, 255, 230)
for k, corpse in pairs(self.called_corpses) do
DrawTarget(corpse, 16, 0.5)
end
end
-- Samples
if self.samples_count != 0 then
surface.SetTexture(sample_scan)
surface.SetTextColor(200, 50, 50, 255)
surface.SetDrawColor(255, 255, 255, 240)
for k, sample in pairs(self.samples) do
DrawTarget(sample, 16, 0.5, true)
end
end
-- Player radar
if (not self.enable) or (not client:IsActiveSpecial()) then return end
surface.SetTexture(indicator)
local remaining = math.max(0, RADAR.endtime - CurTime())
local alpha_base = 50 + 180 * (remaining / RADAR.duration)
local mpos = Vector(ScrW() / 2, ScrH() / 2, 0)
local role, alpha, scrpos, md
for k, tgt in pairs(RADAR.targets) do
alpha = alpha_base
scrpos = tgt.pos:ToScreen()
if not scrpos.visible then
continue
end
md = mpos:Distance(Vector(scrpos.x, scrpos.y, 0))
if md < near_cursor_dist then
alpha = math.Clamp(alpha * (md / near_cursor_dist), 40, 230)
end
role = tgt.role or ROLE_INNOCENT
if role == ROLE_TRAITOR then
surface.SetDrawColor(255, 0, 0, alpha)
surface.SetTextColor(255, 0, 0, alpha)
elseif role == ROLE_DETECTIVE then
surface.SetDrawColor(0, 0, 255, alpha)
surface.SetTextColor(0, 0, 255, alpha)
elseif role == 3 then -- decoys
surface.SetDrawColor(150, 150, 150, alpha)
surface.SetTextColor(150, 150, 150, alpha)
else
surface.SetDrawColor(0, 255, 0, alpha)
surface.SetTextColor(0, 255, 0, alpha)
end
DrawTarget(tgt, 24, 0)
end
-- Time until next scan
surface.SetFont("TabLarge")
surface.SetTextColor(255, 0, 0, 230)
local text = GetPTranslation("radar_hud", {time = FormatTime(remaining, "%02i:%02i")})
local w, h = surface.GetTextSize(text)
surface.SetTextPos(36, ScrH() - 140 - h)
surface.DrawText(text)
end
local function ReceiveC4Warn()
local idx = net.ReadUInt(16)
local armed = net.ReadBit() == 1
if armed then
local pos = net.ReadVector()
local etime = net.ReadFloat()
RADAR.bombs[idx] = {pos=pos, t=etime}
else
RADAR.bombs[idx] = nil
end
RADAR.bombs_count = table.Count(RADAR.bombs)
end
net.Receive("TTT_C4Warn", ReceiveC4Warn)
local function ReceiveCorpseCall()
local pos = net.ReadVector()
table.insert(RADAR.called_corpses, {pos = pos, called = CurTime()})
end
net.Receive("TTT_CorpseCall", ReceiveCorpseCall)
local function ReceiveRadarScan()
local num_targets = net.ReadUInt(8)
RADAR.targets = {}
for i=1, num_targets do
local r = net.ReadUInt(2)
local pos = Vector()
pos.x = net.ReadInt(15)
pos.y = net.ReadInt(15)
pos.z = net.ReadInt(15)
table.insert(RADAR.targets, {role=r, pos=pos})
end
RADAR.enable = true
RADAR.endtime = CurTime() + RADAR.duration
timer.Create("radartimeout", RADAR.duration + 1, 1,
function() RADAR:Timeout() end)
end
net.Receive("TTT_Radar", ReceiveRadarScan)
local GetTranslation = LANG.GetTranslation
function RADAR.CreateMenu(parent, frame)
local w, h = parent:GetSize()
local dform = vgui.Create("DForm", parent)
dform:SetName(GetTranslation("radar_menutitle"))
dform:StretchToParent(0,0,0,0)
dform:SetAutoSize(false)
local owned = LocalPlayer():HasEquipmentItem(EQUIP_RADAR)
if not owned then
dform:Help(GetTranslation("radar_not_owned"))
return dform
end
local bw, bh = 100, 25
local dscan = vgui.Create("DButton", dform)
dscan:SetSize(bw, bh)
dscan:SetText(GetTranslation("radar_scan"))
dscan.DoClick = function(s)
s:SetDisabled(true)
RunConsoleCommand("ttt_radar_scan")
frame:Close()
end
dform:AddItem(dscan)
local dlabel = vgui.Create("DLabel", dform)
dlabel:SetText(GetPTranslation("radar_help", {num = RADAR.duration}))
dlabel:SetWrap(true)
dlabel:SetTall(50)
dform:AddItem(dlabel)
local dcheck = vgui.Create("DCheckBoxLabel", dform)
dcheck:SetText(GetTranslation("radar_auto"))
dcheck:SetIndent(5)
dcheck:SetValue(RADAR.repeating)
dcheck.OnChange = function(s, val)
RADAR.repeating = val
end
dform:AddItem(dcheck)
dform.Think = function(s)
if RADAR.enable or not owned then
dscan:SetDisabled(true)
else
dscan:SetDisabled(false)
end
end
dform:SetVisible(true)
return dform
end

View File

@@ -0,0 +1,107 @@
--[[
| 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/
--]]
--- Traitor radio controls
TRADIO = {}
local sound_names = {
scream ="radio_button_scream",
explosion="radio_button_expl",
pistol ="radio_button_pistol",
m16 ="radio_button_m16",
deagle ="radio_button_deagle",
mac10 ="radio_button_mac10",
shotgun ="radio_button_shotgun",
rifle ="radio_button_rifle",
huge ="radio_button_huge",
beeps ="radio_button_c4",
burning ="radio_button_burn",
footsteps="radio_button_steps"
};
local smatrix = {
{"scream", "burning", "explosion", "footsteps"},
{"pistol", "shotgun", "mac10", "deagle"},
{"m16", "rifle", "huge", "beeps"}
};
local function PlayRadioSound(snd)
local r = LocalPlayer().radio
if IsValid(r) then
RunConsoleCommand("ttt_radio_play", tostring(r:EntIndex()), snd)
end
end
local function ButtonClickPlay(s) PlayRadioSound(s.snd) end
local function CreateSoundBoard(parent)
local b = vgui.Create("DPanel", parent)
--b:SetPaintBackground(false)
local bh, bw = 50, 100
local m = 5
local ver = #smatrix
local hor = #smatrix[1]
local x, y = 0, 0
for ri, row in ipairs(smatrix) do
local rj = ri - 1 -- easier for computing x,y
for rk, snd in ipairs(row) do
local rl = rk - 1
y = (rj * m) + (rj * bh)
x = (rl * m) + (rl * bw)
local but = vgui.Create("DButton", b)
but:SetPos(x, y)
but:SetSize(bw, bh)
but:SetText(LANG.GetTranslation(sound_names[snd]))
but.snd = snd
but.DoClick = ButtonClickPlay
end
end
b:SetSize(bw * hor + m * (hor - 1), bh * ver + m * (ver - 1))
b:SetPos(m, 25)
b:CenterHorizontal()
return b
end
function TRADIO.CreateMenu(parent)
local w, h = parent:GetSize()
local client = LocalPlayer()
local wrap = vgui.Create("DPanel", parent)
wrap:SetSize(w, h)
wrap:SetPaintBackground(false)
local dhelp = vgui.Create("DLabel", wrap)
dhelp:SetFont("TabLarge")
dhelp:SetText(LANG.GetTranslation("radio_help"))
dhelp:SetTextColor(COLOR_WHITE)
if IsValid(client.radio) then
local board = CreateSoundBoard(wrap)
elseif client:HasWeapon("weapon_ttt_radio") then
dhelp:SetText(LANG.GetTranslation("radio_notplaced"))
end
dhelp:SizeToContents()
dhelp:SetPos(10, 5)
dhelp:CenterHorizontal()
return wrap
end

View File

@@ -0,0 +1,71 @@
--[[
| 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/
--]]
-- a much requested darker scoreboard
local table = table
local surface = surface
local draw = draw
local math = math
local team = team
local namecolor = {
admin = Color(220, 180, 0, 255)
};
include("vgui/sb_main.lua")
sboard_panel = nil
local function ScoreboardRemove()
if sboard_panel then
sboard_panel:Remove()
sboard_panel = nil
end
end
hook.Add("TTTLanguageChanged", "RebuildScoreboard", ScoreboardRemove)
function GM:ScoreboardCreate()
ScoreboardRemove()
sboard_panel = vgui.Create("TTTScoreboard")
end
function GM:ScoreboardShow()
self.ShowScoreboard = true
if not sboard_panel then
self:ScoreboardCreate()
end
gui.EnableScreenClicker(true)
sboard_panel:SetVisible(true)
sboard_panel:UpdateScoreboard(true)
sboard_panel:StartUpdateTimer()
end
function GM:ScoreboardHide()
self.ShowScoreboard = false
gui.EnableScreenClicker(false)
if sboard_panel then
sboard_panel:SetVisible(false)
end
end
function GM:GetScoreboardPanel()
return sboard_panel
end
function GM:HUDDrawScoreBoard()
-- replaced by panel version
end

View File

@@ -0,0 +1,630 @@
--[[
| 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)

View File

@@ -0,0 +1,262 @@
--[[
| 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/
--]]
---- Event display information for Event Log in the Round Report
---- Usage:
-- Declare a *unique* event identifier in a shared file, eg.
-- EVENT_PANTS = 800
--
-- Use SCORE:AddEvent serverside in whatever way you want.
--
-- Clientside, tell CLSCORE how to display it, like so:
--
-- CLSCORE.DeclareEventDisplay(EVENT_PANTS,
-- { text = function(e)
-- return "Someone wore " .. e.num .. "pants."
-- end,
-- icon = function(e)
-- return myiconmaterial, "MyTooltip"
-- end
-- })
-- Note that custom events don't have to be in this file, just any file that is
-- loaded on the client.
-- Translation helpers
local T = LANG.GetTranslation
local PT = LANG.GetParamTranslation
-- Icons we'll use
local smile_icon = Material("icon16/emoticon_smile.png")
local magnifier_icon = Material("icon16/magnifier.png")
local bomb_icon = Material("icon16/bomb.png")
local wrong_icon = Material("icon16/cross.png")
local right_icon = Material("icon16/tick.png")
local shield_icon = Material("icon16/shield.png")
local star_icon = Material("icon16/star.png")
local app_icon = Material("icon16/application.png")
local credit_icon = Material("icon16/coins.png")
local wrench_icon = Material("icon16/wrench.png")
-- Shorter name, using it lots
local Event = CLSCORE.DeclareEventDisplay
local is_dmg = util.BitSet
-- Round end event
Event(EVENT_FINISH,
{ text = function(e)
if e.win == WIN_TRAITOR then
return T("ev_win_traitor")
elseif e.win == WIN_INNOCENT then
return T("ev_win_inno")
elseif e.win == WIN_TIMELIMIT then
return T("ev_win_time")
end
end,
icon = function(e)
if e.win == WIN_TRAITOR then
return star_icon, "Traitors won"
elseif e.win == WIN_INNOCENT then
return star_icon, "Innocents won"
else
return star_icon, "Timelimit"
end
end
})
-- Round start event
Event(EVENT_GAME,
{ text = function(e)
if e.state == ROUND_ACTIVE then return T("ev_start") end
end,
icon = function(e)
return app_icon, "Game"
end
})
-- Credits event
Event(EVENT_CREDITFOUND,
{ text = function(e)
return PT("ev_credit", {finder = e.ni,
num = e.cr,
player = e.b})
end,
icon = function(e)
return credit_icon, "Credit found"
end
})
Event(EVENT_BODYFOUND,
{ text = function(e)
return PT("ev_body", {finder = e.ni, victim = e.b})
end,
icon = function(e)
return magnifier_icon, "Body discovered"
end
})
-- C4 fun
Event(EVENT_C4DISARM,
{ text = function(e)
return PT(e.s and "ev_c4_disarm1" or "ev_c4_disarm2",
{player = e.ni, owner = e.own or "aliens"})
end,
icon = function(e)
return wrench_icon, "C4 disarm"
end
})
Event(EVENT_C4EXPLODE,
{ text = function(e)
return PT("ev_c4_boom", {player = e.ni})
end,
icon = function(e)
return bomb_icon, "C4 exploded"
end
})
Event(EVENT_C4PLANT,
{ text = function(e)
return PT("ev_c4_plant", {player = e.ni})
end,
icon = function(e)
return bomb_icon, "C4 planted"
end
})
-- Helper fn for kill events
local function GetWeaponName(gun)
local wname = nil
-- Standard TTT weapons are sent as numeric IDs to save bandwidth
if tonumber(gun) then
wname = EnumToWep(gun)
elseif isstring(gun) then
-- Custom weapons or ones that are otherwise ID-less are sent as
-- string
local wep = util.WeaponForClass(gun)
wname = wep and wep.PrintName
end
return wname
end
-- Generating the text for a kill event requires a lot of logic for special
-- cases, resulting in a long function, so defining it separately here.
local function KillText(e)
local dmg = e.dmg
local trap = dmg.n
if trap == "" then trap = nil end
local weapon = GetWeaponName(dmg.g)
if weapon then
weapon = LANG.TryTranslation(weapon)
end
-- there is only ever one piece of equipment present in a language string,
-- all the different names like "trap", "tool" and "weapon" are aliases.
local eq = trap or weapon
local params = {victim = e.vic.ni, attacker = e.att.ni, trap = eq, tool = eq, weapon = eq}
local txt = nil
if e.att.sid64 == e.vic.sid64 then
if is_dmg(dmg.t, DMG_BLAST) then
txt = trap and "ev_blowup_trap" or "ev_blowup"
elseif is_dmg(dmg.t, DMG_SONIC) then
txt = "ev_tele_self"
else
txt = trap and "ev_sui_using" or "ev_sui"
end
end
-- txt will be non-nil if it was a suicide, don't need to do any of the
-- rest in that case
if txt then
return PT(txt, params)
end
-- we will want to know if the death was caused by a player or not
-- (eg. push vs fall)
local ply_attacker = true
-- if we are dealing with an accidental trap death for example, we want to
-- use the trap name as "attacker"
if e.att.ni == "" then
ply_attacker = false
params.attacker = trap or T("something")
end
-- typically the "_using" strings are only for traps
local using = (not weapon)
if is_dmg(dmg.t, DMG_FALL) then
if ply_attacker then
txt = "ev_fall_pushed"
else
txt = "ev_fall"
end
elseif is_dmg(dmg.t, DMG_BULLET) then
txt = "ev_shot"
using = true
elseif is_dmg(dmg.t, DMG_DROWN) then
txt = "ev_drown"
elseif is_dmg(dmg.t, DMG_BLAST) then
txt = "ev_boom"
elseif is_dmg(dmg.t, DMG_BURN) or is_dmg(dmg.t, DMG_DIRECT) then
txt = "ev_burn"
elseif is_dmg(dmg.t, DMG_CLUB) then
txt = "ev_club"
elseif is_dmg(dmg.t, DMG_SLASH) then
txt = "ev_slash"
elseif is_dmg(dmg.t, DMG_SONIC) then
txt = "ev_tele"
elseif is_dmg(dmg.t, DMG_PHYSGUN) then
txt = "ev_goomba"
using = false
elseif is_dmg(dmg.t, DMG_CRUSH) then
txt = "ev_crush"
else
txt = "ev_other"
end
if ply_attacker and (trap or weapon) and using then
txt = txt .. "_using"
end
return PT(txt, params)
end
Event(EVENT_KILL,
{ text = KillText,
icon = function(e)
if e.att.sid64 == e.vic.sid64 or e.att.sid64 == -1 then
return smile_icon, "Suicide"
end
if e.att.tr == e.vic.tr then
return wrong_icon, "Teamkill"
elseif e.att.tr then
return right_icon, "Traitor killed innocent"
else
return shield_icon, "Innocent killed traitor"
end
end
})

View File

@@ -0,0 +1,514 @@
--[[
| 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/
--]]
-- Body search popup
local T = LANG.GetTranslation
local PT = LANG.GetParamTranslation
local is_dmg = util.BitSet
local dtt = { search_dmg_crush = DMG_CRUSH, search_dmg_bullet = DMG_BULLET, search_dmg_fall = DMG_FALL,
search_dmg_boom = DMG_BLAST, search_dmg_club = DMG_CLUB, search_dmg_drown = DMG_DROWN, search_dmg_stab = DMG_SLASH,
search_dmg_burn = DMG_BURN, search_dmg_tele = DMG_SONIC, search_dmg_car = DMG_VEHICLE }
-- "From his body you can tell XXX"
local function DmgToText(d)
for k, v in pairs(dtt) do
if is_dmg(d, v) then
return T(k)
end
end
if is_dmg(d, DMG_DIRECT) then
return T("search_dmg_burn")
end
return T("search_dmg_other")
end
-- Info type to icon mapping
-- Some icons have different appearances based on the data value. These have a
-- separate table inside the TypeToMat table.
-- Those that have a lot of possible data values are defined separately, either
-- as a function or a table.
local dtm = { bullet = DMG_BULLET, rock = DMG_CRUSH, splode = DMG_BLAST, fall = DMG_FALL, fire = DMG_BURN }
local function DmgToMat(d)
for k, v in pairs(dtm) do
if is_dmg(d, v) then
return k
end
end
if is_dmg(d, DMG_DIRECT) then
return "fire"
else
return "skull"
end
end
local function WeaponToIcon(d)
local wep = util.WeaponForClass(d)
return wep and wep.Icon or "vgui/ttt/icon_nades"
end
local TypeToMat = {
nick="id",
words="halp",
eq_armor="armor",
eq_radar="radar",
eq_disg="disguise",
role={[ROLE_TRAITOR]="traitor", [ROLE_DETECTIVE]="det", [ROLE_INNOCENT]="inno"},
c4="code",
dmg=DmgToMat,
wep=WeaponToIcon,
head="head",
dtime="time",
stime="wtester",
lastid="lastid",
kills="list"
}
-- Accessor for better fail handling
local function IconForInfoType(t, data)
local base = "vgui/ttt/icon_"
local mat = TypeToMat[t]
if istable(mat) then
mat = mat[data]
elseif isfunction(mat) then
mat = mat(data)
end
if not mat then
mat = TypeToMat["nick"]
end
-- ugly special casing for weapons, because they are more likely to be
-- customized and hence need more freedom in their icon filename
if t != "wep" then
return base .. mat
else
return mat
end
end
function PreprocSearch(raw)
local search = {}
for t, d in pairs(raw) do
search[t] = {img=nil, text="", p=10}
if t == "nick" then
search[t].text = PT("search_nick", {player = d})
search[t].p = 1
search[t].nick = d
elseif t == "role" then
if d == ROLE_TRAITOR then
search[t].text = T("search_role_t")
elseif d == ROLE_DETECTIVE then
search[t].text = T("search_role_d")
else
search[t].text = T("search_role_i")
end
search[t].p = 2
elseif t == "words" then
if d != "" then
-- only append "--" if there's no ending interpunction
local final = string.match(d, "[\\.\\!\\?]$") != nil
search[t].text = PT("search_words", {lastwords = d .. (final and "" or "--.")})
end
elseif t == "eq_armor" then
if d then
search[t].text = T("search_armor")
search[t].p = 17
end
elseif t == "eq_disg" then
if d then
search[t].text = T("search_disg")
search[t].p = 18
end
elseif t == "eq_radar" then
if d then
search[t].text = T("search_radar")
search[t].p = 19
end
elseif t == "c4" then
if d > 0 then
search[t].text= PT("search_c4", {num = d})
end
elseif t == "dmg" then
search[t].text = DmgToText(d)
search[t].p = 12
elseif t == "wep" then
local wep = util.WeaponForClass(d)
local wname = wep and LANG.TryTranslation(wep.PrintName)
if wname then
search[t].text = PT("search_weapon", {weapon = wname})
end
elseif t == "head" then
if d then
search[t].text = T("search_head")
end
search[t].p = 15
elseif t == "dtime" then
if d != 0 then
local ftime = util.SimpleTime(d, "%02i:%02i")
search[t].text = PT("search_time", {time = ftime})
search[t].text_icon = ftime
search[t].p = 8
end
elseif t == "stime" then
if d > 0 then
local ftime = util.SimpleTime(d, "%02i:%02i")
search[t].text = PT("search_dna", {time = ftime})
search[t].text_icon = ftime
end
elseif t == "kills" then
local num = table.Count(d)
if num == 1 then
local vic = Entity(d[1])
local dc = d[1] == 0 -- disconnected
if dc or (IsValid(vic) and vic:IsPlayer()) then
search[t].text = PT("search_kills1", {player = (dc and "<Disconnected>" or vic:Nick())})
end
elseif num > 1 then
local txt = T("search_kills2") .. "\n"
local nicks = {}
for k, idx in ipairs(d) do
local vic = Entity(idx)
local dc = idx == 0
if dc or (IsValid(vic) and vic:IsPlayer()) then
table.insert(nicks, (dc and "<Disconnected>" or vic:Nick()))
end
end
local last = #nicks
txt = txt .. table.concat(nicks, "\n", 1, last)
search[t].text = txt
end
search[t].p = 30
elseif t == "lastid" then
if d and d.idx != 0 then
local ent = Entity(d.idx)
if IsValid(ent) and ent:IsPlayer() then
search[t].text = PT("search_eyes", {player = ent:Nick()})
search[t].ply = ent
end
end
else
-- Not matching a type, so don't display
search[t] = nil
end
-- anything matching a type but not given a text should be removed
if search[t] and search[t].text == "" then
search[t] = nil
end
-- if there's still something here, we'll be showing it, so find an icon
if search[t] then
search[t].img = IconForInfoType(t, d)
end
end
hook.Call("TTTBodySearchPopulate", nil, search, raw)
return search
end
-- Returns a function meant to override OnActivePanelChanged, which modifies
-- dactive and dtext based on the search information that is associated with the
-- newly selected panel
local function SearchInfoController(search, dactive, dtext)
return function(s, pold, pnew)
local t = pnew.info_type
local data = search[t]
if not data then
ErrorNoHalt("Search: data not found", t, data,"\n")
return
end
-- If wrapping is on, the Label's SizeToContentsY misbehaves for
-- text that does not need wrapping. I long ago stopped wondering
-- "why" when it comes to VGUI. Apply hack, move on.
dtext:GetLabel():SetWrap(#data.text > 50)
dtext:SetText(data.text)
dactive:SetImage(data.img)
end
end
local function ShowSearchScreen(search_raw)
local client = LocalPlayer()
if not IsValid(client) then return end
local m = 8
local bw, bh = 100, 25
local bw_large = 125
local w, h = 425, 260
local rw, rh = (w - m*2), (h - 25 - m*2)
local rx, ry = 0, 0
local rows = 1
local listw, listh = rw, (64 * rows + 6)
local listx, listy = rx, ry
ry = ry + listh + m*2
rx = m
local descw, desch = rw - m*2, 80
local descx, descy = rx, ry
ry = ry + desch + m
local butx, buty = rx, ry
local dframe = vgui.Create("DFrame")
dframe:SetSize(w, h)
dframe:Center()
dframe:SetTitle(T("search_title") .. " - " .. search_raw.nick or "???")
dframe:SetVisible(true)
dframe:ShowCloseButton(true)
dframe:SetMouseInputEnabled(true)
dframe:SetKeyboardInputEnabled(true)
dframe:SetDeleteOnClose(true)
dframe.OnKeyCodePressed = util.BasicKeyHandler
-- contents wrapper
local dcont = vgui.Create("DPanel", dframe)
dcont:SetPaintBackground(false)
dcont:SetSize(rw, rh)
dcont:SetPos(m, 25 + m)
-- icon list
local dlist = vgui.Create("DPanelSelect", dcont)
dlist:SetPos(listx, listy)
dlist:SetSize(listw, listh)
dlist:EnableHorizontal(true)
dlist:SetSpacing(1)
dlist:SetPadding(2)
if dlist.VBar then
dlist.VBar:Remove()
dlist.VBar = nil
end
-- description area
local dscroll = vgui.Create("DHorizontalScroller", dlist)
dscroll:StretchToParent(3,3,3,3)
local ddesc = vgui.Create("ColoredBox", dcont)
ddesc:SetColor(Color(50, 50, 50))
ddesc:SetName(T("search_info"))
ddesc:SetPos(descx, descy)
ddesc:SetSize(descw, desch)
local dactive = vgui.Create("DImage", ddesc)
dactive:SetImage("vgui/ttt/icon_id")
dactive:SetPos(m, m)
dactive:SetSize(64, 64)
local dtext = vgui.Create("ScrollLabel", ddesc)
dtext:SetSize(descw - 120, desch - m*2)
dtext:MoveRightOf(dactive, m*2)
dtext:AlignTop(m)
dtext:SetText("...")
-- buttons
local by = rh - bh - (m/2)
local dident = vgui.Create("DButton", dcont)
dident:SetPos(m, by)
dident:SetSize(bw_large, bh)
dident:SetText(T("search_confirm"))
local id = search_raw.eidx + search_raw.dtime
dident.DoClick = function() RunConsoleCommand("ttt_confirm_death", search_raw.eidx, id) end
dident:SetDisabled(client:IsSpec() or (not client:KeyDownLast(IN_WALK)))
local dcall = vgui.Create("DButton", dcont)
dcall:SetPos(m*2 + bw_large, by)
dcall:SetSize(bw_large, bh)
dcall:SetText(T("search_call"))
dcall.DoClick = function(s)
client.called_corpses = client.called_corpses or {}
table.insert(client.called_corpses, search_raw.eidx)
s:SetDisabled(true)
RunConsoleCommand("ttt_call_detective", search_raw.eidx)
end
dcall:SetDisabled(client:IsSpec() or table.HasValue(client.called_corpses or {}, search_raw.eidx))
local dconfirm = vgui.Create("DButton", dcont)
dconfirm:SetPos(rw - m - bw, by)
dconfirm:SetSize(bw, bh)
dconfirm:SetText(T("close"))
dconfirm.DoClick = function() dframe:Close() end
-- Finalize search data, prune stuff that won't be shown etc
-- search is a table of tables that have an img and text key
local search = PreprocSearch(search_raw)
-- Install info controller that will link up the icons to the text etc
dlist.OnActivePanelChanged = SearchInfoController(search, dactive, dtext)
-- Create table of SimpleIcons, each standing for a piece of search
-- information.
local start_icon = nil
for t, info in SortedPairsByMemberValue(search, "p") do
local ic = nil
-- Certain items need a special icon conveying additional information
if t == "nick" then
local avply = IsValid(search_raw.owner) and search_raw.owner or nil
ic = vgui.Create("SimpleIconAvatar", dlist)
ic:SetPlayer(avply)
start_icon = ic
elseif t == "lastid" then
ic = vgui.Create("SimpleIconAvatar", dlist)
ic:SetPlayer(info.ply)
ic:SetAvatarSize(24)
elseif info.text_icon then
ic = vgui.Create("SimpleIconLabelled", dlist)
ic:SetIconText(info.text_icon)
else
ic = vgui.Create("SimpleIcon", dlist)
end
ic:SetIconSize(64)
ic:SetIcon(info.img)
ic.info_type = t
dlist:AddPanel(ic)
dscroll:AddPanel(ic)
end
dlist:SelectPanel(start_icon)
dframe:MakePopup()
end
local function StoreSearchResult(search)
if search.owner then
-- if existing result was not ours, it was detective's, and should not
-- be overwritten
local ply = search.owner
if (not ply.search_result) or ply.search_result.show then
ply.search_result = search
-- this is useful for targetid
local rag = Entity(search.eidx)
if IsValid(rag) then
rag.search_result = search
end
end
end
end
local function bitsRequired(num)
local bits, max = 0, 1
while max <= num do
bits = bits + 1
max = max + max
end
return bits
end
local plyBits = bitsRequired(game.MaxPlayers())
local search = {}
local function ReceiveRagdollSearch()
search = {}
-- Basic info
search.eidx = net.ReadUInt(16)
search.owner = Entity(net.ReadUInt(plyBits))
if not (IsValid(search.owner) and search.owner:IsPlayer() and (not search.owner:IsTerror())) then
search.owner = nil
end
search.nick = net.ReadString()
-- Equipment
local eq = net.ReadUInt(bitsRequired(EQUIP_MAX))
-- All equipment pieces get their own icon
search.eq_armor = util.BitSet(eq, EQUIP_ARMOR)
search.eq_radar = util.BitSet(eq, EQUIP_RADAR)
search.eq_disg = util.BitSet(eq, EQUIP_DISGUISE)
-- Traitor things
search.role = net.ReadUInt(2)
search.c4 = net.ReadUInt(bitsRequired(C4_WIRE_COUNT))
-- Kill info
search.dmg = net.ReadUInt(30)
search.wep = net.ReadString()
search.head = net.ReadBool()
search.dtime = net.ReadUInt(15)
search.stime = net.ReadUInt(15)
-- Players killed
local num_kills = net.ReadUInt(8)
if num_kills > 0 then
search.kills = {}
for i=1,num_kills do
table.insert(search.kills, net.ReadUInt(plyBits))
end
else
search.kills = nil
end
search.lastid = {idx=net.ReadUInt(plyBits)}
-- should we show a menu for this result?
search.finder = net.ReadUInt(plyBits)
search.show = (LocalPlayer():EntIndex() == search.finder)
--
-- last words
--
local words = net.ReadString()
search.words = (words ~= "") and words or nil
hook.Call("TTTBodySearchEquipment", nil, search, eq)
if search.show and hook.Run("TTTShowSearchScreen", search) ~= false then
ShowSearchScreen(search)
end
StoreSearchResult(search)
search = nil
end
net.Receive("TTT_RagdollSearch", ReceiveRagdollSearch)

View File

@@ -0,0 +1,367 @@
--[[
| 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/
--]]
local util = util
local surface = surface
local draw = draw
local GetPTranslation = LANG.GetParamTranslation
local GetRaw = LANG.GetRawTranslation
local key_params = {usekey = Key("+use", "USE"), walkkey = Key("+walk", "WALK")}
local ClassHint = {
prop_ragdoll = {
name= "corpse",
hint= "corpse_hint",
fmt = function(ent, txt) return GetPTranslation(txt, key_params) end
}
};
-- Access for servers to display hints using their own HUD/UI.
function GM:GetClassHints()
return ClassHint
end
-- Basic access for servers to add/modify hints. They override hints stored on
-- the entities themselves.
function GM:AddClassHint(cls, hint)
ClassHint[cls] = table.Copy(hint)
end
---- "T" indicator above traitors
local indicator_mat = Material("vgui/ttt/sprite_traitor")
local indicator_col = Color(255, 255, 255, 130)
local client, pos, dir, tgt
local propspec_outline = Material("models/props_combine/portalball001_sheet")
-- using this hook instead of pre/postplayerdraw because playerdraw seems to
-- happen before certain entities are drawn, which then clip over the sprite
function GM:PostDrawTranslucentRenderables()
client = LocalPlayer()
if client:GetTraitor() then
dir = client:GetForward() * -1
render.SetMaterial(indicator_mat)
for _, ply in player.Iterator() do
if ply:IsActiveTraitor() and ply != client then
pos = ply:GetPos()
pos.z = pos.z + 74
render.DrawQuadEasy(pos, dir, 8, 8, indicator_col, 180)
end
end
end
if client:Team() == TEAM_SPEC then
cam.Start3D(EyePos(), EyeAngles())
for _, ply in player.Iterator() do
tgt = ply:GetObserverTarget()
if IsValid(tgt) and tgt:GetNWEntity("spec_owner", nil) == ply then
render.MaterialOverride(propspec_outline)
render.SuppressEngineLighting(true)
render.SetColorModulation(1, 0.5, 0)
tgt:SetModelScale(1.05, 0)
tgt:DrawModel()
render.SetColorModulation(1, 1, 1)
render.SuppressEngineLighting(false)
render.MaterialOverride(nil)
end
end
cam.End3D()
end
end
---- Spectator labels
local function DrawPropSpecLabels(client)
if (not client:IsSpec()) and (GetRoundState() != ROUND_POST) then return end
surface.SetFont("TabLarge")
local tgt = nil
local scrpos = nil
local text = nil
local w = 0
for _, ply in player.Iterator() do
if ply:IsSpec() then
surface.SetTextColor(220,200,0,120)
tgt = ply:GetObserverTarget()
if IsValid(tgt) and tgt:GetNWEntity("spec_owner", nil) == ply then
scrpos = tgt:GetPos():ToScreen()
else
scrpos = nil
end
else
local _, healthcolor = util.HealthToString(ply:Health(), ply:GetMaxHealth())
surface.SetTextColor(clr(healthcolor))
scrpos = ply:EyePos()
scrpos.z = scrpos.z + 20
scrpos = scrpos:ToScreen()
end
if scrpos and (not IsOffScreen(scrpos)) then
text = ply:Nick()
w, _ = surface.GetTextSize(text)
surface.SetTextPos(scrpos.x - w / 2, scrpos.y)
surface.DrawText(text)
end
end
end
---- Crosshair affairs
surface.CreateFont("TargetIDSmall2", {font = "TargetID",
size = 16,
weight = 1000})
local minimalist = CreateConVar("ttt_minimal_targetid", "0", FCVAR_ARCHIVE)
local magnifier_mat = Material("icon16/magnifier.png")
local ring_tex = surface.GetTextureID("effects/select_ring")
local rag_color = Color(200,200,200,255)
local GetLang = LANG.GetUnsafeLanguageTable
local MAX_TRACE_LENGTH = math.sqrt(3) * 2 * 16384
function GM:HUDDrawTargetID()
local client = LocalPlayer()
local L = GetLang()
if hook.Call( "HUDShouldDraw", GAMEMODE, "TTTPropSpec" ) then
DrawPropSpecLabels(client)
end
local startpos = client:EyePos()
local endpos = client:GetAimVector()
endpos:Mul(MAX_TRACE_LENGTH)
endpos:Add(startpos)
local trace = util.TraceLine({
start = startpos,
endpos = endpos,
mask = MASK_SHOT,
filter = client:GetObserverMode() == OBS_MODE_IN_EYE and {client, client:GetObserverTarget()} or client
})
local ent = trace.Entity
if (not IsValid(ent)) or ent.NoTarget then return end
-- some bools for caching what kind of ent we are looking at
local target_traitor = false
local target_detective = false
local target_corpse = false
local text = nil
local color = COLOR_WHITE
-- if a vehicle, we identify the driver instead
if IsValid(ent:GetNWEntity("ttt_driver", nil)) then
ent = ent:GetNWEntity("ttt_driver", nil)
if ent == client then return end
end
local cls = ent:GetClass()
local minimal = minimalist:GetBool()
local hint = (not minimal) and (ent.TargetIDHint or ClassHint[cls])
if ent:IsPlayer() then
if ent:GetNWBool("disguised", false) then
client.last_id = nil
if client:IsTraitor() or client:IsSpec() then
text = ent:Nick() .. L.target_disg
else
-- Do not show anything
return
end
color = COLOR_RED
else
text = ent:Nick()
client.last_id = ent
end
local _ -- Stop global clutter
-- in minimalist targetID, colour nick with health level
if minimal then
_, color = util.HealthToString(ent:Health(), ent:GetMaxHealth())
end
if client:IsTraitor() and GetRoundState() == ROUND_ACTIVE then
target_traitor = ent:IsTraitor()
end
target_detective = GetRoundState() > ROUND_PREP and ent:IsDetective() or false
elseif cls == "prop_ragdoll" then
-- only show this if the ragdoll has a nick, else it could be a mattress
if CORPSE.GetPlayerNick(ent, false) == false then return end
target_corpse = true
if CORPSE.GetFound(ent, false) or not DetectiveMode() then
text = CORPSE.GetPlayerNick(ent, "A Terrorist")
else
text = L.target_unid
color = COLOR_YELLOW
end
elseif not hint then
-- Not something to ID and not something to hint about
return
end
local x_orig = ScrW() / 2.0
local x = x_orig
local y = ScrH() / 2.0
local w, h = 0,0 -- text width/height, reused several times
if target_traitor or target_detective then
surface.SetTexture(ring_tex)
if target_traitor then
surface.SetDrawColor(255, 0, 0, 200)
else
surface.SetDrawColor(0, 0, 255, 220)
end
surface.DrawTexturedRect(x-32, y-32, 64, 64)
end
y = y + 30
local font = "TargetID"
surface.SetFont( font )
-- Draw main title, ie. nickname
if text then
w, h = surface.GetTextSize( text )
x = x - w / 2
draw.SimpleText( text, font, x+1, y+1, COLOR_BLACK )
draw.SimpleText( text, font, x, y, color )
-- for ragdolls searched by detectives, add icon
if ent.search_result and client:IsDetective() then
-- if I am detective and I know a search result for this corpse, then I
-- have searched it or another detective has
surface.SetMaterial(magnifier_mat)
surface.SetDrawColor(200, 200, 255, 255)
surface.DrawTexturedRect(x + w + 5, y, 16, 16)
end
y = y + h + 4
end
-- Minimalist target ID only draws a health-coloured nickname, no hints, no
-- karma, no tag
if minimal then return end
-- Draw subtitle: health or type
local clr = rag_color
if ent:IsPlayer() then
text, clr = util.HealthToString(ent:Health(), ent:GetMaxHealth())
-- HealthToString returns a string id, need to look it up
text = L[text]
elseif hint then
text = GetRaw(hint.name) or hint.name
else
return
end
font = "TargetIDSmall2"
surface.SetFont( font )
w, h = surface.GetTextSize( text )
x = x_orig - w / 2
draw.SimpleText( text, font, x+1, y+1, COLOR_BLACK )
draw.SimpleText( text, font, x, y, clr )
font = "TargetIDSmall"
surface.SetFont( font )
-- Draw second subtitle: karma
if ent:IsPlayer() and KARMA.IsEnabled() then
text, clr = util.KarmaToString(ent:GetBaseKarma())
text = L[text]
w, h = surface.GetTextSize( text )
y = y + h + 5
x = x_orig - w / 2
draw.SimpleText( text, font, x+1, y+1, COLOR_BLACK )
draw.SimpleText( text, font, x, y, clr )
end
-- Draw key hint
if hint and hint.hint then
if not hint.fmt then
text = GetRaw(hint.hint) or hint.hint
else
text = hint.fmt(ent, hint.hint)
end
w, h = surface.GetTextSize(text)
x = x_orig - w / 2
y = y + h + 5
draw.SimpleText( text, font, x+1, y+1, COLOR_BLACK )
draw.SimpleText( text, font, x, y, COLOR_LGRAY )
end
text = nil
if target_traitor then
text = L.target_traitor
clr = COLOR_RED
elseif target_detective then
text = L.target_detective
clr = COLOR_BLUE
elseif ent.sb_tag and ent.sb_tag.txt != nil then
text = L[ ent.sb_tag.txt ]
clr = ent.sb_tag.color
elseif target_corpse and client:IsActiveTraitor() and CORPSE.GetCredits(ent, 0) > 0 then
text = L.target_credits
clr = COLOR_YELLOW
end
if text then
w, h = surface.GetTextSize( text )
x = x_orig - w / 2
y = y + h + 5
draw.SimpleText( text, font, x+1, y+1, COLOR_BLACK )
draw.SimpleText( text, font, x, y, clr )
end
end

View File

@@ -0,0 +1,170 @@
--[[
| 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/
--]]
--- Display of and interaction with ttt_traitor_button
local surface = surface
local pairs = pairs
local math = math
local abs = math.abs
TBHUD = {}
TBHUD.buttons = {}
TBHUD.buttons_count = 0
TBHUD.focus_ent = nil
TBHUD.focus_stick = 0
function TBHUD:Clear()
self.buttons = {}
self.buttons_count = 0
self.focus_ent = nil
self.focus_stick = 0
end
function TBHUD:CacheEnts()
if IsValid(LocalPlayer()) and LocalPlayer():IsActiveTraitor() then
self.buttons = {}
for _, ent in ipairs(ents.FindByClass("ttt_traitor_button")) do
if IsValid(ent) then
self.buttons[ent:EntIndex()] = ent
end
end
else
self.buttons = {}
end
self.buttons_count = table.Count(self.buttons)
end
function TBHUD:PlayerIsFocused()
return IsValid(LocalPlayer()) and LocalPlayer():IsActiveTraitor() and IsValid(self.focus_ent)
end
function TBHUD:UseFocused()
if IsValid(self.focus_ent) and self.focus_stick >= CurTime() then
RunConsoleCommand("ttt_use_tbutton", tostring(self.focus_ent:EntIndex()))
self.focus_ent = nil
return true
else
return false
end
end
local confirm_sound = Sound("buttons/button24.wav")
function TBHUD.ReceiveUseConfirm()
surface.PlaySound(confirm_sound)
TBHUD:CacheEnts()
end
net.Receive("TTT_ConfirmUseTButton", TBHUD.ReceiveUseConfirm)
local function ComputeRangeFactor(plypos, tgtpos)
local d = tgtpos - plypos
d = d:Dot(d)
return d / range
end
local tbut_normal = surface.GetTextureID("vgui/ttt/tbut_hand_line")
local tbut_focus = surface.GetTextureID("vgui/ttt/tbut_hand_filled")
local size = 32
local mid = size / 2
local focus_range = 25
local use_key = Key("+use", "USE")
local GetTranslation = LANG.GetTranslation
local GetPTranslation = LANG.GetParamTranslation
function TBHUD:Draw(client)
if self.buttons_count != 0 then
surface.SetTexture(tbut_normal)
-- we're doing slowish distance computation here, so lots of probably
-- ineffective micro-optimization
local plypos = client:GetPos()
local midscreen_x = ScrW() / 2
local midscreen_y = ScrH() / 2
local pos, scrpos, d
local focus_ent = nil
local focus_d, focus_scrpos_x, focus_scrpos_y = 0, midscreen_x, midscreen_y
-- draw icon on HUD for every button within range
for k, but in pairs(self.buttons) do
if IsValid(but) and but.IsUsable then
pos = but:GetPos()
scrpos = pos:ToScreen()
if (not IsOffScreen(scrpos)) and but:IsUsable() then
d = pos - plypos
d = d:Dot(d) / (but:GetUsableRange() ^ 2)
-- draw if this button is within range, with alpha based on distance
if d < 1 then
surface.SetDrawColor(255, 255, 255, 200 * (1 - d))
surface.DrawTexturedRect(scrpos.x - mid, scrpos.y - mid, size, size)
if d > focus_d then
local x = abs(scrpos.x - midscreen_x)
local y = abs(scrpos.y - midscreen_y)
if (x < focus_range and y < focus_range and
x < focus_scrpos_x and y < focus_scrpos_y) then
-- avoid constantly switching focus every frame causing
-- 2+ buttons to appear in focus, instead "stick" to one
-- ent for a very short time to ensure consistency
if self.focus_stick < CurTime() or but == self.focus_ent then
focus_ent = but
end
end
end
end
end
end
-- draw extra graphics and information for button when it's in-focus
if IsValid(focus_ent) then
self.focus_ent = focus_ent
self.focus_stick = CurTime() + 0.1
local scrpos = focus_ent:GetPos():ToScreen()
local sz = 16
-- redraw in-focus version of icon
surface.SetTexture(tbut_focus)
surface.SetDrawColor(255, 255, 255, 200)
surface.DrawTexturedRect(scrpos.x - mid, scrpos.y - mid, size, size)
-- description
surface.SetTextColor(255, 50, 50, 255)
surface.SetFont("TabLarge")
local x = scrpos.x + sz + 10
local y = scrpos.y - sz - 3
surface.SetTextPos(x, y)
surface.DrawText(focus_ent:GetDescription())
y = y + 12
surface.SetTextPos(x, y)
if focus_ent:GetDelay() < 0 then
surface.DrawText(GetTranslation("tbut_single"))
elseif focus_ent:GetDelay() == 0 then
surface.DrawText(GetTranslation("tbut_reuse"))
else
surface.DrawText(GetPTranslation("tbut_retime", {num = focus_ent:GetDelay()}))
end
y = y + 12
surface.SetTextPos(x, y)
surface.DrawText(GetPTranslation("tbut_help", {key = use_key}))
end
end
end
end

View File

@@ -0,0 +1,291 @@
--[[
| 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/
--]]
---- Tips panel shown to specs
CreateConVar("ttt_tips_enable", "1", FCVAR_ARCHIVE)
local draw = draw
TIPS = {}
--- Tip cycling button
PANEL = {}
PANEL.Colors = {
default = COLOR_LGRAY,
hover = COLOR_WHITE,
press = COLOR_RED
};
function PANEL:Paint()
-- parent panel will deal with the normal bg, we only need to worry about
-- mouse effects
local clr = self.Colors.default
if self.Depressed then
clr = self.Colors.press
elseif self.Hovered then
clr = self.Colors.hover
end
surface.SetDrawColor(clr.r, clr.g, clr.b, clr.a)
self:DrawOutlinedRect()
end
derma.DefineControl("TipsButton", "Tip cycling button", PANEL, "DButton")
--- Main tip panel
local GetTranslation = LANG.GetTranslation
local GetPTranslation = LANG.GetParamTranslation
local tips_bg = Color(0, 0, 0, 200)
local tip_ids = {}
for i=1, 41 do
table.insert(tip_ids, i)
end
table.Shuffle(tip_ids)
local tip_params = {
[1] = {walkkey = Key("+walk", "WALK"), usekey = Key("+use", "USE")},
[24] = {helpkey = Key("+gm_showhelp", "F1")},
[28] = {mutekey = Key("+gm_showteam", "F2")},
[30] = {zoomkey = Key("+zoom", "the 'Suit Zoom' key")},
[31] = {duckkey = Key("+duck", "DUCK")},
[36] = {helpkey = Key("+gm_showhelp", "F1")},
};
PANEL = {}
function PANEL:Init()
self.IdealWidth = 450
self.IdealHeight = 45
self.BgColor = tips_bg
self.NextSwitch = 0
self.AutoDelay = 15
self.ManualDelay = 25
self.tiptext = vgui.Create("DLabel", self)
self.tiptext:SetContentAlignment(5)
self.tiptext:SetText(GetTranslation("tips_panel_title"))
self.bwrap = vgui.Create("Panel", self)
self.buttons = {}
self.buttons.left = vgui.Create("TipsButton", self.bwrap)
self.buttons.left:SetText("<")
self.buttons.left.DoClick = function() self:PrevTip() end
self.buttons.right = vgui.Create("TipsButton", self.bwrap)
self.buttons.right:SetText(">")
self.buttons.right.DoClick = function() self:NextTip() end
self.buttons.help = vgui.Create("TipsButton", self.bwrap)
self.buttons.help:SetText("?")
self.buttons.help:SetConsoleCommand("ttt_helpscreen")
self.buttons.close = vgui.Create("TipsButton", self.bwrap)
self.buttons.close:SetText("X")
self.buttons.close:SetConsoleCommand("ttt_tips_hide")
self.TipIndex = math.random(1, #tip_ids) or 0
self:SetTip(self.TipIndex)
end
function PANEL:SetTip(idx)
if not idx then
self:SetVisible(false)
return
end
self.TipIndex = idx
local tip_id = tip_ids[idx]
local text = nil
if tip_params[tip_id] then
text = GetPTranslation("tip" .. tip_id, tip_params[tip_id])
else
text = GetTranslation("tip" .. tip_id)
end
self.tiptext:SetText(GetTranslation("tips_panel_tip") .. " " .. text)
self:InvalidateLayout(true)
end
function PANEL:NextTip(auto)
local idx = self.TipIndex + 1
if idx > #tip_ids then
idx = 1
end
self:SetTip(idx)
self.NextSwitch = CurTime() + (auto and self.AutoDelay or self.ManualDelay)
end
function PANEL:PrevTip(auto)
local idx = self.TipIndex - 1
if idx < 1 then
idx = #tip_ids
end
self:SetTip(idx)
self.NextSwitch = CurTime() + (auto and self.AutoDelay or self.ManualDelay)
end
function PANEL:PerformLayout()
local m = 8
local off_bottom = 10
-- need to account for voice stuff in the bottom right and the time in the
-- bottom left
local off_left = 260
local off_right = 250
local room = ScrW() - off_left - off_right
local width = math.min(room, self.IdealWidth)
if width < 200 then
-- people who run 640x480 do not deserve tips
self:SetVisible(false)
return
end
local bsize = 14
-- position buttons
self.bwrap:SetSize(bsize * 2 + 2, bsize * 2 + 2)
self.buttons.left:SetSize(bsize,bsize)
self.buttons.left:SetPos(0,0)
self.buttons.right:SetSize(bsize,bsize)
self.buttons.right:SetPos(bsize + 2, 0)
self.buttons.help:SetSize(bsize,bsize)
self.buttons.help:SetPos(0, bsize + 2)
self.buttons.close:SetSize(bsize,bsize)
self.buttons.close:SetPos(bsize + 2, bsize + 2)
-- position content
self.tiptext:SetPos(m, m)
self.tiptext:SetTall(self.IdealHeight)
self.tiptext:SetWide(width - m*2 - self.bwrap:GetWide())
self.tiptext:SizeToContentsY()
local height = math.max(self.IdealHeight, self.tiptext:GetTall() + m*2)
local x = off_left + ((room - width) / 2)
local y = ScrH() - off_bottom - height
self:SetPos(x, y)
self:SetSize(width, height)
self.bwrap:SetPos(width - self.bwrap:GetWide() - m, height - self.bwrap:GetTall() - m)
end
function PANEL:ApplySchemeSettings()
for k, but in pairs(self.buttons) do
but:SetTextColor(COLOR_WHITE)
but:SetContentAlignment(5)
end
self.bwrap:SetPaintBackgroundEnabled(false)
self.tiptext:SetFont("DefaultBold")
self.tiptext:SetTextColor(COLOR_WHITE)
self.tiptext:SetWrap(true)
end
function PANEL:Paint()
draw.RoundedBox(8, 0, 0, self:GetWide(), self:GetTall(), self.BgColor)
end
function PANEL:Think()
if self.NextSwitch < CurTime() then
self:NextTip(true)
end
end
vgui.Register("TTTTips", PANEL, "Panel")
--- Creation
local tips_panel = nil
function TIPS.Create()
if IsValid(tips_panel) then
tips_panel:Remove()
tips_panel = nil
end
tips_panel = vgui.Create("TTTTips")
-- workaround for layout oddities, give it a poke next tick
timer.Simple(0.1, TIPS.Next)
end
function TIPS.Show()
if not GetConVar("ttt_tips_enable"):GetBool() then return end
if not tips_panel then
TIPS.Create()
end
tips_panel:SetVisible(true)
end
function TIPS.Hide()
if tips_panel then
tips_panel:SetVisible(false)
end
if GAMEMODE.ForcedMouse then
-- currently the only use of unlocking the mouse is screwing around with
-- the hints, and it makes sense to lock the mouse again when closing the
-- tips
gui.EnableScreenClicker(false)
GAMEMODE.ForcedMouse = false
end
end
concommand.Add("ttt_tips_hide", TIPS.Hide)
function TIPS.Next()
if tips_panel then
tips_panel:NextTip()
end
end
function TIPS.Prev()
if tips_panel then
tips_panel:PrevTip()
end
end
local function TipsCallback(cv, prev, new)
if tobool(new) then
if LocalPlayer():IsSpec() then
TIPS.Show()
end
else
TIPS.Hide()
end
end
cvars.AddChangeCallback("ttt_tips_enable", TipsCallback)

View File

@@ -0,0 +1,72 @@
--[[
| 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/
--]]
--- Credit transfer tab for equipment menu
local GetTranslation = LANG.GetTranslation
function CreateTransferMenu(parent)
local dform = vgui.Create("DForm", parent)
dform:SetName(GetTranslation("xfer_menutitle"))
dform:StretchToParent(0,0,0,0)
dform:SetAutoSize(false)
if LocalPlayer():GetCredits() <= 0 then
dform:Help(GetTranslation("xfer_no_credits"))
return dform
end
local bw, bh = 100, 20
local dsubmit = vgui.Create("DButton", dform)
dsubmit:SetSize(bw, bh)
dsubmit:SetDisabled(true)
dsubmit:SetText(GetTranslation("xfer_send"))
local selected_sid64 = nil
local dpick = vgui.Create("DComboBox", dform)
dpick.OnSelect = function(s, idx, val, data)
if data then
selected_sid64 = data
dsubmit:SetDisabled(false)
end
end
dpick:SetWide(250)
-- fill combobox
local r = LocalPlayer():GetRole()
for _, p in player.Iterator() do
if IsValid(p) and p:IsActiveRole(r) and p != LocalPlayer() then
dpick:AddChoice(p:Nick(), p:SteamID64())
end
end
-- select first player by default
if dpick:GetOptionText(1) then dpick:ChooseOptionID(1) end
dsubmit.DoClick = function(s)
if selected_sid64 then
RunConsoleCommand("ttt_transfer_credits", selected_sid64, "1")
end
end
dsubmit.Think = function(s)
if LocalPlayer():GetCredits() < 1 then
s:SetDisabled(true)
end
end
dform:AddItem(dpick)
dform:AddItem(dsubmit)
dform:Help(LANG.GetParamTranslation("xfer_help", {role = LocalPlayer():GetRoleString()}))
return dform
end

View File

@@ -0,0 +1,691 @@
--[[
| 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/
--]]
---- Voicechat popup, radio commands, text chat stuff
DEFINE_BASECLASS("gamemode_base")
local GetTranslation = LANG.GetTranslation
local GetPTranslation = LANG.GetParamTranslation
local string = string
local function LastWordsRecv()
local sender = net.ReadPlayer()
local words = net.ReadString()
local was_detective = IsValid(sender) and sender:IsDetective()
local nick = IsValid(sender) and sender:Nick() or "<Unknown>"
chat.AddText(Color( 150, 150, 150 ),
Format("(%s) ", string.upper(GetTranslation("last_words"))),
was_detective and Color(50, 200, 255) or Color(0, 200, 0),
nick,
COLOR_WHITE,
": " .. words)
end
net.Receive("TTT_LastWordsMsg", LastWordsRecv)
local function RoleChatRecv()
-- virtually always our role, but future equipment might allow listening in
local role = net.ReadUInt(2)
local sender = net.ReadPlayer()
if not IsValid(sender) then return end
local text = net.ReadString()
if role == ROLE_TRAITOR then
chat.AddText(Color( 255, 30, 40 ),
Format("(%s) ", string.upper(GetTranslation("traitor"))),
Color( 255, 200, 20),
sender:Nick(),
Color( 255, 255, 200),
": " .. text)
elseif role == ROLE_DETECTIVE then
chat.AddText(Color( 20, 100, 255 ),
Format("(%s) ", string.upper(GetTranslation("detective"))),
Color( 25, 200, 255),
sender:Nick(),
Color( 200, 255, 255),
": " .. text)
end
end
net.Receive("TTT_RoleChat", RoleChatRecv)
-- special processing for certain special chat types
function GM:ChatText(idx, name, text, type)
if type == "joinleave" then
if string.find(text, "Changed name during a round") then
-- prevent nick from showing up
chat.AddText(LANG.GetTranslation("name_kick"))
return true
end
end
return BaseClass.ChatText(self, idx, name, text, type)
end
-- Detectives have a blue name, in both chat and radio messages
local function AddDetectiveText(ply, text)
chat.AddText(Color(50, 200, 255),
ply:Nick(),
Color(255,255,255),
": " .. text)
end
function GM:OnPlayerChat(ply, text, teamchat, dead)
if not IsValid(ply) then return BaseClass.OnPlayerChat(self, ply, text, teamchat, dead) end
if ply:IsActiveDetective() then
AddDetectiveText(ply, text)
return true
end
local team = ply:Team() == TEAM_SPEC
if team and not dead then
dead = true
end
if teamchat and ((not team and not ply:IsSpecial()) or team) then
teamchat = false
end
return BaseClass.OnPlayerChat(self, ply, text, teamchat, dead)
end
local last_chat = ""
function GM:ChatTextChanged(text)
last_chat = text
end
function ChatInterrupt()
local client = LocalPlayer()
local id = net.ReadUInt(32)
local last_seen = IsValid(client.last_id) and client.last_id:EntIndex() or 0
local last_words = "."
if last_chat == "" then
if RADIO.LastRadio.t > CurTime() - 2 then
last_words = RADIO.LastRadio.msg
end
else
last_words = last_chat
end
RunConsoleCommand("_deathrec", tostring(id), tostring(last_seen), last_words)
end
net.Receive("TTT_InterruptChat", ChatInterrupt)
--- Radio
RADIO = {}
RADIO.Show = false
RADIO.StoredTarget = {nick="", t=0}
RADIO.LastRadio = {msg="", t=0}
-- [key] -> command
RADIO.Commands = {
{cmd="yes", text="quick_yes", format=false},
{cmd="no", text="quick_no", format=false},
{cmd="help", text="quick_help", format=false},
{cmd="imwith", text="quick_imwith", format=true},
{cmd="see", text="quick_see", format=true},
{cmd="suspect", text="quick_suspect", format=true},
{cmd="traitor", text="quick_traitor", format=true},
{cmd="innocent", text="quick_inno", format=true},
{cmd="check", text="quick_check", format=false}
};
local radioframe = nil
function RADIO:ShowRadioCommands(state)
if not state then
if radioframe and radioframe:IsValid() then
radioframe:Remove()
radioframe = nil
-- don't capture keys
self.Show = false
end
else
local client = LocalPlayer()
if not IsValid(client) then return end
if not radioframe then
local w, h = 200, 300
radioframe = vgui.Create("DForm")
radioframe:SetName(GetTranslation("quick_title"))
radioframe:SetSize(w, h)
radioframe:SetMouseInputEnabled(false)
radioframe:SetKeyboardInputEnabled(false)
radioframe:CenterVertical()
-- This is not how you should do things
radioframe.ForceResize = function(s)
local w, label = 0, nil
for k,v in pairs(s.Items) do
label = v:GetChild(0)
if label:GetWide() > w then
w = label:GetWide()
end
end
s:SetWide(w + 20)
end
for key, command in pairs(self.Commands) do
local dlabel = vgui.Create("DLabel", radioframe)
local id = key .. ": "
local txt = id
if command.format then
txt = txt .. GetPTranslation(command.text, {player = GetTranslation("quick_nobody")})
else
txt = txt .. GetTranslation(command.text)
end
dlabel:SetText(txt)
dlabel:SetFont("TabLarge")
dlabel:SetTextColor(COLOR_WHITE)
dlabel:SizeToContents()
if command.format then
dlabel.target = nil
dlabel.id = id
dlabel.txt = GetTranslation(command.text)
dlabel.Think = function(s)
local tgt, v = RADIO:GetTarget()
if s.target != tgt then
s.target = tgt
tgt = string.Interp(s.txt, {player = RADIO.ToPrintable(tgt)})
if v then
tgt = util.Capitalize(tgt)
end
s:SetText(s.id .. tgt)
s:SizeToContents()
radioframe:ForceResize()
end
end
end
radioframe:AddItem(dlabel)
end
radioframe:ForceResize()
end
radioframe:MakePopup()
-- grabs input on init(), which happens in makepopup
radioframe:SetMouseInputEnabled(false)
radioframe:SetKeyboardInputEnabled(false)
-- capture slot keys while we're open
self.Show = true
timer.Create("radiocmdshow", 3, 1,
function() RADIO:ShowRadioCommands(false) end)
end
end
function RADIO:SendCommand(slotidx)
local c = self.Commands[slotidx]
if c then
RunConsoleCommand("ttt_radio", c.cmd)
self:ShowRadioCommands(false)
end
end
function RADIO:GetTargetType()
if not IsValid(LocalPlayer()) then return end
local trace = LocalPlayer():GetEyeTrace(MASK_SHOT)
if not trace or (not trace.Hit) or (not IsValid(trace.Entity)) then return end
local ent = trace.Entity
if ent:IsPlayer() and ent:IsTerror() then
if ent:GetNWBool("disguised", false) then
return "quick_disg", true
else
return ent, false
end
elseif ent:GetClass() == "prop_ragdoll" and CORPSE.GetPlayerNick(ent, "") != "" then
if DetectiveMode() and not CORPSE.GetFound(ent, false) then
return "quick_corpse", true
else
return ent, false
end
end
end
function RADIO.ToPrintable(target)
if isstring(target) then
return GetTranslation(target)
elseif IsValid(target) then
if target:IsPlayer() then
return target:Nick()
elseif target:GetClass() == "prop_ragdoll" then
return GetPTranslation("quick_corpse_id", {player = CORPSE.GetPlayerNick(target, "A Terrorist")})
end
end
end
function RADIO:GetTarget()
local client = LocalPlayer()
if IsValid(client) then
local current, vague = self:GetTargetType()
if current then return current, vague end
local stored = self.StoredTarget
if stored.target and stored.t > (CurTime() - 3) then
return stored.target, stored.vague
end
end
return "quick_nobody", true
end
function RADIO:StoreTarget()
local current, vague = self:GetTargetType()
if current then
self.StoredTarget.target = current
self.StoredTarget.vague = vague
self.StoredTarget.t = CurTime()
end
end
-- Radio commands are a console cmd instead of directly sent from RADIO, because
-- this way players can bind keys to them
local function RadioCommand(ply, cmd, arg)
if not IsValid(ply) or #arg != 1 then
print("ttt_radio failed, too many arguments?")
return
end
if RADIO.LastRadio.t > (CurTime() - 0.5) then return end
local msg_type = arg[1]
local target, vague = RADIO:GetTarget()
local msg_name = nil
-- this will not be what is shown, but what is stored in case this message
-- has to be used as last words (which will always be english for now)
local text = nil
for _, msg in pairs(RADIO.Commands) do
if msg.cmd == msg_type then
local eng = LANG.GetTranslationFromLanguage(msg.text, "english")
text = msg.format and string.Interp(eng, {player = RADIO.ToPrintable(target)}) or eng
msg_name = msg.text
break
end
end
if not text then
print("ttt_radio failed, argument not valid radiocommand")
return
end
if vague then
text = util.Capitalize(text)
end
RADIO.LastRadio.t = CurTime()
RADIO.LastRadio.msg = text
-- target is either a lang string or an entity
target = isstring(target) and target or tostring(target:EntIndex())
RunConsoleCommand("_ttt_radio_send", msg_name, tostring(target))
end
local function RadioComplete(cmd, arg)
local c = {}
for k, cmd in pairs(RADIO.Commands) do
local rcmd = "ttt_radio " .. cmd.cmd
table.insert(c, rcmd)
end
return c
end
concommand.Add("ttt_radio", RadioCommand, RadioComplete)
local function RadioMsgRecv()
local sender = net.ReadPlayer()
local msg = net.ReadString()
local param = net.ReadString()
if not (IsValid(sender) and sender:IsPlayer()) then return end
GAMEMODE:PlayerSentRadioCommand(sender, msg, param)
-- if param is a language string, translate it
-- else it's a nickname
local lang_param = LANG.GetNameParam(param)
if lang_param then
if lang_param == "quick_corpse_id" then
-- special case where nested translation is needed
param = GetPTranslation(lang_param, {player = net.ReadString()})
else
param = GetTranslation(lang_param)
end
end
local text = GetPTranslation(msg, {player = param})
-- don't want to capitalize nicks, but everything else is fair game
if lang_param then
text = util.Capitalize(text)
end
if sender:IsDetective() then
AddDetectiveText(sender, text)
else
chat.AddText(sender,
COLOR_WHITE,
": " .. text)
end
end
net.Receive("TTT_RadioMsg", RadioMsgRecv)
local radio_gestures = {
quick_yes = ACT_GMOD_GESTURE_AGREE,
quick_no = ACT_GMOD_GESTURE_DISAGREE,
quick_see = ACT_GMOD_GESTURE_WAVE,
quick_check = ACT_SIGNAL_GROUP,
quick_suspect = ACT_SIGNAL_HALT
};
function GM:PlayerSentRadioCommand(ply, name, target)
local act = radio_gestures[name]
if act then
ply:AnimPerformGesture(act)
end
end
--- voicechat stuff
VOICE = {}
local MutedState = nil
-- voice popups, copied from base gamemode and modified
g_VoicePanelList = nil
-- 255 at 100
-- 5 at 5000
local function VoiceNotifyThink(pnl)
if not (IsValid(pnl) and LocalPlayer() and IsValid(pnl.ply)) then return end
if not (GetGlobalBool("ttt_locational_voice", false) and (not pnl.ply:IsSpec()) and (pnl.ply != LocalPlayer())) then return end
if LocalPlayer():IsActiveTraitor() && pnl.ply:IsActiveTraitor() then return end
local d = LocalPlayer():GetPos():Distance(pnl.ply:GetPos())
pnl:SetAlpha(math.max(-0.1 * d + 255, 15))
end
local PlayerVoicePanels = {}
function GM:PlayerStartVoice( ply )
local client = LocalPlayer()
if not IsValid(g_VoicePanelList) or not IsValid(client) then return end
-- There'd be an extra one if voice_loopback is on, so remove it.
GAMEMODE:PlayerEndVoice(ply, true)
if not IsValid(ply) then return end
-- Tell server this is global
if client == ply then
if client:IsActiveTraitor() then
if (not client:KeyDown(IN_SPEED)) and (not client:KeyDownLast(IN_SPEED)) then
client.traitor_gvoice = true
RunConsoleCommand("tvog", "1")
else
client.traitor_gvoice = false
RunConsoleCommand("tvog", "0")
end
end
VOICE.SetSpeaking(true)
end
local pnl = g_VoicePanelList:Add("VoiceNotify")
pnl:Setup(ply)
pnl:Dock(TOP)
local oldThink = pnl.Think
pnl.Think = function( self )
oldThink( self )
VoiceNotifyThink( self )
end
local shade = Color(0, 0, 0, 150)
pnl.Paint = function(s, w, h)
if not IsValid(s.ply) then return end
draw.RoundedBox(4, 0, 0, w, h, s.Color)
draw.RoundedBox(4, 1, 1, w-2, h-2, shade)
end
if client:IsActiveTraitor() then
if ply == client then
if not client.traitor_gvoice then
pnl.Color = Color(200, 20, 20, 255)
end
elseif ply:IsActiveTraitor() then
if not ply.traitor_gvoice then
pnl.Color = Color(200, 20, 20, 255)
end
end
end
if ply:IsActiveDetective() then
pnl.Color = Color(20, 20, 200, 255)
end
PlayerVoicePanels[ply] = pnl
-- run ear gesture
if not (ply:IsActiveTraitor() and (not ply.traitor_gvoice)) then
ply:AnimPerformGesture(ACT_GMOD_IN_CHAT)
end
end
local function ReceiveVoiceState()
local idx = net.ReadUInt(7) + 1 -- we -1 serverside
local state = net.ReadBit() == 1
-- prevent glitching due to chat starting/ending across round boundary
if GAMEMODE.round_state != ROUND_ACTIVE then return end
if (not IsValid(LocalPlayer())) or (not LocalPlayer():IsActiveTraitor()) then return end
local ply = player.GetByID(idx)
if IsValid(ply) then
ply.traitor_gvoice = state
if IsValid(PlayerVoicePanels[ply]) then
PlayerVoicePanels[ply].Color = state and Color(0,200,0) or Color(200, 0, 0)
end
end
end
net.Receive("TTT_TraitorVoiceState", ReceiveVoiceState)
local function VoiceClean()
for ply, pnl in pairs( PlayerVoicePanels ) do
if (not IsValid(pnl)) or (not IsValid(ply)) then
GAMEMODE:PlayerEndVoice(ply)
end
end
end
timer.Create( "VoiceClean", 10, 0, VoiceClean )
function GM:PlayerEndVoice(ply, no_reset)
if IsValid( PlayerVoicePanels[ply] ) then
PlayerVoicePanels[ply]:Remove()
PlayerVoicePanels[ply] = nil
end
if IsValid(ply) and not no_reset then
ply.traitor_gvoice = false
end
if ply == LocalPlayer() then
VOICE.SetSpeaking(false)
end
end
local function CreateVoiceVGUI()
g_VoicePanelList = vgui.Create( "DPanel" )
g_VoicePanelList:ParentToHUD()
g_VoicePanelList:SetPos(25, 25)
g_VoicePanelList:SetSize(200, ScrH() - 200)
g_VoicePanelList:SetPaintBackground(false)
MutedState = vgui.Create("DLabel")
MutedState:SetPos(ScrW() - 200, ScrH() - 50)
MutedState:SetSize(200, 50)
MutedState:SetFont("Trebuchet18")
MutedState:SetText("")
MutedState:SetTextColor(Color(240, 240, 240, 250))
MutedState:SetVisible(false)
end
hook.Add( "InitPostEntity", "CreateVoiceVGUI", CreateVoiceVGUI )
local MuteStates = {MUTE_NONE, MUTE_TERROR, MUTE_ALL, MUTE_SPEC}
local MuteText = {
[MUTE_NONE] = "",
[MUTE_TERROR] = "mute_living",
[MUTE_ALL] = "mute_all",
[MUTE_SPEC] = "mute_specs"
};
local function SetMuteState(state)
if MutedState then
MutedState:SetText(string.upper(GetTranslation(MuteText[state])))
MutedState:SetVisible(state != MUTE_NONE)
end
end
local mute_state = MUTE_NONE
function VOICE.CycleMuteState(force_state)
mute_state = force_state or next(MuteText, mute_state)
if not mute_state then mute_state = MUTE_NONE end
SetMuteState(mute_state)
return mute_state
end
local battery_max = 100
local battery_min = 10
function VOICE.InitBattery()
LocalPlayer().voice_battery = battery_max
end
local function GetRechargeRate()
local r = GetGlobalFloat("ttt_voice_drain_recharge", 0.05)
if LocalPlayer().voice_battery < battery_min then
r = r / 2
end
return r
end
local function GetDrainRate()
if not GetGlobalBool("ttt_voice_drain", false) then return 0 end
if GetRoundState() != ROUND_ACTIVE then return 0 end
local ply = LocalPlayer()
if (not IsValid(ply)) or ply:IsSpec() then return 0 end
if ply:IsAdmin() or ply:IsDetective() then
return GetGlobalFloat("ttt_voice_drain_admin", 0)
else
return GetGlobalFloat("ttt_voice_drain_normal", 0)
end
end
local function IsTraitorChatting(client)
return client:IsActiveTraitor() and (not client.traitor_gvoice)
end
function VOICE.Tick()
if not GetGlobalBool("ttt_voice_drain", false) then return end
local client = LocalPlayer()
if VOICE.IsSpeaking() and (not IsTraitorChatting(client)) then
client.voice_battery = client.voice_battery - GetDrainRate()
if not VOICE.CanSpeak() then
client.voice_battery = 0
RunConsoleCommand("-voicerecord")
end
elseif client.voice_battery < battery_max then
client.voice_battery = client.voice_battery + GetRechargeRate()
end
end
-- Player:IsSpeaking() does not work for localplayer
function VOICE.IsSpeaking() return LocalPlayer().speaking end
function VOICE.SetSpeaking(state) LocalPlayer().speaking = state end
function VOICE.CanSpeak()
if not GetGlobalBool("ttt_voice_drain", false) then return true end
return LocalPlayer().voice_battery > battery_min or IsTraitorChatting(LocalPlayer())
end
local speaker = surface.GetTextureID("voice/icntlk_sv")
function VOICE.Draw(client)
local b = client.voice_battery
if b >= battery_max then return end
local x = 25
local y = 10
local w = 200
local h = 6
if b < battery_min and CurTime() % 0.2 < 0.1 then
surface.SetDrawColor(200, 0, 0, 155)
else
surface.SetDrawColor(0, 200, 0, 255)
end
surface.DrawOutlinedRect(x, y, w, h)
surface.SetTexture(speaker)
surface.DrawTexturedRect(5, 5, 16, 16)
x = x + 1
y = y + 1
w = w - 2
h = h - 2
surface.SetDrawColor(0, 200, 0, 150)
surface.DrawRect(x, y, w * math.Clamp((client.voice_battery - 10) / 90, 0, 1), h)
end

View File

@@ -0,0 +1,350 @@
--[[
| 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/
--]]
-- we need our own weapon switcher because the hl2 one skips empty weapons
local math = math
local draw = draw
local surface = surface
local table = table
WSWITCH = {}
WSWITCH.Show = false
WSWITCH.Selected = -1
WSWITCH.NextSwitch = -1
WSWITCH.WeaponCache = {}
WSWITCH.cv = {}
WSWITCH.cv.stay = CreateConVar("ttt_weaponswitcher_stay", "0", FCVAR_ARCHIVE)
WSWITCH.cv.fast = CreateConVar("ttt_weaponswitcher_fast", "0", FCVAR_ARCHIVE)
WSWITCH.cv.display = CreateConVar("ttt_weaponswitcher_displayfast", "0", FCVAR_ARCHIVE)
local delay = 0.03
local showtime = 3
local margin = 10
local width = 300
local height = 20
local barcorner = surface.GetTextureID( "gui/corner8" )
local col_active = {
tip = {
[ROLE_INNOCENT] = Color(55, 170, 50, 255),
[ROLE_TRAITOR] = Color(180, 50, 40, 255),
[ROLE_DETECTIVE] = Color(50, 60, 180, 255)
},
bg = Color(20, 20, 20, 250),
text_empty = Color(200, 20, 20, 255),
text = Color(255, 255, 255, 255),
shadow = 255
};
local col_dark = {
tip = {
[ROLE_INNOCENT] = Color(60, 160, 50, 155),
[ROLE_TRAITOR] = Color(160, 50, 60, 155),
[ROLE_DETECTIVE] = Color(50, 60, 160, 155),
},
bg = Color(20, 20, 20, 200),
text_empty = Color(200, 20, 20, 100),
text = Color(255, 255, 255, 100),
shadow = 100
};
-- Draw a bar in the style of the the weapon pickup ones
local round = math.Round
function WSWITCH:DrawBarBg(x, y, w, h, col)
local rx = round(x - 4)
local ry = round(y - (h / 2)-4)
local rw = round(w + 9)
local rh = round(h + 8)
local b = 8 --bordersize
local bh = b / 2
local role = LocalPlayer():GetRole() or ROLE_INNOCENT
local c = col.tip[role]
-- Draw the colour tip
surface.SetTexture(barcorner)
surface.SetDrawColor(c.r, c.g, c.b, c.a)
surface.DrawTexturedRectRotated( rx + bh , ry + bh, b, b, 0 )
surface.DrawTexturedRectRotated( rx + bh , ry + rh -bh, b, b, 90 )
surface.DrawRect( rx, ry+b, b, rh-b*2 )
surface.DrawRect( rx+b, ry, h - 4, rh )
-- Draw the remainder
-- Could just draw a full roundedrect bg and overdraw it with the tip, but
-- I don't have to do the hard work here anymore anyway
c = col.bg
surface.SetDrawColor(c.r, c.g, c.b, c.a)
surface.DrawRect( rx+b+h-4, ry, rw - (h - 4) - b*2, rh )
surface.DrawTexturedRectRotated( rx + rw - bh , ry + rh - bh, b, b, 180 )
surface.DrawTexturedRectRotated( rx + rw - bh , ry + bh, b, b, 270 )
surface.DrawRect( rx+rw-b, ry+b, b, rh-b*2 )
end
local TryTranslation = LANG.TryTranslation
function WSWITCH:DrawWeapon(x, y, c, wep)
if not IsValid(wep) then return false end
local name = TryTranslation(wep:GetPrintName() or wep.PrintName or "...")
local cl1, am1 = wep:Clip1(), wep:Ammo1()
local ammo = false
-- Clip1 will be -1 if a melee weapon
-- Ammo1 will be false if weapon has no owner (was just dropped)
if cl1 != -1 and am1 != false then
ammo = Format("%i + %02i", cl1, am1)
end
-- Slot
local spec = {text=wep.Slot+1, font="Trebuchet22", pos={x+4, y}, yalign=TEXT_ALIGN_CENTER, color=c.text}
draw.TextShadow(spec, 1, c.shadow)
-- Name
spec.text = name
spec.font = "TimeLeft"
spec.pos[1] = x + 10 + height
draw.Text(spec)
if ammo then
local col = c.text
if wep:Clip1() == 0 and wep:Ammo1() == 0 then
col = c.text_empty
end
-- Ammo
spec.text = ammo
spec.pos[1] = ScrW() - margin*3
spec.xalign = TEXT_ALIGN_RIGHT
spec.color = col
draw.Text(spec)
end
return true
end
function WSWITCH:Draw(client)
if not self.Show then return end
local weps = self.WeaponCache
local x = ScrW() - width - margin*2
local y = ScrH() - (#weps * (height + margin))
local col = col_dark
for k, wep in pairs(weps) do
if self.Selected == k then
col = col_active
else
col = col_dark
end
self:DrawBarBg(x, y, width, height, col)
if not self:DrawWeapon(x, y, col, wep) then
self:UpdateWeaponCache()
return
end
y = y + height + margin
end
end
local function SlotSort(a, b)
return a and b and a.Slot and b.Slot and a.Slot < b.Slot
end
local function CopyVals(src, dest)
table.Empty(dest)
for k, v in pairs(src) do
if IsValid(v) then
table.insert(dest, v)
end
end
end
function WSWITCH:UpdateWeaponCache()
-- GetWeapons does not always return a proper numeric table it seems
-- self.WeaponCache = LocalPlayer():GetWeapons()
-- So copy over the weapon refs
self.WeaponCache = {}
CopyVals(LocalPlayer():GetWeapons(), self.WeaponCache)
table.sort(self.WeaponCache, SlotSort)
end
function WSWITCH:SetSelected(idx)
self.Selected = idx
self:UpdateWeaponCache()
end
function WSWITCH:SelectNext()
if self.NextSwitch > CurTime() then return end
self:Enable()
local s = self.Selected + 1
if s > #self.WeaponCache then
s = 1
end
self:DoSelect(s)
self.NextSwitch = CurTime() + delay
end
function WSWITCH:SelectPrev()
if self.NextSwitch > CurTime() then return end
self:Enable()
local s = self.Selected - 1
if s < 1 then
s = #self.WeaponCache
end
self:DoSelect(s)
self.NextSwitch = CurTime() + delay
end
-- Select by index
function WSWITCH:DoSelect(idx)
self:SetSelected(idx)
if self.cv.fast:GetBool() then
-- immediately confirm if fastswitch is on
self:ConfirmSelection(self.cv.display:GetBool())
end
end
-- Numeric key access to direct slots
function WSWITCH:SelectSlot(slot)
if not slot then return end
self:Enable()
self:UpdateWeaponCache()
slot = slot - 1
-- find which idx in the weapon table has the slot we want
local toselect = self.Selected
for k, w in pairs(self.WeaponCache) do
if w.Slot == slot then
toselect = k
break
end
end
self:DoSelect(toselect)
self.NextSwitch = CurTime() + delay
end
-- Show the weapon switcher
function WSWITCH:Enable()
if self.Show == false then
self.Show = true
local wep_active = LocalPlayer():GetActiveWeapon()
self:UpdateWeaponCache()
-- make our active weapon the initial selection
local toselect = 1
for k, w in pairs(self.WeaponCache) do
if w == wep_active then
toselect = k
break
end
end
self:SetSelected(toselect)
end
-- cache for speed, checked every Think
self.Stay = self.cv.stay:GetBool()
end
-- Hide switcher
function WSWITCH:Disable()
self.Show = false
end
-- Switch to the currently selected weapon
function WSWITCH:ConfirmSelection(noHide)
if not noHide then self:Disable() end
for k, w in pairs(self.WeaponCache) do
if k == self.Selected and IsValid(w) then
input.SelectWeapon(w)
return
end
end
end
-- Allow for suppression of the attack command
function WSWITCH:PreventAttack()
return self.Show and !self.cv.fast:GetBool()
end
function WSWITCH:Think()
if (not self.Show) or self.Stay then return end
-- hide after period of inaction
if self.NextSwitch < (CurTime() - showtime) then
self:Disable()
end
end
-- Instantly select a slot and switch to it, without spending time in menu
function WSWITCH:SelectAndConfirm(slot)
if not slot then return end
WSWITCH:SelectSlot(slot)
WSWITCH:ConfirmSelection()
end
local function QuickSlot(ply, cmd, args)
if (not IsValid(ply)) or (not args) or #args != 1 then return end
local slot = tonumber(args[1])
if not slot then return end
local wep = ply:GetActiveWeapon()
if IsValid(wep) then
if wep.Slot == (slot - 1) then
RunConsoleCommand("lastinv")
else
WSWITCH:SelectAndConfirm(slot)
end
end
end
concommand.Add("ttt_quickslot", QuickSlot)
local function SwitchToEquipment(ply, cmd, args)
RunConsoleCommand("ttt_quickslot", tostring(7))
end
concommand.Add("ttt_equipswitch", SwitchToEquipment)

View File

@@ -0,0 +1,477 @@
--[[
| 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/
--]]
---- Corpse functions
-- namespaced because we have no ragdoll metatable
CORPSE = {}
include("corpse_shd.lua")
--- networked data abstraction layer
local dti = CORPSE.dti
function CORPSE.SetFound(rag, state)
--rag:SetNWBool("found", state)
rag:SetDTBool(dti.BOOL_FOUND, state)
end
function CORPSE.SetPlayerNick(rag, ply_or_name)
-- don't have datatable strings, so use a dt entity for common case of
-- still-connected player, and if the player is gone, fall back to nw string
local name = ply_or_name
if IsValid(ply_or_name) then
name = ply_or_name:Nick()
rag:SetDTEntity(dti.ENT_PLAYER, ply_or_name)
end
rag:SetNWString("nick", name)
end
function CORPSE.SetCredits(rag, credits)
--rag:SetNWInt("credits", credits)
rag:SetDTInt(dti.INT_CREDITS, credits)
end
--- ragdoll creation and search
-- If detective mode, announce when someone's body is found
local bodyfound = CreateConVar("ttt_announce_body_found", "1")
function GM:TTTCanIdentifyCorpse(ply, corpse, was_traitor)
-- return true to allow corpse identification, false to disallow
return true
end
local function IdentifyBody(ply, rag)
if not ply:IsTerror() then return end
-- simplified case for those who die and get found during prep
if GetRoundState() == ROUND_PREP then
CORPSE.SetFound(rag, true)
return
end
if not hook.Run("TTTCanIdentifyCorpse", ply, rag, (rag.was_role == ROLE_TRAITOR)) then
return
end
local finder = ply:Nick()
local nick = CORPSE.GetPlayerNick(rag, "")
local traitor = (rag.was_role == ROLE_TRAITOR)
-- Announce body
if bodyfound:GetBool() and not CORPSE.GetFound(rag, false) then
local roletext = nil
local role = rag.was_role
if role == ROLE_TRAITOR then
roletext = "body_found_t"
elseif role == ROLE_DETECTIVE then
roletext = "body_found_d"
else
roletext = "body_found_i"
end
LANG.Msg("body_found", {finder = finder,
victim = nick,
role = LANG.Param(roletext)})
end
-- Register find
if not CORPSE.GetFound(rag, false) then
-- will return either false or a valid ply
local deadply = player.GetBySteamID64(rag.sid64)
if deadply then
deadply:SetNWBool("body_found", true)
if traitor then
-- update innocent's list of traitors
SendConfirmedTraitors(GetInnocentFilter(false))
end
SCORE:HandleBodyFound(ply, deadply)
end
hook.Call( "TTTBodyFound", GAMEMODE, ply, deadply, rag )
CORPSE.SetFound(rag, true)
end
-- Handle kill list
for k, vicsid64 in ipairs(rag.kills) do
-- filter out disconnected
local vic = player.GetBySteamID64(vicsid64)
-- is this an unconfirmed dead?
if IsValid(vic) and (not vic:GetNWBool("body_found", false)) then
LANG.Msg("body_confirm", {finder = finder, victim = vic:Nick()})
-- update scoreboard status
vic:SetNWBool("body_found", true)
end
end
end
-- Covert identify concommand for traitors
local function IdentifyCommand(ply, cmd, args)
if not IsValid(ply) then return end
if #args != 2 then return end
local eidx = tonumber(args[1])
local id = tonumber(args[2])
if (not eidx) or (not id) then return end
if (not ply.search_id) or ply.search_id.id != id or ply.search_id.eidx != eidx then
ply.search_id = nil
return
end
ply.search_id = nil
local rag = Entity(eidx)
if IsValid(rag) and rag.player_ragdoll and rag:GetPos():Distance(ply:GetPos()) < 128 then
if not CORPSE.GetFound(rag, false) then
IdentifyBody(ply, rag)
end
end
end
concommand.Add("ttt_confirm_death", IdentifyCommand)
-- Call detectives to a corpse
local function CallDetective(ply, cmd, args)
if not IsValid(ply) then return end
if #args != 1 then return end
if not ply:IsActive() then return end
local eidx = tonumber(args[1])
if not eidx then return end
local rag = Entity(eidx)
if not (IsValid(rag) and rag.player_ragdoll) then return end
if ((rag.last_detective_call or 0) < (CurTime() - 5)) and (rag:GetPos():Distance(ply:GetPos()) < 128) then
rag.last_detective_call = CurTime()
if CORPSE.GetFound(rag, false) then
-- show indicator to detectives
net.Start("TTT_CorpseCall")
net.WriteVector(rag:GetPos())
net.Send(GetDetectiveFilter(true))
LANG.Msg("body_call", {player = ply:Nick(),
victim = CORPSE.GetPlayerNick(rag, "someone")})
else
LANG.Msg(ply, "body_call_error")
end
end
end
concommand.Add("ttt_call_detective", CallDetective)
local function bitsRequired(num)
local bits, max = 0, 1
while max <= num do
bits = bits + 1
max = max + max
end
return bits
end
local plyBits = bitsRequired(game.MaxPlayers()) -- first game.MaxPlayers() of entities are for players.
function GM:TTTCanSearchCorpse(ply, corpse, is_covert, is_long_range, was_traitor)
-- return true to allow corpse search, false to disallow.
return true
end
-- Send a usermessage to client containing search results
function CORPSE.ShowSearch(ply, rag, covert, long_range)
if not IsValid(ply) or not IsValid(rag) then return end
if rag:IsOnFire() then
LANG.Msg(ply, "body_burning")
return
end
if not hook.Run("TTTCanSearchCorpse", ply, rag, covert, long_range, (rag.was_role == ROLE_TRAITOR)) then
return
end
-- init a heap of data we'll be sending
local nick = CORPSE.GetPlayerNick(rag)
local traitor = (rag.was_role == ROLE_TRAITOR)
local role = rag.was_role
local eq = rag.equipment or EQUIP_NONE
local c4 = rag.bomb_wire or 0
local dmg = rag.dmgtype or DMG_GENERIC
local wep = rag.dmgwep or ""
local words = rag.last_words or ""
local hshot = rag.was_headshot or false
local dtime = rag.time or 0
local owner = player.GetBySteamID64(rag.sid64)
owner = IsValid(owner) and owner:EntIndex() or 0
-- basic sanity check
if nick == nil or eq == nil or role == nil then return end
if DetectiveMode() and not covert then
IdentifyBody(ply, rag)
end
local credits = CORPSE.GetCredits(rag, 0)
if ply:IsActiveSpecial() and credits > 0 and (not long_range) then
LANG.Msg(ply, "body_credits", {num = credits})
ply:AddCredits(credits)
CORPSE.SetCredits(rag, 0)
ServerLog(ply:Nick() .. " took " .. credits .. " credits from the body of " .. nick .. "\n")
SCORE:HandleCreditFound(ply, nick, credits)
end
-- time of death relative to current time (saves bits)
if dtime != 0 then
dtime = math.Round(CurTime() - dtime)
end
-- identifier so we know whether a ttt_confirm_death was legit
ply.search_id = { eidx = rag:EntIndex(), id = rag:EntIndex() + dtime }
-- time of dna sample decay relative to current time
local stime = 0
if rag.killer_sample then
stime = math.max(0, rag.killer_sample.t - CurTime())
end
-- build list of people this traitor killed
local kill_entids = {}
for k, vicsid64 in ipairs(rag.kills) do
-- also send disconnected players as a marker
local vic = player.GetBySteamID64(vicsid64)
table.insert(kill_entids, IsValid(vic) and vic:EntIndex() or 0)
end
local lastid = 0
if rag.lastid and ply:IsActiveDetective() then
-- if the person this victim last id'd has since disconnected, send 0 to
-- indicate this
lastid = IsValid(rag.lastid.ent) and rag.lastid.ent:EntIndex() or 0
end
-- Send a message with basic info
net.Start("TTT_RagdollSearch")
net.WriteUInt(rag:EntIndex(), 16) -- 16 bits
net.WriteUInt(owner, plyBits) -- 128 max players. ( 8 bits )
net.WriteString(nick)
net.WriteUInt(eq, bitsRequired(EQUIP_MAX)) -- Equipment ( default: 3 bits )
net.WriteUInt(role, 2) -- ( 2 bits )
net.WriteUInt(c4, bitsRequired(C4_WIRE_COUNT)) -- 0 -> 2^bits ( default c4: 3 bits )
net.WriteUInt(dmg, 30) -- DMG_BUCKSHOT is the highest. ( 30 bits )
net.WriteString(wep)
net.WriteBool(hshot) -- ( 1 bit )
net.WriteUInt(dtime, 15)
net.WriteUInt(stime, 15)
net.WriteUInt(#kill_entids, 8)
for k, idx in ipairs(kill_entids) do
net.WriteUInt(idx, plyBits)
end
net.WriteUInt(lastid, plyBits)
-- Who found this, so if we get this from a detective we can decide not to
-- show a window
net.WriteUInt(ply:EntIndex(), plyBits)
net.WriteString(words)
-- 93 + string data + plyBits * (3 + #kill_entids)
-- If found by detective, send to all, else just the finder
if ply:IsActiveDetective() then
net.Broadcast()
else
net.Send(ply)
end
end
-- Returns a sample for use in dna scanner if the kill fits certain constraints,
-- else returns nil
local function GetKillerSample(victim, attacker, dmg)
-- only guns and melee damage, not explosions
if not (dmg:IsBulletDamage() or dmg:IsDamageType(DMG_SLASH) or dmg:IsDamageType(DMG_CLUB)) then
return nil
end
if not (IsValid(victim) and IsValid(attacker) and attacker:IsPlayer()) then return end
-- NPCs for which a player is damage owner (meaning despite the NPC dealing
-- the damage, the attacker is a player) should not cause the player's DNA to
-- end up on the corpse.
local infl = dmg:GetInflictor()
if IsValid(infl) and infl:IsNPC() then return end
local dist = victim:GetPos():Distance(attacker:GetPos())
if dist > GetConVar("ttt_killer_dna_range"):GetFloat() then return nil end
local sample = {}
sample.killer = attacker
sample.killer_sid = attacker:SteamID() -- backwards compatibility; use sample.killer_sid64 instead
sample.killer_sid64 = attacker:SteamID64()
sample.victim = victim
sample.t = CurTime() + (-1 * (0.019 * dist)^2 + GetConVar("ttt_killer_dna_basetime"):GetFloat())
return sample
end
local crimescene_keys = {"Fraction", "HitBox", "Normal", "HitPos", "StartPos"}
local poseparams = {
"aim_yaw", "move_yaw", "aim_pitch",
-- "spine_yaw", "head_yaw", "head_pitch"
};
local function GetSceneDataFromPlayer(ply)
local data = {
pos = ply:GetPos(),
ang = ply:GetAngles(),
sequence = ply:GetSequence(),
cycle = ply:GetCycle()
};
for _, param in pairs(poseparams) do
data[param] = ply:GetPoseParameter(param)
end
return data
end
local function GetSceneData(victim, attacker, dmginfo)
-- only for guns for now, hull traces don't work well etc
if not dmginfo:IsBulletDamage() then return end
local scene = {}
if victim.hit_trace then
scene.hit_trace = table.CopyKeys(victim.hit_trace, crimescene_keys)
else
return scene
end
scene.victim = GetSceneDataFromPlayer(victim)
if IsValid(attacker) and attacker:IsPlayer() then
scene.killer = GetSceneDataFromPlayer(attacker)
local att = attacker:LookupAttachment("anim_attachment_RH")
local angpos = attacker:GetAttachment(att)
if not angpos then
scene.hit_trace.StartPos = attacker:GetShootPos()
else
scene.hit_trace.StartPos = angpos.Pos
end
end
return scene
end
local rag_collide = CreateConVar("ttt_ragdoll_collide", "0")
-- Creates client or server ragdoll depending on settings
function CORPSE.Create(ply, attacker, dmginfo)
if not IsValid(ply) then return end
local efn = ply.effect_fn
ply.effect_fn = nil
local rag = ents.Create("prop_ragdoll")
if not IsValid(rag) then return nil end
rag:SetPos(ply:GetPos())
rag:SetModel(ply:GetModel())
rag:SetSkin(ply:GetSkin())
for key, value in pairs(ply:GetBodyGroups()) do
rag:SetBodygroup(value.id, ply:GetBodygroup(value.id))
end
rag:SetAngles(ply:GetAngles())
rag:SetColor(ply:GetColor())
rag:Spawn()
rag:Activate()
-- nonsolid to players, but can be picked up and shot
rag:SetCollisionGroup(rag_collide:GetBool() and COLLISION_GROUP_WEAPON or COLLISION_GROUP_DEBRIS_TRIGGER)
-- flag this ragdoll as being a player's
rag.player_ragdoll = true
rag.sid64 = ply:SteamID64()
rag.sid = ply:SteamID() -- backwards compatibility; use rag.sid64 instead
rag.uqid = ply:UniqueID() -- backwards compatibility; use rag.sid64 instead
-- network data
CORPSE.SetPlayerNick(rag, ply)
CORPSE.SetFound(rag, false)
CORPSE.SetCredits(rag, ply:GetCredits())
-- if someone searches this body they can find info on the victim and the
-- death circumstances
rag.equipment = ply:GetEquipmentItems()
rag.was_role = ply:GetRole()
rag.bomb_wire = ply.bomb_wire
rag.dmgtype = dmginfo:GetDamageType()
local wep = util.WeaponFromDamage(dmginfo)
rag.dmgwep = IsValid(wep) and wep:GetClass() or ""
rag.was_headshot = (ply.was_headshot and dmginfo:IsBulletDamage())
rag.time = CurTime()
rag.kills = table.Copy(ply.kills)
rag.killer_sample = GetKillerSample(ply, attacker, dmginfo)
-- crime scene data
rag.scene = GetSceneData(ply, attacker, dmginfo)
-- position the bones
local num = rag:GetPhysicsObjectCount()-1
local v = ply:GetVelocity()
-- bullets have a lot of force, which feels better when shooting props,
-- but makes bodies fly, so dampen that here
if dmginfo:IsDamageType(DMG_BULLET) or dmginfo:IsDamageType(DMG_SLASH) then
v = v / 5
end
for i=0, num do
local bone = rag:GetPhysicsObjectNum(i)
if IsValid(bone) then
local bp, ba = ply:GetBonePosition(rag:TranslatePhysBoneToBone(i))
if bp and ba then
bone:SetPos(bp)
bone:SetAngles(ba)
end
-- not sure if this will work:
bone:SetVelocity(v)
end
end
-- create advanced death effects (knives)
if efn then
-- next frame, after physics is happy for this ragdoll
timer.Simple(0, function() if IsValid(rag) then efn(rag) end end)
end
hook.Run("TTTOnCorpseCreated", rag, ply)
return rag -- we'll be speccing this
end

View File

@@ -0,0 +1,49 @@
--[[
| 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/
--]]
---- Shared corpsey stuff
CORPSE = CORPSE or {}
-- Manual datatable indexing
CORPSE.dti = {
BOOL_FOUND = 0,
ENT_PLAYER = 0,
INT_CREDITS = 0
};
local dti = CORPSE.dti
--- networked data abstraction
function CORPSE.GetFound(rag, default)
return rag and rag:GetDTBool(dti.BOOL_FOUND) or default
end
function CORPSE.GetPlayerNick(rag, default)
if not IsValid(rag) then return default end
local ply = rag:GetDTEntity(dti.ENT_PLAYER)
if IsValid(ply) then
return ply:Nick()
else
return rag:GetNWString("nick", default)
end
end
function CORPSE.GetCredits(rag, default)
if not IsValid(rag) then return default end
return rag:GetDTInt(dti.INT_CREDITS)
end
function CORPSE.GetPlayer(rag)
if not IsValid(rag) then return NULL end
return rag:GetDTEntity(dti.ENT_PLAYER)
end

View File

@@ -0,0 +1,609 @@
--[[
| 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/
--]]
---- Replace old and boring ents with new and shiny SENTs
ents.TTT = {}
local table = table
local math = math
local pairs = pairs
local function ReplaceSingle(ent, newname)
-- Ammo that has been mapper-placed will not have a pos yet at this point for
-- reasons that have to do with being really annoying. So don't touch those
-- so we can replace them later. Grumble grumble.
if ent:GetPos() == vector_origin then
return
end
ent:SetSolid(SOLID_NONE)
local rent = ents.Create(newname)
rent:SetPos(ent:GetPos())
rent:SetAngles(ent:GetAngles())
rent:Spawn()
rent:Activate()
rent:PhysWake()
ent:Remove()
end
local hl2_ammo_replace = {
["item_ammo_pistol"] = "item_ammo_pistol_ttt",
["item_box_buckshot"] = "item_box_buckshot_ttt",
["item_ammo_smg1"] = "item_ammo_smg1_ttt",
["item_ammo_357"] = "item_ammo_357_ttt",
["item_ammo_357_large"] = "item_ammo_357_ttt",
["item_ammo_revolver"] = "item_ammo_revolver_ttt", -- zm
["item_ammo_ar2"] = "item_ammo_pistol_ttt",
["item_ammo_ar2_large"] = "item_ammo_smg1_ttt",
["item_ammo_smg1_grenade"] = "weapon_zm_pistol",
["item_battery"] = "item_ammo_357_ttt",
["item_healthkit"] = "weapon_zm_shotgun",
["item_suitcharger"] = "weapon_zm_mac10",
["item_ammo_ar2_altfire"] = "weapon_zm_mac10",
["item_rpg_round"] = "item_ammo_357_ttt",
["item_ammo_crossbow"] = "item_box_buckshot_ttt",
["item_healthvial"] = "weapon_zm_molotov",
["item_healthcharger"] = "item_ammo_revolver_ttt",
["item_ammo_crate"] = "weapon_ttt_confgrenade",
["item_item_crate"] = "ttt_random_ammo"
};
-- Replace an ammo entity with the TTT version
-- Optional cls param is the classname, if the caller already has it handy
local function ReplaceAmmoSingle(ent, cls)
if cls == nil then cls = ent:GetClass() end
local rpl = hl2_ammo_replace[cls]
if rpl then
ReplaceSingle(ent, rpl)
end
end
local function ReplaceAmmo()
for _, ent in ipairs(ents.FindByClass("item_*")) do
ReplaceAmmoSingle(ent)
end
end
local hl2_weapon_replace = {
["weapon_smg1"] = "weapon_zm_mac10",
["weapon_shotgun"] = "weapon_zm_shotgun",
["weapon_ar2"] = "weapon_ttt_m16",
["weapon_357"] = "weapon_zm_rifle",
["weapon_crossbow"] = "weapon_zm_pistol",
["weapon_rpg"] = "weapon_zm_sledge",
["weapon_slam"] = "item_ammo_pistol_ttt",
["weapon_frag"] = "weapon_zm_revolver",
["weapon_crowbar"] = "weapon_zm_molotov"
};
local function ReplaceWeaponSingle(ent, cls)
-- Loadout weapons immune
-- we use a SWEP-set property because at this state all SWEPs identify as weapon_swep
if ent.AllowDelete == false then
return
else
if cls == nil then cls = ent:GetClass() end
local rpl = hl2_weapon_replace[cls]
if rpl then
ReplaceSingle(ent, rpl)
end
end
end
local function ReplaceWeapons()
for _, ent in ipairs(ents.FindByClass("weapon_*")) do
ReplaceWeaponSingle(ent)
end
end
-- Remove ZM ragdolls that don't work, AND old player ragdolls.
-- Exposed because it's also done at BeginRound
function ents.TTT.RemoveRagdolls(player_only)
for k, ent in ipairs(ents.FindByClass("prop_ragdoll")) do
if IsValid(ent) then
if not player_only and string.find(ent:GetModel(), "zm_", 6, true) then
ent:Remove()
elseif ent.player_ragdoll then
-- cleanup ought to catch these but you know
ent:Remove()
end
end
end
end
-- People spawn with these, so remove any pickups (ZM maps have them)
local function RemoveCrowbars()
for k, ent in ipairs(ents.FindByClass("weapon_zm_improvised")) do
ent:Remove()
end
end
function ents.TTT.ReplaceEntities()
ReplaceAmmo()
ReplaceWeapons()
RemoveCrowbars()
ents.TTT.RemoveRagdolls()
end
local cls = "" -- avoid allocating
local sub = string.sub
local function ReplaceOnCreated(s, ent)
-- Invalid ents are of no use anyway
if not ent:IsValid() then return end
cls = ent:GetClass()
if sub(cls, 1, 4) == "item" then
ReplaceAmmoSingle(ent, cls)
elseif sub(cls, 1, 6) == "weapon" then
ReplaceWeaponSingle(ent, cls)
end
end
local noop = util.noop
GM.OnEntityCreated = ReplaceOnCreated
-- Helper so we can easily turn off replacement stuff when we don't need it
function ents.TTT.SetReplaceChecking(state)
if state then
GAMEMODE.OnEntityCreated = ReplaceOnCreated
else
GAMEMODE.OnEntityCreated = noop
end
end
-- GMod's game.CleanUpMap destroys rope entities that are parented. This is an
-- experimental fix where the rope is unparented, the map cleaned, and then the
-- rope reparented.
-- Same happens for func_brush.
local broken_parenting_ents = {
"move_rope",
"keyframe_rope",
"info_target",
"func_brush"
}
function ents.TTT.FixParentedPreCleanup()
for _, rcls in pairs(broken_parenting_ents) do
for k,v in ipairs(ents.FindByClass(rcls)) do
if v.GetParent and IsValid(v:GetParent()) then
v.CachedParentName = v:GetParent():GetName()
v:SetParent(nil)
if not v.OrigPos then
v.OrigPos = v:GetPos()
end
end
end
end
end
function ents.TTT.FixParentedPostCleanup()
for _, rcls in pairs(broken_parenting_ents) do
for k,v in ipairs(ents.FindByClass(rcls)) do
if v.CachedParentName then
if v.OrigPos then
v:SetPos(v.OrigPos)
end
local parents = ents.FindByName(v.CachedParentName)
if #parents == 1 then
local parent = parents[1]
v:SetParent(parent)
end
end
end
end
end
function ents.TTT.TriggerRoundStateOutputs(r, param)
r = r or GetRoundState()
for _, ent in ipairs(ents.FindByClass("ttt_map_settings")) do
if IsValid(ent) then
ent:RoundStateTrigger(r, param)
end
end
end
-- CS:S and TF2 maps have a bunch of ents we'd like to abuse for weapon spawns,
-- but to do that we need to register a SENT with their class name, else they
-- will just error out and we can't do anything with them.
local dummify = {
-- CS:S
"hostage_entity",
-- TF2
"item_ammopack_full",
"item_ammopack_medium",
"item_ammopack_small",
"item_healthkit_full",
"item_healthkit_medium",
"item_healthkit_small",
"item_teamflag",
"game_intro_viewpoint",
"info_observer_point",
"team_control_point",
"team_control_point_master",
"team_control_point_round",
-- ZM
"item_ammo_revolver"
};
for k, cls in pairs(dummify) do
scripted_ents.Register({Type="point", IsWeaponDummy=true}, cls)
end
-- Cache this, every ttt_random_weapon uses it in its Init
local SpawnableSWEPs = nil
function ents.TTT.GetSpawnableSWEPs()
if not SpawnableSWEPs then
local tbl = {}
for k,v in pairs(weapons.GetList()) do
if v and v.AutoSpawnable and (not WEPS.IsEquipment(v)) then
table.insert(tbl, v)
end
end
SpawnableSWEPs = tbl
end
return SpawnableSWEPs
end
local SpawnableAmmoClasses = nil
function ents.TTT.GetSpawnableAmmo()
if not SpawnableAmmoClasses then
local tbl = {}
for k,v in pairs(scripted_ents.GetList()) do
if v and (v.AutoSpawnable or (v.t and v.t.AutoSpawnable)) then
table.insert(tbl, k)
end
end
SpawnableAmmoClasses = tbl
end
return SpawnableAmmoClasses
end
local function PlaceWeapon(swep, pos, ang)
local cls = swep and WEPS.GetClass(swep)
if not cls then return end
-- Create the weapon, somewhat in the air in case the spot hugs the ground.
local ent = ents.Create(cls)
pos.z = pos.z + 3
ent:SetPos(pos)
ent:SetAngles(VectorRand():Angle())
ent:Spawn()
-- Create some associated ammo (if any)
if ent.AmmoEnt then
for i=1, math.random(0,3) do
local ammo = ents.Create(ent.AmmoEnt)
if IsValid(ammo) then
pos.z = pos.z + 2
ammo:SetPos(pos)
ammo:SetAngles(VectorRand():Angle())
ammo:Spawn()
ammo:PhysWake()
end
end
end
return ent
end
-- Spawns a bunch of guns (scaling with maxplayers count or
-- by ttt_weapon_spawn_max cvar) at randomly selected
-- entities of the classes given the table
local function PlaceWeaponsAtEnts(spots_classes)
local spots = {}
for _, s in pairs(spots_classes) do
for _, e in ipairs(ents.FindByClass(s)) do
table.insert(spots, e)
end
end
local spawnables = ents.TTT.GetSpawnableSWEPs()
local max = GetConVar( "ttt_weapon_spawn_count" ):GetInt()
if max == 0 then
max = game.MaxPlayers()
max = max + math.max(3, 0.33 * max)
end
local num = 0
local w = nil
for k, v in RandomPairs(spots) do
w = table.Random(spawnables)
if w and IsValid(v) and util.IsInWorld(v:GetPos()) then
local spawned = PlaceWeapon(w, v:GetPos(), v:GetAngles())
num = num + 1
-- People with only a grenade are sad pandas. To get IsGrenade here,
-- we need the spawned ent that has inherited the goods from the
-- basegrenade swep.
if spawned and spawned.IsGrenade then
w = table.Random(spawnables)
if w then
PlaceWeapon(w, v:GetPos(), v:GetAngles())
end
end
end
if num > max then
return
end
end
end
local function PlaceExtraWeaponsForCSS()
MsgN("Weaponless CS:S-like map detected. Placing extra guns.")
local spots_classes = {
"info_player_terrorist",
"info_player_counterterrorist",
"hostage_entity"
};
PlaceWeaponsAtEnts(spots_classes)
end
-- TF2 actually has ammo ents and such, but unlike HL2DM there are not enough
-- different entities to do replacement.
local function PlaceExtraWeaponsForTF2()
MsgN("Weaponless TF2-like map detected. Placing extra guns.")
local spots_classes = {
"info_player_teamspawn",
"team_control_point",
"team_control_point_master",
"team_control_point_round",
"item_ammopack_full",
"item_ammopack_medium",
"item_ammopack_small",
"item_healthkit_full",
"item_healthkit_medium",
"item_healthkit_small",
"item_teamflag",
"game_intro_viewpoint",
"info_observer_point"
};
PlaceWeaponsAtEnts(spots_classes)
end
-- If there are no guns on the map, see if this looks like a TF2/CS:S map and
-- act appropriately
function ents.TTT.PlaceExtraWeapons()
-- If ents.FindByClass is constructed lazily or is an iterator, doing a
-- single loop should be faster than checking the table size.
-- Get out of here if there exists any weapon at all
for k,v in ipairs(ents.FindByClass("weapon_*")) do
-- See if it's the kind of thing we would spawn, to avoid the carry weapon
-- and such. Owned weapons are leftovers on players that will go away.
if IsValid(v) and v.AutoSpawnable and not IsValid(v:GetOwner()) then
return
end
end
-- All current TTT mappers use these, so if we find one we're good
for k,v in ipairs(ents.FindByClass("info_player_deathmatch")) do return end
-- CT spawns on the other hand are unlikely to be seen outside CS:S maps
for k,v in ipairs(ents.FindByClass("info_player_counterterrorist")) do
PlaceExtraWeaponsForCSS()
return
end
-- And same for TF2 team spawns
for k,v in ipairs(ents.FindByClass("info_player_teamspawn")) do
PlaceExtraWeaponsForTF2()
return
end
end
---- Weapon/ammo placement script importing
local function RemoveReplaceables()
-- This could be transformed into lots of FindByClass searches, one for every
-- key in the replace tables. Hopefully this is faster as more of the work is
-- done on the C side. Hard to measure.
for _, ent in ipairs(ents.FindByClass("item_*")) do
if hl2_ammo_replace[ent:GetClass()] then
ent:Remove()
end
end
for _, ent in ipairs(ents.FindByClass("weapon_*")) do
if hl2_weapon_replace[ent:GetClass()] then
ent:Remove()
end
end
end
local function RemoveWeaponEntities()
RemoveReplaceables()
for _, cls in pairs(ents.TTT.GetSpawnableAmmo()) do
for k, ent in ipairs(ents.FindByClass(cls)) do
ent:Remove()
end
end
for _, sw in pairs(ents.TTT.GetSpawnableSWEPs()) do
local cn = WEPS.GetClass(sw)
for k, ent in ipairs(ents.FindByClass(cn)) do
ent:Remove()
end
end
ents.TTT.RemoveRagdolls(false)
RemoveCrowbars()
end
local function RemoveSpawnEntities()
for k, ent in pairs(GetSpawnEnts(false, true)) do
ent.BeingRemoved = true -- they're not gone til next tick
ent:Remove()
end
end
local function CreateImportedEnt(cls, pos, ang, kv)
if not cls or not pos or not ang or not kv then return false end
local ent = ents.Create(cls)
if not IsValid(ent) then return false end
ent:SetPos(pos)
ent:SetAngles(ang)
for k,v in pairs(kv) do
ent:SetKeyValue(k, v)
end
ent:Spawn()
ent:PhysWake()
return true
end
function ents.TTT.CanImportEntities(map)
if not tostring(map) then return false end
if not GetConVar("ttt_use_weapon_spawn_scripts"):GetBool() then return false end
local fname = "maps/" .. map .. "_ttt.txt"
return file.Exists(fname, "GAME")
end
local function ImportSettings(map)
if not ents.TTT.CanImportEntities(map) then return end
local fname = "maps/" .. map .. "_ttt.txt"
local buf = file.Read(fname, "GAME")
local settings = {}
local lines = string.Explode("\n", buf)
for k, line in pairs(lines) do
if string.match(line, "^setting") then
local key, val = string.match(line, "^setting:\t(%w*) ([0-9]*)")
val = tonumber(val)
if key and val then
settings[key] = val
else
ErrorNoHalt("Invalid setting line " .. k .. " in " .. fname .. "\n")
end
end
end
return settings
end
local classremap = {
ttt_playerspawn = "info_player_deathmatch"
};
local function ImportEntities(map)
if not ents.TTT.CanImportEntities(map) then return end
local fname = "maps/" .. map .. "_ttt.txt"
local num = 0
for k, line in ipairs(string.Explode("\n", file.Read(fname, "GAME"))) do
if (not string.match(line, "^#")) and (not string.match(line, "^setting")) and line != "" and string.byte(line) != 0 then
local data = string.Explode("\t", line)
local fail = true -- pessimism
if data[2] and data[3] then
local cls = data[1]
local ang = nil
local pos = nil
local posraw = string.Explode(" ", data[2])
pos = Vector(tonumber(posraw[1]), tonumber(posraw[2]), tonumber(posraw[3]))
local angraw = string.Explode(" ", data[3])
ang = Angle(tonumber(angraw[1]), tonumber(angraw[2]), tonumber(angraw[3]))
-- Random weapons have a useful keyval
local kv = {}
if data[4] then
local kvraw = string.Explode(" ", data[4])
local key = kvraw[1]
local val = tonumber(kvraw[2])
if key and val then
kv[key] = val
end
end
-- Some dummy ents remap to different, real entity names
cls = classremap[cls] or cls
fail = not CreateImportedEnt(cls, pos, ang, kv)
end
if fail then
ErrorNoHalt("Invalid line " .. k .. " in " .. fname .. "\n")
else
num = num + 1
end
end
end
MsgN("Spawned " .. num .. " entities found in script.")
return true
end
function ents.TTT.ProcessImportScript(map)
MsgN("Weapon/ammo placement script found, attempting import...")
MsgN("Reading settings from script...")
local settings = ImportSettings(map)
if tobool(settings.replacespawns) then
MsgN("Removing existing player spawns")
RemoveSpawnEntities()
end
MsgN("Removing existing weapons/ammo")
RemoveWeaponEntities()
MsgN("Importing entities...")
local result = ImportEntities(map)
if result then
MsgN("Weapon placement script import successful!")
else
ErrorNoHalt("Weapon placement script import failed!\n")
end
end

View File

@@ -0,0 +1,28 @@
--[[
| 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/
--]]
local meta = FindMetaTable( "Entity" )
if not meta then return end
function meta:SetDamageOwner(ply)
self.dmg_owner = {ply = ply, t = CurTime()}
end
function meta:GetDamageOwner()
if self.dmg_owner then
return self.dmg_owner.ply, self.dmg_owner.t
end
end
function meta:IsExplosive()
local kv = self:GetKeyValues()["ExplodeDamage"]
return self:Health() > 0 and kv and kv > 0
end

View File

@@ -0,0 +1,133 @@
--[[
| 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/
--]]
-- This table is used by the client to show items in the equipment menu, and by
-- the server to check if a certain role is allowed to buy a certain item.
-- If you have custom items you want to add, consider using a separate lua
-- script that uses table.insert to add an entry to this table. This method
-- means you won't have to add your code back in after every TTT update. Just
-- make sure the script is also run on the client.
--
-- For example:
-- table.insert(EquipmentItems[ROLE_DETECTIVE], { id = EQUIP_ARMOR, ... })
--
-- Note that for existing items you can just do:
-- table.insert(EquipmentItems[ROLE_DETECTIVE], GetEquipmentItem(ROLE_TRAITOR, EQUIP_ARMOR))
-- Special equipment bitflags. Every unique piece of equipment needs its own
-- id.
--
-- Use the GenerateNewEquipmentID function (see below) to get a unique ID for
-- your equipment. This is guaranteed not to clash with other addons (as long
-- as they use the same safe method).
--
-- Details you shouldn't need:
-- The number should increase by a factor of two for every item (ie. ids
-- should be powers of two).
EQUIP_NONE = 0
EQUIP_ARMOR = 1
EQUIP_RADAR = 2
EQUIP_DISGUISE = 4
EQUIP_MAX = 4
-- Icon doesn't have to be in this dir, but all default ones are in here
local mat_dir = "vgui/ttt/"
-- Stick to around 35 characters per description line, and add a "\n" where you
-- want a new line to start.
EquipmentItems = {
[ROLE_DETECTIVE] = {
-- body armor
{ id = EQUIP_ARMOR,
loadout = true, -- default equipment for detectives
type = "item_passive",
material = mat_dir .. "icon_armor",
name = "item_armor",
desc = "item_armor_desc"
},
-- radar
{ id = EQUIP_RADAR,
type = "item_active",
material = mat_dir .. "icon_radar",
name = "item_radar",
desc = "item_radar_desc"
}
-- The default TTT equipment uses the language system to allow
-- translation. Below is an example of how the type, name and desc fields
-- would look with explicit non-localized text (which is probably what you
-- want when modding).
-- { id = EQUIP_ARMOR,
-- loadout = true, -- default equipment for detectives
-- type = "Passive effect item",
-- material = mat_dir .. "icon_armor",
-- name = "Body Armor",
-- desc = "Reduces bullet damage by 30% when\nyou get hit."
-- },
};
[ROLE_TRAITOR] = {
-- body armor
{ id = EQUIP_ARMOR,
type = "item_passive",
material = mat_dir .. "icon_armor",
name = "item_armor",
desc = "item_armor_desc"
},
-- radar
{ id = EQUIP_RADAR,
type = "item_active",
material = mat_dir .. "icon_radar",
name = "item_radar",
desc = "item_radar_desc"
},
-- disguiser
{ id = EQUIP_DISGUISE,
type = "item_active",
material = mat_dir .. "icon_disguise",
name = "item_disg",
desc = "item_disg_desc"
}
};
};
-- Search if an item is in the equipment table of a given role, and return it if
-- it exists, else return nil.
function GetEquipmentItem(role, id)
local tbl = EquipmentItems[role]
if not tbl then return end
for k, v in pairs(tbl) do
if v and v.id == id then
return v
end
end
end
-- Utility function to register a new Equipment ID
function GenerateNewEquipmentID()
EQUIP_MAX = EQUIP_MAX * 2
return EQUIP_MAX
end

View File

@@ -0,0 +1,391 @@
--[[
| 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/
--]]
---- Communicating game state to players
local net = net
local string = string
local table = table
local ipairs = ipairs
local IsValid = IsValid
-- NOTE: most uses of the Msg functions here have been moved to the LANG
-- functions. These functions are essentially deprecated, though they won't be
-- removed and can safely be used by SWEPs and the like.
function GameMsg(msg)
net.Start("TTT_GameMsg")
net.WriteString(msg)
net.WriteBit(false)
net.Broadcast()
end
function CustomMsg(ply_or_rf, msg, clr)
clr = clr or COLOR_WHITE
net.Start("TTT_GameMsgColor")
net.WriteString(msg)
net.WriteUInt(clr.r, 8)
net.WriteUInt(clr.g, 8)
net.WriteUInt(clr.b, 8)
if ply_or_rf then net.Send(ply_or_rf)
else net.Broadcast() end
end
-- Basic status message to single player or a recipientfilter
function PlayerMsg(ply_or_rf, msg, traitor_only)
net.Start("TTT_GameMsg")
net.WriteString(msg)
net.WriteBit(traitor_only)
if ply_or_rf then net.Send(ply_or_rf)
else net.Broadcast() end
end
-- Traitor-specific message that will appear in a special color
function TraitorMsg(ply_or_rfilter, msg)
PlayerMsg(ply_or_rfilter, msg, true)
end
-- Traitorchat
local function RoleChatMsg(sender, role, msg)
net.Start("TTT_RoleChat")
net.WriteUInt(role, 2)
net.WritePlayer(sender)
net.WriteString(msg)
net.Send(GetRoleFilter(role))
end
-- Round start info popup
function ShowRoundStartPopup()
for k, v in player.Iterator() do
if IsValid(v) and v:Team() == TEAM_TERROR and v:Alive() then
v:ConCommand("ttt_cl_startpopup")
end
end
end
local function GetPlayerFilter(pred)
local filter = {}
for k, v in player.Iterator() do
if IsValid(v) and pred(v) then
table.insert(filter, v)
end
end
return filter
end
function GetTraitorFilter(alive_only)
return GetPlayerFilter(function(p) return p:GetTraitor() and (not alive_only or p:IsTerror()) end)
end
function GetDetectiveFilter(alive_only)
return GetPlayerFilter(function(p) return p:IsDetective() and (not alive_only or p:IsTerror()) end)
end
function GetInnocentFilter(alive_only)
return GetPlayerFilter(function(p) return (not p:IsTraitor()) and (not alive_only or p:IsTerror()) end)
end
function GetRoleFilter(role, alive_only)
return GetPlayerFilter(function(p) return p:IsRole(role) and (not alive_only or p:IsTerror()) end)
end
---- Communication control
CreateConVar("ttt_limit_spectator_chat", "1", FCVAR_ARCHIVE + FCVAR_NOTIFY)
CreateConVar("ttt_limit_spectator_voice", "1", FCVAR_ARCHIVE + FCVAR_NOTIFY)
function GM:PlayerCanSeePlayersChat(text, team_only, listener, speaker)
if (not IsValid(listener)) then return false end
if (not IsValid(speaker)) then
if isentity(speaker) then
return true
else
return false
end
end
local sTeam = speaker:Team() == TEAM_SPEC
local lTeam = listener:Team() == TEAM_SPEC
if (GetRoundState() != ROUND_ACTIVE) or -- Round isn't active
(not GetConVar("ttt_limit_spectator_chat"):GetBool()) or -- Spectators can chat freely
(not DetectiveMode()) or -- Mumbling
(not sTeam and ((team_only and not speaker:IsSpecial()) or (not team_only))) or -- If someone alive talks (and not a special role in teamchat's case)
(not sTeam and team_only and speaker:GetRole() == listener:GetRole()) or
(sTeam and lTeam) then -- If the speaker and listener are spectators
return true
end
return false
end
local mumbles = {"mumble", "mm", "hmm", "hum", "mum", "mbm", "mble", "ham", "mammaries", "political situation", "mrmm", "hrm",
"uzbekistan", "mumu", "cheese export", "hmhm", "mmh", "mumble", "mphrrt", "mrh", "hmm", "mumble", "mbmm", "hmml", "mfrrm"}
-- While a round is active, spectators can only talk among themselves. When they
-- try to speak to all players they could divulge information about who killed
-- them. So we mumblify them. In detective mode, we shut them up entirely.
function GM:PlayerSay(ply, text, team_only)
if not IsValid(ply) then return text or "" end
if GetRoundState() == ROUND_ACTIVE then
local team = ply:Team() == TEAM_SPEC
if team and not DetectiveMode() then
local filtered = {}
for k, v in ipairs(string.Explode(" ", text)) do
-- grab word characters and whitelisted interpunction
-- necessary or leetspeek will be used (by trolls especially)
local word, interp = string.match(v, "(%a*)([%.,;!%?]*)")
if word != "" then
table.insert(filtered, mumbles[math.random(1, #mumbles)] .. interp)
end
end
-- make sure we have something to say
if table.IsEmpty(filtered) then
table.insert(filtered, mumbles[math.random(1, #mumbles)])
end
table.insert(filtered, 1, "[MUMBLED]")
return table.concat(filtered, " ")
elseif team_only and not team and ply:IsSpecial() then
RoleChatMsg(ply, ply:GetRole(), text)
return ""
end
end
return text or ""
end
-- Mute players when we are about to run map cleanup, because it might cause
-- net buffer overflows on clients.
local mute_all = false
function MuteForRestart(state)
mute_all = state
end
local loc_voice = CreateConVar("ttt_locational_voice", "0")
-- Of course voice has to be limited as well
function GM:PlayerCanHearPlayersVoice(listener, speaker)
-- Enforced silence
if mute_all then
return false, false
end
if (not IsValid(speaker)) or (not IsValid(listener)) or (listener == speaker) then
return false, false
end
-- limited if specific convar is on, or we're in detective mode
local limit = DetectiveMode() or GetConVar("ttt_limit_spectator_voice"):GetBool()
-- Spectators should not be heard by living players during round
if speaker:IsSpec() and (not listener:IsSpec()) and limit and GetRoundState() == ROUND_ACTIVE then
return false, false
end
-- Specific mute
if listener:IsSpec() and listener.mute_team == speaker:Team() or listener.mute_team == MUTE_ALL then
return false, false
end
-- Specs should not hear each other locationally
if speaker:IsSpec() and listener:IsSpec() then
return true, false
end
-- Traitors "team"chat by default, non-locationally
if speaker:IsActiveTraitor() then
if speaker.traitor_gvoice then
return true, loc_voice:GetBool()
elseif listener:IsActiveTraitor() then
return true, false
else
-- unless traitor_gvoice is true, normal innos can't hear speaker
return false, false
end
end
return true, (loc_voice:GetBool() and GetRoundState() != ROUND_POST)
end
local function SendTraitorVoiceState(speaker, state)
-- send umsg to living traitors that this is traitor-only talk
local rf = GetTraitorFilter(true)
-- make it as small as possible, to get there as fast as possible
-- we can fit it into a mere byte by being cheeky.
net.Start("TTT_TraitorVoiceState")
net.WriteUInt(speaker:EntIndex() - 1, 7) -- player ids can only be 1-128
net.WriteBit(state)
if rf then net.Send(rf)
else net.Broadcast() end
end
local function TraitorGlobalVoice(ply, cmd, args)
if not IsValid(ply) or not ply:IsActiveTraitor() then return end
if #args != 1 then return end
local state = tonumber(args[1])
ply.traitor_gvoice = (state == 1)
SendTraitorVoiceState(ply, ply.traitor_gvoice)
end
concommand.Add("tvog", TraitorGlobalVoice)
local MuteModes = {
[MUTE_NONE] = "mute_off",
[MUTE_TERROR] = "mute_living",
[MUTE_ALL] = "mute_all",
[MUTE_SPEC] = "mute_specs"
}
local function MuteTeam(ply, cmd, args)
if not IsValid(ply) then return end
if not (#args == 1 and tonumber(args[1])) then return end
if not ply:IsSpec() then
ply.mute_team = -1
return
end
local t = tonumber(args[1])
ply.mute_team = t
-- remove all ifs
LANG.Msg(ply, MuteModes[t])
end
concommand.Add("ttt_mute_team", MuteTeam)
local ttt_lastwords = CreateConVar("ttt_lastwords_chatprint", "0")
local LastWordContext = {
[KILL_NORMAL] = "",
[KILL_SUICIDE] = " *kills self*",
[KILL_FALL] = " *SPLUT*",
[KILL_BURN] = " *crackle*"
};
local function LastWordsMsg(ply, words)
-- only append "--" if there's no ending interpunction
local final = string.match(words, "[\\.\\!\\?]$") != nil
-- add optional context relating to death type
local context = LastWordContext[ply.death_type] or ""
local lastWordsStr = words .. (final and "" or "--") .. context
net.Start("TTT_LastWordsMsg")
net.WritePlayer(ply)
net.WriteString(lastWordsStr)
net.Broadcast()
hook.Run("TTTLastWordsMsg", ply, lastWordsStr)
end
local function LastWords(ply, cmd, args)
if IsValid(ply) and (not ply:Alive()) and #args > 1 then
local id = tonumber(args[1])
if id and ply.last_words_id and id == ply.last_words_id then
-- never allow multiple last word stuff
ply.last_words_id = nil
-- we will be storing this on the ragdoll
local rag = ply.server_ragdoll
if not (IsValid(rag) and rag.player_ragdoll) then
rag = nil
end
--- last id'd person
local last_seen = tonumber(args[2])
if last_seen then
local ent = Entity(last_seen)
if IsValid(ent) and ent:IsPlayer() and rag and (not rag.lastid) then
rag.lastid = {ent=ent, t=CurTime()}
end
end
--- last words
local words = string.Trim(args[3])
-- nothing of interest
if string.len(words) < 2 then return end
-- ignore admin commands
local firstchar = string.sub(words, 1, 1)
if firstchar == "!" or firstchar == "@" or firstchar == "/" then return end
if ttt_lastwords:GetBool() or ply.death_type == KILL_FALL then
LastWordsMsg(ply, words)
end
if rag and (not rag.last_words) then
rag.last_words = words
end
else
ply.last_words_id = nil
end
end
end
concommand.Add("_deathrec", LastWords)
-- Override or hook in plugin for spam prevention and whatnot. Return true
-- to block a command.
function GM:TTTPlayerRadioCommand(ply, msg_name, msg_target)
if ply.LastRadioCommand and ply.LastRadioCommand > (CurTime() - 0.5) then return true end
ply.LastRadioCommand = CurTime()
end
local function RadioCommand(ply, cmd, args)
if IsValid(ply) and ply:IsTerror() and #args == 2 then
local msg_name = args[1]
local msg_target = args[2]
local name = ""
local rag_name = nil
if tonumber(msg_target) then
-- player or corpse ent idx
local ent = Entity(tonumber(msg_target))
if IsValid(ent) then
if ent:IsPlayer() then
name = ent:Nick()
elseif ent:GetClass() == "prop_ragdoll" then
name = LANG.NameParam("quick_corpse_id")
rag_name = CORPSE.GetPlayerNick(ent, "A Terrorist")
end
end
msg_target = ent
else
-- lang string
name = LANG.NameParam(msg_target)
end
if hook.Call("TTTPlayerRadioCommand", GAMEMODE, ply, msg_name, msg_target) then
return
end
net.Start("TTT_RadioMsg")
net.WritePlayer(ply)
net.WriteString(msg_name)
net.WriteString(name)
if rag_name then
net.WriteString(rag_name)
end
net.Broadcast()
end
end
concommand.Add("_ttt_radio_send", RadioCommand)

View File

@@ -0,0 +1,978 @@
--[[
| 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/
--]]
---- Trouble in Terrorist Town
AddCSLuaFile("cl_init.lua")
AddCSLuaFile("shared.lua")
AddCSLuaFile("cl_hud.lua")
AddCSLuaFile("cl_msgstack.lua")
AddCSLuaFile("cl_hudpickup.lua")
AddCSLuaFile("cl_keys.lua")
AddCSLuaFile("cl_wepswitch.lua")
AddCSLuaFile("cl_awards.lua")
AddCSLuaFile("cl_scoring_events.lua")
AddCSLuaFile("cl_scoring.lua")
AddCSLuaFile("cl_popups.lua")
AddCSLuaFile("cl_equip.lua")
AddCSLuaFile("equip_items_shd.lua")
AddCSLuaFile("cl_help.lua")
AddCSLuaFile("cl_scoreboard.lua")
AddCSLuaFile("cl_tips.lua")
AddCSLuaFile("cl_voice.lua")
AddCSLuaFile("scoring_shd.lua")
AddCSLuaFile("util.lua")
AddCSLuaFile("lang_shd.lua")
AddCSLuaFile("corpse_shd.lua")
AddCSLuaFile("player_ext_shd.lua")
AddCSLuaFile("weaponry_shd.lua")
AddCSLuaFile("cl_radio.lua")
AddCSLuaFile("cl_radar.lua")
AddCSLuaFile("cl_tbuttons.lua")
AddCSLuaFile("cl_disguise.lua")
AddCSLuaFile("cl_transfer.lua")
AddCSLuaFile("cl_search.lua")
AddCSLuaFile("cl_targetid.lua")
AddCSLuaFile("vgui/ColoredBox.lua")
AddCSLuaFile("vgui/SimpleIcon.lua")
AddCSLuaFile("vgui/ProgressBar.lua")
AddCSLuaFile("vgui/ScrollLabel.lua")
AddCSLuaFile("vgui/sb_main.lua")
AddCSLuaFile("vgui/sb_row.lua")
AddCSLuaFile("vgui/sb_team.lua")
AddCSLuaFile("vgui/sb_info.lua")
include("shared.lua")
include("karma.lua")
include("entity.lua")
include("radar.lua")
include("admin.lua")
include("traitor_state.lua")
include("propspec.lua")
include("weaponry.lua")
include("gamemsg.lua")
include("ent_replace.lua")
include("scoring.lua")
include("corpse.lua")
include("player_ext_shd.lua")
include("player_ext.lua")
include("player.lua")
-- Round times
CreateConVar("ttt_roundtime_minutes", "10", FCVAR_NOTIFY)
CreateConVar("ttt_preptime_seconds", "30", FCVAR_NOTIFY)
CreateConVar("ttt_posttime_seconds", "30", FCVAR_NOTIFY)
CreateConVar("ttt_firstpreptime", "60")
-- Haste mode
local ttt_haste = CreateConVar("ttt_haste", "1", FCVAR_NOTIFY)
CreateConVar("ttt_haste_starting_minutes", "5", FCVAR_NOTIFY)
CreateConVar("ttt_haste_minutes_per_death", "0.5", FCVAR_NOTIFY)
-- Player Spawning
CreateConVar("ttt_spawn_wave_interval", "0")
CreateConVar("ttt_traitor_pct", "0.25")
CreateConVar("ttt_traitor_max", "32")
CreateConVar("ttt_detective_pct", "0.13", FCVAR_NOTIFY)
CreateConVar("ttt_detective_max", "32")
CreateConVar("ttt_detective_min_players", "8")
local detective_karma_min = CreateConVar("ttt_detective_karma_min", "600")
-- Traitor credits
CreateConVar("ttt_credits_starting", "2")
CreateConVar("ttt_credits_award_pct", "0.35")
CreateConVar("ttt_credits_award_size", "1")
CreateConVar("ttt_credits_award_repeat", "1")
CreateConVar("ttt_credits_detectivekill", "1")
CreateConVar("ttt_credits_alonebonus", "1")
-- Detective credits
CreateConVar("ttt_det_credits_starting", "1")
CreateConVar("ttt_det_credits_traitorkill", "0")
CreateConVar("ttt_det_credits_traitordead", "1")
-- Other
CreateConVar("ttt_use_weapon_spawn_scripts", "1")
CreateConVar("ttt_weapon_spawn_count", "0")
CreateConVar("ttt_round_limit", "6", FCVAR_ARCHIVE + FCVAR_NOTIFY + FCVAR_REPLICATED)
CreateConVar("ttt_time_limit_minutes", "75", FCVAR_NOTIFY + FCVAR_REPLICATED)
CreateConVar("ttt_idle_limit", "180", FCVAR_NOTIFY)
CreateConVar("ttt_voice_drain", "0", FCVAR_NOTIFY)
CreateConVar("ttt_voice_drain_normal", "0.2", FCVAR_NOTIFY)
CreateConVar("ttt_voice_drain_admin", "0.05", FCVAR_NOTIFY)
CreateConVar("ttt_voice_drain_recharge", "0.05", FCVAR_NOTIFY)
CreateConVar("ttt_namechange_kick", "1", FCVAR_NOTIFY)
CreateConVar("ttt_namechange_bantime", "10")
local ttt_detective = CreateConVar("ttt_sherlock_mode", "1", FCVAR_ARCHIVE + FCVAR_NOTIFY)
local ttt_minply = CreateConVar("ttt_minimum_players", "2", FCVAR_ARCHIVE + FCVAR_NOTIFY)
-- debuggery
local ttt_dbgwin = CreateConVar("ttt_debug_preventwin", "0")
-- Localise stuff we use often. It's like Lua go-faster stripes.
local math = math
local table = table
local net = net
local player = player
local timer = timer
local util = util
-- Pool some network names.
util.AddNetworkString("TTT_RoundState")
util.AddNetworkString("TTT_RagdollSearch")
util.AddNetworkString("TTT_GameMsg")
util.AddNetworkString("TTT_GameMsgColor")
util.AddNetworkString("TTT_RoleChat")
util.AddNetworkString("TTT_TraitorVoiceState")
util.AddNetworkString("TTT_LastWordsMsg")
util.AddNetworkString("TTT_RadioMsg")
util.AddNetworkString("TTT_ReportStream")
util.AddNetworkString("TTT_ReportStream_Part")
util.AddNetworkString("TTT_LangMsg")
util.AddNetworkString("TTT_ServerLang")
util.AddNetworkString("TTT_Equipment")
util.AddNetworkString("TTT_Credits")
util.AddNetworkString("TTT_Bought")
util.AddNetworkString("TTT_BoughtItem")
util.AddNetworkString("TTT_InterruptChat")
util.AddNetworkString("TTT_PlayerSpawned")
util.AddNetworkString("TTT_PlayerDied")
util.AddNetworkString("TTT_CorpseCall")
util.AddNetworkString("TTT_ClearClientState")
util.AddNetworkString("TTT_PerformGesture")
util.AddNetworkString("TTT_Role")
util.AddNetworkString("TTT_RoleList")
util.AddNetworkString("TTT_ConfirmUseTButton")
util.AddNetworkString("TTT_C4Config")
util.AddNetworkString("TTT_C4DisarmResult")
util.AddNetworkString("TTT_C4Warn")
util.AddNetworkString("TTT_ShowPrints")
util.AddNetworkString("TTT_ScanResult")
util.AddNetworkString("TTT_FlareScorch")
util.AddNetworkString("TTT_Radar")
util.AddNetworkString("TTT_Spectate")
---- Round mechanics
function GM:Initialize()
MsgN("Trouble In Terrorist Town gamemode initializing...")
-- Force friendly fire to be enabled. If it is off, we do not get lag compensation.
RunConsoleCommand("mp_friendlyfire", "1")
-- Default crowbar unlocking settings, may be overridden by config entity
GAMEMODE.crowbar_unlocks = {
[OPEN_DOOR] = true,
[OPEN_ROT] = true,
[OPEN_BUT] = true,
[OPEN_NOTOGGLE]= true
};
-- More map config ent defaults
GAMEMODE.force_plymodel = ""
GAMEMODE.propspec_allow_named = false
GAMEMODE.MapWin = WIN_NONE
GAMEMODE.AwardedCredits = false
GAMEMODE.AwardedCreditsDead = 0
GAMEMODE.round_state = ROUND_WAIT
GAMEMODE.FirstRound = true
GAMEMODE.RoundStartTime = 0
GAMEMODE.DamageLog = {}
GAMEMODE.LastRole = {}
GAMEMODE.playermodel = GetRandomPlayerModel()
GAMEMODE.playercolor = COLOR_WHITE
-- Delay reading of cvars until config has definitely loaded
GAMEMODE.cvar_init = false
SetGlobalFloat("ttt_round_end", -1)
SetGlobalFloat("ttt_haste_end", -1)
-- For the paranoid
math.randomseed(os.time())
WaitForPlayers()
if cvars.Number("sv_alltalk", 0) > 0 then
ErrorNoHalt("TTT WARNING: sv_alltalk is enabled. Dead players will be able to talk to living players. TTT will now attempt to set sv_alltalk 0.\n")
RunConsoleCommand("sv_alltalk", "0")
end
local cstrike = false
for _, g in ipairs(engine.GetGames()) do
if g.folder == 'cstrike' then cstrike = true end
end
if not cstrike then
ErrorNoHalt("TTT WARNING: CS:S does not appear to be mounted by GMod. Things may break in strange ways. Server admin? Check the TTT readme for help.\n")
end
end
-- Used to do this in Initialize, but server cfg has not always run yet by that
-- point.
function GM:InitCvars()
MsgN("TTT initializing convar settings...")
-- Initialize game state that is synced with client
SetGlobalInt("ttt_rounds_left", GetConVar("ttt_round_limit"):GetInt())
GAMEMODE:SyncGlobals()
KARMA.InitState()
self.cvar_init = true
end
function GM:InitPostEntity()
WEPS.ForcePrecache()
end
-- Convar replication is broken in gmod, so we do this.
-- I don't like it any more than you do, dear reader.
function GM:SyncGlobals()
SetGlobalBool("ttt_detective", ttt_detective:GetBool())
SetGlobalBool("ttt_haste", ttt_haste:GetBool())
SetGlobalInt("ttt_time_limit_minutes", GetConVar("ttt_time_limit_minutes"):GetInt())
SetGlobalBool("ttt_highlight_admins", GetConVar("ttt_highlight_admins"):GetBool())
SetGlobalBool("ttt_locational_voice", GetConVar("ttt_locational_voice"):GetBool())
SetGlobalInt("ttt_idle_limit", GetConVar("ttt_idle_limit"):GetInt())
SetGlobalBool("ttt_voice_drain", GetConVar("ttt_voice_drain"):GetBool())
SetGlobalFloat("ttt_voice_drain_normal", GetConVar("ttt_voice_drain_normal"):GetFloat())
SetGlobalFloat("ttt_voice_drain_admin", GetConVar("ttt_voice_drain_admin"):GetFloat())
SetGlobalFloat("ttt_voice_drain_recharge", GetConVar("ttt_voice_drain_recharge"):GetFloat())
end
function SendRoundState(state, ply)
net.Start("TTT_RoundState")
net.WriteUInt(state, 3)
return ply and net.Send(ply) or net.Broadcast()
end
-- Round state is encapsulated by set/get so that it can easily be changed to
-- eg. a networked var if this proves more convenient
function SetRoundState(state)
GAMEMODE.round_state = state
SCORE:RoundStateChange(state)
SendRoundState(state)
end
function GetRoundState()
return GAMEMODE.round_state
end
local function EnoughPlayers()
local ready = 0
-- only count truly available players, ie. no forced specs
for _, ply in player.Iterator() do
if IsValid(ply) and ply:ShouldSpawn() then
ready = ready + 1
end
end
return ready >= ttt_minply:GetInt()
end
-- Used to be in Think/Tick, now in a timer
function WaitingForPlayersChecker()
if GetRoundState() == ROUND_WAIT then
if EnoughPlayers() then
timer.Create("wait2prep", 1, 1, PrepareRound)
timer.Stop("waitingforply")
end
end
end
-- Start waiting for players
function WaitForPlayers()
SetRoundState(ROUND_WAIT)
if not timer.Start("waitingforply") then
timer.Create("waitingforply", 2, 0, WaitingForPlayersChecker)
end
end
-- When a player initially spawns after mapload, everything is a bit strange;
-- just making him spectator for some reason does not work right. Therefore,
-- we regularly check for these broken spectators while we wait for players
-- and immediately fix them.
function FixSpectators()
for k, ply in player.Iterator() do
if ply:IsSpec() and not ply:GetRagdollSpec() and ply:GetMoveType() < MOVETYPE_NOCLIP then
ply:Spectate(OBS_MODE_ROAMING)
end
end
end
-- Used to be in think, now a timer
local function WinChecker()
if GetRoundState() == ROUND_ACTIVE then
if CurTime() > GetGlobalFloat("ttt_round_end", 0) then
EndRound(WIN_TIMELIMIT)
else
local win = hook.Call("TTTCheckForWin", GAMEMODE)
if win != WIN_NONE then
EndRound(win)
end
end
end
end
local function NameChangeKick()
if not GetConVar("ttt_namechange_kick"):GetBool() then
timer.Remove("namecheck")
return
end
if GetRoundState() == ROUND_ACTIVE then
for _, ply in ipairs(player.GetHumans()) do
if ply.spawn_nick then
if ply.has_spawned and ply.spawn_nick != ply:Nick() and not hook.Call("TTTNameChangeKick", GAMEMODE, ply) then
local t = GetConVar("ttt_namechange_bantime"):GetInt()
local msg = "Changed name during a round"
if t > 0 then
ply:KickBan(t, msg)
else
ply:Kick(msg)
end
end
else
ply.spawn_nick = ply:Nick()
end
end
end
end
function StartNameChangeChecks()
if not GetConVar("ttt_namechange_kick"):GetBool() then return end
-- bring nicks up to date, may have been changed during prep/post
for _, ply in player.Iterator() do
ply.spawn_nick = ply:Nick()
end
if not timer.Exists("namecheck") then
timer.Create("namecheck", 3, 0, NameChangeKick)
end
end
function StartWinChecks()
if not timer.Start("winchecker") then
timer.Create("winchecker", 1, 0, WinChecker)
end
end
function StopWinChecks()
timer.Stop("winchecker")
end
local function CleanUp()
local et = ents.TTT
-- if we are going to import entities, it's no use replacing HL2DM ones as
-- soon as they spawn, because they'll be removed anyway
et.SetReplaceChecking(not et.CanImportEntities(game.GetMap()))
et.FixParentedPreCleanup()
game.CleanUpMap(false, nil, function() et.FixParentedPostCleanup() end)
-- Strip players now, so that their weapons are not seen by ReplaceEntities
for k,v in player.Iterator() do
if IsValid(v) then
v:StripWeapons()
end
end
-- a different kind of cleanup
hook.Remove("PlayerSay", "ULXMeCheck")
end
local function SpawnEntities()
local et = ents.TTT
-- Spawn weapons from script if there is one
local import = et.CanImportEntities(game.GetMap())
if import then
et.ProcessImportScript(game.GetMap())
else
-- Replace HL2DM/ZM ammo/weps with our own
et.ReplaceEntities()
-- Populate CS:S/TF2 maps with extra guns
et.PlaceExtraWeapons()
end
-- We're done resetting the map, unlock weapon pickups for the players about to respawn
GAMEMODE.RespawningWeapons = false
-- Finally, get players in there
SpawnWillingPlayers()
end
local function StopRoundTimers()
-- remove all timers
timer.Stop("wait2prep")
timer.Stop("prep2begin")
timer.Stop("end2prep")
timer.Stop("winchecker")
end
-- Make sure we have the players to do a round, people can leave during our
-- preparations so we'll call this numerous times
local function CheckForAbort()
if not EnoughPlayers() then
LANG.Msg("round_minplayers")
StopRoundTimers()
WaitForPlayers()
return true
end
return false
end
function GM:TTTDelayRoundStartForVote()
-- Can be used for custom voting systems
--return true, 30
return false
end
function PrepareRound()
-- Check playercount
if CheckForAbort() then return end
local delay_round, delay_length = hook.Call("TTTDelayRoundStartForVote", GAMEMODE)
if delay_round then
delay_length = delay_length or 30
LANG.Msg("round_voting", {num = delay_length})
timer.Create("delayedprep", delay_length, 1, PrepareRound)
return
end
-- Reset the map entities
GAMEMODE.RespawningWeapons = true
CleanUp()
GAMEMODE.MapWin = WIN_NONE
GAMEMODE.AwardedCredits = false
GAMEMODE.AwardedCreditsDead = 0
SCORE:Reset()
-- Update damage scaling
KARMA.RoundBegin()
-- New look. Random if no forced model set.
GAMEMODE.playermodel = GAMEMODE.force_plymodel == "" and GetRandomPlayerModel() or GAMEMODE.force_plymodel
GAMEMODE.playercolor = hook.Call("TTTPlayerColor", GAMEMODE, GAMEMODE.playermodel)
if CheckForAbort() then return end
-- Schedule round start
local ptime = GetConVar("ttt_preptime_seconds"):GetInt()
if GAMEMODE.FirstRound then
ptime = GetConVar("ttt_firstpreptime"):GetInt()
GAMEMODE.FirstRound = false
end
-- Piggyback on "round end" time global var to show end of phase timer
SetRoundEnd(CurTime() + ptime)
timer.Create("prep2begin", ptime, 1, BeginRound)
-- Mute for a second around traitor selection, to counter a dumb exploit
-- related to traitor's mics cutting off for a second when they're selected.
timer.Create("selectmute", ptime - 1, 1, function() MuteForRestart(true) end)
LANG.Msg("round_begintime", {num = ptime})
SetRoundState(ROUND_PREP)
-- Delay spawning until next frame to avoid ent overload
timer.Simple(0.01, SpawnEntities)
-- Undo the roundrestart mute, though they will once again be muted for the
-- selectmute timer.
timer.Create("restartmute", 1, 1, function() MuteForRestart(false) end)
net.Start("TTT_ClearClientState") net.Broadcast()
-- In case client's cleanup fails, make client set all players to innocent role
timer.Simple(1, SendRoleReset)
-- Tell hooks and map we started prep
hook.Call("TTTPrepareRound")
ents.TTT.TriggerRoundStateOutputs(ROUND_PREP)
end
function SetRoundEnd(endtime)
SetGlobalFloat("ttt_round_end", endtime)
end
function IncRoundEnd(incr)
SetRoundEnd(GetGlobalFloat("ttt_round_end", 0) + incr)
end
function TellTraitorsAboutTraitors()
local traitornicks = {}
for k,v in player.Iterator() do
if v:IsTraitor() then
table.insert(traitornicks, v:Nick())
end
end
-- This is ugly as hell, but it's kinda nice to filter out the names of the
-- traitors themselves in the messages to them
for k,v in player.Iterator() do
if v:IsTraitor() then
if #traitornicks < 2 then
LANG.Msg(v, "round_traitors_one")
return
else
local names = ""
for i,name in ipairs(traitornicks) do
if name != v:Nick() then
names = names .. name .. ", "
end
end
names = string.sub(names, 1, -3)
LANG.Msg(v, "round_traitors_more", {names = names})
end
end
end
end
function SpawnWillingPlayers(dead_only)
local wave_delay = GetConVar("ttt_spawn_wave_interval"):GetFloat()
-- simple method, should make this a case of the other method once that has
-- been tested.
if wave_delay <= 0 or dead_only then
for k, ply in player.Iterator() do
if IsValid(ply) then
ply:SpawnForRound(dead_only)
end
end
else
-- wave method
local num_spawns = #GetSpawnEnts()
local to_spawn = {}
for _, ply in RandomPairs(player.GetAll()) do
if IsValid(ply) and ply:ShouldSpawn() then
table.insert(to_spawn, ply)
GAMEMODE:PlayerSpawnAsSpectator(ply)
end
end
local sfn = function()
local c = 0
-- fill the available spawnpoints with players that need
-- spawning
while c < num_spawns and #to_spawn > 0 do
for k, ply in ipairs(to_spawn) do
if IsValid(ply) and ply:SpawnForRound() then
-- a spawn ent is now occupied
c = c + 1
end
-- Few possible cases:
-- 1) player has now been spawned
-- 2) player should remain spectator after all
-- 3) player has disconnected
-- In all cases we don't need to spawn them again.
table.remove(to_spawn, k)
-- all spawn ents are occupied, so the rest will have
-- to wait for next wave
if c >= num_spawns then
break
end
end
end
MsgN("Spawned " .. c .. " players in spawn wave.")
if #to_spawn == 0 then
timer.Remove("spawnwave")
MsgN("Spawn waves ending, all players spawned.")
end
end
MsgN("Spawn waves starting.")
timer.Create("spawnwave", wave_delay, 0, sfn)
-- already run one wave, which may stop the timer if everyone is spawned
-- in one go
sfn()
end
end
local function InitRoundEndTime()
-- Init round values
local endtime = CurTime() + (GetConVar("ttt_roundtime_minutes"):GetInt() * 60)
if HasteMode() then
endtime = CurTime() + (GetConVar("ttt_haste_starting_minutes"):GetInt() * 60)
-- this is a "fake" time shown to innocents, showing the end time if no
-- one would have been killed, it has no gameplay effect
SetGlobalFloat("ttt_haste_end", endtime)
end
SetRoundEnd(endtime)
end
function BeginRound()
GAMEMODE:SyncGlobals()
if CheckForAbort() then return end
InitRoundEndTime()
if CheckForAbort() then return end
-- Respawn dumb people who died during prep
SpawnWillingPlayers(true)
-- Remove their ragdolls
ents.TTT.RemoveRagdolls(true)
-- Check for low-karma players that weren't banned on round end
if KARMA.cv.autokick:GetBool() then KARMA.CheckAutoKickAll() end
if CheckForAbort() then return end
-- Select traitors & co. This is where things really start so we can't abort
-- anymore.
SelectRoles()
LANG.Msg("round_selected")
SendFullStateUpdate()
-- Edge case where a player joins just as the round starts and is picked as
-- traitor, but for whatever reason does not get the traitor state msg. So
-- re-send after a second just to make sure everyone is getting it.
timer.Simple(1, SendFullStateUpdate)
timer.Simple(10, SendFullStateUpdate)
SCORE:HandleSelection() -- log traitors and detectives
-- Give the StateUpdate messages ample time to arrive
timer.Simple(1.5, TellTraitorsAboutTraitors)
timer.Simple(2.5, ShowRoundStartPopup)
-- Start the win condition check timer
StartWinChecks()
StartNameChangeChecks()
timer.Create("selectmute", 1, 1, function() MuteForRestart(false) end)
GAMEMODE.DamageLog = {}
GAMEMODE.RoundStartTime = CurTime()
-- Sound start alarm
SetRoundState(ROUND_ACTIVE)
LANG.Msg("round_started")
ServerLog("Round proper has begun...\n")
GAMEMODE:UpdatePlayerLoadouts() -- needs to happen when round_active
hook.Call("TTTBeginRound")
ents.TTT.TriggerRoundStateOutputs(ROUND_BEGIN)
end
function PrintResultMessage(type)
ServerLog("Round ended.\n")
if type == WIN_TIMELIMIT then
LANG.Msg("win_time")
ServerLog("Result: timelimit reached, traitors lose.\n")
elseif type == WIN_TRAITOR then
LANG.Msg("win_traitor")
ServerLog("Result: traitors win.\n")
elseif type == WIN_INNOCENT then
LANG.Msg("win_innocent")
ServerLog("Result: innocent win.\n")
else
ServerLog("Result: unknown victory condition!\n")
end
end
function CheckForMapSwitch()
-- Check for mapswitch
local rounds_left = math.max(0, GetGlobalInt("ttt_rounds_left", 6) - 1)
SetGlobalInt("ttt_rounds_left", rounds_left)
local time_left = math.max(0, (GetConVar("ttt_time_limit_minutes"):GetInt() * 60) - CurTime())
local switchmap = false
local nextmap = string.upper(game.GetMapNext())
if rounds_left <= 0 then
LANG.Msg("limit_round", {mapname = nextmap})
switchmap = true
elseif time_left <= 0 then
LANG.Msg("limit_time", {mapname = nextmap})
switchmap = true
end
if switchmap then
timer.Stop("end2prep")
timer.Simple(15, game.LoadNextMap)
else
LANG.Msg("limit_left", {num = rounds_left,
time = math.ceil(time_left / 60),
mapname = nextmap})
end
end
function EndRound(type)
PrintResultMessage(type)
-- first handle round end
SetRoundState(ROUND_POST)
local ptime = math.max(5, GetConVar("ttt_posttime_seconds"):GetInt())
LANG.Msg("win_showreport", {num = ptime})
timer.Create("end2prep", ptime, 1, PrepareRound)
-- Piggyback on "round end" time global var to show end of phase timer
SetRoundEnd(CurTime() + ptime)
timer.Create("restartmute", ptime - 1, 1, function() MuteForRestart(true) end)
-- Stop checking for wins
StopWinChecks()
-- We may need to start a timer for a mapswitch, or start a vote
CheckForMapSwitch()
KARMA.RoundEnd()
-- now handle potentially error prone scoring stuff
-- register an end of round event
SCORE:RoundComplete(type)
-- update player scores
SCORE:ApplyEventLogScores(type)
-- send the clients the round log, players will be shown the report
SCORE:StreamToClients()
-- server plugins might want to start a map vote here or something
-- these hooks are not used by TTT internally
hook.Call("TTTEndRound", GAMEMODE, type)
ents.TTT.TriggerRoundStateOutputs(ROUND_POST, type)
end
function GM:MapTriggeredEnd(wintype)
self.MapWin = wintype
end
-- The most basic win check is whether both sides have one dude alive
function GM:TTTCheckForWin()
if ttt_dbgwin:GetBool() then return WIN_NONE end
if GAMEMODE.MapWin == WIN_TRAITOR or GAMEMODE.MapWin == WIN_INNOCENT then
local mw = GAMEMODE.MapWin
GAMEMODE.MapWin = WIN_NONE
return mw
end
local traitor_alive = false
local innocent_alive = false
for k,v in player.Iterator() do
if v:Alive() and v:IsTerror() then
if v:GetTraitor() then
traitor_alive = true
else
innocent_alive = true
end
end
if traitor_alive and innocent_alive then
return WIN_NONE --early out
end
end
if traitor_alive and not innocent_alive then
return WIN_TRAITOR
elseif not traitor_alive and innocent_alive then
return WIN_INNOCENT
elseif not innocent_alive then
-- ultimately if no one is alive, traitors win
return WIN_TRAITOR
end
return WIN_NONE
end
local function GetTraitorCount(ply_count)
-- get number of traitors: pct of players rounded down
local traitor_count = math.floor(ply_count * GetConVar("ttt_traitor_pct"):GetFloat())
-- make sure there is at least 1 traitor
traitor_count = math.Clamp(traitor_count, 1, GetConVar("ttt_traitor_max"):GetInt())
return traitor_count
end
local function GetDetectiveCount(ply_count)
if ply_count < GetConVar("ttt_detective_min_players"):GetInt() then return 0 end
local det_count = math.floor(ply_count * GetConVar("ttt_detective_pct"):GetFloat())
-- limit to a max
det_count = math.Clamp(det_count, 1, GetConVar("ttt_detective_max"):GetInt())
return det_count
end
function SelectRoles()
local choices = {}
local prev_roles = {
[ROLE_INNOCENT] = {},
[ROLE_TRAITOR] = {},
[ROLE_DETECTIVE] = {}
};
if not GAMEMODE.LastRole then GAMEMODE.LastRole = {} end
for k,v in player.Iterator() do
-- everyone on the spec team is in specmode
if IsValid(v) and (not v:IsSpec()) then
-- save previous role and sign up as possible traitor/detective
local r = GAMEMODE.LastRole[v:SteamID64()] or v:GetRole() or ROLE_INNOCENT
table.insert(prev_roles[r], v)
table.insert(choices, v)
end
v:SetRole(ROLE_INNOCENT)
end
-- determine how many of each role we want
local choice_count = #choices
local traitor_count = GetTraitorCount(choice_count)
local det_count = GetDetectiveCount(choice_count)
if choice_count == 0 then return end
-- first select traitors
local ts = 0
while (ts < traitor_count) and (#choices >= 1) do
-- select random index in choices table
local pick = math.random(1, #choices)
-- the player we consider
local pply = choices[pick]
-- make this guy traitor if he was not a traitor last time, or if he makes
-- a roll
if IsValid(pply) and
((not table.HasValue(prev_roles[ROLE_TRAITOR], pply)) or (math.random(1, 3) == 2)) then
pply:SetRole(ROLE_TRAITOR)
table.remove(choices, pick)
ts = ts + 1
end
end
-- now select detectives, explicitly choosing from players who did not get
-- traitor, so becoming detective does not mean you lost a chance to be
-- traitor
local ds = 0
local min_karma = detective_karma_min:GetInt()
while (ds < det_count) and (#choices >= 1) do
-- sometimes we need all remaining choices to be detective to fill the
-- roles up, this happens more often with a lot of detective-deniers
if #choices <= (det_count - ds) then
for k, pply in ipairs(choices) do
if IsValid(pply) then
pply:SetRole(ROLE_DETECTIVE)
end
end
break -- out of while
end
local pick = math.random(1, #choices)
local pply = choices[pick]
-- we are less likely to be a detective unless we were innocent last round
if (IsValid(pply) and
((pply:GetBaseKarma() > min_karma and
table.HasValue(prev_roles[ROLE_INNOCENT], pply)) or
math.random(1,3) == 2)) then
-- if a player has specified he does not want to be detective, we skip
-- him here (he might still get it if we don't have enough
-- alternatives)
if not pply:GetAvoidDetective() then
pply:SetRole(ROLE_DETECTIVE)
ds = ds + 1
end
table.remove(choices, pick)
end
end
GAMEMODE.LastRole = {}
for _, ply in player.Iterator() do
-- initialize credit count for everyone based on their role
ply:SetDefaultCredits()
-- store a steamid64 -> role map
GAMEMODE.LastRole[ply:SteamID64()] = ply:GetRole()
end
end
local function ForceRoundRestart(ply, command, args)
-- ply is nil on dedicated server console
if (not IsValid(ply)) or ply:IsAdmin() or ply:IsSuperAdmin() or cvars.Bool("sv_cheats", 0) then
LANG.Msg("round_restart")
StopRoundTimers()
-- do prep
PrepareRound()
else
ply:PrintMessage(HUD_PRINTCONSOLE, "You must be a GMod Admin or SuperAdmin on the server to use this command, or sv_cheats must be enabled.")
end
end
concommand.Add("ttt_roundrestart", ForceRoundRestart)
function ShowVersion(ply)
local text = Format("This is TTT version %s\n", GAMEMODE.Version)
if IsValid(ply) then
ply:PrintMessage(HUD_PRINTNOTIFY, text)
else
Msg(text)
end
end
concommand.Add("ttt_version", ShowVersion)

View File

@@ -0,0 +1,383 @@
--[[
| 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/
--]]
---- Karma system stuff
KARMA = {}
-- ply steamid64 -> karma table for disconnected players who might reconnect
KARMA.RememberedPlayers = {}
-- Convars, more convenient access than GetConVar bla bla
KARMA.cv = {}
KARMA.cv.enabled = CreateConVar("ttt_karma", "1", FCVAR_ARCHIVE)
KARMA.cv.strict = CreateConVar("ttt_karma_strict", "1")
KARMA.cv.starting = CreateConVar("ttt_karma_starting", "1000")
KARMA.cv.max = CreateConVar("ttt_karma_max", "1000")
KARMA.cv.ratio = CreateConVar("ttt_karma_ratio", "0.001")
KARMA.cv.killpenalty = CreateConVar("ttt_karma_kill_penalty", "15")
KARMA.cv.roundheal = CreateConVar("ttt_karma_round_increment", "5")
KARMA.cv.clean = CreateConVar("ttt_karma_clean_bonus", "30")
KARMA.cv.tbonus = CreateConVar("ttt_karma_traitorkill_bonus", "40")
KARMA.cv.tratio = CreateConVar("ttt_karma_traitordmg_ratio", "0.0003")
KARMA.cv.debug = CreateConVar("ttt_karma_debugspam", "0")
KARMA.cv.persist = CreateConVar("ttt_karma_persist", "0")
KARMA.cv.falloff = CreateConVar("ttt_karma_clean_half", "0.25")
KARMA.cv.autokick = CreateConVar("ttt_karma_low_autokick", "1")
KARMA.cv.kicklevel = CreateConVar("ttt_karma_low_amount", "450")
KARMA.cv.autoban = CreateConVar("ttt_karma_low_ban", "1")
KARMA.cv.bantime = CreateConVar("ttt_karma_low_ban_minutes", "60")
local config = KARMA.cv
local function IsDebug() return config.debug:GetBool() end
local math = math
cvars.AddChangeCallback("ttt_karma_max", function(cvar, old, new)
SetGlobalInt("ttt_karma_max", new)
end)
function KARMA.InitState()
SetGlobalBool("ttt_karma", config.enabled:GetBool())
SetGlobalInt("ttt_karma_max", config.max:GetFloat())
end
function KARMA.IsEnabled()
return GetGlobalBool("ttt_karma", false)
end
-- Compute penalty for hurting someone a certain amount
function KARMA.GetHurtPenalty(victim_karma, dmg)
return victim_karma * math.Clamp(dmg * config.ratio:GetFloat(), 0, 1)
end
-- Compute penalty for killing someone
function KARMA.GetKillPenalty(victim_karma)
-- the kill penalty handled like dealing a bit of damage
return KARMA.GetHurtPenalty(victim_karma, config.killpenalty:GetFloat())
end
-- Compute reward for hurting a traitor (when innocent yourself)
function KARMA.GetHurtReward(dmg)
return config.max:GetFloat() * math.Clamp(dmg * config.tratio:GetFloat(), 0, 1)
end
-- Compute reward for killing traitor
function KARMA.GetKillReward()
return KARMA.GetHurtReward(config.tbonus:GetFloat())
end
function KARMA.GivePenalty(ply, penalty, victim)
if not hook.Call( "TTTKarmaGivePenalty", nil, ply, penalty, victim ) then
ply:SetLiveKarma(math.max(ply:GetLiveKarma() - penalty, 0))
end
end
function KARMA.GiveReward(ply, reward)
reward = KARMA.DecayedMultiplier(ply) * reward
ply:SetLiveKarma(math.min(ply:GetLiveKarma() + reward, config.max:GetFloat()))
return reward
end
function KARMA.ApplyKarma(ply)
local df = 1
-- any karma at 1000 or over guarantees a df of 1, only when it's lower do we
-- need the penalty curve
if ply:GetBaseKarma() < 1000 and KARMA.IsEnabled() then
local k = ply:GetBaseKarma() - 1000
if config.strict:GetBool() then
-- this penalty curve sinks more quickly, less parabolic
df = 1 + (0.0007 * k) + (-0.000002 * (k^2))
else
df = 1 + -0.0000025 * (k^2)
end
end
ply:SetDamageFactor(math.Clamp(df, 0.1, 1.0))
if IsDebug() then
print(Format("%s has karma %f and gets df %f", ply:Nick(), ply:GetBaseKarma(), df))
end
end
-- Return true if a traitor could have easily avoided the damage/death
local function WasAvoidable(attacker, victim, dmginfo)
local infl = dmginfo:GetInflictor()
if attacker:IsTraitor() and victim:IsTraitor() and IsValid(infl) and infl.Avoidable then
return true
end
return false
end
-- Handle karma change due to one player damaging another. Damage must not have
-- been applied to the victim yet, but must have been scaled according to the
-- damage factor of the attacker.
function KARMA.Hurt(attacker, victim, dmginfo)
if not IsValid(attacker) or not IsValid(victim) then return end
if attacker == victim then return end
if not attacker:IsPlayer() or not victim:IsPlayer() then return end
-- Ignore excess damage
local hurt_amount = math.min(victim:Health(), dmginfo:GetDamage())
if attacker:GetTraitor() == victim:GetTraitor() then
if WasAvoidable(attacker, victim, dmginfo) then return end
local penalty = KARMA.GetHurtPenalty(victim:GetLiveKarma(), hurt_amount)
KARMA.GivePenalty(attacker, penalty, victim)
attacker:SetCleanRound(false)
if IsDebug() then
print(Format("%s (%f) attacked %s (%f) for %d and got penalised for %f", attacker:Nick(), attacker:GetLiveKarma(), victim:Nick(), victim:GetLiveKarma(), hurt_amount, penalty))
end
elseif (not attacker:GetTraitor()) and victim:GetTraitor() then
local reward = KARMA.GetHurtReward(hurt_amount)
reward = KARMA.GiveReward(attacker, reward)
if IsDebug() then
print(Format("%s (%f) attacked %s (%f) for %d and got REWARDED %f", attacker:Nick(), attacker:GetLiveKarma(), victim:Nick(), victim:GetLiveKarma(), hurt_amount, reward))
end
end
end
-- Handle karma change due to one player killing another.
function KARMA.Killed(attacker, victim, dmginfo)
if not IsValid(attacker) or not IsValid(victim) then return end
if attacker == victim then return end
if not attacker:IsPlayer() or not victim:IsPlayer() then return end
if attacker:GetTraitor() == victim:GetTraitor() then
-- don't penalise attacker for stupid victims
if WasAvoidable(attacker, victim, dmginfo) then return end
local penalty = KARMA.GetKillPenalty(victim:GetLiveKarma())
KARMA.GivePenalty(attacker, penalty, victim)
attacker:SetCleanRound(false)
if IsDebug() then
print(Format("%s (%f) killed %s (%f) and gets penalised for %f", attacker:Nick(), attacker:GetLiveKarma(), victim:Nick(), victim:GetLiveKarma(), penalty))
end
elseif (not attacker:GetTraitor()) and victim:GetTraitor() then
local reward = KARMA.GetKillReward()
reward = KARMA.GiveReward(attacker, reward)
if IsDebug() then
print(Format("%s (%f) killed %s (%f) and gets REWARDED %f", attacker:Nick(), attacker:GetLiveKarma(), victim:Nick(), victim:GetLiveKarma(), reward))
end
end
end
local expdecay = math.ExponentialDecay
function KARMA.DecayedMultiplier(ply)
local max = config.max:GetFloat()
local start = config.starting:GetFloat()
local k = ply:GetLiveKarma()
if config.falloff:GetFloat() <= 0 or k < start then
return 1
elseif k < max then
-- if falloff is enabled, then if our karma is above the starting value,
-- our round bonus is going to start decreasing as our karma increases
local basediff = max - start
local plydiff = k - start
local half = math.Clamp(config.falloff:GetFloat(), 0.01, 0.99)
-- exponentially decay the bonus such that when the player's excess karma
-- is at (basediff * half) the bonus is half of the original value
return expdecay(basediff * half, plydiff)
end
return 1
end
-- Handle karma regeneration upon the start of a new round
function KARMA.RoundIncrement()
local healbonus = config.roundheal:GetFloat()
local cleanbonus = config.clean:GetFloat()
for _, ply in player.Iterator() do
if ply:IsDeadTerror() and ply.death_type ~= KILL_SUICIDE or not ply:IsSpec() then
local bonus = healbonus + (ply:GetCleanRound() and cleanbonus or 0)
KARMA.GiveReward(ply, bonus)
if IsDebug() then
print(ply, "gets roundincr", incr)
end
end
end
-- player's CleanRound state will be reset by the ply class
end
-- When a new round starts, Live karma becomes Base karma
function KARMA.Rebase()
for _, ply in player.Iterator() do
if IsDebug() then
print(ply, "rebased from", ply:GetBaseKarma(), "to", ply:GetLiveKarma())
end
ply:SetBaseKarma(ply:GetLiveKarma())
end
end
-- Apply karma to damage factor for all players
function KARMA.ApplyKarmaAll()
for _, ply in player.Iterator() do
KARMA.ApplyKarma(ply)
end
end
function KARMA.NotifyPlayer(ply)
local df = ply:GetDamageFactor() or 1
local k = math.Round(ply:GetBaseKarma())
if df > 0.99 then
LANG.Msg(ply, "karma_dmg_full", {amount = k})
else
LANG.Msg(ply, "karma_dmg_other",
{amount = k,
num = math.ceil((1 - df) * 100)})
end
end
-- These generic fns will be called at round end and start, so that stuff can
-- easily be moved to a different phase
function KARMA.RoundEnd()
if KARMA.IsEnabled() then
KARMA.RoundIncrement()
-- if karma trend needs to be shown in round report, may want to delay
-- rebase until start of next round
KARMA.Rebase()
KARMA.RememberAll()
if config.autokick:GetBool() then
KARMA.CheckAutoKickAll()
end
end
end
function KARMA.RoundBegin()
KARMA.InitState()
if KARMA.IsEnabled() then
for _, ply in player.Iterator() do
KARMA.ApplyKarma(ply)
KARMA.NotifyPlayer(ply)
end
end
end
function KARMA.InitPlayer(ply)
local k = KARMA.Recall(ply) or config.starting:GetFloat()
k = math.Clamp(k, 0, config.max:GetFloat())
ply:SetBaseKarma(k)
ply:SetLiveKarma(k)
ply:SetCleanRound(true)
ply:SetDamageFactor(1.0)
-- compute the damagefactor based on actual (possibly loaded) karma
KARMA.ApplyKarma(ply)
end
function KARMA.Remember(ply)
if ply.karma_kicked or (not ply:IsFullyAuthenticated()) then return end
-- use sql if persistence is on
if config.persist:GetBool() then
ply:SetPData("karma_stored", ply:GetLiveKarma())
end
-- if persist is on, this is purely a backup method
KARMA.RememberedPlayers[ply:SteamID64()] = ply:GetLiveKarma()
end
function KARMA.Recall(ply)
if config.persist:GetBool()then
ply.delay_karma_recall = not ply:IsFullyAuthenticated()
if ply:IsFullyAuthenticated() then
local k = tonumber(ply:GetPData("karma_stored", nil))
if k then
return k
end
end
end
return KARMA.RememberedPlayers[ply:SteamID64()]
end
function KARMA.LateRecallAndSet(ply)
local k = tonumber(ply:GetPData("karma_stored", KARMA.RememberedPlayers[ply:SteamID64()]))
if k and k < ply:GetLiveKarma() then
ply:SetBaseKarma(k)
ply:SetLiveKarma(k)
end
end
function KARMA.RememberAll()
for _, ply in player.Iterator() do
KARMA.Remember(ply)
end
end
local reason = "Karma too low"
function KARMA.CheckAutoKick(ply)
if ply:GetBaseKarma() <= config.kicklevel:GetInt() then
if hook.Call("TTTKarmaLow", GAMEMODE, ply) == false then
return
end
ServerLog(ply:Nick() .. " autokicked/banned for low karma.\n")
-- flag player as autokicked so we don't perform the normal player
-- disconnect logic
ply.karma_kicked = true
if config.persist:GetBool() then
local k = math.Clamp(config.starting:GetFloat() * 0.8, config.kicklevel:GetFloat() * 1.1, config.max:GetFloat())
ply:SetPData("karma_stored", k)
KARMA.RememberedPlayers[ply:SteamID64()] = k
end
if config.autoban:GetBool() then
ply:KickBan(config.bantime:GetInt(), reason)
else
ply:Kick(reason)
end
end
end
function KARMA.CheckAutoKickAll()
for _, ply in player.Iterator() do
KARMA.CheckAutoKick(ply)
end
end
function KARMA.PrintAll(printfn)
for _, ply in player.Iterator() do
printfn(Format("%s : Live = %f -- Base = %f -- Dmg = %f\n",
ply:Nick(),
ply:GetLiveKarma(), ply:GetBaseKarma(),
ply:GetDamageFactor() * 100))
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
--[[
| 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/
--]]
---- Test/gimmick lang
-- Not an example of how you should translate something. See english.lua for that.
local L = LANG.CreateLanguage("Swedish chef")
local gsub = string.gsub
local function Borkify(word)
local b = string.byte(word:sub(1, 1))
if b > 64 and b < 91 then
return "Bork"
end
return "bork"
end
local realised = false
-- Upon selection, borkify every english string.
-- Even with all the string manipulation this only takes a few ms.
local function LanguageChanged(old, new)
if realised or new != "swedish chef" then return end
local eng = LANG.GetUnsafeNamed("english")
for k, v in pairs(eng) do
L[k] = gsub(v, "[{}%w]+", Borkify)
end
realised = true
end
hook.Add("TTTLanguageChanged", "ActivateChef", LanguageChanged)
-- As fallback, non-existent indices translated on the fly.
local GetFrom = LANG.GetTranslationFromLanguage
setmetatable(L,
{
__index = function(t, k)
local w = GetFrom(k, "english") or "bork"
return gsub(w, "[{}%w]+", "BORK")
end
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,141 @@
--[[
| 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/
--]]
---- Shared language stuff
-- tbl is first created here on both server and client
-- could make it a module but meh
if LANG then return end
LANG = {}
util.IncludeClientFile("cl_lang.lua")
-- Add all lua files in our /lang/ dir
local dir = GM.FolderName or "terrortown"
local files = file.Find(dir .. "/gamemode/lang/*.lua", "LUA" )
for _, fname in ipairs(files) do
local path = "lang/" .. fname
-- filter out directories and temp files (like .lua~)
if string.Right(fname, 3) == "lua" then
util.IncludeClientFile(path)
MsgN("Included TTT language file: " .. fname)
end
end
if SERVER then
local count = table.Count
-- Can be called as:
-- 1) LANG.Msg(ply, name, params) -- sent to ply
-- 2) LANG.Msg(name, params) -- sent to all
-- 3) LANG.Msg(role, name, params) -- sent to plys with role
function LANG.Msg(arg1, arg2, arg3)
if isstring(arg1) then
LANG.ProcessMsg(nil, arg1, arg2)
elseif isnumber(arg1) then
LANG.ProcessMsg(GetRoleFilter(arg1), arg2, arg3)
else
LANG.ProcessMsg(arg1, arg2, arg3)
end
end
function LANG.ProcessMsg(send_to, name, params)
-- don't want to send to null ents, but can't just IsValid send_to because
-- it may be a recipientfilter, so type check first
if type(send_to) == "Player" and (not IsValid(send_to)) then return end
-- number of keyval param pairs to send
local c = params and count(params) or 0
net.Start("TTT_LangMsg")
net.WriteString(name)
net.WriteUInt(c, 8)
if c > 0 then
for k, v in pairs(params) do
-- assume keys are strings, but vals may be numbers
net.WriteString(k)
net.WriteString(tostring(v))
end
end
if send_to then
net.Send(send_to)
else
net.Broadcast()
end
end
function LANG.MsgAll(name, params)
LANG.Msg(nil, name, params)
end
local lang_serverdefault = CreateConVar("ttt_lang_serverdefault", "english", FCVAR_ARCHIVE)
local function ServerLangRequest(ply, cmd, args)
if not IsValid(ply) then return end
net.Start("TTT_ServerLang")
net.WriteString(lang_serverdefault:GetString())
net.Send(ply)
end
concommand.Add("_ttt_request_serverlang", ServerLangRequest)
else -- CLIENT
local function RecvMsg()
local name = net.ReadString()
local c = net.ReadUInt(8)
local params = nil
if c > 0 then
params = {}
for i=1, c do
params[net.ReadString()] = net.ReadString()
end
end
LANG.Msg(name, params)
end
net.Receive("TTT_LangMsg", RecvMsg)
LANG.Msg = LANG.ProcessMsg
local function RecvServerLang()
local lang_name = net.ReadString()
lang_name = lang_name and string.lower(lang_name)
if LANG.Strings[lang_name] then
if LANG.IsServerDefault(GetConVar("ttt_language"):GetString()) then
LANG.SetActiveLanguage(lang_name)
end
LANG.ServerLanguage = lang_name
print("Server default language is:", lang_name)
end
end
net.Receive("TTT_ServerLang", RecvServerLang)
end
-- It can be useful to send string names as params, that the client can then
-- localize before interpolating. However, we want to prevent user input like
-- nicknames from being localized, so mark string names with something users
-- can't input.
function LANG.NameParam(name)
return "LID\t" .. name
end
LANG.Param = LANG.NameParam
function LANG.GetNameParam(str)
return string.match(str, "^LID\t([%w_]+)$")
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,366 @@
--[[
| 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/
--]]
-- serverside extensions to player table
local plymeta = FindMetaTable( "Player" )
if not plymeta then Error("FAILED TO FIND PLAYER TABLE") return end
function plymeta:SetRagdollSpec(s)
if s then
self.spec_ragdoll_start = CurTime()
end
self.spec_ragdoll = s
end
function plymeta:GetRagdollSpec() return self.spec_ragdoll end
AccessorFunc(plymeta, "force_spec", "ForceSpec", FORCE_BOOL)
--- Karma
-- The base/start karma is determined once per round and determines the player's
-- damage penalty. It is networked and shown on clients.
function plymeta:SetBaseKarma(k)
self:SetNWFloat("karma", k)
end
-- The live karma starts equal to the base karma, but is updated "live" as the
-- player damages/kills others. When another player damages/kills this one, the
-- live karma is used to determine his karma penalty.
AccessorFunc(plymeta, "live_karma", "LiveKarma", FORCE_NUMBER)
-- The damage factor scales how much damage the player deals, so if it is .9
-- then the player only deals 90% of his original damage.
AccessorFunc(plymeta, "dmg_factor", "DamageFactor", FORCE_NUMBER)
-- If a player does not damage team members in a round, he has a "clean" round
-- and gets a bonus for it.
AccessorFunc(plymeta, "clean_round", "CleanRound", FORCE_BOOL)
function plymeta:InitKarma()
KARMA.InitPlayer(self)
end
--- Equipment credits
function plymeta:SetCredits(amt)
self.equipment_credits = amt
self:SendCredits()
end
function plymeta:AddCredits(amt)
self:SetCredits(self:GetCredits() + amt)
end
function plymeta:SubtractCredits(amt) self:AddCredits(-amt) end
function plymeta:SetDefaultCredits()
if self:GetTraitor() then
local c = GetConVar("ttt_credits_starting"):GetInt()
if CountTraitors() == 1 then
c = c + GetConVar("ttt_credits_alonebonus"):GetInt()
end
self:SetCredits(c)
elseif self:GetDetective() then
self:SetCredits(GetConVar("ttt_det_credits_starting"):GetInt())
else
self:SetCredits(0)
end
end
function plymeta:SendCredits()
net.Start("TTT_Credits")
net.WriteUInt(self:GetCredits(), 8)
net.Send(self)
end
--- Equipment items
function plymeta:AddEquipmentItem(id)
id = tonumber(id)
if id then
self.equipment_items = bit.bor(self.equipment_items, id)
self:SendEquipment()
end
end
-- We do this instead of an NW var in order to limit the info to just this ply
function plymeta:SendEquipment()
net.Start("TTT_Equipment")
net.WriteUInt(self.equipment_items, 16)
net.Send(self)
end
function plymeta:ResetEquipment()
self.equipment_items = EQUIP_NONE
self:SendEquipment()
end
function plymeta:SendBought()
-- Send all as string, even though equipment are numbers, for simplicity
net.Start("TTT_Bought")
net.WriteUInt(#self.bought, 8)
for k, v in pairs(self.bought) do
net.WriteString(v)
end
net.Send(self)
end
local function ResendBought(ply)
if IsValid(ply) then ply:SendBought() end
end
concommand.Add("ttt_resend_bought", ResendBought)
function plymeta:ResetBought()
self.bought = {}
self:SendBought()
end
function plymeta:AddBought(id)
if not self.bought then self.bought = {} end
table.insert(self.bought, tostring(id))
self:SendBought()
end
-- Strips player of all equipment
function plymeta:StripAll()
-- standard stuff
self:StripAmmo()
self:StripWeapons()
-- our stuff
self:ResetEquipment()
self:SetCredits(0)
end
-- Sets all flags (force_spec, etc) to their default
function plymeta:ResetStatus()
self:SetRole(ROLE_INNOCENT)
self:SetRagdollSpec(false)
self:SetForceSpec(false)
self:ResetRoundFlags()
end
-- Sets round-based misc flags to default position. Called at PlayerSpawn.
function plymeta:ResetRoundFlags()
-- equipment
self:ResetEquipment()
self:SetCredits(0)
self:ResetBought()
-- equipment stuff
self.bomb_wire = nil
self.radar_charge = 0
self.decoy = nil
-- corpse
self:SetNWBool("body_found", false)
self.kills = {}
self.dying_wep = nil
self.was_headshot = false
-- communication
self.mute_team = -1
self.traitor_gvoice = false
self:SetNWBool("disguised", false)
-- karma
self:SetCleanRound(true)
self:Freeze(false)
end
function plymeta:GiveEquipmentItem(id)
if self:HasEquipmentItem(id) then
return false
elseif id and id > EQUIP_NONE then
self:AddEquipmentItem(id)
return true
end
end
-- Forced specs and latejoin specs should not get points
function plymeta:ShouldScore()
if self:GetForceSpec() then
return false
elseif self:IsSpec() and self:Alive() then
return false
else
return true
end
end
function plymeta:RecordKill(victim)
if not IsValid(victim) then return end
if not self.kills then
self.kills = {}
end
table.insert(self.kills, victim:SteamID64())
end
function plymeta:SetSpeed(slowed)
-- For player movement prediction to work properly, ply:SetSpeed turned out
-- to be a bad idea. It now uses GM:SetupMove, and the TTTPlayerSpeedModifier
-- hook is provided to let you change player speed without messing up
-- prediction. It needs to be hooked on both client and server and return the
-- same results (ie. same implementation).
error "Player:SetSpeed has been removed - please remove this call and use the TTTPlayerSpeedModifier hook in both CLIENT and SERVER environments"
end
function plymeta:ResetLastWords()
if not IsValid(self) then return end -- timers are dangerous things
self.last_words_id = nil
end
function plymeta:SendLastWords(dmginfo)
-- Use a pseudo unique id to prevent people from abusing the concmd
self.last_words_id = math.floor(CurTime() + math.random(500))
-- See if the damage was interesting
local dtype = KILL_NORMAL
if dmginfo:GetAttacker() == self or dmginfo:GetInflictor() == self then
dtype = KILL_SUICIDE
elseif dmginfo:IsDamageType(DMG_BURN) then
dtype = KILL_BURN
elseif dmginfo:IsFallDamage() then
dtype = KILL_FALL
end
self.death_type = dtype
net.Start("TTT_InterruptChat")
net.WriteUInt(self.last_words_id, 32)
net.Send(self)
-- any longer than this and you're out of luck
local ply = self
timer.Simple(2, function() ply:ResetLastWords() end)
end
function plymeta:ResetViewRoll()
local ang = self:EyeAngles()
if ang.r != 0 then
ang.r = 0
self:SetEyeAngles(ang)
end
end
function plymeta:ShouldSpawn()
-- do not spawn players who have not been through initspawn
if (not self:IsSpec()) and (not self:IsTerror()) then return false end
-- do not spawn forced specs
if self:IsSpec() and self:GetForceSpec() then return false end
return true
end
-- Preps a player for a new round, spawning them if they should. If dead_only is
-- true, only spawns if player is dead, else just makes sure he is healed.
function plymeta:SpawnForRound(dead_only)
hook.Call("PlayerSetModel", GAMEMODE, self)
hook.Call("TTTPlayerSetColor", GAMEMODE, self)
-- wrong alive status and not a willing spec who unforced after prep started
-- (and will therefore be "alive")
if dead_only and self:Alive() and (not self:IsSpec()) then
-- if the player does not need respawn, make sure he has full health
self:SetHealth(self:GetMaxHealth())
return false
end
if not self:ShouldSpawn() then return false end
-- reset propspec state that they may have gotten during prep
PROPSPEC.Clear(self)
-- respawn anyone else
if self:Team() == TEAM_SPEC then
self:UnSpectate()
end
self:StripAll()
self:SetTeam(TEAM_TERROR)
self:Spawn()
-- tell caller that we spawned
return true
end
function plymeta:InitialSpawn()
self.has_spawned = false
-- The team the player spawns on depends on the round state
self:SetTeam(GetRoundState() == ROUND_PREP and TEAM_TERROR or TEAM_SPEC)
-- Change some gmod defaults
self:SetCanZoom(false)
self:SetJumpPower(160)
self:SetCrouchedWalkSpeed(0.3)
self:SetRunSpeed(220)
self:SetWalkSpeed(220)
self:SetMaxSpeed(220)
-- Always spawn innocent initially, traitor will be selected later
self:ResetStatus()
-- Start off with clean, full karma (unless it can and should be loaded)
self:InitKarma()
-- We never have weapons here, but this inits our equipment state
self:StripAll()
end
function plymeta:KickBan(length, reason)
-- see admin.lua
PerformKickBan(self, length, reason)
end
local oldSpectate = plymeta.Spectate
function plymeta:Spectate(type)
oldSpectate(self, type)
-- NPCs should never see spectators. A workaround for the fact that gmod NPCs
-- do not ignore them by default.
self:SetNoTarget(true)
if type == OBS_MODE_ROAMING then
self:SetMoveType(MOVETYPE_NOCLIP)
end
end
local oldSpectateEntity = plymeta.SpectateEntity
function plymeta:SpectateEntity(ent)
oldSpectateEntity(self, ent)
if IsValid(ent) and ent:IsPlayer() then
self:SetupHands(ent)
end
end
local oldUnSpectate = plymeta.UnSpectate
function plymeta:UnSpectate()
oldUnSpectate(self)
self:SetNoTarget(false)
end
function plymeta:GetAvoidDetective()
return self:GetInfoNum("ttt_avoid_detective", 0) > 0
end

View File

@@ -0,0 +1,256 @@
--[[
| 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/
--]]
-- shared extensions to player table
local plymeta = FindMetaTable( "Player" )
if not plymeta then return end
local math = math
function plymeta:IsTerror() return self:Team() == TEAM_TERROR end
function plymeta:IsSpec() return self:Team() == TEAM_SPEC end
AccessorFunc(plymeta, "role", "Role", FORCE_NUMBER)
-- Role access
function plymeta:GetTraitor() return self:GetRole() == ROLE_TRAITOR end
function plymeta:GetDetective() return self:GetRole() == ROLE_DETECTIVE end
plymeta.IsTraitor = plymeta.GetTraitor
plymeta.IsDetective = plymeta.GetDetective
function plymeta:IsSpecial() return self:GetRole() != ROLE_INNOCENT end
-- Player is alive and in an active round
function plymeta:IsActive()
return self:IsTerror() and GetRoundState() == ROUND_ACTIVE
end
-- convenience functions for common patterns
function plymeta:IsRole(role) return self:GetRole() == role end
function plymeta:IsActiveRole(role) return self:IsRole(role) and self:IsActive() end
function plymeta:IsActiveTraitor() return self:IsActiveRole(ROLE_TRAITOR) end
function plymeta:IsActiveDetective() return self:IsActiveRole(ROLE_DETECTIVE) end
function plymeta:IsActiveSpecial() return self:IsSpecial() and self:IsActive() end
local role_strings = {
[ROLE_TRAITOR] = "traitor",
[ROLE_INNOCENT] = "innocent",
[ROLE_DETECTIVE] = "detective"
};
local GetRTranslation = CLIENT and LANG.GetRawTranslation or util.passthrough
-- Returns printable role
function plymeta:GetRoleString()
return GetRTranslation(role_strings[self:GetRole()]) or "???"
end
-- Returns role language string id, caller must translate if desired
function plymeta:GetRoleStringRaw()
return role_strings[self:GetRole()]
end
function plymeta:GetBaseKarma() return self:GetNWFloat("karma", 1000) end
function plymeta:HasEquipmentWeapon()
for _, wep in ipairs(self:GetWeapons()) do
if IsValid(wep) and wep:IsEquipment() then
return true
end
end
return false
end
function plymeta:CanCarryWeapon(wep)
if (not wep) or (not wep.Kind) then return false end
return self:CanCarryType(wep.Kind)
end
function plymeta:CanCarryType(t)
if not t then return false end
for _, w in ipairs(self:GetWeapons()) do
if w.Kind and w.Kind == t then
return false
end
end
return true
end
function plymeta:IsDeadTerror()
return (self:IsSpec() and not self:Alive())
end
function plymeta:HasBought(id)
return self.bought and table.HasValue(self.bought, id)
end
function plymeta:GetCredits() return self.equipment_credits or 0 end
function plymeta:GetEquipmentItems() return self.equipment_items or EQUIP_NONE end
-- Given an equipment id, returns if player owns this. Given nil, returns if
-- player has any equipment item.
function plymeta:HasEquipmentItem(id)
if not id then
return self:GetEquipmentItems() != EQUIP_NONE
else
return util.BitSet(self:GetEquipmentItems(), id)
end
end
function plymeta:HasEquipment()
return self:HasEquipmentItem() or self:HasEquipmentWeapon()
end
-- Override GetEyeTrace for an optional trace mask param. Technically traces
-- like GetEyeTraceNoCursor but who wants to type that all the time, and we
-- never use cursor tracing anyway.
function plymeta:GetEyeTrace(mask)
mask = mask or MASK_SOLID
if CLIENT then
local framenum = FrameNumber()
if self.LastPlayerTrace == framenum and self.LastPlayerTraceMask == mask then
return self.PlayerTrace
end
self.LastPlayerTrace = framenum
self.LastPlayerTraceMask = mask
end
local tr = util.GetPlayerTrace(self)
tr.mask = mask
tr = util.TraceLine(tr)
self.PlayerTrace = tr
return tr
end
if CLIENT then
function plymeta:AnimApplyGesture(act, weight)
self:AnimRestartGesture(GESTURE_SLOT_CUSTOM, act, true) -- true = autokill
self:AnimSetGestureWeight(GESTURE_SLOT_CUSTOM, weight)
end
local simple_runners = {
ACT_GMOD_GESTURE_DISAGREE,
ACT_GMOD_GESTURE_BECON,
ACT_GMOD_GESTURE_AGREE,
ACT_GMOD_GESTURE_WAVE,
ACT_GMOD_GESTURE_BOW,
ACT_SIGNAL_FORWARD,
ACT_SIGNAL_GROUP,
ACT_SIGNAL_HALT,
ACT_GMOD_TAUNT_CHEER,
ACT_GMOD_GESTURE_ITEM_PLACE,
ACT_GMOD_GESTURE_ITEM_DROP,
ACT_GMOD_GESTURE_ITEM_GIVE
}
local function MakeSimpleRunner(act)
return function (ply, w)
-- just let this gesture play itself and get out of its way
if w == 0 then
ply:AnimApplyGesture(act, 1)
return 1
else
return 0
end
end
end
-- act -> gesture runner fn
local act_runner = {
-- ear grab needs weight control
-- sadly it's currently the only one
[ACT_GMOD_IN_CHAT] =
function (ply, w)
local dest = ply:IsSpeaking() and 1 or 0
w = math.Approach(w, dest, FrameTime() * 10)
if w > 0 then
ply:AnimApplyGesture(ACT_GMOD_IN_CHAT, w)
end
return w
end
};
-- Insert all the "simple" gestures that do not need weight control
for _, a in ipairs(simple_runners) do
act_runner[a] = MakeSimpleRunner(a)
end
local show_gestures = CreateConVar("ttt_show_gestures", "1", FCVAR_ARCHIVE)
-- Perform the gesture using the GestureRunner system. If custom_runner is
-- non-nil, it will be used instead of the default runner for the act.
function plymeta:AnimPerformGesture(act, custom_runner)
if not show_gestures:GetBool() then return end
local runner = custom_runner or act_runner[act]
if not runner then return false end
self.GestureWeight = 0
self.GestureRunner = runner
return true
end
-- Perform a gesture update
function plymeta:AnimUpdateGesture()
if self.GestureRunner then
self.GestureWeight = self:GestureRunner(self.GestureWeight)
if self.GestureWeight <= 0 then
self.GestureRunner = nil
end
end
end
function GM:UpdateAnimation(ply, vel, maxseqgroundspeed)
ply:AnimUpdateGesture()
return self.BaseClass.UpdateAnimation(self, ply, vel, maxseqgroundspeed)
end
function GM:GrabEarAnimation(ply) end
net.Receive("TTT_PerformGesture", function()
local ply = net.ReadPlayer()
local act = net.ReadUInt(16)
if IsValid(ply) and act then
ply:AnimPerformGesture(act)
end
end)
else -- SERVER
-- On the server, we just send the client a message that the player is
-- performing a gesture. This allows the client to decide whether it should
-- play, depending on eg. a cvar.
function plymeta:AnimPerformGesture(act)
if not act then return end
net.Start("TTT_PerformGesture")
net.WritePlayer(self)
net.WriteUInt(act, 16)
net.Broadcast()
end
end

View File

@@ -0,0 +1,156 @@
--[[
| 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/
--]]
---- Spectator prop meddling
local string = string
local math = math
PROPSPEC = {}
local propspec_toggle = CreateConVar("ttt_spec_prop_control", "1")
local propspec_base = CreateConVar("ttt_spec_prop_base", "8")
local propspec_min = CreateConVar("ttt_spec_prop_maxpenalty", "-6")
local propspec_max = CreateConVar("ttt_spec_prop_maxbonus", "16")
function PROPSPEC.Start(ply, ent)
ply:Spectate(OBS_MODE_CHASE)
ply:SpectateEntity(ent, true)
local bonus = math.Clamp(math.ceil(ply:Frags() / 2), propspec_min:GetInt(), propspec_max:GetInt())
ply.propspec = {ent=ent, t=0, retime=0, punches=0, max=propspec_base:GetInt() + bonus}
ent:SetNWEntity("spec_owner", ply)
ply:SetNWInt("bonuspunches", bonus)
end
local function IsWhitelistedClass(cls)
return (string.match(cls, "prop_physics*") or
string.match(cls, "func_physbox*"))
end
function PROPSPEC.Target(ply, ent)
if not propspec_toggle:GetBool() then return end
if (not IsValid(ply)) or (not ply:IsSpec()) or (not IsValid(ent)) then return end
if IsValid(ent:GetNWEntity("spec_owner", nil)) then return end
local phys = ent:GetPhysicsObject()
if ent:GetName() != "" and (not GAMEMODE.propspec_allow_named) then return end
if (not IsValid(phys)) or (not phys:IsMoveable()) then return end
-- normally only specific whitelisted ent classes can be possessed, but
-- custom ents can mark themselves possessable as well
if (not ent.AllowPropspec) and (not IsWhitelistedClass(ent:GetClass())) then return end
PROPSPEC.Start(ply, ent)
end
-- Clear any propspec state a player has. Safe even if player is not currently
-- spectating.
function PROPSPEC.Clear(ply)
local ent = (ply.propspec and ply.propspec.ent) or ply:GetObserverTarget()
if IsValid(ent) then
ent:SetNWEntity("spec_owner", nil)
end
ply.propspec = nil
ply:SpectateEntity(nil)
end
function PROPSPEC.End(ply)
PROPSPEC.Clear(ply)
ply:Spectate(OBS_MODE_ROAMING)
ply:ResetViewRoll()
timer.Simple(0.1, function()
if IsValid(ply) then ply:ResetViewRoll() end
end)
end
local propspec_force = CreateConVar("ttt_spec_prop_force", "110")
function PROPSPEC.Key(ply, key)
local ent = ply.propspec.ent
local phys = IsValid(ent) and ent:GetPhysicsObject()
if (not IsValid(ent)) or (not IsValid(phys)) then
PROPSPEC.End(ply)
return false
end
if not phys:IsMoveable() then
PROPSPEC.End(ply)
return true
elseif phys:HasGameFlag(FVPHYSICS_PLAYER_HELD) then
-- we can stay with the prop while it's held, but not affect it
if key == IN_DUCK then
PROPSPEC.End(ply)
end
return true
end
-- always allow leaving
if key == IN_DUCK then
PROPSPEC.End(ply)
return true
end
local pr = ply.propspec
if pr.t > CurTime() then return true end
if pr.punches < 1 then return true end
local m = math.min(150, phys:GetMass())
local force = propspec_force:GetInt()
local aim = ply:GetAimVector()
local mf = m * force
pr.t = CurTime() + 0.15
if key == IN_JUMP then
-- upwards bump
phys:ApplyForceCenter(Vector(0,0, mf))
pr.t = CurTime() + 0.05
elseif key == IN_FORWARD then
-- bump away from player
phys:ApplyForceCenter(aim * mf)
elseif key == IN_BACK then
phys:ApplyForceCenter(aim * (mf * -1))
elseif key == IN_MOVELEFT then
phys:AddAngleVelocity(Vector(0, 0, 200))
phys:ApplyForceCenter(Vector(0,0, mf / 3))
elseif key == IN_MOVERIGHT then
phys:AddAngleVelocity(Vector(0, 0, -200))
phys:ApplyForceCenter(Vector(0,0, mf / 3))
else
return true -- eat other keys, and do not decrement punches
end
pr.punches = math.max(pr.punches - 1, 0)
ply:SetNWFloat("specpunches", pr.punches / pr.max)
return true
end
local propspec_retime = CreateConVar("ttt_spec_prop_rechargetime", "1")
function PROPSPEC.Recharge(ply)
local pr = ply.propspec
if pr.retime < CurTime() then
pr.punches = math.min(pr.punches + 1, pr.max)
ply:SetNWFloat("specpunches", pr.punches / pr.max)
pr.retime = CurTime() + propspec_retime:GetFloat()
end
end

View File

@@ -0,0 +1,81 @@
--[[
| 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/
--]]
-- Traitor radar functionality
-- should mirror client
local chargetime = 30
local math = math
local function RadarScan(ply, cmd, args)
if IsValid(ply) and ply:IsTerror() then
if ply:HasEquipmentItem(EQUIP_RADAR) then
if ply.radar_charge > CurTime() then
LANG.Msg(ply, "radar_charging")
return
end
ply.radar_charge = CurTime() + chargetime
local scan_ents = player.GetAll()
table.Add(scan_ents, ents.FindByClass("ttt_decoy"))
local targets = {}
for k, p in ipairs(scan_ents) do
if ply == p or (not IsValid(p)) then continue end
if p:IsPlayer() then
if not p:IsTerror() then continue end
if p:GetNWBool("disguised", false) and (not ply:IsTraitor()) then continue end
end
local pos = p:LocalToWorld(p:OBBCenter())
-- Round off, easier to send and inaccuracy does not matter
pos.x = math.Round(pos.x)
pos.y = math.Round(pos.y)
pos.z = math.Round(pos.z)
local role = p:IsPlayer() and p:GetRole() or -1
if not p:IsPlayer() then
-- Decoys appear as innocents for non-traitors
if not ply:IsTraitor() then
role = ROLE_INNOCENT
end
elseif role != ROLE_INNOCENT and role != ply:GetRole() then
-- Detectives/Traitors can see who has their role, but not who
-- has the opposite role.
role = ROLE_INNOCENT
end
table.insert(targets, {role=role, pos=pos})
end
net.Start("TTT_Radar")
net.WriteUInt(#targets, 8)
for k, tgt in ipairs(targets) do
net.WriteUInt(tgt.role, 2)
net.WriteInt(tgt.pos.x, 15)
net.WriteInt(tgt.pos.y, 15)
net.WriteInt(tgt.pos.z, 15)
end
net.Send(ply)
else
LANG.Msg(ply, "radar_not_owned")
end
end
end
concommand.Add("ttt_radar_scan", RadarScan)

View File

@@ -0,0 +1,256 @@
--[[
| 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/
--]]
---- Customized scoring
local math = math
local string = string
local table = table
local pairs = pairs
SCORE = SCORE or {}
SCORE.Events = SCORE.Events or {}
include("scoring_shd.lua")
-- One might wonder why all the key names in the event tables are so annoyingly
-- short. Well, the serialisation module in gmod (glon) does not do any
-- compression. At all. This means the difference between all events having a
-- "time_added" key versus a "t" key is very significant for the amount of data
-- we need to send. It's a pain, but I'm not going to code my own compression,
-- so doing it manually is the only way.
-- One decent way to reduce data sent turned out to be rounding the time floats.
-- We don't actually need to know about 10000ths of seconds after all.
function SCORE:AddEvent(entry, t_override)
entry.t = t_override or CurTime()
table.insert(self.Events, entry)
end
local function CopyDmg(dmg)
local wep = util.WeaponFromDamage(dmg)
local g, n
if wep then
local id = WepToEnum(wep)
if id then
g = id
else
-- we can convert each standard TTT weapon name to a preset ID, but
-- that's not workable with custom SWEPs from people, so we'll just
-- have to pay the byte tax there
g = wep:GetClass()
end
else
local infl = dmg:GetInflictor()
if IsValid(infl) and infl.ScoreName then
n = infl.ScoreName
end
end
-- t = type, a = amount, g = gun, h = headshot, n = name
return {
t = dmg:GetDamageType(),
a = dmg:GetDamage(),
h = false,
g = g,
n = n
}
end
function SCORE:HandleKill(victim, attacker, dmginfo)
if not (IsValid(victim) and victim:IsPlayer()) then return end
local e = {
id=EVENT_KILL,
att={ni="", sid64=-1, tr=false},
vic={ni=victim:Nick(), sid64=victim:SteamID64(), tr=false},
dmg=CopyDmg(dmginfo)};
e.dmg.h = victim.was_headshot
e.vic.tr = victim:GetTraitor()
if IsValid(attacker) and attacker:IsPlayer() then
e.att.ni = attacker:Nick()
e.att.sid64 = attacker:SteamID64()
e.att.tr = attacker:GetTraitor()
-- If a traitor gets himself killed by another traitor's C4, it's his own
-- damn fault for ignoring the indicator.
if dmginfo:IsExplosionDamage() and attacker:GetTraitor() and victim:GetTraitor() then
local infl = dmginfo:GetInflictor()
if IsValid(infl) and infl:GetClass() == "ttt_c4" then
e.att = table.Copy(e.vic)
end
end
end
self:AddEvent(e)
end
function SCORE:HandleSpawn(ply)
if ply:Team() == TEAM_TERROR then
self:AddEvent({id=EVENT_SPAWN, ni=ply:Nick(), sid64=ply:SteamID64()})
end
end
function SCORE:HandleSelection()
local traitors = {}
local detectives = {}
for k, ply in player.Iterator() do
if ply:GetTraitor() then
table.insert(traitors, ply:SteamID64())
elseif ply:GetDetective() then
table.insert(detectives, ply:SteamID64())
end
end
self:AddEvent({id=EVENT_SELECTED, traitor_ids=traitors, detective_ids=detectives})
end
function SCORE:HandleBodyFound(finder, found)
self:AddEvent({id=EVENT_BODYFOUND, ni=finder:Nick(), sid64=finder:SteamID64(), b=found:Nick()})
end
function SCORE:HandleC4Explosion(planter, arm_time, exp_time)
local nick = "Someone"
if IsValid(planter) and planter:IsPlayer() then
nick = planter:Nick()
end
self:AddEvent({id=EVENT_C4PLANT, ni=nick}, arm_time)
self:AddEvent({id=EVENT_C4EXPLODE, ni=nick}, exp_time)
end
function SCORE:HandleC4Disarm(disarmer, owner, success)
if disarmer == owner then return end
if not IsValid(disarmer) then return end
local ev = {
id = EVENT_C4DISARM,
ni = disarmer:Nick(),
s = success
};
if IsValid(owner) then
ev.own = owner:Nick()
end
self:AddEvent(ev)
end
function SCORE:HandleCreditFound(finder, found_nick, credits)
self:AddEvent({id=EVENT_CREDITFOUND, ni=finder:Nick(), sid64=finder:SteamID64(), b=found_nick, cr=credits})
end
function SCORE:ApplyEventLogScores(wintype)
local scores = {}
local traitors = {}
local detectives = {}
for k, ply in player.Iterator() do
scores[ply:SteamID64()] = {}
if ply:GetTraitor() then
table.insert(traitors, ply:SteamID64())
elseif ply:GetDetective() then
table.insert(detectives, ply:SteamID64())
end
end
-- individual scores, and count those left alive
local alive = {traitors = 0, innos = 0}
local dead = {traitors = 0, innos = 0}
local scored_log = ScoreEventLog(self.Events, scores, traitors, detectives)
local ply = nil
for sid64, s in pairs(scored_log) do
ply = player.GetBySteamID64(sid64)
if ply and ply:ShouldScore() then
ply:AddFrags(KillsToPoints(s, ply:GetTraitor()))
end
end
-- team scores
local bonus = ScoreTeamBonus(scored_log, wintype)
for sid64, s in pairs(scored_log) do
ply = player.GetBySteamID64(sid64)
if ply and ply:ShouldScore() then
ply:AddFrags(ply:GetTraitor() and bonus.traitors or bonus.innos)
end
end
-- count deaths
local events = self.Events
for i = 1, #events do
local e = events[i]
if e.id == EVENT_KILL then
local victim = player.GetBySteamID64(e.vic.sid64)
if IsValid(victim) and victim:ShouldScore() then
victim:AddDeaths(1)
end
end
end
end
function SCORE:RoundStateChange(newstate)
self:AddEvent({id=EVENT_GAME, state=newstate})
end
function SCORE:RoundComplete(wintype)
self:AddEvent({id=EVENT_FINISH, win=wintype})
end
function SCORE:Reset()
self.Events = {}
end
function SCORE:StreamToClients()
local events = util.TableToJSON(self.Events)
if events == nil then
ErrorNoHalt("Round report event encoding failed!\n")
return
end
events = util.Compress(events)
if events == "" then
ErrorNoHalt("Round report event compression failed!\n")
return
end
-- divide into happy lil bits.
-- this was necessary with user messages, now it's
-- a just-in-case thing if a round somehow manages to be > 64K
local len = #events
local MaxStreamLength = SCORE.MaxStreamLength
if len <= MaxStreamLength then
net.Start("TTT_ReportStream")
net.WriteUInt(len, 16)
net.WriteData(events, len)
net.Broadcast()
else
local curpos = 0
repeat
net.Start("TTT_ReportStream_Part")
net.WriteData(string.sub(events, curpos + 1, curpos + MaxStreamLength + 1), MaxStreamLength)
net.Broadcast()
curpos = curpos + MaxStreamLength + 1
until(len - curpos <= MaxStreamLength)
net.Start("TTT_ReportStream")
net.WriteUInt(len, 16)
net.WriteData(string.sub(events, curpos + 1, len), len - curpos)
net.Broadcast()
end
end

View File

@@ -0,0 +1,212 @@
--[[
| 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/
--]]
-- Server and client both need this for scoring event logs
-- 2^16 bytes - 4 (header) - 2 (UInt length in TTT_ReportStream) - 1 (terminanting byte)
(SERVER and SCORE or CLSCORE).MaxStreamLength = 65529
function ScoreInit()
return {
deaths=0,
suicides=0,
innos=0,
traitors=0,
was_traitor=false,
bonus=0 -- non-kill points to add
};
end
function ScoreEvent(e, scores)
if e.id == EVENT_KILL then
local aid = e.att.sid64
local vid = e.vic.sid64
-- make sure a score table exists for this person
-- he might have disconnected by now
if scores[vid] == nil then
scores[vid] = ScoreInit()
-- normally we have the ply:GetTraitor stuff to base this on, but that
-- won't do for disconnected players
scores[vid].was_traitor = e.vic.tr
end
if scores[aid] == nil then
scores[aid] = ScoreInit()
scores[aid].was_traitor = e.att.tr
end
scores[vid].deaths = scores[vid].deaths + 1
if aid == vid then
scores[vid].suicides = scores[vid].suicides + 1
elseif aid != -1 then
if e.vic.tr then
scores[aid].traitors = scores[aid].traitors + 1
elseif not e.vic.tr then
scores[aid].innos = scores[aid].innos + 1
end
end
elseif e.id == EVENT_BODYFOUND then
local sid64 = e.sid64
if scores[sid64] == nil or scores[sid64].was_traitor then return end
local find_bonus = scores[sid64].was_detective and 3 or 1
scores[sid64].bonus = scores[sid64].bonus + find_bonus
end
end
-- events should be event log as generated by scoring.lua
-- scores should be table with SteamID64s as keys
-- The method of finding these IDs differs between server and client
function ScoreEventLog(events, scores, traitors, detectives)
for k, s in pairs(scores) do
scores[k] = ScoreInit()
scores[k].was_traitor = table.HasValue(traitors, k)
scores[k].was_detective = table.HasValue(detectives, k)
end
local tmp = nil
for k, e in pairs(events) do
ScoreEvent(e, scores)
end
return scores
end
function ScoreTeamBonus(scores, wintype)
local alive = {traitors = 0, innos = 0}
local dead = {traitors = 0, innos = 0}
for k, sc in pairs(scores) do
local state = (sc.deaths == 0) and alive or dead
if sc.was_traitor then
state.traitors = state.traitors + 1
else
state.innos = state.innos + 1
end
end
local bonus = {}
bonus.traitors = (alive.traitors * 1) + math.ceil(dead.innos * 0.5)
bonus.innos = alive.innos * 1
-- running down the clock must never be beneficial for traitors
if wintype == WIN_TIMELIMIT then
bonus.traitors = math.floor(alive.innos * -0.5) + math.ceil(dead.innos * 0.5)
end
return bonus
end
-- Scores were initially calculated as points immediately, but not anymore, so
-- we can convert them using this fn
function KillsToPoints(score, was_traitor)
return ((score.suicides * -1)
+ score.bonus
+ (score.traitors * (was_traitor and -16 or 5))
+ (score.innos * (was_traitor and 1 or -8))
+ (score.deaths == 0 and 1 or 0)) --effectively 2 due to team bonus
--for your own survival
end
---- Weapon AMMO_ enum stuff, used only in score.lua/cl_score.lua these days
-- Not actually ammo identifiers anymore, but still weapon identifiers. Used
-- only in round report (score.lua) to save bandwidth because we can't use
-- pooled strings there. Custom SWEPs are sent as classname string and don't
-- need to bother with these.
AMMO_DEAGLE = 2
AMMO_PISTOL = 3
AMMO_MAC10 = 4
AMMO_RIFLE = 5
AMMO_SHOTGUN = 7
-- Following are custom, intentionally out of ammo enum range
AMMO_CROWBAR = 50
AMMO_SIPISTOL = 51
AMMO_C4 = 52
AMMO_FLARE = 53
AMMO_KNIFE = 54
AMMO_M249 = 55
AMMO_M16 = 56
AMMO_DISCOMB = 57
AMMO_POLTER = 58
AMMO_TELEPORT = 59
AMMO_RADIO = 60
AMMO_DEFUSER = 61
AMMO_WTESTER = 62
AMMO_BEACON = 63
AMMO_HEALTHSTATION = 64
AMMO_MOLOTOV = 65
AMMO_SMOKE = 66
AMMO_BINOCULARS = 67
AMMO_PUSH = 68
AMMO_STUN = 69
AMMO_CSE = 70
AMMO_DECOY = 71
AMMO_GLOCK = 72
local WeaponNames = nil
function GetWeaponClassNames()
if not WeaponNames then
local tbl = {}
for k,v in pairs(weapons.GetList()) do
if v and v.WeaponID then
tbl[v.WeaponID] = WEPS.GetClass(v)
end
end
for k,v in pairs(scripted_ents.GetList()) do
local id = v and (v.WeaponID or (v.t and v.t.WeaponID))
if id then
tbl[id] = WEPS.GetClass(v)
end
end
WeaponNames = tbl
end
return WeaponNames
end
-- reverse lookup from enum to SWEP table
function EnumToSWEP(ammo)
local e2w = GetWeaponClassNames() or {}
if e2w[ammo] then
return util.WeaponForClass(e2w[ammo])
else
return nil
end
end
function EnumToSWEPKey(ammo, key)
local swep = EnumToSWEP(ammo)
return swep and swep[key]
end
-- something the client can display
-- This used to be done with a big table of AMMO_ ids to names, now we just use
-- the weapon PrintNames. This means it is no longer usable from the server (not
-- used there anyway), and means capitalization is slightly less pretty.
function EnumToWep(ammo)
return EnumToSWEPKey(ammo, "PrintName")
end
-- something cheap to send over the network
function WepToEnum(wep)
if not IsValid(wep) then return end
return wep.WeaponID
end

View File

@@ -0,0 +1,246 @@
--[[
| 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/
--]]
GM.Name = "Trouble in Terrorist Town"
GM.Author = "Bad King Urgrain"
GM.Website = "ttt.badking.net"
GM.Version = "shrug emoji"
GM.Customized = false
-- Round status consts
ROUND_WAIT = 1
ROUND_PREP = 2
ROUND_ACTIVE = 3
ROUND_POST = 4
-- Player roles
ROLE_INNOCENT = 0
ROLE_TRAITOR = 1
ROLE_DETECTIVE = 2
ROLE_NONE = ROLE_INNOCENT
-- Game event log defs
EVENT_KILL = 1
EVENT_SPAWN = 2
EVENT_GAME = 3
EVENT_FINISH = 4
EVENT_SELECTED = 5
EVENT_BODYFOUND = 6
EVENT_C4PLANT = 7
EVENT_C4EXPLODE = 8
EVENT_CREDITFOUND = 9
EVENT_C4DISARM = 10
WIN_NONE = 1
WIN_TRAITOR = 2
WIN_INNOCENT = 3
WIN_TIMELIMIT = 4
-- Weapon categories, you can only carry one of each
WEAPON_NONE = 0
WEAPON_MELEE = 1
WEAPON_PISTOL = 2
WEAPON_HEAVY = 3
WEAPON_NADE = 4
WEAPON_CARRY = 5
WEAPON_EQUIP1 = 6
WEAPON_EQUIP2 = 7
WEAPON_ROLE = 8
WEAPON_EQUIP = WEAPON_EQUIP1
WEAPON_UNARMED = -1
-- Kill types discerned by last words
KILL_NORMAL = 0
KILL_SUICIDE = 1
KILL_FALL = 2
KILL_BURN = 3
-- Entity types a crowbar might open
OPEN_NO = 0
OPEN_DOOR = 1
OPEN_ROT = 2
OPEN_BUT = 3
OPEN_NOTOGGLE = 4 --movelinear
-- Mute types
MUTE_NONE = 0
MUTE_TERROR = 1
MUTE_ALL = 2
MUTE_SPEC = 1002
COLOR_WHITE = Color(255, 255, 255, 255)
COLOR_BLACK = Color(0, 0, 0, 255)
COLOR_GREEN = Color(0, 255, 0, 255)
COLOR_DGREEN = Color(0, 100, 0, 255)
COLOR_RED = Color(255, 0, 0, 255)
COLOR_YELLOW = Color(200, 200, 0, 255)
COLOR_LGRAY = Color(200, 200, 200, 255)
COLOR_BLUE = Color(0, 0, 255, 255)
COLOR_NAVY = Color(0, 0, 100, 255)
COLOR_PINK = Color(255,0,255, 255)
COLOR_ORANGE = Color(250, 100, 0, 255)
COLOR_OLIVE = Color(100, 100, 0, 255)
include("util.lua")
include("lang_shd.lua") -- uses some of util
include("equip_items_shd.lua")
function DetectiveMode() return GetGlobalBool("ttt_detective", false) end
function HasteMode() return GetGlobalBool("ttt_haste", false) end
-- Create teams
TEAM_TERROR = 1
TEAM_SPEC = TEAM_SPECTATOR
function GM:CreateTeams()
team.SetUp(TEAM_TERROR, "Terrorists", Color(0, 200, 0, 255), false)
team.SetUp(TEAM_SPEC, "Spectators", Color(200, 200, 0, 255), true)
-- Not that we use this, but feels good
team.SetSpawnPoint(TEAM_TERROR, "info_player_deathmatch")
team.SetSpawnPoint(TEAM_SPEC, "info_player_deathmatch")
end
-- Everyone's model
local ttt_playermodels = {
Model("models/player/phoenix.mdl"),
Model("models/player/arctic.mdl"),
Model("models/player/guerilla.mdl"),
Model("models/player/leet.mdl")
};
function GetRandomPlayerModel()
return table.Random(ttt_playermodels)
end
local ttt_playercolors = {
all = {
COLOR_WHITE,
COLOR_BLACK,
COLOR_GREEN,
COLOR_DGREEN,
COLOR_RED,
COLOR_YELLOW,
COLOR_LGRAY,
COLOR_BLUE,
COLOR_NAVY,
COLOR_PINK,
COLOR_OLIVE,
COLOR_ORANGE
},
serious = {
COLOR_WHITE,
COLOR_BLACK,
COLOR_NAVY,
COLOR_LGRAY,
COLOR_DGREEN,
COLOR_OLIVE
}
};
local playercolor_mode = CreateConVar("ttt_playercolor_mode", "1")
function GM:TTTPlayerColor(model)
local mode = playercolor_mode:GetInt()
if mode == 1 then
return table.Random(ttt_playercolors.serious)
elseif mode == 2 then
return table.Random(ttt_playercolors.all)
elseif mode == 3 then
-- Full randomness
return Color(math.random(0, 255), math.random(0, 255), math.random(0, 255))
end
-- No coloring
return COLOR_WHITE
end
-- Kill footsteps on player and client
function GM:PlayerFootstep(ply, pos, foot, sound, volume, rf)
if IsValid(ply) and (ply:Crouching() or ply:GetMaxSpeed() < 150 or ply:IsSpec()) then
-- do not play anything, just prevent normal sounds from playing
return true
end
end
-- Predicted move speed changes
function GM:Move(ply, mv)
if ply:IsTerror() then
local basemul = 1
local slowed = false
-- Slow down ironsighters
local wep = ply:GetActiveWeapon()
if IsValid(wep) and wep.GetIronsights and wep:GetIronsights() then
basemul = 120 / 220
slowed = true
end
local mul = hook.Call("TTTPlayerSpeedModifier", GAMEMODE, ply, slowed, mv) or 1
mul = basemul * mul
mv:SetMaxClientSpeed(mv:GetMaxClientSpeed() * mul)
mv:SetMaxSpeed(mv:GetMaxSpeed() * mul)
end
end
-- Weapons and items that come with TTT. Weapons that are not in this list will
-- get a little marker on their icon if they're buyable, showing they are custom
-- and unique to the server.
DefaultEquipment = {
-- traitor-buyable by default
[ROLE_TRAITOR] = {
"weapon_ttt_c4",
"weapon_ttt_flaregun",
"weapon_ttt_knife",
"weapon_ttt_phammer",
"weapon_ttt_push",
"weapon_ttt_radio",
"weapon_ttt_sipistol",
"weapon_ttt_teleport",
"weapon_ttt_decoy",
EQUIP_ARMOR,
EQUIP_RADAR,
EQUIP_DISGUISE
},
-- detective-buyable by default
[ROLE_DETECTIVE] = {
"weapon_ttt_binoculars",
"weapon_ttt_defuser",
"weapon_ttt_health_station",
"weapon_ttt_stungun",
"weapon_ttt_cse",
"weapon_ttt_teleport",
EQUIP_ARMOR,
EQUIP_RADAR
},
-- non-buyable
[ROLE_NONE] = {
"weapon_ttt_confgrenade",
"weapon_ttt_m16",
"weapon_ttt_smokegrenade",
"weapon_ttt_unarmed",
"weapon_ttt_wtester",
"weapon_tttbase",
"weapon_tttbasegrenade",
"weapon_zm_carry",
"weapon_zm_improvised",
"weapon_zm_mac10",
"weapon_zm_molotov",
"weapon_zm_pistol",
"weapon_zm_revolver",
"weapon_zm_rifle",
"weapon_zm_shotgun",
"weapon_zm_sledge",
"weapon_ttt_glock"
}
};

View File

@@ -0,0 +1,186 @@
--[[
| 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/
--]]
function GetTraitors()
local trs = {}
for k,v in player.Iterator() do
if v:GetTraitor() then table.insert(trs, v) end
end
return trs
end
function CountTraitors() return #GetTraitors() end
---- Role state communication
-- Send every player their role
local function SendPlayerRoles()
for k, v in player.Iterator() do
net.Start("TTT_Role")
net.WriteUInt(v:GetRole(), 2)
net.Send(v)
end
end
local function SendRoleListMessage(role, role_ids, ply_or_rf)
net.Start("TTT_RoleList")
net.WriteUInt(role, 2)
-- list contents
local num_ids = #role_ids
net.WriteUInt(num_ids, 8)
for i=1, num_ids do
net.WriteUInt(role_ids[i] - 1, 7)
end
if ply_or_rf then net.Send(ply_or_rf)
else net.Broadcast() end
end
local function SendRoleList(role, ply_or_rf, pred)
local role_ids = {}
for k, v in player.Iterator() do
if v:IsRole(role) then
if not pred or (pred and pred(v)) then
table.insert(role_ids, v:EntIndex())
end
end
end
SendRoleListMessage(role, role_ids, ply_or_rf)
end
-- Tell traitors about other traitors
function SendTraitorList(ply_or_rf, pred) SendRoleList(ROLE_TRAITOR, ply_or_rf, pred) end
function SendDetectiveList(ply_or_rf) SendRoleList(ROLE_DETECTIVE, ply_or_rf) end
-- this is purely to make sure last round's traitors/dets ALWAYS get reset
-- not happy with this, but it'll do for now
function SendInnocentList(ply_or_rf)
-- Send innocent and detectives a list of actual innocents + traitors, while
-- sending traitors only a list of actual innocents.
local inno_ids = {}
local traitor_ids = {}
for k, v in player.Iterator() do
if v:IsRole(ROLE_INNOCENT) then
table.insert(inno_ids, v:EntIndex())
elseif v:IsRole(ROLE_TRAITOR) then
table.insert(traitor_ids, v:EntIndex())
end
end
-- traitors get actual innocent, so they do not reset their traitor mates to
-- innocence
SendRoleListMessage(ROLE_INNOCENT, inno_ids, GetTraitorFilter())
-- detectives and innocents get an expanded version of the truth so that they
-- reset everyone who is not detective
table.Add(inno_ids, traitor_ids)
table.Shuffle(inno_ids)
SendRoleListMessage(ROLE_INNOCENT, inno_ids, GetInnocentFilter())
end
function SendConfirmedTraitors(ply_or_rf)
SendTraitorList(ply_or_rf, function(p) return p:GetNWBool("body_found") end)
end
function SendFullStateUpdate()
SendPlayerRoles()
SendInnocentList()
SendTraitorList(GetTraitorFilter())
SendDetectiveList()
-- not useful to sync confirmed traitors here
end
function SendRoleReset(ply_or_rf)
net.Start("TTT_RoleList")
net.WriteUInt(ROLE_INNOCENT, 2)
net.WriteUInt(player.GetCount(), 8)
for k, v in player.Iterator() do
net.WriteUInt(v:EntIndex() - 1, 7)
end
if ply_or_rf then net.Send(ply_or_rf)
else net.Broadcast() end
end
---- Console commands
local function request_rolelist(ply)
-- Client requested a state update. Note that the client can only use this
-- information after entities have been initialised (e.g. in InitPostEntity).
if GetRoundState() != ROUND_WAIT then
SendRoleReset(ply)
SendDetectiveList(ply)
if ply:IsTraitor() then
SendTraitorList(ply)
else
SendConfirmedTraitors(ply)
end
end
end
concommand.Add("_ttt_request_rolelist", request_rolelist)
local function force_terror(ply)
ply:SetRole(ROLE_INNOCENT)
ply:UnSpectate()
ply:SetTeam(TEAM_TERROR)
ply:StripAll()
ply:Spawn()
ply:PrintMessage(HUD_PRINTTALK, "You are now on the terrorist team.")
SendFullStateUpdate()
end
concommand.Add("ttt_force_terror", force_terror, nil, nil, FCVAR_CHEAT)
local function force_traitor(ply)
ply:SetRole(ROLE_TRAITOR)
SendFullStateUpdate()
end
concommand.Add("ttt_force_traitor", force_traitor, nil, nil, FCVAR_CHEAT)
local function force_detective(ply)
ply:SetRole(ROLE_DETECTIVE)
SendFullStateUpdate()
end
concommand.Add("ttt_force_detective", force_detective, nil, nil, FCVAR_CHEAT)
local function force_spectate(ply, cmd, arg)
if IsValid(ply) then
if #arg == 1 and tonumber(arg[1]) == 0 then
ply:SetForceSpec(false)
else
if not ply:IsSpec() then
ply:Kill()
end
GAMEMODE:PlayerSpawnAsSpectator(ply)
ply:SetTeam(TEAM_SPEC)
ply:SetForceSpec(true)
ply:Spawn()
ply:SetRagdollSpec(false) -- dying will enable this, we don't want it here
end
end
end
concommand.Add("ttt_spectate", force_spectate)
net.Receive("TTT_Spectate", function(l, pl)
force_spectate(pl, nil, { net.ReadBool() and 1 or 0 })
end)

View File

@@ -0,0 +1,379 @@
--[[
| 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/
--]]
-- Random stuff
if not util then return end
local math = math
local string = string
local table = table
local pairs = pairs
-- attempts to get the weapon used from a DamageInfo instance needed because the
-- GetAmmoType value is useless and inflictor isn't properly set (yet)
function util.WeaponFromDamage(dmg)
local inf = dmg:GetInflictor()
local wep = nil
if IsValid(inf) then
if inf:IsWeapon() or inf.Projectile then
wep = inf
elseif dmg:IsDamageType(DMG_DIRECT) or dmg:IsDamageType(DMG_CRUSH) then
-- DMG_DIRECT is the player burning, no weapon involved
-- DMG_CRUSH is physics or falling on someone
wep = nil
elseif inf:IsPlayer() then
wep = inf:GetActiveWeapon()
if not IsValid(wep) then
-- this may have been a dying shot, in which case we need a
-- workaround to find the weapon because it was dropped on death
wep = IsValid(inf.dying_wep) and inf.dying_wep or nil
end
end
end
return wep
end
-- Gets the table for a SWEP or a weapon-SENT (throwing knife), so not
-- equivalent to weapons.Get. Do not modify the table returned by this, consider
-- as read-only.
function util.WeaponForClass(cls)
local wep = weapons.GetStored(cls)
if not wep then
wep = scripted_ents.GetStored(cls)
if wep then
-- don't like to rely on this, but the alternative is
-- scripted_ents.Get which does a full table copy, so only do
-- that as last resort
wep = wep.t or scripted_ents.Get(cls)
end
end
return wep
end
function util.GetAlivePlayers()
local alive = {}
for k, p in player.Iterator() do
if IsValid(p) and p:Alive() and p:IsTerror() then
table.insert(alive, p)
end
end
return alive
end
function util.GetNextAlivePlayer(ply)
local alive = util.GetAlivePlayers()
if #alive < 1 then return nil end
local prev = nil
local choice = nil
if IsValid(ply) then
for k,p in ipairs(alive) do
if prev == ply then
choice = p
end
prev = p
end
end
if not IsValid(choice) then
choice = alive[1]
end
return choice
end
-- Uppercases the first character only
function string.Capitalize(str)
return string.upper(string.sub(str, 1, 1)) .. string.sub(str, 2)
end
util.Capitalize = string.Capitalize
-- Color unpacking
function clr(color) return color.r, color.g, color.b, color.a; end
if CLIENT then
-- Is screenpos on screen?
function IsOffScreen(scrpos)
return not scrpos.visible or scrpos.x < 0 or scrpos.y < 0 or scrpos.x > ScrW() or scrpos.y > ScrH()
end
end
function AccessorFuncDT(tbl, varname, name)
tbl["Get" .. name] = function(s) return s.dt and s.dt[varname] end
tbl["Set" .. name] = function(s, v) if s.dt then s.dt[varname] = v end end
end
function util.PaintDown(start, effname, ignore)
local btr = util.TraceLine({start=start, endpos=(start + Vector(0,0,-256)), filter=ignore, mask=MASK_SOLID})
util.Decal(effname, btr.HitPos+btr.HitNormal, btr.HitPos-btr.HitNormal)
end
local function DoBleed(ent)
if not IsValid(ent) or (ent:IsPlayer() and (not ent:Alive() or not ent:IsTerror())) then
return
end
local jitter = VectorRand() * 30
jitter.z = 20
util.PaintDown(ent:GetPos() + jitter, "Blood", ent)
end
-- Something hurt us, start bleeding for a bit depending on the amount
function util.StartBleeding(ent, dmg, t)
if dmg < 5 or not IsValid(ent) then
return
end
if ent:IsPlayer() and (not ent:Alive() or not ent:IsTerror()) then
return
end
local times = math.Clamp(math.Round(dmg / 15), 1, 20)
local delay = math.Clamp(t / times , 0.1, 2)
if ent:IsPlayer() then
times = times * 2
delay = delay / 2
end
timer.Create("bleed" .. ent:EntIndex(), delay, times,
function() DoBleed(ent) end)
end
function util.StopBleeding(ent)
timer.Remove("bleed" .. ent:EntIndex())
end
local zapsound = Sound("npc/assassin/ball_zap1.wav")
function util.EquipmentDestroyed(pos)
local effect = EffectData()
effect:SetOrigin(pos)
util.Effect("cball_explode", effect)
sound.Play(zapsound, pos)
end
-- Useful default behaviour for semi-modal DFrames
function util.BasicKeyHandler(pnl, kc)
-- passthrough F5
if kc == KEY_F5 then
RunConsoleCommand("jpeg")
else
pnl:Close()
end
end
function util.noop() end
function util.passthrough(x) return x end
-- Fisher-Yates shuffle
local rand = math.random
function table.Shuffle(t)
local n = #t
while n > 1 do
-- n is now the last pertinent index
local k = rand(n) -- 1 <= k <= n
-- Quick swap
t[n], t[k] = t[k], t[n]
n = n - 1
end
return t
end
-- Override with nil check
function table.HasValue(tbl, val)
if not tbl then return end
for k, v in pairs(tbl) do
if v == val then return true end
end
return false
end
-- Value equality for tables
function table.EqualValues(a, b)
if a == b then return true end
for k, v in pairs(a) do
if v != b[k] then
return false
end
end
return true
end
-- Basic table.HasValue pointer checks are insufficient when checking a table of
-- tables, so this uses table.EqualValues instead.
function table.HasTable(tbl, needle)
if not tbl then return end
for k, v in pairs(tbl) do
if v == needle then
return true
elseif table.EqualValues(v, needle) then
return true
end
end
return false
end
-- Returns copy of table with only specific keys copied
function table.CopyKeys(tbl, keys)
if not (tbl and keys) then return end
local out = {}
local val = nil
for _, k in pairs(keys) do
val = tbl[k]
if istable(val) then
out[k] = table.Copy(val)
else
out[k] = val
end
end
return out
end
local gsub = string.gsub
-- Simple string interpolation:
-- string.Interp("{killer} killed {victim}", {killer = "Bob", victim = "Joe"})
-- returns "Bob killed Joe"
-- No spaces or special chars in parameter name, just alphanumerics.
function string.Interp(str, tbl)
return gsub(str, '{(%w+)}', tbl)
end
-- Short helper for input.LookupBinding, returns capitalised key or a default
function Key(binding, default)
local b = input.LookupBinding(binding)
if not b then return default end
return string.upper(b)
end
local exp = math.exp
-- Equivalent to ExponentialDecay from Source's mathlib.
-- Convenient for falloff curves.
function math.ExponentialDecay(halflife, dt)
-- ln(0.5) = -0.69..
return exp((-0.69314718 / halflife) * dt)
end
function Dev(level, ...)
if cvars and cvars.Number("developer", 0) >= level then
Msg("[TTT dev]")
-- table.concat does not tostring, derp
local params = {...}
for i=1,#params do
Msg(" " .. tostring(params[i]))
end
Msg("\n")
end
end
function IsPlayer(ent)
return ent and ent:IsValid() and ent:IsPlayer()
end
function IsRagdoll(ent)
return ent and ent:IsValid() and ent:GetClass() == "prop_ragdoll"
end
local band = bit.band
function util.BitSet(val, bit)
return band(val, bit) == bit
end
if CLIENT then
local healthcolors = {
healthy = Color(0, 255, 0, 255),
hurt = Color(170, 230, 10, 255),
wounded = Color(230, 215, 10, 255),
badwound= Color(255, 140, 0, 255),
death = Color(255, 0, 0, 255)
};
function util.HealthToString(health, maxhealth)
maxhealth = maxhealth or 100
if health > maxhealth * 0.9 then
return "hp_healthy", healthcolors.healthy
elseif health > maxhealth * 0.7 then
return "hp_hurt", healthcolors.hurt
elseif health > maxhealth * 0.45 then
return "hp_wounded", healthcolors.wounded
elseif health > maxhealth * 0.2 then
return "hp_badwnd", healthcolors.badwound
else
return "hp_death", healthcolors.death
end
end
local karmacolors = {
max = Color(255, 255, 255, 255),
high = Color(255, 240, 135, 255),
med = Color(245, 220, 60, 255),
low = Color(255, 180, 0, 255),
min = Color(255, 130, 0, 255),
};
function util.KarmaToString(karma)
local maxkarma = GetGlobalInt("ttt_karma_max", 1000)
if karma > maxkarma * 0.89 then
return "karma_max", karmacolors.max
elseif karma > maxkarma * 0.8 then
return "karma_high", karmacolors.high
elseif karma > maxkarma * 0.65 then
return "karma_med", karmacolors.med
elseif karma > maxkarma * 0.5 then
return "karma_low", karmacolors.low
else
return "karma_min", karmacolors.min
end
end
function util.IncludeClientFile(file)
include(file)
end
else
function util.IncludeClientFile(file)
AddCSLuaFile(file)
end
end
-- Like string.FormatTime but simpler (and working), always a string, no hour
-- support
function util.SimpleTime(seconds, fmt)
if not seconds then seconds = 0 end
local ms = (seconds - math.floor(seconds)) * 100
seconds = math.floor(seconds)
local s = seconds % 60
seconds = (seconds - s) / 60
local m = seconds % 60
return string.format(fmt, m, s, ms)
end

View File

@@ -0,0 +1,32 @@
--[[
| 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/
--]]
-- Removed in GM13, still need it
local PANEL = {}
AccessorFunc( PANEL, "m_bBorder", "Border" )
AccessorFunc( PANEL, "m_Color", "Color" )
function PANEL:Init()
self:SetBorder( true )
self:SetColor( Color( 0, 255, 0, 255 ) )
end
function PANEL:Paint()
surface.SetDrawColor( self.m_Color.r, self.m_Color.g, self.m_Color.b, 255 )
self:DrawFilledRect()
end
function PANEL:PaintOver()
if not self.m_bBorder then return end
surface.SetDrawColor( 0, 0, 0, 255 )
self:DrawOutlinedRect()
end
derma.DefineControl( "ColoredBox", "", PANEL, "DPanel" )

View File

@@ -0,0 +1,101 @@
--[[
| 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/
--]]
-- Version of DProgressBar I can mess around with
local PANEL = {}
AccessorFunc( PANEL, "m_iMin", "Min" )
AccessorFunc( PANEL, "m_iMax", "Max" )
AccessorFunc( PANEL, "m_iValue", "Value" )
AccessorFunc( PANEL, "m_Color", "Color" )
function PANEL:Init()
self.Label = vgui.Create( "DLabel", self )
self.Label:SetFont( "DefaultSmall" )
self.Label:SetColor( Color( 0, 0, 0 ) )
self:SetMin( 0 )
self:SetMax( 1000 )
self:SetValue( 253 )
self:SetColor( Color( 50, 205, 255, 255 ) )
end
function PANEL:LabelAsPercentage()
self.m_bLabelAsPercentage = true
self:UpdateText()
end
function PANEL:SetMin( i )
self.m_iMin = i
self:UpdateText()
end
function PANEL:SetMax( i )
self.m_iMax = i
self:UpdateText()
end
function PANEL:SetValue( i )
self.m_iValue = i
self:UpdateText()
end
function PANEL:UpdateText()
if ( !self.m_iMax ) then return end
if ( !self.m_iMin ) then return end
if ( !self.m_iValue ) then return end
local fDelta = 0;
if ( self.m_iMax-self.m_iMin != 0 ) then
fDelta = ( self.m_iValue - self.m_iMin ) / (self.m_iMax-self.m_iMin)
end
if ( self.m_bLabelAsPercentage ) then
self.Label:SetText( Format( "%.2f%%", fDelta * 100 ) )
return
end
if ( self.m_iMin == 0 ) then
self.Label:SetText( Format( "%i / %i", self.m_iValue, self.m_iMax ) )
else
end
end
function PANEL:PerformLayout()
self.Label:SizeToContents()
self.Label:AlignRight( 5 )
self.Label:CenterVertical()
end
function PANEL:Paint()
local fDelta = 0;
if ( self.m_iMax-self.m_iMin != 0 ) then
fDelta = ( self.m_iValue - self.m_iMin ) / (self.m_iMax-self.m_iMin)
end
local Width = self:GetWide()
surface.SetDrawColor( 0, 0, 0, 170 )
surface.DrawRect( 0, 0, Width, self:GetTall() )
surface.SetDrawColor( self.m_Color.r, self.m_Color.g, self.m_Color.b, self.m_Color.a * 0.5 )
surface.DrawRect( 2, 2, Width - 4, self:GetTall() - 4 )
surface.SetDrawColor( self.m_Color.r, self.m_Color.g, self.m_Color.b, self.m_Color.a )
surface.DrawRect( 2, 2, Width * fDelta - 4, self:GetTall() - 4 )
end
vgui.Register( "TTTProgressBar", PANEL, "DPanel" )

View File

@@ -0,0 +1,273 @@
--[[
| 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/
--]]
---- Player info panel, based on sandbox scoreboard's infocard
local vgui = vgui
local GetTranslation = LANG.GetTranslation
local GetPTranslation = LANG.GetParamTranslation
--- Base stuff
local PANEL = {}
function PANEL:Init()
self.Player = nil
--self:SetMouseInputEnabled(false)
end
function PANEL:SetPlayer(ply)
self.Player = ply
self:UpdatePlayerData()
end
function PANEL:UpdatePlayerData()
-- override me
end
function PANEL:Paint()
return true
end
vgui.Register("TTTScorePlayerInfoBase", PANEL, "Panel")
--- Dead player search results
local PANEL = {}
function PANEL:Init()
self.List = vgui.Create("DPanelSelect", self)
self.List:EnableHorizontal(true)
if self.List.VBar then
self.List.VBar:Remove()
self.List.VBar = nil
end
self.Scroll = vgui.Create("DHorizontalScroller", self.List)
self.Help = vgui.Create("DLabel", self)
self.Help:SetText(GetTranslation("sb_info_help"))
self.Help:SetFont("treb_small")
self.Help:SetVisible(false)
end
function PANEL:PerformLayout()
self:SetSize(self:GetWide(), 75)
self.List:SetPos(0, 0)
self.List:SetSize(self:GetWide(), 70)
self.List:SetSpacing(1)
self.List:SetPadding(2)
self.List:SetPaintBackground(false)
self.Scroll:StretchToParent(3,3,3,3)
self.Help:SizeToContents()
self.Help:SetPos(5, 5)
end
function PANEL:UpdatePlayerData()
if not IsValid(self.Player) then return end
if not self.Player.search_result then
self.Help:SetVisible(true)
return
end
self.Help:SetVisible(false)
if self.Search == self.Player.search_result then return end
self.List:Clear(true)
self.Scroll.Panels = {}
local search_raw = self.Player.search_result
-- standard search result preproc
local search = PreprocSearch(search_raw)
-- wipe some stuff we don't need, like id
search.nick = nil
-- Create table of SimpleIcons, each standing for a piece of search
-- information.
for t, info in SortedPairsByMemberValue(search, "p") do
local ic = nil
-- Certain items need a special icon conveying additional information
if t == "lastid" then
ic = vgui.Create("SimpleIconAvatar", self.List)
ic:SetPlayer(info.ply)
ic:SetAvatarSize(24)
elseif t == "dtime" then
ic = vgui.Create("SimpleIconLabelled", self.List)
ic:SetIconText(info.text_icon)
else
ic = vgui.Create("SimpleIcon", self.List)
end
ic:SetIconSize(64)
ic:SetIcon(info.img)
ic:SetTooltip(info.text)
ic.info_type = t
self.List:AddPanel(ic)
self.Scroll:AddPanel(ic)
end
self.Search = search_raw
self.List:InvalidateLayout()
self.Scroll:InvalidateLayout()
self:PerformLayout()
end
vgui.Register("TTTScorePlayerInfoSearch", PANEL, "TTTScorePlayerInfoBase")
--- Living player, tags etc
local tags = {
{txt="sb_tag_friend", color=COLOR_GREEN},
{txt="sb_tag_susp", color=COLOR_YELLOW},
{txt="sb_tag_avoid", color=Color(255, 150, 0, 255)},
{txt="sb_tag_kill", color=COLOR_RED},
{txt="sb_tag_miss", color=Color(130, 190, 130, 255)}
};
local PANEL = {}
function PANEL:Init()
self.TagButtons = {}
for k, tag in ipairs(tags) do
self.TagButtons[k] = vgui.Create("TagButton", self)
self.TagButtons[k]:SetupTag(tag)
end
--self:SetMouseInputEnabled(false)
end
function PANEL:SetPlayer(ply)
self.Player = ply
for _, btn in pairs(self.TagButtons) do
btn:SetPlayer(ply)
end
self:InvalidateLayout()
end
function PANEL:ApplySchemeSettings()
end
function PANEL:UpdateTag()
self:GetParent():UpdatePlayerData()
self:GetParent():SetOpen(false)
end
function PANEL:PerformLayout()
self:SetSize(self:GetWide(), 30)
local margin = 10
local x = 250 --29
local y = 0
for k, btn in ipairs(self.TagButtons) do
btn:SetPos(x, y)
btn:SetCursor("hand")
btn:SizeToContents()
btn:PerformLayout()
x = x + btn:GetWide() + margin
end
end
vgui.Register("TTTScorePlayerInfoTags", PANEL, "TTTScorePlayerInfoBase")
--- Tag button
local PANEL = {}
function PANEL:Init()
self.Player = nil
self:SetText("")
self:SetMouseInputEnabled(true)
self:SetKeyboardInputEnabled(false)
self:SetTall(20)
self:SetPaintBackgroundEnabled(false)
self:SetPaintBorderEnabled(false)
self:SetPaintBackground(false)
self:SetDrawBorder(false)
self:SetFont("treb_small")
self:SetTextColor(self.Tag and self.Tag.color or COLOR_WHITE)
end
function PANEL:SetPlayer(ply)
self.Player = ply
end
function PANEL:SetupTag(tag)
self.Tag = tag
self.Color = tag.color
self.Text = tag.txt
self:SetTextColor(self.Tag and self.Tag.color or COLOR_WHITE)
end
function PANEL:PerformLayout()
self:SetText(self.Tag and GetTranslation(self.Tag.txt) or "")
self:SizeToContents()
self:SetContentAlignment(5)
self:SetSize(self:GetWide() + 10, self:GetTall() + 3)
end
function PANEL:DoRightClick()
if IsValid(self.Player) then
self.Player.sb_tag = nil
self:GetParent():UpdateTag()
end
end
function PANEL:DoClick()
if IsValid(self.Player) then
if self.Player.sb_tag == self.Tag then
self.Player.sb_tag = nil
else
self.Player.sb_tag = self.Tag
end
self:GetParent():UpdateTag()
end
end
local select_color = Color(255, 200, 0, 255)
function PANEL:PaintOver()
if self.Player and self.Player.sb_tag == self.Tag then
surface.SetDrawColor(255,200,0,255)
surface.DrawOutlinedRect(0, 0, self:GetWide(), self:GetTall())
end
end
vgui.Register("TagButton", PANEL, "DButton")

View File

@@ -0,0 +1,486 @@
--[[
| 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/
--]]
---- VGUI panel version of the scoreboard, based on TEAM GARRY's sandbox mode
---- scoreboard.
local surface = surface
local draw = draw
local math = math
local string = string
local vgui = vgui
local GetTranslation = LANG.GetTranslation
local GetPTranslation = LANG.GetParamTranslation
include("sb_team.lua")
surface.CreateFont("cool_small", {font = "coolvetica",
size = 20,
weight = 400})
surface.CreateFont("cool_large", {font = "coolvetica",
size = 24,
weight = 400})
surface.CreateFont("treb_small", {font = "Trebuchet18",
size = 14,
weight = 700})
CreateClientConVar("ttt_scoreboard_sorting", "name", true, false, "name | role | karma | score | deaths | ping")
CreateClientConVar("ttt_scoreboard_ascending", "1", true, false, "Should scoreboard ordering be in ascending order")
local logo = surface.GetTextureID("vgui/ttt/score_logo")
local PANEL = {}
local max = math.max
local floor = math.floor
local function UntilMapChange()
local rounds_left = max(0, GetGlobalInt("ttt_rounds_left", 6))
local time_left = floor(max(0, ((GetGlobalInt("ttt_time_limit_minutes") or 60) * 60) - CurTime()))
local h = floor(time_left / 3600)
time_left = time_left - floor(h * 3600)
local m = floor(time_left / 60)
time_left = time_left - floor(m * 60)
local s = floor(time_left)
return rounds_left, string.format("%02i:%02i:%02i", h, m, s)
end
GROUP_TERROR = 1
GROUP_NOTFOUND = 2
GROUP_FOUND = 3
GROUP_SPEC = 4
GROUP_COUNT = 4
function AddScoreGroup(name) -- Utility function to register a score group
if _G["GROUP_"..name] then error("Group of name '"..name.."' already exists!") return end
GROUP_COUNT = GROUP_COUNT + 1
_G["GROUP_"..name] = GROUP_COUNT
end
function ScoreGroup(p)
if not IsValid(p) then return -1 end -- will not match any group panel
local group = hook.Call( "TTTScoreGroup", nil, p )
if group then -- If that hook gave us a group, use it
return group
end
if DetectiveMode() then
if p:IsSpec() and (not p:Alive()) then
if p:GetNWBool("body_found", false) then
return GROUP_FOUND
else
local client = LocalPlayer()
-- To terrorists, missing players show as alive
if client:IsSpec() or
client:IsActiveTraitor() or
((GAMEMODE.round_state != ROUND_ACTIVE) and client:IsTerror()) then
return GROUP_NOTFOUND
else
return GROUP_TERROR
end
end
end
end
return p:IsTerror() and GROUP_TERROR or GROUP_SPEC
end
-- Comparison functions used to sort scoreboard
sboard_sort = {
name = function (plya, plyb)
-- Automatically sorts by name if this returns 0
return 0
end,
ping = function (plya, plyb)
return plya:Ping() - plyb:Ping()
end,
deaths = function (plya, plyb)
return plya:Deaths() - plyb:Deaths()
end,
score = function (plya, plyb)
return plya:Frags() - plyb:Frags()
end,
role = function (plya, plyb)
local comp = (plya:GetRole() or 0) - (plyb:GetRole() or 0)
-- Reverse on purpose;
-- otherwise the default ascending order puts boring innocents first
comp = 0 - comp
return comp
end,
karma = function (plya, plyb)
return (plya:GetBaseKarma() or 0) - (plyb:GetBaseKarma() or 0)
end
}
----- PANEL START
function PANEL:Init()
self.hostdesc = vgui.Create("DLabel", self)
self.hostdesc:SetText(GetTranslation("sb_playing"))
self.hostdesc:SetContentAlignment(9)
self.hostname = vgui.Create( "DLabel", self )
self.hostname:SetText( GetHostName() )
self.hostname:SetContentAlignment(6)
self.mapchange = vgui.Create("DLabel", self)
self.mapchange:SetText("Map changes in 00 rounds or in 00:00:00")
self.mapchange:SetContentAlignment(9)
self.mapchange.Think = function (sf)
local r, t = UntilMapChange()
sf:SetText(GetPTranslation("sb_mapchange",
{num = r, time = t}))
sf:SizeToContents()
end
self.ply_frame = vgui.Create( "TTTPlayerFrame", self )
self.ply_groups = {}
local t = vgui.Create("TTTScoreGroup", self.ply_frame:GetCanvas())
t:SetGroupInfo(GetTranslation("terrorists"), Color(0,200,0,100), GROUP_TERROR)
self.ply_groups[GROUP_TERROR] = t
t = vgui.Create("TTTScoreGroup", self.ply_frame:GetCanvas())
t:SetGroupInfo(GetTranslation("spectators"), Color(200, 200, 0, 100), GROUP_SPEC)
self.ply_groups[GROUP_SPEC] = t
if DetectiveMode() then
t = vgui.Create("TTTScoreGroup", self.ply_frame:GetCanvas())
t:SetGroupInfo(GetTranslation("sb_mia"), Color(130, 190, 130, 100), GROUP_NOTFOUND)
self.ply_groups[GROUP_NOTFOUND] = t
t = vgui.Create("TTTScoreGroup", self.ply_frame:GetCanvas())
t:SetGroupInfo(GetTranslation("sb_confirmed"), Color(130, 170, 10, 100), GROUP_FOUND)
self.ply_groups[GROUP_FOUND] = t
end
hook.Call( "TTTScoreGroups", nil, self.ply_frame:GetCanvas(), self.ply_groups )
-- the various score column headers
self.cols = {}
self:AddColumn( GetTranslation("sb_ping"), nil, nil, "ping" )
self:AddColumn( GetTranslation("sb_deaths"), nil, nil, "deaths" )
self:AddColumn( GetTranslation("sb_score"), nil, nil, "score" )
if KARMA.IsEnabled() then
self:AddColumn( GetTranslation("sb_karma"), nil, nil, "karma" )
end
self.sort_headers = {}
-- Reuse some translations
-- Columns spaced out a bit to allow for more room for translations
self:AddFakeColumn( GetTranslation("sb_sortby"), nil, 70, nil ) -- "Sort by:"
self:AddFakeColumn( GetTranslation("equip_spec_name"), nil, 70, "name" )
self:AddFakeColumn( GetTranslation("col_role"), nil, 70, "role" )
-- Let hooks add their column headers (via AddColumn() or AddFakeColumn())
hook.Call( "TTTScoreboardColumns", nil, self )
self:UpdateScoreboard()
self:StartUpdateTimer()
end
local function sort_header_handler(self_, lbl)
return function()
surface.PlaySound("ui/buttonclick.wav")
local sorting = GetConVar("ttt_scoreboard_sorting")
local ascending = GetConVar("ttt_scoreboard_ascending")
if lbl.HeadingIdentifier == sorting:GetString() then
ascending:SetBool(not ascending:GetBool())
else
sorting:SetString( lbl.HeadingIdentifier )
ascending:SetBool(true)
end
for _, scoregroup in pairs(self_.ply_groups) do
scoregroup:UpdateSortCache()
scoregroup:InvalidateLayout()
end
self_:ApplySchemeSettings()
end
end
-- For headings only the label parameter is relevant, second param is included for
-- parity with sb_row
local function column_label_work(self_, table_to_add, label, width, sort_identifier, sort_func )
local lbl = vgui.Create( "DLabel", self_ )
lbl:SetText( label )
local can_sort = false
lbl.IsHeading = true
lbl.Width = width or 50 -- Retain compatibility with existing code
if sort_identifier != nil then
can_sort = true
-- If we have an identifier and an existing sort function then it was a built-in
-- Otherwise...
if _G.sboard_sort[sort_identifier] == nil then
if sort_func == nil then
ErrorNoHalt( "Sort ID provided without a sorting function, Label = ", label, " ; ID = ", sort_identifier )
can_sort = false
else
_G.sboard_sort[sort_identifier] = sort_func
end
end
end
if can_sort then
lbl:SetMouseInputEnabled(true)
lbl:SetCursor("hand")
lbl.HeadingIdentifier = sort_identifier
lbl.DoClick = sort_header_handler(self_, lbl)
end
table.insert( table_to_add, lbl )
return lbl
end
function PANEL:AddColumn( label, _, width, sort_id, sort_func )
return column_label_work( self, self.cols, label, width, sort_id, sort_func )
end
-- Adds just column headers without player-specific data
-- Identical to PANEL:AddColumn except it adds to the sort_headers table instead
function PANEL:AddFakeColumn( label, _, width, sort_id, sort_func )
return column_label_work( self, self.sort_headers, label, width, sort_id, sort_func )
end
function PANEL:StartUpdateTimer()
if not timer.Exists("TTTScoreboardUpdater") then
timer.Create( "TTTScoreboardUpdater", 0.3, 0,
function()
local pnl = GAMEMODE:GetScoreboardPanel()
if IsValid(pnl) then
pnl:UpdateScoreboard()
end
end)
end
end
local colors = {
bg = Color(30,30,30, 235),
bar = Color(220,180,0,255)
};
local y_logo_off = 72
function PANEL:Paint()
-- Logo sticks out, so always offset bg
draw.RoundedBox( 8, 0, y_logo_off, self:GetWide(), self:GetTall() - y_logo_off, colors.bg)
-- Server name is outlined by orange/gold area
draw.RoundedBox( 8, 0, y_logo_off + 25, self:GetWide(), 32, colors.bar)
-- TTT Logo
surface.SetTexture( logo )
surface.SetDrawColor( 255, 255, 255, 255 )
surface.DrawTexturedRect( 5, 0, 256, 256 )
end
function PANEL:PerformLayout()
-- position groups and find their total size
local gy = 0
-- can't just use pairs (undefined ordering) or ipairs (group 2 and 3 might not exist)
for i=1, GROUP_COUNT do
local group = self.ply_groups[i]
if IsValid(group) then
if group:HasRows() then
group:SetVisible(true)
group:SetPos(0, gy)
group:SetSize(self.ply_frame:GetWide(), group:GetTall())
group:InvalidateLayout()
gy = gy + group:GetTall() + 5
else
group:SetVisible(false)
end
end
end
self.ply_frame:GetCanvas():SetSize(self.ply_frame:GetCanvas():GetWide(), gy)
local h = y_logo_off + 110 + self.ply_frame:GetCanvas():GetTall()
-- if we will have to clamp our height, enable the mouse so player can scroll
local scrolling = h > ScrH() * 0.95
-- gui.EnableScreenClicker(scrolling)
self.ply_frame:SetScroll(scrolling)
h = math.Clamp(h, 110 + y_logo_off, ScrH() * 0.95)
local w = math.max(ScrW() * 0.6, 640)
self:SetSize(w, h)
self:SetPos( (ScrW() - w) / 2, math.min(72, (ScrH() - h) / 4))
self.ply_frame:SetPos(8, y_logo_off + 109)
self.ply_frame:SetSize(self:GetWide() - 16, self:GetTall() - 109 - y_logo_off - 5)
-- server stuff
self.hostdesc:SizeToContents()
self.hostdesc:SetPos(w - self.hostdesc:GetWide() - 8, y_logo_off + 5)
local hw = w - 180 - 8
self.hostname:SetSize(hw, 32)
self.hostname:SetPos(w - self.hostname:GetWide() - 8, y_logo_off + 27)
surface.SetFont("cool_large")
local hname = self.hostname:GetValue()
local tw, _ = surface.GetTextSize(hname)
while tw > hw do
hname = string.sub(hname, 1, -6) .. "..."
tw, th = surface.GetTextSize(hname)
end
self.hostname:SetText(hname)
self.mapchange:SizeToContents()
self.mapchange:SetPos(w - self.mapchange:GetWide() - 8, y_logo_off + 60)
-- score columns
local cy = y_logo_off + 90
local cx = w - 8 -(scrolling and 16 or 0)
for k,v in ipairs(self.cols) do
v:SizeToContents()
cx = cx - v.Width
v:SetPos(cx - v:GetWide()/2, cy)
end
-- sort headers
-- reuse cy
-- cx = logo width + buffer space
local cx = 256 + 8
for k,v in ipairs(self.sort_headers) do
v:SizeToContents()
cx = cx + v.Width
v:SetPos(cx - v:GetWide()/2, cy)
end
end
function PANEL:ApplySchemeSettings()
self.hostdesc:SetFont("cool_small")
self.hostname:SetFont("cool_large")
self.mapchange:SetFont("treb_small")
self.hostdesc:SetTextColor(COLOR_WHITE)
self.hostname:SetTextColor(COLOR_BLACK)
self.mapchange:SetTextColor(COLOR_WHITE)
local sorting = GetConVar("ttt_scoreboard_sorting"):GetString()
local highlight_color = Color(175, 175, 175, 255)
local default_color = COLOR_WHITE
for k,v in pairs(self.cols) do
v:SetFont("treb_small")
if sorting == v.HeadingIdentifier then
v:SetTextColor(highlight_color)
else
v:SetTextColor(default_color)
end
end
for k,v in pairs(self.sort_headers) do
v:SetFont("treb_small")
if sorting == v.HeadingIdentifier then
v:SetTextColor(highlight_color)
else
v:SetTextColor(default_color)
end
end
end
function PANEL:UpdateScoreboard( force )
if not force and not self:IsVisible() then return end
local layout = false
-- Put players where they belong. Groups will dump them as soon as they don't
-- anymore.
for k, p in player.Iterator() do
if IsValid(p) then
local group = ScoreGroup(p)
if self.ply_groups[group] and not self.ply_groups[group]:HasPlayerRow(p) then
self.ply_groups[group]:AddPlayerRow(p)
layout = true
end
end
end
for k, group in pairs(self.ply_groups) do
if IsValid(group) then
group:SetVisible( group:HasRows() )
group:UpdatePlayerData()
end
end
if layout then
self:PerformLayout()
else
self:InvalidateLayout()
end
end
vgui.Register( "TTTScoreboard", PANEL, "Panel" )
---- PlayerFrame is defined in sandbox and is basically a little scrolling
---- hack. Just putting it here (slightly modified) because it's tiny.
local PANEL = {}
function PANEL:Init()
self.pnlCanvas = vgui.Create( "Panel", self )
self.YOffset = 0
self.scroll = vgui.Create("DVScrollBar", self)
end
function PANEL:GetCanvas() return self.pnlCanvas end
function PANEL:OnMouseWheeled( dlta )
self.scroll:AddScroll(dlta * -2)
self:InvalidateLayout()
end
function PANEL:SetScroll(st)
self.scroll:SetEnabled(st)
end
function PANEL:PerformLayout()
self.pnlCanvas:SetVisible(self:IsVisible())
-- scrollbar
self.scroll:SetPos(self:GetWide() - 16, 0)
self.scroll:SetSize(16, self:GetTall())
local was_on = self.scroll.Enabled
self.scroll:SetUp(self:GetTall(), self.pnlCanvas:GetTall())
self.scroll:SetEnabled(was_on) -- setup mangles enabled state
self.YOffset = self.scroll:GetOffset()
self.pnlCanvas:SetPos( 0, self.YOffset )
self.pnlCanvas:SetSize( self:GetWide() - (self.scroll.Enabled and 16 or 0), self.pnlCanvas:GetTall() )
end
vgui.Register( "TTTPlayerFrame", PANEL, "Panel" )

View File

@@ -0,0 +1,426 @@
--[[
| 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/
--]]
---- Scoreboard player score row, based on sandbox version
include("sb_info.lua")
local GetTranslation = LANG.GetTranslation
local GetPTranslation = LANG.GetParamTranslation
SB_ROW_HEIGHT = 24 --16
local PANEL = {}
function PANEL:Init()
-- cannot create info card until player state is known
self.info = nil
self.open = false
self.cols = {}
self:AddColumn( GetTranslation("sb_ping"), function(ply) return ply:Ping() end )
self:AddColumn( GetTranslation("sb_deaths"), function(ply) return ply:Deaths() end )
self:AddColumn( GetTranslation("sb_score"), function(ply) return ply:Frags() end )
if KARMA.IsEnabled() then
self:AddColumn( GetTranslation("sb_karma"), function(ply) return math.Round(ply:GetBaseKarma()) end )
end
-- Let hooks add their custom columns
hook.Call("TTTScoreboardColumns", nil, self)
for _, c in ipairs(self.cols) do
c:SetMouseInputEnabled(false)
end
self.tag = vgui.Create("DLabel", self)
self.tag:SetText("")
self.tag:SetMouseInputEnabled(false)
self.sresult = vgui.Create("DImage", self)
self.sresult:SetSize(16,16)
self.sresult:SetMouseInputEnabled(false)
self.avatar = vgui.Create( "AvatarImage", self )
self.avatar:SetSize(SB_ROW_HEIGHT, SB_ROW_HEIGHT)
self.avatar:SetMouseInputEnabled(false)
self.nick = vgui.Create("DLabel", self)
self.nick:SetMouseInputEnabled(false)
self.voice = vgui.Create("DImageButton", self)
self.voice:SetSize(16,16)
self:SetCursor( "hand" )
end
function PANEL:AddColumn( label, func, width, _, _ )
local lbl = vgui.Create( "DLabel", self )
lbl.GetPlayerText = func
lbl.IsHeading = false
lbl.Width = width or 50 -- Retain compatibility with existing code
table.insert( self.cols, lbl )
return lbl
end
-- Mirror sb_main, of which it and this file both call using the
-- TTTScoreboardColumns hook, but it is useless in this file
-- Exists only so the hook wont return an error if it tries to
-- use the AddFakeColumn function of `sb_main`, which would
-- cause this file to raise a `function not found` error or others
function PANEL:AddFakeColumn() end
local namecolor = {
default = COLOR_WHITE,
admin = Color(220, 180, 0, 255),
dev = Color(100, 240, 105, 255)
}
local rolecolor = {
default = Color(0, 0, 0, 0),
traitor = Color(255, 0, 0, 30),
detective = Color(0, 0, 255, 30)
}
function GM:TTTScoreboardColorForPlayer(ply)
if not IsValid(ply) then return namecolor.default end
if ply:SteamID() == "STEAM_0:0:1963640" then
return namecolor.dev
elseif ply:IsAdmin() and GetGlobalBool("ttt_highlight_admins", true) then
return namecolor.admin
end
return namecolor.default
end
function GM:TTTScoreboardRowColorForPlayer(ply)
if not IsValid(ply) then return rolecolor.default end
if ply:IsTraitor() then
return rolecolor.traitor
elseif ply:IsDetective() then
return rolecolor.detective
end
return rolecolor.default
end
local function ColorForPlayer(ply)
if IsValid(ply) then
local c = hook.Call("TTTScoreboardColorForPlayer", GAMEMODE, ply)
-- verify that we got a proper color
if c and istable(c) and c.r and c.b and c.g and c.a then
return c
else
ErrorNoHalt("TTTScoreboardColorForPlayer hook returned something that isn't a color!\n")
end
end
return namecolor.default
end
function PANEL:Paint(width, height)
if not IsValid(self.Player) then return end
-- if ( self.Player:GetFriendStatus() == "friend" ) then
-- color = Color( 236, 181, 113, 255 )
-- end
local ply = self.Player
local c = hook.Call("TTTScoreboardRowColorForPlayer", GAMEMODE, ply)
surface.SetDrawColor(c)
surface.DrawRect(0, 0, width, SB_ROW_HEIGHT)
if ply == LocalPlayer() then
surface.SetDrawColor( 200, 200, 200, math.Clamp(math.sin(RealTime() * 2) * 50, 0, 100))
surface.DrawRect(0, 0, width, SB_ROW_HEIGHT )
end
return true
end
function PANEL:SetPlayer(ply)
self.Player = ply
self.avatar:SetPlayer(ply)
if not self.info then
local g = ScoreGroup(ply)
if g == GROUP_TERROR and ply != LocalPlayer() then
self.info = vgui.Create("TTTScorePlayerInfoTags", self)
self.info:SetPlayer(ply)
self:InvalidateLayout()
elseif g == GROUP_FOUND or g == GROUP_NOTFOUND then
self.info = vgui.Create("TTTScorePlayerInfoSearch", self)
self.info:SetPlayer(ply)
self:InvalidateLayout()
end
else
self.info:SetPlayer(ply)
self:InvalidateLayout()
end
self.voice.DoClick = function()
if IsValid(ply) and ply != LocalPlayer() then
ply:SetMuted(not ply:IsMuted())
end
end
self.voice.DoRightClick = function()
if IsValid(ply) and ply != LocalPlayer() then
self:ShowMicVolumeSlider()
end
end
self:UpdatePlayerData()
end
function PANEL:GetPlayer() return self.Player end
function PANEL:UpdatePlayerData()
if not IsValid(self.Player) then return end
local ply = self.Player
for i=1,#self.cols do
-- Set text from function, passing the label along so stuff like text
-- color can be changed
self.cols[i]:SetText( self.cols[i].GetPlayerText(ply, self.cols[i]) )
end
self.nick:SetText(ply:Nick())
self.nick:SizeToContents()
self.nick:SetTextColor(ColorForPlayer(ply))
local ptag = ply.sb_tag
if ScoreGroup(ply) != GROUP_TERROR then
ptag = nil
end
self.tag:SetText(ptag and GetTranslation(ptag.txt) or "")
self.tag:SetTextColor(ptag and ptag.color or COLOR_WHITE)
self.sresult:SetVisible(ply.search_result != nil)
-- more blue if a detective searched them
if ply.search_result and (LocalPlayer():IsDetective() or (not ply.search_result.show)) then
self.sresult:SetImageColor(Color(200, 200, 255))
end
-- cols are likely to need re-centering
self:LayoutColumns()
if self.info then
self.info:UpdatePlayerData()
end
if self.Player != LocalPlayer() then
local muted = self.Player:IsMuted()
self.voice:SetImage(muted and "icon16/sound_mute.png" or "icon16/sound.png")
else
self.voice:Hide()
end
end
function PANEL:ApplySchemeSettings()
for k,v in pairs(self.cols) do
v:SetFont("treb_small")
v:SetTextColor(COLOR_WHITE)
end
self.nick:SetFont("treb_small")
self.nick:SetTextColor(ColorForPlayer(self.Player))
local ptag = self.Player and self.Player.sb_tag
self.tag:SetTextColor(ptag and ptag.color or COLOR_WHITE)
self.tag:SetFont("treb_small")
self.sresult:SetImage("icon16/magnifier.png")
self.sresult:SetImageColor(Color(170, 170, 170, 150))
end
function PANEL:LayoutColumns()
local cx = self:GetWide()
for k,v in ipairs(self.cols) do
v:SizeToContents()
cx = cx - v.Width
v:SetPos(cx - v:GetWide()/2, (SB_ROW_HEIGHT - v:GetTall()) / 2)
end
self.tag:SizeToContents()
cx = cx - 90
self.tag:SetPos(cx - self.tag:GetWide()/2, (SB_ROW_HEIGHT - self.tag:GetTall()) / 2)
self.sresult:SetPos(cx - 8, (SB_ROW_HEIGHT - 16) / 2)
end
function PANEL:PerformLayout()
self.avatar:SetPos(0,0)
self.avatar:SetSize(SB_ROW_HEIGHT,SB_ROW_HEIGHT)
local fw = sboard_panel.ply_frame:GetWide()
self:SetWide( sboard_panel.ply_frame.scroll.Enabled and fw-16 or fw )
if not self.open then
self:SetSize(self:GetWide(), SB_ROW_HEIGHT)
if self.info then self.info:SetVisible(false) end
elseif self.info then
self:SetSize(self:GetWide(), 100 + SB_ROW_HEIGHT)
self.info:SetVisible(true)
self.info:SetPos(5, SB_ROW_HEIGHT + 5)
self.info:SetSize(self:GetWide(), 100)
self.info:PerformLayout()
self:SetSize(self:GetWide(), SB_ROW_HEIGHT + self.info:GetTall())
end
self.nick:SizeToContents()
self.nick:SetPos(SB_ROW_HEIGHT + 10, (SB_ROW_HEIGHT - self.nick:GetTall()) / 2)
self:LayoutColumns()
self.voice:SetVisible(not self.open)
self.voice:SetSize(16, 16)
self.voice:DockMargin(4, 4, 4, 4)
self.voice:Dock(RIGHT)
end
function PANEL:DoClick(x, y)
self:SetOpen(not self.open)
end
function PANEL:SetOpen(o)
if self.open then
surface.PlaySound("ui/buttonclickrelease.wav")
else
surface.PlaySound("ui/buttonclick.wav")
end
self.open = o
self:PerformLayout()
self:GetParent():PerformLayout()
sboard_panel:PerformLayout()
end
function PANEL:DoRightClick()
local menu = DermaMenu()
menu.Player = self:GetPlayer()
local close = hook.Call( "TTTScoreboardMenu", nil, menu )
if close then menu:Remove() return end
menu:Open()
end
function PANEL:ShowMicVolumeSlider()
local width = 300
local height = 50
local padding = 10
local sliderHeight = 16
local sliderDisplayHeight = 8
local x = math.max(gui.MouseX() - width, 0)
local y = math.min(gui.MouseY(), ScrH() - height)
local currentPlayerVolume = self:GetPlayer():GetVoiceVolumeScale()
currentPlayerVolume = currentPlayerVolume != nil and currentPlayerVolume or 1
-- Frame for the slider
local frame = vgui.Create("DFrame")
frame:SetPos(x, y)
frame:SetSize(width, height)
frame:MakePopup()
frame:SetTitle("")
frame:ShowCloseButton(false)
frame:SetDraggable(false)
frame:SetSizable(false)
frame.Paint = function(self, w, h)
draw.RoundedBox(5, 0, 0, w, h, Color(24, 25, 28, 255))
end
-- Automatically close after 10 seconds (something may have gone wrong)
timer.Simple(10, function() if IsValid(frame) then frame:Close() end end)
-- "Player volume"
local label = vgui.Create("DLabel", frame)
label:SetPos(padding, padding)
label:SetFont("cool_small")
label:SetSize(width - padding * 2, 20)
label:SetColor(Color(255, 255, 255, 255))
label:SetText(LANG.GetTranslation("sb_playervolume"))
-- Slider
local slider = vgui.Create("DSlider", frame)
slider:SetHeight(sliderHeight)
slider:Dock(TOP)
slider:DockMargin(padding, 0, padding, 0)
slider:SetSlideX(currentPlayerVolume)
slider:SetLockY(0.5)
slider.TranslateValues = function(slider, x, y)
if IsValid(self:GetPlayer()) then self:GetPlayer():SetVoiceVolumeScale(x) end
return x, y
end
-- Close the slider panel once the player has selected a volume
slider.OnMouseReleased = function(panel, mcode) frame:Close() end
slider.Knob.OnMouseReleased = function(panel, mcode) frame:Close() end
-- Slider rendering
-- Render slider bar
slider.Paint = function(self, w, h)
local volumePercent = slider:GetSlideX()
-- Filled in box
draw.RoundedBox(5, 0, sliderDisplayHeight / 2, w * volumePercent, sliderDisplayHeight, Color(200, 46, 46, 255))
-- Grey box
draw.RoundedBox(5, w * volumePercent, sliderDisplayHeight / 2, w * (1 - volumePercent), sliderDisplayHeight, Color(79, 84, 92, 255))
end
-- Render slider "knob" & text
slider.Knob.Paint = function(self, w, h)
if slider:IsEditing() then
local textValue = math.Round(slider:GetSlideX() * 100) .. "%"
local textPadding = 5
-- The position of the text and size of rounded box are not relative to the text size. May cause problems if font size changes
draw.RoundedBox(
5, -- Radius
-sliderHeight * 0.5 - textPadding, -- X
-25, -- Y
sliderHeight * 2 + textPadding * 2, -- Width
sliderHeight + textPadding * 2, -- Height
Color(52, 54, 57, 255)
)
draw.DrawText(textValue, "cool_small", sliderHeight / 2, -20, Color(255, 255, 255, 255), TEXT_ALIGN_CENTER)
end
draw.RoundedBox(100, 0, 0, sliderHeight, sliderHeight, Color(255, 255, 255, 255))
end
end
vgui.Register( "TTTScorePlayerRow", PANEL, "DButton" )

View File

@@ -0,0 +1,200 @@
--[[
| 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/
--]]
---- Unlike sandbox, we have teams to deal with, so here's an extra panel in the
---- hierarchy that handles a set of player rows belonging to its team.
include("sb_row.lua")
local PANEL = {}
function PANEL:Init()
self.name = "Unnamed"
self.color = COLOR_WHITE
self.rows = {}
self.rowcount = 0
self.rows_sorted = {}
self.group = "spec"
end
function PANEL:SetGroupInfo(name, color, group)
self.name = name
self.color = color
self.group = group
end
local bgcolor = Color(20,20,20, 150)
function PANEL:Paint()
-- Darkened background
draw.RoundedBox(8, 0, 0, self:GetWide(), self:GetTall(), bgcolor)
surface.SetFont("treb_small")
-- Header bg
local txt = self.name .. " (" .. self.rowcount .. ")"
local w, h = surface.GetTextSize(txt)
draw.RoundedBox(8, 0, 0, w + 24, 20, self.color)
-- Shadow
surface.SetTextPos(11, 11 - h/2)
surface.SetTextColor(0,0,0, 200)
surface.DrawText(txt)
-- Text
surface.SetTextPos(10, 10 - h/2)
surface.SetTextColor(255,255,255,255)
surface.DrawText(txt)
-- Alternating row background
local y = 24
for i, row in ipairs(self.rows_sorted) do
if (i % 2) != 0 then
surface.SetDrawColor(75,75,75, 100)
surface.DrawRect(0, y, self:GetWide(), row:GetTall())
end
y = y + row:GetTall() + 1
end
-- Column darkening
local scr = sboard_panel.ply_frame.scroll.Enabled and 16 or 0
surface.SetDrawColor(0,0,0, 80)
if sboard_panel.cols then
local cx = self:GetWide() - scr
for k,v in ipairs(sboard_panel.cols) do
cx = cx - v.Width
if k % 2 == 1 then -- Draw for odd numbered columns
surface.DrawRect(cx-v.Width/2, 0, v.Width, self:GetTall())
end
end
else
-- If columns are not setup yet, fall back to darkening the areas for the
-- default columns
surface.DrawRect(self:GetWide() - 175 - 25 - scr, 0, 50, self:GetTall())
surface.DrawRect(self:GetWide() - 75 - 25 - scr, 0, 50, self:GetTall())
end
end
function PANEL:AddPlayerRow(ply)
if ScoreGroup(ply) == self.group and not self.rows[ply] then
local row = vgui.Create("TTTScorePlayerRow", self)
row:SetPlayer(ply)
self.rows[ply] = row
self.rowcount = table.Count(self.rows)
-- must force layout immediately or it takes its sweet time to do so
self:PerformLayout()
end
end
function PANEL:HasPlayerRow(ply)
return self.rows[ply] != nil
end
function PANEL:HasRows()
return self.rowcount > 0
end
local strlower = string.lower
function PANEL:UpdateSortCache()
self.rows_sorted = {}
for _, row in pairs(self.rows) do
table.insert(self.rows_sorted, row)
end
table.sort(self.rows_sorted, function(rowa, rowb)
local plya = rowa:GetPlayer()
local plyb = rowb:GetPlayer()
if not IsValid(plya) then return false end
if not IsValid(plyb) then return true end
local sort_mode = GetConVar("ttt_scoreboard_sorting"):GetString()
local sort_func = sboard_sort[sort_mode]
local comp = 0
if sort_func != nil then
comp = sort_func(plya, plyb)
end
local ret = true
if comp != 0 then
ret = comp > 0
else
ret = strlower(plya:GetName()) > strlower(plyb:GetName())
end
if GetConVar("ttt_scoreboard_ascending"):GetBool() then
ret = not ret
end
return ret
end)
end
function PANEL:UpdatePlayerData()
local to_remove = {}
for k,v in pairs(self.rows) do
-- Player still belongs in this group?
if IsValid(v) and IsValid(v:GetPlayer()) and ScoreGroup(v:GetPlayer()) == self.group then
v:UpdatePlayerData()
else
-- can't remove now, will break pairs
table.insert(to_remove, k)
end
end
if #to_remove == 0 then return end
for k,ply in pairs(to_remove) do
local pnl = self.rows[ply]
if IsValid(pnl) then
pnl:Remove()
end
self.rows[ply] = nil
end
self.rowcount = table.Count(self.rows)
self:UpdateSortCache()
self:InvalidateLayout()
end
function PANEL:PerformLayout()
if self.rowcount < 1 then
self:SetVisible(false)
return
end
self:SetSize(self:GetWide(), 30 + self.rowcount + self.rowcount * SB_ROW_HEIGHT)
-- Sort and layout player rows
self:UpdateSortCache()
local y = 24
for k, v in ipairs(self.rows_sorted) do
v:SetPos(0, y)
v:SetSize(self:GetWide(), v:GetTall())
y = y + v:GetTall() + 1
end
self:SetSize(self:GetWide(), 30 + (y - 24))
end
vgui.Register("TTTScoreGroup", PANEL, "Panel")

View File

@@ -0,0 +1,88 @@
--[[
| 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/
--]]
--- why can't the default label scroll? welcome to gmod
PANEL = {}
function PANEL:Init()
self.Label = vgui.Create("DLabel", self)
self.Label:SetPos(0, 0)
self.Scroll = vgui.Create("DVScrollBar", self)
end
function PANEL:GetLabel() return self.Label end
function PANEL:OnMouseWheeled(dlta)
if not self.Scroll then return end
self.Scroll:AddScroll(dlta * -2)
self:InvalidateLayout()
end
function PANEL:SetScrollEnabled(st) self.Scroll:SetEnabled(st) end
-- enable/disable scrollbar depending on content size
function PANEL:UpdateScrollState()
if not self.Scroll then return end
self.Scroll:SetScroll(0)
self:SetScrollEnabled(false)
self.Label:SetSize(self:GetWide(), self:GetTall())
self.Label:SizeToContentsY()
self:SetScrollEnabled(self.Label:GetTall() > self:GetTall())
self.Label:InvalidateLayout(true)
self:InvalidateLayout(true)
end
function PANEL:SetText(txt)
if not self.Label then return end
self.Label:SetText(txt)
self:UpdateScrollState()
-- I give up. VGUI, you have won. Here is your ugly hack to make the label
-- resize to the proper height, after you have completely mangled it the
-- first time I call SizeToContents. I don't know how or what happens to the
-- Label's internal state that makes it work when resizing a second time a
-- tick later (it certainly isn't any variant of PerformLayout I can find),
-- but it does.
local pnl = self.Panel
timer.Simple(0, function()
if IsValid(pnl) then
pnl:UpdateScrollState()
end
end)
end
function PANEL:PerformLayout()
if not self.Scroll then return end
self.Label:SetVisible(self:IsVisible())
self.Scroll:SetPos(self:GetWide() - 16, 0)
self.Scroll:SetSize(16, self:GetTall())
local was_on = self.Scroll.Enabled
self.Scroll:SetUp(self:GetTall(), self.Label:GetTall())
self.Scroll:SetEnabled(was_on) -- setup mangles enabled state
self.Label:SetPos( 0, self.Scroll:GetOffset() )
self.Label:SetSize( self:GetWide() - (self.Scroll.Enabled and 16 or 0), self.Label:GetTall() )
end
vgui.Register("ScrollLabel", PANEL, "Panel")

View File

@@ -0,0 +1,240 @@
--[[
| 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/
--]]
-- Altered version of gmod's SpawnIcon
-- This panel does not deal with models and such
local matHover = Material( "vgui/spawnmenu/hover" )
local PANEL = {}
AccessorFunc( PANEL, "m_iIconSize", "IconSize" )
function PANEL:Init()
self.Icon = vgui.Create( "DImage", self )
self.Icon:SetMouseInputEnabled( false )
self.Icon:SetKeyboardInputEnabled( false )
self.animPress = Derma_Anim( "Press", self, self.PressedAnim )
self:SetIconSize(64)
end
function PANEL:OnMousePressed( mcode )
if mcode == MOUSE_LEFT then
self:DoClick()
self.animPress:Start(0.1)
end
end
function PANEL:OnMouseReleased()
end
function PANEL:DoClick()
end
function PANEL:OpenMenu()
end
function PANEL:ApplySchemeSettings()
end
function PANEL:OnCursorEntered()
self.PaintOverOld = self.PaintOver
self.PaintOver = self.PaintOverHovered
end
function PANEL:OnCursorExited()
if self.PaintOver == self.PaintOverHovered then
self.PaintOver = self.PaintOverOld
end
end
function PANEL:PaintOverHovered()
if self.animPress:Active() then return end
surface.SetDrawColor( 255, 255, 255, 80 )
surface.SetMaterial( matHover )
self:DrawTexturedRect()
end
function PANEL:PerformLayout()
if self.animPress:Active() then return end
self:SetSize( self.m_iIconSize, self.m_iIconSize )
self.Icon:StretchToParent( 0, 0, 0, 0 )
end
function PANEL:SetIcon( icon )
self.Icon:SetImage(icon)
end
function PANEL:GetIcon()
return self.Icon:GetImage()
end
function PANEL:SetIconColor(clr)
self.Icon:SetImageColor(clr)
end
function PANEL:Think()
self.animPress:Run()
end
function PANEL:PressedAnim( anim, delta, data )
if anim.Started then
return
end
if anim.Finished then
self.Icon:StretchToParent( 0, 0, 0, 0 )
return
end
local border = math.sin( delta * math.pi ) * (self.m_iIconSize * 0.05 )
self.Icon:StretchToParent( border, border, border, border )
end
vgui.Register( "SimpleIcon", PANEL, "Panel" )
---
local PANEL = {}
function PANEL:Init()
self.Layers = {}
end
-- Add a panel to this icon. Most recent addition will be the top layer.
function PANEL:AddLayer(pnl)
if not IsValid(pnl) then return end
pnl:SetParent(self)
pnl:SetMouseInputEnabled(false)
pnl:SetKeyboardInputEnabled(false)
table.insert(self.Layers, pnl)
end
function PANEL:PerformLayout()
if self.animPress:Active() then return end
self:SetSize( self.m_iIconSize, self.m_iIconSize )
self.Icon:StretchToParent( 0, 0, 0, 0 )
for _, p in ipairs(self.Layers) do
p:SetPos(0, 0)
p:InvalidateLayout()
end
end
function PANEL:EnableMousePassthrough(pnl)
for _, p in pairs(self.Layers) do
if p == pnl then
p.OnMousePressed = function(s, mc) s:GetParent():OnMousePressed(mc) end
p.OnCursorEntered = function(s) s:GetParent():OnCursorEntered() end
p.OnCursorExited = function(s) s:GetParent():OnCursorExited() end
p:SetMouseInputEnabled(true)
end
end
end
vgui.Register("LayeredIcon", PANEL, "SimpleIcon")
-- Avatar icon
local PANEL = {}
function PANEL:Init()
self.imgAvatar = vgui.Create( "AvatarImage", self )
self.imgAvatar:SetMouseInputEnabled( false )
self.imgAvatar:SetKeyboardInputEnabled( false )
self.imgAvatar.PerformLayout = function(s) s:Center() end
self:SetAvatarSize(32)
self:AddLayer(self.imgAvatar)
--return self.BaseClass.Init(self)
end
function PANEL:SetAvatarSize(s)
self.imgAvatar:SetSize(s, s)
end
function PANEL:SetPlayer(ply)
self.imgAvatar:SetPlayer(ply)
end
vgui.Register( "SimpleIconAvatar", PANEL, "LayeredIcon" )
--- Labelled icon
local PANEL = {}
AccessorFunc(PANEL, "IconText", "IconText")
AccessorFunc(PANEL, "IconTextColor", "IconTextColor")
AccessorFunc(PANEL, "IconFont", "IconFont")
AccessorFunc(PANEL, "IconTextShadow", "IconTextShadow")
AccessorFunc(PANEL, "IconTextPos", "IconTextPos")
function PANEL:Init()
self:SetIconText("")
self:SetIconTextColor(Color(255, 200, 0))
self:SetIconFont("TargetID")
self:SetIconTextShadow({opacity=255, offset=2})
self:SetIconTextPos({32, 32})
-- DPanelSelect loves to overwrite its children's PaintOver hooks and such,
-- so have to use a dummy panel to do some custom painting.
self.FakeLabel = vgui.Create("Panel", self)
self.FakeLabel.PerformLayout = function(s) s:StretchToParent(0,0,0,0) end
self:AddLayer(self.FakeLabel)
return self.BaseClass.Init(self)
end
function PANEL:PerformLayout()
self:SetLabelText(self:GetIconText(), self:GetIconTextColor(), self:GetIconFont(), self:GetIconTextPos())
return self.BaseClass.PerformLayout(self)
end
function PANEL:SetIconProperties(color, font, shadow, pos)
self:SetIconTextColor( color or self:GetIconTextColor())
self:SetIconFont( font or self:GetIconFont())
self:SetIconTextShadow(shadow or self:GetIconShadow())
self:SetIconTextPos( pos or self:GetIconTextPos())
end
function PANEL:SetLabelText(text, color, font, pos)
if self.FakeLabel then
local spec = {pos=pos, color=color, text=text, font=font, xalign=TEXT_ALIGN_CENTER, yalign=TEXT_ALIGN_CENTER}
local shadow = self:GetIconTextShadow()
local opacity = shadow and shadow.opacity or 0
local offset = shadow and shadow.offset or 0
local drawfn = shadow and draw.TextShadow or draw.Text
self.FakeLabel.Paint = function()
drawfn(spec, offset, opacity)
end
end
end
vgui.Register("SimpleIconLabelled", PANEL, "LayeredIcon")

View File

@@ -0,0 +1,520 @@
--[[
| 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/
--]]
include("weaponry_shd.lua") -- inits WEPS tbl
---- Weapon system, pickup limits, etc
local IsEquipment = WEPS.IsEquipment
-- Prevent players from picking up multiple weapons of the same type etc
function GM:PlayerCanPickupWeapon(ply, wep)
if not IsValid(wep) or not IsValid(ply) then return end
if ply:IsSpec() then return false end
-- While resetting the map, players should not be allowed to pick up the newly-reset weapon
-- entities, because they would be stripped again during the player spawning process and
-- subsequently be missing.
if GAMEMODE.RespawningWeapons then
return false
end
-- Disallow picking up for ammo
if ply:HasWeapon(wep:GetClass()) then
return false
elseif not ply:CanCarryWeapon(wep) then
return false
elseif IsEquipment(wep) and wep.IsDropped and (not ply:KeyDown(IN_USE)) then
return false
end
local tr = util.TraceEntity({start=wep:GetPos(), endpos=ply:GetShootPos(), mask=MASK_SOLID}, wep)
if tr.Fraction == 1.0 or tr.Entity == ply then
wep:SetPos(ply:GetShootPos())
end
return true
end
-- Cache role -> default-weapons table
local loadout_weapons = nil
local function GetLoadoutWeapons(r)
if not loadout_weapons then
local tbl = {
[ROLE_INNOCENT] = {},
[ROLE_TRAITOR] = {},
[ROLE_DETECTIVE]= {}
};
for k, w in pairs(weapons.GetList()) do
if w and istable(w.InLoadoutFor) then
for _, wrole in pairs(w.InLoadoutFor) do
table.insert(tbl[wrole], WEPS.GetClass(w))
end
end
end
loadout_weapons = tbl
end
return loadout_weapons[r]
end
-- Give player loadout weapons he should have for his role that he does not have
-- yet
local function GiveLoadoutWeapons(ply)
local r = GetRoundState() == ROUND_PREP and ROLE_INNOCENT or ply:GetRole()
local weps = GetLoadoutWeapons(r)
if not weps then return end
for _, cls in pairs(weps) do
if not ply:HasWeapon(cls) and ply:CanCarryType(WEPS.TypeForWeapon(cls)) then
ply:Give(cls)
end
end
end
local function HasLoadoutWeapons(ply)
if ply:IsSpec() then return true end
local r = GetRoundState() == ROUND_PREP and ROLE_INNOCENT or ply:GetRole()
local weps = GetLoadoutWeapons(r)
if not weps then return true end
for _, cls in pairs(weps) do
if not ply:HasWeapon(cls) and ply:CanCarryType(WEPS.TypeForWeapon(cls)) then
return false
end
end
return true
end
-- Give loadout items.
local function GiveLoadoutItems(ply)
local items = EquipmentItems[ply:GetRole()]
if items then
for _, item in pairs(items) do
if item.loadout and item.id then
ply:GiveEquipmentItem(item.id)
end
end
end
end
-- Quick hack to limit hats to models that fit them well
local Hattables = { "phoenix.mdl", "arctic.mdl", "Group01", "monk.mdl" }
local function CanWearHat(ply)
local path = string.Explode("/", ply:GetModel())
if #path == 1 then path = string.Explode("\\", path) end
return table.HasValue(Hattables, path[3])
end
CreateConVar("ttt_detective_hats", "1")
-- Just hats right now
local function GiveLoadoutSpecial(ply)
if ply:IsActiveDetective() and GetConVar("ttt_detective_hats"):GetBool() and CanWearHat(ply) then
if not IsValid(ply.hat) then
local hat = ents.Create("ttt_hat_deerstalker")
if not IsValid(hat) then return end
hat:SetPos(ply:GetPos() + Vector(0,0,70))
hat:SetAngles(ply:GetAngles())
hat:SetParent(ply)
ply.hat = hat
hat:Spawn()
end
else
SafeRemoveEntity(ply.hat)
ply.hat = nil
end
end
-- Sometimes, in cramped map locations, giving players weapons fails. A timer
-- calling this function is used to get them the weapons anyway as soon as
-- possible.
local function LateLoadout(id)
local ply = Entity(id)
if not IsValid(ply) or not ply:IsPlayer() then
timer.Remove("lateloadout" .. id)
return
end
if not HasLoadoutWeapons(ply) then
GiveLoadoutWeapons(ply)
if HasLoadoutWeapons(ply) then
timer.Remove("lateloadout" .. id)
end
end
end
-- Note that this is called both when a player spawns and when a round starts
function GM:PlayerLoadout( ply )
if IsValid(ply) and (not ply:IsSpec()) then
-- clear out equipment flags
ply:ResetEquipment()
-- give default items
GiveLoadoutItems(ply)
-- hand out weaponry
GiveLoadoutWeapons(ply)
GiveLoadoutSpecial(ply)
if not HasLoadoutWeapons(ply) then
MsgN("Could not spawn all loadout weapons for " .. ply:Nick() .. ", will retry.")
timer.Create("lateloadout" .. ply:EntIndex(), 1, 0,
function() LateLoadout(ply:EntIndex()) end)
end
end
end
function GM:UpdatePlayerLoadouts()
for _, ply in player.Iterator() do
hook.Call("PlayerLoadout", GAMEMODE, ply)
end
end
---- Weapon dropping
function WEPS.DropNotifiedWeapon(ply, wep, death_drop)
if IsValid(ply) and IsValid(wep) then
-- Hack to tell the weapon it's about to be dropped and should do what it
-- must right now
if wep.PreDrop then
wep:PreDrop(death_drop)
end
-- PreDrop might destroy weapon
if not IsValid(wep) then return end
-- Tag this weapon as dropped, so that if it's a special weapon we do not
-- auto-pickup when nearby.
wep.IsDropped = true
-- After dropping a weapon, always switch to holstered, so that traitors
-- will never accidentally pull out a traitor weapon.
--
-- Perform this *before* the drop in order to abuse the fact that this
-- holsters the weapon, which in turn aborts any reload that's in
-- progress. We don't want a dropped weapon to be in a reloading state
-- because the relevant timer is reset when picking it up, making the
-- reload happen instantly. This allows one to dodge the delay by dropping
-- during reload. All of this is a workaround for not having access to
-- CBaseWeapon::AbortReload() (and that not being handled in
-- CBaseWeapon::Drop in the first place).
ply:SelectWeapon("weapon_ttt_unarmed")
ply:DropWeapon(wep)
wep:PhysWake()
end
end
local function DropActiveWeapon(ply)
if not IsValid(ply) then return end
local wep = ply:GetActiveWeapon()
if not IsValid(wep) then return end
if wep.AllowDrop == false then
return
end
local tr = util.QuickTrace(ply:GetShootPos(), ply:GetAimVector() * 32, ply)
if tr.HitWorld then
LANG.Msg(ply, "drop_no_room")
return
end
ply:AnimPerformGesture(ACT_GMOD_GESTURE_ITEM_PLACE)
WEPS.DropNotifiedWeapon(ply, wep)
end
concommand.Add("ttt_dropweapon", DropActiveWeapon)
local function DropActiveAmmo(ply)
if not IsValid(ply) then return end
local wep = ply:GetActiveWeapon()
if not IsValid(wep) then return end
if not wep.AmmoEnt then return end
local amt = wep:Clip1()
if amt < 1 or amt <= (wep.Primary.ClipSize * 0.25) then
LANG.Msg(ply, "drop_no_ammo")
return
end
local pos, ang = ply:GetShootPos(), ply:EyeAngles()
local dir = (ang:Forward() * 32) + (ang:Right() * 6) + (ang:Up() * -5)
local tr = util.QuickTrace(pos, dir, ply)
if tr.HitWorld then return end
wep:SetClip1(0)
ply:AnimPerformGesture(ACT_GMOD_GESTURE_ITEM_GIVE)
local box = ents.Create(wep.AmmoEnt)
if not IsValid(box) then return end
box:SetPos(pos + dir)
box:SetOwner(ply)
box:Spawn()
box:PhysWake()
local phys = box:GetPhysicsObject()
if IsValid(phys) then
phys:ApplyForceCenter(ang:Forward() * 1000)
phys:ApplyForceOffset(VectorRand(), vector_origin)
end
box.AmmoAmount = amt
timer.Simple(2, function()
if IsValid(box) then
box:SetOwner(nil)
end
end)
end
concommand.Add("ttt_dropammo", DropActiveAmmo)
-- Give a weapon to a player. If the initial attempt fails due to heisenbugs in
-- the map, keep trying until the player has moved to a better spot where it
-- does work.
local function GiveEquipmentWeapon(sid64, cls)
-- Referring to players by SteamID64 because a player may disconnect while his
-- unique timer still runs, in which case we want to be able to stop it. For
-- that we need its name, and hence his SteamID64.
local ply = player.GetBySteamID64(sid64)
local tmr = "give_equipment" .. sid64
if (not IsValid(ply)) or (not ply:IsActiveSpecial()) then
timer.Remove(tmr)
return
end
-- giving attempt, will fail if we're in a crazy spot in the map or perhaps
-- other glitchy cases
local w = ply:Give(cls)
if (not IsValid(w)) or (not ply:HasWeapon(cls)) then
if not timer.Exists(tmr) then
timer.Create(tmr, 1, 0, function() GiveEquipmentWeapon(sid64, cls) end)
end
-- we will be retrying
else
-- can stop retrying, if we were
timer.Remove(tmr)
if w.WasBought then
-- some weapons give extra ammo after being bought, etc
w:WasBought(ply)
end
end
end
local function HasPendingOrder(ply)
return timer.Exists("give_equipment" .. tostring(ply:SteamID64()))
end
function GM:TTTCanOrderEquipment(ply, id, is_item)
--- return true to allow buying of an equipment item, false to disallow
return true
end
-- Equipment buying
local function OrderEquipment(ply, cmd, args)
if not IsValid(ply) or #args != 1 then return end
if not (ply:IsActiveTraitor() or ply:IsActiveDetective()) then return end
-- no credits, can't happen when buying through menu as button will be off
if ply:GetCredits() < 1 then return end
-- it's an item if the arg is an id instead of an ent name
local id = args[1]
local is_item = tonumber(id)
if not hook.Run("TTTCanOrderEquipment", ply, id, is_item) then return end
-- we use weapons.GetStored to save time on an unnecessary copy, we will not
-- be modifying it
local swep_table = (not is_item) and weapons.GetStored(id) or nil
-- some weapons can only be bought once per player per round, this used to be
-- defined in a table here, but is now in the SWEP's table
if swep_table and swep_table.LimitedStock and ply:HasBought(id) then
LANG.Msg(ply, "buy_no_stock")
return
end
local received = false
if is_item then
id = tonumber(id)
-- item whitelist check
local allowed = GetEquipmentItem(ply:GetRole(), id)
if not allowed then
print(ply, "tried to buy item not buyable for his class:", id)
return
end
-- ownership check and finalise
if id and EQUIP_NONE < id then
if not ply:HasEquipmentItem(id) then
ply:GiveEquipmentItem(id)
received = true
end
end
elseif swep_table then
-- weapon whitelist check
if not table.HasValue(swep_table.CanBuy, ply:GetRole()) then
print(ply, "tried to buy weapon his role is not permitted to buy")
return
end
-- if we have a pending order because we are in a confined space, don't
-- start a new one
if HasPendingOrder(ply) then
LANG.Msg(ply, "buy_pending")
return
end
-- no longer restricted to only WEAPON_EQUIP weapons, just anything that
-- is whitelisted and carryable
if ply:CanCarryWeapon(swep_table) then
GiveEquipmentWeapon(ply:SteamID64(), id)
received = true
end
end
if received then
ply:SubtractCredits(1)
LANG.Msg(ply, "buy_received")
ply:AddBought(id)
timer.Simple(0.5,
function()
if not IsValid(ply) then return end
net.Start("TTT_BoughtItem")
net.WriteBit(is_item)
if is_item then
net.WriteUInt(id, 16)
else
net.WriteString(id)
end
net.Send(ply)
end)
hook.Call("TTTOrderedEquipment", GAMEMODE, ply, id, is_item)
end
end
concommand.Add("ttt_order_equipment", OrderEquipment)
function GM:TTTToggleDisguiser(ply, state)
-- Can be used to prevent players from using this button.
-- return true to prevent it.
end
local function SetDisguise(ply, cmd, args)
if not IsValid(ply) or not ply:IsActiveTraitor() then return end
if ply:HasEquipmentItem(EQUIP_DISGUISE) then
local state = #args == 1 and tobool(args[1])
if hook.Run("TTTToggleDisguiser", ply, state) then return end
ply:SetNWBool("disguised", state)
LANG.Msg(ply, state and "disg_turned_on" or "disg_turned_off")
end
end
concommand.Add("ttt_set_disguise", SetDisguise)
local function CheatCredits(ply)
if IsValid(ply) then
ply:AddCredits(10)
end
end
concommand.Add("ttt_cheat_credits", CheatCredits, nil, nil, FCVAR_CHEAT)
local function TransferCredits(ply, cmd, args)
if (not IsValid(ply)) or (not ply:IsActiveSpecial()) then return end
if #args != 2 then return end
local sid64 = tostring(args[1])
local credits = tonumber(args[2])
if sid64 and credits then
local target = player.GetBySteamID64(sid64)
if (not IsValid(target)) or (not target:IsActiveSpecial()) or (target:GetRole() ~= ply:GetRole()) or (target == ply) then
LANG.Msg(ply, "xfer_no_recip")
return
end
if ply:GetCredits() < credits then
LANG.Msg(ply, "xfer_no_credits")
return
end
credits = math.Clamp(credits, 0, ply:GetCredits())
if credits == 0 then return end
ply:SubtractCredits(credits)
target:AddCredits(credits)
LANG.Msg(ply, "xfer_success", {player=target:Nick()})
LANG.Msg(target, "xfer_received", {player = ply:Nick(), num = credits})
end
end
concommand.Add("ttt_transfer_credits", TransferCredits)
-- Protect against non-TTT weapons that may break the HUD
function GM:WeaponEquip(wep)
if IsValid(wep) then
-- only remove if they lack critical stuff
if not wep.Kind then
wep:Remove()
ErrorNoHalt("Equipped weapon " .. wep:GetClass() .. " is not compatible with TTT\n")
end
end
end
-- non-cheat developer commands can reveal precaching the first time equipment
-- is bought, so trigger it at the start of a round instead
function WEPS.ForcePrecache()
for k, w in ipairs(weapons.GetList()) do
if w.WorldModel then
util.PrecacheModel(w.WorldModel)
end
if w.ViewModel then
util.PrecacheModel(w.ViewModel)
end
end
end

View File

@@ -0,0 +1,41 @@
--[[
| 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/
--]]
WEPS = {}
function WEPS.TypeForWeapon(class)
local tbl = util.WeaponForClass(class)
return tbl and tbl.Kind or WEAPON_NONE
end
-- You'd expect this to go on the weapon entity, but we need to be able to call
-- it on a swep table as well.
function WEPS.IsEquipment(wep)
return wep.Kind and wep.Kind >= WEAPON_EQUIP
end
function WEPS.GetClass(wep)
if istable(wep) then
return wep.ClassName or wep.Classname
elseif IsValid(wep) then
return wep:GetClass()
end
end
function WEPS.DisguiseToggle(ply)
if IsValid(ply) and ply:IsActiveTraitor() then
if not ply:GetNWBool("disguised", false) then
RunConsoleCommand("ttt_set_disguise", "1")
else
RunConsoleCommand("ttt_set_disguise", "0")
end
end
end