diff --git a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs index c889a8687b..8eb02a1afb 100644 --- a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs +++ b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs @@ -20,25 +20,17 @@ namespace Content.IntegrationTests.Tests.Interaction.Click [Reflect(false)] private class TestAttackEntitySystem : EntitySystem { - public EntityEventHandler AttackEvent; + public EntityEventHandler ClickAttackEvent; public EntityEventHandler InteractUsingEvent; public EntityEventHandler InteractHandEvent; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((e) => AttackEvent?.Invoke(e)); + SubscribeLocalEvent((e) => ClickAttackEvent?.Invoke(e)); SubscribeLocalEvent((e) => InteractUsingEvent?.Invoke(e)); SubscribeLocalEvent((e) => InteractHandEvent?.Invoke(e)); } - - public override void Shutdown() - { - base.Shutdown(); - UnsubscribeLocalEvent(); - UnsubscribeLocalEvent(); - UnsubscribeLocalEvent(); - } } [Test] @@ -88,7 +80,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click Assert.That(entitySystemManager.TryGetEntitySystem(out var interactionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem(out var testAttackEntitySystem)); - testAttackEntitySystem.AttackEvent = (ev) => + testAttackEntitySystem.ClickAttackEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(containerEntity.Uid)); attack = true; diff --git a/Content.Server/Actions/DisarmAction.cs b/Content.Server/Actions/DisarmAction.cs index 2d505010d9..814192befd 100644 --- a/Content.Server/Actions/DisarmAction.cs +++ b/Content.Server/Actions/DisarmAction.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems.Click; +using Content.Server.GameObjects.EntitySystems.Weapon.Melee; using Content.Server.Interfaces.GameObjects; using Content.Server.Utility; using Content.Shared.Actions; diff --git a/Content.Server/GameObjects/Components/Chemistry/HyposprayComponent.cs b/Content.Server/GameObjects/Components/Chemistry/HyposprayComponent.cs index 26adceb15a..88e3b6723f 100644 --- a/Content.Server/GameObjects/Components/Chemistry/HyposprayComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/HyposprayComponent.cs @@ -1,29 +1,23 @@ -using System.Threading.Tasks; using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs.State; -using Content.Server.GameObjects.EntitySystems; +using Content.Server.GameObjects.EntitySystems.Weapon.Melee; using Content.Shared.Chemistry; using Content.Shared.GameObjects.Components.Chemistry; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces; -using Content.Shared.Interfaces.GameObjects.Components; -using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.GameObjects; using Robust.Shared.Localization; using Robust.Shared.Maths; using Robust.Shared.Player; using Robust.Shared.Players; -using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; -#nullable enable - namespace Content.Server.GameObjects.Components.Chemistry { [RegisterComponent] - public sealed class HyposprayComponent : SharedHyposprayComponent, IAttack, ISolutionChange, IAfterInteract + public sealed class HyposprayComponent : SharedHyposprayComponent, ISolutionChange { [DataField("ClumsyFailChance")] [ViewVariables(VVAccess.ReadWrite)] @@ -42,23 +36,7 @@ namespace Content.Server.GameObjects.Components.Chemistry Dirty(); } - bool IAttack.ClickAttack(AttackEvent eventArgs) - { - var target = eventArgs.TargetEntity; - var user = eventArgs.User; - - return TryDoInject(target, user); - } - - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) - { - if (!eventArgs.CanReach) - return false; - - return TryDoInject(eventArgs.Target, eventArgs.User); - } - - private bool TryDoInject(IEntity? target, IEntity user) + public bool TryDoInject(IEntity? target, IEntity user) { if (target == null || !EligibleEntity(target)) return false; diff --git a/Content.Server/GameObjects/Components/Power/PowerCellSlotComponent.cs b/Content.Server/GameObjects/Components/Power/PowerCellSlotComponent.cs index 23383521c1..5e0a728b41 100644 --- a/Content.Server/GameObjects/Components/Power/PowerCellSlotComponent.cs +++ b/Content.Server/GameObjects/Components/Power/PowerCellSlotComponent.cs @@ -150,7 +150,8 @@ namespace Content.Server.GameObjects.Components.Power { SoundSystem.Play(Filter.Pvs(Owner), CellRemoveSound, Owner, AudioHelpers.WithVariation(0.125f)); } - SendMessage(new PowerCellChangedMessage(true)); + + Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new PowerCellChangedEvent(true), false); return cell; } @@ -172,7 +173,8 @@ namespace Content.Server.GameObjects.Components.Power { SoundSystem.Play(Filter.Pvs(Owner), CellInsertSound, Owner, AudioHelpers.WithVariation(0.125f)); } - SendMessage(new PowerCellChangedMessage(false)); + + Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new PowerCellChangedEvent(false), false); return true; } @@ -238,14 +240,14 @@ namespace Content.Server.GameObjects.Components.Power } } - public class PowerCellChangedMessage : ComponentMessage + public class PowerCellChangedEvent : EntityEventArgs { /// /// If true, the cell was ejected; if false, it was inserted. /// public bool Ejected { get; } - public PowerCellChangedMessage(bool ejected) + public PowerCellChangedEvent(bool ejected) { Ejected = ejected; } diff --git a/Content.Server/GameObjects/Components/Weapon/Melee/FlashComponent.cs b/Content.Server/GameObjects/Components/Weapon/Melee/FlashComponent.cs index 5295018519..573ddef367 100644 --- a/Content.Server/GameObjects/Components/Weapon/Melee/FlashComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Melee/FlashComponent.cs @@ -1,162 +1,36 @@ -#nullable enable -using System.Collections.Generic; -using Content.Server.GameObjects.Components.Mobs; -using Content.Shared.GameObjects.EntitySystems; -using Content.Shared.Interfaces; -using Content.Shared.Interfaces.GameObjects.Components; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Player; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Weapon.Melee { [RegisterComponent] - public class FlashComponent : MeleeWeaponComponent, IUse, IExamine + public class FlashComponent : Component { public override string Name => "Flash"; - public FlashComponent() { Range = 7f; } + [DataField("duration")] + [ViewVariables(VVAccess.ReadWrite)] + public int FlashDuration { get; set; } = 5000; - [DataField("duration")] [ViewVariables(VVAccess.ReadWrite)] private int _flashDuration = 5000; - [DataField("uses")] [ViewVariables(VVAccess.ReadWrite)] private int _uses = 5; - [ViewVariables(VVAccess.ReadWrite)] private float _range => Range; - [ViewVariables(VVAccess.ReadWrite)] private int _aoeFlashDuration => _internalAoeFlashDuration ?? _flashDuration / 3; - [DataField("aoeFlashDuration")] private int? _internalAoeFlashDuration; - [DataField("slowTo")] [ViewVariables(VVAccess.ReadWrite)] private float _slowTo = 0.75f; - private bool _flashing; + [DataField("uses")] + [ViewVariables(VVAccess.ReadWrite)] + public int Uses { get; set; } = 5; - private int Uses - { - get => _uses; - set - { - _uses = value; - Dirty(); - } - } + [DataField("range")] + [ViewVariables(VVAccess.ReadWrite)] + public float Range { get; set; } = 7f; - private bool HasUses => _uses > 0; + [ViewVariables(VVAccess.ReadWrite)] + [DataField("aoeFlashDuration")] + public int AoeFlashDuration { get; set; } = 2000; - protected override bool OnHitEntities(IReadOnlyList entities, AttackEvent eventArgs) - { - if (entities.Count == 0) - { - return false; - } + [DataField("slowTo")] + [ViewVariables(VVAccess.ReadWrite)] + public float SlowTo { get; set; } = 0.5f; - if (!Use(eventArgs.User)) - { - return false; - } + public bool Flashing; - foreach (var entity in entities) - { - Flash(entity, eventArgs.User); - } - - return true; - } - - bool IUse.UseEntity(UseEntityEventArgs eventArgs) - { - if (!Use(eventArgs.User)) - { - return false; - } - - foreach (var entity in IoCManager.Resolve().GetEntitiesInRange(Owner.Transform.Coordinates, _range)) - { - Flash(entity, eventArgs.User, _aoeFlashDuration); - } - - return true; - } - - private bool Use(IEntity user) - { - if (HasUses) - { - var sprite = Owner.GetComponent(); - if (--Uses == 0) - { - sprite.LayerSetState(0, "burnt"); - Owner.PopupMessage(user, Loc.GetString("flash-component-becomes-empty")); - } - else if (!_flashing) - { - int animLayer = sprite.AddLayerWithState("flashing"); - _flashing = true; - - Owner.SpawnTimer(400, () => - { - sprite.RemoveLayer(animLayer); - _flashing = false; - }); - } - - SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Weapons/flash.ogg", Owner.Transform.Coordinates, - AudioParams.Default); - - return true; - } - - return false; - } - - private void Flash(IEntity entity, IEntity user) - { - Flash(entity, user, _flashDuration); - } - - // TODO: Check if target can be flashed (e.g. things like sunglasses would block a flash) - // TODO: Merge with the code in FlashableComponent - private void Flash(IEntity entity, IEntity user, int flashDuration) - { - if (entity.TryGetComponent(out var flashable)) - { - flashable.Flash(flashDuration / 1000d); - } - - if (entity.TryGetComponent(out var stunnableComponent)) - { - stunnableComponent.Slowdown(flashDuration / 1000f, _slowTo, _slowTo); - } - - if (entity != user) - { - user.PopupMessage(entity, - Loc.GetString( - "flash-component-user-blinds-you", - ("user", user) - ) - ); - } - } - - public void Examine(FormattedMessage message, bool inDetailsRange) - { - if (!HasUses) - { - message.AddText(Loc.GetString("flash-component-examine-empty")); - return; - } - - if (inDetailsRange) - { - message.AddMarkup( - Loc.GetString( - "flash-component-examine-detail-count", - ("count", Uses), - ("markupCountColor", "green") - ) - ); - } - } + public bool HasUses => Uses > 0; } } diff --git a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeChemicalInjectorComponent.cs b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeChemicalInjectorComponent.cs index d11925e4e5..3e806a496f 100644 --- a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeChemicalInjectorComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeChemicalInjectorComponent.cs @@ -1,14 +1,7 @@ -#nullable enable -using Content.Server.GameObjects.Components.Body.Circulatory; -using Content.Server.GameObjects.Components.Chemistry; -using Content.Shared.Chemistry; +using Content.Shared.Chemistry; using Robust.Shared.GameObjects; -using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; using System; -using System.Collections.Generic; -using System.Linq; -using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.GameObjects.Components.Weapon.Melee @@ -24,48 +17,8 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee [ViewVariables(VVAccess.ReadWrite)] public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); } + [DataField("transferEfficiency")] private float _transferEfficiency = 1f; - - public override void HandleMessage(ComponentMessage message, IComponent? component) - { - base.HandleMessage(message, component); - switch (message) - { - case MeleeHitMessage meleeHit: - InjectEntities(meleeHit.HitEntities); - break; - } - } - - private void InjectEntities(List hitEntities) - { - if (!Owner.TryGetComponent(out var solutionContainer)) - return; - - var hitBloodstreams = new List(); - foreach (var entity in hitEntities) - { - if (entity.Deleted) - continue; - - if (entity.TryGetComponent(out var bloodstream)) - hitBloodstreams.Add(bloodstream); - } - - if (!hitBloodstreams.Any()) - return; - - var removedSolution = solutionContainer.Solution.SplitSolution(TransferAmount * hitBloodstreams.Count); - var removedVol = removedSolution.TotalVolume; - var solutionToInject = removedSolution.SplitSolution(removedVol * TransferEfficiency); - var volPerBloodstream = solutionToInject.TotalVolume * (1 / hitBloodstreams.Count); - - foreach (var bloodstream in hitBloodstreams) - { - var individualInjection = solutionToInject.SplitSolution(volPerBloodstream); - bloodstream.TryTransferSolution(individualInjection); - } - } } } diff --git a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs index 68a396c2a8..6e82a94d2d 100644 --- a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs @@ -1,46 +1,31 @@ using System; -using System.Collections.Generic; -using System.Linq; -using Content.Server.GameObjects.EntitySystems; using Content.Shared.Damage; -using Content.Shared.GameObjects.Components.Damage; -using Content.Shared.GameObjects.Components.Items; -using Content.Shared.Interfaces.GameObjects.Components; -using Content.Shared.Physics; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Maths; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Broadphase; -using Robust.Shared.Player; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Timing; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Weapon.Melee { [RegisterComponent] - public class MeleeWeaponComponent : Component, IAttack, IHandSelected + public class MeleeWeaponComponent : Component { - [Dependency] private readonly IGameTiming _gameTiming = default!; - public override string Name => "MeleeWeapon"; - private TimeSpan _lastAttackTime; - private TimeSpan _cooldownEnd; + [ViewVariables(VVAccess.ReadWrite)] [DataField("hitSound")] - private string _hitSound = "/Audio/Weapons/genhit1.ogg"; + public string HitSound { get; set; } = "/Audio/Weapons/genhit1.ogg"; + [ViewVariables(VVAccess.ReadWrite)] [DataField("missSound")] - private string _missSound = "/Audio/Weapons/punchmiss.ogg"; + public string MissSound { get; set; } = "/Audio/Weapons/punchmiss.ogg"; + [ViewVariables] [DataField("arcCooldownTime")] - public float ArcCooldownTime { get; private set; } = 1f; + public float ArcCooldownTime { get; } = 1f; + [ViewVariables] [DataField("cooldownTime")] - public float CooldownTime { get; private set; } = 1f; + public float CooldownTime { get; } = 1f; [ViewVariables(VVAccess.ReadWrite)] [DataField("clickArc")] @@ -50,7 +35,9 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee [DataField("arc")] public string Arc { get; set; } = "default"; - [ViewVariables(VVAccess.ReadWrite)] [DataField("arcwidth")] public float ArcWidth { get; set; } = 90; + [ViewVariables(VVAccess.ReadWrite)] + [DataField("arcwidth")] + public float ArcWidth { get; set; } = 90; [ViewVariables(VVAccess.ReadWrite)] [DataField("range")] @@ -64,185 +51,11 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee [DataField("damageType")] public DamageType DamageType { get; set; } = DamageType.Blunt; - [ViewVariables(VVAccess.ReadWrite)] [DataField("clickAttackEffect")] public bool ClickAttackEffect { get; set; } = true; + [ViewVariables(VVAccess.ReadWrite)] + [DataField("clickAttackEffect")] + public bool ClickAttackEffect { get; set; } = true; - protected virtual bool OnHitEntities(IReadOnlyList entities, AttackEvent eventArgs) - { - return true; - } - - bool IAttack.WideAttack(AttackEvent eventArgs) - { - if (!eventArgs.WideAttack) return true; - - var curTime = _gameTiming.CurTime; - - if (curTime < _cooldownEnd) - return true; - - var location = eventArgs.User.Transform.Coordinates; - var diff = eventArgs.ClickLocation.ToMapPos(Owner.EntityManager) - location.ToMapPos(Owner.EntityManager); - var angle = Angle.FromWorldVec(diff); - - // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes. - var entities = ArcRayCast(eventArgs.User.Transform.WorldPosition, angle, eventArgs.User); - - if (entities.Count != 0) - { - SoundSystem.Play(Filter.Pvs(Owner), _hitSound, entities.First().Transform.Coordinates); - } - else - { - SoundSystem.Play(Filter.Pvs(Owner), _missSound, eventArgs.User.Transform.Coordinates); - } - - var hitEntities = new List(); - foreach (var entity in entities) - { - if (!entity.Transform.IsMapTransform || entity == eventArgs.User) - continue; - - if (entity.TryGetComponent(out IDamageableComponent? damageComponent)) - { - damageComponent.ChangeDamage(DamageType, Damage, false, Owner); - hitEntities.Add(entity); - } - } - SendMessage(new MeleeHitMessage(hitEntities)); - - if (!OnHitEntities(hitEntities, eventArgs)) return false; - - if (Arc != null) - { - var sys = EntitySystem.Get(); - sys.SendAnimation(Arc, angle, eventArgs.User, Owner, hitEntities); - } - - _lastAttackTime = curTime; - _cooldownEnd = _lastAttackTime + TimeSpan.FromSeconds(ArcCooldownTime); - - RefreshItemCooldown(); - - return true; - } - - bool IAttack.ClickAttack(AttackEvent 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.Coordinates; - var diff = eventArgs.ClickLocation.ToMapPos(Owner.EntityManager) - location.ToMapPos(Owner.EntityManager); - var angle = Angle.FromWorldVec(diff); - - if (target != null) - { - SoundSystem.Play(Filter.Pvs(Owner), _hitSound, target); - } - else - { - SoundSystem.Play(Filter.Pvs(Owner), _missSound, eventArgs.User); - return false; - } - - if (target.TryGetComponent(out IDamageableComponent? damageComponent)) - { - damageComponent.ChangeDamage(DamageType, Damage, false, Owner); - } - SendMessage(new MeleeHitMessage(new List { target })); - - 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, false); - } - - _lastAttackTime = curTime; - _cooldownEnd = _lastAttackTime + TimeSpan.FromSeconds(CooldownTime); - - RefreshItemCooldown(); - - return true; - } - - private HashSet ArcRayCast(Vector2 position, Angle angle, IEntity ignore) - { - var widthRad = Angle.FromDegrees(ArcWidth); - var increments = 1 + 35 * (int) Math.Ceiling(widthRad / (2 * Math.PI)); - var increment = widthRad / increments; - var baseAngle = angle - widthRad / 2; - - var resSet = new HashSet(); - - var mapId = Owner.Transform.MapID; - for (var i = 0; i < increments; i++) - { - var castAngle = new Angle(baseAngle + increment * i); - var res = EntitySystem.Get().IntersectRay(mapId, - new CollisionRay(position, castAngle.ToWorldVec(), - (int) (CollisionGroup.Impassable | CollisionGroup.MobImpassable)), Range, ignore).ToList(); - - if (res.Count != 0) - { - resSet.Add(res[0].HitEntity); - } - } - - return resSet; - } - - void IHandSelected.HandSelected(HandSelectedEventArgs eventArgs) - { - var curTime = _gameTiming.CurTime; - var cool = TimeSpan.FromSeconds(CooldownTime * 0.5f); - - if (curTime < _cooldownEnd) - { - if (_cooldownEnd - curTime < cool) - { - _lastAttackTime = curTime; - _cooldownEnd += cool; - } - else - return; - } - else - { - _lastAttackTime = curTime; - _cooldownEnd = curTime + cool; - } - - RefreshItemCooldown(); - } - - private void RefreshItemCooldown() - { - if (Owner.TryGetComponent(out ItemCooldownComponent? cooldown)) - { - cooldown.CooldownStart = _lastAttackTime; - cooldown.CooldownEnd = _cooldownEnd; - } - } - } - - public class MeleeHitMessage : ComponentMessage - { - public readonly List HitEntities; - - public MeleeHitMessage(List hitEntities) - { - HitEntities = hitEntities; - } + public TimeSpan LastAttackTime; + public TimeSpan CooldownEnd; } } diff --git a/Content.Server/GameObjects/Components/Weapon/Melee/StunbatonComponent.cs b/Content.Server/GameObjects/Components/Weapon/Melee/StunbatonComponent.cs index aea3663c63..46a76d7d7f 100644 --- a/Content.Server/GameObjects/Components/Weapon/Melee/StunbatonComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Melee/StunbatonComponent.cs @@ -1,219 +1,35 @@ #nullable enable -using System.Collections.Generic; -using System.Threading.Tasks; -using Content.Server.GameObjects.Components.Items.Storage; -using Content.Server.GameObjects.Components.Mobs; -using Content.Server.GameObjects.Components.Power; -using Content.Shared.Audio; -using Content.Shared.GameObjects.EntitySystems; -using Content.Shared.GameObjects.EntitySystems.ActionBlocker; -using Content.Shared.Interfaces; -using Content.Shared.Interfaces.GameObjects.Components; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Weapon.Melee { [RegisterComponent] - public class StunbatonComponent : MeleeWeaponComponent, IUse, IExamine, IInteractUsing, IThrowCollide + public class StunbatonComponent : Component { - [Dependency] private readonly IRobustRandom _robustRandom = default!; - public override string Name => "Stunbaton"; - private bool _activated = false; - - [ViewVariables] private PowerCellSlotComponent _cellSlot = default!; - private PowerCellComponent? Cell => _cellSlot.Cell; + public bool Activated = false; [ViewVariables(VVAccess.ReadWrite)] [DataField("paralyzeChanceNoSlowdown")] - private float _paralyzeChanceNoSlowdown = 0.35f; + public float ParalyzeChanceNoSlowdown { get; set; } = 0.35f; [ViewVariables(VVAccess.ReadWrite)] [DataField("paralyzeChanceWithSlowdown")] - private float _paralyzeChanceWithSlowdown = 0.85f; + public float ParalyzeChanceWithSlowdown { get; set; } = 0.85f; [ViewVariables(VVAccess.ReadWrite)] [DataField("paralyzeTime")] - private float _paralyzeTime = 10f; + public float ParalyzeTime { get; set; } = 10f; [ViewVariables(VVAccess.ReadWrite)] [DataField("slowdownTime")] - private float _slowdownTime = 5f; + public float SlowdownTime { get; set; } = 5f; - [ViewVariables(VVAccess.ReadWrite)] public float EnergyPerUse { get; set; } = 50; - - [ViewVariables] - public bool Activated => _activated; - - public override void Initialize() - { - base.Initialize(); - _cellSlot = Owner.EnsureComponent(); - } - - public override void HandleMessage(ComponentMessage message, IComponent? component) - { - base.HandleMessage(message, component); - switch (message) - { - case PowerCellChangedMessage m: - if (component is PowerCellSlotComponent slotComponent && slotComponent == _cellSlot) - { - if (m.Ejected) - { - TurnOff(); - } - } - break; - } - } - - protected override bool OnHitEntities(IReadOnlyList entities, AttackEvent eventArgs) - { - if (!Activated || entities.Count == 0 || Cell == null) - return true; - - if (!Cell.TryUseCharge(EnergyPerUse)) - return true; - - SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Weapons/egloves.ogg", Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); - - foreach (var entity in entities) - { - if (!entity.TryGetComponent(out StunnableComponent? stunnable)) continue; - - if(!stunnable.SlowedDown) - { - if(_robustRandom.Prob(_paralyzeChanceNoSlowdown)) - stunnable.Paralyze(_paralyzeTime); - else - stunnable.Slowdown(_slowdownTime); - } - else - { - if(_robustRandom.Prob(_paralyzeChanceWithSlowdown)) - stunnable.Paralyze(_paralyzeTime); - else - stunnable.Slowdown(_slowdownTime); - } - } - - if (!(Cell.CurrentCharge < EnergyPerUse)) return true; - - SoundSystem.Play(Filter.Pvs(Owner), AudioHelpers.GetRandomFileFromSoundCollection("sparks"), Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); - TurnOff(); - - return true; - } - - private bool ToggleStatus(IEntity user) - { - if (!ActionBlockerSystem.CanUse(user)) return false; - if (Activated) - { - TurnOff(); - } - else - { - TurnOn(user); - } - - return true; - } - - private void TurnOff() - { - if (!_activated) - { - return; - } - - var sprite = Owner.GetComponent(); - var item = Owner.GetComponent(); - - SoundSystem.Play(Filter.Pvs(Owner), AudioHelpers.GetRandomFileFromSoundCollection("sparks"), Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); - - item.EquippedPrefix = "off"; - sprite.LayerSetState(0, "stunbaton_off"); - _activated = false; - } - - private void TurnOn(IEntity user) - { - if (_activated) - { - return; - } - - var sprite = Owner.GetComponent(); - var item = Owner.GetComponent(); - - var playerFilter = Filter.Pvs(Owner); - if (Cell == null) - { - SoundSystem.Play(playerFilter, "/Audio/Machines/button.ogg", Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); - - Owner.PopupMessage(user, Loc.GetString("Cell missing...")); - return; - } - - if (Cell.CurrentCharge < EnergyPerUse) - { - SoundSystem.Play(playerFilter, "/Audio/Machines/button.ogg", Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); - Owner.PopupMessage(user, Loc.GetString("Dead cell...")); - return; - } - - SoundSystem.Play(playerFilter, AudioHelpers.GetRandomFileFromSoundCollection("sparks"), Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); - - item.EquippedPrefix = "on"; - sprite.LayerSetState(0, "stunbaton_on"); - _activated = true; - } - - bool IUse.UseEntity(UseEntityEventArgs eventArgs) - { - ToggleStatus(eventArgs.User); - - return true; - } - - async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) - { - if (!ActionBlockerSystem.CanInteract(eventArgs.User)) return false; - if (!_cellSlot.InsertCell(eventArgs.Using)) return false; - Dirty(); - return true; - } - - public void Examine(FormattedMessage message, bool inDetailsRange) - { - if (Activated) - { - message.AddMarkup(Loc.GetString("The light is currently [color=darkgreen]on[/color].")); - } - } - - void IThrowCollide.DoHit(ThrowCollideEventArgs eventArgs) - { - if (!Activated || Cell == null || !Cell.TryUseCharge(EnergyPerUse) || !eventArgs.Target.TryGetComponent(out StunnableComponent? stunnable)) - return; - - SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Weapons/egloves.ogg", Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); - - stunnable.Paralyze(_paralyzeTime); - } + [ViewVariables(VVAccess.ReadWrite)] + [DataField("energyPerUse")] + public float EnergyPerUse { get; set; } = 50; } } diff --git a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs index 2c3ba58163..eec7f55549 100644 --- a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs @@ -817,7 +817,6 @@ namespace Content.Server.GameObjects.EntitySystems.Click return; } - // In a container where the target entity is not the container's owner if (player.TryGetContainer(out var playerContainer) && (!EntityManager.TryGetEntity(targetUid, out var target) || @@ -832,8 +831,6 @@ namespace Content.Server.GameObjects.EntitySystems.Click } } - var eventArgs = new AttackEvent(player, coordinates, wideAttack, targetUid); - // Verify player has a hand, and find what object he is currently holding in his active hand if (player.TryGetComponent(out var hands)) { @@ -841,35 +838,18 @@ namespace Content.Server.GameObjects.EntitySystems.Click if (item != null) { - RaiseLocalEvent(item.Uid, eventArgs, false); - foreach (var attackComponent in item.GetAllComponents()) - { - if (wideAttack ? attackComponent.WideAttack(eventArgs) : attackComponent.ClickAttack(eventArgs)) - return; - } + if(wideAttack) + RaiseLocalEvent(item.Uid, new WideAttackEvent(item, player, coordinates), false); + else + RaiseLocalEvent(item.Uid, new ClickAttackEvent(item, player, coordinates, targetUid), false); } else { // We pick up items if our hand is empty, even if we're in combat mode. - if (EntityManager.TryGetEntity(targetUid, out var targetEnt)) - { - if (targetEnt.HasComponent()) - { - Interaction(player, targetEnt); - return; - } - } + if (!EntityManager.TryGetEntity(targetUid, out var targetEnt) || !targetEnt.HasComponent()) return; + Interaction(player, targetEnt); } } - - RaiseLocalEvent(player.Uid, eventArgs); - foreach (var attackComponent in player.GetAllComponents()) - { - if (wideAttack) - attackComponent.WideAttack(eventArgs); - else - attackComponent.ClickAttack(eventArgs); - } } } } diff --git a/Content.Server/GameObjects/EntitySystems/HypospraySystem.cs b/Content.Server/GameObjects/EntitySystems/HypospraySystem.cs new file mode 100644 index 0000000000..340becbba9 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/HypospraySystem.cs @@ -0,0 +1,35 @@ +using Content.Server.GameObjects.Components.Chemistry; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Shared.GameObjects; + +namespace Content.Server.GameObjects.EntitySystems +{ + public class HypospraySystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnClickAttack); + } + + public void OnAfterInteract(EntityUid uid, HyposprayComponent comp, AfterInteractEvent args) + { + if (!args.CanReach) + return; + var target = args.Target; + var user = args.User; + + comp.TryDoInject(target, user); + } + + public void OnClickAttack(EntityUid uid, HyposprayComponent comp, ClickAttackEvent args) + { + var target = args.TargetEntity; + var user = args.User; + + comp.TryDoInject(target, user); + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/ItemCooldownSystem.cs b/Content.Server/GameObjects/EntitySystems/ItemCooldownSystem.cs new file mode 100644 index 0000000000..aeb1ec924e --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/ItemCooldownSystem.cs @@ -0,0 +1,34 @@ +using System; +using Content.Shared.GameObjects.Components.Items; +using Robust.Shared.GameObjects; + +namespace Content.Server.GameObjects.EntitySystems +{ + public class ItemCooldownSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnItemCooldownRefreshed); + } + + public void OnItemCooldownRefreshed(EntityUid uid, ItemCooldownComponent comp, RefreshItemCooldownEvent args) + { + comp.CooldownStart = args.LastAttackTime; + comp.CooldownEnd = args.CooldownEnd; + } + } + + public class RefreshItemCooldownEvent : EntityEventArgs + { + public TimeSpan LastAttackTime { get; } + public TimeSpan CooldownEnd { get; } + + public RefreshItemCooldownEvent(TimeSpan lastAttackTime, TimeSpan cooldownEnd) + { + LastAttackTime = lastAttackTime; + CooldownEnd = cooldownEnd; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs b/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs deleted file mode 100644 index ad7bdaeb79..0000000000 --- a/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Content.Shared.GameObjects.EntitySystemMessages; -using Robust.Shared.GameObjects; -using Robust.Shared.Maths; -using Robust.Shared.Player; - -namespace Content.Server.GameObjects.EntitySystems -{ - public sealed class MeleeWeaponSystem : EntitySystem - { - public void SendAnimation(string arc, Angle angle, IEntity attacker, IEntity source, IEnumerable hits, bool textureEffect = false, bool arcFollowAttacker = true) - { - RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayMeleeWeaponAnimationMessage(arc, angle, attacker.Uid, source.Uid, - hits.Select(e => e.Uid).ToList(), textureEffect, arcFollowAttacker), Filter.Pvs(source, 1f)); - } - - public void SendLunge(Angle angle, IEntity source) - { - RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayLungeAnimationMessage(angle, source.Uid), Filter.Pvs(source, 1f)); - } - } -} diff --git a/Content.Server/GameObjects/EntitySystems/Weapon/Melee/FlashSystem.cs b/Content.Server/GameObjects/EntitySystems/Weapon/Melee/FlashSystem.cs new file mode 100644 index 0000000000..ffb50dfc34 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/Weapon/Melee/FlashSystem.cs @@ -0,0 +1,152 @@ +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.Components.Weapon; +using Content.Server.GameObjects.Components.Weapon.Melee; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Player; + +namespace Content.Server.GameObjects.EntitySystems.Weapon.Melee +{ + public class FlashSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMeleeHit); + SubscribeLocalEvent(OnMeleeInteract); + SubscribeLocalEvent(OnUseInHand); + + SubscribeLocalEvent(OnExamined); + } + + public void OnMeleeHit(EntityUid uid, FlashComponent comp, MeleeHitEvent args) + { + if (!UseFlash(comp, args.User)) + { + return; + } + + args.Handled = true; + foreach (IEntity e in args.HitEntities) + { + FlashEntity(e, args.User, comp.FlashDuration, comp.SlowTo); + } + } + + private void OnMeleeInteract(EntityUid uid, FlashComponent comp, MeleeInteractEvent args) + { + if (!UseFlash(comp, args.User)) + { + return; + } + + if (args.Entity.HasComponent()) + { + args.CanInteract = true; + FlashEntity(args.Entity, args.User, comp.FlashDuration, comp.SlowTo); + } + } + + public void OnUseInHand(EntityUid uid, FlashComponent comp, UseInHandEvent args) + { + if (!UseFlash(comp, args.User)) + { + return; + } + + foreach (var entity in IoCManager.Resolve().GetEntitiesInRange(comp.Owner.Transform.Coordinates, comp.Range)) + { + FlashEntity(entity, args.User, comp.AoeFlashDuration, comp.SlowTo); + } + } + + private bool UseFlash(FlashComponent comp, IEntity user) + { + if (comp.HasUses) + { + // TODO flash visualizer + if (!comp.Owner.TryGetComponent(out var sprite)) + return false; + + if (--comp.Uses == 0) + { + sprite.LayerSetState(0, "burnt"); + comp.Owner.PopupMessage(user, Loc.GetString("flash-component-becomes-empty")); + } + else if (!comp.Flashing) + { + int animLayer = sprite.AddLayerWithState("flashing"); + comp.Flashing = true; + + comp.Owner.SpawnTimer(400, () => + { + sprite.RemoveLayer(animLayer); + comp.Flashing = false; + }); + } + + SoundSystem.Play(Filter.Pvs(comp.Owner), "/Audio/Weapons/flash.ogg", comp.Owner.Transform.Coordinates, + AudioParams.Default); + + return true; + } + + return false; + } + + // TODO: Check if target can be flashed (e.g. things like sunglasses would block a flash) + // TODO: Merge with the code in FlashableComponent--raise an event on the target, that FlashableComponent or + // another comp will catch + private void FlashEntity(IEntity target, IEntity user, float flashDuration, float slowTo) + { + if (target.TryGetComponent(out var flashable)) + { + flashable.Flash(flashDuration / 1000d); + } + + if (target.TryGetComponent(out var stunnableComponent)) + { + stunnableComponent.Slowdown(flashDuration / 1000f, slowTo, slowTo); + } + + if (target != user) + { + user.PopupMessage(target, + Loc.GetString( + "flash-component-user-blinds-you", + ("user", user) + ) + ); + } + } + + private void OnExamined(EntityUid uid, FlashComponent comp, ExaminedEvent args) + { + if (!comp.HasUses) + { + args.Message.AddText("\n"); + args.Message.AddText(Loc.GetString("flash-component-examine-empty")); + return; + } + + if (args.IsInDetailsRange) + { + args.Message.AddText("\n"); + args.Message.AddMarkup( + Loc.GetString( + "flash-component-examine-detail-count", + ("count", comp.Uses), + ("markupCountColor", "green") + ) + ); + } + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/Weapon/Melee/MeleeWeaponSystem.cs b/Content.Server/GameObjects/EntitySystems/Weapon/Melee/MeleeWeaponSystem.cs new file mode 100644 index 0000000000..d84f484acd --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/Weapon/Melee/MeleeWeaponSystem.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Server.GameObjects.Components.Body.Circulatory; +using Content.Server.GameObjects.Components.Chemistry; +using Content.Server.GameObjects.Components.Weapon.Melee; +using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.EntitySystemMessages; +using Content.Shared.Interfaces.GameObjects.Components; +using Content.Shared.Physics; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Broadphase; +using Robust.Shared.Player; +using Robust.Shared.Timing; + +namespace Content.Server.GameObjects.EntitySystems.Weapon.Melee +{ + public sealed class MeleeWeaponSystem : EntitySystem + { + [Dependency] private IGameTiming _gameTiming = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHandSelected); + SubscribeLocalEvent(OnClickAttack); + SubscribeLocalEvent(OnWideAttack); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnChemicalInjectorHit); + } + + private void OnHandSelected(EntityUid uid, MeleeWeaponComponent comp, HandSelectedEvent args) + { + var curTime = _gameTiming.CurTime; + var cool = TimeSpan.FromSeconds(comp.CooldownTime * 0.5f); + + if (curTime < comp.CooldownEnd) + { + if (comp.CooldownEnd - curTime < cool) + { + comp.LastAttackTime = curTime; + comp.CooldownEnd += cool; + } + else + return; + } + else + { + comp.LastAttackTime = curTime; + comp.CooldownEnd = curTime + cool; + } + + RaiseLocalEvent(uid, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false); + } + + private void OnClickAttack(EntityUid uid, MeleeWeaponComponent comp, ClickAttackEvent args) + { + var curTime = _gameTiming.CurTime; + + if (curTime < comp.CooldownEnd || !args.Target.IsValid()) + return; + + var owner = EntityManager.GetEntity(uid); + var target = args.TargetEntity; + + var location = args.User.Transform.Coordinates; + var diff = args.ClickLocation.ToMapPos(owner.EntityManager) - location.ToMapPos(owner.EntityManager); + var angle = Angle.FromWorldVec(diff); + + if (target != null) + { + // Raise event before doing damage so we can cancel damage if the event is handled + var hitEvent = new MeleeHitEvent(new List() {target}, args.User); + RaiseLocalEvent(uid, hitEvent, false); + + if (!hitEvent.Handled) + { + var targets = new[] {target}; + SendAnimation(comp.ClickArc, angle, args.User, owner, targets, comp.ClickAttackEffect, false); + + if (target.TryGetComponent(out IDamageableComponent? damageableComponent)) + { + damageableComponent.ChangeDamage(comp.DamageType, comp.Damage, false, owner); + } + + SoundSystem.Play(Filter.Pvs(owner), comp.HitSound, target); + } + } + else + { + SoundSystem.Play(Filter.Pvs(owner), comp.MissSound, args.User); + return; + } + + comp.LastAttackTime = curTime; + comp.CooldownEnd = comp.LastAttackTime + TimeSpan.FromSeconds(comp.CooldownTime); + + RaiseLocalEvent(uid, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false); + } + + private void OnWideAttack(EntityUid uid, MeleeWeaponComponent comp, WideAttackEvent args) + { + var curTime = _gameTiming.CurTime; + + if (curTime < comp.CooldownEnd) + { + return; + } + + var owner = EntityManager.GetEntity(uid); + + var location = args.User.Transform.Coordinates; + var diff = args.ClickLocation.ToMapPos(owner.EntityManager) - location.ToMapPos(owner.EntityManager); + var angle = Angle.FromWorldVec(diff); + + // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes. + var entities = ArcRayCast(args.User.Transform.WorldPosition, angle, comp.ArcWidth, comp.Range, owner.Transform.MapID, args.User); + + var hitEntities = new List(); + foreach (var entity in entities) + { + if (!entity.Transform.IsMapTransform || entity == args.User) + continue; + + if (ComponentManager.HasComponent(entity.Uid)) + { + hitEntities.Add(entity); + } + } + + // Raise event before doing damage so we can cancel damage if handled + var hitEvent = new MeleeHitEvent(hitEntities, args.User); + RaiseLocalEvent(uid, hitEvent, false); + SendAnimation(comp.Arc, angle, args.User, owner, hitEntities); + + if (!hitEvent.Handled) + { + if (entities.Count != 0) + { + SoundSystem.Play(Filter.Pvs(owner), comp.HitSound, entities.First().Transform.Coordinates); + } + else + { + SoundSystem.Play(Filter.Pvs(owner), comp.MissSound, args.User.Transform.Coordinates); + } + + foreach (var entity in hitEntities) + { + if (entity.TryGetComponent(out var damageComponent)) + { + damageComponent.ChangeDamage(comp.DamageType, comp.Damage, false, owner); + } + } + } + + comp.LastAttackTime = curTime; + comp.CooldownEnd = comp.LastAttackTime + TimeSpan.FromSeconds(comp.ArcCooldownTime); + + RaiseLocalEvent(uid, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false); + } + + /// + /// Used for melee weapons that want some behavior on AfterInteract, + /// but also want the cooldown (stun batons, flashes) + /// + private void OnAfterInteract(EntityUid uid, MeleeWeaponComponent comp, AfterInteractEvent args) + { + if (!args.CanReach) + return; + + var curTime = _gameTiming.CurTime; + + if (curTime < comp.CooldownEnd) + { + return; + } + + var owner = EntityManager.GetEntity(uid); + + if (args.Target == null) + return; + + var location = args.User.Transform.Coordinates; + var diff = args.ClickLocation.ToMapPos(owner.EntityManager) - location.ToMapPos(owner.EntityManager); + var angle = Angle.FromWorldVec(diff); + + var hitEvent = new MeleeInteractEvent(args.Target, args.User); + RaiseLocalEvent(uid, hitEvent, false); + + if (!hitEvent.CanInteract) return; + SendAnimation(comp.ClickArc, angle, args.User, owner, new List() { args.Target }, comp.ClickAttackEffect, false); + + comp.LastAttackTime = curTime; + comp.CooldownEnd = comp.LastAttackTime + TimeSpan.FromSeconds(comp.CooldownTime); + + RaiseLocalEvent(uid, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false); + } + + private HashSet ArcRayCast(Vector2 position, Angle angle, float arcWidth, float range, MapId mapId, IEntity ignore) + { + var widthRad = Angle.FromDegrees(arcWidth); + var increments = 1 + 35 * (int) Math.Ceiling(widthRad / (2 * Math.PI)); + var increment = widthRad / increments; + var baseAngle = angle - widthRad / 2; + + var resSet = new HashSet(); + + for (var i = 0; i < increments; i++) + { + var castAngle = new Angle(baseAngle + increment * i); + var res = EntitySystem.Get().IntersectRay(mapId, + new CollisionRay(position, castAngle.ToWorldVec(), + (int) (CollisionGroup.Impassable | CollisionGroup.MobImpassable)), range, ignore).ToList(); + + if (res.Count != 0) + { + resSet.Add(res[0].HitEntity); + } + } + + return resSet; + } + + private void OnChemicalInjectorHit(EntityUid uid, MeleeChemicalInjectorComponent comp, MeleeHitEvent args) + { + if (!ComponentManager.TryGetComponent(uid, out var solutionContainer)) + return; + + var hitBloodstreams = new List(); + foreach (var entity in args.HitEntities) + { + if (entity.Deleted) + continue; + + if (entity.TryGetComponent(out var bloodstream)) + hitBloodstreams.Add(bloodstream); + } + + if (hitBloodstreams.Count < 1) + return; + + var removedSolution = solutionContainer.Solution.SplitSolution(comp.TransferAmount * hitBloodstreams.Count); + var removedVol = removedSolution.TotalVolume; + var solutionToInject = removedSolution.SplitSolution(removedVol * comp.TransferEfficiency); + var volPerBloodstream = solutionToInject.TotalVolume * (1 / hitBloodstreams.Count); + + foreach (var bloodstream in hitBloodstreams) + { + var individualInjection = solutionToInject.SplitSolution(volPerBloodstream); + bloodstream.TryTransferSolution(individualInjection); + } + } + + public void SendAnimation(string arc, Angle angle, IEntity attacker, IEntity source, IEnumerable hits, bool textureEffect = false, bool arcFollowAttacker = true) + { + RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayMeleeWeaponAnimationMessage(arc, angle, attacker.Uid, source.Uid, + hits.Select(e => e.Uid).ToList(), textureEffect, arcFollowAttacker), Filter.Pvs(source, 1f)); + } + + public void SendLunge(Angle angle, IEntity source) + { + RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayLungeAnimationMessage(angle, source.Uid), Filter.Pvs(source, 1f)); + } + } + + /// + /// Raised directed on the melee weapon entity used to attack something in combat mode, + /// whether through a click attack or wide attack. + /// + public class MeleeHitEvent : HandledEntityEventArgs + { + /// + /// A list containing every hit entity. Can be zero. + /// + public IEnumerable HitEntities { get; } + + /// + /// The user who attacked with the melee wepaon. + /// + public IEntity User { get; } + + public MeleeHitEvent(List hitEntities, IEntity user) + { + HitEntities = hitEntities; + User = user; + } + } + + /// + /// Raised directed on the melee weapon entity used to attack something in combat mode, + /// whether through a click attack or wide attack. + /// + public class MeleeInteractEvent : EntityEventArgs + { + /// + /// The entity interacted with. + /// + public IEntity Entity { get; } + + /// + /// The user who interacted using the melee weapon. + /// + public IEntity User { get; } + + /// + /// Modified by the event handler to specify whether they could successfully interact with the entity. + /// Used to know whether to send the hit animation or not. + /// + public bool CanInteract { get; set; } = false; + + public MeleeInteractEvent(IEntity entity, IEntity user) + { + Entity = entity; + User = user; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/Weapon/Melee/StunbatonSystem.cs b/Content.Server/GameObjects/EntitySystems/Weapon/Melee/StunbatonSystem.cs new file mode 100644 index 0000000000..9c0293961f --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/Weapon/Melee/StunbatonSystem.cs @@ -0,0 +1,191 @@ +using System.Linq; +using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.Components.Power; +using Content.Server.GameObjects.Components.Weapon.Melee; +using Content.Shared.Audio; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.GameObjects.EntitySystems.ActionBlocker; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Player; +using Robust.Shared.Random; + +namespace Content.Server.GameObjects.EntitySystems.Weapon.Melee +{ + public class StunbatonSystem : EntitySystem + { + [Dependency] private readonly IRobustRandom _robustRandom = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMeleeHit); + SubscribeLocalEvent(OnMeleeInteract); + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnThrowCollide); + SubscribeLocalEvent(OnPowerCellChanged); + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnExamined); + } + + private void OnMeleeHit(EntityUid uid, StunbatonComponent comp, MeleeHitEvent args) + { + if (!comp.Activated || !args.HitEntities.Any()) + return; + + if (!ComponentManager.TryGetComponent(uid, out var slot) || slot.Cell == null || !slot.Cell.TryUseCharge(comp.EnergyPerUse)) + return; + + foreach (IEntity entity in args.HitEntities) + { + StunEntity(entity, comp); + } + } + + private void OnMeleeInteract(EntityUid uid, StunbatonComponent comp, MeleeInteractEvent args) + { + if (!comp.Activated) + return; + + if (!ComponentManager.TryGetComponent(uid, out var slot) || slot.Cell == null || !slot.Cell.TryUseCharge(comp.EnergyPerUse)) + return; + + if (args.Entity.HasComponent()) + { + args.CanInteract = true; + StunEntity(args.Entity, comp); + } + } + + private void OnUseInHand(EntityUid uid, StunbatonComponent comp, UseInHandEvent args) + { + if (!ActionBlockerSystem.CanUse(args.User)) return; + if (comp.Activated) + { + TurnOff(comp); + } + else + { + TurnOn(comp, args.User); + } + } + + private void OnThrowCollide(EntityUid uid, StunbatonComponent comp, ThrowCollideEvent args) + { + if (!ComponentManager.TryGetComponent(uid, out var slot)) return; + if (!comp.Activated || slot.Cell == null || !slot.Cell.TryUseCharge(comp.EnergyPerUse)) return; + + StunEntity(args.Target, comp); + } + + private void OnPowerCellChanged(EntityUid uid, StunbatonComponent comp, PowerCellChangedEvent args) + { + if (args.Ejected) + { + TurnOff(comp); + } + } + + private void OnInteractUsing(EntityUid uid, StunbatonComponent comp, InteractUsingEvent args) + { + if (!ActionBlockerSystem.CanInteract(args.User)) return; + if (ComponentManager.TryGetComponent(uid, out var cellslot)) + cellslot.InsertCell(args.Used); + } + + private void OnExamined(EntityUid uid, StunbatonComponent comp, ExaminedEvent args) + { + args.Message.AddText("\n"); + var msg = comp.Activated + ? Loc.GetString("comp-stunbaton-examined-on") + : Loc.GetString("comp-stunbaton-examined-off"); + args.Message.AddMarkup(msg); + } + + private void StunEntity(IEntity entity, StunbatonComponent comp) + { + if (!entity.TryGetComponent(out StunnableComponent? stunnable) || !comp.Activated) return; + + SoundSystem.Play(Filter.Pvs(comp.Owner), "/Audio/Weapons/egloves.ogg", comp.Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); + if(!stunnable.SlowedDown) + { + if(_robustRandom.Prob(comp.ParalyzeChanceNoSlowdown)) + stunnable.Paralyze(comp.ParalyzeTime); + else + stunnable.Slowdown(comp.SlowdownTime); + } + else + { + if(_robustRandom.Prob(comp.ParalyzeChanceWithSlowdown)) + stunnable.Paralyze(comp.ParalyzeTime); + else + stunnable.Slowdown(comp.SlowdownTime); + } + + + if (!comp.Owner.TryGetComponent(out var slot) || slot.Cell == null || !(slot.Cell.CurrentCharge < comp.EnergyPerUse)) return; + + SoundSystem.Play(Filter.Pvs(comp.Owner), AudioHelpers.GetRandomFileFromSoundCollection("sparks"), comp.Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); + TurnOff(comp); + } + + private void TurnOff(StunbatonComponent comp) + { + if (!comp.Activated) + { + return; + } + + if (!comp.Owner.TryGetComponent(out var sprite) || + !comp.Owner.TryGetComponent(out var item)) return; + + SoundSystem.Play(Filter.Pvs(comp.Owner), AudioHelpers.GetRandomFileFromSoundCollection("sparks"), comp.Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); + item.EquippedPrefix = "off"; + // TODO stunbaton visualizer + sprite.LayerSetState(0, "stunbaton_off"); + comp.Activated = false; + } + + private void TurnOn(StunbatonComponent comp, IEntity user) + { + if (comp.Activated) + { + return; + } + + if (!comp.Owner.TryGetComponent(out var sprite) || + !comp.Owner.TryGetComponent(out var item)) return; + + var playerFilter = Filter.Pvs(comp.Owner); + if (!comp.Owner.TryGetComponent(out var slot)) + return; + + if (slot.Cell == null) + { + SoundSystem.Play(playerFilter, "/Audio/Machines/button.ogg", comp.Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); + user.PopupMessage(Loc.GetString("comp-stunbaton-activated-missing-cell")); + return; + } + + if (slot.Cell != null && slot.Cell.CurrentCharge < comp.EnergyPerUse) + { + SoundSystem.Play(playerFilter, "/Audio/Machines/button.ogg", comp.Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); + user.PopupMessage(Loc.GetString("comp-stunbaton-activated-dead-cell")); + return; + } + + SoundSystem.Play(playerFilter, AudioHelpers.GetRandomFileFromSoundCollection("sparks"), comp.Owner.Transform.Coordinates, AudioHelpers.WithVariation(0.25f)); + + item.EquippedPrefix = "on"; + sprite.LayerSetState(0, "stunbaton_on"); + comp.Activated = true; + } + } +} diff --git a/Content.Shared/Interfaces/GameObjects/Components/Interaction/AttackEvent.cs b/Content.Shared/Interfaces/GameObjects/Components/Interaction/AttackEvent.cs new file mode 100644 index 0000000000..e917e4c639 --- /dev/null +++ b/Content.Shared/Interfaces/GameObjects/Components/Interaction/AttackEvent.cs @@ -0,0 +1,76 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Map; + +namespace Content.Shared.Interfaces.GameObjects.Components +{ + /// + /// Raised directed on the used entity when a target entity is click attacked by a user. + /// + public class ClickAttackEvent : EntityEventArgs + { + /// + /// Entity used to attack, for broadcast purposes. + /// + public IEntity Used { get; } + + /// + /// Entity that triggered the attack. + /// + public IEntity User { get; } + + /// + /// The original location that was clicked by the user. + /// + public EntityCoordinates ClickLocation { get; } + + /// + /// UID of the entity that was attacked. + /// + public EntityUid Target { get; } + + /// + /// Entity that was attacked. + /// + public IEntity? TargetEntity { get; } + + public ClickAttackEvent(IEntity used, IEntity user, EntityCoordinates clickLocation, EntityUid target = default) + { + Used = used; + User = user; + ClickLocation = clickLocation; + Target = target; + + IoCManager.Resolve().TryGetEntity(Target, out var targetEntity); + TargetEntity = targetEntity; + } + } + + /// + /// Raised directed on the used entity when a target entity is wide attacked by a user. + /// + public class WideAttackEvent : EntityEventArgs + { + /// + /// Entity used to attack, for broadcast purposes. + /// + public IEntity Used { get; } + + /// + /// Entity that triggered the attack. + /// + public IEntity User { get; } + + /// + /// The original location that was clicked by the user. + /// + public EntityCoordinates ClickLocation { get; } + + public WideAttackEvent(IEntity used, IEntity user, EntityCoordinates clickLocation) + { + Used = used; + User = user; + ClickLocation = clickLocation; + } + } +} diff --git a/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAttack.cs b/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAttack.cs deleted file mode 100644 index e0cf6a8b3f..0000000000 --- a/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAttack.cs +++ /dev/null @@ -1,65 +0,0 @@ -#nullable enable -using System; -using Robust.Shared.Analyzers; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Map; - -namespace Content.Shared.Interfaces.GameObjects.Components -{ - /// - /// This interface gives components behavior when being used to "attack". - /// - [RequiresExplicitImplementation] - public interface IAttack - { - // Redirects to ClickAttack by default. - [Obsolete("WideAttack")] - bool WideAttack(AttackEvent eventArgs) => ClickAttack(eventArgs); - - [Obsolete("Use ClickAttack instead")] - bool ClickAttack(AttackEvent eventArgs); - } - - /// - /// Raised when a target entity is attacked by a user. - /// - public class AttackEvent : EntityEventArgs - { - /// - /// Entity that triggered the attack. - /// - public IEntity User { get; } - - /// - /// The original location that was clicked by the user. - /// - public EntityCoordinates ClickLocation { get; } - - /// - /// Indicates whether the attack creates a swing attack or attacks the target entity directly. - /// - public bool WideAttack { get; } - - /// - /// UID of the entity that was attacked. - /// - public EntityUid Target { get; } - - /// - /// Entity that was attacked. - /// - public IEntity? TargetEntity { get; } - - public AttackEvent(IEntity user, EntityCoordinates clickLocation, bool wideAttack, EntityUid target = default) - { - User = user; - ClickLocation = clickLocation; - WideAttack = wideAttack; - Target = target; - - IoCManager.Resolve().TryGetEntity(Target, out var targetEntity); - TargetEntity = targetEntity; - } - } -} diff --git a/Resources/Locale/en-US/components/stunbaton-component.ftl b/Resources/Locale/en-US/components/stunbaton-component.ftl new file mode 100644 index 0000000000..24a17466e4 --- /dev/null +++ b/Resources/Locale/en-US/components/stunbaton-component.ftl @@ -0,0 +1,11 @@ +### Stunbaton component + +## Used when examining the stunbaton + +comp-stunbaton-examined-on = The light is currently [color=darkgreen]on[/color]. +comp-stunbaton-examined-off = The light is currently [color=darkred]off[/color] + +## Used when activating the stunbaton, depending on the state of its cell. + +comp-stunbaton-activated-dead-cell = Dead cell... +comp-stunbaton-activated-missing-cell = Missing cell... diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml index b1d56ee315..f28dfe0ff6 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml @@ -24,6 +24,7 @@ - type: ItemCooldown - type: MeleeChemicalInjector - type: SolutionContainer + caps: Refillable maxVol: 5 - type: SolutionTransfer diff --git a/Resources/Prototypes/Entities/Objects/Weapons/security.yml b/Resources/Prototypes/Entities/Objects/Weapons/security.yml index 44b3e66e5f..ad668ea4c0 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/security.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/security.yml @@ -8,9 +8,10 @@ sprite: Objects/Weapons/Melee/stunbaton.rsi state: stunbaton_off - type: Stunbaton - damage: 1 - range: 0.75 - arcwidth: 0 + - type: MeleeWeapon + damage: 10 + range: 1.5 + arcwidth: 60 arc: default - type: PowerCellSlot slotSize: Medium @@ -34,10 +35,11 @@ sprite: Objects/Weapons/Melee/flash.rsi state: flash - type: Flash + - type: MeleeWeapon damage: 0 - cooldownTime: 1 - arc: smash - hitSound: /Audio/Weapons/flash.ogg + range: 1 + arcWidth: 10 + arc: default - type: Item size: 2 sprite: Objects/Weapons/Melee/flash.rsi