Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The Return of Spray Paint #1222

Merged
merged 8 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions Content.Client/Paint/PaintVisualizerSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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)
{
var shader = _protoMan.Index<ShaderPrototype>(component.ShaderName).Instance();

if (args.Sprite == null)
return;

// What is this even doing? It's not even checking what the value is.
if (!_appearance.TryGetData(uid, PaintVisuals.Painted, out bool isPainted))
return;
DEATHB4DEFEAT marked this conversation as resolved.
Show resolved Hide resolved

var sprite = args.Sprite;

foreach (var spriteLayer in 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(uid, component, args);
private void OnEquipmentVisualsUpdated(EntityUid uid, PaintedComponent component, EquipmentVisualsUpdatedEvent args) =>
UpdateVisuals(uid, component, args);
private void UpdateVisuals(EntityUid uid, PaintedComponent component, EntityEventArgs args)
{
if (args is not EquipmentVisualsUpdatedEvent ags
|| ags.RevealedLayers.Count == 0
|| !TryComp(ags.Equipee, out SpriteComponent? sprite))
return;

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

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

if (args.Target is not { Valid: true } target)
return;
DEATHB4DEFEAT marked this conversation as resolved.
Show resolved Hide resolved

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)
{
BreakOnUserMove = true,
BreakOnTargetMove = true,
NeedHand = true,
BreakOnHandChange = true,
};

_doAfterSystem.TryStartDoAfter(doAfterEventArgs);
}
DEATHB4DEFEAT marked this conversation as resolved.
Show resolved Hide resolved

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.Blacklist?.IsValid(target, EntityManager) != true || 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; // 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 (HasComp<PaintedComponent>(slotEnt.Value)
|| !entity.Comp.Blacklist?.IsValid(slotEnt.Value, EntityManager) != true
|| 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);
}
}
}
DEATHB4DEFEAT marked this conversation as resolved.
Show resolved Hide resolved

_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 blacklist, EntityUid target, Color color)
{
if (blacklist.IsValid(target, EntityManager))
return;

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

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

if (HasComp<InventoryComponent>(target))
{
if (_inventory.TryGetSlots(target, out var slotDefinitions))
{
foreach (var slot in slotDefinitions)
{
if (!_inventory.TryGetSlotEntity(target, slot.Name, out var slotEnt)
|| 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);
}
}
}
DEATHB4DEFEAT marked this conversation as resolved.
Show resolved Hide resolved

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

private bool CanPaint(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;
}
DEATHB4DEFEAT marked this conversation as resolved.
Show resolved Hide resolved
}
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: 10 additions & 35 deletions Content.Shared/Clothing/ClothingEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,22 @@

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
public abstract class GetVisualsEvent(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;
}

/// 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) : GetVisualsEvent(equipee, 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 +28,10 @@ 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) : GetVisualsEvent(equipee, slot)
{
/// <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;
}
/// The layers that this item is now revealing.
public HashSet<string> RevealedLayers = revealedLayers;
}

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