Refactor antag rule code (#23445)

* Initial Pass, Rev, Thief

* Zombie initial pass

* Rebase, Traitor

* Nukeops, More overloads

* Revert RevolutionaryRuleComponent

* Use TryRoundStartAttempt, Rewrite nukie spawning

* Comments, Add task scheduler to GameRuleSystem

* Zombie initial testing done

* Sort methods, rework GameRuleTask

* Add CCVar, Initial testing continues

* Might as well get rid of the obsolete logging

* Oops, i dont know how to log apparently

* Suggested formatting fixes

* Suggested changes

* Fix merge issues

* Minor optimisation

* Allowed thief to choose other antags

* Review changes

* Spawn items on floor first, then inserting

* minor tweaks

* Shift as much as possible to ProtoId<>

* Remove unneeded

* Add exclusive antag attribute

* Fix merge issues

* Minor formatting fix

* Convert to struct

* Cleanup

* Review cleanup (need to test a lot)

* Some fixes, (mostly) tested

* oop

* Pass tests (for real)

---------

Co-authored-by: Rainfall <rainfey0+git@gmail.com>
Co-authored-by: AJCM <AJCM@tutanota.com>
This commit is contained in:
Rainfey
2024-02-29 06:25:10 +00:00
committed by GitHub
parent 3966a65c65
commit 4e6c59cfe5
53 changed files with 22454 additions and 22396 deletions

View File

@@ -3,6 +3,7 @@ using Content.Shared.Chat;
using Content.Shared.NukeOps; using Content.Shared.NukeOps;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Timing;
namespace Content.Client.NukeOps; namespace Content.Client.NukeOps;
@@ -10,6 +11,8 @@ namespace Content.Client.NukeOps;
public sealed class WarDeclaratorBoundUserInterface : BoundUserInterface public sealed class WarDeclaratorBoundUserInterface : BoundUserInterface
{ {
[Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ILocalizationManager _localizationManager = default!;
[ViewVariables] [ViewVariables]
private WarDeclaratorWindow? _window; private WarDeclaratorWindow? _window;
@@ -20,7 +23,7 @@ public sealed class WarDeclaratorBoundUserInterface : BoundUserInterface
{ {
base.Open(); base.Open();
_window = new WarDeclaratorWindow(); _window = new WarDeclaratorWindow(_gameTiming, _localizationManager);
if (State != null) if (State != null)
UpdateState(State); UpdateState(State);
@@ -42,7 +45,8 @@ public sealed class WarDeclaratorBoundUserInterface : BoundUserInterface
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);
if (disposing) _window?.Dispose(); if (disposing)
_window?.Dispose();
} }
private void OnWarDeclaratorActivated(string message) private void OnWarDeclaratorActivated(string message)

View File

@@ -1,4 +1,5 @@
<DefaultWindow xmlns="https://spacestation14.io" <controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Loc 'war-declarator-ui-header'}"> Title="{Loc 'war-declarator-ui-header'}">
<BoxContainer Orientation="Vertical" SeparationOverride="4" MinWidth="440"> <BoxContainer Orientation="Vertical" SeparationOverride="4" MinWidth="440">
@@ -7,12 +8,16 @@
MinHeight="200" MinHeight="200"
Access="Public" /> Access="Public" />
<Button Name="WarButton" <Button Name="WarButton"
Text="{Loc 'war-declarator-ui-war-button'}" Text="{Loc 'war-declarator-ui-try-war-button'}"
StyleClasses="Caution" StyleClasses="Caution"
Access="Public"/> Access="Public"/>
<Label Name="StatusLabel" <BoxContainer Orientation="Vertical" HorizontalExpand="True">
Access="Public"/> <Label Name="StatusLabel"
<Label Name="InfoLabel" Align="Center"
Access="Public"/> Access="Public"/>
<Label Name="InfoLabel"
Align="Center"
Access="Public"/>
</BoxContainer>
</BoxContainer> </BoxContainer>
</DefaultWindow> </controls:FancyWindow>

View File

@@ -1,8 +1,7 @@
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.NukeOps; using Content.Shared.NukeOps;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -10,74 +9,83 @@ using Robust.Shared.Utility;
namespace Content.Client.NukeOps; namespace Content.Client.NukeOps;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class WarDeclaratorWindow : DefaultWindow public sealed partial class WarDeclaratorWindow : FancyWindow
{ {
private readonly IGameTiming _gameTiming; private readonly IGameTiming _gameTiming;
public event Action<string>? OnActivated; public event Action<string>? OnActivated;
private TimeSpan _endTime; private TimeSpan _endTime;
private TimeSpan _timeStamp; private TimeSpan _shuttleDisabledTime;
private WarConditionStatus _status; private WarConditionStatus _status;
public WarDeclaratorWindow() public WarDeclaratorWindow(IGameTiming gameTiming, ILocalizationManager localizationManager)
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
_gameTiming = IoCManager.Resolve<IGameTiming>(); _gameTiming = gameTiming;
WarButton.OnPressed += (_) => OnActivated?.Invoke(Rope.Collapse(MessageEdit.TextRope)); WarButton.OnPressed += (_) => OnActivated?.Invoke(Rope.Collapse(MessageEdit.TextRope));
var loc = IoCManager.Resolve<ILocalizationManager>(); MessageEdit.Placeholder = new Rope.Leaf(localizationManager.GetString("war-declarator-message-placeholder"));
MessageEdit.Placeholder = new Rope.Leaf(loc.GetString("war-declarator-message-placeholder"));
} }
protected override void Draw(DrawingHandleScreen handle) protected override void FrameUpdate(FrameEventArgs args)
{ {
base.Draw(handle);
UpdateTimer(); UpdateTimer();
} }
public void UpdateState(WarDeclaratorBoundUserInterfaceState state) public void UpdateState(WarDeclaratorBoundUserInterfaceState state)
{ {
WarButton.Disabled = state.Status != WarConditionStatus.YES_WAR; if (state.Status == null)
return;
WarButton.Disabled = state.Status == WarConditionStatus.WarReady;
_timeStamp = state.Delay;
_endTime = state.EndTime; _endTime = state.EndTime;
_status = state.Status; _shuttleDisabledTime = state.ShuttleDisabledTime;
_status = state.Status.Value;
switch(state.Status) UpdateStatus(state.Status.Value);
}
private void UpdateStatus(WarConditionStatus status)
{
switch (status)
{ {
case WarConditionStatus.WAR_READY: case WarConditionStatus.WarReady:
WarButton.Disabled = true;
StatusLabel.Text = Loc.GetString("war-declarator-boost-declared"); StatusLabel.Text = Loc.GetString("war-declarator-boost-declared");
InfoLabel.Text = Loc.GetString("war-declarator-conditions-ready");
StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateLow);
break;
case WarConditionStatus.WAR_DELAY:
StatusLabel.Text = Loc.GetString("war-declarator-boost-declared-delay");
UpdateTimer(); UpdateTimer();
StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateLow); StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateLow);
break; break;
case WarConditionStatus.YES_WAR: case WarConditionStatus.YesWar:
WarButton.Text = Loc.GetString("war-declarator-ui-war-button");
StatusLabel.Text = Loc.GetString("war-declarator-boost-possible"); StatusLabel.Text = Loc.GetString("war-declarator-boost-possible");
UpdateTimer(); UpdateTimer();
StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateGood); StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateGood);
break; break;
case WarConditionStatus.NO_WAR_SMALL_CREW: case WarConditionStatus.NoWarSmallCrew:
StatusLabel.Text = Loc.GetString("war-declarator-boost-impossible"); StatusLabel.Text = Loc.GetString("war-declarator-boost-impossible");
InfoLabel.Text = Loc.GetString("war-declarator-conditions-small-crew", ("min", state.MinCrew)); InfoLabel.Text = Loc.GetString("war-declarator-conditions-small-crew");
StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateNone); StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateNone);
break; break;
case WarConditionStatus.NO_WAR_SHUTTLE_DEPARTED: case WarConditionStatus.NoWarShuttleDeparted:
StatusLabel.Text = Loc.GetString("war-declarator-boost-impossible"); StatusLabel.Text = Loc.GetString("war-declarator-boost-impossible");
InfoLabel.Text = Loc.GetString("war-declarator-conditions-left-outpost"); InfoLabel.Text = Loc.GetString("war-declarator-conditions-left-outpost");
StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateNone); StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateNone);
break; break;
case WarConditionStatus.NO_WAR_TIMEOUT: case WarConditionStatus.NoWarTimeout:
StatusLabel.Text = Loc.GetString("war-declarator-boost-impossible"); StatusLabel.Text = Loc.GetString("war-declarator-boost-impossible");
InfoLabel.Text = Loc.GetString("war-declarator-conditions-time-out"); InfoLabel.Text = Loc.GetString("war-declarator-conditions-time-out");
StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateNone); StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateNone);
break; break;
case WarConditionStatus.NoWarUnknown:
StatusLabel.Text = Loc.GetString("war-declarator-boost-impossible");
InfoLabel.Text = Loc.GetString("war-declarator-conditions-unknown");
StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateNone);
break;
default: default:
StatusLabel.Text = Loc.GetString("war-declarator-boost-impossible"); StatusLabel.Text = Loc.GetString("war-declarator-boost-impossible");
InfoLabel.Text = Loc.GetString("war-declarator-conditions-unknown"); InfoLabel.Text = Loc.GetString("war-declarator-conditions-unknown");
@@ -86,43 +94,24 @@ public sealed partial class WarDeclaratorWindow : DefaultWindow
} }
} }
public void UpdateTimer() private void UpdateTimer()
{ {
switch(_status) switch(_status)
{ {
case WarConditionStatus.YES_WAR: case WarConditionStatus.YesWar:
var gameruleTime = _gameTiming.CurTime.Subtract(_timeStamp); var timeLeft = _endTime.Subtract(_gameTiming.CurTime);
var timeLeft = _endTime.Subtract(gameruleTime);
if (timeLeft > TimeSpan.Zero) if (timeLeft > TimeSpan.Zero)
{ InfoLabel.Text = Loc.GetString("war-declarator-boost-timer", ("time", timeLeft.ToString("mm\\:ss")));
InfoLabel.Text = Loc.GetString("war-declarator-boost-timer", ("minutes", timeLeft.Minutes), ("seconds", timeLeft.Seconds));
}
else else
{ UpdateStatus(WarConditionStatus.NoWarTimeout);
_status = WarConditionStatus.NO_WAR_TIMEOUT;
StatusLabel.Text = Loc.GetString("war-declarator-boost-impossible");
InfoLabel.Text = Loc.GetString("war-declarator-conditions-time-out");
StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateNone);
WarButton.Disabled = true;
}
break; break;
case WarConditionStatus.WAR_DELAY:
var timeAfterDeclaration = _gameTiming.CurTime.Subtract(_timeStamp);
var timeRemain = _endTime.Subtract(timeAfterDeclaration);
if (timeRemain > TimeSpan.Zero) case WarConditionStatus.WarReady:
{ var time = _shuttleDisabledTime.Subtract(_gameTiming.CurTime);
InfoLabel.Text = Loc.GetString("war-declarator-boost-timer", ("minutes", timeRemain.Minutes), ("seconds", timeRemain.Seconds)); if (time > TimeSpan.Zero)
} InfoLabel.Text = Loc.GetString("war-declarator-boost-timer", ("time", time.ToString("mm\\:ss")));
else else
{
_status = WarConditionStatus.WAR_READY;
StatusLabel.Text = Loc.GetString("war-declarator-boost-declared");
InfoLabel.Text = Loc.GetString("war-declarator-conditions-ready"); InfoLabel.Text = Loc.GetString("war-declarator-conditions-ready");
StatusLabel.SetOnlyStyleClass(StyleNano.StyleClassPowerStateLow);
WarButton.Disabled = true;
}
break; break;
default: default:
return; return;

View File

@@ -31,6 +31,7 @@ namespace Content.IntegrationTests.Tests
{ {
"CentComm", "CentComm",
"Dart", "Dart",
"NukieOutpost"
}; };
private static readonly string[] Grids = private static readonly string[] Grids =
@@ -38,7 +39,7 @@ namespace Content.IntegrationTests.Tests
"/Maps/centcomm.yml", "/Maps/centcomm.yml",
"/Maps/Shuttles/cargo.yml", "/Maps/Shuttles/cargo.yml",
"/Maps/Shuttles/emergency.yml", "/Maps/Shuttles/emergency.yml",
"/Maps/infiltrator.yml", "/Maps/Shuttles/infiltrator.yml",
}; };
private static readonly string[] GameMaps = private static readonly string[] GameMaps =
@@ -53,6 +54,7 @@ namespace Content.IntegrationTests.Tests
"Bagel", "Bagel",
"Origin", "Origin",
"CentComm", "CentComm",
"NukieOutpost",
"Box", "Box",
"Europa", "Europa",
"Saltern", "Saltern",

View File

@@ -1,15 +1,12 @@
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules;
using Content.Server.StationEvents.Events;
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.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mind.Components; using Content.Shared.Mind.Components;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Utility;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Systems; namespace Content.Server.Administration.Systems;
@@ -21,7 +18,6 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!; [Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!;
[Dependency] private readonly PiratesRuleSystem _piratesRule = default!; [Dependency] private readonly PiratesRuleSystem _piratesRule = default!;
[Dependency] private readonly RevolutionaryRuleSystem _revolutionaryRule = default!; [Dependency] private readonly RevolutionaryRuleSystem _revolutionaryRule = default!;
[Dependency] private readonly SharedMindSystem _minds = default!;
// 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)
@@ -34,7 +30,7 @@ public sealed partial class AdminVerbSystem
if (!_adminManager.HasAdminFlag(player, AdminFlags.Fun)) if (!_adminManager.HasAdminFlag(player, AdminFlags.Fun))
return; return;
if (!TryComp<MindContainerComponent>(args.Target, out var targetMindComp)) if (!HasComp<MindContainerComponent>(args.Target))
return; return;
Verb traitor = new() Verb traitor = new()
@@ -44,12 +40,9 @@ 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 (!_minds.TryGetSession(targetMindComp.Mind, out var session))
return;
// if its a monkey or mouse or something dont give uplink or objectives // if its a monkey or mouse or something dont give uplink or objectives
var isHuman = HasComp<HumanoidAppearanceComponent>(args.Target); var isHuman = HasComp<HumanoidAppearanceComponent>(args.Target);
_traitorRule.MakeTraitor(session, giveUplink: isHuman, giveObjectives: isHuman); _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"),
@@ -78,10 +71,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 = () =>
{ {
if (!_minds.TryGetMind(args.Target, out var mindId, out var mind)) _nukeopsRule.MakeLoneNukie(args.Target);
return;
_nukeopsRule.MakeLoneNukie(mindId, mind);
}, },
Impact = LogImpact.High, Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-nuclear-operative"), Message = Loc.GetString("admin-verb-make-nuclear-operative"),
@@ -95,10 +85,7 @@ 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 = () =>
{ {
if (!_minds.TryGetMind(args.Target, out var mindId, out var mind)) _piratesRule.MakePirate(args.Target);
return;
_piratesRule.MakePirate(mindId, mind);
}, },
Impact = LogImpact.High, Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-pirate"), Message = Loc.GetString("admin-verb-make-pirate"),
@@ -113,9 +100,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 = () =>
{ {
if (!_minds.TryGetMind(args.Target, out var mindId, out var mind)) _revolutionaryRule.OnHeadRevAdmin(args.Target);
return;
_revolutionaryRule.OnHeadRevAdmin(mindId, mind);
}, },
Impact = LogImpact.High, Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-head-rev"), Message = Loc.GetString("admin-verb-make-head-rev"),
@@ -129,10 +114,7 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/ihscombat.rsi"), "icon"), Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/ihscombat.rsi"), "icon"),
Act = () => Act = () =>
{ {
if (!_minds.TryGetSession(targetMindComp.Mind, out var session)) _thief.AdminMakeThief(args.Target, false); //Midround add pacified is bad
return;
_thief.AdminMakeThief(session, false); //Midround add pacific is bad
}, },
Impact = LogImpact.High, Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-thief"), Message = Loc.GetString("admin-verb-make-thief"),

