Wizard Item Recall Spell (#34411)

This commit is contained in:
ScarKy0
2025-02-08 22:56:08 +01:00
committed by GitHub
parent 1ef6e0bd57
commit bf6fd4d581
16 changed files with 376 additions and 20 deletions

View File

@@ -137,6 +137,7 @@ namespace Content.Client.Actions
component.Priority = state.Priority; component.Priority = state.Priority;
component.AttachedEntity = EnsureEntity<T>(state.AttachedEntity, uid); component.AttachedEntity = EnsureEntity<T>(state.AttachedEntity, uid);
component.RaiseOnUser = state.RaiseOnUser; component.RaiseOnUser = state.RaiseOnUser;
component.RaiseOnAction = state.RaiseOnAction;
component.AutoPopulate = state.AutoPopulate; component.AutoPopulate = state.AutoPopulate;
component.Temporary = state.Temporary; component.Temporary = state.Temporary;
component.ItemIconStyle = state.ItemIconStyle; component.ItemIconStyle = state.ItemIconStyle;

View File

@@ -0,0 +1,11 @@
using Content.Shared.ItemRecall;
namespace Content.Client.ItemRecall;
/// <summary>
/// System for handling the ItemRecall ability for wizards.
/// </summary>
public sealed partial class ItemRecallSystem : SharedItemRecallSystem
{
}

View File

@@ -0,0 +1,11 @@
using Content.Shared.ItemRecall;
namespace Content.Server.ItemRecall;
/// <summary>
/// System for handling the ItemRecall ability for wizards.
/// </summary>
public sealed partial class ItemRecallSystem : SharedItemRecallSystem
{
}

View File

@@ -167,6 +167,14 @@ public abstract partial class BaseActionComponent : Component
[DataField] [DataField]
public bool RaiseOnUser; public bool RaiseOnUser;
/// <summary>
/// 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.
/// </summary>
[DataField]
[Obsolete("This datafield will be reworked in an upcoming action refactor")]
public bool RaiseOnAction;
/// <summary> /// <summary>
/// Whether or not to automatically add this action to the action bar when it becomes available. /// Whether or not to automatically add this action to the action bar when it becomes available.
/// </summary> /// </summary>
@@ -212,6 +220,7 @@ public abstract class BaseActionComponentState : ComponentState
public int Priority; public int Priority;
public NetEntity? AttachedEntity; public NetEntity? AttachedEntity;
public bool RaiseOnUser; public bool RaiseOnUser;
public bool RaiseOnAction;
public bool AutoPopulate; public bool AutoPopulate;
public bool Temporary; public bool Temporary;
public ItemActionIconStyle ItemIconStyle; public ItemActionIconStyle ItemIconStyle;
@@ -223,6 +232,7 @@ public abstract class BaseActionComponentState : ComponentState
EntityIcon = entManager.GetNetEntity(component.EntIcon); EntityIcon = entManager.GetNetEntity(component.EntIcon);
AttachedEntity = entManager.GetNetEntity(component.AttachedEntity); AttachedEntity = entManager.GetNetEntity(component.AttachedEntity);
RaiseOnUser = component.RaiseOnUser; RaiseOnUser = component.RaiseOnUser;
RaiseOnAction = component.RaiseOnAction;
Icon = component.Icon; Icon = component.Icon;
IconOn = component.IconOn; IconOn = component.IconOn;
IconColor = component.IconColor; IconColor = component.IconColor;

View File

@@ -679,6 +679,9 @@ public abstract class SharedActionsSystem : EntitySystem
if (!action.RaiseOnUser && action.Container != null && !HasComp<MindComponent>(action.Container)) if (!action.RaiseOnUser && action.Container != null && !HasComp<MindComponent>(action.Container))
target = action.Container.Value; target = action.Container.Value;
if (action.RaiseOnAction)
target = actionId;
RaiseLocalEvent(target, (object) actionEvent, broadcast: true); RaiseLocalEvent(target, (object) actionEvent, broadcast: true);
handled = actionEvent.Handled; handled = actionEvent.Handled;
} }

View File

@@ -0,0 +1,43 @@
using Robust.Shared.GameStates;
namespace Content.Shared.ItemRecall;
/// <summary>
/// Component for the ItemRecall action.
/// Used for marking a held item and recalling it back into your hand with second action use.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedItemRecallSystem))]
public sealed partial class ItemRecallComponent : Component
{
/// <summary>
/// The name the action should have while an entity is marked.
/// </summary>
[DataField]
public LocId? WhileMarkedName = "item-recall-marked-name";
/// <summary>
/// The description the action should have while an entity is marked.
/// </summary>
[DataField]
public LocId? WhileMarkedDescription = "item-recall-marked-description";
/// <summary>
/// The name the action starts with.
/// This shouldn't be set in yaml.
/// </summary>
[DataField]
public string? InitialName;
/// <summary>
/// The description the action starts with.
/// This shouldn't be set in yaml.
/// </summary>
[DataField]
public string? InitialDescription;
/// <summary>
/// The entity currently marked to be recalled by this action.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? MarkedEntity;
}

