diff --git a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml index c8eb42cf56..160b6fa281 100644 --- a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml +++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml @@ -1,6 +1,6 @@ + SetSize="360 420" Title="{Loc 'gas-analyzer-window-name'}"> diff --git a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs index 81c35096b5..fa01276085 100644 --- a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs +++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs @@ -1,3 +1,4 @@ +using Content.Client.UserInterface.Controls; using Content.Shared.Atmos; using Content.Shared.Temperature; using Robust.Client.Graphics; @@ -261,11 +262,9 @@ namespace Content.Client.Atmos.UI // 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 @@ -299,25 +298,10 @@ namespace Content.Client.Atmos.UI }); // 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); diff --git a/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs b/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs index e5a2f5b689..5cef1db805 100644 --- a/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs +++ b/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs @@ -1,6 +1,9 @@ -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; @@ -9,23 +12,48 @@ namespace Content.Client.Chemistry.Visualizers; /// public sealed class FoamVisualizerSystem : VisualizerSystem { + [Dependency] private readonly IGameTiming _timing = default!; + public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnComponentInit); } + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!_timing.IsFirstTimePredicted) + return; + + var query = EntityQueryEnumerator(); + + 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); + } + } + } + /// /// Generates the animation used by foam visuals when the foam dissolves. /// 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 = @@ -36,25 +64,6 @@ public sealed class FoamVisualizerSystem : VisualizerSystem - /// Plays the animation used by foam visuals when the foam dissolves. - /// - protected override void OnAppearanceChange(EntityUid uid, FoamVisualsComponent comp, ref AppearanceChangeEvent args) - { - if (AppearanceSystem.TryGetData(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(uid, FoamVisuals.Color, out var color, args.Component)) - { - if (args.Sprite != null) - args.Sprite.Color = color; - } - } } public enum FoamVisualLayers : byte diff --git a/Content.Client/Fluids/MoppingSystem.cs b/Content.Client/Fluids/AbsorbentSystem.cs similarity index 84% rename from Content.Client/Fluids/MoppingSystem.cs rename to Content.Client/Fluids/AbsorbentSystem.cs index 90f8ba4e8b..97c1a80057 100644 --- a/Content.Client/Fluids/MoppingSystem.cs +++ b/Content.Client/Fluids/AbsorbentSystem.cs @@ -5,7 +5,8 @@ using Robust.Client.UserInterface; namespace Content.Client.Fluids; -public sealed class MoppingSystem : SharedMoppingSystem +/// +public sealed class AbsorbentSystem : SharedAbsorbentSystem { public override void Initialize() { diff --git a/Content.Client/Fluids/PuddleSystem.cs b/Content.Client/Fluids/PuddleSystem.cs new file mode 100644 index 0000000000..12004d7ff6 --- /dev/null +++ b/Content.Client/Fluids/PuddleSystem.cs @@ -0,0 +1,67 @@ +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(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(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; + } + } +} diff --git a/Content.Client/Fluids/PuddleVisualizerComponent.cs b/Content.Client/Fluids/PuddleVisualizerComponent.cs deleted file mode 100644 index 6d1bb0b2d8..0000000000 --- a/Content.Client/Fluids/PuddleVisualizerComponent.cs +++ /dev/null @@ -1,30 +0,0 @@ -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; - - /// - /// 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. - /// - [DataField("wetFloorEffectThreshold")] - public FixedPoint2 WetFloorEffectThreshold = FixedPoint2.New(5); - - /// - /// Alpha (opacity) of the wet floor sparkle effect. Higher alpha = more opaque/visible. - /// - [DataField("wetFloorEffectAlpha")] - public float WetFloorEffectAlpha = 0.75f; //should be somewhat transparent by default. - } -} diff --git a/Content.Client/Fluids/PuddleVisualizerSystem.cs b/Content.Client/Fluids/PuddleVisualizerSystem.cs deleted file mode 100644 index 4710caa184..0000000000 --- a/Content.Client/Fluids/PuddleVisualizerSystem.cs +++ /dev/null @@ -1,105 +0,0 @@ -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 - { - [Dependency] private readonly IRobustRandom _random = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(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(uid, PuddleVisuals.VolumeScale, out var volumeScale) - || !AppearanceSystem.TryGetData(uid, PuddleVisuals.CurrentVolume, out var currentVolume) - || !AppearanceSystem.TryGetData(uid, PuddleVisuals.SolutionColor, out var solutionColor) - || !AppearanceSystem.TryGetData(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. - } - } -} diff --git a/Content.Client/Fluids/UI/AbsorbentItemStatus.xaml b/Content.Client/Fluids/UI/AbsorbentItemStatus.xaml index d8cb4fc89b..b3c9f57daf 100644 --- a/Content.Client/Fluids/UI/AbsorbentItemStatus.xaml +++ b/Content.Client/Fluids/UI/AbsorbentItemStatus.xaml @@ -1,13 +1,4 @@ - - - - - - + + diff --git a/Content.Client/Fluids/UI/AbsorbentItemStatus.xaml.cs b/Content.Client/Fluids/UI/AbsorbentItemStatus.xaml.cs index 772be44efd..88c13fab68 100644 --- a/Content.Client/Fluids/UI/AbsorbentItemStatus.xaml.cs +++ b/Content.Client/Fluids/UI/AbsorbentItemStatus.xaml.cs @@ -1,16 +1,20 @@ -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 _progress = new(); public AbsorbentItemStatus(EntityUid uid, IEntityManager entManager) { @@ -25,7 +29,23 @@ namespace Content.Client.Fluids.UI if (!_entManager.TryGetComponent(_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); + } } } } diff --git a/Content.Client/IconSmoothing/IconSmoothComponent.cs b/Content.Client/IconSmoothing/IconSmoothComponent.cs index f6cd53b963..0ba7701890 100644 --- a/Content.Client/IconSmoothing/IconSmoothComponent.cs +++ b/Content.Client/IconSmoothing/IconSmoothComponent.cs @@ -15,6 +15,9 @@ namespace Content.Client.IconSmoothing [RegisterComponent] public sealed class IconSmoothComponent : Component { + [ViewVariables(VVAccess.ReadWrite), DataField("enabled")] + public bool Enabled = true; + public (EntityUid?, Vector2i)? LastPosition; /// diff --git a/Content.Client/IconSmoothing/IconSmoothSystem.cs b/Content.Client/IconSmoothing/IconSmoothSystem.cs index 781089e665..529d0dd1ae 100644 --- a/Content.Client/IconSmoothing/IconSmoothSystem.cs +++ b/Content.Client/IconSmoothing/IconSmoothSystem.cs @@ -3,6 +3,7 @@ using JetBrains.Annotations; 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 @@ -21,6 +22,15 @@ 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(); @@ -67,6 +77,7 @@ namespace Content.Client.IconSmoothing private void OnShutdown(EntityUid uid, IconSmoothComponent component, ComponentShutdown args) { + _dirtyEntities.Enqueue(uid); DirtyNeighbours(uid, component); } @@ -139,28 +150,28 @@ namespace Content.Client.IconSmoothing } // 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 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); } } @@ -184,9 +195,10 @@ namespace Content.Client.IconSmoothing // 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(uid, out var edge) && xformQuery.TryGetComponent(uid, out xform)) { @@ -196,13 +208,13 @@ namespace Content.Client.IconSmoothing { 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; } @@ -218,7 +230,7 @@ namespace Content.Client.IconSmoothing 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; } @@ -270,7 +282,7 @@ namespace Content.Client.IconSmoothing 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) @@ -294,13 +306,13 @@ namespace Content.Client.IconSmoothing } 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}"); @@ -319,12 +331,16 @@ namespace Content.Client.IconSmoothing CalculateEdge(sprite.Owner, directions, sprite); } - private bool MatchingEntity(IconSmoothComponent smooth, IEnumerable candidates, EntityQuery smoothQuery) + private bool MatchingEntity(IconSmoothComponent smooth, AnchoredEntitiesEnumerator candidates, EntityQuery 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; @@ -366,14 +382,14 @@ namespace Content.Client.IconSmoothing private (CornerFill ne, CornerFill nw, CornerFill sw, CornerFill se) CalculateCornerFill(MapGridComponent grid, IconSmoothComponent smooth, TransformComponent xform, EntityQuery 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; diff --git a/Content.Client/UserInterface/Controls/SplitBar.xaml b/Content.Client/UserInterface/Controls/SplitBar.xaml new file mode 100644 index 0000000000..46383cdef7 --- /dev/null +++ b/Content.Client/UserInterface/Controls/SplitBar.xaml @@ -0,0 +1,7 @@ + + diff --git a/Content.Client/UserInterface/Controls/SplitBar.xaml.cs b/Content.Client/UserInterface/Controls/SplitBar.xaml.cs new file mode 100644 index 0000000000..b60cedf185 --- /dev/null +++ b/Content.Client/UserInterface/Controls/SplitBar.xaml.cs @@ -0,0 +1,39 @@ +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) + }); + } + } +} diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs index 6631197f81..c2a494296a 100644 --- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs +++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs @@ -113,7 +113,7 @@ public sealed partial class MeleeWeaponSystem /// /// Does all of the melee effects for a player that are predicted, i.e. character lunge and weapon animation. /// - 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; diff --git a/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs b/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs index b55febd839..8d3417c90f 100644 --- a/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs +++ b/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs @@ -4,8 +4,10 @@ using System.Collections.Generic; 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; @@ -16,7 +18,7 @@ using Robust.Shared.Timing; 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) @@ -30,78 +32,6 @@ public sealed class FluidSpill 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(); - var entityManager = server.ResolveDependency(); - var spillSystem = server.ResolveDependency().GetEntitySystem(); - var gameTiming = server.ResolveDependency(); - var puddleSystem = server.ResolveDependency().GetEntitySystem(); - 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() { @@ -109,7 +39,6 @@ public sealed class FluidSpill var server = pairTracker.Pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); - var spillSystem = server.ResolveDependency().GetEntitySystem(); var puddleSystem = server.ResolveDependency().GetEntitySystem(); var gameTiming = server.ResolveDependency(); MapId mapId; @@ -117,9 +46,9 @@ public sealed class FluidSpill /* In this test, if o is spillage puddle and # are walls, we want to ensure all tiles are empty (`.`) - o # . - # . . . . . + # . . + o # . */ await server.WaitPost(() => { @@ -144,10 +73,9 @@ public sealed class FluidSpill 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); }); diff --git a/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs b/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs index 30f4cd9020..8e534da693 100644 --- a/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs +++ b/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs @@ -5,6 +5,7 @@ using Content.Server.Fluids.EntitySystems; 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; @@ -26,7 +27,7 @@ namespace Content.IntegrationTests.Tests.Fluids var testMap = await PoolManager.CreateTestMap(pairTracker); var entitySystemManager = server.ResolveDependency(); - var spillSystem = entitySystemManager.GetEntitySystem(); + var spillSystem = entitySystemManager.GetEntitySystem(); await server.WaitAssertion(() => { @@ -35,9 +36,9 @@ namespace Content.IntegrationTests.Tests.Fluids 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); @@ -53,7 +54,7 @@ namespace Content.IntegrationTests.Tests.Fluids var testMap = await PoolManager.CreateTestMap(pairTracker); var entitySystemManager = server.ResolveDependency(); - var spillSystem = entitySystemManager.GetEntitySystem(); + var spillSystem = entitySystemManager.GetEntitySystem(); MapGridComponent grid = null; @@ -74,124 +75,11 @@ namespace Content.IntegrationTests.Tests.Fluids { 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(); - var sTileDefinitionManager = server.ResolveDependency(); - var sGameTiming = server.ResolveDependency(); - var entityManager = server.ResolveDependency(); - var metaSystem = entityManager.EntitySysManager.GetEntitySystem(); - - 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(); - var spillSystem = entitySystemManager.GetEntitySystem(); - - // Spawn a puddle - await server.WaitAssertion(() => - { - var solution = new Solution("Water", FixedPoint2.New(amount)); - puddle = spillSystem.SpillAt(solution, sCoordinates, "PuddleSmear"); - meta = entityManager.GetComponent(puddle.Owner); - - // Check that the puddle was created - Assert.NotNull(puddle); - - evaporation = entityManager.GetComponent(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(); - } } } diff --git a/Content.Server/AME/AMENodeGroup.cs b/Content.Server/AME/AMENodeGroup.cs index 7a347ffb6f..444d3cc47b 100644 --- a/Content.Server/AME/AMENodeGroup.cs +++ b/Content.Server/AME/AMENodeGroup.cs @@ -25,11 +25,10 @@ namespace Content.Server.AME [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; @@ -41,7 +40,6 @@ namespace Content.Server.AME { base.LoadNodes(groupNodes); - var mapManager = IoCManager.Resolve(); MapGridComponent? grid = null; foreach (var node in groupNodes) @@ -50,7 +48,7 @@ namespace Content.Server.AME if (_entMan.TryGetComponent(nodeOwner, out AMEShieldComponent? shield)) { var xform = _entMan.GetComponent(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) diff --git a/Content.Server/Animals/Systems/UdderSystem.cs b/Content.Server/Animals/Systems/UdderSystem.cs index dd774b8999..ce4d1762ba 100644 --- a/Content.Server/Animals/Systems/UdderSystem.cs +++ b/Content.Server/Animals/Systems/UdderSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Chemistry.EntitySystems; 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; diff --git a/Content.Server/Atmos/Reactions/WaterVaporReaction.cs b/Content.Server/Atmos/Reactions/WaterVaporReaction.cs index 6d4bb783ff..57ffcac28a 100644 --- a/Content.Server/Atmos/Reactions/WaterVaporReaction.cs +++ b/Content.Server/Atmos/Reactions/WaterVaporReaction.cs @@ -17,25 +17,26 @@ namespace Content.Server.Atmos.Reactions [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() - .SpillAt(tileRef, new Solution(Reagent, FixedPoint2.New(MolesPerUnit)), PuddlePrototype, sound: false); + EntitySystem.Get() + .TrySpillAt(tileRef, new Solution(Reagent, FixedPoint2.New(MolesPerUnit)), out _, sound: false); return ReactionResult.Reacting; } diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs index f152c6b088..b5958d8561 100644 --- a/Content.Server/Body/Systems/BloodstreamSystem.cs +++ b/Content.Server/Body/Systems/BloodstreamSystem.cs @@ -26,15 +26,15 @@ namespace Content.Server.Body.Systems; 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() { @@ -307,12 +307,13 @@ public sealed class BloodstreamSystem : EntitySystem // 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(puddle.Owner); //TODO: Get rid of .Owner if (TryComp(uid, out var dna)) + { + var comp = EnsureComp(puddleUid); comp.DNAs.Add(dna.DNA); + } } component.BloodTemporarySolution.RemoveAllSolution(); @@ -353,13 +354,14 @@ public sealed class BloodstreamSystem : EntitySystem 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(puddle.Owner); //TODO: Get rid of .Owner if (TryComp(uid, out var dna)) + { + var comp = EnsureComp(puddleUid); comp.DNAs.Add(dna.DNA); + } } } } diff --git a/Content.Server/Chemistry/Components/FoamSolutionAreaEffectComponent.cs b/Content.Server/Chemistry/Components/FoamSolutionAreaEffectComponent.cs deleted file mode 100644 index 380f92caac..0000000000 --- a/Content.Server/Chemistry/Components/FoamSolutionAreaEffectComponent.cs +++ /dev/null @@ -1,96 +0,0 @@ -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().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().TryGetSolution(Owner, SolutionName, out var solution)) - return; - - if (!_entMan.TryGetComponent(entity, out BloodstreamComponent? bloodstream)) - return; - - var invSystem = EntitySystem.Get(); - - // 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(); - - 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(Owner).Coordinates); - } - - _entMan.QueueDeleteEntity(Owner); - }); - } - } -} diff --git a/Content.Server/Chemistry/Components/SmokeComponent.cs b/Content.Server/Chemistry/Components/SmokeComponent.cs new file mode 100644 index 0000000000..088287bfa2 --- /dev/null +++ b/Content.Server/Chemistry/Components/SmokeComponent.cs @@ -0,0 +1,26 @@ +using Content.Shared.Fluids.Components; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.Chemistry.Components; + +/// +/// Stores solution on an anchored entity that has touch and ingestion reactions +/// to entities that collide with it. Similar to +/// +[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; + + /// + /// Have we reacted with our tile yet? + /// + [DataField("reactedTile")] + public bool ReactedTile = false; +} diff --git a/Content.Server/Chemistry/Components/SmokeDissipateSpawnComponent.cs b/Content.Server/Chemistry/Components/SmokeDissipateSpawnComponent.cs new file mode 100644 index 0000000000..a791880d93 --- /dev/null +++ b/Content.Server/Chemistry/Components/SmokeDissipateSpawnComponent.cs @@ -0,0 +1,16 @@ +using Content.Server.Fluids.EntitySystems; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Chemistry.Components; + + +/// +/// When a despawns this will spawn another entity in its place. +/// +[RegisterComponent, Access(typeof(SmokeSystem))] +public sealed class SmokeDissipateSpawnComponent : Component +{ + [DataField("prototype", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] + public string Prototype = string.Empty; +} diff --git a/Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs b/Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs deleted file mode 100644 index 2aae1508aa..0000000000 --- a/Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs +++ /dev/null @@ -1,72 +0,0 @@ -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().TryGetSolution(Owner, SolutionName, out var solution)) - { - appearance.SetData(SmokeVisuals.Color, solution.GetColor(_proto)); - } - } - - protected override void ReactWithEntity(EntityUid entity, double solutionFraction) - { - if (!EntitySystem.Get().TryGetSolution(Owner, SolutionName, out var solution)) - return; - - if (!_entMan.TryGetComponent(entity, out BloodstreamComponent? bloodstream)) - return; - - if (_entMan.TryGetComponent(entity, out InternalsComponent? internals) && - IoCManager.Resolve().GetEntitySystem().AreInternalsWorking(internals)) - return; - - var chemistry = EntitySystem.Get(); - 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(); - 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); - } - } -} diff --git a/Content.Server/Chemistry/Components/SolutionAreaEffectComponent.cs b/Content.Server/Chemistry/Components/SolutionAreaEffectComponent.cs deleted file mode 100644 index 459c07dd60..0000000000 --- a/Content.Server/Chemistry/Components/SolutionAreaEffectComponent.cs +++ /dev/null @@ -1,219 +0,0 @@ -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 -{ - /// - /// 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 and . - /// - 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; } - - /// - /// Have we reacted with our tile yet? - /// - public bool ReactedTile = false; - - /// - /// Adds an to owner so the effect starts spreading and reacting. - /// - /// The range of the effect - /// - /// - /// - public void Start(int amount, float duration, float spreadDelay, float removeDelay) - { - if (Inception != null) - return; - - if (_entities.HasComponent(Owner)) - return; - - Amount = amount; - var inception = _entities.AddComponent(Owner); - - inception.Add(this); - inception.Setup(amount, duration, spreadDelay, removeDelay); - } - - /// - /// Gets called by an AreaEffectInceptionComponent. "Clones" Owner into the four directions and copies the - /// solution into each of them. - /// - public void Spread() - { - var meta = _entities.GetComponent(Owner); - if (meta.EntityPrototype == null) - { - Logger.Error("AreaEffectComponent needs its owner to be spawned by a prototype."); - return; - } - - var xform = _entities.GetComponent(Owner); - var solSys = _systems.GetEntitySystem(); - - 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); - } - - /// - /// Gets called by an AreaEffectInceptionComponent. - /// Removes this component from its inception and calls OnKill(). The implementation of OnKill() should - /// eventually delete the entity. - /// - public void Kill() - { - Inception?.Remove(this); - OnKill(); - } - - protected abstract void OnKill(); - - /// - /// 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. - /// - /// How many times will this get called over this area effect's duration, averaged - /// with the other area effects from the inception. - public void React(float averageExposures) - { - if (!_entities.EntitySysManager.GetEntitySystem() - .TryGetSolution(Owner, SolutionName, out var solution) || - solution.Contents.Count == 0) - { - return; - } - - var xform = _entities.GetComponent(Owner); - if (!MapManager.TryGetGrid(xform.GridUid, out var mapGrid)) - return; - - var tile = mapGrid.GetTileRef(xform.Coordinates.ToVector2i(_entities, MapManager)); - var chemistry = _entities.EntitySysManager.GetEntitySystem(); - var lookup = _entities.EntitySysManager.GetEntitySystem(); - - 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(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().TryGetSolution(Owner, SolutionName, out var solutionArea)) - return; - - var addSolution = - solution.SplitSolution(FixedPoint2.Min(solution.Volume, solutionArea.AvailableVolume)); - - EntitySystem.Get().TryAddSolution(Owner, solutionArea, addSolution); - - UpdateVisuals(); - } - - protected abstract void UpdateVisuals(); - - protected override void OnRemove() - { - base.OnRemove(); - Inception?.Remove(this); - } - } -} diff --git a/Content.Server/Chemistry/Components/SolutionAreaEffectInceptionComponent.cs b/Content.Server/Chemistry/Components/SolutionAreaEffectInceptionComponent.cs deleted file mode 100644 index a03da22a4e..0000000000 --- a/Content.Server/Chemistry/Components/SolutionAreaEffectInceptionComponent.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System.Linq; - -namespace Content.Server.Chemistry.Components -{ - /// - /// 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. - /// - /// It should only be manually added to an entity by the and not with a prototype. - [RegisterComponent] - public sealed class SolutionAreaEffectInceptionComponent : Component - { - private const float ReactionDelay = 1.5f; - - private readonly HashSet _group = new(); - - [ViewVariables] private float _lifeTimer; - [ViewVariables] private float _spreadTimer; - [ViewVariables] private float _reactionTimer; - - [ViewVariables] private int _amountCounterSpreading; - [ViewVariables] private int _amountCounterRemoving; - - /// - /// How much time to wait after fully spread before starting to remove itself. - /// - [ViewVariables] private float _duration; - - /// - /// Time between each spread step. Decreasing this makes spreading faster. - /// - [ViewVariables] private float _spreadDelay; - - /// - /// Time between each remove step. Decreasing this makes removing faster. - /// - [ViewVariables] private float _removeDelay; - - /// - /// 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. - /// - [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(_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(_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; - } - } -} diff --git a/Content.Server/Chemistry/Components/SolutionManager/DrainableSolutionComponent.cs b/Content.Server/Chemistry/Components/SolutionManager/DrainableSolutionComponent.cs deleted file mode 100644 index 7d33b5d235..0000000000 --- a/Content.Server/Chemistry/Components/SolutionManager/DrainableSolutionComponent.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Content.Server.Chemistry.Components.SolutionManager -{ - /// - /// Denotes the solution that can be easily removed through any reagent container. - /// Think pouring this or draining from a water tank. - /// - [RegisterComponent] - public sealed class DrainableSolutionComponent : Component - { - /// - /// Solution name that can be drained. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("solution")] - public string Solution { get; set; } = "default"; - } -} diff --git a/Content.Server/Chemistry/Components/SolutionManager/RefillableSolutionComponent.cs b/Content.Server/Chemistry/Components/SolutionManager/RefillableSolutionComponent.cs deleted file mode 100644 index 438960866f..0000000000 --- a/Content.Server/Chemistry/Components/SolutionManager/RefillableSolutionComponent.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Content.Shared.FixedPoint; - -namespace Content.Server.Chemistry.Components.SolutionManager -{ - /// - /// Reagents that can be added easily. For example like - /// pouring something into another beaker, glass, or into the gas - /// tank of a car. - /// - [RegisterComponent] - public sealed class RefillableSolutionComponent : Component - { - /// - /// Solution name that can added to easily. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("solution")] - public string Solution { get; set; } = "default"; - - /// - /// The maximum amount that can be transferred to the solution at once - /// - [DataField("maxRefill")] - [ViewVariables(VVAccess.ReadWrite)] - public FixedPoint2? MaxRefill { get; set; } = null; - - - } -} diff --git a/Content.Server/Chemistry/EntitySystems/SolutionAreaEffectSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionAreaEffectSystem.cs deleted file mode 100644 index 2064a1e666..0000000000 --- a/Content.Server/Chemistry/EntitySystems/SolutionAreaEffectSystem.cs +++ /dev/null @@ -1,43 +0,0 @@ -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(OnReactionAttempt); - } - - public override void Update(float frameTime) - { - foreach (var inception in EntityManager.EntityQuery().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; - } - } - } - } -} diff --git a/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs index 8f9a20e239..2166b9c7f3 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Chemistry.Reagent; using Content.Shared.Examine; using Content.Shared.FixedPoint; using JetBrains.Annotations; +using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -127,6 +128,17 @@ public sealed partial class SolutionContainerSystem : EntitySystem return splitSol; } + /// + /// Splits a solution without the specified reagent. + /// + 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); @@ -491,6 +503,37 @@ public sealed partial class SolutionContainerSystem : EntitySystem return false; } + /// + /// Gets the most common reagent across all solutions by volume. + /// + /// + public ReagentPrototype? GetMaxReagent(SolutionContainerManagerComponent component) + { + if (component.Solutions.Count == 0) + return null; + + var reagentCounts = new Dictionary(); + + 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(max.Key); + } + + public SoundSpecifier? GetSound(SolutionContainerManagerComponent component) + { + var max = GetMaxReagent(component); + return max?.FootstepSound; + } // Thermal energy and temperature management. diff --git a/Content.Server/Chemistry/ReactionEffects/AreaReactionEffect.cs b/Content.Server/Chemistry/ReactionEffects/AreaReactionEffect.cs index 6967a8fb82..60481ba929 100644 --- a/Content.Server/Chemistry/ReactionEffects/AreaReactionEffect.cs +++ b/Content.Server/Chemistry/ReactionEffects/AreaReactionEffect.cs @@ -1,14 +1,18 @@ 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 { @@ -16,47 +20,23 @@ namespace Content.Server.Chemistry.ReactionEffects /// Basically smoke and foam reactions. /// [UsedImplicitly] - [ImplicitDataDefinitionForInheritors] - public abstract class AreaReactionEffect : ReagentEffect, ISerializationHooks + [DataDefinition] + public sealed class AreaReactionEffect : ReagentEffect { - [Dependency] private readonly IMapManager _mapManager = default!; - - /// - /// Used for calculating the spread range of the effect based on the intensity of the reaction. - /// - [DataField("rangeConstant")] private float _rangeConstant; - [DataField("rangeMultiplier")] private float _rangeMultiplier = 1.1f; - [DataField("maxRange")] private int _maxRange = 10; - - /// - /// If true the reagents get diluted or concentrated depending on the range of the effect - /// - [DataField("diluteReagents")] private bool _diluteReagents; - - /// - /// Used to calculate dilution. Increasing this makes the reagents more diluted. - /// - [DataField("reagentDilutionFactor")] private float _reagentDilutionFactor = 1f; - /// /// How many seconds will the effect stay, counting after fully spreading. /// [DataField("duration")] private float _duration = 10; /// - /// How many seconds between each spread step. + /// How many units of reaction for 1 smoke entity. /// - [DataField("spreadDelay")] private float _spreadDelay = 0.5f; + [DataField("overflowThreshold")] public FixedPoint2 OverflowThreshold = FixedPoint2.New(2.5); /// - /// How many seconds between each remove step. + /// The entity prototype that will be spawned as the effect. /// - [DataField("removeDelay")] private float _removeDelay = 0.5f; - - /// - /// The entity prototype that will be spawned as the effect. It needs a component derived from SolutionAreaEffectComponent. - /// - [DataField("prototypeId", required: true)] + [DataField("prototypeId", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] private string _prototypeId = default!; /// @@ -67,55 +47,38 @@ namespace Content.Server.Chemistry.ReactionEffects 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().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); - - if (_diluteReagents) - { - // 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)); - } - + var spreadAmount = (int) Math.Max(0, Math.Ceiling((args.Quantity / OverflowThreshold).Float())); + var splitSolution = args.EntityManager.System().SplitSolution(args.SolutionEntity, args.Source, args.Source.Volume); var transform = args.EntityManager.GetComponent(args.SolutionEntity); + var mapManager = IoCManager.Resolve(); - 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 (!mapManager.TryFindGridAt(transform.MapPosition, out var grid) || + !grid.TryGetTileRef(transform.Coordinates, out var tileRef) || + tileRef.Tile.IsSpace()) { - Logger.Error("Couldn't get AreaEffectComponent from " + _prototypeId); - IoCManager.Resolve().QueueDeleteEntity(ent); return; } - areaEffectComponent.TryAddSolution(splitSolution); - areaEffectComponent.Start(amount, _duration, _spreadDelay, _removeDelay); + var coords = grid.MapToGrid(transform.MapPosition); + var ent = args.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid()); + + if (!args.EntityManager.TryGetComponent(ent, out var smokeComponent)) + { + Logger.Error("Couldn't get AreaEffectComponent from " + _prototypeId); + args.EntityManager.QueueDeleteEntity(ent); + return; + } + + var smoke = args.EntityManager.System(); + 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); } } diff --git a/Content.Server/Chemistry/ReactionEffects/FoamAreaReactionEffect.cs b/Content.Server/Chemistry/ReactionEffects/FoamAreaReactionEffect.cs deleted file mode 100644 index a1a1243d0e..0000000000 --- a/Content.Server/Chemistry/ReactionEffects/FoamAreaReactionEffect.cs +++ /dev/null @@ -1,45 +0,0 @@ -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().GetComponentOrNull(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(); - var ent = entityManager.SpawnEntity(entityPrototype, coords.SnapToGrid()); - - var areaEffectComponent = entityManager.GetComponentOrNull(ent); - - if (areaEffectComponent == null) - { - Logger.Error("Couldn't get AreaEffectComponent from " + entityPrototype); - IoCManager.Resolve().QueueDeleteEntity(ent); - return; - } - - if (contents != null) - areaEffectComponent.TryAddSolution(contents); - areaEffectComponent.Start(amount, duration, spreadDelay, removeDelay); - - entityManager.EntitySysManager.GetEntitySystem() - .PlayPvs(sound, ent, AudioParams.Default.WithVariation(0.125f)); - } - } -} diff --git a/Content.Server/Chemistry/ReactionEffects/SmokeAreaReactionEffect.cs b/Content.Server/Chemistry/ReactionEffects/SmokeAreaReactionEffect.cs deleted file mode 100644 index 3557dbe86e..0000000000 --- a/Content.Server/Chemistry/ReactionEffects/SmokeAreaReactionEffect.cs +++ /dev/null @@ -1,15 +0,0 @@ -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().GetComponentOrNull(entity); - } - } -} diff --git a/Content.Server/Chemistry/TileReactions/SpillIfPuddlePresentTileReaction.cs b/Content.Server/Chemistry/TileReactions/SpillIfPuddlePresentTileReaction.cs index 2cf2915fed..c79d8d41eb 100644 --- a/Content.Server/Chemistry/TileReactions/SpillIfPuddlePresentTileReaction.cs +++ b/Content.Server/Chemistry/TileReactions/SpillIfPuddlePresentTileReaction.cs @@ -1,4 +1,4 @@ -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; @@ -14,10 +14,11 @@ namespace Content.Server.Chemistry.TileReactions { public FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume) { - var spillSystem = EntitySystem.Get(); - if (reactVolume < 5 || !spillSystem.TryGetPuddle(tile, out _)) return FixedPoint2.Zero; + var spillSystem = EntitySystem.Get(); + 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; } diff --git a/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs b/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs index 4ba3efe658..e04ea30cb6 100644 --- a/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs +++ b/Content.Server/Chemistry/TileReactions/SpillTileReaction.cs @@ -19,7 +19,6 @@ namespace Content.Server.Chemistry.TileReactions [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) { @@ -27,19 +26,16 @@ namespace Content.Server.Chemistry.TileReactions var entityManager = IoCManager.Resolve(); - // TODO Make this not puddle smear. - var puddle = entityManager.EntitySysManager.GetEntitySystem() - .SpillAt(tile, new Solution(reagent.ID, reactVolume), "PuddleSmear", _overflow, false, true); - - if (puddle != null) + if (entityManager.EntitySysManager.GetEntitySystem() + .TrySpillAt(tile, new Solution(reagent.ID, reactVolume), out var puddleUid, false, false)) { - var slippery = entityManager.EnsureComponent(puddle.Owner); + var slippery = entityManager.EnsureComponent(puddleUid); slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier; slippery.ParalyzeTime = _paralyzeTime; entityManager.Dirty(slippery); - var step = entityManager.EnsureComponent(puddle.Owner); - entityManager.EntitySysManager.GetEntitySystem().SetRequiredTriggerSpeed(puddle.Owner, _requiredSlipSpeed, step); + var step = entityManager.EnsureComponent(puddleUid); + entityManager.EntitySysManager.GetEntitySystem().SetRequiredTriggerSpeed(puddleUid, _requiredSlipSpeed, step); return reactVolume; } diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs index 45ffc4a967..0483a50d9d 100644 --- a/Content.Server/Cloning/CloningSystem.cs +++ b/Content.Server/Cloning/CloningSystem.cs @@ -50,7 +50,7 @@ namespace Content.Server.Cloning [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!; @@ -316,7 +316,7 @@ namespace Content.Server.Cloning 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); diff --git a/Content.Server/Destructible/DestructibleSystem.cs b/Content.Server/Destructible/DestructibleSystem.cs index 8227926f35..07668ba063 100644 --- a/Content.Server/Destructible/DestructibleSystem.cs +++ b/Content.Server/Destructible/DestructibleSystem.cs @@ -30,7 +30,7 @@ namespace Content.Server.Destructible [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!; diff --git a/Content.Server/Destructible/Thresholds/Behaviors/SolutionExplosionBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/SolutionExplosionBehavior.cs index 490be2360f..76b76c4dc6 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/SolutionExplosionBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/SolutionExplosionBehavior.cs @@ -33,7 +33,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors // 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(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 diff --git a/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs index 293e2e3353..99b6171f7e 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs @@ -23,7 +23,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null) { var solutionContainerSystem = EntitySystem.Get(); - var spillableSystem = EntitySystem.Get(); + var spillableSystem = EntitySystem.Get(); var coordinates = system.EntityManager.GetComponent(owner).Coordinates; @@ -31,12 +31,12 @@ namespace Content.Server.Destructible.Thresholds.Behaviors 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); } } } diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs index a0d8e41f68..906dcdcdaf 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -17,7 +17,6 @@ namespace Content.Server.Entry "ClientEntitySpawner", "HandheldGPS", "CableVisualizer", - "PuddleVisualizer", "UIFragment", "PDABorderColor", }; diff --git a/Content.Server/Fluids/Components/DrainComponent.cs b/Content.Server/Fluids/Components/DrainComponent.cs deleted file mode 100644 index ec8980730a..0000000000 --- a/Content.Server/Fluids/Components/DrainComponent.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Content.Server.Fluids.Components -{ - [RegisterComponent] - public sealed class DrainComponent : Component - { - public const string SolutionName = "drainBuffer"; - - [DataField("accumulator")] - public float Accumulator = 0f; - - /// - /// 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. - /// - [DataField("unitsPerSecond")] - public float UnitsPerSecond = 6f; - - /// - /// How many units are ejected from the buffer per second. - /// - [DataField("unitsDestroyedPerSecond")] - public float UnitsDestroyedPerSecond = 1f; - - /// - /// How many (unobstructed) tiles away the drain will - /// drain puddles from. - /// - [DataField("range")] - public float Range = 2f; - - /// - /// How often in seconds the drain checks for puddles around it. - /// If the EntityQuery seems a bit unperformant this can be increased. - /// - [DataField("drainFrequency")] - public float DrainFrequency = 1f; - } -} diff --git a/Content.Server/Fluids/Components/EvaporationComponent.cs b/Content.Server/Fluids/Components/EvaporationComponent.cs index eaa3a1ad88..0c7a497e00 100644 --- a/Content.Server/Fluids/Components/EvaporationComponent.cs +++ b/Content.Server/Fluids/Components/EvaporationComponent.cs @@ -1,48 +1,24 @@ using Content.Server.Fluids.EntitySystems; using Content.Shared.FixedPoint; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.Fluids.Components +namespace Content.Server.Fluids.Components; + +/// +/// Added to puddles that contain water so it may evaporate over time. +/// +[RegisterComponent, Access(typeof(PuddleSystem))] +public sealed class EvaporationComponent : Component { - [RegisterComponent] - [Access(typeof(EvaporationSystem))] - public sealed class EvaporationComponent : Component - { - /// - /// Is this entity actively evaporating? This toggle lets us pause evaporation under certain conditions. - /// - [DataField("evaporationToggle")] - public bool EvaporationToggle = true; + /// + /// The next time we remove the EvaporationSystem reagent amount from this entity. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))] + public TimeSpan NextTick = TimeSpan.Zero; - /// - /// The time that it will take this puddle to lose one fixed unit of solution, in seconds. - /// - [DataField("evaporateTime")] - public float EvaporateTime { get; set; } = 5f; - - /// - /// Name of referenced solution. Defaults to - /// - [DataField("solution")] - public string SolutionName { get; set; } = PuddleComponent.DefaultSolutionName; - - /// - /// Lower limit below which puddle won't evaporate. Useful when wanting to leave a stain. - /// Defaults to evaporate completely. - /// - [DataField("lowerLimit")] - public FixedPoint2 LowerLimit = FixedPoint2.Zero; - - /// - /// Upper limit above which puddle won't evaporate. Useful when wanting to make sure large puddle will - /// remain forever. Defaults to 100. - /// - [DataField("upperLimit")] - public FixedPoint2 UpperLimit = FixedPoint2.New(100); //TODO: Consider setting this back to PuddleComponent.DefaultOverflowVolume once that behaviour is fixed. - - /// - /// The time accumulated since the start. - /// - [DataField("accumulator")] - public float Accumulator = 0f; - } + /// + /// How much evaporation occurs every tick. + /// + [DataField("evaporationAmount")] + public FixedPoint2 EvaporationAmount = FixedPoint2.New(0.3); } diff --git a/Content.Server/Fluids/Components/EvaporationSparkleComponent.cs b/Content.Server/Fluids/Components/EvaporationSparkleComponent.cs new file mode 100644 index 0000000000..393328e579 --- /dev/null +++ b/Content.Server/Fluids/Components/EvaporationSparkleComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Server.Fluids.Components; + +/// +/// Used to track evaporation sparkles so we can delete if necessary. +/// +[RegisterComponent] +public sealed class EvaporationSparkleComponent : Component +{ + +} diff --git a/Content.Server/Fluids/Components/FluidMapDataComponent.cs b/Content.Server/Fluids/Components/FluidMapDataComponent.cs deleted file mode 100644 index 30ab0a32de..0000000000 --- a/Content.Server/Fluids/Components/FluidMapDataComponent.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 -{ - /// - /// At what time will be checked next - /// - [DataField("goalTime", customTypeSerializer:typeof(TimeOffsetSerializer))] - public TimeSpan GoalTime; - - /// - /// Delay between two runs of - /// - [DataField("delay")] - public TimeSpan Delay = TimeSpan.FromSeconds(2); - - /// - /// Puddles to be expanded. - /// - [DataField("puddles")] public HashSet Puddles = new(); - - /// - /// Convenience method for setting GoalTime to + - /// - /// Time to which to add , defaults to current - public void UpdateGoal(TimeSpan? start = null) - { - GoalTime = (start ?? GoalTime) + Delay; - } -} diff --git a/Content.Server/Fluids/Components/FootstepTrackComponent.cs b/Content.Server/Fluids/Components/FootstepTrackComponent.cs new file mode 100644 index 0000000000..04df4dec75 --- /dev/null +++ b/Content.Server/Fluids/Components/FootstepTrackComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Fluids.Components; + +[RegisterComponent] +public sealed class FootstepTrackComponent : Component +{ + +} diff --git a/Content.Server/Fluids/Components/PuddleComponent.cs b/Content.Server/Fluids/Components/PuddleComponent.cs deleted file mode 100644 index 84b35af4d1..0000000000 --- a/Content.Server/Fluids/Components/PuddleComponent.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Content.Server.Fluids.EntitySystems; -using Content.Shared.FixedPoint; -using Robust.Shared.Audio; - -namespace Content.Server.Fluids.Components -{ - /// - /// Puddle on a floor - /// - [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 - - /// - /// Puddles with volume above this threshold can slip players. - /// - [DataField("slipThreshold")] - public FixedPoint2 SlipThreshold = DefaultSlipThreshold; - - [DataField("spillSound")] - public SoundSpecifier SpillSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg"); - - [DataField("overflowVolume")] - public FixedPoint2 OverflowVolume = DefaultOverflowVolume; - - /// - /// 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. - /// - [DataField("opacityModifier")] public float OpacityModifier = 1.0f; - - [DataField("solution")] public string SolutionName { get; set; } = DefaultSolutionName; - } -} diff --git a/Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs b/Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs new file mode 100644 index 0000000000..a63cc350d0 --- /dev/null +++ b/Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs @@ -0,0 +1,211 @@ +using Content.Server.Chemistry.EntitySystems; +using Content.Server.Popups; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Fluids; +using Content.Shared.Fluids.Components; +using Content.Shared.Interaction; +using Content.Shared.Timing; +using Content.Shared.Weapons.Melee; +using Robust.Server.GameObjects; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Server.Fluids.EntitySystems; + +/// +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(OnAbsorbentInit); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(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(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; + } + + /// + /// Attempt to fill an absorber from some refillable solution. + /// + 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; + } + + /// + /// Logic for an absorbing entity interacting with a puddle. + /// + 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; + } +} diff --git a/Content.Server/Fluids/EntitySystems/DrainSystem.cs b/Content.Server/Fluids/EntitySystems/DrainSystem.cs index 0e855c6c77..50f359ddc5 100644 --- a/Content.Server/Fluids/EntitySystems/DrainSystem.cs +++ b/Content.Server/Fluids/EntitySystems/DrainSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Fluids.Components; using Content.Server.Chemistry.EntitySystems; using Content.Shared.FixedPoint; using Content.Shared.Audio; +using Content.Shared.Fluids.Components; using Robust.Shared.Collections; namespace Content.Server.Fluids.EntitySystems diff --git a/Content.Server/Fluids/EntitySystems/EvaporationSystem.cs b/Content.Server/Fluids/EntitySystems/EvaporationSystem.cs deleted file mode 100644 index 01a0febbc7..0000000000 --- a/Content.Server/Fluids/EntitySystems/EvaporationSystem.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Content.Server.Chemistry.EntitySystems; -using Content.Server.Fluids.Components; -using Content.Shared.FixedPoint; -using JetBrains.Annotations; - -namespace Content.Server.Fluids.EntitySystems -{ - [UsedImplicitly] - public sealed class EvaporationSystem : EntitySystem - { - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - - public override void Update(float frameTime) - { - base.Update(frameTime); - foreach (var evaporationComponent in EntityManager.EntityQuery()) - { - 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; - } - } - - /// - /// Copy constructor to copy initial fields from source to destination. - /// - /// Entity to which we copy properties - /// Component that contains relevant properties - public void CopyConstruct(EntityUid destUid, EvaporationComponent srcEvaporation) - { - var destEvaporation = EntityManager.EnsureComponent(destUid); - destEvaporation.EvaporateTime = srcEvaporation.EvaporateTime; - destEvaporation.EvaporationToggle = srcEvaporation.EvaporationToggle; - destEvaporation.SolutionName = srcEvaporation.SolutionName; - destEvaporation.LowerLimit = srcEvaporation.LowerLimit; - destEvaporation.UpperLimit = srcEvaporation.UpperLimit; - } - } -} diff --git a/Content.Server/Fluids/EntitySystems/FluidSpreaderSystem.cs b/Content.Server/Fluids/EntitySystems/FluidSpreaderSystem.cs deleted file mode 100644 index 20f786d6cd..0000000000 --- a/Content.Server/Fluids/EntitySystems/FluidSpreaderSystem.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Server.Fluids.Components; -using Content.Shared; -using Content.Shared.Directions; -using Content.Shared.Maps; -using Content.Shared.Physics; -using JetBrains.Annotations; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Systems; -using Robust.Shared.Timing; - -namespace Content.Server.Fluids.EntitySystems; - -/// -/// Component that governs overflowing puddles. Controls how Puddles spread and updat -/// -[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!; - - /// - /// Adds an overflow component to the map data component tracking overflowing puddles - /// - /// EntityUid of overflowing puddle - /// Optional PuddleComponent - /// Optional TransformComponent - 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(mapId, out var component); - component.Puddles.Add(puddleUid); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - Span exploreDirections = stackalloc Direction[] - { - Direction.North, - Direction.East, - Direction.South, - Direction.West, - }; - var puddles = new List(4); - var puddleQuery = GetEntityQuery(); - var xFormQuery = GetEntityQuery(); - - foreach (var fluidMapData in EntityQuery()) - { - if (fluidMapData.Puddles.Count == 0 || _gameTiming.CurTime <= fluidMapData.GoalTime) - continue; - - var newIteration = new HashSet(); - 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); - } - } - - - /// - /// Check a tile is valid for solution allocation. - /// - /// Entity Uid of original puddle - /// PuddleComponent attached to srcUid - /// at which to check tile - /// helper param needed to extract entities - /// either found or newly created PuddleComponent. - /// true if tile is empty or occupied by a non-overflowing puddle (or a puddle close to being overflowing) - private bool CheckTile(EntityUid srcUid, PuddleComponent srcPuddle, EntityCoordinates dstPos, - MapGridComponent mapGrid, EntityQuery 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; - } -} diff --git a/Content.Server/Fluids/EntitySystems/MoppingSystem.cs b/Content.Server/Fluids/EntitySystems/MoppingSystem.cs deleted file mode 100644 index 9aab13ba6e..0000000000 --- a/Content.Server/Fluids/EntitySystems/MoppingSystem.cs +++ /dev/null @@ -1,287 +0,0 @@ -using Content.Server.Chemistry.Components.SolutionManager; -using Content.Server.Chemistry.EntitySystems; -using Content.Server.Fluids.Components; -using Content.Server.Popups; -using Content.Shared.Chemistry.Components; -using Content.Shared.DoAfter; -using Content.Shared.FixedPoint; -using Content.Shared.Fluids; -using Content.Shared.Interaction; -using Content.Shared.Tag; -using JetBrains.Annotations; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Map; - -namespace Content.Server.Fluids.EntitySystems; - -[UsedImplicitly] -public sealed class MoppingSystem : SharedMoppingSystem -{ - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly SpillableSystem _spillableSystem = default!; - [Dependency] private readonly TagSystem _tagSystem = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!; - [Dependency] private readonly PopupSystem _popups = default!; - [Dependency] private readonly AudioSystem _audio = default!; - - const string PuddlePrototypeId = "PuddleSmear"; // The puddle prototype to use when releasing liquid to the floor, making a new puddle - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnAbsorbentInit); - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(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); - } - - /// - /// Tries to create a puddle using solutions stored in the absorber entity. - /// - 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; - } - - /// - /// Attempt to fill an absorber from some drainable solution. - /// - 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; - } - - /// - /// Empty an absorber into a refillable solution. - /// - 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; - } - - /// - /// Logic for an absorbing entity interacting with a puddle. - /// - 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; - } -} diff --git a/Content.Server/Fluids/EntitySystems/PuddleDebugDebugOverlaySystem.cs b/Content.Server/Fluids/EntitySystems/PuddleDebugDebugOverlaySystem.cs index 2db1789da6..ce28dad0c1 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleDebugDebugOverlaySystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleDebugDebugOverlaySystem.cs @@ -1,5 +1,6 @@ using Content.Server.Fluids.Components; using Content.Shared.Fluids; +using Content.Shared.Fluids.Components; using Robust.Server.Player; using Robust.Shared.Map; using Robust.Shared.Timing; diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs new file mode 100644 index 0000000000..65eacba315 --- /dev/null +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs @@ -0,0 +1,68 @@ +using Content.Server.Fluids.Components; +using Content.Shared.Chemistry.Components; +using Content.Shared.FixedPoint; +using Content.Shared.Fluids.Components; + +namespace Content.Server.Fluids.EntitySystems; + +public sealed partial class PuddleSystem +{ + private static readonly TimeSpan EvaporationCooldown = TimeSpan.FromSeconds(1); + + public const string EvaporationReagent = "Water"; + + private void OnEvaporationMapInit(EntityUid uid, EvaporationComponent component, MapInitEvent args) + { + component.NextTick = _timing.CurTime + EvaporationCooldown; + } + + private void UpdateEvaporation(EntityUid uid, Solution solution) + { + if (HasComp(uid)) + { + return; + } + + if (solution.ContainsReagent(EvaporationReagent)) + { + var evaporation = AddComp(uid); + evaporation.NextTick = _timing.CurTime + EvaporationCooldown; + return; + } + + RemComp(uid); + } + + private void TickEvaporation() + { + var query = EntityQueryEnumerator(); + var xformQuery = GetEntityQuery(); + 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); + } +} diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs new file mode 100644 index 0000000000..31deaf82bb --- /dev/null +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs @@ -0,0 +1,139 @@ +using Content.Server.Chemistry.EntitySystems; +using Content.Server.Fluids.Components; +using Content.Server.Nutrition.Components; +using Content.Shared.Clothing.Components; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared.FixedPoint; +using Content.Shared.Inventory.Events; +using Content.Shared.Spillable; +using Content.Shared.Throwing; +using Content.Shared.Verbs; + +namespace Content.Server.Fluids.EntitySystems; + +public sealed partial class PuddleSystem +{ + private void InitializeSpillable() + { + SubscribeLocalEvent(SpillOnLand); + SubscribeLocalEvent>(AddSpillVerb); + SubscribeLocalEvent(OnGotEquipped); + SubscribeLocalEvent(OnSpikeOverflow); + SubscribeLocalEvent(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(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 args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + if (!_solutionContainerSystem.TryGetSolution(args.Target, component.SolutionName, out var solution)) + return; + + if (TryComp(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; + } +} diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs new file mode 100644 index 0000000000..4ecdefa7f5 --- /dev/null +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs @@ -0,0 +1,69 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.DragDrop; +using Content.Shared.FixedPoint; +using Content.Shared.Fluids; +using Content.Shared.Fluids.Components; + +namespace Content.Server.Fluids.EntitySystems; + +public sealed partial class PuddleSystem +{ + private void InitializeTransfers() + { + SubscribeLocalEvent(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(args.Target, out var drainable); + + _solutionContainerSystem.TryGetDrainableSolution(args.Target, out var drainableSolution, drainable); + + // Dump reagents into drain + if (TryComp(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); + } + } + } +} diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs index dd0de51db1..6dc6a64f8f 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs @@ -1,313 +1,639 @@ +using Content.Server.Administration.Logs; using Content.Server.Chemistry.EntitySystems; +using Content.Server.DoAfter; using Content.Server.Fluids.Components; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Reaction; +using Content.Server.Spreader; using Content.Shared.Chemistry.Reagent; +using Content.Shared.Database; using Content.Shared.Examine; using Content.Shared.FixedPoint; using Content.Shared.Fluids; using Content.Shared.Popups; using Content.Shared.Slippery; +using Content.Shared.Fluids.Components; using Content.Shared.StepTrigger.Components; using Content.Shared.StepTrigger.Systems; -using JetBrains.Annotations; +using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Map; +using Robust.Shared.Map.Components; using Robust.Shared.Player; using Solution = Content.Shared.Chemistry.Components.Solution; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Robust.Shared.Timing; -namespace Content.Server.Fluids.EntitySystems +namespace Content.Server.Fluids.EntitySystems; + +/// +/// Handles solutions on floors. Also handles the spreader logic for where the solution overflows a specified volume. +/// +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 _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 + */ + + /// + public override void Initialize() { - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly FluidSpreaderSystem _fluidSpreaderSystem = default!; - [Dependency] private readonly StepTriggerSystem _stepTrigger = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly ReactiveSystem _reactive = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly IPrototypeManager _protoMan = default!; - [Dependency] private readonly IRobustRandom _random = default!; + base.Initialize(); - public static float PuddleVolume = 1000; + // Shouldn't need re-anchoring. + SubscribeLocalEvent(OnAnchorChanged); + SubscribeLocalEvent(HandlePuddleExamined); + SubscribeLocalEvent(OnSolutionUpdate); + SubscribeLocalEvent(OnPuddleInit); + SubscribeLocalEvent(OnPuddleSpread); + SubscribeLocalEvent(OnPuddleSlip); - // Using local deletion queue instead of the standard queue so that we can easily "undelete" if a puddle - // loses & then gains reagents in a single tick. - private HashSet _deletionQueue = new(); + SubscribeLocalEvent(OnEvaporationMapInit); - public override void Initialize() + InitializeSpillable(); + InitializeTransfers(); + } + + private void OnPuddleSpread(EntityUid uid, PuddleComponent component, ref SpreadNeighborsEvent args) + { + var overflow = GetOverflowSolution(uid, component); + + if (overflow.Volume == FixedPoint2.Zero) { - base.Initialize(); - - // Shouldn't need re-anchoring. - SubscribeLocalEvent(OnAnchorChanged); - SubscribeLocalEvent(HandlePuddleExamined); - SubscribeLocalEvent(OnSolutionUpdate); - SubscribeLocalEvent(OnPuddleInit); - SubscribeLocalEvent(OnPuddleSlip); + RemCompDeferred(uid); + return; } - public override void Update(float frameTime) + var xform = Transform(uid); + + if (!TryComp(xform.GridUid, out var grid)) { - base.Update(frameTime); - foreach (var ent in _deletionQueue) + RemCompDeferred(uid); + return; + } + + var puddleQuery = GetEntityQuery(); + + // First we overflow to neighbors with overflow capacity + // Then we go to free tiles + // Then we go to anything else. + if (args.Neighbors.Count > 0) + { + _random.Shuffle(args.Neighbors); + + // Overflow to neighbors with remaining space. + foreach (var neighbor in args.Neighbors) { - Del(ent); + if (!puddleQuery.TryGetComponent(neighbor, out var puddle) || + !_solutionContainerSystem.TryGetSolution(neighbor, puddle.SolutionName, out var neighborSolution)) + { + continue; + } + + var remaining = neighborSolution.Volume - puddle.OverflowVolume; + + if (remaining <= FixedPoint2.Zero) + continue; + + var split = overflow.SplitSolution(remaining); + + if (!_solutionContainerSystem.TryAddSolution(neighbor, neighborSolution, split)) + continue; + + args.Updates--; + EnsureComp(neighbor); + + if (args.Updates <= 0) + break; } - _deletionQueue.Clear(); - } - private void OnPuddleInit(EntityUid uid, PuddleComponent component, ComponentInit args) - { - _solutionContainerSystem.EnsureSolution(uid, component.SolutionName, FixedPoint2.New(PuddleVolume), out _); - } - - private void OnPuddleSlip(EntityUid uid, PuddleComponent component, ref SlipEvent args) - { - // Reactive entities have a chance to get a touch reaction from slipping on a puddle - // (i.e. it is implied they fell face first onto it or something) - if (!HasComp(args.Slipped)) - return; - - // Eventually probably have some system of 'body coverage' to tweak the probability but for now just 0.5 - // (implying that spacemen have a 50% chance to either land on their ass or their face) - if (!_random.Prob(0.5f)) - return; - - if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution)) - return; - - _popup.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", uid)), - args.Slipped, args.Slipped, PopupType.SmallCaution); - - // Take 15% of the puddle solution - var splitSol = _solutionContainerSystem.SplitSolution(uid, solution, solution.Volume * 0.15f); - _reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch); - } - - private void OnSolutionUpdate(EntityUid uid, PuddleComponent component, SolutionChangedEvent args) - { - if (args.Solution.Name != component.SolutionName) - return; - - if (args.Solution.Volume <= 0) + if (overflow.Volume == FixedPoint2.Zero) { - _deletionQueue.Add(uid); + RemCompDeferred(uid); return; } - - _deletionQueue.Remove(uid); - UpdateSlip(uid, component); - UpdateAppearance(uid, component); } - private void UpdateAppearance(EntityUid uid, PuddleComponent? puddleComponent = null, AppearanceComponent? appearance = null) + if (args.NeighborFreeTiles.Count > 0 && args.Updates > 0) { - if (!Resolve(uid, ref puddleComponent, ref appearance, false) - || EmptyHolder(uid, puddleComponent)) + _random.Shuffle(args.NeighborFreeTiles); + var spillAmount = overflow.Volume / args.NeighborFreeTiles.Count; + + foreach (var tile in args.NeighborFreeTiles) { - return; + var split = overflow.SplitSolution(spillAmount); + TrySpillAt(grid.GridTileToLocal(tile), split, out _, false); + args.Updates--; + + if (args.Updates <= 0) + break; } - // Opacity based on level of fullness to overflow - // Hard-cap lower bound for visibility reasons - var puddleSolution = _solutionContainerSystem.EnsureSolution(uid, puddleComponent.SolutionName); - var volumeScale = puddleSolution.Volume.Float() / - puddleComponent.OverflowVolume.Float() * - puddleComponent.OpacityModifier; - - bool isEvaporating; - - if (TryComp(uid, out EvaporationComponent? evaporation) - && evaporation.EvaporationToggle)// if puddle is evaporating. - { - isEvaporating = true; - } - else isEvaporating = false; - - var color = puddleSolution.GetColor(_protoMan); - - _appearance.SetData(uid, PuddleVisuals.VolumeScale, volumeScale, appearance); - _appearance.SetData(uid, PuddleVisuals.CurrentVolume, puddleSolution.Volume, appearance); - _appearance.SetData(uid, PuddleVisuals.SolutionColor, color, appearance); - _appearance.SetData(uid, PuddleVisuals.IsEvaporatingVisual, isEvaporating, appearance); + RemCompDeferred(uid); + return; } - private void UpdateSlip(EntityUid entityUid, PuddleComponent puddleComponent) + if (overflow.Volume > FixedPoint2.Zero && args.Neighbors.Count > 0 && args.Updates > 0) { - var vol = CurrentVolume(puddleComponent.Owner, puddleComponent); - if ((puddleComponent.SlipThreshold == FixedPoint2.New(-1) || - vol < puddleComponent.SlipThreshold) && - TryComp(entityUid, out StepTriggerComponent? stepTrigger)) + var spillPerNeighbor = overflow.Volume / args.Neighbors.Count; + + foreach (var neighbor in args.Neighbors) { - _stepTrigger.SetActive(entityUid, false, stepTrigger); - } - else if (vol >= puddleComponent.SlipThreshold) - { - var comp = EnsureComp(entityUid); - _stepTrigger.SetActive(entityUid, true, comp); + // Overflow to neighbours but not if they're already at the cap + // This is to avoid diluting solutions too much. + if (!puddleQuery.TryGetComponent(neighbor, out var puddle) || + !_solutionContainerSystem.TryGetSolution(neighbor, puddle.SolutionName, out var neighborSolution) || + neighborSolution.Volume >= puddle.OverflowVolume) + { + continue; + } + + var split = overflow.SplitSolution(spillPerNeighbor); + + if (!_solutionContainerSystem.TryAddSolution(neighbor, neighborSolution, split)) + continue; + + EnsureComp(neighbor); + args.Updates--; + + if (args.Updates <= 0) + break; } } - private void HandlePuddleExamined(EntityUid uid, PuddleComponent component, ExaminedEvent args) + // Add the remainder back + if (_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var puddleSolution)) { - if (TryComp(uid, out var slippery) && slippery.Active) + _solutionContainerSystem.TryAddSolution(uid, puddleSolution, overflow); + } + } + + private void OnPuddleSlip(EntityUid uid, PuddleComponent component, ref SlipEvent args) + { + // Reactive entities have a chance to get a touch reaction from slipping on a puddle + // (i.e. it is implied they fell face first onto it or something) + if (!HasComp(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); + } + + /// + public override void Update(float frameTime) + { + base.Update(frameTime); + foreach (var ent in _deletionQueue) + { + Del(ent); + } + _deletionQueue.Clear(); + + TickEvaporation(); + } + + private void OnPuddleInit(EntityUid uid, PuddleComponent component, ComponentInit args) + { + _solutionContainerSystem.EnsureSolution(uid, component.SolutionName, FixedPoint2.New(PuddleVolume), out _); + } + + private void OnSolutionUpdate(EntityUid uid, PuddleComponent component, SolutionChangedEvent args) + { + if (args.Solution.Name != component.SolutionName) + return; + + if (args.Solution.Volume <= 0) + { + _deletionQueue.Add(uid); + return; + } + + _deletionQueue.Remove(uid); + UpdateSlip(uid, component, args.Solution); + UpdateEvaporation(uid, args.Solution); + UpdateAppearance(uid, component); + } + + private void UpdateAppearance(EntityUid uid, PuddleComponent? puddleComponent = null, AppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref puddleComponent, ref appearance, false)) + { + return; + } + + var volume = FixedPoint2.Zero; + Color color = Color.White; + + if (_solutionContainerSystem.TryGetSolution(uid, puddleComponent.SolutionName, out var solution)) + { + volume = solution.Volume / puddleComponent.OverflowVolume; + + // Make blood stand out more + // Kinda EH + // Could potentially do alpha per-solution but future problem. + var standoutReagents = new string[] { "Blood", "Slime" }; + + color = solution.GetColorWithout(_prototypeManager, standoutReagents); + color = color.WithAlpha(0.7f); + + foreach (var standout in standoutReagents) { - args.PushText(Loc.GetString("puddle-component-examine-is-slipper-text")); + if (!solution.TryGetReagent(standout, out var quantity)) + continue; + + var interpolateValue = quantity.Float() / solution.Volume.Float(); + color = Color.InterpolateBetween(color, _prototypeManager.Index(standout).SubstanceColor, interpolateValue); } } - private void OnAnchorChanged(EntityUid uid, PuddleComponent puddle, ref AnchorStateChangedEvent args) + _appearance.SetData(uid, PuddleVisuals.CurrentVolume, volume.Float(), appearance); + _appearance.SetData(uid, PuddleVisuals.SolutionColor, color, appearance); + } + + private void UpdateSlip(EntityUid entityUid, PuddleComponent component, Solution solution) + { + var isSlippery = false; + // The base sprite is currently at 0.3 so we require at least 2nd tier to be slippery or else it's too hard to see. + var amountRequired = FixedPoint2.New(component.OverflowVolume.Float() * LowThreshold); + var slipperyAmount = FixedPoint2.Zero; + + foreach (var reagent in solution.Contents) { - if (!args.Anchored) - QueueDel(uid); + var reagentProto = _prototypeManager.Index(reagent.ReagentId); + + if (reagentProto.Slippery) + { + slipperyAmount += reagent.Quantity; + + if (slipperyAmount > amountRequired) + { + isSlippery = true; + break; + } + } } - public bool EmptyHolder(EntityUid uid, PuddleComponent? puddleComponent = null) + if (isSlippery) { - if (!Resolve(uid, ref puddleComponent)) - return true; + var comp = EnsureComp(entityUid); + _stepTrigger.SetActive(entityUid, true, comp); + } + else if (TryComp(entityUid, out var comp)) + { + _stepTrigger.SetActive(entityUid, false, comp); + } + } - return !_solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName, - out var solution) - || solution.Contents.Count == 0; + private void HandlePuddleExamined(EntityUid uid, PuddleComponent component, ExaminedEvent args) + { + if (TryComp(uid, out var slippery) && slippery.Active) + { + args.PushMarkup(Loc.GetString("puddle-component-examine-is-slipper-text")); } - public FixedPoint2 CurrentVolume(EntityUid uid, PuddleComponent? puddleComponent = null) + if (HasComp(uid)) { - if (!Resolve(uid, ref puddleComponent)) - return FixedPoint2.Zero; + if (_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution) && + CanFullyEvaporate(solution)) + { + args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating")); + } + else if (solution?.ContainsReagent(EvaporationReagent) == true) + { + args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-partial")); + } + else + { + args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-no")); + } + } + else + { + args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-no")); + } + } - return _solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName, - out var solution) - ? solution.Volume - : FixedPoint2.Zero; + private void OnAnchorChanged(EntityUid uid, PuddleComponent puddle, ref AnchorStateChangedEvent args) + { + if (!args.Anchored) + QueueDel(uid); + } + + /// + /// Gets the current volume of the given puddle, which may not necessarily be PuddleVolume. + /// + 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; + } + + /// + /// Try to add solution to . + /// + /// Puddle to which we add + /// Solution that is added to puddleComponent + /// Play sound on overflow + /// Overflow on encountered values + /// Optional resolved PuddleComponent + /// + public bool TryAddSolution(EntityUid puddleUid, + Solution addedSolution, + bool sound = true, + bool checkForOverflow = true, + PuddleComponent? puddleComponent = null) + { + if (!Resolve(puddleUid, ref puddleComponent)) + return false; + + if (addedSolution.Volume == 0 || + !_solutionContainerSystem.TryGetSolution(puddleUid, puddleComponent.SolutionName, + out var solution)) + { + return false; } - /// - /// Try to add solution to . - /// - /// Puddle to which we add - /// Solution that is added to puddleComponent - /// Play sound on overflow - /// Overflow on encountered values - /// Optional resolved PuddleComponent - /// - public bool TryAddSolution(EntityUid puddleUid, - Solution addedSolution, - bool sound = true, - bool checkForOverflow = true, - PuddleComponent? puddleComponent = null) + solution.AddSolution(addedSolution, _prototypeManager); + _solutionContainerSystem.UpdateChemicals(puddleUid, solution, true); + + if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent)) { - if (!Resolve(puddleUid, ref puddleComponent)) - return false; + EnsureComp(puddleUid); + } - if (addedSolution.Volume == 0 || - !_solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName, - out var solution)) - { - return false; - } - - solution.AddSolution(addedSolution, _protoMan); - _solutionContainerSystem.UpdateChemicals(puddleUid, solution, true); - - if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent)) - { - _fluidSpreaderSystem.AddOverflowingPuddle(puddleComponent.Owner, puddleComponent); - } - - if (!sound) - { - return true; - } - - SoundSystem.Play(puddleComponent.SpillSound.GetSound(), - Filter.Pvs(puddleComponent.Owner), puddleComponent.Owner); + if (!sound) + { return true; } - /// - /// Given a large srcPuddle and smaller destination puddles, this method will equalize their - /// - /// puddle that donates liquids to other puddles - /// List of puddles that we want to equalize, their puddle should be less than sourcePuddleComponent - /// Total volume of src and destination puddle - /// optional parameter, that after equalization adds all still overflowing puddles. - /// puddleComponent for - public void EqualizePuddles(EntityUid srcPuddle, List destinationPuddles, - FixedPoint2 totalVolume, - HashSet? stillOverflowing = null, - PuddleComponent? sourcePuddleComponent = null) + SoundSystem.Play(puddleComponent.SpillSound.GetSound(), + Filter.Pvs(puddleUid), puddleUid); + return true; + } + + /// + /// Whether adding this solution to this puddle would overflow. + /// + 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; + } + + /// + /// Whether adding this solution to this puddle would overflow. + /// + private bool IsOverflowing(EntityUid uid, PuddleComponent? puddle = null) + { + if (!Resolve(uid, ref puddle)) + return false; + + return CurrentVolume(uid, puddle) > puddle.OverflowVolume; + } + + /// + /// Gets the solution amount above the overflow threshold for the puddle. + /// + public Solution GetOverflowSolution(EntityUid uid, PuddleComponent? puddle = null) + { + if (!Resolve(uid, ref puddle) || !_solutionContainerSystem.TryGetSolution(uid, puddle.SolutionName, + out var solution)) { - if (!Resolve(srcPuddle, ref sourcePuddleComponent) - || !_solutionContainerSystem.TryGetSolution(srcPuddle, sourcePuddleComponent.SolutionName, - out var srcSolution)) - return; + return new Solution(0); + } - var dividedVolume = totalVolume / (destinationPuddles.Count + 1); + // TODO: This is going to fail with struct solutions. + var remaining = puddle.OverflowVolume; + var split = _solutionContainerSystem.SplitSolution(uid, solution, CurrentVolume(uid, puddle) - remaining); + return split; + } - foreach (var destPuddle in destinationPuddles) + #region Spill + + /// + /// 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. + /// + 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(coordinates, 1.0f)) + { + // sorry! no overload for returning uid, so .owner must be used + var owner = ent.Owner; + + // between 5 and 30% + var splitAmount = solution.Volume * _random.NextFloat(0.05f, 0.30f); + var splitSolution = solution.SplitSolution(splitAmount); + + if (user != null) { - if (!_solutionContainerSystem.TryGetSolution(destPuddle.Owner, destPuddle.SolutionName, - out var destSolution)) + _adminLogger.Add(LogType.Landed, + $"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}"); + } + + _reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch); + _popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other", ("spillable", uid), ("target", owner)), owner, PopupType.SmallCaution); + } + + return TrySpillAt(coordinates, solution, out puddleUid, sound); + } + + /// + /// Spills solution at the specified coordinates. + /// Will add to an existing puddle if present or create a new one if not. + /// + public bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true) + { + if (solution.Volume == 0) + { + puddleUid = EntityUid.Invalid; + return false; + } + + if (!_mapManager.TryGetGrid(coordinates.GetGridUid(EntityManager), out var mapGrid)) + { + puddleUid = EntityUid.Invalid; + return false; + } + + return TrySpillAt(mapGrid.GetTileRef(coordinates), solution, out puddleUid, sound); + } + + /// + /// + /// + public bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true, TransformComponent? transformComponent = null) + { + if (!Resolve(uid, ref transformComponent, false)) + { + puddleUid = EntityUid.Invalid; + return false; + } + + return TrySpillAt(transformComponent.Coordinates, solution, out puddleUid, sound: sound); + } + + /// + /// + /// + public bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true, bool tileReact = true) + { + if (solution.Volume <= 0) + { + puddleUid = EntityUid.Invalid; + return false; + } + + // If space return early, let that spill go out into the void + if (tileRef.Tile.IsEmpty) + { + puddleUid = EntityUid.Invalid; + return false; + } + + // Let's not spill to invalid grids. + var gridId = tileRef.GridUid; + if (!_mapManager.TryGetGrid(gridId, out var mapGrid)) + { + puddleUid = EntityUid.Invalid; + return false; + } + + if (tileReact) + { + // First, do all tile reactions + for (var i = 0; i < solution.Contents.Count; i++) + { + var (reagentId, quantity) = solution.Contents[i]; + var proto = _prototypeManager.Index(reagentId); + var removed = proto.ReactionTile(tileRef, quantity); + if (removed <= FixedPoint2.Zero) continue; - var takeAmount = FixedPoint2.Max(0, dividedVolume - destSolution.Volume); - TryAddSolution(destPuddle.Owner, srcSolution.SplitSolution(takeAmount), false, false, destPuddle); - if (stillOverflowing != null && IsOverflowing(destPuddle.Owner, destPuddle)) - { - stillOverflowing.Add(destPuddle.Owner); - } + solution.RemoveReagent(reagentId, removed); } + } - if (stillOverflowing != null && srcSolution.Volume > sourcePuddleComponent.OverflowVolume) + // Tile reactions used up everything. + if (solution.Volume == FixedPoint2.Zero) + { + puddleUid = EntityUid.Invalid; + return false; + } + + // Get normalized co-ordinate for spill location and spill it in the centre + // TODO: Does SnapGrid or something else already do this? + var anchored = mapGrid.GetAnchoredEntitiesEnumerator(tileRef.GridIndices); + var puddleQuery = GetEntityQuery(); + var sparklesQuery = GetEntityQuery(); + + while (anchored.MoveNext(out var ent)) + { + // If there's existing sparkles then delete it + if (sparklesQuery.TryGetComponent(ent, out var sparkles)) { - stillOverflowing.Add(srcPuddle); + QueueDel(ent.Value); + continue; } + + if (!puddleQuery.TryGetComponent(ent, out var puddle)) + continue; + + if (TryAddSolution(ent.Value, solution, sound, puddleComponent: puddle)) + { + EnsureComp(ent.Value); + } + + puddleUid = ent.Value; + return true; } - /// - /// Whether adding this solution to this puddle would overflow. - /// - /// Uid of owning entity - /// Puddle to which we are adding solution - /// Solution we intend to add - /// - public bool WouldOverflow(EntityUid uid, Solution solution, PuddleComponent? puddle = null) + var coords = mapGrid.GridTileToLocal(tileRef.GridIndices); + puddleUid = EntityManager.SpawnEntity("Puddle", coords); + EnsureComp(puddleUid); + if (TryAddSolution(puddleUid, solution, sound)) { - if (!Resolve(uid, ref puddle)) - return false; - - return CurrentVolume(uid, puddle) + solution.Volume > puddle.OverflowVolume; + EnsureComp(puddleUid); } + return true; + } - /// - /// Whether adding this solution to this puddle would overflow. - /// - /// Uid of owning entity - /// Puddle ref param - /// - private bool IsOverflowing(EntityUid uid, PuddleComponent? puddle = null) + #endregion + + /// + /// Tries to get the relevant puddle entity for a tile. + /// + public bool TryGetPuddle(TileRef tile, out EntityUid puddleUid) + { + puddleUid = EntityUid.Invalid; + + if (!TryComp(tile.GridUid, out var grid)) + return false; + + var anc = grid.GetAnchoredEntitiesEnumerator(tile.GridIndices); + var puddleQuery = GetEntityQuery(); + + while (anc.MoveNext(out var ent)) { - if (!Resolve(uid, ref puddle)) - return false; + if (!puddleQuery.HasComponent(ent.Value)) + continue; - return CurrentVolume(uid, puddle) > puddle.OverflowVolume; + puddleUid = ent.Value; + return true; } - public void SpawnPuddle(EntityUid srcUid, EntityCoordinates pos, PuddleComponent srcPuddleComponent, out EntityUid uid, out PuddleComponent component) - { - MetaDataComponent? metadata = null; - Resolve(srcUid, ref metadata); - - var prototype = metadata?.EntityPrototype?.ID ?? "PuddleSmear"; // TODO Spawn a entity based on another entity - - uid = EntityManager.SpawnEntity(prototype, pos); - component = EntityManager.EnsureComponent(uid); - } + return false; } } diff --git a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs new file mode 100644 index 0000000000..2a6aa936b9 --- /dev/null +++ b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs @@ -0,0 +1,326 @@ +using System.Linq; +using Content.Server.Administration.Logs; +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Server.Chemistry.Components; +using Content.Server.Chemistry.EntitySystems; +using Content.Server.Chemistry.ReactionEffects; +using Content.Server.Coordinates.Helpers; +using Content.Server.Spreader; +using Content.Shared.Chemistry; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reaction; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Database; +using Content.Shared.FixedPoint; +using Content.Shared.Smoking; +using Content.Shared.Spawners; +using Content.Shared.Spawners.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.Fluids.EntitySystems; + +/// +/// Handles non-atmos solution entities similar to puddles. +/// +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!; + + /// + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnSmokeUnpaused); + SubscribeLocalEvent(OnSmokeMapInit); + SubscribeLocalEvent(OnReactionAttempt); + SubscribeLocalEvent(OnSmokeSpread); + SubscribeLocalEvent(OnSmokeDissipate); + SubscribeLocalEvent(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(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(uid); + return; + } + + var prototype = MetaData(uid).EntityPrototype; + + if (prototype == null) + { + RemCompDeferred(uid); + return; + } + + TryComp(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(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(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(); + + foreach (var neighbor in args.Neighbors) + { + if (!smokeQuery.TryGetComponent(neighbor, out var smoke)) + continue; + + smoke.SpreadAmount++; + args.Updates--; + + if (component.SpreadAmount == 0) + { + RemCompDeferred(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; + } + + /// + public override void Update(float frameTime) + { + base.Update(frameTime); + var query = EntityQueryEnumerator(); + 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); + } + } + + /// + /// Does the relevant smoke reactions for an entity for the specified exposure duration. + /// + 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(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(entity, out var bloodstream)) + return; + + if (TryComp(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)}"); + } + } + + /// + /// Sets up a smoke component for spreading. + /// + public void Start(EntityUid uid, SmokeComponent component, Solution solution, float duration) + { + TryAddSolution(uid, component, solution); + EnsureComp(uid); + var timer = EnsureComp(uid); + timer.Lifetime = duration; + } + + /// + /// Adds the specified solution to the relevant smoke solution. + /// + 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); + } +} diff --git a/Content.Server/Fluids/EntitySystems/SpillableSystem.cs b/Content.Server/Fluids/EntitySystems/SpillableSystem.cs deleted file mode 100644 index 54d3691c76..0000000000 --- a/Content.Server/Fluids/EntitySystems/SpillableSystem.cs +++ /dev/null @@ -1,366 +0,0 @@ -using Content.Server.Administration.Logs; -using Content.Server.Chemistry.EntitySystems; -using Content.Server.Fluids.Components; -using Content.Server.Nutrition.Components; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Clothing.Components; -using Content.Shared.Database; -using Content.Shared.FixedPoint; -using Content.Shared.Inventory.Events; -using Content.Shared.Throwing; -using Content.Shared.Verbs; -using JetBrains.Annotations; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Shared.Chemistry; -using Content.Shared.Chemistry.Reaction; -using Content.Shared.DoAfter; -using Content.Shared.Examine; -using Content.Shared.IdentityManagement; -using Content.Shared.Popups; -using Content.Shared.Spillable; -using Content.Shared.Weapons.Melee; -using Content.Shared.Weapons.Melee.Events; -using Robust.Shared.Player; -using Robust.Shared.Random; - -namespace Content.Server.Fluids.EntitySystems; - -[UsedImplicitly] -public sealed class SpillableSystem : EntitySystem -{ - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly PuddleSystem _puddleSystem = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly EntityLookupSystem _entityLookup = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly ReactiveSystem _reactive = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(SpillOnLand); - SubscribeLocalEvent(SplashOnMeleeHit); - SubscribeLocalEvent>(AddSpillVerb); - SubscribeLocalEvent(OnGotEquipped); - SubscribeLocalEvent(OnSpikeOverflow); - SubscribeLocalEvent(OnDoAfter); - } - - private void OnExamined(EntityUid uid, SpillableComponent component, ExaminedEvent args) - { - args.PushMarkup(Loc.GetString("spill-examine-is-spillable")); - - if (HasComp(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"); - } - - /// - /// Spills the specified solution at the entity's location if possible. - /// - /// - /// The entity to use as a location to spill the solution at. - /// - /// Initial solution for the prototype. - /// The prototype to use. - /// Play the spill sound. - /// Whether to attempt to merge with existing puddles - /// Optional Transform component - /// The puddle if one was created, null otherwise. - 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(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(uid, out var drink) && !drink.Opened) - return; - - var hitCount = args.HitEntities.Count; - - var totalSplit = FixedPoint2.Min(solution.MaxVolume * 0.25, solution.Volume); - if (TryComp(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(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 args) - { - if (!args.CanAccess || !args.CanInteract) - return; - - if (!_solutionContainerSystem.TryGetDrainableSolution(args.Target, out var solution)) - return; - - if (TryComp(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); - } - - /// - /// 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. - /// - 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(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); - } - - /// - /// Spills solution at the specified grid coordinates. - /// - /// Initial solution for the prototype. - /// The coordinates to spill the solution at. - /// The prototype to use. - /// If the puddle overflow will be calculated. Defaults to true. - /// Whether or not to play the spill sound. - /// Whether to attempt to merge with existing puddles - /// The puddle if one was created, null otherwise. - 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(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(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; - } -} diff --git a/Content.Server/Kudzu/GrowingKudzuComponent.cs b/Content.Server/Kudzu/GrowingKudzuComponent.cs deleted file mode 100644 index 11f6853553..0000000000 --- a/Content.Server/Kudzu/GrowingKudzuComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Content.Server.Kudzu; - -[RegisterComponent] -public sealed class GrowingKudzuComponent : Component -{ - [DataField("growthLevel")] - public int GrowthLevel = 1; - - [DataField("growthTickSkipChance")] - public float GrowthTickSkipChange = 0.0f; -} diff --git a/Content.Server/Kudzu/GrowingKudzuSystem.cs b/Content.Server/Kudzu/GrowingKudzuSystem.cs deleted file mode 100644 index 78ed38d076..0000000000 --- a/Content.Server/Kudzu/GrowingKudzuSystem.cs +++ /dev/null @@ -1,54 +0,0 @@ -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(SetupKudzu); - } - - private void SetupKudzu(EntityUid uid, GrowingKudzuComponent component, ComponentAdd args) - { - if (!EntityManager.TryGetComponent(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()) - { - if (kudzu.GrowthLevel >= 3 || !_robustRandom.Prob(kudzu.GrowthTickSkipChange)) continue; - kudzu.GrowthLevel += 1; - - if (kudzu.GrowthLevel == 3 && - EntityManager.TryGetComponent((kudzu).Owner, out var spreader)) - { - // why cache when you can simply cease to be? Also saves a bit of memory/time. - EntityManager.RemoveComponent((kudzu).Owner); - } - - _appearance.SetData(kudzu.Owner, KudzuVisuals.GrowthLevel, kudzu.GrowthLevel, appearance); - } - } -} diff --git a/Content.Server/Kudzu/SpreaderComponent.cs b/Content.Server/Kudzu/SpreaderComponent.cs deleted file mode 100644 index 88c9ba8060..0000000000 --- a/Content.Server/Kudzu/SpreaderComponent.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Content.Server.Kudzu; - -/// -/// 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. -/// -[RegisterComponent, Access(typeof(SpreaderSystem))] -public sealed class SpreaderComponent : Component -{ - /// - /// 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.). - /// - [DataField("chance", required: true)] - public float Chance; - - /// - /// Prototype spawned on growth success. - /// - [DataField("growthResult", required: true)] - public string GrowthResult = default!; - - [DataField("enabled")] - public bool Enabled = true; -} diff --git a/Content.Server/Kudzu/SpreaderSystem.cs b/Content.Server/Kudzu/SpreaderSystem.cs deleted file mode 100644 index d75c8ad024..0000000000 --- a/Content.Server/Kudzu/SpreaderSystem.cs +++ /dev/null @@ -1,140 +0,0 @@ -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!; - - /// - /// Maximum number of edges that can grow out every interval. - /// - private const int GrowthsPerInterval = 1; - - private float _accumulatedFrameTime = 0.0f; - - private readonly HashSet _edgeGrowths = new (); - - public override void Initialize() - { - SubscribeLocalEvent(SpreaderAddHandler); - SubscribeLocalEvent(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(blocker, out var transform)) - return; // how did we get here? - - if (!_mapManager.TryGetGrid(transform.GridUid, out var grid)) return; - - var spreaderQuery = GetEntityQuery(); - 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(ent, out _)) - return true; - - if (!EntityManager.TryGetComponent(ent, out var airtight)) - return false; - - var oppositeDir = dir.AsDir().GetOpposite().ToAtmosDirection(); - - return airtight.AirBlocked && airtight.AirBlockedDirection.IsFlagSet(oppositeDir); - } -} diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs index a36bc01ed6..57e8a257dd 100644 --- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs +++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs @@ -37,7 +37,7 @@ namespace Content.Server.Medical.BiomassReclaimer [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!; @@ -60,7 +60,7 @@ namespace Content.Server.Medical.BiomassReclaimer { 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) { diff --git a/Content.Server/Medical/VomitSystem.cs b/Content.Server/Medical/VomitSystem.cs index af7989d7ae..897ecb1ce9 100644 --- a/Content.Server/Medical/VomitSystem.cs +++ b/Content.Server/Medical/VomitSystem.cs @@ -7,6 +7,8 @@ using Content.Server.Nutrition.Components; 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; diff --git a/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs b/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs index b014f5dba0..56b1cee3d1 100644 --- a/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs +++ b/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs @@ -345,7 +345,7 @@ namespace Content.Server.NodeContainer.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)) { diff --git a/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs b/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs index b91a866084..e4dac00683 100644 --- a/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs +++ b/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs @@ -59,6 +59,7 @@ namespace Content.Server.NodeContainer.NodeGroups Apc, AMEngine, Pipe, - WireNet + WireNet, + Spreader, } } diff --git a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs index d983b3e147..1f1164f3d4 100644 --- a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs @@ -21,7 +21,7 @@ namespace Content.Server.Nutrition.EntitySystems 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!; @@ -43,7 +43,7 @@ namespace Content.Server.Nutrition.EntitySystems { 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)) { @@ -62,9 +62,9 @@ namespace Content.Server.Nutrition.EntitySystems 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(item.Value, out var timerTrigger)) { @@ -85,7 +85,7 @@ namespace Content.Server.Nutrition.EntitySystems { _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(args.Target, out var actor)) + if (TryComp(args.Target, out var actor)) { otherPlayers.RemovePlayer(actor.PlayerSession); } diff --git a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs index 64efdc89fe..470803298b 100644 --- a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs @@ -9,6 +9,7 @@ using Content.Server.Popups; 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; @@ -44,7 +45,7 @@ namespace Content.Server.Nutrition.EntitySystems [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!; @@ -85,7 +86,7 @@ namespace Content.Server.Nutrition.EntitySystems args.Message.AddMarkup($"\n{Loc.GetString("drink-component-on-examine-details-text", ("colorName", color), ("text", openedText))}"); if (!IsEmpty(uid, component)) { - if (TryComp(component.Owner, out var comp)) + if (TryComp(uid, out var comp)) { //provide exact measurement for beakers args.Message.AddMarkup($" - {Loc.GetString("drink-component-on-examine-exact-volume", ("amount", _solutionContainerSystem.DrainAvailable(uid)))}"); @@ -160,7 +161,7 @@ namespace Content.Server.Nutrition.EntitySystems 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)); } @@ -314,7 +315,7 @@ namespace Content.Server.Nutrition.EntitySystems if (HasComp(args.Args.Target.Value)) { - _spillableSystem.SpillAt(args.Args.User, drained, "PuddleSmear"); + _puddleSystem.TrySpillAt(args.Args.User, drained, out _); args.Handled = true; return; } @@ -334,7 +335,7 @@ namespace Content.Server.Nutrition.EntitySystems 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); diff --git a/Content.Server/Spreader/EdgeSpreaderComponent.cs b/Content.Server/Spreader/EdgeSpreaderComponent.cs new file mode 100644 index 0000000000..8a5c231d3f --- /dev/null +++ b/Content.Server/Spreader/EdgeSpreaderComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Server.Spreader; + +/// +/// Added to entities being considered for spreading via . +/// This needs to be manually added and removed. +/// +[RegisterComponent, Access(typeof(SpreaderSystem))] +public sealed class EdgeSpreaderComponent : Component +{ +} diff --git a/Content.Server/Spreader/EdgeSpreaderPrototype.cs b/Content.Server/Spreader/EdgeSpreaderPrototype.cs new file mode 100644 index 0000000000..c407559de5 --- /dev/null +++ b/Content.Server/Spreader/EdgeSpreaderPrototype.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Prototypes; + +namespace Content.Server.Spreader; + +/// +/// Adds this node group to for tick updates. +/// +[Prototype("edgeSpreader")] +public sealed class EdgeSpreaderPrototype : IPrototype +{ + [IdDataField] public string ID { get; } = string.Empty; +} diff --git a/Content.Server/Spreader/GrowingKudzuComponent.cs b/Content.Server/Spreader/GrowingKudzuComponent.cs new file mode 100644 index 0000000000..52a122ef19 --- /dev/null +++ b/Content.Server/Spreader/GrowingKudzuComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.Spreader; + +[RegisterComponent, Access(typeof(KudzuSystem))] +public sealed class GrowingKudzuComponent : Component +{ + /// + /// At level 3 spreading can occur; prior to that we have a chance of increasing our growth level and changing our sprite. + /// + [DataField("growthLevel")] + public int GrowthLevel = 1; + + [DataField("growthTickChance")] + public float GrowthTickChance = 1f; + + /// + /// The next time kudzu will try to tick its growth level. + /// + [DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))] + public TimeSpan NextTick = TimeSpan.Zero; +} diff --git a/Content.Server/Spreader/KudzuComponent.cs b/Content.Server/Spreader/KudzuComponent.cs new file mode 100644 index 0000000000..ae4aedf650 --- /dev/null +++ b/Content.Server/Spreader/KudzuComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Spreader; + +/// +/// Handles entities that spread out when they reach the relevant growth level. +/// +[RegisterComponent] +public sealed class KudzuComponent : Component +{ + /// + /// Chance to spread whenever an edge spread is possible. + /// + [DataField("spreadChance")] + public float SpreadChance = 1f; +} diff --git a/Content.Server/Spreader/KudzuSystem.cs b/Content.Server/Spreader/KudzuSystem.cs new file mode 100644 index 0000000000..7e35fb394d --- /dev/null +++ b/Content.Server/Spreader/KudzuSystem.cs @@ -0,0 +1,114 @@ +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"; + + /// + public override void Initialize() + { + SubscribeLocalEvent(SetupKudzu); + SubscribeLocalEvent(OnKudzuSpread); + SubscribeLocalEvent(OnKudzuUnpaused); + SubscribeLocalEvent(OnKudzuUpdateRate); + } + + private void OnKudzuSpread(EntityUid uid, KudzuComponent component, ref SpreadNeighborsEvent args) + { + if (TryComp(uid, out var growing) && growing.GrowthLevel < 3) + { + return; + } + + if (args.NeighborFreeTiles.Count == 0 || args.Grid == null) + { + RemCompDeferred(uid); + return; + } + + var prototype = MetaData(uid).EntityPrototype?.ID; + + if (prototype == null) + { + RemCompDeferred(uid); + return; + } + + if (!_robustRandom.Prob(component.SpreadChance)) + return; + + foreach (var neighbor in args.NeighborFreeTiles) + { + var neighborUid = Spawn(prototype, args.Grid.GridTileToLocal(neighbor)); + EnsureComp(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(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) + { + var query = EntityQueryEnumerator(); + 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(uid); + } + + _appearance.SetData(uid, KudzuVisuals.GrowthLevel, kudzu.GrowthLevel, appearance); + } + } +} diff --git a/Content.Server/Spreader/SpreadGroupUpdateRate.cs b/Content.Server/Spreader/SpreadGroupUpdateRate.cs new file mode 100644 index 0000000000..03c7c11512 --- /dev/null +++ b/Content.Server/Spreader/SpreadGroupUpdateRate.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Spreader; + +/// +/// Raised every tick to determine how many updates a particular spreading node group is allowed. +/// +[ByRefEvent] +public record struct SpreadGroupUpdateRate(string Name, int UpdatesPerSecond = 16); \ No newline at end of file diff --git a/Content.Server/Spreader/SpreadNeighborsEvent.cs b/Content.Server/Spreader/SpreadNeighborsEvent.cs new file mode 100644 index 0000000000..723e510d60 --- /dev/null +++ b/Content.Server/Spreader/SpreadNeighborsEvent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.Collections; +using Robust.Shared.Map.Components; + +namespace Content.Server.Spreader; + +/// +/// Raised when trying to spread to neighboring tiles. +/// If the spread is no longer able to happen you MUST cancel this event! +/// +[ByRefEvent] +public record struct SpreadNeighborsEvent +{ + public MapGridComponent? Grid; + public ValueList NeighborFreeTiles; + public ValueList NeighborOccupiedTiles; + public ValueList Neighbors; + + /// + /// How many updates allowed are remaining. + /// Subscribers can handle as they wish. + /// + public int Updates; +} \ No newline at end of file diff --git a/Content.Server/Spreader/SpreaderGridComponent.cs b/Content.Server/Spreader/SpreaderGridComponent.cs new file mode 100644 index 0000000000..2dff8d8747 --- /dev/null +++ b/Content.Server/Spreader/SpreaderGridComponent.cs @@ -0,0 +1,10 @@ +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; +} diff --git a/Content.Server/Spreader/SpreaderNode.cs b/Content.Server/Spreader/SpreaderNode.cs new file mode 100644 index 0000000000..3cf753f6d5 --- /dev/null +++ b/Content.Server/Spreader/SpreaderNode.cs @@ -0,0 +1,33 @@ +using Content.Server.NodeContainer; +using Content.Server.NodeContainer.Nodes; +using Robust.Shared.Map.Components; + +namespace Content.Server.Spreader; + +/// +/// Handles the node for . +/// Functions as a generic tile-based entity spreader for systems such as puddles or smoke. +/// +public sealed class SpreaderNode : Node +{ + /// + public override IEnumerable GetReachableNodes(TransformComponent xform, EntityQuery nodeQuery, EntityQuery xformQuery, + MapGridComponent? grid, IEntityManager entMan) + { + if (grid == null) + yield break; + + entMan.System().GetNeighbors(xform.Owner, Name, out _, out _, out var neighbors); + + foreach (var neighbor in neighbors) + { + if (!nodeQuery.TryGetComponent(neighbor, out var nodeContainer) || + !nodeContainer.TryGetNode(Name, out var neighborNode)) + { + continue; + } + + yield return neighborNode; + } + } +} diff --git a/Content.Server/Spreader/SpreaderNodeGroup.cs b/Content.Server/Spreader/SpreaderNodeGroup.cs new file mode 100644 index 0000000000..a94ad83b0a --- /dev/null +++ b/Content.Server/Spreader/SpreaderNodeGroup.cs @@ -0,0 +1,31 @@ +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!; + + /// + public override void Initialize(Node sourceNode, IEntityManager entMan) + { + base.Initialize(sourceNode, entMan); + _entManager = entMan; + } + + /// + public override void RemoveNode(Node node) + { + base.RemoveNode(node); + + foreach (var neighborNode in node.ReachableNodes) + { + if (_entManager.Deleted(neighborNode.Owner)) + continue; + + _entManager.EnsureComponent(neighborNode.Owner); + } + } +} diff --git a/Content.Server/Spreader/SpreaderSystem.cs b/Content.Server/Spreader/SpreaderSystem.cs new file mode 100644 index 0000000000..03b3e45596 --- /dev/null +++ b/Content.Server/Spreader/SpreaderSystem.cs @@ -0,0 +1,324 @@ +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; + +/// +/// Handles generic spreading logic, where one anchored entity spreads to neighboring tiles. +/// +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 _spreaderGroups = new(); + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnAirtightChanged); + SubscribeLocalEvent(OnGridInit); + SubscribeLocalEvent(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()) + { + _spreaderGroups.Add(id.ID); + } + } + + private void OnAirtightChanged(ref AirtightChanged ev) + { + var neighbors = GetNeighbors(ev.Entity, ev.Airtight); + + foreach (var neighbor in neighbors) + { + EnsureComp(neighbor); + } + } + + private void OnGridUnpaused(EntityUid uid, SpreaderGridComponent component, ref EntityUnpausedEvent args) + { + component.NextUpdate += args.PausedTime; + } + + private void OnGridInit(GridInitializeEvent ev) + { + var comp = EnsureComp(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; + } + + /// + public override void Update(float frameTime) + { + var curTime = _timing.CurTime; + + // Check which grids are valid for spreading. + var spreadable = new ValueList(); + var spreadGrids = EntityQueryEnumerator(); + + 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(); + var nodeQuery = GetEntityQuery(); + var xformQuery = GetEntityQuery(); + var gridQuery = GetEntityQuery(); + + // Events and stuff + var groupUpdates = new Dictionary(); + var spreaders = new List<(EntityUid Uid, EdgeSpreaderComponent Comp)>(Count()); + + 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(uid); + continue; + } + + foreach (var sGroup in _spreaderGroups) + { + // Cleanup + if (!nodeQuery.TryGetComponent(uid, out var nodeComponent)) + { + RemCompDeferred(uid); + continue; + } + + if (!nodeComponent.TryGetNode(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(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; + } + + /// + /// Gets the neighboring node data for the specified entity and the specified node group. + /// + public void GetNeighbors(EntityUid uid, string groupName, out ValueList freeTiles, out ValueList occupiedTiles, out ValueList neighbors) + { + freeTiles = new ValueList(); + occupiedTiles = new ValueList(); + neighbors = new ValueList(); + + if (!EntityManager.TryGetComponent(uid, out var transform)) + return; + + if (!_mapManager.TryGetGrid(transform.GridUid, out var grid)) + return; + + var tile = grid.TileIndicesFor(transform.Coordinates); + var nodeQuery = GetEntityQuery(); + var airtightQuery = GetEntityQuery(); + + 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 GetNeighbors(EntityUid uid, AirtightComponent comp) + { + var neighbors = new List(); + + if (!EntityManager.TryGetComponent(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(); + + 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; + } +} diff --git a/Content.Server/StationEvents/Events/VentClog.cs b/Content.Server/StationEvents/Events/VentClog.cs index 429596de14..04e466f66b 100644 --- a/Content.Server/StationEvents/Events/VentClog.cs +++ b/Content.Server/StationEvents/Events/VentClog.cs @@ -1,5 +1,4 @@ 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; @@ -7,6 +6,9 @@ using JetBrains.Annotations; 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; @@ -43,6 +45,7 @@ public sealed class VentClog : StationEventSystem { continue; } + var solution = new Solution(); if (!RobustRandom.Prob(Math.Min(0.33f * mod, 1.0f))) @@ -50,15 +53,18 @@ public sealed class VentClog : StationEventSystem 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(foamEnt); + smoke.SpreadAmount = 20; + EntityManager.System().Start(foamEnt, smoke, solution, 20f); + EntityManager.System().PlayPvs(sound, transform.Coordinates); } } diff --git a/Content.Server/Tools/ToolSystem.TilePrying.cs b/Content.Server/Tools/ToolSystem.TilePrying.cs index 59c041010b..cefeccbf74 100644 --- a/Content.Server/Tools/ToolSystem.TilePrying.cs +++ b/Content.Server/Tools/ToolSystem.TilePrying.cs @@ -2,6 +2,7 @@ using System.Threading; 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; diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs index 212ac90e29..7d8c0873e9 100644 --- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs @@ -221,9 +221,20 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem 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) diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/ChemicalPuddleArtifactComponent.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/ChemicalPuddleArtifactComponent.cs index fb05b425b4..ad75e1b95d 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/ChemicalPuddleArtifactComponent.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/ChemicalPuddleArtifactComponent.cs @@ -14,12 +14,6 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components; [RegisterComponent, Access(typeof(ChemicalPuddleArtifactSystem))] public sealed class ChemicalPuddleArtifactComponent : Component { - /// - /// The prototype id of the puddle - /// - [DataField("puddlePrototype", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] - public string PuddlePrototype = "PuddleSmear"; - /// /// The solution where all the chemicals are stored /// diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ChemicalPuddleArtifactSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ChemicalPuddleArtifactSystem.cs index 96729ca49f..cd312797ce 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ChemicalPuddleArtifactSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ChemicalPuddleArtifactSystem.cs @@ -12,7 +12,7 @@ public sealed class ChemicalPuddleArtifactSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ArtifactSystem _artifact = default!; - [Dependency] private readonly SpillableSystem _spillable = default!; + [Dependency] private readonly PuddleSystem _puddle = default!; /// /// The key for the node data entry containing @@ -49,6 +49,6 @@ public sealed class ChemicalPuddleArtifactSystem : EntitySystem component.ChemicalSolution.AddReagent(reagent, amountPerChem); } - _spillable.SpillAt(uid, component.ChemicalSolution, component.PuddlePrototype); + _puddle.TrySpillAt(uid, component.ChemicalSolution, out _); } } diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/FoamArtifactSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/FoamArtifactSystem.cs index d034c4989c..667d1363d9 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/FoamArtifactSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/FoamArtifactSystem.cs @@ -1,8 +1,11 @@ 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; @@ -33,10 +36,12 @@ public sealed class FoamArtifactSystem : EntitySystem 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(foamEnt); + EntityManager.System().Start(foamEnt, smoke, sol, 20f); } } diff --git a/Content.Shared/Access/Components/IdCardComponent.cs b/Content.Shared/Access/Components/IdCardComponent.cs index 5c11fef78a..814227d3e5 100644 --- a/Content.Shared/Access/Components/IdCardComponent.cs +++ b/Content.Shared/Access/Components/IdCardComponent.cs @@ -18,6 +18,8 @@ namespace Content.Shared.Access.Components [DataField("jobTitle")] [AutoNetworkedField] + [Access(typeof(SharedIdCardSystem), typeof(SharedPDASystem), typeof(SharedAgentIdCardSystem), + Other = AccessPermissions.ReadWrite)] public string? JobTitle; } } diff --git a/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs b/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs new file mode 100644 index 0000000000..2a69a88088 --- /dev/null +++ b/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Chemistry.Components; + +/// +/// Denotes the solution that can be easily removed through any reagent container. +/// Think pouring this or draining from a water tank. +/// +[RegisterComponent, NetworkedComponent] +public sealed class DrainableSolutionComponent : Component +{ + /// + /// Solution name that can be drained. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("solution")] + public string Solution { get; set; } = "default"; +} diff --git a/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs new file mode 100644 index 0000000000..49f2edbf7b --- /dev/null +++ b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs @@ -0,0 +1,27 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; + +namespace Content.Shared.Chemistry.Components; + +/// +/// Reagents that can be added easily. For example like +/// pouring something into another beaker, glass, or into the gas +/// tank of a car. +/// +[RegisterComponent, NetworkedComponent] +public sealed class RefillableSolutionComponent : Component +{ + /// + /// Solution name that can added to easily. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("solution")] + public string Solution { get; set; } = "default"; + + /// + /// The maximum amount that can be transferred to the solution at once + /// + [DataField("maxRefill")] + [ViewVariables(VVAccess.ReadWrite)] + public FixedPoint2? MaxRefill { get; set; } = null; +} diff --git a/Content.Shared/Chemistry/Components/Solution.cs b/Content.Shared/Chemistry/Components/Solution.cs index 5beab83ff3..353733e715 100644 --- a/Content.Shared/Chemistry/Components/Solution.cs +++ b/Content.Shared/Chemistry/Components/Solution.cs @@ -432,6 +432,18 @@ namespace Content.Shared.Chemistry.Components _heatCapacity = 0; } + /// + /// Splits a solution without the specified reagent. + /// + 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) @@ -599,7 +611,7 @@ namespace Content.Shared.Chemistry.Components ValidateSolution(); } - public Color GetColor(IPrototypeManager? protoMan) + public Color GetColorWithout(IPrototypeManager? protoMan, params string[] without) { if (Volume == FixedPoint2.Zero) { @@ -614,6 +626,9 @@ namespace Content.Shared.Chemistry.Components foreach (var reagent in Contents) { + if (without.Contains(reagent.ReagentId)) + continue; + runningTotalQuantity += reagent.Quantity; if (!protoMan.TryIndex(reagent.ReagentId, out ReagentPrototype? proto)) @@ -634,6 +649,11 @@ namespace Content.Shared.Chemistry.Components return mixColor; } + public Color GetColor(IPrototypeManager? protoMan) + { + return GetColorWithout(protoMan); + } + [Obsolete("Use ReactiveSystem.DoEntityReaction")] public void DoEntityReaction(EntityUid uid, ReactionMethod method) { diff --git a/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs b/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs index b1937819a4..a2a4fec87a 100644 --- a/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs +++ b/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs @@ -250,7 +250,10 @@ namespace Content.Shared.Chemistry.Reaction 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; diff --git a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs index 829b9ba310..09bdb41bef 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs @@ -5,6 +5,7 @@ using Content.Shared.Chemistry.Components; 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; @@ -73,6 +74,12 @@ namespace Content.Shared.Chemistry.Reagent [DataField("metamorphicSprite")] public SpriteSpecifier? MetamorphicSprite { get; } = null; + /// + /// If this reagent is part of a puddle is it slippery. + /// + [DataField("slippery")] + public bool Slippery = false; + [DataField("metabolisms", serverOnly: true, customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] public Dictionary? Metabolisms = null; @@ -85,26 +92,11 @@ namespace Content.Shared.Chemistry.Reagent [DataField("plantMetabolism", serverOnly: true)] public readonly List PlantMetabolisms = new(0); - [DataField("pricePerUnit")] - public float PricePerUnit { get; } + [DataField("pricePerUnit")] public float PricePerUnit; - /// - /// If the substance color is too dark we user a lighter version to make the text color readable when the user examines a solution. - /// - 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) { diff --git a/Content.Shared/Fluids/AbsorbentComponent.cs b/Content.Shared/Fluids/AbsorbentComponent.cs index 011e123e90..b68f2d8b97 100644 --- a/Content.Shared/Fluids/AbsorbentComponent.cs +++ b/Content.Shared/Fluids/AbsorbentComponent.cs @@ -10,48 +10,31 @@ namespace Content.Shared.Fluids; [RegisterComponent, NetworkedComponent] public sealed class AbsorbentComponent : Component { - // TODO: Predicted solutions my beloved. - public float Progress; - public const string SolutionName = "absorbed"; + public Dictionary Progress = new(); + + /// + /// How much solution we can transfer in one interaction. + /// [DataField("pickupAmount")] - public FixedPoint2 PickupAmount = FixedPoint2.New(10); - - /// - /// When using this tool on an empty floor tile, leave this much reagent as a new puddle. - /// - [DataField("residueAmount")] - public FixedPoint2 ResidueAmount = FixedPoint2.New(10); // Should be higher than MopLowerLimit - - /// - /// 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. - /// - [DataField("lowerLimit")] - public FixedPoint2 LowerLimit = FixedPoint2.New(5); + public FixedPoint2 PickupAmount = FixedPoint2.New(60); [DataField("pickupSound")] - public SoundSpecifier PickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg"); + 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/watersplash.ogg"); - - /// - /// Quantity of reagent that this mop can pick up per second. Determines the length of the do-after. - /// - [DataField("speed")] public float Speed = 10; - - /// - /// How many entities can this tool interact with at once? - /// - [DataField("maxEntities")] - public int MaxInteractingEntities = 1; - - /// - /// What entities is this tool interacting with right now? - /// - [ViewVariables] - public HashSet InteractingEntities = new(); + [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), + }; } diff --git a/Content.Shared/Fluids/Components/DrainComponent.cs b/Content.Shared/Fluids/Components/DrainComponent.cs new file mode 100644 index 0000000000..e4879620fc --- /dev/null +++ b/Content.Shared/Fluids/Components/DrainComponent.cs @@ -0,0 +1,40 @@ +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; + + /// + /// 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. + /// + [DataField("unitsPerSecond")] + public float UnitsPerSecond = 6f; + + /// + /// How many units are ejected from the buffer per second. + /// + [DataField("unitsDestroyedPerSecond")] + public float UnitsDestroyedPerSecond = 1f; + + /// + /// How many (unobstructed) tiles away the drain will + /// drain puddles from. + /// + [DataField("range")] + public float Range = 2f; + + /// + /// How often in seconds the drain checks for puddles around it. + /// If the EntityQuery seems a bit unperformant this can be increased. + /// + [DataField("drainFrequency")] + public float DrainFrequency = 1f; +} diff --git a/Content.Shared/Fluids/Components/PuddleComponent.cs b/Content.Shared/Fluids/Components/PuddleComponent.cs new file mode 100644 index 0000000000..614d6701c5 --- /dev/null +++ b/Content.Shared/Fluids/Components/PuddleComponent.cs @@ -0,0 +1,21 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Fluids.Components +{ + /// + /// Puddle on a floor + /// + [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"; + } +} diff --git a/Content.Shared/Fluids/PuddleVisuals.cs b/Content.Shared/Fluids/PuddleVisuals.cs index 89c0eb9238..b50f084ba8 100644 --- a/Content.Shared/Fluids/PuddleVisuals.cs +++ b/Content.Shared/Fluids/PuddleVisuals.cs @@ -5,9 +5,7 @@ namespace Content.Shared.Fluids [Serializable, NetSerializable] public enum PuddleVisuals : byte { - VolumeScale, CurrentVolume, SolutionColor, - IsEvaporatingVisual } } diff --git a/Content.Shared/Fluids/SharedMoppingSystem.cs b/Content.Shared/Fluids/SharedAbsorbentSystem.cs similarity index 56% rename from Content.Shared/Fluids/SharedMoppingSystem.cs rename to Content.Shared/Fluids/SharedAbsorbentSystem.cs index 8e6dcc829a..f121238d51 100644 --- a/Content.Shared/Fluids/SharedMoppingSystem.cs +++ b/Content.Shared/Fluids/SharedAbsorbentSystem.cs @@ -1,9 +1,13 @@ +using System.Linq; using Robust.Shared.GameStates; using Robust.Shared.Serialization; namespace Content.Shared.Fluids; -public abstract class SharedMoppingSystem : EntitySystem +/// +/// Mopping logic for interacting with puddle components. +/// +public abstract class SharedAbsorbentSystem : EntitySystem { public override void Initialize() { @@ -17,23 +21,29 @@ public abstract class SharedMoppingSystem : EntitySystem 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 Progress; + + public AbsorbentComponentState(Dictionary progress) + { + Progress = progress; + } } } diff --git a/Content.Shared/Fluids/SharedPuddleSystem.cs b/Content.Shared/Fluids/SharedPuddleSystem.cs new file mode 100644 index 0000000000..c693632238 --- /dev/null +++ b/Content.Shared/Fluids/SharedPuddleSystem.cs @@ -0,0 +1,44 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.DragDrop; +using Content.Shared.Fluids.Components; + +namespace Content.Shared.Fluids; + +public abstract class SharedPuddleSystem : EntitySystem +{ + /// + /// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle. + /// + public const float LowThreshold = 0.3f; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnRefillableCanDrag); + SubscribeLocalEvent(OnRefillableCanDropDragged); + SubscribeLocalEvent(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(args.Dragged)) + { + args.CanDrop = true; + args.Handled = true; + } + } + + private void OnRefillableCanDropDragged(EntityUid uid, RefillableSolutionComponent component, ref CanDropDraggedEvent args) + { + if (!HasComp(args.Target) && !HasComp(args.Target)) + return; + + args.CanDrop = true; + args.Handled = true; + } +} diff --git a/Content.Shared/Foam/FoamVisuals.cs b/Content.Shared/Foam/FoamVisuals.cs deleted file mode 100644 index 7767d49220..0000000000 --- a/Content.Shared/Foam/FoamVisuals.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.Foam -{ - [Serializable, NetSerializable] - public enum FoamVisuals : byte - { - State, - Color - } -} diff --git a/Content.Shared/Movement/Events/GetFootstepSoundEvent.cs b/Content.Shared/Movement/Events/GetFootstepSoundEvent.cs new file mode 100644 index 0000000000..859963721a --- /dev/null +++ b/Content.Shared/Movement/Events/GetFootstepSoundEvent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.Audio; + +namespace Content.Shared.Movement.Events; + +/// +/// Raised directed on an entity when trying to get a relevant footstep sound +/// +[ByRefEvent] +public record struct GetFootstepSoundEvent(EntityUid User) +{ + public readonly EntityUid User = User; + + /// + /// Set the sound to specify a footstep sound and mark as handled. + /// + public SoundSpecifier? Sound; +} diff --git a/Content.Shared/Movement/Systems/SharedMoverController.cs b/Content.Shared/Movement/Systems/SharedMoverController.cs index 6bd64e89ab..9aec5e07f6 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.cs @@ -466,10 +466,9 @@ namespace Content.Shared.Movement.Systems 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(xform.MapUid, out var modifier)) { @@ -480,8 +479,8 @@ namespace Content.Shared.Movement.Systems 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 @@ -489,6 +488,14 @@ namespace Content.Shared.Movement.Systems while (anchored.MoveNext(out var maybeFootstep)) { + RaiseLocalEvent(maybeFootstep.Value, ref soundEv); + + if (soundEv.Sound != null) + { + sound = soundEv.Sound; + return true; + } + if (TryComp(maybeFootstep, out var footstep)) { sound = footstep.Sound; diff --git a/Content.Shared/Spawners/Components/TimedDespawnComponent.cs b/Content.Shared/Spawners/Components/TimedDespawnComponent.cs index 8f3dc6edc5..6a851e1756 100644 --- a/Content.Shared/Spawners/Components/TimedDespawnComponent.cs +++ b/Content.Shared/Spawners/Components/TimedDespawnComponent.cs @@ -1,9 +1,11 @@ +using Robust.Shared.GameStates; + namespace Content.Shared.Spawners.Components; /// /// Put this component on something you would like to despawn after a certain amount of time /// -[RegisterComponent] +[RegisterComponent, NetworkedComponent] public sealed class TimedDespawnComponent : Component { /// diff --git a/Content.Shared/Spawners/EntitySystems/SharedTimedDespawnSystem.cs b/Content.Shared/Spawners/EntitySystems/SharedTimedDespawnSystem.cs index 87f848c06d..bc9c41b93e 100644 --- a/Content.Shared/Spawners/EntitySystems/SharedTimedDespawnSystem.cs +++ b/Content.Shared/Spawners/EntitySystems/SharedTimedDespawnSystem.cs @@ -1,4 +1,6 @@ using Content.Shared.Spawners.Components; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; using Robust.Shared.Timing; namespace Content.Shared.Spawners.EntitySystems; @@ -11,24 +13,56 @@ public abstract class SharedTimedDespawnSystem : EntitySystem { base.Initialize(); UpdatesOutsidePrediction = true; + SubscribeLocalEvent(OnDespawnGetState); + SubscribeLocalEvent(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()) + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp)) { - if (!CanDelete(comp.Owner)) continue; - 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; + } } diff --git a/Content.Shared/Spawners/TimedDespawnEvent.cs b/Content.Shared/Spawners/TimedDespawnEvent.cs new file mode 100644 index 0000000000..26551d86bf --- /dev/null +++ b/Content.Shared/Spawners/TimedDespawnEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Spawners; + +/// +/// Raised directed on an entity when its timed despawn is over. +/// +[ByRefEvent] +public readonly record struct TimedDespawnEvent; diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs index e5aa4177c2..bd3fa842b4 100644 --- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -775,5 +775,5 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem 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); } diff --git a/Resources/Locale/en-US/fluids/components/absorbent-component.ftl b/Resources/Locale/en-US/fluids/components/absorbent-component.ftl index 9fc4981d8c..670ac0a36a 100644 --- a/Resources/Locale/en-US/fluids/components/absorbent-component.ftl +++ b/Resources/Locale/en-US/fluids/components/absorbent-component.ftl @@ -1,15 +1,8 @@ - -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! diff --git a/Resources/Locale/en-US/fluids/components/puddle-component.ftl b/Resources/Locale/en-US/fluids/components/puddle-component.ftl index 4114733e89..e1ae9bab4b 100644 --- a/Resources/Locale/en-US/fluids/components/puddle-component.ftl +++ b/Resources/Locale/en-US/fluids/components/puddle-component.ftl @@ -1,2 +1,5 @@ -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! diff --git a/Resources/Maps/Test/dev_map.yml b/Resources/Maps/Test/dev_map.yml index 39ecdc3a3e..380ecc1fce 100644 --- a/Resources/Maps/Test/dev_map.yml +++ b/Resources/Maps/Test/dev_map.yml @@ -266,12 +266,6 @@ entities: - 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: @@ -536,12 +530,6 @@ entities: - 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: @@ -685,12 +673,6 @@ entities: - 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: @@ -715,12 +697,6 @@ entities: - 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: diff --git a/Resources/Prototypes/Entities/Effects/chemistry_effects.yml b/Resources/Prototypes/Entities/Effects/chemistry_effects.yml index 29671f4179..beea18f540 100644 --- a/Resources/Prototypes/Entities/Effects/chemistry_effects.yml +++ b/Resources/Prototypes/Entities/Effects/chemistry_effects.yml @@ -3,21 +3,32 @@ 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 @@ -34,7 +45,9 @@ - state: foam map: ["enum.FoamVisualLayers.Base"] - type: AnimationPlayer + netsync: false - type: Appearance + - type: SmokeVisuals - type: FoamVisuals animationTime: 0.6 animationState: foam-dissolve @@ -51,7 +64,13 @@ - ItemMask layer: - SlipLayer - - type: FoamSolutionAreaEffect + - type: Smoke + - type: NodeContainer + nodes: + smoke: + !type:SpreaderNode + nodeGroupID: Spreader + - type: EdgeSpreader - type: SolutionContainerManager solutions: solutionArea: @@ -72,11 +91,13 @@ - 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 @@ -90,11 +111,13 @@ - 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 diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml index d7532583f2..f48ee736ea 100644 --- a/Resources/Prototypes/Entities/Effects/puddle.yml +++ b/Resources/Prototypes/Entities/Effects/puddle.yml @@ -1,204 +1,117 @@ -# 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 - -- type: entity - name: gibblets - id: PuddleGibblet - parent: PuddleBase - description: Gross. - 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: entity - name: puddle id: PuddleSmear - parent: EvaporatingPuddle - description: A puddle of liquid. + parent: PuddleTemporary + +- type: entity + id: PuddleVomit + parent: PuddleTemporary 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 + - type: SolutionContainerManager + solutions: + puddle: + maxVol: 1000 + reagents: + - ReagentId: Nutriment + Quantity: 5 + - ReagentId: Water + Quantity: 5 + +- type: entity + id: PuddleEgg + parent: PuddleTemporary + +- type: entity + id: PuddleTomato + parent: PuddleTemporary + +- type: entity + id: PuddleWatermelon + parent: PuddleTemporary + +- type: entity + id: PuddleFlour + parent: PuddleTemporary + +- type: entity + id: PuddleSparkle + name: Sparkle + placement: + mode: SnapgridCenter + components: + # 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: puddle - id: PuddleSplatter - parent: EvaporatingPuddle + id: Puddle description: A puddle of liquid. + placement: + mode: SnapgridCenter 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 - -- 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 - -- 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 - -- type: entity - name: toxins vomit - id: PuddleVomitToxin - parent: PuddleVomit - description: You probably don't want to get too close to this. - 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 - -- type: entity - name: writing - id: PuddleWriting - parent: PuddleBase - description: A bit of liquid. - 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 diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml index 9ecfae3324..697aa54970 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml @@ -67,25 +67,6 @@ 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 diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml index 8839b4be89..b8794de4fd 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml @@ -2,28 +2,6 @@ # 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 diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index d20aacdc1f..5910191b36 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -501,24 +501,6 @@ - !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 @@ -1097,24 +1079,6 @@ 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 diff --git a/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml b/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml index 3fd82dc798..af62439c87 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml @@ -69,22 +69,27 @@ 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 @@ -128,9 +133,6 @@ behaviors: - !type:DoActsBehavior acts: [ "Destruction" ] - - type: Spreader - growthResult: FleshKudzu - chance: 0.2 - type: DamageContacts damage: types: @@ -139,6 +141,13 @@ ignoreWhitelist: tags: - Flesh + - type: Kudzu + - type: EdgeSpreader + - type: NodeContainer + nodes: + kudzu: + !type:SpreaderNode + nodeGroupID: Spreader - type: SlowContacts walkSpeedModifier: 0.3 sprintSpeedModifier: 0.3 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml index 7595028cb0..9db467e3ce 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml @@ -12,6 +12,8 @@ damage: types: Blunt: 10 + - type: Spillable + solution: absorbed - type: Wieldable - type: IncreaseDamageOnWield damage: @@ -24,7 +26,9 @@ - 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 @@ -43,13 +47,15 @@ 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: @@ -63,11 +69,11 @@ 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 @@ -80,9 +86,6 @@ 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 @@ -117,6 +120,25 @@ 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 diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml index e9fb7540ca..9998353d6b 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml @@ -110,6 +110,7 @@ name: reagent-name-ethanol parent: BaseAlcohol desc: reagent-desc-ethanol + slippery: true physicalDesc: reagent-physical-desc-strong-smelling flavor: alcohol color: "#b05b3c" diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml b/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml index e0267dfca6..a87fc3cee0 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml @@ -2,6 +2,7 @@ id: BaseDrink group: Drinks abstract: true + slippery: true metabolisms: Drink: effects: diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml index 89365d1d77..66f3abdca8 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml @@ -279,6 +279,7 @@ name: reagent-name-water parent: BaseDrink desc: reagent-desc-water + slippery: true physicalDesc: reagent-physical-desc-translucent flavor: water color: "#75b1f0" diff --git a/Resources/Prototypes/Reagents/biological.yml b/Resources/Prototypes/Reagents/biological.yml index 8a7aa61d6e..7ff91a39a3 100644 --- a/Resources/Prototypes/Reagents/biological.yml +++ b/Resources/Prototypes/Reagents/biological.yml @@ -6,6 +6,7 @@ flavor: metallic color: "#800000" physicalDesc: reagent-physical-desc-ferrous + slippery: false metabolisms: Drink: # Quenching! @@ -24,6 +25,7 @@ flavor: slimy color: "#2cf274" physicalDesc: reagent-physical-desc-viscous + slippery: false metabolisms: Food: # Delicious! diff --git a/Resources/Prototypes/Reagents/pyrotechnic.yml b/Resources/Prototypes/Reagents/pyrotechnic.yml index f4a2961464..bc77f44837 100644 --- a/Resources/Prototypes/Reagents/pyrotechnic.yml +++ b/Resources/Prototypes/Reagents/pyrotechnic.yml @@ -142,6 +142,7 @@ parent: BasePyrotechnic desc: reagent-desc-welding-fuel physicalDesc: reagent-physical-desc-oily + slippery: true flavor: bitter color: "#a76b1c" boilingPoint: -84.7 # Acetylene. Close enough. diff --git a/Resources/Prototypes/Recipes/Reactions/chemicals.yml b/Resources/Prototypes/Recipes/Reactions/chemicals.yml index bce981a42b..5f575d2af0 100644 --- a/Resources/Prototypes/Recipes/Reactions/chemicals.yml +++ b/Resources/Prototypes/Recipes/Reactions/chemicals.yml @@ -119,14 +119,8 @@ 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 @@ -141,17 +135,8 @@ 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 @@ -168,17 +153,8 @@ 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 @@ -195,17 +171,8 @@ 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 @@ -378,28 +345,28 @@ 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 diff --git a/Resources/Prototypes/edge_spreaders.yml b/Resources/Prototypes/edge_spreaders.yml new file mode 100644 index 0000000000..4cd1ada1c1 --- /dev/null +++ b/Resources/Prototypes/edge_spreaders.yml @@ -0,0 +1,8 @@ +- type: edgeSpreader + id: kudzu + +- type: edgeSpreader + id: puddle + +- type: edgeSpreader + id: smoke \ No newline at end of file diff --git a/Resources/Textures/Fluids/newliquid.png b/Resources/Textures/Fluids/newliquid.png new file mode 100644 index 0000000000..4d6b400a2c Binary files /dev/null and b/Resources/Textures/Fluids/newliquid.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/meta.json b/Resources/Textures/Fluids/puddle.rsi/meta.json new file mode 100644 index 0000000000..b47a24b44d --- /dev/null +++ b/Resources/Textures/Fluids/puddle.rsi/meta.json @@ -0,0 +1,65 @@ +{ + "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 diff --git a/Resources/Textures/Fluids/puddle.rsi/splat0.png b/Resources/Textures/Fluids/puddle.rsi/splat0.png new file mode 100644 index 0000000000..e6635f0868 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat0.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat1.png b/Resources/Textures/Fluids/puddle.rsi/splat1.png new file mode 100644 index 0000000000..8579a08759 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat1.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat10.png b/Resources/Textures/Fluids/puddle.rsi/splat10.png new file mode 100644 index 0000000000..2b2abe9c32 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat10.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat11.png b/Resources/Textures/Fluids/puddle.rsi/splat11.png new file mode 100644 index 0000000000..b3f0497f72 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat11.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat12.png b/Resources/Textures/Fluids/puddle.rsi/splat12.png new file mode 100644 index 0000000000..a8a2e7f90c Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat12.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat13.png b/Resources/Textures/Fluids/puddle.rsi/splat13.png new file mode 100644 index 0000000000..a1cb82ad90 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat13.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat14.png b/Resources/Textures/Fluids/puddle.rsi/splat14.png new file mode 100644 index 0000000000..fa2c45ae75 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat14.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat15.png b/Resources/Textures/Fluids/puddle.rsi/splat15.png new file mode 100644 index 0000000000..f1f26f871e Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat15.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat2.png b/Resources/Textures/Fluids/puddle.rsi/splat2.png new file mode 100644 index 0000000000..94d18f32d3 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat2.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat3.png b/Resources/Textures/Fluids/puddle.rsi/splat3.png new file mode 100644 index 0000000000..dbffc99bb7 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat3.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat4.png b/Resources/Textures/Fluids/puddle.rsi/splat4.png new file mode 100644 index 0000000000..e99174ba36 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat4.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat5.png b/Resources/Textures/Fluids/puddle.rsi/splat5.png new file mode 100644 index 0000000000..ca044c8285 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat5.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat6.png b/Resources/Textures/Fluids/puddle.rsi/splat6.png new file mode 100644 index 0000000000..02ca2579e5 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat6.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat7.png b/Resources/Textures/Fluids/puddle.rsi/splat7.png new file mode 100644 index 0000000000..05bbc5b8a9 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat7.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat8.png b/Resources/Textures/Fluids/puddle.rsi/splat8.png new file mode 100644 index 0000000000..a2492877d6 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat8.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splat9.png b/Resources/Textures/Fluids/puddle.rsi/splat9.png new file mode 100644 index 0000000000..05122c6752 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splat9.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splata.png b/Resources/Textures/Fluids/puddle.rsi/splata.png new file mode 100644 index 0000000000..f3e5d766bc Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splata.png differ diff --git a/Resources/Textures/Fluids/puddle.rsi/splatb.png b/Resources/Textures/Fluids/puddle.rsi/splatb.png new file mode 100644 index 0000000000..87e9da0263 Binary files /dev/null and b/Resources/Textures/Fluids/puddle.rsi/splatb.png differ