]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Revolutionaries (#18477)
authorcoolmankid12345 <55817627+coolmankid12345@users.noreply.github.com>
Thu, 5 Oct 2023 01:47:33 +0000 (21:47 -0400)
committerGitHub <noreply@github.com>
Thu, 5 Oct 2023 01:47:33 +0000 (18:47 -0700)
Co-authored-by: coolmankid12345 <coolmankid12345@users.noreply.github.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
51 files changed:
Content.Client/Antag/AntagStatusIconSystem.cs [new file with mode: 0644]
Content.Client/Revolutionary/RevolutionarySystem.cs [new file with mode: 0644]
Content.Client/Zombies/ZombieSystem.cs
Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
Content.Server/Antag/AntagSelectionSystem.cs [new file with mode: 0644]
Content.Server/Flash/FlashSystem.cs
Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs [new file with mode: 0644]
Content.Server/Mindshield/MindShieldSystem.cs [new file with mode: 0644]
Content.Server/Revolutionary/Components/CommandStaffComponent.cs [new file with mode: 0644]
Content.Server/Roles/RevolutionaryRoleComponent.cs [new file with mode: 0644]
Content.Server/RoundEnd/RoundEndSystem.cs
Content.Shared/Implants/SharedSubdermalImplantSystem.cs
Content.Shared/Mindshield/Components/MindShieldComponent.cs [new file with mode: 0644]
Content.Shared/Revolutionary/Components/AlwaysRevolutionaryConvertibleComponent.cs [new file with mode: 0644]
Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs [new file with mode: 0644]
Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs [new file with mode: 0644]
Content.Shared/Revolutionary/SharedRevolutionarySystem.cs [new file with mode: 0644]
Resources/Locale/en-US/administration/antag.ftl
Resources/Locale/en-US/game-ticking/game-presets/preset-revolutionary.ftl [new file with mode: 0644]
Resources/Locale/en-US/prototypes/catalog/cargo/cargo-medical.ftl
Resources/Locale/en-US/prototypes/catalog/fills/crates/medical-crates.ftl
Resources/Locale/en-US/round-end/round-end-system.ftl
Resources/Prototypes/Catalog/Cargo/cargo_medical.yml
Resources/Prototypes/Catalog/Fills/Crates/medical.yml
Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
Resources/Prototypes/Entities/Objects/Misc/implanters.yml
Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
Resources/Prototypes/GameRules/roundstart.yml
Resources/Prototypes/Roles/Antags/revolutionary.yml [new file with mode: 0644]
Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml
Resources/Prototypes/Roles/Jobs/Command/captain.yml
Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml
Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml
Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml
Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml
Resources/Prototypes/Roles/Jobs/Science/research_director.yml
Resources/Prototypes/Roles/Jobs/Security/detective.yml
Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml
Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml
Resources/Prototypes/Roles/Jobs/Security/security_officer.yml
Resources/Prototypes/Roles/Jobs/Security/senior_officer.yml
Resources/Prototypes/Roles/Jobs/Security/warden.yml
Resources/Prototypes/StatusIcon/antag.yml
Resources/Prototypes/ai_factions.yml
Resources/Prototypes/game_presets.yml
Resources/Prototypes/secret_weights.yml
Resources/Prototypes/tags.yml
Resources/Textures/Interface/Misc/job_icons.rsi/HeadRevolutionary.png [new file with mode: 0644]
Resources/Textures/Interface/Misc/job_icons.rsi/Revolutionary.png [new file with mode: 0644]
Resources/Textures/Interface/Misc/job_icons.rsi/meta.json

diff --git a/Content.Client/Antag/AntagStatusIconSystem.cs b/Content.Client/Antag/AntagStatusIconSystem.cs
new file mode 100644 (file)
index 0000000..3c1c72d
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Used for assigning specified icons for antags.
+/// </summary>
+public abstract class AntagStatusIconSystem<T> : SharedStatusIconSystem
+    where T : Component
+{
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+    [Dependency] private readonly IPlayerManager _player = default!;
+
+    /// <summary>
+    /// Will check if the local player has the same component as the one who called it and give the status icon.
+    /// </summary>
+    /// <param name="antagStatusIcon">The status icon that your antag uses</param>
+    /// <param name="args">The GetStatusIcon event.</param>
+    protected virtual void GetStatusIcon(string antagStatusIcon, ref GetStatusIconsEvent args)
+    {
+        var ent = _player.LocalPlayer?.ControlledEntity;
+
+        if (!HasComp<T>(ent) && !HasComp<GhostComponent>(ent))
+            return;
+
+        args.StatusIcons.Add(_prototype.Index<StatusIconPrototype>(antagStatusIcon));
+    }
+}
diff --git a/Content.Client/Revolutionary/RevolutionarySystem.cs b/Content.Client/Revolutionary/RevolutionarySystem.cs
new file mode 100644 (file)
index 0000000..0818b14
--- /dev/null
@@ -0,0 +1,35 @@
+using Content.Shared.Revolutionary.Components;
+using Content.Client.Antag;
+using Content.Shared.StatusIcon.Components;
+
+namespace Content.Client.Revolutionary;
+
+/// <summary>
+/// Used for the client to get status icons from other revs.
+/// </summary>
+public sealed class RevolutionarySystem : AntagStatusIconSystem<RevolutionaryComponent>
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RevolutionaryComponent, GetStatusIconsEvent>(GetRevIcon);
+        SubscribeLocalEvent<HeadRevolutionaryComponent, GetStatusIconsEvent>(GetHeadRevIcon);
+    }
+
+    /// <summary>
+    /// Checks if the person who triggers the GetStatusIcon event is also a Rev or a HeadRev.
+    /// </summary>
+    private void GetRevIcon(EntityUid uid, RevolutionaryComponent comp, ref GetStatusIconsEvent args)
+    {
+        if (!HasComp<HeadRevolutionaryComponent>(uid))
+        {
+            GetStatusIcon(comp.RevStatusIcon, ref args);
+        }
+    }
+
+    private void GetHeadRevIcon(EntityUid uid, HeadRevolutionaryComponent comp, ref GetStatusIconsEvent args)
+    {
+        GetStatusIcon(comp.HeadRevStatusIcon, ref args);
+    }
+}
index 8816735a2ac36c4a09feb34fd70ae07fdddd0b00..6d0355f6f8df2ec7b25a0653cbc9730d209f28ce 100644 (file)
@@ -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<ZombieComponent>
 {
-    [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<ZombieComponent>(_player.LocalPlayer?.ControlledEntity))
-            return;
-
-        args.StatusIcons.Add(_prototype.Index<StatusIconPrototype>(component.ZombieStatusIcon));
+        GetStatusIcon(component.ZombieStatusIcon, ref args);
     }
 }
