]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add water flower for clowns (#41469)
authorbeck-thompson <107373427+beck-thompson@users.noreply.github.com>
Mon, 1 Dec 2025 01:31:12 +0000 (17:31 -0800)
committerGitHub <noreply@github.com>
Mon, 1 Dec 2025 01:31:12 +0000 (01:31 +0000)
* Spray!

* Add to clown loadout

* Fix the easy things

* lot nicer

* spray update..

* Fix yaml

* fixes

* changed it to warning!

* review

* review

* sku

17 files changed:
Content.Client/Fluids/SpraySystem.cs [new file with mode: 0644]
Content.Server/Fluids/EntitySystems/SpraySystem.cs
Content.Shared/Fluids/Components/EquipSprayComponent.cs [new file with mode: 0644]
Content.Shared/Fluids/Components/SprayComponent.cs [moved from Content.Server/Fluids/Components/SprayComponent.cs with 76% similarity]
Content.Shared/Fluids/EntitySystems/SharedSpraySystem.cs [new file with mode: 0644]
Content.Shared/Fluids/Events.cs
Content.Shared/Fluids/SpraySafetySystem.cs
Resources/Locale/en-US/fluids/components/equip-spray-component.ftl [new file with mode: 0644]
Resources/Locale/en-US/fluids/components/spray-component.ftl
Resources/Prototypes/Actions/types.yml
Resources/Prototypes/Entities/Clothing/Neck/pins.yml
Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml
Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml
Resources/Prototypes/Loadouts/loadout_groups.yml
Resources/Textures/Clothing/Neck/Misc/pins.rsi/flower-equipped-NECK.png [new file with mode: 0644]
Resources/Textures/Clothing/Neck/Misc/pins.rsi/flower.png [new file with mode: 0644]
Resources/Textures/Clothing/Neck/Misc/pins.rsi/meta.json

diff --git a/Content.Client/Fluids/SpraySystem.cs b/Content.Client/Fluids/SpraySystem.cs
new file mode 100644 (file)
index 0000000..877a2a0
--- /dev/null
@@ -0,0 +1,7 @@
+using Content.Shared.Fluids.Components;
+using Content.Shared.Fluids.EntitySystems;
+using Robust.Shared.Map;
+
+namespace Content.Client.Fluids;
+
+public sealed class SpraySystem : SharedSpraySystem;
index 2a6b0644be32ee680e9da43c877d11d707ccbf9b..4708954ea174f9ca3d902b91e83d8749326ad755 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Server.Chemistry.Components;
 using Content.Server.Chemistry.EntitySystems;
-using Content.Server.Fluids.Components;
 using Content.Server.Gravity;
 using Content.Server.Popups;
 using Content.Shared.CCVar;
@@ -16,11 +15,14 @@ using Robust.Shared.Configuration;
 using Robust.Shared.Physics.Components;
 using Robust.Shared.Prototypes;
 using System.Numerics;
+using Content.Shared.Fluids.EntitySystems;
+using Content.Shared.Fluids.Components;
+using Robust.Server.Containers;
 using Robust.Shared.Map;
 
 namespace Content.Server.Fluids.EntitySystems;
 
-public sealed class SpraySystem : EntitySystem
+public sealed class SpraySystem : SharedSpraySystem
 {
     [Dependency] private readonly IPrototypeManager _proto = default!;
     [Dependency] private readonly GravitySystem _gravity = default!;
@@ -33,6 +35,7 @@ public sealed class SpraySystem : EntitySystem
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly IConfigurationManager _cfg = default!;
+    [Dependency] private readonly ContainerSystem _container = default!;
 
     private float _gridImpulseMultiplier;
 
@@ -54,7 +57,7 @@ public sealed class SpraySystem : EntitySystem
 
         var targetMapPos = _transform.GetMapCoordinates(GetEntityQuery<TransformComponent>().GetComponent(args.Target));
 
-        Spray(entity, args.User, targetMapPos);
+        Spray(entity, targetMapPos, args.User);
     }
 
     private void UpdateGridMassMultiplier(float value)
@@ -71,10 +74,19 @@ public sealed class SpraySystem : EntitySystem
 
         var clickPos = _transform.ToMapCoordinates(args.ClickLocation);
 
-        Spray(entity, args.User, clickPos);
+        Spray(entity, clickPos, args.User);
     }
 
