--- /dev/null
+using Content.Shared.FixedPoint;
+using Content.Shared.Inventory;
+
+namespace Content.Server.Chemistry.Components;
+
+/// <summary>
+/// Base class for components that inject a solution into a target's bloodstream in response to an event.
+/// </summary>
+public abstract partial class BaseSolutionInjectOnEventComponent : Component
+{
+ /// <summary>
+ /// How much solution to remove from this entity per target when transferring.
+ /// </summary>
+ /// <remarks>
+ /// Note that this amount is per target, so the total amount removed will be
+ /// multiplied by the number of targets hit.
+ /// </remarks>
+ [DataField]
+ public FixedPoint2 TransferAmount = FixedPoint2.New(1);
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
+
+ /// <summary>
+ /// Proportion of the <see cref="TransferAmount"/> 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.
+ /// </summary>
+ [DataField("transferEfficiency")]
+ private float _transferEfficiency = 1f;
+
+ /// <summary>
+ /// Solution to inject from.
+ /// </summary>
+ [DataField]
+ public string Solution = "default";
+
+ /// <summary>
+ /// Whether this will inject through hardsuits or not.
+ /// </summary>
+ [DataField]
+ public bool PierceArmor = true;
+
+ /// <summary>
+ /// Contents of popup message to display to the attacker when injection
+ /// fails due to the target wearing a hardsuit.
+ /// </summary>
+ /// <remarks>
+ /// Passed values: $weapon and $target
+ /// </remarks>
+ [DataField]
+ public LocId BlockedByHardsuitPopupMessage = "melee-inject-failed-hardsuit";
+
+ /// <summary>
+ /// If anything covers any of these slots then the injection fails.
+ /// </summary>
+ [DataField]
+ public SlotFlags BlockSlots = SlotFlags.NONE;
+}
-using Content.Shared.FixedPoint;
-
-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;
-
- /// <summary>
- /// Whether this will inject through hardsuits or not.
- /// </summary>
- [DataField("pierceArmor"), ViewVariables(VVAccess.ReadWrite)]
- public bool PierceArmor = true;
-
- /// <summary>
- /// Solution to inject from.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("solution")]
- public string Solution { get; set; } = "default";
- }
-}
+namespace Content.Server.Chemistry.Components;
+
+/// <summary>
+/// Used for melee weapon entities that should try to inject a
+/// contained solution into a target when used to hit it.
+/// </summary>
+[RegisterComponent]
+public sealed partial class MeleeChemicalInjectorComponent : BaseSolutionInjectOnEventComponent { }
+++ /dev/null
-using Content.Shared.FixedPoint;
-using Content.Shared.Inventory;
-using Content.Shared.Projectiles;
-
-namespace Content.Server.Chemistry.Components;
-
-/// <summary>
-/// On colliding with an entity that has a bloodstream will dump its solution onto them.
-/// </summary>
-[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;
-
- /// <summary>
- /// If anything covers any of these slots then the injection fails.
- /// </summary>
- [DataField("blockSlots"), ViewVariables(VVAccess.ReadWrite)]
- public SlotFlags BlockSlots = SlotFlags.MASK;
-}
--- /dev/null
+namespace Content.Server.Chemistry.Components;
+
+/// <summary>
+/// Used for embeddable entities that should try to inject a
+/// contained solution into a target when they become embedded in it.
+/// </summary>
+[RegisterComponent]
+public sealed partial class SolutionInjectOnEmbedComponent : BaseSolutionInjectOnEventComponent { }
--- /dev/null
+namespace Content.Server.Chemistry.Components;
+
+/// <summary>
+/// Used for projectile entities that should try to inject a
+/// contained solution into a target when they hit it.
+/// </summary>
+[RegisterComponent]
+public sealed partial class SolutionInjectOnProjectileHitComponent : BaseSolutionInjectOnEventComponent { }
+++ /dev/null
-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<SolutionInjectOnCollideComponent, ProjectileHitEvent>(HandleInjection);
- }
-
- private void HandleInjection(Entity<SolutionInjectOnCollideComponent> ent, ref ProjectileHitEvent args)
- {
- var component = ent.Comp;
- var target = args.Target;
-
- if (!TryComp<BloodstreamComponent>(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);
- }
-}
--- /dev/null
+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;
+
+/// <summary>
+/// System for handling the different inheritors of <see cref="BaseSolutionInjectOnEventComponent"/>.
+/// Subscribes to relevent events and performs solution injections when they are raised.
+/// </summary>
+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<SolutionInjectOnProjectileHitComponent, ProjectileHitEvent>(HandleProjectileHit);
+ SubscribeLocalEvent<SolutionInjectOnEmbedComponent, EmbedEvent>(HandleEmbed);
+ SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(HandleMeleeHit);
+ }
+
+ private void HandleProjectileHit(Entity<SolutionInjectOnProjectileHitComponent> entity, ref ProjectileHitEvent args)
+ {
+ DoInjection((entity.Owner, entity.Comp), args.Target, args.Shooter);
+ }
+
+ private void HandleEmbed(Entity<SolutionInjectOnEmbedComponent> entity, ref EmbedEvent args)
+ {
+ DoInjection((entity.Owner, entity.Comp), args.Embedded, args.Shooter);
+ }
+
+ private void HandleMeleeHit(Entity<MeleeChemicalInjectorComponent> 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<BaseSolutionInjectOnEventComponent> injectorEntity, EntityUid target, EntityUid? source = null)
+ {
+ TryInjectTargets(injectorEntity, [target], source);
+ }
+
+ /// <summary>
+ /// Filters <paramref name="targets"/> for valid targets and tries to inject a portion of <see cref="BaseSolutionInjectOnEventComponent.Solution"/> into
+ /// each valid target's bloodstream.
+ /// </summary>
+ /// <remarks>
+ /// Targets are invalid if any of the following are true:
+ /// <list type="bullet">
+ /// <item>The target does not have a bloodstream.</item>
+ /// <item><see cref="BaseSolutionInjectOnEventComponent.PierceArmor"/> is false and the target is wearing a hardsuit.</item>
+ /// <item><see cref="BaseSolutionInjectOnEventComponent.BlockSlots"/> is not NONE and the target has an item equipped in any of the specified slots.</item>
+ /// </list>
+ /// </remarks>
+ /// <returns>true if at least one target was successfully injected, otherwise false</returns>
+ private bool TryInjectTargets(Entity<BaseSolutionInjectOnEventComponent> injector, IReadOnlyList<EntityUid> 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<Entity<BloodstreamComponent>>();
+ 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<BloodstreamComponent>(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;
+ }
+}
-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;
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;
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<MeleeChemicalInjectorComponent, MeleeHitEvent>(OnChemicalInjectorHit);
SubscribeLocalEvent<MeleeSpeechComponent, MeleeHitEvent>(OnSpeechHit);
SubscribeLocalEvent<MeleeWeaponComponent, DamageExamineEvent>(OnMeleeExamineDamage);
}
}
}
-
- private void OnChemicalInjectorHit(Entity<MeleeChemicalInjectorComponent> 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<BloodstreamComponent>();
-
- 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);
- }
- }
}
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
solutions:
melee:
maxVol: 7
- - type: SolutionInjectOnCollide
+ - type: SolutionInjectOnEmbed
transferAmount: 7
- blockSlots: NONE
- fixtureId: "throw-fixture"
+ solution: melee
- type: SolutionTransfer
maxTransferAmount: 7
damage:
types:
Piercing: 3
- Slash: 3
-
+ Slash: 3
+
- type: entity
id: PelletShotgunTranquilizer
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
solution: ammo
- type: InjectableSolution
solution: ammo
- - type: SolutionInjectOnCollide
+ - type: SolutionInjectOnEmbed
transferAmount: 2
- blockSlots: NONE
+ solution: ammo
- type: SolutionTransfer
maxTransferAmount: 2
- type: Appearance
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