using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Projectiles;
-using Robust.Server.GameObjects;
using Robust.Shared.Physics.Events;
-using Content.Shared.Mobs.Components;
using Robust.Shared.Player;
namespace Content.Server.Projectiles;
component.DamagedEntity = true;
- if (component.CanPenetrate)
- {
- component.DamagedEntity = false;
+ var afterProjectileHitEvent = new AfterProjectileHitEvent(component.Damage, target, args.OtherFixture);
+ RaiseLocalEvent(uid, ref afterProjectileHitEvent);
- if (component.DeleteOnCollide && !HasComp<MobStateComponent>(target))
- QueueDel(uid);
- }
- else if (component.DeleteOnCollide)
- {
+ if (component.DeleteOnCollide)
QueueDel(uid);
- }
if (component.ImpactEffect != null && TryComp<TransformComponent>(uid, out var xform))
{
--- /dev/null
+using Content.Shared.FixedPoint;
+using Content.Shared.Physics;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Projectiles;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class CanPenetrateComponent : Component
+{
+ /// <summary>
+ /// Should the projectile keep the ability to deal damage after colliding.
+ /// </summary>
+ [DataField]
+ public bool DamageAfterCollide = true;
+
+ /// <summary>
+ /// The CollisionLayer, up to and including the one set, the projectile is allowed to penetrate.
+ /// </summary>
+ ///<remarks>
+ /// Can penetrate everything if this value is not set.
+ /// </remarks>
+ [DataField]
+ public CollisionGroup? PenetrationLayer;
+
+ /// <summary>
+ /// How many times the projectile is allowed to deal damage.
+ /// </summary>
+ /// <remarks>
+ /// Can deal damage on every collision if this value is not set.
+ /// </remarks>
+ [DataField]
+ public float? PenetrationPower;
+
+ /// <summary>
+ /// Modifies the damage of a projectile after it has penetrated an entity.
+ /// </summary>
+ /// <remarks>
+ /// Won't modify the projectile's damage if this value is not set.
+ /// </remarks>
+ [DataField]
+ public FixedPoint2? DamageModifier;
+}
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Projectiles;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ProjectileComponent : Component
{
- [ViewVariables(VVAccess.ReadWrite), DataField("impactEffect", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
- public string? ImpactEffect;
+ /// <summary>
+ /// The effect that appears when a projectile collides with an entity.
+ /// </summary>
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public EntProtoId? ImpactEffect;
/// <summary>
- /// User that shot this projectile.
+ /// User that shot this projectile.
/// </summary>
- [DataField("shooter"), AutoNetworkedField]
+ [DataField, AutoNetworkedField]
public EntityUid? Shooter;
/// <summary>
- /// Weapon used to shoot.
+ /// Weapon used to shoot.
/// </summary>
- [DataField("weapon"), AutoNetworkedField]
+ [DataField, AutoNetworkedField]
public EntityUid? Weapon;
- [DataField("ignoreShooter"), AutoNetworkedField]
+ /// <summary>
+ /// The projectile spawns inside the shooter most of the time, this prevents entities from shooting themselves.
+ /// </summary>
+ [DataField, AutoNetworkedField]
public bool IgnoreShooter = true;
- [DataField("damage", required: true)] [ViewVariables(VVAccess.ReadWrite)]
+ /// <summary>
+ /// The amount of damage the projectile will do.
+ /// </summary>
+ [DataField(required: true)] [ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = new();
- [DataField("deleteOnCollide")]
+ /// <summary>
+ /// If the projectile should be deleted on collision.
+ /// </summary>
+ [DataField]
public bool DeleteOnCollide = true;
- [DataField("canPenetrate")]
- public bool CanPenetrate = false;
-
- [DataField("ignoreResistances")]
+ /// <summary>
+ /// Ignore all damage resistances the target has.
+ /// </summary>
+ [DataField]
public bool IgnoreResistances = false;
- // Get that juicy FPS hit sound
- [DataField("soundHit")] public SoundSpecifier? SoundHit;
+ /// <summary>
+ /// Get that juicy FPS hit sound.
+ /// </summary>
+ [DataField]
+ public SoundSpecifier? SoundHit;
- [DataField("soundForce")]
+ /// <summary>
+ /// Force the projectiles sound to play rather than potentially playing the entity's sound.
+ /// </summary>
+ [DataField]
public bool ForceSound = false;
/// <summary>
- /// Whether this projectile will only collide with entities if it was shot from a gun (if <see cref="Weapon"/> is not null)
+ /// Whether this projectile will only collide with entities if it was shot from a gun (if <see cref="Weapon"/> is not null).
/// </summary>
- [DataField("onlyCollideWhenShot")]
+ [DataField]
public bool OnlyCollideWhenShot = false;
/// <summary>
/// Whether this projectile has already damaged an entity.
/// </summary>
+ [DataField]
public bool DamagedEntity;
}
using Content.Shared.DoAfter;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
+using Content.Shared.Mobs.Components;
using Content.Shared.Throwing;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Serialization;
base.Initialize();
SubscribeLocalEvent<ProjectileComponent, PreventCollideEvent>(PreventCollision);
+ SubscribeLocalEvent<ProjectileComponent, AfterProjectileHitEvent>(AfterProjectileHit);
SubscribeLocalEvent<EmbeddableProjectileComponent, ProjectileHitEvent>(OnEmbedProjectileHit);
SubscribeLocalEvent<EmbeddableProjectileComponent, ThrowDoHitEvent>(OnEmbedThrowDoHit);
SubscribeLocalEvent<EmbeddableProjectileComponent, ActivateInWorldEvent>(OnEmbedActivate);
{
args.Cancel("pacified-cannot-throw-embed");
}
+
+ /// <summary>
+ /// Checks if the projectile is allowed to penetrate the target it hit.
+ /// </summary>
+ private void AfterProjectileHit(EntityUid uid, ProjectileComponent component, ref AfterProjectileHitEvent args)
+ {
+ if (!TryComp<CanPenetrateComponent>(uid, out var damageAfterCollide))
+ return;
+
+ //Delete the projectile if it hits an entity with a CollisionLayer that has a higher value than it's PenetrationLayer.
+ //This allows a projectile to only penetrate a specific set of entities.
+ if (damageAfterCollide.PenetrationLayer != null)
+ {
+ if (args.Fixture.CollisionLayer > (int) damageAfterCollide.PenetrationLayer ||
+ damageAfterCollide.PenetrationPower == 0)
+ {
+ QueueDel(uid);
+ return;
+ }
+ }
+
+ //Allow the projectile to deal damage again.
+ if(damageAfterCollide.DamageAfterCollide)
+ component.DamagedEntity = false;
+
+ //If the projectile has a limit on the amount of penetrations, reduce it.
+ if (damageAfterCollide.PenetrationPower != null)
+ damageAfterCollide.PenetrationPower -= 1;
+
+ //Apply the penetration damage modifier if the projectile has one.
+ if (damageAfterCollide.DamageModifier != null)
+ component.Damage *= damageAfterCollide.DamageModifier.Value;
+
+ //Overrides the original DeleteOnCollide if the projectile passes all penetration checks.
+ //This is to prevent having to set DeleteOnCollide to false on every prototype
+ //you want to give the ability to penetrate entities.
+ if(component.DeleteOnCollide)
+ component.DeleteOnCollide = false;
+ }
}
[Serializable, NetSerializable]
/// </summary>
[ByRefEvent]
public record struct ProjectileHitEvent(DamageSpecifier Damage, EntityUid Target, EntityUid? Shooter = null);
+
+/// <summary>
+/// Raised after a projectile has dealt it's damage.
+/// </summary>
+[ByRefEvent]
+public record struct AfterProjectileHitEvent(DamageSpecifier Damage, EntityUid Target, Fixture Fixture);
state: buckshot
- type: Projectile
deleteOnCollide: false
- canPenetrate: true
damage:
types:
Blunt: 4
+ - type: CanPenetrate
+ penetrationLayer: MobLayer
- type: StaminaDamageOnCollide
damage: 55
- type: TimedDespawn
state: buckshot
- type: Projectile
deleteOnCollide: false
- canPenetrate: true
damage:
types:
Piercing: 45
+ - type: CanPenetrate
+ penetrationLayer: MobLayer
- type: TimedDespawn
lifetime: 0.25
state: buckshot-flare
- type: Projectile
deleteOnCollide: false
- canPenetrate: true
damage:
types:
Blunt: 1
Heat: 2
+ - type: CanPenetrate
+ penetrationLayer: MobLayer
- type: IgniteOnCollide
fireStacks: 3
count: 10
Heat: 5
soundHit:
path: "/Audio/Weapons/Guns/Hits/taser_hit.ogg"
- soundForce: true
+ forceSound: true
- type: StunOnCollide
stunAmount: 5
knockdownAmount: 5
Heat: 5
soundHit:
path: "/Audio/Weapons/tap.ogg"
- soundForce: true
+ forceSound: true
- type: entity
name : disabler bolt practice
Heat: 1
soundHit:
path: "/Audio/Weapons/tap.ogg"
- soundForce: true
+ forceSound: true
- type: entity
name: emitter bolt
Heat: 2
soundHit:
path: "/Audio/Weapons/tap.ogg"
- soundForce: true
+ forceSound: true
- type: entity
name: tesla gun lightning
map: [ "unshaded" ]
- type: Projectile
deleteOnCollide: false
- canPenetrate: true
impactEffect: null
soundHit:
path: /Audio/Weapons/Guns/Hits/bullet_hit.ogg
damage:
types:
Radiation: 25
+ - type: CanPenetrate
- type: Physics
- type: Fixtures
fixtures:
state: particle0
shader: unshaded
map: [ "unshaded" ]
+ - type: Reflective
+ reflective:
+ - Energy
+ - type: Projectile
+ deleteOnCollide: false
+ impactEffect: null
+ soundHit:
+ path: /Audio/Weapons/Guns/Hits/bullet_hit.ogg
+ damage:
+ types:
+ Radiation: 10
+ - type: TimedDespawn
+ lifetime: 0.6
- type: SinguloFood
energy: -10