A return to foam (foam rework) (#20831)

This commit is contained in:
Nemanja
2023-10-26 22:52:11 -04:00
committed by GitHub
parent ddaf7ddc47
commit 0670b56205
11 changed files with 303 additions and 160 deletions

View File

@@ -1,8 +1,6 @@
using Content.Shared.Smoking; using Content.Shared.Chemistry.Components;
using Robust.Shared.Spawners;
using Robust.Client.Animations; using Robust.Client.Animations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Client.Chemistry.Visualizers; namespace Content.Client.Chemistry.Visualizers;
@@ -18,6 +16,7 @@ public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<FoamVisualsComponent, ComponentInit>(OnComponentInit); SubscribeLocalEvent<FoamVisualsComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<FoamVisualsComponent, AnimationCompletedEvent>(OnAnimationComplete);
} }
public override void Update(float frameTime) public override void Update(float frameTime)
@@ -27,11 +26,11 @@ public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent
if (!_timing.IsFirstTimePredicted) if (!_timing.IsFirstTimePredicted)
return; return;
var query = EntityQueryEnumerator<FoamVisualsComponent, TimedDespawnComponent>(); var query = EntityQueryEnumerator<FoamVisualsComponent, SmokeComponent>();
while (query.MoveNext(out var uid, out var comp, out var despawn)) while (query.MoveNext(out var uid, out var comp, out var smoke))
{ {
if (despawn.Lifetime > 1f) if (_timing.CurTime < comp.StartTime + TimeSpan.FromSeconds(smoke.Duration) - TimeSpan.FromSeconds(comp.AnimationTime))
continue; continue;
// Despawn animation. // Despawn animation.
@@ -48,6 +47,7 @@ public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent
/// </summary> /// </summary>
private void OnComponentInit(EntityUid uid, FoamVisualsComponent comp, ComponentInit args) private void OnComponentInit(EntityUid uid, FoamVisualsComponent comp, ComponentInit args)
{ {
comp.StartTime = _timing.CurTime;
comp.Animation = new Animation comp.Animation = new Animation
{ {
Length = TimeSpan.FromSeconds(comp.AnimationTime), Length = TimeSpan.FromSeconds(comp.AnimationTime),
@@ -58,12 +58,21 @@ public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent
LayerKey = FoamVisualLayers.Base, LayerKey = FoamVisualLayers.Base,
KeyFrames = KeyFrames =
{ {
new AnimationTrackSpriteFlick.KeyFrame(comp.State, 0f) new AnimationTrackSpriteFlick.KeyFrame(comp.AnimationState, 0f)
} }
} }
} }
}; };
} }
private void OnAnimationComplete(EntityUid uid, FoamVisualsComponent component, AnimationCompletedEvent args)
{
if (args.Key != FoamVisualsComponent.AnimationKey)
return;
if (TryComp<SpriteComponent>(uid, out var sprite))
sprite.Visible = false;
}
} }
public enum FoamVisualLayers : byte public enum FoamVisualLayers : byte

View File

@@ -1,5 +1,6 @@
using Robust.Client.Animations; using Robust.Client.Animations;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Client.Chemistry.Visualizers; namespace Content.Client.Chemistry.Visualizers;
@@ -15,18 +16,21 @@ public sealed partial class FoamVisualsComponent : Component
/// </summary> /// </summary>
public const string AnimationKey = "foamdissolve_animation"; public const string AnimationKey = "foamdissolve_animation";
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan StartTime;
/// <summary> /// <summary>
/// How long the foam visually dissolves for. /// How long the foam visually dissolves for.
/// </summary> /// </summary>
[DataField("animationTime")] [DataField]
public float AnimationTime = 0.6f; public float AnimationTime = 0.5f;
/// <summary> /// <summary>
/// The state of the entities base sprite RSI that is displayed when the foam dissolves. /// The state of the entities base sprite RSI that is displayed when the foam dissolves.
/// Cannot use <see cref="RSI.StateKey"/> because it does not have <see cref="DataDefinitionAttribute"/> and I am not making an engine PR at this time. /// Cannot use <see cref="RSI.StateKey"/> because it does not have <see cref="DataDefinitionAttribute"/> and I am not making an engine PR at this time.
/// </summary> /// </summary>
[DataField("animationState")] [DataField]
public string State = "foam-dissolve"; public string AnimationState = "foam-dissolve";
/// <summary> /// <summary>
/// The animation used while the foam dissolves. /// The animation used while the foam dissolves.

