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