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:
Radrark
2021-02-03 11:26:46 -03:00
committed by GitHub
parent 684ec60be6
commit 937e261867
30 changed files with 1123 additions and 2 deletions

View File

@@ -0,0 +1,74 @@
#nullable enable
using System;
using Content.Shared.GameObjects.Components.Chemistry;
using JetBrains.Annotations;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.GameObjects.Components.Animations;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Client.GameObjects.Components.Chemistry
{
[UsedImplicitly]
public class FoamVisualizer : AppearanceVisualizer
{
private const string AnimationKey = "foamdissolve_animation";
private Animation _foamDissolve = new();
public override void LoadData(YamlMappingNode node)
{
base.LoadData(node);
var delay = 0.6f;
var state = "foam-dissolve";
if (node.TryGetNode("animationTime", out var delayNode))
{
delay = delayNode.AsFloat();
}
if (node.TryGetNode("animationState", out var stateNode))
{
state = stateNode.AsString();
}
_foamDissolve = new Animation {Length = TimeSpan.FromSeconds(delay)};
var flick = new AnimationTrackSpriteFlick();
_foamDissolve.AnimationTracks.Add(flick);
flick.LayerKey = FoamVisualLayers.Base;
flick.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame(state, 0f));
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (component.TryGetData<bool>(FoamVisuals.State, out var state))
{
if (state)
{
if (component.Owner.TryGetComponent(out AnimationPlayerComponent? animPlayer))
{
if (!animPlayer.HasRunningAnimation(AnimationKey))
animPlayer.Play(_foamDissolve, AnimationKey);
}
}
}
if (component.TryGetData<Color>(FoamVisuals.Color, out var color))
{
if (component.Owner.TryGetComponent(out ISpriteComponent? sprite))
{
sprite.Color = color;
}
}
}
}
public enum FoamVisualLayers : byte
{
Base
}
}

View File

@@ -0,0 +1,26 @@
#nullable enable
using Content.Shared.GameObjects.Components.Chemistry;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.Maths;
namespace Content.Client.GameObjects.Components.Chemistry
{
[UsedImplicitly]
public class SmokeVisualizer : AppearanceVisualizer
{
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (component.TryGetData<Color>(SmokeVisuals.Color, out var color))
{
if (component.Owner.TryGetComponent(out ISpriteComponent? sprite))
{
sprite.Color = color;
}
}
}
}
}

View File

@@ -236,6 +236,8 @@ namespace Content.Client
"SliceableFood", "SliceableFood",
"DamageOtherOnHit", "DamageOtherOnHit",
"DamageOnLand", "DamageOnLand",
"SmokeSolutionAreaEffect",
"FoamSolutionAreaEffect",
"GasFilter", "GasFilter",
"Recyclable", "Recyclable",
"SecretStash", "SecretStash",

View File

