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>
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -236,6 +236,8 @@ namespace Content.Client
|
|||||||
"SliceableFood",
|
"SliceableFood",
|
||||||
"DamageOtherOnHit",
|
"DamageOtherOnHit",
|
||||||
"DamageOnLand",
|
"DamageOnLand",
|
||||||
|
"SmokeSolutionAreaEffect",
|
||||||
|
"FoamSolutionAreaEffect",
|
||||||
"GasFilter",
|
"GasFilter",
|
||||||
"Recyclable",
|
"Recyclable",
|
||||||
"SecretStash",
|
"SecretStash",
|
||||||
|
|||||||
162
Content.Server/Chemistry/ReactionEffects/AreaReactionEffect.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
BIN
Resources/Audio/Effects/smoke.ogg
Normal 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
|
||||||
|
|||||||
149
Resources/Prototypes/Entities/Effects/chemistry_effects.yml
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
BIN
Resources/Textures/Effects/chemsmoke.rsi/chemsmoke.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
1
Resources/Textures/Effects/chemsmoke.rsi/meta.json
Normal 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]]}]}
|
||||||
BIN
Resources/Textures/Effects/foam.rsi/foam-dissolve.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
Resources/Textures/Effects/foam.rsi/foam.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
Resources/Textures/Effects/foam.rsi/ironfoam.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
1
Resources/Textures/Effects/foam.rsi/meta.json
Normal 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]]}]}
|
||||||
BIN
Resources/Textures/Effects/foam.rsi/metalfoam.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Resources/Textures/Effects/foam.rsi/mfoam-dissolve.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
Resources/Textures/Effects/foam.rsi/mfoam.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |