Make mopping predicted (and some other stuff) (#38749)
* refactor: move puddle evaporation + absorbents to shared * refactor: move SolutionRegeneration to shared * refactor: make AbsorbentSystem visuals clientside * style: general formatting/cleanup on touched files - Few logical simplifications - Add field for hard-coded sparkle effect ent - Switch stuff to Entity<T> No actual prediction fixes in this commit (though in retrospect I should've done this commit last). * fix: use predicted variants for predicted code * fix: average out evaporation rates in mixtures * refactor: move SolutionPurge to shared * style: Basic SolutionPurgeComponent field cleanup * fix: general prediction + timing + networking fixes - Moves client side visuals back to shared because other players exist - Don't accumulate CurTime in Purge/RegenerationSystem - Network the next update field in Purge/RegenerationSystem to deal with UI mispredictions??? * fix: add udder bug workaround Not needed for SolutionPurgeSystem which doesn't resolve solutions (probably fine that SolutionPurgeSystem doesn't cache since it's much rarer, though it probably should), and likely not needed for AbsorbentSystem since it only resolves against puddles which, I don't think can be in containers. * fix: don't divide by zero for evaporation speed = 0. * refactor: revert evaporation changes Will cherry-pick these out in another PR. Also reverting the evaporation speed bugfix since it's easier to revert all at once. :) * fix: component cleanup; autopause fields, use ProtoID * fix: remove unused AbsorbentComponentState * fix: ProtoId is not string * refactor: move PuddleSystem.UpdateAppearance to shared * style: general PuddleSystem.UpdateAppearance tweaks - Switch to Entity<T> - Use ProtoIds - Minor simplifications * fix: add udderly silly PVS workaround * cleanup * fix --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <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;
|
||||
|
||||
@@ -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<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 = [];
|
||||
@@ -86,7 +79,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
|
||||
// Shouldn't need re-anchoring.
|
||||
SubscribeLocalEvent<PuddleComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||
SubscribeLocalEvent<PuddleComponent, SolutionContainerChangedEvent>(OnSolutionUpdate);
|
||||
SubscribeLocalEvent<PuddleComponent, SpreadNeighborsEvent>(OnPuddleSpread);
|
||||
SubscribeLocalEvent<PuddleComponent, SlipEvent>(OnPuddleSlip);
|
||||
|
||||
@@ -319,11 +311,13 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
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);
|
||||
@@ -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<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)
|
||||
@@ -451,7 +405,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
if (slipperyUnits > 0)
|
||||
{
|
||||
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits);
|
||||
slipComp.SlipData.ParalyzeTime = (stunTimer/(float)slipperyUnits);
|
||||
slipComp.SlipData.ParalyzeTime = stunTimer / (float)slipperyUnits;
|
||||
}
|
||||
|
||||
// Only make it super slippery if there is enough super slippery units for its own puddle
|
||||
@@ -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<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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <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
|
||||
{
|
||||
@@ -39,6 +39,6 @@ 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;
|
||||
}
|
||||
@@ -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<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);
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <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.
|
||||
@@ -26,23 +32,23 @@ public sealed partial class AbsorbentComponent : Component
|
||||
[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.
|
||||
|
||||
@@ -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;
|
||||
/// <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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// </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 OnAbsorbentHandleState(EntityUid uid, AbsorbentComponent component, ref ComponentHandleState args)
|
||||
private void OnActivateInWorld(Entity<AbsorbentComponent> 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<AbsorbentComponent> ent, ref AfterInteractEvent args)
|
||||
{
|
||||
if (!args.CanReach || args.Handled || args.Target is not { } target)
|
||||
return;
|
||||
|
||||
component.Progress.Clear();
|
||||
foreach (var item in state.Progress)
|
||||
Mop(ent, args.User, target);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnAbsorbentSolutionChange(Entity<AbsorbentComponent> ent, ref SolutionContainerChangedEvent args)
|
||||
{
|
||||
component.Progress.Add(item.Key, item.Value);
|
||||
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);
|
||||
}
|
||||
|
||||
[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;
|
||||
|
||||
// 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
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (!TryComp<PuddleComponent>(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<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;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAbsorbentGetState(EntityUid uid, AbsorbentComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new AbsorbentComponentState(component.Progress);
|
||||
}
|
||||
SolutionContainer.AddSolution(absorberSoln, puddleSplit);
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
protected sealed class AbsorbentComponentState : ComponentState
|
||||
{
|
||||
public Dictionary<Color, float> Progress;
|
||||
_audio.PlayPredicted(absorber.PickupSound, isRemoved ? absorbEnt : target, user);
|
||||
|
||||
public AbsorbentComponentState(Dictionary<Color, float> progress)
|
||||
{
|
||||
Progress = progress;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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>
|
||||
@@ -33,12 +42,23 @@ public abstract partial class SharedPuddleSystem : EntitySystem
|
||||
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;
|
||||
@@ -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<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.
|
||||
|
||||
Reference in New Issue
Block a user