@@ -0,0 +1,162 @@
#nullable enable
using System;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.Interfaces.Chemistry;
using Content.Server.Utility;
using Content.Shared.Audio;
using JetBrains.Annotations;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
namespace Content.Server.Chemistry.ReactionEffects
{
/// <summary>
/// Basically smoke and foam reactions.
/// </summary>
[UsedImplicitly]
public abstract class AreaReactionEffect : IReactionEffect
{
[Dependency] private readonly IMapManager _mapManager = default!;
/// <summary>
/// Used for calculating the spread range of the effect based on the intensity of the reaction.
/// </summary>
private float _rangeConstant;
private float _rangeMultiplier;
private int _maxRange;
/// <summary>
/// If true the reagents get diluted or concentrated depending on the range of the effect
/// </summary>
private bool _diluteReagents;
/// <summary>
/// At what range should the reagents volume stay the same. If the effect range is higher than this then the reagents
/// will get diluted. If the effect range is lower than this then the reagents will get concentrated.
/// </summary>
private int _reagentDilutionStart;
/// <summary>
/// Used to calculate dilution. Increasing this makes the reagents get more diluted. This means that a lower range
/// will be needed to make the reagents volume get closer to zero.
/// </summary>
private float _reagentDilutionFactor;
/// <summary>
/// Used to calculate concentration. Reagents get linearly more concentrated as the range goes from
/// _reagentDilutionStart to zero. When the range is zero the reagents volume gets multiplied by this.
/// </summary>
private float _reagentMaxConcentrationFactor;
/// <summary>
/// How many seconds will the effect stay, counting after fully spreading.
/// </summary>
private float _duration;
/// <summary>
/// How many seconds between each spread step.
/// </summary>
private float _spreadDelay;
/// <summary>
/// How many seconds between each remove step.
/// </summary>
private float _removeDelay;
/// <summary>
/// The entity prototype that will be spawned as the effect. It needs a component derived from SolutionAreaEffectComponent.
/// </summary>
private string? _prototypeId;
/// <summary>
/// Sound that will get played when this reaction effect occurs.
/// </summary>
private string? _sound;
protected AreaReactionEffect()
{
IoCManager.InjectDependencies(this);
}
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(ref _rangeConstant, "rangeConstant",0f);
serializer.DataField(ref _rangeMultiplier, "rangeMultiplier",1.1f);
serializer.DataField(ref _maxRange, "maxRange", 10);
serializer.DataField(ref _diluteReagents, "diluteReagents", false);
serializer.DataField(ref _reagentDilutionStart, "reagentDilutionStart", 4);
serializer.DataField(ref _reagentDilutionFactor, "reagentDilutionFactor", 1f);
serializer.DataField(ref _reagentMaxConcentrationFactor, "reagentMaxConcentrationFactor",2f);
serializer.DataField(ref _duration, "duration", 10f);
serializer.DataField(ref _spreadDelay, "spreadDelay", 0.5f);
serializer.DataField(ref _removeDelay, "removeDelay", 0.5f);
serializer.DataField(ref _sound, "sound", null);
serializer.DataField(ref _prototypeId, "prototypeId", null);
if (_prototypeId == null)
Logger.Error("prototypeId wasn't provided to AreaReactionEffect, check yaml");
}
public void React(IEntity solutionEntity, double intensity)
{
if (!solutionEntity.TryGetComponent(out SolutionContainerComponent? contents))
return;
var solution = contents.SplitSolution(contents.MaxVolume);
// We take the square root so it becomes harder to reach higher amount values
var amount = (int) Math.Round(_rangeConstant + _rangeMultiplier*Math.Sqrt(intensity));
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
float solutionFraction;
if (amount >= _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
solutionFraction = 1 / (_reagentDilutionFactor*(amount - _reagentDilutionStart) + 1);
}
else
{
// Here when amount decreases, solutionFraction gets closer to _reagentMaxConcentrationFactor in a linear manner
solutionFraction = amount * (1 - _reagentMaxConcentrationFactor) / _reagentDilutionStart +
_reagentMaxConcentrationFactor;
}
solution.RemoveSolution(solution.TotalVolume*(1-solutionFraction));
}
if (!_mapManager.TryFindGridAt(solutionEntity.Transform.MapPosition, out var grid)) return;
var coords = grid.MapToGrid(solutionEntity.Transform.MapPosition);
var ent = solutionEntity.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid());
var areaEffectComponent = GetAreaEffectComponent(ent);
if (areaEffectComponent == null)
{
Logger.Error("Couldn't get AreaEffectComponent from " + _prototypeId);
ent.Delete();
return;
}
areaEffectComponent.TryAddSolution(solution);
areaEffectComponent.Start(amount, _duration, _spreadDelay, _removeDelay);
if (!string.IsNullOrEmpty(_sound))
{
EntitySystem.Get<AudioSystem>().PlayFromEntity(_sound, solutionEntity, AudioHelpers.WithVariation(0.125f));
}
}
protected abstract SolutionAreaEffectComponent? GetAreaEffectComponent(IEntity entity);
}
}

View File

