mirror of
https://github.com/corvax-team/ss14-wl.git
synced 2026-02-15 03:31:38 +01:00
Hitscans are now entities (#38035)
* Hitscans are now entities * Cleanup * Cleanup * Silly mistakes but stop sign testing helps :) * Address most of the review * Reviews * perry :( * Final reviews * Add comments * Split event up * better comment * cleanup
This commit is contained in:
@@ -5,6 +5,8 @@ using Content.Client.Items;
|
||||
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;
|
||||
@@ -16,6 +18,7 @@ using Robust.Client.Input;
|
||||
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;
|
||||
@@ -234,6 +237,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Clean this up in a gun refactor at some point - too much copy pasting
|
||||
switch (shootable)
|
||||
{
|
||||
case CartridgeAmmoComponent cartridge:
|
||||
@@ -266,7 +270,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
else
|
||||
RemoveShootable(ent.Value);
|
||||
break;
|
||||
case HitscanPrototype:
|
||||
case HitscanAmmoComponent:
|
||||
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
||||
Recoil(user, direction, gun.CameraRecoilScalarModified);
|
||||
break;
|
||||
@@ -404,4 +408,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
_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) {}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
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;
|
||||
|
||||
@@ -29,9 +24,6 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
{
|
||||
[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;
|
||||
@@ -103,6 +95,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Clean this up in a gun refactor at some point - too much copy pasting
|
||||
switch (shootable)
|
||||
{
|
||||
// Cartridge shoots something else
|
||||
@@ -141,107 +134,21 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
CreateAndFireProjectiles(ent.Value, newAmmo);
|
||||
|
||||
break;
|
||||
case HitscanPrototype hitscan:
|
||||
case HitscanAmmoComponent:
|
||||
if (ent == null)
|
||||
break;
|
||||
|
||||
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)
|
||||
var hitscanEv = new HitscanTraceEvent
|
||||
{
|
||||
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;
|
||||
FromCoordinates = fromCoordinates,
|
||||
ShotDirection = mapDirection.Normalized(),
|
||||
Gun = gunUid,
|
||||
Shooter = user,
|
||||
Target = gun.Target,
|
||||
};
|
||||
RaiseLocalEvent(ent.Value, ref hitscanEv);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastHit != null)
|
||||
{
|
||||
var hitEntity = lastHit.Value;
|
||||
if (hitscan.StaminaDamage > 0f)
|
||||
_stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source: user);
|
||||
|
||||
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;
|
||||
@@ -353,7 +260,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
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");
|
||||
|
||||
@@ -384,69 +291,4 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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;
|
||||
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
}
|
||||
110
Content.Shared/Weapons/Hitscan/Events/HitscanEvents.cs
Normal file
110
Content.Shared/Weapons/Hitscan/Events/HitscanEvents.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
25
Content.Shared/Weapons/Hitscan/Systems/HitscanStunSystem.cs
Normal file
25
Content.Shared/Weapons/Hitscan/Systems/HitscanStunSystem.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Content.Shared.Damage;
|
||||
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;
|
||||
@@ -80,11 +81,10 @@ public abstract partial class SharedGunSystem
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -97,8 +97,11 @@ public abstract partial class SharedGunSystem
|
||||
|
||||
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;
|
||||
@@ -155,7 +158,8 @@ public abstract partial class SharedGunSystem
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ using Content.Shared.Tag;
|
||||
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;
|
||||
@@ -500,6 +501,9 @@ public abstract partial class SharedGunSystem : EntitySystem
|
||||
if (TryComp<CartridgeAmmoComponent>(uid, out var cartridge))
|
||||
return cartridge;
|
||||
|
||||
if (TryComp<HitscanAmmoComponent>(uid, out var hitscanAmmo))
|
||||
return hitscanAmmo;
|
||||
|
||||
return EnsureComp<AmmoComponent>(uid);
|
||||
}
|
||||
|
||||
@@ -614,6 +618,8 @@ public abstract partial class SharedGunSystem : EntitySystem
|
||||
|
||||
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>
|
||||
|
||||
@@ -56,6 +56,10 @@ public sealed partial class ReflectComponent : Component
|
||||
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
|
||||
{
|
||||
|
||||
@@ -16,125 +16,145 @@
|
||||
- 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: hitscan
|
||||
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
|
||||
|
||||
- type: hitscan
|
||||
- type: entity
|
||||
parent: BasicHitscan
|
||||
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
|
||||
categories: [ HideSpawnMenu ]
|
||||
components:
|
||||
- type: HitscanBasicDamage
|
||||
damage:
|
||||
types:
|
||||
Heat: 7
|
||||
|
||||
- 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: RedLaser
|
||||
categories: [ HideSpawnMenu ]
|
||||
components:
|
||||
- type: HitscanBasicDamage
|
||||
damage:
|
||||
types:
|
||||
Heat: 14
|
||||
|
||||
- type: hitscan
|
||||
- type: entity
|
||||
parent: BasicHitscan
|
||||
id: RedMediumLaser
|
||||
categories: [ HideSpawnMenu ]
|
||||
components:
|
||||
- type: HitscanBasicDamage
|
||||
damage:
|
||||
types:
|
||||
Heat: 17
|
||||
|
||||
- type: entity
|
||||
parent: BasicHitscan
|
||||
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
|
||||
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
|
||||
- type: entity
|
||||
parent: BasicHitscan
|
||||
id: RedLaserPractice
|
||||
categories: [ HideSpawnMenu ]
|
||||
components:
|
||||
- type: HitscanBasicDamage
|
||||
damage:
|
||||
types:
|
||||
Heat: 1
|
||||
|
||||
- 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: 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
|
||||
|
||||
Reference in New Issue
Block a user