Wieldable/two-handed weapons (#4554)

* wielding kinda works

* rough out all the edges, wielding works nicely

* popups + loc

* increase damage & extra damage against whitelist

* small fixes

* forgot to actually do that

* reviews

* reviews + thing

* use resistances and not extradamageagainstwhitelist

* slashy

* make increasedamageonwield and melee hit events work with modifiersets

* Silly individual
This commit is contained in:
mirrorcult
2021-09-17 07:16:11 -07:00
committed by GitHub
parent 078a62762f
commit 62f6c8dd8e
36 changed files with 719 additions and 161 deletions

View File

@@ -83,22 +83,22 @@ namespace Content.Server.DoAfter
/// <summary>
/// Event to be raised directed to the <see cref="User"/> entity when the DoAfter is cancelled.
/// </summary>
public EntityEventArgs? UserCancelledEvent { get; set; }
public object? UserCancelledEvent { get; set; }
/// <summary>
/// Event to be raised directed to the <see cref="User"/> entity when the DoAfter is finished successfully.
/// </summary>
public EntityEventArgs? UserFinishedEvent { get; set; }
public object? UserFinishedEvent { get; set; }
/// <summary>
/// Event to be raised directed to the <see cref="Target"/> entity when the DoAfter is cancelled.
/// </summary>
public EntityEventArgs? TargetCancelledEvent { get; set; }
public object? TargetCancelledEvent { get; set; }
/// <summary>
/// Event to be raised directed to the <see cref="Target"/> entity when the DoAfter is finished successfully.
/// </summary>
public EntityEventArgs? TargetFinishedEvent { get; set; }
public object? TargetFinishedEvent { get; set; }
/// <summary>
/// Event to be broadcast when the DoAfter is cancelled.

View File

@@ -0,0 +1,106 @@
using Content.Server.Hands.Components;
using Content.Server.Pulling;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Hands.Systems
{
[UsedImplicitly]
public sealed class HandVirtualItemSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HandVirtualItemComponent, DroppedEvent>(HandleItemDropped);
SubscribeLocalEvent<HandVirtualItemComponent, UnequippedHandEvent>(HandleItemUnequipped);
SubscribeLocalEvent<HandVirtualItemComponent, BeforeInteractEvent>(HandleBeforeInteract);
}
public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user)
{
if (ComponentManager.TryGetComponent<HandsComponent>(user, out var hands))
{
foreach (var handName in hands.ActivePriorityEnumerable())
{
var hand = hands.GetHand(handName);
if (!hand.IsEmpty)
continue;
var pos = hands.Owner.Transform.Coordinates;
var virtualItem = EntityManager.SpawnEntity("HandVirtualItem", pos);
var virtualItemComp = virtualItem.GetComponent<HandVirtualItemComponent>();
virtualItemComp.BlockingEntity = blockingEnt;
hands.PutEntityIntoHand(hand, virtualItem);
return true;
}
}
return false;
}
private static void HandleBeforeInteract(
EntityUid uid,
HandVirtualItemComponent component,
BeforeInteractEvent args)
{
// No interactions with a virtual item, please.
args.Handled = true;
}
// If the virtual item gets removed from the hands for any reason, cancel the pull and delete it.
private void HandleItemUnequipped(EntityUid uid, HandVirtualItemComponent component, UnequippedHandEvent args)
{
Delete(component, args.User.Uid);
}
private void HandleItemDropped(EntityUid uid, HandVirtualItemComponent component, DroppedEvent args)
{
Delete(component, args.User.Uid);
}
/// <summary>
/// Queues a deletion for a virtual item and notifies the blocking entity and user.
/// </summary>
public void Delete(HandVirtualItemComponent comp, EntityUid user)
{
var userEv = new VirtualItemDeletedEvent(comp.BlockingEntity, user);
RaiseLocalEvent(user, userEv, false);
var targEv = new VirtualItemDeletedEvent(comp.BlockingEntity, user);
RaiseLocalEvent(comp.BlockingEntity, targEv, false);
comp.Owner.QueueDelete();
}
/// <summary>
/// Deletes all virtual items in a user's hands with
/// the specified blocked entity.
/// </summary>
public void DeleteInHandsMatching(EntityUid user, EntityUid matching)
{
if (ComponentManager.TryGetComponent<HandsComponent>(user, out var hands))
{
foreach (var handName in hands.ActivePriorityEnumerable())
{
var hand = hands.GetHand(handName);
if (hand.IsEmpty)
continue;
if (hand.HeldEntity != null)
{
if (ComponentManager.TryGetComponent<HandVirtualItemComponent>(hand.HeldEntity.Uid,
out var virt)
&& virt.BlockingEntity == matching)
{
Delete(virt, user);
}
}
}
}
}
}
}

