Last active
January 21, 2020 01:03
-
-
Save joetsoi/7266280 to your computer and use it in GitHub Desktop.
Simple trouble in terrorist town hack that adds new variable ttt_traitor_pct_max . Each round, the percentage of traitors is random between the range ttt_traitor_pct and ttt_traitor_pct_max. Defaults to 0.5. Replace GarrysMod\garrysmod\gamemodes\terrortown\gamemode\init.lua with this file. (e.g C:\Program Files (x86)\Steam\steamapps\common\Garry…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
---- 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("scoring_shd.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_pct_max", "0.5") | |
CreateConVar("ttt_traitor_max", "32") | |
CreateConVar("ttt_detective_pct", "0.13", FCVAR_NOTIFY) | |
CreateConVar("ttt_detective_max", "32") | |
CreateConVar("ttt_detective_min_players", "8") | |
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_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 = true | |
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 ipairs(player.GetAll()) 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 ipairs(player.GetAll()) 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 ipairs(player.GetAll()) 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() | |
et.FixParentedPostCleanup() | |
-- Strip players now, so that their weapons are not seen by ReplaceEntities | |
for k,v in ipairs(player.GetAll()) 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 | |
-- 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 | |
-- Cleanup | |
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 plys = player.GetAll() | |
local traitornicks = {} | |
for k,v in ipairs(plys) 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 ipairs(plys) 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 plys = player.GetAll() | |
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 ipairs(plys) 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(plys) 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) | |
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 ipairs(player.GetAll()) 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 the difference between ttt_traitor_pct and traitor_pct_max as a positive real number | |
local traitor_pct_range = math.abs((GetConVar("ttt_traitor_pct_max"):GetFloat() - GetConVar('ttt_traitor_pct'):GetFloat()) * 100) | |
-- choose a random value in the traitor percentage range | |
local traitor_random = (math.random(0, traitor_pct_range) + 1) / 100 | |
-- get number of traitors: pct of players rounded down (with our additional random traitors) | |
local traitor_count = math.floor(ply_count * (GetConVar("ttt_traitor_pct"):GetFloat() + traitor_random)) | |
-- 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 | |
local plys = player.GetAll() | |
for k,v in ipairs(plys) 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:SteamID()] 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 = GetConVarNumber("ttt_detective_karma_min") or 0 | |
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 ipairs(plys) do | |
-- initialize credit count for everyone based on their role | |
ply:SetDefaultCredits() | |
-- store a steamid -> role map | |
GAMEMODE.LastRole[ply:SteamID()] = 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment