adds the ability to "mix" solutions (reactions caused by using an item on a solution holder) (#13015)
* everything for mixing aside from yaml changes * add recipe and canmix to bottles and the holy mixer tag to the bible * fixes as a result of testing * remove unused usings * remove emptylines that are not required Co-authored-by: 0x6273 <0x40@keemail.me> * more empty line removal! Co-authored-by: 0x6273 <0x40@keemail.me> * add single space between if statement and condition Co-authored-by: 0x6273 <0x40@keemail.me> * fixes indentation on TryGetMixableSolution * raise new AfterMixingEvent after attempting to mix a solution * before mixing event and attempt get mixable solution event * update reaction tests to be a beaker that can be mixed, and then pass a mixer component in to simulate mixing * make two more beaker types mixable, add attribute for mixing feedback * bible mix message * mixing feedback on success * updates test to use SpawnEntity over new as per feedback Co-authored-by: 0x6273 <0x40@keemail.me>
This commit is contained in:
committed by
GitHub
parent
a497167e45
commit
c046666578
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Chemistry.EntitySystems;
|
using Content.Server.Chemistry.EntitySystems;
|
||||||
|
using Content.Server.Engineering.Components;
|
||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared.Chemistry.Components;
|
||||||
using Content.Shared.Chemistry.Reaction;
|
using Content.Shared.Chemistry.Reaction;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@@ -23,7 +24,8 @@ namespace Content.IntegrationTests.Tests.Chemistry
|
|||||||
- type: SolutionContainerManager
|
- type: SolutionContainerManager
|
||||||
solutions:
|
solutions:
|
||||||
beaker:
|
beaker:
|
||||||
maxVol: 50";
|
maxVol: 50
|
||||||
|
canMix: true";
|
||||||
[Test]
|
[Test]
|
||||||
public async Task TryAllTest()
|
public async Task TryAllTest()
|
||||||
{
|
{
|
||||||
@@ -58,6 +60,14 @@ namespace Content.IntegrationTests.Tests.Chemistry
|
|||||||
}
|
}
|
||||||
|
|
||||||
solutionSystem.SetTemperature(beaker, component, reactionPrototype.MinimumTemperature);
|
solutionSystem.SetTemperature(beaker, component, reactionPrototype.MinimumTemperature);
|
||||||
|
|
||||||
|
if (reactionPrototype.MixingCategories != null)
|
||||||
|
{
|
||||||
|
var dummyEntity = entityManager.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||||
|
var mixerComponent = entityManager.AddComponent<ReactionMixerComponent>(dummyEntity);
|
||||||
|
mixerComponent.ReactionTypes = reactionPrototype.MixingCategories;
|
||||||
|
solutionSystem.UpdateChemicals(beaker, component, true, mixerComponent);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.WaitIdleAsync();
|
await server.WaitIdleAsync();
|
||||||
|
|||||||
@@ -28,5 +28,6 @@ public sealed partial class ChemistrySystem : EntitySystem
|
|||||||
// Why ChemMaster duplicates reagentdispenser nobody knows.
|
// Why ChemMaster duplicates reagentdispenser nobody knows.
|
||||||
InitializeHypospray();
|
InitializeHypospray();
|
||||||
InitializeInjector();
|
InitializeInjector();
|
||||||
|
InitializeMixing();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
|
||||||
|
using Content.Shared.Chemistry.Components;
|
||||||
|
using Content.Shared.Chemistry.Reaction;
|
||||||
|
using Content.Shared.IdentityManagement;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Interaction.Events;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
|
||||||
|
namespace Content.Server.Chemistry.EntitySystems;
|
||||||
|
|
||||||
|
public sealed partial class ChemistrySystem
|
||||||
|
{
|
||||||
|
public void InitializeMixing()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<ReactionMixerComponent, AfterInteractEvent>(OnAfterInteract);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAfterInteract(EntityUid uid, ReactionMixerComponent component, AfterInteractEvent args)
|
||||||
|
{
|
||||||
|
if (!args.Target.HasValue || !args.CanReach)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var mixAttemptEvent = new MixingAttemptEvent(uid);
|
||||||
|
RaiseLocalEvent(uid, ref mixAttemptEvent);
|
||||||
|
if(mixAttemptEvent.Cancelled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Solution? solution = null;
|
||||||
|
if (!_solutions.TryGetMixableSolution(args.Target.Value, out solution))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_popup.PopupEntity(Loc.GetString(component.MixMessage, ("mixed", Identity.Entity(args.Target.Value, EntityManager)), ("mixer", Identity.Entity(uid, EntityManager))), args.User, Filter.Entities(args.User)); ;
|
||||||
|
|
||||||
|
_solutions.UpdateChemicals(args.Target.Value, solution, true, component);
|
||||||
|
|
||||||
|
var afterMixingEvent = new AfterMixingEvent(uid, args.Target.Value);
|
||||||
|
RaiseLocalEvent(uid, afterMixingEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
using Content.Server.Chemistry.Components.SolutionManager;
|
using Content.Server.Chemistry.Components.SolutionManager;
|
||||||
using Content.Shared.Chemistry;
|
using Content.Shared.Chemistry;
|
||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared.Chemistry.Components;
|
||||||
@@ -8,6 +9,7 @@ using Content.Shared.Examine;
|
|||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Chemistry.EntitySystems;
|
namespace Content.Server.Chemistry.EntitySystems;
|
||||||
|
|
||||||
@@ -115,12 +117,12 @@ public sealed partial class SolutionContainerSystem : EntitySystem
|
|||||||
return splitSol;
|
return splitSol;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateChemicals(EntityUid uid, Solution solutionHolder, bool needsReactionsProcessing = false)
|
public void UpdateChemicals(EntityUid uid, Solution solutionHolder, bool needsReactionsProcessing = false, ReactionMixerComponent? mixerComponent = null)
|
||||||
{
|
{
|
||||||
// Process reactions
|
// Process reactions
|
||||||
if (needsReactionsProcessing && solutionHolder.CanReact)
|
if (needsReactionsProcessing && solutionHolder.CanReact)
|
||||||
{
|
{
|
||||||
_chemistrySystem.FullyReactSolution(solutionHolder, uid, solutionHolder.MaxVolume);
|
_chemistrySystem.FullyReactSolution(solutionHolder, uid, solutionHolder.MaxVolume, mixerComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateAppearance(uid, solutionHolder);
|
UpdateAppearance(uid, solutionHolder);
|
||||||
@@ -333,6 +335,36 @@ public sealed partial class SolutionContainerSystem : EntitySystem
|
|||||||
return reagentQuantity;
|
return reagentQuantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryGetMixableSolution(EntityUid uid,
|
||||||
|
[NotNullWhen(true)] out Solution? solution,
|
||||||
|
SolutionContainerManagerComponent? solutionsMgr = null)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!Resolve(uid, ref solutionsMgr, false))
|
||||||
|
{
|
||||||
|
solution = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var getMixableSolutionAttempt = new GetMixableSolutionAttemptEvent(uid);
|
||||||
|
RaiseLocalEvent(uid, ref getMixableSolutionAttempt);
|
||||||
|
if(getMixableSolutionAttempt.MixedSolution != null)
|
||||||
|
{
|
||||||
|
solution = getMixableSolutionAttempt.MixedSolution;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tryGetSolution = solutionsMgr.Solutions.FirstOrNull(x => x.Value.CanMix);
|
||||||
|
if (tryGetSolution.HasValue)
|
||||||
|
{
|
||||||
|
solution = tryGetSolution.Value.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
solution = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Thermal energy and temperature management.
|
// Thermal energy and temperature management.
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ namespace Content.Shared.Chemistry.Components
|
|||||||
[DataField("canReact")]
|
[DataField("canReact")]
|
||||||
public bool CanReact { get; set; } = true;
|
public bool CanReact { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If reactions can occur via mixing.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("canMix")]
|
||||||
|
public bool CanMix { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Volume needed to fill this container.
|
/// Volume needed to fill this container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
29
Content.Shared/Chemistry/Reaction/ReactionMixerComponent.cs
Normal file
29
Content.Shared/Chemistry/Reaction/ReactionMixerComponent.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Content.Shared.Chemistry.Components;
|
||||||
|
|
||||||
|
namespace Content.Shared.Chemistry.Reaction;
|
||||||
|
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class ReactionMixerComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A list of IDs for categories of reactions that can be mixed (i.e. HOLY for a bible, DRINK for a spoon)
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
[DataField("reactionTypes")]
|
||||||
|
public List<string> ReactionTypes = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A string which identifies the string to be sent when successfully mixing a solution
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
[DataField("mixMessage")]
|
||||||
|
public string MixMessage = "default-mixing-success";
|
||||||
|
}
|
||||||
|
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct MixingAttemptEvent(EntityUid Mixed, bool Cancelled = false);
|
||||||
|
|
||||||
|
public readonly record struct AfterMixingEvent(EntityUid Mixed, EntityUid Mixer);
|
||||||
|
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct GetMixableSolutionAttemptEvent(EntityUid Mixed, Solution? MixedSolution = null);
|
||||||
@@ -38,6 +38,12 @@ namespace Content.Shared.Chemistry.Reaction
|
|||||||
[DataField("maxTemp")]
|
[DataField("maxTemp")]
|
||||||
public float MaximumTemperature = float.PositiveInfinity;
|
public float MaximumTemperature = float.PositiveInfinity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The required mixing categories for an entity to mix the solution with for the reaction to occur
|
||||||
|
/// </summary>
|
||||||
|
[DataField("requiredMixerCategories")]
|
||||||
|
public List<string>? MixingCategories = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reagents created when the reaction occurs.
|
/// Reagents created when the reaction occurs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Content.Shared.Chemistry.Components;
|
|||||||
using Content.Shared.Chemistry.Reagent;
|
using Content.Shared.Chemistry.Reagent;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
|
using Content.Shared.Interaction.Events;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ namespace Content.Shared.Chemistry.Reaction
|
|||||||
/// <param name="reaction">The reaction to check.</param>
|
/// <param name="reaction">The reaction to check.</param>
|
||||||
/// <param name="lowestUnitReactions">How many times this reaction can occur.</param>
|
/// <param name="lowestUnitReactions">How many times this reaction can occur.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private bool CanReact(Solution solution, ReactionPrototype reaction, EntityUid owner, out FixedPoint2 lowestUnitReactions)
|
private bool CanReact(Solution solution, ReactionPrototype reaction, EntityUid owner, ReactionMixerComponent? mixerComponent, out FixedPoint2 lowestUnitReactions)
|
||||||
{
|
{
|
||||||
lowestUnitReactions = FixedPoint2.MaxValue;
|
lowestUnitReactions = FixedPoint2.MaxValue;
|
||||||
if (solution.Temperature < reaction.MinimumTemperature)
|
if (solution.Temperature < reaction.MinimumTemperature)
|
||||||
@@ -115,6 +116,13 @@ namespace Content.Shared.Chemistry.Reaction
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if((mixerComponent == null && reaction.MixingCategories != null) ||
|
||||||
|
mixerComponent != null && reaction.MixingCategories != null && reaction.MixingCategories.Except(mixerComponent.ReactionTypes).Any())
|
||||||
|
{
|
||||||
|
lowestUnitReactions = FixedPoint2.Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var attempt = new ReactionAttemptEvent(reaction, solution);
|
var attempt = new ReactionAttemptEvent(reaction, solution);
|
||||||
RaiseLocalEvent(owner, attempt, false);
|
RaiseLocalEvent(owner, attempt, false);
|
||||||
if (attempt.Cancelled)
|
if (attempt.Cancelled)
|
||||||
@@ -215,7 +223,7 @@ namespace Content.Shared.Chemistry.Reaction
|
|||||||
/// Removes the reactants from the solution, then returns a solution with all products.
|
/// Removes the reactants from the solution, then returns a solution with all products.
|
||||||
/// WARNING: Does not trigger reactions between solution and new products.
|
/// WARNING: Does not trigger reactions between solution and new products.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool ProcessReactions(Solution solution, EntityUid owner, FixedPoint2 maxVolume, SortedSet<ReactionPrototype> reactions)
|
private bool ProcessReactions(Solution solution, EntityUid owner, FixedPoint2 maxVolume, SortedSet<ReactionPrototype> reactions, ReactionMixerComponent? mixerComponent)
|
||||||
{
|
{
|
||||||
HashSet<ReactionPrototype> toRemove = new();
|
HashSet<ReactionPrototype> toRemove = new();
|
||||||
Solution? products = null;
|
Solution? products = null;
|
||||||
@@ -223,7 +231,7 @@ namespace Content.Shared.Chemistry.Reaction
|
|||||||
// attempt to perform any applicable reaction
|
// attempt to perform any applicable reaction
|
||||||
foreach (var reaction in reactions)
|
foreach (var reaction in reactions)
|
||||||
{
|
{
|
||||||
if (!CanReact(solution, reaction, owner, out var unitReactions))
|
if (!CanReact(solution, reaction, owner, mixerComponent, out var unitReactions))
|
||||||
{
|
{
|
||||||
toRemove.Add(reaction);
|
toRemove.Add(reaction);
|
||||||
continue;
|
continue;
|
||||||
@@ -264,13 +272,13 @@ namespace Content.Shared.Chemistry.Reaction
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Continually react a solution until no more reactions occur.
|
/// Continually react a solution until no more reactions occur.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void FullyReactSolution(Solution solution, EntityUid owner) => FullyReactSolution(solution, owner, FixedPoint2.MaxValue);
|
public void FullyReactSolution(Solution solution, EntityUid owner) => FullyReactSolution(solution, owner, FixedPoint2.MaxValue, null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Continually react a solution until no more reactions occur, with a volume constraint.
|
/// Continually react a solution until no more reactions occur, with a volume constraint.
|
||||||
/// If a reaction's products would exceed the max volume, some product is deleted.
|
/// If a reaction's products would exceed the max volume, some product is deleted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void FullyReactSolution(Solution solution, EntityUid owner, FixedPoint2 maxVolume)
|
public void FullyReactSolution(Solution solution, EntityUid owner, FixedPoint2 maxVolume, ReactionMixerComponent? mixerComponent)
|
||||||
{
|
{
|
||||||
// construct the initial set of reactions to check.
|
// construct the initial set of reactions to check.
|
||||||
SortedSet<ReactionPrototype> reactions = new();
|
SortedSet<ReactionPrototype> reactions = new();
|
||||||
@@ -284,7 +292,7 @@ namespace Content.Shared.Chemistry.Reaction
|
|||||||
// exceed the iteration limit.
|
// exceed the iteration limit.
|
||||||
for (var i = 0; i < MaxReactionIterations; i++)
|
for (var i = 0; i < MaxReactionIterations; i++)
|
||||||
{
|
{
|
||||||
if (!ProcessReactions(solution, owner, maxVolume, reactions))
|
if (!ProcessReactions(solution, owner, maxVolume, reactions, mixerComponent))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## Entity
|
||||||
|
|
||||||
|
default-mixing-success = You mix the {$mixed} with the {$mixer}
|
||||||
|
bible-mixing-success = You bless the {$mixed} with the {$mixer}
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
solutions:
|
solutions:
|
||||||
drink:
|
drink:
|
||||||
maxVol: 10
|
maxVol: 10
|
||||||
|
canMix: true
|
||||||
- type: FitsInDispenser
|
- type: FitsInDispenser
|
||||||
solution: drink
|
solution: drink
|
||||||
- type: DrawableSolution
|
- type: DrawableSolution
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
bibleUserOnly: true
|
bibleUserOnly: true
|
||||||
- type: Summonable
|
- type: Summonable
|
||||||
specialItem: SpawnPointGhostRemilia
|
specialItem: SpawnPointGhostRemilia
|
||||||
|
- type: ReactionMixer
|
||||||
|
mixMessage: "bible-mixing-success"
|
||||||
|
reactionTypes:
|
||||||
|
- Holy
|
||||||
- type: ItemCooldown
|
- type: ItemCooldown
|
||||||
- type: Sprite
|
- type: Sprite
|
||||||
netsync: false
|
netsync: false
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
solutions:
|
solutions:
|
||||||
drink: # This solution name and target volume is hard-coded in ChemMasterComponent
|
drink: # This solution name and target volume is hard-coded in ChemMasterComponent
|
||||||
maxVol: 30
|
maxVol: 30
|
||||||
|
canMix: true
|
||||||
- type: RefillableSolution
|
- type: RefillableSolution
|
||||||
solution: drink
|
solution: drink
|
||||||
- type: DrainableSolution
|
- type: DrainableSolution
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
solutions:
|
solutions:
|
||||||
beaker:
|
beaker:
|
||||||
maxVol: 50
|
maxVol: 50
|
||||||
|
canMix: true
|
||||||
- type: FitsInDispenser
|
- type: FitsInDispenser
|
||||||
solution: beaker
|
solution: beaker
|
||||||
- type: RefillableSolution
|
- type: RefillableSolution
|
||||||
@@ -113,6 +114,7 @@
|
|||||||
solutions:
|
solutions:
|
||||||
beaker:
|
beaker:
|
||||||
maxVol: 100
|
maxVol: 100
|
||||||
|
canMix: true
|
||||||
- type: Appearance
|
- type: Appearance
|
||||||
- type: SolutionContainerVisuals
|
- type: SolutionContainerVisuals
|
||||||
maxFillLevels: 6
|
maxFillLevels: 6
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
solutions:
|
solutions:
|
||||||
beaker:
|
beaker:
|
||||||
maxVol: 300
|
maxVol: 300
|
||||||
|
canMix: true
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
name: dropper
|
name: dropper
|
||||||
|
|||||||
@@ -7,3 +7,14 @@
|
|||||||
amount: 0.5
|
amount: 0.5
|
||||||
products:
|
products:
|
||||||
Protein: 0.5
|
Protein: 0.5
|
||||||
|
|
||||||
|
- type: reaction
|
||||||
|
id: BloodToWine
|
||||||
|
impact: Low
|
||||||
|
requiredMixerCategories:
|
||||||
|
- Holy
|
||||||
|
reactants:
|
||||||
|
Blood:
|
||||||
|
amount: 1
|
||||||
|
products:
|
||||||
|
Wine: 1
|
||||||
|
|||||||
Reference in New Issue
Block a user