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
This commit is contained in:
beck-thompson
2025-11-30 17:31:12 -08:00
committed by GitHub
parent 5334e42500
commit 68ca82cfd7
17 changed files with 258 additions and 33 deletions

View File

@@ -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;

View 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);
}
}

View File

@@ -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;
}

View 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";
}

View File

@@ -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;

View 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()
{

View 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);
}
}

View File

@@ -0,0 +1 @@
equip-spray-verb-press = Press

View 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!

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -587,6 +587,7 @@
minLimit: 0
loadouts:
- LizardPlushieClown
- FlowerWaterClown
- type: loadoutGroup
id: MimeHead

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

View 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
@@ -132,6 +132,13 @@
{
"name": "fluid-equipped-NECK",
"directions": 4
},
{
"name": "flower"
},
{
"name": "flower-equipped-NECK",
"directions": 4
}
]
}