Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to shake fizzy drinks so they spray in peoples' faces #25574

Merged
merged 23 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1cd2f65
Implemented Shakeable
Tayrtahn Feb 23, 2024
6ba649a
Prevent shaking open Openables
Tayrtahn Feb 23, 2024
4ee8a8e
Prevent shaking empty drinks. Moved part of DrinkSystem to Shared.
Tayrtahn Feb 23, 2024
ee78124
DrinkSystem can have a little more prediction, as a treat
Tayrtahn Feb 23, 2024
6375f53
Cleanup
Tayrtahn Feb 23, 2024
7e65864
Overhauled PressurizedDrink
Tayrtahn Feb 24, 2024
33e3291
Make soda cans/bottles and champagne shakeable. The drink shaker too,…
Tayrtahn Feb 24, 2024
22a9a04
We do a little refactoring.
Tayrtahn Feb 25, 2024
37acd88
Documentation, cleanup, and tweaks.
Tayrtahn Feb 25, 2024
51efba6
Changed fizziness calculation to use a cubic-out easing curve.
Tayrtahn Feb 25, 2024
1367142
Removed broken YAML that has avoid the linter's wrath for far too long
Tayrtahn Feb 26, 2024
67d600c
Changed reagent fizzy bool to fizziness float.
Tayrtahn Feb 28, 2024
a5472c5
Rename file to match changed class name
Tayrtahn Mar 4, 2024
a2cc628
DoAfter improvements. Cancel if the user moves away; block if no hands.
Tayrtahn Mar 7, 2024
4c28765
Match these filenames too
Tayrtahn Mar 15, 2024
830b337
And this one
Tayrtahn Mar 15, 2024
bca416c
guh
Tayrtahn Mar 15, 2024
aa10cd6
Merge branch 'master' into soda-shaking
Tayrtahn Apr 1, 2024
9a2b11b
Updated to use Shared puddle methods
Tayrtahn Apr 1, 2024
531ca0e
Merge branch 'master' into soda-shaking
Tayrtahn Apr 17, 2024
f1bb976
Various fixes and improvements.
Tayrtahn Apr 17, 2024
f734bb0
Made AttemptShakeEvent a struct
Tayrtahn Apr 18, 2024
52597a3
AttemptAddFizzinessEvent too
Tayrtahn Apr 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Content.Client/Nutrition/EntitySystems/DrinkSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Content.Shared.Nutrition.EntitySystems;

namespace Content.Client.Nutrition.EntitySystems;

public sealed class DrinkSystem : SharedDrinkSystem
{
}
27 changes: 0 additions & 27 deletions Content.Server/Nutrition/Components/PressurizedDrinkComponent.cs

This file was deleted.

95 changes: 2 additions & 93 deletions Content.Server/Nutrition/EntitySystems/DrinkSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
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;
Expand All @@ -16,7 +15,6 @@
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;
Expand All @@ -25,24 +23,21 @@
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!;
Expand All @@ -66,33 +61,10 @@ public override void Initialize()
SubscribeLocalEvent<DrinkComponent, ComponentInit>(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<DrinkComponent, UseInHandEvent>(OnUse, before: new[] { typeof(ServerInventorySystem) }, after: new[] { typeof(OpenableSystem) });
SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUse, before: [typeof(ServerInventorySystem)], after: [typeof(OpenableSystem)]);
SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<DrinkComponent, GetVerbsEvent<AlternativeVerb>>(AddDrinkVerb);
// put drink amount after opened
SubscribeLocalEvent<DrinkComponent, ExaminedEvent>(OnExamined, after: new[] { typeof(OpenableSystem) });
SubscribeLocalEvent<DrinkComponent, ConsumeDoAfterEvent>(OnDoAfter);

SubscribeLocalEvent<PressurizedDrinkComponent, LandEvent>(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;
}

/// <summary>
Expand Down Expand Up @@ -129,38 +101,6 @@ public float TotalHydration(EntityUid uid, DrinkComponent? comp = null)
return total;
}