View File

@@ -197,6 +197,14 @@ public sealed class InternalsSystem : EntitySystem
return true; return true;
} }
public bool AreInternalsWorking(EntityUid uid, InternalsComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return false;
return AreInternalsWorking(component);
}
public bool AreInternalsWorking(InternalsComponent component) public bool AreInternalsWorking(InternalsComponent component)
{ {
return TryComp(component.BreathToolEntity, out BreathToolComponent? breathTool) && return TryComp(component.BreathToolEntity, out BreathToolComponent? breathTool) &&

View File

@@ -1,26 +0,0 @@
using Content.Shared.Fluids.Components;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Chemistry.Components;
/// <summary>
/// Stores solution on an anchored entity that has touch and ingestion reactions
/// to entities that collide with it. Similar to <see cref="PuddleComponent"/>
/// </summary>
[RegisterComponent]
public sealed partial class SmokeComponent : Component
{
public const string SolutionName = "solutionArea";
[DataField("nextReact", customTypeSerializer:typeof(TimeOffsetSerializer))]
public TimeSpan NextReact = TimeSpan.Zero;
[DataField("spreadAmount")]
public int SpreadAmount = 0;
/// <summary>
/// Have we reacted with our tile yet?
/// </summary>
[DataField("reactedTile")]
public bool ReactedTile = false;
}

View File

@@ -1,4 +1,3 @@
using Content.Server.Chemistry.Components;
using Content.Server.Fluids.EntitySystems; using Content.Server.Fluids.EntitySystems;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
@@ -10,7 +9,6 @@ using Content.Shared.Maps;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -71,18 +69,10 @@ namespace Content.Server.Chemistry.ReactionEffects
var coords = grid.MapToGrid(transform.MapPosition); var coords = grid.MapToGrid(transform.MapPosition);
var ent = args.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid()); var ent = args.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid());
if (!args.EntityManager.TryGetComponent<SmokeComponent>(ent, out var smokeComponent))
{
Logger.Error("Couldn't get AreaEffectComponent from " + _prototypeId);
args.EntityManager.QueueDeleteEntity(ent);
return;
}
var smoke = args.EntityManager.System<SmokeSystem>(); var smoke = args.EntityManager.System<SmokeSystem>();
smokeComponent.SpreadAmount = spreadAmount; smoke.StartSmoke(ent, splitSolution, _duration, spreadAmount);
smoke.Start(ent, smokeComponent, splitSolution, _duration);
SoundSystem.Play(_sound.GetSound(), Filter.Pvs(args.SolutionEntity), args.SolutionEntity, AudioHelpers.WithVariation(0.125f)); args.EntityManager.System<SharedAudioSystem>().PlayPvs(_sound, args.SolutionEntity, AudioHelpers.WithVariation(0.125f));
} }
} }
} }

View File

