diff --git a/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs b/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs index 1a4b1212ee..83a8950ba1 100644 --- a/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs +++ b/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs @@ -1,10 +1,14 @@ -using Content.Client.GameObjects.Components.Mobs; +using System; +using Content.Client.GameObjects.Components.Mobs; using Content.Client.GameObjects.Components.Weapons.Melee; using Content.Shared.GameObjects.Components.Weapons.Melee; using JetBrains.Annotations; +using Robust.Client.GameObjects; using Robust.Client.Interfaces.GameObjects.Components; using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.EntitySystemMessages; using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; @@ -18,6 +22,7 @@ namespace Content.Client.GameObjects.EntitySystems public sealed class MeleeWeaponSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; public override void Initialize() { @@ -53,6 +58,26 @@ namespace Content.Client.GameObjects.EntitySystems var weaponArcAnimation = entity.GetComponent(); weaponArcAnimation.SetData(weaponArc, msg.Angle, attacker); + // Due to ISpriteComponent limitations, weapons that don't use an RSI won't have this effect. + if (EntityManager.TryGetEntity(msg.Source, out var source) && msg.TextureEffect && source.TryGetComponent(out ISpriteComponent sourceSprite) + && sourceSprite.BaseRSI?.Path != null) + { + var sys = Get(); + var curTime = _gameTiming.CurTime; + var effect = new EffectSystemMessage + { + EffectSprite = sourceSprite.BaseRSI.Path.ToString(), + RsiState = sourceSprite.LayerGetState(0).Name, + Coordinates = attacker.Transform.GridPosition, + Color = Vector4.Multiply(new Vector4(255, 255, 255, 125), 1.0f), + ColorDelta = Vector4.Multiply(new Vector4(0, 0, 0, -10), 1.0f), + Velocity = msg.Angle.ToVec(), + Acceleration = msg.Angle.ToVec() * 5f, + Born = curTime, + DeathTime = curTime.Add(TimeSpan.FromMilliseconds(300f)), + }; + sys.CreateEffect(effect); + } foreach (var uid in msg.Hits) { diff --git a/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponSpeedCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponSpeedCon.cs index fc56a593f7..63e1bddd46 100644 --- a/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponSpeedCon.cs +++ b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponSpeedCon.cs @@ -15,7 +15,7 @@ namespace Content.Server.AI.Utility.Considerations.Combat.Melee return 0.0f; } - return meleeWeaponComponent.CooldownTime / 10.0f; + return meleeWeaponComponent.ArcCooldownTime / 10.0f; } } } diff --git a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs index b4f89bca67..ef2cf24729 100644 --- a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs @@ -17,6 +17,8 @@ using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; using Content.Shared.Damage; using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects.EntitySystemMessages; namespace Content.Server.GameObjects.Components.Weapon.Melee { @@ -24,59 +26,54 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee public class MeleeWeaponComponent : Component, IAttack { [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IPhysicsManager _physicsManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; public override string Name => "MeleeWeapon"; private TimeSpan _lastAttackTime; + private TimeSpan _cooldownEnd; - private int _damage; - private float _range; - private float _arcWidth; - private string _arc; private string _hitSound; - public float CooldownTime => _cooldownTime; - private float _cooldownTime = 1f; + private string _missSound; + public float ArcCooldownTime { get; private set; } = 1f; + public float CooldownTime { get; private set; } = 0.5f; [ViewVariables(VVAccess.ReadWrite)] - public string Arc - { - get => _arc; - set => _arc = value; - } + public string ClickArc { get; set; } [ViewVariables(VVAccess.ReadWrite)] - public float ArcWidth - { - get => _arcWidth; - set => _arcWidth = value; - } + public string Arc { get; set; } [ViewVariables(VVAccess.ReadWrite)] - public float Range - { - get => _range; - set => _range = value; - } + public float ArcWidth { get; set; } [ViewVariables(VVAccess.ReadWrite)] - public int Damage - { - get => _damage; - set => _damage = value; - } + public float Range { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public int Damage { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public DamageType DamageType { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public bool ClickAttackEffect { get; set; } public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); - serializer.DataField(ref _damage, "damage", 5); - serializer.DataField(ref _range, "range", 1); - serializer.DataField(ref _arcWidth, "arcwidth", 90); - serializer.DataField(ref _arc, "arc", "default"); - serializer.DataField(ref _hitSound, "hitSound", "/Audio/Weapons/genhit1.ogg"); - serializer.DataField(ref _cooldownTime, "cooldownTime", 1f); + serializer.DataField(this, x => x.Damage, "damage", 5); + serializer.DataField(this, x => x.Range, "range", 1); + serializer.DataField(this, x => x.ArcWidth, "arcwidth", 90); + serializer.DataField(this, x => x.Arc, "arc", "default"); + serializer.DataField(this, x => x.ClickArc, "clickArc", "punch"); + serializer.DataField(this, x => x._hitSound, "hitSound", "/Audio/Weapons/genhit1.ogg"); + serializer.DataField(this, x => x._missSound, "hitSound", "/Audio/Weapons/punchmiss.ogg"); + serializer.DataField(this, x => x.ArcCooldownTime, "arcCooldownTime", 1f); + serializer.DataField(this, x => x.CooldownTime, "cooldownTime", 1f); + serializer.DataField(this, x => x.DamageType, "damageType", DamageType.Blunt); + serializer.DataField(this, x => x.ClickAttackEffect, "clickAttackEffect", true); } protected virtual bool OnHitEntities(IReadOnlyList entities, AttackEventArgs eventArgs) @@ -84,13 +81,15 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee return true; } - void IAttack.Attack(AttackEventArgs eventArgs) + bool IAttack.WideAttack(AttackEventArgs eventArgs) { + if (!eventArgs.WideAttack) return true; + var curTime = _gameTiming.CurTime; - var span = curTime - _lastAttackTime; - if(span.TotalSeconds < _cooldownTime) { - return; - } + + if(curTime < _cooldownEnd) + return true; + var location = eventArgs.User.Transform.GridPosition; var angle = new Angle(eventArgs.ClickLocation.ToMapPos(_mapManager) - location.ToMapPos(_mapManager)); @@ -104,7 +103,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee } else { - audioSystem.PlayFromEntity("/Audio/Weapons/punchmiss.ogg", eventArgs.User); + audioSystem.PlayFromEntity(_missSound, eventArgs.User); } var hitEntities = new List(); @@ -115,26 +114,82 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee if (entity.TryGetComponent(out IDamageableComponent damageComponent)) { - damageComponent.ChangeDamage(DamageType.Blunt, Damage, false, Owner); + damageComponent.ChangeDamage(DamageType, Damage, false, Owner); hitEntities.Add(entity); } } - if(!OnHitEntities(hitEntities, eventArgs)) return; + if(!OnHitEntities(hitEntities, eventArgs)) return false; if (Arc != null) { - var sys = _entitySystemManager.GetEntitySystem(); - sys.SendAnimation(Arc, angle, eventArgs.User, hitEntities); + var sys = EntitySystem.Get(); + sys.SendAnimation(Arc, angle, eventArgs.User, Owner, hitEntities); } - _lastAttackTime = _gameTiming.CurTime; + _lastAttackTime = curTime; + _cooldownEnd = _lastAttackTime + TimeSpan.FromSeconds(ArcCooldownTime); if (Owner.TryGetComponent(out ItemCooldownComponent cooldown)) { cooldown.CooldownStart = _lastAttackTime; - cooldown.CooldownEnd = _lastAttackTime + TimeSpan.FromSeconds(_cooldownTime); + cooldown.CooldownEnd = _cooldownEnd; } + + return true; + } + + bool IAttack.ClickAttack(AttackEventArgs eventArgs) + { + if (eventArgs.WideAttack) return false; + + var curTime = _gameTiming.CurTime; + + if(curTime < _cooldownEnd || !eventArgs.Target.IsValid()) + return true; + + var target = eventArgs.TargetEntity; + + var location = eventArgs.User.Transform.GridPosition; + var angle = new Angle(eventArgs.ClickLocation.ToMapPos(_mapManager) - location.ToMapPos(_mapManager)); + + var audioSystem = EntitySystem.Get(); + if (target != null) + { + audioSystem.PlayFromEntity( _hitSound, target); + } + else + { + audioSystem.PlayFromEntity(_missSound, eventArgs.User); + return false; + } + + if (target.TryGetComponent(out IDamageableComponent damageComponent)) + { + damageComponent.ChangeDamage(DamageType, Damage, false, Owner); + } + + var targets = new[] {target}; + + if (!OnHitEntities(targets, eventArgs)) + return false; + + if (ClickArc != null) + { + var sys = EntitySystem.Get(); + sys.SendAnimation(ClickArc, angle, eventArgs.User, Owner, targets, ClickAttackEffect); + } + + _lastAttackTime = curTime; + _cooldownEnd = _lastAttackTime + TimeSpan.FromSeconds(CooldownTime); + + if (Owner.TryGetComponent(out ItemCooldownComponent cooldown)) + { + cooldown.CooldownStart = _lastAttackTime; + cooldown.CooldownEnd = _cooldownEnd; + } + + return true; } private HashSet ArcRayCast(Vector2 position, Angle angle, IEntity ignore) @@ -150,7 +205,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee for (var i = 0; i < increments; i++) { var castAngle = new Angle(baseAngle + increment * i); - var res = _physicsManager.IntersectRay(mapId, new CollisionRay(position, castAngle.ToVec(), 23), _range, ignore).FirstOrDefault(); + var res = _physicsManager.IntersectRay(mapId, new CollisionRay(position, castAngle.ToVec(), 23), Range, ignore).FirstOrDefault(); if (res.HitEntity != null) { resSet.Add(res.HitEntity); diff --git a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs index ddab046408..2fd492635f 100644 --- a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs @@ -176,7 +176,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click if (userEntity.TryGetComponent(out CombatModeComponent combatMode) && combatMode.IsInCombatMode) { - DoAttack(userEntity, coords); + DoAttack(userEntity, coords, true); } return true; @@ -198,7 +198,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click if (entity.TryGetComponent(out CombatModeComponent combatMode) && combatMode.IsInCombatMode) { - DoAttack(entity, coords); + DoAttack(entity, coords, false); } else { @@ -229,7 +229,10 @@ namespace Content.Server.GameObjects.EntitySystems.Click return true; } - UserInteraction(userEntity, coords, uid); + if(userEntity.TryGetComponent(out CombatModeComponent combat) && combat.IsInCombatMode) + DoAttack(userEntity, coords, false, uid); + else + UserInteraction(userEntity, coords, uid); return true; } @@ -790,7 +793,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click } } - private void DoAttack(IEntity player, GridCoordinates coordinates) + private void DoAttack(IEntity player, GridCoordinates coordinates, bool wideAttack, EntityUid target = default) { // Verify player is on the same map as the entity he clicked on if (_mapManager.GetGrid(coordinates.GridID).ParentMapId != player.Transform.MapID) @@ -800,12 +803,13 @@ namespace Content.Server.GameObjects.EntitySystems.Click return; } - if (!ActionBlockerSystem.CanAttack(player)) + if (!ActionBlockerSystem.CanAttack(player) || + (!wideAttack && !InRangeUnobstructed(player.Transform.MapPosition, coordinates.ToMap(_mapManager), ignoreInsideBlocker:true))) { return; } - var eventArgs = new AttackEventArgs(player, coordinates); + var eventArgs = new AttackEventArgs(player, coordinates, wideAttack, target); // Verify player has a hand, and find what object he is currently holding in his active hand if (player.TryGetComponent(out var hands)) @@ -814,22 +818,20 @@ namespace Content.Server.GameObjects.EntitySystems.Click if (item != null) { - var attacked = false; foreach (var attackComponent in item.GetAllComponents()) { - attackComponent.Attack(eventArgs); - attacked = true; - } - if (attacked) - { - return; + if(wideAttack ? attackComponent.WideAttack(eventArgs) : attackComponent.ClickAttack(eventArgs)) + return; } } } foreach (var attackComponent in player.GetAllComponents()) { - attackComponent.Attack(eventArgs); + if (wideAttack) + attackComponent.WideAttack(eventArgs); + else + attackComponent.ClickAttack(eventArgs); } } } diff --git a/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs b/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs index 82ba72537a..0613f1ec58 100644 --- a/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs @@ -9,10 +9,10 @@ namespace Content.Server.GameObjects.EntitySystems { public sealed class MeleeWeaponSystem : EntitySystem { - public void SendAnimation(string arc, Angle angle, IEntity attacker, IEnumerable hits) + public void SendAnimation(string arc, Angle angle, IEntity attacker, IEntity source, IEnumerable hits, bool textureEffect = false) { - RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayMeleeWeaponAnimationMessage(arc, angle, attacker.Uid, - hits.Select(e => e.Uid).ToList())); + RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayMeleeWeaponAnimationMessage(arc, angle, attacker.Uid, source.Uid, + hits.Select(e => e.Uid).ToList(), textureEffect)); } } } diff --git a/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs b/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs index 989defae06..2f1bd231fa 100644 --- a/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs +++ b/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs @@ -11,18 +11,22 @@ namespace Content.Shared.GameObjects.EntitySystemMessages [Serializable, NetSerializable] public sealed class PlayMeleeWeaponAnimationMessage : EntitySystemMessage { - public PlayMeleeWeaponAnimationMessage(string arcPrototype, Angle angle, EntityUid attacker, List hits) + public PlayMeleeWeaponAnimationMessage(string arcPrototype, Angle angle, EntityUid attacker, EntityUid source, List hits, bool textureEffect = false) { ArcPrototype = arcPrototype; Angle = angle; Attacker = attacker; + Source = source; Hits = hits; + TextureEffect = textureEffect; } public string ArcPrototype { get; } public Angle Angle { get; } public EntityUid Attacker { get; } + public EntityUid Source { get; } public List Hits { get; } + public bool TextureEffect { get; } } } } diff --git a/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAttack.cs b/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAttack.cs index cf2950a519..0a1a310405 100644 --- a/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAttack.cs +++ b/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAttack.cs @@ -1,5 +1,9 @@ -using System; +#nullable enable +using System; +using Content.Shared.GameObjects.Components.Research; +using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Map; namespace Content.Shared.Interfaces.GameObjects.Components @@ -9,18 +13,29 @@ namespace Content.Shared.Interfaces.GameObjects.Components /// public interface IAttack { - void Attack(AttackEventArgs eventArgs); + // Redirects to ClickAttack by default. + bool WideAttack(AttackEventArgs eventArgs) => ClickAttack(eventArgs); + bool ClickAttack(AttackEventArgs eventArgs); } public class AttackEventArgs : EventArgs { - public AttackEventArgs(IEntity user, GridCoordinates clickLocation) + public AttackEventArgs(IEntity user, GridCoordinates clickLocation, bool wideAttack, EntityUid target = default) { User = user; ClickLocation = clickLocation; + WideAttack = wideAttack; + Target = target; + + IEntity? targetEntity = null; + IoCManager.Resolve()?.TryGetEntity(Target, out targetEntity); + TargetEntity = targetEntity; } public IEntity User { get; } public GridCoordinates ClickLocation { get; } + public bool WideAttack { get; } + public EntityUid Target { get; } + public IEntity? TargetEntity { get; } } }