Skip to content

Commit

Permalink
The Return of Spray Paint (#1222)
Browse files Browse the repository at this point in the history
# Description

Fixes some issues described in
space-wizards/space-station-14#26257 and does
some other things.
This PR's changes are licensed under MIT.

---

# Changelog

:cl:
- add: Added spray-painting back

---------

Co-authored-by: metalgearsloth <[email protected]>
Co-authored-by: VMSolidus <[email protected]>
  • Loading branch information
3 people authored Nov 15, 2024
1 parent 8739096 commit 3423e93
Show file tree
Hide file tree
Showing 44 changed files with 1,030 additions and 52 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ csharp_style_expression_bodied_constructors = false:suggestion
#csharp_style_expression_bodied_indexers = true:silent
#csharp_style_expression_bodied_lambdas = true:silent
#csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:suggestion
csharp_style_expression_bodied_methods = true:suggestion
#csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:suggestion

Expand Down
101 changes: 101 additions & 0 deletions Content.Client/Paint/PaintVisualizerSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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>
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;


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)
{
if (args.Sprite == null
|| !_appearance.TryGetData(uid, PaintVisuals.Painted, out bool isPainted))
return;

var shader = _protoMan.Index<ShaderPrototype>(component.ShaderName).Instance();
foreach (var spriteLayer in args.Sprite.AllLayers)
{
if (spriteLayer is not Layer layer)
continue;

if (layer.Shader == null || layer.Shader == shader)
{
layer.Shader = shader;
layer.Color = component.Color;
}
}
}

private void OnShutdown(EntityUid uid, PaintedComponent component, ref ComponentShutdown args)
{
if (!TryComp(uid, out SpriteComponent? sprite))
return;
component.BeforeColor = sprite.Color;

if (Terminating(uid))
return;

foreach (var spriteLayer in sprite.AllLayers)
{
if (spriteLayer is not Layer layer
|| layer.Shader != _protoMan.Index<ShaderPrototype>(component.ShaderName).Instance())
continue;

layer.Shader = null;
if (layer.Color == component.Color)
layer.Color = component.BeforeColor;
}
}

private void OnHeldVisualsUpdated(EntityUid uid, PaintedComponent component, HeldVisualsUpdatedEvent args) =>
UpdateVisuals(component, args);
private void OnEquipmentVisualsUpdated(EntityUid uid, PaintedComponent component, EquipmentVisualsUpdatedEvent args) =>
UpdateVisuals(component, args);
private void UpdateVisuals(PaintedComponent component, EntityEventArgs args)
{
var layers = new HashSet<string>();
var entity = EntityUid.Invalid;

switch (args)
{
case HeldVisualsUpdatedEvent hgs:
layers = hgs.RevealedLayers;
entity = hgs.User;
break;
case EquipmentVisualsUpdatedEvent eqs:
layers = eqs.RevealedLayers;
entity = eqs.Equipee;
break;
}

if (layers.Count == 0 || !TryComp(entity, out SpriteComponent? sprite))
return;

foreach (var revealed in layers)
{
if (!sprite.LayerMapTryGet(revealed, out var layer))
continue;

sprite.LayerSetShader(layer, component.ShaderName);
sprite.LayerSetColor(layer, component.Color);
}
}
}
202 changes: 202 additions & 0 deletions Content.Server/Paint/PaintSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
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;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;

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
|| 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) =>
_doAfterSystem.TryStartDoAfter(
new DoAfterArgs(
EntityManager,
user,
component.Delay,
new PaintDoAfterEvent(),
uid,
target: target,
used: uid)
{
BreakOnUserMove = true,
BreakOnTargetMove = true,
NeedHand = true,
BreakOnHandChange = true,
});

private void OnPaint(Entity<PaintComponent> entity, ref PaintDoAfterEvent args)
{
if (args.Target == null || args.Used == null || args.Handled || args.Cancelled || args.Target is not { Valid: true } target)
return;

Paint(entity, target, args.User);
args.Handled = true;
}

public void Paint(Entity<PaintComponent> entity, EntityUid target, EntityUid user)
{
if (!_openable.IsOpen(entity))
{
_popup.PopupEntity(Loc.GetString("paint-closed", ("used", entity)), user, user, PopupType.Medium);
return;
}

if (HasComp<PaintedComponent>(target) || HasComp<RandomSpriteComponent>(target))
{
_popup.PopupEntity(Loc.GetString("paint-failure-painted", ("target", target)), user, user, PopupType.Medium);
return;
}

if (!entity.Comp.Whitelist?.IsValid(target, EntityManager) == true
|| !entity.Comp.Blacklist?.IsValid(target, EntityManager) == false
|| HasComp<HumanoidAppearanceComponent>(target) || HasComp<SubFloorHideComponent>(target))
{
_popup.PopupEntity(Loc.GetString("paint-failure", ("target", target)), user, user, PopupType.Medium);
return;
}

if (CanPaint(entity, target))
{
EnsureComp<PaintedComponent>(target, out var paint);
EnsureComp<AppearanceComponent>(target);

paint.Color = entity.Comp.Color;
_audio.PlayPvs(entity.Comp.Spray, entity);
paint.Enabled = true;

// Paint any clothing the target is wearing
if (HasComp<InventoryComponent>(target)
&& _inventory.TryGetSlots(target, out var slotDefinitions))
foreach (var slot in slotDefinitions)
{
if (!_inventory.TryGetSlotEntity(target, slot.Name, out var slotEnt)
|| HasComp<PaintedComponent>(slotEnt.Value)
|| !entity.Comp.Whitelist?.IsValid(slotEnt.Value, EntityManager) != true
|| !entity.Comp.Blacklist?.IsValid(slotEnt.Value, EntityManager) != false
|| HasComp<RandomSpriteComponent>(slotEnt.Value)
|| HasComp<HumanoidAppearanceComponent>(slotEnt.Value))
continue;

EnsureComp<PaintedComponent>(slotEnt.Value, out var slotToPaint);
EnsureComp<AppearanceComponent>(slotEnt.Value);
slotToPaint.Color = entity.Comp.Color;
_appearanceSystem.SetData(slotEnt.Value, PaintVisuals.Painted, true);
Dirty(slotEnt.Value, slotToPaint);
}

_popup.PopupEntity(Loc.GetString("paint-success", ("target", target)), user, user, PopupType.Medium);
_appearanceSystem.SetData(target, PaintVisuals.Painted, true);
Dirty(target, paint);
return;
}

if (!CanPaint(entity, target))
_popup.PopupEntity(Loc.GetString("paint-empty", ("used", entity)), user, user, PopupType.Medium);
}

