From 763da782d4e4929d8f09d7292a3b8312c3656b6c Mon Sep 17 00:00:00 2001 From: SimpleStation14 <130339894+SimpleStation14@users.noreply.github.com> Date: Sat, 11 May 2024 22:36:24 -0700 Subject: [PATCH] Mirror: Spray Paint (Review Ready) (#258) ## Mirror of PR #23003: [Spray Paint (Review Ready)](https://github.com/space-wizards/space-station-14/pull/23003) from space-wizards [space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14) ###### `e4d5e7f1aebfc37b1bc3453fdb39578f3897b6a1` PR opened by brainfood1183 at 2023-12-26 12:15:46 UTC --- PR changed 41 files with 1008 additions and 11 deletions. The PR had the following labels: - Changes: Sprites - Status: Needs Review ---

Original Body

> Added - new spray paint sprites and entities. > Added - paint system which lets you paint over entities. > > https://github.com/space-wizards/space-station-14/assets/113240905/b1b30678-b9c5-45e2-83a7-d146d02c3751 > > - [x] Spray paint can paint objects. > - [x] Spray paint can come in different colors. > - [x] Spray paint can be removed. > - [x] Add a doafter for removing paint. > - [x] Stealth objects will keep their post shader. > - [x] Sprite outlines work after object is sprayed. > - [x] Add doafter for applying paint. > - [x] Add a blacklist for items that should not be painted. > - [x] entity sprites in icon slots appear colored correctly. > - [x] Add verbs for paint and remove paint for painting containers. > - [x] Spraying an entity with inventory slots. Now any items in inventory slots are also painted. > - [x] Spray paints now use fewer entities (no cap, open and closed entities) instead they use OpenableComponent and visualizers. > - [x] Spray paints must be opened to use them and can be resealed. > > What Can Be Painted > > Everything with the following exceptions, Mobs with humanoidAppearanceComponent, Items with RandomSpriteComponent, BlackListed Entities such as Holograms, Entities which go under the floor, Entities that are already painted. > > How it works > > Painting an entity will attempt to add both a greyscale shader and color to each layer of the entity. This will fail if the target has humanoidappearance, is already painted, has randomspritecomponent, is an under floor entity or is blacklisted from being painted. > > when paint is applied each layer is shaded and colored only if there is no existing shader. > The icon, inhands and clothing sprites are recolored and shaded. > > When an entity is painted it cannot be painted over without first removing the current paint. > > To remove the paint you use soap on the painted entity. When soap is used on the entity the color is removed (if the layer had a color prior to removal it will revert to that color, seems like it is color + color then color - color to end up back at original color) from the layer provided the current color of the layer matches the color in paintedcomponent. The shader is also removed provided the current shader matches the one in paintedcomponent. > > bugs & work arounds > > Currently entities with RandomSpriteComponent cannot be sprayed, these include screwdrivers and wirecutters (essentially sprites that have a random color on spawn. Entities that have a specific color on spawn work fine.) The paint system checks for RandomSpriteComponent and provides the all ready painted response. > > Entities and clothing in hand or equipped if sprayed or paint removed will not update their visual status until interacted with. > > - [x] I have added screenshots/videos to this PR showcasing its changes ingame, **or** this PR does not require an ingame showcase > > > **Changelog** > :cl: > - add: Added Spray Paints. > >
Co-authored-by: SimpleStation14 --- Content.Client/Paint/PaintVisualizerSystem.cs | 120 +++++++ Content.Server/Paint/PaintSystem.cs | 180 +++++++++++ .../EntitySystems/SpawnItemsOnUseSystem.cs | 6 +- Content.Shared/Paint/PaintComponent.cs | 60 ++++ Content.Shared/Paint/PaintDoAfterEvent.cs | 9 + Content.Shared/Paint/PaintRemoverComponent.cs | 24 ++ .../Paint/PaintRemoverDoAfterEvent.cs | 9 + Content.Shared/Paint/PaintRemoverSystem.cs | 96 ++++++ Content.Shared/Paint/PaintedComponent.cs | 41 +++ Content.Shared/Paint/SharedPaintSystem.cs | 11 + .../SprayPainter/SharedSprayPainterSystem.cs | 3 + Resources/Locale/en-US/paint/paint.ftl | 8 + .../Prototypes/Catalog/Cargo/cargo_fun.yml | 10 + .../Prototypes/Catalog/Fills/Crates/fun.yml | 48 ++- .../Prototypes/Catalog/Fills/Lockers/misc.yml | 4 + .../Markers/Spawners/Random/crates.yml | 1 + .../Markers/Spawners/Random/maintenance.yml | 5 + .../Prototypes/Entities/Mobs/NPCs/carp.yml | 1 + .../Entities/Mobs/NPCs/revenant.yml | 3 + .../Entities/Mobs/Player/guardian.yml | 2 + .../Entities/Objects/Fun/spray_paint.yml | 292 ++++++++++++++++++ .../Objects/Materials/Sheets/glass.yml | 1 + .../Objects/Materials/Sheets/metal.yml | 1 + .../Objects/Materials/Sheets/other.yml | 3 + .../Entities/Objects/Materials/materials.yml | 1 + .../Entities/Objects/Misc/tiles.yml | 3 + .../Objects/Specific/Janitorial/soap.yml | 1 + .../Objects/Weapons/Melee/e_sword.yml | 4 + .../Structures/Decoration/bonfire.yml | 3 + .../Structures/Holographic/projections.yml | 3 + .../Entities/Structures/hydro_tray.yml | 3 + Resources/Prototypes/tags.yml | 3 + .../Textures/Interface/VerbIcons/paint.svg | 39 +++ .../Interface/VerbIcons/paint.svg.192dpi.png | Bin 0 -> 15653 bytes .../VerbIcons/paint.svg.192dpi.png.yml | 2 + .../Fun/spraycans.rsi/clown-inhand-left.png | Bin 0 -> 20773 bytes .../Fun/spraycans.rsi/clown-inhand-right.png | Bin 0 -> 20783 bytes .../Objects/Fun/spraycans.rsi/meta.json | 19 +- .../Fun/spraycans.rsi/spray-inhand-left.png | Bin 0 -> 21199 bytes .../Fun/spraycans.rsi/spray-inhand-right.png | Bin 0 -> 21213 bytes .../Objects/Fun/spraycans.rsi/spray_cap.png | Bin 246 -> 0 bytes 41 files changed, 1008 insertions(+), 11 deletions(-) create mode 100644 Content.Client/Paint/PaintVisualizerSystem.cs create mode 100644 Content.Server/Paint/PaintSystem.cs create mode 100644 Content.Shared/Paint/PaintComponent.cs create mode 100644 Content.Shared/Paint/PaintDoAfterEvent.cs create mode 100644 Content.Shared/Paint/PaintRemoverComponent.cs create mode 100644 Content.Shared/Paint/PaintRemoverDoAfterEvent.cs create mode 100644 Content.Shared/Paint/PaintRemoverSystem.cs create mode 100644 Content.Shared/Paint/PaintedComponent.cs create mode 100644 Content.Shared/Paint/SharedPaintSystem.cs create mode 100644 Resources/Locale/en-US/paint/paint.ftl create mode 100644 Resources/Prototypes/Entities/Objects/Fun/spray_paint.yml create mode 100644 Resources/Textures/Interface/VerbIcons/paint.svg create mode 100644 Resources/Textures/Interface/VerbIcons/paint.svg.192dpi.png create mode 100644 Resources/Textures/Interface/VerbIcons/paint.svg.192dpi.png.yml create mode 100644 Resources/Textures/Objects/Fun/spraycans.rsi/clown-inhand-left.png create mode 100644 Resources/Textures/Objects/Fun/spraycans.rsi/clown-inhand-right.png create mode 100644 Resources/Textures/Objects/Fun/spraycans.rsi/spray-inhand-left.png create mode 100644 Resources/Textures/Objects/Fun/spraycans.rsi/spray-inhand-right.png delete mode 100644 Resources/Textures/Objects/Fun/spraycans.rsi/spray_cap.png diff --git a/Content.Client/Paint/PaintVisualizerSystem.cs b/Content.Client/Paint/PaintVisualizerSystem.cs new file mode 100644 index 00000000000..6c99b2d35f0 --- /dev/null +++ b/Content.Client/Paint/PaintVisualizerSystem.cs @@ -0,0 +1,120 @@ +using System.Linq; +using Robust.Client.GameObjects; +using static Robust.Client.GameObjects.SpriteComponent; +using Content.Shared.Clothing; +using Content.Shared.Hands; +using Content.Shared.Paint; +using Robust.Client.Graphics; +using Robust.Shared.Prototypes; + +namespace Content.Client.Paint +{ + public sealed class PaintedVisualizerSystem : VisualizerSystem + { + /// + /// Visualizer for Paint which applies a shader and colors the entity. + /// + + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; + + public ShaderInstance? Shader; // in Robust.Client.Graphics so cannot move to shared component. + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHeldVisualsUpdated); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnEquipmentVisualsUpdated); + } + + protected override void OnAppearanceChange(EntityUid uid, PaintedComponent component, ref AppearanceChangeEvent args) + { + // ShaderPrototype sadly in Robust.Client, cannot move to shared component. + Shader = _protoMan.Index(component.ShaderName).Instance(); + + if (args.Sprite == null) + return; + + if (!_appearance.TryGetData(uid, PaintVisuals.Painted, out bool isPainted)) + return; + + var sprite = args.Sprite; + + + foreach (var spriteLayer in sprite.AllLayers) + { + if (spriteLayer is not Layer layer) + continue; + + if (layer.Shader == null) // If shader isn't null we dont want to replace the original shader. + { + layer.Shader = Shader; + layer.Color = component.Color; + } + } + } + + private void OnHeldVisualsUpdated(EntityUid uid, PaintedComponent component, HeldVisualsUpdatedEvent args) + { + if (args.RevealedLayers.Count == 0) + return; + + if (!TryComp(args.User, out SpriteComponent? sprite)) + return; + + foreach (var revealed in args.RevealedLayers) + { + if (!sprite.LayerMapTryGet(revealed, out var layer) || sprite[layer] is not Layer notlayer) + continue; + + sprite.LayerSetShader(layer, component.ShaderName); + sprite.LayerSetColor(layer, component.Color); + } + } + + private void OnEquipmentVisualsUpdated(EntityUid uid, PaintedComponent component, EquipmentVisualsUpdatedEvent args) + { + if (args.RevealedLayers.Count == 0) + return; + + if (!TryComp(args.Equipee, out SpriteComponent? sprite)) + return; + + foreach (var revealed in args.RevealedLayers) + { + if (!sprite.LayerMapTryGet(revealed, out var layer) || sprite[layer] is not Layer notlayer) + continue; + + sprite.LayerSetShader(layer, component.ShaderName); + sprite.LayerSetColor(layer, component.Color); + } + } + + private void OnShutdown(EntityUid uid, PaintedComponent component, ref ComponentShutdown args) + { + if (!TryComp(uid, out SpriteComponent? sprite)) + return; + + component.BeforeColor = sprite.Color; + Shader = _protoMan.Index(component.ShaderName).Instance(); + + if (!Terminating(uid)) + { + foreach (var spriteLayer in sprite.AllLayers) + { + if (spriteLayer is not Layer layer) + continue; + + if (layer.Shader == Shader) // If shader isn't same as one in component we need to ignore it. + { + layer.Shader = null; + if (layer.Color == component.Color) // If color isn't the same as one in component we don't want to change it. + layer.Color = component.BeforeColor; + } + } + } + } + } +} diff --git a/Content.Server/Paint/PaintSystem.cs b/Content.Server/Paint/PaintSystem.cs new file mode 100644 index 00000000000..c6718aced68 --- /dev/null +++ b/Content.Server/Paint/PaintSystem.cs @@ -0,0 +1,180 @@ +using Content.Shared.Popups; +using Content.Shared.Paint; +using Content.Shared.Sprite; +using Content.Shared.DoAfter; +using Content.Shared.Interaction; +using Content.Server.Chemistry.Containers.EntitySystems; +using Robust.Shared.Audio.Systems; +using Content.Shared.Humanoid; +using Robust.Shared.Utility; +using Content.Shared.Verbs; +using Content.Shared.SubFloor; +using Content.Server.Nutrition.Components; +using Content.Shared.Inventory; +using Content.Server.Nutrition.EntitySystems; + +namespace Content.Server.Paint; + +/// +/// Colors target and consumes reagent on each color success. +/// +public sealed class PaintSystem : SharedPaintSystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly OpenableSystem _openable = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInteract); + SubscribeLocalEvent(OnPaint); + SubscribeLocalEvent>(OnPaintVerb); + } + + private void OnInteract(EntityUid uid, PaintComponent component, AfterInteractEvent args) + { + if (!args.CanReach) + return; + + if (args.Target is not { Valid: true } target) + return; + + PrepPaint(uid, component, target, args.User); + } + + private void OnPaintVerb(EntityUid uid, PaintComponent component, GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess) + return; + + var paintText = Loc.GetString("paint-verb"); + + var verb = new UtilityVerb() + { + Act = () => + { + PrepPaint(uid, component, args.Target, args.User); + }, + + Text = paintText, + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/paint.svg.192dpi.png")) + }; + args.Verbs.Add(verb); + } + private void PrepPaint(EntityUid uid, PaintComponent component, EntityUid target, EntityUid user) + { + + var doAfterEventArgs = new DoAfterArgs(EntityManager, user, component.Delay, new PaintDoAfterEvent(), uid, target: target, used: uid) + { + BreakOnTargetMove = true, + BreakOnUserMove = true, + BreakOnDamage = true, + NeedHand = true, + BreakOnHandChange = true + }; + + if (!_doAfterSystem.TryStartDoAfter(doAfterEventArgs)) + return; + } + + private void OnPaint(Entity entity, ref PaintDoAfterEvent args) + { + if (args.Target == null || args.Used == null) + return; + + if (args.Handled || args.Cancelled) + return; + + if (args.Target is not { Valid: true } target) + return; + + if (!_openable.IsOpen(entity)) + { + _popup.PopupEntity(Loc.GetString("paint-closed", ("used", args.Used)), args.User, args.User, PopupType.Medium); + return; + } + + if (HasComp(target) || HasComp(target)) + { + _popup.PopupEntity(Loc.GetString("paint-failure-painted", ("target", args.Target)), args.User, args.User, PopupType.Medium); + return; + } + + if (!entity.Comp.Blacklist?.IsValid(target, EntityManager) != true || HasComp(target) || HasComp(target)) + { + _popup.PopupEntity(Loc.GetString("paint-failure", ("target", args.Target)), args.User, args.User, PopupType.Medium); + return; + } + + + if (TryPaint(entity, target)) + { + EnsureComp(target, out PaintedComponent? paint); + EnsureComp(target); + + paint.Color = entity.Comp.Color; // set the target color to the color specified in the spray paint yml. + _audio.PlayPvs(entity.Comp.Spray, entity); + paint.Enabled = true; + + if (HasComp(target)) // Paint any clothing the target is wearing. + { + if (_inventory.TryGetSlots(target, out var slotDefinitions)) + { + foreach (var slot in slotDefinitions) + { + if (!_inventory.TryGetSlotEntity(target, slot.Name, out var slotEnt)) + continue; + + if (slotEnt == null) + return; + + if (HasComp(slotEnt.Value) || !entity.Comp.Blacklist?.IsValid(slotEnt.Value, EntityManager) != true + || HasComp(slotEnt.Value) || HasComp(slotEnt.Value)) + return; + + EnsureComp(slotEnt.Value, out PaintedComponent? slotpaint); + EnsureComp(slotEnt.Value); + slotpaint.Color = entity.Comp.Color; + _appearanceSystem.SetData(slotEnt.Value, PaintVisuals.Painted, true); + Dirty(slotEnt.Value, slotpaint); + } + } + } + + _popup.PopupEntity(Loc.GetString("paint-success", ("target", args.Target)), args.User, args.User, PopupType.Medium); + _appearanceSystem.SetData(target, PaintVisuals.Painted, true); + Dirty(target, paint); + args.Handled = true; + return; + } + + if (!TryPaint(entity, target)) + { + _popup.PopupEntity(Loc.GetString("paint-empty", ("used", args.Used)), args.User, args.User, PopupType.Medium); + return; + } + } + + private bool TryPaint(Entity reagent, EntityUid target) + { + if (HasComp(target) || HasComp(target)) + return false; + + if (_solutionContainer.TryGetSolution(reagent.Owner, reagent.Comp.Solution, out _, out var solution)) + { + var quantity = solution.RemoveReagent(reagent.Comp.Reagent, reagent.Comp.ConsumptionUnit); + if (quantity > 0)// checks quantity of solution is more than 0. + return true; + + if (quantity < 1) + return false; + } + return false; + } +} diff --git a/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs b/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs index c49bfdec931..0b4b13d6e4b 100644 --- a/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs +++ b/Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs @@ -80,11 +80,6 @@ private void OnUseInHand(EntityUid uid, SpawnItemsOnUseComponent component, UseI _adminLogger.Add(LogType.EntitySpawn, LogImpact.Low, $"{ToPrettyString(args.User)} used {ToPrettyString(uid)} which spawned {ToPrettyString(entityToPlaceInHands.Value)}"); } - if (component.Sound != null) - { - _audio.PlayPvs(component.Sound, uid); - } - component.Uses--; // Delete entity only if component was successfully used @@ -97,6 +92,7 @@ private void OnUseInHand(EntityUid uid, SpawnItemsOnUseComponent component, UseI if (entityToPlaceInHands != null) { _hands.PickupOrDrop(args.User, entityToPlaceInHands.Value); + _audio.PlayPvs(component.Sound, entityToPlaceInHands.Value); } } } diff --git a/Content.Shared/Paint/PaintComponent.cs b/Content.Shared/Paint/PaintComponent.cs new file mode 100644 index 00000000000..ad09f4ca730 --- /dev/null +++ b/Content.Shared/Paint/PaintComponent.cs @@ -0,0 +1,60 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Robust.Shared.Audio; +using Content.Shared.Whitelist; +using Robust.Shared.Prototypes; +using Robust.Shared.GameStates; + +namespace Content.Shared.Paint; + +/// +/// Entity when used on another entity will paint target entity. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedPaintSystem))] +public sealed partial class PaintComponent : Component +{ + /// + /// Noise made when paint applied. + /// + [DataField] + public SoundSpecifier Spray = new SoundPathSpecifier("/Audio/Effects/spray2.ogg"); + + /// + /// Solution on the entity that contains the paint. + /// + [DataField] + public string Solution = "drink"; + + /// + /// How long the doafter will take. + /// + [DataField] + public int Delay = 2; + + /// + /// Reagent that will be used as paint. + /// + [DataField, AutoNetworkedField] + public ProtoId Reagent = "SpaceGlue"; + + /// + /// Color that the painting entity will instruct the painted entity to be. + /// + [DataField, AutoNetworkedField] + public Color Color = Color.FromHex("#c62121"); + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public EntityWhitelist? Blacklist; + /// + /// Reagent consumption per use. + /// + [DataField] + public FixedPoint2 ConsumptionUnit = FixedPoint2.New(5); + + /// + /// Duration per unit + /// + [DataField] + public TimeSpan DurationPerUnit = TimeSpan.FromSeconds(6); +} diff --git a/Content.Shared/Paint/PaintDoAfterEvent.cs b/Content.Shared/Paint/PaintDoAfterEvent.cs new file mode 100644 index 00000000000..0851f1541b4 --- /dev/null +++ b/Content.Shared/Paint/PaintDoAfterEvent.cs @@ -0,0 +1,9 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Paint; + +[Serializable, NetSerializable] +public sealed partial class PaintDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Content.Shared/Paint/PaintRemoverComponent.cs b/Content.Shared/Paint/PaintRemoverComponent.cs new file mode 100644 index 00000000000..54d0ed7a71b --- /dev/null +++ b/Content.Shared/Paint/PaintRemoverComponent.cs @@ -0,0 +1,24 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Audio; + +namespace Content.Shared.Paint; + +/// +/// Removes paint from an entity that was painted with spray paint. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(PaintRemoverSystem))] +public sealed partial class PaintRemoverComponent : Component +{ + /// + /// Sound when target is cleaned. + /// + [DataField] + public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg"); + + /// + /// DoAfter wait time. + /// + [DataField] + public float CleanDelay = 2f; +} diff --git a/Content.Shared/Paint/PaintRemoverDoAfterEvent.cs b/Content.Shared/Paint/PaintRemoverDoAfterEvent.cs new file mode 100644 index 00000000000..940b1aa513c --- /dev/null +++ b/Content.Shared/Paint/PaintRemoverDoAfterEvent.cs @@ -0,0 +1,9 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Paint; + +[Serializable, NetSerializable] +public sealed partial class PaintRemoverDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Content.Shared/Paint/PaintRemoverSystem.cs b/Content.Shared/Paint/PaintRemoverSystem.cs new file mode 100644 index 00000000000..ac1cc624cfe --- /dev/null +++ b/Content.Shared/Paint/PaintRemoverSystem.cs @@ -0,0 +1,96 @@ +using Content.Shared.Popups; +using Content.Shared.Interaction; +using Content.Shared.DoAfter; +using Content.Shared.Verbs; +using Content.Shared.Sprite; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Timing; + +namespace Content.Shared.Paint; + +/// +/// Removes paint from an entity. +/// +public sealed class PaintRemoverSystem : SharedPaintSystem +{ + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInteract); + SubscribeLocalEvent(OnDoAfter); + SubscribeLocalEvent>(OnPaintRemoveVerb); + } + + // When entity is painted, remove paint from that entity. + private void OnInteract(EntityUid uid, PaintRemoverComponent component, AfterInteractEvent args) + { + if (args.Handled) + return; + + if (!args.CanReach || args.Target is not { Valid: true } target || !HasComp(target)) + return; + + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.CleanDelay, new PaintRemoverDoAfterEvent(), uid, args.Target, uid) + { + BreakOnUserMove = true, + BreakOnDamage = true, + BreakOnTargetMove = true, + MovementThreshold = 1.0f, + }); + args.Handled = true; + } + + private void OnDoAfter(EntityUid uid, PaintRemoverComponent component, DoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Args.Target == null) + return; + + if (args.Target is not { Valid: true } target) + return; + + if (!TryComp(target, out PaintedComponent? paint)) + return; + + paint.Enabled = false; + _audio.PlayPredicted(component.Sound, target, args.User); + _popup.PopupClient(Loc.GetString("paint-removed", ("target", target)), args.User, args.User, PopupType.Medium); + _appearanceSystem.SetData(target, PaintVisuals.Painted, false); + RemComp(target); + Dirty(target, paint); + + args.Handled = true; + } + + private void OnPaintRemoveVerb(EntityUid uid, PaintRemoverComponent component, GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess) + return; + + var paintremovalText = Loc.GetString("paint-remove-verb"); + + var verb = new UtilityVerb() + { + Act = () => { + + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.CleanDelay, new PaintRemoverDoAfterEvent(), uid, args.Target, uid) + { + BreakOnUserMove = true, + BreakOnDamage = true, + BreakOnTargetMove = true, + MovementThreshold = 1.0f, + }); + }, + + Text = paintremovalText + }; + + args.Verbs.Add(verb); + } +} diff --git a/Content.Shared/Paint/PaintedComponent.cs b/Content.Shared/Paint/PaintedComponent.cs new file mode 100644 index 00000000000..a6ee7377e11 --- /dev/null +++ b/Content.Shared/Paint/PaintedComponent.cs @@ -0,0 +1,41 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Paint; + +/// +/// Component applied to target entity when painted. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class PaintedComponent : Component +{ + /// + /// Color of the paint. + /// + [DataField, AutoNetworkedField] + public Color Color = Color.FromHex("#2cdbd5"); + + /// + /// Used to remove the color when component removed. + /// + [DataField, AutoNetworkedField] + public Color BeforeColor; + + /// + /// If paint is enabled. + /// + [DataField, AutoNetworkedField] + public bool Enabled; + + /// + /// Name of the shader. + /// + [DataField, AutoNetworkedField] + public string ShaderName = "Greyscale"; +} + +[Serializable, NetSerializable] +public enum PaintVisuals : byte +{ + Painted, +} diff --git a/Content.Shared/Paint/SharedPaintSystem.cs b/Content.Shared/Paint/SharedPaintSystem.cs new file mode 100644 index 00000000000..10185817b86 --- /dev/null +++ b/Content.Shared/Paint/SharedPaintSystem.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.Paint; + +/// +/// Colors target and consumes reagent on each color success. +/// +public abstract class SharedPaintSystem : EntitySystem +{ + public virtual void UpdateAppearance(EntityUid uid, PaintedComponent? component = null) + { + } +} diff --git a/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs b/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs index 529e321f8da..fa04a50f8b0 100644 --- a/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs +++ b/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Doors.Components; using Content.Shared.Interaction; using Content.Shared.Popups; +using Content.Shared.Paint; using Content.Shared.SprayPainter.Components; using Content.Shared.SprayPainter.Prototypes; using Robust.Shared.Audio.Systems; @@ -129,6 +130,8 @@ private void OnAirlockInteract(Entity ent, ref Intera return; } + RemComp(ent); + var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, painter.AirlockSprayTime, new SprayPainterDoorDoAfterEvent(sprite, style.Department), args.Used, target: ent, used: args.Used) { BreakOnTargetMove = true, diff --git a/Resources/Locale/en-US/paint/paint.ftl b/Resources/Locale/en-US/paint/paint.ftl new file mode 100644 index 00000000000..200b1f6e3f3 --- /dev/null +++ b/Resources/Locale/en-US/paint/paint.ftl @@ -0,0 +1,8 @@ +paint-success = {THE($target)} has been covered in paint! +paint-failure = Can't cover {THE($target)} in paint! +paint-failure-painted = {THE($target)} is already covered in paint! +paint-empty = {THE($used)} is empty! +paint-removed = You clean off the paint! +paint-closed = You must open {THE($used)} first! +paint-verb = Paint +paint-remove-verb = Remove Paint diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml b/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml index 2d5acf5c90f..d84fedd543f 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml @@ -68,6 +68,16 @@ category: Fun group: market +- type: cargoProduct + id: FunSprayPaints + icon: + sprite: Objects/Fun/spraycans.rsi + state: death2_cap + product: CrateFunSprayPaints + cost: 2000 + category: Fun + group: market + - type: cargoProduct id: FunParty icon: diff --git a/Resources/Prototypes/Catalog/Fills/Crates/fun.yml b/Resources/Prototypes/Catalog/Fills/Crates/fun.yml index 1db0c3121ed..cc5e3b1d174 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/fun.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/fun.yml @@ -292,14 +292,21 @@ contents: - id: SnapPopBox - id: CrazyGlue - amount: 2 - id: PlasticBanana + - id: FunnyPaint + orGroup: Paint + prob: 0.5 + - id: FunnyPaintYellow + orGroup: Paint + prob: 0.5 - id: WhoopieCushion - id: ToyHammer - id: MrChips - orGroup: GiftPool + prob: 0.5 + orGroup: Dummy - id: MrDips - orGroup: Giftpool + prob: 0.5 + orGroup: Dummy - id: RevolverCapGun - id: BalloonNT - id: ClothingShoesClownLarge @@ -332,6 +339,41 @@ amount: 15 prob: 0.05 +- type: entity + id: CrateFunSprayPaints + name: spray paint crate + description: a crate filled with spray paint. + parent: CratePlastic + suffix: Spray Paint + components: + - type: StorageFill + contents: + - id: SprayPaintBlue + amount: 2 + prob: 0.33 + - id: SprayPaintRed + amount: 2 + prob: 0.33 + - id: SprayPaintOrange + amount: 2 + prob: 0.33 + - id: SprayPaintBlack + amount: 2 + prob: 0.33 + - id: SprayPaintGreen + amount: 2 + prob: 0.33 + - id: SprayPaintPurple + amount: 2 + prob: 0.33 + - id: SprayPaintWhite + amount: 2 + prob: 0.33 + - id: DeathPaint + amount: 2 + - id: DeathPaintTwo + amount: 2 + - type: entity name: dartboard box set description: A box with everything you need for a fun game of darts. diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml b/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml index 78a4ea3082e..f4083f42574 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml @@ -142,6 +142,10 @@ prob: 0.25 - id: StrangePill prob: 0.20 + - id: DeathPaint + prob: 0.05 + - id: DeathPaintTwo + prob: 0.05 - id: DrinkMopwataBottleRandom prob: 0.20 - id: ModularReceiver diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/crates.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/crates.yml index ae7e5bcf762..883182aae8d 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/crates.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/crates.yml @@ -44,6 +44,7 @@ - CrateMaterialPlastic - CrateMaterialWood - CrateMaterialPlasteel + - CrateFunSprayPaints - CrateFunArtSupplies - CrateEngineeringCableLV - CrateEngineeringCableMV diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml index 879c1689291..10fb034d0d5 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml @@ -174,7 +174,12 @@ - MaterialCloth10 - MaterialWoodPlank10 - ResearchDisk + - DeathPaint - Plunger + - SprayPaintBlue + - SprayPaintRed + - SprayPaintGreen + - SprayPaintOrange - TechnologyDisk - PowerCellMedium - PowerCellSmall diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index 73082674736..3e6c603626b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -70,6 +70,7 @@ tags: - Carp - DoorBumpOpener + - NoPaint - type: ReplacementAccent accent: genericAggressive - type: Speech diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml index 28355bb459e..ec1ed3a58f6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml @@ -97,3 +97,6 @@ - RevenantTheme - type: Speech speechVerb: Ghost + - type: Tag + tags: + - NoPaint diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml index c7cd40988d4..9f0d54ee64a 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml @@ -104,6 +104,7 @@ - type: Tag tags: - CannotSuicide + - NoPaint # From the uplink injector - type: entity @@ -212,6 +213,7 @@ tags: - CannotSuicide - FootstepSound + - NoPaint - type: Inventory templateId: holoclown - type: Hands diff --git a/Resources/Prototypes/Entities/Objects/Fun/spray_paint.yml b/Resources/Prototypes/Entities/Objects/Fun/spray_paint.yml new file mode 100644 index 00000000000..1b417f6cde0 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Fun/spray_paint.yml @@ -0,0 +1,292 @@ +# Base Paints +- type: entity + parent: BaseItem + id: PaintBase + name: spray paint + description: A tin of spray paint. + noSpawn: true + components: + - type: Appearance + - type: Sprite + sprite: Objects/Fun/spraycans.rsi + state: clown_cap + layers: + - state: clown_cap + map: ["enum.OpenableVisuals.Layer"] + - type: Paint + consumptionUnit: 10 + blacklist: + tags: + - NoPaint + - type: Item + sprite: Objects/Fun/spraycans.rsi + heldPrefix: spray + - type: SolutionContainerManager + solutions: + drink: + maxVol: 50 + reagents: + - ReagentId: SpaceGlue + Quantity: 50 + - type: TrashOnSolutionEmpty + solution: drink + - type: Sealable + - type: Openable + sound: + path: /Audio/Effects/pop_high.ogg + closeable: true + closeSound: + path: /Audio/Effects/pop_high.ogg + +# Paints + +# funnypaint +- type: entity + parent: PaintBase + id: FunnyPaint + name: funny paint + description: A tin of funny paint, manufactured by Honk! Co. + components: + - type: Paint + color: "#fa74df" + - type: Item + sprite: Objects/Fun/spraycans.rsi + heldPrefix: clown + - type: GenericVisualizer + visuals: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: + True: {state: "clown"} + False: {state: "clown_cap"} + +- type: entity + parent: PaintBase + id: FunnyPaintYellow + name: funny paint + description: A tin of funny paint, manufactured by Honk! Co. + components: + - type: Paint + color: "#d5e028" + - type: Item + sprite: Objects/Fun/spraycans.rsi + heldPrefix: clown + - type: Sprite + sprite: Objects/Fun/spraycans.rsi + state: clown2_cap + layers: + - state: clown2_cap + map: ["enum.OpenableVisuals.Layer"] + - type: GenericVisualizer + visuals: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: + True: {state: "clown2"} + False: {state: "clown2_cap"} + +#death paint +- type: entity + parent: PaintBase + id: DeathPaint + components: + - type: Paint + color: "#ff20c8" + - type: Item + sprite: Objects/Fun/spraycans.rsi + heldPrefix: spray + - type: Sprite + sprite: Objects/Fun/spraycans.rsi + state: death_cap + layers: + - state: death_cap + map: ["enum.OpenableVisuals.Layer"] + - type: GenericVisualizer + visuals: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: + True: {state: "death"} + False: {state: "death_cap"} + +- type: entity + parent: PaintBase + id: DeathPaintTwo + components: + - type: Paint + color: "#ff2020" + - type: Item + sprite: Objects/Fun/spraycans.rsi + heldPrefix: spray + - type: Sprite + sprite: Objects/Fun/spraycans.rsi + state: death2_cap + layers: + - state: death2_cap + map: ["enum.OpenableVisuals.Layer"] + - type: GenericVisualizer + visuals: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: + True: {state: "death2"} + False: {state: "death2_cap"} + +#Sprays + +#Blue +- type: entity + parent: PaintBase + id: SprayPaintBlue + suffix: Blue + components: + - type: Sprite + sprite: Objects/Fun/spraycans.rsi + layers: + - state: spray + map: ["Base"] + - state: spray_cap_colors + map: ["enum.OpenableVisuals.Layer"] + color: "#5890f7" + - type: Paint + color: "#5890f7" + - type: GenericVisualizer + visuals: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: + True: {state: "spray_colors" , color: "#5890f7"} + False: {state: "spray_cap_colors" , color: "#5890f7"} + +#Red +- type: entity + parent: PaintBase + id: SprayPaintRed + suffix: Red + components: + - type: Sprite + sprite: Objects/Fun/spraycans.rsi + layers: + - state: spray + map: ["Base"] + - state: spray_cap_colors + map: ["enum.OpenableVisuals.Layer"] + color: "#ff3b3b" + - type: Paint + color: "#ff3b3b" + - type: GenericVisualizer + visuals: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: + True: {state: "spray_colors" , color: "#ff3b3b"} + False: {state: "spray_cap_colors" , color: "#ff3b3b"} + +#Green +- type: entity + parent: PaintBase + id: SprayPaintGreen + suffix: Green + components: + - type: Sprite + sprite: Objects/Fun/spraycans.rsi + layers: + - state: spray + map: ["Base"] + - state: spray_cap_colors + map: ["enum.OpenableVisuals.Layer"] + color: "#73f170" + - type: Paint + color: "#73f170" + - type: GenericVisualizer + visuals: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: + True: {state: "spray_colors" , color: "#73f170"} + False: {state: "spray_cap_colors" , color: "#73f170"} + +#Black +- type: entity + parent: PaintBase + id: SprayPaintBlack + suffix: Black + components: + - type: Sprite + sprite: Objects/Fun/spraycans.rsi + layers: + - state: spray + map: ["Base"] + - state: spray_cap_colors + map: ["enum.OpenableVisuals.Layer"] + color: "#3a3a3a" + - type: Paint + color: "#3a3a3a" + - type: GenericVisualizer + visuals: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: + True: {state: "spray_colors" , color: "#3a3a3a"} + False: {state: "spray_cap_colors" , color: "#3a3a3a"} + +#Orange +- type: entity + parent: PaintBase + id: SprayPaintOrange + suffix: Orange + components: + - type: Sprite + sprite: Objects/Fun/spraycans.rsi + layers: + - state: spray + map: ["Base"] + - state: spray_cap_colors + map: ["enum.OpenableVisuals.Layer"] + color: "#f6a44b" + - type: Paint + color: "#f6a44b" + - type: GenericVisualizer + visuals: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: + True: {state: "spray_colors" , color: "#f6a44b"} + False: {state: "spray_cap_colors" , color: "#f6a44b"} + +#Purple +- type: entity + parent: PaintBase + id: SprayPaintPurple + suffix: Purple + components: + - type: Sprite + sprite: Objects/Fun/spraycans.rsi + layers: + - state: spray + map: ["Base"] + - state: spray_cap_colors + map: ["enum.OpenableVisuals.Layer"] + color: "#c063f5" + - type: Paint + color: "#c063f5" + - type: GenericVisualizer + visuals: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: + True: {state: "spray_colors" , color: "#c063f5"} + False: {state: "spray_cap_colors" , color: "#c063f5"} + +#White +- type: entity + parent: PaintBase + id: SprayPaintWhite + suffix: White + components: + - type: Sprite + sprite: Objects/Fun/spraycans.rsi + layers: + - state: spray + map: ["Base"] + - state: spray_cap_colors + map: ["enum.OpenableVisuals.Layer"] + color: "#f2f2f2" + - type: Paint + color: "#f2f2f2" + - type: GenericVisualizer + visuals: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: + True: {state: "spray_colors" , color: "#f2f2f2"} + False: {state: "spray_cap_colors" , color: "#f2f2f2"} diff --git a/Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml b/Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml index 59d8ed19220..2e0eec7a658 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml @@ -15,6 +15,7 @@ - type: Tag tags: - Sheet + - NoPaint - type: Material - type: Damageable damageContainer: Inorganic diff --git a/Resources/Prototypes/Entities/Objects/Materials/Sheets/metal.yml b/Resources/Prototypes/Entities/Objects/Materials/Sheets/metal.yml index 82b9f62837a..3a887848bf5 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/Sheets/metal.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/Sheets/metal.yml @@ -15,6 +15,7 @@ tags: - Sheet - Metal + - NoPaint - type: Damageable damageContainer: Inorganic damageModifierSet: Metallic diff --git a/Resources/Prototypes/Entities/Objects/Materials/Sheets/other.yml b/Resources/Prototypes/Entities/Objects/Materials/Sheets/other.yml index dfb51336289..9dc87a9117d 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/Sheets/other.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/Sheets/other.yml @@ -12,6 +12,7 @@ - type: Tag tags: - Sheet + - NoPaint - type: Damageable damageContainer: Inorganic - type: Destructible @@ -110,6 +111,7 @@ - type: Tag tags: - Sheet + - NoPaint - type: entity parent: SheetPlasma @@ -132,6 +134,7 @@ tags: - Plastic - Sheet + - NoPaint - type: Material - type: PhysicalComposition materialComposition: diff --git a/Resources/Prototypes/Entities/Objects/Materials/materials.yml b/Resources/Prototypes/Entities/Objects/Materials/materials.yml index e71a163b5d0..96fddefa3ec 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/materials.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/materials.yml @@ -12,6 +12,7 @@ - type: Tag tags: - RawMaterial + - NoPaint - type: Damageable damageContainer: Inorganic - type: Destructible diff --git a/Resources/Prototypes/Entities/Objects/Misc/tiles.yml b/Resources/Prototypes/Entities/Objects/Misc/tiles.yml index 78bbd32f174..99f9ccaa874 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/tiles.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/tiles.yml @@ -17,6 +17,9 @@ sound: /Audio/Weapons/star_hit.ogg - type: Stack count: 1 + - type: Tag + tags: + - NoPaint - type: Damageable damageContainer: Inorganic - type: Destructible diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml index 5678de6bafc..56786057d57 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml @@ -62,6 +62,7 @@ solution: soap - type: DeleteOnSolutionEmpty solution: soap + - type: PaintRemover - type: FlavorProfile flavors: - clean diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml index bc376df5eab..ccbbcc2de84 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml @@ -78,6 +78,9 @@ malus: 0 - type: Reflect enabled: false + - type: Tag + tags: + - NoPaint - type: IgnitionSource temperature: 700 @@ -156,6 +159,7 @@ - type: Tag tags: - Write + - NoPaint - type: DisarmMalus malus: 0 diff --git a/Resources/Prototypes/Entities/Structures/Decoration/bonfire.yml b/Resources/Prototypes/Entities/Structures/Decoration/bonfire.yml index 7777153bbac..f82fe8b51bb 100644 --- a/Resources/Prototypes/Entities/Structures/Decoration/bonfire.yml +++ b/Resources/Prototypes/Entities/Structures/Decoration/bonfire.yml @@ -31,6 +31,9 @@ sound: path: /Audio/Ambience/Objects/fireplace.ogg - type: AlwaysHot + - type: Tag + tags: + - NoPaint - type: entity id: LegionnaireBonfire diff --git a/Resources/Prototypes/Entities/Structures/Holographic/projections.yml b/Resources/Prototypes/Entities/Structures/Holographic/projections.yml index d2a5853fcb0..b8717cf6cc9 100644 --- a/Resources/Prototypes/Entities/Structures/Holographic/projections.yml +++ b/Resources/Prototypes/Entities/Structures/Holographic/projections.yml @@ -25,6 +25,9 @@ behaviors: - !type:DoActsBehavior acts: [ "Destruction" ] + - type: Tag + tags: + - NoPaint - type: entity id: HoloFan diff --git a/Resources/Prototypes/Entities/Structures/hydro_tray.yml b/Resources/Prototypes/Entities/Structures/hydro_tray.yml index 1ab1fd5b2fd..43b8bd197a5 100644 --- a/Resources/Prototypes/Entities/Structures/hydro_tray.yml +++ b/Resources/Prototypes/Entities/Structures/hydro_tray.yml @@ -92,6 +92,9 @@ - type: GuideHelp guides: - Botany + - type: Tag + tags: + - NoPaint - type: entity parent: hydroponicsTray diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index 789b7c92968..2694cbeaf39 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -877,6 +877,9 @@ - type: Tag id: NoBlockAnchoring +- type: Tag + id: NoPaint + - type: Tag id: NozzleBackTank diff --git a/Resources/Textures/Interface/VerbIcons/paint.svg b/Resources/Textures/Interface/VerbIcons/paint.svg new file mode 100644 index 00000000000..78a56e3570a --- /dev/null +++ b/Resources/Textures/Interface/VerbIcons/paint.svg @@ -0,0 +1,39 @@ + + + + + + diff --git a/Resources/Textures/Interface/VerbIcons/paint.svg.192dpi.png b/Resources/Textures/Interface/VerbIcons/paint.svg.192dpi.png new file mode 100644 index 0000000000000000000000000000000000000000..b7bd88245f2ab069dbf5a39850a866af0c5dc174 GIT binary patch literal 15653 zcmeI3Ym6J!6@c$%At6fwD^Z{%uuL393Tejkve)auHrd!ob^#~b&7$l^6=XbfZ4a?$ zoSE^icLQO0mqGyXgEvtlDvC%zAkn5$c`8a@KR`fGMTH1e8(?XniJ&}$1l*bN^RD+2 zK_vcM%O3mObI&>Ve&?R!`7^f-ZQQW9Ye^RXVDaEUY8ZX5aX+WcN7pCx9q*v8g{6Tp z8-NRLbw3?<9X$>KxOkO5GU|+`FPBxb7*sSf1A~=f3C#u|c1fkAsN12#WMED=;(`DD z@x=h6Yw^IAaGFn-k}$6iOj&UA)W#8YYP%ZM0+%GZVig%VC_+bJD#e0f%awS*^DCp@ z?l2c%yeZE1c%a`6$c(0kn51bzCLHW#RlYaEM4~|<9EpS?s~M3O!W=Jg63>c~9OC7W z$W&i}L>KxMv$U){oLXNEhopEQ?>Hry<0d91f)i5Ev~rvfjYc_Mg|JMlok4OD+s_f;&_0~vOe9iqrploBTdd9Iz2rq*pr zWvk#VO;b5ofJJCHHnJ1i+LiLAW7>Ihj*xZroPkkJhV@+DvFq{MuG#YDv|R%7kWQ1W zi^aBugabg z<-_PbBJ|3l^fO+NdA`YmKMiw`HZ?svS#uC#dC_&i^Kv*OhoW13^QK9-IjTHOag^_J z&^)clg{I0`(<&-XLN6*g$d!y-jB5hvH6m++G<$_}5;(-dQ z!K_j)IDtem$umRcOhH$flvz}CgGoV6R~4^5tLI5|^Q4bMRC+}d9SOJEOP-D9nQ&9k zGQAYXIJcw=dg~he(blDTdz-b5SiQ{Tq**X6L>nfg7}svNiAcTl%>G|h4MqM=|}ffstSqQ-{9s>Eth6fBd;s;r=9#7L-5vzZbuw8|-Se054j$BE zT+3)#&A8*Rs9&9t~OehxY&!OXSB8|`Y+G>Wo zZ(n&QB@4DP{=;%`VkPxGa?qwn|9Ba2S^Lq|JnFKj3vsoBTN^mBr1`0At*Ba+&aS|i z`?oBk>P3gi>)Ht+#?2aQME2*8x>Z;xdsQYxMIOC4!M#>(oAtQu*mu}A>v7wL=@o|V zB*bbsY`DqFy?gNg3pS%$WtivVor!L$??281k9Tx33&mg2&IkVT4%%m>p_To@3tNO6(*U|Na`140%lE;1iXOL1X9 z$Rfo>=7VV|E({1+q`1g@FfGM}0U?VN7nu*HrMNI4WRc<`^TD(f7Y2kZQe0#{n3m$g zfRIIsi_8bpQd}4ivPf}}`CwX#3j;zHDK0V}OiOWLK*%D+MdpKPDJ~2MS){ngd@wD= zg#jUp6c?Efrlq(rAY_r^BJ;tt6c+}BEK*!#KA4u`!hn!PBCf8+1F+CQPrXi{$6Mb! zmwOC78q25y!)XAfRsb-4BLH81gT7|~DDwb(xD5dLegMuj@44#GdH_1ggQ@`d;nH-J03Zr|Ct&;EP&wwn%Znb)2De;!;OJaeCP>1qFb`z&qb zg)8@-^X`%LZ){q!ck-1BpXgZqbmESl&P`|DK7V@ADIfoI-Z{G~r|l^uJHXQD`dFJrr zYaZS8?4R>VcFlroKUneXvSrhEzG~gOYt5<*A7mc6YOA>XZxXv~Xw}p^pRE1e-k*Ri zrS~5^-uH*Y_pQ)xy78qiuK-iITc3L1-+S`XlSgL0{?D<$Ja+2N$%Dtaf!HnY9TInZ z)bZ|<2bV7FeDvz;xy2v6_Sfs5(R#+um`?6F@bIGFb{so;PtTRY>VL&{pML1-rJp4h p2EWO5KELuJFpt6C3+_AuIgtw=ow{Om$WDZhO1a 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 Lg-7S^{K9_#hzGN8 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Fun/spraycans.rsi/clown-inhand-right.png b/Resources/Textures/Objects/Fun/spraycans.rsi/clown-inhand-right.png new file mode 100644 index 0000000000000000000000000000000000000000..27b68e2cfe5d2b364b379707b887bdfcb115eeb8 GIT binary patch literal 20783 zcmeI4Z-^vE6~OzF5cJ|fFa!@j#9@LEHMLWJ`d^K^lkDx@c}Lvj*lfsMz~ri{s(0Jl zou16h-R_++UepMRD42Xu6Uc!V@effWLBU*%ND`2!1VM;?2+{cAA^|~<12K5Mp6;3I zot?ew%&nS0FYIn@_j|9Zf3ND*d)3Q)asRC!+p+!q+bzr5v2X9}Jp8^n`F_`x@bi1o zy|2KpcenN)>{!-S*CgNDtZzQL)3R=RJX$!=Juvt2x{OyQgo*<_v9{6zY0H|tX{{yX zVco3+dNFEFRsZ(%v(-wZrmA;3b9k;bqnDz+$J_e$1-*{QEhReN^p&`vJbs+FEd_wZD8I_a=-U~Ye9CT{DBGvOkMU9aNt3F3I3?cG?R zm^d}eY78UF>Nc+1w32;Q8{45Z*;ZkFes)jR9sD&_UFvpQ^;&Ipb#-EuO~mcR8sVJR zFs)IFKmv7+HM?RBH9Oa*M*4PUbw{?NRyT^9mBg+H;v?OuYBlL-@RdE+N^8(jvy-s{ zinTS-s*wp?8>OV=K&N%2z1$m`k~O`oS9G)60X;HOueB6+fJ}-QLZ2L^qua!Hr^><3dBbm>PF!v zc9J`rLeOi`CThu@RIN9sQdds`wE9e2i*DRrh~wo(I$ihoeNma2=}phd4R=OO6|Z(a zG+7%~F%!pF)3c(h8x+G_Bp6}3K%6>r>csm9CUuN+MpDTk8*{OW!ef0K3=xW`y+GVL zV|9n!m8$22hSgyWD$x}eGmsa{nNYGG#_bi+ZA2?#QP*0{#mSnvn3Eh#%KA*a9JgUa zy1^!Eqb`rsnvLa=WK~QzrS55bYC8ATxpgreBi7>^6Y@dM(!6eI1~_STNtQ%tdd2rw<3jI{V*=qyYK>hAthKkbqvl$%FaWE8=0Rp!6M_Oy!Lv}tLuXt!N@6>X?@HK# zSq4N<*GAx7rJ2i_W_nB@ib)YLC6O;enT}f6QXt=9lu{xwJj}SJUCIT~A+T22lG&Ld<$`1({cPr4Lb{%dD z#5|D&RoDeE;=bSx1YBPWHRcQnDWS}V8DhHtycbjj4uMaIwZoXCUNE+4hr&h$;Ryzq zE%diO>ct3hqjYQRH6io}U$`z}jvFF7uwg^QK0`iHTEK4U+y2Ej0Z5)E6l}JRYYXXf zja=phh`Bawu~Ill!CoS5B4nt;i*oH73j!QS6%r&u&5?5K5Vq<7oa6>z%(t~qoWjOJ zhbV4i7tz2M$PutB!S3lJm%376=}V9E;>LszIiMuS1vMC6GDkg|ppao2N=yMK zg5Iw0U4PZ2puG~ZCE6XG6i8FmeuXrsIn#VI%Q}07R$a)X_-x3e?9i~p*IhNk8fZ?Y z0?|fy7DKQ>ee*t6mvr+&WlL(RC$+heP8@buMO#lVLTX>6qDrl-7ao@-;|OXVOJLcC+U0r8aAcXQ5*!;hFeOb#$qj|^dwRFBEAhz(_5 z?(XKw>p2!EXp7)CW0Omwxu{j6)*nv)!tjXR&K~nUAF8;JqJVN6c^Ec$UqMdn!|vm; zAe20W?I#5@hR4_f#`VN$q;y($5bxsVQQdwk5f9?3fb&ER=K^6nUa1U@BP!jvB3iBG zNQxws){Zt+b~-mG;``pq+4vt$P^H_^TL~(;QOZ81$~)|tFFB0D34o%Kg$U9T2RWe) zHLy)Q1jU%^bBE&)!`Vo=Cb5wvm1jRq3-4Y_Ha6%7Su19@h zhi(Y9*H}*5aO#;_`F~^|Z?Eld5F+|uvy5^$z?o6!;!7{8TqJlV@pTYv`<>)$40rTj z-0HV3lD>1E`H~l9u`;h*lvyNAr|kZOD)dRUPhQPz4iStYjEdk@yX<6 zU>&M^s5E=2U<1x;H(bkE>)kt$RO!n{dR4ebF?`iX@-tJ--yT*ycwyE2?O|1;c}Fwq zHsIAmZ!klbb6Uw8%k;(C?eLafH(bYIaE>*6d01nAw5FF2%tr8Ht&^NHxNs`h(;2=o zQt{wsin$w^V!CW{@6&sZNw31&diY0y+TcG0ay~QZ%a*sy6K%O^W3cSRuqev`SnBAQ_Pomtj$$RpKfD$%vG=42uG-5?298Mx?}LSQKcLxC%frA|)=vqCl&} zRREF^DRCJV1zIJp0+5VIiOaAk&?<2ifMi5UT!uw~R*9+U4^cBdG6fF zQ`3LndFQteJ#gcRTb91de(|LB-~$!)=1sp^`OAI$vFc+#d-|zIzhPgOd=%iiueJBy zaV_hx!`DD*&Ke(IsiPCeXx;|o7|r>D>E+xE#9U*7TjGl%Z3e7O4O>ijL&o{2tv!z({*qu+k@>UH+l|MBp1zdjwE zc=R7HzINu>%f1sW|NDvSPCjw@Q=i!O{og!u|F+f7eColg-#mWszO!Gxr}n~YX9v0` ie?JNB^XD(OKJepnS6;F36_MQ9xBJ%FAM85x+5Z4lF0`ir literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Fun/spraycans.rsi/meta.json b/Resources/Textures/Objects/Fun/spraycans.rsi/meta.json index 0f883ee2801..f34820cec45 100644 --- a/Resources/Textures/Objects/Fun/spraycans.rsi/meta.json +++ b/Resources/Textures/Objects/Fun/spraycans.rsi/meta.json @@ -58,9 +58,6 @@ { "name": "spray" }, - { - "name": "spray_cap" - }, { "name": "spray_cap_colors" }, @@ -70,6 +67,22 @@ { "name": "equipped-BELT", "directions": 4 + }, + { + "name": "clown-inhand-right", + "directions": 4 + }, + { + "name": "clown-inhand-left", + "directions": 4 + }, + { + "name": "spray-inhand-right", + "directions": 4 + }, + { + "name": "spray-inhand-left", + "directions": 4 } ] } diff --git a/Resources/Textures/Objects/Fun/spraycans.rsi/spray-inhand-left.png b/Resources/Textures/Objects/Fun/spraycans.rsi/spray-inhand-left.png new file mode 100644 index 0000000000000000000000000000000000000000..ad3ad959de4138922ea09c84316ad9369fedf403 GIT binary patch literal 21199 zcmeI4eT*bU6~G%61ILFZ5K;bdTr`SUJN3~YRm1GS-tL`u35)0667CKYuDYsvuVr_7 zn3=oXJBb1O!)S=2Mx!SZC4MAEi5mFBaK=P}e+c}c5s4o$l3-MVL5+!GqWF4xW~ygy z_O6-RC?q|}?$&m{_p18$>f^mPllj=b8{d1$jw^RKj&sT0J&Vim_xkkbg4e_EZ^pO( z7XDn=+H)H5wvKAci@W>X!GCizN4nisty(^2iX)s2&8CABNV z6sw!ERVA~yI!Q^Zp-$^)d#yJ#t*U0ttea-H1A1hlUh7EGO*%)CExH`u-m+ux$7;) zVJE$_B?Pk>Z=sgn$<%stDsy!mpw$-IMs}0-N|LNKvgx{S;ET$_LT`Fj-gR@_)X7HY z>Uwot#ZDY+%`D2UX;2Jvkzj=B3h`>ptC8RuOllbCjAW8yHkJ|{N5=;?7$Ou=u|oVB zp*6w}W$HPhadlXOT6X2x4CKXfCbX(WNqb#(8}YhaHPu#gwO+LsbCSbJSzAcfk~WOU zG+4bl>GDLa#Y7!VSH*l&nGU$1lXDF?sJHHS%GJ@UgBxpcgY4!zcM6Z~a+%;e7sx!_ zIq!SEhv(;oPj~ZrHC^E#XnXJby?;I1wsfLmU9+RcK9`=N24NF&M2Im+a_S=?IK@n0 z#)LbqX{Ds0U`zQN<_0kug&2M*-(Vt`%dt>Xnq~nDa3lmWzKSx_TzMW+t}vWN+NU&} z(zGk7#*s8gxIm>hY929)A`(hXv1>4%7Kq0~!hMR730!x3M;&OUSx6+9R$iaA0X{0= zfuuB&jL<282(664J_Xh$*xpeKqgfP!)zFBrZ`u%o!a&2bP$nX8S~n`J^x zp;j(3A=1B#?)T*)<$2$}XMFp=>+;Y!2; z+22&*6u?MuDLmM4xsiIx84^)K8HX9-`T)GQsY)CHp9mX|F-3!LYSSKtg9^4M7+`;) zzx+|J#t<82M`N!ETaV<@_Yw2_2)UsP2O{PSaiWcc(~`UV?1umZPqP{hThDi;;=&-G z1tDU-3rDPy9@21@D3?eTndq!s2gbq>he}5T$;b$#JvV}*Is_;AAsFMX;lwL!Eb@pJ zF7^=(xkR3XQwdH_j(qAXjTKjc5XFs24l$r2$OknTUOGnum!ODY8YxTxC&WvWf|Ts) zXz-Jdtj-iVrE%!eQf+Uu)0Gfe?Okl7q+-Ah@r&{`48`|!fUtB!Bp6jFfOs+T!yr)F z??soR>Nb4pn;!HIrnd;8VE5#a2ABALZw7*Yg@!nX7&r6{_&WlM**J5?PV4A>?HCeM zxt5a|cs)?y4CQGQ1rbaf=6Wb}!Syh$m`j*zL?8!UOfYj=S9{_#1U)_)EA~Rr{pR-3 zt2PAfrI0Pz?wB+|nwuG9NW+{ni#PjGXD`vJNtGs_4V9)H2A259RXePqW<3*#Pj_cA z1gEKQ-N(ie(>zn!l9}pBZEd8JMBNS9HuI|x+84>FGOMF%@c> zU_KX~z!8SpNVzD7fk9RvTZ_*j2svk`u|RPG6(&R#WL!LiKwd~6PC*rTjC)}erM0rD zoR(HS-L&e-+!~|hPP&ad=~*_ml2&EFnZJ$Po-{Oxg7t9es1?l$24qTXP}F6Xq;x0$ndL-)f+D@?=pJO;(b@yp{H z`{GTrc3?4vcSAdAama_Dsi!l3W1`~0trT-N`ij}I$$bc-=a}p&e8d7i-BBHWyd&o` zyHjp^%RbSzo3;kaPArQ8trAxONLHl8WmyzxmADE(vLYoe%c4N5#8m*26)AC976n=* zt^$y(NQuj`D9|c#6@X+#N?evjfmVsD03<6?;<79Xv`SnBAX$+Tmt|3)RpKfD$%>S? zEQ)qoTJ!ro;&O~wa+`w70IV>eQY<#zOZ+3er5Al&;RnfmpqCu zUi|6)d*4Fux$?-Tp*^Ug1S?#}!7{0M*M3s<-6 zm)Aji^mTaopZYrfLI}hLeU!MjguK)l5 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Fun/spraycans.rsi/spray-inhand-right.png b/Resources/Textures/Objects/Fun/spraycans.rsi/spray-inhand-right.png new file mode 100644 index 0000000000000000000000000000000000000000..353e47c56fa71b948a6443d4a3a850de2edcb5c0 GIT binary patch literal 21213 zcmeI4eTXDS6~H@+A31Y=Bpw$PbeM2*hpCB-F8?c9PG4T>RXf(VA-UxpxvANYqNMu;M(e~3XeL{A}qm_NjeC>s1DVmMz<&rJ2q z&E9q97EPcRcDJ_sy;s%0SM}<>>R}%~cK1j3?s@+n%d++^9bGsMzwb)E-~Lwk`OWBo z7vR^;jiYB;mi3O?lJA?WC%$&OW!-x=S~=Z5z5Ma2j5nu*iUU2>*=&HcWlbOMG=yB& z?NXpuqxwww?@vEdE=6jle9BqI%Z+)x79HJg>J!^{ugL9n$yNDqZO?S43I;ZHTa-GR z8}(MTGgI!`twKAwTq&2jBJK5=@@&#!>Gblk(tO<1C1=V-61!f><5R@(JlngoL@{wH zm{k}?lvQn9wP~sMRj%!U)^t;a)#D3`z3$+znetk@-KbV7TU%RGTWl(Bu2u-=yn<;mSBAB?mfpzpEc z-P|18b*p{kJRs;H9jn<|xzNy+-a+m_~v4+}FxPzVK z&W;dtC)zLZj-i29;=w>lw(3WlShp4ddpfXxE}mv8pSL`s#GWT+B!gCS`R#-iVtpB3)zC zm2sEHYAwX_T(T-=>r%JC1+9#0z(L)0KPOg)ul8?jL^ZPL&&_d%9kChr=jO>Qott$X z*TJ*1+@*{DbR}8gAZU8;`Mq~N4O=Rfk*b18nh+Ft3Z8{B9y*h{Q4-s6d{@E_%z8is zb!`ORRhqe+X{ILxqL>r`Qxf?ilUnQ+t^qK5B^E|8w--2$Sy6t;pX&20zwX2>qBMlE!p zB+L+C-8a2UF+?j{QQ?Ql4B2fUno$ziz&dn#GeiiFBH?IT_{t9nnRF}Ap>`c^3&cFp z3#zaSV8ngF9SFF-7HYy75>i5$4>QDe0eCm43LFBT5Nn4qNxfiV(+-7=3c?c%u(!}( z`>0nV$c@sivD<{uBYfeyh&gVE?7)T%5&I1JL}>xLrEmM!-vl6eno_XYI<76G&oy$H z7a-=^u*FK@AO(Agw26?R4zJ6#Z!8FKAXP|^2sKB_u|wFZ18|ZXfHB|JK5=pz3mu}k zja@_oUm!=ot^~WMk6h|Xg{3b&&hr}+KIDLsAQ#kNc*z{~Y=T0DX(%xToRBY#OH$IS z!^uxJv$|I5l;oky%hjcNt1Te2T3Vgs5eq@WjubH#27dO|-i zeAT3&-4e1Pnk}6aNHgVrg*2!+(|og+b#@D_s*p+XS(8cGp<#&+T{XiRXildB(bewE zhu|vpo%`5Y)AehWEvc!l)Xqj)aoFAxO+C8`sePV`Dz!SSHfnzSD%llBw8Z%va|4sG zaD>khbs*|^u8ol|m4jRe@s8~V#8Y11%^5F_K4S7P8Ng6IGK5J|JtoH@Hk5styE`in zbIeiD=D}~qCf7uLRjXR1Kb-!B(Gk6#J?48pRB<6i0p&LGFvLM$K~C($?&Gl_lsts( zCj~Qs$Jh$S)x>F}a9Vp1@8bG--Fz((58|qT^F$5j0%0@WEDeq$O6|BL8jX!eiX@a) z&ev7%bZ$_@_q~^~@jslPN;jj|5>zsylzmL4H`p`Za2SOX07WGW5gda!$O&zzfoS-sZo{Uu;Rz%WWjHK5Io=JQi1lzv%GBN568A;zb&wRs+GUS+JsK1cs!Ams_ z@f6OrC=^MA6YjZ^1_}=PibeUlilq5!^NF1{^!_Nd^8cvgcCC;|%ZuUCykG8LUCQ^* zEz-PfG07&AQx}M7*Qwm0B7;|y)l3o4KTFBG^Npwv#m=5})q4-N<8ey9iUx`Zn|ge6&K>T*qN>_&9ob zRO49G(Ho~1B6u6xN)CryI7;p6jNTZlcxETX%#EI6x@DgU2mBu+I7>$V9|+Tk)u`M$^prU6u1nF9IXOZ4oF6%z-3tEXcf3}Kr$i)F2f>6 ztH6~5k`XCz85TKO1+E;Bj7WjYu*lIWaOHqxL<(GnMUGa1D+eSaQs6Qyao!y-qkz?B1%5h-vP7CBl4 zt{jkzNP)|+$k8fr<$z>F3S5Roj#hyy2P7j>;4&<7v_V9D?uM|u2 z=<#LC+CE@e4?bjBuUvuOf3d7{*s?C)Z&}sHEo*=L7@gIqE8+?`J?wf_LBpPSDtRY^kPl@^vvbI-OBgh z{6SD3{_5Ozavy84M{a)X_&=Y2;?7_G=HjoGp1rhn_)R~H=tx2X7U8A;nEvG=NTxeNJV7FiDb@}tVsWL9Qxep_<$e?f*|}G z&bh+e{o=ud@ZRr?zG+8zGDBw!FAJcx-keiP)H~D{7-J4;N1z(WIb#@xoyI^lur_6l zEA$QkK;QQr04&QwFXX8Pu7GMlDFpzCF_urQ6{>-Gp5La`xN5D?I}k!(M>pKQ0}`NP whMaT7TD#GvX~H;;&F@hGYKZljF&6;f1$#d@3sMJ{VE_OC07*qoM6N<$f-h%ecmMzZ