Retractable items system + Arm Blade action (#38150)

This commit is contained in:
ScarKy0
2025-06-14 20:29:06 +02:00
committed by GitHub
parent 635ea4b2b4
commit 208b245dcb
13 changed files with 243 additions and 6 deletions

View File

@@ -71,7 +71,9 @@ public sealed class GlueSystem : SharedGlueSystem
private bool TryGlue(Entity<GlueComponent> entity, EntityUid target, EntityUid actor) 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 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); _popup.PopupEntity(Loc.GetString("glue-failure", ("target", target)), actor, actor, PopupType.Medium);
return false; return false;

View File

@@ -124,6 +124,27 @@ public abstract partial class SharedHandsSystem : EntitySystem
return true; 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> /// <summary>
/// Tries to pick up an entity into any hand, forcing to drop an item if there are no free hands /// 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 /// By default it does check if it's possible to drop items

View File

@@ -215,6 +215,9 @@ namespace Content.Shared.Interaction
/// </summary> /// </summary>
private void OnRemoveAttempt(EntityUid uid, UnremoveableComponent item, ContainerGettingRemovedAttemptEvent args) private void OnRemoveAttempt(EntityUid uid, UnremoveableComponent item, ContainerGettingRemovedAttemptEvent args)
{ {
// 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(); args.Cancel();
} }

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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";
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1 @@
retractable-item-hand-cannot-drop = Your hand is already occupied.

View File

@@ -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

View File

@@ -12,12 +12,13 @@
state: icon state: icon
- type: MeleeWeapon - type: MeleeWeapon
wideAnimationRotation: 90 wideAnimationRotation: 90
attackRate: 0.75 attackRate: 1
damage: damage:
types: types:
Slash: 25 Slash: 10
Piercing: 15 Piercing: 10
- type: Item - type: Item
size: Normal size: Normal
sprite: Objects/Weapons/Melee/armblade.rsi sprite: Objects/Weapons/Melee/armblade.rsi
- type: Prying - type: Prying
pryPowered: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

View File

@@ -0,0 +1,14 @@
{
"version": 1,
"license": "CC0-1.0",
"copyright": "Created by TiniestShark (github)",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "armblade"
}
]
}