diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 6c0357e5c0..807fec5816 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -357,6 +357,8 @@ namespace Content.Server.GameTicking RoundNumberMetric.Inc(); + PlayersJoinedRoundNormally = 0; + RunLevel = GameRunLevel.PreRoundLobby; LobbySong = _robustRandom.Pick(_lobbyMusicCollection.PickFiles).ToString(); RandomizeLobbyBackground(); diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index 4ca295f2ce..4707f252f3 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -28,6 +28,12 @@ namespace Content.Server.GameTicking [ViewVariables(VVAccess.ReadWrite), Obsolete("Due for removal when observer spawning is refactored.")] private EntityCoordinates _spawnPoint; + /// + /// How many players have joined the round through normal methods. + /// Useful for game rules to look at. Doesn't count observers, people in lobby, etc. + /// + public int PlayersJoinedRoundNormally = 0; + // Mainly to avoid allocations. private readonly List _possiblePositions = new(); @@ -193,7 +199,8 @@ namespace Content.Server.GameTicking } // We raise this event directed to the mob, but also broadcast it so game rules can do something now. - var aev = new PlayerSpawnCompleteEvent(mob, player, jobId, lateJoin, station, character); + PlayersJoinedRoundNormally++; + var aev = new PlayerSpawnCompleteEvent(mob, player, jobId, lateJoin, PlayersJoinedRoundNormally, station, character); RaiseLocalEvent(mob, aev, true); } @@ -317,7 +324,10 @@ namespace Content.Server.GameTicking public EntityUid Station { get; } public HumanoidCharacterProfile Profile { get; } - public PlayerSpawnCompleteEvent(EntityUid mob, IPlayerSession player, string? jobId, bool lateJoin, EntityUid station, HumanoidCharacterProfile profile) + // Ex. If this is the 27th person to join, this will be 27. + public int JoinOrder { get; } + + public PlayerSpawnCompleteEvent(EntityUid mob, IPlayerSession player, string? jobId, bool lateJoin, int joinOrder, EntityUid station, HumanoidCharacterProfile profile) { Mob = mob; Player = player; @@ -325,6 +335,7 @@ namespace Content.Server.GameTicking LateJoin = lateJoin; Station = station; Profile = profile; + JoinOrder = joinOrder; } } } diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index 575cbae5cd..2676b2dd5b 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -1,7 +1,5 @@ -using System.Collections.Generic; using System.Linq; using Content.Server.Chat.Managers; -using Content.Server.GameTicking.Rules.Configurations; using Content.Server.Objectives.Interfaces; using Content.Server.Players; using Content.Server.Roles; @@ -42,12 +40,16 @@ public sealed class TraitorRuleSystem : GameRuleSystem public int TotalTraitors => Traitors.Count; public string[] Codewords = new string[3]; + private int _playersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor); + private int _maxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors); + public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStartAttempt); SubscribeLocalEvent(OnPlayersSpawned); + SubscribeLocalEvent(HandleLatejoin); SubscribeLocalEvent(OnRoundEndText); } @@ -107,9 +109,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem if (!RuleAdded) return; - var playersPerTraitor = _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor); - var maxTraitors = _cfg.GetCVar(CCVars.TraitorMaxTraitors); - var numTraitors = MathHelper.Clamp(ev.Players.Length / playersPerTraitor, 1, maxTraitors); + var numTraitors = MathHelper.Clamp(ev.Players.Length / _playersPerTraitor, 1, _maxTraitors); + var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount); var traitorPool = FindPotentialTraitors(ev); var selectedTraitors = PickTraitors(numTraitors, traitorPool); @@ -154,7 +155,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem Logger.InfoS("preset", "Insufficient ready players to fill up with traitors, stopping the selection."); return results; } - + for (var i = 0; i < traitorCount; i++) { results.Add(_random.PickAndTake(prefList)); @@ -211,6 +212,48 @@ public sealed class TraitorRuleSystem : GameRuleSystem return true; } + private void HandleLatejoin(PlayerSpawnCompleteEvent ev) + { + if (!RuleAdded) + return; + if (TotalTraitors >= _maxTraitors) + return; + if (!ev.LateJoin) + return; + if (!ev.Profile.AntagPreferences.Contains(TraitorPrototypeID)) + return; + + + if (ev.JobId == null || !_prototypeManager.TryIndex(ev.JobId, out var job)) + return; + + if (!job.CanBeAntag) + return; + + // the nth player we adjust our probabilities around + int target = ((_playersPerTraitor * TotalTraitors) + 1); + + float chance = (1f / _playersPerTraitor); + + /// If we have too many traitors, divide by how many players below target for next traitor we are. + if (ev.JoinOrder < target) + { + chance /= (target - ev.JoinOrder); + } else // Tick up towards 100% chance. + { + chance *= ((ev.JoinOrder + 1) - target); + } + if (chance > 1) + chance = 1; + + // Now that we've calculated our chance, roll and make them a traitor if we roll under. + // You get one shot. + if (_random.Prob((float) chance)) + { + MakeTraitor(ev.Player); + } + } + private void OnRoundEndText(RoundEndTextAppendEvent ev) { if (!RuleAdded) diff --git a/Content.Server/Objectives/Conditions/KillPersonCondition.cs b/Content.Server/Objectives/Conditions/KillPersonCondition.cs index 88ce4a7469..3e1366b108 100644 --- a/Content.Server/Objectives/Conditions/KillPersonCondition.cs +++ b/Content.Server/Objectives/Conditions/KillPersonCondition.cs @@ -29,7 +29,7 @@ namespace Content.Server.Objectives.Conditions public string Description => Loc.GetString("objective-condition-kill-person-description"); - public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResourcePath("Objects/Weapons/Guns/Pistols/mk58_wood.rsi"), "icon"); + public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResourcePath("Objects/Weapons/Guns/Pistols/viper.rsi"), "icon"); public float Progress => (Target?.CharacterDeadIC ?? true) ? 1f : 0f; diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index e9aa325fab..588469433d 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -246,7 +246,7 @@ namespace Content.Shared.CCVar CVarDef.Create("suspicion.min_traitors", 2); public static readonly CVarDef SuspicionPlayersPerTraitor = - CVarDef.Create("suspicion.players_per_traitor", 5); + CVarDef.Create("suspicion.players_per_traitor", 6); public static readonly CVarDef SuspicionStartingBalance = CVarDef.Create("suspicion.starting_balance", 20); @@ -262,7 +262,7 @@ namespace Content.Shared.CCVar CVarDef.Create("traitor.min_players", 5); public static readonly CVarDef TraitorMaxTraitors = - CVarDef.Create("traitor.max_traitors", 7); + CVarDef.Create("traitor.max_traitors", 12); // Assuming average server maxes somewhere from like 50-80 people public static readonly CVarDef TraitorPlayersPerTraitor = CVarDef.Create("traitor.players_per_traitor", 5);