mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-16 21:33:46 +03:00
979 lines
29 KiB
Lua
979 lines
29 KiB
Lua
--[[
|
|
| This file was obtained through the combined efforts
|
|
| of Madbluntz & Plymouth Antiquarian Society.
|
|
|
|
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
|
|
| Maloy, DrPepper10 @ RIP, Atle!
|
|
|
|
|
| Visit for more: https://plymouth.thetwilightzone.ru/
|
|
--]]
|
|
|
|
---- 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)
|