]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Make energy sword reflect projectiles and hitscan shots (#14029)
authorSlava0135 <40753025+Slava0135@users.noreply.github.com>
Sun, 2 Apr 2023 13:48:32 +0000 (16:48 +0300)
committerGitHub <noreply@github.com>
Sun, 2 Apr 2023 13:48:32 +0000 (23:48 +1000)
16 files changed:
Content.Client/Weapons/Reflect/ReflectSystem.cs [new file with mode: 0644]
Content.Server/Projectiles/ProjectileSystem.cs [new file with mode: 0644]
Content.Server/Projectiles/SharedProjectileSystem.cs [deleted file]
Content.Server/Weapons/Melee/EnergySword/Components/EnergySwordComponent.cs [deleted file]
Content.Server/Weapons/Melee/EnergySword/EnergySwordComponent.cs [new file with mode: 0644]
Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs
Content.Server/Weapons/Ranged/Systems/GunSystem.cs
Content.Server/Weapons/Reflect/ReflectSystem.cs [new file with mode: 0644]
Content.Shared/Projectiles/SharedProjectileSystem.cs
Content.Shared/Weapons/Ranged/Events/HitScanReflectAttempt.cs [new file with mode: 0644]
Content.Shared/Weapons/Reflect/ReflectComponent.cs [new file with mode: 0644]
Content.Shared/Weapons/Reflect/SharedReflectSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/store/uplink-catalog.ftl
Resources/Locale/en-US/weapons/reflect/reflect-component.ftl [new file with mode: 0644]
Resources/Prototypes/Catalog/uplink_catalog.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml

diff --git a/Content.Client/Weapons/Reflect/ReflectSystem.cs b/Content.Client/Weapons/Reflect/ReflectSystem.cs
new file mode 100644 (file)
index 0000000..46f00a4
--- /dev/null
@@ -0,0 +1,7 @@
+using Content.Shared.Weapons.Reflect;
+
+namespace Content.Client.Weapons.Reflect;
+
+public sealed class ReflectSystem : SharedReflectSystem
+{
+}
diff --git a/Content.Server/Projectiles/ProjectileSystem.cs b/Content.Server/Projectiles/ProjectileSystem.cs
new file mode 100644 (file)
index 0000000..659de57
--- /dev/null
@@ -0,0 +1,87 @@
+using Content.Server.Administration.Logs;
+using Content.Server.Weapons.Ranged.Systems;
+using Content.Shared.Camera;
+using Content.Shared.Damage;
+using Content.Shared.Database;
+using Content.Shared.FixedPoint;
+using Content.Shared.Projectiles;
+using Content.Shared.Weapons.Melee;
+using JetBrains.Annotations;
+using Robust.Server.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Player;
+using Robust.Shared.Physics.Events;
+
+namespace Content.Server.Projectiles;
+
+[UsedImplicitly]
+public sealed class ProjectileSystem : SharedProjectileSystem
+{
+    [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+    [Dependency] private readonly GunSystem _guns = default!;
+    [Dependency] private readonly SharedCameraRecoilSystem _sharedCameraRecoil = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<ProjectileComponent, StartCollideEvent>(OnStartCollide);
+        SubscribeLocalEvent<ProjectileComponent, ComponentGetState>(OnGetState);
+    }
+
+    private void OnGetState(EntityUid uid, ProjectileComponent component, ref ComponentGetState args)
+    {
+        args.State = new ProjectileComponentState(component.Shooter, component.IgnoreShooter);
+    }
+
+    private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref StartCollideEvent args)
+    {
+        // This is so entities that shouldn't get a collision are ignored.
+        if (args.OurFixture.ID != ProjectileFixture || !args.OtherFixture.Hard || component.DamagedEntity)
+            return;
+
+        var otherEntity = args.OtherFixture.Body.Owner;
+        // it's here so this check is only done once before possible hit
+        var attemptEv = new ProjectileReflectAttemptEvent(uid, component, false);
+        RaiseLocalEvent(otherEntity, ref attemptEv);
+        if (attemptEv.Cancelled)
+        {
+            SetShooter(component, otherEntity);
+            return;
+        }
+
+        var otherName = ToPrettyString(otherEntity);
+        var direction = args.OurFixture.Body.LinearVelocity.Normalized;
+        var modifiedDamage = _damageableSystem.TryChangeDamage(otherEntity, component.Damage, component.IgnoreResistances, origin: component.Shooter);
+        component.DamagedEntity = true;
+        var deleted = Deleted(otherEntity);
+
+        if (modifiedDamage is not null && EntityManager.EntityExists(component.Shooter))
+        {
+            if (modifiedDamage.Total > FixedPoint2.Zero && !deleted)
+            {
+                RaiseNetworkEvent(new DamageEffectEvent(Color.Red, new List<EntityUid> {otherEntity}), Filter.Pvs(otherEntity, entityManager: EntityManager));
+            }
+
+            _adminLogger.Add(LogType.BulletHit,
+                HasComp<ActorComponent>(otherEntity) ? LogImpact.Extreme : LogImpact.High,
+                $"Projectile {ToPrettyString(uid):projectile} shot by {ToPrettyString(component.Shooter):user} hit {otherName:target} and dealt {modifiedDamage.Total:damage} damage");
+        }
+
+        if (!deleted)
+        {
+            _guns.PlayImpactSound(otherEntity, modifiedDamage, component.SoundHit, component.ForceSound);
+            _sharedCameraRecoil.KickCamera(otherEntity, direction);
+        }
+
+        if (component.DeleteOnCollide)
+        {
+            QueueDel(uid);
+
+            if (component.ImpactEffect != null && TryComp<TransformComponent>(component.Owner, out var xform))
+            {
+                RaiseNetworkEvent(new ImpactEffectEvent(component.ImpactEffect, xform.Coordinates), Filter.Pvs(xform.Coordinates, entityMan: EntityManager));
+            }
+        }
+    }
+}
diff --git a/Content.Server/Projectiles/SharedProjectileSystem.cs b/Content.Server/Projectiles/SharedProjectileSystem.cs
deleted file mode 100644 (file)
index 64c1706..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-using Content.Server.Administration.Logs;
-using Content.Server.Weapons.Ranged.Systems;
-using Content.Shared.Camera;
-using Content.Shared.Damage;
-using Content.Shared.Database;
-using Content.Shared.FixedPoint;
-using Content.Shared.Projectiles;
-using Content.Shared.Weapons.Melee;
-using JetBrains.Annotations;
-using Robust.Server.GameObjects;
-using Robust.Shared.GameStates;
-using Robust.Shared.Player;
-using Robust.Shared.Physics.Events;
-
-namespace Content.Server.Projectiles
-{
-    [UsedImplicitly]
-    public sealed class ProjectileSystem : SharedProjectileSystem
-    {
-        [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-        [Dependency] private readonly DamageableSystem _damageableSystem = default!;
-        [Dependency] private readonly GunSystem _guns = default!;
-        [Dependency] private readonly SharedCameraRecoilSystem _sharedCameraRecoil = default!;
-
-        public override void Initialize()
-        {
-            base.Initialize();
-            SubscribeLocalEvent<ProjectileComponent, StartCollideEvent>(OnStartCollide);
-            SubscribeLocalEvent<ProjectileComponent, ComponentGetState>(OnGetState);
-        }
-
-        private void OnGetState(EntityUid uid, ProjectileComponent component, ref ComponentGetState args)
-        {
-            args.State = new ProjectileComponentState(component.Shooter, component.IgnoreShooter);
-        }
-
-        private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref StartCollideEvent args)
-        {
-            // This is so entities that shouldn't get a collision are ignored.
-            if (args.OurFixture.ID != ProjectileFixture || !args.OtherFixture.Hard || component.DamagedEntity)
-                return;
-
-            var otherEntity = args.OtherFixture.Body.Owner;
-            var otherName = ToPrettyString(otherEntity);
-            var direction = args.OurFixture.Body.LinearVelocity.Normalized;
-            var modifiedDamage = _damageableSystem.TryChangeDamage(otherEntity, component.Damage, component.IgnoreResistances, origin: component.Shooter);
-            component.DamagedEntity = true;
-            var deleted = Deleted(otherEntity);
-
-            if (modifiedDamage is not null && EntityManager.EntityExists(component.Shooter))
-            {
-                if (modifiedDamage.Total > FixedPoint2.Zero && !deleted)
-                {
-                    RaiseNetworkEvent(new DamageEffectEvent(Color.Red, new List<EntityUid> {otherEntity}), Filter.Pvs(otherEntity, entityManager: EntityManager));
-                }
-
-                _adminLogger.Add(LogType.BulletHit,
-                    HasComp<ActorComponent>(otherEntity) ? LogImpact.Extreme : LogImpact.High,
-                    $"Projectile {ToPrettyString(uid):projectile} shot by {ToPrettyString(component.Shooter):user} hit {otherName:target} and dealt {modifiedDamage.Total:damage} damage");
-            }
-
-            if (!deleted)
-            {
-                _guns.PlayImpactSound(otherEntity, modifiedDamage, component.SoundHit, component.ForceSound);
-                _sharedCameraRecoil.KickCamera(otherEntity, direction);
-            }
-
-            if (component.DeleteOnCollide)
-            {
-                QueueDel(uid);
-
-                if (component.ImpactEffect != null && TryComp<TransformComponent>(component.Owner, out var xform))
-                {
-                    RaiseNetworkEvent(new ImpactEffectEvent(component.ImpactEffect, xform.Coordinates), Filter.Pvs(xform.Coordinates, entityMan: EntityManager));
-                }
-            }
-        }
-    }
-}
diff --git a/Content.Server/Weapons/Melee/EnergySword/Components/EnergySwordComponent.cs b/Content.Server/Weapons/Melee/EnergySword/Components/EnergySwordComponent.cs
deleted file mode 100644 (file)
index b482c6c..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-using Content.Shared.Damage;
-using Robust.Shared.Audio;
-
-namespace Content.Server.Weapons.Melee.EnergySword.Components
-{
-    [RegisterComponent]
-    internal sealed class EnergySwordComponent : Component
-    {
-        public Color BladeColor = Color.DodgerBlue;
-
-        public bool Hacked = false;
-
-        public bool Activated = false;
-
-        [DataField("isSharp")]
-        public bool IsSharp = true;
-
-        /// <summary>
-        ///     Does this become hidden when deactivated
-        /// </summary>
-        [DataField("secret")]
-        public bool Secret { get; set; } = false;
-
-        /// <summary>
-        ///     RGB cycle rate for hacked e-swords.
-        /// </summary>
-        [DataField("cycleRate")]
-        public float CycleRate = 1f;
-
-        [DataField("activateSound")]
-        public SoundSpecifier ActivateSound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/ebladeon.ogg");
-
-        [DataField("deActivateSound")]
-        public SoundSpecifier DeActivateSound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/ebladeoff.ogg");
-
-        [DataField("onHitOn")]
-        public SoundSpecifier OnHitOn { get; set; } = new SoundPathSpecifier("/Audio/Weapons/eblade1.ogg");
-
-        [DataField("onHitOff")]
-        public SoundSpecifier OnHitOff { get; set; } = new SoundPathSpecifier("/Audio/Weapons/genhit1.ogg");
-
-        [DataField("colorOptions")]
-        public List<Color> ColorOptions = new()
-        {
-            Color.Tomato,
-            Color.DodgerBlue,
-            Color.Aqua,
-            Color.MediumSpringGreen,
-            Color.MediumOrchid
-        };
-
-        [DataField("litDamageBonus")]
-        public DamageSpecifier LitDamageBonus = new();
-
-        [DataField("litDisarmMalus")]
-        public float litDisarmMalus = 0.6f;
-    }
-}
diff --git a/Content.Server/Weapons/Melee/EnergySword/EnergySwordComponent.cs b/Content.Server/Weapons/Melee/EnergySword/EnergySwordComponent.cs
new file mode 100644 (file)
index 0000000..a6378ea
--- /dev/null
@@ -0,0 +1,63 @@
+using Content.Shared.Damage;
+using Robust.Shared.Audio;
+
+namespace Content.Server.Weapons.Melee.EnergySword;
+
+[RegisterComponent]
+internal sealed class EnergySwordComponent : Component
+{
+    public Color BladeColor = Color.DodgerBlue;
+
+    public bool Hacked = false;
+
+    public bool Activated = false;
+
+    [DataField("isSharp")]
+    public bool IsSharp = true;
+
+    /// <summary>
+    ///     Does this become hidden when deactivated
+    /// </summary>
+    [DataField("secret")]
+    public bool Secret { get; set; } = false;
+
+    /// <summary>
+    ///     RGB cycle rate for hacked e-swords.
+    /// </summary>
+    [DataField("cycleRate")]
+    public float CycleRate = 1f;
+
+    [DataField("activateSound")]
+    public SoundSpecifier ActivateSound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/ebladeon.ogg");
+
+    [DataField("deActivateSound")]
+    public SoundSpecifier DeActivateSound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/ebladeoff.ogg");
+
+    [DataField("onHitOn")]
+    public SoundSpecifier OnHitOn { get; set; } = new SoundPathSpecifier("/Audio/Weapons/eblade1.ogg");
+
+    [DataField("onHitOff")]
+    public SoundSpecifier OnHitOff { get; set; } = new SoundPathSpecifier("/Audio/Weapons/genhit1.ogg");
+
+    [DataField("colorOptions")]
+    public List<Color> ColorOptions = new()
+    {
+        Color.Tomato,
+        Color.DodgerBlue,
+        Color.Aqua,
+        Color.MediumSpringGreen,
+        Color.MediumOrchid
+    };
+
+    [DataField("litDamageBonus")]
+    public DamageSpecifier LitDamageBonus = new();
+
+    [DataField("litDisarmMalus")]
+    public float LitDisarmMalus = 0.6f;
+}
+
+[ByRefEvent]
+public readonly record struct EnergySwordActivatedEvent();
+
+[ByRefEvent]
+public readonly record struct EnergySwordDeactivatedEvent();
index 6ebb293ece3b6f2608c6f68562191a6ee820afe4..823ee642cf07b0ec19c2c25ac8de28fd20908aad 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Server.CombatMode.Disarm;
 using Content.Server.Kitchen.Components;
