From: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Date: Mon, 1 Dec 2025 01:31:12 +0000 (-0800) Subject: Add water flower for clowns (#41469) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=68ca82cfd74585b47f69e52b4f6ac62dff498125;p=space-station-14.git 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 --- 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 0000000000..5d97ebd100 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 index 0000000000..6b9ccfc7a7 Binary files /dev/null and b/Resources/Textures/Clothing/Neck/Misc/pins.rsi/flower.png differ diff --git a/Resources/Textures/Clothing/Neck/Misc/pins.rsi/meta.json b/Resources/Textures/Clothing/Neck/Misc/pins.rsi/meta.json index ac7d927d23..6f93169098 100644 --- a/Resources/Textures/Clothing/Neck/Misc/pins.rsi/meta.json +++ b/Resources/Textures/Clothing/Neck/Misc/pins.rsi/meta.json @@ -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 @@ -132,6 +132,13 @@ { "name": "fluid-equipped-NECK", "directions": 4 + }, + { + "name": "flower" + }, + { + "name": "flower-equipped-NECK", + "directions": 4 } ] }