mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-17 05:43:46 +03:00
Upload
This commit is contained in:
226
gamemodes/terrortown/gamemode/admin.lua
Normal file
226
gamemodes/terrortown/gamemode/admin.lua
Normal 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
|
||||
788
gamemodes/terrortown/gamemode/cl_awards.lua
Normal file
788
gamemodes/terrortown/gamemode/cl_awards.lua
Normal 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 }
|
||||
61
gamemodes/terrortown/gamemode/cl_disguise.lua
Normal file
61
gamemodes/terrortown/gamemode/cl_disguise.lua
Normal 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)
|
||||
515
gamemodes/terrortown/gamemode/cl_equip.lua
Normal file
515
gamemodes/terrortown/gamemode/cl_equip.lua
Normal 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)
|
||||
265
gamemodes/terrortown/gamemode/cl_help.lua
Normal file
265
gamemodes/terrortown/gamemode/cl_help.lua
Normal 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
|
||||
384
gamemodes/terrortown/gamemode/cl_hud.lua
Normal file
384
gamemodes/terrortown/gamemode/cl_hud.lua
Normal 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
|
||||
|
||||
203
gamemodes/terrortown/gamemode/cl_hudpickup.lua
Normal file
203
gamemodes/terrortown/gamemode/cl_hudpickup.lua
Normal 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
|
||||
414
gamemodes/terrortown/gamemode/cl_init.lua
Normal file
414
gamemodes/terrortown/gamemode/cl_init.lua
Normal 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
|
||||
135
gamemodes/terrortown/gamemode/cl_keys.lua
Normal file
135
gamemodes/terrortown/gamemode/cl_keys.lua
Normal 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
|
||||
378
gamemodes/terrortown/gamemode/cl_lang.lua
Normal file
378
gamemodes/terrortown/gamemode/cl_lang.lua
Normal 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
|
||||
246
gamemodes/terrortown/gamemode/cl_msgstack.lua
Normal file
246
gamemodes/terrortown/gamemode/cl_msgstack.lua
Normal 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)
|
||||
140
gamemodes/terrortown/gamemode/cl_popups.lua
Normal file
140
gamemodes/terrortown/gamemode/cl_popups.lua
Normal 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)
|
||||
322
gamemodes/terrortown/gamemode/cl_radar.lua
Normal file
322
gamemodes/terrortown/gamemode/cl_radar.lua
Normal 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
|
||||
|
||||
107
gamemodes/terrortown/gamemode/cl_radio.lua
Normal file
107
gamemodes/terrortown/gamemode/cl_radio.lua
Normal 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
|
||||
|
||||
71
gamemodes/terrortown/gamemode/cl_scoreboard.lua
Normal file
71
gamemodes/terrortown/gamemode/cl_scoreboard.lua
Normal 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
|
||||
630
gamemodes/terrortown/gamemode/cl_scoring.lua
Normal file
630
gamemodes/terrortown/gamemode/cl_scoring.lua
Normal 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)
|
||||
262
gamemodes/terrortown/gamemode/cl_scoring_events.lua
Normal file
262
gamemodes/terrortown/gamemode/cl_scoring_events.lua
Normal 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
|
||||
})
|
||||
|
||||
|
||||
514
gamemodes/terrortown/gamemode/cl_search.lua
Normal file
514
gamemodes/terrortown/gamemode/cl_search.lua
Normal 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)
|
||||
367
gamemodes/terrortown/gamemode/cl_targetid.lua
Normal file
367
gamemodes/terrortown/gamemode/cl_targetid.lua
Normal 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
|
||||
170
gamemodes/terrortown/gamemode/cl_tbuttons.lua
Normal file
170
gamemodes/terrortown/gamemode/cl_tbuttons.lua
Normal 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
|
||||
291
gamemodes/terrortown/gamemode/cl_tips.lua
Normal file
291
gamemodes/terrortown/gamemode/cl_tips.lua
Normal 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)
|
||||
72
gamemodes/terrortown/gamemode/cl_transfer.lua
Normal file
72
gamemodes/terrortown/gamemode/cl_transfer.lua
Normal 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
|
||||
691
gamemodes/terrortown/gamemode/cl_voice.lua
Normal file
691
gamemodes/terrortown/gamemode/cl_voice.lua
Normal 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
|
||||
350
gamemodes/terrortown/gamemode/cl_wepswitch.lua
Normal file
350
gamemodes/terrortown/gamemode/cl_wepswitch.lua
Normal 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)
|
||||
477
gamemodes/terrortown/gamemode/corpse.lua
Normal file
477
gamemodes/terrortown/gamemode/corpse.lua
Normal 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
|
||||
49
gamemodes/terrortown/gamemode/corpse_shd.lua
Normal file
49
gamemodes/terrortown/gamemode/corpse_shd.lua
Normal 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
|
||||
609
gamemodes/terrortown/gamemode/ent_replace.lua
Normal file
609
gamemodes/terrortown/gamemode/ent_replace.lua
Normal 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
|
||||
28
gamemodes/terrortown/gamemode/entity.lua
Normal file
28
gamemodes/terrortown/gamemode/entity.lua
Normal 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
|
||||
133
gamemodes/terrortown/gamemode/equip_items_shd.lua
Normal file
133
gamemodes/terrortown/gamemode/equip_items_shd.lua
Normal 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
|
||||
391
gamemodes/terrortown/gamemode/gamemsg.lua
Normal file
391
gamemodes/terrortown/gamemode/gamemsg.lua
Normal 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)
|
||||
978
gamemodes/terrortown/gamemode/init.lua
Normal file
978
gamemodes/terrortown/gamemode/init.lua
Normal 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)
|
||||
383
gamemodes/terrortown/gamemode/karma.lua
Normal file
383
gamemodes/terrortown/gamemode/karma.lua
Normal 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
|
||||
1098
gamemodes/terrortown/gamemode/lang/brazilian_portuguese.lua
Normal file
1098
gamemodes/terrortown/gamemode/lang/brazilian_portuguese.lua
Normal file
File diff suppressed because it is too large
Load Diff
51
gamemodes/terrortown/gamemode/lang/chef.lua
Normal file
51
gamemodes/terrortown/gamemode/lang/chef.lua
Normal 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
|
||||
})
|
||||
|
||||
1099
gamemodes/terrortown/gamemode/lang/english.lua
Normal file
1099
gamemodes/terrortown/gamemode/lang/english.lua
Normal file
File diff suppressed because it is too large
Load Diff
1094
gamemodes/terrortown/gamemode/lang/french.lua
Normal file
1094
gamemodes/terrortown/gamemode/lang/french.lua
Normal file
File diff suppressed because it is too large
Load Diff
1084
gamemodes/terrortown/gamemode/lang/german.lua
Normal file
1084
gamemodes/terrortown/gamemode/lang/german.lua
Normal file
File diff suppressed because it is too large
Load Diff
1138
gamemodes/terrortown/gamemode/lang/italian.lua
Normal file
1138
gamemodes/terrortown/gamemode/lang/italian.lua
Normal file
File diff suppressed because it is too large
Load Diff
1093
gamemodes/terrortown/gamemode/lang/japanese.lua
Normal file
1093
gamemodes/terrortown/gamemode/lang/japanese.lua
Normal file
File diff suppressed because it is too large
Load Diff
1099
gamemodes/terrortown/gamemode/lang/russian.lua
Normal file
1099
gamemodes/terrortown/gamemode/lang/russian.lua
Normal file
File diff suppressed because it is too large
Load Diff
1080
gamemodes/terrortown/gamemode/lang/simpchinese.lua
Normal file
1080
gamemodes/terrortown/gamemode/lang/simpchinese.lua
Normal file
File diff suppressed because it is too large
Load Diff
1109
gamemodes/terrortown/gamemode/lang/spanish.lua
Normal file
1109
gamemodes/terrortown/gamemode/lang/spanish.lua
Normal file
File diff suppressed because it is too large
Load Diff
1084
gamemodes/terrortown/gamemode/lang/swedish.lua
Normal file
1084
gamemodes/terrortown/gamemode/lang/swedish.lua
Normal file
File diff suppressed because it is too large
Load Diff
1054
gamemodes/terrortown/gamemode/lang/tradchinese.lua
Normal file
1054
gamemodes/terrortown/gamemode/lang/tradchinese.lua
Normal file
File diff suppressed because it is too large
Load Diff
1093
gamemodes/terrortown/gamemode/lang/turkish.lua
Normal file
1093
gamemodes/terrortown/gamemode/lang/turkish.lua
Normal file
File diff suppressed because it is too large
Load Diff
1078
gamemodes/terrortown/gamemode/lang/ukrainian.lua
Normal file
1078
gamemodes/terrortown/gamemode/lang/ukrainian.lua
Normal file
File diff suppressed because it is too large
Load Diff
141
gamemodes/terrortown/gamemode/lang_shd.lua
Normal file
141
gamemodes/terrortown/gamemode/lang_shd.lua
Normal 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
|
||||
1157
gamemodes/terrortown/gamemode/player.lua
Normal file
1157
gamemodes/terrortown/gamemode/player.lua
Normal file
File diff suppressed because it is too large
Load Diff
366
gamemodes/terrortown/gamemode/player_ext.lua
Normal file
366
gamemodes/terrortown/gamemode/player_ext.lua
Normal 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
|
||||
256
gamemodes/terrortown/gamemode/player_ext_shd.lua
Normal file
256
gamemodes/terrortown/gamemode/player_ext_shd.lua
Normal 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
|
||||
156
gamemodes/terrortown/gamemode/propspec.lua
Normal file
156
gamemodes/terrortown/gamemode/propspec.lua
Normal 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
|
||||
|
||||
81
gamemodes/terrortown/gamemode/radar.lua
Normal file
81
gamemodes/terrortown/gamemode/radar.lua
Normal 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)
|
||||
256
gamemodes/terrortown/gamemode/scoring.lua
Normal file
256
gamemodes/terrortown/gamemode/scoring.lua
Normal 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
|
||||
212
gamemodes/terrortown/gamemode/scoring_shd.lua
Normal file
212
gamemodes/terrortown/gamemode/scoring_shd.lua
Normal 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
|
||||
246
gamemodes/terrortown/gamemode/shared.lua
Normal file
246
gamemodes/terrortown/gamemode/shared.lua
Normal 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"
|
||||
}
|
||||
};
|
||||
186
gamemodes/terrortown/gamemode/traitor_state.lua
Normal file
186
gamemodes/terrortown/gamemode/traitor_state.lua
Normal 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)
|
||||
379
gamemodes/terrortown/gamemode/util.lua
Normal file
379
gamemodes/terrortown/gamemode/util.lua
Normal 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
|
||||
32
gamemodes/terrortown/gamemode/vgui/coloredbox.lua
Normal file
32
gamemodes/terrortown/gamemode/vgui/coloredbox.lua
Normal 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" )
|
||||
101
gamemodes/terrortown/gamemode/vgui/progressbar.lua
Normal file
101
gamemodes/terrortown/gamemode/vgui/progressbar.lua
Normal 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" )
|
||||
273
gamemodes/terrortown/gamemode/vgui/sb_info.lua
Normal file
273
gamemodes/terrortown/gamemode/vgui/sb_info.lua
Normal 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")
|
||||
486
gamemodes/terrortown/gamemode/vgui/sb_main.lua
Normal file
486
gamemodes/terrortown/gamemode/vgui/sb_main.lua
Normal 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" )
|
||||
426
gamemodes/terrortown/gamemode/vgui/sb_row.lua
Normal file
426
gamemodes/terrortown/gamemode/vgui/sb_row.lua
Normal 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" )
|
||||
200
gamemodes/terrortown/gamemode/vgui/sb_team.lua
Normal file
200
gamemodes/terrortown/gamemode/vgui/sb_team.lua
Normal 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")
|
||||
88
gamemodes/terrortown/gamemode/vgui/scrolllabel.lua
Normal file
88
gamemodes/terrortown/gamemode/vgui/scrolllabel.lua
Normal 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")
|
||||
240
gamemodes/terrortown/gamemode/vgui/simpleicon.lua
Normal file
240
gamemodes/terrortown/gamemode/vgui/simpleicon.lua
Normal 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")
|
||||
520
gamemodes/terrortown/gamemode/weaponry.lua
Normal file
520
gamemodes/terrortown/gamemode/weaponry.lua
Normal 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
|
||||
41
gamemodes/terrortown/gamemode/weaponry_shd.lua
Normal file
41
gamemodes/terrortown/gamemode/weaponry_shd.lua
Normal 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
|
||||
Reference in New Issue
Block a user