<DefaultWindow xmlns="https://spacestation14.io"
MinSize="270 420"
- SetSize="315 420" Title="{Loc 'gas-analyzer-window-name'}">
+ SetSize="360 420" Title="{Loc 'gas-analyzer-window-name'}">
<BoxContainer Orientation="Vertical" Margin="5 5 5 5">
<BoxContainer Name="CTopBox" Orientation="Horizontal"/>
<!-- Gas Mix Data, Populated by function -->
+using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos;
using Content.Shared.Temperature;
using Robust.Client.Graphics;
// This is the gas bar thingy
var height = 30;
var minSize = 24; // This basically allows gases which are too small, to be shown properly
- var gasBar = new BoxContainer
+ var gasBar = new SplitBar()
{
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- HorizontalExpand = true,
- MinSize = new Vector2(0, height)
+ MinHeight = height
};
// Separator
dataContainer.AddChild(new Control
});
// Add to the gas bar //TODO: highlight the currently hover one
- var left = (j == 0) ? 0f : 2f;
- var right = (j == gasMix.Gases.Length - 1) ? 0f : 2f;
- gasBar.AddChild(new PanelContainer
- {
- ToolTip = Loc.GetString("gas-analyzer-window-molarity-percentage-text",
- ("gasName", gas.Name),
- ("amount", $"{gas.Amount:0.##}"),
- ("percentage", $"{(gas.Amount / totalGasAmount * 100):0.#}")),
- HorizontalExpand = true,
- SizeFlagsStretchRatio = gas.Amount,
- MouseFilter = MouseFilterMode.Stop,
- PanelOverride = new StyleBoxFlat
- {
- BackgroundColor = color,
- PaddingLeft = left,
- PaddingRight = right
- },
- MinSize = new Vector2(minSize, 0)
- });
+ gasBar.AddEntry(gas.Amount, color, tooltip: Loc.GetString("gas-analyzer-window-molarity-percentage-text",
+ ("gasName", gas.Name),
+ ("amount", $"{gas.Amount:0.##}"),
+ ("percentage", $"{(gas.Amount / totalGasAmount * 100):0.#}")));
}
dataContainer.AddChild(gasBar);
-using Content.Shared.Foam;
+using Content.Shared.Smoking;
+using Content.Shared.Spawners.Components;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
+using Robust.Shared.Network;
+using Robust.Shared.Timing;
namespace Content.Client.Chemistry.Visualizers;
/// </summary>
public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent>
{
+ [Dependency] private readonly IGameTiming _timing = default!;
+
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FoamVisualsComponent, ComponentInit>(OnComponentInit);
}
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ var query = EntityQueryEnumerator<FoamVisualsComponent, TimedDespawnComponent>();
+
+ while (query.MoveNext(out var uid, out var comp, out var despawn))
+ {
+ if (despawn.Lifetime > 1f)
+ continue;
+
+ // Despawn animation.
+ if (TryComp(uid, out AnimationPlayerComponent? animPlayer)
+ && !AnimationSystem.HasRunningAnimation(uid, animPlayer, FoamVisualsComponent.AnimationKey))
+ {
+ AnimationSystem.Play(uid, animPlayer, comp.Animation, FoamVisualsComponent.AnimationKey);
+ }
+ }
+ }
+
/// <summary>
/// Generates the animation used by foam visuals when the foam dissolves.
/// </summary>
private void OnComponentInit(EntityUid uid, FoamVisualsComponent comp, ComponentInit args)
{
- comp.Animation = new Animation()
+ comp.Animation = new Animation
{
Length = TimeSpan.FromSeconds(comp.AnimationTime),
AnimationTracks =
{
- new AnimationTrackSpriteFlick()
+ new AnimationTrackSpriteFlick
{
LayerKey = FoamVisualLayers.Base,
KeyFrames =
}
};
}
-
- /// <summary>
- /// Plays the animation used by foam visuals when the foam dissolves.
- /// </summary>
- protected override void OnAppearanceChange(EntityUid uid, FoamVisualsComponent comp, ref AppearanceChangeEvent args)
- {
- if (AppearanceSystem.TryGetData<bool>(uid, FoamVisuals.State, out var state, args.Component) && state)
- {
- if (TryComp(uid, out AnimationPlayerComponent? animPlayer)
- && !AnimationSystem.HasRunningAnimation(uid, animPlayer, FoamVisualsComponent.AnimationKey))
- AnimationSystem.Play(uid, animPlayer, comp.Animation, FoamVisualsComponent.AnimationKey);
- }
-
- if (AppearanceSystem.TryGetData<Color>(uid, FoamVisuals.Color, out var color, args.Component))
- {
- if (args.Sprite != null)
- args.Sprite.Color = color;
- }
- }
}
public enum FoamVisualLayers : byte
namespace Content.Client.Fluids;
-public sealed class MoppingSystem : SharedMoppingSystem
+/// <inheritdoc/>
+public sealed class AbsorbentSystem : SharedAbsorbentSystem
{
public override void Initialize()
{
--- /dev/null
+using Content.Client.IconSmoothing;
+using Content.Shared.Fluids;
+using Content.Shared.Fluids.Components;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Fluids;
+
+public sealed class PuddleSystem : SharedPuddleSystem
+{
+ [Dependency] private readonly IconSmoothSystem _smooth = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<PuddleComponent, AppearanceChangeEvent>(OnPuddleAppearance);
+ }
+
+ private void OnPuddleAppearance(EntityUid uid, PuddleComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null)
+ return;
+
+ float volume = 1f;
+
+ if (args.AppearanceData.TryGetValue(PuddleVisuals.CurrentVolume, out var volumeObj))
+ {
+ volume = (float) volumeObj;
+ }
+
+ // Update smoothing and sprite based on volume.
+ if (TryComp<IconSmoothComponent>(uid, out var smooth))
+ {
+ if (volume < LowThreshold)
+ {
+ args.Sprite.LayerSetState(0, $"{smooth.StateBase}a");
+ _smooth.SetEnabled(uid, false, smooth);
+ }
+ else if (volume < 0.6f)
+ {
+ args.Sprite.LayerSetState(0, $"{smooth.StateBase}b");
+ _smooth.SetEnabled(uid, false, smooth);
+ }
+ else
+ {
+ if (!smooth.Enabled)
+ {
+ args.Sprite.LayerSetState(0, $"{smooth.StateBase}0");
+ _smooth.SetEnabled(uid, true, smooth);
+ _smooth.DirtyNeighbours(uid);
+ }
+ }
+ }
+
+ var baseColor = Color.White;
+
+ if (args.AppearanceData.TryGetValue(PuddleVisuals.SolutionColor, out var colorObj))
+ {
+ var color = (Color) colorObj;
+ args.Sprite.Color = color * baseColor;
+ }
+ else
+ {
+ args.Sprite.Color *= baseColor;
+ }
+ }
+}
+++ /dev/null
-using Content.Shared.FixedPoint;
-using Robust.Client.Graphics;
-
-namespace Content.Client.Fluids
-{
- [RegisterComponent]
- public sealed class PuddleVisualizerComponent : Component
- {
- // Whether the underlying solution color should be used. True in most cases.
- [DataField("recolor")] public bool Recolor = true;
-
- // Whether the puddle has a unique sprite we don't want to overwrite
- [DataField("customPuddleSprite")] public bool CustomPuddleSprite;
-
- // Puddles may change which RSI they use for their sprites (e.g. wet floor effects). This field will store the original RSI they used.
- [DataField("originalRsi")] public RSI? OriginalRsi;
-
- /// <summary>
- /// Puddles with volume below this threshold are able to have their sprite changed to a wet floor effect, though this is not the only factor.
- /// </summary>
- [DataField("wetFloorEffectThreshold")]
- public FixedPoint2 WetFloorEffectThreshold = FixedPoint2.New(5);
-
- /// <summary>
- /// Alpha (opacity) of the wet floor sparkle effect. Higher alpha = more opaque/visible.
- /// </summary>
- [DataField("wetFloorEffectAlpha")]
- public float WetFloorEffectAlpha = 0.75f; //should be somewhat transparent by default.
- }
-}
+++ /dev/null
-using System.Linq;
-using Content.Shared.Fluids;
-using Content.Shared.FixedPoint;
-using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Shared.Random;
-
-namespace Content.Client.Fluids
-{
- [UsedImplicitly]
- public sealed class PuddleVisualizerSystem : VisualizerSystem<PuddleVisualizerComponent>
- {
- [Dependency] private readonly IRobustRandom _random = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent<PuddleVisualizerComponent, ComponentInit>(OnComponentInit);
- }
-
- private void OnComponentInit(EntityUid uid, PuddleVisualizerComponent puddleVisuals, ComponentInit args)
- {
- if (!TryComp(uid, out SpriteComponent? sprite))
- {
- return;
- }
-
- puddleVisuals.OriginalRsi = sprite.BaseRSI; //Back up the original RSI upon initialization
- RandomizeState(sprite, puddleVisuals.OriginalRsi);
- RandomizeRotation(sprite);
- }
-
- protected override void OnAppearanceChange(EntityUid uid, PuddleVisualizerComponent component, ref AppearanceChangeEvent args)
- {
- if (args.Sprite == null)
- {
- Logger.Warning($"Missing SpriteComponent for PuddleVisualizerSystem on entityUid = {uid}");
- return;
- }
-
- if (!AppearanceSystem.TryGetData<float>(uid, PuddleVisuals.VolumeScale, out var volumeScale)
- || !AppearanceSystem.TryGetData<FixedPoint2>(uid, PuddleVisuals.CurrentVolume, out var currentVolume)
- || !AppearanceSystem.TryGetData<Color>(uid, PuddleVisuals.SolutionColor, out var solutionColor)
- || !AppearanceSystem.TryGetData<bool>(uid, PuddleVisuals.IsEvaporatingVisual, out var isEvaporating))
- {
- return;
- }
-
- // volumeScale is our opacity based on level of fullness to overflow. The lower bound is hard-capped for visibility reasons.
- var cappedScale = Math.Min(1.0f, volumeScale * 0.75f + 0.25f);
-
- var newColor = component.Recolor ? solutionColor.WithAlpha(cappedScale) : args.Sprite.Color.WithAlpha(cappedScale);
-
- args.Sprite.LayerSetColor(0, newColor);
-
- // Don't consider wet floor effects if we're using a custom sprite.
- if (component.CustomPuddleSprite)
- return;
-
- if (isEvaporating && currentVolume <= component.WetFloorEffectThreshold)
- {
- // If we need the effect but don't already have it - start it
- if (args.Sprite.LayerGetState(0) != "sparkles")
- StartWetFloorEffect(args.Sprite, component.WetFloorEffectAlpha);
- }
- else
- {
- // If we have the effect but don't need it - end it
- if (args.Sprite.LayerGetState(0) == "sparkles")
- EndWetFloorEffect(args.Sprite, component.OriginalRsi);
- }
- }
-
- private void StartWetFloorEffect(SpriteComponent sprite, float alpha)
- {
- sprite.LayerSetState(0, "sparkles", "Fluids/wet_floor_sparkles.rsi");
- sprite.Color = sprite.Color.WithAlpha(alpha);
- sprite.LayerSetAutoAnimated(0, false);
- sprite.LayerSetAutoAnimated(0, true); //fixes a bug where the sparkle effect would sometimes freeze on a single frame.
- }
-
- private void EndWetFloorEffect(SpriteComponent sprite, RSI? originalRSI)
- {
- RandomizeState(sprite, originalRSI);
- sprite.LayerSetAutoAnimated(0, false);
- }
-
- private void RandomizeState(SpriteComponent sprite, RSI? rsi)
- {
- var maxStates = rsi?.ToArray();
- if (maxStates is not { Length: > 0 }) return;
-
- var selectedState = _random.Next(0, maxStates.Length - 1); //randomly select an index for which RSI state to use.
- sprite.LayerSetState(0, maxStates[selectedState].StateId, rsi); // sets the sprite's state via our randomly selected index.
- }
-
- private void RandomizeRotation(SpriteComponent sprite)
- {
- float rotationDegrees = _random.Next(0, 359); // randomly select a rotation for our puddle sprite.
- sprite.Rotation = Angle.FromDegrees(rotationDegrees); // sets the sprite's rotation to the one we randomly selected.
- }
- }
-}
-<Control xmlns="https://spacestation14.io">
- <BoxContainer Orientation="Horizontal">
- <ProgressBar
- HorizontalExpand="True"
- Name="PercentBar"
- MinSize="20 20"
- VerticalAlignment="Center"
- Margin="2 8 4 2"
- MaxValue="1.0"
- MinValue="0.0">
- </ProgressBar>
- </BoxContainer>
-</Control>
+<controls:SplitBar xmlns="https://spacestation14.io"
+ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ Name="Bar">
+</controls:SplitBar>
-using Content.Shared.Fluids;
+using System.Linq;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Fluids;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Client.Fluids.UI
{
[GenerateTypedNameReferences]
- public sealed partial class AbsorbentItemStatus : Control
+ public sealed partial class AbsorbentItemStatus : SplitBar
{
private readonly IEntityManager _entManager;
private readonly EntityUid _uid;
+ private Dictionary<Color, float> _progress = new();
public AbsorbentItemStatus(EntityUid uid, IEntityManager entManager)
{
if (!_entManager.TryGetComponent<AbsorbentComponent>(_uid, out var absorbent))
return;
- PercentBar.Value = absorbent.Progress;
+ var oldProgress = _progress.ShallowClone();
+ _progress.Clear();
+
+ foreach (var item in absorbent.Progress)
+ {
+ _progress[item.Key] = item.Value;
+ }
+
+ if (oldProgress.OrderBy(x => x.Key.ToArgb()).SequenceEqual(_progress))
+ return;
+
+ Bar.Clear();
+
+ foreach (var (key, value) in absorbent.Progress)
+ {
+ Bar.AddEntry(value, key);
+ }
}
}
}
[RegisterComponent]
public sealed class IconSmoothComponent : Component
{
+ [ViewVariables(VVAccess.ReadWrite), DataField("enabled")]
+ public bool Enabled = true;
+
public (EntityUid?, Vector2i)? LastPosition;
/// <summary>
using Robust.Client.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
+using Robust.Shared.Map.Enumerators;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Content.Client.IconSmoothing
private int _generation;
+ public void SetEnabled(EntityUid uid, bool value, IconSmoothComponent? component = null)
+ {
+ if (!Resolve(uid, ref component, false) || value == component.Enabled)
+ return;
+
+ component.Enabled = value;
+ DirtyNeighbours(uid, component);
+ }
+
public override void Initialize()
{
base.Initialize();
private void OnShutdown(EntityUid uid, IconSmoothComponent component, ComponentShutdown args)
{
+ _dirtyEntities.Enqueue(uid);
DirtyNeighbours(uid, component);
}
}
// Yes, we updates ALL smoothing entities surrounding us even if they would never smooth with us.
- DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(1, 0)));
- DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(-1, 0)));
- DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(0, 1)));
- DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(0, -1)));
+ DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 0)));
+ DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 0)));
+ DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, 1)));
+ DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, -1)));
if (comp.Mode is IconSmoothingMode.Corners or IconSmoothingMode.NoSprite or IconSmoothingMode.Diagonal)
{
- DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(1, 1)));
- DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(-1, -1)));
- DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(-1, 1)));
- DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(1, -1)));
+ DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 1)));
+ DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, -1)));
+ DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 1)));
+ DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, -1)));
}
}
- private void DirtyEntities(IEnumerable<EntityUid> entities)
+ private void DirtyEntities(AnchoredEntitiesEnumerator entities)
{
// Instead of doing HasComp -> Enqueue -> TryGetComp, we will just enqueue all entities. Generally when
// dealing with walls neighboring anchored entities will also be walls, and in those instances that will
// require one less component fetch/check.
- foreach (var entity in entities)
+ while (entities.MoveNext(out var entity))
{
- _dirtyEntities.Enqueue(entity);
+ _dirtyEntities.Enqueue(entity.Value);
}
}
// Generation on the component is set after an update so we can cull updates that happened this generation.
if (!smoothQuery.Resolve(uid, ref smooth, false)
|| smooth.Mode == IconSmoothingMode.NoSprite
- || smooth.UpdateGeneration == _generation)
+ || smooth.UpdateGeneration == _generation ||
+ !smooth.Enabled)
{
- if (smooth != null &&
+ if (smooth is { Enabled: true } &&
TryComp<SmoothEdgeComponent>(uid, out var edge) &&
xformQuery.TryGetComponent(uid, out xform))
{
{
var pos = grid.TileIndicesFor(xform.Coordinates);
- if (MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.North)), smoothQuery))
+ if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.North)), smoothQuery))
directions |= DirectionFlag.North;
- if (MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.South)), smoothQuery))
+ if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.South)), smoothQuery))
directions |= DirectionFlag.South;
- if (MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.East)), smoothQuery))
+ if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.East)), smoothQuery))
directions |= DirectionFlag.East;
- if (MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.West)), smoothQuery))
+ if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.West)), smoothQuery))
directions |= DirectionFlag.West;
}
if (!spriteQuery.TryGetComponent(uid, out var sprite))
{
Logger.Error($"Encountered a icon-smoothing entity without a sprite: {ToPrettyString(uid)}");
- RemComp(uid, smooth);
+ RemCompDeferred(uid, smooth);
return;
}
for (var i = 0; i < neighbors.Length; i++)
{
var neighbor = (Vector2i) rotation.RotateVec(neighbors[i]);
- matching = matching && MatchingEntity(smooth, grid.GetAnchoredEntities(pos + neighbor), smoothQuery);
+ matching = matching && MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos + neighbor), smoothQuery);
}
if (matching)
}
var pos = grid.TileIndicesFor(xform.Coordinates);
- if (MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.North)), smoothQuery))
+ if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.North)), smoothQuery))
dirs |= CardinalConnectDirs.North;
- if (MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.South)), smoothQuery))
+ if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.South)), smoothQuery))
dirs |= CardinalConnectDirs.South;
- if (MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.East)), smoothQuery))
+ if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.East)), smoothQuery))
dirs |= CardinalConnectDirs.East;
- if (MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.West)), smoothQuery))
+ if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.West)), smoothQuery))
dirs |= CardinalConnectDirs.West;
sprite.LayerSetState(0, $"{smooth.StateBase}{(int) dirs}");
CalculateEdge(sprite.Owner, directions, sprite);
}
- private bool MatchingEntity(IconSmoothComponent smooth, IEnumerable<EntityUid> candidates, EntityQuery<IconSmoothComponent> smoothQuery)
+ private bool MatchingEntity(IconSmoothComponent smooth, AnchoredEntitiesEnumerator candidates, EntityQuery<IconSmoothComponent> smoothQuery)
{
- foreach (var entity in candidates)
+ while (candidates.MoveNext(out var entity))
{
- if (smoothQuery.TryGetComponent(entity, out var other) && other.SmoothKey == smooth.SmoothKey)
+ if (smoothQuery.TryGetComponent(entity, out var other) &&
+ other.SmoothKey == smooth.SmoothKey &&
+ other.Enabled)
+ {
return true;
+ }
}
return false;
private (CornerFill ne, CornerFill nw, CornerFill sw, CornerFill se) CalculateCornerFill(MapGridComponent grid, IconSmoothComponent smooth, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
{
var pos = grid.TileIndicesFor(xform.Coordinates);
- var n = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.North)), smoothQuery);
- var ne = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.NorthEast)), smoothQuery);
- var e = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.East)), smoothQuery);
- var se = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.SouthEast)), smoothQuery);
- var s = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.South)), smoothQuery);
- var sw = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.SouthWest)), smoothQuery);
- var w = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.West)), smoothQuery);
- var nw = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.NorthWest)), smoothQuery);
+ var n = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.North)), smoothQuery);
+ var ne = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.NorthEast)), smoothQuery);
+ var e = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.East)), smoothQuery);
+ var se = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.SouthEast)), smoothQuery);
+ var s = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.South)), smoothQuery);
+ var sw = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.SouthWest)), smoothQuery);
+ var w = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.West)), smoothQuery);
+ var nw = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.NorthWest)), smoothQuery);
// ReSharper disable InconsistentNaming
var cornerNE = CornerFill.None;
--- /dev/null
+<controls:SplitBar xmlns="https://spacestation14.io"
+ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ MouseFilter="Stop"
+ Orientation="Horizontal"
+ HorizontalExpand="True"
+ MinSize="0 30">
+</controls:SplitBar>
--- /dev/null
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.UserInterface.Controls
+{
+ [GenerateTypedNameReferences]
+ public partial class SplitBar : BoxContainer
+ {
+ public SplitBar()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void Clear()
+ {
+ DisposeAllChildren();
+ }
+
+ public void AddEntry(float amount, Color color, string? tooltip = null)
+ {
+ AddChild(new PanelContainer
+ {
+ ToolTip = tooltip,
+ HorizontalExpand = true,
+ SizeFlagsStretchRatio = amount,
+ MouseFilter = MouseFilterMode.Stop,
+ PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = color,
+ PaddingLeft = 2f,
+ PaddingRight = 2f,
+ },
+ MinSize = new Vector2(24, 0)
+ });
+ }
+ }
+}
/// <summary>
/// Does all of the melee effects for a player that are predicted, i.e. character lunge and weapon animation.
/// </summary>
- public override void DoLunge(EntityUid user, Angle angle, Vector2 localPos, string? animation)
+ public override void DoLunge(EntityUid user, Angle angle, Vector2 localPos, string? animation, bool predicted = true)
{
if (!Timing.IsFirstTimePredicted)
return;
using System.Threading.Tasks;
using Content.Server.Fluids.Components;
using Content.Server.Fluids.EntitySystems;
+using Content.Server.Spreader;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
+using Content.Shared.Fluids.Components;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Fluids;
[TestFixture]
-[TestOf(typeof(FluidSpreaderSystem))]
+[TestOf(typeof(SpreaderSystem))]
public sealed class FluidSpill
{
private static PuddleComponent? GetPuddle(IEntityManager entityManager, MapGridComponent mapGrid, Vector2i pos)
return null;
}
- private readonly Direction[] _dirs =
- {
- Direction.East,
- Direction.South,
- Direction.West,
- Direction.North,
- };
-
-
- private readonly Vector2i _origin = new(1, 1);
-
- [Test]
- public async Task SpillEvenlyTest()
- {
- await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
- var server = pairTracker.Pair.Server;
- var mapManager = server.ResolveDependency<IMapManager>();
- var entityManager = server.ResolveDependency<IEntityManager>();
- var spillSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SpillableSystem>();
- var gameTiming = server.ResolveDependency<IGameTiming>();
- var puddleSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<PuddleSystem>();
- MapId mapId;
- EntityUid gridId = default;
-
- await server.WaitPost(() =>
- {
- mapId = mapManager.CreateMap();
- var grid = mapManager.CreateGrid(mapId);
- gridId = grid.Owner;
-
- for (var x = 0; x < 3; x++)
- {
- for (var y = 0; y < 3; y++)
- {
- grid.SetTile(new Vector2i(x, y), new Tile(1));
- }
- }
- });
-
- await server.WaitAssertion(() =>
- {
- var grid = mapManager.GetGrid(gridId);
- var solution = new Solution("Water", FixedPoint2.New(100));
- var tileRef = grid.GetTileRef(_origin);
- var puddle = spillSystem.SpillAt(tileRef, solution, "PuddleSmear");
- Assert.That(puddle, Is.Not.Null);
- Assert.That(GetPuddle(entityManager, grid, _origin), Is.Not.Null);
- });
-
- var sTimeToWait = (int) Math.Ceiling(2f * gameTiming.TickRate);
- await server.WaitRunTicks(sTimeToWait);
-
- await server.WaitAssertion(() =>
- {
- var grid = mapManager.GetGrid(gridId);
- var puddle = GetPuddle(entityManager, grid, _origin);
-
- Assert.That(puddle, Is.Not.Null);
- Assert.That(puddleSystem.CurrentVolume(puddle!.Owner, puddle), Is.EqualTo(FixedPoint2.New(20)));
-
- foreach (var direction in _dirs)
- {
- var newPos = _origin.Offset(direction);
- var sidePuddle = GetPuddle(entityManager, grid, newPos);
- Assert.That(sidePuddle, Is.Not.Null);
- Assert.That(puddleSystem.CurrentVolume(sidePuddle!.Owner, sidePuddle), Is.EqualTo(FixedPoint2.New(20)));
- }
- });
-
- await pairTracker.CleanReturnAsync();
- }
-
[Test]
public async Task SpillCorner()
{
var server = pairTracker.Pair.Server;
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
- var spillSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SpillableSystem>();
var puddleSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<PuddleSystem>();
var gameTiming = server.ResolveDependency<IGameTiming>();
MapId mapId;
/*
In this test, if o is spillage puddle and # are walls, we want to ensure all tiles are empty (`.`)
- o # .
- # . .
. . .
+ # . .
+ o # .
*/
await server.WaitPost(() =>
{
await server.WaitAssertion(() =>
{
var grid = mapManager.GetGrid(gridId);
- var solution = new Solution("Water", FixedPoint2.New(100));
+ var solution = new Solution("Blood", FixedPoint2.New(100));
var tileRef = grid.GetTileRef(puddleOrigin);
- var puddle = spillSystem.SpillAt(tileRef, solution, "PuddleSmear");
- Assert.That(puddle, Is.Not.Null);
+ Assert.That(puddleSystem.TrySpillAt(tileRef, solution, out _), Is.True);
Assert.That(GetPuddle(entityManager, grid, puddleOrigin), Is.Not.Null);
});
using Content.Shared.Chemistry.Components;
using Content.Shared.Coordinates;
using Content.Shared.FixedPoint;
+using Content.Shared.Fluids.Components;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
var testMap = await PoolManager.CreateTestMap(pairTracker);
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
- var spillSystem = entitySystemManager.GetEntitySystem<SpillableSystem>();
+ var spillSystem = entitySystemManager.GetEntitySystem<PuddleSystem>();
await server.WaitAssertion(() =>
{
var gridUid = tile.GridUid;
var (x, y) = tile.GridIndices;
var coordinates = new EntityCoordinates(gridUid, x, y);
- var puddle = spillSystem.SpillAt(solution, coordinates, "PuddleSmear");
+ var puddle = spillSystem.TrySpillAt(coordinates, solution, out _);
- Assert.NotNull(puddle);
+ Assert.True(puddle);
});
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
var testMap = await PoolManager.CreateTestMap(pairTracker);
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
- var spillSystem = entitySystemManager.GetEntitySystem<SpillableSystem>();
+ var spillSystem = entitySystemManager.GetEntitySystem<PuddleSystem>();
MapGridComponent grid = null;
{
var coordinates = grid.ToCoordinates();
var solution = new Solution("Water", FixedPoint2.New(20));
- var puddle = spillSystem.SpillAt(solution, coordinates, "PuddleSmear");
- Assert.Null(puddle);
+ var puddle = spillSystem.TrySpillAt(coordinates, solution, out _);
+ Assert.False(puddle);
});
await pairTracker.CleanReturnAsync();
}
-
- [Test]
- public async Task PuddlePauseTest()
- {
- await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
- var server = pairTracker.Pair.Server;
-
- var sMapManager = server.ResolveDependency<IMapManager>();
- var sTileDefinitionManager = server.ResolveDependency<ITileDefinitionManager>();
- var sGameTiming = server.ResolveDependency<IGameTiming>();
- var entityManager = server.ResolveDependency<IEntityManager>();
- var metaSystem = entityManager.EntitySysManager.GetEntitySystem<MetaDataSystem>();
-
- MapId sMapId = default;
- MapGridComponent sGrid;
- EntityUid sGridId = default;
- EntityCoordinates sCoordinates = default;
-
- // Spawn a paused map with one tile to spawn puddles on
- await server.WaitPost(() =>
- {
- sMapId = sMapManager.CreateMap();
- sMapManager.SetMapPaused(sMapId, true);
- sGrid = sMapManager.CreateGrid(sMapId);
- sGridId = sGrid.Owner;
- metaSystem.SetEntityPaused(sGridId, true); // See https://github.com/space-wizards/RobustToolbox/issues/1444
-
- var tileDefinition = sTileDefinitionManager["UnderPlating"];
- var tile = new Tile(tileDefinition.TileId);
- sCoordinates = sGrid.ToCoordinates();
-
- sGrid.SetTile(sCoordinates, tile);
- });
-
- // Check that the map and grid are paused
- await server.WaitAssertion(() =>
- {
- Assert.True(metaSystem.EntityPaused(sGridId));
- Assert.True(sMapManager.IsMapPaused(sMapId));
- });
-
- float evaporateTime = default;
- PuddleComponent puddle = null;
- MetaDataComponent meta = null;
- EvaporationComponent evaporation;
-
- var amount = 2;
-
- var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
- var spillSystem = entitySystemManager.GetEntitySystem<SpillableSystem>();
-
- // Spawn a puddle
- await server.WaitAssertion(() =>
- {
- var solution = new Solution("Water", FixedPoint2.New(amount));
- puddle = spillSystem.SpillAt(solution, sCoordinates, "PuddleSmear");
- meta = entityManager.GetComponent<MetaDataComponent>(puddle.Owner);
-
- // Check that the puddle was created
- Assert.NotNull(puddle);
-
- evaporation = entityManager.GetComponent<EvaporationComponent>(puddle.Owner);
- metaSystem.SetEntityPaused(puddle.Owner, true, meta); // See https://github.com/space-wizards/RobustToolbox/issues/1445
-
- Assert.True(metaSystem.EntityPaused(puddle.Owner, meta));
-
- // Check that the puddle is going to evaporate
- Assert.Positive(evaporation.EvaporateTime);
-
- // Should have a timer component added to it for evaporation
- Assert.That(evaporation.Accumulator, Is.EqualTo(0f));
-
- evaporateTime = evaporation.EvaporateTime;
- });
-
- // Wait enough time for it to evaporate if it was unpaused
- var sTimeToWait = 5 + (int)Math.Ceiling(amount * evaporateTime * sGameTiming.TickRate);
- await PoolManager.RunTicksSync(pairTracker.Pair, sTimeToWait);
-
- // No evaporation due to being paused
- await server.WaitAssertion(() =>
- {
- Assert.True(meta.EntityPaused);
-
- // Check that the puddle still exists
- Assert.False(meta.EntityDeleted);
- });
-
- // Unpause the map
- await server.WaitPost(() => { sMapManager.SetMapPaused(sMapId, false); });
-
- // Check that the map, grid and puddle are unpaused
- await server.WaitAssertion(() =>
- {
- Assert.False(sMapManager.IsMapPaused(sMapId));
- Assert.False(metaSystem.EntityPaused(sGridId));
- Assert.False(meta.EntityPaused);
-
- // Check that the puddle still exists
- Assert.False(meta.EntityDeleted);
- });
-
- // Wait enough time for it to evaporate
- await PoolManager.RunTicksSync(pairTracker.Pair, sTimeToWait);
-
- // Puddle evaporation should have ticked
- await server.WaitAssertion(() =>
- {
- // Check that puddle has been deleted
- Assert.True(puddle.Deleted);
- });
- await pairTracker.CleanReturnAsync();
- }
}
}
[ViewVariables]
private AMEControllerComponent? _masterController;
- [Dependency] private readonly IRobustRandom _random = default!;
-
- [Dependency] private readonly IEntityManager _entMan = default!;
-
[Dependency] private readonly IChatManager _chat = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
public AMEControllerComponent? MasterController => _masterController;
{
base.LoadNodes(groupNodes);
- var mapManager = IoCManager.Resolve<IMapManager>();
MapGridComponent? grid = null;
foreach (var node in groupNodes)
if (_entMan.TryGetComponent(nodeOwner, out AMEShieldComponent? shield))
{
var xform = _entMan.GetComponent<TransformComponent>(nodeOwner);
- if (xform.GridUid != grid?.Owner && !mapManager.TryGetGrid(xform.GridUid, out grid))
+ if (xform.GridUid != grid?.Owner && !_mapManager.TryGetGrid(xform.GridUid, out grid))
continue;
if (grid == null)
using Content.Server.DoAfter;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
+using Content.Shared.Chemistry.Components;
using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement;
using Content.Shared.Nutrition.Components;
[DataField("molesPerUnit")] public float MolesPerUnit { get; } = 1;
- [DataField("puddlePrototype")] public string? PuddlePrototype { get; } = "PuddleSmear";
-
public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, AtmosphereSystem atmosphereSystem)
{
// If any of the prototypes is invalid, we do nothing.
- if (string.IsNullOrEmpty(Reagent) || string.IsNullOrEmpty(PuddlePrototype)) return ReactionResult.NoReaction;
+ if (string.IsNullOrEmpty(Reagent))
+ return ReactionResult.NoReaction;
// If we're not reacting on a tile, do nothing.
- if (holder is not TileAtmosphere tile) return ReactionResult.NoReaction;
+ if (holder is not TileAtmosphere tile)
+ return ReactionResult.NoReaction;
// If we don't have enough moles of the specified gas, do nothing.
- if (mixture.GetMoles(GasId) < MolesPerUnit) return ReactionResult.NoReaction;
+ if (mixture.GetMoles(GasId) < MolesPerUnit)
+ return ReactionResult.NoReaction;
// Remove the moles from the mixture...
mixture.AdjustMoles(GasId, -MolesPerUnit);
var tileRef = tile.GridIndices.GetTileRef(tile.GridIndex);
- EntitySystem.Get<SpillableSystem>()
- .SpillAt(tileRef, new Solution(Reagent, FixedPoint2.New(MolesPerUnit)), PuddlePrototype, sound: false);
+ EntitySystem.Get<PuddleSystem>()
+ .TrySpillAt(tileRef, new Solution(Reagent, FixedPoint2.New(MolesPerUnit)), out _, sound: false);
return ReactionResult.Reacting;
}
public sealed class BloodstreamSystem : EntitySystem
{
- [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
- [Dependency] private readonly DamageableSystem _damageableSystem = default!;
- [Dependency] private readonly SpillableSystem _spillableSystem = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
- [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly AudioSystem _audio = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly PuddleSystem _puddleSystem = default!;
+ [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedDrunkSystem _drunkSystem = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
public override void Initialize()
{
// Pass some of the chemstream into the spilled blood.
var temp = component.ChemicalSolution.SplitSolution(component.BloodTemporarySolution.Volume / 10);
component.BloodTemporarySolution.AddSolution(temp, _prototypeManager);
- var puddle = _spillableSystem.SpillAt(uid, component.BloodTemporarySolution, "PuddleBlood", false);
- if (puddle != null)
+ if (_puddleSystem.TrySpillAt(uid, component.BloodTemporarySolution, out var puddleUid, false))
{
- var comp = EnsureComp<ForensicsComponent>(puddle.Owner); //TODO: Get rid of .Owner
if (TryComp<DnaComponent>(uid, out var dna))
+ {
+ var comp = EnsureComp<ForensicsComponent>(puddleUid);
comp.DNAs.Add(dna.DNA);
+ }
}
component.BloodTemporarySolution.RemoveAllSolution();
component.BloodTemporarySolution.RemoveAllSolution();
tempSol.AddSolution(component.ChemicalSolution, _prototypeManager);
component.ChemicalSolution.RemoveAllSolution();
- var puddle = _spillableSystem.SpillAt(uid, tempSol, "PuddleBlood", true);
- if (puddle != null)
+ if (_puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid))
{
- var comp = EnsureComp<ForensicsComponent>(puddle.Owner); //TODO: Get rid of .Owner
if (TryComp<DnaComponent>(uid, out var dna))
+ {
+ var comp = EnsureComp<ForensicsComponent>(puddleUid);
comp.DNAs.Add(dna.DNA);
+ }
}
}
}
+++ /dev/null
-using Content.Server.Body.Components;
-using Content.Server.Body.Systems;
-using Content.Server.Chemistry.EntitySystems;
-using Content.Shared.Administration.Logs;
-using Content.Shared.Database;
-using Content.Shared.FixedPoint;
-using Content.Shared.Foam;
-using Content.Shared.Inventory;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.Chemistry.Components
-{
- [RegisterComponent]
- [ComponentReference(typeof(SolutionAreaEffectComponent))]
- public sealed class FoamSolutionAreaEffectComponent : SolutionAreaEffectComponent
- {
- [Dependency] private readonly IEntityManager _entMan = default!;
- [Dependency] private readonly IPrototypeManager _proto = default!;
- [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
-
- public new const string SolutionName = "solutionArea";
-
- [DataField("foamedMetalPrototype")] private string? _foamedMetalPrototype;
-
- protected override void UpdateVisuals()
- {
- if (_entMan.TryGetComponent(Owner, out AppearanceComponent? appearance) &&
- EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
- {
- appearance.SetData(FoamVisuals.Color, solution.GetColor(_proto).WithAlpha(0.80f));
- }
- }
-
- protected override void ReactWithEntity(EntityUid entity, double solutionFraction)
- {
- if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
- return;
-
- if (!_entMan.TryGetComponent(entity, out BloodstreamComponent? bloodstream))
- return;
-
- var invSystem = EntitySystem.Get<InventorySystem>();
-
- // TODO: Add a permeability property to clothing
- // For now it just adds to protection for each clothing equipped
- var protection = 0f;
- if (invSystem.TryGetSlots(entity, out var slotDefinitions))
- {
- foreach (var slot in slotDefinitions)
- {
- if (slot.Name == "back" ||
- slot.Name == "pocket1" ||
- slot.Name == "pocket2" ||
- slot.Name == "id")
- continue;
-
- if (invSystem.TryGetSlotEntity(entity, slot.Name, out _))
- protection += 0.025f;
- }
- }
-
- var bloodstreamSys = EntitySystem.Get<BloodstreamSystem>();
-
- var cloneSolution = solution.Clone();
- var transferAmount = FixedPoint2.Min(cloneSolution.Volume * solutionFraction * (1 - protection),
- bloodstream.ChemicalSolution.AvailableVolume);
- var transferSolution = cloneSolution.SplitSolution(transferAmount);
-
- if (bloodstreamSys.TryAddToChemicals(entity, transferSolution, bloodstream))
- {
- // Log solution addition by foam
- _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{_entMan.ToPrettyString(entity):target} was affected by foam {SolutionContainerSystem.ToPrettyString(transferSolution)}");
- }
- }
-
- protected override void OnKill()
- {
- if (_entMan.Deleted(Owner))
- return;
- if (_entMan.TryGetComponent(Owner, out AppearanceComponent? appearance))
- {
- appearance.SetData(FoamVisuals.State, true);
- }
-
- Owner.SpawnTimer(600, () =>
- {
- if (!string.IsNullOrEmpty(_foamedMetalPrototype))
- {
- _entMan.SpawnEntity(_foamedMetalPrototype, _entMan.GetComponent<TransformComponent>(Owner).Coordinates);
- }
-
- _entMan.QueueDeleteEntity(Owner);
- });
- }
- }
-}
--- /dev/null
+using Content.Shared.Fluids.Components;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.Chemistry.Components;
+
+/// <summary>
+/// Stores solution on an anchored entity that has touch and ingestion reactions
+/// to entities that collide with it. Similar to <see cref="PuddleComponent"/>
+/// </summary>
+[RegisterComponent]
+public sealed class SmokeComponent : Component
+{
+ public const string SolutionName = "solutionArea";
+
+ [DataField("nextReact", customTypeSerializer:typeof(TimeOffsetSerializer))]
+ public TimeSpan NextReact = TimeSpan.Zero;
+
+ [DataField("spreadAmount")]
+ public int SpreadAmount = 0;
+
+ /// <summary>
+ /// Have we reacted with our tile yet?
+ /// </summary>
+ [DataField("reactedTile")]
+ public bool ReactedTile = false;
+}
--- /dev/null
+using Content.Server.Fluids.EntitySystems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Chemistry.Components;
+
+
+/// <summary>
+/// When a <see cref="SmokeComponent"/> despawns this will spawn another entity in its place.
+/// </summary>
+[RegisterComponent, Access(typeof(SmokeSystem))]
+public sealed class SmokeDissipateSpawnComponent : Component
+{
+ [DataField("prototype", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
+ public string Prototype = string.Empty;
+}
+++ /dev/null
-using Content.Server.Body.Components;
-using Content.Server.Body.Systems;
-using Content.Server.Chemistry.EntitySystems;
-using Content.Shared.Administration.Logs;
-using Content.Shared.Chemistry;
-using Content.Shared.Chemistry.Reagent;
-using Content.Shared.Database;
-using Content.Shared.FixedPoint;
-using Content.Shared.Smoking;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.Chemistry.Components
-{
- [RegisterComponent]
- [ComponentReference(typeof(SolutionAreaEffectComponent))]
- public sealed class SmokeSolutionAreaEffectComponent : SolutionAreaEffectComponent
- {
- [Dependency] private readonly IEntityManager _entMan = default!;
- [Dependency] private readonly IPrototypeManager _proto = default!;
- [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
-
- public new const string SolutionName = "solutionArea";
-
- protected override void UpdateVisuals()
- {
- if (_entMan.TryGetComponent(Owner, out AppearanceComponent? appearance) &&
- EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
- {
- appearance.SetData(SmokeVisuals.Color, solution.GetColor(_proto));
- }
- }
-
- protected override void ReactWithEntity(EntityUid entity, double solutionFraction)
- {
- if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
- return;
-
- if (!_entMan.TryGetComponent(entity, out BloodstreamComponent? bloodstream))
- return;
-
- if (_entMan.TryGetComponent(entity, out InternalsComponent? internals) &&
- IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InternalsSystem>().AreInternalsWorking(internals))
- return;
-
- var chemistry = EntitySystem.Get<ReactiveSystem>();
- 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;
- chemistry.ReactionEntity(entity, ReactionMethod.Ingestion, reagentQuantity.ReagentId, reagentQuantity.Quantity, transferSolution);
- }
-
- var bloodstreamSys = EntitySystem.Get<BloodstreamSystem>();
- if (bloodstreamSys.TryAddToChemicals(entity, transferSolution, bloodstream))
- {
- // Log solution addition by smoke
- _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{_entMan.ToPrettyString(entity):target} was affected by smoke {SolutionContainerSystem.ToPrettyString(transferSolution)}");
- }
- }
-
-
- protected override void OnKill()
- {
- if (_entMan.Deleted(Owner))
- return;
- _entMan.DeleteEntity(Owner);
- }
- }
-}
+++ /dev/null
-using System.Linq;
-using Content.Server.Atmos.Components;
-using Content.Server.Chemistry.EntitySystems;
-using Content.Shared.Chemistry;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.Reagent;
-using Content.Shared.FixedPoint;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Chemistry.Components
-{
- /// <summary>
- /// Used to clone its owner repeatedly and group up them all so they behave like one unit, that way you can have
- /// effects that cover an area. Inherited by <see cref="SmokeSolutionAreaEffectComponent"/> and <see cref="FoamSolutionAreaEffectComponent"/>.
- /// </summary>
- public abstract class SolutionAreaEffectComponent : Component
- {
- public const string SolutionName = "solutionArea";
-
- [Dependency] protected readonly IMapManager MapManager = default!;
- [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
- [Dependency] private readonly IEntityManager _entities = default!;
- [Dependency] private readonly IEntitySystemManager _systems = default!;
-
- public int Amount { get; set; }
- public SolutionAreaEffectInceptionComponent? Inception { get; set; }
-
- /// <summary>
- /// Have we reacted with our tile yet?
- /// </summary>
- public bool ReactedTile = false;
-
- /// <summary>
- /// Adds an <see cref="SolutionAreaEffectInceptionComponent"/> to owner so the effect starts spreading and reacting.
- /// </summary>
- /// <param name="amount">The range of the effect</param>
- /// <param name="duration"></param>
- /// <param name="spreadDelay"></param>
- /// <param name="removeDelay"></param>
- public void Start(int amount, float duration, float spreadDelay, float removeDelay)
- {
- if (Inception != null)
- return;
-
- if (_entities.HasComponent<SolutionAreaEffectInceptionComponent>(Owner))
- return;
-
- Amount = amount;
- var inception = _entities.AddComponent<SolutionAreaEffectInceptionComponent>(Owner);
-
- inception.Add(this);
- inception.Setup(amount, duration, spreadDelay, removeDelay);
- }
-
- /// <summary>
- /// Gets called by an AreaEffectInceptionComponent. "Clones" Owner into the four directions and copies the
- /// solution into each of them.
- /// </summary>
- public void Spread()
- {
- var meta = _entities.GetComponent<MetaDataComponent>(Owner);
- if (meta.EntityPrototype == null)
- {
- Logger.Error("AreaEffectComponent needs its owner to be spawned by a prototype.");
- return;
- }
-
- var xform = _entities.GetComponent<TransformComponent>(Owner);
- var solSys = _systems.GetEntitySystem<SolutionContainerSystem>();
-
- if (!_entities.TryGetComponent(xform.GridUid, out MapGridComponent? gridComp))
- return;
-
- var origin = gridComp.TileIndicesFor(xform.Coordinates);
-
- DebugTools.Assert(xform.Anchored, "Area effect entity prototypes must be anchored.");
-
- void SpreadToDir(Direction dir)
- {
- // Currently no support for spreading off or across grids.
- var index = origin + dir.ToIntVec();
- if (!gridComp.TryGetTileRef(index, out var tile) || tile.Tile.IsEmpty)
- return;
-
- foreach (var neighbor in gridComp.GetAnchoredEntities(index))
- {
- if (_entities.TryGetComponent(neighbor,
- out SolutionAreaEffectComponent? comp) && comp.Inception == Inception)
- return;
-
- // TODO for thindows and the like, need to check the directions that are being blocked.
- // --> would then also mean you need to check for blockers on the origin tile.
- if (_entities.TryGetComponent(neighbor,
- out AirtightComponent? airtight) && airtight.AirBlocked)
- return;
- }
-
- var newEffect = _entities.SpawnEntity(
- meta.EntityPrototype.ID,
- gridComp.GridTileToLocal(index));
-
- if (!_entities.TryGetComponent(newEffect, out SolutionAreaEffectComponent? effectComponent))
- {
- _entities.DeleteEntity(newEffect);
- return;
- }
-
- if (solSys.TryGetSolution(Owner, SolutionName, out var solution))
- {
- effectComponent.TryAddSolution(solution.Clone());
- }
-
- effectComponent.Amount = Amount - 1;
- Inception?.Add(effectComponent);
- }
-
- SpreadToDir(Direction.North);
- SpreadToDir(Direction.East);
- SpreadToDir(Direction.South);
- SpreadToDir(Direction.West);
- }
-
- /// <summary>
- /// Gets called by an AreaEffectInceptionComponent.
- /// Removes this component from its inception and calls OnKill(). The implementation of OnKill() should
- /// eventually delete the entity.
- /// </summary>
- public void Kill()
- {
- Inception?.Remove(this);
- OnKill();
- }
-
- protected abstract void OnKill();
-
- /// <summary>
- /// Gets called by an AreaEffectInceptionComponent.
- /// Makes this effect's reagents react with the tile its on and with the entities it covers. Also calls
- /// ReactWithEntity on the entities so inheritors can implement more specific behavior.
- /// </summary>
- /// <param name="averageExposures">How many times will this get called over this area effect's duration, averaged
- /// with the other area effects from the inception.</param>
- public void React(float averageExposures)
- {
- if (!_entities.EntitySysManager.GetEntitySystem<SolutionContainerSystem>()
- .TryGetSolution(Owner, SolutionName, out var solution) ||
- solution.Contents.Count == 0)
- {
- return;
- }
-
- var xform = _entities.GetComponent<TransformComponent>(Owner);
- if (!MapManager.TryGetGrid(xform.GridUid, out var mapGrid))
- return;
-
- var tile = mapGrid.GetTileRef(xform.Coordinates.ToVector2i(_entities, MapManager));
- var chemistry = _entities.EntitySysManager.GetEntitySystem<ReactiveSystem>();
- var lookup = _entities.EntitySysManager.GetEntitySystem<EntityLookupSystem>();
-
- var solutionFraction = 1 / Math.Floor(averageExposures);
- var ents = lookup.GetEntitiesIntersecting(tile, LookupFlags.Uncontained).ToArray();
-
- foreach (var reagentQuantity in solution.Contents.ToArray())
- {
- if (reagentQuantity.Quantity == FixedPoint2.Zero) continue;
- var reagent = PrototypeManager.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 (!ReactedTile)
- {
- reagent.ReactionTile(tile, reagentQuantity.Quantity);
- ReactedTile = true;
- }
-
- // Touch every entity on the tile
- foreach (var entity in ents)
- {
- chemistry.ReactionEntity(entity, ReactionMethod.Touch, reagent,
- reagentQuantity.Quantity * solutionFraction, solution);
- }
- }
-
- foreach (var entity in ents)
- {
- ReactWithEntity(entity, solutionFraction);
- }
- }
-
- protected abstract void ReactWithEntity(EntityUid entity, double solutionFraction);
-
- public void TryAddSolution(Solution solution)
- {
- if (solution.Volume == 0)
- return;
-
- if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solutionArea))
- return;
-
- var addSolution =
- solution.SplitSolution(FixedPoint2.Min(solution.Volume, solutionArea.AvailableVolume));
-
- EntitySystem.Get<SolutionContainerSystem>().TryAddSolution(Owner, solutionArea, addSolution);
-
- UpdateVisuals();
- }
-
- protected abstract void UpdateVisuals();
-
- protected override void OnRemove()
- {
- base.OnRemove();
- Inception?.Remove(this);
- }
- }
-}
+++ /dev/null
-using System.Linq;
-
-namespace Content.Server.Chemistry.Components
-{
- /// <summary>
- /// The "mastermind" of a SolutionAreaEffect group. It gets updated by the SolutionAreaEffectSystem and tells the
- /// group when to spread, react and remove itself. This makes the group act like a single unit.
- /// </summary>
- /// <remarks> It should only be manually added to an entity by the <see cref="SolutionAreaEffectComponent"/> and not with a prototype.</remarks>
- [RegisterComponent]
- public sealed class SolutionAreaEffectInceptionComponent : Component
- {
- private const float ReactionDelay = 1.5f;
-
- private readonly HashSet<SolutionAreaEffectComponent> _group = new();
-
- [ViewVariables] private float _lifeTimer;
- [ViewVariables] private float _spreadTimer;
- [ViewVariables] private float _reactionTimer;
-
- [ViewVariables] private int _amountCounterSpreading;
- [ViewVariables] private int _amountCounterRemoving;
-
- /// <summary>
- /// How much time to wait after fully spread before starting to remove itself.
- /// </summary>
- [ViewVariables] private float _duration;
-
- /// <summary>
- /// Time between each spread step. Decreasing this makes spreading faster.
- /// </summary>
- [ViewVariables] private float _spreadDelay;
-
- /// <summary>
- /// Time between each remove step. Decreasing this makes removing faster.
- /// </summary>
- [ViewVariables] private float _removeDelay;
-
- /// <summary>
- /// How many times will the effect react. As some entities from the group last a different amount of time than
- /// others, they will react a different amount of times, so we calculate the average to make the group behave
- /// a bit more uniformly.
- /// </summary>
- [ViewVariables] private float _averageExposures;
-
- public void Setup(int amount, float duration, float spreadDelay, float removeDelay)
- {
- _amountCounterSpreading = amount;
- _duration = duration;
- _spreadDelay = spreadDelay;
- _removeDelay = removeDelay;
-
- // So the first square reacts immediately after spawning
- _reactionTimer = ReactionDelay;
- /*
- The group takes amount*spreadDelay seconds to fully spread, same with fully disappearing.
- The outer squares will last duration seconds.
- The first square will last duration + how many seconds the group takes to fully spread and fully disappear, so
- it will last duration + amount*(spreadDelay+removeDelay).
- Thus, the average lifetime of the smokes will be (outerSmokeLifetime + firstSmokeLifetime)/2 = duration + amount*(spreadDelay+removeDelay)/2
- */
- _averageExposures = (duration + amount * (spreadDelay+removeDelay) / 2)/ReactionDelay;
- }
-
- public void InceptionUpdate(float frameTime)
- {
- _group.RemoveWhere(effect => effect.Deleted);
- if (_group.Count == 0)
- return;
-
- // Make every outer square from the group spread
- if (_amountCounterSpreading > 0)
- {
- _spreadTimer += frameTime;
- if (_spreadTimer > _spreadDelay)
- {
- _spreadTimer -= _spreadDelay;
-
- var outerEffects = new HashSet<SolutionAreaEffectComponent>(_group.Where(effect => effect.Amount == _amountCounterSpreading));
- foreach (var effect in outerEffects)
- {
- effect.Spread();
- }
-
- _amountCounterSpreading -= 1;
- }
- }
- // Start counting for _duration after fully spreading
- else
- {
- _lifeTimer += frameTime;
- }
-
- // Delete every outer square
- if (_lifeTimer > _duration)
- {
- _spreadTimer += frameTime;
- if (_spreadTimer > _removeDelay)
- {
- _spreadTimer -= _removeDelay;
-
- var outerEffects = new HashSet<SolutionAreaEffectComponent>(_group.Where(effect => effect.Amount == _amountCounterRemoving));
- foreach (var effect in outerEffects)
- {
- effect.Kill();
- }
-
- _amountCounterRemoving += 1;
- }
- }
-
- // Make every square from the group react with the tile and entities
- _reactionTimer += frameTime;
- if (_reactionTimer > ReactionDelay)
- {
- _reactionTimer -= ReactionDelay;
- foreach (var effect in _group)
- {
- effect.React(_averageExposures);
- }
- }
- }
-
- public void Add(SolutionAreaEffectComponent effect)
- {
- _group.Add(effect);
- effect.Inception = this;
- }
-
- public void Remove(SolutionAreaEffectComponent effect)
- {
- _group.Remove(effect);
- effect.Inception = null;
- }
- }
-}
+++ /dev/null
-namespace Content.Server.Chemistry.Components.SolutionManager
-{
- /// <summary>
- /// Denotes the solution that can be easily removed through any reagent container.
- /// Think pouring this or draining from a water tank.
- /// </summary>
- [RegisterComponent]
- public sealed class DrainableSolutionComponent : Component
- {
- /// <summary>
- /// Solution name that can be drained.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("solution")]
- public string Solution { get; set; } = "default";
- }
-}
+++ /dev/null
-using Content.Shared.FixedPoint;
-
-namespace Content.Server.Chemistry.Components.SolutionManager
-{
- /// <summary>
- /// Reagents that can be added easily. For example like
- /// pouring something into another beaker, glass, or into the gas
- /// tank of a car.
- /// </summary>
- [RegisterComponent]
- public sealed class RefillableSolutionComponent : Component
- {
- /// <summary>
- /// Solution name that can added to easily.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("solution")]
- public string Solution { get; set; } = "default";
-
- /// <summary>
- /// The maximum amount that can be transferred to the solution at once
- /// </summary>
- [DataField("maxRefill")]
- [ViewVariables(VVAccess.ReadWrite)]
- public FixedPoint2? MaxRefill { get; set; } = null;
-
-
- }
-}
+++ /dev/null
-using System.Linq;
-using Content.Server.Chemistry.Components;
-using Content.Server.Chemistry.ReactionEffects;
-using Content.Shared.Chemistry.Reaction;
-using JetBrains.Annotations;
-
-namespace Content.Server.Chemistry.EntitySystems
-{
- [UsedImplicitly]
- public sealed class SolutionAreaEffectSystem : EntitySystem
- {
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent<SolutionAreaEffectComponent, ReactionAttemptEvent>(OnReactionAttempt);
- }
-
- public override void Update(float frameTime)
- {
- foreach (var inception in EntityManager.EntityQuery<SolutionAreaEffectInceptionComponent>().ToArray())
- {
- inception.InceptionUpdate(frameTime);
- }
- }
-
- private void OnReactionAttempt(EntityUid uid, SolutionAreaEffectComponent component, ReactionAttemptEvent args)
- {
- if (args.Solution.Name != SolutionAreaEffectComponent.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;
- }
- }
- }
- }
-}
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
+using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
return splitSol;
}
+ /// <summary>
+ /// Splits a solution without the specified reagent.
+ /// </summary>
+ public Solution SplitSolutionWithout(EntityUid targetUid, Solution solutionHolder, FixedPoint2 quantity,
+ string reagent)
+ {
+ var splitSol = solutionHolder.SplitSolutionWithout(quantity, reagent);
+ UpdateChemicals(targetUid, solutionHolder);
+ return splitSol;
+ }
+
public void UpdateChemicals(EntityUid uid, Solution solutionHolder, bool needsReactionsProcessing = false, ReactionMixerComponent? mixerComponent = null)
{
DebugTools.Assert(solutionHolder.Name != null && TryGetSolution(uid, solutionHolder.Name, out var tmp) && tmp == solutionHolder);
return false;
}
+ /// <summary>
+ /// Gets the most common reagent across all solutions by volume.
+ /// </summary>
+ /// <param name="component"></param>
+ public ReagentPrototype? GetMaxReagent(SolutionContainerManagerComponent component)
+ {
+ if (component.Solutions.Count == 0)
+ return null;
+
+ var reagentCounts = new Dictionary<string, FixedPoint2>();
+
+ foreach (var solution in component.Solutions.Values)
+ {
+ foreach (var reagent in solution.Contents)
+ {
+ reagentCounts.TryGetValue(reagent.ReagentId, out var existing);
+ existing += reagent.Quantity;
+ reagentCounts[reagent.ReagentId] = existing;
+ }
+ }
+
+ var max = reagentCounts.Max();
+
+ return _prototypeManager.Index<ReagentPrototype>(max.Key);
+ }
+
+ public SoundSpecifier? GetSound(SolutionContainerManagerComponent component)
+ {
+ var max = GetMaxReagent(component);
+ return max?.FootstepSound;
+ }
// Thermal energy and temperature management.
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Coordinates.Helpers;
+using Content.Server.Fluids.EntitySystems;
using Content.Shared.Audio;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
+using Content.Shared.FixedPoint;
+using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Player;
-using Robust.Shared.Serialization;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Chemistry.ReactionEffects
{
/// Basically smoke and foam reactions.
/// </summary>
[UsedImplicitly]
- [ImplicitDataDefinitionForInheritors]
- public abstract class AreaReactionEffect : ReagentEffect, ISerializationHooks
+ [DataDefinition]
+ public sealed class AreaReactionEffect : ReagentEffect
{
- [Dependency] private readonly IMapManager _mapManager = default!;
-
- /// <summary>
- /// Used for calculating the spread range of the effect based on the intensity of the reaction.
- /// </summary>
- [DataField("rangeConstant")] private float _rangeConstant;
- [DataField("rangeMultiplier")] private float _rangeMultiplier = 1.1f;
- [DataField("maxRange")] private int _maxRange = 10;
-
- /// <summary>
- /// If true the reagents get diluted or concentrated depending on the range of the effect
- /// </summary>
- [DataField("diluteReagents")] private bool _diluteReagents;
-
- /// <summary>
- /// Used to calculate dilution. Increasing this makes the reagents more diluted.
- /// </summary>
- [DataField("reagentDilutionFactor")] private float _reagentDilutionFactor = 1f;
-
/// <summary>
/// How many seconds will the effect stay, counting after fully spreading.
/// </summary>
[DataField("duration")] private float _duration = 10;
/// <summary>
- /// How many seconds between each spread step.
- /// </summary>
- [DataField("spreadDelay")] private float _spreadDelay = 0.5f;
-
- /// <summary>
- /// How many seconds between each remove step.
+ /// How many units of reaction for 1 smoke entity.
/// </summary>
- [DataField("removeDelay")] private float _removeDelay = 0.5f;
+ [DataField("overflowThreshold")] public FixedPoint2 OverflowThreshold = FixedPoint2.New(2.5);
/// <summary>
- /// The entity prototype that will be spawned as the effect. It needs a component derived from SolutionAreaEffectComponent.
+ /// The entity prototype that will be spawned as the effect.
/// </summary>
- [DataField("prototypeId", required: true)]
+ [DataField("prototypeId", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
private string _prototypeId = default!;
/// <summary>
public override bool ShouldLog => true;
public override LogImpact LogImpact => LogImpact.High;
- void ISerializationHooks.AfterDeserialization()
- {
- IoCManager.InjectDependencies(this);
- }
-
public override void Effect(ReagentEffectArgs args)
{
if (args.Source == null)
return;
- var splitSolution = EntitySystem.Get<SolutionContainerSystem>().SplitSolution(args.SolutionEntity, args.Source, args.Source.Volume);
- // We take the square root so it becomes harder to reach higher amount values
- var amount = (int) Math.Round(_rangeConstant + _rangeMultiplier*Math.Sqrt(args.Quantity.Float()));
- amount = Math.Min(amount, _maxRange);
+ var spreadAmount = (int) Math.Max(0, Math.Ceiling((args.Quantity / OverflowThreshold).Float()));
+ var splitSolution = args.EntityManager.System<SolutionContainerSystem>().SplitSolution(args.SolutionEntity, args.Source, args.Source.Volume);
+ var transform = args.EntityManager.GetComponent<TransformComponent>(args.SolutionEntity);
+ var mapManager = IoCManager.Resolve<IMapManager>();
- if (_diluteReagents)
+ if (!mapManager.TryFindGridAt(transform.MapPosition, out var grid) ||
+ !grid.TryGetTileRef(transform.Coordinates, out var tileRef) ||
+ tileRef.Tile.IsSpace())
{
- // The maximum value of solutionFraction is _reagentMaxConcentrationFactor, achieved when amount = 0
- // The infimum of solutionFraction is 0, which is approached when amount tends to infinity
- // solutionFraction is equal to 1 only when amount equals _reagentDilutionStart
- // Weird formulas here but basically when amount increases, solutionFraction gets closer to 0 in a reciprocal manner
- // _reagentDilutionFactor defines how fast solutionFraction gets closer to 0
- float solutionFraction = 1 / (_reagentDilutionFactor*(amount) + 1);
- splitSolution.RemoveSolution(splitSolution.Volume * (1 - solutionFraction));
+ return;
}
- var transform = args.EntityManager.GetComponent<TransformComponent>(args.SolutionEntity);
-
- if (!_mapManager.TryFindGridAt(transform.MapPosition, out var grid)) return;
-
var coords = grid.MapToGrid(transform.MapPosition);
-
var ent = args.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid());
- var areaEffectComponent = GetAreaEffectComponent(ent);
-
- if (areaEffectComponent == null)
+ if (!args.EntityManager.TryGetComponent<SmokeComponent>(ent, out var smokeComponent))
{
Logger.Error("Couldn't get AreaEffectComponent from " + _prototypeId);
- IoCManager.Resolve<IEntityManager>().QueueDeleteEntity(ent);
+ args.EntityManager.QueueDeleteEntity(ent);
return;
}
- areaEffectComponent.TryAddSolution(splitSolution);
- areaEffectComponent.Start(amount, _duration, _spreadDelay, _removeDelay);
+ var smoke = args.EntityManager.System<SmokeSystem>();
+ smokeComponent.SpreadAmount = spreadAmount;
+ smoke.Start(ent, smokeComponent, splitSolution, _duration);
SoundSystem.Play(_sound.GetSound(), Filter.Pvs(args.SolutionEntity), args.SolutionEntity, AudioHelpers.WithVariation(0.125f));
}
-
- protected abstract SolutionAreaEffectComponent? GetAreaEffectComponent(EntityUid entity);
}
}
+++ /dev/null
-using Content.Server.Chemistry.Components;
-using Content.Server.Coordinates.Helpers;
-using Content.Shared.Audio;
-using Content.Shared.Chemistry.Components;
-using JetBrains.Annotations;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
-
-namespace Content.Server.Chemistry.ReactionEffects
-{
- [UsedImplicitly]
- [DataDefinition]
- public sealed class FoamAreaReactionEffect : AreaReactionEffect
- {
- protected override SolutionAreaEffectComponent? GetAreaEffectComponent(EntityUid entity)
- {
- return IoCManager.Resolve<IEntityManager>().GetComponentOrNull<FoamSolutionAreaEffectComponent>(entity);
- }
-
- public static void SpawnFoam(string entityPrototype, EntityCoordinates coords, Solution? contents, int amount, float duration, float spreadDelay,
- float removeDelay, SoundSpecifier? sound = null, IEntityManager? entityManager = null)
- {
- entityManager ??= IoCManager.Resolve<IEntityManager>();
- var ent = entityManager.SpawnEntity(entityPrototype, coords.SnapToGrid());
-
- var areaEffectComponent = entityManager.GetComponentOrNull<FoamSolutionAreaEffectComponent>(ent);
-
- if (areaEffectComponent == null)
- {
- Logger.Error("Couldn't get AreaEffectComponent from " + entityPrototype);
- IoCManager.Resolve<IEntityManager>().QueueDeleteEntity(ent);
- return;
- }
-
- if (contents != null)
- areaEffectComponent.TryAddSolution(contents);
- areaEffectComponent.Start(amount, duration, spreadDelay, removeDelay);
-
- entityManager.EntitySysManager.GetEntitySystem<AudioSystem>()
- .PlayPvs(sound, ent, AudioParams.Default.WithVariation(0.125f));
- }
- }
-}
+++ /dev/null
-using Content.Server.Chemistry.Components;
-using JetBrains.Annotations;
-
-namespace Content.Server.Chemistry.ReactionEffects
-{
- [UsedImplicitly]
- [DataDefinition]
- public sealed class SmokeAreaReactionEffect : AreaReactionEffect
- {
- protected override SolutionAreaEffectComponent? GetAreaEffectComponent(EntityUid entity)
- {
- return IoCManager.Resolve<IEntityManager>().GetComponentOrNull<SmokeSolutionAreaEffectComponent>(entity);
- }
- }
-}
-using Content.Server.Fluids.EntitySystems;
+using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
{
public FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume)
{
- var spillSystem = EntitySystem.Get<SpillableSystem>();
- if (reactVolume < 5 || !spillSystem.TryGetPuddle(tile, out _)) return FixedPoint2.Zero;
+ var spillSystem = EntitySystem.Get<PuddleSystem>();
+ if (reactVolume < 5 || !spillSystem.TryGetPuddle(tile, out _))
+ return FixedPoint2.Zero;
- return spillSystem.SpillAt(tile,new Solution(reagent.ID, reactVolume), "PuddleSmear", true, false, true) != null
+ return spillSystem.TrySpillAt(tile, new Solution(reagent.ID, reactVolume), out _, sound: false, tileReact: false)
? reactVolume
: FixedPoint2.Zero;
}
[DataField("launchForwardsMultiplier")] private float _launchForwardsMultiplier = 1;
[DataField("requiredSlipSpeed")] private float _requiredSlipSpeed = 6;
[DataField("paralyzeTime")] private float _paralyzeTime = 1;
- [DataField("overflow")] private bool _overflow;
public FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume)
{
var entityManager = IoCManager.Resolve<IEntityManager>();
- // TODO Make this not puddle smear.
- var puddle = entityManager.EntitySysManager.GetEntitySystem<SpillableSystem>()
- .SpillAt(tile, new Solution(reagent.ID, reactVolume), "PuddleSmear", _overflow, false, true);
-
- if (puddle != null)
+ if (entityManager.EntitySysManager.GetEntitySystem<PuddleSystem>()
+ .TrySpillAt(tile, new Solution(reagent.ID, reactVolume), out var puddleUid, false, false))
{
- var slippery = entityManager.EnsureComponent<SlipperyComponent>(puddle.Owner);
+ var slippery = entityManager.EnsureComponent<SlipperyComponent>(puddleUid);
slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier;
slippery.ParalyzeTime = _paralyzeTime;
entityManager.Dirty(slippery);
- var step = entityManager.EnsureComponent<StepTriggerComponent>(puddle.Owner);
- entityManager.EntitySysManager.GetEntitySystem<StepTriggerSystem>().SetRequiredTriggerSpeed(puddle.Owner, _requiredSlipSpeed, step);
+ var step = entityManager.EnsureComponent<StepTriggerComponent>(puddleUid);
+ entityManager.EntitySysManager.GetEntitySystem<StepTriggerSystem>().SetRequiredTriggerSpeed(puddleUid, _requiredSlipSpeed, step);
return reactVolume;
}
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly SpillableSystem _spillableSystem = default!;
+ [Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly MaterialStorageSystem _material = default!;
if (_robustRandom.Prob(0.2f))
i++;
}
- _spillableSystem.SpillAt(uid, bloodSolution, "PuddleBlood");
+ _puddleSystem.TrySpillAt(uid, bloodSolution, out _);
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
[Dependency] public readonly StackSystem StackSystem = default!;
[Dependency] public readonly TriggerSystem TriggerSystem = default!;
[Dependency] public readonly SolutionContainerSystem SolutionContainerSystem = default!;
- [Dependency] public readonly SpillableSystem SpillableSystem = default!;
+ [Dependency] public readonly PuddleSystem PuddleSystem = default!;
[Dependency] public readonly IPrototypeManager PrototypeManager = default!;
[Dependency] public readonly IComponentFactory ComponentFactory = default!;
// Spill the solution out into the world
// Spill before exploding in anticipation of a future where the explosion can light the solution on fire.
var coordinates = system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates;
- system.SpillableSystem.SpillAt(explodingSolution, coordinates, "PuddleSmear", combine: true);
+ system.PuddleSystem.TrySpillAt(coordinates, explodingSolution, out _);
// Explode
// Don't delete the object here - let other processes like physical damage from the
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
{
var solutionContainerSystem = EntitySystem.Get<SolutionContainerSystem>();
- var spillableSystem = EntitySystem.Get<SpillableSystem>();
+ var spillableSystem = EntitySystem.Get<PuddleSystem>();
var coordinates = system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates;
solutionContainerSystem.TryGetSolution(owner, spillableComponent.SolutionName,
out var compSolution))
{
- spillableSystem.SplashSpillAt(owner, compSolution, coordinates, "PuddleSmear", false, user: cause);
+ spillableSystem.TrySplashSpillAt(owner, coordinates, compSolution, out _, false, user: cause);
}
else if (Solution != null &&
solutionContainerSystem.TryGetSolution(owner, Solution, out var behaviorSolution))
{
- spillableSystem.SplashSpillAt(owner, behaviorSolution, coordinates, "PuddleSmear", user: cause);
+ spillableSystem.TrySplashSpillAt(owner, coordinates, behaviorSolution, out _, user: cause);
}
}
}
"ClientEntitySpawner",
"HandheldGPS",
"CableVisualizer",
- "PuddleVisualizer",
"UIFragment",
"PDABorderColor",
};
+++ /dev/null
-namespace Content.Server.Fluids.Components
-{
- [RegisterComponent]
- public sealed class DrainComponent : Component
- {
- public const string SolutionName = "drainBuffer";
-
- [DataField("accumulator")]
- public float Accumulator = 0f;
-
- /// <summary>
- /// How many units per second the drain can absorb from the surrounding puddles.
- /// Divided by puddles, so if there are 5 puddles this will take 1/5 from each puddle.
- /// This will stay fixed to 1 second no matter what DrainFrequency is.
- /// </summary>
- [DataField("unitsPerSecond")]
- public float UnitsPerSecond = 6f;
-
- /// <summary>
- /// How many units are ejected from the buffer per second.
- /// </summary>
- [DataField("unitsDestroyedPerSecond")]
- public float UnitsDestroyedPerSecond = 1f;
-
- /// <summary>
- /// How many (unobstructed) tiles away the drain will
- /// drain puddles from.
- /// </summary>
- [DataField("range")]
- public float Range = 2f;
-
- /// <summary>
- /// How often in seconds the drain checks for puddles around it.
- /// If the EntityQuery seems a bit unperformant this can be increased.
- /// </summary>
- [DataField("drainFrequency")]
- public float DrainFrequency = 1f;
- }
-}
using Content.Server.Fluids.EntitySystems;
using Content.Shared.FixedPoint;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-namespace Content.Server.Fluids.Components
-{
- [RegisterComponent]
- [Access(typeof(EvaporationSystem))]
- public sealed class EvaporationComponent : Component
- {
- /// <summary>
- /// Is this entity actively evaporating? This toggle lets us pause evaporation under certain conditions.
- /// </summary>
- [DataField("evaporationToggle")]
- public bool EvaporationToggle = true;
-
- /// <summary>
- /// The time that it will take this puddle to lose one fixed unit of solution, in seconds.
- /// </summary>
- [DataField("evaporateTime")]
- public float EvaporateTime { get; set; } = 5f;
-
- /// <summary>
- /// Name of referenced solution. Defaults to <see cref="PuddleComponent.DefaultSolutionName"/>
- /// </summary>
- [DataField("solution")]
- public string SolutionName { get; set; } = PuddleComponent.DefaultSolutionName;
+namespace Content.Server.Fluids.Components;
- /// <summary>
- /// Lower limit below which puddle won't evaporate. Useful when wanting to leave a stain.
- /// Defaults to evaporate completely.
- /// </summary>
- [DataField("lowerLimit")]
- public FixedPoint2 LowerLimit = FixedPoint2.Zero;
-
- /// <summary>
- /// Upper limit above which puddle won't evaporate. Useful when wanting to make sure large puddle will
- /// remain forever. Defaults to 100.
- /// </summary>
- [DataField("upperLimit")]
- public FixedPoint2 UpperLimit = FixedPoint2.New(100); //TODO: Consider setting this back to PuddleComponent.DefaultOverflowVolume once that behaviour is fixed.
+/// <summary>
+/// Added to puddles that contain water so it may evaporate over time.
+/// </summary>
+[RegisterComponent, Access(typeof(PuddleSystem))]
+public sealed class EvaporationComponent : Component
+{
+ /// <summary>
+ /// The next time we remove the EvaporationSystem reagent amount from this entity.
+ /// </summary>
+ [ViewVariables(VVAccess.ReadWrite), DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))]
+ public TimeSpan NextTick = TimeSpan.Zero;
- /// <summary>
- /// The time accumulated since the start.
- /// </summary>
- [DataField("accumulator")]
- public float Accumulator = 0f;
- }
+ /// <summary>
+ /// How much evaporation occurs every tick.
+ /// </summary>
+ [DataField("evaporationAmount")]
+ public FixedPoint2 EvaporationAmount = FixedPoint2.New(0.3);
}
--- /dev/null
+namespace Content.Server.Fluids.Components;
+
+/// <summary>
+/// Used to track evaporation sparkles so we can delete if necessary.
+/// </summary>
+[RegisterComponent]
+public sealed class EvaporationSparkleComponent : Component
+{
+
+}
+++ /dev/null
-using Content.Server.Fluids.EntitySystems;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-
-namespace Content.Server.Fluids.Components;
-
-[RegisterComponent]
-[Access(typeof(FluidSpreaderSystem))]
-public sealed class FluidMapDataComponent : Component
-{
- /// <summary>
- /// At what time will <see cref="FluidSpreaderSystem"/> be checked next
- /// </summary>
- [DataField("goalTime", customTypeSerializer:typeof(TimeOffsetSerializer))]
- public TimeSpan GoalTime;
-
- /// <summary>
- /// Delay between two runs of <see cref="FluidSpreaderSystem"/>
- /// </summary>
- [DataField("delay")]
- public TimeSpan Delay = TimeSpan.FromSeconds(2);
-
- /// <summary>
- /// Puddles to be expanded.
- /// </summary>
- [DataField("puddles")] public HashSet<EntityUid> Puddles = new();
-
- /// <summary>
- /// Convenience method for setting GoalTime to <paramref name="start"/> + <see cref="Delay"/>
- /// </summary>
- /// <param name="start">Time to which to add <see cref="Delay"/>, defaults to current <see cref="GoalTime"/></param>
- public void UpdateGoal(TimeSpan? start = null)
- {
- GoalTime = (start ?? GoalTime) + Delay;
- }
-}
--- /dev/null
+namespace Content.Server.Fluids.Components;
+
+[RegisterComponent]
+public sealed class FootstepTrackComponent : Component
+{
+
+}
+++ /dev/null
-using Content.Server.Fluids.EntitySystems;
-using Content.Shared.FixedPoint;
-using Robust.Shared.Audio;
-
-namespace Content.Server.Fluids.Components
-{
- /// <summary>
- /// Puddle on a floor
- /// </summary>
- [RegisterComponent]
- [Access(typeof(PuddleSystem))]
- public sealed class PuddleComponent : Component
- {
- public const string DefaultSolutionName = "puddle";
- private static readonly FixedPoint2 DefaultSlipThreshold = FixedPoint2.New(-1); //Not slippery by default. Set specific slipThresholds in YAML if you want your puddles to be slippery. Lower = more slippery, and zero means any volume can slip.
- public static readonly FixedPoint2 DefaultOverflowVolume = FixedPoint2.New(20);
-
- // Current design: Something calls the SpillHelper.Spill, that will either
- // A) Add to an existing puddle at the location (normalised to tile-center) or
- // B) add a new one
- // From this every time a puddle is spilt on it will try and overflow to its neighbours if possible,
- // and also update its appearance based on volume level (opacity) and chemistry color
- // Small puddles will evaporate after a set delay
-
- // TODO: 'leaves fluidtracks', probably in a separate component for stuff like gibb chunks?;
-
- // based on behaviour (e.g. someone being punched vs slashed with a sword would have different blood sprite)
- // to check for low volumes for evaporation or whatever
-
- /// <summary>
- /// Puddles with volume above this threshold can slip players.
- /// </summary>
- [DataField("slipThreshold")]
- public FixedPoint2 SlipThreshold = DefaultSlipThreshold;
-
- [DataField("spillSound")]
- public SoundSpecifier SpillSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
-
- [DataField("overflowVolume")]
- public FixedPoint2 OverflowVolume = DefaultOverflowVolume;
-
- /// <summary>
- /// How much should this puddle's opacity be multiplied by?
- /// Useful for puddles that have a high overflow volume but still want to be mostly opaque.
- /// </summary>
- [DataField("opacityModifier")] public float OpacityModifier = 1.0f;
-
- [DataField("solution")] public string SolutionName { get; set; } = DefaultSolutionName;
- }
-}
--- /dev/null
+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;
+ }
+}
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
+++ /dev/null
-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;
- }
- }
-}
+++ /dev/null
-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;
- }
-}
+++ /dev/null
-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;
- }
-}
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;
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+ }
+ }
+}
+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();
+
+ // 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);
+
+ SubscribeLocalEvent<EvaporationComponent, MapInitEvent>(OnEvaporationMapInit);
- public static float PuddleVolume = 1000;
+ InitializeSpillable();
+ InitializeTransfers();
+ }
- // 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();
+ private void OnPuddleSpread(EntityUid uid, PuddleComponent component, ref SpreadNeighborsEvent args)
+ {
+ var overflow = GetOverflowSolution(uid, component);
- public override void Initialize()
+ if (overflow.Volume == FixedPoint2.Zero)
{
- base.Initialize();
+ RemCompDeferred<EdgeSpreaderComponent>(uid);
+ return;
+ }
- // 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);
+ var xform = Transform(uid);
+
+ if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
+ {
+ RemCompDeferred<EdgeSpreaderComponent>(uid);
+ return;
}
- public override void Update(float frameTime)
+ 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)
{
- base.Update(frameTime);
- foreach (var ent in _deletionQueue)
+ _random.Shuffle(args.Neighbors);
+
+ // Overflow to neighbors with remaining space.
+ foreach (var neighbor in args.Neighbors)
+ {
+ 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;
+ }
+
+ if (overflow.Volume == FixedPoint2.Zero)
{
- Del(ent);
+ RemCompDeferred<EdgeSpreaderComponent>(uid);
+ return;
}
- _deletionQueue.Clear();
}
- private void OnPuddleInit(EntityUid uid, PuddleComponent component, ComponentInit args)
+ if (args.NeighborFreeTiles.Count > 0 && args.Updates > 0)
{
- _solutionContainerSystem.EnsureSolution(uid, component.SolutionName, FixedPoint2.New(PuddleVolume), out _);
+ _random.Shuffle(args.NeighborFreeTiles);
+ var spillAmount = overflow.Volume / args.NeighborFreeTiles.Count;
+
+ foreach (var tile in args.NeighborFreeTiles)
+ {
+ var split = overflow.SplitSolution(spillAmount);
+ TrySpillAt(grid.GridTileToLocal(tile), split, out _, false);
+ args.Updates--;
+
+ if (args.Updates <= 0)
+ break;
+ }
+
+ RemCompDeferred<EdgeSpreaderComponent>(uid);
+ return;
}
- private void OnPuddleSlip(EntityUid uid, PuddleComponent component, ref SlipEvent args)
+ if (overflow.Volume > FixedPoint2.Zero && args.Neighbors.Count > 0 && args.Updates > 0)
{
- // 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;
+ var spillPerNeighbor = overflow.Volume / args.Neighbors.Count;
- // 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;
+ foreach (var neighbor in args.Neighbors)
+ {
+ // 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;
+ }
- if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
- return;
+ var split = overflow.SplitSolution(spillPerNeighbor);
+
+ if (!_solutionContainerSystem.TryAddSolution(neighbor, neighborSolution, split))
+ continue;
- _popup.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", uid)),
- args.Slipped, args.Slipped, PopupType.SmallCaution);
+ EnsureComp<EdgeSpreaderComponent>(neighbor);
+ args.Updates--;
- // Take 15% of the puddle solution
- var splitSol = _solutionContainerSystem.SplitSolution(uid, solution, solution.Volume * 0.15f);
- _reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch);
+ if (args.Updates <= 0)
+ break;
+ }
}
- private void OnSolutionUpdate(EntityUid uid, PuddleComponent component, SolutionChangedEvent args)
+ // Add the remainder back
+ if (_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var puddleSolution))
{
- if (args.Solution.Name != component.SolutionName)
- return;
+ _solutionContainerSystem.TryAddSolution(uid, puddleSolution, overflow);
+ }
+ }
- if (args.Solution.Volume <= 0)
- {
- _deletionQueue.Add(uid);
- return;
- }
+ 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();
+ }
- _deletionQueue.Remove(uid);
- UpdateSlip(uid, component);
- UpdateAppearance(uid, component);
+ 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;
}
- private void UpdateAppearance(EntityUid uid, PuddleComponent? puddleComponent = null, AppearanceComponent? appearance = null)
+ var volume = FixedPoint2.Zero;
+ Color color = Color.White;
+
+ if (_solutionContainerSystem.TryGetSolution(uid, puddleComponent.SolutionName, out var solution))
{
- if (!Resolve(uid, ref puddleComponent, ref appearance, false)
- || EmptyHolder(uid, puddleComponent))
+ 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)
{
- return;
+ 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);
}
+ }
- // 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;
+ _appearance.SetData(uid, PuddleVisuals.CurrentVolume, volume.Float(), appearance);
+ _appearance.SetData(uid, PuddleVisuals.SolutionColor, color, appearance);
+ }
- bool isEvaporating;
+ 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;
- if (TryComp(uid, out EvaporationComponent? evaporation)
- && evaporation.EvaporationToggle)// if puddle is evaporating.
+ foreach (var reagent in solution.Contents)
+ {
+ var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.ReagentId);
+
+ if (reagentProto.Slippery)
{
- isEvaporating = true;
+ slipperyAmount += reagent.Quantity;
+
+ if (slipperyAmount > amountRequired)
+ {
+ isSlippery = true;
+ break;
+ }
}
- else isEvaporating = false;
+ }
- var color = puddleSolution.GetColor(_protoMan);
+ if (isSlippery)
+ {
+ var comp = EnsureComp<StepTriggerComponent>(entityUid);
+ _stepTrigger.SetActive(entityUid, true, comp);
+ }
+ else if (TryComp<StepTriggerComponent>(entityUid, out var comp))
+ {
+ _stepTrigger.SetActive(entityUid, false, comp);
+ }
+ }
- _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);
+ 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"));
}
- private void UpdateSlip(EntityUid entityUid, PuddleComponent puddleComponent)
+ if (HasComp<EvaporationComponent>(uid))
{
- var vol = CurrentVolume(puddleComponent.Owner, puddleComponent);
- if ((puddleComponent.SlipThreshold == FixedPoint2.New(-1) ||
- vol < puddleComponent.SlipThreshold) &&
- TryComp(entityUid, out StepTriggerComponent? stepTrigger))
+ if (_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution) &&
+ CanFullyEvaporate(solution))
{
- _stepTrigger.SetActive(entityUid, false, stepTrigger);
+ args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating"));
}
- else if (vol >= puddleComponent.SlipThreshold)
+ else if (solution?.ContainsReagent(EvaporationReagent) == true)
{
- var comp = EnsureComp<StepTriggerComponent>(entityUid);
- _stepTrigger.SetActive(entityUid, true, comp);
+ args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-partial"));
}
- }
-
- private void HandlePuddleExamined(EntityUid uid, PuddleComponent component, ExaminedEvent args)
- {
- if (TryComp<StepTriggerComponent>(uid, out var slippery) && slippery.Active)
+ else
{
- args.PushText(Loc.GetString("puddle-component-examine-is-slipper-text"));
+ args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-no"));
}
}
-
- private void OnAnchorChanged(EntityUid uid, PuddleComponent puddle, ref AnchorStateChangedEvent args)
+ else
{
- if (!args.Anchored)
- QueueDel(uid);
+ args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-no"));
}
+ }
- public bool EmptyHolder(EntityUid uid, PuddleComponent? puddleComponent = null)
+ 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))
{
- if (!Resolve(uid, ref puddleComponent))
- return true;
+ return false;
+ }
+
+ solution.AddSolution(addedSolution, _prototypeManager);
+ _solutionContainerSystem.UpdateChemicals(puddleUid, solution, true);
- return !_solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName,
- out var solution)
- || solution.Contents.Count == 0;
+ if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent))
+ {
+ EnsureComp<EdgeSpreaderComponent>(puddleUid);
}
- public FixedPoint2 CurrentVolume(EntityUid uid, PuddleComponent? puddleComponent = null)
+ if (!sound)
{
- if (!Resolve(uid, ref puddleComponent))
- return FixedPoint2.Zero;
+ return true;
+ }
+
+ 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;
+ }
- return _solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName,
- out var solution)
- ? solution.Volume
- : FixedPoint2.Zero;
+ /// <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))
+ {
+ return new Solution(0);
}
- /// <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)
+ // 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;
+ }
+
+ #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))
{
- if (!Resolve(puddleUid, ref puddleComponent))
- return false;
+ // 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 (addedSolution.Volume == 0 ||
- !_solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName,
- out var solution))
+ if (user != null)
{
- return false;
+ _adminLogger.Add(LogType.Landed,
+ $"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}");
}
- solution.AddSolution(addedSolution, _protoMan);
- _solutionContainerSystem.UpdateChemicals(puddleUid, solution, true);
+ _reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
+ _popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other", ("spillable", uid), ("target", owner)), owner, PopupType.SmallCaution);
+ }
- if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent))
- {
- _fluidSpreaderSystem.AddOverflowingPuddle(puddleComponent.Owner, puddleComponent);
- }
+ return TrySpillAt(coordinates, solution, out puddleUid, sound);
+ }
- if (!sound)
- {
- return true;
- }
+ /// <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;
+ }
- SoundSystem.Play(puddleComponent.SpillSound.GetSound(),
- Filter.Pvs(puddleComponent.Owner), puddleComponent.Owner);
- return true;
+ if (!_mapManager.TryGetGrid(coordinates.GetGridUid(EntityManager), out var mapGrid))
+ {
+ puddleUid = EntityUid.Invalid;
+ return false;
}
- /// <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)
- {
- if (!Resolve(srcPuddle, ref sourcePuddleComponent)
- || !_solutionContainerSystem.TryGetSolution(srcPuddle, sourcePuddleComponent.SolutionName,
- out var srcSolution))
- return;
+ return TrySpillAt(mapGrid.GetTileRef(coordinates), solution, out puddleUid, sound);
+ }
- var dividedVolume = totalVolume / (destinationPuddles.Count + 1);
+ /// <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;
+ }
- foreach (var destPuddle in destinationPuddles)
- {
- if (!_solutionContainerSystem.TryGetSolution(destPuddle.Owner, destPuddle.SolutionName,
- out var destSolution))
- continue;
+ return TrySpillAt(transformComponent.Coordinates, solution, out puddleUid, sound: sound);
+ }
- 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);
- }
- }
+ /// <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 (stillOverflowing != null && srcSolution.Volume > sourcePuddleComponent.OverflowVolume)
+ if (tileReact)
+ {
+ // First, do all tile reactions
+ for (var i = 0; i < solution.Contents.Count; i++)
{
- stillOverflowing.Add(srcPuddle);
+ 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);
}
}
- /// <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)
+ // Tile reactions used up everything.
+ if (solution.Volume == FixedPoint2.Zero)
{
- if (!Resolve(uid, ref puddle))
- return false;
-
- return CurrentVolume(uid, puddle) + solution.Volume > puddle.OverflowVolume;
+ puddleUid = EntityUid.Invalid;
+ return false;
}
- /// <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)
+ // 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 (!Resolve(uid, ref puddle))
- return false;
+ // If there's existing sparkles then delete it
+ if (sparklesQuery.TryGetComponent(ent, out var sparkles))
+ {
+ QueueDel(ent.Value);
+ continue;
+ }
+
+ if (!puddleQuery.TryGetComponent(ent, out var puddle))
+ continue;
+
+ if (TryAddSolution(ent.Value, solution, sound, puddleComponent: puddle))
+ {
+ EnsureComp<EdgeSpreaderComponent>(ent.Value);
+ }
- 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)
+ var coords = mapGrid.GridTileToLocal(tileRef.GridIndices);
+ puddleUid = EntityManager.SpawnEntity("Puddle", coords);
+ EnsureComp<PuddleComponent>(puddleUid);
+ if (TryAddSolution(puddleUid, solution, sound))
{
- MetaDataComponent? metadata = null;
- Resolve(srcUid, ref metadata);
+ EnsureComp<EdgeSpreaderComponent>(puddleUid);
+ }
+ return true;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Tries to get the relevant puddle entity for a tile.
+ /// </summary>
+ public bool TryGetPuddle(TileRef tile, out EntityUid puddleUid)
+ {
+ puddleUid = EntityUid.Invalid;
- var prototype = metadata?.EntityPrototype?.ID ?? "PuddleSmear"; // TODO Spawn a entity based on another entity
+ if (!TryComp<MapGridComponent>(tile.GridUid, out var grid))
+ return false;
- uid = EntityManager.SpawnEntity(prototype, pos);
- component = EntityManager.EnsureComponent<PuddleComponent>(uid);
+ var anc = grid.GetAnchoredEntitiesEnumerator(tile.GridIndices);
+ var puddleQuery = GetEntityQuery<PuddleComponent>();
+
+ while (anc.MoveNext(out var ent))
+ {
+ if (!puddleQuery.HasComponent(ent.Value))
+ continue;
+
+ puddleUid = ent.Value;
+ return true;
}
+
+ return false;
}
}
--- /dev/null
+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);
+ }
+}
+++ /dev/null
-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;
- }
-}
+++ /dev/null
-namespace Content.Server.Kudzu;
-
-[RegisterComponent]
-public sealed class GrowingKudzuComponent : Component
-{
- [DataField("growthLevel")]
- public int GrowthLevel = 1;
-
- [DataField("growthTickSkipChance")]
- public float GrowthTickSkipChange = 0.0f;
-}
+++ /dev/null
-using Content.Shared.Kudzu;
-using Robust.Server.GameObjects;
-using Robust.Shared.Random;
-
-namespace Content.Server.Kudzu;
-
-public sealed class GrowingKudzuSystem : EntitySystem
-{
- [Dependency] private readonly IRobustRandom _robustRandom = default!;
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-
- private float _accumulatedFrameTime = 0.0f;
-
- public override void Initialize()
- {
- SubscribeLocalEvent<GrowingKudzuComponent, ComponentAdd>(SetupKudzu);
- }
-
- private void SetupKudzu(EntityUid uid, GrowingKudzuComponent component, ComponentAdd args)
- {
- if (!EntityManager.TryGetComponent<AppearanceComponent>(uid, out var appearance))
- {
- return;
- }
-
- _appearance.SetData(uid, KudzuVisuals.Variant, _robustRandom.Next(1, 3), appearance);
- _appearance.SetData(uid, KudzuVisuals.GrowthLevel, 1, appearance);
- }
-
- public override void Update(float frameTime)
- {
- _accumulatedFrameTime += frameTime;
-
- if (!(_accumulatedFrameTime >= 0.5f))
- return;
-
- _accumulatedFrameTime -= 0.5f;
-
- foreach (var (kudzu, appearance) in EntityManager.EntityQuery<GrowingKudzuComponent, AppearanceComponent>())
- {
- if (kudzu.GrowthLevel >= 3 || !_robustRandom.Prob(kudzu.GrowthTickSkipChange)) continue;
- kudzu.GrowthLevel += 1;
-
- if (kudzu.GrowthLevel == 3 &&
- EntityManager.TryGetComponent<SpreaderComponent>((kudzu).Owner, out var spreader))
- {
- // why cache when you can simply cease to be? Also saves a bit of memory/time.
- EntityManager.RemoveComponent<GrowingKudzuComponent>((kudzu).Owner);
- }
-
- _appearance.SetData(kudzu.Owner, KudzuVisuals.GrowthLevel, kudzu.GrowthLevel, appearance);
- }
- }
-}
+++ /dev/null
-namespace Content.Server.Kudzu;
-
-/// <summary>
-/// Component for rapidly spreading objects, like Kudzu.
-/// ONLY USE THIS FOR ANCHORED OBJECTS. An error will be logged if not anchored/static.
-/// Currently does not support growing in space.
-/// </summary>
-[RegisterComponent, Access(typeof(SpreaderSystem))]
-public sealed class SpreaderComponent : Component
-{
- /// <summary>
- /// Chance for it to grow on any given tick, after the normal growth rate-limit (if it doesn't grow, SpreaderSystem will pick another one.).
- /// </summary>
- [DataField("chance", required: true)]
- public float Chance;
-
- /// <summary>
- /// Prototype spawned on growth success.
- /// </summary>
- [DataField("growthResult", required: true)]
- public string GrowthResult = default!;
-
- [DataField("enabled")]
- public bool Enabled = true;
-}
+++ /dev/null
-using System.Linq;
-using Content.Server.Atmos.Components;
-using Content.Server.Atmos.EntitySystems;
-using Content.Shared.Atmos;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Random;
-
-namespace Content.Server.Kudzu;
-
-// Future work includes making the growths per interval thing not global, but instead per "group"
-public sealed class SpreaderSystem : EntitySystem
-{
- [Dependency] private readonly IRobustRandom _robustRandom = default!;
- [Dependency] private readonly IMapManager _mapManager = default!;
-
- /// <summary>
- /// Maximum number of edges that can grow out every interval.
- /// </summary>
- private const int GrowthsPerInterval = 1;
-
- private float _accumulatedFrameTime = 0.0f;
-
- private readonly HashSet<EntityUid> _edgeGrowths = new ();
-
- public override void Initialize()
- {
- SubscribeLocalEvent<SpreaderComponent, ComponentAdd>(SpreaderAddHandler);
- SubscribeLocalEvent<AirtightChanged>(OnAirtightChanged);
- }
-
- private void OnAirtightChanged(ref AirtightChanged ev)
- {
- UpdateNearbySpreaders(ev.Entity, ev.Airtight);
- }
-
- private void SpreaderAddHandler(EntityUid uid, SpreaderComponent component, ComponentAdd args)
- {
- if (component.Enabled)
- _edgeGrowths.Add(uid); // ez
- }
-
- public void UpdateNearbySpreaders(EntityUid blocker, AirtightComponent comp)
- {
- if (!EntityManager.TryGetComponent<TransformComponent>(blocker, out var transform))
- return; // how did we get here?
-
- if (!_mapManager.TryGetGrid(transform.GridUid, out var grid)) return;
-
- var spreaderQuery = GetEntityQuery<SpreaderComponent>();
- var tile = grid.TileIndicesFor(transform.Coordinates);
-
- for (var i = 0; i < Atmospherics.Directions; i++)
- {
- var direction = (AtmosDirection) (1 << i);
- if (!comp.AirBlockedDirection.IsFlagSet(direction)) continue;
-
- var directionEnumerator =
- grid.GetAnchoredEntitiesEnumerator(SharedMapSystem.GetDirection(tile, direction.ToDirection()));
-
- while (directionEnumerator.MoveNext(out var ent))
- {
- if (spreaderQuery.TryGetComponent(ent, out var s) && s.Enabled)
- _edgeGrowths.Add(ent.Value);
- }
- }
- }
-
- public override void Update(float frameTime)
- {
- _accumulatedFrameTime += frameTime;
-
- if (!(_accumulatedFrameTime >= 1.0f))
- return;
-
- _accumulatedFrameTime -= 1.0f;
-
- var growthList = _edgeGrowths.ToList();
- _robustRandom.Shuffle(growthList);
-
- var successes = 0;
- foreach (var entity in growthList)
- {
- if (!TryGrow(entity)) continue;
-
- successes += 1;
- if (successes >= GrowthsPerInterval)
- break;
- }
- }
-
- private bool TryGrow(EntityUid ent, TransformComponent? transform = null, SpreaderComponent? spreader = null)
- {
- if (!Resolve(ent, ref transform, ref spreader, false))
- return false;
-
- if (spreader.Enabled == false) return false;
-
- if (!_mapManager.TryGetGrid(transform.GridUid, out var grid)) return false;
-
- var didGrow = false;
-
- for (var i = 0; i < 4; i++)
- {
- var direction = (DirectionFlag) (1 << i);
- var coords = transform.Coordinates.Offset(direction.AsDir().ToVec());
- if (grid.GetTileRef(coords).Tile.IsEmpty || _robustRandom.Prob(1 - spreader.Chance)) continue;
- var ents = grid.GetLocal(coords);
-
- if (ents.Any(x => IsTileBlockedFrom(x, direction))) continue;
-
- // Ok, spawn a plant
- didGrow = true;
- EntityManager.SpawnEntity(spreader.GrowthResult, transform.Coordinates.Offset(direction.AsDir().ToVec()));
- }
-
- return didGrow;
- }
-
- public void EnableSpreader(EntityUid ent, SpreaderComponent? component = null)
- {
- if (!Resolve(ent, ref component))
- return;
- component.Enabled = true;
- _edgeGrowths.Add(ent);
- }
-
- private bool IsTileBlockedFrom(EntityUid ent, DirectionFlag dir)
- {
- if (EntityManager.TryGetComponent<SpreaderComponent>(ent, out _))
- return true;
-
- if (!EntityManager.TryGetComponent<AirtightComponent>(ent, out var airtight))
- return false;
-
- var oppositeDir = dir.AsDir().GetOpposite().ToAtmosDirection();
-
- return airtight.AirBlocked && airtight.AirBlockedDirection.IsFlagSet(oppositeDir);
- }
-}
[Dependency] private readonly SharedAudioSystem _sharedAudioSystem = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
- [Dependency] private readonly SpillableSystem _spillableSystem = default!;
+ [Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
{
Solution blood = new();
blood.AddReagent(reclaimer.BloodReagent, 50);
- _spillableSystem.SpillAt(reclaimer.Owner, blood, "PuddleBlood");
+ _puddleSystem.TrySpillAt(reclaimer.Owner, blood, out _);
}
if (_robustRandom.Prob(0.03f) && reclaimer.SpawnedEntities.Count > 0)
{
using Content.Server.Nutrition.EntitySystems;
using Content.Server.Popups;
using Content.Server.Stunnable;
+using Content.Shared.Audio;
+using Content.Shared.Fluids.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
_mapManager.TryGetGrid(xform.GridUid, out var grid);
if (!node.Connectable(EntityManager, xform))
- yield break;
+ yield break;
foreach (var reachable in node.GetReachableNodes(xform, nodeQuery, xformQuery, grid, EntityManager))
{
Apc,
AMEngine,
Pipe,
- WireNet
+ WireNet,
+ Spreader,
}
}
public sealed class CreamPieSystem : SharedCreamPieSystem
{
[Dependency] private readonly SolutionContainerSystem _solutions = default!;
- [Dependency] private readonly SpillableSystem _spillable = default!;
+ [Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly TriggerSystem _trigger = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
{
if (_solutions.TryGetSolution(uid, foodComp.SolutionName, out var solution))
{
- _spillable.SpillAt(uid, solution, "PuddleSmear", false);
+ _puddle.TrySpillAt(uid, solution, out _, false);
}
if (!string.IsNullOrEmpty(foodComp.TrashPrototype))
{
private void ActivatePayload(EntityUid uid)
{
- if (_itemSlots.TryGetSlot(uid, CreamPieComponent.PayloadSlotName, out var itemSlot))
+ if (_itemSlots.TryGetSlot(uid, CreamPieComponent.PayloadSlotName, out var itemSlot))
{
- if (_itemSlots.TryEject(uid, itemSlot, user: null, out var item))
+ if (_itemSlots.TryEject(uid, itemSlot, user: null, out var item))
{
if (TryComp<OnUseTimerTriggerComponent>(item.Value, out var timerTrigger))
{
{
_popup.PopupEntity(Loc.GetString("cream-pied-component-on-hit-by-message",("thrower", args.Thrown)), uid, args.Target);
var otherPlayers = Filter.Empty().AddPlayersByPvs(uid);
- if (TryComp<ActorComponent>(args.Target, out var actor))
+ if (TryComp<ActorComponent>(args.Target, out var actor))
{
otherPlayers.RemovePlayer(actor.PlayerSession);
}
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry;
+using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.DoAfter;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
- [Dependency] private readonly SpillableSystem _spillableSystem = default!;
+ [Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
args.Message.AddMarkup($"\n{Loc.GetString("drink-component-on-examine-details-text", ("colorName", color), ("text", openedText))}");
if (!IsEmpty(uid, component))
{
- if (TryComp<ExaminableSolutionComponent>(component.Owner, out var comp))
+ if (TryComp<ExaminableSolutionComponent>(uid, out var comp))
{
//provide exact measurement for beakers
args.Message.AddMarkup($" - {Loc.GetString("drink-component-on-examine-exact-volume", ("amount", _solutionContainerSystem.DrainAvailable(uid)))}");
UpdateAppearance(component);
var solution = _solutionContainerSystem.Drain(uid, interactions, interactions.Volume);
- _spillableSystem.SpillAt(uid, solution, "PuddleSmear");
+ _puddleSystem.TrySpillAt(uid, solution, out _);
_audio.PlayPvs(_audio.GetSound(component.BurstSound), uid, AudioParams.Default.WithVolume(-4));
}
if (HasComp<RefillableSolutionComponent>(args.Args.Target.Value))
{
- _spillableSystem.SpillAt(args.Args.User, drained, "PuddleSmear");
+ _puddleSystem.TrySpillAt(args.Args.User, drained, out _);
args.Handled = true;
return;
}
if (forceDrink)
{
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough-other"), args.Args.Target.Value, args.Args.User);
- _spillableSystem.SpillAt(args.Args.Target.Value, drained, "PuddleSmear");
+ _puddleSystem.TrySpillAt(args.Args.Target.Value, drained, out _);
}
else
_solutionContainerSystem.TryAddSolution(uid, solution, drained);
--- /dev/null
+namespace Content.Server.Spreader;
+
+/// <summary>
+/// Added to entities being considered for spreading via <see cref="SpreaderSystem"/>.
+/// This needs to be manually added and removed.
+/// </summary>
+[RegisterComponent, Access(typeof(SpreaderSystem))]
+public sealed class EdgeSpreaderComponent : Component
+{
+}
--- /dev/null
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Spreader;
+
+/// <summary>
+/// Adds this node group to <see cref="SpreaderSystem"/> for tick updates.
+/// </summary>
+[Prototype("edgeSpreader")]
+public sealed class EdgeSpreaderPrototype : IPrototype
+{
+ [IdDataField] public string ID { get; } = string.Empty;
+}
--- /dev/null
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.Spreader;
+
+[RegisterComponent, Access(typeof(KudzuSystem))]
+public sealed class GrowingKudzuComponent : Component
+{
+ /// <summary>
+ /// At level 3 spreading can occur; prior to that we have a chance of increasing our growth level and changing our sprite.
+ /// </summary>
+ [DataField("growthLevel")]
+ public int GrowthLevel = 1;
+
+ [DataField("growthTickChance")]
+ public float GrowthTickChance = 1f;
+
+ /// <summary>
+ /// The next time kudzu will try to tick its growth level.
+ /// </summary>
+ [DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))]
+ public TimeSpan NextTick = TimeSpan.Zero;
+}
--- /dev/null
+namespace Content.Server.Spreader;
+
+/// <summary>
+/// Handles entities that spread out when they reach the relevant growth level.
+/// </summary>
+[RegisterComponent]
+public sealed class KudzuComponent : Component
+{
+ /// <summary>
+ /// Chance to spread whenever an edge spread is possible.
+ /// </summary>
+ [DataField("spreadChance")]
+ public float SpreadChance = 1f;
+}
--- /dev/null
+using Content.Shared.Kudzu;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Spreader;
+
+public sealed class KudzuSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IRobustRandom _robustRandom = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+ private const string KudzuGroup = "kudzu";
+
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ SubscribeLocalEvent<KudzuComponent, ComponentStartup>(SetupKudzu);
+ SubscribeLocalEvent<KudzuComponent, SpreadNeighborsEvent>(OnKudzuSpread);
+ SubscribeLocalEvent<GrowingKudzuComponent, EntityUnpausedEvent>(OnKudzuUnpaused);
+ SubscribeLocalEvent<SpreadGroupUpdateRate>(OnKudzuUpdateRate);
+ }
+
+ private void OnKudzuSpread(EntityUid uid, KudzuComponent component, ref SpreadNeighborsEvent args)
+ {
+ if (TryComp<GrowingKudzuComponent>(uid, out var growing) && growing.GrowthLevel < 3)
+ {
+ return;
+ }
+
+ if (args.NeighborFreeTiles.Count == 0 || args.Grid == null)
+ {
+ RemCompDeferred<EdgeSpreaderComponent>(uid);
+ return;
+ }
+
+ var prototype = MetaData(uid).EntityPrototype?.ID;
+
+ if (prototype == null)
+ {
+ RemCompDeferred<EdgeSpreaderComponent>(uid);
+ return;
+ }
+
+ if (!_robustRandom.Prob(component.SpreadChance))
+ return;
+
+ foreach (var neighbor in args.NeighborFreeTiles)
+ {
+ var neighborUid = Spawn(prototype, args.Grid.GridTileToLocal(neighbor));
+ EnsureComp<EdgeSpreaderComponent>(neighborUid);
+ args.Updates--;
+
+ if (args.Updates <= 0)
+ return;
+ }
+ }
+
+ private void OnKudzuUpdateRate(ref SpreadGroupUpdateRate args)
+ {
+ if (args.Name != KudzuGroup)
+ return;
+
+ args.UpdatesPerSecond = 1;
+ }
+
+ private void OnKudzuUnpaused(EntityUid uid, GrowingKudzuComponent component, ref EntityUnpausedEvent args)
+ {
+ component.NextTick += args.PausedTime;
+ }
+
+ private void SetupKudzu(EntityUid uid, KudzuComponent component, ComponentStartup args)
+ {
+ if (!EntityManager.TryGetComponent<AppearanceComponent>(uid, out var appearance))
+ {
+ return;
+ }
+
+ _appearance.SetData(uid, KudzuVisuals.Variant, _robustRandom.Next(1, 3), appearance);
+ _appearance.SetData(uid, KudzuVisuals.GrowthLevel, 1, appearance);
+ }
+
+ /// <inheritdoc/>
+ public override void Update(float frameTime)
+ {
+ var query = EntityQueryEnumerator<GrowingKudzuComponent, AppearanceComponent>();
+ var curTime = _timing.CurTime;
+
+ while (query.MoveNext(out var uid, out var kudzu, out var appearance))
+ {
+ if (kudzu.NextTick > curTime)
+ {
+ continue;
+ }
+
+ kudzu.NextTick = curTime + TimeSpan.FromSeconds(0.5);
+
+ if (!_robustRandom.Prob(kudzu.GrowthTickChance))
+ {
+ continue;
+ }
+
+ kudzu.GrowthLevel += 1;
+
+ if (kudzu.GrowthLevel >= 3)
+ {
+ // why cache when you can simply cease to be? Also saves a bit of memory/time.
+ RemCompDeferred<GrowingKudzuComponent>(uid);
+ }
+
+ _appearance.SetData(uid, KudzuVisuals.GrowthLevel, kudzu.GrowthLevel, appearance);
+ }
+ }
+}
--- /dev/null
+namespace Content.Server.Spreader;
+
+/// <summary>
+/// Raised every tick to determine how many updates a particular spreading node group is allowed.
+/// </summary>
+[ByRefEvent]
+public record struct SpreadGroupUpdateRate(string Name, int UpdatesPerSecond = 16);
\ No newline at end of file
--- /dev/null
+using Robust.Shared.Collections;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Spreader;
+
+/// <summary>
+/// Raised when trying to spread to neighboring tiles.
+/// If the spread is no longer able to happen you MUST cancel this event!
+/// </summary>
+[ByRefEvent]
+public record struct SpreadNeighborsEvent
+{
+ public MapGridComponent? Grid;
+ public ValueList<Vector2i> NeighborFreeTiles;
+ public ValueList<Vector2i> NeighborOccupiedTiles;
+ public ValueList<EntityUid> Neighbors;
+
+ /// <summary>
+ /// How many updates allowed are remaining.
+ /// Subscribers can handle as they wish.
+ /// </summary>
+ public int Updates;
+}
\ No newline at end of file
--- /dev/null
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.Spreader;
+
+[RegisterComponent]
+public sealed class SpreaderGridComponent : Component
+{
+ [DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))]
+ public TimeSpan NextUpdate = TimeSpan.Zero;
+}
--- /dev/null
+using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Spreader;
+
+/// <summary>
+/// Handles the node for <see cref="EdgeSpreaderComponent"/>.
+/// Functions as a generic tile-based entity spreader for systems such as puddles or smoke.
+/// </summary>
+public sealed class SpreaderNode : Node
+{
+ /// <inheritdoc/>
+ public override IEnumerable<Node> GetReachableNodes(TransformComponent xform, EntityQuery<NodeContainerComponent> nodeQuery, EntityQuery<TransformComponent> xformQuery,
+ MapGridComponent? grid, IEntityManager entMan)
+ {
+ if (grid == null)
+ yield break;
+
+ entMan.System<SpreaderSystem>().GetNeighbors(xform.Owner, Name, out _, out _, out var neighbors);
+
+ foreach (var neighbor in neighbors)
+ {
+ if (!nodeQuery.TryGetComponent(neighbor, out var nodeContainer) ||
+ !nodeContainer.TryGetNode<SpreaderNode>(Name, out var neighborNode))
+ {
+ continue;
+ }
+
+ yield return neighborNode;
+ }
+ }
+}
--- /dev/null
+using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.NodeContainer.Nodes;
+
+namespace Content.Server.Spreader;
+
+[NodeGroup(NodeGroupID.Spreader)]
+public sealed class SpreaderNodeGroup : BaseNodeGroup
+{
+ private IEntityManager _entManager = default!;
+
+ /// <inheritdoc/>
+ public override void Initialize(Node sourceNode, IEntityManager entMan)
+ {
+ base.Initialize(sourceNode, entMan);
+ _entManager = entMan;
+ }
+
+ /// <inheritdoc/>
+ public override void RemoveNode(Node node)
+ {
+ base.RemoveNode(node);
+
+ foreach (var neighborNode in node.ReachableNodes)
+ {
+ if (_entManager.Deleted(neighborNode.Owner))
+ continue;
+
+ _entManager.EnsureComponent<EdgeSpreaderComponent>(neighborNode.Owner);
+ }
+ }
+}
--- /dev/null
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.NodeGroups;
+using Content.Shared.Atmos;
+using Robust.Shared.Collections;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Spreader;
+
+/// <summary>
+/// Handles generic spreading logic, where one anchored entity spreads to neighboring tiles.
+/// </summary>
+public sealed class SpreaderSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IRobustRandom _robustRandom = default!;
+
+ private static readonly TimeSpan SpreadCooldown = TimeSpan.FromSeconds(1);
+
+ private readonly List<string> _spreaderGroups = new();
+
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ SubscribeLocalEvent<AirtightChanged>(OnAirtightChanged);
+ SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
+ SubscribeLocalEvent<SpreaderGridComponent, EntityUnpausedEvent>(OnGridUnpaused);
+
+ SetupPrototypes();
+ _prototype.PrototypesReloaded += OnPrototypeReload;
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _prototype.PrototypesReloaded -= OnPrototypeReload;
+ }
+
+ private void OnPrototypeReload(PrototypesReloadedEventArgs obj)
+ {
+ if (!obj.ByType.ContainsKey(typeof(EdgeSpreaderPrototype)))
+ return;
+
+ SetupPrototypes();
+ }
+
+ private void SetupPrototypes()
+ {
+ _spreaderGroups.Clear();
+
+ foreach (var id in _prototype.EnumeratePrototypes<EdgeSpreaderPrototype>())
+ {
+ _spreaderGroups.Add(id.ID);
+ }
+ }
+
+ private void OnAirtightChanged(ref AirtightChanged ev)
+ {
+ var neighbors = GetNeighbors(ev.Entity, ev.Airtight);
+
+ foreach (var neighbor in neighbors)
+ {
+ EnsureComp<EdgeSpreaderComponent>(neighbor);
+ }
+ }
+
+ private void OnGridUnpaused(EntityUid uid, SpreaderGridComponent component, ref EntityUnpausedEvent args)
+ {
+ component.NextUpdate += args.PausedTime;
+ }
+
+ private void OnGridInit(GridInitializeEvent ev)
+ {
+ var comp = EnsureComp<SpreaderGridComponent>(ev.EntityUid);
+ var nextUpdate = _timing.CurTime;
+
+ // TODO: I believe we need grid mapinit events so we can set the time correctly only on mapinit
+ // and not touch it on regular init.
+ if (comp.NextUpdate < nextUpdate)
+ comp.NextUpdate = nextUpdate;
+ }
+
+ /// <inheritdoc/>
+ public override void Update(float frameTime)
+ {
+ var curTime = _timing.CurTime;
+
+ // Check which grids are valid for spreading.
+ var spreadable = new ValueList<EntityUid>();
+ var spreadGrids = EntityQueryEnumerator<SpreaderGridComponent>();
+
+ while (spreadGrids.MoveNext(out var uid, out var grid))
+ {
+ if (grid.NextUpdate > curTime)
+ continue;
+
+ spreadable.Add(uid);
+ grid.NextUpdate += SpreadCooldown;
+ }
+
+ if (spreadable.Count == 0)
+ return;
+
+ var query = EntityQueryEnumerator<EdgeSpreaderComponent>();
+ var nodeQuery = GetEntityQuery<NodeContainerComponent>();
+ var xformQuery = GetEntityQuery<TransformComponent>();
+ var gridQuery = GetEntityQuery<SpreaderGridComponent>();
+
+ // Events and stuff
+ var groupUpdates = new Dictionary<INodeGroup, int>();
+ var spreaders = new List<(EntityUid Uid, EdgeSpreaderComponent Comp)>(Count<EdgeSpreaderComponent>());
+
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ spreaders.Add((uid, comp));
+ }
+
+ _robustRandom.Shuffle(spreaders);
+
+ foreach (var (uid, comp) in spreaders)
+ {
+ if (!xformQuery.TryGetComponent(uid, out var xform) ||
+ xform.GridUid == null ||
+ !gridQuery.HasComponent(xform.GridUid.Value))
+ {
+ RemCompDeferred<EdgeSpreaderComponent>(uid);
+ continue;
+ }
+
+ foreach (var sGroup in _spreaderGroups)
+ {
+ // Cleanup
+ if (!nodeQuery.TryGetComponent(uid, out var nodeComponent))
+ {
+ RemCompDeferred<EdgeSpreaderComponent>(uid);
+ continue;
+ }
+
+ if (!nodeComponent.TryGetNode<SpreaderNode>(sGroup, out var node))
+ continue;
+
+ // Not allowed this tick?
+ if (node.NodeGroup == null ||
+ !spreadable.Contains(xform.GridUid.Value))
+ {
+ continue;
+ }
+
+ // While we could check if it's an edge here the subscribing system may have its own definition
+ // of an edge so we'll let them handle it.
+ if (!groupUpdates.TryGetValue(node.NodeGroup, out var updates))
+ {
+ var spreadEv = new SpreadGroupUpdateRate()
+ {
+ Name = node.Name,
+ };
+ RaiseLocalEvent(ref spreadEv);
+ updates = (int) (spreadEv.UpdatesPerSecond * SpreadCooldown / TimeSpan.FromSeconds(1));
+ }
+
+ if (updates <= 0)
+ {
+ continue;
+ }
+
+ Spread(uid, node, node.NodeGroup, ref updates);
+ groupUpdates[node.NodeGroup] = updates;
+ }
+ }
+ }
+
+ private void Spread(EntityUid uid, SpreaderNode node, INodeGroup group, ref int updates)
+ {
+ GetNeighbors(uid, node.Name, out var freeTiles, out var occupiedTiles, out var neighbors);
+
+ TryComp<MapGridComponent>(Transform(uid).GridUid, out var grid);
+
+ var ev = new SpreadNeighborsEvent()
+ {
+ Grid = grid,
+ NeighborFreeTiles = freeTiles,
+ NeighborOccupiedTiles = occupiedTiles,
+ Neighbors = neighbors,
+ Updates = updates,
+ };
+
+ RaiseLocalEvent(uid, ref ev);
+ updates = ev.Updates;
+ }
+
+ /// <summary>
+ /// Gets the neighboring node data for the specified entity and the specified node group.
+ /// </summary>
+ public void GetNeighbors(EntityUid uid, string groupName, out ValueList<Vector2i> freeTiles, out ValueList<Vector2i> occupiedTiles, out ValueList<EntityUid> neighbors)
+ {
+ freeTiles = new ValueList<Vector2i>();
+ occupiedTiles = new ValueList<Vector2i>();
+ neighbors = new ValueList<EntityUid>();
+
+ if (!EntityManager.TryGetComponent<TransformComponent>(uid, out var transform))
+ return;
+
+ if (!_mapManager.TryGetGrid(transform.GridUid, out var grid))
+ return;
+
+ var tile = grid.TileIndicesFor(transform.Coordinates);
+ var nodeQuery = GetEntityQuery<NodeContainerComponent>();
+ var airtightQuery = GetEntityQuery<AirtightComponent>();
+
+ for (var i = 0; i < 4; i++)
+ {
+ var direction = (Direction) (i * 2);
+ var neighborPos = SharedMapSystem.GetDirection(tile, direction);
+
+ if (!grid.TryGetTileRef(neighborPos, out var tileRef) || tileRef.Tile.IsEmpty)
+ continue;
+
+ var directionEnumerator =
+ grid.GetAnchoredEntitiesEnumerator(neighborPos);
+ var occupied = false;
+
+ while (directionEnumerator.MoveNext(out var ent))
+ {
+ if (airtightQuery.TryGetComponent(ent, out var airtight) && airtight.AirBlocked)
+ {
+ // Check if air direction matters.
+ var blocked = false;
+
+ foreach (var value in new[] { AtmosDirection.North, AtmosDirection.East})
+ {
+ if ((value & airtight.AirBlockedDirection) == 0x0)
+ continue;
+
+ var airDirection = value.ToDirection();
+ var oppositeDirection = value.ToDirection();
+
+ if (direction != airDirection && direction != oppositeDirection)
+ continue;
+
+ blocked = true;
+ break;
+ }
+
+ if (!blocked)
+ continue;
+
+ occupied = true;
+ break;
+ }
+ }
+
+ if (occupied)
+ continue;
+
+ var oldCount = occupiedTiles.Count;
+ directionEnumerator =
+ grid.GetAnchoredEntitiesEnumerator(neighborPos);
+
+ while (directionEnumerator.MoveNext(out var ent))
+ {
+ if (!nodeQuery.TryGetComponent(ent, out var nodeContainer))
+ continue;
+
+ if (!nodeContainer.Nodes.ContainsKey(groupName))
+ continue;
+
+ neighbors.Add(ent.Value);
+ occupiedTiles.Add(neighborPos);
+ break;
+ }
+
+ if (oldCount == occupiedTiles.Count)
+ freeTiles.Add(neighborPos);
+ }
+ }
+
+ public List<EntityUid> GetNeighbors(EntityUid uid, AirtightComponent comp)
+ {
+ var neighbors = new List<EntityUid>();
+
+ if (!EntityManager.TryGetComponent<TransformComponent>(uid, out var transform))
+ return neighbors; // how did we get here?
+
+ if (!_mapManager.TryGetGrid(transform.GridUid, out var grid))
+ return neighbors;
+
+ var tile = grid.TileIndicesFor(transform.Coordinates);
+ var nodeQuery = GetEntityQuery<NodeContainerComponent>();
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection) (1 << i);
+ if (!comp.AirBlockedDirection.IsFlagSet(direction))
+ continue;
+
+ var directionEnumerator =
+ grid.GetAnchoredEntitiesEnumerator(SharedMapSystem.GetDirection(tile, direction.ToDirection()));
+
+ while (directionEnumerator.MoveNext(out var ent))
+ {
+ if (!nodeQuery.TryGetComponent(ent, out var nodeContainer))
+ continue;
+
+ foreach (var name in _spreaderGroups)
+ {
+ if (!nodeContainer.Nodes.ContainsKey(name))
+ continue;
+
+ neighbors.Add(ent.Value);
+ break;
+ }
+ }
+ }
+
+ return neighbors;
+ }
+}
using Content.Server.Atmos.Piping.Unary.Components;
-using Content.Server.Chemistry.ReactionEffects;
using Content.Server.Station.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Audio;
using Robust.Shared.Random;
using System.Linq;
+using Content.Server.Chemistry.Components;
+using Content.Server.Fluids.EntitySystems;
+using Robust.Server.GameObjects;
namespace Content.Server.StationEvents.Events;
{
continue;
}
+
var solution = new Solution();
if (!RobustRandom.Prob(Math.Min(0.33f * mod, 1.0f)))
if (RobustRandom.Prob(Math.Min(0.05f * mod, 1.0f)))
{
- solution.AddReagent(RobustRandom.Pick(allReagents), 100);
+ solution.AddReagent(RobustRandom.Pick(allReagents), 200);
}
else
{
- solution.AddReagent(RobustRandom.Pick(SafeishVentChemicals), 100);
+ solution.AddReagent(RobustRandom.Pick(SafeishVentChemicals), 200);
}
- FoamAreaReactionEffect.SpawnFoam("Foam", transform.Coordinates, solution, (int) (RobustRandom.Next(2, 6) * mod), 20, 1,
- 1, sound, EntityManager);
+ var foamEnt = Spawn("Foam", transform.Coordinates);
+ var smoke = EnsureComp<SmokeComponent>(foamEnt);
+ smoke.SpreadAmount = 20;
+ EntityManager.System<SmokeSystem>().Start(foamEnt, smoke, solution, 20f);
+ EntityManager.System<AudioSystem>().PlayPvs(sound, transform.Coordinates);
}
}
using Content.Server.Fluids.Components;
using Content.Server.Tools.Components;
using Content.Shared.DoAfter;
+using Content.Shared.Fluids.Components;
using Content.Shared.Interaction;
using Content.Shared.Maps;
using Content.Shared.Tools.Components;
return Math.Clamp(chance, 0f, 1f);
}
- public override void DoLunge(EntityUid user, Angle angle, Vector2 localPos, string? animation)
+ public override void DoLunge(EntityUid user, Angle angle, Vector2 localPos, string? animation, bool predicted = true)
{
- RaiseNetworkEvent(new MeleeLungeEvent(user, angle, localPos, animation), Filter.PvsExcept(user, entityManager: EntityManager));
+ Filter filter;
+
+ if (predicted)
+ {
+ filter = Filter.PvsExcept(user, entityManager: EntityManager);
+ }
+ else
+ {
+ filter = Filter.Pvs(user, entityManager: EntityManager);
+ }
+
+ RaiseNetworkEvent(new MeleeLungeEvent(user, angle, localPos, animation), filter);
}
private void OnChemicalInjectorHit(EntityUid owner, MeleeChemicalInjectorComponent comp, MeleeHitEvent args)
[RegisterComponent, Access(typeof(ChemicalPuddleArtifactSystem))]
public sealed class ChemicalPuddleArtifactComponent : Component
{
- /// <summary>
- /// The prototype id of the puddle
- /// </summary>
- [DataField("puddlePrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
- public string PuddlePrototype = "PuddleSmear";
-
/// <summary>
/// The solution where all the chemicals are stored
/// </summary>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ArtifactSystem _artifact = default!;
- [Dependency] private readonly SpillableSystem _spillable = default!;
+ [Dependency] private readonly PuddleSystem _puddle = default!;
/// <summary>
/// The key for the node data entry containing
component.ChemicalSolution.AddReagent(reagent, amountPerChem);
}
- _spillable.SpillAt(uid, component.ChemicalSolution, component.PuddlePrototype);
+ _puddle.TrySpillAt(uid, component.ChemicalSolution, out _);
}
}
using System.Linq;
+using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.ReactionEffects;
+using Content.Server.Fluids.EntitySystems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Chemistry.Components;
+using Robust.Server.GameObjects;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
var sol = new Solution();
var xform = Transform(uid);
- sol.AddReagent(component.SelectedReagent, component.ReagentAmount);
+ sol.AddReagent(component.SelectedReagent, component.ReagentAmount *
+ (component.MinFoamAmount +
+ (component.MaxFoamAmount - component.MinFoamAmount) * _random.NextFloat()));
- FoamAreaReactionEffect.SpawnFoam("Foam", xform.Coordinates, sol,
- _random.Next(component.MinFoamAmount, component.MaxFoamAmount), component.Duration,
- component.SpreadDuration, component.SpreadDuration, entityManager: EntityManager);
+ var foamEnt = Spawn("Foam", xform.Coordinates);
+ var smoke = EnsureComp<SmokeComponent>(foamEnt);
+ EntityManager.System<SmokeSystem>().Start(foamEnt, smoke, sol, 20f);
}
}
[DataField("jobTitle")]
[AutoNetworkedField]
+ [Access(typeof(SharedIdCardSystem), typeof(SharedPDASystem), typeof(SharedAgentIdCardSystem),
+ Other = AccessPermissions.ReadWrite)]
public string? JobTitle;
}
}
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Chemistry.Components;
+
+/// <summary>
+/// Denotes the solution that can be easily removed through any reagent container.
+/// Think pouring this or draining from a water tank.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed class DrainableSolutionComponent : Component
+{
+ /// <summary>
+ /// Solution name that can be drained.
+ /// </summary>
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("solution")]
+ public string Solution { get; set; } = "default";
+}
--- /dev/null
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Chemistry.Components;
+
+/// <summary>
+/// Reagents that can be added easily. For example like
+/// pouring something into another beaker, glass, or into the gas
+/// tank of a car.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed class RefillableSolutionComponent : Component
+{
+ /// <summary>
+ /// Solution name that can added to easily.
+ /// </summary>
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("solution")]
+ public string Solution { get; set; } = "default";
+
+ /// <summary>
+ /// The maximum amount that can be transferred to the solution at once
+ /// </summary>
+ [DataField("maxRefill")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public FixedPoint2? MaxRefill { get; set; } = null;
+}
_heatCapacity = 0;
}
+ /// <summary>
+ /// Splits a solution without the specified reagent.
+ /// </summary>
+ public Solution SplitSolutionWithout(FixedPoint2 toTake, string without)
+ {
+ TryGetReagent(without, out var existing);
+ RemoveReagent(without, toTake);
+ var sol = SplitSolution(toTake);
+ AddReagent(without, existing);
+ return sol;
+ }
+
public Solution SplitSolution(FixedPoint2 toTake)
{
if (toTake <= FixedPoint2.Zero)
ValidateSolution();
}
- public Color GetColor(IPrototypeManager? protoMan)
+ public Color GetColorWithout(IPrototypeManager? protoMan, params string[] without)
{
if (Volume == FixedPoint2.Zero)
{
foreach (var reagent in Contents)
{
+ if (without.Contains(reagent.ReagentId))
+ continue;
+
runningTotalQuantity += reagent.Quantity;
if (!protoMan.TryIndex(reagent.ReagentId, out ReagentPrototype? proto))
return mixColor;
}
+ public Color GetColor(IPrototypeManager? protoMan)
+ {
+ return GetColorWithout(protoMan);
+ }
+
[Obsolete("Use ReactiveSystem.DoEntityReaction")]
public void DoEntityReaction(EntityUid uid, ReactionMethod method)
{
return false;
// Remove any reactions that were not applicable. Avoids re-iterating over them in future.
- reactions.Except(toRemove);
+ foreach (var proto in toRemove)
+ {
+ reactions.Remove(proto);
+ }
if (products.Volume <= 0)
return true;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
+using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
[DataField("metamorphicSprite")]
public SpriteSpecifier? MetamorphicSprite { get; } = null;
+ /// <summary>
+ /// If this reagent is part of a puddle is it slippery.
+ /// </summary>
+ [DataField("slippery")]
+ public bool Slippery = false;
+
[DataField("metabolisms", serverOnly: true, customTypeSerializer: typeof(PrototypeIdDictionarySerializer<ReagentEffectsEntry, MetabolismGroupPrototype>))]
public Dictionary<string, ReagentEffectsEntry>? Metabolisms = null;
[DataField("plantMetabolism", serverOnly: true)]
public readonly List<ReagentEffect> PlantMetabolisms = new(0);
- [DataField("pricePerUnit")]
- public float PricePerUnit { get; }
+ [DataField("pricePerUnit")] public float PricePerUnit;
- /// <summary>
- /// If the substance color is too dark we user a lighter version to make the text color readable when the user examines a solution.
- /// </summary>
- public Color GetSubstanceTextColor()
- {
- var highestValue = MathF.Max(SubstanceColor.R, MathF.Max(SubstanceColor.G, SubstanceColor.B));
- var difference = 0.5f - highestValue;
-
- if (difference > 0f)
- {
- return new Color(SubstanceColor.R + difference,
- SubstanceColor.G + difference,
- SubstanceColor.B + difference);
- }
-
- return SubstanceColor;
- }
+ // TODO: Pick the highest reagent for sounds and add sticky to cola, juice, etc.
+ [DataField("footstepSound")]
+ public SoundSpecifier FootstepSound = new SoundCollectionSpecifier("FootstepWater");
public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume)
{
[RegisterComponent, NetworkedComponent]
public sealed class AbsorbentComponent : Component
{
- // TODO: Predicted solutions my beloved.
- public float Progress;
-
public const string SolutionName = "absorbed";
- [DataField("pickupAmount")]
- public FixedPoint2 PickupAmount = FixedPoint2.New(10);
-
- /// <summary>
- /// When using this tool on an empty floor tile, leave this much reagent as a new puddle.
- /// </summary>
- [DataField("residueAmount")]
- public FixedPoint2 ResidueAmount = FixedPoint2.New(10); // Should be higher than MopLowerLimit
+ public Dictionary<Color, float> Progress = new();
/// <summary>
- /// To leave behind a wet floor, this tool will be unable to take from puddles with a volume less than this
- /// amount. This limit is ignored if the target puddle does not evaporate.
+ /// How much solution we can transfer in one interaction.
/// </summary>
- [DataField("lowerLimit")]
- public FixedPoint2 LowerLimit = FixedPoint2.New(5);
+ [DataField("pickupAmount")]
+ public FixedPoint2 PickupAmount = FixedPoint2.New(60);
[DataField("pickupSound")]
- public SoundSpecifier PickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg");
-
- [DataField("transferSound")]
- public SoundSpecifier TransferSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg");
-
- /// <summary>
- /// Quantity of reagent that this mop can pick up per second. Determines the length of the do-after.
- /// </summary>
- [DataField("speed")] public float Speed = 10;
-
- /// <summary>
- /// How many entities can this tool interact with at once?
- /// </summary>
- [DataField("maxEntities")]
- public int MaxInteractingEntities = 1;
-
- /// <summary>
- /// What entities is this tool interacting with right now?
- /// </summary>
- [ViewVariables]
- public HashSet<EntityUid> InteractingEntities = new();
-
+ public SoundSpecifier PickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg")
+ {
+ Params = AudioParams.Default.WithVariation(0.05f),
+ };
+
+ [DataField("transferSound")] public SoundSpecifier TransferSound =
+ new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg")
+ {
+ Params = AudioParams.Default.WithVariation(0.05f).WithVolume(-3f),
+ };
+
+ public static readonly SoundSpecifier DefaultTransferSound =
+ new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg")
+ {
+ Params = AudioParams.Default.WithVariation(0.05f).WithVolume(-3f),
+ };
}
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Fluids.Components;
+
+[RegisterComponent, NetworkedComponent]
+public sealed class DrainComponent : Component
+{
+ public const string SolutionName = "drainBuffer";
+
+ [DataField("accumulator")]
+ public float Accumulator = 0f;
+
+ /// <summary>
+ /// How many units per second the drain can absorb from the surrounding puddles.
+ /// Divided by puddles, so if there are 5 puddles this will take 1/5 from each puddle.
+ /// This will stay fixed to 1 second no matter what DrainFrequency is.
+ /// </summary>
+ [DataField("unitsPerSecond")]
+ public float UnitsPerSecond = 6f;
+
+ /// <summary>
+ /// How many units are ejected from the buffer per second.
+ /// </summary>
+ [DataField("unitsDestroyedPerSecond")]
+ public float UnitsDestroyedPerSecond = 1f;
+
+ /// <summary>
+ /// How many (unobstructed) tiles away the drain will
+ /// drain puddles from.
+ /// </summary>
+ [DataField("range")]
+ public float Range = 2f;
+
+ /// <summary>
+ /// How often in seconds the drain checks for puddles around it.
+ /// If the EntityQuery seems a bit unperformant this can be increased.
+ /// </summary>
+ [DataField("drainFrequency")]
+ public float DrainFrequency = 1f;
+}
--- /dev/null
+using Content.Shared.FixedPoint;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Fluids.Components
+{
+ /// <summary>
+ /// Puddle on a floor
+ /// </summary>
+ [RegisterComponent, NetworkedComponent, Access(typeof(SharedPuddleSystem))]
+ public sealed class PuddleComponent : Component
+ {
+ [DataField("spillSound")]
+ public SoundSpecifier SpillSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
+
+ [DataField("overflowVolume")]
+ public FixedPoint2 OverflowVolume = FixedPoint2.New(20);
+
+ [DataField("solution")] public string SolutionName = "puddle";
+ }
+}
[Serializable, NetSerializable]
public enum PuddleVisuals : byte
{
- VolumeScale,
CurrentVolume,
SolutionColor,
- IsEvaporatingVisual
}
}
+using System.Linq;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Fluids;
-public abstract class SharedMoppingSystem : EntitySystem
+/// <summary>
+/// Mopping logic for interacting with puddle components.
+/// </summary>
+public abstract class SharedAbsorbentSystem : EntitySystem
{
public override void Initialize()
{
if (args.Current is not AbsorbentComponentState state)
return;
- if (component.Progress.Equals(state.Progress))
+ if (component.Progress.OrderBy(x => x.Key.ToArgb()).SequenceEqual(state.Progress))
return;
- component.Progress = state.Progress;
+ component.Progress.Clear();
+ foreach (var item in state.Progress)
+ {
+ component.Progress.Add(item.Key, item.Value);
+ }
}
private void OnAbsorbentGetState(EntityUid uid, AbsorbentComponent component, ref ComponentGetState args)
{
- args.State = new AbsorbentComponentState()
- {
- Progress = component.Progress,
- };
+ args.State = new AbsorbentComponentState(component.Progress);
}
[Serializable, NetSerializable]
protected sealed class AbsorbentComponentState : ComponentState
{
- public float Progress;
+ public Dictionary<Color, float> Progress;
+
+ public AbsorbentComponentState(Dictionary<Color, float> progress)
+ {
+ Progress = progress;
+ }
}
}
--- /dev/null
+using Content.Shared.Chemistry.Components;
+using Content.Shared.DragDrop;
+using Content.Shared.Fluids.Components;
+
+namespace Content.Shared.Fluids;
+
+public abstract class SharedPuddleSystem : EntitySystem
+{
+ /// <summary>
+ /// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle.
+ /// </summary>
+ public const float LowThreshold = 0.3f;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<RefillableSolutionComponent, CanDragEvent>(OnRefillableCanDrag);
+ SubscribeLocalEvent<RefillableSolutionComponent, CanDropDraggedEvent>(OnRefillableCanDropDragged);
+ SubscribeLocalEvent<DrainableSolutionComponent, CanDropTargetEvent>(OnDrainCanDropTarget);
+ }
+
+ private void OnRefillableCanDrag(EntityUid uid, RefillableSolutionComponent component, ref CanDragEvent args)
+ {
+ args.Handled = true;
+ }
+
+ private void OnDrainCanDropTarget(EntityUid uid, DrainableSolutionComponent component, ref CanDropTargetEvent args)
+ {
+ if (HasComp<RefillableSolutionComponent>(args.Dragged))
+ {
+ args.CanDrop = true;
+ args.Handled = true;
+ }
+ }
+
+ private void OnRefillableCanDropDragged(EntityUid uid, RefillableSolutionComponent component, ref CanDropDraggedEvent args)
+ {
+ if (!HasComp<DrainableSolutionComponent>(args.Target) && !HasComp<DrainComponent>(args.Target))
+ return;
+
+ args.CanDrop = true;
+ args.Handled = true;
+ }
+}
+++ /dev/null
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Foam
-{
- [Serializable, NetSerializable]
- public enum FoamVisuals : byte
- {
- State,
- Color
- }
-}
--- /dev/null
+using Robust.Shared.Audio;
+
+namespace Content.Shared.Movement.Events;
+
+/// <summary>
+/// Raised directed on an entity when trying to get a relevant footstep sound
+/// </summary>
+[ByRefEvent]
+public record struct GetFootstepSoundEvent(EntityUid User)
+{
+ public readonly EntityUid User = User;
+
+ /// <summary>
+ /// Set the sound to specify a footstep sound and mark as handled.
+ /// </summary>
+ public SoundSpecifier? Sound;
+}
private bool TryGetFootstepSound(TransformComponent xform, bool haveShoes, [NotNullWhen(true)] out SoundSpecifier? sound)
{
sound = null;
- MapGridComponent? grid;
// Fallback to the map?
- if (xform.GridUid == null)
+ if (!_mapManager.TryGetGrid(xform.GridUid, out var grid))
{
if (TryComp<FootstepModifierComponent>(xform.MapUid, out var modifier))
{
return false;
}
- grid = _mapManager.GetGrid(xform.GridUid.Value);
var position = grid.LocalToTile(xform.Coordinates);
+ var soundEv = new GetFootstepSoundEvent(xform.Owner);
// If the coordinates have a FootstepModifier component
// i.e. component that emit sound on footsteps emit that sound
while (anchored.MoveNext(out var maybeFootstep))
{
+ RaiseLocalEvent(maybeFootstep.Value, ref soundEv);
+
+ if (soundEv.Sound != null)
+ {
+ sound = soundEv.Sound;
+ return true;
+ }
+
if (TryComp<FootstepModifierComponent>(maybeFootstep, out var footstep))
{
sound = footstep.Sound;
+using Robust.Shared.GameStates;
+
namespace Content.Shared.Spawners.Components;
/// <summary>
/// Put this component on something you would like to despawn after a certain amount of time
/// </summary>
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent]
public sealed class TimedDespawnComponent : Component
{
/// <summary>
using Content.Shared.Spawners.Components;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Content.Shared.Spawners.EntitySystems;
{
base.Initialize();
UpdatesOutsidePrediction = true;
+ SubscribeLocalEvent<TimedDespawnComponent, ComponentGetState>(OnDespawnGetState);
+ SubscribeLocalEvent<TimedDespawnComponent, ComponentHandleState>(OnDespawnHandleState);
+ }
+
+ private void OnDespawnGetState(EntityUid uid, TimedDespawnComponent component, ref ComponentGetState args)
+ {
+ args.State = new TimedDespawnComponentState()
+ {
+ Lifetime = component.Lifetime,
+ };
+ }
+
+ private void OnDespawnHandleState(EntityUid uid, TimedDespawnComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not TimedDespawnComponentState state)
+ return;
+
+ component.Lifetime = state.Lifetime;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
- if (!_timing.IsFirstTimePredicted) return;
+ if (!_timing.IsFirstTimePredicted)
+ return;
- foreach (var comp in EntityQuery<TimedDespawnComponent>())
- {
- if (!CanDelete(comp.Owner)) continue;
+ var query = EntityQueryEnumerator<TimedDespawnComponent>();
+ while (query.MoveNext(out var uid, out var comp))
+ {
comp.Lifetime -= frameTime;
+ if (!CanDelete(uid))
+ continue;
+
if (comp.Lifetime <= 0)
- EntityManager.QueueDeleteEntity(comp.Owner);
+ {
+ var ev = new TimedDespawnEvent();
+ RaiseLocalEvent(uid, ref ev);
+ QueueDel(uid);
+ }
}
}
protected abstract bool CanDelete(EntityUid uid);
+
+ [Serializable, NetSerializable]
+ private sealed class TimedDespawnComponentState : ComponentState
+ {
+ public float Lifetime;
+ }
}
--- /dev/null
+namespace Content.Shared.Spawners;
+
+/// <summary>
+/// Raised directed on an entity when its timed despawn is over.
+/// </summary>
+[ByRefEvent]
+public readonly record struct TimedDespawnEvent;
DoLunge(user, angle, localPos, animation);
}
- public abstract void DoLunge(EntityUid user, Angle angle, Vector2 localPos, string? animation);
+ public abstract void DoLunge(EntityUid user, Angle angle, Vector2 localPos, string? animation, bool predicted = true);
}
-
-mopping-system-tool-full = { CAPITALIZE(THE($used)) } is full!
-mopping-system-puddle-diluted = You dilute the puddle.
-mopping-system-puddle-success = You mop the puddle.
-mopping-system-release-to-floor = You squeeze some liquid onto the floor.
-
-mopping-system-target-container-full = { CAPITALIZE(THE($target)) } is full!
mopping-system-target-container-empty = { CAPITALIZE(THE($target)) } is empty!
-mopping-system-target-container-too-small = { CAPITALIZE(THE($target)) } is too small for that!
-mopping-system-refillable-success = You wring { THE($used) } into { THE($target) }.
-mopping-system-drainable-success = You wet { THE($used) } from { THE($target) }.
+mopping-system-target-container-empty-water = { CAPITALIZE(THE($target)) } has no water!
+mopping-system-puddle-space = { THE($used) } is full of water
+mopping-system-puddle-evaporate = { THE($target) } is evaporating
+mopping-system-no-water = { THE($used) } has no water!
-mopping-system-hand-squeeze-still-wet = You wring { THE($used) } with your hands. It's still wet.
-mopping-system-hand-squeeze-little-wet = You wring { THE($used) } with your hands. It's still a little wet.
-mopping-system-hand-squeeze-dry = You wring { THE($used) } with your hands. It's pretty much dry.
+mopping-system-full = { THE($used) } is full!
+mopping-system-empty = { THE($used) } is empty!
-puddle-component-examine-is-slipper-text = It looks slippery.
+puddle-component-examine-is-slipper-text = It looks [color=#169C9C]slippery[/color].
+puddle-component-examine-evaporating = It is [color=#5E7C16]evaporating[/color].
+puddle-component-examine-evaporating-partial = It is [color=#FED83D]partially evaporating[/color].
+puddle-component-examine-evaporating-no = It is [color=#B02E26]not evaporating[/color].
puddle-component-slipped-touch-reaction = The chemicals in {THE($puddle)} get on your skin!
- pos: 10.5,25.5
parent: 179
type: Transform
-- uid: 42
- type: PuddleVomit
- components:
- - pos: 9.5,16.5
- parent: 179
- type: Transform
- uid: 43
type: WallSolid
components:
- pos: 17.5,15.5
parent: 179
type: Transform
-- uid: 87
- type: PuddleVomit
- components:
- - pos: 8.5,15.5
- parent: 179
- type: Transform
- uid: 88
type: ChairOfficeLight
components:
- pos: 10.5,15.5
parent: 179
type: Transform
-- uid: 111
- type: PuddleVomit
- components:
- - pos: 8.5,16.5
- parent: 179
- type: Transform
- uid: 112
type: WallSolid
components:
- pos: 4.5,18.5
parent: 179
type: Transform
-- uid: 116
- type: PuddleVomit
- components:
- - pos: 9.5,15.5
- parent: 179
- type: Transform
- uid: 117
type: WallSolid
components:
name: smoke
noSpawn: true
components:
+ - type: Occluder
- type: Sprite
drawdepth: Effects
sprite: Effects/chemsmoke.rsi
state: chemsmoke
- type: Appearance
- type: SmokeVisuals
- - type: Occluder
- type: Transform
anchored: true
- - type: SmokeSolutionAreaEffect
+ - type: Smoke
+ - type: NodeContainer
+ nodes:
+ smoke:
+ !type:SpreaderNode
+ nodeGroupID: Spreader
+ - type: EdgeSpreader
- type: SolutionContainerManager
solutions:
solutionArea:
maxVol: 600
canReact: false
+ - type: TimedDespawn
+ lifetime: 10
+ - type: Tag
+ tags:
+ - HideContextMenu
- type: entity
id: Foam
- state: foam
map: ["enum.FoamVisualLayers.Base"]
- type: AnimationPlayer
+ netsync: false
- type: Appearance
+ - type: SmokeVisuals
- type: FoamVisuals
animationTime: 0.6
animationState: foam-dissolve
- ItemMask
layer:
- SlipLayer
- - type: FoamSolutionAreaEffect
+ - type: Smoke
+ - type: NodeContainer
+ nodes:
+ smoke:
+ !type:SpreaderNode
+ nodeGroupID: Spreader
+ - type: EdgeSpreader
- type: SolutionContainerManager
solutions:
solutionArea:
- state: mfoam
map: ["enum.FoamVisualLayers.Base"]
- type: Appearance
+ - type: SmokeVisuals
- type: FoamVisuals
animationTime: 0.6
animationState: mfoam-dissolve
- - type: FoamSolutionAreaEffect
- foamedMetalPrototype: FoamedIronMetal
+ - type: Smoke
+ - type: SmokeDissipateSpawn
+ prototype: FoamedIronMetal
- type: entity
id: AluminiumMetalFoam
- state: mfoam
map: ["enum.FoamVisualLayers.Base"]
- type: Appearance
+ - type: SmokeVisuals
- type: FoamVisuals
animationTime: 0.6
animationState: mfoam-dissolve
- - type: FoamSolutionAreaEffect
- foamedMetalPrototype: FoamedAluminiumMetal
+ - type: Smoke
+ - type: SmokeDissipateSpawn
+ prototype: FoamedAluminiumMetal
- type: entity
id: BaseFoamedMetal
-# TODO: Add the other mess types
+# TODO: Fix - The idea is that blood and vomit is potentially not tile-bound versions of puddles(?)
- type: entity
- id: PuddleBase
+ id: PuddleTemporary
+ parent: Puddle
abstract: true
components:
- - type: FootstepModifier
- footstepSoundCollection:
- collection: FootstepWater
- - type: Transform
- anchored: true
- - type: Sprite
- drawdepth: FloorObjects
- - type: SolutionContainerManager
- solutions:
- puddle: { maxVol: 1000 }
- - type: Puddle
- spillSound:
- path: /Audio/Effects/Fluids/splat.ogg
- recolor: true
- - type: Clickable
- - type: Physics
- - type: Fixtures
- fixtures:
- - id: slipFixture
- shape:
- !type:PhysShapeAabb
- bounds: "-0.4,-0.4,0.4,0.4"
- mask:
- - ItemMask
- layer:
- - SlipLayer
- hard: false
- - type: Appearance
- - type: PuddleVisualizer
- wetFloorEffectThreshold: 0 # non-evaporating puddles don't become sparkles.
+ - type: Transform
+ anchored: true
+ noRot: false
- type: entity
- id: EvaporatingPuddle
- parent: PuddleBase
- abstract: true
- components:
- - type: Evaporation
- - type: PuddleVisualizer
- wetFloorEffectThreshold: 5
+ id: PuddleSmear
+ parent: PuddleTemporary
- type: entity
- name: gibblets
- id: PuddleGibblet
- parent: PuddleBase
- description: Gross.
+ id: PuddleVomit
+ parent: PuddleTemporary
components:
- - type: Sprite
- sprite: Fluids/gibblet.rsi # Placeholder
- state: gibblet-0
- netsync: false
- - type: SolutionContainerManager
- solutions:
- puddle:
- maxVol: 1000
- reagents:
- - ReagentId: Water
- Quantity: 10
- - type: Slippery
- launchForwardsMultiplier: 2.0
- - type: StepTrigger
+ - type: SolutionContainerManager
+ solutions:
+ puddle:
+ maxVol: 1000
+ reagents:
+ - ReagentId: Nutriment
+ Quantity: 5
+ - ReagentId: Water
+ Quantity: 5
- type: entity
- name: puddle
- id: PuddleSmear
- parent: EvaporatingPuddle
- description: A puddle of liquid.
- components:
- - type: Sprite
- sprite: Fluids/smear.rsi # Placeholder
- state: smear-0
- netsync: false
- - type: Puddle
- slipThreshold: 3
- - type: Appearance
- - type: PuddleVisualizer
- - type: Slippery
- launchForwardsMultiplier: 2.0
- - type: StepTrigger
+ id: PuddleEgg
+ parent: PuddleTemporary
- type: entity
- name: puddle
- id: PuddleSplatter
- parent: EvaporatingPuddle
- description: A puddle of liquid.
- components:
- - type: Sprite
- sprite: Fluids/splatter.rsi # Placeholder
- state: splatter-0
- netsync: false
- - type: Puddle
- slipThreshold: 3
- - type: Appearance
- - type: PuddleVisualizer
- - type: Slippery
- launchForwardsMultiplier: 2.0
- - type: StepTrigger
+ id: PuddleTomato
+ parent: PuddleTemporary
- type: entity
- id: PuddleBlood
- name: blood
- description: This can't be a good sign.
- parent: PuddleBase
- components:
- - type: Sprite
- sprite: Fluids/splatter.rsi # Placeholder
- state: splatter-0
- netsync: false
- - type: Puddle
- overflowVolume: 50
- opacityModifier: 8
+ id: PuddleWatermelon
+ parent: PuddleTemporary
- type: entity
- name: vomit
- id: PuddleVomit # No parent because we don't want the VisualizerSystem to behave in the standard way
- description: Gross.
- components:
- - type: Transform
- anchored: true
- - type: Clickable
- - type: Physics
- - type: Fixtures
- fixtures:
- - id: slipFixture
- shape:
- !type:PhysShapeAabb
- bounds: "-0.4,-0.4,0.4,0.4"
- mask:
- - ItemMask
- layer:
- - SlipLayer
- hard: false
- - type: Sprite
- sprite: Fluids/vomit.rsi
- state: vomit-0
- netsync: false
- - type: Puddle
- slipThreshold: 5
- recolor: false
- - type: SolutionContainerManager
- solutions:
- puddle:
- maxVol: 1000
- reagents:
- - ReagentId: Nutriment
- Quantity: 5
- - ReagentId: Water
- Quantity: 5
- - type: Slippery
- launchForwardsMultiplier: 2.0
- - type: StepTrigger
- - type: Appearance
- - type: PuddleVisualizer
- customPuddleSprite: true
+ id: PuddleFlour
+ parent: PuddleTemporary
- type: entity
- name: toxins vomit
- id: PuddleVomitToxin
- parent: PuddleVomit
- description: You probably don't want to get too close to this.
+ id: PuddleSparkle
+ name: Sparkle
+ placement:
+ mode: SnapgridCenter
components:
- - type: Sprite
- sprite: Fluids/vomit_toxin.rsi
- state: vomit_toxin-0
- netsync: false
- - type: Puddle
- - type: SolutionContainerManager
- solutions:
- puddle:
- maxVol: 1000
- reagents:
- - ReagentId: Toxin
- Quantity: 5
- - ReagentId: Water
- Quantity: 5
- - type: Appearance
- - type: PuddleVisualizer
- customPuddleSprite: true
- - type: Slippery
- launchForwardsMultiplier: 2.0
- - type: StepTrigger
+ # Animation is like 3 something seconds so we just need to despawn it before then.
+ - type: TimedDespawn
+ lifetime: 1
+ - type: EvaporationSparkle
+ - type: Transform
+ noRot: true
+ anchored: true
+ - type: Sprite
+ layers:
+ - sprite: Fluids/wet_floor_sparkles.rsi
+ state: sparkles
+ netsync: false
+ drawdepth: FloorObjects
+ color: "#FFFFFF80"
- type: entity
- name: writing
- id: PuddleWriting
- parent: PuddleBase
- description: A bit of liquid.
+ name: puddle
+ id: Puddle
+ description: A puddle of liquid.
+ placement:
+ mode: SnapgridCenter
components:
- - type: Sprite
- sprite: Fluids/writing.rsi # Placeholder
- state: writing-0
- netsync: false
- - type: Puddle
- - type: Evaporation
- evaporateTime: 10
- - type: Appearance
- - type: PuddleVisualizer
- - type: Slippery
- launchForwardsMultiplier: 2.0
- - type: StepTrigger
+ - type: Clickable
+ - type: FootstepModifier
+ footstepSoundCollection:
+ collection: FootstepWater
+ - type: Slippery
+ launchForwardsMultiplier: 2.0
+ - type: Transform
+ noRot: true
+ anchored: true
+ - type: Sprite
+ layers:
+ - sprite: Fluids/puddle.rsi
+ state: splat0
+ netsync: false
+ drawdepth: FloorObjects
+ color: "#FFFFFF80"
+ - type: Physics
+ bodyType: Static
+ - type: Fixtures
+ fixtures:
+ - id: slipFixture
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.4,-0.4,0.4,0.4"
+ mask:
+ - ItemMask
+ layer:
+ - SlipLayer
+ hard: false
+ - type: IconSmooth
+ key: puddles
+ base: splat
+ mode: CardinalFlags
+ - type: SolutionContainerManager
+ solutions:
+ puddle: { maxVol: 1000 }
+ - type: Puddle
+ - type: Appearance
+ - type: EdgeSpreader
+ - type: StepTrigger
+ - type: NodeContainer
+ nodes:
+ puddle:
+ !type:SpreaderNode
+ nodeGroupID: Spreader
acts: [ "Destruction" ]
# Splat
-
-- type: entity
- name: egg
- id: PuddleEgg
- parent: PuddleBase
- description: If the floor was a little hotter this would fry.
- components:
- - type: Sprite
- sprite: Fluids/egg_splat.rsi
- state: egg-0
- netsync: false
- - type: SolutionContainerManager
- solutions:
- puddle:
- maxVol: 1000
- reagents:
- - ReagentId: Egg
- Quantity: 2
-
- type: entity
name: eggshells
parent: BaseItem
# Powder (For when you throw stuff like flour and it explodes)
-- type: entity
- name: flour
- id: PuddleFlour
- parent: PuddleBase
- description: Call the janitor.
- components:
- - type: Sprite
- sprite: Fluids/powder.rsi
- state: powder
- color: white
- netsync: false
- - type: Puddle
- - type: SolutionContainerManager
- solutions:
- puddle:
- maxVol: 1000
- reagents:
- - ReagentId: Flour
- Quantity: 10
- - type: Appearance
- - type: PuddleVisualizer
-
# Reagent Containers
- type: entity
- !type:DoActsBehavior
acts: [ "Destruction" ]
-- type: entity
- name: tomato
- id: PuddleTomato
- parent: PuddleBase
- description: Splat.
- components:
- - type: Sprite
- sprite: Fluids/tomato_splat.rsi
- state: puddle-0
- netsync: false
- - type: SolutionContainerManager
- solutions:
- puddle:
- maxVol: 1000
- reagents:
- - ReagentId: JuiceTomato
- Quantity: 10
-
- type: entity
name: eggplant
parent: FoodProduceBase
spawned:
- id: ClothingHeadClothingHeadHatWatermelon
-- type: entity
- name: watermelon
- id: PuddleWatermelon
- parent: PuddleBase
- description: Splat.
- components:
- - type: Sprite
- sprite: Fluids/tomato_splat.rsi
- state: puddle-0
- netsync: false
- - type: SolutionContainerManager
- solutions:
- puddle:
- maxVol: 1000
- reagents:
- - ReagentId: JuiceWatermelon
- Quantity: 20
-
- type: entity
name: watermelon slice
parent: ProduceSliceBase
types:
Heat: 10
- type: AtmosExposed
- - type: Spreader
- growthResult: Kudzu
- chance: 1
+ - type: Kudzu
- type: GrowingKudzu
- growthTickSkipChance: 0.6666
+ growthTickChance: 0.3
- type: SlowContacts
walkSpeedModifier: 0.2
sprintSpeedModifier: 0.2
+ - type: EdgeSpreader
+ - type: NodeContainer
+ nodes:
+ kudzu:
+ !type:SpreaderNode
+ nodeGroupID: Spreader
+
- type: entity
id: WeakKudzu
parent: Kudzu
+ suffix: Weak
components:
- - type: Spreader
- growthResult: WeakKudzu
- chance: 0.3
+ - type: Kudzu
+ spreadChance: 0.3
- type: entity
id: FleshKudzu
behaviors:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- - type: Spreader
- growthResult: FleshKudzu
- chance: 0.2
- type: DamageContacts
damage:
types:
ignoreWhitelist:
tags:
- Flesh
+ - type: Kudzu
+ - type: EdgeSpreader
+ - type: NodeContainer
+ nodes:
+ kudzu:
+ !type:SpreaderNode
+ nodeGroupID: Spreader
- type: SlowContacts
walkSpeedModifier: 0.3
sprintSpeedModifier: 0.3
damage:
types:
Blunt: 10
+ - type: Spillable
+ solution: absorbed
- type: Wieldable
- type: IncreaseDamageOnWield
damage:
- type: SolutionContainerManager
solutions:
absorbed:
- maxVol: 50
+ maxVol: 60
+ - type: UseDelay
+ delay: 1.5
- type: Tag
tags:
- DroneUsable #No bucket because it holds chems, they can drag the cart or use a drain
damage:
types:
Blunt: 10
+ - type: Spillable
+ solution: absorbed
- type: Item
size: 15
sprite: Objects/Specific/Janitorial/advmop.rsi
- type: Absorbent
- maxEntities: 3
- pickupAmount: 25
- speed: 12.5
+ pickupAmount: 100
+ - type: UseDelay
+ delay: 1.0
- type: SolutionContainerManager
solutions:
absorbed:
name: mop bucket
id: MopBucket
description: Holds water and the tears of the janitor.
- suffix: Full
components:
- type: Clickable
- type: Sprite
sprite: Objects/Specific/Janitorial/janitorial.rsi
+ netsync: false
noRot: true
layers:
- state: mopbucket
solutions:
bucket:
maxVol: 500
- reagents:
- - ReagentId: Water
- Quantity: 250 # half-full at roundstart to leave room for puddles
- type: Spillable
spillDelay: 3.0
- type: DrainableSolution
maxFillLevels: 3
fillBaseName: mopbucket_water-
+- type: entity
+ name: mop bucket
+ id: MopBucketFull
+ parent: MopBucket
+ suffix: full
+ components:
+ - type: Sprite
+ layers:
+ - state: mopbucket
+ - state: mopbucket_water-3
+ map: [ "enum.SolutionContainerLayers.Fill" ]
+ - type: SolutionContainerManager
+ solutions:
+ bucket:
+ maxVol: 500
+ reagents:
+ - ReagentId: Water
+ Quantity: 500
+
- type: entity
name: wet floor sign
id: WetFloorSign
name: reagent-name-ethanol
parent: BaseAlcohol
desc: reagent-desc-ethanol
+ slippery: true
physicalDesc: reagent-physical-desc-strong-smelling
flavor: alcohol
color: "#b05b3c"
id: BaseDrink
group: Drinks
abstract: true
+ slippery: true
metabolisms:
Drink:
effects:
name: reagent-name-water
parent: BaseDrink
desc: reagent-desc-water
+ slippery: true
physicalDesc: reagent-physical-desc-translucent
flavor: water
color: "#75b1f0"
flavor: metallic
color: "#800000"
physicalDesc: reagent-physical-desc-ferrous
+ slippery: false
metabolisms:
Drink:
# Quenching!
flavor: slimy
color: "#2cf274"
physicalDesc: reagent-physical-desc-viscous
+ slippery: false
metabolisms:
Food:
# Delicious!
parent: BasePyrotechnic
desc: reagent-desc-welding-fuel
physicalDesc: reagent-physical-desc-oily
+ slippery: true
flavor: bitter
color: "#a76b1c"
boilingPoint: -84.7 # Acetylene. Close enough.
Sugar:
amount: 1
effects:
- - !type:SmokeAreaReactionEffect
- rangeConstant: 0
- rangeMultiplier: 1.1 #Range formula: rangeConstant + rangeMultiplier*sqrt(ReactionUnits)
- maxRange: 10
+ - !type:AreaReactionEffect
duration: 10
- spreadDelay: 0.5
- removeDelay: 0.5
- diluteReagents: true
prototypeId: Smoke
sound:
path: /Audio/Effects/smoke.ogg
Water:
amount: 1
effects:
- - !type:FoamAreaReactionEffect
- rangeConstant: 0
- rangeMultiplier: 1.1 #Range formula: rangeConstant + rangeMultiplier*sqrt(ReactionUnits)
- maxRange: 10
+ - !type:AreaReactionEffect
duration: 10
- spreadDelay: 1
- removeDelay: 0
- diluteReagents: true
- reagentDilutionStart: 4 #At what range should the reagents start diluting
- reagentDilutionFactor: 1
- reagentMaxConcentrationFactor: 2 #The reagents will get multiplied by this number if the range turns out to be 0
prototypeId: Foam
sound:
path: /Audio/Effects/extinguish.ogg
FluorosulfuricAcid:
amount: 1
effects:
- - !type:FoamAreaReactionEffect
- rangeConstant: 0
- rangeMultiplier: 1.1
- maxRange: 10
+ - !type:AreaReactionEffect
duration: 10
- spreadDelay: 1
- removeDelay: 0
- diluteReagents: true
- reagentDilutionStart: 4
- reagentDilutionFactor: 1
- reagentMaxConcentrationFactor: 2
prototypeId: IronMetalFoam
sound:
path: /Audio/Effects/extinguish.ogg
FluorosulfuricAcid:
amount: 1
effects:
- - !type:FoamAreaReactionEffect
- rangeConstant: 0
- rangeMultiplier: 1.1
- maxRange: 10
+ - !type:AreaReactionEffect
duration: 10
- spreadDelay: 1
- removeDelay: 0
- diluteReagents: true
- reagentDilutionStart: 4
- reagentDilutionFactor: 1
- reagentMaxConcentrationFactor: 2
prototypeId: AluminiumMetalFoam
sound:
path: /Audio/Effects/extinguish.ogg
id: Diphenylmethylamine
impact: Medium
reactants:
- Ethyloxyephedrine:
+ Ethyloxyephedrine:
amount: 1
Charcoal:
amount: 1
Pax:
amount: 1
- Coffee:
+ Coffee:
amount: 1
products:
Diphenylmethylamine: 2
-
+
- type: reaction
id: SodiumCarbonate
impact: Medium
reactants:
- Ammonia:
+ Ammonia:
amount: 1
TableSalt:
amount: 1
Carbon:
amount: 1
- Oxygen:
+ Oxygen:
amount: 1
products:
SodiumCarbonate: 4
--- /dev/null
+- type: edgeSpreader
+ id: kudzu
+
+- type: edgeSpreader
+ id: puddle
+
+- type: edgeSpreader
+ id: smoke
\ No newline at end of file
--- /dev/null
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Made by Alekshhh on github",
+ "states": [
+ {
+ "name": "splata"
+ },
+ {
+ "name": "splatb"
+ },
+ {
+ "name": "splat0"
+ },
+ {
+ "name": "splat1"
+ },
+ {
+ "name": "splat2"
+ },
+ {
+ "name": "splat3"
+ },
+ {
+ "name": "splat4"
+ },
+ {
+ "name": "splat5"
+ },
+ {
+ "name": "splat6"
+ },
+ {
+ "name": "splat7"
+ },
+ {
+ "name": "splat8"
+ },
+ {
+ "name": "splat9"
+ },
+ {
+ "name": "splat10"
+ },
+ {
+ "name": "splat11"
+ },
+ {
+ "name": "splat12"
+ },
+ {
+ "name": "splat13"
+ },
+ {
+ "name": "splat14"
+ },
+ {
+ "name": "splat15"
+ }
+ ]
+}
\ No newline at end of file