diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index b594817701ea..d836c2ed7a88 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -137,6 +137,7 @@ private void BaseHandleState(EntityUid uid, BaseActionComponent component, Ba component.Priority = state.Priority; component.AttachedEntity = EnsureEntity(state.AttachedEntity, uid); component.RaiseOnUser = state.RaiseOnUser; + component.RaiseOnAction = state.RaiseOnAction; component.AutoPopulate = state.AutoPopulate; component.Temporary = state.Temporary; component.ItemIconStyle = state.ItemIconStyle; diff --git a/Content.Client/ItemRecall/ItemRecallSystem.cs b/Content.Client/ItemRecall/ItemRecallSystem.cs new file mode 100644 index 000000000000..11d3015c21ff --- /dev/null +++ b/Content.Client/ItemRecall/ItemRecallSystem.cs @@ -0,0 +1,11 @@ +using Content.Shared.ItemRecall; + +namespace Content.Client.ItemRecall; + +/// +/// System for handling the ItemRecall ability for wizards. +/// +public sealed partial class ItemRecallSystem : SharedItemRecallSystem +{ + +} diff --git a/Content.Server/ItemRecall/ItemRecallSystem.cs b/Content.Server/ItemRecall/ItemRecallSystem.cs new file mode 100644 index 000000000000..88972e9e359f --- /dev/null +++ b/Content.Server/ItemRecall/ItemRecallSystem.cs @@ -0,0 +1,11 @@ +using Content.Shared.ItemRecall; + +namespace Content.Server.ItemRecall; + +/// +/// System for handling the ItemRecall ability for wizards. +/// +public sealed partial class ItemRecallSystem : SharedItemRecallSystem +{ + +} diff --git a/Content.Shared/Actions/BaseActionComponent.cs b/Content.Shared/Actions/BaseActionComponent.cs index c3aa6cc97eeb..1744751715c4 100644 --- a/Content.Shared/Actions/BaseActionComponent.cs +++ b/Content.Shared/Actions/BaseActionComponent.cs @@ -167,6 +167,13 @@ public EntityUid? EntityIcon [DataField] public bool RaiseOnUser; + /// + /// If true, this will cause the the action event to always be raised directed at the action itself instead of the action's container/provider. + /// Takes priority over RaiseOnUser. + /// + [DataField] + public bool RaiseOnAction; + /// /// Whether or not to automatically add this action to the action bar when it becomes available. /// @@ -212,6 +219,7 @@ public abstract class BaseActionComponentState : ComponentState public int Priority; public NetEntity? AttachedEntity; public bool RaiseOnUser; + public bool RaiseOnAction; public bool AutoPopulate; public bool Temporary; public ItemActionIconStyle ItemIconStyle; @@ -223,6 +231,7 @@ protected BaseActionComponentState(BaseActionComponent component, IEntityManager EntityIcon = entManager.GetNetEntity(component.EntIcon); AttachedEntity = entManager.GetNetEntity(component.AttachedEntity); RaiseOnUser = component.RaiseOnUser; + RaiseOnAction = component.RaiseOnAction; Icon = component.Icon; IconOn = component.IconOn; IconColor = component.IconColor; diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index fc6f0baf7721..8079885a5ac5 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -679,6 +679,9 @@ public void PerformAction(EntityUid performer, ActionsComponent? component, Enti if (!action.RaiseOnUser && action.Container != null && !HasComp(action.Container)) target = action.Container.Value; + if (action.RaiseOnAction) + target = actionId; + RaiseLocalEvent(target, (object) actionEvent, broadcast: true); handled = actionEvent.Handled; } diff --git a/Content.Shared/ItemRecall/ItemRecallComponent.cs b/Content.Shared/ItemRecall/ItemRecallComponent.cs new file mode 100644 index 000000000000..e057a9945c21 --- /dev/null +++ b/Content.Shared/ItemRecall/ItemRecallComponent.cs @@ -0,0 +1,43 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.ItemRecall; + +/// +/// Component for the ItemRecall action. +/// Used for marking a held item and recalling it back into your hand with second action use. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedItemRecallSystem))] +public sealed partial class ItemRecallComponent : Component +{ + /// + /// The name the action should have while an entity is marked. + /// + [DataField] + public LocId? WhileMarkedName = "item-recall-marked-name"; + + /// + /// The description the action should have while an entity is marked. + /// + [DataField] + public LocId? WhileMarkedDescription = "item-recall-marked-description"; + + /// + /// The name the action starts with. + /// This shouldn't be set in yaml. + /// + [DataField] + public string? InitialName; + + /// + /// The description the action starts with. + /// This shouldn't be set in yaml. + /// + [DataField] + public string? InitialDescription; + + /// + /// The entity currently marked to be recalled by this action. + /// + [DataField, AutoNetworkedField] + public EntityUid? MarkedEntity; +} diff --git a/Content.Shared/ItemRecall/ItemRecallEvents.cs b/Content.Shared/ItemRecall/ItemRecallEvents.cs new file mode 100644 index 000000000000..8bee46a09831 --- /dev/null +++ b/Content.Shared/ItemRecall/ItemRecallEvents.cs @@ -0,0 +1,9 @@ +using Content.Shared.Actions; + +namespace Content.Shared.ItemRecall; + +/// +/// Raised when using the ItemRecall action. +/// +[ByRefEvent] +public sealed partial class OnItemRecallActionEvent : InstantActionEvent; diff --git a/Content.Shared/ItemRecall/RecallMarkerComponent.cs b/Content.Shared/ItemRecall/RecallMarkerComponent.cs new file mode 100644 index 000000000000..a85b22e9e340 --- /dev/null +++ b/Content.Shared/ItemRecall/RecallMarkerComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Utility; + +namespace Content.Shared.ItemRecall; + + +/// +/// Component used as a marker for an item marked by the ItemRecall ability. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedItemRecallSystem))] +public sealed partial class RecallMarkerComponent : Component +{ + /// + /// The action that marked this item. + /// + [DataField, AutoNetworkedField] + public EntityUid? MarkedByAction; +} diff --git a/Content.Shared/ItemRecall/SharedItemRecallSystem.cs b/Content.Shared/ItemRecall/SharedItemRecallSystem.cs new file mode 100644 index 000000000000..63d38203c654 --- /dev/null +++ b/Content.Shared/ItemRecall/SharedItemRecallSystem.cs @@ -0,0 +1,187 @@ +using Content.Shared.Actions; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Popups; +using Content.Shared.Projectiles; +using Robust.Shared.GameStates; +using Robust.Shared.Player; + +namespace Content.Shared.ItemRecall; + +/// +/// System for handling the ItemRecall ability for wizards. +/// +public abstract partial class SharedItemRecallSystem : EntitySystem +{ + [Dependency] private readonly ISharedPlayerManager _player = default!; + [Dependency] private readonly SharedPvsOverrideSystem _pvs = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedPopupSystem _popups = default!; + [Dependency] private readonly SharedProjectileSystem _proj = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnItemRecallActionUse); + + SubscribeLocalEvent(OnRecallMarkerShutdown); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.InitialName = Name(ent); + ent.Comp.InitialDescription = Description(ent); + } + + private void OnItemRecallActionUse(Entity ent, ref OnItemRecallActionEvent args) + { + if (ent.Comp.MarkedEntity == null) + { + if (!TryComp(args.Performer, out var hands)) + return; + + var markItem = _hands.GetActiveItem((args.Performer, hands)); + + if (markItem == null) + { + _popups.PopupClient(Loc.GetString("item-recall-item-mark-empty"), args.Performer, args.Performer); + return; + } + + if (HasComp(markItem)) + { + _popups.PopupClient(Loc.GetString("item-recall-item-already-marked", ("item", markItem)), args.Performer, args.Performer); + return; + } + + _popups.PopupClient(Loc.GetString("item-recall-item-marked", ("item", markItem.Value)), args.Performer, args.Performer); + TryMarkItem(ent, markItem.Value); + return; + } + + RecallItem(ent.Comp.MarkedEntity.Value); + args.Handled = true; + } + + private void RecallItem(Entity ent) + { + if (!Resolve(ent.Owner, ref ent.Comp, false)) + return; + + if (!TryComp(ent.Comp.MarkedByAction, out var instantAction)) + return; + + var actionOwner = instantAction.AttachedEntity; + + if (actionOwner == null) + return; + + if (TryComp(ent, out var projectile)) + _proj.UnEmbed(ent, projectile, actionOwner.Value); + + _popups.PopupPredicted(Loc.GetString("item-recall-item-summon", ("item", ent)), actionOwner.Value, actionOwner.Value); + + _hands.TryForcePickupAnyHand(actionOwner.Value, ent); + } + + private void OnRecallMarkerShutdown(Entity ent, ref ComponentShutdown args) + { + TryUnmarkItem(ent); + } + + private void TryMarkItem(Entity ent, EntityUid item) + { + if (!TryComp(ent, out var instantAction)) + return; + + var actionOwner = instantAction.AttachedEntity; + + if (actionOwner == null) + return; + + AddToPvsOverride(item, actionOwner.Value); + + var marker = AddComp(item); + ent.Comp.MarkedEntity = item; + Dirty(ent); + + marker.MarkedByAction = ent.Owner; + + UpdateActionAppearance(ent); + Dirty(item, marker); + } + + private void TryUnmarkItem(EntityUid item) + { + if (!TryComp(item, out var marker)) + return; + + if (!TryComp(marker.MarkedByAction, out var instantAction)) + return; + + if (TryComp(marker.MarkedByAction, out var action)) + { + // For some reason client thinks the station grid owns the action on client and this doesn't work. It doesn't work in PopupEntity(mispredicts) and PopupPredicted either(doesnt show). + // I don't have the heart to move this code to server because of this small thing. + // This line will only do something once that is fixed. + if (instantAction.AttachedEntity != null) + { + _popups.PopupClient(Loc.GetString("item-recall-item-unmark", ("item", item)), instantAction.AttachedEntity.Value, instantAction.AttachedEntity.Value, PopupType.MediumCaution); + RemoveFromPvsOverride(item, instantAction.AttachedEntity.Value); + } + + action.MarkedEntity = null; + UpdateActionAppearance((marker.MarkedByAction.Value, action)); + Dirty(marker.MarkedByAction.Value, action); + } + + RemCompDeferred(item); + } + + private void UpdateActionAppearance(Entity action) + { + if (!TryComp(action, out var instantAction)) + return; + + if (action.Comp.MarkedEntity == null) + { + if (action.Comp.InitialName != null) + _metaData.SetEntityName(action, action.Comp.InitialName); + if (action.Comp.InitialDescription != null) + _metaData.SetEntityDescription(action, action.Comp.InitialDescription); + _actions.SetEntityIcon(action, null, instantAction); + } + else + { + if (action.Comp.WhileMarkedName != null) + _metaData.SetEntityName(action, Loc.GetString(action.Comp.WhileMarkedName, + ("item", action.Comp.MarkedEntity.Value))); + + if (action.Comp.WhileMarkedDescription != null) + _metaData.SetEntityDescription(action, Loc.GetString(action.Comp.WhileMarkedDescription, + ("item", action.Comp.MarkedEntity.Value))); + + _actions.SetEntityIcon(action, action.Comp.MarkedEntity, instantAction); + } + } + + private void AddToPvsOverride(EntityUid uid, EntityUid user) + { + if (!_player.TryGetSessionByEntity(user, out var mindSession)) + return; + + _pvs.AddSessionOverride(uid, mindSession); + } + + private void RemoveFromPvsOverride(EntityUid uid, EntityUid user) + { + if (!_player.TryGetSessionByEntity(user, out var mindSession)) + return; + + _pvs.RemoveSessionOverride(uid, mindSession); + } +} diff --git a/Content.Shared/Projectiles/SharedProjectileSystem.cs b/Content.Shared/Projectiles/SharedProjectileSystem.cs index bca9b36f8985..1d0fc16cbd7d 100644 --- a/Content.Shared/Projectiles/SharedProjectileSystem.cs +++ b/Content.Shared/Projectiles/SharedProjectileSystem.cs @@ -67,25 +67,7 @@ private void OnEmbedRemove(EntityUid uid, EmbeddableProjectileComponent componen return; } - var xform = Transform(uid); - TryComp(uid, out var physics); - _physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform); - _transform.AttachToGridOrMap(uid, xform); - component.EmbeddedIntoUid = null; - Dirty(uid, component); - - // Reset whether the projectile has damaged anything if it successfully was removed - if (TryComp(uid, out var projectile)) - { - projectile.Shooter = null; - projectile.Weapon = null; - projectile.ProjectileSpent = false; - } - - // Land it just coz uhhh yeah - var landEv = new LandEvent(args.User, true); - RaiseLocalEvent(uid, ref landEv); - _physics.WakeBody(uid, body: physics); + UnEmbed(uid, component, args.User); // try place it in the user's hand _hands.TryPickupAnyHand(args.User, uid); @@ -135,6 +117,38 @@ private void Embed(EntityUid uid, EntityUid target, EntityUid? user, EmbeddableP Dirty(uid, component); } + public void UnEmbed(EntityUid uid, EmbeddableProjectileComponent? component, EntityUid? user = null) + { + if (!Resolve(uid, ref component)) + return; + + var xform = Transform(uid); + TryComp(uid, out var physics); + _physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform); + _transform.AttachToGridOrMap(uid, xform); + component.EmbeddedIntoUid = null; + Dirty(uid, component); + + // Reset whether the projectile has damaged anything if it successfully was removed + if (TryComp(uid, out var projectile)) + { + projectile.Shooter = null; + projectile.Weapon = null; + projectile.ProjectileSpent = false; + + Dirty(uid, projectile); + } + + if (user != null) + { + // Land it just coz uhhh yeah + var landEv = new LandEvent(user, true); + RaiseLocalEvent(uid, ref landEv); + } + + _physics.WakeBody(uid, body: physics); + } + private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args) { if (component.IgnoreShooter && (args.OtherEntity == component.Shooter || args.OtherEntity == component.Weapon)) diff --git a/Resources/Locale/en-US/item-recall/item-recall.ftl b/Resources/Locale/en-US/item-recall/item-recall.ftl new file mode 100644 index 000000000000..680c7b7b3fb7 --- /dev/null +++ b/Resources/Locale/en-US/item-recall/item-recall.ftl @@ -0,0 +1,9 @@ +item-recall-marked-name = Recall {CAPITALIZE($item)} +item-recall-marked-description = Recall {THE($item)} back into your hand. + +item-recall-item-marked = You draw a magical sigil on {THE($item)}. +item-recall-item-already-marked = {CAPITALIZE(THE($item))} is already marked! +item-recall-item-mark-empty = You must be holding an item! +item-recall-item-summon = {CAPITALIZE(THE($item))} appears in your hand! +item-recall-item-unmark = You feel your connection with {THE($item)} sever. + diff --git a/Resources/Locale/en-US/store/spellbook-catalog.ftl b/Resources/Locale/en-US/store/spellbook-catalog.ftl index b18cac4f9a70..95a8b25e686d 100644 --- a/Resources/Locale/en-US/store/spellbook-catalog.ftl +++ b/Resources/Locale/en-US/store/spellbook-catalog.ftl @@ -35,6 +35,9 @@ spellbook-cluwne-desc = For when you really hate someone and Smite isn't enough. spellbook-slip-name = Slippery Slope spellbook-slip-desc = Learn the ancient ways of the Janitor and curse your target to be slippery. Requires Wizard Robe & Hat. +spellbook-item-recall-name = Item Recall +spellbook-item-recall-description = Mark a held item and summon it back at any time with just a snap of your fingers! + # Equipment spellbook-wand-polymorph-door-name = Wand of Entrance diff --git a/Resources/Prototypes/Catalog/spellbook_catalog.yml b/Resources/Prototypes/Catalog/spellbook_catalog.yml index dfd171d9b277..3ba3189771be 100644 --- a/Resources/Prototypes/Catalog/spellbook_catalog.yml +++ b/Resources/Prototypes/Catalog/spellbook_catalog.yml @@ -278,3 +278,16 @@ - SpellbookJaunt - !type:ListingLimitedStockCondition stock: 2 + +- type: listing + id: SpellbookItemRecallSwap + name: spellbook-item-recall-name + description: spellbook-item-recall-description + productAction: ActionItemRecall + cost: + WizCoin: 1 + categories: + - SpellbookUtility + conditions: + - !type:ListingLimitedStockCondition + stock: 1 diff --git a/Resources/Prototypes/Magic/recall_spell.yml b/Resources/Prototypes/Magic/recall_spell.yml new file mode 100644 index 000000000000..c5bb96870db0 --- /dev/null +++ b/Resources/Prototypes/Magic/recall_spell.yml @@ -0,0 +1,21 @@ +- type: entity + id: ActionItemRecall + name: Mark Item + description: Mark a held item to later summon into your hand. + components: + - type: InstantAction + useDelay: 10 + raiseOnAction: true + itemIconStyle: BigAction + sound: !type:SoundPathSpecifier + path: /Audio/Magic/forcewall.ogg + params: + volume: -5 + pitch: 1.2 + maxDistance: 5 + variation: 0.2 + icon: + sprite: Objects/Magic/magicactions.rsi + state: item_recall + event: !type:OnItemRecallActionEvent + - type: ItemRecall diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/item_recall.png b/Resources/Textures/Objects/Magic/magicactions.rsi/item_recall.png new file mode 100644 index 000000000000..00bbe363793c Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/item_recall.png differ diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/meta.json b/Resources/Textures/Objects/Magic/magicactions.rsi/meta.json index a8da3d8bc3a6..a1112f0c6d60 100644 --- a/Resources/Textures/Objects/Magic/magicactions.rsi/meta.json +++ b/Resources/Textures/Objects/Magic/magicactions.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "https://github.com/Citadel-Station-13/Citadel-Station-13/commit/78db6bd5c2b2b3d1f5cd8fd75be3a39d5d929943 andhttps://github.com/tgstation/tgstation/commit/906fb0682bab6a0975b45036001c54f021f58ae7 ", + "copyright": "https://github.com/Citadel-Station-13/Citadel-Station-13/commit/78db6bd5c2b2b3d1f5cd8fd75be3a39d5d929943 and https://github.com/tgstation/tgstation/commit/906fb0682bab6a0975b45036001c54f021f58ae7, item_recall by ScarKy0", "size": { "x": 32, "y": 32 @@ -30,6 +30,9 @@ }, { "name": "gib" + }, + { + "name": "item_recall" } ] }