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;
///
/// Mopping logic for interacting with puddle components.
///
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(OnAfterInteract);
SubscribeLocalEvent(OnActivateInWorld);
SubscribeLocalEvent(OnAbsorbentSolutionChange);
}
private void OnActivateInWorld(Entity ent, ref UserActivateInWorldEvent args)
{
if (args.Handled)
return;
Mop(ent, args.User, args.Target);
args.Handled = true;
}
private void OnAfterInteract(Entity ent, ref AfterInteractEvent args)
{
if (!args.CanReach || args.Handled || args.Target is not { } target)
return;
Mop(ent, args.User, target);
args.Handled = true;
}
private void OnAbsorbentSolutionChange(Entity ent, ref SolutionContainerChangedEvent args)
{
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 variant")]
public void Mop(EntityUid user, EntityUid target, EntityUid used, AbsorbentComponent component)
{
Mop((used, component), user, target);
}
public void Mop(Entity absorbEnt, EntityUid user, EntityUid target)
{
if (!SolutionContainer.TryGetSolution(absorbEnt.Owner, absorbEnt.Comp.SolutionName, out var absorberSoln))
return;
// Use the non-optional form of IsDelayed to safe the TryComp in Mop
if (TryComp(absorbEnt, out var useDelay)
&& _useDelay.IsDelayed((absorbEnt.Owner, useDelay)))
return;
// Try to slurp up the puddle.
// We're then done if our mop doesn't use absorber solutions, since those don't need refilling.
if (TryPuddleInteract((absorbEnt.Owner, absorbEnt.Comp, useDelay), absorberSoln.Value, user, target)
|| !absorbEnt.Comp.UseAbsorberSolution)
return;
// If it's refillable try to transfer
TryRefillableInteract((absorbEnt.Owner, absorbEnt.Comp, useDelay), absorberSoln.Value, user, target);
}
///
/// Logic for an absorbing entity interacting with a refillable.
///
private bool TryRefillableInteract(Entity absorbEnt,
Entity absorbentSoln,
EntityUid user,
EntityUid target)
{
if (!TryComp(target, out var refillable))
return false;
if (!SolutionContainer.TryGetRefillableSolution((target, refillable, null),
out var refillableSoln,
out var refillableSolution))
return false;
if (refillableSolution.Volume <= 0)
{
// Target empty - only transfer absorbent contents into refillable
if (!TryTransferFromAbsorbentToRefillable(absorbEnt, absorbentSoln, refillableSoln.Value, user, target))
return false;
}
else
{
// Target non-empty - do a two-way transfer
if (!TryTwoWayAbsorbentRefillableTransfer(absorbEnt, absorbentSoln, refillableSoln.Value, user, target))
return false;
}
var (used, absorber, useDelay) = absorbEnt;
_audio.PlayPredicted(absorber.TransferSound, target, user);
if (useDelay != null)
_useDelay.TryResetDelay((used, useDelay));
return true;
}
///
/// Logic for an transferring solution from absorber to an empty refillable.
///
private bool TryTransferFromAbsorbentToRefillable(Entity absorbEnt,
Entity absorbentSoln,
Entity refillableSoln,
EntityUid user,
EntityUid target)
{
var absorbentSolution = absorbentSoln.Comp.Solution;
if (absorbentSolution.Volume <= 0)
{
_popups.PopupClient(Loc.GetString("mopping-system-target-container-empty", ("target", target)), user, user);
return false;
}
var refillableSolution = refillableSoln.Comp.Solution;
var transferAmount = absorbEnt.Comp.PickupAmount < refillableSolution.AvailableVolume
? absorbEnt.Comp.PickupAmount
: refillableSolution.AvailableVolume;
if (transferAmount <= 0)
{
_popups.PopupClient(Loc.GetString("mopping-system-full", ("used", absorbEnt)), absorbEnt, user);
return false;
}
// Prioritize transferring non-evaporatives if absorbent has any
var contaminants = SolutionContainer.SplitSolutionWithout(absorbentSoln,
transferAmount,
Puddle.GetAbsorbentReagents(absorbentSoln.Comp.Solution));
SolutionContainer.TryAddSolution(refillableSoln,
contaminants.Volume > 0
? contaminants
: SolutionContainer.SplitSolution(absorbentSoln, transferAmount));
return true;
}
///
/// Logic for an transferring contaminants to a non-empty refillable & reabsorbing water if any available.
///
private bool TryTwoWayAbsorbentRefillableTransfer(Entity absorbEnt,
Entity absorbentSoln,
Entity refillableSoln,
EntityUid user,
EntityUid target)
{
var contaminantsFromAbsorbent = SolutionContainer.SplitSolutionWithout(absorbentSoln,
absorbEnt.Comp.PickupAmount,
Puddle.GetAbsorbentReagents(absorbentSoln.Comp.Solution));
var absorbentSolution = absorbentSoln.Comp.Solution;
if (contaminantsFromAbsorbent.Volume == FixedPoint2.Zero
&& absorbentSolution.AvailableVolume == FixedPoint2.Zero)
{
// Nothing to transfer to refillable and no room to absorb anything extra
_popups.PopupClient(Loc.GetString("mopping-system-puddle-space", ("used", absorbEnt)), user, user);
// We can return cleanly because nothing was split from absorbent solution
return false;
}
var waterPulled = absorbEnt.Comp.PickupAmount < absorbentSolution.AvailableVolume
? absorbEnt.Comp.PickupAmount
: absorbentSolution.AvailableVolume;
var refillableSolution = refillableSoln.Comp.Solution;
var waterFromRefillable =
refillableSolution.SplitSolutionWithOnly(waterPulled,
Puddle.GetAbsorbentReagents(refillableSoln.Comp.Solution));
SolutionContainer.UpdateChemicals(refillableSoln);
if (waterFromRefillable.Volume == FixedPoint2.Zero && contaminantsFromAbsorbent.Volume == FixedPoint2.Zero)
{
// Nothing to transfer in either direction
_popups.PopupClient(Loc.GetString("mopping-system-target-container-empty-water", ("target", target)),
user,
user);
// We can return cleanly because nothing was split from refillable solution
return false;
}
var anyTransferOccurred = false;
if (waterFromRefillable.Volume > FixedPoint2.Zero)
{
// transfer water to absorbent
SolutionContainer.TryAddSolution(absorbentSoln, waterFromRefillable);
anyTransferOccurred = true;
}
if (contaminantsFromAbsorbent.Volume <= 0)
return anyTransferOccurred;
if (refillableSolution.AvailableVolume <= 0)
{
_popups.PopupClient(Loc.GetString("mopping-system-full", ("used", target)), user, user);
}
else
{
// transfer as much contaminants to refillable as will fit
var contaminantsForRefillable = contaminantsFromAbsorbent.SplitSolution(refillableSolution.AvailableVolume);
SolutionContainer.TryAddSolution(refillableSoln, contaminantsForRefillable);
anyTransferOccurred = true;
}
// absorb everything that did not fit in the refillable back by the absorbent
SolutionContainer.TryAddSolution(absorbentSoln, contaminantsFromAbsorbent);
return anyTransferOccurred;
}
///
/// Logic for an absorbing entity interacting with a puddle.
///
private bool TryPuddleInteract(Entity absorbEnt,
Entity absorberSoln,
EntityUid user,
EntityUid target)
{
if (!TryComp(target, out var puddle))
return false;
if (!SolutionContainer.ResolveSolution(target, puddle.SolutionName, ref puddle.Solution, out var puddleSolution)
|| puddleSolution.Volume <= 0)
return false;
var (_, absorber, useDelay) = absorbEnt;
Solution puddleSplit;
var isRemoved = false;
if (absorber.UseAbsorberSolution)
{
// No reason to mop something that 1) can evaporate, 2) is an absorber, and 3) is being mopped with
// something that uses absorbers.
var puddleAbsorberVolume =
puddleSolution.GetTotalPrototypeQuantity(Puddle.GetAbsorbentReagents(puddleSolution));
if (puddleAbsorberVolume == puddleSolution.Volume)
{
_popups.PopupClient(Loc.GetString("mopping-system-puddle-already-mopped", ("target", target)),
target,
user);
return true;
}
// Check if we have any evaporative reagents on our absorber to transfer
var absorberSolution = absorberSoln.Comp.Solution;
var available = absorberSolution.GetTotalPrototypeQuantity(Puddle.GetAbsorbentReagents(absorberSolution));
// No material
if (available == FixedPoint2.Zero)
{
_popups.PopupClient(Loc.GetString("mopping-system-no-water", ("used", absorbEnt)), absorbEnt, user);
return true;
}
var transferMax = absorber.PickupAmount;
var transferAmount = available > transferMax ? transferMax : available;
puddleSplit =
puddleSolution.SplitSolutionWithout(transferAmount, Puddle.GetAbsorbentReagents(puddleSolution));
var absorberSplit =
absorberSolution.SplitSolutionWithOnly(puddleSplit.Volume,
Puddle.GetAbsorbentReagents(absorberSolution));
// Do tile reactions first
var targetXform = Transform(target);
var gridUid = targetXform.GridUid;
if (TryComp(gridUid, out var mapGrid))
{
var tileRef = _mapSystem.GetTileRef(gridUid.Value, mapGrid, targetXform.Coordinates);
Puddle.DoTileReactions(tileRef, absorberSplit);
}
SolutionContainer.AddSolution(puddle.Solution.Value, absorberSplit);
}
else
{
// Note: arguably shouldn't this get all solutions?
puddleSplit = puddleSolution.SplitSolutionWithout(absorber.PickupAmount, Puddle.GetAbsorbentReagents(puddleSolution));
// Despawn if we're done
if (puddleSolution.Volume == FixedPoint2.Zero)
{
// Spawn a *sparkle*
PredictedSpawnAttachedTo(absorber.MoppedEffect, Transform(target).Coordinates);
PredictedQueueDel(target);
isRemoved = true;
}
}
SolutionContainer.AddSolution(absorberSoln, puddleSplit);
_audio.PlayPredicted(absorber.PickupSound, isRemoved ? absorbEnt : target, user);
if (useDelay != null)
_useDelay.TryResetDelay((absorbEnt, useDelay));
var userXform = Transform(user);
var targetPos = _transform.GetWorldPosition(target);
var localPos = Vector2.Transform(targetPos, _transform.GetInvWorldMatrix(userXform));
localPos = userXform.LocalRotation.RotateVec(localPos);
_melee.DoLunge(user, absorbEnt, Angle.Zero, localPos, null);
return true;
}
}