diff --git a/Content.Client/Antag/AntagStatusIconSystem.cs b/Content.Client/Antag/AntagStatusIconSystem.cs
new file mode 100644
index 0000000000..3c1c72d03b
--- /dev/null
+++ b/Content.Client/Antag/AntagStatusIconSystem.cs
@@ -0,0 +1,32 @@
+using Content.Shared.StatusIcon;
+using Content.Shared.StatusIcon.Components;
+using Robust.Shared.Prototypes;
+using Content.Shared.Ghost;
+using Robust.Client.Player;
+
+namespace Content.Client.Antag;
+
+///
+/// Used for assigning specified icons for antags.
+///
+public abstract class AntagStatusIconSystem : SharedStatusIconSystem
+ where T : Component
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+
+ ///
+ /// Will check if the local player has the same component as the one who called it and give the status icon.
+ ///
+ /// The status icon that your antag uses
+ /// The GetStatusIcon event.
+ protected virtual void GetStatusIcon(string antagStatusIcon, ref GetStatusIconsEvent args)
+ {
+ var ent = _player.LocalPlayer?.ControlledEntity;
+
+ if (!HasComp(ent) && !HasComp(ent))
+ return;
+
+ args.StatusIcons.Add(_prototype.Index(antagStatusIcon));
+ }
+}
diff --git a/Content.Client/Revolutionary/RevolutionarySystem.cs b/Content.Client/Revolutionary/RevolutionarySystem.cs
new file mode 100644
index 0000000000..0818b14bc0
--- /dev/null
+++ b/Content.Client/Revolutionary/RevolutionarySystem.cs
@@ -0,0 +1,35 @@
+using Content.Shared.Revolutionary.Components;
+using Content.Client.Antag;
+using Content.Shared.StatusIcon.Components;
+
+namespace Content.Client.Revolutionary;
+
+///
+/// Used for the client to get status icons from other revs.
+///
+public sealed class RevolutionarySystem : AntagStatusIconSystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(GetRevIcon);
+ SubscribeLocalEvent(GetHeadRevIcon);
+ }
+
+ ///
+ /// Checks if the person who triggers the GetStatusIcon event is also a Rev or a HeadRev.
+ ///
+ private void GetRevIcon(EntityUid uid, RevolutionaryComponent comp, ref GetStatusIconsEvent args)
+ {
+ if (!HasComp(uid))
+ {
+ GetStatusIcon(comp.RevStatusIcon, ref args);
+ }
+ }
+
+ private void GetHeadRevIcon(EntityUid uid, HeadRevolutionaryComponent comp, ref GetStatusIconsEvent args)
+ {
+ GetStatusIcon(comp.HeadRevStatusIcon, ref args);
+ }
+}
diff --git a/Content.Client/Zombies/ZombieSystem.cs b/Content.Client/Zombies/ZombieSystem.cs
index 8816735a2a..6d0355f6f8 100644
--- a/Content.Client/Zombies/ZombieSystem.cs
+++ b/Content.Client/Zombies/ZombieSystem.cs
@@ -1,18 +1,14 @@
-using System.Linq;
+using System.Linq;
+using Content.Client.Antag;
using Content.Shared.Humanoid;
-using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Content.Shared.Zombies;
using Robust.Client.GameObjects;
-using Robust.Client.Player;
-using Robust.Shared.Prototypes;
namespace Content.Client.Zombies;
-public sealed class ZombieSystem : SharedZombieSystem
+public sealed class ZombieSystem : AntagStatusIconSystem
{
- [Dependency] private readonly IPlayerManager _player = default!;
- [Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
@@ -38,9 +34,6 @@ public sealed class ZombieSystem : SharedZombieSystem
private void OnGetStatusIcon(EntityUid uid, ZombieComponent component, ref GetStatusIconsEvent args)
{
- if (!HasComp(_player.LocalPlayer?.ControlledEntity))
- return;
-
- args.StatusIcons.Add(_prototype.Index(component.ZombieStatusIcon));
+ GetStatusIcon(component.ZombieStatusIcon, ref args);
}
}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
index 3471c8bb78..6fe526af11 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
@@ -1,3 +1,4 @@
+using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.Zombies;
using Content.Shared.Administration;
@@ -8,6 +9,8 @@ using Content.Shared.Mind.Components;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Utility;
+using Content.Server.GameTicking.Rules.Components;
+using System.Linq;
namespace Content.Server.Administration.Systems;
@@ -17,7 +20,9 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
[Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!;
[Dependency] private readonly PiratesRuleSystem _piratesRule = default!;
+ [Dependency] private readonly RevolutionaryRuleSystem _revolutionaryRule = default!;
[Dependency] private readonly SharedMindSystem _minds = default!;
+ [Dependency] private readonly GameTicker _gameTicker = default!;
// All antag verbs have names so invokeverb works.
private void AddAntagVerbs(GetVerbsEvent args)
@@ -100,5 +105,22 @@ public sealed partial class AdminVerbSystem
Message = Loc.GetString("admin-verb-make-pirate"),
};
args.Verbs.Add(pirate);
+
+ //todo come here at some point dear lort.
+ Verb headRev = new()
+ {
+ Text = Loc.GetString("admin-verb-text-make-head-rev"),
+ Category = VerbCategory.Antag,
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Misc/job_icons.rsi/HeadRevolutionary.png")),
+ Act = () =>
+ {
+ if (!_minds.TryGetMind(args.Target, out var mindId, out var mind))
+ return;
+ _revolutionaryRule.OnHeadRevAdmin(mindId, mind);
+ },
+ Impact = LogImpact.High,
+ Message = Loc.GetString("admin-verb-make-head-rev"),
+ };
+ args.Verbs.Add(headRev);
}
}
diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs
new file mode 100644
index 0000000000..f1ca181756
--- /dev/null
+++ b/Content.Server/Antag/AntagSelectionSystem.cs
@@ -0,0 +1,237 @@
+using Content.Server.GameTicking.Rules;
+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 Robust.Shared.Random;
+using Robust.Shared.Map;
+using System.Numerics;
+using Content.Shared.Inventory;
+using Content.Server.Storage.EntitySystems;
+using Robust.Shared.Audio;
+using Robust.Server.GameObjects;
+using Content.Server.Chat.Managers;
+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.Containers;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Antag;
+
+public sealed class AntagSelectionSystem : GameRuleSystem
+{
+ [Dependency] private readonly IChatManager _chatManager = 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 ContainerSystem _containerSystem = default!;
+ [Dependency] private readonly JobSystem _jobs = default!;
+ [Dependency] private readonly MindSystem _mindSystem = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly StorageSystem _storageSystem = default!;
+ [Dependency] private readonly StationSystem _stationSystem = default!;
+ [Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
+
+ ///
+ /// Attempts to start the game rule by checking if there are enough players in lobby and readied.
+ ///
+ /// The roundstart attempt event
+ /// The entity the gamerule you are using is on
+ /// The minimum amount of players needed for you gamerule to start.
+ /// The gamerule component.
+
+ public void AttemptStartGameRule(RoundStartAttemptEvent ev, EntityUid uid, int minPlayers, GameRuleComponent gameRule)
+ {
+ if (GameTicker.IsGameRuleAdded(uid, gameRule))
+ {
+ if (!ev.Forced && ev.Players.Length < minPlayers)
+ {
+ _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();
+ }
+ }
+ }
+
+ ///
+ /// Will check which players are eligible to be chosen for antagonist and give them the given antag.
+ ///
+ /// The antag prototype from your rule component.
+ /// How many antags can be present in any given round.
+ /// How many players you need to spawn an additional antag.
+ /// The intro sound that plays when the antag is chosen.
+ /// The antag message you want shown when the antag is chosen.
+ /// The color of the message for the antag greeting in hex.
+ /// A list of all the antags chosen in case you need to add stuff after.
+ /// Whether or not heads can be chosen as antags for this gamemode.
+ public void EligiblePlayers(string antagPrototype,
+ int maxAntags,
+ int antagsPerPlayer,
+ SoundSpecifier? antagSound,
+ string antagGreeting,
+ string greetingColor,
+ out List chosen,
+ bool includeHeads = false)
+ {
+ var allPlayers = _playerSystem.ServerSessions.ToList();
+ var playerList = new List();
+ var prefList = new List();
+ chosen = new List();
+ foreach (var player in allPlayers)
+ {
+ if (includeHeads == false)
+ {
+ if (!_jobs.CanBeAntag(player))
+ continue;
+ }
+
+ if (player.AttachedEntity == null || HasComp(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;
+
+ var antags = Math.Clamp(allPlayers.Count / antagsPerPlayer, 1, maxAntags);
+ for (var antag = 0; antag < antags; antag++)
+ {
+ IPlayerSession chosenPlayer;
+ if (prefList.Count == 0)
+ {
+ if (playerList.Count == 0)
+ {
+ break;
+ }
+ chosenPlayer = _random.PickAndTake(playerList);
+ }
+ else
+ {
+ chosenPlayer = _random.PickAndTake(prefList);
+ playerList.Remove(chosenPlayer);
+ }
+
+ if (!_mindSystem.TryGetMind(chosenPlayer, out _, out var mind) ||
+ mind.OwnedEntity is not { } ownedEntity)
+ {
+ 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.ConnectedClient, Color.FromHex(greetingColor));
+ }
+ }
+ }
+
+ ///
+ /// Will take a group of entities and check if they are all alive or dead
+ ///
+ /// The list of the entities
+ /// 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)
+ ///
+ public bool IsGroupDead(List list, bool checkOffStation)
+ {
+ var dead = 0;
+ foreach (var entity in list)
+ {
+ if (TryComp(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;
+ }
+
+ ///
+ /// Will attempt to spawn an item inside of a persons bag and then pockets.
+ ///
+ /// The entity that you want to spawn an item on
+ /// A list of prototype IDs that you want to spawn in the bag.
+ public void GiveAntagBagGear(EntityUid antag, List items)
+ {
+ foreach (var item in items)
+ {
+ GiveAntagBagGear(antag, item);
+ }
+ }
+
+ ///
+ /// Will attempt to spawn an item inside of a persons bag and then pockets.
+ ///
+ /// The entity that you want to spawn an item on
+ /// The prototype ID that you want to spawn in the bag.
+ public void GiveAntagBagGear(EntityUid antag, string item)
+ {
+ var itemToSpawn = Spawn(item, new EntityCoordinates(antag, Vector2.Zero));
+ if (!_inventory.TryGetSlotContainer(antag, "back", out var backSlot, out _))
+ return;
+
+ var bag = backSlot.ContainedEntity;
+ if (bag != null && HasComp(bag) && _storageSystem.CanInsert(bag.Value, itemToSpawn, out _))
+ {
+ _storageSystem.Insert(bag.Value, itemToSpawn, out _);
+ }
+ 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))
+ {
+ pocket1Slot.Insert(itemToSpawn);
+ }
+ }
+ else if (_inventory.TryGetSlotContainer(antag, "pocket2", out var pocket2Slot, out _))
+ {
+ if (pocket2Slot.ContainedEntity == null)
+ {
+ if (_containerSystem.CanInsert(itemToSpawn, pocket2Slot))
+ {
+ pocket2Slot.Insert(itemToSpawn);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/Content.Server/Flash/FlashSystem.cs b/Content.Server/Flash/FlashSystem.cs
index 04b3f0cd24..bc2c4ddaba 100644
--- a/Content.Server/Flash/FlashSystem.cs
+++ b/Content.Server/Flash/FlashSystem.cs
@@ -5,7 +5,6 @@ using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
-using Content.Shared.Damage;
using Content.Shared.Eye.Blinding.Components;
using Content.Shared.Flash;
using Content.Shared.IdentityManagement;
@@ -13,7 +12,6 @@ using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Physics;
-using Content.Shared.Popups;
using Content.Shared.Tag;
using Content.Shared.Traits.Assorted;
using Content.Shared.Weapons.Melee.Events;
@@ -41,11 +39,11 @@ namespace Content.Server.Flash
public override void Initialize()
{
base.Initialize();
+
SubscribeLocalEvent(OnFlashMeleeHit);
// ran before toggling light for extra-bright lantern
SubscribeLocalEvent(OnFlashUseInHand, before: new []{ typeof(HandheldLightSystem) });
SubscribeLocalEvent(OnInventoryFlashAttempt);
-
SubscribeLocalEvent(OnFlashImmunityFlashAttempt);
SubscribeLocalEvent(OnPermanentBlindnessFlashAttempt);
SubscribeLocalEvent(OnTemporaryBlindnessFlashAttempt);
@@ -63,7 +61,7 @@ namespace Content.Server.Flash
args.Handled = true;
foreach (var e in args.HitEntities)
{
- Flash(e, args.User, uid, comp.FlashDuration, comp.SlowTo);
+ Flash(e, args.User, uid, comp.FlashDuration, comp.SlowTo, melee: true);
}
}
@@ -106,9 +104,17 @@ namespace Content.Server.Flash
return true;
}
- public void Flash(EntityUid target, EntityUid? user, EntityUid? used, float flashDuration, float slowTo, bool displayPopup = true, FlashableComponent? flashable = null)
+ public void Flash(EntityUid target,
+ EntityUid? user,
+ EntityUid? used,
+ float flashDuration,
+ float slowTo,
+ bool displayPopup = true,
+ FlashableComponent? flashable = null,
+ bool melee = false)
{
- if (!Resolve(target, ref flashable, false)) return;
+ if (!Resolve(target, ref flashable, false))
+ return;
var attempt = new FlashAttemptEvent(target, user, used);
RaiseLocalEvent(target, attempt, true);
@@ -116,18 +122,28 @@ namespace Content.Server.Flash
if (attempt.Cancelled)
return;
+ if (melee)
+ {
+ var ev = new AfterFlashedEvent(target, user, used);
+ if (user != null)
+ RaiseLocalEvent(user.Value, ref ev);
+ if (used != null)
+ RaiseLocalEvent(used.Value, ref ev);
+ }
+
flashable.LastFlash = _timing.CurTime;
flashable.Duration = flashDuration / 1000f; // TODO: Make this sane...
- Dirty(flashable);
+ Dirty(target, flashable);
_stun.TrySlowdown(target, TimeSpan.FromSeconds(flashDuration/1000f), true,
slowTo, slowTo);
- if (displayPopup && user != null && target != user && EntityManager.EntityExists(user.Value))
+ if (displayPopup && user != null && target != user && Exists(user.Value))
{
- user.Value.PopupMessage(target, Loc.GetString("flash-component-user-blinds-you",
- ("user", Identity.Entity(user.Value, EntityManager))));
+ _popup.PopupEntity(Loc.GetString("flash-component-user-blinds-you",
+ ("user", Identity.Entity(user.Value, EntityManager))), target, target);
}
+
}
public void FlashArea(EntityUid source, EntityUid? user, float range, float duration, float slowTo = 0.8f, bool displayPopup = false, SoundSpecifier? sound = null)
@@ -201,4 +217,24 @@ namespace Content.Server.Flash
Used = used;
}
}
+ ///
+ /// Called after a flash is used via melee on another person to check for rev conversion.
+ /// Raised on the user of the flash, the target hit by the flash, and the flash used.
+ ///
+ [ByRefEvent]
+ public readonly struct AfterFlashedEvent
+ {
+ public readonly EntityUid Target;
+ public readonly EntityUid? User;
+ public readonly EntityUid? Used;
+
+ public AfterFlashedEvent(EntityUid target, EntityUid? user, EntityUid? used)
+ {
+ Target = target;
+ User = user;
+ Used = used;
+ }
+ }
+
+
}
diff --git a/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs
new file mode 100644
index 0000000000..f015f8bd57
--- /dev/null
+++ b/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs
@@ -0,0 +1,74 @@
+using Content.Shared.Roles;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.GameTicking.Rules.Components;
+
+///
+/// Component for the RevolutionaryRuleSystem that stores info about winning/losing, player counts required for starting, as well as prototypes for Revolutionaries and their gear.
+///
+[RegisterComponent, Access(typeof(RevolutionaryRuleSystem))]
+public sealed partial class RevolutionaryRuleComponent : Component
+{
+ ///
+ /// When the round will if all the command are dead (Incase they are in space)
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan CommandCheck;
+
+ ///
+ /// The amount of time between each check for command check.
+ ///
+ [DataField]
+ public TimeSpan TimerWait = TimeSpan.FromSeconds(20);
+
+ ///
+ /// Stores players minds
+ ///
+ [DataField]
+ public Dictionary HeadRevs = new();
+
+ [DataField]
+ public ProtoId RevPrototypeId = "Rev";
+
+ ///
+ /// Sound that plays when you are chosen as Rev. (Placeholder until I find something cool I guess)
+ ///
+ [DataField]
+ public SoundSpecifier HeadRevStartSound = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
+
+ ///
+ /// Min players needed for Revolutionary gamemode to start.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public int MinPlayers = 15;
+
+ ///
+ /// Max Head Revs allowed during selection.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public int MaxHeadRevs = 3;
+
+ ///
+ /// The amount of Head Revs that will spawn per this amount of players.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public int PlayersPerHeadRev = 15;
+
+ ///
+ /// The gear head revolutionaries are given on spawn.
+ ///
+ [DataField]
+ public List StartingGear = new()
+ {
+ "Flash",
+ "ClothingEyesGlassesSunglasses"
+ };
+
+ ///
+ /// The time it takes after the last head is killed for the shuttle to arrive.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan ShuttleCallTime = TimeSpan.FromMinutes(5);
+}
diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
new file mode 100644
index 0000000000..7efd3a9ce3
--- /dev/null
+++ b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
@@ -0,0 +1,311 @@
+using System.Linq;
+using Content.Server.Chat.Managers;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Mind;
+using Content.Server.NPC.Systems;
+using Content.Server.Roles;
+using Content.Shared.Humanoid;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Revolutionary.Components;
+using Content.Shared.Roles;
+using Content.Shared.Stunnable;
+using Robust.Shared.Timing;
+using Content.Server.Popups;
+using Content.Server.Revolutionary.Components;
+using Content.Shared.IdentityManagement;
+using Content.Server.Flash;
+using Content.Shared.Mindshield.Components;
+using Content.Server.Administration.Logs;
+using Content.Shared.Database;
+using Content.Server.Antag;
+using Content.Server.NPC.Components;
+using Content.Server.RoundEnd;
+using Content.Shared.Chat;
+using Content.Shared.Mind;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Zombies;
+
+namespace Content.Server.GameTicking.Rules;
+
+///
+/// Where all the main stuff for Revolutionaries happens (Assigning Head Revs, Command on station, and checking for the game to end.)
+///
+public sealed class RevolutionaryRuleSystem : GameRuleSystem
+{
+ [Dependency] private readonly IAdminLogManager _adminLogManager = default!;
+ [Dependency] private readonly IChatManager _chatManager = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
+ [Dependency] private readonly MindSystem _mind = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly RoleSystem _role = default!;
+ [Dependency] private readonly SharedStunSystem _stun = default!;
+ [Dependency] private readonly RoundEndSystem _roundEnd = default!;
+
+ [ValidatePrototypeId]
+ public const string RevolutionaryNpcFaction = "Revolutionary";
+ [ValidatePrototypeId]
+ public const string RevolutionaryAntagRole = "Rev";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnStartAttempt);
+ SubscribeLocalEvent(OnPlayerJobAssigned);
+ SubscribeLocalEvent(OnCommandMobStateChanged);
+ SubscribeLocalEvent(OnHeadRevMobStateChanged);
+ SubscribeLocalEvent(OnRoundEndText);
+ SubscribeLocalEvent(OnPostFlash);
+ }
+
+ protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+ {
+ base.Started(uid, component, gameRule, args);
+ component.CommandCheck = _timing.CurTime + component.TimerWait;
+ }
+
+ ///
+ /// Checks if the round should end and also checks who has a mindshield.
+ ///
+ protected override void ActiveTick(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, float frameTime)
+ {
+ base.ActiveTick(uid, component, gameRule, frameTime);
+ if (component.CommandCheck <= _timing.CurTime)
+ {
+ component.CommandCheck = _timing.CurTime + component.TimerWait;
+
+ if (CheckCommandLose())
+ {
+ _roundEnd.DoRoundEndBehavior(RoundEndBehavior.ShuttleCall, component.ShuttleCallTime);
+ GameTicker.EndGameRule(uid, gameRule);
+ }
+ }
+ }
+
+ private void OnRoundEndText(RoundEndTextAppendEvent ev)
+ {
+ var revsLost = CheckRevsLose();
+ var commandLost = CheckCommandLose();
+ var query = AllEntityQuery();
+ while (query.MoveNext(out var headrev))
+ {
+ // This is (revsLost, commandsLost) concatted together
+ // (moony wrote this comment idk what it means)
+ var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
+ ev.AddLine(Loc.GetString(Outcomes[index]));
+
+ ev.AddLine(Loc.GetString("head-rev-initial-count", ("initialCount", headrev.HeadRevs.Count)));
+ foreach (var player in headrev.HeadRevs)
+ {
+ _mind.TryGetSession(player.Value, out var session);
+ var username = session?.Name;
+ if (username != null)
+ {
+ ev.AddLine(Loc.GetString("head-rev-initial",
+ ("name", player.Key),
+ ("username", username)));
+ }
+ else
+ {
+ ev.AddLine(Loc.GetString("head-rev-initial",
+ ("name", player.Key)));
+ }
+ }
+ break;
+ }
+ }
+
+ private void OnStartAttempt(RoundStartAttemptEvent ev)
+ {
+ var query = AllEntityQuery();
+ while (query.MoveNext(out var uid, out var comp, out var gameRule))
+ {
+ _antagSelection.AttemptStartGameRule(ev, uid, comp.MinPlayers, gameRule);
+ }
+ }
+
+ private void OnPlayerJobAssigned(RulePlayerJobsAssignedEvent ev)
+ {
+ var query = QueryActiveRules();
+ while (query.MoveNext(out _, out var comp, out _))
+ {
+ _antagSelection.EligiblePlayers(comp.RevPrototypeId, comp.MaxHeadRevs, comp.PlayersPerHeadRev, comp.HeadRevStartSound,
+ "head-rev-role-greeting", "#5e9cff", out var chosen);
+ GiveHeadRev(chosen, comp.RevPrototypeId, comp);
+ }
+ }
+
+ private void GiveHeadRev(List chosen, string antagProto, RevolutionaryRuleComponent comp)
+ {
+ foreach (var headRev in chosen)
+ {
+ RemComp(headRev);
+
+ var inCharacterName = MetaData(headRev).EntityName;
+ if (_mind.TryGetMind(headRev, out var mindId, out var mind))
+ {
+ if (!_role.MindHasRole(mindId))
+ {
+ _role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = antagProto });
+ }
+ if (mind.Session != null)
+ {
+ comp.HeadRevs.Add(inCharacterName, mindId);
+ }
+ }
+
+ _antagSelection.GiveAntagBagGear(headRev, comp.StartingGear);
+ EnsureComp(headRev);
+ EnsureComp(headRev);
+ }
+ }
+
+ ///
+ /// Called when a Head Rev uses a flash in melee to convert somebody else.
+ ///
+ public void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref AfterFlashedEvent ev)
+ {
+ TryComp(ev.Target, out var alwaysConvertibleComp);
+ var alwaysConvertible = alwaysConvertibleComp != null;
+
+ if (!_mind.TryGetMind(ev.Target, out var mindId, out var mind) && !alwaysConvertible)
+ return;
+
+ if (HasComp(ev.Target) ||
+ HasComp(ev.Target) ||
+ !HasComp(ev.Target) &&
+ !alwaysConvertible ||
+ !_mobState.IsAlive(ev.Target) ||
+ HasComp(ev.Target))
+ {
+ return;
+ }
+
+ _npcFaction.AddFaction(ev.Target, RevolutionaryNpcFaction);
+ EnsureComp(ev.Target);
+ _stun.TryParalyze(ev.Target, comp.StunTime, true);
+ if (ev.User != null)
+ {
+ _adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
+ }
+
+ if (mindId == default || !_role.MindHasRole(mindId))
+ {
+ _role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = RevolutionaryAntagRole });
+ }
+ if (mind?.Session != null)
+ {
+ 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.ConnectedClient, Color.Red);
+ }
+ }
+
+ public void OnHeadRevAdmin(EntityUid mindId, MindComponent? mind = null)
+ {
+ if (!Resolve(mindId, ref mind))
+ return;
+
+ var revRule = EntityQuery().FirstOrDefault();
+ if (revRule == null)
+ {
+ GameTicker.StartGameRule("Revolutionary", out var ruleEnt);
+ revRule = Comp(ruleEnt);
+ }
+
+ if (!HasComp(mind.OwnedEntity))
+ {
+ if (mind.OwnedEntity != null)
+ {
+ var player = new List
+ {
+ 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.ConnectedClient, Color.FromHex("#5e9cff"));
+ }
+ }
+ }
+ private void OnCommandMobStateChanged(EntityUid uid, CommandStaffComponent comp, MobStateChangedEvent ev)
+ {
+ if (ev.NewMobState == MobState.Dead || ev.NewMobState == MobState.Invalid)
+ CheckCommandLose();
+ }
+
+ ///
+ /// Checks if all of command is dead and if so will remove all sec and command jobs if there were any left.
+ ///
+ private bool CheckCommandLose()
+ {
+ var commandList = new List();
+
+ var heads = AllEntityQuery();
+ while (heads.MoveNext(out var id, out _))
+ {
+ commandList.Add(id);
+ }
+
+ return _antagSelection.IsGroupDead(commandList, true);
+ }
+
+ private void OnHeadRevMobStateChanged(EntityUid uid, HeadRevolutionaryComponent comp, MobStateChangedEvent ev)
+ {
+ if (ev.NewMobState == MobState.Dead || ev.NewMobState == MobState.Invalid)
+ CheckRevsLose();
+ }
+
+ ///
+ /// Checks if all the Head Revs are dead and if so will deconvert all regular revs.
+ ///
+ private bool CheckRevsLose()
+ {
+ var stunTime = TimeSpan.FromSeconds(4);
+ var headRevList = new List();
+
+ var headRevs = AllEntityQuery();
+ while (headRevs.MoveNext(out var uid, out _, out _))
+ {
+ headRevList.Add(uid);
+ }
+
+ // If no Head Revs are alive all normal Revs will lose their Rev status and rejoin Nanotrasen
+ if (_antagSelection.IsGroupDead(headRevList, false))
+ {
+ var rev = AllEntityQuery();
+ while (rev.MoveNext(out var uid, out _))
+ {
+ if (!HasComp(uid))
+ {
+ _npcFaction.RemoveFaction(uid, RevolutionaryNpcFaction);
+ _stun.TryParalyze(uid, stunTime, true);
+ RemCompDeferred(uid);
+ _popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid);
+ _adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying.");
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private static readonly string[] Outcomes =
+ {
+ // revs survived and heads survived... how
+ "rev-reverse-stalemate",
+ // revs won and heads died
+ "rev-won",
+ // revs lost and heads survived
+ "rev-lost",
+ // revs lost and heads died
+ "rev-stalemate"
+ };
+}
diff --git a/Content.Server/Mindshield/MindShieldSystem.cs b/Content.Server/Mindshield/MindShieldSystem.cs
new file mode 100644
index 0000000000..714055bd94
--- /dev/null
+++ b/Content.Server/Mindshield/MindShieldSystem.cs
@@ -0,0 +1,63 @@
+using Content.Shared.Mindshield.Components;
+using Content.Shared.Revolutionary.Components;
+using Content.Server.Popups;
+using Content.Shared.Database;
+using Content.Server.Administration.Logs;
+using Content.Server.Mind;
+using Content.Shared.Implants;
+using Content.Shared.Tag;
+using Content.Server.Roles;
+using Content.Shared.Implants.Components;
+
+namespace Content.Server.Mindshield;
+
+///
+/// System used for checking if the implanted is a Rev or Head Rev.
+///
+public sealed class MindShieldSystem : EntitySystem
+{
+ [Dependency] private readonly IAdminLogManager _adminLogManager = default!;
+ [Dependency] private readonly RoleSystem _roleSystem = default!;
+ [Dependency] private readonly MindSystem _mindSystem = default!;
+ [Dependency] private readonly TagSystem _tag = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+
+ [ValidatePrototypeId]
+ public const string MindShieldTag = "MindShield";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(ImplantCheck);
+ }
+
+ ///
+ /// Checks if the implant was a mindshield or not
+ ///
+ public void ImplantCheck(EntityUid uid, SubdermalImplantComponent comp, ref ImplantImplantedEvent ev)
+ {
+ if (_tag.HasTag(ev.Implant, MindShieldTag) && ev.Implanted != null)
+ {
+ EnsureComp(ev.Implanted.Value);
+ MindShieldRemovalCheck(ev.Implanted, ev.Implant);
+ }
+ }
+
+ ///
+ /// Checks if the implanted person was a Rev or Head Rev and remove role or destroy mindshield respectively.
+ ///
+ public void MindShieldRemovalCheck(EntityUid? implanted, EntityUid implant)
+ {
+ if (HasComp(implanted) && !HasComp(implanted))
+ {
+ _mindSystem.TryGetMind(implanted.Value, out var mindId, out _);
+ _adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(implanted.Value)} was deconverted due to being implanted with a Mindshield.");
+ _roleSystem.MindTryRemoveRole(mindId);
+ }
+ else if (HasComp(implanted))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("head-rev-break-mindshield"), implanted.Value);
+ QueueDel(implant);
+ }
+ }
+}
diff --git a/Content.Server/Revolutionary/Components/CommandStaffComponent.cs b/Content.Server/Revolutionary/Components/CommandStaffComponent.cs
new file mode 100644
index 0000000000..8e42f41cb3
--- /dev/null
+++ b/Content.Server/Revolutionary/Components/CommandStaffComponent.cs
@@ -0,0 +1,12 @@
+using Content.Server.GameTicking.Rules;
+
+namespace Content.Server.Revolutionary.Components;
+
+///
+/// Given to heads at round start for Revs. Used for tracking if heads died or not.
+///
+[RegisterComponent, Access(typeof(RevolutionaryRuleSystem))]
+public sealed partial class CommandStaffComponent : Component
+{
+
+}
diff --git a/Content.Server/Roles/RevolutionaryRoleComponent.cs b/Content.Server/Roles/RevolutionaryRoleComponent.cs
new file mode 100644
index 0000000000..fa06cc3191
--- /dev/null
+++ b/Content.Server/Roles/RevolutionaryRoleComponent.cs
@@ -0,0 +1,12 @@
+using Content.Shared.Roles;
+
+namespace Content.Server.Roles;
+
+///
+/// Added to mind entities to tag that they are a Revolutionary.
+///
+[RegisterComponent]
+public sealed partial class RevolutionaryRoleComponent : AntagonistRoleComponent
+{
+
+}
diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs
index 12cfb0e666..6043f3fbf9 100644
--- a/Content.Server/RoundEnd/RoundEndSystem.cs
+++ b/Content.Server/RoundEnd/RoundEndSystem.cs
@@ -224,7 +224,19 @@ namespace Content.Server.RoundEnd
Timer.Spawn(countdownTime.Value, AfterEndRoundRestart, _countdownTokenSource.Token);
}
- public void DoRoundEndBehavior(RoundEndBehavior behavior, TimeSpan time, string sender, string textCall, string textAnnounce)
+ ///
+ /// Starts a behavior to end the round
+ ///
+ /// The way in which the round will end
+ ///
+ ///
+ ///
+ ///
+ public void DoRoundEndBehavior(RoundEndBehavior behavior,
+ TimeSpan time,
+ string sender = "comms-console-announcement-title-centcom",
+ string textCall = "round-end-system-shuttle-called-announcement",
+ string textAnnounce = "round-end-system-shuttle-already-called-announcement")
{
switch (behavior)
{
diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs
index 55db027225..2d68f1cdaa 100644
--- a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs
+++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs
@@ -1,10 +1,11 @@
-using System.Linq;
+using System.Linq;
using Content.Shared.Actions;
using Content.Shared.Implants.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Mobs;
using Content.Shared.Tag;
+using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Network;
@@ -52,6 +53,9 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
}
}
}
+
+ var ev = new ImplantImplantedEvent(uid, component.ImplantedEntity.Value);
+ RaiseLocalEvent(uid, ref ev);
}
private void OnRemoveAttempt(EntityUid uid, SubdermalImplantComponent component, ContainerGettingRemovedAttemptEvent args)
@@ -128,7 +132,7 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
///
/// the implanted entity
/// the implant
- /// the implant component
+ [PublicAPI]
public void ForceRemove(EntityUid target, EntityUid implant)
{
if (!TryComp(target, out var implanted))
@@ -144,6 +148,7 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
/// Removes and deletes implants by force
///
/// The entity to have implants removed
+ [PublicAPI]
public void WipeImplants(EntityUid target)
{
if (!TryComp(target, out var implanted))
@@ -180,3 +185,23 @@ public sealed class ImplantRelayEvent where T : notnull
Event = ev;
}
}
+
+///
+/// Event that is raised whenever someone is implanted with any given implant.
+/// Raised on the the implant entity.
+///
+///
+/// implant implant implant implant
+///
+[ByRefEvent]
+public readonly struct ImplantImplantedEvent
+{
+ public readonly EntityUid Implant;
+ public readonly EntityUid? Implanted;
+
+ public ImplantImplantedEvent(EntityUid implant, EntityUid? implanted)
+ {
+ Implant = implant;
+ Implanted = implanted;
+ }
+}
diff --git a/Content.Shared/Mindshield/Components/MindShieldComponent.cs b/Content.Shared/Mindshield/Components/MindShieldComponent.cs
new file mode 100644
index 0000000000..1cc21d14bd
--- /dev/null
+++ b/Content.Shared/Mindshield/Components/MindShieldComponent.cs
@@ -0,0 +1,12 @@
+using Content.Shared.Revolutionary;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Mindshield.Components;
+
+///
+/// If a player has a Mindshield they will get this component to prevent conversion.
+///
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))]
+public sealed partial class MindShieldComponent : Component
+{
+}
diff --git a/Content.Shared/Revolutionary/Components/AlwaysRevolutionaryConvertibleComponent.cs b/Content.Shared/Revolutionary/Components/AlwaysRevolutionaryConvertibleComponent.cs
new file mode 100644
index 0000000000..694c6d2f06
--- /dev/null
+++ b/Content.Shared/Revolutionary/Components/AlwaysRevolutionaryConvertibleComponent.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Revolutionary.Components;
+
+///
+/// Component used for allowing non-humans to be converted. (Mainly monkeys)
+///
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))]
+public sealed partial class AlwaysRevolutionaryConvertibleComponent : Component
+{
+
+}
diff --git a/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs b/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs
new file mode 100644
index 0000000000..48d7c23097
--- /dev/null
+++ b/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs
@@ -0,0 +1,24 @@
+using Robust.Shared.GameStates;
+using Content.Shared.StatusIcon;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Revolutionary.Components;
+
+///
+/// Component used for marking a Head Rev for conversion and winning/losing.
+///
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))]
+public sealed partial class HeadRevolutionaryComponent : Component
+{
+ ///
+ /// The status icon corresponding to the head revolutionary.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public ProtoId HeadRevStatusIcon = "HeadRevolutionaryFaction";
+
+ ///
+ /// How long the stun will last after the user is converted.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan StunTime = TimeSpan.FromSeconds(3);
+}
diff --git a/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs b/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs
new file mode 100644
index 0000000000..e55c87786b
--- /dev/null
+++ b/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs
@@ -0,0 +1,18 @@
+using Robust.Shared.GameStates;
+using Content.Shared.StatusIcon;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Revolutionary.Components;
+
+///
+/// Used for marking regular revs as well as storing icon prototypes so you can see fellow revs.
+///
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))]
+public sealed partial class RevolutionaryComponent : Component
+{
+ ///
+ /// The status icon prototype displayed for revolutionaries
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public ProtoId RevStatusIcon = "RevolutionaryFaction";
+}
diff --git a/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs b/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs
new file mode 100644
index 0000000000..993c74d19f
--- /dev/null
+++ b/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs
@@ -0,0 +1,38 @@
+using Content.Shared.Revolutionary.Components;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Mindshield.Components;
+using Content.Shared.Popups;
+using Content.Shared.Stunnable;
+
+namespace Content.Shared.Revolutionary;
+
+public sealed class SharedRevolutionarySystem : EntitySystem
+{
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly SharedStunSystem _sharedStun = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(MindShieldImplanted);
+ }
+
+ ///
+ /// When the mindshield is implanted in the rev it will popup saying they were deconverted. In Head Revs it will remove the mindshield component.
+ ///
+ private void MindShieldImplanted(EntityUid uid, MindShieldComponent comp, ComponentInit init)
+ {
+ if (HasComp(uid) && !HasComp(uid))
+ {
+ var stunTime = TimeSpan.FromSeconds(4);
+ var name = Identity.Entity(uid, EntityManager);
+ RemComp(uid);
+ _sharedStun.TryParalyze(uid, stunTime, true);
+ _popupSystem.PopupEntity(Loc.GetString("rev-break-control", ("name", name)), uid);
+ }
+ else if (HasComp(uid))
+ {
+ RemCompDeferred(uid);
+ }
+ }
+}
diff --git a/Resources/Locale/en-US/administration/antag.ftl b/Resources/Locale/en-US/administration/antag.ftl
index ec428b0580..3d098c1c54 100644
--- a/Resources/Locale/en-US/administration/antag.ftl
+++ b/Resources/Locale/en-US/administration/antag.ftl
@@ -2,9 +2,11 @@ verb-categories-antag = Antag ctrl
admin-verb-make-traitor = Make the target into a traitor.
admin-verb-make-zombie = Zombifies the target immediately.
admin-verb-make-nuclear-operative = Make target a into lone Nuclear Operative.
-admin-verb-make-pirate = Make the target into a pirate. Note that this doesn't configure the game rule.
+admin-verb-make-pirate = Make the target into a pirate. Note this doesn't configure the game rule.
+admin-verb-make-head-rev = Make the target into a Head Revolutionary.
admin-verb-text-make-traitor = Make Traitor
admin-verb-text-make-zombie = Make Zombie
admin-verb-text-make-nuclear-operative = Make Nuclear Operative
admin-verb-text-make-pirate = Make Pirate
+admin-verb-text-make-head-rev = Make Head Rev
diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-revolutionary.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-revolutionary.ftl
new file mode 100644
index 0000000000..53aea2a20b
--- /dev/null
+++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-revolutionary.ftl
@@ -0,0 +1,53 @@
+## Rev Head
+
+roles-antag-rev-head-name = Head Revolutionary
+roles-antag-rev-head-objective = Your objective is to take over the station by converting people to your cause and kill all Command staff on station.
+
+head-rev-role-greeting =
+ You are a Head Revolutionary.
+ You are tasked with taking over the station by any means necessary.
+ The Syndicate has sponsored you with a flash that converts the crew to your side.
+ Beware, this won't work on Security, Command, or those wearing sunglasses.
+ Viva la revolución!
+
+head-rev-initial = [color=#5e9cff]{$name}[/color] ([color=gray]{$username}[/color]) was one of the Head Revolutionaries.
+
+head-rev-initial-count = {$initialCount ->
+ [one] There was one Head Revolutionary:
+ *[other] There were {$initialCount} Head Revolutionaries:
+}
+
+head-rev-break-mindshield = The Mindshield was destroyed!
+
+## Rev
+
+roles-antag-rev-name = Revolutionary
+roles-antag-rev-objective = Your objective is to ensure the safety and follow the orders of the Head Revolutionaries as well as killing all Command staff on station.
+
+rev-break-control = {$name} has remembered their true allegiance!
+
+rev-role-greeting =
+ You are a Revolutionary.
+ You are tasked with taking over the station and protecting the Head Revolutionaries.
+ Eliminate all of the command staff.
+ Viva la revolución!
+
+## General
+
+rev-title = Revolutionaries
+rev-description = Revolutionaries are among us.
+
+rev-not-enough-ready-players = Not enough players readied up for the game. There were {$readyPlayersCount} players readied up out of {$minimumPlayers} needed. Can't start a Revolution.
+rev-no-one-ready = No players readied up! Can't start a Revolution.
+
+rev-all-heads-dead = All the heads are dead, now finish up the rest of the crew!
+
+rev-won = The Head Revs survived and killed all of command.
+
+rev-lost = Command survived and killed all of the Head Revs.
+
+rev-stalemate = All of the Head Revs died and so did all of command. We'll call it a draw.
+
+rev-reverse-stalemate = I think the Head Revs and command forgot to fight because they are both still alive.
+
+
diff --git a/Resources/Locale/en-US/prototypes/catalog/cargo/cargo-medical.ftl b/Resources/Locale/en-US/prototypes/catalog/cargo/cargo-medical.ftl
index c03883a73e..42c5880ec0 100644
--- a/Resources/Locale/en-US/prototypes/catalog/cargo/cargo-medical.ftl
+++ b/Resources/Locale/en-US/prototypes/catalog/cargo/cargo-medical.ftl
@@ -4,6 +4,9 @@ ent-MedicalSupplies = { ent-CrateMedicalSupplies }
ent-MedicalChemistrySupplies = { ent-CrateChemistrySupplies }
.desc = { ent-CrateChemistrySupplies.desc }
+ent-MedicalMindShieldImplants = { ent-MedicalMindShieldImplants }
+ .desc = { ent-MedicalMindShieldImplants.desc }
+
ent-EmergencyBurnKit = { ent-CrateEmergencyBurnKit }
.desc = { ent-CrateEmergencyBurnKit.desc }
@@ -29,4 +32,4 @@ ent-ChemistryS = { ent-CrateChemistryS }
.desc = { ent-CrateChemistryS.desc }
ent-ChemistryD = { ent-CrateChemistryD }
- .desc = { ent-CrateChemistryD.desc }
\ No newline at end of file
+ .desc = { ent-CrateChemistryD.desc }
diff --git a/Resources/Locale/en-US/prototypes/catalog/fills/crates/medical-crates.ftl b/Resources/Locale/en-US/prototypes/catalog/fills/crates/medical-crates.ftl
index e7cd8346f5..840965cf1d 100644
--- a/Resources/Locale/en-US/prototypes/catalog/fills/crates/medical-crates.ftl
+++ b/Resources/Locale/en-US/prototypes/catalog/fills/crates/medical-crates.ftl
@@ -6,6 +6,9 @@ ent-CrateMedicalSupplies = Medical supplies crate
ent-CrateChemistrySupplies = Chemistry supplies crate
.desc = Basic chemistry supplies.
+ent-CrateMindShieldImplants = MindShield implant crate
+ .desc = Crate filled with 3 MindShield implants.
+
ent-CrateMedicalSurgery = Surgical supplies crate
.desc = Surgical instruments.
diff --git a/Resources/Locale/en-US/round-end/round-end-system.ftl b/Resources/Locale/en-US/round-end/round-end-system.ftl
index f85bee0323..f86851506b 100644
--- a/Resources/Locale/en-US/round-end/round-end-system.ftl
+++ b/Resources/Locale/en-US/round-end/round-end-system.ftl
@@ -1,6 +1,7 @@
## RoundEndSystem
round-end-system-shuttle-called-announcement = An emergency shuttle has been sent. ETA: {$time} {$units}.
+round-end-system-shuttle-already-called-announcement = An emergency shuttle has already been sent.
round-end-system-shuttle-auto-called-announcement = An automatic crew shift change shuttle has been sent. ETA: {$time} {$units}. Recall the shuttle to extend the shift.
round-end-system-shuttle-recalled-announcement = The emergency shuttle has been recalled.
round-end-system-round-restart-eta-announcement = Restarting the round in {$time} {$units}...
diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_medical.yml b/Resources/Prototypes/Catalog/Cargo/cargo_medical.yml
index 1dc572e204..94927c1a79 100644
--- a/Resources/Prototypes/Catalog/Cargo/cargo_medical.yml
+++ b/Resources/Prototypes/Catalog/Cargo/cargo_medical.yml
@@ -98,6 +98,16 @@
category: Medical
group: market
+- type: cargoProduct
+ id: MedicalMindShieldImplants
+ icon:
+ sprite: Objects/Specific/Chemistry/syringe.rsi
+ state: syringe_base0
+ product: CrateMindShieldImplants
+ cost: 5000
+ category: Medical
+ group: market
+
- type: cargoProduct
id: ChemistryP
icon:
diff --git a/Resources/Prototypes/Catalog/Fills/Crates/medical.yml b/Resources/Prototypes/Catalog/Fills/Crates/medical.yml
index a9795ef02d..a487138afd 100644
--- a/Resources/Prototypes/Catalog/Fills/Crates/medical.yml
+++ b/Resources/Prototypes/Catalog/Fills/Crates/medical.yml
@@ -28,6 +28,15 @@
- id: BoxBottle
amount: 2
+- type: entity
+ id: CrateMindShieldImplants
+ parent: CrateMedical
+ components:
+ - type: StorageFill
+ contents:
+ - id: MindShieldImplanter
+ amount: 3
+
- type: entity
id: CrateMedicalSurgery
parent: CrateSurgery
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
index bd112f855b..32e2e48ad6 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
@@ -876,6 +876,7 @@
clumsySound:
path: /Audio/Animals/monkey_scream.ogg
- type: IdExaminable
+ - type: AlwaysRevolutionaryConvertible
- type: entity
name: guidebook monkey
diff --git a/Resources/Prototypes/Entities/Objects/Misc/implanters.yml b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml
index a3d5fbef39..8cb55e386e 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/implanters.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml
@@ -206,3 +206,13 @@
components:
- type: Implanter
implant: DeathRattleImplant
+
+# Security and Command implanters
+
+- type: entity
+ id: MindShieldImplanter
+ name: mind-shield implanter
+ parent: BaseImplantOnlyImplanter
+ components:
+ - type: Implanter
+ implant: MindShieldImplant
diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
index 772dd45029..6632010a79 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
@@ -256,3 +256,17 @@
- Dead
- type: Rattle
+# Sec and Command implants
+
+- type: entity
+ parent: BaseSubdermalImplant
+ id: MindShieldImplant
+ name: mind-shield implant
+ description: This implant will ensure loyalty to Nanotrasen and prevent mind control devices.
+ noSpawn: true
+ components:
+ - type: SubdermalImplant
+ permanent: true
+ - type: Tag
+ tags:
+ - MindShield
diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml
index 0fc6de32ec..a0fc66bb81 100644
--- a/Resources/Prototypes/GameRules/roundstart.yml
+++ b/Resources/Prototypes/GameRules/roundstart.yml
@@ -75,6 +75,13 @@
components:
- type: TraitorRule
+- type: entity
+ id: Revolutionary
+ parent: BaseGameRule
+ noSpawn: true
+ components:
+ - type: RevolutionaryRule
+
- type: entity
id: Sandbox
parent: BaseGameRule
diff --git a/Resources/Prototypes/Roles/Antags/revolutionary.yml b/Resources/Prototypes/Roles/Antags/revolutionary.yml
new file mode 100644
index 0000000000..c5e6cb8149
--- /dev/null
+++ b/Resources/Prototypes/Roles/Antags/revolutionary.yml
@@ -0,0 +1,13 @@
+- type: antag
+ id: HeadRev
+ name: roles-antag-rev-head-name
+ antagonist: true
+ setPreference: true
+ objective: roles-antag-rev-head-objective
+
+- type: antag
+ id: Rev
+ name: roles-antag-rev-name
+ antagonist: true
+ setPreference: false
+ objective: roles-antag-rev-objective
diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml
index 3341c94269..d2dfafd324 100644
--- a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml
+++ b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml
@@ -14,7 +14,7 @@
department: Cargo
time: 36000 #10 hours
- !type:OverallPlaytimeRequirement
- time: 144000 #40 hrs
+ time: 144000 #40 hrs
weight: 10
startingGear: QuartermasterGear
icon: "JobIconQuarterMaster"
@@ -27,6 +27,12 @@
- Maintenance
- External
- Command
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
+ - !type:AddComponentSpecial
+ components:
+ - type: CommandStaff
- type: startingGear
id: QuartermasterGear
diff --git a/Resources/Prototypes/Roles/Jobs/Command/captain.yml b/Resources/Prototypes/Roles/Jobs/Command/captain.yml
index e2956aa394..87c743463c 100644
--- a/Resources/Prototypes/Roles/Jobs/Command/captain.yml
+++ b/Resources/Prototypes/Roles/Jobs/Command/captain.yml
@@ -25,6 +25,12 @@
canBeAntag: false
accessGroups:
- AllAccess
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
+ - !type:AddComponentSpecial
+ components:
+ - type: CommandStaff
- type: startingGear
id: CaptainGear
diff --git a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml
index 2768452e3b..2a42d8e0c4 100644
--- a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml
+++ b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml
@@ -47,6 +47,12 @@
- Cargo
- Atmospherics
- Medical
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
+ - !type:AddComponentSpecial
+ components:
+ - type: CommandStaff
- type: startingGear
id: HoPGear
diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml
index 713117b10b..bddbd8f2cd 100644
--- a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml
+++ b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml
@@ -14,7 +14,7 @@
department: Engineering
time: 36000 #10 hrs
- !type:OverallPlaytimeRequirement
- time: 144000 #40 hrs
+ time: 144000 #40 hrs
weight: 10
startingGear: ChiefEngineerGear
icon: "JobIconChiefEngineer"
@@ -28,6 +28,12 @@
- External
- ChiefEngineer
- Atmospherics
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
+ - !type:AddComponentSpecial
+ components:
+ - type: CommandStaff
- type: startingGear
id: ChiefEngineerGear
diff --git a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml
index 3a633ab3f1..146fa3ed96 100644
--- a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml
+++ b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml
@@ -326,7 +326,13 @@
satchel: ClothingBackpackSatchelBrigmedicFilled
duffelbag: ClothingBackpackDuffelBrigmedicFilled
- #Gladiator with spear
+#Head Rev Gear
+- type: startingGear
+ id: HeadRevGear
+ equipment:
+ pocket2: Flash
+
+#Gladiator with spear
- type: startingGear
id: GladiatorGear
equipment:
@@ -335,7 +341,7 @@
head: ClothingHeadHatGladiator
shoes: ClothingShoesCult
- #Ash Walker
+#Ash Walker
- type: startingGear
id: AshWalker
equipment:
diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml
index d9083d2c8a..785f7ad626 100644
--- a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml
+++ b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml
@@ -16,7 +16,7 @@
department: Medical
time: 36000 #10 hrs
- !type:OverallPlaytimeRequirement
- time: 144000 #40 hrs
+ time: 144000 #40 hrs
weight: 10
startingGear: CMOGear
icon: "JobIconChiefMedicalOfficer"
@@ -29,6 +29,12 @@
- Maintenance
- Chemistry
- ChiefMedicalOfficer
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
+ - !type:AddComponentSpecial
+ components:
+ - type: CommandStaff
- type: startingGear
id: CMOGear
diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml
index b5ad3292e9..964bffb3a2 100644
--- a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml
+++ b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml
@@ -8,7 +8,7 @@
department: Science
time: 36000 #10 hrs
- !type:OverallPlaytimeRequirement
- time: 144000 #40 hrs
+ time: 144000 #40 hrs
weight: 10
startingGear: ResearchDirectorGear
icon: "JobIconResearchDirector"
@@ -20,6 +20,12 @@
- Command
- Maintenance
- ResearchDirector
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
+ - !type:AddComponentSpecial
+ components:
+ - type: CommandStaff
- type: startingGear
id: ResearchDirectorGear
diff --git a/Resources/Prototypes/Roles/Jobs/Security/detective.yml b/Resources/Prototypes/Roles/Jobs/Security/detective.yml
index 00c971c2c6..feef05dc87 100644
--- a/Resources/Prototypes/Roles/Jobs/Security/detective.yml
+++ b/Resources/Prototypes/Roles/Jobs/Security/detective.yml
@@ -17,6 +17,9 @@
- Maintenance
- Service
- Detective
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
- type: startingGear
id: DetectiveGear
diff --git a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml
index 7b70149d96..58a96d1a5b 100644
--- a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml
+++ b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml
@@ -14,7 +14,7 @@
department: Security
time: 108000 # 30 hrs
- !type:OverallPlaytimeRequirement
- time: 144000 #40 hrs
+ time: 144000 #40 hrs
weight: 10
startingGear: HoSGear
icon: "JobIconHeadOfSecurity"
@@ -31,6 +31,12 @@
- Service
- External
- Detective
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
+ - !type:AddComponentSpecial
+ components:
+ - type: CommandStaff
- type: startingGear
id: HoSGear
diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml b/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml
index b58009ae86..30ab144860 100644
--- a/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml
+++ b/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml
@@ -1,4 +1,4 @@
-- type: job
+- type: job
id: SecurityCadet
name: job-name-cadet
description: job-description-cadet
@@ -18,6 +18,9 @@
- Security
- Brig
- Maintenance
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
- type: startingGear
id: SecurityCadetGear
diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml
index e38256b8a1..01cf5b44b9 100644
--- a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml
+++ b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml
@@ -17,6 +17,9 @@
- Maintenance
- Service
- External
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
- type: startingGear
id: SecurityOfficerGear
diff --git a/Resources/Prototypes/Roles/Jobs/Security/senior_officer.yml b/Resources/Prototypes/Roles/Jobs/Security/senior_officer.yml
index 5da1347e14..46abc8664e 100644
--- a/Resources/Prototypes/Roles/Jobs/Security/senior_officer.yml
+++ b/Resources/Prototypes/Roles/Jobs/Security/senior_officer.yml
@@ -26,6 +26,9 @@
- Maintenance
- Service
- External
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
- type: startingGear
id: SeniorOfficerGear
diff --git a/Resources/Prototypes/Roles/Jobs/Security/warden.yml b/Resources/Prototypes/Roles/Jobs/Security/warden.yml
index 53ba868dcf..46142d1550 100644
--- a/Resources/Prototypes/Roles/Jobs/Security/warden.yml
+++ b/Resources/Prototypes/Roles/Jobs/Security/warden.yml
@@ -19,6 +19,9 @@
- Brig
- External
- Detective
+ special:
+ - !type:AddImplantSpecial
+ implants: [ MindShieldImplant ]
- type: startingGear
id: WardenGear
diff --git a/Resources/Prototypes/StatusIcon/antag.yml b/Resources/Prototypes/StatusIcon/antag.yml
index a4690caf06..564d29a35b 100644
--- a/Resources/Prototypes/StatusIcon/antag.yml
+++ b/Resources/Prototypes/StatusIcon/antag.yml
@@ -1,6 +1,20 @@
-- type: statusIcon
+- type: statusIcon
id: ZombieFaction
priority: 11
icon:
sprite: Interface/Misc/job_icons.rsi
state: Zombie
+
+- type: statusIcon
+ id: RevolutionaryFaction
+ priority: 11
+ icon:
+ sprite: Interface/Misc/job_icons.rsi
+ state: Revolutionary
+
+- type: statusIcon
+ id: HeadRevolutionaryFaction
+ priority: 11
+ icon:
+ sprite: Interface/Misc/job_icons.rsi
+ state: HeadRevolutionary
diff --git a/Resources/Prototypes/ai_factions.yml b/Resources/Prototypes/ai_factions.yml
index 9c8f001577..3dfb35c7a6 100644
--- a/Resources/Prototypes/ai_factions.yml
+++ b/Resources/Prototypes/ai_factions.yml
@@ -6,6 +6,7 @@
- Xeno
- PetsNT
- Zombie
+ - Revolutionary
- type: npcFaction
id: NanoTrasen
@@ -14,6 +15,7 @@
- Syndicate
- Xeno
- Zombie
+ - Revolutionary
- type: npcFaction
id: Mouse
@@ -39,6 +41,7 @@
- Passive
- PetsNT
- Zombie
+ - Revolutionary
- type: npcFaction
id: SimpleNeutral
@@ -60,6 +63,7 @@
- Passive
- PetsNT
- Zombie
+ - Revolutionary
- type: npcFaction
id: Zombie
@@ -70,3 +74,12 @@
- Syndicate
- Passive
- PetsNT
+ - Revolutionary
+
+- type: npcFaction
+ id: Revolutionary
+ hostile:
+ - NanoTrasen
+ - Zombie
+ - SimpleHostile
+ - Dragon
diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml
index e12822b2ea..b42b2e49f9 100644
--- a/Resources/Prototypes/game_presets.yml
+++ b/Resources/Prototypes/game_presets.yml
@@ -76,6 +76,19 @@
- Nukeops
- BasicStationEventScheduler
+- type: gamePreset
+ id: Revolutionary
+ alias:
+ - rev
+ - revs
+ - revolutionaries
+ name: rev-title
+ description: rev-description
+ showInVote: false
+ rules:
+ - Revolutionary
+ - BasicStationEventScheduler
+
- type: gamePreset
id: Zombie
alias:
diff --git a/Resources/Prototypes/secret_weights.yml b/Resources/Prototypes/secret_weights.yml
index b23daa2a4e..6c7cc12ce1 100644
--- a/Resources/Prototypes/secret_weights.yml
+++ b/Resources/Prototypes/secret_weights.yml
@@ -1,6 +1,8 @@
- type: weightedRandom
id: Secret
weights:
- Nukeops: 0.25
- Traitor: 0.65
+ Nukeops: 0.15
+ Traitor: 0.60
Zombie: 0.10
+ Revolutionary: 0.15
+
\ No newline at end of file
diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml
index 0750117305..d01f8c1ddf 100644
--- a/Resources/Prototypes/tags.yml
+++ b/Resources/Prototypes/tags.yml
@@ -1057,3 +1057,5 @@
- type: Tag
id: ModularReceiver
+- type: Tag
+ id: MindShield
diff --git a/Resources/Textures/Interface/Misc/job_icons.rsi/HeadRevolutionary.png b/Resources/Textures/Interface/Misc/job_icons.rsi/HeadRevolutionary.png
new file mode 100644
index 0000000000..21f5c12021
Binary files /dev/null and b/Resources/Textures/Interface/Misc/job_icons.rsi/HeadRevolutionary.png differ
diff --git a/Resources/Textures/Interface/Misc/job_icons.rsi/Revolutionary.png b/Resources/Textures/Interface/Misc/job_icons.rsi/Revolutionary.png
new file mode 100644
index 0000000000..7dab7886ec
Binary files /dev/null and b/Resources/Textures/Interface/Misc/job_icons.rsi/Revolutionary.png differ
diff --git a/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json b/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json
index 0ead115802..2d71fcd056 100644
--- a/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json
+++ b/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json
@@ -1,7 +1,8 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
- "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/e71d6c4fba5a51f99b81c295dcaec4fc2f58fb19/icons/mob/screen1.dmi | Brigmedic icon made by PuroSlavKing (Github) | Zombie icon made by RamZ | Zookeper by netwy (discort)",
+ "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/e71d6c4fba5a51f99b81c295dcaec4fc2f58fb19/icons/mob/screen1.dmi | Brigmedic icon made by PuroSlavKing (Github) | Zombie icon made by RamZ | Zookeper by netwy (discort) | Rev and Head Rev icon taken from https://tgstation13.org/wiki/HUD and edited by coolmankid12345 (Discord)",
+
"size": {
"x": 8,
"y": 8
@@ -162,6 +163,12 @@
},
{
"name": "SeniorOfficer"
+ },
+ {
+ "name": "Revolutionary"
+ },
+ {
+ "name": "HeadRevolutionary"
}
]
}