Skip to content

Commit

Permalink
Mirror: Spray Paint (Review Ready) (#258)
Browse files Browse the repository at this point in the history
## Mirror of PR #23003: [Spray Paint (Review
Ready)](space-wizards/space-station-14#23003)
from <img src="https://avatars.githubusercontent.com/u/10567778?v=4"
alt="space-wizards" width="22"/>
[space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14)

###### `e4d5e7f1aebfc37b1bc3453fdb39578f3897b6a1`

PR opened by <img
src="https://avatars.githubusercontent.com/u/113240905?v=4"
width="16"/><a href="https://github.com/brainfood1183">
brainfood1183</a> 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


---

<details open="true"><summary><h1>Original Body</h1></summary>

> 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**
> 🆑
> - add: Added Spray Paints.
> 
> 


</details>

Co-authored-by: SimpleStation14 <Unknown>
  • Loading branch information
SimpleStation14 committed May 12, 2024
1 parent c51af1b commit 763da78
Show file tree
Hide file tree
Showing 41 changed files with 1,008 additions and 11 deletions.
120 changes: 120 additions & 0 deletions Content.Client/Paint/PaintVisualizerSystem.cs
Original file line number Diff line number Diff line change
@@ -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<PaintedComponent>
{
/// <summary>
/// Visualizer for Paint which applies a shader and colors the entity.
/// </summary>

[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<PaintedComponent, HeldVisualsUpdatedEvent>(OnHeldVisualsUpdated);
SubscribeLocalEvent<PaintedComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<PaintedComponent, EquipmentVisualsUpdatedEvent>(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<ShaderPrototype>(component.ShaderName).Instance();

if (args.Sprite == null)
return;

if (!_appearance.TryGetData<bool>(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<ShaderPrototype>(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;
}
}
}
}
}
}
180 changes: 180 additions & 0 deletions Content.Server/Paint/PaintSystem.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Colors target and consumes reagent on each color success.
/// </summary>
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<PaintComponent, AfterInteractEvent>(OnInteract);
SubscribeLocalEvent<PaintComponent, PaintDoAfterEvent>(OnPaint);
SubscribeLocalEvent<PaintComponent, GetVerbsEvent<UtilityVerb>>(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<UtilityVerb> 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<PaintComponent> 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<PaintedComponent>(target) || HasComp<RandomSpriteComponent>(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<HumanoidAppearanceComponent>(target) || HasComp<SubFloorHideComponent>(target))
{
_popup.PopupEntity(Loc.GetString("paint-failure", ("target", args.Target)), args.User, args.User, PopupType.Medium);
return;
}


if (TryPaint(entity, target))
{
EnsureComp<PaintedComponent>(target, out PaintedComponent? paint);
EnsureComp<AppearanceComponent>(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<InventoryComponent>(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<PaintedComponent>(slotEnt.Value) || !entity.Comp.Blacklist?.IsValid(slotEnt.Value, EntityManager) != true
|| HasComp<RandomSpriteComponent>(slotEnt.Value) || HasComp<HumanoidAppearanceComponent>(slotEnt.Value))
return;

EnsureComp<PaintedComponent>(slotEnt.Value, out PaintedComponent? slotpaint);
EnsureComp<AppearanceComponent>(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<PaintComponent> reagent, EntityUid target)
{
if (HasComp<HumanoidAppearanceComponent>(target) || HasComp<SubFloorHideComponent>(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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}
}
Expand Down
60 changes: 60 additions & 0 deletions Content.Shared/Paint/PaintComponent.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Entity when used on another entity will paint target entity.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedPaintSystem))]
public sealed partial class PaintComponent : Component
{
/// <summary>
/// Noise made when paint applied.
/// </summary>
[DataField]
public SoundSpecifier Spray = new SoundPathSpecifier("/Audio/Effects/spray2.ogg");

/// <summary>
/// Solution on the entity that contains the paint.
/// </summary>
[DataField]
public string Solution = "drink";

/// <summary>
/// How long the doafter will take.
/// </summary>
[DataField]
public int Delay = 2;

/// <summary>
/// Reagent that will be used as paint.
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<ReagentPrototype> Reagent = "SpaceGlue";

/// <summary>
/// Color that the painting entity will instruct the painted entity to be.
/// </summary>
[DataField, AutoNetworkedField]
public Color Color = Color.FromHex("#c62121");

[DataField, ViewVariables(VVAccess.ReadWrite)]
public EntityWhitelist? Blacklist;
/// <summary>
/// Reagent consumption per use.
/// </summary>
[DataField]
public FixedPoint2 ConsumptionUnit = FixedPoint2.New(5);

/// <summary>
/// Duration per unit
/// </summary>
[DataField]
public TimeSpan DurationPerUnit = TimeSpan.FromSeconds(6);
}
9 changes: 9 additions & 0 deletions Content.Shared/Paint/PaintDoAfterEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;

namespace Content.Shared.Paint;

[Serializable, NetSerializable]
public sealed partial class PaintDoAfterEvent : SimpleDoAfterEvent
{
}
Loading

0 comments on commit 763da78

Please sign in to comment.