From be0669e99461af2c9a2244e22f6c8b0c9053bc1b Mon Sep 17 00:00:00 2001 From: gordod3 Date: Tue, 28 Jul 2026 04:50:02 +0600 Subject: [PATCH] Implement Wizard Spray Painter --- .../GasCanisterAppearanceSystem.cs | 30 ++ Content.Client/Doors/DoorSystem.cs | 100 ++++-- .../SprayPainter/SprayPainterSystem.cs | 141 ++++++-- .../UI/SprayPainterBoundUserInterface.cs | 94 ++++- .../SprayPainter/UI/SprayPainterDecals.xaml | 41 +++ .../UI/SprayPainterDecals.xaml.cs | 217 +++++++++++ .../SprayPainter/UI/SprayPainterGroup.xaml | 12 + .../SprayPainter/UI/SprayPainterGroup.xaml.cs | 69 ++++ .../SprayPainter/UI/SprayPainterWindow.xaml | 36 +- .../UI/SprayPainterWindow.xaml.cs | 280 +++++++++++++-- .../EntityStorageVisualizerSystem.cs | 71 +++- .../SprayPainter/SprayPainterSystem.cs | 184 +++++++++- .../Doors/Components/DoorComponent.cs | 30 +- .../Components/PaintableAirlockComponent.cs | 45 +-- .../Components/PaintableComponent.cs | 22 ++ .../Components/PaintedComponent.cs | 21 ++ .../Components/SprayPainterComponent.cs | 115 +++++- .../Prototypes/AirlockDepartmentsPrototype.cs | 39 +- .../Prototypes/AirlockGroupPrototype.cs | 33 +- .../PaintableGroupCategoryPrototype.cs | 22 ++ .../Prototypes/PaintableGroupPrototype.cs | 56 +++ .../SprayPainter/SharedSprayPainterSystem.cs | 338 +++++++++++++----- .../SprayPainter/SprayPainterEvents.cs | 95 +++-- .../ru-RU/spray-painter/spray-painter.ftl | 194 ++++++++++ .../VendingMachines/Inventories/youtool.yml | 1 + .../Entities/Objects/Tools/spray_painter.yml | 62 +++- .../Structures/Doors/Airlocks/airlocks.yml | 48 +-- .../Doors/Airlocks/base_structureairlocks.yml | 18 +- .../Structures/Doors/Airlocks/clockwork.yml | 4 + .../Structures/Doors/Airlocks/external.yml | 14 +- .../Structures/Doors/Airlocks/shuttle.yml | 18 +- .../Structures/Doors/airlock_groups.yml | 160 +++++---- .../Storage/Canisters/gas_canisters.yml | 2 + .../Closets/Lockers/base_structurelockers.yml | 2 + .../Storage/Closets/Lockers/lockers.yml | 4 + .../Storage/Closets/base_structureclosets.yml | 4 + .../Storage/Closets/wall_lockers.yml | 26 ++ .../Storage/Crates/base_structurecrates.yml | 2 + .../Structures/Storage/Crates/crates.yml | 10 + .../Prototypes/Paintable/airlock_groups.yml | 44 +++ .../Prototypes/Paintable/canister_groups.yml | 21 ++ Resources/Prototypes/Paintable/categories.yml | 25 ++ .../Prototypes/Paintable/crate_groups.yml | 50 +++ .../Prototypes/Paintable/locker_groups.yml | 99 +++++ .../Recipes/Lathes/Packs/engineering.yml | 1 + Resources/Prototypes/Recipes/Lathes/tools.yml | 12 +- .../Doors/Airlocks/access_heavy.yml | 8 +- .../Doors/Airlocks/heavy_airlock.yml | 16 +- .../VendingMachines/Inventories/youtool.yml | 3 +- .../Structures/Doors/Airlocks/airlocks.yml | 10 +- .../Packs/Deprecated/engineering_techfab.yml | 2 + .../_Mono/Shuttles/Expedition/comet.yml | 4 +- .../SharedMaps/_Mono/Shuttles/shareholder.yml | 4 +- .../spray_painter.rsi/ammo-inhand-left.png | Bin 0 -> 274 bytes .../spray_painter.rsi/ammo-inhand-right.png | Bin 0 -> 305 bytes .../Objects/Tools/spray_painter.rsi/ammo.png | Bin 0 -> 947 bytes .../Tools/spray_painter.rsi/inhand-left.png | Bin 0 -> 462 bytes .../Tools/spray_painter.rsi/inhand-right.png | Bin 0 -> 480 bytes .../Objects/Tools/spray_painter.rsi/meta.json | 43 ++- 59 files changed, 2503 insertions(+), 499 deletions(-) create mode 100644 Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs create mode 100644 Content.Client/SprayPainter/UI/SprayPainterDecals.xaml create mode 100644 Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs create mode 100644 Content.Client/SprayPainter/UI/SprayPainterGroup.xaml create mode 100644 Content.Client/SprayPainter/UI/SprayPainterGroup.xaml.cs create mode 100644 Content.Shared/SprayPainter/Components/PaintableComponent.cs create mode 100644 Content.Shared/SprayPainter/Components/PaintedComponent.cs create mode 100644 Content.Shared/SprayPainter/Prototypes/PaintableGroupCategoryPrototype.cs create mode 100644 Content.Shared/SprayPainter/Prototypes/PaintableGroupPrototype.cs create mode 100644 Resources/Locale/ru-RU/spray-painter/spray-painter.ftl create mode 100644 Resources/Prototypes/Paintable/airlock_groups.yml create mode 100644 Resources/Prototypes/Paintable/canister_groups.yml create mode 100644 Resources/Prototypes/Paintable/categories.yml create mode 100644 Resources/Prototypes/Paintable/crate_groups.yml create mode 100644 Resources/Prototypes/Paintable/locker_groups.yml create mode 100644 Resources/Textures/Objects/Tools/spray_painter.rsi/ammo-inhand-left.png create mode 100644 Resources/Textures/Objects/Tools/spray_painter.rsi/ammo-inhand-right.png create mode 100644 Resources/Textures/Objects/Tools/spray_painter.rsi/ammo.png create mode 100644 Resources/Textures/Objects/Tools/spray_painter.rsi/inhand-left.png create mode 100644 Resources/Textures/Objects/Tools/spray_painter.rsi/inhand-right.png 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 8ac3b3780a9..7ccaf3ce528 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,6 +639,8 @@ 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: entity parent: CrateBaseSecure @@ -659,6 +667,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: EmergencyN27Y z$KLum<~7wYga*!9rLp*Y&CM0eSC{{vHdF0W&QFGQnVY&S-v>`{7l0TEBrnXqEgx|t zE61;1;l-nm;xT#0jgIZiSNq>zWBrFexqLysTK(=0#+DZ$il)TeKBF-4*|8^{s~`1D|ovdXU{&Ep!lkS-AZk$rFwh! z?xuhB($WuG-pl3v7GMOL1Ox%oDiR;MecYaW1;|)9^L1uhne+AuHo4PIEc#rs&T3Cb zN3dzYy(dK`1)KifGMO&=bkF9~3qRGYk~a9pEcv@JWp?P~oUl8avc8%+-@bg&VnVL` z`B!cmJ?7cX+qQaUXeslZlaKz*n|XB8GVe&JjSLLC7O(!#H)Z9mhvBbEKIEotX*j~& wt!H}dy@c+9Jofu(tIB@dTwhbm!0_SzI_?U~=)jxzKZBg=>FVdQ&MBb@01zRBY5)KL literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Tools/spray_painter.rsi/ammo.png b/Resources/Textures/Objects/Tools/spray_painter.rsi/ammo.png new file mode 100644 index 0000000000000000000000000000000000000000..ca64c2a7580d2cde77a5506c067ce836c3199ea6 GIT binary patch literal 947 zcmV;k15EshP)Px&ZAnByR9J=WmO)5ccO1t*d3pbrm;^MLB!_0o%d@Snktk}Ezz!?5P&TCVRM?@J zQmCiy&|9Im-g@aNg7Dg7>A|(KQ7~?6SrOw~K*`3+NEAy;`(zRe*7nh^Z--ddjIq2# z*)D!>@Av=ze*e$={{H{>e{i4!{m*hWnt1$^nEn3q_dl#OR7-9oX=GHh)9Elz*2@4K z3I_=UUG~g$scwk60p4GHpl=WWBcCIk4g=H;Q8$1MXqXS6N??MYVj{ipHh631)!sCn9q8qs^)*U^3YLjM=j6}kJ=|%(>yM( z0}u^{iTB;$+ixlce$jiKuOde=H!H_7(h{WECEL@bpKC(YoS+>UwNHFdS-fMg4}fAW zPc#^2ZsP`tKYruOu*R18JDnd70r2q01p<*)uCJG<1>wknf@RPh9H5xX0~H4fma#jY zvrGmf5gz@t3_u{#O3pH!ly z961nA!~pO}3LZ(RiUCAEIs|}_+z82C727j_B}?d;EE5+-*zgNB{DO%KBj}kdmMkF) z!MuKlGndYzxFr;~#Ff=gmz#Z*Jn$*lFe_Z|zSDyg)Lgp=YxAtXKG~SLm56 z$&`k7XUk44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O_~e2l#}zPPAf>lau?O#h|OJ%kW>(*4CCGkW*Jp&d0$xJIdYGn8C-#hl`7A zW<5ig2ZN-fBv79Xmm0`Y!IB`q;QwfVL3QoSB%mZ`fk$L90|Vay5N6D}*2xKU47;a` zV@QPi+i9nTniY6lJ3FG}r~dzcOD)UvMd^{3CAVD9F*Pc(7aT7A=Y^#>Evgxe2x!|0`(&_&D7UaqO;tBaFkR1G+TS_nW zu+|4hwOP{P2~k{ka~9|YXwR*2JoxpmgNfvhr3=%U*BoaG|8hR@$*s&IZ!K)9W9o}o zpC5a9LHq5C52rrYPw3|KQJ8UnrHON&>Wur$XY4E;4(iEdOlz-V=DJj{V5WRlEGw(0 z#f1<5J_W>cePevu`^NrkJZFi;g)@$gJ4JFHIxsggfvDzYCIOX)4u_48FJNG4FIZs7 j!m6;NowH8A<0I>|$=|kjuy36N3?~LpS3j3^P6k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O_~e2l#}z%E`%1v|?cRuc)i5`#+0;A&}G7)>cO>_%)r2R0E8K{u61$(9V6-K z;usR){&w2ibx&JHITK(OJ;^PF~S_e!oSII?DxoqYmi5Y|=~E^}wE! z!AHh)Lc1Fyk64VvzT*W?=2_m!zh*A)cI~n5(rLFHUb|Q3DR8_IDE@r(XZ^CU=EBD< zFLG}*bEN!t*z7A|UbthsCqp^^<~>=;GKp;U)8A~7ONcyK@KXECYo;wmZx{-r&}_%+Qs!;<8)Ly{iG@i0`Z)ydrADeU{CNW^S_xNck8O8FY57 zgQ-oQNNYpZyOVM|+B=+G6%(9G`Ip=JXx?~T=lDWV>j_inchl##E^@11?Mr&E*tg=s zo_E<9ZdDyWW-LtJysLi6J!hH8u}irB#Wnn3iCg$>{Ry$V9Keuc@O1TaS?83{1OQop BzwiJ6 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Tools/spray_painter.rsi/meta.json b/Resources/Textures/Objects/Tools/spray_painter.rsi/meta.json index 056ba0a8563..14af8406380 100644 --- a/Resources/Textures/Objects/Tools/spray_painter.rsi/meta.json +++ b/Resources/Textures/Objects/Tools/spray_painter.rsi/meta.json @@ -1,14 +1,33 @@ { - "copyright" : "Taken from https://github.com/tgstation/tgstation at commit a21274e56ae84b2c96e8b6beeca805df3d5402e8.", - "license" : "CC-BY-SA-3.0", - "size" : { - "x" : 32, - "y" : 32 - }, - "states" : [ - { - "name" : "spray_painter" - } - ], - "version" : 1 + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/tgstation/tgstation at commit a21274e56ae84b2c96e8b6beeca805df3d5402e8, Inhand sprites by onesch, ammo by Paradoxmi (Discord).", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "spray_painter" + }, + { + "name": "ammo" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + }, + { + "name": "ammo-inhand-left", + "directions": 4 + }, + { + "name": "ammo-inhand-right", + "directions": 4 + } + ] }