-    public void Spray(Entity<SprayComponent> entity, EntityUid user, MapCoordinates mapcoord)
+    public override void Spray(Entity<SprayComponent> entity, EntityUid? user = null)
+    {
+        var xform = Transform(entity);
+        var throwing = xform.LocalRotation.ToWorldVec() * entity.Comp.SprayDistance;
+        var direction = xform.Coordinates.Offset(throwing);
+
+        Spray(entity, _transform.ToMapCoordinates(direction), user);
+    }
+
+    public override void Spray(Entity<SprayComponent> entity, MapCoordinates mapcoord, EntityUid? user = null)
     {
         if (!_solutionContainer.TryGetSolution(entity.Owner, SprayComponent.SolutionName, out var soln, out var solution))
             return;
@@ -82,25 +94,29 @@ public sealed class SpraySystem : EntitySystem
         var ev = new SprayAttemptEvent(user);
         RaiseLocalEvent(entity, ref ev);
         if (ev.Cancelled)
+        {
+            if (ev.CancelPopupMessage != null && user != null)
+                _popupSystem.PopupEntity(Loc.GetString(ev.CancelPopupMessage), entity.Owner, user.Value);
             return;
+        }
 
-        if (TryComp<UseDelayComponent>(entity, out var useDelay)
-            && _useDelay.IsDelayed((entity, useDelay)))
+        if (_useDelay.IsDelayed((entity, null)))
             return;
 
         if (solution.Volume <= 0)
         {
-            _popupSystem.PopupEntity(Loc.GetString("spray-component-is-empty-message"), entity.Owner, user);
+            if (user != null)
+                _popupSystem.PopupEntity(Loc.GetString(entity.Comp.SprayEmptyPopupMessage, ("entity", entity)), entity.Owner, user.Value);
             return;
         }
 
         var xformQuery = GetEntityQuery<TransformComponent>();
-        var userXform = xformQuery.GetComponent(user);
+        var sprayerXform = xformQuery.GetComponent(entity);
 
-        var userMapPos = _transform.GetMapCoordinates(userXform);
+        var sprayerMapPos = _transform.GetMapCoordinates(sprayerXform);
         var clickMapPos = mapcoord;
 
-        var diffPos = clickMapPos.Position - userMapPos.Position;
+        var diffPos = clickMapPos.Position - sprayerMapPos.Position;
         if (diffPos == Vector2.Zero || diffPos == Vector2Helpers.NaN)
             return;
 
@@ -127,12 +143,12 @@ public sealed class SpraySystem : EntitySystem
                                      Angle.FromDegrees(spread * (amount - 1) / 2));
 
             // Calculate the destination for the vapor cloud. Limit to the maximum spray distance.
-            var target = userMapPos
+            var target = sprayerMapPos
                 .Offset((diffNorm + rotation.ToVec()).Normalized() * diffLength + quarter);
 
-            var distance = (target.Position - userMapPos.Position).Length();
+            var distance = (target.Position - sprayerMapPos.Position).Length();
             if (distance > entity.Comp.SprayDistance)
-                target = userMapPos.Offset(diffNorm * entity.Comp.SprayDistance);
+                target = sprayerMapPos.Offset(diffNorm * entity.Comp.SprayDistance);
 
             var adjustedSolutionAmount = entity.Comp.TransferAmount / entity.Comp.VaporAmount;
             var newSolution = _solutionContainer.SplitSolution(soln.Value, adjustedSolutionAmount);
