From 317a4013eb93d1c0b2280834dc837e59bc83e444 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Mon, 10 Apr 2023 15:37:03 +1000 Subject: [PATCH] Puddles & spreader refactor (#15191) --- .../Atmos/UI/GasAnalyzerWindow.xaml | 2 +- .../Atmos/UI/GasAnalyzerWindow.xaml.cs | 30 +- .../Visualizers/FoamVisualizerSystem.cs | 53 +- .../{MoppingSystem.cs => AbsorbentSystem.cs} | 3 +- Content.Client/Fluids/PuddleSystem.cs | 67 ++ .../Fluids/PuddleVisualizerComponent.cs | 30 - .../Fluids/PuddleVisualizerSystem.cs | 105 --- .../Fluids/UI/AbsorbentItemStatus.xaml | 17 +- .../Fluids/UI/AbsorbentItemStatus.xaml.cs | 26 +- .../IconSmoothing/IconSmoothComponent.cs | 3 + .../IconSmoothing/IconSmoothSystem.cs | 84 +- .../UserInterface/Controls/SplitBar.xaml | 7 + .../UserInterface/Controls/SplitBar.xaml.cs | 39 + .../Melee/MeleeWeaponSystem.Effects.cs | 2 +- .../Tests/Fluids/FluidSpillTest.cs | 86 +- .../Tests/Fluids/PuddleTest.cs | 126 +-- Content.Server/AME/AMENodeGroup.cs | 10 +- Content.Server/Animals/Systems/UdderSystem.cs | 1 + .../Atmos/Reactions/WaterVaporReaction.cs | 15 +- .../Body/Systems/BloodstreamSystem.cs | 24 +- .../FoamSolutionAreaEffectComponent.cs | 96 --- .../Chemistry/Components/SmokeComponent.cs | 26 + .../SmokeDissipateSpawnComponent.cs | 16 + .../SmokeSolutionAreaEffectComponent.cs | 72 -- .../Components/SolutionAreaEffectComponent.cs | 219 ----- .../SolutionAreaEffectInceptionComponent.cs | 136 --- .../DrainableSolutionComponent.cs | 17 - .../RefillableSolutionComponent.cs | 29 - .../EntitySystems/SolutionAreaEffectSystem.cs | 43 - .../EntitySystems/SolutionContainerSystem.cs | 43 + .../ReactionEffects/AreaReactionEffect.cs | 97 +-- .../ReactionEffects/FoamAreaReactionEffect.cs | 45 - .../SmokeAreaReactionEffect.cs | 15 - .../SpillIfPuddlePresentTileReaction.cs | 9 +- .../TileReactions/SpillTileReaction.cs | 14 +- Content.Server/Cloning/CloningSystem.cs | 4 +- .../Destructible/DestructibleSystem.cs | 2 +- .../Behaviors/SolutionExplosionBehavior.cs | 2 +- .../Thresholds/Behaviors/SpillBehavior.cs | 6 +- Content.Server/Entry/IgnoredComponents.cs | 1 - .../Fluids/Components/DrainComponent.cs | 39 - .../Fluids/Components/EvaporationComponent.cs | 60 +- .../Components/EvaporationSparkleComponent.cs | 10 + .../Components/FluidMapDataComponent.cs | 35 - .../Components/FootstepTrackComponent.cs | 7 + .../Fluids/Components/PuddleComponent.cs | 50 -- .../Fluids/EntitySystems/AbsorbentSystem.cs | 211 +++++ .../Fluids/EntitySystems/DrainSystem.cs | 1 + .../Fluids/EntitySystems/EvaporationSystem.cs | 60 -- .../EntitySystems/FluidSpreaderSystem.cs | 157 ---- .../Fluids/EntitySystems/MoppingSystem.cs | 287 ------- .../PuddleDebugDebugOverlaySystem.cs | 1 + .../EntitySystems/PuddleSystem.Evaporation.cs | 68 ++ .../EntitySystems/PuddleSystem.Spillable.cs | 139 ++++ .../EntitySystems/PuddleSystem.Transfers.cs | 69 ++ .../Fluids/EntitySystems/PuddleSystem.cs | 774 +++++++++++++----- .../Fluids/EntitySystems/SmokeSystem.cs | 326 ++++++++ .../Fluids/EntitySystems/SpillableSystem.cs | 366 --------- Content.Server/Kudzu/GrowingKudzuComponent.cs | 11 - Content.Server/Kudzu/GrowingKudzuSystem.cs | 54 -- Content.Server/Kudzu/SpreaderComponent.cs | 25 - Content.Server/Kudzu/SpreaderSystem.cs | 140 ---- .../BiomassReclaimerSystem.cs | 4 +- Content.Server/Medical/VomitSystem.cs | 2 + .../EntitySystems/NodeGroupSystem.cs | 2 +- .../NodeGroups/NodeGroupFactory.cs | 3 +- .../Nutrition/EntitySystems/CreamPieSystem.cs | 10 +- .../Nutrition/EntitySystems/DrinkSystem.cs | 11 +- .../Spreader/EdgeSpreaderComponent.cs | 10 + .../Spreader/EdgeSpreaderPrototype.cs | 12 + .../Spreader/GrowingKudzuComponent.cs | 22 + Content.Server/Spreader/KudzuComponent.cs | 14 + Content.Server/Spreader/KudzuSystem.cs | 114 +++ .../Spreader/SpreadGroupUpdateRate.cs | 7 + .../Spreader/SpreadNeighborsEvent.cs | 23 + .../Spreader/SpreaderGridComponent.cs | 10 + Content.Server/Spreader/SpreaderNode.cs | 33 + Content.Server/Spreader/SpreaderNodeGroup.cs | 31 + Content.Server/Spreader/SpreaderSystem.cs | 324 ++++++++ .../StationEvents/Events/VentClog.cs | 16 +- Content.Server/Tools/ToolSystem.TilePrying.cs | 1 + .../Weapons/Melee/MeleeWeaponSystem.cs | 15 +- .../ChemicalPuddleArtifactComponent.cs | 6 - .../Systems/ChemicalPuddleArtifactSystem.cs | 4 +- .../Effects/Systems/FoamArtifactSystem.cs | 13 +- .../Access/Components/IdCardComponent.cs | 2 + .../Components/DrainableSolutionComponent.cs | 18 + .../Components/RefillableSolutionComponent.cs | 27 + .../Chemistry/Components/Solution.cs | 22 +- .../Reaction/SharedChemicalReactionSystem.cs | 5 +- .../Chemistry/Reagent/ReagentPrototype.cs | 30 +- Content.Shared/Fluids/AbsorbentComponent.cs | 57 +- .../Fluids/Components/DrainComponent.cs | 40 + .../Fluids/Components/PuddleComponent.cs | 21 + Content.Shared/Fluids/PuddleVisuals.cs | 2 - ...pingSystem.cs => SharedAbsorbentSystem.cs} | 26 +- Content.Shared/Fluids/SharedPuddleSystem.cs | 44 + Content.Shared/Foam/FoamVisuals.cs | 11 - .../Movement/Events/GetFootstepSoundEvent.cs | 17 + .../Movement/Systems/SharedMoverController.cs | 13 +- .../Components/TimedDespawnComponent.cs | 4 +- .../EntitySystems/SharedTimedDespawnSystem.cs | 44 +- Content.Shared/Spawners/TimedDespawnEvent.cs | 7 + .../Weapons/Melee/SharedMeleeWeaponSystem.cs | 2 +- .../fluids/components/absorbent-component.ftl | 19 +- .../fluids/components/puddle-component.ftl | 5 +- Resources/Maps/Test/dev_map.yml | 24 - .../Entities/Effects/chemistry_effects.yml | 37 +- .../Prototypes/Entities/Effects/puddle.yml | 297 +++---- .../Entities/Objects/Consumable/Food/egg.yml | 19 - .../Objects/Consumable/Food/ingredients.yml | 22 - .../Objects/Consumable/Food/produce.yml | 36 - .../Entities/Objects/Misc/kudzu.yml | 29 +- .../Objects/Specific/Janitorial/janitor.yml | 38 +- .../Reagents/Consumable/Drink/alcohol.yml | 1 + .../Reagents/Consumable/Drink/base_drink.yml | 1 + .../Reagents/Consumable/Drink/drinks.yml | 1 + Resources/Prototypes/Reagents/biological.yml | 2 + Resources/Prototypes/Reagents/pyrotechnic.yml | 1 + .../Recipes/Reactions/chemicals.yml | 51 +- Resources/Prototypes/edge_spreaders.yml | 8 + Resources/Textures/Fluids/newliquid.png | Bin 0 -> 9394 bytes .../Textures/Fluids/puddle.rsi/meta.json | 65 ++ .../Textures/Fluids/puddle.rsi/splat0.png | Bin 0 -> 5173 bytes .../Textures/Fluids/puddle.rsi/splat1.png | Bin 0 -> 954 bytes .../Textures/Fluids/puddle.rsi/splat10.png | Bin 0 -> 861 bytes .../Textures/Fluids/puddle.rsi/splat11.png | Bin 0 -> 298 bytes .../Textures/Fluids/puddle.rsi/splat12.png | Bin 0 -> 670 bytes .../Textures/Fluids/puddle.rsi/splat13.png | Bin 0 -> 284 bytes .../Textures/Fluids/puddle.rsi/splat14.png | Bin 0 -> 269 bytes .../Textures/Fluids/puddle.rsi/splat15.png | Bin 0 -> 4905 bytes .../Textures/Fluids/puddle.rsi/splat2.png | Bin 0 -> 935 bytes .../Textures/Fluids/puddle.rsi/splat3.png | Bin 0 -> 670 bytes .../Textures/Fluids/puddle.rsi/splat4.png | Bin 0 -> 938 bytes .../Textures/Fluids/puddle.rsi/splat5.png | Bin 0 -> 904 bytes .../Textures/Fluids/puddle.rsi/splat6.png | Bin 0 -> 866 bytes .../Textures/Fluids/puddle.rsi/splat7.png | Bin 0 -> 344 bytes .../Textures/Fluids/puddle.rsi/splat8.png | Bin 0 -> 939 bytes .../Textures/Fluids/puddle.rsi/splat9.png | Bin 0 -> 860 bytes .../Textures/Fluids/puddle.rsi/splata.png | Bin 0 -> 5208 bytes .../Textures/Fluids/puddle.rsi/splatb.png | Bin 0 -> 5209 bytes 141 files changed, 3046 insertions(+), 3201 deletions(-) rename Content.Client/Fluids/{MoppingSystem.cs => AbsorbentSystem.cs} (84%) create mode 100644 Content.Client/Fluids/PuddleSystem.cs delete mode 100644 Content.Client/Fluids/PuddleVisualizerComponent.cs delete mode 100644 Content.Client/Fluids/PuddleVisualizerSystem.cs create mode 100644 Content.Client/UserInterface/Controls/SplitBar.xaml create mode 100644 Content.Client/UserInterface/Controls/SplitBar.xaml.cs delete mode 100644 Content.Server/Chemistry/Components/FoamSolutionAreaEffectComponent.cs create mode 100644 Content.Server/Chemistry/Components/SmokeComponent.cs create mode 100644 Content.Server/Chemistry/Components/SmokeDissipateSpawnComponent.cs delete mode 100644 Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs delete mode 100644 Content.Server/Chemistry/Components/SolutionAreaEffectComponent.cs delete mode 100644 Content.Server/Chemistry/Components/SolutionAreaEffectInceptionComponent.cs delete mode 100644 Content.Server/Chemistry/Components/SolutionManager/DrainableSolutionComponent.cs delete mode 100644 Content.Server/Chemistry/Components/SolutionManager/RefillableSolutionComponent.cs delete mode 100644 Content.Server/Chemistry/EntitySystems/SolutionAreaEffectSystem.cs delete mode 100644 Content.Server/Chemistry/ReactionEffects/FoamAreaReactionEffect.cs delete mode 100644 Content.Server/Chemistry/ReactionEffects/SmokeAreaReactionEffect.cs delete mode 100644 Content.Server/Fluids/Components/DrainComponent.cs create mode 100644 Content.Server/Fluids/Components/EvaporationSparkleComponent.cs delete mode 100644 Content.Server/Fluids/Components/FluidMapDataComponent.cs create mode 100644 Content.Server/Fluids/Components/FootstepTrackComponent.cs delete mode 100644 Content.Server/Fluids/Components/PuddleComponent.cs create mode 100644 Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs delete mode 100644 Content.Server/Fluids/EntitySystems/EvaporationSystem.cs delete mode 100644 Content.Server/Fluids/EntitySystems/FluidSpreaderSystem.cs delete mode 100644 Content.Server/Fluids/EntitySystems/MoppingSystem.cs create mode 100644 Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs create mode 100644 Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs create mode 100644 Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs create mode 100644 Content.Server/Fluids/EntitySystems/SmokeSystem.cs delete mode 100644 Content.Server/Fluids/EntitySystems/SpillableSystem.cs delete mode 100644 Content.Server/Kudzu/GrowingKudzuComponent.cs delete mode 100644 Content.Server/Kudzu/GrowingKudzuSystem.cs delete mode 100644 Content.Server/Kudzu/SpreaderComponent.cs delete mode 100644 Content.Server/Kudzu/SpreaderSystem.cs create mode 100644 Content.Server/Spreader/EdgeSpreaderComponent.cs create mode 100644 Content.Server/Spreader/EdgeSpreaderPrototype.cs create mode 100644 Content.Server/Spreader/GrowingKudzuComponent.cs create mode 100644 Content.Server/Spreader/KudzuComponent.cs create mode 100644 Content.Server/Spreader/KudzuSystem.cs create mode 100644 Content.Server/Spreader/SpreadGroupUpdateRate.cs create mode 100644 Content.Server/Spreader/SpreadNeighborsEvent.cs create mode 100644 Content.Server/Spreader/SpreaderGridComponent.cs create mode 100644 Content.Server/Spreader/SpreaderNode.cs create mode 100644 Content.Server/Spreader/SpreaderNodeGroup.cs create mode 100644 Content.Server/Spreader/SpreaderSystem.cs create mode 100644 Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs create mode 100644 Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs create mode 100644 Content.Shared/Fluids/Components/DrainComponent.cs create mode 100644 Content.Shared/Fluids/Components/PuddleComponent.cs rename Content.Shared/Fluids/{SharedMoppingSystem.cs => SharedAbsorbentSystem.cs} (56%) create mode 100644 Content.Shared/Fluids/SharedPuddleSystem.cs delete mode 100644 Content.Shared/Foam/FoamVisuals.cs create mode 100644 Content.Shared/Movement/Events/GetFootstepSoundEvent.cs create mode 100644 Content.Shared/Spawners/TimedDespawnEvent.cs create mode 100644 Resources/Prototypes/edge_spreaders.yml create mode 100644 Resources/Textures/Fluids/newliquid.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/meta.json create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat0.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat1.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat10.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat11.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat12.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat13.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat14.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat15.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat2.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat3.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat4.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat5.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat6.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat7.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat8.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splat9.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splata.png create mode 100644 Resources/Textures/Fluids/puddle.rsi/splatb.png 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 0000000000000000000000000000000000000000..4d6b400a2c1047c089895d17a124e5be35ab695c GIT binary patch literal 9394 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e6635f08682ecb7eb21f9c578dca74eb6b9b5ef6 GIT binary patch literal 5173 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Fluids/puddle.rsi/splat12.png b/Resources/Textures/Fluids/puddle.rsi/splat12.png new file mode 100644 index 0000000000000000000000000000000000000000..a8a2e7f90c967fc30e42177244b14797fff2ca6e GIT binary patch literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFt%hmI|q0=J1Z0v<)>xlq%tsQ zOst(~>v7mY=4gEI)FU9wk>Tv&jskxe3QG%vN+t^ZpDq%0q@v(XfBAR0^7jl2q&~S3B zDD(Oc$pW!2T&ExLv7Hy?bGW$X?&0ItcXOQoWcU5~i{K(P<{G7sGM9O~E1E0PE?=^I zFWepx7}RmT^EmG=g^$YDQ>Wjz5SJ0FNIT)7{=C!qNYcXIr)Fuc&Z_g&__SBK{d78( z+WJ{6eQQkW+3HW?Y{j>{x?Y>O3mg`D*r_F~S47zJR)5uxcG>!-7^9tD z2~Q#%ShZA+^Z#StzuWY4@Z`K81Fn*NH@+R$5V*lAWqSLc-L|*KI3Lurt=lqfe-L|` zz32L^TRsS0*v}giv)!bUf5#ohws|EdC&--i4?gj{@w@S_WjY20++Wu0+-v&1@E609 zm8+jhKX@P@p&jsT*YVxEZ&&~R?z4HnJm>$S+Oi23>w(e2mgMd3!tfsi7wla=87RV8 z;1OBOz@VoL!i*J5?aP3I>?NMQuI$g5#n||i&+1)v2MS4+xJHyX=jZ08=9Mrw7o{ea zq^2m8XO?6rxO@5rgg5eu0~M8dx;TbJ9DaMEi2;E=Ju|KURD!V9KA|A=1}o++BOpaW*s1 zOgOmBXtY~+${W^;S9O?`>gA#{z9|2Tds%j;IVNDYL&xv8HZMU+Jzf1=);T3K0RY)` B6%hac literal 0 HcmV?d00001 diff --git a/Resources/Textures/Fluids/puddle.rsi/splat13.png b/Resources/Textures/Fluids/puddle.rsi/splat13.png new file mode 100644 index 0000000000000000000000000000000000000000..a1cb82ad9037b2f53f4d540ac05d6fe326e100a5 GIT binary patch literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ$DS^ZArY-_C!gg!Y{1j{_)MZr z8^7e?<}@C88@CS!qgD1abgf#kHrn8&Bhy5MoW1_`Wv_R=yWDGbUo($^Edho;{AalL z`*J>WhoRLEkB*s>^iO|UF=Nk^O~NNM7_LWIyKc+f%~8nk=X%ucNvHZUzZbB5H2%1o zi<7DSzi!rU0e7R~21g2%rnXInOixFwbN2|ssLY;QSmqHufmHVI+zOFWNQmXuCW zIPoIpd(p?(i#}c1+&+eK|Fd?dir!hWL+12q+utuMKYDF>@?u(y)xSwWUWey>6=sP! dG|SVPA$Z1uxD#TIe}Ud*@O1TaS?83{1OOlLcbWhI literal 0 HcmV?d00001 diff --git a/Resources/Textures/Fluids/puddle.rsi/splat14.png b/Resources/Textures/Fluids/puddle.rsi/splat14.png new file mode 100644 index 0000000000000000000000000000000000000000..fa2c45ae753e37081341ec65a829eede063d0167 GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJE1oWnArY-_C!OYHR^)M+u92#6 z>IQRafJSP;0-hs@yA@`6#97veDW*=9u~Bca|D1DUTmAJy8P#JEd%i#I`?so`(>eLf zZyBc_9hT2`nZ`3V6rS7Zc_}J?$NICG+8vWQYL-m7RC(`vtm$QjMf;o|P1Q7i#ul^q zponDA$M&{`JIN|Ap2!Zh4die6$Ku=0W;~(F*=G7Yk*ZG$Oh3NvZJTo^>UPb! zi8po1m{W_oh=O!$?Ygm2qc=i6U?eD2w4hW!&5I2w@90dBonwT+d*&&7cLWAJqK Kb6Mw<&;$VGUTtdt literal 0 HcmV?d00001 diff --git a/Resources/Textures/Fluids/puddle.rsi/splat15.png b/Resources/Textures/Fluids/puddle.rsi/splat15.png new file mode 100644 index 0000000000000000000000000000000000000000..f1f26f871e0a78191c92dc10fdbf88d7c255e496 GIT binary patch literal 4905 zcmeHKX;>5I7LHKa(IS+hMVtl%0ka24Mj}GOs)2yGp+zQ>NivenWFP^>+~P(>TR;R9 zE4HQB)}@G7qn#b5Igm&s=hzrQ68MJ9mz@px)MaNsB$2F_WG62rk`O(l(W<4m0)vPw4F+Kb zTuLGtItEJ8KkS5@&NS$@c)e?yP=5VharSzz24jrx$8D-PU0)^rtq^n?wF{-nO<(+TJyAITM z)s5Ucn3w8!*t_MunBdmgra91qxBRB&?)5F?x?tzF?24R>*)`X2K2FGuM(g@3;_E)q z#yza>>AG}(?SjG+(3zT?j=@=-&dvDduPJNh_!qXIzWnlDXVEA7ecbqqmsCbv?O!Ykm?{2w+Kn$0tHN_?cl3usoznx~ z_~qOYSXk`3q`+sW=0e&IAEA?5;9(_QhcqSo&iZV}8#MRf{#?c{ ziBQL{Ti^OqyyxVmceZyPU^olpp5e}cjiuVB3l3T3B2ighg&P`6-ix$&JR zT|Y6uN?J18FFtoZtFnx}EY4rn-g04gHH=1_-VGB>bVUR@M-D5j!6{fp{VwYyfS zIxDX|kyqn2B`J4X3*)+mFQ9EI-fy7=tN5Mwyti_Q_89Mq)=38@-<-IL5M7U472mpj zTJ|DZfIQU?VOrBO_sm=2XXV3Akd_w?hn^KmmpA?9_x?3WS<5t`80wp|+JiuD8T{b0 zIeFy^ji7whA?WWWS$d3N_yGT0VTaasR-+PZJ_% zB#Jaih2KrzlJdie`qKdo$M36}bBN`awprCq(hhP2ANOWFT|?fzuJ!g4(SfH&$ft+f zJyurlOswcNnFhMA4qNZje*r(?PrcnO&R#v_&_3Wk=w8>Rdz4kyIYpqm(yOdmarxlc zupfqvK_AV1Yh>~knEw-zVe*RwmB%>_hbGz@-S>K*qXo1vDf&vo5?lWLAszFFZK?OX>9 zdADJuJ)BZccZ0OOx$4#jr%sE%%)d#MV*HtVd_SD^Rn+eh=dq0s+-0>XvVk@C-?nk= zh}(3B9bMQsr6?wR!&W%pi>8RJncLi4armP(kD@(|TUIc?2}rxKbe?m7&zZ?jY#Y|3 zyYKVus!a3j%c&mthyfjTJUY!ZlB2kK(6#ZY(+YgL@t}Rnx}xqrd7*cj=AGlm@5=9c zX}+;`3cFqOF1M(nN;KPQQ1(!mmDcCpw^H}`^4v4GtqVR+rSy<&H#ZtSIMT-J?rQ5g zW=!uGc|0M`s4Z>jPQk$}^uWQ)TPj=tqiQ7;k*LKO)u7aXnMfjqh8Z*nnvM~W7?a^D z9(nLY0~x|4Jo1ttAzi59V{$wuTZ=8sj!#Cj(^0O395&Y>)BpnjB}O2SL8(ybU;~eA z!G*!SSxh5C784?!M_wuvL436qgVVU^-NbYY1Geg3OqRSe-%e z$Yh|0p5>?12!+q#Rk|@203S31qMJWaT&b~m0kU7RByi~q zvR=x~tg)0cJ`v#l9QP&bXSG|5ft64Q3)E#v8QWhqaQrJu;MqvkY zq!bQ=#iek#C=+2x#9{_ZJPsm3i-WF26yvipLrDM>!r_YPY!-(?4`Q<^Y&ymPP$6sz zLyB?ObPh^q2B8)x2?|H4wMqmmC$2gpdLUum^CJ zM6K7oP$lC^Y$1V|`D6r#a6;(8Og1Mtgu!42zc5;iX?36%&8Q4ImC3Pa%nJj9asai6 zxl;jvMGmTg`C1Gi)Y@dVTEQcmlY-2imIjAHN0%iA*8z(xbI*@;{X%T@=+o#XP~a97 z1X&svM$pk9IwTX5SONjRQ57mjR5A?g@UfP9CddC`x#&SGsRR`VQ9=+*Okqo<9121Y zmQsR(rD8fAW1uWX(97sLwUp2!S}a@!Dh1Vm7P3?W%^D-q|7Cl<95XXTXR;`C7KOn| zW-?($2n<-GjTK8X@9AgNhSL6rk5G%jxFG=iMrB~QfLTR*KCH(0GE?|BzQ)$!Z;Sw- zUk7<5eqYn|nyy!3;FXMDch_sWUWtKMGJf4%|2MiEUL1EY75E3F2Zxyk$2E!IAY>z+ zA1xp~HNW?MyKg&?*lS{z=}06;SMxQ2v}?Bq5ZV&4!bsa&*3OPDp4^Ky{y?-RRuG=N z^wdDfN&7~~De_XUN6|>ahu5GYUdQ&CJBoItG~cuh&0CrCZ?z`yb5A#qITLMx7m2jN z+9#cUYwewUyKiszu&E@MZ=kZBWjj;StDdyEUdUg!%+vSJwG%(vSub5-bK#c-n9ZD-khYd Y`qD7}WWW56aC6Am$auk_h_u}Q0?gh}DF6Tf literal 0 HcmV?d00001 diff --git a/Resources/Textures/Fluids/puddle.rsi/splat2.png b/Resources/Textures/Fluids/puddle.rsi/splat2.png new file mode 100644 index 0000000000000000000000000000000000000000..94d18f32d369de12ff9df038ef84d9ee8df08d77 GIT binary patch literal 935 zcmV;Y16cftP)EX>4Tx04R}tkv&MmKpe$iQ?;TM2aAX}WT;LSL`58>ibb$c+6t{Ym|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfb#YR3krMxx6k5c1aNLh~_a1le0HI!Hs@X9CsG4OY zV^J}aT@?eb@M92N=tY;rOg)}jOv7`0-NVP%y9m$nKKJM7Q*tH)d?N82(+!JwgLr1s z(mC%FhgeZkh|h_~4Z0xlBiCh@-#8Z>7Ik9zor9e;vcGPz1% zthv24_i_3Fq^Yar8{ps& z7%Ncry2rb_+k5->OtZfq3IB3^c97A300006VoOIv0RI600RN!9r;`8x010qNS#tmY zE+YT{E+YYWr9XB6000McNliru=K~f73?I;U%|ZYG02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00DnVL_t(o!^Kw3t%e{F9LOue1ECBKmSLe=h6lSXVIl+APcMS>EL5fFbZ-DS}p768Cn3nGfW-q1BlMfcv#0pL7O z?E8KxU>E=qMQbh2^Bgi%DpN_aZU8e2%?{m~z|2cQ=n+~+M(P{yeVhi+Yr^!Tgb?uf z6wFqfA6X)b%zVp&UZHDRy-0Y+qHa;%YeHj8ah`Kd8DnlGQF-5&HtM|}>IM0~z3o`n zG~KpM^`3%MXXl*q-p88zivZF^j^j{76otZ35b3)-rS_~#Ve+I0F!R6j^`UB#9H$TxpWhV(Xl3t;vhIFgjP zCvq)Z0yG#F)-G^;shlCVu$39npbJqf-C?002ov JPDHLkV1g8}nBD*Y literal 0 HcmV?d00001 diff --git a/Resources/Textures/Fluids/puddle.rsi/splat3.png b/Resources/Textures/Fluids/puddle.rsi/splat3.png new file mode 100644 index 0000000000000000000000000000000000000000..dbffc99bb73553f45256ee32531a840ecf54d3cc GIT binary patch literal 670 zcmV;P0%84$P)EX>4Tx04R}tkv&MmKpe$iQ?;TM2aAX}WT;LSL`58>ibb$c+6t{Ym|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfb#YR3krMxx6k5c1aNLh~_a1le0HI!Hs@X9CsG4OY zV^J}aT@?eb@M92N=tY;rOg)}jOv7`0-NVP%y9m$nKKJM7Q*tH)d?N82(+!JwgLr1s z(mC%FhgeZkh|h_~4Z0xlBiCh@-#8Z>7Ik9zor9e;vcGPz1% zthv24_i_3Fq^Yar8{ps& z7%Ncry2rb_+k5->OtZfq3IB3^c97A300006VoOIv0RI600RN!9r;`8x010qNS#tmY zE+YT{E+YYWr9XB6000McNliru=K~f74j6nTI%5C;02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{004GLL_t(o!|j(r4gett16!Zihx*0clL;|;rJIEw^+17v zDCe9Hj2bYjwgE{b05~|1L;zqs%rLS7A6u|r{*HlDf<1<9z;57Mdmux5k|Q(whVW{@ z`6^1UfPD%Y!s{l?1Tk_@vjwr*bs=0;ux~=z4V3>KzbgALXBPl@>Hq)$07*qoM6N<$ Eg43iFA^-pY literal 0 HcmV?d00001 diff --git a/Resources/Textures/Fluids/puddle.rsi/splat4.png b/Resources/Textures/Fluids/puddle.rsi/splat4.png new file mode 100644 index 0000000000000000000000000000000000000000..e99174ba3663fddfdca4b007b72832886af60e50 GIT binary patch literal 938 zcmV;b16BNqP)EX>4Tx04R}tkv&MmKpe$iQ?;TM2aAX}WT;LSL`58>ibb$c+6t{Ym|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfb#YR3krMxx6k5c1aNLh~_a1le0HI!Hs@X9CsG4OY zV^J}aT@?eb@M92N=tY;rOg)}jOv7`0-NVP%y9m$nKKJM7Q*tH)d?N82(+!JwgLr1s z(mC%FhgeZkh|h_~4Z0xlBiCh@-#8Z>7Ik9zor9e;vcGPz1% zthv24_i_3Fq^Yar8{ps& z7%Ncry2rb_+k5->OtZfq3IB3^c97A300006VoOIv0RI600RN!9r;`8x010qNS#tmY zE+YT{E+YYWr9XB6000McNliru=K~f74GN^O80Y{102y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00DwYL_t(o!^Kz2t;8SV zo=On$o{E0sw#zg5mFpXbb=mi}&8-oC|MWgOD4xMokM-0{}3r zf!?M^Xkk#=9zy_JKq;jrC_Y^)2vf#1w7xTEWh1Ma>Cv(<8umzPIbh9uU5r=8OOq5o z+6fV{+yP)TJ17#8IX!vuQvjY0+Eu|fQ#}MA zrPk`*UkC7ubgKa5si$S2Za&i}m=55VnP>u#|9;JEJW(rF>wKEUYQ4MCE$x-g$M0Ge zyaqXHZdNWn%6-d}O5?&lVcRx{F{&U;Z7zDhVl8;@;*oPM(vlA;kg3%cu?wniJ?t4d z8_TkooO97AH9arbtMJ}cOS?BPh+18TE+$tU^O5goH$gzeGQAbQ0S68=wcK)^rT_o{ M07*qoM6N<$f;i%l1poj5 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Fluids/puddle.rsi/splat5.png b/Resources/Textures/Fluids/puddle.rsi/splat5.png new file mode 100644 index 0000000000000000000000000000000000000000..ca044c828558046c2fe867cf6b4eb3c4cc5a8841 GIT binary patch literal 904 zcmV;319$w1P)EX>4Tx04R}tkv&MmKpe$iQ?;TM2aAX}WT;LSL`58>ibb$c+6t{Ym|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfb#YR3krMxx6k5c1aNLh~_a1le0HI!Hs@X9CsG4OY zV^J}aT@?eb@M92N=tY;rOg)}jOv7`0-NVP%y9m$nKKJM7Q*tH)d?N82(+!JwgLr1s z(mC%FhgeZkh|h_~4Z0xlBiCh@-#8Z>7Ik9zor9e;vcGPz1% zthv24_i_3Fq^Yar8{ps& z7%Ncry2rb_+k5->OtZfq3IB3^c97A300006VoOIv0RI600RN!9r;`8x010qNS#tmY zE+YT{E+YYWr9XB6000McNliru=K~f77bM6^@B9D&02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00Cf0L_t(o!|hfvvVkIOn^!Snd($Tv(O` zdQL#L1XTN8%@>!Y+55D}!5 z_c0Kw;V=v=KD&|?R&yXxnx=`xJHl-Mj+D__3sOp0mgOD*xeclH4|z1+>>boCAmYHW zADzG6>46rZ%H5x0AZ|87OhAs7Rq{!L=FY1O!~t@dq&}n<#+WJ#hF&-iaWQ`JoX0000EX>4Tx04R}tkv&MmKpe$iQ?;TM2aAX}WT;LSL`58>ibb$c+6t{Ym|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfb#YR3krMxx6k5c1aNLh~_a1le0HI!Hs@X9CsG4OY zV^J}aT@?eb@M92N=tY;rOg)}jOv7`0-NVP%y9m$nKKJM7Q*tH)d?N82(+!JwgLr1s z(mC%FhgeZkh|h_~4Z0xlBiCh@-#8Z>7Ik9zor9e;vcGPz1% zthv24_i_3Fq^Yar8{ps& z7%Ncry2rb_+k5->OtZfq3IB3^c97A300006VoOIv0RI600RN!9r;`8x010qNS#tmY zE+YT{E+YYWr9XB6000McNliru=K~f779&qJOCA6K02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00BBlL_t(o!|j%_4a6V}ML#G*q_7x+B4aRU#z?^!h$35X zl_Lw001@0Z=Q07_x4|EPfBfUep=Ja2eNO;DL@>sT5#YT~#u%*Ys_vjAUw*?HZL5TN;L3zd! z%E7=GV>288mezsJIVGVzkRbx@DlyH7_*B>6-uuM=xIKG9*NlYC#CxCeeV6c|6=0`3 zj^nnE^Qx3O<&}UOE`cRLZ`*YpLf(c_B}R2a)d!y7)4Je1&&0Ob;_t(KAcSz8{+aMc zAILA5Sy4R$rJL<50<5)acJ|G;VQ&@Wcp?H3VcWKMFQV3$&Xb7zp(58>W|g)ixb#iG s&8lb4)4XMN7u2>+zbBj z$$j>hTxwvNrVUTc;?OHM*ffo5@k;;;T>`)ZP}g-T07(zf*9U0BJkPrqGz@vH0E)r& za0OJ}?D{!8`_Fr6_>a>`wrt_xalZ^4z!9X(zPGKPzcMt9Ez*nir2-08)GjeGI@# q(Iqc^=Kl;}332&jVs;;R3gQE6XQ7h~q#OhQ0000yKCf literal 0 HcmV?d00001 diff --git a/Resources/Textures/Fluids/puddle.rsi/splat8.png b/Resources/Textures/Fluids/puddle.rsi/splat8.png new file mode 100644 index 0000000000000000000000000000000000000000..a2492877d6a928f3cd36f0f445ccaade32fcac3d GIT binary patch literal 939 zcmV;c162HpP)EX>4Tx04R}tkv&MmKpe$iQ?;TM2aAX}WT;LSL`58>ibb$c+6t{Ym|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfb#YR3krMxx6k5c1aNLh~_a1le0HI!Hs@X9CsG4OY zV^J}aT@?eb@M92N=tY;rOg)}jOv7`0-NVP%y9m$nKKJM7Q*tH)d?N82(+!JwgLr1s z(mC%FhgeZkh|h_~4Z0xlBiCh@-#8Z>7Ik9zor9e;vcGPz1% zthv24_i_3Fq^Yar8{ps& z7%Ncry2rb_+k5->OtZfq3IB3^c97A300006VoOIv0RI600RN!9r;`8x010qNS#tmY zE+YT{E+YYWr9XB6000McNliru=K~f74J3GH6(;}y02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00DzZL_t(o!`)XwuEZb+#h)I7cnlXF!-IMZ7aqf~=q>y! zCe)&~Kedy5l1XMP0)0FffShxMFUm$lkaMm?1R(@7yAWfnhQvmFseJgopt%a0Q*BI-{Aac%i6x1OA05~APsnHx+YY%VEdM%Q( z^!~I5l9bLl)1tz^Q%XQYpAVn6sA+Z&B-F^n%51!Su8Bl|c@sh~J^d~MJ;!%MBFE4Z z5$yY3F9qp;wAaQXm40&Ps6 zZA+&VNZ8uTJn4>BwYw|YX*24LPVInkcLf_(0W*4Jl%GB6TpgX?mS30{8xPK0a?t<) N002ovPDHLkV1j@5ke2`e literal 0 HcmV?d00001 diff --git a/Resources/Textures/Fluids/puddle.rsi/splat9.png b/Resources/Textures/Fluids/puddle.rsi/splat9.png new file mode 100644 index 0000000000000000000000000000000000000000..05122c675237c9b3de93f1839be00b7638329a67 GIT binary patch literal 860 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Fluids/puddle.rsi/splatb.png b/Resources/Textures/Fluids/puddle.rsi/splatb.png new file mode 100644 index 0000000000000000000000000000000000000000..87e9da02637e08dcc10549939fc6786498f6288b GIT binary patch literal 5209 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 literal 0 HcmV?d00001