-using Content.Server.Weapons.Melee.EnergySword.Components;
 using Content.Shared.Interaction;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Item;
@@ -14,151 +13,150 @@ using Content.Shared.Weapons.Melee.Events;
 using Robust.Shared.Player;
 using Robust.Shared.Random;
 
-namespace Content.Server.Weapons.Melee.EnergySword
+namespace Content.Server.Weapons.Melee.EnergySword;
+
+public sealed class EnergySwordSystem : EntitySystem
 {
-    public sealed class EnergySwordSystem : EntitySystem
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly SharedRgbLightControllerSystem _rgbSystem = default!;
+    [Dependency] private readonly SharedItemSystem _item = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+    public override void Initialize()
     {
-        [Dependency] private readonly IRobustRandom _random = default!;
-        [Dependency] private readonly SharedRgbLightControllerSystem _rgbSystem = default!;
-        [Dependency] private readonly SharedItemSystem _item = default!;
-        [Dependency] private readonly SharedAudioSystem _audio = default!;
-        [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+        base.Initialize();
+
+        SubscribeLocalEvent<EnergySwordComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<EnergySwordComponent, MeleeHitEvent>(OnMeleeHit);
+        SubscribeLocalEvent<EnergySwordComponent, UseInHandEvent>(OnUseInHand);
+        SubscribeLocalEvent<EnergySwordComponent, InteractUsingEvent>(OnInteractUsing);
+        SubscribeLocalEvent<EnergySwordComponent, IsHotEvent>(OnIsHotEvent);
+        SubscribeLocalEvent<EnergySwordComponent, EnergySwordDeactivatedEvent>(TurnOff);
+        SubscribeLocalEvent<EnergySwordComponent, EnergySwordActivatedEvent>(TurnOn);
+    }
 
-        public override void Initialize()
-        {
-            base.Initialize();
+    private void OnMapInit(EntityUid uid, EnergySwordComponent comp, MapInitEvent args)
+    {
+        if (comp.ColorOptions.Count != 0)
+            comp.BladeColor = _random.Pick(comp.ColorOptions);
+    }
 
-            SubscribeLocalEvent<EnergySwordComponent, MapInitEvent>(OnMapInit);
-            SubscribeLocalEvent<EnergySwordComponent, MeleeHitEvent>(OnMeleeHit);
-            SubscribeLocalEvent<EnergySwordComponent, UseInHandEvent>(OnUseInHand);
-            SubscribeLocalEvent<EnergySwordComponent, InteractUsingEvent>(OnInteractUsing);
-            SubscribeLocalEvent<EnergySwordComponent, IsHotEvent>(OnIsHotEvent);
-        }
+    private void OnMeleeHit(EntityUid uid, EnergySwordComponent comp, MeleeHitEvent args)
+    {
+        if (!comp.Activated)
+            return;
 
-        private void OnMapInit(EntityUid uid, EnergySwordComponent comp, MapInitEvent args)
-        {
-            if (comp.ColorOptions.Count != 0)
-                comp.BladeColor = _random.Pick(comp.ColorOptions);
-        }
+        // Overrides basic blunt damage with burn+slash as set in yaml
+        args.BonusDamage = comp.LitDamageBonus;
+    }
 
-        private void OnMeleeHit(EntityUid uid, EnergySwordComponent comp, MeleeHitEvent args)
-        {
-            if (!comp.Activated)
-                return;
+    private void OnUseInHand(EntityUid uid, EnergySwordComponent comp, UseInHandEvent args)
+    {
+        if (args.Handled)
+            return;
 
-            // Overrides basic blunt damage with burn+slash as set in yaml
-            args.BonusDamage = comp.LitDamageBonus;
+        args.Handled = true;
+        
+        if (comp.Activated)
+        {
+            var ev = new EnergySwordDeactivatedEvent();
+            RaiseLocalEvent(uid, ref ev);
         }
-
-        private void OnUseInHand(EntityUid uid, EnergySwordComponent comp, UseInHandEvent args)
+        else
         {
-            if (args.Handled)
-                return;
+            var ev = new EnergySwordActivatedEvent();
+            RaiseLocalEvent(uid, ref ev);
+        }
 
-            args.Handled = true;
+        UpdateAppearance(uid, comp);
+    }
 
-            if (comp.Activated)
-            {
-                TurnOff(comp);
-            }
-            else
-            {
-                TurnOn(comp);
-            }
+    private void TurnOff(EntityUid uid, EnergySwordComponent comp, ref EnergySwordDeactivatedEvent args)
+    {
+        if (TryComp(uid, out ItemComponent? item))
+        {
+            _item.SetSize(uid, 5, item);
+        }
 
-            UpdateAppearance(comp);
+        if (TryComp<DisarmMalusComponent>(uid, out var malus))
+        {
+            malus.Malus -= comp.LitDisarmMalus;
         }
 
-        private void TurnOff(EnergySwordComponent comp)
+        if (TryComp<MeleeWeaponComponent>(uid, out var weaponComp))
         {
-            if (!comp.Activated)
-                return;
+            weaponComp.HitSound = comp.OnHitOff;
+            if (comp.Secret)
+                weaponComp.HideFromExamine = true;
+        }
 
-            if (TryComp(comp.Owner, out ItemComponent? item))
-            {
-                _item.SetSize(comp.Owner, 5, item);
-            }
+        if (comp.IsSharp)
+            RemComp<SharpComponent>(uid);
 
-            if (TryComp<DisarmMalusComponent>(comp.Owner, out var malus))
-            {
-                malus.Malus -= comp.litDisarmMalus;
-            }
+        _audio.Play(comp.DeActivateSound, Filter.Pvs(uid, entityManager: EntityManager), uid, true, comp.DeActivateSound.Params);
 
-            if(TryComp<MeleeWeaponComponent>(comp.Owner, out var weaponComp))
-            {
-                weaponComp.HitSound = comp.OnHitOff;
-                if (comp.Secret)
-                    weaponComp.HideFromExamine = true;
-            }
+        comp.Activated = false;
+    }
 
-            if (comp.IsSharp)
-                RemComp<SharpComponent>(comp.Owner);
+    private void TurnOn(EntityUid uid, EnergySwordComponent comp, ref EnergySwordActivatedEvent args)
+    {
+        if (TryComp(uid, out ItemComponent? item))
+        {
+            _item.SetSize(uid, 9999, item);
+        }
 
-            _audio.Play(comp.DeActivateSound, Filter.Pvs(comp.Owner, entityManager: EntityManager), comp.Owner, true, comp.DeActivateSound.Params);
+        if (comp.IsSharp)
+            EnsureComp<SharpComponent>(uid);
 
-            comp.Activated = false;
+        if (TryComp<MeleeWeaponComponent>(uid, out var weaponComp))
+        {
+            weaponComp.HitSound = comp.OnHitOn;
+            if (comp.Secret)
+                weaponComp.HideFromExamine = false;
         }
 
-        private void TurnOn(EnergySwordComponent comp)
+        if (TryComp<DisarmMalusComponent>(uid, out var malus))
         {
-            if (comp.Activated)
-                return;
-
-            if (TryComp(comp.Owner, out ItemComponent? item))
-            {
-                _item.SetSize(comp.Owner, 9999, item);
-            }
-
-            if (comp.IsSharp)
-                EnsureComp<SharpComponent>(comp.Owner);
-
-            if(TryComp<MeleeWeaponComponent>(comp.Owner, out var weaponComp))
-            {
-                weaponComp.HitSound = comp.OnHitOn;
-                if (comp.Secret)
-                    weaponComp.HideFromExamine = false;
-            }
-            _audio.Play(comp.ActivateSound, Filter.Pvs(comp.Owner, entityManager: EntityManager), comp.Owner, true, comp.ActivateSound.Params);
-
-            if (TryComp<DisarmMalusComponent>(comp.Owner, out var malus))
-            {
-                malus.Malus += comp.litDisarmMalus;
-            }
-
-            comp.Activated = true;
+            malus.Malus += comp.LitDisarmMalus;
         }
+        
+        _audio.Play(comp.ActivateSound, Filter.Pvs(uid, entityManager: EntityManager), uid, true, comp.ActivateSound.Params);
 
-        private void UpdateAppearance(EnergySwordComponent component)
-        {
-            if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent))
-                return;
+        comp.Activated = true;
+    }
 
-            _appearance.SetData(component.Owner, ToggleableLightVisuals.Enabled, component.Activated, appearanceComponent);
-            _appearance.SetData(component.Owner, ToggleableLightVisuals.Color, component.BladeColor, appearanceComponent);
-        }
+    private void UpdateAppearance(EntityUid uid, EnergySwordComponent component)
+    {
+        if (!TryComp(uid, out AppearanceComponent? appearanceComponent))
+            return;
 
-        private void OnInteractUsing(EntityUid uid, EnergySwordComponent comp, InteractUsingEvent args)
-        {
-            if (args.Handled)
-                return;
-
-            if (!TryComp(args.Used, out ToolComponent? tool) || !tool.Qualities.ContainsAny("Pulsing"))
-                return;
-
-            args.Handled = true;
-            comp.Hacked = !comp.Hacked;
-
-            if (comp.Hacked)
-            {
-                var rgb = EnsureComp<RgbLightControllerComponent>(uid);
-                _rgbSystem.SetCycleRate(uid, comp.CycleRate, rgb);
-            }
-            else
-                RemComp<RgbLightControllerComponent>(uid);
-        }
-        private void OnIsHotEvent(EntityUid uid, EnergySwordComponent energySword, IsHotEvent args)
+        _appearance.SetData(uid, ToggleableLightVisuals.Enabled, component.Activated, appearanceComponent);
+        _appearance.SetData(uid, ToggleableLightVisuals.Color, component.BladeColor, appearanceComponent);
+    }
+
+    private void OnInteractUsing(EntityUid uid, EnergySwordComponent comp, InteractUsingEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        if (!TryComp(args.Used, out ToolComponent? tool) || !tool.Qualities.ContainsAny("Pulsing"))
+            return;
+
+        args.Handled = true;
+        comp.Hacked = !comp.Hacked;
+
+        if (comp.Hacked)
         {
-            args.IsHot = energySword.Activated;
+            var rgb = EnsureComp<RgbLightControllerComponent>(uid);
+            _rgbSystem.SetCycleRate(uid, comp.CycleRate, rgb);
         }
+        else
+            RemComp<RgbLightControllerComponent>(uid);
+    }
+
+    private void OnIsHotEvent(EntityUid uid, EnergySwordComponent energySword, IsHotEvent args)
+    {
+        args.IsHot = energySword.Activated;
     }
 }
