From 68ca82cfd74585b47f69e52b4f6ac62dff498125 Mon Sep 17 00:00:00 2001 From: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:31:12 -0800 Subject: [PATCH] Add water flower for clowns (#41469) * Spray! * Add to clown loadout * Fix the easy things * lot nicer * spray update.. * Fix yaml * fixes * changed it to warning! * review * review * sku --- Content.Client/Fluids/SpraySystem.cs | 7 ++ .../Fluids/EntitySystems/SpraySystem.cs | 61 ++++++++----- .../Fluids/Components/EquipSprayComponent.cs | 16 ++++ .../Fluids/Components/SprayComponent.cs | 11 ++- .../Fluids/EntitySystems/SharedSpraySystem.cs | 80 ++++++++++++++++++ Content.Shared/Fluids/Events.cs | 2 +- Content.Shared/Fluids/SpraySafetySystem.cs | 10 +-- .../components/equip-spray-component.ftl | 1 + .../fluids/components/spray-component.ftl | 4 +- Resources/Prototypes/Actions/types.yml | 11 +++ .../Entities/Clothing/Neck/pins.yml | 52 ++++++++++++ .../Objects/Specific/Janitorial/spray.yml | 14 +++ .../Loadouts/Miscellaneous/jobtrinkets.yml | 12 +++ .../Prototypes/Loadouts/loadout_groups.yml | 1 + .../Misc/pins.rsi/flower-equipped-NECK.png | Bin 0 -> 164 bytes .../Clothing/Neck/Misc/pins.rsi/flower.png | Bin 0 -> 342 bytes .../Clothing/Neck/Misc/pins.rsi/meta.json | 9 +- 17 files changed, 258 insertions(+), 33 deletions(-) create mode 100644 Content.Client/Fluids/SpraySystem.cs create mode 100644 Content.Shared/Fluids/Components/EquipSprayComponent.cs rename {Content.Server => Content.Shared}/Fluids/Components/SprayComponent.cs (76%) create mode 100644 Content.Shared/Fluids/EntitySystems/SharedSpraySystem.cs create mode 100644 Resources/Locale/en-US/fluids/components/equip-spray-component.ftl create mode 100644 Resources/Textures/Clothing/Neck/Misc/pins.rsi/flower-equipped-NECK.png create mode 100644 Resources/Textures/Clothing/Neck/Misc/pins.rsi/flower.png diff --git a/Content.Client/Fluids/SpraySystem.cs b/Content.Client/Fluids/SpraySystem.cs new file mode 100644 index 0000000000..877a2a0592 --- /dev/null +++ b/Content.Client/Fluids/SpraySystem.cs @@ -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; diff --git a/Content.Server/Fluids/EntitySystems/SpraySystem.cs b/Content.Server/Fluids/EntitySystems/SpraySystem.cs index 2a6b0644be..4708954ea1 100644 --- a/Content.Server/Fluids/EntitySystems/SpraySystem.cs +++ b/Content.Server/Fluids/EntitySystems/SpraySystem.cs @@ -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().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 entity, EntityUid user, MapCoordinates mapcoord) + public override void Spray(Entity 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 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(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(); - 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(user, out var body)) + var thingGettingPushed = entity.Owner; + if (_container.TryGetOuterContainer(entity, sprayerXform, out var container)) + thingGettingPushed = container.Owner; + + if (TryComp(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 index 0000000000..fe6cf97211 --- /dev/null +++ b/Content.Shared/Fluids/Components/EquipSprayComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Fluids.Components; + +/// +/// Allows items with the spray component to be equipped and sprayable with a unique action. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class EquipSprayComponent : Component +{ + /// + /// Verb locid that will come up when interacting with the sprayer. Set to null for no verb! + /// + [DataField] + public LocId? VerbLocId; +} diff --git a/Content.Server/Fluids/Components/SprayComponent.cs b/Content.Shared/Fluids/Components/SprayComponent.cs similarity index 76% rename from Content.Server/Fluids/Components/SprayComponent.cs rename to Content.Shared/Fluids/Components/SprayComponent.cs index 128fdecfa7..cc0032c3fb 100644 --- a/Content.Server/Fluids/Components/SprayComponent.cs +++ b/Content.Shared/Fluids/Components/SprayComponent.cs @@ -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 index 0000000000..42883f385e --- /dev/null +++ b/Content.Shared/Fluids/EntitySystems/SharedSpraySystem.cs @@ -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>(OnGetVerb); + SubscribeLocalEvent(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(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 entity, ref GetVerbsEvent args) + { + if (entity.Comp.VerbLocId == null || !args.CanAccess || !args.CanInteract) + return; + + var sprayComponent = Comp(entity); + var user = args.User; + + var verb = new EquipmentVerb + { + Act = () => + { + Spray((entity, sprayComponent), user); + }, + Text = Loc.GetString(entity.Comp.VerbLocId), + }; + args.Verbs.Add(verb); + } + + /// + /// 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. + /// + /// Entity that is spraying. + /// The coordinates being aimed at. + /// The user that is using the spraying device. + public virtual void Spray(Entity entity, MapCoordinates mapcoord, EntityUid? user = null) + { + // do nothing! + } + + /// + /// Spray starting from the entity and facing the direction its pointing. + /// + /// Entity that is spraying. + /// User that is using the spraying device. + public virtual void Spray(Entity entity, EntityUid? user = null) + { + // do nothing! + } +} + +public sealed partial class SprayLiquidEvent : InstantActionEvent; + diff --git a/Content.Shared/Fluids/Events.cs b/Content.Shared/Fluids/Events.cs index 198e888774..e9f2bb8594 100644 --- a/Content.Shared/Fluids/Events.cs +++ b/Content.Shared/Fluids/Events.cs @@ -39,7 +39,7 @@ public sealed partial class AbsorbantDoAfterEvent : DoAfterEvent /// Raised when trying to spray something, for example a fire extinguisher. /// [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() { diff --git a/Content.Shared/Fluids/SpraySafetySystem.cs b/Content.Shared/Fluids/SpraySafetySystem.cs index 82006a995b..c206bbda08 100644 --- a/Content.Shared/Fluids/SpraySafetySystem.cs +++ b/Content.Shared/Fluids/SpraySafetySystem.cs @@ -35,10 +35,10 @@ public sealed class SpraySafetySystem : EntitySystem private void OnSprayAttempt(Entity 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 index 0000000000..f2ab8319be --- /dev/null +++ b/Resources/Locale/en-US/fluids/components/equip-spray-component.ftl @@ -0,0 +1 @@ +equip-spray-verb-press = Press diff --git a/Resources/Locale/en-US/fluids/components/spray-component.ftl b/Resources/Locale/en-US/fluids/components/spray-component.ftl index e7060f2287..a7cd308edf 100644 --- a/Resources/Locale/en-US/fluids/components/spray-component.ftl +++ b/Resources/Locale/en-US/fluids/components/spray-component.ftl @@ -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! diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index 752aeb13f8..d5ad1f3b55 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -254,6 +254,17 @@ - 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 diff --git a/Resources/Prototypes/Entities/Clothing/Neck/pins.yml b/Resources/Prototypes/Entities/Clothing/Neck/pins.yml index f540596afa..af23e97116 100644 --- a/Resources/Prototypes/Entities/Clothing/Neck/pins.yml +++ b/Resources/Prototypes/Entities/Clothing/Neck/pins.yml @@ -261,3 +261,55 @@ 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 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml index f335244806..a8fb3b55a6 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml @@ -194,3 +194,17 @@ 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 diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml b/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml index b5e3f6bdd7..0cfc932c77 100644 --- a/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml +++ b/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml @@ -153,6 +153,18 @@ back: - PlushieLizardJobClown +- type: loadout + id: FlowerWaterClown + effects: + - !type:JobRequirementLoadoutEffect + requirement: + !type:RoleTimeRequirement + role: JobClown + time: 4h + storage: + back: + - SprayFlowerPin + - type: loadout id: LizardPlushieMime effects: diff --git a/Resources/Prototypes/Loadouts/loadout_groups.yml b/Resources/Prototypes/Loadouts/loadout_groups.yml index c79689d5a0..b1b1a3a294 100644 --- a/Resources/Prototypes/Loadouts/loadout_groups.yml +++ b/Resources/Prototypes/Loadouts/loadout_groups.yml @@ -587,6 +587,7 @@ 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 index 0000000000000000000000000000000000000000..5d97ebd100c621e725e053e491832d8e80e1bae2 GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|;yhg(Ln`LH zJ?G8a;vnE~@LX1gU`J%FLK@G4rr!eV=1B9_OxhX3Sy3qnRD5Uq*35Skw{Fdp`)lOV zzsBm{Rpsn2r-k=#Z`}NJ({rix(@$rs0+qo)L%H7devxq7$QVVCkf*Dk%Q~loCIBM) BHqZb7 literal 0 HcmV?d00001 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 index 0000000000000000000000000000000000000000..6b9ccfc7a7ca0cb268eb382bbbae5a06d1fdc528 GIT binary patch literal 342 zcmV-c0jd6pP)Px$5J^NqR9J=Wl`(F@KoCWr1D(qZrbgldbWRxwF2w;-<`}s^N@ydw^93xq_G(*7 zWC~(Gtv>`cOLW=LV$Nu^00s!9L=OLeu7tkQ> zVJ9qBOEQ}W37Gm6b>A1O<)866Hqqo&O`z*IvpJc~IqZbGtS;8IhxNxqH3gLboQ^v00eJn!B(uKjx~xby79MSZo3JYYV3OIz8sFlKnZSQm_>5mI zU}z82Wz}0@q_`1gq)>@aA47N7ryENOV1JLIE21u|b6kP91uk7dBgM^ntp(}{Tqhj+ o