Add Smoke and Foam chemical reaction effects. (#2913)
* Adds smoke reaction effect * smoke tweaks * address reviews * Smoke fix * Refactor smoke and add foam * Fix stuff * Remove thing * Little things * Address some comments * Address more things * More addressing * License stuff * Address refactor request * Small things * Add nullability * Update Content.Server/GameObjects/EntitySystems/SolutionAreaEffectSystem.cs Co-authored-by: Paul Ritter <ritter.paul1@googlemail.com>
This commit is contained in:
@@ -59,5 +59,15 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AreInternalsWorking()
|
||||
{
|
||||
return BreathToolEntity != null &&
|
||||
GasTankEntity != null &&
|
||||
BreathToolEntity.TryGetComponent(out BreathToolComponent? breathTool) &&
|
||||
breathTool.IsFunctional &&
|
||||
GasTankEntity.TryGetComponent(out GasTankComponent? gasTank) &&
|
||||
gasTank.Air != null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#nullable enable
|
||||
using Content.Server.GameObjects.Components.Body.Circulatory;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components.Timers;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SolutionAreaEffectComponent))]
|
||||
public class FoamSolutionAreaEffectComponent : SolutionAreaEffectComponent
|
||||
{
|
||||
public override string Name => "FoamSolutionAreaEffect";
|
||||
|
||||
private string? _foamedMetalPrototype;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _foamedMetalPrototype, "foamedMetalPrototype", null);
|
||||
}
|
||||
|
||||
protected override void UpdateVisuals()
|
||||
{
|
||||
if (Owner.TryGetComponent(out AppearanceComponent? appearance) &&
|
||||
SolutionContainerComponent != null)
|
||||
{
|
||||
appearance.SetData(FoamVisuals.Color, SolutionContainerComponent.Color.WithAlpha(0.80f));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ReactWithEntity(IEntity entity, double solutionFraction)
|
||||
{
|
||||
if (SolutionContainerComponent == null)
|
||||
return;
|
||||
|
||||
if (!entity.TryGetComponent(out BloodstreamComponent? bloodstream))
|
||||
return;
|
||||
|
||||
// TODO: Add a permeability property to clothing
|
||||
// For now it just adds to protection for each clothing equipped
|
||||
var protection = 0f;
|
||||
if (entity.TryGetComponent(out InventoryComponent? inventory))
|
||||
{
|
||||
foreach (var slot in inventory.Slots)
|
||||
{
|
||||
if (slot == EquipmentSlotDefines.Slots.BACKPACK ||
|
||||
slot == EquipmentSlotDefines.Slots.POCKET1 ||
|
||||
slot == EquipmentSlotDefines.Slots.POCKET2 ||
|
||||
slot == EquipmentSlotDefines.Slots.IDCARD)
|
||||
continue;
|
||||
|
||||
if (inventory.TryGetSlotItem(slot, out ItemComponent _))
|
||||
protection += 0.025f;
|
||||
}
|
||||
}
|
||||
|
||||
var cloneSolution = SolutionContainerComponent.Solution.Clone();
|
||||
var transferAmount = ReagentUnit.Min(cloneSolution.TotalVolume * solutionFraction * (1 - protection), bloodstream.EmptyVolume);
|
||||
var transferSolution = cloneSolution.SplitSolution(transferAmount);
|
||||
|
||||
bloodstream.TryTransferSolution(transferSolution);
|
||||
}
|
||||
|
||||
protected override void OnKill()
|
||||
{
|
||||
if (Owner.Deleted)
|
||||
return;
|
||||
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
|
||||
{
|
||||
appearance.SetData(FoamVisuals.State, true);
|
||||
}
|
||||
Owner.SpawnTimer(600, () =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_foamedMetalPrototype))
|
||||
{
|
||||
Owner.EntityManager.SpawnEntity(_foamedMetalPrototype, Owner.Transform.Coordinates);
|
||||
}
|
||||
Owner.Delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Body.Circulatory;
|
||||
using Content.Server.GameObjects.Components.Body.Respiratory;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SolutionAreaEffectComponent))]
|
||||
public class SmokeSolutionAreaEffectComponent : SolutionAreaEffectComponent
|
||||
{
|
||||
public override string Name => "SmokeSolutionAreaEffect";
|
||||
|
||||
protected override void UpdateVisuals()
|
||||
{
|
||||
if (Owner.TryGetComponent(out AppearanceComponent? appearance) &&
|
||||
SolutionContainerComponent != null)
|
||||
{
|
||||
appearance.SetData(SmokeVisuals.Color, SolutionContainerComponent.Color);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ReactWithEntity(IEntity entity, double solutionFraction)
|
||||
{
|
||||
if (SolutionContainerComponent == null)
|
||||
return;
|
||||
|
||||
if (!entity.TryGetComponent(out BloodstreamComponent? bloodstream))
|
||||
return;
|
||||
|
||||
if (entity.TryGetComponent(out InternalsComponent? internals) &&
|
||||
internals.AreInternalsWorking())
|
||||
return;
|
||||
|
||||
var cloneSolution = SolutionContainerComponent.Solution.Clone();
|
||||
var transferAmount = ReagentUnit.Min(cloneSolution.TotalVolume * solutionFraction, bloodstream.EmptyVolume);
|
||||
var transferSolution = cloneSolution.SplitSolution(transferAmount);
|
||||
|
||||
foreach (var reagentQuantity in transferSolution.Contents.ToArray())
|
||||
{
|
||||
if (reagentQuantity.Quantity == ReagentUnit.Zero) continue;
|
||||
var reagent = PrototypeManager.Index<ReagentPrototype>(reagentQuantity.ReagentId);
|
||||
transferSolution.RemoveReagent(reagentQuantity.ReagentId,reagent.ReactionEntity(entity, ReactionMethod.Ingestion, reagentQuantity.Quantity));
|
||||
}
|
||||
|
||||
bloodstream.TryTransferSolution(transferSolution);
|
||||
}
|
||||
|
||||
|
||||
protected override void OnKill()
|
||||
{
|
||||
if (Owner.Deleted)
|
||||
return;
|
||||
Owner.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Atmos;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.ComponentDependencies;
|
||||
using Robust.Shared.GameObjects.Components.Transform;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
/// <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
|
||||
{
|
||||
[Dependency] protected readonly IMapManager MapManager = default!;
|
||||
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
|
||||
|
||||
[ComponentDependency] protected readonly SnapGridComponent? SnapGridComponent = default!;
|
||||
[ComponentDependency] protected readonly SolutionContainerComponent? SolutionContainerComponent = default!;
|
||||
public int Amount { get; set; }
|
||||
public SolutionAreaEffectInceptionComponent? Inception { get; set; }
|
||||
|
||||
/// <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 (Owner.HasComponent<SolutionAreaEffectInceptionComponent>())
|
||||
return;
|
||||
|
||||
Amount = amount;
|
||||
var inception = Owner.AddComponent<SolutionAreaEffectInceptionComponent>();
|
||||
|
||||
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()
|
||||
{
|
||||
if (Owner.Prototype == null)
|
||||
{
|
||||
Logger.Error("AreaEffectComponent needs its owner to be spawned by a prototype.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SnapGridComponent == null)
|
||||
{
|
||||
Logger.Error("AreaEffectComponent attached to " + Owner.Prototype.ID +
|
||||
" couldn't get SnapGridComponent from owner.");
|
||||
return;
|
||||
}
|
||||
|
||||
void SpreadToDir(Direction dir)
|
||||
{
|
||||
foreach (var neighbor in SnapGridComponent.GetInDir(dir))
|
||||
{
|
||||
if (neighbor.TryGetComponent(out SolutionAreaEffectComponent? comp) && comp.Inception == Inception)
|
||||
return;
|
||||
|
||||
if (neighbor.TryGetComponent(out AirtightComponent? airtight) && airtight.AirBlocked)
|
||||
return;
|
||||
}
|
||||
|
||||
var newEffect =
|
||||
Owner.EntityManager.SpawnEntity(Owner.Prototype.ID, SnapGridComponent.DirectionToGrid(dir));
|
||||
|
||||
if (!newEffect.TryGetComponent(out SolutionAreaEffectComponent? effectComponent))
|
||||
{
|
||||
newEffect.Delete();
|
||||
return;
|
||||
}
|
||||
|
||||
if (SolutionContainerComponent != null)
|
||||
{
|
||||
effectComponent.TryAddSolution(SolutionContainerComponent.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 (SolutionContainerComponent == null)
|
||||
return;
|
||||
|
||||
var mapGrid = MapManager.GetGrid(Owner.Transform.GridID);
|
||||
var tile = mapGrid.GetTileRef(Owner.Transform.Coordinates.ToVector2i(Owner.EntityManager, MapManager));
|
||||
|
||||
var solutionFraction = 1 / Math.Floor(averageExposures);
|
||||
|
||||
foreach (var reagentQuantity in SolutionContainerComponent.ReagentList.ToArray())
|
||||
{
|
||||
if (reagentQuantity.Quantity == ReagentUnit.Zero) continue;
|
||||
var reagent = PrototypeManager.Index<ReagentPrototype>(reagentQuantity.ReagentId);
|
||||
|
||||
// React with the tile the effect is on
|
||||
reagent.ReactionTile(tile, reagentQuantity.Quantity * solutionFraction);
|
||||
|
||||
// Touch every entity on the tile
|
||||
foreach (var entity in tile.GetEntitiesInTileFast())
|
||||
{
|
||||
reagent.ReactionEntity(entity, ReactionMethod.Touch, reagentQuantity.Quantity * solutionFraction);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entity in tile.GetEntitiesInTileFast())
|
||||
{
|
||||
ReactWithEntity(entity, solutionFraction);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void ReactWithEntity(IEntity entity, double solutionFraction);
|
||||
|
||||
public void TryAddSolution(Solution solution)
|
||||
{
|
||||
if (solution.TotalVolume == 0)
|
||||
return;
|
||||
|
||||
if (SolutionContainerComponent == null)
|
||||
return;
|
||||
|
||||
var addSolution =
|
||||
solution.SplitSolution(ReagentUnit.Min(solution.TotalVolume, SolutionContainerComponent.EmptyVolume));
|
||||
|
||||
SolutionContainerComponent.TryAddSolution(addSolution);
|
||||
|
||||
UpdateVisuals();
|
||||
}
|
||||
|
||||
protected abstract void UpdateVisuals();
|
||||
|
||||
public override void OnRemove()
|
||||
{
|
||||
base.OnRemove();
|
||||
Inception?.Remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
/// <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 class SolutionAreaEffectInceptionComponent : Component
|
||||
{
|
||||
public override string Name => "AreaEffectInception";
|
||||
|
||||
private const float ReactionDelay = 0.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user