View File

@@ -1,305 +1,347 @@
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.Roles.Jobs;
using Content.Server.Preferences.Managers;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
using Robust.Server.Player;
using System.Linq;
using Content.Server.Mind; using Content.Server.Mind;
using Robust.Shared.Random; using Content.Server.Preferences.Managers;
using Robust.Shared.Map; using Content.Server.Roles.Jobs;
using System.Numerics; using Content.Server.Shuttles.Components;
using Content.Shared.Inventory; using Content.Shared.Antag;
using Content.Server.Storage.EntitySystems; using Content.Shared.Humanoid;
using Robust.Shared.Audio; using Content.Shared.Players;
using Robust.Server.GameObjects; using Content.Shared.Preferences;
using Content.Server.Chat.Managers; using Content.Shared.Roles;
using Content.Server.GameTicking;
using Robust.Shared.Containers;
using Content.Shared.Mobs.Components;
using Content.Server.Station.Systems;
using Content.Server.Shuttles.Systems;
using Content.Shared.Mobs;
using Robust.Server.Audio; using Robust.Server.Audio;
using Robust.Server.Containers; using Robust.Shared.Audio;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Content.Server.Shuttles.Components; using Robust.Shared.Random;
using System.Linq;
using Content.Shared.Chat;
using Robust.Shared.Enums;
namespace Content.Server.Antag; namespace Content.Server.Antag;
public sealed class AntagSelectionSystem : GameRuleSystem<GameRuleComponent> public sealed class AntagSelectionSystem : GameRuleSystem<GameRuleComponent>
{ {
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = default!; [Dependency] private readonly IServerPreferencesManager _prefs = default!;
[Dependency] private readonly IPlayerManager _playerSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AudioSystem _audioSystem = default!; [Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly JobSystem _jobs = default!; [Dependency] private readonly JobSystem _jobs = default!;
[Dependency] private readonly MindSystem _mindSystem = default!; [Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly StorageSystem _storageSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
#region Eligible Player Selection
/// <summary> /// <summary>
/// Attempts to start the game rule by checking if there are enough players in lobby and readied. /// Get all players that are eligible for an antag role
/// </summary> /// </summary>
/// <param name="ev">The roundstart attempt event</param> /// <param name="playerSessions">All sessions from which to select eligible players</param>
/// <param name="uid">The entity the gamerule you are using is on</param> /// <param name="antagPrototype">The prototype to get eligible players for</param>
/// <param name="minPlayers">The minimum amount of players needed for you gamerule to start.</param> /// <param name="includeAllJobs">Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included</param>
/// <param name="gameRule">The gamerule component.</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>
public void AttemptStartGameRule(RoundStartAttemptEvent ev, EntityUid uid, int minPlayers, GameRuleComponent gameRule) /// <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)
{ {
if (GameTicker.IsGameRuleAdded(uid, gameRule)) var eligiblePlayers = new List<EntityUid>();
foreach (var player in playerSessions)
{ {
if (!ev.Forced && ev.Players.Length < minPlayers) if (IsPlayerEligible(player, antagPrototype, includeAllJobs, acceptableAntags, ignorePreferences, allowNonHumanoids, customExcludeCondition))
{ eligiblePlayers.Add(player.AttachedEntity!.Value);
_chatManager.SendAdminAnnouncement(Loc.GetString("rev-not-enough-ready-players",
("readyPlayersCount", ev.Players.Length),
("minimumPlayers", minPlayers)));
ev.Cancel();
}
else if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rev-no-one-ready"));
ev.Cancel();
}
} }
return eligiblePlayers;
} }
/// <summary> /// <summary>
/// Will check which players are eligible to be chosen for antagonist and give them the given antag. /// 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> /// </summary>
/// <param name="antagPrototype">The antag prototype from your rule component.</param> /// <param name="playerSessions">All sessions from which to select eligible players</param>
/// <param name="maxAntags">How many antags can be present in any given round.</param> /// <param name="antagPrototype">The prototype to get eligible players for</param>
/// <param name="antagsPerPlayer">How many players you need to spawn an additional antag.</param> /// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
/// <param name="antagSound">The intro sound that plays when the antag is chosen.</param> /// <returns>List of all player sessions that match the requirements</returns>
/// <param name="antagGreeting">The antag message you want shown when the antag is chosen.</param> public List<ICommonSession> GetEligibleSessions(IEnumerable<ICommonSession> playerSessions, ProtoId<AntagPrototype> antagPrototype, bool ignorePreferences = false)
/// <param name="greetingColor">The color of the message for the antag greeting in hex.</param>
/// <param name="chosen">A list of all the antags chosen in case you need to add stuff after.</param>
/// <param name="includeHeads">Whether or not heads can be chosen as antags for this gamemode.</param>
public void EligiblePlayers(string antagPrototype,
int maxAntags,
int antagsPerPlayer,
SoundSpecifier? antagSound,
string antagGreeting,
string greetingColor,
out List<EntityUid> chosen,
bool includeHeads = false)
{ {
var allPlayers = _playerSystem.Sessions.ToList(); var eligibleSessions = new List<ICommonSession>();
var playerList = new List<ICommonSession>();
var prefList = new List<ICommonSession>(); foreach (var session in playerSessions)
chosen = new List<EntityUid>();
foreach (var player in allPlayers)
{ {
if (includeHeads == false) if (IsSessionEligible(session, antagPrototype, ignorePreferences))
{ eligibleSessions.Add(session);
if (!_jobs.CanBeAntag(player))
continue;
}
if (player.AttachedEntity == null || HasComp<HumanoidAppearanceComponent>(player.AttachedEntity))
playerList.Add(player);
else
continue;
var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter;
if (pref.AntagPreferences.Contains(antagPrototype))
prefList.Add(player);
} }
if (playerList.Count == 0) return eligibleSessions;
return; }
var antags = Math.Clamp(allPlayers.Count / antagsPerPlayer, 1, maxAntags); /// <summary>
for (var antag = 0; antag < antags; antag++) /// 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)
{ {
ICommonSession? chosenPlayer = null; //If we dont want to select any antag roles
if (prefList.Count == 0) case AntagAcceptability.None:
{
if (playerList.Count == 0)
{ {
if (_roleSystem.MindIsAntagonist(playerMind))
return false;
break; break;
} }
} //If we dont want to select exclusive antag roles
else 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)
{ {
chosenPlayer = _random.PickAndTake(prefList);
playerList.Remove(chosenPlayer); playerList.Remove(chosenPlayer);
} }
if (!_mindSystem.TryGetMind(chosenPlayer, out _, out var mind) || //If we have reached the desired number of players, skip
mind.OwnedEntity is not { } ownedEntity) if (chosenPlayers.Count >= count)
{
continue;
}
chosen.Add(ownedEntity);
_audioSystem.PlayGlobal(antagSound, ownedEntity);
if (mind.Session != null)
{
var message = Loc.GetString(antagGreeting);
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message, wrappedMessage, default, false, mind.Session.Channel, Color.FromHex(greetingColor));
}
}
}
/// <summary>
/// The function walks through all players, checking their role and preferences to generate a list of players who can become antagonists.
/// </summary>
/// <param name="candidates">a list of players to check out</param>
/// <param name="antagPreferenceId">antagonist's code id</param>
/// <returns></returns>
public List<ICommonSession> FindPotentialAntags(in Dictionary<ICommonSession, HumanoidCharacterProfile> candidates, string antagPreferenceId)
{
var list = new List<ICommonSession>();
var pendingQuery = GetEntityQuery<PendingClockInComponent>();
foreach (var player in candidates.Keys)
{
// Role prevents antag.
if (!_jobs.CanBeAntag(player))
continue; continue;
// Latejoin //Pick and choose a random number of players from this list
if (player.AttachedEntity != null && pendingQuery.HasComponent(player.AttachedEntity.Value)) chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList));
}
return chosenPlayers;
}
/// <summary>
/// Helper method to choose antags from a list
/// </summary>
/// <param name="eligiblePlayers">List of eligible players</param>
/// <param name="count">How many to choose</param>
/// <returns>Up to the specified count of elements from the provided list</returns>
public List<EntityUid> ChooseAntags(int count, List<EntityUid> eligiblePlayers)
{
var chosenPlayers = new List<EntityUid>();
for (var i = 0; i < count; i++)
{
if (eligiblePlayers.Count == 0)
break;
chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
}
return chosenPlayers;
}
/// <summary>
/// Selects a set number of sessions from several lists, prioritising the first list till its empty, then second list etc
/// </summary>
/// <param name="eligiblePlayerLists">Array of lists, which are chosen from in order until the correct number of items are selected</param>
/// <param name="count">How many items to select</param>
/// <returns>Up to the specified count of elements from all provided lists</returns>
public List<ICommonSession> ChooseAntags(int count, params List<ICommonSession>[] eligiblePlayerLists)
{
var chosenPlayers = new List<ICommonSession>();
foreach (var playerList in eligiblePlayerLists)
{
//Remove all chosen players from this list, to prevent duplicates
foreach (var chosenPlayer in chosenPlayers)
{
playerList.Remove(chosenPlayer);
}
//If we have reached the desired number of players, skip
if (chosenPlayers.Count >= count)
continue; continue;
list.Add(player); //Pick and choose a random number of players from this list
chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList));
} }
return chosenPlayers;
var prefList = new List<ICommonSession>();
foreach (var player in list)
{
//player preferences to play as this antag
var profile = candidates[player];
if (profile.AntagPreferences.Contains(antagPreferenceId))
{
prefList.Add(player);
}
}
if (prefList.Count == 0)
{
Log.Info($"Insufficient preferred antag:{antagPreferenceId}, picking at random.");
prefList = list;
}
return prefList;
} }
/// <summary> /// <summary>
/// selects the specified number of players from the list /// Helper method to choose sessions from a list
/// </summary> /// </summary>
/// <param name="antagCount">how many players to take</param> /// <param name="eligiblePlayers">List of eligible sessions</param>
/// <param name="prefList">a list of players from which to draw</param> /// <param name="count">How many to choose</param>
/// <returns></returns> /// <returns>Up to the specified count of elements from the provided list</returns>
public List<ICommonSession> PickAntag(int antagCount, List<ICommonSession> prefList) public List<ICommonSession> ChooseAntags(int count, List<ICommonSession> eligiblePlayers)
{ {
var results = new List<ICommonSession>(antagCount); var chosenPlayers = new List<ICommonSession>();
if (prefList.Count == 0)
for (int i = 0; i < count; i++)
{ {
Log.Info("Insufficient ready players to fill up with antags, stopping the selection."); if (eligiblePlayers.Count == 0)
return results; break;
chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
} }
for (var i = 0; i < antagCount; i++) return chosenPlayers;
{
results.Add(_random.PickAndTake(prefList));
Log.Info("Selected a preferred antag.");
}
return results;
} }
#endregion
#region Briefings
/// <summary> /// <summary>
/// Will take a group of entities and check if they are all alive or dead /// Helper method to send the briefing text and sound to a list of entities
/// </summary> /// </summary>
/// <param name="list">The list of the entities</param> /// <param name="entities">The players chosen to be antags</param>
/// <param name="checkOffStation">Bool for if you want to check if someone is in space and consider them dead. (Won't check when emergency shuttle arrives just in case)</param> /// <param name="briefing">The briefing text to send</param>
/// <returns></returns> /// <param name="briefingColor">The color the briefing should be, null for default</param>
public bool IsGroupDead(List<EntityUid> list, bool checkOffStation) /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
public void SendBriefing(List<EntityUid> entities, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
{ {
var dead = 0; foreach (var entity in entities)
foreach (var entity in list)
{ {
if (TryComp<MobStateComponent>(entity, out var state)) SendBriefing(entity, briefing, briefingColor, briefingSound);
{
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
{
dead++;
}
else if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
{
dead++;
}
}
//If they don't have the MobStateComponent they might as well be dead.
else
{
dead++;
}
}
return dead == list.Count || list.Count == 0;
}
/// <summary>
/// Will attempt to spawn an item inside of a persons bag and then pockets.
/// </summary>
/// <param name="antag">The entity that you want to spawn an item on</param>
/// <param name="items">A list of prototype IDs that you want to spawn in the bag.</param>
public void GiveAntagBagGear(EntityUid antag, List<EntProtoId> items)
{
foreach (var item in items)
{
GiveAntagBagGear(antag, item);
} }
} }
/// <summary> /// <summary>
/// Will attempt to spawn an item inside of a persons bag and then pockets. /// Helper method to send the briefing text and sound to a player entity
/// </summary> /// </summary>
/// <param name="antag">The entity that you want to spawn an item on</param> /// <param name="entity">The entity chosen to be antag</param>
/// <param name="item">The prototype ID that you want to spawn in the bag.</param> /// <param name="briefing">The briefing text to send</param>
public void GiveAntagBagGear(EntityUid antag, string item) /// <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)
{ {
var itemToSpawn = Spawn(item, new EntityCoordinates(antag, Vector2.Zero)); if (!_mindSystem.TryGetMind(entity, out _, out var mindComponent))
if (!_inventory.TryGetSlotContainer(antag, "back", out var backSlot, out _))
return; return;
var bag = backSlot.ContainedEntity; if (mindComponent.Session == null)
if (bag != null && HasComp<ContainerManagerComponent>(bag) && _storageSystem.CanInsert(bag.Value, itemToSpawn, out _)) 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"></param>
/// <param name="briefing"></param>
/// <param name="briefingColor"></param>
/// <param name="briefingSound"></param>
public void SendBriefing(List<ICommonSession> sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
{
foreach (var session in sessions)
{ {
_storageSystem.Insert(bag.Value, itemToSpawn, out _); SendBriefing(session, briefing, briefingColor, briefingSound);
}
else if (_inventory.TryGetSlotContainer(antag, "jumpsuit", out var jumpsuit, out _) && jumpsuit.ContainedEntity != null)
{
if (_inventory.TryGetSlotContainer(antag, "pocket1", out var pocket1Slot, out _))
{
if (pocket1Slot.ContainedEntity == null)
{
if (_containerSystem.CanInsert(itemToSpawn, pocket1Slot))
{
_containerSystem.Insert(itemToSpawn, pocket1Slot);
}
}
else if (_inventory.TryGetSlotContainer(antag, "pocket2", out var pocket2Slot, out _))
{
if (pocket2Slot.ContainedEntity == null)
{
if (_containerSystem.CanInsert(itemToSpawn, pocket2Slot))
{
_containerSystem.Insert(itemToSpawn, pocket2Slot);
}
}
}
}
} }
} }
} /// <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)
{
_audioSystem.PlayGlobal(briefingSound, session);
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing));
ChatManager.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, briefingColor);
}
#endregion
}

View File

@@ -1,4 +1,4 @@
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;

View File

@@ -14,9 +14,6 @@ public sealed partial class NukeOperativeSpawnerComponent : Component
[DataField("name", required:true)] [DataField("name", required:true)]
public string OperativeName = default!; public string OperativeName = default!;
[DataField("rolePrototype", customTypeSerializer:typeof(PrototypeIdSerializer<AntagPrototype>), required:true)] [DataField]
public string OperativeRolePrototype = default!; public NukeopSpawnPreset SpawnDetails = default!;
[DataField("startingGearPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<StartingGearPrototype>), required:true)]
public string OperativeStartingGear = default!;
} }

