Metagame improvements to antag-before-job selection system (#35830)
* Initial commit * Update the selection to only count for people who have one of the preferences assigned; latejoin on delay no longer applies pre-selection.
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Events;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Ghost.Roles;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
@@ -65,6 +66,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
|
||||
SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
|
||||
|
||||
SubscribeLocalEvent<NoJobsAvailableSpawningEvent>(OnJobNotAssigned);
|
||||
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
|
||||
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
|
||||
@@ -136,6 +138,28 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
}
|
||||
}
|
||||
|
||||
private void OnJobNotAssigned(NoJobsAvailableSpawningEvent args)
|
||||
{
|
||||
// If someone fails to spawn in due to there being no jobs, they should be removed from any preselected antags.
|
||||
// We only care about delayed rules, since if they're active the player should have already been removed via MakeAntag.
|
||||
var query = QueryDelayedRules();
|
||||
while (query.MoveNext(out var uid, out _, out var comp, out _))
|
||||
{
|
||||
if (comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
|
||||
continue;
|
||||
|
||||
if (!comp.RemoveUponFailedSpawn)
|
||||
continue;
|
||||
|
||||
foreach (var def in comp.Definitions)
|
||||
{
|
||||
if (!comp.PreSelectedSessions.TryGetValue(def, out var session))
|
||||
break;
|
||||
session.Remove(args.Player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSpawnComplete(PlayerSpawnCompleteEvent args)
|
||||
{
|
||||
if (!args.LateJoin)
|
||||
@@ -149,8 +173,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
var rules = new List<(EntityUid, AntagSelectionComponent)>();
|
||||
while (query.MoveNext(out var uid, out var antag, out _))
|
||||
{
|
||||
if (HasComp<ActiveGameRuleComponent>(uid) ||
|
||||
(HasComp<DelayedStartRuleComponent>(uid) && antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn)) //IntraPlayerSpawn selects antags before spawning, but doesn't activate until after.
|
||||
if (HasComp<ActiveGameRuleComponent>(uid))
|
||||
rules.Add((uid, antag));
|
||||
}
|
||||
RobustRandom.Shuffle(rules);
|
||||
@@ -171,9 +194,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
|
||||
continue;
|
||||
|
||||
var onlyPreSelect = (antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn && !antag.AssignmentComplete); // Don't wanna give them antag status if the rule hasn't assigned its existing ones yet
|
||||
|
||||
if (TryMakeAntag((uid, antag), args.Player, def.Value, onlyPreSelect: onlyPreSelect))
|
||||
if (TryMakeAntag((uid, antag), args.Player, def.Value))
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -209,16 +230,12 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (component.AssignmentComplete)
|
||||
return;
|
||||
|
||||
if (!component.PreSelectionsComplete)
|
||||
{
|
||||
var players = _playerManager.Sessions
|
||||
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) &&
|
||||
status == PlayerGameStatus.JoinedGame)
|
||||
.ToList();
|
||||
|
||||
ChooseAntags((uid, component), players, midround: true);
|
||||
}
|
||||
|
||||
AssignPreSelectedSessions((uid, component));
|
||||
}
|
||||
|
||||
@@ -230,9 +247,6 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
|
||||
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
|
||||
{
|
||||
if (ent.Comp.PreSelectionsComplete)
|
||||
return;
|
||||
|
||||
foreach (var def in ent.Comp.Definitions)
|
||||
{
|
||||
ChooseAntags(ent, pool, def, midround: midround);
|
||||
@@ -254,7 +268,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
bool midround = false)
|
||||
{
|
||||
var playerPool = GetPlayerPool(ent, pool, def);
|
||||
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def);
|
||||
var existingAntagCount = ent.Comp.PreSelectedSessions.TryGetValue(def, out var existingAntags) ? existingAntags.Count : 0;
|
||||
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def) - existingAntagCount;
|
||||
|
||||
// if there is both a spawner and players getting picked, let it fall back to a spawner.
|
||||
var noSpawner = def.SpawnerPrototype == null;
|
||||
@@ -327,6 +342,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
/// </summary>
|
||||
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true, bool onlyPreSelect = false)
|
||||
{
|
||||
_adminLogger.Add(LogType.AntagSelection, $"Start trying to make {session} become the antagonist: {ToPrettyString(ent)}");
|
||||
|
||||
if (checkPref && !HasPrimaryAntagPreference(session, def))
|
||||
return false;
|
||||
|
||||
@@ -384,7 +401,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (antagEnt is not { } player)
|
||||
{
|
||||
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
|
||||
if (session != null)
|
||||
_adminLogger.Add(LogType.AntagSelection,$"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
|
||||
if (session != null && ent.Comp.RemoveUponFailedSpawn)
|
||||
{
|
||||
ent.Comp.AssignedSessions.Remove(session);
|
||||
ent.Comp.PreSelectedSessions[def].Remove(session);
|
||||
@@ -414,6 +432,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp))
|
||||
{
|
||||
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
|
||||
_adminLogger.Add(LogType.AntagSelection,$"Antag spawner {player} in gamerule {ToPrettyString(ent)} failed due to not having GhostRoleAntagSpawnerComponent.");
|
||||
if (session != null)
|
||||
{
|
||||
ent.Comp.AssignedSessions.Remove(session);
|
||||
@@ -475,6 +494,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session.AttachedEntity, def))
|
||||
continue;
|
||||
|
||||
if (ent.Comp.PreSelectedSessions.TryGetValue(def, out var preSelected) && preSelected.Contains(session))
|
||||
continue;
|
||||
|
||||
if (HasPrimaryAntagPreference(session, def))
|
||||
{
|
||||
preferredList.Add(session);
|
||||
|
||||
@@ -61,6 +61,13 @@ public sealed partial class AntagSelectionComponent : Component
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId? AgentName;
|
||||
|
||||
/// <summary>
|
||||
/// If the player is pre-selected but fails to spawn in (e.g. due to only having antag-immune jobs selected),
|
||||
/// should they be removed from the pre-selection list?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RemoveUponFailedSpawn = true;
|
||||
}
|
||||
|
||||
[DataDefinition]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.GameTicking.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Raised on players who attempt to spawn in but fail to get a job, due to there not being any job slots available.
|
||||
/// </summary>
|
||||
public readonly record struct NoJobsAvailableSpawningEvent(ICommonSession Player);
|
||||
@@ -98,6 +98,9 @@ namespace Content.Server.GameTicking
|
||||
if (job == null)
|
||||
{
|
||||
var playerSession = _playerManager.GetSessionById(netUser);
|
||||
var evNoJobs = new NoJobsAvailableSpawningEvent(playerSession); // Used by gamerules to wipe their antag slot, if they got one
|
||||
RaiseLocalEvent(evNoJobs);
|
||||
|
||||
_chatManager.DispatchServerMessage(playerSession, Loc.GetString("job-not-available-wait-in-lobby"));
|
||||
}
|
||||
else
|
||||
@@ -209,6 +212,9 @@ namespace Content.Server.GameTicking
|
||||
JoinAsObserver(player);
|
||||
}
|
||||
|
||||
var evNoJobs = new NoJobsAvailableSpawningEvent(player); // Used by gamerules to wipe their antag slot, if they got one
|
||||
RaiseLocalEvent(evNoJobs);
|
||||
|
||||
_chatManager.DispatchServerMessage(player,
|
||||
Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
|
||||
return;
|
||||
|
||||
@@ -34,6 +34,15 @@
|
||||
- id: Thief
|
||||
prob: 0.5
|
||||
|
||||
- type: entity
|
||||
parent: BaseGameRule
|
||||
id: DummyNonAntagChance
|
||||
components:
|
||||
- type: SubGamemodes
|
||||
rules:
|
||||
- id: DummyNonAntag
|
||||
prob: 0.3
|
||||
|
||||
- type: entity
|
||||
id: DeathMatch31
|
||||
parent: BaseGameRule
|
||||
@@ -333,6 +342,22 @@
|
||||
mindRoles:
|
||||
- MindRoleInitialInfected
|
||||
|
||||
# This rule makes the chosen players unable to get other antag rules, as a way to prevent metagaming job rolls.
|
||||
# Put this before antags assigned to station jobs, but after non-job antags (NukeOps/Wiz).
|
||||
- type: entity
|
||||
id: DummyNonAntag
|
||||
parent: BaseGameRule
|
||||
components:
|
||||
- type: GameRule
|
||||
minPlayers: 5
|
||||
- type: AntagSelection
|
||||
selectionTime: IntraPlayerSpawn # Pre-selection before jobs; assignment doesn't really matter though, we only care about the pre-selection to block other antags.
|
||||
removeUponFailedSpawn: false
|
||||
definitions:
|
||||
- prefRoles: [ InitialInfected, Traitor, Thief, HeadRev ]
|
||||
max: 2
|
||||
playerRatio: 30
|
||||
|
||||
# event schedulers
|
||||
|
||||
- type: entityTable
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
description: traitor-description
|
||||
showInVote: false
|
||||
rules:
|
||||
- DummyNonAntagChance
|
||||
- Traitor
|
||||
- SubGamemodesRule
|
||||
- BasicStationEventScheduler
|
||||
@@ -178,6 +179,7 @@
|
||||
showInVote: false
|
||||
rules:
|
||||
- Nukeops
|
||||
- DummyNonAntagChance
|
||||
- SubGamemodesRule
|
||||
- BasicStationEventScheduler
|
||||
- MeteorSwarmScheduler
|
||||
@@ -194,6 +196,7 @@
|
||||
description: rev-description
|
||||
showInVote: false
|
||||
rules:
|
||||
- DummyNonAntagChance
|
||||
- Revolutionary
|
||||
- SubGamemodesRule
|
||||
- BasicStationEventScheduler
|
||||
@@ -210,6 +213,7 @@
|
||||
showInVote: false
|
||||
rules:
|
||||
- Wizard
|
||||
- DummyNonAntagChance
|
||||
- SubGamemodesRuleNoWizard #No Dual Wizards at the start, midround is fine
|
||||
- BasicStationEventScheduler
|
||||
- MeteorSwarmScheduler
|
||||
|
||||
Reference in New Issue
Block a user