@@ -141,7 +157,7 @@ public sealed class SpraySystem : EntitySystem
                 break;
 
             // Spawn the vapor cloud onto the grid/map the user is present on. Offset the start position based on how far the target destination is.
-            var vaporPos = userMapPos.Offset(distance < 1 ? quarter : threeQuarters);
+            var vaporPos = sprayerMapPos.Offset(distance < 1 ? quarter : threeQuarters);
             var vapor = Spawn(entity.Comp.SprayedPrototype, vaporPos);
             var vaporXform = xformQuery.GetComponent(vapor);
 
@@ -164,17 +180,21 @@ public sealed class SpraySystem : EntitySystem
 
             _vapor.Start(ent, vaporXform, impulseDirection * diffLength, entity.Comp.SprayVelocity, target, time, user);
 
-            if (TryComp<PhysicsComponent>(user, out var body))
+            var thingGettingPushed = entity.Owner;
+            if (_container.TryGetOuterContainer(entity, sprayerXform, out var container))
+                thingGettingPushed = container.Owner;
+
+            if (TryComp<PhysicsComponent>(thingGettingPushed, out var body))
             {
-                if (_gravity.IsWeightless(user))
+                if (_gravity.IsWeightless(thingGettingPushed))
                 {
                     // push back the player
-                    _physics.ApplyLinearImpulse(user, -impulseDirection * entity.Comp.PushbackAmount, body: body);
+                    _physics.ApplyLinearImpulse(thingGettingPushed, -impulseDirection * entity.Comp.PushbackAmount, body: body);
                 }
                 else
                 {
                     // push back the grid the player is standing on
-                    var userTransform = Transform(user);
+                    var userTransform = Transform(thingGettingPushed);
                     if (userTransform.GridUid == userTransform.ParentUid)
                     {
                         // apply both linear and angular momentum depending on the player position
@@ -187,7 +207,6 @@ public sealed class SpraySystem : EntitySystem
 
         _audio.PlayPvs(entity.Comp.SpraySound, entity, entity.Comp.SpraySound.Params.WithVariation(0.125f));
 
-        if (useDelay != null)
-            _useDelay.TryResetDelay((entity, useDelay));
+        _useDelay.TryResetDelay(entity);
     }
 }
diff --git a/Content.Shared/Fluids/Components/EquipSprayComponent.cs b/Content.Shared/Fluids/Components/EquipSprayComponent.cs
new file mode 100644 (file)
index 0000000..fe6cf97
--- /dev/null
@@ -0,0 +1,16 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Fluids.Components;
+
+/// <summary>
+/// Allows items with the spray component to be equipped and sprayable with a unique action.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class EquipSprayComponent : Component
+{
+    /// <summary>
+    /// Verb locid that will come up when interacting with the sprayer. Set to null for no verb!
+    /// </summary>
+    [DataField]
+    public LocId? VerbLocId;
+}
similarity index 76%
rename from Content.Server/Fluids/Components/SprayComponent.cs
rename to Content.Shared/Fluids/Components/SprayComponent.cs
index 128fdecfa7f50dbda902bb9ba0a01de5ecd80074..cc0032c3fb4ac5c942b93b363a73447f0956e016 100644 (file)
@@ -1,12 +1,12 @@
-using Content.Server.Fluids.EntitySystems;
 using Content.Shared.FixedPoint;
+using Content.Shared.Fluids.EntitySystems;
 using Robust.Shared.Audio;
 using Robust.Shared.Prototypes;
 
-namespace Content.Server.Fluids.Components;
+namespace Content.Shared.Fluids.Components;
 
 [RegisterComponent]
-[Access(typeof(SpraySystem))]
+[Access(typeof(SharedSpraySystem))]
 public sealed partial class SprayComponent : Component
 {
     public const string SolutionName = "spray";
@@ -36,6 +36,9 @@ public sealed partial class SprayComponent : Component
     public float PushbackAmount = 5f;
 
     [DataField(required: true)]
-    [Access(typeof(SpraySystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
+    [Access(typeof(SharedSpraySystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
     public SoundSpecifier SpraySound { get; private set; } = default!;
+
+    [DataField]
+    public LocId SprayEmptyPopupMessage = "spray-component-is-empty-message";
 }
diff --git a/Content.Shared/Fluids/EntitySystems/SharedSpraySystem.cs b/Content.Shared/Fluids/EntitySystems/SharedSpraySystem.cs
new file mode 100644 (file)
index 0000000..42883f3
--- /dev/null
@@ -0,0 +1,80 @@
+using Content.Shared.Actions;
+using Content.Shared.Fluids.Components;
+using Content.Shared.Verbs;
+using Robust.Shared.Map;
+
+namespace Content.Shared.Fluids.EntitySystems;
+
+public abstract class SharedSpraySystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<EquipSprayComponent, GetVerbsEvent<EquipmentVerb>>(OnGetVerb);
+        SubscribeLocalEvent<SprayLiquidEvent>(SprayLiquid);
+    }
+
+    private void SprayLiquid(SprayLiquidEvent ev)
+    {
+        var equipSprayEnt = ev.Action.Comp.Container;
+
+        if (equipSprayEnt == null)
+        {
+            Log.Warning($"{ev.Action.Comp.AttachedEntity} tried to use the SprayLiquidEvent but the entity was null.");
+            return;
+        }
+
+        if (!TryComp<SprayComponent>(equipSprayEnt, out var sprayComponent))
+        {
+            Log.Warning($"{ev.Action.Comp.AttachedEntity} tried to use the SprayLiquidEvent on {equipSprayEnt} but the SprayComponent did not exist.");
+            return;
+        }
+
+        Spray((equipSprayEnt.Value, sprayComponent), ev.Performer);
+    }
+
+    private void OnGetVerb(Entity<EquipSprayComponent> entity, ref GetVerbsEvent<EquipmentVerb> args)
+    {
+        if (entity.Comp.VerbLocId == null || !args.CanAccess || !args.CanInteract)
+            return;
+
+        var sprayComponent = Comp<SprayComponent>(entity);
+        var user = args.User;
+
+        var verb = new EquipmentVerb
+        {
+            Act = () =>
+            {
+                Spray((entity, sprayComponent), user);
+            },
+            Text = Loc.GetString(entity.Comp.VerbLocId),
+        };
+        args.Verbs.Add(verb);
+    }
+
+    /// <summary>
+    /// Spray starting from the entity, to the given coordinates. If the user is supplied, will give them failure
+    /// popups and will also push them in space.
+    /// </summary>
+    /// <param name="entity">Entity that is spraying.</param>
+    /// <param name="mapcoord">The coordinates being aimed at.</param>
+    /// <param name="user">The user that is using the spraying device.</param>
+    public virtual void Spray(Entity<SprayComponent> entity, MapCoordinates mapcoord, EntityUid? user = null)
+    {
+        // do nothing!
+    }
+
+    /// <summary>
+    /// Spray starting from the entity and facing the direction its pointing.
+    /// </summary>
+    /// <param name="entity">Entity that is spraying.</param>
+    /// <param name="user">User that is using the spraying device.</param>
+    public virtual void Spray(Entity<SprayComponent> entity, EntityUid? user = null)
+    {
+        // do nothing!
+    }
+}
+
+public sealed partial class SprayLiquidEvent : InstantActionEvent;
+
index 198e888774288efd5037e6c195d85e72a81e0fa4..e9f2bb8594a5a13a8512dba5646fe9fccd5a7ff2 100644 (file)
@@ -39,7 +39,7 @@ public sealed partial class AbsorbantDoAfterEvent : DoAfterEvent
 /// Raised when trying to spray something, for example a fire extinguisher.
 /// </summary>
 [ByRefEvent]
-public record struct SprayAttemptEvent(EntityUid User, bool Cancelled = false)
+public record struct SprayAttemptEvent(EntityUid? User, bool Cancelled = false, string? CancelPopupMessage = null)
 {
     public void Cancel()
     {
index 82006a995b96eb57ad73f27d46081c7b57664ca1..c206bbda083b767efc0e3554b78af35bef612447 100644 (file)
@@ -35,10 +35,10 @@ public sealed class SpraySafetySystem : EntitySystem
 
     private void OnSprayAttempt(Entity<SpraySafetyComponent> ent, ref SprayAttemptEvent args)
     {
-        if (!_toggle.IsActivated(ent.Owner))
-        {
-            _popup.PopupEntity(Loc.GetString(ent.Comp.Popup), ent, args.User);
-            args.Cancel();
-        }
+        if (_toggle.IsActivated(ent.Owner) || args.Cancelled)
+            return;
+
+        args.Cancel();
+        args.CancelPopupMessage = Loc.GetString(ent.Comp.Popup);
     }
 }
diff --git a/Resources/Locale/en-US/fluids/components/equip-spray-component.ftl b/Resources/Locale/en-US/fluids/components/equip-spray-component.ftl
new file mode 100644 (file)
index 0000000..f2ab831
--- /dev/null
@@ -0,0 +1 @@
+equip-spray-verb-press = Press
index e7060f2287478761a7769bbfd551573aa175247a..a7cd308edf548ab2fa464a9c88f20fda081db2f3 100644 (file)
@@ -1 +1,3 @@
-spray-component-is-empty-message = It's empty!
+spray-component-is-empty-message = {CAPITALIZE(THE($entity))} is empty!
+
+pin-spray-popup-empty = {CAPITALIZE(THE($entity))} is wilting and needs to be watered!
index 752aeb13f8887a34cce1bb95a5d8746da2d94699..d5ad1f3b55e4de3b664f9e0e845b655a6fa45b6a 100644 (file)
   - type: InstantAction
     event: !type:VoiceMaskSetNameEvent
 
+- type: entity
+  parent: BaseAction
+  id: ActionShootWater
+  name: Spray water!
+  description: Spray water towards your enemies.
+  components:
+  - type: Action
+    icon: { sprite: Clothing/Neck/Misc/pins.rsi, state: flower }
+  - type: InstantAction
+    event: !type:SprayLiquidEvent
+
 - type: entity
   parent: BaseAction
   id: ActionVendingThrow
index f540596afa5019b29df93dec1c383c4daa69f15b..af23e971165374f5acf58623479c0556badccfa0 100644 (file)
     state: goldautism
   - type: Clothing
     equippedPrefix: goldautism
+
+- type: entity
+  parent: BaseItem
+  id: SprayFlowerPin
+  name: flower pin
+  description: A cute flower pin. Something seems off with it...
+  components:
+  - type: Item
+    size: Tiny
+  - type: Sprite
+    sprite: Clothing/Neck/Misc/pins.rsi
+    state: flower
+  - type: Clothing
+    equippedPrefix: flower
+    sprite: Clothing/Neck/Misc/pins.rsi
+    quickEquip: true
+    slots:
+    - neck
+  - type: EquipSpray
+    verbLocId: equip-spray-verb-press
+  - type: SolutionContainerManager
+    solutions:
+      spray:
+        maxVol: 30
+        reagents:
+        - ReagentId: Water
+          Quantity: 30
+  - type: RefillableSolution
+    solution: spray
+  - type: DrainableSolution
+    solution: spray
+  - type: SolutionTransfer
+    maxTransferAmount: 30
+    transferAmount: 30
+  - type: UseDelay
+  - type: Spray
+    transferAmount: 5
+    pushbackAmount: 30
+    spraySound:
+      path: /Audio/Effects/spray3.ogg
+    sprayedPrototype: FlowerVapor
+    vaporAmount: 1
+    vaporSpread: 90
+    sprayVelocity: 1.0
+    sprayEmptyPopupMessage: pin-spray-popup-empty
+  - type: ActionGrant
+    actions:
+    - ActionShootWater
+  - type: ItemActionGrant
+    actions:
+    - ActionShootWater
+    activeIfWorn: true
index f335244806569a141bb5001ba5d0521602a58cd4..a8fb3b55a687e510b95283167abd9c2ddd24dd4a 100644 (file)
         mask:
         - FullTileMask
         - Opaque
+
+- type: entity
+  parent: Vapor
+  id: FlowerVapor
+  categories: [ HideSpawnMenu ]
+  components:
+  - type: Sprite
+    sprite: Effects/extinguisherSpray.rsi
+    layers:
+    - state: extinguish
+      map: [ "enum.VaporVisualLayers.Base" ]
+  - type: VaporVisuals
+    animationTime: 0.8
+    animationState: extinguish
index b5e3f6bdd7610357c130c5b3ae361b3d825d95bb..0cfc932c7751ed0587c320d0e11a1b0dc0e21d7d 100644 (file)
     back:
     - PlushieLizardJobClown
 
+- type: loadout
+  id: FlowerWaterClown
+  effects:
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:RoleTimeRequirement
+      role: JobClown
+      time: 4h
+  storage:
+    back:
+    - SprayFlowerPin
+
 - type: loadout
   id: LizardPlushieMime
   effects:
index c79689d5a0de5c2cf0279e148f619188e969c21d..b1b1a3a2944ac26ef94d84f198413267fef3ad4e 100644 (file)
   minLimit: 0
   loadouts:
   - LizardPlushieClown
+  - FlowerWaterClown
 
 - type: loadoutGroup
   id: MimeHead
diff --git a/Resources/Textures/Clothing/Neck/Misc/pins.rsi/flower-equipped-NECK.png b/Resources/Textures/Clothing/Neck/Misc/pins.rsi/flower-equipped-NECK.png
new file mode 100644 (file)
index 0000000..5d97ebd
Binary files /dev/null and b/Resources/Textures/Clothing/Neck/Misc/pins.rsi/flower-equipped-NECK.png differ
diff --git a/Resources/Textures/Clothing/Neck/Misc/pins.rsi/flower.png b/Resources/Textures/Clothing/Neck/Misc/pins.rsi/flower.png
new file mode 100644 (file)
index 0000000..6b9ccfc
Binary files /dev/null and b/Resources/Textures/Clothing/Neck/Misc/pins.rsi/flower.png differ
index ac7d927d23117895055405ef2e74958aee23def1..6f93169098d36ab0a6d1a1c346ccdba102231f35 100644 (file)
@@ -1,7 +1,7 @@
 {
   "version": 1,
   "license": "CC-BY-SA-3.0",
-  "copyright": "Aromantic, asexual, bisexual, intersex, lesbian, lgbt, non-binary, pansexual and transgender pins by PixelTK, gay pin by BackeTako, autism pins by Terraspark, omnisexual pin by juliangiebel, genderqueer and genderfluid by centcomofficer24, ally by FairlySadPanda, aroace by momochitters, plural by CubixThree",
+  "copyright": "Aromantic, asexual, bisexual, intersex, lesbian, lgbt, non-binary, pansexual and transgender pins by PixelTK, gay pin by BackeTako, autism pins by Terraspark, omnisexual pin by juliangiebel, genderqueer and genderfluid by centcomofficer24, ally by FairlySadPanda, aroace by momochitters, plural by CubixThree, flower by toast_enjoyer1 (Discord)",
   "size": {
     "x": 32,
     "y": 32
         {
             "name": "fluid-equipped-NECK",
             "directions": 4
+        },
+        {
+            "name": "flower"
+        },
+        {
+            "name": "flower-equipped-NECK",
+            "directions": 4
         }
     ]
 }