Mega Antag Refactor (#25786)
* Mega Antag Refactor * last minute delta save * more workshopping * more shit * ok tested this for once * okkkkk sure * generic delays for starting rules * well darn * nukies partially * ouagh * ballin' faded and smonkin wed * obliterated the diff * Spread my arms and soak up congratulations * I've got plenty of love, but nothing to show for it * but there’s too much sunlight Shining on my laptop monitor, so I Can’t see anything with any amount of clarity * ok this junk * OOK! * fubar * most of sloth's review * oh boy * eek * hell yea! * ASDFJASDJFvsakcvjkzjnhhhyh
This commit is contained in:
@@ -108,8 +108,11 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
||||
/// This should not be used if the entity is owned by the server. The server will otherwise
|
||||
/// override this with the appearance data it sends over.
|
||||
/// </remarks>
|
||||
public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null)
|
||||
public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (profile == null)
|
||||
return;
|
||||
|
||||
if (!Resolve(uid, ref humanoid))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -112,22 +112,38 @@ public sealed class NukeOpsTest
|
||||
|
||||
// The game rule exists, and all the stations/shuttles/maps are properly initialized
|
||||
var rule = entMan.AllComponents<NukeopsRuleComponent>().Single().Component;
|
||||
Assert.That(entMan.EntityExists(rule.NukieOutpost));
|
||||
Assert.That(entMan.EntityExists(rule.NukieShuttle));
|
||||
var mapRule = entMan.AllComponents<LoadMapRuleComponent>().Single().Component;
|
||||
foreach (var grid in mapRule.MapGrids)
|
||||
{
|
||||
Assert.That(entMan.EntityExists(grid));
|
||||
Assert.That(entMan.HasComponent<MapGridComponent>(grid));
|
||||
Assert.That(entMan.HasComponent<StationMemberComponent>(grid));
|
||||
}
|
||||
Assert.That(entMan.EntityExists(rule.TargetStation));
|
||||
|
||||
Assert.That(entMan.HasComponent<MapGridComponent>(rule.NukieOutpost));
|
||||
Assert.That(entMan.HasComponent<MapGridComponent>(rule.NukieShuttle));
|
||||
|
||||
Assert.That(entMan.HasComponent<StationMemberComponent>(rule.NukieOutpost));
|
||||
Assert.That(entMan.HasComponent<StationDataComponent>(rule.TargetStation));
|
||||
|
||||
var nukieStation = entMan.GetComponent<StationMemberComponent>(rule.NukieOutpost!.Value);
|
||||
var nukieShuttlEnt = entMan.AllComponents<NukeOpsShuttleComponent>().FirstOrDefault().Uid;
|
||||
Assert.That(entMan.EntityExists(nukieShuttlEnt));
|
||||
|
||||
EntityUid? nukieStationEnt = null;
|
||||
foreach (var grid in mapRule.MapGrids)
|
||||
{
|
||||
if (entMan.HasComponent<StationMemberComponent>(grid))
|
||||
{
|
||||
nukieStationEnt = grid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.That(entMan.EntityExists(nukieStationEnt));
|
||||
var nukieStation = entMan.GetComponent<StationMemberComponent>(nukieStationEnt!.Value);
|
||||
|
||||
Assert.That(entMan.EntityExists(nukieStation.Station));
|
||||
Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation));
|
||||
|
||||
Assert.That(server.MapMan.MapExists(rule.NukiePlanet));
|
||||
var nukieMap = mapSys.GetMap(rule.NukiePlanet!.Value);
|
||||
Assert.That(server.MapMan.MapExists(mapRule.Map));
|
||||
var nukieMap = mapSys.GetMap(mapRule.Map!.Value);
|
||||
|
||||
var targetStation = entMan.GetComponent<StationDataComponent>(rule.TargetStation!.Value);
|
||||
var targetGrid = targetStation.Grids.First();
|
||||
@@ -135,8 +151,8 @@ public sealed class NukeOpsTest
|
||||
Assert.That(targetMap, Is.Not.EqualTo(nukieMap));
|
||||
|
||||
Assert.That(entMan.GetComponent<TransformComponent>(player).MapUid, Is.EqualTo(nukieMap));
|
||||
Assert.That(entMan.GetComponent<TransformComponent>(rule.NukieOutpost!.Value).MapUid, Is.EqualTo(nukieMap));
|
||||
Assert.That(entMan.GetComponent<TransformComponent>(rule.NukieShuttle!.Value).MapUid, Is.EqualTo(nukieMap));
|
||||
Assert.That(entMan.GetComponent<TransformComponent>(nukieStationEnt.Value).MapUid, Is.EqualTo(nukieMap));
|
||||
Assert.That(entMan.GetComponent<TransformComponent>(nukieShuttlEnt).MapUid, Is.EqualTo(nukieMap));
|
||||
|
||||
// The maps are all map-initialized, including the player
|
||||
// Yes, this is necessary as this has repeatedly been broken somehow.
|
||||
@@ -149,8 +165,8 @@ public sealed class NukeOpsTest
|
||||
Assert.That(LifeStage(player), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||
Assert.That(LifeStage(nukieMap), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||
Assert.That(LifeStage(targetMap), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||
Assert.That(LifeStage(rule.NukieOutpost), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||
Assert.That(LifeStage(rule.NukieShuttle), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||
Assert.That(LifeStage(nukieStationEnt.Value), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||
Assert.That(LifeStage(nukieShuttlEnt), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||
Assert.That(LifeStage(rule.TargetStation), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||
|
||||
// Make sure the player has hands. We've had fucking disarmed nukies before.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Commands;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Shared.CCVar;
|
||||
|
||||
@@ -17,6 +17,7 @@ public sealed class SecretStartsTest
|
||||
|
||||
var server = pair.Server;
|
||||
await server.WaitIdleAsync();
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var gameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
@@ -32,10 +33,7 @@ public sealed class SecretStartsTest
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var rule in gameTicker.GetAddedGameRules())
|
||||
{
|
||||
Assert.That(gameTicker.GetActiveGameRules(), Does.Contain(rule));
|
||||
}
|
||||
Assert.That(gameTicker.GetAddedGameRules().Count(), Is.GreaterThan(1), $"No additional rules started by secret rule.");
|
||||
|
||||
// End all rules
|
||||
gameTicker.ClearGameRules();
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Administration.Systems;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Presets;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Maps;
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Administration.Commands;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Zombies;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Administration.Systems;
|
||||
|
||||
public sealed partial class AdminVerbSystem
|
||||
{
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly ZombieSystem _zombie = default!;
|
||||
[Dependency] private readonly ThiefRuleSystem _thief = default!;
|
||||
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
|
||||
[Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!;
|
||||
[Dependency] private readonly PiratesRuleSystem _piratesRule = default!;
|
||||
[Dependency] private readonly RevolutionaryRuleSystem _revolutionaryRule = default!;
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string DefaultTraitorRule = "Traitor";
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string DefaultNukeOpRule = "LoneOpsSpawn";
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string DefaultRevsRule = "Revolutionary";
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string DefaultThiefRule = "Thief";
|
||||
|
||||
[ValidatePrototypeId<StartingGearPrototype>]
|
||||
private const string PirateGearId = "PirateGear";
|
||||
|
||||
// All antag verbs have names so invokeverb works.
|
||||
private void AddAntagVerbs(GetVerbsEvent<Verb> args)
|
||||
@@ -40,9 +54,7 @@ public sealed partial class AdminVerbSystem
|
||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"),
|
||||
Act = () =>
|
||||
{
|
||||
// if its a monkey or mouse or something dont give uplink or objectives
|
||||
var isHuman = HasComp<HumanoidAppearanceComponent>(args.Target);
|
||||
_traitorRule.MakeTraitorAdmin(args.Target, giveUplink: isHuman, giveObjectives: isHuman);
|
||||
_antag.ForceMakeAntag<TraitorRuleComponent>(player, DefaultTraitorRule);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = Loc.GetString("admin-verb-make-traitor"),
|
||||
@@ -71,7 +83,7 @@ public sealed partial class AdminVerbSystem
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"),
|
||||
Act = () =>
|
||||
{
|
||||
_nukeopsRule.MakeLoneNukie(args.Target);
|
||||
_antag.ForceMakeAntag<NukeopsRuleComponent>(player, DefaultNukeOpRule);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = Loc.GetString("admin-verb-make-nuclear-operative"),
|
||||
@@ -85,14 +97,14 @@ public sealed partial class AdminVerbSystem
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
_piratesRule.MakePirate(args.Target);
|
||||
// pirates just get an outfit because they don't really have logic associated with them
|
||||
SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = Loc.GetString("admin-verb-make-pirate"),
|
||||
};
|
||||
args.Verbs.Add(pirate);
|
||||
|
||||
//todo come here at some point dear lort.
|
||||
Verb headRev = new()
|
||||
{
|
||||
Text = Loc.GetString("admin-verb-text-make-head-rev"),
|
||||
@@ -100,7 +112,7 @@ public sealed partial class AdminVerbSystem
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"),
|
||||
Act = () =>
|
||||
{
|
||||
_revolutionaryRule.OnHeadRevAdmin(args.Target);
|
||||
_antag.ForceMakeAntag<RevolutionaryRuleComponent>(player, DefaultRevsRule);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = Loc.GetString("admin-verb-make-head-rev"),
|
||||
@@ -114,7 +126,7 @@ public sealed partial class AdminVerbSystem
|
||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
_thief.AdminMakeThief(args.Target, false); //Midround add pacified is bad
|
||||
_antag.ForceMakeAntag<ThiefRuleComponent>(player, DefaultThiefRule);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = Loc.GetString("admin-verb-make-thief"),
|
||||
|
||||
29
Content.Server/Antag/AntagSelectionPlayerPool.cs
Normal file
29
Content.Server/Antag/AntagSelectionPlayerPool.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Antag;
|
||||
|
||||
public sealed class AntagSelectionPlayerPool(params List<ICommonSession>[] sessions)
|
||||
{
|
||||
private readonly List<List<ICommonSession>> _orderedPools = sessions.ToList();
|
||||
|
||||
public bool TryPickAndTake(IRobustRandom random, [NotNullWhen(true)] out ICommonSession? session)
|
||||
{
|
||||
session = null;
|
||||
|
||||
foreach (var pool in _orderedPools)
|
||||
{
|
||||
if (pool.Count == 0)
|
||||
continue;
|
||||
|
||||
session = random.PickAndTake(pool);
|
||||
break;
|
||||
}
|
||||
|
||||
return session != null;
|
||||
}
|
||||
|
||||
public int Count => _orderedPools.Sum(p => p.Count);
|
||||
}
|
||||
302
Content.Server/Antag/AntagSelectionSystem.API.cs
Normal file
302
Content.Server/Antag/AntagSelectionSystem.API.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Objectives;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Mind;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Antag;
|
||||
|
||||
public sealed partial class AntagSelectionSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to get the next non-filled definition based on the current amount of selected minds and other factors.
|
||||
/// </summary>
|
||||
public bool TryGetNextAvailableDefinition(Entity<AntagSelectionComponent> ent,
|
||||
[NotNullWhen(true)] out AntagSelectionDefinition? definition)
|
||||
{
|
||||
definition = null;
|
||||
|
||||
var totalTargetCount = GetTargetAntagCount(ent);
|
||||
var mindCount = ent.Comp.SelectedMinds.Count;
|
||||
if (mindCount >= totalTargetCount)
|
||||
return false;
|
||||
|
||||
foreach (var def in ent.Comp.Definitions)
|
||||
{
|
||||
var target = GetTargetAntagCount(ent, null, def);
|
||||
|
||||
if (mindCount < target)
|
||||
{
|
||||
definition = def;
|
||||
return true;
|
||||
}
|
||||
|
||||
mindCount -= target;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of antagonists that should be present for a given rule based on the provided pool.
|
||||
/// A null pool will simply use the player count.
|
||||
/// </summary>
|
||||
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool = null)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var def in ent.Comp.Definitions)
|
||||
{
|
||||
count += GetTargetAntagCount(ent, pool, def);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of antagonists that should be present for a given antag definition based on the provided pool.
|
||||
/// A null pool will simply use the player count.
|
||||
/// </summary>
|
||||
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool, AntagSelectionDefinition def)
|
||||
{
|
||||
var poolSize = pool?.Count ?? _playerManager.Sessions.Length;
|
||||
// factor in other definitions' affect on the count.
|
||||
var countOffset = 0;
|
||||
foreach (var otherDef in ent.Comp.Definitions)
|
||||
{
|
||||
countOffset += Math.Clamp(poolSize / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio;
|
||||
}
|
||||
// make sure we don't double-count the current selection
|
||||
countOffset -= Math.Clamp((poolSize + countOffset) / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
|
||||
|
||||
return Math.Clamp((poolSize - countOffset) / def.PlayerRatio, def.Min, def.Max);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns identifiable information for all antagonists to be used in a round end summary.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A list containing, in order, the antag's mind, the session data, and the original name stored as a string.
|
||||
/// </returns>
|
||||
public List<(EntityUid, SessionData, string)> GetAntagIdentifiers(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return new List<(EntityUid, SessionData, string)>();
|
||||
|
||||
var output = new List<(EntityUid, SessionData, string)>();
|
||||
foreach (var (mind, name) in ent.Comp.SelectedMinds)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
||||
continue;
|
||||
|
||||
if (!_playerManager.TryGetPlayerData(mindComp.OriginalOwnerUserId.Value, out var data))
|
||||
continue;
|
||||
|
||||
output.Add((mind, data, name));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all the minds of antagonists.
|
||||
/// </summary>
|
||||
public List<Entity<MindComponent>> GetAntagMinds(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return new();
|
||||
|
||||
var output = new List<Entity<MindComponent>>();
|
||||
foreach (var (mind, _) in ent.Comp.SelectedMinds)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
||||
continue;
|
||||
|
||||
output.Add((mind, mindComp));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Helper specifically for <see cref="ObjectivesTextGetInfoEvent"/>
|
||||
/// </remarks>
|
||||
public List<EntityUid> GetAntagMindEntityUids(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return new();
|
||||
|
||||
return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all the antagonists for this rule who are currently alive
|
||||
/// </summary>
|
||||
public IEnumerable<EntityUid> GetAliveAntags(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
yield break;
|
||||
|
||||
var minds = GetAntagMinds(ent);
|
||||
foreach (var mind in minds)
|
||||
{
|
||||
if (_mind.IsCharacterDeadIc(mind))
|
||||
continue;
|
||||
|
||||
if (mind.Comp.OriginalOwnedEntity != null)
|
||||
yield return GetEntity(mind.Comp.OriginalOwnedEntity.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of alive antagonists for this rule.
|
||||
/// </summary>
|
||||
public int GetAliveAntagCount(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return 0;
|
||||
|
||||
var numbah = 0;
|
||||
var minds = GetAntagMinds(ent);
|
||||
foreach (var mind in minds)
|
||||
{
|
||||
if (_mind.IsCharacterDeadIc(mind))
|
||||
continue;
|
||||
|
||||
numbah++;
|
||||
}
|
||||
|
||||
return numbah;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if there are any remaining antagonists alive for this rule.
|
||||
/// </summary>
|
||||
public bool AnyAliveAntags(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return false;
|
||||
|
||||
return GetAliveAntags(ent).Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all the antagonists for this rule are alive.
|
||||
/// </summary>
|
||||
public bool AllAntagsAlive(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return false;
|
||||
|
||||
return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to send the briefing text and sound to a player entity
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity chosen to be antag</param>
|
||||
/// <param name="briefing">The briefing text to send</param>
|
||||
/// <param name="briefingColor">The color the briefing should be, null for default</param>
|
||||
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
|
||||
public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
|
||||
{
|
||||
if (!_mind.TryGetMind(entity, out _, out var mindComponent))
|
||||
return;
|
||||
|
||||
if (mindComponent.Session == null)
|
||||
return;
|
||||
|
||||
SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to send the briefing text and sound to a list of sessions
|
||||
/// </summary>
|
||||
/// <param name="sessions">The sessions that will be sent the briefing</param>
|
||||
/// <param name="briefing">The briefing text to send</param>
|
||||
/// <param name="briefingColor">The color the briefing should be, null for default</param>
|
||||
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
|
||||
[PublicAPI]
|
||||
public void SendBriefing(List<ICommonSession> sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
|
||||
{
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
SendBriefing(session, briefing, briefingColor, briefingSound);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to send the briefing text and sound to a session
|
||||
/// </summary>
|
||||
/// <param name="session">The player chosen to be an antag</param>
|
||||
/// <param name="data">The briefing data</param>
|
||||
public void SendBriefing(
|
||||
ICommonSession? session,
|
||||
BriefingData? data)
|
||||
{
|
||||
if (session == null || data == null)
|
||||
return;
|
||||
|
||||
var text = data.Value.Text == null ? string.Empty : Loc.GetString(data.Value.Text);
|
||||
SendBriefing(session, text, data.Value.Color, data.Value.Sound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to send the briefing text and sound to a session
|
||||
/// </summary>
|
||||
/// <param name="session">The player chosen to be an antag</param>
|
||||
/// <param name="briefing">The briefing text to send</param>
|
||||
/// <param name="briefingColor">The color the briefing should be, null for default</param>
|
||||
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
|
||||
public void SendBriefing(
|
||||
ICommonSession? session,
|
||||
string briefing,
|
||||
Color? briefingColor,
|
||||
SoundSpecifier? briefingSound)
|
||||
{
|
||||
if (session == null)
|
||||
return;
|
||||
|
||||
_audio.PlayGlobal(briefingSound, session);
|
||||
if (!string.IsNullOrEmpty(briefing))
|
||||
{
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing));
|
||||
_chat.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel,
|
||||
briefingColor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This technically is a gamerule-ent-less way to make an entity an antag.
|
||||
/// You should almost never be using this.
|
||||
/// </summary>
|
||||
public void ForceMakeAntag<T>(ICommonSession? player, string defaultRule) where T : Component
|
||||
{
|
||||
var rule = ForceGetGameRuleEnt<T>(defaultRule);
|
||||
|
||||
if (!TryGetNextAvailableDefinition(rule, out var def))
|
||||
def = rule.Comp.Definitions.Last();
|
||||
MakeAntag(rule, player, def.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to grab one of the weird specific antag gamerule ents or starts a new one.
|
||||
/// This is gross code but also most of this is pretty gross to begin with.
|
||||
/// </summary>
|
||||
public Entity<AntagSelectionComponent> ForceGetGameRuleEnt<T>(string id) where T : Component
|
||||
{
|
||||
var query = EntityQueryEnumerator<T, AntagSelectionComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var comp))
|
||||
{
|
||||
return (uid, comp);
|
||||
}
|
||||
var ruleEnt = GameTicker.AddGameRule(id);
|
||||
RemComp<LoadMapRuleComponent>(ruleEnt);
|
||||
var antag = Comp<AntagSelectionComponent>(ruleEnt);
|
||||
antag.SelectionsComplete = true; // don't do normal selection.
|
||||
GameTicker.StartGameRule(ruleEnt);
|
||||
return (ruleEnt, antag);
|
||||
}
|
||||
}
|
||||
@@ -1,347 +1,444 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Ghost.Roles;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Roles.Jobs;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.Audio;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
using Content.Shared.Chat;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Antag;
|
||||
|
||||
public sealed class AntagSelectionSystem : GameRuleSystem<GameRuleComponent>
|
||||
public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
|
||||
{
|
||||
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
|
||||
[Dependency] private readonly AudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IServerPreferencesManager _pref = default!;
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
[Dependency] private readonly GhostRoleSystem _ghostRole = default!;
|
||||
[Dependency] private readonly JobSystem _jobs = default!;
|
||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
||||
[Dependency] private readonly MapSystem _map = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly RoleSystem _role = default!;
|
||||
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
|
||||
#region Eligible Player Selection
|
||||
/// <summary>
|
||||
/// Get all players that are eligible for an antag role
|
||||
/// </summary>
|
||||
/// <param name="playerSessions">All sessions from which to select eligible players</param>
|
||||
/// <param name="antagPrototype">The prototype to get eligible players for</param>
|
||||
/// <param name="includeAllJobs">Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included</param>
|
||||
/// <param name="acceptableAntags">Should players already selected as antags be eligible</param>
|
||||
/// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
|
||||
/// <param name="customExcludeCondition">A custom condition that each player is tested against, if it returns true the player is excluded from eligibility</param>
|
||||
/// <returns>List of all player entities that match the requirements</returns>
|
||||
public List<EntityUid> GetEligiblePlayers(IEnumerable<ICommonSession> playerSessions,
|
||||
ProtoId<AntagPrototype> antagPrototype,
|
||||
bool includeAllJobs = false,
|
||||
AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive,
|
||||
bool ignorePreferences = false,
|
||||
bool allowNonHumanoids = false,
|
||||
Func<EntityUid?, bool>? customExcludeCondition = null)
|
||||
{
|
||||
var eligiblePlayers = new List<EntityUid>();
|
||||
// arbitrary random number to give late joining some mild interest.
|
||||
public const float LateJoinRandomChance = 0.5f;
|
||||
|
||||
foreach (var player in playerSessions)
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
if (IsPlayerEligible(player, antagPrototype, includeAllJobs, acceptableAntags, ignorePreferences, allowNonHumanoids, customExcludeCondition))
|
||||
eligiblePlayers.Add(player.AttachedEntity!.Value);
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
|
||||
|
||||
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
|
||||
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
|
||||
}
|
||||
|
||||
return eligiblePlayers;
|
||||
private void OnTakeGhostRole(Entity<GhostRoleAntagSpawnerComponent> ent, ref TakeGhostRoleEvent args)
|
||||
{
|
||||
if (args.TookRole)
|
||||
return;
|
||||
|
||||
if (ent.Comp.Rule is not { } rule || ent.Comp.Definition is not { } def)
|
||||
return;
|
||||
|
||||
if (!Exists(rule) || !TryComp<AntagSelectionComponent>(rule, out var select))
|
||||
return;
|
||||
|
||||
MakeAntag((rule, select), args.Player, def, ignoreSpawner: true);
|
||||
args.TookRole = true;
|
||||
_ghostRole.UnregisterGhostRole((ent, Comp<GhostRoleComponent>(ent)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all sessions that are eligible for an antag role, can be run prior to sessions being attached to an entity
|
||||
/// This does not exclude sessions that have already been chosen as antags - that must be handled manually
|
||||
/// </summary>
|
||||
/// <param name="playerSessions">All sessions from which to select eligible players</param>
|
||||
/// <param name="antagPrototype">The prototype to get eligible players for</param>
|
||||
/// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
|
||||
/// <returns>List of all player sessions that match the requirements</returns>
|
||||
public List<ICommonSession> GetEligibleSessions(IEnumerable<ICommonSession> playerSessions, ProtoId<AntagPrototype> antagPrototype, bool ignorePreferences = false)
|
||||
private void OnPlayerSpawning(RulePlayerSpawningEvent args)
|
||||
{
|
||||
var eligibleSessions = new List<ICommonSession>();
|
||||
var pool = args.PlayerPool;
|
||||
|
||||
foreach (var session in playerSessions)
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out var uid, out _, out var comp, out _))
|
||||
{
|
||||
if (IsSessionEligible(session, antagPrototype, ignorePreferences))
|
||||
eligibleSessions.Add(session);
|
||||
}
|
||||
|
||||
return eligibleSessions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test eligibility of the player for a specific antag role
|
||||
/// </summary>
|
||||
/// <param name="session">The player session to test</param>
|
||||
/// <param name="antagPrototype">The prototype to get eligible players for</param>
|
||||
/// <param name="includeAllJobs">Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included</param>
|
||||
/// <param name="acceptableAntags">Should players already selected as antags be eligible</param>
|
||||
/// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
|
||||
/// <param name="customExcludeCondition">A function, accepting an EntityUid and returning bool. Each player is tested against this, returning truw will exclude the player from eligibility</param>
|
||||
/// <returns>True if the player session matches the requirements, false otherwise</returns>
|
||||
public bool IsPlayerEligible(ICommonSession session,
|
||||
ProtoId<AntagPrototype> antagPrototype,
|
||||
bool includeAllJobs = false,
|
||||
AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive,
|
||||
bool ignorePreferences = false,
|
||||
bool allowNonHumanoids = false,
|
||||
Func<EntityUid?, bool>? customExcludeCondition = null)
|
||||
{
|
||||
if (!IsSessionEligible(session, antagPrototype, ignorePreferences))
|
||||
return false;
|
||||
|
||||
//Ensure the player has a mind
|
||||
if (session.GetMind() is not { } playerMind)
|
||||
return false;
|
||||
|
||||
//Ensure the player has an attached entity
|
||||
if (session.AttachedEntity is not { } playerEntity)
|
||||
return false;
|
||||
|
||||
//Ignore latejoined players, ie those on the arrivals station
|
||||
if (HasComp<PendingClockInComponent>(playerEntity))
|
||||
return false;
|
||||
|
||||
//Exclude jobs that cannot be antag, unless explicitly allowed
|
||||
if (!includeAllJobs && !_jobs.CanBeAntag(session))
|
||||
return false;
|
||||
|
||||
//Check if the entity is already an antag
|
||||
switch (acceptableAntags)
|
||||
{
|
||||
//If we dont want to select any antag roles
|
||||
case AntagAcceptability.None:
|
||||
{
|
||||
if (_roleSystem.MindIsAntagonist(playerMind))
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
//If we dont want to select exclusive antag roles
|
||||
case AntagAcceptability.NotExclusive:
|
||||
{
|
||||
if (_roleSystem.MindIsExclusiveAntagonist(playerMind))
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//Unless explictly allowed, ignore non humanoids (eg pets)
|
||||
if (!allowNonHumanoids && !HasComp<HumanoidAppearanceComponent>(playerEntity))
|
||||
return false;
|
||||
|
||||
//If a custom condition was provided, test it and exclude the player if it returns true
|
||||
if (customExcludeCondition != null && customExcludeCondition(playerEntity))
|
||||
return false;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the session is eligible for a role, can be run prior to the session being attached to an entity
|
||||
/// </summary>
|
||||
/// <param name="session">Player session to check</param>
|
||||
/// <param name="antagPrototype">Which antag prototype to check for</param>
|
||||
/// <param name="ignorePreferences">Ignore if the player has enabled this antag</param>
|
||||
/// <returns>True if the session matches the requirements, false otherwise</returns>
|
||||
public bool IsSessionEligible(ICommonSession session, ProtoId<AntagPrototype> antagPrototype, bool ignorePreferences = false)
|
||||
{
|
||||
//Exclude disconnected or zombie sessions
|
||||
//No point giving antag roles to them
|
||||
if (session.Status == SessionStatus.Disconnected ||
|
||||
session.Status == SessionStatus.Zombie)
|
||||
return false;
|
||||
|
||||
//Check the player has this antag preference selected
|
||||
//Unless we are ignoring preferences, in which case add them anyway
|
||||
var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(session.UserId).SelectedCharacter;
|
||||
if (!pref.AntagPreferences.Contains(antagPrototype.Id) && !ignorePreferences)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to calculate the number of antags to select based upon the number of players
|
||||
/// </summary>
|
||||
/// <param name="playerCount">How many players there are on the server</param>
|
||||
/// <param name="playersPerAntag">How many players should there be for an additional antag</param>
|
||||
/// <param name="maxAntags">Maximum number of antags allowed</param>
|
||||
/// <returns>The number of antags that should be chosen</returns>
|
||||
public int CalculateAntagCount(int playerCount, int playersPerAntag, int maxAntags)
|
||||
{
|
||||
return Math.Clamp(playerCount / playersPerAntag, 1, maxAntags);
|
||||
}
|
||||
|
||||
#region Antag Selection
|
||||
/// <summary>
|
||||
/// Selects a set number of entities from several lists, prioritising the first list till its empty, then second list etc
|
||||
/// </summary>
|
||||
/// <param name="eligiblePlayerLists">Array of lists, which are chosen from in order until the correct number of items are selected</param>
|
||||
/// <param name="count">How many items to select</param>
|
||||
/// <returns>Up to the specified count of elements from all provided lists</returns>
|
||||
public List<EntityUid> ChooseAntags(int count, params List<EntityUid>[] eligiblePlayerLists)
|
||||
{
|
||||
var chosenPlayers = new List<EntityUid>();
|
||||
foreach (var playerList in eligiblePlayerLists)
|
||||
{
|
||||
//Remove all chosen players from this list, to prevent duplicates
|
||||
foreach (var chosenPlayer in chosenPlayers)
|
||||
{
|
||||
playerList.Remove(chosenPlayer);
|
||||
}
|
||||
|
||||
//If we have reached the desired number of players, skip
|
||||
if (chosenPlayers.Count >= count)
|
||||
if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn)
|
||||
continue;
|
||||
|
||||
//Pick and choose a random number of players from this list
|
||||
chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList));
|
||||
}
|
||||
return chosenPlayers;
|
||||
}
|
||||
/// <summary>
|
||||
/// Helper method to choose antags from a list
|
||||
/// </summary>
|
||||
/// <param name="eligiblePlayers">List of eligible players</param>
|
||||
/// <param name="count">How many to choose</param>
|
||||
/// <returns>Up to the specified count of elements from the provided list</returns>
|
||||
public List<EntityUid> ChooseAntags(int count, List<EntityUid> eligiblePlayers)
|
||||
if (comp.SelectionsComplete)
|
||||
return;
|
||||
|
||||
ChooseAntags((uid, comp), pool);
|
||||
comp.SelectionsComplete = true;
|
||||
|
||||
foreach (var session in comp.SelectedSessions)
|
||||
{
|
||||
var chosenPlayers = new List<EntityUid>();
|
||||
args.PlayerPool.Remove(session);
|
||||
GameTicker.PlayerJoinGame(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnJobsAssigned(RulePlayerJobsAssignedEvent args)
|
||||
{
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out var uid, out _, out var comp, out _))
|
||||
{
|
||||
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
|
||||
continue;
|
||||
|
||||
if (comp.SelectionsComplete)
|
||||
continue;
|
||||
|
||||
ChooseAntags((uid, comp));
|
||||
comp.SelectionsComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSpawnComplete(PlayerSpawnCompleteEvent args)
|
||||
{
|
||||
if (!args.LateJoin)
|
||||
return;
|
||||
|
||||
// TODO: this really doesn't handle multiple latejoin definitions well
|
||||
// eventually this should probably store the players per definition with some kind of unique identifier.
|
||||
// something to figure out later.
|
||||
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out var uid, out _, out var antag, out _))
|
||||
{
|
||||
if (!RobustRandom.Prob(LateJoinRandomChance))
|
||||
continue;
|
||||
|
||||
if (!antag.Definitions.Any(p => p.LateJoinAdditional))
|
||||
continue;
|
||||
|
||||
if (!TryGetNextAvailableDefinition((uid, antag), out var def))
|
||||
continue;
|
||||
|
||||
MakeAntag((uid, antag), args.Player, def.Value);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Added(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
||||
{
|
||||
base.Added(uid, component, gameRule, args);
|
||||
|
||||
for (var i = 0; i < component.Definitions.Count; i++)
|
||||
{
|
||||
var def = component.Definitions[i];
|
||||
|
||||
if (def.MinRange != null)
|
||||
{
|
||||
def.Min = def.MinRange.Value.Next(RobustRandom);
|
||||
}
|
||||
|
||||
if (def.MaxRange != null)
|
||||
{
|
||||
def.Max = def.MaxRange.Value.Next(RobustRandom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Started(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
|
||||
if (component.SelectionsComplete)
|
||||
return;
|
||||
|
||||
if (GameTicker.RunLevel != GameRunLevel.InRound)
|
||||
return;
|
||||
|
||||
if (GameTicker.RunLevel == GameRunLevel.InRound && component.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
|
||||
return;
|
||||
|
||||
ChooseAntags((uid, component));
|
||||
component.SelectionsComplete = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chooses antagonists from the current selection of players
|
||||
/// </summary>
|
||||
public void ChooseAntags(Entity<AntagSelectionComponent> ent)
|
||||
{
|
||||
var sessions = _playerManager.Sessions.ToList();
|
||||
ChooseAntags(ent, sessions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chooses antagonists from the given selection of players
|
||||
/// </summary>
|
||||
public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool)
|
||||
{
|
||||
foreach (var def in ent.Comp.Definitions)
|
||||
{
|
||||
ChooseAntags(ent, pool, def);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chooses antagonists from the given selection of players for the given antag definition.
|
||||
/// </summary>
|
||||
public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool, AntagSelectionDefinition def)
|
||||
{
|
||||
var playerPool = GetPlayerPool(ent, pool, def);
|
||||
var count = GetTargetAntagCount(ent, playerPool, def);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (eligiblePlayers.Count == 0)
|
||||
var session = (ICommonSession?) null;
|
||||
if (def.PickPlayer)
|
||||
{
|
||||
if (!playerPool.TryPickAndTake(RobustRandom, out session))
|
||||
break;
|
||||
|
||||
chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
|
||||
}
|
||||
|
||||
return chosenPlayers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a set number of sessions from several lists, prioritising the first list till its empty, then second list etc
|
||||
/// </summary>
|
||||
/// <param name="eligiblePlayerLists">Array of lists, which are chosen from in order until the correct number of items are selected</param>
|
||||
/// <param name="count">How many items to select</param>
|
||||
/// <returns>Up to the specified count of elements from all provided lists</returns>
|
||||
public List<ICommonSession> ChooseAntags(int count, params List<ICommonSession>[] eligiblePlayerLists)
|
||||
{
|
||||
var chosenPlayers = new List<ICommonSession>();
|
||||
foreach (var playerList in eligiblePlayerLists)
|
||||
{
|
||||
//Remove all chosen players from this list, to prevent duplicates
|
||||
foreach (var chosenPlayer in chosenPlayers)
|
||||
{
|
||||
playerList.Remove(chosenPlayer);
|
||||
}
|
||||
|
||||
//If we have reached the desired number of players, skip
|
||||
if (chosenPlayers.Count >= count)
|
||||
if (ent.Comp.SelectedSessions.Contains(session))
|
||||
continue;
|
||||
|
||||
//Pick and choose a random number of players from this list
|
||||
chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList));
|
||||
}
|
||||
return chosenPlayers;
|
||||
}
|
||||
/// <summary>
|
||||
/// Helper method to choose sessions from a list
|
||||
/// </summary>
|
||||
/// <param name="eligiblePlayers">List of eligible sessions</param>
|
||||
/// <param name="count">How many to choose</param>
|
||||
/// <returns>Up to the specified count of elements from the provided list</returns>
|
||||
public List<ICommonSession> ChooseAntags(int count, List<ICommonSession> eligiblePlayers)
|
||||
{
|
||||
var chosenPlayers = new List<ICommonSession>();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (eligiblePlayers.Count == 0)
|
||||
break;
|
||||
|
||||
chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
|
||||
}
|
||||
|
||||
return chosenPlayers;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Briefings
|
||||
/// <summary>
|
||||
/// Helper method to send the briefing text and sound to a list of entities
|
||||
/// </summary>
|
||||
/// <param name="entities">The players chosen to be antags</param>
|
||||
/// <param name="briefing">The briefing text to send</param>
|
||||
/// <param name="briefingColor">The color the briefing should be, null for default</param>
|
||||
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
|
||||
public void SendBriefing(List<EntityUid> entities, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
SendBriefing(entity, briefing, briefingColor, briefingSound);
|
||||
MakeAntag(ent, session, def);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to send the briefing text and sound to a player entity
|
||||
/// Makes a given player into the specified antagonist.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity chosen to be antag</param>
|
||||
/// <param name="briefing">The briefing text to send</param>
|
||||
/// <param name="briefingColor">The color the briefing should be, null for default</param>
|
||||
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
|
||||
public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
|
||||
public void MakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false)
|
||||
{
|
||||
if (!_mindSystem.TryGetMind(entity, out _, out var mindComponent))
|
||||
var antagEnt = (EntityUid?) null;
|
||||
var isSpawner = false;
|
||||
|
||||
if (session != null)
|
||||
{
|
||||
ent.Comp.SelectedSessions.Add(session);
|
||||
|
||||
// we shouldn't be blocking the entity if they're just a ghost or smth.
|
||||
if (!HasComp<GhostComponent>(session.AttachedEntity))
|
||||
antagEnt = session.AttachedEntity;
|
||||
}
|
||||
else if (!ignoreSpawner && def.SpawnerPrototype != null) // don't add spawners if we have a player, dummy.
|
||||
{
|
||||
antagEnt = Spawn(def.SpawnerPrototype);
|
||||
isSpawner = true;
|
||||
}
|
||||
|
||||
if (!antagEnt.HasValue)
|
||||
{
|
||||
var getEntEv = new AntagSelectEntityEvent(session, ent);
|
||||
RaiseLocalEvent(ent, ref getEntEv, true);
|
||||
|
||||
if (!getEntEv.Handled)
|
||||
{
|
||||
throw new InvalidOperationException($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
|
||||
}
|
||||
|
||||
antagEnt = getEntEv.Entity;
|
||||
}
|
||||
|
||||
if (antagEnt is not { } player)
|
||||
return;
|
||||
|
||||
if (mindComponent.Session == null)
|
||||
return;
|
||||
var getPosEv = new AntagSelectLocationEvent(session, ent);
|
||||
RaiseLocalEvent(ent, ref getPosEv, true);
|
||||
if (getPosEv.Handled)
|
||||
{
|
||||
var playerXform = Transform(player);
|
||||
var pos = RobustRandom.Pick(getPosEv.Coordinates);
|
||||
var mapEnt = _map.GetMap(pos.MapId);
|
||||
_transform.SetMapCoordinates((player, playerXform), pos);
|
||||
}
|
||||
|
||||
SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound);
|
||||
if (isSpawner)
|
||||
{
|
||||
if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp))
|
||||
{
|
||||
Log.Error("Antag spawner with GhostRoleAntagSpawnerComponent.");
|
||||
return;
|
||||
}
|
||||
|
||||
spawnerComp.Rule = ent;
|
||||
spawnerComp.Definition = def;
|
||||
return;
|
||||
}
|
||||
|
||||
EntityManager.AddComponents(player, def.Components);
|
||||
_stationSpawning.EquipStartingGear(player, def.StartingGear);
|
||||
|
||||
if (session != null)
|
||||
{
|
||||
var curMind = session.GetMind();
|
||||
if (curMind == null)
|
||||
{
|
||||
curMind = _mind.CreateMind(session.UserId, Name(antagEnt.Value));
|
||||
_mind.SetUserId(curMind.Value, session.UserId);
|
||||
}
|
||||
|
||||
EntityManager.AddComponents(curMind.Value, def.MindComponents);
|
||||
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
|
||||
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
|
||||
}
|
||||
|
||||
if (def.Briefing is { } briefing)
|
||||
{
|
||||
SendBriefing(session, briefing);
|
||||
}
|
||||
|
||||
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
|
||||
RaiseLocalEvent(ent, ref afterEv, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to send the briefing text and sound to a list of sessions
|
||||
/// Gets an ordered player pool based on player preferences and the antagonist definition.
|
||||
/// </summary>
|
||||
/// <param name="sessions"></param>
|
||||
/// <param name="briefing"></param>
|
||||
/// <param name="briefingColor"></param>
|
||||
/// <param name="briefingSound"></param>
|
||||
|
||||
public void SendBriefing(List<ICommonSession> sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
|
||||
public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, List<ICommonSession> sessions, AntagSelectionDefinition def)
|
||||
{
|
||||
var primaryList = new List<ICommonSession>();
|
||||
var secondaryList = new List<ICommonSession>();
|
||||
var fallbackList = new List<ICommonSession>();
|
||||
var rawList = new List<ICommonSession>();
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
SendBriefing(session, briefing, briefingColor, briefingSound);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Helper method to send the briefing text and sound to a session
|
||||
/// </summary>
|
||||
/// <param name="session">The player chosen to be an antag</param>
|
||||
/// <param name="briefing">The briefing text to send</param>
|
||||
/// <param name="briefingColor">The color the briefing should be, null for default</param>
|
||||
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
|
||||
|
||||
public void SendBriefing(ICommonSession session, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
|
||||
if (!IsSessionValid(ent, session, def) ||
|
||||
!IsEntityValid(session.AttachedEntity, def))
|
||||
{
|
||||
_audioSystem.PlayGlobal(briefingSound, session);
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing));
|
||||
ChatManager.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, briefingColor);
|
||||
rawList.Add(session);
|
||||
continue;
|
||||
}
|
||||
#endregion
|
||||
|
||||
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
|
||||
if (def.PrefRoles.Count == 0 || pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p)))
|
||||
{
|
||||
primaryList.Add(session);
|
||||
}
|
||||
else if (def.PrefRoles.Count == 0 || pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p)))
|
||||
{
|
||||
secondaryList.Add(session);
|
||||
}
|
||||
else
|
||||
{
|
||||
fallbackList.Add(session);
|
||||
}
|
||||
}
|
||||
|
||||
return new AntagSelectionPlayerPool(primaryList, secondaryList, fallbackList, rawList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given session is valid for an antagonist.
|
||||
/// </summary>
|
||||
public bool IsSessionValid(Entity<AntagSelectionComponent> ent, ICommonSession session, AntagSelectionDefinition def, EntityUid? mind = null)
|
||||
{
|
||||
mind ??= session.GetMind();
|
||||
|
||||
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
|
||||
return false;
|
||||
|
||||
if (ent.Comp.SelectedSessions.Contains(session))
|
||||
return false;
|
||||
|
||||
//todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds)
|
||||
|
||||
switch (def.MultiAntagSetting)
|
||||
{
|
||||
case AntagAcceptability.None:
|
||||
{
|
||||
if (_role.MindIsAntagonist(mind))
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case AntagAcceptability.NotExclusive:
|
||||
{
|
||||
if (_role.MindIsExclusiveAntagonist(mind))
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: expand this to allow for more fine antag-selection logic for game rules.
|
||||
if (!_jobs.CanBeAntag(session))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given entity (mind/session not included) is valid for a given antagonist.
|
||||
/// </summary>
|
||||
private bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def)
|
||||
{
|
||||
if (entity == null)
|
||||
return false;
|
||||
|
||||
if (HasComp<PendingClockInComponent>(entity))
|
||||
return false;
|
||||
|
||||
if (!def.AllowNonHumans && !HasComp<HumanoidAppearanceComponent>(entity))
|
||||
return false;
|
||||
|
||||
if (def.Whitelist != null)
|
||||
{
|
||||
if (!def.Whitelist.IsValid(entity.Value, EntityManager))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (def.Blacklist != null)
|
||||
{
|
||||
if (def.Blacklist.IsValid(entity.Value, EntityManager))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised on a game rule entity in order to determine what the antagonist entity will be.
|
||||
/// Only raised if the selected player's current entity is invalid.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
|
||||
{
|
||||
public readonly ICommonSession? Session = Session;
|
||||
|
||||
public bool Handled => Entity != null;
|
||||
|
||||
public EntityUid? Entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised on a game rule entity to determine the location for the antagonist.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
|
||||
{
|
||||
public readonly ICommonSession? Session = Session;
|
||||
|
||||
public bool Handled => Coordinates.Any();
|
||||
|
||||
public List<MapCoordinates> Coordinates = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised on a game rule entity after the setup logic for an antag is complete.
|
||||
/// Used for applying additional more complex setup logic.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct AfterAntagEntitySelectedEvent(ICommonSession? Session, EntityUid EntityUid, Entity<AntagSelectionComponent> GameRule, AntagSelectionDefinition Def);
|
||||
|
||||
189
Content.Server/Antag/Components/AntagSelectionComponent.cs
Normal file
189
Content.Server/Antag/Components/AntagSelectionComponent.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using Content.Server.Administration.Systems;
|
||||
using Content.Server.Destructible.Thresholds;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Antag.Components;
|
||||
|
||||
[RegisterComponent, Access(typeof(AntagSelectionSystem), typeof(AdminVerbSystem))]
|
||||
public sealed partial class AntagSelectionComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Has the primary selection of antagonists finished yet?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool SelectionsComplete;
|
||||
|
||||
/// <summary>
|
||||
/// The definitions for the antagonists
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<AntagSelectionDefinition> Definitions = new();
|
||||
|
||||
/// <summary>
|
||||
/// The minds and original names of the players selected to be antagonists.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<(EntityUid, string)> SelectedMinds = new();
|
||||
|
||||
/// <summary>
|
||||
/// When the antag selection will occur.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn;
|
||||
|
||||
/// <summary>
|
||||
/// Cached sessions of players who are chosen. Used so we don't have to rebuild the pool multiple times in a tick.
|
||||
/// Is not serialized.
|
||||
/// </summary>
|
||||
public HashSet<ICommonSession> SelectedSessions = new();
|
||||
}
|
||||
|
||||
[DataDefinition]
|
||||
public partial struct AntagSelectionDefinition()
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of antagonist roles that are used for selecting which players will be antagonists.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<ProtoId<AntagPrototype>> PrefRoles = new();
|
||||
|
||||
/// <summary>
|
||||
/// Fallback for <see cref="PrefRoles"/>. Useful if you need multiple role preferences for a team antagonist.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<ProtoId<AntagPrototype>> FallbackRoles = new();
|
||||
|
||||
/// <summary>
|
||||
/// Should we allow people who already have an antagonist role?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public AntagAcceptability MultiAntagSetting = AntagAcceptability.None;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum number of this antag.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Min = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of this antag.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Max = 1;
|
||||
|
||||
/// <summary>
|
||||
/// A range used to randomly select <see cref="Min"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public MinMax? MinRange;
|
||||
|
||||
/// <summary>
|
||||
/// A range used to randomly select <see cref="Max"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public MinMax? MaxRange;
|
||||
|
||||
/// <summary>
|
||||
/// a player to antag ratio: used to determine the amount of antags that will be present.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int PlayerRatio = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not players should be picked to inhabit this antag or not.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool PickPlayer = true;
|
||||
|
||||
/// <summary>
|
||||
/// If true, players that latejoin into a round have a chance of being converted into antagonists.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool LateJoinAdditional = false;
|
||||
|
||||
//todo: find out how to do this with minimal boilerplate: filler department, maybe?
|
||||
//public HashSet<ProtoId<JobPrototype>> JobBlacklist = new()
|
||||
|
||||
/// <remarks>
|
||||
/// Mostly just here for legacy compatibility and reducing boilerplate
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public bool AllowNonHumans = false;
|
||||
|
||||
/// <summary>
|
||||
/// A whitelist for selecting which players can become this antag.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// A blacklist for selecting which players can become this antag.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Blacklist;
|
||||
|
||||
/// <summary>
|
||||
/// Components added to the player.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ComponentRegistry Components = new();
|
||||
|
||||
/// <summary>
|
||||
/// Components added to the player's mind.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ComponentRegistry MindComponents = new();
|
||||
|
||||
/// <summary>
|
||||
/// A set of starting gear that's equipped to the player.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<StartingGearPrototype>? StartingGear;
|
||||
|
||||
/// <summary>
|
||||
/// A briefing shown to the player.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public BriefingData? Briefing;
|
||||
|
||||
/// <summary>
|
||||
/// A spawner used to defer the selection of this particular definition.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not the cleanest way of doing this code but it's just an odd specific behavior.
|
||||
/// Sue me.
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public EntProtoId? SpawnerPrototype;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains data used to generate a briefing.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public partial struct BriefingData
|
||||
{
|
||||
/// <summary>
|
||||
/// The text shown
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId? Text;
|
||||
|
||||
/// <summary>
|
||||
/// The color of the text.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Color? Color;
|
||||
|
||||
/// <summary>
|
||||
/// The sound played.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? Sound;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Content.Server.Antag.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Ghost role spawner that creates an antag for the associated gamerule.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(AntagSelectionSystem))]
|
||||
public sealed partial class GhostRoleAntagSpawnerComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public EntityUid? Rule;
|
||||
|
||||
[DataField]
|
||||
public AntagSelectionDefinition? Definition;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Antag.Mimic;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Shared.VendingMachines;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Content.Server.Destructible.Thresholds
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Destructible.Thresholds
|
||||
{
|
||||
[Serializable]
|
||||
[DataDefinition]
|
||||
@@ -9,5 +11,16 @@
|
||||
|
||||
[DataField("max")]
|
||||
public int Max;
|
||||
|
||||
public MinMax(int min, int max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
|
||||
public int Next(IRobustRandom random)
|
||||
{
|
||||
return random.Next(Min, Max + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
namespace Content.Server.GameTicking.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Added to game rules before <see cref="GameRuleStartedEvent"/> and removed before <see cref="GameRuleEndedEvent"/>.
|
||||
@@ -0,0 +1,16 @@
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.GameTicking.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Generic component used to track a gamerule that's start has been delayed.
|
||||
/// </summary>
|
||||
[RegisterComponent, AutoGenerateComponentPause]
|
||||
public sealed partial class DelayedStartRuleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The time at which the rule will start properly.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
|
||||
public TimeSpan RuleStartTime;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
namespace Content.Server.GameTicking.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Added to game rules before <see cref="GameRuleEndedEvent"/>.
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.Destructible.Thresholds;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
namespace Content.Server.GameTicking.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Component attached to all gamerule entities.
|
||||
@@ -20,6 +21,12 @@ public sealed partial class GameRuleComponent : Component
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MinPlayers;
|
||||
|
||||
/// <summary>
|
||||
/// A delay for when the rule the is started and when the starting logic actually runs.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public MinMax? Delay;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Prototypes;
|
||||
@@ -102,6 +102,22 @@ public sealed partial class GameTicker
|
||||
if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
|
||||
return false;
|
||||
|
||||
// If we already have it, then we just skip the delay as it has already happened.
|
||||
if (!RemComp<DelayedStartRuleComponent>(ruleEntity) && ruleData.Delay != null)
|
||||
{
|
||||
var delayTime = TimeSpan.FromSeconds(ruleData.Delay.Value.Next(_robustRandom));
|
||||
|
||||
if (delayTime > TimeSpan.Zero)
|
||||
{
|
||||
_sawmill.Info($"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}");
|
||||
_adminLogger.Add(LogType.EventStarted, $"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}");
|
||||
|
||||
var delayed = EnsureComp<DelayedStartRuleComponent>(ruleEntity);
|
||||
delayed.RuleStartTime = _gameTiming.CurTime + (delayTime);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_allPreviousGameRules.Add((RoundDuration(), id));
|
||||
_sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}");
|
||||
_adminLogger.Add(LogType.EventStarted, $"Started game rule {ToPrettyString(ruleEntity)}");
|
||||
@@ -255,6 +271,18 @@ public sealed partial class GameTicker
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGameRules()
|
||||
{
|
||||
var query = EntityQueryEnumerator<DelayedStartRuleComponent, GameRuleComponent>();
|
||||
while (query.MoveNext(out var uid, out var delay, out var rule))
|
||||
{
|
||||
if (_gameTiming.CurTime < delay.RuleStartTime)
|
||||
continue;
|
||||
|
||||
StartGameRule(uid, rule);
|
||||
}
|
||||
}
|
||||
|
||||
#region Command Implementations
|
||||
|
||||
[AdminCommand(AdminFlags.Fun)]
|
||||
@@ -323,38 +351,3 @@ public sealed partial class GameTicker
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/*
|
||||
/// <summary>
|
||||
/// Raised broadcast when a game rule is selected, but not started yet.
|
||||
/// </summary>
|
||||
public sealed class GameRuleAddedEvent
|
||||
{
|
||||
public GameRulePrototype Rule { get; }
|
||||
|
||||
public GameRuleAddedEvent(GameRulePrototype rule)
|
||||
{
|
||||
Rule = rule;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GameRuleStartedEvent
|
||||
{
|
||||
public GameRulePrototype Rule { get; }
|
||||
|
||||
public GameRuleStartedEvent(GameRulePrototype rule)
|
||||
{
|
||||
Rule = rule;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GameRuleEndedEvent
|
||||
{
|
||||
public GameRulePrototype Rule { get; }
|
||||
|
||||
public GameRuleEndedEvent(GameRulePrototype rule)
|
||||
{
|
||||
Rule = rule;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -133,6 +133,7 @@ namespace Content.Server.GameTicking
|
||||
return;
|
||||
base.Update(frameTime);
|
||||
UpdateRoundFlow(frameTime);
|
||||
UpdateGameRules();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using Content.Server.Maps;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for a game rule that loads a map when activated.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class LoadMapRuleComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public MapId? Map;
|
||||
|
||||
[DataField]
|
||||
public ProtoId<GameMapPrototype>? GameMap ;
|
||||
|
||||
[DataField]
|
||||
public ResPath? MapPath;
|
||||
|
||||
[DataField]
|
||||
public List<EntityUid> MapGrids = new();
|
||||
|
||||
[DataField]
|
||||
public EntityWhitelist? SpawnerWhitelist;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Stores some configuration used by the ninja system.
|
||||
/// Objectives and roundend summary are handled by <see cref="GenericAntagRuleComponent/">.
|
||||
/// Objectives and roundend summary are handled by <see cref="GenericAntagRuleComponent"/>.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(SpaceNinjaSystem))]
|
||||
public sealed partial class NinjaRuleComponent : Component
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
/// <summary>
|
||||
@@ -9,11 +6,5 @@ namespace Content.Server.GameTicking.Rules.Components;
|
||||
/// TODO: Remove once systems can request spawns from the ghost role system directly.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class NukeOperativeSpawnerComponent : Component
|
||||
{
|
||||
[DataField("name", required:true)]
|
||||
public string OperativeName = default!;
|
||||
public sealed partial class NukeOperativeSpawnerComponent : Component;
|
||||
|
||||
[DataField]
|
||||
public NukeopSpawnPreset SpawnDetails = default!;
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
[RegisterComponent]
|
||||
public sealed partial class NukeOpsShuttleComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public EntityUid AssociatedRule;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Server.StationEvents.Events;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.NPC.Prototypes;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))]
|
||||
[RegisterComponent, Access(typeof(NukeopsRuleSystem))]
|
||||
public sealed partial class NukeopsRuleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int PlayersPerOperative = 10;
|
||||
|
||||
[DataField]
|
||||
public int MaxOps = 5;
|
||||
|
||||
/// <summary>
|
||||
/// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event.
|
||||
/// </summary>
|
||||
@@ -52,12 +41,6 @@ public sealed partial class NukeopsRuleComponent : Component
|
||||
[DataField]
|
||||
public TimeSpan EvacShuttleTime = TimeSpan.FromMinutes(3);
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to spawn the nuclear operative outpost. Used by LoneOpsSpawn event.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool SpawnOutpost = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not nukie left their outpost
|
||||
/// </summary>
|
||||
@@ -80,7 +63,7 @@ public sealed partial class NukeopsRuleComponent : Component
|
||||
/// This amount of TC will be given to each nukie
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int WarTCAmountPerNukie = 40;
|
||||
public int WarTcAmountPerNukie = 40;
|
||||
|
||||
/// <summary>
|
||||
/// Delay between war declaration and nuke ops arrival on station map. Gives crew time to prepare
|
||||
@@ -94,50 +77,23 @@ public sealed partial class NukeopsRuleComponent : Component
|
||||
[DataField]
|
||||
public int WarDeclarationMinOps = 4;
|
||||
|
||||
[DataField]
|
||||
public EntProtoId SpawnPointProto = "SpawnPointNukies";
|
||||
|
||||
[DataField]
|
||||
public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
|
||||
|
||||
[DataField]
|
||||
public string OperationName = "Test Operation";
|
||||
|
||||
[DataField]
|
||||
public ProtoId<GameMapPrototype> OutpostMapPrototype = "NukieOutpost";
|
||||
|
||||
[DataField]
|
||||
public WinType WinType = WinType.Neutral;
|
||||
|
||||
[DataField]
|
||||
public List<WinCondition> WinConditions = new ();
|
||||
|
||||
// TODO full game save
|
||||
// TODO: use components, don't just cache entity UIDs
|
||||
// There have been (and probably still are) bugs where these refer to deleted entities from old rounds.
|
||||
// Whenever this gets fixed, update NukiesTest.
|
||||
public EntityUid? NukieOutpost;
|
||||
public EntityUid? NukieShuttle;
|
||||
[DataField]
|
||||
public EntityUid? TargetStation;
|
||||
public MapId? NukiePlanet;
|
||||
|
||||
[DataField]
|
||||
public ProtoId<NpcFactionPrototype> Faction = "Syndicate";
|
||||
|
||||
/// <summary>
|
||||
/// Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
|
||||
/// Path to antagonist alert sound.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<EntityUid, string> OperativeMindPendingData = new();
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<NpcFactionPrototype> Faction;
|
||||
|
||||
[DataField]
|
||||
public NukeopSpawnPreset CommanderSpawnDetails = new() { AntagRoleProto = "NukeopsCommander", GearProto = "SyndicateCommanderGearFull", NamePrefix = "nukeops-role-commander", NameList = "SyndicateNamesElite" };
|
||||
|
||||
[DataField]
|
||||
public NukeopSpawnPreset AgentSpawnDetails = new() { AntagRoleProto = "NukeopsMedic", GearProto = "SyndicateOperativeMedicFull", NamePrefix = "nukeops-role-agent", NameList = "SyndicateNamesNormal" };
|
||||
|
||||
[DataField]
|
||||
public NukeopSpawnPreset OperativeSpawnDetails = new();
|
||||
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
[RegisterComponent, Access(typeof(PiratesRuleSystem))]
|
||||
public sealed partial class PiratesRuleComponent : Component
|
||||
{
|
||||
[ViewVariables]
|
||||
public List<EntityUid> Pirates = new();
|
||||
[ViewVariables]
|
||||
public EntityUid PirateShip = EntityUid.Invalid;
|
||||
[ViewVariables]
|
||||
public HashSet<EntityUid> InitialItems = new();
|
||||
[ViewVariables]
|
||||
public double InitialShipValue;
|
||||
|
||||
/// <summary>
|
||||
/// Path to antagonist alert sound.
|
||||
/// </summary>
|
||||
[DataField("pirateAlertSound")]
|
||||
public SoundSpecifier PirateAlertSound = new SoundPathSpecifier(
|
||||
"/Audio/Ambience/Antag/pirate_start.ogg",
|
||||
AudioParams.Default.WithVolume(4));
|
||||
}
|
||||
@@ -22,43 +22,6 @@ public sealed partial class RevolutionaryRuleComponent : Component
|
||||
[DataField]
|
||||
public TimeSpan TimerWait = TimeSpan.FromSeconds(20);
|
||||
|
||||
/// <summary>
|
||||
/// Stores players minds
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<string, EntityUid> HeadRevs = new();
|
||||
|
||||
[DataField]
|
||||
public ProtoId<AntagPrototype> HeadRevPrototypeId = "HeadRev";
|
||||
|
||||
/// <summary>
|
||||
/// Min players needed for Revolutionary gamemode to start.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public int MinPlayers = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Max Head Revs allowed during selection.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public int MaxHeadRevs = 3;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of Head Revs that will spawn per this amount of players.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public int PlayersPerHeadRev = 15;
|
||||
|
||||
/// <summary>
|
||||
/// The gear head revolutionaries are given on spawn.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<EntProtoId> StartingGear = new()
|
||||
{
|
||||
"Flash",
|
||||
"ClothingEyesGlassesSunglasses"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes after the last head is killed for the shuttle to arrive.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Stores data for <see cref="ThiefRuleSystem/">.
|
||||
/// Stores data for <see cref="ThiefRuleSystem"/>.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(ThiefRuleSystem))]
|
||||
public sealed partial class ThiefRuleComponent : Component
|
||||
@@ -23,42 +22,9 @@ public sealed partial class ThiefRuleComponent : Component
|
||||
[DataField]
|
||||
public float BigObjectiveChance = 0.7f;
|
||||
|
||||
/// <summary>
|
||||
/// Add a Pacified comp to thieves
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool PacifistThieves = true;
|
||||
|
||||
[DataField]
|
||||
public ProtoId<AntagPrototype> ThiefPrototypeId = "Thief";
|
||||
|
||||
[DataField]
|
||||
public float MaxObjectiveDifficulty = 2.5f;
|
||||
|
||||
[DataField]
|
||||
public int MaxStealObjectives = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Things that will be given to thieves
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<EntProtoId> StarterItems = new() { "ToolboxThief", "ClothingHandsChameleonThief" };
|
||||
|
||||
/// <summary>
|
||||
/// All Thieves created by this rule
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<EntityUid> ThievesMinds = new();
|
||||
|
||||
/// <summary>
|
||||
/// Max Thiefs created by rule on roundstart
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MaxAllowThief = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Sound played when making the player a thief via antag control or ghost role
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? GreetingSound = new SoundPathSpecifier("/Audio/Misc/thief_greeting.ogg");
|
||||
}
|
||||
|
||||
@@ -57,4 +57,19 @@ public sealed partial class TraitorRuleComponent : Component
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// The amount of codewords that are selected.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int CodewordCount = 4;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of TC traitors start with.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int StartingBalance = 20;
|
||||
|
||||
[DataField]
|
||||
public int MaxDifficulty = 20;
|
||||
}
|
||||
|
||||
@@ -8,12 +8,6 @@ namespace Content.Server.GameTicking.Rules.Components;
|
||||
[RegisterComponent, Access(typeof(ZombieRuleSystem))]
|
||||
public sealed partial class ZombieRuleComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public Dictionary<string, string> InitialInfectedNames = new();
|
||||
|
||||
[DataField]
|
||||
public ProtoId<AntagPrototype> PatientZeroPrototypeId = "InitialInfected";
|
||||
|
||||
/// <summary>
|
||||
/// When the round will next check for round end.
|
||||
/// </summary>
|
||||
@@ -26,61 +20,9 @@ public sealed partial class ZombieRuleComponent : Component
|
||||
[DataField]
|
||||
public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// The time at which the initial infected will be chosen.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan? StartTime;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum amount of time after the round starts that the initial infected will be chosen.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan MinStartDelay = TimeSpan.FromMinutes(10);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum amount of time after the round starts that the initial infected will be chosen.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan MaxStartDelay = TimeSpan.FromMinutes(15);
|
||||
|
||||
/// <summary>
|
||||
/// The sound that plays when someone becomes an initial infected.
|
||||
/// todo: this should have a unique sound instead of reusing the zombie one.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier InitialInfectedSound = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// The minimum amount of time initial infected have before they start taking infection damage.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum amount of time initial infected have before they start taking damage.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f);
|
||||
|
||||
/// <summary>
|
||||
/// How many players for each initial infected.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int PlayersPerInfected = 10;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of initial infected.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MaxInitialInfected = 6;
|
||||
|
||||
/// <summary>
|
||||
/// After this amount of the crew become zombies, the shuttle will be automatically called.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float ZombieShuttleCallPercentage = 0.7f;
|
||||
|
||||
[DataField]
|
||||
public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Commands;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.KillTracking;
|
||||
using Content.Server.Mind;
|
||||
@@ -33,7 +34,6 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
|
||||
SubscribeLocalEvent<KillReportedEvent>(OnKillReported);
|
||||
SubscribeLocalEvent<DeathMatchRuleComponent, PlayerPointChangedEvent>(OnPointChanged);
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextAppend);
|
||||
}
|
||||
|
||||
private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev)
|
||||
@@ -113,21 +113,17 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
|
||||
_roundEnd.EndRound(component.RestartDelay);
|
||||
}
|
||||
|
||||
private void OnRoundEndTextAppend(RoundEndTextAppendEvent ev)
|
||||
protected override void AppendRoundEndText(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args)
|
||||
{
|
||||
var query = EntityQueryEnumerator<DeathMatchRuleComponent, PointManagerComponent, GameRuleComponent>();
|
||||
while (query.MoveNext(out var uid, out var dm, out var point, out var rule))
|
||||
{
|
||||
if (!GameTicker.IsGameRuleAdded(uid, rule))
|
||||
continue;
|
||||
if (!TryComp<PointManagerComponent>(uid, out var point))
|
||||
return;
|
||||
|
||||
if (dm.Victor != null && _player.TryGetPlayerData(dm.Victor.Value, out var data))
|
||||
if (component.Victor != null && _player.TryGetPlayerData(component.Victor.Value, out var data))
|
||||
{
|
||||
ev.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName)));
|
||||
ev.AddLine("");
|
||||
}
|
||||
ev.AddLine(Loc.GetString("point-scoreboard-header"));
|
||||
ev.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup());
|
||||
args.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName)));
|
||||
args.AddLine("");
|
||||
}
|
||||
args.AddLine(Loc.GetString("point-scoreboard-header"));
|
||||
args.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Robust.Shared.Collections;
|
||||
@@ -15,31 +16,6 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
|
||||
return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
|
||||
}
|
||||
|
||||
protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName)
|
||||
{
|
||||
var query = EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
|
||||
while (query.MoveNext(out _, out _, out _, out var gameRule))
|
||||
{
|
||||
var minPlayers = gameRule.MinPlayers;
|
||||
if (!ev.Forced && ev.Players.Length < minPlayers)
|
||||
{
|
||||
ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
|
||||
("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers),
|
||||
("presetName", localizedPresetName)));
|
||||
ev.Cancel();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ev.Players.Length == 0)
|
||||
{
|
||||
ChatManager.DispatchServerAnnouncement(Loc.GetString("preset-no-one-ready"));
|
||||
ev.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
return !ev.Cancelled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility function for finding a random event-eligible station entity
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -22,9 +22,31 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
||||
SubscribeLocalEvent<T, GameRuleAddedEvent>(OnGameRuleAdded);
|
||||
SubscribeLocalEvent<T, GameRuleStartedEvent>(OnGameRuleStarted);
|
||||
SubscribeLocalEvent<T, GameRuleEndedEvent>(OnGameRuleEnded);
|
||||
SubscribeLocalEvent<T, RoundEndTextAppendEvent>(OnRoundEndTextAppend);
|
||||
}
|
||||
|
||||
private void OnStartAttempt(RoundStartAttemptEvent args)
|
||||
{
|
||||
if (args.Forced || args.Cancelled)
|
||||
return;
|
||||
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out var uid, out _, out _, out var gameRule))
|
||||
{
|
||||
var minPlayers = gameRule.MinPlayers;
|
||||
if (args.Players.Length >= minPlayers)
|
||||
continue;
|
||||
|
||||
ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
|
||||
("readyPlayersCount", args.Players.Length),
|
||||
("minimumPlayers", minPlayers),
|
||||
("presetName", ToPrettyString(uid))));
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGameRuleAdded(EntityUid uid, T component, ref GameRuleAddedEvent args)
|
||||
@@ -48,6 +70,12 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
|
||||
Ended(uid, component, ruleData, args);
|
||||
}
|
||||
|
||||
private void OnRoundEndTextAppend(Entity<T> ent, ref RoundEndTextAppendEvent args)
|
||||
{
|
||||
if (!TryComp<GameRuleComponent>(ent, out var ruleData))
|
||||
return;
|
||||
AppendRoundEndText(ent, ent, ruleData, ref args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the gamerule is added
|
||||
@@ -73,6 +101,14 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called at the end of a round when text needs to be added for a game rule.
|
||||
/// </summary>
|
||||
protected virtual void AppendRoundEndText(EntityUid uid, T component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on an active gamerule entity in the Update function
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.KillTracking;
|
||||
using Content.Shared.Chat;
|
||||
|
||||
80
Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs
Normal file
80
Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Maps;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class LoadMapRuleSystem : GameRuleSystem<LoadMapRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly MapSystem _map = default!;
|
||||
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<LoadMapRuleComponent, AntagSelectLocationEvent>(OnSelectLocation);
|
||||
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
|
||||
}
|
||||
|
||||
private void OnGridSplit(ref GridSplitEvent args)
|
||||
{
|
||||
var rule = QueryActiveRules();
|
||||
while (rule.MoveNext(out _, out var mapComp, out _))
|
||||
{
|
||||
if (!mapComp.MapGrids.Contains(args.Grid))
|
||||
continue;
|
||||
|
||||
mapComp.MapGrids.AddRange(args.NewGrids);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Added(EntityUid uid, LoadMapRuleComponent comp, GameRuleComponent rule, GameRuleAddedEvent args)
|
||||
{
|
||||
if (comp.Map != null)
|
||||
return;
|
||||
|
||||
_map.CreateMap(out var mapId);
|
||||
comp.Map = mapId;
|
||||
|
||||
if (comp.GameMap != null)
|
||||
{
|
||||
var gameMap = _prototypeManager.Index(comp.GameMap.Value);
|
||||
comp.MapGrids.AddRange(GameTicker.LoadGameMap(gameMap, comp.Map.Value, new MapLoadOptions()));
|
||||
}
|
||||
else if (comp.MapPath != null)
|
||||
{
|
||||
if (_mapLoader.TryLoad(comp.Map.Value, comp.MapPath.Value.ToString(), out var roots, new MapLoadOptions { LoadMap = true }))
|
||||
comp.MapGrids.AddRange(roots);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"No valid map prototype or map path associated with the rule {ToPrettyString(uid)}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectLocation(Entity<LoadMapRuleComponent> ent, ref AntagSelectLocationEvent args)
|
||||
{
|
||||
var query = EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var xform))
|
||||
{
|
||||
if (xform.MapID != ent.Comp.Map)
|
||||
continue;
|
||||
|
||||
if (xform.GridUid == null || !ent.Comp.MapGrids.Contains(xform.GridUid.Value))
|
||||
continue;
|
||||
|
||||
if (ent.Comp.SpawnerWhitelist != null && !ent.Comp.SpawnerWhitelist.IsValid(uid, EntityManager))
|
||||
continue;
|
||||
|
||||
args.Coordinates.Add(_transform.GetMapCoordinates(xform));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
using Content.Server.Administration.Commands;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.Communications;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Ghost.Roles.Events;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Nuke;
|
||||
using Content.Server.NukeOps;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.RandomMetadata;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Server.Shuttles.Events;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.Store.Components;
|
||||
using Content.Server.Store.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.NPC.Components;
|
||||
@@ -33,45 +22,30 @@ using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Nuke;
|
||||
using Content.Shared.NukeOps;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Store;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Zombies;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking.Components;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergency = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly RandomMetadataSystem _randomMetadata = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
|
||||
[Dependency] private readonly StoreSystem _store = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
[ValidatePrototypeId<CurrencyPrototype>]
|
||||
private const string TelecrystalCurrencyPrototype = "Telecrystal";
|
||||
@@ -79,141 +53,67 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
[ValidatePrototypeId<TagPrototype>]
|
||||
private const string NukeOpsUplinkTagPrototype = "NukeOpsUplink";
|
||||
|
||||
[ValidatePrototypeId<AntagPrototype>]
|
||||
public const string NukeopsId = "Nukeops";
|
||||
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string OperationPrefixDataset = "operationPrefix";
|
||||
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string OperationSuffixDataset = "operationSuffix";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_sawmill = _logManager.GetSawmill("NukeOps");
|
||||
|
||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
||||
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayersSpawning);
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
||||
SubscribeLocalEvent<NukeExplodedEvent>(OnNukeExploded);
|
||||
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRunLevelChanged);
|
||||
SubscribeLocalEvent<NukeDisarmSuccessEvent>(OnNukeDisarm);
|
||||
|
||||
SubscribeLocalEvent<NukeOperativeComponent, ComponentRemove>(OnComponentRemove);
|
||||
SubscribeLocalEvent<NukeOperativeComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
SubscribeLocalEvent<NukeOperativeComponent, GhostRoleSpawnerUsedEvent>(OnPlayersGhostSpawning);
|
||||
SubscribeLocalEvent<NukeOperativeComponent, MindAddedMessage>(OnMindAdded);
|
||||
SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
|
||||
|
||||
SubscribeLocalEvent<NukeOpsShuttleComponent, MapInitEvent>(OnMapInit);
|
||||
|
||||
SubscribeLocalEvent<ConsoleFTLAttemptEvent>(OnShuttleFTLAttempt);
|
||||
SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
|
||||
SubscribeLocalEvent<CommunicationConsoleCallShuttleAttemptEvent>(OnShuttleCallAttempt);
|
||||
|
||||
SubscribeLocalEvent<NukeopsRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity);
|
||||
SubscribeLocalEvent<NukeopsRuleComponent, AfterAntagEntitySelectedEvent>(OnAfterAntagEntSelected);
|
||||
}
|
||||
|
||||
protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
|
||||
GameRuleStartedEvent args)
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
var eligible = new List<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
|
||||
var eligibleQuery = EntityQueryEnumerator<StationEventEligibleComponent, NpcFactionMemberComponent>();
|
||||
while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member))
|
||||
{
|
||||
if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member)))
|
||||
continue;
|
||||
|
||||
if (GameTicker.RunLevel == GameRunLevel.InRound)
|
||||
SpawnOperativesForGhostRoles(uid, component);
|
||||
eligible.Add((eligibleUid, eligibleComp, member));
|
||||
}
|
||||
|
||||
if (eligible.Count == 0)
|
||||
return;
|
||||
|
||||
component.TargetStation = RobustRandom.Pick(eligible);
|
||||
}
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void OnStartAttempt(RoundStartAttemptEvent ev)
|
||||
protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
|
||||
ref RoundEndTextAppendEvent args)
|
||||
{
|
||||
TryRoundStartAttempt(ev, Loc.GetString("nukeops-title"));
|
||||
}
|
||||
var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}");
|
||||
args.AddLine(winText);
|
||||
|
||||
private void OnPlayersSpawning(RulePlayerSpawningEvent ev)
|
||||
{
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
|
||||
{
|
||||
if (!SpawnMap((uid, nukeops)))
|
||||
{
|
||||
_sawmill.Info("Failed to load map for nukeops");
|
||||
continue;
|
||||
}
|
||||
|
||||
//Handle there being nobody readied up
|
||||
if (ev.PlayerPool.Count == 0)
|
||||
continue;
|
||||
|
||||
var commanderEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.CommanderSpawnDetails.AntagRoleProto);
|
||||
var agentEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.AgentSpawnDetails.AntagRoleProto);
|
||||
var operativeEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.OperativeSpawnDetails.AntagRoleProto);
|
||||
//Calculate how large the nukeops team needs to be
|
||||
var nukiesToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, nukeops.PlayersPerOperative, nukeops.MaxOps);
|
||||
|
||||
//Select Nukies
|
||||
//Select Commander, priority : commanderEligible, agentEligible, operativeEligible, all players
|
||||
var selectedCommander = _antagSelection.ChooseAntags(1, commanderEligible, agentEligible, operativeEligible, ev.PlayerPool).FirstOrDefault();
|
||||
//Select Agent, priority : agentEligible, operativeEligible, all players
|
||||
var selectedAgent = _antagSelection.ChooseAntags(1, agentEligible, operativeEligible, ev.PlayerPool).FirstOrDefault();
|
||||
//Select Operatives, priority : operativeEligible, all players
|
||||
var selectedOperatives = _antagSelection.ChooseAntags(nukiesToSelect - 2, operativeEligible, ev.PlayerPool);
|
||||
|
||||
//Create the team!
|
||||
//If the session is null, they will be spawned as ghost roles (provided the cvar is set)
|
||||
var operatives = new List<NukieSpawn> { new NukieSpawn(selectedCommander, nukeops.CommanderSpawnDetails) };
|
||||
if (nukiesToSelect > 1)
|
||||
operatives.Add(new NukieSpawn(selectedAgent, nukeops.AgentSpawnDetails));
|
||||
|
||||
for (var i = 0; i < nukiesToSelect - 2; i++)
|
||||
{
|
||||
//Use up all available sessions first, then spawn the rest as ghost roles (if enabled)
|
||||
if (selectedOperatives.Count > i)
|
||||
{
|
||||
operatives.Add(new NukieSpawn(selectedOperatives[i], nukeops.OperativeSpawnDetails));
|
||||
}
|
||||
else
|
||||
{
|
||||
operatives.Add(new NukieSpawn(null, nukeops.OperativeSpawnDetails));
|
||||
}
|
||||
}
|
||||
|
||||
SpawnOperatives(operatives, _cfg.GetCVar(CCVars.NukeopsSpawnGhostRoles), nukeops);
|
||||
|
||||
foreach (var nukieSpawn in operatives)
|
||||
{
|
||||
if (nukieSpawn.Session == null)
|
||||
continue;
|
||||
|
||||
GameTicker.PlayerJoinGame(nukieSpawn.Session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoundEndText(RoundEndTextAppendEvent ev)
|
||||
{
|
||||
var ruleQuery = QueryActiveRules();
|
||||
while (ruleQuery.MoveNext(out _, out _, out var nukeops, out _))
|
||||
{
|
||||
var winText = Loc.GetString($"nukeops-{nukeops.WinType.ToString().ToLower()}");
|
||||
ev.AddLine(winText);
|
||||
|
||||
foreach (var cond in nukeops.WinConditions)
|
||||
foreach (var cond in component.WinConditions)
|
||||
{
|
||||
var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
|
||||
ev.AddLine(text);
|
||||
}
|
||||
args.AddLine(text);
|
||||
}
|
||||
|
||||
ev.AddLine(Loc.GetString("nukeops-list-start"));
|
||||
args.AddLine(Loc.GetString("nukeops-list-start"));
|
||||
|
||||
var nukiesQuery = EntityQueryEnumerator<NukeopsRoleComponent, MindContainerComponent>();
|
||||
while (nukiesQuery.MoveNext(out var nukeopsUid, out _, out var mindContainer))
|
||||
var antags =_antag.GetAntagIdentifiers(uid);
|
||||
|
||||
foreach (var (_, sessionData, name) in antags)
|
||||
{
|
||||
if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer))
|
||||
continue;
|
||||
|
||||
ev.AddLine(mind.Session != null
|
||||
? Loc.GetString("nukeops-list-name-user", ("name", Name(nukeopsUid)), ("user", mind.Session.Name))
|
||||
: Loc.GetString("nukeops-list-name", ("name", Name(nukeopsUid))));
|
||||
args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,10 +124,10 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
{
|
||||
if (ev.OwningStation != null)
|
||||
{
|
||||
if (ev.OwningStation == nukeops.NukieOutpost)
|
||||
if (ev.OwningStation == GetOutpost(uid))
|
||||
{
|
||||
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost);
|
||||
SetWinType(uid, WinType.CrewMajor, nukeops);
|
||||
SetWinType((uid, nukeops), WinType.CrewMajor);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -242,7 +142,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
}
|
||||
|
||||
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation);
|
||||
SetWinType(uid, WinType.OpsMajor, nukeops);
|
||||
SetWinType((uid, nukeops), WinType.OpsMajor);
|
||||
correctStation = true;
|
||||
}
|
||||
|
||||
@@ -263,19 +163,85 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
|
||||
private void OnRunLevelChanged(GameRunLevelChangedEvent ev)
|
||||
{
|
||||
if (ev.New is not GameRunLevel.PostRound)
|
||||
return;
|
||||
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
|
||||
{
|
||||
switch (ev.New)
|
||||
OnRoundEnd((uid, nukeops));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoundEnd(Entity<NukeopsRuleComponent> ent)
|
||||
{
|
||||
case GameRunLevel.InRound:
|
||||
OnRoundStart(uid, nukeops);
|
||||
break;
|
||||
case GameRunLevel.PostRound:
|
||||
OnRoundEnd(uid, nukeops);
|
||||
break;
|
||||
// If the win condition was set to operative/crew major win, ignore.
|
||||
if (ent.Comp.WinType == WinType.OpsMajor || ent.Comp.WinType == WinType.CrewMajor)
|
||||
return;
|
||||
|
||||
var nukeQuery = AllEntityQuery<NukeComponent, TransformComponent>();
|
||||
var centcomms = _emergency.GetCentcommMaps();
|
||||
|
||||
while (nukeQuery.MoveNext(out var nuke, out var nukeTransform))
|
||||
{
|
||||
if (nuke.Status != NukeStatus.ARMED)
|
||||
continue;
|
||||
|
||||
// UH OH
|
||||
if (nukeTransform.MapUid != null && centcomms.Contains(nukeTransform.MapUid.Value))
|
||||
{
|
||||
ent.Comp.WinConditions.Add(WinCondition.NukeActiveAtCentCom);
|
||||
SetWinType((ent, ent), WinType.OpsMajor);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nukeTransform.GridUid == null || ent.Comp.TargetStation == null)
|
||||
continue;
|
||||
|
||||
if (!TryComp(ent.Comp.TargetStation.Value, out StationDataComponent? data))
|
||||
continue;
|
||||
|
||||
foreach (var grid in data.Grids)
|
||||
{
|
||||
if (grid != nukeTransform.GridUid)
|
||||
continue;
|
||||
|
||||
ent.Comp.WinConditions.Add(WinCondition.NukeActiveInStation);
|
||||
SetWinType(ent, WinType.OpsMajor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_antag.AllAntagsAlive(ent.Owner))
|
||||
{
|
||||
SetWinType(ent, WinType.OpsMinor);
|
||||
ent.Comp.WinConditions.Add(WinCondition.AllNukiesAlive);
|
||||
return;
|
||||
}
|
||||
|
||||
ent.Comp.WinConditions.Add(_antag.AnyAliveAntags(ent.Owner)
|
||||
? WinCondition.SomeNukiesAlive
|
||||
: WinCondition.AllNukiesDead);
|
||||
|
||||
var diskAtCentCom = false;
|
||||
var diskQuery = AllEntityQuery<NukeDiskComponent, TransformComponent>();
|
||||
while (diskQuery.MoveNext(out _, out var transform))
|
||||
{
|
||||
diskAtCentCom = transform.MapUid != null && centcomms.Contains(transform.MapUid.Value);
|
||||
|
||||
// TODO: The target station should be stored, and the nuke disk should store its original station.
|
||||
// This is fine for now, because we can assume a single station in base SS14.
|
||||
break;
|
||||
}
|
||||
|
||||
// If the disk is currently at Central Command, the crew wins - just slightly.
|
||||
// This also implies that some nuclear operatives have died.
|
||||
SetWinType(ent, diskAtCentCom
|
||||
? WinType.CrewMinor
|
||||
: WinType.OpsMinor);
|
||||
ent.Comp.WinConditions.Add(diskAtCentCom
|
||||
? WinCondition.NukeDiskOnCentCom
|
||||
: WinCondition.NukeDiskNotOnCentCom);
|
||||
}
|
||||
|
||||
private void OnNukeDisarm(NukeDisarmSuccessEvent ev)
|
||||
@@ -294,66 +260,31 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
CheckRoundShouldEnd();
|
||||
}
|
||||
|
||||
private void OnPlayersGhostSpawning(EntityUid uid, NukeOperativeComponent component, GhostRoleSpawnerUsedEvent args)
|
||||
{
|
||||
var spawner = args.Spawner;
|
||||
|
||||
if (!TryComp<NukeOperativeSpawnerComponent>(spawner, out var nukeOpSpawner))
|
||||
return;
|
||||
|
||||
HumanoidCharacterProfile? profile = null;
|
||||
if (TryComp(args.Spawned, out ActorComponent? actor))
|
||||
profile = _prefs.GetPreferences(actor.PlayerSession.UserId).SelectedCharacter as HumanoidCharacterProfile;
|
||||
|
||||
// TODO: this is kinda awful for multi-nukies
|
||||
foreach (var nukeops in EntityQuery<NukeopsRuleComponent>())
|
||||
{
|
||||
SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.SpawnDetails, profile);
|
||||
|
||||
nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.SpawnDetails.AntagRoleProto);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args)
|
||||
{
|
||||
if (!_mind.TryGetMind(uid, out var mindId, out var mind))
|
||||
return;
|
||||
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out _, out _, out var nukeops, out _))
|
||||
{
|
||||
if (nukeops.OperativeMindPendingData.TryGetValue(uid, out var role) || !nukeops.SpawnOutpost ||
|
||||
nukeops.RoundEndBehavior == RoundEndBehavior.Nothing)
|
||||
{
|
||||
role ??= nukeops.OperativeSpawnDetails.AntagRoleProto;
|
||||
_roles.MindAddRole(mindId, new NukeopsRoleComponent { PrototypeId = role });
|
||||
nukeops.OperativeMindPendingData.Remove(uid);
|
||||
}
|
||||
|
||||
if (mind.Session is not { } playerSession)
|
||||
return;
|
||||
|
||||
if (GameTicker.RunLevel != GameRunLevel.InRound)
|
||||
return;
|
||||
|
||||
if (nukeops.TargetStation != null && !string.IsNullOrEmpty(Name(nukeops.TargetStation.Value)))
|
||||
{
|
||||
NotifyNukie(playerSession, component, nukeops);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args)
|
||||
{
|
||||
RemCompDeferred(uid, component);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<NukeOpsShuttleComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
var map = Transform(ent).MapID;
|
||||
|
||||
var rules = EntityQueryEnumerator<NukeopsRuleComponent, LoadMapRuleComponent>();
|
||||
while (rules.MoveNext(out var uid, out _, out var mapRule))
|
||||
{
|
||||
if (map != mapRule.Map)
|
||||
continue;
|
||||
ent.Comp.AssociatedRule = uid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShuttleFTLAttempt(ref ConsoleFTLAttemptEvent ev)
|
||||
{
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out _, out _, out var nukeops, out _))
|
||||
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
|
||||
{
|
||||
if (ev.Uid != nukeops.NukieShuttle)
|
||||
if (ev.Uid != GetShuttle((uid, nukeops)))
|
||||
continue;
|
||||
|
||||
if (nukeops.WarDeclaredTime != null)
|
||||
@@ -397,12 +328,12 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
{
|
||||
// TODO: this is VERY awful for multi-nukies
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out _, out _, out var nukeops, out _))
|
||||
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
|
||||
{
|
||||
if (nukeops.WarDeclaredTime != null)
|
||||
continue;
|
||||
|
||||
if (Transform(ev.DeclaratorEntity).MapID != nukeops.NukiePlanet)
|
||||
if (TryComp<LoadMapRuleComponent>(uid, out var mapComp) && Transform(ev.DeclaratorEntity).MapID != mapComp.Map)
|
||||
continue;
|
||||
|
||||
var newStatus = GetWarCondition(nukeops, ev.Status);
|
||||
@@ -448,161 +379,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
if (!_tag.HasTag(uid, NukeOpsUplinkTagPrototype))
|
||||
continue;
|
||||
|
||||
if (!nukieRule.NukieOutpost.HasValue)
|
||||
if (GetOutpost(uid) is not {} outpost)
|
||||
continue;
|
||||
|
||||
if (Transform(uid).MapID != Transform(nukieRule.NukieOutpost.Value).MapID) // Will receive bonus TC only on their start outpost
|
||||
if (Transform(uid).MapID != Transform(outpost).MapID) // Will receive bonus TC only on their start outpost
|
||||
continue;
|
||||
|
||||
_store.TryAddCurrency(new () { { TelecrystalCurrencyPrototype, nukieRule.WarTCAmountPerNukie } }, uid, component);
|
||||
_store.TryAddCurrency(new () { { TelecrystalCurrencyPrototype, nukieRule.WarTcAmountPerNukie } }, uid, component);
|
||||
|
||||
var msg = Loc.GetString("store-currency-war-boost-given", ("target", uid));
|
||||
_popupSystem.PopupEntity(msg, uid);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoundStart(EntityUid uid, NukeopsRuleComponent? component = null)
|
||||
private void SetWinType(Entity<NukeopsRuleComponent> ent, WinType type, bool endRound = true)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
// TODO: This needs to try and target a Nanotrasen station. At the very least,
|
||||
// we can only currently guarantee that NT stations are the only station to
|
||||
// exist in the base game.
|
||||
|
||||
var eligible = new List<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
|
||||
var eligibleQuery = EntityQueryEnumerator<StationEventEligibleComponent, NpcFactionMemberComponent>();
|
||||
while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member))
|
||||
{
|
||||
if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member)))
|
||||
continue;
|
||||
|
||||
eligible.Add((eligibleUid, eligibleComp, member));
|
||||
}
|
||||
|
||||
if (eligible.Count == 0)
|
||||
return;
|
||||
|
||||
component.TargetStation = RobustRandom.Pick(eligible);
|
||||
component.OperationName = _randomMetadata.GetRandomFromSegments([OperationPrefixDataset, OperationSuffixDataset], " ");
|
||||
|
||||
var filter = Filter.Empty();
|
||||
var query = EntityQueryEnumerator<NukeOperativeComponent, ActorComponent>();
|
||||
while (query.MoveNext(out _, out var nukeops, out var actor))
|
||||
{
|
||||
NotifyNukie(actor.PlayerSession, nukeops, component);
|
||||
filter.AddPlayer(actor.PlayerSession);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoundEnd(EntityUid uid, NukeopsRuleComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
// If the win condition was set to operative/crew major win, ignore.
|
||||
if (component.WinType == WinType.OpsMajor || component.WinType == WinType.CrewMajor)
|
||||
return;
|
||||
|
||||
var nukeQuery = AllEntityQuery<NukeComponent, TransformComponent>();
|
||||
var centcomms = _emergency.GetCentcommMaps();
|
||||
|
||||
while (nukeQuery.MoveNext(out var nuke, out var nukeTransform))
|
||||
{
|
||||
if (nuke.Status != NukeStatus.ARMED)
|
||||
continue;
|
||||
|
||||
// UH OH
|
||||
if (nukeTransform.MapUid != null && centcomms.Contains(nukeTransform.MapUid.Value))
|
||||
{
|
||||
component.WinConditions.Add(WinCondition.NukeActiveAtCentCom);
|
||||
SetWinType(uid, WinType.OpsMajor, component);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nukeTransform.GridUid == null || component.TargetStation == null)
|
||||
continue;
|
||||
|
||||
if (!TryComp(component.TargetStation.Value, out StationDataComponent? data))
|
||||
continue;
|
||||
|
||||
foreach (var grid in data.Grids)
|
||||
{
|
||||
if (grid != nukeTransform.GridUid)
|
||||
continue;
|
||||
|
||||
component.WinConditions.Add(WinCondition.NukeActiveInStation);
|
||||
SetWinType(uid, WinType.OpsMajor, component);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var allAlive = true;
|
||||
var query = EntityQueryEnumerator<NukeopsRoleComponent, MindContainerComponent, MobStateComponent>();
|
||||
while (query.MoveNext(out var nukeopsUid, out _, out var mindContainer, out var mobState))
|
||||
{
|
||||
// mind got deleted somehow so ignore it
|
||||
if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer))
|
||||
continue;
|
||||
|
||||
// check if player got gibbed or ghosted or something - count as dead
|
||||
if (mind.OwnedEntity != null &&
|
||||
// if the player somehow isn't a mob anymore that also counts as dead
|
||||
// have to be alive, not crit or dead
|
||||
mobState.CurrentState is MobState.Alive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
allAlive = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// If all nuke ops were alive at the end of the round,
|
||||
// the nuke ops win. This is to prevent people from
|
||||
// running away the moment nuke ops appear.
|
||||
if (allAlive)
|
||||
{
|
||||
SetWinType(uid, WinType.OpsMinor, component);
|
||||
component.WinConditions.Add(WinCondition.AllNukiesAlive);
|
||||
return;
|
||||
}
|
||||
|
||||
component.WinConditions.Add(WinCondition.SomeNukiesAlive);
|
||||
|
||||
var diskAtCentCom = false;
|
||||
var diskQuery = AllEntityQuery<NukeDiskComponent, TransformComponent>();
|
||||
|
||||
while (diskQuery.MoveNext(out _, out var transform))
|
||||
{
|
||||
diskAtCentCom = transform.MapUid != null && centcomms.Contains(transform.MapUid.Value);
|
||||
|
||||
// TODO: The target station should be stored, and the nuke disk should store its original station.
|
||||
// This is fine for now, because we can assume a single station in base SS14.
|
||||
break;
|
||||
}
|
||||
|
||||
// If the disk is currently at Central Command, the crew wins - just slightly.
|
||||
// This also implies that some nuclear operatives have died.
|
||||
if (diskAtCentCom)
|
||||
{
|
||||
SetWinType(uid, WinType.CrewMinor, component);
|
||||
component.WinConditions.Add(WinCondition.NukeDiskOnCentCom);
|
||||
}
|
||||
// Otherwise, the nuke ops win.
|
||||
else
|
||||
{
|
||||
SetWinType(uid, WinType.OpsMinor, component);
|
||||
component.WinConditions.Add(WinCondition.NukeDiskNotOnCentCom);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetWinType(EntityUid uid, WinType type, NukeopsRuleComponent? component = null, bool endRound = true)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
component.WinType = type;
|
||||
ent.Comp.WinType = type;
|
||||
|
||||
if (endRound && (type == WinType.CrewMajor || type == WinType.OpsMajor))
|
||||
_roundEndSystem.EndRound();
|
||||
@@ -613,24 +405,29 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
|
||||
{
|
||||
CheckRoundShouldEnd((uid, nukeops));
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckRoundShouldEnd(Entity<NukeopsRuleComponent> ent)
|
||||
{
|
||||
var nukeops = ent.Comp;
|
||||
|
||||
if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor)
|
||||
continue;
|
||||
return;
|
||||
|
||||
|
||||
// If there are any nuclear bombs that are active, immediately return. We're not over yet.
|
||||
var armed = false;
|
||||
foreach (var nuke in EntityQuery<NukeComponent>())
|
||||
{
|
||||
if (nuke.Status == NukeStatus.ARMED)
|
||||
{
|
||||
armed = true;
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (armed)
|
||||
continue;
|
||||
|
||||
MapId? shuttleMapId = Exists(nukeops.NukieShuttle)
|
||||
? Transform(nukeops.NukieShuttle.Value).MapID
|
||||
var shuttle = GetShuttle((ent, ent));
|
||||
|
||||
MapId? shuttleMapId = Exists(shuttle)
|
||||
? Transform(shuttle.Value).MapID
|
||||
: null;
|
||||
|
||||
MapId? targetStationMap = null;
|
||||
@@ -647,18 +444,18 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
// If there are, the round can continue.
|
||||
var operatives = EntityQuery<NukeOperativeComponent, MobStateComponent, TransformComponent>(true);
|
||||
var operativesAlive = operatives
|
||||
.Where(ent =>
|
||||
ent.Item3.MapID == shuttleMapId
|
||||
|| ent.Item3.MapID == targetStationMap)
|
||||
.Any(ent => ent.Item2.CurrentState == MobState.Alive && ent.Item1.Running);
|
||||
.Where(op =>
|
||||
op.Item3.MapID == shuttleMapId
|
||||
|| op.Item3.MapID == targetStationMap)
|
||||
.Any(op => op.Item2.CurrentState == MobState.Alive && op.Item1.Running);
|
||||
|
||||
if (operativesAlive)
|
||||
continue; // There are living operatives than can access the shuttle, or are still on the station's map.
|
||||
return; // There are living operatives than can access the shuttle, or are still on the station's map.
|
||||
|
||||
// Check that there are spawns available and that they can access the shuttle.
|
||||
var spawnsAvailable = EntityQuery<NukeOperativeSpawnerComponent>(true).Any();
|
||||
if (spawnsAvailable && shuttleMapId == nukeops.NukiePlanet)
|
||||
continue; // Ghost spawns can still access the shuttle. Continue the round.
|
||||
if (spawnsAvailable && CompOrNull<LoadMapRuleComponent>(ent)?.Map == shuttleMapId)
|
||||
return; // Ghost spawns can still access the shuttle. Continue the round.
|
||||
|
||||
// The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives,
|
||||
// and there are no nuclear operatives on the target station's map.
|
||||
@@ -666,190 +463,72 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
? WinCondition.NukiesAbandoned
|
||||
: WinCondition.AllNukiesDead);
|
||||
|
||||
SetWinType(uid, WinType.CrewMajor, nukeops, false);
|
||||
SetWinType(ent, WinType.CrewMajor, false);
|
||||
_roundEndSystem.DoRoundEndBehavior(
|
||||
nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement);
|
||||
|
||||
// prevent it called multiple times
|
||||
nukeops.RoundEndBehavior = RoundEndBehavior.Nothing;
|
||||
}
|
||||
}
|
||||
|
||||
private bool SpawnMap(Entity<NukeopsRuleComponent> ent)
|
||||
// this should really go anywhere else but im tired.
|
||||
private void OnAntagSelectEntity(Entity<NukeopsRuleComponent> ent, ref AntagSelectEntityEvent args)
|
||||
{
|
||||
if (!ent.Comp.SpawnOutpost
|
||||
|| ent.Comp.NukiePlanet != null)
|
||||
return true;
|
||||
|
||||
ent.Comp.NukiePlanet = _mapManager.CreateMap();
|
||||
var gameMap = _prototypeManager.Index(ent.Comp.OutpostMapPrototype);
|
||||
ent.Comp.NukieOutpost = GameTicker.LoadGameMap(gameMap, ent.Comp.NukiePlanet.Value, null)[0];
|
||||
var query = EntityQueryEnumerator<NukeOpsShuttleComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var grid, out _, out var shuttleTransform))
|
||||
{
|
||||
if (shuttleTransform.MapID != ent.Comp.NukiePlanet)
|
||||
continue;
|
||||
|
||||
ent.Comp.NukieShuttle = grid;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds missing nuke operative components, equips starting gear and renames the entity.
|
||||
/// </summary>
|
||||
private void SetupOperativeEntity(EntityUid mob, string name, NukeopSpawnPreset spawnDetails, HumanoidCharacterProfile? profile)
|
||||
{
|
||||
_metaData.SetEntityName(mob, name);
|
||||
EnsureComp<NukeOperativeComponent>(mob);
|
||||
|
||||
if (profile != null)
|
||||
_humanoid.LoadProfile(mob, profile);
|
||||
|
||||
var gear = _prototypeManager.Index(spawnDetails.GearProto);
|
||||
_stationSpawning.EquipStartingGear(mob, gear);
|
||||
|
||||
_npcFaction.RemoveFaction(mob, "NanoTrasen", false);
|
||||
_npcFaction.AddFaction(mob, "Syndicate");
|
||||
}
|
||||
|
||||
private void SpawnOperatives(List<NukieSpawn> sessions, bool spawnGhostRoles, NukeopsRuleComponent component)
|
||||
{
|
||||
if (component.NukieOutpost is not { Valid: true } outpostUid)
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
var spawns = new List<EntityCoordinates>();
|
||||
foreach (var (_, meta, xform) in EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
|
||||
{
|
||||
if (meta.EntityPrototype?.ID != component.SpawnPointProto.Id)
|
||||
continue;
|
||||
|
||||
if (xform.ParentUid != component.NukieOutpost)
|
||||
continue;
|
||||
|
||||
spawns.Add(xform.Coordinates);
|
||||
break;
|
||||
}
|
||||
|
||||
//Fallback, spawn at the centre of the map
|
||||
if (spawns.Count == 0)
|
||||
{
|
||||
spawns.Add(Transform(outpostUid).Coordinates);
|
||||
_sawmill.Warning($"Fell back to default spawn for nukies!");
|
||||
}
|
||||
|
||||
//Spawn the team
|
||||
foreach (var nukieSession in sessions)
|
||||
{
|
||||
var name = $"{Loc.GetString(nukieSession.Type.NamePrefix)} {RobustRandom.PickAndTake(_prototypeManager.Index(nukieSession.Type.NameList).Values.ToList())}";
|
||||
|
||||
var nukeOpsAntag = _prototypeManager.Index(nukieSession.Type.AntagRoleProto);
|
||||
|
||||
//If a session is available, spawn mob and transfer mind into it
|
||||
if (nukieSession.Session != null)
|
||||
{
|
||||
var profile = _prefs.GetPreferences(nukieSession.Session.UserId).SelectedCharacter as HumanoidCharacterProfile;
|
||||
var profile = args.Session != null
|
||||
? _prefs.GetPreferences(args.Session.UserId).SelectedCharacter as HumanoidCharacterProfile
|
||||
: HumanoidCharacterProfile.RandomWithSpecies();
|
||||
if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species))
|
||||
{
|
||||
species = _prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
|
||||
}
|
||||
|
||||
var mob = Spawn(species.Prototype, RobustRandom.Pick(spawns));
|
||||
SetupOperativeEntity(mob, name, nukieSession.Type, profile);
|
||||
|
||||
var newMind = _mind.CreateMind(nukieSession.Session.UserId, name);
|
||||
_mind.SetUserId(newMind, nukieSession.Session.UserId);
|
||||
_roles.MindAddRole(newMind, new NukeopsRoleComponent { PrototypeId = nukieSession.Type.AntagRoleProto });
|
||||
|
||||
_mind.TransferTo(newMind, mob);
|
||||
args.Entity = Spawn(species.Prototype);
|
||||
_humanoid.LoadProfile(args.Entity.Value, profile);
|
||||
}
|
||||
//Otherwise, spawn as a ghost role
|
||||
else if (spawnGhostRoles)
|
||||
|
||||
private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||
{
|
||||
var spawnPoint = Spawn(component.GhostSpawnPointProto, RobustRandom.Pick(spawns));
|
||||
var ghostRole = EnsureComp<GhostRoleComponent>(spawnPoint);
|
||||
EnsureComp<GhostRoleMobSpawnerComponent>(spawnPoint);
|
||||
ghostRole.RoleName = Loc.GetString(nukeOpsAntag.Name);
|
||||
ghostRole.RoleDescription = Loc.GetString(nukeOpsAntag.Objective);
|
||||
|
||||
var nukeOpSpawner = EnsureComp<NukeOperativeSpawnerComponent>(spawnPoint);
|
||||
nukeOpSpawner.OperativeName = name;
|
||||
nukeOpSpawner.SpawnDetails = nukieSession.Type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display a greeting message and play a sound for a nukie
|
||||
/// </summary>
|
||||
private void NotifyNukie(ICommonSession session, NukeOperativeComponent nukeop, NukeopsRuleComponent nukeopsRule)
|
||||
{
|
||||
if (nukeopsRule.TargetStation is not { } station)
|
||||
if (ent.Comp.TargetStation is not { } station)
|
||||
return;
|
||||
|
||||
_antagSelection.SendBriefing(session, Loc.GetString("nukeops-welcome", ("station", station), ("name", nukeopsRule.OperationName)), Color.Red, nukeop.GreetSoundNotification);
|
||||
_antag.SendBriefing(args.Session, Loc.GetString("nukeops-welcome",
|
||||
("station", station),
|
||||
("name", Name(ent))),
|
||||
Color.Red,
|
||||
ent.Comp.GreetSoundNotification);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn nukie ghost roles if this gamerule was started mid round
|
||||
/// </summary>
|
||||
private void SpawnOperativesForGhostRoles(EntityUid uid, NukeopsRuleComponent? component = null)
|
||||
/// <remarks>
|
||||
/// Is this method the shitty glue holding together the last of my sanity? yes.
|
||||
/// Do i have a better solution? not presently.
|
||||
/// </remarks>
|
||||
private EntityUid? GetOutpost(Entity<LoadMapRuleComponent?> ent)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return null;
|
||||
|
||||
if (!SpawnMap((uid, component)))
|
||||
return ent.Comp.MapGrids.FirstOrNull();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Is this method the shitty glue holding together the last of my sanity? yes.
|
||||
/// Do i have a better solution? not presently.
|
||||
/// </remarks>
|
||||
private EntityUid? GetShuttle(Entity<NukeopsRuleComponent?> ent)
|
||||
{
|
||||
_sawmill.Info("Failed to load map for nukeops");
|
||||
return;
|
||||
}
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return null;
|
||||
|
||||
var numNukies = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerOperative, component.MaxOps);
|
||||
|
||||
//Dont continue if we have no nukies to spawn
|
||||
if (numNukies == 0)
|
||||
return;
|
||||
|
||||
//Fill the ranks, commander first, then agent, then operatives
|
||||
//TODO: Possible alternative team compositions? Like multiple commanders or agents
|
||||
var operatives = new List<NukieSpawn>();
|
||||
if (numNukies >= 1)
|
||||
operatives.Add(new NukieSpawn(null, component.CommanderSpawnDetails));
|
||||
if (numNukies >= 2)
|
||||
operatives.Add(new NukieSpawn(null, component.AgentSpawnDetails));
|
||||
if (numNukies >= 3)
|
||||
var query = EntityQueryEnumerator<NukeOpsShuttleComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
{
|
||||
for (var i = 2; i < numNukies; i++)
|
||||
{
|
||||
operatives.Add(new NukieSpawn(null, component.OperativeSpawnDetails));
|
||||
}
|
||||
if (comp.AssociatedRule == ent.Owner)
|
||||
return uid;
|
||||
}
|
||||
|
||||
SpawnOperatives(operatives, true, component);
|
||||
}
|
||||
|
||||
//For admins forcing someone to nukeOps.
|
||||
public void MakeLoneNukie(EntityUid entity)
|
||||
{
|
||||
if (!_mind.TryGetMind(entity, out var mindId, out var mindComponent))
|
||||
return;
|
||||
|
||||
//ok hardcoded value bad but so is everything else here
|
||||
_roles.MindAddRole(mindId, new NukeopsRoleComponent { PrototypeId = NukeopsId }, mindComponent);
|
||||
SetOutfitCommand.SetOutfit(entity, "SyndicateOperativeGearFull", EntityManager);
|
||||
}
|
||||
|
||||
private sealed class NukieSpawn
|
||||
{
|
||||
public ICommonSession? Session { get; private set; }
|
||||
public NukeopSpawnPreset Type { get; private set; }
|
||||
|
||||
public NukieSpawn(ICommonSession? session, NukeopSpawnPreset type)
|
||||
{
|
||||
Session = session;
|
||||
Type = type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Administration.Commands;
|
||||
using Content.Server.Cargo.Systems;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.NPC.Prototypes;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Maps;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// This handles the Pirates minor antag, which is designed to coincide with other modes on occasion.
|
||||
/// </summary>
|
||||
public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
|
||||
[Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!;
|
||||
[Dependency] private readonly PricingSystem _pricingSystem = default!;
|
||||
[Dependency] private readonly MapLoaderSystem _map = default!;
|
||||
[Dependency] private readonly NamingSystem _namingSystem = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string GameRuleId = "Pirates";
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string MobId = "MobHuman";
|
||||
|
||||
[ValidatePrototypeId<SpeciesPrototype>]
|
||||
private const string SpeciesId = "Human";
|
||||
|
||||
[ValidatePrototypeId<NpcFactionPrototype>]
|
||||
private const string PirateFactionId = "Syndicate";
|
||||
|
||||
[ValidatePrototypeId<NpcFactionPrototype>]
|
||||
private const string EnemyFactionId = "NanoTrasen";
|
||||
|
||||
[ValidatePrototypeId<StartingGearPrototype>]
|
||||
private const string GearId = "PirateGear";
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string SpawnPointId = "SpawnPointPirates";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawningEvent);
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextEvent);
|
||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
||||
}
|
||||
|
||||
private void OnRoundEndTextEvent(RoundEndTextAppendEvent ev)
|
||||
{
|
||||
var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
|
||||
while (query.MoveNext(out var uid, out var pirates, out var gameRule))
|
||||
{
|
||||
if (Deleted(pirates.PirateShip))
|
||||
{
|
||||
// Major loss, the ship somehow got annihilated.
|
||||
ev.AddLine(Loc.GetString("pirates-no-ship"));
|
||||
}
|
||||
else
|
||||
{
|
||||
List<(double, EntityUid)> mostValuableThefts = new();
|
||||
|
||||
var comp1 = pirates;
|
||||
var finalValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
|
||||
{
|
||||
foreach (var mindId in comp1.Pirates)
|
||||
{
|
||||
if (TryComp(mindId, out MindComponent? mind) && mind.CurrentEntity == uid)
|
||||
return false; // Don't appraise the pirates twice, we count them in separately.
|
||||
}
|
||||
|
||||
return true;
|
||||
}, (uid, price) =>
|
||||
{
|
||||
if (comp1.InitialItems.Contains(uid))
|
||||
return;
|
||||
|
||||
mostValuableThefts.Add((price, uid));
|
||||
mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1));
|
||||
if (mostValuableThefts.Count > 5)
|
||||
mostValuableThefts.Pop();
|
||||
});
|
||||
|
||||
foreach (var mindId in pirates.Pirates)
|
||||
{
|
||||
if (TryComp(mindId, out MindComponent? mind) && mind.CurrentEntity is not null)
|
||||
finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value);
|
||||
}
|
||||
|
||||
var score = finalValue - pirates.InitialShipValue;
|
||||
|
||||
ev.AddLine(Loc.GetString("pirates-final-score", ("score", $"{score:F2}")));
|
||||
ev.AddLine(Loc.GetString("pirates-final-score-2", ("finalPrice", $"{finalValue:F2}")));
|
||||
|
||||
ev.AddLine("");
|
||||
ev.AddLine(Loc.GetString("pirates-most-valuable"));
|
||||
|
||||
foreach (var (price, obj) in mostValuableThefts)
|
||||
{
|
||||
ev.AddLine(Loc.GetString("pirates-stolen-item-entry", ("entity", obj), ("credits", $"{price:F2}")));
|
||||
}
|
||||
|
||||
if (mostValuableThefts.Count == 0)
|
||||
ev.AddLine(Loc.GetString("pirates-stole-nothing"));
|
||||
}
|
||||
|
||||
ev.AddLine("");
|
||||
ev.AddLine(Loc.GetString("pirates-list-start"));
|
||||
foreach (var pirate in pirates.Pirates)
|
||||
{
|
||||
if (TryComp(pirate, out MindComponent? mind))
|
||||
{
|
||||
ev.AddLine($"- {mind.CharacterName} ({mind.Session?.Name})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerSpawningEvent(RulePlayerSpawningEvent ev)
|
||||
{
|
||||
var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
|
||||
while (query.MoveNext(out var uid, out var pirates, out var gameRule))
|
||||
{
|
||||
// Forgive me for copy-pasting nukies.
|
||||
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
|
||||
return;
|
||||
|
||||
pirates.Pirates.Clear();
|
||||
pirates.InitialItems.Clear();
|
||||
|
||||
// Between 1 and <max pirate count>: needs at least n players per op.
|
||||
var numOps = Math.Max(1,
|
||||
(int) Math.Min(
|
||||
Math.Floor((double) ev.PlayerPool.Count / _cfg.GetCVar(CCVars.PiratesPlayersPerOp)),
|
||||
_cfg.GetCVar(CCVars.PiratesMaxOps)));
|
||||
var ops = new ICommonSession[numOps];
|
||||
for (var i = 0; i < numOps; i++)
|
||||
{
|
||||
ops[i] = _random.PickAndTake(ev.PlayerPool);
|
||||
}
|
||||
|
||||
var map = "/Maps/Shuttles/pirate.yml";
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
var aabbs = EntityQuery<StationDataComponent>().SelectMany(x =>
|
||||
x.Grids.Select(x =>
|
||||
xformQuery.GetComponent(x).WorldMatrix.TransformBox(Comp<MapGridComponent>(x).LocalAABB)))
|
||||
.ToArray();
|
||||
|
||||
var aabb = aabbs[0];
|
||||
|
||||
for (var i = 1; i < aabbs.Length; i++)
|
||||
{
|
||||
aabb.Union(aabbs[i]);
|
||||
}
|
||||
|
||||
// (Not commented?)
|
||||
var a = MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f;
|
||||
|
||||
var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions
|
||||
{
|
||||
Offset = aabb.Center + new Vector2(a, a),
|
||||
LoadMap = false,
|
||||
});
|
||||
|
||||
if (!gridId.HasValue)
|
||||
{
|
||||
Log.Error($"Gridid was null when loading \"{map}\", aborting.");
|
||||
foreach (var session in ops)
|
||||
{
|
||||
ev.PlayerPool.Add(session);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
pirates.PirateShip = gridId.Value;
|
||||
|
||||
// TODO: Loot table or something
|
||||
var pirateGear = _prototypeManager.Index<StartingGearPrototype>(GearId); // YARRR
|
||||
|
||||
var spawns = new List<EntityCoordinates>();
|
||||
|
||||
// Forgive me for hardcoding prototypes
|
||||
foreach (var (_, meta, xform) in
|
||||
EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
|
||||
{
|
||||
if (meta.EntityPrototype?.ID != SpawnPointId || xform.ParentUid != pirates.PirateShip)
|
||||
continue;
|
||||
|
||||
spawns.Add(xform.Coordinates);
|
||||
}
|
||||
|
||||
if (spawns.Count == 0)
|
||||
{
|
||||
spawns.Add(Transform(pirates.PirateShip).Coordinates);
|
||||
Log.Warning($"Fell back to default spawn for pirates!");
|
||||
}
|
||||
|
||||
for (var i = 0; i < ops.Length; i++)
|
||||
{
|
||||
var sex = _random.Prob(0.5f) ? Sex.Male : Sex.Female;
|
||||
var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
|
||||
|
||||
var name = _namingSystem.GetName(SpeciesId, gender);
|
||||
|
||||
var session = ops[i];
|
||||
var newMind = _mindSystem.CreateMind(session.UserId, name);
|
||||
_mindSystem.SetUserId(newMind, session.UserId);
|
||||
|
||||
var mob = Spawn(MobId, _random.Pick(spawns));
|
||||
_metaData.SetEntityName(mob, name);
|
||||
|
||||
_mindSystem.TransferTo(newMind, mob);
|
||||
var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
|
||||
_stationSpawningSystem.EquipStartingGear(mob, pirateGear);
|
||||
|
||||
_npcFaction.RemoveFaction(mob, EnemyFactionId, false);
|
||||
_npcFaction.AddFaction(mob, PirateFactionId);
|
||||
|
||||
pirates.Pirates.Add(newMind);
|
||||
|
||||
// Notificate every player about a pirate antagonist role with sound
|
||||
_audioSystem.PlayGlobal(pirates.PirateAlertSound, session);
|
||||
|
||||
GameTicker.PlayerJoinGame(session);
|
||||
}
|
||||
|
||||
pirates.InitialShipValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
|
||||
{
|
||||
pirates.InitialItems.Add(uid);
|
||||
return true;
|
||||
}); // Include the players in the appraisal.
|
||||
}
|
||||
}
|
||||
|
||||
//Forcing one player to be a pirate.
|
||||
public void MakePirate(EntityUid entity)
|
||||
{
|
||||
if (!_mindSystem.TryGetMind(entity, out var mindId, out var mind))
|
||||
return;
|
||||
|
||||
SetOutfitCommand.SetOutfit(entity, GearId, EntityManager);
|
||||
|
||||
var pirateRule = EntityQuery<PiratesRuleComponent>().FirstOrDefault();
|
||||
if (pirateRule == null)
|
||||
{
|
||||
//todo fuck me this shit is awful
|
||||
GameTicker.StartGameRule(GameRuleId, out var ruleEntity);
|
||||
pirateRule = Comp<PiratesRuleComponent>(ruleEntity);
|
||||
}
|
||||
|
||||
// Notificate every player about a pirate antagonist role with sound
|
||||
if (mind.Session != null)
|
||||
{
|
||||
_audioSystem.PlayGlobal(pirateRule.PirateAlertSound, mind.Session);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStartAttempt(RoundStartAttemptEvent ev)
|
||||
{
|
||||
var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
|
||||
while (query.MoveNext(out var uid, out var pirates, out var gameRule))
|
||||
{
|
||||
if (!GameTicker.IsGameRuleActive(uid, gameRule))
|
||||
return;
|
||||
|
||||
var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers);
|
||||
if (!ev.Forced && ev.Players.Length < minPlayers)
|
||||
{
|
||||
_chatManager.SendAdminAnnouncement(Loc.GetString("nukeops-not-enough-ready-players",
|
||||
("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
|
||||
ev.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.Players.Length == 0)
|
||||
{
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready"));
|
||||
ev.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Chat;
|
||||
|
||||
@@ -14,7 +14,6 @@ using Content.Server.Station.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Mindshield.Components;
|
||||
@@ -24,12 +23,11 @@ using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.NPC.Prototypes;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Revolutionary.Components;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Zombies;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking.Components;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
@@ -40,7 +38,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
{
|
||||
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly EuiManager _euiMan = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
@@ -51,7 +49,6 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
|
||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
|
||||
//Used in OnPostFlash, no reference to the rule component is available
|
||||
public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
|
||||
@@ -60,23 +57,12 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
||||
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayerJobAssigned);
|
||||
SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
|
||||
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
||||
SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
||||
SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(OnPostFlash);
|
||||
}
|
||||
|
||||
//Set miniumum players
|
||||
protected override void Added(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
||||
{
|
||||
base.Added(uid, component, gameRule, args);
|
||||
|
||||
gameRule.MinPlayers = component.MinPlayers;
|
||||
}
|
||||
|
||||
protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
@@ -98,42 +84,31 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoundEndText(RoundEndTextAppendEvent ev)
|
||||
protected override void AppendRoundEndText(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule,
|
||||
ref RoundEndTextAppendEvent args)
|
||||
{
|
||||
base.AppendRoundEndText(uid, component, gameRule, ref args);
|
||||
|
||||
var revsLost = CheckRevsLose();
|
||||
var commandLost = CheckCommandLose();
|
||||
var query = AllEntityQuery<RevolutionaryRuleComponent>();
|
||||
while (query.MoveNext(out var headrev))
|
||||
{
|
||||
// This is (revsLost, commandsLost) concatted together
|
||||
// (moony wrote this comment idk what it means)
|
||||
var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
|
||||
ev.AddLine(Loc.GetString(Outcomes[index]));
|
||||
args.AddLine(Loc.GetString(Outcomes[index]));
|
||||
|
||||
ev.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", headrev.HeadRevs.Count)));
|
||||
foreach (var player in headrev.HeadRevs)
|
||||
var sessionData = _antag.GetAntagIdentifiers(uid);
|
||||
args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count)));
|
||||
foreach (var (mind, data, name) in sessionData)
|
||||
{
|
||||
// TODO: when role entities are a thing this has to change
|
||||
var count = CompOrNull<RevolutionaryRoleComponent>(player.Value)?.ConvertedCount ?? 0;
|
||||
|
||||
_mind.TryGetSession(player.Value, out var session);
|
||||
var username = session?.Name;
|
||||
if (username != null)
|
||||
{
|
||||
ev.AddLine(Loc.GetString("rev-headrev-name-user",
|
||||
("name", player.Key),
|
||||
("username", username), ("count", count)));
|
||||
}
|
||||
else
|
||||
{
|
||||
ev.AddLine(Loc.GetString("rev-headrev-name",
|
||||
("name", player.Key), ("count", count)));
|
||||
}
|
||||
var count = CompOrNull<RevolutionaryRoleComponent>(mind)?.ConvertedCount ?? 0;
|
||||
args.AddLine(Loc.GetString("rev-headrev-name-user",
|
||||
("name", name),
|
||||
("username", data.UserName),
|
||||
("count", count)));
|
||||
|
||||
// TODO: someone suggested listing all alive? revs maybe implement at some point
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGetBriefing(EntityUid uid, RevolutionaryRoleComponent comp, ref GetBriefingEvent args)
|
||||
{
|
||||
@@ -144,57 +119,6 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing"));
|
||||
}
|
||||
|
||||
//Check for enough players to start rule
|
||||
private void OnStartAttempt(RoundStartAttemptEvent ev)
|
||||
{
|
||||
TryRoundStartAttempt(ev, Loc.GetString("roles-antag-rev-name"));
|
||||
}
|
||||
|
||||
private void OnPlayerJobAssigned(RulePlayerJobsAssignedEvent ev)
|
||||
{
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out var uid, out var activeGameRule, out var comp, out var gameRule))
|
||||
{
|
||||
var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.HeadRevPrototypeId);
|
||||
|
||||
if (eligiblePlayers.Count == 0)
|
||||
continue;
|
||||
|
||||
var headRevCount = _antagSelection.CalculateAntagCount(ev.Players.Length, comp.PlayersPerHeadRev, comp.MaxHeadRevs);
|
||||
|
||||
var headRevs = _antagSelection.ChooseAntags(headRevCount, eligiblePlayers);
|
||||
|
||||
GiveHeadRev(headRevs, comp.HeadRevPrototypeId, comp);
|
||||
}
|
||||
}
|
||||
|
||||
private void GiveHeadRev(IEnumerable<EntityUid> chosen, ProtoId<AntagPrototype> antagProto, RevolutionaryRuleComponent comp)
|
||||
{
|
||||
foreach (var headRev in chosen)
|
||||
GiveHeadRev(headRev, antagProto, comp);
|
||||
}
|
||||
private void GiveHeadRev(EntityUid chosen, ProtoId<AntagPrototype> antagProto, RevolutionaryRuleComponent comp)
|
||||
{
|
||||
RemComp<CommandStaffComponent>(chosen);
|
||||
|
||||
var inCharacterName = MetaData(chosen).EntityName;
|
||||
|
||||
if (!_mind.TryGetMind(chosen, out var mind, out _))
|
||||
return;
|
||||
|
||||
if (!_role.MindHasRole<RevolutionaryRoleComponent>(mind))
|
||||
{
|
||||
_role.MindAddRole(mind, new RevolutionaryRoleComponent { PrototypeId = antagProto }, silent: true);
|
||||
}
|
||||
|
||||
comp.HeadRevs.Add(inCharacterName, mind);
|
||||
_inventory.SpawnItemsOnEntity(chosen, comp.StartingGear);
|
||||
var revComp = EnsureComp<RevolutionaryComponent>(chosen);
|
||||
EnsureComp<HeadRevolutionaryComponent>(chosen);
|
||||
|
||||
_antagSelection.SendBriefing(chosen, Loc.GetString("head-rev-role-greeting"), Color.CornflowerBlue, revComp.RevStartSound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Head Rev uses a flash in melee to convert somebody else.
|
||||
/// </summary>
|
||||
@@ -232,22 +156,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
}
|
||||
|
||||
if (mind?.Session != null)
|
||||
_antagSelection.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound);
|
||||
}
|
||||
|
||||
public void OnHeadRevAdmin(EntityUid entity)
|
||||
{
|
||||
if (HasComp<HeadRevolutionaryComponent>(entity))
|
||||
return;
|
||||
|
||||
var revRule = EntityQuery<RevolutionaryRuleComponent>().FirstOrDefault();
|
||||
if (revRule == null)
|
||||
{
|
||||
GameTicker.StartGameRule("Revolutionary", out var ruleEnt);
|
||||
revRule = Comp<RevolutionaryRuleComponent>(ruleEnt);
|
||||
}
|
||||
|
||||
GiveHeadRev(entity, revRule.HeadRevPrototypeId, revRule);
|
||||
_antag.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound);
|
||||
}
|
||||
|
||||
//TODO: Enemies of the revolution
|
||||
@@ -308,7 +217,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
_popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid);
|
||||
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying.");
|
||||
|
||||
if (!_mind.TryGetMind(uid, out var mindId, out var mind, mc))
|
||||
if (!_mind.TryGetMind(uid, out var mindId, out _, mc))
|
||||
continue;
|
||||
|
||||
// remove their antag role
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Server.Station.Components;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Sandbox;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking.Presets;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Shared.Storage;
|
||||
|
||||
|
||||
@@ -3,118 +3,37 @@ using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Objectives;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.CombatMode.Pacification;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
|
||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly ObjectivesSystem _objectives = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
|
||||
SubscribeLocalEvent<ThiefRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagSelected);
|
||||
|
||||
SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
||||
SubscribeLocalEvent<ThiefRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
|
||||
}
|
||||
|
||||
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
|
||||
private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||
{
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out var uid, out _, out var comp, out var gameRule))
|
||||
{
|
||||
//Get all players eligible for this role, allow selecting existing antags
|
||||
//TO DO: When voxes specifies are added, increase their chance of becoming a thief by 4 times >:)
|
||||
var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.ThiefPrototypeId, acceptableAntags: AntagAcceptability.All, allowNonHumanoids: true);
|
||||
|
||||
//Abort if there are none
|
||||
if (eligiblePlayers.Count == 0)
|
||||
{
|
||||
Log.Warning($"No eligible thieves found, ending game rule {ToPrettyString(uid):rule}");
|
||||
GameTicker.EndGameRule(uid, gameRule);
|
||||
continue;
|
||||
}
|
||||
|
||||
//Calculate number of thieves to choose
|
||||
var thiefCount = _random.Next(1, comp.MaxAllowThief + 1);
|
||||
|
||||
//Select our theives
|
||||
var thieves = _antagSelection.ChooseAntags(thiefCount, eligiblePlayers);
|
||||
|
||||
MakeThief(thieves, comp, comp.PacifistThieves);
|
||||
}
|
||||
}
|
||||
|
||||
public void MakeThief(List<EntityUid> players, ThiefRuleComponent thiefRule, bool addPacified)
|
||||
{
|
||||
foreach (var thief in players)
|
||||
{
|
||||
MakeThief(thief, thiefRule, addPacified);
|
||||
}
|
||||
}
|
||||
|
||||
public void MakeThief(EntityUid thief, ThiefRuleComponent thiefRule, bool addPacified)
|
||||
{
|
||||
if (!_mindSystem.TryGetMind(thief, out var mindId, out var mind))
|
||||
if (!_mindSystem.TryGetMind(args.EntityUid, out var mindId, out var mind))
|
||||
return;
|
||||
|
||||
if (HasComp<ThiefRoleComponent>(mindId))
|
||||
return;
|
||||
|
||||
// Assign thief roles
|
||||
_roleSystem.MindAddRole(mindId, new ThiefRoleComponent
|
||||
{
|
||||
PrototypeId = thiefRule.ThiefPrototypeId,
|
||||
}, silent: true);
|
||||
|
||||
//Add Pacified
|
||||
//To Do: Long-term this should just be using the antag code to add components.
|
||||
if (addPacified) //This check is important because some servers may want to disable the thief's pacifism. Do not remove.
|
||||
{
|
||||
EnsureComp<PacifiedComponent>(thief);
|
||||
}
|
||||
|
||||
//Generate objectives
|
||||
GenerateObjectives(mindId, mind, thiefRule);
|
||||
|
||||
//Send briefing here to account for humanoid/animal
|
||||
_antagSelection.SendBriefing(thief, MakeBriefing(thief), null, thiefRule.GreetingSound);
|
||||
|
||||
// Give starting items
|
||||
_inventory.SpawnItemsOnEntity(thief, thiefRule.StarterItems);
|
||||
|
||||
thiefRule.ThievesMinds.Add(mindId);
|
||||
}
|
||||
|
||||
public void AdminMakeThief(EntityUid entity, bool addPacified)
|
||||
{
|
||||
var thiefRule = EntityQuery<ThiefRuleComponent>().FirstOrDefault();
|
||||
if (thiefRule == null)
|
||||
{
|
||||
GameTicker.StartGameRule("Thief", out var ruleEntity);
|
||||
thiefRule = Comp<ThiefRuleComponent>(ruleEntity);
|
||||
}
|
||||
|
||||
if (HasComp<ThiefRoleComponent>(entity))
|
||||
return;
|
||||
|
||||
MakeThief(entity, thiefRule, addPacified);
|
||||
GenerateObjectives(mindId, mind, ent);
|
||||
}
|
||||
|
||||
private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule)
|
||||
@@ -160,8 +79,7 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
|
||||
private string MakeBriefing(EntityUid thief)
|
||||
{
|
||||
var isHuman = HasComp<HumanoidAppearanceComponent>(thief);
|
||||
var briefing = "\n";
|
||||
briefing = isHuman
|
||||
var briefing = isHuman
|
||||
? Loc.GetString("thief-role-greeting-human")
|
||||
: Loc.GetString("thief-role-greeting-animal");
|
||||
|
||||
@@ -169,9 +87,9 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
|
||||
return briefing;
|
||||
}
|
||||
|
||||
private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> thiefs, ref ObjectivesTextGetInfoEvent args)
|
||||
private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> ent, ref ObjectivesTextGetInfoEvent args)
|
||||
{
|
||||
args.Minds = thiefs.Comp.ThievesMinds;
|
||||
args.Minds = _antag.GetAntagMindEntityUids(ent.Owner);
|
||||
args.AgentName = Loc.GetString("thief-round-end-agent-name");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,97 +5,61 @@ using Content.Server.Objectives;
|
||||
using Content.Server.PDA.Ringer;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.GameTicking.Components;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly UplinkSystem _uplink = default!;
|
||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||
[Dependency] private readonly ObjectivesSystem _objectives = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
|
||||
private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors);
|
||||
public const int MaxPicks = 20;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
||||
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(HandleLatejoin);
|
||||
SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
|
||||
|
||||
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
|
||||
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
|
||||
}
|
||||
|
||||
//Set min players on game rule
|
||||
protected override void Added(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
||||
{
|
||||
base.Added(uid, component, gameRule, args);
|
||||
|
||||
gameRule.MinPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
|
||||
}
|
||||
|
||||
protected override void Started(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
MakeCodewords(component);
|
||||
}
|
||||
|
||||
protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime)
|
||||
private void AfterEntitySelected(Entity<TraitorRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||
{
|
||||
base.ActiveTick(uid, component, gameRule, frameTime);
|
||||
|
||||
if (component.SelectionStatus < TraitorRuleComponent.SelectionState.Started && component.AnnounceAt < _timing.CurTime)
|
||||
{
|
||||
DoTraitorStart(component);
|
||||
component.SelectionStatus = TraitorRuleComponent.SelectionState.Started;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for enough players
|
||||
/// </summary>
|
||||
/// <param name="ev"></param>
|
||||
private void OnStartAttempt(RoundStartAttemptEvent ev)
|
||||
{
|
||||
TryRoundStartAttempt(ev, Loc.GetString("traitor-title"));
|
||||
MakeTraitor(args.EntityUid, ent);
|
||||
}
|
||||
|
||||
private void MakeCodewords(TraitorRuleComponent component)
|
||||
{
|
||||
var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
|
||||
var adjectives = _prototypeManager.Index<DatasetPrototype>(component.CodewordAdjectives).Values;
|
||||
var verbs = _prototypeManager.Index<DatasetPrototype>(component.CodewordVerbs).Values;
|
||||
var adjectives = _prototypeManager.Index(component.CodewordAdjectives).Values;
|
||||
var verbs = _prototypeManager.Index(component.CodewordVerbs).Values;
|
||||
var codewordPool = adjectives.Concat(verbs).ToList();
|
||||
var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count);
|
||||
var finalCodewordCount = Math.Min(component.CodewordCount, codewordPool.Count);
|
||||
component.Codewords = new string[finalCodewordCount];
|
||||
for (var i = 0; i < finalCodewordCount; i++)
|
||||
{
|
||||
@@ -103,66 +67,19 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
}
|
||||
}
|
||||
|
||||
private void DoTraitorStart(TraitorRuleComponent component)
|
||||
{
|
||||
var eligiblePlayers = _antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.TraitorPrototypeId);
|
||||
|
||||
if (eligiblePlayers.Count == 0)
|
||||
return;
|
||||
|
||||
var traitorsToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, PlayersPerTraitor, MaxTraitors);
|
||||
|
||||
var selectedTraitors = _antagSelection.ChooseAntags(traitorsToSelect, eligiblePlayers);
|
||||
|
||||
MakeTraitor(selectedTraitors, component);
|
||||
}
|
||||
|
||||
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
|
||||
{
|
||||
//Start the timer
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out _, out var comp, out var gameRuleComponent))
|
||||
{
|
||||
var delay = TimeSpan.FromSeconds(
|
||||
_cfg.GetCVar(CCVars.TraitorStartDelay) +
|
||||
_random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance)));
|
||||
|
||||
//Set the delay for choosing traitors
|
||||
comp.AnnounceAt = _timing.CurTime + delay;
|
||||
|
||||
comp.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToStart;
|
||||
}
|
||||
}
|
||||
|
||||
public bool MakeTraitor(List<EntityUid> traitors, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
|
||||
{
|
||||
foreach (var traitor in traitors)
|
||||
{
|
||||
MakeTraitor(traitor, component, giveUplink, giveObjectives);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
|
||||
{
|
||||
//Grab the mind if it wasnt provided
|
||||
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
|
||||
return false;
|
||||
|
||||
if (HasComp<TraitorRoleComponent>(mindId))
|
||||
{
|
||||
Log.Error($"Player {mind.CharacterName} is already a traitor.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
|
||||
|
||||
Note[]? code = null;
|
||||
if (giveUplink)
|
||||
{
|
||||
// Calculate the amount of currency on the uplink.
|
||||
var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance);
|
||||
var startingBalance = component.StartingBalance;
|
||||
if (_jobs.MindTryGetJob(mindId, out _, out var prototype))
|
||||
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
|
||||
|
||||
@@ -180,19 +97,14 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
|
||||
}
|
||||
|
||||
_antagSelection.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification);
|
||||
_antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification);
|
||||
|
||||
component.TraitorMinds.Add(mindId);
|
||||
|
||||
// Assign traitor roles
|
||||
_roleSystem.MindAddRole(mindId, new TraitorRoleComponent
|
||||
{
|
||||
PrototypeId = component.TraitorPrototypeId
|
||||
}, mind, true);
|
||||
// Assign briefing
|
||||
_roleSystem.MindAddRole(mindId, new RoleBriefingComponent
|
||||
{
|
||||
Briefing = briefing.ToString()
|
||||
Briefing = briefing
|
||||
}, mind, true);
|
||||
|
||||
// Change the faction
|
||||
@@ -202,11 +114,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
// Give traitors their objectives
|
||||
if (giveObjectives)
|
||||
{
|
||||
var maxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty);
|
||||
var maxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks);
|
||||
var difficulty = 0f;
|
||||
Log.Debug($"Attempting {maxPicks} objective picks with {maxDifficulty} difficulty");
|
||||
for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++)
|
||||
for (var pick = 0; pick < MaxPicks && component.MaxDifficulty > difficulty; pick++)
|
||||
{
|
||||
var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup);
|
||||
if (objective == null)
|
||||
@@ -222,53 +131,9 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleLatejoin(PlayerSpawnCompleteEvent ev)
|
||||
{
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out _, out var comp, out _))
|
||||
{
|
||||
if (comp.TotalTraitors >= MaxTraitors)
|
||||
continue;
|
||||
|
||||
if (!ev.LateJoin)
|
||||
continue;
|
||||
|
||||
if (!_antagSelection.IsPlayerEligible(ev.Player, comp.TraitorPrototypeId))
|
||||
continue;
|
||||
|
||||
//If its before we have selected traitors, continue
|
||||
if (comp.SelectionStatus < TraitorRuleComponent.SelectionState.Started)
|
||||
continue;
|
||||
|
||||
// the nth player we adjust our probabilities around
|
||||
var target = PlayersPerTraitor * comp.TotalTraitors + 1;
|
||||
var 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(chance))
|
||||
{
|
||||
MakeTraitor(ev.Mob, comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnObjectivesTextGetInfo(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextGetInfoEvent args)
|
||||
{
|
||||
args.Minds = comp.TraitorMinds;
|
||||
args.Minds = _antag.GetAntagMindEntityUids(uid);
|
||||
args.AgentName = Loc.GetString("traitor-round-end-agent-name");
|
||||
}
|
||||
|
||||
@@ -277,27 +142,6 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start this game rule manually
|
||||
/// </summary>
|
||||
public TraitorRuleComponent StartGameRule()
|
||||
{
|
||||
var comp = EntityQuery<TraitorRuleComponent>().FirstOrDefault();
|
||||
if (comp == null)
|
||||
{
|
||||
GameTicker.StartGameRule("Traitor", out var ruleEntity);
|
||||
comp = Comp<TraitorRuleComponent>(ruleEntity);
|
||||
}
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
public void MakeTraitorAdmin(EntityUid entity, bool giveUplink, bool giveObjectives)
|
||||
{
|
||||
var traitorRule = StartGameRule();
|
||||
MakeTraitor(entity, traitorRule, giveUplink, giveObjectives);
|
||||
}
|
||||
|
||||
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
@@ -312,9 +156,11 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
public List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind)
|
||||
{
|
||||
List<(EntityUid Id, MindComponent Mind)> allTraitors = new();
|
||||
foreach (var traitor in EntityQuery<TraitorRuleComponent>())
|
||||
|
||||
var query = EntityQueryEnumerator<TraitorRuleComponent>();
|
||||
while (query.MoveNext(out var uid, out var traitor))
|
||||
{
|
||||
foreach (var role in GetOtherTraitorMindsAliveAndConnected(ourMind, traitor))
|
||||
foreach (var role in GetOtherTraitorMindsAliveAndConnected(ourMind, (uid, traitor)))
|
||||
{
|
||||
if (!allTraitors.Contains(role))
|
||||
allTraitors.Add(role);
|
||||
@@ -324,20 +170,15 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
return allTraitors;
|
||||
}
|
||||
|
||||
private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, TraitorRuleComponent component)
|
||||
private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, Entity<TraitorRuleComponent> rule)
|
||||
{
|
||||
var traitors = new List<(EntityUid Id, MindComponent Mind)>();
|
||||
foreach (var traitor in component.TraitorMinds)
|
||||
foreach (var mind in _antag.GetAntagMinds(rule.Owner))
|
||||
{
|
||||
if (TryComp(traitor, out MindComponent? mind) &&
|
||||
mind.OwnedEntity != null &&
|
||||
mind.Session != null &&
|
||||
mind != ourMind &&
|
||||
_mobStateSystem.IsAlive(mind.OwnedEntity.Value) &&
|
||||
mind.CurrentEntity == mind.OwnedEntity)
|
||||
{
|
||||
traitors.Add((traitor, mind));
|
||||
}
|
||||
if (mind.Comp == ourMind)
|
||||
continue;
|
||||
|
||||
traitors.Add((mind, mind));
|
||||
}
|
||||
|
||||
return traitors;
|
||||
|
||||
@@ -1,99 +1,78 @@
|
||||
using Content.Server.Actions;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.Zombies;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Zombies;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Globalization;
|
||||
using Content.Server.GameTicking.Components;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly ActionsSystem _action = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly ZombieSystem _zombie = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
||||
SubscribeLocalEvent<PendingZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the required minimum players for this gamemode to start
|
||||
/// </summary>
|
||||
protected override void Added(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
||||
protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule,
|
||||
ref RoundEndTextAppendEvent args)
|
||||
{
|
||||
base.Added(uid, component, gameRule, args);
|
||||
base.AppendRoundEndText(uid, component, gameRule, ref args);
|
||||
|
||||
gameRule.MinPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
|
||||
}
|
||||
|
||||
private void OnRoundEndText(RoundEndTextAppendEvent ev)
|
||||
{
|
||||
foreach (var zombie in EntityQuery<ZombieRuleComponent>())
|
||||
{
|
||||
// This is just the general condition thing used for determining the win/lose text
|
||||
var fraction = GetInfectedFraction(true, true);
|
||||
|
||||
if (fraction <= 0)
|
||||
ev.AddLine(Loc.GetString("zombie-round-end-amount-none"));
|
||||
args.AddLine(Loc.GetString("zombie-round-end-amount-none"));
|
||||
else if (fraction <= 0.25)
|
||||
ev.AddLine(Loc.GetString("zombie-round-end-amount-low"));
|
||||
args.AddLine(Loc.GetString("zombie-round-end-amount-low"));
|
||||
else if (fraction <= 0.5)
|
||||
ev.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
|
||||
args.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
|
||||
else if (fraction < 1)
|
||||
ev.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
|
||||
args.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
|
||||
else
|
||||
ev.AddLine(Loc.GetString("zombie-round-end-amount-all"));
|
||||
args.AddLine(Loc.GetString("zombie-round-end-amount-all"));
|
||||
|
||||
ev.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", zombie.InitialInfectedNames.Count)));
|
||||
foreach (var player in zombie.InitialInfectedNames)
|
||||
var antags = _antag.GetAntagIdentifiers(uid);
|
||||
args.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", antags.Count)));
|
||||
foreach (var (_, data, entName) in antags)
|
||||
{
|
||||
ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
|
||||
("name", player.Key),
|
||||
("username", player.Value)));
|
||||
args.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
|
||||
("name", entName),
|
||||
("username", data.UserName)));
|
||||
}
|
||||
|
||||
var healthy = GetHealthyHumans();
|
||||
// Gets a bunch of the living players and displays them if they're under a threshold.
|
||||
// InitialInfected is used for the threshold because it scales with the player count well.
|
||||
if (healthy.Count <= 0 || healthy.Count > 2 * zombie.InitialInfectedNames.Count)
|
||||
continue;
|
||||
ev.AddLine("");
|
||||
ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count)));
|
||||
if (healthy.Count <= 0 || healthy.Count > 2 * antags.Count)
|
||||
return;
|
||||
args.AddLine("");
|
||||
args.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count)));
|
||||
foreach (var survivor in healthy)
|
||||
{
|
||||
var meta = MetaData(survivor);
|
||||
@@ -103,12 +82,11 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
||||
username = mind.Session.Name;
|
||||
}
|
||||
|
||||
ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
|
||||
args.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
|
||||
("name", meta.EntityName),
|
||||
("username", username)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The big kahoona function for checking if the round is gonna end
|
||||
@@ -134,39 +112,21 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
||||
_roundEnd.EndRound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check we have enough players to start this game mode, if not - cancel and announce
|
||||
/// </summary>
|
||||
private void OnStartAttempt(RoundStartAttemptEvent ev)
|
||||
{
|
||||
TryRoundStartAttempt(ev, Loc.GetString("zombie-title"));
|
||||
}
|
||||
|
||||
protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
|
||||
var delay = _random.Next(component.MinStartDelay, component.MaxStartDelay);
|
||||
component.StartTime = _timing.CurTime + delay;
|
||||
component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
|
||||
}
|
||||
|
||||
protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime)
|
||||
{
|
||||
base.ActiveTick(uid, component, gameRule, frameTime);
|
||||
|
||||
if (component.StartTime.HasValue && component.StartTime < _timing.CurTime)
|
||||
{
|
||||
InfectInitialPlayers(component);
|
||||
component.StartTime = null;
|
||||
component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
|
||||
}
|
||||
|
||||
if (component.NextRoundEndCheck.HasValue && component.NextRoundEndCheck < _timing.CurTime)
|
||||
{
|
||||
if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime)
|
||||
return;
|
||||
CheckRoundEnd(component);
|
||||
component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnZombifySelf(EntityUid uid, PendingZombieComponent component, ZombifySelfActionEvent args)
|
||||
{
|
||||
@@ -232,81 +192,4 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
||||
}
|
||||
return healthy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Infects the first players with the passive zombie virus.
|
||||
/// Also records their names for the end of round screen.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The reason this code is written separately is to facilitate
|
||||
/// allowing this gamemode to be started midround. As such, it doesn't need
|
||||
/// any information besides just running.
|
||||
/// </remarks>
|
||||
private void InfectInitialPlayers(ZombieRuleComponent component)
|
||||
{
|
||||
//Get all players with initial infected enabled, and exclude those with the ZombieImmuneComponent and roles with CanBeAntag = False
|
||||
var eligiblePlayers = _antagSelection.GetEligiblePlayers(
|
||||
_playerManager.Sessions,
|
||||
component.PatientZeroPrototypeId,
|
||||
includeAllJobs: false,
|
||||
customExcludeCondition: player => HasComp<ZombieImmuneComponent>(player) || HasComp<InitialInfectedExemptComponent>(player)
|
||||
);
|
||||
|
||||
//And get all players, excluding ZombieImmune and roles with CanBeAntag = False - to fill any leftover initial infected slots
|
||||
var allPlayers = _antagSelection.GetEligiblePlayers(
|
||||
_playerManager.Sessions,
|
||||
component.PatientZeroPrototypeId,
|
||||
acceptableAntags: Shared.Antag.AntagAcceptability.All,
|
||||
includeAllJobs: false ,
|
||||
ignorePreferences: true,
|
||||
customExcludeCondition: HasComp<ZombieImmuneComponent>
|
||||
);
|
||||
|
||||
//If there are no players to choose, abort
|
||||
if (allPlayers.Count == 0)
|
||||
return;
|
||||
|
||||
//How many initial infected should we select
|
||||
var initialInfectedCount = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerInfected, component.MaxInitialInfected);
|
||||
|
||||
//Choose the required number of initial infected from the eligible players, making up any shortfall by choosing from all players
|
||||
var initialInfected = _antagSelection.ChooseAntags(initialInfectedCount, eligiblePlayers, allPlayers);
|
||||
|
||||
//Make brain craving
|
||||
MakeZombie(initialInfected, component);
|
||||
|
||||
//Send the briefing, play greeting sound
|
||||
_antagSelection.SendBriefing(initialInfected, Loc.GetString("zombie-patientzero-role-greeting"), Color.Plum, component.InitialInfectedSound);
|
||||
}
|
||||
|
||||
private void MakeZombie(List<EntityUid> entities, ZombieRuleComponent component)
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
MakeZombie(entity, component);
|
||||
}
|
||||
}
|
||||
private void MakeZombie(EntityUid entity, ZombieRuleComponent component)
|
||||
{
|
||||
if (!_mindSystem.TryGetMind(entity, out var mind, out var mindComponent))
|
||||
return;
|
||||
|
||||
//Add the role to the mind silently (to avoid repeating job assignment)
|
||||
_roles.MindAddRole(mind, new InitialInfectedRoleComponent { PrototypeId = component.PatientZeroPrototypeId }, silent: true);
|
||||
EnsureComp<InitialInfectedComponent>(entity);
|
||||
|
||||
//Add the zombie components and grace period
|
||||
var pending = EnsureComp<PendingZombieComponent>(entity);
|
||||
pending.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
|
||||
EnsureComp<ZombifyOnDeathComponent>(entity);
|
||||
EnsureComp<IncurableZombieComponent>(entity);
|
||||
|
||||
//Add the zombify action
|
||||
_action.AddAction(entity, ref pending.Action, component.ZombifySelfActionPrototype, entity);
|
||||
|
||||
//Get names for the round end screen, incase they leave mid-round
|
||||
var inCharacterName = MetaData(entity).EntityName;
|
||||
var accountName = mindComponent.Session == null ? string.Empty : mindComponent.Session.Name;
|
||||
component.InitialInfectedNames.Add(inCharacterName, accountName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.Cuffs.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Shared.Objectives.Systems;
|
||||
using Content.Shared.Random;
|
||||
@@ -12,7 +9,9 @@ using Content.Shared.Random.Helpers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using System.Text;
|
||||
using Robust.Server.Player;
|
||||
|
||||
namespace Content.Server.Objectives;
|
||||
|
||||
@@ -20,8 +19,8 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
||||
{
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -179,8 +178,10 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
||||
.ThenByDescending(x => x.completedObjectives);
|
||||
|
||||
foreach (var (summary, _, _) in sortedAgents)
|
||||
{
|
||||
result.AppendLine(summary);
|
||||
}
|
||||
}
|
||||
|
||||
public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto)
|
||||
{
|
||||
@@ -244,8 +245,14 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
||||
return null;
|
||||
|
||||
var name = mind.CharacterName;
|
||||
_mind.TryGetSession(mindId, out var session);
|
||||
var username = session?.Name;
|
||||
var username = (string?) null;
|
||||
|
||||
if (mind.OriginalOwnerUserId != null &&
|
||||
_player.TryGetPlayerData(mind.OriginalOwnerUserId.Value, out var sessionData))
|
||||
{
|
||||
username = sessionData.UserName;
|
||||
}
|
||||
|
||||
|
||||
if (username != null)
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.GameTicking.Components;
|
||||
|
||||
namespace Content.Server.Power.EntitySystems;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Content.Server.Preferences.Managers
|
||||
|
||||
bool TryGetCachedPreferences(NetUserId userId, [NotNullWhen(true)] out PlayerPreferences? playerPreferences);
|
||||
PlayerPreferences GetPreferences(NetUserId userId);
|
||||
PlayerPreferences? GetPreferencesOrNull(NetUserId? userId);
|
||||
IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds);
|
||||
bool HavePreferencesLoaded(ICommonSession session);
|
||||
}
|
||||
|
||||
@@ -256,6 +256,20 @@ namespace Content.Server.Preferences.Managers
|
||||
return prefs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves preferences for the given username from storage or returns null.
|
||||
/// Creates and saves default preferences if they are not found, then returns them.
|
||||
/// </summary>
|
||||
public PlayerPreferences? GetPreferencesOrNull(NetUserId? userId)
|
||||
{
|
||||
if (userId == null)
|
||||
return null;
|
||||
|
||||
if (_cachedPlayerPrefs.TryGetValue(userId.Value, out var pref))
|
||||
return pref.Prefs;
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<PlayerPreferences> GetOrCreatePreferencesAsync(NetUserId userId)
|
||||
{
|
||||
var prefs = await _db.GetPlayerPreferencesAsync(userId);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Dataset;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
@@ -47,9 +47,12 @@ public sealed class RandomMetadataSystem : EntitySystem
|
||||
var outputSegments = new List<string>();
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
outputSegments.Add(_prototype.TryIndex<DatasetPrototype>(segment, out var proto)
|
||||
? Loc.GetString(_random.Pick(proto.Values))
|
||||
: Loc.GetString(segment));
|
||||
if (_prototype.TryIndex<DatasetPrototype>(segment, out var proto))
|
||||
outputSegments.Add(_random.Pick(proto.Values));
|
||||
else if (Loc.TryGetString(segment, out var localizedSegment))
|
||||
outputSegments.Add(localizedSegment);
|
||||
else
|
||||
outputSegments.Add(segment);
|
||||
}
|
||||
return string.Join(separator, outputSegments);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Numerics;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Spawners.Components;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
using Content.Server.StationEvents.Events;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.StationEvents.Components;
|
||||
|
||||
[RegisterComponent, Access(typeof(LoneOpsSpawnRule))]
|
||||
public sealed partial class LoneOpsSpawnRuleComponent : Component
|
||||
{
|
||||
[DataField("loneOpsShuttlePath")]
|
||||
public string LoneOpsShuttlePath = "Maps/Shuttles/striker.yml";
|
||||
|
||||
[DataField("gameRuleProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string GameRuleProto = "Nukeops";
|
||||
|
||||
[DataField("additionalRule")]
|
||||
public EntityUid? AdditionalRule;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Anomaly;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Resist;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Station.Components;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using Content.Server.Cargo.Components;
|
||||
using Content.Server.Cargo.Systems;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Server.StationRecords;
|
||||
using Content.Server.StationRecords.Systems;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.ImmovableRod;
|
||||
using Content.Server.StationEvents.Components;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Silicons.Laws;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Maps;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Server.RoundEnd;
|
||||
|
||||
namespace Content.Server.StationEvents.Events;
|
||||
|
||||
public sealed class LoneOpsSpawnRule : StationEventSystem<LoneOpsSpawnRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly MapLoaderSystem _map = default!;
|
||||
|
||||
protected override void Started(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
|
||||
// Loneops can only spawn if there is no nukeops active
|
||||
if (GameTicker.IsGameRuleAdded<NukeopsRuleComponent>())
|
||||
{
|
||||
ForceEndSelf(uid, gameRule);
|
||||
return;
|
||||
}
|
||||
|
||||
var shuttleMap = MapManager.CreateMap();
|
||||
var options = new MapLoadOptions
|
||||
{
|
||||
LoadMap = true,
|
||||
};
|
||||
|
||||
_map.TryLoad(shuttleMap, component.LoneOpsShuttlePath, out _, options);
|
||||
|
||||
var nukeopsEntity = GameTicker.AddGameRule(component.GameRuleProto);
|
||||
component.AdditionalRule = nukeopsEntity;
|
||||
var nukeopsComp = Comp<NukeopsRuleComponent>(nukeopsEntity);
|
||||
nukeopsComp.SpawnOutpost = false;
|
||||
nukeopsComp.RoundEndBehavior = RoundEndBehavior.Nothing;
|
||||
GameTicker.StartGameRule(nukeopsEntity);
|
||||
}
|
||||
|
||||
protected override void Ended(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
|
||||
{
|
||||
base.Ended(uid, component, gameRule, args);
|
||||
|
||||
if (component.AdditionalRule != null)
|
||||
GameTicker.EndGameRule(component.AdditionalRule.Value);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Server.Traits.Assorted;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Ninja.Systems;
|
||||
using Content.Server.Station.Components;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Server.Storage.Components;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Radio;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
|
||||
@@ -6,6 +6,7 @@ using JetBrains.Annotations;
|
||||
using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Station.Components;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.Traitor.Components;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Traitor.Systems;
|
||||
|
||||
@@ -9,7 +10,10 @@ namespace Content.Server.Traitor.Systems;
|
||||
/// </summary>
|
||||
public sealed class AutoTraitorSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string DefaultTraitorRule = "Traitor";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -20,44 +24,6 @@ public sealed class AutoTraitorSystem : EntitySystem
|
||||
|
||||
private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args)
|
||||
{
|
||||
TryMakeTraitor(uid, comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the GiveUplink field.
|
||||
/// </summary>
|
||||
public void SetGiveUplink(EntityUid uid, bool giveUplink, AutoTraitorComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp))
|
||||
return;
|
||||
|
||||
comp.GiveUplink = giveUplink;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the GiveObjectives field.
|
||||
/// </summary>
|
||||
public void SetGiveObjectives(EntityUid uid, bool giveObjectives, AutoTraitorComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp))
|
||||
return;
|
||||
|
||||
comp.GiveObjectives = giveObjectives;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there is a mind, then makes it a traitor using the options.
|
||||
/// </summary>
|
||||
public bool TryMakeTraitor(EntityUid uid, AutoTraitorComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp))
|
||||
return false;
|
||||
|
||||
//Start the rule if it has not already been started
|
||||
var traitorRuleComponent = _traitorRule.StartGameRule();
|
||||
_traitorRule.MakeTraitor(uid, traitorRuleComponent, giveUplink: comp.GiveUplink, giveObjectives: comp.GiveObjectives);
|
||||
// prevent spamming anything if it fails
|
||||
RemComp<AutoTraitorComponent>(uid);
|
||||
return true;
|
||||
_antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, DefaultTraitorRule);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,12 +83,9 @@ namespace Content.Server.Traitor.Uplink.Commands
|
||||
uplinkEntity = eUid;
|
||||
}
|
||||
|
||||
// Get TC count
|
||||
var tcCount = _cfgManager.GetCVar(CCVars.TraitorStartingBalance);
|
||||
Logger.Debug(_entManager.ToPrettyString(user));
|
||||
// Finally add uplink
|
||||
var uplinkSys = _entManager.System<UplinkSystem>();
|
||||
if (!uplinkSys.AddUplink(user, FixedPoint2.New(tcCount), uplinkEntity: uplinkEntity))
|
||||
if (!uplinkSys.AddUplink(user, 20, uplinkEntity: uplinkEntity))
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("add-uplink-command-error-2"));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Damage;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.Zombies;
|
||||
@@ -35,6 +36,21 @@ public sealed partial class PendingZombieComponent : Component
|
||||
[DataField("gracePeriod"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan GracePeriod = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum amount of time initial infected have before they start taking infection damage.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum amount of time initial infected have before they start taking damage.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f);
|
||||
|
||||
[DataField]
|
||||
public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead";
|
||||
|
||||
/// <summary>
|
||||
/// The chance each second that a warning will be shown.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Actions;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chat;
|
||||
using Content.Server.Chat.Systems;
|
||||
@@ -30,6 +31,7 @@ namespace Content.Server.Zombies
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly ActionsSystem _actions = default!;
|
||||
[Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
|
||||
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
@@ -74,6 +76,8 @@ namespace Content.Server.Zombies
|
||||
}
|
||||
|
||||
component.NextTick = _timing.CurTime + TimeSpan.FromSeconds(1f);
|
||||
component.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
|
||||
_actions.AddAction(uid, ref component.Action, component.ZombifySelfActionPrototype);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
|
||||
@@ -20,3 +20,8 @@ public enum AntagAcceptability
|
||||
All
|
||||
}
|
||||
|
||||
public enum AntagSelectionTime : byte
|
||||
{
|
||||
PrePlayerSpawn,
|
||||
PostPlayerSpawn
|
||||
}
|
||||
|
||||
@@ -403,91 +403,6 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
|
||||
CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY);
|
||||
|
||||
|
||||
/*
|
||||
* Suspicion
|
||||
*/
|
||||
|
||||
public static readonly CVarDef<int> SuspicionMinPlayers =
|
||||
CVarDef.Create("suspicion.min_players", 5);
|
||||
|
||||
public static readonly CVarDef<int> SuspicionMinTraitors =
|
||||
CVarDef.Create("suspicion.min_traitors", 2);
|
||||
|
||||
public static readonly CVarDef<int> SuspicionPlayersPerTraitor =
|
||||
CVarDef.Create("suspicion.players_per_traitor", 6);
|
||||
|
||||
public static readonly CVarDef<int> SuspicionStartingBalance =
|
||||
CVarDef.Create("suspicion.starting_balance", 20);
|
||||
|
||||
public static readonly CVarDef<int> SuspicionMaxTimeSeconds =
|
||||
CVarDef.Create("suspicion.max_time_seconds", 300);
|
||||
|
||||
/*
|
||||
* Traitor
|
||||
*/
|
||||
|
||||
public static readonly CVarDef<int> TraitorMinPlayers =
|
||||
CVarDef.Create("traitor.min_players", 5);
|
||||
|
||||
public static readonly CVarDef<int> TraitorMaxTraitors =
|
||||
CVarDef.Create("traitor.max_traitors", 12); // Assuming average server maxes somewhere from like 50-80 people
|
||||
|
||||
public static readonly CVarDef<int> TraitorPlayersPerTraitor =
|
||||
CVarDef.Create("traitor.players_per_traitor", 10);
|
||||
|
||||
public static readonly CVarDef<int> TraitorCodewordCount =
|
||||
CVarDef.Create("traitor.codeword_count", 4);
|
||||
|
||||
public static readonly CVarDef<int> TraitorStartingBalance =
|
||||
CVarDef.Create("traitor.starting_balance", 20);
|
||||
|
||||
public static readonly CVarDef<int> TraitorMaxDifficulty =
|
||||
CVarDef.Create("traitor.max_difficulty", 5);
|
||||
|
||||
public static readonly CVarDef<int> TraitorMaxPicks =
|
||||
CVarDef.Create("traitor.max_picks", 20);
|
||||
|
||||
public static readonly CVarDef<float> TraitorStartDelay =
|
||||
CVarDef.Create("traitor.start_delay", 4f * 60f);
|
||||
|
||||
public static readonly CVarDef<float> TraitorStartDelayVariance =
|
||||
CVarDef.Create("traitor.start_delay_variance", 3f * 60f);
|
||||
|
||||
/*
|
||||
* TraitorDeathMatch
|
||||
*/
|
||||
|
||||
public static readonly CVarDef<int> TraitorDeathMatchStartingBalance =
|
||||
CVarDef.Create("traitordm.starting_balance", 20);
|
||||
|
||||
/*
|
||||
* Zombie
|
||||
*/
|
||||
|
||||
public static readonly CVarDef<int> ZombieMinPlayers =
|
||||
CVarDef.Create("zombie.min_players", 20);
|
||||
|
||||
/*
|
||||
* Pirates
|
||||
*/
|
||||
|
||||
public static readonly CVarDef<int> PiratesMinPlayers =
|
||||
CVarDef.Create("pirates.min_players", 25);
|
||||
|
||||
public static readonly CVarDef<int> PiratesMaxOps =
|
||||
CVarDef.Create("pirates.max_pirates", 6);
|
||||
|
||||
public static readonly CVarDef<int> PiratesPlayersPerOp =
|
||||
CVarDef.Create("pirates.players_per_pirate", 5);
|
||||
|
||||
/*
|
||||
* Nukeops
|
||||
*/
|
||||
|
||||
public static readonly CVarDef<bool> NukeopsSpawnGhostRoles =
|
||||
CVarDef.Create("nukeops.spawn_ghost_roles", false);
|
||||
|
||||
/*
|
||||
* Tips
|
||||
*/
|
||||
|
||||
@@ -267,8 +267,11 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
||||
/// <param name="uid">The mob's entity UID.</param>
|
||||
/// <param name="profile">The character profile to load.</param>
|
||||
/// <param name="humanoid">Humanoid component of the entity</param>
|
||||
public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null)
|
||||
public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (profile == null)
|
||||
return;
|
||||
|
||||
if (!Resolve(uid, ref humanoid))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Inventory;
|
||||
@@ -96,7 +94,7 @@ public partial class InventorySystem
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity that you want to spawn an item on</param>
|
||||
/// <param name="items">A list of prototype IDs that you want to spawn in the bag.</param>
|
||||
public void SpawnItemsOnEntity(EntityUid entity, List<EntProtoId> items)
|
||||
public void SpawnItemsOnEntity(EntityUid entity, List<string> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
|
||||
@@ -13,11 +13,6 @@ namespace Content.Shared.NukeOps;
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class NukeOperativeComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Path to antagonist alert sound.
|
||||
/// </summary>
|
||||
[DataField("greetSoundNotification")]
|
||||
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg");
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
|
||||
@@ -62,6 +62,32 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
_antagTypes.Add(typeof(T));
|
||||
}
|
||||
|
||||
public void MindAddRole(EntityUid mindId, Component component, MindComponent? mind = null, bool silent = false)
|
||||
{
|
||||
if (!Resolve(mindId, ref mind))
|
||||
return;
|
||||
|
||||
if (HasComp(mindId, component.GetType()))
|
||||
{
|
||||
throw new ArgumentException($"We already have this role: {component}");
|
||||
}
|
||||
|
||||
EntityManager.AddComponent(mindId, component);
|
||||
var antagonist = IsAntagonistRole(component.GetType());
|
||||
|
||||
var mindEv = new MindRoleAddedEvent(silent);
|
||||
RaiseLocalEvent(mindId, ref mindEv);
|
||||
|
||||
var message = new RoleAddedEvent(mindId, mind, antagonist, silent);
|
||||
if (mind.OwnedEntity != null)
|
||||
{
|
||||
RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
|
||||
}
|
||||
|
||||
_adminLogger.Add(LogType.Mind, LogImpact.Low,
|
||||
$"'Role {component}' added to mind of {_minds.MindOwnerLoggingString(mind)}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gives this mind a new role.
|
||||
/// </summary>
|
||||
@@ -180,6 +206,11 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
return _antagTypes.Contains(typeof(T));
|
||||
}
|
||||
|
||||
public bool IsAntagonistRole(Type component)
|
||||
{
|
||||
return _antagTypes.Contains(component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play a sound for the mind, if it has a session attached.
|
||||
/// Use this for role greeting sounds.
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Station;
|
||||
|
||||
public abstract class SharedStationSpawningSystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
|
||||
[Dependency] protected readonly InventorySystem InventorySystem = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||
[Dependency] private readonly SharedStorageSystem _storage = default!;
|
||||
@@ -21,8 +22,22 @@ public abstract class SharedStationSpawningSystem : EntitySystem
|
||||
/// </summary>
|
||||
/// <param name="entity">Entity to load out.</param>
|
||||
/// <param name="startingGear">Starting gear to use.</param>
|
||||
public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear)
|
||||
public void EquipStartingGear(EntityUid entity, ProtoId<StartingGearPrototype>? startingGear)
|
||||
{
|
||||
PrototypeManager.TryIndex(startingGear, out var gearProto);
|
||||
EquipStartingGear(entity, gearProto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equips starting gear onto the given entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">Entity to load out.</param>
|
||||
/// <param name="startingGear">Starting gear to use.</param>
|
||||
public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingGear)
|
||||
{
|
||||
if (startingGear == null)
|
||||
return;
|
||||
|
||||
if (InventorySystem.TryGetSlots(entity, out var slotDefinitions))
|
||||
{
|
||||
foreach (var slot in slotDefinitions)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
pirates-title = Privateers
|
||||
pirates-description = A group of privateers has approached your lowly station. Hostile or not, their sole goal is to end the round with as many knicknacks on their ship as they can get.
|
||||
|
||||
pirates-no-ship = Through unknown circumstances, the privateer's ship was completely and utterly destroyed. No score.
|
||||
pirates-final-score = The privateers successfully obtained {$score} spesos worth
|
||||
pirates-final-score-2 = of knicknacks, with a total of {$finalPrice} spesos.
|
||||
pirates-list-start = The privateers were:
|
||||
pirates-most-valuable = The most valuable stolen items were:
|
||||
pirates-stolen-item-entry = {$entity} ({$credits} spesos)
|
||||
pirates-stole-nothing = - The pirates stole absolutely nothing at all. Point and laugh.
|
||||
@@ -1771,7 +1771,7 @@ entities:
|
||||
- type: Transform
|
||||
pos: 0.5436061,-7.5129323
|
||||
parent: 325
|
||||
- proto: SpawnPointLoneNukeOperative
|
||||
- proto: SpawnPointNukies
|
||||
entities:
|
||||
- uid: 322
|
||||
components:
|
||||
|
||||
@@ -84,8 +84,7 @@
|
||||
name: ghost-role-information-loneop-name
|
||||
description: ghost-role-information-loneop-description
|
||||
rules: ghost-role-information-loneop-rules
|
||||
- type: GhostRoleMobSpawner
|
||||
prototype: MobHumanLoneNuclearOperative
|
||||
- type: GhostRoleAntagSpawner
|
||||
- type: Sprite
|
||||
sprite: Markers/jobs.rsi
|
||||
layers:
|
||||
|
||||
@@ -405,11 +405,28 @@
|
||||
weight: 3
|
||||
duration: 1
|
||||
- type: ZombieRule
|
||||
minStartDelay: 0 #let them know immediately
|
||||
maxStartDelay: 10
|
||||
maxInitialInfected: 3 #fewer zombies
|
||||
minInitialInfectedGrace: 300 #less time to prepare
|
||||
- type: AntagSelection
|
||||
definitions:
|
||||
- prefRoles: [ InitialInfected ]
|
||||
max: 3
|
||||
playerRatio: 10
|
||||
blacklist:
|
||||
components:
|
||||
- ZombieImmune
|
||||
- InitialInfectedExempt
|
||||
briefing:
|
||||
text: zombie-patientzero-role-greeting
|
||||
color: Plum
|
||||
sound: "/Audio/Ambience/Antag/zombie_start.ogg"
|
||||
components:
|
||||
- type: PendingZombie #less time to prepare than normal
|
||||
minInitialInfectedGrace: 300
|
||||
maxInitialInfectedGrace: 450
|
||||
- type: ZombifyOnDeath
|
||||
- type: IncurableZombie
|
||||
mindComponents:
|
||||
- type: InitialInfectedRole
|
||||
prototype: InitialInfected
|
||||
|
||||
- type: entity
|
||||
id: LoneOpsSpawn
|
||||
@@ -422,7 +439,29 @@
|
||||
minimumPlayers: 20
|
||||
reoccurrenceDelay: 30
|
||||
duration: 1
|
||||
- type: LoneOpsSpawnRule
|
||||
- type: LoadMapRule
|
||||
mapPath: /Maps/Shuttles/striker.yml
|
||||
- type: NukeopsRule
|
||||
roundEndBehavior: Nothing
|
||||
- type: AntagSelection
|
||||
definitions:
|
||||
- spawnerPrototype: SpawnPointLoneNukeOperative
|
||||
min: 1
|
||||
max: 1
|
||||
pickPlayer: false
|
||||
startingGear: SyndicateLoneOperativeGearFull
|
||||
components:
|
||||
- type: NukeOperative
|
||||
- type: RandomMetadata
|
||||
nameSegments:
|
||||
- SyndicateNamesPrefix
|
||||
- SyndicateNamesNormal
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- Syndicate
|
||||
mindComponents:
|
||||
- type: NukeopsRole
|
||||
prototype: Nukeops
|
||||
|
||||
- type: entity
|
||||
id: MassHallucinations
|
||||
|
||||
@@ -34,6 +34,23 @@
|
||||
id: Thief
|
||||
components:
|
||||
- type: ThiefRule
|
||||
- type: AntagSelection
|
||||
definitions:
|
||||
- prefRoles: [ Thief ]
|
||||
maxRange:
|
||||
min: 1
|
||||
max: 3
|
||||
playerRatio: 1
|
||||
allowNonHumans: true
|
||||
multiAntagSetting: All
|
||||
startingGear: ThiefGear
|
||||
components:
|
||||
- type: Pacified
|
||||
mindComponents:
|
||||
- type: ThiefRole
|
||||
prototype: Thief
|
||||
briefing:
|
||||
sound: "/Audio/Misc/thief_greeting.ogg"
|
||||
|
||||
- type: entity
|
||||
noSpawn: true
|
||||
|
||||
@@ -70,29 +70,114 @@
|
||||
components:
|
||||
- type: GameRule
|
||||
minPlayers: 20
|
||||
- type: RandomMetadata #this generates the random operation name cuz it's cool.
|
||||
nameSegments:
|
||||
- operationPrefix
|
||||
- operationSuffix
|
||||
- type: NukeopsRule
|
||||
faction: Syndicate
|
||||
|
||||
- type: entity
|
||||
id: Pirates
|
||||
parent: BaseGameRule
|
||||
noSpawn: true
|
||||
- type: LoadMapRule
|
||||
gameMap: NukieOutpost
|
||||
- type: AntagSelection
|
||||
selectionTime: PrePlayerSpawn
|
||||
definitions:
|
||||
- prefRoles: [ NukeopsCommander ]
|
||||
fallbackRoles: [ Nukeops, NukeopsMedic ]
|
||||
max: 1
|
||||
playerRatio: 10
|
||||
startingGear: SyndicateCommanderGearFull
|
||||
components:
|
||||
- type: PiratesRule
|
||||
- type: NukeOperative
|
||||
- type: RandomMetadata
|
||||
nameSegments:
|
||||
- nukeops-role-commander
|
||||
- SyndicateNamesElite
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- Syndicate
|
||||
mindComponents:
|
||||
- type: NukeopsRole
|
||||
prototype: NukeopsCommander
|
||||
- prefRoles: [ NukeopsMedic ]
|
||||
fallbackRoles: [ Nukeops, NukeopsCommander ]
|
||||
max: 1
|
||||
playerRatio: 10
|
||||
startingGear: SyndicateOperativeMedicFull
|
||||
components:
|
||||
- type: NukeOperative
|
||||
- type: RandomMetadata
|
||||
nameSegments:
|
||||
- nukeops-role-agent
|
||||
- SyndicateNamesNormal
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- Syndicate
|
||||
mindComponents:
|
||||
- type: NukeopsRole
|
||||
prototype: NukeopsMedic
|
||||
- prefRoles: [ Nukeops ]
|
||||
fallbackRoles: [ NukeopsCommander, NukeopsMedic ]
|
||||
min: 0
|
||||
max: 3
|
||||
playerRatio: 10
|
||||
startingGear: SyndicateOperativeGearFull
|
||||
components:
|
||||
- type: NukeOperative
|
||||
- type: RandomMetadata
|
||||
nameSegments:
|
||||
- nukeops-role-operator
|
||||
- SyndicateNamesNormal
|
||||
- type: NpcFactionMember
|
||||
factions:
|
||||
- Syndicate
|
||||
mindComponents:
|
||||
- type: NukeopsRole
|
||||
prototype: Nukeops
|
||||
|
||||
- type: entity
|
||||
id: Traitor
|
||||
parent: BaseGameRule
|
||||
noSpawn: true
|
||||
components:
|
||||
- type: GameRule
|
||||
minPlayers: 5
|
||||
delay:
|
||||
min: 240
|
||||
max: 420
|
||||
- type: TraitorRule
|
||||
- type: AntagSelection
|
||||
definitions:
|
||||
- prefRoles: [ Traitor ]
|
||||
max: 12
|
||||
playerRatio: 10
|
||||
lateJoinAdditional: true
|
||||
mindComponents:
|
||||
- type: TraitorRole
|
||||
prototype: Traitor
|
||||
|
||||
- type: entity
|
||||
id: Revolutionary
|
||||
parent: BaseGameRule
|
||||
noSpawn: true
|
||||
components:
|
||||
- type: GameRule
|
||||
minPlayers: 15
|
||||
- type: RevolutionaryRule
|
||||
- type: AntagSelection
|
||||
definitions:
|
||||
- prefRoles: [ HeadRev ]
|
||||
max: 3
|
||||
playerRatio: 15
|
||||
briefing:
|
||||
text: head-rev-role-greeting
|
||||
color: CornflowerBlue
|
||||
sound: "/Audio/Ambience/Antag/headrev_start.ogg"
|
||||
startingGear: HeadRevGear
|
||||
components:
|
||||
- type: Revolutionary
|
||||
- type: HeadRevolutionary
|
||||
mindComponents:
|
||||
- type: RevolutionaryRole
|
||||
prototype: HeadRev
|
||||
|
||||
- type: entity
|
||||
id: Sandbox
|
||||
@@ -113,7 +198,32 @@
|
||||
parent: BaseGameRule
|
||||
noSpawn: true
|
||||
components:
|
||||
- type: GameRule
|
||||
minPlayers: 20
|
||||
delay:
|
||||
min: 600
|
||||
max: 900
|
||||
- type: ZombieRule
|
||||
- type: AntagSelection
|
||||
definitions:
|
||||
- prefRoles: [ InitialInfected ]
|
||||
max: 6
|
||||
playerRatio: 10
|
||||
blacklist:
|
||||
components:
|
||||
- ZombieImmune
|
||||
- InitialInfectedExempt
|
||||
briefing:
|
||||
text: zombie-patientzero-role-greeting
|
||||
color: Plum
|
||||
sound: "/Audio/Ambience/Antag/zombie_start.ogg"
|
||||
components:
|
||||
- type: PendingZombie
|
||||
- type: ZombifyOnDeath
|
||||
- type: IncurableZombie
|
||||
mindComponents:
|
||||
- type: InitialInfectedRole
|
||||
prototype: InitialInfected
|
||||
|
||||
# event schedulers
|
||||
- type: entity
|
||||
@@ -142,7 +252,6 @@
|
||||
- id: BasicTrashVariationPass
|
||||
- id: SolidWallRustingVariationPass
|
||||
- id: ReinforcedWallRustingVariationPass
|
||||
- id: CutWireVariationPass
|
||||
- id: BasicPuddleMessVariationPass
|
||||
prob: 0.99
|
||||
orGroup: puddleMess
|
||||
|
||||
@@ -273,8 +273,18 @@
|
||||
#Head Rev Gear
|
||||
- type: startingGear
|
||||
id: HeadRevGear
|
||||
equipment:
|
||||
pocket2: Flash
|
||||
storage:
|
||||
back:
|
||||
- Flash
|
||||
- ClothingEyesGlassesSunglasses
|
||||
|
||||
#Thief Gear
|
||||
- type: startingGear
|
||||
id: ThiefGear
|
||||
storage:
|
||||
back:
|
||||
- ToolboxThief
|
||||
- ClothingHandsChameleonThief
|
||||
|
||||
#Gladiator with spear
|
||||
- type: startingGear
|
||||
|
||||
@@ -153,15 +153,3 @@
|
||||
- Zombie
|
||||
- BasicStationEventScheduler
|
||||
- BasicRoundstartVariation
|
||||
|
||||
- type: gamePreset
|
||||
id: Pirates
|
||||
alias:
|
||||
- pirates
|
||||
name: pirates-title
|
||||
description: pirates-description
|
||||
showInVote: false
|
||||
rules:
|
||||
- Pirates
|
||||
- BasicStationEventScheduler
|
||||
- BasicRoundstartVariation
|
||||
|
||||
Reference in New Issue
Block a user