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);
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);
+++ /dev/null
-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<SolutionRegenerationComponent, SolutionContainerManagerComponent>();
- 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);
- }
- }
- }
-}
-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;
/// <inheritdoc/>
-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<AbsorbentComponent, ComponentInit>(OnAbsorbentInit);
- SubscribeLocalEvent<AbsorbentComponent, AfterInteractEvent>(OnAfterInteract);
- SubscribeLocalEvent<AbsorbentComponent, UserActivateInWorldEvent>(OnActivateInWorld);
- SubscribeLocalEvent<AbsorbentComponent, SolutionContainerChangedEvent>(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<UseDelayComponent>(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;
- }
- }
-
- /// <summary>
- /// Logic for an absorbing entity interacting with a refillable.
- /// </summary>
- private bool TryRefillableInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, UseDelayComponent? useDelay, Entity<SolutionComponent> 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;
- }
-
- /// <summary>
- /// Logic for an transferring solution from absorber to an empty refillable.
- /// </summary>
- private bool TryTransferFromAbsorbentToRefillable(
- EntityUid user,
- EntityUid used,
- EntityUid target,
- AbsorbentComponent component,
- Entity<SolutionComponent> absorbentSoln,
- Entity<SolutionComponent> 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;
- }
-
- /// <summary>
- /// Logic for an transferring contaminants to a non-empty refillable & reabsorbing water if any available.
- /// </summary>
- private bool TryTwoWayAbsorbentRefillableTransfer(
- EntityUid user,
- EntityUid used,
- EntityUid target,
- AbsorbentComponent component,
- Entity<SolutionComponent> absorbentSoln,
- Entity<SolutionComponent> 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;
- }
-
- /// <summary>
- /// Logic for an absorbing entity interacting with a puddle.
- /// </summary>
- private bool TryPuddleInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent absorber, UseDelayComponent? useDelay, Entity<SolutionComponent> 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<TransformComponent>().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;
[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!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TurfSystem _turf = default!;
- private static readonly ProtoId<ReagentPrototype> Blood = "Blood";
- private static readonly ProtoId<ReagentPrototype> Slime = "Slime";
- private static readonly ProtoId<ReagentPrototype> 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<EntityUid> _deletionQueue = [];
// Shouldn't need re-anchoring.
SubscribeLocalEvent<PuddleComponent, AnchorStateChangedEvent>(OnAnchorChanged);
- SubscribeLocalEvent<PuddleComponent, SolutionContainerChangedEvent>(OnSolutionUpdate);
SubscribeLocalEvent<PuddleComponent, SpreadNeighborsEvent>(OnPuddleSpread);
SubscribeLocalEvent<PuddleComponent, SlipEvent>(OnPuddleSlip);
TickEvaporation();
}
- private void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
+ protected override void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
{
if (args.SolutionId != entity.Comp.SolutionName)
return;
+ base.OnSolutionUpdate(entity, ref args);
+
if (args.Solution.Volume <= 0)
{
_deletionQueue.Add(entity);
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<ReagentPrototype>(standout).SubstanceColor, interpolateValue);
- }
- }
-
- _appearance.SetData(uid, PuddleVisuals.CurrentVolume, volume.Float(), appearance);
- _appearance.SetData(uid, PuddleVisuals.SolutionColor, color, appearance);
}
private void UpdateSlip(Entity<PuddleComponent> entity, Solution solution)
// 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);
#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<ReagentPrototype>(reagent.Prototype);
- var removed = proto.ReactionTile(tileRef, quantity, EntityManager, reagent.Data);
- if (removed <= FixedPoint2.Zero)
- continue;
-
- solution.RemoveReagent(reagent, removed);
- }
- }
-
/// <summary>
/// Tries to get the relevant puddle entity for a tile.
/// </summary>
-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;
/// <summary>
/// Passively decreases a solution's quantity of reagent(s).
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
+[NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SolutionPurgeSystem))]
public sealed partial class SolutionPurgeComponent : Component
{
/// <summary>
/// The name of the solution to detract from.
/// </summary>
- [DataField("solution", required: true), ViewVariables(VVAccess.ReadWrite)]
+ [DataField(required: true)]
public string Solution = string.Empty;
/// <summary>
/// The reagent(s) to be ignored when purging the solution
/// </summary>
- [DataField("preserve", customTypeSerializer: typeof(PrototypeIdListSerializer<ReagentPrototype>))]
- [ViewVariables(VVAccess.ReadWrite)]
- public List<string> Preserve = new();
+ [DataField]
+ public List<ProtoId<ReagentPrototype>> Preserve = [];
/// <summary>
/// Amount of reagent(s) that are purged
/// </summary>
- [DataField("quantity", required: true), ViewVariables(VVAccess.ReadWrite)]
- public FixedPoint2 Quantity = default!;
+ [DataField(required: true)]
+ public FixedPoint2 Quantity;
/// <summary>
/// How long it takes to purge once.
/// </summary>
- [DataField("duration"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public TimeSpan Duration = TimeSpan.FromSeconds(1);
/// <summary>
/// The time when the next purge will occur.
/// </summary>
- [DataField("nextPurgeTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
- [AutoPausedField]
- public TimeSpan NextPurgeTime = TimeSpan.FromSeconds(0);
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoPausedField, AutoNetworkedField]
+ public TimeSpan NextPurgeTime;
}
-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;
/// <summary>
/// Passively increases a solution's quantity of a reagent.
/// </summary>
-[RegisterComponent, AutoGenerateComponentPause]
+[RegisterComponent, AutoGenerateComponentPause, AutoGenerateComponentState, NetworkedComponent]
[Access(typeof(SolutionRegenerationSystem))]
public sealed partial class SolutionRegenerationComponent : Component
{
/// The time when the next regeneration will occur.
/// </summary>
[DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
- [AutoPausedField]
- public TimeSpan NextRegenTime = TimeSpan.FromSeconds(0);
+ [AutoPausedField, AutoNetworkedField]
+ public TimeSpan NextRegenTime;
}
-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<SolutionPurgeComponent, MapInitEvent>(OnMapInit);
+ }
+
+ private void OnMapInit(Entity<SolutionPurgeComponent> ent, ref MapInitEvent args)
+ {
+ ent.Comp.NextPurgeTime = _timing.CurTime + ent.Comp.Duration;
+ Dirty(ent);
+ }
+
public override void Update(float frameTime)
{
base.Update(frameTime);
// 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());
+ }
}
}
}
--- /dev/null
+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<SolutionRegenerationComponent, MapInitEvent>(OnMapInit);
+ SubscribeLocalEvent<SolutionRegenerationComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
+ }
+
+ private void OnMapInit(Entity<SolutionRegenerationComponent> 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<SolutionRegenerationComponent> 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<SolutionRegenerationComponent, SolutionContainerManagerComponent>();
+ 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);
+ }
+ }
+}
using Content.Shared.FixedPoint;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
namespace Content.Shared.Fluids;
/// <summary>
/// For entities that can clean up puddles
/// </summary>
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class AbsorbentComponent : Component
{
- public Dictionary<Color, float> Progress = new();
+ /// <summary>
+ /// 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.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public Dictionary<Color, float> Progress = [];
/// <summary>
/// Name for solution container, that should be used for absorbed solution storage and as source of absorber solution.
[DataField]
public FixedPoint2 PickupAmount = FixedPoint2.New(100);
+ /// <summary>
+ /// The effect spawned when the puddle fully evaporates.
+ /// </summary>
[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));
/// <summary>
/// Marker that absorbent component owner should try to use 'absorber solution' to replace solution to be absorbed.
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Fluids.Components;
/// <summary>
/// Added to puddles that contain water so it may evaporate over time.
/// </summary>
-[NetworkedComponent]
+[NetworkedComponent, AutoGenerateComponentPause]
[RegisterComponent, Access(typeof(SharedPuddleSystem))]
public sealed partial class EvaporationComponent : Component
{
/// <summary>
/// The next time we remove the EvaporationSystem reagent amount from this entity.
/// </summary>
- [ViewVariables(VVAccess.ReadWrite), DataField("nextTick", customTypeSerializer: typeof(TimeOffsetSerializer))]
- public TimeSpan NextTick = TimeSpan.Zero;
+ [AutoPausedField, DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan NextTick;
/// <summary>
/// Evaporation factor. Multiplied by the evaporating speed of the reagent.
/// </summary>
- [DataField("evaporationAmount")]
+ [DataField]
public FixedPoint2 EvaporationAmount = FixedPoint2.New(1);
+
+ /// <summary>
+ /// The effect spawned when the puddle fully evaporates.
+ /// </summary>
+ [DataField]
+ public EntProtoId EvaporationEffect = "PuddleSparkle";
}
-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;
/// </summary>
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<AbsorbentComponent, ComponentGetState>(OnAbsorbentGetState);
- SubscribeLocalEvent<AbsorbentComponent, ComponentHandleState>(OnAbsorbentHandleState);
+
+ SubscribeLocalEvent<AbsorbentComponent, AfterInteractEvent>(OnAfterInteract);
+ SubscribeLocalEvent<AbsorbentComponent, UserActivateInWorldEvent>(OnActivateInWorld);
+ SubscribeLocalEvent<AbsorbentComponent, SolutionContainerChangedEvent>(OnAbsorbentSolutionChange);
+ }
+
+ private void OnActivateInWorld(Entity<AbsorbentComponent> ent, ref UserActivateInWorldEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ Mop(ent, args.User, args.Target);
+ args.Handled = true;
+ }
+
+ private void OnAfterInteract(Entity<AbsorbentComponent> ent, ref AfterInteractEvent args)
+ {
+ if (!args.CanReach || args.Handled || args.Target is not { } target)
+ return;
+
+ Mop(ent, args.User, target);
+ args.Handled = true;
}
- private void OnAbsorbentHandleState(EntityUid uid, AbsorbentComponent component, ref ComponentHandleState args)
+ private void OnAbsorbentSolutionChange(Entity<AbsorbentComponent> ent, ref SolutionContainerChangedEvent args)
{
- if (args.Current is not AbsorbentComponentState state)
+ if (!SolutionContainer.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out _, out var solution))
return;
- if (component.Progress.OrderBy(x => x.Key.ToArgb()).SequenceEqual(state.Progress))
+ 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);
+ }
+
+ [Obsolete("Use Entity<T> variant")]
+ public void Mop(EntityUid user, EntityUid target, EntityUid used, AbsorbentComponent component)
+ {
+ Mop((used, component), user, target);
+ }
+
+ public void Mop(Entity<AbsorbentComponent> absorbEnt, EntityUid user, EntityUid target)
+ {
+ if (!SolutionContainer.TryGetSolution(absorbEnt.Owner, absorbEnt.Comp.SolutionName, out var absorberSoln))
return;
- component.Progress.Clear();
- foreach (var item in state.Progress)
+ // Use the non-optional form of IsDelayed to safe the TryComp in Mop
+ if (TryComp<UseDelayComponent>(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);
+ }
+
+ /// <summary>
+ /// Logic for an absorbing entity interacting with a refillable.
+ /// </summary>
+ private bool TryRefillableInteract(Entity<AbsorbentComponent, UseDelayComponent?> absorbEnt,
+ Entity<SolutionComponent> absorbentSoln,
+ EntityUid user,
+ EntityUid target)
+ {
+ if (!TryComp<RefillableSolutionComponent>(target, out var refillable))
+ return false;
+
+ if (!SolutionContainer.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(absorbEnt, absorbentSoln, refillableSoln.Value, user, target))
+ return false;
+ }
+ else
{
- component.Progress.Add(item.Key, item.Value);
+ // 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;
}
- private void OnAbsorbentGetState(EntityUid uid, AbsorbentComponent component, ref ComponentGetState args)
+ /// <summary>
+ /// Logic for an transferring solution from absorber to an empty refillable.
+ /// </summary>
+ private bool TryTransferFromAbsorbentToRefillable(Entity<AbsorbentComponent> absorbEnt,
+ Entity<SolutionComponent> absorbentSoln,
+ Entity<SolutionComponent> refillableSoln,
+ EntityUid user,
+ EntityUid target)
{
- args.State = new AbsorbentComponentState(component.Progress);
+ 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;
+ }
+
+ /// <summary>
+ /// Logic for an transferring contaminants to a non-empty refillable & reabsorbing water if any available.
+ /// </summary>
+ private bool TryTwoWayAbsorbentRefillableTransfer(Entity<AbsorbentComponent> absorbEnt,
+ Entity<SolutionComponent> absorbentSoln,
+ Entity<SolutionComponent> 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;
}
- [Serializable, NetSerializable]
- protected sealed class AbsorbentComponentState : ComponentState
+ /// <summary>
+ /// Logic for an absorbing entity interacting with a puddle.
+ /// </summary>
+ private bool TryPuddleInteract(Entity<AbsorbentComponent, UseDelayComponent?> absorbEnt,
+ Entity<SolutionComponent> absorberSoln,
+ EntityUid user,
+ EntityUid target)
{
- public Dictionary<Color, float> Progress;
+ if (!TryComp<PuddleComponent>(target, out var puddle))
+ return false;
- public AbsorbentComponentState(Dictionary<Color, float> progress)
+ 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)
{
- Progress = progress;
+ // 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<MapGridComponent>(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;
}
}
+using System.Linq;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
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;
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<ReagentPrototype> Blood = "Blood";
+ private static readonly ProtoId<ReagentPrototype> Slime = "Slime";
+ private static readonly ProtoId<ReagentPrototype> CopperBlood = "CopperBlood";
+
+ private static readonly string[] StandoutReagents = [Blood, Slime, CopperBlood];
+
/// <summary>
/// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle.
/// </summary>
SubscribeLocalEvent<DumpableSolutionComponent, CanDropTargetEvent>(OnDumpCanDropTarget);
SubscribeLocalEvent<DrainableSolutionComponent, CanDropTargetEvent>(OnDrainCanDropTarget);
SubscribeLocalEvent<RefillableSolutionComponent, CanDropDraggedEvent>(OnRefillableCanDropDragged);
+
+ SubscribeLocalEvent<PuddleComponent, SolutionContainerChangedEvent>(OnSolutionUpdate);
SubscribeLocalEvent<PuddleComponent, GetFootstepSoundEvent>(OnGetFootstepSound);
SubscribeLocalEvent<PuddleComponent, ExaminedEvent>(HandlePuddleExamined);
+ SubscribeLocalEvent<PuddleComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
InitializeSpillable();
}
+ protected virtual void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
+ {
+ if (args.SolutionId != entity.Comp.SolutionName)
+ return;
+
+ UpdateAppearance((entity, entity.Comp));
+ }
+
private void OnRefillableCanDrag(Entity<RefillableSolutionComponent> entity, ref CanDragEvent args)
{
args.Handled = true;
}
}
+ // Workaround for https://github.com/space-wizards/space-station-14/pull/35314
+ private void OnEntRemoved(Entity<PuddleComponent> 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<PuddleComponent?, AppearanceComponent?> 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<ReagentPrototype>(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<ReagentPrototype>(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.