From 64122893349f2d8b505995e147364ece14c28c47 Mon Sep 17 00:00:00 2001 From: Slava0135 <40753025+Slava0135@users.noreply.github.com> Date: Sun, 2 Apr 2023 16:48:32 +0300 Subject: [PATCH] Make energy sword reflect projectiles and hitscan shots (#14029) --- .../Weapons/Reflect/ReflectSystem.cs | 7 + .../Projectiles/ProjectileSystem.cs | 87 +++++++ .../Projectiles/SharedProjectileSystem.cs | 79 ------ .../Components/EnergySwordComponent.cs | 58 ----- .../Melee/EnergySword/EnergySwordComponent.cs | 63 +++++ .../Melee/EnergySword/EnergySwordSystem.cs | 232 +++++++++--------- .../Weapons/Ranged/Systems/GunSystem.cs | 43 +++- .../Weapons/Reflect/ReflectSystem.cs | 26 ++ .../Projectiles/SharedProjectileSystem.cs | 8 +- .../Ranged/Events/HitScanReflectAttempt.cs | 8 + .../Weapons/Reflect/ReflectComponent.cs | 49 ++++ .../Weapons/Reflect/SharedReflectSystem.cs | 107 ++++++++ .../Locale/en-US/store/uplink-catalog.ftl | 2 +- .../weapons/reflect/reflect-component.ftl | 1 + .../Prototypes/Catalog/uplink_catalog.yml | 2 +- .../Objects/Weapons/Melee/e_sword.yml | 7 +- 16 files changed, 512 insertions(+), 267 deletions(-) create mode 100644 Content.Client/Weapons/Reflect/ReflectSystem.cs create mode 100644 Content.Server/Projectiles/ProjectileSystem.cs delete mode 100644 Content.Server/Projectiles/SharedProjectileSystem.cs delete mode 100644 Content.Server/Weapons/Melee/EnergySword/Components/EnergySwordComponent.cs create mode 100644 Content.Server/Weapons/Melee/EnergySword/EnergySwordComponent.cs create mode 100644 Content.Server/Weapons/Reflect/ReflectSystem.cs create mode 100644 Content.Shared/Weapons/Ranged/Events/HitScanReflectAttempt.cs create mode 100644 Content.Shared/Weapons/Reflect/ReflectComponent.cs create mode 100644 Content.Shared/Weapons/Reflect/SharedReflectSystem.cs create mode 100644 Resources/Locale/en-US/weapons/reflect/reflect-component.ftl diff --git a/Content.Client/Weapons/Reflect/ReflectSystem.cs b/Content.Client/Weapons/Reflect/ReflectSystem.cs new file mode 100644 index 0000000000..46f00a4d70 --- /dev/null +++ b/Content.Client/Weapons/Reflect/ReflectSystem.cs @@ -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 index 0000000000..659de57e47 --- /dev/null +++ b/Content.Server/Projectiles/ProjectileSystem.cs @@ -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(OnStartCollide); + SubscribeLocalEvent(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 {otherEntity}), Filter.Pvs(otherEntity, entityManager: EntityManager)); + } + + _adminLogger.Add(LogType.BulletHit, + HasComp(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(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 index 64c1706d88..0000000000 --- a/Content.Server/Projectiles/SharedProjectileSystem.cs +++ /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(OnStartCollide); - SubscribeLocalEvent(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 {otherEntity}), Filter.Pvs(otherEntity, entityManager: EntityManager)); - } - - _adminLogger.Add(LogType.BulletHit, - HasComp(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(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 index b482c6c738..0000000000 --- a/Content.Server/Weapons/Melee/EnergySword/Components/EnergySwordComponent.cs +++ /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; - - /// - /// Does this become hidden when deactivated - /// - [DataField("secret")] - public bool Secret { get; set; } = false; - - /// - /// RGB cycle rate for hacked e-swords. - /// - [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 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 index 0000000000..a6378ea28b --- /dev/null +++ b/Content.Server/Weapons/Melee/EnergySword/EnergySwordComponent.cs @@ -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; + + /// + /// Does this become hidden when deactivated + /// + [DataField("secret")] + public bool Secret { get; set; } = false; + + /// + /// RGB cycle rate for hacked e-swords. + /// + [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 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(); diff --git a/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs b/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs index 6ebb293ece..823ee642cf 100644 --- a/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs +++ b/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs @@ -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(OnMapInit); + SubscribeLocalEvent(OnMeleeHit); + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnIsHotEvent); + SubscribeLocalEvent(TurnOff); + SubscribeLocalEvent(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(OnMapInit); - SubscribeLocalEvent(OnMeleeHit); - SubscribeLocalEvent(OnUseInHand); - SubscribeLocalEvent(OnInteractUsing); - SubscribeLocalEvent(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(uid, out var malus)) + { + malus.Malus -= comp.LitDisarmMalus; } - private void TurnOff(EnergySwordComponent comp) + if (TryComp(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(uid); - if (TryComp(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(comp.Owner, out var weaponComp)) - { - weaponComp.HitSound = comp.OnHitOff; - if (comp.Secret) - weaponComp.HideFromExamine = true; - } + comp.Activated = false; + } - if (comp.IsSharp) - RemComp(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(uid); - comp.Activated = false; + if (TryComp(uid, out var weaponComp)) + { + weaponComp.HitSound = comp.OnHitOn; + if (comp.Secret) + weaponComp.HideFromExamine = false; } - private void TurnOn(EnergySwordComponent comp) + if (TryComp(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(comp.Owner); - - if(TryComp(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(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(uid); - _rgbSystem.SetCycleRate(uid, comp.CycleRate, rgb); - } - else - RemComp(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(uid); + _rgbSystem.SetCycleRate(uid, comp.CycleRate, rgb); } + else + RemComp(uid); + } + + private void OnIsHotEvent(EntityUid uid, EnergySwordComponent energySword, IsHotEvent args) + { + args.IsHot = energySword.Activated; } } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs index 6b1a154a83..5868a45086 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -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 {result.HitEntity}), Filter.Pvs(hitEntity, entityManager: EntityManager)); + RaiseNetworkEvent(new DamageEffectEvent(Color.Red, new List {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 index 0000000000..3fe8dc8028 --- /dev/null +++ b/Content.Server/Weapons/Reflect/ReflectSystem.cs @@ -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(EnableReflect); + SubscribeLocalEvent(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 diff --git a/Content.Shared/Projectiles/SharedProjectileSystem.cs b/Content.Shared/Projectiles/SharedProjectileSystem.cs index 101bc49eba..53a271f34c 100644 --- a/Content.Shared/Projectiles/SharedProjectileSystem.cs +++ b/Content.Shared/Projectiles/SharedProjectileSystem.cs @@ -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 } } } + +/// +/// Raised when entity is just about to be hit with projectile but can reflect it +/// +[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 index 0000000000..2bee1e41b4 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Events/HitScanReflectAttempt.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.Weapons.Ranged.Events; + +/// +/// Shot may be reflected by setting to true +/// and changing where shot will go next +/// +[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 index 0000000000..d68bc42a0b --- /dev/null +++ b/Content.Shared/Weapons/Reflect/ReflectComponent.cs @@ -0,0 +1,49 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Weapons.Reflect; + +/// +/// Entities with this component have a chance to reflect projectiles and hitscan shots +/// +[RegisterComponent, NetworkedComponent] +public sealed class ReflectComponent : Component +{ + /// + /// Can only reflect when enabled + /// + [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)] + public bool Enabled = true; + + /// + /// Reflect chance for hitscan weapons (lasers) and projectiles with heat damage (disabler) + /// + [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 index 0000000000..972841f1b9 --- /dev/null +++ b/Content.Shared/Weapons/Reflect/SharedReflectSystem.cs @@ -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; + +/// +/// This handles reflecting projectiles and hitscan shots. +/// +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(OnHandReflectProjectile); + SubscribeLocalEvent(OnHandsReflectHitscan); + + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(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(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(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; + } +} diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl index c63b0563ef..e935cc982e 100644 --- a/Resources/Locale/en-US/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/store/uplink-catalog.ftl @@ -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 index 0000000000..9adc1a97f8 --- /dev/null +++ b/Resources/Locale/en-US/weapons/reflect/reflect-component.ftl @@ -0,0 +1 @@ +reflect-shot = Reflected! diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index b43d1ffeae..a0caee11d2 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -50,7 +50,7 @@ icon: { sprite: /Textures/Objects/Weapons/Melee/e_sword.rsi, state: icon } productEntity: EnergySword cost: - Telecrystal: 6 + Telecrystal: 8 categories: - UplinkWeapons diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml index eb02ce1a29..0a18c6256f 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml @@ -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: @@ -51,6 +51,11 @@ shader: unshaded - type: DisarmMalus malus: 0 + - type: Reflect + enabled: false + energeticChance: 0.5 + kineticChance: 0.25 + spread: 45 - type: entity name: pen -- 2.51.2