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:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <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";
|
||||
}
|
||||
}
|
||||
/// <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 { }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -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 { }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -36,20 +30,15 @@ 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!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(OnChemicalInjectorHit);
|
||||
SubscribeLocalEvent<MeleeSpeechComponent, MeleeHitEvent>(OnSpeechHit);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user