Puddles & spreader refactor (#15191)

This commit is contained in:
metalgearsloth
2023-04-10 15:37:03 +10:00
committed by GitHub
parent 3178ab83f6
commit 317a4013eb
141 changed files with 3046 additions and 3201 deletions

View File

@@ -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);
});
}
}
}

View 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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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";
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -8,6 +8,7 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -127,6 +128,17 @@ public sealed partial class SolutionContainerSystem : EntitySystem
return splitSol;
}
/// <summary>
/// Splits a solution without the specified reagent.
/// </summary>
public Solution SplitSolutionWithout(EntityUid targetUid, Solution solutionHolder, FixedPoint2 quantity,
string reagent)
{
var splitSol = solutionHolder.SplitSolutionWithout(quantity, reagent);
UpdateChemicals(targetUid, solutionHolder);
return splitSol;
}
public void UpdateChemicals(EntityUid uid, Solution solutionHolder, bool needsReactionsProcessing = false, ReactionMixerComponent? mixerComponent = null)
{
DebugTools.Assert(solutionHolder.Name != null && TryGetSolution(uid, solutionHolder.Name, out var tmp) && tmp == solutionHolder);
@@ -491,6 +503,37 @@ public sealed partial class SolutionContainerSystem : EntitySystem
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.

View File

@@ -1,14 +1,18 @@
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Coordinates.Helpers;
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Audio;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Serialization;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Chemistry.ReactionEffects
{
@@ -16,47 +20,23 @@ namespace Content.Server.Chemistry.ReactionEffects
/// Basically smoke and foam reactions.
/// </summary>
[UsedImplicitly]
[ImplicitDataDefinitionForInheritors]
public abstract class AreaReactionEffect : ReagentEffect, ISerializationHooks
[DataDefinition]
public sealed class AreaReactionEffect : ReagentEffect
{
[Dependency] private readonly IMapManager _mapManager = default!;
/// <summary>
/// Used for calculating the spread range of the effect based on the intensity of the reaction.
/// </summary>
[DataField("rangeConstant")] private float _rangeConstant;
[DataField("rangeMultiplier")] private float _rangeMultiplier = 1.1f;
[DataField("maxRange")] private int _maxRange = 10;
/// <summary>
/// If true the reagents get diluted or concentrated depending on the range of the effect
/// </summary>
[DataField("diluteReagents")] private bool _diluteReagents;
/// <summary>
/// Used to calculate dilution. Increasing this makes the reagents more diluted.
/// </summary>
[DataField("reagentDilutionFactor")] private float _reagentDilutionFactor = 1f;
/// <summary>
/// How many seconds will the effect stay, counting after fully spreading.
/// </summary>
[DataField("duration")] private float _duration = 10;
/// <summary>
/// How many seconds between each spread step.
/// How many units of reaction for 1 smoke entity.
/// </summary>
[DataField("spreadDelay")] private float _spreadDelay = 0.5f;
[DataField("overflowThreshold")] public FixedPoint2 OverflowThreshold = FixedPoint2.New(2.5);
/// <summary>
/// How many seconds between each remove step.
/// The entity prototype that will be spawned as the effect.
/// </summary>
[DataField("removeDelay")] private float _removeDelay = 0.5f;
/// <summary>
/// The entity prototype that will be spawned as the effect. It needs a component derived from SolutionAreaEffectComponent.
/// </summary>
[DataField("prototypeId", required: true)]
[DataField("prototypeId", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
private string _prototypeId = default!;
/// <summary>
@@ -67,55 +47,38 @@ namespace Content.Server.Chemistry.ReactionEffects
public override bool ShouldLog => true;
public override LogImpact LogImpact => LogImpact.High;
void ISerializationHooks.AfterDeserialization()
{
IoCManager.InjectDependencies(this);
}
public override void Effect(ReagentEffectArgs args)
{
if (args.Source == null)
return;
var splitSolution = EntitySystem.Get<SolutionContainerSystem>().SplitSolution(args.SolutionEntity, args.Source, args.Source.Volume);
// We take the square root so it becomes harder to reach higher amount values
var amount = (int) Math.Round(_rangeConstant + _rangeMultiplier*Math.Sqrt(args.Quantity.Float()));
amount = Math.Min(amount, _maxRange);
if (_diluteReagents)
{
// The maximum value of solutionFraction is _reagentMaxConcentrationFactor, achieved when amount = 0
// The infimum of solutionFraction is 0, which is approached when amount tends to infinity
// solutionFraction is equal to 1 only when amount equals _reagentDilutionStart
// Weird formulas here but basically when amount increases, solutionFraction gets closer to 0 in a reciprocal manner
// _reagentDilutionFactor defines how fast solutionFraction gets closer to 0
float solutionFraction = 1 / (_reagentDilutionFactor*(amount) + 1);
splitSolution.RemoveSolution(splitSolution.Volume * (1 - solutionFraction));
}
var spreadAmount = (int) Math.Max(0, Math.Ceiling((args.Quantity / OverflowThreshold).Float()));
var splitSolution = args.EntityManager.System<SolutionContainerSystem>().SplitSolution(args.SolutionEntity, args.Source, args.Source.Volume);
var transform = args.EntityManager.GetComponent<TransformComponent>(args.SolutionEntity);
var mapManager = IoCManager.Resolve<IMapManager>();
if (!_mapManager.TryFindGridAt(transform.MapPosition, out var grid)) return;
var coords = grid.MapToGrid(transform.MapPosition);
var ent = args.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid());
var areaEffectComponent = GetAreaEffectComponent(ent);
if (areaEffectComponent == null)
if (!mapManager.TryFindGridAt(transform.MapPosition, out var grid) ||
!grid.TryGetTileRef(transform.Coordinates, out var tileRef) ||
tileRef.Tile.IsSpace())
{
Logger.Error("Couldn't get AreaEffectComponent from " + _prototypeId);
IoCManager.Resolve<IEntityManager>().QueueDeleteEntity(ent);
return;
}
areaEffectComponent.TryAddSolution(splitSolution);
areaEffectComponent.Start(amount, _duration, _spreadDelay, _removeDelay);
var coords = grid.MapToGrid(transform.MapPosition);
var ent = args.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid());
if (!args.EntityManager.TryGetComponent<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));
}
protected abstract SolutionAreaEffectComponent? GetAreaEffectComponent(EntityUid entity);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -1,4 +1,4 @@
using Content.Server.Fluids.EntitySystems;
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
@@ -14,10 +14,11 @@ namespace Content.Server.Chemistry.TileReactions
{
public FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume)
{
var spillSystem = EntitySystem.Get<SpillableSystem>();
if (reactVolume < 5 || !spillSystem.TryGetPuddle(tile, out _)) return FixedPoint2.Zero;
var spillSystem = EntitySystem.Get<PuddleSystem>();
if (reactVolume < 5 || !spillSystem.TryGetPuddle(tile, out _))
return FixedPoint2.Zero;
return spillSystem.SpillAt(tile,new Solution(reagent.ID, reactVolume), "PuddleSmear", true, false, true) != null
return spillSystem.TrySpillAt(tile, new Solution(reagent.ID, reactVolume), out _, sound: false, tileReact: false)
? reactVolume
: FixedPoint2.Zero;
}

View File

@@ -19,7 +19,6 @@ namespace Content.Server.Chemistry.TileReactions
[DataField("launchForwardsMultiplier")] private float _launchForwardsMultiplier = 1;
[DataField("requiredSlipSpeed")] private float _requiredSlipSpeed = 6;
[DataField("paralyzeTime")] private float _paralyzeTime = 1;
[DataField("overflow")] private bool _overflow;
public FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume)
{
@@ -27,19 +26,16 @@ namespace Content.Server.Chemistry.TileReactions
var entityManager = IoCManager.Resolve<IEntityManager>();
// TODO Make this not puddle smear.
var puddle = entityManager.EntitySysManager.GetEntitySystem<SpillableSystem>()
.SpillAt(tile, new Solution(reagent.ID, reactVolume), "PuddleSmear", _overflow, false, true);
if (puddle != null)
if (entityManager.EntitySysManager.GetEntitySystem<PuddleSystem>()
.TrySpillAt(tile, new Solution(reagent.ID, reactVolume), out var puddleUid, false, false))
{
var slippery = entityManager.EnsureComponent<SlipperyComponent>(puddle.Owner);
var slippery = entityManager.EnsureComponent<SlipperyComponent>(puddleUid);
slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier;
slippery.ParalyzeTime = _paralyzeTime;
entityManager.Dirty(slippery);
var step = entityManager.EnsureComponent<StepTriggerComponent>(puddle.Owner);
entityManager.EntitySysManager.GetEntitySystem<StepTriggerSystem>().SetRequiredTriggerSpeed(puddle.Owner, _requiredSlipSpeed, step);
var step = entityManager.EnsureComponent<StepTriggerComponent>(puddleUid);
entityManager.EntitySysManager.GetEntitySystem<StepTriggerSystem>().SetRequiredTriggerSpeed(puddleUid, _requiredSlipSpeed, step);
return reactVolume;
}