View File

@@ -1,3 +1,4 @@
using Content.Server.Maps;
using Content.Server.NPC.Components; using Content.Server.NPC.Components;
using Content.Server.RoundEnd; using Content.Server.RoundEnd;
using Content.Server.StationEvents.Events; using Content.Server.StationEvents.Events;
@@ -16,15 +17,8 @@ namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))] [RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))]
public sealed partial class NukeopsRuleComponent : Component public sealed partial class NukeopsRuleComponent : Component
{ {
// TODO Replace with GameRuleComponent.minPlayers
/// <summary> /// <summary>
/// The minimum needed amount of players /// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
/// </summary>
[DataField]
public int MinPlayers = 20;
/// <summary>
/// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
/// </summary> /// </summary>
[DataField] [DataField]
public int PlayersPerOperative = 10; public int PlayersPerOperative = 10;
@@ -92,17 +86,11 @@ public sealed partial class NukeopsRuleComponent : Component
[DataField] [DataField]
public int WarTCAmountPerNukie = 40; public int WarTCAmountPerNukie = 40;
/// <summary>
/// Time allowed for declaration of war
/// </summary>
[DataField("warDeclarationDelay")]
public TimeSpan WarDeclarationDelay = TimeSpan.FromMinutes(6);
/// <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
/// </summary> /// </summary>
[DataField] [DataField]
public TimeSpan? WarNukieArriveDelay = TimeSpan.FromMinutes(15); public TimeSpan WarNukieArriveDelay = TimeSpan.FromMinutes(15);
/// <summary> /// <summary>
/// Minimal operatives count for war declaration /// Minimal operatives count for war declaration
@@ -116,38 +104,11 @@ public sealed partial class NukeopsRuleComponent : Component
[DataField] [DataField]
public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative"; public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
[DataField]
public ProtoId<AntagPrototype> CommanderRoleProto = "NukeopsCommander";
[DataField]
public ProtoId<AntagPrototype> OperativeRoleProto = "Nukeops";
[DataField]
public ProtoId<AntagPrototype> MedicRoleProto = "NukeopsMedic";
[DataField]
public ProtoId<StartingGearPrototype> CommanderStartGearProto = "SyndicateCommanderGearFull";
[DataField]
public ProtoId<StartingGearPrototype> MedicStartGearProto = "SyndicateOperativeMedicFull";
[DataField]
public ProtoId<StartingGearPrototype> OperativeStartGearProto = "SyndicateOperativeGearFull";
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
public string EliteNames = "SyndicateNamesElite";
[DataField] [DataField]
public string OperationName = "Test Operation"; public string OperationName = "Test Operation";
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))] [DataField]
public string NormalNames = "SyndicateNamesNormal"; public ProtoId<GameMapPrototype> OutpostMapPrototype = "NukieOutpost";
[DataField(customTypeSerializer: typeof(ResPathSerializer))]
public ResPath OutpostMap = new("/Maps/nukieplanet.yml");
[DataField(customTypeSerializer: typeof(ResPathSerializer))]
public ResPath ShuttleMap = new("/Maps/infiltrator.yml");
[DataField] [DataField]
public WinType WinType = WinType.Neutral; public WinType WinType = WinType.Neutral;
@@ -163,33 +124,53 @@ public sealed partial class NukeopsRuleComponent : Component
public EntityUid? NukieShuttle; public EntityUid? NukieShuttle;
public EntityUid? TargetStation; public EntityUid? TargetStation;
/// <summary>
/// Cached starting gear prototypes.
/// </summary>
[DataField]
public Dictionary<string, StartingGearPrototype> StartingGearPrototypes = new ();
/// <summary>
/// Cached operator name prototypes.
/// </summary>
[DataField]
public Dictionary<string, List<string>> OperativeNames = new();
/// <summary> /// <summary>
/// Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added. /// Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
/// </summary> /// </summary>
[DataField] [DataField]
public Dictionary<EntityUid, string> OperativeMindPendingData = new(); public Dictionary<EntityUid, string> OperativeMindPendingData = new();
/// <summary>
/// Players who played as an operative at some point in the round.
/// Stores the mind as well as the entity name
/// </summary>
[DataField]
public Dictionary<string, EntityUid> OperativePlayers = new();
[DataField(required: true)] [DataField(required: true)]
public ProtoId<NpcFactionPrototype> Faction = default!; public ProtoId<NpcFactionPrototype> Faction = default!;
[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>
/// Stores the presets for each operative type
/// Ie Commander, Agent and Operative
/// </summary>
[DataDefinition, Serializable]
public sealed partial class NukeopSpawnPreset
{
[DataField]
public ProtoId<AntagPrototype> AntagRoleProto = "Nukeops";
/// <summary>
/// The equipment set this operative will be given when spawned
/// </summary>
[DataField]
public ProtoId<StartingGearPrototype> GearProto = "SyndicateOperativeGearFull";
/// <summary>
/// The name prefix, ie "Agent"
/// </summary>
[DataField]
public LocId NamePrefix = "nukeops-role-operator";
/// <summary>
/// The entity name suffix will be chosen from this list randomly
/// </summary>
[DataField]
public ProtoId<DatasetPrototype> NameList = "SyndicateNamesNormal";
} }
public enum WinType : byte public enum WinType : byte

View File

@@ -1,5 +1,4 @@
using Content.Shared.Roles; using Content.Shared.Roles;
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;
@@ -32,15 +31,6 @@ public sealed partial class RevolutionaryRuleComponent : Component
[DataField] [DataField]
public ProtoId<AntagPrototype> HeadRevPrototypeId = "HeadRev"; public ProtoId<AntagPrototype> HeadRevPrototypeId = "HeadRev";
[DataField]
public ProtoId<AntagPrototype> RevPrototypeId = "Rev";
/// <summary>
/// Sound that plays when you are chosen as Rev. (Placeholder until I find something cool I guess)
/// </summary>
[DataField]
public SoundSpecifier HeadRevStartSound = new SoundPathSpecifier("/Audio/Ambience/Antag/headrev_start.ogg");
/// <summary> /// <summary>
/// Min players needed for Revolutionary gamemode to start. /// Min players needed for Revolutionary gamemode to start.
/// </summary> /// </summary>

View File

@@ -1,8 +1,7 @@
using Content.Shared.Random;
using Content.Shared.Roles;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Content.Shared.Roles;
using Robust.Shared.Player;
using Content.Shared.Preferences;
namespace Content.Server.GameTicking.Rules.Components; namespace Content.Server.GameTicking.Rules.Components;
@@ -12,6 +11,18 @@ namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(ThiefRuleSystem))] [RegisterComponent, Access(typeof(ThiefRuleSystem))]
public sealed partial class ThiefRuleComponent : Component public sealed partial class ThiefRuleComponent : Component
{ {
[DataField]
public ProtoId<WeightedRandomPrototype> BigObjectiveGroup = "ThiefBigObjectiveGroups";
[DataField]
public ProtoId<WeightedRandomPrototype> SmallObjectiveGroup = "ThiefObjectiveGroups";
[DataField]
public ProtoId<WeightedRandomPrototype> EscapeObjectiveGroup = "ThiefEscapeObjectiveGroups";
[DataField]
public float BigObjectiveChance = 0.7f;
/// <summary> /// <summary>
/// Add a Pacified comp to thieves /// Add a Pacified comp to thieves
/// </summary> /// </summary>
@@ -27,8 +38,6 @@ public sealed partial class ThiefRuleComponent : Component
[DataField] [DataField]
public ProtoId<AntagPrototype> ThiefPrototypeId = "Thief"; public ProtoId<AntagPrototype> ThiefPrototypeId = "Thief";
public Dictionary<ICommonSession, HumanoidCharacterProfile> StartCandidates = new();
[DataField] [DataField]
public float MaxObjectiveDifficulty = 2.5f; public float MaxObjectiveDifficulty = 2.5f;
@@ -39,7 +48,7 @@ public sealed partial class ThiefRuleComponent : Component
/// Things that will be given to thieves /// Things that will be given to thieves
/// </summary> /// </summary>
[DataField] [DataField]
public List<EntProtoId> StarterItems = new List<EntProtoId> { "ToolboxThief", "ClothingHandsChameleonThief" }; //TO DO - replace to chameleon thieving gloves whem merg public List<EntProtoId> StarterItems = new() { "ToolboxThief", "ClothingHandsChameleonThief" };
/// <summary> /// <summary>
/// All Thieves created by this rule /// All Thieves created by this rule

View File

@@ -1,8 +1,10 @@
using Content.Shared.Preferences; using Content.Server.NPC.Components;
using Content.Shared.Dataset;
using Content.Shared.Random;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Player; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.GameTicking.Rules.Components; namespace Content.Server.GameTicking.Rules.Components;
@@ -11,8 +13,23 @@ public sealed partial class TraitorRuleComponent : Component
{ {
public readonly List<EntityUid> TraitorMinds = new(); public readonly List<EntityUid> TraitorMinds = new();
[DataField("traitorPrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))] [DataField]
public string TraitorPrototypeId = "Traitor"; public ProtoId<AntagPrototype> TraitorPrototypeId = "Traitor";
[DataField]
public ProtoId<NpcFactionPrototype> NanoTrasenFaction = "NanoTrasen";
[DataField]
public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
[DataField]
public ProtoId<WeightedRandomPrototype> ObjectiveGroup = "TraitorObjectiveGroups";
[DataField]
public ProtoId<DatasetPrototype> CodewordAdjectives = "adjectives";
[DataField]
public ProtoId<DatasetPrototype> CodewordVerbs = "verbs";
public int TotalTraitors => TraitorMinds.Count; public int TotalTraitors => TraitorMinds.Count;
public string[] Codewords = new string[3]; public string[] Codewords = new string[3];
@@ -20,17 +37,24 @@ public sealed partial class TraitorRuleComponent : Component
public enum SelectionState public enum SelectionState
{ {
WaitingForSpawn = 0, WaitingForSpawn = 0,
ReadyToSelect = 1, ReadyToStart = 1,
SelectionMade = 2, Started = 2,
} }
/// <summary>
/// Current state of the rule
/// </summary>
public SelectionState SelectionStatus = SelectionState.WaitingForSpawn; public SelectionState SelectionStatus = SelectionState.WaitingForSpawn;
public TimeSpan AnnounceAt = TimeSpan.Zero;
public Dictionary<ICommonSession, HumanoidCharacterProfile> StartCandidates = new(); /// <summary>
/// When should traitors be selected and the announcement made
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan? AnnounceAt;
/// <summary> /// <summary>
/// Path to antagonist alert sound. /// Path to antagonist alert sound.
/// </summary> /// </summary>
[DataField("greetSoundNotification")] [DataField]
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg"); public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
} }

View File

@@ -2,98 +2,85 @@ using Content.Shared.Roles;
using Robust.Shared.Audio; 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;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.GameTicking.Rules.Components; 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("initialInfectedNames")] [DataField]
public Dictionary<string, string> InitialInfectedNames = new(); public Dictionary<string, string> InitialInfectedNames = new();
[DataField("patientZeroPrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))] [DataField]
public string PatientZeroPrototypeId = "InitialInfected"; public ProtoId<AntagPrototype> PatientZeroPrototypeId = "InitialInfected";
/// <summary>
/// Whether or not the initial infected have been chosen.
/// </summary>
[DataField("infectedChosen")]
public bool InfectedChosen;
/// <summary> /// <summary>
/// When the round will next check for round end. /// When the round will next check for round end.
/// </summary> /// </summary>
[DataField("nextRoundEndCheck", customTypeSerializer: typeof(TimeOffsetSerializer))] [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextRoundEndCheck; public TimeSpan? NextRoundEndCheck;
/// <summary> /// <summary>
/// The amount of time between each check for the end of the round. /// The amount of time between each check for the end of the round.
/// </summary> /// </summary>
[DataField("endCheckDelay")] [DataField]
public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30); public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30);
/// <summary> /// <summary>
/// The time at which the initial infected will be chosen. /// The time at which the initial infected will be chosen.
/// </summary> /// </summary>
[DataField("startTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan? StartTime; public TimeSpan? StartTime;
/// <summary> /// <summary>
/// The minimum amount of time after the round starts that the initial infected will be chosen. /// The minimum amount of time after the round starts that the initial infected will be chosen.
/// </summary> /// </summary>
[DataField("minStartDelay")] [DataField]
public TimeSpan MinStartDelay = TimeSpan.FromMinutes(10); public TimeSpan MinStartDelay = TimeSpan.FromMinutes(10);
/// <summary> /// <summary>
/// The maximum amount of time after the round starts that the initial infected will be chosen. /// The maximum amount of time after the round starts that the initial infected will be chosen.
/// </summary> /// </summary>
[DataField("maxStartDelay")] [DataField]
public TimeSpan MaxStartDelay = TimeSpan.FromMinutes(15); public TimeSpan MaxStartDelay = TimeSpan.FromMinutes(15);
/// <summary> /// <summary>
/// The sound that plays when someone becomes an initial infected. /// The sound that plays when someone becomes an initial infected.
/// todo: this should have a unique sound instead of reusing the zombie one. /// todo: this should have a unique sound instead of reusing the zombie one.
/// </summary> /// </summary>
[DataField("initialInfectedSound")] [DataField]
public SoundSpecifier InitialInfectedSound = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg"); public SoundSpecifier InitialInfectedSound = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg");
/// <summary> /// <summary>
/// The minimum amount of time initial infected have before they start taking infection damage. /// The minimum amount of time initial infected have before they start taking infection damage.
/// </summary> /// </summary>
[DataField("minInitialInfectedGrace")] [DataField]
public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f); public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f);
/// <summary> /// <summary>
/// The maximum amount of time initial infected have before they start taking damage. /// The maximum amount of time initial infected have before they start taking damage.
/// </summary> /// </summary>
[DataField("maxInitialInfectedGrace")] [DataField]
public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f); public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f);
/// <summary> /// <summary>
/// How many players for each initial infected. /// How many players for each initial infected.
/// </summary> /// </summary>
[DataField("playersPerInfected")] [DataField]
public int PlayersPerInfected = 10; public int PlayersPerInfected = 10;
/// <summary> /// <summary>
/// The maximum number of initial infected. /// The maximum number of initial infected.
/// </summary> /// </summary>
[DataField("maxInitialInfected")] [DataField]
public int MaxInitialInfected = 6; 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("zombieShuttleCallPercentage")] [DataField]
public float ZombieShuttleCallPercentage = 0.7f; public float ZombieShuttleCallPercentage = 0.7f;
/// <summary> [DataField]
/// Have we called the evac shuttle yet? public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead";
/// </summary>
[DataField("shuttleCalled")]
public bool ShuttleCalled;
[ValidatePrototypeId<EntityPrototype>]
public const string ZombifySelfActionPrototype = "ActionTurnUndead";
} }

View File

@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
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;

View File