@@ -0,0 +1,16 @@
#nullable enable
using Content.Server.GameObjects.Components.Chemistry;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.Chemistry.ReactionEffects
{
[UsedImplicitly]
public class FoamAreaReactionEffect : AreaReactionEffect
{
protected override SolutionAreaEffectComponent? GetAreaEffectComponent(IEntity entity)
{
return entity.GetComponentOrNull<FoamSolutionAreaEffectComponent>();
}
}
}

View File

@@ -0,0 +1,16 @@
#nullable enable
using Content.Server.GameObjects.Components.Chemistry;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.Chemistry.ReactionEffects
{
[UsedImplicitly]
public class SmokeAreaReactionEffect : AreaReactionEffect
{
protected override SolutionAreaEffectComponent? GetAreaEffectComponent(IEntity entity)
{
return entity.GetComponentOrNull<SmokeSolutionAreaEffectComponent>();
}
}
}

View File

@@ -59,5 +59,15 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory
return true; 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;
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
#nullable enable
using Content.Server.GameObjects.Components.Chemistry;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class SolutionAreaEffectSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var inception in ComponentManager.EntityQuery<SolutionAreaEffectInceptionComponent>())
{
inception.InceptionUpdate(frameTime);
}
}
}
}

View File

@@ -0,0 +1,13 @@
#nullable enable
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Chemistry
{
[Serializable, NetSerializable]
public enum FoamVisuals : byte
{
State,
Color
}
}

View File

@@ -0,0 +1,12 @@
#nullable enable
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Chemistry
{
[Serializable, NetSerializable]
public enum SmokeVisuals : byte
{
Color
}
}

View File

@@ -22,7 +22,8 @@ namespace Content.Shared.GameObjects
Objects = DrawDepthTag.Default, Objects = DrawDepthTag.Default,
Items = DrawDepthTag.Default + 1, Items = DrawDepthTag.Default + 1,
Mobs = DrawDepthTag.Default + 2, Mobs = DrawDepthTag.Default + 2,
Ghosts = DrawDepthTag.Default + 3, Effects = DrawDepthTag.Default + 3,
Overlays = DrawDepthTag.Default + 4, Ghosts = DrawDepthTag.Default + 4,
Overlays = DrawDepthTag.Default + 5,
} }
} }

View File