index 6b1a154a830f59c824f5a2cb58605a79957b9ccb..5868a45086e496920b7e9cd808be53621ff0a6c5 100644 (file)
@@ -36,6 +36,7 @@ public sealed partial class GunSystem : SharedGunSystem
     [Dependency] private readonly PricingSystem _pricing = default!;
     [Dependency] private readonly StaminaSystem _stamina = default!;
     [Dependency] private readonly StunSystem _stun = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
 
     public const float DamagePitchVariation = SharedMeleeWeaponSystem.DamagePitchVariation;
     public const float GunClumsyChance = 0.5f;
@@ -192,18 +193,42 @@ public sealed partial class GunSystem : SharedGunSystem
                     ShootProjectile(ent.Value, mapDirection, gunVelocity, user, gun.ProjectileSpeed);
                     break;
                 case HitscanPrototype hitscan:
-                    var ray = new CollisionRay(fromMap.Position, mapDirection.Normalized, hitscan.CollisionMask);
 
-                    var rayCastResults =
-                        Physics.IntersectRay(fromMap.MapId, ray, hitscan.MaxLength, user, false).ToList();
+                    EntityUid? lastHit = null;
 
-                    if (rayCastResults.Count >= 1)
+                    var from = fromMap;
+                    var fromEffect = fromCoordinates; // can't use map coords above because funny FireEffects
+                    var dir = mapDirection.Normalized;
+                    var lastUser = user;
+                    for (var reflectAttempt = 0; reflectAttempt < 3; reflectAttempt++)
                     {
+                        var ray = new CollisionRay(from.Position, dir, hitscan.CollisionMask);
+                        var rayCastResults =
+                            Physics.IntersectRay(from.MapId, ray, hitscan.MaxLength, lastUser, false).ToList();
+                        if (!rayCastResults.Any())
+                            break;
+
                         var result = rayCastResults[0];
-                        var hitEntity = result.HitEntity;
-                        var distance = result.Distance;
-                        FireEffects(fromCoordinates, distance, mapDirection.ToAngle(), hitscan, hitEntity);
+                        var hit = result.HitEntity;
+                        lastHit = hit;
+
+                        FireEffects(fromEffect, result.Distance, dir.Normalized.ToAngle(), hitscan, hit);
+
+                        var ev = new HitScanReflectAttemptEvent(dir, false);
+                        RaiseLocalEvent(hit, ref ev);
+
+                        if (!ev.Reflected)
+                            break;
 
+                        fromEffect = Transform(hit).Coordinates;
+                        from = fromEffect.ToMap(EntityManager, _transform);
+                        dir = ev.Direction;
+                        lastUser = hit;
+                    }
+
+                    if (lastHit != null)
+                    {
+                        EntityUid hitEntity = lastHit.Value;
                         if (hitscan.StaminaDamage > 0f)
                             _stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source:user);
 
