diff --git a/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs b/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs index bf47768274..1afed38966 100644 --- a/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs +++ b/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs @@ -110,7 +110,7 @@ public sealed class AbsorbentTest solutionContainerSystem.AddSolution(refillableSoln.Value, new Solution(NonEvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfNonEvaporable)); // Act - absorbentSystem.Mop(user, refillable, absorbent, component); + absorbentSystem.Mop((absorbent, component), user, refillable); // Assert var absorbentComposition = absorbentSolution.GetReagentPrototypes(prototypeManager).ToDictionary(r => r.Key.ID, r => r.Value); @@ -167,7 +167,7 @@ public sealed class AbsorbentTest solutionContainerSystem.AddSolution(refillableSoln.Value, new Solution(NonEvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfNonEvaporable)); // Act - absorbentSystem.Mop(user, refillable, absorbent, component); + absorbentSystem.Mop((absorbent, component), user, refillable); // Assert var absorbentComposition = absorbentSolution.GetReagentPrototypes(prototypeManager).ToDictionary(r => r.Key.ID, r => r.Value); diff --git a/Content.Server/Chemistry/EntitySystems/SolutionRegenerationSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionRegenerationSystem.cs deleted file mode 100644 index bccd594706..0000000000 --- a/Content.Server/Chemistry/EntitySystems/SolutionRegenerationSystem.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Content.Server.Chemistry.Components; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.FixedPoint; -using Robust.Shared.Timing; - -namespace Content.Server.Chemistry.EntitySystems; - -public sealed class SolutionRegenerationSystem : EntitySystem -{ - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; - [Dependency] private readonly IGameTiming _timing = default!; - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var regen, out var manager)) - { - if (_timing.CurTime < regen.NextRegenTime) - continue; - - // timer ignores if its full, it's just a fixed cycle - regen.NextRegenTime = _timing.CurTime + regen.Duration; - if (_solutionContainer.ResolveSolution((uid, manager), regen.SolutionName, ref regen.SolutionRef, out var solution)) - { - var amount = FixedPoint2.Min(solution.AvailableVolume, regen.Generated.Volume); - if (amount <= FixedPoint2.Zero) - continue; - - // dont bother cloning and splitting if adding the whole thing - Solution generated; - if (amount == regen.Generated.Volume) - { - generated = regen.Generated; - } - else - { - generated = regen.Generated.Clone().SplitSolution(amount); - } - - _solutionContainer.TryAddSolution(regen.SolutionRef.Value, generated); - } - } - } -} diff --git a/Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs b/Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs index 28c36602e1..c7348b0856 100644 --- a/Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs +++ b/Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs @@ -1,352 +1,6 @@ -using System.Numerics; -using Content.Server.Popups; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.FixedPoint; using Content.Shared.Fluids; -using Content.Shared.Fluids.Components; -using Content.Shared.Interaction; -using Content.Shared.Timing; -using Content.Shared.Weapons.Melee; -using Robust.Server.Audio; -using Robust.Server.GameObjects; -using Robust.Shared.Map.Components; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; namespace Content.Server.Fluids.EntitySystems; /// -public sealed class AbsorbentSystem : SharedAbsorbentSystem -{ - private static readonly EntProtoId Sparkles = "PuddleSparkle"; - - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly AudioSystem _audio = default!; - [Dependency] private readonly PopupSystem _popups = default!; - [Dependency] private readonly PuddleSystem _puddleSystem = default!; - [Dependency] private readonly SharedMeleeWeaponSystem _melee = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly UseDelaySystem _useDelay = default!; - [Dependency] private readonly MapSystem _mapSystem = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnAbsorbentInit); - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnActivateInWorld); - SubscribeLocalEvent(OnAbsorbentSolutionChange); - } - - private void OnAbsorbentInit(EntityUid uid, AbsorbentComponent component, ComponentInit args) - { - // TODO: I know dirty on init but no prediction moment. - UpdateAbsorbent(uid, component); - } - - private void OnAbsorbentSolutionChange(EntityUid uid, AbsorbentComponent component, ref SolutionContainerChangedEvent args) - { - UpdateAbsorbent(uid, component); - } - - private void UpdateAbsorbent(EntityUid uid, AbsorbentComponent component) - { - if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out _, out var solution)) - return; - - var oldProgress = component.Progress.ShallowClone(); - component.Progress.Clear(); - - var mopReagent = solution.GetTotalPrototypeQuantity(_puddleSystem.GetAbsorbentReagents(solution)); - if (mopReagent > FixedPoint2.Zero) - { - component.Progress[solution.GetColorWithOnly(_prototype, _puddleSystem.GetAbsorbentReagents(solution))] = mopReagent.Float(); - } - - var otherColor = solution.GetColorWithout(_prototype, _puddleSystem.GetAbsorbentReagents(solution)); - var other = (solution.Volume - mopReagent).Float(); - - if (other > 0f) - { - component.Progress[otherColor] = other; - } - - var remainder = solution.AvailableVolume; - - if (remainder > FixedPoint2.Zero) - { - component.Progress[Color.DarkGray] = remainder.Float(); - } - - if (component.Progress.Equals(oldProgress)) - return; - - Dirty(uid, component); - } - - private void OnActivateInWorld(EntityUid uid, AbsorbentComponent component, UserActivateInWorldEvent args) - { - if (args.Handled) - return; - - Mop(uid, args.Target, uid, component); - args.Handled = true; - } - - private void OnAfterInteract(EntityUid uid, AbsorbentComponent component, AfterInteractEvent args) - { - if (!args.CanReach || args.Handled || args.Target == null) - return; - - Mop(args.User, args.Target.Value, args.Used, component); - args.Handled = true; - } - - public void Mop(EntityUid user, EntityUid target, EntityUid used, AbsorbentComponent component) - { - if (!_solutionContainerSystem.TryGetSolution(used, component.SolutionName, out var absorberSoln)) - return; - - if (TryComp(used, out var useDelay) - && _useDelay.IsDelayed((used, useDelay))) - return; - - // If it's a puddle try to grab from - if (!TryPuddleInteract(user, used, target, component, useDelay, absorberSoln.Value) && component.UseAbsorberSolution) - { - // If it's refillable try to transfer - if (!TryRefillableInteract(user, used, target, component, useDelay, absorberSoln.Value)) - return; - } - } - - /// - /// Logic for an absorbing entity interacting with a refillable. - /// - private bool TryRefillableInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, UseDelayComponent? useDelay, Entity absorbentSoln) - { - if (!TryComp(target, out RefillableSolutionComponent? refillable)) - return false; - - if (!_solutionContainerSystem.TryGetRefillableSolution((target, refillable, null), out var refillableSoln, out var refillableSolution)) - return false; - - if (refillableSolution.Volume <= 0) - { - // Target empty - only transfer absorbent contents into refillable - if (!TryTransferFromAbsorbentToRefillable(user, used, target, component, absorbentSoln, refillableSoln.Value)) - return false; - } - else - { - // Target non-empty - do a two-way transfer - if (!TryTwoWayAbsorbentRefillableTransfer(user, used, target, component, absorbentSoln, refillableSoln.Value)) - return false; - } - - _audio.PlayPvs(component.TransferSound, target); - if (useDelay != null) - _useDelay.TryResetDelay((used, useDelay)); - return true; - } - - /// - /// Logic for an transferring solution from absorber to an empty refillable. - /// - private bool TryTransferFromAbsorbentToRefillable( - EntityUid user, - EntityUid used, - EntityUid target, - AbsorbentComponent component, - Entity absorbentSoln, - Entity refillableSoln) - { - var absorbentSolution = absorbentSoln.Comp.Solution; - if (absorbentSolution.Volume <= 0) - { - _popups.PopupEntity(Loc.GetString("mopping-system-target-container-empty", ("target", target)), user, user); - return false; - } - - var refillableSolution = refillableSoln.Comp.Solution; - var transferAmount = component.PickupAmount < refillableSolution.AvailableVolume ? - component.PickupAmount : - refillableSolution.AvailableVolume; - - if (transferAmount <= 0) - { - _popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", used)), used, user); - return false; - } - - // Prioritize transferring non-evaporatives if absorbent has any - var contaminants = _solutionContainerSystem.SplitSolutionWithout(absorbentSoln, transferAmount, _puddleSystem.GetAbsorbentReagents(absorbentSoln.Comp.Solution)); - if (contaminants.Volume > 0) - { - _solutionContainerSystem.TryAddSolution(refillableSoln, contaminants); - } - else - { - var evaporatives = _solutionContainerSystem.SplitSolution(absorbentSoln, transferAmount); - _solutionContainerSystem.TryAddSolution(refillableSoln, evaporatives); - } - - return true; - } - - /// - /// Logic for an transferring contaminants to a non-empty refillable & reabsorbing water if any available. - /// - private bool TryTwoWayAbsorbentRefillableTransfer( - EntityUid user, - EntityUid used, - EntityUid target, - AbsorbentComponent component, - Entity absorbentSoln, - Entity refillableSoln) - { - var contaminantsFromAbsorbent = _solutionContainerSystem.SplitSolutionWithout(absorbentSoln, component.PickupAmount, _puddleSystem.GetAbsorbentReagents(absorbentSoln.Comp.Solution)); - - var absorbentSolution = absorbentSoln.Comp.Solution; - if (contaminantsFromAbsorbent.Volume == FixedPoint2.Zero && absorbentSolution.AvailableVolume == FixedPoint2.Zero) - { - // Nothing to transfer to refillable and no room to absorb anything extra - _popups.PopupEntity(Loc.GetString("mopping-system-puddle-space", ("used", used)), user, user); - - // We can return cleanly because nothing was split from absorbent solution - return false; - } - - var waterPulled = component.PickupAmount < absorbentSolution.AvailableVolume ? - component.PickupAmount : - absorbentSolution.AvailableVolume; - - var refillableSolution = refillableSoln.Comp.Solution; - var waterFromRefillable = refillableSolution.SplitSolutionWithOnly(waterPulled, _puddleSystem.GetAbsorbentReagents(refillableSoln.Comp.Solution)); - _solutionContainerSystem.UpdateChemicals(refillableSoln); - - if (waterFromRefillable.Volume == FixedPoint2.Zero && contaminantsFromAbsorbent.Volume == FixedPoint2.Zero) - { - // Nothing to transfer in either direction - _popups.PopupEntity(Loc.GetString("mopping-system-target-container-empty-water", ("target", target)), user, user); - - // We can return cleanly because nothing was split from refillable solution - return false; - } - - var anyTransferOccurred = false; - - if (waterFromRefillable.Volume > FixedPoint2.Zero) - { - // transfer water to absorbent - _solutionContainerSystem.TryAddSolution(absorbentSoln, waterFromRefillable); - anyTransferOccurred = true; - } - - if (contaminantsFromAbsorbent.Volume > 0) - { - if (refillableSolution.AvailableVolume <= 0) - { - _popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", target)), user, user); - } - else - { - // transfer as much contaminants to refillable as will fit - var contaminantsForRefillable = contaminantsFromAbsorbent.SplitSolution(refillableSolution.AvailableVolume); - _solutionContainerSystem.TryAddSolution(refillableSoln, contaminantsForRefillable); - anyTransferOccurred = true; - } - - // absorb everything that did not fit in the refillable back by the absorbent - _solutionContainerSystem.TryAddSolution(absorbentSoln, contaminantsFromAbsorbent); - } - - return anyTransferOccurred; - } - - /// - /// Logic for an absorbing entity interacting with a puddle. - /// - private bool TryPuddleInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent absorber, UseDelayComponent? useDelay, Entity absorberSoln) - { - if (!TryComp(target, out PuddleComponent? puddle)) - return false; - - if (!_solutionContainerSystem.ResolveSolution(target, puddle.SolutionName, ref puddle.Solution, out var puddleSolution) || puddleSolution.Volume <= 0) - return false; - - Solution puddleSplit; - var isRemoved = false; - if (absorber.UseAbsorberSolution) - { - // No reason to mop something that 1) can evaporate, 2) is an absorber, and 3) is being mopped with - // something that uses absorbers. - var puddleAbsorberVolume = - puddleSolution.GetTotalPrototypeQuantity(_puddleSystem.GetAbsorbentReagents(puddleSolution)); - if (puddleAbsorberVolume == puddleSolution.Volume) - { - _popups.PopupEntity(Loc.GetString("mopping-system-puddle-already-mopped", ("target", target)), - user, - user); - return true; - } - - // Check if we have any evaporative reagents on our absorber to transfer - var absorberSolution = absorberSoln.Comp.Solution; - var available = absorberSolution.GetTotalPrototypeQuantity(_puddleSystem.GetAbsorbentReagents(absorberSolution)); - - // No material - if (available == FixedPoint2.Zero) - { - _popups.PopupEntity(Loc.GetString("mopping-system-no-water", ("used", used)), user, user); - return true; - } - - var transferMax = absorber.PickupAmount; - var transferAmount = available > transferMax ? transferMax : available; - - puddleSplit = puddleSolution.SplitSolutionWithout(transferAmount, _puddleSystem.GetAbsorbentReagents(puddleSolution)); - var absorberSplit = absorberSolution.SplitSolutionWithOnly(puddleSplit.Volume, _puddleSystem.GetAbsorbentReagents(absorberSolution)); - - // Do tile reactions first - var transform = Transform(target); - var gridUid = transform.GridUid; - if (TryComp(gridUid, out MapGridComponent? mapGrid)) - { - var tileRef = _mapSystem.GetTileRef(gridUid.Value, mapGrid, transform.Coordinates); - _puddleSystem.DoTileReactions(tileRef, absorberSplit); - } - _solutionContainerSystem.AddSolution(puddle.Solution.Value, absorberSplit); - } - else - { - // Note: arguably shouldn't this get all solutions? - puddleSplit = puddleSolution.SplitSolutionWithout(absorber.PickupAmount, _puddleSystem.GetAbsorbentReagents(puddleSolution)); - // Despawn if we're done - if (puddleSolution.Volume == FixedPoint2.Zero) - { - // Spawn a *sparkle* - Spawn(Sparkles, GetEntityQuery().GetComponent(target).Coordinates); - QueueDel(target); - isRemoved = true; - } - } - - _solutionContainerSystem.AddSolution(absorberSoln, puddleSplit); - - _audio.PlayPvs(absorber.PickupSound, isRemoved ? used : target); - if (useDelay != null) - _useDelay.TryResetDelay((used, useDelay)); - - var userXform = Transform(user); - var targetPos = _transform.GetWorldPosition(target); - var localPos = Vector2.Transform(targetPos, _transform.GetInvWorldMatrix(userXform)); - localPos = userXform.LocalRotation.RotateVec(localPos); - - _melee.DoLunge(user, used, Angle.Zero, localPos, null, false); - - return true; - } -} +public sealed class AbsorbentSystem : SharedAbsorbentSystem; diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs index 44bbfe2450..1176ce54c7 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs @@ -50,7 +50,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly ReactiveSystem _reactive = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly SharedPopupSystem _popups = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; @@ -60,12 +59,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly TurfSystem _turf = default!; - private static readonly ProtoId Blood = "Blood"; - private static readonly ProtoId Slime = "Slime"; - private static readonly ProtoId CopperBlood = "CopperBlood"; - - private static readonly string[] StandoutReagents = [Blood, Slime, CopperBlood]; - // Using local deletion queue instead of the standard queue so that we can easily "undelete" if a puddle // loses & then gains reagents in a single tick. private HashSet _deletionQueue = []; @@ -86,7 +79,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem // Shouldn't need re-anchoring. SubscribeLocalEvent(OnAnchorChanged); - SubscribeLocalEvent(OnSolutionUpdate); SubscribeLocalEvent(OnPuddleSpread); SubscribeLocalEvent(OnPuddleSlip); @@ -319,11 +311,13 @@ public sealed partial class PuddleSystem : SharedPuddleSystem TickEvaporation(); } - private void OnSolutionUpdate(Entity entity, ref SolutionContainerChangedEvent args) + protected override void OnSolutionUpdate(Entity entity, ref SolutionContainerChangedEvent args) { if (args.SolutionId != entity.Comp.SolutionName) return; + base.OnSolutionUpdate(entity, ref args); + if (args.Solution.Volume <= 0) { _deletionQueue.Add(entity); @@ -334,46 +328,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem UpdateSlip((entity, entity.Comp), args.Solution); UpdateSlow(entity, args.Solution); UpdateEvaporation(entity, args.Solution); - UpdateAppearance(entity, entity.Comp); - } - - private void UpdateAppearance(EntityUid uid, PuddleComponent? puddleComponent = null, - AppearanceComponent? appearance = null) - { - if (!Resolve(uid, ref puddleComponent, ref appearance, false)) - { - return; - } - - var volume = FixedPoint2.Zero; - Color color = Color.White; - - if (_solutionContainerSystem.ResolveSolution(uid, puddleComponent.SolutionName, ref puddleComponent.Solution, - out var solution)) - { - volume = solution.Volume / puddleComponent.OverflowVolume; - - // Make blood stand out more - // Kinda EH - // Could potentially do alpha per-solution but future problem. - - color = solution.GetColorWithout(_prototypeManager, StandoutReagents); - color = color.WithAlpha(0.7f); - - foreach (var standout in StandoutReagents) - { - var quantity = solution.GetTotalPrototypeQuantity(standout); - if (quantity <= FixedPoint2.Zero) - continue; - - var interpolateValue = quantity.Float() / solution.Volume.Float(); - color = Color.InterpolateBetween(color, - _prototypeManager.Index(standout).SubstanceColor, interpolateValue); - } - } - - _appearance.SetData(uid, PuddleVisuals.CurrentVolume, volume.Float(), appearance); - _appearance.SetData(uid, PuddleVisuals.SolutionColor, color, appearance); } private void UpdateSlip(Entity entity, Solution solution) @@ -450,15 +404,15 @@ public sealed partial class PuddleSystem : SharedPuddleSystem // A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount. if (slipperyUnits > 0) { - slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits); - slipComp.SlipData.ParalyzeTime = (stunTimer/(float)slipperyUnits); + slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits); + slipComp.SlipData.ParalyzeTime = stunTimer / (float)slipperyUnits; } // Only make it super slippery if there is enough super slippery units for its own puddle slipComp.SlipData.SuperSlippery = superSlipperyUnits >= smallPuddleThreshold; // Lower tile friction based on how slippery it is, lets items slide across a puddle of lube - slipComp.SlipData.SlipFriction = (float)(puddleFriction/solution.Volume); + slipComp.SlipData.SlipFriction = (float)(puddleFriction / solution.Volume); _tile.SetModifier(entity, slipComp.SlipData.SlipFriction); Dirty(entity, slipComp); @@ -748,20 +702,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem #endregion - public void DoTileReactions(TileRef tileRef, Solution solution) - { - for (var i = solution.Contents.Count - 1; i >= 0; i--) - { - var (reagent, quantity) = solution.Contents[i]; - var proto = _prototypeManager.Index(reagent.Prototype); - var removed = proto.ReactionTile(tileRef, quantity, EntityManager, reagent.Data); - if (removed <= FixedPoint2.Zero) - continue; - - solution.RemoveReagent(reagent, removed); - } - } - /// /// Tries to get the relevant puddle entity for a tile. /// diff --git a/Content.Server/Chemistry/Components/SolutionPurgeComponent.cs b/Content.Shared/Chemistry/Components/SolutionPurgeComponent.cs similarity index 52% rename from Content.Server/Chemistry/Components/SolutionPurgeComponent.cs rename to Content.Shared/Chemistry/Components/SolutionPurgeComponent.cs index 9b9294b6f9..acaebb7ef8 100644 --- a/Content.Server/Chemistry/Components/SolutionPurgeComponent.cs +++ b/Content.Shared/Chemistry/Components/SolutionPurgeComponent.cs @@ -1,47 +1,48 @@ -using Content.Server.Chemistry.EntitySystems; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reagent; using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; -namespace Content.Server.Chemistry.Components; +namespace Content.Shared.Chemistry.Components; /// /// Passively decreases a solution's quantity of reagent(s). /// [RegisterComponent, AutoGenerateComponentPause] +[NetworkedComponent, AutoGenerateComponentState] [Access(typeof(SolutionPurgeSystem))] public sealed partial class SolutionPurgeComponent : Component { /// /// The name of the solution to detract from. /// - [DataField("solution", required: true), ViewVariables(VVAccess.ReadWrite)] + [DataField(required: true)] public string Solution = string.Empty; /// /// The reagent(s) to be ignored when purging the solution /// - [DataField("preserve", customTypeSerializer: typeof(PrototypeIdListSerializer))] - [ViewVariables(VVAccess.ReadWrite)] - public List Preserve = new(); + [DataField] + public List> Preserve = []; /// /// Amount of reagent(s) that are purged /// - [DataField("quantity", required: true), ViewVariables(VVAccess.ReadWrite)] - public FixedPoint2 Quantity = default!; + [DataField(required: true)] + public FixedPoint2 Quantity; /// /// How long it takes to purge once. /// - [DataField("duration"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public TimeSpan Duration = TimeSpan.FromSeconds(1); /// /// The time when the next purge will occur. /// - [DataField("nextPurgeTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] - [AutoPausedField] - public TimeSpan NextPurgeTime = TimeSpan.FromSeconds(0); + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField, AutoNetworkedField] + public TimeSpan NextPurgeTime; } diff --git a/Content.Server/Chemistry/Components/SolutionRegenerationComponent.cs b/Content.Shared/Chemistry/Components/SolutionRegenerationComponent.cs similarity index 78% rename from Content.Server/Chemistry/Components/SolutionRegenerationComponent.cs rename to Content.Shared/Chemistry/Components/SolutionRegenerationComponent.cs index 9266c460f1..be41ace321 100644 --- a/Content.Server/Chemistry/Components/SolutionRegenerationComponent.cs +++ b/Content.Shared/Chemistry/Components/SolutionRegenerationComponent.cs @@ -1,13 +1,13 @@ -using Content.Server.Chemistry.EntitySystems; -using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Robust.Shared.GameStates; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.Chemistry.Components; +namespace Content.Shared.Chemistry.Components; /// /// Passively increases a solution's quantity of a reagent. /// -[RegisterComponent, AutoGenerateComponentPause] +[RegisterComponent, AutoGenerateComponentPause, AutoGenerateComponentState, NetworkedComponent] [Access(typeof(SolutionRegenerationSystem))] public sealed partial class SolutionRegenerationComponent : Component { @@ -39,6 +39,6 @@ public sealed partial class SolutionRegenerationComponent : Component /// The time when the next regeneration will occur. /// [DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer))] - [AutoPausedField] - public TimeSpan NextRegenTime = TimeSpan.FromSeconds(0); + [AutoPausedField, AutoNetworkedField] + public TimeSpan NextRegenTime; } diff --git a/Content.Server/Chemistry/EntitySystems/SolutionPurgeSystem.cs b/Content.Shared/Chemistry/EntitySystems/SolutionPurgeSystem.cs similarity index 56% rename from Content.Server/Chemistry/EntitySystems/SolutionPurgeSystem.cs rename to Content.Shared/Chemistry/EntitySystems/SolutionPurgeSystem.cs index 5a25e76456..bf39aaecc4 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionPurgeSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SolutionPurgeSystem.cs @@ -1,15 +1,28 @@ -using Content.Server.Chemistry.Components; -using Content.Shared.Chemistry.EntitySystems; +using System.Linq; +using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Robust.Shared.Timing; -namespace Content.Server.Chemistry.EntitySystems; +namespace Content.Shared.Chemistry.EntitySystems; public sealed class SolutionPurgeSystem : EntitySystem { [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; [Dependency] private readonly IGameTiming _timing = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.NextPurgeTime = _timing.CurTime + ent.Comp.Duration; + Dirty(ent); + } + public override void Update(float frameTime) { base.Update(frameTime); @@ -22,8 +35,15 @@ public sealed class SolutionPurgeSystem : EntitySystem // timer ignores if it's empty, it's just a fixed cycle purge.NextPurgeTime += purge.Duration; + // Needs to be networked and dirtied so that the client can reroll it during prediction + Dirty(uid, purge); + if (_solutionContainer.TryGetSolution((uid, manager), purge.Solution, out var solution)) - _solutionContainer.SplitSolutionWithout(solution.Value, purge.Quantity, purge.Preserve.ToArray()); + { + _solutionContainer.SplitSolutionWithout(solution.Value, + purge.Quantity, + purge.Preserve.Select(proto => proto.Id).ToArray()); + } } } } diff --git a/Content.Shared/Chemistry/EntitySystems/SolutionRegenerationSystem.cs b/Content.Shared/Chemistry/EntitySystems/SolutionRegenerationSystem.cs new file mode 100644 index 0000000000..369b837202 --- /dev/null +++ b/Content.Shared/Chemistry/EntitySystems/SolutionRegenerationSystem.cs @@ -0,0 +1,69 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.FixedPoint; +using Robust.Shared.Containers; +using Robust.Shared.Timing; + +namespace Content.Shared.Chemistry.EntitySystems; + +public sealed class SolutionRegenerationSystem : EntitySystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnEntRemoved); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.NextRegenTime = _timing.CurTime + ent.Comp.Duration; + + Dirty(ent); + } + + // Workaround for https://github.com/space-wizards/space-station-14/pull/35314 + private void OnEntRemoved(Entity ent, ref EntRemovedFromContainerMessage args) + { + // Make sure the removed entity was our contained solution and clear our cached reference + if (args.Entity == ent.Comp.SolutionRef?.Owner) + ent.Comp.SolutionRef = null; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var regen, out var manager)) + { + if (_timing.CurTime < regen.NextRegenTime) + continue; + + // timer ignores if its full, it's just a fixed cycle + regen.NextRegenTime += regen.Duration; + // Needs to be networked and dirtied so that the client can reroll it during prediction + Dirty(uid, regen); + if (!_solutionContainer.ResolveSolution((uid, manager), + regen.SolutionName, + ref regen.SolutionRef, + out var solution)) + continue; + + var amount = FixedPoint2.Min(solution.AvailableVolume, regen.Generated.Volume); + if (amount <= FixedPoint2.Zero) + continue; + + // Don't bother cloning and splitting if adding the whole thing + var generated = amount == regen.Generated.Volume + ? regen.Generated + : regen.Generated.Clone().SplitSolution(amount); + + _solutionContainer.TryAddSolution(regen.SolutionRef.Value, generated); + } + } +} diff --git a/Content.Shared/Fluids/AbsorbentComponent.cs b/Content.Shared/Fluids/AbsorbentComponent.cs index 2d1a922381..c499a2d2ee 100644 --- a/Content.Shared/Fluids/AbsorbentComponent.cs +++ b/Content.Shared/Fluids/AbsorbentComponent.cs @@ -2,16 +2,22 @@ using Content.Shared.Audio; using Content.Shared.FixedPoint; using Robust.Shared.Audio; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; namespace Content.Shared.Fluids; /// /// For entities that can clean up puddles /// -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class AbsorbentComponent : Component { - public Dictionary Progress = new(); + /// + /// Used by the client to display a bar showing the reagents contained when held. + /// Has to still be networked in case the item is given to someone who didn't see a mop in PVS. + /// + [DataField, AutoNetworkedField] + public Dictionary Progress = []; /// /// Name for solution container, that should be used for absorbed solution storage and as source of absorber solution. @@ -26,23 +32,23 @@ public sealed partial class AbsorbentComponent : Component [DataField] public FixedPoint2 PickupAmount = FixedPoint2.New(100); + /// + /// The effect spawned when the puddle fully evaporates. + /// [DataField] - public SoundSpecifier PickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg") - { - Params = AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation), - }; + public EntProtoId MoppedEffect = "PuddleSparkle"; - [DataField] public SoundSpecifier TransferSound = - new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg") - { - Params = AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-3f), - }; + [DataField] + public SoundSpecifier PickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg", + AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation)); + + [DataField] + public SoundSpecifier TransferSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg", + AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-3f)); public static readonly SoundSpecifier DefaultTransferSound = - new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg") - { - Params = AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-3f), - }; + new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg", + AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-3f)); /// /// Marker that absorbent component owner should try to use 'absorber solution' to replace solution to be absorbed. diff --git a/Content.Shared/Fluids/Components/EvaporationComponent.cs b/Content.Shared/Fluids/Components/EvaporationComponent.cs index 9b46629439..88cea52945 100644 --- a/Content.Shared/Fluids/Components/EvaporationComponent.cs +++ b/Content.Shared/Fluids/Components/EvaporationComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.FixedPoint; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Fluids.Components; @@ -7,19 +8,25 @@ namespace Content.Shared.Fluids.Components; /// /// Added to puddles that contain water so it may evaporate over time. /// -[NetworkedComponent] +[NetworkedComponent, AutoGenerateComponentPause] [RegisterComponent, Access(typeof(SharedPuddleSystem))] public sealed partial class EvaporationComponent : Component { /// /// The next time we remove the EvaporationSystem reagent amount from this entity. /// - [ViewVariables(VVAccess.ReadWrite), DataField("nextTick", customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan NextTick = TimeSpan.Zero; + [AutoPausedField, DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextTick; /// /// Evaporation factor. Multiplied by the evaporating speed of the reagent. /// - [DataField("evaporationAmount")] + [DataField] public FixedPoint2 EvaporationAmount = FixedPoint2.New(1); + + /// + /// The effect spawned when the puddle fully evaporates. + /// + [DataField] + public EntProtoId EvaporationEffect = "PuddleSparkle"; } diff --git a/Content.Shared/Fluids/SharedAbsorbentSystem.cs b/Content.Shared/Fluids/SharedAbsorbentSystem.cs index f121238d51..77f40aeee6 100644 --- a/Content.Shared/Fluids/SharedAbsorbentSystem.cs +++ b/Content.Shared/Fluids/SharedAbsorbentSystem.cs @@ -1,6 +1,16 @@ -using System.Linq; -using Robust.Shared.GameStates; -using Robust.Shared.Serialization; +using System.Numerics; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.FixedPoint; +using Content.Shared.Fluids.Components; +using Content.Shared.Interaction; +using Content.Shared.Item; +using Content.Shared.Popups; +using Content.Shared.Timing; +using Content.Shared.Weapons.Melee; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; namespace Content.Shared.Fluids; @@ -9,41 +19,338 @@ namespace Content.Shared.Fluids; /// public abstract class SharedAbsorbentSystem : EntitySystem { + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popups = default!; + [Dependency] protected readonly SharedPuddleSystem Puddle = default!; + [Dependency] private readonly SharedMeleeWeaponSystem _melee = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] protected readonly SharedSolutionContainerSystem SolutionContainer = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; + [Dependency] private readonly SharedMapSystem _mapSystem = default!; + [Dependency] private readonly SharedItemSystem _item = default!; + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnAbsorbentGetState); - SubscribeLocalEvent(OnAbsorbentHandleState); + + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnActivateInWorld); + SubscribeLocalEvent(OnAbsorbentSolutionChange); } - private void OnAbsorbentHandleState(EntityUid uid, AbsorbentComponent component, ref ComponentHandleState args) + private void OnActivateInWorld(Entity ent, ref UserActivateInWorldEvent args) { - if (args.Current is not AbsorbentComponentState state) + if (args.Handled) return; - if (component.Progress.OrderBy(x => x.Key.ToArgb()).SequenceEqual(state.Progress)) + Mop(ent, args.User, args.Target); + args.Handled = true; + } + + private void OnAfterInteract(Entity ent, ref AfterInteractEvent args) + { + if (!args.CanReach || args.Handled || args.Target is not { } target) return; - component.Progress.Clear(); - foreach (var item in state.Progress) - { - component.Progress.Add(item.Key, item.Value); - } + Mop(ent, args.User, target); + args.Handled = true; } - private void OnAbsorbentGetState(EntityUid uid, AbsorbentComponent component, ref ComponentGetState args) + private void OnAbsorbentSolutionChange(Entity ent, ref SolutionContainerChangedEvent args) { - args.State = new AbsorbentComponentState(component.Progress); + if (!SolutionContainer.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out _, out var solution)) + return; + + ent.Comp.Progress.Clear(); + + var absorbentReagents = Puddle.GetAbsorbentReagents(solution); + var mopReagent = solution.GetTotalPrototypeQuantity(absorbentReagents); + if (mopReagent > FixedPoint2.Zero) + ent.Comp.Progress[solution.GetColorWithOnly(_proto, absorbentReagents)] = mopReagent.Float(); + + var otherColor = solution.GetColorWithout(_proto, absorbentReagents); + var other = solution.Volume - mopReagent; + if (other > FixedPoint2.Zero) + ent.Comp.Progress[otherColor] = other.Float(); + + if (solution.AvailableVolume > FixedPoint2.Zero) + ent.Comp.Progress[Color.DarkGray] = solution.AvailableVolume.Float(); + + Dirty(ent); + _item.VisualsChanged(ent); } - [Serializable, NetSerializable] - protected sealed class AbsorbentComponentState : ComponentState + [Obsolete("Use Entity variant")] + public void Mop(EntityUid user, EntityUid target, EntityUid used, AbsorbentComponent component) { - public Dictionary Progress; + Mop((used, component), user, target); + } - public AbsorbentComponentState(Dictionary progress) + public void Mop(Entity absorbEnt, EntityUid user, EntityUid target) + { + if (!SolutionContainer.TryGetSolution(absorbEnt.Owner, absorbEnt.Comp.SolutionName, out var absorberSoln)) + return; + + // Use the non-optional form of IsDelayed to safe the TryComp in Mop + if (TryComp(absorbEnt, out var useDelay) + && _useDelay.IsDelayed((absorbEnt.Owner, useDelay))) + return; + + // Try to slurp up the puddle. + // We're then done if our mop doesn't use absorber solutions, since those don't need refilling. + if (TryPuddleInteract((absorbEnt.Owner, absorbEnt.Comp, useDelay), absorberSoln.Value, user, target) + || !absorbEnt.Comp.UseAbsorberSolution) + return; + + // If it's refillable try to transfer + TryRefillableInteract((absorbEnt.Owner, absorbEnt.Comp, useDelay), absorberSoln.Value, user, target); + } + + /// + /// Logic for an absorbing entity interacting with a refillable. + /// + private bool TryRefillableInteract(Entity absorbEnt, + Entity absorbentSoln, + EntityUid user, + EntityUid target) + { + if (!TryComp(target, out var refillable)) + return false; + + if (!SolutionContainer.TryGetRefillableSolution((target, refillable, null), + out var refillableSoln, + out var refillableSolution)) + return false; + + if (refillableSolution.Volume <= 0) { - Progress = progress; + // Target empty - only transfer absorbent contents into refillable + if (!TryTransferFromAbsorbentToRefillable(absorbEnt, absorbentSoln, refillableSoln.Value, user, target)) + return false; } + else + { + // Target non-empty - do a two-way transfer + if (!TryTwoWayAbsorbentRefillableTransfer(absorbEnt, absorbentSoln, refillableSoln.Value, user, target)) + return false; + } + + var (used, absorber, useDelay) = absorbEnt; + _audio.PlayPredicted(absorber.TransferSound, target, user); + + if (useDelay != null) + _useDelay.TryResetDelay((used, useDelay)); + + return true; + } + + /// + /// Logic for an transferring solution from absorber to an empty refillable. + /// + private bool TryTransferFromAbsorbentToRefillable(Entity absorbEnt, + Entity absorbentSoln, + Entity refillableSoln, + EntityUid user, + EntityUid target) + { + var absorbentSolution = absorbentSoln.Comp.Solution; + if (absorbentSolution.Volume <= 0) + { + _popups.PopupClient(Loc.GetString("mopping-system-target-container-empty", ("target", target)), user, user); + return false; + } + + var refillableSolution = refillableSoln.Comp.Solution; + var transferAmount = absorbEnt.Comp.PickupAmount < refillableSolution.AvailableVolume + ? absorbEnt.Comp.PickupAmount + : refillableSolution.AvailableVolume; + + if (transferAmount <= 0) + { + _popups.PopupClient(Loc.GetString("mopping-system-full", ("used", absorbEnt)), absorbEnt, user); + return false; + } + + // Prioritize transferring non-evaporatives if absorbent has any + var contaminants = SolutionContainer.SplitSolutionWithout(absorbentSoln, + transferAmount, + Puddle.GetAbsorbentReagents(absorbentSoln.Comp.Solution)); + + SolutionContainer.TryAddSolution(refillableSoln, + contaminants.Volume > 0 + ? contaminants + : SolutionContainer.SplitSolution(absorbentSoln, transferAmount)); + + return true; + } + + /// + /// Logic for an transferring contaminants to a non-empty refillable & reabsorbing water if any available. + /// + private bool TryTwoWayAbsorbentRefillableTransfer(Entity absorbEnt, + Entity absorbentSoln, + Entity refillableSoln, + EntityUid user, + EntityUid target) + { + var contaminantsFromAbsorbent = SolutionContainer.SplitSolutionWithout(absorbentSoln, + absorbEnt.Comp.PickupAmount, + Puddle.GetAbsorbentReagents(absorbentSoln.Comp.Solution)); + + var absorbentSolution = absorbentSoln.Comp.Solution; + if (contaminantsFromAbsorbent.Volume == FixedPoint2.Zero + && absorbentSolution.AvailableVolume == FixedPoint2.Zero) + { + // Nothing to transfer to refillable and no room to absorb anything extra + _popups.PopupClient(Loc.GetString("mopping-system-puddle-space", ("used", absorbEnt)), user, user); + + // We can return cleanly because nothing was split from absorbent solution + return false; + } + + var waterPulled = absorbEnt.Comp.PickupAmount < absorbentSolution.AvailableVolume + ? absorbEnt.Comp.PickupAmount + : absorbentSolution.AvailableVolume; + + var refillableSolution = refillableSoln.Comp.Solution; + var waterFromRefillable = + refillableSolution.SplitSolutionWithOnly(waterPulled, + Puddle.GetAbsorbentReagents(refillableSoln.Comp.Solution)); + SolutionContainer.UpdateChemicals(refillableSoln); + + if (waterFromRefillable.Volume == FixedPoint2.Zero && contaminantsFromAbsorbent.Volume == FixedPoint2.Zero) + { + // Nothing to transfer in either direction + _popups.PopupClient(Loc.GetString("mopping-system-target-container-empty-water", ("target", target)), + user, + user); + + // We can return cleanly because nothing was split from refillable solution + return false; + } + + var anyTransferOccurred = false; + + if (waterFromRefillable.Volume > FixedPoint2.Zero) + { + // transfer water to absorbent + SolutionContainer.TryAddSolution(absorbentSoln, waterFromRefillable); + anyTransferOccurred = true; + } + + if (contaminantsFromAbsorbent.Volume <= 0) + return anyTransferOccurred; + + if (refillableSolution.AvailableVolume <= 0) + { + _popups.PopupClient(Loc.GetString("mopping-system-full", ("used", target)), user, user); + } + else + { + // transfer as much contaminants to refillable as will fit + var contaminantsForRefillable = contaminantsFromAbsorbent.SplitSolution(refillableSolution.AvailableVolume); + SolutionContainer.TryAddSolution(refillableSoln, contaminantsForRefillable); + anyTransferOccurred = true; + } + + // absorb everything that did not fit in the refillable back by the absorbent + SolutionContainer.TryAddSolution(absorbentSoln, contaminantsFromAbsorbent); + + return anyTransferOccurred; + } + + /// + /// Logic for an absorbing entity interacting with a puddle. + /// + private bool TryPuddleInteract(Entity absorbEnt, + Entity absorberSoln, + EntityUid user, + EntityUid target) + { + if (!TryComp(target, out var puddle)) + return false; + + if (!SolutionContainer.ResolveSolution(target, puddle.SolutionName, ref puddle.Solution, out var puddleSolution) + || puddleSolution.Volume <= 0) + return false; + + var (_, absorber, useDelay) = absorbEnt; + + Solution puddleSplit; + var isRemoved = false; + if (absorber.UseAbsorberSolution) + { + // No reason to mop something that 1) can evaporate, 2) is an absorber, and 3) is being mopped with + // something that uses absorbers. + var puddleAbsorberVolume = + puddleSolution.GetTotalPrototypeQuantity(Puddle.GetAbsorbentReagents(puddleSolution)); + if (puddleAbsorberVolume == puddleSolution.Volume) + { + _popups.PopupClient(Loc.GetString("mopping-system-puddle-already-mopped", ("target", target)), + target, + user); + return true; + } + + // Check if we have any evaporative reagents on our absorber to transfer + var absorberSolution = absorberSoln.Comp.Solution; + var available = absorberSolution.GetTotalPrototypeQuantity(Puddle.GetAbsorbentReagents(absorberSolution)); + + // No material + if (available == FixedPoint2.Zero) + { + _popups.PopupClient(Loc.GetString("mopping-system-no-water", ("used", absorbEnt)), absorbEnt, user); + return true; + } + + var transferMax = absorber.PickupAmount; + var transferAmount = available > transferMax ? transferMax : available; + + puddleSplit = + puddleSolution.SplitSolutionWithout(transferAmount, Puddle.GetAbsorbentReagents(puddleSolution)); + var absorberSplit = + absorberSolution.SplitSolutionWithOnly(puddleSplit.Volume, + Puddle.GetAbsorbentReagents(absorberSolution)); + + // Do tile reactions first + var targetXform = Transform(target); + var gridUid = targetXform.GridUid; + if (TryComp(gridUid, out var mapGrid)) + { + var tileRef = _mapSystem.GetTileRef(gridUid.Value, mapGrid, targetXform.Coordinates); + Puddle.DoTileReactions(tileRef, absorberSplit); + } + SolutionContainer.AddSolution(puddle.Solution.Value, absorberSplit); + } + else + { + // Note: arguably shouldn't this get all solutions? + puddleSplit = puddleSolution.SplitSolutionWithout(absorber.PickupAmount, Puddle.GetAbsorbentReagents(puddleSolution)); + // Despawn if we're done + if (puddleSolution.Volume == FixedPoint2.Zero) + { + // Spawn a *sparkle* + PredictedSpawnAttachedTo(absorber.MoppedEffect, Transform(target).Coordinates); + PredictedQueueDel(target); + isRemoved = true; + } + } + + SolutionContainer.AddSolution(absorberSoln, puddleSplit); + + _audio.PlayPredicted(absorber.PickupSound, isRemoved ? absorbEnt : target, user); + + if (useDelay != null) + _useDelay.TryResetDelay((absorbEnt, useDelay)); + + var userXform = Transform(user); + var targetPos = _transform.GetWorldPosition(target); + var localPos = Vector2.Transform(targetPos, _transform.GetInvWorldMatrix(userXform)); + localPos = userXform.LocalRotation.RotateVec(localPos); + + _melee.DoLunge(user, absorbEnt, Angle.Zero, localPos, null); + + return true; } } diff --git a/Content.Shared/Fluids/SharedPuddleSystem.cs b/Content.Shared/Fluids/SharedPuddleSystem.cs index 34ba8ba803..a2f0764ce0 100644 --- a/Content.Shared/Fluids/SharedPuddleSystem.cs +++ b/Content.Shared/Fluids/SharedPuddleSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reagent; @@ -8,6 +9,7 @@ using Content.Shared.FixedPoint; using Content.Shared.Fluids.Components; using Content.Shared.Movement.Events; using Content.Shared.StepTrigger.Components; +using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -16,9 +18,16 @@ namespace Content.Shared.Fluids; public abstract partial class SharedPuddleSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + private static readonly ProtoId Blood = "Blood"; + private static readonly ProtoId Slime = "Slime"; + private static readonly ProtoId CopperBlood = "CopperBlood"; + + private static readonly string[] StandoutReagents = [Blood, Slime, CopperBlood]; + /// /// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle. /// @@ -33,12 +42,23 @@ public abstract partial class SharedPuddleSystem : EntitySystem SubscribeLocalEvent(OnDumpCanDropTarget); SubscribeLocalEvent(OnDrainCanDropTarget); SubscribeLocalEvent(OnRefillableCanDropDragged); + + SubscribeLocalEvent(OnSolutionUpdate); SubscribeLocalEvent(OnGetFootstepSound); SubscribeLocalEvent(HandlePuddleExamined); + SubscribeLocalEvent(OnEntRemoved); InitializeSpillable(); } + protected virtual void OnSolutionUpdate(Entity entity, ref SolutionContainerChangedEvent args) + { + if (args.SolutionId != entity.Comp.SolutionName) + return; + + UpdateAppearance((entity, entity.Comp)); + } + private void OnRefillableCanDrag(Entity entity, ref CanDragEvent args) { args.Handled = true; @@ -110,6 +130,68 @@ public abstract partial class SharedPuddleSystem : EntitySystem } } + // Workaround for https://github.com/space-wizards/space-station-14/pull/35314 + private void OnEntRemoved(Entity ent, ref EntRemovedFromContainerMessage args) + { + // Make sure the removed entity was our contained solution and clear our cached reference + if (args.Entity == ent.Comp.Solution?.Owner) + ent.Comp.Solution = null; + } + + private void UpdateAppearance(Entity ent) + { + var (uid, puddle, appearance) = ent; + if (!Resolve(ent, ref puddle, ref appearance)) + return; + + var volume = FixedPoint2.Zero; + var color = Color.White; + + if (_solutionContainerSystem.ResolveSolution(uid, + puddle.SolutionName, + ref puddle.Solution, + out var solution)) + { + volume = solution.Volume / puddle.OverflowVolume; + + // Make blood stand out more + // Kinda EH + // Could potentially do alpha per-solution but future problem. + + color = solution.GetColorWithout(_prototypeManager, StandoutReagents); + color = color.WithAlpha(0.7f); + + foreach (var standout in StandoutReagents) + { + var quantity = solution.GetTotalPrototypeQuantity(standout); + if (quantity <= FixedPoint2.Zero) + continue; + + var interpolateValue = quantity.Float() / solution.Volume.Float(); + color = Color.InterpolateBetween(color, + _prototypeManager.Index(standout).SubstanceColor, + interpolateValue); + } + } + + _appearance.SetData(ent, PuddleVisuals.CurrentVolume, volume.Float(), appearance); + _appearance.SetData(ent, PuddleVisuals.SolutionColor, color, appearance); + } + + public void DoTileReactions(TileRef tileRef, Solution solution) + { + for (var i = solution.Contents.Count - 1; i >= 0; i--) + { + var (reagent, quantity) = solution.Contents[i]; + var proto = _prototypeManager.Index(reagent.Prototype); + var removed = proto.ReactionTile(tileRef, quantity, EntityManager, reagent.Data); + if (removed <= FixedPoint2.Zero) + continue; + + solution.RemoveReagent(reagent, removed); + } + } + #region Spill // These methods are in Shared to make it easier to interact with PuddleSystem in Shared code. // Note that they always fail when run on the client, not creating a puddle and returning false.