@@ -1,14 +1,23 @@
using System.Linq; using System.Linq;
using Content.Server.Chemistry.Components; using Content.Server.Administration.Logs;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.ReactionEffects; using Content.Server.Chemistry.ReactionEffects;
using Content.Server.Spreader; using Content.Server.Spreader;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Smoking; using Content.Shared.Smoking;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -22,70 +31,140 @@ namespace Content.Server.Fluids.EntitySystems;
public sealed class SmokeSystem : EntitySystem public sealed class SmokeSystem : EntitySystem
{ {
// If I could do it all again this could probably use a lot more of puddles. // If I could do it all again this could probably use a lot more of puddles.
[Dependency] private readonly IAdminLogManager _logger = default!;
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly BloodstreamSystem _blood = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
[Dependency] private readonly ReactiveSystem _reactive = default!;
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
[Dependency] private readonly TransformSystem _transform = default!;
private EntityQuery<SmokeComponent> _smokeQuery;
private EntityQuery<SmokeAffectedComponent> _smokeAffectedQuery;
/// <inheritdoc/> /// <inheritdoc/>
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<SmokeComponent, EntityUnpausedEvent>(OnSmokeUnpaused);
_smokeQuery = GetEntityQuery<SmokeComponent>();
_smokeAffectedQuery = GetEntityQuery<SmokeAffectedComponent>();
SubscribeLocalEvent<SmokeComponent, StartCollideEvent>(OnStartCollide);
SubscribeLocalEvent<SmokeComponent, EndCollideEvent>(OnEndCollide);
SubscribeLocalEvent<SmokeComponent, ReactionAttemptEvent>(OnReactionAttempt); SubscribeLocalEvent<SmokeComponent, ReactionAttemptEvent>(OnReactionAttempt);
SubscribeLocalEvent<SmokeComponent, SpreadNeighborsEvent>(OnSmokeSpread); SubscribeLocalEvent<SmokeComponent, SpreadNeighborsEvent>(OnSmokeSpread);
SubscribeLocalEvent<SmokeAffectedComponent, EntityUnpausedEvent>(OnAffectedUnpaused);
}
/// <inheritdoc/>
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<SmokeAffectedComponent>();
var curTime = _timing.CurTime;
while (query.MoveNext(out var uid, out var smoke))
{
if (curTime < smoke.NextSecond)
continue;
smoke.NextSecond += TimeSpan.FromSeconds(1);
SmokeReact(uid, smoke.SmokeEntity);
}
}
private void OnStartCollide(EntityUid uid, SmokeComponent component, ref StartCollideEvent args)
{
if (_smokeAffectedQuery.HasComponent(args.OtherEntity))
return;
var smokeAffected = AddComp<SmokeAffectedComponent>(args.OtherEntity);
smokeAffected.SmokeEntity = uid;
smokeAffected.NextSecond = _timing.CurTime + TimeSpan.FromSeconds(1);
}
private void OnEndCollide(EntityUid uid, SmokeComponent component, ref EndCollideEvent args)
{
// if we are already in smoke, make sure the thing we are exiting is the current smoke we are in.
if (_smokeAffectedQuery.TryGetComponent(args.OtherEntity, out var smokeAffectedComponent))
{
if (smokeAffectedComponent.SmokeEntity != uid)
return;
}
var exists = Exists(uid);
if (!TryComp<PhysicsComponent>(args.OtherEntity, out var body))
return;
foreach (var ent in _physics.GetContactingEntities(args.OtherEntity, body))
{
if (exists && ent == uid)
continue;
if (!_smokeQuery.HasComponent(ent))
continue;
smokeAffectedComponent ??= EnsureComp<SmokeAffectedComponent>(args.OtherEntity);
smokeAffectedComponent.SmokeEntity = ent;
return; // exit the function so we don't remove the component.
}
if (smokeAffectedComponent != null)
RemComp(args.OtherEntity, smokeAffectedComponent);
}
private void OnAffectedUnpaused(EntityUid uid, SmokeAffectedComponent component, ref EntityUnpausedEvent args)
{
component.NextSecond += args.PausedTime;
} }
private void OnSmokeSpread(EntityUid uid, SmokeComponent component, ref SpreadNeighborsEvent args) private void OnSmokeSpread(EntityUid uid, SmokeComponent component, ref SpreadNeighborsEvent args)
{ {
if (component.SpreadAmount == 0 if (component.SpreadAmount == 0 || !_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution))
|| !_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution))
{ {
RemCompDeferred<ActiveEdgeSpreaderComponent>(uid); RemCompDeferred<ActiveEdgeSpreaderComponent>(uid);
return; return;
} }
var prototype = MetaData(uid).EntityPrototype; if (Prototype(uid) is not { } prototype)
if (prototype == null)
{ {
RemCompDeferred<ActiveEdgeSpreaderComponent>(uid); RemCompDeferred<ActiveEdgeSpreaderComponent>(uid);
return; return;
} }
if (!args.NeighborFreeTiles.Any())
return;
TryComp<TimedDespawnComponent>(uid, out var timer); TryComp<TimedDespawnComponent>(uid, out var timer);
_appearance.TryGetData(uid, SmokeVisuals.Color, out var color);
// wtf is the logic behind any of this. // wtf is the logic behind any of this.
var smokePerSpread = 1 + component.SpreadAmount / Math.Max(1, args.NeighborFreeTiles.Count); var smokePerSpread = component.SpreadAmount / Math.Max(1, args.NeighborFreeTiles.Count);
foreach (var neighbor in args.NeighborFreeTiles) foreach (var neighbor in args.NeighborFreeTiles)
{ {
var coords = neighbor.Grid.GridTileToLocal(neighbor.Tile); var coords = neighbor.Grid.GridTileToLocal(neighbor.Tile);
var ent = Spawn(prototype.ID, coords); var ent = Spawn(prototype.ID, coords);
var neighborSmoke = EnsureComp<SmokeComponent>(ent); var spreadAmount = Math.Max(0, smokePerSpread);
neighborSmoke.SpreadAmount = Math.Max(0, smokePerSpread - 2); // why - 2? who the fuck knows. component.SpreadAmount -= args.NeighborFreeTiles.Count();
component.SpreadAmount--;
args.Updates--;
// Listen this is the old behaviour iunno StartSmoke(ent, solution.Clone(), timer?.Lifetime ?? component.Duration, spreadAmount);
Start(ent, neighborSmoke, solution.Clone(), timer?.Lifetime ?? 10f);
if (color != null)
_appearance.SetData(ent, SmokeVisuals.Color, color);
if (component.SpreadAmount == 0) if (component.SpreadAmount == 0)
{ {
RemCompDeferred<ActiveEdgeSpreaderComponent>(uid); RemCompDeferred<ActiveEdgeSpreaderComponent>(uid);
break; break;
} }
if (args.Updates <= 0)
break;
} }
args.Updates--;
if (args.NeighborFreeTiles.Count > 0 || args.Neighbors.Count == 0 || component.SpreadAmount < 1) if (args.NeighborFreeTiles.Count > 0 || args.Neighbors.Count == 0 || component.SpreadAmount < 1)
return; return;
@@ -100,7 +179,6 @@ public sealed class SmokeSystem : EntitySystem
continue; continue;
smoke.SpreadAmount++; smoke.SpreadAmount++;
args.Updates--;
component.SpreadAmount--; component.SpreadAmount--;
EnsureComp<ActiveEdgeSpreaderComponent>(neighbor); EnsureComp<ActiveEdgeSpreaderComponent>(neighbor);
@@ -110,6 +188,7 @@ public sealed class SmokeSystem : EntitySystem
break; break;
} }
} }
} }
private void OnReactionAttempt(EntityUid uid, SmokeComponent component, ReactionAttemptEvent args) private void OnReactionAttempt(EntityUid uid, SmokeComponent component, ReactionAttemptEvent args)
@@ -128,101 +207,117 @@ public sealed class SmokeSystem : EntitySystem
} }
} }
private void OnSmokeUnpaused(EntityUid uid, SmokeComponent component, ref EntityUnpausedEvent args) /// <summary>
/// Sets up a smoke component for spreading.
/// </summary>
public void StartSmoke(EntityUid uid, Solution solution, float duration, int spreadAmount, SmokeComponent? component = null)
{ {
component.NextReact += args.PausedTime; if (!Resolve(uid, ref component))
} return;
/// <inheritdoc/> component.SpreadAmount = spreadAmount;
public override void Update(float frameTime) component.Duration = duration;
{ component.TransferRate = solution.Volume / duration;
base.Update(frameTime); TryAddSolution(uid, solution);
var query = EntityQueryEnumerator<SmokeComponent>(); Dirty(uid, component);
var curTime = _timing.CurTime; EnsureComp<ActiveEdgeSpreaderComponent>(uid);
while (query.MoveNext(out var uid, out var smoke)) if (TryComp<PhysicsComponent>(uid, out var body) && TryComp<FixturesComponent>(uid, out var fixtures))
{ {
if (smoke.NextReact > curTime) var xform = Transform(uid);
continue; _physics.SetBodyType(uid, BodyType.Dynamic, fixtures, body, xform);
_physics.SetCanCollide(uid, true, manager: fixtures, body: body);
smoke.NextReact += TimeSpan.FromSeconds(1.5); _broadphase.RegenerateContacts(uid, body, fixtures, xform);
SmokeReact(uid, 1f, smoke);
} }
var timer = EnsureComp<TimedDespawnComponent>(uid);
timer.Lifetime = duration;
// The tile reaction happens here because it only occurs once.
ReactOnTile(uid, component);
} }
/// <summary> /// <summary>
/// Does the relevant smoke reactions for an entity for the specified exposure duration. /// Does the relevant smoke reactions for an entity.
/// </summary> /// </summary>
public void SmokeReact(EntityUid uid, float frameTime, SmokeComponent? component = null, TransformComponent? xform = null) public void SmokeReact(EntityUid entity, EntityUid smokeUid, SmokeComponent? component = null)
{ {
if (!Resolve(uid, ref component, ref xform)) if (!Resolve(smokeUid, ref component))
return; return;
if (!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution) || if (!_solutionSystem.TryGetSolution(smokeUid, SmokeComponent.SolutionName, out var solution) ||
solution.Contents.Count == 0) solution.Contents.Count == 0)
{ {
return; return;
} }
ReactWithEntity(entity, smokeUid, solution, component);
UpdateVisuals(smokeUid);
}
private void ReactWithEntity(EntityUid entity, EntityUid smokeUid, Solution solution, SmokeComponent? component = null)
{
if (!Resolve(smokeUid, ref component))
return;
if (!TryComp<BloodstreamComponent>(entity, out var bloodstream))
return;
var blockIngestion = _internals.AreInternalsWorking(entity);
var cloneSolution = solution.Clone();
var availableTransfer = FixedPoint2.Min(cloneSolution.Volume, component.TransferRate);
var transferAmount = FixedPoint2.Min(availableTransfer, bloodstream.ChemicalSolution.AvailableVolume);
var transferSolution = cloneSolution.SplitSolution(transferAmount);
foreach (var reagentQuantity in transferSolution.Contents.ToArray())
{
if (reagentQuantity.Quantity == FixedPoint2.Zero)
continue;
var reagentProto = _prototype.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
_reactive.ReactionEntity(entity, ReactionMethod.Touch, reagentProto, reagentQuantity, transferSolution);
if (!blockIngestion)
_reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentProto, reagentQuantity, transferSolution);
}
if (blockIngestion)
return;
if (_blood.TryAddToChemicals(entity, transferSolution, bloodstream))
{
// Log solution addition by smoke
_logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} ingested smoke {SolutionContainerSystem.ToPrettyString(transferSolution)}");
}
}
private void ReactOnTile(EntityUid uid, SmokeComponent? component = null, TransformComponent? xform = null)
{
if (!Resolve(uid, ref component, ref xform))
return;
if (!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution) || !solution.Any())
return;
if (!_mapManager.TryGetGrid(xform.GridUid, out var mapGrid)) if (!_mapManager.TryGetGrid(xform.GridUid, out var mapGrid))
return; return;
var tile = mapGrid.GetTileRef(xform.Coordinates.ToVector2i(EntityManager, _mapManager)); var tile = mapGrid.GetTileRef(xform.Coordinates.ToVector2i(EntityManager, _mapManager, _transform));
var solutionFraction = 1 / Math.Floor(frameTime);
var ents = _lookup.GetEntitiesIntersecting(tile, 0f, flags: LookupFlags.Uncontained).ToArray();
foreach (var reagentQuantity in solution.Contents.ToArray()) foreach (var reagentQuantity in solution.Contents.ToArray())
{ {
if (reagentQuantity.Quantity == FixedPoint2.Zero) if (reagentQuantity.Quantity == FixedPoint2.Zero)
continue; continue;
// NOOP, react with entities on the tile or whatever. var reagent = _prototype.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
reagent.ReactionTile(tile, reagentQuantity.Quantity);
} }
foreach (var entity in ents)
{
if (entity == uid)
continue;
ReactWithEntity(entity, solution, solutionFraction);
}
UpdateVisuals(uid);
}
private void UpdateVisuals(EntityUid uid)
{
if (TryComp(uid, out AppearanceComponent? appearance) &&
_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution))
{
var color = solution.GetColor(_prototype);
_appearance.SetData(uid, SmokeVisuals.Color, color, appearance);
}
}
private void ReactWithEntity(EntityUid entity, Solution solution, double solutionFraction)
{
// NOOP due to people complaining constantly.
return;
}
/// <summary>
/// Sets up a smoke component for spreading.
/// </summary>
public void Start(EntityUid uid, SmokeComponent component, Solution solution, float duration)
{
TryAddSolution(uid, component, solution);
EnsureComp<ActiveEdgeSpreaderComponent>(uid);
var timer = EnsureComp<TimedDespawnComponent>(uid);
timer.Lifetime = duration;
} }
/// <summary> /// <summary>
/// Adds the specified solution to the relevant smoke solution. /// Adds the specified solution to the relevant smoke solution.
/// </summary> /// </summary>
public void TryAddSolution(EntityUid uid, SmokeComponent component, Solution solution) private void TryAddSolution(EntityUid uid, Solution solution)
{ {
if (solution.Volume == FixedPoint2.Zero) if (solution.Volume == FixedPoint2.Zero)
return; return;
@@ -237,4 +332,14 @@ public sealed class SmokeSystem : EntitySystem
UpdateVisuals(uid); UpdateVisuals(uid);
} }
private void UpdateVisuals(EntityUid uid)
{
if (!TryComp(uid, out AppearanceComponent? appearance) ||
!_solutionSystem.TryGetSolution(uid, SmokeComponent.SolutionName, out var solution))
return;
var color = solution.GetColor(_prototype);
_appearance.SetData(uid, SmokeVisuals.Color, color, appearance);
}
} }