@@ -1,13 +1,9 @@
using System.Diagnostics.CodeAnalysis;
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.Rules.Components;
using Content.Server.Station.Components;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.GameTicking.Rules; namespace Content.Server.GameTicking.Rules;
@@ -16,9 +12,9 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
[Dependency] protected readonly IRobustRandom RobustRandom = default!; [Dependency] protected readonly IRobustRandom RobustRandom = default!;
[Dependency] protected readonly IChatManager ChatManager = default!; [Dependency] protected readonly IChatManager ChatManager = default!;
[Dependency] protected readonly GameTicker GameTicker = default!; [Dependency] protected readonly GameTicker GameTicker = default!;
[Dependency] protected readonly IGameTiming Timing = default!;
// Not protected, just to be used in utility methods // Not protected, just to be used in utility methods
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!; [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly MapSystem _map = default!; [Dependency] private readonly MapSystem _map = default!;

File diff suppressed because it is too large Load Diff

View File

@@ -271,11 +271,12 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
} }
//Forcing one player to be a pirate. //Forcing one player to be a pirate.
public void MakePirate(EntityUid mindId, MindComponent mind) public void MakePirate(EntityUid entity)
{ {
if (!mind.OwnedEntity.HasValue) if (!_mindSystem.TryGetMind(entity, out var mindId, out var mind))
return; return;
SetOutfitCommand.SetOutfit(mind.OwnedEntity.Value, GearId, EntityManager);
SetOutfitCommand.SetOutfit(entity, GearId, EntityManager);
var pirateRule = EntityQuery<PiratesRuleComponent>().FirstOrDefault(); var pirateRule = EntityQuery<PiratesRuleComponent>().FirstOrDefault();
if (pirateRule == null) if (pirateRule == null)

View File

@@ -1,7 +1,5 @@
using System.Linq;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Antag; using Content.Server.Antag;
using Content.Server.Chat.Managers;
using Content.Server.EUI; using Content.Server.EUI;
using Content.Server.Flash; using Content.Server.Flash;
using Content.Server.GameTicking.Rules.Components; using Content.Server.GameTicking.Rules.Components;
@@ -13,10 +11,12 @@ using Content.Server.Revolutionary;
using Content.Server.Revolutionary.Components; using Content.Server.Revolutionary.Components;
using Content.Server.Roles; using Content.Server.Roles;
using Content.Server.RoundEnd; using Content.Server.RoundEnd;
using Content.Shared.Chat; using Content.Server.Shuttles.Systems;
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;
@@ -27,9 +27,9 @@ using Content.Shared.Revolutionary.Components;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Stunnable; using Content.Shared.Stunnable;
using Content.Shared.Zombies; using Content.Shared.Zombies;
using Robust.Server.Audio; using Robust.Shared.Prototypes;
using Robust.Server.GameObjects;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using System.Linq;
namespace Content.Server.GameTicking.Rules; namespace Content.Server.GameTicking.Rules;
@@ -39,7 +39,6 @@ namespace Content.Server.GameTicking.Rules;
public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleComponent> public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleComponent>
{ {
[Dependency] private readonly IAdminLogManager _adminLogManager = default!; [Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!; [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
[Dependency] private readonly EuiManager _euiMan = default!; [Dependency] private readonly EuiManager _euiMan = default!;
@@ -50,12 +49,13 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
[Dependency] private readonly RoleSystem _role = default!; [Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly SharedStunSystem _stun = default!; [Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!; [Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly AudioSystem _audioSystem = default!; [Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[ValidatePrototypeId<NpcFactionPrototype>] //Used in OnPostFlash, no reference to the rule component is available
public const string RevolutionaryNpcFaction = "Revolutionary"; public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
[ValidatePrototypeId<AntagPrototype>] public readonly ProtoId<NpcFactionPrototype> RevPrototypeId = "Rev";
public const string RevolutionaryAntagRole = "Rev";
public override void Initialize() public override void Initialize()
{ {
@@ -69,15 +69,20 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
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);
component.CommandCheck = _timing.CurTime + component.TimerWait; component.CommandCheck = _timing.CurTime + component.TimerWait;
} }
/// <summary>
/// Checks if the round should end and also checks who has a mindshield.
/// </summary>
protected override void ActiveTick(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, float frameTime) protected override void ActiveTick(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, float frameTime)
{ {
base.ActiveTick(uid, component, gameRule, frameTime); base.ActiveTick(uid, component, gameRule, frameTime);
@@ -139,63 +144,63 @@ 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) private void OnStartAttempt(RoundStartAttemptEvent ev)
{ {
var query = AllEntityQuery<RevolutionaryRuleComponent, GameRuleComponent>(); TryRoundStartAttempt(ev, Loc.GetString("roles-antag-rev-name"));
while (query.MoveNext(out var uid, out var comp, out var gameRule))
{
_antagSelection.AttemptStartGameRule(ev, uid, comp.MinPlayers, gameRule);
}
} }
private void OnPlayerJobAssigned(RulePlayerJobsAssignedEvent ev) private void OnPlayerJobAssigned(RulePlayerJobsAssignedEvent ev)
{ {
var query = QueryActiveRules(); var query = QueryActiveRules();
while (query.MoveNext(out _, out var comp, out _)) while (query.MoveNext(out var uid, out var activeGameRule, out var comp, out var gameRule))
{ {
_antagSelection.EligiblePlayers(comp.HeadRevPrototypeId, comp.MaxHeadRevs, comp.PlayersPerHeadRev, comp.HeadRevStartSound, var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.HeadRevPrototypeId);
"head-rev-role-greeting", "#5e9cff", out var chosen);
if (chosen.Any()) if (eligiblePlayers.Count == 0)
GiveHeadRev(chosen, comp.HeadRevPrototypeId, comp); continue;
else
{ var headRevCount = _antagSelection.CalculateAntagCount(ev.Players.Length, comp.PlayersPerHeadRev, comp.MaxHeadRevs);
_chatManager.SendAdminAnnouncement(Loc.GetString("rev-no-heads"));
} var headRevs = _antagSelection.ChooseAntags(headRevCount, eligiblePlayers);
GiveHeadRev(headRevs, comp.HeadRevPrototypeId, comp);
} }
} }
private void GiveHeadRev(List<EntityUid> chosen, string antagProto, RevolutionaryRuleComponent comp) private void GiveHeadRev(IEnumerable<EntityUid> chosen, ProtoId<AntagPrototype> antagProto, RevolutionaryRuleComponent comp)
{ {
foreach (var headRev in chosen) 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))
{ {
RemComp<CommandStaffComponent>(headRev); _role.MindAddRole(mind, new RevolutionaryRoleComponent { PrototypeId = antagProto }, silent: true);
var inCharacterName = MetaData(headRev).EntityName;
if (_mind.TryGetMind(headRev, out var mindId, out var mind))
{
if (!_role.MindHasRole<RevolutionaryRoleComponent>(mindId))
{
_role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = antagProto });
}
if (mind.Session != null)
{
comp.HeadRevs.Add(inCharacterName, mindId);
}
}
_antagSelection.GiveAntagBagGear(headRev, comp.StartingGear);
EnsureComp<RevolutionaryComponent>(headRev);
EnsureComp<HeadRevolutionaryComponent>(headRev);
} }
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>
public void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref AfterFlashedEvent ev) private void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref AfterFlashedEvent ev)
{ {
TryComp<AlwaysRevolutionaryConvertibleComponent>(ev.Target, out var alwaysConvertibleComp); var alwaysConvertible = HasComp<AlwaysRevolutionaryConvertibleComponent>(ev.Target);
var alwaysConvertible = alwaysConvertibleComp != null;
if (!_mind.TryGetMind(ev.Target, out var mindId, out var mind) && !alwaysConvertible) if (!_mind.TryGetMind(ev.Target, out var mindId, out var mind) && !alwaysConvertible)
return; return;
@@ -211,8 +216,9 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
} }
_npcFaction.AddFaction(ev.Target, RevolutionaryNpcFaction); _npcFaction.AddFaction(ev.Target, RevolutionaryNpcFaction);
EnsureComp<RevolutionaryComponent>(ev.Target); var revComp = EnsureComp<RevolutionaryComponent>(ev.Target);
_stun.TryParalyze(ev.Target, comp.StunTime, true); _stun.TryParalyze(ev.Target, comp.StunTime, true);
if (ev.User != null) if (ev.User != null)
{ {
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary"); _adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
@@ -223,20 +229,16 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
if (mindId == default || !_role.MindHasRole<RevolutionaryRoleComponent>(mindId)) if (mindId == default || !_role.MindHasRole<RevolutionaryRoleComponent>(mindId))
{ {
_role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = RevolutionaryAntagRole }); _role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = RevPrototypeId });
} }
if (mind?.Session != null) if (mind?.Session != null)
{ _antagSelection.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound);
var message = Loc.GetString("rev-role-greeting");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, mind.Session.Channel, Color.Red);
_audioSystem.PlayGlobal("/Audio/Ambience/Antag/headrev_start.ogg", ev.Target);
}
} }
public void OnHeadRevAdmin(EntityUid mindId, MindComponent? mind = null) public void OnHeadRevAdmin(EntityUid entity)
{ {
if (!Resolve(mindId, ref mind)) if (HasComp<HeadRevolutionaryComponent>(entity))
return; return;
var revRule = EntityQuery<RevolutionaryRuleComponent>().FirstOrDefault(); var revRule = EntityQuery<RevolutionaryRuleComponent>().FirstOrDefault();
@@ -246,24 +248,10 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
revRule = Comp<RevolutionaryRuleComponent>(ruleEnt); revRule = Comp<RevolutionaryRuleComponent>(ruleEnt);
} }
if (!HasComp<HeadRevolutionaryComponent>(mind.OwnedEntity)) GiveHeadRev(entity, revRule.HeadRevPrototypeId, revRule);
{
if (mind.OwnedEntity != null)
{
var player = new List<EntityUid>
{
mind.OwnedEntity.Value
};
GiveHeadRev(player, RevolutionaryAntagRole, revRule);
}
if (mind.Session != null)
{
var message = Loc.GetString("head-rev-role-greeting");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, mind.Session.Channel, Color.FromHex("#5e9cff"));
}
}
} }
//TODO: Enemies of the revolution
private void OnCommandMobStateChanged(EntityUid uid, CommandStaffComponent comp, MobStateChangedEvent ev) private void OnCommandMobStateChanged(EntityUid uid, CommandStaffComponent comp, MobStateChangedEvent ev)
{ {
if (ev.NewMobState == MobState.Dead || ev.NewMobState == MobState.Invalid) if (ev.NewMobState == MobState.Dead || ev.NewMobState == MobState.Invalid)
@@ -283,7 +271,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
commandList.Add(id); commandList.Add(id);
} }
return _antagSelection.IsGroupDead(commandList, true); return IsGroupDead(commandList, true);
} }
private void OnHeadRevMobStateChanged(EntityUid uid, HeadRevolutionaryComponent comp, MobStateChangedEvent ev) private void OnHeadRevMobStateChanged(EntityUid uid, HeadRevolutionaryComponent comp, MobStateChangedEvent ev)
@@ -307,7 +295,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
} }
// If no Head Revs are alive all normal Revs will lose their Rev status and rejoin Nanotrasen // If no Head Revs are alive all normal Revs will lose their Rev status and rejoin Nanotrasen
if (_antagSelection.IsGroupDead(headRevList, false)) if (IsGroupDead(headRevList, false))
{ {
var rev = AllEntityQuery<RevolutionaryComponent, MindContainerComponent>(); var rev = AllEntityQuery<RevolutionaryComponent, MindContainerComponent>();
while (rev.MoveNext(out var uid, out _, out var mc)) while (rev.MoveNext(out var uid, out _, out var mc))
@@ -338,6 +326,38 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
return false; return false;
} }
/// <summary>
/// Will take a group of entities and check if they are all alive or dead
/// </summary>
/// <param name="list">The list of the entities</param>
/// <param name="checkOffStation">Bool for if you want to check if someone is in space and consider them dead. (Won't check when emergency shuttle arrives just in case)</param>
/// <returns></returns>
private bool IsGroupDead(List<EntityUid> list, bool checkOffStation)
{
var dead = 0;
foreach (var entity in list)
{
if (TryComp<MobStateComponent>(entity, out var state))
{
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
{
dead++;
}
else if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
{
dead++;
}
}
//If they don't have the MobStateComponent they might as well be dead.
else
{
dead++;
}
}
return dead == list.Count || list.Count == 0;
}
private static readonly string[] Outcomes = private static readonly string[] Outcomes =
{ {
// revs survived and heads survived... how // revs survived and heads survived... how

View File

@@ -1,42 +1,29 @@
using Content.Server.Chat.Managers; using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components; 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.Inventory;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.Objectives.Components; using Content.Shared.Objectives.Components;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Player;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Prototypes;
using System.Linq; using System.Linq;
using Content.Shared.Humanoid;
using Content.Server.Antag;
using Robust.Server.Audio;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Random;
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 IChatManager _chatManager = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!; [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
[Dependency] private readonly AudioSystem _audio = 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 ObjectivesSystem _objectives = default!; [Dependency] private readonly ObjectivesSystem _objectives = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[ValidatePrototypeId<WeightedRandomPrototype>]
const string BigObjectiveGroup = "ThiefBigObjectiveGroups";
[ValidatePrototypeId<WeightedRandomPrototype>]
const string SmallObjectiveGroup = "ThiefObjectiveGroups";
[ValidatePrototypeId<WeightedRandomPrototype>]
const string EscapeObjectiveGroup = "ThiefEscapeObjectiveGroups";
private const float BigObjectiveChance = 0.7f;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -49,99 +36,95 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev) private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
{ {
var query = EntityQueryEnumerator<ThiefRuleComponent, GameRuleComponent>(); var query = QueryActiveRules();
while (query.MoveNext(out var uid, out var thief, out var gameRule)) while (query.MoveNext(out _, out var comp, out _))
{ {
//Chance to not lauch gamerule //Chance to not launch the game rule
if (_random.Prob(thief.RuleChance)) if (!_random.Prob(comp.RuleChance))
{ continue;
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
foreach (var player in ev.Players) //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 >:)
if (!ev.Profiles.TryGetValue(player.UserId, out var profile)) var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.ThiefPrototypeId, acceptableAntags: AntagAcceptability.All, allowNonHumanoids: true);
continue;
thief.StartCandidates[player] = profile; //Abort if there are none
} if (eligiblePlayers.Count == 0)
DoThiefStart(thief); 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);
} }
} }
private void DoThiefStart(ThiefRuleComponent component) public void MakeThief(List<EntityUid> players, ThiefRuleComponent thiefRule, bool addPacified)
{ {
if (!component.StartCandidates.Any()) foreach (var thief in players)
{ {
Log.Error("There are no players who can become thieves."); MakeThief(thief, thiefRule, addPacified);
return;
}
var startThiefCount = Math.Min(component.MaxAllowThief, component.StartCandidates.Count);
var thiefPool = _antagSelection.FindPotentialAntags(component.StartCandidates, component.ThiefPrototypeId);
//TO DO: When voxes specifies are added, increase their chance of becoming a thief by 4 times >:)
//Add 1, as Next() is exclusive of maxValue
var numberOfThievesToSelect = _random.Next(1, startThiefCount + 1);
//While we dont have the correct number of thieves, and there are potential thieves remaining
while (component.ThievesMinds.Count < numberOfThievesToSelect && thiefPool.Count > 0)
{
Log.Info($"{numberOfThievesToSelect} thieves required, {component.ThievesMinds.Count} currently chosen, {thiefPool.Count} potentials");
var selectedThieves = _antagSelection.PickAntag(numberOfThievesToSelect - component.ThievesMinds.Count, thiefPool);
foreach (var thief in selectedThieves)
{
MakeThief(component, thief, component.PacifistThieves);
}
} }
} }
public bool MakeThief(ThiefRuleComponent thiefRule, ICommonSession thief, bool addPacified) public void MakeThief(EntityUid thief, ThiefRuleComponent thiefRule, bool addPacified)
{ {
//checks
if (!_mindSystem.TryGetMind(thief, out var mindId, out var mind)) if (!_mindSystem.TryGetMind(thief, out var mindId, out var mind))
{ return;
Log.Info("Failed getting mind for picked thief.");
return false;
}
if (HasComp<ThiefRoleComponent>(mindId)) if (HasComp<ThiefRoleComponent>(mindId))
{ return;
Log.Error($"Player {thief.Name} is already a thief.");
return false;
}
if (mind.OwnedEntity is not { } entity)
{
Log.Error("Mind picked for thief did not have an attached entity.");
return false;
}
// Assign thief roles // Assign thief roles
_roleSystem.MindAddRole(mindId, new ThiefRoleComponent _roleSystem.MindAddRole(mindId, new ThiefRoleComponent
{ {
PrototypeId = thiefRule.ThiefPrototypeId PrototypeId = thiefRule.ThiefPrototypeId,
}); }, silent: true);
//Add Pacified //Add Pacified
//To Do: Long-term this should just be using the antag code to add components. //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. if (addPacified) //This check is important because some servers may want to disable the thief's pacifism. Do not remove.
{ {
EnsureComp<PacifiedComponent>(mind.OwnedEntity.Value); EnsureComp<PacifiedComponent>(thief);
} }
// Notificate player about new role assignment //Generate objectives
if (_mindSystem.TryGetSession(mindId, out var session)) GenerateObjectives(mindId, mind, thiefRule);
//Send briefing here to account for humanoid/animal
_antagSelection.SendBriefing(thief, MakeBriefing(thief), null, thiefRule.GreetingSound);
// Give starting items
_inventory.SpawnItemsOnEntity(thief, thiefRule.StarterItems);
thiefRule.ThievesMinds.Add(mindId);
}
public void AdminMakeThief(EntityUid entity, bool addPacified)
{
var thiefRule = EntityQuery<ThiefRuleComponent>().FirstOrDefault();
if (thiefRule == null)
{ {
_audio.PlayGlobal(thiefRule.GreetingSound, session); GameTicker.StartGameRule("Thief", out var ruleEntity);
_chatManager.DispatchServerMessage(session, MakeBriefing(mind.OwnedEntity.Value)); thiefRule = Comp<ThiefRuleComponent>(ruleEntity);
} }
if (HasComp<ThiefRoleComponent>(entity))
return;
MakeThief(entity, thiefRule, addPacified);
}
private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule)
{
// Give thieves their objectives // Give thieves their objectives
var difficulty = 0f; var difficulty = 0f;
if (_random.Prob(BigObjectiveChance)) // 70% chance to 1 big objective (structure or animal) if (_random.Prob(thiefRule.BigObjectiveChance)) // 70% chance to 1 big objective (structure or animal)
{ {
var objective = _objectives.GetRandomObjective(mindId, mind, BigObjectiveGroup); var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.BigObjectiveGroup);
if (objective != null) if (objective != null)
{ {
_mindSystem.AddObjective(mindId, mind, objective.Value); _mindSystem.AddObjective(mindId, mind, objective.Value);
@@ -151,7 +134,7 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
for (var i = 0; i < thiefRule.MaxStealObjectives && thiefRule.MaxObjectiveDifficulty > difficulty; i++) // Many small objectives for (var i = 0; i < thiefRule.MaxStealObjectives && thiefRule.MaxObjectiveDifficulty > difficulty; i++) // Many small objectives
{ {
var objective = _objectives.GetRandomObjective(mindId, mind, SmallObjectiveGroup); var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.SmallObjectiveGroup);
if (objective == null) if (objective == null)
continue; continue;
@@ -160,27 +143,9 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
} }
//Escape target //Escape target
var escapeObjective = _objectives.GetRandomObjective(mindId, mind, EscapeObjectiveGroup); var escapeObjective = _objectives.GetRandomObjective(mindId, mind, thiefRule.EscapeObjectiveGroup);
if (escapeObjective != null) if (escapeObjective != null)
_mindSystem.AddObjective(mindId, mind, escapeObjective.Value); _mindSystem.AddObjective(mindId, mind, escapeObjective.Value);
// Give starting items
_antagSelection.GiveAntagBagGear(mind.OwnedEntity.Value, thiefRule.StarterItems);
thiefRule.ThievesMinds.Add(mindId);
return true;
}
public void AdminMakeThief(ICommonSession thief, bool addPacified)
{
var thiefRule = EntityQuery<ThiefRuleComponent>().FirstOrDefault();
if (thiefRule == null)
{
GameTicker.StartGameRule("Thief", out var ruleEntity);
thiefRule = Comp<ThiefRuleComponent>(ruleEntity);
}
MakeThief(thiefRule, thief, addPacified);
} }
//Add mind briefing //Add mind briefing

View File

@@ -1,13 +1,10 @@
using System.Linq;
using Content.Server.Antag; using Content.Server.Antag;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components; using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind; using Content.Server.Mind;
using Content.Server.NPC.Systems; using Content.Server.NPC.Systems;
using Content.Server.Objectives; 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.Shuttles.Components;
using Content.Server.Traitor.Uplink; using Content.Server.Traitor.Uplink;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Dataset; using Content.Shared.Dataset;
@@ -15,17 +12,15 @@ using Content.Shared.Mind;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.Objectives.Components; using Content.Shared.Objectives.Components;
using Content.Shared.PDA; using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Roles.Jobs; using Content.Shared.Roles.Jobs;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using System.Linq;
using System.Text;
namespace Content.Server.GameTicking.Rules; namespace Content.Server.GameTicking.Rules;
@@ -35,16 +30,15 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
[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 IConfigurationManager _cfg = default!;
[Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!; [Dependency] private readonly UplinkSystem _uplink = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = 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); private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors); private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors);
@@ -61,46 +55,45 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
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)
{
base.Added(uid, component, gameRule, args);
gameRule.MinPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
}
protected override void Started(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
MakeCodewords(component);
}
protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime) protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime)
{ {
base.ActiveTick(uid, component, gameRule, frameTime); base.ActiveTick(uid, component, gameRule, frameTime);
if (component.SelectionStatus == TraitorRuleComponent.SelectionState.ReadyToSelect && _gameTiming.CurTime > component.AnnounceAt) if (component.SelectionStatus < TraitorRuleComponent.SelectionState.Started && component.AnnounceAt < _timing.CurTime)
{
DoTraitorStart(component); DoTraitorStart(component);
component.SelectionStatus = TraitorRuleComponent.SelectionState.Started;
}
} }
/// <summary>
/// Check for enough players
/// </summary>
/// <param name="ev"></param>
private void OnStartAttempt(RoundStartAttemptEvent ev) private void OnStartAttempt(RoundStartAttemptEvent ev)
{ {
var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>(); TryRoundStartAttempt(ev, Loc.GetString("traitor-title"));
while (query.MoveNext(out var uid, out var traitor, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
MakeCodewords(traitor);
var minPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
_chatManager.SendAdminAnnouncement(Loc.GetString("traitor-not-enough-ready-players",
("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
ev.Cancel();
continue;
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready"));
ev.Cancel();
}
}
} }
private void MakeCodewords(TraitorRuleComponent component) private void MakeCodewords(TraitorRuleComponent component)
{ {
var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount); var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
var adjectives = _prototypeManager.Index<DatasetPrototype>("adjectives").Values; var adjectives = _prototypeManager.Index<DatasetPrototype>(component.CodewordAdjectives).Values;
var verbs = _prototypeManager.Index<DatasetPrototype>("verbs").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(codewordCount, codewordPool.Count);
component.Codewords = new string[finalCodewordCount]; component.Codewords = new string[finalCodewordCount];
@@ -112,125 +105,99 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
private void DoTraitorStart(TraitorRuleComponent component) private void DoTraitorStart(TraitorRuleComponent component)
{ {
if (!component.StartCandidates.Any()) var eligiblePlayers = _antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.TraitorPrototypeId);
{
Log.Error("Tried to start Traitor mode without any candidates."); if (eligiblePlayers.Count == 0)
return; return;
}
var numTraitors = MathHelper.Clamp(component.StartCandidates.Count / PlayersPerTraitor, 1, MaxTraitors); var traitorsToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, PlayersPerTraitor, MaxTraitors);
var traitorPool = _antagSelection.FindPotentialAntags(component.StartCandidates, component.TraitorPrototypeId);
var selectedTraitors = _antagSelection.PickAntag(numTraitors, traitorPool);
foreach (var traitor in selectedTraitors) var selectedTraitors = _antagSelection.ChooseAntags(traitorsToSelect, eligiblePlayers);
{
MakeTraitor(traitor);
}
component.SelectionStatus = TraitorRuleComponent.SelectionState.SelectionMade; MakeTraitor(selectedTraitors, component);
} }
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev) private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
{ {
var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>(); //Start the timer
while (query.MoveNext(out var uid, out var traitor, out var gameRule)) var query = QueryActiveRules();
while (query.MoveNext(out _, out var comp, out var gameRuleComponent))
{ {
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
foreach (var player in ev.Players)
{
if (!ev.Profiles.ContainsKey(player.UserId))
continue;
traitor.StartCandidates[player] = ev.Profiles[player.UserId];
}
var delay = TimeSpan.FromSeconds( var delay = TimeSpan.FromSeconds(
_cfg.GetCVar(CCVars.TraitorStartDelay) + _cfg.GetCVar(CCVars.TraitorStartDelay) +
_random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance))); _random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance)));
traitor.AnnounceAt = _gameTiming.CurTime + delay; //Set the delay for choosing traitors
comp.AnnounceAt = _timing.CurTime + delay;
traitor.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToSelect; comp.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToStart;
} }
} }
public bool MakeTraitor(ICommonSession traitor, bool giveUplink = true, bool giveObjectives = true) public bool MakeTraitor(List<EntityUid> traitors, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
{ {
var traitorRule = EntityQuery<TraitorRuleComponent>().FirstOrDefault(); foreach (var traitor in traitors)
if (traitorRule == null)
{ {
//todo fuck me this shit is awful MakeTraitor(traitor, component, giveUplink, giveObjectives);
//no i wont fuck you, erp is against rules
GameTicker.StartGameRule("Traitor", out var ruleEntity);
traitorRule = Comp<TraitorRuleComponent>(ruleEntity);
MakeCodewords(traitorRule);
} }
return true;
}
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
{
//Grab the mind if it wasnt provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind)) if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
{
Log.Info("Failed getting mind for picked traitor.");
return false; return false;
}
if (HasComp<TraitorRoleComponent>(mindId)) if (HasComp<TraitorRoleComponent>(mindId))
{ {
Log.Error($"Player {traitor.Name} is already a traitor."); Log.Error($"Player {mind.CharacterName} is already a traitor.");
return false; return false;
} }
if (mind.OwnedEntity is not { } entity) var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
{
Log.Error("Mind picked for traitor did not have an attached entity.");
return false;
}
// Calculate the amount of currency on the uplink.
var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance);
if (_jobs.MindTryGetJob(mindId, out _, out var prototype))
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
// Give traitors their codewords and uplink code to keep in their character info menu
var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", traitorRule.Codewords)));
Note[]? code = null; Note[]? code = null;
if (giveUplink) if (giveUplink)
{ {
// Calculate the amount of currency on the uplink.
var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance);
if (_jobs.MindTryGetJob(mindId, out _, out var prototype))
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
// creadth: we need to create uplink for the antag. // creadth: we need to create uplink for the antag.
// PDA should be in place already // PDA should be in place already
var pda = _uplink.FindUplinkTarget(mind.OwnedEntity!.Value); var pda = _uplink.FindUplinkTarget(traitor);
if (pda == null || !_uplink.AddUplink(mind.OwnedEntity.Value, startingBalance)) if (pda == null || !_uplink.AddUplink(traitor, startingBalance))
return false; return false;
// Give traitors their codewords and uplink code to keep in their character info menu // Give traitors their codewords and uplink code to keep in their character info menu
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code; code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
// If giveUplink is false the uplink code part is omitted // If giveUplink is false the uplink code part is omitted
briefing = string.Format("{0}\n{1}", briefing, briefing = string.Format("{0}\n{1}", briefing,
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", "#"))));
} }
// Prepare traitor role _antagSelection.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification);
var traitorRole = new TraitorRoleComponent
{ component.TraitorMinds.Add(mindId);
PrototypeId = traitorRule.TraitorPrototypeId,
};
// Assign traitor roles // Assign traitor roles
_roleSystem.MindAddRole(mindId, new TraitorRoleComponent _roleSystem.MindAddRole(mindId, new TraitorRoleComponent
{ {
PrototypeId = traitorRule.TraitorPrototypeId PrototypeId = component.TraitorPrototypeId
}, mind); }, mind, true);
// Assign briefing and greeting sound // Assign briefing
_roleSystem.MindAddRole(mindId, new RoleBriefingComponent _roleSystem.MindAddRole(mindId, new RoleBriefingComponent
{ {
Briefing = briefing Briefing = briefing.ToString()
}, mind); }, mind, true);
_roleSystem.MindPlaySound(mindId, traitorRule.GreetSoundNotification, mind);
SendTraitorBriefing(mindId, traitorRule.Codewords, code);
traitorRule.TraitorMinds.Add(mindId);
// Change the faction // Change the faction
_npcFaction.RemoveFaction(entity, "NanoTrasen", false); _npcFaction.RemoveFaction(traitor, component.NanoTrasenFaction, false);
_npcFaction.AddFaction(entity, "Syndicate"); _npcFaction.AddFaction(traitor, component.SyndicateFaction);
// Give traitors their objectives // Give traitors their objectives
if (giveObjectives) if (giveObjectives)
@@ -241,7 +208,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
Log.Debug($"Attempting {maxPicks} objective picks with {maxDifficulty} difficulty"); Log.Debug($"Attempting {maxPicks} objective picks with {maxDifficulty} difficulty");
for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++) for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++)
{ {
var objective = _objectives.GetRandomObjective(mindId, mind, "TraitorObjectiveGroups"); var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup);
if (objective == null) if (objective == null)
continue; continue;
@@ -255,54 +222,26 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
return true; return true;
} }
/// <summary>
/// Send a codewords and uplink codes to traitor chat.
/// </summary>
/// <param name="mind">A mind (player)</param>
/// <param name="codewords">Codewords</param>
/// <param name="code">Uplink codes</param>
private void SendTraitorBriefing(EntityUid mind, string[] codewords, Note[]? code)
{
if (!_mindSystem.TryGetSession(mind, out var session))
return;
_chatManager.DispatchServerMessage(session, Loc.GetString("traitor-role-greeting"));
_chatManager.DispatchServerMessage(session, Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords))));
if (code != null)
_chatManager.DispatchServerMessage(session, Loc.GetString("traitor-role-uplink-code", ("code", string.Join("-", code).Replace("sharp","#"))));
}
private void HandleLatejoin(PlayerSpawnCompleteEvent ev) private void HandleLatejoin(PlayerSpawnCompleteEvent ev)
{ {
var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>(); var query = QueryActiveRules();
while (query.MoveNext(out var uid, out var traitor, out var gameRule)) while (query.MoveNext(out _, out var comp, out _))
{ {
if (!GameTicker.IsGameRuleAdded(uid, gameRule)) if (comp.TotalTraitors >= MaxTraitors)
continue; continue;
if (traitor.TotalTraitors >= MaxTraitors)
continue;
if (!ev.LateJoin) if (!ev.LateJoin)
continue; continue;
if (!ev.Profile.AntagPreferences.Contains(traitor.TraitorPrototypeId))
if (!_antagSelection.IsPlayerEligible(ev.Player, comp.TraitorPrototypeId))
continue; continue;
if (ev.JobId == null || !_prototypeManager.TryIndex<JobPrototype>(ev.JobId, out var job)) //If its before we have selected traitors, continue
if (comp.SelectionStatus < TraitorRuleComponent.SelectionState.Started)
continue; continue;
if (!job.CanBeAntag)
continue;
// Before the announcement is made, late-joiners are considered the same as players who readied.
if (traitor.SelectionStatus < TraitorRuleComponent.SelectionState.SelectionMade)
{
traitor.StartCandidates[ev.Player] = ev.Profile;
continue;
}
// the nth player we adjust our probabilities around // the nth player we adjust our probabilities around
var target = PlayersPerTraitor * traitor.TotalTraitors + 1; var target = PlayersPerTraitor * comp.TotalTraitors + 1;
var chance = 1f / PlayersPerTraitor; var chance = 1f / PlayersPerTraitor;
// If we have too many traitors, divide by how many players below target for next traitor we are. // If we have too many traitors, divide by how many players below target for next traitor we are.
@@ -322,7 +261,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
// You get one shot. // You get one shot.
if (_random.Prob(chance)) if (_random.Prob(chance))
{ {
MakeTraitor(ev.Player); MakeTraitor(ev.Mob, comp);
} }
} }
} }
@@ -338,6 +277,38 @@ 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)
{
var sb = new StringBuilder();
sb.AppendLine(Loc.GetString("traitor-role-greeting"));
sb.AppendLine(Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", codewords))));
if (uplinkCode != null)
sb.AppendLine(Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", uplinkCode).Replace("sharp", "#"))));
return sb.ToString();
}
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();

View File

@@ -1,13 +1,9 @@
using System.Globalization;
using System.Linq;
using Content.Server.Actions; using Content.Server.Actions;
using Content.Server.Chat.Managers; 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.Preferences.Managers;
using Content.Server.Roles; using Content.Server.Roles;
using Content.Server.Roles.Jobs;
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;
@@ -18,17 +14,14 @@ 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.Preferences;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Zombies; using Content.Shared.Zombies;
using Robust.Server.GameObjects;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using System.Globalization;
namespace Content.Server.GameTicking.Rules; namespace Content.Server.GameTicking.Rules;
@@ -36,10 +29,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
{ {
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = 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!;
@@ -49,8 +39,8 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
[Dependency] private readonly SharedMindSystem _mindSystem = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!; [Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly StationSystem _station = default!; [Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
[Dependency] private readonly JobSystem _jobs = default!; [Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -61,6 +51,16 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
SubscribeLocalEvent<PendingZombieComponent, ZombifySelfActionEvent>(OnZombifySelf); SubscribeLocalEvent<PendingZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
} }
/// <summary>
/// Set the required minimum players for this gamemode to start
/// </summary>
protected override void Added(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, component, gameRule, args);
gameRule.MinPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
}
private void OnRoundEndText(RoundEndTextAppendEvent ev) private void OnRoundEndText(RoundEndTextAppendEvent ev)
{ {
foreach (var zombie in EntityQuery<ZombieRuleComponent>()) foreach (var zombie in EntityQuery<ZombieRuleComponent>())
@@ -113,85 +113,59 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
/// <summary> /// <summary>
/// The big kahoona function for checking if the round is gonna end /// The big kahoona function for checking if the round is gonna end
/// </summary> /// </summary>
private void CheckRoundEnd() private void CheckRoundEnd(ZombieRuleComponent zombieRuleComponent)
{ {
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>(); var healthy = GetHealthyHumans();
while (query.MoveNext(out var uid, out var comp, out var gameRule)) if (healthy.Count == 1) // Only one human left. spooky
_popup.PopupEntity(Loc.GetString("zombie-alone"), healthy[0], healthy[0]);
if (GetInfectedFraction(false) > zombieRuleComponent.ZombieShuttleCallPercentage && !_roundEnd.IsRoundEndRequested())
{ {
if (!GameTicker.IsGameRuleActive(uid, gameRule)) foreach (var station in _station.GetStations())
continue;
var healthy = GetHealthyHumans();
if (healthy.Count == 1) // Only one human left. spooky
_popup.PopupEntity(Loc.GetString("zombie-alone"), healthy[0], healthy[0]);
if (!comp.ShuttleCalled && GetInfectedFraction(false) >= comp.ZombieShuttleCallPercentage)
{ {
comp.ShuttleCalled = true; _chat.DispatchStationAnnouncement(station, Loc.GetString("zombie-shuttle-call"), colorOverride: Color.Crimson);
foreach (var station in _station.GetStations())
{
_chat.DispatchStationAnnouncement(station, Loc.GetString("zombie-shuttle-call"), colorOverride: Color.Crimson);
}
_roundEnd.RequestRoundEnd(null, false);
} }
_roundEnd.RequestRoundEnd(null, false);
// we include dead for this count because we don't want to end the round
// when everyone gets on the shuttle.
if (GetInfectedFraction() >= 1) // Oops, all zombies
_roundEnd.EndRound();
} }
// we include dead for this count because we don't want to end the round
// when everyone gets on the shuttle.
if (GetInfectedFraction() >= 1) // Oops, all zombies
_roundEnd.EndRound();
} }
/// <summary>
/// Check we have enough players to start this game mode, if not - cancel and announce
/// </summary>
private void OnStartAttempt(RoundStartAttemptEvent ev) private void OnStartAttempt(RoundStartAttemptEvent ev)
{ {
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>(); TryRoundStartAttempt(ev, Loc.GetString("zombie-title"));
while (query.MoveNext(out var uid, out _, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
var minPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
_chatManager.SendAdminAnnouncement(Loc.GetString("zombie-not-enough-ready-players",
("readyPlayersCount", ev.Players.Length),
("minimumPlayers", minPlayers)));
ev.Cancel();
continue;
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-no-one-ready"));
ev.Cancel();
}
}
} }
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);
component.StartTime = _timing.CurTime + _random.Next(component.MinStartDelay, component.MaxStartDelay);
var delay = _random.Next(component.MinStartDelay, component.MaxStartDelay);
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.InfectedChosen) if (component.StartTime.HasValue && component.StartTime < _timing.CurTime)
{ {
if (_timing.CurTime >= component.NextRoundEndCheck) InfectInitialPlayers(component);
{ component.StartTime = null;
component.NextRoundEndCheck += component.EndCheckDelay; component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
CheckRoundEnd();
}
return;
} }
if (component.StartTime == null || _timing.CurTime < component.StartTime) if (component.NextRoundEndCheck.HasValue && component.NextRoundEndCheck < _timing.CurTime)
return; {
CheckRoundEnd(component);
InfectInitialPlayers(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)
@@ -201,6 +175,12 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
Del(component.Action.Value); Del(component.Action.Value);
} }
/// <summary>
/// Get the fraction of players that are infected, between 0 and 1
/// </summary>
/// <param name="includeOffStation">Include healthy players that are not on the station grid</param>
/// <param name="includeDead">Should dead zombies be included in the count</param>
/// <returns></returns>
private float GetInfectedFraction(bool includeOffStation = true, bool includeDead = false) private float GetInfectedFraction(bool includeOffStation = true, bool includeDead = false)
{ {
var players = GetHealthyHumans(includeOffStation); var players = GetHealthyHumans(includeOffStation);
@@ -264,87 +244,55 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
/// </remarks> /// </remarks>
private void InfectInitialPlayers(ZombieRuleComponent component) private void InfectInitialPlayers(ZombieRuleComponent component)
{ {
if (component.InfectedChosen) //Get all players with initial infected enabled, and exclude those with the ZombieImmuneComponent
return; var eligiblePlayers = _antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.PatientZeroPrototypeId, includeAllJobs: true, customExcludeCondition: x => HasComp<ZombieImmuneComponent>(x) || HasComp<InitialInfectedExemptComponent>(x));
component.InfectedChosen = true; //And get all players, excluding ZombieImmune - to fill any leftover initial infected slots
var allPlayers = _antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.PatientZeroPrototypeId, acceptableAntags: Shared.Antag.AntagAcceptability.All, includeAllJobs: true, ignorePreferences: true, customExcludeCondition: HasComp<ZombieImmuneComponent>);
var allPlayers = _playerManager.Sessions.ToList(); //If there are no players to choose, abort
var playerList = new List<ICommonSession>(); if (allPlayers.Count == 0)
var prefList = new List<ICommonSession>();
foreach (var player in allPlayers)
{
if (player.AttachedEntity == null || !HasComp<HumanoidAppearanceComponent>(player.AttachedEntity) ||
HasComp<ZombieImmuneComponent>(player.AttachedEntity) || !_jobs.CanBeAntag(player))
continue;
if (HasComp<InitialInfectedExemptComponent>(player.AttachedEntity))
continue; // used (for example) on ERT
playerList.Add(player);
var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter;
if (pref.AntagPreferences.Contains(component.PatientZeroPrototypeId))
prefList.Add(player);
}
if (playerList.Count == 0)
return; return;
var numInfected = Math.Max(1, //How many initial infected should we select
(int) Math.Min( var initialInfectedCount = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerInfected, component.MaxInitialInfected);
Math.Floor((double) playerList.Count / component.PlayersPerInfected), component.MaxInitialInfected));
var totalInfected = 0; //Choose the required number of initial infected from the eligible players, making up any shortfall by choosing from all players
while (totalInfected < numInfected) 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)
{ {
ICommonSession zombie; MakeZombie(entity, component);
if (prefList.Count == 0)
{
if (playerList.Count == 0)
{
Log.Info("Insufficient number of players. stopping selection.");
break;
}
zombie = _random.Pick(playerList);
Log.Info("Insufficient preferred patient 0, picking at random.");
}
else
{
zombie = _random.Pick(prefList);
Log.Info("Selected a patient 0.");
}
prefList.Remove(zombie);
playerList.Remove(zombie);
if (!_mindSystem.TryGetMind(zombie, out var mindId, out var mind) ||
mind.OwnedEntity is not { } ownedEntity)
{
continue;
}
totalInfected++;
_roles.MindAddRole(mindId, new InitialInfectedRoleComponent { PrototypeId = component.PatientZeroPrototypeId });
var pending = EnsureComp<PendingZombieComponent>(ownedEntity);
pending.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
EnsureComp<ZombifyOnDeathComponent>(ownedEntity);
EnsureComp<IncurableZombieComponent>(ownedEntity);
var inCharacterName = MetaData(ownedEntity).EntityName;
_action.AddAction(ownedEntity, ref pending.Action, ZombieRuleComponent.ZombifySelfActionPrototype, ownedEntity);
var message = Loc.GetString("zombie-patientzero-role-greeting");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
//gets the names now in case the players leave.
//this gets unhappy if people with the same name get chosen. Probably shouldn't happen.
component.InitialInfectedNames.Add(inCharacterName, zombie.Name);
// I went all the way to ChatManager.cs and all i got was this lousy T-shirt
// You got a free T-shirt!?!?
_chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message,
wrappedMessage, default, false, zombie.Channel, Color.Plum);
_audio.PlayGlobal(component.InitialInfectedSound, ownedEntity);
} }
} }
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);
//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);
}
} }

