Puddles & spreader refactor (#15191)
This commit is contained in:
211
Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs
Normal file
211
Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
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.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed class AbsorbentSystem : SharedAbsorbentSystem
|
||||
{
|
||||
[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 SolutionContainerSystem _solutionSystem = default!;
|
||||
[Dependency] private readonly UseDelaySystem _useDelay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<AbsorbentComponent, ComponentInit>(OnAbsorbentInit);
|
||||
SubscribeLocalEvent<AbsorbentComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<AbsorbentComponent, SolutionChangedEvent>(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, SolutionChangedEvent args)
|
||||
{
|
||||
UpdateAbsorbent(uid, component);
|
||||
}
|
||||
|
||||
private void UpdateAbsorbent(EntityUid uid, AbsorbentComponent component)
|
||||
{
|
||||
if (!_solutionSystem.TryGetSolution(uid, AbsorbentComponent.SolutionName, out var solution))
|
||||
return;
|
||||
|
||||
var oldProgress = component.Progress.ShallowClone();
|
||||
component.Progress.Clear();
|
||||
|
||||
if (solution.TryGetReagent(PuddleSystem.EvaporationReagent, out var water))
|
||||
{
|
||||
component.Progress[_prototype.Index<ReagentPrototype>(PuddleSystem.EvaporationReagent).SubstanceColor] = water.Float();
|
||||
}
|
||||
|
||||
var otherColor = solution.GetColorWithout(_prototype, PuddleSystem.EvaporationReagent);
|
||||
var other = (solution.Volume - water).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(component);
|
||||
}
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, AbsorbentComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (!args.CanReach || args.Handled || _useDelay.ActiveDelay(uid))
|
||||
return;
|
||||
|
||||
if (!_solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorberSoln))
|
||||
return;
|
||||
|
||||
// Didn't click anything so don't do anything.
|
||||
if (args.Target is not { Valid: true } target)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's a puddle try to grab from
|
||||
if (!TryPuddleInteract(args.User, uid, target, component, absorberSoln))
|
||||
{
|
||||
// Do a transfer, try to get water onto us and transfer anything else to them.
|
||||
|
||||
// If it's anything else transfer to
|
||||
if (!TryTransferAbsorber(args.User, uid, target, component, absorberSoln))
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to fill an absorber from some refillable solution.
|
||||
/// </summary>
|
||||
private bool TryTransferAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln)
|
||||
{
|
||||
if (!TryComp(target, out RefillableSolutionComponent? refillable))
|
||||
return false;
|
||||
|
||||
if (!_solutionSystem.TryGetRefillableSolution(target, out var refillableSolution, refillable: refillable))
|
||||
return false;
|
||||
|
||||
if (refillableSolution.Volume <= 0)
|
||||
{
|
||||
var msg = Loc.GetString("mopping-system-target-container-empty", ("target", target));
|
||||
_popups.PopupEntity(msg, user, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove the non-water reagents.
|
||||
// Remove water on target
|
||||
// Then do the transfer.
|
||||
var nonWater = absorberSoln.SplitSolutionWithout(component.PickupAmount, PuddleSystem.EvaporationReagent);
|
||||
|
||||
if (nonWater.Volume == FixedPoint2.Zero && absorberSoln.AvailableVolume == FixedPoint2.Zero)
|
||||
{
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-puddle-space", ("used", used)), user, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
var transferAmount = component.PickupAmount < absorberSoln.AvailableVolume ?
|
||||
component.PickupAmount :
|
||||
absorberSoln.AvailableVolume;
|
||||
|
||||
var water = refillableSolution.RemoveReagent(PuddleSystem.EvaporationReagent, transferAmount);
|
||||
|
||||
if (water == FixedPoint2.Zero && nonWater.Volume == FixedPoint2.Zero)
|
||||
{
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-target-container-empty-water", ("target", target)), user, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
absorberSoln.AddReagent(PuddleSystem.EvaporationReagent, water);
|
||||
refillableSolution.AddSolution(nonWater, _prototype);
|
||||
|
||||
_solutionSystem.UpdateChemicals(used, absorberSoln);
|
||||
_solutionSystem.UpdateChemicals(target, refillableSolution);
|
||||
_audio.PlayPvs(component.TransferSound, target);
|
||||
_useDelay.BeginDelay(used);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logic for an absorbing entity interacting with a puddle.
|
||||
/// </summary>
|
||||
private bool TryPuddleInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent absorber, Solution absorberSoln)
|
||||
{
|
||||
if (!TryComp(target, out PuddleComponent? puddle))
|
||||
return false;
|
||||
|
||||
if (!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) || puddleSolution.Volume <= 0)
|
||||
return false;
|
||||
|
||||
// Check if the puddle has any non-evaporative reagents
|
||||
if (_puddleSystem.CanFullyEvaporate(puddleSolution))
|
||||
{
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-puddle-evaporate", ("target", target)), user, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if we have any evaporative reagents on our absorber to transfer
|
||||
absorberSoln.TryGetReagent(PuddleSystem.EvaporationReagent, out var available);
|
||||
|
||||
// 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;
|
||||
|
||||
var split = puddleSolution.SplitSolutionWithout(transferAmount, PuddleSystem.EvaporationReagent);
|
||||
|
||||
absorberSoln.RemoveReagent(PuddleSystem.EvaporationReagent, split.Volume);
|
||||
puddleSolution.AddReagent(PuddleSystem.EvaporationReagent, split.Volume);
|
||||
absorberSoln.AddSolution(split, _prototype);
|
||||
|
||||
_solutionSystem.UpdateChemicals(used, absorberSoln);
|
||||
_solutionSystem.UpdateChemicals(target, puddleSolution);
|
||||
_audio.PlayPvs(absorber.PickupSound, target);
|
||||
_useDelay.BeginDelay(used);
|
||||
|
||||
var userXform = Transform(user);
|
||||
var targetPos = _transform.GetWorldPosition(target);
|
||||
var localPos = _transform.GetInvWorldMatrix(userXform).Transform(targetPos);
|
||||
localPos = userXform.LocalRotation.RotateVec(localPos);
|
||||
|
||||
_melee.DoLunge(user, Angle.Zero, localPos, null, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Content.Server.Fluids.Components;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Fluids.Components;
|
||||
using Robust.Shared.Collections;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class EvaporationSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
foreach (var evaporationComponent in EntityManager.EntityQuery<EvaporationComponent>())
|
||||
{
|
||||
var uid = evaporationComponent.Owner;
|
||||
evaporationComponent.Accumulator += frameTime;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, evaporationComponent.SolutionName, out var solution))
|
||||
{
|
||||
// If no solution, delete the entity
|
||||
EntityManager.QueueDeleteEntity(uid);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evaporationComponent.Accumulator < evaporationComponent.EvaporateTime)
|
||||
continue;
|
||||
|
||||
evaporationComponent.Accumulator -= evaporationComponent.EvaporateTime;
|
||||
|
||||
if (evaporationComponent.EvaporationToggle)
|
||||
{
|
||||
_solutionContainerSystem.SplitSolution(uid, solution,
|
||||
FixedPoint2.Min(FixedPoint2.New(1), solution.Volume)); // removes 1 unit, or solution current volume, whichever is lower.
|
||||
}
|
||||
|
||||
evaporationComponent.EvaporationToggle =
|
||||
solution.Volume > evaporationComponent.LowerLimit
|
||||
&& solution.Volume < evaporationComponent.UpperLimit;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy constructor to copy initial fields from source to destination.
|
||||
/// </summary>
|
||||
/// <param name="destUid">Entity to which we copy <paramref name="srcEvaporation"/> properties</param>
|
||||
/// <param name="srcEvaporation">Component that contains relevant properties</param>
|
||||
public void CopyConstruct(EntityUid destUid, EvaporationComponent srcEvaporation)
|
||||
{
|
||||
var destEvaporation = EntityManager.EnsureComponent<EvaporationComponent>(destUid);
|
||||
destEvaporation.EvaporateTime = srcEvaporation.EvaporateTime;
|
||||
destEvaporation.EvaporationToggle = srcEvaporation.EvaporationToggle;
|
||||
destEvaporation.SolutionName = srcEvaporation.SolutionName;
|
||||
destEvaporation.LowerLimit = srcEvaporation.LowerLimit;
|
||||
destEvaporation.UpperLimit = srcEvaporation.UpperLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Shared;
|
||||
using Content.Shared.Directions;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Physics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Component that governs overflowing puddles. Controls how Puddles spread and updat
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class FluidSpreaderSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Adds an overflow component to the map data component tracking overflowing puddles
|
||||
/// </summary>
|
||||
/// <param name="puddleUid">EntityUid of overflowing puddle</param>
|
||||
/// <param name="puddle">Optional PuddleComponent</param>
|
||||
/// <param name="xform">Optional TransformComponent</param>
|
||||
public void AddOverflowingPuddle(EntityUid puddleUid, PuddleComponent? puddle = null,
|
||||
TransformComponent? xform = null)
|
||||
{
|
||||
if (!Resolve(puddleUid, ref puddle, ref xform, false) || xform.MapUid == null)
|
||||
return;
|
||||
|
||||
var mapId = xform.MapUid.Value;
|
||||
|
||||
EntityManager.EnsureComponent<FluidMapDataComponent>(mapId, out var component);
|
||||
component.Puddles.Add(puddleUid);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
Span<Direction> exploreDirections = stackalloc Direction[]
|
||||
{
|
||||
Direction.North,
|
||||
Direction.East,
|
||||
Direction.South,
|
||||
Direction.West,
|
||||
};
|
||||
var puddles = new List<PuddleComponent>(4);
|
||||
var puddleQuery = GetEntityQuery<PuddleComponent>();
|
||||
var xFormQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
foreach (var fluidMapData in EntityQuery<FluidMapDataComponent>())
|
||||
{
|
||||
if (fluidMapData.Puddles.Count == 0 || _gameTiming.CurTime <= fluidMapData.GoalTime)
|
||||
continue;
|
||||
|
||||
var newIteration = new HashSet<EntityUid>();
|
||||
foreach (var puddleUid in fluidMapData.Puddles)
|
||||
{
|
||||
if (!puddleQuery.TryGetComponent(puddleUid, out var puddle)
|
||||
|| !xFormQuery.TryGetComponent(puddleUid, out var transform)
|
||||
|| !_mapManager.TryGetGrid(transform.GridUid, out var mapGrid))
|
||||
continue;
|
||||
|
||||
puddles.Clear();
|
||||
var pos = transform.Coordinates;
|
||||
|
||||
var totalVolume = _puddleSystem.CurrentVolume(puddleUid, puddle);
|
||||
exploreDirections.Shuffle();
|
||||
foreach (var direction in exploreDirections)
|
||||
{
|
||||
var newPos = pos.Offset(direction);
|
||||
if (CheckTile(puddleUid, puddle, newPos, mapGrid, puddleQuery, out var uid, out var component))
|
||||
{
|
||||
puddles.Add(component);
|
||||
totalVolume += _puddleSystem.CurrentVolume(uid.Value, component);
|
||||
}
|
||||
}
|
||||
|
||||
_puddleSystem.EqualizePuddles(puddleUid, puddles, totalVolume, newIteration, puddle);
|
||||
}
|
||||
|
||||
fluidMapData.Puddles.Clear();
|
||||
fluidMapData.Puddles.UnionWith(newIteration);
|
||||
fluidMapData.UpdateGoal(_gameTiming.CurTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check a tile is valid for solution allocation.
|
||||
/// </summary>
|
||||
/// <param name="srcUid">Entity Uid of original puddle</param>
|
||||
/// <param name="srcPuddle">PuddleComponent attached to srcUid</param>
|
||||
/// <param name="dstPos">at which to check tile</param>
|
||||
/// <param name="mapGrid">helper param needed to extract entities</param>
|
||||
/// <param name="newPuddleUid">either found or newly created PuddleComponent.</param>
|
||||
/// <returns>true if tile is empty or occupied by a non-overflowing puddle (or a puddle close to being overflowing)</returns>
|
||||
private bool CheckTile(EntityUid srcUid, PuddleComponent srcPuddle, EntityCoordinates dstPos,
|
||||
MapGridComponent mapGrid, EntityQuery<PuddleComponent> puddleQuery,
|
||||
[NotNullWhen(true)] out EntityUid? newPuddleUid, [NotNullWhen(true)] out PuddleComponent? newPuddleComp)
|
||||
{
|
||||
if (!mapGrid.TryGetTileRef(dstPos, out var tileRef)
|
||||
|| tileRef.Tile.IsEmpty)
|
||||
{
|
||||
newPuddleUid = null;
|
||||
newPuddleComp = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if puddle can spread there at all
|
||||
var dstMap = dstPos.ToMap(EntityManager, _transform);
|
||||
var dst = dstMap.Position;
|
||||
var src = Transform(srcUid).MapPosition.Position;
|
||||
var dir = src - dst;
|
||||
var ray = new CollisionRay(dst, dir.Normalized, (int) (CollisionGroup.Impassable | CollisionGroup.HighImpassable));
|
||||
var mapId = dstMap.MapId;
|
||||
var results = _physics.IntersectRay(mapId, ray, dir.Length, returnOnFirstHit: true);
|
||||
if (results.Any())
|
||||
{
|
||||
newPuddleUid = null;
|
||||
newPuddleComp = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var puddleCurrentVolume = _puddleSystem.CurrentVolume(srcUid, srcPuddle);
|
||||
foreach (var entity in dstPos.GetEntitiesInTile())
|
||||
{
|
||||
if (puddleQuery.TryGetComponent(entity, out var existingPuddle))
|
||||
{
|
||||
if (_puddleSystem.CurrentVolume(entity, existingPuddle) >= puddleCurrentVolume)
|
||||
{
|
||||
newPuddleUid = null;
|
||||
newPuddleComp = null;
|
||||
return false;
|
||||
}
|
||||
newPuddleUid = entity;
|
||||
newPuddleComp = existingPuddle;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_puddleSystem.SpawnPuddle(srcUid, dstPos, srcPuddle, out var uid, out var comp);
|
||||
newPuddleUid = uid;
|
||||
newPuddleComp = comp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
using Content.Server.Chemistry.Components.SolutionManager;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Fluids;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Tag;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class MoppingSystem : SharedMoppingSystem
|
||||
{
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly SpillableSystem _spillableSystem = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popups = default!;
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
|
||||
const string PuddlePrototypeId = "PuddleSmear"; // The puddle prototype to use when releasing liquid to the floor, making a new puddle
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<AbsorbentComponent, ComponentInit>(OnAbsorbentInit);
|
||||
SubscribeLocalEvent<AbsorbentComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<AbsorbentComponent, AbsorbantDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<AbsorbentComponent, SolutionChangedEvent>(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, SolutionChangedEvent args)
|
||||
{
|
||||
UpdateAbsorbent(uid, component);
|
||||
}
|
||||
|
||||
private void UpdateAbsorbent(EntityUid uid, AbsorbentComponent component)
|
||||
{
|
||||
if (!_solutionSystem.TryGetSolution(uid, AbsorbentComponent.SolutionName, out var solution))
|
||||
return;
|
||||
|
||||
var oldProgress = component.Progress;
|
||||
|
||||
component.Progress = (float) (solution.Volume / solution.MaxVolume);
|
||||
if (component.Progress.Equals(oldProgress))
|
||||
return;
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, AbsorbentComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (!args.CanReach || args.Handled)
|
||||
return;
|
||||
|
||||
if (!_solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorberSoln))
|
||||
return;
|
||||
|
||||
if (args.Target is not { Valid: true } target)
|
||||
{
|
||||
// Add liquid to an empty floor tile
|
||||
args.Handled = TryCreatePuddle(args.User, args.ClickLocation, component, absorberSoln);
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = TryPuddleInteract(args.User, uid, target, component, absorberSoln)
|
||||
|| TryEmptyAbsorber(args.User, uid, target, component, absorberSoln)
|
||||
|| TryFillAbsorber(args.User, uid, target, component, absorberSoln);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to create a puddle using solutions stored in the absorber entity.
|
||||
/// </summary>
|
||||
private bool TryCreatePuddle(EntityUid user, EntityCoordinates clickLocation, AbsorbentComponent absorbent, Solution absorberSoln)
|
||||
{
|
||||
if (absorberSoln.Volume <= 0)
|
||||
return false;
|
||||
|
||||
if (!_mapManager.TryGetGrid(clickLocation.GetGridUid(EntityManager), out var mapGrid))
|
||||
return false;
|
||||
|
||||
var releaseAmount = FixedPoint2.Min(absorbent.ResidueAmount, absorberSoln.Volume);
|
||||
var releasedSolution = _solutionSystem.SplitSolution(absorbent.Owner, absorberSoln, releaseAmount);
|
||||
_spillableSystem.SpillAt(mapGrid.GetTileRef(clickLocation), releasedSolution, PuddlePrototypeId);
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-release-to-floor"), user, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to fill an absorber from some drainable solution.
|
||||
/// </summary>
|
||||
private bool TryFillAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln)
|
||||
{
|
||||
if (absorberSoln.AvailableVolume <= 0 || !TryComp(target, out DrainableSolutionComponent? drainable))
|
||||
return false;
|
||||
|
||||
if (!_solutionSystem.TryGetDrainableSolution(target, out var drainableSolution))
|
||||
return false;
|
||||
|
||||
if (drainableSolution.Volume <= 0)
|
||||
{
|
||||
var msg = Loc.GetString("mopping-system-target-container-empty", ("target", target));
|
||||
_popups.PopupEntity(msg, user, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Let's transfer up to to half the tool's available capacity to the tool.
|
||||
var quantity = FixedPoint2.Max(component.PickupAmount, absorberSoln.AvailableVolume / 2);
|
||||
quantity = FixedPoint2.Min(quantity, drainableSolution.Volume);
|
||||
|
||||
DoMopInteraction(user, used, target, component, drainable.Solution, quantity, 1, "mopping-system-drainable-success", component.TransferSound);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Empty an absorber into a refillable solution.
|
||||
/// </summary>
|
||||
private bool TryEmptyAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln)
|
||||
{
|
||||
if (absorberSoln.Volume <= 0 || !TryComp(target, out RefillableSolutionComponent? refillable))
|
||||
return false;
|
||||
|
||||
if (!_solutionSystem.TryGetRefillableSolution(target, out var targetSolution))
|
||||
return false;
|
||||
|
||||
string msg;
|
||||
if (targetSolution.AvailableVolume <= 0)
|
||||
{
|
||||
msg = Loc.GetString("mopping-system-target-container-full", ("target", target));
|
||||
_popups.PopupEntity(msg, user, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if the target container is too small (e.g. syringe)
|
||||
// TODO this should really be a tag or something, not a capacity check.
|
||||
if (targetSolution.MaxVolume <= FixedPoint2.New(20))
|
||||
{
|
||||
msg = Loc.GetString("mopping-system-target-container-too-small", ("target", target));
|
||||
_popups.PopupEntity(msg, user, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
float delay;
|
||||
FixedPoint2 quantity = absorberSoln.Volume;
|
||||
|
||||
// TODO this really needs cleaning up. Less magic numbers, more data-fields.
|
||||
|
||||
if (_tagSystem.HasTag(used, "Mop") // if the tool used is a literal mop (and not a sponge, rag, etc.)
|
||||
&& !_tagSystem.HasTag(target, "Wringer")) // and if the target does not have a wringer for properly drying the mop
|
||||
{
|
||||
delay = 5.0f; // Should take much longer if you don't have a wringer
|
||||
|
||||
var frac = quantity / absorberSoln.MaxVolume;
|
||||
|
||||
// squeeze up to 60% of the solution from the mop if the mop is more than one-quarter full
|
||||
if (frac > 0.25)
|
||||
quantity *= 0.6;
|
||||
|
||||
if (frac > 0.5)
|
||||
msg = "mopping-system-hand-squeeze-still-wet";
|
||||
else if (frac > 0.5)
|
||||
msg = "mopping-system-hand-squeeze-little-wet";
|
||||
else
|
||||
msg = "mopping-system-hand-squeeze-dry";
|
||||
}
|
||||
else
|
||||
{
|
||||
msg = "mopping-system-refillable-success";
|
||||
delay = 1.0f;
|
||||
}
|
||||
|
||||
// negative quantity as we are removing solutions from the mop
|
||||
quantity = -FixedPoint2.Min(targetSolution.AvailableVolume, quantity);
|
||||
|
||||
DoMopInteraction(user, used, target, component, refillable.Solution, quantity, delay, msg, component.TransferSound);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logic for an absorbing entity interacting with a puddle.
|
||||
/// </summary>
|
||||
private bool TryPuddleInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent absorber, Solution absorberSoln)
|
||||
{
|
||||
if (!TryComp(target, out PuddleComponent? puddle))
|
||||
return false;
|
||||
|
||||
if (!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) || puddleSolution.Volume <= 0)
|
||||
return false;
|
||||
|
||||
FixedPoint2 quantity;
|
||||
|
||||
// Get lower limit for mopping
|
||||
FixedPoint2 lowerLimit = FixedPoint2.Zero;
|
||||
if (TryComp(target, out EvaporationComponent? evaporation)
|
||||
&& evaporation.EvaporationToggle
|
||||
&& evaporation.LowerLimit == 0)
|
||||
{
|
||||
lowerLimit = absorber.LowerLimit;
|
||||
}
|
||||
|
||||
// Can our absorber even absorb any liquid?
|
||||
if (puddleSolution.Volume <= lowerLimit)
|
||||
{
|
||||
// Cannot absorb any more liquid. So clearly the user wants to add liquid to the puddle... right?
|
||||
// This is the old behavior and I CBF fixing this, for the record I don't like this.
|
||||
|
||||
quantity = FixedPoint2.Min(absorber.ResidueAmount, absorberSoln.Volume);
|
||||
if (quantity <= 0)
|
||||
return false;
|
||||
|
||||
// Dilutes the puddle with some solution from the tool
|
||||
_solutionSystem.TryTransferSolution(used, target, absorberSoln, puddleSolution, quantity);
|
||||
_audio.PlayPvs(absorber.TransferSound, used);
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-puddle-diluted"), user);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (absorberSoln.AvailableVolume < 0)
|
||||
{
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-tool-full", ("used", used)), user, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
quantity = FixedPoint2.Min(absorber.PickupAmount, puddleSolution.Volume - lowerLimit, absorberSoln.AvailableVolume);
|
||||
if (quantity <= 0)
|
||||
return false;
|
||||
|
||||
var delay = absorber.PickupAmount.Float() / absorber.Speed;
|
||||
DoMopInteraction(user, used, target, absorber, puddle.SolutionName, quantity, delay, "mopping-system-puddle-success", absorber.PickupSound);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DoMopInteraction(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, string targetSolution,
|
||||
FixedPoint2 transferAmount, float delay, string msg, SoundSpecifier sfx)
|
||||
{
|
||||
// Can't interact with too many entities at once.
|
||||
if (component.MaxInteractingEntities < component.InteractingEntities.Count + 1)
|
||||
return;
|
||||
|
||||
// Can't interact with the same container multiple times at once.
|
||||
if (!component.InteractingEntities.Add(target))
|
||||
return;
|
||||
|
||||
var ev = new AbsorbantDoAfterEvent(targetSolution, msg, sfx, transferAmount);
|
||||
|
||||
var doAfterArgs = new DoAfterArgs(user, delay, ev, used, target: target, used: used)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
MovementThreshold = 0.2f
|
||||
};
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(doAfterArgs);
|
||||
}
|
||||
|
||||
private void OnDoAfter(EntityUid uid, AbsorbentComponent component, AbsorbantDoAfterEvent args)
|
||||
{
|
||||
if (args.Target == null)
|
||||
return;
|
||||
|
||||
component.InteractingEntities.Remove(args.Target.Value);
|
||||
|
||||
if (args.Cancelled || args.Handled)
|
||||
return;
|
||||
|
||||
_audio.PlayPvs(args.Sound, uid);
|
||||
_popups.PopupEntity(Loc.GetString(args.Message, ("target", args.Target.Value), ("used", uid)), uid);
|
||||
_solutionSystem.TryTransferSolution(args.Target.Value, uid, args.TargetSolution,
|
||||
AbsorbentComponent.SolutionName, args.TransferAmount);
|
||||
component.InteractingEntities.Remove(args.Target.Value);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Shared.Fluids;
|
||||
using Content.Shared.Fluids.Components;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Fluids.Components;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
public sealed partial class PuddleSystem
|
||||
{
|
||||
private static readonly TimeSpan EvaporationCooldown = TimeSpan.FromSeconds(1);
|
||||
|
||||
public const string EvaporationReagent = "Water";
|
||||
|
||||
private void OnEvaporationMapInit(EntityUid uid, EvaporationComponent component, MapInitEvent args)
|
||||
{
|
||||
component.NextTick = _timing.CurTime + EvaporationCooldown;
|
||||
}
|
||||
|
||||
private void UpdateEvaporation(EntityUid uid, Solution solution)
|
||||
{
|
||||
if (HasComp<EvaporationComponent>(uid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (solution.ContainsReagent(EvaporationReagent))
|
||||
{
|
||||
var evaporation = AddComp<EvaporationComponent>(uid);
|
||||
evaporation.NextTick = _timing.CurTime + EvaporationCooldown;
|
||||
return;
|
||||
}
|
||||
|
||||
RemComp<EvaporationComponent>(uid);
|
||||
}
|
||||
|
||||
private void TickEvaporation()
|
||||
{
|
||||
var query = EntityQueryEnumerator<EvaporationComponent, PuddleComponent>();
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var curTime = _timing.CurTime;
|
||||
while (query.MoveNext(out var uid, out var evaporation, out var puddle))
|
||||
{
|
||||
if (evaporation.NextTick > curTime)
|
||||
continue;
|
||||
|
||||
evaporation.NextTick += EvaporationCooldown;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, puddle.SolutionName, out var puddleSolution))
|
||||
continue;
|
||||
|
||||
var reagentTick = evaporation.EvaporationAmount * EvaporationCooldown.TotalSeconds;
|
||||
puddleSolution.RemoveReagent(EvaporationReagent, reagentTick);
|
||||
|
||||
// Despawn if we're done
|
||||
if (puddleSolution.Volume == FixedPoint2.Zero)
|
||||
{
|
||||
// Spawn a *sparkle*
|
||||
Spawn("PuddleSparkle", xformQuery.GetComponent(uid).Coordinates);
|
||||
QueueDel(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanFullyEvaporate(Solution solution)
|
||||
{
|
||||
return solution.Contents.Count == 1 && solution.ContainsReagent(EvaporationReagent);
|
||||
}
|
||||
}
|
||||
139
Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs
Normal file
139
Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Server.Nutrition.Components;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Spillable;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Verbs;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
public sealed partial class PuddleSystem
|
||||
{
|
||||
private void InitializeSpillable()
|
||||
{
|
||||
SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand);
|
||||
SubscribeLocalEvent<SpillableComponent, GetVerbsEvent<Verb>>(AddSpillVerb);
|
||||
SubscribeLocalEvent<SpillableComponent, GotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<SpillableComponent, SolutionSpikeOverflowEvent>(OnSpikeOverflow);
|
||||
SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter);
|
||||
}
|
||||
|
||||
private void OnSpikeOverflow(EntityUid uid, SpillableComponent component, SolutionSpikeOverflowEvent args)
|
||||
{
|
||||
if (!args.Handled)
|
||||
{
|
||||
TrySpillAt(Transform(uid).Coordinates, args.Overflow, out _);
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnGotEquipped(EntityUid uid, SpillableComponent component, GotEquippedEvent args)
|
||||
{
|
||||
if (!component.SpillWorn)
|
||||
return;
|
||||
|
||||
if (!TryComp(uid, out ClothingComponent? clothing))
|
||||
return;
|
||||
|
||||
// check if entity was actually used as clothing
|
||||
// not just taken in pockets or something
|
||||
var isCorrectSlot = clothing.Slots.HasFlag(args.SlotFlags);
|
||||
if (!isCorrectSlot)
|
||||
return;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
|
||||
return;
|
||||
|
||||
if (solution.Volume == 0)
|
||||
return;
|
||||
|
||||
// spill all solution on the player
|
||||
var drainedSolution = _solutionContainerSystem.Drain(uid, solution, solution.Volume);
|
||||
TrySpillAt(args.Equipee, drainedSolution, out _);
|
||||
}
|
||||
|
||||
private void SpillOnLand(EntityUid uid, SpillableComponent component, ref LandEvent args)
|
||||
{
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
|
||||
return;
|
||||
|
||||
if (TryComp<DrinkComponent>(uid, out var drink) && !drink.Opened)
|
||||
return;
|
||||
|
||||
if (args.User != null)
|
||||
{
|
||||
_adminLogger.Add(LogType.Landed,
|
||||
$"{ToPrettyString(uid):entity} spilled a solution {SolutionContainerSystem.ToPrettyString(solution):solution} on landing");
|
||||
}
|
||||
|
||||
var drainedSolution = _solutionContainerSystem.Drain(uid, solution, solution.Volume);
|
||||
TrySplashSpillAt(uid, Transform(uid).Coordinates, drainedSolution, out _);
|
||||
}
|
||||
|
||||
private void AddSpillVerb(EntityUid uid, SpillableComponent component, GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract)
|
||||
return;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(args.Target, component.SolutionName, out var solution))
|
||||
return;
|
||||
|
||||
if (TryComp<DrinkComponent>(args.Target, out var drink) && (!drink.Opened))
|
||||
return;
|
||||
|
||||
if (solution.Volume == FixedPoint2.Zero)
|
||||
return;
|
||||
|
||||
Verb verb = new()
|
||||
{
|
||||
Text = Loc.GetString("spill-target-verb-get-data-text")
|
||||
};
|
||||
|
||||
// TODO VERB ICONS spill icon? pouring out a glass/beaker?
|
||||
if (component.SpillDelay == null)
|
||||
{
|
||||
verb.Act = () =>
|
||||
{
|
||||
var puddleSolution = _solutionContainerSystem.SplitSolution(args.Target,
|
||||
solution, solution.Volume);
|
||||
TrySpillAt(Transform(args.Target).Coordinates, puddleSolution, out _);
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
verb.Act = () =>
|
||||
{
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(args.User, component.SpillDelay ?? 0, new SpillDoAfterEvent(), uid, target: uid)
|
||||
{
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
NeedHand = true,
|
||||
});
|
||||
};
|
||||
}
|
||||
verb.Impact = LogImpact.Medium; // dangerous reagent reaction are logged separately.
|
||||
verb.DoContactInteraction = true;
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private void OnDoAfter(EntityUid uid, SpillableComponent component, DoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || args.Args.Target == null)
|
||||
return;
|
||||
|
||||
//solution gone by other means before doafter completes
|
||||
if (!_solutionContainerSystem.TryGetDrainableSolution(uid, out var solution) || solution.Volume == 0)
|
||||
return;
|
||||
|
||||
var puddleSolution = _solutionContainerSystem.SplitSolution(uid, solution, solution.Volume);
|
||||
TrySpillAt(Transform(uid).Coordinates, puddleSolution, out _);
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.DragDrop;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Fluids;
|
||||
using Content.Shared.Fluids.Components;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
public sealed partial class PuddleSystem
|
||||
{
|
||||
private void InitializeTransfers()
|
||||
{
|
||||
SubscribeLocalEvent<RefillableSolutionComponent, DragDropDraggedEvent>(OnRefillableDragged);
|
||||
}
|
||||
|
||||
private void OnRefillableDragged(EntityUid uid, RefillableSolutionComponent component, ref DragDropDraggedEvent args)
|
||||
{
|
||||
_solutionContainerSystem.TryGetSolution(uid, component.Solution, out var solution);
|
||||
|
||||
if (solution?.Volume == FixedPoint2.Zero)
|
||||
{
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-empty", ("used", uid)), uid, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
TryComp<DrainableSolutionComponent>(args.Target, out var drainable);
|
||||
|
||||
_solutionContainerSystem.TryGetDrainableSolution(args.Target, out var drainableSolution, drainable);
|
||||
|
||||
// Dump reagents into drain
|
||||
if (TryComp<DrainComponent>(args.Target, out var drain) && drainable != null)
|
||||
{
|
||||
if (drainableSolution == null || solution == null)
|
||||
return;
|
||||
|
||||
var split = _solutionContainerSystem.SplitSolution(uid, solution, drainableSolution.AvailableVolume);
|
||||
|
||||
// TODO: Drane refactor
|
||||
if (_solutionContainerSystem.TryAddSolution(args.Target, drainableSolution, split))
|
||||
{
|
||||
_audio.PlayPvs(AbsorbentComponent.DefaultTransferSound, args.Target);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", args.Target)), args.Target, args.User);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Take reagents from target
|
||||
if (drainable != null)
|
||||
{
|
||||
if (drainableSolution == null || solution == null)
|
||||
return;
|
||||
|
||||
var split = _solutionContainerSystem.SplitSolution(args.Target, drainableSolution, solution.AvailableVolume);
|
||||
|
||||
if (_solutionContainerSystem.TryAddSolution(uid, solution, split))
|
||||
{
|
||||
_audio.PlayPvs(AbsorbentComponent.DefaultTransferSound, uid);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", uid)), uid, args.User);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,313 +1,639 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Server.Spreader;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Fluids;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Slippery;
|
||||
using Content.Shared.Fluids.Components;
|
||||
using Content.Shared.StepTrigger.Components;
|
||||
using Content.Shared.StepTrigger.Systems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Solution = Content.Shared.Chemistry.Components.Solution;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles solutions on floors. Also handles the spreader logic for where the solution overflows a specified volume.
|
||||
/// </summary>
|
||||
public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class PuddleSystem : EntitySystem
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger= default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly ReactiveSystem _reactive = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popups = default!;
|
||||
[Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||
|
||||
public static float PuddleVolume = 1000;
|
||||
|
||||
// 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 = new();
|
||||
|
||||
/*
|
||||
* TODO: Need some sort of way to do blood slash / vomit solution spill on its own
|
||||
* This would then evaporate into the puddle tile below
|
||||
*/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly FluidSpreaderSystem _fluidSpreaderSystem = default!;
|
||||
[Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly ReactiveSystem _reactive = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
base.Initialize();
|
||||
|
||||
public static float PuddleVolume = 1000;
|
||||
// Shouldn't need re-anchoring.
|
||||
SubscribeLocalEvent<PuddleComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||
SubscribeLocalEvent<PuddleComponent, ExaminedEvent>(HandlePuddleExamined);
|
||||
SubscribeLocalEvent<PuddleComponent, SolutionChangedEvent>(OnSolutionUpdate);
|
||||
SubscribeLocalEvent<PuddleComponent, ComponentInit>(OnPuddleInit);
|
||||
SubscribeLocalEvent<PuddleComponent, SpreadNeighborsEvent>(OnPuddleSpread);
|
||||
SubscribeLocalEvent<PuddleComponent, SlipEvent>(OnPuddleSlip);
|
||||
|
||||
// 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 = new();
|
||||
SubscribeLocalEvent<EvaporationComponent, MapInitEvent>(OnEvaporationMapInit);
|
||||
|
||||
public override void Initialize()
|
||||
InitializeSpillable();
|
||||
InitializeTransfers();
|
||||
}
|
||||
|
||||
private void OnPuddleSpread(EntityUid uid, PuddleComponent component, ref SpreadNeighborsEvent args)
|
||||
{
|
||||
var overflow = GetOverflowSolution(uid, component);
|
||||
|
||||
if (overflow.Volume == FixedPoint2.Zero)
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// Shouldn't need re-anchoring.
|
||||
SubscribeLocalEvent<PuddleComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||
SubscribeLocalEvent<PuddleComponent, ExaminedEvent>(HandlePuddleExamined);
|
||||
SubscribeLocalEvent<PuddleComponent, SolutionChangedEvent>(OnSolutionUpdate);
|
||||
SubscribeLocalEvent<PuddleComponent, ComponentInit>(OnPuddleInit);
|
||||
SubscribeLocalEvent<PuddleComponent, SlipEvent>(OnPuddleSlip);
|
||||
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
var xform = Transform(uid);
|
||||
|
||||
if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
|
||||
{
|
||||
base.Update(frameTime);
|
||||
foreach (var ent in _deletionQueue)
|
||||
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
var puddleQuery = GetEntityQuery<PuddleComponent>();
|
||||
|
||||
// First we overflow to neighbors with overflow capacity
|
||||
// Then we go to free tiles
|
||||
// Then we go to anything else.
|
||||
if (args.Neighbors.Count > 0)
|
||||
{
|
||||
_random.Shuffle(args.Neighbors);
|
||||
|
||||
// Overflow to neighbors with remaining space.
|
||||
foreach (var neighbor in args.Neighbors)
|
||||
{
|
||||
Del(ent);
|
||||
if (!puddleQuery.TryGetComponent(neighbor, out var puddle) ||
|
||||
!_solutionContainerSystem.TryGetSolution(neighbor, puddle.SolutionName, out var neighborSolution))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var remaining = neighborSolution.Volume - puddle.OverflowVolume;
|
||||
|
||||
if (remaining <= FixedPoint2.Zero)
|
||||
continue;
|
||||
|
||||
var split = overflow.SplitSolution(remaining);
|
||||
|
||||
if (!_solutionContainerSystem.TryAddSolution(neighbor, neighborSolution, split))
|
||||
continue;
|
||||
|
||||
args.Updates--;
|
||||
EnsureComp<EdgeSpreaderComponent>(neighbor);
|
||||
|
||||
if (args.Updates <= 0)
|
||||
break;
|
||||
}
|
||||
_deletionQueue.Clear();
|
||||
}
|
||||
|
||||
private void OnPuddleInit(EntityUid uid, PuddleComponent component, ComponentInit args)
|
||||
{
|
||||
_solutionContainerSystem.EnsureSolution(uid, component.SolutionName, FixedPoint2.New(PuddleVolume), out _);
|
||||
}
|
||||
|
||||
private void OnPuddleSlip(EntityUid uid, PuddleComponent component, ref SlipEvent args)
|
||||
{
|
||||
// Reactive entities have a chance to get a touch reaction from slipping on a puddle
|
||||
// (i.e. it is implied they fell face first onto it or something)
|
||||
if (!HasComp<ReactiveComponent>(args.Slipped))
|
||||
return;
|
||||
|
||||
// Eventually probably have some system of 'body coverage' to tweak the probability but for now just 0.5
|
||||
// (implying that spacemen have a 50% chance to either land on their ass or their face)
|
||||
if (!_random.Prob(0.5f))
|
||||
return;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
|
||||
return;
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", uid)),
|
||||
args.Slipped, args.Slipped, PopupType.SmallCaution);
|
||||
|
||||
// Take 15% of the puddle solution
|
||||
var splitSol = _solutionContainerSystem.SplitSolution(uid, solution, solution.Volume * 0.15f);
|
||||
_reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch);
|
||||
}
|
||||
|
||||
private void OnSolutionUpdate(EntityUid uid, PuddleComponent component, SolutionChangedEvent args)
|
||||
{
|
||||
if (args.Solution.Name != component.SolutionName)
|
||||
return;
|
||||
|
||||
if (args.Solution.Volume <= 0)
|
||||
if (overflow.Volume == FixedPoint2.Zero)
|
||||
{
|
||||
_deletionQueue.Add(uid);
|
||||
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
_deletionQueue.Remove(uid);
|
||||
UpdateSlip(uid, component);
|
||||
UpdateAppearance(uid, component);
|
||||
}
|
||||
|
||||
private void UpdateAppearance(EntityUid uid, PuddleComponent? puddleComponent = null, AppearanceComponent? appearance = null)
|
||||
if (args.NeighborFreeTiles.Count > 0 && args.Updates > 0)
|
||||
{
|
||||
if (!Resolve(uid, ref puddleComponent, ref appearance, false)
|
||||
|| EmptyHolder(uid, puddleComponent))
|
||||
_random.Shuffle(args.NeighborFreeTiles);
|
||||
var spillAmount = overflow.Volume / args.NeighborFreeTiles.Count;
|
||||
|
||||
foreach (var tile in args.NeighborFreeTiles)
|
||||
{
|
||||
return;
|
||||
var split = overflow.SplitSolution(spillAmount);
|
||||
TrySpillAt(grid.GridTileToLocal(tile), split, out _, false);
|
||||
args.Updates--;
|
||||
|
||||
if (args.Updates <= 0)
|
||||
break;
|
||||
}
|
||||
|
||||
// Opacity based on level of fullness to overflow
|
||||
// Hard-cap lower bound for visibility reasons
|
||||
var puddleSolution = _solutionContainerSystem.EnsureSolution(uid, puddleComponent.SolutionName);
|
||||
var volumeScale = puddleSolution.Volume.Float() /
|
||||
puddleComponent.OverflowVolume.Float() *
|
||||
puddleComponent.OpacityModifier;
|
||||
|
||||
bool isEvaporating;
|
||||
|
||||
if (TryComp(uid, out EvaporationComponent? evaporation)
|
||||
&& evaporation.EvaporationToggle)// if puddle is evaporating.
|
||||
{
|
||||
isEvaporating = true;
|
||||
}
|
||||
else isEvaporating = false;
|
||||
|
||||
var color = puddleSolution.GetColor(_protoMan);
|
||||
|
||||
_appearance.SetData(uid, PuddleVisuals.VolumeScale, volumeScale, appearance);
|
||||
_appearance.SetData(uid, PuddleVisuals.CurrentVolume, puddleSolution.Volume, appearance);
|
||||
_appearance.SetData(uid, PuddleVisuals.SolutionColor, color, appearance);
|
||||
_appearance.SetData(uid, PuddleVisuals.IsEvaporatingVisual, isEvaporating, appearance);
|
||||
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
private void UpdateSlip(EntityUid entityUid, PuddleComponent puddleComponent)
|
||||
if (overflow.Volume > FixedPoint2.Zero && args.Neighbors.Count > 0 && args.Updates > 0)
|
||||
{
|
||||
var vol = CurrentVolume(puddleComponent.Owner, puddleComponent);
|
||||
if ((puddleComponent.SlipThreshold == FixedPoint2.New(-1) ||
|
||||
vol < puddleComponent.SlipThreshold) &&
|
||||
TryComp(entityUid, out StepTriggerComponent? stepTrigger))
|
||||
var spillPerNeighbor = overflow.Volume / args.Neighbors.Count;
|
||||
|
||||
foreach (var neighbor in args.Neighbors)
|
||||
{
|
||||
_stepTrigger.SetActive(entityUid, false, stepTrigger);
|
||||
}
|
||||
else if (vol >= puddleComponent.SlipThreshold)
|
||||
{
|
||||
var comp = EnsureComp<StepTriggerComponent>(entityUid);
|
||||
_stepTrigger.SetActive(entityUid, true, comp);
|
||||
// Overflow to neighbours but not if they're already at the cap
|
||||
// This is to avoid diluting solutions too much.
|
||||
if (!puddleQuery.TryGetComponent(neighbor, out var puddle) ||
|
||||
!_solutionContainerSystem.TryGetSolution(neighbor, puddle.SolutionName, out var neighborSolution) ||
|
||||
neighborSolution.Volume >= puddle.OverflowVolume)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var split = overflow.SplitSolution(spillPerNeighbor);
|
||||
|
||||
if (!_solutionContainerSystem.TryAddSolution(neighbor, neighborSolution, split))
|
||||
continue;
|
||||
|
||||
EnsureComp<EdgeSpreaderComponent>(neighbor);
|
||||
args.Updates--;
|
||||
|
||||
if (args.Updates <= 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePuddleExamined(EntityUid uid, PuddleComponent component, ExaminedEvent args)
|
||||
// Add the remainder back
|
||||
if (_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var puddleSolution))
|
||||
{
|
||||
if (TryComp<StepTriggerComponent>(uid, out var slippery) && slippery.Active)
|
||||
_solutionContainerSystem.TryAddSolution(uid, puddleSolution, overflow);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPuddleSlip(EntityUid uid, PuddleComponent component, ref SlipEvent args)
|
||||
{
|
||||
// Reactive entities have a chance to get a touch reaction from slipping on a puddle
|
||||
// (i.e. it is implied they fell face first onto it or something)
|
||||
if (!HasComp<ReactiveComponent>(args.Slipped))
|
||||
return;
|
||||
|
||||
// Eventually probably have some system of 'body coverage' to tweak the probability but for now just 0.5
|
||||
// (implying that spacemen have a 50% chance to either land on their ass or their face)
|
||||
if (!_random.Prob(0.5f))
|
||||
return;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
|
||||
return;
|
||||
|
||||
_popups.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", uid)),
|
||||
args.Slipped, args.Slipped, PopupType.SmallCaution);
|
||||
|
||||
// Take 15% of the puddle solution
|
||||
var splitSol = _solutionContainerSystem.SplitSolution(uid, solution, solution.Volume * 0.15f);
|
||||
_reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
foreach (var ent in _deletionQueue)
|
||||
{
|
||||
Del(ent);
|
||||
}
|
||||
_deletionQueue.Clear();
|
||||
|
||||
TickEvaporation();
|
||||
}
|
||||
|
||||
private void OnPuddleInit(EntityUid uid, PuddleComponent component, ComponentInit args)
|
||||
{
|
||||
_solutionContainerSystem.EnsureSolution(uid, component.SolutionName, FixedPoint2.New(PuddleVolume), out _);
|
||||
}
|
||||
|
||||
private void OnSolutionUpdate(EntityUid uid, PuddleComponent component, SolutionChangedEvent args)
|
||||
{
|
||||
if (args.Solution.Name != component.SolutionName)
|
||||
return;
|
||||
|
||||
if (args.Solution.Volume <= 0)
|
||||
{
|
||||
_deletionQueue.Add(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
_deletionQueue.Remove(uid);
|
||||
UpdateSlip(uid, component, args.Solution);
|
||||
UpdateEvaporation(uid, args.Solution);
|
||||
UpdateAppearance(uid, component);
|
||||
}
|
||||
|
||||
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.TryGetSolution(uid, puddleComponent.SolutionName, out var solution))
|
||||
{
|
||||
volume = solution.Volume / puddleComponent.OverflowVolume;
|
||||
|
||||
// Make blood stand out more
|
||||
// Kinda EH
|
||||
// Could potentially do alpha per-solution but future problem.
|
||||
var standoutReagents = new string[] { "Blood", "Slime" };
|
||||
|
||||
color = solution.GetColorWithout(_prototypeManager, standoutReagents);
|
||||
color = color.WithAlpha(0.7f);
|
||||
|
||||
foreach (var standout in standoutReagents)
|
||||
{
|
||||
args.PushText(Loc.GetString("puddle-component-examine-is-slipper-text"));
|
||||
if (!solution.TryGetReagent(standout, out var quantity))
|
||||
continue;
|
||||
|
||||
var interpolateValue = quantity.Float() / solution.Volume.Float();
|
||||
color = Color.InterpolateBetween(color, _prototypeManager.Index<ReagentPrototype>(standout).SubstanceColor, interpolateValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAnchorChanged(EntityUid uid, PuddleComponent puddle, ref AnchorStateChangedEvent args)
|
||||
_appearance.SetData(uid, PuddleVisuals.CurrentVolume, volume.Float(), appearance);
|
||||
_appearance.SetData(uid, PuddleVisuals.SolutionColor, color, appearance);
|
||||
}
|
||||
|
||||
private void UpdateSlip(EntityUid entityUid, PuddleComponent component, Solution solution)
|
||||
{
|
||||
var isSlippery = false;
|
||||
// The base sprite is currently at 0.3 so we require at least 2nd tier to be slippery or else it's too hard to see.
|
||||
var amountRequired = FixedPoint2.New(component.OverflowVolume.Float() * LowThreshold);
|
||||
var slipperyAmount = FixedPoint2.Zero;
|
||||
|
||||
foreach (var reagent in solution.Contents)
|
||||
{
|
||||
if (!args.Anchored)
|
||||
QueueDel(uid);
|
||||
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.ReagentId);
|
||||
|
||||
if (reagentProto.Slippery)
|
||||
{
|
||||
slipperyAmount += reagent.Quantity;
|
||||
|
||||
if (slipperyAmount > amountRequired)
|
||||
{
|
||||
isSlippery = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool EmptyHolder(EntityUid uid, PuddleComponent? puddleComponent = null)
|
||||
if (isSlippery)
|
||||
{
|
||||
if (!Resolve(uid, ref puddleComponent))
|
||||
return true;
|
||||
var comp = EnsureComp<StepTriggerComponent>(entityUid);
|
||||
_stepTrigger.SetActive(entityUid, true, comp);
|
||||
}
|
||||
else if (TryComp<StepTriggerComponent>(entityUid, out var comp))
|
||||
{
|
||||
_stepTrigger.SetActive(entityUid, false, comp);
|
||||
}
|
||||
}
|
||||
|
||||
return !_solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName,
|
||||
out var solution)
|
||||
|| solution.Contents.Count == 0;
|
||||
private void HandlePuddleExamined(EntityUid uid, PuddleComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (TryComp<StepTriggerComponent>(uid, out var slippery) && slippery.Active)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("puddle-component-examine-is-slipper-text"));
|
||||
}
|
||||
|
||||
public FixedPoint2 CurrentVolume(EntityUid uid, PuddleComponent? puddleComponent = null)
|
||||
if (HasComp<EvaporationComponent>(uid))
|
||||
{
|
||||
if (!Resolve(uid, ref puddleComponent))
|
||||
return FixedPoint2.Zero;
|
||||
if (_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution) &&
|
||||
CanFullyEvaporate(solution))
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating"));
|
||||
}
|
||||
else if (solution?.ContainsReagent(EvaporationReagent) == true)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-partial"));
|
||||
}
|
||||
else
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-no"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-no"));
|
||||
}
|
||||
}
|
||||
|
||||
return _solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName,
|
||||
out var solution)
|
||||
? solution.Volume
|
||||
: FixedPoint2.Zero;
|
||||
private void OnAnchorChanged(EntityUid uid, PuddleComponent puddle, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
if (!args.Anchored)
|
||||
QueueDel(uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current volume of the given puddle, which may not necessarily be PuddleVolume.
|
||||
/// </summary>
|
||||
public FixedPoint2 CurrentVolume(EntityUid uid, PuddleComponent? puddleComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref puddleComponent))
|
||||
return FixedPoint2.Zero;
|
||||
|
||||
return _solutionContainerSystem.TryGetSolution(uid, puddleComponent.SolutionName,
|
||||
out var solution)
|
||||
? solution.Volume
|
||||
: FixedPoint2.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to add solution to <paramref name="puddleUid"/>.
|
||||
/// </summary>
|
||||
/// <param name="puddleUid">Puddle to which we add</param>
|
||||
/// <param name="addedSolution">Solution that is added to puddleComponent</param>
|
||||
/// <param name="sound">Play sound on overflow</param>
|
||||
/// <param name="checkForOverflow">Overflow on encountered values</param>
|
||||
/// <param name="puddleComponent">Optional resolved PuddleComponent</param>
|
||||
/// <returns></returns>
|
||||
public bool TryAddSolution(EntityUid puddleUid,
|
||||
Solution addedSolution,
|
||||
bool sound = true,
|
||||
bool checkForOverflow = true,
|
||||
PuddleComponent? puddleComponent = null)
|
||||
{
|
||||
if (!Resolve(puddleUid, ref puddleComponent))
|
||||
return false;
|
||||
|
||||
if (addedSolution.Volume == 0 ||
|
||||
!_solutionContainerSystem.TryGetSolution(puddleUid, puddleComponent.SolutionName,
|
||||
out var solution))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to add solution to <paramref name="puddleUid"/>.
|
||||
/// </summary>
|
||||
/// <param name="puddleUid">Puddle to which we add</param>
|
||||
/// <param name="addedSolution">Solution that is added to puddleComponent</param>
|
||||
/// <param name="sound">Play sound on overflow</param>
|
||||
/// <param name="checkForOverflow">Overflow on encountered values</param>
|
||||
/// <param name="puddleComponent">Optional resolved PuddleComponent</param>
|
||||
/// <returns></returns>
|
||||
public bool TryAddSolution(EntityUid puddleUid,
|
||||
Solution addedSolution,
|
||||
bool sound = true,
|
||||
bool checkForOverflow = true,
|
||||
PuddleComponent? puddleComponent = null)
|
||||
solution.AddSolution(addedSolution, _prototypeManager);
|
||||
_solutionContainerSystem.UpdateChemicals(puddleUid, solution, true);
|
||||
|
||||
if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent))
|
||||
{
|
||||
if (!Resolve(puddleUid, ref puddleComponent))
|
||||
return false;
|
||||
EnsureComp<EdgeSpreaderComponent>(puddleUid);
|
||||
}
|
||||
|
||||
if (addedSolution.Volume == 0 ||
|
||||
!_solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName,
|
||||
out var solution))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
solution.AddSolution(addedSolution, _protoMan);
|
||||
_solutionContainerSystem.UpdateChemicals(puddleUid, solution, true);
|
||||
|
||||
if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent))
|
||||
{
|
||||
_fluidSpreaderSystem.AddOverflowingPuddle(puddleComponent.Owner, puddleComponent);
|
||||
}
|
||||
|
||||
if (!sound)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
SoundSystem.Play(puddleComponent.SpillSound.GetSound(),
|
||||
Filter.Pvs(puddleComponent.Owner), puddleComponent.Owner);
|
||||
if (!sound)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a large srcPuddle and smaller destination puddles, this method will equalize their <see cref="Solution.CurrentVolume"/>
|
||||
/// </summary>
|
||||
/// <param name="srcPuddle">puddle that donates liquids to other puddles</param>
|
||||
/// <param name="destinationPuddles">List of puddles that we want to equalize, their puddle <see cref="Solution.CurrentVolume"/> should be less than sourcePuddleComponent</param>
|
||||
/// <param name="totalVolume">Total volume of src and destination puddle</param>
|
||||
/// <param name="stillOverflowing">optional parameter, that after equalization adds all still overflowing puddles.</param>
|
||||
/// <param name="sourcePuddleComponent">puddleComponent for <paramref name="srcPuddle"/></param>
|
||||
public void EqualizePuddles(EntityUid srcPuddle, List<PuddleComponent> destinationPuddles,
|
||||
FixedPoint2 totalVolume,
|
||||
HashSet<EntityUid>? stillOverflowing = null,
|
||||
PuddleComponent? sourcePuddleComponent = null)
|
||||
SoundSystem.Play(puddleComponent.SpillSound.GetSound(),
|
||||
Filter.Pvs(puddleUid), puddleUid);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether adding this solution to this puddle would overflow.
|
||||
/// </summary>
|
||||
public bool WouldOverflow(EntityUid uid, Solution solution, PuddleComponent? puddle = null)
|
||||
{
|
||||
if (!Resolve(uid, ref puddle))
|
||||
return false;
|
||||
|
||||
return CurrentVolume(uid, puddle) + solution.Volume > puddle.OverflowVolume;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether adding this solution to this puddle would overflow.
|
||||
/// </summary>
|
||||
private bool IsOverflowing(EntityUid uid, PuddleComponent? puddle = null)
|
||||
{
|
||||
if (!Resolve(uid, ref puddle))
|
||||
return false;
|
||||
|
||||
return CurrentVolume(uid, puddle) > puddle.OverflowVolume;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the solution amount above the overflow threshold for the puddle.
|
||||
/// </summary>
|
||||
public Solution GetOverflowSolution(EntityUid uid, PuddleComponent? puddle = null)
|
||||
{
|
||||
if (!Resolve(uid, ref puddle) || !_solutionContainerSystem.TryGetSolution(uid, puddle.SolutionName,
|
||||
out var solution))
|
||||
{
|
||||
if (!Resolve(srcPuddle, ref sourcePuddleComponent)
|
||||
|| !_solutionContainerSystem.TryGetSolution(srcPuddle, sourcePuddleComponent.SolutionName,
|
||||
out var srcSolution))
|
||||
return;
|
||||
return new Solution(0);
|
||||
}
|
||||
|
||||
var dividedVolume = totalVolume / (destinationPuddles.Count + 1);
|
||||
// TODO: This is going to fail with struct solutions.
|
||||
var remaining = puddle.OverflowVolume;
|
||||
var split = _solutionContainerSystem.SplitSolution(uid, solution, CurrentVolume(uid, puddle) - remaining);
|
||||
return split;
|
||||
}
|
||||
|
||||
foreach (var destPuddle in destinationPuddles)
|
||||
#region Spill
|
||||
|
||||
/// <summary>
|
||||
/// First splashes reagent on reactive entities near the spilling entity, then spills the rest regularly to a
|
||||
/// puddle. This is intended for 'destructive' spills, like when entities are destroyed or thrown.
|
||||
/// </summary>
|
||||
public bool TrySplashSpillAt(EntityUid uid,
|
||||
EntityCoordinates coordinates,
|
||||
Solution solution,
|
||||
out EntityUid puddleUid,
|
||||
bool sound = true,
|
||||
EntityUid? user = null)
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
|
||||
if (solution.Volume == 0)
|
||||
return false;
|
||||
|
||||
// Get reactive entities nearby--if there are some, it'll spill a bit on them instead.
|
||||
foreach (var ent in _lookup.GetComponentsInRange<ReactiveComponent>(coordinates, 1.0f))
|
||||
{
|
||||
// sorry! no overload for returning uid, so .owner must be used
|
||||
var owner = ent.Owner;
|
||||
|
||||
// between 5 and 30%
|
||||
var splitAmount = solution.Volume * _random.NextFloat(0.05f, 0.30f);
|
||||
var splitSolution = solution.SplitSolution(splitAmount);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
if (!_solutionContainerSystem.TryGetSolution(destPuddle.Owner, destPuddle.SolutionName,
|
||||
out var destSolution))
|
||||
_adminLogger.Add(LogType.Landed,
|
||||
$"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}");
|
||||
}
|
||||
|
||||
_reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
|
||||
_popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other", ("spillable", uid), ("target", owner)), owner, PopupType.SmallCaution);
|
||||
}
|
||||
|
||||
return TrySpillAt(coordinates, solution, out puddleUid, sound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spills solution at the specified coordinates.
|
||||
/// Will add to an existing puddle if present or create a new one if not.
|
||||
/// </summary>
|
||||
public bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true)
|
||||
{
|
||||
if (solution.Volume == 0)
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_mapManager.TryGetGrid(coordinates.GetGridUid(EntityManager), out var mapGrid))
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
return TrySpillAt(mapGrid.GetTileRef(coordinates), solution, out puddleUid, sound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="TrySpillAt(Robust.Shared.Map.EntityCoordinates,Content.Shared.Chemistry.Components.Solution,out Robust.Shared.GameObjects.EntityUid,bool)"/>
|
||||
/// </summary>
|
||||
public bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true, TransformComponent? transformComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref transformComponent, false))
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
return TrySpillAt(transformComponent.Coordinates, solution, out puddleUid, sound: sound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="TrySpillAt(Robust.Shared.Map.EntityCoordinates,Content.Shared.Chemistry.Components.Solution,out Robust.Shared.GameObjects.EntityUid,bool)"/>
|
||||
/// </summary>
|
||||
public bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true, bool tileReact = true)
|
||||
{
|
||||
if (solution.Volume <= 0)
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
// If space return early, let that spill go out into the void
|
||||
if (tileRef.Tile.IsEmpty)
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Let's not spill to invalid grids.
|
||||
var gridId = tileRef.GridUid;
|
||||
if (!_mapManager.TryGetGrid(gridId, out var mapGrid))
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tileReact)
|
||||
{
|
||||
// First, do all tile reactions
|
||||
for (var i = 0; i < solution.Contents.Count; i++)
|
||||
{
|
||||
var (reagentId, quantity) = solution.Contents[i];
|
||||
var proto = _prototypeManager.Index<ReagentPrototype>(reagentId);
|
||||
var removed = proto.ReactionTile(tileRef, quantity);
|
||||
if (removed <= FixedPoint2.Zero)
|
||||
continue;
|
||||
|
||||
var takeAmount = FixedPoint2.Max(0, dividedVolume - destSolution.Volume);
|
||||
TryAddSolution(destPuddle.Owner, srcSolution.SplitSolution(takeAmount), false, false, destPuddle);
|
||||
if (stillOverflowing != null && IsOverflowing(destPuddle.Owner, destPuddle))
|
||||
{
|
||||
stillOverflowing.Add(destPuddle.Owner);
|
||||
}
|
||||
solution.RemoveReagent(reagentId, removed);
|
||||
}
|
||||
}
|
||||
|
||||
if (stillOverflowing != null && srcSolution.Volume > sourcePuddleComponent.OverflowVolume)
|
||||
// Tile reactions used up everything.
|
||||
if (solution.Volume == FixedPoint2.Zero)
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get normalized co-ordinate for spill location and spill it in the centre
|
||||
// TODO: Does SnapGrid or something else already do this?
|
||||
var anchored = mapGrid.GetAnchoredEntitiesEnumerator(tileRef.GridIndices);
|
||||
var puddleQuery = GetEntityQuery<PuddleComponent>();
|
||||
var sparklesQuery = GetEntityQuery<EvaporationSparkleComponent>();
|
||||
|
||||
while (anchored.MoveNext(out var ent))
|
||||
{
|
||||
// If there's existing sparkles then delete it
|
||||
if (sparklesQuery.TryGetComponent(ent, out var sparkles))
|
||||
{
|
||||
stillOverflowing.Add(srcPuddle);
|
||||
QueueDel(ent.Value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!puddleQuery.TryGetComponent(ent, out var puddle))
|
||||
continue;
|
||||
|
||||
if (TryAddSolution(ent.Value, solution, sound, puddleComponent: puddle))
|
||||
{
|
||||
EnsureComp<EdgeSpreaderComponent>(ent.Value);
|
||||
}
|
||||
|
||||
puddleUid = ent.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether adding this solution to this puddle would overflow.
|
||||
/// </summary>
|
||||
/// <param name="uid">Uid of owning entity</param>
|
||||
/// <param name="puddle">Puddle to which we are adding solution</param>
|
||||
/// <param name="solution">Solution we intend to add</param>
|
||||
/// <returns></returns>
|
||||
public bool WouldOverflow(EntityUid uid, Solution solution, PuddleComponent? puddle = null)
|
||||
var coords = mapGrid.GridTileToLocal(tileRef.GridIndices);
|
||||
puddleUid = EntityManager.SpawnEntity("Puddle", coords);
|
||||
EnsureComp<PuddleComponent>(puddleUid);
|
||||
if (TryAddSolution(puddleUid, solution, sound))
|
||||
{
|
||||
if (!Resolve(uid, ref puddle))
|
||||
return false;
|
||||
|
||||
return CurrentVolume(uid, puddle) + solution.Volume > puddle.OverflowVolume;
|
||||
EnsureComp<EdgeSpreaderComponent>(puddleUid);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether adding this solution to this puddle would overflow.
|
||||
/// </summary>
|
||||
/// <param name="uid">Uid of owning entity</param>
|
||||
/// <param name="puddle">Puddle ref param</param>
|
||||
/// <returns></returns>
|
||||
private bool IsOverflowing(EntityUid uid, PuddleComponent? puddle = null)
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the relevant puddle entity for a tile.
|
||||
/// </summary>
|
||||
public bool TryGetPuddle(TileRef tile, out EntityUid puddleUid)
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
|
||||
if (!TryComp<MapGridComponent>(tile.GridUid, out var grid))
|
||||
return false;
|
||||
|
||||
var anc = grid.GetAnchoredEntitiesEnumerator(tile.GridIndices);
|
||||
var puddleQuery = GetEntityQuery<PuddleComponent>();
|
||||
|
||||
while (anc.MoveNext(out var ent))
|
||||
{
|
||||
if (!Resolve(uid, ref puddle))
|
||||
return false;
|
||||
if (!puddleQuery.HasComponent(ent.Value))
|
||||
continue;
|
||||
|
||||
return CurrentVolume(uid, puddle) > puddle.OverflowVolume;
|
||||
puddleUid = ent.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SpawnPuddle(EntityUid srcUid, EntityCoordinates pos, PuddleComponent srcPuddleComponent, out EntityUid uid, out PuddleComponent component)
|
||||
{
|
||||
MetaDataComponent? metadata = null;
|
||||
Resolve(srcUid, ref metadata);
|
||||
|
||||
var prototype = metadata?.EntityPrototype?.ID ?? "PuddleSmear"; // TODO Spawn a entity based on another entity
|
||||
|
||||
uid = EntityManager.SpawnEntity(prototype, pos);
|
||||
component = EntityManager.EnsureComponent<PuddleComponent>(uid);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
326
Content.Server/Fluids/EntitySystems/SmokeSystem.cs
Normal file
326
Content.Server/Fluids/EntitySystems/SmokeSystem.cs
Normal file
@@ -0,0 +1,326 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Chemistry.ReactionEffects;
|
||||
using Content.Server.Coordinates.Helpers;
|
||||
using Content.Server.Spreader;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Smoking;
|
||||
using Content.Shared.Spawners;
|
||||
using Content.Shared.Spawners.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles non-atmos solution entities similar to puddles.
|
||||
/// </summary>
|
||||
public sealed class SmokeSystem : EntitySystem
|
||||
{
|
||||
// If I could do it all again this could probably use a lot more of puddles.
|
||||
[Dependency] private readonly IAdminLogManager _logger = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly BloodstreamSystem _blood = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly InternalsSystem _internals = default!;
|
||||
[Dependency] private readonly ReactiveSystem _reactive = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SmokeComponent, EntityUnpausedEvent>(OnSmokeUnpaused);
|
||||
SubscribeLocalEvent<SmokeComponent, MapInitEvent>(OnSmokeMapInit);
|
||||
SubscribeLocalEvent<SmokeComponent, ReactionAttemptEvent>(OnReactionAttempt);
|
||||
SubscribeLocalEvent<SmokeComponent, SpreadNeighborsEvent>(OnSmokeSpread);
|
||||
SubscribeLocalEvent<SmokeDissipateSpawnComponent, TimedDespawnEvent>(OnSmokeDissipate);
|
||||
SubscribeLocalEvent<SpreadGroupUpdateRate>(OnSpreadUpdateRate);
|
||||
}
|
||||
|
||||
private void OnSpreadUpdateRate(ref SpreadGroupUpdateRate ev)
|
||||
{
|
||||
if (ev.Name != "smoke")
|
||||
return;
|
||||
|
||||
ev.UpdatesPerSecond = 8;
|
||||
}
|
||||
|
||||
private void OnSmokeDissipate(EntityUid uid, SmokeDissipateSpawnComponent component, ref TimedDespawnEvent args)
|
||||
{
|
||||
if (!TryComp<TransformComponent>(uid, out var xform))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Spawn(component.Prototype, xform.Coordinates);
|
||||
}
|
||||
|
||||
private void OnSmokeSpread(EntityUid uid, SmokeComponent component, ref SpreadNeighborsEvent args)
|
||||
{
|
||||
if (component.SpreadAmount == 0 ||
|
||||
args.Grid == null ||
|
||||
!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution) ||
|
||||
args.NeighborFreeTiles.Count == 0)
|
||||
{
|
||||
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
var prototype = MetaData(uid).EntityPrototype;
|
||||
|
||||
if (prototype == null)
|
||||
{
|
||||
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
TryComp<TimedDespawnComponent>(uid, out var timer);
|
||||
|
||||
var smokePerSpread = component.SpreadAmount / args.NeighborFreeTiles.Count;
|
||||
component.SpreadAmount -= smokePerSpread;
|
||||
|
||||
foreach (var tile in args.NeighborFreeTiles)
|
||||
{
|
||||
var coords = args.Grid.GridTileToLocal(tile);
|
||||
var ent = Spawn(prototype.ID, coords.SnapToGrid());
|
||||
var neighborSmoke = EnsureComp<SmokeComponent>(ent);
|
||||
neighborSmoke.SpreadAmount = Math.Max(0, smokePerSpread - 1);
|
||||
args.Updates--;
|
||||
|
||||
// Listen this is the old behaviour iunno
|
||||
Start(ent, neighborSmoke, solution.Clone(), timer?.Lifetime ?? 10f);
|
||||
|
||||
if (_appearance.TryGetData(uid, SmokeVisuals.Color, out var color))
|
||||
{
|
||||
_appearance.SetData(ent, SmokeVisuals.Color, color);
|
||||
}
|
||||
|
||||
// Only 1 spread then ig?
|
||||
if (smokePerSpread == 0)
|
||||
{
|
||||
component.SpreadAmount--;
|
||||
|
||||
if (component.SpreadAmount == 0)
|
||||
{
|
||||
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (args.Updates <= 0)
|
||||
break;
|
||||
}
|
||||
|
||||
// Give our spread to neighbor tiles.
|
||||
if (args.NeighborFreeTiles.Count == 0 && args.Neighbors.Count > 0 && component.SpreadAmount > 0)
|
||||
{
|
||||
var smokeQuery = GetEntityQuery<SmokeComponent>();
|
||||
|
||||
foreach (var neighbor in args.Neighbors)
|
||||
{
|
||||
if (!smokeQuery.TryGetComponent(neighbor, out var smoke))
|
||||
continue;
|
||||
|
||||
smoke.SpreadAmount++;
|
||||
args.Updates--;
|
||||
|
||||
if (component.SpreadAmount == 0)
|
||||
{
|
||||
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||
break;
|
||||
}
|
||||
|
||||
if (args.Updates <= 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReactionAttempt(EntityUid uid, SmokeComponent component, ReactionAttemptEvent args)
|
||||
{
|
||||
if (args.Solution.Name != SmokeComponent.SolutionName)
|
||||
return;
|
||||
|
||||
// Prevent smoke/foam fork bombs (smoke creating more smoke).
|
||||
foreach (var effect in args.Reaction.Effects)
|
||||
{
|
||||
if (effect is AreaReactionEffect)
|
||||
{
|
||||
args.Cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSmokeMapInit(EntityUid uid, SmokeComponent component, MapInitEvent args)
|
||||
{
|
||||
component.NextReact = _timing.CurTime;
|
||||
}
|
||||
|
||||
private void OnSmokeUnpaused(EntityUid uid, SmokeComponent component, ref EntityUnpausedEvent args)
|
||||
{
|
||||
component.NextReact += args.PausedTime;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
var query = EntityQueryEnumerator<SmokeComponent>();
|
||||
var curTime = _timing.CurTime;
|
||||
|
||||
while (query.MoveNext(out var uid, out var smoke))
|
||||
{
|
||||
if (smoke.NextReact > curTime)
|
||||
continue;
|
||||
|
||||
smoke.NextReact += TimeSpan.FromSeconds(1.5);
|
||||
|
||||
SmokeReact(uid, 1f, smoke);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the relevant smoke reactions for an entity for the specified exposure duration.
|
||||
/// </summary>
|
||||
public void SmokeReact(EntityUid uid, float frameTime, SmokeComponent? component = null, TransformComponent? xform = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component, ref xform))
|
||||
return;
|
||||
|
||||
if (!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution) ||
|
||||
solution.Contents.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_mapManager.TryGetGrid(xform.GridUid, out var mapGrid))
|
||||
return;
|
||||
|
||||
var tile = mapGrid.GetTileRef(xform.Coordinates.ToVector2i(EntityManager, _mapManager));
|
||||
|
||||
var solutionFraction = 1 / Math.Floor(frameTime);
|
||||
var ents = _lookup.GetEntitiesIntersecting(tile, LookupFlags.Uncontained).ToArray();
|
||||
|
||||
foreach (var reagentQuantity in solution.Contents.ToArray())
|
||||
{
|
||||
if (reagentQuantity.Quantity == FixedPoint2.Zero)
|
||||
continue;
|
||||
|
||||
var reagent = _prototype.Index<ReagentPrototype>(reagentQuantity.ReagentId);
|
||||
|
||||
// React with the tile the effect is on
|
||||
// We don't multiply by solutionFraction here since the tile is only ever reacted once
|
||||
if (!component.ReactedTile)
|
||||
{
|
||||
reagent.ReactionTile(tile, reagentQuantity.Quantity);
|
||||
component.ReactedTile = true;
|
||||
}
|
||||
|
||||
// Touch every entity on tile.
|
||||
foreach (var entity in ents)
|
||||
{
|
||||
if (entity == uid)
|
||||
continue;
|
||||
|
||||
_reactive.ReactionEntity(entity, ReactionMethod.Touch, reagent,
|
||||
reagentQuantity.Quantity * solutionFraction, solution);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entity in ents)
|
||||
{
|
||||
if (entity == uid)
|
||||
continue;
|
||||
|
||||
ReactWithEntity(entity, solution, solutionFraction);
|
||||
}
|
||||
|
||||
UpdateVisuals(uid);
|
||||
}
|
||||
|
||||
private void UpdateVisuals(EntityUid uid)
|
||||
{
|
||||
if (TryComp(uid, out AppearanceComponent? appearance) &&
|
||||
_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution))
|
||||
{
|
||||
var color = solution.GetColor(_prototype);
|
||||
_appearance.SetData(uid, SmokeVisuals.Color, color, appearance);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReactWithEntity(EntityUid entity, Solution solution, double solutionFraction)
|
||||
{
|
||||
if (!TryComp<BloodstreamComponent>(entity, out var bloodstream))
|
||||
return;
|
||||
|
||||
if (TryComp<InternalsComponent>(entity, out var internals) &&
|
||||
_internals.AreInternalsWorking(internals))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cloneSolution = solution.Clone();
|
||||
var transferAmount = FixedPoint2.Min(cloneSolution.Volume * solutionFraction, bloodstream.ChemicalSolution.AvailableVolume);
|
||||
var transferSolution = cloneSolution.SplitSolution(transferAmount);
|
||||
|
||||
foreach (var reagentQuantity in transferSolution.Contents.ToArray())
|
||||
{
|
||||
if (reagentQuantity.Quantity == FixedPoint2.Zero)
|
||||
continue;
|
||||
|
||||
_reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentQuantity.ReagentId, reagentQuantity.Quantity, transferSolution);
|
||||
}
|
||||
|
||||
if (_blood.TryAddToChemicals(entity, transferSolution, bloodstream))
|
||||
{
|
||||
// Log solution addition by smoke
|
||||
_logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} was affected by smoke {SolutionContainerSystem.ToPrettyString(transferSolution)}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up a smoke component for spreading.
|
||||
/// </summary>
|
||||
public void Start(EntityUid uid, SmokeComponent component, Solution solution, float duration)
|
||||
{
|
||||
TryAddSolution(uid, component, solution);
|
||||
EnsureComp<EdgeSpreaderComponent>(uid);
|
||||
var timer = EnsureComp<TimedDespawnComponent>(uid);
|
||||
timer.Lifetime = duration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified solution to the relevant smoke solution.
|
||||
/// </summary>
|
||||
public void TryAddSolution(EntityUid uid, SmokeComponent component, Solution solution)
|
||||
{
|
||||
if (solution.Volume == FixedPoint2.Zero)
|
||||
return;
|
||||
|
||||
if (!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solutionArea))
|
||||
return;
|
||||
|
||||
var addSolution =
|
||||
solution.SplitSolution(FixedPoint2.Min(solution.Volume, solutionArea.AvailableVolume));
|
||||
|
||||
_solutionSystem.TryAddSolution(uid, solutionArea, addSolution);
|
||||
|
||||
UpdateVisuals(uid);
|
||||
}
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Server.Nutrition.Components;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Spillable;
|
||||
using Content.Shared.Weapons.Melee;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class SpillableSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly ReactiveSystem _reactive = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SpillableComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand);
|
||||
SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit);
|
||||
SubscribeLocalEvent<SpillableComponent, GetVerbsEvent<Verb>>(AddSpillVerb);
|
||||
SubscribeLocalEvent<SpillableComponent, GotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<SpillableComponent, SolutionSpikeOverflowEvent>(OnSpikeOverflow);
|
||||
SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter);
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, SpillableComponent component, ExaminedEvent args)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("spill-examine-is-spillable"));
|
||||
|
||||
if (HasComp<MeleeWeaponComponent>(uid))
|
||||
args.PushMarkup(Loc.GetString("spill-examine-spillable-weapon"));
|
||||
}
|
||||
|
||||
private void OnSpikeOverflow(EntityUid uid, SpillableComponent component, SolutionSpikeOverflowEvent args)
|
||||
{
|
||||
if (!args.Handled)
|
||||
{
|
||||
SpillAt(args.Overflow, Transform(uid).Coordinates, "PuddleSmear");
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnGotEquipped(EntityUid uid, SpillableComponent component, GotEquippedEvent args)
|
||||
{
|
||||
if (!component.SpillWorn)
|
||||
return;
|
||||
|
||||
if (!TryComp(uid, out ClothingComponent? clothing))
|
||||
return;
|
||||
|
||||
// check if entity was actually used as clothing
|
||||
// not just taken in pockets or something
|
||||
var isCorrectSlot = clothing.Slots.HasFlag(args.SlotFlags);
|
||||
if (!isCorrectSlot) return;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
|
||||
return;
|
||||
if (solution.Volume == 0)
|
||||
return;
|
||||
|
||||
// spill all solution on the player
|
||||
var drainedSolution = _solutionContainerSystem.Drain(uid, solution, solution.Volume);
|
||||
SpillAt(args.Equipee, drainedSolution, "PuddleSmear");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spills the specified solution at the entity's location if possible.
|
||||
/// </summary>
|
||||
/// <param name="uid">
|
||||
/// The entity to use as a location to spill the solution at.
|
||||
/// </param>
|
||||
/// <param name="solution">Initial solution for the prototype.</param>
|
||||
/// <param name="prototype">The prototype to use.</param>
|
||||
/// <param name="sound">Play the spill sound.</param>
|
||||
/// <param name="combine">Whether to attempt to merge with existing puddles</param>
|
||||
/// <param name="transformComponent">Optional Transform component</param>
|
||||
/// <returns>The puddle if one was created, null otherwise.</returns>
|
||||
public PuddleComponent? SpillAt(EntityUid uid, Solution solution, string prototype,
|
||||
bool sound = true, bool combine = true, TransformComponent? transformComponent = null)
|
||||
{
|
||||
return !Resolve(uid, ref transformComponent, false)
|
||||
? null
|
||||
: SpillAt(solution, transformComponent.Coordinates, prototype, sound: sound, combine: combine);
|
||||
}
|
||||
|
||||
private void SpillOnLand(EntityUid uid, SpillableComponent component, ref LandEvent args)
|
||||
{
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution)) return;
|
||||
|
||||
if (TryComp<DrinkComponent>(uid, out var drink) && (!drink.Opened))
|
||||
return;
|
||||
|
||||
if (args.User != null)
|
||||
{
|
||||
_adminLogger.Add(LogType.Landed,
|
||||
$"{ToPrettyString(args.User.Value):user} threw {ToPrettyString(uid):entity} which spilled a solution {SolutionContainerSystem.ToPrettyString(solution):solution} on landing");
|
||||
}
|
||||
|
||||
var drainedSolution = _solutionContainerSystem.Drain(uid, solution, solution.Volume);
|
||||
SplashSpillAt(uid, drainedSolution, Transform(uid).Coordinates, "PuddleSmear");
|
||||
}
|
||||
|
||||
private void SplashOnMeleeHit(EntityUid uid, SpillableComponent component, MeleeHitEvent args)
|
||||
{
|
||||
// When attacking someone reactive with a spillable entity,
|
||||
// splash a little on them (touch react)
|
||||
// If this also has solution transfer, then assume the transfer amount is how much we want to spill.
|
||||
// Otherwise let's say they want to spill a quarter of its max volume.
|
||||
|
||||
if (!_solutionContainerSystem.TryGetDrainableSolution(uid, out var solution))
|
||||
return;
|
||||
|
||||
if (TryComp<DrinkComponent>(uid, out var drink) && !drink.Opened)
|
||||
return;
|
||||
|
||||
var hitCount = args.HitEntities.Count;
|
||||
|
||||
var totalSplit = FixedPoint2.Min(solution.MaxVolume * 0.25, solution.Volume);
|
||||
if (TryComp<SolutionTransferComponent>(uid, out var transfer))
|
||||
{
|
||||
totalSplit = FixedPoint2.Min(transfer.TransferAmount, solution.Volume);
|
||||
}
|
||||
|
||||
// a little lame, but reagent quantity is not very balanced and we don't want people
|
||||
// spilling like 100u of reagent on someone at once!
|
||||
totalSplit = FixedPoint2.Min(totalSplit, component.MaxMeleeSpillAmount);
|
||||
|
||||
foreach (var hit in args.HitEntities)
|
||||
{
|
||||
if (!HasComp<ReactiveComponent>(hit))
|
||||
{
|
||||
hitCount -= 1; // so we don't undershoot solution calculation for actual reactive entities
|
||||
continue;
|
||||
}
|
||||
|
||||
var splitSolution = _solutionContainerSystem.SplitSolution(uid, solution, totalSplit / hitCount);
|
||||
|
||||
_adminLogger.Add(LogType.MeleeHit, $"{ToPrettyString(args.User)} splashed {SolutionContainerSystem.ToPrettyString(splitSolution):solution} from {ToPrettyString(uid):entity} onto {ToPrettyString(hit):target}");
|
||||
_reactive.DoEntityReaction(hit, splitSolution, ReactionMethod.Touch);
|
||||
|
||||
_popup.PopupEntity(
|
||||
Loc.GetString("spill-melee-hit-attacker", ("amount", totalSplit / hitCount), ("spillable", uid),
|
||||
("target", Identity.Entity(hit, EntityManager))),
|
||||
hit, args.User);
|
||||
|
||||
_popup.PopupEntity(
|
||||
Loc.GetString("spill-melee-hit-others", ("attacker", args.User), ("spillable", uid),
|
||||
("target", Identity.Entity(hit, EntityManager))),
|
||||
hit, Filter.PvsExcept(args.User), true, PopupType.SmallCaution);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSpillVerb(EntityUid uid, SpillableComponent component, GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract)
|
||||
return;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetDrainableSolution(args.Target, out var solution))
|
||||
return;
|
||||
|
||||
if (TryComp<DrinkComponent>(args.Target, out var drink) && (!drink.Opened))
|
||||
return;
|
||||
|
||||
if (solution.Volume == FixedPoint2.Zero)
|
||||
return;
|
||||
|
||||
Verb verb = new();
|
||||
verb.Text = Loc.GetString("spill-target-verb-get-data-text");
|
||||
// TODO VERB ICONS spill icon? pouring out a glass/beaker?
|
||||
|
||||
verb.Act = () =>
|
||||
{
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(args.User, component.SpillDelay ?? 0, new SpillDoAfterEvent(), uid, target: uid)
|
||||
{
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
NeedHand = true,
|
||||
});
|
||||
};
|
||||
verb.Impact = LogImpact.Medium; // dangerous reagent reaction are logged separately.
|
||||
verb.DoContactInteraction = true;
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// First splashes reagent on reactive entities near the spilling entity, then spills the rest regularly to a
|
||||
/// puddle. This is intended for 'destructive' spills, like when entities are destroyed or thrown.
|
||||
/// </summary>
|
||||
public PuddleComponent? SplashSpillAt(EntityUid uid, Solution solution, EntityCoordinates coordinates, string prototype,
|
||||
bool overflow = true, bool sound = true, bool combine = true, EntityUid? user=null)
|
||||
{
|
||||
if (solution.Volume == 0)
|
||||
return null;
|
||||
|
||||
// Get reactive entities nearby--if there are some, it'll spill a bit on them instead.
|
||||
foreach (var ent in _entityLookup.GetComponentsInRange<ReactiveComponent>(coordinates, 1.0f))
|
||||
{
|
||||
// sorry! no overload for returning uid, so .owner must be used
|
||||
var owner = ent.Owner;
|
||||
|
||||
// between 5 and 30%
|
||||
var splitAmount = solution.Volume * _random.NextFloat(0.05f, 0.30f);
|
||||
var splitSolution = solution.SplitSolution(splitAmount);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
_adminLogger.Add(LogType.Landed,
|
||||
$"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}");
|
||||
}
|
||||
|
||||
_reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
|
||||
_popup.PopupEntity(Loc.GetString("spill-land-spilled-on-other", ("spillable", uid), ("target", owner)), owner, PopupType.SmallCaution);
|
||||
}
|
||||
|
||||
return SpillAt(solution, coordinates, prototype, overflow, sound, combine: combine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spills solution at the specified grid coordinates.
|
||||
/// </summary>
|
||||
/// <param name="solution">Initial solution for the prototype.</param>
|
||||
/// <param name="coordinates">The coordinates to spill the solution at.</param>
|
||||
/// <param name="prototype">The prototype to use.</param>
|
||||
/// <param name="overflow">If the puddle overflow will be calculated. Defaults to true.</param>
|
||||
/// <param name="sound">Whether or not to play the spill sound.</param>
|
||||
/// <param name="combine">Whether to attempt to merge with existing puddles</param>
|
||||
/// <returns>The puddle if one was created, null otherwise.</returns>
|
||||
public PuddleComponent? SpillAt(Solution solution, EntityCoordinates coordinates, string prototype,
|
||||
bool overflow = true, bool sound = true, bool combine = true)
|
||||
{
|
||||
if (solution.Volume == 0) return null;
|
||||
|
||||
if (!_mapManager.TryGetGrid(coordinates.GetGridUid(EntityManager), out var mapGrid))
|
||||
return null; // Let's not spill to space.
|
||||
|
||||
return SpillAt(mapGrid.GetTileRef(coordinates), solution, prototype, overflow, sound,
|
||||
combine: combine);
|
||||
}
|
||||
|
||||
public bool TryGetPuddle(TileRef tileRef, [NotNullWhen(true)] out PuddleComponent? puddle)
|
||||
{
|
||||
foreach (var entity in _entityLookup.GetEntitiesIntersecting(tileRef))
|
||||
{
|
||||
if (EntityManager.TryGetComponent(entity, out PuddleComponent? p))
|
||||
{
|
||||
puddle = p;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
puddle = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public PuddleComponent? SpillAt(TileRef tileRef, Solution solution, string prototype,
|
||||
bool overflow = true, bool sound = true, bool noTileReact = false, bool combine = true)
|
||||
{
|
||||
if (solution.Volume <= 0) return null;
|
||||
|
||||
// If space return early, let that spill go out into the void
|
||||
if (tileRef.Tile.IsEmpty) return null;
|
||||
|
||||
var gridId = tileRef.GridUid;
|
||||
if (!_mapManager.TryGetGrid(gridId, out var mapGrid)) return null; // Let's not spill to invalid grids.
|
||||
|
||||
if (!noTileReact)
|
||||
{
|
||||
// First, do all tile reactions
|
||||
for (var i = 0; i < solution.Contents.Count; i++)
|
||||
{
|
||||
var (reagentId, quantity) = solution.Contents[i];
|
||||
var proto = _prototypeManager.Index<ReagentPrototype>(reagentId);
|
||||
var removed = proto.ReactionTile(tileRef, quantity);
|
||||
if (removed <= FixedPoint2.Zero) continue;
|
||||
solution.RemoveReagent(reagentId, removed);
|
||||
}
|
||||
}
|
||||
|
||||
// Tile reactions used up everything.
|
||||
if (solution.Volume == FixedPoint2.Zero)
|
||||
return null;
|
||||
|
||||
// Get normalized co-ordinate for spill location and spill it in the centre
|
||||
// TODO: Does SnapGrid or something else already do this?
|
||||
var spillGridCoords = mapGrid.GridTileToLocal(tileRef.GridIndices);
|
||||
var startEntity = EntityUid.Invalid;
|
||||
PuddleComponent? puddleComponent = null;
|
||||
|
||||
if (combine)
|
||||
{
|
||||
var spillEntities = _entityLookup.GetEntitiesIntersecting(tileRef).ToArray();
|
||||
|
||||
foreach (var spillEntity in spillEntities)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(spillEntity, out puddleComponent)) continue;
|
||||
|
||||
if (!overflow && _puddleSystem.WouldOverflow(puddleComponent.Owner, solution, puddleComponent))
|
||||
return null;
|
||||
|
||||
if (!_puddleSystem.TryAddSolution(puddleComponent.Owner, solution, sound, overflow)) continue;
|
||||
|
||||
startEntity = puddleComponent.Owner;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startEntity != EntityUid.Invalid)
|
||||
return puddleComponent;
|
||||
|
||||
startEntity = EntityManager.SpawnEntity(prototype, spillGridCoords);
|
||||
puddleComponent = EntityManager.EnsureComponent<PuddleComponent>(startEntity);
|
||||
_puddleSystem.TryAddSolution(startEntity, solution, sound, overflow);
|
||||
|
||||
return puddleComponent;
|
||||
}
|
||||
|
||||
private void OnDoAfter(EntityUid uid, SpillableComponent component, DoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || args.Args.Target == null)
|
||||
return;
|
||||
|
||||
//solution gone by other means before doafter completes
|
||||
if (!_solutionContainerSystem.TryGetDrainableSolution(uid, out var solution) || solution.Volume == 0)
|
||||
return;
|
||||
|
||||
var puddleSolution = _solutionContainerSystem.SplitSolution(uid, solution, solution.Volume);
|
||||
|
||||
SpillAt(puddleSolution, Transform(uid).Coordinates, "PuddleSmear");
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user