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;
continue;
var item = Spawn(slot.StartingItem, Transform(uid).Coordinates);
-
+
if (slot.ContainerSlot != null)
_containers.Insert(item, slot.ContainerSlot);
}
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
/// </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>
--- /dev/null
+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;
+}
--- /dev/null
+using Content.Shared.Actions;
+
+namespace Content.Shared.RetractableItemAction;
+
+/// <summary>
+/// Raised when using the RetractableItem action.
+/// </summary>
+[ByRefEvent]
+public sealed partial class OnRetractableItemActionEvent : InstantActionEvent;
--- /dev/null
+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";
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+retractable-item-hand-cannot-drop = Your hand is already occupied.
--- /dev/null
+- 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
+
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
--- /dev/null
+{
+ "version": 1,
+ "license": "CC0-1.0",
+ "copyright": "Created by TiniestShark (github)",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "armblade"
+ }
+ ]
+}