From 927ad18d8189cbfc284f7a34ceeb363911959624 Mon Sep 17 00:00:00 2001 From: Rachel <60523642+11BelowStudio@users.noreply.github.com> Date: Sun, 27 Sep 2026 19:04:52 +0000 Subject: [PATCH] Zombies now have hands (less metagameable in the strip menu) - but they're too clumsy to hold anything. (#45721) * Solving #43723 by making zombies clumsy (they are unable to hold anything in their hands, passing items to them will fail). Also made zombies uncuff themselves whenever they transform (seeing as they no longer lose their hands meaning that cuffs would otherwise remain on them) * Cleaning up the code * Renamed ClumsyHoldStatusEffectComponent.cs to ClumsyGrabStatusEffectComponent.cs (and renamed associated files). I think "ClumsyGrab" works better than "ClumsyHold" (as the effect happens when the item is first grabbed, not whilst the item is being held). everything still works Reordered appropriate functions in ClumsyStatusTest.cs and ClumsyStatusEffectSystem.cs to retain alphabetical order. * oopsies :p * Appeased the YAMLLinter (at the cost of a somewhat hackier implementation). ZombieSystem.Transform.cs now applies the zombie clumsiness via creating a new PermanentStatusEffectComponent with StatusEffectClumsyZombie as its status effect. * requested changes have been made (also re-added newline to end of clumsy-component.ftl because it has a newline at the end of it on the master branch, I guess it's probably supposed to be there) * reordering so private members (including new one) are before the internal member * hands are forcibly emptied - even if something's glued to them. * UnZombify should remove the lingering ZombieClumsy status effect (still needs to revert the rest of the zombification process, but at least the new bit I added can be undone) * Minor refactor of my own refactor. New order of operations: 1. Uncuffs zombie (if needed) 2. Attempts to `DropAll` items from hands 3. Iterates through hand containers to empty them (removes otherwise unremovable items from hands) Will refactor process to force drop all items if #45844 becomes implementable + is merged. * formatting * how the hell did the metabolizer code go missing!? * uncuffing now happens in a safer way less likely to accidentally infinitely loop * tweaking that one comment I was asked to tweak (this is unrelated to the hand malarkey) hopefully it won't lead to any merge conflicts later on * formatting * file style --------- Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com> --- .../Tests/Clumsy/ClumsyStatusTest.cs | 32 +++++++++++++++ .../Tests/Clumsy/ClumsyTestPrototypes.cs | 14 +++++++ .../Zombies/ZombieSystem.Transform.cs | 29 +++++++++++--- Content.Server/Zombies/ZombieSystem.cs | 6 ++- .../Clumsy/ClumsyStatusEffectSystem.cs | 19 +++++++++ .../ClumsyGrabStatusEffectComponent.cs | 39 +++++++++++++++++++ .../StatusEffectSystem.Relay.cs | 2 + .../clown/components/clumsy-component.ftl | 3 ++ .../Entities/StatusEffects/clumsy.yml | 8 ++++ 9 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 Content.Shared/Clumsy/Components/ClumsyGrabStatusEffectComponent.cs diff --git a/Content.IntegrationTests/Tests/Clumsy/ClumsyStatusTest.cs b/Content.IntegrationTests/Tests/Clumsy/ClumsyStatusTest.cs index cf42a3b241..b936c7cde0 100644 --- a/Content.IntegrationTests/Tests/Clumsy/ClumsyStatusTest.cs +++ b/Content.IntegrationTests/Tests/Clumsy/ClumsyStatusTest.cs @@ -8,6 +8,7 @@ using Content.Shared.Climbing.Components; using Content.Shared.Climbing.Events; using Content.Shared.Climbing.Systems; using Content.Shared.Clumsy; +using Content.Shared.Hands; using Content.Shared.Medical; using Content.Shared.Mobs.Components; using Content.Shared.StatusEffectNew; @@ -28,6 +29,7 @@ public sealed class ClumsyStatusTest : InteractionTest private sealed class DefibListenerSystem : TestListenerSystem; private sealed class GunListenerSystem : TestListenerSystem; private sealed class InjectListenerSystem : TestListenerSystem; + private sealed class BeforeEquipListenerSystem : TestListenerSystem; [SidedDependency(Side.Server)] private readonly ClimbSystem _sClimbSystem = default!; [SidedDependency(Side.Server)] private readonly StatusEffectsSystem _sStatusSystem = default!; @@ -164,4 +166,34 @@ public sealed class ClumsyStatusTest : InteractionTest Assert.That(ev.Cancelled, Is.True, "Clumsy mob didn't cancel climb event."); } } + + [Test, Description("Test that a mob with the ClumsyGrab status will be unable to hold items in hands.")] + public async Task TestClumsyGrab() + { + await Server.WaitPost(() => + { + SEntMan.EnsureComponent(SPlayer); + _sStatusSystem.TrySetStatusEffectDuration(SPlayer, ClumsyGrabProto); + }); + + Assume.That(_sStatusSystem.HasStatusEffect(SPlayer, ClumsyGrabProto), Is.True); + Assume.That(HandSys.ActiveHandIsEmpty(SPlayer), Is.True, "Clumsy mob was already holding something!"); + var freeHands = HandSys.CountFreeHands(SPlayer); //"before" free hands + + // can't use PlaceInHands, that method will fail an assertion under this status effect. + await Server.WaitPost(() => + { + var location = SEntMan.EnsureComponent(SPlayer).Coordinates; + var item = SSpawnAtPosition(ItemProto, location); + HandSys.TryPickup(SPlayer, item, handsComp: Hands); + }); + + Assert.That(HandSys.ActiveHandIsEmpty(SPlayer), Is.True, "The clumsy mob has something in their active hand (they picked up the item!?)"); + Assert.That(HandSys.CountFreeHands(SPlayer), Is.EqualTo(freeHands), "A hand became occupied ('before' and 'after' free hand count has changed)"); + + foreach (var ev in GetEvents(SPlayer)) + { + Assert.That(ev.Cancelled, Is.True, "Clumsy mob didn't cancel BeforeEquippingHandEvent."); + } + } } diff --git a/Content.IntegrationTests/Tests/Clumsy/ClumsyTestPrototypes.cs b/Content.IntegrationTests/Tests/Clumsy/ClumsyTestPrototypes.cs index 8013393c58..76eae4ff6b 100644 --- a/Content.IntegrationTests/Tests/Clumsy/ClumsyTestPrototypes.cs +++ b/Content.IntegrationTests/Tests/Clumsy/ClumsyTestPrototypes.cs @@ -9,9 +9,11 @@ public static class ClumsyTestPrototypes public static readonly EntProtoId SyringeProto = "Syringe"; public const string ClumsyStatusAll100 = "ClumsyStatusAll100"; + public const string ClumsyGrabProto = "ClumsyGrabProto"; public const string BallProto = "BallProto"; public const string DefibProto = "DefibProto"; public const string TableProto = "TableProto"; + public const string ItemProto = "ItemProto"; [TestPrototypes] public static readonly string ClumsyPrototypes = @$" @@ -30,6 +32,13 @@ public static class ClumsyTestPrototypes - type: ClumsyVaultStatusEffect clumsyChance: 1 +- type: entity + id: {ClumsyGrabProto} + components: + - type: StatusEffect + - type: ClumsyGrabStatusEffect + clumsyChance: 1 + - type: entity id: {BallProto} components: @@ -55,5 +64,10 @@ public static class ClumsyTestPrototypes components: - type: Climbable - type: Bonkable + +- type: entity + id: {ItemProto} + components: + - type: Item "; } diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index 7b126f57e8..08833cc35c 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -15,8 +15,8 @@ using Content.Shared.Body; using Content.Shared.Body.Components; using Content.Shared.CombatMode; using Content.Shared.CombatMode.Pacification; +using Content.Shared.Cuffs; using Content.Shared.Ghost.Roles.Components; -using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; @@ -35,12 +35,15 @@ using Content.Shared.Popups; using Content.Shared.Prying.Components; using Content.Shared.Roles; using Content.Shared.Speech.EntitySystems; +using Content.Shared.StatusEffectNew; +using Content.Shared.StatusEffectNew.Components; using Content.Shared.Tag; using Content.Shared.Temperature.Components; using Content.Shared.Traits.Assorted; using Content.Shared.Weapons.Melee; using Content.Shared.Zombies; using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -73,12 +76,16 @@ public sealed partial class ZombieSystem [Dependency] private TagSystem _tag = default!; [Dependency] private ISharedPlayerManager _player = default!; [Dependency] private BodySystem _body = default!; + [Dependency] private SharedContainerSystem _containerSystem = default!; + [Dependency] private StatusEffectsSystem _statusEffects = default!; + [Dependency] private SharedCuffableSystem _cuffable = default!; private static readonly ProtoId InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell"; private static readonly ProtoId CannotSuicideTag = "CannotSuicide"; private static readonly ProtoId ZombieFaction = "Zombie"; private static readonly string MindRoleZombie = "MindRoleZombie"; private static readonly List> BannableZombiePrototypes = ["Zombie"]; + private static readonly EntProtoId ClumsyZombieStatus = "StatusEffectClumsyZombie"; internal static readonly HashSet AdditionalZombieLayers = [HumanoidVisualLayers.Tail, HumanoidVisualLayers.HeadSide, HumanoidVisualLayers.HeadTop, HumanoidVisualLayers.Snout]; /// @@ -328,12 +335,24 @@ public sealed partial class ZombieSystem MakeGhostRole(target); } - if (TryComp(target, out var handsComp)) + // Uncuffing the zombie + foreach (var cuff in _cuffable.GetAllCuffs(target)) { - _hands.RemoveHands(target); - RemComp(target, handsComp); + _cuffable.Uncuff(target, null, cuff); } + // forcibly empties hands (even if they contain something sticky/unremovable) + _hands.DropAll(target); // TODO refactor to force drop all if #45844 gets merged + // temp backup to get rid of unremovable items + foreach (var hand in _hands.EnumerateHands(target)) + { + if (_containerSystem.TryGetContainer(target, hand, out var handContainer)) + _containerSystem.EmptyContainer(handContainer, true); + } + + // the zombie is now clumsy. it will drop anything handed to it. + _statusEffects.TrySetStatusEffectDuration(target, ClumsyZombieStatus); + // Sloth: What the fuck? // How long until compregistry lmao. RemComp(target); @@ -345,7 +364,7 @@ public sealed partial class ZombieSystem //zombie gamemode stuff var ev = new EntityZombifiedEvent(target); RaiseLocalEvent(target, ref ev, true); - //zombies get slowdown once they convert + //zombies revert to their default movement speed. _movementSpeedModifier.RefreshMovementSpeedModifiers(target); //Need to prevent them from getting an item, they have no hands. diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs index dde4c8b0c6..717a8ce370 100644 --- a/Content.Server/Zombies/ZombieSystem.cs +++ b/Content.Server/Zombies/ZombieSystem.cs @@ -291,8 +291,9 @@ namespace Content.Server.Zombies /// the entity you want to unzombify (different from source in case of cloning, for example) /// /// - /// this currently only restore the skin/eye color from before zombified + /// this currently only restores the skin/eye color from before zombified + removes clumsy zombie status effect /// TODO: completely rethink how zombies are done to allow reversal. + /// TODO: maybe move to ZombieSystem.Transform.cs? /// public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp) { @@ -308,6 +309,9 @@ namespace Content.Server.Zombies _bloodstream.ChangeBloodRefreshAmount(target, zombiecomp.BeforeZombifiedBloodRefresh); _bloodstream.ChangeBloodIncreaseEnabled(target, true); + // no longer a zombie, no longer *that* clumsy + _statusEffects.TryRemoveStatusEffect(target, ClumsyZombieStatus); + // Remove the tags that we added during Zombification _tag.RemoveTag(target, CannotSuicideTag); _tag.RemoveTag(target, InvalidForGlobalSpawnSpellTag); diff --git a/Content.Shared/Clumsy/ClumsyStatusEffectSystem.cs b/Content.Shared/Clumsy/ClumsyStatusEffectSystem.cs index a0adf17ecf..65e5fb922c 100644 --- a/Content.Shared/Clumsy/ClumsyStatusEffectSystem.cs +++ b/Content.Shared/Clumsy/ClumsyStatusEffectSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Climbing.Events; using Content.Shared.Climbing.Systems; using Content.Shared.Clumsy.Components; using Content.Shared.Damage.Systems; +using Content.Shared.Hands; using Content.Shared.IdentityManagement; using Content.Shared.Medical; using Content.Shared.Popups; @@ -87,6 +88,24 @@ public sealed partial class ClumsyStatusEffectSystem : EntitySystem _audio.PlayPredicted(status.Comp.ClumsySound, args.AppliedTo, args.AppliedTo); } + /// Especially clumsy people may sometimes fail to pick things up, and fail to hold on to things they are given. + [SubscribeLocalEvent] + private void OnBeforeEquippingHandEvent(Entity status, + ref StatusEffectRelayedEvent args) + { + if (args.Args.Cancelled + || !SharedRandomExtensions.PredictedProb(_timing, status.Comp.ClumsyChance, GetNetEntity(status), GetNetEntity(args.AppliedTo))) + return; + + var ev = args.Args; + ev.Cancelled = true; + args.Args = ev; + + var selfMessage = status.Comp.SelfFailedMessage == null ? null : Loc.GetString(status.Comp.SelfFailedMessage, ("item", args.Args.Item)); + var othersMessage = status.Comp.OtherFailedMessage == null ? null : Loc.GetString(status.Comp.OtherFailedMessage, ("item", args.Args.Item)); + _popup.PopupEntity(selfMessage, othersMessage, args.AppliedTo, args.AppliedTo); + } + /// Clumsy people can't be trusted with guns! [SubscribeLocalEvent] private void OnBeforeGunShotEvent(Entity status, ref StatusEffectRelayedEvent args) diff --git a/Content.Shared/Clumsy/Components/ClumsyGrabStatusEffectComponent.cs b/Content.Shared/Clumsy/Components/ClumsyGrabStatusEffectComponent.cs new file mode 100644 index 0000000000..631e9bd844 --- /dev/null +++ b/Content.Shared/Clumsy/Components/ClumsyGrabStatusEffectComponent.cs @@ -0,0 +1,39 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Clumsy.Components; + +/// +/// Afflicted entity will occasionally fail to pick up items or receive items into their hands. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(ClumsyStatusEffectSystem))] +public sealed partial class ClumsyGrabStatusEffectComponent : Component +{ + /// + /// How often they fail. + /// + [DataField] + public float ClumsyChance = 0.5f; + + /// + /// Popup played to the afflicted when they fail to grab the item. + /// + /// Parameters passed in: + /// + /// item - The item which got dropped. + /// + /// + [DataField] + public LocId? SelfFailedMessage = "clumsy-grab-fail-message-user"; + + /// + /// Popup played to others when the afflicted fails to grab the item. + /// + /// Parameters passed in: + /// + /// item - The item which got dropped. + /// + /// + [DataField] + public LocId? OtherFailedMessage = "clumsy-grab-fail-message-others"; +} diff --git a/Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs b/Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs index 06f47fed55..19d2fcd8ab 100644 --- a/Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs +++ b/Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs @@ -9,6 +9,7 @@ using Content.Shared.Damage.Systems; using Content.Shared.Examine; using Content.Shared.Eye.Blinding.Systems; using Content.Shared.Flash; +using Content.Shared.Hands; using Content.Shared.Medical; using Content.Shared.Mobs; using Content.Shared.Mobs.Events; @@ -71,6 +72,7 @@ public sealed partial class StatusEffectsSystem SubscribeLocalEvent(RefRelayStatusEffectEvent); SubscribeLocalEvent(RelayStatusEffectEvent); + SubscribeLocalEvent(RefRelayStatusEffectEvent); SubscribeLocalEvent(RefRelayStatusEffectEvent); SubscribeLocalEvent(RefRelayStatusEffectEvent); diff --git a/Resources/Locale/en-US/clown/components/clumsy-component.ftl b/Resources/Locale/en-US/clown/components/clumsy-component.ftl index 9e84cd1736..8079d5f4bf 100644 --- a/Resources/Locale/en-US/clown/components/clumsy-component.ftl +++ b/Resources/Locale/en-US/clown/components/clumsy-component.ftl @@ -9,3 +9,6 @@ clumsy-defibrillator-fail-message = Oops! You zapped yourself. clumsy-catch-fail-message-user = { CAPITALIZE(THE($item)) } hits your head! clumsy-catch-fail-message-others = { CAPITALIZE(THE($item)) } hits { THE($catcher) }'s head! + +clumsy-grab-fail-message-user = { CAPITALIZE(THE($item)) } slips out of your hands... +clumsy-grab-fail-message-others = { CAPITALIZE(THE($item)) } slips out of their hands... diff --git a/Resources/Prototypes/Entities/StatusEffects/clumsy.yml b/Resources/Prototypes/Entities/StatusEffects/clumsy.yml index 0e70eb37a9..fb8ee8e0b3 100644 --- a/Resources/Prototypes/Entities/StatusEffects/clumsy.yml +++ b/Resources/Prototypes/Entities/StatusEffects/clumsy.yml @@ -134,4 +134,12 @@ clumsyChance: *ChanceDrunk clumsySound: *SoundGlug +# gruhhhhh (Zombies don't know how to hold things in their hands.) +- type: entity + parent: MobStatusEffectDebuff + id: StatusEffectClumsyZombie + components: + - type: ClumsyGrabStatusEffect + clumsyChance: *Chance100 + #endregion