From cfa94be4c2044146298d07c703f3b71bc377ca63 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Wed, 17 Apr 2024 21:49:58 -0400 Subject: [PATCH] Add ability to shake fizzy drinks so they spray in peoples' faces (#25574) * Implemented Shakeable * Prevent shaking open Openables * Prevent shaking empty drinks. Moved part of DrinkSystem to Shared. * DrinkSystem can have a little more prediction, as a treat * Cleanup * Overhauled PressurizedDrink * Make soda cans/bottles and champagne shakeable. The drink shaker too, for fun. * We do a little refactoring. PressurizedDrink is now PressurizedSolution, and fizziness now only works on solutions containing a reagent marked as fizzy. * Documentation, cleanup, and tweaks. * Changed fizziness calculation to use a cubic-out easing curve. * Removed broken YAML that has avoid the linter's wrath for far too long * Changed reagent fizzy bool to fizziness float. Solution fizzability now scales with reagent proportion. * Rename file to match changed class name * DoAfter improvements. Cancel if the user moves away; block if no hands. * Match these filenames too * And this one * guh * Updated to use Shared puddle methods * Various fixes and improvements. * Made AttemptShakeEvent a struct * AttemptAddFizzinessEvent too --- .../Nutrition/EntitySystems/DrinkSystem.cs | 7 + .../Components/PressurizedDrinkComponent.cs | 27 -- .../Nutrition/EntitySystems/DrinkSystem.cs | 95 +----- .../Chemistry/Reagent/ReagentPrototype.cs | 7 + .../Nutrition/Components/DrinkComponent.cs | 22 +- .../PressurizedSolutionComponent.cs | 106 +++++++ .../Components/ShakeableComponent.cs | 50 +++ .../Nutrition/EntitySystems/OpenableSystem.cs | 44 ++- .../PressurizedSolutionSystem.cs | 285 ++++++++++++++++++ .../EntitySystems/ShakeableSystem.cs | 155 ++++++++++ .../EntitySystems/SharedDrinkSystem.cs | 90 ++++++ Resources/Audio/Items/attributions.yml | 10 + Resources/Audio/Items/soda_shake.ogg | Bin 0 -> 18034 bytes Resources/Audio/Items/soda_spray.ogg | Bin 0 -> 16910 bytes .../pressurized-solution-component.ftl | 3 + .../components/shakeable-component.ftl | 3 + .../Consumable/Drinks/drinks-cartons.yml | 3 + .../Consumable/Drinks/drinks_bottles.yml | 3 + .../Objects/Consumable/Drinks/drinks_cans.yml | 4 +- .../Objects/Consumable/Drinks/drinks_fun.yml | 3 + .../Consumable/Drinks/drinks_special.yml | 1 + .../Reagents/Consumable/Drink/alcohol.yml | 22 ++ .../Reagents/Consumable/Drink/base_drink.yml | 3 +- .../Reagents/Consumable/Drink/drinks.yml | 6 + .../Reagents/Consumable/Drink/soda.yml | 4 + 25 files changed, 807 insertions(+), 146 deletions(-) create mode 100644 Content.Client/Nutrition/EntitySystems/DrinkSystem.cs delete mode 100644 Content.Server/Nutrition/Components/PressurizedDrinkComponent.cs rename {Content.Server => Content.Shared}/Nutrition/Components/DrinkComponent.cs (66%) create mode 100644 Content.Shared/Nutrition/Components/PressurizedSolutionComponent.cs create mode 100644 Content.Shared/Nutrition/Components/ShakeableComponent.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/PressurizedSolutionSystem.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/ShakeableSystem.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs create mode 100644 Resources/Audio/Items/soda_shake.ogg create mode 100644 Resources/Audio/Items/soda_spray.ogg create mode 100644 Resources/Locale/en-US/nutrition/components/pressurized-solution-component.ftl create mode 100644 Resources/Locale/en-US/nutrition/components/shakeable-component.ftl diff --git a/Content.Client/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Client/Nutrition/EntitySystems/DrinkSystem.cs new file mode 100644 index 0000000000..16dbecb793 --- /dev/null +++ b/Content.Client/Nutrition/EntitySystems/DrinkSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Nutrition.EntitySystems; + +namespace Content.Client.Nutrition.EntitySystems; + +public sealed class DrinkSystem : SharedDrinkSystem +{ +} diff --git a/Content.Server/Nutrition/Components/PressurizedDrinkComponent.cs b/Content.Server/Nutrition/Components/PressurizedDrinkComponent.cs deleted file mode 100644 index aafb3bc106..0000000000 --- a/Content.Server/Nutrition/Components/PressurizedDrinkComponent.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Content.Server.Nutrition.EntitySystems; -using Robust.Shared.Audio; - -namespace Content.Server.Nutrition.Components; - -/// -/// Lets a drink burst open when thrown while closed. -/// Requires and to work. -/// -[RegisterComponent, Access(typeof(DrinkSystem))] -public sealed partial class PressurizedDrinkComponent : Component -{ - /// - /// Chance for the drink to burst when thrown while closed. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float BurstChance = 0.25f; - - /// - /// Sound played when the drink bursts. - /// - [DataField] - public SoundSpecifier BurstSound = new SoundPathSpecifier("/Audio/Effects/flash_bang.ogg") - { - Params = AudioParams.Default.WithVolume(-4) - }; -} diff --git a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs index 74637d4813..aa2ed71d8f 100644 --- a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs @@ -5,7 +5,6 @@ using Content.Server.Chemistry.ReagentEffects; using Content.Server.Fluids.EntitySystems; using Content.Server.Forensics; using Content.Server.Inventory; -using Content.Server.Nutrition.Components; using Content.Server.Popups; using Content.Shared.Administration.Logs; using Content.Shared.Body.Components; @@ -16,7 +15,6 @@ using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; using Content.Shared.DoAfter; -using Content.Shared.Examine; using Content.Shared.FixedPoint; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; @@ -25,24 +23,21 @@ using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; -using Content.Shared.Throwing; using Content.Shared.Verbs; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Server.Nutrition.EntitySystems; -public sealed class DrinkSystem : EntitySystem +public sealed class DrinkSystem : SharedDrinkSystem { [Dependency] private readonly BodySystem _body = default!; [Dependency] private readonly FlavorProfileSystem _flavorProfile = default!; [Dependency] private readonly FoodSystem _food = default!; [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly OpenableSystem _openable = default!; @@ -66,33 +61,10 @@ public sealed class DrinkSystem : EntitySystem SubscribeLocalEvent(OnDrinkInit); // run before inventory so for bucket it always tries to drink before equipping (when empty) // run after openable so its always open -> drink - SubscribeLocalEvent(OnUse, before: new[] { typeof(ServerInventorySystem) }, after: new[] { typeof(OpenableSystem) }); + SubscribeLocalEvent(OnUse, before: [typeof(ServerInventorySystem)], after: [typeof(OpenableSystem)]); SubscribeLocalEvent(AfterInteract); SubscribeLocalEvent>(AddDrinkVerb); - // put drink amount after opened - SubscribeLocalEvent(OnExamined, after: new[] { typeof(OpenableSystem) }); SubscribeLocalEvent(OnDoAfter); - - SubscribeLocalEvent(OnPressurizedDrinkLand); - } - - private FixedPoint2 DrinkVolume(EntityUid uid, DrinkComponent? component = null) - { - if (!Resolve(uid, ref component)) - return FixedPoint2.Zero; - - if (!_solutionContainer.TryGetSolution(uid, component.Solution, out _, out var sol)) - return FixedPoint2.Zero; - - return sol.Volume; - } - - public bool IsEmpty(EntityUid uid, DrinkComponent? component = null) - { - if (!Resolve(uid, ref component)) - return true; - - return DrinkVolume(uid, component) <= 0; } /// @@ -129,38 +101,6 @@ public sealed class DrinkSystem : EntitySystem return total; } - private void OnExamined(Entity entity, ref ExaminedEvent args) - { - TryComp(entity, out var openable); - if (_openable.IsClosed(entity.Owner, null, openable) || !args.IsInDetailsRange || !entity.Comp.Examinable) - return; - - var empty = IsEmpty(entity, entity.Comp); - if (empty) - { - args.PushMarkup(Loc.GetString("drink-component-on-examine-is-empty")); - return; - } - - if (HasComp(entity)) - { - //provide exact measurement for beakers - args.PushText(Loc.GetString("drink-component-on-examine-exact-volume", ("amount", DrinkVolume(entity, entity.Comp)))); - } - else - { - //general approximation - var remainingString = (int) _solutionContainer.PercentFull(entity) switch - { - 100 => "drink-component-on-examine-is-full", - > 66 => "drink-component-on-examine-is-mostly-full", - > 33 => HalfEmptyOrHalfFull(args), - _ => "drink-component-on-examine-is-mostly-empty", - }; - args.PushMarkup(Loc.GetString(remainingString)); - } - } - private void AfterInteract(Entity entity, ref AfterInteractEvent args) { if (args.Handled || args.Target == null || !args.CanReach) @@ -177,25 +117,6 @@ public sealed class DrinkSystem : EntitySystem args.Handled = TryDrink(args.User, args.User, entity.Comp, entity); } - private void OnPressurizedDrinkLand(Entity entity, ref LandEvent args) - { - if (!TryComp(entity, out var drink) || !TryComp(entity, out var openable)) - return; - - if (!openable.Opened && - _random.Prob(entity.Comp.BurstChance) && - _solutionContainer.TryGetSolution(entity.Owner, drink.Solution, out var soln, out var interactions)) - { - // using SetOpen instead of TryOpen to not play 2 sounds - _openable.SetOpen(entity, true, openable); - - var solution = _solutionContainer.SplitSolution(soln.Value, interactions.Volume); - _puddle.TrySpillAt(entity, solution, out _); - - _audio.PlayPvs(entity.Comp.BurstSound, entity); - } - } - private void OnDrinkInit(Entity entity, ref ComponentInit args) { if (TryComp(entity, out var existingDrainable)) @@ -433,16 +354,4 @@ public sealed class DrinkSystem : EntitySystem ev.Verbs.Add(verb); } - - // some see half empty, and others see half full - private string HalfEmptyOrHalfFull(ExaminedEvent args) - { - string remainingString = "drink-component-on-examine-is-half-full"; - - if (TryComp(args.Examiner, out var examiner) && examiner.EntityName.Length > 0 - && string.Compare(examiner.EntityName.Substring(0, 1), "m", StringComparison.InvariantCultureIgnoreCase) > 0) - remainingString = "drink-component-on-examine-is-half-empty"; - - return remainingString; - } } diff --git a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs index 5d6d9d2120..df1b1aa20b 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs @@ -104,6 +104,13 @@ namespace Content.Shared.Chemistry.Reagent [DataField] public bool Slippery; + /// + /// How easily this reagent becomes fizzy when aggitated. + /// 0 - completely flat, 1 - fizzes up when nudged. + /// + [DataField] + public float Fizziness; + /// /// How much reagent slows entities down if it's part of a puddle. /// 0 - no slowdown; 1 - can't move. diff --git a/Content.Server/Nutrition/Components/DrinkComponent.cs b/Content.Shared/Nutrition/Components/DrinkComponent.cs similarity index 66% rename from Content.Server/Nutrition/Components/DrinkComponent.cs rename to Content.Shared/Nutrition/Components/DrinkComponent.cs index 20d47cda88..17baaef5a3 100644 --- a/Content.Server/Nutrition/Components/DrinkComponent.cs +++ b/Content.Shared/Nutrition/Components/DrinkComponent.cs @@ -1,28 +1,30 @@ -using Content.Server.Nutrition.EntitySystems; +using Content.Shared.Nutrition.EntitySystems; using Content.Shared.FixedPoint; using Robust.Shared.Audio; +using Robust.Shared.GameStates; -namespace Content.Server.Nutrition.Components; +namespace Content.Shared.Nutrition.Components; -[RegisterComponent, Access(typeof(DrinkSystem))] +[NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, Access(typeof(SharedDrinkSystem))] public sealed partial class DrinkComponent : Component { - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string Solution = "drink"; - [DataField] + [DataField, AutoNetworkedField] public SoundSpecifier UseSound = new SoundPathSpecifier("/Audio/Items/drink.ogg"); - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public FixedPoint2 TransferAmount = FixedPoint2.New(5); /// /// How long it takes to drink this yourself. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public float Delay = 1; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public bool Examinable = true; /// @@ -30,12 +32,12 @@ public sealed partial class DrinkComponent : Component /// This means other systems such as equipping on use can run. /// Example usecase is the bucket. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public bool IgnoreEmpty; /// /// This is how many seconds it takes to force feed someone this drink. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public float ForceFeedDelay = 3; } diff --git a/Content.Shared/Nutrition/Components/PressurizedSolutionComponent.cs b/Content.Shared/Nutrition/Components/PressurizedSolutionComponent.cs new file mode 100644 index 0000000000..7060f3bf79 --- /dev/null +++ b/Content.Shared/Nutrition/Components/PressurizedSolutionComponent.cs @@ -0,0 +1,106 @@ +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Nutrition.Components; + +/// +/// Represents a solution container that can hold the pressure from a solution that +/// gets fizzy when aggitated, and can spray the solution when opened or thrown. +/// Handles simulating the fizziness of the solution, responding to aggitating events, +/// and spraying the solution out when opening or throwing the entity. +/// +[NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[RegisterComponent, Access(typeof(PressurizedSolutionSystem))] +public sealed partial class PressurizedSolutionComponent : Component +{ + /// + /// The name of the solution to use. + /// + [DataField] + public string Solution = "drink"; + + /// + /// The sound to play when the solution sprays out of the container. + /// + [DataField] + public SoundSpecifier SpraySound = new SoundPathSpecifier("/Audio/Items/soda_spray.ogg"); + + /// + /// The longest amount of time that the solution can remain fizzy after being aggitated. + /// Put another way, how long the solution will remain fizzy when aggitated the maximum amount. + /// Used to calculate the current fizziness level. + /// + [DataField] + public TimeSpan FizzinessMaxDuration = TimeSpan.FromSeconds(120); + + /// + /// The time at which the solution will be fully settled after being shaken. + /// + [DataField, AutoNetworkedField, AutoPausedField] + public TimeSpan FizzySettleTime; + + /// + /// How much to increase the solution's fizziness each time it's shaken. + /// This assumes the solution has maximum fizzability. + /// A value of 1 will maximize it with a single shake, and a value of + /// 0.5 will increase it by half with each shake. + /// + [DataField] + public float FizzinessAddedOnShake = 1.0f; + + /// + /// How much to increase the solution's fizziness when it lands after being thrown. + /// This assumes the solution has maximum fizzability. + /// + [DataField] + public float FizzinessAddedOnLand = 0.25f; + + /// + /// How much to modify the chance of spraying when the entity is opened. + /// Increasing this effectively increases the fizziness value when checking if it should spray. + /// + [DataField] + public float SprayChanceModOnOpened = -0.01f; // Just enough to prevent spraying at 0 fizziness + + /// + /// How much to modify the chance of spraying when the entity is shaken. + /// Increasing this effectively increases the fizziness value when checking if it should spray. + /// + [DataField] + public float SprayChanceModOnShake = -1; // No spraying when shaken by default + + /// + /// How much to modify the chance of spraying when the entity lands after being thrown. + /// Increasing this effectively increases the fizziness value when checking if it should spray. + /// + [DataField] + public float SprayChanceModOnLand = 0.25f; + + /// + /// Holds the current randomly-rolled threshold value for spraying. + /// If fizziness exceeds this value when the entity is opened, it will spray. + /// By rolling this value when the entity is aggitated, we can have randomization + /// while still having prediction! + /// + [DataField, AutoNetworkedField] + public float SprayFizzinessThresholdRoll; + + /// + /// Popup message shown to user when sprayed by the solution. + /// + [DataField] + public LocId SprayHolderMessageSelf = "pressurized-solution-spray-holder-self"; + + /// + /// Popup message shown to others when a user is sprayed by the solution. + /// + [DataField] + public LocId SprayHolderMessageOthers = "pressurized-solution-spray-holder-others"; + + /// + /// Popup message shown above the entity when the solution sprays without a target. + /// + [DataField] + public LocId SprayGroundMessage = "pressurized-solution-spray-ground"; +} diff --git a/Content.Shared/Nutrition/Components/ShakeableComponent.cs b/Content.Shared/Nutrition/Components/ShakeableComponent.cs new file mode 100644 index 0000000000..cc1c08a9b2 --- /dev/null +++ b/Content.Shared/Nutrition/Components/ShakeableComponent.cs @@ -0,0 +1,50 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Nutrition.Components; + +/// +/// Adds a "Shake" verb to the entity's verb menu. +/// Handles checking the entity can be shaken, displaying popups when shaking, +/// and raising a ShakeEvent when a shake occurs. +/// Reacting to being shaken is left up to other components. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class ShakeableComponent : Component +{ + /// + /// How long it takes to shake this item. + /// + [DataField] + public TimeSpan ShakeDuration = TimeSpan.FromSeconds(1f); + + /// + /// Does the entity need to be in the user's hand in order to be shaken? + /// + [DataField] + public bool RequireInHand; + + /// + /// Label to display in the verbs menu for this item's shake action. + /// + [DataField] + public LocId ShakeVerbText = "shakeable-verb"; + + /// + /// Text that will be displayed to the user when shaking this item. + /// + [DataField] + public LocId ShakePopupMessageSelf = "shakeable-popup-message-self"; + + /// + /// Text that will be displayed to other users when someone shakes this item. + /// + [DataField] + public LocId ShakePopupMessageOthers = "shakeable-popup-message-others"; + + /// + /// The sound that will be played when shaking this item. + /// + [DataField] + public SoundSpecifier ShakeSound = new SoundPathSpecifier("/Audio/Items/soda_shake.ogg"); +} diff --git a/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs b/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs index 0ad0877d22..2934ced8b4 100644 --- a/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs @@ -16,9 +16,9 @@ namespace Content.Shared.Nutrition.EntitySystems; /// public sealed partial class OpenableSystem : EntitySystem { - [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; - [Dependency] protected readonly SharedAudioSystem Audio = default!; - [Dependency] protected readonly SharedPopupSystem Popup = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; public override void Initialize() { @@ -31,6 +31,8 @@ public sealed partial class OpenableSystem : EntitySystem SubscribeLocalEvent(HandleIfClosed); SubscribeLocalEvent>(AddOpenCloseVerbs); SubscribeLocalEvent(OnTransferAttempt); + SubscribeLocalEvent(OnAttemptShake); + SubscribeLocalEvent(OnAttemptAddFizziness); } private void OnInit(EntityUid uid, OpenableComponent comp, ComponentInit args) @@ -100,6 +102,20 @@ public sealed partial class OpenableSystem : EntitySystem } } + private void OnAttemptShake(Entity entity, ref AttemptShakeEvent args) + { + // Prevent shaking open containers + if (entity.Comp.Opened) + args.Cancelled = true; + } + + private void OnAttemptAddFizziness(Entity entity, ref AttemptAddFizzinessEvent args) + { + // Can't add fizziness to an open container + if (entity.Comp.Opened) + args.Cancelled = true; + } + /// /// Returns true if the entity either does not have OpenableComponent or it is opened. /// Drinks that don't have OpenableComponent are automatically open, so it returns true. @@ -126,7 +142,7 @@ public sealed partial class OpenableSystem : EntitySystem return false; if (user != null) - Popup.PopupEntity(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value); + _popup.PopupEntity(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value); return true; } @@ -139,13 +155,13 @@ public sealed partial class OpenableSystem : EntitySystem if (!Resolve(uid, ref comp)) return; - Appearance.SetData(uid, OpenableVisuals.Opened, comp.Opened, appearance); + _appearance.SetData(uid, OpenableVisuals.Opened, comp.Opened, appearance); } /// /// Sets the opened field and updates open visuals. /// - public void SetOpen(EntityUid uid, bool opened = true, OpenableComponent? comp = null) + public void SetOpen(EntityUid uid, bool opened = true, OpenableComponent? comp = null, EntityUid? user = null) { if (!Resolve(uid, ref comp, false) || opened == comp.Opened) return; @@ -155,12 +171,12 @@ public sealed partial class OpenableSystem : EntitySystem if (opened) { - var ev = new OpenableOpenedEvent(); + var ev = new OpenableOpenedEvent(user); RaiseLocalEvent(uid, ref ev); } else { - var ev = new OpenableClosedEvent(); + var ev = new OpenableClosedEvent(user); RaiseLocalEvent(uid, ref ev); } @@ -176,8 +192,8 @@ public sealed partial class OpenableSystem : EntitySystem if (!Resolve(uid, ref comp, false) || comp.Opened) return false; - SetOpen(uid, true, comp); - Audio.PlayPredicted(comp.Sound, uid, user); + SetOpen(uid, true, comp, user); + _audio.PlayPredicted(comp.Sound, uid, user); return true; } @@ -190,9 +206,9 @@ public sealed partial class OpenableSystem : EntitySystem if (!Resolve(uid, ref comp, false) || !comp.Opened || !comp.Closeable) return false; - SetOpen(uid, false, comp); + SetOpen(uid, false, comp, user); if (comp.CloseSound != null) - Audio.PlayPredicted(comp.CloseSound, uid, user); + _audio.PlayPredicted(comp.CloseSound, uid, user); return true; } } @@ -201,10 +217,10 @@ public sealed partial class OpenableSystem : EntitySystem /// Raised after an Openable is opened. /// [ByRefEvent] -public record struct OpenableOpenedEvent; +public record struct OpenableOpenedEvent(EntityUid? User = null); /// /// Raised after an Openable is closed. /// [ByRefEvent] -public record struct OpenableClosedEvent; +public record struct OpenableClosedEvent(EntityUid? User = null); diff --git a/Content.Shared/Nutrition/EntitySystems/PressurizedSolutionSystem.cs b/Content.Shared/Nutrition/EntitySystems/PressurizedSolutionSystem.cs new file mode 100644 index 0000000000..d63b8e7326 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/PressurizedSolutionSystem.cs @@ -0,0 +1,285 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Nutrition.Components; +using Content.Shared.Throwing; +using Content.Shared.IdentityManagement; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Random; +using Robust.Shared.Timing; +using Robust.Shared.Prototypes; +using Robust.Shared.Network; +using Content.Shared.Fluids; +using Content.Shared.Popups; + +namespace Content.Shared.Nutrition.EntitySystems; + +public sealed partial class PressurizedSolutionSystem : EntitySystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly OpenableSystem _openable = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedPuddleSystem _puddle = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShake); + SubscribeLocalEvent(OnOpened); + SubscribeLocalEvent(OnLand); + SubscribeLocalEvent(OnSolutionUpdate); + } + + /// + /// Helper method for checking if the solution's fizziness is high enough to spray. + /// is added to the actual fizziness for the comparison. + /// + private bool SprayCheck(Entity entity, float chanceMod = 0) + { + return Fizziness((entity, entity.Comp)) + chanceMod > entity.Comp.SprayFizzinessThresholdRoll; + } + + /// + /// Calculates how readily the contained solution becomes fizzy. + /// + private float SolutionFizzability(Entity entity) + { + if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var _, out var solution)) + return 0; + + // An empty solution can't be fizzy + if (solution.Volume <= 0) + return 0; + + var totalFizzability = 0f; + + // Check each reagent in the solution + foreach (var reagent in solution.Contents) + { + if (_prototypeManager.TryIndex(reagent.Reagent.Prototype, out ReagentPrototype? reagentProto) && reagentProto != null) + { + // What portion of the solution is this reagent? + var proportion = (float) (reagent.Quantity / solution.Volume); + totalFizzability += reagentProto.Fizziness * proportion; + } + } + + return totalFizzability; + } + + /// + /// Increases the fizziness level of the solution by the given amount, + /// scaled by the solution's fizzability. + /// 0 will result in no change, and 1 will maximize fizziness. + /// Also rerolls the spray threshold. + /// + private void AddFizziness(Entity entity, float amount) + { + var fizzability = SolutionFizzability(entity); + + // Can't add fizziness if the solution isn't fizzy + if (fizzability <= 0) + return; + + // Make sure nothing is preventing fizziness from being added + var attemptEv = new AttemptAddFizzinessEvent(entity, amount); + RaiseLocalEvent(entity, ref attemptEv); + if (attemptEv.Cancelled) + return; + + // Scale added fizziness by the solution's fizzability + amount *= fizzability; + + // Convert fizziness to time + var duration = amount * entity.Comp.FizzinessMaxDuration; + + // Add to the existing settle time, if one exists. Otherwise, add to the current time + var start = entity.Comp.FizzySettleTime > _timing.CurTime ? entity.Comp.FizzySettleTime : _timing.CurTime; + var newTime = start + duration; + + // Cap the maximum fizziness + var maxEnd = _timing.CurTime + entity.Comp.FizzinessMaxDuration; + if (newTime > maxEnd) + newTime = maxEnd; + + entity.Comp.FizzySettleTime = newTime; + + // Roll a new fizziness threshold + RollSprayThreshold(entity); + } + + /// + /// Helper method. Performs a . If it passes, calls . If it fails, . + /// + private void SprayOrAddFizziness(Entity entity, float chanceMod = 0, float fizzinessToAdd = 0, EntityUid? user = null) + { + if (SprayCheck(entity, chanceMod)) + TrySpray((entity, entity.Comp), user); + else + AddFizziness(entity, fizzinessToAdd); + } + + /// + /// Randomly generates a new spray threshold. + /// This is the value used to compare fizziness against when doing . + /// Since RNG will give different results between client and server, this is run on the server + /// and synced to the client by marking the component dirty. + /// We roll this in advance, rather than during , so that the value (hopefully) + /// has time to get synced to the client, so we can try be accurate with prediction. + /// + private void RollSprayThreshold(Entity entity) + { + // Can't predict random, so we wait for the server to tell us + if (!_net.IsServer) + return; + + entity.Comp.SprayFizzinessThresholdRoll = _random.NextFloat(); + Dirty(entity, entity.Comp); + } + + #region Public API + + /// + /// Does the entity contain a solution capable of being fizzy? + /// + public bool CanSpray(Entity entity) + { + if (!Resolve(entity, ref entity.Comp, false)) + return false; + + return SolutionFizzability((entity, entity.Comp)) > 0; + } + + /// + /// Attempts to spray the solution onto the given entity, or the ground if none is given. + /// Fails if the solution isn't able to be sprayed. + /// + public bool TrySpray(Entity entity, EntityUid? target = null) + { + if (!Resolve(entity, ref entity.Comp)) + return false; + + if (!CanSpray(entity)) + return false; + + if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var soln, out var interactions)) + return false; + + // If the container is openable, open it + _openable.SetOpen(entity, true); + + // Get the spray solution from the container + var solution = _solutionContainer.SplitSolution(soln.Value, interactions.Volume); + + // Spray the solution onto the ground and anyone nearby + if (TryComp(entity, out var transform)) + _puddle.TrySplashSpillAt(entity, transform.Coordinates, solution, out _, sound: false); + + var drinkName = Identity.Entity(entity, EntityManager); + + if (target != null) + { + var victimName = Identity.Entity(target.Value, EntityManager); + + var selfMessage = Loc.GetString(entity.Comp.SprayHolderMessageSelf, ("victim", victimName), ("drink", drinkName)); + var othersMessage = Loc.GetString(entity.Comp.SprayHolderMessageOthers, ("victim", victimName), ("drink", drinkName)); + _popup.PopupPredicted(selfMessage, othersMessage, target.Value, target.Value); + } + else + { + // Show a popup to everyone in PVS range + if (_timing.IsFirstTimePredicted) + _popup.PopupEntity(Loc.GetString(entity.Comp.SprayGroundMessage, ("drink", drinkName)), entity); + } + + _audio.PlayPredicted(entity.Comp.SpraySound, entity, target); + + // We just used all our fizziness, so clear it + TryClearFizziness(entity); + + return true; + } + + /// + /// What is the current fizziness level of the solution, from 0 to 1? + /// + public double Fizziness(Entity entity) + { + // No component means no fizz + if (!Resolve(entity, ref entity.Comp, false)) + return 0; + + // No negative fizziness + if (entity.Comp.FizzySettleTime <= _timing.CurTime) + return 0; + + var currentDuration = entity.Comp.FizzySettleTime - _timing.CurTime; + return Easings.InOutCubic((float) Math.Min(currentDuration / entity.Comp.FizzinessMaxDuration, 1)); + } + + /// + /// Attempts to clear any fizziness in the solution. + /// + /// Rolls a new spray threshold. + public void TryClearFizziness(Entity entity) + { + if (!Resolve(entity, ref entity.Comp)) + return; + + entity.Comp.FizzySettleTime = TimeSpan.Zero; + + // Roll a new fizziness threshold + RollSprayThreshold((entity, entity.Comp)); + } + + #endregion + + #region Event Handlers + private void OnMapInit(Entity entity, ref MapInitEvent args) + { + RollSprayThreshold(entity); + } + + private void OnOpened(Entity entity, ref OpenableOpenedEvent args) + { + // Make sure the opener is actually holding the drink + var held = args.User != null && _hands.IsHolding(args.User.Value, entity, out _); + + SprayOrAddFizziness(entity, entity.Comp.SprayChanceModOnOpened, -1, held ? args.User : null); + } + + private void OnShake(Entity entity, ref ShakeEvent args) + { + SprayOrAddFizziness(entity, entity.Comp.SprayChanceModOnShake, entity.Comp.FizzinessAddedOnShake, args.Shaker); + } + + private void OnLand(Entity entity, ref LandEvent args) + { + SprayOrAddFizziness(entity, entity.Comp.SprayChanceModOnLand, entity.Comp.FizzinessAddedOnLand); + } + + private void OnSolutionUpdate(Entity entity, ref SolutionContainerChangedEvent args) + { + if (args.SolutionId != entity.Comp.Solution) + return; + + // If the solution is no longer capable of being fizzy, clear any built up fizziness + if (SolutionFizzability(entity) <= 0) + TryClearFizziness((entity, entity.Comp)); + } + + #endregion +} + +[ByRefEvent] +public record struct AttemptAddFizzinessEvent(Entity Entity, float Amount) +{ + public bool Cancelled; +} diff --git a/Content.Shared/Nutrition/EntitySystems/ShakeableSystem.cs b/Content.Shared/Nutrition/EntitySystems/ShakeableSystem.cs new file mode 100644 index 0000000000..39890aada9 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/ShakeableSystem.cs @@ -0,0 +1,155 @@ +using Content.Shared.DoAfter; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.IdentityManagement; +using Content.Shared.Nutrition.Components; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Serialization; + +namespace Content.Shared.Nutrition.EntitySystems; + +public sealed partial class ShakeableSystem : EntitySystem +{ + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(AddShakeVerb); + SubscribeLocalEvent(OnShakeDoAfter); + } + + private void AddShakeVerb(EntityUid uid, ShakeableComponent component, GetVerbsEvent args) + { + if (args.Hands == null || !args.CanAccess || !args.CanInteract) + return; + + if (!CanShake((uid, component), args.User)) + return; + + var shakeVerb = new Verb() + { + Text = Loc.GetString(component.ShakeVerbText), + Act = () => TryStartShake((args.Target, component), args.User) + }; + args.Verbs.Add(shakeVerb); + } + + private void OnShakeDoAfter(Entity entity, ref ShakeDoAfterEvent args) + { + if (args.Handled || args.Cancelled) + return; + + TryShake((entity, entity.Comp), args.User); + } + + /// + /// Attempts to start the doAfter to shake the entity. + /// Fails and returns false if the entity cannot be shaken for any reason. + /// If successful, displays popup messages, plays shake sound, and starts the doAfter. + /// + public bool TryStartShake(Entity entity, EntityUid user) + { + if (!Resolve(entity, ref entity.Comp)) + return false; + + if (!CanShake(entity, user)) + return false; + + var doAfterArgs = new DoAfterArgs(EntityManager, + user, + entity.Comp.ShakeDuration, + new ShakeDoAfterEvent(), + eventTarget: entity, + target: user, + used: entity) + { + NeedHand = true, + BreakOnDamage = true, + DistanceThreshold = 1, + MovementThreshold = 0.01f, + BreakOnHandChange = entity.Comp.RequireInHand, + }; + if (entity.Comp.RequireInHand) + doAfterArgs.BreakOnHandChange = true; + + if (!_doAfter.TryStartDoAfter(doAfterArgs)) + return false; + + var userName = Identity.Entity(user, EntityManager); + var shakeableName = Identity.Entity(entity, EntityManager); + + var selfMessage = Loc.GetString(entity.Comp.ShakePopupMessageSelf, ("user", userName), ("shakeable", shakeableName)); + var othersMessage = Loc.GetString(entity.Comp.ShakePopupMessageOthers, ("user", userName), ("shakeable", shakeableName)); + _popup.PopupPredicted(selfMessage, othersMessage, user, user); + + _audio.PlayPredicted(entity.Comp.ShakeSound, entity, user); + + return true; + } + + /// + /// Attempts to shake the entity, skipping the doAfter. + /// Fails and returns false if the entity cannot be shaken for any reason. + /// If successful, raises a ShakeEvent on the entity. + /// + public bool TryShake(Entity entity, EntityUid? user = null) + { + if (!Resolve(entity, ref entity.Comp)) + return false; + + if (!CanShake(entity, user)) + return false; + + var ev = new ShakeEvent(user); + RaiseLocalEvent(entity, ref ev); + + return true; + } + + + /// + /// Is it possible for the given user to shake the entity? + /// + public bool CanShake(Entity entity, EntityUid? user = null) + { + if (!Resolve(entity, ref entity.Comp, false)) + return false; + + // If required to be in hand, fail if the user is not holding this entity + if (user != null && entity.Comp.RequireInHand && !_hands.IsHolding(user.Value, entity, out _)) + return false; + + var attemptEv = new AttemptShakeEvent(); + RaiseLocalEvent(entity, ref attemptEv); + if (attemptEv.Cancelled) + return false; + return true; + } +} + +/// +/// Raised when a ShakeableComponent is shaken, after the doAfter completes. +/// +[ByRefEvent] +public record struct ShakeEvent(EntityUid? Shaker); + +/// +/// Raised when trying to shake a ShakeableComponent. If cancelled, the +/// entity will not be shaken. +/// +[ByRefEvent] +public record struct AttemptShakeEvent() +{ + public bool Cancelled; +} + +[Serializable, NetSerializable] +public sealed partial class ShakeDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs b/Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs new file mode 100644 index 0000000000..7cae3b9208 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs @@ -0,0 +1,90 @@ +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Examine; +using Content.Shared.FixedPoint; +using Content.Shared.Nutrition.Components; + +namespace Content.Shared.Nutrition.EntitySystems; + +public abstract partial class SharedDrinkSystem : EntitySystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly OpenableSystem _openable = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAttemptShake); + SubscribeLocalEvent(OnExamined); + } + + protected void OnAttemptShake(Entity entity, ref AttemptShakeEvent args) + { + if (IsEmpty(entity, entity.Comp)) + args.Cancelled = true; + } + + protected void OnExamined(Entity entity, ref ExaminedEvent args) + { + TryComp(entity, out var openable); + if (_openable.IsClosed(entity.Owner, null, openable) || !args.IsInDetailsRange || !entity.Comp.Examinable) + return; + + var empty = IsEmpty(entity, entity.Comp); + if (empty) + { + args.PushMarkup(Loc.GetString("drink-component-on-examine-is-empty")); + return; + } + + if (HasComp(entity)) + { + //provide exact measurement for beakers + args.PushText(Loc.GetString("drink-component-on-examine-exact-volume", ("amount", DrinkVolume(entity, entity.Comp)))); + } + else + { + //general approximation + var remainingString = (int) _solutionContainer.PercentFull(entity) switch + { + 100 => "drink-component-on-examine-is-full", + > 66 => "drink-component-on-examine-is-mostly-full", + > 33 => HalfEmptyOrHalfFull(args), + _ => "drink-component-on-examine-is-mostly-empty", + }; + args.PushMarkup(Loc.GetString(remainingString)); + } + } + + protected FixedPoint2 DrinkVolume(EntityUid uid, DrinkComponent? component = null) + { + if (!Resolve(uid, ref component)) + return FixedPoint2.Zero; + + if (!_solutionContainer.TryGetSolution(uid, component.Solution, out _, out var sol)) + return FixedPoint2.Zero; + + return sol.Volume; + } + + protected bool IsEmpty(EntityUid uid, DrinkComponent? component = null) + { + if (!Resolve(uid, ref component)) + return true; + + return DrinkVolume(uid, component) <= 0; + } + + // some see half empty, and others see half full + private string HalfEmptyOrHalfFull(ExaminedEvent args) + { + string remainingString = "drink-component-on-examine-is-half-full"; + + if (TryComp(args.Examiner, out var examiner) && examiner.EntityName.Length > 0 + && string.Compare(examiner.EntityName.Substring(0, 1), "m", StringComparison.InvariantCultureIgnoreCase) > 0) + remainingString = "drink-component-on-examine-is-half-empty"; + + return remainingString; + } +} diff --git a/Resources/Audio/Items/attributions.yml b/Resources/Audio/Items/attributions.yml index c6fea50bd2..675e4fff24 100644 --- a/Resources/Audio/Items/attributions.yml +++ b/Resources/Audio/Items/attributions.yml @@ -93,6 +93,16 @@ copyright: "User Hanbaal on freesound.org. Converted to ogg by TheShuEd" source: "https://freesound.org/people/Hanbaal/sounds/178669/" +- files: ["soda_shake.ogg"] + license: "CC-BY-NC-4.0" + copyright: "User mcmast on freesound.org. Converted and edited by Tayrtahn" + source: "https://freesound.org/people/mcmast/sounds/456703/" + +- files: ["soda_spray.ogg"] + license: "CC0-1.0" + copyright: "User Hajisounds on freesound.org. Converted and edited by Tayrtahn" + source: "https://freesound.org/people/Hajisounds/sounds/709149/" + - files: ["newton_cradle.ogg"] license: "CC-BY-4.0" copyright: "User LoafDV on freesound.org. Converted to ogg end edited by lzk228" diff --git a/Resources/Audio/Items/soda_shake.ogg b/Resources/Audio/Items/soda_shake.ogg new file mode 100644 index 0000000000000000000000000000000000000000..a596379c93a87618cef43d31da3c6c179abd7728 GIT binary patch literal 18034 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 literal 0 HcmV?d00001 diff --git a/Resources/Locale/en-US/nutrition/components/pressurized-solution-component.ftl b/Resources/Locale/en-US/nutrition/components/pressurized-solution-component.ftl new file mode 100644 index 0000000000..a227d811f6 --- /dev/null +++ b/Resources/Locale/en-US/nutrition/components/pressurized-solution-component.ftl @@ -0,0 +1,3 @@ +pressurized-solution-spray-holder-self = { CAPITALIZE(THE($drink)) } sprays on you! +pressurized-solution-spray-holder-others = { CAPITALIZE(THE($drink)) } sprays on { THE($victim) }! +pressurized-solution-spray-ground = The contents of { THE($drink) } spray out! diff --git a/Resources/Locale/en-US/nutrition/components/shakeable-component.ftl b/Resources/Locale/en-US/nutrition/components/shakeable-component.ftl new file mode 100644 index 0000000000..acc1ecd848 --- /dev/null +++ b/Resources/Locale/en-US/nutrition/components/shakeable-component.ftl @@ -0,0 +1,3 @@ +shakeable-verb = Shake +shakeable-popup-message-others = { CAPITALIZE(THE($user)) } shakes { THE($shakeable) } +shakeable-popup-message-self = You shake { THE($shakeable) } diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml index d60134fce0..aef0c5a8f5 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml @@ -15,6 +15,9 @@ solutions: drink: maxVol: 50 + - type: PressurizedSolution + solution: drink + - type: Shakeable - type: Sprite state: icon - type: Item diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml index 0c7707c5f2..73b1e06f9b 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml @@ -39,6 +39,9 @@ - type: PhysicalComposition materialComposition: Plastic: 100 + - type: PressurizedSolution + solution: drink + - type: Shakeable - type: entity parent: DrinkBottlePlasticBaseFull diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml index 585e5ed14d..7dcd3fa603 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml @@ -6,7 +6,7 @@ components: - type: Drink - type: Openable - - type: PressurizedDrink + - type: Shakeable - type: SolutionContainerManager solutions: drink: @@ -34,6 +34,8 @@ solution: drink - type: DrainableSolution solution: drink + - type: PressurizedSolution + solution: drink - type: Appearance - type: GenericVisualizer visuals: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml index 2fd2331f91..ef6208b69d 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml @@ -146,6 +146,9 @@ - type: RandomFillSolution solution: drink weightedRandomId: RandomFillMopwata + - type: PressurizedSolution + solution: drink + - type: Shakeable - type: Appearance - type: GenericVisualizer visuals: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml index c80398e349..a7f1bdbec6 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml @@ -9,6 +9,7 @@ drink: maxVol: 100 - type: Drink + - type: Shakeable # Doesn't do anything, but I mean... - type: FitsInDispenser solution: drink - type: DrawableSolution diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml index 44eba0f848..ef02161165 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml @@ -39,6 +39,7 @@ metamorphicMaxFillLevels: 5 metamorphicFillBaseName: fill- metamorphicChangeColor: false + fizziness: 0.6 - type: reagent id: Beer @@ -55,6 +56,7 @@ metamorphicMaxFillLevels: 6 metamorphicFillBaseName: fill- metamorphicChangeColor: true + fizziness: 0.6 - type: reagent id: BlueCuracao @@ -458,6 +460,7 @@ - !type:AdjustReagent reagent: Ethanol amount: 0.3 + fizziness: 0.8 # Mixed Alcohol @@ -675,6 +678,7 @@ - !type:AdjustReagent reagent: Ethanol amount: 0.15 + fizziness: 0.3 - type: reagent id: BlackRussian @@ -814,6 +818,7 @@ - !type:AdjustReagent reagent: Ethanol amount: 0.07 + fizziness: 0.2 - type: reagent id: DemonsBlood @@ -829,6 +834,7 @@ metamorphicMaxFillLevels: 4 metamorphicFillBaseName: fill- metamorphicChangeColor: true + fizziness: 0.3 - type: reagent id: DevilsKiss @@ -916,6 +922,7 @@ metamorphicMaxFillLevels: 5 metamorphicFillBaseName: fill- metamorphicChangeColor: true + fizziness: 0.15 - type: reagent id: GargleBlaster @@ -962,6 +969,7 @@ - !type:AdjustReagent reagent: Ethanol amount: 0.07 + fizziness: 0.4 # A little high, but it has fizz in the name - type: reagent id: GinTonic @@ -985,6 +993,7 @@ - !type:AdjustReagent reagent: Ethanol amount: 0.07 + fizziness: 0.4 - type: reagent id: Gildlager @@ -1062,6 +1071,7 @@ metamorphicMaxFillLevels: 6 metamorphicFillBaseName: fill- metamorphicChangeColor: false + fizziness: 0.6 - type: reagent id: IrishCarBomb @@ -1199,6 +1209,7 @@ metamorphicMaxFillLevels: 2 metamorphicFillBaseName: fill- metamorphicChangeColor: false + fizziness: 0.7 - type: reagent id: Margarita @@ -1252,6 +1263,7 @@ metamorphicMaxFillLevels: 5 metamorphicFillBaseName: fill- metamorphicChangeColor: true + fizziness: 0.4 - type: reagent id: Mojito @@ -1267,6 +1279,7 @@ metamorphicMaxFillLevels: 6 metamorphicFillBaseName: fill- metamorphicChangeColor: false + fizziness: 0.3 - type: reagent id: Moonshine @@ -1371,6 +1384,7 @@ metamorphicMaxFillLevels: 5 metamorphicFillBaseName: fill- metamorphicChangeColor: true + fizziness: 0.4 - type: reagent id: PinaColada @@ -1500,6 +1514,7 @@ metamorphicMaxFillLevels: 6 metamorphicFillBaseName: fill- metamorphicChangeColor: true + fizziness: 0.3 - type: reagent id: SuiDream @@ -1515,6 +1530,7 @@ metamorphicMaxFillLevels: 5 metamorphicFillBaseName: fill- metamorphicChangeColor: false + fizziness: 0.2 - type: reagent id: SyndicateBomb @@ -1530,6 +1546,7 @@ metamorphicMaxFillLevels: 6 metamorphicFillBaseName: fill- metamorphicChangeColor: true + fizziness: 0.6 - type: reagent id: TequilaSunrise @@ -1568,6 +1585,7 @@ metamorphicMaxFillLevels: 3 metamorphicFillBaseName: fill- metamorphicChangeColor: false + fizziness: 0.2 - type: reagent id: ThreeMileIsland @@ -1655,6 +1673,7 @@ - !type:AdjustReagent reagent: Ethanol amount: 0.07 + fizziness: 0.4 - type: reagent id: WhiskeyCola @@ -1678,6 +1697,7 @@ - !type:AdjustReagent reagent: Ethanol amount: 0.07 + fizziness: 0.3 - type: reagent id: WhiskeySoda @@ -1701,6 +1721,7 @@ - !type:AdjustReagent reagent: Ethanol amount: 0.07 + fizziness: 0.4 - type: reagent id: WhiteGilgamesh @@ -1718,6 +1739,7 @@ - !type:AdjustReagent reagent: Ethanol amount: 0.15 + fizziness: 0.5 - type: reagent id: WhiteRussian diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml b/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml index 9984b4c0cf..19a5e1bf8f 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml @@ -40,6 +40,7 @@ collection: FootstepSticky params: volume: 6 + fizziness: 0.5 - type: reagent id: BaseAlcohol @@ -75,4 +76,4 @@ footstepSound: collection: FootstepSticky params: - volume: 6 \ No newline at end of file + volume: 6 diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml index 5c09b3c909..71de67adb9 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml @@ -322,6 +322,7 @@ damage: types: Poison: 1 + fizziness: 0.5 - type: reagent id: SodaWater @@ -331,6 +332,7 @@ physicalDesc: reagent-physical-desc-fizzy flavor: fizzy color: "#619494" + fizziness: 0.8 - type: reagent id: SoyLatte @@ -373,6 +375,7 @@ physicalDesc: reagent-physical-desc-fizzy flavor: tonicwater color: "#0064C8" + fizziness: 0.4 - type: reagent id: Water @@ -467,6 +470,7 @@ effects: - !type:SatiateThirst factor: 1 + fizziness: 0.3 - type: reagent id: Posca @@ -491,6 +495,7 @@ metamorphicMaxFillLevels: 3 metamorphicFillBaseName: fill- metamorphicChangeColor: false + fizziness: 0.3 - type: reagent id: Rewriter @@ -506,6 +511,7 @@ metamorphicMaxFillLevels: 5 metamorphicFillBaseName: fill- metamorphicChangeColor: true + fizziness: 0.3 - type: reagent id: Mopwata diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml b/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml index ba5adc4f2a..3dda5b5329 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml @@ -59,6 +59,7 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 + fizziness: 0.4 - type: reagent id: GrapeSoda @@ -84,6 +85,7 @@ metamorphicMaxFillLevels: 5 metamorphicFillBaseName: fill- metamorphicChangeColor: true + fizziness: 0 - type: reagent id: LemonLime @@ -102,6 +104,7 @@ physicalDesc: reagent-physical-desc-fizzy flavor: pwrgamesoda color: "#9385bf" + fizziness: 0.9 # gamers crave the fizz - type: reagent id: RootBeer @@ -132,6 +135,7 @@ metamorphicMaxFillLevels: 7 metamorphicFillBaseName: fill- metamorphicChangeColor: false + fizziness: 0.4 - type: reagent id: SolDry