* Moved abstract spill methods to shared; added prediction to spill container verb.
* Rerun tests
* Requested changes
* Note Client behavior in Spill method docs
using Content.Client.IconSmoothing;
+using Content.Shared.Chemistry.Components;
using Content.Shared.Fluids;
using Content.Shared.Fluids.Components;
using Robust.Client.GameObjects;
+using Robust.Shared.Map;
namespace Content.Client.Fluids;
if (args.Sprite == null)
return;
- float volume = 1f;
+ var volume = 1f;
if (args.AppearanceData.TryGetValue(PuddleVisuals.CurrentVolume, out var volumeObj))
{
args.Sprite.Color *= baseColor;
}
}
+
+ #region Spill
+
+ // Maybe someday we'll have clientside prediction for entity spawning, but not today.
+ // Until then, these methods do nothing on the client.
+ /// <inheritdoc/>
+ public override bool TrySplashSpillAt(EntityUid uid, EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true, EntityUid? user = null)
+ {
+ puddleUid = EntityUid.Invalid;
+ return false;
+ }
+
+ /// <inheritdoc/>
+ public override bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true)
+ {
+ puddleUid = EntityUid.Invalid;
+ return false;
+ }
+
+ /// <inheritdoc/>
+ public override bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true, TransformComponent? transformComponent = null)
+ {
+ puddleUid = EntityUid.Invalid;
+ return false;
+ }
+
+ /// <inheritdoc/>
+ public override bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true, bool tileReact = true)
+ {
+ puddleUid = EntityUid.Invalid;
+ return false;
+ }
+
+ #endregion Spill
}
+++ /dev/null
-namespace Content.Server.Fluids.Components;
-
-[RegisterComponent]
-public sealed partial class PreventSpillerComponent : Component
-{
-
-}
using Content.Server.Chemistry.Containers.EntitySystems;
-using Content.Server.Fluids.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Clothing.Components;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Database;
-using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Fluids.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Popups;
using Content.Shared.Spillable;
using Content.Shared.Throwing;
-using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Player;
public sealed partial class PuddleSystem
{
- [Dependency] private readonly OpenableSystem _openable = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
-
protected override void InitializeSpillable()
{
base.InitializeSpillable();
SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand);
// Openable handles the event if it's closed
SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit, after: [typeof(OpenableSystem)]);
- SubscribeLocalEvent<SpillableComponent, GetVerbsEvent<Verb>>(AddSpillVerb);
SubscribeLocalEvent<SpillableComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<SpillableComponent, SolutionContainerOverflowEvent>(OnOverflow);
SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter);
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
return;
- if (_openable.IsClosed(entity.Owner))
+ if (Openable.IsClosed(entity.Owner))
return;
if (args.User != null)
private void OnAttemptPacifiedThrow(Entity<SpillableComponent> ent, ref AttemptPacifiedThrowEvent args)
{
// Don’t care about closed containers.
- if (_openable.IsClosed(ent))
+ if (Openable.IsClosed(ent))
return;
// Don’t care about empty containers.
args.Cancel("pacified-cannot-throw-spill");
}
- private void AddSpillVerb(Entity<SpillableComponent> entity, ref GetVerbsEvent<Verb> args)
- {
- if (!args.CanAccess || !args.CanInteract)
- return;
-
- if (!_solutionContainerSystem.TryGetSolution(args.Target, entity.Comp.SolutionName, out var soln, out var solution))
- return;
-
- if (_openable.IsClosed(args.Target))
- return;
-
- if (solution.Volume == FixedPoint2.Zero)
- return;
-
- if (_entityManager.HasComponent<PreventSpillerComponent>(args.User))
- return;
-
-
- Verb verb = new()
- {
- Text = Loc.GetString("spill-target-verb-get-data-text")
- };
-
- // TODO VERB ICONS spill icon? pouring out a glass/beaker?
- if (entity.Comp.SpillDelay == null)
- {
- var target = args.Target;
- verb.Act = () =>
- {
- var puddleSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume);
- TrySpillAt(Transform(target).Coordinates, puddleSolution, out _);
- };
- }
- else
- {
- var user = args.User;
- verb.Act = () =>
- {
- _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, entity.Comp.SpillDelay ?? 0, new SpillDoAfterEvent(), entity.Owner, target: entity.Owner)
- {
- BreakOnDamage = true,
- BreakOnMove = true,
- NeedHand = true,
- });
- };
- }
- verb.Impact = LogImpact.Medium; // dangerous reagent reaction are logged separately.
- verb.DoContactInteraction = true;
- args.Verbs.Add(verb);
- }
-
private void OnDoAfter(Entity<SpillableComponent> entity, ref SpillDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Target == null)
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
[Dependency] private readonly AudioSystem _audio = default!;
- [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly ReactiveSystem _reactive = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
#region Spill
- /// <summary>
- /// First splashes reagent on reactive entities near the spilling entity, then spills the rest regularly to a
- /// puddle. This is intended for 'destructive' spills, like when entities are destroyed or thrown.
- /// </summary>
- public bool TrySplashSpillAt(EntityUid uid,
+ /// <inheritdoc/>
+ public override bool TrySplashSpillAt(EntityUid uid,
EntityCoordinates coordinates,
Solution solution,
out EntityUid puddleUid,
return TrySpillAt(coordinates, solution, out puddleUid, sound);
}
- /// <summary>
- /// Spills solution at the specified coordinates.
- /// Will add to an existing puddle if present or create a new one if not.
- /// </summary>
- public bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true)
+ /// <inheritdoc/>
+ public override bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true)
{
if (solution.Volume == 0)
{
return TrySpillAt(_map.GetTileRef(gridUid.Value, mapGrid, coordinates), solution, out puddleUid, sound);
}
- /// <summary>
- /// <see cref="TrySpillAt(Robust.Shared.Map.EntityCoordinates,Content.Shared.Chemistry.Components.Solution,out Robust.Shared.GameObjects.EntityUid,bool)"/>
- /// </summary>
- public bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true,
+ /// <inheritdoc/>
+ public override bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true,
TransformComponent? transformComponent = null)
{
if (!Resolve(uid, ref transformComponent, false))
return TrySpillAt(transformComponent.Coordinates, solution, out puddleUid, sound: sound);
}
- /// <summary>
- /// <see cref="TrySpillAt(Robust.Shared.Map.EntityCoordinates,Content.Shared.Chemistry.Components.Solution,out Robust.Shared.GameObjects.EntityUid,bool)"/>
- /// </summary>
- public bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true,
+ /// <inheritdoc/>
+ public override bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true,
bool tileReact = true)
{
if (solution.Volume <= 0)
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Fluids.Components;
+
+/// <summary>
+/// Blocks this entity's ability to spill solution containing entities via the verb menu.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class PreventSpillerComponent : Component
+{
+
+}
namespace Content.Shared.Fluids.Components;
+/// <summary>
+/// Makes a solution contained in this entity spillable.
+/// Spills can occur when a container with this component overflows,
+/// is used to melee attack something, is equipped (see <see cref="SpillWorn"/>),
+/// lands after being thrown, or has the Spill verb used.
+/// </summary>
[RegisterComponent]
public sealed partial class SpillableComponent : Component
{
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
using Content.Shared.Examine;
+using Content.Shared.FixedPoint;
using Content.Shared.Fluids.Components;
+using Content.Shared.Nutrition.EntitySystems;
+using Content.Shared.Spillable;
+using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee;
namespace Content.Shared.Fluids;
public abstract partial class SharedPuddleSystem
{
+ [Dependency] protected readonly SharedOpenableSystem Openable = default!;
+
protected virtual void InitializeSpillable()
{
SubscribeLocalEvent<SpillableComponent, ExaminedEvent>(OnExamined);
+ SubscribeLocalEvent<SpillableComponent, GetVerbsEvent<Verb>>(AddSpillVerb);
}
private void OnExamined(Entity<SpillableComponent> entity, ref ExaminedEvent args)
args.PushMarkup(Loc.GetString("spill-examine-spillable-weapon"));
}
}
+
+ private void AddSpillVerb(Entity<SpillableComponent> entity, ref GetVerbsEvent<Verb> args)
+ {
+ if (!args.CanAccess || !args.CanInteract)
+ return;
+
+ if (!_solutionContainerSystem.TryGetSolution(args.Target, entity.Comp.SolutionName, out var soln, out var solution))
+ return;
+
+ if (Openable.IsClosed(args.Target))
+ return;
+
+ if (solution.Volume == FixedPoint2.Zero)
+ return;
+
+ if (HasComp<PreventSpillerComponent>(args.User))
+ return;
+
+
+ Verb verb = new()
+ {
+ Text = Loc.GetString("spill-target-verb-get-data-text")
+ };
+
+ // TODO VERB ICONS spill icon? pouring out a glass/beaker?
+ if (entity.Comp.SpillDelay == null)
+ {
+ var target = args.Target;
+ verb.Act = () =>
+ {
+ var puddleSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume);
+ TrySpillAt(Transform(target).Coordinates, puddleSolution, out _);
+ };
+ }
+ else
+ {
+ var user = args.User;
+ verb.Act = () =>
+ {
+ _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, entity.Comp.SpillDelay ?? 0, new SpillDoAfterEvent(), entity.Owner, target: entity.Owner)
+ {
+ BreakOnDamage = true,
+ BreakOnMove = true,
+ NeedHand = true,
+ });
+ };
+ }
+ verb.Impact = LogImpact.Medium; // dangerous reagent reaction are logged separately.
+ verb.DoContactInteraction = true;
+ args.Verbs.Add(verb);
+ }
}
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
+using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Fluids.Components;
using Content.Shared.Movement.Events;
using Content.Shared.StepTrigger.Components;
+using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Shared.Fluids;
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
/// <summary>
/// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle.
args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-no"));
}
}
+
+ #region Spill
+ // These methods are in Shared to make it easier to interact with PuddleSystem in Shared code.
+ // Note that they always fail when run on the client, not creating a puddle and returning false.
+ // Adding proper prediction to this system would require spawning temporary puddle entities on the
+ // client and replacing or merging them with the ones spawned by the server when the client goes to
+ // replicate those, and I am not enough of a wizard to attempt implementing that.
+
+ /// <summary>
+ /// First splashes reagent on reactive entities near the spilling entity, then spills the rest regularly to a
+ /// puddle. This is intended for 'destructive' spills, like when entities are destroyed or thrown.
+ /// </summary>
+ /// <remarks>
+ /// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
+ /// </remarks>
+ public abstract bool TrySplashSpillAt(EntityUid uid,
+ EntityCoordinates coordinates,
+ Solution solution,
+ out EntityUid puddleUid,
+ bool sound = true,
+ EntityUid? user = null);
+
+ /// <summary>
+ /// Spills solution at the specified coordinates.
+ /// Will add to an existing puddle if present or create a new one if not.
+ /// </summary>
+ /// <remarks>
+ /// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
+ /// </remarks>
+ public abstract bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true);
+
+ /// <summary>
+ /// <see cref="TrySpillAt(EntityCoordinates, Solution, out EntityUid, bool)"/>
+ /// </summary>
+ /// <remarks>
+ /// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
+ /// </remarks>
+ public abstract bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true,
+ TransformComponent? transformComponent = null);
+
+ /// <summary>
+ /// <see cref="TrySpillAt(EntityCoordinates, Solution, out EntityUid, bool)"/>
+ /// </summary>
+ /// <remarks>
+ /// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
+ /// </remarks>
+ public abstract bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true,
+ bool tileReact = true);
+
+ #endregion Spill
}