View File

@@ -1,11 +1,15 @@
using Robust.Shared.Audio; using Content.Server.GameTicking.Rules;
using Content.Shared.NukeOps;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.NukeOps; namespace Content.Server.NukeOps;
/// <summary> /// <summary>
/// Used with NukeOps game rule to send war declaration announcement /// Used with NukeOps game rule to send war declaration announcement
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent, AutoGenerateComponentPause]
[Access(typeof(WarDeclaratorSystem), typeof(NukeopsRuleSystem))]
public sealed partial class WarDeclaratorComponent : Component public sealed partial class WarDeclaratorComponent : Component
{ {
/// <summary> /// <summary>
@@ -23,22 +27,43 @@ public sealed partial class WarDeclaratorComponent : Component
public bool AllowEditingMessage = true; public bool AllowEditingMessage = true;
/// <summary> /// <summary>
/// War declarement text color /// War declaration text color
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField] [DataField]
public Color Color = Color.Red; public Color Color = Color.Red;
/// <summary> /// <summary>
/// War declarement sound file path /// War declaration sound file path
/// </summary> /// </summary>
[DataField] [DataField]
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Announcements/war.ogg"); public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Announcements/war.ogg");
/// <summary> /// <summary>
/// Fluent ID for the declarement title /// Fluent ID for the declaration sender title
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField] [DataField]
public LocId Title = "comms-console-announcement-title-nukie"; public LocId SenderTitle = "comms-console-announcement-title-nukie";
/// <summary>
/// Time allowed for declaration of war
/// </summary>
[DataField]
public float WarDeclarationDelay = 6.0f;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan DisableAt;
/// <summary>
/// How long the shuttle will be disabled for
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan ShuttleDisabledTime;
[DataField]
public WarConditionStatus? CurrentStatus;
} }
[ByRefEvent]
public record struct WarDeclaredEvent(WarConditionStatus? Status, Entity<WarDeclaratorComponent> DeclaratorEntity);

