diff --git a/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs b/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs index 1a4b1212ee..daefa7c8c9 100644 --- a/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs +++ b/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs @@ -21,7 +21,8 @@ namespace Content.Client.GameObjects.EntitySystems public override void Initialize() { - SubscribeNetworkEvent(PlayWeaponArc); + SubscribeNetworkEvent(PlayWeaponArc); + SubscribeNetworkEvent(PlayWeapon); } public override void FrameUpdate(float frameTime) @@ -34,7 +35,7 @@ namespace Content.Client.GameObjects.EntitySystems } } - private void PlayWeaponArc(PlayMeleeWeaponAnimationMessage msg) + private void PlayWeaponArc(PlayMeleeWeaponArcAnimationMessage msg) { if (!_prototypeManager.TryIndex(msg.ArcPrototype, out MeleeWeaponAnimationPrototype weaponArc)) { @@ -80,5 +81,32 @@ namespace Content.Client.GameObjects.EntitySystems }); } } + + private void PlayWeapon(PlayMeleeWeaponAnimationMessage msg) + { + var attacker = EntityManager.GetEntity(msg.Attacker); + + var lunge = attacker.EnsureComponent(); + lunge.SetData(msg.Angle); + + if (!EntityManager.TryGetEntity(msg.Hit, out var hitEntity) + || !hitEntity.TryGetComponent(out ISpriteComponent sprite)) + { + return; + } + + var originalColor = sprite.Color; + var newColor = Color.Red * originalColor; + sprite.Color = newColor; + + Timer.Spawn(100, () => + { + // Only reset back to the original color if something else didn't change the color in the mean time. + if (sprite.Color == newColor) + { + sprite.Color = originalColor; + } + }); + } } } 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 ce4a5bf376..7f4f536b0f 100644 --- a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs @@ -29,53 +29,41 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee 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 Arc { get; set; } [ViewVariables(VVAccess.ReadWrite)] - public float ArcWidth - { - get => _arcWidth; - set => _arcWidth = value; - } + public float ArcWidth { get; set; } [ViewVariables(VVAccess.ReadWrite)] - public float Range - { - get => _range; - set => _range = value; - } + public float Range { get; set; } [ViewVariables(VVAccess.ReadWrite)] - public int Damage - { - get => _damage; - set => _damage = value; - } + public int Damage { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public DamageType DamageType { 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._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); } protected virtual bool OnHitEntities(IReadOnlyList entities, AttackEventArgs eventArgs) @@ -83,13 +71,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 = IoCManager.Resolve().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)); @@ -103,7 +93,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(); @@ -114,12 +104,12 @@ 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 true; if (Arc != null) { @@ -127,13 +117,64 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee sys.SendAnimation(Arc, angle, eventArgs.User, hitEntities); } - _lastAttackTime = IoCManager.Resolve().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 = IoCManager.Resolve().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 true; + } + + if (target.TryGetComponent(out IDamageableComponent damageComponent)) + { + damageComponent.ChangeDamage(DamageType, Damage, false, Owner); + } + + if (!OnHitEntities(new[] {target}, eventArgs)) + return true; + + var sys = _entitySystemManager.GetEntitySystem(); + sys.SendAnimation(angle, eventArgs.User, target); + + _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) @@ -149,7 +190,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..986a278535 100644 --- a/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs @@ -11,8 +11,13 @@ namespace Content.Server.GameObjects.EntitySystems { public void SendAnimation(string arc, Angle angle, IEntity attacker, IEnumerable hits) { - RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayMeleeWeaponAnimationMessage(arc, angle, attacker.Uid, + RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayMeleeWeaponArcAnimationMessage(arc, angle, attacker.Uid, hits.Select(e => e.Uid).ToList())); } + + public void SendAnimation(Angle angle, IEntity attacker, IEntity hit) + { + RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayMeleeWeaponAnimationMessage(angle, attacker.Uid, hit.Uid)); + } } } diff --git a/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs b/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs index 989defae06..849ba09936 100644 --- a/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs +++ b/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs @@ -9,9 +9,9 @@ namespace Content.Shared.GameObjects.EntitySystemMessages public static class MeleeWeaponSystemMessages { [Serializable, NetSerializable] - public sealed class PlayMeleeWeaponAnimationMessage : EntitySystemMessage + public sealed class PlayMeleeWeaponArcAnimationMessage : EntitySystemMessage { - public PlayMeleeWeaponAnimationMessage(string arcPrototype, Angle angle, EntityUid attacker, List hits) + public PlayMeleeWeaponArcAnimationMessage(string arcPrototype, Angle angle, EntityUid attacker, List hits) { ArcPrototype = arcPrototype; Angle = angle; @@ -24,5 +24,19 @@ namespace Content.Shared.GameObjects.EntitySystemMessages public EntityUid Attacker { get; } public List Hits { get; } } + + [Serializable, NetSerializable] + public sealed class PlayMeleeWeaponAnimationMessage : EntitySystemMessage + { + public PlayMeleeWeaponAnimationMessage(Angle angle, EntityUid attacker, EntityUid hit) + { + Attacker = attacker; + Hit = hit; + } + + public Angle Angle { get; } + public EntityUid Attacker { get; } + public EntityUid Hit { get; } + } } } diff --git a/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAttack.cs b/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAttack.cs index cf2950a519..218b403600 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,25 @@ 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; } public IEntity User { get; } public GridCoordinates ClickLocation { get; } + public bool WideAttack { get; } + public EntityUid Target { get; } + public IEntity? TargetEntity => IoCManager.Resolve()?.GetEntity(Target) ?? null; } }