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);