View File

@@ -1,7 +1,7 @@
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.GameTicking.Rules; using Content.Server.Chat.Systems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Shared.Access.Systems;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Database; using Content.Shared.Database;
@@ -9,6 +9,7 @@ using Content.Shared.NukeOps;
using Content.Shared.UserInterface; using Content.Shared.UserInterface;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Timing;
namespace Content.Server.NukeOps; namespace Content.Server.NukeOps;
@@ -17,101 +18,74 @@ namespace Content.Server.NukeOps;
/// </summary> /// </summary>
public sealed class WarDeclaratorSystem : EntitySystem public sealed class WarDeclaratorSystem : EntitySystem
{ {
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly NukeopsRuleSystem _nukeopsRuleSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); SubscribeLocalEvent<WarDeclaratorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<WarDeclaratorComponent, WarDeclaratorActivateMessage>(OnActivated);
SubscribeLocalEvent<WarDeclaratorComponent, ActivatableUIOpenAttemptEvent>(OnAttemptOpenUI); SubscribeLocalEvent<WarDeclaratorComponent, ActivatableUIOpenAttemptEvent>(OnAttemptOpenUI);
SubscribeLocalEvent<WarDeclaratorComponent, WarDeclaratorActivateMessage>(OnActivated);
} }
private void OnAttemptOpenUI(EntityUid uid, WarDeclaratorComponent component, ActivatableUIOpenAttemptEvent args) private void OnMapInit(Entity<WarDeclaratorComponent> ent, ref MapInitEvent args)
{ {
if (!_nukeopsRuleSystem.TryGetRuleFromOperative(args.User, out var comps)) ent.Comp.Message = Loc.GetString("war-declarator-default-message");
ent.Comp.DisableAt = _gameTiming.CurTime + TimeSpan.FromMinutes(ent.Comp.WarDeclarationDelay);
}
private void OnAttemptOpenUI(Entity<WarDeclaratorComponent> ent, ref ActivatableUIOpenAttemptEvent args)
{
if (!_accessReaderSystem.IsAllowed(args.User, ent))
{ {
var msg = Loc.GetString("war-declarator-not-nukeops"); var msg = Loc.GetString("war-declarator-not-working");
_popupSystem.PopupEntity(msg, uid); _popupSystem.PopupEntity(msg, ent);
args.Cancel(); args.Cancel();
return; return;
} }
UpdateUI(uid, comps.Value.Item1, comps.Value.Item2); UpdateUI(ent, ent.Comp.CurrentStatus);
} }
private void OnActivated(EntityUid uid, WarDeclaratorComponent component, WarDeclaratorActivateMessage args) private void OnActivated(Entity<WarDeclaratorComponent> ent, ref WarDeclaratorActivateMessage args)
{ {
if (!args.Session.AttachedEntity.HasValue || if (args.Session.AttachedEntity is not {} playerEntity)
!_nukeopsRuleSystem.TryGetRuleFromOperative(args.Session.AttachedEntity.Value, out var comps))
return; return;
var condition = _nukeopsRuleSystem.GetWarCondition(comps.Value.Item1, comps.Value.Item2); var ev = new WarDeclaredEvent(ent.Comp.CurrentStatus, ent);
if (condition != WarConditionStatus.YES_WAR) RaiseLocalEvent(ref ev);
{
UpdateUI(uid, comps.Value.Item1, comps.Value.Item2); if (ent.Comp.DisableAt < _gameTiming.CurTime)
return; ev.Status = WarConditionStatus.NoWarTimeout;
}
ent.Comp.CurrentStatus = ev.Status;
var maxLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength); var maxLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
var message = SharedChatSystem.SanitizeAnnouncement(args.Message, maxLength); var message = SharedChatSystem.SanitizeAnnouncement(args.Message, maxLength);
if (component.AllowEditingMessage && message != string.Empty) if (ent.Comp.AllowEditingMessage && message != string.Empty)
{ ent.Comp.Message = message;
component.Message = message;
}
else
{
message = Loc.GetString("war-declarator-default-message");
}
var title = Loc.GetString(component.Title);
_nukeopsRuleSystem.DeclareWar(args.Session.AttachedEntity.Value, message, title, component.Sound, component.Color); if (ev.Status == WarConditionStatus.WarReady)
{
var title = Loc.GetString(ent.Comp.SenderTitle);
_chat.DispatchGlobalAnnouncement(ent.Comp.Message, title, true, ent.Comp.Sound, ent.Comp.Color);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(playerEntity):player} has declared war with this text: {ent.Comp.Message}");
}
if (args.Session.AttachedEntity != null) UpdateUI(ent, ev.Status);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(args.Session.AttachedEntity.Value):player} has declared war with this text: {message}");
} }
public void RefreshAllUI(NukeopsRuleComponent nukeops, GameRuleComponent gameRule) private void UpdateUI(Entity<WarDeclaratorComponent> ent, WarConditionStatus? status = null)
{ {
var enumerator = EntityQueryEnumerator<WarDeclaratorComponent>();
while (enumerator.MoveNext(out var uid, out _))
{
UpdateUI(uid, nukeops, gameRule);
}
}
private void UpdateUI(EntityUid declaratorUid, NukeopsRuleComponent nukeops, GameRuleComponent gameRule)
{
var condition = _nukeopsRuleSystem.GetWarCondition(nukeops, gameRule);
TimeSpan startTime;
TimeSpan delayTime;
switch(condition)
{
case WarConditionStatus.YES_WAR:
startTime = gameRule.ActivatedAt;
delayTime = nukeops.WarDeclarationDelay;
break;
case WarConditionStatus.WAR_DELAY:
startTime = nukeops.WarDeclaredTime!.Value;
delayTime = nukeops.WarNukieArriveDelay!.Value;
break;
default:
startTime = TimeSpan.Zero;
delayTime = TimeSpan.Zero;
break;
}
_userInterfaceSystem.TrySetUiState( _userInterfaceSystem.TrySetUiState(
declaratorUid, ent,
WarDeclaratorUiKey.Key, WarDeclaratorUiKey.Key,
new WarDeclaratorBoundUserInterfaceState( new WarDeclaratorBoundUserInterfaceState(status, ent.Comp.DisableAt, ent.Comp.ShuttleDisabledTime));
condition,
nukeops.WarDeclarationMinOps,
delayTime,
startTime));
} }
} }