index 3471c8bb7864d387ae77a0461a28b34e9fb81d39..6fe526af11d92e042fb0ec5ab7e792e44879a203 100644 (file)
@@ -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<Verb> 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 (file)
index 0000000..f1ca181
--- /dev/null
@@ -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<GameRuleComponent>
+{
+    [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!;
+
+    /// <summary>
+    /// Attempts to start the game rule by checking if there are enough players in lobby and readied.
+    /// </summary>
+    /// <param name="ev">The roundstart attempt event</param>
+    /// <param name="uid">The entity the gamerule you are using is on</param>
+    /// <param name="minPlayers">The minimum amount of players needed for you gamerule to start.</param>
+    /// <param name="gameRule">The gamerule component.</param>
+
+    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();
+            }
+        }
+    }
+
+    /// <summary>
+    /// Will check which players are eligible to be chosen for antagonist and give them the given antag.
+    /// </summary>
+    /// <param name="antagPrototype">The antag prototype from your rule component.</param>
+    /// <param name="maxAntags">How many antags can be present in any given round.</param>
+    /// <param name="antagsPerPlayer">How many players you need to spawn an additional antag.</param>
+    /// <param name="antagSound">The intro sound that plays when the antag is chosen.</param>
+    /// <param name="antagGreeting">The antag message you want shown when the antag is chosen.</param>
+    /// <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.ServerSessions.ToList();
+        var playerList = new List<IPlayerSession>();
+        var prefList = new List<IPlayerSession>();
+        chosen = new List<EntityUid>();
+        foreach (var player in allPlayers)
+        {
+            if (includeHeads == false)
+            {
+                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;
+
+        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));
+            }
+        }
+    }
+
+    /// <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>
+    public 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;
+    }
+
+    /// <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>
+    /// 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="item">The prototype ID that you want to spawn in the bag.</param>
+    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<ContainerManagerComponent>(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);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
index 04b3f0cd24618c67c4d148c46670dfc0b76f7e66..bc2c4ddabafd8b29da31f261b9d10a417e441aad 100644 (file)
@@ -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<FlashComponent, MeleeHitEvent>(OnFlashMeleeHit);
             // ran before toggling light for extra-bright lantern
             SubscribeLocalEvent<FlashComponent, UseInHandEvent>(OnFlashUseInHand, before: new []{ typeof(HandheldLightSystem) });
             SubscribeLocalEvent<InventoryComponent, FlashAttemptEvent>(OnInventoryFlashAttempt);
