From 208b245dcbf2550d2a3d444b0b68df0c2d99c82e Mon Sep 17 00:00:00 2001 From: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Date: Sat, 14 Jun 2025 20:29:06 +0200 Subject: [PATCH] Retractable items system + Arm Blade action (#38150) --- Content.Server/Glue/GlueSystem.cs | 4 +- .../Containers/ItemSlot/ItemSlotsSystem.cs | 2 +- .../EntitySystems/SharedHandsSystem.Pickup.cs | 21 ++++ .../Interaction/SharedInteractionSystem.cs | 5 +- .../ActionRetractableItemComponent.cs | 18 +++ .../RetractableItemAction/ItemActionEvents.cs | 9 ++ .../RetractableItemActionComponent.cs | 41 +++++++ .../RetractableItemActionSystem.cs | 105 ++++++++++++++++++ .../retractable-item-action.ftl | 1 + Resources/Prototypes/Actions/changeling.yml | 22 ++++ .../Objects/Weapons/Melee/armblade.yml | 7 +- .../Actions/changeling.rsi/armblade.png | Bin 0 -> 943 bytes .../Actions/changeling.rsi/meta.json | 14 +++ 13 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 Content.Shared/RetractableItemAction/ActionRetractableItemComponent.cs create mode 100644 Content.Shared/RetractableItemAction/ItemActionEvents.cs create mode 100644 Content.Shared/RetractableItemAction/RetractableItemActionComponent.cs create mode 100644 Content.Shared/RetractableItemAction/RetractableItemActionSystem.cs create mode 100644 Resources/Locale/en-US/retractable-item-action/retractable-item-action.ftl create mode 100644 Resources/Prototypes/Actions/changeling.yml create mode 100644 Resources/Textures/Interface/Actions/changeling.rsi/armblade.png create mode 100644 Resources/Textures/Interface/Actions/changeling.rsi/meta.json diff --git a/Content.Server/Glue/GlueSystem.cs b/Content.Server/Glue/GlueSystem.cs index 6a91de4761..d8f8e687d2 100644 --- a/Content.Server/Glue/GlueSystem.cs +++ b/Content.Server/Glue/GlueSystem.cs @@ -71,7 +71,9 @@ public sealed class GlueSystem : SharedGlueSystem private bool TryGlue(Entity 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(target) || !HasComp(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(target) || !HasComp(target) || HasComp(target)) { _popup.PopupEntity(Loc.GetString("glue-failure", ("target", target)), actor, actor, PopupType.Medium); return false; diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs index 479690847c..3b5a880d46 100644 --- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs @@ -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); } diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs index 5c95983631..5addd7c029 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs @@ -124,6 +124,27 @@ public abstract partial class SharedHandsSystem : EntitySystem return true; } + /// + /// 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. + /// + 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); + } + /// /// 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 diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index 5f063a9070..91b3ffea0d 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -215,7 +215,10 @@ namespace Content.Shared.Interaction /// 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(); } /// diff --git a/Content.Shared/RetractableItemAction/ActionRetractableItemComponent.cs b/Content.Shared/RetractableItemAction/ActionRetractableItemComponent.cs new file mode 100644 index 0000000000..58258a8b79 --- /dev/null +++ b/Content.Shared/RetractableItemAction/ActionRetractableItemComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Utility; + +namespace Content.Shared.RetractableItemAction; + +/// +/// Component used as a marker for items summoned by the RetractableItemAction system. +/// Used for keeping track of items summoned by said action. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(RetractableItemActionSystem))] +public sealed partial class ActionRetractableItemComponent : Component +{ + /// + /// The action that marked this item. + /// + [DataField, AutoNetworkedField] + public EntityUid? SummoningAction; +} diff --git a/Content.Shared/RetractableItemAction/ItemActionEvents.cs b/Content.Shared/RetractableItemAction/ItemActionEvents.cs new file mode 100644 index 0000000000..8d4dd0da6e --- /dev/null +++ b/Content.Shared/RetractableItemAction/ItemActionEvents.cs @@ -0,0 +1,9 @@ +using Content.Shared.Actions; + +namespace Content.Shared.RetractableItemAction; + +/// +/// Raised when using the RetractableItem action. +/// +[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 index 0000000000..9f48a05e23 --- /dev/null +++ b/Content.Shared/RetractableItemAction/RetractableItemActionComponent.cs @@ -0,0 +1,41 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.RetractableItemAction; + +/// +/// Used for storing an unremovable item within an action and summoning it into your hand on use. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(RetractableItemActionSystem))] +public sealed partial class RetractableItemActionComponent : Component +{ + /// + /// The item that will appear be spawned by the action. + /// + [DataField(required: true)] + public EntProtoId SpawnedPrototype; + + /// + /// Sound collection to play when the item is summoned. + /// + [DataField] + public SoundCollectionSpecifier? SummonSounds; + + /// + /// Sound collection to play when the summoned item is retracted back into the action. + /// + [DataField] + public SoundCollectionSpecifier? RetractSounds; + + /// + /// The item managed by the action. Will be summoned and hidden as the action is used. + /// + [DataField, AutoNetworkedField] + public EntityUid? ActionItemUid; + + /// + /// The container ID used to store the item. + /// + 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 index 0000000000..b99b653cf0 --- /dev/null +++ b/Content.Shared/RetractableItemAction/RetractableItemActionSystem.cs @@ -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; + +/// +/// System for handling retractable items, such as armblades. +/// +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(OnActionInit); + SubscribeLocalEvent(OnRetractableItemAction); + + SubscribeLocalEvent(OnActionSummonedShutdown); + } + + private void OnActionInit(Entity ent, ref MapInitEvent args) + { + _containers.EnsureContainer(ent, RetractableItemActionComponent.ContainerId); + + PopulateActionItem(ent.Owner); + } + + private void OnRetractableItemAction(Entity 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(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(ent.Comp.ActionItemUid.Value); + } + + args.Handled = true; + } + + private void OnActionSummonedShutdown(Entity ent, ref ComponentShutdown args) + { + if (_actions.GetAction(ent.Comp.SummoningAction) is not { } action) + return; + + if (!TryComp(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 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(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 index 0000000000..e3dca38922 --- /dev/null +++ b/Resources/Locale/en-US/retractable-item-action/retractable-item-action.ftl @@ -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 index 0000000000..273bb8ed6b --- /dev/null +++ b/Resources/Prototypes/Actions/changeling.yml @@ -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 + diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml index 398c04aee6..457e8ea10d 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml @@ -12,12 +12,13 @@ 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 index 0000000000000000000000000000000000000000..f52a62a83fad11cffa6658e22ea0d048cd25e07d GIT binary patch literal 943 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7e6l0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#NP@+A+C&U#1IABGIwoi)rzFB7iKI5@g{`Zx3Ntk%<8vv$L?fPid${VP|l96EI9nuEjb+qXY_`0)Mv_pPC! zR}w>K+S>mA|Nqh|D;A(r7)yfuf*Bm1-ADs*lDyqrbave>Tn*%~mw5WRvcF_w7qhSq zG++J>DE-CL#WBRngN zaf3k95|xc?f~uagmU(KmPT2i(`lYBhPyCovf{v+38m5{2Ghmc;-n(b&Pv)8SC22f@ zIUiEXFP&3dYIxJABVCKIn8fbKA9pYw%_^`mjrV7E2_Hx*u(Nz`D}J&xPOtXD9WP5}1B?0lY}5_ybHyL4X1!^ZP+?a7@~Xh% zNv@&2!Y}WTJFgEauwVP$u_(f;Z|!%*C|>47o>da(@Bgli%(Irf*!6FclfUp2gL#Zo zRJT9oG!XRN#q_xNU-A8QhAsD0_P8W{zFn#FI{tWTXHR2Gtnffme^FJOw__5jdXs7ti1#;_+x6Az7$)4#nUu3WC zmz~u54a{^6%tMTf ztW3?UOf9ty46F