Puddles & spreader refactor (#15191)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<DefaultWindow xmlns="https://spacestation14.io"
|
<DefaultWindow xmlns="https://spacestation14.io"
|
||||||
MinSize="270 420"
|
MinSize="270 420"
|
||||||
SetSize="315 420" Title="{Loc 'gas-analyzer-window-name'}">
|
SetSize="360 420" Title="{Loc 'gas-analyzer-window-name'}">
|
||||||
<BoxContainer Orientation="Vertical" Margin="5 5 5 5">
|
<BoxContainer Orientation="Vertical" Margin="5 5 5 5">
|
||||||
<BoxContainer Name="CTopBox" Orientation="Horizontal"/>
|
<BoxContainer Name="CTopBox" Orientation="Horizontal"/>
|
||||||
<!-- Gas Mix Data, Populated by function -->
|
<!-- Gas Mix Data, Populated by function -->
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Client.UserInterface.Controls;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Temperature;
|
using Content.Shared.Temperature;
|
||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
@@ -261,11 +262,9 @@ namespace Content.Client.Atmos.UI
|
|||||||
// This is the gas bar thingy
|
// This is the gas bar thingy
|
||||||
var height = 30;
|
var height = 30;
|
||||||
var minSize = 24; // This basically allows gases which are too small, to be shown properly
|
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,
|
MinHeight = height
|
||||||
HorizontalExpand = true,
|
|
||||||
MinSize = new Vector2(0, height)
|
|
||||||
};
|
};
|
||||||
// Separator
|
// Separator
|
||||||
dataContainer.AddChild(new Control
|
dataContainer.AddChild(new Control
|
||||||
@@ -299,25 +298,10 @@ namespace Content.Client.Atmos.UI
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add to the gas bar //TODO: highlight the currently hover one
|
// Add to the gas bar //TODO: highlight the currently hover one
|
||||||
var left = (j == 0) ? 0f : 2f;
|
gasBar.AddEntry(gas.Amount, color, tooltip: Loc.GetString("gas-analyzer-window-molarity-percentage-text",
|
||||||
var right = (j == gasMix.Gases.Length - 1) ? 0f : 2f;
|
("gasName", gas.Name),
|
||||||
gasBar.AddChild(new PanelContainer
|
("amount", $"{gas.Amount:0.##}"),
|
||||||
{
|
("percentage", $"{(gas.Amount / totalGasAmount * 100):0.#}")));
|
||||||
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)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dataContainer.AddChild(gasBar);
|
dataContainer.AddChild(gasBar);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using Content.Shared.Foam;
|
using Content.Shared.Smoking;
|
||||||
|
using Content.Shared.Spawners.Components;
|
||||||
using Robust.Client.Animations;
|
using Robust.Client.Animations;
|
||||||
using Robust.Client.GameObjects;
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Client.Chemistry.Visualizers;
|
namespace Content.Client.Chemistry.Visualizers;
|
||||||
|
|
||||||
@@ -9,23 +12,48 @@ namespace Content.Client.Chemistry.Visualizers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent>
|
public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent>
|
||||||
{
|
{
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
SubscribeLocalEvent<FoamVisualsComponent, ComponentInit>(OnComponentInit);
|
SubscribeLocalEvent<FoamVisualsComponent, ComponentInit>(OnComponentInit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
base.Update(frameTime);
|
||||||
|
|
||||||
|
if (!_timing.IsFirstTimePredicted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var query = EntityQueryEnumerator<FoamVisualsComponent, TimedDespawnComponent>();
|
||||||
|
|
||||||
|
while (query.MoveNext(out var uid, out var comp, out var despawn))
|
||||||
|
{
|
||||||
|
if (despawn.Lifetime > 1f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Despawn animation.
|
||||||
|
if (TryComp(uid, out AnimationPlayerComponent? animPlayer)
|
||||||
|
&& !AnimationSystem.HasRunningAnimation(uid, animPlayer, FoamVisualsComponent.AnimationKey))
|
||||||
|
{
|
||||||
|
AnimationSystem.Play(uid, animPlayer, comp.Animation, FoamVisualsComponent.AnimationKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates the animation used by foam visuals when the foam dissolves.
|
/// Generates the animation used by foam visuals when the foam dissolves.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnComponentInit(EntityUid uid, FoamVisualsComponent comp, ComponentInit args)
|
private void OnComponentInit(EntityUid uid, FoamVisualsComponent comp, ComponentInit args)
|
||||||
{
|
{
|
||||||
comp.Animation = new Animation()
|
comp.Animation = new Animation
|
||||||
{
|
{
|
||||||
Length = TimeSpan.FromSeconds(comp.AnimationTime),
|
Length = TimeSpan.FromSeconds(comp.AnimationTime),
|
||||||
AnimationTracks =
|
AnimationTracks =
|
||||||
{
|
{
|
||||||
new AnimationTrackSpriteFlick()
|
new AnimationTrackSpriteFlick
|
||||||
{
|
{
|
||||||
LayerKey = FoamVisualLayers.Base,
|
LayerKey = FoamVisualLayers.Base,
|
||||||
KeyFrames =
|
KeyFrames =
|
||||||
@@ -36,25 +64,6 @@ public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Plays the animation used by foam visuals when the foam dissolves.
|
|
||||||
/// </summary>
|
|
||||||
protected override void OnAppearanceChange(EntityUid uid, FoamVisualsComponent comp, ref AppearanceChangeEvent args)
|
|
||||||
{
|
|
||||||
if (AppearanceSystem.TryGetData<bool>(uid, FoamVisuals.State, out var state, args.Component) && state)
|
|
||||||
{
|
|
||||||
if (TryComp(uid, out AnimationPlayerComponent? animPlayer)
|
|
||||||
&& !AnimationSystem.HasRunningAnimation(uid, animPlayer, FoamVisualsComponent.AnimationKey))
|
|
||||||
AnimationSystem.Play(uid, animPlayer, comp.Animation, FoamVisualsComponent.AnimationKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AppearanceSystem.TryGetData<Color>(uid, FoamVisuals.Color, out var color, args.Component))
|
|
||||||
{
|
|
||||||
if (args.Sprite != null)
|
|
||||||
args.Sprite.Color = color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FoamVisualLayers : byte
|
public enum FoamVisualLayers : byte
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ using Robust.Client.UserInterface;
|
|||||||
|
|
||||||
namespace Content.Client.Fluids;
|
namespace Content.Client.Fluids;
|
||||||
|
|
||||||
public sealed class MoppingSystem : SharedMoppingSystem
|
/// <inheritdoc/>
|
||||||
|
public sealed class AbsorbentSystem : SharedAbsorbentSystem
|
||||||
{
|
{
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
67
Content.Client/Fluids/PuddleSystem.cs
Normal file
67
Content.Client/Fluids/PuddleSystem.cs
Normal file
@@ -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<PuddleComponent, AppearanceChangeEvent>(OnPuddleAppearance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPuddleAppearance(EntityUid uid, PuddleComponent component, ref AppearanceChangeEvent args)
|
||||||
|
{
|
||||||
|
if (args.Sprite == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float volume = 1f;
|
||||||
|
|
||||||
|
if (args.AppearanceData.TryGetValue(PuddleVisuals.CurrentVolume, out var volumeObj))
|
||||||
|
{
|
||||||
|
volume = (float) volumeObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update smoothing and sprite based on volume.
|
||||||
|
if (TryComp<IconSmoothComponent>(uid, out var smooth))
|
||||||
|
{
|
||||||
|
if (volume < LowThreshold)
|
||||||
|
{
|
||||||
|
args.Sprite.LayerSetState(0, $"{smooth.StateBase}a");
|
||||||
|
_smooth.SetEnabled(uid, false, smooth);
|
||||||
|
}
|
||||||
|
else if (volume < 0.6f)
|
||||||
|
{
|
||||||
|
args.Sprite.LayerSetState(0, $"{smooth.StateBase}b");
|
||||||
|
_smooth.SetEnabled(uid, false, smooth);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!smooth.Enabled)
|
||||||
|
{
|
||||||
|
args.Sprite.LayerSetState(0, $"{smooth.StateBase}0");
|
||||||
|
_smooth.SetEnabled(uid, true, smooth);
|
||||||
|
_smooth.DirtyNeighbours(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseColor = Color.White;
|
||||||
|
|
||||||
|
if (args.AppearanceData.TryGetValue(PuddleVisuals.SolutionColor, out var colorObj))
|
||||||
|
{
|
||||||
|
var color = (Color) colorObj;
|
||||||
|
args.Sprite.Color = color * baseColor;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
args.Sprite.Color *= baseColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Puddles with volume below this threshold are able to have their sprite changed to a wet floor effect, though this is not the only factor.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("wetFloorEffectThreshold")]
|
|
||||||
public FixedPoint2 WetFloorEffectThreshold = FixedPoint2.New(5);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Alpha (opacity) of the wet floor sparkle effect. Higher alpha = more opaque/visible.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("wetFloorEffectAlpha")]
|
|
||||||
public float WetFloorEffectAlpha = 0.75f; //should be somewhat transparent by default.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<PuddleVisualizerComponent>
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
SubscribeLocalEvent<PuddleVisualizerComponent, ComponentInit>(OnComponentInit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnComponentInit(EntityUid uid, PuddleVisualizerComponent puddleVisuals, ComponentInit args)
|
|
||||||
{
|
|
||||||
if (!TryComp(uid, out SpriteComponent? sprite))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
puddleVisuals.OriginalRsi = sprite.BaseRSI; //Back up the original RSI upon initialization
|
|
||||||
RandomizeState(sprite, puddleVisuals.OriginalRsi);
|
|
||||||
RandomizeRotation(sprite);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnAppearanceChange(EntityUid uid, PuddleVisualizerComponent component, ref AppearanceChangeEvent args)
|
|
||||||
{
|
|
||||||
if (args.Sprite == null)
|
|
||||||
{
|
|
||||||
Logger.Warning($"Missing SpriteComponent for PuddleVisualizerSystem on entityUid = {uid}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!AppearanceSystem.TryGetData<float>(uid, PuddleVisuals.VolumeScale, out var volumeScale)
|
|
||||||
|| !AppearanceSystem.TryGetData<FixedPoint2>(uid, PuddleVisuals.CurrentVolume, out var currentVolume)
|
|
||||||
|| !AppearanceSystem.TryGetData<Color>(uid, PuddleVisuals.SolutionColor, out var solutionColor)
|
|
||||||
|| !AppearanceSystem.TryGetData<bool>(uid, PuddleVisuals.IsEvaporatingVisual, out var isEvaporating))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// volumeScale is our opacity based on level of fullness to overflow. The lower bound is hard-capped for visibility reasons.
|
|
||||||
var cappedScale = Math.Min(1.0f, volumeScale * 0.75f + 0.25f);
|
|
||||||
|
|
||||||
var newColor = component.Recolor ? solutionColor.WithAlpha(cappedScale) : args.Sprite.Color.WithAlpha(cappedScale);
|
|
||||||
|
|
||||||
args.Sprite.LayerSetColor(0, newColor);
|
|
||||||
|
|
||||||
// Don't consider wet floor effects if we're using a custom sprite.
|
|
||||||
if (component.CustomPuddleSprite)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (isEvaporating && currentVolume <= component.WetFloorEffectThreshold)
|
|
||||||
{
|
|
||||||
// If we need the effect but don't already have it - start it
|
|
||||||
if (args.Sprite.LayerGetState(0) != "sparkles")
|
|
||||||
StartWetFloorEffect(args.Sprite, component.WetFloorEffectAlpha);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// If we have the effect but don't need it - end it
|
|
||||||
if (args.Sprite.LayerGetState(0) == "sparkles")
|
|
||||||
EndWetFloorEffect(args.Sprite, component.OriginalRsi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartWetFloorEffect(SpriteComponent sprite, float alpha)
|
|
||||||
{
|
|
||||||
sprite.LayerSetState(0, "sparkles", "Fluids/wet_floor_sparkles.rsi");
|
|
||||||
sprite.Color = sprite.Color.WithAlpha(alpha);
|
|
||||||
sprite.LayerSetAutoAnimated(0, false);
|
|
||||||
sprite.LayerSetAutoAnimated(0, true); //fixes a bug where the sparkle effect would sometimes freeze on a single frame.
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EndWetFloorEffect(SpriteComponent sprite, RSI? originalRSI)
|
|
||||||
{
|
|
||||||
RandomizeState(sprite, originalRSI);
|
|
||||||
sprite.LayerSetAutoAnimated(0, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RandomizeState(SpriteComponent sprite, RSI? rsi)
|
|
||||||
{
|
|
||||||
var maxStates = rsi?.ToArray();
|
|
||||||
if (maxStates is not { Length: > 0 }) return;
|
|
||||||
|
|
||||||
var selectedState = _random.Next(0, maxStates.Length - 1); //randomly select an index for which RSI state to use.
|
|
||||||
sprite.LayerSetState(0, maxStates[selectedState].StateId, rsi); // sets the sprite's state via our randomly selected index.
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RandomizeRotation(SpriteComponent sprite)
|
|
||||||
{
|
|
||||||
float rotationDegrees = _random.Next(0, 359); // randomly select a rotation for our puddle sprite.
|
|
||||||
sprite.Rotation = Angle.FromDegrees(rotationDegrees); // sets the sprite's rotation to the one we randomly selected.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,4 @@
|
|||||||
<Control xmlns="https://spacestation14.io">
|
<controls:SplitBar xmlns="https://spacestation14.io"
|
||||||
<BoxContainer Orientation="Horizontal">
|
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||||
<ProgressBar
|
Name="Bar">
|
||||||
HorizontalExpand="True"
|
</controls:SplitBar>
|
||||||
Name="PercentBar"
|
|
||||||
MinSize="20 20"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="2 8 4 2"
|
|
||||||
MaxValue="1.0"
|
|
||||||
MinValue="0.0">
|
|
||||||
</ProgressBar>
|
|
||||||
</BoxContainer>
|
|
||||||
</Control>
|
|
||||||
|
|||||||
@@ -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.AutoGenerated;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Client.Fluids.UI
|
namespace Content.Client.Fluids.UI
|
||||||
{
|
{
|
||||||
[GenerateTypedNameReferences]
|
[GenerateTypedNameReferences]
|
||||||
public sealed partial class AbsorbentItemStatus : Control
|
public sealed partial class AbsorbentItemStatus : SplitBar
|
||||||
{
|
{
|
||||||
private readonly IEntityManager _entManager;
|
private readonly IEntityManager _entManager;
|
||||||
private readonly EntityUid _uid;
|
private readonly EntityUid _uid;
|
||||||
|
private Dictionary<Color, float> _progress = new();
|
||||||
|
|
||||||
public AbsorbentItemStatus(EntityUid uid, IEntityManager entManager)
|
public AbsorbentItemStatus(EntityUid uid, IEntityManager entManager)
|
||||||
{
|
{
|
||||||
@@ -25,7 +29,23 @@ namespace Content.Client.Fluids.UI
|
|||||||
if (!_entManager.TryGetComponent<AbsorbentComponent>(_uid, out var absorbent))
|
if (!_entManager.TryGetComponent<AbsorbentComponent>(_uid, out var absorbent))
|
||||||
return;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ namespace Content.Client.IconSmoothing
|
|||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
public sealed class IconSmoothComponent : Component
|
public sealed class IconSmoothComponent : Component
|
||||||
{
|
{
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("enabled")]
|
||||||
|
public bool Enabled = true;
|
||||||
|
|
||||||
public (EntityUid?, Vector2i)? LastPosition;
|
public (EntityUid?, Vector2i)? LastPosition;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using JetBrains.Annotations;
|
|||||||
using Robust.Client.GameObjects;
|
using Robust.Client.GameObjects;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Map.Components;
|
using Robust.Shared.Map.Components;
|
||||||
|
using Robust.Shared.Map.Enumerators;
|
||||||
using static Robust.Client.GameObjects.SpriteComponent;
|
using static Robust.Client.GameObjects.SpriteComponent;
|
||||||
|
|
||||||
namespace Content.Client.IconSmoothing
|
namespace Content.Client.IconSmoothing
|
||||||
@@ -21,6 +22,15 @@ namespace Content.Client.IconSmoothing
|
|||||||
|
|
||||||
private int _generation;
|
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()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
@@ -67,6 +77,7 @@ namespace Content.Client.IconSmoothing
|
|||||||
|
|
||||||
private void OnShutdown(EntityUid uid, IconSmoothComponent component, ComponentShutdown args)
|
private void OnShutdown(EntityUid uid, IconSmoothComponent component, ComponentShutdown args)
|
||||||
{
|
{
|
||||||
|
_dirtyEntities.Enqueue(uid);
|
||||||
DirtyNeighbours(uid, component);
|
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.
|
// 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.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 0)));
|
||||||
DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(-1, 0)));
|
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 0)));
|
||||||
DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(0, 1)));
|
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, 1)));
|
||||||
DirtyEntities(grid.GetAnchoredEntities(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)
|
if (comp.Mode is IconSmoothingMode.Corners or IconSmoothingMode.NoSprite or IconSmoothingMode.Diagonal)
|
||||||
{
|
{
|
||||||
DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(1, 1)));
|
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 1)));
|
||||||
DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(-1, -1)));
|
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, -1)));
|
||||||
DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(-1, 1)));
|
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 1)));
|
||||||
DirtyEntities(grid.GetAnchoredEntities(pos + new Vector2i(1, -1)));
|
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, -1)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DirtyEntities(IEnumerable<EntityUid> entities)
|
private void DirtyEntities(AnchoredEntitiesEnumerator entities)
|
||||||
{
|
{
|
||||||
// Instead of doing HasComp -> Enqueue -> TryGetComp, we will just enqueue all entities. Generally when
|
// 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
|
// dealing with walls neighboring anchored entities will also be walls, and in those instances that will
|
||||||
// require one less component fetch/check.
|
// 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.
|
// 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)
|
if (!smoothQuery.Resolve(uid, ref smooth, false)
|
||||||
|| smooth.Mode == IconSmoothingMode.NoSprite
|
|| smooth.Mode == IconSmoothingMode.NoSprite
|
||||||
|| smooth.UpdateGeneration == _generation)
|
|| smooth.UpdateGeneration == _generation ||
|
||||||
|
!smooth.Enabled)
|
||||||
{
|
{
|
||||||
if (smooth != null &&
|
if (smooth is { Enabled: true } &&
|
||||||
TryComp<SmoothEdgeComponent>(uid, out var edge) &&
|
TryComp<SmoothEdgeComponent>(uid, out var edge) &&
|
||||||
xformQuery.TryGetComponent(uid, out xform))
|
xformQuery.TryGetComponent(uid, out xform))
|
||||||
{
|
{
|
||||||
@@ -196,13 +208,13 @@ namespace Content.Client.IconSmoothing
|
|||||||
{
|
{
|
||||||
var pos = grid.TileIndicesFor(xform.Coordinates);
|
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;
|
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;
|
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;
|
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;
|
directions |= DirectionFlag.West;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +230,7 @@ namespace Content.Client.IconSmoothing
|
|||||||
if (!spriteQuery.TryGetComponent(uid, out var sprite))
|
if (!spriteQuery.TryGetComponent(uid, out var sprite))
|
||||||
{
|
{
|
||||||
Logger.Error($"Encountered a icon-smoothing entity without a sprite: {ToPrettyString(uid)}");
|
Logger.Error($"Encountered a icon-smoothing entity without a sprite: {ToPrettyString(uid)}");
|
||||||
RemComp(uid, smooth);
|
RemCompDeferred(uid, smooth);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +282,7 @@ namespace Content.Client.IconSmoothing
|
|||||||
for (var i = 0; i < neighbors.Length; i++)
|
for (var i = 0; i < neighbors.Length; i++)
|
||||||
{
|
{
|
||||||
var neighbor = (Vector2i) rotation.RotateVec(neighbors[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)
|
if (matching)
|
||||||
@@ -294,13 +306,13 @@ namespace Content.Client.IconSmoothing
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pos = grid.TileIndicesFor(xform.Coordinates);
|
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;
|
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;
|
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;
|
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;
|
dirs |= CardinalConnectDirs.West;
|
||||||
|
|
||||||
sprite.LayerSetState(0, $"{smooth.StateBase}{(int) dirs}");
|
sprite.LayerSetState(0, $"{smooth.StateBase}{(int) dirs}");
|
||||||
@@ -319,12 +331,16 @@ namespace Content.Client.IconSmoothing
|
|||||||
CalculateEdge(sprite.Owner, directions, sprite);
|
CalculateEdge(sprite.Owner, directions, sprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool MatchingEntity(IconSmoothComponent smooth, IEnumerable<EntityUid> candidates, EntityQuery<IconSmoothComponent> smoothQuery)
|
private bool MatchingEntity(IconSmoothComponent smooth, AnchoredEntitiesEnumerator candidates, EntityQuery<IconSmoothComponent> smoothQuery)
|
||||||
{
|
{
|
||||||
foreach (var entity in candidates)
|
while (candidates.MoveNext(out var entity))
|
||||||
{
|
{
|
||||||
if (smoothQuery.TryGetComponent(entity, out var other) && other.SmoothKey == smooth.SmoothKey)
|
if (smoothQuery.TryGetComponent(entity, out var other) &&
|
||||||
|
other.SmoothKey == smooth.SmoothKey &&
|
||||||
|
other.Enabled)
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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<IconSmoothComponent> smoothQuery)
|
private (CornerFill ne, CornerFill nw, CornerFill sw, CornerFill se) CalculateCornerFill(MapGridComponent grid, IconSmoothComponent smooth, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
|
||||||
{
|
{
|
||||||
var pos = grid.TileIndicesFor(xform.Coordinates);
|
var pos = grid.TileIndicesFor(xform.Coordinates);
|
||||||
var n = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.North)), smoothQuery);
|
var n = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.North)), smoothQuery);
|
||||||
var ne = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.NorthEast)), smoothQuery);
|
var ne = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.NorthEast)), smoothQuery);
|
||||||
var e = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.East)), smoothQuery);
|
var e = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.East)), smoothQuery);
|
||||||
var se = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.SouthEast)), smoothQuery);
|
var se = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.SouthEast)), smoothQuery);
|
||||||
var s = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.South)), smoothQuery);
|
var s = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.South)), smoothQuery);
|
||||||
var sw = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.SouthWest)), smoothQuery);
|
var sw = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.SouthWest)), smoothQuery);
|
||||||
var w = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.West)), smoothQuery);
|
var w = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.West)), smoothQuery);
|
||||||
var nw = MatchingEntity(smooth, grid.GetAnchoredEntities(pos.Offset(Direction.NorthWest)), smoothQuery);
|
var nw = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.NorthWest)), smoothQuery);
|
||||||
|
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
var cornerNE = CornerFill.None;
|
var cornerNE = CornerFill.None;
|
||||||
|
|||||||
7
Content.Client/UserInterface/Controls/SplitBar.xaml
Normal file
7
Content.Client/UserInterface/Controls/SplitBar.xaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<controls:SplitBar xmlns="https://spacestation14.io"
|
||||||
|
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||||
|
MouseFilter="Stop"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalExpand="True"
|
||||||
|
MinSize="0 30">
|
||||||
|
</controls:SplitBar>
|
||||||
39
Content.Client/UserInterface/Controls/SplitBar.xaml.cs
Normal file
39
Content.Client/UserInterface/Controls/SplitBar.xaml.cs
Normal file
@@ -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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,7 +113,7 @@ public sealed partial class MeleeWeaponSystem
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Does all of the melee effects for a player that are predicted, i.e. character lunge and weapon animation.
|
/// Does all of the melee effects for a player that are predicted, i.e. character lunge and weapon animation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override void DoLunge(EntityUid user, Angle angle, Vector2 localPos, string? animation)
|
public override void DoLunge(EntityUid user, Angle angle, Vector2 localPos, string? animation, bool predicted = true)
|
||||||
{
|
{
|
||||||
if (!Timing.IsFirstTimePredicted)
|
if (!Timing.IsFirstTimePredicted)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ using System.Collections.Generic;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Fluids.Components;
|
using Content.Server.Fluids.Components;
|
||||||
using Content.Server.Fluids.EntitySystems;
|
using Content.Server.Fluids.EntitySystems;
|
||||||
|
using Content.Server.Spreader;
|
||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared.Chemistry.Components;
|
||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
|
using Content.Shared.Fluids.Components;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
@@ -16,7 +18,7 @@ using Robust.Shared.Timing;
|
|||||||
namespace Content.IntegrationTests.Tests.Fluids;
|
namespace Content.IntegrationTests.Tests.Fluids;
|
||||||
|
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
[TestOf(typeof(FluidSpreaderSystem))]
|
[TestOf(typeof(SpreaderSystem))]
|
||||||
public sealed class FluidSpill
|
public sealed class FluidSpill
|
||||||
{
|
{
|
||||||
private static PuddleComponent? GetPuddle(IEntityManager entityManager, MapGridComponent mapGrid, Vector2i pos)
|
private static PuddleComponent? GetPuddle(IEntityManager entityManager, MapGridComponent mapGrid, Vector2i pos)
|
||||||
@@ -30,78 +32,6 @@ public sealed class FluidSpill
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Direction[] _dirs =
|
|
||||||
{
|
|
||||||
Direction.East,
|
|
||||||
Direction.South,
|
|
||||||
Direction.West,
|
|
||||||
Direction.North,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
private readonly Vector2i _origin = new(1, 1);
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task SpillEvenlyTest()
|
|
||||||
{
|
|
||||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
|
|
||||||
var server = pairTracker.Pair.Server;
|
|
||||||
var mapManager = server.ResolveDependency<IMapManager>();
|
|
||||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
|
||||||
var spillSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SpillableSystem>();
|
|
||||||
var gameTiming = server.ResolveDependency<IGameTiming>();
|
|
||||||
var puddleSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<PuddleSystem>();
|
|
||||||
MapId mapId;
|
|
||||||
EntityUid gridId = default;
|
|
||||||
|
|
||||||
await server.WaitPost(() =>
|
|
||||||
{
|
|
||||||
mapId = mapManager.CreateMap();
|
|
||||||
var grid = mapManager.CreateGrid(mapId);
|
|
||||||
gridId = grid.Owner;
|
|
||||||
|
|
||||||
for (var x = 0; x < 3; x++)
|
|
||||||
{
|
|
||||||
for (var y = 0; y < 3; y++)
|
|
||||||
{
|
|
||||||
grid.SetTile(new Vector2i(x, y), new Tile(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await server.WaitAssertion(() =>
|
|
||||||
{
|
|
||||||
var grid = mapManager.GetGrid(gridId);
|
|
||||||
var solution = new Solution("Water", FixedPoint2.New(100));
|
|
||||||
var tileRef = grid.GetTileRef(_origin);
|
|
||||||
var puddle = spillSystem.SpillAt(tileRef, solution, "PuddleSmear");
|
|
||||||
Assert.That(puddle, Is.Not.Null);
|
|
||||||
Assert.That(GetPuddle(entityManager, grid, _origin), Is.Not.Null);
|
|
||||||
});
|
|
||||||
|
|
||||||
var sTimeToWait = (int) Math.Ceiling(2f * gameTiming.TickRate);
|
|
||||||
await server.WaitRunTicks(sTimeToWait);
|
|
||||||
|
|
||||||
await server.WaitAssertion(() =>
|
|
||||||
{
|
|
||||||
var grid = mapManager.GetGrid(gridId);
|
|
||||||
var puddle = GetPuddle(entityManager, grid, _origin);
|
|
||||||
|
|
||||||
Assert.That(puddle, Is.Not.Null);
|
|
||||||
Assert.That(puddleSystem.CurrentVolume(puddle!.Owner, puddle), Is.EqualTo(FixedPoint2.New(20)));
|
|
||||||
|
|
||||||
foreach (var direction in _dirs)
|
|
||||||
{
|
|
||||||
var newPos = _origin.Offset(direction);
|
|
||||||
var sidePuddle = GetPuddle(entityManager, grid, newPos);
|
|
||||||
Assert.That(sidePuddle, Is.Not.Null);
|
|
||||||
Assert.That(puddleSystem.CurrentVolume(sidePuddle!.Owner, sidePuddle), Is.EqualTo(FixedPoint2.New(20)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await pairTracker.CleanReturnAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task SpillCorner()
|
public async Task SpillCorner()
|
||||||
{
|
{
|
||||||
@@ -109,7 +39,6 @@ public sealed class FluidSpill
|
|||||||
var server = pairTracker.Pair.Server;
|
var server = pairTracker.Pair.Server;
|
||||||
var mapManager = server.ResolveDependency<IMapManager>();
|
var mapManager = server.ResolveDependency<IMapManager>();
|
||||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||||
var spillSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SpillableSystem>();
|
|
||||||
var puddleSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<PuddleSystem>();
|
var puddleSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<PuddleSystem>();
|
||||||
var gameTiming = server.ResolveDependency<IGameTiming>();
|
var gameTiming = server.ResolveDependency<IGameTiming>();
|
||||||
MapId mapId;
|
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 (`.`)
|
In this test, if o is spillage puddle and # are walls, we want to ensure all tiles are empty (`.`)
|
||||||
o # .
|
|
||||||
# . .
|
|
||||||
. . .
|
. . .
|
||||||
|
# . .
|
||||||
|
o # .
|
||||||
*/
|
*/
|
||||||
await server.WaitPost(() =>
|
await server.WaitPost(() =>
|
||||||
{
|
{
|
||||||
@@ -144,10 +73,9 @@ public sealed class FluidSpill
|
|||||||
await server.WaitAssertion(() =>
|
await server.WaitAssertion(() =>
|
||||||
{
|
{
|
||||||
var grid = mapManager.GetGrid(gridId);
|
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 tileRef = grid.GetTileRef(puddleOrigin);
|
||||||
var puddle = spillSystem.SpillAt(tileRef, solution, "PuddleSmear");
|
Assert.That(puddleSystem.TrySpillAt(tileRef, solution, out _), Is.True);
|
||||||
Assert.That(puddle, Is.Not.Null);
|
|
||||||
Assert.That(GetPuddle(entityManager, grid, puddleOrigin), Is.Not.Null);
|
Assert.That(GetPuddle(entityManager, grid, puddleOrigin), Is.Not.Null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Content.Server.Fluids.EntitySystems;
|
|||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared.Chemistry.Components;
|
||||||
using Content.Shared.Coordinates;
|
using Content.Shared.Coordinates;
|
||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
|
using Content.Shared.Fluids.Components;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
@@ -26,7 +27,7 @@ namespace Content.IntegrationTests.Tests.Fluids
|
|||||||
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
||||||
|
|
||||||
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
|
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
|
||||||
var spillSystem = entitySystemManager.GetEntitySystem<SpillableSystem>();
|
var spillSystem = entitySystemManager.GetEntitySystem<PuddleSystem>();
|
||||||
|
|
||||||
await server.WaitAssertion(() =>
|
await server.WaitAssertion(() =>
|
||||||
{
|
{
|
||||||
@@ -35,9 +36,9 @@ namespace Content.IntegrationTests.Tests.Fluids
|
|||||||
var gridUid = tile.GridUid;
|
var gridUid = tile.GridUid;
|
||||||
var (x, y) = tile.GridIndices;
|
var (x, y) = tile.GridIndices;
|
||||||
var coordinates = new EntityCoordinates(gridUid, x, y);
|
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);
|
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ namespace Content.IntegrationTests.Tests.Fluids
|
|||||||
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
||||||
|
|
||||||
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
|
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
|
||||||
var spillSystem = entitySystemManager.GetEntitySystem<SpillableSystem>();
|
var spillSystem = entitySystemManager.GetEntitySystem<PuddleSystem>();
|
||||||
|
|
||||||
MapGridComponent grid = null;
|
MapGridComponent grid = null;
|
||||||
|
|
||||||
@@ -74,124 +75,11 @@ namespace Content.IntegrationTests.Tests.Fluids
|
|||||||
{
|
{
|
||||||
var coordinates = grid.ToCoordinates();
|
var coordinates = grid.ToCoordinates();
|
||||||
var solution = new Solution("Water", FixedPoint2.New(20));
|
var solution = new Solution("Water", FixedPoint2.New(20));
|
||||||
var puddle = spillSystem.SpillAt(solution, coordinates, "PuddleSmear");
|
var puddle = spillSystem.TrySpillAt(coordinates, solution, out _);
|
||||||
Assert.Null(puddle);
|
Assert.False(puddle);
|
||||||
});
|
});
|
||||||
|
|
||||||
await pairTracker.CleanReturnAsync();
|
await pairTracker.CleanReturnAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task PuddlePauseTest()
|
|
||||||
{
|
|
||||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
|
|
||||||
var server = pairTracker.Pair.Server;
|
|
||||||
|
|
||||||
var sMapManager = server.ResolveDependency<IMapManager>();
|
|
||||||
var sTileDefinitionManager = server.ResolveDependency<ITileDefinitionManager>();
|
|
||||||
var sGameTiming = server.ResolveDependency<IGameTiming>();
|
|
||||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
|
||||||
var metaSystem = entityManager.EntitySysManager.GetEntitySystem<MetaDataSystem>();
|
|
||||||
|
|
||||||
MapId sMapId = default;
|
|
||||||
MapGridComponent sGrid;
|
|
||||||
EntityUid sGridId = default;
|
|
||||||
EntityCoordinates sCoordinates = default;
|
|
||||||
|
|
||||||
// Spawn a paused map with one tile to spawn puddles on
|
|
||||||
await server.WaitPost(() =>
|
|
||||||
{
|
|
||||||
sMapId = sMapManager.CreateMap();
|
|
||||||
sMapManager.SetMapPaused(sMapId, true);
|
|
||||||
sGrid = sMapManager.CreateGrid(sMapId);
|
|
||||||
sGridId = sGrid.Owner;
|
|
||||||
metaSystem.SetEntityPaused(sGridId, true); // See https://github.com/space-wizards/RobustToolbox/issues/1444
|
|
||||||
|
|
||||||
var tileDefinition = sTileDefinitionManager["UnderPlating"];
|
|
||||||
var tile = new Tile(tileDefinition.TileId);
|
|
||||||
sCoordinates = sGrid.ToCoordinates();
|
|
||||||
|
|
||||||
sGrid.SetTile(sCoordinates, tile);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check that the map and grid are paused
|
|
||||||
await server.WaitAssertion(() =>
|
|
||||||
{
|
|
||||||
Assert.True(metaSystem.EntityPaused(sGridId));
|
|
||||||
Assert.True(sMapManager.IsMapPaused(sMapId));
|
|
||||||
});
|
|
||||||
|
|
||||||
float evaporateTime = default;
|
|
||||||
PuddleComponent puddle = null;
|
|
||||||
MetaDataComponent meta = null;
|
|
||||||
EvaporationComponent evaporation;
|
|
||||||
|
|
||||||
var amount = 2;
|
|
||||||
|
|
||||||
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
|
|
||||||
var spillSystem = entitySystemManager.GetEntitySystem<SpillableSystem>();
|
|
||||||
|
|
||||||
// Spawn a puddle
|
|
||||||
await server.WaitAssertion(() =>
|
|
||||||
{
|
|
||||||
var solution = new Solution("Water", FixedPoint2.New(amount));
|
|
||||||
puddle = spillSystem.SpillAt(solution, sCoordinates, "PuddleSmear");
|
|
||||||
meta = entityManager.GetComponent<MetaDataComponent>(puddle.Owner);
|
|
||||||
|
|
||||||
// Check that the puddle was created
|
|
||||||
Assert.NotNull(puddle);
|
|
||||||
|
|
||||||
evaporation = entityManager.GetComponent<EvaporationComponent>(puddle.Owner);
|
|
||||||
metaSystem.SetEntityPaused(puddle.Owner, true, meta); // See https://github.com/space-wizards/RobustToolbox/issues/1445
|
|
||||||
|
|
||||||
Assert.True(metaSystem.EntityPaused(puddle.Owner, meta));
|
|
||||||
|
|
||||||
// Check that the puddle is going to evaporate
|
|
||||||
Assert.Positive(evaporation.EvaporateTime);
|
|
||||||
|
|
||||||
// Should have a timer component added to it for evaporation
|
|
||||||
Assert.That(evaporation.Accumulator, Is.EqualTo(0f));
|
|
||||||
|
|
||||||
evaporateTime = evaporation.EvaporateTime;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait enough time for it to evaporate if it was unpaused
|
|
||||||
var sTimeToWait = 5 + (int)Math.Ceiling(amount * evaporateTime * sGameTiming.TickRate);
|
|
||||||
await PoolManager.RunTicksSync(pairTracker.Pair, sTimeToWait);
|
|
||||||
|
|
||||||
// No evaporation due to being paused
|
|
||||||
await server.WaitAssertion(() =>
|
|
||||||
{
|
|
||||||
Assert.True(meta.EntityPaused);
|
|
||||||
|
|
||||||
// Check that the puddle still exists
|
|
||||||
Assert.False(meta.EntityDeleted);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Unpause the map
|
|
||||||
await server.WaitPost(() => { sMapManager.SetMapPaused(sMapId, false); });
|
|
||||||
|
|
||||||
// Check that the map, grid and puddle are unpaused
|
|
||||||
await server.WaitAssertion(() =>
|
|
||||||
{
|
|
||||||
Assert.False(sMapManager.IsMapPaused(sMapId));
|
|
||||||
Assert.False(metaSystem.EntityPaused(sGridId));
|
|
||||||
Assert.False(meta.EntityPaused);
|
|
||||||
|
|
||||||
// Check that the puddle still exists
|
|
||||||
Assert.False(meta.EntityDeleted);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait enough time for it to evaporate
|
|
||||||
await PoolManager.RunTicksSync(pairTracker.Pair, sTimeToWait);
|
|
||||||
|
|
||||||
// Puddle evaporation should have ticked
|
|
||||||
await server.WaitAssertion(() =>
|
|
||||||
{
|
|
||||||
// Check that puddle has been deleted
|
|
||||||
Assert.True(puddle.Deleted);
|
|
||||||
});
|
|
||||||
await pairTracker.CleanReturnAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,10 @@ namespace Content.Server.AME
|
|||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
private AMEControllerComponent? _masterController;
|
private AMEControllerComponent? _masterController;
|
||||||
|
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
|
||||||
|
|
||||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
|
||||||
|
|
||||||
[Dependency] private readonly IChatManager _chat = 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;
|
public AMEControllerComponent? MasterController => _masterController;
|
||||||
|
|
||||||
@@ -41,7 +40,6 @@ namespace Content.Server.AME
|
|||||||
{
|
{
|
||||||
base.LoadNodes(groupNodes);
|
base.LoadNodes(groupNodes);
|
||||||
|
|
||||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
|
||||||
MapGridComponent? grid = null;
|
MapGridComponent? grid = null;
|
||||||
|
|
||||||
foreach (var node in groupNodes)
|
foreach (var node in groupNodes)
|
||||||
@@ -50,7 +48,7 @@ namespace Content.Server.AME
|
|||||||
if (_entMan.TryGetComponent(nodeOwner, out AMEShieldComponent? shield))
|
if (_entMan.TryGetComponent(nodeOwner, out AMEShieldComponent? shield))
|
||||||
{
|
{
|
||||||
var xform = _entMan.GetComponent<TransformComponent>(nodeOwner);
|
var xform = _entMan.GetComponent<TransformComponent>(nodeOwner);
|
||||||
if (xform.GridUid != grid?.Owner && !mapManager.TryGetGrid(xform.GridUid, out grid))
|
if (xform.GridUid != grid?.Owner && !_mapManager.TryGetGrid(xform.GridUid, out grid))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (grid == null)
|
if (grid == null)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Content.Server.Chemistry.EntitySystems;
|
|||||||
using Content.Server.DoAfter;
|
using Content.Server.DoAfter;
|
||||||
using Content.Server.Nutrition.Components;
|
using Content.Server.Nutrition.Components;
|
||||||
using Content.Server.Popups;
|
using Content.Server.Popups;
|
||||||
|
using Content.Shared.Chemistry.Components;
|
||||||
using Content.Shared.DoAfter;
|
using Content.Shared.DoAfter;
|
||||||
using Content.Shared.IdentityManagement;
|
using Content.Shared.IdentityManagement;
|
||||||
using Content.Shared.Nutrition.Components;
|
using Content.Shared.Nutrition.Components;
|
||||||
|
|||||||
@@ -17,25 +17,26 @@ namespace Content.Server.Atmos.Reactions
|
|||||||
|
|
||||||
[DataField("molesPerUnit")] public float MolesPerUnit { get; } = 1;
|
[DataField("molesPerUnit")] public float MolesPerUnit { get; } = 1;
|
||||||
|
|
||||||
[DataField("puddlePrototype")] public string? PuddlePrototype { get; } = "PuddleSmear";
|
|
||||||
|
|
||||||
public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, AtmosphereSystem atmosphereSystem)
|
public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, AtmosphereSystem atmosphereSystem)
|
||||||
{
|
{
|
||||||
// If any of the prototypes is invalid, we do nothing.
|
// 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 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 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...
|
// Remove the moles from the mixture...
|
||||||
mixture.AdjustMoles(GasId, -MolesPerUnit);
|
mixture.AdjustMoles(GasId, -MolesPerUnit);
|
||||||
|
|
||||||
var tileRef = tile.GridIndices.GetTileRef(tile.GridIndex);
|
var tileRef = tile.GridIndices.GetTileRef(tile.GridIndex);
|
||||||
EntitySystem.Get<SpillableSystem>()
|
EntitySystem.Get<PuddleSystem>()
|
||||||
.SpillAt(tileRef, new Solution(Reagent, FixedPoint2.New(MolesPerUnit)), PuddlePrototype, sound: false);
|
.TrySpillAt(tileRef, new Solution(Reagent, FixedPoint2.New(MolesPerUnit)), out _, sound: false);
|
||||||
|
|
||||||
return ReactionResult.Reacting;
|
return ReactionResult.Reacting;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ namespace Content.Server.Body.Systems;
|
|||||||
|
|
||||||
public sealed class BloodstreamSystem : EntitySystem
|
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 IPrototypeManager _prototypeManager = default!;
|
||||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||||
[Dependency] private readonly AudioSystem _audio = 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 SharedDrunkSystem _drunkSystem = default!;
|
||||||
|
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -307,12 +307,13 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||||||
// Pass some of the chemstream into the spilled blood.
|
// Pass some of the chemstream into the spilled blood.
|
||||||
var temp = component.ChemicalSolution.SplitSolution(component.BloodTemporarySolution.Volume / 10);
|
var temp = component.ChemicalSolution.SplitSolution(component.BloodTemporarySolution.Volume / 10);
|
||||||
component.BloodTemporarySolution.AddSolution(temp, _prototypeManager);
|
component.BloodTemporarySolution.AddSolution(temp, _prototypeManager);
|
||||||
var puddle = _spillableSystem.SpillAt(uid, component.BloodTemporarySolution, "PuddleBlood", false);
|
if (_puddleSystem.TrySpillAt(uid, component.BloodTemporarySolution, out var puddleUid, false))
|
||||||
if (puddle != null)
|
|
||||||
{
|
{
|
||||||
var comp = EnsureComp<ForensicsComponent>(puddle.Owner); //TODO: Get rid of .Owner
|
|
||||||
if (TryComp<DnaComponent>(uid, out var dna))
|
if (TryComp<DnaComponent>(uid, out var dna))
|
||||||
|
{
|
||||||
|
var comp = EnsureComp<ForensicsComponent>(puddleUid);
|
||||||
comp.DNAs.Add(dna.DNA);
|
comp.DNAs.Add(dna.DNA);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component.BloodTemporarySolution.RemoveAllSolution();
|
component.BloodTemporarySolution.RemoveAllSolution();
|
||||||
@@ -353,13 +354,14 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||||||
component.BloodTemporarySolution.RemoveAllSolution();
|
component.BloodTemporarySolution.RemoveAllSolution();
|
||||||
tempSol.AddSolution(component.ChemicalSolution, _prototypeManager);
|
tempSol.AddSolution(component.ChemicalSolution, _prototypeManager);
|
||||||
component.ChemicalSolution.RemoveAllSolution();
|
component.ChemicalSolution.RemoveAllSolution();
|
||||||
var puddle = _spillableSystem.SpillAt(uid, tempSol, "PuddleBlood", true);
|
|
||||||
|
|
||||||
if (puddle != null)
|
if (_puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid))
|
||||||
{
|
{
|
||||||
var comp = EnsureComp<ForensicsComponent>(puddle.Owner); //TODO: Get rid of .Owner
|
|
||||||
if (TryComp<DnaComponent>(uid, out var dna))
|
if (TryComp<DnaComponent>(uid, out var dna))
|
||||||
|
{
|
||||||
|
var comp = EnsureComp<ForensicsComponent>(puddleUid);
|
||||||
comp.DNAs.Add(dna.DNA);
|
comp.DNAs.Add(dna.DNA);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
|
|
||||||
{
|
|
||||||
appearance.SetData(FoamVisuals.Color, solution.GetColor(_proto).WithAlpha(0.80f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ReactWithEntity(EntityUid entity, double solutionFraction)
|
|
||||||
{
|
|
||||||
if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_entMan.TryGetComponent(entity, out BloodstreamComponent? bloodstream))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var invSystem = EntitySystem.Get<InventorySystem>();
|
|
||||||
|
|
||||||
// TODO: Add a permeability property to clothing
|
|
||||||
// For now it just adds to protection for each clothing equipped
|
|
||||||
var protection = 0f;
|
|
||||||
if (invSystem.TryGetSlots(entity, out var slotDefinitions))
|
|
||||||
{
|
|
||||||
foreach (var slot in slotDefinitions)
|
|
||||||
{
|
|
||||||
if (slot.Name == "back" ||
|
|
||||||
slot.Name == "pocket1" ||
|
|
||||||
slot.Name == "pocket2" ||
|
|
||||||
slot.Name == "id")
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (invSystem.TryGetSlotEntity(entity, slot.Name, out _))
|
|
||||||
protection += 0.025f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var bloodstreamSys = EntitySystem.Get<BloodstreamSystem>();
|
|
||||||
|
|
||||||
var cloneSolution = solution.Clone();
|
|
||||||
var transferAmount = FixedPoint2.Min(cloneSolution.Volume * solutionFraction * (1 - protection),
|
|
||||||
bloodstream.ChemicalSolution.AvailableVolume);
|
|
||||||
var transferSolution = cloneSolution.SplitSolution(transferAmount);
|
|
||||||
|
|
||||||
if (bloodstreamSys.TryAddToChemicals(entity, transferSolution, bloodstream))
|
|
||||||
{
|
|
||||||
// Log solution addition by foam
|
|
||||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{_entMan.ToPrettyString(entity):target} was affected by foam {SolutionContainerSystem.ToPrettyString(transferSolution)}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnKill()
|
|
||||||
{
|
|
||||||
if (_entMan.Deleted(Owner))
|
|
||||||
return;
|
|
||||||
if (_entMan.TryGetComponent(Owner, out AppearanceComponent? appearance))
|
|
||||||
{
|
|
||||||
appearance.SetData(FoamVisuals.State, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Owner.SpawnTimer(600, () =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_foamedMetalPrototype))
|
|
||||||
{
|
|
||||||
_entMan.SpawnEntity(_foamedMetalPrototype, _entMan.GetComponent<TransformComponent>(Owner).Coordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
_entMan.QueueDeleteEntity(Owner);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
Content.Server/Chemistry/Components/SmokeComponent.cs
Normal file
26
Content.Server/Chemistry/Components/SmokeComponent.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Content.Shared.Fluids.Components;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||||
|
|
||||||
|
namespace Content.Server.Chemistry.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores solution on an anchored entity that has touch and ingestion reactions
|
||||||
|
/// to entities that collide with it. Similar to <see cref="PuddleComponent"/>
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class SmokeComponent : Component
|
||||||
|
{
|
||||||
|
public const string SolutionName = "solutionArea";
|
||||||
|
|
||||||
|
[DataField("nextReact", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
||||||
|
public TimeSpan NextReact = TimeSpan.Zero;
|
||||||
|
|
||||||
|
[DataField("spreadAmount")]
|
||||||
|
public int SpreadAmount = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Have we reacted with our tile yet?
|
||||||
|
/// </summary>
|
||||||
|
[DataField("reactedTile")]
|
||||||
|
public bool ReactedTile = false;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When a <see cref="SmokeComponent"/> despawns this will spawn another entity in its place.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, Access(typeof(SmokeSystem))]
|
||||||
|
public sealed class SmokeDissipateSpawnComponent : Component
|
||||||
|
{
|
||||||
|
[DataField("prototype", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
|
public string Prototype = string.Empty;
|
||||||
|
}
|
||||||
@@ -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<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
|
|
||||||
{
|
|
||||||
appearance.SetData(SmokeVisuals.Color, solution.GetColor(_proto));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ReactWithEntity(EntityUid entity, double solutionFraction)
|
|
||||||
{
|
|
||||||
if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_entMan.TryGetComponent(entity, out BloodstreamComponent? bloodstream))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_entMan.TryGetComponent(entity, out InternalsComponent? internals) &&
|
|
||||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InternalsSystem>().AreInternalsWorking(internals))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var chemistry = EntitySystem.Get<ReactiveSystem>();
|
|
||||||
var cloneSolution = solution.Clone();
|
|
||||||
var transferAmount = FixedPoint2.Min(cloneSolution.Volume * solutionFraction, bloodstream.ChemicalSolution.AvailableVolume);
|
|
||||||
var transferSolution = cloneSolution.SplitSolution(transferAmount);
|
|
||||||
|
|
||||||
foreach (var reagentQuantity in transferSolution.Contents.ToArray())
|
|
||||||
{
|
|
||||||
if (reagentQuantity.Quantity == FixedPoint2.Zero) continue;
|
|
||||||
chemistry.ReactionEntity(entity, ReactionMethod.Ingestion, reagentQuantity.ReagentId, reagentQuantity.Quantity, transferSolution);
|
|
||||||
}
|
|
||||||
|
|
||||||
var bloodstreamSys = EntitySystem.Get<BloodstreamSystem>();
|
|
||||||
if (bloodstreamSys.TryAddToChemicals(entity, transferSolution, bloodstream))
|
|
||||||
{
|
|
||||||
// Log solution addition by smoke
|
|
||||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{_entMan.ToPrettyString(entity):target} was affected by smoke {SolutionContainerSystem.ToPrettyString(transferSolution)}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected override void OnKill()
|
|
||||||
{
|
|
||||||
if (_entMan.Deleted(Owner))
|
|
||||||
return;
|
|
||||||
_entMan.DeleteEntity(Owner);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Used to clone its owner repeatedly and group up them all so they behave like one unit, that way you can have
|
|
||||||
/// effects that cover an area. Inherited by <see cref="SmokeSolutionAreaEffectComponent"/> and <see cref="FoamSolutionAreaEffectComponent"/>.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class SolutionAreaEffectComponent : Component
|
|
||||||
{
|
|
||||||
public const string SolutionName = "solutionArea";
|
|
||||||
|
|
||||||
[Dependency] protected readonly IMapManager MapManager = default!;
|
|
||||||
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
|
|
||||||
[Dependency] private readonly IEntityManager _entities = default!;
|
|
||||||
[Dependency] private readonly IEntitySystemManager _systems = default!;
|
|
||||||
|
|
||||||
public int Amount { get; set; }
|
|
||||||
public SolutionAreaEffectInceptionComponent? Inception { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Have we reacted with our tile yet?
|
|
||||||
/// </summary>
|
|
||||||
public bool ReactedTile = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds an <see cref="SolutionAreaEffectInceptionComponent"/> to owner so the effect starts spreading and reacting.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="amount">The range of the effect</param>
|
|
||||||
/// <param name="duration"></param>
|
|
||||||
/// <param name="spreadDelay"></param>
|
|
||||||
/// <param name="removeDelay"></param>
|
|
||||||
public void Start(int amount, float duration, float spreadDelay, float removeDelay)
|
|
||||||
{
|
|
||||||
if (Inception != null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_entities.HasComponent<SolutionAreaEffectInceptionComponent>(Owner))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Amount = amount;
|
|
||||||
var inception = _entities.AddComponent<SolutionAreaEffectInceptionComponent>(Owner);
|
|
||||||
|
|
||||||
inception.Add(this);
|
|
||||||
inception.Setup(amount, duration, spreadDelay, removeDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets called by an AreaEffectInceptionComponent. "Clones" Owner into the four directions and copies the
|
|
||||||
/// solution into each of them.
|
|
||||||
/// </summary>
|
|
||||||
public void Spread()
|
|
||||||
{
|
|
||||||
var meta = _entities.GetComponent<MetaDataComponent>(Owner);
|
|
||||||
if (meta.EntityPrototype == null)
|
|
||||||
{
|
|
||||||
Logger.Error("AreaEffectComponent needs its owner to be spawned by a prototype.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var xform = _entities.GetComponent<TransformComponent>(Owner);
|
|
||||||
var solSys = _systems.GetEntitySystem<SolutionContainerSystem>();
|
|
||||||
|
|
||||||
if (!_entities.TryGetComponent(xform.GridUid, out MapGridComponent? gridComp))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var origin = gridComp.TileIndicesFor(xform.Coordinates);
|
|
||||||
|
|
||||||
DebugTools.Assert(xform.Anchored, "Area effect entity prototypes must be anchored.");
|
|
||||||
|
|
||||||
void SpreadToDir(Direction dir)
|
|
||||||
{
|
|
||||||
// Currently no support for spreading off or across grids.
|
|
||||||
var index = origin + dir.ToIntVec();
|
|
||||||
if (!gridComp.TryGetTileRef(index, out var tile) || tile.Tile.IsEmpty)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var neighbor in gridComp.GetAnchoredEntities(index))
|
|
||||||
{
|
|
||||||
if (_entities.TryGetComponent(neighbor,
|
|
||||||
out SolutionAreaEffectComponent? comp) && comp.Inception == Inception)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// TODO for thindows and the like, need to check the directions that are being blocked.
|
|
||||||
// --> would then also mean you need to check for blockers on the origin tile.
|
|
||||||
if (_entities.TryGetComponent(neighbor,
|
|
||||||
out AirtightComponent? airtight) && airtight.AirBlocked)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newEffect = _entities.SpawnEntity(
|
|
||||||
meta.EntityPrototype.ID,
|
|
||||||
gridComp.GridTileToLocal(index));
|
|
||||||
|
|
||||||
if (!_entities.TryGetComponent(newEffect, out SolutionAreaEffectComponent? effectComponent))
|
|
||||||
{
|
|
||||||
_entities.DeleteEntity(newEffect);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (solSys.TryGetSolution(Owner, SolutionName, out var solution))
|
|
||||||
{
|
|
||||||
effectComponent.TryAddSolution(solution.Clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
effectComponent.Amount = Amount - 1;
|
|
||||||
Inception?.Add(effectComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
SpreadToDir(Direction.North);
|
|
||||||
SpreadToDir(Direction.East);
|
|
||||||
SpreadToDir(Direction.South);
|
|
||||||
SpreadToDir(Direction.West);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets called by an AreaEffectInceptionComponent.
|
|
||||||
/// Removes this component from its inception and calls OnKill(). The implementation of OnKill() should
|
|
||||||
/// eventually delete the entity.
|
|
||||||
/// </summary>
|
|
||||||
public void Kill()
|
|
||||||
{
|
|
||||||
Inception?.Remove(this);
|
|
||||||
OnKill();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void OnKill();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets called by an AreaEffectInceptionComponent.
|
|
||||||
/// Makes this effect's reagents react with the tile its on and with the entities it covers. Also calls
|
|
||||||
/// ReactWithEntity on the entities so inheritors can implement more specific behavior.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="averageExposures">How many times will this get called over this area effect's duration, averaged
|
|
||||||
/// with the other area effects from the inception.</param>
|
|
||||||
public void React(float averageExposures)
|
|
||||||
{
|
|
||||||
if (!_entities.EntitySysManager.GetEntitySystem<SolutionContainerSystem>()
|
|
||||||
.TryGetSolution(Owner, SolutionName, out var solution) ||
|
|
||||||
solution.Contents.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var xform = _entities.GetComponent<TransformComponent>(Owner);
|
|
||||||
if (!MapManager.TryGetGrid(xform.GridUid, out var mapGrid))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var tile = mapGrid.GetTileRef(xform.Coordinates.ToVector2i(_entities, MapManager));
|
|
||||||
var chemistry = _entities.EntitySysManager.GetEntitySystem<ReactiveSystem>();
|
|
||||||
var lookup = _entities.EntitySysManager.GetEntitySystem<EntityLookupSystem>();
|
|
||||||
|
|
||||||
var solutionFraction = 1 / Math.Floor(averageExposures);
|
|
||||||
var ents = lookup.GetEntitiesIntersecting(tile, LookupFlags.Uncontained).ToArray();
|
|
||||||
|
|
||||||
foreach (var reagentQuantity in solution.Contents.ToArray())
|
|
||||||
{
|
|
||||||
if (reagentQuantity.Quantity == FixedPoint2.Zero) continue;
|
|
||||||
var reagent = PrototypeManager.Index<ReagentPrototype>(reagentQuantity.ReagentId);
|
|
||||||
|
|
||||||
// React with the tile the effect is on
|
|
||||||
// We don't multiply by solutionFraction here since the tile is only ever reacted once
|
|
||||||
if (!ReactedTile)
|
|
||||||
{
|
|
||||||
reagent.ReactionTile(tile, reagentQuantity.Quantity);
|
|
||||||
ReactedTile = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Touch every entity on the tile
|
|
||||||
foreach (var entity in ents)
|
|
||||||
{
|
|
||||||
chemistry.ReactionEntity(entity, ReactionMethod.Touch, reagent,
|
|
||||||
reagentQuantity.Quantity * solutionFraction, solution);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entity in ents)
|
|
||||||
{
|
|
||||||
ReactWithEntity(entity, solutionFraction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void ReactWithEntity(EntityUid entity, double solutionFraction);
|
|
||||||
|
|
||||||
public void TryAddSolution(Solution solution)
|
|
||||||
{
|
|
||||||
if (solution.Volume == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solutionArea))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var addSolution =
|
|
||||||
solution.SplitSolution(FixedPoint2.Min(solution.Volume, solutionArea.AvailableVolume));
|
|
||||||
|
|
||||||
EntitySystem.Get<SolutionContainerSystem>().TryAddSolution(Owner, solutionArea, addSolution);
|
|
||||||
|
|
||||||
UpdateVisuals();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void UpdateVisuals();
|
|
||||||
|
|
||||||
protected override void OnRemove()
|
|
||||||
{
|
|
||||||
base.OnRemove();
|
|
||||||
Inception?.Remove(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Content.Server.Chemistry.Components
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The "mastermind" of a SolutionAreaEffect group. It gets updated by the SolutionAreaEffectSystem and tells the
|
|
||||||
/// group when to spread, react and remove itself. This makes the group act like a single unit.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks> It should only be manually added to an entity by the <see cref="SolutionAreaEffectComponent"/> and not with a prototype.</remarks>
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed class SolutionAreaEffectInceptionComponent : Component
|
|
||||||
{
|
|
||||||
private const float ReactionDelay = 1.5f;
|
|
||||||
|
|
||||||
private readonly HashSet<SolutionAreaEffectComponent> _group = new();
|
|
||||||
|
|
||||||
[ViewVariables] private float _lifeTimer;
|
|
||||||
[ViewVariables] private float _spreadTimer;
|
|
||||||
[ViewVariables] private float _reactionTimer;
|
|
||||||
|
|
||||||
[ViewVariables] private int _amountCounterSpreading;
|
|
||||||
[ViewVariables] private int _amountCounterRemoving;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How much time to wait after fully spread before starting to remove itself.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables] private float _duration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Time between each spread step. Decreasing this makes spreading faster.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables] private float _spreadDelay;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Time between each remove step. Decreasing this makes removing faster.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables] private float _removeDelay;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How many times will the effect react. As some entities from the group last a different amount of time than
|
|
||||||
/// others, they will react a different amount of times, so we calculate the average to make the group behave
|
|
||||||
/// a bit more uniformly.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables] private float _averageExposures;
|
|
||||||
|
|
||||||
public void Setup(int amount, float duration, float spreadDelay, float removeDelay)
|
|
||||||
{
|
|
||||||
_amountCounterSpreading = amount;
|
|
||||||
_duration = duration;
|
|
||||||
_spreadDelay = spreadDelay;
|
|
||||||
_removeDelay = removeDelay;
|
|
||||||
|
|
||||||
// So the first square reacts immediately after spawning
|
|
||||||
_reactionTimer = ReactionDelay;
|
|
||||||
/*
|
|
||||||
The group takes amount*spreadDelay seconds to fully spread, same with fully disappearing.
|
|
||||||
The outer squares will last duration seconds.
|
|
||||||
The first square will last duration + how many seconds the group takes to fully spread and fully disappear, so
|
|
||||||
it will last duration + amount*(spreadDelay+removeDelay).
|
|
||||||
Thus, the average lifetime of the smokes will be (outerSmokeLifetime + firstSmokeLifetime)/2 = duration + amount*(spreadDelay+removeDelay)/2
|
|
||||||
*/
|
|
||||||
_averageExposures = (duration + amount * (spreadDelay+removeDelay) / 2)/ReactionDelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void InceptionUpdate(float frameTime)
|
|
||||||
{
|
|
||||||
_group.RemoveWhere(effect => effect.Deleted);
|
|
||||||
if (_group.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Make every outer square from the group spread
|
|
||||||
if (_amountCounterSpreading > 0)
|
|
||||||
{
|
|
||||||
_spreadTimer += frameTime;
|
|
||||||
if (_spreadTimer > _spreadDelay)
|
|
||||||
{
|
|
||||||
_spreadTimer -= _spreadDelay;
|
|
||||||
|
|
||||||
var outerEffects = new HashSet<SolutionAreaEffectComponent>(_group.Where(effect => effect.Amount == _amountCounterSpreading));
|
|
||||||
foreach (var effect in outerEffects)
|
|
||||||
{
|
|
||||||
effect.Spread();
|
|
||||||
}
|
|
||||||
|
|
||||||
_amountCounterSpreading -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Start counting for _duration after fully spreading
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_lifeTimer += frameTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete every outer square
|
|
||||||
if (_lifeTimer > _duration)
|
|
||||||
{
|
|
||||||
_spreadTimer += frameTime;
|
|
||||||
if (_spreadTimer > _removeDelay)
|
|
||||||
{
|
|
||||||
_spreadTimer -= _removeDelay;
|
|
||||||
|
|
||||||
var outerEffects = new HashSet<SolutionAreaEffectComponent>(_group.Where(effect => effect.Amount == _amountCounterRemoving));
|
|
||||||
foreach (var effect in outerEffects)
|
|
||||||
{
|
|
||||||
effect.Kill();
|
|
||||||
}
|
|
||||||
|
|
||||||
_amountCounterRemoving += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make every square from the group react with the tile and entities
|
|
||||||
_reactionTimer += frameTime;
|
|
||||||
if (_reactionTimer > ReactionDelay)
|
|
||||||
{
|
|
||||||
_reactionTimer -= ReactionDelay;
|
|
||||||
foreach (var effect in _group)
|
|
||||||
{
|
|
||||||
effect.React(_averageExposures);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(SolutionAreaEffectComponent effect)
|
|
||||||
{
|
|
||||||
_group.Add(effect);
|
|
||||||
effect.Inception = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Remove(SolutionAreaEffectComponent effect)
|
|
||||||
{
|
|
||||||
_group.Remove(effect);
|
|
||||||
effect.Inception = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace Content.Server.Chemistry.Components.SolutionManager
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Denotes the solution that can be easily removed through any reagent container.
|
|
||||||
/// Think pouring this or draining from a water tank.
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed class DrainableSolutionComponent : Component
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Solution name that can be drained.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
[DataField("solution")]
|
|
||||||
public string Solution { get; set; } = "default";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using Content.Shared.FixedPoint;
|
|
||||||
|
|
||||||
namespace Content.Server.Chemistry.Components.SolutionManager
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Reagents that can be added easily. For example like
|
|
||||||
/// pouring something into another beaker, glass, or into the gas
|
|
||||||
/// tank of a car.
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed class RefillableSolutionComponent : Component
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Solution name that can added to easily.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
[DataField("solution")]
|
|
||||||
public string Solution { get; set; } = "default";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The maximum amount that can be transferred to the solution at once
|
|
||||||
/// </summary>
|
|
||||||
[DataField("maxRefill")]
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
public FixedPoint2? MaxRefill { get; set; } = null;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<SolutionAreaEffectComponent, ReactionAttemptEvent>(OnReactionAttempt);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
|
||||||
{
|
|
||||||
foreach (var inception in EntityManager.EntityQuery<SolutionAreaEffectInceptionComponent>().ToArray())
|
|
||||||
{
|
|
||||||
inception.InceptionUpdate(frameTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnReactionAttempt(EntityUid uid, SolutionAreaEffectComponent component, ReactionAttemptEvent args)
|
|
||||||
{
|
|
||||||
if (args.Solution.Name != SolutionAreaEffectComponent.SolutionName)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Prevent smoke/foam fork bombs (smoke creating more smoke).
|
|
||||||
foreach (var effect in args.Reaction.Effects)
|
|
||||||
{
|
|
||||||
if (effect is AreaReactionEffect)
|
|
||||||
{
|
|
||||||
args.Cancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ using Content.Shared.Chemistry.Reagent;
|
|||||||
using Content.Shared.Examine;
|
using Content.Shared.Examine;
|
||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
@@ -127,6 +128,17 @@ public sealed partial class SolutionContainerSystem : EntitySystem
|
|||||||
return splitSol;
|
return splitSol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Splits a solution without the specified reagent.
|
||||||
|
/// </summary>
|
||||||
|
public Solution SplitSolutionWithout(EntityUid targetUid, Solution solutionHolder, FixedPoint2 quantity,
|
||||||
|
string reagent)
|
||||||
|
{
|
||||||
|
var splitSol = solutionHolder.SplitSolutionWithout(quantity, reagent);
|
||||||
|
UpdateChemicals(targetUid, solutionHolder);
|
||||||
|
return splitSol;
|
||||||
|
}
|
||||||
|
|
||||||
public void UpdateChemicals(EntityUid uid, Solution solutionHolder, bool needsReactionsProcessing = false, ReactionMixerComponent? mixerComponent = null)
|
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);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the most common reagent across all solutions by volume.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="component"></param>
|
||||||
|
public ReagentPrototype? GetMaxReagent(SolutionContainerManagerComponent component)
|
||||||
|
{
|
||||||
|
if (component.Solutions.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var reagentCounts = new Dictionary<string, FixedPoint2>();
|
||||||
|
|
||||||
|
foreach (var solution in component.Solutions.Values)
|
||||||
|
{
|
||||||
|
foreach (var reagent in solution.Contents)
|
||||||
|
{
|
||||||
|
reagentCounts.TryGetValue(reagent.ReagentId, out var existing);
|
||||||
|
existing += reagent.Quantity;
|
||||||
|
reagentCounts[reagent.ReagentId] = existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var max = reagentCounts.Max();
|
||||||
|
|
||||||
|
return _prototypeManager.Index<ReagentPrototype>(max.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoundSpecifier? GetSound(SolutionContainerManagerComponent component)
|
||||||
|
{
|
||||||
|
var max = GetMaxReagent(component);
|
||||||
|
return max?.FootstepSound;
|
||||||
|
}
|
||||||
|
|
||||||
// Thermal energy and temperature management.
|
// Thermal energy and temperature management.
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
using Content.Server.Chemistry.Components;
|
using Content.Server.Chemistry.Components;
|
||||||
using Content.Server.Chemistry.EntitySystems;
|
using Content.Server.Chemistry.EntitySystems;
|
||||||
using Content.Server.Coordinates.Helpers;
|
using Content.Server.Coordinates.Helpers;
|
||||||
|
using Content.Server.Fluids.EntitySystems;
|
||||||
using Content.Shared.Audio;
|
using Content.Shared.Audio;
|
||||||
using Content.Shared.Chemistry.Reagent;
|
using Content.Shared.Chemistry.Reagent;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
|
using Content.Shared.FixedPoint;
|
||||||
|
using Content.Shared.Maps;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Player;
|
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
|
namespace Content.Server.Chemistry.ReactionEffects
|
||||||
{
|
{
|
||||||
@@ -16,47 +20,23 @@ namespace Content.Server.Chemistry.ReactionEffects
|
|||||||
/// Basically smoke and foam reactions.
|
/// Basically smoke and foam reactions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
[ImplicitDataDefinitionForInheritors]
|
[DataDefinition]
|
||||||
public abstract class AreaReactionEffect : ReagentEffect, ISerializationHooks
|
public sealed class AreaReactionEffect : ReagentEffect
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used for calculating the spread range of the effect based on the intensity of the reaction.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("rangeConstant")] private float _rangeConstant;
|
|
||||||
[DataField("rangeMultiplier")] private float _rangeMultiplier = 1.1f;
|
|
||||||
[DataField("maxRange")] private int _maxRange = 10;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true the reagents get diluted or concentrated depending on the range of the effect
|
|
||||||
/// </summary>
|
|
||||||
[DataField("diluteReagents")] private bool _diluteReagents;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used to calculate dilution. Increasing this makes the reagents more diluted.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("reagentDilutionFactor")] private float _reagentDilutionFactor = 1f;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How many seconds will the effect stay, counting after fully spreading.
|
/// How many seconds will the effect stay, counting after fully spreading.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("duration")] private float _duration = 10;
|
[DataField("duration")] private float _duration = 10;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How many seconds between each spread step.
|
/// How many units of reaction for 1 smoke entity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("spreadDelay")] private float _spreadDelay = 0.5f;
|
[DataField("overflowThreshold")] public FixedPoint2 OverflowThreshold = FixedPoint2.New(2.5);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How many seconds between each remove step.
|
/// The entity prototype that will be spawned as the effect.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("removeDelay")] private float _removeDelay = 0.5f;
|
[DataField("prototypeId", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The entity prototype that will be spawned as the effect. It needs a component derived from SolutionAreaEffectComponent.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("prototypeId", required: true)]
|
|
||||||
private string _prototypeId = default!;
|
private string _prototypeId = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -67,55 +47,38 @@ namespace Content.Server.Chemistry.ReactionEffects
|
|||||||
public override bool ShouldLog => true;
|
public override bool ShouldLog => true;
|
||||||
public override LogImpact LogImpact => LogImpact.High;
|
public override LogImpact LogImpact => LogImpact.High;
|
||||||
|
|
||||||
void ISerializationHooks.AfterDeserialization()
|
|
||||||
{
|
|
||||||
IoCManager.InjectDependencies(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Effect(ReagentEffectArgs args)
|
public override void Effect(ReagentEffectArgs args)
|
||||||
{
|
{
|
||||||
if (args.Source == null)
|
if (args.Source == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var splitSolution = EntitySystem.Get<SolutionContainerSystem>().SplitSolution(args.SolutionEntity, args.Source, args.Source.Volume);
|
var spreadAmount = (int) Math.Max(0, Math.Ceiling((args.Quantity / OverflowThreshold).Float()));
|
||||||
// We take the square root so it becomes harder to reach higher amount values
|
var splitSolution = args.EntityManager.System<SolutionContainerSystem>().SplitSolution(args.SolutionEntity, args.Source, args.Source.Volume);
|
||||||
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 transform = args.EntityManager.GetComponent<TransformComponent>(args.SolutionEntity);
|
var transform = args.EntityManager.GetComponent<TransformComponent>(args.SolutionEntity);
|
||||||
|
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||||
|
|
||||||
if (!_mapManager.TryFindGridAt(transform.MapPosition, out var grid)) return;
|
if (!mapManager.TryFindGridAt(transform.MapPosition, out var grid) ||
|
||||||
|
!grid.TryGetTileRef(transform.Coordinates, out var tileRef) ||
|
||||||
var coords = grid.MapToGrid(transform.MapPosition);
|
tileRef.Tile.IsSpace())
|
||||||
|
|
||||||
var ent = args.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid());
|
|
||||||
|
|
||||||
var areaEffectComponent = GetAreaEffectComponent(ent);
|
|
||||||
|
|
||||||
if (areaEffectComponent == null)
|
|
||||||
{
|
{
|
||||||
Logger.Error("Couldn't get AreaEffectComponent from " + _prototypeId);
|
|
||||||
IoCManager.Resolve<IEntityManager>().QueueDeleteEntity(ent);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
areaEffectComponent.TryAddSolution(splitSolution);
|
var coords = grid.MapToGrid(transform.MapPosition);
|
||||||
areaEffectComponent.Start(amount, _duration, _spreadDelay, _removeDelay);
|
var ent = args.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid());
|
||||||
|
|
||||||
|
if (!args.EntityManager.TryGetComponent<SmokeComponent>(ent, out var smokeComponent))
|
||||||
|
{
|
||||||
|
Logger.Error("Couldn't get AreaEffectComponent from " + _prototypeId);
|
||||||
|
args.EntityManager.QueueDeleteEntity(ent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var smoke = args.EntityManager.System<SmokeSystem>();
|
||||||
|
smokeComponent.SpreadAmount = spreadAmount;
|
||||||
|
smoke.Start(ent, smokeComponent, splitSolution, _duration);
|
||||||
|
|
||||||
SoundSystem.Play(_sound.GetSound(), Filter.Pvs(args.SolutionEntity), args.SolutionEntity, AudioHelpers.WithVariation(0.125f));
|
SoundSystem.Play(_sound.GetSound(), Filter.Pvs(args.SolutionEntity), args.SolutionEntity, AudioHelpers.WithVariation(0.125f));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract SolutionAreaEffectComponent? GetAreaEffectComponent(EntityUid entity);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<IEntityManager>().GetComponentOrNull<FoamSolutionAreaEffectComponent>(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SpawnFoam(string entityPrototype, EntityCoordinates coords, Solution? contents, int amount, float duration, float spreadDelay,
|
|
||||||
float removeDelay, SoundSpecifier? sound = null, IEntityManager? entityManager = null)
|
|
||||||
{
|
|
||||||
entityManager ??= IoCManager.Resolve<IEntityManager>();
|
|
||||||
var ent = entityManager.SpawnEntity(entityPrototype, coords.SnapToGrid());
|
|
||||||
|
|
||||||
var areaEffectComponent = entityManager.GetComponentOrNull<FoamSolutionAreaEffectComponent>(ent);
|
|
||||||
|
|
||||||
if (areaEffectComponent == null)
|
|
||||||
{
|
|
||||||
Logger.Error("Couldn't get AreaEffectComponent from " + entityPrototype);
|
|
||||||
IoCManager.Resolve<IEntityManager>().QueueDeleteEntity(ent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contents != null)
|
|
||||||
areaEffectComponent.TryAddSolution(contents);
|
|
||||||
areaEffectComponent.Start(amount, duration, spreadDelay, removeDelay);
|
|
||||||
|
|
||||||
entityManager.EntitySysManager.GetEntitySystem<AudioSystem>()
|
|
||||||
.PlayPvs(sound, ent, AudioParams.Default.WithVariation(0.125f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<IEntityManager>().GetComponentOrNull<SmokeSolutionAreaEffectComponent>(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Content.Server.Fluids.EntitySystems;
|
using Content.Server.Fluids.EntitySystems;
|
||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared.Chemistry.Components;
|
||||||
using Content.Shared.Chemistry.Reaction;
|
using Content.Shared.Chemistry.Reaction;
|
||||||
using Content.Shared.Chemistry.Reagent;
|
using Content.Shared.Chemistry.Reagent;
|
||||||
@@ -14,10 +14,11 @@ namespace Content.Server.Chemistry.TileReactions
|
|||||||
{
|
{
|
||||||
public FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume)
|
public FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume)
|
||||||
{
|
{
|
||||||
var spillSystem = EntitySystem.Get<SpillableSystem>();
|
var spillSystem = EntitySystem.Get<PuddleSystem>();
|
||||||
if (reactVolume < 5 || !spillSystem.TryGetPuddle(tile, out _)) return FixedPoint2.Zero;
|
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
|
? reactVolume
|
||||||
: FixedPoint2.Zero;
|
: FixedPoint2.Zero;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ namespace Content.Server.Chemistry.TileReactions
|
|||||||
[DataField("launchForwardsMultiplier")] private float _launchForwardsMultiplier = 1;
|
[DataField("launchForwardsMultiplier")] private float _launchForwardsMultiplier = 1;
|
||||||
[DataField("requiredSlipSpeed")] private float _requiredSlipSpeed = 6;
|
[DataField("requiredSlipSpeed")] private float _requiredSlipSpeed = 6;
|
||||||
[DataField("paralyzeTime")] private float _paralyzeTime = 1;
|
[DataField("paralyzeTime")] private float _paralyzeTime = 1;
|
||||||
[DataField("overflow")] private bool _overflow;
|
|
||||||
|
|
||||||
public FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume)
|
public FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume)
|
||||||
{
|
{
|
||||||
@@ -27,19 +26,16 @@ namespace Content.Server.Chemistry.TileReactions
|
|||||||
|
|
||||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||||
|
|
||||||
// TODO Make this not puddle smear.
|
if (entityManager.EntitySysManager.GetEntitySystem<PuddleSystem>()
|
||||||
var puddle = entityManager.EntitySysManager.GetEntitySystem<SpillableSystem>()
|
.TrySpillAt(tile, new Solution(reagent.ID, reactVolume), out var puddleUid, false, false))
|
||||||
.SpillAt(tile, new Solution(reagent.ID, reactVolume), "PuddleSmear", _overflow, false, true);
|
|
||||||
|
|
||||||
if (puddle != null)
|
|
||||||
{
|
{
|
||||||
var slippery = entityManager.EnsureComponent<SlipperyComponent>(puddle.Owner);
|
var slippery = entityManager.EnsureComponent<SlipperyComponent>(puddleUid);
|
||||||
slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier;
|
slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier;
|
||||||
slippery.ParalyzeTime = _paralyzeTime;
|
slippery.ParalyzeTime = _paralyzeTime;
|
||||||
entityManager.Dirty(slippery);
|
entityManager.Dirty(slippery);
|
||||||
|
|
||||||
var step = entityManager.EnsureComponent<StepTriggerComponent>(puddle.Owner);
|
var step = entityManager.EnsureComponent<StepTriggerComponent>(puddleUid);
|
||||||
entityManager.EntitySysManager.GetEntitySystem<StepTriggerSystem>().SetRequiredTriggerSpeed(puddle.Owner, _requiredSlipSpeed, step);
|
entityManager.EntitySysManager.GetEntitySystem<StepTriggerSystem>().SetRequiredTriggerSpeed(puddleUid, _requiredSlipSpeed, step);
|
||||||
|
|
||||||
return reactVolume;
|
return reactVolume;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace Content.Server.Cloning
|
|||||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||||
[Dependency] private readonly SharedAppearanceSystem _appearance = 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 ChatSystem _chatSystem = default!;
|
||||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||||
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
||||||
@@ -316,7 +316,7 @@ namespace Content.Server.Cloning
|
|||||||
if (_robustRandom.Prob(0.2f))
|
if (_robustRandom.Prob(0.2f))
|
||||||
i++;
|
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);
|
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace Content.Server.Destructible
|
|||||||
[Dependency] public readonly StackSystem StackSystem = default!;
|
[Dependency] public readonly StackSystem StackSystem = default!;
|
||||||
[Dependency] public readonly TriggerSystem TriggerSystem = default!;
|
[Dependency] public readonly TriggerSystem TriggerSystem = default!;
|
||||||
[Dependency] public readonly SolutionContainerSystem SolutionContainerSystem = 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 IPrototypeManager PrototypeManager = default!;
|
||||||
[Dependency] public readonly IComponentFactory ComponentFactory = default!;
|
[Dependency] public readonly IComponentFactory ComponentFactory = default!;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
|
|||||||
// Spill the solution out into the world
|
// Spill the solution out into the world
|
||||||
// Spill before exploding in anticipation of a future where the explosion can light the solution on fire.
|
// Spill before exploding in anticipation of a future where the explosion can light the solution on fire.
|
||||||
var coordinates = system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates;
|
var coordinates = system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates;
|
||||||
system.SpillableSystem.SpillAt(explodingSolution, coordinates, "PuddleSmear", combine: true);
|
system.PuddleSystem.TrySpillAt(coordinates, explodingSolution, out _);
|
||||||
|
|
||||||
// Explode
|
// Explode
|
||||||
// Don't delete the object here - let other processes like physical damage from the
|
// Don't delete the object here - let other processes like physical damage from the
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
|
|||||||
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
|
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
|
||||||
{
|
{
|
||||||
var solutionContainerSystem = EntitySystem.Get<SolutionContainerSystem>();
|
var solutionContainerSystem = EntitySystem.Get<SolutionContainerSystem>();
|
||||||
var spillableSystem = EntitySystem.Get<SpillableSystem>();
|
var spillableSystem = EntitySystem.Get<PuddleSystem>();
|
||||||
|
|
||||||
var coordinates = system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates;
|
var coordinates = system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates;
|
||||||
|
|
||||||
@@ -31,12 +31,12 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
|
|||||||
solutionContainerSystem.TryGetSolution(owner, spillableComponent.SolutionName,
|
solutionContainerSystem.TryGetSolution(owner, spillableComponent.SolutionName,
|
||||||
out var compSolution))
|
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 &&
|
else if (Solution != null &&
|
||||||
solutionContainerSystem.TryGetSolution(owner, Solution, out var behaviorSolution))
|
solutionContainerSystem.TryGetSolution(owner, Solution, out var behaviorSolution))
|
||||||
{
|
{
|
||||||
spillableSystem.SplashSpillAt(owner, behaviorSolution, coordinates, "PuddleSmear", user: cause);
|
spillableSystem.TrySplashSpillAt(owner, coordinates, behaviorSolution, out _, user: cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ namespace Content.Server.Entry
|
|||||||
"ClientEntitySpawner",
|
"ClientEntitySpawner",
|
||||||
"HandheldGPS",
|
"HandheldGPS",
|
||||||
"CableVisualizer",
|
"CableVisualizer",
|
||||||
"PuddleVisualizer",
|
|
||||||
"UIFragment",
|
"UIFragment",
|
||||||
"PDABorderColor",
|
"PDABorderColor",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How many units per second the drain can absorb from the surrounding puddles.
|
|
||||||
/// Divided by puddles, so if there are 5 puddles this will take 1/5 from each puddle.
|
|
||||||
/// This will stay fixed to 1 second no matter what DrainFrequency is.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("unitsPerSecond")]
|
|
||||||
public float UnitsPerSecond = 6f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How many units are ejected from the buffer per second.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("unitsDestroyedPerSecond")]
|
|
||||||
public float UnitsDestroyedPerSecond = 1f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How many (unobstructed) tiles away the drain will
|
|
||||||
/// drain puddles from.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("range")]
|
|
||||||
public float Range = 2f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How often in seconds the drain checks for puddles around it.
|
|
||||||
/// If the EntityQuery seems a bit unperformant this can be increased.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("drainFrequency")]
|
|
||||||
public float DrainFrequency = 1f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +1,24 @@
|
|||||||
using Content.Server.Fluids.EntitySystems;
|
using Content.Server.Fluids.EntitySystems;
|
||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||||
|
|
||||||
namespace Content.Server.Fluids.Components
|
namespace Content.Server.Fluids.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Added to puddles that contain water so it may evaporate over time.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, Access(typeof(PuddleSystem))]
|
||||||
|
public sealed class EvaporationComponent : Component
|
||||||
{
|
{
|
||||||
[RegisterComponent]
|
/// <summary>
|
||||||
[Access(typeof(EvaporationSystem))]
|
/// The next time we remove the EvaporationSystem reagent amount from this entity.
|
||||||
public sealed class EvaporationComponent : Component
|
/// </summary>
|
||||||
{
|
[ViewVariables(VVAccess.ReadWrite), DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
||||||
/// <summary>
|
public TimeSpan NextTick = TimeSpan.Zero;
|
||||||
/// Is this entity actively evaporating? This toggle lets us pause evaporation under certain conditions.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("evaporationToggle")]
|
|
||||||
public bool EvaporationToggle = true;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time that it will take this puddle to lose one fixed unit of solution, in seconds.
|
/// How much evaporation occurs every tick.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("evaporateTime")]
|
[DataField("evaporationAmount")]
|
||||||
public float EvaporateTime { get; set; } = 5f;
|
public FixedPoint2 EvaporationAmount = FixedPoint2.New(0.3);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Name of referenced solution. Defaults to <see cref="PuddleComponent.DefaultSolutionName"/>
|
|
||||||
/// </summary>
|
|
||||||
[DataField("solution")]
|
|
||||||
public string SolutionName { get; set; } = PuddleComponent.DefaultSolutionName;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Lower limit below which puddle won't evaporate. Useful when wanting to leave a stain.
|
|
||||||
/// Defaults to evaporate completely.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("lowerLimit")]
|
|
||||||
public FixedPoint2 LowerLimit = FixedPoint2.Zero;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Upper limit above which puddle won't evaporate. Useful when wanting to make sure large puddle will
|
|
||||||
/// remain forever. Defaults to 100.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("upperLimit")]
|
|
||||||
public FixedPoint2 UpperLimit = FixedPoint2.New(100); //TODO: Consider setting this back to PuddleComponent.DefaultOverflowVolume once that behaviour is fixed.
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The time accumulated since the start.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("accumulator")]
|
|
||||||
public float Accumulator = 0f;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Content.Server.Fluids.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to track evaporation sparkles so we can delete if necessary.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class EvaporationSparkleComponent : Component
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// At what time will <see cref="FluidSpreaderSystem"/> be checked next
|
|
||||||
/// </summary>
|
|
||||||
[DataField("goalTime", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
|
||||||
public TimeSpan GoalTime;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delay between two runs of <see cref="FluidSpreaderSystem"/>
|
|
||||||
/// </summary>
|
|
||||||
[DataField("delay")]
|
|
||||||
public TimeSpan Delay = TimeSpan.FromSeconds(2);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Puddles to be expanded.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("puddles")] public HashSet<EntityUid> Puddles = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convenience method for setting GoalTime to <paramref name="start"/> + <see cref="Delay"/>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="start">Time to which to add <see cref="Delay"/>, defaults to current <see cref="GoalTime"/></param>
|
|
||||||
public void UpdateGoal(TimeSpan? start = null)
|
|
||||||
{
|
|
||||||
GoalTime = (start ?? GoalTime) + Delay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Content.Server.Fluids.Components;
|
||||||
|
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class FootstepTrackComponent : Component
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using Content.Server.Fluids.EntitySystems;
|
|
||||||
using Content.Shared.FixedPoint;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
|
|
||||||
namespace Content.Server.Fluids.Components
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Puddle on a floor
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent]
|
|
||||||
[Access(typeof(PuddleSystem))]
|
|
||||||
public sealed class PuddleComponent : Component
|
|
||||||
{
|
|
||||||
public const string DefaultSolutionName = "puddle";
|
|
||||||
private static readonly FixedPoint2 DefaultSlipThreshold = FixedPoint2.New(-1); //Not slippery by default. Set specific slipThresholds in YAML if you want your puddles to be slippery. Lower = more slippery, and zero means any volume can slip.
|
|
||||||
public static readonly FixedPoint2 DefaultOverflowVolume = FixedPoint2.New(20);
|
|
||||||
|
|
||||||
// Current design: Something calls the SpillHelper.Spill, that will either
|
|
||||||
// A) Add to an existing puddle at the location (normalised to tile-center) or
|
|
||||||
// B) add a new one
|
|
||||||
// From this every time a puddle is spilt on it will try and overflow to its neighbours if possible,
|
|
||||||
// and also update its appearance based on volume level (opacity) and chemistry color
|
|
||||||
// Small puddles will evaporate after a set delay
|
|
||||||
|
|
||||||
// TODO: 'leaves fluidtracks', probably in a separate component for stuff like gibb chunks?;
|
|
||||||
|
|
||||||
// based on behaviour (e.g. someone being punched vs slashed with a sword would have different blood sprite)
|
|
||||||
// to check for low volumes for evaporation or whatever
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Puddles with volume above this threshold can slip players.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("slipThreshold")]
|
|
||||||
public FixedPoint2 SlipThreshold = DefaultSlipThreshold;
|
|
||||||
|
|
||||||
[DataField("spillSound")]
|
|
||||||
public SoundSpecifier SpillSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
|
|
||||||
|
|
||||||
[DataField("overflowVolume")]
|
|
||||||
public FixedPoint2 OverflowVolume = DefaultOverflowVolume;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How much should this puddle's opacity be multiplied by?
|
|
||||||
/// Useful for puddles that have a high overflow volume but still want to be mostly opaque.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("opacityModifier")] public float OpacityModifier = 1.0f;
|
|
||||||
|
|
||||||
[DataField("solution")] public string SolutionName { get; set; } = DefaultSolutionName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
211
Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs
Normal file
211
Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
using Content.Server.Chemistry.EntitySystems;
|
||||||
|
using Content.Server.Popups;
|
||||||
|
using Content.Shared.Chemistry.Components;
|
||||||
|
using Content.Shared.Chemistry.Reagent;
|
||||||
|
using Content.Shared.FixedPoint;
|
||||||
|
using Content.Shared.Fluids;
|
||||||
|
using Content.Shared.Fluids.Components;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Timing;
|
||||||
|
using Content.Shared.Weapons.Melee;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.Fluids.EntitySystems;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public sealed class AbsorbentSystem : SharedAbsorbentSystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||||
|
[Dependency] private readonly AudioSystem _audio = default!;
|
||||||
|
[Dependency] private readonly PopupSystem _popups = default!;
|
||||||
|
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
||||||
|
[Dependency] private readonly SharedMeleeWeaponSystem _melee = default!;
|
||||||
|
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||||
|
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
|
||||||
|
[Dependency] private readonly UseDelaySystem _useDelay = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
SubscribeLocalEvent<AbsorbentComponent, ComponentInit>(OnAbsorbentInit);
|
||||||
|
SubscribeLocalEvent<AbsorbentComponent, AfterInteractEvent>(OnAfterInteract);
|
||||||
|
SubscribeLocalEvent<AbsorbentComponent, SolutionChangedEvent>(OnAbsorbentSolutionChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAbsorbentInit(EntityUid uid, AbsorbentComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
// TODO: I know dirty on init but no prediction moment.
|
||||||
|
UpdateAbsorbent(uid, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAbsorbentSolutionChange(EntityUid uid, AbsorbentComponent component, SolutionChangedEvent args)
|
||||||
|
{
|
||||||
|
UpdateAbsorbent(uid, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAbsorbent(EntityUid uid, AbsorbentComponent component)
|
||||||
|
{
|
||||||
|
if (!_solutionSystem.TryGetSolution(uid, AbsorbentComponent.SolutionName, out var solution))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var oldProgress = component.Progress.ShallowClone();
|
||||||
|
component.Progress.Clear();
|
||||||
|
|
||||||
|
if (solution.TryGetReagent(PuddleSystem.EvaporationReagent, out var water))
|
||||||
|
{
|
||||||
|
component.Progress[_prototype.Index<ReagentPrototype>(PuddleSystem.EvaporationReagent).SubstanceColor] = water.Float();
|
||||||
|
}
|
||||||
|
|
||||||
|
var otherColor = solution.GetColorWithout(_prototype, PuddleSystem.EvaporationReagent);
|
||||||
|
var other = (solution.Volume - water).Float();
|
||||||
|
|
||||||
|
if (other > 0f)
|
||||||
|
{
|
||||||
|
component.Progress[otherColor] = other;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainder = solution.AvailableVolume;
|
||||||
|
|
||||||
|
if (remainder > FixedPoint2.Zero)
|
||||||
|
{
|
||||||
|
component.Progress[Color.DarkGray] = remainder.Float();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.Progress.Equals(oldProgress))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Dirty(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAfterInteract(EntityUid uid, AbsorbentComponent component, AfterInteractEvent args)
|
||||||
|
{
|
||||||
|
if (!args.CanReach || args.Handled || _useDelay.ActiveDelay(uid))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorberSoln))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Didn't click anything so don't do anything.
|
||||||
|
if (args.Target is not { Valid: true } target)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a puddle try to grab from
|
||||||
|
if (!TryPuddleInteract(args.User, uid, target, component, absorberSoln))
|
||||||
|
{
|
||||||
|
// Do a transfer, try to get water onto us and transfer anything else to them.
|
||||||
|
|
||||||
|
// If it's anything else transfer to
|
||||||
|
if (!TryTransferAbsorber(args.User, uid, target, component, absorberSoln))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempt to fill an absorber from some refillable solution.
|
||||||
|
/// </summary>
|
||||||
|
private bool TryTransferAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln)
|
||||||
|
{
|
||||||
|
if (!TryComp(target, out RefillableSolutionComponent? refillable))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!_solutionSystem.TryGetRefillableSolution(target, out var refillableSolution, refillable: refillable))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (refillableSolution.Volume <= 0)
|
||||||
|
{
|
||||||
|
var msg = Loc.GetString("mopping-system-target-container-empty", ("target", target));
|
||||||
|
_popups.PopupEntity(msg, user, user);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the non-water reagents.
|
||||||
|
// Remove water on target
|
||||||
|
// Then do the transfer.
|
||||||
|
var nonWater = absorberSoln.SplitSolutionWithout(component.PickupAmount, PuddleSystem.EvaporationReagent);
|
||||||
|
|
||||||
|
if (nonWater.Volume == FixedPoint2.Zero && absorberSoln.AvailableVolume == FixedPoint2.Zero)
|
||||||
|
{
|
||||||
|
_popups.PopupEntity(Loc.GetString("mopping-system-puddle-space", ("used", used)), user, user);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var transferAmount = component.PickupAmount < absorberSoln.AvailableVolume ?
|
||||||
|
component.PickupAmount :
|
||||||
|
absorberSoln.AvailableVolume;
|
||||||
|
|
||||||
|
var water = refillableSolution.RemoveReagent(PuddleSystem.EvaporationReagent, transferAmount);
|
||||||
|
|
||||||
|
if (water == FixedPoint2.Zero && nonWater.Volume == FixedPoint2.Zero)
|
||||||
|
{
|
||||||
|
_popups.PopupEntity(Loc.GetString("mopping-system-target-container-empty-water", ("target", target)), user, user);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
absorberSoln.AddReagent(PuddleSystem.EvaporationReagent, water);
|
||||||
|
refillableSolution.AddSolution(nonWater, _prototype);
|
||||||
|
|
||||||
|
_solutionSystem.UpdateChemicals(used, absorberSoln);
|
||||||
|
_solutionSystem.UpdateChemicals(target, refillableSolution);
|
||||||
|
_audio.PlayPvs(component.TransferSound, target);
|
||||||
|
_useDelay.BeginDelay(used);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logic for an absorbing entity interacting with a puddle.
|
||||||
|
/// </summary>
|
||||||
|
private bool TryPuddleInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent absorber, Solution absorberSoln)
|
||||||
|
{
|
||||||
|
if (!TryComp(target, out PuddleComponent? puddle))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) || puddleSolution.Volume <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Check if the puddle has any non-evaporative reagents
|
||||||
|
if (_puddleSystem.CanFullyEvaporate(puddleSolution))
|
||||||
|
{
|
||||||
|
_popups.PopupEntity(Loc.GetString("mopping-system-puddle-evaporate", ("target", target)), user, user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have any evaporative reagents on our absorber to transfer
|
||||||
|
absorberSoln.TryGetReagent(PuddleSystem.EvaporationReagent, out var available);
|
||||||
|
|
||||||
|
// No material
|
||||||
|
if (available == FixedPoint2.Zero)
|
||||||
|
{
|
||||||
|
_popups.PopupEntity(Loc.GetString("mopping-system-no-water", ("used", used)), user, user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var transferMax = absorber.PickupAmount;
|
||||||
|
var transferAmount = available > transferMax ? transferMax : available;
|
||||||
|
|
||||||
|
var split = puddleSolution.SplitSolutionWithout(transferAmount, PuddleSystem.EvaporationReagent);
|
||||||
|
|
||||||
|
absorberSoln.RemoveReagent(PuddleSystem.EvaporationReagent, split.Volume);
|
||||||
|
puddleSolution.AddReagent(PuddleSystem.EvaporationReagent, split.Volume);
|
||||||
|
absorberSoln.AddSolution(split, _prototype);
|
||||||
|
|
||||||
|
_solutionSystem.UpdateChemicals(used, absorberSoln);
|
||||||
|
_solutionSystem.UpdateChemicals(target, puddleSolution);
|
||||||
|
_audio.PlayPvs(absorber.PickupSound, target);
|
||||||
|
_useDelay.BeginDelay(used);
|
||||||
|
|
||||||
|
var userXform = Transform(user);
|
||||||
|
var targetPos = _transform.GetWorldPosition(target);
|
||||||
|
var localPos = _transform.GetInvWorldMatrix(userXform).Transform(targetPos);
|
||||||
|
localPos = userXform.LocalRotation.RotateVec(localPos);
|
||||||
|
|
||||||
|
_melee.DoLunge(user, Angle.Zero, localPos, null, false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using Content.Server.Fluids.Components;
|
|||||||
using Content.Server.Chemistry.EntitySystems;
|
using Content.Server.Chemistry.EntitySystems;
|
||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
using Content.Shared.Audio;
|
using Content.Shared.Audio;
|
||||||
|
using Content.Shared.Fluids.Components;
|
||||||
using Robust.Shared.Collections;
|
using Robust.Shared.Collections;
|
||||||
|
|
||||||
namespace Content.Server.Fluids.EntitySystems
|
namespace Content.Server.Fluids.EntitySystems
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
using Content.Server.Chemistry.EntitySystems;
|
|
||||||
using Content.Server.Fluids.Components;
|
|
||||||
using Content.Shared.FixedPoint;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
|
|
||||||
namespace Content.Server.Fluids.EntitySystems
|
|
||||||
{
|
|
||||||
[UsedImplicitly]
|
|
||||||
public sealed class EvaporationSystem : EntitySystem
|
|
||||||
{
|
|
||||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
|
||||||
{
|
|
||||||
base.Update(frameTime);
|
|
||||||
foreach (var evaporationComponent in EntityManager.EntityQuery<EvaporationComponent>())
|
|
||||||
{
|
|
||||||
var uid = evaporationComponent.Owner;
|
|
||||||
evaporationComponent.Accumulator += frameTime;
|
|
||||||
|
|
||||||
if (!_solutionContainerSystem.TryGetSolution(uid, evaporationComponent.SolutionName, out var solution))
|
|
||||||
{
|
|
||||||
// If no solution, delete the entity
|
|
||||||
EntityManager.QueueDeleteEntity(uid);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evaporationComponent.Accumulator < evaporationComponent.EvaporateTime)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
evaporationComponent.Accumulator -= evaporationComponent.EvaporateTime;
|
|
||||||
|
|
||||||
if (evaporationComponent.EvaporationToggle)
|
|
||||||
{
|
|
||||||
_solutionContainerSystem.SplitSolution(uid, solution,
|
|
||||||
FixedPoint2.Min(FixedPoint2.New(1), solution.Volume)); // removes 1 unit, or solution current volume, whichever is lower.
|
|
||||||
}
|
|
||||||
|
|
||||||
evaporationComponent.EvaporationToggle =
|
|
||||||
solution.Volume > evaporationComponent.LowerLimit
|
|
||||||
&& solution.Volume < evaporationComponent.UpperLimit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Copy constructor to copy initial fields from source to destination.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="destUid">Entity to which we copy <paramref name="srcEvaporation"/> properties</param>
|
|
||||||
/// <param name="srcEvaporation">Component that contains relevant properties</param>
|
|
||||||
public void CopyConstruct(EntityUid destUid, EvaporationComponent srcEvaporation)
|
|
||||||
{
|
|
||||||
var destEvaporation = EntityManager.EnsureComponent<EvaporationComponent>(destUid);
|
|
||||||
destEvaporation.EvaporateTime = srcEvaporation.EvaporateTime;
|
|
||||||
destEvaporation.EvaporationToggle = srcEvaporation.EvaporationToggle;
|
|
||||||
destEvaporation.SolutionName = srcEvaporation.SolutionName;
|
|
||||||
destEvaporation.LowerLimit = srcEvaporation.LowerLimit;
|
|
||||||
destEvaporation.UpperLimit = srcEvaporation.UpperLimit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
|
||||||
using Content.Server.Fluids.Components;
|
|
||||||
using Content.Shared;
|
|
||||||
using Content.Shared.Directions;
|
|
||||||
using Content.Shared.Maps;
|
|
||||||
using Content.Shared.Physics;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Map.Components;
|
|
||||||
using Robust.Shared.Physics;
|
|
||||||
using Robust.Shared.Physics.Systems;
|
|
||||||
using Robust.Shared.Timing;
|
|
||||||
|
|
||||||
namespace Content.Server.Fluids.EntitySystems;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Component that governs overflowing puddles. Controls how Puddles spread and updat
|
|
||||||
/// </summary>
|
|
||||||
[UsedImplicitly]
|
|
||||||
public sealed class FluidSpreaderSystem : EntitySystem
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
|
||||||
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
|
||||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
|
||||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds an overflow component to the map data component tracking overflowing puddles
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="puddleUid">EntityUid of overflowing puddle</param>
|
|
||||||
/// <param name="puddle">Optional PuddleComponent</param>
|
|
||||||
/// <param name="xform">Optional TransformComponent</param>
|
|
||||||
public void AddOverflowingPuddle(EntityUid puddleUid, PuddleComponent? puddle = null,
|
|
||||||
TransformComponent? xform = null)
|
|
||||||
{
|
|
||||||
if (!Resolve(puddleUid, ref puddle, ref xform, false) || xform.MapUid == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var mapId = xform.MapUid.Value;
|
|
||||||
|
|
||||||
EntityManager.EnsureComponent<FluidMapDataComponent>(mapId, out var component);
|
|
||||||
component.Puddles.Add(puddleUid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
|
||||||
{
|
|
||||||
base.Update(frameTime);
|
|
||||||
Span<Direction> exploreDirections = stackalloc Direction[]
|
|
||||||
{
|
|
||||||
Direction.North,
|
|
||||||
Direction.East,
|
|
||||||
Direction.South,
|
|
||||||
Direction.West,
|
|
||||||
};
|
|
||||||
var puddles = new List<PuddleComponent>(4);
|
|
||||||
var puddleQuery = GetEntityQuery<PuddleComponent>();
|
|
||||||
var xFormQuery = GetEntityQuery<TransformComponent>();
|
|
||||||
|
|
||||||
foreach (var fluidMapData in EntityQuery<FluidMapDataComponent>())
|
|
||||||
{
|
|
||||||
if (fluidMapData.Puddles.Count == 0 || _gameTiming.CurTime <= fluidMapData.GoalTime)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var newIteration = new HashSet<EntityUid>();
|
|
||||||
foreach (var puddleUid in fluidMapData.Puddles)
|
|
||||||
{
|
|
||||||
if (!puddleQuery.TryGetComponent(puddleUid, out var puddle)
|
|
||||||
|| !xFormQuery.TryGetComponent(puddleUid, out var transform)
|
|
||||||
|| !_mapManager.TryGetGrid(transform.GridUid, out var mapGrid))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
puddles.Clear();
|
|
||||||
var pos = transform.Coordinates;
|
|
||||||
|
|
||||||
var totalVolume = _puddleSystem.CurrentVolume(puddleUid, puddle);
|
|
||||||
exploreDirections.Shuffle();
|
|
||||||
foreach (var direction in exploreDirections)
|
|
||||||
{
|
|
||||||
var newPos = pos.Offset(direction);
|
|
||||||
if (CheckTile(puddleUid, puddle, newPos, mapGrid, puddleQuery, out var uid, out var component))
|
|
||||||
{
|
|
||||||
puddles.Add(component);
|
|
||||||
totalVolume += _puddleSystem.CurrentVolume(uid.Value, component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_puddleSystem.EqualizePuddles(puddleUid, puddles, totalVolume, newIteration, puddle);
|
|
||||||
}
|
|
||||||
|
|
||||||
fluidMapData.Puddles.Clear();
|
|
||||||
fluidMapData.Puddles.UnionWith(newIteration);
|
|
||||||
fluidMapData.UpdateGoal(_gameTiming.CurTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check a tile is valid for solution allocation.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="srcUid">Entity Uid of original puddle</param>
|
|
||||||
/// <param name="srcPuddle">PuddleComponent attached to srcUid</param>
|
|
||||||
/// <param name="dstPos">at which to check tile</param>
|
|
||||||
/// <param name="mapGrid">helper param needed to extract entities</param>
|
|
||||||
/// <param name="newPuddleUid">either found or newly created PuddleComponent.</param>
|
|
||||||
/// <returns>true if tile is empty or occupied by a non-overflowing puddle (or a puddle close to being overflowing)</returns>
|
|
||||||
private bool CheckTile(EntityUid srcUid, PuddleComponent srcPuddle, EntityCoordinates dstPos,
|
|
||||||
MapGridComponent mapGrid, EntityQuery<PuddleComponent> puddleQuery,
|
|
||||||
[NotNullWhen(true)] out EntityUid? newPuddleUid, [NotNullWhen(true)] out PuddleComponent? newPuddleComp)
|
|
||||||
{
|
|
||||||
if (!mapGrid.TryGetTileRef(dstPos, out var tileRef)
|
|
||||||
|| tileRef.Tile.IsEmpty)
|
|
||||||
{
|
|
||||||
newPuddleUid = null;
|
|
||||||
newPuddleComp = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if puddle can spread there at all
|
|
||||||
var dstMap = dstPos.ToMap(EntityManager, _transform);
|
|
||||||
var dst = dstMap.Position;
|
|
||||||
var src = Transform(srcUid).MapPosition.Position;
|
|
||||||
var dir = src - dst;
|
|
||||||
var ray = new CollisionRay(dst, dir.Normalized, (int) (CollisionGroup.Impassable | CollisionGroup.HighImpassable));
|
|
||||||
var mapId = dstMap.MapId;
|
|
||||||
var results = _physics.IntersectRay(mapId, ray, dir.Length, returnOnFirstHit: true);
|
|
||||||
if (results.Any())
|
|
||||||
{
|
|
||||||
newPuddleUid = null;
|
|
||||||
newPuddleComp = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var puddleCurrentVolume = _puddleSystem.CurrentVolume(srcUid, srcPuddle);
|
|
||||||
foreach (var entity in dstPos.GetEntitiesInTile())
|
|
||||||
{
|
|
||||||
if (puddleQuery.TryGetComponent(entity, out var existingPuddle))
|
|
||||||
{
|
|
||||||
if (_puddleSystem.CurrentVolume(entity, existingPuddle) >= puddleCurrentVolume)
|
|
||||||
{
|
|
||||||
newPuddleUid = null;
|
|
||||||
newPuddleComp = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
newPuddleUid = entity;
|
|
||||||
newPuddleComp = existingPuddle;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_puddleSystem.SpawnPuddle(srcUid, dstPos, srcPuddle, out var uid, out var comp);
|
|
||||||
newPuddleUid = uid;
|
|
||||||
newPuddleComp = comp;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
using Content.Server.Chemistry.Components.SolutionManager;
|
|
||||||
using Content.Server.Chemistry.EntitySystems;
|
|
||||||
using Content.Server.Fluids.Components;
|
|
||||||
using Content.Server.Popups;
|
|
||||||
using Content.Shared.Chemistry.Components;
|
|
||||||
using Content.Shared.DoAfter;
|
|
||||||
using Content.Shared.FixedPoint;
|
|
||||||
using Content.Shared.Fluids;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Tag;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Robust.Server.GameObjects;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
|
|
||||||
namespace Content.Server.Fluids.EntitySystems;
|
|
||||||
|
|
||||||
[UsedImplicitly]
|
|
||||||
public sealed class MoppingSystem : SharedMoppingSystem
|
|
||||||
{
|
|
||||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
|
||||||
[Dependency] private readonly SpillableSystem _spillableSystem = default!;
|
|
||||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
|
||||||
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
|
|
||||||
[Dependency] private readonly PopupSystem _popups = default!;
|
|
||||||
[Dependency] private readonly AudioSystem _audio = default!;
|
|
||||||
|
|
||||||
const string PuddlePrototypeId = "PuddleSmear"; // The puddle prototype to use when releasing liquid to the floor, making a new puddle
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
SubscribeLocalEvent<AbsorbentComponent, ComponentInit>(OnAbsorbentInit);
|
|
||||||
SubscribeLocalEvent<AbsorbentComponent, AfterInteractEvent>(OnAfterInteract);
|
|
||||||
SubscribeLocalEvent<AbsorbentComponent, AbsorbantDoAfterEvent>(OnDoAfter);
|
|
||||||
SubscribeLocalEvent<AbsorbentComponent, SolutionChangedEvent>(OnAbsorbentSolutionChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAbsorbentInit(EntityUid uid, AbsorbentComponent component, ComponentInit args)
|
|
||||||
{
|
|
||||||
// TODO: I know dirty on init but no prediction moment.
|
|
||||||
UpdateAbsorbent(uid, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAbsorbentSolutionChange(EntityUid uid, AbsorbentComponent component, SolutionChangedEvent args)
|
|
||||||
{
|
|
||||||
UpdateAbsorbent(uid, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAbsorbent(EntityUid uid, AbsorbentComponent component)
|
|
||||||
{
|
|
||||||
if (!_solutionSystem.TryGetSolution(uid, AbsorbentComponent.SolutionName, out var solution))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var oldProgress = component.Progress;
|
|
||||||
|
|
||||||
component.Progress = (float) (solution.Volume / solution.MaxVolume);
|
|
||||||
if (component.Progress.Equals(oldProgress))
|
|
||||||
return;
|
|
||||||
Dirty(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAfterInteract(EntityUid uid, AbsorbentComponent component, AfterInteractEvent args)
|
|
||||||
{
|
|
||||||
if (!args.CanReach || args.Handled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorberSoln))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (args.Target is not { Valid: true } target)
|
|
||||||
{
|
|
||||||
// Add liquid to an empty floor tile
|
|
||||||
args.Handled = TryCreatePuddle(args.User, args.ClickLocation, component, absorberSoln);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
args.Handled = TryPuddleInteract(args.User, uid, target, component, absorberSoln)
|
|
||||||
|| TryEmptyAbsorber(args.User, uid, target, component, absorberSoln)
|
|
||||||
|| TryFillAbsorber(args.User, uid, target, component, absorberSoln);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to create a puddle using solutions stored in the absorber entity.
|
|
||||||
/// </summary>
|
|
||||||
private bool TryCreatePuddle(EntityUid user, EntityCoordinates clickLocation, AbsorbentComponent absorbent, Solution absorberSoln)
|
|
||||||
{
|
|
||||||
if (absorberSoln.Volume <= 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!_mapManager.TryGetGrid(clickLocation.GetGridUid(EntityManager), out var mapGrid))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var releaseAmount = FixedPoint2.Min(absorbent.ResidueAmount, absorberSoln.Volume);
|
|
||||||
var releasedSolution = _solutionSystem.SplitSolution(absorbent.Owner, absorberSoln, releaseAmount);
|
|
||||||
_spillableSystem.SpillAt(mapGrid.GetTileRef(clickLocation), releasedSolution, PuddlePrototypeId);
|
|
||||||
_popups.PopupEntity(Loc.GetString("mopping-system-release-to-floor"), user, user);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempt to fill an absorber from some drainable solution.
|
|
||||||
/// </summary>
|
|
||||||
private bool TryFillAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln)
|
|
||||||
{
|
|
||||||
if (absorberSoln.AvailableVolume <= 0 || !TryComp(target, out DrainableSolutionComponent? drainable))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!_solutionSystem.TryGetDrainableSolution(target, out var drainableSolution))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (drainableSolution.Volume <= 0)
|
|
||||||
{
|
|
||||||
var msg = Loc.GetString("mopping-system-target-container-empty", ("target", target));
|
|
||||||
_popups.PopupEntity(msg, user, user);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's transfer up to to half the tool's available capacity to the tool.
|
|
||||||
var quantity = FixedPoint2.Max(component.PickupAmount, absorberSoln.AvailableVolume / 2);
|
|
||||||
quantity = FixedPoint2.Min(quantity, drainableSolution.Volume);
|
|
||||||
|
|
||||||
DoMopInteraction(user, used, target, component, drainable.Solution, quantity, 1, "mopping-system-drainable-success", component.TransferSound);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Empty an absorber into a refillable solution.
|
|
||||||
/// </summary>
|
|
||||||
private bool TryEmptyAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln)
|
|
||||||
{
|
|
||||||
if (absorberSoln.Volume <= 0 || !TryComp(target, out RefillableSolutionComponent? refillable))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!_solutionSystem.TryGetRefillableSolution(target, out var targetSolution))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
string msg;
|
|
||||||
if (targetSolution.AvailableVolume <= 0)
|
|
||||||
{
|
|
||||||
msg = Loc.GetString("mopping-system-target-container-full", ("target", target));
|
|
||||||
_popups.PopupEntity(msg, user, user);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the target container is too small (e.g. syringe)
|
|
||||||
// TODO this should really be a tag or something, not a capacity check.
|
|
||||||
if (targetSolution.MaxVolume <= FixedPoint2.New(20))
|
|
||||||
{
|
|
||||||
msg = Loc.GetString("mopping-system-target-container-too-small", ("target", target));
|
|
||||||
_popups.PopupEntity(msg, user, user);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
float delay;
|
|
||||||
FixedPoint2 quantity = absorberSoln.Volume;
|
|
||||||
|
|
||||||
// TODO this really needs cleaning up. Less magic numbers, more data-fields.
|
|
||||||
|
|
||||||
if (_tagSystem.HasTag(used, "Mop") // if the tool used is a literal mop (and not a sponge, rag, etc.)
|
|
||||||
&& !_tagSystem.HasTag(target, "Wringer")) // and if the target does not have a wringer for properly drying the mop
|
|
||||||
{
|
|
||||||
delay = 5.0f; // Should take much longer if you don't have a wringer
|
|
||||||
|
|
||||||
var frac = quantity / absorberSoln.MaxVolume;
|
|
||||||
|
|
||||||
// squeeze up to 60% of the solution from the mop if the mop is more than one-quarter full
|
|
||||||
if (frac > 0.25)
|
|
||||||
quantity *= 0.6;
|
|
||||||
|
|
||||||
if (frac > 0.5)
|
|
||||||
msg = "mopping-system-hand-squeeze-still-wet";
|
|
||||||
else if (frac > 0.5)
|
|
||||||
msg = "mopping-system-hand-squeeze-little-wet";
|
|
||||||
else
|
|
||||||
msg = "mopping-system-hand-squeeze-dry";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
msg = "mopping-system-refillable-success";
|
|
||||||
delay = 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// negative quantity as we are removing solutions from the mop
|
|
||||||
quantity = -FixedPoint2.Min(targetSolution.AvailableVolume, quantity);
|
|
||||||
|
|
||||||
DoMopInteraction(user, used, target, component, refillable.Solution, quantity, delay, msg, component.TransferSound);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Logic for an absorbing entity interacting with a puddle.
|
|
||||||
/// </summary>
|
|
||||||
private bool TryPuddleInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent absorber, Solution absorberSoln)
|
|
||||||
{
|
|
||||||
if (!TryComp(target, out PuddleComponent? puddle))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) || puddleSolution.Volume <= 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
FixedPoint2 quantity;
|
|
||||||
|
|
||||||
// Get lower limit for mopping
|
|
||||||
FixedPoint2 lowerLimit = FixedPoint2.Zero;
|
|
||||||
if (TryComp(target, out EvaporationComponent? evaporation)
|
|
||||||
&& evaporation.EvaporationToggle
|
|
||||||
&& evaporation.LowerLimit == 0)
|
|
||||||
{
|
|
||||||
lowerLimit = absorber.LowerLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can our absorber even absorb any liquid?
|
|
||||||
if (puddleSolution.Volume <= lowerLimit)
|
|
||||||
{
|
|
||||||
// Cannot absorb any more liquid. So clearly the user wants to add liquid to the puddle... right?
|
|
||||||
// This is the old behavior and I CBF fixing this, for the record I don't like this.
|
|
||||||
|
|
||||||
quantity = FixedPoint2.Min(absorber.ResidueAmount, absorberSoln.Volume);
|
|
||||||
if (quantity <= 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Dilutes the puddle with some solution from the tool
|
|
||||||
_solutionSystem.TryTransferSolution(used, target, absorberSoln, puddleSolution, quantity);
|
|
||||||
_audio.PlayPvs(absorber.TransferSound, used);
|
|
||||||
_popups.PopupEntity(Loc.GetString("mopping-system-puddle-diluted"), user);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (absorberSoln.AvailableVolume < 0)
|
|
||||||
{
|
|
||||||
_popups.PopupEntity(Loc.GetString("mopping-system-tool-full", ("used", used)), user, user);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
quantity = FixedPoint2.Min(absorber.PickupAmount, puddleSolution.Volume - lowerLimit, absorberSoln.AvailableVolume);
|
|
||||||
if (quantity <= 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var delay = absorber.PickupAmount.Float() / absorber.Speed;
|
|
||||||
DoMopInteraction(user, used, target, absorber, puddle.SolutionName, quantity, delay, "mopping-system-puddle-success", absorber.PickupSound);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DoMopInteraction(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, string targetSolution,
|
|
||||||
FixedPoint2 transferAmount, float delay, string msg, SoundSpecifier sfx)
|
|
||||||
{
|
|
||||||
// Can't interact with too many entities at once.
|
|
||||||
if (component.MaxInteractingEntities < component.InteractingEntities.Count + 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Can't interact with the same container multiple times at once.
|
|
||||||
if (!component.InteractingEntities.Add(target))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var ev = new AbsorbantDoAfterEvent(targetSolution, msg, sfx, transferAmount);
|
|
||||||
|
|
||||||
var doAfterArgs = new DoAfterArgs(user, delay, ev, used, target: target, used: used)
|
|
||||||
{
|
|
||||||
BreakOnUserMove = true,
|
|
||||||
BreakOnDamage = true,
|
|
||||||
MovementThreshold = 0.2f
|
|
||||||
};
|
|
||||||
|
|
||||||
_doAfterSystem.TryStartDoAfter(doAfterArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDoAfter(EntityUid uid, AbsorbentComponent component, AbsorbantDoAfterEvent args)
|
|
||||||
{
|
|
||||||
if (args.Target == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
component.InteractingEntities.Remove(args.Target.Value);
|
|
||||||
|
|
||||||
if (args.Cancelled || args.Handled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_audio.PlayPvs(args.Sound, uid);
|
|
||||||
_popups.PopupEntity(Loc.GetString(args.Message, ("target", args.Target.Value), ("used", uid)), uid);
|
|
||||||
_solutionSystem.TryTransferSolution(args.Target.Value, uid, args.TargetSolution,
|
|
||||||
AbsorbentComponent.SolutionName, args.TransferAmount);
|
|
||||||
component.InteractingEntities.Remove(args.Target.Value);
|
|
||||||
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Content.Server.Fluids.Components;
|
using Content.Server.Fluids.Components;
|
||||||
using Content.Shared.Fluids;
|
using Content.Shared.Fluids;
|
||||||
|
using Content.Shared.Fluids.Components;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using Content.Server.Fluids.Components;
|
||||||
|
using Content.Shared.Chemistry.Components;
|
||||||
|
using Content.Shared.FixedPoint;
|
||||||
|
using Content.Shared.Fluids.Components;
|
||||||
|
|
||||||
|
namespace Content.Server.Fluids.EntitySystems;
|
||||||
|
|
||||||
|
public sealed partial class PuddleSystem
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan EvaporationCooldown = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
|
public const string EvaporationReagent = "Water";
|
||||||
|
|
||||||
|
private void OnEvaporationMapInit(EntityUid uid, EvaporationComponent component, MapInitEvent args)
|
||||||
|
{
|
||||||
|
component.NextTick = _timing.CurTime + EvaporationCooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateEvaporation(EntityUid uid, Solution solution)
|
||||||
|
{
|
||||||
|
if (HasComp<EvaporationComponent>(uid))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (solution.ContainsReagent(EvaporationReagent))
|
||||||
|
{
|
||||||
|
var evaporation = AddComp<EvaporationComponent>(uid);
|
||||||
|
evaporation.NextTick = _timing.CurTime + EvaporationCooldown;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemComp<EvaporationComponent>(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TickEvaporation()
|
||||||
|
{
|
||||||
|
var query = EntityQueryEnumerator<EvaporationComponent, PuddleComponent>();
|
||||||
|
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||||
|
var curTime = _timing.CurTime;
|
||||||
|
while (query.MoveNext(out var uid, out var evaporation, out var puddle))
|
||||||
|
{
|
||||||
|
if (evaporation.NextTick > curTime)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
evaporation.NextTick += EvaporationCooldown;
|
||||||
|
|
||||||
|
if (!_solutionContainerSystem.TryGetSolution(uid, puddle.SolutionName, out var puddleSolution))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var reagentTick = evaporation.EvaporationAmount * EvaporationCooldown.TotalSeconds;
|
||||||
|
puddleSolution.RemoveReagent(EvaporationReagent, reagentTick);
|
||||||
|
|
||||||
|
// Despawn if we're done
|
||||||
|
if (puddleSolution.Volume == FixedPoint2.Zero)
|
||||||
|
{
|
||||||
|
// Spawn a *sparkle*
|
||||||
|
Spawn("PuddleSparkle", xformQuery.GetComponent(uid).Coordinates);
|
||||||
|
QueueDel(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanFullyEvaporate(Solution solution)
|
||||||
|
{
|
||||||
|
return solution.Contents.Count == 1 && solution.ContainsReagent(EvaporationReagent);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs
Normal file
139
Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
using Content.Server.Chemistry.EntitySystems;
|
||||||
|
using Content.Server.Fluids.Components;
|
||||||
|
using Content.Server.Nutrition.Components;
|
||||||
|
using Content.Shared.Clothing.Components;
|
||||||
|
using Content.Shared.Database;
|
||||||
|
using Content.Shared.DoAfter;
|
||||||
|
using Content.Shared.FixedPoint;
|
||||||
|
using Content.Shared.Inventory.Events;
|
||||||
|
using Content.Shared.Spillable;
|
||||||
|
using Content.Shared.Throwing;
|
||||||
|
using Content.Shared.Verbs;
|
||||||
|
|
||||||
|
namespace Content.Server.Fluids.EntitySystems;
|
||||||
|
|
||||||
|
public sealed partial class PuddleSystem
|
||||||
|
{
|
||||||
|
private void InitializeSpillable()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand);
|
||||||
|
SubscribeLocalEvent<SpillableComponent, GetVerbsEvent<Verb>>(AddSpillVerb);
|
||||||
|
SubscribeLocalEvent<SpillableComponent, GotEquippedEvent>(OnGotEquipped);
|
||||||
|
SubscribeLocalEvent<SpillableComponent, SolutionSpikeOverflowEvent>(OnSpikeOverflow);
|
||||||
|
SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSpikeOverflow(EntityUid uid, SpillableComponent component, SolutionSpikeOverflowEvent args)
|
||||||
|
{
|
||||||
|
if (!args.Handled)
|
||||||
|
{
|
||||||
|
TrySpillAt(Transform(uid).Coordinates, args.Overflow, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGotEquipped(EntityUid uid, SpillableComponent component, GotEquippedEvent args)
|
||||||
|
{
|
||||||
|
if (!component.SpillWorn)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!TryComp(uid, out ClothingComponent? clothing))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// check if entity was actually used as clothing
|
||||||
|
// not just taken in pockets or something
|
||||||
|
var isCorrectSlot = clothing.Slots.HasFlag(args.SlotFlags);
|
||||||
|
if (!isCorrectSlot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (solution.Volume == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// spill all solution on the player
|
||||||
|
var drainedSolution = _solutionContainerSystem.Drain(uid, solution, solution.Volume);
|
||||||
|
TrySpillAt(args.Equipee, drainedSolution, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpillOnLand(EntityUid uid, SpillableComponent component, ref LandEvent args)
|
||||||
|
{
|
||||||
|
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (TryComp<DrinkComponent>(uid, out var drink) && !drink.Opened)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.User != null)
|
||||||
|
{
|
||||||
|
_adminLogger.Add(LogType.Landed,
|
||||||
|
$"{ToPrettyString(uid):entity} spilled a solution {SolutionContainerSystem.ToPrettyString(solution):solution} on landing");
|
||||||
|
}
|
||||||
|
|
||||||
|
var drainedSolution = _solutionContainerSystem.Drain(uid, solution, solution.Volume);
|
||||||
|
TrySplashSpillAt(uid, Transform(uid).Coordinates, drainedSolution, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddSpillVerb(EntityUid uid, SpillableComponent component, GetVerbsEvent<Verb> args)
|
||||||
|
{
|
||||||
|
if (!args.CanAccess || !args.CanInteract)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_solutionContainerSystem.TryGetSolution(args.Target, component.SolutionName, out var solution))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (TryComp<DrinkComponent>(args.Target, out var drink) && (!drink.Opened))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (solution.Volume == FixedPoint2.Zero)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Verb verb = new()
|
||||||
|
{
|
||||||
|
Text = Loc.GetString("spill-target-verb-get-data-text")
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO VERB ICONS spill icon? pouring out a glass/beaker?
|
||||||
|
if (component.SpillDelay == null)
|
||||||
|
{
|
||||||
|
verb.Act = () =>
|
||||||
|
{
|
||||||
|
var puddleSolution = _solutionContainerSystem.SplitSolution(args.Target,
|
||||||
|
solution, solution.Volume);
|
||||||
|
TrySpillAt(Transform(args.Target).Coordinates, puddleSolution, out _);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
verb.Act = () =>
|
||||||
|
{
|
||||||
|
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(args.User, component.SpillDelay ?? 0, new SpillDoAfterEvent(), uid, target: uid)
|
||||||
|
{
|
||||||
|
BreakOnTargetMove = true,
|
||||||
|
BreakOnUserMove = true,
|
||||||
|
BreakOnDamage = true,
|
||||||
|
NeedHand = true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
verb.Impact = LogImpact.Medium; // dangerous reagent reaction are logged separately.
|
||||||
|
verb.DoContactInteraction = true;
|
||||||
|
args.Verbs.Add(verb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDoAfter(EntityUid uid, SpillableComponent component, DoAfterEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled || args.Cancelled || args.Args.Target == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//solution gone by other means before doafter completes
|
||||||
|
if (!_solutionContainerSystem.TryGetDrainableSolution(uid, out var solution) || solution.Volume == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var puddleSolution = _solutionContainerSystem.SplitSolution(uid, solution, solution.Volume);
|
||||||
|
TrySpillAt(Transform(uid).Coordinates, puddleSolution, out _);
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using Content.Shared.Chemistry.Components;
|
||||||
|
using Content.Shared.DragDrop;
|
||||||
|
using Content.Shared.FixedPoint;
|
||||||
|
using Content.Shared.Fluids;
|
||||||
|
using Content.Shared.Fluids.Components;
|
||||||
|
|
||||||
|
namespace Content.Server.Fluids.EntitySystems;
|
||||||
|
|
||||||
|
public sealed partial class PuddleSystem
|
||||||
|
{
|
||||||
|
private void InitializeTransfers()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<RefillableSolutionComponent, DragDropDraggedEvent>(OnRefillableDragged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRefillableDragged(EntityUid uid, RefillableSolutionComponent component, ref DragDropDraggedEvent args)
|
||||||
|
{
|
||||||
|
_solutionContainerSystem.TryGetSolution(uid, component.Solution, out var solution);
|
||||||
|
|
||||||
|
if (solution?.Volume == FixedPoint2.Zero)
|
||||||
|
{
|
||||||
|
_popups.PopupEntity(Loc.GetString("mopping-system-empty", ("used", uid)), uid, args.User);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryComp<DrainableSolutionComponent>(args.Target, out var drainable);
|
||||||
|
|
||||||
|
_solutionContainerSystem.TryGetDrainableSolution(args.Target, out var drainableSolution, drainable);
|
||||||
|
|
||||||
|
// Dump reagents into drain
|
||||||
|
if (TryComp<DrainComponent>(args.Target, out var drain) && drainable != null)
|
||||||
|
{
|
||||||
|
if (drainableSolution == null || solution == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var split = _solutionContainerSystem.SplitSolution(uid, solution, drainableSolution.AvailableVolume);
|
||||||
|
|
||||||
|
// TODO: Drane refactor
|
||||||
|
if (_solutionContainerSystem.TryAddSolution(args.Target, drainableSolution, split))
|
||||||
|
{
|
||||||
|
_audio.PlayPvs(AbsorbentComponent.DefaultTransferSound, args.Target);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", args.Target)), args.Target, args.User);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take reagents from target
|
||||||
|
if (drainable != null)
|
||||||
|
{
|
||||||
|
if (drainableSolution == null || solution == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var split = _solutionContainerSystem.SplitSolution(args.Target, drainableSolution, solution.AvailableVolume);
|
||||||
|
|
||||||
|
if (_solutionContainerSystem.TryAddSolution(uid, solution, split))
|
||||||
|
{
|
||||||
|
_audio.PlayPvs(AbsorbentComponent.DefaultTransferSound, uid);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", uid)), uid, args.User);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,313 +1,639 @@
|
|||||||
|
using Content.Server.Administration.Logs;
|
||||||
using Content.Server.Chemistry.EntitySystems;
|
using Content.Server.Chemistry.EntitySystems;
|
||||||
|
using Content.Server.DoAfter;
|
||||||
using Content.Server.Fluids.Components;
|
using Content.Server.Fluids.Components;
|
||||||
using Content.Shared.Chemistry;
|
using Content.Shared.Chemistry;
|
||||||
using Content.Shared.Chemistry.Reaction;
|
using Content.Shared.Chemistry.Reaction;
|
||||||
|
using Content.Server.Spreader;
|
||||||
using Content.Shared.Chemistry.Reagent;
|
using Content.Shared.Chemistry.Reagent;
|
||||||
|
using Content.Shared.Database;
|
||||||
using Content.Shared.Examine;
|
using Content.Shared.Examine;
|
||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
using Content.Shared.Fluids;
|
using Content.Shared.Fluids;
|
||||||
using Content.Shared.Popups;
|
using Content.Shared.Popups;
|
||||||
using Content.Shared.Slippery;
|
using Content.Shared.Slippery;
|
||||||
|
using Content.Shared.Fluids.Components;
|
||||||
using Content.Shared.StepTrigger.Components;
|
using Content.Shared.StepTrigger.Components;
|
||||||
using Content.Shared.StepTrigger.Systems;
|
using Content.Shared.StepTrigger.Systems;
|
||||||
using JetBrains.Annotations;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Map.Components;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Solution = Content.Shared.Chemistry.Components.Solution;
|
using Solution = Content.Shared.Chemistry.Components.Solution;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Server.Fluids.EntitySystems
|
namespace Content.Server.Fluids.EntitySystems;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles solutions on floors. Also handles the spreader logic for where the solution overflows a specified volume.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||||
{
|
{
|
||||||
[UsedImplicitly]
|
[Dependency] private readonly IAdminLogManager _adminLogger= default!;
|
||||||
public sealed class PuddleSystem : EntitySystem
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
[Dependency] private readonly AudioSystem _audio = default!;
|
||||||
|
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||||
|
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||||
|
[Dependency] private readonly ReactiveSystem _reactive = default!;
|
||||||
|
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||||
|
[Dependency] private readonly SharedPopupSystem _popups = default!;
|
||||||
|
[Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
|
||||||
|
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||||
|
|
||||||
|
public static float PuddleVolume = 1000;
|
||||||
|
|
||||||
|
// Using local deletion queue instead of the standard queue so that we can easily "undelete" if a puddle
|
||||||
|
// loses & then gains reagents in a single tick.
|
||||||
|
private HashSet<EntityUid> _deletionQueue = new();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Need some sort of way to do blood slash / vomit solution spill on its own
|
||||||
|
* This would then evaporate into the puddle tile below
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
base.Initialize();
|
||||||
[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!;
|
|
||||||
|
|
||||||
public static float PuddleVolume = 1000;
|
// Shouldn't need re-anchoring.
|
||||||
|
SubscribeLocalEvent<PuddleComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||||
|
SubscribeLocalEvent<PuddleComponent, ExaminedEvent>(HandlePuddleExamined);
|
||||||
|
SubscribeLocalEvent<PuddleComponent, SolutionChangedEvent>(OnSolutionUpdate);
|
||||||
|
SubscribeLocalEvent<PuddleComponent, ComponentInit>(OnPuddleInit);
|
||||||
|
SubscribeLocalEvent<PuddleComponent, SpreadNeighborsEvent>(OnPuddleSpread);
|
||||||
|
SubscribeLocalEvent<PuddleComponent, SlipEvent>(OnPuddleSlip);
|
||||||
|
|
||||||
// Using local deletion queue instead of the standard queue so that we can easily "undelete" if a puddle
|
SubscribeLocalEvent<EvaporationComponent, MapInitEvent>(OnEvaporationMapInit);
|
||||||
// loses & then gains reagents in a single tick.
|
|
||||||
private HashSet<EntityUid> _deletionQueue = new();
|
|
||||||
|
|
||||||
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();
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
|
return;
|
||||||
// Shouldn't need re-anchoring.
|
|
||||||
SubscribeLocalEvent<PuddleComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
|
||||||
SubscribeLocalEvent<PuddleComponent, ExaminedEvent>(HandlePuddleExamined);
|
|
||||||
SubscribeLocalEvent<PuddleComponent, SolutionChangedEvent>(OnSolutionUpdate);
|
|
||||||
SubscribeLocalEvent<PuddleComponent, ComponentInit>(OnPuddleInit);
|
|
||||||
SubscribeLocalEvent<PuddleComponent, SlipEvent>(OnPuddleSlip);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
var xform = Transform(uid);
|
||||||
|
|
||||||
|
if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
|
||||||
{
|
{
|
||||||
base.Update(frameTime);
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
foreach (var ent in _deletionQueue)
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var puddleQuery = GetEntityQuery<PuddleComponent>();
|
||||||
|
|
||||||
|
// First we overflow to neighbors with overflow capacity
|
||||||
|
// Then we go to free tiles
|
||||||
|
// Then we go to anything else.
|
||||||
|
if (args.Neighbors.Count > 0)
|
||||||
|
{
|
||||||
|
_random.Shuffle(args.Neighbors);
|
||||||
|
|
||||||
|
// Overflow to neighbors with remaining space.
|
||||||
|
foreach (var neighbor in args.Neighbors)
|
||||||
{
|
{
|
||||||
Del(ent);
|
if (!puddleQuery.TryGetComponent(neighbor, out var puddle) ||
|
||||||
|
!_solutionContainerSystem.TryGetSolution(neighbor, puddle.SolutionName, out var neighborSolution))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remaining = neighborSolution.Volume - puddle.OverflowVolume;
|
||||||
|
|
||||||
|
if (remaining <= FixedPoint2.Zero)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var split = overflow.SplitSolution(remaining);
|
||||||
|
|
||||||
|
if (!_solutionContainerSystem.TryAddSolution(neighbor, neighborSolution, split))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
args.Updates--;
|
||||||
|
EnsureComp<EdgeSpreaderComponent>(neighbor);
|
||||||
|
|
||||||
|
if (args.Updates <= 0)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
_deletionQueue.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPuddleInit(EntityUid uid, PuddleComponent component, ComponentInit args)
|
if (overflow.Volume == FixedPoint2.Zero)
|
||||||
{
|
|
||||||
_solutionContainerSystem.EnsureSolution(uid, component.SolutionName, FixedPoint2.New(PuddleVolume), out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPuddleSlip(EntityUid uid, PuddleComponent component, ref SlipEvent args)
|
|
||||||
{
|
|
||||||
// Reactive entities have a chance to get a touch reaction from slipping on a puddle
|
|
||||||
// (i.e. it is implied they fell face first onto it or something)
|
|
||||||
if (!HasComp<ReactiveComponent>(args.Slipped))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Eventually probably have some system of 'body coverage' to tweak the probability but for now just 0.5
|
|
||||||
// (implying that spacemen have a 50% chance to either land on their ass or their face)
|
|
||||||
if (!_random.Prob(0.5f))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_popup.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", uid)),
|
|
||||||
args.Slipped, args.Slipped, PopupType.SmallCaution);
|
|
||||||
|
|
||||||
// Take 15% of the puddle solution
|
|
||||||
var splitSol = _solutionContainerSystem.SplitSolution(uid, solution, solution.Volume * 0.15f);
|
|
||||||
_reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSolutionUpdate(EntityUid uid, PuddleComponent component, SolutionChangedEvent args)
|
|
||||||
{
|
|
||||||
if (args.Solution.Name != component.SolutionName)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (args.Solution.Volume <= 0)
|
|
||||||
{
|
{
|
||||||
_deletionQueue.Add(uid);
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
return;
|
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)
|
_random.Shuffle(args.NeighborFreeTiles);
|
||||||
|| EmptyHolder(uid, puddleComponent))
|
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
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
// Hard-cap lower bound for visibility reasons
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
var spillPerNeighbor = overflow.Volume / args.Neighbors.Count;
|
||||||
if ((puddleComponent.SlipThreshold == FixedPoint2.New(-1) ||
|
|
||||||
vol < puddleComponent.SlipThreshold) &&
|
foreach (var neighbor in args.Neighbors)
|
||||||
TryComp(entityUid, out StepTriggerComponent? stepTrigger))
|
|
||||||
{
|
{
|
||||||
_stepTrigger.SetActive(entityUid, false, stepTrigger);
|
// Overflow to neighbours but not if they're already at the cap
|
||||||
}
|
// This is to avoid diluting solutions too much.
|
||||||
else if (vol >= puddleComponent.SlipThreshold)
|
if (!puddleQuery.TryGetComponent(neighbor, out var puddle) ||
|
||||||
{
|
!_solutionContainerSystem.TryGetSolution(neighbor, puddle.SolutionName, out var neighborSolution) ||
|
||||||
var comp = EnsureComp<StepTriggerComponent>(entityUid);
|
neighborSolution.Volume >= puddle.OverflowVolume)
|
||||||
_stepTrigger.SetActive(entityUid, true, comp);
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var split = overflow.SplitSolution(spillPerNeighbor);
|
||||||
|
|
||||||
|
if (!_solutionContainerSystem.TryAddSolution(neighbor, neighborSolution, split))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
EnsureComp<EdgeSpreaderComponent>(neighbor);
|
||||||
|
args.Updates--;
|
||||||
|
|
||||||
|
if (args.Updates <= 0)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandlePuddleExamined(EntityUid uid, PuddleComponent component, ExaminedEvent args)
|
// Add the remainder back
|
||||||
|
if (_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var puddleSolution))
|
||||||
{
|
{
|
||||||
if (TryComp<StepTriggerComponent>(uid, out var slippery) && slippery.Active)
|
_solutionContainerSystem.TryAddSolution(uid, puddleSolution, overflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPuddleSlip(EntityUid uid, PuddleComponent component, ref SlipEvent args)
|
||||||
|
{
|
||||||
|
// Reactive entities have a chance to get a touch reaction from slipping on a puddle
|
||||||
|
// (i.e. it is implied they fell face first onto it or something)
|
||||||
|
if (!HasComp<ReactiveComponent>(args.Slipped))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Eventually probably have some system of 'body coverage' to tweak the probability but for now just 0.5
|
||||||
|
// (implying that spacemen have a 50% chance to either land on their ass or their face)
|
||||||
|
if (!_random.Prob(0.5f))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_popups.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", uid)),
|
||||||
|
args.Slipped, args.Slipped, PopupType.SmallCaution);
|
||||||
|
|
||||||
|
// Take 15% of the puddle solution
|
||||||
|
var splitSol = _solutionContainerSystem.SplitSolution(uid, solution, solution.Volume * 0.15f);
|
||||||
|
_reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
base.Update(frameTime);
|
||||||
|
foreach (var ent in _deletionQueue)
|
||||||
|
{
|
||||||
|
Del(ent);
|
||||||
|
}
|
||||||
|
_deletionQueue.Clear();
|
||||||
|
|
||||||
|
TickEvaporation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPuddleInit(EntityUid uid, PuddleComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
_solutionContainerSystem.EnsureSolution(uid, component.SolutionName, FixedPoint2.New(PuddleVolume), out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSolutionUpdate(EntityUid uid, PuddleComponent component, SolutionChangedEvent args)
|
||||||
|
{
|
||||||
|
if (args.Solution.Name != component.SolutionName)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.Solution.Volume <= 0)
|
||||||
|
{
|
||||||
|
_deletionQueue.Add(uid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_deletionQueue.Remove(uid);
|
||||||
|
UpdateSlip(uid, component, args.Solution);
|
||||||
|
UpdateEvaporation(uid, args.Solution);
|
||||||
|
UpdateAppearance(uid, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAppearance(EntityUid uid, PuddleComponent? puddleComponent = null, AppearanceComponent? appearance = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref puddleComponent, ref appearance, false))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var volume = FixedPoint2.Zero;
|
||||||
|
Color color = Color.White;
|
||||||
|
|
||||||
|
if (_solutionContainerSystem.TryGetSolution(uid, puddleComponent.SolutionName, out var solution))
|
||||||
|
{
|
||||||
|
volume = solution.Volume / puddleComponent.OverflowVolume;
|
||||||
|
|
||||||
|
// Make blood stand out more
|
||||||
|
// Kinda EH
|
||||||
|
// Could potentially do alpha per-solution but future problem.
|
||||||
|
var standoutReagents = new string[] { "Blood", "Slime" };
|
||||||
|
|
||||||
|
color = solution.GetColorWithout(_prototypeManager, standoutReagents);
|
||||||
|
color = color.WithAlpha(0.7f);
|
||||||
|
|
||||||
|
foreach (var standout in standoutReagents)
|
||||||
{
|
{
|
||||||
args.PushText(Loc.GetString("puddle-component-examine-is-slipper-text"));
|
if (!solution.TryGetReagent(standout, out var quantity))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var interpolateValue = quantity.Float() / solution.Volume.Float();
|
||||||
|
color = Color.InterpolateBetween(color, _prototypeManager.Index<ReagentPrototype>(standout).SubstanceColor, interpolateValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAnchorChanged(EntityUid uid, PuddleComponent puddle, ref AnchorStateChangedEvent args)
|
_appearance.SetData(uid, PuddleVisuals.CurrentVolume, volume.Float(), appearance);
|
||||||
|
_appearance.SetData(uid, PuddleVisuals.SolutionColor, color, appearance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSlip(EntityUid entityUid, PuddleComponent component, Solution solution)
|
||||||
|
{
|
||||||
|
var isSlippery = false;
|
||||||
|
// The base sprite is currently at 0.3 so we require at least 2nd tier to be slippery or else it's too hard to see.
|
||||||
|
var amountRequired = FixedPoint2.New(component.OverflowVolume.Float() * LowThreshold);
|
||||||
|
var slipperyAmount = FixedPoint2.Zero;
|
||||||
|
|
||||||
|
foreach (var reagent in solution.Contents)
|
||||||
{
|
{
|
||||||
if (!args.Anchored)
|
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.ReagentId);
|
||||||
QueueDel(uid);
|
|
||||||
|
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))
|
var comp = EnsureComp<StepTriggerComponent>(entityUid);
|
||||||
return true;
|
_stepTrigger.SetActive(entityUid, true, comp);
|
||||||
|
}
|
||||||
|
else if (TryComp<StepTriggerComponent>(entityUid, out var comp))
|
||||||
|
{
|
||||||
|
_stepTrigger.SetActive(entityUid, false, comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return !_solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName,
|
private void HandlePuddleExamined(EntityUid uid, PuddleComponent component, ExaminedEvent args)
|
||||||
out var solution)
|
{
|
||||||
|| solution.Contents.Count == 0;
|
if (TryComp<StepTriggerComponent>(uid, out var slippery) && slippery.Active)
|
||||||
|
{
|
||||||
|
args.PushMarkup(Loc.GetString("puddle-component-examine-is-slipper-text"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public FixedPoint2 CurrentVolume(EntityUid uid, PuddleComponent? puddleComponent = null)
|
if (HasComp<EvaporationComponent>(uid))
|
||||||
{
|
{
|
||||||
if (!Resolve(uid, ref puddleComponent))
|
if (_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution) &&
|
||||||
return FixedPoint2.Zero;
|
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,
|
private void OnAnchorChanged(EntityUid uid, PuddleComponent puddle, ref AnchorStateChangedEvent args)
|
||||||
out var solution)
|
{
|
||||||
? solution.Volume
|
if (!args.Anchored)
|
||||||
: FixedPoint2.Zero;
|
QueueDel(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current volume of the given puddle, which may not necessarily be PuddleVolume.
|
||||||
|
/// </summary>
|
||||||
|
public FixedPoint2 CurrentVolume(EntityUid uid, PuddleComponent? puddleComponent = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref puddleComponent))
|
||||||
|
return FixedPoint2.Zero;
|
||||||
|
|
||||||
|
return _solutionContainerSystem.TryGetSolution(uid, puddleComponent.SolutionName,
|
||||||
|
out var solution)
|
||||||
|
? solution.Volume
|
||||||
|
: FixedPoint2.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to add solution to <paramref name="puddleUid"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="puddleUid">Puddle to which we add</param>
|
||||||
|
/// <param name="addedSolution">Solution that is added to puddleComponent</param>
|
||||||
|
/// <param name="sound">Play sound on overflow</param>
|
||||||
|
/// <param name="checkForOverflow">Overflow on encountered values</param>
|
||||||
|
/// <param name="puddleComponent">Optional resolved PuddleComponent</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool TryAddSolution(EntityUid puddleUid,
|
||||||
|
Solution addedSolution,
|
||||||
|
bool sound = true,
|
||||||
|
bool checkForOverflow = true,
|
||||||
|
PuddleComponent? puddleComponent = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(puddleUid, ref puddleComponent))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (addedSolution.Volume == 0 ||
|
||||||
|
!_solutionContainerSystem.TryGetSolution(puddleUid, puddleComponent.SolutionName,
|
||||||
|
out var solution))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
solution.AddSolution(addedSolution, _prototypeManager);
|
||||||
/// Try to add solution to <paramref name="puddleUid"/>.
|
_solutionContainerSystem.UpdateChemicals(puddleUid, solution, true);
|
||||||
/// </summary>
|
|
||||||
/// <param name="puddleUid">Puddle to which we add</param>
|
if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent))
|
||||||
/// <param name="addedSolution">Solution that is added to puddleComponent</param>
|
|
||||||
/// <param name="sound">Play sound on overflow</param>
|
|
||||||
/// <param name="checkForOverflow">Overflow on encountered values</param>
|
|
||||||
/// <param name="puddleComponent">Optional resolved PuddleComponent</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public bool TryAddSolution(EntityUid puddleUid,
|
|
||||||
Solution addedSolution,
|
|
||||||
bool sound = true,
|
|
||||||
bool checkForOverflow = true,
|
|
||||||
PuddleComponent? puddleComponent = null)
|
|
||||||
{
|
{
|
||||||
if (!Resolve(puddleUid, ref puddleComponent))
|
EnsureComp<EdgeSpreaderComponent>(puddleUid);
|
||||||
return false;
|
}
|
||||||
|
|
||||||
if (addedSolution.Volume == 0 ||
|
if (!sound)
|
||||||
!_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);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
SoundSystem.Play(puddleComponent.SpillSound.GetSound(),
|
||||||
/// Given a large srcPuddle and smaller destination puddles, this method will equalize their <see cref="Solution.CurrentVolume"/>
|
Filter.Pvs(puddleUid), puddleUid);
|
||||||
/// </summary>
|
return true;
|
||||||
/// <param name="srcPuddle">puddle that donates liquids to other puddles</param>
|
}
|
||||||
/// <param name="destinationPuddles">List of puddles that we want to equalize, their puddle <see cref="Solution.CurrentVolume"/> should be less than sourcePuddleComponent</param>
|
|
||||||
/// <param name="totalVolume">Total volume of src and destination puddle</param>
|
/// <summary>
|
||||||
/// <param name="stillOverflowing">optional parameter, that after equalization adds all still overflowing puddles.</param>
|
/// Whether adding this solution to this puddle would overflow.
|
||||||
/// <param name="sourcePuddleComponent">puddleComponent for <paramref name="srcPuddle"/></param>
|
/// </summary>
|
||||||
public void EqualizePuddles(EntityUid srcPuddle, List<PuddleComponent> destinationPuddles,
|
public bool WouldOverflow(EntityUid uid, Solution solution, PuddleComponent? puddle = null)
|
||||||
FixedPoint2 totalVolume,
|
{
|
||||||
HashSet<EntityUid>? stillOverflowing = null,
|
if (!Resolve(uid, ref puddle))
|
||||||
PuddleComponent? sourcePuddleComponent = null)
|
return false;
|
||||||
|
|
||||||
|
return CurrentVolume(uid, puddle) + solution.Volume > puddle.OverflowVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether adding this solution to this puddle would overflow.
|
||||||
|
/// </summary>
|
||||||
|
private bool IsOverflowing(EntityUid uid, PuddleComponent? puddle = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref puddle))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return CurrentVolume(uid, puddle) > puddle.OverflowVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the solution amount above the overflow threshold for the puddle.
|
||||||
|
/// </summary>
|
||||||
|
public Solution GetOverflowSolution(EntityUid uid, PuddleComponent? puddle = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref puddle) || !_solutionContainerSystem.TryGetSolution(uid, puddle.SolutionName,
|
||||||
|
out var solution))
|
||||||
{
|
{
|
||||||
if (!Resolve(srcPuddle, ref sourcePuddleComponent)
|
return new Solution(0);
|
||||||
|| !_solutionContainerSystem.TryGetSolution(srcPuddle, sourcePuddleComponent.SolutionName,
|
}
|
||||||
out var srcSolution))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var dividedVolume = totalVolume / (destinationPuddles.Count + 1);
|
// TODO: This is going to fail with struct solutions.
|
||||||
|
var remaining = puddle.OverflowVolume;
|
||||||
|
var split = _solutionContainerSystem.SplitSolution(uid, solution, CurrentVolume(uid, puddle) - remaining);
|
||||||
|
return split;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var destPuddle in destinationPuddles)
|
#region Spill
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// First splashes reagent on reactive entities near the spilling entity, then spills the rest regularly to a
|
||||||
|
/// puddle. This is intended for 'destructive' spills, like when entities are destroyed or thrown.
|
||||||
|
/// </summary>
|
||||||
|
public bool TrySplashSpillAt(EntityUid uid,
|
||||||
|
EntityCoordinates coordinates,
|
||||||
|
Solution solution,
|
||||||
|
out EntityUid puddleUid,
|
||||||
|
bool sound = true,
|
||||||
|
EntityUid? user = null)
|
||||||
|
{
|
||||||
|
puddleUid = EntityUid.Invalid;
|
||||||
|
|
||||||
|
if (solution.Volume == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Get reactive entities nearby--if there are some, it'll spill a bit on them instead.
|
||||||
|
foreach (var ent in _lookup.GetComponentsInRange<ReactiveComponent>(coordinates, 1.0f))
|
||||||
|
{
|
||||||
|
// sorry! no overload for returning uid, so .owner must be used
|
||||||
|
var owner = ent.Owner;
|
||||||
|
|
||||||
|
// between 5 and 30%
|
||||||
|
var splitAmount = solution.Volume * _random.NextFloat(0.05f, 0.30f);
|
||||||
|
var splitSolution = solution.SplitSolution(splitAmount);
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
{
|
{
|
||||||
if (!_solutionContainerSystem.TryGetSolution(destPuddle.Owner, destPuddle.SolutionName,
|
_adminLogger.Add(LogType.Landed,
|
||||||
out var destSolution))
|
$"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
|
||||||
|
_popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other", ("spillable", uid), ("target", owner)), owner, PopupType.SmallCaution);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrySpillAt(coordinates, solution, out puddleUid, sound);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spills solution at the specified coordinates.
|
||||||
|
/// Will add to an existing puddle if present or create a new one if not.
|
||||||
|
/// </summary>
|
||||||
|
public bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true)
|
||||||
|
{
|
||||||
|
if (solution.Volume == 0)
|
||||||
|
{
|
||||||
|
puddleUid = EntityUid.Invalid;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_mapManager.TryGetGrid(coordinates.GetGridUid(EntityManager), out var mapGrid))
|
||||||
|
{
|
||||||
|
puddleUid = EntityUid.Invalid;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrySpillAt(mapGrid.GetTileRef(coordinates), solution, out puddleUid, sound);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="TrySpillAt(Robust.Shared.Map.EntityCoordinates,Content.Shared.Chemistry.Components.Solution,out Robust.Shared.GameObjects.EntityUid,bool)"/>
|
||||||
|
/// </summary>
|
||||||
|
public bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true, TransformComponent? transformComponent = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref transformComponent, false))
|
||||||
|
{
|
||||||
|
puddleUid = EntityUid.Invalid;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrySpillAt(transformComponent.Coordinates, solution, out puddleUid, sound: sound);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="TrySpillAt(Robust.Shared.Map.EntityCoordinates,Content.Shared.Chemistry.Components.Solution,out Robust.Shared.GameObjects.EntityUid,bool)"/>
|
||||||
|
/// </summary>
|
||||||
|
public bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true, bool tileReact = true)
|
||||||
|
{
|
||||||
|
if (solution.Volume <= 0)
|
||||||
|
{
|
||||||
|
puddleUid = EntityUid.Invalid;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If space return early, let that spill go out into the void
|
||||||
|
if (tileRef.Tile.IsEmpty)
|
||||||
|
{
|
||||||
|
puddleUid = EntityUid.Invalid;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's not spill to invalid grids.
|
||||||
|
var gridId = tileRef.GridUid;
|
||||||
|
if (!_mapManager.TryGetGrid(gridId, out var mapGrid))
|
||||||
|
{
|
||||||
|
puddleUid = EntityUid.Invalid;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tileReact)
|
||||||
|
{
|
||||||
|
// First, do all tile reactions
|
||||||
|
for (var i = 0; i < solution.Contents.Count; i++)
|
||||||
|
{
|
||||||
|
var (reagentId, quantity) = solution.Contents[i];
|
||||||
|
var proto = _prototypeManager.Index<ReagentPrototype>(reagentId);
|
||||||
|
var removed = proto.ReactionTile(tileRef, quantity);
|
||||||
|
if (removed <= FixedPoint2.Zero)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var takeAmount = FixedPoint2.Max(0, dividedVolume - destSolution.Volume);
|
solution.RemoveReagent(reagentId, removed);
|
||||||
TryAddSolution(destPuddle.Owner, srcSolution.SplitSolution(takeAmount), false, false, destPuddle);
|
|
||||||
if (stillOverflowing != null && IsOverflowing(destPuddle.Owner, destPuddle))
|
|
||||||
{
|
|
||||||
stillOverflowing.Add(destPuddle.Owner);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (stillOverflowing != null && srcSolution.Volume > sourcePuddleComponent.OverflowVolume)
|
// Tile reactions used up everything.
|
||||||
|
if (solution.Volume == FixedPoint2.Zero)
|
||||||
|
{
|
||||||
|
puddleUid = EntityUid.Invalid;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get normalized co-ordinate for spill location and spill it in the centre
|
||||||
|
// TODO: Does SnapGrid or something else already do this?
|
||||||
|
var anchored = mapGrid.GetAnchoredEntitiesEnumerator(tileRef.GridIndices);
|
||||||
|
var puddleQuery = GetEntityQuery<PuddleComponent>();
|
||||||
|
var sparklesQuery = GetEntityQuery<EvaporationSparkleComponent>();
|
||||||
|
|
||||||
|
while (anchored.MoveNext(out var ent))
|
||||||
|
{
|
||||||
|
// If there's existing sparkles then delete it
|
||||||
|
if (sparklesQuery.TryGetComponent(ent, out var sparkles))
|
||||||
{
|
{
|
||||||
stillOverflowing.Add(srcPuddle);
|
QueueDel(ent.Value);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!puddleQuery.TryGetComponent(ent, out var puddle))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (TryAddSolution(ent.Value, solution, sound, puddleComponent: puddle))
|
||||||
|
{
|
||||||
|
EnsureComp<EdgeSpreaderComponent>(ent.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
puddleUid = ent.Value;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var coords = mapGrid.GridTileToLocal(tileRef.GridIndices);
|
||||||
/// Whether adding this solution to this puddle would overflow.
|
puddleUid = EntityManager.SpawnEntity("Puddle", coords);
|
||||||
/// </summary>
|
EnsureComp<PuddleComponent>(puddleUid);
|
||||||
/// <param name="uid">Uid of owning entity</param>
|
if (TryAddSolution(puddleUid, solution, sound))
|
||||||
/// <param name="puddle">Puddle to which we are adding solution</param>
|
|
||||||
/// <param name="solution">Solution we intend to add</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public bool WouldOverflow(EntityUid uid, Solution solution, PuddleComponent? puddle = null)
|
|
||||||
{
|
{
|
||||||
if (!Resolve(uid, ref puddle))
|
EnsureComp<EdgeSpreaderComponent>(puddleUid);
|
||||||
return false;
|
|
||||||
|
|
||||||
return CurrentVolume(uid, puddle) + solution.Volume > puddle.OverflowVolume;
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
#endregion
|
||||||
/// Whether adding this solution to this puddle would overflow.
|
|
||||||
/// </summary>
|
/// <summary>
|
||||||
/// <param name="uid">Uid of owning entity</param>
|
/// Tries to get the relevant puddle entity for a tile.
|
||||||
/// <param name="puddle">Puddle ref param</param>
|
/// </summary>
|
||||||
/// <returns></returns>
|
public bool TryGetPuddle(TileRef tile, out EntityUid puddleUid)
|
||||||
private bool IsOverflowing(EntityUid uid, PuddleComponent? puddle = null)
|
{
|
||||||
|
puddleUid = EntityUid.Invalid;
|
||||||
|
|
||||||
|
if (!TryComp<MapGridComponent>(tile.GridUid, out var grid))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var anc = grid.GetAnchoredEntitiesEnumerator(tile.GridIndices);
|
||||||
|
var puddleQuery = GetEntityQuery<PuddleComponent>();
|
||||||
|
|
||||||
|
while (anc.MoveNext(out var ent))
|
||||||
{
|
{
|
||||||
if (!Resolve(uid, ref puddle))
|
if (!puddleQuery.HasComponent(ent.Value))
|
||||||
return false;
|
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)
|
return false;
|
||||||
{
|
|
||||||
MetaDataComponent? metadata = null;
|
|
||||||
Resolve(srcUid, ref metadata);
|
|
||||||
|
|
||||||
var prototype = metadata?.EntityPrototype?.ID ?? "PuddleSmear"; // TODO Spawn a entity based on another entity
|
|
||||||
|
|
||||||
uid = EntityManager.SpawnEntity(prototype, pos);
|
|
||||||
component = EntityManager.EnsureComponent<PuddleComponent>(uid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
326
Content.Server/Fluids/EntitySystems/SmokeSystem.cs
Normal file
326
Content.Server/Fluids/EntitySystems/SmokeSystem.cs
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Content.Server.Administration.Logs;
|
||||||
|
using Content.Server.Body.Components;
|
||||||
|
using Content.Server.Body.Systems;
|
||||||
|
using Content.Server.Chemistry.Components;
|
||||||
|
using Content.Server.Chemistry.EntitySystems;
|
||||||
|
using Content.Server.Chemistry.ReactionEffects;
|
||||||
|
using Content.Server.Coordinates.Helpers;
|
||||||
|
using Content.Server.Spreader;
|
||||||
|
using Content.Shared.Chemistry;
|
||||||
|
using Content.Shared.Chemistry.Components;
|
||||||
|
using Content.Shared.Chemistry.Reaction;
|
||||||
|
using Content.Shared.Chemistry.Reagent;
|
||||||
|
using Content.Shared.Database;
|
||||||
|
using Content.Shared.FixedPoint;
|
||||||
|
using Content.Shared.Smoking;
|
||||||
|
using Content.Shared.Spawners;
|
||||||
|
using Content.Shared.Spawners.Components;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
|
namespace Content.Server.Fluids.EntitySystems;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles non-atmos solution entities similar to puddles.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SmokeSystem : EntitySystem
|
||||||
|
{
|
||||||
|
// If I could do it all again this could probably use a lot more of puddles.
|
||||||
|
[Dependency] private readonly IAdminLogManager _logger = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||||
|
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
||||||
|
[Dependency] private readonly BloodstreamSystem _blood = default!;
|
||||||
|
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||||
|
[Dependency] private readonly InternalsSystem _internals = default!;
|
||||||
|
[Dependency] private readonly ReactiveSystem _reactive = default!;
|
||||||
|
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
SubscribeLocalEvent<SmokeComponent, EntityUnpausedEvent>(OnSmokeUnpaused);
|
||||||
|
SubscribeLocalEvent<SmokeComponent, MapInitEvent>(OnSmokeMapInit);
|
||||||
|
SubscribeLocalEvent<SmokeComponent, ReactionAttemptEvent>(OnReactionAttempt);
|
||||||
|
SubscribeLocalEvent<SmokeComponent, SpreadNeighborsEvent>(OnSmokeSpread);
|
||||||
|
SubscribeLocalEvent<SmokeDissipateSpawnComponent, TimedDespawnEvent>(OnSmokeDissipate);
|
||||||
|
SubscribeLocalEvent<SpreadGroupUpdateRate>(OnSpreadUpdateRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSpreadUpdateRate(ref SpreadGroupUpdateRate ev)
|
||||||
|
{
|
||||||
|
if (ev.Name != "smoke")
|
||||||
|
return;
|
||||||
|
|
||||||
|
ev.UpdatesPerSecond = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSmokeDissipate(EntityUid uid, SmokeDissipateSpawnComponent component, ref TimedDespawnEvent args)
|
||||||
|
{
|
||||||
|
if (!TryComp<TransformComponent>(uid, out var xform))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Spawn(component.Prototype, xform.Coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSmokeSpread(EntityUid uid, SmokeComponent component, ref SpreadNeighborsEvent args)
|
||||||
|
{
|
||||||
|
if (component.SpreadAmount == 0 ||
|
||||||
|
args.Grid == null ||
|
||||||
|
!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution) ||
|
||||||
|
args.NeighborFreeTiles.Count == 0)
|
||||||
|
{
|
||||||
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prototype = MetaData(uid).EntityPrototype;
|
||||||
|
|
||||||
|
if (prototype == null)
|
||||||
|
{
|
||||||
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryComp<TimedDespawnComponent>(uid, out var timer);
|
||||||
|
|
||||||
|
var smokePerSpread = component.SpreadAmount / args.NeighborFreeTiles.Count;
|
||||||
|
component.SpreadAmount -= smokePerSpread;
|
||||||
|
|
||||||
|
foreach (var tile in args.NeighborFreeTiles)
|
||||||
|
{
|
||||||
|
var coords = args.Grid.GridTileToLocal(tile);
|
||||||
|
var ent = Spawn(prototype.ID, coords.SnapToGrid());
|
||||||
|
var neighborSmoke = EnsureComp<SmokeComponent>(ent);
|
||||||
|
neighborSmoke.SpreadAmount = Math.Max(0, smokePerSpread - 1);
|
||||||
|
args.Updates--;
|
||||||
|
|
||||||
|
// Listen this is the old behaviour iunno
|
||||||
|
Start(ent, neighborSmoke, solution.Clone(), timer?.Lifetime ?? 10f);
|
||||||
|
|
||||||
|
if (_appearance.TryGetData(uid, SmokeVisuals.Color, out var color))
|
||||||
|
{
|
||||||
|
_appearance.SetData(ent, SmokeVisuals.Color, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only 1 spread then ig?
|
||||||
|
if (smokePerSpread == 0)
|
||||||
|
{
|
||||||
|
component.SpreadAmount--;
|
||||||
|
|
||||||
|
if (component.SpreadAmount == 0)
|
||||||
|
{
|
||||||
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Updates <= 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give our spread to neighbor tiles.
|
||||||
|
if (args.NeighborFreeTiles.Count == 0 && args.Neighbors.Count > 0 && component.SpreadAmount > 0)
|
||||||
|
{
|
||||||
|
var smokeQuery = GetEntityQuery<SmokeComponent>();
|
||||||
|
|
||||||
|
foreach (var neighbor in args.Neighbors)
|
||||||
|
{
|
||||||
|
if (!smokeQuery.TryGetComponent(neighbor, out var smoke))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
smoke.SpreadAmount++;
|
||||||
|
args.Updates--;
|
||||||
|
|
||||||
|
if (component.SpreadAmount == 0)
|
||||||
|
{
|
||||||
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Updates <= 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReactionAttempt(EntityUid uid, SmokeComponent component, ReactionAttemptEvent args)
|
||||||
|
{
|
||||||
|
if (args.Solution.Name != SmokeComponent.SolutionName)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Prevent smoke/foam fork bombs (smoke creating more smoke).
|
||||||
|
foreach (var effect in args.Reaction.Effects)
|
||||||
|
{
|
||||||
|
if (effect is AreaReactionEffect)
|
||||||
|
{
|
||||||
|
args.Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSmokeMapInit(EntityUid uid, SmokeComponent component, MapInitEvent args)
|
||||||
|
{
|
||||||
|
component.NextReact = _timing.CurTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSmokeUnpaused(EntityUid uid, SmokeComponent component, ref EntityUnpausedEvent args)
|
||||||
|
{
|
||||||
|
component.NextReact += args.PausedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
base.Update(frameTime);
|
||||||
|
var query = EntityQueryEnumerator<SmokeComponent>();
|
||||||
|
var curTime = _timing.CurTime;
|
||||||
|
|
||||||
|
while (query.MoveNext(out var uid, out var smoke))
|
||||||
|
{
|
||||||
|
if (smoke.NextReact > curTime)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
smoke.NextReact += TimeSpan.FromSeconds(1.5);
|
||||||
|
|
||||||
|
SmokeReact(uid, 1f, smoke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Does the relevant smoke reactions for an entity for the specified exposure duration.
|
||||||
|
/// </summary>
|
||||||
|
public void SmokeReact(EntityUid uid, float frameTime, SmokeComponent? component = null, TransformComponent? xform = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref component, ref xform))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution) ||
|
||||||
|
solution.Contents.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_mapManager.TryGetGrid(xform.GridUid, out var mapGrid))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var tile = mapGrid.GetTileRef(xform.Coordinates.ToVector2i(EntityManager, _mapManager));
|
||||||
|
|
||||||
|
var solutionFraction = 1 / Math.Floor(frameTime);
|
||||||
|
var ents = _lookup.GetEntitiesIntersecting(tile, LookupFlags.Uncontained).ToArray();
|
||||||
|
|
||||||
|
foreach (var reagentQuantity in solution.Contents.ToArray())
|
||||||
|
{
|
||||||
|
if (reagentQuantity.Quantity == FixedPoint2.Zero)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var reagent = _prototype.Index<ReagentPrototype>(reagentQuantity.ReagentId);
|
||||||
|
|
||||||
|
// React with the tile the effect is on
|
||||||
|
// We don't multiply by solutionFraction here since the tile is only ever reacted once
|
||||||
|
if (!component.ReactedTile)
|
||||||
|
{
|
||||||
|
reagent.ReactionTile(tile, reagentQuantity.Quantity);
|
||||||
|
component.ReactedTile = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch every entity on tile.
|
||||||
|
foreach (var entity in ents)
|
||||||
|
{
|
||||||
|
if (entity == uid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_reactive.ReactionEntity(entity, ReactionMethod.Touch, reagent,
|
||||||
|
reagentQuantity.Quantity * solutionFraction, solution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entity in ents)
|
||||||
|
{
|
||||||
|
if (entity == uid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ReactWithEntity(entity, solution, solutionFraction);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateVisuals(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateVisuals(EntityUid uid)
|
||||||
|
{
|
||||||
|
if (TryComp(uid, out AppearanceComponent? appearance) &&
|
||||||
|
_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution))
|
||||||
|
{
|
||||||
|
var color = solution.GetColor(_prototype);
|
||||||
|
_appearance.SetData(uid, SmokeVisuals.Color, color, appearance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReactWithEntity(EntityUid entity, Solution solution, double solutionFraction)
|
||||||
|
{
|
||||||
|
if (!TryComp<BloodstreamComponent>(entity, out var bloodstream))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (TryComp<InternalsComponent>(entity, out var internals) &&
|
||||||
|
_internals.AreInternalsWorking(internals))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cloneSolution = solution.Clone();
|
||||||
|
var transferAmount = FixedPoint2.Min(cloneSolution.Volume * solutionFraction, bloodstream.ChemicalSolution.AvailableVolume);
|
||||||
|
var transferSolution = cloneSolution.SplitSolution(transferAmount);
|
||||||
|
|
||||||
|
foreach (var reagentQuantity in transferSolution.Contents.ToArray())
|
||||||
|
{
|
||||||
|
if (reagentQuantity.Quantity == FixedPoint2.Zero)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentQuantity.ReagentId, reagentQuantity.Quantity, transferSolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_blood.TryAddToChemicals(entity, transferSolution, bloodstream))
|
||||||
|
{
|
||||||
|
// Log solution addition by smoke
|
||||||
|
_logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} was affected by smoke {SolutionContainerSystem.ToPrettyString(transferSolution)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets up a smoke component for spreading.
|
||||||
|
/// </summary>
|
||||||
|
public void Start(EntityUid uid, SmokeComponent component, Solution solution, float duration)
|
||||||
|
{
|
||||||
|
TryAddSolution(uid, component, solution);
|
||||||
|
EnsureComp<EdgeSpreaderComponent>(uid);
|
||||||
|
var timer = EnsureComp<TimedDespawnComponent>(uid);
|
||||||
|
timer.Lifetime = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the specified solution to the relevant smoke solution.
|
||||||
|
/// </summary>
|
||||||
|
public void TryAddSolution(EntityUid uid, SmokeComponent component, Solution solution)
|
||||||
|
{
|
||||||
|
if (solution.Volume == FixedPoint2.Zero)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solutionArea))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var addSolution =
|
||||||
|
solution.SplitSolution(FixedPoint2.Min(solution.Volume, solutionArea.AvailableVolume));
|
||||||
|
|
||||||
|
_solutionSystem.TryAddSolution(uid, solutionArea, addSolution);
|
||||||
|
|
||||||
|
UpdateVisuals(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,366 +0,0 @@
|
|||||||
using Content.Server.Administration.Logs;
|
|
||||||
using Content.Server.Chemistry.EntitySystems;
|
|
||||||
using Content.Server.Fluids.Components;
|
|
||||||
using Content.Server.Nutrition.Components;
|
|
||||||
using Content.Shared.Chemistry.Components;
|
|
||||||
using Content.Shared.Chemistry.Reagent;
|
|
||||||
using Content.Shared.Clothing.Components;
|
|
||||||
using Content.Shared.Database;
|
|
||||||
using Content.Shared.FixedPoint;
|
|
||||||
using Content.Shared.Inventory.Events;
|
|
||||||
using Content.Shared.Throwing;
|
|
||||||
using Content.Shared.Verbs;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
|
||||||
using Content.Shared.Chemistry;
|
|
||||||
using Content.Shared.Chemistry.Reaction;
|
|
||||||
using Content.Shared.DoAfter;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.IdentityManagement;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Spillable;
|
|
||||||
using Content.Shared.Weapons.Melee;
|
|
||||||
using Content.Shared.Weapons.Melee.Events;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Random;
|
|
||||||
|
|
||||||
namespace Content.Server.Fluids.EntitySystems;
|
|
||||||
|
|
||||||
[UsedImplicitly]
|
|
||||||
public sealed class SpillableSystem : EntitySystem
|
|
||||||
{
|
|
||||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
|
||||||
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
|
||||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
||||||
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
|
|
||||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
|
||||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
|
||||||
[Dependency] private readonly ReactiveSystem _reactive = default!;
|
|
||||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
SubscribeLocalEvent<SpillableComponent, ExaminedEvent>(OnExamined);
|
|
||||||
SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand);
|
|
||||||
SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit);
|
|
||||||
SubscribeLocalEvent<SpillableComponent, GetVerbsEvent<Verb>>(AddSpillVerb);
|
|
||||||
SubscribeLocalEvent<SpillableComponent, GotEquippedEvent>(OnGotEquipped);
|
|
||||||
SubscribeLocalEvent<SpillableComponent, SolutionSpikeOverflowEvent>(OnSpikeOverflow);
|
|
||||||
SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnExamined(EntityUid uid, SpillableComponent component, ExaminedEvent args)
|
|
||||||
{
|
|
||||||
args.PushMarkup(Loc.GetString("spill-examine-is-spillable"));
|
|
||||||
|
|
||||||
if (HasComp<MeleeWeaponComponent>(uid))
|
|
||||||
args.PushMarkup(Loc.GetString("spill-examine-spillable-weapon"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSpikeOverflow(EntityUid uid, SpillableComponent component, SolutionSpikeOverflowEvent args)
|
|
||||||
{
|
|
||||||
if (!args.Handled)
|
|
||||||
{
|
|
||||||
SpillAt(args.Overflow, Transform(uid).Coordinates, "PuddleSmear");
|
|
||||||
}
|
|
||||||
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnGotEquipped(EntityUid uid, SpillableComponent component, GotEquippedEvent args)
|
|
||||||
{
|
|
||||||
if (!component.SpillWorn)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!TryComp(uid, out ClothingComponent? clothing))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// check if entity was actually used as clothing
|
|
||||||
// not just taken in pockets or something
|
|
||||||
var isCorrectSlot = clothing.Slots.HasFlag(args.SlotFlags);
|
|
||||||
if (!isCorrectSlot) return;
|
|
||||||
|
|
||||||
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
|
|
||||||
return;
|
|
||||||
if (solution.Volume == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// spill all solution on the player
|
|
||||||
var drainedSolution = _solutionContainerSystem.Drain(uid, solution, solution.Volume);
|
|
||||||
SpillAt(args.Equipee, drainedSolution, "PuddleSmear");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Spills the specified solution at the entity's location if possible.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">
|
|
||||||
/// The entity to use as a location to spill the solution at.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="solution">Initial solution for the prototype.</param>
|
|
||||||
/// <param name="prototype">The prototype to use.</param>
|
|
||||||
/// <param name="sound">Play the spill sound.</param>
|
|
||||||
/// <param name="combine">Whether to attempt to merge with existing puddles</param>
|
|
||||||
/// <param name="transformComponent">Optional Transform component</param>
|
|
||||||
/// <returns>The puddle if one was created, null otherwise.</returns>
|
|
||||||
public PuddleComponent? SpillAt(EntityUid uid, Solution solution, string prototype,
|
|
||||||
bool sound = true, bool combine = true, TransformComponent? transformComponent = null)
|
|
||||||
{
|
|
||||||
return !Resolve(uid, ref transformComponent, false)
|
|
||||||
? null
|
|
||||||
: SpillAt(solution, transformComponent.Coordinates, prototype, sound: sound, combine: combine);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SpillOnLand(EntityUid uid, SpillableComponent component, ref LandEvent args)
|
|
||||||
{
|
|
||||||
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution)) return;
|
|
||||||
|
|
||||||
if (TryComp<DrinkComponent>(uid, out var drink) && (!drink.Opened))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (args.User != null)
|
|
||||||
{
|
|
||||||
_adminLogger.Add(LogType.Landed,
|
|
||||||
$"{ToPrettyString(args.User.Value):user} threw {ToPrettyString(uid):entity} which spilled a solution {SolutionContainerSystem.ToPrettyString(solution):solution} on landing");
|
|
||||||
}
|
|
||||||
|
|
||||||
var drainedSolution = _solutionContainerSystem.Drain(uid, solution, solution.Volume);
|
|
||||||
SplashSpillAt(uid, drainedSolution, Transform(uid).Coordinates, "PuddleSmear");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SplashOnMeleeHit(EntityUid uid, SpillableComponent component, MeleeHitEvent args)
|
|
||||||
{
|
|
||||||
// When attacking someone reactive with a spillable entity,
|
|
||||||
// splash a little on them (touch react)
|
|
||||||
// If this also has solution transfer, then assume the transfer amount is how much we want to spill.
|
|
||||||
// Otherwise let's say they want to spill a quarter of its max volume.
|
|
||||||
|
|
||||||
if (!_solutionContainerSystem.TryGetDrainableSolution(uid, out var solution))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (TryComp<DrinkComponent>(uid, out var drink) && !drink.Opened)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var hitCount = args.HitEntities.Count;
|
|
||||||
|
|
||||||
var totalSplit = FixedPoint2.Min(solution.MaxVolume * 0.25, solution.Volume);
|
|
||||||
if (TryComp<SolutionTransferComponent>(uid, out var transfer))
|
|
||||||
{
|
|
||||||
totalSplit = FixedPoint2.Min(transfer.TransferAmount, solution.Volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
// a little lame, but reagent quantity is not very balanced and we don't want people
|
|
||||||
// spilling like 100u of reagent on someone at once!
|
|
||||||
totalSplit = FixedPoint2.Min(totalSplit, component.MaxMeleeSpillAmount);
|
|
||||||
|
|
||||||
foreach (var hit in args.HitEntities)
|
|
||||||
{
|
|
||||||
if (!HasComp<ReactiveComponent>(hit))
|
|
||||||
{
|
|
||||||
hitCount -= 1; // so we don't undershoot solution calculation for actual reactive entities
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var splitSolution = _solutionContainerSystem.SplitSolution(uid, solution, totalSplit / hitCount);
|
|
||||||
|
|
||||||
_adminLogger.Add(LogType.MeleeHit, $"{ToPrettyString(args.User)} splashed {SolutionContainerSystem.ToPrettyString(splitSolution):solution} from {ToPrettyString(uid):entity} onto {ToPrettyString(hit):target}");
|
|
||||||
_reactive.DoEntityReaction(hit, splitSolution, ReactionMethod.Touch);
|
|
||||||
|
|
||||||
_popup.PopupEntity(
|
|
||||||
Loc.GetString("spill-melee-hit-attacker", ("amount", totalSplit / hitCount), ("spillable", uid),
|
|
||||||
("target", Identity.Entity(hit, EntityManager))),
|
|
||||||
hit, args.User);
|
|
||||||
|
|
||||||
_popup.PopupEntity(
|
|
||||||
Loc.GetString("spill-melee-hit-others", ("attacker", args.User), ("spillable", uid),
|
|
||||||
("target", Identity.Entity(hit, EntityManager))),
|
|
||||||
hit, Filter.PvsExcept(args.User), true, PopupType.SmallCaution);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddSpillVerb(EntityUid uid, SpillableComponent component, GetVerbsEvent<Verb> args)
|
|
||||||
{
|
|
||||||
if (!args.CanAccess || !args.CanInteract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_solutionContainerSystem.TryGetDrainableSolution(args.Target, out var solution))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (TryComp<DrinkComponent>(args.Target, out var drink) && (!drink.Opened))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (solution.Volume == FixedPoint2.Zero)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Verb verb = new();
|
|
||||||
verb.Text = Loc.GetString("spill-target-verb-get-data-text");
|
|
||||||
// TODO VERB ICONS spill icon? pouring out a glass/beaker?
|
|
||||||
|
|
||||||
verb.Act = () =>
|
|
||||||
{
|
|
||||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(args.User, component.SpillDelay ?? 0, new SpillDoAfterEvent(), uid, target: uid)
|
|
||||||
{
|
|
||||||
BreakOnTargetMove = true,
|
|
||||||
BreakOnUserMove = true,
|
|
||||||
BreakOnDamage = true,
|
|
||||||
NeedHand = true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
verb.Impact = LogImpact.Medium; // dangerous reagent reaction are logged separately.
|
|
||||||
verb.DoContactInteraction = true;
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// First splashes reagent on reactive entities near the spilling entity, then spills the rest regularly to a
|
|
||||||
/// puddle. This is intended for 'destructive' spills, like when entities are destroyed or thrown.
|
|
||||||
/// </summary>
|
|
||||||
public PuddleComponent? SplashSpillAt(EntityUid uid, Solution solution, EntityCoordinates coordinates, string prototype,
|
|
||||||
bool overflow = true, bool sound = true, bool combine = true, EntityUid? user=null)
|
|
||||||
{
|
|
||||||
if (solution.Volume == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// Get reactive entities nearby--if there are some, it'll spill a bit on them instead.
|
|
||||||
foreach (var ent in _entityLookup.GetComponentsInRange<ReactiveComponent>(coordinates, 1.0f))
|
|
||||||
{
|
|
||||||
// sorry! no overload for returning uid, so .owner must be used
|
|
||||||
var owner = ent.Owner;
|
|
||||||
|
|
||||||
// between 5 and 30%
|
|
||||||
var splitAmount = solution.Volume * _random.NextFloat(0.05f, 0.30f);
|
|
||||||
var splitSolution = solution.SplitSolution(splitAmount);
|
|
||||||
|
|
||||||
if (user != null)
|
|
||||||
{
|
|
||||||
_adminLogger.Add(LogType.Landed,
|
|
||||||
$"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}");
|
|
||||||
}
|
|
||||||
|
|
||||||
_reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
|
|
||||||
_popup.PopupEntity(Loc.GetString("spill-land-spilled-on-other", ("spillable", uid), ("target", owner)), owner, PopupType.SmallCaution);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SpillAt(solution, coordinates, prototype, overflow, sound, combine: combine);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Spills solution at the specified grid coordinates.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="solution">Initial solution for the prototype.</param>
|
|
||||||
/// <param name="coordinates">The coordinates to spill the solution at.</param>
|
|
||||||
/// <param name="prototype">The prototype to use.</param>
|
|
||||||
/// <param name="overflow">If the puddle overflow will be calculated. Defaults to true.</param>
|
|
||||||
/// <param name="sound">Whether or not to play the spill sound.</param>
|
|
||||||
/// <param name="combine">Whether to attempt to merge with existing puddles</param>
|
|
||||||
/// <returns>The puddle if one was created, null otherwise.</returns>
|
|
||||||
public PuddleComponent? SpillAt(Solution solution, EntityCoordinates coordinates, string prototype,
|
|
||||||
bool overflow = true, bool sound = true, bool combine = true)
|
|
||||||
{
|
|
||||||
if (solution.Volume == 0) return null;
|
|
||||||
|
|
||||||
if (!_mapManager.TryGetGrid(coordinates.GetGridUid(EntityManager), out var mapGrid))
|
|
||||||
return null; // Let's not spill to space.
|
|
||||||
|
|
||||||
return SpillAt(mapGrid.GetTileRef(coordinates), solution, prototype, overflow, sound,
|
|
||||||
combine: combine);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetPuddle(TileRef tileRef, [NotNullWhen(true)] out PuddleComponent? puddle)
|
|
||||||
{
|
|
||||||
foreach (var entity in _entityLookup.GetEntitiesIntersecting(tileRef))
|
|
||||||
{
|
|
||||||
if (EntityManager.TryGetComponent(entity, out PuddleComponent? p))
|
|
||||||
{
|
|
||||||
puddle = p;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
puddle = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PuddleComponent? SpillAt(TileRef tileRef, Solution solution, string prototype,
|
|
||||||
bool overflow = true, bool sound = true, bool noTileReact = false, bool combine = true)
|
|
||||||
{
|
|
||||||
if (solution.Volume <= 0) return null;
|
|
||||||
|
|
||||||
// If space return early, let that spill go out into the void
|
|
||||||
if (tileRef.Tile.IsEmpty) return null;
|
|
||||||
|
|
||||||
var gridId = tileRef.GridUid;
|
|
||||||
if (!_mapManager.TryGetGrid(gridId, out var mapGrid)) return null; // Let's not spill to invalid grids.
|
|
||||||
|
|
||||||
if (!noTileReact)
|
|
||||||
{
|
|
||||||
// First, do all tile reactions
|
|
||||||
for (var i = 0; i < solution.Contents.Count; i++)
|
|
||||||
{
|
|
||||||
var (reagentId, quantity) = solution.Contents[i];
|
|
||||||
var proto = _prototypeManager.Index<ReagentPrototype>(reagentId);
|
|
||||||
var removed = proto.ReactionTile(tileRef, quantity);
|
|
||||||
if (removed <= FixedPoint2.Zero) continue;
|
|
||||||
solution.RemoveReagent(reagentId, removed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tile reactions used up everything.
|
|
||||||
if (solution.Volume == FixedPoint2.Zero)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// Get normalized co-ordinate for spill location and spill it in the centre
|
|
||||||
// TODO: Does SnapGrid or something else already do this?
|
|
||||||
var spillGridCoords = mapGrid.GridTileToLocal(tileRef.GridIndices);
|
|
||||||
var startEntity = EntityUid.Invalid;
|
|
||||||
PuddleComponent? puddleComponent = null;
|
|
||||||
|
|
||||||
if (combine)
|
|
||||||
{
|
|
||||||
var spillEntities = _entityLookup.GetEntitiesIntersecting(tileRef).ToArray();
|
|
||||||
|
|
||||||
foreach (var spillEntity in spillEntities)
|
|
||||||
{
|
|
||||||
if (!EntityManager.TryGetComponent(spillEntity, out puddleComponent)) continue;
|
|
||||||
|
|
||||||
if (!overflow && _puddleSystem.WouldOverflow(puddleComponent.Owner, solution, puddleComponent))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!_puddleSystem.TryAddSolution(puddleComponent.Owner, solution, sound, overflow)) continue;
|
|
||||||
|
|
||||||
startEntity = puddleComponent.Owner;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startEntity != EntityUid.Invalid)
|
|
||||||
return puddleComponent;
|
|
||||||
|
|
||||||
startEntity = EntityManager.SpawnEntity(prototype, spillGridCoords);
|
|
||||||
puddleComponent = EntityManager.EnsureComponent<PuddleComponent>(startEntity);
|
|
||||||
_puddleSystem.TryAddSolution(startEntity, solution, sound, overflow);
|
|
||||||
|
|
||||||
return puddleComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDoAfter(EntityUid uid, SpillableComponent component, DoAfterEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled || args.Cancelled || args.Args.Target == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
//solution gone by other means before doafter completes
|
|
||||||
if (!_solutionContainerSystem.TryGetDrainableSolution(uid, out var solution) || solution.Volume == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var puddleSolution = _solutionContainerSystem.SplitSolution(uid, solution, solution.Volume);
|
|
||||||
|
|
||||||
SpillAt(puddleSolution, Transform(uid).Coordinates, "PuddleSmear");
|
|
||||||
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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<GrowingKudzuComponent, ComponentAdd>(SetupKudzu);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupKudzu(EntityUid uid, GrowingKudzuComponent component, ComponentAdd args)
|
|
||||||
{
|
|
||||||
if (!EntityManager.TryGetComponent<AppearanceComponent>(uid, out var appearance))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_appearance.SetData(uid, KudzuVisuals.Variant, _robustRandom.Next(1, 3), appearance);
|
|
||||||
_appearance.SetData(uid, KudzuVisuals.GrowthLevel, 1, appearance);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
|
||||||
{
|
|
||||||
_accumulatedFrameTime += frameTime;
|
|
||||||
|
|
||||||
if (!(_accumulatedFrameTime >= 0.5f))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_accumulatedFrameTime -= 0.5f;
|
|
||||||
|
|
||||||
foreach (var (kudzu, appearance) in EntityManager.EntityQuery<GrowingKudzuComponent, AppearanceComponent>())
|
|
||||||
{
|
|
||||||
if (kudzu.GrowthLevel >= 3 || !_robustRandom.Prob(kudzu.GrowthTickSkipChange)) continue;
|
|
||||||
kudzu.GrowthLevel += 1;
|
|
||||||
|
|
||||||
if (kudzu.GrowthLevel == 3 &&
|
|
||||||
EntityManager.TryGetComponent<SpreaderComponent>((kudzu).Owner, out var spreader))
|
|
||||||
{
|
|
||||||
// why cache when you can simply cease to be? Also saves a bit of memory/time.
|
|
||||||
EntityManager.RemoveComponent<GrowingKudzuComponent>((kudzu).Owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
_appearance.SetData(kudzu.Owner, KudzuVisuals.GrowthLevel, kudzu.GrowthLevel, appearance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
namespace Content.Server.Kudzu;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Component for rapidly spreading objects, like Kudzu.
|
|
||||||
/// ONLY USE THIS FOR ANCHORED OBJECTS. An error will be logged if not anchored/static.
|
|
||||||
/// Currently does not support growing in space.
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent, Access(typeof(SpreaderSystem))]
|
|
||||||
public sealed class SpreaderComponent : Component
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Chance for it to grow on any given tick, after the normal growth rate-limit (if it doesn't grow, SpreaderSystem will pick another one.).
|
|
||||||
/// </summary>
|
|
||||||
[DataField("chance", required: true)]
|
|
||||||
public float Chance;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prototype spawned on growth success.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("growthResult", required: true)]
|
|
||||||
public string GrowthResult = default!;
|
|
||||||
|
|
||||||
[DataField("enabled")]
|
|
||||||
public bool Enabled = true;
|
|
||||||
}
|
|
||||||
@@ -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!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum number of edges that can grow out every interval.
|
|
||||||
/// </summary>
|
|
||||||
private const int GrowthsPerInterval = 1;
|
|
||||||
|
|
||||||
private float _accumulatedFrameTime = 0.0f;
|
|
||||||
|
|
||||||
private readonly HashSet<EntityUid> _edgeGrowths = new ();
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
SubscribeLocalEvent<SpreaderComponent, ComponentAdd>(SpreaderAddHandler);
|
|
||||||
SubscribeLocalEvent<AirtightChanged>(OnAirtightChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAirtightChanged(ref AirtightChanged ev)
|
|
||||||
{
|
|
||||||
UpdateNearbySpreaders(ev.Entity, ev.Airtight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SpreaderAddHandler(EntityUid uid, SpreaderComponent component, ComponentAdd args)
|
|
||||||
{
|
|
||||||
if (component.Enabled)
|
|
||||||
_edgeGrowths.Add(uid); // ez
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateNearbySpreaders(EntityUid blocker, AirtightComponent comp)
|
|
||||||
{
|
|
||||||
if (!EntityManager.TryGetComponent<TransformComponent>(blocker, out var transform))
|
|
||||||
return; // how did we get here?
|
|
||||||
|
|
||||||
if (!_mapManager.TryGetGrid(transform.GridUid, out var grid)) return;
|
|
||||||
|
|
||||||
var spreaderQuery = GetEntityQuery<SpreaderComponent>();
|
|
||||||
var tile = grid.TileIndicesFor(transform.Coordinates);
|
|
||||||
|
|
||||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
|
||||||
{
|
|
||||||
var direction = (AtmosDirection) (1 << i);
|
|
||||||
if (!comp.AirBlockedDirection.IsFlagSet(direction)) continue;
|
|
||||||
|
|
||||||
var directionEnumerator =
|
|
||||||
grid.GetAnchoredEntitiesEnumerator(SharedMapSystem.GetDirection(tile, direction.ToDirection()));
|
|
||||||
|
|
||||||
while (directionEnumerator.MoveNext(out var ent))
|
|
||||||
{
|
|
||||||
if (spreaderQuery.TryGetComponent(ent, out var s) && s.Enabled)
|
|
||||||
_edgeGrowths.Add(ent.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
|
||||||
{
|
|
||||||
_accumulatedFrameTime += frameTime;
|
|
||||||
|
|
||||||
if (!(_accumulatedFrameTime >= 1.0f))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_accumulatedFrameTime -= 1.0f;
|
|
||||||
|
|
||||||
var growthList = _edgeGrowths.ToList();
|
|
||||||
_robustRandom.Shuffle(growthList);
|
|
||||||
|
|
||||||
var successes = 0;
|
|
||||||
foreach (var entity in growthList)
|
|
||||||
{
|
|
||||||
if (!TryGrow(entity)) continue;
|
|
||||||
|
|
||||||
successes += 1;
|
|
||||||
if (successes >= GrowthsPerInterval)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGrow(EntityUid ent, TransformComponent? transform = null, SpreaderComponent? spreader = null)
|
|
||||||
{
|
|
||||||
if (!Resolve(ent, ref transform, ref spreader, false))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (spreader.Enabled == false) return false;
|
|
||||||
|
|
||||||
if (!_mapManager.TryGetGrid(transform.GridUid, out var grid)) return false;
|
|
||||||
|
|
||||||
var didGrow = false;
|
|
||||||
|
|
||||||
for (var i = 0; i < 4; i++)
|
|
||||||
{
|
|
||||||
var direction = (DirectionFlag) (1 << i);
|
|
||||||
var coords = transform.Coordinates.Offset(direction.AsDir().ToVec());
|
|
||||||
if (grid.GetTileRef(coords).Tile.IsEmpty || _robustRandom.Prob(1 - spreader.Chance)) continue;
|
|
||||||
var ents = grid.GetLocal(coords);
|
|
||||||
|
|
||||||
if (ents.Any(x => IsTileBlockedFrom(x, direction))) continue;
|
|
||||||
|
|
||||||
// Ok, spawn a plant
|
|
||||||
didGrow = true;
|
|
||||||
EntityManager.SpawnEntity(spreader.GrowthResult, transform.Coordinates.Offset(direction.AsDir().ToVec()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return didGrow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EnableSpreader(EntityUid ent, SpreaderComponent? component = null)
|
|
||||||
{
|
|
||||||
if (!Resolve(ent, ref component))
|
|
||||||
return;
|
|
||||||
component.Enabled = true;
|
|
||||||
_edgeGrowths.Add(ent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsTileBlockedFrom(EntityUid ent, DirectionFlag dir)
|
|
||||||
{
|
|
||||||
if (EntityManager.TryGetComponent<SpreaderComponent>(ent, out _))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (!EntityManager.TryGetComponent<AirtightComponent>(ent, out var airtight))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var oppositeDir = dir.AsDir().GetOpposite().ToAtmosDirection();
|
|
||||||
|
|
||||||
return airtight.AirBlocked && airtight.AirBlockedDirection.IsFlagSet(oppositeDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,7 @@ namespace Content.Server.Medical.BiomassReclaimer
|
|||||||
[Dependency] private readonly SharedAudioSystem _sharedAudioSystem = default!;
|
[Dependency] private readonly SharedAudioSystem _sharedAudioSystem = default!;
|
||||||
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
|
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
|
||||||
[Dependency] private readonly SharedPopupSystem _popup = 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 ThrowingSystem _throwing = default!;
|
||||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||||
@@ -60,7 +60,7 @@ namespace Content.Server.Medical.BiomassReclaimer
|
|||||||
{
|
{
|
||||||
Solution blood = new();
|
Solution blood = new();
|
||||||
blood.AddReagent(reclaimer.BloodReagent, 50);
|
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)
|
if (_robustRandom.Prob(0.03f) && reclaimer.SpawnedEntities.Count > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ using Content.Server.Nutrition.Components;
|
|||||||
using Content.Server.Nutrition.EntitySystems;
|
using Content.Server.Nutrition.EntitySystems;
|
||||||
using Content.Server.Popups;
|
using Content.Server.Popups;
|
||||||
using Content.Server.Stunnable;
|
using Content.Server.Stunnable;
|
||||||
|
using Content.Shared.Audio;
|
||||||
|
using Content.Shared.Fluids.Components;
|
||||||
using Content.Shared.IdentityManagement;
|
using Content.Shared.IdentityManagement;
|
||||||
using Content.Shared.Nutrition.Components;
|
using Content.Shared.Nutrition.Components;
|
||||||
using Content.Shared.Nutrition.EntitySystems;
|
using Content.Shared.Nutrition.EntitySystems;
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ namespace Content.Server.NodeContainer.EntitySystems
|
|||||||
_mapManager.TryGetGrid(xform.GridUid, out var grid);
|
_mapManager.TryGetGrid(xform.GridUid, out var grid);
|
||||||
|
|
||||||
if (!node.Connectable(EntityManager, xform))
|
if (!node.Connectable(EntityManager, xform))
|
||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
foreach (var reachable in node.GetReachableNodes(xform, nodeQuery, xformQuery, grid, EntityManager))
|
foreach (var reachable in node.GetReachableNodes(xform, nodeQuery, xformQuery, grid, EntityManager))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ namespace Content.Server.NodeContainer.NodeGroups
|
|||||||
Apc,
|
Apc,
|
||||||
AMEngine,
|
AMEngine,
|
||||||
Pipe,
|
Pipe,
|
||||||
WireNet
|
WireNet,
|
||||||
|
Spreader,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||||||
public sealed class CreamPieSystem : SharedCreamPieSystem
|
public sealed class CreamPieSystem : SharedCreamPieSystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly SolutionContainerSystem _solutions = default!;
|
[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 ItemSlotsSystem _itemSlots = default!;
|
||||||
[Dependency] private readonly TriggerSystem _trigger = default!;
|
[Dependency] private readonly TriggerSystem _trigger = default!;
|
||||||
[Dependency] private readonly SharedAudioSystem _audio = 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))
|
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))
|
if (!string.IsNullOrEmpty(foodComp.TrashPrototype))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Content.Server.Popups;
|
|||||||
using Content.Shared.Administration.Logs;
|
using Content.Shared.Administration.Logs;
|
||||||
using Content.Shared.Body.Components;
|
using Content.Shared.Body.Components;
|
||||||
using Content.Shared.Chemistry;
|
using Content.Shared.Chemistry;
|
||||||
|
using Content.Shared.Chemistry.Components;
|
||||||
using Content.Shared.Chemistry.Reagent;
|
using Content.Shared.Chemistry.Reagent;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.DoAfter;
|
using Content.Shared.DoAfter;
|
||||||
@@ -44,7 +45,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||||
[Dependency] private readonly MobStateSystem _mobStateSystem = 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 SharedInteractionSystem _interactionSystem = default!;
|
||||||
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
||||||
[Dependency] private readonly SharedAudioSystem _audio = 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))}");
|
args.Message.AddMarkup($"\n{Loc.GetString("drink-component-on-examine-details-text", ("colorName", color), ("text", openedText))}");
|
||||||
if (!IsEmpty(uid, component))
|
if (!IsEmpty(uid, component))
|
||||||
{
|
{
|
||||||
if (TryComp<ExaminableSolutionComponent>(component.Owner, out var comp))
|
if (TryComp<ExaminableSolutionComponent>(uid, out var comp))
|
||||||
{
|
{
|
||||||
//provide exact measurement for beakers
|
//provide exact measurement for beakers
|
||||||
args.Message.AddMarkup($" - {Loc.GetString("drink-component-on-examine-exact-volume", ("amount", _solutionContainerSystem.DrainAvailable(uid)))}");
|
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);
|
UpdateAppearance(component);
|
||||||
|
|
||||||
var solution = _solutionContainerSystem.Drain(uid, interactions, interactions.Volume);
|
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));
|
_audio.PlayPvs(_audio.GetSound(component.BurstSound), uid, AudioParams.Default.WithVolume(-4));
|
||||||
}
|
}
|
||||||
@@ -314,7 +315,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||||||
|
|
||||||
if (HasComp<RefillableSolutionComponent>(args.Args.Target.Value))
|
if (HasComp<RefillableSolutionComponent>(args.Args.Target.Value))
|
||||||
{
|
{
|
||||||
_spillableSystem.SpillAt(args.Args.User, drained, "PuddleSmear");
|
_puddleSystem.TrySpillAt(args.Args.User, drained, out _);
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -334,7 +335,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||||||
if (forceDrink)
|
if (forceDrink)
|
||||||
{
|
{
|
||||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough-other"), args.Args.Target.Value, args.Args.User);
|
_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
|
else
|
||||||
_solutionContainerSystem.TryAddSolution(uid, solution, drained);
|
_solutionContainerSystem.TryAddSolution(uid, solution, drained);
|
||||||
|
|||||||
10
Content.Server/Spreader/EdgeSpreaderComponent.cs
Normal file
10
Content.Server/Spreader/EdgeSpreaderComponent.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Content.Server.Spreader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Added to entities being considered for spreading via <see cref="SpreaderSystem"/>.
|
||||||
|
/// This needs to be manually added and removed.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, Access(typeof(SpreaderSystem))]
|
||||||
|
public sealed class EdgeSpreaderComponent : Component
|
||||||
|
{
|
||||||
|
}
|
||||||
12
Content.Server/Spreader/EdgeSpreaderPrototype.cs
Normal file
12
Content.Server/Spreader/EdgeSpreaderPrototype.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Server.Spreader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds this node group to <see cref="SpreaderSystem"/> for tick updates.
|
||||||
|
/// </summary>
|
||||||
|
[Prototype("edgeSpreader")]
|
||||||
|
public sealed class EdgeSpreaderPrototype : IPrototype
|
||||||
|
{
|
||||||
|
[IdDataField] public string ID { get; } = string.Empty;
|
||||||
|
}
|
||||||
22
Content.Server/Spreader/GrowingKudzuComponent.cs
Normal file
22
Content.Server/Spreader/GrowingKudzuComponent.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||||
|
|
||||||
|
namespace Content.Server.Spreader;
|
||||||
|
|
||||||
|
[RegisterComponent, Access(typeof(KudzuSystem))]
|
||||||
|
public sealed class GrowingKudzuComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// At level 3 spreading can occur; prior to that we have a chance of increasing our growth level and changing our sprite.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("growthLevel")]
|
||||||
|
public int GrowthLevel = 1;
|
||||||
|
|
||||||
|
[DataField("growthTickChance")]
|
||||||
|
public float GrowthTickChance = 1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The next time kudzu will try to tick its growth level.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("nextTick", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
||||||
|
public TimeSpan NextTick = TimeSpan.Zero;
|
||||||
|
}
|
||||||
14
Content.Server/Spreader/KudzuComponent.cs
Normal file
14
Content.Server/Spreader/KudzuComponent.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Content.Server.Spreader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles entities that spread out when they reach the relevant growth level.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class KudzuComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Chance to spread whenever an edge spread is possible.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("spreadChance")]
|
||||||
|
public float SpreadChance = 1f;
|
||||||
|
}
|
||||||
114
Content.Server/Spreader/KudzuSystem.cs
Normal file
114
Content.Server/Spreader/KudzuSystem.cs
Normal file
@@ -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";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<KudzuComponent, ComponentStartup>(SetupKudzu);
|
||||||
|
SubscribeLocalEvent<KudzuComponent, SpreadNeighborsEvent>(OnKudzuSpread);
|
||||||
|
SubscribeLocalEvent<GrowingKudzuComponent, EntityUnpausedEvent>(OnKudzuUnpaused);
|
||||||
|
SubscribeLocalEvent<SpreadGroupUpdateRate>(OnKudzuUpdateRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnKudzuSpread(EntityUid uid, KudzuComponent component, ref SpreadNeighborsEvent args)
|
||||||
|
{
|
||||||
|
if (TryComp<GrowingKudzuComponent>(uid, out var growing) && growing.GrowthLevel < 3)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.NeighborFreeTiles.Count == 0 || args.Grid == null)
|
||||||
|
{
|
||||||
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prototype = MetaData(uid).EntityPrototype?.ID;
|
||||||
|
|
||||||
|
if (prototype == null)
|
||||||
|
{
|
||||||
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_robustRandom.Prob(component.SpreadChance))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var neighbor in args.NeighborFreeTiles)
|
||||||
|
{
|
||||||
|
var neighborUid = Spawn(prototype, args.Grid.GridTileToLocal(neighbor));
|
||||||
|
EnsureComp<EdgeSpreaderComponent>(neighborUid);
|
||||||
|
args.Updates--;
|
||||||
|
|
||||||
|
if (args.Updates <= 0)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnKudzuUpdateRate(ref SpreadGroupUpdateRate args)
|
||||||
|
{
|
||||||
|
if (args.Name != KudzuGroup)
|
||||||
|
return;
|
||||||
|
|
||||||
|
args.UpdatesPerSecond = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnKudzuUnpaused(EntityUid uid, GrowingKudzuComponent component, ref EntityUnpausedEvent args)
|
||||||
|
{
|
||||||
|
component.NextTick += args.PausedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupKudzu(EntityUid uid, KudzuComponent component, ComponentStartup args)
|
||||||
|
{
|
||||||
|
if (!EntityManager.TryGetComponent<AppearanceComponent>(uid, out var appearance))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_appearance.SetData(uid, KudzuVisuals.Variant, _robustRandom.Next(1, 3), appearance);
|
||||||
|
_appearance.SetData(uid, KudzuVisuals.GrowthLevel, 1, appearance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
var query = EntityQueryEnumerator<GrowingKudzuComponent, AppearanceComponent>();
|
||||||
|
var curTime = _timing.CurTime;
|
||||||
|
|
||||||
|
while (query.MoveNext(out var uid, out var kudzu, out var appearance))
|
||||||
|
{
|
||||||
|
if (kudzu.NextTick > curTime)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
kudzu.NextTick = curTime + TimeSpan.FromSeconds(0.5);
|
||||||
|
|
||||||
|
if (!_robustRandom.Prob(kudzu.GrowthTickChance))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
kudzu.GrowthLevel += 1;
|
||||||
|
|
||||||
|
if (kudzu.GrowthLevel >= 3)
|
||||||
|
{
|
||||||
|
// why cache when you can simply cease to be? Also saves a bit of memory/time.
|
||||||
|
RemCompDeferred<GrowingKudzuComponent>(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
_appearance.SetData(uid, KudzuVisuals.GrowthLevel, kudzu.GrowthLevel, appearance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Content.Server/Spreader/SpreadGroupUpdateRate.cs
Normal file
7
Content.Server/Spreader/SpreadGroupUpdateRate.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Content.Server.Spreader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised every tick to determine how many updates a particular spreading node group is allowed.
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct SpreadGroupUpdateRate(string Name, int UpdatesPerSecond = 16);
|
||||||
23
Content.Server/Spreader/SpreadNeighborsEvent.cs
Normal file
23
Content.Server/Spreader/SpreadNeighborsEvent.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Map.Components;
|
||||||
|
|
||||||
|
namespace Content.Server.Spreader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when trying to spread to neighboring tiles.
|
||||||
|
/// If the spread is no longer able to happen you MUST cancel this event!
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct SpreadNeighborsEvent
|
||||||
|
{
|
||||||
|
public MapGridComponent? Grid;
|
||||||
|
public ValueList<Vector2i> NeighborFreeTiles;
|
||||||
|
public ValueList<Vector2i> NeighborOccupiedTiles;
|
||||||
|
public ValueList<EntityUid> Neighbors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many updates allowed are remaining.
|
||||||
|
/// Subscribers can handle as they wish.
|
||||||
|
/// </summary>
|
||||||
|
public int Updates;
|
||||||
|
}
|
||||||
10
Content.Server/Spreader/SpreaderGridComponent.cs
Normal file
10
Content.Server/Spreader/SpreaderGridComponent.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
33
Content.Server/Spreader/SpreaderNode.cs
Normal file
33
Content.Server/Spreader/SpreaderNode.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Content.Server.NodeContainer;
|
||||||
|
using Content.Server.NodeContainer.Nodes;
|
||||||
|
using Robust.Shared.Map.Components;
|
||||||
|
|
||||||
|
namespace Content.Server.Spreader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the node for <see cref="EdgeSpreaderComponent"/>.
|
||||||
|
/// Functions as a generic tile-based entity spreader for systems such as puddles or smoke.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SpreaderNode : Node
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform, EntityQuery<NodeContainerComponent> nodeQuery, EntityQuery<TransformComponent> xformQuery,
|
||||||
|
MapGridComponent? grid, IEntityManager entMan)
|
||||||
|
{
|
||||||
|
if (grid == null)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
entMan.System<SpreaderSystem>().GetNeighbors(xform.Owner, Name, out _, out _, out var neighbors);
|
||||||
|
|
||||||
|
foreach (var neighbor in neighbors)
|
||||||
|
{
|
||||||
|
if (!nodeQuery.TryGetComponent(neighbor, out var nodeContainer) ||
|
||||||
|
!nodeContainer.TryGetNode<SpreaderNode>(Name, out var neighborNode))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return neighborNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Content.Server/Spreader/SpreaderNodeGroup.cs
Normal file
31
Content.Server/Spreader/SpreaderNodeGroup.cs
Normal file
@@ -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!;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Initialize(Node sourceNode, IEntityManager entMan)
|
||||||
|
{
|
||||||
|
base.Initialize(sourceNode, entMan);
|
||||||
|
_entManager = entMan;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void RemoveNode(Node node)
|
||||||
|
{
|
||||||
|
base.RemoveNode(node);
|
||||||
|
|
||||||
|
foreach (var neighborNode in node.ReachableNodes)
|
||||||
|
{
|
||||||
|
if (_entManager.Deleted(neighborNode.Owner))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_entManager.EnsureComponent<EdgeSpreaderComponent>(neighborNode.Owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
324
Content.Server/Spreader/SpreaderSystem.cs
Normal file
324
Content.Server/Spreader/SpreaderSystem.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles generic spreading logic, where one anchored entity spreads to neighboring tiles.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SpreaderSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||||
|
|
||||||
|
private static readonly TimeSpan SpreadCooldown = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
|
private readonly List<string> _spreaderGroups = new();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<AirtightChanged>(OnAirtightChanged);
|
||||||
|
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
|
||||||
|
SubscribeLocalEvent<SpreaderGridComponent, EntityUnpausedEvent>(OnGridUnpaused);
|
||||||
|
|
||||||
|
SetupPrototypes();
|
||||||
|
_prototype.PrototypesReloaded += OnPrototypeReload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Shutdown()
|
||||||
|
{
|
||||||
|
base.Shutdown();
|
||||||
|
_prototype.PrototypesReloaded -= OnPrototypeReload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPrototypeReload(PrototypesReloadedEventArgs obj)
|
||||||
|
{
|
||||||
|
if (!obj.ByType.ContainsKey(typeof(EdgeSpreaderPrototype)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
SetupPrototypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupPrototypes()
|
||||||
|
{
|
||||||
|
_spreaderGroups.Clear();
|
||||||
|
|
||||||
|
foreach (var id in _prototype.EnumeratePrototypes<EdgeSpreaderPrototype>())
|
||||||
|
{
|
||||||
|
_spreaderGroups.Add(id.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAirtightChanged(ref AirtightChanged ev)
|
||||||
|
{
|
||||||
|
var neighbors = GetNeighbors(ev.Entity, ev.Airtight);
|
||||||
|
|
||||||
|
foreach (var neighbor in neighbors)
|
||||||
|
{
|
||||||
|
EnsureComp<EdgeSpreaderComponent>(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGridUnpaused(EntityUid uid, SpreaderGridComponent component, ref EntityUnpausedEvent args)
|
||||||
|
{
|
||||||
|
component.NextUpdate += args.PausedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGridInit(GridInitializeEvent ev)
|
||||||
|
{
|
||||||
|
var comp = EnsureComp<SpreaderGridComponent>(ev.EntityUid);
|
||||||
|
var nextUpdate = _timing.CurTime;
|
||||||
|
|
||||||
|
// TODO: I believe we need grid mapinit events so we can set the time correctly only on mapinit
|
||||||
|
// and not touch it on regular init.
|
||||||
|
if (comp.NextUpdate < nextUpdate)
|
||||||
|
comp.NextUpdate = nextUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
var curTime = _timing.CurTime;
|
||||||
|
|
||||||
|
// Check which grids are valid for spreading.
|
||||||
|
var spreadable = new ValueList<EntityUid>();
|
||||||
|
var spreadGrids = EntityQueryEnumerator<SpreaderGridComponent>();
|
||||||
|
|
||||||
|
while (spreadGrids.MoveNext(out var uid, out var grid))
|
||||||
|
{
|
||||||
|
if (grid.NextUpdate > curTime)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
spreadable.Add(uid);
|
||||||
|
grid.NextUpdate += SpreadCooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spreadable.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var query = EntityQueryEnumerator<EdgeSpreaderComponent>();
|
||||||
|
var nodeQuery = GetEntityQuery<NodeContainerComponent>();
|
||||||
|
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||||
|
var gridQuery = GetEntityQuery<SpreaderGridComponent>();
|
||||||
|
|
||||||
|
// Events and stuff
|
||||||
|
var groupUpdates = new Dictionary<INodeGroup, int>();
|
||||||
|
var spreaders = new List<(EntityUid Uid, EdgeSpreaderComponent Comp)>(Count<EdgeSpreaderComponent>());
|
||||||
|
|
||||||
|
while (query.MoveNext(out var uid, out var comp))
|
||||||
|
{
|
||||||
|
spreaders.Add((uid, comp));
|
||||||
|
}
|
||||||
|
|
||||||
|
_robustRandom.Shuffle(spreaders);
|
||||||
|
|
||||||
|
foreach (var (uid, comp) in spreaders)
|
||||||
|
{
|
||||||
|
if (!xformQuery.TryGetComponent(uid, out var xform) ||
|
||||||
|
xform.GridUid == null ||
|
||||||
|
!gridQuery.HasComponent(xform.GridUid.Value))
|
||||||
|
{
|
||||||
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var sGroup in _spreaderGroups)
|
||||||
|
{
|
||||||
|
// Cleanup
|
||||||
|
if (!nodeQuery.TryGetComponent(uid, out var nodeComponent))
|
||||||
|
{
|
||||||
|
RemCompDeferred<EdgeSpreaderComponent>(uid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeComponent.TryGetNode<SpreaderNode>(sGroup, out var node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Not allowed this tick?
|
||||||
|
if (node.NodeGroup == null ||
|
||||||
|
!spreadable.Contains(xform.GridUid.Value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// While we could check if it's an edge here the subscribing system may have its own definition
|
||||||
|
// of an edge so we'll let them handle it.
|
||||||
|
if (!groupUpdates.TryGetValue(node.NodeGroup, out var updates))
|
||||||
|
{
|
||||||
|
var spreadEv = new SpreadGroupUpdateRate()
|
||||||
|
{
|
||||||
|
Name = node.Name,
|
||||||
|
};
|
||||||
|
RaiseLocalEvent(ref spreadEv);
|
||||||
|
updates = (int) (spreadEv.UpdatesPerSecond * SpreadCooldown / TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Spread(uid, node, node.NodeGroup, ref updates);
|
||||||
|
groupUpdates[node.NodeGroup] = updates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Spread(EntityUid uid, SpreaderNode node, INodeGroup group, ref int updates)
|
||||||
|
{
|
||||||
|
GetNeighbors(uid, node.Name, out var freeTiles, out var occupiedTiles, out var neighbors);
|
||||||
|
|
||||||
|
TryComp<MapGridComponent>(Transform(uid).GridUid, out var grid);
|
||||||
|
|
||||||
|
var ev = new SpreadNeighborsEvent()
|
||||||
|
{
|
||||||
|
Grid = grid,
|
||||||
|
NeighborFreeTiles = freeTiles,
|
||||||
|
NeighborOccupiedTiles = occupiedTiles,
|
||||||
|
Neighbors = neighbors,
|
||||||
|
Updates = updates,
|
||||||
|
};
|
||||||
|
|
||||||
|
RaiseLocalEvent(uid, ref ev);
|
||||||
|
updates = ev.Updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the neighboring node data for the specified entity and the specified node group.
|
||||||
|
/// </summary>
|
||||||
|
public void GetNeighbors(EntityUid uid, string groupName, out ValueList<Vector2i> freeTiles, out ValueList<Vector2i> occupiedTiles, out ValueList<EntityUid> neighbors)
|
||||||
|
{
|
||||||
|
freeTiles = new ValueList<Vector2i>();
|
||||||
|
occupiedTiles = new ValueList<Vector2i>();
|
||||||
|
neighbors = new ValueList<EntityUid>();
|
||||||
|
|
||||||
|
if (!EntityManager.TryGetComponent<TransformComponent>(uid, out var transform))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_mapManager.TryGetGrid(transform.GridUid, out var grid))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var tile = grid.TileIndicesFor(transform.Coordinates);
|
||||||
|
var nodeQuery = GetEntityQuery<NodeContainerComponent>();
|
||||||
|
var airtightQuery = GetEntityQuery<AirtightComponent>();
|
||||||
|
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
var direction = (Direction) (i * 2);
|
||||||
|
var neighborPos = SharedMapSystem.GetDirection(tile, direction);
|
||||||
|
|
||||||
|
if (!grid.TryGetTileRef(neighborPos, out var tileRef) || tileRef.Tile.IsEmpty)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var directionEnumerator =
|
||||||
|
grid.GetAnchoredEntitiesEnumerator(neighborPos);
|
||||||
|
var occupied = false;
|
||||||
|
|
||||||
|
while (directionEnumerator.MoveNext(out var ent))
|
||||||
|
{
|
||||||
|
if (airtightQuery.TryGetComponent(ent, out var airtight) && airtight.AirBlocked)
|
||||||
|
{
|
||||||
|
// Check if air direction matters.
|
||||||
|
var blocked = false;
|
||||||
|
|
||||||
|
foreach (var value in new[] { AtmosDirection.North, AtmosDirection.East})
|
||||||
|
{
|
||||||
|
if ((value & airtight.AirBlockedDirection) == 0x0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var airDirection = value.ToDirection();
|
||||||
|
var oppositeDirection = value.ToDirection();
|
||||||
|
|
||||||
|
if (direction != airDirection && direction != oppositeDirection)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
blocked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!blocked)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
occupied = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (occupied)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var oldCount = occupiedTiles.Count;
|
||||||
|
directionEnumerator =
|
||||||
|
grid.GetAnchoredEntitiesEnumerator(neighborPos);
|
||||||
|
|
||||||
|
while (directionEnumerator.MoveNext(out var ent))
|
||||||
|
{
|
||||||
|
if (!nodeQuery.TryGetComponent(ent, out var nodeContainer))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!nodeContainer.Nodes.ContainsKey(groupName))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
neighbors.Add(ent.Value);
|
||||||
|
occupiedTiles.Add(neighborPos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldCount == occupiedTiles.Count)
|
||||||
|
freeTiles.Add(neighborPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EntityUid> GetNeighbors(EntityUid uid, AirtightComponent comp)
|
||||||
|
{
|
||||||
|
var neighbors = new List<EntityUid>();
|
||||||
|
|
||||||
|
if (!EntityManager.TryGetComponent<TransformComponent>(uid, out var transform))
|
||||||
|
return neighbors; // how did we get here?
|
||||||
|
|
||||||
|
if (!_mapManager.TryGetGrid(transform.GridUid, out var grid))
|
||||||
|
return neighbors;
|
||||||
|
|
||||||
|
var tile = grid.TileIndicesFor(transform.Coordinates);
|
||||||
|
var nodeQuery = GetEntityQuery<NodeContainerComponent>();
|
||||||
|
|
||||||
|
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||||
|
{
|
||||||
|
var direction = (AtmosDirection) (1 << i);
|
||||||
|
if (!comp.AirBlockedDirection.IsFlagSet(direction))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var directionEnumerator =
|
||||||
|
grid.GetAnchoredEntitiesEnumerator(SharedMapSystem.GetDirection(tile, direction.ToDirection()));
|
||||||
|
|
||||||
|
while (directionEnumerator.MoveNext(out var ent))
|
||||||
|
{
|
||||||
|
if (!nodeQuery.TryGetComponent(ent, out var nodeContainer))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var name in _spreaderGroups)
|
||||||
|
{
|
||||||
|
if (!nodeContainer.Nodes.ContainsKey(name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
neighbors.Add(ent.Value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return neighbors;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Content.Server.Atmos.Piping.Unary.Components;
|
using Content.Server.Atmos.Piping.Unary.Components;
|
||||||
using Content.Server.Chemistry.ReactionEffects;
|
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared.Chemistry.Components;
|
||||||
using Content.Shared.Chemistry.Reagent;
|
using Content.Shared.Chemistry.Reagent;
|
||||||
@@ -7,6 +6,9 @@ using JetBrains.Annotations;
|
|||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Server.Chemistry.Components;
|
||||||
|
using Content.Server.Fluids.EntitySystems;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
|
||||||
namespace Content.Server.StationEvents.Events;
|
namespace Content.Server.StationEvents.Events;
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ public sealed class VentClog : StationEventSystem
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var solution = new Solution();
|
var solution = new Solution();
|
||||||
|
|
||||||
if (!RobustRandom.Prob(Math.Min(0.33f * mod, 1.0f)))
|
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)))
|
if (RobustRandom.Prob(Math.Min(0.05f * mod, 1.0f)))
|
||||||
{
|
{
|
||||||
solution.AddReagent(RobustRandom.Pick(allReagents), 100);
|
solution.AddReagent(RobustRandom.Pick(allReagents), 200);
|
||||||
}
|
}
|
||||||
else
|
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,
|
var foamEnt = Spawn("Foam", transform.Coordinates);
|
||||||
1, sound, EntityManager);
|
var smoke = EnsureComp<SmokeComponent>(foamEnt);
|
||||||
|
smoke.SpreadAmount = 20;
|
||||||
|
EntityManager.System<SmokeSystem>().Start(foamEnt, smoke, solution, 20f);
|
||||||
|
EntityManager.System<AudioSystem>().PlayPvs(sound, transform.Coordinates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Threading;
|
|||||||
using Content.Server.Fluids.Components;
|
using Content.Server.Fluids.Components;
|
||||||
using Content.Server.Tools.Components;
|
using Content.Server.Tools.Components;
|
||||||
using Content.Shared.DoAfter;
|
using Content.Shared.DoAfter;
|
||||||
|
using Content.Shared.Fluids.Components;
|
||||||
using Content.Shared.Interaction;
|
using Content.Shared.Interaction;
|
||||||
using Content.Shared.Maps;
|
using Content.Shared.Maps;
|
||||||
using Content.Shared.Tools.Components;
|
using Content.Shared.Tools.Components;
|
||||||
|
|||||||
@@ -221,9 +221,20 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
|
|||||||
return Math.Clamp(chance, 0f, 1f);
|
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)
|
private void OnChemicalInjectorHit(EntityUid owner, MeleeChemicalInjectorComponent comp, MeleeHitEvent args)
|
||||||
|
|||||||
@@ -14,12 +14,6 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
|||||||
[RegisterComponent, Access(typeof(ChemicalPuddleArtifactSystem))]
|
[RegisterComponent, Access(typeof(ChemicalPuddleArtifactSystem))]
|
||||||
public sealed class ChemicalPuddleArtifactComponent : Component
|
public sealed class ChemicalPuddleArtifactComponent : Component
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The prototype id of the puddle
|
|
||||||
/// </summary>
|
|
||||||
[DataField("puddlePrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
public string PuddlePrototype = "PuddleSmear";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The solution where all the chemicals are stored
|
/// The solution where all the chemicals are stored
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public sealed class ChemicalPuddleArtifactSystem : EntitySystem
|
|||||||
{
|
{
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
[Dependency] private readonly ArtifactSystem _artifact = default!;
|
[Dependency] private readonly ArtifactSystem _artifact = default!;
|
||||||
[Dependency] private readonly SpillableSystem _spillable = default!;
|
[Dependency] private readonly PuddleSystem _puddle = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The key for the node data entry containing
|
/// The key for the node data entry containing
|
||||||
@@ -49,6 +49,6 @@ public sealed class ChemicalPuddleArtifactSystem : EntitySystem
|
|||||||
component.ChemicalSolution.AddReagent(reagent, amountPerChem);
|
component.ChemicalSolution.AddReagent(reagent, amountPerChem);
|
||||||
}
|
}
|
||||||
|
|
||||||
_spillable.SpillAt(uid, component.ChemicalSolution, component.PuddlePrototype);
|
_puddle.TrySpillAt(uid, component.ChemicalSolution, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Server.Chemistry.Components;
|
||||||
using Content.Server.Chemistry.ReactionEffects;
|
using Content.Server.Chemistry.ReactionEffects;
|
||||||
|
using Content.Server.Fluids.EntitySystems;
|
||||||
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
||||||
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
|
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
|
||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared.Chemistry.Components;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
|
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
|
||||||
@@ -33,10 +36,12 @@ public sealed class FoamArtifactSystem : EntitySystem
|
|||||||
|
|
||||||
var sol = new Solution();
|
var sol = new Solution();
|
||||||
var xform = Transform(uid);
|
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,
|
var foamEnt = Spawn("Foam", xform.Coordinates);
|
||||||
_random.Next(component.MinFoamAmount, component.MaxFoamAmount), component.Duration,
|
var smoke = EnsureComp<SmokeComponent>(foamEnt);
|
||||||
component.SpreadDuration, component.SpreadDuration, entityManager: EntityManager);
|
EntityManager.System<SmokeSystem>().Start(foamEnt, smoke, sol, 20f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ namespace Content.Shared.Access.Components
|
|||||||
|
|
||||||
[DataField("jobTitle")]
|
[DataField("jobTitle")]
|
||||||
[AutoNetworkedField]
|
[AutoNetworkedField]
|
||||||
|
[Access(typeof(SharedIdCardSystem), typeof(SharedPDASystem), typeof(SharedAgentIdCardSystem),
|
||||||
|
Other = AccessPermissions.ReadWrite)]
|
||||||
public string? JobTitle;
|
public string? JobTitle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
|
namespace Content.Shared.Chemistry.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Denotes the solution that can be easily removed through any reagent container.
|
||||||
|
/// Think pouring this or draining from a water tank.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
public sealed class DrainableSolutionComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Solution name that can be drained.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("solution")]
|
||||||
|
public string Solution { get; set; } = "default";
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Content.Shared.FixedPoint;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
|
namespace Content.Shared.Chemistry.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reagents that can be added easily. For example like
|
||||||
|
/// pouring something into another beaker, glass, or into the gas
|
||||||
|
/// tank of a car.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
public sealed class RefillableSolutionComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Solution name that can added to easily.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("solution")]
|
||||||
|
public string Solution { get; set; } = "default";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum amount that can be transferred to the solution at once
|
||||||
|
/// </summary>
|
||||||
|
[DataField("maxRefill")]
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public FixedPoint2? MaxRefill { get; set; } = null;
|
||||||
|
}
|
||||||
@@ -432,6 +432,18 @@ namespace Content.Shared.Chemistry.Components
|
|||||||
_heatCapacity = 0;
|
_heatCapacity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Splits a solution without the specified reagent.
|
||||||
|
/// </summary>
|
||||||
|
public Solution SplitSolutionWithout(FixedPoint2 toTake, string without)
|
||||||
|
{
|
||||||
|
TryGetReagent(without, out var existing);
|
||||||
|
RemoveReagent(without, toTake);
|
||||||
|
var sol = SplitSolution(toTake);
|
||||||
|
AddReagent(without, existing);
|
||||||
|
return sol;
|
||||||
|
}
|
||||||
|
|
||||||
public Solution SplitSolution(FixedPoint2 toTake)
|
public Solution SplitSolution(FixedPoint2 toTake)
|
||||||
{
|
{
|
||||||
if (toTake <= FixedPoint2.Zero)
|
if (toTake <= FixedPoint2.Zero)
|
||||||
@@ -599,7 +611,7 @@ namespace Content.Shared.Chemistry.Components
|
|||||||
ValidateSolution();
|
ValidateSolution();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color GetColor(IPrototypeManager? protoMan)
|
public Color GetColorWithout(IPrototypeManager? protoMan, params string[] without)
|
||||||
{
|
{
|
||||||
if (Volume == FixedPoint2.Zero)
|
if (Volume == FixedPoint2.Zero)
|
||||||
{
|
{
|
||||||
@@ -614,6 +626,9 @@ namespace Content.Shared.Chemistry.Components
|
|||||||
|
|
||||||
foreach (var reagent in Contents)
|
foreach (var reagent in Contents)
|
||||||
{
|
{
|
||||||
|
if (without.Contains(reagent.ReagentId))
|
||||||
|
continue;
|
||||||
|
|
||||||
runningTotalQuantity += reagent.Quantity;
|
runningTotalQuantity += reagent.Quantity;
|
||||||
|
|
||||||
if (!protoMan.TryIndex(reagent.ReagentId, out ReagentPrototype? proto))
|
if (!protoMan.TryIndex(reagent.ReagentId, out ReagentPrototype? proto))
|
||||||
@@ -634,6 +649,11 @@ namespace Content.Shared.Chemistry.Components
|
|||||||
return mixColor;
|
return mixColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Color GetColor(IPrototypeManager? protoMan)
|
||||||
|
{
|
||||||
|
return GetColorWithout(protoMan);
|
||||||
|
}
|
||||||
|
|
||||||
[Obsolete("Use ReactiveSystem.DoEntityReaction")]
|
[Obsolete("Use ReactiveSystem.DoEntityReaction")]
|
||||||
public void DoEntityReaction(EntityUid uid, ReactionMethod method)
|
public void DoEntityReaction(EntityUid uid, ReactionMethod method)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -250,7 +250,10 @@ namespace Content.Shared.Chemistry.Reaction
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Remove any reactions that were not applicable. Avoids re-iterating over them in future.
|
// 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)
|
if (products.Volume <= 0)
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Content.Shared.Chemistry.Components;
|
|||||||
using Content.Shared.Chemistry.Reaction;
|
using Content.Shared.Chemistry.Reaction;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
@@ -73,6 +74,12 @@ namespace Content.Shared.Chemistry.Reagent
|
|||||||
[DataField("metamorphicSprite")]
|
[DataField("metamorphicSprite")]
|
||||||
public SpriteSpecifier? MetamorphicSprite { get; } = null;
|
public SpriteSpecifier? MetamorphicSprite { get; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If this reagent is part of a puddle is it slippery.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("slippery")]
|
||||||
|
public bool Slippery = false;
|
||||||
|
|
||||||
[DataField("metabolisms", serverOnly: true, customTypeSerializer: typeof(PrototypeIdDictionarySerializer<ReagentEffectsEntry, MetabolismGroupPrototype>))]
|
[DataField("metabolisms", serverOnly: true, customTypeSerializer: typeof(PrototypeIdDictionarySerializer<ReagentEffectsEntry, MetabolismGroupPrototype>))]
|
||||||
public Dictionary<string, ReagentEffectsEntry>? Metabolisms = null;
|
public Dictionary<string, ReagentEffectsEntry>? Metabolisms = null;
|
||||||
|
|
||||||
@@ -85,26 +92,11 @@ namespace Content.Shared.Chemistry.Reagent
|
|||||||
[DataField("plantMetabolism", serverOnly: true)]
|
[DataField("plantMetabolism", serverOnly: true)]
|
||||||
public readonly List<ReagentEffect> PlantMetabolisms = new(0);
|
public readonly List<ReagentEffect> PlantMetabolisms = new(0);
|
||||||
|
|
||||||
[DataField("pricePerUnit")]
|
[DataField("pricePerUnit")] public float PricePerUnit;
|
||||||
public float PricePerUnit { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
// TODO: Pick the highest reagent for sounds and add sticky to cola, juice, etc.
|
||||||
/// If the substance color is too dark we user a lighter version to make the text color readable when the user examines a solution.
|
[DataField("footstepSound")]
|
||||||
/// </summary>
|
public SoundSpecifier FootstepSound = new SoundCollectionSpecifier("FootstepWater");
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume)
|
public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,48 +10,31 @@ namespace Content.Shared.Fluids;
|
|||||||
[RegisterComponent, NetworkedComponent]
|
[RegisterComponent, NetworkedComponent]
|
||||||
public sealed class AbsorbentComponent : Component
|
public sealed class AbsorbentComponent : Component
|
||||||
{
|
{
|
||||||
// TODO: Predicted solutions my beloved.
|
|
||||||
public float Progress;
|
|
||||||
|
|
||||||
public const string SolutionName = "absorbed";
|
public const string SolutionName = "absorbed";
|
||||||
|
|
||||||
|
public Dictionary<Color, float> Progress = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How much solution we can transfer in one interaction.
|
||||||
|
/// </summary>
|
||||||
[DataField("pickupAmount")]
|
[DataField("pickupAmount")]
|
||||||
public FixedPoint2 PickupAmount = FixedPoint2.New(10);
|
public FixedPoint2 PickupAmount = FixedPoint2.New(60);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When using this tool on an empty floor tile, leave this much reagent as a new puddle.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("residueAmount")]
|
|
||||||
public FixedPoint2 ResidueAmount = FixedPoint2.New(10); // Should be higher than MopLowerLimit
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// To leave behind a wet floor, this tool will be unable to take from puddles with a volume less than this
|
|
||||||
/// amount. This limit is ignored if the target puddle does not evaporate.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("lowerLimit")]
|
|
||||||
public FixedPoint2 LowerLimit = FixedPoint2.New(5);
|
|
||||||
|
|
||||||
[DataField("pickupSound")]
|
[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")]
|
[DataField("transferSound")] public SoundSpecifier TransferSound =
|
||||||
public SoundSpecifier TransferSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg");
|
new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg")
|
||||||
|
{
|
||||||
/// <summary>
|
Params = AudioParams.Default.WithVariation(0.05f).WithVolume(-3f),
|
||||||
/// Quantity of reagent that this mop can pick up per second. Determines the length of the do-after.
|
};
|
||||||
/// </summary>
|
|
||||||
[DataField("speed")] public float Speed = 10;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How many entities can this tool interact with at once?
|
|
||||||
/// </summary>
|
|
||||||
[DataField("maxEntities")]
|
|
||||||
public int MaxInteractingEntities = 1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// What entities is this tool interacting with right now?
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables]
|
|
||||||
public HashSet<EntityUid> InteractingEntities = new();
|
|
||||||
|
|
||||||
|
public static readonly SoundSpecifier DefaultTransferSound =
|
||||||
|
new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg")
|
||||||
|
{
|
||||||
|
Params = AudioParams.Default.WithVariation(0.05f).WithVolume(-3f),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
40
Content.Shared/Fluids/Components/DrainComponent.cs
Normal file
40
Content.Shared/Fluids/Components/DrainComponent.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many units per second the drain can absorb from the surrounding puddles.
|
||||||
|
/// Divided by puddles, so if there are 5 puddles this will take 1/5 from each puddle.
|
||||||
|
/// This will stay fixed to 1 second no matter what DrainFrequency is.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("unitsPerSecond")]
|
||||||
|
public float UnitsPerSecond = 6f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many units are ejected from the buffer per second.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("unitsDestroyedPerSecond")]
|
||||||
|
public float UnitsDestroyedPerSecond = 1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many (unobstructed) tiles away the drain will
|
||||||
|
/// drain puddles from.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("range")]
|
||||||
|
public float Range = 2f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How often in seconds the drain checks for puddles around it.
|
||||||
|
/// If the EntityQuery seems a bit unperformant this can be increased.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("drainFrequency")]
|
||||||
|
public float DrainFrequency = 1f;
|
||||||
|
}
|
||||||
21
Content.Shared/Fluids/Components/PuddleComponent.cs
Normal file
21
Content.Shared/Fluids/Components/PuddleComponent.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Content.Shared.FixedPoint;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
|
namespace Content.Shared.Fluids.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Puddle on a floor
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, NetworkedComponent, Access(typeof(SharedPuddleSystem))]
|
||||||
|
public sealed class PuddleComponent : Component
|
||||||
|
{
|
||||||
|
[DataField("spillSound")]
|
||||||
|
public SoundSpecifier SpillSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
|
||||||
|
|
||||||
|
[DataField("overflowVolume")]
|
||||||
|
public FixedPoint2 OverflowVolume = FixedPoint2.New(20);
|
||||||
|
|
||||||
|
[DataField("solution")] public string SolutionName = "puddle";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,7 @@ namespace Content.Shared.Fluids
|
|||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
public enum PuddleVisuals : byte
|
public enum PuddleVisuals : byte
|
||||||
{
|
{
|
||||||
VolumeScale,
|
|
||||||
CurrentVolume,
|
CurrentVolume,
|
||||||
SolutionColor,
|
SolutionColor,
|
||||||
IsEvaporatingVisual
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
using System.Linq;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
namespace Content.Shared.Fluids;
|
namespace Content.Shared.Fluids;
|
||||||
|
|
||||||
public abstract class SharedMoppingSystem : EntitySystem
|
/// <summary>
|
||||||
|
/// Mopping logic for interacting with puddle components.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class SharedAbsorbentSystem : EntitySystem
|
||||||
{
|
{
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -17,23 +21,29 @@ public abstract class SharedMoppingSystem : EntitySystem
|
|||||||
if (args.Current is not AbsorbentComponentState state)
|
if (args.Current is not AbsorbentComponentState state)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (component.Progress.Equals(state.Progress))
|
if (component.Progress.OrderBy(x => x.Key.ToArgb()).SequenceEqual(state.Progress))
|
||||||
return;
|
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)
|
private void OnAbsorbentGetState(EntityUid uid, AbsorbentComponent component, ref ComponentGetState args)
|
||||||
{
|
{
|
||||||
args.State = new AbsorbentComponentState()
|
args.State = new AbsorbentComponentState(component.Progress);
|
||||||
{
|
|
||||||
Progress = component.Progress,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
protected sealed class AbsorbentComponentState : ComponentState
|
protected sealed class AbsorbentComponentState : ComponentState
|
||||||
{
|
{
|
||||||
public float Progress;
|
public Dictionary<Color, float> Progress;
|
||||||
|
|
||||||
|
public AbsorbentComponentState(Dictionary<Color, float> progress)
|
||||||
|
{
|
||||||
|
Progress = progress;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
44
Content.Shared/Fluids/SharedPuddleSystem.cs
Normal file
44
Content.Shared/Fluids/SharedPuddleSystem.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle.
|
||||||
|
/// </summary>
|
||||||
|
public const float LowThreshold = 0.3f;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
SubscribeLocalEvent<RefillableSolutionComponent, CanDragEvent>(OnRefillableCanDrag);
|
||||||
|
SubscribeLocalEvent<RefillableSolutionComponent, CanDropDraggedEvent>(OnRefillableCanDropDragged);
|
||||||
|
SubscribeLocalEvent<DrainableSolutionComponent, CanDropTargetEvent>(OnDrainCanDropTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRefillableCanDrag(EntityUid uid, RefillableSolutionComponent component, ref CanDragEvent args)
|
||||||
|
{
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDrainCanDropTarget(EntityUid uid, DrainableSolutionComponent component, ref CanDropTargetEvent args)
|
||||||
|
{
|
||||||
|
if (HasComp<RefillableSolutionComponent>(args.Dragged))
|
||||||
|
{
|
||||||
|
args.CanDrop = true;
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRefillableCanDropDragged(EntityUid uid, RefillableSolutionComponent component, ref CanDropDraggedEvent args)
|
||||||
|
{
|
||||||
|
if (!HasComp<DrainableSolutionComponent>(args.Target) && !HasComp<DrainComponent>(args.Target))
|
||||||
|
return;
|
||||||
|
|
||||||
|
args.CanDrop = true;
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using Robust.Shared.Serialization;
|
|
||||||
|
|
||||||
namespace Content.Shared.Foam
|
|
||||||
{
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public enum FoamVisuals : byte
|
|
||||||
{
|
|
||||||
State,
|
|
||||||
Color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
Content.Shared/Movement/Events/GetFootstepSoundEvent.cs
Normal file
17
Content.Shared/Movement/Events/GetFootstepSoundEvent.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Robust.Shared.Audio;
|
||||||
|
|
||||||
|
namespace Content.Shared.Movement.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised directed on an entity when trying to get a relevant footstep sound
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct GetFootstepSoundEvent(EntityUid User)
|
||||||
|
{
|
||||||
|
public readonly EntityUid User = User;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the sound to specify a footstep sound and mark as handled.
|
||||||
|
/// </summary>
|
||||||
|
public SoundSpecifier? Sound;
|
||||||
|
}
|
||||||
@@ -466,10 +466,9 @@ namespace Content.Shared.Movement.Systems
|
|||||||
private bool TryGetFootstepSound(TransformComponent xform, bool haveShoes, [NotNullWhen(true)] out SoundSpecifier? sound)
|
private bool TryGetFootstepSound(TransformComponent xform, bool haveShoes, [NotNullWhen(true)] out SoundSpecifier? sound)
|
||||||
{
|
{
|
||||||
sound = null;
|
sound = null;
|
||||||
MapGridComponent? grid;
|
|
||||||
|
|
||||||
// Fallback to the map?
|
// Fallback to the map?
|
||||||
if (xform.GridUid == null)
|
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid))
|
||||||
{
|
{
|
||||||
if (TryComp<FootstepModifierComponent>(xform.MapUid, out var modifier))
|
if (TryComp<FootstepModifierComponent>(xform.MapUid, out var modifier))
|
||||||
{
|
{
|
||||||
@@ -480,8 +479,8 @@ namespace Content.Shared.Movement.Systems
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
grid = _mapManager.GetGrid(xform.GridUid.Value);
|
|
||||||
var position = grid.LocalToTile(xform.Coordinates);
|
var position = grid.LocalToTile(xform.Coordinates);
|
||||||
|
var soundEv = new GetFootstepSoundEvent(xform.Owner);
|
||||||
|
|
||||||
// If the coordinates have a FootstepModifier component
|
// If the coordinates have a FootstepModifier component
|
||||||
// i.e. component that emit sound on footsteps emit that sound
|
// 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))
|
while (anchored.MoveNext(out var maybeFootstep))
|
||||||
{
|
{
|
||||||
|
RaiseLocalEvent(maybeFootstep.Value, ref soundEv);
|
||||||
|
|
||||||
|
if (soundEv.Sound != null)
|
||||||
|
{
|
||||||
|
sound = soundEv.Sound;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (TryComp<FootstepModifierComponent>(maybeFootstep, out var footstep))
|
if (TryComp<FootstepModifierComponent>(maybeFootstep, out var footstep))
|
||||||
{
|
{
|
||||||
sound = footstep.Sound;
|
sound = footstep.Sound;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user