View File

@@ -6,7 +6,7 @@ namespace Content.Server.Roles;
/// <summary> /// <summary>
/// Role used to keep track of space dragons for antag purposes. /// Role used to keep track of space dragons for antag purposes.
/// </summary> /// </summary>
[RegisterComponent, Access(typeof(DragonSystem))] [RegisterComponent, Access(typeof(DragonSystem)), ExclusiveAntagonist]
public sealed partial class DragonRoleComponent : AntagonistRoleComponent public sealed partial class DragonRoleComponent : AntagonistRoleComponent
{ {
} }

View File

@@ -1,8 +1,8 @@
using Content.Shared.Roles; using Content.Shared.Roles;
namespace Content.Server.Roles; namespace Content.Server.Roles;
[RegisterComponent] [RegisterComponent, ExclusiveAntagonist]
public sealed partial class InitialInfectedRoleComponent : AntagonistRoleComponent public sealed partial class InitialInfectedRoleComponent : AntagonistRoleComponent
{ {

View File

@@ -2,7 +2,7 @@ using Content.Shared.Roles;
namespace Content.Server.Roles; namespace Content.Server.Roles;
[RegisterComponent] [RegisterComponent, ExclusiveAntagonist]
public sealed partial class NinjaRoleComponent : AntagonistRoleComponent public sealed partial class NinjaRoleComponent : AntagonistRoleComponent
{ {
} }

View File

@@ -1,11 +1,11 @@
using Content.Shared.Roles; using Content.Shared.Roles;
namespace Content.Server.Roles; namespace Content.Server.Roles;
/// <summary> /// <summary>
/// Added to mind entities to tag that they are a nuke operative. /// Added to mind entities to tag that they are a nuke operative.
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent, ExclusiveAntagonist]
public sealed partial class NukeopsRoleComponent : AntagonistRoleComponent public sealed partial class NukeopsRoleComponent : AntagonistRoleComponent
{ {
} }

View File

@@ -5,7 +5,7 @@ namespace Content.Server.Roles;
/// <summary> /// <summary>
/// Added to mind entities to tag that they are a Revolutionary. /// Added to mind entities to tag that they are a Revolutionary.
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent, ExclusiveAntagonist]
public sealed partial class RevolutionaryRoleComponent : AntagonistRoleComponent public sealed partial class RevolutionaryRoleComponent : AntagonistRoleComponent
{ {
/// <summary> /// <summary>

View File

@@ -1,8 +1,8 @@
using Content.Shared.Roles; using Content.Shared.Roles;
namespace Content.Server.Roles; namespace Content.Server.Roles;
[RegisterComponent] [RegisterComponent, ExclusiveAntagonist]
public sealed partial class TerminatorRoleComponent : AntagonistRoleComponent public sealed partial class TerminatorRoleComponent : AntagonistRoleComponent
{ {
} }

View File

@@ -1,8 +1,8 @@
using Content.Shared.Roles; using Content.Shared.Roles;
namespace Content.Server.Roles; namespace Content.Server.Roles;
[RegisterComponent] [RegisterComponent, ExclusiveAntagonist]
public sealed partial class TraitorRoleComponent : AntagonistRoleComponent public sealed partial class TraitorRoleComponent : AntagonistRoleComponent
{ {
} }

View File

@@ -1,8 +1,8 @@
using Content.Shared.Roles; using Content.Shared.Roles;
namespace Content.Server.Roles; namespace Content.Server.Roles;
[RegisterComponent] [RegisterComponent, ExclusiveAntagonist]
public sealed partial class ZombieRoleComponent : AntagonistRoleComponent public sealed partial class ZombieRoleComponent : AntagonistRoleComponent
{ {
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.Shuttles.Systems; using Content.Server.Shuttles.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Shuttles.Components; namespace Content.Server.Shuttles.Components;
@@ -9,5 +10,12 @@ namespace Content.Server.Shuttles.Components;
[RegisterComponent, Access(typeof(ShuttleSystem))] [RegisterComponent, Access(typeof(ShuttleSystem))]
public sealed partial class GridFillComponent : Component public sealed partial class GridFillComponent : Component
{ {
[DataField("path")] public ResPath Path = new("/Maps/Shuttles/escape_pod_small.yml"); [DataField]
public ResPath Path = new("/Maps/Shuttles/escape_pod_small.yml");
/// <summary>
/// Components to be added to any spawned grids.
/// </summary>
[DataField]
public ComponentRegistry AddComponents = new();
} }

View File

@@ -216,6 +216,17 @@ public sealed partial class ShuttleSystem
valid = true; valid = true;
} }
} }
foreach (var compReg in component.AddComponents.Values)
{
var compType = compReg.Component.GetType();
if (HasComp(ent[0], compType))
continue;
var comp = _factory.GetComponent(compType);
AddComp(ent[0], comp, true);
}
} }
if (!valid) if (!valid)

View File

@@ -1,8 +1,5 @@
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.Maps; using Robust.Server.Maps;
using Robust.Shared.Map;
using Content.Server.GameTicking;
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;
using Content.Server.RoundEnd; using Content.Server.RoundEnd;
@@ -11,22 +8,20 @@ namespace Content.Server.StationEvents.Events;
public sealed class LoneOpsSpawnRule : StationEventSystem<LoneOpsSpawnRuleComponent> public sealed class LoneOpsSpawnRule : StationEventSystem<LoneOpsSpawnRuleComponent>
{ {
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly MapLoaderSystem _map = default!; [Dependency] private readonly MapLoaderSystem _map = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly NukeopsRuleSystem _nukeopsRuleSystem = default!;
protected override void Started(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) protected override void Started(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{ {
base.Started(uid, component, gameRule, args); base.Started(uid, component, gameRule, args);
if (!_nukeopsRuleSystem.CheckLoneOpsSpawn()) // Loneops can only spawn if there is no nukeops active
if (GameTicker.IsGameRuleAdded<NukeopsRuleComponent>())
{ {
ForceEndSelf(uid, gameRule); ForceEndSelf(uid, gameRule);
return; return;
} }
var shuttleMap = _mapManager.CreateMap(); var shuttleMap = MapManager.CreateMap();
var options = new MapLoadOptions var options = new MapLoadOptions
{ {
LoadMap = true, LoadMap = true,
@@ -34,12 +29,12 @@ public sealed class LoneOpsSpawnRule : StationEventSystem<LoneOpsSpawnRuleCompon
_map.TryLoad(shuttleMap, component.LoneOpsShuttlePath, out _, options); _map.TryLoad(shuttleMap, component.LoneOpsShuttlePath, out _, options);
var nukeopsEntity = _gameTicker.AddGameRule(component.GameRuleProto); var nukeopsEntity = GameTicker.AddGameRule(component.GameRuleProto);
component.AdditionalRule = nukeopsEntity; component.AdditionalRule = nukeopsEntity;
var nukeopsComp = EntityManager.GetComponent<NukeopsRuleComponent>(nukeopsEntity); var nukeopsComp = Comp<NukeopsRuleComponent>(nukeopsEntity);
nukeopsComp.SpawnOutpost = false; nukeopsComp.SpawnOutpost = false;
nukeopsComp.RoundEndBehavior = RoundEndBehavior.Nothing; nukeopsComp.RoundEndBehavior = RoundEndBehavior.Nothing;
_gameTicker.StartGameRule(nukeopsEntity); GameTicker.StartGameRule(nukeopsEntity);
} }
protected override void Ended(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) protected override void Ended(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
@@ -50,4 +45,3 @@ public sealed class LoneOpsSpawnRule : StationEventSystem<LoneOpsSpawnRuleCompon
GameTicker.EndGameRule(component.AdditionalRule.Value); GameTicker.EndGameRule(component.AdditionalRule.Value);
} }
} }

View File

@@ -1,38 +1,27 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
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.Components;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Content.Server.StationEvents.Components; using Content.Server.StationEvents.Components;
using Content.Shared.Database; using Content.Shared.Database;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Collections;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.StationEvents.Events; namespace Content.Server.StationEvents.Events;
/// <summary> /// <summary>
/// An abstract entity system inherited by all station events for their behavior. /// An abstract entity system inherited by all station events for their behavior.
/// </summary> /// </summary>
public abstract partial class StationEventSystem<T> : GameRuleSystem<T> where T : IComponent public abstract class StationEventSystem<T> : GameRuleSystem<T> where T : IComponent
{ {
[Dependency] protected readonly IAdminLogManager AdminLogManager = default!; [Dependency] protected readonly IAdminLogManager AdminLogManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] protected readonly IMapManager MapManager = default!; [Dependency] protected readonly IMapManager MapManager = default!;
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] protected readonly ChatSystem ChatSystem = default!; [Dependency] protected readonly ChatSystem ChatSystem = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!; [Dependency] protected readonly SharedAudioSystem Audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] protected readonly StationSystem StationSystem = default!; [Dependency] protected readonly StationSystem StationSystem = default!;
protected ISawmill Sawmill = default!; protected ISawmill Sawmill = default!;
@@ -60,7 +49,7 @@ public abstract partial class StationEventSystem<T> : GameRuleSystem<T> where T
} }
Audio.PlayGlobal(stationEvent.StartAudio, Filter.Broadcast(), true); Audio.PlayGlobal(stationEvent.StartAudio, Filter.Broadcast(), true);
stationEvent.StartTime = _timing.CurTime + stationEvent.StartDelay; stationEvent.StartTime = Timing.CurTime + stationEvent.StartDelay;
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -79,7 +68,7 @@ public abstract partial class StationEventSystem<T> : GameRuleSystem<T> where T
? stationEvent.Duration ? stationEvent.Duration
: TimeSpan.FromSeconds(RobustRandom.NextDouble(stationEvent.Duration.Value.TotalSeconds, : TimeSpan.FromSeconds(RobustRandom.NextDouble(stationEvent.Duration.Value.TotalSeconds,
stationEvent.MaxDuration.Value.TotalSeconds)); stationEvent.MaxDuration.Value.TotalSeconds));
stationEvent.EndTime = _timing.CurTime + duration; stationEvent.EndTime = Timing.CurTime + duration;
} }
} }
@@ -116,11 +105,11 @@ public abstract partial class StationEventSystem<T> : GameRuleSystem<T> where T
if (!GameTicker.IsGameRuleAdded(uid, ruleData)) if (!GameTicker.IsGameRuleAdded(uid, ruleData))
continue; continue;
if (!GameTicker.IsGameRuleActive(uid, ruleData) && _timing.CurTime >= stationEvent.StartTime) if (!GameTicker.IsGameRuleActive(uid, ruleData) && Timing.CurTime >= stationEvent.StartTime)
{ {
GameTicker.StartGameRule(uid, ruleData); GameTicker.StartGameRule(uid, ruleData);
} }
else if (stationEvent.EndTime != null && _timing.CurTime >= stationEvent.EndTime && GameTicker.IsGameRuleActive(uid, ruleData)) else if (stationEvent.EndTime != null && Timing.CurTime >= stationEvent.EndTime && GameTicker.IsGameRuleActive(uid, ruleData))
{ {
GameTicker.EndGameRule(uid, ruleData); GameTicker.EndGameRule(uid, ruleData);
} }

View File

