Combine solution injection systems; Fix embeddable injectors (#26268)

* Combine injection systems

* Update Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
Tayrtahn
2024-03-31 23:39:34 -04:00
committed by GitHub
parent d8d4feec38
commit d0d12760a8
12 changed files with 249 additions and 182 deletions

View File

@@ -0,0 +1,60 @@
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;
}

View File

@@ -1,31 +1,8 @@
using Content.Shared.FixedPoint; namespace Content.Server.Chemistry.Components;
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] [RegisterComponent]
public sealed partial class MeleeChemicalInjectorComponent : Component public sealed partial class MeleeChemicalInjectorComponent : BaseSolutionInjectOnEventComponent { }
{
[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";
}
}

View File

@@ -1,28 +0,0 @@
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;
}

View File

@@ -0,0 +1,8 @@
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 { }

View File

@@ -0,0 +1,8 @@
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 { }

View File

@@ -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<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);
}
}

View File

@@ -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;
/// <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;
}
}

View File

@@ -1,8 +1,4 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chat.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.CombatMode.Disarm;
using Content.Server.Movement.Systems; using Content.Server.Movement.Systems;
using Content.Shared.Actions.Events; using Content.Shared.Actions.Events;
@@ -14,12 +10,10 @@ using Content.Shared.Database;
using Content.Shared.Effects; using Content.Shared.Effects;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Inventory;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Speech.Components; using Content.Shared.Speech.Components;
using Content.Shared.StatusEffect; using Content.Shared.StatusEffect;
using Content.Shared.Tag;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio; using Robust.Shared.Audio;
@@ -36,20 +30,15 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
{ {
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly DamageExamineSystem _damageExamine = default!; [Dependency] private readonly DamageExamineSystem _damageExamine = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly LagCompensationSystem _lag = default!; [Dependency] private readonly LagCompensationSystem _lag = default!;
[Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly SolutionContainerSystem _solutions = default!;
[Dependency] private readonly TagSystem _tag = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(OnChemicalInjectorHit);
SubscribeLocalEvent<MeleeSpeechComponent, MeleeHitEvent>(OnSpeechHit); SubscribeLocalEvent<MeleeSpeechComponent, MeleeHitEvent>(OnSpeechHit);
SubscribeLocalEvent<MeleeWeaponComponent, DamageExamineEvent>(OnMeleeExamineDamage); SubscribeLocalEvent<MeleeWeaponComponent, DamageExamineEvent>(OnMeleeExamineDamage);
} }
@@ -263,47 +252,4 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
} }
} }
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);
}
}
} }

View File

@@ -53,10 +53,10 @@
solution: melee solution: melee
- type: InjectableSolution - type: InjectableSolution
solution: melee solution: melee
- type: SolutionInjectOnCollide - type: SolutionInjectOnEmbed
transferAmount: 2 transferAmount: 2
solution: melee
blockSlots: OUTERCLOTHING blockSlots: OUTERCLOTHING
fixtureId: "throw-fixture"
- type: SolutionTransfer - type: SolutionTransfer
maxTransferAmount: 2 maxTransferAmount: 2
- type: Damageable - type: Damageable
@@ -124,10 +124,9 @@
solutions: solutions:
melee: melee:
maxVol: 7 maxVol: 7
- type: SolutionInjectOnCollide - type: SolutionInjectOnEmbed
transferAmount: 7 transferAmount: 7
blockSlots: NONE solution: melee
fixtureId: "throw-fixture"
- type: SolutionTransfer - type: SolutionTransfer
maxTransferAmount: 7 maxTransferAmount: 7

View File

@@ -110,9 +110,9 @@
solution: ammo solution: ammo
- type: DrainableSolution - type: DrainableSolution
solution: ammo solution: ammo
- type: SolutionInjectOnCollide - type: SolutionInjectOnProjectileHit
transferAmount: 15 transferAmount: 15
blockSlots: NONE #tranquillizer darts shouldn't be blocked by a mask solution: ammo
- type: InjectableSolution - type: InjectableSolution
solution: ammo solution: ammo

View File

@@ -50,9 +50,9 @@
solution: ammo solution: ammo
- type: InjectableSolution - type: InjectableSolution
solution: ammo solution: ammo
- type: SolutionInjectOnCollide - type: SolutionInjectOnEmbed
transferAmount: 2 transferAmount: 2
blockSlots: NONE solution: ammo
- type: SolutionTransfer - type: SolutionTransfer
maxTransferAmount: 2 maxTransferAmount: 2
- type: Appearance - type: Appearance

View File

@@ -65,10 +65,9 @@
solution: melee solution: melee
- type: InjectableSolution - type: InjectableSolution
solution: melee solution: melee
- type: SolutionInjectOnCollide - type: SolutionInjectOnEmbed
transferAmount: 2 transferAmount: 2
fixtureId: "throw-fixture" solution: melee
blockSlots: NONE
- type: SolutionTransfer - type: SolutionTransfer
maxTransferAmount: 2 maxTransferAmount: 2
- type: Wieldable - type: Wieldable