diff --git a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs index 065067eef0..282b2a169d 100644 --- a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs +++ b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading.Tasks; using Content.Server.Chemistry.EntitySystems; +using Content.Server.Engineering.Components; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reaction; using NUnit.Framework; @@ -23,7 +24,8 @@ namespace Content.IntegrationTests.Tests.Chemistry - type: SolutionContainerManager solutions: beaker: - maxVol: 50"; + maxVol: 50 + canMix: true"; [Test] public async Task TryAllTest() { @@ -58,6 +60,14 @@ namespace Content.IntegrationTests.Tests.Chemistry } solutionSystem.SetTemperature(beaker, component, reactionPrototype.MinimumTemperature); + + if (reactionPrototype.MixingCategories != null) + { + var dummyEntity = entityManager.SpawnEntity(null, MapCoordinates.Nullspace); + var mixerComponent = entityManager.AddComponent(dummyEntity); + mixerComponent.ReactionTypes = reactionPrototype.MixingCategories; + solutionSystem.UpdateChemicals(beaker, component, true, mixerComponent); + } }); await server.WaitIdleAsync(); diff --git a/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs index f4efbf9d47..8be0926fb7 100644 --- a/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs +++ b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs @@ -28,5 +28,6 @@ public sealed partial class ChemistrySystem : EntitySystem // Why ChemMaster duplicates reagentdispenser nobody knows. InitializeHypospray(); InitializeInjector(); + InitializeMixing(); } } diff --git a/Content.Server/Chemistry/EntitySystems/ChemistrySystemMixer.cs b/Content.Server/Chemistry/EntitySystems/ChemistrySystemMixer.cs new file mode 100644 index 0000000000..6f9cf3fd0c --- /dev/null +++ b/Content.Server/Chemistry/EntitySystems/ChemistrySystemMixer.cs @@ -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(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); + } +} diff --git a/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs index 979cf15456..ea2e9e67d7 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Linq; using Content.Server.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; @@ -8,6 +9,7 @@ using Content.Shared.Examine; using Content.Shared.FixedPoint; using JetBrains.Annotations; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Server.Chemistry.EntitySystems; @@ -115,12 +117,12 @@ public sealed partial class SolutionContainerSystem : EntitySystem 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 if (needsReactionsProcessing && solutionHolder.CanReact) { - _chemistrySystem.FullyReactSolution(solutionHolder, uid, solutionHolder.MaxVolume); + _chemistrySystem.FullyReactSolution(solutionHolder, uid, solutionHolder.MaxVolume, mixerComponent); } UpdateAppearance(uid, solutionHolder); @@ -333,6 +335,36 @@ public sealed partial class SolutionContainerSystem : EntitySystem 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. diff --git a/Content.Shared/Chemistry/Components/Solution.Managerial.cs b/Content.Shared/Chemistry/Components/Solution.Managerial.cs index 5319116307..436d7e186a 100644 --- a/Content.Shared/Chemistry/Components/Solution.Managerial.cs +++ b/Content.Shared/Chemistry/Components/Solution.Managerial.cs @@ -14,6 +14,13 @@ namespace Content.Shared.Chemistry.Components [DataField("canReact")] public bool CanReact { get; set; } = true; + /// + /// If reactions can occur via mixing. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("canMix")] + public bool CanMix { get; set; } = false; + /// /// Volume needed to fill this container. /// diff --git a/Content.Shared/Chemistry/Reaction/ReactionMixerComponent.cs b/Content.Shared/Chemistry/Reaction/ReactionMixerComponent.cs new file mode 100644 index 0000000000..55d19e353b --- /dev/null +++ b/Content.Shared/Chemistry/Reaction/ReactionMixerComponent.cs @@ -0,0 +1,29 @@ +using Content.Shared.Chemistry.Components; + +namespace Content.Shared.Chemistry.Reaction; + +[RegisterComponent] +public sealed class ReactionMixerComponent : Component +{ + /// + /// A list of IDs for categories of reactions that can be mixed (i.e. HOLY for a bible, DRINK for a spoon) + /// + [ViewVariables] + [DataField("reactionTypes")] + public List ReactionTypes = default!; + + /// + /// A string which identifies the string to be sent when successfully mixing a solution + /// + [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); diff --git a/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs b/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs index 9f45ae9305..c99b50774e 100644 --- a/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs +++ b/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs @@ -38,6 +38,12 @@ namespace Content.Shared.Chemistry.Reaction [DataField("maxTemp")] public float MaximumTemperature = float.PositiveInfinity; + /// + /// The required mixing categories for an entity to mix the solution with for the reaction to occur + /// + [DataField("requiredMixerCategories")] + public List? MixingCategories = null; + /// /// Reagents created when the reaction occurs. /// diff --git a/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs b/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs index c0c224ab36..2bf1129f9e 100644 --- a/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs +++ b/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; using Content.Shared.FixedPoint; +using Content.Shared.Interaction.Events; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -102,7 +103,7 @@ namespace Content.Shared.Chemistry.Reaction /// The reaction to check. /// How many times this reaction can occur. /// - 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; if (solution.Temperature < reaction.MinimumTemperature) @@ -115,6 +116,13 @@ namespace Content.Shared.Chemistry.Reaction 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); RaiseLocalEvent(owner, attempt, false); 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. /// WARNING: Does not trigger reactions between solution and new products. /// - private bool ProcessReactions(Solution solution, EntityUid owner, FixedPoint2 maxVolume, SortedSet reactions) + private bool ProcessReactions(Solution solution, EntityUid owner, FixedPoint2 maxVolume, SortedSet reactions, ReactionMixerComponent? mixerComponent) { HashSet toRemove = new(); Solution? products = null; @@ -223,7 +231,7 @@ namespace Content.Shared.Chemistry.Reaction // attempt to perform any applicable reaction 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); continue; @@ -264,13 +272,13 @@ namespace Content.Shared.Chemistry.Reaction /// /// Continually react a solution until no more reactions occur. /// - 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); /// /// 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. /// - 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. SortedSet reactions = new(); @@ -284,7 +292,7 @@ namespace Content.Shared.Chemistry.Reaction // exceed the iteration limit. for (var i = 0; i < MaxReactionIterations; i++) { - if (!ProcessReactions(solution, owner, maxVolume, reactions)) + if (!ProcessReactions(solution, owner, maxVolume, reactions, mixerComponent)) return; } diff --git a/Resources/Locale/en-US/chemistry/components/mixing-component.ftl b/Resources/Locale/en-US/chemistry/components/mixing-component.ftl new file mode 100644 index 0000000000..2abe5644f5 --- /dev/null +++ b/Resources/Locale/en-US/chemistry/components/mixing-component.ftl @@ -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} + diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml index 866c45ef86..6cd33b1f19 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml @@ -9,6 +9,7 @@ solutions: drink: maxVol: 10 + canMix: true - type: FitsInDispenser solution: drink - type: DrawableSolution diff --git a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml index f948e85685..28beeb979a 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml @@ -22,6 +22,10 @@ bibleUserOnly: true - type: Summonable specialItem: SpawnPointGhostRemilia + - type: ReactionMixer + mixMessage: "bible-mixing-success" + reactionTypes: + - Holy - type: ItemCooldown - type: Sprite netsync: false diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemistry-bottles.yml b/Resources/Prototypes/Entities/Objects/Specific/chemistry-bottles.yml index 3eaa719a77..c038fd18a8 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/chemistry-bottles.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/chemistry-bottles.yml @@ -21,6 +21,7 @@ solutions: drink: # This solution name and target volume is hard-coded in ChemMasterComponent maxVol: 30 + canMix: true - type: RefillableSolution solution: drink - type: DrainableSolution diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml index a3af012bed..2076a8e482 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml @@ -20,6 +20,7 @@ solutions: beaker: maxVol: 50 + canMix: true - type: FitsInDispenser solution: beaker - type: RefillableSolution @@ -113,6 +114,7 @@ solutions: beaker: maxVol: 100 + canMix: true - type: Appearance - type: SolutionContainerVisuals maxFillLevels: 6 @@ -169,6 +171,7 @@ solutions: beaker: maxVol: 300 + canMix: true - type: entity name: dropper diff --git a/Resources/Prototypes/Recipes/Reactions/single_reagent.yml b/Resources/Prototypes/Recipes/Reactions/single_reagent.yml index a99b764851..cd15576d29 100644 --- a/Resources/Prototypes/Recipes/Reactions/single_reagent.yml +++ b/Resources/Prototypes/Recipes/Reactions/single_reagent.yml @@ -7,3 +7,14 @@ amount: 0.5 products: Protein: 0.5 + +- type: reaction + id: BloodToWine + impact: Low + requiredMixerCategories: + - Holy + reactants: + Blood: + amount: 1 + products: + Wine: 1