@@ -1,6 +1,5 @@
using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules;
using Content.Server.Traitor.Components; using Content.Server.Traitor.Components;
using Content.Shared.Mind;
using Content.Shared.Mind.Components; using Content.Shared.Mind.Components;
namespace Content.Server.Traitor.Systems; namespace Content.Server.Traitor.Systems;
@@ -16,15 +15,9 @@ public sealed class AutoTraitorSystem : EntitySystem
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<AutoTraitorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<AutoTraitorComponent, MindAddedMessage>(OnMindAdded); SubscribeLocalEvent<AutoTraitorComponent, MindAddedMessage>(OnMindAdded);
} }
private void OnMapInit(EntityUid uid, AutoTraitorComponent comp, MapInitEvent args)
{
TryMakeTraitor(uid, comp);
}
private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args) private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args)
{ {
TryMakeTraitor(uid, comp); TryMakeTraitor(uid, comp);
@@ -60,15 +53,9 @@ public sealed class AutoTraitorSystem : EntitySystem
if (!Resolve(uid, ref comp)) if (!Resolve(uid, ref comp))
return false; return false;
if (!TryComp<MindContainerComponent>(uid, out var mindContainer) || mindContainer.Mind == null) //Start the rule if it has not already been started
return false; var traitorRuleComponent = _traitorRule.StartGameRule();
_traitorRule.MakeTraitor(uid, traitorRuleComponent, giveUplink: comp.GiveUplink, giveObjectives: comp.GiveObjectives);
var mindId = mindContainer.Mind.Value;
if (!TryComp<MindComponent>(mindId, out var mind) || mind.Session == null)
return false;
var session = mind.Session;
_traitorRule.MakeTraitor(session, giveUplink: comp.GiveUplink, giveObjectives: comp.GiveObjectives);
// prevent spamming anything if it fails // prevent spamming anything if it fails
RemComp<AutoTraitorComponent>(uid); RemComp<AutoTraitorComponent>(uid);
return true; return true;

View File

@@ -0,0 +1,22 @@
namespace Content.Shared.Antag;
/// <summary>
/// Used by AntagSelectionSystem to indicate which types of antag roles are allowed to choose the same entity
/// For example, Thief HeadRev
/// </summary>
public enum AntagAcceptability
{
/// <summary>
/// Dont choose anyone who already has an antag role
/// </summary>
None,
/// <summary>
/// Dont choose anyone who has an exclusive antag role
/// </summary>
NotExclusive,
/// <summary>
/// Choose anyone
/// </summary>
All
}

View File

@@ -487,6 +487,13 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<int> PiratesPlayersPerOp = public static readonly CVarDef<int> PiratesPlayersPerOp =
CVarDef.Create("pirates.players_per_pirate", 5); CVarDef.Create("pirates.players_per_pirate", 5);
/*
* Nukeops
*/
public static readonly CVarDef<bool> NukeopsSpawnGhostRoles =
CVarDef.Create("nukeops.spawn_ghost_roles", false);
/* /*
* Tips * Tips
*/ */

View File

@@ -1,12 +1,16 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
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;
public partial class InventorySystem public partial class InventorySystem
{ {
[Dependency] private readonly SharedStorageSystem _storageSystem = default!;
/// <summary> /// <summary>
/// Yields all entities in hands or inventory slots with the specific flags. /// Yields all entities in hands or inventory slots with the specific flags.
/// </summary> /// </summary>
@@ -86,4 +90,55 @@ public partial class InventorySystem
// We finally try to equip the item, otherwise we delete it. // We finally try to equip the item, otherwise we delete it.
return TryEquip(uid, item, slot, silent, force) || DeleteItem(); return TryEquip(uid, item, slot, silent, force) || DeleteItem();
} }
/// <summary>
/// Will attempt to spawn a list of items inside of an entities bag, pockets, hands or nearby
/// </summary>
/// <param name="entity">The entity that you want to spawn an item on</param>
/// <param name="items">A list of prototype IDs that you want to spawn in the bag.</param>
public void SpawnItemsOnEntity(EntityUid entity, List<EntProtoId> items)
{
foreach (var item in items)
{
SpawnItemOnEntity(entity, item);
}
}
/// <summary>
/// Will attempt to spawn an item inside of an entities bag, pockets, hands or nearby
/// </summary>
/// <param name="entity">The entity that you want to spawn an item on</param>
/// <param name="item">The prototype ID that you want to spawn in the bag.</param>
public void SpawnItemOnEntity(EntityUid entity, EntProtoId item)
{
//Transform() throws error if TransformComponent doesnt exist
if (!HasComp<TransformComponent>(entity))
return;
var xform = Transform(entity);
var mapCoords = _transform.GetMapCoordinates(xform);
var itemToSpawn = Spawn(item, mapCoords);
//Try insert into the backpack
if (TryGetSlotContainer(entity, "back", out var backSlot, out _)
&& backSlot.ContainedEntity.HasValue
&& _storageSystem.Insert(backSlot.ContainedEntity.Value, itemToSpawn, out _)
)
return;
//Try insert into pockets
if (TryGetSlotContainer(entity, "pocket1", out var pocket1, out _)
&& _containerSystem.Insert(itemToSpawn, pocket1)
)
return;
if (TryGetSlotContainer(entity, "pocket2", out var pocket2, out _)
&& _containerSystem.Insert(itemToSpawn, pocket2)
)
return;
//Try insert into hands, or drop on the floor
_handsSystem.PickupOrDrop(entity, itemToSpawn, false);
}
} }

View File

@@ -10,30 +10,28 @@ public enum WarDeclaratorUiKey
public enum WarConditionStatus : byte public enum WarConditionStatus : byte
{ {
WAR_READY, WarReady,
WAR_DELAY, YesWar,
YES_WAR, NoWarUnknown,
NO_WAR_UNKNOWN, NoWarTimeout,
NO_WAR_TIMEOUT, NoWarSmallCrew,
NO_WAR_SMALL_CREW, NoWarShuttleDeparted
NO_WAR_SHUTTLE_DEPARTED
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class WarDeclaratorBoundUserInterfaceState : BoundUserInterfaceState public sealed class WarDeclaratorBoundUserInterfaceState : BoundUserInterfaceState
{ {
public WarConditionStatus Status; public WarConditionStatus? Status;
public int MinCrew; public TimeSpan ShuttleDisabledTime;
public TimeSpan Delay;
public TimeSpan EndTime; public TimeSpan EndTime;
public WarDeclaratorBoundUserInterfaceState(WarConditionStatus status, int minCrew, TimeSpan delay, TimeSpan endTime) public WarDeclaratorBoundUserInterfaceState(WarConditionStatus? status, TimeSpan endTime, TimeSpan shuttleDisabledTime)
{ {
Status = status; Status = status;
MinCrew = minCrew;
Delay = delay;
EndTime = endTime; EndTime = endTime;
ShuttleDisabledTime = shuttleDisabledTime;
} }
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]

View File

@@ -2,6 +2,7 @@ using Content.Shared.Antag;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Content.Shared.StatusIcon; using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Audio;
namespace Content.Shared.Revolutionary.Components; namespace Content.Shared.Revolutionary.Components;
@@ -17,8 +18,14 @@ public sealed partial class RevolutionaryComponent : Component, IAntagStatusIcon
[DataField, ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
public ProtoId<StatusIconPrototype> StatusIcon { get; set; } = "RevolutionaryFaction"; public ProtoId<StatusIconPrototype> StatusIcon { get; set; } = "RevolutionaryFaction";
/// <summary>
/// Sound that plays when you are chosen as Rev. (Placeholder until I find something cool I guess)
/// </summary>
[DataField]
public SoundSpecifier RevStartSound = new SoundPathSpecifier("/Audio/Ambience/Antag/headrev_start.ogg");
public override bool SessionSpecific => true; public override bool SessionSpecific => true;
[DataField] [DataField]
public bool IconVisibleToGhost { get; set; } = true; public bool IconVisibleToGhost { get; set; } = true;
} }

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using JetBrains.Annotations;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Roles; namespace Content.Shared.Roles;
@@ -7,3 +8,13 @@ public abstract partial class AntagonistRoleComponent : Component
[DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))] [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public string? PrototypeId; public string? PrototypeId;
} }
/// <summary>
/// Mark the antagonist role component as being exclusive
/// IE by default other antagonists should refuse to select the same entity for a different antag role
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
[BaseTypeRequired(typeof(AntagonistRoleComponent))]
public sealed partial class ExclusiveAntagonistAttribute : Attribute
{
}

View File

@@ -1,9 +1,10 @@
namespace Content.Shared.Roles; namespace Content.Shared.Roles;
/// <summary> /// <summary>
/// Event raised on a mind entity id to get whether or not the player is considered an antagonist, /// Event raised on a mind entity id to get whether or not the player is considered an antagonist,
/// depending on their roles. /// depending on their roles.
/// </summary> /// </summary>
/// <param name="IsAntagonist">Whether or not the player is an antagonist.</param> /// <param name="IsAntagonist">Whether or not the player is an antagonist.</param>
/// <param name="IsExclusiveAntagonist">Whether or not AntagSelectionSystem should exclude this player from other antag roles</param
[ByRefEvent] [ByRefEvent]
public record struct MindIsAntagonistEvent(bool IsAntagonist); public record struct MindIsAntagonistEvent(bool IsAntagonist, bool IsExclusiveAntagonist);

View File

@@ -1,10 +1,11 @@
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.Roles.Jobs; using Content.Shared.Roles.Jobs;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Roles; namespace Content.Shared.Roles;
@@ -57,7 +58,7 @@ public abstract class SharedRoleSystem : EntitySystem
args.Roles.Add(new RoleInfo(component, name, true, null, prototype)); args.Roles.Add(new RoleInfo(component, name, true, null, prototype));
}); });
SubscribeLocalEvent((EntityUid _, T _, ref MindIsAntagonistEvent args) => args.IsAntagonist = true); SubscribeLocalEvent((EntityUid _, T _, ref MindIsAntagonistEvent args) => { args.IsAntagonist = true; args.IsExclusiveAntagonist |= typeof(T).TryGetCustomAttribute<ExclusiveAntagonistAttribute>(out _); });
_antagTypes.Add(typeof(T)); _antagTypes.Add(typeof(T));
} }
@@ -85,7 +86,7 @@ public abstract class SharedRoleSystem : EntitySystem
AddComp(mindId, component); AddComp(mindId, component);
var antagonist = IsAntagonistRole<T>(); var antagonist = IsAntagonistRole<T>();
var mindEv = new MindRoleAddedEvent(); var mindEv = new MindRoleAddedEvent(silent);
RaiseLocalEvent(mindId, ref mindEv); RaiseLocalEvent(mindId, ref mindEv);
var message = new RoleAddedEvent(mindId, mind, antagonist, silent); var message = new RoleAddedEvent(mindId, mind, antagonist, silent);
@@ -156,6 +157,21 @@ public abstract class SharedRoleSystem : EntitySystem
return ev.IsAntagonist; return ev.IsAntagonist;
} }
/// <summary>
/// Does this mind possess an exclusive antagonist role
/// </summary>
/// <param name="mindId">The mind entity</param>
/// <returns>True if the mind possesses an exclusive antag role</returns>
public bool MindIsExclusiveAntagonist(EntityUid? mindId)
{
if (mindId == null)
return false;
var ev = new MindIsAntagonistEvent();
RaiseLocalEvent(mindId.Value, ref ev);
return ev.IsExclusiveAntagonist;
}
public bool IsAntagonistRole<T>() public bool IsAntagonistRole<T>()
{ {
return _antagTypes.Contains(typeof(T)); return _antagTypes.Contains(typeof(T));

View File

@@ -37,3 +37,6 @@ latejoin-arrival-announcement = {$character} ({$job}) has arrived at the station
latejoin-arrival-sender = Station latejoin-arrival-sender = Station
latejoin-arrivals-direction = A shuttle transferring you to your station will arrive shortly. latejoin-arrivals-direction = A shuttle transferring you to your station will arrive shortly.
latejoin-arrivals-direction-time = A shuttle transferring you to your station will arrive in {$time}. latejoin-arrivals-direction-time = A shuttle transferring you to your station will arrive in {$time}.
preset-not-enough-ready-players = Can't start {$presetName}. Requires {$minimumPlayers} players but we have {$readyPlayersCount}.
preset-no-one-ready = Can't start {$presetName}. No players are ready.

View File

@@ -1,16 +1,15 @@
war-declarator-not-nukeops = The device makes beeping noises, but nothing happens... war-declarator-not-working = The device makes beeping noises, but nothing happens...
war-declarator-ui-header = Declaration of War war-declarator-ui-header = Declaration of War
war-declarator-ui-war-button = DECLARE WAR! war-declarator-ui-war-button = DECLARE WAR!
war-declarator-conditions-small-crew = Less than { $min } operatives war-declarator-ui-try-war-button = Try to declare war
war-declarator-conditions-small-crew = There are not enough nuclear operatives to declare war!
war-declarator-conditions-left-outpost = Shuttle left the syndicate outpost war-declarator-conditions-left-outpost = Shuttle left the syndicate outpost
war-declarator-conditions-time-out = War declaration time passed war-declarator-conditions-time-out = War declaration time passed
war-declarator-conditions-delay = Shuttle departure temporarily unavailable
war-declarator-conditions-ready = Shuttle can leave the outpost! war-declarator-conditions-ready = Shuttle can leave the outpost!
war-declarator-conditions-unknown = Unknown war-declarator-conditions-unknown = Unknown
war-declarator-boost-possible = Able to declare war war-declarator-boost-possible = Able to declare war
war-declarator-boost-impossible = Unable to declare war war-declarator-boost-impossible = Unable to declare war
war-declarator-boost-declared = War declared! war-declarator-boost-declared = War declared! Shuttle may be disabled for...
war-declarator-boost-declared-delay = War declared! Shuttle departure temporarily disabled war-declarator-boost-timer = Time left: {$time} minutes.
war-declarator-boost-timer = Time left: {$minutes} minutes and {$seconds} seconds
war-declarator-default-message = A syndicate fringe group has declared their intent to utterly destroy station with a nuclear device, and dares the crew to try and stop them. war-declarator-default-message = A syndicate fringe group has declared their intent to utterly destroy station with a nuclear device, and dares the crew to try and stop them.
war-declarator-message-placeholder = Write a custom declaration of war here... war-declarator-message-placeholder = Write a custom declaration of war here...

View File

@@ -1,2 +1,2 @@
war-ops-infiltrator-unavailable = ERROR: FTL Travel recalculation in progress. Estimated time: {$minutes} minutes and {$seconds} seconds war-ops-infiltrator-unavailable = ERROR: FTL Travel recalculation in progress. Estimated time: {$time} minutes.
war-ops-shuttle-call-unavailable = Evacuation shuttle is currently unavailable. Please wait war-ops-shuttle-call-unavailable = Evacuation shuttle is currently unavailable. Please wait

View File

@@ -1,7 +1,7 @@
- type: entity - type: entity
parent: BaseItem parent: BaseItem
id: NukeOpsDeclarationOfWar id: NukeOpsDeclarationOfWar
name: declaration of war name: war declarator
description: Use to send a declaration of hostilities to the target, delaying your shuttle departure while they prepare for your assault. Such a brazen move will attract the attention of powerful benefactors within the Syndicate, who will supply your team with a massive amount of bonus telecrystals. Must be used at start of mission, or your benefactors will lose interest. description: Use to send a declaration of hostilities to the target, delaying your shuttle departure while they prepare for your assault. Such a brazen move will attract the attention of powerful benefactors within the Syndicate, who will supply your team with a massive amount of bonus telecrystals. Must be used at start of mission, or your benefactors will lose interest.
components: components:
- type: Sprite - type: Sprite
@@ -22,4 +22,5 @@
type: WarDeclaratorBoundUserInterface type: WarDeclaratorBoundUserInterface
- type: WarDeclarator - type: WarDeclarator
message: war-declarator-default-message message: war-declarator-default-message
# - type: WarConditionOnExamine - type: AccessReader
access: [["NuclearOperative"]]

View File

@@ -0,0 +1,16 @@
- type: entity
abstract: true
id: BaseStationSyndicate
components:
- type: NpcFactionMember
factions:
- Syndicate
- type: entity
id: StandardNukieOutpost
parent:
- BaseStation
- BaseStationSyndicate
noSpawn: true
components:
- type: Transform

View File

@@ -58,6 +58,8 @@
parent: BaseGameRule parent: BaseGameRule
noSpawn: true noSpawn: true
components: components:
- type: GameRule
minPlayers: 20
- type: NukeopsRule - type: NukeopsRule
faction: Syndicate faction: Syndicate
- type: ThiefRule #the thieves come as an extension of another gamemode - type: ThiefRule #the thieves come as an extension of another gamemode

View File

@@ -0,0 +1,11 @@
- type: gameMap
id: NukieOutpost
mapName: Nukie Outpost
mapPath: /Maps/Nonstations/nukieplanet.yml
minPlayers: 0
stations:
SyndicateOutpost:
stationProto: StandardNukieOutpost
components:
- type: StationNameSetup
mapNameTemplate: "Nukie Outpost"