View File

@@ -1,57 +0,0 @@
using Content.Server.Pulling;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Hands
{
[UsedImplicitly]
public sealed class HandVirtualPullSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HandVirtualPullComponent, DroppedEvent>(HandlePullerDropped);
SubscribeLocalEvent<HandVirtualPullComponent, UnequippedHandEvent>(HandlePullerUnequipped);
SubscribeLocalEvent<HandVirtualPullComponent, BeforeInteractEvent>(HandleBeforeInteract);
}
private static void HandleBeforeInteract(
EntityUid uid,
HandVirtualPullComponent component,
BeforeInteractEvent args)
{
// No interactions with a virtual pull, please.
args.Handled = true;
}
// If the virtual pull gets removed from the hands for any reason, cancel the pull and delete it.
private void HandlePullerUnequipped(EntityUid uid, HandVirtualPullComponent component, UnequippedHandEvent args)
{
MaybeDelete(component, args.User);
}
private void HandlePullerDropped(EntityUid uid, HandVirtualPullComponent component, DroppedEvent args)
{
MaybeDelete(component, args.User);
}
private void MaybeDelete(HandVirtualPullComponent comp, IEntity? user)
{
var pulled = comp.PulledEntity;
if (!ComponentManager.TryGetComponent(pulled, out PullableComponent? pullable))
return;
if (pullable.Puller != user)
return;
pullable.TryStopPull(user);
comp.Owner.QueueDelete();
}
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Hands.Components;
using Content.Server.Hands.Systems;
using Content.Server.Interaction;
using Content.Server.Inventory.Components;
using Content.Server.Items;
@@ -26,13 +27,14 @@ using Robust.Shared.Players;
using Robust.Shared.Utility;
using static Content.Shared.Inventory.EquipmentSlotDefines;
namespace Content.Server.Hands
namespace Content.Server.Hands.Systems
{
[UsedImplicitly]
internal sealed class HandsSystem : SharedHandsSystem
{
[Dependency] private readonly InteractionSystem _interactionSystem = default!;
[Dependency] private readonly StackSystem _stackSystem = default!;
[Dependency] private readonly HandVirtualItemSystem _virtualItemSystem = default!;
public override void Initialize()
{
@@ -68,21 +70,10 @@ namespace Content.Server.Hands
private void HandlePullStarted(EntityUid uid, HandsComponent component, PullStartedMessage args)
{
foreach (var handName in component.ActivePriorityEnumerable())
if (!_virtualItemSystem.TrySpawnVirtualItemInHand(args.Pulled.Owner.Uid, uid))
{
var hand = component.GetHand(handName);
if (!hand.IsEmpty)
continue;
var pos = component.Owner.Transform.Coordinates;
var virtualPull = EntityManager.SpawnEntity("HandVirtualPull", pos);
var virtualPullComp = virtualPull.GetComponent<HandVirtualPullComponent>();
virtualPullComp.PulledEntity = args.Pulled.Owner.Uid;
component.PutEntityIntoHand(hand, virtualPull);
return;
DebugTools.Assert("Unable to find available hand when starting pulling??");
}
DebugTools.Assert("Unable to find available hand when starting pulling??");
}
private void HandlePullStopped(EntityUid uid, HandsComponent component, PullStoppedMessage args)
@@ -92,8 +83,8 @@ namespace Content.Server.Hands
foreach (var hand in component.Hands)
{
if (hand.HeldEntity == null
|| !hand.HeldEntity.TryGetComponent(out HandVirtualPullComponent? virtualPull)
|| virtualPull.PulledEntity != args.Pulled.Owner.Uid)
|| !hand.HeldEntity.TryGetComponent(out HandVirtualItemComponent? virtualItem)
|| virtualItem.BlockingEntity != args.Pulled.Owner.Uid)
continue;
hand.HeldEntity.Delete();

View File

@@ -88,7 +88,9 @@ namespace Content.Server.Weapon.Melee
{
var targets = new[] { target };
SendAnimation(comp.ClickArc, angle, args.User, owner, targets, comp.ClickAttackEffect, false);
_damageableSystem.TryChangeDamage(target.Uid, comp.Damage);
_damageableSystem.TryChangeDamage(target.Uid,
DamageSpecifier.ApplyModifierSets(comp.Damage, hitEvent.ModifiersList));
SoundSystem.Play(Filter.Pvs(owner), comp.HitSound.GetSound(), target);
}
}
@@ -153,7 +155,8 @@ namespace Content.Server.Weapon.Melee
foreach (var entity in hitEntities)
{
_damageableSystem.TryChangeDamage(entity.Uid, comp.Damage);
_damageableSystem.TryChangeDamage(entity.Uid,
DamageSpecifier.ApplyModifierSets(comp.Damage, hitEvent.ModifiersList));
}
}
@@ -274,6 +277,17 @@ namespace Content.Server.Weapon.Melee
/// </summary>
public class MeleeHitEvent : HandledEntityEventArgs
{
/// <summary>
/// Modifier sets to apply to the hit event when it's all said and done.
/// This should be modified by adding a new entry to the list.
/// </summary>
public List<DamageModifierSet> ModifiersList = new();
/// <summary>
/// A flat amount of damage to add. Same reason as above with Multiplier.
/// </summary>
public int FlatDamage = 0;
/// <summary>
/// A list containing every hit entity. Can be zero.
/// </summary>

View File

@@ -0,0 +1,17 @@
using Content.Shared.Damage;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Wieldable.Components
{
[RegisterComponent, Friend(typeof(WieldableSystem))]
public class IncreaseDamageOnWieldComponent : Component
{
public override string Name { get; } = "IncreaseDamageOnWield";
[DataField("modifiers", required: true)]
public DamageModifierSet Modifiers = default!;
}
}

View File

@@ -0,0 +1,59 @@
using System.ComponentModel.DataAnnotations.Schema;
using Content.Shared.Sound;
using Content.Shared.Verbs;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Wieldable.Components
{
/// <summary>
/// Used for objects that can be wielded in two or more hands,
/// </summary>
[RegisterComponent, Friend(typeof(WieldableSystem))]
public class WieldableComponent : Component
{
public override string Name => "Wieldable";
[DataField("wieldSound")]
public SoundSpecifier? WieldSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
[DataField("unwieldSound")]
public SoundSpecifier? UnwieldSound = default!;
/// <summary>
/// Number of free hands required (excluding the item itself) required
/// to wield it
/// </summary>
[DataField("freeHandsRequired")]
public int FreeHandsRequired = 1;
public bool Wielded = false;
public string WieldedInhandPrefix = "wielded";
public string? OldInhandPrefix = null;
[DataField("wieldTime")]
public float WieldTime = 1.5f;
[Verb]
public sealed class ToggleWieldVerb : Verb<WieldableComponent>
{
protected override void GetData(IEntity user, WieldableComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Visible;
data.Text = component.Wielded ? "Unwield" : "Wield";
}
protected override void Activate(IEntity user, WieldableComponent component)
{
if(!component.Wielded)
EntitySystem.Get<WieldableSystem>().AttemptWield(component.Owner.Uid, component, user);
else
EntitySystem.Get<WieldableSystem>().AttemptUnwield(component.Owner.Uid, component, user);
}
}
}
}

View File

@@ -0,0 +1,289 @@
using Content.Server.DoAfter;
using Content.Server.Hands.Components;
using Content.Server.Hands.Systems;
using Content.Server.Items;
using Content.Server.Weapon.Melee;
using Content.Server.Wieldable.Components;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Notification.Managers;
using Content.Shared.Throwing;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Player;
namespace Content.Server.Wieldable
{
public class WieldableSystem : EntitySystem
{
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly HandVirtualItemSystem _virtualItemSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WieldableComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<WieldableComponent, ItemWieldedEvent>(OnItemWielded);
SubscribeLocalEvent<WieldableComponent, ItemUnwieldedEvent>(OnItemUnwielded);
SubscribeLocalEvent<WieldableComponent, UnequippedHandEvent>(OnItemLeaveHand);
SubscribeLocalEvent<WieldableComponent, VirtualItemDeletedEvent>(OnVirtualItemDeleted);
SubscribeLocalEvent<IncreaseDamageOnWieldComponent, MeleeHitEvent>(OnMeleeHit);
}
private void OnUseInHand(EntityUid uid, WieldableComponent component, UseInHandEvent args)
{
if (args.Handled)
return;
if(!component.Wielded)
AttemptWield(uid, component, args.User);
else
AttemptUnwield(uid, component, args.User);
}
public bool CanWield(EntityUid uid, WieldableComponent component, IEntity user, bool quiet=false)
{
// Do they have enough hands free?
if (!ComponentManager.TryGetComponent<HandsComponent>(user.Uid, out var hands))
{
if(!quiet)
user.PopupMessage(Loc.GetString("wieldable-component-no-hands"));
return false;
}
if (hands.GetFreeHands() < component.FreeHandsRequired)
{
// TODO FLUENT need function to change 'hands' to 'hand' when there's only 1 required
if (!quiet)
{
user.PopupMessage(Loc.GetString("wieldable-component-not-enough-free-hands",
("number", component.FreeHandsRequired),
("item", EntityManager.GetEntity(uid))));
}
return false;
}
// Is it.. actually in one of their hands?
if (!hands.TryGetHandHoldingEntity(EntityManager.GetEntity(uid), out var _))
{
if (!quiet)
{
user.PopupMessage(Loc.GetString("wieldable-component-not-in-hands",
("item", EntityManager.GetEntity(uid))));
}
return false;
}
// Seems legit.
return true;
}
/// <summary>
/// Attempts to wield an item, creating a DoAfter..
/// </summary>
public void AttemptWield(EntityUid uid, WieldableComponent component, IEntity user)
{
if (!CanWield(uid, component, user))
return;
var ev = new BeforeWieldEvent();
RaiseLocalEvent(uid, ev, false);
var used = EntityManager.GetEntity(uid);
if (ev.Cancelled) return;
var doargs = new DoAfterEventArgs(
user,
component.WieldTime,
default,
used
)
{
BreakOnUserMove = false,
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = true,
TargetFinishedEvent = new ItemWieldedEvent(user),
UserFinishedEvent = new WieldedItemEvent(used)
};
_doAfter.DoAfter(doargs);
}
/// <summary>
/// Attempts to unwield an item, with no DoAfter.
/// </summary>
public void AttemptUnwield(EntityUid uid, WieldableComponent component, IEntity user)
{
var ev = new BeforeUnwieldEvent();
RaiseLocalEvent(uid, ev, false);
var used = EntityManager.GetEntity(uid);
if (ev.Cancelled) return;
var targEv = new ItemUnwieldedEvent(user);
var userEv = new UnwieldedItemEvent(used);
RaiseLocalEvent(uid, targEv, false);
RaiseLocalEvent(user.Uid, userEv, false);
}
private void OnItemWielded(EntityUid uid, WieldableComponent component, ItemWieldedEvent args)
{
if (args.User == null)
return;
if (!CanWield(uid, component, args.User) || component.Wielded)
return;
if (ComponentManager.TryGetComponent<ItemComponent>(uid, out var item))
{
component.OldInhandPrefix = item.EquippedPrefix;
item.EquippedPrefix = component.WieldedInhandPrefix;
}
component.Wielded = true;
if (component.WieldSound != null)
{
SoundSystem.Play(Filter.Pvs(EntityManager.GetEntity(uid)), component.WieldSound.GetSound());
}
for (var i = 0; i < component.FreeHandsRequired; i++)
{
_virtualItemSystem.TrySpawnVirtualItemInHand(uid, args.User.Uid);
}
args.User.PopupMessage(Loc.GetString("wieldable-component-successful-wield",
("item", EntityManager.GetEntity(uid))));
}
private void OnItemUnwielded(EntityUid uid, WieldableComponent component, ItemUnwieldedEvent args)
{
if (args.User == null)
return;
if (!component.Wielded)
return;
if (ComponentManager.TryGetComponent<ItemComponent>(uid, out var item))
{
item.EquippedPrefix = component.OldInhandPrefix;
}
component.Wielded = false;
if (!args.Force) // don't play sound/popup if this was a forced unwield
{
if (component.UnwieldSound != null)
{
SoundSystem.Play(Filter.Pvs(EntityManager.GetEntity(uid)),
component.UnwieldSound.GetSound());
}
args.User.PopupMessage(Loc.GetString("wieldable-component-failed-wield",
("item", EntityManager.GetEntity(uid))));
}
_virtualItemSystem.DeleteInHandsMatching(args.User.Uid, uid);
}
private void OnItemLeaveHand(EntityUid uid, WieldableComponent component, UnequippedHandEvent args)
{
if (!component.Wielded || component.Owner.Uid != args.Unequipped.Uid)
return;
RaiseLocalEvent(uid, new ItemUnwieldedEvent(args.User, force: true));
}
private void OnVirtualItemDeleted(EntityUid uid, WieldableComponent component, VirtualItemDeletedEvent args)
{
if(args.BlockingEntity == uid && component.Wielded)
AttemptUnwield(args.BlockingEntity, component, EntityManager.GetEntity(args.User));
}
private void OnMeleeHit(EntityUid uid, IncreaseDamageOnWieldComponent component, MeleeHitEvent args)
{
if (ComponentManager.TryGetComponent<WieldableComponent>(uid, out var wield))
{
if (!wield.Wielded)
return;
}
if (args.Handled)
return;
args.ModifiersList.Add(component.Modifiers);
}
}
#region Events
public class BeforeWieldEvent : CancellableEntityEventArgs
{
}
/// <summary>
/// Raised on the item that has been wielded.
/// </summary>
public class ItemWieldedEvent : EntityEventArgs
{
public IEntity? User;
public ItemWieldedEvent(IEntity? user=null)
{
User = user;
}
}
/// <summary>
/// Raised on the user who wielded the item.
/// </summary>
public class WieldedItemEvent : EntityEventArgs
{
public IEntity Item;
public WieldedItemEvent(IEntity item)
{
Item = item;
}
}
public class BeforeUnwieldEvent : CancellableEntityEventArgs
{
}
/// <summary>
/// Raised on the item that has been unwielded.
/// </summary>
public class ItemUnwieldedEvent : EntityEventArgs
{
public IEntity? User;
/// <summary>
/// Whether the item is being forced to be unwielded, or if the player chose to unwield it themselves.
/// </summary>
public bool Force;
public ItemUnwieldedEvent(IEntity? user=null, bool force=false)
{
User = user;
Force = force;
}
}
/// <summary>
/// Raised on the user who unwielded the item.
/// </summary>
public class UnwieldedItemEvent : EntityEventArgs
{
public IEntity Item;
public UnwieldedItemEvent(IEntity item)
{
Item = item;
}
}
#endregion
}