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