@@ -1,2 +1,4 @@
hit_kick.ogg is made by Taira Komori hit_kick.ogg is made by Taira Komori
(https://taira-komori.jpn.org/freesounden.html) (https://taira-komori.jpn.org/freesounden.html)
smoke.ogg taken from https://github.com/tgstation/tgstation/blob/a5d362ce84e4f0c61026236d5ec84d3c81553664/sound/effects/smoke.ogg

Binary file not shown.

View File

@@ -21,6 +21,7 @@
- chem.Water - chem.Water
- chem.Ethanol - chem.Ethanol
- chem.Glucose - chem.Glucose
- chem.Sugar
- chem.Hydrogen - chem.Hydrogen
- chem.Oxygen - chem.Oxygen
- chem.Sulfur - chem.Sulfur

View File

@@ -0,0 +1,149 @@
- type: entity
id: Smoke
name: smoke
abstract: true
components:
- type: Sprite
drawdepth: Effects
sprite: Effects/chemsmoke.rsi
state: chemsmoke
- type: Appearance
visuals:
- type: SmokeVisualizer
- type: Occluder
sizeX: 32
sizeY: 32
- type: SnapGrid
offset: Center
- type: SmokeSolutionAreaEffect
- type: SolutionContainer
maxVol: 600
- type: entity
id: Foam
name: foam
abstract: true
components:
- type: Sprite
netsync: false
drawdepth: Effects
color: "#ffffffcc" #Add some transparency
sprite: Effects/foam.rsi
state: foam
layers:
- state: foam
map: ["enum.FoamVisualLayers.Base"]
- type: AnimationPlayer
- type: Appearance
visuals:
- type: FoamVisualizer
animationTime: 0.6
animationState: foam-dissolve
- type: SnapGrid
offset: Center
- type: Physics
shapes:
- !type:PhysShapeAabb
bounds: "-0.4,-0.4,0.4,0.4"
layer:
- MobImpassable
- type: FoamSolutionAreaEffect
- type: SolutionContainer
maxVol: 600
- type: Slippery
- type: entity
id: IronMetalFoam
name: iron metal foam
abstract: true
parent: Foam
components:
- type: Sprite
state: mfoam
layers:
- state: mfoam
map: ["enum.FoamVisualLayers.Base"]
- type: Appearance
visuals:
- type: FoamVisualizer
animationTime: 0.6
animationState: mfoam-dissolve
- type: FoamSolutionAreaEffect
foamedMetalPrototype: FoamedIronMetal
- type: entity
id: AluminiumMetalFoam
name: aluminium metal foam
abstract: true
parent: Foam
components:
- type: Sprite
state: mfoam
layers:
- state: mfoam
map: ["enum.FoamVisualLayers.Base"]
- type: Appearance
visuals:
- type: FoamVisualizer
animationTime: 0.6
animationState: mfoam-dissolve
- type: FoamSolutionAreaEffect
foamedMetalPrototype: FoamedAluminiumMetal
- type: entity
id: BaseFoamedMetal
name: base foamed metal
description: Keeps the air in and the greytide out.
abstract: true
placement:
mode: SnapgridCenter
snap:
- Wall
components:
- type: RCDDeconstructWhitelist
- type: Clickable
- type: InteractionOutline
- type: Sprite
netsync: false
drawdepth: Walls
- type: Physics
shapes:
- !type:PhysShapeAabb
layer:
- Opaque
- Impassable
- MobImpassable
- VaultImpassable
- SmallImpassable
- type: Occluder
sizeX: 32
sizeY: 32
- type: SnapGrid
offset: Center
- type: Airtight
- type: Damageable
resistances: metallicResistances
- type: Destructible
thresholds:
50:
behaviors:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: entity
id: FoamedIronMetal
name: foamed iron metal
parent: BaseFoamedMetal
components:
- type: Sprite
sprite: Effects/foam.rsi
state: ironfoam
- type: entity
id: FoamedAluminiumMetal
name: foamed aluminium metal
parent: BaseFoamedMetal
components:
- type: Sprite
sprite: Effects/foam.rsi
state: metalfoam

View File

@@ -126,6 +126,15 @@
- !type:AdjustHealth - !type:AdjustHealth
amount: -8 amount: -8
- type: reagent
id: chem.FluorosulfuricAcid
name: fluorosulfuric acid
desc: An extremely corrosive chemical substance.
physicalDecs: strong-smelling
color: "#5050ff"
boilingPoint: 165
meltingPoint: -87
- type: reagent - type: reagent
id: chem.TableSalt id: chem.TableSalt
name: table salt name: table salt
@@ -241,3 +250,13 @@
meltingPoint: -80.7 meltingPoint: -80.7
tileReactions: tileReactions:
- !type:FlammableTileReaction {} - !type:FlammableTileReaction {}
- type: reagent
id: chem.Fluorosurfactant
name: fluorosurfactant
desc: A perfluoronated sulfonic acid that forms a foam when mixed with water.
physicalDesc: opaque
color: "#9e6b38"
boilingPoint: 190.0 # Perfluorooctanoic Acid.
meltingPoint: 45.0

View File

@@ -40,6 +40,20 @@
products: products:
chem.PolytrinicAcid: 3 chem.PolytrinicAcid: 3
- type: reaction
id: react.FluorosulfuricAcid
reactants:
chem.Fluorine:
amount: 1
chem.Hydrogen:
amount: 1
chem.Potassium:
amount: 1
chem.SulfuricAcid:
amount: 1
products:
chem.FluorosulfuricAcid: 4
- type: reaction - type: reaction
id: react.PotassiumExplosion id: react.PotassiumExplosion
reactants: reactants:
@@ -57,6 +71,94 @@
scaled: true #Scaled proportionally to amount of potassium and water scaled: true #Scaled proportionally to amount of potassium and water
maxScale: 30 #Explosion strength stops scaling at 30 potassium + 30 water maxScale: 30 #Explosion strength stops scaling at 30 potassium + 30 water
- type: reaction
id: react.Smoke
reactants:
chem.Phosphorus:
amount: 1
chem.Potassium:
amount: 1
chem.Sugar:
amount: 1
effects:
- !type:SmokeAreaReactionEffect
rangeConstant: 0
rangeMultiplier: 1.1 #Range formula: rangeConstant + rangeMultiplier*sqrt(ReactionUnits)
maxRange: 10
duration: 10
spreadDelay: 0.5
removeDelay: 0.5
diluteReagents: false
prototypeId: Smoke
sound: /Audio/Effects/smoke.ogg
- type: reaction
id: react.Foam
reactants:
chem.Fluorosurfactant:
amount: 1
chem.Water:
amount: 1
effects:
- !type:FoamAreaReactionEffect
rangeConstant: 0
rangeMultiplier: 1.1 #Range formula: rangeConstant + rangeMultiplier*sqrt(ReactionUnits)
maxRange: 10
duration: 10
spreadDelay: 1
removeDelay: 0
diluteReagents: true
reagentDilutionStart: 4 #At what range should the reagents start diluting
reagentDilutionFactor: 1
reagentMaxConcentrationFactor: 2 #The reagents will get multiplied by this number if the range turns out to be 0
prototypeId: Foam
- type: reaction
id: react.IronMetalFoam
reactants:
chem.Iron:
amount: 3
chem.FoamingAgent:
amount: 1
chem.FluorosulfuricAcid:
amount: 1
effects:
- !type:FoamAreaReactionEffect
rangeConstant: 0
rangeMultiplier: 1.1
maxRange: 10
duration: 10
spreadDelay: 1
removeDelay: 0
diluteReagents: true
reagentDilutionStart: 4
reagentDilutionFactor: 1
reagentMaxConcentrationFactor: 2
prototypeId: IronMetalFoam
- type: reaction
id: react.AluminiumMetalFoam
reactants:
chem.Aluminium:
amount: 3
chem.FoamingAgent:
amount: 1
chem.FluorosulfuricAcid:
amount: 1
effects:
- !type:FoamAreaReactionEffect
rangeConstant: 0
rangeMultiplier: 1.1
maxRange: 10
duration: 10
spreadDelay: 1
removeDelay: 0
diluteReagents: true
reagentDilutionStart: 4
reagentDilutionFactor: 1
reagentMaxConcentrationFactor: 2
prototypeId: AluminiumMetalFoam
- type: reaction - type: reaction
id: react.TableSalt id: react.TableSalt
reactants: reactants:
@@ -100,3 +202,15 @@
amount: 1 amount: 1
products: products:
chem.Water: 2 chem.Water: 2
- type: reaction
id: react.Fluorosurfactant
reactants:
chem.Carbon:
amount: 2
chem.Fluorine:
amount: 2
chem.SulfuricAcid:
amount: 1
products:
chem.Fluorosurfactant: 5

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -0,0 +1 @@
{"version": 1, "size": {"x": 96, "y": 96}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/discordia-space/CEV-Eris/blob/81b3a082ccdfb425f36bbed6e5bc1f0faed346ec/icons/effects/chemsmoke.dmi", "states": [{"name": "chemsmoke", "directions": 4, "delays": [[0.2, 0.2, 0.2], [0.2, 0.2, 0.2], [0.2, 0.2, 0.2], [0.2, 0.2, 0.2]]}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1 @@
{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/discordia-space/CEV-Eris/blob/81b3a082ccdfb425f36bbed6e5bc1f0faed346ec/icons/effects/effects.dmi", "states": [{"name": "foam", "directions": 1}, {"name": "foam-dissolve", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "ironfoam", "directions": 1}, {"name": "metalfoam", "directions": 1}, {"name": "mfoam", "directions": 1}, {"name": "mfoam-dissolve", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB