diff --git a/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs b/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs
new file mode 100644
index 00000000000..cafb36ba62c
--- /dev/null
+++ b/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs
@@ -0,0 +1,30 @@
+/// Forge-Change-Start
+using Content.Shared.Atmos.Piping.Unary.Components;
+using Content.Shared.SprayPainter.Prototypes;
+using Robust.Client.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Atmos.EntitySystems;
+
+///
+/// Used to change the appearance of gas canisters.
+///
+public sealed partial class GasCanisterAppearanceSystem : VisualizerSystem
+{
+ [Dependency] private IPrototypeManager _prototypeManager = default!;
+ protected override void OnAppearanceChange(EntityUid uid, GasCanisterComponent component, ref AppearanceChangeEvent args)
+ {
+ if (!AppearanceSystem.TryGetData(uid, PaintableVisuals.Prototype, out var protoName, args.Component) || args.Sprite is not { } old)
+ return;
+
+ if (!_prototypeManager.HasIndex(protoName))
+ return;
+
+ // Create the given prototype and get its first layer.
+ var tempUid = Spawn(protoName);
+ SpriteSystem.LayerSetRsiState(uid, 0, SpriteSystem.LayerGetRsiState(tempUid, 0));
+ QueueDel(tempUid);
+ }
+}
+/// Forge-Change-End
+///
diff --git a/Content.Client/Doors/DoorSystem.cs b/Content.Client/Doors/DoorSystem.cs
index 21886d97fa3..860c855fd86 100644
--- a/Content.Client/Doors/DoorSystem.cs
+++ b/Content.Client/Doors/DoorSystem.cs
@@ -1,17 +1,18 @@
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
+using Content.Shared.SprayPainter.Prototypes; /// Forge-Chane
using Robust.Client.Animations;
using Robust.Client.GameObjects;
-using Robust.Client.ResourceManagement;
-using Robust.Shared.Serialization.TypeSerializers.Implementations;
+using Robust.Shared.Prototypes; /// Forge-Chane
namespace Content.Client.Doors;
public sealed partial class DoorSystem : SharedDoorSystem
{
[Dependency] private AnimationPlayerSystem _animationSystem = default!;
- [Dependency] private IResourceCache _resourceCache = default!;
+ // [Dependency] private IResourceCache _resourceCache = default!; /// Forge-Chane-Del
[Dependency] private SpriteSystem _sprite = default!;
+ [Dependency] private IPrototypeManager _prototypeManager = default!; /// Forge-Chane
public override void Initialize()
{
@@ -22,15 +23,15 @@ public override void Initialize()
protected override void OnComponentInit(Entity ent, ref ComponentInit args)
{
var comp = ent.Comp;
- comp.OpenSpriteStates = new List<(DoorVisualLayers, string)>(2);
- comp.ClosedSpriteStates = new List<(DoorVisualLayers, string)>(2);
+ comp.OpenSpriteStates = new List<(Enum, string)>(2); /// Forge-Chane
+ comp.ClosedSpriteStates = new List<(Enum, string)>(2); /// Forge-Chane
comp.OpenSpriteStates.Add((DoorVisualLayers.Base, comp.OpenSpriteState));
comp.ClosedSpriteStates.Add((DoorVisualLayers.Base, comp.ClosedSpriteState));
comp.OpeningAnimation = new Animation
{
- Length = TimeSpan.FromSeconds(comp.OpeningAnimationTime),
+ Length = comp.OpeningAnimationTime, /// Forge-Chane
AnimationTracks =
{
new AnimationTrackSpriteFlick
@@ -46,7 +47,7 @@ protected override void OnComponentInit(Entity ent, ref Component
comp.ClosingAnimation = new Animation
{
- Length = TimeSpan.FromSeconds(comp.ClosingAnimationTime),
+ Length = comp.ClosingAnimationTime, /// Forge-Chane
AnimationTracks =
{
new AnimationTrackSpriteFlick
@@ -62,7 +63,7 @@ protected override void OnComponentInit(Entity ent, ref Component
comp.EmaggingAnimation = new Animation
{
- Length = TimeSpan.FromSeconds(comp.EmaggingAnimationTime),
+ Length = comp.EmaggingAnimationTime, /// Forge-Chane
AnimationTracks =
{
new AnimationTrackSpriteFlick
@@ -85,11 +86,8 @@ private void OnAppearanceChange(Entity entity, ref AppearanceChan
if (!AppearanceSystem.TryGetData(entity, DoorVisuals.State, out var state, args.Component))
state = DoorState.Closed;
- if (AppearanceSystem.TryGetData(entity, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
- UpdateSpriteLayers((entity.Owner, args.Sprite), baseRsi);
-
- if (_animationSystem.HasRunningAnimation(entity, DoorComponent.AnimationKey))
- _animationSystem.Stop(entity.Owner, DoorComponent.AnimationKey);
+ if (AppearanceSystem.TryGetData(entity, PaintableVisuals.Prototype, out var prototype, args.Component)) /// Forge-Chane
+ UpdateSpriteLayers((entity.Owner, args.Sprite), prototype); /// Forge-Chane
// We are checking beforehand since some doors may not have an emagging visual layer, and we don't want LayerSetVisible to throw an error.
if (_sprite.TryGetLayer(entity.Owner, DoorVisualLayers.BaseEmagging, out var _, false))
@@ -105,54 +103,110 @@ private void UpdateAppearanceForDoorState(Entity entity, SpriteCo
switch (state)
{
case DoorState.Open:
+ /// Forge-Chane-Start
+ if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenKey))
+ return;
+
+ if (_animationSystem.HasRunningAnimation(entity, DoorComponent.CloseKey))
+ {
+ _animationSystem.Stop(entity, null, DoorComponent.CloseKey);
+ _animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.OpenKey);
+ }
+
foreach (var (layer, layerState) in entity.Comp.OpenSpriteStates)
{
+ // Allow animations to play while it's open (e.g., pinion);
+ // the animation unsets this so we gotta set it again.
+ _sprite.LayerSetAutoAnimated((entity.Owner, sprite), layer, true);
+ /// Forge-Chane-End
_sprite.LayerSetRsiState((entity.Owner, sprite), layer, layerState);
}
return;
case DoorState.Closed:
+ /// Forge-Chane-Start
+ if (_animationSystem.HasRunningAnimation(entity, DoorComponent.CloseKey))
+ return;
+
+ if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenKey))
+ {
+ _animationSystem.Stop(entity, null, DoorComponent.OpenKey);
+ _animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.CloseKey);
+ }
+ /// Forge-Chane-End
foreach (var (layer, layerState) in entity.Comp.ClosedSpriteStates)
{
+ _sprite.LayerSetAutoAnimated((entity.Owner, sprite), layer, true); /// Forge-Chane
_sprite.LayerSetRsiState((entity.Owner, sprite), layer, layerState);
}
return;
case DoorState.Opening:
- if (entity.Comp.OpeningAnimationTime == 0.0)
+ /// Forge-Chane-Start
+ if (entity.Comp.OpeningAnimationTime == TimeSpan.Zero)
return;
- _animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.AnimationKey);
+ if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenKey))
+ /// Forge-Chane-End
+ return;
+
+ _animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.OpenKey); /// Forge-Chane
return;
case DoorState.Closing:
- if (entity.Comp.ClosingAnimationTime == 0.0 || entity.Comp.CurrentlyCrushing.Count != 0)
+ /// Forge-Chane-Start
+ if (entity.Comp.ClosingAnimationTime == TimeSpan.Zero)
+ return;
+
+ if (_animationSystem.HasRunningAnimation(entity, DoorComponent.CloseKey))
+ /// Forge-Chane-End
return;
- _animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.AnimationKey);
+ _animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.CloseKey); /// Forge-Chane
return;
case DoorState.Denying:
- _animationSystem.Play(entity, (Animation)entity.Comp.DenyingAnimation, DoorComponent.AnimationKey);
+ /// Forge-Chane-Start
+ if (_animationSystem.HasRunningAnimation(entity, DoorComponent.DenyKey))
+ return;
+
+ _animationSystem.Play(entity, (Animation)entity.Comp.DenyingAnimation, DoorComponent.DenyKey);
+ /// Forge-Chane-End
return;
case DoorState.Emagging:
+ /// Forge-Chane-Start
+ if (_animationSystem.HasRunningAnimation(entity, DoorComponent.EmagKey))
+ return;
+ /// Forge-Chane-End
// We are checking beforehand since some doors may not have an emagging visual layer.
if (_sprite.TryGetLayer(entity.Owner, DoorVisualLayers.BaseEmagging, out var _, false))
- _animationSystem.Play(entity, (Animation)entity.Comp.EmaggingAnimation, DoorComponent.AnimationKey);
+ _animationSystem.Play(entity, (Animation)entity.Comp.EmaggingAnimation, DoorComponent.EmagKey); /// Forge-Chane
return;
}
}
- private void UpdateSpriteLayers(Entity sprite, string baseRsi)
+/// Forge-Chane-Start
+ private void UpdateSpriteLayers(Entity sprite, string targetProto)
{
- if (!_resourceCache.TryGetResource(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
+ if (!_prototypeManager.HasIndex(targetProto))
+ return;
+
+ // Spawn the target prototype client-side so we can copy its sprite base RSI.
+ var tempUid = Spawn(targetProto);
+
+ if (!TryComp(tempUid, out var targetSprite))
{
- Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
+ QueueDel(tempUid);
return;
}
- _sprite.SetBaseRsi(sprite.AsNullable(), res.RSI);
+ if (targetSprite.BaseRSI != null)
+ _sprite.SetBaseRsi(sprite.AsNullable(), targetSprite.BaseRSI);
+
+ QueueDel(tempUid);
}
}
+/// Forge-Chane-End
+///
diff --git a/Content.Client/SprayPainter/SprayPainterSystem.cs b/Content.Client/SprayPainter/SprayPainterSystem.cs
index a990acd8610..4f8d8c7e4c5 100644
--- a/Content.Client/SprayPainter/SprayPainterSystem.cs
+++ b/Content.Client/SprayPainter/SprayPainterSystem.cs
@@ -1,56 +1,133 @@
+/// Forge-Chane-Start
+using System.Linq;
+using Content.Client.Items;
+using Content.Client.Message;
+using Content.Client.Stylesheets;
+using Content.Shared.Decals;
using Content.Shared.SprayPainter;
-using Robust.Client.Graphics;
-using Robust.Client.ResourceManagement;
-using Robust.Shared.Serialization.TypeSerializers.Implementations;
+using Content.Shared.SprayPainter.Components;
+using Content.Shared.SprayPainter.Prototypes;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
using Robust.Shared.Utility;
-using System.Linq;
using Robust.Shared.Graphics;
namespace Content.Client.SprayPainter;
+///
+/// Client-side spray painter functions. Caches information for spray painter windows and updates the UI to reflect component state.
+///
public sealed partial class SprayPainterSystem : SharedSprayPainterSystem
{
- [Dependency] private IResourceCache _resourceCache = default!;
+ [Dependency] private UserInterfaceSystem _ui = default!;
+ [Dependency] private IPrototypeManager _protoMan = default!;
+
+ public List Decals = [];
+ public Dictionary> PaintableGroupsByCategory = new();
+ public Dictionary> PaintableStylesByGroup = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
- public List Entries { get; private set; } = new();
+ Subs.ItemStatus(ent => new StatusControl(ent));
+ SubscribeLocalEvent(OnStateUpdate);
+ SubscribeLocalEvent(OnPrototypesReloaded);
- protected override void CacheStyles()
+ CachePrototypes();
+ }
+
+ private void OnStateUpdate(Entity ent, ref AfterAutoHandleStateEvent args)
{
- base.CacheStyles();
+ UpdateUi(ent);
+ }
- Entries.Clear();
- foreach (var style in Styles)
+ protected override void UpdateUi(Entity ent)
+ {
+ if (_ui.TryGetOpenUi(ent.Owner, SprayPainterUiKey.Key, out var bui))
+ bui.Update();
+ }
+
+ private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
+ {
+ if (!args.WasModified() || !args.WasModified() || !args.WasModified())
+ return;
+
+ CachePrototypes();
+ }
+
+ private void CachePrototypes()
+ {
+ PaintableGroupsByCategory.Clear();
+ PaintableStylesByGroup.Clear();
+ foreach (var category in _protoMan.EnumeratePrototypes().OrderBy(x => x.ID))
{
- var name = style.Name;
- string? iconPath = Groups
- .FindAll(x => x.StylePaths.ContainsKey(name))?
- .MaxBy(x => x.IconPriority)?.StylePaths[name];
- if (iconPath == null)
+ var groupList = new List();
+ foreach (var groupId in category.Groups)
{
- Entries.Add(new SprayPainterEntry(name, null));
- continue;
+ if (!_protoMan.Resolve(groupId, out var group))
+ continue;
+
+ groupList.Add(groupId);
+ PaintableStylesByGroup[groupId] = group.Styles;
}
- RSIResource doorRsi = _resourceCache.GetResource(SpriteSpecifierSerializer.TextureRoot / new ResPath(iconPath));
- if (!doorRsi.RSI.TryGetState("closed", out var icon))
- {
- Entries.Add(new SprayPainterEntry(name, null));
+ if (groupList.Count > 0)
+ PaintableGroupsByCategory[category.ID] = groupList;
+ }
+
+ Decals.Clear();
+ foreach (var decalPrototype in _protoMan.EnumeratePrototypes().OrderBy(x => x.ID))
+ {
+ if (!decalPrototype.Tags.Contains("station")
+ && !decalPrototype.Tags.Contains("markings")
+ || decalPrototype.Tags.Contains("dirty"))
continue;
- }
- Entries.Add(new SprayPainterEntry(name, icon.Frame0));
+ Decals.Add(new SprayPainterDecalEntry(decalPrototype.ID, decalPrototype.Sprite));
}
}
-}
-public sealed class SprayPainterEntry
-{
- public string Name;
- public Texture? Icon;
-
- public SprayPainterEntry(string name, Texture? icon)
+ private sealed class StatusControl : Control
{
- Name = name;
- Icon = icon;
+ private readonly RichTextLabel _label;
+ private readonly Entity _entity;
+ private DecalPaintMode? _lastPaintingDecals = null;
+
+ public StatusControl(Entity ent)
+ {
+ _entity = ent;
+ _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
+ AddChild(_label);
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ if (_entity.Comp.DecalMode == _lastPaintingDecals)
+ return;
+
+ _lastPaintingDecals = _entity.Comp.DecalMode;
+
+ string modeLocString = _entity.Comp.DecalMode switch
+ {
+ DecalPaintMode.Add => "spray-painter-item-status-add",
+ DecalPaintMode.Remove => "spray-painter-item-status-remove",
+ _ => "spray-painter-item-status-off"
+ };
+
+ _label.SetMarkupPermissive(Robust.Shared.Localization.Loc.GetString("spray-painter-item-status-label",
+ ("mode", Robust.Shared.Localization.Loc.GetString(modeLocString))));
+ }
}
}
+
+///
+/// A spray paintable decal, mapped by ID.
+///
+public sealed record SprayPainterDecalEntry(string Name, SpriteSpecifier Sprite);
+/// Forge-Chane-End
diff --git a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs
index 7d6a6cf2a5a..9896a1b8ba7 100644
--- a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs
+++ b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs
@@ -1,42 +1,106 @@
+/// Forge-Chane-Start
+using Content.Shared.Decals;
using Content.Shared.SprayPainter;
using Content.Shared.SprayPainter.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
namespace Content.Client.SprayPainter.UI;
-public sealed class SprayPainterBoundUserInterface : BoundUserInterface
+///
+/// A BUI for a spray painter. Allows selecting pipe colours, decals, and paintable object types sorted by category.
+///
+public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private SprayPainterWindow? _window;
- public SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ protected override void Open()
{
+ base.Open();
+
+ if (_window == null)
+ {
+ _window = this.CreateWindow();
+
+ _window.OnSpritePicked += OnSpritePicked;
+ _window.OnSetPipeColor += OnSetPipeColor;
+ _window.OnTabChanged += OnTabChanged;
+ _window.OnDecalChanged += OnDecalChanged;
+ _window.OnDecalColorChanged += OnDecalColorChanged;
+ _window.OnDecalAngleChanged += OnDecalAngleChanged;
+ _window.OnDecalSnapChanged += OnDecalSnapChanged;
+ _window.OnDecalColorPickerToggled += OnDecalColorPickerToggled;
+ }
+
+ var sprayPainter = EntMan.System();
+ _window.PopulateCategories(sprayPainter.PaintableStylesByGroup, sprayPainter.PaintableGroupsByCategory, sprayPainter.Decals);
+ Update();
+
+ if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainterComp))
+ _window.SetSelectedTab(sprayPainterComp.SelectedTab);
}
- protected override void Open()
+ public override void Update()
{
- base.Open();
+ if (_window == null)
+ return;
- _window = this.CreateWindow();
+ if (!EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainter))
+ return;
- _window.OnSpritePicked = OnSpritePicked;
- _window.OnColorPicked = OnColorPicked;
+ _window.PopulateColors(sprayPainter.ColorPalette);
+ if (sprayPainter.PickedColor != null)
+ _window.SelectColor(sprayPainter.PickedColor);
+ _window.SetSelectedStyles(sprayPainter.StylesByGroup);
+ _window.SetSelectedDecal(sprayPainter.SelectedDecal);
+ _window.SetDecalAngle(sprayPainter.SelectedDecalAngle);
+ _window.SetDecalColor(sprayPainter.SelectedDecalColor);
+ _window.SetDecalSnap(sprayPainter.SnapDecals);
+ _window.SetDecalColorPicker(sprayPainter.ColorPickerEnabled);
+ }
- if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? comp))
- {
- _window.Populate(EntMan.System().Entries, comp.Index, comp.PickedColor, comp.ColorPalette);
- }
+ private void OnDecalSnapChanged(bool snap)
+ {
+ SendPredictedMessage(new SprayPainterSetDecalSnapMessage(snap));
}
- private void OnSpritePicked(ItemList.ItemListSelectedEventArgs args)
+ private void OnDecalAngleChanged(int angle)
{
- SendMessage(new SprayPainterSpritePickedMessage(args.ItemIndex));
+ SendPredictedMessage(new SprayPainterSetDecalAngleMessage(angle));
}
- private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
+ private void OnDecalColorChanged(Color? color)
+ {
+ SendPredictedMessage(new SprayPainterSetDecalColorMessage(color));
+ }
+
+ private void OnDecalChanged(ProtoId protoId)
+ {
+ SendPredictedMessage(new SprayPainterSetDecalMessage(protoId));
+ }
+
+ private void OnTabChanged(int index, bool isSelectedTabWithDecals)
+ {
+ SendPredictedMessage(new SprayPainterTabChangedMessage(index, isSelectedTabWithDecals));
+ }
+
+ private void OnSpritePicked(string group, string style)
+ {
+ SendPredictedMessage(new SprayPainterSetPaintableStyleMessage(group, style));
+ }
+
+ private void OnSetPipeColor(ItemList.ItemListSelectedEventArgs args)
{
var key = _window?.IndexToColorKey(args.ItemIndex);
- SendMessage(new SprayPainterColorPickedMessage(key));
+ SendPredictedMessage(new SprayPainterSetPipeColorMessage(key));
+ }
+
+ private void OnDecalColorPickerToggled(bool toggle)
+ {
+ SendPredictedMessage(new SprayPainterSetDecalColorPickerMessage(toggle));
}
}
+/// Forge-Chane-End
+///
diff --git a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml
new file mode 100644
index 00000000000..ef4379e6cb1
--- /dev/null
+++ b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs
new file mode 100644
index 00000000000..ced30f07c7c
--- /dev/null
+++ b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs
@@ -0,0 +1,217 @@
+/// Forge-Change-Start
+using Content.Client.Decals.UI;
+using Content.Client.Stylesheets;
+using Content.Shared.Decals;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using System.Linq;
+using System.Numerics;
+
+namespace Content.Client.SprayPainter.UI;
+
+///
+/// Used to control decal painting parameters for the spray painter.
+///
+[GenerateTypedNameReferences]
+public sealed partial class SprayPainterDecals : Control
+{
+ public Action>? OnDecalSelected;
+ public Action? OnColorChanged;
+ public Action? OnAngleChanged;
+ public Action? OnSnapChanged;
+ public Action? OnColorPickerToggled;
+
+ private PaletteColorPicker? _palette;
+
+ private SpriteSystem? _sprite;
+ private string _selectedDecal = string.Empty;
+ private List _decals = [];
+
+ public SprayPainterDecals()
+ {
+ RobustXamlLoader.Load(this);
+
+ AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value + 90) % 360;
+ SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value - 90) % 360;
+ SetZeroAngleButton.OnButtonUp += _ => AngleSpinBox.Value = 0;
+ AngleSpinBox.ValueChanged += args => OnAngleChanged?.Invoke(args.Value);
+
+ UseCustomColorCheckBox.OnPressed += UseCustomColorCheckBoxOnOnPressed;
+ SnapToTileCheckBox.OnPressed += SnapToTileCheckBoxOnOnPressed;
+ ColorSelector.OnColorChanged += OnColorSelected;
+
+ ColorPalette.OnPressed += ColorPaletteOnPressed;
+ ColorPicker.OnPressed += args => OnColorPickerToggled?.Invoke(args.Button.Pressed);
+ }
+
+ private void UseCustomColorCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
+ {
+ OnColorChanged?.Invoke(UseCustomColorCheckBox.Pressed ? ColorSelector.Color : null);
+ UpdateColorButtons(UseCustomColorCheckBox.Pressed);
+ }
+
+ private void SnapToTileCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
+ {
+ OnSnapChanged?.Invoke(SnapToTileCheckBox.Pressed);
+ }
+
+ ///
+ /// Updates the decal list.
+ ///
+ public void PopulateDecals(List decals, SpriteSystem sprite)
+ {
+ _sprite ??= sprite;
+
+ _decals = decals;
+ DecalsGrid.Children.Clear();
+
+ foreach (var decal in decals)
+ {
+ var button = new TextureButton()
+ {
+ TextureNormal = sprite.Frame0(decal.Sprite),
+ Name = decal.Name,
+ ToolTip = decal.Name,
+ Scale = new Vector2(2, 2),
+ };
+ button.OnPressed += DecalButtonOnPressed;
+
+ if (UseCustomColorCheckBox.Pressed)
+ {
+ button.Modulate = ColorSelector.Color;
+ }
+
+ if (_selectedDecal == decal.Name)
+ {
+ var panelContainer = new PanelContainer()
+ {
+ PanelOverride = new StyleBoxFlat()
+ {
+ BackgroundColor = StyleNano.ButtonColorDefault,
+ },
+ Children =
+ {
+ button,
+ },
+ };
+ DecalsGrid.AddChild(panelContainer);
+ }
+ else
+ {
+ DecalsGrid.AddChild(button);
+ }
+ }
+ }
+
+ private void OnColorSelected(Color color)
+ {
+ if (!UseCustomColorCheckBox.Pressed)
+ return;
+
+ OnColorChanged?.Invoke(color);
+
+ UpdateColorButtons(UseCustomColorCheckBox.Pressed);
+ }
+
+ private void UpdateColorButtons(bool apply)
+ {
+ Color modulateColor = apply ? ColorSelector.Color : Color.White;
+ foreach (var button in DecalsGrid.Children)
+ {
+ switch (button)
+ {
+ case TextureButton:
+ button.Modulate = modulateColor;
+ break;
+ case PanelContainer panelContainer:
+ {
+ foreach (TextureButton textureButton in panelContainer.Children)
+ textureButton.Modulate = modulateColor;
+
+ break;
+ }
+ }
+ }
+ }
+
+ private void DecalButtonOnPressed(BaseButton.ButtonEventArgs obj)
+ {
+ if (obj.Button.Name is not { } name)
+ return;
+
+ _selectedDecal = name;
+ OnDecalSelected?.Invoke(_selectedDecal);
+
+ if (_sprite is null)
+ return;
+
+ PopulateDecals(_decals, _sprite);
+ }
+
+ public void SetSelectedDecal(string name)
+ {
+ _selectedDecal = name;
+ SelectedDecalName.Text = name;
+
+ if (_sprite is null)
+ return;
+
+ PopulateDecals(_decals, _sprite);
+ }
+
+ public void SetAngle(int degrees)
+ {
+ AngleSpinBox.OverrideValue(degrees);
+ }
+
+ public void SetColor(Color? color)
+ {
+ UseCustomColorCheckBox.Pressed = color != null;
+ if (color != null)
+ ColorSelector.Color = color.Value;
+ UpdateColorButtons(UseCustomColorCheckBox.Pressed);
+ }
+
+ public void SetSnap(bool snap)
+ {
+ SnapToTileCheckBox.Pressed = snap;
+ }
+
+ private void ColorPaletteOnPressed(BaseButton.ButtonEventArgs _)
+ {
+ // Code copied from other implementations of `PaletteColorPicker`.
+ if (_palette is null)
+ {
+ _palette = new PaletteColorPicker();
+ _palette.OpenCenteredLeft();
+ _palette.PaletteList.OnItemSelected += args =>
+ {
+ var color = (args.ItemList.GetSelected().First().Metadata as Color?)!.Value;
+ ColorSelector.Color = color;
+ OnColorSelected(color);
+ };
+ return;
+ }
+
+ if (_palette.IsOpen)
+ {
+ _palette.Close();
+ }
+ else
+ {
+ _palette.Open();
+ }
+ }
+
+ public void SetColorPicker(bool enabled)
+ {
+ ColorPicker.Pressed = enabled;
+ }
+}
+/// Forge-Change-End
+///
diff --git a/Content.Client/SprayPainter/UI/SprayPainterGroup.xaml b/Content.Client/SprayPainter/UI/SprayPainterGroup.xaml
new file mode 100644
index 00000000000..aeb0d07158e
--- /dev/null
+++ b/Content.Client/SprayPainter/UI/SprayPainterGroup.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/Content.Client/SprayPainter/UI/SprayPainterGroup.xaml.cs b/Content.Client/SprayPainter/UI/SprayPainterGroup.xaml.cs
new file mode 100644
index 00000000000..f66a7fb5bd0
--- /dev/null
+++ b/Content.Client/SprayPainter/UI/SprayPainterGroup.xaml.cs
@@ -0,0 +1,69 @@
+/// Forge-Change-Start
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.SprayPainter.UI;
+
+///
+/// Used to display a group of paintable styles in the spray painter menu.
+/// (e.g. each type of paintable locker or plastic crate)
+///
+[GenerateTypedNameReferences]
+public sealed partial class SprayPainterGroup : BoxContainer
+{
+ public event Action? OnButtonPressed;
+
+ public SprayPainterGroup()
+ {
+ RobustXamlLoader.Load(this);
+
+ StyleList.GenerateItem = GenerateItems;
+ }
+
+ public void PopulateList(List spriteList)
+ {
+ StyleList.PopulateList(spriteList);
+ }
+
+ public void SelectItemByStyle(string key)
+ {
+ foreach (var elem in StyleList.Data)
+ {
+ if (elem is not SpriteListData spriteElem)
+ continue;
+
+ if (spriteElem.Style == key)
+ {
+ StyleList.Select(spriteElem);
+ break;
+ }
+ }
+ }
+
+ private void GenerateItems(ListData data, ListContainerButton button)
+ {
+ if (data is not SpriteListData spriteListData)
+ return;
+
+ var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal };
+ var protoView = new EntityPrototypeView();
+ protoView.SetPrototype(spriteListData.Prototype);
+ var label = new Label()
+ {
+ Text = Loc.GetString($"spray-painter-style-{spriteListData.Group.ToLower()}-{spriteListData.Style.ToLower()}")
+ };
+
+ box.AddChild(protoView);
+ box.AddChild(label);
+ button.AddChild(box);
+ button.AddStyleClass(ListContainer.StyleClassListContainerButton);
+ button.OnPressed += _ => OnButtonPressed?.Invoke(spriteListData);
+
+ if (spriteListData.SelectedIndex == button.Index)
+ button.Pressed = true;
+ }
+}
+/// Forge-Change-End
+///
diff --git a/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml b/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml
index 13e500c46c8..46facb5d321 100644
--- a/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml
+++ b/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml
@@ -1,34 +1,6 @@
-
-
-
-
-
-
-
-
-
-
+ MinSize="520 300"
+ SetSize="520 700"
+ Title="{Loc 'spray-painter-window-title'}">
+
diff --git a/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs b/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs
index 617e3ad08ea..66626fb7462 100644
--- a/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs
+++ b/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs
@@ -1,25 +1,56 @@
+/// Forge-Chane-Start
+using System.Linq;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Decals;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.SprayPainter.UI;
+///
+/// A window to select spray painter settings by object type, as well as pipe colours and decals.
+///
[GenerateTypedNameReferences]
public sealed partial class SprayPainterWindow : DefaultWindow
{
[Dependency] private IEntitySystemManager _sysMan = default!;
+ [Dependency] private ILocalizationManager _loc = default!;
+
private readonly SpriteSystem _spriteSystem;
- public Action? OnSpritePicked;
- public Action? OnColorPicked;
+ // Events
+ public event Action? OnSpritePicked;
+ public event Action? OnTabChanged;
+ public event Action>? OnDecalChanged;
+ public event Action? OnSetPipeColor;
+ public event Action? OnDecalColorChanged;
+ public event Action? OnDecalAngleChanged;
+ public event Action? OnDecalSnapChanged;
+ public event Action? OnDecalColorPickerToggled;
+
+ // Pipe color data
+ private ItemList _colorList = default!;
public Dictionary ItemColorIndex = new();
- private Dictionary currentPalette = new();
- private const string colorLocKeyPrefix = "pipe-painter-color-";
- private List CurrentEntries = new List();
+ private Dictionary _currentPalette = new();
+ private const string ColorLocKeyPrefix = "pipe-painter-color-";
+
+ // Paintable objects
+ private Dictionary> _currentStylesByGroup = new();
+ private Dictionary> _currentGroupsByCategory = new();
+
+ // Tab controls
+ private Dictionary _paintableControls = new();
+ private BoxContainer? _pipeControl;
+
+ // Decals
+ private List _currentDecals = [];
+ private SprayPainterDecals? _sprayPainterDecals;
private readonly SpriteSpecifier _colorEntryIconTexture = new SpriteSpecifier.Rsi(
new ResPath("Structures/Piping/Atmospherics/pipe.rsi"),
@@ -30,67 +61,254 @@ public SprayPainterWindow()
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_spriteSystem = _sysMan.GetEntitySystem();
+ Tabs.OnTabChanged += (index) => OnTabChanged?.Invoke(index, _sprayPainterDecals?.GetPositionInParent() == index);
}
- private static string GetColorLocString(string? colorKey)
+ private string GetColorLocString(string? colorKey)
{
if (string.IsNullOrEmpty(colorKey))
return Loc.GetString("pipe-painter-no-color-selected");
- var locKey = colorLocKeyPrefix + colorKey;
+ var locKey = ColorLocKeyPrefix + colorKey;
- if (!Loc.TryGetString(locKey, out var locString))
+ if (!_loc.TryGetString(locKey, out var locString))
locString = colorKey;
return locString;
- }
+ }
public string? IndexToColorKey(int index)
{
- return (string?) ColorList[index].Metadata;
+ return _colorList[index].Text;
+ }
+
+ private void OnStyleSelected(ListData data)
+ {
+ if (data is SpriteListData listData)
+ OnSpritePicked?.Invoke(listData.Group, listData.Style);
}
- public void Populate(List entries, int selectedStyle, string? selectedColorKey, Dictionary palette)
+ ///
+ /// Wrapper to allow for selecting/deselecting the event to avoid loops
+ ///
+ private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
{
+ OnSetPipeColor?.Invoke(args);
+ }
+
+ ///
+ /// Setup function for the window.
+ ///
+ /// Each group, mapped by name to the set of named styles by their associated entity prototype.
+ /// The set of categories and the groups associated with them.
+ /// A list of each decal.
+ public void PopulateCategories(Dictionary> stylesByGroup, Dictionary> groupsByCategory, List decals)
+ {
+ bool tabsCleared = false;
+ var lastTab = Tabs.CurrentTab;
+
+ if (!_currentGroupsByCategory.Equals(groupsByCategory))
+ {
+ // Destroy all existing tabs
+ tabsCleared = true;
+ _paintableControls.Clear();
+ _pipeControl = null;
+ _sprayPainterDecals = null;
+ Tabs.RemoveAllChildren();
+ }
+
// Only clear if the entries change. Otherwise the list would "jump" after selecting an item
- if (!CurrentEntries.Equals(entries))
+ if (tabsCleared || !_currentStylesByGroup.Equals(stylesByGroup))
{
- CurrentEntries = entries;
- SpriteList.Clear();
- foreach (var entry in entries)
+ _currentStylesByGroup = stylesByGroup;
+
+ var tabIndex = 0;
+ foreach (var (categoryName, categoryGroups) in groupsByCategory.OrderBy(c => c.Key))
{
- SpriteList.AddItem(entry.Name, entry.Icon);
+ if (categoryGroups.Count <= 0)
+ continue;
+
+ // Repopulating controls:
+ // ensure that categories with multiple groups have separate subtabs
+ // but single-group categories do not.
+ if (tabsCleared)
+ {
+ TabContainer? subTabs = null;
+ if (categoryGroups.Count > 1)
+ subTabs = new();
+
+ foreach (var group in categoryGroups)
+ {
+ if (!stylesByGroup.TryGetValue(group, out var styles))
+ continue;
+
+ var groupControl = new SprayPainterGroup();
+ groupControl.OnButtonPressed += OnStyleSelected;
+ _paintableControls[group] = groupControl;
+ if (categoryGroups.Count > 1)
+ {
+ if (subTabs != null)
+ {
+ subTabs?.AddChild(groupControl);
+ var subTabLocalization = Loc.GetString("spray-painter-tab-group-" + group.ToLower());
+ TabContainer.SetTabTitle(groupControl, subTabLocalization);
+ }
+ }
+ else
+ {
+ Tabs.AddChild(groupControl);
+ }
+ }
+
+ if (subTabs != null)
+ Tabs.AddChild(subTabs);
+
+ var tabLocalization = Loc.GetString("spray-painter-tab-category-" + categoryName.ToLower());
+ Tabs.SetTabTitle(tabIndex, tabLocalization);
+ tabIndex++;
+ }
+
+ // Finally, populate all groups with new data.
+ foreach (var group in categoryGroups)
+ {
+ if (!stylesByGroup.TryGetValue(group, out var styles) ||
+ !_paintableControls.TryGetValue(group, out var control))
+ continue;
+
+ var dataList = styles
+ .Select(e => new SpriteListData(group, e.Key, e.Value, 0))
+ .OrderBy(d => Loc.GetString($"spray-painter-style-{group.ToLower()}-{d.Style.ToLower()}"))
+ .ToList();
+ control.PopulateList(dataList);
+ }
}
}
- if (!currentPalette.Equals(palette))
+ PopulateColors(_currentPalette);
+
+ if (!_currentDecals.Equals(decals))
+ {
+ _currentDecals = decals;
+
+ if (_sprayPainterDecals is null)
+ {
+ _sprayPainterDecals = new SprayPainterDecals();
+
+ _sprayPainterDecals.OnDecalSelected += id => OnDecalChanged?.Invoke(id);
+ _sprayPainterDecals.OnColorChanged += color => OnDecalColorChanged?.Invoke(color);
+ _sprayPainterDecals.OnAngleChanged += angle => OnDecalAngleChanged?.Invoke(angle);
+ _sprayPainterDecals.OnSnapChanged += snap => OnDecalSnapChanged?.Invoke(snap);
+ _sprayPainterDecals.OnColorPickerToggled += toggle => OnDecalColorPickerToggled?.Invoke(toggle);
+
+ Tabs.AddChild(_sprayPainterDecals);
+ TabContainer.SetTabTitle(_sprayPainterDecals, Loc.GetString("spray-painter-tab-category-decals"));
+ }
+
+ _sprayPainterDecals.PopulateDecals(decals, _spriteSystem);
+ }
+
+ if (tabsCleared)
+ SetSelectedTab(lastTab);
+ }
+
+ public void PopulateColors(Dictionary palette)
+ {
+ // Create pipe tab controls if they don't exist
+ bool tabCreated = false;
+ if (_pipeControl == null)
+ {
+ _pipeControl = new BoxContainer() { Orientation = BoxContainer.LayoutOrientation.Vertical };
+
+ var label = new Label() { Text = Loc.GetString("spray-painter-selected-color") };
+
+ _colorList = new ItemList() { VerticalExpand = true };
+ _colorList.OnItemSelected += OnColorPicked;
+
+ _pipeControl.AddChild(label);
+ _pipeControl.AddChild(_colorList);
+
+ Tabs.AddChild(_pipeControl);
+ TabContainer.SetTabTitle(_pipeControl, Loc.GetString("spray-painter-tab-category-pipes"));
+ tabCreated = true;
+ }
+
+ // Populate the tab if needed (new tab/new data)
+ if (tabCreated || !_currentPalette.Equals(palette))
{
- currentPalette = palette;
+ _currentPalette = palette;
ItemColorIndex.Clear();
- ColorList.Clear();
+ _colorList.Clear();
+ int index = 0;
foreach (var color in palette)
{
var locString = GetColorLocString(color.Key);
- var item = ColorList.AddItem(locString, _spriteSystem.Frame0(_colorEntryIconTexture));
+ var item = _colorList.AddItem(locString, _spriteSystem.Frame0(_colorEntryIconTexture), metadata: color.Key);
item.IconModulate = color.Value;
- item.Metadata = color.Key;
- ItemColorIndex.Add(color.Key, ColorList.IndexOf(item));
+ ItemColorIndex.Add(color.Key, index);
+ index++;
}
}
+ }
- // Disable event so we don't send a new event for pre-selectedStyle entry and end up in a loop
+ # region Setters
+ public void SetSelectedStyles(Dictionary selectedStyles)
+ {
+ foreach (var (group, style) in selectedStyles)
+ {
+ if (!_paintableControls.TryGetValue(group, out var control))
+ continue;
- if (selectedColorKey != null)
+ control.SelectItemByStyle(style);
+ }
+ }
+
+ public void SelectColor(string color)
+ {
+ if (_colorList != null && ItemColorIndex.TryGetValue(color, out var colorIdx))
{
- var index = ItemColorIndex[selectedColorKey];
- ColorList.OnItemSelected -= OnColorPicked;
- ColorList[index].Selected = true;
- ColorList.OnItemSelected += OnColorPicked;
+ _colorList.OnItemSelected -= OnColorPicked;
+ _colorList[colorIdx].Selected = true;
+ _colorList.OnItemSelected += OnColorPicked;
}
+ }
- SpriteList.OnItemSelected -= OnSpritePicked;
- SpriteList[selectedStyle].Selected = true;
- SpriteList.OnItemSelected += OnSpritePicked;
+ public void SetSelectedTab(int tab)
+ {
+ Tabs.CurrentTab = int.Min(tab, Tabs.ChildCount - 1);
}
+
+ public void SetSelectedDecal(string decal)
+ {
+ if (_sprayPainterDecals != null)
+ _sprayPainterDecals.SetSelectedDecal(decal);
+ }
+
+ public void SetDecalAngle(int angle)
+ {
+ if (_sprayPainterDecals != null)
+ _sprayPainterDecals.SetAngle(angle);
+ }
+
+ public void SetDecalColor(Color? color)
+ {
+ if (_sprayPainterDecals != null)
+ _sprayPainterDecals.SetColor(color);
+ }
+
+ public void SetDecalSnap(bool snap)
+ {
+ if (_sprayPainterDecals != null)
+ _sprayPainterDecals.SetSnap(snap);
+ }
+
+ public void SetDecalColorPicker(bool colorPickerEnabled)
+ {
+ _sprayPainterDecals?.SetColorPicker(colorPickerEnabled);
+ }
+ #endregion
}
+
+public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData;
+/// Forge-Chane-End
+///
diff --git a/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs b/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
index ee4f2fdfd6c..0c09cd1ef09 100644
--- a/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
+++ b/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
@@ -1,10 +1,15 @@
+/// Forge-Chane-Start
+using Content.Shared.SprayPainter.Prototypes;
using Content.Shared.Storage;
using Robust.Client.GameObjects;
+using Robust.Shared.Prototypes;
namespace Content.Client.Storage.Visualizers;
-public sealed class EntityStorageVisualizerSystem : VisualizerSystem
+public sealed partial class EntityStorageVisualizerSystem : VisualizerSystem
{
+
+ [Dependency] private IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
@@ -23,51 +28,85 @@ private void OnComponentInit(EntityUid uid, EntityStorageVisualsComponent comp,
if (!TryComp(uid, out var sprite))
return;
- sprite.LayerSetState(StorageVisualLayers.Base, comp.StateBaseClosed);
+ SpriteSystem.LayerSetRsiState((uid, sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
}
- protected override void OnAppearanceChange(EntityUid uid, EntityStorageVisualsComponent comp, ref AppearanceChangeEvent args)
+ protected override void OnAppearanceChange(EntityUid uid,
+ EntityStorageVisualsComponent comp,
+ ref AppearanceChangeEvent args)
{
if (args.Sprite == null
- || !AppearanceSystem.TryGetData(uid, StorageVisuals.Open, out var open, args.Component))
+ || !AppearanceSystem.TryGetData(uid, StorageVisuals.Open, out var open, args.Component))
return;
+ var forceRedrawBase = false;
+ /// Forge-Change-Start
+ if (AppearanceSystem.TryGetData(uid, PaintableVisuals.Prototype, out var prototype, args.Component))
+ {
+ if (_prototypeManager.HasIndex(prototype))
+ {
+ // Spawn the paint target prototype client-side so we can copy its visuals.
+ var tempUid = Spawn(prototype);
+
+ if (TryComp(tempUid, out var sprite) && sprite.BaseRSI != null)
+ {
+ SpriteSystem.SetBaseRsi((uid, args.Sprite), sprite.BaseRSI);
+ }
+
+ if (TryComp(tempUid, out var visuals))
+ {
+ comp.StateBaseOpen = visuals.StateBaseOpen;
+ comp.StateBaseClosed = visuals.StateBaseClosed;
+ comp.StateDoorOpen = visuals.StateDoorOpen;
+ comp.StateDoorClosed = visuals.StateDoorClosed;
+ forceRedrawBase = true;
+ }
+
+ QueueDel(tempUid);
+ }
+ }
+ /// Forge-Change-End
+
// Open/Closed state for the storage entity.
- if (args.Sprite.LayerMapTryGet(StorageVisualLayers.Door, out _))
+ if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), StorageVisualLayers.Door, out _, false))
{
if (open)
{
if (comp.OpenDrawDepth != null)
- args.Sprite.DrawDepth = comp.OpenDrawDepth.Value;
+ SpriteSystem.SetDrawDepth((uid, args.Sprite), comp.OpenDrawDepth.Value);
if (comp.StateDoorOpen != null)
{
- args.Sprite.LayerSetState(StorageVisualLayers.Door, comp.StateDoorOpen);
- args.Sprite.LayerSetVisible(StorageVisualLayers.Door, true);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Door, comp.StateDoorOpen);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), StorageVisualLayers.Door, true);
}
else
{
- args.Sprite.LayerSetVisible(StorageVisualLayers.Door, false);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), StorageVisualLayers.Door, false);
}
if (comp.StateBaseOpen != null)
- args.Sprite.LayerSetState(StorageVisualLayers.Base, comp.StateBaseOpen);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseOpen);
+ else if (forceRedrawBase && comp.StateBaseClosed != null)
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
}
else
{
if (comp.ClosedDrawDepth != null)
- args.Sprite.DrawDepth = comp.ClosedDrawDepth.Value;
+ SpriteSystem.SetDrawDepth((uid, args.Sprite), comp.ClosedDrawDepth.Value);
if (comp.StateDoorClosed != null)
{
- args.Sprite.LayerSetState(StorageVisualLayers.Door, comp.StateDoorClosed);
- args.Sprite.LayerSetVisible(StorageVisualLayers.Door, true);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Door, comp.StateDoorClosed);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), StorageVisualLayers.Door, true);
}
else
- args.Sprite.LayerSetVisible(StorageVisualLayers.Door, false);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), StorageVisualLayers.Door, false);
if (comp.StateBaseClosed != null)
- args.Sprite.LayerSetState(StorageVisualLayers.Base, comp.StateBaseClosed);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
+ else if (forceRedrawBase && comp.StateBaseOpen != null)
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseOpen);
}
}
}
@@ -78,3 +117,5 @@ public enum StorageVisualLayers : byte
Base,
Door
}
+/// Forge-Chane-End
+///
diff --git a/Content.Server/SprayPainter/SprayPainterSystem.cs b/Content.Server/SprayPainter/SprayPainterSystem.cs
index 83a9687fd16..2813838c913 100644
--- a/Content.Server/SprayPainter/SprayPainterSystem.cs
+++ b/Content.Server/SprayPainter/SprayPainterSystem.cs
@@ -1,27 +1,150 @@
+/// Forge-Chane-Start
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.EntitySystems;
+using Content.Server.Charges;
+using Content.Shared.Charges.Systems;
+using Content.Server.Charges.Systems;
+using Content.Server.Decals;
+using Content.Server.Destructible;
+using Content.Server.Popups;
+using Content.Shared.Atmos.Piping.Unary.Components;
+using Content.Shared.Charges.Components;
+using Content.Shared.Coordinates.Helpers;
+using Content.Shared.Database;
+using Content.Shared.Decals;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.SprayPainter;
using Content.Shared.SprayPainter.Components;
+using Robust.Server.Audio;
+using Robust.Server.GameObjects;
+using Robust.Shared.Prototypes;
+using System.Linq;
+using System.Numerics;
namespace Content.Server.SprayPainter;
///
-/// Handles spraying pipes using a spray painter.
-/// Airlocks are handled in shared.
+/// Handles spraying pipes and decals using a spray painter.
+/// Other paintable objects are handled in shared.
///
public sealed partial class SprayPainterSystem : SharedSprayPainterSystem
{
[Dependency] private AtmosPipeColorSystem _pipeColor = default!;
+ [Dependency] private PopupSystem _popup = default!;
+ [Dependency] private DecalSystem _decals = default!;
+ [Dependency] private AudioSystem _audio = default!;
+ [Dependency] private ChargesSystem _charges = default!;
+ [Dependency] private TransformSystem _transform = default!;
+ [Dependency] private IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnPipeDoAfter);
-
+ SubscribeLocalEvent(OnFloorAfterInteract);
SubscribeLocalEvent(OnPipeInteract);
+ SubscribeLocalEvent(OnCanisterPainted);
+ }
+
+ ///
+ /// Handles drawing decals when a spray painter is used to interact with the floor.
+ /// Spray painter must have decal painting enabled and enough charges of paint to paint on the floor.
+ ///
+ private void OnFloorAfterInteract(Entity ent, ref AfterInteractEvent args)
+ {
+ if (args.Handled || args.Target != null)
+ return;
+
+ if (ent.Comp.ColorPickerEnabled)
+ {
+ PickColor(ent, ref args);
+ return;
+ }
+
+ if (!args.CanReach)
+ return;
+
+ // Includes both off and all other don't cares
+ if (ent.Comp.DecalMode != DecalPaintMode.Add && ent.Comp.DecalMode != DecalPaintMode.Remove)
+ return;
+
+ args.Handled = true;
+ if (!TryComp(ent, out LimitedChargesComponent? charges)
+ || _charges.IsEmpty(ent, charges)
+ || _charges.HasInsufficientCharges(ent, ent.Comp.DecalChargeCost, charges))
+ {
+ _popup.PopupEntity(Loc.GetString("spray-painter-interact-no-charges"), args.User, args.User);
+ return;
+ }
+
+ var position = args.ClickLocation;
+ if (ent.Comp.SnapDecals)
+ position = position.SnapToGrid(EntityManager);
+
+ if (ent.Comp.DecalMode == DecalPaintMode.Add)
+ {
+ // Offset painting for adding decals
+ position = position.Offset(new(-0.5f));
+
+ if (!_decals.TryAddDecal(ent.Comp.SelectedDecal, position, out _, ent.Comp.SelectedDecalColor, Angle.FromDegrees(ent.Comp.SelectedDecalAngle), 0, false))
+ return;
+ }
+ else
+ {
+ var gridUid = _transform.GetGrid(args.ClickLocation);
+ if (gridUid is not { } grid || !TryComp(grid, out var decalGridComp))
+ {
+ _popup.PopupEntity(Loc.GetString("spray-painter-interact-nothing-to-remove"), args.User, args.User);
+ return;
+ }
+
+ var decals = _decals.GetDecalsInRange(grid, position.Position, validDelegate: IsDecalValid);
+ if (decals.Count <= 0)
+ {
+ _popup.PopupEntity(Loc.GetString("spray-painter-interact-nothing-to-remove"), args.User, args.User);
+ return;
+ }
+
+ foreach (var decal in decals)
+ {
+ _decals.RemoveDecal(grid, decal.Index, decalGridComp);
+ }
+ }
+
+ _audio.PlayPvs(ent.Comp.SpraySound, ent);
+
+ _charges.UseCharges(ent, ent.Comp.DecalChargeCost, charges);
+
+ AdminLogger.Add(LogType.CrayonDraw, LogImpact.Low, $"{ToPrettyString(args.User):user} painted a {ent.Comp.SelectedDecal}");
+ }
+
+ ///
+ /// Returns whether is valid to interact with when a spray painter is used to interact with the floor.
+ ///
+ private bool IsDecalValid(Decal decal)
+ {
+ if (!_prototypeManager.TryIndex(decal.Id, out var decalProto))
+ return false;
+
+ return (decalProto.Tags.Contains("station")
+ || decalProto.Tags.Contains("markings"))
+ && !decalProto.Tags.Contains("dirty");
+ }
+
+ ///
+ /// Event handler when gas canisters are painted.
+ /// The canister's color should not change when it's destroyed.
+ ///
+ private void OnCanisterPainted(Entity ent, ref EntityPaintedEvent args)
+ {
+ var dummy = Spawn(args.Prototype);
+
+ var destructibleComp = EnsureComp(dummy);
+ CopyComp(dummy, ent, destructibleComp);
+
+ Del(dummy);
}
private void OnPipeDoAfter(Entity ent, ref SprayPainterPipeDoAfterEvent args)
@@ -29,14 +152,20 @@ private void OnPipeDoAfter(Entity ent, ref SprayPainterPi
if (args.Handled || args.Cancelled)
return;
- if (args.Args.Target is not {} target)
+ if (args.Args.Target is not { } target)
return;
if (!TryComp(target, out var color))
return;
- Audio.PlayPvs(ent.Comp.SpraySound, ent);
+ if (!TryComp(ent, out var charges)
+ || _charges.IsEmpty(ent, charges)
+ || _charges.HasInsufficientCharges(ent, ent.Comp.PipeChargeCost, charges))
+ return;
+
+ _charges.UseCharges(ent, ent.Comp.PipeChargeCost, charges);
+ Audio.PlayPvs(ent.Comp.SpraySound, ent);
_pipeColor.SetColor(target, color, args.Color);
args.Handled = true;
@@ -47,13 +176,29 @@ private void OnPipeInteract(Entity ent, ref InteractUsi
if (args.Handled)
return;
- if (!TryComp(args.Used, out var painter) || painter.PickedColor is not {} colorName)
+ if (!TryComp(args.Used, out var painter) ||
+ painter.PickedColor is not { } colorName)
return;
if (!painter.ColorPalette.TryGetValue(colorName, out var color))
return;
- var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, painter.PipeSprayTime, new SprayPainterPipeDoAfterEvent(color), args.Used, target: ent, used: args.Used)
+ if (!TryComp(args.Used, out var charges)
+ || _charges.IsEmpty(args.Used, charges)
+ || _charges.HasInsufficientCharges(args.Used, painter.PipeChargeCost, charges))
+ {
+ var msg = Loc.GetString("spray-painter-interact-no-charges");
+ _popup.PopupEntity(msg, args.User, args.User);
+ return;
+ }
+
+ var doAfterEventArgs = new DoAfterArgs(EntityManager,
+ args.User,
+ painter.PipeSprayTime,
+ new SprayPainterPipeDoAfterEvent(color),
+ args.Used,
+ target: ent,
+ used: args.Used)
{
BreakOnMove = true,
BreakOnDamage = true,
@@ -64,4 +209,29 @@ private void OnPipeInteract(Entity ent, ref InteractUsi
args.Handled = DoAfter.TryStartDoAfter(doAfterEventArgs);
}
+
+ private void PickColor(Entity ent, ref AfterInteractEvent args)
+ {
+ if (!args.ClickLocation.IsValid(EntityManager) || _transform.GetGrid(args.ClickLocation) is not { } grid)
+ return;
+
+ var clickPos = args.ClickLocation.Position;
+ var decals = _decals.GetDecalsInRange(grid, clickPos, validDelegate: IsDecalValid);
+ if (decals.Count == 0)
+ {
+ _popup.PopupEntity(Loc.GetString("spray-painter-interact-no-color-pick"), args.User, args.User);
+ return;
+ }
+
+ var closestDecal = decals.MinBy(d => Vector2.Distance(d.Decal.Coordinates, clickPos)).Decal;
+
+ _popup.PopupEntity(Loc.GetString("spray-painter-interact-color-picked", ("id", closestDecal.Id)), args.User, args.User);
+
+ ent.Comp.SelectedDecalColor = closestDecal.Color;
+ ent.Comp.ColorPickerEnabled = false;
+ Dirty(ent);
+ }
+
}
+/// Forge-Chane-End
+///
diff --git a/Content.Shared/Doors/Components/DoorComponent.cs b/Content.Shared/Doors/Components/DoorComponent.cs
index 0c9eae7a062..baef5bd8f20 100644
--- a/Content.Shared/Doors/Components/DoorComponent.cs
+++ b/Content.Shared/Doors/Components/DoorComponent.cs
@@ -136,11 +136,20 @@ public sealed partial class DoorComponent : Component
#endregion
#region Graphics
+ /// Forge-Chane-Sart
+ public const string OpenKey = "door_animation_open";
+
+ public const string CloseKey = "door_animation_close";
+
+ ///
+ /// The key used when playing door deny animations.
+ ///
+ public const string DenyKey = "door_animation_deny";
///
- /// The key used when playing door opening/closing/emagging/deny animations.
+ /// The key used when playing door emag animations.
///
- public const string AnimationKey = "door_animation";
+ public const string EmagKey = "door_animation_emag";
///
/// The sprite state used for the door when it's open.
@@ -153,7 +162,7 @@ public sealed partial class DoorComponent : Component
/// The sprite states used for the door while it's open.
///
[ViewVariables(VVAccess.ReadOnly)]
- public List<(DoorVisualLayers, string)> OpenSpriteStates = default!;
+ public List<(Enum, string)> OpenSpriteStates = default!;
///
/// The sprite state used for the door when it's closed.
@@ -166,7 +175,7 @@ public sealed partial class DoorComponent : Component
/// The sprite states used for the door while it's closed.
///
[ViewVariables(VVAccess.ReadOnly)]
- public List<(DoorVisualLayers, string)> ClosedSpriteStates = default!;
+ public List<(Enum, string)> ClosedSpriteStates = default!;
///
/// The sprite state used for the door when it's opening.
@@ -187,22 +196,23 @@ public sealed partial class DoorComponent : Component
public string EmaggingSpriteState = "sparks";
///
- /// The sprite state used for the door when it's open.
+ /// The length of the door's opening animation.
///
[DataField]
- public float OpeningAnimationTime = 0.8f;
+ public TimeSpan OpeningAnimationTime = TimeSpan.FromSeconds(0.8);
///
- /// The sprite state used for the door when it's open.
+ /// The length of the door's closing animation.
///
[DataField]
- public float ClosingAnimationTime = 0.8f;
+ public TimeSpan ClosingAnimationTime = TimeSpan.FromSeconds(0.8);
///
- /// The sprite state used for the door when it's open.
+ /// The length of the door's emagging animation.
///
[DataField]
- public float EmaggingAnimationTime = 1.5f;
+ public TimeSpan EmaggingAnimationTime = TimeSpan.FromSeconds(1.5);
+ /// Forge-Chane-End
///
/// The animation used when the door opens.
diff --git a/Content.Shared/SprayPainter/Components/PaintableAirlockComponent.cs b/Content.Shared/SprayPainter/Components/PaintableAirlockComponent.cs
index fdd0aeeb7f9..21094d1a524 100644
--- a/Content.Shared/SprayPainter/Components/PaintableAirlockComponent.cs
+++ b/Content.Shared/SprayPainter/Components/PaintableAirlockComponent.cs
@@ -1,24 +1,27 @@
-using Content.Shared.Roles;
-using Content.Shared.SprayPainter.Prototypes;
-using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
+/// Forge-Chane-Start
+// using Content.Shared.Roles;
+// using Content.Shared.SprayPainter.Prototypes;
+// using Robust.Shared.GameStates;
+// using Robust.Shared.Prototypes;
-namespace Content.Shared.SprayPainter.Components;
+// namespace Content.Shared.SprayPainter.Components;
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
-public sealed partial class PaintableAirlockComponent : Component
-{
- ///
- /// Group of styles this airlock can be painted with, e.g. glass, standard or external.
- ///
- [DataField(required: true), AutoNetworkedField]
- public ProtoId Group = string.Empty;
+// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+// public sealed partial class PaintableAirlockComponent : Component
+// {
+// ///
+// /// Group of styles this airlock can be painted with, e.g. glass, standard or external.
+// ///
+// [DataField(required: true), AutoNetworkedField]
+// public ProtoId Group = string.Empty;
- ///
- /// Department this airlock is painted as, or none.
- /// Must be specified in prototypes for turf war to work.
- /// To better catch any mistakes, you need to explicitly state a non-styled airlock has a null department.
- ///
- [DataField(required: true), AutoNetworkedField]
- public ProtoId? Department;
-}
+// ///
+// /// Department this airlock is painted as, or none.
+// /// Must be specified in prototypes for turf war to work.
+// /// To better catch any mistakes, you need to explicitly state a non-styled airlock has a null department.
+// ///
+// [DataField(required: true), AutoNetworkedField]
+// public ProtoId? Department;
+// }
+/// Forge-Chane-End
+///
diff --git a/Content.Shared/SprayPainter/Components/PaintableComponent.cs b/Content.Shared/SprayPainter/Components/PaintableComponent.cs
new file mode 100644
index 00000000000..4eab2549cd2
--- /dev/null
+++ b/Content.Shared/SprayPainter/Components/PaintableComponent.cs
@@ -0,0 +1,22 @@
+/// Forge-Change-Start
+using Content.Shared.SprayPainter.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.SprayPainter.Components;
+
+///
+/// Marks objects that can be painted with the spray painter.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class PaintableComponent : Component
+{
+ ///
+ /// Group of styles this airlock can be painted with, e.g. glass, standard or external.
+ /// Set to null to make an entity unpaintable.
+ ///
+ [DataField(required: true)]
+ public ProtoId? Group;
+}
+/// Forge-Change-End
+///
diff --git a/Content.Shared/SprayPainter/Components/PaintedComponent.cs b/Content.Shared/SprayPainter/Components/PaintedComponent.cs
new file mode 100644
index 00000000000..41244b6aeb1
--- /dev/null
+++ b/Content.Shared/SprayPainter/Components/PaintedComponent.cs
@@ -0,0 +1,21 @@
+/// Forge-Change-Start
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.SprayPainter.Components;
+
+///
+/// Used to mark an entity that has been repainted.
+///
+[RegisterComponent, NetworkedComponent]
+[AutoGenerateComponentState, AutoGenerateComponentPause]
+public sealed partial class PaintedComponent : Component
+{
+ ///
+ /// The time after which the entity is dried and does not appear as "freshly painted".
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
+ public TimeSpan DryTime;
+}
+/// Forge-Change-End
+///
diff --git a/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs b/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs
index 0591cb2dcbd..54c146b495c 100644
--- a/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs
+++ b/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs
@@ -1,26 +1,44 @@
-using Content.Shared.DoAfter;
+/// Forge-Chane-Start
+using Content.Shared.Decals;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
namespace Content.Shared.SprayPainter.Components;
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+///
+/// Denotes an object that can be used to alter the appearance of paintable objects (e.g. doors, gas canisters).
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
public sealed partial class SprayPainterComponent : Component
{
+ public const string DefaultPickedColor = "red";
+ public static readonly ProtoId DefaultDecal = "Arrows";
+
+ ///
+ /// The sound to be played after painting the entities.
+ ///
[DataField]
public SoundSpecifier SpraySound = new SoundPathSpecifier("/Audio/Effects/spray2.ogg");
+ ///
+ /// The amount of time it takes to paint a pipe.
+ ///
[DataField]
- public TimeSpan AirlockSprayTime = TimeSpan.FromSeconds(3);
+ public TimeSpan PipeSprayTime = TimeSpan.FromSeconds(1);
+ ///
+ /// The cost of spray painting a pipe, in charges.
+ ///
[DataField]
- public TimeSpan PipeSprayTime = TimeSpan.FromSeconds(1);
+ public int PipeChargeCost = 1;
///
/// Pipe color chosen to spray with.
///
[DataField, AutoNetworkedField]
- public string? PickedColor;
+ public string PickedColor = DefaultPickedColor;
///
/// Pipe colors that can be selected.
@@ -29,9 +47,90 @@ public sealed partial class SprayPainterComponent : Component
public Dictionary ColorPalette = new();
///
- /// Airlock style index selected.
- /// After prototype reload this might not be the same style but it will never be out of bounds.
+ /// Spray paintable object styles selected per object.
+ ///
+ [DataField, AutoNetworkedField]
+ public Dictionary StylesByGroup = new();
+
+ ///
+ /// The currently open tab of the painter
+ /// (Are you selecting canister color?)
+ ///
+ [DataField, AutoNetworkedField]
+ public int SelectedTab;
+
+ ///
+ /// Whether or not the painter should be painting or removing decals when clicked.
+ ///
+ [DataField, AutoNetworkedField]
+ public DecalPaintMode DecalMode = DecalPaintMode.Off;
+
+ ///
+ /// The currently selected decal prototype.
+ ///
+ [DataField, AutoNetworkedField]
+ public ProtoId SelectedDecal = DefaultDecal;
+
+ ///
+ /// The color in which to paint the decal.
+ ///
+ [DataField, AutoNetworkedField]
+ public Color? SelectedDecalColor;
+
+ ///
+ /// The angle at which to paint the decal.
///
[DataField, AutoNetworkedField]
- public int Index;
+ public int SelectedDecalAngle;
+
+ ///
+ /// The angle at which to paint the decal.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool SnapDecals = true;
+
+ ///
+ /// The cost of spray painting a decal, in charges.
+ ///
+ [DataField]
+ public int DecalChargeCost = 1;
+
+ ///
+ /// How long does the painter leave items as freshly painted?
+ ///
+ [DataField]
+ public TimeSpan FreshPaintDuration = TimeSpan.FromMinutes(15);
+
+ ///
+ /// The sound to play when swapping between decal modes.
+ ///
+ [DataField]
+ public SoundSpecifier SoundSwitchDecalMode = new SoundPathSpecifier("/Audio/Machines/quickbeep.ogg", AudioParams.Default.WithVolume(1.5f));
+
+ ///
+ /// Whether the decal color picker is currently active.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool ColorPickerEnabled = false;
+}
+
+///
+/// A set of operating modes for decal painting.
+///
+[Serializable, NetSerializable]
+public enum DecalPaintMode : byte
+{
+ ///
+ /// Clicking on the floor does nothing.
+ ///
+ Off = 0,
+ ///
+ /// Clicking on the floor adds a decal at the requested spot (or snapped to the grid)
+ ///
+ Add = 1,
+ ///
+ /// Clicking on the floor removes all decals at the requested spot (or snapped to the grid)
+ ///
+ Remove = 2,
+/// Forge-Chane-End
}
diff --git a/Content.Shared/SprayPainter/Prototypes/AirlockDepartmentsPrototype.cs b/Content.Shared/SprayPainter/Prototypes/AirlockDepartmentsPrototype.cs
index 8f98a1a3c79..48c2cf24c3e 100644
--- a/Content.Shared/SprayPainter/Prototypes/AirlockDepartmentsPrototype.cs
+++ b/Content.Shared/SprayPainter/Prototypes/AirlockDepartmentsPrototype.cs
@@ -1,21 +1,24 @@
-using Content.Shared.Roles;
-using Robust.Shared.Prototypes;
+/// Forge-Chane-Start
+// using Content.Shared.Roles;
+// using Robust.Shared.Prototypes;
-namespace Content.Shared.SprayPainter.Prototypes;
+// namespace Content.Shared.SprayPainter.Prototypes;
-///
-/// Maps airlock style names to department ids.
-///
-[Prototype]
-public sealed partial class AirlockDepartmentsPrototype : IPrototype
-{
- [IdDataField]
- public string ID { get; private set; } = default!;
+// ///
+// /// Maps airlock style names to department ids.
+// ///
+// [Prototype]
+// public sealed partial class AirlockDepartmentsPrototype : IPrototype
+// {
+// [IdDataField]
+// public string ID { get; private set; } = default!;
- ///
- /// Dictionary of style names to department ids.
- /// If a style does not have a department (e.g. external) it is set to null.
- ///
- [DataField(required: true)]
- public Dictionary> Departments = new();
-}
+// ///
+// /// Dictionary of style names to department ids.
+// /// If a style does not have a department (e.g. external) it is set to null.
+// ///
+// [DataField(required: true)]
+// public Dictionary> Departments = new();
+// }
+/// Forge-Chane-End
+///
diff --git a/Content.Shared/SprayPainter/Prototypes/AirlockGroupPrototype.cs b/Content.Shared/SprayPainter/Prototypes/AirlockGroupPrototype.cs
index 24c28b8b7a7..c1d293e54f6 100644
--- a/Content.Shared/SprayPainter/Prototypes/AirlockGroupPrototype.cs
+++ b/Content.Shared/SprayPainter/Prototypes/AirlockGroupPrototype.cs
@@ -1,19 +1,22 @@
-using Robust.Shared.Prototypes;
+/// Forge-Chane-Start
+// using Robust.Shared.Prototypes;
-namespace Content.Shared.SprayPainter.Prototypes;
+// namespace Content.Shared.SprayPainter.Prototypes;
-[Prototype("AirlockGroup")]
-public sealed partial class AirlockGroupPrototype : IPrototype
-{
- [IdDataField]
- public string ID { get; private set; } = default!;
+// [Prototype("AirlockGroup")]
+// public sealed partial class AirlockGroupPrototype : IPrototype
+// {
+// [IdDataField]
+// public string ID { get; private set; } = default!;
- [DataField("stylePaths")]
- public Dictionary StylePaths = default!;
+// [DataField("stylePaths")]
+// public Dictionary StylePaths = default!;
- // The priority determines, which sprite is used when showing
- // the icon for a style in the SprayPainter UI. The highest priority
- // gets shown.
- [DataField("iconPriority")]
- public int IconPriority = 0;
-}
+// // The priority determines, which sprite is used when showing
+// // the icon for a style in the SprayPainter UI. The highest priority
+// // gets shown.
+// [DataField("iconPriority")]
+// public int IconPriority = 0;
+// }
+/// Forge-Chane-End
+///
diff --git a/Content.Shared/SprayPainter/Prototypes/PaintableGroupCategoryPrototype.cs b/Content.Shared/SprayPainter/Prototypes/PaintableGroupCategoryPrototype.cs
new file mode 100644
index 00000000000..56faed1164d
--- /dev/null
+++ b/Content.Shared/SprayPainter/Prototypes/PaintableGroupCategoryPrototype.cs
@@ -0,0 +1,22 @@
+/// Forge-Change-Start
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.SprayPainter.Prototypes;
+
+///
+/// A category of spray paintable items (e.g. airlocks, crates)
+///
+[Prototype]
+public sealed partial class PaintableGroupCategoryPrototype : IPrototype
+{
+ [IdDataField]
+ public string ID { get; private set; } = default!;
+
+ ///
+ /// Each group that makes up this category.
+ ///
+ [DataField(required: true)]
+ public List> Groups = new();
+}
+/// Forge-Change-End
+///
diff --git a/Content.Shared/SprayPainter/Prototypes/PaintableGroupPrototype.cs b/Content.Shared/SprayPainter/Prototypes/PaintableGroupPrototype.cs
new file mode 100644
index 00000000000..2c6120f8cda
--- /dev/null
+++ b/Content.Shared/SprayPainter/Prototypes/PaintableGroupPrototype.cs
@@ -0,0 +1,56 @@
+/// Forge-Change-Start
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.SprayPainter.Prototypes;
+
+///
+/// Contains a map of the objects from which the spray painter will take texture to paint another from the same group.
+///
+[Prototype]
+public sealed partial class PaintableGroupPrototype : IPrototype
+{
+ [IdDataField]
+ public string ID { get; private set; } = default!;
+
+ ///
+ /// The time required to paint an object from a given group, in seconds.
+ ///
+ [DataField]
+ public float Time = 2.0f;
+
+ ///
+ /// To number of charges needed to paint an object of this group.
+ ///
+ [DataField]
+ public int Cost = 1;
+
+ ///
+ /// The default style to start painting.
+ ///
+ [DataField(required: true)]
+ public string DefaultStyle = default!;
+
+ ///
+ /// Map from localization keys and entity identifiers displayed in the spray painter menu.
+ ///
+ [DataField(required: true)]
+ public Dictionary Styles = new();
+
+ ///
+ /// If multiple groups have the same key, the group with the highest IconPriority has its icon displayed.
+ ///
+ [DataField]
+ public int IconPriority;
+}
+
+[Serializable, NetSerializable]
+public enum PaintableVisuals
+{
+ ///
+ /// The prototype to base the object's visuals off.
+ ///
+ Prototype
+}
+/// Forge-Change-End
+///
diff --git a/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs b/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs
index 56da1bab308..03f3e3fe76a 100644
--- a/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs
+++ b/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs
@@ -1,115 +1,184 @@
+/// Forge-Chane-Start
using Content.Shared.Administration.Logs;
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
using Content.Shared.Database;
using Content.Shared.DoAfter;
-using Content.Shared.Doors.Components;
+using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.SprayPainter.Components;
using Content.Shared.SprayPainter.Prototypes;
+using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Shared.SprayPainter;
///
-/// System for painting airlocks using a spray painter.
+/// System for painting paintable objects using a spray painter.
/// Pipes are handled serverside since AtmosPipeColorSystem is server only.
///
public abstract partial class SharedSprayPainterSystem : EntitySystem
{
- [Dependency] protected IPrototypeManager Proto = default!;
- [Dependency] private ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private IGameTiming _timing = default!;
+ [Dependency] protected ISharedAdminLogManager AdminLogger = default!;
[Dependency] protected SharedAppearanceSystem Appearance = default!;
[Dependency] protected SharedAudioSystem Audio = default!;
+ [Dependency] protected SharedChargesSystem Charges = default!;
[Dependency] protected SharedDoAfterSystem DoAfter = default!;
- [Dependency] private SharedPopupSystem _popup = default!;
-
- public List Styles { get; private set; } = new();
- public List Groups { get; private set; } = new();
-
- [ValidatePrototypeId]
- private const string Departments = "Departments";
+ [Dependency] private SharedPopupSystem _popup = default!;
+ [Dependency] private IPrototypeManager _protoMan = default!;
public override void Initialize()
{
base.Initialize();
- CacheStyles();
-
SubscribeLocalEvent(OnMapInit);
- SubscribeLocalEvent(OnDoorDoAfter);
- Subs.BuiEvents(SprayPainterUiKey.Key, subs =>
- {
- subs.Event(OnSpritePicked);
- subs.Event(OnColorPicked);
- });
- SubscribeLocalEvent(OnAirlockInteract);
+ SubscribeLocalEvent(OnPainterDoAfter);
+ SubscribeLocalEvent>(OnPainterGetAltVerbs);
+ SubscribeLocalEvent(OnPaintableInteract);
+ SubscribeLocalEvent(OnPainedExamined);
- SubscribeLocalEvent(OnPrototypesReloaded);
+ Subs.BuiEvents(SprayPainterUiKey.Key,
+ subs =>
+ {
+ subs.Event(OnSetPaintable);
+ subs.Event(OnSetPipeColor);
+ subs.Event(OnTabChanged);
+ subs.Event(OnSetDecal);
+ subs.Event(OnSetDecalColor);
+ subs.Event(OnSetDecalAngle);
+ subs.Event(OnSetDecalSnap);
+ subs.Event(OnSetDecalColorPicker);
+ });
}
private void OnMapInit(Entity ent, ref MapInitEvent args)
{
- if (ent.Comp.ColorPalette.Count == 0)
+ bool stylesByGroupPopulated = false;
+ foreach (var groupProto in _protoMan.EnumeratePrototypes())
+ {
+ ent.Comp.StylesByGroup[groupProto.ID] = groupProto.DefaultStyle;
+ stylesByGroupPopulated = true;
+ }
+ if (stylesByGroupPopulated)
+ Dirty(ent);
+
+ if (ent.Comp.ColorPalette.Count > 0)
+ SetPipeColor(ent, ent.Comp.ColorPalette.First().Key);
+ }
+
+ private void SetPipeColor(Entity ent, string? paletteKey)
+ {
+ if (paletteKey == null || paletteKey == ent.Comp.PickedColor)
+ return;
+
+ if (!ent.Comp.ColorPalette.ContainsKey(paletteKey))
return;
- SetColor(ent, ent.Comp.ColorPalette.First().Key);
+ ent.Comp.PickedColor = paletteKey;
+ Dirty(ent);
+ UpdateUi(ent);
}
- private void OnDoorDoAfter(Entity ent, ref SprayPainterDoorDoAfterEvent args)
+ #region Interaction
+
+ private void OnPainterDoAfter(Entity ent, ref SprayPainterDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
- if (args.Args.Target is not {} target)
+ if (args.Args.Target is not { } target)
return;
- if (!TryComp(target, out var airlock))
+ if (!HasComp(target))
return;
- airlock.Department = args.Department;
- Dirty(target, airlock);
+ if (!TryComp(ent, out var charges)
+ || Charges.IsEmpty(ent, charges)
+ || Charges.HasInsufficientCharges(ent, args.Cost, charges))
+ return;
+ Appearance.SetData(target, PaintableVisuals.Prototype, args.Prototype);
Audio.PlayPredicted(ent.Comp.SpraySound, ent, args.Args.User);
- Appearance.SetData(target, DoorVisuals.BaseRSI, args.Sprite);
- _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Args.User):user} painted {ToPrettyString(args.Args.Target.Value):target}");
+ Charges.UseCharges(ent, args.Cost, charges);
- args.Handled = true;
- }
+ var paintedComponent = EnsureComp(target);
+ paintedComponent.DryTime = _timing.CurTime + ent.Comp.FreshPaintDuration;
- #region UI messages
+ var ev = new EntityPaintedEvent(
+ User: args.User,
+ Tool: ent,
+ Prototype: args.Prototype,
+ Group: args.Group);
+ RaiseLocalEvent(target, ref ev);
- private void OnColorPicked(Entity ent, ref SprayPainterColorPickedMessage args)
- {
- SetColor(ent, args.Key);
+ AdminLogger.Add(LogType.Action,
+ LogImpact.Low,
+ $"{ToPrettyString(args.Args.User):user} painted {ToPrettyString(args.Args.Target.Value):target}");
+
+ args.Handled = true;
+ Dirty(target, paintedComponent);
}
- private void OnSpritePicked(Entity ent, ref SprayPainterSpritePickedMessage args)
+ private void OnPainterGetAltVerbs(Entity ent, ref GetVerbsEvent args)
{
- if (args.Index >= Styles.Count)
+ if (!args.CanAccess || !args.CanInteract || !args.Using.HasValue)
return;
- ent.Comp.Index = args.Index;
- Dirty(ent, ent.Comp);
+ var user = args.User;
+
+ AlternativeVerb verb = new()
+ {
+ Text = Loc.GetString("spray-painter-verb-toggle-decals"),
+ Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
+ Act = () => TogglePaintDecals(ent, user),
+ Impact = LogImpact.Low
+ };
+ args.Verbs.Add(verb);
}
- private void SetColor(Entity ent, string? paletteKey)
+ ///
+ /// Toggles whether clicking on the floor paints a decal or not.
+ ///
+ private void TogglePaintDecals(Entity ent, EntityUid user)
{
- if (paletteKey == null || paletteKey == ent.Comp.PickedColor)
+ if (!_timing.IsFirstTimePredicted)
return;
- if (!ent.Comp.ColorPalette.ContainsKey(paletteKey))
- return;
+ var pitch = 1.0f;
+ switch (ent.Comp.DecalMode)
+ {
+ case DecalPaintMode.Off:
+ default:
+ ent.Comp.DecalMode = DecalPaintMode.Add;
+ pitch = 1.0f;
+ break;
+ case DecalPaintMode.Add:
+ ent.Comp.DecalMode = DecalPaintMode.Remove;
+ pitch = 1.2f;
+ break;
+ case DecalPaintMode.Remove:
+ ent.Comp.DecalMode = DecalPaintMode.Off;
+ pitch = 0.8f;
+ break;
+ }
+ Dirty(ent);
- ent.Comp.PickedColor = paletteKey;
- Dirty(ent, ent.Comp);
+ // Make the machine beep.
+ Audio.PlayPredicted(ent.Comp.SoundSwitchDecalMode, ent, user, ent.Comp.SoundSwitchDecalMode.Params.WithPitchScale(pitch));
}
- #endregion
-
- private void OnAirlockInteract(Entity ent, ref InteractUsingEvent args)
+ ///
+ /// Handles spray paint interactions with an object.
+ /// An object must belong to a spray paintable group to be painted, and the painter must have sufficient ammo to paint it.
+ ///
+ private void OnPaintableInteract(Entity ent, ref InteractUsingEvent args)
{
if (args.Handled)
return;
@@ -117,79 +186,154 @@ private void OnAirlockInteract(Entity ent, ref Intera
if (!TryComp(args.Used, out var painter))
return;
- var group = Proto.Index(ent.Comp.Group);
+ if (ent.Comp.Group is not { } group
+ || !painter.StylesByGroup.TryGetValue(group, out var selectedStyle)
+ || !_protoMan.Resolve(group, out PaintableGroupPrototype? targetGroup))
+ return;
+
+ // Valid paint target.
+ args.Handled = true;
+
+ if (!TryComp(args.Used, out var charges))
+ return;
+
+ if (Charges.IsEmpty(args.Used, charges) || Charges.HasInsufficientCharges(args.Used, targetGroup.Cost, charges))
+ {
+ var msg = Loc.GetString("spray-painter-interact-no-charges");
+ _popup.PopupEntity(msg, args.User, args.User);
+ return;
+ }
- var style = Styles[painter.Index];
- if (!group.StylePaths.TryGetValue(style.Name, out var sprite))
+ if (!targetGroup.Styles.TryGetValue(selectedStyle, out var proto))
{
- string msg = Loc.GetString("spray-painter-style-not-available");
- _popup.PopupClient(msg, args.User, args.User);
+ var msg = Loc.GetString("spray-painter-style-not-available");
+ _popup.PopupEntity(msg, args.User, args.User);
return;
}
- var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, painter.AirlockSprayTime, new SprayPainterDoorDoAfterEvent(sprite, style.Department), args.Used, target: ent, used: args.Used)
+ var doAfterEventArgs = new DoAfterArgs(EntityManager,
+ args.User,
+ targetGroup.Time,
+ new SprayPainterDoAfterEvent(proto, group, targetGroup.Cost),
+ args.Used,
+ target: ent,
+ used: args.Used)
{
BreakOnMove = true,
BreakOnDamage = true,
NeedHand = true,
};
- if (!DoAfter.TryStartDoAfter(doAfterEventArgs, out var id))
- return;
- args.Handled = true;
+ if (!DoAfter.TryStartDoAfter(doAfterEventArgs, out _))
+ return;
// Log the attempt
- _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} is painting {ToPrettyString(ent):target} to '{style.Name}' at {Transform(ent).Coordinates:targetlocation}");
+ AdminLogger.Add(LogType.Action,
+ LogImpact.Low,
+ $"{ToPrettyString(args.User):user} is painting {ToPrettyString(ent):target} to '{selectedStyle}' at {Transform(ent).Coordinates:targetlocation}");
+ }
+
+ ///
+ /// Prints out if an object has been painted recently.
+ ///
+ private void OnPainedExamined(Entity ent, ref ExaminedEvent args)
+ {
+ // If the paint's dried, it isn't detectable.
+ if (_timing.CurTime > ent.Comp.DryTime)
+ return;
+
+ args.PushText(Loc.GetString("spray-painter-on-examined-painted-message"));
}
- #region Style caching
+ #endregion Interaction
+
+ #region UI
- private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
+ ///
+ /// Sets the style that a particular type of paintable object (e.g. lockers) should be painted in.
+ ///
+ private void OnSetPaintable(Entity ent, ref SprayPainterSetPaintableStyleMessage args)
{
- if (!args.WasModified() && !args.WasModified())
+ if (!ent.Comp.StylesByGroup.ContainsKey(args.Group))
return;
- Styles.Clear();
- Groups.Clear();
- CacheStyles();
+ ent.Comp.StylesByGroup[args.Group] = args.Style;
+ Dirty(ent);
+ UpdateUi(ent);
+ }
- // style index might be invalid now so check them all
- var max = Styles.Count - 1;
- var query = AllEntityQuery();
- while (query.MoveNext(out var uid, out var comp))
- {
- if (comp.Index > max)
- {
- comp.Index = max;
- Dirty(uid, comp);
- }
- }
+ ///
+ /// Changes the color to paint pipes in.
+ ///
+ private void OnSetPipeColor(Entity ent, ref SprayPainterSetPipeColorMessage args)
+ {
+ SetPipeColor(ent, args.Key);
}
- protected virtual void CacheStyles()
+ ///
+ /// Tracks the tab the spray painter was on.
+ ///
+ private void OnTabChanged(Entity ent, ref SprayPainterTabChangedMessage args)
{
- // collect every style's name
- var names = new SortedSet();
- foreach (var group in Proto.EnumeratePrototypes())
- {
- Groups.Add(group);
- foreach (var style in group.StylePaths.Keys)
- {
- names.Add(style);
- }
- }
+ ent.Comp.SelectedTab = args.Index;
+ Dirty(ent);
+ }
- // get their department ids too for the final style list
- var departments = Proto.Index(Departments);
- Styles.Capacity = names.Count;
- foreach (var name in names)
- {
- departments.Departments.TryGetValue(name, out var department);
- Styles.Add(new AirlockStyle(name, department));
- }
+ ///
+ /// Sets the decal prototype to paint.
+ ///
+ private void OnSetDecal(Entity ent, ref SprayPainterSetDecalMessage args)
+ {
+ ent.Comp.SelectedDecal = args.DecalPrototype;
+ Dirty(ent);
+ UpdateUi(ent);
+ }
+
+ ///
+ /// Sets the angle to paint decals at.
+ ///
+ private void OnSetDecalAngle(Entity ent, ref SprayPainterSetDecalAngleMessage args)
+ {
+ ent.Comp.SelectedDecalAngle = args.Angle;
+ Dirty(ent);
+ UpdateUi(ent);
+ }
+
+ ///
+ /// Enables or disables snap-to-grid when painting decals.
+ ///
+ private void OnSetDecalSnap(Entity ent, ref SprayPainterSetDecalSnapMessage args)
+ {
+ ent.Comp.SnapDecals = args.Snap;
+ Dirty(ent);
+ UpdateUi(ent);
+ }
+
+ ///
+ /// Enables or disables the decal colour picker.
+ ///
+ private void OnSetDecalColorPicker(Entity ent, ref SprayPainterSetDecalColorPickerMessage args)
+ {
+ ent.Comp.ColorPickerEnabled = args.Toggle;
+ Dirty(ent);
+ UpdateUi(ent);
+ }
+
+ ///
+ /// Sets the decal to paint on the ground.
+ ///
+ private void OnSetDecalColor(Entity ent, ref SprayPainterSetDecalColorMessage args)
+ {
+ ent.Comp.SelectedDecalColor = args.Color;
+ Dirty(ent);
+ UpdateUi(ent);
+ }
+
+ protected virtual void UpdateUi(Entity ent)
+ {
}
#endregion
}
-
-public record struct AirlockStyle(string Name, string? Department);
+/// Forge-Chane-End
+///
diff --git a/Content.Shared/SprayPainter/SprayPainterEvents.cs b/Content.Shared/SprayPainter/SprayPainterEvents.cs
index b88b054ad14..449efc60e93 100644
--- a/Content.Shared/SprayPainter/SprayPainterEvents.cs
+++ b/Content.Shared/SprayPainter/SprayPainterEvents.cs
@@ -1,4 +1,8 @@
+/// Forge-Chane-Start
+using Content.Shared.Decals;
using Content.Shared.DoAfter;
+using Content.Shared.SprayPainter.Prototypes;
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.SprayPainter;
@@ -10,46 +14,81 @@ public enum SprayPainterUiKey
}
[Serializable, NetSerializable]
-public sealed class SprayPainterSpritePickedMessage : BoundUserInterfaceMessage
+public sealed class SprayPainterSetDecalMessage(ProtoId protoId) : BoundUserInterfaceMessage
{
- public readonly int Index;
+ public ProtoId DecalPrototype = protoId;
+}
- public SprayPainterSpritePickedMessage(int index)
- {
- Index = index;
- }
+[Serializable, NetSerializable]
+public sealed class SprayPainterSetDecalColorMessage(Color? color) : BoundUserInterfaceMessage
+{
+ public Color? Color = color;
}
[Serializable, NetSerializable]
-public sealed class SprayPainterColorPickedMessage : BoundUserInterfaceMessage
+public sealed class SprayPainterSetDecalSnapMessage(bool snap) : BoundUserInterfaceMessage
{
- public readonly string? Key;
+ public bool Snap = snap;
+}
- public SprayPainterColorPickedMessage(string? key)
- {
- Key = key;
- }
+[Serializable, NetSerializable]
+public sealed class SprayPainterSetDecalAngleMessage(int angle) : BoundUserInterfaceMessage
+{
+ public int Angle = angle;
+}
+
+[Serializable, NetSerializable]
+public sealed class SprayPainterTabChangedMessage(int index, bool isSelectedTabWithDecals) : BoundUserInterfaceMessage
+{
+ public readonly int Index = index;
+ public readonly bool IsSelectedTabWithDecals = isSelectedTabWithDecals;
}
[Serializable, NetSerializable]
-public sealed partial class SprayPainterDoorDoAfterEvent : DoAfterEvent
+public sealed class SprayPainterSetPaintableStyleMessage(string group, string style) : BoundUserInterfaceMessage
+{
+ public readonly string Group = group;
+ public readonly string Style = style;
+}
+
+[Serializable, NetSerializable]
+public sealed class SprayPainterSetPipeColorMessage(string? key) : BoundUserInterfaceMessage
+{
+ public readonly string? Key = key;
+}
+
+[Serializable, NetSerializable]
+public sealed class SprayPainterSetDecalColorPickerMessage(bool toggle) : BoundUserInterfaceMessage
+{
+ public bool Toggle = toggle;
+}
+
+[Serializable, NetSerializable]
+public sealed partial class SprayPainterDoAfterEvent : DoAfterEvent
{
///
- /// Base RSI path to set for the door sprite.
+ /// The prototype to use to repaint this object.
///
[DataField]
- public string Sprite;
+ public string Prototype;
///
- /// Department id to set for the door, if the style has one.
+ /// The group ID of the object being painted.
///
[DataField]
- public string? Department;
+ public string Group;
- public SprayPainterDoorDoAfterEvent(string sprite, string? department)
+ ///
+ /// The cost, in charges, to paint this object.
+ ///
+ [DataField]
+ public int Cost;
+
+ public SprayPainterDoAfterEvent(string prototype, string group, int cost)
{
- Sprite = sprite;
- Department = department;
+ Prototype = prototype;
+ Group = group;
+ Cost = cost;
}
public override DoAfterEvent Clone() => this;
@@ -71,3 +110,19 @@ public SprayPainterPipeDoAfterEvent(Color color)
public override DoAfterEvent Clone() => this;
}
+
+///
+/// An action raised on an entity when it is spray painted.
+///
+/// The entity painting this item.
+/// The entity used to paint this item.
+/// The prototype used to generate the new painted appearance.
+/// The group of the entity being painted (e.g. airlocks with glass, canisters).
+[ByRefEvent]
+public partial record struct EntityPaintedEvent(
+ EntityUid? User,
+ EntityUid Tool,
+ EntProtoId Prototype,
+ ProtoId Group);
+/// Forge-Chane-End
+///
diff --git a/Resources/Locale/ru-RU/spray-painter/spray-painter.ftl b/Resources/Locale/ru-RU/spray-painter/spray-painter.ftl
new file mode 100644
index 00000000000..d274008c922
--- /dev/null
+++ b/Resources/Locale/ru-RU/spray-painter/spray-painter.ftl
@@ -0,0 +1,194 @@
+# Components
+spray-painter-ammo-on-examine = Содержит {$charges} зарядов.
+spray-painter-ammo-after-interact-full = Краскопульт уже полностью заправлен!
+spray-painter-ammo-after-interact-refilled = Вы заправляете краскопульт.
+
+spray-painter-interact-no-charges = Недостаточно краски.
+spray-painter-interact-nothing-to-remove = Здесь нечего удалять!
+
+spray-painter-on-examined-painted-message = Похоже, это недавно покрасили.
+spray-painter-style-not-available = Невозможно применить выбранный стиль к этому объекту.
+
+spray-painter-verb-toggle-decals = Переключить режим покраски декалей
+
+spray-painter-item-status-label = Декали: {$mode}
+spray-painter-item-status-add = [color=green]Добавление[/color]
+spray-painter-item-status-remove = [color=red]Удаление[/color]
+spray-painter-item-status-off = [color=gray]Выкл[/color]
+
+# UI
+spray-painter-window-title = Краскопульт
+
+spray-painter-selected-style = Выбранный стиль:
+
+spray-painter-selected-decals = Выбранная декаль:
+spray-painter-use-custom-color = Использовать свой цвет
+spray-painter-use-snap-to-tile = Привязка к плитке
+
+spray-painter-angle-rotation = Поворот:
+spray-painter-angle-rotation-90-sub = -90°
+spray-painter-angle-rotation-reset = 0°
+spray-painter-angle-rotation-90-add = +90°
+
+spray-painter-selected-color = Выбранный цвет:
+spray-painter-color-red = красный
+spray-painter-color-yellow = жёлтый
+spray-painter-color-brown = коричневый
+spray-painter-color-green = зелёный
+spray-painter-color-cyan = голубой
+spray-painter-color-blue = синий
+spray-painter-color-white = белый
+spray-painter-color-black = чёрный
+
+# Categories (tabs)
+spray-painter-tab-category-airlocks = Шлюзы
+spray-painter-tab-category-canisters = Канистры
+spray-painter-tab-category-crates = Ящики
+spray-painter-tab-category-lockers = Шкафчики
+spray-painter-tab-category-pipes = Трубы
+spray-painter-tab-category-decals = Декали
+
+# Groups (subtabs)
+spray-painter-tab-group-airlockstandard = Стандартные
+spray-painter-tab-group-airlockglass = Стеклянные
+
+spray-painter-tab-group-cratesteel = Стальные
+spray-painter-tab-group-crateplastic = Пластиковые
+spray-painter-tab-group-cratesecure = Защищённые
+
+spray-painter-tab-group-closet = Обычные
+spray-painter-tab-group-locker = Защищённые
+spray-painter-tab-group-wallcloset = Обычные (настенные)
+spray-painter-tab-group-walllocker = Защищённые (настенные)
+
+# Airlocks
+spray-painter-style-airlockstandard-atmospherics = Атмосферный
+spray-painter-style-airlockstandard-basic = Базовый
+spray-painter-style-airlockstandard-cargo = Карго
+spray-painter-style-airlockstandard-chemistry = Химия
+spray-painter-style-airlockstandard-command = Командование
+spray-painter-style-airlockstandard-engineering = Инженерный
+spray-painter-style-airlockstandard-freezer = Морозильник
+spray-painter-style-airlockstandard-hydroponics = Гидропоника
+spray-painter-style-airlockstandard-maintenance = Техобслуживание
+spray-painter-style-airlockstandard-medical = Медицинский
+spray-painter-style-airlockstandard-salvage = Утилизаторский
+spray-painter-style-airlockstandard-science = Научный
+spray-painter-style-airlockstandard-security = Служба безопасности
+spray-painter-style-airlockstandard-virology = Вирусология
+
+spray-painter-style-airlockglass-atmospherics = Атмосферный
+spray-painter-style-airlockglass-basic = Базовый
+spray-painter-style-airlockglass-cargo = Карго
+spray-painter-style-airlockglass-chemistry = Химия
+spray-painter-style-airlockglass-command = Командование
+spray-painter-style-airlockglass-engineering = Инженерный
+spray-painter-style-airlockglass-hydroponics = Гидропоника
+spray-painter-style-airlockglass-maintenance = Техобслуживание
+spray-painter-style-airlockglass-medical = Медицинский
+spray-painter-style-airlockglass-salvage = Утилизаторский
+spray-painter-style-airlockglass-science = Научный
+spray-painter-style-airlockglass-security = Служба безопасности
+spray-painter-style-airlockglass-virology = Вирусология
+
+# Lockers
+spray-painter-style-locker-atmospherics = Атмосферный
+spray-painter-style-locker-basic = Базовый
+spray-painter-style-locker-botanist = Ботаник
+spray-painter-style-locker-brigmedic = Бригмедик
+spray-painter-style-locker-captain = Капитан
+spray-painter-style-locker-ce = Старший инженер
+spray-painter-style-locker-chemical = Химик
+spray-painter-style-locker-clown = Клоун
+spray-painter-style-locker-cmo = Главный врач
+spray-painter-style-locker-doctor = Врач
+spray-painter-style-locker-electrical = Электрик
+spray-painter-style-locker-engineer = Инженер
+spray-painter-style-locker-evac = Ремонт эвакуации
+spray-painter-style-locker-hop = Глава персонала
+spray-painter-style-locker-hos = Глава службы безопасности
+spray-painter-style-locker-medicine = Медицина
+spray-painter-style-locker-mime = Мим
+spray-painter-style-locker-paramedic = Парамедик
+spray-painter-style-locker-quartermaster = Квартирмейстер
+spray-painter-style-locker-rd = Научный руководитель
+spray-painter-style-locker-representative = Представитель
+spray-painter-style-locker-salvage = Утилизатор
+spray-painter-style-locker-scientist = Учёный
+spray-painter-style-locker-security = Служба безопасности
+spray-painter-style-locker-welding = Сварка
+
+spray-painter-style-closet-basic = Базовый
+spray-painter-style-closet-biohazard = Биологическая опасность
+spray-painter-style-closet-biohazard-science = Биологическая опасность (наука)
+spray-painter-style-closet-biohazard-virology = Биологическая опасность (вирусология)
+spray-painter-style-closet-biohazard-security = Биологическая опасность (СБ)
+spray-painter-style-closet-biohazard-janitor = Биологическая опасность (уборщик)
+spray-painter-style-closet-bomb = Костюм сапёра
+spray-painter-style-closet-bomb-janitor = Костюм сапёра (уборщик)
+spray-painter-style-closet-chef = Повар
+spray-painter-style-closet-fire = Пожарная безопасность
+spray-painter-style-closet-janitor = Уборщик
+spray-painter-style-closet-legal = Юрист
+spray-painter-style-closet-nitrogen = Внутренняя атмосфера (азот)
+spray-painter-style-closet-oxygen = Внутренняя атмосфера (кислород)
+spray-painter-style-closet-radiation = Радиационный костюм
+spray-painter-style-closet-tool = Инструменты
+
+spray-painter-style-wallcloset-atmospherics = Атмосферный
+spray-painter-style-wallcloset-basic = Базовый
+spray-painter-style-wallcloset-black = Чёрный
+spray-painter-style-wallcloset-blue = Синий
+spray-painter-style-wallcloset-fire = Пожарная безопасность
+spray-painter-style-wallcloset-green = Зелёный
+spray-painter-style-wallcloset-grey = Серый
+spray-painter-style-wallcloset-mixed = Смешанный
+spray-painter-style-wallcloset-nitrogen = Внутренняя атмосфера (азот)
+spray-painter-style-wallcloset-orange = Оранжевый
+spray-painter-style-wallcloset-oxygen = Внутренняя атмосфера (кислород)
+spray-painter-style-wallcloset-pink = Розовый
+spray-painter-style-wallcloset-white = Белый
+spray-painter-style-wallcloset-yellow = Жёлтый
+
+spray-painter-style-walllocker-evac = Ремонт эвакуации
+spray-painter-style-walllocker-medical = Медицинский
+
+# Crates
+spray-painter-style-cratesteel-basic = Базовый
+spray-painter-style-cratesteel-electrical = Электрика
+spray-painter-style-cratesteel-engineering = Инженерный
+spray-painter-style-cratesteel-radiation = Радиация
+spray-painter-style-cratesteel-science = Научный
+spray-painter-style-cratesteel-surgery = Хирургия
+
+spray-painter-style-crateplastic-basic = Базовый
+spray-painter-style-crateplastic-chemistry = Химия
+spray-painter-style-crateplastic-command = Командование
+spray-painter-style-crateplastic-hydroponics = Гидропоника
+spray-painter-style-crateplastic-medical = Медицинский
+spray-painter-style-crateplastic-oxygen = Кислород
+
+spray-painter-style-cratesecure-basic = Базовый
+spray-painter-style-cratesecure-chemistry = Химия
+spray-painter-style-cratesecure-command = Командование
+spray-painter-style-cratesecure-engineering = Инженерный
+spray-painter-style-cratesecure-hydroponics = Гидропоника
+spray-painter-style-cratesecure-medical = Медицинский
+spray-painter-style-cratesecure-plasma = Плазма
+spray-painter-style-cratesecure-private = Частный
+spray-painter-style-cratesecure-science = Научный
+spray-painter-style-cratesecure-secgear = Снаряжение СБ
+spray-painter-style-cratesecure-weapon = Оружие
+
+# Canisters
+spray-painter-style-canisters-air = Воздух
+spray-painter-style-canisters-ammonia = Аммиак
+spray-painter-style-canisters-carbon-dioxide = Углекислый газ
+spray-painter-style-canisters-frezon = Фрезон
+spray-painter-style-canisters-nitrogen = Азот
+spray-painter-style-canisters-nitrous-oxide = Оксид Азота
+spray-painter-style-canisters-oxygen = Кислород
+spray-painter-style-canisters-plasma = Плазма
+spray-painter-style-canisters-storage = Хранилище
+spray-painter-style-canisters-tritium = Тритий
+spray-painter-style-canisters-water-vapor = Водяной пар
diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/youtool.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/youtool.yml
index 0f16e366c09..24858d48757 100644
--- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/youtool.yml
+++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/youtool.yml
@@ -17,6 +17,7 @@
trayScanner: 15
GasAnalyzer: 15
SprayPainter: 15
+ SprayPainterAmmo: 30 # Forge-Change
# Some engineer forgot to take the multitool out the youtool when working on it, happens.
contrabandInventory:
Multitool: 5
diff --git a/Resources/Prototypes/Entities/Objects/Tools/spray_painter.yml b/Resources/Prototypes/Entities/Objects/Tools/spray_painter.yml
index d2154326783..c49a2aabebf 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/spray_painter.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/spray_painter.yml
@@ -2,7 +2,7 @@
parent: [BaseItem, RecyclableItemDeviceSmall] # Frontier: added RecyclableItemDeviceSmall
id: SprayPainter
name: spray painter
- description: A spray painter for painting airlocks and pipes.
+ description: A spray painter for painting airlocks, pipes, and other items.
components:
- type: Sprite
sprite: Objects/Tools/spray_painter.rsi
@@ -30,5 +30,63 @@
distro: '#0055cc'
air: '#03fcd3'
mix: '#947507'
+ # Forge-Change-Start
- type: StaticPrice
- price: 20 # Frontier 40<20
+ price: 40
+ - type: LimitedCharges
+ maxCharges: 30
+ charges: 30
+ - type: PhysicalComposition
+ materialComposition:
+ Steel: 100
+
+- type: entity
+ parent: SprayPainter
+ id: SprayPainterRecharging
+ suffix: Admeme
+ components:
+ - type: AutoRecharge
+ rechargeDuration: 1
+
+- type: entity
+ parent: SprayPainter
+ name: experimental spray painter
+ description: An experimental recharging spray painter that can infinitely replicate compressed paint.
+ id: SprayPainterBorg
+ suffix: Borg
+ components:
+ - type: AutoRecharge
+ rechargeDuration: 5
+
+- type: entity
+ parent: SprayPainter
+ id: SprayPainterEmpty
+ suffix: Empty
+ components:
+ - type: LimitedCharges
+ charges: 0
+
+- type: entity
+ parent: BaseItem
+ id: SprayPainterAmmo
+ name: compressed paint
+ description: A cartridge of highly compressed paint, commonly used in spray painters.
+ components:
+ - type: LimitedChargesAmmo
+ charges: 30
+ whitelist:
+ components:
+ - SprayPainter
+ - type: Sprite
+ sprite: Objects/Tools/spray_painter.rsi
+ state: ammo
+ - type: Item
+ sprite: Objects/Tools/spray_painter.rsi
+ heldPrefix: ammo
+ - type: PhysicalComposition
+ materialComposition:
+ Steel: 10
+ Plastic: 10
+ - type: StaticPrice
+ price: 30
+ # Forge-Change-End
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
index ae3f399ea37..3e5e04912e8 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
@@ -15,8 +15,8 @@
components:
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Standard/engineering.rsi # Forge-Change
- - type: PaintableAirlock
- department: Engineering
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Engineering # Forge-Change-Del
- type: Wires
layoutId: AirlockEngineering
@@ -35,8 +35,8 @@
components:
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Standard/cargo.rsi # Forge-Change
- - type: PaintableAirlock
- department: Cargo
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Cargo # Forge-Change-Del
- type: Wires
layoutId: AirlockCargo
@@ -57,8 +57,8 @@
components:
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Standard/medical.rsi # Forge-Change
- - type: PaintableAirlock
- department: Medical
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Medical # Forge-Change-Del
- type: Wires
layoutId: AirlockMedical
@@ -85,8 +85,8 @@
components:
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Standard/science.rsi # Forge-Change
- - type: PaintableAirlock
- department: Science
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Science # Forge-Change-Del
- type: Wires
layoutId: AirlockScience
@@ -99,8 +99,8 @@
sprite: Forge/Structures/Doors/Airlocks/Standard/command.rsi # Forge-Change
- type: WiresPanelSecurity
securityLevel: medSecurity
- - type: PaintableAirlock
- department: Command
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Command # Forge-Change-Del
- type: Wires
layoutId: AirlockCommand
@@ -111,8 +111,8 @@
components:
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Standard/security.rsi # Forge-Change
- - type: PaintableAirlock
- department: Security
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Security # Forge-Change-Del
- type: Wires
layoutId: AirlockSecurity
@@ -174,8 +174,8 @@
components:
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Glass/engineering.rsi # Forge-Change
- - type: PaintableAirlock
- department: Engineering
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Engineering # Forge-Change-Del
- type: Wires
layoutId: AirlockEngineering
@@ -202,8 +202,8 @@
components:
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Glass/cargo.rsi # Forge-Change
- - type: PaintableAirlock
- department: Cargo
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Cargo # Forge-Change-Del
- type: Wires
layoutId: AirlockCargo
@@ -224,8 +224,8 @@
components:
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Glass/medical.rsi # Forge-Change
- - type: PaintableAirlock
- department: Medical
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Medical # Forge-Change-Del
- type: Wires
layoutId: AirlockMedical
@@ -252,8 +252,8 @@
components:
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Glass/science.rsi # Forge-Change
- - type: PaintableAirlock
- department: Science
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Science # Forge-Change-Del
- type: Wires
layoutId: AirlockScience
@@ -264,8 +264,8 @@
components:
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Glass/command.rsi # Forge-Change
- - type: PaintableAirlock
- department: Command
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Command # Forge-Change-Del
- type: WiresPanelSecurity
securityLevel: medSecurity
- type: Wires
@@ -278,8 +278,8 @@
components:
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Glass/security.rsi # Forge-Change
- - type: PaintableAirlock
- department: Security
+ # - type: PaintableAirlock # Forge-Change-Del
+ # department: Security # Forge-Change-Del
- type: Wires
layoutId: AirlockSecurity
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
index f0f7e2e49d5..5d710a76b7d 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
@@ -173,9 +173,13 @@
- type: IconSmooth
key: walls
mode: NoSprite
- - type: PaintableAirlock
- group: Standard
- department: Civilian
+ # Forge-Change-Start
+ # - type: PaintableAirlock
+ # group: Standard
+ # department: Civilian
+ - type: Paintable
+ group: AirlockStandard
+ # Forge-Change-End
- type: StaticPrice
price: 150
- type: LightningTarget
@@ -239,8 +243,12 @@
- type: Construction
graph: Airlock
node: glassAirlock
- - type: PaintableAirlock
- group: Glass
+ # Forge-Change-Start
+ # - type: PaintableAirlock
+ # group: Glass
+ - type: Paintable
+ group: AirlockGlass
+ # Forge-Change-End
- type: RadiationBlocker
resistance: 2
- type: Tag
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/clockwork.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/clockwork.yml
index 9cb598e90ea..8ba77c1a647 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/clockwork.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/clockwork.yml
@@ -10,6 +10,8 @@
node: airlock
containers:
- board
+ - type: Paintable # Forge-Change
+ group: null # Forge-Change
- type: entity
parent: AirlockGlass
@@ -25,3 +27,5 @@
- board
# - type: StaticPrice # Frontier - TODO: material value rework
# price: 165 # Frontier
+- type: Paintable # Forge-Change
+ group: null # Forge-Change
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/external.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/external.yml
index ea96ec131b3..f01c6187146 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/external.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/external.yml
@@ -16,9 +16,13 @@
path: /Audio/Machines/airlock_deny.ogg
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Standard/external.rsi # Forge-Change
- - type: PaintableAirlock
- group: External
- department: null
+ # Forge-Change-Start
+ # - type: PaintableAirlock
+ # group: External
+ # department: null
+ - type: Paintable
+ group: null
+ # Forge-Change-End
- type: Wires
layoutId: AirlockExternal
@@ -33,8 +37,8 @@
enabled: false
- type: Sprite
sprite: Forge/Structures/Doors/Airlocks/Glass/external.rsi # Forge-Change
- - type: PaintableAirlock
- group: ExternalGlass
+ # - type: PaintableAirlock # Forge-Change-Del
+ # group: ExternalGlass # Forge-Change-Del
- type: Fixtures
fixtures:
fix1:
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml
index c5a6b268f97..778d3e28098 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml
@@ -81,9 +81,13 @@
- type: Tag
tags:
- ForceNoFixRotations
- - type: PaintableAirlock
- group: Shuttle
- department: null
+ # Forge-Change-Start
+ # - type: PaintableAirlock
+ # group: Shuttle
+ # department: null
+ - type: Paintable
+ group: null
+ # Forge-Change-End
- type: Construction
graph: AirlockShuttle
node: airlock
@@ -113,8 +117,12 @@
sprite: Forge/Structures/Doors/Airlocks/Glass/shuttle.rsi # Forge-Change
- type: Occluder
enabled: false
- - type: PaintableAirlock
- group: ShuttleGlass
+ # Forge-Change-Start
+ # - type: PaintableAirlock
+ # group: ShuttleGlass
+ - type: Paintable
+ group: null
+ # Forge-Change-End
- type: Door
occludes: false
- type: Fixtures
diff --git a/Resources/Prototypes/Entities/Structures/Doors/airlock_groups.yml b/Resources/Prototypes/Entities/Structures/Doors/airlock_groups.yml
index b74a819a784..1f5438d5d9d 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/airlock_groups.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/airlock_groups.yml
@@ -1,86 +1,88 @@
-- type: AirlockGroup
- id: Standard
- iconPriority: 100
- stylePaths:
- atmospherics: Forge/Structures/Doors/Airlocks/Standard/atmospherics.rsi # Forge-Change
- basic: Forge/Structures/Doors/Airlocks/Standard/basic.rsi # Forge-Change
- cargo: Forge/Structures/Doors/Airlocks/Standard/cargo.rsi # Forge-Change
- chemistry: Forge/Structures/Doors/Airlocks/Standard/chemistry.rsi # Forge-Change
- command: Forge/Structures/Doors/Airlocks/Standard/command.rsi # Forge-Change
- engineering: Forge/Structures/Doors/Airlocks/Standard/engineering.rsi # Forge-Change
- freezer: Forge/Structures/Doors/Airlocks/Standard/freezer.rsi # Forge-Change
- hydroponics: Forge/Structures/Doors/Airlocks/Standard/hydroponics.rsi # Forge-Change
- maintenance: Forge/Structures/Doors/Airlocks/Standard/maint.rsi # Forge-Change
- medical: Forge/Structures/Doors/Airlocks/Standard/medical.rsi # Forge-Change
- science: Forge/Structures/Doors/Airlocks/Standard/science.rsi # Forge-Change
- security: Forge/Structures/Doors/Airlocks/Standard/security.rsi # Forge-Change
- virology: Forge/Structures/Doors/Airlocks/Standard/virology.rsi # Forge-Change
- mercenary: Forge/Structures/Doors/Airlocks/Standard/mercenary.rsi # Forge-Change
+# Forge-Change-Start
+# - type: AirlockGroup
+# id: Standard
+# iconPriority: 100
+# stylePaths:
+# atmospherics: Forge/Structures/Doors/Airlocks/Standard/atmospherics.rsi # Forge-Change
+# basic: Forge/Structures/Doors/Airlocks/Standard/basic.rsi # Forge-Change
+# cargo: Forge/Structures/Doors/Airlocks/Standard/cargo.rsi # Forge-Change
+# chemistry: Forge/Structures/Doors/Airlocks/Standard/chemistry.rsi # Forge-Change
+# command: Forge/Structures/Doors/Airlocks/Standard/command.rsi # Forge-Change
+# engineering: Forge/Structures/Doors/Airlocks/Standard/engineering.rsi # Forge-Change
+# freezer: Forge/Structures/Doors/Airlocks/Standard/freezer.rsi # Forge-Change
+# hydroponics: Forge/Structures/Doors/Airlocks/Standard/hydroponics.rsi # Forge-Change
+# maintenance: Forge/Structures/Doors/Airlocks/Standard/maint.rsi # Forge-Change
+# medical: Forge/Structures/Doors/Airlocks/Standard/medical.rsi # Forge-Change
+# science: Forge/Structures/Doors/Airlocks/Standard/science.rsi # Forge-Change
+# security: Forge/Structures/Doors/Airlocks/Standard/security.rsi # Forge-Change
+# virology: Forge/Structures/Doors/Airlocks/Standard/virology.rsi # Forge-Change
+# mercenary: Forge/Structures/Doors/Airlocks/Standard/mercenary.rsi # Forge-Change
-- type: AirlockGroup
- id: Glass
- iconPriority: 90
- stylePaths:
- atmospherics: Forge/Structures/Doors/Airlocks/Glass/atmospherics.rsi # Forge-Change
- basic: Forge/Structures/Doors/Airlocks/Glass/basic.rsi # Forge-Change
- cargo: Forge/Structures/Doors/Airlocks/Glass/cargo.rsi # Forge-Change
- command: Forge/Structures/Doors/Airlocks/Glass/command.rsi # Forge-Change
- chemistry: Forge/Structures/Doors/Airlocks/Glass/chemistry.rsi # Forge-Change
- science: Forge/Structures/Doors/Airlocks/Glass/science.rsi # Forge-Change
- engineering: Forge/Structures/Doors/Airlocks/Glass/engineering.rsi # Forge-Change
- glass: Forge/Structures/Doors/Airlocks/Glass/glass.rsi # Forge-Change
- hydroponics: Forge/Structures/Doors/Airlocks/Glass/hydroponics.rsi # Forge-Change
- maintenance: Forge/Structures/Doors/Airlocks/Glass/maint.rsi # Forge-Change
- medical: Forge/Structures/Doors/Airlocks/Glass/medical.rsi # Forge-Change
- security: Forge/Structures/Doors/Airlocks/Glass/security.rsi # Forge-Change
- virology: Forge/Structures/Doors/Airlocks/Glass/virology.rsi # Forge-Change
- mercenary: Forge/Structures/Doors/Airlocks/Glass/mercenary.rsi # Forge-Change
+# - type: AirlockGroup
+# id: Glass
+# iconPriority: 90
+# stylePaths:
+# atmospherics: Forge/Structures/Doors/Airlocks/Glass/atmospherics.rsi # Forge-Change
+# basic: Forge/Structures/Doors/Airlocks/Glass/basic.rsi # Forge-Change
+# cargo: Forge/Structures/Doors/Airlocks/Glass/cargo.rsi # Forge-Change
+# command: Forge/Structures/Doors/Airlocks/Glass/command.rsi # Forge-Change
+# chemistry: Forge/Structures/Doors/Airlocks/Glass/chemistry.rsi # Forge-Change
+# science: Forge/Structures/Doors/Airlocks/Glass/science.rsi # Forge-Change
+# engineering: Forge/Structures/Doors/Airlocks/Glass/engineering.rsi # Forge-Change
+# glass: Forge/Structures/Doors/Airlocks/Glass/glass.rsi # Forge-Change
+# hydroponics: Forge/Structures/Doors/Airlocks/Glass/hydroponics.rsi # Forge-Change
+# maintenance: Forge/Structures/Doors/Airlocks/Glass/maint.rsi # Forge-Change
+# medical: Forge/Structures/Doors/Airlocks/Glass/medical.rsi # Forge-Change
+# security: Forge/Structures/Doors/Airlocks/Glass/security.rsi # Forge-Change
+# virology: Forge/Structures/Doors/Airlocks/Glass/virology.rsi # Forge-Change
+# mercenary: Forge/Structures/Doors/Airlocks/Glass/mercenary.rsi # Forge-Change
-- type: AirlockGroup
- id: Windoor
- iconPriority: 80
- stylePaths:
- basic: Forge/Structures/Doors/Airlocks/Glass/glass.rsi # Forge-Change
+# - type: AirlockGroup
+# id: Windoor
+# iconPriority: 80
+# stylePaths:
+# basic: Forge/Structures/Doors/Airlocks/Glass/glass.rsi # Forge-Change
-- type: AirlockGroup
- id: External
- iconPriority: 70
- stylePaths:
- external: Forge/Structures/Doors/Airlocks/Standard/external.rsi # Forge-Change
+# - type: AirlockGroup
+# id: External
+# iconPriority: 70
+# stylePaths:
+# external: Forge/Structures/Doors/Airlocks/Standard/external.rsi # Forge-Change
-- type: AirlockGroup
- id: ExternalGlass
- iconPriority: 60
- stylePaths:
- external: Forge/Structures/Doors/Airlocks/Glass/external.rsi # Forge-Change
+# - type: AirlockGroup
+# id: ExternalGlass
+# iconPriority: 60
+# stylePaths:
+# external: Forge/Structures/Doors/Airlocks/Glass/external.rsi # Forge-Change
-- type: AirlockGroup
- id: Shuttle
- iconPriority: 50
- stylePaths:
- shuttle: Forge/Structures/Doors/Airlocks/Standard/shuttle.rsi # Forge-Change
+# - type: AirlockGroup
+# id: Shuttle
+# iconPriority: 50
+# stylePaths:
+# shuttle: Forge/Structures/Doors/Airlocks/Standard/shuttle.rsi # Forge-Change
-- type: AirlockGroup
- id: ShuttleGlass
- iconPriority: 40
- stylePaths:
- shuttle: Forge/Structures/Doors/Airlocks/Glass/shuttle.rsi # Forge-Change
+# - type: AirlockGroup
+# id: ShuttleGlass
+# iconPriority: 40
+# stylePaths:
+# shuttle: Forge/Structures/Doors/Airlocks/Glass/shuttle.rsi # Forge-Change
-# fun
-- type: airlockDepartments
- id: Departments
- departments:
- atmospherics: Engineering
- basic: Civilian
- cargo: Cargo
- chemistry: Medical
- command: Command
- engineering: Engineering
- freezer: Civilian
- glass: Civilian
- hydroponics: Civilian
- maintenance: Civilian
- medical: Medical
- science: Science
- security: Security
- virology: Medical
+# # fun
+# - type: airlockDepartments
+# id: Departments
+# departments:
+# atmospherics: Engineering
+# basic: Civilian
+# cargo: Cargo
+# chemistry: Medical
+# command: Command
+# engineering: Engineering
+# freezer: Civilian
+# glass: Civilian
+# hydroponics: Civilian
+# maintenance: Civilian
+# medical: Medical
+# science: Science
+# security: Security
+# virology: Medical
+# Forge-Change-End
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml
index 37aa7010ed7..1d6e5c06cb2 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml
@@ -15,6 +15,8 @@
layers:
- state: air # Frontier
- type: Appearance
+ - type: Paintable # Forge-Change
+ group: Canisters # Forge-Change
- type: GenericVisualizer
visuals:
enum.AnchorVisuals.Anchored:
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/base_structurelockers.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/base_structurelockers.yml
index 8e2d1a6e54c..8a055188ddd 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/base_structurelockers.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/base_structurelockers.yml
@@ -54,6 +54,8 @@
node: done
containers:
- entity_storage
+ - type: Paintable # Forge-Change
+ group: Locker # Forge-Change
- type: entity
id: LockerBaseSecure
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/lockers.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/lockers.yml
index 349eed3ca11..a2366d4c7d5 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/lockers.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/lockers.yml
@@ -16,6 +16,8 @@
path: /Audio/Effects/woodenclosetclose.ogg
openSound:
path: /Audio/Effects/woodenclosetopen.ogg
+ - type: Paintable # Forge-Change
+ group: null # not shaped like other lockers # Forge-Change
# Basic
- type: entity
@@ -174,6 +176,8 @@
node: done
containers:
- entity_storage
+ - type: Paintable # Forge-Change
+ group: null # not shaped like other lockers # Forge-Change
- type: entity
id: LockerFreezer
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/base_structureclosets.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/base_structureclosets.yml
index a610eac873c..31f222e0317 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Closets/base_structureclosets.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/base_structureclosets.yml
@@ -119,6 +119,8 @@
id: ClosetSteelBase
parent: ClosetBase
components:
+ - type: Paintable # Forge-Chane
+ group: Closet # Forge-Chane
- type: Construction
graph: ClosetSteel
node: done
@@ -200,6 +202,8 @@
node: done
containers:
- entity_storage
+ - type: Paintable # Forge-Change
+ group: WallCloset # Forge-Change
#Wall locker
- type: entity
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/wall_lockers.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/wall_lockers.yml
index ac427696bce..b482a7c9dcd 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Closets/wall_lockers.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/wall_lockers.yml
@@ -4,6 +4,8 @@
name: maintenance wall closet
description: It's a storage unit.
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: generic
@@ -16,6 +18,8 @@
parent: BaseWallCloset
description: It's a storage unit for emergency breath masks and O2 tanks.
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: emergency
@@ -28,6 +32,8 @@
parent: BaseWallCloset
description: It's a storage unit for fire-fighting supplies.
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: fire
@@ -40,6 +46,8 @@
name: blue wall closet
description: "A wardrobe packed with stylish blue clothing."
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: generic
@@ -52,6 +60,8 @@
name: pink wall closet
description: "A wardrobe packed with fabulous pink clothing."
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: generic
@@ -64,6 +74,8 @@
name: black wall closet
description: "A wardrobe packed with stylish black clothing."
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: generic
@@ -76,6 +88,8 @@
name: green wall closet
description: "A wardrobe packed with stylish green clothing."
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: generic
@@ -87,6 +101,8 @@
parent: BaseWallCloset
name: prison wall closet
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: generic
@@ -99,6 +115,8 @@
name: yellow wall closet
description: "A wardrobe packed with stylish yellow clothing."
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: generic
@@ -111,6 +129,8 @@
name: white wall closet
description: "A wardrobe packed with stylish white clothing."
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: generic
@@ -123,6 +143,8 @@
name: grey wall closet
description: "A wardrobe packed with a tide of grey clothing."
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: generic
@@ -135,6 +157,8 @@
name: mixed wall closet
description: "A wardrobe packed with a mix of colorful clothing."
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: generic
@@ -146,6 +170,8 @@
parent: BaseWallCloset
name: atmospherics wall closet
components:
+ - type: Paintable # Forge-Chane
+ group: WallCloset # Forge-Chane
- type: Appearance
- type: EntityStorageVisuals
stateBaseClosed: generic
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml b/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml
index abb5e2d605e..2d343bf4ff8 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml
@@ -108,6 +108,8 @@
id: CrateBaseSecure
suffix: Secure
components:
+ - type: Paintable # Forge-Change
+ group: CrateSecure # Forge-Change
- type: Lock
- type: LockVisuals
- type: AccessReader
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml b/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml
index 6bb9dcdbfe9..4193efbe5bb 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml
@@ -12,6 +12,8 @@
- Energy
reflectProb: 0.2
spread: 90
+ - type: Paintable # Forge-Change
+ group: CrateSteel # Forge-Change
- type: RadiationBlockingContainer
resistance: 2.5
@@ -20,6 +22,8 @@
id: CratePlastic
name: plastic crate
components:
+ - type: Paintable # Forge-Change
+ group: CratePlastic # Forge-Change
- type: Icon
sprite: Structures/Storage/Crates/plastic.rsi
- type: Sprite
@@ -66,6 +70,8 @@
node: done
containers:
- entity_storage
+ - type: Paintable # Forge-Change
+ group: null # not shaped like other lockers # Forge-Change
- type: entity
parent: CratePlastic
@@ -633,8 +639,11 @@
sprite: Structures/Storage/Crates/labels.rsi
offset: "0.0,0.03125"
map: ["enum.PaperLabelVisuals.Layer"]
+ - type: Paintable # Forge-Change
+ group: null # not shaped like other lockers # Forge-Change
- type: RatKingRummageable # Forge-Change
+
- type: entity
parent: CrateBaseSecure
id: CrateTrashCartJani
@@ -660,6 +669,8 @@
map: ["enum.PaperLabelVisuals.Layer"]
- type: AccessReader
# access: [["Janitor"]]
+ - type: Paintable # Forge-Change
+ group: null # not shaped like other lockers # Forge-Change
- type: entity
parent: CrateBaseWeldable
diff --git a/Resources/Prototypes/Paintable/airlock_groups.yml b/Resources/Prototypes/Paintable/airlock_groups.yml
new file mode 100644
index 00000000000..10fe4f2c797
--- /dev/null
+++ b/Resources/Prototypes/Paintable/airlock_groups.yml
@@ -0,0 +1,44 @@
+- type: paintableGroup
+ id: AirlockStandard
+ time: 3
+ cost: 3
+ defaultStyle: basic
+ styles:
+ atmospherics: AirlockAtmospherics
+ basic: Airlock
+ cargo: AirlockCargo
+ chemistry: AirlockChemistry
+ command: AirlockCommand
+ engineering: AirlockEngineering
+ freezer: AirlockFreezer
+ hydroponics: AirlockHydroponics
+ maintenance: AirlockMaint
+ medical: AirlockMedical
+ salvage: AirlockSalvageLocked
+ science: AirlockScience
+ security: AirlockSecurity
+ virology: AirlockVirology
+ mercenary: AirlockMercenary # Frontier
+ nfsd: AirlockNfsd # Frontier: surely nothing bad will happen
+
+- type: paintableGroup
+ id: AirlockGlass
+ time: 3
+ cost: 3
+ defaultStyle: basic
+ styles:
+ atmospherics: AirlockAtmosphericsGlass
+ basic: AirlockGlass
+ cargo: AirlockCargoGlass
+ chemistry: AirlockChemistryGlass
+ command: AirlockCommandGlass
+ engineering: AirlockEngineeringGlass
+ hydroponics: AirlockHydroponicsGlass
+ maintenance: AirlockMaintGlass
+ medical: AirlockMedicalGlass
+ salvage: AirlockSalvageGlassLocked
+ science: AirlockScienceGlass
+ security: AirlockSecurityGlass
+ virology: AirlockVirologyGlass
+ mercenary: AirlockMercenaryGlass # Frontier
+ nfsd: AirlockNfsdGlass # Frontier: surely nothing bad will happen
diff --git a/Resources/Prototypes/Paintable/canister_groups.yml b/Resources/Prototypes/Paintable/canister_groups.yml
new file mode 100644
index 00000000000..3b69159cb3e
--- /dev/null
+++ b/Resources/Prototypes/Paintable/canister_groups.yml
@@ -0,0 +1,21 @@
+# Added in Upstream#37341
+
+- type: paintableGroup
+ cost: 2
+ id: Canisters
+ defaultStyle: storage
+ styles:
+ air: AirCanister
+ ammonia: AmmoniaCanister
+ carbon-dioxide: CarbonDioxideCanister
+ frezon: FrezonCanister
+ nitrogen: NitrogenCanister
+ nitrous-oxide: NitrousOxideCanister
+ oxygen: OxygenCanister
+ plasma: PlasmaCanister
+ storage: StorageCanister
+ tritium: TritiumCanister
+ water-vapor: WaterVaporCanister
+ liquid-oxygen: LiquidOxygenCanister # Frontier
+ liquid-nitrogen: LiquidNitrogenCanister # Frontier
+ liquid-carbon-dioxide: LiquidCarbonDioxideCanister # Frontier
diff --git a/Resources/Prototypes/Paintable/categories.yml b/Resources/Prototypes/Paintable/categories.yml
new file mode 100644
index 00000000000..4b8fb4a171b
--- /dev/null
+++ b/Resources/Prototypes/Paintable/categories.yml
@@ -0,0 +1,25 @@
+- type: paintableGroupCategory
+ id: Airlocks
+ groups:
+ - AirlockStandard
+ - AirlockGlass
+
+- type: paintableGroupCategory
+ id: Canisters
+ groups:
+ - Canisters
+
+- type: paintableGroupCategory
+ id: Crates
+ groups:
+ - CrateSteel
+ - CratePlastic
+ - CrateSecure
+
+- type: paintableGroupCategory
+ id: Lockers
+ groups:
+ - Locker
+ - Closet
+ # - WallLocker # Frontier: restore when there are more than one lockable wall style
+ - WallCloset
diff --git a/Resources/Prototypes/Paintable/crate_groups.yml b/Resources/Prototypes/Paintable/crate_groups.yml
new file mode 100644
index 00000000000..c1e2d92ed5f
--- /dev/null
+++ b/Resources/Prototypes/Paintable/crate_groups.yml
@@ -0,0 +1,50 @@
+# Added in Upstream#37341
+
+- type: paintableGroup
+ id: CrateSteel
+ cost: 2
+ defaultStyle: basic
+ styles:
+ basic: CrateGenericSteel
+ electrical: CrateElectrical
+ engineering: CrateEngineering
+ radiation: CrateRadiation
+ science: CrateScience
+ surgery: CrateSurgery
+ ammo: CrateAmmoGeneric # Frontier
+
+- type: paintableGroup
+ id: CratePlastic
+ cost: 2
+ defaultStyle: basic
+ styles:
+ basic: CratePlastic
+ hydroponics: CrateHydroponics
+ medical: CrateMedical
+ oxygen: CrateInternals
+ biodegradable: CratePlasticBiodegradable # Frontier
+
+- type: paintableGroup
+ id: CrateSecure
+ cost: 2
+ defaultStyle: basic
+ styles:
+ basic: CrateSecure
+ chemistry: CrateChemistrySecure
+ command: CrateCommandSecure
+ engineering: CrateEngineeringSecure
+ hydroponics: CrateHydroSecure
+ medical: CrateMedicalSecure
+ plasma: CratePlasma
+ private: CratePrivateSecure
+ science: CrateScienceSecure
+ secgear: CrateSecgear
+ weapon: CrateWeaponSecure
+ mercenary: CrateSecureMercenary # Frontier
+ private-security: CrateSecureMercenaryPrivateSec # Frontier
+ mercenary-ammo: CrateAmmoSecureMercenary # Frontier
+ firearms: CrateFirearmsSecure # Frontier
+ uranium: CrateUranium # Frontier
+ brigmedic: CrateNfsdBrigmedic # Frontier
+ nfsd1: CrateNfsdSecure1 # Frontier
+ nfsd2: CrateNfsdSecure2 # Frontier
diff --git a/Resources/Prototypes/Paintable/locker_groups.yml b/Resources/Prototypes/Paintable/locker_groups.yml
new file mode 100644
index 00000000000..e0af91cb9f5
--- /dev/null
+++ b/Resources/Prototypes/Paintable/locker_groups.yml
@@ -0,0 +1,99 @@
+# Added in Upstream#37341
+
+- type: paintableGroup
+ id: Locker
+ cost: 2
+ defaultStyle: basic
+ styles:
+ atmospherics: LockerAtmospherics
+ basic: ClosetSteelBase
+ botanist: LockerBotanist
+ brigmedic: LockerBrigmedic
+ captain: LockerCaptain
+ ce: LockerChiefEngineer
+ chemical: LockerChemistry
+ clown: LockerClown
+ cmo: LockerChiefMedicalOfficer
+ doctor: LockerMedical
+ electrical: LockerElectricalSupplies
+ engineer: LockerEngineer
+ # evac: LockerEvacRepair # Frontier: not yet in, pending upstream merge
+ hop: LockerHeadOfPersonnel
+ hos: LockerHeadOfSecurity
+ mime: LockerMime
+ medicine: LockerMedicine
+ paramedic: LockerParamedic
+ quartermaster: LockerQuarterMaster
+ rd: LockerResearchDirector
+ representative: LockerRepresentative
+ salvage: LockerSalvageSpecialist
+ scientist: LockerScientist
+ security: LockerSecurity
+ welding: LockerWeldingSupplies
+ mail: LockerMailCarrier # Frontier
+ mercenary: LockerMercenary # Frontier
+ janitor: LockerJanitor # Frontier
+ pilot: LockerPilot # Frontier
+ nfsd: LockerNfsdEvidence # Frontier
+ nfsd-copper: LockerNfsdCopper # Frontier
+ nfsd-silver: LockerNfsdSilver # Frontier
+ nfsd-brigmedic: LockerNfsdBrigmedic # Frontier
+ nfsd-gold: LockerNfsdSergeant # Frontier
+ nfsd-sheriff: LockerNfsdSheriff # Frontier
+ station-representative: LockerStationRepresentative # Frontier
+ fsb: ClosetFsbEva # Frontier
+
+- type: paintableGroup
+ id: Closet
+ cost: 2
+ defaultStyle: basic
+ styles:
+ basic: ClosetSteelBase
+ biohazard: ClosetL3
+ biohazard-janitor: ClosetL3Janitor
+ biohazard-science: ClosetL3Virology
+ biohazard-security: ClosetL3Security
+ biohazard-virology: ClosetL3Virology
+ bomb: ClosetBomb
+ bomb-janitor: ClosetJanitorBomb
+ chef: ClosetChef
+ fire: ClosetFire
+ janitor: ClosetJanitor
+ legal: ClosetLegal
+ nitrogen: ClosetEmergencyN2
+ oxygen: ClosetEmergency
+ radiation: ClosetRadiationSuit
+ tool: ClosetTool
+ internals: ClosetO2N2 # Frontier
+
+- type: paintableGroup
+ id: WallCloset
+ cost: 2
+ defaultStyle: basic
+ styles:
+ atmospherics: ClosetWallAtmospherics
+ basic: ClosetWall
+ black: ClosetWallBlack
+ blue: ClosetWallBlue
+ fire: ClosetWallFire
+ green: ClosetWallGreen
+ grey: ClosetWallGrey
+ mixed: ClosetWallMixed
+ nitrogen: ClosetWallN2 # Frontier: EmergencyN2