Make energy sword reflect projectiles and hitscan shots (#14029)

This commit is contained in:
Slava0135
2023-04-02 16:48:32 +03:00
committed by GitHub
parent b8014bc8af
commit 6412289334
16 changed files with 517 additions and 272 deletions

View File

@@ -0,0 +1,7 @@
using Content.Shared.Weapons.Reflect;
namespace Content.Client.Weapons.Reflect;
public sealed class ReflectSystem : SharedReflectSystem
{
}

View File

@@ -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));
}
}
}
}

View File

@@ -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));
}
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -1,6 +1,5 @@
using Content.Server.CombatMode.Disarm; using Content.Server.CombatMode.Disarm;
using Content.Server.Kitchen.Components; using Content.Server.Kitchen.Components;
using Content.Server.Weapons.Melee.EnergySword.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
using Content.Shared.Item; using Content.Shared.Item;
@@ -14,151 +13,150 @@ using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Random; 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!; base.Initialize();
[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() 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);
}
private void OnMapInit(EntityUid uid, EnergySwordComponent comp, MapInitEvent args)
{
if (comp.ColorOptions.Count != 0)
comp.BladeColor = _random.Pick(comp.ColorOptions);
}
private void OnMeleeHit(EntityUid uid, EnergySwordComponent comp, MeleeHitEvent args)
{
if (!comp.Activated)
return;
// Overrides basic blunt damage with burn+slash as set in yaml
args.BonusDamage = comp.LitDamageBonus;
}
private void OnUseInHand(EntityUid uid, EnergySwordComponent comp, UseInHandEvent args)
{
if (args.Handled)
return;
args.Handled = true;
if (comp.Activated)
{ {
base.Initialize(); var ev = new EnergySwordDeactivatedEvent();
RaiseLocalEvent(uid, ref ev);
SubscribeLocalEvent<EnergySwordComponent, MapInitEvent>(OnMapInit); }
SubscribeLocalEvent<EnergySwordComponent, MeleeHitEvent>(OnMeleeHit); else
SubscribeLocalEvent<EnergySwordComponent, UseInHandEvent>(OnUseInHand); {
SubscribeLocalEvent<EnergySwordComponent, InteractUsingEvent>(OnInteractUsing); var ev = new EnergySwordActivatedEvent();
SubscribeLocalEvent<EnergySwordComponent, IsHotEvent>(OnIsHotEvent); RaiseLocalEvent(uid, ref ev);
} }
private void OnMapInit(EntityUid uid, EnergySwordComponent comp, MapInitEvent args) UpdateAppearance(uid, comp);
}
private void TurnOff(EntityUid uid, EnergySwordComponent comp, ref EnergySwordDeactivatedEvent args)
{
if (TryComp(uid, out ItemComponent? item))
{ {
if (comp.ColorOptions.Count != 0) _item.SetSize(uid, 5, item);
comp.BladeColor = _random.Pick(comp.ColorOptions);
} }
private void OnMeleeHit(EntityUid uid, EnergySwordComponent comp, MeleeHitEvent args) if (TryComp<DisarmMalusComponent>(uid, out var malus))
{ {
if (!comp.Activated) malus.Malus -= comp.LitDisarmMalus;
return;
// Overrides basic blunt damage with burn+slash as set in yaml
args.BonusDamage = comp.LitDamageBonus;
} }
private void OnUseInHand(EntityUid uid, EnergySwordComponent comp, UseInHandEvent args) if (TryComp<MeleeWeaponComponent>(uid, out var weaponComp))
{ {
if (args.Handled) weaponComp.HitSound = comp.OnHitOff;
return; if (comp.Secret)
weaponComp.HideFromExamine = true;
args.Handled = true;
if (comp.Activated)
{
TurnOff(comp);
}
else
{
TurnOn(comp);
}
UpdateAppearance(comp);
} }
private void TurnOff(EnergySwordComponent comp) if (comp.IsSharp)
RemComp<SharpComponent>(uid);
_audio.Play(comp.DeActivateSound, Filter.Pvs(uid, entityManager: EntityManager), uid, true, comp.DeActivateSound.Params);
comp.Activated = false;
}
private void TurnOn(EntityUid uid, EnergySwordComponent comp, ref EnergySwordActivatedEvent args)
{
if (TryComp(uid, out ItemComponent? item))
{ {
if (!comp.Activated) _item.SetSize(uid, 9999, item);
return;
if (TryComp(comp.Owner, out ItemComponent? item))
{
_item.SetSize(comp.Owner, 5, item);
}
if (TryComp<DisarmMalusComponent>(comp.Owner, out var malus))
{
malus.Malus -= comp.litDisarmMalus;
}
if(TryComp<MeleeWeaponComponent>(comp.Owner, out var weaponComp))
{
weaponComp.HitSound = comp.OnHitOff;
if (comp.Secret)
weaponComp.HideFromExamine = true;
}
if (comp.IsSharp)
RemComp<SharpComponent>(comp.Owner);
_audio.Play(comp.DeActivateSound, Filter.Pvs(comp.Owner, entityManager: EntityManager), comp.Owner, true, comp.DeActivateSound.Params);
comp.Activated = false;
} }
private void TurnOn(EnergySwordComponent comp) if (comp.IsSharp)
EnsureComp<SharpComponent>(uid);
if (TryComp<MeleeWeaponComponent>(uid, out var weaponComp))
{ {
if (comp.Activated) weaponComp.HitSound = comp.OnHitOn;
return; if (comp.Secret)
weaponComp.HideFromExamine = false;
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;
} }
private void UpdateAppearance(EnergySwordComponent component) if (TryComp<DisarmMalusComponent>(uid, out var malus))
{ {
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) malus.Malus += comp.LitDisarmMalus;
return;
_appearance.SetData(component.Owner, ToggleableLightVisuals.Enabled, component.Activated, appearanceComponent);
_appearance.SetData(component.Owner, ToggleableLightVisuals.Color, component.BladeColor, appearanceComponent);
} }
_audio.Play(comp.ActivateSound, Filter.Pvs(uid, entityManager: EntityManager), uid, true, comp.ActivateSound.Params);
private void OnInteractUsing(EntityUid uid, EnergySwordComponent comp, InteractUsingEvent args) comp.Activated = true;
}
private void UpdateAppearance(EntityUid uid, EnergySwordComponent component)
{
if (!TryComp(uid, out AppearanceComponent? appearanceComponent))
return;
_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)
{ {
if (args.Handled) var rgb = EnsureComp<RgbLightControllerComponent>(uid);
return; _rgbSystem.SetCycleRate(uid, comp.CycleRate, rgb);
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)
{
args.IsHot = energySword.Activated;
} }
else
RemComp<RgbLightControllerComponent>(uid);
}
private void OnIsHotEvent(EntityUid uid, EnergySwordComponent energySword, IsHotEvent args)
{
args.IsHot = energySword.Activated;
} }
} }

