]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Retractable items system + Arm Blade action (#38150)
authorScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Sat, 14 Jun 2025 18:29:06 +0000 (20:29 +0200)
committerGitHub <noreply@github.com>
Sat, 14 Jun 2025 18:29:06 +0000 (21:29 +0300)
13 files changed:
Content.Server/Glue/GlueSystem.cs
Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs
Content.Shared/Interaction/SharedInteractionSystem.cs
Content.Shared/RetractableItemAction/ActionRetractableItemComponent.cs [new file with mode: 0644]
Content.Shared/RetractableItemAction/ItemActionEvents.cs [new file with mode: 0644]
Content.Shared/RetractableItemAction/RetractableItemActionComponent.cs [new file with mode: 0644]
Content.Shared/RetractableItemAction/RetractableItemActionSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/retractable-item-action/retractable-item-action.ftl [new file with mode: 0644]
Resources/Prototypes/Actions/changeling.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml
Resources/Textures/Interface/Actions/changeling.rsi/armblade.png [new file with mode: 0644]
Resources/Textures/Interface/Actions/changeling.rsi/meta.json [new file with mode: 0644]

index 6a91de4761f1da858a93db8733640594643e739c..d8f8e687d27fa44e5e42bdde2945fb6ed177178b 100644 (file)
@@ -71,7 +71,9 @@ public sealed class GlueSystem : SharedGlueSystem
     private bool TryGlue(Entity<GlueComponent> entity, EntityUid target, EntityUid actor)
     {
         // if item is glued then don't apply glue again so it can be removed for reasonable time
-        if (HasComp<GluedComponent>(target) || !HasComp<ItemComponent>(target))
+        // If glue is applied to an unremoveable item, the component will disappear after the duration.
+        // This effecitvely means any unremoveable item could be removed with a bottle of glue.
+        if (HasComp<GluedComponent>(target) || !HasComp<ItemComponent>(target) || HasComp<UnremoveableComponent>(target))
         {
             _popup.PopupEntity(Loc.GetString("glue-failure", ("target", target)), actor, actor, PopupType.Medium);
             return false;
index 479690847c3645b58063ff23eed19f399fcc61f7..3b5a880d4635030b2d88ab6d3e3c976c0953a22b 100644 (file)
@@ -72,7 +72,7 @@ namespace Content.Shared.Containers.ItemSlots
                     continue;
 
                 var item = Spawn(slot.StartingItem, Transform(uid).Coordinates);
-                    
+
                 if (slot.ContainerSlot != null)
                     _containers.Insert(item, slot.ContainerSlot);
             }
index 5c95983631446e5a477ab36e5d197bb2433d8189..5addd7c029aa308d71ae2ea5b45260a91aabbfef 100644 (file)
@@ -124,6 +124,27 @@ public abstract partial class SharedHandsSystem : EntitySystem
         return true;
     }
 
+    /// <summary>
+    /// Tries to pick up an entity into a hand, forcing to drop an item if its not free.
+    /// By default it does check if it's possible to drop items.
+    /// </summary>
+    public bool TryForcePickup(
+        EntityUid uid,
+        EntityUid entity,
+        Hand hand,
+        bool checkActionBlocker = true,
+        bool animate = true,
+        HandsComponent? handsComp = null,
+        ItemComponent? item = null)
+    {
+        if (!Resolve(uid, ref handsComp, false))
+            return false;
+
+        TryDrop(uid, hand, checkActionBlocker: checkActionBlocker, handsComp: handsComp);
+
+        return TryPickup(uid, entity, hand, checkActionBlocker, animate, handsComp, item);
+    }
+
     /// <summary>
     ///     Tries to pick up an entity into any hand, forcing to drop an item if there are no free hands
     ///     By default it does check if it's possible to drop items
index 5f063a9070b20f868775729ce8d9bc7367311e7b..91b3ffea0dc8c5081565383e1da0f8af552fbc33 100644 (file)
@@ -215,7 +215,10 @@ namespace Content.Shared.Interaction
         /// </summary>
         private void OnRemoveAttempt(EntityUid uid, UnremoveableComponent item, ContainerGettingRemovedAttemptEvent args)
         {
-            args.Cancel();
+            // don't prevent the server state for the container from being applied to the client correctly
+            // otherwise this will cause an error if the client predicts adding UnremoveableComponent
+            if (!_gameTiming.ApplyingState)
+                args.Cancel();
         }
 
         /// <summary>
diff --git a/Content.Shared/RetractableItemAction/ActionRetractableItemComponent.cs b/Content.Shared/RetractableItemAction/ActionRetractableItemComponent.cs
new file mode 100644 (file)
index 0000000..58258a8
--- /dev/null
@@ -0,0 +1,18 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.RetractableItemAction;
+
+/// <summary>
+/// Component used as a marker for items summoned by the RetractableItemAction system.
+/// Used for keeping track of items summoned by said action.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(RetractableItemActionSystem))]
+public sealed partial class ActionRetractableItemComponent : Component
+{
+    /// <summary>
+    /// The action that marked this item.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityUid? SummoningAction;
+}
diff --git a/Content.Shared/RetractableItemAction/ItemActionEvents.cs b/Content.Shared/RetractableItemAction/ItemActionEvents.cs
new file mode 100644 (file)
index 0000000..8d4dd0d
--- /dev/null
@@ -0,0 +1,9 @@
+using Content.Shared.Actions;
+
+namespace Content.Shared.RetractableItemAction;
+
+/// <summary>
+/// Raised when using the RetractableItem action.
+/// </summary>
+[ByRefEvent]
+public sealed partial class OnRetractableItemActionEvent : InstantActionEvent;
diff --git a/Content.Shared/RetractableItemAction/RetractableItemActionComponent.cs b/Content.Shared/RetractableItemAction/RetractableItemActionComponent.cs
new file mode 100644 (file)
index 0000000..9f48a05
--- /dev/null
@@ -0,0 +1,41 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.RetractableItemAction;
+
+/// <summary>
+/// Used for storing an unremovable item within an action and summoning it into your hand on use.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(RetractableItemActionSystem))]
+public sealed partial class RetractableItemActionComponent : Component
+{
+    /// <summary>
+    /// The item that will appear be spawned by the action.
+    /// </summary>
+    [DataField(required: true)]
+    public EntProtoId SpawnedPrototype;
+
+    /// <summary>
+    /// Sound collection to play when the item is summoned.
+    /// </summary>
+    [DataField]
+    public SoundCollectionSpecifier? SummonSounds;
+
+    /// <summary>
+    /// Sound collection to play when the summoned item is retracted back into the action.
+    /// </summary>
+    [DataField]
+    public SoundCollectionSpecifier? RetractSounds;
+
+    /// <summary>
+    /// The item managed by the action. Will be summoned and hidden as the action is used.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityUid? ActionItemUid;
+
+    /// <summary>
+    /// The container ID used to store the item.
+    /// </summary>
+    public const string ContainerId = "item-action-item-container";
+}
diff --git a/Content.Shared/RetractableItemAction/RetractableItemActionSystem.cs b/Content.Shared/RetractableItemAction/RetractableItemActionSystem.cs
new file mode 100644 (file)
index 0000000..b99b653
--- /dev/null
@@ -0,0 +1,105 @@
+using Content.Shared.Actions;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Interaction.Components;
+using Content.Shared.Popups;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+
+namespace Content.Shared.RetractableItemAction;
+
+/// <summary>
+/// System for handling retractable items, such as armblades.
+/// </summary>
+public sealed class RetractableItemActionSystem : EntitySystem
+{
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
+    [Dependency] private readonly SharedContainerSystem _containers = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedActionsSystem _actions = default!;
+    [Dependency] private readonly SharedPopupSystem _popups = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RetractableItemActionComponent, MapInitEvent>(OnActionInit);
+        SubscribeLocalEvent<RetractableItemActionComponent, OnRetractableItemActionEvent>(OnRetractableItemAction);
+
+        SubscribeLocalEvent<ActionRetractableItemComponent, ComponentShutdown>(OnActionSummonedShutdown);
+    }
+
+    private void OnActionInit(Entity<RetractableItemActionComponent> ent, ref MapInitEvent args)
+    {
+        _containers.EnsureContainer<Container>(ent, RetractableItemActionComponent.ContainerId);
+
+        PopulateActionItem(ent.Owner);
+    }
+
+    private void OnRetractableItemAction(Entity<RetractableItemActionComponent> ent, ref OnRetractableItemActionEvent args)
+    {
+        if (_hands.GetActiveHand(args.Performer) is not { } userHand)
+            return;
+
+        if (_actions.GetAction(ent.Owner) is not { } action)
+            return;
+
+        if (action.Comp.AttachedEntity == null)
+            return;
+
+        if (ent.Comp.ActionItemUid == null)
+            return;
+
+        // Don't allow to summon an item if holding an unremoveable item unless that item is summoned by the action.
+        if (userHand.HeldEntity != null && !_hands.IsHolding(args.Performer, ent.Comp.ActionItemUid) && !_hands.CanDropHeld(args.Performer, userHand, false))
+        {
+            _popups.PopupClient(Loc.GetString("retractable-item-hand-cannot-drop"), args.Performer, args.Performer);
+            return;
+        }
+
+        if (_hands.IsHolding(args.Performer, ent.Comp.ActionItemUid))
+        {
+            RemComp<UnremoveableComponent>(ent.Comp.ActionItemUid.Value);
+            var container = _containers.GetContainer(ent, RetractableItemActionComponent.ContainerId);
+            _containers.Insert(ent.Comp.ActionItemUid.Value, container);
+            _audio.PlayPredicted(ent.Comp.RetractSounds, action.Comp.AttachedEntity.Value, action.Comp.AttachedEntity.Value);
+        }
+        else
+        {
+            _hands.TryForcePickup(args.Performer, ent.Comp.ActionItemUid.Value, userHand, checkActionBlocker: false);
+            _audio.PlayPredicted(ent.Comp.SummonSounds, action.Comp.AttachedEntity.Value, action.Comp.AttachedEntity.Value);
+            EnsureComp<UnremoveableComponent>(ent.Comp.ActionItemUid.Value);
+        }
+
+        args.Handled = true;
+    }
+
+    private void OnActionSummonedShutdown(Entity<ActionRetractableItemComponent> ent, ref ComponentShutdown args)
+    {
+        if (_actions.GetAction(ent.Comp.SummoningAction) is not { } action)
+            return;
+
+        if (!TryComp<RetractableItemActionComponent>(action, out var retract) || retract.ActionItemUid != ent.Owner)
+            return;
+
+        // If the item is somehow destroyed, re-add it to the action.
+        PopulateActionItem(action.Owner);
+    }
+
+    private void PopulateActionItem(Entity<RetractableItemActionComponent?> ent)
+    {
+        if (!Resolve(ent.Owner, ref ent.Comp, false) || TerminatingOrDeleted(ent))
+            return;
+
+        if (!PredictedTrySpawnInContainer(ent.Comp.SpawnedPrototype, ent.Owner, RetractableItemActionComponent.ContainerId, out var summoned))
+            return;
+
+        ent.Comp.ActionItemUid = summoned.Value;
+
+        // Mark the unremovable item so it can be added back into the action.
+        var summonedComp = AddComp<ActionRetractableItemComponent>(summoned.Value);
+        summonedComp.SummoningAction = ent.Owner;
+        Dirty(summoned.Value, summonedComp);
+
+        Dirty(ent);
+    }
+}
diff --git a/Resources/Locale/en-US/retractable-item-action/retractable-item-action.ftl b/Resources/Locale/en-US/retractable-item-action/retractable-item-action.ftl
new file mode 100644 (file)
index 0000000..e3dca38
--- /dev/null
@@ -0,0 +1 @@
+retractable-item-hand-cannot-drop = Your hand is already occupied.
diff --git a/Resources/Prototypes/Actions/changeling.yml b/Resources/Prototypes/Actions/changeling.yml
new file mode 100644 (file)
index 0000000..273bb8e
--- /dev/null
@@ -0,0 +1,22 @@
+- type: entity
+  parent: BaseAction
+  id: ActionRetractableItemArmBlade
+  name: Arm Blade
+  description: Shed your flesh and reform it into a fleshy blade.
+  components:
+  - type: Action
+    useDelay: 2
+    raiseOnAction: true
+    itemIconStyle: BigAction
+    icon:
+      sprite: Interface/Actions/changeling.rsi
+      state: armblade
+  - type: InstantAction
+    event: !type:OnRetractableItemActionEvent
+  - type: RetractableItemAction
+    spawnedPrototype: ArmBlade
+    summonSounds:
+      collection: gib # Placeholder
+    retractSounds:
+      collection: gib # Placeholder
+
index 398c04aee6e6ae35e42c8e7c467a6de1733c64a5..457e8ea10d51f589b3011e47dfb80968a40e71bb 100644 (file)
     state: icon
   - type: MeleeWeapon
     wideAnimationRotation: 90
-    attackRate: 0.75
+    attackRate: 1
     damage:
       types:
-        Slash: 25
-        Piercing: 15
+        Slash: 10
+        Piercing: 10
   - type: Item
     size: Normal
     sprite: Objects/Weapons/Melee/armblade.rsi
   - type: Prying
+    pryPowered: true
diff --git a/Resources/Textures/Interface/Actions/changeling.rsi/armblade.png b/Resources/Textures/Interface/Actions/changeling.rsi/armblade.png
new file mode 100644 (file)
index 0000000..f52a62a
Binary files /dev/null and b/Resources/Textures/Interface/Actions/changeling.rsi/armblade.png differ
diff --git a/Resources/Textures/Interface/Actions/changeling.rsi/meta.json b/Resources/Textures/Interface/Actions/changeling.rsi/meta.json
new file mode 100644 (file)
index 0000000..babe776
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "version": 1,
+    "license": "CC0-1.0",
+    "copyright": "Created by TiniestShark (github)",
+    "size": {
+        "x": 32,
+        "y": 32
+    },
+    "states": [
+        {
+            "name": "armblade"
+        }
+    ]
+}