using Content.Client.Weapons.Ranged.Components;
using Content.Shared.Camera;
using Content.Shared.CombatMode;
+using Content.Shared.Damage;
+using Content.Shared.Weapons.Hitscan.Components;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Shared.Animations;
+using Robust.Shared.Audio;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
continue;
}
+ // TODO: Clean this up in a gun refactor at some point - too much copy pasting
switch (shootable)
{
case CartridgeAmmoComponent cartridge:
else
RemoveShootable(ent.Value);
break;
- case HitscanPrototype:
+ case HitscanAmmoComponent:
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
Recoil(user, direction, gun.CameraRecoilScalarModified);
break;
_animPlayer.Stop(gunUid, uidPlayer, "muzzle-flash-light");
_animPlayer.Play((gunUid, uidPlayer), animTwo, "muzzle-flash-light");
}
+
+ // TODO: Move RangedDamageSoundComponent to shared so this can be predicted.
+ public override void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) {}
}
-using System.Linq;
using System.Numerics;
using Content.Server.Cargo.Systems;
using Content.Server.Weapons.Ranged.Components;
using Content.Shared.Cargo;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
-using Content.Shared.Database;
-using Content.Shared.Effects;
using Content.Shared.Projectiles;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Weapons.Ranged.Systems;
-using Content.Shared.Weapons.Reflect;
-using Content.Shared.Damage.Components;
+using Content.Shared.Weapons.Hitscan.Components;
+using Content.Shared.Weapons.Hitscan.Events;
using Robust.Shared.Audio;
using Robust.Shared.Map;
-using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
-using Robust.Shared.Containers;
namespace Content.Server.Weapons.Ranged.Systems;
{
[Dependency] private readonly DamageExamineSystem _damageExamine = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
- [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
- [Dependency] private readonly SharedStaminaSystem _stamina = default!;
- [Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
private const float DamagePitchVariation = 0.05f;
continue;
}
+ // TODO: Clean this up in a gun refactor at some point - too much copy pasting
switch (shootable)
{
// Cartridge shoots something else
CreateAndFireProjectiles(ent.Value, newAmmo);
break;
- case HitscanPrototype hitscan:
-
- EntityUid? lastHit = null;
-
- var from = fromMap;
- // can't use map coords above because funny FireEffects
- var fromEffect = fromCoordinates;
- var dir = mapDirection.Normalized();
-
- //in the situation when user == null, means that the cannon fires on its own (via signals). And we need the gun to not fire by itself in this case
- var lastUser = user ?? gunUid;
-
- if (hitscan.Reflective != ReflectType.None)
- {
- 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];
-
- // Check if laser is shot from in a container
- if (!_container.IsEntityOrParentInContainer(lastUser))
- {
- // Checks if the laser should pass over unless targeted by its user
- foreach (var collide in rayCastResults)
- {
- if (collide.HitEntity != gun.Target &&
- CompOrNull<RequireProjectileTargetComponent>(collide.HitEntity)?.Active == true)
- {
- continue;
- }
-
- result = collide;
- break;
- }
- }
-
- var hit = result.HitEntity;
- lastHit = hit;
-
- FireEffects(fromEffect, result.Distance, dir.Normalized().ToAngle(), hitscan, hit);
-
- var ev = new HitScanReflectAttemptEvent(user, gunUid, hitscan.Reflective, dir, false);
- RaiseLocalEvent(hit, ref ev);
-
- if (!ev.Reflected)
- break;
-
- fromEffect = Transform(hit).Coordinates;
- from = TransformSystem.ToMapCoordinates(fromEffect);
- dir = ev.Direction;
- lastUser = hit;
- }
- }
+ case HitscanAmmoComponent:
+ if (ent == null)
+ break;
- if (lastHit != null)
+ var hitscanEv = new HitscanTraceEvent
{
- var hitEntity = lastHit.Value;
- if (hitscan.StaminaDamage > 0f)
- _stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source: user);
+ FromCoordinates = fromCoordinates,
+ ShotDirection = mapDirection.Normalized(),
+ Gun = gunUid,
+ Shooter = user,
+ Target = gun.Target,
+ };
+ RaiseLocalEvent(ent.Value, ref hitscanEv);
- var dmg = hitscan.Damage;
-
- var hitName = ToPrettyString(hitEntity);
- if (dmg != null)
- dmg = Damageable.TryChangeDamage(hitEntity, dmg * Damageable.UniversalHitscanDamageModifier, origin: user);
-
- // check null again, as TryChangeDamage returns modified damage values
- if (dmg != null)
- {
- if (!Deleted(hitEntity))
- {
- if (dmg.AnyPositive())
- {
- _color.RaiseEffect(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);
- }
-
- if (user != null)
- {
- Logs.Add(LogType.HitScanHit,
- $"{ToPrettyString(user.Value):user} hit {hitName:target} using hitscan and dealt {dmg.GetTotal():damage} damage");
- }
- else
- {
- Logs.Add(LogType.HitScanHit,
- $"{hitName:target} hit by hitscan dealing {dmg.GetTotal():damage} damage");
- }
- }
- }
- else
- {
- FireEffects(fromEffect, hitscan.MaxLength, dir.ToAngle(), hitscan);
- }
+ Del(ent);
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
break;
RaiseNetworkEvent(message, filter);
}
- public void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound)
+ public override void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound)
{
DebugTools.Assert(!Deleted(otherEntity), "Impact sound entity was deleted");
Audio.PlayPvs(weaponSound, otherEntity);
}
}
-
- // TODO: Pseudo RNG so the client can predict these.
- #region Hitscan effects
-
- private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle angle, HitscanPrototype hitscan, EntityUid? hitEntity = null)
- {
- // Lord
- // Forgive me for the shitcode I am about to do
- // Effects tempt me not
- var sprites = new List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>();
- var fromXform = Transform(fromCoordinates.EntityId);
-
- // We'll get the effects relative to the grid / map of the firer
- // Look you could probably optimise this a bit with redundant transforms at this point.
-
- var gridUid = fromXform.GridUid;
- if (gridUid != fromCoordinates.EntityId && TryComp(gridUid, out TransformComponent? gridXform))
- {
- var (_, gridRot, gridInvMatrix) = TransformSystem.GetWorldPositionRotationInvMatrix(gridXform);
- var map = TransformSystem.ToMapCoordinates(fromCoordinates);
- fromCoordinates = new EntityCoordinates(gridUid.Value, Vector2.Transform(map.Position, gridInvMatrix));
- angle -= gridRot;
- }
- else
- {
- angle -= TransformSystem.GetWorldRotation(fromXform);
- }
-
- if (distance >= 1f)
- {
- if (hitscan.MuzzleFlash != null)
- {
- var coords = fromCoordinates.Offset(angle.ToVec().Normalized() / 2);
- var netCoords = GetNetCoordinates(coords);
-
- sprites.Add((netCoords, angle, hitscan.MuzzleFlash, 1f));
- }
-
- if (hitscan.TravelFlash != null)
- {
- var coords = fromCoordinates.Offset(angle.ToVec() * (distance + 0.5f) / 2);
- var netCoords = GetNetCoordinates(coords);
-
- sprites.Add((netCoords, angle, hitscan.TravelFlash, distance - 1.5f));
- }
- }
-
- if (hitscan.ImpactFlash != null)
- {
- var coords = fromCoordinates.Offset(angle.ToVec() * distance);
- var netCoords = GetNetCoordinates(coords);
-
- sprites.Add((netCoords, angle.FlipPositive(), hitscan.ImpactFlash, 1f));
- }
-
- if (sprites.Count > 0)
- {
- RaiseNetworkEvent(new HitscanEvent
- {
- Sprites = sprites,
- }, Filter.Pvs(fromCoordinates, entityMan: EntityManager));
- }
- }
-
- #endregion
}
--- /dev/null
+using Content.Shared.Weapons.Ranged;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Weapons.Hitscan.Components;
+
+/// <summary>
+/// This component is used to indicate an entity is shootable from a hitscan weapon.
+/// This is placed on the laser entity being shot, not the gun itself.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class HitscanAmmoComponent : Component, IShootable;
--- /dev/null
+using Content.Shared.Damage;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Weapons.Hitscan.Components;
+
+/// <summary>
+/// Hitscan entities that have this component will do the damage specified to hit targets (Who didn't reflect it).
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class HitscanBasicDamageComponent : Component
+{
+ /// <summary>
+ /// How much damage the hitscan weapon will do when hitting a target.
+ /// </summary>
+ [DataField(required: true)]
+ public DamageSpecifier Damage;
+}
--- /dev/null
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Weapons.Hitscan.Components;
+
+/// <summary>
+/// System or basic "effects" like sounds and hit markers for hitscans.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class HitscanBasicEffectsComponent : Component
+{
+ /// <summary>
+ /// This will turn hit entities this color briefly.
+ /// </summary>
+ [DataField]
+ public Color? HitColor = Color.Red;
+
+ /// <summary>
+ /// Sound that plays upon the thing being hit.
+ /// </summary>
+ [DataField]
+ public SoundSpecifier? Sound;
+
+ /// <summary>
+ /// Force the hitscan sound to play rather than playing the entity's override sound (if it exists).
+ /// </summary>
+ [DataField]
+ public bool ForceSound;
+}
--- /dev/null
+using Content.Shared.Physics;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Weapons.Hitscan.Components;
+
+/// <summary>
+/// A basic raycast system that will shoot in a straight line when triggered.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class HitscanBasicRaycastComponent : Component
+{
+ /// <summary>
+ /// Maximum distance the raycast will travel before giving up. Reflections will reset the distance traveled
+ /// </summary>
+ [DataField]
+ public float MaxDistance = 20.0f;
+
+ /// <summary>
+ /// The collision mask the hitscan ray uses to collide with other objects. See the enum for more information
+ /// </summary>
+ [DataField]
+ public CollisionGroup CollisionMask = CollisionGroup.Opaque;
+}
--- /dev/null
+using Robust.Shared.GameStates;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Weapons.Hitscan.Components;
+
+/// <summary>
+/// Provides basic visuals for hitscan weapons - works with <see cref="HitscanBasicRaycastComponent"/>
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class HitscanBasicVisualsComponent : Component
+{
+ /// <summary>
+ /// The muzzle flash from the hitscan weapon.
+ /// </summary>
+ [DataField]
+ public SpriteSpecifier? MuzzleFlash;
+
+ /// <summary>
+ /// The "travel" sprite, this gets repeated until it hits the target.
+ /// </summary>
+ [DataField]
+ public SpriteSpecifier? TravelFlash;
+
+ /// <summary>
+ /// The sprite that gets shown on the impact of the laser.
+ /// </summary>
+ [DataField]
+ public SpriteSpecifier? ImpactFlash;
+}
--- /dev/null
+using Content.Shared.Weapons.Reflect;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Weapons.Hitscan.Components;
+
+/// <summary>
+/// Hitscan entities with this component will get reflected by certain things (E.G energy swords).
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class HitscanReflectComponent : Component
+{
+ /// <summary>
+ /// The reflective type, will only reflect from entities that have a matching reflection type.
+ /// </summary>
+ [DataField]
+ public ReflectType ReflectiveType = ReflectType.Energy;
+
+ /// <summary>
+ /// The maximum number of reflections the laser will make. <see cref="CurrentReflections"/>
+ /// </summary>
+ [DataField]
+ public int MaxReflections = 3;
+
+ /// <summary>
+ /// Current number of times this hitscan entity was reflected. Will not be more than <see cref="MaxReflections"/>
+ /// </summary>
+ [DataField]
+ public int CurrentReflections;
+}
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Weapons.Hitscan.Components;
+
+/// <summary>
+/// Hitscan entities that have this component will deal stamina damage to the target.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class HitscanStaminaDamageComponent : Component
+{
+ /// <summary>
+ /// How much stamania damage the hitscan weapon will do when hitting a target.
+ /// </summary>
+ [DataField]
+ public float StaminaDamage = 10.0f;
+}
--- /dev/null
+using System.Numerics;
+using Content.Shared.Damage;
+using Robust.Shared.Map;
+
+namespace Content.Shared.Weapons.Hitscan.Events;
+
+/// <summary>
+/// Raised on the hitscan entity when "fired". This could be from reflections or from the gun. This is the catalyst that
+/// other systems will listen for to actually shoot the gun.
+/// </summary>
+[ByRefEvent]
+public record struct HitscanTraceEvent
+{
+ /// <summary>
+ /// Location the hitscan was fired from.
+ /// </summary>
+ public EntityCoordinates FromCoordinates;
+
+ /// <summary>
+ /// Direction that the ray was fired towards.
+ /// </summary>
+ public Vector2 ShotDirection;
+
+ /// <summary>
+ /// Gun that was fired - this will always be the original weapon even if reflected.
+ /// </summary>
+ public EntityUid Gun;
+
+ /// <summary>
+ /// Player who shot the gun, if null the gun was fired by itself.
+ /// </summary>
+ public EntityUid? Shooter;
+
+ /// <summary>
+ /// Target that was being aimed at (Not necessarily hit).
+ /// </summary>
+ public EntityUid? Target;
+}
+
+/// <summary>
+/// All data known data for when a hitscan is actually fired.
+/// </summary>
+public record struct HitscanRaycastFiredData
+{
+ /// <summary>
+ /// Direction that the ray was fired towards.
+ /// </summary>
+ public Vector2 ShotDirection;
+
+ /// <summary>
+ /// The entity that got hit, if null the raycast didn't hit anyone.
+ /// </summary>
+ public EntityUid? HitEntity;
+
+ /// <summary>
+ /// Gun that fired the raycast.
+ /// </summary>
+ public EntityUid Gun;
+
+ /// <summary>
+ /// Player who shot the gun, if null the gun was fired by itself.
+ /// </summary>
+ public EntityUid? Shooter;
+}
+
+/// <summary>
+/// Try to hit the targeted entity with a hitscan laser. Stuff like the reflection system should listen for this and
+/// cancel the event if the laser was reflected.
+/// </summary>
+[ByRefEvent]
+public struct AttemptHitscanRaycastFiredEvent
+{
+ /// <summary>
+ /// Data for the hitscan that was fired.
+ /// </summary>
+ public HitscanRaycastFiredData Data;
+
+ /// <summary>
+ /// Set to true the hitscan is cancelled (e.g. due to reflection).
+ /// Cancelled hitscans should not apply damage or trigger follow-up effects.
+ /// </summary>
+ public bool Cancelled;
+}
+
+/// <summary>
+/// Results of a hitscan raycast and will be raised on the raycast entity on itself. Stuff like the damage system should
+/// listen for this. At this point we KNOW the laser hit the entity.
+/// </summary>
+[ByRefEvent]
+public struct HitscanRaycastFiredEvent
+{
+ /// <summary>
+ /// Data for the hitscan that was fired.
+ /// </summary>
+ public HitscanRaycastFiredData Data;
+}
+
+[ByRefEvent]
+public record struct HitscanDamageDealtEvent
+{
+ /// <summary>
+ /// Target that was dealt damage.
+ /// </summary>
+ public EntityUid Target;
+
+ /// <summary>
+ /// The amount of damage that the target was dealt.
+ /// </summary>
+ public DamageSpecifier DamageDealt;
+}
--- /dev/null
+using Content.Shared.Damage;
+using Content.Shared.Weapons.Hitscan.Components;
+using Content.Shared.Weapons.Hitscan.Events;
+
+namespace Content.Shared.Weapons.Hitscan.Systems;
+
+public sealed class HitscanBasicDamageSystem : EntitySystem
+{
+ [Dependency] private readonly DamageableSystem _damage = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<HitscanBasicDamageComponent, HitscanRaycastFiredEvent>(OnHitscanHit);
+ }
+
+ private void OnHitscanHit(Entity<HitscanBasicDamageComponent> ent, ref HitscanRaycastFiredEvent args)
+ {
+ if (args.Data.HitEntity == null)
+ return;
+
+ var dmg = ent.Comp.Damage * _damage.UniversalHitscanDamageModifier;
+
+ var damageDealt = _damage.TryChangeDamage(args.Data.HitEntity, dmg, origin: args.Data.Gun);
+
+ if (damageDealt == null)
+ return;
+
+ var damageEvent = new HitscanDamageDealtEvent
+ {
+ Target = args.Data.HitEntity.Value,
+ DamageDealt = damageDealt,
+ };
+
+ RaiseLocalEvent(ent, ref damageEvent);
+ }
+}
--- /dev/null
+using Content.Shared.Damage;
+using Content.Shared.Effects;
+using Content.Shared.Weapons.Hitscan.Components;
+using Content.Shared.Weapons.Hitscan.Events;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Shared.Player;
+
+namespace Content.Shared.Weapons.Hitscan.Systems;
+
+public sealed class HitscanBasicEffectsSystem : EntitySystem
+{
+ [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
+ [Dependency] private readonly SharedGunSystem _gun = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<HitscanBasicEffectsComponent, HitscanDamageDealtEvent>(OnHitscanDamageDealt);
+ }
+
+ private void OnHitscanDamageDealt(Entity<HitscanBasicEffectsComponent> ent, ref HitscanDamageDealtEvent args)
+ {
+ if (Deleted(args.Target))
+ return;
+
+ if (ent.Comp.HitColor != null && args.DamageDealt.GetTotal() != 0)
+ {
+ _color.RaiseEffect(ent.Comp.HitColor.Value,
+ new List<EntityUid> { args.Target },
+ Filter.Pvs(args.Target, entityManager: EntityManager));
+ }
+
+ _gun.PlayImpactSound(args.Target, args.DamageDealt, ent.Comp.Sound, ent.Comp.ForceSound);
+ }
+}
--- /dev/null
+using System.Numerics;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Damage.Components;
+using Content.Shared.Database;
+using Content.Shared.Weapons.Hitscan.Components;
+using Content.Shared.Weapons.Hitscan.Events;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Weapons.Hitscan.Systems;
+
+public sealed class HitscanBasicRaycastSystem : EntitySystem
+{
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly ISharedAdminLogManager _log = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+ private EntityQuery<HitscanBasicVisualsComponent> _visualsQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _visualsQuery = GetEntityQuery<HitscanBasicVisualsComponent>();
+
+ SubscribeLocalEvent<HitscanBasicRaycastComponent, HitscanTraceEvent>(OnHitscanFired);
+ }
+
+ private void OnHitscanFired(Entity<HitscanBasicRaycastComponent> ent, ref HitscanTraceEvent args)
+ {
+ var shooter = args.Shooter ?? args.Gun;
+ var mapCords = _transform.ToMapCoordinates(args.FromCoordinates);
+ var ray = new CollisionRay(mapCords.Position, args.ShotDirection, (int) ent.Comp.CollisionMask);
+ var rayCastResults = _physics.IntersectRay(mapCords.MapId, ray, ent.Comp.MaxDistance, shooter, false);
+
+ var target = args.Target;
+ // If you are in a container, use the raycast result
+ // Otherwise:
+ // 1.) Hit the first entity that you targeted.
+ // 2.) Hit the first entity that doesn't require you to aim at it specifically to be hit.
+ var result = _container.IsEntityOrParentInContainer(shooter)
+ ? rayCastResults.FirstOrNull()
+ : rayCastResults.FirstOrNull(hit => hit.HitEntity == target
+ || CompOrNull<RequireProjectileTargetComponent>(hit.HitEntity)?.Active != true);
+
+ var distanceTried = result?.Distance ?? ent.Comp.MaxDistance;
+
+ // Do visuals without an event. They should always happen and putting it on the attempt event is weird!
+ // If more stuff gets added here, it should probably be turned into an event.
+ FireEffects(args.FromCoordinates, distanceTried, args.ShotDirection.ToAngle(), ent.Owner);
+
+ // Admin logging
+ if (result?.HitEntity != null)
+ {
+ _log.Add(LogType.HitScanHit,
+ $"{ToPrettyString(shooter):user} hit {ToPrettyString(result.Value.HitEntity):target}"
+ + $" using {ToPrettyString(args.Gun):entity}.");
+ }
+
+ var data = new HitscanRaycastFiredData
+ {
+ ShotDirection = args.ShotDirection,
+ Gun = args.Gun,
+ Shooter = args.Shooter,
+ HitEntity = result?.HitEntity,
+ };
+
+ var attemptEvent = new AttemptHitscanRaycastFiredEvent { Data = data };
+ RaiseLocalEvent(ent, ref attemptEvent);
+
+ if (attemptEvent.Cancelled)
+ return;
+
+ var hitEvent = new HitscanRaycastFiredEvent { Data = data };
+ RaiseLocalEvent(ent, ref hitEvent);
+ }
+
+ /// <summary>
+ /// Create visual effects for the fired hitscan weapon.
+ /// </summary>
+ /// <param name="fromCoordinates">Location to start the effect.</param>
+ /// <param name="distance">Distance of the hitscan shot.</param>
+ /// <param name="shotAngle">Angle of the shot.</param>
+ /// <param name="hitscanUid">The hitscan entity itself.</param>
+ private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle shotAngle, EntityUid hitscanUid)
+ {
+ if (distance == 0 || !_visualsQuery.TryComp(hitscanUid, out var vizComp))
+ return;
+
+ var sprites = new List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>();
+ var fromXform = Transform(fromCoordinates.EntityId);
+
+ // We'll get the effects relative to the grid / map of the firer
+ // Look you could probably optimise this a bit with redundant transforms at this point.
+
+ var gridUid = fromXform.GridUid;
+ if (gridUid != fromCoordinates.EntityId && TryComp(gridUid, out TransformComponent? gridXform))
+ {
+ var (_, gridRot, gridInvMatrix) = _transform.GetWorldPositionRotationInvMatrix(gridXform);
+ var map = _transform.ToMapCoordinates(fromCoordinates);
+ fromCoordinates = new EntityCoordinates(gridUid.Value, Vector2.Transform(map.Position, gridInvMatrix));
+ shotAngle -= gridRot;
+ }
+ else
+ {
+ shotAngle -= _transform.GetWorldRotation(fromXform);
+ }
+
+ if (distance >= 1f)
+ {
+ if (vizComp.MuzzleFlash != null)
+ {
+ var coords = fromCoordinates.Offset(shotAngle.ToVec().Normalized() / 2);
+ var netCoords = GetNetCoordinates(coords);
+
+ sprites.Add((netCoords, shotAngle, vizComp.MuzzleFlash, 1f));
+ }
+
+ if (vizComp.TravelFlash != null)
+ {
+ var coords = fromCoordinates.Offset(shotAngle.ToVec() * (distance + 0.5f) / 2);
+ var netCoords = GetNetCoordinates(coords);
+
+ sprites.Add((netCoords, shotAngle, vizComp.TravelFlash, distance - 1.5f));
+ }
+ }
+
+ if (vizComp.ImpactFlash != null)
+ {
+ var coords = fromCoordinates.Offset(shotAngle.ToVec() * distance);
+ var netCoords = GetNetCoordinates(coords);
+
+ sprites.Add((netCoords, shotAngle.FlipPositive(), vizComp.ImpactFlash, 1f));
+ }
+
+ if (sprites.Count > 0)
+ {
+ RaiseNetworkEvent(new SharedGunSystem.HitscanEvent
+ {
+ Sprites = sprites,
+ }, Filter.Pvs(fromCoordinates, entityMan: EntityManager));
+ }
+ }
+}
--- /dev/null
+using Content.Shared.Weapons.Hitscan.Components;
+using Content.Shared.Weapons.Hitscan.Events;
+using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Weapons.Reflect;
+using Robust.Shared.Random;
+
+namespace Content.Shared.Weapons.Hitscan.Systems;
+
+public sealed class HitscanReflectSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<HitscanReflectComponent, AttemptHitscanRaycastFiredEvent>(OnHitscanHit);
+ }
+
+ private void OnHitscanHit(Entity<HitscanReflectComponent> hitscan, ref AttemptHitscanRaycastFiredEvent args)
+ {
+ var data = args.Data;
+
+ if (hitscan.Comp.ReflectiveType == ReflectType.None || data.HitEntity == null)
+ return;
+
+ if (hitscan.Comp.CurrentReflections >= hitscan.Comp.MaxReflections)
+ return;
+
+ var ev = new HitScanReflectAttemptEvent(data.Shooter ?? data.Gun, data.Gun, hitscan.Comp.ReflectiveType, data.ShotDirection, false);
+ RaiseLocalEvent(data.HitEntity.Value, ref ev);
+
+ if (!ev.Reflected)
+ return;
+
+ hitscan.Comp.CurrentReflections++;
+
+ args.Cancelled = true;
+
+ var fromEffect = Transform(data.HitEntity.Value).Coordinates;
+
+ var hitFiredEvent = new HitscanTraceEvent
+ {
+ FromCoordinates = fromEffect,
+ ShotDirection = ev.Direction,
+ Gun = data.Gun,
+ Shooter = data.HitEntity.Value,
+ };
+
+ RaiseLocalEvent(hitscan, ref hitFiredEvent);
+ }
+}
--- /dev/null
+using Content.Shared.Damage.Systems;
+using Content.Shared.Weapons.Hitscan.Components;
+using Content.Shared.Weapons.Hitscan.Events;
+
+namespace Content.Shared.Weapons.Hitscan.Systems;
+
+public sealed class HitscanStunSystem : EntitySystem
+{
+ [Dependency] private readonly SharedStaminaSystem _stamina = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<HitscanStaminaDamageComponent, HitscanRaycastFiredEvent>(OnHitscanHit);
+ }
+
+ private void OnHitscanHit(Entity<HitscanStaminaDamageComponent> hitscan, ref HitscanRaycastFiredEvent args)
+ {
+ if (args.Data.HitEntity == null)
+ return;
+
+ _stamina.TakeStaminaDamage(args.Data.HitEntity.Value, hitscan.Comp.StaminaDamage, source: args.Data.Shooter ?? args.Data.Gun);
+ }
+}
using Robust.Shared.GameStates;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Prototypes;
namespace Content.Shared.Weapons.Ranged.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class HitscanBatteryAmmoProviderComponent : BatteryAmmoProviderComponent
{
- [ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<HitscanPrototype>))]
- public string Prototype = default!;
+ [DataField("proto", required: true)]
+ public EntProtoId HitscanEntityProto;
}
+++ /dev/null
-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;
-
-namespace Content.Shared.Weapons.Ranged;
-
-[Prototype]
-public sealed partial class HitscanPrototype : IPrototype, IShootable
-{
- [ViewVariables]
- [IdDataField]
- public string ID { get; private set; } = default!;
-
- [ViewVariables(VVAccess.ReadWrite), DataField("staminaDamage")]
- public float StaminaDamage;
-
- [ViewVariables(VVAccess.ReadWrite), DataField("damage")]
- public DamageSpecifier? Damage;
-
- [ViewVariables(VVAccess.ReadOnly), DataField("muzzleFlash")]
- public SpriteSpecifier? MuzzleFlash;
-
- [ViewVariables(VVAccess.ReadOnly), DataField("travelFlash")]
- public SpriteSpecifier? TravelFlash;
-
- [ViewVariables(VVAccess.ReadOnly), DataField("impactFlash")]
- public SpriteSpecifier? ImpactFlash;
-
- [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>
- [DataField("sound")]
- public SoundSpecifier? Sound;
-
- /// <summary>
- /// Force the hitscan sound to play rather than potentially playing the entity's sound.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite), DataField("forceSound")]
- public bool ForceSound;
-
- /// <summary>
- /// Try not to set this too high.
- /// </summary>
- [DataField("maxLength")]
- public float MaxLength = 20f;
-}
using Content.Shared.Damage.Events;
using Content.Shared.Examine;
using Content.Shared.Projectiles;
+using Content.Shared.Weapons.Hitscan.Components;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.GameStates;
{
if (component is ProjectileBatteryAmmoProviderComponent battery)
{
- if (ProtoManager.Index<EntityPrototype>(battery.Prototype)
- .Components
+ if (ProtoManager.Index<EntityPrototype>(battery.Prototype).Components
.TryGetValue(Factory.GetComponentName<ProjectileComponent>(), out var projectile))
{
- var p = (ProjectileComponent)projectile.Component;
+ var p = (ProjectileComponent) projectile.Component;
if (!p.Damage.Empty)
{
if (component is HitscanBatteryAmmoProviderComponent hitscan)
{
- var dmg = ProtoManager.Index<HitscanPrototype>(hitscan.Prototype).Damage;
- return dmg == null ? dmg : dmg * Damageable.UniversalHitscanDamageModifier;
+ var dmg = ProtoManager.Index(hitscan.HitscanEntityProto);
+ if (!dmg.TryGetComponent<HitscanBasicDamageComponent>(out var basicDamageComp, Factory))
+ return null;
+
+ return basicDamageComp.Damage * Damageable.UniversalHitscanDamageModifier;
}
return null;
var ent = Spawn(proj.Prototype, coordinates);
return (ent, EnsureShootable(ent));
case HitscanBatteryAmmoProviderComponent hitscan:
- return (null, ProtoManager.Index<HitscanPrototype>(hitscan.Prototype));
+ var hitscanEnt = Spawn(hitscan.HitscanEntityProto);
+ return (hitscanEnt, EnsureShootable(hitscanEnt));
default:
throw new ArgumentOutOfRangeException();
}
using Content.Shared.Throwing;
using Content.Shared.Timing;
using Content.Shared.Verbs;
+using Content.Shared.Weapons.Hitscan.Components;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components;
if (TryComp<CartridgeAmmoComponent>(uid, out var cartridge))
return cartridge;
+ if (TryComp<HitscanAmmoComponent>(uid, out var hitscanAmmo))
+ return hitscanAmmo;
+
return EnsureComp<AmmoComponent>(uid);
}
protected abstract void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? user = null);
+ public abstract void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound);
+
/// <summary>
/// Used for animated effects on the client.
/// </summary>
public SoundSpecifier? SoundOnReflect = new SoundPathSpecifier("/Audio/Weapons/Guns/Hits/laser_sear_wall.ogg", AudioParams.Default.WithVariation(0.05f));
}
+/// <summary>
+/// Used for both the projectiles being reflected and the entities reflecting. If there is ever overlap between the
+/// reflection types, the projectile will be reflected.
+/// </summary>
[Flags, Serializable, NetSerializable]
public enum ReflectType : byte
{
- HideContextMenu
- type: AnimationPlayer
-- type: hitscan
- id: RedLaser
- damage:
- types:
- Heat: 14
- muzzleFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: muzzle_laser
- travelFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: beam
- impactFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: impact_laser
+- type: entity
+ id: BasicHitscan
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: HitscanAmmo
+ - type: HitscanBasicRaycast
+ - type: HitscanBasicVisuals
+ muzzleFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: muzzle_laser
+ travelFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: beam
+ impactFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: impact_laser
+ - type: HitscanReflect
+ - type: HitscanBasicEffects
-- type: hitscan
- id: RedLaserPractice
- damage:
- types:
- Heat: 1
- muzzleFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: muzzle_laser
- travelFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: beam
- impactFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: impact_laser
+- type: entity
+ parent: BasicHitscan
+ id: RedLightLaser
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: HitscanBasicDamage
+ damage:
+ types:
+ Heat: 7
-- type: hitscan
+- type: entity
+ parent: BasicHitscan
+ id: RedLaser
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: HitscanBasicDamage
+ damage:
+ types:
+ Heat: 14
+
+- type: entity
+ parent: BasicHitscan
id: RedMediumLaser
- damage:
- types:
- Heat: 17
- muzzleFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: muzzle_laser
- travelFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: beam
- impactFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: impact_laser
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: HitscanBasicDamage
+ damage:
+ types:
+ Heat: 17
-- type: hitscan
- id: RedLightLaser
- damage:
- types:
- Heat: 7
- muzzleFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: muzzle_laser
- travelFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: beam
- impactFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: impact_laser
+- type: entity
+ parent: BasicHitscan
+ id: RedHeavyLaser
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: HitscanBasicDamage
+ damage:
+ types:
+ Heat: 28
+ - type: HitscanBasicVisuals
+ muzzleFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: muzzle_beam_heavy
+ travelFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: beam_heavy
+ impactFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: impact_beam_heavy
-- type: hitscan
- id: XrayLaser
- damage:
- types:
- Heat: 10
- Radiation: 10
- muzzleFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: muzzle_xray
- travelFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: xray
- impactFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: impact_xray
+- type: entity
+ parent: BasicHitscan
+ id: RedLaserPractice
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: HitscanBasicDamage
+ damage:
+ types:
+ Heat: 1
-- type: hitscan
- id: RedHeavyLaser
- damage:
- types:
- Heat: 28
- muzzleFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: muzzle_beam_heavy
- travelFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: beam_heavy
- impactFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: impact_beam_heavy
+- type: entity
+ parent: BasicHitscan
+ id: XrayLaser
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: HitscanBasicDamage
+ damage:
+ types:
+ Heat: 10
+ Radiation: 10
+ - type: HitscanBasicVisuals
+ muzzleFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: muzzle_xray
+ travelFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: xray
+ impactFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: impact_xray
-- type: hitscan
+- type: entity
+ parent: BasicHitscan
id: Pulse
- damage:
- types:
- Heat: 35
- muzzleFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: muzzle_blue
- travelFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: beam_blue
- impactFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: impact_blue
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: HitscanBasicDamage
+ damage:
+ types:
+ Heat: 35
+ - type: HitscanBasicVisuals
+ muzzleFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: muzzle_blue
+ travelFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: beam_blue
+ impactFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: impact_blue
-- type: hitscan
+- type: entity
+ parent: BasicHitscan
id: RedShuttleLaser
- maxLength: 60
- damage:
- types:
- Heat: 45
- Structural: 10
- muzzleFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: muzzle_beam_heavy2
- travelFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: beam_heavy2
- impactFlash:
- sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
- state: impact_beam_heavy2
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: HitscanBasicRaycast
+ maxDistance: 60.0
+ - type: HitscanBasicDamage
+ damage:
+ types:
+ Heat: 45
+ Structural: 10
+ - type: HitscanBasicVisuals
+ muzzleFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: muzzle_beam_heavy2
+ travelFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: beam_heavy2
+ impactFlash:
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles.rsi
+ state: impact_beam_heavy2