View File

@@ -36,6 +36,7 @@ public sealed partial class GunSystem : SharedGunSystem
[Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly StaminaSystem _stamina = default!; [Dependency] private readonly StaminaSystem _stamina = default!;
[Dependency] private readonly StunSystem _stun = default!; [Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public const float DamagePitchVariation = SharedMeleeWeaponSystem.DamagePitchVariation; public const float DamagePitchVariation = SharedMeleeWeaponSystem.DamagePitchVariation;
public const float GunClumsyChance = 0.5f; public const float GunClumsyChance = 0.5f;
@@ -192,18 +193,42 @@ public sealed partial class GunSystem : SharedGunSystem
ShootProjectile(ent.Value, mapDirection, gunVelocity, user, gun.ProjectileSpeed); ShootProjectile(ent.Value, mapDirection, gunVelocity, user, gun.ProjectileSpeed);
break; break;
case HitscanPrototype hitscan: case HitscanPrototype hitscan:
var ray = new CollisionRay(fromMap.Position, mapDirection.Normalized, hitscan.CollisionMask);
var rayCastResults = EntityUid? lastHit = null;
Physics.IntersectRay(fromMap.MapId, ray, hitscan.MaxLength, user, false).ToList();
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 result = rayCastResults[0]; var ray = new CollisionRay(from.Position, dir, hitscan.CollisionMask);
var hitEntity = result.HitEntity; var rayCastResults =
var distance = result.Distance; Physics.IntersectRay(from.MapId, ray, hitscan.MaxLength, lastUser, false).ToList();
FireEffects(fromCoordinates, distance, mapDirection.ToAngle(), hitscan, hitEntity); if (!rayCastResults.Any())
break;
var result = rayCastResults[0];
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) if (hitscan.StaminaDamage > 0f)
_stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source:user); _stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source:user);
@@ -219,7 +244,7 @@ public sealed partial class GunSystem : SharedGunSystem
if (!Deleted(hitEntity)) if (!Deleted(hitEntity))
{ {
if (dmg.Total > FixedPoint2.Zero) 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. // TODO get fallback position for playing hit sound.
PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound); PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound);
@@ -239,7 +264,7 @@ public sealed partial class GunSystem : SharedGunSystem
} }
else else
{ {
FireEffects(fromCoordinates, hitscan.MaxLength, mapDirection.ToAngle(), hitscan); FireEffects(fromEffect, hitscan.MaxLength, dir.ToAngle(), hitscan);
} }
Audio.PlayPredicted(gun.SoundGunshot, gunUid, user); Audio.PlayPredicted(gun.SoundGunshot, gunUid, user);

View File

@@ -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);
}
}

View File

@@ -1,5 +1,5 @@
using Content.Shared.Projectiles;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Events;
using Robust.Shared.Serialization; 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);

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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-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-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-name = Energy Dagger
uplink-edagger-desc = A small energy blade conveniently disguised in the form of a pen. uplink-edagger-desc = A small energy blade conveniently disguised in the form of a pen.

View File

@@ -0,0 +1 @@
reflect-shot = Reflected!

View File

@@ -50,7 +50,7 @@
icon: { sprite: /Textures/Objects/Weapons/Melee/e_sword.rsi, state: icon } icon: { sprite: /Textures/Objects/Weapons/Melee/e_sword.rsi, state: icon }
productEntity: EnergySword productEntity: EnergySword
cost: cost:
Telecrystal: 6 Telecrystal: 8
categories: categories:
- UplinkWeapons - UplinkWeapons

View File

@@ -2,7 +2,7 @@
name: energy sword name: energy sword
parent: BaseItem parent: BaseItem
id: EnergySword 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: components:
- type: EnergySword - type: EnergySword
litDamageBonus: litDamageBonus:
@@ -51,6 +51,11 @@
shader: unshaded shader: unshaded
- type: DisarmMalus - type: DisarmMalus
malus: 0 malus: 0
- type: Reflect
enabled: false
energeticChance: 0.5
kineticChance: 0.25
spread: 45
- type: entity - type: entity
name: pen name: pen