diff --git a/Content.Server/Chemistry/Components/VaporComponent.cs b/Content.Server/Chemistry/Components/VaporComponent.cs
deleted file mode 100644
index 551172b06c2..00000000000
--- a/Content.Server/Chemistry/Components/VaporComponent.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Robust.Shared.Map;
-
-namespace Content.Server.Chemistry.Components
-{
- [RegisterComponent]
- public sealed partial class VaporComponent : Component
- {
- public const string SolutionName = "vapor";
-
- ///
- /// Stores data on the previously reacted tile. We only want to do reaction checks once per tile.
- ///
- [DataField]
- public TileRef? PreviousTileRef;
-
- ///
- /// Percentage of the reagent that is reacted with the TileReaction.
- ///
- /// 0.5 = 50% of the reagent is reacted.
- ///
- ///
- [DataField]
- public float TransferAmountPercentage;
-
- ///
- /// The minimum amount of the reagent that will be reacted with the TileReaction.
- /// We do this to prevent floating point issues. A reagent with a low percentage transfer amount will
- /// transfer 0.01~ forever and never get deleted.
- /// Defaults to 0.05 if not defined, a good general value.
- ///
- [DataField]
- public float MinimumTransferAmount = 0.05f;
-
- [DataField]
- public bool Active;
-
- [DataField]
- public EntityUid? Origin;
- }
-}
diff --git a/Content.Server/Chemistry/EntitySystems/VaporSystem.cs b/Content.Server/Chemistry/EntitySystems/VaporSystem.cs
deleted file mode 100644
index 5558c0383de..00000000000
--- a/Content.Server/Chemistry/EntitySystems/VaporSystem.cs
+++ /dev/null
@@ -1,173 +0,0 @@
-using Content.Server.Chemistry.Components;
-using Content.Shared.Chemistry;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.Components.SolutionManager;
-using Content.Shared.Chemistry.Reagent;
-using Content.Shared.FixedPoint;
-using Content.Shared.Physics;
-using Content.Shared.Throwing;
-using Content.Shared.Chemistry.EntitySystems;
-using JetBrains.Annotations;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Events;
-using Robust.Shared.Physics.Systems;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Spawners;
-using System.Numerics;
-
-namespace Content.Server.Chemistry.EntitySystems
-{
- [UsedImplicitly]
- internal sealed partial class VaporSystem : EntitySystem
- {
- [Dependency] private IPrototypeManager _protoManager = default!;
- [Dependency] private SharedMapSystem _map = default!;
- [Dependency] private SharedPhysicsSystem _physics = default!;
- [Dependency] private SharedSolutionContainerSystem _solutionContainerSystem = default!;
- [Dependency] private ThrowingSystem _throwing = default!;
- [Dependency] private ReactiveSystem _reactive = default!;
- [Dependency] private SharedTransformSystem _transformSystem = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(HandleCollide);
- }
-
- private void HandleCollide(Entity entity, ref StartCollideEvent args)
- {
- if (!TryComp(entity.Owner, out SolutionContainerManagerComponent? contents)) return;
-
- var origin = Exists(entity.Comp.Origin) && !TerminatingOrDeleted(entity.Comp.Origin)
- ? entity.Comp.Origin
- : null;
-
- foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions((entity.Owner, contents)))
- {
- var solution = soln.Comp.Solution;
- _reactive.DoEntityReaction(args.OtherEntity, solution, ReactionMethod.Touch, origin: origin);
- }
-
- // Check for collision with a impassable object (e.g. wall) and stop
- if ((args.OtherFixture.CollisionLayer & (int)CollisionGroup.Impassable) != 0 && args.OtherFixture.Hard)
- {
- QueueDel(entity);
- }
- }
-
- public void Start(Entity vapor,
- TransformComponent vaporXform,
- Vector2 dir,
- float speed,
- MapCoordinates target,
- float aliveTime,
- EntityUid? user = null)
- {
- vapor.Comp.Active = true;
- var despawn = EnsureComp(vapor);
- despawn.Lifetime = aliveTime;
-
- // Set Move
- if (TryComp(vapor, out PhysicsComponent? physics))
- {
- _physics.SetLinearDamping(vapor, physics, 0f);
- _physics.SetAngularDamping(vapor, physics, 0f);
-
- _throwing.TryThrow(vapor, dir, speed, user: user);
-
- var distance = (target.Position - _transformSystem.GetWorldPosition(vaporXform)).Length();
- var time = (distance / physics.LinearVelocity.Length());
- despawn.Lifetime = MathF.Min(aliveTime, time);
- }
- }
-
- internal bool TryAddSolution(Entity vapor, Solution solution)
- {
- if (solution.Volume == 0)
- {
- return false;
- }
-
- if (!_solutionContainerSystem.TryGetSolution(vapor.Owner,
- VaporComponent.SolutionName,
- out var vaporSolution))
- {
- return false;
- }
-
- return _solutionContainerSystem.TryAddSolution(vaporSolution.Value, solution);
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- // Enumerate over all VaporComponents
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var vaporComp, out var container, out var xform))
- {
- // Return early if we're not active
- if (!vaporComp.Active)
- continue;
-
- // Get the current location of the vapor entity first
- if (TryComp(xform.GridUid, out MapGridComponent? gridComp))
- {
- var tile = _map.GetTileRef(xform.GridUid.Value, gridComp, xform.Coordinates);
-
- // Check if the tile is a tile we've reacted with previously. If so, skip it.
- // If we have no previous tile reference, we don't return so we can save one.
- if (vaporComp.PreviousTileRef != null && tile == vaporComp.PreviousTileRef)
- continue;
-
- // Enumerate over all the reagents in the vapor entity solution
- foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions((uid, container)))
- {
- // Iterate over the reagents in the solution
- // Reason: Each reagent in our solution may have a unique TileReaction
- // In this instance, we check individually for each reagent's TileReaction
- // This is not doing chemical reactions!
- var contents = soln.Comp.Solution;
- foreach (var reagentQuantity in contents.Contents.ToArray())
- {
- // Check if the reagent is empty
- if (reagentQuantity.Quantity == FixedPoint2.Zero)
- continue;
-
- var reagent = _protoManager.Index(reagentQuantity.Reagent.Prototype);
-
- // Limit the reaction amount to a minimum value to ensure no floating point funnies.
- // Ex: A solution with a low percentage transfer amount will slowly approach 0.01... and never get deleted
- var clampedAmount = Math.Max(
- (float)reagentQuantity.Quantity * vaporComp.TransferAmountPercentage,
- vaporComp.MinimumTransferAmount);
-
- // Preform the reagent's TileReaction
- var reaction =
- reagent.ReactionTile(tile,
- clampedAmount,
- EntityManager,
- reagentQuantity.Reagent.Data);
-
- if (reaction > reagentQuantity.Quantity)
- reaction = reagentQuantity.Quantity;
-
- _solutionContainerSystem.RemoveReagent(soln, reagentQuantity.Reagent, reaction);
- }
-
- // Delete the vapor entity if it has no contents
- if (contents.Volume == 0)
- QueueDel(uid);
-
- }
-
- // Set the previous tile reference to the current tile
- vaporComp.PreviousTileRef = tile;
- }
- }
- }
- }
-}
diff --git a/Content.Server/Fluids/EntitySystems/SpraySystem.cs b/Content.Server/Fluids/EntitySystems/SpraySystem.cs
index 1452f0cb1ee..f99be826a36 100644
--- a/Content.Server/Fluids/EntitySystems/SpraySystem.cs
+++ b/Content.Server/Fluids/EntitySystems/SpraySystem.cs
@@ -1,213 +1,5 @@
-using Content.Server.Chemistry.Components;
-using Content.Server.Chemistry.EntitySystems;
-using Content.Server.Gravity;
-using Content.Server.Popups;
-using Content.Shared.CCVar;
-using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.FixedPoint;
-using Content.Shared.Fluids;
-using Content.Shared.Interaction;
-using Content.Shared.Timing;
-using Content.Shared.Vapor;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Configuration;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Prototypes;
-using System.Numerics;
using Content.Shared.Fluids.EntitySystems;
-using Content.Shared.Fluids.Components;
-using Robust.Server.Containers;
-using Robust.Shared.Map;
namespace Content.Server.Fluids.EntitySystems;
-public sealed partial class SpraySystem : SharedSpraySystem
-{
- [Dependency] private IPrototypeManager _proto = default!;
- [Dependency] private GravitySystem _gravity = default!;
- [Dependency] private PhysicsSystem _physics = default!;
- [Dependency] private UseDelaySystem _useDelay = default!;
- [Dependency] private PopupSystem _popupSystem = default!;
- [Dependency] private SharedAudioSystem _audio = default!;
- [Dependency] private SharedSolutionContainerSystem _solutionContainer = default!;
- [Dependency] private VaporSystem _vapor = default!;
- [Dependency] private SharedAppearanceSystem _appearance = default!;
- [Dependency] private SharedTransformSystem _transform = default!;
- [Dependency] private IConfigurationManager _cfg = default!;
- [Dependency] private ContainerSystem _container = default!;
-
- private float _gridImpulseMultiplier;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnAfterInteract);
- SubscribeLocalEvent(OnActivateInWorld);
- Subs.CVar(_cfg, CCVars.GridImpulseMultiplier, UpdateGridMassMultiplier, true);
- }
-
- private void OnActivateInWorld(Entity entity, ref UserActivateInWorldEvent args)
- {
- if (args.Handled)
- return;
-
- args.Handled = true;
-
- var targetMapPos = _transform.GetMapCoordinates(GetEntityQuery().GetComponent(args.Target));
-
- Spray(entity, targetMapPos, args.User);
- }
-
- private void UpdateGridMassMultiplier(float value)
- {
- _gridImpulseMultiplier = value;
- }
-
- private void OnAfterInteract(Entity entity, ref AfterInteractEvent args)
- {
- if (args.Handled)
- return;
-
- args.Handled = true;
-
- var clickPos = _transform.ToMapCoordinates(args.ClickLocation);
-
- Spray(entity, clickPos, args.User);
- }
-
- public override void Spray(Entity entity, EntityUid? user = null)
- {
- var xform = Transform(entity);
- var throwing = xform.LocalRotation.ToWorldVec() * entity.Comp.SprayDistance;
- var direction = xform.Coordinates.Offset(throwing);
-
- Spray(entity, _transform.ToMapCoordinates(direction), user);
- }
-
- public override void Spray(Entity entity, MapCoordinates mapcoord, EntityUid? user = null)
- {
- if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var soln, out var solution))
- return;
-
- var ev = new SprayAttemptEvent(user);
- RaiseLocalEvent(entity, ref ev);
- if (ev.Cancelled)
- {
- if (ev.CancelPopupMessage != null && user != null)
- _popupSystem.PopupEntity(Loc.GetString(ev.CancelPopupMessage), entity.Owner, user.Value);
- return;
- }
-
- if (_useDelay.IsDelayed((entity, null)))
- return;
-
- if (solution.Volume <= 0)
- {
- if (user != null)
- _popupSystem.PopupEntity(Loc.GetString(entity.Comp.SprayEmptyPopupMessage, ("entity", entity)), entity.Owner, user.Value);
- return;
- }
-
- var xformQuery = GetEntityQuery();
- var sprayerXform = xformQuery.GetComponent(entity);
-
- var sprayerMapPos = _transform.GetMapCoordinates(sprayerXform);
- var clickMapPos = mapcoord;
-
- var diffPos = clickMapPos.Position - sprayerMapPos.Position;
- if (diffPos == Vector2.Zero || diffPos == Vector2Helpers.NaN)
- return;
-
- var diffNorm = diffPos.Normalized();
- var diffLength = diffPos.Length();
-
- if (diffLength > entity.Comp.SprayDistance)
- {
- diffLength = entity.Comp.SprayDistance;
- }
-
- var diffAngle = diffNorm.ToAngle();
-
- // Vectors to determine the spawn offset of the vapor clouds.
- var threeQuarters = diffNorm * 0.75f;
- var quarter = diffNorm * 0.25f;
-
- var amount = Math.Max(Math.Min((solution.Volume / entity.Comp.TransferAmount).Int(), entity.Comp.VaporAmount), 1);
- var spread = entity.Comp.VaporSpread / amount;
-
- for (var i = 0; i < amount; i++)
- {
- var rotation = new Angle(diffAngle + Angle.FromDegrees(spread * i) -
- Angle.FromDegrees(spread * (amount - 1) / 2));
-
- // Calculate the destination for the vapor cloud. Limit to the maximum spray distance.
- var target = sprayerMapPos
- .Offset((diffNorm + rotation.ToVec()).Normalized() * diffLength + quarter);
-
- var distance = (target.Position - sprayerMapPos.Position).Length();
- if (distance > entity.Comp.SprayDistance)
- target = sprayerMapPos.Offset(diffNorm * entity.Comp.SprayDistance);
-
- var adjustedSolutionAmount = entity.Comp.TransferAmount / entity.Comp.VaporAmount;
- var newSolution = _solutionContainer.SplitSolution(soln.Value, adjustedSolutionAmount);
-
- if (newSolution.Volume <= FixedPoint2.Zero)
- break;
-
- // Spawn the vapor cloud onto the grid/map the user is present on. Offset the start position based on how far the target destination is.
- var vaporPos = sprayerMapPos.Offset(distance < 1 ? quarter : threeQuarters);
- var vapor = Spawn(entity.Comp.SprayedPrototype, vaporPos);
- var vaporXform = xformQuery.GetComponent(vapor);
-
- _transform.SetWorldRotation(vaporXform, rotation);
-
- if (TryComp(vapor, out AppearanceComponent? appearance))
- {
- _appearance.SetData(vapor, VaporVisuals.Color, solution.GetColor(_proto).WithAlpha(1f), appearance);
- _appearance.SetData(vapor, VaporVisuals.State, true, appearance);
- }
-
- // Add the solution to the vapor and actually send the thing
- var vaporComponent = Comp(vapor);
- var ent = (vapor, vaporComponent);
- vaporComponent.Origin = user;
- _vapor.TryAddSolution(ent, newSolution);
-
- // impulse direction is defined in world-coordinates, not local coordinates
- var impulseDirection = rotation.ToVec();
- var time = diffLength / entity.Comp.SprayVelocity;
-
- _vapor.Start(ent, vaporXform, impulseDirection * diffLength, entity.Comp.SprayVelocity, target, time, user);
-
- var thingGettingPushed = entity.Owner;
- if (_container.TryGetOuterContainer(entity, sprayerXform, out var container))
- thingGettingPushed = container.Owner;
-
- if (TryComp(thingGettingPushed, out var body))
- {
- if (_gravity.IsWeightless(thingGettingPushed))
- {
- // push back the player
- _physics.ApplyLinearImpulse(thingGettingPushed, -impulseDirection * entity.Comp.PushbackAmount, body: body);
- }
- else
- {
- // push back the grid the player is standing on
- var userTransform = Transform(thingGettingPushed);
- if (userTransform.GridUid == userTransform.ParentUid)
- {
- // apply both linear and angular momentum depending on the player position
- // multiply by a cvar because grid mass is currently extremely small compared to all other masses
- _physics.ApplyLinearImpulse(userTransform.GridUid.Value, -impulseDirection * _gridImpulseMultiplier * entity.Comp.PushbackAmount, userTransform.LocalPosition);
- }
- }
- }
- }
-
- _audio.PlayPvs(entity.Comp.SpraySound, entity, entity.Comp.SpraySound.Params.WithVariation(0.125f));
-
- _useDelay.TryResetDelay(entity);
- }
-}
+public sealed partial class SpraySystem : SharedSpraySystem;
diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Solution.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Solution.cs
index 894e89be93e..0b182bc3bab 100644
--- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Solution.cs
+++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Solution.cs
@@ -1,4 +1,4 @@
-using Content.Server.Chemistry.Components;
+using Content.Shared._ES.Chemistry.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
diff --git a/Content.Shared/CCVar/CCVars.Shuttle.cs b/Content.Shared/CCVar/CCVars.Shuttle.cs
index ad8e9b2d70c..e77b62c961d 100644
--- a/Content.Shared/CCVar/CCVars.Shuttle.cs
+++ b/Content.Shared/CCVar/CCVars.Shuttle.cs
@@ -200,7 +200,7 @@ public sealed partial class CCVars
/// At the moment they have a very low mass of roughly 0.48 kg per tile independent of any walls or anchored objects on them.
///
public static readonly CVarDef GridImpulseMultiplier =
- CVarDef.Create("shuttle.grid_impulse_multiplier", 0.01f, CVar.SERVERONLY);
+ CVarDef.Create("shuttle.grid_impulse_multiplier", 0.01f, CVar.SERVER | CVar.REPLICATED);
#region impacts
diff --git a/Content.Shared/Fluids/EntitySystems/SharedSpraySystem.cs b/Content.Shared/Fluids/EntitySystems/SharedSpraySystem.cs
index 42883f385e2..0e363bc8144 100644
--- a/Content.Shared/Fluids/EntitySystems/SharedSpraySystem.cs
+++ b/Content.Shared/Fluids/EntitySystems/SharedSpraySystem.cs
@@ -1,18 +1,57 @@
+using System.Numerics;
+using Content.Shared._ES.Chemistry;
+using Content.Shared._ES.Chemistry.Components;
using Content.Shared.Actions;
+using Content.Shared.CCVar;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.FixedPoint;
using Content.Shared.Fluids.Components;
+using Content.Shared.Gravity;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.Timing;
+using Content.Shared.Vapor;
using Content.Shared.Verbs;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Configuration;
+using Robust.Shared.Containers;
using Robust.Shared.Map;
+using Robust.Shared.Network;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Prototypes;
namespace Content.Shared.Fluids.EntitySystems;
-public abstract class SharedSpraySystem : EntitySystem
+public abstract partial class SharedSpraySystem : EntitySystem
{
+ [Dependency] private IConfigurationManager _cfg = default!;
+ [Dependency] private INetManager _net = default!;
+ [Dependency] private IPrototypeManager _proto = default!;
+ [Dependency] private SharedGravitySystem _gravity = default!;
+ [Dependency] private SharedPhysicsSystem _physics = default!;
+ [Dependency] private UseDelaySystem _useDelay = default!;
+ [Dependency] private SharedPopupSystem _popupSystem = default!;
+ [Dependency] private SharedAudioSystem _audio = default!;
+ [Dependency] private SharedSolutionContainerSystem _solutionContainer = default!;
+ [Dependency] private VaporSystem _vapor = default!;
+ [Dependency] private SharedAppearanceSystem _appearance = default!;
+ [Dependency] private SharedTransformSystem _transform = default!;
+ [Dependency] private SharedContainerSystem _container = default!;
+
+ private float _gridImpulseMultiplier;
+
+ private const string SprayUseDelay = "spray-delay";
+
public override void Initialize()
{
base.Initialize();
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnActivateInWorld);
SubscribeLocalEvent>(OnGetVerb);
SubscribeLocalEvent(SprayLiquid);
+ Subs.CVar(_cfg, CCVars.GridImpulseMultiplier, UpdateGridMassMultiplier, true);
}
private void SprayLiquid(SprayLiquidEvent ev)
@@ -53,6 +92,35 @@ private void OnGetVerb(Entity entity, ref GetVerbsEvent entity, ref UserActivateInWorldEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ args.Handled = true;
+
+ var targetMapPos = _transform.GetMapCoordinates(GetEntityQuery().GetComponent(args.Target));
+
+ Spray(entity, targetMapPos, args.User);
+ }
+
+ private void UpdateGridMassMultiplier(float value)
+ {
+ _gridImpulseMultiplier = value;
+ }
+
+ private void OnAfterInteract(Entity entity, ref AfterInteractEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ args.Handled = true;
+
+ var clickPos = _transform.ToMapCoordinates(args.ClickLocation);
+
+ Spray(entity, clickPos, args.User);
+ }
+
///
/// Spray starting from the entity, to the given coordinates. If the user is supplied, will give them failure
/// popups and will also push them in space.
@@ -60,9 +128,142 @@ private void OnGetVerb(Entity entity, ref GetVerbsEventEntity that is spraying.
/// The coordinates being aimed at.
/// The user that is using the spraying device.
- public virtual void Spray(Entity entity, MapCoordinates mapcoord, EntityUid? user = null)
+ public void Spray(Entity entity, MapCoordinates mapcoord, EntityUid? user = null)
{
- // do nothing!
+ if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var soln, out var solution))
+ return;
+
+ var ev = new SprayAttemptEvent(user);
+ RaiseLocalEvent(entity, ref ev);
+ if (ev.Cancelled)
+ {
+ if (ev.CancelPopupMessage != null && user != null)
+ _popupSystem.PopupEntity(ev.CancelPopupMessage, entity.Owner, user.Value);
+ return;
+ }
+
+ if (_useDelay.IsDelayed((entity, null), SprayUseDelay))
+ return;
+
+ if (solution.Volume <= 0)
+ {
+ if (user != null)
+ _popupSystem.PopupEntity(Loc.GetString(entity.Comp.SprayEmptyPopupMessage, ("entity", entity)), entity.Owner, user.Value);
+ return;
+ }
+
+ var xformQuery = GetEntityQuery();
+ var sprayerXform = xformQuery.GetComponent(entity);
+
+ var sprayerMapPos = _transform.GetMapCoordinates(sprayerXform);
+ var clickMapPos = mapcoord;
+
+ var diffPos = clickMapPos.Position - sprayerMapPos.Position;
+ if (diffPos == Vector2.Zero || diffPos == Vector2Helpers.NaN)
+ return;
+
+ var diffNorm = diffPos.Normalized();
+ var diffLength = entity.Comp.SprayDistance;
+
+ var diffAngle = diffNorm.ToAngle();
+
+ // Vectors to determine the spawn offset of the vapor clouds.
+ var threeQuarters = diffNorm * 0.75f;
+ var quarter = diffNorm * 0.25f;
+
+ var amount = Math.Max(Math.Min((solution.Volume / (entity.Comp.TransferAmount / entity.Comp.VaporAmount)).Int(), entity.Comp.VaporAmount), 1);
+ var spread = entity.Comp.VaporSpread / amount;
+
+ var sprayedSolution = _solutionContainer.SplitSolution(soln.Value, entity.Comp.TransferAmount);
+
+ for (var i = 0; i < amount; i++)
+ {
+ var rotation = new Angle(diffAngle + Angle.FromDegrees(spread * i) -
+ Angle.FromDegrees(spread * (amount - 1) / 2));
+
+ // Calculate the destination for the vapor cloud. Limit to the maximum spray distance.
+ var target = sprayerMapPos
+ .Offset((diffNorm + rotation.ToVec()).Normalized() * diffLength + quarter);
+
+ var distance = (target.Position - sprayerMapPos.Position).Length();
+ if (distance > entity.Comp.SprayDistance)
+ target = sprayerMapPos.Offset(diffNorm * entity.Comp.SprayDistance);
+
+ var adjustedSolutionAmount = i != amount
+ ? entity.Comp.TransferAmount / entity.Comp.VaporAmount
+ : sprayedSolution.Volume;
+ var newSolution = sprayedSolution.SplitSolution(adjustedSolutionAmount);
+
+ if (newSolution.Volume <= FixedPoint2.Zero)
+ break;
+
+
+ // impulse direction is defined in world-coordinates, not local coordinates
+ var impulseDirection = rotation.ToVec();
+
+ if (_net.IsServer)
+ {
+ // Spawn the vapor cloud onto the grid/map the user is present on. Offset the start position based on how far the target destination is.
+ var vaporPos = sprayerMapPos.Offset(distance < 1 ? quarter : threeQuarters);
+ var vapor = Spawn(entity.Comp.SprayedPrototype, vaporPos);
+ var vaporXform = xformQuery.GetComponent(vapor);
+
+ _transform.SetWorldRotation(vaporXform, rotation);
+
+ if (TryComp(vapor, out AppearanceComponent? appearance))
+ {
+ _appearance.SetData(vapor,
+ VaporVisuals.Color,
+ newSolution.GetColor(_proto).WithAlpha(1f),
+ appearance);
+ _appearance.SetData(vapor, VaporVisuals.State, true, appearance);
+ }
+
+ // Add the solution to the vapor and actually send the thing
+ var vaporComponent = Comp(vapor);
+ var ent = (vapor, vaporComponent);
+ vaporComponent.Origin = user;
+ _vapor.TryAddSolution(ent, newSolution);
+
+ var time = diffLength / entity.Comp.SprayVelocity;
+
+ _vapor.Start(ent,
+ vaporXform,
+ impulseDirection * diffLength,
+ entity.Comp.SprayVelocity,
+ target,
+ time,
+ user);
+ }
+
+ var thingGettingPushed = entity.Owner;
+ if (_container.TryGetOuterContainer(entity, sprayerXform, out var container))
+ thingGettingPushed = container.Owner;
+
+ if (TryComp(thingGettingPushed, out var body))
+ {
+ if (_gravity.IsWeightless(thingGettingPushed))
+ {
+ // push back the player
+ _physics.ApplyLinearImpulse(thingGettingPushed, -impulseDirection * entity.Comp.PushbackAmount, body: body);
+ }
+ else
+ {
+ // push back the grid the player is standing on
+ var userTransform = Transform(thingGettingPushed);
+ if (userTransform.GridUid == userTransform.ParentUid)
+ {
+ // apply both linear and angular momentum depending on the player position
+ // multiply by a cvar because grid mass is currently extremely small compared to all other masses
+ _physics.ApplyLinearImpulse(userTransform.GridUid.Value, -impulseDirection * _gridImpulseMultiplier * entity.Comp.PushbackAmount, userTransform.LocalPosition);
+ }
+ }
+ }
+ }
+
+ _audio.PlayPredicted(entity.Comp.SpraySound, entity, user, entity.Comp.SpraySound.Params.WithVariation(0.125f));
+
+ _useDelay.TryResetDelay(entity, id: SprayUseDelay);
}
///
@@ -70,9 +271,13 @@ public virtual void Spray(Entity entity, MapCoordinates mapcoord
///
/// Entity that is spraying.
/// User that is using the spraying device.
- public virtual void Spray(Entity entity, EntityUid? user = null)
+ public void Spray(Entity entity, EntityUid? user = null)
{
- // do nothing!
+ var xform = Transform(entity);
+ var throwing = xform.LocalRotation.ToWorldVec() * entity.Comp.SprayDistance;
+ var direction = xform.Coordinates.Offset(throwing);
+
+ Spray(entity, _transform.ToMapCoordinates(direction), user);
}
}
diff --git a/Content.Shared/_ES/Chemistry/Components/VaporComponent.cs b/Content.Shared/_ES/Chemistry/Components/VaporComponent.cs
new file mode 100644
index 00000000000..fcfcb492010
--- /dev/null
+++ b/Content.Shared/_ES/Chemistry/Components/VaporComponent.cs
@@ -0,0 +1,39 @@
+using Robust.Shared.Map;
+
+namespace Content.Shared._ES.Chemistry.Components;
+
+[RegisterComponent]
+public sealed partial class VaporComponent : Component
+{
+ public const string SolutionName = "vapor";
+
+ ///
+ /// Stores data on the previously reacted tile. We only want to do reaction checks once per tile.
+ ///
+ [DataField]
+ public TileRef? PreviousTileRef;
+
+ ///
+ /// Percentage of the reagent that is reacted with the TileReaction.
+ ///
+ /// 0.5 = 50% of the reagent is reacted.
+ ///
+ ///
+ [DataField]
+ public float TransferAmountPercentage;
+
+ ///
+ /// The minimum amount of the reagent that will be reacted with the TileReaction.
+ /// We do this to prevent floating point issues. A reagent with a low percentage transfer amount will
+ /// transfer 0.01~ forever and never get deleted.
+ /// Defaults to 0.05 if not defined, a good general value.
+ ///
+ [DataField]
+ public float MinimumTransferAmount = 0.05f;
+
+ [DataField]
+ public bool Active;
+
+ [DataField]
+ public EntityUid? Origin;
+}
diff --git a/Content.Shared/_ES/Chemistry/VaporSystem.cs b/Content.Shared/_ES/Chemistry/VaporSystem.cs
new file mode 100644
index 00000000000..3e5552dfea4
--- /dev/null
+++ b/Content.Shared/_ES/Chemistry/VaporSystem.cs
@@ -0,0 +1,173 @@
+using Content.Shared.Chemistry;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.FixedPoint;
+using Content.Shared.Physics;
+using Content.Shared.Throwing;
+using Content.Shared.Chemistry.EntitySystems;
+using JetBrains.Annotations;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Events;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Spawners;
+using System.Numerics;
+using Content.Shared._ES.Chemistry.Components;
+
+namespace Content.Shared._ES.Chemistry;
+
+[UsedImplicitly]
+public sealed partial class VaporSystem : EntitySystem
+{
+ [Dependency] private IPrototypeManager _protoManager = default!;
+ [Dependency] private SharedMapSystem _map = default!;
+ [Dependency] private SharedPhysicsSystem _physics = default!;
+ [Dependency] private SharedSolutionContainerSystem _solutionContainerSystem = default!;
+ [Dependency] private ThrowingSystem _throwing = default!;
+ [Dependency] private ReactiveSystem _reactive = default!;
+ [Dependency] private SharedTransformSystem _transformSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(HandleCollide);
+ }
+
+ private void HandleCollide(Entity entity, ref StartCollideEvent args)
+ {
+ if (!TryComp(entity.Owner, out SolutionContainerManagerComponent? contents))
+ return;
+
+ var origin = Exists(entity.Comp.Origin) && !TerminatingOrDeleted(entity.Comp.Origin)
+ ? entity.Comp.Origin
+ : null;
+
+ foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions((entity.Owner, contents)))
+ {
+ var solution = soln.Comp.Solution;
+ _reactive.DoEntityReaction(args.OtherEntity, solution, ReactionMethod.Touch, origin: origin);
+ }
+
+ // Check for collision with a impassable object (e.g. wall) and stop
+ if ((args.OtherFixture.CollisionLayer & (int)CollisionGroup.Impassable) != 0 && args.OtherFixture.Hard)
+ {
+ PredictedQueueDel(entity);
+ }
+ }
+
+ public void Start(Entity vapor,
+ TransformComponent vaporXform,
+ Vector2 dir,
+ float speed,
+ MapCoordinates target,
+ float aliveTime,
+ EntityUid? user = null)
+ {
+ vapor.Comp.Active = true;
+ var despawn = EnsureComp(vapor);
+ despawn.Lifetime = aliveTime;
+
+ // Set Move
+ if (TryComp(vapor, out PhysicsComponent? physics))
+ {
+ _physics.SetLinearDamping(vapor, physics, 0f);
+ _physics.SetAngularDamping(vapor, physics, 0f);
+
+ _throwing.TryThrow(vapor, dir, speed, user: user);
+
+ var distance = (target.Position - _transformSystem.GetWorldPosition(vaporXform)).Length();
+ var time = (distance / physics.LinearVelocity.Length());
+ despawn.Lifetime = MathF.Min(aliveTime, time);
+ }
+ }
+
+ internal bool TryAddSolution(Entity vapor, Solution solution)
+ {
+ if (solution.Volume == 0)
+ {
+ return false;
+ }
+
+ if (!_solutionContainerSystem.TryGetSolution(vapor.Owner,
+ VaporComponent.SolutionName,
+ out var vaporSolution))
+ {
+ return false;
+ }
+
+ return _solutionContainerSystem.TryAddSolution(vaporSolution.Value, solution);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ // Enumerate over all VaporComponents
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var vaporComp, out var container, out var xform))
+ {
+ // Return early if we're not active
+ if (!vaporComp.Active)
+ continue;
+
+ // Get the current location of the vapor entity first
+ if (TryComp(xform.GridUid, out MapGridComponent? gridComp))
+ {
+ var tile = _map.GetTileRef(xform.GridUid.Value, gridComp, xform.Coordinates);
+
+ // Check if the tile is a tile we've reacted with previously. If so, skip it.
+ // If we have no previous tile reference, we don't return so we can save one.
+ if (vaporComp.PreviousTileRef != null && tile == vaporComp.PreviousTileRef)
+ continue;
+
+ // Enumerate over all the reagents in the vapor entity solution
+ foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions((uid, container)))
+ {
+ // Iterate over the reagents in the solution
+ // Reason: Each reagent in our solution may have a unique TileReaction
+ // In this instance, we check individually for each reagent's TileReaction
+ // This is not doing chemical reactions!
+ var contents = soln.Comp.Solution;
+ foreach (var reagentQuantity in contents.Contents.ToArray())
+ {
+ // Check if the reagent is empty
+ if (reagentQuantity.Quantity == FixedPoint2.Zero)
+ continue;
+
+ var reagent = _protoManager.Index(reagentQuantity.Reagent.Prototype);
+
+ // Limit the reaction amount to a minimum value to ensure no floating point funnies.
+ // Ex: A solution with a low percentage transfer amount will slowly approach 0.01... and never get deleted
+ var clampedAmount = Math.Max(
+ (float)reagentQuantity.Quantity * vaporComp.TransferAmountPercentage,
+ vaporComp.MinimumTransferAmount);
+
+ // Preform the reagent's TileReaction
+ var reaction =
+ reagent.ReactionTile(tile,
+ clampedAmount,
+ EntityManager,
+ reagentQuantity.Reagent.Data);
+
+ if (reaction > reagentQuantity.Quantity)
+ reaction = reagentQuantity.Quantity;
+
+ _solutionContainerSystem.RemoveReagent(soln, reagentQuantity.Reagent, reaction);
+ }
+
+ // Delete the vapor entity if it has no contents
+ if (contents.Volume == 0)
+ PredictedQueueDel(uid);
+
+ }
+
+ // Set the previous tile reference to the current tile
+ vaporComp.PreviousTileRef = tile;
+ }
+ }
+ }
+}
diff --git a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml
index 16bd3b5541f..18c6d58472e 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml
@@ -31,6 +31,9 @@
maxTransferAmount: 100
transferAmount: 100
- type: UseDelay
+ delays:
+ spray-delay:
+ length: 1
- type: Spray
transferAmount: 10
pushbackAmount: 60