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" } ] }