View File

@@ -0,0 +1,9 @@
using Content.Shared.Actions;
namespace Content.Shared.ItemRecall;
/// <summary>
/// Raised when using the ItemRecall action.
/// </summary>
[ByRefEvent]
public sealed partial class OnItemRecallActionEvent : InstantActionEvent;

View File

@@ -0,0 +1,18 @@
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
namespace Content.Shared.ItemRecall;
/// <summary>
/// Component used as a marker for an item marked by the ItemRecall ability.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedItemRecallSystem))]
public sealed partial class RecallMarkerComponent : Component
{
/// <summary>
/// The action that marked this item.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? MarkedByAction;
}

View File

@@ -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;
/// <summary>
/// System for handling the ItemRecall ability for wizards.
/// </summary>
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<ItemRecallComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ItemRecallComponent, OnItemRecallActionEvent>(OnItemRecallActionUse);
SubscribeLocalEvent<RecallMarkerComponent, ComponentShutdown>(OnRecallMarkerShutdown);
}
private void OnMapInit(Entity<ItemRecallComponent> ent, ref MapInitEvent args)
{
ent.Comp.InitialName = Name(ent);
ent.Comp.InitialDescription = Description(ent);
}
private void OnItemRecallActionUse(Entity<ItemRecallComponent> ent, ref OnItemRecallActionEvent args)
{
if (ent.Comp.MarkedEntity == null)
{
if (!TryComp<HandsComponent>(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<RecallMarkerComponent>(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<RecallMarkerComponent?> ent)
{
if (!Resolve(ent.Owner, ref ent.Comp, false))
return;
if (!TryComp<InstantActionComponent>(ent.Comp.MarkedByAction, out var instantAction))
return;
var actionOwner = instantAction.AttachedEntity;
if (actionOwner == null)
return;
if (TryComp<EmbeddableProjectileComponent>(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<RecallMarkerComponent> ent, ref ComponentShutdown args)
{
TryUnmarkItem(ent);
}
private void TryMarkItem(Entity<ItemRecallComponent> ent, EntityUid item)
{
if (!TryComp<InstantActionComponent>(ent, out var instantAction))
return;
var actionOwner = instantAction.AttachedEntity;
if (actionOwner == null)
return;
AddToPvsOverride(item, actionOwner.Value);
var marker = AddComp<RecallMarkerComponent>(item);
ent.Comp.MarkedEntity = item;
Dirty(ent);
marker.MarkedByAction = ent.Owner;
UpdateActionAppearance(ent);
Dirty(item, marker);
}
private void TryUnmarkItem(EntityUid item)
{
if (!TryComp<RecallMarkerComponent>(item, out var marker))
return;
if (!TryComp<InstantActionComponent>(marker.MarkedByAction, out var instantAction))
return;
if (TryComp<ItemRecallComponent>(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<RecallMarkerComponent>(item);
}
private void UpdateActionAppearance(Entity<ItemRecallComponent> action)
{
if (!TryComp<InstantActionComponent>(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);
}
}

View File

@@ -67,25 +67,7 @@ public abstract partial class SharedProjectileSystem : EntitySystem
return; return;
} }
var xform = Transform(uid); UnEmbed(uid, component, args.User);
TryComp<PhysicsComponent>(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<ProjectileComponent>(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);
// try place it in the user's hand // try place it in the user's hand
_hands.TryPickupAnyHand(args.User, uid); _hands.TryPickupAnyHand(args.User, uid);
@@ -135,6 +117,38 @@ public abstract partial class SharedProjectileSystem : EntitySystem
Dirty(uid, component); Dirty(uid, component);
} }
public void UnEmbed(EntityUid uid, EmbeddableProjectileComponent? component, EntityUid? user = null)
{
if (!Resolve(uid, ref component))
return;
var xform = Transform(uid);
TryComp<PhysicsComponent>(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<ProjectileComponent>(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) private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args)
{ {
if (component.IgnoreShooter && (args.OtherEntity == component.Shooter || args.OtherEntity == component.Weapon)) if (component.IgnoreShooter && (args.OtherEntity == component.Shooter || args.OtherEntity == component.Weapon))

View File

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

View File

@@ -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-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-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 # Equipment
spellbook-wand-polymorph-door-name = Wand of Entrance spellbook-wand-polymorph-door-name = Wand of Entrance

View File

@@ -278,3 +278,16 @@
- SpellbookJaunt - SpellbookJaunt
- !type:ListingLimitedStockCondition - !type:ListingLimitedStockCondition
stock: 2 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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"license": "CC-BY-SA-3.0", "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": { "size": {
"x": 32, "x": 32,
"y": 32 "y": 32
@@ -30,6 +30,9 @@
}, },
{ {
"name": "gib" "name": "gib"
},
{
"name": "item_recall"
} }
] ]
} }