diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs index f8eb12df914..4bb49fecc14 100644 --- a/Content.Client/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs @@ -19,6 +19,7 @@ using Robust.Client.GameObjects; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Client.Player; using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -31,6 +32,7 @@ namespace Content.Client.Inventory public sealed class StrippableBoundUserInterface : BoundUserInterface { [Dependency] private readonly IUserInterfaceManager _ui = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; private readonly ExamineSystem _examine; private readonly InventorySystem _inv; private readonly SharedCuffableSystem _cuffable; @@ -198,7 +200,8 @@ private void AddInventoryButton(EntityUid invUid, string slotId, InventoryCompon var entity = container.ContainedEntity; // If this is a full pocket, obscure the real entity - if (entity != null && slotDef.StripHidden) + if (entity != null && slotDef.StripHidden + && !(EntMan.TryGetComponent(_playerManager.LocalEntity, out var thiefcomponent) && thiefcomponent.IgnoreStripHidden)) entity = _virtualHiddenEntity; var button = new SlotButton(new SlotData(slotDef, container)); diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 3b38b65a19d..686570f7dca 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -36,6 +36,7 @@ public sealed class StrippableSystem : SharedStrippableSystem [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly ThievingSystem _thieving = default!; // TODO: ECS popups. Not all of these have ECS equivalents yet. @@ -251,15 +252,17 @@ private void StartStripInsertInventory( var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); - if (!stealth) - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large); + bool hidden = stealth == ThievingStealth.Hidden; - var prefix = stealth ? "stealthily " : ""; + if (!hidden) + StripPopup("strippable-component-alert-owner-insert", stealth, target, user: Identity.Entity(user, EntityManager), item: user.Comp.ActiveHandEntity!.Value); + + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, true, slot), user, target, held) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -340,20 +343,22 @@ private void StartStripRemoveInventory( var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); - if (!stealth) + bool hidden = stealth == ThievingStealth.Hidden; + + if (!hidden) { if (slotDef.StripHidden) - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, target, PopupType.Large); + StripPopup("strippable-component-alert-owner-hidden", stealth, target, slot: slot); else - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target, PopupType.Large); + StripPopup("strippable-component-alert-owner", stealth, target, user: Identity.Entity(user, EntityManager), item: item); } - var prefix = stealth ? "stealthily " : ""; + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, true, slot), user, target, item) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -374,7 +379,7 @@ private void StripRemoveInventory( EntityUid target, EntityUid item, string slot, - bool stealth) + bool hidden) { if (!CanStripRemoveInventory(user, target, item, slot)) return; @@ -384,7 +389,7 @@ private void StripRemoveInventory( RaiseLocalEvent(item, new DroppedEvent(user), true); // Gas tank internals etc. - _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth); + _handsSystem.PickupOrDrop(user, item, animateUser: hidden, animate: hidden); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot"); } @@ -446,12 +451,14 @@ private void StartStripInsertHand( var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay); - var prefix = stealth ? "stealthily " : ""; + bool hidden = stealth == ThievingStealth.Hidden; + + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, false, handName), user, target, held) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -471,7 +478,7 @@ private void StripInsertHand( Entity target, EntityUid held, string handName, - bool stealth) + bool hidden) { if (!Resolve(user, ref user.Comp) || !Resolve(target, ref target.Comp)) @@ -481,7 +488,7 @@ private void StripInsertHand( return; _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: user.Comp); - _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: stealth, animate: stealth, handsComp: target.Comp); + _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: hidden, animate: hidden, handsComp: target.Comp); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); // Hand update will trigger strippable update. @@ -543,15 +550,17 @@ private void StartStripRemoveHand( var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay); - if (!stealth) - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target); + bool hidden = stealth == ThievingStealth.Hidden; + + if (!hidden) + StripPopup("strippable-component-alert-owner", stealth, target, user: Identity.Entity(user, EntityManager), item: item); - var prefix = stealth ? "stealthily " : ""; + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, false, handName), user, target, item) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -572,7 +581,7 @@ private void StripRemoveHand( Entity target, EntityUid item, string handName, - bool stealth) + bool hidden) { if (!Resolve(user, ref user.Comp) || !Resolve(target, ref target.Comp)) @@ -582,7 +591,7 @@ private void StripRemoveHand( return; _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: target.Comp); - _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth, handsComp: user.Comp); + _handsSystem.PickupOrDrop(user, item, animateUser: hidden, animate: hidden, handsComp: user.Comp); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); // Hand update will trigger strippable update. diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs index 22a1d1a8f52..4abe7bc876a 100644 --- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -27,6 +27,7 @@ public sealed class ToggleableClothingSystem : EntitySystem [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedStrippableSystem _strippable = default!; + [Dependency] private readonly ThievingSystem _thieving = default!; public override void Initialize() { @@ -97,6 +98,8 @@ private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, Togg var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, component.StripDelay.Value); + bool hidden = (stealth == ThievingStealth.Hidden); + var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), item, wearer, item) { BreakOnDamage = true, @@ -110,11 +113,8 @@ private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, Togg if (!_doAfter.TryStartDoAfter(args)) return; - if (!stealth) - { - var popup = Loc.GetString("strippable-component-alert-owner-interact", ("user", Identity.Entity(user, EntityManager)), ("item", item)); - _popupSystem.PopupEntity(popup, wearer, wearer, PopupType.Large); - } + if (!hidden) + _strippable.StripPopup("strippable-component-alert-owner-interact", stealth, wearer, user: Identity.Entity(user, EntityManager), item: item); } private void OnGetAttachedStripVerbsEvent(EntityUid uid, AttachedClothingComponent component, GetVerbsEvent args) diff --git a/Content.Shared/Strip/Components/StrippableComponent.cs b/Content.Shared/Strip/Components/StrippableComponent.cs index 4faca4d8f21..00725808297 100644 --- a/Content.Shared/Strip/Components/StrippableComponent.cs +++ b/Content.Shared/Strip/Components/StrippableComponent.cs @@ -32,12 +32,12 @@ public sealed class StrippingSlotButtonPressed(string slot, bool isHand) : Bound public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage; [ByRefEvent] - public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = false) : EntityEventArgs, IInventoryRelayEvent + public abstract class BaseBeforeStripEvent(TimeSpan initialTime, ThievingStealth stealth = ThievingStealth.Obvious) : EntityEventArgs, IInventoryRelayEvent { public readonly TimeSpan InitialTime = initialTime; public float Multiplier = 1f; public TimeSpan Additive = TimeSpan.Zero; - public bool Stealth = stealth; + public ThievingStealth Stealth = stealth; public TimeSpan Time => TimeSpan.FromSeconds(MathF.Max(InitialTime.Seconds * Multiplier + Additive.Seconds, 0f)); @@ -51,7 +51,7 @@ public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. /// [ByRefEvent] - public sealed class BeforeStripEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth); + public sealed class BeforeStripEvent(TimeSpan initialTime, ThievingStealth stealth = ThievingStealth.Obvious) : BaseBeforeStripEvent(initialTime, stealth); /// /// Used to modify strip times. Raised directed at the target. @@ -60,7 +60,7 @@ public sealed class BeforeStripEvent(TimeSpan initialTime, bool stealth = false) /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. /// [ByRefEvent] - public sealed class BeforeGettingStrippedEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth); + public sealed class BeforeGettingStrippedEvent(TimeSpan initialTime, ThievingStealth stealth = ThievingStealth.Obvious) : BaseBeforeStripEvent(initialTime, stealth); /// /// Organizes the behavior of DoAfters for . diff --git a/Content.Shared/Strip/Components/ThievingComponent.cs b/Content.Shared/Strip/Components/ThievingComponent.cs index a851dd5ef63..1d584627727 100644 --- a/Content.Shared/Strip/Components/ThievingComponent.cs +++ b/Content.Shared/Strip/Components/ThievingComponent.cs @@ -9,14 +9,24 @@ public sealed partial class ThievingComponent : Component /// /// How much the strip time should be shortened by /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("stripTimeReduction")] + [DataField] public TimeSpan StripTimeReduction = TimeSpan.FromSeconds(0.5f); + /// + /// A multiplier coefficient for strip time + /// + [DataField] + public float StripTimeMultiplier = 1f; + /// /// Should it notify the user if they're stripping a pocket? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("stealthy")] - public bool Stealthy; + [DataField] + public ThievingStealth Stealth = ThievingStealth.Hidden; + + /// + /// Should the user be able to see hidden items? (i.e pockets) + /// + [DataField] + public bool IgnoreStripHidden; } diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs index 7afd4f245a1..64dd6a81f3a 100644 --- a/Content.Shared/Strip/SharedStrippableSystem.cs +++ b/Content.Shared/Strip/SharedStrippableSystem.cs @@ -1,11 +1,14 @@ using Content.Shared.DragDrop; using Content.Shared.Hands.Components; +using Content.Shared.Popups; using Content.Shared.Strip.Components; namespace Content.Shared.Strip; public abstract class SharedStrippableSystem : EntitySystem { + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ThievingSystem _thieving = default!; public override void Initialize() { base.Initialize(); @@ -14,7 +17,7 @@ public override void Initialize() SubscribeLocalEvent(OnDragDrop); } - public (TimeSpan Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, TimeSpan initialTime) + public (TimeSpan Time, ThievingStealth Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, TimeSpan initialTime) { var userEv = new BeforeStripEvent(initialTime); RaiseLocalEvent(user, ref userEv); @@ -55,4 +58,17 @@ private void OnCanDrop(EntityUid uid, StrippableComponent component, ref CanDrop if (args.CanDrop) args.Handled = true; } + + public void StripPopup(string messageId, ThievingStealth stealth, EntityUid target, EntityUid? user = null, EntityUid? item = null, string slot = "") + { + bool subtle = stealth == ThievingStealth.Subtle; + PopupType? popupSize = _thieving.GetPopupTypeFromStealth(stealth); + + if (popupSize.HasValue) // We should always have a value if we're not hidden + _popup.PopupEntity(Loc.GetString(messageId, + ("user", subtle ? Loc.GetString("thieving-component-user") : user ?? EntityUid.Invalid), + ("item", subtle ? Loc.GetString("thieving-component-item") : item ?? EntityUid.Invalid), + ("slot", slot)), + target, target, popupSize.Value); + } } diff --git a/Content.Shared/Strip/ThievingSystem.cs b/Content.Shared/Strip/ThievingSystem.cs index 2b3d3b38a00..8f523accfea 100644 --- a/Content.Shared/Strip/ThievingSystem.cs +++ b/Content.Shared/Strip/ThievingSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Inventory; -using Content.Shared.Strip; +using Content.Shared.Popups; using Content.Shared.Strip.Components; +using Robust.Shared.Serialization; namespace Content.Shared.Strip; @@ -17,7 +18,41 @@ public override void Initialize() private void OnBeforeStrip(EntityUid uid, ThievingComponent component, BeforeStripEvent args) { - args.Stealth |= component.Stealthy; + args.Stealth = (ThievingStealth) Math.Max((sbyte) args.Stealth, (sbyte) component.Stealth); args.Additive -= component.StripTimeReduction; + args.Multiplier *= component.StripTimeMultiplier; } + + public PopupType? GetPopupTypeFromStealth(ThievingStealth stealth) + { + switch (stealth) + { + case ThievingStealth.Hidden: + return null; + + case ThievingStealth.Subtle: + return PopupType.Small; + + default: + return PopupType.Large; + } + } +} +[Serializable, NetSerializable] +public enum ThievingStealth : sbyte +{ + /// + /// Target sees a large popup indicating that an item is being stolen by who + /// + Obvious = 0, + + /// + /// Target sees a small popup indicating that an item is being stolen + /// + Subtle = 1, + + /// + /// Target does not see any popup regarding the stealing of an item + /// + Hidden = 2 } diff --git a/Resources/Locale/en-US/strip/strippable-component.ftl b/Resources/Locale/en-US/strip/strippable-component.ftl index 7654b20b03f..65d7844ee22 100644 --- a/Resources/Locale/en-US/strip/strippable-component.ftl +++ b/Resources/Locale/en-US/strip/strippable-component.ftl @@ -19,4 +19,8 @@ strip-verb-get-data-text = Strip ## UI strippable-bound-user-interface-stripping-menu-title = {$ownerName}'s inventory -strippable-bound-user-interface-stripping-menu-ensnare-button = Remove Leg Restraints \ No newline at end of file +strippable-bound-user-interface-stripping-menu-ensnare-button = Remove Leg Restraints + +# Stealth +thieving-component-user = Someone +thieving-component-item = something \ No newline at end of file diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index e9163bdb548..80680ac0db2 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -31,3 +31,8 @@ trait-description-SocialAnxiety = You are anxious when you speak and stutter. trait-name-Snoring = Snoring trait-description-Snoring = You will snore while sleeping. + +trait-name-Thieving = Thieving +trait-description-Thieving = + You are deft with your hands, and talented at convincing people of their belongings. + You can identify pocketed items, steal them quieter, and steal ~33% faster. diff --git a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml index bf08db78f71..4cd0c04e2be 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml @@ -231,7 +231,6 @@ - type: FingerprintMask - type: Thieving stripTimeReduction: 1 - stealthy: true - type: NinjaGloves - type: entity @@ -332,7 +331,6 @@ tags: [] # ignore "WhitelistChameleon" tag - type: Thieving stripTimeReduction: 1.5 - stealthy: true - type: entity parent: ClothingHandsGlovesColorWhite diff --git a/Resources/Prototypes/Entities/Clothing/Hands/specific.yml b/Resources/Prototypes/Entities/Clothing/Hands/specific.yml index e6a57319999..db34297b42a 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/specific.yml @@ -29,4 +29,3 @@ components: - type: Thieving stripTimeReduction: 2 - stealthy: true diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index 80e87d3670c..9bdfb18830e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -85,8 +85,8 @@ range: 500 - type: StationLimitedNetwork - type: Thieving - stripTimeReduction: 9999 - stealthy: true + stripTimeMultiplier: 0 + ignoreStripHidden: true - type: Stripping - type: SolutionScanner - type: IgnoreUIRange diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml index 5bc02461eed..d23607b16d5 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml @@ -74,6 +74,11 @@ - GalacticCommon - SolCommon - Nekomimetic + - type: Thieving + ignoreStripHidden: true + stealth: Subtle + stripTimeReduction: 0 + stripTimeMultiplier: 0.667 - type: entity save: false diff --git a/Resources/Prototypes/Traits/skills.yml b/Resources/Prototypes/Traits/skills.yml new file mode 100644 index 00000000000..6175834c1fc --- /dev/null +++ b/Resources/Prototypes/Traits/skills.yml @@ -0,0 +1,14 @@ +- type: trait + id: Thieving + category: Physical + points: -4 + components: + - type: Thieving + ignoreStripHidden: true + stealth: Subtle + stripTimeReduction: 0 + stripTimeMultiplier: 0.667 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: Felinid