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:
Timothy Teakettle
2022-12-20 04:05:02 +00:00
committed by GitHub
parent a497167e45
commit c046666578
14 changed files with 168 additions and 9 deletions

View File

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

View File

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

View File

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

View File

@@ -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.

View File

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

View 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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