namespace Content.Server.Projectiles;
-[UsedImplicitly]
public sealed class ProjectileSystem : SharedProjectileSystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
var otherName = ToPrettyString(otherEntity);
var direction = args.OurBody.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))
_sharedCameraRecoil.KickCamera(otherEntity, direction);
}
- if (component.DeleteOnCollide)
+ var ev = new ProjectileCollideEvent(uid, false);
+ RaiseLocalEvent(args.OtherEntity, ref ev);
+
+ if (!ev.Cancelled)
{
- QueueDel(uid);
+ component.DamagedEntity = true;
+
+ if (component.DeleteOnCollide)
+ {
+ QueueDel(uid);
+ }
- if (component.ImpactEffect != null && TryComp<TransformComponent>(component.Owner, out var xform))
+ if (component.ImpactEffect != null && TryComp<TransformComponent>(uid, out var xform))
{
RaiseNetworkEvent(new ImpactEffectEvent(component.ImpactEffect, xform.Coordinates), Filter.Pvs(xform.Coordinates, entityMan: EntityManager));
}
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Weapons.Reflect;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Map;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly BatterySystem _battery = default!;
-
public const float DamagePitchVariation = SharedMeleeWeaponSystem.DamagePitchVariation;
public const float GunClumsyChance = 0.5f;
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++)
+
+ if (hitscan.Reflective != ReflectType.None)
{
- 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;
+ 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 hit = result.HitEntity;
- lastHit = hit;
+ var result = rayCastResults[0];
+ var hit = result.HitEntity;
+ lastHit = hit;
- FireEffects(fromEffect, result.Distance, dir.Normalized.ToAngle(), hitscan, hit);
+ FireEffects(fromEffect, result.Distance, dir.Normalized.ToAngle(), hitscan, hit);
- var ev = new HitScanReflectAttemptEvent(dir, false);
- RaiseLocalEvent(hit, ref ev);
+ var ev = new HitScanReflectAttemptEvent(hitscan.Reflective, dir, false);
+ RaiseLocalEvent(hit, ref ev);
- if (!ev.Reflected)
- break;
+ if (!ev.Reflected)
+ break;
- fromEffect = Transform(hit).Coordinates;
- from = fromEffect.ToMap(EntityManager, _transform);
- dir = ev.Direction;
- lastUser = hit;
+ fromEffect = Transform(hit).Coordinates;
+ from = fromEffect.ToMap(EntityManager, _transform);
+ dir = ev.Direction;
+ lastUser = hit;
+ }
}
if (lastHit != null)
if (xformQuery.TryGetComponent(gridUid, out var gridXform))
{
- var (_, gridRot, gridInvMatrix) = TransformSystem.GetWorldPositionRotationInvMatrix(gridUid.Value, xformQuery);
+ var (_, gridRot, gridInvMatrix) = TransformSystem.GetWorldPositionRotationInvMatrix(gridXform, xformQuery);
fromCoordinates = new EntityCoordinates(gridUid.Value,
gridInvMatrix.Transform(fromCoordinates.ToMapPos(EntityManager, TransformSystem)));
--- /dev/null
+namespace Content.Shared.Projectiles;
+
+/// <summary>
+/// Raised directed on what a projectile collides with. Can have its deletion cancelled.
+/// </summary>
+[ByRefEvent]
+public record struct ProjectileCollideEvent(EntityUid OtherEntity, bool Cancelled);
--- /dev/null
+using Content.Shared.Weapons.Reflect;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Weapons.Ranged.Components;
+
+/// <summary>
+/// Can this entity be reflected.
+/// Only applies if it is shot like a projectile and not if it is thrown.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed class ReflectiveComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite), DataField("reflective")]
+ public ReflectType Reflective = ReflectType.NonEnergy;
+}
+using Content.Shared.Weapons.Reflect;
+
namespace Content.Shared.Weapons.Ranged.Events;
/// <summary>
/// and changing <see cref="Direction"/> where shot will go next
/// </summary>
[ByRefEvent]
-public record struct HitScanReflectAttemptEvent(Vector2 Direction, bool Reflected);
+public record struct HitScanReflectAttemptEvent(ReflectType Reflective, Vector2 Direction, bool Reflected);
using Content.Shared.Damage;
using Content.Shared.Physics;
+using Content.Shared.Weapons.Reflect;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
[DataField("collisionMask")]
public int CollisionMask = (int) CollisionGroup.Opaque;
+ /// <summary>
+ /// What we count as for reflection.
+ /// </summary>
+ [DataField("reflective")] public ReflectType Reflective = ReflectType.Energy;
+
/// <summary>
/// Sound that plays upon the thing being hit.
/// </summary>
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
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ReflectComponent : Component
{
/// <summary>
/// Can only reflect when enabled
/// </summary>
- [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField("enabled"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public bool Enabled = true;
+ /// <summary>
+ /// What we reflect.
+ /// </summary>
+ [ViewVariables(VVAccess.ReadWrite), DataField("reflects")]
+ public ReflectType Reflects = ReflectType.Energy | ReflectType.NonEnergy;
+
/// <summary>
/// Probability for a projectile to be reflected.
/// </summary>
- [DataField("reflectProb"), ViewVariables(VVAccess.ReadWrite)]
- public float ReflectProb;
+ [DataField("reflectProb"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ public float ReflectProb = 0.25f;
- [DataField("spread"), ViewVariables(VVAccess.ReadWrite)]
- public Angle Spread = Angle.FromDegrees(5);
+ [DataField("spread"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ public Angle Spread = Angle.FromDegrees(45);
[DataField("soundOnReflect")]
public SoundSpecifier? SoundOnReflect = new SoundPathSpecifier("/Audio/Weapons/Guns/Hits/laser_sear_wall.ogg");
}
-[Serializable, NetSerializable]
-public sealed class ReflectComponentState : ComponentState
+[Flags]
+public enum ReflectType : byte
{
- public bool Enabled;
- public float ReflectProb;
- public Angle Spread;
- public ReflectComponentState(bool enabled, float reflectProb, Angle spread)
- {
- Enabled = enabled;
- ReflectProb = reflectProb;
- Spread = spread;
- }
+ None = 0,
+ NonEnergy = 1 << 0,
+ Energy = 1 << 1,
}
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Audio;
using Content.Shared.Hands.Components;
-using Robust.Shared.GameStates;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Physics.Components;
using Content.Shared.Popups;
+using Content.Shared.Projectiles;
+using Content.Shared.Weapons.Ranged.Components;
+using Robust.Shared.Network;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Random;
/// </summary>
public abstract class SharedReflectSystem : EntitySystem
{
+ [Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
SubscribeLocalEvent<HandsComponent, ProjectileReflectAttemptEvent>(OnHandReflectProjectile);
SubscribeLocalEvent<HandsComponent, HitScanReflectAttemptEvent>(OnHandsReflectHitscan);
- SubscribeLocalEvent<ReflectComponent, ComponentHandleState>(OnHandleState);
- SubscribeLocalEvent<ReflectComponent, ComponentGetState>(OnGetState);
+ SubscribeLocalEvent<ReflectComponent, ProjectileCollideEvent>(OnReflectCollide);
+ SubscribeLocalEvent<ReflectComponent, HitScanReflectAttemptEvent>(OnReflectHitscan);
}
- private static void OnHandleState(EntityUid uid, ReflectComponent component, ref ComponentHandleState args)
+ private void OnReflectCollide(EntityUid uid, ReflectComponent component, ref ProjectileCollideEvent args)
{
- if (args.Current is not ReflectComponentState state)
+ if (args.Cancelled)
+ {
return;
+ }
- component.Enabled = state.Enabled;
- component.ReflectProb = state.ReflectProb;
- component.Spread = state.Spread;
- }
-
- private static void OnGetState(EntityUid uid, ReflectComponent component, ref ComponentGetState args)
- {
- args.State = new ReflectComponentState(component.Enabled, component.ReflectProb, component.Spread);
+ if (TryReflectProjectile(uid, args.OtherEntity, reflect: component))
+ args.Cancelled = true;
}
private void OnHandReflectProjectile(EntityUid uid, HandsComponent hands, ref ProjectileReflectAttemptEvent args)
if (args.Cancelled)
return;
- if (TryReflectProjectile(uid, hands.ActiveHandEntity, args.ProjUid))
+ if (hands.ActiveHandEntity != null && TryReflectProjectile(hands.ActiveHandEntity.Value, args.ProjUid))
args.Cancelled = true;
}
- private bool TryReflectProjectile(EntityUid user, EntityUid? reflector, EntityUid projectile)
+ private bool TryReflectProjectile(EntityUid reflector, EntityUid projectile, ProjectileComponent? projectileComp = null, ReflectComponent? reflect = null)
{
- if (!TryComp<ReflectComponent>(reflector, out var reflect) ||
+ if (!Resolve(reflector, ref reflect, false) ||
!reflect.Enabled ||
+ !TryComp<ReflectiveComponent>(projectile, out var reflective) ||
+ (reflect.Reflects & reflective.Reflective) == 0x0 ||
!_random.Prob(reflect.ReflectProb) ||
!TryComp<PhysicsComponent>(projectile, out var physics))
{
var rotation = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2).Opposite();
var existingVelocity = _physics.GetMapLinearVelocity(projectile, component: physics);
- var relativeVelocity = existingVelocity - _physics.GetMapLinearVelocity(user);
+ var relativeVelocity = existingVelocity - _physics.GetMapLinearVelocity(reflector);
var newVelocity = rotation.RotateVec(relativeVelocity);
// Have the velocity in world terms above so need to convert it back to local.
var newRot = rotation.RotateVec(locRot.ToVec());
_transform.SetLocalRotation(projectile, newRot.ToAngle());
- _popup.PopupEntity(Loc.GetString("reflect-shot"), user);
- _audio.PlayPvs(reflect.SoundOnReflect, user, AudioHelpers.WithVariation(0.05f, _random));
+ if (_netManager.IsServer)
+ {
+ _popup.PopupEntity(Loc.GetString("reflect-shot"), reflector);
+ _audio.PlayPvs(reflect.SoundOnReflect, reflector, AudioHelpers.WithVariation(0.05f, _random));
+ }
+
+ if (Resolve(projectile, ref projectileComp, false))
+ {
+ projectileComp.Shooter = reflector;
+ projectileComp.Weapon = reflector;
+ Dirty(projectileComp);
+ }
+
return true;
}
private void OnHandsReflectHitscan(EntityUid uid, HandsComponent hands, ref HitScanReflectAttemptEvent args)
{
- if (args.Reflected)
+ if (args.Reflected || hands.ActiveHandEntity == null)
+ return;
+
+ if (TryReflectHitscan(hands.ActiveHandEntity.Value, args.Direction, out var dir))
+ {
+ args.Direction = dir.Value;
+ args.Reflected = true;
+ }
+ }
+
+ private void OnReflectHitscan(EntityUid uid, ReflectComponent component, ref HitScanReflectAttemptEvent args)
+ {
+ if (args.Reflected ||
+ (component.Reflects & args.Reflective) == 0x0)
+ {
return;
+ }
- if (TryReflectHitscan(uid, hands.ActiveHandEntity, args.Direction, out var dir))
+ if (TryReflectHitscan(uid, 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)
+ private bool TryReflectHitscan(EntityUid reflector, Vector2 direction,
+ [NotNullWhen(true)] out Vector2? newDirection)
{
- if (TryComp<ReflectComponent>(reflector, out var reflect) &&
- reflect.Enabled &&
- _random.Prob(reflect.ReflectProb))
+ if (!TryComp<ReflectComponent>(reflector, out var reflect) ||
+ !reflect.Enabled ||
+ !_random.Prob(reflect.ReflectProb))
+ {
+ newDirection = null;
+ return false;
+ }
+
+ if (_netManager.IsServer)
{
- _popup.PopupEntity(Loc.GetString("reflect-shot"), user, PopupType.Small);
- _audio.PlayPvs(reflect.SoundOnReflect, user, AudioHelpers.WithVariation(0.05f, _random));
- var spread = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2);
- newDirection = -spread.RotateVec(direction);
- return true;
+ _popup.PopupEntity(Loc.GetString("reflect-shot"), reflector);
+ _audio.PlayPvs(reflect.SoundOnReflect, reflector, AudioHelpers.WithVariation(0.05f, _random));
}
- newDirection = null;
- return false;
+ var spread = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2);
+ newDirection = -spread.RotateVec(direction);
+ return true;
}
}
description: If you can see this you're probably dead!
abstract: true
components:
+ - type: Reflective
- type: FlyBySound
- type: Clickable
- type: Sprite
parent: BaseBullet
noSpawn: true
components:
+ - type: Reflective
+ reflective:
+ - Energy
- type: FlyBySound
sound:
collection: EnergyMiss
noSpawn: true
description: Not too bad, but you still don't want to get hit by it.
components:
+ - type: Reflective
+ reflective:
+ - NonEnergy
- type: Sprite
noRot: false
sprite: Objects/Weapons/Guns/Projectiles/magic.rsi
noSpawn: true
description: Marks a target for additional damage.
components:
+ - type: Reflective
+ reflective:
+ - NonEnergy
- type: Sprite
noRot: false
sprite: Objects/Weapons/Guns/Projectiles/magic.rsi
malus: 0
- type: Reflect
enabled: false
- reflectProb: 0.25
- spread: 45
- type: entity
name: pen
- type: Sprite
sprite: Structures/Decoration/crystal.rsi
state: crystal_green
- netsync: false
+ - type: Reflect
+ reflectProb: 0.5
+ reflects:
+ - Energy
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeCircle
+ radius: 0.45
+ density: 60
+ mask:
+ - MachineMask
+ layer:
+ - MidImpassable
+ - LowImpassable
+ - BulletImpassable
+ - Opaque
- type: PointLight
radius: 3
energy: 3