public void Paint(EntityWhitelist whitelist, EntityWhitelist blacklist, EntityUid target, Color color)
{
if (!whitelist.IsValid(target, EntityManager)
|| blacklist.IsValid(target, EntityManager))
return;

EnsureComp<PaintedComponent>(target, out var paint);
EnsureComp<AppearanceComponent>(target);

paint.Color = color;
paint.Enabled = true;

if (HasComp<InventoryComponent>(target)
&& _inventory.TryGetSlots(target, out var slotDefinitions))
foreach (var slot in slotDefinitions)
{
if (!_inventory.TryGetSlotEntity(target, slot.Name, out var slotEnt)
|| !whitelist.IsValid(slotEnt.Value, EntityManager)
|| blacklist.IsValid(slotEnt.Value, EntityManager))
continue;

EnsureComp<PaintedComponent>(slotEnt.Value, out var slotToPaint);
EnsureComp<AppearanceComponent>(slotEnt.Value);
slotToPaint.Color = color;
_appearanceSystem.SetData(slotEnt.Value, PaintVisuals.Painted, true);
Dirty(slotEnt.Value, slotToPaint);
}

_appearanceSystem.SetData(target, PaintVisuals.Painted, true);
Dirty(target, paint);
}

private bool CanPaint(Entity<PaintComponent> reagent, EntityUid target)
{
if (HasComp<HumanoidAppearanceComponent>(target)
|| HasComp<SubFloorHideComponent>(target)
|| !_solutionContainer.TryGetSolution(reagent.Owner, reagent.Comp.Solution, out _, out var solution))
return false;
var quantity = solution.RemoveReagent(reagent.Comp.Reagent, reagent.Comp.ConsumptionUnit);
return (quantity > 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,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
45 changes: 9 additions & 36 deletions Content.Shared/Clothing/ClothingEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,18 @@

namespace Content.Shared.Clothing;

/// <summary>
/// Raised directed at a piece of clothing to get the set of layers to show on the wearer's sprite
/// </summary>
public sealed class GetEquipmentVisualsEvent : EntityEventArgs
/// Raised directed at a piece of clothing to get the set of layers to show on the wearer's sprite
public sealed class GetEquipmentVisualsEvent(EntityUid equipee, string slot) : EntityEventArgs
{
/// <summary>
/// Entity that is wearing the item.
/// </summary>
public readonly EntityUid Equipee;

public readonly string Slot;

public readonly EntityUid Equipee = equipee;
public readonly string Slot = slot;
/// <summary>
/// The layers that will be added to the entity that is wearing this item.
/// </summary>
/// <remarks>
/// Note that the actual ordering of the layers depends on the order in which they are added to this list;
/// </remarks>
public List<(string, PrototypeLayerData)> Layers = new();

public GetEquipmentVisualsEvent(EntityUid equipee, string slot)
{
Equipee = equipee;
Slot = slot;
}
}

/// <summary>
Expand All @@ -37,26 +24,12 @@ public GetEquipmentVisualsEvent(EntityUid equipee, string slot)
/// <remarks>
/// Useful for systems/components that modify the visual layers that an item adds to a player. (e.g. RGB memes)
/// </remarks>
public sealed class EquipmentVisualsUpdatedEvent : EntityEventArgs
public sealed class EquipmentVisualsUpdatedEvent(EntityUid equipee, string slot, HashSet<string> revealedLayers) : EntityEventArgs
{
/// <summary>
/// Entity that is wearing the item.
/// </summary>
public readonly EntityUid Equipee;

public readonly string Slot;

/// <summary>
/// The layers that this item is now revealing.
/// </summary>
public HashSet<string> RevealedLayers;

public EquipmentVisualsUpdatedEvent(EntityUid equipee, string slot, HashSet<string> revealedLayers)
{
Equipee = equipee;
Slot = slot;
RevealedLayers = revealedLayers;
}
public readonly EntityUid Equipee = equipee;
public readonly string Slot = slot;
/// The layers that this item is now revealing.
public HashSet<string> RevealedLayers = revealedLayers;
}

public sealed partial class ToggleMaskEvent : InstantActionEvent { }
Expand Down
Loading

0 comments on commit 3423e93

Please sign in to comment.