@@ -219,7 +244,7 @@ public sealed partial class GunSystem : SharedGunSystem
                             if (!Deleted(hitEntity))
                             {
                                 if (dmg.Total > FixedPoint2.Zero)
-                                    RaiseNetworkEvent(new DamageEffectEvent(Color.Red, new List<EntityUid> {result.HitEntity}), Filter.Pvs(hitEntity, entityManager: EntityManager));
+                                    RaiseNetworkEvent(new DamageEffectEvent(Color.Red, new List<EntityUid> {hitEntity}), Filter.Pvs(hitEntity, entityManager: EntityManager));
 
                                 // TODO get fallback position for playing hit sound.
                                 PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound);
@@ -239,7 +264,7 @@ public sealed partial class GunSystem : SharedGunSystem
                     }
                     else
                     {
-                        FireEffects(fromCoordinates, hitscan.MaxLength, mapDirection.ToAngle(), hitscan);
+                        FireEffects(fromEffect, hitscan.MaxLength, dir.ToAngle(), hitscan);
                     }
 
                     Audio.PlayPredicted(gun.SoundGunshot, gunUid, user);
diff --git a/Content.Server/Weapons/Reflect/ReflectSystem.cs b/Content.Server/Weapons/Reflect/ReflectSystem.cs
new file mode 100644 (file)
index 0000000..3fe8dc8
--- /dev/null
@@ -0,0 +1,26 @@
+using Content.Server.Weapons.Melee.EnergySword;
+using Content.Shared.Weapons.Reflect;
+
+namespace Content.Server.Weapons.Reflect;
+
+public sealed class ReflectSystem : SharedReflectSystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<ReflectComponent, EnergySwordActivatedEvent>(EnableReflect);
+        SubscribeLocalEvent<ReflectComponent, EnergySwordDeactivatedEvent>(DisableReflect);
+    }
+
+    private void EnableReflect(EntityUid uid, ReflectComponent comp, ref EnergySwordActivatedEvent args)
+    {
+        comp.Enabled = true;
+        Dirty(comp);
+    }
+
+    private void DisableReflect(EntityUid uid, ReflectComponent comp, ref EnergySwordDeactivatedEvent args)
+    {
+        comp.Enabled = false;
+        Dirty(comp);
+    }
+}
\ No newline at end of file
index 101bc49eba0dd46159572b7732b2da75b00b7b85..53a271f34cfc756bda3d81e978c7d2bedc694cb6 100644 (file)
@@ -1,5 +1,5 @@
+using Content.Shared.Projectiles;
 using Robust.Shared.Map;
