diff --git a/Content.Server/Chemistry/Components/BaseSolutionInjectOnEventComponent.cs b/Content.Server/Chemistry/Components/BaseSolutionInjectOnEventComponent.cs
new file mode 100644
index 0000000000..708c1ef005
--- /dev/null
+++ b/Content.Server/Chemistry/Components/BaseSolutionInjectOnEventComponent.cs
@@ -0,0 +1,60 @@
+using Content.Shared.FixedPoint;
+using Content.Shared.Inventory;
+
+namespace Content.Server.Chemistry.Components;
+
+///
+/// Base class for components that inject a solution into a target's bloodstream in response to an event.
+///
+public abstract partial class BaseSolutionInjectOnEventComponent : Component
+{
+ ///
+ /// How much solution to remove from this entity per target when transferring.
+ ///
+ ///
+ /// Note that this amount is per target, so the total amount removed will be
+ /// multiplied by the number of targets hit.
+ ///
+ [DataField]
+ public FixedPoint2 TransferAmount = FixedPoint2.New(1);
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
+
+ ///
+ /// Proportion of the that will actually be injected
+ /// into the target's bloodstream. The rest is lost.
+ /// 0 means none of the transferred solution will enter the bloodstream.
+ /// 1 means the entire amount will enter the bloodstream.
+ ///
+ [DataField("transferEfficiency")]
+ private float _transferEfficiency = 1f;
+
+ ///
+ /// Solution to inject from.
+ ///
+ [DataField]
+ public string Solution = "default";
+
+ ///
+ /// Whether this will inject through hardsuits or not.
+ ///
+ [DataField]
+ public bool PierceArmor = true;
+
+ ///
+ /// Contents of popup message to display to the attacker when injection
+ /// fails due to the target wearing a hardsuit.
+ ///
+ ///
+ /// Passed values: $weapon and $target
+ ///
+ [DataField]
+ public LocId BlockedByHardsuitPopupMessage = "melee-inject-failed-hardsuit";
+
+ ///
+ /// If anything covers any of these slots then the injection fails.
+ ///
+ [DataField]
+ public SlotFlags BlockSlots = SlotFlags.NONE;
+}
diff --git a/Content.Server/Chemistry/Components/MeleeChemicalInjectorComponent.cs b/Content.Server/Chemistry/Components/MeleeChemicalInjectorComponent.cs
index 6b6ce830a9..6b64b82f78 100644
--- a/Content.Server/Chemistry/Components/MeleeChemicalInjectorComponent.cs
+++ b/Content.Server/Chemistry/Components/MeleeChemicalInjectorComponent.cs
@@ -1,31 +1,8 @@
-using Content.Shared.FixedPoint;
+namespace Content.Server.Chemistry.Components;
-namespace Content.Server.Chemistry.Components
-{
- [RegisterComponent]
- public sealed partial class MeleeChemicalInjectorComponent : Component
- {
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("transferAmount")]
- public FixedPoint2 TransferAmount { get; set; } = FixedPoint2.New(1);
-
- [ViewVariables(VVAccess.ReadWrite)]
- public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
-
- [DataField("transferEfficiency")]
- private float _transferEfficiency = 1f;
-
- ///
- /// Whether this will inject through hardsuits or not.
- ///
- [DataField("pierceArmor"), ViewVariables(VVAccess.ReadWrite)]
- public bool PierceArmor = true;
-
- ///
- /// Solution to inject from.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("solution")]
- public string Solution { get; set; } = "default";
- }
-}
+///
+/// Used for melee weapon entities that should try to inject a
+/// contained solution into a target when used to hit it.
+///
+[RegisterComponent]
+public sealed partial class MeleeChemicalInjectorComponent : BaseSolutionInjectOnEventComponent { }
diff --git a/Content.Server/Chemistry/Components/SolutionInjectOnCollideComponent.cs b/Content.Server/Chemistry/Components/SolutionInjectOnCollideComponent.cs
deleted file mode 100644
index 76bb5294bc..0000000000
--- a/Content.Server/Chemistry/Components/SolutionInjectOnCollideComponent.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using Content.Shared.FixedPoint;
-using Content.Shared.Inventory;
-using Content.Shared.Projectiles;
-
-namespace Content.Server.Chemistry.Components;
-
-///
-/// On colliding with an entity that has a bloodstream will dump its solution onto them.
-///
-[RegisterComponent]
-public sealed partial class SolutionInjectOnCollideComponent : Component
-{
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("transferAmount")]
- public FixedPoint2 TransferAmount = FixedPoint2.New(1);
-
- [ViewVariables(VVAccess.ReadWrite)]
- public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
-
- [DataField("transferEfficiency")]
- private float _transferEfficiency = 1f;
-
- ///
- /// If anything covers any of these slots then the injection fails.
- ///
- [DataField("blockSlots"), ViewVariables(VVAccess.ReadWrite)]
- public SlotFlags BlockSlots = SlotFlags.MASK;
-}
diff --git a/Content.Server/Chemistry/Components/SolutionInjectOnEmbedComponent.cs b/Content.Server/Chemistry/Components/SolutionInjectOnEmbedComponent.cs
new file mode 100644
index 0000000000..241da38045
--- /dev/null
+++ b/Content.Server/Chemistry/Components/SolutionInjectOnEmbedComponent.cs
@@ -0,0 +1,8 @@
+namespace Content.Server.Chemistry.Components;
+
+///
+/// Used for embeddable entities that should try to inject a
+/// contained solution into a target when they become embedded in it.
+///
+[RegisterComponent]
+public sealed partial class SolutionInjectOnEmbedComponent : BaseSolutionInjectOnEventComponent { }
diff --git a/Content.Server/Chemistry/Components/SolutionInjectOnProjectileHitComponent.cs b/Content.Server/Chemistry/Components/SolutionInjectOnProjectileHitComponent.cs
new file mode 100644
index 0000000000..395a075298
--- /dev/null
+++ b/Content.Server/Chemistry/Components/SolutionInjectOnProjectileHitComponent.cs
@@ -0,0 +1,8 @@
+namespace Content.Server.Chemistry.Components;
+
+///
+/// Used for projectile entities that should try to inject a
+/// contained solution into a target when they hit it.
+///
+[RegisterComponent]
+public sealed partial class SolutionInjectOnProjectileHitComponent : BaseSolutionInjectOnEventComponent { }
diff --git a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnCollideSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnCollideSystem.cs
deleted file mode 100644
index fb84aca3e4..0000000000
--- a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnCollideSystem.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using Content.Server.Body.Components;
-using Content.Server.Body.Systems;
-using Content.Server.Chemistry.Components;
-using Content.Server.Chemistry.Containers.EntitySystems;
-using Content.Shared.Inventory;
-using Content.Shared.Projectiles;
-
-namespace Content.Server.Chemistry.EntitySystems;
-
-public sealed class SolutionInjectOnCollideSystem : EntitySystem
-{
- [Dependency] private readonly SolutionContainerSystem _solutionContainersSystem = default!;
- [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
- [Dependency] private readonly InventorySystem _inventorySystem = default!;
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(HandleInjection);
- }
-
- private void HandleInjection(Entity ent, ref ProjectileHitEvent args)
- {
- var component = ent.Comp;
- var target = args.Target;
-
- if (!TryComp(target, out var bloodstream) ||
- !_solutionContainersSystem.TryGetInjectableSolution(ent.Owner, out var solution, out _))
- {
- return;
- }
-
- if (component.BlockSlots != 0x0)
- {
- var containerEnumerator = _inventorySystem.GetSlotEnumerator(target, component.BlockSlots);
-
- // TODO add a helper method for this?
- if (containerEnumerator.MoveNext(out _))
- return;
- }
-
- var solRemoved = _solutionContainersSystem.SplitSolution(solution.Value, component.TransferAmount);
- var solRemovedVol = solRemoved.Volume;
-
- var solToInject = solRemoved.SplitSolution(solRemovedVol * component.TransferEfficiency);
-
- _bloodstreamSystem.TryAddToChemicals(target, solToInject, bloodstream);
- }
-}
diff --git a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs
new file mode 100644
index 0000000000..8ba36e3a29
--- /dev/null
+++ b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs
@@ -0,0 +1,147 @@
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Server.Chemistry.Components;
+using Content.Server.Chemistry.Containers.EntitySystems;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+using Content.Shared.Projectiles;
+using Content.Shared.Tag;
+using Content.Shared.Weapons.Melee.Events;
+
+namespace Content.Server.Chemistry.EntitySystems;
+
+///
+/// System for handling the different inheritors of .
+/// Subscribes to relevent events and performs solution injections when they are raised.
+///
+public sealed class SolutionInjectOnCollideSystem : EntitySystem
+{
+ [Dependency] private readonly BloodstreamSystem _bloodstream = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
+ [Dependency] private readonly TagSystem _tag = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(HandleProjectileHit);
+ SubscribeLocalEvent(HandleEmbed);
+ SubscribeLocalEvent(HandleMeleeHit);
+ }
+
+ private void HandleProjectileHit(Entity entity, ref ProjectileHitEvent args)
+ {
+ DoInjection((entity.Owner, entity.Comp), args.Target, args.Shooter);
+ }
+
+ private void HandleEmbed(Entity entity, ref EmbedEvent args)
+ {
+ DoInjection((entity.Owner, entity.Comp), args.Embedded, args.Shooter);
+ }
+
+ private void HandleMeleeHit(Entity entity, ref MeleeHitEvent args)
+ {
+ // MeleeHitEvent is weird, so we have to filter to make sure we actually
+ // hit something and aren't just examining the weapon.
+ if (args.IsHit)
+ TryInjectTargets((entity.Owner, entity.Comp), args.HitEntities, args.User);
+ }
+
+ private void DoInjection(Entity injectorEntity, EntityUid target, EntityUid? source = null)
+ {
+ TryInjectTargets(injectorEntity, [target], source);
+ }
+
+ ///
+ /// Filters for valid targets and tries to inject a portion of into
+ /// each valid target's bloodstream.
+ ///
+ ///
+ /// Targets are invalid if any of the following are true:
+ ///
+ /// - The target does not have a bloodstream.
+ /// - is false and the target is wearing a hardsuit.
+ /// - is not NONE and the target has an item equipped in any of the specified slots.
+ ///
+ ///
+ /// true if at least one target was successfully injected, otherwise false
+ private bool TryInjectTargets(Entity injector, IReadOnlyList targets, EntityUid? source = null)
+ {
+ // Make sure we have at least one target
+ if (targets.Count == 0)
+ return false;
+
+ // Get the solution to inject
+ if (!_solutionContainer.TryGetSolution(injector.Owner, injector.Comp.Solution, out var injectorSolution))
+ return false;
+
+ // Build a list of bloodstreams to inject into
+ var targetBloodstreams = new ValueList>();
+ foreach (var target in targets)
+ {
+ if (Deleted(target))
+ continue;
+
+ // Yuck, this is way to hardcodey for my tastes
+ // TODO blocking injection with a hardsuit should probably done with a cancellable event or something
+ if (!injector.Comp.PierceArmor && _inventory.TryGetSlotEntity(target, "outerClothing", out var suit) && _tag.HasTag(suit.Value, "Hardsuit"))
+ {
+ // Only show popup to attacker
+ if (source != null)
+ _popup.PopupEntity(Loc.GetString(injector.Comp.BlockedByHardsuitPopupMessage, ("weapon", injector.Owner), ("target", target)), target, source.Value, PopupType.SmallCaution);
+
+ continue;
+ }
+
+ // Check if the target has anything equipped in a slot that would block injection
+ if (injector.Comp.BlockSlots != SlotFlags.NONE)
+ {
+ var blocked = false;
+ var containerEnumerator = _inventory.GetSlotEnumerator(target, injector.Comp.BlockSlots);
+ while (containerEnumerator.MoveNext(out var container))
+ {
+ if (container.ContainedEntity != null)
+ {
+ blocked = true;
+ break;
+ }
+ }
+ if (blocked)
+ continue;
+ }
+
+ // Make sure the target has a bloodstream
+ if (!TryComp(target, out var bloodstream))
+ continue;
+
+
+ // Checks passed; add this target's bloodstream to the list
+ targetBloodstreams.Add((target, bloodstream));
+ }
+
+ // Make sure we got at least one bloodstream
+ if (targetBloodstreams.Count == 0)
+ return false;
+
+ // Extract total needed solution from the injector
+ var removedSolution = _solutionContainer.SplitSolution(injectorSolution.Value, injector.Comp.TransferAmount * targetBloodstreams.Count);
+ // Adjust solution amount based on transfer efficiency
+ var solutionToInject = removedSolution.SplitSolution(removedSolution.Volume * injector.Comp.TransferEfficiency);
+ // Calculate how much of the adjusted solution each target will get
+ var volumePerBloodstream = solutionToInject.Volume * (1f / targetBloodstreams.Count);
+
+ var anySuccess = false;
+ foreach (var targetBloodstream in targetBloodstreams)
+ {
+ // Take our portion of the adjusted solution for this target
+ var individualInjection = solutionToInject.SplitSolution(volumePerBloodstream);
+ // Inject our portion into the target's bloodstream
+ if (_bloodstream.TryAddToChemicals(targetBloodstream.Owner, individualInjection, targetBloodstream.Comp))
+ anySuccess = true;
+ }
+
+ // Huzzah!
+ return anySuccess;
+ }
+}
diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
index ef4b161477..2612e99ec9 100644
--- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
+++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
@@ -1,8 +1,4 @@
-using Content.Server.Body.Components;
-using Content.Server.Body.Systems;
using Content.Server.Chat.Systems;
-using Content.Server.Chemistry.Components;
-using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.CombatMode.Disarm;
using Content.Server.Movement.Systems;
using Content.Shared.Actions.Events;
@@ -14,12 +10,10 @@ using Content.Shared.Database;
using Content.Shared.Effects;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
-using Content.Shared.Inventory;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Speech.Components;
using Content.Shared.StatusEffect;
-using Content.Shared.Tag;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio;
@@ -34,22 +28,17 @@ namespace Content.Server.Weapons.Melee;
public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
{
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly BloodstreamSystem _bloodstream = default!;
- [Dependency] private readonly ChatSystem _chat = default!;
- [Dependency] private readonly DamageExamineSystem _damageExamine = default!;
- [Dependency] private readonly InventorySystem _inventory = default!;
- [Dependency] private readonly LagCompensationSystem _lag = default!;
- [Dependency] private readonly MobStateSystem _mobState = default!;
- [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
- [Dependency] private readonly SolutionContainerSystem _solutions = default!;
- [Dependency] private readonly TagSystem _tag = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly DamageExamineSystem _damageExamine = default!;
+ [Dependency] private readonly LagCompensationSystem _lag = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(OnChemicalInjectorHit);
SubscribeLocalEvent(OnSpeechHit);
SubscribeLocalEvent(OnMeleeExamineDamage);
}
@@ -263,47 +252,4 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
}
}
-
- private void OnChemicalInjectorHit(Entity entity, ref MeleeHitEvent args)
- {
- if (!args.IsHit ||
- !args.HitEntities.Any() ||
- !_solutions.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solutionContainer))
- {
- return;
- }
-
- var hitBloodstreams = new List<(EntityUid Entity, BloodstreamComponent Component)>();
- var bloodQuery = GetEntityQuery();
-
- foreach (var hit in args.HitEntities)
- {
- if (Deleted(hit))
- continue;
-
- // prevent deathnettles injecting through hardsuits
- if (!entity.Comp.PierceArmor && _inventory.TryGetSlotEntity(hit, "outerClothing", out var suit) && _tag.HasTag(suit.Value, "Hardsuit"))
- {
- PopupSystem.PopupEntity(Loc.GetString("melee-inject-failed-hardsuit", ("weapon", entity.Owner)), args.User, args.User, PopupType.SmallCaution);
- continue;
- }
-
- if (bloodQuery.TryGetComponent(hit, out var bloodstream))
- hitBloodstreams.Add((hit, bloodstream));
- }
-
- if (!hitBloodstreams.Any())
- return;
-
- var removedSolution = _solutions.SplitSolution(solutionContainer.Value, entity.Comp.TransferAmount * hitBloodstreams.Count);
- var removedVol = removedSolution.Volume;
- var solutionToInject = removedSolution.SplitSolution(removedVol * entity.Comp.TransferEfficiency);
- var volPerBloodstream = solutionToInject.Volume * (1 / hitBloodstreams.Count);
-
- foreach (var (ent, bloodstream) in hitBloodstreams)
- {
- var individualInjection = solutionToInject.SplitSolution(volPerBloodstream);
- _bloodstream.TryAddToChemicals(ent, individualInjection, bloodstream);
- }
- }
}
diff --git a/Resources/Prototypes/Entities/Objects/Fun/darts.yml b/Resources/Prototypes/Entities/Objects/Fun/darts.yml
index 391823dc52..4c7ae68420 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/darts.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/darts.yml
@@ -53,10 +53,10 @@
solution: melee
- type: InjectableSolution
solution: melee
- - type: SolutionInjectOnCollide
+ - type: SolutionInjectOnEmbed
transferAmount: 2
+ solution: melee
blockSlots: OUTERCLOTHING
- fixtureId: "throw-fixture"
- type: SolutionTransfer
maxTransferAmount: 2
- type: Damageable
@@ -124,10 +124,9 @@
solutions:
melee:
maxVol: 7
- - type: SolutionInjectOnCollide
+ - type: SolutionInjectOnEmbed
transferAmount: 7
- blockSlots: NONE
- fixtureId: "throw-fixture"
+ solution: melee
- type: SolutionTransfer
maxTransferAmount: 7
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml
index 757b8934d4..e119a846c9 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml
@@ -86,8 +86,8 @@
damage:
types:
Piercing: 3
- Slash: 3
-
+ Slash: 3
+
- type: entity
id: PelletShotgunTranquilizer
@@ -110,9 +110,9 @@
solution: ammo
- type: DrainableSolution
solution: ammo
- - type: SolutionInjectOnCollide
+ - type: SolutionInjectOnProjectileHit
transferAmount: 15
- blockSlots: NONE #tranquillizer darts shouldn't be blocked by a mask
+ solution: ammo
- type: InjectableSolution
solution: ammo
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml
index 8dbcf2b303..52c5dc8a9d 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml
@@ -50,9 +50,9 @@
solution: ammo
- type: InjectableSolution
solution: ammo
- - type: SolutionInjectOnCollide
+ - type: SolutionInjectOnEmbed
transferAmount: 2
- blockSlots: NONE
+ solution: ammo
- type: SolutionTransfer
maxTransferAmount: 2
- type: Appearance
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml
index 279fed8043..3758487bd4 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml
@@ -65,10 +65,9 @@
solution: melee
- type: InjectableSolution
solution: melee
- - type: SolutionInjectOnCollide
+ - type: SolutionInjectOnEmbed
transferAmount: 2
- fixtureId: "throw-fixture"
- blockSlots: NONE
+ solution: melee
- type: SolutionTransfer
maxTransferAmount: 2
- type: Wieldable