-
             SubscribeLocalEvent<FlashImmunityComponent, FlashAttemptEvent>(OnFlashImmunityFlashAttempt);
             SubscribeLocalEvent<PermanentBlindnessComponent, FlashAttemptEvent>(OnPermanentBlindnessFlashAttempt);
             SubscribeLocalEvent<TemporaryBlindnessComponent, FlashAttemptEvent>(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;
         }
     }
+    /// <summary>
+    /// 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.
+    /// </summary>
+    [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 (file)
index 0000000..f015f8b
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Component for the RevolutionaryRuleSystem that stores info about winning/losing, player counts required for starting, as well as prototypes for Revolutionaries and their gear.
+/// </summary>
+[RegisterComponent, Access(typeof(RevolutionaryRuleSystem))]
+public sealed partial class RevolutionaryRuleComponent : Component
+{
+    /// <summary>
+    /// When the round will if all the command are dead (Incase they are in space)
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    public TimeSpan CommandCheck;
+
+    /// <summary>
+    /// The amount of time between each check for command check.
+    /// </summary>
+    [DataField]
+    public TimeSpan TimerWait = TimeSpan.FromSeconds(20);
+
+    /// <summary>
+    /// Stores players minds
+    /// </summary>
+    [DataField]
+    public Dictionary<string, EntityUid> HeadRevs = new();
+
+    [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/traitor_start.ogg");
+
+    /// <summary>
+    /// Min players needed for Revolutionary gamemode to start.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public int MinPlayers = 15;
+
+    /// <summary>
+    /// Max Head Revs allowed during selection.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public int MaxHeadRevs = 3;
+
+    /// <summary>
+    /// The amount of Head Revs that will spawn per this amount of players.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public int PlayersPerHeadRev = 15;
+
+    /// <summary>
+    /// The gear head revolutionaries are given on spawn.
+    /// </summary>
+    [DataField]
+    public List<EntProtoId> StartingGear = new()
+    {
+        "Flash",
+        "ClothingEyesGlassesSunglasses"
+    };
+
+    /// <summary>
+    /// The time it takes after the last head is killed for the shuttle to arrive.
+    /// </summary>
+    [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 (file)
index 0000000..7efd3a9
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Where all the main stuff for Revolutionaries happens (Assigning Head Revs, Command on station, and checking for the game to end.)
+/// </summary>
+public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleComponent>
+{
+    [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<NpcFactionPrototype>]
+    public const string RevolutionaryNpcFaction = "Revolutionary";
+    [ValidatePrototypeId<AntagPrototype>]
+    public const string RevolutionaryAntagRole = "Rev";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
+        SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayerJobAssigned);
+        SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
+        SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
+        SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
+        SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(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;
+    }
+
+    /// <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)
+    {
+        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<RevolutionaryRuleComponent>();
+        while (query.MoveNext(out var headrev))
+        {
+            // This is (revsLost, commandsLost) concatted together
+            // (moony wrote this comment idk what it means)
+            var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
+            ev.AddLine(Loc.GetString(Outcomes[index]));
+
+            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<RevolutionaryRuleComponent, GameRuleComponent>();
+        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<EntityUid> chosen, string antagProto, RevolutionaryRuleComponent comp)
+    {
+        foreach (var headRev in chosen)
+        {
+            RemComp<CommandStaffComponent>(headRev);
+
+            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);
+        }
+    }
+
+    /// <summary>
+    /// Called when a Head Rev uses a flash in melee to convert somebody else.
+    /// </summary>
+    public void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref AfterFlashedEvent ev)
+    {
+        TryComp<AlwaysRevolutionaryConvertibleComponent>(ev.Target, out var alwaysConvertibleComp);
+        var alwaysConvertible = alwaysConvertibleComp != null;
+
+        if (!_mind.TryGetMind(ev.Target, out var mindId, out var mind) && !alwaysConvertible)
+            return;
+
+        if (HasComp<RevolutionaryComponent>(ev.Target) ||
+            HasComp<MindShieldComponent>(ev.Target) ||
+            !HasComp<HumanoidAppearanceComponent>(ev.Target) &&
+            !alwaysConvertible ||
+            !_mobState.IsAlive(ev.Target) ||
+            HasComp<ZombieComponent>(ev.Target))
+        {
+            return;
+        }
+
+        _npcFaction.AddFaction(ev.Target, RevolutionaryNpcFaction);
+        EnsureComp<RevolutionaryComponent>(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<RevolutionaryRoleComponent>(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<RevolutionaryRuleComponent>().FirstOrDefault();
+        if (revRule == null)
+        {
+            GameTicker.StartGameRule("Revolutionary", out var ruleEnt);
+            revRule = Comp<RevolutionaryRuleComponent>(ruleEnt);
+        }
+
+        if (!HasComp<HeadRevolutionaryComponent>(mind.OwnedEntity))
+        {
+            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.ConnectedClient, Color.FromHex("#5e9cff"));
+            }
+        }
+    }
+    private void OnCommandMobStateChanged(EntityUid uid, CommandStaffComponent comp, MobStateChangedEvent ev)
+    {
+        if (ev.NewMobState == MobState.Dead || ev.NewMobState == MobState.Invalid)
+            CheckCommandLose();
+    }
+
+    /// <summary>
+    /// Checks if all of command is dead and if so will remove all sec and command jobs if there were any left.
+    /// </summary>
+    private bool CheckCommandLose()
+    {
+        var commandList = new List<EntityUid>();
+
+        var heads = AllEntityQuery<CommandStaffComponent>();
+        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();
+    }
+
+    /// <summary>
+    /// Checks if all the Head Revs are dead and if so will deconvert all regular revs.
+    /// </summary>
+    private bool CheckRevsLose()
+    {
+        var stunTime = TimeSpan.FromSeconds(4);
+        var headRevList = new List<EntityUid>();
+
+        var headRevs = AllEntityQuery<HeadRevolutionaryComponent, MobStateComponent>();
+        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<RevolutionaryComponent>();
+            while (rev.MoveNext(out var uid, out _))
+            {
+                if (!HasComp<HeadRevolutionaryComponent>(uid))
+                {
+                    _npcFaction.RemoveFaction(uid, RevolutionaryNpcFaction);
+                    _stun.TryParalyze(uid, stunTime, true);
+                    RemCompDeferred<RevolutionaryComponent>(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 (file)
index 0000000..714055b
--- /dev/null
@@ -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;
+
+/// <summary>
+/// System used for checking if the implanted is a Rev or Head Rev.
+/// </summary>
+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<TagPrototype>]
+    public const string MindShieldTag = "MindShield";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<SubdermalImplantComponent, ImplantImplantedEvent>(ImplantCheck);
+    }
+
+    /// <summary>
+    /// Checks if the implant was a mindshield or not
+    /// </summary>
+    public void ImplantCheck(EntityUid uid, SubdermalImplantComponent comp, ref ImplantImplantedEvent ev)
+    {
+        if (_tag.HasTag(ev.Implant, MindShieldTag) && ev.Implanted != null)
+        {
+            EnsureComp<MindShieldComponent>(ev.Implanted.Value);
+            MindShieldRemovalCheck(ev.Implanted, ev.Implant);
+        }
+    }
+
+    /// <summary>
+    /// Checks if the implanted person was a Rev or Head Rev and remove role or destroy mindshield respectively.
+    /// </summary>
+    public void MindShieldRemovalCheck(EntityUid? implanted, EntityUid implant)
+    {
+        if (HasComp<RevolutionaryComponent>(implanted) && !HasComp<HeadRevolutionaryComponent>(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<RevolutionaryRoleComponent>(mindId);
+        }
+        else if (HasComp<RevolutionaryComponent>(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 (file)
index 0000000..8e42f41
--- /dev/null
@@ -0,0 +1,12 @@
+using Content.Server.GameTicking.Rules;
+
+namespace Content.Server.Revolutionary.Components;
+
+/// <summary>
+/// Given to heads at round start for Revs. Used for tracking if heads died or not.
+/// </summary>
+[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 (file)
index 0000000..fa06cc3
--- /dev/null
@@ -0,0 +1,12 @@
+using Content.Shared.Roles;
+
+namespace Content.Server.Roles;
+
+/// <summary>
+///     Added to mind entities to tag that they are a Revolutionary.
+/// </summary>
+[RegisterComponent]
+public sealed partial class RevolutionaryRoleComponent : AntagonistRoleComponent
+{
+
+}
index 12cfb0e6667d0e8b13d1b18bc5118685f0fe89f3..6043f3fbf92ff2f3d135972ae096217b4f4386aa 100644 (file)
@@ -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)
+        /// <summary>
+        /// Starts a behavior to end the round
+        /// </summary>
+        /// <param name="behavior">The way in which the round will end</param>
+        /// <param name="time"></param>
+        /// <param name="sender"></param>
+        /// <param name="textCall"></param>
+        /// <param name="textAnnounce"></param>
+        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)
             {
index 55db027225619564b29ecfa52b110dcd8ca6632f..2d68f1cdaaa3a824ac8d5dd426db757311f758a8 100644 (file)
@@ -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
     /// </summary>
     /// <param name="target">the implanted entity</param>
     /// <param name="implant">the implant</param>
-    /// <param name="component">the implant component</param>
+    [PublicAPI]
     public void ForceRemove(EntityUid target, EntityUid implant)
     {
         if (!TryComp<ImplantedComponent>(target, out var implanted))
@@ -144,6 +148,7 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
     /// Removes and deletes implants by force
     /// </summary>
     /// <param name="target">The entity to have implants removed</param>
+    [PublicAPI]
     public void WipeImplants(EntityUid target)
     {
         if (!TryComp<ImplantedComponent>(target, out var implanted))
@@ -180,3 +185,23 @@ public sealed class ImplantRelayEvent<T> where T : notnull
         Event = ev;
     }
 }
+
+/// <summary>
+/// Event that is raised whenever someone is implanted with any given implant.
+/// Raised on the the implant entity.
+/// </summary>
+/// <remarks>
+/// implant implant implant implant
+/// </remarks>
+[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 (file)
index 0000000..1cc21d1
--- /dev/null
@@ -0,0 +1,12 @@
+using Content.Shared.Revolutionary;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Mindshield.Components;
+
+/// <summary>
+/// If a player has a Mindshield they will get this component to prevent conversion.
+/// </summary>
+[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 (file)
index 0000000..694c6d2
--- /dev/null
@@ -0,0 +1,12 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Revolutionary.Components;
+
+/// <summary>
+/// Component used for allowing non-humans to be converted. (Mainly monkeys)
+/// </summary>
+[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 (file)
index 0000000..48d7c23
--- /dev/null
@@ -0,0 +1,24 @@
+using Robust.Shared.GameStates;
+using Content.Shared.StatusIcon;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Revolutionary.Components;
+
+/// <summary>
+/// Component used for marking a Head Rev for conversion and winning/losing.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))]
+public sealed partial class HeadRevolutionaryComponent : Component
+{
+    /// <summary>
+    /// The status icon corresponding to the head revolutionary.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public ProtoId<StatusIconPrototype> HeadRevStatusIcon = "HeadRevolutionaryFaction";
+
+    /// <summary>
+    /// How long the stun will last after the user is converted.
+    /// </summary>
+    [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 (file)
index 0000000..e55c877
--- /dev/null
@@ -0,0 +1,18 @@
+using Robust.Shared.GameStates;
+using Content.Shared.StatusIcon;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Revolutionary.Components;
+
+/// <summary>
+/// Used for marking regular revs as well as storing icon prototypes so you can see fellow revs.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))]
+public sealed partial class RevolutionaryComponent : Component
+{
+    /// <summary>
+    /// The status icon prototype displayed for revolutionaries
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public ProtoId<StatusIconPrototype> RevStatusIcon = "RevolutionaryFaction";
+}
diff --git a/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs b/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs
new file mode 100644 (file)
index 0000000..993c74d
--- /dev/null
@@ -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<MindShieldComponent, ComponentInit>(MindShieldImplanted);
+    }
+
+    /// <summary>
+    /// When the mindshield is implanted in the rev it will popup saying they were deconverted. In Head Revs it will remove the mindshield component.
+    /// </summary>
+    private void MindShieldImplanted(EntityUid uid, MindShieldComponent comp, ComponentInit init)
+    {
+        if (HasComp<RevolutionaryComponent>(uid) && !HasComp<HeadRevolutionaryComponent>(uid))
+        {
+            var stunTime = TimeSpan.FromSeconds(4);
+            var name = Identity.Entity(uid, EntityManager);
+            RemComp<RevolutionaryComponent>(uid);
+            _sharedStun.TryParalyze(uid, stunTime, true);
+            _popupSystem.PopupEntity(Loc.GetString("rev-break-control", ("name", name)), uid);
+        }
+        else if (HasComp<HeadRevolutionaryComponent>(uid))
+        {
+            RemCompDeferred<MindShieldComponent>(uid);
+        }
+    }
+}
index ec428b058008ec829ec82ff37e3ddb098e445b99..3d098c1c549d4f24897cec1579ab40f4dd45bb73 100644 (file)
@@ -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 (file)
index 0000000..53aea2a
--- /dev/null
@@ -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.
+
+
index c03883a73e2e8d40f2e443fe688db62a41bbe072..42c5880ec0dc95f11d3a27e5425d7cfc5496d37c 100644 (file)
@@ -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 }
index e7cd8346f5d3fb7c2f4cc6cdeffdc449771fc470..840965cf1d5c3553701feaf05d2812db773b9f7b 100644 (file)
@@ -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.
 
index f85bee0323ae9db8c9bb0cd0c4d1ca097577cba1..f86851506bc5eabfb116a416c64e6179f230c0c1 100644 (file)
@@ -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}...
index 1dc572e204a7144646fc1421d4653af11fc12852..94927c1a79693b8c91f1908b4bee0b5ea6048f54 100644 (file)
   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:
index a9795ef02d299f33920335e4dbd2b151b3fa1b0e..a487138afda10b7f8f030f9ded13943a9e8aa4fc 100644 (file)
       - id: BoxBottle
         amount: 2
 
+- type: entity
+  id: CrateMindShieldImplants
+  parent: CrateMedical
+  components:
+  - type: StorageFill
+    contents:
+      - id: MindShieldImplanter
+        amount: 3
+
 - type: entity
   id: CrateMedicalSurgery
   parent: CrateSurgery
index bd112f855b2890073af8f4dd01b106b106059a54..32e2e48ad6c8eeac7885f9001c73bc3310b32574 100644 (file)
     clumsySound:
       path: /Audio/Animals/monkey_scream.ogg
   - type: IdExaminable
+  - type: AlwaysRevolutionaryConvertible
 
 - type: entity
   name: guidebook monkey
index a3d5fbef397ad7f1342cf3a63173f5eba22a400d..8cb55e386e7a5bd75dc434775e10f8c087fa9a71 100644 (file)
   components:
     - type: Implanter
       implant: DeathRattleImplant
+
+# Security and Command implanters
+
+- type: entity
+  id: MindShieldImplanter
+  name: mind-shield implanter
+  parent: BaseImplantOnlyImplanter
+  components:
+    - type: Implanter
+      implant: MindShieldImplant
index 772dd45029d3df958df5dbd9732a7e6d33d3909f..6632010a79b89658838a92bd75fef184153f4793 100644 (file)
       - 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
index 0fc6de32ecec28c97c56525371a9f4d6140eee7a..a0fc66bb8115997299c7d6be5a17d014906867e7 100644 (file)
   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 (file)
index 0000000..c5e6cb8
--- /dev/null
@@ -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
index 3341c94269c0ceeb848149918f279de896fe0a41..d2dfafd32425a34c984e972dbb61acb01898c17c 100644 (file)
@@ -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"
   - Maintenance
   - External
   - Command
+  special:
+  - !type:AddImplantSpecial
+    implants: [ MindShieldImplant ]
+  - !type:AddComponentSpecial
+    components:
+      - type: CommandStaff
 
 - type: startingGear
   id: QuartermasterGear
index e2956aa3947e09b2e9ffa15bf5155c865f0ce87c..87c743463c096aecdaa21241221e7a6513c504e8 100644 (file)
   canBeAntag: false
   accessGroups:
   - AllAccess
+  special:
+  - !type:AddImplantSpecial
+    implants: [ MindShieldImplant ]
+  - !type:AddComponentSpecial
+    components:
+      - type: CommandStaff
 
 - type: startingGear
   id: CaptainGear
index 2768452e3b45f1d118f691c1d905a387df565c7d..2a42d8e0c44e1705c32e310adff7755b72a86da4 100644 (file)
   - Cargo
   - Atmospherics
   - Medical
+  special:
+  - !type:AddImplantSpecial
+    implants: [ MindShieldImplant ]
+  - !type:AddComponentSpecial
+    components:
+      - type: CommandStaff
 
 - type: startingGear
   id: HoPGear
index 713117b10b4f5cb5e5090048ea6ad242c2ecd714..bddbd8f2cd453b0af1753273dfca2d8f7f88839c 100644 (file)
@@ -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"
   - External
   - ChiefEngineer
   - Atmospherics
+  special:
+  - !type:AddImplantSpecial
+    implants: [ MindShieldImplant ]
+  - !type:AddComponentSpecial
+    components:
+      - type: CommandStaff
 
 - type: startingGear
   id: ChiefEngineerGear
index 3a633ab3f1b0c3a12abc3c6eccb04e5ebd2772ac..146fa3ed96b95a0799ce7deae9e5dbd10c089d53 100644 (file)
   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:
     head: ClothingHeadHatGladiator
     shoes: ClothingShoesCult
 
- #Ash Walker 
+#Ash Walker
 - type: startingGear
   id: AshWalker
   equipment:
index d9083d2c8a6c6ec0166ee190eee357f9870c6b2d..785f7ad62633884cb4b6bf92c17222acf1a890af 100644 (file)
@@ -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"
   - Maintenance
   - Chemistry
   - ChiefMedicalOfficer
+  special:
+  - !type:AddImplantSpecial
+    implants: [ MindShieldImplant ]
+  - !type:AddComponentSpecial
+    components:
+      - type: CommandStaff
 
 - type: startingGear
   id: CMOGear
index b5ad3292e90151957ce01467ce85b40577f96f04..964bffb3a2da55ea943c40272e956d091b92dc72 100644 (file)
@@ -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"
   - Command
   - Maintenance
   - ResearchDirector
+  special:
+  - !type:AddImplantSpecial
+    implants: [ MindShieldImplant ]
+  - !type:AddComponentSpecial
+    components:
+      - type: CommandStaff
 
 - type: startingGear
   id: ResearchDirectorGear
index 00c971c2c69cdd54c178d47247619c609cf24c5a..feef05dc878f829f2aed38f5610df3f24ae3e5e3 100644 (file)
@@ -17,6 +17,9 @@
   - Maintenance\r
   - Service\r
   - Detective\r
+  special:\r
+  - !type:AddImplantSpecial\r
+    implants: [ MindShieldImplant ]\r
 \r
 - type: startingGear\r
   id: DetectiveGear\r
index 7b70149d961a3fe1b2e7cf5e7da2e5a692e6598f..58a96d1a5b5ced4a64e7839926c63c52c12631e6 100644 (file)
@@ -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"
   - Service
   - External
   - Detective
+  special:
+  - !type:AddImplantSpecial
+    implants: [ MindShieldImplant ]
+  - !type:AddComponentSpecial
+    components:
+      - type: CommandStaff
 
 - type: startingGear
   id: HoSGear
index b58009ae86e109197a96b4c3df5389602a396cd1..30ab14486022d4cd0db4e6968b9870846bb71d18 100644 (file)
@@ -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
index e38256b8a1c324b978e9f825e044379bb82b3ec8..01cf5b44b95cde612108b8873c2b51c287bee84e 100644 (file)
@@ -17,6 +17,9 @@
   - Maintenance
   - Service
   - External
+  special:
+  - !type:AddImplantSpecial
+    implants: [ MindShieldImplant ]
 
 - type: startingGear
   id: SecurityOfficerGear
index 5da1347e147de8ca9dd61563d5f1404a917787fd..46abc8664e86ace37ae629de7ca314838bdaa4d7 100644 (file)
@@ -26,6 +26,9 @@
   - Maintenance
   - Service
   - External
+  special:
+  - !type:AddImplantSpecial
+    implants: [ MindShieldImplant ]
 
 - type: startingGear
   id: SeniorOfficerGear
index 53ba868dcf94fbf4e2888f360aae7c013cfa2853..46142d1550c6ab0e8f040ec52d3a8372e08ac8c2 100644 (file)
@@ -19,6 +19,9 @@
   - Brig
   - External
   - Detective
+  special:
+  - !type:AddImplantSpecial
+    implants: [ MindShieldImplant ]
 
 - type: startingGear
   id: WardenGear
index a4690caf06eb6aaecddfbc7338822ed21fd28743..564d29a35b21ae851ed7531bc890ceb20e2f7985 100644 (file)
@@ -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
index 9c8f00157741bfcbbe1e8ac5f72233c9b16ffa14..3dfb35c7a69000b0a3f95bd1aa32c456eddc76c2 100644 (file)
@@ -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
   - Syndicate
   - Passive
   - PetsNT
+  - Revolutionary
+
+- type: npcFaction
+  id: Revolutionary
+  hostile:
+  - NanoTrasen
+  - Zombie
+  - SimpleHostile
+  - Dragon
index e12822b2eaf740155422758358827be74fa4aed5..b42b2e49f968cf90eb0789c7ecd87d31564c47ca 100644 (file)
     - 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:
index b23daa2a4e93c882a5d4e5f3d3f62a3663cd4b8c..6c7cc12ce1a4ec92cb561cd50df184c616254b38 100644 (file)
@@ -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
index 075011730545c4e35b562901457fc9a21f67be4f..d01f8c1ddfb89e1c0b731b61db036013d66cbdae 100644 (file)
 - 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 (file)
index 0000000..21f5c12
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 (file)
index 0000000..7dab788
Binary files /dev/null and b/Resources/Textures/Interface/Misc/job_icons.rsi/Revolutionary.png differ
index 0ead1158021b728401b916b14dff2a64bb0dfb9a..2d71fcd05606e1906d5a5df2c301f02036a8e9b3 100644 (file)
@@ -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
         },
         {
             "name": "SeniorOfficer"
+        },
+        {
+            "name": "Revolutionary"
+        },
+        {
+            "name": "HeadRevolutionary"
         }
     ]
 }