From ea2dd83e773a9d57f39644abc4416c04a17b8c23 Mon Sep 17 00:00:00 2001 From: KeTuFaisPiKiNut <85769816+ketufaispikinut@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:39:13 -0400 Subject: [PATCH] Mindshield Refactor + Changeling Fake Mindshield (#43864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * the great mindshield rework * it just works :D * glory to nyanotransen, I said with my very obviously fake mindshield. * work on things to remove manual mindshield queries * very wip impl * yeah there we go apc destroyed mission accomplished * some comments * textures! * i will stab the person who decided to spell it differently. * rename things since i am crazy (i was also crazy once) * minor spelling mistake :( * Revert "minor spelling mistake :(" This reverts commit ece1f1c3509063d1c43b7f136509ed4df2cd62e7. * Revert "rename things since i am crazy (i was also crazy once)" This reverts commit e75411d4bb84e819ca525b9fa0fe1792b6ace0b6. * Revert "i will stab the person who decided to spell it differently." This reverts commit 696ffdc062094cc9e4cf382f1e39b4ae4e3d781d. * fixed things + better texs * aaaa * INSANITY RAHHHHH * e * changing things a bit. * testifying things actually no insanity * cleaning * only doing this cuz of heseintests failing * cleanup * Update Content.Server/Revolutionary/RevolutionarySystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Apply suggestions from code review fucking suggestions WHY IS THE BUTTON THERE EVEN WHEN ITS DISABLED!!!! Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * half of things for scarky0 * event merging stub / beginning * big rewrite * there ya go scar * e * NETWORK YOU PRICK * Update Content.Shared/Mindshield/SharedMindShieldSystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update Content.Shared/Mindshield/SharedMindShieldSystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update Content.Server/Revolutionary/RevolutionarySystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update Content.Shared/Mindshield/FakeMindshieldSystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * e * RAHHHH * cleaning * Update changeling.yml * Update meta.json * slarti 1/3 * slarti things 2 * le real fix * commit & push * Update Content.Client/Overlays/ShowMindShieldIconsSystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update Content.Server/Implants/MindshieldImplantSystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update Content.Shared/Mindshield/SharedMindShieldSystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update Content.Shared/Mindshield/FakeMindshieldSystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update Content.Shared/Mindshield/Components/MindShieldStatusComponent.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * scarky0 * dirty * fml * robust thyself now * minor fix * stub * Revert "stub" This reverts commit c1204b88d3ae1bbdca2deeb075a9dd0d6c529326. * s * jrwebiwerhweir * shrimple fix * Apply suggestions from code review Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * yeeeuuuwhqeheueuuu * rweifohgeterwt * cleaning, compiling * container bs and icons * werewrewr * fix * crisis averted * howd i forget this * leave me ALONE --------- Co-authored-by: nomdéraisonnablementlong Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Co-authored-by: ScarKy0 Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com> --- .../Overlays/ShowMindShieldIconsSystem.cs | 28 +-- .../Revolutionary/RevolutionarySystem.cs | 34 --- .../Rules/RevolutionaryRuleSystem.cs | 5 +- Content.Server/Mindshield/MindShieldSystem.cs | 70 ------ .../Revolutionary/RevolutionarySystem.cs | 5 - .../Store/Systems/StoreSystem.Ui.cs | 6 +- .../Implants/MindshieldImplantSystem.cs | 52 +++++ .../SharedSubdermalImplantSystem.Relays.cs | 4 + .../Inventory/InventorySystem.Relay.cs | 3 + .../FakeMindShieldImplantComponent.cs | 8 - .../Components/FakeMindshieldComponent.cs | 45 +++- .../Components/MindShieldComponent.cs | 14 +- .../Components/MindShieldImplantComponent.cs | 4 +- .../Components/MindShieldStatusComponent.cs | 23 ++ .../FakeMindShield/FakeMindshieldSystem.cs | 98 --------- .../Mindshield/FakeMindshieldSystem.cs | 200 ++++++++++++++++++ Content.Shared/Mindshield/MindShieldSystem.cs | 139 ++++++++++++ ...AlwaysRevolutionaryConvertibleComponent.cs | 2 +- .../Components/HeadRevolutionaryComponent.cs | 2 +- .../Components/RevolutionaryComponent.cs | 8 +- ...ionarySystem.cs => RevolutionarySystem.cs} | 56 ++--- .../Locale/en-US/changeling/changeling.ftl | 4 + .../Locale/en-US/store/changeling-catalog.ftl | 3 + Resources/Prototypes/Actions/changeling.yml | 21 ++ .../Prototypes/Catalog/changeling_catalog.yml | 14 ++ .../Entities/Mobs/Player/changeling.yml | 12 ++ .../Objects/Misc/subdermal_implants.yml | 5 +- Resources/Prototypes/tags.yml | 3 + .../actions_fakemindshield.rsi/icon-on.png | Bin 5111 -> 1057 bytes .../actions_fakemindshield.rsi/icon.png | Bin 5105 -> 1049 bytes .../actions_fakemindshield.rsi/meta.json | 4 +- .../Actions/changeling2.rsi/meta.json | 8 +- .../changeling2.rsi/mindshield_off.png | Bin 0 -> 1971 bytes .../Actions/changeling2.rsi/mindshield_on.png | Bin 0 -> 2013 bytes 34 files changed, 583 insertions(+), 297 deletions(-) delete mode 100644 Content.Client/Revolutionary/RevolutionarySystem.cs delete mode 100644 Content.Server/Mindshield/MindShieldSystem.cs delete mode 100644 Content.Server/Revolutionary/RevolutionarySystem.cs create mode 100644 Content.Shared/Implants/MindshieldImplantSystem.cs delete mode 100644 Content.Shared/Mindshield/Components/FakeMindShieldImplantComponent.cs create mode 100644 Content.Shared/Mindshield/Components/MindShieldStatusComponent.cs delete mode 100644 Content.Shared/Mindshield/FakeMindShield/FakeMindshieldSystem.cs create mode 100644 Content.Shared/Mindshield/FakeMindshieldSystem.cs create mode 100644 Content.Shared/Mindshield/MindShieldSystem.cs rename Content.Shared/Revolutionary/{SharedRevolutionarySystem.cs => RevolutionarySystem.cs} (65%) create mode 100644 Resources/Textures/Interface/Actions/changeling2.rsi/mindshield_off.png create mode 100644 Resources/Textures/Interface/Actions/changeling2.rsi/mindshield_on.png diff --git a/Content.Client/Overlays/ShowMindShieldIconsSystem.cs b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs index 6ae2b46f79..494b80cfc5 100644 --- a/Content.Client/Overlays/ShowMindShieldIconsSystem.cs +++ b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Mindshield.Components; +using Content.Shared.Mindshield; using Content.Shared.Overlays; using Content.Shared.StatusIcon.Components; @@ -6,29 +6,17 @@ namespace Content.Client.Overlays; public sealed partial class ShowMindShieldIconsSystem : EquipmentHudSystem { - public override void Initialize() - { - base.Initialize(); + [Dependency] private MindShieldSystem _mindShield = default!; - SubscribeLocalEvent(OnGetStatusIconsEvent); - SubscribeLocalEvent(OnGetStatusIconsEventFake); - } - // TODO: Probably need to get this OFF of client since this can be read by bad actors rather easily - // ...imagine cheating in a game about silly paper dolls - private void OnGetStatusIconsEventFake(EntityUid uid, FakeMindShieldComponent component, ref GetStatusIconsEvent ev) - { - if (!IsActive) - return; - if (component.IsEnabled && ProtoMan.Resolve(component.MindShieldStatusIcon, out var fakeStatusIconPrototype)) - ev.StatusIcons.Add(fakeStatusIconPrototype); - } - - private void OnGetStatusIconsEvent(EntityUid uid, MindShieldComponent component, ref GetStatusIconsEvent ev) + [SubscribeLocalEvent] + private void OnGetStatusIconsEvent(Entity ent, ref GetStatusIconsEvent args) { + // Is active checks for our ability to display status icons if (!IsActive) return; - if (ProtoMan.Resolve(component.MindShieldStatusIcon, out var iconPrototype)) - ev.StatusIcons.Add(iconPrototype); + _mindShield.GetMindshieldStatus(ent.Owner, out _, out var isVisible); + if (isVisible && ProtoMan.Resolve(MindShieldSystem.StatusIcon, out var statusIconPrototype)) + args.StatusIcons.Add(statusIconPrototype); } } diff --git a/Content.Client/Revolutionary/RevolutionarySystem.cs b/Content.Client/Revolutionary/RevolutionarySystem.cs deleted file mode 100644 index 4edd4d3669..0000000000 --- a/Content.Client/Revolutionary/RevolutionarySystem.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Content.Shared.Revolutionary.Components; -using Content.Shared.Revolutionary; -using Content.Shared.StatusIcon.Components; - -namespace Content.Client.Revolutionary; - -/// -/// Used for the client to get status icons from other revs. -/// -public sealed partial class RevolutionarySystem : SharedRevolutionarySystem -{ - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(GetRevIcon); - SubscribeLocalEvent(GetHeadRevIcon); - } - - private void GetRevIcon(Entity ent, ref GetStatusIconsEvent args) - { - if (HasComp(ent)) - return; - - if (ProtoMan.Resolve(ent.Comp.StatusIcon, out var iconPrototype)) - args.StatusIcons.Add(iconPrototype); - } - - private void GetHeadRevIcon(Entity ent, ref GetStatusIconsEvent args) - { - if (ProtoMan.Resolve(ent.Comp.StatusIcon, out var iconPrototype)) - args.StatusIcons.Add(iconPrototype); - } -} diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs index 2c8dbf0486..9a63c0fca1 100644 --- a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs @@ -30,6 +30,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Content.Shared.Cuffs.Components; using Robust.Shared.Player; +using Content.Shared.Mindshield; namespace Content.Server.GameTicking.Rules; @@ -52,6 +53,7 @@ public sealed partial class RevolutionaryRuleSystem : GameRuleSystem RevolutionaryNpcFaction = "Revolutionary"; @@ -152,7 +154,8 @@ public sealed partial class RevolutionaryRuleSystem : GameRuleSystem -/// System used for adding or removing components with a mindshield implant -/// as well as checking if the implanted is a Rev or Head Rev. -/// -public sealed partial class MindShieldSystem : EntitySystem -{ - [Dependency] private IAdminLogManager _adminLogManager = default!; - [Dependency] private RoleSystem _roleSystem = default!; - [Dependency] private MindSystem _mindSystem = default!; - [Dependency] private PopupSystem _popupSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnImplantImplanted); - SubscribeLocalEvent(OnImplantRemoved); - SubscribeLocalEvent(OnAttemptConvert); - } - - private void OnImplantImplanted(Entity ent, ref ImplantImplantedEvent ev) - { - EnsureComp(ev.Implanted); - MindShieldRemovalCheck(ev.Implanted, ev.Implant); - } - - /// - /// Checks if the implanted person was a Rev or Head Rev and remove role or destroy mindshield respectively. - /// - private void MindShieldRemovalCheck(EntityUid implanted, EntityUid implant) - { - if (HasComp(implanted)) - { - _popupSystem.PopupEntity(Loc.GetString("head-rev-break-mindshield"), implanted); - QueueDel(implant); - return; - } - - if (_mindSystem.TryGetMind(implanted, out var mindId, out _) && - _roleSystem.MindRemoveRole(mindId)) - { - _adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(implanted)} was deconverted due to being implanted with a Mindshield."); - } - } - - private void OnImplantRemoved(Entity ent, ref ImplantRemovedEvent args) - { - RemComp(args.Implanted); - } - - private void OnAttemptConvert(Entity ent, ref AttemptConvertRevolutionaryEvent args) - { - args.Cancelled = true; - } -} - diff --git a/Content.Server/Revolutionary/RevolutionarySystem.cs b/Content.Server/Revolutionary/RevolutionarySystem.cs deleted file mode 100644 index 597c4896b9..0000000000 --- a/Content.Server/Revolutionary/RevolutionarySystem.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Content.Shared.Revolutionary; - -namespace Content.Server.Revolutionary; - -public sealed class RevolutionarySystem : SharedRevolutionarySystem; diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index 51bb5827db..a324ed2c42 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -1,13 +1,13 @@ using System.Linq; using Content.Server.Actions; using Content.Server.Administration.Logs; +using Content.Shared.Mindshield; using Content.Server.Stack; using Content.Server.Store.Components; using Content.Shared.Actions; using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Hands.EntitySystems; -using Content.Shared.Mindshield.Components; using Content.Shared.NPC.Systems; using Content.Shared.Store; using Content.Shared.Store.Components; @@ -26,6 +26,7 @@ public sealed partial class StoreSystem [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private SharedHandsSystem _hands = default!; [Dependency] private StackSystem _stack = default!; + [Dependency] private MindShieldSystem _mindShield = default!; private void InitializeUi() { @@ -221,7 +222,8 @@ public sealed partial class StoreSystem logImpact = LogImpact.High; logExtraInfo = ", but was not from an expected faction"; - if (HasComp(buyer)) + _mindShield.GetMindshieldStatus(buyer, out var isMindshielded, out _); + if (isMindshielded) { logImpact = LogImpact.Extreme; logExtraInfo += " while also possessing a mindshield"; diff --git a/Content.Shared/Implants/MindshieldImplantSystem.cs b/Content.Shared/Implants/MindshieldImplantSystem.cs new file mode 100644 index 0000000000..185874b696 --- /dev/null +++ b/Content.Shared/Implants/MindshieldImplantSystem.cs @@ -0,0 +1,52 @@ +using Content.Shared.Administration.Logs; +using Content.Shared.Database; +using Content.Shared.IdentityManagement; +using Content.Shared.Mind; +using Content.Shared.Mindshield.Components; +using Content.Shared.Popups; +using Content.Shared.Revolutionary.Components; +using Content.Shared.Roles; +using Content.Shared.Roles.Components; +using Content.Shared.Stunnable; + +namespace Content.Shared.Implants; + +public sealed partial class MindshieldImplantSystem : EntitySystem +{ + [Dependency] private SharedPopupSystem _popup = default!; + [Dependency] private SharedStunSystem _sharedStun = default!; + [Dependency] private SharedMindSystem _mind = default!; + [Dependency] private SharedRoleSystem _role = default!; + [Dependency] private ISharedAdminLogManager _log = default!; + + /// + /// When the mindshield is implanted in the rev it will popup saying they were deconverted. In Head Revs it will remove the mindshield component. + /// + [SubscribeLocalEvent] + private void OnImplantImplanted(Entity ent, ref ImplantImplantedEvent args) + { + // Entity that was implanted + var uid = args.Implanted; + if (HasComp(uid)) + { + _popup.PopupEntity(Loc.GetString("head-rev-break-mindshield"), uid); + PredictedQueueDel(args.Implant); + return; + } + + if (TryComp(uid, out var comp)) + { + if (_mind.TryGetMind(uid, out var mindId, out _)) + { + _log.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to being implanted with a Mindshield."); + + if (_role.MindHasRole(mindId)) + _role.MindRemoveRole(mindId); + } + var name = Identity.Entity(uid, EntityManager); + RemComp(uid); + _sharedStun.TryUpdateParalyzeDuration(uid, comp.StunTime); + _popup.PopupEntity(Loc.GetString("rev-break-control", ("name", name)), uid); + } + } +} diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs index b92369ab44..c14c6210e8 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs @@ -2,6 +2,7 @@ using Content.Shared.Chat; using Content.Shared.IdentityManagement.Components; using Content.Shared.Implants.Components; using Content.Shared.Interaction.Events; +using Content.Shared.Mindshield; using Content.Shared.Mobs; using Content.Shared.Store; using Content.Shared.VoiceMask; @@ -18,10 +19,13 @@ public abstract partial class SharedSubdermalImplantSystem SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); + SubscribeLocalEvent(RelayToImplantEvent); // Ref relays, for when you need to write to the event! SubscribeLocalEvent(RefRelayToImplantEvent); SubscribeLocalEvent(RefRelayToImplantEvent); + SubscribeLocalEvent(RefRelayToImplantEvent, after: [typeof(MindShieldSystem)]); + SubscribeLocalEvent(RefRelayToImplantEvent); } /// diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index 7283fb6153..39724ffd75 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -16,6 +16,7 @@ using Content.Shared.Gravity; using Content.Shared.IdentityManagement.Components; using Content.Shared.Implants; using Content.Shared.Inventory.Events; +using Content.Shared.Mindshield; using Content.Shared.Lube; using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; @@ -64,6 +65,7 @@ public partial class InventorySystem SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); + SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); @@ -85,6 +87,7 @@ public partial class InventorySystem SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); + SubscribeLocalEvent(RefRelayInventoryEvent, after: [typeof(SharedSubdermalImplantSystem)]); // ordering so that things like mindshield-flipping hats can be done SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); diff --git a/Content.Shared/Mindshield/Components/FakeMindShieldImplantComponent.cs b/Content.Shared/Mindshield/Components/FakeMindShieldImplantComponent.cs deleted file mode 100644 index 788de804f4..0000000000 --- a/Content.Shared/Mindshield/Components/FakeMindShieldImplantComponent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Mindshield.Components; - -[RegisterComponent, NetworkedComponent] -public sealed partial class FakeMindShieldImplantComponent : Component -{ -} diff --git a/Content.Shared/Mindshield/Components/FakeMindshieldComponent.cs b/Content.Shared/Mindshield/Components/FakeMindshieldComponent.cs index 106f43367e..5705398d5b 100644 --- a/Content.Shared/Mindshield/Components/FakeMindshieldComponent.cs +++ b/Content.Shared/Mindshield/Components/FakeMindshieldComponent.cs @@ -1,22 +1,57 @@ -using Content.Shared.StatusIcon; +using Content.Shared.Tag; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; namespace Content.Shared.Mindshield.Components; +/// +/// A toggleable fake mindshield that only produces mindshield visuals, but does not protect against anything. +/// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(MindShieldSystem), typeof(FakeMindShieldSystem))] public sealed partial class FakeMindShieldComponent : Component { - /// /// The state of the Fake mindshield, if true the owning entity will display a mindshield effect on their job icon /// [DataField, AutoNetworkedField] - public bool IsEnabled { get; set; } = false; + public bool IsEnabled; /// - /// The Security status icon displayed to the security officer. Should be a duplicate of the one the mindshield uses since it's spoofing that + /// This tag should be placed on the fake mindshield action so there is a way to easily identify which action goes with which fake mindshield. Of course, this implies that no two similar fake mindshields should be on the same entity at the same time. /// [DataField, AutoNetworkedField] - public ProtoId MindShieldStatusIcon = "MindShieldIcon"; + public ProtoId ActionTag = "FakeMindShieldImplant"; + + /// + /// Makes it so chameleon implants affect this fake mindshield. + /// + [DataField, AutoNetworkedField] + public bool ChameleonControllable = true; + + /// + /// Whether the fake mindshield is innate to the entity. + /// When added to an entity while this field is set to true, the entity itself will gain the action & UI necessary to toggle its fake mindshield. + /// When this field is set to false, then the entity with this component will be a provider (either through implanting or through wearing) of the fake mindshield abilities for another entity. + /// + [DataField, AutoNetworkedField] + public bool IsInnate; + + /// + /// Action linked to the fake mindshield, used for innate components + /// + [DataField, AutoNetworkedField] + public EntProtoId Action = "FakeMindShieldToggleAction"; + + /// + /// Popup to show to the owner of the fake mindshield when it is enabled. + /// + [DataField, AutoNetworkedField] + public LocId EnabledPopup = "fake-mindshield-enabled"; + + /// + /// Popup to show to the owner of the fake mindshield when it is enabled. + /// + [DataField, AutoNetworkedField] + public LocId DisabledPopup = "fake-mindshield-disabled"; } diff --git a/Content.Shared/Mindshield/Components/MindShieldComponent.cs b/Content.Shared/Mindshield/Components/MindShieldComponent.cs index 77f5497973..db04097693 100644 --- a/Content.Shared/Mindshield/Components/MindShieldComponent.cs +++ b/Content.Shared/Mindshield/Components/MindShieldComponent.cs @@ -1,16 +1,10 @@ -using Content.Shared.Revolutionary; using Robust.Shared.GameStates; -using Content.Shared.StatusIcon; -using Robust.Shared.Prototypes; namespace Content.Shared.Mindshield.Components; /// -/// If a player has a Mindshield they will get this component to prevent conversion. +/// This component, on a clothing item, on an implant or on an entity, prevents "mind control". This means that you won't be convertable to the revolution, for instance. /// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))] -public sealed partial class MindShieldComponent : Component -{ - [DataField, ViewVariables(VVAccess.ReadWrite)] - public ProtoId MindShieldStatusIcon = "MindShieldIcon"; -} +[RegisterComponent, NetworkedComponent] +[Access(typeof(MindShieldSystem))] +public sealed partial class MindShieldComponent : Component; diff --git a/Content.Shared/Mindshield/Components/MindShieldImplantComponent.cs b/Content.Shared/Mindshield/Components/MindShieldImplantComponent.cs index 09de4467be..7fd866e430 100644 --- a/Content.Shared/Mindshield/Components/MindShieldImplantComponent.cs +++ b/Content.Shared/Mindshield/Components/MindShieldImplantComponent.cs @@ -4,7 +4,7 @@ using Robust.Shared.GameStates; namespace Content.Shared.Mindshield.Components; /// -/// Component given to an entity to mark it is a mindshield implant. +/// Component given to an entity to mark it is a mindshield implant that will unconvert revolutionaries when implanted. /// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))] +[RegisterComponent, NetworkedComponent, Access(typeof(RevolutionarySystem))] public sealed partial class MindShieldImplantComponent : Component; diff --git a/Content.Shared/Mindshield/Components/MindShieldStatusComponent.cs b/Content.Shared/Mindshield/Components/MindShieldStatusComponent.cs new file mode 100644 index 0000000000..393fd0afb2 --- /dev/null +++ b/Content.Shared/Mindshield/Components/MindShieldStatusComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Mindshield.Components; + +/// +/// This component stores an entity's mindshield status for easier retrieval later on +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(MindShieldSystem))] +public sealed partial class MindShieldStatusComponent : Component +{ + /// + /// Whether the entity is protected from mind control & co. + /// + [DataField, AutoNetworkedField] + public bool IsMindshielded; + + /// + /// Whether the sec HUD will show a mindshield icon + /// + [DataField, AutoNetworkedField] + public bool IsVisible; +} diff --git a/Content.Shared/Mindshield/FakeMindShield/FakeMindshieldSystem.cs b/Content.Shared/Mindshield/FakeMindShield/FakeMindshieldSystem.cs deleted file mode 100644 index 9414332068..0000000000 --- a/Content.Shared/Mindshield/FakeMindShield/FakeMindshieldSystem.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Content.Shared.Actions; -using Content.Shared.Actions.Components; -using Content.Shared.Implants; -using Content.Shared.Mindshield.Components; -using Content.Shared.Popups; -using Content.Shared.Tag; -using Robust.Shared.Prototypes; - -namespace Content.Shared.Mindshield.FakeMindShield; - -/// -/// This system is responsible for handling the fake mindshield implant. -/// -public sealed partial class FakeMindShieldSystem : EntitySystem -{ - [Dependency] private SharedActionsSystem _actions = default!; - [Dependency] private SharedPopupSystem _popup = default!; - [Dependency] private TagSystem _tag = default!; - - /// - /// This tag should be placed on the fake mindshield action for identification. - /// - private static readonly ProtoId FakeMindShieldImplantTag = "FakeMindShieldImplant"; - - /// - /// Displays a popup to inform the player of activation or deactivation. - /// - /// - /// An associated tuple. The state in will be used - /// to display the appropriate popup. - /// - private void ShowTogglePopup(Entity ent) - { - var message = ent.Comp.IsEnabled - ? Loc.GetString("fake-mindshield-enabled") - : Loc.GetString("fake-mindshield-disabled"); - - _popup.PopupEntity(message, ent, ent); - } - - /// - /// Run when is raised. Toggles the component and the action indication. - /// - /// An associated tuple. The state in will be toggled. - /// The event arguments. Will be marked as handled. - [SubscribeLocalEvent] - private void OnToggleMindshield(Entity ent, ref FakeMindShieldToggleEvent args) - { - ent.Comp.IsEnabled = !ent.Comp.IsEnabled; - args.Toggle = true; - args.Handled = true; - ShowTogglePopup(ent); - Dirty(ent); - } - - /// - /// Handles implant interactions with chameleon outfits. - /// (De)Activates the mindshield if the chameleon outfit requires it. Displays a popup if state changes. - /// - /// An associated tuple. The state in will be set if changed. - /// The event arguments. - [SubscribeLocalEvent] - private void OnChameleonControllerOutfitSelected(Entity ent, ref ChameleonControllerOutfitSelectedEvent args) - { - if (ent.Comp.IsEnabled == args.ChameleonOutfit.HasMindShield) - return; - - // This assumes there is only one fake mindshield action per entity (This is currently enforced) - if (!TryComp(ent, out var actionsComp)) - return; - - ent.Comp.IsEnabled = args.ChameleonOutfit.HasMindShield; - - foreach (var action in actionsComp.Actions) - { - if (!_tag.HasTag(action, FakeMindShieldImplantTag)) - continue; - - if (!TryComp(action, out var actionComp)) - continue; - - _actions.SetToggled(action, args.ChameleonOutfit.HasMindShield); - ShowTogglePopup(ent); - - if (actionComp.UseDelay != null) - _actions.SetCooldown(action, actionComp.UseDelay.Value); - - break; - } - - Dirty(ent); - } -} - -/// -/// Generic . -/// -public sealed partial class FakeMindShieldToggleEvent : InstantActionEvent; diff --git a/Content.Shared/Mindshield/FakeMindshieldSystem.cs b/Content.Shared/Mindshield/FakeMindshieldSystem.cs new file mode 100644 index 0000000000..64783d56a3 --- /dev/null +++ b/Content.Shared/Mindshield/FakeMindshieldSystem.cs @@ -0,0 +1,200 @@ +using Content.Shared.Actions; +using Content.Shared.Actions.Components; +using Content.Shared.Clothing; +using Content.Shared.Implants; +using Content.Shared.Inventory; +using Content.Shared.Mindshield.Components; +using Content.Shared.Popups; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Mindshield; + +/// +/// This system handles fake mindshields represented by . Works using 's +/// +public sealed partial class FakeMindShieldSystem : EntitySystem +{ + [Dependency] private SharedActionsSystem _actions = default!; + [Dependency] private TagSystem _tag = default!; + [Dependency] private IGameTiming _timing = default!; + [Dependency] private MindShieldSystem _mindShields = default!; + [Dependency] private SharedPopupSystem _popups = default!; + + public override void Initialize() + { + base.Initialize(); + + // Other events + SubscribeLocalEvent>((ent, ref relay) => OnChameleonControllerOutfitSelectedImplant(ent, ref relay.Args, relay.ImplantedEntity)); + + // Toggle events + SubscribeLocalEvent>((e, ref sk) => OnToggleMindshield(e, ref sk.Args)); + SubscribeLocalEvent>((e, ref sk) => OnToggleMindshield(e, ref sk.Args)); + + // Mindshield events + SubscribeLocalEvent>((a, ref k) => OnQueryStatus(a, ref k.Args)); + SubscribeLocalEvent>((a, ref k) => OnQueryStatus(a, ref k.Args)); + } + + /// + /// Displays a popup to inform the player of activation or deactivation. + /// + /// + /// An associated tuple. The state in will be used + /// to display the appropriate popup. + /// + /// The entity that toggled the fake mindshield. + private void ShowTogglePopup(Entity ent, EntityUid performer) + { + var message = ent.Comp.IsEnabled + ? Loc.GetString(ent.Comp.EnabledPopup) + : Loc.GetString(ent.Comp.DisabledPopup); + + _popups.PopupEntity(message, performer, performer); + } + + [SubscribeLocalEvent] + private void OnFakeMindshieldEquip(Entity ent, ref ClothingGotEquippedEvent args) + { + _mindShields.RefreshMindshieldStatus(args.Wearer); + } + + [SubscribeLocalEvent] + private void OnFakeMindshieldUnequip(Entity ent, ref ClothingGotUnequippedEvent args) + { + _mindShields.RefreshMindshieldStatus(args.Wearer); + } + + [SubscribeLocalEvent] + private void OnFakeMindshieldRemoved(Entity ent, ref ComponentRemove args) + { + if (!ent.Comp.IsInnate) + return; + + _mindShields.RefreshMindshieldStatus(ent.Owner); + } + + [SubscribeLocalEvent] + private void OnFakeMindshieldImplantRemoved(Entity ent, ref ImplantRemovedEvent args) + { + // when we reinject the mindshield, the action will be disabled (since its a fresh action) + // which would cause it to be desynced with the state of the mindshield + // so we just disable it here + ent.Comp.IsEnabled = false; + Dirty(ent); + + _mindShields.RefreshMindshieldStatus(args.Implanted); + } + + [SubscribeLocalEvent] + private void OnFakeMindshieldImplanted(Entity ent, ref ImplantImplantedEvent args) + { + _mindShields.RefreshMindshieldStatus(args.Implanted); + } + + [SubscribeLocalEvent] + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + if (!ent.Comp.IsInnate) + return; + + _actions.AddAction(ent.Owner, ent.Comp.Action); + _mindShields.RefreshMindshieldStatus(ent.Owner); + } + + [SubscribeLocalEvent] + private void OnQueryStatus(Entity ent, ref GetMindShieldStatusEvent args) + { + args.IsVisible |= ent.Comp.IsEnabled; + } + + [SubscribeLocalEvent] + private void OnToggleMindshield(Entity ent, ref FakeMindShieldToggleEvent args) + { + if (args.ActionTag != ent.Comp.ActionTag) + return; + + ent.Comp.IsEnabled = !ent.Comp.IsEnabled; + args.Toggle = true; + args.Handled = true; + ShowTogglePopup(ent, args.Performer); + Dirty(ent.Owner, ent.Comp); + _mindShields.RefreshMindshieldStatus(args.Performer); + } + + [SubscribeLocalEvent] + private void OnChameleonControllerOutfitSelected(Entity ent, ref ChameleonControllerOutfitSelectedEvent args) + { + HandleChameleonOutfit(ent, ent.Comp, ent.Owner, ent.Owner, ref args); + } + + private void OnChameleonControllerOutfitSelectedImplant(Entity ent, ref ChameleonControllerOutfitSelectedEvent args, EntityUid implantedEntity) + { + HandleChameleonOutfit(ent, ent.Comp, implantedEntity, implantedEntity, ref args); + } + + private void HandleChameleonOutfit(EntityUid fakeMindshieldUid, FakeMindShieldComponent component, EntityUid actionOwner, EntityUid statusOwner, ref ChameleonControllerOutfitSelectedEvent args) + { + if (!component.ChameleonControllable) + return; + + if (component.IsEnabled == args.ChameleonOutfit.HasMindShield) + return; + + // This assumes there is only one fake mindshield action per entity (This is currently enforced) + if (!TryComp(actionOwner, out var actionsComp)) + return; + + // In case the fake mindshield ever doesn't have an action. + var actionFound = false; + + foreach (var action in actionsComp.Actions) + { + if (!_tag.HasTag(action, component.ActionTag)) + continue; + + if (!TryComp(action, out var actionComp)) + continue; + + actionFound = true; + + if (_actions.IsCooldownActive(actionComp, _timing.CurTime)) + continue; + + component.IsEnabled = args.ChameleonOutfit.HasMindShield; + _actions.SetToggled(action, args.ChameleonOutfit.HasMindShield); + Dirty(fakeMindshieldUid, component); + + if (actionComp.UseDelay != null) + _actions.SetCooldown(action, actionComp.UseDelay.Value); + + _mindShields.RefreshMindshieldStatus(statusOwner); + return; + } + + // If they don't have the action for some reason, still set it correctly. + if (!actionFound) + { + component.IsEnabled = args.ChameleonOutfit.HasMindShield; + Dirty(fakeMindshieldUid, component); + _mindShields.RefreshMindshieldStatus(statusOwner); + } + } +} + +/// +/// Inventory relayed event telling all fake mindshields with the same ActionTag to try to toggle themselves. +/// +public sealed partial class FakeMindShieldToggleEvent : InstantActionEvent, IInventoryRelayEvent +{ + public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; + + /// + /// The tag the fake mindshield needs to have to be successfully toggled. + /// We seperate it out to allow an entity to, for example, have an implant one and a store bought one without conflicts. + /// + [DataField] + public ProtoId ActionTag = "FakeMindShieldImplant"; +} diff --git a/Content.Shared/Mindshield/MindShieldSystem.cs b/Content.Shared/Mindshield/MindShieldSystem.cs new file mode 100644 index 0000000000..0b5c187b36 --- /dev/null +++ b/Content.Shared/Mindshield/MindShieldSystem.cs @@ -0,0 +1,139 @@ +using Content.Shared.Clothing; +using Content.Shared.Implants; +using Content.Shared.Inventory; +using Content.Shared.Mindshield.Components; +using Content.Shared.StatusIcon; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Mindshield; + +public sealed partial class MindShieldSystem : EntitySystem +{ + [Dependency] private IGameTiming _timing = default!; + /// + /// Status icon displayed in the sec HUD. + /// + public static ProtoId StatusIcon = "MindShieldIcon"; + + public override void Initialize() + { + base.Initialize(); + + // Mind shield status events + SubscribeLocalEvent>((e, ref k) => OnStatusQuery(e, ref k.Args)); + SubscribeLocalEvent>((e, ref k) => OnStatusQuery(e, ref k.Args)); + } + + [SubscribeLocalEvent] + private void OnMindshieldUnequip(Entity ent, ref ClothingGotUnequippedEvent args) + { + if (_timing.ApplyingState) + return; + + RefreshMindshieldStatus(args.Wearer); + } + + [SubscribeLocalEvent] + private void OnMindshieldEquip(Entity ent, ref ClothingGotEquippedEvent args) + { + if (_timing.ApplyingState) + return; + + RefreshMindshieldStatus(args.Wearer); + } + + [SubscribeLocalEvent] + private void OnMindshieldRemoved(Entity ent, ref ComponentRemove args) + { + RefreshMindshieldStatus(ent.Owner); + } + + [SubscribeLocalEvent] + private void OnMindshieldImplantRemoved(Entity ent, ref ImplantRemovedEvent args) + { + RefreshMindshieldStatus(args.Implanted); + } + + [SubscribeLocalEvent] + private void OnMindshieldImplanted(Entity ent, ref ImplantImplantedEvent args) + { + RefreshMindshieldStatus(args.Implanted); + } + + [SubscribeLocalEvent] + private void OnMindshieldMapInit(Entity ent, ref MapInitEvent args) + { + // todo: make it not refresh on implant & clothing items + RefreshMindshieldStatus(ent.Owner); + } + + /// + /// This function updates . It should be called when anything makes a modification of its mindshielded-ness. + /// + public void RefreshMindshieldStatus(EntityUid ent) + { + var ev = new GetMindShieldStatusEvent(); + RaiseLocalEvent(ent, ref ev); + var mindshielded = ev.IsMindshielded; + var visible = ev.IsVisible; + + if (!mindshielded && !visible) + { + if (HasComp(ent)) + RemCompDeferred(ent); + } + else + { + EnsureComp(ent, out var c); + c.IsMindshielded = mindshielded; + c.IsVisible = visible; + Dirty(ent, c); + } + } + + [SubscribeLocalEvent] + private void OnStatusQuery(Entity e, ref GetMindShieldStatusEvent args) + { + args.IsMindshielded = true; + args.IsVisible = true; + } + + /// + /// Retrieves mindshielding data of an entity. Works via , and so requires proper dirtying on the part of mindshield providers. + /// + /// The entity to check the mindshield status of. + /// If the entity has a functional mind shield + /// Wether the entity shows a mindshield icon on the sec HUD + /// You should never look for a mindshield component and instead use this function. + public void GetMindshieldStatus(EntityUid entity, out bool isMindshielded, out bool isVisible) + { + if (TryComp(entity, out var comp)) + { + isMindshielded = comp.IsMindshielded; + isVisible = comp.IsVisible; + } + else + { + isMindshielded = isVisible = false; + } + } +} + +/// +/// Raised in order to get whether an entity is mindshielded visually, mechanically or both. +/// +[ByRefEvent] +public sealed class GetMindShieldStatusEvent : EntityEventArgs, IInventoryRelayEvent +{ + public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; + /// + /// Whether the entity is mindshielded. + /// + public bool IsMindshielded; + + /// + /// Whether a mindshield icon is present + /// + public bool IsVisible; +} diff --git a/Content.Shared/Revolutionary/Components/AlwaysRevolutionaryConvertibleComponent.cs b/Content.Shared/Revolutionary/Components/AlwaysRevolutionaryConvertibleComponent.cs index 694c6d2f06..4b5f09aee9 100644 --- a/Content.Shared/Revolutionary/Components/AlwaysRevolutionaryConvertibleComponent.cs +++ b/Content.Shared/Revolutionary/Components/AlwaysRevolutionaryConvertibleComponent.cs @@ -5,7 +5,7 @@ namespace Content.Shared.Revolutionary.Components; /// /// Component used for allowing non-humans to be converted. (Mainly monkeys) /// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))] +[RegisterComponent, NetworkedComponent, Access(typeof(RevolutionarySystem))] public sealed partial class AlwaysRevolutionaryConvertibleComponent : Component { diff --git a/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs b/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs index 12589850e6..c8c7c41fea 100644 --- a/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs +++ b/Content.Shared/Revolutionary/Components/HeadRevolutionaryComponent.cs @@ -8,7 +8,7 @@ namespace Content.Shared.Revolutionary.Components; /// /// Component used for marking a Head Rev for conversion and winning/losing. /// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))] +[RegisterComponent, NetworkedComponent, Access(typeof(RevolutionarySystem))] public sealed partial class HeadRevolutionaryComponent : Component { /// diff --git a/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs b/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs index 513e8897bd..a32bbca7a9 100644 --- a/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs +++ b/Content.Shared/Revolutionary/Components/RevolutionaryComponent.cs @@ -9,7 +9,7 @@ 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))] +[RegisterComponent, NetworkedComponent, Access(typeof(RevolutionarySystem))] public sealed partial class RevolutionaryComponent : Component { /// @@ -24,5 +24,11 @@ public sealed partial class RevolutionaryComponent : Component [DataField] public SoundSpecifier RevStartSound = new SoundPathSpecifier("/Audio/Ambience/Antag/headrev_start.ogg"); + /// + /// The duration of the stun when you get deconverted. + /// + [DataField] + public TimeSpan StunTime = TimeSpan.FromSeconds(4f); + public override bool SessionSpecific => true; } diff --git a/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs b/Content.Shared/Revolutionary/RevolutionarySystem.cs similarity index 65% rename from Content.Shared/Revolutionary/SharedRevolutionarySystem.cs rename to Content.Shared/Revolutionary/RevolutionarySystem.cs index a5947fd0fc..fa8a4f926e 100644 --- a/Content.Shared/Revolutionary/SharedRevolutionarySystem.cs +++ b/Content.Shared/Revolutionary/RevolutionarySystem.cs @@ -1,56 +1,26 @@ -using Content.Shared.IdentityManagement; -using Content.Shared.Mindshield.Components; -using Content.Shared.Popups; using Content.Shared.Revolutionary.Components; -using Content.Shared.Stunnable; using Robust.Shared.GameStates; using Robust.Shared.Player; using Content.Shared.Antag; +using Content.Shared.StatusIcon.Components; namespace Content.Shared.Revolutionary; -public abstract partial class SharedRevolutionarySystem : EntitySystem +public sealed partial class RevolutionarySystem : EntitySystem { - [Dependency] private SharedPopupSystem _popupSystem = default!; - [Dependency] private SharedStunSystem _sharedStun = default!; - public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(MindShieldImplanted); - SubscribeLocalEvent(OnRevCompGetStateAttempt); - SubscribeLocalEvent(OnRevCompGetStateAttempt); SubscribeLocalEvent(DirtyRevComps); SubscribeLocalEvent(DirtyRevComps); SubscribeLocalEvent(DirtyRevComps); - SubscribeLocalEvent(OnAttemptConvert); - } - - /// - /// 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, MapInitEvent init) - { - if (HasComp(uid)) - { - RemCompDeferred(uid); - return; - } - - if (HasComp(uid)) - { - var stunTime = TimeSpan.FromSeconds(4); - var name = Identity.Entity(uid, EntityManager); - RemComp(uid); - _sharedStun.TryUpdateParalyzeDuration(uid, stunTime); - _popupSystem.PopupEntity(Loc.GetString("rev-break-control", ("name", name)), uid); - } } /// /// Determines if a HeadRev component should be sent to the client. /// + [SubscribeLocalEvent] private void OnRevCompGetStateAttempt(EntityUid uid, HeadRevolutionaryComponent comp, ref ComponentGetStateAttemptEvent args) { args.Cancelled = !CanGetState(args.Player); @@ -59,6 +29,7 @@ public abstract partial class SharedRevolutionarySystem : EntitySystem /// /// Determines if a Rev component should be sent to the client. /// + [SubscribeLocalEvent] private void OnRevCompGetStateAttempt(EntityUid uid, RevolutionaryComponent comp, ref ComponentGetStateAttemptEvent args) { args.Cancelled = !CanGetState(args.Player); @@ -80,6 +51,7 @@ public abstract partial class SharedRevolutionarySystem : EntitySystem return HasComp(uid); } + /// /// Dirties all the Rev components so they are sent to clients. /// @@ -102,6 +74,7 @@ public abstract partial class SharedRevolutionarySystem : EntitySystem } } + [SubscribeLocalEvent] private void OnAttemptConvert(Entity ent, ref AttemptConvertRevolutionaryEvent args) { args.Cancelled = true; @@ -112,4 +85,21 @@ public abstract partial class SharedRevolutionarySystem : EntitySystem { args.Cancelled = true; } + + [SubscribeLocalEvent] + private void GetRevIcon(Entity ent, ref GetStatusIconsEvent args) + { + if (HasComp(ent)) + return; + + if (ProtoMan.Resolve(ent.Comp.StatusIcon, out var iconPrototype)) + args.StatusIcons.Add(iconPrototype); + } + + [SubscribeLocalEvent] + private void GetHeadRevIcon(Entity ent, ref GetStatusIconsEvent args) + { + if (ProtoMan.Resolve(ent.Comp.StatusIcon, out var iconPrototype)) + args.StatusIcons.Add(iconPrototype); + } } diff --git a/Resources/Locale/en-US/changeling/changeling.ftl b/Resources/Locale/en-US/changeling/changeling.ftl index 02636a33e5..087e07f40f 100644 --- a/Resources/Locale/en-US/changeling/changeling.ftl +++ b/Resources/Locale/en-US/changeling/changeling.ftl @@ -51,5 +51,9 @@ changeling-takeover-success-self = We have successfully infested the body and as # stings changeling-sting-success = We silently sting {THE($target)}! +# mindshield +changeling-fake-mindshield-enabled = We emit fake mindshield waves. +changeling-fake-mindshield-disabled = We stop emitting fake mindshield waves. + # other changeling-paused-map-name = Changeling identity storage map diff --git a/Resources/Locale/en-US/store/changeling-catalog.ftl b/Resources/Locale/en-US/store/changeling-catalog.ftl index 5184fd62d9..7524ef322c 100644 --- a/Resources/Locale/en-US/store/changeling-catalog.ftl +++ b/Resources/Locale/en-US/store/changeling-catalog.ftl @@ -33,5 +33,8 @@ changeling-catalog-last-resort-desc = Gib your current body and escape as a weak Warning! This resets your progress as a changeling, removing your store purchases and absorbed identities! +changeling-catalog-voice-mindshield-name = Fake Mindshield +changeling-catalog-voice-mindshield-desc = Modify your neuron's natural waves to copy those produced by Nanotrasen mindshield implants, making you look mindshielded to security members. Can be toggled. + changeling-catalog-night-vision-name = Night Vision changeling-catalog-night-vision-desc = You modify your photoreceptors and heighten your senses as to become able to see in complete darkness. diff --git a/Resources/Prototypes/Actions/changeling.yml b/Resources/Prototypes/Actions/changeling.yml index d5864f781d..3554546312 100644 --- a/Resources/Prototypes/Actions/changeling.yml +++ b/Resources/Prototypes/Actions/changeling.yml @@ -304,3 +304,24 @@ probability: 0.5 - type: InstantAction event: !type:ScreechActionEvent + +- type: entity + parent: FakeMindShieldToggleAction + id: ActionChangelingFakeMindshieldToggle + name: '[color=green]Toggle Fake Mindshield[/color]' + description: Turn our mindshield visibility on or off. + components: + - type: Sprite + sprite: Interface/Actions/changeling2.rsi + layers: + - state: mindshield_off + map: ["enum.ActionVisuals.Icon"] + - state: mindshield_on + map: ["enum.ActionVisuals.IconToggled"] + visible: false + - type: InstantAction + event: !type:FakeMindShieldToggleEvent + actionTag: FakeMindShieldChangeling + - type: Tag + tags: + - FakeMindShieldChangeling # used to distinguish the changeling's toggle mindshield action from the implant's diff --git a/Resources/Prototypes/Catalog/changeling_catalog.yml b/Resources/Prototypes/Catalog/changeling_catalog.yml index 7c8676810e..748cd9f50b 100644 --- a/Resources/Prototypes/Catalog/changeling_catalog.yml +++ b/Resources/Prototypes/Catalog/changeling_catalog.yml @@ -174,3 +174,17 @@ - !type:ListingLimitedStockCondition stock: 1 productAction: ActionChangelingScreech + +- type: listing + id: ChangelingFakeMindshield + name: changeling-catalog-voice-mindshield-name + description: changeling-catalog-voice-mindshield-desc + icon: { sprite: /Textures/Interface/Actions/changeling2.rsi, state: mindshield_on } + cost: + ChangelingDNA: 10 + categories: + - ChangelingStoreStealth + conditions: + - !type:ListingLimitedStockCondition + stock: 1 + productComponents: ChangelingFakeMindshieldDummy diff --git a/Resources/Prototypes/Entities/Mobs/Player/changeling.yml b/Resources/Prototypes/Entities/Mobs/Player/changeling.yml index 543a34e30c..b16ebc7862 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/changeling.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/changeling.yml @@ -108,3 +108,15 @@ noiseMultiplier: 2 overlayColor: "#AB0808AA" prioritized: false # Technically eye augments, so we want things such as NVGs to take priority if able + +- type: entity # dummy prototype for the store listing + id: ChangelingFakeMindshieldDummy + categories: [ HideSpawnMenu ] + components: + - type: FakeMindShield + actionTag: FakeMindShieldChangeling + chameleonControllable: false + isInnate: true + enabledPopup: changeling-fake-mindshield-enabled + disabledPopup: changeling-fake-mindshield-disabled + action: ActionChangelingFakeMindshieldToggle diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml index f96703519d..efadfffe62 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml @@ -366,9 +366,7 @@ components: - type: SubdermalImplant implantAction: FakeMindShieldToggleAction - implantComponents: - - type: FakeMindShield # TODO: put the component on the implant and use implant relay events for the status icon - - type: FakeMindShieldImplant + - type: FakeMindShield # Sec and Command implants @@ -381,6 +379,7 @@ components: - type: SubdermalImplant - type: MindShieldImplant + - type: MindShield # Centcomm implants diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index 08002a7947..860f07052f 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -559,6 +559,9 @@ - type: Tag id: FakeMindShieldImplant # Used by FakeMindShieldSystem to toggle the tagged action when a chameleon outfit is selected. +- type: Tag # Also used by FakeMindShieldSystem, but only for changelings + id: FakeMindShieldChangeling + - type: Tag id: FakeNukeDisk # Exists so that the fake nuke disk can be blacklisted by storages that blacklist the real disk. diff --git a/Resources/Textures/Interface/Actions/actions_fakemindshield.rsi/icon-on.png b/Resources/Textures/Interface/Actions/actions_fakemindshield.rsi/icon-on.png index 19b4e13ec1de201a8796f0aed015f80c89fac805..45f085996a0863bede94f2d0d8c231e13e081344 100644 GIT binary patch delta 1037 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 T?rKOZ%(;I6jiy)X diff --git a/Resources/Textures/Interface/Actions/actions_fakemindshield.rsi/icon.png b/Resources/Textures/Interface/Actions/actions_fakemindshield.rsi/icon.png index 9eb33d5350c59302827974286a5bc166c5b62723..658ff0afc1f24c6ca79c293271245311cc169326 100644 GIT binary patch delta 1028 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 Ly5c}=&XRuvnz2Z< diff --git a/Resources/Textures/Interface/Actions/actions_fakemindshield.rsi/meta.json b/Resources/Textures/Interface/Actions/actions_fakemindshield.rsi/meta.json index 31f288131f..9aa1bd716b 100644 --- a/Resources/Textures/Interface/Actions/actions_fakemindshield.rsi/meta.json +++ b/Resources/Textures/Interface/Actions/actions_fakemindshield.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, - "license": "CC-BY-SA-3.0", - "copyright": "Created by Ubaser (Discord) for SS14, Modified for purpose by brassicaprime69 (Discord)", + "license": "CC-BY-3.0", + "copyright": "By ketufaispikinut (Github)", "size": { "x": 32, "y": 32 diff --git a/Resources/Textures/Interface/Actions/changeling2.rsi/meta.json b/Resources/Textures/Interface/Actions/changeling2.rsi/meta.json index d1c4f3d110..07e68df6ba 100644 --- a/Resources/Textures/Interface/Actions/changeling2.rsi/meta.json +++ b/Resources/Textures/Interface/Actions/changeling2.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Made by ketufaispikinut from a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (scientist suit), a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (hydro suit) and the changeling ability border/background sprites created by TiniestShark. Biodegrade, Mimicry and Extract Dna On/Off, Blind, Mute, Neurodepressant, Cryogenic, Lethal and Hallucination stings, screech, nv on/off by ketufaispikinut, based on TiniestShark's textures. Shop icon by ketufaispikinut, based on TiniestShark's textures. last_resort taken from https://github.com/tgstation/tgstation/blob/5c90546a27a1e5ea59b9f127ae274d7341acc70b/icons/mob/actions/actions_changeling.dmi and modified by Pok27 (GitHub). take_over_corpse by Pok27 (GitHub).", + "copyright": "Made by ketufaispikinut from a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (scientist suit), a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (hydro suit) and the changeling ability border/background sprites created by TiniestShark. Biodegrade, Mimicry and Extract Dna On/Off, Blind, Mute, Neurodepressant, Cryogenic, Lethal and Hallucination stings, screech, nv on/off by ketufaispikinut, based on TiniestShark's textures. Shop icon by ketufaispikinut, based on TiniestShark's textures. last_resort taken from https://github.com/tgstation/tgstation/blob/5c90546a27a1e5ea59b9f127ae274d7341acc70b/icons/mob/actions/actions_changeling.dmi and modified by Pok27 (GitHub). take_over_corpse by Pok27 (GitHub). Mindshield on/off textures by ketufaispikinut, based on TiniestShark's textures.", "size": { "x": 32, "y": 32 @@ -43,6 +43,12 @@ { "name": "extract_dna_off" }, + { + "name": "mindshield_on" + }, + { + "name": "mindshield_off" + }, { "name": "shop" }, diff --git a/Resources/Textures/Interface/Actions/changeling2.rsi/mindshield_off.png b/Resources/Textures/Interface/Actions/changeling2.rsi/mindshield_off.png new file mode 100644 index 0000000000000000000000000000000000000000..5067de30323253bda83cb42c9deb8ca71e3a0160 GIT binary patch literal 1971 zcmV;k2Tb^hP)EX>4Tx04R}tk-ba9P!z>aTT7)>94sgzn4vn^MMWHI6^c+H)C#RSn7s50nlvOS zE{=k0!NHHks)LKOt`4q(Aou~|A0SSOE>hxsNufoI7as4&dG{RNyASX;t4uX}5`d~% zMkbXMa`{yu^ok(D@L?2#5;OHVQB1?LzV4}$>MqW+y!-yFKBZtXz$XwVm~L3a8^kl4 zmd<&fILgYBLVQj)XFRyI`PY2uioYLxHK zxvX&B;;dHdtbI@Z!bm||S>`&e5u~t)C5R9pqmBxyun?zRBgI69&XXSgVaG3$OD0zZ zj2sK7L51Y_!T;d*Y|Y}-q?;5;0>KyC{ul#7yFja9+uz5w-8umR&%l+|@z?i4s zjutxt`nQ3L>y9Sx0hc?#(3371k|PCZ`HMy1{fxdT4-DJ_;Wc+|?Q@(y09l$<`UW^S z1jb90z3%bu-tM{m+tZ%k4^fSBnFETljsO4v24YJ`L;y|zP5?suR^1^0000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2kHe10V5OVgQ3^}00pl}L_t(o!>yNZXcKoB$3GV@ zqHSX?(q%~uv}Ng{)U_gF=ZkTEj2W?&AhbPtu;Aw4bE$GdvU$%DoT&czQq0Pr<=?E9Qnz}MtqA^ivE?vDrn1HIjbH{8M(Km8(z zPGRwMo;J4|Fb%nT=7gcjX}sYUR5@+)d6paP^I#@Jq;(GfVc_A!V-7XD?fV8GM+fsl zD}B3{jBv>OSC!M;J#)gaz5Fs7HB99uS&Z`BQkloj?=!k?D&Ml1^vtZScv4ZSkdj?K zT7n?}KGy#MpdcFF^BGa1_4Zf;K}Y2Lhv^W9zC92w(%)A{bMGF1D=#|3dPli|pY z9X4#=96yFCr}2oc4GAqQmo{v`&5b-g!Q zqX2llUQ|`BIt#POw;Hm`@8?cWk8x^fNDxgp(jczIw$l4o;dZ=UF90r=dDqa;KsX#; zOU~7^yvnDl>bk~Qr~`<*{C)~7A{DtYjwR;6Yhft50XlyYz2`>gkUg8dwY8o|NQ(s zsZJ z27?5HL9?mF;{cROC5+XOIurKX{SjfHx7#S|g#GbRnm2DM%Yke*OCS)Sxw#op6swF3 z1OjD70BD*KmP-_tOLk^gD%c1Wi$(JJye(tB8RDu{*B;Sj zqhKZ2|Dj^BSohwu+-Mq5*}E+SYSC}sUt3q;;Po48L#UyFP$*=hT3OnYio&F#m{zF?jt3N4~mH^FPZWrx7!{&Wx^`W%%Up*K~#&VY<2sYK6dQM5bWI z*r~iyMr9Su=sIm~w>b;!8WviC(Xvjcswz!QO%#ho78VxD%J1;yOK(k3Ns`E9G6)sn zY;(Jr(e!OfIB$%6LL`$(a=9GBY?8tTyQO&RDHh&wR)kAAgwvfdqd|Cu`ua5|J3IOI{;%&OWTTH) z%ekw)M^P6SnN$>Z`TdCIvJgU@eT=%e_|6mWU0%gkdygV#v&0nzYY7ticJDTv1LDYz z9en*~{Jrl4KOBfr&~^GB4RiXVsGSRfb0rJ=e8{<+nItvVtI9;Gr1k!(#S2i-b<+TV znBR|_%at+A=(^pr2YS1W@&%SEUTaCM>@8M>*>^r49=Dr^i;HIBT1Gf2)*@9YvDRzn zLJM$kZ!Zrg9+&aneiAR^z0lQVY+qS>5x?BDi5p{M!hdU{)sze5)@uL&002ovPDHLk FV1jI(r33%~ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Actions/changeling2.rsi/mindshield_on.png b/Resources/Textures/Interface/Actions/changeling2.rsi/mindshield_on.png new file mode 100644 index 0000000000000000000000000000000000000000..1df291340b10554776fe1250b8370d396751abef GIT binary patch literal 2013 zcmV<32O{{1P)EX>4Tx04R}tk-ba9P!z>aTT7)>94sgzn4vn^MMWHI6^c+H)C#RSn7s50nlvOS zE{=k0!NHHks)LKOt`4q(Aou~|A0SSOE>hxsNufoI7as4&dG{RNyASX;t4uX}5`d~% zMkbXMa`{yu^ok(D@L?2#5;OHVQB1?LzV4}$>MqW+y!-yFKBZtXz$XwVm~L3a8^kl4 zmd<&fILgYBLVQj)XFRyI`PY2uioYLxHK zxvX&B;;dHdtbI@Z!bm||S>`&e5u~t)C5R9pqmBxyun?zRBgI69&XXSgVaG3$OD0zZ zj2sK7L51Y_!T;d*Y|Y}-q?;5;0>KyC{ul#7yFja9+uz5w-8umR&%l+|@z?i4s zjutxt`nQ3L>y9Sx0hc?#(3371k|PCZ`HMy1{fxdT4-DJ_;Wc+|?Q@(y09l$<`UW^S z1jb90z3%bu-tM{m+tZ%k4^fSBnFETljsO4v24YJ`L;y|zP5?suR^1^0000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2kHe10V4yNJXj69-$3Hi{ zS3}b*rfBTiKnyzLzf~`Dj!(*N<0j1i)Y8wZ1FUS^PC#9?HLP^zwiJ(B0Oe`+_0% zfBC&2+7IiK9}$)$z)D`S$Q$-LmqnNJKY;8)PyjMsr0L+uz4E%@FQhf*ikZ2_8zN z97rWxenQXI1F%+`03a*o5KI`&P3J~peA?MTCLz<+ceO0+r(2r<7!y_jAdijm)4eMT zy6SXm6J32*+5X&%XfyK)3g@Fy{ZcYXSdut55@Y$2Dvq^m=FGq#|2G|L*<6Hb*R5NE zcqy4=XG4Pp+k1Oj6e$ zWD@d%)^A<<3;+?go0zIXm2C}No0(^A(5OKSW zw5qNKK$IjpRxY!6Hq8A-#tpuhs#a9SL><7jJ`gDK%b9^eviVu-dBd}uj!+#NRxU&I z`B))IL|@s)TK9aJxU1^iRgH46b;ou@pN~LoE&F`|yrRo`ObSa9X3Y`j^Ey`+?&h}- zlIyu>q>o#`)4rR!J^PCIR?q!DxbPjho{MwJNla8%vqr74knoBw9zH2p`?}j&bn!rM zuVAlcc8a*&v%kFt?Tye15>$!oq&Yj?Pj?qemvgB};%WFFVW^k1XfBm;BMZ!l8b~^YBB`T9Q?=ci8t=$Vj zRw}%b5-TK$ZQdmgmzp$`N->m587>IcdmK&2q_u9r@aNoryKr-D2BO^!=01Dy*|Q)% zhmHtvV_c=N-UB1fEGXxK#l(R9jM6NKYZ_rmGJ1i@Xmqx#MU^n7s%-T6n9wu|g+kH6 zrPfzM?}u^K;m)umSqQiQR@AL?WI;AR%e9#)@|g^c&2RF@2cHm+#}^c+C<=wDi?3rb0l;In!JaQZ5)$bYq32(#cxX&j*)(;7vFn#epFHlc*h;O>&XQfX17E`n z#8kDYshP-{H>jdY7_1E}STL^5Oo0p?p*qV-Py23^Xd3`mjvZq{(`amd6L~bry_=UE z*4q&X&MG9*Db9AD(961@+-+#MFwGD013E%=mXC8i7p*ki=3PSnUq6ycrx$f8u@E@h zc|r$}Z~p4&1r8+g1`?6+pV%;W88I8qo$*Jc7Py3z-iU^hx9J}nHC&KLryRYY5)Buo z8O}`;lU3`M6kDX=QVPy02!|S5^=e^?jH=Sv*vR{rd!Fi$#a>=b<34O_Co?(8P%6dx zKmgGg7V62|zfNXy@~N$Onp^RQZS5o;JfJU?!W@Ex)0;L~#XhcS>}+V@-9P*O_nF}1 zms-hc8eM;1;n3=c6$^rWBn$feBoxI6k}~5}X&^c3K!rW!cmcASW+(vA8VHamDK2qM zvubvCTZ>*iz~bOFht$&AVrH22&F{x6N%T)n8ndCPgq>PL%N#W vMZD*3^cC^Gvw5>#YwM`#swzGo92EWqrikXJ2lfh literal 0 HcmV?d00001