-using Robust.Shared.Physics.Dynamics;
 using Robust.Shared.Physics.Events;
 using Robust.Shared.Serialization;
 
@@ -58,3 +58,9 @@ namespace Content.Shared.Projectiles
         }
     }
 }
+
+/// <summary>
+/// Raised when entity is just about to be hit with projectile but can reflect it
+/// </summary>
+[ByRefEvent]
+public record struct ProjectileReflectAttemptEvent(EntityUid ProjUid, ProjectileComponent Component, bool Cancelled);
diff --git a/Content.Shared/Weapons/Ranged/Events/HitScanReflectAttempt.cs b/Content.Shared/Weapons/Ranged/Events/HitScanReflectAttempt.cs
new file mode 100644 (file)
index 0000000..2bee1e4
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Content.Shared.Weapons.Ranged.Events;
+
+/// <summary>
+/// Shot may be reflected by setting <see cref="Reflected"/> to true
+/// and changing <see cref="Direction"/> where shot will go next
+/// </summary>
+[ByRefEvent]
+public record struct HitScanReflectAttemptEvent(Vector2 Direction, bool Reflected);
diff --git a/Content.Shared/Weapons/Reflect/ReflectComponent.cs b/Content.Shared/Weapons/Reflect/ReflectComponent.cs
new file mode 100644 (file)
index 0000000..d68bc42
--- /dev/null
@@ -0,0 +1,49 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Weapons.Reflect;
+
+/// <summary>
+/// Entities with this component have a chance to reflect projectiles and hitscan shots
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed class ReflectComponent : Component
+{
+    /// <summary>
+    /// Can only reflect when enabled
+    /// </summary>
+    [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)]
+    public bool Enabled = true;
+
+    /// <summary>
+    /// Reflect chance for hitscan weapons (lasers) and projectiles with heat damage (disabler)
+    /// </summary>
+    [DataField("energeticChance"), ViewVariables(VVAccess.ReadWrite)]
+    public float EnergeticChance;
+
+    [DataField("kineticChance"), ViewVariables(VVAccess.ReadWrite)]
+    public float KineticChance;
+
+    [DataField("spread"), ViewVariables(VVAccess.ReadWrite)]
+    public Angle Spread = Angle.FromDegrees(5);
+
+    [DataField("onReflect")]
+    public SoundSpecifier? OnReflect = new SoundPathSpecifier("/Audio/Weapons/Guns/Hits/laser_sear_wall.ogg");
+}
+
+[Serializable, NetSerializable]
+public sealed class ReflectComponentState : ComponentState
+{
+    public bool Enabled;
+    public float EnergeticChance;
+    public float KineticChance;
+    public Angle Spread;
+    public ReflectComponentState(bool enabled, float energeticChance, float kineticChance, Angle spread)
+    {
+        Enabled = enabled;
+        EnergeticChance = energeticChance;
+        KineticChance = kineticChance;
+        Spread = spread;
+    }
+}
diff --git a/Content.Shared/Weapons/Reflect/SharedReflectSystem.cs b/Content.Shared/Weapons/Reflect/SharedReflectSystem.cs
new file mode 100644 (file)
index 0000000..972841f
--- /dev/null
@@ -0,0 +1,107 @@
+using Content.Shared.Audio;
+using Content.Shared.Popups;
+using Robust.Shared.Random;
+using Robust.Shared.Physics.Systems;
+using Content.Shared.Hands.Components;
+using Robust.Shared.GameStates;
+using Content.Shared.Weapons.Ranged.Events;
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Projectiles;
+
+namespace Content.Shared.Weapons.Reflect;
+
+/// <summary>
+/// This handles reflecting projectiles and hitscan shots.
+/// </summary>
+public abstract class SharedReflectSystem : EntitySystem
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<SharedHandsComponent, ProjectileReflectAttemptEvent>(OnHandReflectProjectile);
+        SubscribeLocalEvent<SharedHandsComponent, HitScanReflectAttemptEvent>(OnHandsReflectHitscan);
+
+        SubscribeLocalEvent<ReflectComponent, ComponentHandleState>(OnHandleState);
+        SubscribeLocalEvent<ReflectComponent, ComponentGetState>(OnGetState);
+    }
+
+    private static void OnHandleState(EntityUid uid, ReflectComponent component, ref ComponentHandleState args)
+    {
+        if (args.Current is not ReflectComponentState state) return;
+        component.Enabled = state.Enabled;
+        component.EnergeticChance = state.EnergeticChance;
+        component.KineticChance = state.KineticChance;
+        component.Spread = state.Spread;
+    }
+
+    private static void OnGetState(EntityUid uid, ReflectComponent component, ref ComponentGetState args)
+    {
+        args.State = new ReflectComponentState(component.Enabled, component.EnergeticChance, component.KineticChance, component.Spread);
+    }
+
+    private void OnHandReflectProjectile(EntityUid uid, SharedHandsComponent hands, ref ProjectileReflectAttemptEvent args)
+    {
+        if (args.Cancelled)
+            return;
+        if (TryReflectProjectile(uid, hands.ActiveHandEntity, args.ProjUid, args.Component))
+            args.Cancelled = true;
+    }
+    
+    private bool TryReflectProjectile(EntityUid user, EntityUid? reflector, EntityUid projectile, ProjectileComponent component)
+    {
+        var isEnergyProjectile = component.Damage.DamageDict.ContainsKey("Heat");
+        var isKineticProjectile = !isEnergyProjectile;
+        if (TryComp<ReflectComponent>(reflector, out var reflect) &&
+            reflect.Enabled && 
+            (isEnergyProjectile && _random.Prob(reflect.EnergeticChance) || isKineticProjectile && _random.Prob(reflect.KineticChance)))
+        {
+            var rotation = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2).Opposite();
+
+            var relVel = _physics.GetMapLinearVelocity(projectile) - _physics.GetMapLinearVelocity(user);
+            var newVel = rotation.RotateVec(relVel);
+            _physics.SetLinearVelocity(projectile, newVel);
+
+            var locRot = Transform(projectile).LocalRotation;
+            var newRot = rotation.RotateVec(locRot.ToVec());
+            _transform.SetLocalRotation(projectile, newRot.ToAngle());
+
+            _popup.PopupEntity(Loc.GetString("reflect-shot"), user, PopupType.Small);
+            _audio.PlayPvs(reflect.OnReflect, user, AudioHelpers.WithVariation(0.05f, _random));
+            return true;
+        }
+        return false;
+    }
+
+    private void OnHandsReflectHitscan(EntityUid uid, SharedHandsComponent hands, ref HitScanReflectAttemptEvent args)
+    {
+        if (args.Reflected)
+            return;
+        if (TryReflectHitscan(uid, hands.ActiveHandEntity, args.Direction, out var dir))
+        {
+            args.Direction = dir.Value;
+            args.Reflected = true;
+        }
+    }
+
+    private bool TryReflectHitscan(EntityUid user, EntityUid? reflector, Vector2 direction, [NotNullWhen(true)] out Vector2? newDirection)
+    {
+        if (TryComp<ReflectComponent>(reflector, out var reflect) &&
+            reflect.Enabled &&
+            _random.Prob(reflect.EnergeticChance))
+        {
+            _popup.PopupEntity(Loc.GetString("reflect-shot"), user, PopupType.Small);
+            _audio.PlayPvs(reflect.OnReflect, user, AudioHelpers.WithVariation(0.05f, _random));
+            var spread = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2);
+            newDirection = -spread.RotateVec(direction);
+            return true;
+        }
+        newDirection = null;
+        return false;
+    }
+}
index c63b0563efee451e3cbed3fc4e3fce36ea937c5a..e935cc982e6e143886eb46f6d4bf3aaf4f8b4dc3 100644 (file)
@@ -12,7 +12,7 @@ uplink-rifle-mosin-name = Surplus Rifle
 uplink-rifle-mosin-desc = A bolt action service rifle that has seen many wars. Not modern by any standard, hand loaded, and terrible recoil, but it is cheap.
 
 uplink-esword-name = Energy Sword