View File

@@ -12,7 +12,7 @@ public sealed partial class VentClogRuleComponent : Component
/// Somewhat safe chemicals to put in foam that probably won't instantly kill you. /// Somewhat safe chemicals to put in foam that probably won't instantly kill you.
/// There is a small chance of using any reagent, ignoring this. /// There is a small chance of using any reagent, ignoring this.
/// </summary> /// </summary>
[DataField("safeishVentChemicals", customTypeSerializer: typeof(PrototypeIdListSerializer<ReagentPrototype>))] [DataField(customTypeSerializer: typeof(PrototypeIdListSerializer<ReagentPrototype>))]
public IReadOnlyList<string> SafeishVentChemicals = new[] public IReadOnlyList<string> SafeishVentChemicals = new[]
{ {
"Water", "Blood", "Slime", "SpaceDrugs", "SpaceCleaner", "Nutriment", "Sugar", "SpaceLube", "Ephedrine", "Ale", "Beer", "SpaceGlue" "Water", "Blood", "Slime", "SpaceDrugs", "SpaceCleaner", "Nutriment", "Sugar", "SpaceLube", "Ephedrine", "Ale", "Beer", "SpaceGlue"
@@ -21,31 +21,31 @@ public sealed partial class VentClogRuleComponent : Component
/// <summary> /// <summary>
/// Sound played when foam is being created. /// Sound played when foam is being created.
/// </summary> /// </summary>
[DataField("sound")] [DataField]
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/extinguish.ogg"); public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/extinguish.ogg");
/// <summary> /// <summary>
/// The standard reagent quantity to put in the foam, modfied by event severity. /// The standard reagent quantity to put in the foam, modified by event severity.
/// </summary> /// </summary>
[DataField("reagentQuantity"), ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
public int ReagentQuantity = 200; public int ReagentQuantity = 100;
/// <summary> /// <summary>
/// The standard spreading of the foam, not modfied by event severity. /// The standard spreading of the foam, not modified by event severity.
/// </summary> /// </summary>
[DataField("spread"), ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
public int Spread = 20; public int Spread = 16;
/// <summary> /// <summary>
/// How long the foam lasts for /// How long the foam lasts for
/// </summary> /// </summary>
[DataField("time"), ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
public float Time = 20f; public float Time = 20f;
/// <summary> /// <summary>
/// Reagents that gets the weak numbers used instead of regular ones. /// Reagents that gets the weak numbers used instead of regular ones.
/// </summary> /// </summary>
[DataField("weakReagents", customTypeSerializer: typeof(PrototypeIdListSerializer<ReagentPrototype>))] [DataField(customTypeSerializer: typeof(PrototypeIdListSerializer<ReagentPrototype>))]
public IReadOnlyList<string> WeakReagents = new[] public IReadOnlyList<string> WeakReagents = new[]
{ {
"SpaceLube", "SpaceGlue" "SpaceLube", "SpaceGlue"
@@ -54,12 +54,12 @@ public sealed partial class VentClogRuleComponent : Component
/// <summary> /// <summary>
/// Quantity of weak reagents to put in the foam. /// Quantity of weak reagents to put in the foam.
/// </summary> /// </summary>
[DataField("weakReagentQuantity"), ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
public int WeakReagentQuantity = 60; public int WeakReagentQuantity = 50;
/// <summary> /// <summary>
/// Spread of the foam for weak reagents. /// Spread of the foam for weak reagents.
/// </summary> /// </summary>
[DataField("weakSpread"), ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
public int WeakSpread = 2; public int WeakSpread = 3;
} }

View File

@@ -3,10 +3,8 @@ using Content.Server.Station.Components;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Random; using Robust.Shared.Random;
using System.Linq; using System.Linq;
using Content.Server.Chemistry.Components;
using Content.Server.Fluids.EntitySystems; using Content.Server.Fluids.EntitySystems;
using Content.Server.GameTicking.Rules.Components; using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components; using Content.Server.StationEvents.Components;
@@ -53,9 +51,8 @@ public sealed class VentClogRule : StationEventSystem<VentClogRuleComponent>
solution.AddReagent(reagent, quantity); solution.AddReagent(reagent, quantity);
var foamEnt = Spawn("Foam", transform.Coordinates); var foamEnt = Spawn("Foam", transform.Coordinates);
var smoke = EnsureComp<SmokeComponent>(foamEnt); var spreadAmount = weak ? component.WeakSpread : component.Spread;
smoke.SpreadAmount = weak ? component.WeakSpread : component.Spread; _smoke.StartSmoke(foamEnt, solution, component.Time, spreadAmount);
_smoke.Start(foamEnt, smoke, solution, component.Time);
Audio.PlayPvs(component.Sound, transform.Coordinates); Audio.PlayPvs(component.Sound, transform.Coordinates);
} }
} }

View File

@@ -1,5 +1,4 @@
using System.Linq; using System.Linq;
using Content.Server.Chemistry.Components;
using Content.Server.Fluids.EntitySystems; using Content.Server.Fluids.EntitySystems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components; using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events; using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
@@ -38,8 +37,7 @@ public sealed class FoamArtifactSystem : EntitySystem
var range = (int) MathF.Round(MathHelper.Lerp(component.MinFoamAmount, component.MaxFoamAmount, _random.NextFloat(0, 1f))); var range = (int) MathF.Round(MathHelper.Lerp(component.MinFoamAmount, component.MaxFoamAmount, _random.NextFloat(0, 1f)));
sol.AddReagent(component.SelectedReagent, component.ReagentAmount); sol.AddReagent(component.SelectedReagent, component.ReagentAmount);
var foamEnt = Spawn("Foam", xform.Coordinates); var foamEnt = Spawn("Foam", xform.Coordinates);
var smoke = EnsureComp<SmokeComponent>(foamEnt); var spreadAmount = range * 4;
smoke.SpreadAmount = range * 4; _smoke.StartSmoke(foamEnt, sol, component.Duration, spreadAmount);
_smoke.Start(foamEnt, smoke, sol, component.Duration);
} }
} }

View File

@@ -0,0 +1,24 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Chemistry.Components;
/// <summary>
/// This is used for entities which are currently being affected by smoke.
/// Manages the gradual metabolism every second.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class SmokeAffectedComponent : Component
{
/// <summary>
/// The time at which the next smoke metabolism will occur.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextSecond;
/// <summary>
/// The smoke that is currently affecting this entity.
/// </summary>
[DataField]
public EntityUid SmokeEntity;
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.FixedPoint;
using Content.Shared.Fluids.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Chemistry.Components;
/// <summary>
/// Stores solution on an anchored entity that has touch and ingestion reactions
/// to entities that collide with it. Similar to <see cref="PuddleComponent"/>
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class SmokeComponent : Component
{
public const string SolutionName = "solutionArea";
/// <summary>
/// The max amount of tiles this smoke cloud can spread to.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int SpreadAmount;
/// <summary>
/// The max rate at which chemicals are transferred from the smoke to the person inhaling it.
/// Calculated as (total volume of chemicals in smoke) / (<see cref="Duration"/>)
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public FixedPoint2 TransferRate;
/// <summary>
/// The total lifespan of the smoke.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public float Duration = 10;
}