-
Notifications
You must be signed in to change notification settings - Fork 137
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Mirror: Spray Paint (Review Ready) (#258)
## 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
1 parent
c51af1b
commit 763da78
Showing
41 changed files
with
1,008 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
{ | ||
} |
Oops, something went wrong.