private void OnExamined(Entity<DrinkComponent> entity, ref ExaminedEvent args)
{
TryComp<OpenableComponent>(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<ExaminableSolutionComponent>(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<DrinkComponent> entity, ref AfterInteractEvent args)
{
if (args.Handled || args.Target == null || !args.CanReach)
Expand All @@ -177,25 +117,6 @@ private void OnUse(Entity<DrinkComponent> entity, ref UseInHandEvent args)
args.Handled = TryDrink(args.User, args.User, entity.Comp, entity);
}

private void OnPressurizedDrinkLand(Entity<PressurizedDrinkComponent> entity, ref LandEvent args)
{
if (!TryComp<DrinkComponent>(entity, out var drink) || !TryComp<OpenableComponent>(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<DrinkComponent> entity, ref ComponentInit args)
{
if (TryComp<DrainableSolutionComponent>(entity, out var existingDrainable))
Expand Down Expand Up @@ -433,16 +354,4 @@ private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<Alter

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<MetaDataComponent>(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;
}
}
7 changes: 7 additions & 0 deletions Content.Shared/Chemistry/Reagent/ReagentPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ public sealed partial class ReagentPrototype : IPrototype, IInheritingPrototype
[DataField]
public bool Slippery;

/// <summary>
/// How easily this reagent becomes fizzy when aggitated.
/// 0 - completely flat, 1 - fizzes up when nudged.
/// </summary>
[DataField]
public float Fizziness;

/// <summary>
/// How much reagent slows entities down if it's part of a puddle.
/// 0 - no slowdown; 1 - can't move.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,43 @@
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);

/// <summary>
/// How long it takes to drink this yourself.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public float Delay = 1;

[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public bool Examinable = true;

/// <summary>
/// If true, trying to drink when empty will not handle the event.
/// This means other systems such as equipping on use can run.
/// Example usecase is the bucket.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool IgnoreEmpty;

/// <summary>
/// This is how many seconds it takes to force feed someone this drink.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public float ForceFeedDelay = 3;
}
106 changes: 106 additions & 0 deletions Content.Shared/Nutrition/Components/PressurizedSolutionComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;

namespace Content.Shared.Nutrition.Components;

/// <summary>
/// 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.
/// </summary>
[NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[RegisterComponent, Access(typeof(PressurizedSolutionSystem))]
public sealed partial class PressurizedSolutionComponent : Component
{
/// <summary>
/// The name of the solution to use.
/// </summary>
[DataField]
public string Solution = "drink";

/// <summary>
/// The sound to play when the solution sprays out of the container.
/// </summary>
[DataField]
public SoundSpecifier SpraySound = new SoundPathSpecifier("/Audio/Items/soda_spray.ogg");

/// <summary>
/// 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.
/// </summary>
[DataField]
public TimeSpan FizzinessMaxDuration = TimeSpan.FromSeconds(120);

/// <summary>
/// The time at which the solution will be fully settled after being shaken.
/// </summary>
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan FizzySettleTime;

/// <summary>
/// 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.
/// </summary>
[DataField]
public float FizzinessAddedOnShake = 1.0f;

/// <summary>
/// How much to increase the solution's fizziness when it lands after being thrown.
/// This assumes the solution has maximum fizzability.
/// </summary>
[DataField]
public float FizzinessAddedOnLand = 0.25f;

/// <summary>
/// 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.
/// </summary>
[DataField]
public float SprayChanceModOnOpened = -0.01f; // Just enough to prevent spraying at 0 fizziness

/// <summary>
/// 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.
/// </summary>
[DataField]
public float SprayChanceModOnShake = -1; // No spraying when shaken by default

/// <summary>
/// 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.
/// </summary>
[DataField]
public float SprayChanceModOnLand = 0.25f;

/// <summary>
/// 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!
/// </summary>
[DataField, AutoNetworkedField]
public float SprayFizzinessThresholdRoll;

/// <summary>
/// Popup message shown to user when sprayed by the solution.
/// </summary>
[DataField]
public LocId SprayHolderMessageSelf = "pressurized-solution-spray-holder-self";

/// <summary>
/// Popup message shown to others when a user is sprayed by the solution.
/// </summary>
[DataField]
public LocId SprayHolderMessageOthers = "pressurized-solution-spray-holder-others";

/// <summary>
/// Popup message shown above the entity when the solution sprays without a target.
/// </summary>
[DataField]
public LocId SprayGroundMessage = "pressurized-solution-spray-ground";
}
Loading
Loading