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
|
/// 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.
|
/// override this with the appearance data it sends over.
|
||||||
/// </remarks>
|
/// </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))
|
if (!Resolve(uid, ref humanoid))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -112,22 +112,38 @@ public sealed class NukeOpsTest
|
|||||||
|
|
||||||
// The game rule exists, and all the stations/shuttles/maps are properly initialized
|
// The game rule exists, and all the stations/shuttles/maps are properly initialized
|
||||||
var rule = entMan.AllComponents<NukeopsRuleComponent>().Single().Component;
|
var rule = entMan.AllComponents<NukeopsRuleComponent>().Single().Component;
|
||||||
Assert.That(entMan.EntityExists(rule.NukieOutpost));
|
var mapRule = entMan.AllComponents<LoadMapRuleComponent>().Single().Component;
|
||||||
Assert.That(entMan.EntityExists(rule.NukieShuttle));
|
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.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));
|
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(entMan.EntityExists(nukieStation.Station));
|
||||||
Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation));
|
Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation));
|
||||||
|
|
||||||
Assert.That(server.MapMan.MapExists(rule.NukiePlanet));
|
Assert.That(server.MapMan.MapExists(mapRule.Map));
|
||||||
var nukieMap = mapSys.GetMap(rule.NukiePlanet!.Value);
|
var nukieMap = mapSys.GetMap(mapRule.Map!.Value);
|
||||||
|
|
||||||
var targetStation = entMan.GetComponent<StationDataComponent>(rule.TargetStation!.Value);
|
var targetStation = entMan.GetComponent<StationDataComponent>(rule.TargetStation!.Value);
|
||||||
var targetGrid = targetStation.Grids.First();
|
var targetGrid = targetStation.Grids.First();
|
||||||
@@ -135,8 +151,8 @@ public sealed class NukeOpsTest
|
|||||||
Assert.That(targetMap, Is.Not.EqualTo(nukieMap));
|
Assert.That(targetMap, Is.Not.EqualTo(nukieMap));
|
||||||
|
|
||||||
Assert.That(entMan.GetComponent<TransformComponent>(player).MapUid, Is.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>(nukieStationEnt.Value).MapUid, Is.EqualTo(nukieMap));
|
||||||
Assert.That(entMan.GetComponent<TransformComponent>(rule.NukieShuttle!.Value).MapUid, Is.EqualTo(nukieMap));
|
Assert.That(entMan.GetComponent<TransformComponent>(nukieShuttlEnt).MapUid, Is.EqualTo(nukieMap));
|
||||||
|
|
||||||
// The maps are all map-initialized, including the player
|
// The maps are all map-initialized, including the player
|
||||||
// Yes, this is necessary as this has repeatedly been broken somehow.
|
// 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(player), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||||
Assert.That(LifeStage(nukieMap), Is.GreaterThan(EntityLifeStage.Initialized));
|
Assert.That(LifeStage(nukieMap), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||||
Assert.That(LifeStage(targetMap), Is.GreaterThan(EntityLifeStage.Initialized));
|
Assert.That(LifeStage(targetMap), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||||
Assert.That(LifeStage(rule.NukieOutpost), Is.GreaterThan(EntityLifeStage.Initialized));
|
Assert.That(LifeStage(nukieStationEnt.Value), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||||
Assert.That(LifeStage(rule.NukieShuttle), Is.GreaterThan(EntityLifeStage.Initialized));
|
Assert.That(LifeStage(nukieShuttlEnt), Is.GreaterThan(EntityLifeStage.Initialized));
|
||||||
Assert.That(LifeStage(rule.TargetStation), 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.
|
// 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;
|
||||||
using Content.Server.GameTicking.Commands;
|
using Content.Server.GameTicking.Commands;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules;
|
using Content.Server.GameTicking.Rules;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Shared.CCVar;
|
using Content.Shared.CCVar;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public sealed class SecretStartsTest
|
|||||||
|
|
||||||
var server = pair.Server;
|
var server = pair.Server;
|
||||||
await server.WaitIdleAsync();
|
await server.WaitIdleAsync();
|
||||||
|
var entMan = server.ResolveDependency<IEntityManager>();
|
||||||
var gameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
|
var gameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
|
||||||
|
|
||||||
await server.WaitAssertion(() =>
|
await server.WaitAssertion(() =>
|
||||||
@@ -32,10 +33,7 @@ public sealed class SecretStartsTest
|
|||||||
|
|
||||||
await server.WaitAssertion(() =>
|
await server.WaitAssertion(() =>
|
||||||
{
|
{
|
||||||
foreach (var rule in gameTicker.GetAddedGameRules())
|
Assert.That(gameTicker.GetAddedGameRules().Count(), Is.GreaterThan(1), $"No additional rules started by secret rule.");
|
||||||
{
|
|
||||||
Assert.That(gameTicker.GetActiveGameRules(), Does.Contain(rule));
|
|
||||||
}
|
|
||||||
|
|
||||||
// End all rules
|
// End all rules
|
||||||
gameTicker.ClearGameRules();
|
gameTicker.ClearGameRules();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Text.Json.Nodes;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Administration.Systems;
|
using Content.Server.Administration.Systems;
|
||||||
using Content.Server.GameTicking;
|
using Content.Server.GameTicking;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Presets;
|
using Content.Server.GameTicking.Presets;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Maps;
|
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.Server.Zombies;
|
||||||
using Content.Shared.Administration;
|
using Content.Shared.Administration;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.Humanoid;
|
|
||||||
using Content.Shared.Mind.Components;
|
using Content.Shared.Mind.Components;
|
||||||
|
using Content.Shared.Roles;
|
||||||
using Content.Shared.Verbs;
|
using Content.Shared.Verbs;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Administration.Systems;
|
namespace Content.Server.Administration.Systems;
|
||||||
|
|
||||||
public sealed partial class AdminVerbSystem
|
public sealed partial class AdminVerbSystem
|
||||||
{
|
{
|
||||||
|
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||||
[Dependency] private readonly ZombieSystem _zombie = default!;
|
[Dependency] private readonly ZombieSystem _zombie = default!;
|
||||||
[Dependency] private readonly ThiefRuleSystem _thief = default!;
|
|
||||||
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
|
[ValidatePrototypeId<EntityPrototype>]
|
||||||
[Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!;
|
private const string DefaultTraitorRule = "Traitor";
|
||||||
[Dependency] private readonly PiratesRuleSystem _piratesRule = default!;
|
|
||||||
[Dependency] private readonly RevolutionaryRuleSystem _revolutionaryRule = default!;
|
[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.
|
// All antag verbs have names so invokeverb works.
|
||||||
private void AddAntagVerbs(GetVerbsEvent<Verb> args)
|
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"),
|
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
{
|
{
|
||||||
// if its a monkey or mouse or something dont give uplink or objectives
|
_antag.ForceMakeAntag<TraitorRuleComponent>(player, DefaultTraitorRule);
|
||||||
var isHuman = HasComp<HumanoidAppearanceComponent>(args.Target);
|
|
||||||
_traitorRule.MakeTraitorAdmin(args.Target, giveUplink: isHuman, giveObjectives: isHuman);
|
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-traitor"),
|
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"),
|
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
{
|
{
|
||||||
_nukeopsRule.MakeLoneNukie(args.Target);
|
_antag.ForceMakeAntag<NukeopsRuleComponent>(player, DefaultNukeOpRule);
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-nuclear-operative"),
|
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"),
|
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"),
|
||||||
Act = () =>
|
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,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-pirate"),
|
Message = Loc.GetString("admin-verb-make-pirate"),
|
||||||
};
|
};
|
||||||
args.Verbs.Add(pirate);
|
args.Verbs.Add(pirate);
|
||||||
|
|
||||||
//todo come here at some point dear lort.
|
|
||||||
Verb headRev = new()
|
Verb headRev = new()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("admin-verb-text-make-head-rev"),
|
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"),
|
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
{
|
{
|
||||||
_revolutionaryRule.OnHeadRevAdmin(args.Target);
|
_antag.ForceMakeAntag<RevolutionaryRuleComponent>(player, DefaultRevsRule);
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-head-rev"),
|
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"),
|
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
{
|
{
|
||||||
_thief.AdminMakeThief(args.Target, false); //Midround add pacified is bad
|
_antag.ForceMakeAntag<ThiefRuleComponent>(player, DefaultThiefRule);
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-thief"),
|
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;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.Ghost.Roles;
|
||||||
|
using Content.Server.Ghost.Roles.Components;
|
||||||
using Content.Server.Mind;
|
using Content.Server.Mind;
|
||||||
using Content.Server.Preferences.Managers;
|
using Content.Server.Preferences.Managers;
|
||||||
|
using Content.Server.Roles;
|
||||||
using Content.Server.Roles.Jobs;
|
using Content.Server.Roles.Jobs;
|
||||||
using Content.Server.Shuttles.Components;
|
using Content.Server.Shuttles.Components;
|
||||||
|
using Content.Server.Station.Systems;
|
||||||
using Content.Shared.Antag;
|
using Content.Shared.Antag;
|
||||||
|
using Content.Shared.Ghost;
|
||||||
using Content.Shared.Humanoid;
|
using Content.Shared.Humanoid;
|
||||||
using Content.Shared.Players;
|
using Content.Shared.Players;
|
||||||
using Content.Shared.Preferences;
|
using Content.Shared.Preferences;
|
||||||
using Content.Shared.Roles;
|
|
||||||
using Robust.Server.Audio;
|
using Robust.Server.Audio;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Random;
|
|
||||||
using System.Linq;
|
|
||||||
using Content.Shared.Chat;
|
|
||||||
using Robust.Shared.Enums;
|
using Robust.Shared.Enums;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
namespace Content.Server.Antag;
|
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 IChatManager _chat = default!;
|
||||||
[Dependency] private readonly AudioSystem _audioSystem = 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 JobSystem _jobs = default!;
|
||||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
[Dependency] private readonly MapSystem _map = default!;
|
||||||
[Dependency] private readonly SharedRoleSystem _roleSystem = 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
|
// arbitrary random number to give late joining some mild interest.
|
||||||
/// <summary>
|
public const float LateJoinRandomChance = 0.5f;
|
||||||
/// Get all players that are eligible for an antag role
|
|
||||||
/// </summary>
|
/// <inheritdoc/>
|
||||||
/// <param name="playerSessions">All sessions from which to select eligible players</param>
|
public override void Initialize()
|
||||||
/// <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>();
|
base.Initialize();
|
||||||
|
|
||||||
foreach (var player in playerSessions)
|
SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
|
||||||
|
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
|
||||||
|
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayerSpawning(RulePlayerSpawningEvent args)
|
||||||
|
{
|
||||||
|
var pool = args.PlayerPool;
|
||||||
|
|
||||||
|
var query = QueryActiveRules();
|
||||||
|
while (query.MoveNext(out var uid, out _, out var comp, out _))
|
||||||
{
|
{
|
||||||
if (IsPlayerEligible(player, antagPrototype, includeAllJobs, acceptableAntags, ignorePreferences, allowNonHumanoids, customExcludeCondition))
|
if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn)
|
||||||
eligiblePlayers.Add(player.AttachedEntity!.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return eligiblePlayers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
var eligibleSessions = new List<ICommonSession>();
|
|
||||||
|
|
||||||
foreach (var session in playerSessions)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
//Pick and choose a random number of players from this list
|
if (comp.SelectionsComplete)
|
||||||
chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList));
|
return;
|
||||||
|
|
||||||
|
ChooseAntags((uid, comp), pool);
|
||||||
|
comp.SelectionsComplete = true;
|
||||||
|
|
||||||
|
foreach (var session in comp.SelectedSessions)
|
||||||
|
{
|
||||||
|
args.PlayerPool.Remove(session);
|
||||||
|
GameTicker.PlayerJoinGame(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return chosenPlayers;
|
|
||||||
}
|
}
|
||||||
/// <summary>
|
|
||||||
/// Helper method to choose antags from a list
|
private void OnJobsAssigned(RulePlayerJobsAssignedEvent args)
|
||||||
/// </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)
|
|
||||||
{
|
{
|
||||||
var chosenPlayers = new List<EntityUid>();
|
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++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
if (eligiblePlayers.Count == 0)
|
var session = (ICommonSession?) null;
|
||||||
break;
|
if (def.PickPlayer)
|
||||||
|
|
||||||
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 (!playerPool.TryPickAndTake(RobustRandom, out session))
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (ent.Comp.SelectedSessions.Contains(session))
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
//If we have reached the desired number of players, skip
|
MakeAntag(ent, session, def);
|
||||||
if (chosenPlayers.Count >= count)
|
|
||||||
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++)
|
/// <summary>
|
||||||
|
/// Makes a given player into the specified antagonist.
|
||||||
|
/// </summary>
|
||||||
|
public void MakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false)
|
||||||
|
{
|
||||||
|
var antagEnt = (EntityUid?) null;
|
||||||
|
var isSpawner = false;
|
||||||
|
|
||||||
|
if (session != null)
|
||||||
{
|
{
|
||||||
if (eligiblePlayers.Count == 0)
|
ent.Comp.SelectedSessions.Add(session);
|
||||||
break;
|
|
||||||
|
|
||||||
chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
|
// 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.
|
||||||
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);
|
antagEnt = Spawn(def.SpawnerPrototype);
|
||||||
|
isSpawner = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
if (!antagEnt.HasValue)
|
||||||
/// Helper method to send the briefing text and sound to a player entity
|
{
|
||||||
/// </summary>
|
var getEntEv = new AntagSelectEntityEvent(session, ent);
|
||||||
/// <param name="entity">The entity chosen to be antag</param>
|
RaiseLocalEvent(ent, ref getEntEv, true);
|
||||||
/// <param name="briefing">The briefing text to send</param>
|
|
||||||
/// <param name="briefingColor">The color the briefing should be, null for default</param>
|
if (!getEntEv.Handled)
|
||||||
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
|
{
|
||||||
public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
|
throw new InvalidOperationException($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
|
||||||
{
|
}
|
||||||
if (!_mindSystem.TryGetMind(entity, out _, out var mindComponent))
|
|
||||||
|
antagEnt = getEntEv.Entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (antagEnt is not { } player)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (mindComponent.Session == null)
|
var getPosEv = new AntagSelectLocationEvent(session, ent);
|
||||||
return;
|
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>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="sessions"></param>
|
public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, List<ICommonSession> sessions, AntagSelectionDefinition def)
|
||||||
/// <param name="briefing"></param>
|
|
||||||
/// <param name="briefingColor"></param>
|
|
||||||
/// <param name="briefingSound"></param>
|
|
||||||
|
|
||||||
public void SendBriefing(List<ICommonSession> sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
|
|
||||||
{
|
{
|
||||||
|
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)
|
foreach (var session in sessions)
|
||||||
{
|
{
|
||||||
SendBriefing(session, briefing, briefingColor, briefingSound);
|
if (!IsSessionValid(ent, session, def) ||
|
||||||
}
|
!IsEntityValid(session.AttachedEntity, def))
|
||||||
}
|
{
|
||||||
/// <summary>
|
rawList.Add(session);
|
||||||
/// Helper method to send the briefing text and sound to a session
|
continue;
|
||||||
/// </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)
|
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
|
||||||
{
|
if (def.PrefRoles.Count == 0 || pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p)))
|
||||||
_audioSystem.PlayGlobal(briefingSound, session);
|
{
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing));
|
primaryList.Add(session);
|
||||||
ChatManager.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, briefingColor);
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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.Antag.Mimic;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules;
|
using Content.Server.GameTicking.Rules;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Shared.VendingMachines;
|
using Content.Shared.VendingMachines;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Content.Server.Destructible.Thresholds
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Destructible.Thresholds
|
||||||
{
|
{
|
||||||
[Serializable]
|
[Serializable]
|
||||||
[DataDefinition]
|
[DataDefinition]
|
||||||
@@ -9,5 +11,16 @@
|
|||||||
|
|
||||||
[DataField("max")]
|
[DataField("max")]
|
||||||
public int 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>
|
/// <summary>
|
||||||
/// Added to game rules before <see cref="GameRuleStartedEvent"/> and removed before <see cref="GameRuleEndedEvent"/>.
|
/// 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>
|
/// <summary>
|
||||||
/// Added to game rules before <see cref="GameRuleEndedEvent"/>.
|
/// Added to game rules before <see cref="GameRuleEndedEvent"/>.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
using Content.Server.Destructible.Thresholds;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||||
|
|
||||||
namespace Content.Server.GameTicking.Rules.Components;
|
namespace Content.Server.GameTicking.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Component attached to all gamerule entities.
|
/// Component attached to all gamerule entities.
|
||||||
@@ -20,6 +21,12 @@ public sealed partial class GameRuleComponent : Component
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField]
|
[DataField]
|
||||||
public int MinPlayers;
|
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>
|
/// <summary>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.Administration;
|
using Content.Server.Administration;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Shared.Administration;
|
using Content.Shared.Administration;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.Prototypes;
|
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
|
if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
|
||||||
return false;
|
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));
|
_allPreviousGameRules.Add((RoundDuration(), id));
|
||||||
_sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}");
|
_sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}");
|
||||||
_adminLogger.Add(LogType.EventStarted, $"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
|
#region Command Implementations
|
||||||
|
|
||||||
[AdminCommand(AdminFlags.Fun)]
|
[AdminCommand(AdminFlags.Fun)]
|
||||||
@@ -323,38 +351,3 @@ public sealed partial class GameTicker
|
|||||||
|
|
||||||
#endregion
|
#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;
|
return;
|
||||||
base.Update(frameTime);
|
base.Update(frameTime);
|
||||||
UpdateRoundFlow(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>
|
/// <summary>
|
||||||
/// Stores some configuration used by the ninja system.
|
/// 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>
|
/// </summary>
|
||||||
[RegisterComponent, Access(typeof(SpaceNinjaSystem))]
|
[RegisterComponent, Access(typeof(SpaceNinjaSystem))]
|
||||||
public sealed partial class NinjaRuleComponent : Component
|
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;
|
namespace Content.Server.GameTicking.Rules.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -9,11 +6,5 @@ namespace Content.Server.GameTicking.Rules.Components;
|
|||||||
/// TODO: Remove once systems can request spawns from the ghost role system directly.
|
/// TODO: Remove once systems can request spawns from the ghost role system directly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
public sealed partial class NukeOperativeSpawnerComponent : Component
|
public sealed partial class NukeOperativeSpawnerComponent : Component;
|
||||||
{
|
|
||||||
[DataField("name", required:true)]
|
|
||||||
public string OperativeName = default!;
|
|
||||||
|
|
||||||
[DataField]
|
|
||||||
public NukeopSpawnPreset SpawnDetails = default!;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,4 +6,6 @@
|
|||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
public sealed partial class NukeOpsShuttleComponent : Component
|
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.RoundEnd;
|
||||||
using Content.Server.StationEvents.Events;
|
|
||||||
using Content.Shared.Dataset;
|
using Content.Shared.Dataset;
|
||||||
using Content.Shared.NPC.Prototypes;
|
using Content.Shared.NPC.Prototypes;
|
||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||||
|
|
||||||
namespace Content.Server.GameTicking.Rules.Components;
|
namespace Content.Server.GameTicking.Rules.Components;
|
||||||
|
|
||||||
[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))]
|
[RegisterComponent, Access(typeof(NukeopsRuleSystem))]
|
||||||
public sealed partial class NukeopsRuleComponent : Component
|
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>
|
/// <summary>
|
||||||
/// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event.
|
/// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -52,12 +41,6 @@ public sealed partial class NukeopsRuleComponent : Component
|
|||||||
[DataField]
|
[DataField]
|
||||||
public TimeSpan EvacShuttleTime = TimeSpan.FromMinutes(3);
|
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>
|
/// <summary>
|
||||||
/// Whether or not nukie left their outpost
|
/// Whether or not nukie left their outpost
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -80,7 +63,7 @@ public sealed partial class NukeopsRuleComponent : Component
|
|||||||
/// This amount of TC will be given to each nukie
|
/// This amount of TC will be given to each nukie
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField]
|
[DataField]
|
||||||
public int WarTCAmountPerNukie = 40;
|
public int WarTcAmountPerNukie = 40;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delay between war declaration and nuke ops arrival on station map. Gives crew time to prepare
|
/// 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]
|
[DataField]
|
||||||
public int WarDeclarationMinOps = 4;
|
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]
|
[DataField]
|
||||||
public WinType WinType = WinType.Neutral;
|
public WinType WinType = WinType.Neutral;
|
||||||
|
|
||||||
[DataField]
|
[DataField]
|
||||||
public List<WinCondition> WinConditions = new ();
|
public List<WinCondition> WinConditions = new ();
|
||||||
|
|
||||||
// TODO full game save
|
[DataField]
|
||||||
// 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;
|
|
||||||
public EntityUid? TargetStation;
|
public EntityUid? TargetStation;
|
||||||
public MapId? NukiePlanet;
|
|
||||||
|
[DataField]
|
||||||
|
public ProtoId<NpcFactionPrototype> Faction = "Syndicate";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
|
/// Path to antagonist alert sound.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField]
|
[DataField]
|
||||||
public Dictionary<EntityUid, string> OperativeMindPendingData = new();
|
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg");
|
||||||
|
|
||||||
[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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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]
|
[DataField]
|
||||||
public TimeSpan TimerWait = TimeSpan.FromSeconds(20);
|
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>
|
/// <summary>
|
||||||
/// The time it takes after the last head is killed for the shuttle to arrive.
|
/// The time it takes after the last head is killed for the shuttle to arrive.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
using Content.Shared.Random;
|
using Content.Shared.Random;
|
||||||
using Content.Shared.Roles;
|
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
namespace Content.Server.GameTicking.Rules.Components;
|
namespace Content.Server.GameTicking.Rules.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stores data for <see cref="ThiefRuleSystem/">.
|
/// Stores data for <see cref="ThiefRuleSystem"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent, Access(typeof(ThiefRuleSystem))]
|
[RegisterComponent, Access(typeof(ThiefRuleSystem))]
|
||||||
public sealed partial class ThiefRuleComponent : Component
|
public sealed partial class ThiefRuleComponent : Component
|
||||||
@@ -23,42 +22,9 @@ public sealed partial class ThiefRuleComponent : Component
|
|||||||
[DataField]
|
[DataField]
|
||||||
public float BigObjectiveChance = 0.7f;
|
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]
|
[DataField]
|
||||||
public float MaxObjectiveDifficulty = 2.5f;
|
public float MaxObjectiveDifficulty = 2.5f;
|
||||||
|
|
||||||
[DataField]
|
[DataField]
|
||||||
public int MaxStealObjectives = 10;
|
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>
|
/// </summary>
|
||||||
[DataField]
|
[DataField]
|
||||||
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
|
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))]
|
[RegisterComponent, Access(typeof(ZombieRuleSystem))]
|
||||||
public sealed partial class ZombieRuleComponent : Component
|
public sealed partial class ZombieRuleComponent : Component
|
||||||
{
|
{
|
||||||
[DataField]
|
|
||||||
public Dictionary<string, string> InitialInfectedNames = new();
|
|
||||||
|
|
||||||
[DataField]
|
|
||||||
public ProtoId<AntagPrototype> PatientZeroPrototypeId = "InitialInfected";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When the round will next check for round end.
|
/// When the round will next check for round end.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -26,61 +20,9 @@ public sealed partial class ZombieRuleComponent : Component
|
|||||||
[DataField]
|
[DataField]
|
||||||
public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30);
|
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>
|
/// <summary>
|
||||||
/// After this amount of the crew become zombies, the shuttle will be automatically called.
|
/// After this amount of the crew become zombies, the shuttle will be automatically called.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField]
|
[DataField]
|
||||||
public float ZombieShuttleCallPercentage = 0.7f;
|
public float ZombieShuttleCallPercentage = 0.7f;
|
||||||
|
|
||||||
[DataField]
|
|
||||||
public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.Administration.Commands;
|
using Content.Server.Administration.Commands;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.KillTracking;
|
using Content.Server.KillTracking;
|
||||||
using Content.Server.Mind;
|
using Content.Server.Mind;
|
||||||
@@ -33,7 +34,6 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
|
|||||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
|
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
|
||||||
SubscribeLocalEvent<KillReportedEvent>(OnKillReported);
|
SubscribeLocalEvent<KillReportedEvent>(OnKillReported);
|
||||||
SubscribeLocalEvent<DeathMatchRuleComponent, PlayerPointChangedEvent>(OnPointChanged);
|
SubscribeLocalEvent<DeathMatchRuleComponent, PlayerPointChangedEvent>(OnPointChanged);
|
||||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextAppend);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev)
|
private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev)
|
||||||
@@ -113,21 +113,17 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
|
|||||||
_roundEnd.EndRound(component.RestartDelay);
|
_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>();
|
if (!TryComp<PointManagerComponent>(uid, out var point))
|
||||||
while (query.MoveNext(out var uid, out var dm, out var point, out var rule))
|
return;
|
||||||
{
|
|
||||||
if (!GameTicker.IsGameRuleAdded(uid, rule))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
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)));
|
args.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName)));
|
||||||
ev.AddLine("");
|
args.AddLine("");
|
||||||
}
|
|
||||||
ev.AddLine(Loc.GetString("point-scoreboard-header"));
|
|
||||||
ev.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup());
|
|
||||||
}
|
}
|
||||||
|
args.AddLine(Loc.GetString("point-scoreboard-header"));
|
||||||
|
args.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
using Robust.Shared.Collections;
|
using Robust.Shared.Collections;
|
||||||
@@ -15,31 +16,6 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
|
|||||||
return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
|
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>
|
/// <summary>
|
||||||
/// Utility function for finding a random event-eligible station entity
|
/// Utility function for finding a random event-eligible station entity
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Content.Server.Atmos.EntitySystems;
|
using Content.Server.Atmos.EntitySystems;
|
||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Components;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
@@ -22,9 +22,31 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
|
|||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
||||||
SubscribeLocalEvent<T, GameRuleAddedEvent>(OnGameRuleAdded);
|
SubscribeLocalEvent<T, GameRuleAddedEvent>(OnGameRuleAdded);
|
||||||
SubscribeLocalEvent<T, GameRuleStartedEvent>(OnGameRuleStarted);
|
SubscribeLocalEvent<T, GameRuleStartedEvent>(OnGameRuleStarted);
|
||||||
SubscribeLocalEvent<T, GameRuleEndedEvent>(OnGameRuleEnded);
|
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)
|
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);
|
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>
|
/// <summary>
|
||||||
/// Called when the gamerule is added
|
/// 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>
|
/// <summary>
|
||||||
/// Called on an active gamerule entity in the Update function
|
/// Called on an active gamerule entity in the Update function
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.KillTracking;
|
using Content.Server.KillTracking;
|
||||||
using Content.Shared.Chat;
|
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 System.Threading;
|
||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Timer = Robust.Shared.Timing.Timer;
|
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.Antag;
|
||||||
using Content.Server.Communications;
|
using Content.Server.Communications;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
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.Humanoid;
|
||||||
using Content.Server.Mind;
|
|
||||||
using Content.Server.Nuke;
|
using Content.Server.Nuke;
|
||||||
using Content.Server.NukeOps;
|
using Content.Server.NukeOps;
|
||||||
using Content.Server.Popups;
|
using Content.Server.Popups;
|
||||||
using Content.Server.Preferences.Managers;
|
using Content.Server.Preferences.Managers;
|
||||||
using Content.Server.RandomMetadata;
|
|
||||||
using Content.Server.Roles;
|
using Content.Server.Roles;
|
||||||
using Content.Server.RoundEnd;
|
using Content.Server.RoundEnd;
|
||||||
using Content.Server.Shuttles.Events;
|
using Content.Server.Shuttles.Events;
|
||||||
using Content.Server.Shuttles.Systems;
|
using Content.Server.Shuttles.Systems;
|
||||||
using Content.Server.Spawners.Components;
|
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
using Content.Server.Station.Systems;
|
|
||||||
using Content.Server.Store.Components;
|
using Content.Server.Store.Components;
|
||||||
using Content.Server.Store.Systems;
|
using Content.Server.Store.Systems;
|
||||||
using Content.Shared.CCVar;
|
|
||||||
using Content.Shared.Dataset;
|
|
||||||
using Content.Shared.Humanoid;
|
using Content.Shared.Humanoid;
|
||||||
using Content.Shared.Humanoid.Prototypes;
|
using Content.Shared.Humanoid.Prototypes;
|
||||||
using Content.Shared.Mind.Components;
|
|
||||||
using Content.Shared.Mobs;
|
using Content.Shared.Mobs;
|
||||||
using Content.Shared.Mobs.Components;
|
using Content.Shared.Mobs.Components;
|
||||||
using Content.Shared.NPC.Components;
|
using Content.Shared.NPC.Components;
|
||||||
@@ -33,45 +22,30 @@ using Content.Shared.NPC.Systems;
|
|||||||
using Content.Shared.Nuke;
|
using Content.Shared.Nuke;
|
||||||
using Content.Shared.NukeOps;
|
using Content.Shared.NukeOps;
|
||||||
using Content.Shared.Preferences;
|
using Content.Shared.Preferences;
|
||||||
using Content.Shared.Roles;
|
|
||||||
using Content.Shared.Store;
|
using Content.Shared.Store;
|
||||||
using Content.Shared.Tag;
|
using Content.Shared.Tag;
|
||||||
using Content.Shared.Zombies;
|
using Content.Shared.Zombies;
|
||||||
using Robust.Server.Player;
|
|
||||||
using Robust.Shared.Configuration;
|
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
|
|
||||||
namespace Content.Server.GameTicking.Rules;
|
namespace Content.Server.GameTicking.Rules;
|
||||||
|
|
||||||
public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
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 IPrototypeManager _prototypeManager = default!;
|
||||||
[Dependency] private readonly IServerPreferencesManager _prefs = 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 EmergencyShuttleSystem _emergency = default!;
|
||||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = 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 NpcFactionSystem _npcFaction = default!;
|
||||||
|
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||||
[Dependency] private readonly RoundEndSystem _roundEndSystem = 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 StoreSystem _store = default!;
|
||||||
[Dependency] private readonly TagSystem _tag = default!;
|
[Dependency] private readonly TagSystem _tag = default!;
|
||||||
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
|
|
||||||
|
|
||||||
private ISawmill _sawmill = default!;
|
|
||||||
|
|
||||||
[ValidatePrototypeId<CurrencyPrototype>]
|
[ValidatePrototypeId<CurrencyPrototype>]
|
||||||
private const string TelecrystalCurrencyPrototype = "Telecrystal";
|
private const string TelecrystalCurrencyPrototype = "Telecrystal";
|
||||||
@@ -79,141 +53,67 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
|||||||
[ValidatePrototypeId<TagPrototype>]
|
[ValidatePrototypeId<TagPrototype>]
|
||||||
private const string NukeOpsUplinkTagPrototype = "NukeOpsUplink";
|
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()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
_sawmill = _logManager.GetSawmill("NukeOps");
|
|
||||||
|
|
||||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
|
||||||
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayersSpawning);
|
|
||||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
|
||||||
SubscribeLocalEvent<NukeExplodedEvent>(OnNukeExploded);
|
SubscribeLocalEvent<NukeExplodedEvent>(OnNukeExploded);
|
||||||
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRunLevelChanged);
|
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRunLevelChanged);
|
||||||
SubscribeLocalEvent<NukeDisarmSuccessEvent>(OnNukeDisarm);
|
SubscribeLocalEvent<NukeDisarmSuccessEvent>(OnNukeDisarm);
|
||||||
|
|
||||||
SubscribeLocalEvent<NukeOperativeComponent, ComponentRemove>(OnComponentRemove);
|
SubscribeLocalEvent<NukeOperativeComponent, ComponentRemove>(OnComponentRemove);
|
||||||
SubscribeLocalEvent<NukeOperativeComponent, MobStateChangedEvent>(OnMobStateChanged);
|
SubscribeLocalEvent<NukeOperativeComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||||
SubscribeLocalEvent<NukeOperativeComponent, GhostRoleSpawnerUsedEvent>(OnPlayersGhostSpawning);
|
|
||||||
SubscribeLocalEvent<NukeOperativeComponent, MindAddedMessage>(OnMindAdded);
|
|
||||||
SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
|
SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<NukeOpsShuttleComponent, MapInitEvent>(OnMapInit);
|
||||||
|
|
||||||
SubscribeLocalEvent<ConsoleFTLAttemptEvent>(OnShuttleFTLAttempt);
|
SubscribeLocalEvent<ConsoleFTLAttemptEvent>(OnShuttleFTLAttempt);
|
||||||
SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
|
SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
|
||||||
SubscribeLocalEvent<CommunicationConsoleCallShuttleAttemptEvent>(OnShuttleCallAttempt);
|
SubscribeLocalEvent<CommunicationConsoleCallShuttleAttemptEvent>(OnShuttleCallAttempt);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<NukeopsRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity);
|
||||||
|
SubscribeLocalEvent<NukeopsRuleComponent, AfterAntagEntitySelectedEvent>(OnAfterAntagEntSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
|
protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
|
||||||
GameRuleStartedEvent args)
|
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)
|
eligible.Add((eligibleUid, eligibleComp, member));
|
||||||
SpawnOperativesForGhostRoles(uid, component);
|
}
|
||||||
|
|
||||||
|
if (eligible.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
component.TargetStation = RobustRandom.Pick(eligible);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Event Handlers
|
#region Event Handlers
|
||||||
|
protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
|
||||||
private void OnStartAttempt(RoundStartAttemptEvent ev)
|
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)
|
foreach (var cond in component.WinConditions)
|
||||||
{
|
|
||||||
var query = QueryActiveRules();
|
|
||||||
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
|
|
||||||
{
|
{
|
||||||
if (!SpawnMap((uid, nukeops)))
|
var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
|
||||||
{
|
args.AddLine(text);
|
||||||
_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)
|
|
||||||
{
|
|
||||||
var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
|
|
||||||
ev.AddLine(text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.AddLine(Loc.GetString("nukeops-list-start"));
|
args.AddLine(Loc.GetString("nukeops-list-start"));
|
||||||
|
|
||||||
var nukiesQuery = EntityQueryEnumerator<NukeopsRoleComponent, MindContainerComponent>();
|
var antags =_antag.GetAntagIdentifiers(uid);
|
||||||
while (nukiesQuery.MoveNext(out var nukeopsUid, out _, out var mindContainer))
|
|
||||||
|
foreach (var (_, sessionData, name) in antags)
|
||||||
{
|
{
|
||||||
if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer))
|
args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName)));
|
||||||
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))));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,10 +124,10 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
|||||||
{
|
{
|
||||||
if (ev.OwningStation != null)
|
if (ev.OwningStation != null)
|
||||||
{
|
{
|
||||||
if (ev.OwningStation == nukeops.NukieOutpost)
|
if (ev.OwningStation == GetOutpost(uid))
|
||||||
{
|
{
|
||||||
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost);
|
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost);
|
||||||
SetWinType(uid, WinType.CrewMajor, nukeops);
|
SetWinType((uid, nukeops), WinType.CrewMajor);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +142,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
|||||||
}
|
}
|
||||||
|
|
||||||
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation);
|
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation);
|
||||||
SetWinType(uid, WinType.OpsMajor, nukeops);
|
SetWinType((uid, nukeops), WinType.OpsMajor);
|
||||||
correctStation = true;
|
correctStation = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,19 +163,85 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
|||||||
|
|
||||||
private void OnRunLevelChanged(GameRunLevelChangedEvent ev)
|
private void OnRunLevelChanged(GameRunLevelChangedEvent ev)
|
||||||
{
|
{
|
||||||
|
if (ev.New is not GameRunLevel.PostRound)
|
||||||
|
return;
|
||||||
|
|
||||||
var query = QueryActiveRules();
|
var query = QueryActiveRules();
|
||||||
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
|
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
|
||||||
{
|
{
|
||||||
switch (ev.New)
|
OnRoundEnd((uid, nukeops));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRoundEnd(Entity<NukeopsRuleComponent> ent)
|
||||||
|
{
|
||||||
|
// 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))
|
||||||
{
|
{
|
||||||
case GameRunLevel.InRound:
|
ent.Comp.WinConditions.Add(WinCondition.NukeActiveAtCentCom);
|
||||||
OnRoundStart(uid, nukeops);
|
SetWinType((ent, ent), WinType.OpsMajor);
|
||||||
break;
|
return;
|
||||||
case GameRunLevel.PostRound:
|
}
|
||||||
OnRoundEnd(uid, nukeops);
|
|
||||||
break;
|
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)
|
private void OnNukeDisarm(NukeDisarmSuccessEvent ev)
|
||||||
@@ -294,66 +260,31 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
|||||||
CheckRoundShouldEnd();
|
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)
|
private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args)
|
||||||
{
|
{
|
||||||
RemCompDeferred(uid, component);
|
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)
|
private void OnShuttleFTLAttempt(ref ConsoleFTLAttemptEvent ev)
|
||||||
{
|
{
|
||||||
var query = QueryActiveRules();
|
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;
|
continue;
|
||||||
|
|
||||||
if (nukeops.WarDeclaredTime != null)
|
if (nukeops.WarDeclaredTime != null)
|
||||||
@@ -397,12 +328,12 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
|||||||
{
|
{
|
||||||
// TODO: this is VERY awful for multi-nukies
|
// TODO: this is VERY awful for multi-nukies
|
||||||
var query = QueryActiveRules();
|
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)
|
if (nukeops.WarDeclaredTime != null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (Transform(ev.DeclaratorEntity).MapID != nukeops.NukiePlanet)
|
if (TryComp<LoadMapRuleComponent>(uid, out var mapComp) && Transform(ev.DeclaratorEntity).MapID != mapComp.Map)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var newStatus = GetWarCondition(nukeops, ev.Status);
|
var newStatus = GetWarCondition(nukeops, ev.Status);
|
||||||
@@ -448,161 +379,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
|||||||
if (!_tag.HasTag(uid, NukeOpsUplinkTagPrototype))
|
if (!_tag.HasTag(uid, NukeOpsUplinkTagPrototype))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!nukieRule.NukieOutpost.HasValue)
|
if (GetOutpost(uid) is not {} outpost)
|
||||||
continue;
|
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;
|
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));
|
var msg = Loc.GetString("store-currency-war-boost-given", ("target", uid));
|
||||||
_popupSystem.PopupEntity(msg, 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))
|
ent.Comp.WinType = type;
|
||||||
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;
|
|
||||||
|
|
||||||
if (endRound && (type == WinType.CrewMajor || type == WinType.OpsMajor))
|
if (endRound && (type == WinType.CrewMajor || type == WinType.OpsMajor))
|
||||||
_roundEndSystem.EndRound();
|
_roundEndSystem.EndRound();
|
||||||
@@ -613,243 +405,130 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
|||||||
var query = QueryActiveRules();
|
var query = QueryActiveRules();
|
||||||
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
|
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
|
||||||
{
|
{
|
||||||
if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor)
|
CheckRoundShouldEnd((uid, nukeops));
|
||||||
continue;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If there are any nuclear bombs that are active, immediately return. We're not over yet.
|
private void CheckRoundShouldEnd(Entity<NukeopsRuleComponent> ent)
|
||||||
var armed = false;
|
{
|
||||||
foreach (var nuke in EntityQuery<NukeComponent>())
|
var nukeops = ent.Comp;
|
||||||
{
|
|
||||||
if (nuke.Status == NukeStatus.ARMED)
|
|
||||||
{
|
|
||||||
armed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (armed)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
MapId? shuttleMapId = Exists(nukeops.NukieShuttle)
|
if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor)
|
||||||
? Transform(nukeops.NukieShuttle.Value).MapID
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
// If there are any nuclear bombs that are active, immediately return. We're not over yet.
|
||||||
|
foreach (var nuke in EntityQuery<NukeComponent>())
|
||||||
|
{
|
||||||
|
if (nuke.Status == NukeStatus.ARMED)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shuttle = GetShuttle((ent, ent));
|
||||||
|
|
||||||
|
MapId? shuttleMapId = Exists(shuttle)
|
||||||
|
? Transform(shuttle.Value).MapID
|
||||||
|
: null;
|
||||||
|
|
||||||
|
MapId? targetStationMap = null;
|
||||||
|
if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data))
|
||||||
|
{
|
||||||
|
var grid = data.Grids.FirstOrNull();
|
||||||
|
targetStationMap = grid != null
|
||||||
|
? Transform(grid.Value).MapID
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
MapId? targetStationMap = null;
|
|
||||||
if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data))
|
|
||||||
{
|
|
||||||
var grid = data.Grids.FirstOrNull();
|
|
||||||
targetStationMap = grid != null
|
|
||||||
? Transform(grid.Value).MapID
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there are nuke operatives still alive on the same map as the shuttle,
|
|
||||||
// or on the same map as the station.
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
if (operativesAlive)
|
|
||||||
continue; // 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.
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
nukeops.WinConditions.Add(spawnsAvailable
|
|
||||||
? WinCondition.NukiesAbandoned
|
|
||||||
: WinCondition.AllNukiesDead);
|
|
||||||
|
|
||||||
SetWinType(uid, WinType.CrewMajor, nukeops, 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)
|
|
||||||
{
|
|
||||||
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;
|
// Check if there are nuke operatives still alive on the same map as the shuttle,
|
||||||
|
// or on the same map as the station.
|
||||||
|
// If there are, the round can continue.
|
||||||
|
var operatives = EntityQuery<NukeOperativeComponent, MobStateComponent, TransformComponent>(true);
|
||||||
|
var operativesAlive = operatives
|
||||||
|
.Where(op =>
|
||||||
|
op.Item3.MapID == shuttleMapId
|
||||||
|
|| op.Item3.MapID == targetStationMap)
|
||||||
|
.Any(op => op.Item2.CurrentState == MobState.Alive && op.Item1.Running);
|
||||||
|
|
||||||
|
if (operativesAlive)
|
||||||
|
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 && 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.
|
||||||
|
nukeops.WinConditions.Add(spawnsAvailable
|
||||||
|
? WinCondition.NukiesAbandoned
|
||||||
|
: WinCondition.AllNukiesDead);
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// this should really go anywhere else but im tired.
|
||||||
/// Adds missing nuke operative components, equips starting gear and renames the entity.
|
private void OnAntagSelectEntity(Entity<NukeopsRuleComponent> ent, ref AntagSelectEntityEvent args)
|
||||||
/// </summary>
|
|
||||||
private void SetupOperativeEntity(EntityUid mob, string name, NukeopSpawnPreset spawnDetails, HumanoidCharacterProfile? profile)
|
|
||||||
{
|
{
|
||||||
_metaData.SetEntityName(mob, name);
|
if (args.Handled)
|
||||||
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)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var spawns = new List<EntityCoordinates>();
|
var profile = args.Session != null
|
||||||
foreach (var (_, meta, xform) in EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
|
? _prefs.GetPreferences(args.Session.UserId).SelectedCharacter as HumanoidCharacterProfile
|
||||||
|
: HumanoidCharacterProfile.RandomWithSpecies();
|
||||||
|
if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species))
|
||||||
{
|
{
|
||||||
if (meta.EntityPrototype?.ID != component.SpawnPointProto.Id)
|
species = _prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
|
||||||
continue;
|
|
||||||
|
|
||||||
if (xform.ParentUid != component.NukieOutpost)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
spawns.Add(xform.Coordinates);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Fallback, spawn at the centre of the map
|
args.Entity = Spawn(species.Prototype);
|
||||||
if (spawns.Count == 0)
|
_humanoid.LoadProfile(args.Entity.Value, profile);
|
||||||
{
|
|
||||||
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;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
//Otherwise, spawn as a ghost role
|
|
||||||
else if (spawnGhostRoles)
|
|
||||||
{
|
|
||||||
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>
|
private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||||
/// 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;
|
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>
|
/// <remarks>
|
||||||
/// Spawn nukie ghost roles if this gamerule was started mid round
|
/// Is this method the shitty glue holding together the last of my sanity? yes.
|
||||||
/// </summary>
|
/// Do i have a better solution? not presently.
|
||||||
private void SpawnOperativesForGhostRoles(EntityUid uid, NukeopsRuleComponent? component = null)
|
/// </remarks>
|
||||||
|
private EntityUid? GetOutpost(Entity<LoadMapRuleComponent?> ent)
|
||||||
{
|
{
|
||||||
if (!Resolve(uid, ref component))
|
if (!Resolve(ent, ref ent.Comp, false))
|
||||||
return;
|
return null;
|
||||||
|
|
||||||
if (!SpawnMap((uid, component)))
|
return ent.Comp.MapGrids.FirstOrNull();
|
||||||
{
|
|
||||||
_sawmill.Info("Failed to load map for nukeops");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
for (var i = 2; i < numNukies; i++)
|
|
||||||
{
|
|
||||||
operatives.Add(new NukieSpawn(null, component.OperativeSpawnDetails));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SpawnOperatives(operatives, true, component);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//For admins forcing someone to nukeOps.
|
/// <remarks>
|
||||||
public void MakeLoneNukie(EntityUid entity)
|
/// 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)
|
||||||
{
|
{
|
||||||
if (!_mind.TryGetMind(entity, out var mindId, out var mindComponent))
|
if (!Resolve(ent, ref ent.Comp, false))
|
||||||
return;
|
return null;
|
||||||
|
|
||||||
//ok hardcoded value bad but so is everything else here
|
var query = EntityQueryEnumerator<NukeOpsShuttleComponent>();
|
||||||
_roles.MindAddRole(mindId, new NukeopsRoleComponent { PrototypeId = NukeopsId }, mindComponent);
|
while (query.MoveNext(out var uid, out var comp))
|
||||||
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;
|
if (comp.AssociatedRule == ent.Owner)
|
||||||
Type = type;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.Chat.Managers;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Station.Systems;
|
using Content.Server.Station.Systems;
|
||||||
using Content.Shared.Chat;
|
using Content.Shared.Chat;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ using Content.Server.Station.Systems;
|
|||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.Humanoid;
|
using Content.Shared.Humanoid;
|
||||||
using Content.Shared.IdentityManagement;
|
using Content.Shared.IdentityManagement;
|
||||||
using Content.Shared.Inventory;
|
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
using Content.Shared.Mind.Components;
|
using Content.Shared.Mind.Components;
|
||||||
using Content.Shared.Mindshield.Components;
|
using Content.Shared.Mindshield.Components;
|
||||||
@@ -24,12 +23,11 @@ using Content.Shared.Mobs.Systems;
|
|||||||
using Content.Shared.NPC.Prototypes;
|
using Content.Shared.NPC.Prototypes;
|
||||||
using Content.Shared.NPC.Systems;
|
using Content.Shared.NPC.Systems;
|
||||||
using Content.Shared.Revolutionary.Components;
|
using Content.Shared.Revolutionary.Components;
|
||||||
using Content.Shared.Roles;
|
|
||||||
using Content.Shared.Stunnable;
|
using Content.Shared.Stunnable;
|
||||||
using Content.Shared.Zombies;
|
using Content.Shared.Zombies;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
using System.Linq;
|
using Content.Server.GameTicking.Components;
|
||||||
|
|
||||||
namespace Content.Server.GameTicking.Rules;
|
namespace Content.Server.GameTicking.Rules;
|
||||||
|
|
||||||
@@ -40,7 +38,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
|||||||
{
|
{
|
||||||
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
|
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
|
||||||
[Dependency] private readonly IGameTiming _timing = 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 EuiManager _euiMan = default!;
|
||||||
[Dependency] private readonly MindSystem _mind = default!;
|
[Dependency] private readonly MindSystem _mind = default!;
|
||||||
[Dependency] private readonly MobStateSystem _mobState = 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 RoundEndSystem _roundEnd = default!;
|
||||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
|
||||||
|
|
||||||
//Used in OnPostFlash, no reference to the rule component is available
|
//Used in OnPostFlash, no reference to the rule component is available
|
||||||
public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
|
public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
|
||||||
@@ -60,23 +57,12 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
|||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
|
||||||
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayerJobAssigned);
|
|
||||||
SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
|
SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
|
||||||
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
|
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
|
||||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
|
||||||
SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
||||||
SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(OnPostFlash);
|
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)
|
protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||||
{
|
{
|
||||||
base.Started(uid, component, gameRule, args);
|
base.Started(uid, component, gameRule, args);
|
||||||
@@ -98,40 +84,29 @@ 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 revsLost = CheckRevsLose();
|
||||||
var commandLost = CheckCommandLose();
|
var commandLost = CheckCommandLose();
|
||||||
var query = AllEntityQuery<RevolutionaryRuleComponent>();
|
// This is (revsLost, commandsLost) concatted together
|
||||||
while (query.MoveNext(out var headrev))
|
// (moony wrote this comment idk what it means)
|
||||||
|
var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
|
||||||
|
args.AddLine(Loc.GetString(Outcomes[index]));
|
||||||
|
|
||||||
|
var sessionData = _antag.GetAntagIdentifiers(uid);
|
||||||
|
args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count)));
|
||||||
|
foreach (var (mind, data, name) in sessionData)
|
||||||
{
|
{
|
||||||
// This is (revsLost, commandsLost) concatted together
|
var count = CompOrNull<RevolutionaryRoleComponent>(mind)?.ConvertedCount ?? 0;
|
||||||
// (moony wrote this comment idk what it means)
|
args.AddLine(Loc.GetString("rev-headrev-name-user",
|
||||||
var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
|
("name", name),
|
||||||
ev.AddLine(Loc.GetString(Outcomes[index]));
|
("username", data.UserName),
|
||||||
|
("count", count)));
|
||||||
|
|
||||||
ev.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", headrev.HeadRevs.Count)));
|
// TODO: someone suggested listing all alive? revs maybe implement at some point
|
||||||
foreach (var player in headrev.HeadRevs)
|
|
||||||
{
|
|
||||||
// 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)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: someone suggested listing all alive? revs maybe implement at some point
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,57 +119,6 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
|||||||
args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing"));
|
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>
|
/// <summary>
|
||||||
/// Called when a Head Rev uses a flash in melee to convert somebody else.
|
/// Called when a Head Rev uses a flash in melee to convert somebody else.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -232,22 +156,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mind?.Session != null)
|
if (mind?.Session != null)
|
||||||
_antagSelection.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound);
|
_antag.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Enemies of the revolution
|
//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);
|
_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.");
|
_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;
|
continue;
|
||||||
|
|
||||||
// remove their antag role
|
// remove their antag role
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Shuttles.Systems;
|
using Content.Server.Shuttles.Systems;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Sandbox;
|
using Content.Server.Sandbox;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Server.Administration.Logs;
|
using Content.Server.Administration.Logs;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
using Content.Server.GameTicking.Presets;
|
using Content.Server.GameTicking.Presets;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Shared.Storage;
|
using Content.Shared.Storage;
|
||||||
|
|
||||||
|
|||||||
@@ -3,118 +3,37 @@ using Content.Server.GameTicking.Rules.Components;
|
|||||||
using Content.Server.Mind;
|
using Content.Server.Mind;
|
||||||
using Content.Server.Objectives;
|
using Content.Server.Objectives;
|
||||||
using Content.Server.Roles;
|
using Content.Server.Roles;
|
||||||
using Content.Shared.Antag;
|
|
||||||
using Content.Shared.CombatMode.Pacification;
|
|
||||||
using Content.Shared.Humanoid;
|
using Content.Shared.Humanoid;
|
||||||
using Content.Shared.Inventory;
|
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
using Content.Shared.Objectives.Components;
|
using Content.Shared.Objectives.Components;
|
||||||
using Content.Shared.Roles;
|
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Content.Server.GameTicking.Rules;
|
namespace Content.Server.GameTicking.Rules;
|
||||||
|
|
||||||
public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
|
public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
|
|
||||||
[Dependency] private readonly MindSystem _mindSystem = 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 ObjectivesSystem _objectives = default!;
|
||||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
|
SubscribeLocalEvent<ThiefRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagSelected);
|
||||||
|
|
||||||
SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
||||||
SubscribeLocalEvent<ThiefRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
|
SubscribeLocalEvent<ThiefRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
|
private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||||
{
|
{
|
||||||
var query = QueryActiveRules();
|
if (!_mindSystem.TryGetMind(args.EntityUid, out var mindId, out var mind))
|
||||||
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))
|
|
||||||
return;
|
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
|
//Generate objectives
|
||||||
GenerateObjectives(mindId, mind, thiefRule);
|
GenerateObjectives(mindId, mind, ent);
|
||||||
|
|
||||||
//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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule)
|
private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule)
|
||||||
@@ -160,8 +79,7 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
|
|||||||
private string MakeBriefing(EntityUid thief)
|
private string MakeBriefing(EntityUid thief)
|
||||||
{
|
{
|
||||||
var isHuman = HasComp<HumanoidAppearanceComponent>(thief);
|
var isHuman = HasComp<HumanoidAppearanceComponent>(thief);
|
||||||
var briefing = "\n";
|
var briefing = isHuman
|
||||||
briefing = isHuman
|
|
||||||
? Loc.GetString("thief-role-greeting-human")
|
? Loc.GetString("thief-role-greeting-human")
|
||||||
: Loc.GetString("thief-role-greeting-animal");
|
: Loc.GetString("thief-role-greeting-animal");
|
||||||
|
|
||||||
@@ -169,9 +87,9 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
|
|||||||
return briefing;
|
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");
|
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.PDA.Ringer;
|
||||||
using Content.Server.Roles;
|
using Content.Server.Roles;
|
||||||
using Content.Server.Traitor.Uplink;
|
using Content.Server.Traitor.Uplink;
|
||||||
using Content.Shared.CCVar;
|
|
||||||
using Content.Shared.Dataset;
|
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
using Content.Shared.Mobs.Systems;
|
|
||||||
using Content.Shared.NPC.Systems;
|
using Content.Shared.NPC.Systems;
|
||||||
using Content.Shared.Objectives.Components;
|
using Content.Shared.Objectives.Components;
|
||||||
using Content.Shared.PDA;
|
using Content.Shared.PDA;
|
||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
using Content.Shared.Roles.Jobs;
|
using Content.Shared.Roles.Jobs;
|
||||||
using Robust.Server.Player;
|
|
||||||
using Robust.Shared.Configuration;
|
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Timing;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
|
|
||||||
namespace Content.Server.GameTicking.Rules;
|
namespace Content.Server.GameTicking.Rules;
|
||||||
|
|
||||||
public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||||
{
|
{
|
||||||
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
|
|
||||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
[Dependency] private readonly IRobustRandom _random = 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 NpcFactionSystem _npcFaction = default!;
|
||||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||||
[Dependency] private readonly UplinkSystem _uplink = default!;
|
[Dependency] private readonly UplinkSystem _uplink = default!;
|
||||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||||
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
||||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||||
[Dependency] private readonly ObjectivesSystem _objectives = default!;
|
[Dependency] private readonly ObjectivesSystem _objectives = default!;
|
||||||
[Dependency] private readonly IGameTiming _timing = default!;
|
|
||||||
|
|
||||||
private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
|
public const int MaxPicks = 20;
|
||||||
private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors);
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
|
||||||
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
|
|
||||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(HandleLatejoin);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
|
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
|
||||||
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
|
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Set min players on game rule
|
|
||||||
protected override void Added(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
protected override void Added(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
||||||
{
|
{
|
||||||
base.Added(uid, component, gameRule, 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);
|
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);
|
MakeTraitor(args.EntityUid, ent);
|
||||||
|
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MakeCodewords(TraitorRuleComponent component)
|
private void MakeCodewords(TraitorRuleComponent component)
|
||||||
{
|
{
|
||||||
var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
|
var adjectives = _prototypeManager.Index(component.CodewordAdjectives).Values;
|
||||||
var adjectives = _prototypeManager.Index<DatasetPrototype>(component.CodewordAdjectives).Values;
|
var verbs = _prototypeManager.Index(component.CodewordVerbs).Values;
|
||||||
var verbs = _prototypeManager.Index<DatasetPrototype>(component.CodewordVerbs).Values;
|
|
||||||
var codewordPool = adjectives.Concat(verbs).ToList();
|
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];
|
component.Codewords = new string[finalCodewordCount];
|
||||||
for (var i = 0; i < finalCodewordCount; i++)
|
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)
|
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
|
||||||
{
|
{
|
||||||
//Grab the mind if it wasnt provided
|
//Grab the mind if it wasnt provided
|
||||||
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
|
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
|
||||||
return false;
|
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)));
|
var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
|
||||||
|
|
||||||
Note[]? code = null;
|
Note[]? code = null;
|
||||||
if (giveUplink)
|
if (giveUplink)
|
||||||
{
|
{
|
||||||
// Calculate the amount of currency on the uplink.
|
// 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))
|
if (_jobs.MindTryGetJob(mindId, out _, out var prototype))
|
||||||
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
|
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", "#"))));
|
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);
|
component.TraitorMinds.Add(mindId);
|
||||||
|
|
||||||
// Assign traitor roles
|
|
||||||
_roleSystem.MindAddRole(mindId, new TraitorRoleComponent
|
|
||||||
{
|
|
||||||
PrototypeId = component.TraitorPrototypeId
|
|
||||||
}, mind, true);
|
|
||||||
// Assign briefing
|
// Assign briefing
|
||||||
_roleSystem.MindAddRole(mindId, new RoleBriefingComponent
|
_roleSystem.MindAddRole(mindId, new RoleBriefingComponent
|
||||||
{
|
{
|
||||||
Briefing = briefing.ToString()
|
Briefing = briefing
|
||||||
}, mind, true);
|
}, mind, true);
|
||||||
|
|
||||||
// Change the faction
|
// Change the faction
|
||||||
@@ -202,11 +114,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
|||||||
// Give traitors their objectives
|
// Give traitors their objectives
|
||||||
if (giveObjectives)
|
if (giveObjectives)
|
||||||
{
|
{
|
||||||
var maxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty);
|
|
||||||
var maxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks);
|
|
||||||
var difficulty = 0f;
|
var difficulty = 0f;
|
||||||
Log.Debug($"Attempting {maxPicks} objective picks with {maxDifficulty} difficulty");
|
for (var pick = 0; pick < MaxPicks && component.MaxDifficulty > difficulty; pick++)
|
||||||
for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++)
|
|
||||||
{
|
{
|
||||||
var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup);
|
var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup);
|
||||||
if (objective == null)
|
if (objective == null)
|
||||||
@@ -222,53 +131,9 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
|||||||
return true;
|
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)
|
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");
|
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)));
|
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)
|
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
@@ -312,9 +156,11 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
|||||||
public List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind)
|
public List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind)
|
||||||
{
|
{
|
||||||
List<(EntityUid Id, MindComponent Mind)> allTraitors = new();
|
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))
|
if (!allTraitors.Contains(role))
|
||||||
allTraitors.Add(role);
|
allTraitors.Add(role);
|
||||||
@@ -324,20 +170,15 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
|||||||
return allTraitors;
|
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)>();
|
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) &&
|
if (mind.Comp == ourMind)
|
||||||
mind.OwnedEntity != null &&
|
continue;
|
||||||
mind.Session != null &&
|
|
||||||
mind != ourMind &&
|
traitors.Add((mind, mind));
|
||||||
_mobStateSystem.IsAlive(mind.OwnedEntity.Value) &&
|
|
||||||
mind.CurrentEntity == mind.OwnedEntity)
|
|
||||||
{
|
|
||||||
traitors.Add((traitor, mind));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return traitors;
|
return traitors;
|
||||||
|
|||||||
@@ -1,112 +1,90 @@
|
|||||||
using Content.Server.Actions;
|
|
||||||
using Content.Server.Antag;
|
using Content.Server.Antag;
|
||||||
using Content.Server.Chat.Systems;
|
using Content.Server.Chat.Systems;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Popups;
|
using Content.Server.Popups;
|
||||||
using Content.Server.Roles;
|
|
||||||
using Content.Server.RoundEnd;
|
using Content.Server.RoundEnd;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
using Content.Server.Station.Systems;
|
using Content.Server.Station.Systems;
|
||||||
using Content.Server.Zombies;
|
using Content.Server.Zombies;
|
||||||
using Content.Shared.CCVar;
|
|
||||||
using Content.Shared.Humanoid;
|
using Content.Shared.Humanoid;
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
using Content.Shared.Mobs;
|
using Content.Shared.Mobs;
|
||||||
using Content.Shared.Mobs.Components;
|
using Content.Shared.Mobs.Components;
|
||||||
using Content.Shared.Mobs.Systems;
|
using Content.Shared.Mobs.Systems;
|
||||||
using Content.Shared.Roles;
|
|
||||||
using Content.Shared.Zombies;
|
using Content.Shared.Zombies;
|
||||||
using Robust.Server.Player;
|
|
||||||
using Robust.Shared.Configuration;
|
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Random;
|
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
|
|
||||||
namespace Content.Server.GameTicking.Rules;
|
namespace Content.Server.GameTicking.Rules;
|
||||||
|
|
||||||
public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
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 ChatSystem _chat = default!;
|
||||||
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
|
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
|
||||||
[Dependency] private readonly PopupSystem _popup = default!;
|
[Dependency] private readonly PopupSystem _popup = default!;
|
||||||
[Dependency] private readonly ActionsSystem _action = default!;
|
|
||||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||||
[Dependency] private readonly ZombieSystem _zombie = default!;
|
[Dependency] private readonly ZombieSystem _zombie = default!;
|
||||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
|
||||||
[Dependency] private readonly StationSystem _station = 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!;
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
|
||||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
|
||||||
SubscribeLocalEvent<PendingZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
|
SubscribeLocalEvent<PendingZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule,
|
||||||
/// Set the required minimum players for this gamemode to start
|
ref RoundEndTextAppendEvent args)
|
||||||
/// </summary>
|
|
||||||
protected override void Added(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
|
||||||
{
|
{
|
||||||
base.Added(uid, component, gameRule, args);
|
base.AppendRoundEndText(uid, component, gameRule, ref args);
|
||||||
|
|
||||||
gameRule.MinPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
|
// This is just the general condition thing used for determining the win/lose text
|
||||||
}
|
var fraction = GetInfectedFraction(true, true);
|
||||||
|
|
||||||
private void OnRoundEndText(RoundEndTextAppendEvent ev)
|
if (fraction <= 0)
|
||||||
{
|
args.AddLine(Loc.GetString("zombie-round-end-amount-none"));
|
||||||
foreach (var zombie in EntityQuery<ZombieRuleComponent>())
|
else if (fraction <= 0.25)
|
||||||
|
args.AddLine(Loc.GetString("zombie-round-end-amount-low"));
|
||||||
|
else if (fraction <= 0.5)
|
||||||
|
args.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
|
||||||
|
else if (fraction < 1)
|
||||||
|
args.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
|
||||||
|
else
|
||||||
|
args.AddLine(Loc.GetString("zombie-round-end-amount-all"));
|
||||||
|
|
||||||
|
var antags = _antag.GetAntagIdentifiers(uid);
|
||||||
|
args.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", antags.Count)));
|
||||||
|
foreach (var (_, data, entName) in antags)
|
||||||
{
|
{
|
||||||
// This is just the general condition thing used for determining the win/lose text
|
args.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
|
||||||
var fraction = GetInfectedFraction(true, true);
|
("name", entName),
|
||||||
|
("username", data.UserName)));
|
||||||
|
}
|
||||||
|
|
||||||
if (fraction <= 0)
|
var healthy = GetHealthyHumans();
|
||||||
ev.AddLine(Loc.GetString("zombie-round-end-amount-none"));
|
// Gets a bunch of the living players and displays them if they're under a threshold.
|
||||||
else if (fraction <= 0.25)
|
// InitialInfected is used for the threshold because it scales with the player count well.
|
||||||
ev.AddLine(Loc.GetString("zombie-round-end-amount-low"));
|
if (healthy.Count <= 0 || healthy.Count > 2 * antags.Count)
|
||||||
else if (fraction <= 0.5)
|
return;
|
||||||
ev.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
|
args.AddLine("");
|
||||||
else if (fraction < 1)
|
args.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count)));
|
||||||
ev.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
|
foreach (var survivor in healthy)
|
||||||
else
|
{
|
||||||
ev.AddLine(Loc.GetString("zombie-round-end-amount-all"));
|
var meta = MetaData(survivor);
|
||||||
|
var username = string.Empty;
|
||||||
ev.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", zombie.InitialInfectedNames.Count)));
|
if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null)
|
||||||
foreach (var player in zombie.InitialInfectedNames)
|
|
||||||
{
|
{
|
||||||
ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
|
username = mind.Session.Name;
|
||||||
("name", player.Key),
|
|
||||||
("username", player.Value)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var healthy = GetHealthyHumans();
|
args.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
|
||||||
// Gets a bunch of the living players and displays them if they're under a threshold.
|
("name", meta.EntityName),
|
||||||
// InitialInfected is used for the threshold because it scales with the player count well.
|
("username", username)));
|
||||||
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)));
|
|
||||||
foreach (var survivor in healthy)
|
|
||||||
{
|
|
||||||
var meta = MetaData(survivor);
|
|
||||||
var username = string.Empty;
|
|
||||||
if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null)
|
|
||||||
{
|
|
||||||
username = mind.Session.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
|
|
||||||
("name", meta.EntityName),
|
|
||||||
("username", username)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,38 +112,20 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
|||||||
_roundEnd.EndRound();
|
_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)
|
protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||||
{
|
{
|
||||||
base.Started(uid, component, gameRule, args);
|
base.Started(uid, component, gameRule, args);
|
||||||
|
|
||||||
var delay = _random.Next(component.MinStartDelay, component.MaxStartDelay);
|
component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
|
||||||
component.StartTime = _timing.CurTime + delay;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime)
|
protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime)
|
||||||
{
|
{
|
||||||
base.ActiveTick(uid, component, gameRule, frameTime);
|
base.ActiveTick(uid, component, gameRule, frameTime);
|
||||||
|
if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime)
|
||||||
if (component.StartTime.HasValue && component.StartTime < _timing.CurTime)
|
return;
|
||||||
{
|
CheckRoundEnd(component);
|
||||||
InfectInitialPlayers(component);
|
component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
|
||||||
component.StartTime = null;
|
|
||||||
component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.NextRoundEndCheck.HasValue && component.NextRoundEndCheck < _timing.CurTime)
|
|
||||||
{
|
|
||||||
CheckRoundEnd(component);
|
|
||||||
component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnZombifySelf(EntityUid uid, PendingZombieComponent component, ZombifySelfActionEvent args)
|
private void OnZombifySelf(EntityUid uid, PendingZombieComponent component, ZombifySelfActionEvent args)
|
||||||
@@ -232,81 +192,4 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
|||||||
}
|
}
|
||||||
return healthy;
|
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;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
|
||||||
using Content.Server.Mind;
|
|
||||||
using Content.Server.Shuttles.Systems;
|
using Content.Server.Shuttles.Systems;
|
||||||
using Content.Shared.Cuffs.Components;
|
using Content.Shared.Cuffs.Components;
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
using Content.Shared.Mobs.Systems;
|
|
||||||
using Content.Shared.Objectives.Components;
|
using Content.Shared.Objectives.Components;
|
||||||
using Content.Shared.Objectives.Systems;
|
using Content.Shared.Objectives.Systems;
|
||||||
using Content.Shared.Random;
|
using Content.Shared.Random;
|
||||||
@@ -12,7 +9,9 @@ using Content.Shared.Random.Helpers;
|
|||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
|
||||||
namespace Content.Server.Objectives;
|
namespace Content.Server.Objectives;
|
||||||
|
|
||||||
@@ -20,8 +19,8 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
|||||||
{
|
{
|
||||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _player = default!;
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
[Dependency] private readonly MindSystem _mind = default!;
|
|
||||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
@@ -179,7 +178,9 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
|||||||
.ThenByDescending(x => x.completedObjectives);
|
.ThenByDescending(x => x.completedObjectives);
|
||||||
|
|
||||||
foreach (var (summary, _, _) in sortedAgents)
|
foreach (var (summary, _, _) in sortedAgents)
|
||||||
|
{
|
||||||
result.AppendLine(summary);
|
result.AppendLine(summary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto)
|
public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto)
|
||||||
@@ -244,8 +245,14 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
var name = mind.CharacterName;
|
var name = mind.CharacterName;
|
||||||
_mind.TryGetSession(mindId, out var session);
|
var username = (string?) null;
|
||||||
var username = session?.Name;
|
|
||||||
|
if (mind.OriginalOwnerUserId != null &&
|
||||||
|
_player.TryGetPlayerData(mind.OriginalOwnerUserId.Value, out var sessionData))
|
||||||
|
{
|
||||||
|
username = sessionData.UserName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (username != null)
|
if (username != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using Robust.Shared.Player;
|
|||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
|
|
||||||
namespace Content.Server.Power.EntitySystems;
|
namespace Content.Server.Power.EntitySystems;
|
||||||
|
|
||||||
@@ -723,8 +724,8 @@ internal sealed partial class PowerMonitoringConsoleSystem : SharedPowerMonitori
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Designates a supplied entity as a 'collection master'. Other entities which share this
|
// Designates a supplied entity as a 'collection master'. Other entities which share this
|
||||||
// entities collection name and are attached on the same load network are assigned this entity
|
// entities collection name and are attached on the same load network are assigned this entity
|
||||||
// as the master that represents them on the console UI. This way you can have one device
|
// as the master that represents them on the console UI. This way you can have one device
|
||||||
// represent multiple connected devices
|
// represent multiple connected devices
|
||||||
private void AssignEntityAsCollectionMaster
|
private void AssignEntityAsCollectionMaster
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace Content.Server.Preferences.Managers
|
|||||||
|
|
||||||
bool TryGetCachedPreferences(NetUserId userId, [NotNullWhen(true)] out PlayerPreferences? playerPreferences);
|
bool TryGetCachedPreferences(NetUserId userId, [NotNullWhen(true)] out PlayerPreferences? playerPreferences);
|
||||||
PlayerPreferences GetPreferences(NetUserId userId);
|
PlayerPreferences GetPreferences(NetUserId userId);
|
||||||
|
PlayerPreferences? GetPreferencesOrNull(NetUserId? userId);
|
||||||
IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds);
|
IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds);
|
||||||
bool HavePreferencesLoaded(ICommonSession session);
|
bool HavePreferencesLoaded(ICommonSession session);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,6 +256,20 @@ namespace Content.Server.Preferences.Managers
|
|||||||
return prefs;
|
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)
|
private async Task<PlayerPreferences> GetOrCreatePreferencesAsync(NetUserId userId)
|
||||||
{
|
{
|
||||||
var prefs = await _db.GetPlayerPreferencesAsync(userId);
|
var prefs = await _db.GetPlayerPreferencesAsync(userId);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Content.Shared.Dataset;
|
using Content.Shared.Dataset;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
@@ -47,9 +47,12 @@ public sealed class RandomMetadataSystem : EntitySystem
|
|||||||
var outputSegments = new List<string>();
|
var outputSegments = new List<string>();
|
||||||
foreach (var segment in segments)
|
foreach (var segment in segments)
|
||||||
{
|
{
|
||||||
outputSegments.Add(_prototype.TryIndex<DatasetPrototype>(segment, out var proto)
|
if (_prototype.TryIndex<DatasetPrototype>(segment, out var proto))
|
||||||
? Loc.GetString(_random.Pick(proto.Values))
|
outputSegments.Add(_random.Pick(proto.Values));
|
||||||
: Loc.GetString(segment));
|
else if (Loc.TryGetString(segment, out var localizedSegment))
|
||||||
|
outputSegments.Add(localizedSegment);
|
||||||
|
else
|
||||||
|
outputSegments.Add(segment);
|
||||||
}
|
}
|
||||||
return string.Join(separator, outputSegments);
|
return string.Join(separator, outputSegments);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Content.Server.GameTicking;
|
using Content.Server.GameTicking;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Spawners.Components;
|
using Content.Server.Spawners.Components;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.Administration;
|
using Content.Server.Administration;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules;
|
using Content.Server.GameTicking.Rules;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.StationEvents.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.Anomaly;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
using Content.Server.StationEvents.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.StationEvents.Components;
|
||||||
using Robust.Shared.Random;
|
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.Resist;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
using Content.Server.StationEvents.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.Components;
|
||||||
using Content.Server.Power.EntitySystems;
|
using Content.Server.Power.EntitySystems;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
using Content.Server.Station.Systems;
|
using Content.Server.Station.Systems;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Linq;
|
|||||||
using Content.Server.Cargo.Components;
|
using Content.Server.Cargo.Components;
|
||||||
using Content.Server.Cargo.Systems;
|
using Content.Server.Cargo.Systems;
|
||||||
using Content.Server.GameTicking;
|
using Content.Server.GameTicking;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
using Content.Server.StationEvents.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.StationEvents.Components;
|
||||||
using Content.Server.StationRecords;
|
using Content.Server.StationRecords;
|
||||||
using Content.Server.StationRecords.Systems;
|
using Content.Server.StationRecords.Systems;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.StationEvents.Components;
|
using Content.Server.StationEvents.Components;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Server.Atmos.EntitySystems;
|
using Content.Server.Atmos.EntitySystems;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.StationEvents.Components;
|
using Content.Server.StationEvents.Components;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.ImmovableRod;
|
using Content.Server.ImmovableRod;
|
||||||
using Content.Server.StationEvents.Components;
|
using Content.Server.StationEvents.Components;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
|
||||||
using Content.Server.Silicons.Laws;
|
using Content.Server.Silicons.Laws;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
using Content.Server.StationEvents.Components;
|
using Content.Server.StationEvents.Components;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.StationEvents.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.GameTicking.Rules.Components;
|
||||||
using Content.Server.StationEvents.Components;
|
using Content.Server.StationEvents.Components;
|
||||||
using Content.Server.Traits.Assorted;
|
using Content.Server.Traits.Assorted;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.StationEvents.Components;
|
using Content.Server.StationEvents.Components;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Ninja.Systems;
|
using Content.Server.Ninja.Systems;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Power.Components;
|
using Content.Server.Power.Components;
|
||||||
using Content.Server.Power.EntitySystems;
|
using Content.Server.Power.EntitySystems;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.StationEvents.Components;
|
using Content.Server.StationEvents.Components;
|
||||||
using Content.Server.Storage.Components;
|
using Content.Server.Storage.Components;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Ghost.Roles.Components;
|
using Content.Server.Ghost.Roles.Components;
|
||||||
using Content.Server.StationEvents.Components;
|
using Content.Server.StationEvents.Components;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.StationEvents.Components;
|
using Content.Server.StationEvents.Components;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Radio;
|
using Content.Server.Radio;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Content.Server.Administration.Logs;
|
using Content.Server.Administration.Logs;
|
||||||
using Content.Server.Chat.Systems;
|
using Content.Server.Chat.Systems;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules;
|
using Content.Server.GameTicking.Rules;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Station.Systems;
|
using Content.Server.Station.Systems;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using JetBrains.Annotations;
|
|||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.Fluids.EntitySystems;
|
using Content.Server.Fluids.EntitySystems;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.StationEvents.Components;
|
using Content.Server.StationEvents.Components;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.StationEvents.Components;
|
using Content.Server.StationEvents.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Server.GameTicking;
|
using Content.Server.GameTicking;
|
||||||
|
using Content.Server.GameTicking.Components;
|
||||||
using Content.Server.GameTicking.Rules;
|
using Content.Server.GameTicking.Rules;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.StationEvents.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.Server.Traitor.Components;
|
||||||
using Content.Shared.Mind.Components;
|
using Content.Shared.Mind.Components;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
namespace Content.Server.Traitor.Systems;
|
namespace Content.Server.Traitor.Systems;
|
||||||
|
|
||||||
@@ -9,7 +10,10 @@ namespace Content.Server.Traitor.Systems;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class AutoTraitorSystem : EntitySystem
|
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()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -20,44 +24,6 @@ public sealed class AutoTraitorSystem : EntitySystem
|
|||||||
|
|
||||||
private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args)
|
private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args)
|
||||||
{
|
{
|
||||||
TryMakeTraitor(uid, comp);
|
_antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, DefaultTraitorRule);
|
||||||
}
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,12 +83,9 @@ namespace Content.Server.Traitor.Uplink.Commands
|
|||||||
uplinkEntity = eUid;
|
uplinkEntity = eUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get TC count
|
|
||||||
var tcCount = _cfgManager.GetCVar(CCVars.TraitorStartingBalance);
|
|
||||||
Logger.Debug(_entManager.ToPrettyString(user));
|
|
||||||
// Finally add uplink
|
// Finally add uplink
|
||||||
var uplinkSys = _entManager.System<UplinkSystem>();
|
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"));
|
shell.WriteLine(Loc.GetString("add-uplink-command-error-2"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Shared.Damage;
|
using Content.Shared.Damage;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||||
|
|
||||||
namespace Content.Server.Zombies;
|
namespace Content.Server.Zombies;
|
||||||
@@ -35,6 +36,21 @@ public sealed partial class PendingZombieComponent : Component
|
|||||||
[DataField("gracePeriod"), ViewVariables(VVAccess.ReadWrite)]
|
[DataField("gracePeriod"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
public TimeSpan GracePeriod = TimeSpan.Zero;
|
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>
|
/// <summary>
|
||||||
/// The chance each second that a warning will be shown.
|
/// The chance each second that a warning will be shown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Server.Actions;
|
||||||
using Content.Server.Body.Systems;
|
using Content.Server.Body.Systems;
|
||||||
using Content.Server.Chat;
|
using Content.Server.Chat;
|
||||||
using Content.Server.Chat.Systems;
|
using Content.Server.Chat.Systems;
|
||||||
@@ -30,6 +31,7 @@ namespace Content.Server.Zombies
|
|||||||
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
|
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
|
||||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||||
[Dependency] private readonly ChatSystem _chat = default!;
|
[Dependency] private readonly ChatSystem _chat = default!;
|
||||||
|
[Dependency] private readonly ActionsSystem _actions = default!;
|
||||||
[Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
|
[Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
|
||||||
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
|
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
|
||||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||||
@@ -74,6 +76,8 @@ namespace Content.Server.Zombies
|
|||||||
}
|
}
|
||||||
|
|
||||||
component.NextTick = _timing.CurTime + TimeSpan.FromSeconds(1f);
|
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)
|
public override void Update(float frameTime)
|
||||||
|
|||||||
@@ -20,3 +20,8 @@ public enum AntagAcceptability
|
|||||||
All
|
All
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum AntagSelectionTime : byte
|
||||||
|
{
|
||||||
|
PrePlayerSpawn,
|
||||||
|
PostPlayerSpawn
|
||||||
|
}
|
||||||
|
|||||||
@@ -403,91 +403,6 @@ namespace Content.Shared.CCVar
|
|||||||
public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
|
public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
|
||||||
CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY);
|
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
|
* Tips
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -267,8 +267,11 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
|||||||
/// <param name="uid">The mob's entity UID.</param>
|
/// <param name="uid">The mob's entity UID.</param>
|
||||||
/// <param name="profile">The character profile to load.</param>
|
/// <param name="profile">The character profile to load.</param>
|
||||||
/// <param name="humanoid">Humanoid component of the entity</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))
|
if (!Resolve(uid, ref humanoid))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
|
||||||
using Content.Shared.Hands.Components;
|
using Content.Shared.Hands.Components;
|
||||||
using Content.Shared.Storage.EntitySystems;
|
using Content.Shared.Storage.EntitySystems;
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
namespace Content.Shared.Inventory;
|
namespace Content.Shared.Inventory;
|
||||||
@@ -96,7 +94,7 @@ public partial class InventorySystem
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="entity">The entity that you want to spawn an item on</param>
|
/// <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>
|
/// <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)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,14 +13,9 @@ namespace Content.Shared.NukeOps;
|
|||||||
[RegisterComponent, NetworkedComponent]
|
[RegisterComponent, NetworkedComponent]
|
||||||
public sealed partial class NukeOperativeComponent : Component
|
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>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("syndStatusIcon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))]
|
[DataField("syndStatusIcon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))]
|
||||||
public string SyndStatusIcon = "SyndicateFaction";
|
public string SyndStatusIcon = "SyndicateFaction";
|
||||||
|
|||||||
@@ -62,6 +62,32 @@ public abstract class SharedRoleSystem : EntitySystem
|
|||||||
_antagTypes.Add(typeof(T));
|
_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>
|
/// <summary>
|
||||||
/// Gives this mind a new role.
|
/// Gives this mind a new role.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -180,6 +206,11 @@ public abstract class SharedRoleSystem : EntitySystem
|
|||||||
return _antagTypes.Contains(typeof(T));
|
return _antagTypes.Contains(typeof(T));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsAntagonistRole(Type component)
|
||||||
|
{
|
||||||
|
return _antagTypes.Contains(component);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Play a sound for the mind, if it has a session attached.
|
/// Play a sound for the mind, if it has a session attached.
|
||||||
/// Use this for role greeting sounds.
|
/// Use this for role greeting sounds.
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
using Content.Shared.Hands.Components;
|
using Content.Shared.Hands.Components;
|
||||||
using Content.Shared.Hands.EntitySystems;
|
using Content.Shared.Hands.EntitySystems;
|
||||||
using Content.Shared.Inventory;
|
using Content.Shared.Inventory;
|
||||||
using Content.Shared.Preferences;
|
|
||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
using Content.Shared.Storage;
|
using Content.Shared.Storage;
|
||||||
using Content.Shared.Storage.EntitySystems;
|
using Content.Shared.Storage.EntitySystems;
|
||||||
using Robust.Shared.Collections;
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
namespace Content.Shared.Station;
|
namespace Content.Shared.Station;
|
||||||
|
|
||||||
public abstract class SharedStationSpawningSystem : EntitySystem
|
public abstract class SharedStationSpawningSystem : EntitySystem
|
||||||
{
|
{
|
||||||
|
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
|
||||||
[Dependency] protected readonly InventorySystem InventorySystem = default!;
|
[Dependency] protected readonly InventorySystem InventorySystem = default!;
|
||||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||||
[Dependency] private readonly SharedStorageSystem _storage = default!;
|
[Dependency] private readonly SharedStorageSystem _storage = default!;
|
||||||
@@ -21,8 +22,22 @@ public abstract class SharedStationSpawningSystem : EntitySystem
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="entity">Entity to load out.</param>
|
/// <param name="entity">Entity to load out.</param>
|
||||||
/// <param name="startingGear">Starting gear to use.</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))
|
if (InventorySystem.TryGetSlots(entity, out var slotDefinitions))
|
||||||
{
|
{
|
||||||
foreach (var slot in 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
|
- type: Transform
|
||||||
pos: 0.5436061,-7.5129323
|
pos: 0.5436061,-7.5129323
|
||||||
parent: 325
|
parent: 325
|
||||||
- proto: SpawnPointLoneNukeOperative
|
- proto: SpawnPointNukies
|
||||||
entities:
|
entities:
|
||||||
- uid: 322
|
- uid: 322
|
||||||
components:
|
components:
|
||||||
|
|||||||
@@ -84,8 +84,7 @@
|
|||||||
name: ghost-role-information-loneop-name
|
name: ghost-role-information-loneop-name
|
||||||
description: ghost-role-information-loneop-description
|
description: ghost-role-information-loneop-description
|
||||||
rules: ghost-role-information-loneop-rules
|
rules: ghost-role-information-loneop-rules
|
||||||
- type: GhostRoleMobSpawner
|
- type: GhostRoleAntagSpawner
|
||||||
prototype: MobHumanLoneNuclearOperative
|
|
||||||
- type: Sprite
|
- type: Sprite
|
||||||
sprite: Markers/jobs.rsi
|
sprite: Markers/jobs.rsi
|
||||||
layers:
|
layers:
|
||||||
|
|||||||
@@ -405,11 +405,28 @@
|
|||||||
weight: 3
|
weight: 3
|
||||||
duration: 1
|
duration: 1
|
||||||
- type: ZombieRule
|
- type: ZombieRule
|
||||||
minStartDelay: 0 #let them know immediately
|
- type: AntagSelection
|
||||||
maxStartDelay: 10
|
definitions:
|
||||||
maxInitialInfected: 3 #fewer zombies
|
- prefRoles: [ InitialInfected ]
|
||||||
minInitialInfectedGrace: 300 #less time to prepare
|
max: 3
|
||||||
maxInitialInfectedGrace: 450
|
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
|
- type: entity
|
||||||
id: LoneOpsSpawn
|
id: LoneOpsSpawn
|
||||||
@@ -422,7 +439,29 @@
|
|||||||
minimumPlayers: 20
|
minimumPlayers: 20
|
||||||
reoccurrenceDelay: 30
|
reoccurrenceDelay: 30
|
||||||
duration: 1
|
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
|
- type: entity
|
||||||
id: MassHallucinations
|
id: MassHallucinations
|
||||||
|
|||||||
@@ -34,6 +34,23 @@
|
|||||||
id: Thief
|
id: Thief
|
||||||
components:
|
components:
|
||||||
- type: ThiefRule
|
- 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
|
- type: entity
|
||||||
noSpawn: true
|
noSpawn: true
|
||||||
|
|||||||
@@ -70,29 +70,114 @@
|
|||||||
components:
|
components:
|
||||||
- type: GameRule
|
- type: GameRule
|
||||||
minPlayers: 20
|
minPlayers: 20
|
||||||
|
- type: RandomMetadata #this generates the random operation name cuz it's cool.
|
||||||
|
nameSegments:
|
||||||
|
- operationPrefix
|
||||||
|
- operationSuffix
|
||||||
- type: NukeopsRule
|
- type: NukeopsRule
|
||||||
faction: Syndicate
|
- type: LoadMapRule
|
||||||
|
gameMap: NukieOutpost
|
||||||
- type: entity
|
- type: AntagSelection
|
||||||
id: Pirates
|
selectionTime: PrePlayerSpawn
|
||||||
parent: BaseGameRule
|
definitions:
|
||||||
noSpawn: true
|
- prefRoles: [ NukeopsCommander ]
|
||||||
components:
|
fallbackRoles: [ Nukeops, NukeopsMedic ]
|
||||||
- type: PiratesRule
|
max: 1
|
||||||
|
playerRatio: 10
|
||||||
|
startingGear: SyndicateCommanderGearFull
|
||||||
|
components:
|
||||||
|
- 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
|
- type: entity
|
||||||
id: Traitor
|
id: Traitor
|
||||||
parent: BaseGameRule
|
parent: BaseGameRule
|
||||||
noSpawn: true
|
noSpawn: true
|
||||||
components:
|
components:
|
||||||
|
- type: GameRule
|
||||||
|
minPlayers: 5
|
||||||
|
delay:
|
||||||
|
min: 240
|
||||||
|
max: 420
|
||||||
- type: TraitorRule
|
- type: TraitorRule
|
||||||
|
- type: AntagSelection
|
||||||
|
definitions:
|
||||||
|
- prefRoles: [ Traitor ]
|
||||||
|
max: 12
|
||||||
|
playerRatio: 10
|
||||||
|
lateJoinAdditional: true
|
||||||
|
mindComponents:
|
||||||
|
- type: TraitorRole
|
||||||
|
prototype: Traitor
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: Revolutionary
|
id: Revolutionary
|
||||||
parent: BaseGameRule
|
parent: BaseGameRule
|
||||||
noSpawn: true
|
noSpawn: true
|
||||||
components:
|
components:
|
||||||
|
- type: GameRule
|
||||||
|
minPlayers: 15
|
||||||
- type: RevolutionaryRule
|
- 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
|
- type: entity
|
||||||
id: Sandbox
|
id: Sandbox
|
||||||
@@ -113,7 +198,32 @@
|
|||||||
parent: BaseGameRule
|
parent: BaseGameRule
|
||||||
noSpawn: true
|
noSpawn: true
|
||||||
components:
|
components:
|
||||||
|
- type: GameRule
|
||||||
|
minPlayers: 20
|
||||||
|
delay:
|
||||||
|
min: 600
|
||||||
|
max: 900
|
||||||
- type: ZombieRule
|
- 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
|
# event schedulers
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -142,7 +252,6 @@
|
|||||||
- id: BasicTrashVariationPass
|
- id: BasicTrashVariationPass
|
||||||
- id: SolidWallRustingVariationPass
|
- id: SolidWallRustingVariationPass
|
||||||
- id: ReinforcedWallRustingVariationPass
|
- id: ReinforcedWallRustingVariationPass
|
||||||
- id: CutWireVariationPass
|
|
||||||
- id: BasicPuddleMessVariationPass
|
- id: BasicPuddleMessVariationPass
|
||||||
prob: 0.99
|
prob: 0.99
|
||||||
orGroup: puddleMess
|
orGroup: puddleMess
|
||||||
|
|||||||
@@ -273,8 +273,18 @@
|
|||||||
#Head Rev Gear
|
#Head Rev Gear
|
||||||
- type: startingGear
|
- type: startingGear
|
||||||
id: HeadRevGear
|
id: HeadRevGear
|
||||||
equipment:
|
storage:
|
||||||
pocket2: Flash
|
back:
|
||||||
|
- Flash
|
||||||
|
- ClothingEyesGlassesSunglasses
|
||||||
|
|
||||||
|
#Thief Gear
|
||||||
|
- type: startingGear
|
||||||
|
id: ThiefGear
|
||||||
|
storage:
|
||||||
|
back:
|
||||||
|
- ToolboxThief
|
||||||
|
- ClothingHandsChameleonThief
|
||||||
|
|
||||||
#Gladiator with spear
|
#Gladiator with spear
|
||||||
- type: startingGear
|
- type: startingGear
|
||||||
|
|||||||
@@ -153,15 +153,3 @@
|
|||||||
- Zombie
|
- Zombie
|
||||||
- BasicStationEventScheduler
|
- BasicStationEventScheduler
|
||||||
- BasicRoundstartVariation
|
- 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