-uplink-esword-desc = A very dangerous energy sword. Can be stored in pockets when turned off. Makes a lot of noise when used or turned on.
+uplink-esword-desc = A very dangerous energy sword that can reflect shots. Can be stored in pockets when turned off. Makes a lot of noise when used or turned on.
 
 uplink-edagger-name = Energy Dagger
 uplink-edagger-desc = A small energy blade conveniently disguised in the form of a pen.
diff --git a/Resources/Locale/en-US/weapons/reflect/reflect-component.ftl b/Resources/Locale/en-US/weapons/reflect/reflect-component.ftl
new file mode 100644 (file)
index 0000000..9adc1a9
--- /dev/null
@@ -0,0 +1 @@
+reflect-shot = Reflected!
index b43d1ffeaeb2201049b208fbce47fc6b5eb2587b..a0caee11d29164537b9b01937a88e890f5a16c42 100644 (file)
@@ -50,7 +50,7 @@
   icon: { sprite: /Textures/Objects/Weapons/Melee/e_sword.rsi, state: icon }
   productEntity: EnergySword
   cost:
-    Telecrystal: 6
+    Telecrystal: 8
   categories:
   - UplinkWeapons
 
index eb02ce1a29ab9e007fa4a3b10ad8473ea717f9fa..0a18c6256fb402a750f853f398b6aac7a0c21498 100644 (file)
@@ -2,7 +2,7 @@
   name: energy sword
   parent: BaseItem
   id: EnergySword
-  description: Very loud and very dangerous. Can be stored in pockets when turned off.
+  description: Very loud and very dangerous energy sword that can reflect shots. Can be stored in pockets when turned off.
   components:
   - type: EnergySword
     litDamageBonus:
         shader: unshaded
   - type: DisarmMalus
     malus: 0
+  - type: Reflect
+    enabled: false
+    energeticChance: 0.5
+    kineticChance: 0.25
+    spread: 45
 
 - type: entity
   name: pen