Add basic chemical reactions (#376)
* Add basic chemical reaction system What it adds: - Reactions defined in yaml with an arbitrary amount of reactants (can be catalysts), products, and effects. What it doesn't add: - Temperature dependent reactions - Metabolism or other medical/health effects * Add many common SS13 chemicals and reactions Added many of the common SS13 medicines and other chemicals, and their chemical reactions. Note that many of them are lacking their effects since we don't have medical yet. * Add ExplosiveReactionEffect Shows how IReactionEffect can be implemented to have effects that occur with a reaction by adding ExplosionReactionEffect and the potassium + water explosion reaction. * Move ReactionSystem logic into SolutionComponent No need for this to be a system currently so the behavior for reaction checking has been moved into SolutionComponent. Now it only checks for reactions when a reagent or solution is added to a solution. Also fixed a bug with SolutionValidReaction incorrectly returning true. * Move explosion logic out of ExplosiveComponent Allows you to create explosions without needing to add an ExplosiveComponent to an entity first. * Add SolutionComponent.SolutionChanged event. Trigger dispenser ui updates with it. This removes the need for SolutionComponent having a reference to the dispenser it's in. Instead the dispenser subscribes to the event and updates it's UI whenever the event is triggered. * Add forgotten checks `SolutionComponent.TryAddReagent` and `SolutionComponent.TryAddSolution` now check to see if `skipReactionCheck` is false before checking for a chemical reaction to avoid unnecessarily checking themselves for a reaction again when `SolutionComponent.PerformReaction` calls either of them. * Change SolutionComponent.SolutionChanged to an Action The arguments for event handler have no use here, and any class that can access SolutionChanged can access the SolutionComponent so it can just be an Action with no arguments instead.
This commit is contained in:
committed by
Pieter-Jan Briers
parent
bd5a4e33ab
commit
427836fec9
@@ -9,7 +9,6 @@ using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.Components.UserInterface;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -40,6 +39,8 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
public bool HasBeaker => _beakerContainer.ContainedEntity != null;
|
||||
public int DispenseAmount = 10;
|
||||
|
||||
private SolutionComponent _solution => _beakerContainer.ContainedEntity.GetComponent<SolutionComponent>();
|
||||
|
||||
/// <summary>
|
||||
/// Shows the serializer how to save/load this components yaml prototype.
|
||||
/// </summary>
|
||||
@@ -153,7 +154,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
/// <summary>
|
||||
/// Gets current component data as a <see cref="SharedReagentDispenserComponent.ReagentDispenserBoundUserInterfaceState"/> and sends it to the client.
|
||||
/// </summary>
|
||||
private void UpdateUserInterface()
|
||||
public void UpdateUserInterface()
|
||||
{
|
||||
var state = GetUserInterfaceState();
|
||||
_userInterface.SetState(state);
|
||||
@@ -162,9 +163,10 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
/// <summary>
|
||||
/// If this component contains an entity with a <see cref="SolutionComponent"/>, eject it.
|
||||
/// </summary>
|
||||
private void TryEject()
|
||||
public void TryEject()
|
||||
{
|
||||
if(!HasBeaker) return;
|
||||
_solution.SolutionChanged -= HandleSolutionChangedEvent;
|
||||
_beakerContainer.Remove(_beakerContainer.ContainedEntity);
|
||||
|
||||
UpdateUserInterface();
|
||||
@@ -253,6 +255,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
else
|
||||
{
|
||||
_beakerContainer.Insert(activeHandEntity);
|
||||
_solution.SolutionChanged += HandleSolutionChangedEvent;
|
||||
UpdateUserInterface();
|
||||
}
|
||||
}
|
||||
@@ -264,5 +267,10 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void HandleSolutionChangedEvent()
|
||||
{
|
||||
UpdateUserInterface();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Chemistry;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -20,14 +23,26 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
#pragma warning disable 649
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager;
|
||||
[Dependency] private readonly ILocalizationManager _localizationManager;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager;
|
||||
#pragma warning restore 649
|
||||
|
||||
private IEnumerable<ReactionPrototype> _reactions;
|
||||
private AudioSystem _audioSystem;
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
_reactions = _prototypeManager.EnumeratePrototypes<ReactionPrototype>();
|
||||
_audioSystem = _entitySystemManager.GetEntitySystem<AudioSystem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfers solution from the held container to the target container.
|
||||
/// </summary>
|
||||
[Verb]
|
||||
private sealed class FillTargetVerb : Verb<SolutionComponent>
|
||||
{
|
||||
{
|
||||
protected override string GetText(IEntity user, SolutionComponent component)
|
||||
{
|
||||
if(!user.TryGetComponent<HandsComponent>(out var hands))
|
||||
@@ -164,5 +179,116 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
handSolutionComp.TryAddSolution(transferSolution);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckForReaction()
|
||||
{
|
||||
//Check the solution for every reaction
|
||||
foreach (var reaction in _reactions)
|
||||
{
|
||||
if (SolutionValidReaction(reaction, out int unitReactions))
|
||||
{
|
||||
PerformReaction(reaction, unitReactions);
|
||||
break; //Only perform one reaction per solution per update.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryAddReagent(string reagentId, int quantity, out int acceptedQuantity, bool skipReactionCheck = false)
|
||||
{
|
||||
if (quantity > _maxVolume - _containedSolution.TotalVolume)
|
||||
{
|
||||
acceptedQuantity = _maxVolume - _containedSolution.TotalVolume;
|
||||
if (acceptedQuantity == 0) return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
acceptedQuantity = quantity;
|
||||
}
|
||||
|
||||
_containedSolution.AddReagent(reagentId, acceptedQuantity);
|
||||
RecalculateColor();
|
||||
if(!skipReactionCheck)
|
||||
CheckForReaction();
|
||||
OnSolutionChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryAddSolution(Solution solution, bool skipReactionCheck = false)
|
||||
{
|
||||
if (solution.TotalVolume > (_maxVolume - _containedSolution.TotalVolume))
|
||||
return false;
|
||||
|
||||
_containedSolution.AddSolution(solution);
|
||||
RecalculateColor();
|
||||
if(!skipReactionCheck)
|
||||
CheckForReaction();
|
||||
OnSolutionChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a solution has the reactants required to cause a specified reaction.
|
||||
/// </summary>
|
||||
/// <param name="solution">The solution to check for reaction conditions.</param>
|
||||
/// <param name="reaction">The reaction whose reactants will be checked for in the solution.</param>
|
||||
/// <param name="unitReactions">The number of times the reaction can occur with the given solution.</param>
|
||||
/// <returns></returns>
|
||||
private bool SolutionValidReaction(ReactionPrototype reaction, out int unitReactions)
|
||||
{
|
||||
unitReactions = int.MaxValue; //Set to some impossibly large number initially
|
||||
foreach (var reactant in reaction.Reactants)
|
||||
{
|
||||
if (!ContainsReagent(reactant.Key, out int reagentQuantity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
int currentUnitReactions = reagentQuantity / reactant.Value.Amount;
|
||||
if (currentUnitReactions < unitReactions)
|
||||
{
|
||||
unitReactions = currentUnitReactions;
|
||||
}
|
||||
}
|
||||
|
||||
if (unitReactions == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a reaction on a solution. This assumes all reaction criteria have already been checked and are met.
|
||||
/// </summary>
|
||||
/// <param name="solution">Solution to be reacted.</param>
|
||||
/// <param name="reaction">Reaction to occur.</param>
|
||||
/// <param name="unitReactions">The number of times to cause this reaction.</param>
|
||||
private void PerformReaction(ReactionPrototype reaction, int unitReactions)
|
||||
{
|
||||
//Remove non-catalysts
|
||||
foreach (var reactant in reaction.Reactants)
|
||||
{
|
||||
if (!reactant.Value.Catalyst)
|
||||
{
|
||||
int amountToRemove = unitReactions * reactant.Value.Amount;
|
||||
TryRemoveReagent(reactant.Key, amountToRemove);
|
||||
}
|
||||
}
|
||||
//Add products
|
||||
foreach (var product in reaction.Products)
|
||||
{
|
||||
TryAddReagent(product.Key, (int)(unitReactions * product.Value), out int acceptedQuantity, true);
|
||||
}
|
||||
//Trigger reaction effects
|
||||
foreach (var effect in reaction.Effects)
|
||||
{
|
||||
effect.React(Owner, unitReactions);
|
||||
}
|
||||
|
||||
//Play reaction sound client-side
|
||||
_audioSystem.Play("/Audio/effects/chemistry/bubbles.ogg", Owner.Transform.GridPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.EntitySystemMessages;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Content.Server.Explosions;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Explosive
|
||||
{
|
||||
@@ -32,7 +22,7 @@ namespace Content.Server.GameObjects.Components.Explosive
|
||||
#pragma warning restore 649
|
||||
|
||||
public override string Name => "Explosive";
|
||||
|
||||
|
||||
public int DevastationRange = 0;
|
||||
public int HeavyImpactRange = 0;
|
||||
public int LightImpactRange = 0;
|
||||
@@ -50,121 +40,13 @@ namespace Content.Server.GameObjects.Components.Explosive
|
||||
serializer.DataField(ref FlashRange, "flashRange", 0);
|
||||
}
|
||||
|
||||
private bool Explosion()
|
||||
public bool Explosion()
|
||||
{
|
||||
//Prevent adjacent explosives from infinitely blowing each other up.
|
||||
if (_beingExploded) return true;
|
||||
_beingExploded = true;
|
||||
|
||||
var maxRange = MathHelper.Max(DevastationRange, HeavyImpactRange, LightImpactRange, 0f);
|
||||
//Entity damage calculation
|
||||
var entitiesAll = _serverEntityManager.GetEntitiesInRange(Owner.Transform.GridPosition, maxRange).ToList();
|
||||
|
||||
foreach (var entity in entitiesAll)
|
||||
{
|
||||
if (entity == Owner)
|
||||
continue;
|
||||
if (!entity.Transform.IsMapTransform)
|
||||
continue;
|
||||
var distanceFromEntity = (int)entity.Transform.GridPosition.Distance(_mapManager, Owner.Transform.GridPosition);
|
||||
var exAct = _entitySystemManager.GetEntitySystem<ActSystem>();
|
||||
var severity = ExplosionSeverity.Destruction;
|
||||
if (distanceFromEntity < DevastationRange)
|
||||
{
|
||||
severity = ExplosionSeverity.Destruction;
|
||||
}
|
||||
else if (distanceFromEntity < HeavyImpactRange)
|
||||
{
|
||||
severity = ExplosionSeverity.Heavy;
|
||||
}
|
||||
else if (distanceFromEntity < LightImpactRange)
|
||||
{
|
||||
severity = ExplosionSeverity.Light;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
exAct.HandleExplosion(Owner, entity, severity);
|
||||
}
|
||||
|
||||
//Tile damage calculation mockup
|
||||
//TODO: make it into some sort of actual damage component or whatever the boys think is appropriate
|
||||
var mapGrid = _mapManager.GetGrid(Owner.Transform.GridPosition.GridID);
|
||||
var circle = new Circle(Owner.Transform.GridPosition.Position, maxRange);
|
||||
var tiles = mapGrid.GetTilesIntersecting(circle);
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
var tileLoc = mapGrid.GridTileToLocal(tile.GridIndices);
|
||||
var tileDef = (ContentTileDefinition)_tileDefinitionManager[tile.Tile.TypeId];
|
||||
var distanceFromTile = (int)tileLoc.Distance(_mapManager, Owner.Transform.GridPosition);
|
||||
if (!string.IsNullOrWhiteSpace(tileDef.SubFloor)) {
|
||||
if (distanceFromTile < DevastationRange)
|
||||
mapGrid.SetTile(tileLoc, new Tile(_tileDefinitionManager["space"].TileId));
|
||||
if (distanceFromTile < HeavyImpactRange)
|
||||
{
|
||||
if (_robustRandom.Prob(80))
|
||||
{
|
||||
mapGrid.SetTile(tileLoc, new Tile(_tileDefinitionManager[tileDef.SubFloor].TileId));
|
||||
}
|
||||
else
|
||||
{
|
||||
mapGrid.SetTile(tileLoc, new Tile(_tileDefinitionManager["space"].TileId));
|
||||
}
|
||||
}
|
||||
if (distanceFromTile < LightImpactRange)
|
||||
{
|
||||
if (_robustRandom.Prob(50))
|
||||
{
|
||||
mapGrid.SetTile(tileLoc, new Tile(_tileDefinitionManager[tileDef.SubFloor].TileId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Effects and sounds
|
||||
var time = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
var message = new EffectSystemMessage
|
||||
{
|
||||
EffectSprite = "Effects/explosion.rsi",
|
||||
RsiState = "explosionfast",
|
||||
Born = time,
|
||||
DeathTime = time + TimeSpan.FromSeconds(5),
|
||||
Size = new Vector2(FlashRange / 2, FlashRange / 2),
|
||||
Coordinates = Owner.Transform.GridPosition,
|
||||
//Rotated from east facing
|
||||
Rotation = 0f,
|
||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), 0.5f),
|
||||
Shaded = false
|
||||
};
|
||||
_entitySystemManager.GetEntitySystem<EffectSystem>().CreateParticle(message);
|
||||
_entitySystemManager.GetEntitySystem<AudioSystem>().Play("/Audio/effects/explosion.ogg", Owner);
|
||||
|
||||
// Knock back cameras of all players in the area.
|
||||
|
||||
var playerManager = IoCManager.Resolve<IPlayerManager>();
|
||||
var selfPos = Owner.Transform.WorldPosition;
|
||||
foreach (var player in playerManager.GetAllPlayers())
|
||||
{
|
||||
if (player.AttachedEntity == null
|
||||
|| player.AttachedEntity.Transform.MapID != mapGrid.ParentMapId
|
||||
|| !player.AttachedEntity.TryGetComponent(out CameraRecoilComponent recoil))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var playerPos = player.AttachedEntity.Transform.WorldPosition;
|
||||
var delta = selfPos - playerPos;
|
||||
var distance = delta.LengthSquared;
|
||||
|
||||
var effect = 1 / (1 + 0.2f * distance);
|
||||
if (effect > 0.01f)
|
||||
{
|
||||
var kick = -delta.Normalized * effect;
|
||||
recoil.Kick(kick);
|
||||
}
|
||||
}
|
||||
ExplosionHelper.SpawnExplosion(Owner.Transform.GridPosition, DevastationRange, HeavyImpactRange, LightImpactRange, FlashRange);
|
||||
|
||||
Owner.Delete();
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user